n8n/packages/editor-ui/src/components/VariableSelector.vue
Mutasem Aldmour 5bd8f7c93e
Implement design system (#2050)
* split up main, sass imports, import new nds

* migrate most buttons

* update sizes based on feedback

* update copy buttons

* update executions list

* fix issues

* force message box buttons

* update warning color

* update more buttons

* wrap message box buttons

* update last component

* lint fixes

* add build report step

* breakout imports

* set package.json

* fix notification bug

* clean up imports

* use build directories directly

* update imports

* remove xl size

* update number inputs

* fix input width

* update line height, fix icon bug

* fix up editor

* fix spacing between buttons

* Reset line height

* revert changes to this

* revert changes

* clean up button sizes

* change to outline

* update select height

* update tooltip

* remove build report step

* clean up impl

* remove regenerator runtime

* add design system repo

* apply editorconfig

* apply editor config prettier

* lint issue

* switch to tabs

* switch to single space

* update eslintrc

* remove git modules

* update sass package

* support dart sass

* add build

* update dependency

* update contributing.md

* set repo

* update versions

* add tslint step

* update spacing to spaces, add dev step

* add test step

* add test step

* update browser lint rc

* remove .github

* delete .gitignore

* set comment for icons

* remove preview link

* update button interface

* update types

* set types

* clean up intro

* update intro

* remove assets

* move into preview

* remove headline comment

* reduce theme build

* loading executions

* match deps versions

* match deps versions

* fix lint issues

* fix lint issues

* update callback

* disable codacy for docs.css

* fix storybook issues

* add design system to docker image

* update spacing around delete sort button

* set line height to stop juggling headline

* update sizes

* clean up vars

* fix scss issues

* update button vars

* add shade color

* fix button without click

* fix buttons bug

* fix bug with executions list

* clean up theme

* update link styling

* fix typo

* run prettier

* 🎨 code format

* 🎨 code format

* 🔥 remove empty files

*  N8n 2284 new inputs (#2075)

* implement inputs

* prettier fixes

* revert unnessary change

* move input components and tooltip

* remove form elements

* move select

* update input placements

* update sizes

* update credentails

* clean up select size

* fix caret issue

* update inputs

* clean up select

* fix tags dropdown border

* clean up tags input

* fix workflow name bug

* clean up select here

* add sizes template

* fix option caret

* fix input sizes

* update date input size

* remove tags input override

* update prop

* update input size

* center run data inputs

* update disabled colors

* update execution header

* update scrollbar

* update text area spacing

* fix items in header

* update run data tooltip

* remove popover

* update prefix positions

* add filterable demo

* address design issues

* fix input issues, flip boolean input to text

* update input sufffix colors

* remove override

* speed up switch, fix toggle spacing issue

* update icon

* remove icon classes

* clean up inputs

* clean up inputs with icons

* update input spacing again

* update suffix position

* build

* Add support for xlarge inputs

* fix input issues

* fix input issue

* update listeners

* update number inputs for settings

* update append/prepend spacing

* clean up inputs, set expression input as text

* fix type errors

* fix workflow number input

* fix tags dropdown bug

* fix bugs

* fix menu item bug

* remove font weight from link element

* remove default

* fix select option

* fix contrast issues

* allow overflow x for multi selects

* fix icon

* update options select

* fix issue that resolves expression to null

* update how actions are centered

* fix up selects

* update selects to support limiting size

* update option styles

*  Apply suggestions BHesseldieck

Co-authored-by: Ben Hesseldieck <1849459+BHesseldieck@users.noreply.github.com>

* 🎨 code format

Co-authored-by: Jan <janober@users.noreply.github.com>
Co-authored-by: Ben Hesseldieck <1849459+BHesseldieck@users.noreply.github.com>
Co-authored-by: Ben Hesseldieck <b.hesseldieck@gmail.com>

*  Revert "🔥 remove empty files"

This reverts commit e91ace4e52.

*  Remove private from n8n-design-system package

* 🎨 Change to spaces to stay consistent with editorconfig & others
package files

*  Fix year in license

Co-authored-by: Ben Hesseldieck <1849459+BHesseldieck@users.noreply.github.com>
Co-authored-by: Ben Hesseldieck <b.hesseldieck@gmail.com>
Co-authored-by: Jan <janober@users.noreply.github.com>
Co-authored-by: Jan Oberhauser <jan.oberhauser@gmail.com>
2021-08-29 13:36:17 +02:00

644 lines
19 KiB
Vue

<template>
<div @keydown.stop class="variable-selector-wrapper">
<div class="input-wrapper">
<n8n-input placeholder="Variable filter..." v-model="variableFilter" ref="inputField" size="small" type="text"></n8n-input>
</div>
<div class="result-wrapper">
<variable-selector-item :item="option" v-for="option in currentResults" :key="option.key" :extendAll="extendAll" @itemSelected="forwardItemSelected"></variable-selector-item>
</div>
</div>
</template>
<script lang="ts">
import {
PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
} from '@/constants';
import {
GenericValue,
IContextObject,
IDataObject,
IRunData,
IRunExecutionData,
IWorkflowDataProxyAdditionalKeys,
Workflow,
WorkflowDataProxy,
} from 'n8n-workflow';
import VariableSelectorItem from '@/components/VariableSelectorItem.vue';
import {
IExecutionResponse,
INodeUi,
IVariableItemSelected,
IVariableSelectorOption,
} from '@/Interface';
import { workflowHelpers } from '@/components/mixins/workflowHelpers';
import mixins from 'vue-typed-mixins';
export default mixins(
workflowHelpers,
)
.extend({
name: 'VariableSelector',
components: {
VariableSelectorItem,
},
props: [
'path',
],
data () {
return {
variableFilter: '',
selectorOpenInputIndex: null as number | null,
};
},
computed: {
extendAll (): boolean {
if (this.variableFilter) {
return true;
}
return false;
},
currentResults (): IVariableSelectorOption[] {
return this.getFilterResults(this.variableFilter.toLowerCase(), 0);
},
workflow (): Workflow {
return this.getWorkflow();
},
},
methods: {
forwardItemSelected (eventData: IVariableItemSelected) {
this.$emit('itemSelected', eventData);
},
sortOptions (options: IVariableSelectorOption[] | null): IVariableSelectorOption[] | null {
if (options === null) {
return null;
}
return options.sort((a: IVariableSelectorOption, b: IVariableSelectorOption) => {
const aHasOptions = a.hasOwnProperty('options');
const bHasOptions = b.hasOwnProperty('options');
if (bHasOptions && !aHasOptions) {
// When b has options but a not list it first
return 1;
} else if (!bHasOptions && aHasOptions) {
// When a has options but b not list it first
return -1;
}
// Else simply sort alphabetically
if (a.name < b.name) { return -1; }
if (a.name > b.name) { return 1; }
return 0;
});
},
removeEmptyEntries (inputData: IVariableSelectorOption[] | IVariableSelectorOption | null): IVariableSelectorOption[] | IVariableSelectorOption | null {
if (Array.isArray(inputData)) {
const newItems: IVariableSelectorOption[] = [];
let tempItem: IVariableSelectorOption;
inputData.forEach((item) => {
tempItem = this.removeEmptyEntries(item) as IVariableSelectorOption;
if (tempItem !== null) {
newItems.push(tempItem);
}
});
return newItems;
}
if (inputData && inputData.options) {
const newOptions = this.removeEmptyEntries(inputData.options);
if (Array.isArray(newOptions) && newOptions.length) {
// Has still options left so return
inputData.options = this.sortOptions(newOptions);
return inputData;
} else if (Array.isArray(newOptions) && newOptions.length === 0) {
delete inputData.options;
return inputData;
}
// Has no options left so remove
return null;
} else {
// Is an item no category
return inputData;
}
},
// Normalizes the path so compare paths which have use dots or brakets
getPathNormalized (path: string | undefined): string {
if (path === undefined) {
return '';
}
const pathArray = path.split('.');
const finalArray = [];
let item: string;
for (const pathPart of pathArray) {
const pathParts = pathPart.match(/\[.*?\]/g);
if (pathParts === null) {
// Does not have any brakets so add as it is
finalArray.push(pathPart);
} else {
// Has brakets so clean up the items and add them
if (pathPart.charAt(0) !== '[') {
// Does not start with a braket so there is a part before
// we have to add
finalArray.push(pathPart.substr(0, pathPart.indexOf('[')));
}
for (item of pathParts) {
item = item.slice(1, -1);
if (['"', "'"].includes(item.charAt(0))) {
// Is a string
item = item.slice(1, -1);
finalArray.push(item);
} else {
// Is a number
finalArray.push(`[${item}]`);
}
}
}
}
return finalArray.join('|');
},
jsonDataToFilterOption (inputData: IDataObject | GenericValue | IDataObject[] | GenericValue[] | null, parentPath: string, propertyName: string, filterText?: string, propertyIndex?: number, displayName?: string, skipKey?: string): IVariableSelectorOption[] {
let fullpath = `${parentPath}["${propertyName}"]`;
if (propertyIndex !== undefined) {
fullpath += `[${propertyIndex}]`;
}
const returnData: IVariableSelectorOption[] = [];
if (inputData === null) {
returnData.push(
{
name: propertyName,
key: fullpath,
value: '[null]',
} as IVariableSelectorOption,
);
return returnData;
} else if (Array.isArray(inputData)) {
let newPropertyName = propertyName;
let newParentPath = parentPath;
if (propertyIndex !== undefined) {
newParentPath += `["${propertyName}"]`;
newPropertyName = propertyIndex.toString();
}
const arrayData: IVariableSelectorOption[] = [];
for (let i = 0; i < inputData.length; i++) {
arrayData.push.apply(arrayData, this.jsonDataToFilterOption(inputData[i], newParentPath, newPropertyName, filterText, i, `[Item: ${i}]`, skipKey));
}
returnData.push(
{
name: displayName || propertyName,
options: arrayData,
key: fullpath,
allowParentSelect: true,
dataType: 'array',
} as IVariableSelectorOption,
);
} else if (typeof inputData === 'object') {
const tempValue: IVariableSelectorOption[] = [];
for (const key of Object.keys(inputData)) {
tempValue.push.apply(tempValue, this.jsonDataToFilterOption((inputData as IDataObject)[key], fullpath, key, filterText, undefined, undefined, skipKey));
}
if (tempValue.length) {
returnData.push(
{
name: displayName || propertyName,
options: this.sortOptions(tempValue),
key: fullpath,
allowParentSelect: true,
dataType: 'object',
} as IVariableSelectorOption,
);
}
} else {
if (filterText !== undefined && propertyName.toLowerCase().indexOf(filterText) === -1) {
return returnData;
}
// Skip is currently only needed for leafs so only check here
if (this.getPathNormalized(skipKey) !== this.getPathNormalized(fullpath)) {
returnData.push(
{
name: propertyName,
key: fullpath,
value: inputData,
} as IVariableSelectorOption,
);
}
}
return returnData;
},
/**
* Returns the data the a node does output
*
* @param {IRunData} runData The data of the run to get the data of
* @param {string} nodeName The name of the node to get the data of
* @param {string} filterText Filter text for parameters
* @param {number} [itemIndex=0] The index of the item
* @param {number} [runIndex=0] The index of the run
* @param {string} [inputName='main'] The name of the input
* @param {number} [outputIndex=0] The index of the output
* @returns
* @memberof Workflow
*/
getNodeOutputData (runData: IRunData, nodeName: string, filterText: string, itemIndex = 0, runIndex = 0, inputName = 'main', outputIndex = 0, useShort = false): IVariableSelectorOption[] | null {
if (!runData.hasOwnProperty(nodeName)) {
// No data found for node
return null;
}
if (runData[nodeName].length <= runIndex) {
// No data for given runIndex
return null;
}
if (!runData[nodeName][runIndex].hasOwnProperty('data') ||
runData[nodeName][runIndex].data === null ||
runData[nodeName][runIndex].data === undefined) {
// Data property does not exist or is not set (even though it normally has to)
return null;
}
if (!runData[nodeName][runIndex].data!.hasOwnProperty(inputName)) {
// No data found for inputName
return null;
}
if (runData[nodeName][runIndex].data![inputName].length <= outputIndex) {
// No data found for output Index
return null;
}
// The data should be identical no matter to which node it gets so always select the first one
if (runData[nodeName][runIndex].data![inputName][outputIndex] === null ||
runData[nodeName][runIndex].data![inputName][outputIndex]!.length <= itemIndex) {
// No data found for node connection found
return null;
}
const outputData = runData[nodeName][runIndex].data![inputName][outputIndex]![itemIndex];
const returnData: IVariableSelectorOption[] = [];
// Get json data
if (outputData.hasOwnProperty('json')) {
const jsonPropertyPrefix = useShort === true ? '$json' : `$node["${nodeName}"].json`;
const jsonDataOptions: IVariableSelectorOption[] = [];
for (const propertyName of Object.keys(outputData.json)) {
jsonDataOptions.push.apply(jsonDataOptions, this.jsonDataToFilterOption(outputData.json[propertyName], jsonPropertyPrefix, propertyName, filterText));
}
if (jsonDataOptions.length) {
returnData.push(
{
name: 'JSON',
options: this.sortOptions(jsonDataOptions),
},
);
}
}
// Get binary data
if (outputData.hasOwnProperty('binary')) {
const binaryPropertyPrefix = useShort === true ? '$binary' : `$node["${nodeName}"].binary`;
const binaryData = [];
let binaryPropertyData = [];
for (const dataPropertyName of Object.keys(outputData.binary!)) {
binaryPropertyData = [];
for (const propertyName in outputData.binary![dataPropertyName]) {
if (propertyName === 'data') {
continue;
}
if (filterText && propertyName.toLowerCase().indexOf(filterText) === -1) {
// If filter is set apply it
continue;
}
binaryPropertyData.push(
{
name: propertyName,
key: `${binaryPropertyPrefix}.${dataPropertyName}.${propertyName}`,
value: outputData.binary![dataPropertyName][propertyName],
},
);
}
if (binaryPropertyData.length) {
binaryData.push(
{
name: dataPropertyName,
key: `${binaryPropertyPrefix}.${dataPropertyName}`,
options: this.sortOptions(binaryPropertyData),
allowParentSelect: true,
},
);
}
}
if (binaryData.length) {
returnData.push(
{
name: 'Binary',
key: binaryPropertyPrefix,
options: this.sortOptions(binaryData),
allowParentSelect: true,
},
);
}
}
return returnData;
},
getNodeContext (workflow: Workflow, runExecutionData: IRunExecutionData | null, parentNode: string[], nodeName: string, filterText: string): IVariableSelectorOption[] | null {
const inputIndex = 0;
const itemIndex = 0;
const inputName = 'main';
const runIndex = 0;
const returnData: IVariableSelectorOption[] = [];
const connectionInputData = this.connectionInputData(parentNode, inputName, runIndex, inputIndex);
if (connectionInputData === null) {
return returnData;
}
const additionalKeys: IWorkflowDataProxyAdditionalKeys = {
$executionId: PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
$resumeWebhookUrl: PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
};
const dataProxy = new WorkflowDataProxy(workflow, runExecutionData, runIndex, itemIndex, nodeName, connectionInputData, {}, 'manual', additionalKeys);
const proxy = dataProxy.getDataProxy();
// @ts-ignore
const nodeContext = proxy.$node[nodeName].context as IContextObject;
for (const key of Object.keys(nodeContext)) {
if (filterText !== undefined && key.toLowerCase().indexOf(filterText) === -1) {
// If filter is set apply it
continue;
}
returnData.push({
name: key,
key: `$node["${nodeName}"].context["${key}"]`,
// @ts-ignore
value: nodeContext[key],
});
}
return returnData;
},
/**
* Returns all the node parameters with values
*
* @param {string} nodeName The name of the node to return data of
* @param {string} path The path to the node to pretend to key
* @param {string} [skipParameter] Parameter to skip
* @param {string} [filterText] Filter text for parameters
* @returns
* @memberof Workflow
*/
getNodeParameters (nodeName: string, path: string, skipParameter?: string, filterText?: string): IVariableSelectorOption[] | null {
const node = this.workflow.getNode(nodeName);
if (node === null) {
return null;
}
const returnParameters: IVariableSelectorOption[] = [];
for (const parameterName in node.parameters) {
if (parameterName === skipParameter) {
// Skip the parameter
continue;
}
if (filterText !== undefined && parameterName.toLowerCase().indexOf(filterText) === -1) {
// If filter is set apply it
continue;
}
returnParameters.push.apply(returnParameters, this.jsonDataToFilterOption(node.parameters[parameterName], path, parameterName, filterText, undefined, undefined, skipParameter));
}
return returnParameters;
},
getFilterResults (filterText: string, itemIndex: number): IVariableSelectorOption[] {
const inputName = 'main';
const activeNode: INodeUi | null = this.$store.getters.activeNode;
if (activeNode === null) {
return [];
}
const executionData = this.$store.getters.getWorkflowExecution as IExecutionResponse | null;
let parentNode = this.workflow.getParentNodes(activeNode.name, inputName, 1);
let runData = this.$store.getters.getWorkflowRunData as IRunData | null;
if (runData === null) {
runData = {};
}
let returnData: IVariableSelectorOption[] | null = [];
// -----------------------------------------
// Add the parameters of the current node
// -----------------------------------------
// Add the parameters
const currentNodeData: IVariableSelectorOption[] = [];
let tempOptions: IVariableSelectorOption[];
if (executionData !== null) {
const runExecutionData: IRunExecutionData = executionData.data;
tempOptions = this.getNodeContext(this.workflow, runExecutionData, parentNode, activeNode.name, filterText) as IVariableSelectorOption[];
if (tempOptions.length) {
currentNodeData.push(
{
name: 'Context',
options: this.sortOptions(tempOptions),
} as IVariableSelectorOption,
);
}
}
let tempOutputData;
if (parentNode.length) {
// If the node has an input node add the input data
// Check from which output to read the data.
// Depends on how the nodes are connected.
// (example "IF" node. If node is connected to "true" or to "false" output)
const outputIndex = this.workflow.getNodeConnectionOutputIndex(activeNode.name, parentNode[0], 'main');
tempOutputData = this.getNodeOutputData(runData, parentNode[0], filterText, itemIndex, 0, 'main', outputIndex, true) as IVariableSelectorOption[];
if (tempOutputData) {
if (JSON.stringify(tempOutputData).length < 102400) {
// Data is reasonable small (< 100kb) so add it
currentNodeData.push(
{
name: 'Input Data',
options: this.sortOptions(tempOutputData),
},
);
} else {
// Data is to large so do not add
currentNodeData.push(
{
name: 'Input Data',
options: [
{
name: '[Data to large]',
},
],
},
);
}
}
}
const initialPath = '$parameter';
let skipParameter = this.path;
if (skipParameter.startsWith('parameters.')) {
skipParameter = initialPath + skipParameter.substring(10);
}
currentNodeData.push(
{
name: 'Parameters',
options: this.sortOptions(this.getNodeParameters(activeNode.name, initialPath, skipParameter, filterText) as IVariableSelectorOption[]),
},
);
returnData.push(
{
name: 'Current Node',
options: this.sortOptions(currentNodeData),
},
);
// Add the input data
// -----------------------------------------
// Add all the nodes and their data
// -----------------------------------------
const allNodesData: IVariableSelectorOption[] = [];
let nodeOptions: IVariableSelectorOption[];
const upstreamNodes = this.workflow.getParentNodes(activeNode.name, inputName);
for (const nodeName of Object.keys(this.workflow.nodes)) {
// Add the parameters of all nodes
// TODO: Later have to make sure that no parameters can be referenced which have expression which use input-data (for nodes which are not parent nodes)
if (nodeName === activeNode.name) {
// Skip the current node as this one get added separately
continue;
}
nodeOptions = [
{
name: 'Parameters',
options: this.sortOptions(this.getNodeParameters(nodeName, `$node["${nodeName}"].parameter`, undefined, filterText)),
} as IVariableSelectorOption,
];
if (executionData !== null) {
const runExecutionData: IRunExecutionData = executionData.data;
parentNode = this.workflow.getParentNodes(nodeName, inputName, 1);
tempOptions = this.getNodeContext(this.workflow, runExecutionData, parentNode, nodeName, filterText) as IVariableSelectorOption[];
if (tempOptions.length) {
nodeOptions = [
{
name: 'Context',
options: this.sortOptions(tempOptions),
} as IVariableSelectorOption,
];
}
}
if (upstreamNodes.includes(nodeName)) {
// If the node is an upstream node add also the output data which can be referenced
tempOutputData = this.getNodeOutputData(runData, nodeName, filterText, itemIndex);
if (tempOutputData) {
nodeOptions.push(
{
name: 'Output Data',
options: this.sortOptions(tempOutputData),
} as IVariableSelectorOption,
);
}
}
allNodesData.push(
{
name: nodeName,
options: this.sortOptions(nodeOptions),
},
);
}
returnData.push(
{
name: 'Nodes',
options: this.sortOptions(allNodesData),
},
);
// Remove empty entries and return
returnData = this.removeEmptyEntries(returnData) as IVariableSelectorOption[] | null;
if (returnData === null) {
return [];
}
return returnData;
},
},
});
</script>
<style scoped lang="scss">
.variable-selector-wrapper {
border-radius: 0 0 4px 4px;
width: 100%;
height: 100%;
position: relative;
}
.result-wrapper {
line-height: 1em;
height: 370px;
overflow-x: hidden;
overflow-y: auto;
margin: 0.5em 0;
width: 100%;
}
.result-item {
font-size: 0.7em;
}
</style>