fix(editor): Fix design system typecheck errors (no-changelog) (#9447)

This commit is contained in:
Alex Grozav 2024-05-21 20:53:19 +03:00 committed by GitHub
parent d21ad15c1f
commit eef5479e96
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 161 additions and 65 deletions

View file

@ -3,6 +3,7 @@ import { computed, ref } from 'vue';
import { uid } from '../../utils';
import { ElColorPicker } from 'element-plus';
import N8nInput from '../N8nInput';
import type { ElementPlusSizePropType } from '@/types';
export type ColorPickerProps = {
disabled?: boolean;
@ -19,10 +20,12 @@ export type ColorPickerProps = {
defineOptions({ name: 'N8nColorPicker' });
const props = withDefaults(defineProps<ColorPickerProps>(), {
disabled: false,
size: 'medium',
size: 'default',
showAlpha: false,
colorFormat: 'hex',
popperClass: '',
predefine: undefined,
modelValue: undefined,
showInput: true,
name: uid('color-picker'),
});
@ -30,7 +33,7 @@ const props = withDefaults(defineProps<ColorPickerProps>(), {
const color = ref(props.modelValue);
const colorPickerProps = computed(() => {
const { showInput, ...rest } = props;
const { showInput, modelValue, size, ...rest } = props;
return rest;
});
@ -40,6 +43,8 @@ const emit = defineEmits<{
(event: 'active-change', value: string): void;
}>();
const resolvedSize = computed(() => props.size as ElementPlusSizePropType);
const onChange = (value: string) => {
emit('change', value);
};
@ -61,7 +66,7 @@ const onColorSelect = (value: string) => {
<span :class="['n8n-color-picker', $style.component]">
<ElColorPicker
v-bind="colorPickerProps"
size="default"
:size="resolvedSize"
@change="onChange"
@active-change="onActiveChange"
@update:model-value="onColorSelect"
@ -70,7 +75,7 @@ const onColorSelect = (value: string) => {
v-if="showInput"
:class="$style.input"
:disabled="props.disabled"
:size="props.size"
:size="size"
:model-value="color"
:name="name"
type="text"

View file

@ -64,7 +64,7 @@ exports[`components > N8nColorPicker > should render with input 1`] = `
<!--teleport end-->
<div
class="el-input el-input--medium n8n-input input input"
class="el-input el-input--default n8n-input input input"
data-v-dab78bb8=""
>
<!-- input -->
@ -79,7 +79,6 @@ exports[`components > N8nColorPicker > should render with input 1`] = `
<input
autocomplete="off"
class="el-input__inner"
maxlength="Infinity"
name="color-picker"
placeholder=""
rows="2"

View file

@ -1,8 +1,11 @@
<template>
<N8nCheckbox
v-if="type === 'checkbox'"
v-bind="$props"
ref="inputRef"
:label="label"
:disabled="disabled"
:label-size="labelSize as CheckboxLabelSizePropType"
:model-value="modelValue as CheckboxModelValuePropType"
@update:model-value="onUpdateModelValue"
@focus="onFocus"
/>
@ -17,7 +20,7 @@
{{ tooltipText }}
</template>
<ElSwitch
:model-value="modelValue"
:model-value="modelValue as SwitchModelValuePropType"
:active-color="activeColor"
:inactive-color="inactiveColor"
@update:model-value="onUpdateModelValue"
@ -59,9 +62,9 @@
v-else
ref="inputRef"
:name="name"
:type="type"
:type="type as InputTypePropType"
:placeholder="placeholder"
:model-value="modelValue"
:model-value="modelValue as InputModelValuePropType"
:maxlength="maxlength"
:autocomplete="autocomplete"
:disabled="disabled"
@ -99,7 +102,18 @@ import N8nCheckbox from '../N8nCheckbox';
import { ElSwitch } from 'element-plus';
import { getValidationError, VALIDATORS } from './validators';
import type { Rule, RuleGroup, IValidator, Validatable, FormState } from '../../types';
import type {
Rule,
RuleGroup,
IValidator,
Validatable,
InputModelValuePropType,
InputTypePropType,
SwitchModelValuePropType,
CheckboxModelValuePropType,
CheckboxLabelSizePropType,
InputAutocompletePropType,
} from '../../types';
import { t } from '../../locale';
@ -120,10 +134,10 @@ export interface Props {
validators?: { [key: string]: IValidator | RuleGroup };
maxlength?: number;
options?: Array<{ value: string | number; label: string; disabled?: boolean }>;
autocomplete?: string;
autocomplete?: InputAutocompletePropType;
name?: string;
focusInitially?: boolean;
labelSize?: 'small' | 'medium';
labelSize?: 'small' | 'medium' | 'large';
disabled?: boolean;
activeLabel?: string;
activeColor?: string;
@ -206,7 +220,7 @@ function onBlur() {
$emit('blur');
}
function onUpdateModelValue(value: FormState) {
function onUpdateModelValue(value: Validatable) {
state.isTyping = true;
$emit('update:modelValue', value);
}
@ -225,9 +239,9 @@ const validationError = computed<string | null>(() => {
const error = getInputValidationError();
if (error) {
if (error.messageKey) {
return t(error.messageKey, error.options);
} else {
if ('messageKey' in error) {
return t(error.messageKey, error.options as object);
} else if ('message' in error) {
return error.message;
}
}

View file

@ -20,7 +20,7 @@ export const requiredValidator: IValidator<{}> = {
};
export const minLengthValidator: IValidator<{ minimum: number }> = {
validate: (value: Validatable, config: { minimum: number }) => {
validate: (value: Validatable, config) => {
if (typeof value === 'string' && value.length < config.minimum) {
return {
messageKey: 'formInput.validator.minCharactersRequired',
@ -76,7 +76,7 @@ export const emailValidator: IValidator<{}> = {
};
export const containsUpperCaseValidator: IValidator<{ minimum: number }> = {
validate: (value: Validatable, config: { minimum: number }) => {
validate: (value: Validatable, config) => {
if (typeof value !== 'string') {
return false;
}
@ -94,7 +94,7 @@ export const containsUpperCaseValidator: IValidator<{ minimum: number }> = {
};
export const matchRegex: IValidator<{ regex: RegExp; message: string }> = {
validate: (value: Validatable, config: { regex: RegExp; message: string }) => {
validate: (value: Validatable, config) => {
if (!config.regex.test(`${value as string}`)) {
return {
message: config.message,

View file

@ -75,7 +75,7 @@ export default defineComponent({
default: true,
},
tagSize: {
type: String,
type: String as PropType<'small' | 'medium'>,
default: 'small',
validator: (value: string): boolean => ['small', 'medium'].includes(value),
},

View file

@ -1,11 +1,19 @@
<template>
<ElInput
ref="innerInput"
:size="computedSize"
:model-value="modelValue"
:type="type"
:size="resolvedSize"
:class="['n8n-input', ...classes]"
:autocomplete="autocomplete"
:name="name"
v-bind="{ ...$props, ...$attrs }"
:placeholder="placeholder"
:disabled="disabled"
:readonly="readonly"
:clearable="clearable"
:rows="rows"
:title="title"
v-bind="$attrs"
>
<template v-if="$slots.prepend" #prepend>
<slot name="prepend" />
@ -27,6 +35,7 @@ import { computed, ref } from 'vue';
import { ElInput } from 'element-plus';
import { uid } from '../../utils';
import type { InputSize, InputType } from '@/types/input';
import type { ElementPlusSizePropType, InputAutocompletePropType } from '@/types';
interface InputProps {
modelValue?: string | number;
@ -40,12 +49,13 @@ interface InputProps {
maxlength?: number;
title?: string;
name?: string;
autocomplete?: 'off' | 'autocomplete';
autocomplete?: InputAutocompletePropType;
}
defineOptions({ name: 'N8nInput' });
const props = withDefaults(defineProps<InputProps>(), {
modelValue: '',
type: 'text',
size: 'large',
placeholder: '',
disabled: false,
@ -58,7 +68,9 @@ const props = withDefaults(defineProps<InputProps>(), {
autocomplete: 'off',
});
const computedSize = computed(() => (props.size === 'xlarge' ? undefined : props.size));
const resolvedSize = computed(
() => (props.size === 'xlarge' ? undefined : props.size) as ElementPlusSizePropType,
);
const classes = computed(() => {
const applied: string[] = [];

View file

@ -7,7 +7,7 @@ exports[`N8nInput > should render correctly 1`] = `
<!--v-if-->
<div class="el-input__wrapper">
<!-- prefix slot -->
<!--v-if--><input class="el-input__inner" name="input" rows="2" maxlength="Infinity" title="" type="text" autocomplete="off" tabindex="0" placeholder=""><!-- suffix slot -->
<!--v-if--><input class="el-input__inner" name="input" rows="2" title="" type="text" autocomplete="off" tabindex="0" placeholder=""><!-- suffix slot -->
<!--v-if-->
</div><!-- append slot -->
<!--v-if-->

View file

@ -1,6 +1,7 @@
<script setup lang="ts">
import type { InputSize } from '@/types';
import type { ElementPlusSizePropType, InputSize } from '@/types';
import { ElInputNumber } from 'element-plus';
import { computed } from 'vue';
type InputNumberProps = {
size?: InputSize;
@ -10,9 +11,24 @@ type InputNumberProps = {
precision?: number;
};
defineProps<InputNumberProps>();
const props = withDefaults(defineProps<InputNumberProps>(), {
size: undefined,
step: 1,
precision: 0,
min: -Infinity,
max: Infinity,
});
const resolvedSize = computed(() => props.size as ElementPlusSizePropType);
</script>
<template>
<ElInputNumber v-bind="{ ...$props, ...$attrs }" />
<ElInputNumber
:size="resolvedSize"
:min="min"
:max="max"
:step="step"
:precision="precision"
v-bind="$attrs"
/>
</template>

View file

@ -17,7 +17,7 @@ import type { TextSize } from '@/types/text';
const THEME = ['primary', 'danger', 'text', 'secondary'] as const;
interface LinkProps {
to?: RouteLocationRaw;
to?: RouteLocationRaw | string;
size?: TextSize;
newWindow?: boolean;
bold?: boolean;
@ -27,6 +27,8 @@ interface LinkProps {
defineOptions({ name: 'N8nLink' });
withDefaults(defineProps<LinkProps>(), {
to: undefined,
size: undefined,
bold: false,
underline: false,
theme: 'primary',

View file

@ -13,7 +13,7 @@
<template #content>{{ nodeTypeName }}</template>
<div v-if="type !== 'unknown'" :class="$style.icon">
<img v-if="type === 'file'" :src="src" :class="$style.nodeIconImage" />
<FontAwesomeIcon v-else :icon="name" :class="$style.iconFa" :style="fontStyleData" />
<FontAwesomeIcon v-else :icon="`${name}`" :class="$style.iconFa" :style="fontStyleData" />
</div>
<div v-else :class="$style.nodeIconPlaceholder">
{{ nodeTypeName ? nodeTypeName.charAt(0) : '?' }}
@ -22,7 +22,7 @@
<template v-else>
<div v-if="type !== 'unknown'" :class="$style.icon">
<img v-if="type === 'file'" :src="src" :class="$style.nodeIconImage" />
<FontAwesomeIcon v-else :icon="name" :style="fontStyleData" />
<FontAwesomeIcon v-else :icon="`${name}`" :style="fontStyleData" />
<div v-if="badge" :class="$style.badge" :style="badgeStyleData">
<n8n-node-icon :type="badge.type" :src="badge.src" :size="badgeSize"></n8n-node-icon>
</div>

View file

@ -1,8 +1,13 @@
<template>
<router-link v-if="useRouterLink" :to="to" v-bind="$attrs">
<router-link v-if="useRouterLink && to" :to="to" v-bind="$attrs">
<slot></slot>
</router-link>
<a v-else :href="to" :target="openNewWindow ? '_blank' : '_self'" v-bind="$attrs">
<a
v-else
:href="to ? `${to}` : undefined"
:target="openNewWindow ? '_blank' : '_self'"
v-bind="$attrs"
>
<slot></slot>
</a>
</template>
@ -12,7 +17,7 @@ import { computed } from 'vue';
import { type RouteLocationRaw } from 'vue-router';
interface RouteProps {
to?: RouteLocationRaw;
to?: RouteLocationRaw | string;
newWindow?: boolean;
}

View file

@ -32,6 +32,7 @@
import { ElSelect } from 'element-plus';
import { type PropType, defineComponent } from 'vue';
import type { SelectSize } from '@/types';
import { isEventBindingElementAttribute } from '../../utils';
type InnerSelectRef = InstanceType<typeof ElSelect>;
@ -86,13 +87,16 @@ export default defineComponent({
},
computed: {
listeners() {
return Object.entries(this.$attrs).reduce<Record<string, () => {}>>((acc, [key, value]) => {
if (/^on[A-Z]/.test(key)) {
return Object.entries(this.$attrs).reduce<Record<string, (...args: unknown[]) => {}>>(
(acc, [key, value]) => {
if (isEventBindingElementAttribute(value, key)) {
acc[key] = value;
}
return acc;
}, {});
},
{},
);
},
computedSize(): InnerSelectRef['$props']['size'] {
if (this.size === 'medium') {

View file

@ -13,8 +13,7 @@ export default {
};
const methods = {
onReinvite: action('reinvite'),
onDelete: action('delete'),
action: ({ action: actionName }: { action: string; userId: string }) => action(actionName),
};
const Template: StoryFn = (args, { argTypes }) => ({
@ -23,8 +22,7 @@ const Template: StoryFn = (args, { argTypes }) => ({
components: {
N8nUsersList,
},
template:
'<n8n-users-list v-bind="args" :actions="actions" @reinvite="onReinvite" @delete="onDelete" />',
template: '<n8n-users-list v-bind="args" :actions="actions" @action="action" />',
methods,
});

View file

@ -52,6 +52,7 @@ interface UsersListProps {
const props = withDefaults(defineProps<UsersListProps>(), {
readonly: false,
currentUserId: '',
users: () => [],
actions: () => [],
isSamlLoginEnabled: false,
@ -101,11 +102,15 @@ const defaultGuard = () => true;
const getActions = (user: IUser): UserAction[] => {
if (user.isOwner) return [];
return props.actions.filter((action) => (action.guard || defaultGuard)(user));
return props.actions.filter((action) => (action.guard ?? defaultGuard)(user));
};
const $emit = defineEmits(['*']);
const onUserAction = (user: IUser, action: string) => $emit(action, user.id);
const $emit = defineEmits(['action']);
const onUserAction = (user: IUser, action: string) =>
$emit('action', {
action,
userId: user.id,
});
</script>
<style lang="scss" module>

View file

@ -1,4 +1,4 @@
import type { VNode } from 'vue';
import type { Component, VNode } from 'vue';
export type DatatableRowDataType = string | number | boolean | null | undefined;
@ -14,5 +14,5 @@ export interface DatatableColumn {
label: string;
classes?: string[];
width?: string;
render?: (row: DatatableRow) => (() => VNode | VNode[]) | DatatableRowDataType;
render?: Component | ((row: DatatableRow) => (() => VNode | VNode[]) | DatatableRowDataType);
}

View file

@ -11,7 +11,11 @@ export type IValidator<T = unknown> = {
validate: (
value: Validatable,
config: T,
) => false | { messageKey: string; message?: string; options?: unknown } | null;
) =>
| false
| { message: string; options?: unknown }
| { messageKey: string; options?: unknown }
| null;
};
export type FormState = {
@ -45,13 +49,7 @@ export type IFormInput = {
infoText?: string;
placeholder?: string;
options?: Array<{ label: string; value: string; disabled?: boolean }>;
autocomplete?:
| 'off'
| 'new-password'
| 'current-password'
| 'given-name'
| 'family-name'
| 'email'; // https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete
autocomplete?: InputAutocompletePropType;
capitalize?: boolean;
focusInitially?: boolean;
disabled?: boolean;
@ -72,3 +70,17 @@ export type IFormBoxConfig = {
redirectLink?: string;
redirectText?: string;
};
export type CheckboxLabelSizePropType = 'small' | 'medium' | undefined;
export type CheckboxModelValuePropType = boolean | undefined;
export type SwitchModelValuePropType = boolean | undefined;
export type InputModelValuePropType = string | number | undefined;
export type InputTypePropType = 'number' | 'text' | 'email' | 'password' | 'textarea' | undefined;
export type InputAutocompletePropType =
| 'off'
| 'new-password'
| 'current-password'
| 'given-name'
| 'family-name'
| 'email'; // https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete
export type ElementPlusSizePropType = '' | 'small' | 'large' | 'default' | undefined;

View file

@ -1,4 +1,5 @@
export * from './event-bus';
export * from './markdown';
export * from './typeguards';
export * from './uid';
export * from './valueByPath';

View file

@ -0,0 +1,6 @@
export function isEventBindingElementAttribute(
_attributeValue: unknown,
attributeName: string,
): _attributeValue is (...args: unknown[]) => {} {
return /^on[A-Z]/.test(attributeName);
}

View file

@ -11,7 +11,7 @@
"allowSyntheticDefaultImports": true,
"baseUrl": ".",
"types": ["vitest/globals"],
"typeRoots": ["@testing-library", "@types", "../../node_modules"],
"typeRoots": ["./node_modules/@testing-library", "./node_modules/@types", "../../node_modules", "../../node_modules/@types"],
"paths": {
"@/*": ["src/*"]
},

View file

@ -55,12 +55,7 @@
:users="usersStore.allUsers"
:current-user-id="usersStore.currentUserId"
:is-saml-login-enabled="ssoStore.isSamlLoginEnabled"
@delete="onDelete"
@reinvite="onReinvite"
@copy-invite-link="onCopyInviteLink"
@copy-password-reset-link="onCopyPasswordResetLink"
@allow-s-s-o-manual-login="onAllowSSOManualLogin"
@disallow-s-s-o-manual-login="onDisallowSSOManualLogin"
@action="onUsersListAction"
>
<template #actions="{ user }">
<n8n-select
@ -192,6 +187,28 @@ export default defineComponent({
}
},
methods: {
async onUsersListAction({ action, userId }: { action: string; userId: string }) {
switch (action) {
case 'delete':
await this.onDelete(userId);
break;
case 'reinvite':
await this.onReinvite(userId);
break;
case 'copyInviteLink':
await this.onCopyInviteLink(userId);
break;
case 'copyPasswordResetLink':
await this.onCopyPasswordResetLink(userId);
break;
case 'allowSSOManualLogin':
await this.onAllowSSOManualLogin(userId);
break;
case 'disallowSSOManualLogin':
await this.onDisallowSSOManualLogin(userId);
break;
}
},
redirectToSetup() {
void this.$router.push({ name: VIEWS.SETUP });
},