feat(editor): Replace root events with event bus events (no-changelog) (#6454)

* feat: replace root events with event bus events

* fix: prevent cypress from replacing global with globalThis in import path

* feat: remove emitter mixin

* fix: replace component events with event bus

* fix: fix linting issue

* fix: fix breaking expression switch

* chore: prettify ndv e2e suite code
This commit is contained in:
Alex Grozav 2023-06-20 13:00:53 +03:00 committed by GitHub
parent 18f588444f
commit 0154a97773
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 215 additions and 158 deletions

View file

@ -50,7 +50,7 @@ describe('NDV', () => {
workflowPage.getters.canvasNodes().last().dblclick(); workflowPage.getters.canvasNodes().last().dblclick();
ndv.getters.inputSelect().click(); ndv.getters.inputSelect().click();
ndv.getters.inputOption().last().click(); ndv.getters.inputOption().last().click();
ndv.getters.inputDataContainer().find('[class*=schema_]').should('exist') ndv.getters.inputDataContainer().find('[class*=schema_]').should('exist');
ndv.getters.inputDataContainer().should('contain', 'start'); ndv.getters.inputDataContainer().should('contain', 'start');
}); });
@ -96,10 +96,20 @@ describe('NDV', () => {
ndv.getters.container().should('be.visible'); ndv.getters.container().should('be.visible');
workflowPage.actions.saveWorkflowUsingKeyboardShortcut(); workflowPage.actions.saveWorkflowUsingKeyboardShortcut();
workflowPage.getters.isWorkflowSaved(); workflowPage.getters.isWorkflowSaved();
}) });
describe('test output schema view', () => { describe('test output schema view', () => {
const schemaKeys = ['id', 'name', 'email', 'notes', 'country', 'created', 'objectValue', 'prop1', 'prop2']; const schemaKeys = [
'id',
'name',
'email',
'notes',
'country',
'created',
'objectValue',
'prop1',
'prop2',
];
function setupSchemaWorkflow() { function setupSchemaWorkflow() {
cy.createFixtureWorkflow('Test_workflow_schema_test.json', `NDV test schema view ${uuid()}`); cy.createFixtureWorkflow('Test_workflow_schema_test.json', `NDV test schema view ${uuid()}`);
workflowPage.actions.zoomToFit(); workflowPage.actions.zoomToFit();
@ -108,41 +118,62 @@ describe('NDV', () => {
} }
it('should switch to output schema view and validate it', () => { it('should switch to output schema view and validate it', () => {
setupSchemaWorkflow() setupSchemaWorkflow();
ndv.getters.outputDisplayMode().children().should('have.length', 3); ndv.getters.outputDisplayMode().children().should('have.length', 3);
ndv.getters.outputDisplayMode().find('[class*=active]').should('contain', 'Table'); ndv.getters.outputDisplayMode().find('[class*=active]').should('contain', 'Table');
ndv.getters.outputDisplayMode().contains('Schema').click(); ndv.getters.outputDisplayMode().contains('Schema').click();
ndv.getters.outputDisplayMode().find('[class*=active]').should('contain', 'Schema'); ndv.getters.outputDisplayMode().find('[class*=active]').should('contain', 'Schema');
schemaKeys.forEach((key) => { schemaKeys.forEach((key) => {
ndv.getters.outputPanel().find('[data-test-id=run-data-schema-item]').contains(key).should('exist'); ndv.getters
.outputPanel()
.find('[data-test-id=run-data-schema-item]')
.contains(key)
.should('exist');
}); });
}); });
it('should preserve schema view after execution', () => { it('should preserve schema view after execution', () => {
setupSchemaWorkflow() setupSchemaWorkflow();
ndv.getters.outputDisplayMode().contains('Schema').click(); ndv.getters.outputDisplayMode().contains('Schema').click();
ndv.actions.execute(); ndv.actions.execute();
ndv.getters.outputDisplayMode().find('[class*=active]').should('contain', 'Schema'); ndv.getters.outputDisplayMode().find('[class*=active]').should('contain', 'Schema');
}) });
it('should collapse and expand nested schema object', () => { it('should collapse and expand nested schema object', () => {
setupSchemaWorkflow() setupSchemaWorkflow();
const expandedObjectProps = ['prop1', 'prop2'];; const expandedObjectProps = ['prop1', 'prop2'];
const getObjectValueItem = () => ndv.getters.outputPanel().find('[data-test-id=run-data-schema-item]').filter(':contains("objectValue")'); const getObjectValueItem = () =>
ndv.getters
.outputPanel()
.find('[data-test-id=run-data-schema-item]')
.filter(':contains("objectValue")');
ndv.getters.outputDisplayMode().contains('Schema').click(); ndv.getters.outputDisplayMode().contains('Schema').click();
expandedObjectProps.forEach((key) => { expandedObjectProps.forEach((key) => {
ndv.getters.outputPanel().find('[data-test-id=run-data-schema-item]').contains(key).should('be.visible'); ndv.getters
.outputPanel()
.find('[data-test-id=run-data-schema-item]')
.contains(key)
.should('be.visible');
}); });
getObjectValueItem().find('label').click(); getObjectValueItem().find('label').click();
expandedObjectProps.forEach((key) => { expandedObjectProps.forEach((key) => {
ndv.getters.outputPanel().find('[data-test-id=run-data-schema-item]').contains(key).should('not.be.visible'); ndv.getters
.outputPanel()
.find('[data-test-id=run-data-schema-item]')
.contains(key)
.should('not.be.visible');
});
}); });
})
it('should not display pagination for schema', () => { it('should not display pagination for schema', () => {
setupSchemaWorkflow() setupSchemaWorkflow();
ndv.getters.backToCanvas().click(); ndv.getters.backToCanvas().click();
workflowPage.getters.canvasNodeByName('Set').click(); workflowPage.getters.canvasNodeByName('Set').click();
workflowPage.actions.addNodeToCanvas('Customer Datastore (n8n training)', true, true, 'Get All People'); workflowPage.actions.addNodeToCanvas(
'Customer Datastore (n8n training)',
true,
true,
'Get All People',
);
ndv.actions.execute(); ndv.actions.execute();
ndv.getters.outputPanel().contains('25 items').should('exist'); ndv.getters.outputPanel().contains('25 items').should('exist');
ndv.getters.outputPanel().find('[class*=_pagination]').should('exist'); ndv.getters.outputPanel().find('[class*=_pagination]').should('exist');
@ -150,9 +181,12 @@ describe('NDV', () => {
ndv.getters.outputPanel().find('[class*=_pagination]').should('not.exist'); ndv.getters.outputPanel().find('[class*=_pagination]').should('not.exist');
ndv.getters.outputDisplayMode().contains('JSON').click(); ndv.getters.outputDisplayMode().contains('JSON').click();
ndv.getters.outputPanel().find('[class*=_pagination]').should('exist'); ndv.getters.outputPanel().find('[class*=_pagination]').should('exist');
}) });
it('should display large schema', () => { it('should display large schema', () => {
cy.createFixtureWorkflow('Test_workflow_schema_test_pinned_data.json', `NDV test schema view ${uuid()}`); cy.createFixtureWorkflow(
'Test_workflow_schema_test_pinned_data.json',
`NDV test schema view ${uuid()}`,
);
workflowPage.actions.zoomToFit(); workflowPage.actions.zoomToFit();
workflowPage.actions.openNode('Set'); workflowPage.actions.openNode('Set');
@ -160,8 +194,11 @@ describe('NDV', () => {
ndv.getters.outputPanel().find('[class*=_pagination]').should('exist'); ndv.getters.outputPanel().find('[class*=_pagination]').should('exist');
ndv.getters.outputDisplayMode().contains('Schema').click(); ndv.getters.outputDisplayMode().contains('Schema').click();
ndv.getters.outputPanel().find('[class*=_pagination]').should('not.exist'); ndv.getters.outputPanel().find('[class*=_pagination]').should('not.exist');
ndv.getters.outputPanel().find('[data-test-id=run-data-schema-item] [data-test-id=run-data-schema-item]').should('have.length', 20); ndv.getters
}) .outputPanel()
.find('[data-test-id=run-data-schema-item] [data-test-id=run-data-schema-item]')
.should('have.length', 20);
});
}); });
it('can link and unlink run selectors between input and output', () => { it('can link and unlink run selectors between input and output', () => {
@ -170,11 +207,13 @@ describe('NDV', () => {
workflowPage.actions.executeWorkflow(); workflowPage.actions.executeWorkflow();
workflowPage.actions.openNode('Set3'); workflowPage.actions.openNode('Set3');
ndv.getters.inputRunSelector() ndv.getters
.inputRunSelector()
.should('exist') .should('exist')
.find('input') .find('input')
.should('include.value', '2 of 2 (6 items)'); .should('include.value', '2 of 2 (6 items)');
ndv.getters.outputRunSelector() ndv.getters
.outputRunSelector()
.should('exist') .should('exist')
.find('input') .find('input')
.should('include.value', '2 of 2 (6 items)'); .should('include.value', '2 of 2 (6 items)');
@ -183,23 +222,20 @@ describe('NDV', () => {
ndv.actions.switchOutputMode('Table'); ndv.actions.switchOutputMode('Table');
ndv.actions.changeOutputRunSelector('1 of 2 (6 items)'); ndv.actions.changeOutputRunSelector('1 of 2 (6 items)');
ndv.getters.inputRunSelector() ndv.getters.inputRunSelector().find('input').should('include.value', '1 of 2 (6 items)');
.find('input')
.should('include.value', '1 of 2 (6 items)');
ndv.getters.inputTbodyCell(1, 0).should('have.text', '1111'); ndv.getters.inputTbodyCell(1, 0).should('have.text', '1111');
ndv.getters.outputTbodyCell(1, 0).should('have.text', '1111'); ndv.getters.outputTbodyCell(1, 0).should('have.text', '1111');
ndv.getters.inputTbodyCell(1, 0).click(); // remove tooltip ndv.getters.inputTbodyCell(1, 0).click(); // remove tooltip
ndv.actions.changeInputRunSelector('2 of 2 (6 items)'); ndv.actions.changeInputRunSelector('2 of 2 (6 items)');
ndv.getters.outputRunSelector() ndv.getters.outputRunSelector().find('input').should('include.value', '2 of 2 (6 items)');
.find('input')
.should('include.value', '2 of 2 (6 items)');
// unlink // unlink
ndv.actions.toggleOutputRunLinking(); ndv.actions.toggleOutputRunLinking();
ndv.getters.inputTbodyCell(1, 0).click(); // remove tooltip ndv.getters.inputTbodyCell(1, 0).click(); // remove tooltip
ndv.actions.changeOutputRunSelector('1 of 2 (6 items)'); ndv.actions.changeOutputRunSelector('1 of 2 (6 items)');
ndv.getters.inputRunSelector() ndv.getters
.inputRunSelector()
.should('exist') .should('exist')
.find('input') .find('input')
.should('include.value', '2 of 2 (6 items)'); .should('include.value', '2 of 2 (6 items)');
@ -207,24 +243,18 @@ describe('NDV', () => {
// link again // link again
ndv.actions.toggleOutputRunLinking(); ndv.actions.toggleOutputRunLinking();
ndv.getters.inputTbodyCell(1, 0).click(); // remove tooltip ndv.getters.inputTbodyCell(1, 0).click(); // remove tooltip
ndv.getters.inputRunSelector() ndv.getters.inputRunSelector().find('input').should('include.value', '1 of 2 (6 items)');
.find('input')
.should('include.value', '1 of 2 (6 items)');
// unlink again // unlink again
ndv.actions.toggleInputRunLinking(); ndv.actions.toggleInputRunLinking();
ndv.getters.inputTbodyCell(1, 0).click(); // remove tooltip ndv.getters.inputTbodyCell(1, 0).click(); // remove tooltip
ndv.actions.changeInputRunSelector('2 of 2 (6 items)'); ndv.actions.changeInputRunSelector('2 of 2 (6 items)');
ndv.getters.outputRunSelector() ndv.getters.outputRunSelector().find('input').should('include.value', '1 of 2 (6 items)');
.find('input')
.should('include.value', '1 of 2 (6 items)');
// link again // link again
ndv.actions.toggleInputRunLinking(); ndv.actions.toggleInputRunLinking();
ndv.getters.inputTbodyCell(1, 0).click(); // remove tooltip ndv.getters.inputTbodyCell(1, 0).click(); // remove tooltip
ndv.getters.outputRunSelector() ndv.getters.outputRunSelector().find('input').should('include.value', '2 of 2 (6 items)');
.find('input')
.should('include.value', '2 of 2 (6 items)');
}); });
it('should display parameter hints correctly', () => { it('should display parameter hints correctly', () => {
@ -247,14 +277,12 @@ describe('NDV', () => {
input: ' test', input: ' test',
}, },
{ {
input: ' ' input: ' ',
}, },
{ {
input: '<div></div>' input: '<div></div>',
}, },
].forEach(({ input, output }) => { ].forEach(({ input, output }) => {
if (input) { if (input) {
ndv.actions.typeIntoParameterInput('value', input); ndv.actions.typeIntoParameterInput('value', input);
} }

View file

@ -5,25 +5,35 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import type { PropType } from 'vue';
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import emitter from '@/mixins/emitter'; import type { EventBus } from 'n8n-design-system/utils';
import { createEventBus } from 'n8n-design-system/utils';
export default defineComponent({ export default defineComponent({
name: 'IntersectionObserved', name: 'IntersectionObserved',
mixins: [emitter], props: {
props: ['enabled'], enabled: {
type: Boolean,
default: false,
},
eventBus: {
type: Object as PropType<EventBus>,
default: () => createEventBus(),
},
},
mounted() { mounted() {
if (!this.enabled) { if (!this.enabled) {
return; return;
} }
this.$nextTick(() => { this.$nextTick(() => {
this.$dispatch('IntersectionObserver', 'observe', this.$refs.observed); this.eventBus.emit('observe', this.$refs.observed);
}); });
}, },
beforeDestroy() { beforeDestroy() {
if (this.enabled) { if (this.enabled) {
this.$dispatch('IntersectionObserver', 'unobserve', this.$refs.observed); this.eventBus.emit('unobserve', this.$refs.observed);
} }
}, },
}); });

View file

@ -5,11 +5,27 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import type { PropType } from 'vue';
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import type { EventBus } from 'n8n-design-system/utils';
import { createEventBus } from 'n8n-design-system/utils';
export default defineComponent({ export default defineComponent({
name: 'IntersectionObserver', name: 'IntersectionObserver',
props: ['threshold', 'enabled'], props: {
threshold: {
type: Number,
default: 0,
},
enabled: {
type: Boolean,
default: false,
},
eventBus: {
type: Object as PropType<EventBus>,
default: () => createEventBus(),
},
},
data() { data() {
return { return {
observer: null, observer: null,
@ -35,13 +51,13 @@ export default defineComponent({
}); });
}, options); }, options);
this.$data.observer = observer; this.observer = observer;
this.$on('observe', (observed: Element) => { this.eventBus.on('observe', (observed: Element) => {
observer.observe(observed); observer.observe(observed);
}); });
this.$on('unobserve', (observed: Element) => { this.eventBus.on('unobserve', (observed: Element) => {
observer.unobserve(observed); observer.unobserve(observed);
}); });
}, },

View file

@ -166,6 +166,7 @@ import type { IPermissions } from '@/permissions';
import { getWorkflowPermissions } from '@/permissions'; import { getWorkflowPermissions } from '@/permissions';
import { createEventBus } from 'n8n-design-system'; import { createEventBus } from 'n8n-design-system';
import { useCloudPlanStore } from '@/stores'; import { useCloudPlanStore } from '@/stores';
import { nodeViewEventBus } from '@/event-bus';
const hasChanged = (prev: string[], curr: string[]) => { const hasChanged = (prev: string[], curr: string[]) => {
if (prev.length !== curr.length) { if (prev.length !== curr.length) {
@ -445,7 +446,7 @@ export default defineComponent({
return; return;
} }
this.$root.$emit('importWorkflowData', { data: workflowData }); nodeViewEventBus.emit('importWorkflowData', { data: workflowData });
}; };
const inputRef = this.$refs.importFile as HTMLInputElement | undefined; const inputRef = this.$refs.importFile as HTMLInputElement | undefined;
@ -505,7 +506,7 @@ export default defineComponent({
}, },
)) as MessageBoxInputData; )) as MessageBoxInputData;
this.$root.$emit('importWorkflowUrl', { url: promptResponse.value }); nodeViewEventBus.emit('importWorkflowUrl', { url: promptResponse.value });
} catch (e) {} } catch (e) {}
break; break;
} }

View file

@ -28,6 +28,7 @@
:droppable="droppable" :droppable="droppable"
:node="node" :node="node"
:path="path" :path="path"
:event-bus="eventBus"
@input="valueChanged" @input="valueChanged"
@modalOpenerClick="openExpressionEditorModal" @modalOpenerClick="openExpressionEditorModal"
@focus="setFocus" @focus="setFocus"
@ -391,8 +392,8 @@ import { useCredentialsStore } from '@/stores/credentials.store';
import { useSettingsStore } from '@/stores/settings.store'; import { useSettingsStore } from '@/stores/settings.store';
import { htmlEditorEventBus } from '@/event-bus'; import { htmlEditorEventBus } from '@/event-bus';
import Vue from 'vue'; import Vue from 'vue';
import type { EventBus } from 'n8n-design-system/utils';
type ResourceLocatorRef = InstanceType<typeof ResourceLocator>; import { createEventBus } from 'n8n-design-system/utils';
export default defineComponent({ export default defineComponent({
name: 'parameter-input', name: 'parameter-input',
@ -463,6 +464,10 @@ export default defineComponent({
size: 'small', size: 'small',
}), }),
}, },
eventBus: {
type: Object as PropType<EventBus>,
default: () => createEventBus(),
},
}, },
data() { data() {
return { return {
@ -1112,9 +1117,7 @@ export default defineComponent({
} }
} else if (command === 'refreshOptions') { } else if (command === 'refreshOptions') {
if (this.isResourceLocatorParameter) { if (this.isResourceLocatorParameter) {
const resourceLocatorRef = this.$refs.resourceLocator as ResourceLocatorRef | undefined; this.eventBus.emit('refreshList');
resourceLocatorRef?.$emit('refreshList');
} }
void this.loadRemoteParameterOptions(); void this.loadRemoteParameterOptions();
} else if (command === 'formatHtml') { } else if (command === 'formatHtml') {
@ -1146,7 +1149,7 @@ export default defineComponent({
}); });
}, },
mounted() { mounted() {
this.$on('optionSelected', this.optionSelected); this.eventBus.on('optionSelected', this.optionSelected);
this.tempValue = this.displayValue as string; this.tempValue = this.displayValue as string;
if (this.node !== null) { if (this.node !== null) {
@ -1186,6 +1189,9 @@ export default defineComponent({
inputFieldRef: this.$refs['inputField'], inputFieldRef: this.$refs['inputField'],
}); });
}, },
beforeDestroy() {
this.eventBus.off('optionSelected', this.optionSelected);
},
}); });
</script> </script>

View file

@ -32,6 +32,7 @@
:isForCredential="true" :isForCredential="true"
:eventSource="eventSource" :eventSource="eventSource"
:hint="!showRequiredErrors ? hint : ''" :hint="!showRequiredErrors ? hint : ''"
:event-bus="eventBus"
@focus="onFocus" @focus="onFocus"
@blur="onBlur" @blur="onBlur"
@textInput="valueChanged" @textInput="valueChanged"
@ -65,6 +66,7 @@ import { isValueExpression } from '@/utils';
import type { INodeParameterResourceLocator, INodeProperties, IParameterLabel } from 'n8n-workflow'; import type { INodeParameterResourceLocator, INodeProperties, IParameterLabel } from 'n8n-workflow';
import { mapStores } from 'pinia'; import { mapStores } from 'pinia';
import { useWorkflowsStore } from '@/stores/workflows.store'; import { useWorkflowsStore } from '@/stores/workflows.store';
import { createEventBus } from 'n8n-design-system/utils';
type ParamRef = InstanceType<typeof ParameterInputWrapper>; type ParamRef = InstanceType<typeof ParameterInputWrapper>;
@ -100,6 +102,7 @@ export default defineComponent({
focused: false, focused: false,
blurredEver: false, blurredEver: false,
menuExpanded: false, menuExpanded: false,
eventBus: createEventBus(),
}; };
}, },
computed: { computed: {
@ -147,9 +150,7 @@ export default defineComponent({
this.menuExpanded = expanded; this.menuExpanded = expanded;
}, },
optionSelected(command: string) { optionSelected(command: string) {
if (this.$refs.param) { this.eventBus.emit('optionSelected', command);
(this.$refs.param as ParamRef).$emit('optionSelected', command);
}
}, },
valueChanged(parameterData: IUpdateInformation) { valueChanged(parameterData: IUpdateInformation) {
this.$emit('change', parameterData); this.$emit('change', parameterData);

View file

@ -56,6 +56,7 @@
:hint="hint" :hint="hint"
:hide-issues="hideIssues" :hide-issues="hideIssues"
:label="label" :label="label"
:event-bus="eventBus"
@valueChanged="valueChanged" @valueChanged="valueChanged"
@textInput="onTextInput" @textInput="onTextInput"
@focus="onFocus" @focus="onFocus"
@ -98,6 +99,7 @@ import { useNDVStore } from '@/stores/ndv.store';
import { useSegment } from '@/stores/segment.store'; import { useSegment } from '@/stores/segment.store';
import { externalHooks } from '@/mixins/externalHooks'; import { externalHooks } from '@/mixins/externalHooks';
import { getMappedResult } from '@/utils/mappingUtils'; import { getMappedResult } from '@/utils/mappingUtils';
import { createEventBus } from 'n8n-design-system/utils';
type ParameterInputWrapperRef = InstanceType<typeof ParameterInputWrapper>; type ParameterInputWrapperRef = InstanceType<typeof ParameterInputWrapper>;
@ -112,7 +114,10 @@ export default defineComponent({
ParameterInputWrapper, ParameterInputWrapper,
}, },
setup() { setup() {
const eventBus = createEventBus();
return { return {
eventBus,
...useToast(), ...useToast(),
}; };
}, },
@ -234,17 +239,14 @@ export default defineComponent({
this.menuExpanded = expanded; this.menuExpanded = expanded;
}, },
optionSelected(command: string) { optionSelected(command: string) {
const paramRef = this.$refs.param as ParameterInputWrapperRef | undefined; this.eventBus.emit('optionSelected', command);
paramRef?.$emit('optionSelected', command);
}, },
valueChanged(parameterData: IUpdateInformation) { valueChanged(parameterData: IUpdateInformation) {
this.$emit('valueChanged', parameterData); this.$emit('valueChanged', parameterData);
}, },
onTextInput(parameterData: IUpdateInformation) { onTextInput(parameterData: IUpdateInformation) {
const paramRef = this.$refs.param as ParameterInputWrapperRef | undefined;
if (isValueExpression(this.parameter, parameterData.value)) { if (isValueExpression(this.parameter, parameterData.value)) {
paramRef?.$emit('optionSelected', 'addExpression'); this.eventBus.emit('optionSelected', 'addExpression');
} }
}, },
onDrop(newParamValue: string) { onDrop(newParamValue: string) {

View file

@ -18,6 +18,7 @@
:expressionEvaluated="expressionValueComputed" :expressionEvaluated="expressionValueComputed"
:label="label" :label="label"
:data-test-id="`parameter-input-${parameter.name}`" :data-test-id="`parameter-input-${parameter.name}`"
:event-bus="internalEventBus"
@focus="onFocus" @focus="onFocus"
@blur="onBlur" @blur="onBlur"
@drop="onDrop" @drop="onDrop"
@ -61,8 +62,8 @@ import type { INodeUi, IUpdateInformation, TargetItem } from '@/Interface';
import { workflowHelpers } from '@/mixins/workflowHelpers'; import { workflowHelpers } from '@/mixins/workflowHelpers';
import { isValueExpression } from '@/utils'; import { isValueExpression } from '@/utils';
import { useNDVStore } from '@/stores/ndv.store'; import { useNDVStore } from '@/stores/ndv.store';
import type { EventBus } from 'n8n-design-system/utils';
type ParamRef = InstanceType<typeof ParameterInput>; import { createEventBus } from 'n8n-design-system/utils';
export default defineComponent({ export default defineComponent({
name: 'parameter-input-wrapper', name: 'parameter-input-wrapper',
@ -71,8 +72,16 @@ export default defineComponent({
ParameterInput, ParameterInput,
InputHint, InputHint,
}, },
data() {
return {
internalEventBus: createEventBus(),
};
},
mounted() { mounted() {
this.$on('optionSelected', this.optionSelected); this.eventBus.on('optionSelected', this.optionSelected);
},
beforeDestroy() {
this.eventBus.off('optionSelected', this.optionSelected);
}, },
props: { props: {
isReadOnly: { isReadOnly: {
@ -124,6 +133,10 @@ export default defineComponent({
size: 'small', size: 'small',
}), }),
}, },
eventBus: {
type: Object as PropType<EventBus>,
default: () => createEventBus(),
},
}, },
computed: { computed: {
...mapStores(useNDVStore), ...mapStores(useNDVStore),
@ -217,9 +230,7 @@ export default defineComponent({
this.$emit('drop', data); this.$emit('drop', data);
}, },
optionSelected(command: string) { optionSelected(command: string) {
const paramRef = this.$refs.param as ParamRef | undefined; this.internalEventBus.emit('optionSelected', command);
paramRef?.$emit('optionSelected', command);
}, },
onValueChanged(parameterData: IUpdateInformation) { onValueChanged(parameterData: IUpdateInformation) {
this.$emit('valueChanged', parameterData); this.$emit('valueChanged', parameterData);

View file

@ -15,11 +15,11 @@
:hasMore="currentQueryHasMore" :hasMore="currentQueryHasMore"
:errorView="currentQueryError" :errorView="currentQueryError"
:width="width" :width="width"
:event-bus="eventBus"
@input="onListItemSelected" @input="onListItemSelected"
@hide="onDropdownHide" @hide="onDropdownHide"
@filter="onSearchFilter" @filter="onSearchFilter"
@loadMore="loadResourcesDebounced" @loadMore="loadResourcesDebounced"
ref="dropdown"
> >
<template #error> <template #error>
<div :class="$style.error" data-test-id="rlc-error-container"> <div :class="$style.error" data-test-id="rlc-error-container">
@ -176,8 +176,8 @@ import { useWorkflowsStore } from '@/stores/workflows.store';
import { useRootStore } from '@/stores/n8nRoot.store'; import { useRootStore } from '@/stores/n8nRoot.store';
import { useNDVStore } from '@/stores/ndv.store'; import { useNDVStore } from '@/stores/ndv.store';
import { useNodeTypesStore } from '@/stores/nodeTypes.store'; import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { createEventBus } from 'n8n-design-system/utils';
type ResourceLocatorDropdownRef = InstanceType<typeof ResourceLocatorDropdown>; import type { EventBus } from 'n8n-design-system/utils';
interface IResourceLocatorQuery { interface IResourceLocatorQuery {
results: INodeListSearchItems[]; results: INodeListSearchItems[];
@ -256,6 +256,10 @@ export default defineComponent({
loadOptionsMethod: { loadOptionsMethod: {
type: String, type: String,
}, },
eventBus: {
type: Object as PropType<EventBus>,
default: () => createEventBus(),
},
}, },
data() { data() {
return { return {
@ -475,17 +479,20 @@ export default defineComponent({
}, },
}, },
mounted() { mounted() {
this.$on('refreshList', this.refreshList); this.eventBus.on('refreshList', this.refreshList);
window.addEventListener('resize', this.setWidth); window.addEventListener('resize', this.setWidth);
useNDVStore().$subscribe((mutation, state) => { useNDVStore().$subscribe((mutation, state) => {
// Update the width when main panel dimension change // Update the width when main panel dimension change
this.setWidth(); this.setWidth();
}); });
setTimeout(() => { setTimeout(() => {
this.setWidth(); this.setWidth();
}, 0); }, 0);
}, },
beforeDestroy() { beforeDestroy() {
this.eventBus.off('refreshList', this.refreshList);
window.removeEventListener('resize', this.setWidth); window.removeEventListener('resize', this.setWidth);
}, },
methods: { methods: {
@ -510,9 +517,8 @@ export default defineComponent({
this.trackEvent('User refreshed resource locator list'); this.trackEvent('User refreshed resource locator list');
}, },
onKeyDown(e: MouseEvent) { onKeyDown(e: MouseEvent) {
const dropdownRef = this.$refs.dropdown as ResourceLocatorDropdownRef | undefined; if (this.showResourceDropdown && !this.isSearchable) {
if (dropdownRef && this.showResourceDropdown && !this.isSearchable) { this.eventBus.emit('keyDown', e);
dropdownRef.$emit('keyDown', e);
} }
}, },
openResource(url: string) { openResource(url: string) {

View file

@ -82,6 +82,8 @@
import type { IResourceLocatorResultExpanded } from '@/Interface'; import type { IResourceLocatorResultExpanded } from '@/Interface';
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import type { PropType } from 'vue'; import type { PropType } from 'vue';
import type { EventBus } from 'n8n-design-system/utils';
import { createEventBus } from 'n8n-design-system/utils';
const SEARCH_BAR_HEIGHT_PX = 40; const SEARCH_BAR_HEIGHT_PX = 40;
const SCROLL_MARGIN_PX = 10; const SCROLL_MARGIN_PX = 10;
@ -120,6 +122,10 @@ export default defineComponent({
width: { width: {
type: Number, type: Number,
}, },
eventBus: {
type: Object as PropType<EventBus>,
default: () => createEventBus(),
},
}, },
data() { data() {
return { return {
@ -128,7 +134,10 @@ export default defineComponent({
}; };
}, },
mounted() { mounted() {
this.$on('keyDown', this.onKeyDown); this.eventBus.on('keyDown', this.onKeyDown);
},
beforeDestroy() {
this.eventBus.off('keyDown', this.onKeyDown);
}, },
computed: { computed: {
sortedResources(): IResourceLocatorResultExpanded[] { sortedResources(): IResourceLocatorResultExpanded[] {

View file

@ -4,6 +4,7 @@
@observed="onObserved" @observed="onObserved"
class="tags-container" class="tags-container"
:enabled="responsive" :enabled="responsive"
:event-bus="intersectionEventBus"
> >
<template> <template>
<span class="tags"> <span class="tags">
@ -26,6 +27,7 @@
:class="{ hidden: tag.hidden }" :class="{ hidden: tag.hidden }"
:data-id="tag.id" :data-id="tag.id"
:enabled="responsive" :enabled="responsive"
:event-bus="intersectionEventBus"
v-else v-else
> >
<el-tag :title="tag.name" type="info" size="small" :class="{ hoverable }"> <el-tag :title="tag.name" type="info" size="small" :class="{ hoverable }">
@ -46,6 +48,7 @@ import IntersectionObserver from './IntersectionObserver.vue';
import IntersectionObserved from './IntersectionObserved.vue'; import IntersectionObserved from './IntersectionObserved.vue';
import { mapStores } from 'pinia'; import { mapStores } from 'pinia';
import { useTagsStore } from '@/stores/tags.store'; import { useTagsStore } from '@/stores/tags.store';
import { createEventBus } from 'n8n-design-system/utils';
// random upper limit if none is set to minimize performance impact of observers // random upper limit if none is set to minimize performance impact of observers
const DEFAULT_MAX_TAGS_LIMIT = 20; const DEFAULT_MAX_TAGS_LIMIT = 20;
@ -62,6 +65,7 @@ export default defineComponent({
props: ['tagIds', 'limit', 'clickable', 'responsive', 'hoverable'], props: ['tagIds', 'limit', 'clickable', 'responsive', 'hoverable'],
data() { data() {
return { return {
intersectionEventBus: createEventBus(),
visibility: {} as { [id: string]: boolean }, visibility: {} as { [id: string]: boolean },
}; };
}, },

View file

@ -3,13 +3,14 @@
* unsafe onclick attribute * unsafe onclick attribute
*/ */
import { reactive, del, computed, onMounted, onUnmounted, getCurrentInstance } from 'vue'; import { reactive, del, computed, onMounted, onUnmounted, getCurrentInstance } from 'vue';
import { globalLinkActionsEventBus } from '@/event-bus';
const state = reactive({ const state = reactive({
customActions: {} as Record<string, Function>, customActions: {} as Record<string, Function>,
}); });
export default () => { export default () => {
function registerCustomAction(key: string, action: Function) { function registerCustomAction({ key, action }: { key: string; action: Function }) {
state.customActions[key] = action; state.customActions[key] = action;
} }
function unregisterCustomAction(key: string) { function unregisterCustomAction(key: string) {
@ -42,13 +43,15 @@ export default () => {
onMounted(() => { onMounted(() => {
const instance = getCurrentInstance(); const instance = getCurrentInstance();
window.addEventListener('click', delegateClick); window.addEventListener('click', delegateClick);
instance?.proxy.$root.$on('registerGlobalLinkAction', registerCustomAction);
globalLinkActionsEventBus.on('registerGlobalLinkAction', registerCustomAction);
}); });
onUnmounted(() => { onUnmounted(() => {
const instance = getCurrentInstance(); const instance = getCurrentInstance();
window.removeEventListener('click', delegateClick); window.removeEventListener('click', delegateClick);
instance?.proxy.$root.$off('registerGlobalLinkAction', registerCustomAction);
globalLinkActionsEventBus.off('registerGlobalLinkAction', registerCustomAction);
}); });
return { return {

View file

@ -1,4 +1,5 @@
export * from './code-node-editor'; export * from './code-node-editor';
export * from './data-pinning'; export * from './data-pinning';
export * from './link-actions';
export * from './html-editor'; export * from './html-editor';
export * from './node-view'; export * from './node-view';

View file

@ -0,0 +1,3 @@
import { createEventBus } from 'n8n-design-system';
export const globalLinkActionsEventBus = createEventBus();

View file

@ -1,50 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { defineComponent } from 'vue';
function broadcast(
this: InstanceType<typeof EmitterMixin>,
componentName: string,
eventName: string,
params: any,
) {
this.$children.forEach((child) => {
const name = child.$options.name;
if (name === componentName) {
// eslint-disable-next-line prefer-spread
child.$emit.apply(child, [eventName].concat(params) as Parameters<typeof child.$emit>);
} else {
broadcast.apply(
child as InstanceType<typeof EmitterMixin>,
[componentName, eventName].concat([params]) as Parameters<typeof broadcast>,
);
}
});
}
const EmitterMixin = defineComponent({
methods: {
$dispatch(componentName: string, eventName: string, params: any) {
let parent = this.$parent || this.$root;
let name = parent.$options.name;
while (parent && (!name || name !== componentName)) {
parent = parent.$parent as InstanceType<typeof EmitterMixin>;
if (parent) {
name = parent.$options.name;
}
}
if (parent) {
// eslint-disable-next-line prefer-spread
parent.$emit.apply(parent, [eventName].concat(params) as Parameters<typeof parent.$emit>);
}
},
$broadcast(componentName: string, eventName: string, params: any) {
broadcast.call(this, componentName, eventName, params);
},
},
});
export default EmitterMixin;

View file

@ -24,7 +24,7 @@ import { TelemetryHelpers } from 'n8n-workflow';
import { WORKFLOW_SETTINGS_MODAL_KEY } from '@/constants'; import { WORKFLOW_SETTINGS_MODAL_KEY } from '@/constants';
import { getTriggerNodeServiceName } from '@/utils'; import { getTriggerNodeServiceName } from '@/utils';
import { codeNodeEditorEventBus } from '@/event-bus'; import { codeNodeEditorEventBus, globalLinkActionsEventBus } from '@/event-bus';
import { mapStores } from 'pinia'; import { mapStores } from 'pinia';
import { useUIStore } from '@/stores/ui.store'; import { useUIStore } from '@/stores/ui.store';
import { useWorkflowsStore } from '@/stores/workflows.store'; import { useWorkflowsStore } from '@/stores/workflows.store';
@ -351,9 +351,12 @@ export const pushConnection = defineComponent({
let action; let action;
if (!isSavingExecutions) { if (!isSavingExecutions) {
this.$root.$emit('registerGlobalLinkAction', 'open-settings', async () => { globalLinkActionsEventBus.emit('registerGlobalLinkAction', {
key: 'open-settings',
action: async () => {
if (this.workflowsStore.isNewWorkflow) await this.saveAsNewWorkflow(); if (this.workflowsStore.isNewWorkflow) await this.saveAsNewWorkflow();
this.uiStore.openModal(WORKFLOW_SETTINGS_MODAL_KEY); this.uiStore.openModal(WORKFLOW_SETTINGS_MODAL_KEY);
},
}); });
action = action =

View file

@ -672,9 +672,12 @@ export default defineComponent({
? this.$locale.baseText('nodeView.addOrEnableTriggerNode') ? this.$locale.baseText('nodeView.addOrEnableTriggerNode')
: this.$locale.baseText('nodeView.addATriggerNodeFirst'); : this.$locale.baseText('nodeView.addATriggerNodeFirst');
this.registerCustomAction('showNodeCreator', () => this.registerCustomAction({
key: 'showNodeCreator',
action: () =>
this.showTriggerCreator(NODE_CREATOR_OPEN_SOURCES.NO_TRIGGER_EXECUTION_TOOLTIP), this.showTriggerCreator(NODE_CREATOR_OPEN_SOURCES.NO_TRIGGER_EXECUTION_TOOLTIP),
); });
const notice = this.showMessage({ const notice = this.showMessage({
type: 'info', type: 'info',
title: this.$locale.baseText('nodeView.cantExecuteNoTrigger'), title: this.$locale.baseText('nodeView.cantExecuteNoTrigger'),
@ -1050,7 +1053,7 @@ export default defineComponent({
} }
if (this.$router.currentRoute.name === VIEWS.NEW_WORKFLOW) { if (this.$router.currentRoute.name === VIEWS.NEW_WORKFLOW) {
this.$root.$emit('newWorkflow'); nodeViewEventBus.emit('newWorkflow');
} else { } else {
void this.$router.push({ name: VIEWS.NEW_WORKFLOW }); void this.$router.push({ name: VIEWS.NEW_WORKFLOW });
} }
@ -3913,9 +3916,9 @@ export default defineComponent({
window.addEventListener('message', this.onPostMessageReceived); window.addEventListener('message', this.onPostMessageReceived);
window.addEventListener('pageshow', this.onPageShow); window.addEventListener('pageshow', this.onPageShow);
this.$root.$on('newWorkflow', this.newWorkflow); nodeViewEventBus.on('newWorkflow', this.newWorkflow);
this.$root.$on('importWorkflowData', this.onImportWorkflowDataEvent); nodeViewEventBus.on('importWorkflowData', this.onImportWorkflowDataEvent);
this.$root.$on('importWorkflowUrl', this.onImportWorkflowUrlEvent); nodeViewEventBus.on('importWorkflowUrl', this.onImportWorkflowUrlEvent);
historyBus.on('nodeMove', this.onMoveNode); historyBus.on('nodeMove', this.onMoveNode);
historyBus.on('revertAddNode', this.onRevertAddNode); historyBus.on('revertAddNode', this.onRevertAddNode);
historyBus.on('revertRemoveNode', this.onRevertRemoveNode); historyBus.on('revertRemoveNode', this.onRevertRemoveNode);
@ -3938,9 +3941,9 @@ export default defineComponent({
window.removeEventListener('beforeunload', this.onBeforeUnload); window.removeEventListener('beforeunload', this.onBeforeUnload);
window.removeEventListener('pageshow', this.onPageShow); window.removeEventListener('pageshow', this.onPageShow);
this.$root.$off('newWorkflow', this.newWorkflow); nodeViewEventBus.off('newWorkflow', this.newWorkflow);
this.$root.$off('importWorkflowData', this.onImportWorkflowDataEvent); nodeViewEventBus.off('importWorkflowData', this.onImportWorkflowDataEvent);
this.$root.$off('importWorkflowUrl', this.onImportWorkflowUrlEvent); nodeViewEventBus.off('importWorkflowUrl', this.onImportWorkflowUrlEvent);
historyBus.off('nodeMove', this.onMoveNode); historyBus.off('nodeMove', this.onMoveNode);
historyBus.off('revertAddNode', this.onRevertAddNode); historyBus.off('revertAddNode', this.onRevertAddNode);
historyBus.off('revertRemoveNode', this.onRevertRemoveNode); historyBus.off('revertRemoveNode', this.onRevertRemoveNode);
@ -3959,9 +3962,9 @@ export default defineComponent({
this.instance.destroy(); this.instance.destroy();
this.uiStore.stateIsDirty = false; this.uiStore.stateIsDirty = false;
window.removeEventListener('message', this.onPostMessageReceived); window.removeEventListener('message', this.onPostMessageReceived);
this.$root.$off('newWorkflow', this.newWorkflow); nodeViewEventBus.off('newWorkflow', this.newWorkflow);
this.$root.$off('importWorkflowData', this.onImportWorkflowDataEvent); nodeViewEventBus.off('importWorkflowData', this.onImportWorkflowDataEvent);
this.$root.$off('importWorkflowUrl', this.onImportWorkflowUrlEvent); nodeViewEventBus.off('importWorkflowUrl', this.onImportWorkflowUrlEvent);
this.workflowsStore.setWorkflowId(PLACEHOLDER_EMPTY_WORKFLOW_ID); this.workflowsStore.setWorkflowId(PLACEHOLDER_EMPTY_WORKFLOW_ID);
}, },
}); });