mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-30 06:59:41 -08:00
feat(editor): Add option to create sub workflow from workflows list in Execute Workflow
node (#11706)
This commit is contained in:
parent
b38ce14ec9
commit
c265d44841
|
@ -27,7 +27,7 @@ describe('Workflow Selector Parameter', () => {
|
||||||
getVisiblePopper()
|
getVisiblePopper()
|
||||||
.should('have.length', 1)
|
.should('have.length', 1)
|
||||||
.findChildByTestId('rlc-item')
|
.findChildByTestId('rlc-item')
|
||||||
.should('have.length', 2);
|
.should('have.length', 3);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show required parameter warning', () => {
|
it('should show required parameter warning', () => {
|
||||||
|
@ -44,7 +44,8 @@ describe('Workflow Selector Parameter', () => {
|
||||||
getVisiblePopper()
|
getVisiblePopper()
|
||||||
.should('have.length', 1)
|
.should('have.length', 1)
|
||||||
.findChildByTestId('rlc-item')
|
.findChildByTestId('rlc-item')
|
||||||
.should('have.length', 1)
|
.should('have.length', 2)
|
||||||
|
.eq(1)
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
ndv.getters
|
ndv.getters
|
||||||
|
@ -57,7 +58,7 @@ describe('Workflow Selector Parameter', () => {
|
||||||
ndv.getters.resourceLocator('workflowId').should('be.visible');
|
ndv.getters.resourceLocator('workflowId').should('be.visible');
|
||||||
ndv.getters.resourceLocatorInput('workflowId').click();
|
ndv.getters.resourceLocatorInput('workflowId').click();
|
||||||
|
|
||||||
getVisiblePopper().findChildByTestId('rlc-item').first().click();
|
getVisiblePopper().findChildByTestId('rlc-item').eq(1).click();
|
||||||
|
|
||||||
ndv.getters.resourceLocatorInput('workflowId').find('a').should('exist');
|
ndv.getters.resourceLocatorInput('workflowId').find('a').should('exist');
|
||||||
cy.getByTestId('radio-button-expression').eq(1).click();
|
cy.getByTestId('radio-button-expression').eq(1).click();
|
||||||
|
@ -68,7 +69,7 @@ describe('Workflow Selector Parameter', () => {
|
||||||
ndv.getters.resourceLocator('workflowId').should('be.visible');
|
ndv.getters.resourceLocator('workflowId').should('be.visible');
|
||||||
ndv.getters.resourceLocatorInput('workflowId').click();
|
ndv.getters.resourceLocatorInput('workflowId').click();
|
||||||
|
|
||||||
getVisiblePopper().findChildByTestId('rlc-item').first().click();
|
getVisiblePopper().findChildByTestId('rlc-item').eq(1).click();
|
||||||
ndv.getters
|
ndv.getters
|
||||||
.resourceLocatorModeSelector('workflowId')
|
.resourceLocatorModeSelector('workflowId')
|
||||||
.find('input')
|
.find('input')
|
||||||
|
@ -79,4 +80,24 @@ describe('Workflow Selector Parameter', () => {
|
||||||
.find('input')
|
.find('input')
|
||||||
.should('have.value', 'By ID');
|
.should('have.value', 'By ID');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should render add resource option and redirect to the correct route when clicked', () => {
|
||||||
|
cy.window().then((win) => {
|
||||||
|
cy.stub(win, 'open').as('windowOpen');
|
||||||
|
});
|
||||||
|
|
||||||
|
ndv.getters.resourceLocator('workflowId').should('be.visible');
|
||||||
|
ndv.getters.resourceLocatorInput('workflowId').click();
|
||||||
|
|
||||||
|
getVisiblePopper().findChildByTestId('rlc-item').eq(0).should('exist');
|
||||||
|
getVisiblePopper()
|
||||||
|
.findChildByTestId('rlc-item')
|
||||||
|
.eq(0)
|
||||||
|
.find('span')
|
||||||
|
.should('have.text', 'Create a new sub-workflow');
|
||||||
|
|
||||||
|
getVisiblePopper().findChildByTestId('rlc-item').eq(0).click();
|
||||||
|
|
||||||
|
cy.get('@windowOpen').should('be.calledWith', '/workflows/onboarding/0?sampleSubWorkflows=0');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -239,6 +239,10 @@ export interface IWorkflowDataUpdate {
|
||||||
meta?: WorkflowMetadata;
|
meta?: WorkflowMetadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IWorkflowDataCreate extends IWorkflowDataUpdate {
|
||||||
|
projectId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IWorkflowToShare extends IWorkflowDataUpdate {
|
export interface IWorkflowToShare extends IWorkflowDataUpdate {
|
||||||
meta: WorkflowMetadata;
|
meta: WorkflowMetadata;
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,9 @@ type Props = {
|
||||||
filterRequired?: boolean;
|
filterRequired?: boolean;
|
||||||
width?: number;
|
width?: number;
|
||||||
eventBus?: EventBus;
|
eventBus?: EventBus;
|
||||||
|
allowNewResources?: {
|
||||||
|
label?: string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
@ -35,6 +38,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||||
errorView: false,
|
errorView: false,
|
||||||
filterRequired: false,
|
filterRequired: false,
|
||||||
width: undefined,
|
width: undefined,
|
||||||
|
allowNewResources: () => ({}),
|
||||||
eventBus: () => createEventBus(),
|
eventBus: () => createEventBus(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -42,6 +46,7 @@ const emit = defineEmits<{
|
||||||
'update:modelValue': [value: NodeParameterValue];
|
'update:modelValue': [value: NodeParameterValue];
|
||||||
loadMore: [];
|
loadMore: [];
|
||||||
filter: [filter: string];
|
filter: [filter: string];
|
||||||
|
addResourceClick: [];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
|
@ -225,7 +230,7 @@ function onResultsEnd() {
|
||||||
{{ i18n.baseText('resourceLocator.mode.list.searchRequired') }}
|
{{ i18n.baseText('resourceLocator.mode.list.searchRequired') }}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else-if="!errorView && sortedResources.length === 0 && !loading"
|
v-else-if="!errorView && !allowNewResources.label && sortedResources.length === 0 && !loading"
|
||||||
:class="$style.messageContainer"
|
:class="$style.messageContainer"
|
||||||
>
|
>
|
||||||
{{ i18n.baseText('resourceLocator.mode.list.noResults') }}
|
{{ i18n.baseText('resourceLocator.mode.list.noResults') }}
|
||||||
|
@ -236,6 +241,24 @@ function onResultsEnd() {
|
||||||
:class="$style.container"
|
:class="$style.container"
|
||||||
@scroll="onResultsEnd"
|
@scroll="onResultsEnd"
|
||||||
>
|
>
|
||||||
|
<div
|
||||||
|
v-if="allowNewResources.label"
|
||||||
|
key="addResourceKey"
|
||||||
|
ref="itemsRef"
|
||||||
|
data-test-id="rlc-item"
|
||||||
|
:class="{
|
||||||
|
[$style.resourceItem]: true,
|
||||||
|
[$style.hovering]: hoverIndex === 0,
|
||||||
|
}"
|
||||||
|
@mouseenter="() => onItemHover(0)"
|
||||||
|
@mouseleave="() => onItemHoverLeave()"
|
||||||
|
@click="() => emit('addResourceClick')"
|
||||||
|
>
|
||||||
|
<div :class="$style.resourceNameContainer">
|
||||||
|
<span :class="$style.addResourceText">{{ allowNewResources.label }}</span>
|
||||||
|
<font-awesome-icon :class="$style.addResourceIcon" :icon="['fa', 'plus']" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
v-for="(result, i) in sortedResources"
|
v-for="(result, i) in sortedResources"
|
||||||
:key="result.value.toString()"
|
:key="result.value.toString()"
|
||||||
|
@ -243,11 +266,11 @@ function onResultsEnd() {
|
||||||
:class="{
|
:class="{
|
||||||
[$style.resourceItem]: true,
|
[$style.resourceItem]: true,
|
||||||
[$style.selected]: result.value === modelValue,
|
[$style.selected]: result.value === modelValue,
|
||||||
[$style.hovering]: hoverIndex === i,
|
[$style.hovering]: hoverIndex === i + 1,
|
||||||
}"
|
}"
|
||||||
data-test-id="rlc-item"
|
data-test-id="rlc-item"
|
||||||
@click="() => onItemClick(result.value)"
|
@click="() => onItemClick(result.value)"
|
||||||
@mouseenter="() => onItemHover(i)"
|
@mouseenter="() => onItemHover(i + 1)"
|
||||||
@mouseleave="() => onItemHoverLeave()"
|
@mouseleave="() => onItemHoverLeave()"
|
||||||
>
|
>
|
||||||
<div :class="$style.resourceNameContainer">
|
<div :class="$style.resourceNameContainer">
|
||||||
|
@ -255,7 +278,7 @@ function onResultsEnd() {
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.urlLink">
|
<div :class="$style.urlLink">
|
||||||
<font-awesome-icon
|
<font-awesome-icon
|
||||||
v-if="showHoverUrl && result.url && hoverIndex === i"
|
v-if="showHoverUrl && result.url && hoverIndex === i + 1"
|
||||||
icon="external-link-alt"
|
icon="external-link-alt"
|
||||||
:title="result.linkAlt || i18n.baseText('resourceLocator.mode.list.openUrl')"
|
:title="result.linkAlt || i18n.baseText('resourceLocator.mode.list.openUrl')"
|
||||||
@click="openUrl($event, result.url)"
|
@click="openUrl($event, result.url)"
|
||||||
|
@ -384,4 +407,14 @@ function onResultsEnd() {
|
||||||
.searchIcon {
|
.searchIcon {
|
||||||
color: var(--color-text-light);
|
color: var(--color-text-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.addResourceText {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.addResourceIcon {
|
||||||
|
color: var(--color-text-light);
|
||||||
|
|
||||||
|
margin-left: var(--spacing-2xs);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -18,6 +18,9 @@ import { useRouter } from 'vue-router';
|
||||||
import { useWorkflowResourceLocatorDropdown } from './useWorkflowResourceLocatorDropdown';
|
import { useWorkflowResourceLocatorDropdown } from './useWorkflowResourceLocatorDropdown';
|
||||||
import { useWorkflowResourceLocatorModes } from './useWorkflowResourceLocatorModes';
|
import { useWorkflowResourceLocatorModes } from './useWorkflowResourceLocatorModes';
|
||||||
import { useWorkflowResourcesLocator } from './useWorkflowResourcesLocator';
|
import { useWorkflowResourcesLocator } from './useWorkflowResourcesLocator';
|
||||||
|
import { useProjectsStore } from '@/stores/projects.store';
|
||||||
|
import { useTelemetry } from '@/composables/useTelemetry';
|
||||||
|
import { NEW_SAMPLE_WORKFLOW_CREATED_CHANNEL, SAMPLE_SUBWORKFLOW_WORKFLOW_ID } from '@/constants';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
modelValue: INodeParameterResourceLocator;
|
modelValue: INodeParameterResourceLocator;
|
||||||
|
@ -50,11 +53,14 @@ const emit = defineEmits<{
|
||||||
blur: [];
|
blur: [];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const workflowsStore = useWorkflowsStore();
|
const workflowsStore = useWorkflowsStore();
|
||||||
|
const projectStore = useProjectsStore();
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const container = ref<HTMLDivElement>();
|
const container = ref<HTMLDivElement>();
|
||||||
const dropdown = ref<ComponentInstance<typeof ResourceLocatorDropdown>>();
|
const dropdown = ref<ComponentInstance<typeof ResourceLocatorDropdown>>();
|
||||||
|
const telemetry = useTelemetry();
|
||||||
|
|
||||||
const width = ref(0);
|
const width = ref(0);
|
||||||
const inputRef = ref<HTMLInputElement | undefined>();
|
const inputRef = ref<HTMLInputElement | undefined>();
|
||||||
|
@ -73,14 +79,35 @@ const {
|
||||||
hasMoreWorkflowsToLoad,
|
hasMoreWorkflowsToLoad,
|
||||||
isLoadingResources,
|
isLoadingResources,
|
||||||
filteredResources,
|
filteredResources,
|
||||||
onSearchFilter,
|
|
||||||
searchFilter,
|
searchFilter,
|
||||||
|
onSearchFilter,
|
||||||
getWorkflowName,
|
getWorkflowName,
|
||||||
populateNextWorkflowsPage,
|
populateNextWorkflowsPage,
|
||||||
setWorkflowsResources,
|
setWorkflowsResources,
|
||||||
|
reloadWorkflows,
|
||||||
getWorkflowUrl,
|
getWorkflowUrl,
|
||||||
} = useWorkflowResourcesLocator(router);
|
} = useWorkflowResourcesLocator(router);
|
||||||
|
|
||||||
|
const currentProjectName = computed(() => {
|
||||||
|
if (!projectStore.isTeamProjectFeatureEnabled) return '';
|
||||||
|
|
||||||
|
if (!projectStore?.currentProject || projectStore.currentProject?.type === 'personal') {
|
||||||
|
return `'${i18n.baseText('projects.menu.personal')}'`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `'${projectStore.currentProject?.name}'`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const getCreateResourceLabel = computed(() => {
|
||||||
|
if (!currentProjectName.value) {
|
||||||
|
return i18n.baseText('executeWorkflowTrigger.createNewSubworkflow.noProject');
|
||||||
|
}
|
||||||
|
|
||||||
|
return i18n.baseText('executeWorkflowTrigger.createNewSubworkflow', {
|
||||||
|
interpolate: { projectName: currentProjectName.value },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const valueToDisplay = computed<NodeParameterValue>(() => {
|
const valueToDisplay = computed<NodeParameterValue>(() => {
|
||||||
if (typeof props.modelValue !== 'object') {
|
if (typeof props.modelValue !== 'object') {
|
||||||
return props.modelValue;
|
return props.modelValue;
|
||||||
|
@ -122,13 +149,13 @@ function onInputChange(value: NodeParameterValue): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
function onListItemSelected(value: NodeParameterValue) {
|
function onListItemSelected(value: NodeParameterValue) {
|
||||||
|
telemetry.track('User chose sub-workflow', {}, { withPostHog: true });
|
||||||
onInputChange(value);
|
onInputChange(value);
|
||||||
hideDropdown();
|
hideDropdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
function onInputFocus(): void {
|
function onInputFocus(): void {
|
||||||
setWidth();
|
setWidth();
|
||||||
showDropdown();
|
|
||||||
emit('focus');
|
emit('focus');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -176,8 +203,39 @@ watch(
|
||||||
onClickOutside(dropdown, () => {
|
onClickOutside(dropdown, () => {
|
||||||
isDropdownVisible.value = false;
|
isDropdownVisible.value = false;
|
||||||
});
|
});
|
||||||
</script>
|
|
||||||
|
|
||||||
|
const onAddResourceClicked = () => {
|
||||||
|
const subWorkflowNameRegex = /My\s+Sub-Workflow\s+\d+/;
|
||||||
|
|
||||||
|
const urlSearchParams = new URLSearchParams();
|
||||||
|
|
||||||
|
if (projectStore.currentProjectId) {
|
||||||
|
urlSearchParams.set('projectId', projectStore.currentProjectId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sampleSubWorkflows = workflowsStore.allWorkflows.filter(
|
||||||
|
(w) => w.name && subWorkflowNameRegex.test(w.name),
|
||||||
|
);
|
||||||
|
|
||||||
|
urlSearchParams.set('sampleSubWorkflows', sampleSubWorkflows.length.toString());
|
||||||
|
|
||||||
|
telemetry.track('User clicked create new sub-workflow button', {}, { withPostHog: true });
|
||||||
|
|
||||||
|
const sampleSubworkflowChannel = new BroadcastChannel(NEW_SAMPLE_WORKFLOW_CREATED_CHANNEL);
|
||||||
|
|
||||||
|
sampleSubworkflowChannel.onmessage = async (event: MessageEvent<{ workflowId: string }>) => {
|
||||||
|
const workflowId = event.data.workflowId;
|
||||||
|
await reloadWorkflows();
|
||||||
|
onInputChange(workflowId);
|
||||||
|
hideDropdown();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.open(
|
||||||
|
`/workflows/onboarding/${SAMPLE_SUBWORKFLOW_WORKFLOW_ID}?${urlSearchParams.toString()}`,
|
||||||
|
'_blank',
|
||||||
|
);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
ref="container"
|
ref="container"
|
||||||
|
@ -194,11 +252,15 @@ onClickOutside(dropdown, () => {
|
||||||
:filter="searchFilter"
|
:filter="searchFilter"
|
||||||
:has-more="hasMoreWorkflowsToLoad"
|
:has-more="hasMoreWorkflowsToLoad"
|
||||||
:error-view="false"
|
:error-view="false"
|
||||||
|
:allow-new-resources="{
|
||||||
|
label: getCreateResourceLabel,
|
||||||
|
}"
|
||||||
:width="width"
|
:width="width"
|
||||||
:event-bus="eventBus"
|
:event-bus="eventBus"
|
||||||
@update:model-value="onListItemSelected"
|
@update:model-value="onListItemSelected"
|
||||||
@filter="onSearchFilter"
|
@filter="onSearchFilter"
|
||||||
@load-more="populateNextWorkflowsPage"
|
@load-more="populateNextWorkflowsPage"
|
||||||
|
@add-resource-click="onAddResourceClicked"
|
||||||
>
|
>
|
||||||
<template #error>
|
<template #error>
|
||||||
<div :class="$style.error" data-test-id="rlc-error-container">
|
<div :class="$style.error" data-test-id="rlc-error-container">
|
||||||
|
|
|
@ -24,17 +24,13 @@ export function useWorkflowResourcesLocator(router: Router) {
|
||||||
);
|
);
|
||||||
|
|
||||||
const filteredResources = computed(() => {
|
const filteredResources = computed(() => {
|
||||||
if (!searchFilter.value) return workflowsResources.value;
|
|
||||||
|
|
||||||
return workflowsStore.allWorkflows
|
return workflowsStore.allWorkflows
|
||||||
.filter((resource) => resource.name.toLowerCase().includes(searchFilter.value.toLowerCase()))
|
.filter((resource) => resource.name.toLowerCase().includes(searchFilter.value.toLowerCase()))
|
||||||
.map(workflowDbToResourceMapper);
|
.map(workflowDbToResourceMapper);
|
||||||
});
|
});
|
||||||
|
|
||||||
async function populateNextWorkflowsPage() {
|
async function populateNextWorkflowsPage() {
|
||||||
if (workflowsStore.allWorkflows.length <= 1) {
|
await workflowsStore.fetchAllWorkflows();
|
||||||
await workflowsStore.fetchAllWorkflows();
|
|
||||||
}
|
|
||||||
const nextPage = sortedWorkflows.value.slice(
|
const nextPage = sortedWorkflows.value.slice(
|
||||||
workflowsResources.value.length,
|
workflowsResources.value.length,
|
||||||
workflowsResources.value.length + PAGE_SIZE,
|
workflowsResources.value.length + PAGE_SIZE,
|
||||||
|
@ -49,6 +45,12 @@ export function useWorkflowResourcesLocator(router: Router) {
|
||||||
isLoadingResources.value = false;
|
isLoadingResources.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function reloadWorkflows() {
|
||||||
|
isLoadingResources.value = true;
|
||||||
|
await workflowsStore.fetchAllWorkflows();
|
||||||
|
isLoadingResources.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
function workflowDbToResourceMapper(workflow: IWorkflowDb) {
|
function workflowDbToResourceMapper(workflow: IWorkflowDb) {
|
||||||
return {
|
return {
|
||||||
name: getWorkflowName(workflow.id),
|
name: getWorkflowName(workflow.id),
|
||||||
|
@ -84,6 +86,7 @@ export function useWorkflowResourcesLocator(router: Router) {
|
||||||
hasMoreWorkflowsToLoad,
|
hasMoreWorkflowsToLoad,
|
||||||
filteredResources,
|
filteredResources,
|
||||||
searchFilter,
|
searchFilter,
|
||||||
|
reloadWorkflows,
|
||||||
getWorkflowUrl,
|
getWorkflowUrl,
|
||||||
onSearchFilter,
|
onSearchFilter,
|
||||||
getWorkflowName,
|
getWorkflowName,
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import type {
|
import type {
|
||||||
EnterpriseEditionFeatureKey,
|
EnterpriseEditionFeatureKey,
|
||||||
EnterpriseEditionFeatureValue,
|
EnterpriseEditionFeatureValue,
|
||||||
|
INodeUi,
|
||||||
|
IWorkflowDataCreate,
|
||||||
NodeCreatorOpenSource,
|
NodeCreatorOpenSource,
|
||||||
} from './Interface';
|
} from './Interface';
|
||||||
import { NodeConnectionType } from 'n8n-workflow';
|
import { NodeConnectionType } from 'n8n-workflow';
|
||||||
|
@ -872,3 +874,44 @@ export const CanvasNodeHandleKey =
|
||||||
export const BROWSER_ID_STORAGE_KEY = 'n8n-browserId';
|
export const BROWSER_ID_STORAGE_KEY = 'n8n-browserId';
|
||||||
|
|
||||||
export const APP_MODALS_ELEMENT_ID = 'app-modals';
|
export const APP_MODALS_ELEMENT_ID = 'app-modals';
|
||||||
|
|
||||||
|
export const SAMPLE_SUBWORKFLOW_WORKFLOW_ID = '0';
|
||||||
|
|
||||||
|
export const NEW_SAMPLE_WORKFLOW_CREATED_CHANNEL = 'new-sample-sub-workflow-created';
|
||||||
|
|
||||||
|
export const SAMPLE_SUBWORKFLOW_WORKFLOW: IWorkflowDataCreate = {
|
||||||
|
name: 'My Sub-Workflow',
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
parameters: {},
|
||||||
|
id: 'c055762a-8fe7-4141-a639-df2372f30060',
|
||||||
|
name: 'Execute Workflow Trigger',
|
||||||
|
type: 'n8n-nodes-base.executeWorkflowTrigger',
|
||||||
|
position: [260, 340],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parameters: {},
|
||||||
|
id: 'b5942df6-0160-4ef7-965d-57583acdc8aa',
|
||||||
|
name: 'Replace me with your logic',
|
||||||
|
type: 'n8n-nodes-base.noOp',
|
||||||
|
position: [520, 340],
|
||||||
|
},
|
||||||
|
] as INodeUi[],
|
||||||
|
connections: {
|
||||||
|
'Execute Workflow Trigger': {
|
||||||
|
main: [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
node: 'Replace me with your logic',
|
||||||
|
type: NodeConnectionType.Main,
|
||||||
|
index: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
executionOrder: 'v1',
|
||||||
|
},
|
||||||
|
pinData: {},
|
||||||
|
};
|
||||||
|
|
|
@ -2708,5 +2708,7 @@
|
||||||
"communityPlusModal.features.third.description": "Search and organize past workflow executions for easier review",
|
"communityPlusModal.features.third.description": "Search and organize past workflow executions for easier review",
|
||||||
"communityPlusModal.input.email.label": "Enter email to receive your license key",
|
"communityPlusModal.input.email.label": "Enter email to receive your license key",
|
||||||
"communityPlusModal.button.skip": "Skip",
|
"communityPlusModal.button.skip": "Skip",
|
||||||
"communityPlusModal.button.confirm": "Send me a free license key"
|
"communityPlusModal.button.confirm": "Send me a free license key",
|
||||||
|
"executeWorkflowTrigger.createNewSubworkflow": "Create a sub-workflow in {projectName}",
|
||||||
|
"executeWorkflowTrigger.createNewSubworkflow.noProject": "Create a new sub-workflow"
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,6 +28,7 @@ import type {
|
||||||
WorkflowMetadata,
|
WorkflowMetadata,
|
||||||
IExecutionFlattedResponse,
|
IExecutionFlattedResponse,
|
||||||
IWorkflowTemplateNode,
|
IWorkflowTemplateNode,
|
||||||
|
IWorkflowDataCreate,
|
||||||
} from '@/Interface';
|
} from '@/Interface';
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import type {
|
import type {
|
||||||
|
@ -1390,13 +1391,18 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
|
||||||
return response && unflattenExecutionData(response);
|
return response && unflattenExecutionData(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createNewWorkflow(sendData: IWorkflowDataUpdate): Promise<IWorkflowDb> {
|
/**
|
||||||
|
* Creates a new workflow with the provided data.
|
||||||
|
* Ensures that the new workflow is not active upon creation.
|
||||||
|
* If the project ID is not provided in the data, it assigns the current project ID from the project store.
|
||||||
|
*/
|
||||||
|
async function createNewWorkflow(sendData: IWorkflowDataCreate): Promise<IWorkflowDb> {
|
||||||
// make sure that the new ones are not active
|
// make sure that the new ones are not active
|
||||||
sendData.active = false;
|
sendData.active = false;
|
||||||
|
|
||||||
const projectStore = useProjectsStore();
|
const projectStore = useProjectsStore();
|
||||||
|
|
||||||
if (projectStore.currentProjectId) {
|
if (!sendData.projectId && projectStore.currentProjectId) {
|
||||||
(sendData as unknown as IDataObject).projectId = projectStore.currentProjectId;
|
(sendData as unknown as IDataObject).projectId = projectStore.currentProjectId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,17 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useLoadingService } from '@/composables/useLoadingService';
|
import { useLoadingService } from '@/composables/useLoadingService';
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
import { VIEWS } from '@/constants';
|
import {
|
||||||
|
NEW_SAMPLE_WORKFLOW_CREATED_CHANNEL,
|
||||||
|
SAMPLE_SUBWORKFLOW_WORKFLOW,
|
||||||
|
SAMPLE_SUBWORKFLOW_WORKFLOW_ID,
|
||||||
|
VIEWS,
|
||||||
|
} from '@/constants';
|
||||||
import { useTemplatesStore } from '@/stores/templates.store';
|
import { useTemplatesStore } from '@/stores/templates.store';
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
import { onMounted } from 'vue';
|
import { onMounted } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
import type { IWorkflowDataCreate } from '@/Interface';
|
||||||
|
|
||||||
const loadingService = useLoadingService();
|
const loadingService = useLoadingService();
|
||||||
const templateStore = useTemplatesStore();
|
const templateStore = useTemplatesStore();
|
||||||
|
@ -15,6 +21,11 @@ const route = useRoute();
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
|
|
||||||
const openWorkflowTemplate = async (templateId: string) => {
|
const openWorkflowTemplate = async (templateId: string) => {
|
||||||
|
if (templateId === SAMPLE_SUBWORKFLOW_WORKFLOW_ID) {
|
||||||
|
await openSampleSubworkflow();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
loadingService.startLoading();
|
loadingService.startLoading();
|
||||||
const template = await templateStore.getFixedWorkflowTemplate(templateId);
|
const template = await templateStore.getFixedWorkflowTemplate(templateId);
|
||||||
|
@ -52,6 +63,42 @@ const openWorkflowTemplate = async (templateId: string) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openSampleSubworkflow = async () => {
|
||||||
|
try {
|
||||||
|
loadingService.startLoading();
|
||||||
|
|
||||||
|
const projectId = route.query?.projectId;
|
||||||
|
|
||||||
|
const sampleSubWorkflows = Number(route.query?.sampleSubWorkflows ?? 0);
|
||||||
|
|
||||||
|
const workflowName = `${SAMPLE_SUBWORKFLOW_WORKFLOW.name} ${sampleSubWorkflows + 1}`;
|
||||||
|
|
||||||
|
const workflow: IWorkflowDataCreate = {
|
||||||
|
...SAMPLE_SUBWORKFLOW_WORKFLOW,
|
||||||
|
name: workflowName,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (projectId) {
|
||||||
|
workflow.projectId = projectId as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newWorkflow = await workflowsStore.createNewWorkflow(workflow);
|
||||||
|
|
||||||
|
const sampleSubworkflowChannel = new BroadcastChannel(NEW_SAMPLE_WORKFLOW_CREATED_CHANNEL);
|
||||||
|
|
||||||
|
sampleSubworkflowChannel.postMessage({ workflowId: newWorkflow.id });
|
||||||
|
|
||||||
|
await router.replace({
|
||||||
|
name: VIEWS.WORKFLOW,
|
||||||
|
params: { name: newWorkflow.id },
|
||||||
|
});
|
||||||
|
loadingService.stopLoading();
|
||||||
|
} catch (e) {
|
||||||
|
await router.replace({ name: VIEWS.NEW_WORKFLOW });
|
||||||
|
loadingService.stopLoading();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const templateId = route.params.id;
|
const templateId = route.params.id;
|
||||||
if (!templateId || typeof templateId !== 'string') {
|
if (!templateId || typeof templateId !== 'string') {
|
||||||
|
|
Loading…
Reference in a new issue