mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
feat(editor): Help users discover expressions when using drag n drop (#8869)
This commit is contained in:
parent
71f1b23771
commit
e78cc2d8d2
|
@ -253,7 +253,6 @@ describe('Data mapping', () => {
|
||||||
workflowPage.actions.openNode('Set');
|
workflowPage.actions.openNode('Set');
|
||||||
|
|
||||||
ndv.actions.typeIntoParameterInput('value', 'delete me');
|
ndv.actions.typeIntoParameterInput('value', 'delete me');
|
||||||
ndv.actions.dismissMappingTooltip();
|
|
||||||
|
|
||||||
ndv.actions.typeIntoParameterInput('name', 'test');
|
ndv.actions.typeIntoParameterInput('name', 'test');
|
||||||
|
|
||||||
|
|
|
@ -42,7 +42,8 @@ describe('n8n Form Trigger', () => {
|
||||||
':nth-child(3) > .border-top-dashed > .parameter-input-list-wrapper > :nth-child(1) > .parameter-item',
|
':nth-child(3) > .border-top-dashed > .parameter-input-list-wrapper > :nth-child(1) > .parameter-item',
|
||||||
)
|
)
|
||||||
.find('input[placeholder*="e.g. What is your name?"]')
|
.find('input[placeholder*="e.g. What is your name?"]')
|
||||||
.type('Test Field 3');
|
.type('Test Field 3')
|
||||||
|
.blur();
|
||||||
cy.get(
|
cy.get(
|
||||||
':nth-child(3) > .border-top-dashed > .parameter-input-list-wrapper > :nth-child(2) > .parameter-item',
|
':nth-child(3) > .border-top-dashed > .parameter-input-list-wrapper > :nth-child(2) > .parameter-item',
|
||||||
).click();
|
).click();
|
||||||
|
@ -53,7 +54,8 @@ describe('n8n Form Trigger', () => {
|
||||||
':nth-child(4) > .border-top-dashed > .parameter-input-list-wrapper > :nth-child(1) > .parameter-item',
|
':nth-child(4) > .border-top-dashed > .parameter-input-list-wrapper > :nth-child(1) > .parameter-item',
|
||||||
)
|
)
|
||||||
.find('input[placeholder*="e.g. What is your name?"]')
|
.find('input[placeholder*="e.g. What is your name?"]')
|
||||||
.type('Test Field 4');
|
.type('Test Field 4')
|
||||||
|
.blur();
|
||||||
cy.get(
|
cy.get(
|
||||||
':nth-child(4) > .border-top-dashed > .parameter-input-list-wrapper > :nth-child(2) > .parameter-item',
|
':nth-child(4) > .border-top-dashed > .parameter-input-list-wrapper > :nth-child(2) > .parameter-item',
|
||||||
).click();
|
).click();
|
||||||
|
@ -65,12 +67,14 @@ describe('n8n Form Trigger', () => {
|
||||||
':nth-child(4) > :nth-child(1) > :nth-child(2) > :nth-child(3) > .multi-parameter > .fixed-collection-parameter > .fixed-collection-parameter-property > :nth-child(1) > :nth-child(1)',
|
':nth-child(4) > :nth-child(1) > :nth-child(2) > :nth-child(3) > .multi-parameter > .fixed-collection-parameter > .fixed-collection-parameter-property > :nth-child(1) > :nth-child(1)',
|
||||||
)
|
)
|
||||||
.find('input')
|
.find('input')
|
||||||
.type('Option 1');
|
.type('Option 1')
|
||||||
|
.blur();
|
||||||
cy.get(
|
cy.get(
|
||||||
':nth-child(4) > :nth-child(1) > :nth-child(2) > :nth-child(3) > .multi-parameter > .fixed-collection-parameter > .fixed-collection-parameter-property > :nth-child(1) > :nth-child(2)',
|
':nth-child(4) > :nth-child(1) > :nth-child(2) > :nth-child(3) > .multi-parameter > .fixed-collection-parameter > .fixed-collection-parameter-property > :nth-child(1) > :nth-child(2)',
|
||||||
)
|
)
|
||||||
.find('input')
|
.find('input')
|
||||||
.type('Option 2');
|
.type('Option 2')
|
||||||
|
.blur();
|
||||||
|
|
||||||
//add optional submitted message
|
//add optional submitted message
|
||||||
cy.get('.param-options').click();
|
cy.get('.param-options').click();
|
||||||
|
|
|
@ -182,7 +182,7 @@ describe('Webhook Trigger node', async () => {
|
||||||
workflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME);
|
workflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME);
|
||||||
workflowPage.actions.openNode(EDIT_FIELDS_SET_NODE_NAME);
|
workflowPage.actions.openNode(EDIT_FIELDS_SET_NODE_NAME);
|
||||||
ndv.getters.assignmentCollectionAdd('assignments').click();
|
ndv.getters.assignmentCollectionAdd('assignments').click();
|
||||||
ndv.getters.assignmentName('assignments').type('data');
|
ndv.getters.assignmentName('assignments').type('data').find('input').blur();
|
||||||
ndv.getters.assignmentType('assignments').click();
|
ndv.getters.assignmentType('assignments').click();
|
||||||
ndv.getters.assignmentValue('assignments').paste(cowBase64);
|
ndv.getters.assignmentValue('assignments').paste(cowBase64);
|
||||||
|
|
||||||
|
@ -313,7 +313,7 @@ const addEditFields = () => {
|
||||||
workflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME);
|
workflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME);
|
||||||
workflowPage.actions.openNode(EDIT_FIELDS_SET_NODE_NAME);
|
workflowPage.actions.openNode(EDIT_FIELDS_SET_NODE_NAME);
|
||||||
ndv.getters.assignmentCollectionAdd('assignments').click();
|
ndv.getters.assignmentCollectionAdd('assignments').click();
|
||||||
ndv.getters.assignmentName('assignments').type('MyValue');
|
ndv.getters.assignmentName('assignments').type('MyValue').find('input').blur();
|
||||||
ndv.getters.assignmentType('assignments').click();
|
ndv.getters.assignmentType('assignments').click();
|
||||||
getVisibleSelect().find('li').contains('Number').click();
|
getVisibleSelect().find('li').contains('Number').click();
|
||||||
ndv.getters.assignmentValue('assignments').type('1234');
|
ndv.getters.assignmentValue('assignments').type('1234');
|
||||||
|
|
|
@ -186,6 +186,7 @@ describe('NDV', () => {
|
||||||
|
|
||||||
ndv.getters.inputTableRow(1).invoke('attr', 'data-test-id').should('equal', 'hovering-item');
|
ndv.getters.inputTableRow(1).invoke('attr', 'data-test-id').should('equal', 'hovering-item');
|
||||||
ndv.getters.inputTableRow(1).realHover();
|
ndv.getters.inputTableRow(1).realHover();
|
||||||
|
cy.wait(100);
|
||||||
ndv.getters.outputHoveringItem().should('not.exist');
|
ndv.getters.outputHoveringItem().should('not.exist');
|
||||||
ndv.getters.parameterExpressionPreview('value').should('include.text', '1111');
|
ndv.getters.parameterExpressionPreview('value').should('include.text', '1111');
|
||||||
|
|
||||||
|
@ -200,6 +201,7 @@ describe('NDV', () => {
|
||||||
ndv.actions.selectInputNode('Code');
|
ndv.actions.selectInputNode('Code');
|
||||||
|
|
||||||
ndv.getters.inputTableRow(1).realHover();
|
ndv.getters.inputTableRow(1).realHover();
|
||||||
|
cy.wait(100);
|
||||||
ndv.getters.inputTableRow(1).should('have.text', '6666');
|
ndv.getters.inputTableRow(1).should('have.text', '6666');
|
||||||
|
|
||||||
ndv.getters.inputTableRow(1).invoke('attr', 'data-test-id').should('equal', 'hovering-item');
|
ndv.getters.inputTableRow(1).invoke('attr', 'data-test-id').should('equal', 'hovering-item');
|
||||||
|
|
|
@ -167,10 +167,6 @@ export class NDV extends BasePage {
|
||||||
selectOptionInParameterDropdown: (parameterName: string, content: string) => {
|
selectOptionInParameterDropdown: (parameterName: string, content: string) => {
|
||||||
getVisibleSelect().find('.option-headline').contains(content).click();
|
getVisibleSelect().find('.option-headline').contains(content).click();
|
||||||
},
|
},
|
||||||
dismissMappingTooltip: () => {
|
|
||||||
cy.getByTestId('dismiss-mapping-tooltip').click();
|
|
||||||
cy.getByTestId('dismiss-mapping-tooltip').should('not.be.visible');
|
|
||||||
},
|
|
||||||
rename: (newName: string) => {
|
rename: (newName: string) => {
|
||||||
this.getters.nodeNameContainer().click();
|
this.getters.nodeNameContainer().click();
|
||||||
this.getters.nodeRenameInput().should('be.visible').type('{selectall}').type(newName);
|
this.getters.nodeRenameInput().should('be.visible').type('{selectall}').type(newName);
|
||||||
|
@ -244,7 +240,7 @@ export class NDV extends BasePage {
|
||||||
getVisiblePopper().find('li').last().click();
|
getVisiblePopper().find('li').last().click();
|
||||||
},
|
},
|
||||||
addFilterCondition: (paramName: string) => {
|
addFilterCondition: (paramName: string) => {
|
||||||
this.getters.filterConditionAdd(paramName).click();
|
this.getters.filterConditionAdd(paramName).click({ force: true });
|
||||||
},
|
},
|
||||||
removeFilterCondition: (paramName: string, index: number) => {
|
removeFilterCondition: (paramName: string, index: number) => {
|
||||||
this.getters.filterConditionRemove(paramName, index).click();
|
this.getters.filterConditionRemove(paramName, index).click();
|
||||||
|
|
|
@ -97,7 +97,7 @@
|
||||||
--color-json-brackets: var(--prim-gray-670);
|
--color-json-brackets: var(--prim-gray-670);
|
||||||
--color-json-brackets-hover: var(--prim-color-alt-e);
|
--color-json-brackets-hover: var(--prim-color-alt-e);
|
||||||
--color-json-line: var(--prim-gray-200);
|
--color-json-line: var(--prim-gray-200);
|
||||||
--color-json-highlight: var(--prim-gray-70);
|
--color-json-highlight: var(--color-background-base);
|
||||||
--color-code-background: var(--prim-gray-800);
|
--color-code-background: var(--prim-gray-800);
|
||||||
--color-code-background-readonly: var(--prim-gray-740);
|
--color-code-background-readonly: var(--prim-gray-740);
|
||||||
--color-code-lineHighlight: var(--prim-gray-740);
|
--color-code-lineHighlight: var(--prim-gray-740);
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 363 KiB |
Binary file not shown.
Before Width: | Height: | Size: 464 KiB |
|
@ -1245,6 +1245,7 @@ export interface NDVState {
|
||||||
activeTarget: { id: string; stickyPosition: null | XYPosition } | null;
|
activeTarget: { id: string; stickyPosition: null | XYPosition } | null;
|
||||||
};
|
};
|
||||||
isMappingOnboarded: boolean;
|
isMappingOnboarded: boolean;
|
||||||
|
isAutocompleteOnboarded: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NotificationOptions extends Partial<ElementNotificationOptions> {
|
export interface NotificationOptions extends Partial<ElementNotificationOptions> {
|
||||||
|
|
|
@ -58,7 +58,7 @@ const assignmentTypeToNodeProperty = (
|
||||||
|
|
||||||
const nameParameter = computed<INodeProperties>(() => ({
|
const nameParameter = computed<INodeProperties>(() => ({
|
||||||
name: 'name',
|
name: 'name',
|
||||||
displayName: '',
|
displayName: 'Name',
|
||||||
default: '',
|
default: '',
|
||||||
requiresDataPath: 'single',
|
requiresDataPath: 'single',
|
||||||
placeholder: 'name',
|
placeholder: 'name',
|
||||||
|
@ -68,7 +68,7 @@ const nameParameter = computed<INodeProperties>(() => ({
|
||||||
const valueParameter = computed<INodeProperties>(() => {
|
const valueParameter = computed<INodeProperties>(() => {
|
||||||
return {
|
return {
|
||||||
name: 'value',
|
name: 'value',
|
||||||
displayName: '',
|
displayName: 'Value',
|
||||||
default: '',
|
default: '',
|
||||||
placeholder: 'value',
|
placeholder: 'value',
|
||||||
...assignmentTypeToNodeProperty(assignment.value.type ?? 'string'),
|
...assignmentTypeToNodeProperty(assignment.value.type ?? 'string'),
|
||||||
|
|
|
@ -22,6 +22,7 @@
|
||||||
:rows="rows"
|
:rows="rows"
|
||||||
:additional-data="additionalExpressionData"
|
:additional-data="additionalExpressionData"
|
||||||
:path="path"
|
:path="path"
|
||||||
|
:event-bus="eventBus"
|
||||||
@focus="onFocus"
|
@focus="onFocus"
|
||||||
@blur="onBlur"
|
@blur="onBlur"
|
||||||
@change="onChange"
|
@change="onChange"
|
||||||
|
@ -64,6 +65,7 @@ import type { Segment } from '@/types/expressions';
|
||||||
import type { TargetItem } from '@/Interface';
|
import type { TargetItem } from '@/Interface';
|
||||||
import type { IDataObject } from 'n8n-workflow';
|
import type { IDataObject } from 'n8n-workflow';
|
||||||
import { useDebounce } from '@/composables/useDebounce';
|
import { useDebounce } from '@/composables/useDebounce';
|
||||||
|
import { type EventBus, createEventBus } from 'n8n-design-system/utils';
|
||||||
|
|
||||||
type InlineExpressionEditorInputRef = InstanceType<typeof InlineExpressionEditorInput>;
|
type InlineExpressionEditorInputRef = InstanceType<typeof InlineExpressionEditorInput>;
|
||||||
|
|
||||||
|
@ -97,6 +99,10 @@ export default defineComponent({
|
||||||
type: Object as PropType<IDataObject>,
|
type: Object as PropType<IDataObject>,
|
||||||
default: () => ({}),
|
default: () => ({}),
|
||||||
},
|
},
|
||||||
|
eventBus: {
|
||||||
|
type: Object as PropType<EventBus>,
|
||||||
|
default: () => createEventBus(),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const { callDebounced } = useDebounce();
|
const { callDebounced } = useDebounce();
|
||||||
|
|
|
@ -3,27 +3,30 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { completionStatus, startCompletion } from '@codemirror/autocomplete';
|
||||||
import { history } from '@codemirror/commands';
|
import { history } from '@codemirror/commands';
|
||||||
import { Compartment, EditorState, Prec } from '@codemirror/state';
|
import { Compartment, EditorState, Prec } from '@codemirror/state';
|
||||||
import { EditorView, keymap } from '@codemirror/view';
|
import { EditorView, keymap } from '@codemirror/view';
|
||||||
import type { PropType } from 'vue';
|
import type { PropType } from 'vue';
|
||||||
import { defineComponent } from 'vue';
|
import { defineComponent, nextTick } from 'vue';
|
||||||
|
|
||||||
import { completionManager } from '@/mixins/completionManager';
|
import { completionManager } from '@/mixins/completionManager';
|
||||||
import { expressionManager } from '@/mixins/expressionManager';
|
import { expressionManager } from '@/mixins/expressionManager';
|
||||||
import { expressionInputHandler } from '@/plugins/codemirror/inputHandlers/expression.inputHandler';
|
import { expressionInputHandler } from '@/plugins/codemirror/inputHandlers/expression.inputHandler';
|
||||||
import { n8nAutocompletion, n8nLang } from '@/plugins/codemirror/n8nLang';
|
|
||||||
import { highlighter } from '@/plugins/codemirror/resolvableHighlighter';
|
|
||||||
import { isEqual } from 'lodash-es';
|
|
||||||
import type { IDataObject } from 'n8n-workflow';
|
|
||||||
import { inputTheme } from './theme';
|
|
||||||
import {
|
import {
|
||||||
autocompleteKeyMap,
|
autocompleteKeyMap,
|
||||||
enterKeyMap,
|
enterKeyMap,
|
||||||
historyKeyMap,
|
historyKeyMap,
|
||||||
tabKeyMap,
|
tabKeyMap,
|
||||||
} from '@/plugins/codemirror/keymap';
|
} from '@/plugins/codemirror/keymap';
|
||||||
import { completionStatus } from '@codemirror/autocomplete';
|
import { n8nAutocompletion, n8nLang } from '@/plugins/codemirror/n8nLang';
|
||||||
|
import { highlighter } from '@/plugins/codemirror/resolvableHighlighter';
|
||||||
|
import { isEqual } from 'lodash-es';
|
||||||
|
import { createEventBus, type EventBus } from 'n8n-design-system/utils';
|
||||||
|
import type { IDataObject } from 'n8n-workflow';
|
||||||
|
import { inputTheme } from './theme';
|
||||||
|
import { useNDVStore } from '@/stores/ndv.store';
|
||||||
|
import { mapStores } from 'pinia';
|
||||||
|
|
||||||
const editableConf = new Compartment();
|
const editableConf = new Compartment();
|
||||||
|
|
||||||
|
@ -51,6 +54,13 @@ export default defineComponent({
|
||||||
type: Object as PropType<IDataObject>,
|
type: Object as PropType<IDataObject>,
|
||||||
default: () => ({}),
|
default: () => ({}),
|
||||||
},
|
},
|
||||||
|
eventBus: {
|
||||||
|
type: Object as PropType<EventBus>,
|
||||||
|
default: () => createEventBus(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapStores(useNDVStore),
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
isReadOnly(newValue: boolean) {
|
isReadOnly(newValue: boolean) {
|
||||||
|
@ -132,14 +142,34 @@ export default defineComponent({
|
||||||
this.editorState = this.editor.state;
|
this.editorState = this.editor.state;
|
||||||
|
|
||||||
highlighter.addColor(this.editor, this.resolvableSegments);
|
highlighter.addColor(this.editor, this.resolvableSegments);
|
||||||
|
|
||||||
|
this.eventBus.on('drop', this.onDrop);
|
||||||
},
|
},
|
||||||
beforeUnmount() {
|
beforeUnmount() {
|
||||||
this.editor?.destroy();
|
this.editor?.destroy();
|
||||||
|
this.eventBus.off('drop', this.onDrop);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
focus() {
|
focus() {
|
||||||
this.editor?.focus();
|
this.editor?.focus();
|
||||||
},
|
},
|
||||||
|
setCursorPosition(pos: number) {
|
||||||
|
this.editor.dispatch({ selection: { anchor: pos, head: pos } });
|
||||||
|
},
|
||||||
|
async onDrop() {
|
||||||
|
await nextTick();
|
||||||
|
this.focus();
|
||||||
|
|
||||||
|
const END_OF_EXPRESSION = ' }}';
|
||||||
|
const value = this.editor.state.sliceDoc(0);
|
||||||
|
const cursorPosition = Math.max(value.lastIndexOf(END_OF_EXPRESSION), 0);
|
||||||
|
|
||||||
|
this.setCursorPosition(cursorPosition);
|
||||||
|
|
||||||
|
if (!this.ndvStore.isAutocompleteOnboarded) {
|
||||||
|
startCompletion(this.editor as EditorView);
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -7,130 +7,111 @@
|
||||||
<div ref="root" data-test-id="inline-expression-editor-output"></div>
|
<div ref="root" data-test-id="inline-expression-editor-output"></div>
|
||||||
</n8n-text>
|
</n8n-text>
|
||||||
<div :class="$style.footer">
|
<div :class="$style.footer">
|
||||||
<n8n-text size="small" compact>
|
<InlineExpressionTip />
|
||||||
{{ i18n.baseText('parameterInput.anythingInside') }}
|
|
||||||
</n8n-text>
|
|
||||||
<div :class="$style['expression-syntax-example']" v-text="`{{ }}`"></div>
|
|
||||||
<n8n-text size="small" compact>
|
|
||||||
{{ i18n.baseText('parameterInput.isJavaScript') }}
|
|
||||||
</n8n-text>
|
|
||||||
{{ ' ' }}
|
|
||||||
<n8n-link
|
|
||||||
:class="$style['learn-more']"
|
|
||||||
size="small"
|
|
||||||
underline
|
|
||||||
theme="text"
|
|
||||||
:to="expressionsDocsUrl"
|
|
||||||
>
|
|
||||||
{{ i18n.baseText('parameterInput.learnMore') }}
|
|
||||||
</n8n-link>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import { EditorView } from '@codemirror/view';
|
import { EditorView } from '@codemirror/view';
|
||||||
import { EditorState } from '@codemirror/state';
|
import { EditorState } from '@codemirror/state';
|
||||||
|
|
||||||
import { defineComponent } from 'vue';
|
|
||||||
import type { PropType } from 'vue';
|
|
||||||
|
|
||||||
import { highlighter } from '@/plugins/codemirror/resolvableHighlighter';
|
|
||||||
import { outputTheme } from './theme';
|
|
||||||
|
|
||||||
import type { Plaintext, Resolved, Segment } from '@/types/expressions';
|
|
||||||
import { EXPRESSIONS_DOCS_URL } from '@/constants';
|
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
|
import type { Plaintext, Resolved, Segment } from '@/types/expressions';
|
||||||
|
import { highlighter } from '@/plugins/codemirror/resolvableHighlighter';
|
||||||
|
import { computed, onMounted, onBeforeUnmount, ref, watch } from 'vue';
|
||||||
|
import { outputTheme } from './theme';
|
||||||
|
import InlineExpressionTip from './InlineExpressionTip.vue';
|
||||||
|
|
||||||
export default defineComponent({
|
interface InlineExpressionEditorOutputProps {
|
||||||
name: 'InlineExpressionEditorOutput',
|
segments: Segment[];
|
||||||
props: {
|
hoveringItemNumber: number;
|
||||||
segments: {
|
isReadOnly?: boolean;
|
||||||
type: Array as PropType<Segment[]>,
|
visible?: boolean;
|
||||||
required: true,
|
noInputData?: boolean;
|
||||||
},
|
}
|
||||||
isReadOnly: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
visible: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
noInputData: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
hoveringItemNumber: {
|
|
||||||
type: Number,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
setup() {
|
|
||||||
const i18n = useI18n();
|
|
||||||
|
|
||||||
return {
|
const props = withDefaults(defineProps<InlineExpressionEditorOutputProps>(), {
|
||||||
i18n,
|
readOnly: false,
|
||||||
};
|
visible: false,
|
||||||
},
|
noInputData: false,
|
||||||
data() {
|
});
|
||||||
return {
|
|
||||||
editor: null as EditorView | null,
|
|
||||||
expressionsDocsUrl: EXPRESSIONS_DOCS_URL,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
resolvedExpression(): string {
|
|
||||||
return this.segments.reduce((acc, segment) => {
|
|
||||||
acc += segment.kind === 'resolvable' ? segment.resolved : segment.plaintext;
|
|
||||||
return acc;
|
|
||||||
}, '');
|
|
||||||
},
|
|
||||||
plaintextSegments(): Plaintext[] {
|
|
||||||
return this.segments.filter((s): s is Plaintext => s.kind === 'plaintext');
|
|
||||||
},
|
|
||||||
resolvedSegments(): Resolved[] {
|
|
||||||
let cursor = 0;
|
|
||||||
|
|
||||||
return this.segments
|
const i18n = useI18n();
|
||||||
.map((segment) => {
|
|
||||||
segment.from = cursor;
|
|
||||||
cursor +=
|
|
||||||
segment.kind === 'plaintext'
|
|
||||||
? segment.plaintext.length
|
|
||||||
: segment.resolved
|
|
||||||
? segment.resolved.toString().length
|
|
||||||
: 0;
|
|
||||||
segment.to = cursor;
|
|
||||||
return segment;
|
|
||||||
})
|
|
||||||
.filter((segment): segment is Resolved => segment.kind === 'resolvable');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
segments() {
|
|
||||||
if (!this.editor) return;
|
|
||||||
|
|
||||||
this.editor.dispatch({
|
const editor = ref<EditorView | null>(null);
|
||||||
changes: { from: 0, to: this.editor.state.doc.length, insert: this.resolvedExpression },
|
const root = ref<HTMLElement | null>(null);
|
||||||
});
|
|
||||||
|
|
||||||
highlighter.addColor(this.editor, this.resolvedSegments);
|
const resolvedExpression = computed(() => {
|
||||||
highlighter.removeColor(this.editor, this.plaintextSegments);
|
if (props.segments.length === 0) {
|
||||||
},
|
return i18n.baseText('parameterInput.emptyString');
|
||||||
},
|
}
|
||||||
mounted() {
|
|
||||||
this.editor = new EditorView({
|
return props.segments.reduce((acc, segment) => {
|
||||||
parent: this.$refs.root as HTMLDivElement,
|
acc += segment.kind === 'resolvable' ? (segment.resolved as string) : segment.plaintext;
|
||||||
state: EditorState.create({
|
return acc;
|
||||||
doc: this.resolvedExpression,
|
}, '');
|
||||||
extensions: [outputTheme(), EditorState.readOnly.of(true), EditorView.lineWrapping],
|
});
|
||||||
}),
|
|
||||||
|
const plaintextSegments = computed<Plaintext[]>(() => {
|
||||||
|
if (props.segments.length === 0) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
from: 0,
|
||||||
|
to: resolvedExpression.value.length - 1,
|
||||||
|
plaintext: resolvedExpression.value,
|
||||||
|
kind: 'plaintext',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return props.segments.filter((s): s is Plaintext => s.kind === 'plaintext');
|
||||||
|
});
|
||||||
|
|
||||||
|
const resolvedSegments = computed<Resolved[]>(() => {
|
||||||
|
let cursor = 0;
|
||||||
|
|
||||||
|
return props.segments
|
||||||
|
.map((segment) => {
|
||||||
|
segment.from = cursor;
|
||||||
|
cursor +=
|
||||||
|
segment.kind === 'plaintext'
|
||||||
|
? segment.plaintext.length
|
||||||
|
: segment.resolved
|
||||||
|
? (segment.resolved as string | number | boolean).toString().length
|
||||||
|
: 0;
|
||||||
|
segment.to = cursor;
|
||||||
|
return segment;
|
||||||
|
})
|
||||||
|
.filter((segment): segment is Resolved => segment.kind === 'resolvable');
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.segments,
|
||||||
|
() => {
|
||||||
|
if (!editor.value) return;
|
||||||
|
|
||||||
|
editor.value.dispatch({
|
||||||
|
changes: { from: 0, to: editor.value.state.doc.length, insert: resolvedExpression.value },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
highlighter.addColor(editor.value as EditorView, resolvedSegments.value);
|
||||||
|
highlighter.removeColor(editor.value as EditorView, plaintextSegments.value);
|
||||||
},
|
},
|
||||||
beforeUnmount() {
|
);
|
||||||
this.editor?.destroy();
|
|
||||||
},
|
onMounted(() => {
|
||||||
|
editor.value = new EditorView({
|
||||||
|
parent: root.value as HTMLElement,
|
||||||
|
state: EditorState.create({
|
||||||
|
doc: resolvedExpression.value,
|
||||||
|
extensions: [outputTheme(), EditorState.readOnly.of(true), EditorView.lineWrapping],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
editor.value?.destroy();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -157,11 +138,14 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
|
|
||||||
.header,
|
.header,
|
||||||
.body,
|
.body {
|
||||||
.footer {
|
|
||||||
padding: var(--spacing-3xs);
|
padding: var(--spacing-3xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
border-top: var(--border-base);
|
||||||
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
color: var(--color-text-dark);
|
color: var(--color-text-dark);
|
||||||
font-weight: var(--font-weight-bold);
|
font-weight: var(--font-weight-bold);
|
||||||
|
@ -178,28 +162,5 @@ export default defineComponent({
|
||||||
padding-top: var(--spacing-2xs);
|
padding-top: var(--spacing-2xs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer {
|
|
||||||
border-top: var(--border-base);
|
|
||||||
padding: var(--spacing-4xs);
|
|
||||||
padding-left: var(--spacing-2xs);
|
|
||||||
padding-top: 0;
|
|
||||||
line-height: var(--font-line-height-regular);
|
|
||||||
color: var(--color-text-base);
|
|
||||||
|
|
||||||
.expression-syntax-example {
|
|
||||||
display: inline-block;
|
|
||||||
font-size: var(--font-size-2xs);
|
|
||||||
height: var(--font-size-m);
|
|
||||||
background-color: var(--color-expression-syntax-example);
|
|
||||||
margin-left: var(--spacing-5xs);
|
|
||||||
margin-right: var(--spacing-5xs);
|
|
||||||
}
|
|
||||||
|
|
||||||
.learn-more {
|
|
||||||
line-height: 1;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,127 @@
|
||||||
|
<template>
|
||||||
|
<div v-if="tip === 'drag'" :class="$style.tip">
|
||||||
|
<n8n-text size="small" :class="$style.tipText"
|
||||||
|
>{{ $locale.baseText('parameterInput.tip') }}:
|
||||||
|
</n8n-text>
|
||||||
|
<n8n-text size="small" :class="$style.text">
|
||||||
|
{{ $locale.baseText('parameterInput.dragTipBeforePill') }}
|
||||||
|
</n8n-text>
|
||||||
|
<div :class="[$style.pill, { [$style.highlight]: !ndvStore.isMappingOnboarded }]">
|
||||||
|
{{ $locale.baseText('parameterInput.inputField') }}
|
||||||
|
</div>
|
||||||
|
<n8n-text size="small" :class="$style.text">
|
||||||
|
{{ $locale.baseText('parameterInput.dragTipAfterPill') }}
|
||||||
|
</n8n-text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="tip === 'executePrevious'" :class="$style.tip">
|
||||||
|
<n8n-text size="small" :class="$style.tipText"
|
||||||
|
>{{ $locale.baseText('parameterInput.tip') }}:
|
||||||
|
</n8n-text>
|
||||||
|
<n8n-text size="small" :class="$style.text"
|
||||||
|
>{{ $locale.baseText('expressionTip.noExecutionData') }}
|
||||||
|
</n8n-text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else :class="$style.tip">
|
||||||
|
<n8n-text size="small" :class="$style.tipText"
|
||||||
|
>{{ $locale.baseText('parameterInput.tip') }}:
|
||||||
|
</n8n-text>
|
||||||
|
<n8n-text size="small" :class="$style.text">
|
||||||
|
{{ i18n.baseText('parameterInput.anythingInside') }}
|
||||||
|
</n8n-text>
|
||||||
|
<code v-text="`{{ }}`"></code>
|
||||||
|
<n8n-text size="small" :class="$style.text">
|
||||||
|
{{ i18n.baseText('parameterInput.isJavaScript') }}
|
||||||
|
</n8n-text>
|
||||||
|
<n8n-link
|
||||||
|
:class="$style['learn-more']"
|
||||||
|
size="small"
|
||||||
|
underline
|
||||||
|
theme="text"
|
||||||
|
:to="expressionsDocsUrl"
|
||||||
|
>
|
||||||
|
{{ i18n.baseText('parameterInput.learnMore') }}
|
||||||
|
</n8n-link>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useI18n } from '@/composables/useI18n';
|
||||||
|
import { useNDVStore } from '@/stores/ndv.store';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { EXPRESSIONS_DOCS_URL } from '@/constants';
|
||||||
|
|
||||||
|
const i18n = useI18n();
|
||||||
|
const ndvStore = useNDVStore();
|
||||||
|
|
||||||
|
const props = defineProps<{ tip?: 'drag' | 'default' }>();
|
||||||
|
|
||||||
|
const tip = computed(() => {
|
||||||
|
if (!ndvStore.hasInputData) {
|
||||||
|
return 'executePrevious';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.tip) return props.tip;
|
||||||
|
|
||||||
|
if (ndvStore.focusedMappableInput) return 'drag';
|
||||||
|
|
||||||
|
return 'default';
|
||||||
|
});
|
||||||
|
const expressionsDocsUrl = EXPRESSIONS_DOCS_URL;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.tip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
line-height: var(--font-line-height-regular);
|
||||||
|
color: var(--color-text-base);
|
||||||
|
font-size: var(--font-size-2xs);
|
||||||
|
padding: var(--spacing-2xs);
|
||||||
|
gap: var(--spacing-4xs);
|
||||||
|
|
||||||
|
.tipText {
|
||||||
|
color: var(--color-text-dark);
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
flex-shrink: 1;
|
||||||
|
white-space: nowrap;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-size: var(--font-size-3xs);
|
||||||
|
background: var(--color-background-base);
|
||||||
|
padding: var(--spacing-5xs);
|
||||||
|
border-radius: var(--border-radius-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pill {
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--color-text-dark);
|
||||||
|
|
||||||
|
border: var(--border-base);
|
||||||
|
border-color: var(--color-foreground-light);
|
||||||
|
background-color: var(--color-background-xlight);
|
||||||
|
padding: var(--spacing-5xs) var(--spacing-3xs);
|
||||||
|
border-radius: var(--border-radius-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlight {
|
||||||
|
color: var(--color-primary);
|
||||||
|
background-color: var(--color-primary-tint-3);
|
||||||
|
border-color: var(--color-primary-tint-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -51,6 +51,7 @@
|
||||||
:path="path"
|
:path="path"
|
||||||
:additional-expression-data="additionalExpressionData"
|
:additional-expression-data="additionalExpressionData"
|
||||||
:class="{ 'ph-no-capture': shouldRedactValue }"
|
:class="{ 'ph-no-capture': shouldRedactValue }"
|
||||||
|
:event-bus="eventBus"
|
||||||
@update:model-value="expressionUpdated"
|
@update:model-value="expressionUpdated"
|
||||||
@modalOpenerClick="openExpressionEditorModal"
|
@modalOpenerClick="openExpressionEditorModal"
|
||||||
@focus="setFocus"
|
@focus="setFocus"
|
||||||
|
|
|
@ -29,46 +29,34 @@
|
||||||
@drop="onDrop"
|
@drop="onDrop"
|
||||||
>
|
>
|
||||||
<template #default="{ droppable, activeDrop }">
|
<template #default="{ droppable, activeDrop }">
|
||||||
<n8n-tooltip
|
<ParameterInputWrapper
|
||||||
placement="left"
|
ref="param"
|
||||||
:visible="showMappingTooltip"
|
:parameter="parameter"
|
||||||
:buttons="dataMappingTooltipButtons"
|
:model-value="value"
|
||||||
>
|
:path="path"
|
||||||
<template #content>
|
:is-read-only="isReadOnly"
|
||||||
<span
|
:is-assignment="isAssignment"
|
||||||
v-html="
|
:rows="rows"
|
||||||
i18n.baseText(`dataMapping.${displayMode}Hint`, {
|
:droppable="droppable"
|
||||||
interpolate: { name: parameter.displayName },
|
:active-drop="activeDrop"
|
||||||
})
|
:force-show-expression="forceShowExpression"
|
||||||
"
|
:hint="hint"
|
||||||
/>
|
:hide-hint="hideHint"
|
||||||
</template>
|
:hide-issues="hideIssues"
|
||||||
<ParameterInputWrapper
|
:label="label"
|
||||||
ref="param"
|
:event-bus="eventBus"
|
||||||
:parameter="parameter"
|
input-size="small"
|
||||||
:model-value="value"
|
@update="valueChanged"
|
||||||
:path="path"
|
@textInput="onTextInput"
|
||||||
:is-read-only="isReadOnly"
|
@focus="onFocus"
|
||||||
:is-assignment="isAssignment"
|
@blur="onBlur"
|
||||||
:rows="rows"
|
@drop="onDrop"
|
||||||
:droppable="droppable"
|
/>
|
||||||
:active-drop="activeDrop"
|
|
||||||
:force-show-expression="forceShowExpression"
|
|
||||||
:hint="hint"
|
|
||||||
:hide-hint="hideHint"
|
|
||||||
:hide-issues="hideIssues"
|
|
||||||
:label="label"
|
|
||||||
:event-bus="eventBus"
|
|
||||||
input-size="small"
|
|
||||||
@update="valueChanged"
|
|
||||||
@textInput="onTextInput"
|
|
||||||
@focus="onFocus"
|
|
||||||
@blur="onBlur"
|
|
||||||
@drop="onDrop"
|
|
||||||
/>
|
|
||||||
</n8n-tooltip>
|
|
||||||
</template>
|
</template>
|
||||||
</DraggableTarget>
|
</DraggableTarget>
|
||||||
|
<div v-if="showDragnDropTip" :class="$style.tip">
|
||||||
|
<InlineExpressionTip tip="drag" />
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
:class="{
|
:class="{
|
||||||
[$style.options]: true,
|
[$style.options]: true,
|
||||||
|
@ -94,7 +82,7 @@ import { defineComponent } from 'vue';
|
||||||
import type { PropType } from 'vue';
|
import type { PropType } from 'vue';
|
||||||
import { mapStores } from 'pinia';
|
import { mapStores } from 'pinia';
|
||||||
|
|
||||||
import type { IN8nButton, INodeUi, IRunDataDisplayMode, IUpdateInformation } from '@/Interface';
|
import type { INodeUi, IRunDataDisplayMode, IUpdateInformation } from '@/Interface';
|
||||||
|
|
||||||
import ParameterOptions from '@/components/ParameterOptions.vue';
|
import ParameterOptions from '@/components/ParameterOptions.vue';
|
||||||
import DraggableTarget from '@/components/DraggableTarget.vue';
|
import DraggableTarget from '@/components/DraggableTarget.vue';
|
||||||
|
@ -109,13 +97,11 @@ import type {
|
||||||
IParameterLabel,
|
IParameterLabel,
|
||||||
NodeParameterValueType,
|
NodeParameterValueType,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import type { BaseTextKey } from '@/plugins/i18n';
|
|
||||||
import { useNDVStore } from '@/stores/ndv.store';
|
import { useNDVStore } from '@/stores/ndv.store';
|
||||||
import { useSegment } from '@/stores/segment.store';
|
import { useSegment } from '@/stores/segment.store';
|
||||||
import { getMappedResult } from '@/utils/mappingUtils';
|
import { getMappedResult } from '@/utils/mappingUtils';
|
||||||
import { createEventBus } from 'n8n-design-system/utils';
|
import { createEventBus } from 'n8n-design-system/utils';
|
||||||
|
import InlineExpressionTip from './InlineExpressionEditor/InlineExpressionTip.vue';
|
||||||
const DISPLAY_MODES_WITH_DATA_MAPPING = ['table', 'json', 'schema'];
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'ParameterInputFull',
|
name: 'ParameterInputFull',
|
||||||
|
@ -123,6 +109,7 @@ export default defineComponent({
|
||||||
ParameterOptions,
|
ParameterOptions,
|
||||||
DraggableTarget,
|
DraggableTarget,
|
||||||
ParameterInputWrapper,
|
ParameterInputWrapper,
|
||||||
|
InlineExpressionTip,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
displayOptions: {
|
displayOptions: {
|
||||||
|
@ -159,6 +146,7 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
parameter: {
|
parameter: {
|
||||||
type: Object as PropType<INodeProperties>,
|
type: Object as PropType<INodeProperties>,
|
||||||
|
required: true,
|
||||||
},
|
},
|
||||||
path: {
|
path: {
|
||||||
type: String,
|
type: String,
|
||||||
|
@ -192,24 +180,8 @@ export default defineComponent({
|
||||||
focused: false,
|
focused: false,
|
||||||
menuExpanded: false,
|
menuExpanded: false,
|
||||||
forceShowExpression: false,
|
forceShowExpression: false,
|
||||||
dataMappingTooltipButtons: [] as IN8nButton[],
|
|
||||||
mappingTooltipEnabled: false,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
|
||||||
const mappingTooltipDismissHandler = this.onMappingTooltipDismissed.bind(this);
|
|
||||||
this.dataMappingTooltipButtons = [
|
|
||||||
{
|
|
||||||
attrs: {
|
|
||||||
label: this.i18n.baseText('_reusableBaseText.dismiss' as BaseTextKey),
|
|
||||||
'data-test-id': 'dismiss-mapping-tooltip',
|
|
||||||
},
|
|
||||||
listeners: {
|
|
||||||
onClick: mappingTooltipDismissHandler,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapStores(useNDVStore),
|
...mapStores(useNDVStore),
|
||||||
node(): INodeUi | null {
|
node(): INodeUi | null {
|
||||||
|
@ -221,6 +193,9 @@ export default defineComponent({
|
||||||
isInputTypeString(): boolean {
|
isInputTypeString(): boolean {
|
||||||
return this.parameter.type === 'string';
|
return this.parameter.type === 'string';
|
||||||
},
|
},
|
||||||
|
isInputTypeNumber(): boolean {
|
||||||
|
return this.parameter.type === 'number';
|
||||||
|
},
|
||||||
isResourceLocator(): boolean {
|
isResourceLocator(): boolean {
|
||||||
return this.parameter.type === 'resourceLocator';
|
return this.parameter.type === 'resourceLocator';
|
||||||
},
|
},
|
||||||
|
@ -239,31 +214,29 @@ export default defineComponent({
|
||||||
displayMode(): IRunDataDisplayMode {
|
displayMode(): IRunDataDisplayMode {
|
||||||
return this.ndvStore.inputPanelDisplayMode;
|
return this.ndvStore.inputPanelDisplayMode;
|
||||||
},
|
},
|
||||||
showMappingTooltip(): boolean {
|
showDragnDropTip(): boolean {
|
||||||
return (
|
return (
|
||||||
this.mappingTooltipEnabled &&
|
|
||||||
!this.ndvStore.isMappingOnboarded &&
|
|
||||||
this.focused &&
|
this.focused &&
|
||||||
this.isInputTypeString &&
|
(this.isInputTypeString || this.isInputTypeNumber) &&
|
||||||
!this.isInputDataEmpty &&
|
!this.isValueExpression &&
|
||||||
DISPLAY_MODES_WITH_DATA_MAPPING.includes(this.displayMode)
|
!this.isDropDisabled &&
|
||||||
|
!this.ndvStore.isMappingOnboarded
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onFocus() {
|
onFocus() {
|
||||||
this.focused = true;
|
this.focused = true;
|
||||||
setTimeout(() => {
|
|
||||||
this.mappingTooltipEnabled = true;
|
|
||||||
}, 500);
|
|
||||||
if (!this.parameter.noDataExpression) {
|
if (!this.parameter.noDataExpression) {
|
||||||
this.ndvStore.setMappableNDVInputFocus(this.parameter.displayName);
|
this.ndvStore.setMappableNDVInputFocus(this.parameter.displayName);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onBlur() {
|
onBlur() {
|
||||||
this.focused = false;
|
this.focused = false;
|
||||||
this.mappingTooltipEnabled = false;
|
if (
|
||||||
if (!this.parameter.noDataExpression) {
|
!this.parameter.noDataExpression &&
|
||||||
|
this.ndvStore.focusedMappableInput === this.parameter.displayName
|
||||||
|
) {
|
||||||
this.ndvStore.setMappableNDVInputFocus('');
|
this.ndvStore.setMappableNDVInputFocus('');
|
||||||
}
|
}
|
||||||
this.$emit('blur');
|
this.$emit('blur');
|
||||||
|
@ -333,6 +306,7 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
|
|
||||||
this.valueChanged(parameterData);
|
this.valueChanged(parameterData);
|
||||||
|
this.eventBus.emit('drop', updatedValue);
|
||||||
|
|
||||||
if (!this.ndvStore.isMappingOnboarded) {
|
if (!this.ndvStore.isMappingOnboarded) {
|
||||||
this.showMessage({
|
this.showMessage({
|
||||||
|
@ -342,7 +316,7 @@ export default defineComponent({
|
||||||
dangerouslyUseHTMLString: true,
|
dangerouslyUseHTMLString: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.ndvStore.disableMappingHint();
|
this.ndvStore.setMappingOnboarded();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.ndvStore.setMappingTelemetry({
|
this.ndvStore.setMappingTelemetry({
|
||||||
|
@ -364,21 +338,11 @@ export default defineComponent({
|
||||||
this.forceShowExpression = false;
|
this.forceShowExpression = false;
|
||||||
}, 200);
|
}, 200);
|
||||||
},
|
},
|
||||||
onMappingTooltipDismissed() {
|
|
||||||
this.ndvStore.disableMappingHint(false);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
showMappingTooltip(newValue: boolean) {
|
|
||||||
if (!newValue) {
|
|
||||||
this.$telemetry.track('User viewed data mapping tooltip', { type: 'param focus' });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style module>
|
<style lang="scss" module>
|
||||||
.wrapper {
|
.wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
@ -388,6 +352,20 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tip {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 2;
|
||||||
|
top: 100%;
|
||||||
|
background: var(--color-code-background);
|
||||||
|
border: var(--border-base);
|
||||||
|
border-top: none;
|
||||||
|
width: 100%;
|
||||||
|
box-shadow: 0 2px 6px 0 rgba(#441c17, 0.1);
|
||||||
|
border-bottom-left-radius: 4px;
|
||||||
|
border-bottom-right-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.options {
|
.options {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: -22px;
|
bottom: -22px;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div :class="$style.jsonDisplay">
|
<div :class="[$style.jsonDisplay, { [$style.highlight]: highlight }]">
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<RunDataJsonActions
|
<RunDataJsonActions
|
||||||
v-if="!editMode.enabled"
|
v-if="!editMode.enabled"
|
||||||
|
@ -157,6 +157,9 @@ export default defineComponent({
|
||||||
jsonData(): IDataObject[] {
|
jsonData(): IDataObject[] {
|
||||||
return executionDataToJson(this.inputData);
|
return executionDataToJson(this.inputData);
|
||||||
},
|
},
|
||||||
|
highlight(): boolean {
|
||||||
|
return !this.ndvStore.isMappingOnboarded && Boolean(this.ndvStore.focusedMappableInput);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
getShortKey(el: HTMLElement): string {
|
getShortKey(el: HTMLElement): string {
|
||||||
|
@ -200,7 +203,9 @@ export default defineComponent({
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
void this.externalHooks.run('runDataJson.onDragEnd', telemetryPayload);
|
void this.externalHooks.run('runDataJson.onDragEnd', telemetryPayload);
|
||||||
this.$telemetry.track('User dragged data for mapping', telemetryPayload);
|
this.$telemetry.track('User dragged data for mapping', telemetryPayload, {
|
||||||
|
withPostHog: true,
|
||||||
|
});
|
||||||
}, 1000); // ensure dest data gets set if drop
|
}, 1000); // ensure dest data gets set if drop
|
||||||
},
|
},
|
||||||
getContent(value: unknown): string {
|
getContent(value: unknown): string {
|
||||||
|
@ -232,20 +237,22 @@ export default defineComponent({
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.mappable {
|
.mappable {
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--color-json-highlight);
|
background-color: var(--color-json-highlight);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.dragged {
|
&.highlight .mappable,
|
||||||
&,
|
.dragged {
|
||||||
&:hover {
|
&,
|
||||||
background-color: var(--color-primary-tint-2);
|
&:hover {
|
||||||
|
background-color: var(--color-primary-tint-2);
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -35,6 +35,10 @@ const schema = computed(() => getSchemaForExecutionData(props.data));
|
||||||
|
|
||||||
const isDataEmpty = computed(() => isEmpty(props.data));
|
const isDataEmpty = computed(() => isEmpty(props.data));
|
||||||
|
|
||||||
|
const highlight = computed(() => {
|
||||||
|
return !ndvStore.isMappingOnboarded && Boolean(ndvStore.focusedMappableInput);
|
||||||
|
});
|
||||||
|
|
||||||
const onDragStart = (el: HTMLElement) => {
|
const onDragStart = (el: HTMLElement) => {
|
||||||
if (el?.dataset?.path) {
|
if (el?.dataset?.path) {
|
||||||
draggingPath.value = el.dataset.path;
|
draggingPath.value = el.dataset.path;
|
||||||
|
@ -62,13 +66,13 @@ const onDragEnd = (el: HTMLElement) => {
|
||||||
|
|
||||||
void useExternalHooks().run('runDataJson.onDragEnd', telemetryPayload);
|
void useExternalHooks().run('runDataJson.onDragEnd', telemetryPayload);
|
||||||
|
|
||||||
telemetry.track('User dragged data for mapping', telemetryPayload);
|
telemetry.track('User dragged data for mapping', telemetryPayload, { withPostHog: true });
|
||||||
}, 1000); // ensure dest data gets set if drop
|
}, 1000); // ensure dest data gets set if drop
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="$style.schemaWrapper">
|
<div :class="[$style.schemaWrapper, { highlightSchema: highlight }]">
|
||||||
<n8n-info-tip v-if="isDataEmpty">{{
|
<n8n-info-tip v-if="isDataEmpty">{{
|
||||||
i18n.baseText('dataMapping.schemaView.emptyData')
|
i18n.baseText('dataMapping.schemaView.emptyData')
|
||||||
}}</n8n-info-tip>
|
}}</n8n-info-tip>
|
||||||
|
|
|
@ -39,6 +39,8 @@ const text = computed(() =>
|
||||||
Array.isArray(props.schema.value) ? '' : shorten(props.schema.value, 600, 0),
|
Array.isArray(props.schema.value) ? '' : shorten(props.schema.value, 600, 0),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const dragged = computed(() => props.draggingPath === props.schema.path);
|
||||||
|
|
||||||
const getJsonParameterPath = (path: string): string =>
|
const getJsonParameterPath = (path: string): string =>
|
||||||
getMappedExpression({
|
getMappedExpression({
|
||||||
nodeName: props.node!.name,
|
nodeName: props.node!.name,
|
||||||
|
@ -83,7 +85,7 @@ const getIconBySchemaType = (type: Schema['type']): string => {
|
||||||
:class="{
|
:class="{
|
||||||
[$style.pill]: true,
|
[$style.pill]: true,
|
||||||
[$style.mappable]: mappingEnabled,
|
[$style.mappable]: mappingEnabled,
|
||||||
[$style.dragged]: draggingPath === schema.path,
|
[$style.highlight]: dragged,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
|
@ -203,6 +205,25 @@ const getIconBySchemaType = (type: Schema['type']): string => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(.highlightSchema) {
|
||||||
|
.pill.mappable {
|
||||||
|
&,
|
||||||
|
&:hover,
|
||||||
|
span,
|
||||||
|
&:hover span span {
|
||||||
|
color: var(--color-primary);
|
||||||
|
border-color: var(--color-primary-tint-1);
|
||||||
|
background-color: var(--color-primary-tint-3);
|
||||||
|
|
||||||
|
svg {
|
||||||
|
path {
|
||||||
|
fill: var(--color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.pill {
|
.pill {
|
||||||
float: left;
|
float: left;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
@ -237,22 +258,6 @@ const getIconBySchemaType = (type: Schema['type']): string => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.dragged {
|
|
||||||
&,
|
|
||||||
&:hover,
|
|
||||||
span {
|
|
||||||
color: var(--color-primary);
|
|
||||||
border-color: var(--color-primary-tint-1);
|
|
||||||
background-color: var(--color-primary-tint-3);
|
|
||||||
|
|
||||||
svg {
|
|
||||||
path {
|
|
||||||
fill: var(--color-primary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div :class="$style.dataDisplay">
|
<div :class="[$style.dataDisplay, { [$style.highlight]: highlight }]">
|
||||||
<table v-if="tableData.columns && tableData.columns.length === 0" :class="$style.table">
|
<table v-if="tableData.columns && tableData.columns.length === 0" :class="$style.table">
|
||||||
<tr>
|
<tr>
|
||||||
<th :class="$style.emptyCell"></th>
|
<th :class="$style.emptyCell"></th>
|
||||||
|
@ -259,6 +259,9 @@ export default defineComponent({
|
||||||
focusedMappableInput(): string {
|
focusedMappableInput(): string {
|
||||||
return this.ndvStore.focusedMappableInput;
|
return this.ndvStore.focusedMappableInput;
|
||||||
},
|
},
|
||||||
|
highlight(): boolean {
|
||||||
|
return !this.ndvStore.isMappingOnboarded && Boolean(this.ndvStore.focusedMappableInput);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
shorten,
|
shorten,
|
||||||
|
@ -438,7 +441,9 @@ export default defineComponent({
|
||||||
|
|
||||||
void this.externalHooks.run('runDataTable.onDragEnd', telemetryPayload);
|
void this.externalHooks.run('runDataTable.onDragEnd', telemetryPayload);
|
||||||
|
|
||||||
this.$telemetry.track('User dragged data for mapping', telemetryPayload);
|
this.$telemetry.track('User dragged data for mapping', telemetryPayload, {
|
||||||
|
withPostHog: true,
|
||||||
|
});
|
||||||
}, 1000); // ensure dest data gets set if drop
|
}, 1000); // ensure dest data gets set if drop
|
||||||
},
|
},
|
||||||
isSimple(data: unknown): boolean {
|
isSimple(data: unknown): boolean {
|
||||||
|
@ -642,7 +647,12 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.highlight .draggableHeader {
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
.draggingHeader {
|
.draggingHeader {
|
||||||
|
color: var(--color-primary);
|
||||||
background-color: var(--color-primary-tint-2);
|
background-color: var(--color-primary-tint-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -395,6 +395,7 @@ export const LOCAL_STORAGE_ACTIVATION_FLAG = 'N8N_HIDE_ACTIVATION_ALERT';
|
||||||
export const LOCAL_STORAGE_PIN_DATA_DISCOVERY_NDV_FLAG = 'N8N_PIN_DATA_DISCOVERY_NDV';
|
export const LOCAL_STORAGE_PIN_DATA_DISCOVERY_NDV_FLAG = 'N8N_PIN_DATA_DISCOVERY_NDV';
|
||||||
export const LOCAL_STORAGE_PIN_DATA_DISCOVERY_CANVAS_FLAG = 'N8N_PIN_DATA_DISCOVERY_CANVAS';
|
export const LOCAL_STORAGE_PIN_DATA_DISCOVERY_CANVAS_FLAG = 'N8N_PIN_DATA_DISCOVERY_CANVAS';
|
||||||
export const LOCAL_STORAGE_MAPPING_IS_ONBOARDED = 'N8N_MAPPING_ONBOARDED';
|
export const LOCAL_STORAGE_MAPPING_IS_ONBOARDED = 'N8N_MAPPING_ONBOARDED';
|
||||||
|
export const LOCAL_STORAGE_AUTOCOMPLETE_IS_ONBOARDED = 'N8N_AUTOCOMPLETE_ONBOARDED';
|
||||||
export const LOCAL_STORAGE_MAIN_PANEL_RELATIVE_WIDTH = 'N8N_MAIN_PANEL_RELATIVE_WIDTH';
|
export const LOCAL_STORAGE_MAIN_PANEL_RELATIVE_WIDTH = 'N8N_MAIN_PANEL_RELATIVE_WIDTH';
|
||||||
export const LOCAL_STORAGE_ACTIVE_MODAL = 'N8N_ACTIVE_MODAL';
|
export const LOCAL_STORAGE_ACTIVE_MODAL = 'N8N_ACTIVE_MODAL';
|
||||||
export const LOCAL_STORAGE_THEME = 'N8N_THEME';
|
export const LOCAL_STORAGE_THEME = 'N8N_THEME';
|
||||||
|
|
|
@ -32,6 +32,8 @@ export const completionManager = defineComponent({
|
||||||
|
|
||||||
if (!completionTx) return;
|
if (!completionTx) return;
|
||||||
|
|
||||||
|
this.ndvStore.setAutocompleteOnboarded();
|
||||||
|
|
||||||
let completion = '';
|
let completion = '';
|
||||||
let completionBase = '';
|
let completionBase = '';
|
||||||
|
|
||||||
|
|
|
@ -599,9 +599,24 @@ describe('Resolution-based completions', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('explicit completions (opened by Ctrl+Space or programatically)', () => {
|
||||||
|
test('should return completions for: {{ $json.foo| }}', () => {
|
||||||
|
vi.spyOn(workflowHelpers, 'resolveParameter')
|
||||||
|
// @ts-expect-error Spied function is mistyped
|
||||||
|
.mockReturnValueOnce(undefined)
|
||||||
|
// @ts-expect-error Spied function is mistyped
|
||||||
|
.mockReturnValueOnce('foo');
|
||||||
|
|
||||||
|
const result = completions('{{ $json.foo| }}', true);
|
||||||
|
expect(result).toHaveLength(
|
||||||
|
extensions('string').length + natives('string').length + STRING_RECOMMENDED_OPTIONS.length,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
export function completions(docWithCursor: string) {
|
export function completions(docWithCursor: string, explicit = false) {
|
||||||
const cursorPosition = docWithCursor.indexOf('|');
|
const cursorPosition = docWithCursor.indexOf('|');
|
||||||
|
|
||||||
const doc = docWithCursor.slice(0, cursorPosition) + docWithCursor.slice(cursorPosition + 1);
|
const doc = docWithCursor.slice(0, cursorPosition) + docWithCursor.slice(cursorPosition + 1);
|
||||||
|
@ -612,7 +627,7 @@ export function completions(docWithCursor: string) {
|
||||||
extensions: [n8nLang()],
|
extensions: [n8nLang()],
|
||||||
});
|
});
|
||||||
|
|
||||||
const context = new CompletionContext(state, cursorPosition, false);
|
const context = new CompletionContext(state, cursorPosition, explicit);
|
||||||
|
|
||||||
for (const completionSource of state.languageDataAt<CompletionSource>(
|
for (const completionSource of state.languageDataAt<CompletionSource>(
|
||||||
'autocomplete',
|
'autocomplete',
|
||||||
|
|
|
@ -67,8 +67,7 @@ export function datatypeCompletions(context: CompletionContext): CompletionResul
|
||||||
|
|
||||||
if (word.from === word.to && !context.explicit) return null;
|
if (word.from === word.to && !context.explicit) return null;
|
||||||
|
|
||||||
// eslint-disable-next-line prefer-const
|
const [base, tail] = splitBaseTail(word.text);
|
||||||
let [base, tail] = splitBaseTail(word.text);
|
|
||||||
|
|
||||||
let options: Completion[] = [];
|
let options: Completion[] = [];
|
||||||
|
|
||||||
|
@ -102,14 +101,23 @@ export function datatypeCompletions(context: CompletionContext): CompletionResul
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.length === 0) return null;
|
|
||||||
|
|
||||||
if (tail !== '') {
|
if (tail !== '') {
|
||||||
options = options.filter((o) => prefixMatch(o.label, tail) && o.label !== tail);
|
options = options.filter((o) => prefixMatch(o.label, tail) && o.label !== tail);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let from = word.to - tail.length;
|
||||||
|
|
||||||
|
// When autocomplete is explicitely opened (by Ctrl+Space or programatically), add completions for the current word with '.' prefix
|
||||||
|
// example: {{ $json.str| }} -> ['length', 'includes()'...] (would usually need a '.' suffix)
|
||||||
|
if (context.explicit && !word.text.endsWith('.') && options.length === 0) {
|
||||||
|
options = explicitDataTypeOptions(word.text);
|
||||||
|
from = word.to;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.length === 0) return null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
from: word.to - tail.length,
|
from,
|
||||||
options,
|
options,
|
||||||
filter: false,
|
filter: false,
|
||||||
getMatch(completion: Completion) {
|
getMatch(completion: Completion) {
|
||||||
|
@ -120,6 +128,20 @@ export function datatypeCompletions(context: CompletionContext): CompletionResul
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function explicitDataTypeOptions(expression: string): Completion[] {
|
||||||
|
try {
|
||||||
|
const resolved = resolveParameter(`={{ ${expression} }}`);
|
||||||
|
return datatypeOptions({
|
||||||
|
resolved,
|
||||||
|
base: expression,
|
||||||
|
tail: '',
|
||||||
|
transformLabel: (label) => '.' + label,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function datatypeOptions(input: AutocompleteInput): Completion[] {
|
function datatypeOptions(input: AutocompleteInput): Completion[] {
|
||||||
const { resolved } = input;
|
const { resolved } = input;
|
||||||
|
|
||||||
|
@ -134,11 +156,11 @@ function datatypeOptions(input: AutocompleteInput): Completion[] {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resolved instanceof DateTime) {
|
if (resolved instanceof DateTime) {
|
||||||
return luxonOptions();
|
return luxonOptions(input as AutocompleteInput<DateTime>);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resolved instanceof Date) {
|
if (resolved instanceof Date) {
|
||||||
return dateOptions();
|
return dateOptions(input as AutocompleteInput<Date>);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(resolved)) {
|
if (Array.isArray(resolved)) {
|
||||||
|
@ -152,18 +174,33 @@ function datatypeOptions(input: AutocompleteInput): Completion[] {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const natives = (typeName: ExtensionTypeName): Completion[] => {
|
export const natives = (
|
||||||
|
typeName: ExtensionTypeName,
|
||||||
|
transformLabel: (label: string) => string = (label) => label,
|
||||||
|
): Completion[] => {
|
||||||
const natives: NativeDoc = NativeMethods.find((ee) => ee.typeName.toLowerCase() === typeName);
|
const natives: NativeDoc = NativeMethods.find((ee) => ee.typeName.toLowerCase() === typeName);
|
||||||
|
|
||||||
if (!natives) return [];
|
if (!natives) return [];
|
||||||
|
|
||||||
const nativeProps = natives.properties ? toOptions(natives.properties, typeName, 'keyword') : [];
|
const nativeProps = natives.properties
|
||||||
const nativeMethods = toOptions(natives.functions, typeName, 'native-function');
|
? toOptions(natives.properties, typeName, 'keyword', false, transformLabel)
|
||||||
|
: [];
|
||||||
|
const nativeMethods = toOptions(
|
||||||
|
natives.functions,
|
||||||
|
typeName,
|
||||||
|
'native-function',
|
||||||
|
false,
|
||||||
|
transformLabel,
|
||||||
|
);
|
||||||
|
|
||||||
return [...nativeProps, ...nativeMethods];
|
return [...nativeProps, ...nativeMethods];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const extensions = (typeName: ExtensionTypeName, includeHidden = false) => {
|
export const extensions = (
|
||||||
|
typeName: ExtensionTypeName,
|
||||||
|
includeHidden = false,
|
||||||
|
transformLabel: (label: string) => string = (label) => label,
|
||||||
|
) => {
|
||||||
const extensions = ExpressionExtensions.find((ee) => ee.typeName.toLowerCase() === typeName);
|
const extensions = ExpressionExtensions.find((ee) => ee.typeName.toLowerCase() === typeName);
|
||||||
|
|
||||||
if (!extensions) return [];
|
if (!extensions) return [];
|
||||||
|
@ -172,7 +209,7 @@ export const extensions = (typeName: ExtensionTypeName, includeHidden = false) =
|
||||||
return { ...acc, [fnName]: { doc: fn.doc } };
|
return { ...acc, [fnName]: { doc: fn.doc } };
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
return toOptions(fnToDoc, typeName, 'extension-function', includeHidden);
|
return toOptions(fnToDoc, typeName, 'extension-function', includeHidden, transformLabel);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const toOptions = (
|
export const toOptions = (
|
||||||
|
@ -180,12 +217,13 @@ export const toOptions = (
|
||||||
typeName: ExtensionTypeName,
|
typeName: ExtensionTypeName,
|
||||||
optionType: AutocompleteOptionType = 'native-function',
|
optionType: AutocompleteOptionType = 'native-function',
|
||||||
includeHidden = false,
|
includeHidden = false,
|
||||||
|
transformLabel: (label: string) => string = (label) => label,
|
||||||
) => {
|
) => {
|
||||||
return Object.entries(fnToDoc)
|
return Object.entries(fnToDoc)
|
||||||
.sort((a, b) => a[0].localeCompare(b[0]))
|
.sort((a, b) => a[0].localeCompare(b[0]))
|
||||||
.filter(([, docInfo]) => !docInfo.doc?.hidden || includeHidden)
|
.filter(([, docInfo]) => !docInfo.doc?.hidden || includeHidden)
|
||||||
.map(([fnName, docInfo]) => {
|
.map(([fnName, docInfo]) => {
|
||||||
return createCompletionOption(typeName, fnName, optionType, docInfo);
|
return createCompletionOption(typeName, fnName, optionType, docInfo, transformLabel);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -194,6 +232,7 @@ const createCompletionOption = (
|
||||||
name: string,
|
name: string,
|
||||||
optionType: AutocompleteOptionType,
|
optionType: AutocompleteOptionType,
|
||||||
docInfo: { doc?: DocMetadata | undefined },
|
docInfo: { doc?: DocMetadata | undefined },
|
||||||
|
transformLabel: (label: string) => string = (label) => label,
|
||||||
): Completion => {
|
): Completion => {
|
||||||
const isFunction = isFunctionOption(optionType);
|
const isFunction = isFunctionOption(optionType);
|
||||||
const label = isFunction ? name + '()' : name;
|
const label = isFunction ? name + '()' : name;
|
||||||
|
@ -201,7 +240,7 @@ const createCompletionOption = (
|
||||||
label,
|
label,
|
||||||
type: optionType,
|
type: optionType,
|
||||||
section: docInfo.doc?.section,
|
section: docInfo.doc?.section,
|
||||||
apply: applyCompletion(hasRequiredArgs(docInfo?.doc)),
|
apply: applyCompletion(hasRequiredArgs(docInfo?.doc), transformLabel),
|
||||||
};
|
};
|
||||||
|
|
||||||
option.info = () => {
|
option.info = () => {
|
||||||
|
@ -304,7 +343,7 @@ const createPropHeader = (typeName: string, property: { doc?: DocMetadata | unde
|
||||||
};
|
};
|
||||||
|
|
||||||
const objectOptions = (input: AutocompleteInput<IDataObject>): Completion[] => {
|
const objectOptions = (input: AutocompleteInput<IDataObject>): Completion[] => {
|
||||||
const { base, resolved } = input;
|
const { base, resolved, transformLabel } = input;
|
||||||
const rank = setRank(['item', 'all', 'first', 'last']);
|
const rank = setRank(['item', 'all', 'first', 'last']);
|
||||||
const SKIP = new Set(['__ob__', 'pairedItem']);
|
const SKIP = new Set(['__ob__', 'pairedItem']);
|
||||||
|
|
||||||
|
@ -338,17 +377,23 @@ const objectOptions = (input: AutocompleteInput<IDataObject>): Completion[] => {
|
||||||
label: isFunction ? key + '()' : key,
|
label: isFunction ? key + '()' : key,
|
||||||
type: isFunction ? 'function' : 'keyword',
|
type: isFunction ? 'function' : 'keyword',
|
||||||
section: getObjectPropertySection({ name, key, isFunction }),
|
section: getObjectPropertySection({ name, key, isFunction }),
|
||||||
apply: applyCompletion(hasArgs),
|
apply: applyCompletion(hasArgs, transformLabel),
|
||||||
};
|
};
|
||||||
|
|
||||||
const infoKey = [name, key].join('.');
|
const infoKey = [name, key].join('.');
|
||||||
option.info = createCompletionOption('', key, isFunction ? 'native-function' : 'keyword', {
|
option.info = createCompletionOption(
|
||||||
doc: {
|
'',
|
||||||
name: key,
|
key,
|
||||||
returnType: typeof resolvedProp,
|
isFunction ? 'native-function' : 'keyword',
|
||||||
description: i18n.proxyVars[infoKey],
|
{
|
||||||
|
doc: {
|
||||||
|
name: key,
|
||||||
|
returnType: typeof resolvedProp,
|
||||||
|
description: i18n.proxyVars[infoKey],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}).info;
|
transformLabel,
|
||||||
|
).info;
|
||||||
|
|
||||||
return option;
|
return option;
|
||||||
});
|
});
|
||||||
|
@ -448,8 +493,11 @@ const isUrl = (url: string): boolean => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const stringOptions = (input: AutocompleteInput<string>): Completion[] => {
|
const stringOptions = (input: AutocompleteInput<string>): Completion[] => {
|
||||||
const { resolved, tail } = input;
|
const { resolved, transformLabel } = input;
|
||||||
const options = sortCompletionsAlpha([...natives('string'), ...extensions('string')]);
|
const options = sortCompletionsAlpha([
|
||||||
|
...natives('string', transformLabel),
|
||||||
|
...extensions('string', false, transformLabel),
|
||||||
|
]);
|
||||||
|
|
||||||
if (validateFieldType('string', resolved, 'number').valid) {
|
if (validateFieldType('string', resolved, 'number').valid) {
|
||||||
return applySections({
|
return applySections({
|
||||||
|
@ -491,8 +539,11 @@ const stringOptions = (input: AutocompleteInput<string>): Completion[] => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const numberOptions = (input: AutocompleteInput<number>): Completion[] => {
|
const numberOptions = (input: AutocompleteInput<number>): Completion[] => {
|
||||||
const { resolved } = input;
|
const { resolved, transformLabel } = input;
|
||||||
const options = sortCompletionsAlpha([...natives('number'), ...extensions('number')]);
|
const options = sortCompletionsAlpha([
|
||||||
|
...natives('number', transformLabel),
|
||||||
|
...extensions('number', false, transformLabel),
|
||||||
|
]);
|
||||||
const ONLY_INTEGER = ['isEven()', 'isOdd()'];
|
const ONLY_INTEGER = ['isEven()', 'isOdd()'];
|
||||||
|
|
||||||
if (Number.isInteger(resolved)) {
|
if (Number.isInteger(resolved)) {
|
||||||
|
@ -509,17 +560,26 @@ const numberOptions = (input: AutocompleteInput<number>): Completion[] => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const dateOptions = (): Completion[] => {
|
const dateOptions = (input: AutocompleteInput<Date>): Completion[] => {
|
||||||
return applySections({
|
return applySections({
|
||||||
options: sortCompletionsAlpha([...natives('date'), ...extensions('date', true)]),
|
options: sortCompletionsAlpha([
|
||||||
|
...natives('date', input.transformLabel),
|
||||||
|
...extensions('date', true, input.transformLabel),
|
||||||
|
]),
|
||||||
recommended: DATE_RECOMMENDED_OPTIONS,
|
recommended: DATE_RECOMMENDED_OPTIONS,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const luxonOptions = (): Completion[] => {
|
const luxonOptions = (input: AutocompleteInput<DateTime>): Completion[] => {
|
||||||
return applySections({
|
return applySections({
|
||||||
options: sortCompletionsAlpha(
|
options: sortCompletionsAlpha(
|
||||||
uniqBy([...extensions('date'), ...luxonInstanceOptions()], (option) => option.label),
|
uniqBy(
|
||||||
|
[
|
||||||
|
...extensions('date', false, input.transformLabel),
|
||||||
|
...luxonInstanceOptions(false, input.transformLabel),
|
||||||
|
],
|
||||||
|
(option) => option.label,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
recommended: LUXON_RECOMMENDED_OPTIONS,
|
recommended: LUXON_RECOMMENDED_OPTIONS,
|
||||||
sections: LUXON_SECTIONS,
|
sections: LUXON_SECTIONS,
|
||||||
|
@ -527,9 +587,12 @@ const luxonOptions = (): Completion[] => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const arrayOptions = (input: AutocompleteInput<unknown[]>): Completion[] => {
|
const arrayOptions = (input: AutocompleteInput<unknown[]>): Completion[] => {
|
||||||
const { resolved } = input;
|
const { resolved, transformLabel } = input;
|
||||||
const options = applySections({
|
const options = applySections({
|
||||||
options: sortCompletionsAlpha([...natives('array'), ...extensions('array')]),
|
options: sortCompletionsAlpha([
|
||||||
|
...natives('array', transformLabel),
|
||||||
|
...extensions('array', false, transformLabel),
|
||||||
|
]),
|
||||||
recommended: ARRAY_RECOMMENDED_OPTIONS,
|
recommended: ARRAY_RECOMMENDED_OPTIONS,
|
||||||
methodsSection: OTHER_SECTION,
|
methodsSection: OTHER_SECTION,
|
||||||
propSection: OTHER_SECTION,
|
propSection: OTHER_SECTION,
|
||||||
|
@ -620,7 +683,10 @@ export const secretProvidersOptions = () => {
|
||||||
/**
|
/**
|
||||||
* Methods and fields defined on a Luxon `DateTime` class instance.
|
* Methods and fields defined on a Luxon `DateTime` class instance.
|
||||||
*/
|
*/
|
||||||
export const luxonInstanceOptions = (includeHidden = false) => {
|
export const luxonInstanceOptions = (
|
||||||
|
includeHidden = false,
|
||||||
|
transformLabel: (label: string) => string = (label) => label,
|
||||||
|
) => {
|
||||||
const SKIP = new Set(['constructor', 'get', 'invalidExplanation', 'invalidReason']);
|
const SKIP = new Set(['constructor', 'get', 'invalidExplanation', 'invalidReason']);
|
||||||
|
|
||||||
return Object.entries(Object.getOwnPropertyDescriptors(DateTime.prototype))
|
return Object.entries(Object.getOwnPropertyDescriptors(DateTime.prototype))
|
||||||
|
@ -635,6 +701,7 @@ export const luxonInstanceOptions = (includeHidden = false) => {
|
||||||
luxonInstanceDocs,
|
luxonInstanceDocs,
|
||||||
i18n.luxonInstance,
|
i18n.luxonInstance,
|
||||||
includeHidden,
|
includeHidden,
|
||||||
|
transformLabel,
|
||||||
) as Completion;
|
) as Completion;
|
||||||
})
|
})
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
@ -667,6 +734,7 @@ const createLuxonAutocompleteOption = (
|
||||||
docDefinition: NativeDoc,
|
docDefinition: NativeDoc,
|
||||||
translations: Record<string, string | undefined>,
|
translations: Record<string, string | undefined>,
|
||||||
includeHidden = false,
|
includeHidden = false,
|
||||||
|
transformLabel: (label: string) => string = (label) => label,
|
||||||
): Completion | null => {
|
): Completion | null => {
|
||||||
const isFunction = isFunctionOption(type);
|
const isFunction = isFunctionOption(type);
|
||||||
const label = isFunction ? name + '()' : name;
|
const label = isFunction ? name + '()' : name;
|
||||||
|
@ -696,12 +764,18 @@ const createLuxonAutocompleteOption = (
|
||||||
label,
|
label,
|
||||||
type,
|
type,
|
||||||
section: doc?.section,
|
section: doc?.section,
|
||||||
apply: applyCompletion(hasRequiredArgs(doc)),
|
apply: applyCompletion(hasRequiredArgs(doc), transformLabel),
|
||||||
};
|
};
|
||||||
option.info = createCompletionOption('DateTime', name, type, {
|
option.info = createCompletionOption(
|
||||||
// Add translated description
|
'DateTime',
|
||||||
doc: { ...doc, description: translations[name] } as DocMetadata,
|
name,
|
||||||
}).info;
|
type,
|
||||||
|
{
|
||||||
|
// Add translated description
|
||||||
|
doc: { ...doc, description: translations[name] } as DocMetadata,
|
||||||
|
},
|
||||||
|
transformLabel,
|
||||||
|
).info;
|
||||||
return option;
|
return option;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -13,4 +13,5 @@ export type AutocompleteInput<R = Resolved> = {
|
||||||
resolved: R;
|
resolved: R;
|
||||||
base: string;
|
base: string;
|
||||||
tail: string;
|
tail: string;
|
||||||
|
transformLabel?: (label: string) => string;
|
||||||
};
|
};
|
||||||
|
|
|
@ -157,15 +157,16 @@ export const stripExcessParens = (context: CompletionContext) => (option: Comple
|
||||||
* @example `$max()` -> `$max()<cursor>`
|
* @example `$max()` -> `$max()<cursor>`
|
||||||
*/
|
*/
|
||||||
export const applyCompletion =
|
export const applyCompletion =
|
||||||
(hasArgs = true) =>
|
(hasArgs = true, transform: (label: string) => string = (label) => label) =>
|
||||||
(view: EditorView, completion: Completion, from: number, to: number): void => {
|
(view: EditorView, completion: Completion, from: number, to: number): void => {
|
||||||
|
const label = transform(completion.label);
|
||||||
const tx: TransactionSpec = {
|
const tx: TransactionSpec = {
|
||||||
...insertCompletionText(view.state, completion.label, from, to),
|
...insertCompletionText(view.state, label, from, to),
|
||||||
annotations: pickedCompletion.of(completion),
|
annotations: pickedCompletion.of(completion),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (completion.label.endsWith('()') && hasArgs) {
|
if (label.endsWith('()') && hasArgs) {
|
||||||
const cursorPosition = from + completion.label.length - 1;
|
const cursorPosition = from + label.length - 1;
|
||||||
tx.selection = { anchor: cursorPosition, head: cursorPosition };
|
tx.selection = { anchor: cursorPosition, head: cursorPosition };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -510,9 +510,6 @@
|
||||||
"dataMapping.dragFromPreviousHint": "Map data from previous nodes to <b>{name}</b> by first clicking this button",
|
"dataMapping.dragFromPreviousHint": "Map data from previous nodes to <b>{name}</b> by first clicking this button",
|
||||||
"dataMapping.success.title": "You just mapped some data!",
|
"dataMapping.success.title": "You just mapped some data!",
|
||||||
"dataMapping.success.moreInfo": "Check out our <a href=\"https://docs.n8n.io/data/data-mapping\" target=\"_blank\">docs</a> for more details on mapping data in n8n",
|
"dataMapping.success.moreInfo": "Check out our <a href=\"https://docs.n8n.io/data/data-mapping\" target=\"_blank\">docs</a> for more details on mapping data in n8n",
|
||||||
"dataMapping.tableHint": "<img src='/static/data-mapping-gif.gif'/><br/> Drag a column onto <b>{name}</b> to map it",
|
|
||||||
"dataMapping.jsonHint": "<img src='/static/json-mapping-gif.gif'/><br/> Drag a JSON key onto <b>{name}</b> to map data",
|
|
||||||
"dataMapping.schemaHint": "<img src='/static/schema-mapping-gif.gif'/><br/> Drag a datapill onto <b>{name}</b> to map data",
|
|
||||||
"dataMapping.tableView.tableColumnsExceeded": "Some columns are hidden",
|
"dataMapping.tableView.tableColumnsExceeded": "Some columns are hidden",
|
||||||
"dataMapping.tableView.tableColumnsExceeded.tooltip": "Your data has more than {columnLimit} columns so some are hidden. Switch to {link} to see all data.",
|
"dataMapping.tableView.tableColumnsExceeded.tooltip": "Your data has more than {columnLimit} columns so some are hidden. Switch to {link} to see all data.",
|
||||||
"dataMapping.tableView.tableColumnsExceeded.tooltip.link": "JSON view",
|
"dataMapping.tableView.tableColumnsExceeded.tooltip.link": "JSON view",
|
||||||
|
@ -682,6 +679,7 @@
|
||||||
"expressionModalInput.empty": "[empty]",
|
"expressionModalInput.empty": "[empty]",
|
||||||
"expressionModalInput.undefined": "[undefined]",
|
"expressionModalInput.undefined": "[undefined]",
|
||||||
"expressionModalInput.null": "null",
|
"expressionModalInput.null": "null",
|
||||||
|
"expressionTip.noExecutionData": "Execute previous nodes to use input data",
|
||||||
"expressionModalInput.noExecutionData": "Execute previous nodes for preview",
|
"expressionModalInput.noExecutionData": "Execute previous nodes for preview",
|
||||||
"expressionModalInput.noNodeExecutionData": "Execute node ‘{node}’ for preview",
|
"expressionModalInput.noNodeExecutionData": "Execute node ‘{node}’ for preview",
|
||||||
"expressionModalInput.noInputConnection": "No input connected",
|
"expressionModalInput.noInputConnection": "No input connected",
|
||||||
|
@ -1197,8 +1195,12 @@
|
||||||
"openWorkflow.workflowImportError": "Could not import workflow",
|
"openWorkflow.workflowImportError": "Could not import workflow",
|
||||||
"openWorkflow.workflowNotFoundError": "Could not find workflow",
|
"openWorkflow.workflowNotFoundError": "Could not find workflow",
|
||||||
"parameterInput.expressionResult": "e.g. {result}",
|
"parameterInput.expressionResult": "e.g. {result}",
|
||||||
|
"parameterInput.tip": "Tip",
|
||||||
"parameterInput.anythingInside": "Anything inside ",
|
"parameterInput.anythingInside": "Anything inside ",
|
||||||
"parameterInput.isJavaScript": " is JavaScript.",
|
"parameterInput.isJavaScript": " is JavaScript.",
|
||||||
|
"parameterInput.dragTipBeforePill": "Drag an",
|
||||||
|
"parameterInput.inputField": "input field",
|
||||||
|
"parameterInput.dragTipAfterPill": "from the left to use it here.",
|
||||||
"parameterInput.learnMore": "Learn more",
|
"parameterInput.learnMore": "Learn more",
|
||||||
"parameterInput.resultForItem": "Result for Item",
|
"parameterInput.resultForItem": "Result for Item",
|
||||||
"parameterInput.emptyString": "[empty]",
|
"parameterInput.emptyString": "[empty]",
|
||||||
|
|
|
@ -7,7 +7,11 @@ import type {
|
||||||
XYPosition,
|
XYPosition,
|
||||||
} from '@/Interface';
|
} from '@/Interface';
|
||||||
import { useStorage } from '@/composables/useStorage';
|
import { useStorage } from '@/composables/useStorage';
|
||||||
import { LOCAL_STORAGE_MAPPING_IS_ONBOARDED, STORES } from '@/constants';
|
import {
|
||||||
|
LOCAL_STORAGE_AUTOCOMPLETE_IS_ONBOARDED,
|
||||||
|
LOCAL_STORAGE_MAPPING_IS_ONBOARDED,
|
||||||
|
STORES,
|
||||||
|
} from '@/constants';
|
||||||
import type { INodeExecutionData, INodeIssues } from 'n8n-workflow';
|
import type { INodeExecutionData, INodeIssues } from 'n8n-workflow';
|
||||||
import { NodeConnectionType } from 'n8n-workflow';
|
import { NodeConnectionType } from 'n8n-workflow';
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
|
@ -50,6 +54,7 @@ export const useNDVStore = defineStore(STORES.NDV, {
|
||||||
activeTarget: null,
|
activeTarget: null,
|
||||||
},
|
},
|
||||||
isMappingOnboarded: useStorage(LOCAL_STORAGE_MAPPING_IS_ONBOARDED).value === 'true',
|
isMappingOnboarded: useStorage(LOCAL_STORAGE_MAPPING_IS_ONBOARDED).value === 'true',
|
||||||
|
isAutocompleteOnboarded: useStorage(LOCAL_STORAGE_AUTOCOMPLETE_IS_ONBOARDED).value === 'true',
|
||||||
}),
|
}),
|
||||||
getters: {
|
getters: {
|
||||||
activeNode(): INodeUi | null {
|
activeNode(): INodeUi | null {
|
||||||
|
@ -77,7 +82,9 @@ export const useNDVStore = defineStore(STORES.NDV, {
|
||||||
},
|
},
|
||||||
hasInputData(): boolean {
|
hasInputData(): boolean {
|
||||||
const data = this.ndvInputData;
|
const data = this.ndvInputData;
|
||||||
return data && data.length > 0;
|
const pinData =
|
||||||
|
this.ndvInputNodeName && useWorkflowsStore().pinDataByNodeName(this.ndvInputNodeName);
|
||||||
|
return !!(data && data.length > 0) || !!(pinData && pinData.length > 0);
|
||||||
},
|
},
|
||||||
getPanelDisplayMode() {
|
getPanelDisplayMode() {
|
||||||
return (panel: NodePanelType) => this[panel].displayMode;
|
return (panel: NodePanelType) => this[panel].displayMode;
|
||||||
|
@ -236,11 +243,13 @@ export const useNDVStore = defineStore(STORES.NDV, {
|
||||||
setNDVPanelDataIsEmpty(payload: { panel: 'input' | 'output'; isEmpty: boolean }): void {
|
setNDVPanelDataIsEmpty(payload: { panel: 'input' | 'output'; isEmpty: boolean }): void {
|
||||||
this[payload.panel].data.isEmpty = payload.isEmpty;
|
this[payload.panel].data.isEmpty = payload.isEmpty;
|
||||||
},
|
},
|
||||||
disableMappingHint(store = true) {
|
setMappingOnboarded() {
|
||||||
this.isMappingOnboarded = true;
|
this.isMappingOnboarded = true;
|
||||||
if (store) {
|
useStorage(LOCAL_STORAGE_MAPPING_IS_ONBOARDED).value = 'true';
|
||||||
useStorage(LOCAL_STORAGE_MAPPING_IS_ONBOARDED).value = 'true';
|
},
|
||||||
}
|
setAutocompleteOnboarded() {
|
||||||
|
this.isAutocompleteOnboarded = true;
|
||||||
|
useStorage(LOCAL_STORAGE_AUTOCOMPLETE_IS_ONBOARDED).value = 'true';
|
||||||
},
|
},
|
||||||
updateNodeParameterIssues(issues: INodeIssues): void {
|
updateNodeParameterIssues(issues: INodeIssues): void {
|
||||||
const workflowsStore = useWorkflowsStore();
|
const workflowsStore = useWorkflowsStore();
|
||||||
|
|
Loading…
Reference in a new issue