mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
feat(editor): Support partial executions of tool nodes (#14945)
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import FromAiParametersModal from '@/components/FromAiParametersModal.vue';
|
||||
import { FROM_AI_PARAMETERS_MODAL_KEY, STORES } from '@/constants';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { useParameterOverridesStore } from '@/stores/parameterOverrides.store';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { NodeConnectionTypes } from 'n8n-workflow';
|
||||
|
||||
const ModalStub = {
|
||||
template: `
|
||||
<div>
|
||||
<slot name="header" />
|
||||
<slot name="title" />
|
||||
<slot name="content" />
|
||||
<slot name="footer" />
|
||||
</div>
|
||||
`,
|
||||
};
|
||||
|
||||
vi.mock('vue-router');
|
||||
|
||||
vi.mocked(useRouter);
|
||||
|
||||
const mockNode = {
|
||||
id: 'id1',
|
||||
name: 'Test Node',
|
||||
parameters: {
|
||||
testBoolean: "={{ $fromAI('testBoolean', ``, 'boolean') }}",
|
||||
testParam: "={{ $fromAi('testParam', ``, 'string') }}",
|
||||
},
|
||||
};
|
||||
|
||||
const mockParentNode = {
|
||||
name: 'Parent Node',
|
||||
};
|
||||
|
||||
const mockRunData = {
|
||||
data: {
|
||||
resultData: {
|
||||
runData: {
|
||||
['Test Node']: [
|
||||
{
|
||||
inputOverride: {
|
||||
[NodeConnectionTypes.AiTool]: [[{ json: { testParam: 'override' } }]],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const mockWorkflow = {
|
||||
id: 'test-workflow',
|
||||
getChildNodes: () => ['Parent Node'],
|
||||
};
|
||||
|
||||
const renderModal = createComponentRenderer(FromAiParametersModal);
|
||||
let pinia: ReturnType<typeof createTestingPinia>;
|
||||
let workflowsStore: ReturnType<typeof useWorkflowsStore>;
|
||||
let parameterOverridesStore: ReturnType<typeof useParameterOverridesStore>;
|
||||
describe('FromAiParametersModal', () => {
|
||||
beforeEach(() => {
|
||||
pinia = createTestingPinia({
|
||||
initialState: {
|
||||
[STORES.UI]: {
|
||||
modalsById: {
|
||||
[FROM_AI_PARAMETERS_MODAL_KEY]: {
|
||||
open: true,
|
||||
data: {
|
||||
nodeName: 'Test Node',
|
||||
},
|
||||
},
|
||||
},
|
||||
modalStack: [FROM_AI_PARAMETERS_MODAL_KEY],
|
||||
},
|
||||
[STORES.WORKFLOWS]: {
|
||||
workflow: mockWorkflow,
|
||||
workflowExecutionData: mockRunData,
|
||||
},
|
||||
},
|
||||
});
|
||||
workflowsStore = useWorkflowsStore();
|
||||
workflowsStore.getNodeByName = vi
|
||||
.fn()
|
||||
.mockImplementation((name: string) => (name === 'Test Node' ? mockNode : mockParentNode));
|
||||
workflowsStore.getCurrentWorkflow = vi.fn().mockReturnValue(mockWorkflow);
|
||||
parameterOverridesStore = useParameterOverridesStore();
|
||||
parameterOverridesStore.clearParameterOverrides = vi.fn();
|
||||
parameterOverridesStore.addParameterOverrides = vi.fn();
|
||||
parameterOverridesStore.substituteParameters = vi.fn();
|
||||
});
|
||||
|
||||
it('renders correctly with node data', () => {
|
||||
const { getByTitle } = renderModal({
|
||||
props: {
|
||||
modalName: FROM_AI_PARAMETERS_MODAL_KEY,
|
||||
data: {
|
||||
nodeName: 'Test Node',
|
||||
},
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
Modal: ModalStub,
|
||||
},
|
||||
},
|
||||
pinia,
|
||||
});
|
||||
|
||||
expect(getByTitle('Test Test Node')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('uses run data when available as initial values', async () => {
|
||||
const { getByTestId } = renderModal({
|
||||
props: {
|
||||
modalName: FROM_AI_PARAMETERS_MODAL_KEY,
|
||||
data: {
|
||||
nodeName: 'Test Node',
|
||||
},
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
Modal: ModalStub,
|
||||
},
|
||||
},
|
||||
pinia,
|
||||
});
|
||||
|
||||
await userEvent.click(getByTestId('execute-workflow-button'));
|
||||
|
||||
expect(parameterOverridesStore.addParameterOverrides).toHaveBeenCalledWith(
|
||||
'test-workflow',
|
||||
'id1',
|
||||
{
|
||||
testBoolean: true,
|
||||
testParam: 'override',
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('clears parameter overrides when modal is executed', async () => {
|
||||
const { getByTestId } = renderModal({
|
||||
props: {
|
||||
modalName: FROM_AI_PARAMETERS_MODAL_KEY,
|
||||
data: {
|
||||
nodeName: 'Test Node',
|
||||
},
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
Modal: ModalStub,
|
||||
},
|
||||
},
|
||||
pinia,
|
||||
});
|
||||
|
||||
await userEvent.click(getByTestId('execute-workflow-button'));
|
||||
|
||||
expect(parameterOverridesStore.clearParameterOverrides).toHaveBeenCalledWith(
|
||||
'test-workflow',
|
||||
'id1',
|
||||
);
|
||||
});
|
||||
|
||||
it('adds substitutes for parameters when executed', async () => {
|
||||
const { getByTestId } = renderModal({
|
||||
props: {
|
||||
modalName: FROM_AI_PARAMETERS_MODAL_KEY,
|
||||
data: {
|
||||
nodeName: 'Test Node',
|
||||
},
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
Modal: ModalStub,
|
||||
},
|
||||
},
|
||||
pinia,
|
||||
});
|
||||
|
||||
const inputs = getByTestId('from-ai-parameters-modal-inputs');
|
||||
await userEvent.click(inputs.querySelector('input[value="testBoolean"]') as Element);
|
||||
await userEvent.clear(inputs.querySelector('input[name="testParam"]') as Element);
|
||||
await userEvent.type(inputs.querySelector('input[name="testParam"]') as Element, 'given value');
|
||||
await userEvent.click(getByTestId('execute-workflow-button'));
|
||||
|
||||
expect(parameterOverridesStore.addParameterOverrides).toHaveBeenCalledWith(
|
||||
'test-workflow',
|
||||
'id1',
|
||||
{
|
||||
testBoolean: false,
|
||||
testParam: 'given value',
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,203 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import { useRunWorkflow } from '@/composables/useRunWorkflow';
|
||||
import { FROM_AI_PARAMETERS_MODAL_KEY } from '@/constants';
|
||||
import { useParameterOverridesStore } from '@/stores/parameterOverrides.store';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { createEventBus } from '@n8n/utils/event-bus';
|
||||
import {
|
||||
type FromAIArgument,
|
||||
NodeConnectionTypes,
|
||||
traverseNodeParametersWithParamNames,
|
||||
} from 'n8n-workflow';
|
||||
import { computed, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { type IFormInput } from '@n8n/design-system';
|
||||
import { useTelemetry } from '@/composables/useTelemetry';
|
||||
import { useNDVStore } from '@/stores/ndv.store';
|
||||
|
||||
type Value = string | number | boolean | null | undefined;
|
||||
|
||||
const props = defineProps<{
|
||||
modalName: string;
|
||||
data: {
|
||||
nodeName: string | undefined;
|
||||
};
|
||||
}>();
|
||||
|
||||
const inputs = ref<{ getValues: () => Record<string, Value> }>();
|
||||
const i18n = useI18n();
|
||||
const telemetry = useTelemetry();
|
||||
const ndvStore = useNDVStore();
|
||||
const modalBus = createEventBus();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const router = useRouter();
|
||||
const { runWorkflow } = useRunWorkflow({ router });
|
||||
const parameterOverridesStore = useParameterOverridesStore();
|
||||
|
||||
const node = computed(() =>
|
||||
props.data.nodeName ? workflowsStore.getNodeByName(props.data.nodeName) : undefined,
|
||||
);
|
||||
|
||||
const parentNode = computed(() => {
|
||||
if (!node.value) return undefined;
|
||||
const workflow = workflowsStore.getCurrentWorkflow();
|
||||
const parentNodes = workflow.getChildNodes(node.value.name, 'ALL', 1);
|
||||
if (parentNodes.length === 0) return undefined;
|
||||
return workflowsStore.getNodeByName(parentNodes[0])?.name;
|
||||
});
|
||||
|
||||
const nodeRunData = computed(() => {
|
||||
if (!node.value) return undefined;
|
||||
|
||||
const workflowExecutionData = workflowsStore.getWorkflowExecution;
|
||||
const lastRunData = workflowExecutionData?.data?.resultData.runData[node.value?.name];
|
||||
if (!lastRunData) return undefined;
|
||||
return lastRunData[0];
|
||||
});
|
||||
|
||||
const mapTypes: {
|
||||
[key: string]: {
|
||||
inputType: 'text' | 'number' | 'checkbox';
|
||||
defaultValue: string | number | boolean | null | undefined;
|
||||
};
|
||||
} = {
|
||||
['string']: {
|
||||
inputType: 'text',
|
||||
defaultValue: '',
|
||||
},
|
||||
['boolean']: {
|
||||
inputType: 'checkbox',
|
||||
defaultValue: true,
|
||||
},
|
||||
['number']: {
|
||||
inputType: 'number',
|
||||
defaultValue: 0,
|
||||
},
|
||||
['json']: {
|
||||
inputType: 'text',
|
||||
defaultValue: '',
|
||||
},
|
||||
};
|
||||
|
||||
const parameters = computed(() => {
|
||||
if (!node.value) return [];
|
||||
|
||||
const result: IFormInput[] = [];
|
||||
const params = node.value.parameters;
|
||||
const collectedArgs: Map<string, FromAIArgument> = new Map();
|
||||
traverseNodeParametersWithParamNames(params, collectedArgs);
|
||||
const inputOverrides =
|
||||
nodeRunData.value?.inputOverride?.[NodeConnectionTypes.AiTool]?.[0]?.[0].json;
|
||||
|
||||
collectedArgs.forEach((value: FromAIArgument, paramName: string) => {
|
||||
const type = value.type ?? 'string';
|
||||
const initialValue = inputOverrides?.[value.key]
|
||||
? inputOverrides[value.key]
|
||||
: (parameterOverridesStore.getParameterOverride(
|
||||
workflowsStore.workflowId,
|
||||
node.value!.id,
|
||||
paramName,
|
||||
) ?? mapTypes[type]?.defaultValue);
|
||||
|
||||
result.push({
|
||||
name: paramName,
|
||||
initialValue: initialValue as string | number | boolean | null | undefined,
|
||||
properties: {
|
||||
label: value.key,
|
||||
type: mapTypes[type].inputType,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
return result;
|
||||
});
|
||||
|
||||
const onClose = () => {
|
||||
modalBus.emit('close');
|
||||
};
|
||||
|
||||
const onExecute = async () => {
|
||||
if (!node.value) return;
|
||||
const inputValues = inputs.value!.getValues();
|
||||
|
||||
parameterOverridesStore.clearParameterOverrides(workflowsStore.workflowId, node.value.id);
|
||||
parameterOverridesStore.addParameterOverrides(
|
||||
workflowsStore.workflowId,
|
||||
node.value.id,
|
||||
inputValues,
|
||||
);
|
||||
|
||||
const telemetryPayload = {
|
||||
node_type: node.value.type,
|
||||
workflow_id: workflowsStore.workflowId,
|
||||
source: 'from-ai-parameters-modal',
|
||||
push_ref: ndvStore.pushRef,
|
||||
};
|
||||
|
||||
telemetry.track('User clicked execute node button in modal', telemetryPayload);
|
||||
|
||||
await runWorkflow({
|
||||
destinationNode: node.value.name,
|
||||
source: 'RunData.TestExecuteModal',
|
||||
});
|
||||
|
||||
onClose();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal
|
||||
max-width="540px"
|
||||
:title="
|
||||
i18n.baseText('fromAiParametersModal.title', { interpolate: { nodeName: node?.name || '' } })
|
||||
"
|
||||
:event-bus="modalBus"
|
||||
:name="FROM_AI_PARAMETERS_MODAL_KEY"
|
||||
:center="true"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<template #content>
|
||||
<el-col>
|
||||
<el-row :class="$style.row">
|
||||
<n8n-text data-testid="from-ai-parameters-modal-description">
|
||||
{{
|
||||
i18n.baseText('fromAiParametersModal.description', {
|
||||
interpolate: { parentNodeName: parentNode || '' },
|
||||
})
|
||||
}}
|
||||
</n8n-text>
|
||||
</el-row>
|
||||
</el-col>
|
||||
<el-col>
|
||||
<el-row :class="$style.row">
|
||||
<N8nFormInputs
|
||||
ref="inputs"
|
||||
:inputs="parameters"
|
||||
:column-view="true"
|
||||
data-test-id="from-ai-parameters-modal-inputs"
|
||||
@submit="onExecute"
|
||||
></N8nFormInputs>
|
||||
</el-row>
|
||||
</el-col>
|
||||
</template>
|
||||
<template #footer>
|
||||
<el-row justify="end">
|
||||
<el-col :span="5" :offset="19">
|
||||
<n8n-button
|
||||
data-test-id="execute-workflow-button"
|
||||
icon="flask"
|
||||
:label="i18n.baseText('fromAiParametersModal.execute')"
|
||||
@click="onExecute"
|
||||
/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style lang="scss" module>
|
||||
.row {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
DELETE_FOLDER_MODAL_KEY,
|
||||
MOVE_FOLDER_MODAL_KEY,
|
||||
WORKFLOW_ACTIVATION_CONFLICTING_WEBHOOK_MODAL_KEY,
|
||||
FROM_AI_PARAMETERS_MODAL_KEY,
|
||||
} from '@/constants';
|
||||
|
||||
import AboutModal from '@/components/AboutModal.vue';
|
||||
@@ -73,6 +74,7 @@ import NewAssistantSessionModal from '@/components/AskAssistant/Chat/NewAssistan
|
||||
import PromptMfaCodeModal from './PromptMfaCodeModal/PromptMfaCodeModal.vue';
|
||||
import CommunityPlusEnrollmentModal from '@/components/CommunityPlusEnrollmentModal.vue';
|
||||
import WorkflowActivationConflictingWebhookModal from '@/components/WorkflowActivationConflictingWebhookModal.vue';
|
||||
import FromAiParametersModal from '@/components/FromAiParametersModal.vue';
|
||||
import type { EventBus } from '@n8n/utils/event-bus';
|
||||
</script>
|
||||
|
||||
@@ -302,5 +304,11 @@ import type { EventBus } from '@n8n/utils/event-bus';
|
||||
<WorkflowActivationConflictingWebhookModal :data="data" :modal-name="modalName" />
|
||||
</template>
|
||||
</ModalRoot>
|
||||
|
||||
<ModalRoot :name="FROM_AI_PARAMETERS_MODAL_KEY">
|
||||
<template #default="{ modalName, data }">
|
||||
<FromAiParametersModal :modal-name="modalName" :data="data" />
|
||||
</template>
|
||||
</ModalRoot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
MODAL_CONFIRM,
|
||||
FORM_TRIGGER_NODE_TYPE,
|
||||
CHAT_TRIGGER_NODE_TYPE,
|
||||
FROM_AI_PARAMETERS_MODAL_KEY,
|
||||
} from '@/constants';
|
||||
import {
|
||||
AI_TRANSFORM_CODE_GENERATED_FOR_PROMPT,
|
||||
@@ -27,6 +28,7 @@ import { useI18n } from '@/composables/useI18n';
|
||||
import { useTelemetry } from '@/composables/useTelemetry';
|
||||
import { type IUpdateInformation } from '@/Interface';
|
||||
import { generateCodeForAiTransform } from '@/components/ButtonParameter/utils';
|
||||
import { hasFromAiExpressions } from '@/utils/nodes/nodeTransforms';
|
||||
|
||||
const NODE_TEST_STEP_POPUP_COUNT_KEY = 'N8N_NODE_TEST_STEP_POPUP_COUNT';
|
||||
const MAX_POPUP_COUNT = 10;
|
||||
@@ -341,22 +343,31 @@ async function onClick() {
|
||||
}
|
||||
|
||||
if (!pinnedData.hasData.value || shouldUnpinAndExecute) {
|
||||
const telemetryPayload = {
|
||||
node_type: nodeType.value ? nodeType.value.name : null,
|
||||
workflow_id: workflowsStore.workflowId,
|
||||
source: props.telemetrySource,
|
||||
push_ref: ndvStore.pushRef,
|
||||
};
|
||||
if (node.value && hasFromAiExpressions(node.value)) {
|
||||
uiStore.openModalWithData({
|
||||
name: FROM_AI_PARAMETERS_MODAL_KEY,
|
||||
data: {
|
||||
nodeName: props.nodeName,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
const telemetryPayload = {
|
||||
node_type: nodeType.value ? nodeType.value.name : null,
|
||||
workflow_id: workflowsStore.workflowId,
|
||||
source: props.telemetrySource,
|
||||
push_ref: ndvStore.pushRef,
|
||||
};
|
||||
|
||||
telemetry.track('User clicked execute node button', telemetryPayload);
|
||||
await externalHooks.run('nodeExecuteButton.onClick', telemetryPayload);
|
||||
telemetry.track('User clicked execute node button', telemetryPayload);
|
||||
await externalHooks.run('nodeExecuteButton.onClick', telemetryPayload);
|
||||
|
||||
await runWorkflow({
|
||||
destinationNode: props.nodeName,
|
||||
source: 'RunData.ExecuteNodeButton',
|
||||
});
|
||||
await runWorkflow({
|
||||
destinationNode: props.nodeName,
|
||||
source: 'RunData.ExecuteNodeButton',
|
||||
});
|
||||
|
||||
emit('execute');
|
||||
emit('execute');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,6 +130,10 @@ const node = computed(() => ndvStore.activeNode);
|
||||
|
||||
const isTriggerNode = computed(() => !!node.value && nodeTypesStore.isTriggerNode(node.value.type));
|
||||
|
||||
const isNodesAsToolNode = computed(
|
||||
() => !!node.value && nodeTypesStore.isNodesAsToolNode(node.value.type),
|
||||
);
|
||||
|
||||
const isExecutable = computed(() => {
|
||||
if (props.nodeType && node.value) {
|
||||
const workflowNode = currentWorkflowInstance.value.getNode(node.value.name);
|
||||
@@ -140,7 +144,11 @@ const isExecutable = computed(() => {
|
||||
);
|
||||
const inputNames = NodeHelpers.getConnectionTypes(inputs);
|
||||
|
||||
if (!inputNames.includes(NodeConnectionTypes.Main) && !isTriggerNode.value) {
|
||||
if (
|
||||
!inputNames.includes(NodeConnectionTypes.Main) &&
|
||||
!isNodesAsToolNode.value &&
|
||||
!isTriggerNode.value
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,21 @@ import CanvasNodeToolbar from '@/components/canvas/elements/nodes/CanvasNodeTool
|
||||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import { createCanvasNodeProvide, createCanvasProvide } from '@/__tests__/data';
|
||||
import { CanvasNodeRenderType } from '@/types';
|
||||
import { createPinia, setActivePinia, type Pinia } from 'pinia';
|
||||
|
||||
const renderComponent = createComponentRenderer(CanvasNodeToolbar);
|
||||
|
||||
describe('CanvasNodeToolbar', () => {
|
||||
let pinia: Pinia;
|
||||
|
||||
beforeEach(() => {
|
||||
pinia = createPinia();
|
||||
setActivePinia(pinia);
|
||||
});
|
||||
|
||||
it('should render execute node button when renderType is not configuration', async () => {
|
||||
const { getByTestId } = renderComponent({
|
||||
pinia,
|
||||
global: {
|
||||
provide: {
|
||||
...createCanvasNodeProvide(),
|
||||
@@ -23,6 +32,7 @@ describe('CanvasNodeToolbar', () => {
|
||||
|
||||
it('should render disabled execute node button when canvas is executing', () => {
|
||||
const { getByTestId } = renderComponent({
|
||||
pinia,
|
||||
global: {
|
||||
provide: {
|
||||
...createCanvasNodeProvide(),
|
||||
@@ -38,6 +48,7 @@ describe('CanvasNodeToolbar', () => {
|
||||
|
||||
it('should render disabled execute node button when node is deactivated', async () => {
|
||||
const { getByTestId, getByRole } = renderComponent({
|
||||
pinia,
|
||||
global: {
|
||||
provide: {
|
||||
...createCanvasNodeProvide({
|
||||
@@ -96,6 +107,7 @@ describe('CanvasNodeToolbar', () => {
|
||||
|
||||
it('should emit "toggle" when disable node button is clicked', async () => {
|
||||
const { getByTestId, emitted } = renderComponent({
|
||||
pinia,
|
||||
global: {
|
||||
provide: {
|
||||
...createCanvasNodeProvide(),
|
||||
@@ -111,6 +123,7 @@ describe('CanvasNodeToolbar', () => {
|
||||
|
||||
it('should emit "delete" when delete node button is clicked', async () => {
|
||||
const { getByTestId, emitted } = renderComponent({
|
||||
pinia,
|
||||
global: {
|
||||
provide: {
|
||||
...createCanvasNodeProvide(),
|
||||
@@ -126,6 +139,7 @@ describe('CanvasNodeToolbar', () => {
|
||||
|
||||
it('should emit "open:contextmenu" when overflow node button is clicked', async () => {
|
||||
const { getByTestId, emitted } = renderComponent({
|
||||
pinia,
|
||||
global: {
|
||||
provide: {
|
||||
...createCanvasNodeProvide(),
|
||||
@@ -141,6 +155,7 @@ describe('CanvasNodeToolbar', () => {
|
||||
|
||||
it('should emit "update" when sticky note color is changed', async () => {
|
||||
const { getAllByTestId, getByTestId, emitted } = renderComponent({
|
||||
pinia,
|
||||
global: {
|
||||
provide: {
|
||||
...createCanvasNodeProvide({
|
||||
@@ -164,6 +179,7 @@ describe('CanvasNodeToolbar', () => {
|
||||
|
||||
it('should have "forceVisible" class when hovered', async () => {
|
||||
const { getByTestId } = renderComponent({
|
||||
pinia,
|
||||
global: {
|
||||
provide: {
|
||||
...createCanvasNodeProvide(),
|
||||
@@ -181,6 +197,7 @@ describe('CanvasNodeToolbar', () => {
|
||||
|
||||
it('should have "forceVisible" class when sticky color picker is visible', async () => {
|
||||
const { getByTestId } = renderComponent({
|
||||
pinia,
|
||||
global: {
|
||||
provide: {
|
||||
...createCanvasNodeProvide({
|
||||
|
||||
@@ -4,6 +4,8 @@ import { useI18n } from '@/composables/useI18n';
|
||||
import { useCanvasNode } from '@/composables/useCanvasNode';
|
||||
import { CanvasNodeRenderType } from '@/types';
|
||||
import { useCanvas } from '@/composables/useCanvas';
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
|
||||
const emit = defineEmits<{
|
||||
delete: [];
|
||||
@@ -21,7 +23,15 @@ const $style = useCssModule();
|
||||
const i18n = useI18n();
|
||||
|
||||
const { isExecuting } = useCanvas();
|
||||
const { isDisabled, render } = useCanvasNode();
|
||||
const { isDisabled, render, name } = useCanvasNode();
|
||||
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
|
||||
const node = computed(() => !!name.value && workflowsStore.getNodeByName(name.value));
|
||||
const isNodesAsToolNode = computed(
|
||||
() => !!node.value && nodeTypesStore.isNodesAsToolNode(node.value.type),
|
||||
);
|
||||
|
||||
const nodeDisabledTitle = computed(() => {
|
||||
return isDisabled.value ? i18n.baseText('node.enable') : i18n.baseText('node.disable');
|
||||
@@ -41,7 +51,7 @@ const isExecuteNodeVisible = computed(() => {
|
||||
!props.readOnly &&
|
||||
render.value.type === CanvasNodeRenderType.Default &&
|
||||
'configuration' in render.value.options &&
|
||||
!render.value.options.configuration
|
||||
(!render.value.options.configuration || isNodesAsToolNode.value)
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user