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,27 +1,3 @@
<template>
<div
v-if="globalConfig.brandName && !disableBranding"
class="px-0 py-3 flex justify-center"
>
<a
:href="brandRedirectURL"
rel="noreferrer noopener nofollow"
target="_blank"
class="branding--link justify-center items-center leading-3"
>
<img
class="branding--image"
:alt="globalConfig.brandName"
:src="globalConfig.logoThumbnail"
/>
<span>
{{ useInstallationName($t('POWERED_BY'), globalConfig.brandName) }}
</span>
</a>
</div>
<div v-else class="p-3" />
</template>
<script>
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
@@ -68,6 +44,30 @@ export default {
};
</script>
<template>
<div
v-if="globalConfig.brandName && !disableBranding"
class="px-0 py-3 flex justify-center"
>
<a
:href="brandRedirectURL"
rel="noreferrer noopener nofollow"
target="_blank"
class="branding--link justify-center items-center leading-3"
>
<img
class="branding--image"
:alt="globalConfig.brandName"
:src="globalConfig.logoThumbnail"
/>
<span>
{{ useInstallationName($t('POWERED_BY'), globalConfig.brandName) }}
</span>
</a>
</div>
<div v-else class="p-3" />
</template>
<style scoped lang="scss">
@import '~widget/assets/scss/variables.scss';

View File

@@ -1,13 +1,3 @@
<template>
<button
:class="buttonClassName"
:style="buttonStyles"
:disabled="disabled"
@click="onClick"
>
<slot />
</button>
</template>
<script>
export default {
props: {
@@ -65,3 +55,14 @@ export default {
},
};
</script>
<template>
<button
:class="buttonClassName"
:style="buttonStyles"
:disabled="disabled"
@click="onClick"
>
<slot />
</button>
</template>

View File

@@ -1,29 +1,3 @@
<template>
<a
v-if="isLink"
:key="action.uri"
class="action-button button"
:href="action.uri"
:style="{
background: widgetColor,
borderColor: widgetColor,
color: textColor,
}"
target="_blank"
rel="noopener nofollow noreferrer"
>
{{ action.text }}
</a>
<button
v-else
:key="action.payload"
class="action-button button"
:style="{ borderColor: widgetColor, color: widgetColor }"
@click="onClick"
>
{{ action.text }}
</button>
</template>
<script>
import { mapGetters } from 'vuex';
import { getContrastingTextColor } from '@chatwoot/utils';
@@ -54,6 +28,33 @@ export default {
};
</script>
<template>
<a
v-if="isLink"
:key="action.uri"
class="action-button button"
:href="action.uri"
:style="{
background: widgetColor,
borderColor: widgetColor,
color: textColor,
}"
target="_blank"
rel="noopener nofollow noreferrer"
>
{{ action.text }}
</a>
<button
v-else
:key="action.payload"
class="action-button button"
:style="{ borderColor: widgetColor, color: widgetColor }"
@click="onClick"
>
{{ action.text }}
</button>
</template>
<style scoped lang="scss">
@import '~widget/assets/scss/variables.scss';
@import '~dashboard/assets/scss/mixins.scss';

View File

@@ -1,25 +1,3 @@
<template>
<div
class="card-message chat-bubble agent"
:class="$dm('bg-white', 'dark:bg-slate-700')"
>
<img class="media" :src="mediaUrl" />
<div class="card-body">
<h4 class="title" :class="$dm('text-black-900', 'dark:text-slate-50')">
{{ title }}
</h4>
<p class="body" :class="$dm('text-black-700', 'dark:text-slate-100')">
{{ description }}
</p>
<card-button
v-for="action in actions"
:key="action.id"
:action="action"
/>
</div>
</div>
</template>
<script>
import CardButton from 'shared/components/CardButton.vue';
import darkModeMixin from 'widget/mixins/darkModeMixin.js';
@@ -46,12 +24,29 @@ export default {
type: Array,
default: () => [],
},
showAvatar: Boolean,
},
computed: {},
};
</script>
<template>
<div
class="card-message chat-bubble agent"
:class="$dm('bg-white', 'dark:bg-slate-700')"
>
<img class="media" :src="mediaUrl" />
<div class="card-body">
<h4 class="title" :class="$dm('text-black-900', 'dark:text-slate-50')">
{{ title }}
</h4>
<p class="body" :class="$dm('text-black-700', 'dark:text-slate-100')">
{{ description }}
</p>
<CardButton v-for="action in actions" :key="action.id" :action="action" />
</div>
</div>
</template>
<style scoped lang="scss">
@import '~widget/assets/scss/variables.scss';
@import '~dashboard/assets/scss/mixins.scss';

View File

