feat: Eslint rules (#9839)

# Pull Request Template

## Description

This PR adds new eslint rules to the code base.

**Error rules**

|    Rule name     | Type | Files updated |
| ----------------- | --- | - |
| `vue/block-order`  | error  |    |
| `vue/component-name-in-template-casing`  | error  |    |
| `vue/component-options-name-casing`  | error  |    |
| `vue/custom-event-name-casing`  | error  |    |
| `vue/define-emits-declaration`  | error  |    |
| `vue/no-unused-properties`  | error  |    |
| `vue/define-macros-order`  | error  |    |
| `vue/define-props-declaration`  | error  |    |
| `vue/match-component-import-name`  | error  |    |
| `vue/next-tick-style`  | error  |    |
| `vue/no-bare-strings-in-template`  | error  |    |
| `vue/no-empty-component-block`  | error  |    |
| `vue/no-multiple-objects-in-class`  | error  |    |
| `vue/no-required-prop-with-default`  | error  |    |
| `vue/no-static-inline-styles`  | error  |    |
| `vue/no-template-target-blank`  | error  |    |
| `vue/no-this-in-before-route-enter`  | error  |    |
| `vue/no-undef-components`  | error  |    |
| `vue/no-unused-emit-declarations`  | error  |    |
| `vue/no-unused-refs`  | error  |    |
| `vue/no-use-v-else-with-v-for`  | error  |    |
| `vue/no-useless-v-bind`  | error  |    |
| `vue/no-v-text`  | error  |    |
| `vue/padding-line-between-blocks`  | error  |    |
| ~`vue/prefer-prop-type-boolean-first`~ | ~error~ |  (removed this
rule, cause a bug in displaying custom attributes) |
| `vue/prefer-separate-static-class`  | error  |    |
| `vue/prefer-true-attribute-shorthand`  | error  |    |
| `vue/require-explicit-slots`  | error  |    |
| `vue/require-macro-variable-name`  | error  |    |


**Warn rules**

|    Rule name     | Type | Files updated |
| ---- | ------------- | ------------- |
| `vue/no-root-v-if`  | warn  |    |


Fixes https://linear.app/chatwoot/issue/CW-3492/vue-eslint-rules

## Type of change

- [x] New feature (non-breaking change which adds functionality)


## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

---------

Co-authored-by: Fayaz Ahmed <fayazara@gmail.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
Co-authored-by: Pranav <pranav@chatwoot.com>
This commit is contained in:
Sivin Varghese
2024-08-05 14:02:16 +05:30
committed by GitHub
parent 6166ccb014
commit b4b308336f
625 changed files with 23071 additions and 22980 deletions

View File

@@ -1,18 +1,3 @@
<template>
<button
:type="type"
data-testid="submit_button"
:disabled="disabled"
:class="computedClass"
class="flex items-center w-full justify-center rounded-md bg-woot-500 py-3 px-3 text-base font-medium text-white shadow-sm hover:bg-woot-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-woot-500 cursor-pointer"
@click="onClick"
>
<span>{{ buttonText }}</span>
<fluent-icon v-if="!!iconClass" :icon="iconClass" class="icon" />
<spinner v-if="loading" />
</button>
</template>
<script>
import Spinner from 'shared/components/Spinner.vue';
@@ -61,3 +46,18 @@ export default {
},
};
</script>
<template>
<button
:type="type"
data-testid="submit_button"
:disabled="disabled"
:class="computedClass"
class="flex items-center w-full justify-center rounded-md bg-woot-500 py-3 px-3 text-base font-medium text-white shadow-sm hover:bg-woot-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-woot-500 cursor-pointer"
@click="onClick"
>
<span>{{ buttonText }}</span>
<fluent-icon v-if="!!iconClass" :icon="iconClass" class="icon" />
<Spinner v-if="loading" />
</button>
</template>

View File

@@ -1,3 +1,14 @@
<script>
export default {
props: {
label: {
type: String,
default: '',
},
},
};
</script>
<template>
<div class="relative my-4 section-separator">
<div class="absolute inset-0 flex items-center" aria-hidden="true">
@@ -12,13 +23,3 @@
</div>
</div>
</template>
<script>
export default {
props: {
label: {
type: String,
default: '',
},
},
};
</script>

