feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const isMacOS = /(Macintosh|MacIntel|MacPPC|Mac68K|iPad)/.test(
|
||||
navigator.userAgent,
|
||||
);
|
||||
|
||||
const CTRL = isMacOS ? '⌘' : 'Ctrl';
|
||||
const SHIFT = isMacOS ? '⇧' : 'Shift';
|
||||
|
||||
export const ENCAPSULATE_SHORTCUTS = {
|
||||
encapsulate: `${CTRL} G`,
|
||||
decapsulate: `${CTRL} ${SHIFT} G`,
|
||||
};
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type MouseEvent, useRef, type MouseEventHandler } from 'react';
|
||||
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Button } from '@coze-arch/coze-design';
|
||||
import { useService } from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
import { useValidate } from '../hooks/use-validate';
|
||||
import { useEncapsulate } from '../hooks/use-encapsulate';
|
||||
import { EncapsulateTooltip } from '../encapsulate-tooltip';
|
||||
import { EncapsulateRenderService } from '../encapsulate-render-service';
|
||||
import { ENCAPSULATE_SHORTCUTS } from '../constants';
|
||||
|
||||
import styles from './styles.module.less';
|
||||
|
||||
const HOVER_DELAY = 200;
|
||||
|
||||
/**
|
||||
* 封装按钮
|
||||
*/
|
||||
export function EncapsulateButton() {
|
||||
const encapsulateRenderService = useService<EncapsulateRenderService>(
|
||||
EncapsulateRenderService,
|
||||
);
|
||||
const { handleEncapsulate, loading } = useEncapsulate();
|
||||
|
||||
const { validating, errors } = useValidate();
|
||||
|
||||
const handleClick = (e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
if (validating) {
|
||||
return;
|
||||
}
|
||||
handleEncapsulate();
|
||||
};
|
||||
|
||||
const hasError = errors && errors.length > 0;
|
||||
const disabled = !!(!errors || hasError);
|
||||
const timeOutRef = useRef<number>();
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleMouseLeave: MouseEventHandler<HTMLDivElement> = () => {
|
||||
if (!timeOutRef.current) {
|
||||
timeOutRef.current = window.setTimeout(() => {
|
||||
encapsulateRenderService.hideTooltip();
|
||||
timeOutRef.current = undefined;
|
||||
}, HOVER_DELAY);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseEnter: MouseEventHandler<HTMLDivElement> = () => {
|
||||
if (timeOutRef.current) {
|
||||
clearTimeout(timeOutRef.current);
|
||||
timeOutRef.current = undefined;
|
||||
}
|
||||
encapsulateRenderService.showTooltip();
|
||||
};
|
||||
|
||||
return (
|
||||
<EncapsulateTooltip
|
||||
errors={errors}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<div
|
||||
className="pointer-events-auto"
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
ref={ref}
|
||||
>
|
||||
<Button
|
||||
loading={loading}
|
||||
disabled={disabled}
|
||||
className={styles.button}
|
||||
color="highlight"
|
||||
onMouseDown={handleClick}
|
||||
>
|
||||
<span>
|
||||
{I18n.t('workflow_encapsulate_button', undefined, '封装工作流')}
|
||||
</span>
|
||||
<span className={styles.shortcut}>
|
||||
{ENCAPSULATE_SHORTCUTS.encapsulate}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
</EncapsulateTooltip>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
.button {
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.shortcut {
|
||||
// color: rgba(255, 255, 255, 0.6);
|
||||
margin-left: 4px;
|
||||
font-family: 'PICO Sans VFE SC, PingFang SC,Noto Sans SC,sans-serif';
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
|
||||
|
||||
@keyframes encapsulate-panel-show {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.encapsulate-panel {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
top: 16px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.35s ease;
|
||||
display: flex;
|
||||
opacity: 0;
|
||||
|
||||
&-show {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.encapsulate-panel-content {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 40px;
|
||||
padding: 0 4px 0 8px;
|
||||
background-color: var(--coz-bg-max);
|
||||
border: 1px solid var(--coz-stroke-plus);
|
||||
border-radius: 10px;
|
||||
color: var(--coz-fg-primary);
|
||||
font-weight: 500;
|
||||
box-shadow: var(--coz-shadow-small);
|
||||
gap: 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* 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 { useEffect, useState, type FC } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { useService } from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
import { useSelectedNodes } from '../hooks/use-selected-nodes';
|
||||
import { EncapsulateRenderService } from '../encapsulate-render-service';
|
||||
import { EncapsulateButton } from '../encapsulate-button';
|
||||
import { EncapsulateService } from '../../encapsulate';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export const EncapsulatePanel: FC = () => {
|
||||
const { selectedNodes } = useSelectedNodes();
|
||||
const { length } = selectedNodes || [];
|
||||
const [show, setShow] = useState(false);
|
||||
|
||||
const encapsulateService = useService<EncapsulateService>(EncapsulateService);
|
||||
const encapsulateRenderService = useService<EncapsulateRenderService>(
|
||||
EncapsulateRenderService,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const display = encapsulateService.canEncapsulate() && length > 1;
|
||||
if (!display) {
|
||||
encapsulateRenderService.hideTooltip();
|
||||
}
|
||||
setShow(display);
|
||||
}, [length]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(styles['encapsulate-panel'], {
|
||||
[styles['encapsulate-panel-show']]: show,
|
||||
})}
|
||||
>
|
||||
<div className={styles['encapsulate-panel-content']}>
|
||||
{I18n.t(
|
||||
'workflow_encapsulate_selecet',
|
||||
{ length },
|
||||
`已选中 ${length} 个节点`,
|
||||
)}{' '}
|
||||
<EncapsulateButton />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* 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 { ContainerModule } from 'inversify';
|
||||
import { bindContributions } from '@flowgram-adapter/free-layout-editor';
|
||||
import { WorkflowShortcutsContribution } from '@coze-workflow/render';
|
||||
|
||||
import { EncapsulateShortcutsContribution } from './encapsulate-shortcuts-contribution';
|
||||
import { EncapsulateRenderService } from './encapsulate-render-service';
|
||||
|
||||
export const EncapsulateRenderContainerModule = new ContainerModule(bind => {
|
||||
bindContributions(bind, EncapsulateShortcutsContribution, [
|
||||
WorkflowShortcutsContribution,
|
||||
]);
|
||||
bind(EncapsulateRenderService).toSelf().inSingletonScope();
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* 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 { injectable } from 'inversify';
|
||||
import { Emitter } from '@flowgram-adapter/common';
|
||||
|
||||
@injectable()
|
||||
export class EncapsulateRenderService {
|
||||
private isModalVisible = false;
|
||||
private onModalVisibleChangeEmitter = new Emitter<boolean>();
|
||||
readonly onModalVisibleChange = this.onModalVisibleChangeEmitter.event;
|
||||
|
||||
private isTooltipVisible = false;
|
||||
private onTooltipVisibleChangeEmitter = new Emitter<boolean>();
|
||||
readonly onTooltipVisibleChange = this.onTooltipVisibleChangeEmitter.event;
|
||||
|
||||
private isLoading = false;
|
||||
private onLoadingChangeEmitter = new Emitter<boolean>();
|
||||
readonly onLoadingChange = this.onLoadingChangeEmitter.event;
|
||||
|
||||
get modalVisible() {
|
||||
return this.isModalVisible;
|
||||
}
|
||||
|
||||
get tooltipVisible() {
|
||||
return this.isTooltipVisible;
|
||||
}
|
||||
|
||||
get loading() {
|
||||
return this.isLoading;
|
||||
}
|
||||
|
||||
setLoading(value: boolean) {
|
||||
this.isLoading = value;
|
||||
this.onLoadingChangeEmitter.fire(value);
|
||||
}
|
||||
|
||||
openModal() {
|
||||
this.setModalVisible(true);
|
||||
}
|
||||
|
||||
closeModal() {
|
||||
this.setModalVisible(false);
|
||||
}
|
||||
|
||||
showTooltip() {
|
||||
this.setTooltipVisible(true);
|
||||
}
|
||||
|
||||
hideTooltip() {
|
||||
this.setTooltipVisible(false);
|
||||
}
|
||||
|
||||
setTooltipVisible(value: boolean) {
|
||||
this.isTooltipVisible = value;
|
||||
this.onTooltipVisibleChangeEmitter.fire(value);
|
||||
}
|
||||
|
||||
private setModalVisible(value: boolean) {
|
||||
this.isModalVisible = value;
|
||||
this.onModalVisibleChangeEmitter.fire(value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* 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 { inject, injectable } from 'inversify';
|
||||
import { PlaygroundConfigEntity } from '@flowgram-adapter/free-layout-editor';
|
||||
import {
|
||||
type WorkflowNodeEntity,
|
||||
WorkflowSelectService,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
import {
|
||||
type WorkflowShortcutsContribution,
|
||||
type WorkflowShortcutsRegistry,
|
||||
} from '@coze-workflow/render';
|
||||
|
||||
import { EncapsulateService } from '../encapsulate';
|
||||
import { EncapsulateCommands } from './types';
|
||||
import { EncapsulateRenderService } from './encapsulate-render-service';
|
||||
|
||||
/**
|
||||
* 封装 快捷键
|
||||
*/
|
||||
@injectable()
|
||||
export class EncapsulateShortcutsContribution
|
||||
implements WorkflowShortcutsContribution
|
||||
{
|
||||
@inject(PlaygroundConfigEntity)
|
||||
private playgroundConfigEntity: PlaygroundConfigEntity;
|
||||
|
||||
@inject(EncapsulateService)
|
||||
private encapsulateService: EncapsulateService;
|
||||
|
||||
@inject(EncapsulateRenderService)
|
||||
private encapsulateRenderService: EncapsulateRenderService;
|
||||
|
||||
@inject(WorkflowSelectService)
|
||||
private workflowSelectService: WorkflowSelectService;
|
||||
|
||||
registerShortcuts(registry: WorkflowShortcutsRegistry): void {
|
||||
registry.addHandlers(
|
||||
/**
|
||||
* 封装
|
||||
*/
|
||||
{
|
||||
commandId: EncapsulateCommands.ENCAPSULATE,
|
||||
shortcuts: ['meta g', 'ctrl g'],
|
||||
|
||||
isEnabled: () => !this.playgroundConfigEntity.readonly,
|
||||
|
||||
execute: async () => {
|
||||
if (!this.encapsulateService.canEncapsulate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await this.encapsulateService.validate();
|
||||
|
||||
if (res.hasError()) {
|
||||
this.encapsulateRenderService.showTooltip();
|
||||
return;
|
||||
}
|
||||
|
||||
this.encapsulateRenderService.setLoading(true);
|
||||
try {
|
||||
await this.encapsulateService.encapsulate();
|
||||
this.encapsulateRenderService.closeModal();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
this.encapsulateRenderService.setLoading(false);
|
||||
},
|
||||
},
|
||||
/**
|
||||
* 解封
|
||||
*/
|
||||
{
|
||||
commandId: EncapsulateCommands.DECAPSULATE,
|
||||
shortcuts: ['meta shift g', 'ctrl shift g'],
|
||||
isEnabled: () => !this.playgroundConfigEntity.readonly,
|
||||
execute: () => {
|
||||
const { selectedNodes } = this.workflowSelectService;
|
||||
if (selectedNodes.length !== 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const node = selectedNodes[0] as WorkflowNodeEntity;
|
||||
if (!this.encapsulateService.canDecapsulate(node)) {
|
||||
return;
|
||||
}
|
||||
this.encapsulateService.decapsulate(node);
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* 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 } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { Typography } from '@coze-arch/coze-design';
|
||||
import { type FlowNodeEntity } from '@flowgram-adapter/free-layout-editor';
|
||||
import {
|
||||
usePlayground,
|
||||
useService,
|
||||
WorkflowSelectService,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
import { type EncapsulateValidateError } from '../../validate';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
interface Props {
|
||||
error: EncapsulateValidateError;
|
||||
}
|
||||
|
||||
export const ErrorTitle: FC<Props> = ({ error }) => {
|
||||
const selectServices = useService<WorkflowSelectService>(
|
||||
WorkflowSelectService,
|
||||
);
|
||||
|
||||
const playground = usePlayground();
|
||||
|
||||
if (!error?.sourceName && !error.sourceIcon) {
|
||||
return <div></div>;
|
||||
}
|
||||
|
||||
const scrollToNode = async (nodeId: string) => {
|
||||
let success = false;
|
||||
const node = playground.entityManager.getEntityById<FlowNodeEntity>(nodeId);
|
||||
|
||||
if (node) {
|
||||
await selectServices.selectNodeAndScrollToView(node, true);
|
||||
success = true;
|
||||
}
|
||||
return success;
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex items-center gap-1 cursor-pointer max-w-[120px]"
|
||||
onClick={() => {
|
||||
if (error.source) {
|
||||
scrollToNode(error.source);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{error.sourceIcon ? (
|
||||
<img
|
||||
width={18}
|
||||
height={18}
|
||||
src={error.sourceIcon}
|
||||
className="w-4.5 h-4.5 rounded-[4px]"
|
||||
/>
|
||||
) : null}
|
||||
{error.sourceName ? (
|
||||
<Typography.Paragraph
|
||||
className={classNames(
|
||||
'font-medium coz-fg-primary',
|
||||
styles['error-name'],
|
||||
)}
|
||||
ellipsis={{
|
||||
rows: 1,
|
||||
showTooltip: {
|
||||
type: 'tooltip',
|
||||
opts: {
|
||||
style: {
|
||||
width: '100%',
|
||||
wordBreak: 'break-word',
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{error.sourceName}
|
||||
</Typography.Paragraph>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
.error-name {
|
||||
span {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
max-width: 460px;
|
||||
max-height: 260px;
|
||||
overflow-y: auto;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.errors {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, max-content) 1fr;
|
||||
grid-gap: 16px;
|
||||
grid-row-gap: 12px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* 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 {
|
||||
useEffect,
|
||||
useState,
|
||||
type PropsWithChildren,
|
||||
type FC,
|
||||
type MouseEventHandler,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
|
||||
import { groupBy } from 'lodash-es';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Tooltip } from '@coze-arch/coze-design';
|
||||
import { useService } from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
import { EncapsulateRenderService } from '../encapsulate-render-service';
|
||||
import { type EncapsulateValidateError } from '../../validate';
|
||||
import { ErrorTitle } from './error-title';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
interface Props {
|
||||
errors: EncapsulateValidateError[];
|
||||
onMouseEnter?: MouseEventHandler<HTMLDivElement>;
|
||||
onMouseLeave?: MouseEventHandler<HTMLDivElement>;
|
||||
getPopupContainer?: () => HTMLElement;
|
||||
}
|
||||
|
||||
const ErrorMessage: FC<{
|
||||
error: EncapsulateValidateError;
|
||||
}> = ({ error }) => (
|
||||
<div className="flex-1 coz-fg-primary font-normal">{error.message}</div>
|
||||
);
|
||||
|
||||
export const EncapsulateTooltip: FC<PropsWithChildren<Props>> = ({
|
||||
errors = [],
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
children,
|
||||
}) => {
|
||||
const encapsulateRenderService = useService<EncapsulateRenderService>(
|
||||
EncapsulateRenderService,
|
||||
);
|
||||
|
||||
const [tooltipVisible, setTooltipVisible] = useState(
|
||||
encapsulateRenderService.tooltipVisible,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const disposable =
|
||||
encapsulateRenderService.onTooltipVisibleChange(setTooltipVisible);
|
||||
|
||||
return () => {
|
||||
disposable.dispose();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const hasError = errors.length;
|
||||
|
||||
const groupErrors = useMemo(
|
||||
() =>
|
||||
groupBy(
|
||||
errors.filter(e => e.message),
|
||||
error =>
|
||||
error?.sourceName || error?.sourceIcon
|
||||
? 'withSource'
|
||||
: 'withoutSource',
|
||||
),
|
||||
[errors],
|
||||
);
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
trigger="custom"
|
||||
position="bottom"
|
||||
visible={tooltipVisible && errors?.length > 0}
|
||||
showArrow={false}
|
||||
onClickOutSide={() => {
|
||||
setTooltipVisible(false);
|
||||
}}
|
||||
className="p-0 max-w-[460px] overflow-hidden"
|
||||
content={
|
||||
hasError ? (
|
||||
<div
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
className={styles.tooltip}
|
||||
>
|
||||
<div className="coz-fg-plus font-medium text-[16px]">
|
||||
{I18n.t(
|
||||
'workflow_encapsulate_button_unable',
|
||||
undefined,
|
||||
'无法封装工作流',
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 没有错误来源的 */}
|
||||
{(groupErrors.withoutSource || []).map((error, index) => (
|
||||
<div key={index} className="flex mt-3 gap-4 items-start">
|
||||
<ErrorMessage error={error} />
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* 有错误来源的 */}
|
||||
{(groupErrors.withSource || []).length ? (
|
||||
<div className={styles.errors}>
|
||||
{(groupErrors.withSource || []).map(error => (
|
||||
<>
|
||||
<ErrorTitle error={error} />
|
||||
<ErrorMessage error={error} />
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* 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 { useEffect, useState } from 'react';
|
||||
|
||||
import { useService } from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
import { EncapsulateRenderService } from '../encapsulate-render-service';
|
||||
import { EncapsulateService } from '../../encapsulate';
|
||||
|
||||
export const useEncapsulate = () => {
|
||||
const encapsulateService = useService<EncapsulateService>(EncapsulateService);
|
||||
const encapsulateRenderService = useService<EncapsulateRenderService>(
|
||||
EncapsulateRenderService,
|
||||
);
|
||||
const [loading, setLoading] = useState(encapsulateRenderService.loading);
|
||||
|
||||
useEffect(() => {
|
||||
const disposable = encapsulateRenderService.onLoadingChange(setLoading);
|
||||
return () => {
|
||||
disposable.dispose();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleEncapsulate = async () => {
|
||||
encapsulateRenderService.setLoading(true);
|
||||
try {
|
||||
await encapsulateService.encapsulate();
|
||||
encapsulateRenderService.closeModal();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
encapsulateRenderService.setLoading(false);
|
||||
};
|
||||
|
||||
return {
|
||||
handleEncapsulate,
|
||||
loading,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useService } from '@flowgram-adapter/free-layout-editor';
|
||||
import { WorkflowSelectService } from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
/**
|
||||
* 选中节点
|
||||
*/
|
||||
export function useSelectedNodes() {
|
||||
const selectService = useService<WorkflowSelectService>(
|
||||
WorkflowSelectService,
|
||||
);
|
||||
|
||||
const [selectedNodes, setSelectedNodes] = useState(
|
||||
selectService.selectedNodes,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const disposable = selectService.onSelectionChanged(() => {
|
||||
setSelectedNodes(selectService.selectedNodes);
|
||||
});
|
||||
|
||||
return () => {
|
||||
disposable.dispose();
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
selectedNodes,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useState, useRef } from 'react';
|
||||
|
||||
import { useDebounceEffect } from 'ahooks';
|
||||
import { useService } from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
import {
|
||||
EncapsulateValidateErrorCode,
|
||||
type EncapsulateValidateError,
|
||||
} from '../../validate';
|
||||
import { EncapsulateService } from '../../encapsulate';
|
||||
import { useVariableChange } from './use-variable-change';
|
||||
import { useSelectedNodes } from './use-selected-nodes';
|
||||
|
||||
const DEBOUNCE_DELAY = 100;
|
||||
|
||||
/**
|
||||
* 校验
|
||||
*/
|
||||
export function useValidate() {
|
||||
const { selectedNodes } = useSelectedNodes();
|
||||
const encapsulateService = useService<EncapsulateService>(EncapsulateService);
|
||||
|
||||
const [validating, setValidating] = useState(false);
|
||||
const [errors, setErrors] = useState<EncapsulateValidateError[]>([]);
|
||||
const validationIdRef = useRef(0); // 新增校验ID跟踪
|
||||
|
||||
const handleValidate = async () => {
|
||||
if (selectedNodes.length <= 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
setValidating(true);
|
||||
// 生成当前校验ID
|
||||
const currentValidationId = ++validationIdRef.current;
|
||||
|
||||
try {
|
||||
const validateResult = await encapsulateService.validate();
|
||||
|
||||
// 只处理最后一次校验结果
|
||||
if (currentValidationId === validationIdRef.current) {
|
||||
setErrors(validateResult.getErrors());
|
||||
setValidating(false);
|
||||
}
|
||||
} catch (error) {
|
||||
setErrors([
|
||||
{
|
||||
code: EncapsulateValidateErrorCode.VALIDATE_ERROR,
|
||||
message: (error as Error).message,
|
||||
},
|
||||
]);
|
||||
setValidating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const { version: variableVersion } = useVariableChange(selectedNodes);
|
||||
|
||||
useDebounceEffect(
|
||||
() => {
|
||||
handleValidate();
|
||||
},
|
||||
[selectedNodes, variableVersion],
|
||||
{
|
||||
wait: DEBOUNCE_DELAY,
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
validating,
|
||||
errors,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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 { useEffect, useState } from 'react';
|
||||
|
||||
import { FlowNodeVariableData } from '@coze-workflow/variable';
|
||||
|
||||
export const useVariableChange = nodes => {
|
||||
const [version, setVersion] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const disposables = nodes
|
||||
.filter(node => node.getData(FlowNodeVariableData)?.public?.available)
|
||||
.map(node =>
|
||||
node.getData(FlowNodeVariableData).public.available.onDataChange(() => {
|
||||
setVersion(version + 1);
|
||||
}),
|
||||
);
|
||||
return () => {
|
||||
disposables.forEach(disposable => disposable?.dispose());
|
||||
};
|
||||
}, [nodes, version]);
|
||||
|
||||
return {
|
||||
version,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './types';
|
||||
export * from './encapsulate-render-container-module';
|
||||
export { EncapsulatePanel } from './encapsulate-panel';
|
||||
export * from './constants';
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 封装/解封命令
|
||||
*/
|
||||
export enum EncapsulateCommands {
|
||||
/**
|
||||
* 封装
|
||||
*/
|
||||
ENCAPSULATE = 'ENCAPSULATE',
|
||||
/**
|
||||
* 解封
|
||||
*/
|
||||
DECAPSULATE = 'DECAPSULATE',
|
||||
}
|
||||
Reference in New Issue
Block a user