@@ -1,3 +1,82 @@
<script>
import { mapGetters } from 'vuex';
import { getContrastingTextColor } from '@chatwoot/utils';
import darkModeMixin from 'widget/mixins/darkModeMixin';
export default {
mixins: [darkModeMixin],
props: {
buttonLabel: {
type: String,
default: '',
},
items: {
type: Array,
default: () => [],
},
submittedValues: {
type: Array,
default: () => [],
},
},
data() {
return {
formValues: {},
hasSubmitted: false,
};
},
computed: {
...mapGetters({
widgetColor: 'appConfig/getWidgetColor',
}),
textColor() {
return getContrastingTextColor(this.widgetColor);
},
inputColor() {
return `${this.$dm('bg-white', 'dark:bg-slate-600')}
${this.$dm('text-black-900', 'dark:text-slate-50')}`;
},
isFormValid() {
return this.items.reduce((acc, { name }) => {
return !!this.formValues[name] && acc;
}, true);
},
},
mounted() {
if (this.submittedValues.length) {
this.updateFormValues();
} else {
this.setFormDefaults();
}
},
methods: {
onSubmitClick() {
this.hasSubmitted = true;
},
onSubmit() {
if (!this.isFormValid) {
return;
}
this.$emit('submit', this.formValues);
},
buildFormObject(formObjectArray) {
return formObjectArray.reduce((acc, obj) => {
return {
...acc,
[obj.name]: obj.value || obj.default,
};
}, {});
},
updateFormValues() {
this.formValues = this.buildFormObject(this.submittedValues);
},
setFormDefaults() {
this.formValues = this.buildFormObject(this.items);
},
},
};
</script>
<template>
<div
class="form chat-bubble agent"
@@ -84,85 +163,6 @@
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import { getContrastingTextColor } from '@chatwoot/utils';
import darkModeMixin from 'widget/mixins/darkModeMixin';
export default {
mixins: [darkModeMixin],
props: {
buttonLabel: {
type: String,
default: '',
},
items: {
type: Array,
default: () => [],
},
submittedValues: {
type: Array,
default: () => [],
},
},
data() {
return {
formValues: {},
hasSubmitted: false,
};
},
computed: {
...mapGetters({
widgetColor: 'appConfig/getWidgetColor',
}),
textColor() {
return getContrastingTextColor(this.widgetColor);
},
inputColor() {
return `${this.$dm('bg-white', 'dark:bg-slate-600')}
${this.$dm('text-black-900', 'dark:text-slate-50')}`;
},
isFormValid() {
return this.items.reduce((acc, { name }) => {
return !!this.formValues[name] && acc;
}, true);
},
},
mounted() {
if (this.submittedValues.length) {
this.updateFormValues();
} else {
this.setFormDefaults();
}
},
methods: {
onSubmitClick() {
this.hasSubmitted = true;
},
onSubmit() {
if (!this.isFormValid) {
return;
}
this.$emit('submit', this.formValues);
},
buildFormObject(formObjectArray) {
return formObjectArray.reduce((acc, obj) => {
return {
...acc,
[obj.name]: obj.value || obj.default,
};
}, {});
},
updateFormValues() {
this.formValues = this.buildFormObject(this.submittedValues);
},
setFormDefaults() {
this.formValues = this.buildFormObject(this.items);
},
},
};
</script>
<style scoped lang="scss">
@import '~widget/assets/scss/variables.scss';

View File

@@ -1,15 +1,3 @@
<template>
<li
class="option"
:class="{ 'is-selected': isSelected }"
:style="{ borderColor: widgetColor }"
>
<button class="option-button button" @click="onClick">
<span :style="{ color: widgetColor }">{{ action.title }}</span>
</button>
</li>
</template>
<script>
import { mapGetters } from 'vuex';
@@ -38,6 +26,18 @@ export default {
};
</script>
<template>
<li
class="option"
:class="{ 'is-selected': isSelected }"
:style="{ borderColor: widgetColor }"
>
<button class="option-button button" @click="onClick">
<span :style="{ color: widgetColor }">{{ action.title }}</span>
</button>
</li>
</template>
<style scoped lang="scss">
@import '~widget/assets/scss/variables.scss';

View File

@@ -1,29 +1,3 @@
<template>
<div class="options-message chat-bubble agent bg-white dark:bg-slate-700">
<div class="card-body">
<h4 class="title text-black-900 dark:text-slate-50">
<div
v-dompurify-html="formatMessage(title, false)"
class="message-content text-black-900 dark:text-slate-50"
/>
</h4>
<ul
v-if="!hideFields"
class="options"
:class="{ 'has-selected': !!selected }"
>
<chat-option
v-for="option in options"
:key="option.id"
:action="option"
:is-selected="isSelected(option)"
@click="onClick"
/>
</ul>
</div>
</div>
</template>
<script>
import ChatOption from 'shared/components/ChatOption.vue';
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
@@ -62,6 +36,32 @@ export default {
};
</script>
<template>
<div class="options-message chat-bubble agent bg-white dark:bg-slate-700">
<div class="card-body">
<h4 class="title text-black-900 dark:text-slate-50">
<div
v-dompurify-html="formatMessage(title, false)"
class="message-content text-black-900 dark:text-slate-50"
/>
</h4>
<ul
v-if="!hideFields"
class="options"
:class="{ 'has-selected': !!selected }"
>
<ChatOption
v-for="option in options"
:key="option.id"
:action="option"
:is-selected="isSelected(option)"
@click="onClick"
/>
</ul>
</div>
</div>
</template>
<style lang="scss">
@import '~dashboard/assets/scss/variables.scss';
.has-selected {
@@ -71,6 +71,7 @@ export default {
}
}
</style>
<style scoped lang="scss">
@import '~widget/assets/scss/variables.scss';

View File