View File

@@ -1,30 +1,3 @@
<template>
<button
class="inline-flex items-center gap-1 text-sm font-medium reset-base rounded-xl w-fit"
:class="buttonClasses"
v-bind="$attrs"
@click="onClick"
>
<fluent-icon
v-if="icon && !trailingIcon"
size="1.16em"
:icon="icon"
class="flex-shrink-0"
/>
<span
v-if="$slots.default"
class="text-sm font-medium truncate ltr:text-left rtl:text-right"
>
<slot />
</span>
<fluent-icon
v-if="icon && trailingIcon"
size="1.16em"
:icon="icon"
class="flex-shrink-0"
/>
</button>
</template>
<script setup>
import { computed, useAttrs } from 'vue';
@@ -54,6 +27,8 @@ const props = defineProps({
},
});
const emit = defineEmits(['click']);
const attrs = useAttrs();
const baseClasses = {
@@ -99,8 +74,6 @@ const sizeClass = computed(() => {
const buttonClasses = computed(() => [colorClass.value, sizeClass.value]);
const emit = defineEmits(['click']);
const onClick = () => {
if (props.disabled) {
return;
@@ -108,3 +81,31 @@ const onClick = () => {
emit('click');
};
</script>
<template>
<button
class="inline-flex items-center gap-1 text-sm font-medium reset-base rounded-xl w-fit"
:class="buttonClasses"
v-bind="$attrs"
@click="onClick"
>
<fluent-icon
v-if="icon && !trailingIcon"
size="1.16em"
:icon="icon"
class="flex-shrink-0"
/>
<span
v-if="$slots.default"
class="text-sm font-medium truncate ltr:text-left rtl:text-right"
>
<slot />
</span>
<fluent-icon
v-if="icon && trailingIcon"
size="1.16em"
:icon="icon"
class="flex-shrink-0"
/>
</button>
</template>

View File

@@ -1,13 +1,3 @@
<template>
<input
:id="value"
v-model="checked"
type="checkbox"
:value="value"
class="flex-shrink-0 mt-0.5 border-ash-200 border bg-ash-50 checked:border-none checked:bg-primary-600 dark:checked:bg-primary-600 shadow-sm appearance-none rounded-[4px] w-4 h-4 focus:ring-1 after:content-[''] after:text-white checked:after:content-['✓'] after:flex after:items-center after:justify-center after:text-center after:text-xs after:font-bold after:relative"
/>
</template>
<script setup>
import { computed } from 'vue';
@@ -22,10 +12,20 @@ const props = defineProps({
},
});
const emit = defineEmits(['input']);
const emit = defineEmits(['update']);
const checked = computed({
get: () => props.isChecked,
set: value => emit('update', props.value, value),
});
</script>
<template>
<input
:id="value"
v-model="checked"
type="checkbox"
:value="value"
class="flex-shrink-0 mt-0.5 border-ash-200 border bg-ash-50 checked:border-none checked:bg-primary-600 dark:checked:bg-primary-600 shadow-sm appearance-none rounded-[4px] w-4 h-4 focus:ring-1 after:content-[''] after:text-white checked:after:content-['✓'] after:flex after:items-center after:justify-center after:text-center after:text-xs after:font-bold after:relative"
/>
</template>

View File

@@ -1,27 +1,6 @@
<template>
<div
class="rounded-xl flex leading-[100%] font-medium items-center justify-center text-center cursor-default"
:class="`h-[${size}px] w-[${size}px] ${colorClass}`"
:style="style"
aria-hidden="true"
>
<slot>{{ initial }}</slot>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { userInitial } from 'v3/helpers/CommonHelper';
const colors = {
1: 'bg-ash-200 text-ash-900',
2: 'bg-amber-200 text-amber-900',
3: 'bg-pink-100 text-pink-800',
4: 'bg-purple-100 text-purple-800',
5: 'bg-indigo-100 text-indigo-800',
6: 'bg-grass-100 text-grass-800',
7: 'bg-mint-100 text-mint-800',
8: 'bg-orange-100 text-orange-800',
};
const props = defineProps({
name: {
type: String,
@@ -32,6 +11,16 @@ const props = defineProps({
default: 72,
},
});
const colors = {
1: 'bg-ash-200 text-ash-900',
2: 'bg-amber-200 text-amber-900',
3: 'bg-pink-100 text-pink-800',
4: 'bg-purple-100 text-purple-800',
5: 'bg-indigo-100 text-indigo-800',
6: 'bg-grass-100 text-grass-800',
7: 'bg-mint-100 text-mint-800',
8: 'bg-orange-100 text-orange-800',
};
const style = computed(() => ({
fontSize: `${Math.floor(props.size / 2.5)}px`,
}));
@@ -41,3 +30,14 @@ const colorClass = computed(() => {
const initial = computed(() => userInitial(props.name));
</script>
<template>
<div
class="rounded-xl flex leading-[100%] font-medium items-center justify-center text-center cursor-default"
:class="`h-[${size}px] w-[${size}px] ${colorClass}`"
:style="style"
aria-hidden="true"
>
<slot>{{ initial }}</slot>
</div>
</template>

View File

@@ -1,39 +1,3 @@
<template>
<with-label
:label="label"
:icon="icon"
:name="name"
:has-error="hasError"
:error-message="errorMessage"
>
<template #rightOfLabel>
<slot />
</template>
<input
:id="name"
:name="name"
:type="type"
autocomplete="off"
:tabindex="tabindex"
:required="required"
:placeholder="placeholder"
:data-testid="dataTestid"
:value="value"
:class="{
'focus:outline-red-600 outline-red-600 dark:focus:outline-red-600 dark:outline-red-600':
hasError,
'outline-slate-200 dark:outline-slate-600 dark:focus:outline-woot-500 focus:outline-woot-500':
!hasError,
'px-3 py-3': spacing === 'base',
'px-3 py-2 mb-0': spacing === 'compact',
'pl-9': icon,
}"
class="block w-full border-none rounded-md shadow-sm appearance-none outline outline-1 focus:outline-2 text-slate-900 dark:text-slate-100 placeholder:text-slate-400 sm:text-sm sm:leading-6 dark:bg-slate-800"
@input="onInput"
@blur="$emit('blur')"
/>
</with-label>
</template>
<script>
import WithLabel from './WithLabel.vue';
export default {
@@ -98,3 +62,40 @@ export default {
},
};
</script>
<template>
<WithLabel
:label="label"
:icon="icon"
:name="name"
:has-error="hasError"
:error-message="errorMessage"
>
<template #rightOfLabel>
<slot />
</template>
<input
:id="name"
:name="name"
:type="type"
autocomplete="off"
:tabindex="tabindex"
:required="required"
:placeholder="placeholder"
:data-testid="dataTestid"
:value="value"
:class="{
'focus:outline-red-600 outline-red-600 dark:focus:outline-red-600 dark:outline-red-600':
hasError,
'outline-slate-200 dark:outline-slate-600 dark:focus:outline-woot-500 focus:outline-woot-500':
!hasError,
'px-3 py-3': spacing === 'base',
'px-3 py-2 mb-0': spacing === 'compact',
'pl-9': icon,
}"
class="block w-full border-none rounded-md shadow-sm appearance-none outline outline-1 focus:outline-2 text-slate-900 dark:text-slate-100 placeholder:text-slate-400 sm:text-sm sm:leading-6 dark:bg-slate-800"
@input="onInput"
@blur="$emit('blur')"
/>
</WithLabel>
</template>

View File

@@ -1,3 +1,51 @@
<script setup>
import { computed, ref } from 'vue';
import InitialsAvatar from './InitialsAvatar.vue';
const props = defineProps({
src: {
type: String,
default: '',
},
name: {
type: String,
default: '',
},
});
const emit = defineEmits(['change', 'delete']);
const hasImageLoaded = ref(false);
const imageLoadedError = ref(false);
const fileInputRef = ref(null);
const shouldShowImage = computed(() => props.src && !imageLoadedError.value);
const onImageLoadError = () => {
imageLoadedError.value = true;
};
const onImageLoad = () => {
hasImageLoaded.value = true;
imageLoadedError.value = false;
};
const openFileInput = () => {
fileInputRef.value.click();
};
const onImageUpload = event => {
const [file] = event.target.files;
emit('change', {
file,
url: file ? URL.createObjectURL(file) : null,
});
};
const onAvatarDelete = () => {
emit('delete');
};
</script>
<template>
<div class="relative rounded-xl h-[72px] w-[72px] cursor-pointe group">
<img
@@ -9,7 +57,7 @@
@load="onImageLoad"
@error="onImageLoadError"
/>
<initials-avatar v-else-if="!shouldShowImage" :name="name" :size="72" />
<InitialsAvatar v-else-if="!shouldShowImage" :name="name" :size="72" />
<input
ref="fileInputRef"
@@ -35,50 +83,3 @@
</div>
</div>
</template>
<script setup>
import { computed, ref } from 'vue';
import InitialsAvatar from './InitialsAvatar.vue';
const props = defineProps({
src: {
type: String,
default: '',
},
name: {
type: String,
default: '',
},
});
const emits = defineEmits(['change', 'delete']);
const hasImageLoaded = ref(false);
const imageLoadedError = ref(false);
const fileInputRef = ref(null);
const shouldShowImage = computed(() => props.src && !imageLoadedError.value);
const onImageLoadError = () => {
imageLoadedError.value = true;
};
const onImageLoad = () => {
hasImageLoaded.value = true;
imageLoadedError.value = false;
};
const openFileInput = () => {
fileInputRef.value.click();
};
const onImageUpload = event => {
const [file] = event.target.files;
emits('change', {
file,
url: file ? URL.createObjectURL(file) : null,
});
};
const onAvatarDelete = () => {
emits('delete');
};
</script>

View File

@@ -1,25 +1,3 @@
<template>
<with-label
:label="label"
:name="name"
:has-error="hasError"
:error-message="errorMessage"
>
<div class="flex gap-2 flex-wrap">
<woot-button
v-for="option in options"
:key="option.value"
:variant="value === option.value ? '' : 'hollow'"
:color-scheme="value === option.value ? 'primary' : 'secondary'"
size="small"
@click="$emit('input', option.value)"
>
{{ option.label }}
</woot-button>
</div>
</with-label>
</template>
<script>
import WithLabel from './WithLabel.vue';
export default {
@@ -27,10 +5,6 @@ export default {
WithLabel,
},
props: {
id: {
type: String,
default: '',
},
options: {
type: Array,
default: () => [],
@@ -58,3 +32,25 @@ export default {
},
};
</script>
<template>
<WithLabel
:label="label"
:name="name"
:has-error="hasError"
:error-message="errorMessage"
>
<div class="flex flex-wrap gap-2">
<woot-button
v-for="option in options"
:key="option.value"
:variant="value === option.value ? '' : 'hollow'"
:color-scheme="value === option.value ? 'primary' : 'secondary'"
size="small"
@click="$emit('input', option.value)"
>
{{ option.label }}
</woot-button>
</div>
</WithLabel>
</template>

View File

@@ -1,35 +1,3 @@
<template>
<with-label
:label="label"
:icon="icon"
:name="name"
:has-error="hasError"
:error-message="errorMessage"
>
<select
:id="id"
:selected="value"
:name="name"
:class="{
'text-ash-400': !value,
'text-ash-900': value,
'pl-9': icon,
}"
class="block w-full px-3 py-2 pr-6 mb-0 border-0 shadow-sm outline-none appearance-none rounded-xl select-caret ring-ash-200 ring-1 ring-inset placeholder:text-ash-900 focus:ring-2 focus:ring-inset focus:ring-primary-500 sm:text-sm sm:leading-6"
@input="onInput"
>
<option value="" disabled selected class="hidden">
{{ placeholder }}
</option>
<slot>
<option v-for="opt in options" :key="opt.value" :value="opt.value">
{{ opt.label }}
</option>
</slot>
</select>
</with-label>
</template>
<script>
import WithLabel from './WithLabel.vue';
export default {
@@ -81,6 +49,39 @@ export default {
},
};
</script>
<template>
<WithLabel
:label="label"
:icon="icon"
:name="name"
:has-error="hasError"
:error-message="errorMessage"
>
<select
:id="id"
:selected="value"
:name="name"
:class="{
'text-ash-400': !value,
'text-ash-900': value,
'pl-9': icon,
}"
class="block w-full px-3 py-2 pr-6 mb-0 border-0 shadow-sm outline-none appearance-none rounded-xl select-caret ring-ash-200 ring-1 ring-inset placeholder:text-ash-900 focus:ring-2 focus:ring-inset focus:ring-primary-500 sm:text-sm sm:leading-6"
@input="onInput"
>
<option value="" disabled selected class="hidden">
{{ placeholder }}
</option>
<slot>
<option v-for="opt in options" :key="opt.value" :value="opt.value">
{{ opt.label }}
</option>
</slot>
</select>
</WithLabel>
</template>
<style scoped>
.select-caret {
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' version='1.1' width='32' height='24' viewBox='0 0 32 24'><polygon points='0,0 32,0 16,24' style='fill: rgb%28110, 111, 115%29'></polygon></svg>");

View File

@@ -1,3 +1,16 @@
<script>
export default {
props: {
value: { type: Boolean, default: false },
},
methods: {
onClick() {
this.$emit('input', !this.value);
},
},
};
</script>
<template>
<button
type="button"
@@ -20,16 +33,3 @@
/>
</button>
</template>
<script>
export default {
props: {
value: { type: Boolean, default: false },
},
methods: {
onClick() {
this.$emit('input', !this.value);
},
},
};
</script>

View File

@@ -1,31 +1,3 @@
<template>
<with-label
:label="label"
:name="name"
:has-error="hasError"
:error-message="errorMessage"
>
<textarea
:id="name"
:name="name"
autocomplete="off"
:required="required"
:placeholder="placeholder"
:data-testid="dataTestid"
:value="value"
:rows="rows"
:class="{
'focus:outline-red-600 outline-red-600': hasError,
'dark:outline-slate-600 dark:focus:outline-woot-500 outline-slate-200 focus:outline-woot-500':
!hasError,
'resize-none': !allowResize,
}"
class="block w-full p-3 bg-white border-none rounded-md shadow-sm appearance-none outline outline-1 focus:outline focus:outline-2 text-slate-900 dark:text-slate-100 placeholder:text-slate-400 dark:bg-slate-800"
@input="onInput"
@blur="$emit('blur')"
/>
</with-label>
</template>
<script>
import WithLabel from './WithLabel.vue';
export default {
@@ -81,3 +53,32 @@ export default {
},
};
</script>
<template>
<WithLabel
:label="label"
:name="name"
:has-error="hasError"
:error-message="errorMessage"
>
<textarea
:id="name"
:name="name"
autocomplete="off"
:required="required"
:placeholder="placeholder"
:data-testid="dataTestid"
:value="value"
:rows="rows"
:class="{
'focus:outline-red-600 outline-red-600': hasError,
'dark:outline-slate-600 dark:focus:outline-woot-500 outline-slate-200 focus:outline-woot-500':
!hasError,
'resize-none': !allowResize,
}"
class="block w-full p-3 bg-white border-none rounded-md shadow-sm appearance-none outline outline-1 focus:outline focus:outline-2 text-slate-900 dark:text-slate-100 placeholder:text-slate-400 dark:bg-slate-800"
@input="onInput"
@blur="$emit('blur')"
/>
</WithLabel>
</template>

View File

@@ -1,3 +1,30 @@
<script>
export default {
props: {
label: {
type: String,
default: '',
},
name: {
type: String,
required: true,
},
icon: {
type: String,
default: '',
},
hasError: {
type: Boolean,
default: false,
},
errorMessage: {
type: String,
default: '',
},
},
};
</script>
<template>
<div class="space-y-1">
<label
@@ -30,29 +57,3 @@
</div>
</div>
</template>
<script>
export default {
props: {
label: {
type: String,
default: '',
},
name: {
type: String,
required: true,
},
icon: {
type: String,
default: '',
},
hasError: {
type: Boolean,
default: false,
},
errorMessage: {
type: String,
default: '',
},
},
};
</script>

View File

@@ -1,23 +1,3 @@
<template>
<div class="flex flex-col">
<a
:href="getGoogleAuthUrl()"
class="inline-flex w-full justify-center rounded-md bg-white py-3 px-4 shadow-sm ring-1 ring-inset ring-slate-200 dark:ring-slate-600 hover:bg-slate-50 focus:outline-offset-0 dark:bg-slate-700 dark:hover:bg-slate-700"
>
<img src="/assets/images/auth/google.svg" alt="Google Logo" class="h-6" />
<span class="text-base font-medium ml-2 text-slate-600 dark:text-white">
{{ $t('LOGIN.OAUTH.GOOGLE_LOGIN') }}
</span>
</a>
<simple-divider
v-if="showSeparator"
ref="divider"
:label="$t('COMMON.OR')"
class="uppercase"
/>
</div>
</template>
<script>
import SimpleDivider from '../Divider/SimpleDivider.vue';
@@ -58,3 +38,25 @@ export default {
},
};
</script>
<!-- eslint-disable vue/no-unused-refs -->
<!-- Added ref for writing specs -->
<template>
<div class="flex flex-col">
<a
:href="getGoogleAuthUrl()"
class="inline-flex justify-center w-full px-4 py-3 bg-white rounded-md shadow-sm ring-1 ring-inset ring-slate-200 dark:ring-slate-600 hover:bg-slate-50 focus:outline-offset-0 dark:bg-slate-700 dark:hover:bg-slate-700"
>
<img src="/assets/images/auth/google.svg" alt="Google Logo" class="h-6" />
<span class="ml-2 text-base font-medium text-slate-600 dark:text-white">
{{ $t('LOGIN.OAUTH.GOOGLE_LOGIN') }}
</span>
</a>
<SimpleDivider
v-if="showSeparator"
ref="divider"
:label="$t('COMMON.OR')"
class="uppercase"
/>
</div>
</template>

View File

@@ -1,18 +1,3 @@
<template>
<transition-group
name="toast-fade"
tag="div"
class="fixed left-0 right-0 mx-auto overflow-hidden text-center top-10 z-50 max-w-[40rem]"
>
<snackbar-item
v-for="snackbarAlertMessage in snackbarAlertMessages"
:key="snackbarAlertMessage.key"
:message="snackbarAlertMessage.message"
:action="snackbarAlertMessage.action"
/>
</transition-group>
</template>
<script>
import { BUS_EVENTS } from 'shared/constants/busEvents';
import SnackbarItem from './Item.vue';
@@ -52,3 +37,18 @@ export default {
},
};
</script>
<template>
<transition-group
name="toast-fade"
tag="div"
class="fixed left-0 right-0 mx-auto overflow-hidden text-center top-10 z-50 max-w-[40rem]"
>
<SnackbarItem
v-for="snackbarAlertMessage in snackbarAlertMessages"
:key="snackbarAlertMessage.key"
:message="snackbarAlertMessage.message"
:action="snackbarAlertMessage.action"
/>
</transition-group>
</template>

View File

@@ -1,3 +1,20 @@
<script>
export default {
props: {
message: { type: String, default: '' },
action: {
type: Object,
default: () => ({}),
},
},
computed: {
isActionPresent() {
return this.action && this.action.message;
},
},
};
</script>
<template>
<div
class="bg-slate-900 dark:bg-slate-800 rounded-md drop-shadow-md mb-4 max-w-[40rem] inline-flex items-center min-w-[22rem] py-3 px-4"
@@ -13,25 +30,3 @@
</div>
</div>
</template>
<script>
export default {
props: {
message: { type: String, default: '' },
action: {
type: Object,
default: () => ({}),
},
showButton: Boolean,
duration: {
type: [String, Number],
default: 3000,
},
},
computed: {
isActionPresent() {
return this.action && this.action.message;
},
},
};
</script>