refactor: Refactor input components to composition API (no-changelog) (#9744)

This commit is contained in:
Elias Meire
2024-06-18 15:04:08 +02:00
committed by GitHub
parent 8f94dcc0e9
commit e3cbce5028
12 changed files with 1007 additions and 1178 deletions

View File

@@ -21,72 +21,49 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from 'vue';
import { useToast } from '@/composables/useToast';
import { i18n } from '@/plugins/i18n';
import { useClipboard } from '@/composables/useClipboard'; import { useClipboard } from '@/composables/useClipboard';
import { useI18n } from '@/composables/useI18n';
import { useToast } from '@/composables/useToast';
export default defineComponent({ type Props = {
props: { label?: string;
label: { hint?: string;
type: String, value?: string;
}, copyButtonText: string;
hint: { toastTitle?: string;
type: String, toastMessage?: string;
}, size?: 'medium' | 'large';
value: { collapse?: boolean;
type: String, redactValue?: boolean;
},
copyButtonText: {
type: String,
default(): string {
return i18n.baseText('generic.copy');
},
},
toastTitle: {
type: String,
default(): string {
return i18n.baseText('generic.copiedToClipboard');
},
},
toastMessage: {
type: String,
},
collapse: {
type: Boolean,
default: false,
},
size: {
type: String,
default: 'large',
},
redactValue: {
type: Boolean,
default: false,
},
},
setup() {
const clipboard = useClipboard();
return {
clipboard,
...useToast(),
}; };
},
methods: {
copy(): void {
this.$emit('copy');
void this.clipboard.copy(this.value ?? '');
this.showMessage({ const props = withDefaults(defineProps<Props>(), {
title: this.toastTitle, value: '',
message: this.toastMessage, placeholder: '',
label: '',
hint: '',
size: 'medium',
copyButtonText: useI18n().baseText('generic.copy'),
toastTitle: useI18n().baseText('generic.copiedToClipboard'),
});
const emit = defineEmits<{
(event: 'copy'): void;
}>();
const clipboard = useClipboard();
const { showMessage } = useToast();
function copy() {
emit('copy');
void clipboard.copy(props.value ?? '');
showMessage({
title: props.toastTitle,
message: props.toastMessage,
type: 'success', type: 'success',
}); });
}, }
},
});
</script> </script>
<style lang="scss" module> <style lang="scss" module>

View File

@@ -144,7 +144,12 @@ import { defineComponent } from 'vue';
import type { PropType } from 'vue'; import type { PropType } from 'vue';
import { mapStores } from 'pinia'; import { mapStores } from 'pinia';
import type { ICredentialType, INodeProperties, INodeTypeDescription } from 'n8n-workflow'; import type {
ICredentialDataDecryptedObject,
ICredentialType,
INodeProperties,
INodeTypeDescription,
} from 'n8n-workflow';
import { getAppNameFromCredType, isCommunityPackageName } from '@/utils/nodeTypesUtils'; import { getAppNameFromCredType, isCommunityPackageName } from '@/utils/nodeTypesUtils';
import Banner from '../Banner.vue'; import Banner from '../Banner.vue';
@@ -161,7 +166,7 @@ import { useRootStore } from '@/stores/n8nRoot.store';
import { useNDVStore } from '@/stores/ndv.store'; import { useNDVStore } from '@/stores/ndv.store';
import { useCredentialsStore } from '@/stores/credentials.store'; import { useCredentialsStore } from '@/stores/credentials.store';
import { useNodeTypesStore } from '@/stores/nodeTypes.store'; import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import type { ICredentialsResponse } from '@/Interface'; import type { ICredentialsResponse, IUpdateInformation } from '@/Interface';
import AuthTypeSelector from '@/components/CredentialEdit/AuthTypeSelector.vue'; import AuthTypeSelector from '@/components/CredentialEdit/AuthTypeSelector.vue';
import GoogleAuthButton from './GoogleAuthButton.vue'; import GoogleAuthButton from './GoogleAuthButton.vue';
import EnterpriseEdition from '@/components/EnterpriseEdition.ee.vue'; import EnterpriseEdition from '@/components/EnterpriseEdition.ee.vue';
@@ -190,7 +195,10 @@ export default defineComponent({
type: Array as PropType<string[]>, type: Array as PropType<string[]>,
default: () => [], default: () => [],
}, },
credentialData: {}, credentialData: {
type: Object as PropType<ICredentialDataDecryptedObject>,
required: true,
},
credentialId: { credentialId: {
type: String, type: String,
default: '', default: '',
@@ -351,7 +359,7 @@ export default defineComponent({
getCredentialOptions(type: string): ICredentialsResponse[] { getCredentialOptions(type: string): ICredentialsResponse[] {
return this.credentialsStore.allUsableCredentialsByType[type]; return this.credentialsStore.allUsableCredentialsByType[type];
}, },
onDataChange(event: { name: string; value: string | number | boolean | Date | null }): void { onDataChange(event: IUpdateInformation): void {
this.$emit('update', event); this.$emit('update', event);
}, },
onDocumentationUrlClick(): void { onDocumentationUrlClick(): void {

View File

@@ -12,10 +12,10 @@
<ParameterInputExpanded <ParameterInputExpanded
v-else v-else
:parameter="parameter" :parameter="parameter"
:value="credentialData[parameter.name]" :value="credentialDataValues[parameter.name]"
:documentation-url="documentationUrl" :documentation-url="documentationUrl"
:show-validation-warnings="showValidationWarnings" :show-validation-warnings="showValidationWarnings"
:label="label" :label="{ size: 'medium' }"
event-source="credentials" event-source="credentials"
@update="valueChanged" @update="valueChanged"
/> />
@@ -23,41 +23,41 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from 'vue'; import type {
import type { IParameterLabel } from 'n8n-workflow'; ICredentialDataDecryptedObject,
INodeProperties,
NodeParameterValueType,
} from 'n8n-workflow';
import type { IUpdateInformation } from '@/Interface'; import type { IUpdateInformation } from '@/Interface';
import ParameterInputExpanded from '../ParameterInputExpanded.vue'; import ParameterInputExpanded from '../ParameterInputExpanded.vue';
import { computed } from 'vue';
export default defineComponent({ type Props = {
name: 'CredentialsInput', credentialProperties: INodeProperties[];
components: { credentialData: ICredentialDataDecryptedObject;
ParameterInputExpanded, documentationUrl: string;
}, showValidationWarnings?: boolean;
props: [
'credentialProperties',
'credentialData', // ICredentialsDecryptedResponse
'documentationUrl',
'showValidationWarnings',
],
data(): { label: IParameterLabel } {
return {
label: {
size: 'medium',
},
}; };
},
methods: {
valueChanged(parameterData: IUpdateInformation) {
const name = parameterData.name.split('.').pop();
this.$emit('update', { const props = defineProps<Props>();
const credentialDataValues = computed(
() => props.credentialData as Record<string, NodeParameterValueType>,
);
const emit = defineEmits<{
(event: 'update', value: IUpdateInformation): void;
}>();
function valueChanged(parameterData: IUpdateInformation) {
const name = parameterData.name.split('.').pop() ?? parameterData.name;
emit('update', {
name, name,
value: parameterData.value, value: parameterData.value,
}); });
}, }
},
});
</script> </script>
<style lang="scss" module> <style lang="scss" module>

View File

@@ -5,23 +5,24 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from 'vue'; import { computed } from 'vue';
export default defineComponent({ type Props = {
name: 'ExpandableInputBase', modelValue: string;
props: ['modelValue', 'placeholder', 'staticSize'], placeholder?: string;
computed: { staticSize?: boolean;
hiddenValue() { };
let value = (this.modelValue as string).replace(/\s/g, '.'); // force input to expand on space chars
const props = withDefaults(defineProps<Props>(), { staticSize: false, placeholder: '' });
const hiddenValue = computed(() => {
let value = props.modelValue.replace(/\s/g, '.'); // force input to expand on space chars
if (!value) { if (!value) {
// @ts-ignore value = props.placeholder;
value = this.placeholder;
} }
return `${value}`; // adjust for padding return `${value}`; // adjust for padding
},
},
}); });
</script> </script>

View File

@@ -1,7 +1,7 @@
<template> <template>
<ExpandableInputBase :model-value="modelValue" :placeholder="placeholder"> <ExpandableInputBase :model-value="modelValue" :placeholder="placeholder">
<input <input
ref="input" ref="inputRef"
v-on-click-outside="onClickOutside" v-on-click-outside="onClickOutside"
class="el-input__inner" class="el-input__inner"
:value="modelValue" :value="modelValue"
@@ -15,58 +15,66 @@
</ExpandableInputBase> </ExpandableInputBase>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from 'vue';
import ExpandableInputBase from './ExpandableInputBase.vue';
import type { PropType } from 'vue';
import type { EventBus } from 'n8n-design-system'; import type { EventBus } from 'n8n-design-system';
import { onBeforeUnmount, onMounted, ref } from 'vue';
import ExpandableInputBase from './ExpandableInputBase.vue';
export default defineComponent({ type Props = {
name: 'ExpandableInputEdit', modelValue: string;
components: { ExpandableInputBase }, placeholder: string;
props: { maxlength?: number;
modelValue: { autofocus?: boolean;
type: String, eventBus?: EventBus;
required: true, };
},
placeholder: { type: String, required: true }, const props = defineProps<Props>();
maxlength: { type: Number }, const emit = defineEmits<{
autofocus: { type: Boolean }, (event: 'update:model-value', value: string): void;
eventBus: { (event: 'enter', value: string): void;
type: Object as PropType<EventBus>, (event: 'blur', value: string): void;
}, (event: 'esc'): void;
}, }>();
emits: ['update:modelValue', 'enter', 'blur', 'esc'],
mounted() { const inputRef = ref<HTMLInputElement>();
onMounted(() => {
// autofocus on input element is not reliable // autofocus on input element is not reliable
if (this.autofocus && this.$refs.input) { if (props.autofocus && inputRef.value) {
this.focus(); focus();
} }
this.eventBus?.on('focus', this.focus); props.eventBus?.on('focus', focus);
},
beforeUnmount() {
this.eventBus?.off('focus', this.focus);
},
methods: {
focus() {
if (this.$refs.input) {
(this.$refs.input as HTMLInputElement).focus();
}
},
onInput() {
this.$emit('update:modelValue', (this.$refs.input as HTMLInputElement).value);
},
onEnter() {
this.$emit('enter', (this.$refs.input as HTMLInputElement).value);
},
onClickOutside(e: Event) {
if (e.type === 'click') {
this.$emit('blur', (this.$refs.input as HTMLInputElement).value);
}
},
onEscape() {
this.$emit('esc');
},
},
}); });
onBeforeUnmount(() => {
props.eventBus?.off('focus', focus);
});
function focus() {
if (inputRef.value) {
inputRef.value.focus();
}
}
function onInput() {
if (inputRef.value) {
emit('update:model-value', inputRef.value.value);
}
}
function onEnter() {
if (inputRef.value) {
emit('enter', inputRef.value.value);
}
}
function onClickOutside(e: Event) {
if (e.type === 'click' && inputRef.value) {
emit('blur', inputRef.value.value);
}
}
function onEscape() {
emit('esc');
}
</script> </script>

View File

@@ -9,15 +9,14 @@
</ExpandableInputBase> </ExpandableInputBase>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from 'vue';
import ExpandableInputBase from './ExpandableInputBase.vue'; import ExpandableInputBase from './ExpandableInputBase.vue';
export default defineComponent({ type Props = {
name: 'ExpandableInputPreview', modelValue: string;
components: { ExpandableInputBase }, };
props: ['modelValue'],
}); defineProps<Props>();
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -105,13 +105,13 @@ const rightParameter = computed<INodeProperties>(() => {
const debouncedEmitUpdate = debounce(() => emit('update', condition.value), { debounceTime: 500 }); const debouncedEmitUpdate = debounce(() => emit('update', condition.value), { debounceTime: 500 });
const onLeftValueChange = (update: IUpdateInformation<NodeParameterValue>): void => { const onLeftValueChange = (update: IUpdateInformation): void => {
condition.value.leftValue = update.value; condition.value.leftValue = update.value as NodeParameterValue;
debouncedEmitUpdate(); debouncedEmitUpdate();
}; };
const onRightValueChange = (update: IUpdateInformation<NodeParameterValue>): void => { const onRightValueChange = (update: IUpdateInformation): void => {
condition.value.rightValue = update.value; condition.value.rightValue = update.value as NodeParameterValue;
debouncedEmitUpdate(); debouncedEmitUpdate();
}; };

View File

@@ -54,124 +54,114 @@
</n8n-input-label> </n8n-input-label>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import type { IUpdateInformation } from '@/Interface'; import type { IUpdateInformation } from '@/Interface';
import ParameterOptions from './ParameterOptions.vue'; import { useI18n } from '@/composables/useI18n';
import { defineComponent } from 'vue'; import { useTelemetry } from '@/composables/useTelemetry';
import type { PropType } from 'vue'; import { useWorkflowsStore } from '@/stores/workflows.store';
import ParameterInputWrapper from './ParameterInputWrapper.vue'; import { isValueExpression as isValueExpressionUtil } from '@/utils/nodeTypesUtils';
import { isValueExpression } from '@/utils/nodeTypesUtils'; import { createEventBus } from 'n8n-design-system/utils';
import type { import type {
INodeParameterResourceLocator, INodeParameterResourceLocator,
INodeProperties, INodeProperties,
IParameterLabel, IParameterLabel,
NodeParameterValueType, NodeParameterValueType,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { mapStores } from 'pinia'; import { computed, ref } from 'vue';
import { useWorkflowsStore } from '@/stores/workflows.store'; import ParameterInputWrapper from './ParameterInputWrapper.vue';
import { createEventBus } from 'n8n-design-system/utils'; import ParameterOptions from './ParameterOptions.vue';
export default defineComponent({ type Props = {
name: 'ParameterInputExpanded', parameter: INodeProperties;
components: { value: NodeParameterValueType;
ParameterOptions, showValidationWarnings?: boolean;
ParameterInputWrapper, documentationUrl?: string;
}, eventSource?: string;
props: { label?: IParameterLabel;
parameter: {
type: Object as PropType<INodeProperties>,
required: true,
},
value: {
type: Object as PropType<NodeParameterValueType>,
},
showValidationWarnings: {
type: Boolean,
},
documentationUrl: {
type: String,
},
eventSource: {
type: String,
},
label: {
type: Object as PropType<IParameterLabel>,
default: () => ({
size: 'small',
}),
},
},
data() {
return {
focused: false,
blurredEver: false,
menuExpanded: false,
eventBus: createEventBus(),
}; };
},
computed: { const props = withDefaults(defineProps<Props>(), {
...mapStores(useWorkflowsStore), label: () => ({ size: 'small' }),
showRequiredErrors(): boolean { });
if (!this.parameter.required) { const emit = defineEmits<{
(event: 'update', value: IUpdateInformation): void;
}>();
const focused = ref(false);
const blurredEver = ref(false);
const menuExpanded = ref(false);
const eventBus = ref(createEventBus());
const workflowsStore = useWorkflowsStore();
const i18n = useI18n();
const telemetry = useTelemetry();
const showRequiredErrors = computed(() => {
if (!props.parameter.required) {
return false; return false;
} }
if (this.blurredEver || this.showValidationWarnings) { if (blurredEver.value || props.showValidationWarnings) {
if (this.parameter.type === 'string') { if (props.parameter.type === 'string') {
return !this.value; return !props.value;
} }
if (this.parameter.type === 'number') { if (props.parameter.type === 'number') {
if (typeof this.value === 'string' && this.value.startsWith('=')) { if (typeof props.value === 'string' && props.value.startsWith('=')) {
return false; return false;
} }
return typeof this.value !== 'number'; return typeof props.value !== 'number';
} }
} }
return false; return false;
}, });
hint(): string | null {
if (this.isValueExpression) { const hint = computed(() => {
if (isValueExpression.value) {
return null; return null;
} }
return this.$locale.credText().hint(this.parameter); return i18n.credText().hint(props.parameter);
}, });
isValueExpression(): boolean {
return isValueExpression( const isValueExpression = computed(() => {
this.parameter, return isValueExpressionUtil(
this.value as string | INodeParameterResourceLocator, props.parameter,
props.value as string | INodeParameterResourceLocator,
); );
}, });
},
methods: { function onFocus() {
onFocus() { focused.value = true;
this.focused = true; }
},
onBlur() { function onBlur() {
this.blurredEver = true; blurredEver.value = true;
this.focused = false; focused.value = false;
}, }
onMenuExpanded(expanded: boolean) {
this.menuExpanded = expanded; function onMenuExpanded(expanded: boolean) {
}, menuExpanded.value = expanded;
optionSelected(command: string) { }
this.eventBus.emit('optionSelected', command);
}, function optionSelected(command: string) {
valueChanged(parameterData: IUpdateInformation) { eventBus.value.emit('optionSelected', command);
this.$emit('update', parameterData); }
},
onDocumentationUrlClick(): void { function valueChanged(parameterData: IUpdateInformation) {
this.$telemetry.track('User clicked credential modal docs link', { emit('update', parameterData);
docs_link: this.documentationUrl, }
function onDocumentationUrlClick(): void {
telemetry.track('User clicked credential modal docs link', {
docs_link: props.documentationUrl,
source: 'field', source: 'field',
workflow_id: this.workflowsStore.workflowId, workflow_id: workflowsStore.workflowId,
});
},
},
}); });
}
</script> </script>
<style lang="scss" module> <style lang="scss" module>

View File

@@ -25,7 +25,7 @@
type="mapping" type="mapping"
:disabled="isDropDisabled" :disabled="isDropDisabled"
:sticky="true" :sticky="true"
:sticky-offset="isValueExpression ? [26, 3] : [3, 3]" :sticky-offset="isExpression ? [26, 3] : [3, 3]"
@drop="onDrop" @drop="onDrop"
> >
<template #default="{ droppable, activeDrop }"> <template #default="{ droppable, activeDrop }">
@@ -77,258 +77,192 @@
</n8n-input-label> </n8n-input-label>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from 'vue'; import { computed, ref } from 'vue';
import type { PropType } from 'vue';
import { mapStores } from 'pinia';
import type { INodeUi, IRunDataDisplayMode, IUpdateInformation } from '@/Interface'; import type { IUpdateInformation } from '@/Interface';
import ParameterOptions from '@/components/ParameterOptions.vue';
import DraggableTarget from '@/components/DraggableTarget.vue'; import DraggableTarget from '@/components/DraggableTarget.vue';
import ParameterInputWrapper from '@/components/ParameterInputWrapper.vue';
import ParameterOptions from '@/components/ParameterOptions.vue';
import { useI18n } from '@/composables/useI18n'; import { useI18n } from '@/composables/useI18n';
import { useToast } from '@/composables/useToast'; import { useToast } from '@/composables/useToast';
import { hasExpressionMapping, hasOnlyListMode, isValueExpression } from '@/utils/nodeTypesUtils';
import { isResourceLocatorValue } from '@/utils/typeGuards';
import ParameterInputWrapper from '@/components/ParameterInputWrapper.vue';
import type {
INodeProperties,
INodePropertyMode,
IParameterLabel,
NodeParameterValueType,
} from 'n8n-workflow';
import { useNDVStore } from '@/stores/ndv.store'; import { useNDVStore } from '@/stores/ndv.store';
import { useSegment } from '@/stores/segment.store'; import { useSegment } from '@/stores/segment.store';
import { getMappedResult } from '@/utils/mappingUtils'; import { getMappedResult } from '@/utils/mappingUtils';
import { hasExpressionMapping, hasOnlyListMode, isValueExpression } from '@/utils/nodeTypesUtils';
import { isResourceLocatorValue } from '@/utils/typeGuards';
import { createEventBus } from 'n8n-design-system/utils'; import { createEventBus } from 'n8n-design-system/utils';
import type { INodeProperties, IParameterLabel, NodeParameterValueType } from 'n8n-workflow';
import InlineExpressionTip from './InlineExpressionEditor/InlineExpressionTip.vue'; import InlineExpressionTip from './InlineExpressionEditor/InlineExpressionTip.vue';
export default defineComponent({ type Props = {
name: 'ParameterInputFull', parameter: INodeProperties;
components: { path: string;
ParameterOptions, value: NodeParameterValueType;
DraggableTarget, label?: IParameterLabel;
ParameterInputWrapper, displayOptions?: boolean;
InlineExpressionTip, optionsPosition?: 'bottom' | 'top';
}, hideHint?: boolean;
props: { isReadOnly?: boolean;
displayOptions: { rows?: number;
type: Boolean, isAssignment?: boolean;
default: false, hideLabel?: boolean;
}, hideIssues?: boolean;
optionsPosition: { entryIndex?: number;
type: String as PropType<'bottom' | 'top'>, };
default: 'top',
},
hideHint: {
type: Boolean,
default: false,
},
isReadOnly: {
type: Boolean,
default: false,
},
rows: {
type: Number,
default: 5,
},
isAssignment: {
type: Boolean,
default: false,
},
hideLabel: {
type: Boolean,
default: false,
},
hideIssues: {
type: Boolean,
default: false,
},
parameter: {
type: Object as PropType<INodeProperties>,
required: true,
},
path: {
type: String,
required: true,
},
value: {
type: [Number, String, Boolean, Array, Object] as PropType<NodeParameterValueType>,
},
label: {
type: Object as PropType<IParameterLabel>,
default: () => ({
size: 'small',
}),
},
entryIndex: {
type: Number,
default: undefined,
},
},
setup() {
const eventBus = createEventBus();
const i18n = useI18n();
return { const props = withDefaults(defineProps<Props>(), {
i18n, optionsPosition: 'top',
eventBus, hideHint: false,
...useToast(), isReadOnly: false,
}; rows: 5,
}, hideLabel: false,
data() { hideIssues: false,
return { label: () => ({ size: 'small' }),
focused: false, });
menuExpanded: false, const emit = defineEmits<{
forceShowExpression: false, (event: 'blur'): void;
}; (event: 'update', value: IUpdateInformation): void;
}, }>();
computed: {
...mapStores(useNDVStore), const i18n = useI18n();
node(): INodeUi | null { const toast = useToast();
return this.ndvStore.activeNode;
}, const eventBus = ref(createEventBus());
hint(): string { const focused = ref(false);
return this.i18n.nodeText().hint(this.parameter, this.path); const menuExpanded = ref(false);
}, const forceShowExpression = ref(false);
isInputTypeString(): boolean {
return this.parameter.type === 'string'; const ndvStore = useNDVStore();
},
isInputTypeNumber(): boolean { const node = computed(() => ndvStore.activeNode);
return this.parameter.type === 'number'; const hint = computed(() => i18n.nodeText().hint(props.parameter, props.path));
}, const isInputTypeString = computed(() => props.parameter.type === 'string');
isResourceLocator(): boolean { const isInputTypeNumber = computed(() => props.parameter.type === 'number');
return this.parameter.type === 'resourceLocator'; const isResourceLocator = computed(() => props.parameter.type === 'resourceLocator');
}, const isDropDisabled = computed(
isDropDisabled(): boolean { () => props.parameter.noDataExpression || props.isReadOnly || isResourceLocator.value,
return this.parameter.noDataExpression || this.isReadOnly || this.isResourceLocator;
},
isValueExpression(): boolean {
return isValueExpression(this.parameter, this.value);
},
showExpressionSelector(): boolean {
return this.isResourceLocator ? !hasOnlyListMode(this.parameter) : true;
},
isInputDataEmpty(): boolean {
return this.ndvStore.isNDVDataEmpty('input');
},
displayMode(): IRunDataDisplayMode {
return this.ndvStore.inputPanelDisplayMode;
},
showDragnDropTip(): boolean {
return (
this.focused &&
(this.isInputTypeString || this.isInputTypeNumber) &&
!this.isValueExpression &&
!this.isDropDisabled &&
(!this.ndvStore.hasInputData || !this.isInputDataEmpty) &&
!this.ndvStore.isMappingOnboarded &&
this.ndvStore.isInputParentOfActiveNode
); );
}, const isExpression = computed(() => isValueExpression(props.parameter, props.value));
}, const showExpressionSelector = computed(() =>
methods: { isResourceLocator.value ? !hasOnlyListMode(props.parameter) : true,
onFocus() { );
this.focused = true; const isInputDataEmpty = computed(() => ndvStore.isNDVDataEmpty('input'));
if (!this.parameter.noDataExpression) { const showDragnDropTip = computed(
this.ndvStore.setMappableNDVInputFocus(this.parameter.displayName); () =>
focused.value &&
(isInputTypeString.value || isInputTypeNumber.value) &&
!isExpression.value &&
!isDropDisabled.value &&
(!ndvStore.hasInputData || !isInputDataEmpty.value) &&
!ndvStore.isMappingOnboarded &&
ndvStore.isInputParentOfActiveNode,
);
function onFocus() {
focused.value = true;
if (!props.parameter.noDataExpression) {
ndvStore.setMappableNDVInputFocus(props.parameter.displayName);
} }
this.ndvStore.setFocusedInputPath(this.path ?? ''); ndvStore.setFocusedInputPath(props.path ?? '');
}, }
onBlur() {
this.focused = false; function onBlur() {
focused.value = false;
if ( if (
!this.parameter.noDataExpression && !props.parameter.noDataExpression &&
this.ndvStore.focusedMappableInput === this.parameter.displayName ndvStore.focusedMappableInput === props.parameter.displayName
) { ) {
this.ndvStore.setMappableNDVInputFocus(''); ndvStore.setMappableNDVInputFocus('');
} }
this.ndvStore.setFocusedInputPath(''); ndvStore.setFocusedInputPath('');
this.$emit('blur'); emit('blur');
},
onMenuExpanded(expanded: boolean) {
this.menuExpanded = expanded;
},
optionSelected(command: string) {
this.eventBus.emit('optionSelected', command);
},
valueChanged(parameterData: IUpdateInformation) {
this.$emit('update', parameterData);
},
onTextInput(parameterData: IUpdateInformation) {
if (isValueExpression(this.parameter, parameterData.value)) {
this.eventBus.emit('optionSelected', 'addExpression');
} }
},
onDrop(newParamValue: string) { function onMenuExpanded(expanded: boolean) {
const value = this.value; menuExpanded.value = expanded;
const updatedValue = getMappedResult(this.parameter, newParamValue, value); }
const prevValue =
this.isResourceLocator && isResourceLocatorValue(value) ? value.value : value; function optionSelected(command: string) {
eventBus.value.emit('optionSelected', command);
}
function valueChanged(parameterData: IUpdateInformation) {
emit('update', parameterData);
}
function onTextInput(parameterData: IUpdateInformation) {
if (isValueExpression(props.parameter, parameterData.value)) {
eventBus.value.emit('optionSelected', 'addExpression');
}
}
function onDrop(newParamValue: string) {
const value = props.value;
const updatedValue = getMappedResult(props.parameter, newParamValue, value);
const prevValue = isResourceLocator.value && isResourceLocatorValue(value) ? value.value : value;
if (updatedValue.startsWith('=')) { if (updatedValue.startsWith('=')) {
this.forceShowExpression = true; forceShowExpression.value = true;
} }
setTimeout(() => { setTimeout(() => {
if (this.node) { if (node.value) {
let parameterData; let parameterData;
if (this.isResourceLocator) { if (isResourceLocator.value) {
if (!isResourceLocatorValue(this.value)) { if (!isResourceLocatorValue(props.value)) {
parameterData = { parameterData = {
node: this.node.name, node: node.value.name,
name: this.path, name: props.path,
value: { __rl: true, value: updatedValue, mode: '' }, value: { __rl: true, value: updatedValue, mode: '' },
}; };
} else if ( } else if (
this.value.mode === 'list' && props.value.mode === 'list' &&
this.parameter.modes && props.parameter.modes &&
this.parameter.modes.length > 1 props.parameter.modes.length > 1
) { ) {
let mode = let mode = props.parameter.modes.find((m) => m.name === 'id') ?? null;
this.parameter.modes.find((mode: INodePropertyMode) => mode.name === 'id') || null;
if (!mode) { if (!mode) {
mode = this.parameter.modes.filter( mode = props.parameter.modes.filter((m) => m.name !== 'list')[0];
(mode: INodePropertyMode) => mode.name !== 'list',
)[0];
} }
parameterData = { parameterData = {
node: this.node.name, node: node.value.name,
name: this.path, name: props.path,
value: { __rl: true, value: updatedValue, mode: mode ? mode.name : '' }, value: { __rl: true, value: updatedValue, mode: mode ? mode.name : '' },
}; };
} else { } else {
parameterData = { parameterData = {
node: this.node.name, node: node.value.name,
name: this.path, name: props.path,
value: { __rl: true, value: updatedValue, mode: this.value.mode }, value: { __rl: true, value: updatedValue, mode: props.value?.mode },
}; };
} }
} else { } else {
parameterData = { parameterData = {
node: this.node.name, node: node.value.name,
name: this.path, name: props.path,
value: updatedValue, value: updatedValue,
}; };
} }
this.valueChanged(parameterData); valueChanged(parameterData);
this.eventBus.emit('drop', updatedValue); eventBus.value.emit('drop', updatedValue);
if (!this.ndvStore.isMappingOnboarded) { if (!ndvStore.isMappingOnboarded) {
this.showMessage({ toast.showMessage({
title: this.i18n.baseText('dataMapping.success.title'), title: i18n.baseText('dataMapping.success.title'),
message: this.i18n.baseText('dataMapping.success.moreInfo'), message: i18n.baseText('dataMapping.success.moreInfo'),
type: 'success', type: 'success',
dangerouslyUseHTMLString: true, dangerouslyUseHTMLString: true,
}); });
this.ndvStore.setMappingOnboarded(); ndvStore.setMappingOnboarded();
} }
this.ndvStore.setMappingTelemetry({ ndvStore.setMappingTelemetry({
dest_node_type: this.node.type, dest_node_type: node.value.type,
dest_parameter: this.path, dest_parameter: props.path,
dest_parameter_mode: dest_parameter_mode:
typeof prevValue === 'string' && prevValue.startsWith('=') ? 'expression' : 'fixed', typeof prevValue === 'string' && prevValue.startsWith('=') ? 'expression' : 'fixed',
dest_parameter_empty: prevValue === '' || prevValue === undefined, dest_parameter_empty: prevValue === '' || prevValue === undefined,
@@ -342,11 +276,9 @@ export default defineComponent({
const segment = useSegment(); const segment = useSegment();
segment.track(segment.EVENTS.MAPPED_DATA); segment.track(segment.EVENTS.MAPPED_DATA);
} }
this.forceShowExpression = false; forceShowExpression.value = false;
}, 200); }, 200);
}, }
},
});
</script> </script>
<style lang="scss" module> <style lang="scss" module>

View File

@@ -1,46 +1,51 @@
<template> <template>
<n8n-text v-if="hint" size="small" color="text-base" tag="div"> <n8n-text v-if="hint" size="small" color="text-base" tag="div">
<div v-if="!renderHTML" :class="classes"><span v-html="simplyText"></span></div> <div
v-if="!renderHTML"
:class="{
[$style.singleline]: singleLine,
[$style.highlight]: highlight,
}"
>
<span v-html="simplyText"></span>
</div>
<div <div
v-else v-else
ref="hint" ref="hintTextRef"
:class="{ [$style.singleline]: singleLine, [$style.highlight]: highlight }" :class="{ [$style.singleline]: singleLine, [$style.highlight]: highlight }"
v-html="sanitizeHtml(hint)" v-html="sanitizeHtml(hint)"
></div> ></div>
</n8n-text> </n8n-text>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from 'vue';
import { sanitizeHtml } from '@/utils/htmlUtils'; import { sanitizeHtml } from '@/utils/htmlUtils';
import { computed, onMounted, ref } from 'vue';
export default defineComponent({ type Props = {
name: 'InputHint', hint: string;
props: { highlight?: boolean;
hint: { singleLine?: boolean;
type: String, renderHTML?: boolean;
},
highlight: {
type: Boolean,
},
singleLine: {
type: Boolean,
},
renderHTML: {
type: Boolean,
default: false,
},
},
computed: {
classes() {
return {
[this.$style.singleline]: this.singleLine,
[this.$style.highlight]: this.highlight,
}; };
},
simplyText(): string { const hintTextRef = ref<HTMLDivElement>();
if (this.hint) {
return String(this.hint) const props = withDefaults(defineProps<Props>(), {
highlight: false,
singleLine: false,
renderHTML: false,
});
onMounted(() => {
if (hintTextRef.value) {
hintTextRef.value.querySelectorAll('a').forEach((a) => (a.target = '_blank'));
}
});
const simplyText = computed(() => {
if (props.hint) {
return String(props.hint)
.replace(/&/g, '&amp;') // allows us to keep spaces at the beginning of an expression .replace(/&/g, '&amp;') // allows us to keep spaces at the beginning of an expression
.replace(/</g, '&lt;') // prevent XSS exploits since we are rendering HTML .replace(/</g, '&lt;') // prevent XSS exploits since we are rendering HTML
.replace(/>/g, '&gt;') .replace(/>/g, '&gt;')
@@ -49,16 +54,6 @@ export default defineComponent({
} }
return ''; return '';
},
},
mounted() {
if (this.$refs.hint) {
(this.$refs.hint as Element).querySelectorAll('a').forEach((a) => (a.target = '_blank'));
}
},
methods: {
sanitizeHtml,
},
}); });
</script> </script>

View File

@@ -161,27 +161,26 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import type { import type {
INodeParameters, INodeParameters,
INodeProperties, INodeProperties,
INodeTypeDescription,
NodeParameterValue, NodeParameterValue,
NodeParameterValueType, NodeParameterValueType,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { deepCopy } from 'n8n-workflow'; import { deepCopy } from 'n8n-workflow';
import { mapStores } from 'pinia'; import { computed, defineAsyncComponent, onErrorCaptured, ref, watch } from 'vue';
import type { PropType } from 'vue';
import { defineAsyncComponent, defineComponent, onErrorCaptured, ref } from 'vue';
import type { INodeUi, IUpdateInformation } from '@/Interface'; import type { IUpdateInformation } from '@/Interface';
import AssignmentCollection from '@/components/AssignmentCollection/AssignmentCollection.vue';
import FilterConditions from '@/components/FilterConditions/FilterConditions.vue';
import ImportCurlParameter from '@/components/ImportCurlParameter.vue'; import ImportCurlParameter from '@/components/ImportCurlParameter.vue';
import MultipleParameter from '@/components/MultipleParameter.vue'; import MultipleParameter from '@/components/MultipleParameter.vue';
import ParameterInputFull from '@/components/ParameterInputFull.vue'; import ParameterInputFull from '@/components/ParameterInputFull.vue';
import ResourceMapper from '@/components/ResourceMapper/ResourceMapper.vue'; import ResourceMapper from '@/components/ResourceMapper/ResourceMapper.vue';
import FilterConditions from '@/components/FilterConditions/FilterConditions.vue'; import { useNodeHelpers } from '@/composables/useNodeHelpers';
import AssignmentCollection from '@/components/AssignmentCollection/AssignmentCollection.vue'; import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
import { KEEP_AUTH_IN_NDV_FOR_NODES } from '@/constants'; import { KEEP_AUTH_IN_NDV_FOR_NODES } from '@/constants';
import { useNDVStore } from '@/stores/ndv.store'; import { useNDVStore } from '@/stores/ndv.store';
import { useNodeTypesStore } from '@/stores/nodeTypes.store'; import { useNodeTypesStore } from '@/stores/nodeTypes.store';
@@ -191,9 +190,7 @@ import {
isAuthRelatedParameter, isAuthRelatedParameter,
} from '@/utils/nodeTypesUtils'; } from '@/utils/nodeTypesUtils';
import { get, set } from 'lodash-es'; import { get, set } from 'lodash-es';
import { useNodeHelpers } from '@/composables/useNodeHelpers';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
const FixedCollectionParameter = defineAsyncComponent( const FixedCollectionParameter = defineAsyncComponent(
async () => await import('./FixedCollectionParameter.vue'), async () => await import('./FixedCollectionParameter.vue'),
@@ -202,59 +199,32 @@ const CollectionParameter = defineAsyncComponent(
async () => await import('./CollectionParameter.vue'), async () => await import('./CollectionParameter.vue'),
); );
export default defineComponent({ type Props = {
name: 'ParameterInputList', nodeValues: INodeParameters;
components: { parameters: INodeProperties[];
MultipleParameter, path?: string;
ParameterInputFull, hideDelete?: boolean;
FixedCollectionParameter, indent?: boolean;
CollectionParameter, isReadOnly?: boolean;
ImportCurlParameter, hiddenIssuesInputs?: string[];
ResourceMapper, entryIndex?: number;
FilterConditions, };
AssignmentCollection,
}, const props = withDefaults(defineProps<Props>(), { path: '', hiddenIssuesInputs: () => [] });
props: { const emit = defineEmits<{
nodeValues: { (event: 'activate'): void;
type: Object as PropType<INodeParameters>, (event: 'valueChanged', value: IUpdateInformation): void;
required: true, (event: 'parameterBlur', value: string): void;
}, }>();
parameters: {
type: Array as PropType<INodeProperties[]>, const nodeTypesStore = useNodeTypesStore();
required: true, const ndvStore = useNDVStore();
},
path: {
type: String,
default: '',
},
hideDelete: {
type: Boolean,
default: false,
},
indent: {
type: Boolean,
default: false,
},
isReadOnly: {
type: Boolean,
default: false,
},
hiddenIssuesInputs: {
type: Array as PropType<string[]>,
default: () => [],
},
entryIndex: {
type: Number,
default: undefined,
},
},
setup() {
const nodeHelpers = useNodeHelpers(); const nodeHelpers = useNodeHelpers();
const asyncLoadingError = ref(false); const asyncLoadingError = ref(false);
const router = useRouter(); const router = useRouter();
const workflowHelpers = useWorkflowHelpers({ router }); const workflowHelpers = useWorkflowHelpers({ router });
// This will catch errors in async components
onErrorCaptured((e, component) => { onErrorCaptured((e, component) => {
if ( if (
!['FixedCollectionParameter', 'CollectionParameter'].includes( !['FixedCollectionParameter', 'CollectionParameter'].includes(
@@ -274,76 +244,56 @@ export default defineComponent({
return false; return false;
}); });
return { const nodeType = computed(() => {
nodeHelpers, if (node.value) {
asyncLoadingError, return nodeTypesStore.getNodeType(node.value.type, node.value.typeVersion);
workflowHelpers,
};
},
computed: {
...mapStores(useNodeTypesStore, useNDVStore),
nodeTypeVersion(): number | null {
if (this.node) {
return this.node.typeVersion;
} }
return null; return null;
}, });
nodeTypeName(): string {
if (this.node) {
return this.node.type;
}
return '';
},
nodeType(): INodeTypeDescription | null {
if (this.node) {
return this.nodeTypesStore.getNodeType(this.node.type, this.node.typeVersion);
}
return null;
},
filteredParameters(): INodeProperties[] {
return this.parameters.filter((parameter: INodeProperties) =>
this.displayNodeParameter(parameter),
);
},
filteredParameterNames(): string[] {
return this.filteredParameters.map((parameter) => parameter.name);
},
node(): INodeUi | null {
return this.ndvStore.activeNode;
},
nodeAuthFields(): INodeProperties[] {
return getNodeAuthFields(this.nodeType);
},
credentialsParameterIndex(): number {
return this.filteredParameters.findIndex((parameter) => parameter.type === 'credentials');
},
indexToShowSlotAt(): number {
const credentialsParameterIndex = this.credentialsParameterIndex;
if (credentialsParameterIndex !== -1) { const filteredParameters = computed(() => {
return credentialsParameterIndex; return props.parameters.filter((parameter: INodeProperties) => displayNodeParameter(parameter));
});
const filteredParameterNames = computed(() => {
return filteredParameters.value.map((parameter) => parameter.name);
});
const node = computed(() => ndvStore.activeNode);
const nodeAuthFields = computed(() => {
return getNodeAuthFields(nodeType.value);
});
const credentialsParameterIndex = computed(() => {
return filteredParameters.value.findIndex((parameter) => parameter.type === 'credentials');
});
const indexToShowSlotAt = computed(() => {
if (credentialsParameterIndex.value !== -1) {
return credentialsParameterIndex.value;
} }
let index = 0; let index = 0;
// For nodes that use old credentials UI, keep credentials below authentication field in NDV // For nodes that use old credentials UI, keep credentials below authentication field in NDV
// otherwise credentials will use auth filed position since the auth field is moved to credentials modal // otherwise credentials will use auth filed position since the auth field is moved to credentials modal
const fieldOffset = KEEP_AUTH_IN_NDV_FOR_NODES.includes(this.nodeType?.name || '') ? 1 : 0; const fieldOffset = KEEP_AUTH_IN_NDV_FOR_NODES.includes(nodeType.value?.name || '') ? 1 : 0;
const credentialsDependencies = this.getCredentialsDependencies(); const credentialsDependencies = getCredentialsDependencies();
this.filteredParameters.forEach((prop, propIndex) => { filteredParameters.value.forEach((prop, propIndex) => {
if (credentialsDependencies.has(prop.name)) { if (credentialsDependencies.has(prop.name)) {
index = propIndex + fieldOffset; index = propIndex + fieldOffset;
} }
}); });
return Math.min(index, this.filteredParameters.length - 1); return Math.min(index, filteredParameters.value.length - 1);
}, });
mainNodeAuthField(): INodeProperties | null {
return getMainAuthField(this.nodeType || null); const mainNodeAuthField = computed(() => {
}, return getMainAuthField(nodeType.value || null);
}, });
watch: {
filteredParameterNames(newValue, oldValue) { watch(filteredParameterNames, (newValue, oldValue) => {
if (newValue === undefined) { if (newValue === undefined) {
return; return;
} }
@@ -353,42 +303,38 @@ export default defineComponent({
for (const parameter of oldValue) { for (const parameter of oldValue) {
if (!newValue.includes(parameter)) { if (!newValue.includes(parameter)) {
const parameterData = { const parameterData = {
name: `${this.path}.${parameter}`, name: `${props.path}.${parameter}`,
node: this.ndvStore.activeNode?.name || '', node: ndvStore.activeNode?.name || '',
value: undefined, value: undefined,
}; };
this.$emit('valueChanged', parameterData); emit('valueChanged', parameterData);
} }
} }
}, });
},
methods: { function onParameterBlur(parameterName: string) {
onParameterBlur(parameterName: string) { emit('parameterBlur', parameterName);
this.$emit('parameterBlur', parameterName); }
},
getCredentialsDependencies() { function getCredentialsDependencies() {
const dependencies = new Set(); const dependencies = new Set();
const nodeType = this.nodeTypesStore.getNodeType(
this.node?.type || '',
this.node?.typeVersion,
);
// Get names of all fields that credentials rendering depends on (using displayOptions > show) // Get names of all fields that credentials rendering depends on (using displayOptions > show)
if (nodeType?.credentials) { if (nodeType.value?.credentials) {
for (const cred of nodeType.credentials) { for (const cred of nodeType.value.credentials) {
if (cred.displayOptions?.show) { if (cred.displayOptions?.show) {
Object.keys(cred.displayOptions.show).forEach((fieldName) => Object.keys(cred.displayOptions.show).forEach((fieldName) => dependencies.add(fieldName));
dependencies.add(fieldName),
);
} }
} }
} }
return dependencies; return dependencies;
}, }
multipleValues(parameter: INodeProperties): boolean {
return this.getArgument('multipleValues', parameter) === true; function multipleValues(parameter: INodeProperties): boolean {
}, return getArgument('multipleValues', parameter) === true;
getArgument( }
function getArgument(
argumentName: string, argumentName: string,
parameter: INodeProperties, parameter: INodeProperties,
): string | string[] | number | boolean | undefined { ): string | string[] | number | boolean | undefined {
@@ -401,22 +347,27 @@ export default defineComponent({
} }
return parameter.typeOptions[argumentName]; return parameter.typeOptions[argumentName];
}, }
getPath(parameterName: string): string {
return (this.path ? `${this.path}.` : '') + parameterName; function getPath(parameterName: string): string {
}, return (props.path ? `${props.path}.` : '') + parameterName;
deleteOption(optionName: string): void { }
function deleteOption(optionName: string): void {
const parameterData = { const parameterData = {
name: this.getPath(optionName), name: getPath(optionName),
value: undefined, value: undefined,
}; };
// TODO: If there is only one option it should delete the whole one // TODO: If there is only one option it should delete the whole one
this.$emit('valueChanged', parameterData); emit('valueChanged', parameterData);
}, }
mustHideDuringCustomApiCall(parameter: INodeProperties, nodeValues: INodeParameters): boolean { function mustHideDuringCustomApiCall(
parameter: INodeProperties,
nodeValues: INodeParameters,
): boolean {
if (parameter?.displayOptions?.hide) return true; if (parameter?.displayOptions?.hide) return true;
const MUST_REMAIN_VISIBLE = [ const MUST_REMAIN_VISIBLE = [
@@ -427,25 +378,25 @@ export default defineComponent({
]; ];
return !MUST_REMAIN_VISIBLE.includes(parameter.name); return !MUST_REMAIN_VISIBLE.includes(parameter.name);
}, }
displayNodeParameter(parameter: INodeProperties): boolean {
function displayNodeParameter(parameter: INodeProperties): boolean {
if (parameter.type === 'hidden') { if (parameter.type === 'hidden') {
return false; return false;
} }
if ( if (
this.nodeHelpers.isCustomApiCallSelected(this.nodeValues) && nodeHelpers.isCustomApiCallSelected(props.nodeValues) &&
this.mustHideDuringCustomApiCall(parameter, this.nodeValues) mustHideDuringCustomApiCall(parameter, props.nodeValues)
) { ) {
return false; return false;
} }
// Hide authentication related fields since it will now be part of credentials modal // Hide authentication related fields since it will now be part of credentials modal
if ( if (
!KEEP_AUTH_IN_NDV_FOR_NODES.includes(this.node?.type || '') && !KEEP_AUTH_IN_NDV_FOR_NODES.includes(node.value?.type || '') &&
this.mainNodeAuthField && mainNodeAuthField.value &&
(parameter.name === this.mainNodeAuthField?.name || (parameter.name === mainNodeAuthField.value?.name || shouldHideAuthRelatedParameter(parameter))
this.shouldHideAuthRelatedParameter(parameter))
) { ) {
return false; return false;
} }
@@ -456,9 +407,9 @@ export default defineComponent({
} }
const nodeValues: INodeParameters = {}; const nodeValues: INodeParameters = {};
let rawValues = this.nodeValues; let rawValues = props.nodeValues;
if (this.path) { if (props.path) {
rawValues = get(this.nodeValues, this.path) as INodeParameters; rawValues = get(props.nodeValues, props.path) as INodeParameters;
} }
if (!rawValues) { if (!rawValues) {
@@ -484,7 +435,7 @@ export default defineComponent({
} else { } else {
// Contains probably no expression with a missing parameter so resolve // Contains probably no expression with a missing parameter so resolve
try { try {
nodeValues[key] = this.workflowHelpers.resolveExpression( nodeValues[key] = workflowHelpers.resolveExpression(
value, value,
nodeValues, nodeValues,
) as NodeParameterValue; ) as NodeParameterValue;
@@ -506,51 +457,58 @@ export default defineComponent({
} while (resolveKeys.length !== 0); } while (resolveKeys.length !== 0);
if (parameterGotResolved) { if (parameterGotResolved) {
if (this.path) { if (props.path) {
rawValues = deepCopy(this.nodeValues); rawValues = deepCopy(props.nodeValues);
set(rawValues, this.path, nodeValues); set(rawValues, props.path, nodeValues);
return this.nodeHelpers.displayParameter(rawValues, parameter, this.path, this.node); return nodeHelpers.displayParameter(rawValues, parameter, props.path, node.value);
} else { } else {
return this.nodeHelpers.displayParameter(nodeValues, parameter, '', this.node); return nodeHelpers.displayParameter(nodeValues, parameter, '', node.value);
} }
} }
return this.nodeHelpers.displayParameter(this.nodeValues, parameter, this.path, this.node); return nodeHelpers.displayParameter(props.nodeValues, parameter, props.path, node.value);
},
valueChanged(parameterData: IUpdateInformation): void {
this.$emit('valueChanged', parameterData);
},
onNoticeAction(action: string) {
if (action === 'activate') {
this.$emit('activate');
} }
},
function valueChanged(parameterData: IUpdateInformation): void {
emit('valueChanged', parameterData);
}
function onNoticeAction(action: string) {
if (action === 'activate') {
emit('activate');
}
}
/** /**
* Handles default node button parameter type actions * Handles default node button parameter type actions
* @param parameter * @param parameter
*/ */
onButtonAction(parameter: INodeProperties) { function onButtonAction(parameter: INodeProperties) {
const action: string | undefined = parameter.typeOptions?.action; const action: string | undefined = parameter.typeOptions?.action;
switch (action) { switch (action) {
default: default:
return; return;
} }
}, }
isNodeAuthField(name: string): boolean {
return this.nodeAuthFields.find((field) => field.name === name) !== undefined; function isNodeAuthField(name: string): boolean {
}, return nodeAuthFields.value.find((field) => field.name === name) !== undefined;
shouldHideAuthRelatedParameter(parameter: INodeProperties): boolean { }
function shouldHideAuthRelatedParameter(parameter: INodeProperties): boolean {
// TODO: For now, hide all fields that are used in authentication fields displayOptions // TODO: For now, hide all fields that are used in authentication fields displayOptions
// Ideally, we should check if any non-auth field depends on it before hiding it but // Ideally, we should check if any non-auth field depends on it before hiding it but
// since there is no such case, omitting it to avoid additional computation // since there is no such case, omitting it to avoid additional computation
return isAuthRelatedParameter(this.nodeAuthFields, parameter); return isAuthRelatedParameter(nodeAuthFields.value, parameter);
}, }
shouldShowOptions(parameter: INodeProperties): boolean {
function shouldShowOptions(parameter: INodeProperties): boolean {
return parameter.type !== 'resourceMapper'; return parameter.type !== 'resourceMapper';
}, }
getDependentParametersValues(parameter: INodeProperties): string | null {
const loadOptionsDependsOn = this.getArgument('loadOptionsDependsOn', parameter) as function getDependentParametersValues(parameter: INodeProperties): string | null {
const loadOptionsDependsOn = getArgument('loadOptionsDependsOn', parameter) as
| string[] | string[]
| undefined; | undefined;
@@ -559,9 +517,9 @@ export default defineComponent({
} }
// Get the resolved parameter values of the current node // Get the resolved parameter values of the current node
const currentNodeParameters = this.ndvStore.activeNode?.parameters; const currentNodeParameters = ndvStore.activeNode?.parameters;
try { try {
const resolvedNodeParameters = this.workflowHelpers.resolveParameter(currentNodeParameters); const resolvedNodeParameters = workflowHelpers.resolveParameter(currentNodeParameters);
const returnValues: string[] = []; const returnValues: string[] = [];
for (const parameterPath of loadOptionsDependsOn) { for (const parameterPath of loadOptionsDependsOn) {
@@ -572,12 +530,13 @@ export default defineComponent({
} catch (error) { } catch (error) {
return null; return null;
} }
}, }
getParameterValue<T extends NodeParameterValueType = NodeParameterValueType>(name: string): T {
return this.nodeHelpers.getParameterValue(this.nodeValues, name, this.path) as T; function getParameterValue<T extends NodeParameterValueType = NodeParameterValueType>(
}, name: string,
}, ): T {
}); return nodeHelpers.getParameterValue(props.nodeValues, name, props.path) as T;
}
</script> </script>
<style lang="scss"> <style lang="scss">

View File

@@ -45,234 +45,194 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { mapStores } from 'pinia'; import type { IUpdateInformation, InputSize } from '@/Interface';
import type { PropType } from 'vue';
import { defineComponent } from 'vue';
import type { INodeUi, IUpdateInformation, InputSize, TargetItem } from '@/Interface';
import ParameterInput from '@/components/ParameterInput.vue'; import ParameterInput from '@/components/ParameterInput.vue';
import InputHint from '@/components/ParameterInputHint.vue'; import InputHint from '@/components/ParameterInputHint.vue';
import { useEnvironmentsStore } from '@/stores/environments.ee.store'; import {
isResourceLocatorValue,
type IDataObject,
type INodeProperties,
type INodePropertyMode,
type IParameterLabel,
type NodeParameterValueType,
type Result,
} from 'n8n-workflow';
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
import useEnvironmentsStore from '@/stores/environments.ee.store';
import { useExternalSecretsStore } from '@/stores/externalSecrets.ee.store'; import { useExternalSecretsStore } from '@/stores/externalSecrets.ee.store';
import { useNDVStore } from '@/stores/ndv.store'; import { useNDVStore } from '@/stores/ndv.store';
import { stringifyExpressionResult } from '@/utils/expressions';
import { isValueExpression, parseResourceMapperFieldName } from '@/utils/nodeTypesUtils'; import { isValueExpression, parseResourceMapperFieldName } from '@/utils/nodeTypesUtils';
import type {
IDataObject,
INodeProperties,
INodePropertyMode,
IParameterLabel,
NodeParameterValueType,
Result,
} from 'n8n-workflow';
import { isResourceLocatorValue } from 'n8n-workflow';
import type { EventBus } from 'n8n-design-system/utils'; import type { EventBus } from 'n8n-design-system/utils';
import { createEventBus } from 'n8n-design-system/utils'; import { createEventBus } from 'n8n-design-system/utils';
import { computed } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
import { stringifyExpressionResult } from '@/utils/expressions';
export default defineComponent({ type Props = {
name: 'ParameterInputWrapper', parameter: INodeProperties;
components: { path: string;
ParameterInput, modelValue: NodeParameterValueType;
InputHint, additionalExpressionData?: IDataObject;
}, rows?: number;
props: { isReadOnly?: boolean;
additionalExpressionData: { isAssignment?: boolean;
type: Object as PropType<IDataObject>, droppable?: boolean;
default: () => ({}), activeDrop?: boolean;
}, forceShowExpression?: boolean;
isReadOnly: { hint?: string;
type: Boolean, hideHint?: boolean;
}, inputSize?: InputSize;
rows: { hideIssues?: boolean;
type: Number, documentationUrl?: string;
default: 5, errorHighlight?: boolean;
}, isForCredential?: boolean;
isAssignment: { eventSource?: string;
type: Boolean, label?: IParameterLabel;
}, eventBus?: EventBus;
parameter: { };
type: Object as PropType<INodeProperties>,
required: true, const props = withDefaults(defineProps<Props>(), {
}, additionalExpressionData: () => ({}),
path: { rows: 5,
type: String, label: () => ({ size: 'small' }),
required: true, eventBus: () => createEventBus(),
}, });
modelValue: {
type: [String, Number, Boolean, Array, Object] as PropType<NodeParameterValueType>, const emit = defineEmits<{
}, (event: 'focus'): void;
droppable: { (event: 'blur'): void;
type: Boolean, (event: 'drop', value: string): void;
}, (event: 'update', value: IUpdateInformation): void;
activeDrop: { (event: 'textInput', value: IUpdateInformation): void;
type: Boolean, }>();
},
forceShowExpression: {
type: Boolean,
},
hint: {
type: String,
required: false,
},
hideHint: {
type: Boolean,
required: false,
},
inputSize: {
type: String as PropType<InputSize>,
},
hideIssues: {
type: Boolean,
},
documentationUrl: {
type: String as PropType<string | undefined>,
},
errorHighlight: {
type: Boolean,
},
isForCredential: {
type: Boolean,
},
eventSource: {
type: String,
},
label: {
type: Object as PropType<IParameterLabel>,
default: () => ({
size: 'small',
}),
},
eventBus: {
type: Object as PropType<EventBus>,
default: () => createEventBus(),
},
},
setup() {
const router = useRouter(); const router = useRouter();
const workflowHelpers = useWorkflowHelpers({ router }); const workflowHelpers = useWorkflowHelpers({ router });
return { const ndvStore = useNDVStore();
workflowHelpers, const externalSecretsStore = useExternalSecretsStore();
}; const environmentsStore = useEnvironmentsStore();
},
computed: { const isExpression = computed(() => {
...mapStores(useNDVStore, useExternalSecretsStore, useEnvironmentsStore), return isValueExpression(props.parameter, props.modelValue);
isValueExpression() { });
return isValueExpression(this.parameter, this.modelValue);
}, const activeNode = computed(() => ndvStore.activeNode);
activeNode(): INodeUi | null {
return this.ndvStore.activeNode; const selectedRLMode = computed(() => {
},
selectedRLMode(): INodePropertyMode | undefined {
if ( if (
typeof this.modelValue !== 'object' || typeof props.modelValue !== 'object' ||
this.parameter.type !== 'resourceLocator' || props.parameter.type !== 'resourceLocator' ||
!isResourceLocatorValue(this.modelValue) !isResourceLocatorValue(props.modelValue)
) { ) {
return undefined; return undefined;
} }
const mode = this.modelValue.mode; const mode = props.modelValue.mode;
if (mode) { if (mode) {
return this.parameter.modes?.find((m: INodePropertyMode) => m.name === mode); return props.parameter.modes?.find((m: INodePropertyMode) => m.name === mode);
} }
return undefined; return undefined;
}, });
parameterHint(): string | undefined {
if (this.isValueExpression) { const parameterHint = computed(() => {
if (isExpression.value) {
return undefined; return undefined;
} }
if (this.selectedRLMode?.hint) { if (selectedRLMode.value?.hint) {
return this.selectedRLMode.hint; return selectedRLMode.value.hint;
} }
return this.hint; return props.hint;
}, });
targetItem(): TargetItem | null {
return this.ndvStore.hoveringItem;
},
isInputParentOfActiveNode(): boolean {
return this.ndvStore.isInputParentOfActiveNode;
},
evaluatedExpression(): Result<unknown, Error> {
const value = isResourceLocatorValue(this.modelValue)
? this.modelValue.value
: this.modelValue;
if (!this.activeNode || !this.isValueExpression || typeof value !== 'string') { const targetItem = computed(() => ndvStore.hoveringItem);
const isInputParentOfActiveNode = computed(() => ndvStore.isInputParentOfActiveNode);
const evaluatedExpression = computed<Result<unknown, Error>>(() => {
const value = isResourceLocatorValue(props.modelValue)
? props.modelValue.value
: props.modelValue;
if (!activeNode.value || !isExpression.value || typeof value !== 'string') {
return { ok: false, error: new Error() }; return { ok: false, error: new Error() };
} }
try { try {
let opts: Parameters<typeof this.workflowHelpers.resolveExpression>[2] = { let opts: Parameters<typeof workflowHelpers.resolveExpression>[2] = {
isForCredential: this.isForCredential, isForCredential: props.isForCredential,
}; };
if (this.ndvStore.isInputParentOfActiveNode) { if (ndvStore.isInputParentOfActiveNode) {
opts = { opts = {
...opts, ...opts,
targetItem: this.targetItem ?? undefined, targetItem: targetItem.value ?? undefined,
inputNodeName: this.ndvStore.ndvInputNodeName, inputNodeName: ndvStore.ndvInputNodeName,
inputRunIndex: this.ndvStore.ndvInputRunIndex, inputRunIndex: ndvStore.ndvInputRunIndex,
inputBranchIndex: this.ndvStore.ndvInputBranchIndex, inputBranchIndex: ndvStore.ndvInputBranchIndex,
additionalKeys: this.resolvedAdditionalExpressionData, additionalKeys: resolvedAdditionalExpressionData.value,
}; };
} }
return { ok: true, result: this.workflowHelpers.resolveExpression(value, undefined, opts) }; return { ok: true, result: workflowHelpers.resolveExpression(value, undefined, opts) };
} catch (error) { } catch (error) {
return { ok: false, error }; return { ok: false, error };
} }
}, });
evaluatedExpressionValue(): unknown {
const evaluated = this.evaluatedExpression; const evaluatedExpressionValue = computed(() => {
const evaluated = evaluatedExpression.value;
return evaluated.ok ? evaluated.result : null; return evaluated.ok ? evaluated.result : null;
}, });
evaluatedExpressionString(): string | null {
return stringifyExpressionResult(this.evaluatedExpression); const evaluatedExpressionString = computed(() => {
}, return stringifyExpressionResult(evaluatedExpression.value);
expressionOutput(): string | null { });
if (this.isValueExpression && this.evaluatedExpressionString) {
return this.evaluatedExpressionString; const expressionOutput = computed(() => {
if (isExpression.value && evaluatedExpressionString.value) {
return evaluatedExpressionString.value;
} }
return null; return null;
},
resolvedAdditionalExpressionData() {
return {
$vars: this.environmentsStore.variablesAsObject,
...(this.externalSecretsStore.isEnterpriseExternalSecretsEnabled && this.isForCredential
? { $secrets: this.externalSecretsStore.secretsAsObject }
: {}),
...this.additionalExpressionData,
};
},
parsedParameterName() {
return parseResourceMapperFieldName(this.parameter?.name ?? '');
},
},
methods: {
onFocus() {
this.$emit('focus');
},
onBlur() {
this.$emit('blur');
},
onDrop(data: string) {
this.$emit('drop', data);
},
onValueChanged(parameterData: IUpdateInformation) {
this.$emit('update', parameterData);
},
onTextInput(parameterData: IUpdateInformation) {
this.$emit('textInput', parameterData);
},
},
}); });
const resolvedAdditionalExpressionData = computed(() => {
return {
$vars: environmentsStore.variablesAsObject,
...(externalSecretsStore.isEnterpriseExternalSecretsEnabled && props.isForCredential
? { $secrets: externalSecretsStore.secretsAsObject }
: {}),
...props.additionalExpressionData,
};
});
const parsedParameterName = computed(() => {
return parseResourceMapperFieldName(props.parameter?.name ?? '');
});
function onFocus() {
emit('focus');
}
function onBlur() {
emit('blur');
}
function onDrop(data: string) {
emit('drop', data);
}
function onValueChanged(parameterData: IUpdateInformation) {
emit('update', parameterData);
}
function onTextInput(parameterData: IUpdateInformation) {
emit('textInput', parameterData);
}
</script> </script>
<style lang="scss" module> <style lang="scss" module>