From c265d44841eb147115563ce24c56666b1e321433 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Tue, 19 Nov 2024 10:09:06 -0500 Subject: [PATCH] feat(editor): Add option to create sub workflow from workflows list in `Execute Workflow` node (#11706) --- .../e2e/45-workflow-selector-parameter.cy.ts | 29 ++++++-- packages/editor-ui/src/Interface.ts | 4 ++ .../ResourceLocatorDropdown.vue | 41 +++++++++-- .../WorkflowSelectorParameterInput.vue | 70 +++++++++++++++++-- .../useWorkflowResourcesLocator.ts | 13 ++-- packages/editor-ui/src/constants.ts | 43 ++++++++++++ .../src/plugins/i18n/locales/en.json | 4 +- .../editor-ui/src/stores/workflows.store.ts | 10 ++- .../src/views/WorkflowOnboardingView.vue | 49 ++++++++++++- 9 files changed, 242 insertions(+), 21 deletions(-) diff --git a/cypress/e2e/45-workflow-selector-parameter.cy.ts b/cypress/e2e/45-workflow-selector-parameter.cy.ts index a6dc23e6c2..3a4de55f50 100644 --- a/cypress/e2e/45-workflow-selector-parameter.cy.ts +++ b/cypress/e2e/45-workflow-selector-parameter.cy.ts @@ -27,7 +27,7 @@ describe('Workflow Selector Parameter', () => { getVisiblePopper() .should('have.length', 1) .findChildByTestId('rlc-item') - .should('have.length', 2); + .should('have.length', 3); }); it('should show required parameter warning', () => { @@ -44,7 +44,8 @@ describe('Workflow Selector Parameter', () => { getVisiblePopper() .should('have.length', 1) .findChildByTestId('rlc-item') - .should('have.length', 1) + .should('have.length', 2) + .eq(1) .click(); ndv.getters @@ -57,7 +58,7 @@ describe('Workflow Selector Parameter', () => { ndv.getters.resourceLocator('workflowId').should('be.visible'); 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'); 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.resourceLocatorInput('workflowId').click(); - getVisiblePopper().findChildByTestId('rlc-item').first().click(); + getVisiblePopper().findChildByTestId('rlc-item').eq(1).click(); ndv.getters .resourceLocatorModeSelector('workflowId') .find('input') @@ -79,4 +80,24 @@ describe('Workflow Selector Parameter', () => { .find('input') .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'); + }); }); diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index 29d7d78e04..45a09849b4 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -239,6 +239,10 @@ export interface IWorkflowDataUpdate { meta?: WorkflowMetadata; } +export interface IWorkflowDataCreate extends IWorkflowDataUpdate { + projectId?: string; +} + export interface IWorkflowToShare extends IWorkflowDataUpdate { meta: WorkflowMetadata; } diff --git a/packages/editor-ui/src/components/ResourceLocator/ResourceLocatorDropdown.vue b/packages/editor-ui/src/components/ResourceLocator/ResourceLocatorDropdown.vue index c40c8e3db0..8cab61b64a 100644 --- a/packages/editor-ui/src/components/ResourceLocator/ResourceLocatorDropdown.vue +++ b/packages/editor-ui/src/components/ResourceLocator/ResourceLocatorDropdown.vue @@ -22,6 +22,9 @@ type Props = { filterRequired?: boolean; width?: number; eventBus?: EventBus; + allowNewResources?: { + label?: string; + }; }; const props = withDefaults(defineProps(), { @@ -35,6 +38,7 @@ const props = withDefaults(defineProps(), { errorView: false, filterRequired: false, width: undefined, + allowNewResources: () => ({}), eventBus: () => createEventBus(), }); @@ -42,6 +46,7 @@ const emit = defineEmits<{ 'update:modelValue': [value: NodeParameterValue]; loadMore: []; filter: [filter: string]; + addResourceClick: []; }>(); const i18n = useI18n(); @@ -225,7 +230,7 @@ function onResultsEnd() { {{ i18n.baseText('resourceLocator.mode.list.searchRequired') }}
{{ i18n.baseText('resourceLocator.mode.list.noResults') }} @@ -236,6 +241,24 @@ function onResultsEnd() { :class="$style.container" @scroll="onResultsEnd" > +
+
+ {{ allowNewResources.label }} + +
+
@@ -255,7 +278,7 @@ function onResultsEnd() {
diff --git a/packages/editor-ui/src/components/WorkflowSelectorParameterInput/WorkflowSelectorParameterInput.vue b/packages/editor-ui/src/components/WorkflowSelectorParameterInput/WorkflowSelectorParameterInput.vue index 3a3817b58c..4b0ef278ea 100644 --- a/packages/editor-ui/src/components/WorkflowSelectorParameterInput/WorkflowSelectorParameterInput.vue +++ b/packages/editor-ui/src/components/WorkflowSelectorParameterInput/WorkflowSelectorParameterInput.vue @@ -18,6 +18,9 @@ import { useRouter } from 'vue-router'; import { useWorkflowResourceLocatorDropdown } from './useWorkflowResourceLocatorDropdown'; import { useWorkflowResourceLocatorModes } from './useWorkflowResourceLocatorModes'; 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 { modelValue: INodeParameterResourceLocator; @@ -50,11 +53,14 @@ const emit = defineEmits<{ blur: []; }>(); -const router = useRouter(); const workflowsStore = useWorkflowsStore(); +const projectStore = useProjectsStore(); + +const router = useRouter(); const i18n = useI18n(); const container = ref(); const dropdown = ref>(); +const telemetry = useTelemetry(); const width = ref(0); const inputRef = ref(); @@ -73,14 +79,35 @@ const { hasMoreWorkflowsToLoad, isLoadingResources, filteredResources, - onSearchFilter, searchFilter, + onSearchFilter, getWorkflowName, populateNextWorkflowsPage, setWorkflowsResources, + reloadWorkflows, getWorkflowUrl, } = 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(() => { if (typeof props.modelValue !== 'object') { return props.modelValue; @@ -122,13 +149,13 @@ function onInputChange(value: NodeParameterValue): void { } function onListItemSelected(value: NodeParameterValue) { + telemetry.track('User chose sub-workflow', {}, { withPostHog: true }); onInputChange(value); hideDropdown(); } function onInputFocus(): void { setWidth(); - showDropdown(); emit('focus'); } @@ -176,8 +203,39 @@ watch( onClickOutside(dropdown, () => { isDropdownVisible.value = false; }); - +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', + ); +}; +