@@ -1,50 +1,3 @@
<template>
<div
class="customer-satisfaction"
:class="$dm('bg-white', 'dark:bg-slate-700')"
:style="{ borderColor: widgetColor }"
>
<h6 class="title" :class="$dm('text-slate-900', 'dark:text-slate-50')">
{{ title }}
</h6>
<div class="ratings">
<button
v-for="rating in ratings"
:key="rating.key"
:class="buttonClass(rating)"
@click="selectRating(rating)"
>
{{ rating.emoji }}
</button>
</div>
<form
v-if="!isFeedbackSubmitted"
class="feedback-form"
@submit.prevent="onSubmit()"
>
<input
v-model="feedback"
class="form-input"
:class="inputColor"
:placeholder="$t('CSAT.PLACEHOLDER')"
@keydown.enter="onSubmit"
/>
<button
class="button small"
:disabled="isButtonDisabled"
:style="{
background: widgetColor,
borderColor: widgetColor,
color: textColor,
}"
>
<spinner v-if="isUpdating && feedback" />
<fluent-icon v-else icon="chevron-right" />
</button>
</form>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import Spinner from 'shared/components/Spinner.vue';
@@ -149,6 +102,53 @@ export default {
};
</script>
<template>
<div
class="customer-satisfaction"
:class="$dm('bg-white', 'dark:bg-slate-700')"
:style="{ borderColor: widgetColor }"
>
<h6 class="title" :class="$dm('text-slate-900', 'dark:text-slate-50')">
{{ title }}
</h6>
<div class="ratings">
<button
v-for="rating in ratings"
:key="rating.key"
:class="buttonClass(rating)"
@click="selectRating(rating)"
>
{{ rating.emoji }}
</button>
</div>
<form
v-if="!isFeedbackSubmitted"
class="feedback-form"
@submit.prevent="onSubmit()"
>
<input
v-model="feedback"
class="form-input"
:class="inputColor"
:placeholder="$t('CSAT.PLACEHOLDER')"
@keydown.enter="onSubmit"
/>
<button
class="button small"
:disabled="isButtonDisabled"
:style="{
background: widgetColor,
borderColor: widgetColor,
color: textColor,
}"
>
<Spinner v-if="isUpdating && feedback" />
<FluentIcon v-else icon="chevron-right" />
</button>
</form>
</div>
</template>
<style lang="scss" scoped>
@import '~widget/assets/scss/variables.scss';
@import '~widget/assets/scss/mixins.scss';

View File

@@ -1,12 +1,3 @@
<template>
<div
class="date--separator"
:class="$dm('text-slate-700', 'dark:text-slate-200')"
>
{{ formattedDate }}
</div>
</template>
<script>
import { formatDate } from 'shared/helpers/DateHelper';
import darkModeMixin from 'widget/mixins/darkModeMixin.js';
@@ -31,6 +22,15 @@ export default {
};
</script>
<template>
<div
class="date--separator"
:class="$dm('text-slate-700', 'dark:text-slate-200')"
>
{{ formattedDate }}
</div>
</template>
<style lang="scss" scoped>
@import '~widget/assets/scss/variables';

View File

@@ -1,14 +1,3 @@
<template>
<i v-if="showEmoji" :class="className">{{ iconContent }}</i>
<fluent-icon
v-else-if="showIcon"
:size="iconSize"
:icon="icon"
class="flex-shrink-0"
:class="className"
/>
</template>
<script>
// 🚨 This component is deprecated. Please use fluent-icon instead.
import { hasEmojiSupport } from 'shared/helpers/emoji';
@@ -46,6 +35,17 @@ export default {
};
</script>
<template>
<i v-if="showEmoji" :class="className">{{ iconContent }}</i>
<fluent-icon
v-else-if="showIcon"
:size="iconSize"
:icon="icon"
class="flex-shrink-0"
:class="className"
/>
</template>
<style lang="scss" scoped>
.icon--emoji {
font-style: normal;

View File

@@ -1,13 +1,3 @@
<template>
<base-icon
:size="size"
:icon="icon"
:type="type"
:icons="icons"
:view-box="viewBox"
:icon-lib="iconLib"
/>
</template>
<script>
import BaseIcon from './Icon.vue';
import icons from './dashboard-icons.json';
@@ -44,3 +34,14 @@ export default {
},
};
</script>
<template>
<BaseIcon
:size="size"
:icon="icon"
:type="type"
:icons="icons"
:view-box="viewBox"
:icon-lib="iconLib"
/>
</template>

View File

@@ -1,37 +1,3 @@
<template>
<svg
v-if="iconLib === 'fluent'"
:width="size"
:height="size"
fill="none"
:viewBox="viewBox"
xmlns="http://www.w3.org/2000/svg"
>
<path
v-for="source in pathSource"
:key="source"
:d="source"
fill="currentColor"
/>
</svg>
<svg
v-else
:width="size"
:height="size"
fill="none"
:viewBox="viewBox"
xmlns="http://www.w3.org/2000/svg"
>
<g v-for="(pathData, index) in pathSource" :key="index">
<path
:key="pathData"
:d="pathData"
stroke="currentColor"
stroke-width="1.66667"
/>
</g>
</svg>
</template>
<script>
export default {
props: {
@@ -73,3 +39,38 @@ export default {
},
};
</script>
<template>
<svg
v-if="iconLib === 'fluent'"
:width="size"
:height="size"
fill="none"
:viewBox="viewBox"
xmlns="http://www.w3.org/2000/svg"
>
<path
v-for="source in pathSource"
:key="source"
:d="source"
fill="currentColor"
/>
</svg>
<svg
v-else
:width="size"
:height="size"
fill="none"
:viewBox="viewBox"
xmlns="http://www.w3.org/2000/svg"
>
<g v-for="(pathData, index) in pathSource" :key="index">
<path
:key="pathData"
:d="pathData"
stroke="currentColor"
stroke-width="1.66667"
/>
</g>
</svg>
</template>

View File

@@ -1,13 +1,3 @@
<template>
<base-icon
:size="size"
:icon="icon"
:type="type"
:icons="icons"
:view-box="viewBox"
:icon-lib="iconLib"
/>
</template>
<script>
import BaseIcon from './Icon.vue';
import icons from './icons.json';
@@ -44,3 +34,14 @@ export default {
},
};
</script>
<template>
<BaseIcon
:size="size"
:icon="icon"
:type="type"
:icons="icons"
:view-box="viewBox"
:icon-lib="iconLib"
/>
</template>

View File

