feat(editor): Migrate Design System and Editor UI to Vue 3 (#6476)
* feat: remove vue-fragment (no-changelog) * feat: partial design-system migration * feat: migrate info-accordion and info-tip components * feat: migrate several components to vue 3 * feat: migrated several components * feat: migrate several components * feat: migrate several components * feat: migrate several components * feat: re-exported all design system components * fix: fix design for popper components * fix: editor kind of working, lots of issues to fix * fix: fix several vue 3 migration issues * fix: replace @change with @update:modelValue in several places * fix: fix translation linking * fix: fix inline-edit input * fix: fix ndv and dialog design * fix: update parameter input event bindings * fix: rename deprecated lifecycle methods * fix: fix json view mapping * build: update lock file * fix(editor): revisit last conflict with master and fix issues * fix(editor): revisit last conflict with master and fix issues * fix: fix expression editor bug causing code mirror to no longer be reactive * fix: fix resource locator bug * fix: fix vue-agile integration * fix: remove global import for vue-agile * fix: replace element-plus buttons with n8n-buttons everywhere * fix(editor): Fix various element-plus styles (#6571) * fix(editor): Fix various element-plus styles Signed-off-by: Oleg Ivaniv <me@olegivaniv.com> * Remove debugging code Signed-off-by: Oleg Ivaniv <me@olegivaniv.com> * Address PR comments Signed-off-by: Oleg Ivaniv <me@olegivaniv.com> --------- Signed-off-by: Oleg Ivaniv <me@olegivaniv.com> * fix(editor): Fix loading in production mode [Vue 3] (#6578) Signed-off-by: Oleg Ivaniv <me@olegivaniv.com> * fix(editor): First round of e2e tests fixes with Vue 3 (#6579) * fix(editor): Fix broken smoke and workflow list e2e tests * ✔️ Fix failing canvas action tests. Updating some selectors used in credentials and workflow tests * feat: add vue 3 eslint rules and fix issues * fix: fix tags-dropdown * fix: fix white-space issues caused by i18n-t * fix: rename non-generic click events * fix: fix search in resources list layout * fix: fix datatable paginator * fix: fix popper select caret and dropdown size * fix: add width to action-dropdown * fix: fix workflow settings icon not being hidden * fix: refactor newly added code * fix: fix merge issue * fix: fix ndv credentials watcher * fix: fix workflow saving and grabber notch * fix: fix nodes list panel transition * fix: fix node title visibility * fix: fix data unpinning * fix: fix value access * fix: show input panel only if trigger panel enabled or not trigger node * fix: fix tags dropdown and executions status spcing * fix(editor): Prevent execution list to load back when leaving the route (#6697) fix(editor): prevent execution list to load back when leaving the route * fix: fix drawer visibility * fix: fix expression toggle padding * fix: fix expressions editor styling * chore: prepare for testing * fix: fix styling for el-button without patching * test: fix unit tests in design-system * test: fix most unit tests * fix: remove import cycle. * fix: fix personalization modal tests * fix further resource mapper test adjustments * fix: fix multiple tests and n8n-route attr duplication * fix: fix source control tets * fix: fixed remaining unit tests * fix: fix workflows and credentials e2e tests * fix: fix localizeNodeNames * fix: update ndv e2e tests * fix: fix popper left placement arrow * fix: fix 5-ndv e2e tests * fix: fix 6-code-node e2e tests * fix(editor): Drop click outside directive from NodeCreator (#6716) * fix(editor): Drop click outside directive from NodeCreator * fix(editor): make sure mouseup outside is unbound at least before the component is unmounted * fix: fix 10-settings-log-streaming e2e tests * fix: fix node redrawing * fix: fix tooltip buttons styling * fix: fix varous e2e suites * fix: fix 15-scheduler-node e2e suite * fix: fix route watcher * fix: fixed param name update and credential edit * feat: update event names * refactor: Remove deprecated `$data` (#6576) Co-authored-by: Alex Grozav <alex@grozav.com> * fix: fix 17-sharing e2e suite * fix: fix tags dropdown * fix: fix tags manager * fix(editor): move :deep selectors to a separate scoped style block * fix: fix sticky component and inline text edit * fix: update e2e tests * fix: remove button override references * fix(editor): Adjust spacing in templates for Vue 3 (#6744) * fix(editor): Adjust spacing in templates * fix: Undo unneeded change * fix: Undo unneeded change * fix(editor): Adjust NDV height for Vue 3 (#6742) fix(editor): Adjust NDV height * fix(editor): Restore collapsed sidebar items for Vue 3 (#6743) fix(editor): Restore collapsed sidebar items * fix: fix linting issues * fix: fix design-system deps * fix: post-merge fixes * fix: update tests * fix: increase timeout for executionslist tets * chore: fix linting issue * fix: fix 14-mapping e2e tests in ci * fix: re-enable tests * fix: fix workflow duplication e2e tests after tags update * fix(editor): Change component prop to be typed * fix: fix tags dropdown in duplicate wf modal * fix: fix focus behaviour in tags selector * fix: fix tag creation * fix: fix log streaming e2e race condition * fix(editor): Fix Vue 3 linting issues (#6748) * fix(editor): Fix Vue 3 linting issues Signed-off-by: Oleg Ivaniv <me@olegivaniv.com> * fix MainSidebar linter issues * revert pnpm lock * update pnpm lock file --------- Signed-off-by: Oleg Ivaniv <me@olegivaniv.com> Co-authored-by: Alex Grozav <alex@grozav.com> * fix(editor): Some css fixes for vue3 branch (#6749) * ✨ Fixing filter button height * ✨ Update input modal button position * ✨ Updating tags styling * ✨ Fix event logging settings spacing * 👕 Fixing lint errors * fix: fix linting issues * Revert to `// eslint-disable-next-line @typescript-eslint/no-misused-promises` disabling of mixins init Signed-off-by: Oleg Ivaniv <me@olegivaniv.com> * fix: fix css issue * fix(editor): Lint fix * fix(editor): Fix settings initialisation (#6750) Signed-off-by: Oleg Ivaniv <me@olegivaniv.com> * fix: fix initial settings loading * fix: replace realClick with click force * fix: fix randomly failing mapping e2e tests * fix(editor): Fix menu item event handling * fix: fix resource filters dropdown events (#6752) * fix: fix resource filters dropdown events * fix: remove teleported:false * fix: fix event selection event naming (#6753) * fix: removed console.log (#6754) * fix: rever await nextTick changes * fix: redo linting changes * fix(editor): Redraw node connections if adding more than one node to canvas (#6755) * fix(editor): Redraw node connections if adding more than one node to canvas Signed-off-by: Oleg Ivaniv <me@olegivaniv.com> * Update position before connection two nodes * Lint fix --------- Signed-off-by: Oleg Ivaniv <me@olegivaniv.com> Co-authored-by: Alex Grozav <alex@grozav.com> * fix(editor): Fix `ResourceMapper` unit tests (#6758) * ✔️ Fix matching columns test * ✔️ Fix multiple matching columns test * ✔️ Removing `skip` from the last test * fix: Allow pasting a big workflow (#6760) * fix: pasting a big workflow * chore: update comment * refactor: move try/catch to function * refactor: move try/catch to function * fix(editor): Fix modal layer width * fix: fix position changes * fix: undo it.only * fix: make undo/redo multiple steps more verbose * fix: Fix value survey styles (#6764) * fix: fix value survey styles * fix: lint * Revert "fix: lint" 72869c431f1448861df021be041b61c62f1e3118 * fix: lint * fix(editor): Fix collapsed sub menu * fix: Fix drawer animation (#6767) fix: drawer animation * fix(editor): Fix source control buttons (#6769) * fix(editor): Fix App loading & auth (#6768) * fix(editor): Fix App loading & auth Signed-off-by: Oleg Ivaniv <me@olegivaniv.com> * Await promises Signed-off-by: Oleg Ivaniv <me@olegivaniv.com> * Fix eslint error Signed-off-by: Oleg Ivaniv <me@olegivaniv.com> --------- Signed-off-by: Oleg Ivaniv <me@olegivaniv.com> --------- Signed-off-by: Oleg Ivaniv <me@olegivaniv.com> Co-authored-by: Csaba Tuncsik <csaba@n8n.io> Co-authored-by: OlegIvaniv <me@olegivaniv.com> Co-authored-by: Milorad FIlipović <milorad@n8n.io> Co-authored-by: Iván Ovejero <ivov.src@gmail.com> Co-authored-by: Mutasem Aldmour <4711238+mutdmour@users.noreply.github.com>
This commit is contained in:
@@ -21,7 +21,7 @@ const categoryName = computed(() => {
|
||||
<template>
|
||||
<div
|
||||
:class="$style.categoryWrapper"
|
||||
v-on="$listeners"
|
||||
v-bind="$attrs"
|
||||
data-keyboard-nav="true"
|
||||
data-test-id="node-creator-category-item"
|
||||
>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<p
|
||||
:class="$style.communityNodeIcon"
|
||||
v-html="
|
||||
$locale.baseText('generic.communityNode.tooltip', {
|
||||
i18n.baseText('generic.communityNode.tooltip', {
|
||||
interpolate: {
|
||||
packageName: nodeType.name.split('.')[0],
|
||||
docURL: COMMUNITY_NODES_INSTALLATION_DOCS_URL,
|
||||
@@ -38,7 +38,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, getCurrentInstance } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import type { SimplifiedNodeType } from '@/Interface';
|
||||
import { COMMUNITY_NODES_INSTALLATION_DOCS_URL, DEFAULT_SUBCATEGORY } from '@/constants';
|
||||
|
||||
@@ -48,6 +48,7 @@ import { useNodeCreatorStore } from '@/stores/nodeCreator.store';
|
||||
import NodeIcon from '@/components/NodeIcon.vue';
|
||||
|
||||
import { useActions } from '../composables/useActions';
|
||||
import { useI18n, useTelemetry } from '@/composables';
|
||||
|
||||
export interface Props {
|
||||
nodeType: SimplifiedNodeType;
|
||||
@@ -59,16 +60,18 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
active: false,
|
||||
});
|
||||
|
||||
const i18n = useI18n();
|
||||
const telemetry = useTelemetry();
|
||||
|
||||
const { actions } = useNodeCreatorStore();
|
||||
const { getNodeTypesWithManualTrigger } = useActions();
|
||||
const instance = getCurrentInstance();
|
||||
|
||||
const dragging = ref(false);
|
||||
const draggablePosition = ref({ x: -100, y: -100 });
|
||||
const draggableDataTransfer = ref(null as Element | null);
|
||||
|
||||
const description = computed<string>(() => {
|
||||
return instance?.proxy.$locale.headerText({
|
||||
return i18n.headerText({
|
||||
key: `headers.${shortNodeType.value}.description`,
|
||||
fallback: props.nodeType.description,
|
||||
}) as string;
|
||||
@@ -84,9 +87,7 @@ const nodeActions = computed(() => {
|
||||
return nodeActions;
|
||||
});
|
||||
|
||||
const shortNodeType = computed<string>(
|
||||
() => instance?.proxy.$locale.shortNodeType(props.nodeType.name) || '',
|
||||
);
|
||||
const shortNodeType = computed<string>(() => i18n.shortNodeType(props.nodeType.name) || '');
|
||||
|
||||
const draggableStyle = computed<{ top: string; left: string }>(() => ({
|
||||
top: `${draggablePosition.value.y}px`,
|
||||
@@ -99,7 +100,7 @@ const isCommunityNode = computed<boolean>(() => isCommunityPackageName(props.nod
|
||||
const displayName = computed<any>(() => {
|
||||
const displayName = props.nodeType.displayName.trimEnd();
|
||||
|
||||
return instance?.proxy.$locale.headerText({
|
||||
return i18n.headerText({
|
||||
key: `headers.${shortNodeType.value}.displayName`,
|
||||
fallback: hasActions.value ? displayName.replace('Trigger', '') : displayName,
|
||||
});
|
||||
@@ -153,7 +154,7 @@ function onDragEnd(event: DragEvent): void {
|
||||
|
||||
function onCommunityNodeTooltipClick(event: MouseEvent) {
|
||||
if ((event.target as Element).localName === 'a') {
|
||||
instance?.proxy.$telemetry.track('user clicked cnr docs link', { source: 'nodes panel node' });
|
||||
telemetry.track('user clicked cnr docs link', { source: 'nodes panel node' });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<n8n-node-creator-node
|
||||
:class="$style.subCategory"
|
||||
:title="$locale.baseText(`nodeCreator.subcategoryNames.${subcategoryName}`)"
|
||||
:title="i18n.baseText(`nodeCreator.subcategoryNames.${subcategoryName}`)"
|
||||
:isTrigger="false"
|
||||
:description="$locale.baseText(`nodeCreator.subcategoryDescriptions.${subcategoryName}`)"
|
||||
:description="i18n.baseText(`nodeCreator.subcategoryDescriptions.${subcategoryName}`)"
|
||||
:showActionArrow="true"
|
||||
>
|
||||
<template #icon>
|
||||
@@ -16,11 +16,14 @@
|
||||
import type { SubcategoryItemProps } from '@/Interface';
|
||||
import { camelCase } from 'lodash-es';
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from '@/composables';
|
||||
|
||||
export interface Props {
|
||||
item: SubcategoryItemProps;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const i18n = useI18n();
|
||||
const subcategoryName = computed(() => camelCase(props.item.subcategory || props.item.title));
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, getCurrentInstance, onMounted, defineComponent } from 'vue';
|
||||
import type { VNode, PropType } from 'vue';
|
||||
import { computed, getCurrentInstance, onMounted, defineComponent, h } from 'vue';
|
||||
import type { PropType } from 'vue';
|
||||
import type {
|
||||
INodeCreateElement,
|
||||
ActionTypeDescription,
|
||||
@@ -205,15 +205,15 @@ const OrderSwitcher = defineComponent({
|
||||
type: String as PropType<NodeFilterType>,
|
||||
},
|
||||
},
|
||||
render(h): VNode {
|
||||
const triggers = this.$slots?.triggers?.[0];
|
||||
const actions = this.$slots?.actions?.[0];
|
||||
|
||||
return h(
|
||||
'div',
|
||||
{},
|
||||
this.rootView === REGULAR_NODE_CREATOR_VIEW ? [actions, triggers] : [triggers, actions],
|
||||
);
|
||||
setup(props, { slots }) {
|
||||
return () =>
|
||||
h(
|
||||
'div',
|
||||
{},
|
||||
props.rootView === REGULAR_NODE_CREATOR_VIEW
|
||||
? [slots.actions?.(), slots.triggers?.()]
|
||||
: [slots.triggers?.(), slots.actions?.()],
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -236,32 +236,30 @@ onMounted(() => {
|
||||
@selected="onSelected"
|
||||
>
|
||||
<!-- Empty state -->
|
||||
<template #empty>
|
||||
<template v-if="hasNoTriggerActions">
|
||||
<n8n-callout
|
||||
theme="info"
|
||||
iconless
|
||||
slim
|
||||
data-test-id="actions-panel-no-triggers-callout"
|
||||
>
|
||||
<span
|
||||
v-html="
|
||||
$locale.baseText('nodeCreator.actionsCallout.noTriggerItems', {
|
||||
interpolate: { nodeName: subcategory },
|
||||
})
|
||||
"
|
||||
/>
|
||||
</n8n-callout>
|
||||
<ItemsRenderer @selected="onSelected" :elements="placeholderTriggerActions" />
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<p
|
||||
:class="$style.resetSearch"
|
||||
v-html="$locale.baseText('nodeCreator.actionsCategory.noMatchingTriggers')"
|
||||
@click="resetSearch"
|
||||
<template #empty v-if="hasNoTriggerActions">
|
||||
<n8n-callout
|
||||
v-if="hasNoTriggerActions"
|
||||
theme="info"
|
||||
iconless
|
||||
slim
|
||||
data-test-id="actions-panel-no-triggers-callout"
|
||||
>
|
||||
<span
|
||||
v-html="
|
||||
$locale.baseText('nodeCreator.actionsCallout.noTriggerItems', {
|
||||
interpolate: { nodeName: subcategory },
|
||||
})
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
</n8n-callout>
|
||||
<ItemsRenderer @selected="onSelected" :elements="placeholderTriggerActions" />
|
||||
</template>
|
||||
<template #empty v-else>
|
||||
<p
|
||||
:class="$style.resetSearch"
|
||||
v-html="$locale.baseText('nodeCreator.actionsCategory.noMatchingTriggers')"
|
||||
@click="resetSearch"
|
||||
/>
|
||||
</template>
|
||||
</CategorizedItemsRenderer>
|
||||
</template>
|
||||
@@ -274,17 +272,15 @@ onMounted(() => {
|
||||
:expanded="!isTriggerRootView || parsedTriggerActions.length === 0"
|
||||
@selected="onSelected"
|
||||
>
|
||||
<template>
|
||||
<n8n-callout
|
||||
theme="info"
|
||||
iconless
|
||||
v-if="!userActivated && isTriggerRootView"
|
||||
slim
|
||||
data-test-id="actions-panel-activation-callout"
|
||||
>
|
||||
<span v-html="$locale.baseText('nodeCreator.actionsCallout.triggersStartWorkflow')" />
|
||||
</n8n-callout>
|
||||
</template>
|
||||
<n8n-callout
|
||||
theme="info"
|
||||
iconless
|
||||
v-if="!userActivated && isTriggerRootView"
|
||||
slim
|
||||
data-test-id="actions-panel-activation-callout"
|
||||
>
|
||||
<span v-html="$locale.baseText('nodeCreator.actionsCallout.triggersStartWorkflow')" />
|
||||
</n8n-callout>
|
||||
<!-- Empty state -->
|
||||
<template #empty>
|
||||
<n8n-info-tip theme="info" type="note" v-if="!search" :class="$style.actionsEmpty">
|
||||
@@ -296,14 +292,13 @@ onMounted(() => {
|
||||
"
|
||||
/>
|
||||
</n8n-info-tip>
|
||||
<template v-else>
|
||||
<p
|
||||
:class="$style.resetSearch"
|
||||
v-html="$locale.baseText('nodeCreator.actionsCategory.noMatchingActions')"
|
||||
@click="resetSearch"
|
||||
data-test-id="actions-panel-no-matching-actions"
|
||||
/>
|
||||
</template>
|
||||
<p
|
||||
v-else
|
||||
:class="$style.resetSearch"
|
||||
v-html="$locale.baseText('nodeCreator.actionsCategory.noMatchingActions')"
|
||||
@click="resetSearch"
|
||||
data-test-id="actions-panel-no-matching-actions"
|
||||
/>
|
||||
</template>
|
||||
</CategorizedItemsRenderer>
|
||||
</template>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { camelCase } from 'lodash-es';
|
||||
import { getCurrentInstance, computed } from 'vue';
|
||||
import { computed } from 'vue';
|
||||
import type { INodeCreateElement, NodeFilterType } from '@/Interface';
|
||||
import { TRIGGER_NODE_CREATOR_VIEW, HTTP_REQUEST_NODE_TYPE, WEBHOOK_NODE_TYPE } from '@/constants';
|
||||
|
||||
@@ -16,6 +16,7 @@ import { useKeyboardNavigation } from '../composables/useKeyboardNavigation';
|
||||
import ItemsRenderer from '../Renderers/ItemsRenderer.vue';
|
||||
import CategorizedItemsRenderer from '../Renderers/CategorizedItemsRenderer.vue';
|
||||
import NoResults from '../Panel/NoResults.vue';
|
||||
import { useI18n, useTelemetry } from '@/composables';
|
||||
|
||||
export interface Props {
|
||||
rootView: 'trigger' | 'action';
|
||||
@@ -25,7 +26,9 @@ const emit = defineEmits({
|
||||
nodeTypeSelected: (nodeTypes: string[]) => true,
|
||||
});
|
||||
|
||||
const instance = getCurrentInstance();
|
||||
const i18n = useI18n();
|
||||
const telemetry = useTelemetry();
|
||||
|
||||
const { mergedNodes, actions } = useNodeCreatorStore();
|
||||
const { baseUrl } = useRootStore();
|
||||
const { getNodeTypesWithManualTrigger } = useActions();
|
||||
@@ -45,7 +48,7 @@ function selectNodeType(nodeTypes: string[]) {
|
||||
|
||||
function onSelected(item: INodeCreateElement) {
|
||||
if (item.type === 'subcategory') {
|
||||
const title = instance?.proxy.$locale.baseText(
|
||||
const title = i18n.baseText(
|
||||
`nodeCreator.subcategoryNames.${camelCase(item.properties.title)}` as BaseTextKey,
|
||||
);
|
||||
|
||||
@@ -59,7 +62,7 @@ function onSelected(item: INodeCreateElement) {
|
||||
itemsMapper: subcategoriesMapper,
|
||||
});
|
||||
|
||||
instance?.proxy.$telemetry.trackNodesPanel('nodeCreateList.onSubcategorySelected', {
|
||||
telemetry.trackNodesPanel('nodeCreateList.onSubcategorySelected', {
|
||||
subcategory: item.key,
|
||||
});
|
||||
}
|
||||
@@ -96,10 +99,7 @@ function onSelected(item: INodeCreateElement) {
|
||||
}
|
||||
|
||||
if (item.type === 'view') {
|
||||
const view =
|
||||
item.key === TRIGGER_NODE_CREATOR_VIEW
|
||||
? TriggerView(instance?.proxy?.$locale)
|
||||
: RegularView(instance?.proxy?.$locale);
|
||||
const view = item.key === TRIGGER_NODE_CREATOR_VIEW ? TriggerView() : RegularView();
|
||||
|
||||
pushViewStack({
|
||||
title: view.title,
|
||||
|
||||
@@ -7,21 +7,20 @@
|
||||
:class="$style.nodeCreator"
|
||||
:style="nodeCreatorInlineStyle"
|
||||
ref="nodeCreator"
|
||||
v-click-outside="onClickOutside"
|
||||
@dragover="onDragOver"
|
||||
@drop="onDrop"
|
||||
@mousedown="onMouseDown"
|
||||
@mouseup="onMouseUp"
|
||||
data-test-id="node-creator"
|
||||
>
|
||||
<NodesListPanel @nodeTypeSelected="$listeners.nodeTypeSelected" />
|
||||
<NodesListPanel @nodeTypeSelected="onNodeTypeSelected" />
|
||||
</div>
|
||||
</slide-transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { watch, reactive, toRefs, computed } from 'vue';
|
||||
import { watch, reactive, toRefs, computed, onBeforeUnmount } from 'vue';
|
||||
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
import { useNodeCreatorStore } from '@/stores/nodeCreator.store';
|
||||
@@ -35,6 +34,7 @@ import { useUIStore } from '@/stores';
|
||||
|
||||
export interface Props {
|
||||
active?: boolean;
|
||||
onNodeTypeSelected?: (nodeType: string) => void;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
@@ -61,13 +61,7 @@ const viewStacksLength = computed(() => useViewStacks().viewStacks.length);
|
||||
const nodeCreatorInlineStyle = computed(() => {
|
||||
return { top: `${uiStore.bannersHeight + uiStore.headerHeight}px` };
|
||||
});
|
||||
|
||||
function onClickOutside(event: Event) {
|
||||
// We need to prevent cases where user would click inside the node creator
|
||||
// and try to drag non-draggable element. In that case the click event would
|
||||
// be fired and the node creator would be closed. So we stop that if we detect
|
||||
// that the click event originated from inside the node creator. And fire click even on the
|
||||
// original target.
|
||||
function onMouseUpOutside() {
|
||||
if (state.mousedownInsideEvent) {
|
||||
const clickEvent = new MouseEvent('click', {
|
||||
bubbles: true,
|
||||
@@ -75,18 +69,21 @@ function onClickOutside(event: Event) {
|
||||
});
|
||||
state.mousedownInsideEvent.target?.dispatchEvent(clickEvent);
|
||||
state.mousedownInsideEvent = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.type === 'click') {
|
||||
emit('closeNodeCreator');
|
||||
unBindOnMouseUpOutside();
|
||||
}
|
||||
}
|
||||
function unBindOnMouseUpOutside() {
|
||||
document.removeEventListener('mouseup', onMouseUpOutside);
|
||||
document.removeEventListener('touchstart', onMouseUpOutside);
|
||||
}
|
||||
function onMouseUp() {
|
||||
state.mousedownInsideEvent = null;
|
||||
unBindOnMouseUpOutside();
|
||||
}
|
||||
function onMouseDown(event: MouseEvent) {
|
||||
state.mousedownInsideEvent = event;
|
||||
document.addEventListener('mouseup', onMouseUpOutside);
|
||||
document.addEventListener('touchstart', onMouseUpOutside);
|
||||
}
|
||||
function onDragOver(event: DragEvent) {
|
||||
event.preventDefault();
|
||||
@@ -147,6 +144,10 @@ watch(
|
||||
{ immediate: true },
|
||||
);
|
||||
const { nodeCreator } = toRefs(state);
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
unBindOnMouseUpOutside();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style module lang="scss">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { getCurrentInstance, computed, onMounted, onUnmounted, watch } from 'vue';
|
||||
import { computed, onMounted, onUnmounted, watch } from 'vue';
|
||||
import type { INodeCreateElement } from '@/Interface';
|
||||
import { TRIGGER_NODE_CREATOR_VIEW } from '@/constants';
|
||||
|
||||
@@ -11,8 +11,9 @@ import { useKeyboardNavigation } from '../composables/useKeyboardNavigation';
|
||||
import SearchBar from './SearchBar.vue';
|
||||
import ActionsRenderer from '../Modes/ActionsMode.vue';
|
||||
import NodesRenderer from '../Modes/NodesMode.vue';
|
||||
import { useI18n } from '@/composables';
|
||||
|
||||
const instance = getCurrentInstance();
|
||||
const i18n = useI18n();
|
||||
|
||||
const { mergedNodes } = useNodeCreatorStore();
|
||||
const { pushViewStack, popViewStack, updateCurrentViewStack } = useViewStacks();
|
||||
@@ -25,10 +26,10 @@ const viewStacks = computed(() => useViewStacks().viewStacks);
|
||||
const isActionsMode = computed(() => useViewStacks().activeViewStackMode === 'actions');
|
||||
const searchPlaceholder = computed(() =>
|
||||
isActionsMode.value
|
||||
? instance?.proxy?.$locale.baseText('nodeCreator.actionsCategory.searchActions', {
|
||||
? i18n.baseText('nodeCreator.actionsCategory.searchActions', {
|
||||
interpolate: { node: activeViewStack.value.title as string },
|
||||
})
|
||||
: instance?.proxy?.$locale.baseText('nodeCreator.searchBar.searchNodes'),
|
||||
: i18n.baseText('nodeCreator.searchBar.searchNodes'),
|
||||
);
|
||||
|
||||
const nodeCreatorView = computed(() => useNodeCreatorStore().selectedView);
|
||||
@@ -58,10 +59,7 @@ onUnmounted(() => {
|
||||
watch(
|
||||
() => nodeCreatorView.value,
|
||||
(selectedView) => {
|
||||
const view =
|
||||
selectedView === TRIGGER_NODE_CREATOR_VIEW
|
||||
? TriggerView(instance?.proxy?.$locale)
|
||||
: RegularView(instance?.proxy?.$locale);
|
||||
const view = selectedView === TRIGGER_NODE_CREATOR_VIEW ? TriggerView() : RegularView();
|
||||
|
||||
pushViewStack({
|
||||
title: view.title,
|
||||
@@ -124,15 +122,15 @@ function onBackButton() {
|
||||
? searchPlaceholder
|
||||
: $locale.baseText('nodeCreator.searchBar.searchNodes')
|
||||
"
|
||||
@input="onSearch"
|
||||
:value="activeViewStack.search"
|
||||
:modelValue="activeViewStack.search"
|
||||
@update:modelValue="onSearch"
|
||||
/>
|
||||
<div :class="$style.renderedItems">
|
||||
<!-- Actions mode -->
|
||||
<ActionsRenderer v-if="isActionsMode && activeViewStack.subcategory" v-on="$listeners" />
|
||||
<ActionsRenderer v-if="isActionsMode && activeViewStack.subcategory" v-bind="$attrs" />
|
||||
|
||||
<!-- Nodes Mode -->
|
||||
<NodesRenderer v-else :rootView="nodeCreatorView" v-on="$listeners" />
|
||||
<NodesRenderer v-else :rootView="nodeCreatorView" v-bind="$attrs" />
|
||||
</div>
|
||||
</aside>
|
||||
</transition>
|
||||
@@ -149,14 +147,14 @@ function onBackButton() {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
:global(.panel-slide-out-enter),
|
||||
:global(.panel-slide-out-enter-from),
|
||||
:global(.panel-slide-in-leave-to) {
|
||||
transform: translateX(0);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
:global(.panel-slide-out-leave-to),
|
||||
:global(.panel-slide-in-enter) {
|
||||
:global(.panel-slide-in-enter-from) {
|
||||
transform: translateX(100%);
|
||||
// Make sure the leaving panel stays on top
|
||||
// for the slide-out panel effect
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
<template>
|
||||
<div :class="$style.searchContainer" data-test-id="search-bar">
|
||||
<div :class="{ [$style.prefix]: true, [$style.active]: value.length > 0 }">
|
||||
<div :class="{ [$style.prefix]: true, [$style.active]: modelValue.length > 0 }">
|
||||
<font-awesome-icon icon="search" size="sm" />
|
||||
</div>
|
||||
<div :class="$style.text">
|
||||
<input
|
||||
:placeholder="placeholder"
|
||||
:value="value"
|
||||
@input="onInput"
|
||||
:value="modelValue"
|
||||
:class="$style.input"
|
||||
ref="inputRef"
|
||||
autofocus
|
||||
data-test-id="node-creator-search-bar"
|
||||
tabindex="0"
|
||||
@input="onInput"
|
||||
/>
|
||||
</div>
|
||||
<div :class="$style.suffix" v-if="value.length > 0" @click="clear">
|
||||
<div :class="$style.suffix" v-if="modelValue.length > 0" @click="clear">
|
||||
<button :class="[$style.clear, $style.clickable]">
|
||||
<font-awesome-icon icon="times-circle" />
|
||||
</button>
|
||||
@@ -30,16 +30,16 @@ import { runExternalHook } from '@/utils';
|
||||
|
||||
export interface Props {
|
||||
placeholder: string;
|
||||
value: string;
|
||||
modelValue: string;
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
placeholder: '',
|
||||
value: '',
|
||||
modelValue: '',
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'input', value: string): void;
|
||||
(event: 'update:modelValue', value: string): void;
|
||||
}>();
|
||||
|
||||
const state = reactive({
|
||||
@@ -52,11 +52,11 @@ function focus() {
|
||||
|
||||
function onInput(event: Event) {
|
||||
const input = event.target as HTMLInputElement;
|
||||
emit('input', input.value);
|
||||
emit('update:modelValue', input.value);
|
||||
}
|
||||
|
||||
function clear() {
|
||||
emit('input', '');
|
||||
emit('update:modelValue', '');
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
@@ -117,8 +117,8 @@ registerKeyHook(`CategoryLeft_${props.category}`, {
|
||||
<!-- Pass through listeners & empty slot to ItemsRenderer -->
|
||||
<ItemsRenderer
|
||||
v-if="expanded"
|
||||
v-bind="$attrs"
|
||||
:elements="elements"
|
||||
v-on="$listeners"
|
||||
:isTrigger="isTriggerCategory"
|
||||
>
|
||||
<template #default> </template>
|
||||
|
||||
@@ -125,7 +125,7 @@ watch(
|
||||
:data-keyboard-nav-id="item.uuid"
|
||||
@click="wrappedEmit('selected', item)"
|
||||
>
|
||||
<template v-if="renderedItems.includes(item)">
|
||||
<div v-if="renderedItems.includes(item)">
|
||||
<label-item v-if="item.type === 'label'" :item="item" />
|
||||
<subcategory-item v-if="item.type === 'subcategory'" :item="item.properties" />
|
||||
|
||||
@@ -148,7 +148,7 @@ watch(
|
||||
:view="item.properties"
|
||||
:class="$style.viewItem"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<n8n-loading :loading="true" :rows="1" variant="p" :class="$style.itemSkeleton" v-else />
|
||||
</div>
|
||||
|
||||
@@ -1,31 +1,34 @@
|
||||
import { render, screen } from '@testing-library/vue';
|
||||
import { screen } from '@testing-library/vue';
|
||||
import CategoryItem from '../ItemTypes/CategoryItem.vue';
|
||||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
|
||||
const renderComponent = createComponentRenderer(CategoryItem);
|
||||
|
||||
describe('CategoryItem', () => {
|
||||
it('should allow expand and collapse', async () => {
|
||||
const { container, updateProps } = render(CategoryItem, { props: { name: 'Category Test' } });
|
||||
const { container, rerender } = renderComponent({ props: { name: 'Category Test' } });
|
||||
|
||||
expect(container.querySelector('[data-icon="chevron-down"]')).toBeInTheDocument();
|
||||
await updateProps({ expanded: false });
|
||||
await rerender({ expanded: false });
|
||||
expect(container.querySelector('[data-icon="chevron-down"]')).not.toBeInTheDocument();
|
||||
expect(container.querySelector('[data-icon="chevron-up"]')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show count', async () => {
|
||||
const { updateProps } = render(CategoryItem, { props: { name: 'Category Test', count: 10 } });
|
||||
const { rerender } = renderComponent({ props: { name: 'Category Test', count: 10 } });
|
||||
|
||||
expect(screen.getByText('Category Test (10)')).toBeInTheDocument();
|
||||
await updateProps({ count: 0 });
|
||||
await rerender({ count: 0 });
|
||||
expect(screen.getByText('Category Test')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show trigger icon', async () => {
|
||||
const { updateProps, container } = render(CategoryItem, {
|
||||
const { rerender, container } = renderComponent({
|
||||
props: { name: 'Category Test', isTrigger: true },
|
||||
});
|
||||
|
||||
expect(container.querySelector('[data-icon="bolt"]')).toBeInTheDocument();
|
||||
await updateProps({ isTrigger: false });
|
||||
await rerender({ isTrigger: false });
|
||||
expect(container.querySelector('[data-icon="bolt"]')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import Vue from 'vue';
|
||||
import { PiniaVuePlugin } from 'pinia';
|
||||
import { nextTick } from 'vue';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { render, fireEvent } from '@testing-library/vue';
|
||||
import { fireEvent } from '@testing-library/vue';
|
||||
import {
|
||||
mockSubcategoryCreateElement,
|
||||
mockLabelCreateElement,
|
||||
@@ -10,6 +9,9 @@ import {
|
||||
import ItemsRenderer from '../Renderers/ItemsRenderer.vue';
|
||||
import { mockActionCreateElement } from './utils';
|
||||
import { mockViewCreateElement } from './utils';
|
||||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
|
||||
const renderComponent = createComponentRenderer(ItemsRenderer);
|
||||
|
||||
describe('ItemsRenderer', () => {
|
||||
it('should render items', async () => {
|
||||
@@ -24,18 +26,16 @@ describe('ItemsRenderer', () => {
|
||||
mockNodeCreateElement('subcategory', { displayName: 'Node 3', name: 'node3' }),
|
||||
mockSubcategoryCreateElement({ title: 'Subcategory 2' }),
|
||||
];
|
||||
const { container } = render(
|
||||
ItemsRenderer,
|
||||
{
|
||||
pinia: createTestingPinia(),
|
||||
props: { elements: items },
|
||||
|
||||
const { container } = renderComponent({
|
||||
pinia: createTestingPinia(),
|
||||
props: { elements: items },
|
||||
global: {
|
||||
stubs: ['n8n-loading'],
|
||||
},
|
||||
(vue) => {
|
||||
vue.use(PiniaVuePlugin);
|
||||
},
|
||||
);
|
||||
//
|
||||
await Vue.nextTick();
|
||||
});
|
||||
|
||||
await nextTick();
|
||||
|
||||
const nodeItems = container.querySelectorAll('.iteratorItem .nodeItem');
|
||||
const labels = container.querySelectorAll('.iteratorItem .label');
|
||||
@@ -53,18 +53,12 @@ describe('ItemsRenderer', () => {
|
||||
mockActionCreateElement(),
|
||||
mockViewCreateElement(),
|
||||
];
|
||||
const { container, emitted } = render(
|
||||
ItemsRenderer,
|
||||
{
|
||||
pinia: createTestingPinia(),
|
||||
props: { elements: items },
|
||||
},
|
||||
(vue) => {
|
||||
vue.use(PiniaVuePlugin);
|
||||
},
|
||||
);
|
||||
const { container, emitted } = renderComponent({
|
||||
pinia: createTestingPinia(),
|
||||
props: { elements: items },
|
||||
});
|
||||
//
|
||||
await Vue.nextTick();
|
||||
await nextTick();
|
||||
|
||||
const itemTypes = {
|
||||
node: container.querySelector('.iteratorItem .nodeItem'),
|
||||
|
||||
@@ -1,30 +1,14 @@
|
||||
import Vue, { defineComponent, watch } from 'vue';
|
||||
import { defineComponent, nextTick, watch } from 'vue';
|
||||
import type { PropType } from 'vue';
|
||||
import { PiniaVuePlugin, createPinia } from 'pinia';
|
||||
import { render, screen, fireEvent } from '@testing-library/vue';
|
||||
import { createPinia } from 'pinia';
|
||||
import { screen, fireEvent } from '@testing-library/vue';
|
||||
import type { INodeTypeDescription } from 'n8n-workflow';
|
||||
import { useNodeCreatorStore } from '@/stores/nodeCreator.store';
|
||||
import { mockSimplifiedNodeType } from './utils';
|
||||
import NodesListPanel from '../Panel/NodesListPanel.vue';
|
||||
import { REGULAR_NODE_CREATOR_VIEW } from '@/constants';
|
||||
import type { NodeFilterType } from '@/Interface';
|
||||
|
||||
function TelemetryPlugin(vue: typeof Vue): void {
|
||||
Object.defineProperty(vue, '$telemetry', {
|
||||
get() {
|
||||
return {
|
||||
trackNodesPanel: () => {},
|
||||
};
|
||||
},
|
||||
});
|
||||
Object.defineProperty(vue.prototype, '$telemetry', {
|
||||
get() {
|
||||
return {
|
||||
trackNodesPanel: () => {},
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
|
||||
function getWrapperComponent(setup: () => void) {
|
||||
const wrapperComponent = defineComponent({
|
||||
@@ -41,16 +25,11 @@ function getWrapperComponent(setup: () => void) {
|
||||
template: '<NodesListPanel @nodeTypeSelected="e => $emit(\'nodeTypeSelected\', e)" />',
|
||||
});
|
||||
|
||||
return render(
|
||||
wrapperComponent,
|
||||
{
|
||||
pinia: createPinia(),
|
||||
return createComponentRenderer(wrapperComponent, {
|
||||
global: {
|
||||
plugins: [createPinia()],
|
||||
},
|
||||
(vue) => {
|
||||
vue.use(PiniaVuePlugin);
|
||||
vue.use(TelemetryPlugin);
|
||||
},
|
||||
);
|
||||
})();
|
||||
}
|
||||
|
||||
describe('NodesListPanel', () => {
|
||||
@@ -78,11 +57,11 @@ describe('NodesListPanel', () => {
|
||||
return {};
|
||||
});
|
||||
|
||||
await Vue.nextTick();
|
||||
await nextTick();
|
||||
expect(screen.getByText('Select a trigger')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('node-creator-search-bar')).toBeInTheDocument();
|
||||
screen.getByText('On app event').click();
|
||||
await Vue.nextTick();
|
||||
await nextTick();
|
||||
expect(screen.queryByTestId('node-creator-search-bar')).not.toBeInTheDocument();
|
||||
mockedTriggerNodes.forEach((n) => {
|
||||
expect(screen.queryByText(n.name)).toBeInTheDocument();
|
||||
@@ -95,7 +74,7 @@ describe('NodesListPanel', () => {
|
||||
expect(container.querySelector('.backButton')).toBeInTheDocument();
|
||||
|
||||
await fireEvent.click(container.querySelector('.backButton')!);
|
||||
await Vue.nextTick();
|
||||
await nextTick();
|
||||
|
||||
expect(screen.queryAllByTestId('item-iterator-item')).toHaveLength(6);
|
||||
});
|
||||
@@ -145,27 +124,22 @@ describe('NodesListPanel', () => {
|
||||
template: '<NodesListPanel @nodeTypeSelected="e => $emit(\'nodeTypeSelected\', e)" />',
|
||||
});
|
||||
|
||||
render(
|
||||
wrapperComponent,
|
||||
{
|
||||
pinia: createPinia(),
|
||||
props: {
|
||||
nodeTypes: mockedNodes,
|
||||
selectedView: REGULAR_NODE_CREATOR_VIEW,
|
||||
},
|
||||
},
|
||||
(vue) => {
|
||||
vue.use(PiniaVuePlugin);
|
||||
vue.use(TelemetryPlugin);
|
||||
},
|
||||
);
|
||||
const renderComponent = createComponentRenderer(wrapperComponent);
|
||||
|
||||
await Vue.nextTick();
|
||||
renderComponent({
|
||||
pinia: createPinia(),
|
||||
props: {
|
||||
nodeTypes: mockedNodes,
|
||||
selectedView: REGULAR_NODE_CREATOR_VIEW,
|
||||
},
|
||||
});
|
||||
|
||||
await nextTick();
|
||||
expect(screen.getByText('What happens next?')).toBeInTheDocument();
|
||||
expect(screen.queryAllByTestId('item-iterator-item')).toHaveLength(6);
|
||||
|
||||
screen.getByText('Action in an app').click();
|
||||
await Vue.nextTick();
|
||||
await nextTick();
|
||||
mockedNodes.forEach((n) => {
|
||||
expect(screen.queryByText(n.displayName)).toBeInTheDocument();
|
||||
});
|
||||
@@ -205,40 +179,31 @@ describe('NodesListPanel', () => {
|
||||
template: '<NodesListPanel @nodeTypeSelected="e => $emit(\'nodeTypeSelected\', e)" />',
|
||||
});
|
||||
|
||||
function renderComponent() {
|
||||
return render(
|
||||
wrapperComponent,
|
||||
{
|
||||
pinia: createPinia(),
|
||||
props: {
|
||||
nodeTypes: mockedNodes,
|
||||
},
|
||||
},
|
||||
(vue) => {
|
||||
vue.use(PiniaVuePlugin);
|
||||
vue.use(TelemetryPlugin);
|
||||
},
|
||||
);
|
||||
}
|
||||
const renderComponent = createComponentRenderer(wrapperComponent, {
|
||||
pinia: createPinia(),
|
||||
props: {
|
||||
nodeTypes: mockedNodes,
|
||||
},
|
||||
});
|
||||
|
||||
it('should be visible in the root view', async () => {
|
||||
renderComponent();
|
||||
await Vue.nextTick();
|
||||
await nextTick();
|
||||
|
||||
expect(screen.queryByTestId('node-creator-search-bar')).toBeInTheDocument();
|
||||
});
|
||||
it('should not be visible if subcategory contains less than 9 items', async () => {
|
||||
renderComponent();
|
||||
await Vue.nextTick();
|
||||
await nextTick();
|
||||
|
||||
screen.getByText('On app event').click();
|
||||
await Vue.nextTick();
|
||||
await nextTick();
|
||||
expect(screen.queryByTestId('node-creator-search-bar')).not.toBeInTheDocument();
|
||||
expect(screen.queryAllByTestId('item-iterator-item')).toHaveLength(8);
|
||||
});
|
||||
it('should be visible if subcategory contains 9 or more items', async () => {
|
||||
const { updateProps } = renderComponent();
|
||||
await Vue.nextTick();
|
||||
const { rerender } = renderComponent();
|
||||
await nextTick();
|
||||
|
||||
mockedNodes.push(
|
||||
mockSimplifiedNodeType({
|
||||
@@ -248,11 +213,11 @@ describe('NodesListPanel', () => {
|
||||
}),
|
||||
);
|
||||
|
||||
await updateProps({ nodeTypes: [...mockedNodes] });
|
||||
await Vue.nextTick();
|
||||
await rerender({ nodeTypes: [...mockedNodes] });
|
||||
await nextTick();
|
||||
|
||||
screen.getByText('On app event').click();
|
||||
await Vue.nextTick();
|
||||
await nextTick();
|
||||
|
||||
expect(screen.queryAllByTestId('item-iterator-item')).toHaveLength(9);
|
||||
expect(screen.queryByTestId('node-creator-search-bar')).toBeInTheDocument();
|
||||
@@ -260,26 +225,26 @@ describe('NodesListPanel', () => {
|
||||
|
||||
it('should correctly handle search', async () => {
|
||||
const { container } = renderComponent();
|
||||
await Vue.nextTick();
|
||||
await nextTick();
|
||||
|
||||
screen.getByText('On app event').click();
|
||||
await Vue.nextTick();
|
||||
await nextTick();
|
||||
|
||||
await fireEvent.input(screen.getByTestId('node-creator-search-bar'), {
|
||||
target: { value: 'Ninth' },
|
||||
});
|
||||
await Vue.nextTick();
|
||||
await nextTick();
|
||||
expect(screen.queryAllByTestId('item-iterator-item')).toHaveLength(1);
|
||||
|
||||
await fireEvent.input(screen.getByTestId('node-creator-search-bar'), {
|
||||
target: { value: 'Non sense' },
|
||||
});
|
||||
await Vue.nextTick();
|
||||
await nextTick();
|
||||
expect(screen.queryAllByTestId('item-iterator-item')).toHaveLength(0);
|
||||
expect(screen.queryByText("We didn't make that... yet")).toBeInTheDocument();
|
||||
|
||||
await fireEvent.click(container.querySelector('.clear')!);
|
||||
await Vue.nextTick();
|
||||
await nextTick();
|
||||
expect(screen.queryAllByTestId('item-iterator-item')).toHaveLength(9);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { render } from '@testing-library/vue';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { defineComponent, computed } from 'vue';
|
||||
import { useKeyboardNavigation } from '../composables/useKeyboardNavigation';
|
||||
import { PiniaVuePlugin, createPinia } from 'pinia';
|
||||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import { createPinia } from 'pinia';
|
||||
|
||||
const eventHookSpy = vi.fn();
|
||||
describe('useKeyboardNavigation', () => {
|
||||
@@ -38,18 +38,16 @@ describe('useKeyboardNavigation', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const renderTestComponent = () => {
|
||||
return render(TestComponent, { pinia: createPinia() }, (vue) => {
|
||||
vue.use(PiniaVuePlugin);
|
||||
});
|
||||
};
|
||||
const renderComponent = createComponentRenderer(TestComponent, {
|
||||
pinia: createPinia(),
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
eventHookSpy.mockClear();
|
||||
});
|
||||
|
||||
test('ArrowDown moves to the next item, cycling after last item', async () => {
|
||||
const { container } = renderTestComponent();
|
||||
const { container } = renderComponent();
|
||||
|
||||
expect(container.querySelector('[data-keyboard-nav-id="item1"]')).toHaveClass('active');
|
||||
await userEvent.keyboard('{arrowdown}');
|
||||
@@ -60,7 +58,7 @@ describe('useKeyboardNavigation', () => {
|
||||
});
|
||||
|
||||
test('ArrowUp moves to the previous item, cycling after firstitem', async () => {
|
||||
const { container } = renderTestComponent();
|
||||
const { container } = renderComponent();
|
||||
|
||||
expect(container.querySelector('[data-keyboard-nav-id="item1"]')).toHaveClass('active');
|
||||
await userEvent.keyboard('{arrowup}');
|
||||
@@ -72,7 +70,7 @@ describe('useKeyboardNavigation', () => {
|
||||
});
|
||||
|
||||
test('Key hooks are executed', async () => {
|
||||
renderTestComponent();
|
||||
renderComponent();
|
||||
|
||||
await userEvent.keyboard('{arrowup}');
|
||||
expect(eventHookSpy).toHaveBeenCalledWith('item3', 'ArrowUp');
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ref, set } from 'vue';
|
||||
import { ref } from 'vue';
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
export type KeyboardKey = (typeof WATCHED_KEYS)[number];
|
||||
@@ -125,7 +125,7 @@ export const useKeyboardNavigation = defineStore('nodeCreatorKeyboardNavigation'
|
||||
function registerKeyHook(name: string, hook: KeyHook) {
|
||||
hook.keyboardKeys.forEach((keyboardKey) => {
|
||||
if (WATCHED_KEYS.includes(keyboardKey)) {
|
||||
set(keysHooks.value, name, hook);
|
||||
keysHooks.value = { ...keysHooks.value, [name]: hook };
|
||||
} else {
|
||||
throw new Error(`Key ${keyboardKey} is not supported`);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { computed, ref, set } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import { defineStore } from 'pinia';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import type { INodeCreateElement, NodeFilterType, SimplifiedNodeType } from '@/Interface';
|
||||
@@ -167,7 +167,10 @@ export const useViewStacks = defineStore('nodeCreatorViewStacks', () => {
|
||||
// For each key in the stack, update the matched stack
|
||||
Object.keys(stack).forEach((key) => {
|
||||
const typedKey = key as keyof ViewStack;
|
||||
set(viewStacks.value[matchedIndex], key, stack[typedKey]);
|
||||
viewStacks.value[matchedIndex] = {
|
||||
...viewStacks.value[matchedIndex],
|
||||
[key]: stack[typedKey],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -14,12 +14,15 @@ import {
|
||||
EMAIL_IMAP_NODE_TYPE,
|
||||
DEFAULT_SUBCATEGORY,
|
||||
} from '@/constants';
|
||||
import { useI18n } from '@/composables';
|
||||
|
||||
export function TriggerView() {
|
||||
const i18n = useI18n();
|
||||
|
||||
export function TriggerView($locale: any) {
|
||||
return {
|
||||
value: TRIGGER_NODE_CREATOR_VIEW,
|
||||
title: $locale.baseText('nodeCreator.triggerHelperPanel.selectATrigger'),
|
||||
subtitle: $locale.baseText('nodeCreator.triggerHelperPanel.selectATriggerDescription'),
|
||||
title: i18n.baseText('nodeCreator.triggerHelperPanel.selectATrigger'),
|
||||
subtitle: i18n.baseText('nodeCreator.triggerHelperPanel.selectATriggerDescription'),
|
||||
items: [
|
||||
{
|
||||
key: DEFAULT_SUBCATEGORY,
|
||||
@@ -37,12 +40,8 @@ export function TriggerView($locale: any) {
|
||||
properties: {
|
||||
group: [],
|
||||
name: SCHEDULE_TRIGGER_NODE_TYPE,
|
||||
displayName: $locale.baseText(
|
||||
'nodeCreator.triggerHelperPanel.scheduleTriggerDisplayName',
|
||||
),
|
||||
description: $locale.baseText(
|
||||
'nodeCreator.triggerHelperPanel.scheduleTriggerDescription',
|
||||
),
|
||||
displayName: i18n.baseText('nodeCreator.triggerHelperPanel.scheduleTriggerDisplayName'),
|
||||
description: i18n.baseText('nodeCreator.triggerHelperPanel.scheduleTriggerDescription'),
|
||||
icon: 'fa:clock',
|
||||
},
|
||||
},
|
||||
@@ -53,8 +52,8 @@ export function TriggerView($locale: any) {
|
||||
properties: {
|
||||
group: [],
|
||||
name: WEBHOOK_NODE_TYPE,
|
||||
displayName: $locale.baseText('nodeCreator.triggerHelperPanel.webhookTriggerDisplayName'),
|
||||
description: $locale.baseText('nodeCreator.triggerHelperPanel.webhookTriggerDescription'),
|
||||
displayName: i18n.baseText('nodeCreator.triggerHelperPanel.webhookTriggerDisplayName'),
|
||||
description: i18n.baseText('nodeCreator.triggerHelperPanel.webhookTriggerDescription'),
|
||||
iconData: {
|
||||
type: 'file',
|
||||
icon: 'webhook',
|
||||
@@ -69,8 +68,8 @@ export function TriggerView($locale: any) {
|
||||
properties: {
|
||||
group: [],
|
||||
name: MANUAL_TRIGGER_NODE_TYPE,
|
||||
displayName: $locale.baseText('nodeCreator.triggerHelperPanel.manualTriggerDisplayName'),
|
||||
description: $locale.baseText('nodeCreator.triggerHelperPanel.manualTriggerDescription'),
|
||||
displayName: i18n.baseText('nodeCreator.triggerHelperPanel.manualTriggerDisplayName'),
|
||||
description: i18n.baseText('nodeCreator.triggerHelperPanel.manualTriggerDescription'),
|
||||
icon: 'fa:mouse-pointer',
|
||||
},
|
||||
},
|
||||
@@ -81,12 +80,8 @@ export function TriggerView($locale: any) {
|
||||
properties: {
|
||||
group: [],
|
||||
name: EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE,
|
||||
displayName: $locale.baseText(
|
||||
'nodeCreator.triggerHelperPanel.workflowTriggerDisplayName',
|
||||
),
|
||||
description: $locale.baseText(
|
||||
'nodeCreator.triggerHelperPanel.workflowTriggerDescription',
|
||||
),
|
||||
displayName: i18n.baseText('nodeCreator.triggerHelperPanel.workflowTriggerDisplayName'),
|
||||
description: i18n.baseText('nodeCreator.triggerHelperPanel.workflowTriggerDescription'),
|
||||
icon: 'fa:sign-out-alt',
|
||||
},
|
||||
},
|
||||
@@ -103,10 +98,12 @@ export function TriggerView($locale: any) {
|
||||
};
|
||||
}
|
||||
|
||||
export function RegularView($locale: any) {
|
||||
export function RegularView() {
|
||||
const i18n = useI18n();
|
||||
|
||||
return {
|
||||
value: REGULAR_NODE_CREATOR_VIEW,
|
||||
title: $locale.baseText('nodeCreator.triggerHelperPanel.whatHappensNext'),
|
||||
title: i18n.baseText('nodeCreator.triggerHelperPanel.whatHappensNext'),
|
||||
items: [
|
||||
{
|
||||
key: DEFAULT_SUBCATEGORY,
|
||||
@@ -156,11 +153,9 @@ export function RegularView($locale: any) {
|
||||
key: TRIGGER_NODE_CREATOR_VIEW,
|
||||
type: 'view',
|
||||
properties: {
|
||||
title: $locale.baseText('nodeCreator.triggerHelperPanel.addAnotherTrigger'),
|
||||
title: i18n.baseText('nodeCreator.triggerHelperPanel.addAnotherTrigger'),
|
||||
icon: 'bolt',
|
||||
description: $locale.baseText(
|
||||
'nodeCreator.triggerHelperPanel.addAnotherTriggerDescription',
|
||||
),
|
||||
description: i18n.baseText('nodeCreator.triggerHelperPanel.addAnotherTriggerDescription'),
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user