mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
refactor(editor): migrate FormInput to Composition API (#4406)
♻️ Refactor N8nFormInput to use composition API and make labels accesible
This commit is contained in:
@@ -19,7 +19,9 @@ const Template = (args, { argTypes }) => ({
|
|||||||
components: {
|
components: {
|
||||||
N8nFormInput,
|
N8nFormInput,
|
||||||
},
|
},
|
||||||
template: '<n8n-form-input v-bind="$props" v-model="val" @input="onInput" @change="onChange" @focus="onFocus" />',
|
template: `
|
||||||
|
<n8n-form-input v-bind="$props" v-model="val" @input="onInput" @change="onChange" @focus="onFocus" />
|
||||||
|
`,
|
||||||
methods,
|
methods,
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -4,11 +4,11 @@
|
|||||||
v-bind="$props"
|
v-bind="$props"
|
||||||
@input="onInput"
|
@input="onInput"
|
||||||
@focus="onFocus"
|
@focus="onFocus"
|
||||||
ref="input"
|
ref="inputRef"
|
||||||
></n8n-checkbox>
|
/>
|
||||||
<n8n-input-label v-else :label="label" :tooltipText="tooltipText" :required="required && showRequiredAsterisk">
|
<n8n-input-label v-else :inputName="name" :label="label" :tooltipText="tooltipText" :required="required && showRequiredAsterisk">
|
||||||
<div :class="showErrors ? $style.errorInput : ''" @keydown.stop @keydown.enter="onEnter">
|
<div :class="showErrors ? $style.errorInput : ''" @keydown.stop @keydown.enter="onEnter">
|
||||||
<slot v-if="hasDefaultSlot"></slot>
|
<slot v-if="hasDefaultSlot" />
|
||||||
<n8n-select
|
<n8n-select
|
||||||
v-else-if="type === 'select' || type === 'multi-select'"
|
v-else-if="type === 'select' || type === 'multi-select'"
|
||||||
:value="value"
|
:value="value"
|
||||||
@@ -17,7 +17,8 @@
|
|||||||
@change="onInput"
|
@change="onInput"
|
||||||
@focus="onFocus"
|
@focus="onFocus"
|
||||||
@blur="onBlur"
|
@blur="onBlur"
|
||||||
ref="input"
|
:name="name"
|
||||||
|
ref="inputRef"
|
||||||
>
|
>
|
||||||
<n8n-option
|
<n8n-option
|
||||||
v-for="option in (options || [])"
|
v-for="option in (options || [])"
|
||||||
@@ -28,6 +29,7 @@
|
|||||||
</n8n-select>
|
</n8n-select>
|
||||||
<n8n-input
|
<n8n-input
|
||||||
v-else
|
v-else
|
||||||
|
:name="name"
|
||||||
:type="type"
|
:type="type"
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
:value="value"
|
:value="value"
|
||||||
@@ -36,11 +38,11 @@
|
|||||||
@input="onInput"
|
@input="onInput"
|
||||||
@blur="onBlur"
|
@blur="onBlur"
|
||||||
@focus="onFocus"
|
@focus="onFocus"
|
||||||
ref="input"
|
ref="inputRef"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.errors" v-if="showErrors">
|
<div :class="$style.errors" v-if="showErrors">
|
||||||
<span>{{ validationError }}</span>
|
<span v-text="validationError" />
|
||||||
<n8n-link
|
<n8n-link
|
||||||
v-if="documentationUrl && documentationText"
|
v-if="documentationUrl && documentationText"
|
||||||
:to="documentationUrl"
|
:to="documentationUrl"
|
||||||
@@ -52,13 +54,14 @@
|
|||||||
</n8n-link>
|
</n8n-link>
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.infoText" v-else-if="infoText">
|
<div :class="$style.infoText" v-else-if="infoText">
|
||||||
<span size="small">{{ infoText }}</span>
|
<span size="small" v-text="infoText" />
|
||||||
</div>
|
</div>
|
||||||
</n8n-input-label>
|
</n8n-input-label>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import Vue from 'vue';
|
import { computed, reactive, onMounted, ref, watch, useSlots } from 'vue';
|
||||||
|
|
||||||
import N8nInput from '../N8nInput';
|
import N8nInput from '../N8nInput';
|
||||||
import N8nSelect from '../N8nSelect';
|
import N8nSelect from '../N8nSelect';
|
||||||
import N8nOption from '../N8nOption';
|
import N8nOption from '../N8nOption';
|
||||||
@@ -68,185 +71,137 @@ import N8nCheckbox from '../N8nCheckbox';
|
|||||||
import { getValidationError, VALIDATORS } from './validators';
|
import { getValidationError, VALIDATORS } from './validators';
|
||||||
import { Rule, RuleGroup, IValidator } from "../../types";
|
import { Rule, RuleGroup, IValidator } from "../../types";
|
||||||
|
|
||||||
import Locale from '../../mixins/locale';
|
import { t } from '../../locale';
|
||||||
import mixins from 'vue-typed-mixins';
|
|
||||||
|
|
||||||
export default mixins(Locale).extend({
|
export interface Props {
|
||||||
name: 'n8n-form-input',
|
value: any;
|
||||||
components: {
|
label: string;
|
||||||
N8nInput,
|
infoText?: string;
|
||||||
N8nInputLabel,
|
required?: boolean;
|
||||||
N8nOption,
|
showRequiredAsterisk?: boolean;
|
||||||
N8nSelect,
|
type?: string;
|
||||||
N8nCheckbox,
|
placeholder?: string;
|
||||||
},
|
tooltipText?: string;
|
||||||
data() {
|
showValidationWarnings?: boolean;
|
||||||
return {
|
validateOnBlur?: boolean;
|
||||||
hasBlurred: false,
|
documentationUrl?: string;
|
||||||
isTyping: false,
|
documentationText?: string;
|
||||||
};
|
validationRules?: Array<Rule | RuleGroup>;
|
||||||
},
|
validators?: {[key: string]: IValidator | RuleGroup};
|
||||||
props: {
|
maxlength?: number;
|
||||||
value: {
|
options?: Array<{value: string | number, label: string}>;
|
||||||
},
|
autocomplete?: string;
|
||||||
label: {
|
name?: string;
|
||||||
type: String,
|
focusInitially?: boolean;
|
||||||
},
|
labelSize?: 'small' | 'medium';
|
||||||
infoText: {
|
}
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
required: {
|
|
||||||
type: Boolean,
|
|
||||||
},
|
|
||||||
showRequiredAsterisk: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
type: {
|
|
||||||
type: String,
|
|
||||||
default: 'text',
|
|
||||||
},
|
|
||||||
placeholder: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
tooltipText: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
showValidationWarnings: {
|
|
||||||
type: Boolean,
|
|
||||||
},
|
|
||||||
validateOnBlur: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
documentationUrl: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
documentationText: {
|
|
||||||
type: String,
|
|
||||||
default: 'Open docs',
|
|
||||||
},
|
|
||||||
validationRules: {
|
|
||||||
type: Array,
|
|
||||||
},
|
|
||||||
validators: {
|
|
||||||
type: Object,
|
|
||||||
},
|
|
||||||
maxlength: {
|
|
||||||
type: Number,
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
type: Array,
|
|
||||||
},
|
|
||||||
autocomplete: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
focusInitially: {
|
|
||||||
type: Boolean,
|
|
||||||
},
|
|
||||||
labelSize: {
|
|
||||||
type: String,
|
|
||||||
default: 'medium',
|
|
||||||
validator: (value: string): boolean =>
|
|
||||||
['small', 'medium'].includes(value),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.$emit('validate', !this.validationError);
|
|
||||||
|
|
||||||
if (this.focusInitially && this.$refs.input) {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
(this.$refs.input as HTMLTextAreaElement).focus();
|
documentationText: 'Open docs',
|
||||||
}
|
labelSize: 'medium',
|
||||||
},
|
type: 'text',
|
||||||
computed: {
|
showRequiredAsterisk: true,
|
||||||
validationError(): string | null {
|
validateOnBlur: true,
|
||||||
const error = this.getValidationError();
|
|
||||||
if (error) {
|
|
||||||
return this.t(error.messageKey, error.options);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
hasDefaultSlot(): boolean {
|
|
||||||
return !!this.$slots.default;
|
|
||||||
},
|
|
||||||
showErrors(): boolean {
|
|
||||||
return (
|
|
||||||
!!this.validationError &&
|
|
||||||
((this.validateOnBlur && this.hasBlurred && !this.isTyping) || this.showValidationWarnings)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
getValidationError(): ReturnType<IValidator['validate']> {
|
|
||||||
const rules = (this.validationRules || []) as Array<Rule | RuleGroup>;
|
|
||||||
const validators = {
|
|
||||||
...VALIDATORS,
|
|
||||||
...(this.validators || {}),
|
|
||||||
} as { [key: string]: IValidator | RuleGroup };
|
|
||||||
|
|
||||||
if (this.required) {
|
|
||||||
const error = getValidationError(this.value, validators, validators.REQUIRED as IValidator);
|
|
||||||
if (error) {
|
|
||||||
return error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < rules.length; i++) {
|
|
||||||
if (rules[i].hasOwnProperty('name')) {
|
|
||||||
const rule = rules[i] as Rule;
|
|
||||||
if (validators[rule.name]) {
|
|
||||||
const error = getValidationError(
|
|
||||||
this.value,
|
|
||||||
validators,
|
|
||||||
validators[rule.name] as IValidator,
|
|
||||||
rule.config,
|
|
||||||
);
|
|
||||||
if (error) {
|
|
||||||
return error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rules[i].hasOwnProperty('rules')) {
|
|
||||||
const rule = rules[i] as RuleGroup;
|
|
||||||
const error = getValidationError(
|
|
||||||
this.value,
|
|
||||||
validators,
|
|
||||||
rule,
|
|
||||||
);
|
|
||||||
if (error) {
|
|
||||||
return error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
onBlur() {
|
|
||||||
this.hasBlurred = true;
|
|
||||||
this.isTyping = false;
|
|
||||||
this.$emit('blur');
|
|
||||||
},
|
|
||||||
onInput(value: any) {
|
|
||||||
this.isTyping = true;
|
|
||||||
this.$emit('input', value);
|
|
||||||
},
|
|
||||||
onFocus() {
|
|
||||||
this.$emit('focus');
|
|
||||||
},
|
|
||||||
onEnter(event: Event) {
|
|
||||||
event.stopPropagation();
|
|
||||||
event.preventDefault();
|
|
||||||
this.$emit('enter');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
validationError(newValue: string | null, oldValue: string | null) {
|
|
||||||
this.$emit('validate', !newValue);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'validate', shouldValidate: boolean): void,
|
||||||
|
(event: 'input', value: any): void,
|
||||||
|
(event: 'focus'): void,
|
||||||
|
(event: 'blur'): void,
|
||||||
|
(event: 'enter'): void,
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
hasBlurred: false,
|
||||||
|
isTyping: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const slots = useSlots();
|
||||||
|
|
||||||
|
const inputRef = ref<HTMLTextAreaElement | null>(null);
|
||||||
|
|
||||||
|
function getInputValidationError(): ReturnType<IValidator['validate']> {
|
||||||
|
const rules = props.validationRules || [];
|
||||||
|
const validators = {
|
||||||
|
...VALIDATORS,
|
||||||
|
...(props.validators || {}),
|
||||||
|
} as { [key: string]: IValidator | RuleGroup };
|
||||||
|
|
||||||
|
if (props.required) {
|
||||||
|
const error = getValidationError(props.value, validators, validators.REQUIRED as IValidator);
|
||||||
|
if (error) return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < rules.length; i++) {
|
||||||
|
if (rules[i].hasOwnProperty('name')) {
|
||||||
|
const rule = rules[i] as Rule;
|
||||||
|
if (validators[rule.name]) {
|
||||||
|
const error = getValidationError(
|
||||||
|
props.value,
|
||||||
|
validators,
|
||||||
|
validators[rule.name] as IValidator,
|
||||||
|
rule.config,
|
||||||
|
);
|
||||||
|
if (error) return error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rules[i].hasOwnProperty('rules')) {
|
||||||
|
const rule = rules[i] as RuleGroup;
|
||||||
|
const error = getValidationError(
|
||||||
|
props.value,
|
||||||
|
validators,
|
||||||
|
rule,
|
||||||
|
);
|
||||||
|
if (error) return error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onBlur() {
|
||||||
|
state.hasBlurred = true;
|
||||||
|
state.isTyping = false;
|
||||||
|
emit('blur');
|
||||||
|
}
|
||||||
|
|
||||||
|
function onInput(value: any) {
|
||||||
|
state.isTyping = true;
|
||||||
|
emit('input', value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onFocus() {
|
||||||
|
emit('focus');
|
||||||
|
}
|
||||||
|
|
||||||
|
function onEnter(event: Event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
emit('enter');
|
||||||
|
}
|
||||||
|
|
||||||
|
const validationError = computed<string | null>(() => {
|
||||||
|
const error = getInputValidationError();
|
||||||
|
|
||||||
|
return error ? t(error.messageKey, error.options) : null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasDefaultSlot = computed(() => !!slots.default);
|
||||||
|
|
||||||
|
const showErrors = computed(() => (
|
||||||
|
!!validationError.value &&
|
||||||
|
((props.validateOnBlur && state.hasBlurred && !state.isTyping) || props.showValidationWarnings)
|
||||||
|
));
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (props.focusInitially && inputRef.value) inputRef.value.focus();
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(() => validationError.value, (error) => emit('validate', !error));
|
||||||
|
|
||||||
|
defineExpose({ inputRef });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
<n8n-form-input
|
<n8n-form-input
|
||||||
v-else
|
v-else
|
||||||
v-bind="input.properties"
|
v-bind="input.properties"
|
||||||
|
:name="input.name"
|
||||||
:value="values[input.name]"
|
:value="values[input.name]"
|
||||||
:showValidationWarnings="showValidationWarnings"
|
:showValidationWarnings="showValidationWarnings"
|
||||||
@input="(value) => onInput(input.name, value)"
|
@input="(value) => onInput(input.name, value)"
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
:autoComplete="autocomplete"
|
:autoComplete="autocomplete"
|
||||||
ref="innerInput"
|
ref="innerInput"
|
||||||
v-on="$listeners"
|
v-on="$listeners"
|
||||||
|
:name="name"
|
||||||
>
|
>
|
||||||
<template #prepend>
|
<template #prepend>
|
||||||
<slot name="prepend" />
|
<slot name="prepend" />
|
||||||
@@ -66,6 +67,9 @@ export default Vue.extend({
|
|||||||
title: {
|
title: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
autocomplete: {
|
autocomplete: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'off',
|
default: 'off',
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<div :class="$style.container">
|
<div :class="$style.container">
|
||||||
<div v-if="label || $slots.options" :class="{
|
<label
|
||||||
'n8n-input-label': true,
|
v-if="label || $slots.options"
|
||||||
[this.$style.heading]: !!label,
|
:for="inputName"
|
||||||
[this.$style.underline]: underline,
|
:class="{
|
||||||
[this.$style[this.size]]: true,
|
[$style.inputLabel]: true,
|
||||||
|
[$style.heading]: !!label,
|
||||||
|
[$style.underline]: underline,
|
||||||
|
[$style[size]]: true,
|
||||||
[$style.overflow]: !!$slots.options,
|
[$style.overflow]: !!$slots.options,
|
||||||
}">
|
}"
|
||||||
|
>
|
||||||
<div :class="$style.title" v-if="label">
|
<div :class="$style.title" v-if="label">
|
||||||
<n8n-text :bold="bold" :size="size" :compact="!underline && !$slots.options" :color="color">
|
<n8n-text :bold="bold" :size="size" :compact="!underline && !$slots.options" :color="color">
|
||||||
{{ label }}
|
{{ label }}
|
||||||
@@ -16,15 +20,15 @@
|
|||||||
<span :class="[$style.infoIcon, showTooltip ? $style.visible: $style.hidden]" v-if="tooltipText && label">
|
<span :class="[$style.infoIcon, showTooltip ? $style.visible: $style.hidden]" v-if="tooltipText && label">
|
||||||
<n8n-tooltip placement="top" :popper-class="$style.tooltipPopper">
|
<n8n-tooltip placement="top" :popper-class="$style.tooltipPopper">
|
||||||
<n8n-icon icon="question-circle" size="small" />
|
<n8n-icon icon="question-circle" size="small" />
|
||||||
<div slot="content" v-html="addTargetBlank(tooltipText)"></div>
|
<div slot="content" v-html="addTargetBlank(tooltipText)" />
|
||||||
</n8n-tooltip>
|
</n8n-tooltip>
|
||||||
</span>
|
</span>
|
||||||
<div v-if="$slots.options && label" :class="{[$style.overlay]: true, [$style.visible]: showOptions}"><div></div></div>
|
<div v-if="$slots.options && label" :class="{[$style.overlay]: true, [$style.visible]: showOptions}" />
|
||||||
<div v-if="$slots.options" :class="{[$style.options]: true, [$style.visible]: showOptions}">
|
<div v-if="$slots.options" :class="{[$style.options]: true, [$style.visible]: showOptions}">
|
||||||
<slot name="options"></slot>
|
<slot name="options"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</label>
|
||||||
<slot></slot>
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -54,6 +58,9 @@ export default Vue.extend({
|
|||||||
tooltipText: {
|
tooltipText: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
|
inputName: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
required: {
|
required: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
},
|
},
|
||||||
@@ -88,7 +95,9 @@ export default Vue.extend({
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
.inputLabel {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
.container:hover,.inputLabel:hover {
|
.container:hover,.inputLabel:hover {
|
||||||
.infoIcon {
|
.infoIcon {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
|||||||
Reference in New Issue
Block a user