@@ -1,32 +1,3 @@
<template>
<section class="w-3/4">
<div
v-if="richtext"
class="py-0 px-4 rounded-md border border-solid border-slate-200 dark:border-slate-600 bg-white dark:bg-slate-900 mt-0 mx-0 mb-4"
>
<woot-message-editor
v-model="greetingsMessage"
:is-format-mode="true"
:enable-variables="true"
class="input bg-white dark:bg-slate-900"
:placeholder="placeholder"
:min-height="4"
@input="handleInput"
/>
</div>
<resizable-text-area
v-else
v-model="greetingsMessage"
:rows="4"
type="text"
class="greetings--textarea"
:label="label"
:placeholder="placeholder"
@input="handleInput"
/>
</section>
</template>
<script>
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
import ResizableTextArea from 'shared/components/ResizableTextArea.vue';
@@ -71,3 +42,32 @@ export default {
},
};
</script>
<template>
<section class="w-3/4">
<div
v-if="richtext"
class="px-4 py-0 mx-0 mt-0 mb-4 bg-white border border-solid rounded-md border-slate-200 dark:border-slate-600 dark:bg-slate-900"
>
<WootMessageEditor
v-model="greetingsMessage"
is-format-mode
enable-variables
class="bg-white input dark:bg-slate-900"
:placeholder="placeholder"
:min-height="4"
@input="handleInput"
/>
</div>
<ResizableTextArea
v-else
v-model="greetingsMessage"
:rows="4"
type="text"
class="greetings--textarea"
:label="label"
:placeholder="placeholder"
@input="handleInput"
/>
</section>
</template>

View File

@@ -1,25 +1,3 @@
<template>
<div class="relative overflow-hidden pb-1/2 h-full">
<iframe
v-if="url"
:src="url"
class="absolute w-full h-full top-0 left-0"
@load="handleIframeLoad"
@error="handleIframeError"
/>
<article-skeleton-loader
v-if="isLoading"
class="absolute w-full h-full top-0 left-0"
/>
<div
v-if="showEmptyState"
class="absolute w-full h-full top-0 left-0 flex justify-center items-center"
>
<p>{{ $t('PORTAL.IFRAME_ERROR') }}</p>
</div>
</div>
</template>
<script>
import ArticleSkeletonLoader from 'shared/components/ArticleSkeletonLoader.vue';
@@ -53,3 +31,25 @@ export default {
},
};
</script>
<template>
<div class="relative overflow-hidden pb-1/2 h-full">
<iframe
v-if="url"
:src="url"
class="absolute w-full h-full top-0 left-0"
@load="handleIframeLoad"
@error="handleIframeError"
/>
<ArticleSkeletonLoader
v-if="isLoading"
class="absolute w-full h-full top-0 left-0"
/>
<div
v-if="showEmptyState"
class="absolute w-full h-full top-0 left-0 flex justify-center items-center"
>
<p>{{ $t('PORTAL.IFRAME_ERROR') }}</p>
</div>
</div>
</template>

View File

