mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-28 22:52:16 +03:00
feat(editor): Overhaul expression editor modal (#4631)
* feat(editor): Integrate CodeMirror into expression editor modal (#4563) * ✨ Initial setup * 👕 Fix lint * ⚡ Extract segments * ⚡ Implement var insertion * 👕 Ignore `.d.cts` * ⚡ Refactor to simplify * ✨ Add brace handler * ✨ Fully replace input and output * feat(editor): Adjust resolved expression to match parameter input hint (#4600) * ✨ Initial adjustments * 🐛 Prevent empty decorations * ⚡ Adjust resolved expression to match param input hint * ✏️ Improve comment * 👕 Remove lint rule * ✏️ Fix typo * ✏️ Fix closing brace * ⚡ Clean up `displayableSegments()` * feat(editor): Apply styling to expression editor modal (#4607) 🎨 Apply styling * feat(core): Improve errors in evaluated expression (#4619) * 🐛 Fix env var access for FE * 🔥 Remove excess closing bracket * 🚧 Set up TODO * ✏️ Update copy * ⚡ Deny env vars access to FE * 👕 Remove unneeded lint exception * 📘 Remove unneeded typing * feat(editor): Dynamically delay evaluation resolution (#4625) * ✏️ Update copy * ⚡ Dynamically delay evaluation resolution * 🔥 Remove unneeded computed property * refactor(editor): Pre-review cleanup (#4627) * 🔥 Remove `ExpressionInput` component * 🔥 Remove Quill * ✏️ Rename i18n key * 🎨 Place border on correct element * 🐛 Handle syntax errors * ⚡ Add sample autocompletions * 🐛 Fix auto-extending behavior * feat(editor): Improve escaping behavior (#4641) * 🎨 Hide hint on small screen * ⚡ Improve escaping * refactor(editor): Apply styling feedback to expression editor modal (#4660) * 🎨 Restyle hint * 🎨 Restyle param input hint * 🔥 Remove `e.g.` * ⚡ Tweak delay * 🎨 Restyle output * 🎨 Tweak theme * ✏️ Tweak copy * refactor(editor): Apply feedback 2022.11.22 (#4697) * 🎨 Change background color * ⚡ Focus on mount * ⚡ Account for preexisting braces on injection * 🐛 Fix `$workflow` showing as not saved * ✏️ Tweak copy * 🐛 Fix readonly focus * ⚡ Focus input on paste * ⚡ Sync inputs with modal * ✏️ Tweak copy * refactor(editor): Apply feedback 2022.11.23 (#4705) * ⚡ Allow newlines * ⚡ Set cursor at end of content * ⚡ Do not defocus on paste on Chrome * ⚡ Fix import * 🧪 Add e2e tests * ⚡ Cleanup * ⚡ Add telemetry * 🔥 Remove log * ⚡ Expose error properties * 🧪 Rename test * ⚡ Move `getCurrentWorkflow()` call * ⏪ Revert highlighting removal per feedback * ⚡ Add i18n keys * 🚚 Move computed property to local state * 🎨 Use CSS vars * ⚡ Update `pnpm-lock.yaml` * ⚡ Apply readonly state * ⚡ Use prop * ⚡ Complete fix
This commit is contained in:
parent
830bda5f55
commit
59771c80ea
65
cypress/e2e/9-expression-editor-modal.cy.ts
Normal file
65
cypress/e2e/9-expression-editor-modal.cy.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
|
||||
|
||||
const WorkflowPage = new WorkflowPageClass();
|
||||
|
||||
describe('Expression editor modal', () => {
|
||||
before(() => {
|
||||
cy.task('db:reset');
|
||||
cy.skipSetup();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
WorkflowPage.actions.visit();
|
||||
WorkflowPage.actions.addInitialNodeToCanvas('Manual Trigger');
|
||||
WorkflowPage.actions.addNodeToCanvas('Hacker News');
|
||||
WorkflowPage.actions.openNodeNdv('Hacker News');
|
||||
WorkflowPage.actions.openExpressionEditor();
|
||||
});
|
||||
|
||||
it('should resolve primitive resolvables', () => {
|
||||
WorkflowPage.getters.expressionModalInput().type('{{');
|
||||
WorkflowPage.getters.expressionModalInput().type('1 + 2');
|
||||
WorkflowPage.getters.expressionModalOutput().contains(/^3$/);
|
||||
WorkflowPage.getters.expressionModalInput().clear();
|
||||
|
||||
WorkflowPage.getters.expressionModalInput().type('{{');
|
||||
WorkflowPage.getters.expressionModalInput().type('"ab" + "cd"');
|
||||
WorkflowPage.getters.expressionModalOutput().contains(/^abcd$/);
|
||||
|
||||
WorkflowPage.getters.expressionModalInput().clear();
|
||||
|
||||
WorkflowPage.getters.expressionModalInput().type('{{');
|
||||
WorkflowPage.getters.expressionModalInput().type('true && false');
|
||||
WorkflowPage.getters.expressionModalOutput().contains(/^false$/);
|
||||
});
|
||||
|
||||
it('should resolve object resolvables', () => {
|
||||
WorkflowPage.getters.expressionModalInput().type('{{');
|
||||
WorkflowPage.getters.expressionModalInput().type('{{} a: 1');
|
||||
WorkflowPage.getters.expressionModalOutput().contains(/^\[Object: \{"a":1\}\]$/);
|
||||
|
||||
WorkflowPage.getters.expressionModalInput().clear();
|
||||
|
||||
WorkflowPage.getters.expressionModalInput().type('{{');
|
||||
WorkflowPage.getters.expressionModalInput().type('{{} a: 1 }.a{del}{del}');
|
||||
WorkflowPage.getters.expressionModalOutput().contains(/^1$/);
|
||||
});
|
||||
|
||||
it('should resolve array resolvables', () => {
|
||||
WorkflowPage.getters.expressionModalInput().type('{{');
|
||||
WorkflowPage.getters.expressionModalInput().type('[1, 2, 3]');
|
||||
WorkflowPage.getters.expressionModalOutput().contains(/^\[Array: \[1,2,3\]\]$/);
|
||||
|
||||
WorkflowPage.getters.expressionModalInput().clear();
|
||||
|
||||
WorkflowPage.getters.expressionModalInput().type('{{');
|
||||
WorkflowPage.getters.expressionModalInput().type('[1, 2, 3][0]');
|
||||
WorkflowPage.getters.expressionModalOutput().contains(/^1$/);
|
||||
});
|
||||
|
||||
it('should resolve $parameter[]', () => {
|
||||
WorkflowPage.getters.expressionModalInput().type('{{');
|
||||
WorkflowPage.getters.expressionModalInput().type('$parameter["operation"]');
|
||||
WorkflowPage.getters.expressionModalOutput().contains(/^get$/);
|
||||
});
|
||||
});
|
@ -27,6 +27,8 @@ export class WorkflowPage extends BasePage {
|
||||
firstStepButton: () => cy.getByTestId('canvas-add-button'),
|
||||
isWorkflowSaved: () => this.getters.saveButton().should('match', 'span'), // In Element UI, disabled button turn into spans 🤷♂️
|
||||
isWorkflowActivated: () => this.getters.activatorSwitch().should('have.class', 'is-checked'),
|
||||
expressionModalInput: () => cy.getByTestId('expression-modal-input'),
|
||||
expressionModalOutput: () => cy.getByTestId('expression-modal-output'),
|
||||
};
|
||||
actions = {
|
||||
visit: () => {
|
||||
@ -47,6 +49,9 @@ export class WorkflowPage extends BasePage {
|
||||
openNodeNdv: (nodeTypeName: string) => {
|
||||
this.getters.canvasNodeByName(nodeTypeName).dblclick();
|
||||
},
|
||||
openExpressionEditor: () => {
|
||||
cy.get('input[value="expression"]').parent('label').click();
|
||||
},
|
||||
typeIntoParameterInput: (parameterName: string, content: string) => {
|
||||
this.getters.ndvParameterInput(parameterName).type(content);
|
||||
},
|
||||
|
@ -6,7 +6,6 @@ FROM n8nio/base:${NODE_VERSION} as builder
|
||||
COPY turbo.json package.json .npmrc pnpm-lock.yaml pnpm-workspace.yaml tsconfig.json ./
|
||||
COPY scripts ./scripts
|
||||
COPY packages ./packages
|
||||
COPY patches ./patches
|
||||
|
||||
RUN corepack enable && corepack prepare --activate
|
||||
RUN chown -R node:node .
|
||||
|
@ -57,10 +57,6 @@
|
||||
"typescript": "^4.8.4"
|
||||
},
|
||||
"pnpm": {
|
||||
"patchedDependencies": {
|
||||
"quill@2.0.0-dev.4": "patches/quill@2.0.0-dev.4.patch",
|
||||
"element-ui@2.15.12": "patches/element-ui@2.15.12.patch"
|
||||
},
|
||||
"onlyBuiltDependencies": [
|
||||
"sqlite3",
|
||||
"vue-demi"
|
||||
|
@ -391,6 +391,13 @@
|
||||
--color-code-tags-variable: #c82829;
|
||||
--color-code-tags-definition: #4271ae;
|
||||
|
||||
--color-expression-editor-background: #fff;
|
||||
--color-valid-resolvable-foreground: #29a568;
|
||||
--color-valid-resolvable-background: #e1f3d8;
|
||||
--color-invalid-resolvable-foreground: #f45959;
|
||||
--color-invalid-resolvable-background: #fef0f0;
|
||||
--color-expression-syntax-example: #f0f0f0;
|
||||
|
||||
// Generated Color Shades from 50 to 950
|
||||
// Not yet used in design system
|
||||
@each $color in ('neutral', 'success', 'warning', 'danger') {
|
||||
|
@ -10,6 +10,8 @@ module.exports = {
|
||||
extraFileExtensions: ['.vue'],
|
||||
},
|
||||
|
||||
ignorePatterns: ['*.d.cts'],
|
||||
|
||||
rules: {
|
||||
// TODO: Remove these
|
||||
'id-denylist': 'off',
|
||||
|
@ -32,8 +32,8 @@
|
||||
"@codemirror/lang-javascript": "^6.0.2",
|
||||
"@codemirror/language": "^6.2.1",
|
||||
"@codemirror/lint": "^6.0.0",
|
||||
"@codemirror/state": "^6.1.1",
|
||||
"@codemirror/view": "^6.2.1",
|
||||
"@codemirror/state": "^6.1.4",
|
||||
"@codemirror/view": "^6.5.1",
|
||||
"@fontsource/open-sans": "^4.5.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.35",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.1.1",
|
||||
@ -61,8 +61,6 @@
|
||||
"normalize-wheel": "^1.0.1",
|
||||
"pinia": "^2.0.22",
|
||||
"prismjs": "^1.17.1",
|
||||
"quill": "2.0.0-dev.4",
|
||||
"quill-autoformat": "^0.1.1",
|
||||
"timeago.js": "^4.0.2",
|
||||
"uuid": "^8.3.2",
|
||||
"v-click-outside": "^3.1.2",
|
||||
@ -92,7 +90,6 @@
|
||||
"@types/lodash.get": "^4.4.6",
|
||||
"@types/lodash.set": "^4.3.6",
|
||||
"@types/luxon": "^2.0.9",
|
||||
"@types/quill": "^2.0.1",
|
||||
"@types/uuid": "^8.3.2",
|
||||
"@vitejs/plugin-legacy": "^1.8.2",
|
||||
"@vitejs/plugin-vue2": "^1.1.2",
|
||||
|
@ -19,19 +19,43 @@
|
||||
<el-col :span="16" class="right-side">
|
||||
<div class="expression-editor-wrapper">
|
||||
<div class="editor-description">
|
||||
{{ $locale.baseText('expressionEdit.expression') }}
|
||||
<div>
|
||||
{{ $locale.baseText('expressionEdit.expression') }}
|
||||
</div>
|
||||
<div class="hint">
|
||||
<span>
|
||||
{{ $locale.baseText('expressionEdit.anythingInside') }}
|
||||
</span>
|
||||
<div class="expression-syntax-example" v-text="`{{ }}`"></div>
|
||||
<span>
|
||||
{{ $locale.baseText('expressionEdit.isJavaScript') }}
|
||||
</span>
|
||||
<n8n-link size="medium" :to="expressionsDocsUrl">
|
||||
{{ $locale.baseText('expressionEdit.learnMore') }}
|
||||
</n8n-link>
|
||||
</div>
|
||||
</div>
|
||||
<div class="expression-editor ph-no-capture">
|
||||
<expression-input :parameter="parameter" ref="inputFieldExpression" rows="8" :value="value" :path="path" @change="valueChanged" @keydown.stop="noOp"></expression-input>
|
||||
<expression-modal-input
|
||||
:value="value"
|
||||
:isReadOnly="isReadOnly"
|
||||
@change="valueChanged"
|
||||
ref="inputFieldExpression"
|
||||
data-test-id="expression-modal-input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="expression-result-wrapper">
|
||||
<div class="editor-description">
|
||||
{{ $locale.baseText('expressionEdit.result') }}
|
||||
{{ $locale.baseText('expressionEdit.resultOfItem1') }}
|
||||
</div>
|
||||
<div class="ph-no-capture">
|
||||
<expression-input :parameter="parameter" resolvedValue="true" ref="expressionResult" rows="8" :value="displayValue" :path="path"></expression-input>
|
||||
<expression-modal-output
|
||||
:segments="segments"
|
||||
ref="expressionResult"
|
||||
data-test-id="expression-modal-output"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -43,7 +67,8 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import ExpressionInput from '@/components/ExpressionInput.vue';
|
||||
import ExpressionModalInput from '@/components/ExpressionEditorModal/ExpressionModalInput.vue';
|
||||
import ExpressionModalOutput from '@/components/ExpressionEditorModal/ExpressionModalOutput.vue';
|
||||
import VariableSelector from '@/components/VariableSelector.vue';
|
||||
|
||||
import { IVariableItemSelected } from '@/Interface';
|
||||
@ -51,6 +76,8 @@ import { IVariableItemSelected } from '@/Interface';
|
||||
import { externalHooks } from '@/mixins/externalHooks';
|
||||
import { genericHelpers } from '@/mixins/genericHelpers';
|
||||
|
||||
import { EXPRESSIONS_DOCS_URL } from '@/constants';
|
||||
|
||||
import mixins from 'vue-typed-mixins';
|
||||
import { hasExpressionMapping } from '@/utils';
|
||||
import { debounceHelper } from '@/mixins/debounce';
|
||||
@ -58,6 +85,8 @@ import { mapStores } from 'pinia';
|
||||
import { useWorkflowsStore } from '@/stores/workflows';
|
||||
import { useNDVStore } from '@/stores/ndv';
|
||||
|
||||
import type { Resolvable, Segment } from './ExpressionEditorModal/types';
|
||||
|
||||
export default mixins(
|
||||
externalHooks,
|
||||
genericHelpers,
|
||||
@ -70,15 +99,19 @@ export default mixins(
|
||||
'path',
|
||||
'value',
|
||||
'eventSource',
|
||||
'isReadOnly',
|
||||
],
|
||||
components: {
|
||||
ExpressionInput,
|
||||
ExpressionModalInput,
|
||||
ExpressionModalOutput,
|
||||
VariableSelector,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
displayValue: '',
|
||||
latestValue: '',
|
||||
segments: [] as Segment[],
|
||||
expressionsDocsUrl: EXPRESSIONS_DOCS_URL,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@ -88,8 +121,9 @@ export default mixins(
|
||||
),
|
||||
},
|
||||
methods: {
|
||||
valueChanged (value: string, forceUpdate = false) {
|
||||
valueChanged ({ value, segments }: { value: string, segments: Segment[] }, forceUpdate = false) {
|
||||
this.latestValue = value;
|
||||
this.segments = segments;
|
||||
|
||||
if (forceUpdate === true) {
|
||||
this.updateDisplayValue();
|
||||
@ -180,6 +214,16 @@ export default mixins(
|
||||
this.$externalHooks().run('expressionEdit.dialogVisibleChanged', { dialogVisible: newValue, parameter: this.parameter, value: this.value, resolvedExpressionValue });
|
||||
|
||||
if (!newValue) {
|
||||
const resolvables = this.segments.filter((s): s is Resolvable => s.kind === 'resolvable');
|
||||
const errorResolvables = resolvables.filter(r => r.error);
|
||||
|
||||
const exposeErrorProperties = (error: Error) => {
|
||||
return Object.getOwnPropertyNames(error).reduce<Record<string, unknown>>((acc, key) => {
|
||||
// @ts-ignore
|
||||
return acc[key] = error[key], acc;
|
||||
}, {});
|
||||
};
|
||||
|
||||
const telemetryPayload = {
|
||||
empty_expression: (this.value === '=') || (this.value === '={{}}') || !this.value,
|
||||
workflow_id: this.workflowsStore.workflowId,
|
||||
@ -187,7 +231,17 @@ export default mixins(
|
||||
session_id: this.ndvStore.sessionId,
|
||||
has_parameter: this.value.includes('$parameter'),
|
||||
has_mapping: hasExpressionMapping(this.value),
|
||||
node_type: this.ndvStore.activeNode?.type ?? '',
|
||||
handlebar_count: resolvables.length,
|
||||
handlebar_error_count: errorResolvables.length,
|
||||
full_errors: errorResolvables.map(errorResolvable => {
|
||||
return errorResolvable.fullError
|
||||
? { ...exposeErrorProperties(errorResolvable.fullError), stack: errorResolvable.fullError.stack }
|
||||
: null;
|
||||
}),
|
||||
short_errors: errorResolvables.map(r => r.resolved ?? null),
|
||||
};
|
||||
|
||||
this.$telemetry.track('User closed Expression Editor', telemetryPayload);
|
||||
this.$externalHooks().run('expressionEdit.closeDialog', telemetryPayload);
|
||||
}
|
||||
@ -200,7 +254,32 @@ export default mixins(
|
||||
.editor-description {
|
||||
line-height: 1.5;
|
||||
font-weight: bold;
|
||||
padding: 0 0 0.5em 0.2em;;
|
||||
padding: 0 0 0.5em 0.2em;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.hint {
|
||||
color: var(--color-text-base);
|
||||
font-weight: normal;
|
||||
display: flex;
|
||||
|
||||
@media (max-width: $breakpoint-xs) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
span {
|
||||
margin-right: var(--spacing-4xs);
|
||||
}
|
||||
.expression-syntax-example {
|
||||
display: inline-block;
|
||||
margin-top: 3px;
|
||||
height: 16px;
|
||||
line-height: 1;
|
||||
background-color: var(--color-expression-syntax-example);
|
||||
color: var(--color-text-dark);
|
||||
margin-right: var(--spacing-4xs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.expression-result-wrapper,
|
||||
|
@ -0,0 +1,281 @@
|
||||
<template>
|
||||
<div ref="root" class="ph-no-capture" />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import mixins from 'vue-typed-mixins';
|
||||
import { mapStores } from 'pinia';
|
||||
import { EditorView } from '@codemirror/view';
|
||||
import { EditorState } from '@codemirror/state';
|
||||
import { history } from '@codemirror/commands';
|
||||
import { syntaxTree } from '@codemirror/language';
|
||||
|
||||
import { workflowHelpers } from '@/mixins/workflowHelpers';
|
||||
import { useNDVStore } from '@/stores/ndv';
|
||||
import { n8nLanguageSupport } from './n8nLanguageSupport';
|
||||
import { braceHandler } from './braceHandler';
|
||||
import { EXPRESSION_EDITOR_THEME } from './theme';
|
||||
import { addColor, removeColor } from './colorDecorations';
|
||||
|
||||
import type { IVariableItemSelected } from '@/Interface';
|
||||
import type { RawSegment, Segment, Resolvable, Plaintext } from './types';
|
||||
|
||||
const EVALUATION_DELAY = 300; // ms
|
||||
|
||||
export default mixins(workflowHelpers).extend({
|
||||
name: 'expression-modal-input',
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
},
|
||||
isReadOnly: {
|
||||
type: Boolean,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
editor: null as EditorView | null,
|
||||
errorsInSuccession: 0,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
const extensions = [
|
||||
EXPRESSION_EDITOR_THEME,
|
||||
n8nLanguageSupport(),
|
||||
history(),
|
||||
braceHandler(),
|
||||
EditorView.lineWrapping,
|
||||
EditorState.readOnly.of(this.isReadOnly),
|
||||
EditorView.updateListener.of((viewUpdate) => {
|
||||
if (!this.editor || !viewUpdate.docChanged) return;
|
||||
|
||||
removeColor(this.editor, this.plaintextSegments);
|
||||
|
||||
addColor(this.editor, this.resolvableSegments);
|
||||
|
||||
const prevErrorsInSuccession = this.errorsInSuccession;
|
||||
|
||||
if (this.resolvableSegments.filter((s) => s.error).length > 0) {
|
||||
this.errorsInSuccession += 1;
|
||||
} else {
|
||||
this.errorsInSuccession = 0;
|
||||
}
|
||||
|
||||
const addsNewError = this.errorsInSuccession > prevErrorsInSuccession;
|
||||
|
||||
let delay = EVALUATION_DELAY;
|
||||
|
||||
if (addsNewError && this.errorsInSuccession > 1 && this.errorsInSuccession < 5) {
|
||||
delay = EVALUATION_DELAY * this.errorsInSuccession;
|
||||
} else if (addsNewError && this.errorsInSuccession >= 5) {
|
||||
delay = 0;
|
||||
}
|
||||
|
||||
setTimeout(() => this.editor?.focus()); // prevent blur on paste
|
||||
|
||||
setTimeout(() => {
|
||||
this.$emit('change', {
|
||||
value: this.unresolvedExpression,
|
||||
segments: this.displayableSegments,
|
||||
});
|
||||
}, delay);
|
||||
}),
|
||||
];
|
||||
|
||||
this.editor = new EditorView({
|
||||
parent: this.$refs.root as HTMLDivElement,
|
||||
state: EditorState.create({
|
||||
doc: this.value.startsWith('=') ? this.value.slice(1) : this.value,
|
||||
extensions,
|
||||
}),
|
||||
});
|
||||
|
||||
this.editor.focus();
|
||||
|
||||
addColor(this.editor, this.resolvableSegments);
|
||||
|
||||
this.editor.dispatch({
|
||||
selection: { anchor: this.editor.state.doc.length },
|
||||
});
|
||||
|
||||
this.$emit('change', { value: this.unresolvedExpression, segments: this.displayableSegments });
|
||||
},
|
||||
destroyed() {
|
||||
this.editor?.destroy();
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useNDVStore),
|
||||
unresolvedExpression(): string {
|
||||
return this.segments.reduce((acc, segment) => {
|
||||
acc += segment.kind === 'resolvable' ? segment.resolvable : segment.plaintext;
|
||||
|
||||
return acc;
|
||||
}, '=');
|
||||
},
|
||||
resolvableSegments(): Resolvable[] {
|
||||
return this.segments.filter((s): s is Resolvable => s.kind === 'resolvable');
|
||||
},
|
||||
plaintextSegments(): Plaintext[] {
|
||||
return this.segments.filter((s): s is Plaintext => s.kind === 'plaintext');
|
||||
},
|
||||
|
||||
/**
|
||||
* Some segments are conditionally displayed, i.e. not displayed when part of the
|
||||
* expression result but displayed when the entire result.
|
||||
*
|
||||
* Example:
|
||||
* - Expression `This is a {{ null }} test` is displayed as `This is a test`.
|
||||
* - Expression `{{ null }}` is displayed as `[Object: null]`.
|
||||
*
|
||||
* Conditionally displayed segments:
|
||||
* - `[Object: null]`
|
||||
* - `[Array: []]`
|
||||
* - `[empty]` (from `''`, not from `undefined`)
|
||||
* - `null` (from `NaN`)
|
||||
*
|
||||
* For these two segments, display differs based on context:
|
||||
* - Date displayed as
|
||||
* - `Mon Nov 14 2022 17:26:13 GMT+0100 (CST)` when part of the result
|
||||
* - `[Object: "2022-11-14T17:26:13.130Z"]` when the entire result
|
||||
* - Non-empty array displayed as
|
||||
* - `1,2,3` when part of the result
|
||||
* - `[Array: [1, 2, 3]]` when the entire result
|
||||
*
|
||||
*/
|
||||
displayableSegments(): Segment[] {
|
||||
return this.segments
|
||||
.map((s) => {
|
||||
if (this.segments.length <= 1 || s.kind !== 'resolvable') return s;
|
||||
|
||||
if (typeof s.resolved === 'string' && /\[Object: "\d{4}-\d{2}-\d{2}T/.test(s.resolved)) {
|
||||
const utcDateString = s.resolved.replace(/(\[Object: "|\"\])/g, '');
|
||||
s.resolved = new Date(utcDateString).toString();
|
||||
}
|
||||
|
||||
if (typeof s.resolved === 'string' && /\[Array:\s\[.+\]\]/.test(s.resolved)) {
|
||||
s.resolved = s.resolved.replace(/(\[Array: \[|\])/g, '');
|
||||
}
|
||||
|
||||
return s;
|
||||
})
|
||||
.filter((s) => {
|
||||
if (
|
||||
this.segments.length > 1 &&
|
||||
s.kind === 'resolvable' &&
|
||||
typeof s.resolved === 'string' &&
|
||||
(['[Object: null]', '[Array: []]'].includes(s.resolved) ||
|
||||
s.resolved === this.$locale.baseText('expressionModalInput.empty') ||
|
||||
s.resolved === this.$locale.baseText('expressionModalInput.null'))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
},
|
||||
segments(): Segment[] {
|
||||
if (!this.editor) return [];
|
||||
|
||||
const rawSegments: RawSegment[] = [];
|
||||
|
||||
syntaxTree(this.editor.state)
|
||||
.cursor()
|
||||
.iterate((node) => {
|
||||
if (!this.editor || node.type.name === 'Program') return;
|
||||
|
||||
rawSegments.push({
|
||||
from: node.from,
|
||||
to: node.to,
|
||||
text: this.editor.state.sliceDoc(node.from, node.to),
|
||||
type: node.type.name,
|
||||
});
|
||||
});
|
||||
|
||||
return rawSegments.reduce<Segment[]>((acc, segment) => {
|
||||
const { from, to, text, type } = segment;
|
||||
|
||||
if (type === 'Resolvable') {
|
||||
const { resolved, error, fullError } = this.resolve(text);
|
||||
|
||||
acc.push({ kind: 'resolvable', from, to, resolvable: text, resolved, error, fullError });
|
||||
|
||||
return acc;
|
||||
}
|
||||
|
||||
// broken resolvable included in plaintext
|
||||
|
||||
acc.push({ kind: 'plaintext', from, to, plaintext: text });
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
isEmptyExpression(resolvable: string) {
|
||||
return /\{\{\s*\}\}/.test(resolvable);
|
||||
},
|
||||
resolve(resolvable: string) {
|
||||
const result: { resolved: unknown; error: boolean; fullError: Error | null } = {
|
||||
resolved: undefined,
|
||||
error: false,
|
||||
fullError: null,
|
||||
};
|
||||
|
||||
try {
|
||||
result.resolved = this.resolveExpression('=' + resolvable, undefined, {
|
||||
inputNodeName: this.ndvStore.ndvInputNodeName,
|
||||
inputRunIndex: this.ndvStore.ndvInputRunIndex,
|
||||
inputBranchIndex: this.ndvStore.ndvInputBranchIndex,
|
||||
});
|
||||
} catch (error) {
|
||||
result.resolved = `[${error.message}]`;
|
||||
result.error = true;
|
||||
result.fullError = error;
|
||||
}
|
||||
|
||||
if (result.resolved === '') {
|
||||
result.resolved = this.$locale.baseText('expressionModalInput.empty');
|
||||
}
|
||||
|
||||
if (result.resolved === undefined && this.isEmptyExpression(resolvable)) {
|
||||
result.resolved = this.$locale.baseText('expressionModalInput.empty');
|
||||
}
|
||||
|
||||
if (result.resolved === undefined) {
|
||||
result.resolved = this.$locale.baseText('expressionModalInput.undefined');
|
||||
result.error = true;
|
||||
}
|
||||
|
||||
if (typeof result.resolved === 'number' && isNaN(result.resolved)) {
|
||||
result.resolved = this.$locale.baseText('expressionModalInput.null');
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
itemSelected({ variable }: IVariableItemSelected) {
|
||||
if (!this.editor || this.isReadOnly) return;
|
||||
|
||||
const OPEN_MARKER = '{{';
|
||||
const CLOSE_MARKER = '}}';
|
||||
|
||||
const { doc, selection } = this.editor.state;
|
||||
const { head } = selection.main;
|
||||
|
||||
const beforeBraced = doc.toString().slice(0, head).includes(OPEN_MARKER);
|
||||
const afterBraced = doc.toString().slice(head, doc.length).includes(CLOSE_MARKER);
|
||||
|
||||
this.editor.dispatch({
|
||||
changes: {
|
||||
from: head,
|
||||
insert:
|
||||
beforeBraced && afterBraced
|
||||
? variable
|
||||
: [OPEN_MARKER, variable, CLOSE_MARKER].join(' '),
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss"></style>
|
@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<div ref="root" class="ph-no-capture" />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue, { PropType } from 'vue';
|
||||
import { EditorView } from '@codemirror/view';
|
||||
import { EditorState } from '@codemirror/state';
|
||||
|
||||
import { EXPRESSION_EDITOR_THEME } from './theme';
|
||||
import { addColor, removeColor } from './colorDecorations';
|
||||
|
||||
import type { Plaintext, Resolved, Segment } from './types';
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'expression-modal-output',
|
||||
props: {
|
||||
segments: {
|
||||
type: Array as PropType<Segment[]>,
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
segments() {
|
||||
if (!this.editor) return;
|
||||
|
||||
this.editor.dispatch({
|
||||
changes: { from: 0, to: this.editor.state.doc.length, insert: this.resolvedExpression },
|
||||
});
|
||||
|
||||
addColor(this.editor, this.resolvedSegments);
|
||||
removeColor(this.editor, this.plaintextSegments);
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
editor: null as EditorView | null,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
const extensions = [
|
||||
EXPRESSION_EDITOR_THEME,
|
||||
EditorState.readOnly.of(true),
|
||||
EditorView.lineWrapping,
|
||||
];
|
||||
|
||||
this.editor = new EditorView({
|
||||
parent: this.$refs.root as HTMLDivElement,
|
||||
state: EditorState.create({
|
||||
doc: this.resolvedExpression,
|
||||
extensions,
|
||||
}),
|
||||
});
|
||||
},
|
||||
destroyed() {
|
||||
this.editor?.destroy();
|
||||
},
|
||||
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
|
||||
.map((segment) => {
|
||||
segment.from = cursor;
|
||||
|
||||
cursor +=
|
||||
segment.kind === 'plaintext'
|
||||
? segment.plaintext.length
|
||||
: (segment.resolved as any).toString().length;
|
||||
|
||||
segment.to = cursor;
|
||||
|
||||
return segment;
|
||||
})
|
||||
.filter((segment): segment is Resolved => segment.kind === 'resolvable');
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getValue() {
|
||||
return '=' + this.resolvedExpression;
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss"></style>
|
@ -0,0 +1,57 @@
|
||||
import { closeBrackets, insertBracket } from '@codemirror/autocomplete';
|
||||
import { codePointAt, codePointSize, Extension } from '@codemirror/state';
|
||||
import { EditorView } from '@codemirror/view';
|
||||
|
||||
const braceInputHandler = EditorView.inputHandler.of((view, from, to, insert) => {
|
||||
if (view.composing || view.state.readOnly) return false;
|
||||
|
||||
const selection = view.state.selection.main;
|
||||
|
||||
if (
|
||||
insert.length > 2 ||
|
||||
(insert.length === 2 && codePointSize(codePointAt(insert, 0)) === 1) ||
|
||||
from !== selection.from ||
|
||||
to !== selection.to
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const transaction = insertBracket(view.state, insert);
|
||||
|
||||
if (!transaction) return false;
|
||||
|
||||
view.dispatch(transaction);
|
||||
|
||||
// customization to rearrange spacing and cursor for expression
|
||||
|
||||
const cursor = view.state.selection.main.head;
|
||||
|
||||
const isSecondBraceForNewExpression =
|
||||
view.state.sliceDoc(cursor - 2, cursor) === '{{' &&
|
||||
view.state.sliceDoc(cursor, cursor + 1) === '}';
|
||||
|
||||
if (isSecondBraceForNewExpression) {
|
||||
view.dispatch({
|
||||
changes: { from: cursor, to: cursor + 2, insert: ' }' },
|
||||
selection: { anchor: cursor + 1 },
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const isFirstBraceForNewExpression =
|
||||
view.state.sliceDoc(cursor - 1, cursor) === '{' &&
|
||||
view.state.sliceDoc(cursor, cursor + 1) === '}';
|
||||
|
||||
if (isFirstBraceForNewExpression) {
|
||||
view.dispatch({ changes: { from: cursor, insert: ' ' } });
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
const [_, bracketState] = closeBrackets() as readonly Extension[];
|
||||
|
||||
export const braceHandler = () => [braceInputHandler, bracketState];
|
@ -0,0 +1,94 @@
|
||||
import { EditorView, Decoration, DecorationSet } from '@codemirror/view';
|
||||
import { StateField, StateEffect } from '@codemirror/state';
|
||||
|
||||
import { DYNAMICALLY_STYLED_RESOLVABLES_THEME, SYNTAX_HIGHLIGHTING_CLASSES } from './theme';
|
||||
|
||||
import type { ColoringStateEffect, Plaintext, Resolvable, Resolved } from './types';
|
||||
|
||||
const stateEffects = {
|
||||
addColor: StateEffect.define<ColoringStateEffect.Value>({
|
||||
map: ({ from, to, kind, error }, change) => ({
|
||||
from: change.mapPos(from),
|
||||
to: change.mapPos(to),
|
||||
kind,
|
||||
error,
|
||||
}),
|
||||
}),
|
||||
removeColor: StateEffect.define<{ from: number; to: number }>({
|
||||
map: ({ from, to }, change) => ({
|
||||
from: change.mapPos(from),
|
||||
to: change.mapPos(to),
|
||||
}),
|
||||
}),
|
||||
};
|
||||
|
||||
const marks = {
|
||||
valid: Decoration.mark({ class: SYNTAX_HIGHLIGHTING_CLASSES.validResolvable }),
|
||||
invalid: Decoration.mark({ class: SYNTAX_HIGHLIGHTING_CLASSES.invalidResolvable }),
|
||||
};
|
||||
|
||||
const coloringField = StateField.define<DecorationSet>({
|
||||
provide: (stateField) => EditorView.decorations.from(stateField),
|
||||
create() {
|
||||
return Decoration.none;
|
||||
},
|
||||
update(colorings, transaction) {
|
||||
colorings = colorings.map(transaction.changes);
|
||||
|
||||
for (const txEffect of transaction.effects) {
|
||||
if (txEffect.is(stateEffects.removeColor)) {
|
||||
colorings = colorings.update({
|
||||
filter: (from, to) => txEffect.value.from !== from && txEffect.value.to !== to,
|
||||
});
|
||||
}
|
||||
|
||||
if (txEffect.is(stateEffects.addColor)) {
|
||||
colorings = colorings.update({
|
||||
filter: (from, to) => txEffect.value.from !== from && txEffect.value.to !== to,
|
||||
});
|
||||
|
||||
const decoration = txEffect.value.error ? marks.invalid : marks.valid;
|
||||
|
||||
if (txEffect.value.from === 0 && txEffect.value.to === 0) continue;
|
||||
|
||||
colorings = colorings.update({
|
||||
add: [decoration.range(txEffect.value.from, txEffect.value.to)],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return colorings;
|
||||
},
|
||||
});
|
||||
|
||||
export function addColor(view: EditorView, segments: Array<Resolvable | Resolved>) {
|
||||
const effects: Array<StateEffect<unknown>> = segments.map(({ from, to, kind, error }) =>
|
||||
stateEffects.addColor.of({ from, to, kind, error }),
|
||||
);
|
||||
|
||||
if (effects.length === 0) return;
|
||||
|
||||
if (!view.state.field(coloringField, false)) {
|
||||
effects.push(
|
||||
StateEffect.appendConfig.of([coloringField, DYNAMICALLY_STYLED_RESOLVABLES_THEME]),
|
||||
);
|
||||
}
|
||||
|
||||
view.dispatch({ effects });
|
||||
}
|
||||
|
||||
export function removeColor(view: EditorView, segments: Plaintext[]) {
|
||||
const effects: Array<StateEffect<unknown>> = segments.map(({ from, to }) =>
|
||||
stateEffects.removeColor.of({ from, to }),
|
||||
);
|
||||
|
||||
if (effects.length === 0) return;
|
||||
|
||||
if (!view.state.field(coloringField, false)) {
|
||||
effects.push(
|
||||
StateEffect.appendConfig.of([coloringField, DYNAMICALLY_STYLED_RESOLVABLES_THEME]),
|
||||
);
|
||||
}
|
||||
|
||||
view.dispatch({ effects });
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
'use strict';
|
||||
|
||||
Object.defineProperty(exports, '__esModule', { value: true });
|
||||
|
||||
var autocomplete = require('@codemirror/autocomplete');
|
||||
var lr = require('@lezer/lr');
|
||||
var language = require('@codemirror/language');
|
||||
var highlight = require('@lezer/highlight');
|
||||
|
||||
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
||||
const parser = lr.LRParser.deserialize({
|
||||
version: 14,
|
||||
states: "nQQOPOOOOOO'#Cc'#CcOOOO'#Ca'#CaQQOPOOOOOO-E6_-E6_",
|
||||
stateData: "]~OQPORPOSPO~O",
|
||||
goto: "cWPPPPPXP_QRORSRTQOR",
|
||||
nodeNames: "⚠ Program Plaintext Resolvable BrokenResolvable",
|
||||
maxTerm: 7,
|
||||
skippedNodes: [0],
|
||||
repeatNodeCount: 1,
|
||||
tokenData: "4f~RRO#o[#o#p{#p~[~aRQ~O#o[#o#pj#p~[~mRO#o[#p~[~~v~{OQ~~!OSO#o[#o#p![#p~[~~v~!a!YS~OX&PX^![^p&Ppq![qr![rs![st![tu![uv![vw![wx![xy![yz![z{![{|![|}![}!O![!O!P![!P!Q![!Q![![![!]![!]!^![!^!_![!_!`![!`!a![!a!b![!b!c![!c!}![!}#O![#O#P&d#P#Q![#Q#R![#R#S![#S#T![#T#o![#o#p![#p#q![#q#r4W#r#s![#s#y&P#y#z![#z$f&P$f$g![$g#BY&P#BY#BZ![#BZ$IS&P$IS$I_![$I_$I|&P$I|$JO![$JO$JT&P$JT$JU![$JU$KV&P$KV$KW![$KW&FU&P&FU&FV![&FV~&P~&URS~O#q&P#q#r&_#r~&P~&dOS~~&i!YS~OX&PX^![^p&Ppq![qr![rs![st![tu![uv![vw![wx![xy![yz![z{![{|![|}![}!O![!O!P![!P!Q![!Q![![![!]![!]!^![!^!_![!_!`![!`!a![!a!b![!b!c![!c!}![!}#O![#O#P&d#P#Q![#Q#R![#R#S![#S#T![#T#o![#o#p![#p#q![#q#r*X#r#s![#s#y&P#y#z![#z$f&P$f$g![$g#BY&P#BY#BZ![#BZ$IS&P$IS$I_![$I_$I|&P$I|$JO![$JO$JT&P$JT$JU![$JU$KV&P$KV$KW![$KW&FU&P&FU&FV![&FV~&P~*^RS~O#q*g#q#r0s#r~*g~*j}X^*gpq*gqr*grs*gst*gtu*guv*gvw*gwx*gxy*gyz*gz{*g{|*g|}*g}!O*g!O!P*g!P!Q*g!Q![*g![!]*g!]!^*g!^!_*g!_!`*g!`!a*g!a!b*g!b!c*g!c!}*g!}#O*g#O#P-g#P#Q*g#Q#R*g#R#S*g#S#T*g#T#o*g#o#p*g#p#q*g#q#r3u#r#s*g#y#z*g$f$g*g#BY#BZ*g$IS$I_*g$I|$JO*g$JT$JU*g$KV$KW*g&FU&FV*g~-j}X^*gpq*gqr*grs*gst*gtu*guv*gvw*gwx*gxy*gyz*gz{*g{|*g|}*g}!O*g!O!P*g!P!Q*g!Q![*g![!]*g!]!^*g!^!_*g!_!`*g!`!a*g!a!b*g!b!c*g!c!}*g!}#O*g#O#P-g#P#Q*g#Q#R*g#R#S*g#S#T*g#T#o*g#o#p*g#p#q*g#q#r0g#r#s*g#y#z*g$f$g*g#BY#BZ*g$IS$I_*g$I|$JO*g$JT$JU*g$KV$KW*g&FU&FV*g~0jRO#q*g#q#r0s#r~*g~0x}R~X^*gpq*gqr*grs*gst*gtu*guv*gvw*gwx*gxy*gyz*gz{*g{|*g|}*g}!O*g!O!P*g!P!Q*g!Q![*g![!]*g!]!^*g!^!_*g!_!`*g!`!a*g!a!b*g!b!c*g!c!}*g!}#O*g#O#P-g#P#Q*g#Q#R*g#R#S*g#S#T*g#T#o*g#o#p*g#p#q*g#q#r3u#r#s*g#y#z*g$f$g*g#BY#BZ*g$IS$I_*g$I|$JO*g$JT$JU*g$KV$KW*g&FU&FV*g~3xRO#q*g#q#r4R#r~*g~4WOR~~4]RS~O#q*g#q#r4R#r~*g",
|
||||
tokenizers: [0],
|
||||
topRules: {"Program":[0,1]},
|
||||
tokenPrec: 0
|
||||
});
|
||||
|
||||
const parserWithMetaData = parser.configure({
|
||||
props: [
|
||||
language.foldNodeProp.add({
|
||||
Application: language.foldInside,
|
||||
}),
|
||||
highlight.styleTags({
|
||||
OpenMarker: highlight.tags.brace,
|
||||
CloseMarker: highlight.tags.brace,
|
||||
Plaintext: highlight.tags.content,
|
||||
Resolvable: highlight.tags.string,
|
||||
BrokenResolvable: highlight.tags.className,
|
||||
}),
|
||||
],
|
||||
});
|
||||
const n8nExpressionLanguage = language.LRLanguage.define({
|
||||
parser: parserWithMetaData,
|
||||
languageData: {
|
||||
commentTokens: { line: ";" },
|
||||
},
|
||||
});
|
||||
const completions = n8nExpressionLanguage.data.of({
|
||||
autocomplete: autocomplete.completeFromList([
|
||||
{ label: "abcdefg", type: "keyword" },
|
||||
]),
|
||||
});
|
||||
function n8nExpression() {
|
||||
return new language.LanguageSupport(n8nExpressionLanguage, [completions]);
|
||||
}
|
||||
|
||||
exports.n8nExpression = n8nExpression;
|
||||
exports.n8nExpressionLanguage = n8nExpressionLanguage;
|
||||
exports.parserWithMetaData = parserWithMetaData;
|
@ -0,0 +1,5 @@
|
||||
import { LRLanguage, LanguageSupport } from "@codemirror/language";
|
||||
declare const parserWithMetaData: import("@lezer/lr").LRParser;
|
||||
declare const n8nExpressionLanguage: LRLanguage;
|
||||
declare function n8nExpression(): LanguageSupport;
|
||||
export { parserWithMetaData, n8nExpressionLanguage, n8nExpression };
|
5
packages/editor-ui/src/components/ExpressionEditorModal/n8nLanguagePack/index.d.ts
vendored
Normal file
5
packages/editor-ui/src/components/ExpressionEditorModal/n8nLanguagePack/index.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
import { LRLanguage, LanguageSupport } from "@codemirror/language";
|
||||
declare const parserWithMetaData: import("@lezer/lr").LRParser;
|
||||
declare const n8nExpressionLanguage: LRLanguage;
|
||||
declare function n8nExpression(): LanguageSupport;
|
||||
export { parserWithMetaData, n8nExpressionLanguage, n8nExpression };
|
@ -0,0 +1,51 @@
|
||||
import { completeFromList } from '@codemirror/autocomplete';
|
||||
import { LRParser } from '@lezer/lr';
|
||||
import { foldNodeProp, foldInside, LRLanguage, LanguageSupport } from '@codemirror/language';
|
||||
import { styleTags, tags } from '@lezer/highlight';
|
||||
|
||||
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
||||
const parser = LRParser.deserialize({
|
||||
version: 14,
|
||||
states: "nQQOPOOOOOO'#Cc'#CcOOOO'#Ca'#CaQQOPOOOOOO-E6_-E6_",
|
||||
stateData: "]~OQPORPOSPO~O",
|
||||
goto: "cWPPPPPXP_QRORSRTQOR",
|
||||
nodeNames: "⚠ Program Plaintext Resolvable BrokenResolvable",
|
||||
maxTerm: 7,
|
||||
skippedNodes: [0],
|
||||
repeatNodeCount: 1,
|
||||
tokenData: "4f~RRO#o[#o#p{#p~[~aRQ~O#o[#o#pj#p~[~mRO#o[#p~[~~v~{OQ~~!OSO#o[#o#p![#p~[~~v~!a!YS~OX&PX^![^p&Ppq![qr![rs![st![tu![uv![vw![wx![xy![yz![z{![{|![|}![}!O![!O!P![!P!Q![!Q![![![!]![!]!^![!^!_![!_!`![!`!a![!a!b![!b!c![!c!}![!}#O![#O#P&d#P#Q![#Q#R![#R#S![#S#T![#T#o![#o#p![#p#q![#q#r4W#r#s![#s#y&P#y#z![#z$f&P$f$g![$g#BY&P#BY#BZ![#BZ$IS&P$IS$I_![$I_$I|&P$I|$JO![$JO$JT&P$JT$JU![$JU$KV&P$KV$KW![$KW&FU&P&FU&FV![&FV~&P~&URS~O#q&P#q#r&_#r~&P~&dOS~~&i!YS~OX&PX^![^p&Ppq![qr![rs![st![tu![uv![vw![wx![xy![yz![z{![{|![|}![}!O![!O!P![!P!Q![!Q![![![!]![!]!^![!^!_![!_!`![!`!a![!a!b![!b!c![!c!}![!}#O![#O#P&d#P#Q![#Q#R![#R#S![#S#T![#T#o![#o#p![#p#q![#q#r*X#r#s![#s#y&P#y#z![#z$f&P$f$g![$g#BY&P#BY#BZ![#BZ$IS&P$IS$I_![$I_$I|&P$I|$JO![$JO$JT&P$JT$JU![$JU$KV&P$KV$KW![$KW&FU&P&FU&FV![&FV~&P~*^RS~O#q*g#q#r0s#r~*g~*j}X^*gpq*gqr*grs*gst*gtu*guv*gvw*gwx*gxy*gyz*gz{*g{|*g|}*g}!O*g!O!P*g!P!Q*g!Q![*g![!]*g!]!^*g!^!_*g!_!`*g!`!a*g!a!b*g!b!c*g!c!}*g!}#O*g#O#P-g#P#Q*g#Q#R*g#R#S*g#S#T*g#T#o*g#o#p*g#p#q*g#q#r3u#r#s*g#y#z*g$f$g*g#BY#BZ*g$IS$I_*g$I|$JO*g$JT$JU*g$KV$KW*g&FU&FV*g~-j}X^*gpq*gqr*grs*gst*gtu*guv*gvw*gwx*gxy*gyz*gz{*g{|*g|}*g}!O*g!O!P*g!P!Q*g!Q![*g![!]*g!]!^*g!^!_*g!_!`*g!`!a*g!a!b*g!b!c*g!c!}*g!}#O*g#O#P-g#P#Q*g#Q#R*g#R#S*g#S#T*g#T#o*g#o#p*g#p#q*g#q#r0g#r#s*g#y#z*g$f$g*g#BY#BZ*g$IS$I_*g$I|$JO*g$JT$JU*g$KV$KW*g&FU&FV*g~0jRO#q*g#q#r0s#r~*g~0x}R~X^*gpq*gqr*grs*gst*gtu*guv*gvw*gwx*gxy*gyz*gz{*g{|*g|}*g}!O*g!O!P*g!P!Q*g!Q![*g![!]*g!]!^*g!^!_*g!_!`*g!`!a*g!a!b*g!b!c*g!c!}*g!}#O*g#O#P-g#P#Q*g#Q#R*g#R#S*g#S#T*g#T#o*g#o#p*g#p#q*g#q#r3u#r#s*g#y#z*g$f$g*g#BY#BZ*g$IS$I_*g$I|$JO*g$JT$JU*g$KV$KW*g&FU&FV*g~3xRO#q*g#q#r4R#r~*g~4WOR~~4]RS~O#q*g#q#r4R#r~*g",
|
||||
tokenizers: [0],
|
||||
topRules: {"Program":[0,1]},
|
||||
tokenPrec: 0
|
||||
});
|
||||
|
||||
const parserWithMetaData = parser.configure({
|
||||
props: [
|
||||
foldNodeProp.add({
|
||||
Application: foldInside,
|
||||
}),
|
||||
styleTags({
|
||||
OpenMarker: tags.brace,
|
||||
CloseMarker: tags.brace,
|
||||
Plaintext: tags.content,
|
||||
Resolvable: tags.string,
|
||||
BrokenResolvable: tags.className,
|
||||
}),
|
||||
],
|
||||
});
|
||||
const n8nExpressionLanguage = LRLanguage.define({
|
||||
parser: parserWithMetaData,
|
||||
languageData: {
|
||||
commentTokens: { line: ";" },
|
||||
},
|
||||
});
|
||||
const completions = n8nExpressionLanguage.data.of({
|
||||
autocomplete: completeFromList([
|
||||
{ label: "abcdefg", type: "keyword" },
|
||||
]),
|
||||
});
|
||||
function n8nExpression() {
|
||||
return new LanguageSupport(n8nExpressionLanguage, [completions]);
|
||||
}
|
||||
|
||||
export { n8nExpression, n8nExpressionLanguage, parserWithMetaData };
|
@ -0,0 +1,20 @@
|
||||
import { LanguageSupport, LRLanguage } from '@codemirror/language';
|
||||
import { parseMixed } from '@lezer/common';
|
||||
import { parser as jsParser } from '@lezer/javascript';
|
||||
import { parserWithMetaData as n8nParser } from './n8nLanguagePack';
|
||||
|
||||
const parserWithNestedJsParser = n8nParser.configure({
|
||||
wrap: parseMixed((node) => {
|
||||
if (node.type.isTop) return null;
|
||||
|
||||
return node.name === 'Resolvable'
|
||||
? { parser: jsParser, overlay: (node) => node.type.name === 'Resolvable' }
|
||||
: null;
|
||||
}),
|
||||
});
|
||||
|
||||
const n8nLanguage = LRLanguage.define({ parser: parserWithNestedJsParser });
|
||||
|
||||
export function n8nLanguageSupport() {
|
||||
return new LanguageSupport(n8nLanguage);
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
import { CompletionContext, CompletionResult } from '@codemirror/autocomplete';
|
||||
import { syntaxTree } from '@codemirror/language';
|
||||
|
||||
/**
|
||||
* Completions available inside the resolvable segment `{{ ... }}` of an n8n expression.
|
||||
*
|
||||
* Currently unused.
|
||||
*/
|
||||
export function resolvableCompletions(context: CompletionContext): CompletionResult | null {
|
||||
const nodeBefore = syntaxTree(context.state).resolveInner(context.pos, -1);
|
||||
|
||||
if (nodeBefore.name !== 'Resolvable') return null;
|
||||
|
||||
const pattern = /(?<quotedString>('|")\w*('|"))\./;
|
||||
|
||||
const preCursor = context.matchBefore(pattern);
|
||||
|
||||
if (!preCursor || (preCursor.from === preCursor.to && !context.explicit)) return null;
|
||||
|
||||
const match = preCursor.text.match(pattern);
|
||||
|
||||
if (!match?.groups?.quotedString) return null;
|
||||
|
||||
const { quotedString } = match.groups;
|
||||
|
||||
return {
|
||||
from: preCursor.from,
|
||||
options: [
|
||||
{ label: `${quotedString}.replace()`, info: 'Replace part of a string with another' },
|
||||
{ label: `${quotedString}.slice()`, info: 'Copy part of a string' },
|
||||
],
|
||||
};
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
import { HighlightStyle, syntaxHighlighting } from '@codemirror/language';
|
||||
import { EditorView } from '@codemirror/view';
|
||||
import { tags } from '@lezer/highlight';
|
||||
|
||||
export const SYNTAX_HIGHLIGHTING_CLASSES = {
|
||||
validResolvable: 'cm-valid-resolvable',
|
||||
invalidResolvable: 'cm-invalid-resolvable',
|
||||
brokenResolvable: 'cm-broken-resolvable',
|
||||
plaintext: 'cm-plaintext',
|
||||
};
|
||||
|
||||
export const EXPRESSION_EDITOR_THEME = [
|
||||
EditorView.theme({
|
||||
'&': {
|
||||
borderWidth: 'var(--border-width-base)',
|
||||
borderStyle: 'var(--input-border-style, var(--border-style-base))',
|
||||
borderColor: 'var(--input-border-color, var(--border-color-base))',
|
||||
borderRadius: 'var(--input-border-radius, var(--border-radius-base))',
|
||||
backgroundColor: 'var(--color-expression-editor-background)',
|
||||
},
|
||||
'&.cm-focused': {
|
||||
borderColor: 'var(--color-secondary)',
|
||||
outline: 'unset !important',
|
||||
},
|
||||
'.cm-content': {
|
||||
fontFamily: "Menlo, Consolas, 'DejaVu Sans Mono', monospace !important",
|
||||
height: '220px',
|
||||
padding: 'var(--spacing-xs)',
|
||||
color: 'var(--input-font-color, var(--color-text-dark))',
|
||||
},
|
||||
'.cm-line': {
|
||||
padding: '0',
|
||||
},
|
||||
}),
|
||||
syntaxHighlighting(
|
||||
HighlightStyle.define([
|
||||
{
|
||||
tag: tags.content,
|
||||
class: SYNTAX_HIGHLIGHTING_CLASSES.plaintext,
|
||||
},
|
||||
{
|
||||
tag: tags.className,
|
||||
class: SYNTAX_HIGHLIGHTING_CLASSES.brokenResolvable,
|
||||
},
|
||||
/**
|
||||
* Resolvables are dynamically styled with
|
||||
* `cm-valid-resolvable` and `cm-invalid-resolvable`
|
||||
*/
|
||||
]),
|
||||
),
|
||||
];
|
||||
|
||||
export const DYNAMICALLY_STYLED_RESOLVABLES_THEME = EditorView.theme({
|
||||
['.' + SYNTAX_HIGHLIGHTING_CLASSES.validResolvable]: {
|
||||
color: 'var(--color-valid-resolvable-foreground)',
|
||||
backgroundColor: 'var(--color-valid-resolvable-background)',
|
||||
},
|
||||
['.' + SYNTAX_HIGHLIGHTING_CLASSES.invalidResolvable]: {
|
||||
color: 'var(--color-invalid-resolvable-foreground)',
|
||||
backgroundColor: 'var(--color-invalid-resolvable-background)',
|
||||
},
|
||||
});
|
@ -0,0 +1,24 @@
|
||||
type Range = { from: number; to: number };
|
||||
|
||||
export type RawSegment = { text: string; type: string } & Range;
|
||||
|
||||
export type Segment = Plaintext | Resolvable;
|
||||
|
||||
export type Plaintext = { kind: 'plaintext'; plaintext: string } & Range;
|
||||
|
||||
export type Resolvable = {
|
||||
kind: 'resolvable';
|
||||
resolvable: string;
|
||||
resolved: unknown;
|
||||
error: boolean;
|
||||
fullError: Error | null;
|
||||
} & Range;
|
||||
|
||||
export type Resolved = Resolvable;
|
||||
|
||||
export namespace ColoringStateEffect {
|
||||
export type Value = {
|
||||
kind: 'plaintext' | 'resolvable';
|
||||
error: boolean;
|
||||
} & Range;
|
||||
}
|
@ -1,373 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div ref="expression-editor" :style="editorStyle" class="ignore-key-press" @keydown.stop></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
import Vue from 'vue';
|
||||
|
||||
import 'quill/dist/quill.core.css';
|
||||
|
||||
// @ts-ignore
|
||||
import Quill from 'quill';
|
||||
import DeltaOperation from 'quill-delta';
|
||||
// @ts-ignore
|
||||
import AutoFormat from 'quill-autoformat';
|
||||
import {
|
||||
NodeParameterValue,
|
||||
Workflow,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
IVariableItemSelected,
|
||||
} from '@/Interface';
|
||||
import { genericHelpers } from '@/mixins/genericHelpers';
|
||||
import { workflowHelpers } from '@/mixins/workflowHelpers';
|
||||
|
||||
import mixins from 'vue-typed-mixins';
|
||||
|
||||
export default mixins(
|
||||
genericHelpers,
|
||||
workflowHelpers,
|
||||
)
|
||||
.extend({
|
||||
name: 'ExpressionInput',
|
||||
props: [
|
||||
'rows',
|
||||
'value',
|
||||
'parameter',
|
||||
'path',
|
||||
'resolvedValue',
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
editor: null as null | Quill,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
editorStyle () {
|
||||
let rows = 1;
|
||||
if (this.rows) {
|
||||
rows = parseInt(this.rows, 10);
|
||||
}
|
||||
|
||||
return {
|
||||
'height': Math.max((rows * 26 + 10), 40) + 'px',
|
||||
};
|
||||
},
|
||||
workflow (): Workflow {
|
||||
return this.getCurrentWorkflow();
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
value () {
|
||||
if (this.resolvedValue) {
|
||||
// When resolved value gets displayed update the input automatically
|
||||
this.initValue();
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
const that = this;
|
||||
|
||||
// tslint:disable-next-line
|
||||
const Inline = Quill.import('blots/inline');
|
||||
|
||||
class VariableField extends Inline {
|
||||
static create (value: string) {
|
||||
const node = super.create(value);
|
||||
node.setAttribute('data-value', value);
|
||||
node.setAttribute('class', 'variable');
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
static formats (domNode: HTMLElement) {
|
||||
// For the not resolved one the value can be read directly from the dom
|
||||
let variableName = domNode.innerHTML.trim();
|
||||
if (that.resolvedValue) {
|
||||
// For the resolve done it has to get the one from creation.
|
||||
// It will not update on change but because the init runs on every change it does not really matter
|
||||
variableName = domNode.getAttribute('data-value') as string;
|
||||
}
|
||||
|
||||
const newClasses = that.getPlaceholderClasses(variableName);
|
||||
if (domNode.getAttribute('class') !== newClasses) {
|
||||
// Only update when it changed else we get an endless loop!
|
||||
domNode.setAttribute('class', newClasses);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
VariableField.blotName = 'variable';
|
||||
// @ts-ignore
|
||||
VariableField.className = 'variable';
|
||||
// @ts-ignore
|
||||
VariableField.tagName = 'span';
|
||||
|
||||
AutoFormat.DEFAULTS = {
|
||||
expression: {
|
||||
trigger: /\B[\w\s]/,
|
||||
find: /\{\{[^\s,;:!?}]+\}\}/i,
|
||||
format: 'variable',
|
||||
},
|
||||
};
|
||||
|
||||
Quill.register({
|
||||
'modules/autoformat': AutoFormat,
|
||||
'formats/variable': VariableField,
|
||||
});
|
||||
|
||||
this.editor = new Quill(this.$refs['expression-editor'] as Element, {
|
||||
readOnly: !!this.resolvedValue || this.isReadOnly,
|
||||
modules: {
|
||||
autoformat: {},
|
||||
keyboard: {
|
||||
bindings: {
|
||||
'list autofill': null,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
this.editor.root.addEventListener('blur', (event: Event) => {
|
||||
this.$emit('blur', event);
|
||||
});
|
||||
|
||||
this.initValue();
|
||||
|
||||
if (!this.resolvedValue) {
|
||||
// Only call update when not resolved value gets displayed
|
||||
this.setFocus();
|
||||
this.editor.on('text-change', () => this.update());
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// ------------------------------- EDITOR -------------------------------
|
||||
customizeVariable (variableName: string) {
|
||||
const returnData = {
|
||||
classes: [] as string[],
|
||||
message: variableName as string,
|
||||
};
|
||||
|
||||
let value;
|
||||
try {
|
||||
value = this.resolveExpression(`=${variableName}`);
|
||||
|
||||
if (value !== undefined) {
|
||||
returnData.classes.push('valid');
|
||||
} else {
|
||||
returnData.classes.push('invalid');
|
||||
}
|
||||
} catch (e) {
|
||||
returnData.classes.push('invalid');
|
||||
}
|
||||
|
||||
return returnData;
|
||||
},
|
||||
// Resolves the given variable. If it is not valid it will return
|
||||
// an error-string.
|
||||
resolveParameterString (variableName: string) {
|
||||
let returnValue;
|
||||
try {
|
||||
returnValue = this.resolveExpression(`=${variableName}`);
|
||||
} catch (error) {
|
||||
return `[invalid (${error.message})]`;
|
||||
}
|
||||
if (returnValue === undefined) {
|
||||
return '[not found]';
|
||||
}
|
||||
|
||||
return returnValue;
|
||||
},
|
||||
getPlaceholderClasses (variableName: string) {
|
||||
const customizeData = this.customizeVariable(variableName);
|
||||
return 'variable ' + customizeData.classes.join(' ');
|
||||
},
|
||||
getValue () {
|
||||
if (!this.editor) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const content = this.editor.getContents();
|
||||
if (!content || !content.ops) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let returnValue = '';
|
||||
|
||||
// Convert the editor operations into a string
|
||||
content.ops.forEach((item: DeltaOperation) => {
|
||||
if (!item.insert) {
|
||||
return;
|
||||
}
|
||||
|
||||
returnValue += item.insert;
|
||||
});
|
||||
|
||||
// For some unknown reason does the Quill always return a "\n"
|
||||
// at the end. Remove it here manually
|
||||
return '=' + returnValue.replace(/\s+$/g, '');
|
||||
},
|
||||
setFocus () {
|
||||
// TODO: There is a bug that when opening ExpressionEditor and typing directly it shows the first letter and
|
||||
// then adds the second letter in from of the first on
|
||||
this.editor!.focus();
|
||||
},
|
||||
|
||||
itemSelected (eventData: IVariableItemSelected) {
|
||||
// We can only get the selection if editor is in focus so make
|
||||
// sure it is
|
||||
this.editor!.focus();
|
||||
const selection = this.editor!.getSelection();
|
||||
|
||||
let addIndex = null;
|
||||
if (selection) {
|
||||
addIndex = selection.index;
|
||||
}
|
||||
|
||||
if (addIndex) {
|
||||
// If we have a location to add variable to add it there
|
||||
this.editor!.insertText(addIndex, `{{${eventData.variable}}}`, 'variable', true);
|
||||
this.update();
|
||||
} else {
|
||||
// If no position got found add it to end
|
||||
let newValue = this.getValue();
|
||||
if (newValue === '=' || newValue === '=0') {
|
||||
newValue = `{{${eventData.variable}}}\n`;
|
||||
} else {
|
||||
newValue += ` {{${eventData.variable}}}\n`;
|
||||
}
|
||||
|
||||
this.$emit('change', newValue, true);
|
||||
if (!this.resolvedValue) {
|
||||
Vue.nextTick(() => {
|
||||
this.initValue();
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
initValue () {
|
||||
if (!this.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
let currentValue = this.value;
|
||||
|
||||
if (currentValue.charAt(0) === '=') {
|
||||
currentValue = currentValue.slice(1);
|
||||
}
|
||||
|
||||
// Convert the expression string into a Quill Operations
|
||||
const editorOperations: DeltaOperation[] = [];
|
||||
currentValue.replace(/\{\{(.*?)\}\}/ig, '*%%#_@^$1*%%#_@').split('*%%#_@').forEach((value: string) => {
|
||||
if (value && value.charAt(0) === '^') {
|
||||
// Is variable
|
||||
let displayValue = `{{${value.slice(1)}}}` as string | number | boolean | null | undefined;
|
||||
if (this.resolvedValue) {
|
||||
displayValue = [null, undefined].includes(displayValue as null | undefined) ? '' : displayValue;
|
||||
displayValue = this.resolveParameterString((displayValue as string).toString()) as NodeParameterValue;
|
||||
}
|
||||
|
||||
displayValue = [null, undefined].includes(displayValue as null | undefined) ? '' : displayValue;
|
||||
|
||||
editorOperations.push({
|
||||
attributes: {
|
||||
variable: `{{${value.slice(1)}}}`,
|
||||
},
|
||||
insert: (displayValue as string).toString(),
|
||||
});
|
||||
} else {
|
||||
// Is text
|
||||
editorOperations.push({
|
||||
insert: value,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
this.editor!.setContents(editorOperations);
|
||||
},
|
||||
update () {
|
||||
this.$emit('input', this.getValue());
|
||||
this.$emit('change', this.getValue());
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
.variable-wrapper {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.variable-value {
|
||||
font-weight: bold;
|
||||
color: var(--color-text-dark);
|
||||
background-color: var(--color-text-base);
|
||||
padding: 3px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.variable-delete {
|
||||
position: relative;
|
||||
left: -3px;
|
||||
top: -8px;
|
||||
display: none;
|
||||
color: var(--color-text-xlight);
|
||||
font-weight: bold;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
.variable-wrapper:hover .variable-delete {
|
||||
display: inline;
|
||||
background-color: var(--color-danger);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.variable {
|
||||
font-weight: bold;
|
||||
color: #000;
|
||||
background-color: var(--color-text-base);
|
||||
padding: 3px;
|
||||
border-radius: 3px;
|
||||
margin: 0 2px;
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
&.invalid {
|
||||
background-color: var(--color-danger);
|
||||
}
|
||||
|
||||
&.valid {
|
||||
background-color: var(--color-success);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.ql-editor {
|
||||
padding: 0.5em 1em;
|
||||
}
|
||||
|
||||
.ql-disabled .ql-editor {
|
||||
border-width: 1px;
|
||||
border: 1px solid $custom-expression-text;
|
||||
color: $custom-expression-text;
|
||||
background-color: $custom-expression-background;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.ql-disabled .ql-editor .variable {
|
||||
color: #303030;
|
||||
}
|
||||
|
||||
</style>
|
@ -6,6 +6,7 @@
|
||||
:parameter="parameter"
|
||||
:path="path"
|
||||
:eventSource="eventSource || 'ndv'"
|
||||
:isReadOnly="isReadOnly"
|
||||
@closeDialog="closeExpressionEditDialog"
|
||||
@valueChanged="expressionUpdated"
|
||||
></expression-edit>
|
||||
|
@ -157,23 +157,14 @@ export default mixins(
|
||||
computedValue = this.$locale.baseText('parameterInput.emptyString');
|
||||
}
|
||||
} catch (error) {
|
||||
computedValue = `[${this.$locale.baseText('parameterInput.error')}}: ${error.message}]`;
|
||||
computedValue = `[${this.$locale.baseText('parameterInput.error')}: ${error.message}]`;
|
||||
}
|
||||
|
||||
return typeof computedValue === 'string' ? computedValue : JSON.stringify(computedValue);
|
||||
},
|
||||
expressionOutput(): string | null {
|
||||
if (this.isValueExpression && this.expressionValueComputed) {
|
||||
const inputData = this.ndvStore.ndvInputData;
|
||||
if (!inputData || (inputData && inputData.length <= 1)) {
|
||||
return this.expressionValueComputed;
|
||||
}
|
||||
|
||||
return this.$locale.baseText(`parameterInput.expressionResult`, {
|
||||
interpolate: {
|
||||
result: this.expressionValueComputed,
|
||||
},
|
||||
});
|
||||
return this.expressionValueComputed;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
@ -537,13 +537,15 @@ export default mixins(
|
||||
if (parentNode.length) {
|
||||
// If the node has an input node add the input data
|
||||
|
||||
const activeInputParentNode = parentNode.find(node => node === this.ndvStore.ndvInputNodeName)!;
|
||||
|
||||
// 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 nodeConnection = this.workflow.getNodeConnectionIndexes(activeNode.name, parentNode[0], 'main');
|
||||
const nodeConnection = this.workflow.getNodeConnectionIndexes(activeNode.name, activeInputParentNode, 'main');
|
||||
const outputIndex = nodeConnection === undefined ? 0: nodeConnection.sourceIndex;
|
||||
|
||||
tempOutputData = this.getNodeRunDataOutput(parentNode[0], runData, filterText, itemIndex, 0, 'main', outputIndex, true) as IVariableSelectorOption[];
|
||||
tempOutputData = this.getNodeRunDataOutput(activeInputParentNode, runData, filterText, itemIndex, 0, 'main', outputIndex, true) as IVariableSelectorOption[];
|
||||
|
||||
const pinDataOptions: IVariableSelectorOption[] = [
|
||||
{
|
||||
|
@ -53,6 +53,20 @@ export default mixins(externalHooks).extend({
|
||||
'extendAll',
|
||||
'item',
|
||||
],
|
||||
mounted() {
|
||||
if (this.extended) return;
|
||||
|
||||
const shouldAutoExtend = [
|
||||
this.$locale.baseText('variableSelectorItem.currentNode'),
|
||||
this.$locale.baseText('variableSelectorItem.inputData'),
|
||||
this.$locale.baseText('variableSelectorItem.binary'),
|
||||
this.$locale.baseText('variableSelectorItem.json'),
|
||||
].includes(this.item.name) && this.item.key === undefined;
|
||||
|
||||
if (shouldAutoExtend) {
|
||||
this.extended = true;
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
itemAddOperations () {
|
||||
const returnOptions = [
|
||||
|
@ -69,7 +69,7 @@ export const COMMUNITY_NODES_NPM_INSTALLATION_URL = 'https://docs.npmjs.com/down
|
||||
export const COMMUNITY_NODES_RISKS_DOCS_URL = `https://docs.n8n.io/integrations/community-nodes/risks/`;
|
||||
export const COMMUNITY_NODES_BLOCKLIST_DOCS_URL = `https://docs.n8n.io/integrations/community-nodes/blocklist/`;
|
||||
export const CUSTOM_NODES_DOCS_URL = `https://docs.n8n.io/integrations/creating-nodes/code/create-n8n-nodes-module/`;
|
||||
|
||||
export const EXPRESSIONS_DOCS_URL = 'https://docs.n8n.io/code-examples/expressions/';
|
||||
|
||||
// node types
|
||||
export const BAMBOO_HR_NODE_TYPE = 'n8n-nodes-base.bambooHr';
|
||||
|
@ -845,6 +845,7 @@ export const workflowHelpers = mixins(
|
||||
this.uiStore.stateIsDirty = false;
|
||||
this.$externalHooks().run('workflow.afterUpdate', { workflowData });
|
||||
|
||||
this.getCurrentWorkflow(true); // refresh cache
|
||||
return true;
|
||||
} catch (e) {
|
||||
this.uiStore.removeActiveAction('workflowSaving');
|
||||
|
@ -479,10 +479,16 @@
|
||||
"executionSidebar.searchPlaceholder": "Search executions...",
|
||||
"executionView.onPaste.title": "Cannot paste here",
|
||||
"executionView.onPaste.message": "This view is read-only. Switch to <i>Workflow</i> tab to be able to edit the current workflow",
|
||||
"expressionEdit.anythingInside": "Anything inside",
|
||||
"expressionEdit.isJavaScript": "is JavaScript.",
|
||||
"expressionEdit.learnMore": "Learn more",
|
||||
"expressionEdit.editExpression": "Edit Expression",
|
||||
"expressionEdit.expression": "Expression",
|
||||
"expressionEdit.result": "Result",
|
||||
"expressionEdit.resultOfItem1": "Result of item 1",
|
||||
"expressionEdit.variableSelector": "Variable Selector",
|
||||
"expressionModalInput.empty": "[empty]",
|
||||
"expressionModalInput.undefined": "[undefined]",
|
||||
"expressionModalInput.null": "null",
|
||||
"fakeDoor.credentialEdit.sharing.name": "Sharing",
|
||||
"fakeDoor.credentialEdit.sharing.actionBox.title": "Sharing is only available on <a href=\"https://n8n.io/cloud/\" target=\"_blank\">n8n Cloud</a> right now",
|
||||
"fakeDoor.credentialEdit.sharing.actionBox.description": "We’re working on bringing it to this edition of n8n, as a paid feature. If you’d like to be the first to hear when it’s ready, join the list.",
|
||||
@ -1227,7 +1233,11 @@
|
||||
"variableSelector.outputData": "Output Data",
|
||||
"variableSelector.parameters": "Parameters",
|
||||
"variableSelector.variableFilter": "Variable filter...",
|
||||
"variableSelectorItem.binary": "Binary",
|
||||
"variableSelectorItem.currentNode": "Current Node",
|
||||
"variableSelectorItem.empty": "--- EMPTY ---",
|
||||
"variableSelectorItem.inputData": "Input Data",
|
||||
"variableSelectorItem.json": "JSON",
|
||||
"variableSelectorItem.selectItem": "Select Item",
|
||||
"versionCard.breakingChanges": "Breaking changes",
|
||||
"versionCard.released": "Released",
|
||||
|
@ -63,10 +63,6 @@
|
||||
// "no-default-export": true,
|
||||
"no-duplicate-variable": true,
|
||||
"no-inferrable-types": true,
|
||||
"no-namespace": [
|
||||
true,
|
||||
"allow-declarations"
|
||||
],
|
||||
"no-reference": true,
|
||||
"no-string-throw": true,
|
||||
"no-unused-expression": true,
|
||||
|
@ -277,7 +277,16 @@ export class Expression {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
error instanceof Error &&
|
||||
typeof error.message === 'string' &&
|
||||
error.name === 'SyntaxError'
|
||||
) {
|
||||
throw new Error(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -289,15 +289,12 @@ export class WorkflowDataProxy {
|
||||
|
||||
if (!that.runExecutionData.resultData.runData.hasOwnProperty(nodeName)) {
|
||||
if (that.workflow.getNode(nodeName)) {
|
||||
throw new ExpressionError(
|
||||
`The node "${nodeName}" hasn't been executed yet, so you can't reference its output data`,
|
||||
{
|
||||
runIndex: that.runIndex,
|
||||
itemIndex: that.itemIndex,
|
||||
},
|
||||
);
|
||||
throw new ExpressionError(`no data, execute "${nodeName}" node first`, {
|
||||
runIndex: that.runIndex,
|
||||
itemIndex: that.itemIndex,
|
||||
});
|
||||
}
|
||||
throw new ExpressionError(`No node called "${nodeName}" in this workflow`, {
|
||||
throw new ExpressionError(`"${nodeName}" node doesn't exist`, {
|
||||
runIndex: that.runIndex,
|
||||
itemIndex: that.itemIndex,
|
||||
});
|
||||
@ -335,13 +332,10 @@ export class WorkflowDataProxy {
|
||||
);
|
||||
|
||||
if (nodeConnection === undefined) {
|
||||
throw new ExpressionError(
|
||||
`The node "${that.activeNodeName}" is not connected with node "${nodeName}" so no data can get returned from it.`,
|
||||
{
|
||||
runIndex: that.runIndex,
|
||||
itemIndex: that.itemIndex,
|
||||
},
|
||||
);
|
||||
throw new ExpressionError(`connect ${that.activeNodeName} to ${nodeName}`, {
|
||||
runIndex: that.runIndex,
|
||||
itemIndex: that.itemIndex,
|
||||
});
|
||||
}
|
||||
outputIndex = nodeConnection.sourceIndex;
|
||||
}
|
||||
@ -383,16 +377,16 @@ export class WorkflowDataProxy {
|
||||
const that = this;
|
||||
const node = this.workflow.nodes[nodeName];
|
||||
|
||||
if (!node) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return new Proxy(
|
||||
{ binary: undefined, data: undefined, json: undefined },
|
||||
{
|
||||
get(target, name, receiver) {
|
||||
name = name.toString();
|
||||
|
||||
if (!node) {
|
||||
throw new ExpressionError(`"${nodeName}" node doesn't exist`);
|
||||
}
|
||||
|
||||
if (['binary', 'data', 'json'].includes(name)) {
|
||||
const executionData = that.getNodeExecutionData(nodeName, shortSyntax, undefined);
|
||||
if (executionData.length <= that.itemIndex) {
|
||||
@ -463,8 +457,11 @@ export class WorkflowDataProxy {
|
||||
{},
|
||||
{
|
||||
get(target, name, receiver) {
|
||||
if (process.env.N8N_BLOCK_ENV_ACCESS_IN_NODE === 'true') {
|
||||
throw new ExpressionError('Environment variable access got disabled', {
|
||||
if (
|
||||
typeof process === 'undefined' || // env vars are inaccessible to frontend
|
||||
process.env.N8N_BLOCK_ENV_ACCESS_IN_NODE === 'true'
|
||||
) {
|
||||
throw new ExpressionError('access to env vars denied', {
|
||||
causeDetailed:
|
||||
'If you need access please contact the administrator to remove the environment variable ‘N8N_BLOCK_ENV_ACCESS_IN_NODE‘',
|
||||
runIndex: that.runIndex,
|
||||
@ -544,7 +541,7 @@ export class WorkflowDataProxy {
|
||||
const value = that.workflow[name as keyof typeof target];
|
||||
|
||||
if (value === undefined && name === 'id') {
|
||||
throw new ExpressionError('Workflow is not saved', {
|
||||
throw new ExpressionError('save workflow to view', {
|
||||
description: `Please save the workflow first to use $workflow`,
|
||||
runIndex: that.runIndex,
|
||||
itemIndex: that.itemIndex,
|
||||
@ -906,7 +903,7 @@ export class WorkflowDataProxy {
|
||||
|
||||
const referencedNode = that.workflow.getNode(nodeName);
|
||||
if (referencedNode === null) {
|
||||
throw createExpressionError(`No node called ‘${nodeName}‘`);
|
||||
throw createExpressionError(`"${nodeName}" node doesn't exist`);
|
||||
}
|
||||
|
||||
return new Proxy(
|
||||
|
File diff suppressed because one or more lines are too long
178
pnpm-lock.yaml
178
pnpm-lock.yaml
@ -10,14 +10,6 @@ overrides:
|
||||
fork-ts-checker-webpack-plugin: ^6.0.4
|
||||
globby: ^11.1.0
|
||||
|
||||
patchedDependencies:
|
||||
quill@2.0.0-dev.4:
|
||||
hash: wzpqp2wkmsfkgdbvlakyt3wndy
|
||||
path: patches/quill@2.0.0-dev.4.patch
|
||||
element-ui@2.15.12:
|
||||
hash: aaa3sc7bmwb4jwg35ga5npx4he
|
||||
path: patches/element-ui@2.15.12.patch
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
@ -487,8 +479,8 @@ importers:
|
||||
'@codemirror/lang-javascript': ^6.0.2
|
||||
'@codemirror/language': ^6.2.1
|
||||
'@codemirror/lint': ^6.0.0
|
||||
'@codemirror/state': ^6.1.1
|
||||
'@codemirror/view': ^6.2.1
|
||||
'@codemirror/state': ^6.1.4
|
||||
'@codemirror/view': ^6.5.1
|
||||
'@fontsource/open-sans': ^4.5.0
|
||||
'@fortawesome/fontawesome-svg-core': ^1.2.35
|
||||
'@fortawesome/free-regular-svg-icons': ^6.1.1
|
||||
@ -505,7 +497,6 @@ importers:
|
||||
'@types/lodash.get': ^4.4.6
|
||||
'@types/lodash.set': ^4.3.6
|
||||
'@types/luxon': ^2.0.9
|
||||
'@types/quill': ^2.0.1
|
||||
'@types/uuid': ^8.3.2
|
||||
'@vitejs/plugin-legacy': ^1.8.2
|
||||
'@vitejs/plugin-vue2': ^1.1.2
|
||||
@ -533,8 +524,6 @@ importers:
|
||||
normalize-wheel: ^1.0.1
|
||||
pinia: ^2.0.22
|
||||
prismjs: ^1.17.1
|
||||
quill: 2.0.0-dev.4
|
||||
quill-autoformat: ^0.1.1
|
||||
sass: ^1.55.0
|
||||
sass-loader: ^10.1.1
|
||||
string-template-parser: ^1.2.6
|
||||
@ -560,13 +549,13 @@ importers:
|
||||
vue2-touch-events: ^3.2.1
|
||||
xss: ^1.0.10
|
||||
dependencies:
|
||||
'@codemirror/autocomplete': 6.3.0_2jvnbzbiudbravcwrws2kgi36q
|
||||
'@codemirror/autocomplete': 6.3.0_wo7q3lvweq5evsu423o7qzum5i
|
||||
'@codemirror/commands': 6.1.2
|
||||
'@codemirror/lang-javascript': 6.1.0
|
||||
'@codemirror/language': 6.2.1
|
||||
'@codemirror/lint': 6.0.0
|
||||
'@codemirror/state': 6.1.2
|
||||
'@codemirror/view': 6.3.1
|
||||
'@codemirror/state': 6.1.4
|
||||
'@codemirror/view': 6.5.1
|
||||
'@fontsource/open-sans': 4.5.12
|
||||
'@fortawesome/fontawesome-svg-core': 1.2.36
|
||||
'@fortawesome/free-regular-svg-icons': 6.2.0
|
||||
@ -594,8 +583,6 @@ importers:
|
||||
normalize-wheel: 1.0.1
|
||||
pinia: 2.0.23_xjcbg5znturqejtkpd33hx726m
|
||||
prismjs: 1.29.0
|
||||
quill: 2.0.0-dev.4_wzpqp2wkmsfkgdbvlakyt3wndy
|
||||
quill-autoformat: 0.1.2
|
||||
timeago.js: 4.0.2
|
||||
uuid: 8.3.2
|
||||
v-click-outside: 3.2.0
|
||||
@ -624,7 +611,6 @@ importers:
|
||||
'@types/lodash.get': 4.4.7
|
||||
'@types/lodash.set': 4.3.7
|
||||
'@types/luxon': 2.4.0
|
||||
'@types/quill': 2.0.9
|
||||
'@types/uuid': 8.3.4
|
||||
'@vitejs/plugin-legacy': 1.8.2_vite@2.9.5
|
||||
'@vitejs/plugin-vue2': 1.1.2_vite@2.9.5+vue@2.7.13
|
||||
@ -2627,7 +2613,7 @@ packages:
|
||||
minimist: 1.2.7
|
||||
dev: true
|
||||
|
||||
/@codemirror/autocomplete/6.3.0_2jvnbzbiudbravcwrws2kgi36q:
|
||||
/@codemirror/autocomplete/6.3.0_wo7q3lvweq5evsu423o7qzum5i:
|
||||
resolution: {integrity: sha512-4jEvh3AjJZTDKazd10J6ZsCIqaYxDMCeua5ouQxY8hlFIml+nr7le0SgBhT3SIytFBmdzPK3AUhXGuW3T79nVg==}
|
||||
peerDependencies:
|
||||
'@codemirror/language': ^6.0.0
|
||||
@ -2636,8 +2622,8 @@ packages:
|
||||
'@lezer/common': ^1.0.0
|
||||
dependencies:
|
||||
'@codemirror/language': 6.2.1
|
||||
'@codemirror/state': 6.1.2
|
||||
'@codemirror/view': 6.3.1
|
||||
'@codemirror/state': 6.1.4
|
||||
'@codemirror/view': 6.5.1
|
||||
'@lezer/common': 1.0.1
|
||||
dev: false
|
||||
|
||||
@ -2645,19 +2631,19 @@ packages:
|
||||
resolution: {integrity: sha512-sO3jdX1s0pam6lIdeSJLMN3DQ6mPEbM4yLvyKkdqtmd/UDwhXA5+AwFJ89rRXm6vTeOXBsE5cAmlos/t7MJdgg==}
|
||||
dependencies:
|
||||
'@codemirror/language': 6.2.1
|
||||
'@codemirror/state': 6.1.2
|
||||
'@codemirror/view': 6.3.1
|
||||
'@codemirror/state': 6.1.4
|
||||
'@codemirror/view': 6.5.1
|
||||
'@lezer/common': 1.0.1
|
||||
dev: false
|
||||
|
||||
/@codemirror/lang-javascript/6.1.0:
|
||||
resolution: {integrity: sha512-wAWEY1Wdis2cKDy9A5q/rUmzLHFbZgoupJBcGaeMMsDPi68Rm90NsmzAEODE5kW8mYdRKFhQ157WJghOZ3yYdg==}
|
||||
dependencies:
|
||||
'@codemirror/autocomplete': 6.3.0_2jvnbzbiudbravcwrws2kgi36q
|
||||
'@codemirror/autocomplete': 6.3.0_wo7q3lvweq5evsu423o7qzum5i
|
||||
'@codemirror/language': 6.2.1
|
||||
'@codemirror/lint': 6.0.0
|
||||
'@codemirror/state': 6.1.2
|
||||
'@codemirror/view': 6.3.1
|
||||
'@codemirror/state': 6.1.4
|
||||
'@codemirror/view': 6.5.1
|
||||
'@lezer/common': 1.0.1
|
||||
'@lezer/javascript': 1.0.2
|
||||
dev: false
|
||||
@ -2665,8 +2651,8 @@ packages:
|
||||
/@codemirror/language/6.2.1:
|
||||
resolution: {integrity: sha512-MC3svxuvIj0MRpFlGHxLS6vPyIdbTr2KKPEW46kCoCXw2ktb4NTkpkPBI/lSP/FoNXLCBJ0mrnUi1OoZxtpW1Q==}
|
||||
dependencies:
|
||||
'@codemirror/state': 6.1.2
|
||||
'@codemirror/view': 6.3.1
|
||||
'@codemirror/state': 6.1.4
|
||||
'@codemirror/view': 6.5.1
|
||||
'@lezer/common': 1.0.1
|
||||
'@lezer/highlight': 1.1.1
|
||||
'@lezer/lr': 1.2.3
|
||||
@ -2676,19 +2662,19 @@ packages:
|
||||
/@codemirror/lint/6.0.0:
|
||||
resolution: {integrity: sha512-nUUXcJW1Xp54kNs+a1ToPLK8MadO0rMTnJB8Zk4Z8gBdrN0kqV7uvUraU/T2yqg+grDNR38Vmy/MrhQN/RgwiA==}
|
||||
dependencies:
|
||||
'@codemirror/state': 6.1.2
|
||||
'@codemirror/view': 6.3.1
|
||||
'@codemirror/state': 6.1.4
|
||||
'@codemirror/view': 6.5.1
|
||||
crelt: 1.0.5
|
||||
dev: false
|
||||
|
||||
/@codemirror/state/6.1.2:
|
||||
resolution: {integrity: sha512-Mxff85Hp5va+zuj+H748KbubXjrinX/k28lj43H14T2D0+4kuvEFIEIO7hCEcvBT8ubZyIelt9yGOjj2MWOEQA==}
|
||||
/@codemirror/state/6.1.4:
|
||||
resolution: {integrity: sha512-g+3OJuRylV5qsXuuhrc6Cvs1NQluNioepYMM2fhnpYkNk7NgX+j0AFuevKSVKzTDmDyt9+Puju+zPdHNECzCNQ==}
|
||||
dev: false
|
||||
|
||||
/@codemirror/view/6.3.1:
|
||||
resolution: {integrity: sha512-NKPBphoV9W2Q6tKXk+ge4q5EhMOOC0rpwdGS80/slNSfsVqkN4gwXIEqSprXJFlf9aUKZU7WhPvqRBMNH+hJkQ==}
|
||||
/@codemirror/view/6.5.1:
|
||||
resolution: {integrity: sha512-xBKP8N3AXOs06VcKvIuvIQoUlGs7Hb78ftJWahLaRX909jKPMgGxR5XjvrawzTTZMSTU3DzdjDNPwG6fPM/ypQ==}
|
||||
dependencies:
|
||||
'@codemirror/state': 6.1.2
|
||||
'@codemirror/state': 6.1.4
|
||||
style-mod: 4.0.0
|
||||
w3c-keyname: 2.2.6
|
||||
dev: false
|
||||
@ -5981,13 +5967,6 @@ packages:
|
||||
/@types/qs/6.9.7:
|
||||
resolution: {integrity: sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==}
|
||||
|
||||
/@types/quill/2.0.9:
|
||||
resolution: {integrity: sha512-/n40Ypp+jF3GDLqB/5z1P+Odq1K98txXbBgRDkG6Z90LGC1AwQPtZWNeOdDg0yUlgBSUASmpeDn3eBPUuPXtuw==}
|
||||
dependencies:
|
||||
parchment: 1.1.4
|
||||
quill-delta: 4.2.2
|
||||
dev: true
|
||||
|
||||
/@types/range-parser/1.2.4:
|
||||
resolution: {integrity: sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==}
|
||||
|
||||
@ -8877,6 +8856,7 @@ packages:
|
||||
/clone/2.1.2:
|
||||
resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==}
|
||||
engines: {node: '>=0.8'}
|
||||
dev: true
|
||||
|
||||
/cloneable-readable/1.1.3:
|
||||
resolution: {integrity: sha512-2EF8zTQOxYq70Y4XKtorQupqF0m49MBz2/yf5Bj+MHjvpG3Hy7sImifnqD6UA+TKYxeSV+u6qqQPawN5UvnpKQ==}
|
||||
@ -9942,37 +9922,6 @@ packages:
|
||||
type-detect: 4.0.8
|
||||
dev: true
|
||||
|
||||
/deep-equal/1.1.1:
|
||||
resolution: {integrity: sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==}
|
||||
dependencies:
|
||||
is-arguments: 1.1.1
|
||||
is-date-object: 1.0.5
|
||||
is-regex: 1.1.4
|
||||
object-is: 1.1.5
|
||||
object-keys: 1.1.1
|
||||
regexp.prototype.flags: 1.4.3
|
||||
dev: false
|
||||
|
||||
/deep-equal/2.0.5:
|
||||
resolution: {integrity: sha512-nPiRgmbAtm1a3JsnLCf6/SLfXcjyN5v8L1TXzdCmHrXJ4hx+gW/w1YCcn7z8gJtSiDArZCgYtbao3QqLm/N1Sw==}
|
||||
dependencies:
|
||||
call-bind: 1.0.2
|
||||
es-get-iterator: 1.1.2
|
||||
get-intrinsic: 1.1.3
|
||||
is-arguments: 1.1.1
|
||||
is-date-object: 1.0.5
|
||||
is-regex: 1.1.4
|
||||
isarray: 2.0.5
|
||||
object-is: 1.1.5
|
||||
object-keys: 1.1.1
|
||||
object.assign: 4.1.4
|
||||
regexp.prototype.flags: 1.4.3
|
||||
side-channel: 1.0.4
|
||||
which-boxed-primitive: 1.0.2
|
||||
which-collection: 1.0.1
|
||||
which-typed-array: 1.1.8
|
||||
dev: false
|
||||
|
||||
/deep-is/0.1.4:
|
||||
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
|
||||
|
||||
@ -10594,6 +10543,7 @@ packages:
|
||||
is-set: 2.0.2
|
||||
is-string: 1.0.7
|
||||
isarray: 2.0.5
|
||||
dev: true
|
||||
|
||||
/es-module-lexer/0.9.3:
|
||||
resolution: {integrity: sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==}
|
||||
@ -11258,10 +11208,6 @@ packages:
|
||||
resolution: {integrity: sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==}
|
||||
dev: true
|
||||
|
||||
/eventemitter3/4.0.7:
|
||||
resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
|
||||
dev: false
|
||||
|
||||
/events/1.1.1:
|
||||
resolution: {integrity: sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==}
|
||||
engines: {node: '>=0.4.x'}
|
||||
@ -11544,6 +11490,7 @@ packages:
|
||||
|
||||
/fast-diff/1.2.0:
|
||||
resolution: {integrity: sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==}
|
||||
dev: true
|
||||
|
||||
/fast-glob/3.2.12:
|
||||
resolution: {integrity: sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==}
|
||||
@ -13416,6 +13363,7 @@ packages:
|
||||
|
||||
/is-map/2.0.2:
|
||||
resolution: {integrity: sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==}
|
||||
dev: true
|
||||
|
||||
/is-nan/1.3.2:
|
||||
resolution: {integrity: sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==}
|
||||
@ -13521,6 +13469,7 @@ packages:
|
||||
|
||||
/is-set/2.0.2:
|
||||
resolution: {integrity: sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==}
|
||||
dev: true
|
||||
|
||||
/is-shared-array-buffer/1.0.2:
|
||||
resolution: {integrity: sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==}
|
||||
@ -13582,22 +13531,11 @@ packages:
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: true
|
||||
|
||||
/is-weakmap/2.0.1:
|
||||
resolution: {integrity: sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==}
|
||||
dev: false
|
||||
|
||||
/is-weakref/1.0.2:
|
||||
resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==}
|
||||
dependencies:
|
||||
call-bind: 1.0.2
|
||||
|
||||
/is-weakset/2.0.2:
|
||||
resolution: {integrity: sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==}
|
||||
dependencies:
|
||||
call-bind: 1.0.2
|
||||
get-intrinsic: 1.1.3
|
||||
dev: false
|
||||
|
||||
/is-whitespace-character/1.0.4:
|
||||
resolution: {integrity: sha512-SDweEzfIZM0SJV0EUga669UTKlmL0Pq8Lno0QDQsPnvECB3IM2aP0gdx5TrU0A01MAPfViaZiI2V1QMZLaKK5w==}
|
||||
dev: true
|
||||
@ -13638,6 +13576,7 @@ packages:
|
||||
|
||||
/isarray/2.0.5:
|
||||
resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==}
|
||||
dev: true
|
||||
|
||||
/isbot/3.6.1:
|
||||
resolution: {integrity: sha512-e1RmjWns87x60QyiHberWWMJGutL3+Ad0nZ8cz735iDEDDS6ApPfKSFo4EMj0PmMZ0m0ntpWIM0ADdqDFvUJPQ==}
|
||||
@ -15024,6 +14963,7 @@ packages:
|
||||
|
||||
/lodash.clonedeep/4.5.0:
|
||||
resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==}
|
||||
dev: false
|
||||
|
||||
/lodash.debounce/4.0.8:
|
||||
resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
|
||||
@ -16316,14 +16256,6 @@ packages:
|
||||
/object-inspect/1.12.2:
|
||||
resolution: {integrity: sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==}
|
||||
|
||||
/object-is/1.1.5:
|
||||
resolution: {integrity: sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
dependencies:
|
||||
call-bind: 1.0.2
|
||||
define-properties: 1.1.4
|
||||
dev: false
|
||||
|
||||
/object-keys/1.1.1:
|
||||
resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@ -16689,14 +16621,6 @@ packages:
|
||||
dot-case: 3.0.4
|
||||
tslib: 2.4.0
|
||||
|
||||
/parchment/1.1.4:
|
||||
resolution: {integrity: sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg==}
|
||||
dev: true
|
||||
|
||||
/parchment/2.0.0-dev.2:
|
||||
resolution: {integrity: sha512-4fgRny4pPISoML08Zp7poi52Dff3E2G1ORTi2D/acJ/RiROdDAMDB6VcQNfBcmehrX5Wixp6dxh6JjLyE5yUNQ==}
|
||||
dev: false
|
||||
|
||||
/parent-module/1.0.1:
|
||||
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
|
||||
engines: {node: '>=6'}
|
||||
@ -17868,41 +17792,6 @@ packages:
|
||||
/queue-microtask/1.2.3:
|
||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||
|
||||
/quill-autoformat/0.1.2:
|
||||
resolution: {integrity: sha512-kRe2rTSmcBDg/oCxhzbjmXXOpQUl0Gak6ZCQshxek/RLvdR8o715qC0WcBRUozqaYbR6PJ+0Z/piINqlYStxWw==}
|
||||
engines: {node: '>=10.15.3', yarn: '>=1.15.2'}
|
||||
dependencies:
|
||||
quill: 2.0.0-dev.4_wzpqp2wkmsfkgdbvlakyt3wndy
|
||||
dev: false
|
||||
|
||||
/quill-delta/4.2.1:
|
||||
resolution: {integrity: sha512-Y2nksOj6Q+4hizre8n0dml76vLNGK4/y86EoI1d7rv6EL1bx7DPDYRmqQMPu1UqFQO/uQuVHQ3fOmm4ZSzWrfA==}
|
||||
dependencies:
|
||||
deep-equal: 1.1.1
|
||||
extend: 3.0.2
|
||||
fast-diff: 1.2.0
|
||||
dev: false
|
||||
|
||||
/quill-delta/4.2.2:
|
||||
resolution: {integrity: sha512-qjbn82b/yJzOjstBgkhtBjN2TNK+ZHP/BgUQO+j6bRhWQQdmj2lH6hXG7+nwwLF41Xgn//7/83lxs9n2BkTtTg==}
|
||||
dependencies:
|
||||
fast-diff: 1.2.0
|
||||
lodash.clonedeep: 4.5.0
|
||||
lodash.isequal: 4.5.0
|
||||
dev: true
|
||||
|
||||
/quill/2.0.0-dev.4_wzpqp2wkmsfkgdbvlakyt3wndy:
|
||||
resolution: {integrity: sha512-9WmMVCEIhf3lDdhzl+i+GBDeDl0BFi65waC4Im8Y4HudEJ9kEEb1lciAz9A8pcDmLMjiMbvz84lNt/U5OBS8Vg==}
|
||||
dependencies:
|
||||
clone: 2.1.2
|
||||
deep-equal: 2.0.5
|
||||
eventemitter3: 4.0.7
|
||||
extend: 3.0.2
|
||||
parchment: 2.0.0-dev.2
|
||||
quill-delta: 4.2.1
|
||||
dev: false
|
||||
patched: true
|
||||
|
||||
/quoted-printable/1.0.1:
|
||||
resolution: {integrity: sha512-cihC68OcGiQOjGiXuo5Jk6XHANTHl1K4JLk/xlEJRTIXfy19Sg6XzB95XonYgr+1rB88bCpr7WZE7D7AlZow4g==}
|
||||
hasBin: true
|
||||
@ -22096,15 +21985,6 @@ packages:
|
||||
is-string: 1.0.7
|
||||
is-symbol: 1.0.4
|
||||
|
||||
/which-collection/1.0.1:
|
||||
resolution: {integrity: sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==}
|
||||
dependencies:
|
||||
is-map: 2.0.2
|
||||
is-set: 2.0.2
|
||||
is-weakmap: 2.0.1
|
||||
is-weakset: 2.0.2
|
||||
dev: false
|
||||
|
||||
/which-module/1.0.0:
|
||||
resolution: {integrity: sha512-F6+WgncZi/mJDrammbTuHe1q0R5hOXv/mBaiNA2TCNT/LTHusX0V+CJnj9XT8ki5ln2UZyyddDgHfCzyrOH7MQ==}
|
||||
dev: true
|
||||
|
Loading…
Reference in New Issue
Block a user