@@ -1,16 +1,3 @@
<template>
<textarea
ref="textarea"
:placeholder="placeholder"
:rows="rows"
:value="value"
@input="onInput"
@focus="onFocus"
@keyup="onKeyup"
@blur="onBlur"
/>
</template>
<script>
import {
appendSignature,
@@ -57,10 +44,10 @@ export default {
return {
typingIndicator: createTypingIndicator(
() => {
this.$emit('typing-on');
this.$emit('typingOn');
},
() => {
this.$emit('typing-off');
this.$emit('typingOff');
},
TYPING_INDICATOR_IDLE_TIME
),
@@ -162,3 +149,16 @@ export default {
},
};
</script>
<template>
<textarea
ref="textarea"
:placeholder="placeholder"
:rows="rows"
:value="value"
@input="onInput"
@focus="onFocus"
@keyup="onKeyup"
@blur="onBlur"
/>
</template>

View File

@@ -1,6 +1,3 @@
<template>
<span class="spinner" :class="`${size} ${colorSchemeClasses}`" />
</template>
<script>
export default {
props: {
@@ -32,6 +29,11 @@ export default {
},
};
</script>
<template>
<span class="spinner" :class="`${size} ${colorSchemeClasses}`" />
</template>
<style scoped lang="scss">
@import '~widget/assets/scss/variables';

View File

@@ -1,31 +1,3 @@
<template>
<label class="block">
<div
v-if="label"
class="mb-2 text-xs font-medium"
:class="{
'text-black-800': !error,
'text-red-400': error,
}"
>
{{ label }}
</div>
<textarea
class="resize-none border rounded w-full py-2 px-3 text-slate-700 leading-tight outline-none"
:class="{
'border-black-200 hover:border-black-300 focus:border-black-300':
!error,
'border-red-200 hover:border-red-300 focus:border-red-300': error,
}"
:placeholder="placeholder"
:value="value"
@change="onChange"
/>
<div v-if="error" class="text-red-400 mt-2 text-xs font-medium">
{{ error }}
</div>
</label>
</template>
<script>
export default {
props: {
@@ -33,10 +5,6 @@ export default {
type: String,
default: '',
},
type: {
type: String,
default: 'text',
},
placeholder: {
type: String,
default: '',
@@ -57,6 +25,36 @@ export default {
},
};
</script>
<template>
<label class="block">
<div
v-if="label"
class="mb-2 text-xs font-medium"
:class="{
'text-black-800': !error,
'text-red-400': error,
}"
>
{{ label }}
</div>
<textarea
class="w-full px-3 py-2 leading-tight border rounded outline-none resize-none text-slate-700"
:class="{
'border-black-200 hover:border-black-300 focus:border-black-300':
!error,
'border-red-200 hover:border-red-300 focus:border-red-300': error,
}"
:placeholder="placeholder"
:value="value"
@change="onChange"
/>
<div v-if="error" class="mt-2 text-xs font-medium text-red-400">
{{ error }}
</div>
</label>
</template>
<style lang="scss" scoped>
textarea {
min-height: 8rem;

View File

@@ -1,102 +1,3 @@
<template>
<div
role="dialog"
class="emoji-dialog bg-white shadow-lg dark:bg-slate-900 rounded-md border border-solid border-slate-75 dark:border-slate-800/50 box-content h-[300px] absolute right-0 -top-[95px] w-80 z-20"
>
<div class="flex flex-col">
<div class="flex gap-2 emoji-search--wrap">
<input
ref="searchbar"
v-model="search"
type="text"
class="emoji-search--input focus:box-shadow-blue dark:focus:box-shadow-dark !mb-0"
:placeholder="$t('EMOJI.PLACEHOLDER')"
/>
<woot-button
v-if="showRemoveButton"
size="small"
variant="smooth"
class="dark:!bg-slate-800 dark:!hover:bg-slate-700"
color-scheme="secondary"
@click="onClick('')"
>
{{ $t('EMOJI.REMOVE') }}
</woot-button>
</div>
<div v-if="hasNoSearch" ref="emojiItem" class="emoji-item">
<h5 class="emoji-category--title">
{{ selectedKey }}
</h5>
<div class="emoji--row">
<button
v-for="item in filterEmojisByCategory"
:key="item.slug"
v-dompurify-html="item.emoji"
class="emoji--item"
track-by="$index"
@click="onClick(item.emoji)"
/>
</div>
</div>
<div v-else ref="emojiItem" class="emoji-item">
<div v-for="category in filterAllEmojisBySearch" :key="category.slug">
<h5 v-if="category.emojis.length > 0" class="emoji-category--title">
{{ category.name }}
</h5>
<div v-if="category.emojis.length > 0" class="emoji--row">
<button
v-for="item in category.emojis"
:key="item.slug"
v-dompurify-html="item.emoji"
class="emoji--item"
track-by="$index"
@click="onClick(item.emoji)"
/>
</div>
</div>
<div v-if="hasEmptySearchResult" class="empty-message">
<div class="emoji-icon">
<fluent-icon icon="emoji" size="48" />
</div>
<span class="empty-message--text">
{{ $t('EMOJI.NOT_FOUND') }}
</span>
</div>
</div>
<div class="emoji-dialog--footer" role="menu">
<ul>
<li>
<button
class="emoji--item"
:class="{ active: selectedKey === 'Search' }"
@click="changeCategory('Search')"
>
<fluent-icon
icon="search"
size="16"
class="text-slate-700 dark:text-slate-100"
/>
</button>
</li>
<li
v-for="category in categories"
:key="category.slug"
@click="changeCategory(category.name)"
>
<button
v-dompurify-html="getFirstEmojiByCategoryName(category.name)"
class="emoji--item"
:class="{ active: selectedKey === category.name }"
@click="changeCategory(category.name)"
/>
</li>
</ul>
</div>
</div>
</div>
</template>
<script>
import emojis from './emojisGroup.json';
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
@@ -181,6 +82,106 @@ export default {
},
};
</script>
<template>
<div
role="dialog"
class="emoji-dialog bg-white shadow-lg dark:bg-slate-900 rounded-md border border-solid border-slate-75 dark:border-slate-800/50 box-content h-[300px] absolute right-0 -top-[95px] w-80 z-20"
>
<div class="flex flex-col">
<div class="flex gap-2 emoji-search--wrap">
<input
ref="searchbar"
v-model="search"
type="text"
class="emoji-search--input focus:box-shadow-blue dark:focus:box-shadow-dark !mb-0 !h-8 !text-sm"
:placeholder="$t('EMOJI.PLACEHOLDER')"
/>
<woot-button
v-if="showRemoveButton"
size="small"
variant="smooth"
class="dark:!bg-slate-800 dark:!hover:bg-slate-700"
color-scheme="secondary"
@click="onClick('')"
>
{{ $t('EMOJI.REMOVE') }}
</woot-button>
</div>
<div v-if="hasNoSearch" ref="emojiItem" class="emoji-item">
<h5 class="emoji-category--title">
{{ selectedKey }}
</h5>
<div class="emoji--row">
<button
v-for="item in filterEmojisByCategory"
:key="item.slug"
v-dompurify-html="item.emoji"
class="emoji--item"
track-by="$index"
@click="onClick(item.emoji)"
/>
</div>
</div>
<div v-else ref="emojiItem" class="emoji-item">
<div v-for="category in filterAllEmojisBySearch" :key="category.slug">
<h5 v-if="category.emojis.length > 0" class="emoji-category--title">
{{ category.name }}
</h5>
<div v-if="category.emojis.length > 0" class="emoji--row">
<button
v-for="item in category.emojis"
:key="item.slug"
v-dompurify-html="item.emoji"
class="emoji--item"
track-by="$index"
@click="onClick(item.emoji)"
/>
</div>
</div>
<div v-if="hasEmptySearchResult" class="empty-message">
<div class="emoji-icon">
<FluentIcon icon="emoji" size="48" />
</div>
<span class="empty-message--text">
{{ $t('EMOJI.NOT_FOUND') }}
</span>
</div>
</div>
<div class="emoji-dialog--footer" role="menu">
<ul>
<li>
<button
class="emoji--item"
:class="{ active: selectedKey === 'Search' }"
@click="changeCategory('Search')"
>
<FluentIcon
icon="search"
size="16"
class="text-slate-700 dark:text-slate-100"
/>
</button>
</li>
<li
v-for="category in categories"
:key="category.slug"
@click="changeCategory(category.name)"
>
<button
v-dompurify-html="getFirstEmojiByCategoryName(category.name)"
class="emoji--item"
:class="{ active: selectedKey === category.name }"
@click="changeCategory(category.name)"
/>
</li>
</ul>
</div>
</div>
</div>
</template>
<style scoped>
@tailwind components;
@layer components {
@@ -197,6 +198,7 @@ export default {
}
}
</style>
<style lang="scss">
@import '~dashboard/assets/scss/mixins';

View File

@@ -1,76 +1,3 @@
<template>
<div
v-on-clickaway="onCloseDropdown"
class="relative w-full mb-2"
@keyup.esc="onCloseDropdown"
>
<woot-button
variant="hollow"
color-scheme="secondary"
class="w-full border border-solid border-slate-100 dark:border-slate-700 px-2 hover:border-slate-75 dark:hover:border-slate-600"
@click="toggleDropdown"
>
<div class="flex gap-1">
<Thumbnail
v-if="hasValue && hasThumbnail"
:src="selectedItem.thumbnail"
size="24px"
:status="selectedItem.availability_status"
:username="selectedItem.name"
/>
<div class="flex justify-between w-full min-w-0 items-center">
<h4
v-if="!hasValue"
class="text-ellipsis text-sm text-slate-800 dark:text-slate-100"
>
{{ multiselectorPlaceholder }}
</h4>
<h4
v-else
class="items-center leading-tight overflow-hidden whitespace-nowrap text-ellipsis text-sm text-slate-800 dark:text-slate-100"
:title="selectedItem.name"
>
{{ selectedItem.name }}
</h4>
<i
v-if="showSearchDropdown"
class="icon ion-chevron-up text-slate-600 mr-1"
/>
<i v-else class="icon ion-chevron-down text-slate-600 mr-1" />
</div>
</div>
</woot-button>
<div
:class="{ 'dropdown-pane--open': showSearchDropdown }"
class="dropdown-pane"
>
<div class="flex justify-between items-center mb-1">
<h4
class="text-sm text-slate-800 dark:text-slate-100 m-0 overflow-hidden whitespace-nowrap text-ellipsis"
>
{{ multiselectorTitle }}
</h4>
<woot-button
icon="dismiss"
size="tiny"
color-scheme="secondary"
variant="clear"
@click="onCloseDropdown"
/>
</div>
<multiselect-dropdown-items
v-if="showSearchDropdown"
:options="options"
:selected-items="[selectedItem]"
:has-thumbnail="hasThumbnail"
:input-placeholder="inputPlaceholder"
:no-search-result="noSearchResult"
@click="onClickSelectItem"
/>
</div>
</div>
</template>
<script>
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
import MultiselectDropdownItems from 'shared/components/ui/MultiselectDropdownItems.vue';
@@ -139,6 +66,79 @@ export default {
};
</script>
<template>
<div
v-on-clickaway="onCloseDropdown"
class="relative w-full mb-2"
@keyup.esc="onCloseDropdown"
>
<woot-button
variant="hollow"
color-scheme="secondary"
class="w-full border border-solid border-slate-100 dark:border-slate-700 px-2 hover:border-slate-75 dark:hover:border-slate-600"
@click="toggleDropdown"
>
<div class="flex gap-1">
<Thumbnail
v-if="hasValue && hasThumbnail"
:src="selectedItem.thumbnail"
size="24px"
:status="selectedItem.availability_status"
:username="selectedItem.name"
/>
<div class="flex justify-between w-full min-w-0 items-center">
<h4
v-if="!hasValue"
class="text-ellipsis text-sm text-slate-800 dark:text-slate-100"
>
{{ multiselectorPlaceholder }}
</h4>
<h4
v-else
class="items-center leading-tight overflow-hidden whitespace-nowrap text-ellipsis text-sm text-slate-800 dark:text-slate-100"
:title="selectedItem.name"
>
{{ selectedItem.name }}
</h4>
<i
v-if="showSearchDropdown"
class="icon ion-chevron-up text-slate-600 mr-1"
/>
<i v-else class="icon ion-chevron-down text-slate-600 mr-1" />
</div>
</div>
</woot-button>
<div
:class="{ 'dropdown-pane--open': showSearchDropdown }"
class="dropdown-pane"
>
<div class="flex justify-between items-center mb-1">
<h4
class="text-sm text-slate-800 dark:text-slate-100 m-0 overflow-hidden whitespace-nowrap text-ellipsis"
>
{{ multiselectorTitle }}
</h4>
<woot-button
icon="dismiss"
size="tiny"
color-scheme="secondary"
variant="clear"
@click="onCloseDropdown"
/>
</div>
<MultiselectDropdownItems
v-if="showSearchDropdown"
:options="options"
:selected-items="[selectedItem]"
:has-thumbnail="hasThumbnail"
:input-placeholder="inputPlaceholder"
:no-search-result="noSearchResult"
@click="onClickSelectItem"
/>
</div>
</div>
</template>
<style lang="scss" scoped>
.dropdown-pane {
@apply box-border top-[2.625rem] w-full;

View File

@@ -1,66 +1,3 @@
<template>
<div class="dropdown-wrap">
<div class="mb-2 flex-shrink-0 flex-grow-0 flex-auto max-h-8">
<input
ref="searchbar"
v-model="search"
type="text"
class="search-input"
autofocus="true"
:placeholder="inputPlaceholder"
/>
</div>
<div class="flex justify-start items-start flex-auto overflow-auto">
<div class="w-full max-h-[10rem]">
<woot-dropdown-menu>
<woot-dropdown-item
v-for="option in filteredOptions"
:key="option.id"
>
<woot-button
class="multiselect-dropdown--item"
:variant="isActive(option) ? 'hollow' : 'clear'"
color-scheme="secondary"
:class="{
active: isActive(option),
}"
@click="() => onclick(option)"
>
<div class="flex items-center gap-1.5">
<Thumbnail
v-if="hasThumbnail"
:src="option.thumbnail"
size="24px"
:username="option.name"
:status="option.availability_status"
has-border
/>
<div
class="flex items-center justify-between w-full min-w-0 gap-2"
>
<span
class="leading-4 my-0 overflow-hidden whitespace-nowrap text-ellipsis text-sm"
:title="option.name"
>
{{ option.name }}
</span>
<fluent-icon v-if="isActive(option)" icon="checkmark" />
</div>
</div>
</woot-button>
</woot-dropdown-item>
</woot-dropdown-menu>
<h4
v-if="noResult"
class="w-full justify-center items-center flex text-slate-500 dark:text-slate-300 py-2 px-2.5 overflow-hidden whitespace-nowrap text-ellipsis text-sm"
>
{{ noSearchResult }}
</h4>
</div>
</div>
</div>
</template>
<script>
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
@@ -131,6 +68,66 @@ export default {
};
</script>
<template>
<div class="dropdown-wrap">
<div class="mb-2 flex-shrink-0 flex-grow-0 flex-auto max-h-8">
<input
ref="searchbar"
v-model="search"
type="text"
class="search-input"
autofocus="true"
:placeholder="inputPlaceholder"
/>
</div>
<div class="flex justify-start items-start flex-auto overflow-auto">
<div class="w-full max-h-[10rem]">
<WootDropdownMenu>
<WootDropdownItem v-for="option in filteredOptions" :key="option.id">
<woot-button
class="multiselect-dropdown--item"
:variant="isActive(option) ? 'hollow' : 'clear'"
color-scheme="secondary"
:class="{
active: isActive(option),
}"
@click="() => onclick(option)"
>
<div class="flex items-center gap-1.5">
<Thumbnail
v-if="hasThumbnail"
:src="option.thumbnail"
size="24px"
:username="option.name"
:status="option.availability_status"
has-border
/>
<div
class="flex items-center justify-between w-full min-w-0 gap-2"
>
<span
class="leading-4 my-0 overflow-hidden whitespace-nowrap text-ellipsis text-sm"
:title="option.name"
>
{{ option.name }}
</span>
<fluent-icon v-if="isActive(option)" icon="checkmark" />
</div>
</div>
</woot-button>
</WootDropdownItem>
</WootDropdownMenu>
<h4
v-if="noResult"
class="w-full justify-center items-center flex text-slate-500 dark:text-slate-300 py-2 px-2.5 overflow-hidden whitespace-nowrap text-ellipsis text-sm"
>
{{ noSearchResult }}
</h4>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.dropdown-wrap {
@apply w-full flex flex-col max-h-[12.5rem];

View File

@@ -1,3 +1,13 @@
<script>
export default {
methods: {
addLabel() {
this.$emit('add');
},
},
};
</script>
<template>
<woot-button
variant="smooth"
@@ -10,16 +20,6 @@
</woot-button>
</template>
<script>
export default {
methods: {
addLabel() {
this.$emit('add');
},
},
};
</script>
<style lang="scss" scoped>
.label--add {
margin-bottom: var(--space-micro);

View File

@@ -1,3 +1,7 @@
<script>
export default {};
</script>
<template>
<li
class="list-none my-1 mx-0 border-b border-slate-50 dark:border-slate-700"
@@ -5,6 +9,3 @@
:aria-disabled="true"
/>
</template>
<script>
export default {};
</script>

View File

@@ -1,13 +1,3 @@
<template>
<li class="inline-flex list-none" :tabindex="null" :aria-disabled="true">
<span
class="text-xs text-slate-600 dark:text-slate-100 mt-1 font-medium w-full block text-left rtl:text-right whitespace-nowrap p-2"
>
{{ title }}
</span>
<slot />
</li>
</template>
<script>
export default {
componentName: 'WootDropdownMenu',
@@ -19,3 +9,14 @@ export default {
},
};
</script>
<template>
<li class="inline-flex list-none" :tabindex="null" :aria-disabled="true">
<span
class="text-xs text-slate-600 dark:text-slate-100 mt-1 font-medium w-full block text-left rtl:text-right whitespace-nowrap p-2"
>
{{ title }}
</span>
<slot />
</li>
</template>

View File

@@ -1,15 +1,3 @@
<template>
<li
class="dropdown-menu__item list-none mb-1"
:class="{
'is-disabled': disabled,
}"
:tabindex="disabled ? null : -1"
:aria-disabled="disabled"
>
<slot />
</li>
</template>
<script>
export default {
name: 'WootDropdownItem',
@@ -19,13 +7,23 @@ export default {
type: Boolean,
default: false,
},
className: {
type: String,
default: '',
},
},
};
</script>
<template>
<li
class="mb-1 list-none dropdown-menu__item"
:class="{
'is-disabled': disabled,
}"
:tabindex="disabled ? null : -1"
:aria-disabled="disabled"
>
<slot />
</li>
</template>
<style lang="scss" scoped>
.dropdown-menu__item {
::v-deep {

View File

@@ -1,12 +1,3 @@
<template>
<ul
ref="dropdownMenu"
class="dropdown menu vertical"
:class="[placement && `dropdown--${placement}`]"
>
<slot />
</ul>
</template>
<script>
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
export default {
@@ -59,3 +50,13 @@ export default {
},
};
</script>
<template>
<ul
ref="dropdownMenu"
class="dropdown menu vertical"
:class="[placement && `dropdown--${placement}`]"
>
<slot />
</ul>
</template>

View File

@@ -1,11 +1,3 @@
<template>
<li class="sub-menu-container">
<ul class="sub-menu-li-container">
<woot-dropdown-header v-if="title" :title="title" />
<slot />
</ul>
</li>
</template>
<script>
import WootDropdownHeader from 'shared/components/ui/dropdown/DropdownHeader.vue';
@@ -24,6 +16,16 @@ export default {
},
};
</script>
<template>
<li class="sub-menu-container">
<ul class="sub-menu-li-container">
<WootDropdownHeader v-if="title" :title="title" />
<slot />
</ul>
</li>
</template>
<style lang="scss" scoped>
.sub-menu-container {
margin-top: var(--space-micro);

View File

@@ -1,80 +1,3 @@
<template>
<div class="flex flex-col w-full max-h-[12.5rem]">
<div class="flex items-center justify-center mb-1">
<h4
class="text-sm text-slate-800 dark:text-slate-100 m-0 overflow-hidden whitespace-nowrap text-ellipsis flex-grow"
>
{{ $t('CONTACT_PANEL.LABELS.LABEL_SELECT.TITLE') }}
</h4>
<hotkey
custom-class="text-slate-800 dark:text-slate-100 bg-slate-50 dark:bg-slate-600 text-xxs border border-solid border-slate-75 dark:border-slate-600"
>
L
</hotkey>
</div>
<div class="mb-2 flex-shrink-0 flex-grow-0 flex-auto max-h-8">
<input
ref="searchbar"
v-model="search"
type="text"
class="search-input"
autofocus="true"
:placeholder="$t('CONTACT_PANEL.LABELS.LABEL_SELECT.PLACEHOLDER')"
/>
</div>
<div
class="flex justify-start items-start flex-grow flex-shrink flex-auto overflow-auto"
>
<div class="w-full">
<woot-dropdown-menu>
<label-dropdown-item
v-for="label in filteredActiveLabels"
:key="label.title"
:title="label.title"
:color="label.color"
:selected="selectedLabels.includes(label.title)"
@click="onAddRemove(label)"
/>
</woot-dropdown-menu>
<div
v-if="noResult"
class="flex justify-center py-4 px-2.5 font-medium text-xs text-slate-700 dark:text-slate-200"
>
{{ $t('CONTACT_PANEL.LABELS.LABEL_SELECT.NO_RESULT') }}
</div>
<div
v-if="allowCreation && shouldShowCreate"
class="flex pt-1 border-t border-solid border-slate-100 dark:border-slate-900"
>
<woot-button
size="small"
variant="clear"
color-scheme="secondary"
icon="add"
is-expanded
class="button-new-label"
:is-disabled="hasExactMatchInResults"
@click="showCreateModal"
>
{{ createLabelPlaceholder }}
{{ parsedSearch }}
</woot-button>
<woot-modal
:show.sync="createModalVisible"
:on-close="hideCreateModal"
>
<add-label-modal
:prefill-title="parsedSearch"
@close="hideCreateModal"
/>
</woot-modal>
</div>
</div>
</div>
</div>
</template>
<script>
import LabelDropdownItem from './LabelDropdownItem.vue';
import Hotkey from 'dashboard/components/base/Hotkey.vue';
@@ -184,6 +107,83 @@ export default {
};
</script>
<template>
<div class="flex flex-col w-full max-h-[12.5rem]">
<div class="flex items-center justify-center mb-1">
<h4
class="flex-grow m-0 overflow-hidden text-sm text-slate-800 dark:text-slate-100 whitespace-nowrap text-ellipsis"
>
{{ $t('CONTACT_PANEL.LABELS.LABEL_SELECT.TITLE') }}
</h4>
<Hotkey
custom-class="border border-solid text-slate-800 dark:text-slate-100 bg-slate-50 dark:bg-slate-600 text-xxs border-slate-75 dark:border-slate-600"
>
{{ 'L' }}
</Hotkey>
</div>
<div class="flex-auto flex-grow-0 flex-shrink-0 mb-2 max-h-8">
<input
ref="searchbar"
v-model="search"
type="text"
class="search-input"
autofocus="true"
:placeholder="$t('CONTACT_PANEL.LABELS.LABEL_SELECT.PLACEHOLDER')"
/>
</div>
<div
class="flex items-start justify-start flex-auto flex-grow flex-shrink overflow-auto"
>
<div class="w-full">
<woot-dropdown-menu>
<LabelDropdownItem
v-for="label in filteredActiveLabels"
:key="label.title"
:title="label.title"
:color="label.color"
:selected="selectedLabels.includes(label.title)"
@click="onAddRemove(label)"
/>
</woot-dropdown-menu>
<div
v-if="noResult"
class="flex justify-center py-4 px-2.5 font-medium text-xs text-slate-700 dark:text-slate-200"
>
{{ $t('CONTACT_PANEL.LABELS.LABEL_SELECT.NO_RESULT') }}
</div>
<div
v-if="allowCreation && shouldShowCreate"
class="flex pt-1 border-t border-solid border-slate-100 dark:border-slate-900"
>
<woot-button
size="small"
variant="clear"
color-scheme="secondary"
icon="add"
is-expanded
class="button-new-label"
:is-disabled="hasExactMatchInResults"
@click="showCreateModal"
>
{{ createLabelPlaceholder }}
{{ parsedSearch }}
</woot-button>
<woot-modal
:show.sync="createModalVisible"
:on-close="hideCreateModal"
>
<AddLabelModal
:prefill-title="parsedSearch"
@close="hideCreateModal"
/>
</woot-modal>
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.hotkey {
@apply flex-shrink-0;

View File

@@ -1,25 +1,3 @@
<template>
<woot-dropdown-item>
<div class="item-wrap">
<woot-button variant="clear" @click="onClick">
<div class="button-wrap">
<div class="name-label-wrap">
<div
v-if="color"
class="label-color--display"
:style="{ backgroundColor: color }"
/>
<span class="label-text" :title="title">{{ title }}</span>
</div>
<div>
<i v-if="selected" class="icon ion-checkmark-round" />
</div>
</div>
</woot-button>
</div>
</woot-dropdown-item>
</template>
<script>
export default {
props: {
@@ -45,6 +23,28 @@ export default {
};
</script>
<template>
<woot-dropdown-item>
<div class="item-wrap">
<woot-button variant="clear" @click="onClick">
<div class="button-wrap">
<div class="name-label-wrap">
<div
v-if="color"
class="label-color--display"
:style="{ backgroundColor: color }"
/>
<span class="label-text" :title="title">{{ title }}</span>
</div>
<div>
<i v-if="selected" class="icon ion-checkmark-round" />
</div>
</div>
</woot-button>
</div>
</woot-dropdown-item>
</template>
<style lang="scss" scoped>
.item-wrap {
@apply flex;