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

@@ -27,6 +27,160 @@ module.exports = {
'import/no-unresolved': 'off',
'vue/html-indent': 'off',
'vue/multi-word-component-names': 'off',
'vue/next-tick-style': ['error', 'callback'],
'vue/block-order': [
'error',
{
order: ['script', 'template', 'style'],
},
],
'vue/component-name-in-template-casing': [
'error',
'PascalCase',
{
registeredComponentsOnly: true,
},
],
'vue/component-options-name-casing': ['error', 'PascalCase'],
'vue/custom-event-name-casing': ['error', 'camelCase'],
'vue/define-emits-declaration': ['error'],
'vue/define-macros-order': [
'error',
{
order: ['defineProps', 'defineEmits'],
defineExposeLast: false,
},
],
'vue/define-props-declaration': ['error', 'runtime'],
'vue/match-component-import-name': ['error'],
'vue/no-bare-strings-in-template': [
'error',
{
allowlist: [
'(',
')',
',',
'.',
'&',
'+',
'-',
'=',
'*',
'/',
'#',
'%',
'!',
'?',
':',
'[',
']',
'{',
'}',
'<',
'>',
'⌘',
'📄',
'🎉',
'💬',
'👥',
'📥',
'🔖',
'❌',
'✅',
'\u00b7',
'\u2022',
'\u2010',
'\u2013',
'\u2014',
'\u2212',
'|',
],
attributes: {
'/.+/': [
'title',
'aria-label',
'aria-placeholder',
'aria-roledescription',
'aria-valuetext',
],
input: ['placeholder'],
},
directives: ['v-text'],
},
],
'vue/no-empty-component-block': 'error',
'vue/no-multiple-objects-in-class': 'error',
'vue/no-root-v-if': 'warn',
'vue/no-static-inline-styles': [
'error',
{
allowBinding: false,
},
],
'vue/no-template-target-blank': [
'error',
{
allowReferrer: false,
enforceDynamicLinks: 'always',
},
],
'vue/no-required-prop-with-default': [
'error',
{
autofix: false,
},
],
'vue/no-this-in-before-route-enter': 'error',
'vue/no-undef-components': [
'error',
{
ignorePatterns: [
'^woot-',
'^fluent-',
'^multiselect',
'^router-link',
'^router-view',
'^ninja-keys',
'^FormulateForm',
'^FormulateInput',
'^highlightjs',
],
},
],
'vue/no-unused-emit-declarations': 'error',
'vue/no-unused-refs': 'error',
'vue/no-use-v-else-with-v-for': 'error',
'vue/prefer-true-attribute-shorthand': 'error',
'vue/no-useless-v-bind': [
'error',
{
ignoreIncludesComment: false,
ignoreStringEscape: false,
},
],
'vue/no-v-text': 'error',
'vue/padding-line-between-blocks': ['error', 'always'],
'vue/prefer-separate-static-class': 'error',
'vue/require-explicit-slots': 'error',
'vue/require-macro-variable-name': [
'error',
{
defineProps: 'props',
defineEmits: 'emit',
defineSlots: 'slots',
useSlots: 'slots',
useAttrs: 'attrs',
},
],
'vue/no-unused-properties': [
'error',
{
groups: ['props'],
deepData: false,
ignorePublicMembers: false,
unreferencedOptions: [],
},
],
'vue/max-attributes-per-line': [
'error',
{

View File

@@ -1,30 +1,3 @@
<template>
<div
v-if="!authUIFlags.isFetching && !accountUIFlags.isFetchingItem"
id="app"
class="flex-grow-0 w-full h-full min-h-0 app-wrapper"
:class="{ 'app-rtl--wrapper': isRTLView }"
:dir="isRTLView ? 'rtl' : 'ltr'"
>
<update-banner :latest-chatwoot-version="latestChatwootVersion" />
<template v-if="currentAccountId">
<pending-email-verification-banner v-if="hideOnOnboardingView" />
<payment-pending-banner v-if="hideOnOnboardingView" />
<upgrade-banner />
</template>
<transition name="fade" mode="out-in">
<router-view />
</transition>
<add-account-modal
:show="showAddAccountModal"
:has-accounts="hasAccounts"
/>
<woot-snackbar-box />
<network-notification />
</div>
<loading-state v-else />
</template>
<script>
import { mapGetters } from 'vuex';
import router from '../dashboard/routes';
@@ -74,7 +47,6 @@ export default {
...mapGetters({
getAccount: 'accounts/getAccount',
currentUser: 'getCurrentUser',
globalConfig: 'globalConfig/get',
authUIFlags: 'getAuthUIFlags',
accountUIFlags: 'accounts/getUIFlags',
currentAccountId: 'getCurrentAccountId',
@@ -147,6 +119,30 @@ export default {
};
</script>
<template>
<div
v-if="!authUIFlags.isFetching && !accountUIFlags.isFetchingItem"
id="app"
class="flex-grow-0 w-full h-full min-h-0 app-wrapper"
:class="{ 'app-rtl--wrapper': isRTLView }"
:dir="isRTLView ? 'rtl' : 'ltr'"
>
<UpdateBanner :latest-chatwoot-version="latestChatwootVersion" />
<template v-if="currentAccountId">
<PendingEmailVerificationBanner v-if="hideOnOnboardingView" />
<PaymentPendingBanner v-if="hideOnOnboardingView" />
<UpgradeBanner />
</template>
<transition name="fade" mode="out-in">
<router-view />
</transition>
<AddAccountModal :show="showAddAccountModal" :has-accounts="hasAccounts" />
<WootSnackbarBox />
<NetworkNotification />
</div>
<LoadingState v-else />
</template>
<style lang="scss">
@import './assets/scss/app';
</style>

View File

@@ -1,35 +1,3 @@
<template>
<div class="-mt-px text-sm">
<button
class="flex items-center select-none w-full rounded-none bg-slate-50 dark:bg-slate-800 border border-l-0 border-r-0 border-solid m-0 border-slate-100 dark:border-slate-700/50 cursor-grab justify-between py-2 px-4 drag-handle"
@click="$emit('click')"
>
<div class="flex justify-between mb-0.5">
<emoji-or-icon class="inline-block w-5" :icon="icon" :emoji="emoji" />
<h5
class="text-slate-800 text-sm dark:text-slate-100 mb-0 py-0 pr-2 pl-0"
>
{{ title }}
</h5>
</div>
<div class="flex flex-row">
<slot name="button" />
<div class="flex justify-end w-3 text-woot-500">
<fluent-icon v-if="isOpen" size="24" icon="subtract" type="solid" />
<fluent-icon v-else size="24" icon="add" type="solid" />
</div>
</div>
</button>
<div
v-if="isOpen"
class="bg-white dark:bg-slate-900"
:class="compact ? 'p-0' : 'p-4'"
>
<slot />
</div>
</div>
</template>
<script>
import EmojiOrIcon from 'shared/components/EmojiOrIcon.vue';
@@ -61,3 +29,35 @@ export default {
},
};
</script>
<template>
<div class="-mt-px text-sm">
<button
class="flex items-center select-none w-full rounded-none bg-slate-50 dark:bg-slate-800 border border-l-0 border-r-0 border-solid m-0 border-slate-100 dark:border-slate-700/50 cursor-grab justify-between py-2 px-4 drag-handle"
@click="$emit('click')"
>
<div class="flex justify-between mb-0.5">
<EmojiOrIcon class="inline-block w-5" :icon="icon" :emoji="emoji" />
<h5
class="text-slate-800 text-sm dark:text-slate-100 mb-0 py-0 pr-2 pl-0"
>
{{ title }}
</h5>
</div>
<div class="flex flex-row">
<slot name="button" />
<div class="flex justify-end w-3 text-woot-500">
<fluent-icon v-if="isOpen" size="24" icon="subtract" type="solid" />
<fluent-icon v-else size="24" icon="add" type="solid" />
</div>
</div>
</button>
<div
v-if="isOpen"
class="bg-white dark:bg-slate-900"
:class="compact ? 'p-0' : 'p-4'"
>
<slot />
</div>
</div>
</template>

View File

@@ -1,17 +1,3 @@
<template>
<button
class="bg-white dark:bg-slate-900 cursor-pointer flex flex-col justify-end transition-all duration-200 ease-in -m-px py-4 px-0 items-center border border-solid border-slate-25 dark:border-slate-800 hover:border-woot-500 dark:hover:border-woot-500 hover:shadow-md hover:z-50 disabled:opacity-60"
@click="$emit('click')"
>
<img :src="src" :alt="title" class="w-1/2 my-4 mx-auto" />
<h3
class="text-slate-800 dark:text-slate-100 text-base text-center capitalize"
>
{{ title }}
</h3>
</button>
</template>
<script>
export default {
props: {
@@ -27,6 +13,20 @@ export default {
};
</script>
<template>
<button
class="bg-white dark:bg-slate-900 cursor-pointer flex flex-col justify-end transition-all duration-200 ease-in -m-px py-4 px-0 items-center border border-solid border-slate-25 dark:border-slate-800 hover:border-woot-500 dark:hover:border-woot-500 hover:shadow-md hover:z-50 disabled:opacity-60"
@click="$emit('click')"
>
<img :src="src" :alt="title" class="w-1/2 my-4 mx-auto" />
<h3
class="text-slate-800 dark:text-slate-100 text-base text-center capitalize"
>
{{ title }}
</h3>
</button>
</template>
<style scoped lang="scss">
.inactive {
img {

View File

@@ -1,119 +1,3 @@
<template>
<div
class="flex flex-col flex-shrink-0 overflow-hidden border-r conversations-list-wrap rtl:border-r-0 rtl:border-l border-slate-50 dark:border-slate-800/50"
:class="[
{ hidden: !showConversationList },
isOnExpandedLayout ? 'basis-full' : 'flex-basis-clamp',
]"
>
<slot />
<chat-list-header
:page-title="pageTitle"
:has-applied-filters="hasAppliedFilters"
:has-active-folders="hasActiveFolders"
:active-status="activeStatus"
@add-folders="onClickOpenAddFoldersModal"
@delete-folders="onClickOpenDeleteFoldersModal"
@filters-modal="onToggleAdvanceFiltersModal"
@reset-filters="resetAndFetchData"
@basic-filter-change="onBasicFilterChange"
/>
<add-custom-views
v-if="showAddFoldersModal"
:custom-views-query="foldersQuery"
:open-last-saved-item="openLastSavedItemInFolder"
@close="onCloseAddFoldersModal"
/>
<delete-custom-views
v-if="showDeleteFoldersModal"
:show-delete-popup.sync="showDeleteFoldersModal"
:active-custom-view="activeFolder"
:custom-views-id="foldersId"
:open-last-item-after-delete="openLastItemAfterDeleteInFolder"
@close="onCloseDeleteFoldersModal"
/>
<chat-type-tabs
v-if="!hasAppliedFiltersOrActiveFolders"
:items="assigneeTabItems"
:active-tab="activeAssigneeTab"
class="tab--chat-type"
@chatTabChange="updateAssigneeTab"
/>
<p
v-if="!chatListLoading && !conversationList.length"
class="flex items-center justify-center p-4 overflow-auto"
>
{{ $t('CHAT_LIST.LIST.404') }}
</p>
<conversation-bulk-actions
v-if="selectedConversations.length"
:conversations="selectedConversations"
:all-conversations-selected="allConversationsSelected"
:selected-inboxes="uniqueInboxes"
:show-open-action="allSelectedConversationsStatus('open')"
:show-resolved-action="allSelectedConversationsStatus('resolved')"
:show-snoozed-action="allSelectedConversationsStatus('snoozed')"
@select-all-conversations="selectAllConversations"
@assign-agent="onAssignAgent"
@update-conversations="onUpdateConversations"
@assign-labels="onAssignLabels"
@assign-team="onAssignTeamsForBulk"
/>
<div
ref="conversationList"
class="flex-1 conversations-list"
:class="{ 'overflow-hidden': isContextMenuOpen }"
>
<virtual-list
ref="conversationVirtualList"
:data-key="'id'"
:data-sources="conversationList"
:data-component="itemComponent"
:extra-props="virtualListExtraProps"
class="w-full h-full overflow-auto"
footer-tag="div"
>
<template #footer>
<div v-if="chatListLoading" class="text-center">
<span class="mt-4 mb-4 spinner" />
</div>
<p
v-if="showEndOfListMessage"
class="p-4 text-center text-slate-400 dark:text-slate-300"
>
{{ $t('CHAT_LIST.EOF') }}
</p>
<intersection-observer
v-if="!showEndOfListMessage && !chatListLoading"
:options="infiniteLoaderOptions"
@observed="loadMoreConversations"
/>
</template>
</virtual-list>
</div>
<woot-modal
:show.sync="showAdvancedFilters"
:on-close="closeAdvanceFiltersModal"
size="medium"
>
<conversation-advanced-filter
v-if="showAdvancedFilters"
:initial-filter-types="advancedFilterTypes"
:initial-applied-filters="appliedFilter"
:active-folder-name="activeFolderName"
:on-close="closeAdvanceFiltersModal"
:is-folder-view="hasActiveFolders"
@applyFilter="onApplyFilter"
@updateFolder="onUpdateSavedFilter"
/>
</woot-modal>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import { useUISettings } from 'dashboard/composables/useUISettings';
@@ -247,20 +131,16 @@ export default {
},
computed: {
...mapGetters({
currentChat: 'getSelectedChat',
currentUser: 'getCurrentUser',
chatLists: 'getAllConversations',
mineChatsList: 'getMineChats',
allChatList: 'getAllStatusChats',
chatListFilters: 'getChatListFilters',
unAssignedChatsList: 'getUnAssignedChats',
chatListLoading: 'getChatListLoadingStatus',
currentUserID: 'getCurrentUserID',
activeInbox: 'getSelectedInbox',
conversationStats: 'conversationStats/getStats',
appliedFilters: 'getAppliedConversationFilters',
folders: 'customViews/getCustomViews',
inboxes: 'inboxes/getInboxes',
agentList: 'agents/getAgents',
teamsList: 'teams/getTeams',
inboxesList: 'inboxes/getInboxes',
@@ -728,7 +608,7 @@ export default {
}
},
emitConversationLoaded() {
this.$emit('conversation-load');
this.$emit('conversationLoad');
this.$nextTick(() => {
// Addressing a known issue in the virtual list library where dynamically added items
// might not render correctly. This workaround involves a slight manual adjustment
@@ -980,6 +860,123 @@ export default {
},
};
</script>
<template>
<div
class="flex flex-col flex-shrink-0 overflow-hidden border-r conversations-list-wrap rtl:border-r-0 rtl:border-l border-slate-50 dark:border-slate-800/50"
:class="[
{ hidden: !showConversationList },
isOnExpandedLayout ? 'basis-full' : 'flex-basis-clamp',
]"
>
<slot />
<ChatListHeader
:page-title="pageTitle"
:has-applied-filters="hasAppliedFilters"
:has-active-folders="hasActiveFolders"
:active-status="activeStatus"
@addFolders="onClickOpenAddFoldersModal"
@deleteFolders="onClickOpenDeleteFoldersModal"
@filtersModal="onToggleAdvanceFiltersModal"
@resetFilters="resetAndFetchData"
@basicFilterChange="onBasicFilterChange"
/>
<AddCustomViews
v-if="showAddFoldersModal"
:custom-views-query="foldersQuery"
:open-last-saved-item="openLastSavedItemInFolder"
@close="onCloseAddFoldersModal"
/>
<DeleteCustomViews
v-if="showDeleteFoldersModal"
:show-delete-popup.sync="showDeleteFoldersModal"
:active-custom-view="activeFolder"
:custom-views-id="foldersId"
:open-last-item-after-delete="openLastItemAfterDeleteInFolder"
@close="onCloseDeleteFoldersModal"
/>
<ChatTypeTabs
v-if="!hasAppliedFiltersOrActiveFolders"
:items="assigneeTabItems"
:active-tab="activeAssigneeTab"
class="tab--chat-type"
@chatTabChange="updateAssigneeTab"
/>
<p
v-if="!chatListLoading && !conversationList.length"
class="flex items-center justify-center p-4 overflow-auto"
>
{{ $t('CHAT_LIST.LIST.404') }}
</p>
<ConversationBulkActions
v-if="selectedConversations.length"
:conversations="selectedConversations"
:all-conversations-selected="allConversationsSelected"
:selected-inboxes="uniqueInboxes"
:show-open-action="allSelectedConversationsStatus('open')"
:show-resolved-action="allSelectedConversationsStatus('resolved')"
:show-snoozed-action="allSelectedConversationsStatus('snoozed')"
@selectAllConversations="selectAllConversations"
@assignAgent="onAssignAgent"
@updateConversations="onUpdateConversations"
@assignLabels="onAssignLabels"
@assignTeam="onAssignTeamsForBulk"
/>
<div
ref="conversationList"
class="flex-1 conversations-list"
:class="{ 'overflow-hidden': isContextMenuOpen }"
>
<VirtualList
ref="conversationVirtualList"
data-key="id"
:data-sources="conversationList"
:data-component="itemComponent"
:extra-props="virtualListExtraProps"
class="w-full h-full overflow-auto"
footer-tag="div"
>
<template #footer>
<div v-if="chatListLoading" class="text-center">
<span class="mt-4 mb-4 spinner" />
</div>
<p
v-if="showEndOfListMessage"
class="p-4 text-center text-slate-400 dark:text-slate-300"
>
{{ $t('CHAT_LIST.EOF') }}
</p>
<IntersectionObserver
v-if="!showEndOfListMessage && !chatListLoading"
:options="infiniteLoaderOptions"
@observed="loadMoreConversations"
/>
</template>
</VirtualList>
</div>
<woot-modal
:show.sync="showAdvancedFilters"
:on-close="closeAdvanceFiltersModal"
size="medium"
>
<ConversationAdvancedFilter
v-if="showAdvancedFilters"
:initial-filter-types="advancedFilterTypes"
:initial-applied-filters="appliedFilter"
:active-folder-name="activeFolderName"
:on-close="closeAdvanceFiltersModal"
:is-folder-view="hasActiveFolders"
@applyFilter="onApplyFilter"
@updateFolder="onUpdateSavedFilter"
/>
</woot-modal>
</div>
</template>
<style scoped>
@tailwind components;
@layer components {

View File

@@ -21,16 +21,16 @@ const props = defineProps({
},
});
const emits = defineEmits([
'add-folders',
'delete-folders',
'reset-filters',
'basic-filter-change',
'filters-modal',
const emit = defineEmits([
'addFolders',
'deleteFolders',
'resetFilters',
'basicFilterChange',
'filtersModal',
]);
const onBasicFilterChange = (value, type) => {
emits('basic-filter-change', value, type);
emit('basicFilterChange', value, type);
};
const hasAppliedFiltersOrActiveFolders = computed(() => {
@@ -68,7 +68,7 @@ const hasAppliedFiltersOrActiveFolders = computed(() => {
variant="smooth"
color-scheme="secondary"
icon="save"
@click="emits('add-folders')"
@click="emit('addFolders')"
/>
<woot-button
v-tooltip.top-end="$t('FILTER.CLEAR_BUTTON_LABEL')"
@@ -76,7 +76,7 @@ const hasAppliedFiltersOrActiveFolders = computed(() => {
variant="smooth"
color-scheme="alert"
icon="dismiss-circle"
@click="emits('reset-filters')"
@click="emit('resetFilters')"
/>
</div>
<div v-if="hasActiveFolders">
@@ -86,7 +86,7 @@ const hasAppliedFiltersOrActiveFolders = computed(() => {
variant="smooth"
color-scheme="secondary"
icon="edit"
@click="emits('filters-modal')"
@click="emit('filtersModal')"
/>
<woot-button
v-tooltip.top-end="$t('FILTER.CUSTOM_VIEWS.DELETE.DELETE_BUTTON')"
@@ -94,7 +94,7 @@ const hasAppliedFiltersOrActiveFolders = computed(() => {
variant="smooth"
color-scheme="alert"
icon="delete"
@click="emits('delete-folders')"
@click="emit('deleteFolders')"
/>
</div>
<woot-button
@@ -104,9 +104,9 @@ const hasAppliedFiltersOrActiveFolders = computed(() => {
color-scheme="secondary"
icon="filter"
size="tiny"
@click="emits('filters-modal')"
@click="emit('filtersModal')"
/>
<conversation-basic-filter
<ConversationBasicFilter
v-if="!hasAppliedFiltersOrActiveFolders"
@changeFilter="onBasicFilterChange"
/>

View File

@@ -1,27 +1,3 @@
<template>
<div class="code--container">
<div class="code--action-area">
<form
v-if="enableCodePen"
class="code--codeopen-form"
action="https://codepen.io/pen/define"
method="POST"
target="_blank"
>
<input type="hidden" name="data" :value="codepenScriptValue" />
<button type="submit" class="button secondary tiny">
{{ $t('COMPONENTS.CODE.CODEPEN') }}
</button>
</form>
<button class="button secondary tiny" @click="onCopy">
{{ $t('COMPONENTS.CODE.BUTTON_TEXT') }}
</button>
</div>
<highlightjs v-if="script" :language="lang" :code="script" />
</div>
</template>
<script>
import 'highlight.js/styles/default.css';
import { copyTextToClipboard } from 'shared/helpers/clipboard';
@@ -66,6 +42,30 @@ export default {
};
</script>
<template>
<div class="code--container">
<div class="code--action-area">
<form
v-if="enableCodePen"
class="code--codeopen-form"
action="https://codepen.io/pen/define"
method="POST"
target="_blank"
>
<input type="hidden" name="data" :value="codepenScriptValue" />
<button type="submit" class="button secondary tiny">
{{ $t('COMPONENTS.CODE.CODEPEN') }}
</button>
</form>
<button class="button secondary tiny" @click="onCopy">
{{ $t('COMPONENTS.CODE.BUTTON_TEXT') }}
</button>
</div>
<highlightjs v-if="script" :language="lang" :code="script" />
</div>
</template>
<style lang="scss" scoped>
.code--container {
position: relative;

View File

@@ -1,26 +1,3 @@
<template>
<conversation-card
:key="source.id"
:active-label="label"
:team-id="teamId"
:folders-id="foldersId"
:chat="source"
:conversation-type="conversationType"
:selected="isConversationSelected(source.id)"
:show-assignee="showAssignee"
:enable-context-menu="true"
@select-conversation="selectConversation"
@de-select-conversation="deSelectConversation"
@assign-agent="assignAgent"
@assign-team="assignTeam"
@assign-label="assignLabels"
@update-conversation-status="updateConversationStatus"
@context-menu-toggle="toggleContextMenu"
@mark-as-unread="markAsUnread"
@assign-priority="assignPriority"
/>
</template>
<script>
import ConversationCard from './widgets/conversation/ConversationCard.vue';
export default {
@@ -70,3 +47,26 @@ export default {
},
};
</script>
<template>
<ConversationCard
:key="source.id"
:active-label="label"
:team-id="teamId"
:folders-id="foldersId"
:chat="source"
:conversation-type="conversationType"
:selected="isConversationSelected(source.id)"
:show-assignee="showAssignee"
enable-context-menu
@selectConversation="selectConversation"
@deSelectConversation="deSelectConversation"
@assignAgent="assignAgent"
@assignTeam="assignTeam"
@assignLabel="assignLabels"
@updateConversationStatus="updateConversationStatus"
@contextMenuToggle="toggleContextMenu"
@markAsUnread="markAsUnread"
@assignPriority="assignPriority"
/>
</template>

View File

@@ -1,139 +1,3 @@
<template>
<div class="py-3 px-4">
<div class="flex items-center mb-1">
<h4 class="text-sm flex items-center m-0 w-full error">
<div v-if="isAttributeTypeCheckbox" class="flex items-center">
<input
v-model="editedValue"
class="!my-0 mr-2 ml-0"
type="checkbox"
@change="onUpdate"
/>
</div>
<div class="flex items-center justify-between w-full">
<span
class="w-full inline-flex gap-1.5 items-start font-medium whitespace-nowrap text-sm mb-0"
:class="
v$.editedValue.$error
? 'text-red-400 dark:text-red-500'
: 'text-slate-800 dark:text-slate-100'
"
>
{{ label }}
<helper-text-popup
v-if="description"
:message="description"
class="mt-0.5"
/>
</span>
<woot-button
v-if="showActions && value"
v-tooltip.left="$t('CUSTOM_ATTRIBUTES.ACTIONS.DELETE')"
variant="link"
size="medium"
color-scheme="secondary"
icon="delete"
class-names="flex justify-end w-4"
@click="onDelete"
/>
</div>
</h4>
</div>
<div v-if="notAttributeTypeCheckboxAndList">
<div v-if="isEditing" v-on-clickaway="onClickAway">
<div class="mb-2 w-full flex items-center">
<input
ref="inputfield"
v-model="editedValue"
:type="inputType"
class="!h-8 ltr:!rounded-r-none rtl:!rounded-l-none !mb-0 !text-sm"
autofocus="true"
:class="{ error: v$.editedValue.$error }"
@blur="v$.editedValue.$touch"
@keyup.enter="onUpdate"
/>
<div>
<woot-button
size="small"
icon="checkmark"
class="rounded-l-none rtl:rounded-r-none"
@click="onUpdate"
/>
</div>
</div>
<span
v-if="shouldShowErrorMessage"
class="text-red-400 dark:text-red-500 text-sm block font-normal -mt-px w-full"
>
{{ errorMessage }}
</span>
</div>
<div
v-show="!isEditing"
class="flex group"
:class="{ 'is-editable': showActions }"
>
<a
v-if="isAttributeTypeLink"
:href="hrefURL"
target="_blank"
rel="noopener noreferrer"
class="group-hover:bg-slate-50 group-hover:dark:bg-slate-700 inline-block rounded-sm mb-0 break-all py-0.5 px-1"
>
{{ urlValue }}
</a>
<p
v-else
class="group-hover:bg-slate-50 group-hover:dark:bg-slate-700 inline-block rounded-sm mb-0 break-all py-0.5 px-1"
>
{{ displayValue || '---' }}
</p>
<div class="flex max-w-[2rem] gap-1 ml-1 rtl:mr-1 rtl:ml-0">
<woot-button
v-if="showActions && value"
v-tooltip="$t('CUSTOM_ATTRIBUTES.ACTIONS.COPY')"
variant="link"
size="small"
color-scheme="secondary"
icon="clipboard"
class-names="hidden group-hover:flex !w-6 flex-shrink-0"
@click="onCopy"
/>
<woot-button
v-if="showActions"
v-tooltip.right="$t('CUSTOM_ATTRIBUTES.ACTIONS.EDIT')"
variant="link"
size="small"
color-scheme="secondary"
icon="edit"
class-names="hidden group-hover:flex !w-6 flex-shrink-0"
@click="onEdit"
/>
</div>
</div>
</div>
<div v-if="isAttributeTypeList">
<multiselect-dropdown
:options="listOptions"
:selected-item="selectedItem"
:has-thumbnail="false"
:multiselector-placeholder="
$t('CUSTOM_ATTRIBUTES.FORM.ATTRIBUTE_TYPE.LIST.PLACEHOLDER')
"
:no-search-result="
$t('CUSTOM_ATTRIBUTES.FORM.ATTRIBUTE_TYPE.LIST.NO_RESULT')
"
:input-placeholder="
$t(
'CUSTOM_ATTRIBUTES.FORM.ATTRIBUTE_TYPE.LIST.SEARCH_INPUT_PLACEHOLDER'
)
"
@click="onUpdateListValue"
/>
</div>
</div>
</template>
<script>
import { format, parseISO } from 'date-fns';
import { required, url } from '@vuelidate/validators';
@@ -164,7 +28,6 @@ export default {
default: null,
},
regexCue: { type: String, default: null },
regexEnabled: { type: Boolean, default: false },
attributeKey: { type: String, required: true },
contactId: { type: Number, default: null },
},
@@ -330,6 +193,142 @@ export default {
};
</script>
<template>
<div class="px-4 py-3">
<div class="flex items-center mb-1">
<h4 class="flex items-center w-full m-0 text-sm error">
<div v-if="isAttributeTypeCheckbox" class="flex items-center">
<input
v-model="editedValue"
class="!my-0 mr-2 ml-0"
type="checkbox"
@change="onUpdate"
/>
</div>
<div class="flex items-center justify-between w-full">
<span
class="w-full inline-flex gap-1.5 items-start font-medium whitespace-nowrap text-sm mb-0"
:class="
v$.editedValue.$error
? 'text-red-400 dark:text-red-500'
: 'text-slate-800 dark:text-slate-100'
"
>
{{ label }}
<HelperTextPopup
v-if="description"
:message="description"
class="mt-0.5"
/>
</span>
<woot-button
v-if="showActions && value"
v-tooltip.left="$t('CUSTOM_ATTRIBUTES.ACTIONS.DELETE')"
variant="link"
size="medium"
color-scheme="secondary"
icon="delete"
class-names="flex justify-end w-4"
@click="onDelete"
/>
</div>
</h4>
</div>
<div v-if="notAttributeTypeCheckboxAndList">
<div v-if="isEditing" v-on-clickaway="onClickAway">
<div class="flex items-center w-full mb-2">
<input
ref="inputfield"
v-model="editedValue"
:type="inputType"
class="!h-8 ltr:!rounded-r-none rtl:!rounded-l-none !mb-0 !text-sm"
autofocus="true"
:class="{ error: v$.editedValue.$error }"
@blur="v$.editedValue.$touch"
@keyup.enter="onUpdate"
/>
<div>
<woot-button
size="small"
icon="checkmark"
class="rounded-l-none rtl:rounded-r-none"
@click="onUpdate"
/>
</div>
</div>
<span
v-if="shouldShowErrorMessage"
class="block w-full -mt-px text-sm font-normal text-red-400 dark:text-red-500"
>
{{ errorMessage }}
</span>
</div>
<div
v-show="!isEditing"
class="flex group"
:class="{ 'is-editable': showActions }"
>
<a
v-if="isAttributeTypeLink"
:href="hrefURL"
target="_blank"
rel="noopener noreferrer"
class="group-hover:bg-slate-50 group-hover:dark:bg-slate-700 inline-block rounded-sm mb-0 break-all py-0.5 px-1"
>
{{ urlValue }}
</a>
<p
v-else
class="group-hover:bg-slate-50 group-hover:dark:bg-slate-700 inline-block rounded-sm mb-0 break-all py-0.5 px-1"
>
{{ displayValue || '---' }}
</p>
<div class="flex max-w-[2rem] gap-1 ml-1 rtl:mr-1 rtl:ml-0">
<woot-button
v-if="showActions && value"
v-tooltip="$t('CUSTOM_ATTRIBUTES.ACTIONS.COPY')"
variant="link"
size="small"
color-scheme="secondary"
icon="clipboard"
class-names="hidden group-hover:flex !w-6 flex-shrink-0"
@click="onCopy"
/>
<woot-button
v-if="showActions"
v-tooltip.right="$t('CUSTOM_ATTRIBUTES.ACTIONS.EDIT')"
variant="link"
size="small"
color-scheme="secondary"
icon="edit"
class-names="hidden group-hover:flex !w-6 flex-shrink-0"
@click="onEdit"
/>
</div>
</div>
</div>
<div v-if="isAttributeTypeList">
<MultiselectDropdown
:options="listOptions"
:selected-item="selectedItem"
:has-thumbnail="false"
:multiselector-placeholder="
$t('CUSTOM_ATTRIBUTES.FORM.ATTRIBUTE_TYPE.LIST.PLACEHOLDER')
"
:no-search-result="
$t('CUSTOM_ATTRIBUTES.FORM.ATTRIBUTE_TYPE.LIST.NO_RESULT')
"
:input-placeholder="
$t(
'CUSTOM_ATTRIBUTES.FORM.ATTRIBUTE_TYPE.LIST.SEARCH_INPUT_PLACEHOLDER'
)
"
@click="onUpdateListValue"
/>
</div>
</div>
</template>
<style lang="scss" scoped>
::v-deep {
.selector-wrap {

View File

@@ -1,28 +1,3 @@
<template>
<div class="flex flex-col">
<woot-modal-header :header-title="$t('CONVERSATION.CUSTOM_SNOOZE.TITLE')" />
<form class="modal-content" @submit.prevent="chooseTime">
<date-picker
v-model="snoozeTime"
type="datetime"
inline
:lang="lang"
:disabled-date="disabledDate"
:disabled-time="disabledTime"
:popup-style="{ width: '100%' }"
/>
<div class="flex flex-row justify-end gap-2 py-2 px-0 w-full">
<woot-button variant="clear" @click.prevent="onClose">
{{ $t('CONVERSATION.CUSTOM_SNOOZE.CANCEL') }}
</woot-button>
<woot-button>
{{ $t('CONVERSATION.CUSTOM_SNOOZE.APPLY') }}
</woot-button>
</div>
</form>
</div>
</template>
<script>
import DatePicker from 'vue2-datepicker';
@@ -47,7 +22,7 @@ export default {
this.$emit('close');
},
chooseTime() {
this.$emit('choose-time', this.snoozeTime);
this.$emit('chooseTime', this.snoozeTime);
},
disabledDate(date) {
// Disable all the previous dates
@@ -64,6 +39,32 @@ export default {
},
};
</script>
<template>
<div class="flex flex-col">
<woot-modal-header :header-title="$t('CONVERSATION.CUSTOM_SNOOZE.TITLE')" />
<form class="modal-content" @submit.prevent="chooseTime">
<DatePicker
v-model="snoozeTime"
type="datetime"
inline
:lang="lang"
:disabled-date="disabledDate"
:disabled-time="disabledTime"
:popup-style="{ width: '100%' }"
/>
<div class="flex flex-row justify-end w-full gap-2 px-0 py-2">
<woot-button variant="clear" @click.prevent="onClose">
{{ $t('CONVERSATION.CUSTOM_SNOOZE.CANCEL') }}
</woot-button>
<woot-button>
{{ $t('CONVERSATION.CUSTOM_SNOOZE.APPLY') }}
</woot-button>
</div>
</form>
</div>
</template>
<style lang="scss" scoped>
.modal-content {
@apply pt-2 px-5 pb-6;

View File

@@ -1,3 +1,16 @@
<script setup>
defineProps({
title: {
type: String,
default: '',
},
description: {
type: String,
default: '',
},
});
</script>
<template>
<div class="flex flex-col items-start w-full gap-6">
<div class="flex flex-col w-full gap-4">
@@ -16,16 +29,3 @@
</div>
</div>
</template>
<script setup>
defineProps({
title: {
type: String,
default: '',
},
description: {
type: String,
default: '',
},
});
</script>

View File

@@ -1,7 +1,3 @@
<template>
<div ref="observedElement" class="h-6 w-full" />
</template>
<script>
export default {
props: {
@@ -32,3 +28,7 @@ export default {
},
};
</script>
<template>
<div ref="observedElement" class="h-6 w-full" />
</template>

View File

@@ -1,20 +1,3 @@
<template>
<div class="text--container">
<woot-button size="small" class="button--text" @click="onCopy">
{{ $t('COMPONENTS.CODE.BUTTON_TEXT') }}
</woot-button>
<woot-button
variant="clear"
size="small"
class="button--visibility"
color-scheme="secondary"
:icon="masked ? 'eye-show' : 'eye-hide'"
@click.prevent="toggleMasked"
/>
<highlightjs v-if="value" :code="masked ? '•'.repeat(10) : value" />
</div>
</template>
<script>
import 'highlight.js/styles/default.css';
import { copyTextToClipboard } from 'shared/helpers/clipboard';
@@ -45,6 +28,23 @@ export default {
};
</script>
<template>
<div class="text--container">
<woot-button size="small" class="button--text" @click="onCopy">
{{ $t('COMPONENTS.CODE.BUTTON_TEXT') }}
</woot-button>
<woot-button
variant="clear"
size="small"
class="button--visibility"
color-scheme="secondary"
:icon="masked ? 'eye-show' : 'eye-hide'"
@click.prevent="toggleMasked"
/>
<highlightjs v-if="value" :code="masked ? '•'.repeat(10) : value" />
</div>
</template>
<style lang="scss" scoped>
.text--container {
position: relative;

View File

@@ -1,36 +1,3 @@
<template>
<transition name="modal-fade">
<div
v-if="show"
:class="modalClassName"
transition="modal"
@mousedown="handleMouseDown"
>
<div
:class="{
'modal-container rtl:text-right shadow-md max-h-full overflow-auto relative bg-white dark:bg-slate-800 skip-context-menu': true,
'rounded-xl w-[37.5rem]': !fullWidth,
'items-center rounded-none flex h-full justify-center w-full':
fullWidth,
[size]: true,
}"
@mouse.stop
@mousedown="event => event.stopPropagation()"
>
<woot-button
v-if="showCloseButton"
color-scheme="secondary"
icon="dismiss"
variant="clear"
class="absolute z-10 ltr:right-2 rtl:left-2 top-2"
@click="close"
/>
<slot />
</div>
</div>
</transition>
</template>
<script>
export default {
props: {
@@ -108,6 +75,39 @@ export default {
};
</script>
<template>
<transition name="modal-fade">
<div
v-if="show"
:class="modalClassName"
transition="modal"
@mousedown="handleMouseDown"
>
<div
class="relative max-h-full overflow-auto bg-white shadow-md modal-container rtl:text-right dark:bg-slate-800 skip-context-menu"
:class="{
'rounded-xl w-[37.5rem]': !fullWidth,
'items-center rounded-none flex h-full justify-center w-full':
fullWidth,
[size]: true,
}"
@mouse.stop
@mousedown="event => event.stopPropagation()"
>
<woot-button
v-if="showCloseButton"
color-scheme="secondary"
icon="dismiss"
variant="clear"
class="absolute z-10 ltr:right-2 rtl:left-2 top-2"
@click="close"
/>
<slot />
</div>
</div>
</transition>
</template>
<style lang="scss">
.modal-mask {
@apply flex items-center justify-center bg-modal-backdrop-light dark:bg-modal-backdrop-dark z-[9990] h-full left-0 fixed top-0 w-full;

View File

@@ -1,3 +1,28 @@
<script>
export default {
props: {
headerTitle: {
type: String,
default: '',
},
headerContent: {
type: String,
default: '',
},
headerContentValue: {
type: String,
default: '',
},
headerImage: {
type: String,
default: '',
},
},
};
</script>
<!-- eslint-disable vue/no-unused-refs -->
<!-- Added ref for writing specs -->
<template>
<div class="flex flex-col items-start px-8 pt-8 pb-0">
<img v-if="headerImage" :src="headerImage" alt="No image" />
@@ -23,26 +48,3 @@
<slot />
</div>
</template>
<script>
export default {
props: {
headerTitle: {
type: String,
default: '',
},
headerContent: {
type: String,
default: '',
},
headerContentValue: {
type: String,
default: '',
},
headerImage: {
type: String,
default: '',
},
},
};
</script>

View File

@@ -1,3 +1,26 @@
<script>
export default {
props: {
title: {
type: String,
default: '',
},
subTitle: {
type: String,
default: '',
},
showBorder: {
type: Boolean,
default: true,
},
note: {
type: String,
default: '',
},
},
};
</script>
<template>
<div
class="ml-0 mr-0 flex py-8 w-full xl:w-3/4 flex-col xl:flex-row"
@@ -30,26 +53,3 @@
</div>
</div>
</template>
<script>
export default {
props: {
title: {
type: String,
default: '',
},
subTitle: {
type: String,
default: '',
},
showBorder: {
type: Boolean,
default: true,
},
note: {
type: String,
default: '',
},
},
};
</script>

View File

@@ -1,14 +1,3 @@
<template>
<woot-button
:size="size"
variant="clear"
color-scheme="secondary"
class="-ml-3 text-black-900 dark:text-slate-300"
icon="list"
@click="onMenuItemClick"
/>
</template>
<script>
import { BUS_EVENTS } from 'shared/constants/busEvents';
@@ -26,3 +15,14 @@ export default {
},
};
</script>
<template>
<woot-button
:size="size"
variant="clear"
color-scheme="secondary"
class="-ml-3 text-black-900 dark:text-slate-300"
icon="list"
@click="onMenuItemClick"
/>
</template>

View File

@@ -1,24 +1,3 @@
<template>
<div>
<div
class="shadow-sm bg-slate-800 dark:bg-slate-700 rounded-[4px] items-center gap-3 inline-flex mb-2 max-w-[25rem] min-h-[1.875rem] min-w-[15rem] px-6 py-3 text-left"
>
<div class="text-white dark:text-white text-sm font-medium">
{{ message }}
</div>
<div v-if="action">
<router-link
v-if="action.type == 'link'"
:to="action.to"
class="text-woot-500 dark:text-woot-500 cursor-pointer font-medium hover:text-woot-600 dark:hover:text-woot-600 select-none"
>
{{ action.message }}
</router-link>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
@@ -27,11 +6,6 @@ export default {
type: Object,
default: () => {},
},
showButton: Boolean,
duration: {
type: [String, Number],
default: 3000,
},
},
data() {
return {
@@ -42,3 +16,24 @@ export default {
methods: {},
};
</script>
<template>
<div>
<div
class="shadow-sm bg-slate-800 dark:bg-slate-700 rounded-[4px] items-center gap-3 inline-flex mb-2 max-w-[25rem] min-h-[1.875rem] min-w-[15rem] px-6 py-3 text-left"
>
<div class="text-sm font-medium text-white dark:text-white">
{{ message }}
</div>
<div v-if="action">
<router-link
v-if="action.type == 'link'"
:to="action.to"
class="font-medium cursor-pointer select-none text-woot-500 dark:text-woot-500 hover:text-woot-600 dark:hover:text-woot-600"
>
{{ action.message }}
</router-link>
</div>
</div>
</div>
</template>

View File

@@ -1,18 +1,3 @@
<template>
<transition-group
name="toast-fade"
tag="div"
class="left-0 my-0 mx-auto max-w-[25rem] overflow-hidden absolute right-0 text-center top-4 z-[9999]"
>
<woot-snackbar
v-for="snackMessage in snackMessages"
:key="snackMessage.key"
:message="snackMessage.message"
:action="snackMessage.action"
/>
</transition-group>
</template>
<script>
import WootSnackbar from './Snackbar.vue';
@@ -53,3 +38,18 @@ export default {
},
};
</script>
<template>
<transition-group
name="toast-fade"
tag="div"
class="left-0 my-0 mx-auto max-w-[25rem] overflow-hidden absolute right-0 text-center top-4 z-[9999]"
>
<WootSnackbar
v-for="snackMessage in snackMessages"
:key="snackMessage.key"
:message="snackMessage.message"
:action="snackMessage.action"
/>
</transition-group>
</template>

View File

@@ -1,14 +1,3 @@
<template>
<banner
v-if="shouldShowBanner"
color-scheme="alert"
:banner-message="bannerMessage"
:action-button-label="actionButtonMessage"
has-action-button
@click="routeToBilling"
/>
</template>
<script>
import { mapGetters } from 'vuex';
import { useAdmin } from 'dashboard/composables/useAdmin';
@@ -86,3 +75,14 @@ export default {
},
};
</script>
<template>
<Banner
v-if="shouldShowBanner"
color-scheme="alert"
:banner-message="bannerMessage"
:action-button-label="actionButtonMessage"
has-action-button
@click="routeToBilling"
/>
</template>

View File

@@ -1,15 +1,3 @@
<template>
<banner
v-if="shouldShowBanner"
color-scheme="alert"
:banner-message="bannerMessage"
:action-button-label="actionButtonMessage"
action-button-icon="mail"
has-action-button
@click="resendVerificationEmail"
/>
</template>
<script>
import Banner from 'dashboard/components/ui/Banner.vue';
import { mapGetters } from 'vuex';
@@ -41,3 +29,15 @@ export default {
},
};
</script>
<template>
<Banner
v-if="shouldShowBanner"
color-scheme="alert"
:banner-message="bannerMessage"
:action-button-label="actionButtonMessage"
action-button-icon="mail"
has-action-button
@click="resendVerificationEmail"
/>
</template>

View File

@@ -1,14 +1,3 @@
<template>
<banner
v-if="shouldShowBanner"
color-scheme="primary"
:banner-message="bannerMessage"
href-link="https://github.com/chatwoot/chatwoot/releases"
:href-link-text="$t('GENERAL_SETTINGS.LEARN_MORE')"
has-close-button
@close="dismissUpdateBanner"
/>
</template>
<script>
import Banner from 'dashboard/components/ui/Banner.vue';
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
@@ -77,3 +66,15 @@ export default {
},
};
</script>
<template>
<Banner
v-if="shouldShowBanner"
color-scheme="primary"
:banner-message="bannerMessage"
href-link="https://github.com/chatwoot/chatwoot/releases"
:href-link-text="$t('GENERAL_SETTINGS.LEARN_MORE')"
has-close-button
@close="dismissUpdateBanner"
/>
</template>

View File

@@ -1,14 +1,3 @@
<template>
<banner
v-if="shouldShowBanner"
color-scheme="alert"
:banner-message="bannerMessage"
:action-button-label="actionButtonMessage"
has-action-button
@click="routeToBilling"
/>
</template>
<script>
import Banner from 'dashboard/components/ui/Banner.vue';
import { mapGetters } from 'vuex';
@@ -86,3 +75,14 @@ export default {
},
};
</script>
<template>
<Banner
v-if="shouldShowBanner"
color-scheme="alert"
:banner-message="bannerMessage"
:action-button-label="actionButtonMessage"
has-action-button
@click="routeToBilling"
/>
</template>

View File

@@ -1,8 +1,3 @@
<template>
<kbd class="hotkey p-0.5 min-w-[1rem] uppercase" :class="customClass">
<slot />
</kbd>
</template>
<script>
export default {
props: {
@@ -14,6 +9,12 @@ export default {
};
</script>
<template>
<kbd class="hotkey p-0.5 min-w-[1rem] uppercase" :class="customClass">
<slot />
</kbd>
</template>
<style lang="scss">
kbd.hotkey {
@apply inline-flex leading-[0.625rem] rounded tracking-wide flex-shrink-0 items-center select-none justify-center;

View File

@@ -1,18 +1,10 @@
<template>
<button :type="type" class="button nice" :class="variant" @click="onClick">
<fluent-icon
v-if="!isLoading && icon"
class="icon"
:class="buttonIconClass"
:icon="icon"
/>
<spinner v-if="isLoading" />
<slot />
</button>
</template>
<script>
import Spinner from 'shared/components/Spinner.vue';
export default {
components: {
Spinner,
},
props: {
isLoading: {
type: Boolean,
@@ -42,3 +34,16 @@ export default {
},
};
</script>
<template>
<button :type="type" class="button nice" :class="variant" @click="onClick">
<fluent-icon
v-if="!isLoading && icon"
class="icon"
:class="buttonIconClass"
:icon="icon"
/>
<Spinner v-if="isLoading" />
<slot />
</button>
</template>

View File

@@ -1,17 +1,3 @@
<template>
<button
:type="type"
data-testid="submit_button"
:disabled="disabled"
:class="computedClass"
@click="onClick"
>
<fluent-icon v-if="!!iconClass" :icon="iconClass" class="icon" />
<span>{{ buttonText }}</span>
<spinner v-if="loading" class="ml-2" :color-scheme="spinnerClass" />
</button>
</template>
<script>
import Spinner from 'shared/components/Spinner.vue';
@@ -61,6 +47,21 @@ export default {
},
};
</script>
<template>
<button
:type="type"
data-testid="submit_button"
:disabled="disabled"
:class="computedClass"
@click="onClick"
>
<fluent-icon v-if="!!iconClass" :icon="iconClass" class="icon" />
<span>{{ buttonText }}</span>
<Spinner v-if="loading" class="ml-2" :color-scheme="spinnerClass" />
</button>
</template>
<style lang="scss" scoped>
button:disabled {
@apply bg-woot-100 dark:bg-woot-500/25 dark:text-slate-500 opacity-100;

View File

@@ -1,81 +1,3 @@
<template>
<div class="relative flex items-center justify-end resolve-actions">
<div class="button-group">
<woot-button
v-if="isOpen"
class-names="resolve"
color-scheme="success"
icon="checkmark"
emoji="✅"
:is-loading="isLoading"
@click="onCmdResolveConversation"
>
{{ $t('CONVERSATION.HEADER.RESOLVE_ACTION') }}
</woot-button>
<woot-button
v-else-if="isResolved"
class-names="resolve"
color-scheme="warning"
icon="arrow-redo"
emoji="👀"
:is-loading="isLoading"
@click="onCmdOpenConversation"
>
{{ $t('CONVERSATION.HEADER.REOPEN_ACTION') }}
</woot-button>
<woot-button
v-else-if="showOpenButton"
class-names="resolve"
color-scheme="primary"
icon="person"
:is-loading="isLoading"
@click="onCmdOpenConversation"
>
{{ $t('CONVERSATION.HEADER.OPEN_ACTION') }}
</woot-button>
<woot-button
v-if="showAdditionalActions"
ref="arrowDownButton"
:color-scheme="buttonClass"
:disabled="isLoading"
icon="chevron-down"
emoji="🔽"
@click="openDropdown"
/>
</div>
<div
v-if="showActionsDropdown"
v-on-clickaway="closeDropdown"
class="dropdown-pane dropdown-pane--open"
>
<woot-dropdown-menu class="mb-0">
<woot-dropdown-item v-if="!isPending">
<woot-button
variant="clear"
color-scheme="secondary"
size="small"
icon="snooze"
@click="() => openSnoozeModal()"
>
{{ $t('CONVERSATION.RESOLVE_DROPDOWN.SNOOZE_UNTIL') }}
</woot-button>
</woot-dropdown-item>
<woot-dropdown-item v-if="!isPending">
<woot-button
variant="clear"
color-scheme="secondary"
size="small"
icon="book-clock"
@click="() => toggleStatus(STATUS_TYPE.PENDING)"
>
{{ $t('CONVERSATION.RESOLVE_DROPDOWN.MARK_PENDING') }}
</woot-button>
</woot-dropdown-item>
</woot-dropdown-menu>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import { useAlert } from 'dashboard/composables';
@@ -95,7 +17,6 @@ export default {
WootDropdownMenu,
},
mixins: [keyboardEventListenerMixins],
props: { conversationId: { type: [String, Number], required: true } },
data() {
return {
isLoading: false,
@@ -220,6 +141,85 @@ export default {
},
};
</script>
<template>
<div class="relative flex items-center justify-end resolve-actions">
<div class="button-group">
<woot-button
v-if="isOpen"
class-names="resolve"
color-scheme="success"
icon="checkmark"
emoji="✅"
:is-loading="isLoading"
@click="onCmdResolveConversation"
>
{{ $t('CONVERSATION.HEADER.RESOLVE_ACTION') }}
</woot-button>
<woot-button
v-else-if="isResolved"
class-names="resolve"
color-scheme="warning"
icon="arrow-redo"
emoji="👀"
:is-loading="isLoading"
@click="onCmdOpenConversation"
>
{{ $t('CONVERSATION.HEADER.REOPEN_ACTION') }}
</woot-button>
<woot-button
v-else-if="showOpenButton"
class-names="resolve"
color-scheme="primary"
icon="person"
:is-loading="isLoading"
@click="onCmdOpenConversation"
>
{{ $t('CONVERSATION.HEADER.OPEN_ACTION') }}
</woot-button>
<woot-button
v-if="showAdditionalActions"
ref="arrowDownButton"
:color-scheme="buttonClass"
:disabled="isLoading"
icon="chevron-down"
emoji="🔽"
@click="openDropdown"
/>
</div>
<div
v-if="showActionsDropdown"
v-on-clickaway="closeDropdown"
class="dropdown-pane dropdown-pane--open"
>
<WootDropdownMenu class="mb-0">
<WootDropdownItem v-if="!isPending">
<woot-button
variant="clear"
color-scheme="secondary"
size="small"
icon="snooze"
@click="() => openSnoozeModal()"
>
{{ $t('CONVERSATION.RESOLVE_DROPDOWN.SNOOZE_UNTIL') }}
</woot-button>
</WootDropdownItem>
<WootDropdownItem v-if="!isPending">
<woot-button
variant="clear"
color-scheme="secondary"
size="small"
icon="book-clock"
@click="() => toggleStatus(STATUS_TYPE.PENDING)"
>
{{ $t('CONVERSATION.RESOLVE_DROPDOWN.MARK_PENDING') }}
</woot-button>
</WootDropdownItem>
</WootDropdownMenu>
</div>
</div>
</template>
<style lang="scss" scoped>
.dropdown-pane {
@apply left-auto top-[2.625rem] mt-0.5 right-0 max-w-[12.5rem] min-w-[9.75rem];

View File

@@ -1,50 +1,3 @@
<template>
<woot-dropdown-menu>
<woot-dropdown-header :title="$t('SIDEBAR.SET_AVAILABILITY_TITLE')" />
<woot-dropdown-item
v-for="status in availabilityStatuses"
:key="status.value"
class="flex items-baseline"
>
<woot-button
size="small"
:color-scheme="status.disabled ? '' : 'secondary'"
:variant="status.disabled ? 'smooth' : 'clear'"
class-names="status-change--dropdown-button"
@click="changeAvailabilityStatus(status.value)"
>
<availability-status-badge :status="status.value" />
{{ status.label }}
</woot-button>
</woot-dropdown-item>
<woot-dropdown-divider />
<woot-dropdown-item class="flex items-center justify-between p-2 m-0">
<div class="flex items-center">
<fluent-icon
v-tooltip.right-start="$t('SIDEBAR.SET_AUTO_OFFLINE.INFO_TEXT')"
icon="info"
size="14"
class="mt-px"
/>
<span
class="mx-1 my-0 text-xs font-medium text-slate-600 dark:text-slate-100"
>
{{ $t('SIDEBAR.SET_AUTO_OFFLINE.TEXT') }}
</span>
</div>
<woot-switch
size="small"
class="mx-1 mt-px mb-0"
:value="currentUserAutoOffline"
@input="updateAutoOffline"
/>
</woot-dropdown-item>
<woot-dropdown-divider />
</woot-dropdown-menu>
</template>
<script>
import { mapGetters } from 'vuex';
import { useAlert } from 'dashboard/composables';
@@ -136,3 +89,50 @@ export default {
},
};
</script>
<template>
<WootDropdownMenu>
<WootDropdownHeader :title="$t('SIDEBAR.SET_AVAILABILITY_TITLE')" />
<WootDropdownItem
v-for="status in availabilityStatuses"
:key="status.value"
class="flex items-baseline"
>
<woot-button
size="small"
:color-scheme="status.disabled ? '' : 'secondary'"
:variant="status.disabled ? 'smooth' : 'clear'"
class-names="status-change--dropdown-button"
@click="changeAvailabilityStatus(status.value)"
>
<AvailabilityStatusBadge :status="status.value" />
{{ status.label }}
</woot-button>
</WootDropdownItem>
<WootDropdownDivider />
<WootDropdownItem class="flex items-center justify-between p-2 m-0">
<div class="flex items-center">
<fluent-icon
v-tooltip.right-start="$t('SIDEBAR.SET_AUTO_OFFLINE.INFO_TEXT')"
icon="info"
size="14"
class="mt-px"
/>
<span
class="mx-1 my-0 text-xs font-medium text-slate-600 dark:text-slate-100"
>
{{ $t('SIDEBAR.SET_AUTO_OFFLINE.TEXT') }}
</span>
</div>
<woot-switch
size="small"
class="mx-1 mt-px mb-0"
:value="currentUserAutoOffline"
@input="updateAutoOffline"
/>
</WootDropdownItem>
<WootDropdownDivider />
</WootDropdownMenu>
</template>

View File

@@ -1,33 +1,3 @@
<template>
<aside class="flex h-full">
<primary-sidebar
:logo-source="globalConfig.logoThumbnail"
:installation-name="globalConfig.installationName"
:is-a-custom-branded-instance="isACustomBrandedInstance"
:account-id="accountId"
:menu-items="primaryMenuItems"
:active-menu-item="activePrimaryMenu.key"
@toggle-accounts="toggleAccountModal"
@key-shortcut-modal="toggleKeyShortcutModal"
@open-notification-panel="openNotificationPanel"
/>
<secondary-sidebar
v-if="showSecondarySidebar"
:class="sidebarClassName"
:account-id="accountId"
:inboxes="inboxes"
:labels="labels"
:teams="teams"
:custom-views="customViews"
:menu-config="activeSecondaryMenu"
:current-user="currentUser"
:is-on-chatwoot-cloud="isOnChatwootCloud"
@add-label="showAddLabelPopup"
@toggle-accounts="toggleAccountModal"
/>
</aside>
</template>
<script>
import { mapGetters } from 'vuex';
import { getSidebarItems } from './config/default-sidebar';
@@ -63,7 +33,6 @@ export default {
computed: {
...mapGetters({
accountId: 'getCurrentAccountId',
currentRole: 'getCurrentRole',
currentUser: 'getCurrentUser',
globalConfig: 'globalConfig/get',
inboxes: 'inboxes/getInboxes',
@@ -163,10 +132,10 @@ export default {
}
},
toggleKeyShortcutModal() {
this.$emit('open-key-shortcut-modal');
this.$emit('openKeyShortcutModal');
},
closeKeyShortcutModal() {
this.$emit('close-key-shortcut-modal');
this.$emit('closeKeyShortcutModal');
},
getKeyboardEvents() {
return {
@@ -198,14 +167,44 @@ export default {
window.$chatwoot.toggle();
},
toggleAccountModal() {
this.$emit('toggle-account-modal');
this.$emit('toggleAccountModal');
},
showAddLabelPopup() {
this.$emit('show-add-label-popup');
this.$emit('showAddLabelPopup');
},
openNotificationPanel() {
this.$emit('open-notification-panel');
this.$emit('openNotificationPanel');
},
},
};
</script>
<template>
<aside class="flex h-full">
<PrimarySidebar
:logo-source="globalConfig.logoThumbnail"
:installation-name="globalConfig.installationName"
:is-a-custom-branded-instance="isACustomBrandedInstance"
:account-id="accountId"
:menu-items="primaryMenuItems"
:active-menu-item="activePrimaryMenu.key"
@toggleAccounts="toggleAccountModal"
@openKeyShortcutModal="toggleKeyShortcutModal"
@openNotificationPanel="openNotificationPanel"
/>
<SecondarySidebar
v-if="showSecondarySidebar"
:class="sidebarClassName"
:account-id="accountId"
:inboxes="inboxes"
:labels="labels"
:teams="teams"
:custom-views="customViews"
:menu-config="activeSecondaryMenu"
:current-user="currentUser"
:is-on-chatwoot-cloud="isOnChatwootCloud"
@addLabel="showAddLabelPopup"
@toggleAccounts="toggleAccountModal"
/>
</aside>
</template>

View File

@@ -1,35 +1,3 @@
<template>
<div
v-if="showShowCurrentAccountContext"
class="text-slate-700 dark:text-slate-200 rounded-md text-xs py-2 px-2 mt-2 relative border border-slate-50 dark:border-slate-800/50 hover:bg-slate-50 dark:hover:bg-slate-800 cursor-pointer"
@mouseover="setShowSwitch"
@mouseleave="resetShowSwitch"
>
{{ $t('SIDEBAR.CURRENTLY_VIEWING_ACCOUNT') }}
<p
class="text-ellipsis overflow-hidden whitespace-nowrap font-medium mb-0 text-slate-800 dark:text-slate-100"
>
{{ account.name }}
</p>
<transition name="fade">
<div
v-if="showSwitchButton"
class="ltr:overlay-shadow ltr:dark:overlay-shadow-dark rtl:rtl-overlay-shadow rtl:dark:rtl-overlay-shadow-dark flex items-center h-full rounded-md justify-end absolute top-0 right-0 w-full"
>
<div class="my-0 mx-2">
<woot-button
variant="clear"
size="tiny"
icon="arrow-swap"
@click="$emit('toggle-accounts')"
>
{{ $t('SIDEBAR.SWITCH') }}
</woot-button>
</div>
</div>
</transition>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
@@ -56,6 +24,40 @@ export default {
},
};
</script>
<template>
<div
v-if="showShowCurrentAccountContext"
class="relative px-2 py-2 mt-2 text-xs border rounded-md cursor-pointer text-slate-700 dark:text-slate-200 border-slate-50 dark:border-slate-800/50 hover:bg-slate-50 dark:hover:bg-slate-800"
@mouseover="setShowSwitch"
@mouseleave="resetShowSwitch"
>
{{ $t('SIDEBAR.CURRENTLY_VIEWING_ACCOUNT') }}
<p
class="mb-0 overflow-hidden font-medium text-ellipsis whitespace-nowrap text-slate-800 dark:text-slate-100"
>
{{ account.name }}
</p>
<transition name="fade">
<div
v-if="showSwitchButton"
class="absolute top-0 right-0 flex items-center justify-end w-full h-full rounded-md ltr:overlay-shadow ltr:dark:overlay-shadow-dark rtl:rtl-overlay-shadow rtl:dark:rtl-overlay-shadow-dark"
>
<div class="mx-2 my-0">
<woot-button
variant="clear"
size="tiny"
icon="arrow-swap"
@click="$emit('toggleAccounts')"
>
{{ $t('SIDEBAR.SWITCH') }}
</woot-button>
</div>
</div>
</transition>
</div>
</template>
<style scoped>
@tailwind components;
@layer components {

View File

@@ -1,62 +1,3 @@
<template>
<woot-modal
:show="showAccountModal"
:on-close="() => $emit('close-account-modal')"
>
<woot-modal-header
:header-title="$t('SIDEBAR_ITEMS.CHANGE_ACCOUNTS')"
:header-content="$t('SIDEBAR_ITEMS.SELECTOR_SUBTITLE')"
/>
<div class="px-8 py-4">
<div
v-for="account in currentUser.accounts"
:id="`account-${account.id}`"
:key="account.id"
class="pt-0 pb-0"
>
<button
class="flex justify-between items-center expanded clear link cursor-pointer px-4 py-3 w-full rounded-lg hover:underline hover:bg-slate-25 dark:hover:bg-slate-900"
@click="onChangeAccount(account.id)"
>
<span class="w-full">
<label :for="account.name" class="text-left rtl:text-right">
<div
class="text-slate-700 text-lg dark:text-slate-100 font-medium hover:underline-offset-4 leading-5"
>
{{ account.name }}
</div>
<div
class="text-slate-500 text-xs dark:text-slate-500 font-medium hover:underline-offset-4"
>
{{ account.role }}
</div>
</label>
</span>
<fluent-icon
v-show="account.id === accountId"
class="text-slate-800 dark:text-slate-100"
icon="checkmark-circle"
type="solid"
size="24"
/>
</button>
</div>
</div>
<div
v-if="globalConfig.createNewAccountFromDashboard"
class="flex justify-end items-center px-8 pb-8 pt-4 gap-2"
>
<button
class="button success large expanded nice w-full"
@click="$emit('show-create-account-modal')"
>
{{ $t('CREATE_ACCOUNT.NEW_ACCOUNT') }}
</button>
</div>
</woot-modal>
</template>
<script>
import { mapGetters } from 'vuex';
export default {
@@ -82,3 +23,62 @@ export default {
},
};
</script>
<template>
<woot-modal
:show="showAccountModal"
:on-close="() => $emit('closeAccountModal')"
>
<woot-modal-header
:header-title="$t('SIDEBAR_ITEMS.CHANGE_ACCOUNTS')"
:header-content="$t('SIDEBAR_ITEMS.SELECTOR_SUBTITLE')"
/>
<div class="px-8 py-4">
<div
v-for="account in currentUser.accounts"
:id="`account-${account.id}`"
:key="account.id"
class="pt-0 pb-0"
>
<button
class="flex items-center justify-between w-full px-4 py-3 rounded-lg cursor-pointer expanded clear link hover:underline hover:bg-slate-25 dark:hover:bg-slate-900"
@click="onChangeAccount(account.id)"
>
<span class="w-full">
<label :for="account.name" class="text-left rtl:text-right">
<div
class="text-lg font-medium leading-5 text-slate-700 dark:text-slate-100 hover:underline-offset-4"
>
{{ account.name }}
</div>
<div
class="text-xs font-medium text-slate-500 dark:text-slate-500 hover:underline-offset-4"
>
{{ account.role }}
</div>
</label>
</span>
<fluent-icon
v-show="account.id === accountId"
class="text-slate-800 dark:text-slate-100"
icon="checkmark-circle"
type="solid"
size="24"
/>
</button>
</div>
</div>
<div
v-if="globalConfig.createNewAccountFromDashboard"
class="flex items-center justify-end gap-2 px-8 pt-4 pb-8"
>
<button
class="w-full button success large expanded nice"
@click="$emit('showCreateAccountModal')"
>
{{ $t('CREATE_ACCOUNT.NEW_ACCOUNT') }}
</button>
</div>
</woot-modal>
</template>

View File

@@ -1,53 +1,3 @@
<template>
<woot-modal
:show="show"
:on-close="() => $emit('close-account-create-modal')"
>
<div class="h-auto overflow-auto flex flex-col">
<woot-modal-header
:header-title="$t('CREATE_ACCOUNT.NEW_ACCOUNT')"
:header-content="$t('CREATE_ACCOUNT.SELECTOR_SUBTITLE')"
/>
<div v-if="!hasAccounts" class="text-sm mt-6 mx-8 mb-0">
<div class="items-center rounded-md flex alert">
<div class="ml-1 mr-3">
<fluent-icon icon="warning" />
</div>
{{ $t('CREATE_ACCOUNT.NO_ACCOUNT_WARNING') }}
</div>
</div>
<form class="flex flex-col w-full" @submit.prevent="addAccount">
<div class="w-full">
<label :class="{ error: v$.accountName.$error }">
{{ $t('CREATE_ACCOUNT.FORM.NAME.LABEL') }}
<input
v-model.trim="accountName"
type="text"
:placeholder="$t('CREATE_ACCOUNT.FORM.NAME.PLACEHOLDER')"
@input="v$.accountName.$touch"
/>
</label>
</div>
<div class="w-full">
<div class="w-full">
<woot-submit-button
:disabled="
v$.accountName.$invalid ||
v$.accountName.$invalid ||
uiFlags.isCreating
"
:button-text="$t('CREATE_ACCOUNT.FORM.SUBMIT')"
:loading="uiFlags.isCreating"
button-class="large expanded"
/>
</div>
</div>
</form>
</div>
</woot-modal>
</template>
<script>
import { useVuelidate } from '@vuelidate/core';
import { required, minLength } from '@vuelidate/validators';
@@ -90,7 +40,7 @@ export default {
const account_id = await this.$store.dispatch('accounts/create', {
account_name: this.accountName,
});
this.$emit('close-account-create-modal');
this.$emit('closeAccountCreateModal');
useAlert(this.$t('CREATE_ACCOUNT.API.SUCCESS_MESSAGE'));
window.location = `/app/accounts/${account_id}/dashboard`;
} catch (error) {
@@ -104,3 +54,50 @@ export default {
},
};
</script>
<template>
<woot-modal :show="show" :on-close="() => $emit('closeAccountCreateModal')">
<div class="flex flex-col h-auto overflow-auto">
<woot-modal-header
:header-title="$t('CREATE_ACCOUNT.NEW_ACCOUNT')"
:header-content="$t('CREATE_ACCOUNT.SELECTOR_SUBTITLE')"
/>
<div v-if="!hasAccounts" class="mx-8 mt-6 mb-0 text-sm">
<div class="flex items-center rounded-md alert">
<div class="ml-1 mr-3">
<fluent-icon icon="warning" />
</div>
{{ $t('CREATE_ACCOUNT.NO_ACCOUNT_WARNING') }}
</div>
</div>
<form class="flex flex-col w-full" @submit.prevent="addAccount">
<div class="w-full">
<label :class="{ error: v$.accountName.$error }">
{{ $t('CREATE_ACCOUNT.FORM.NAME.LABEL') }}
<input
v-model.trim="accountName"
type="text"
:placeholder="$t('CREATE_ACCOUNT.FORM.NAME.PLACEHOLDER')"
@input="v$.accountName.$touch"
/>
</label>
</div>
<div class="w-full">
<div class="w-full">
<woot-submit-button
:disabled="
v$.accountName.$invalid ||
v$.accountName.$invalid ||
uiFlags.isCreating
"
:button-text="$t('CREATE_ACCOUNT.FORM.SUBMIT')"
:loading="uiFlags.isCreating"
button-class="large expanded"
/>
</div>
</div>
</form>
</div>
</woot-modal>
</template>

View File

@@ -1,19 +1,3 @@
<template>
<woot-button
v-tooltip.right="$t(`SIDEBAR.PROFILE_SETTINGS`)"
variant="link"
class="items-center flex rounded-full"
@click="handleClick"
>
<thumbnail
:src="currentUser.avatar_url"
:username="currentUser.name"
:status="statusOfAgent"
should-show-status-always
size="32px"
/>
</woot-button>
</template>
<script>
import { mapGetters } from 'vuex';
import Thumbnail from '../../widgets/Thumbnail.vue';
@@ -33,8 +17,25 @@ export default {
},
methods: {
handleClick() {
this.$emit('toggle-menu');
this.$emit('toggleMenu');
},
},
};
</script>
<template>
<woot-button
v-tooltip.right="$t(`SIDEBAR.PROFILE_SETTINGS`)"
variant="link"
class="flex items-center rounded-full"
@click="handleClick"
>
<Thumbnail
:src="currentUser.avatar_url"
:username="currentUser.name"
:status="statusOfAgent"
should-show-status-always
size="32px"
/>
</woot-button>
</template>

View File

@@ -1,10 +1,3 @@
<template>
<div class="w-8 h-8">
<router-link :to="dashboardPath" replace>
<img :src="source" :alt="name" />
</router-link>
</div>
</template>
<script>
import { frontendURL } from 'dashboard/helper/URLHelper';
@@ -30,3 +23,11 @@ export default {
},
};
</script>
<template>
<div class="w-8 h-8">
<router-link :to="dashboardPath" replace>
<img :src="source" :alt="name" />
</router-link>
</div>
</template>

View File

@@ -1,7 +1,38 @@
<script>
import { mapGetters } from 'vuex';
export default {
computed: {
...mapGetters({
notificationMetadata: 'notifications/getMeta',
}),
unreadCount() {
if (!this.notificationMetadata.unreadCount) {
return '';
}
return this.notificationMetadata.unreadCount < 100
? `${this.notificationMetadata.unreadCount}`
: '99+';
},
isNotificationPanelActive() {
return this.$route.name === 'notifications_index';
},
},
methods: {
openNotificationPanel() {
if (this.$route.name !== 'notifications_index') {
this.$emit('openNotificationPanel');
}
},
},
};
</script>
<template>
<div class="mb-4">
<button
class="text-slate-600 dark:text-slate-100 w-10 h-10 my-2 p-0 flex items-center justify-center rounded-lg hover:bg-slate-25 dark:hover:bg-slate-700 dark:hover:text-slate-100 hover:text-slate-600 relative"
class="relative flex items-center justify-center w-10 h-10 p-0 my-2 rounded-lg text-slate-600 dark:text-slate-100 hover:bg-slate-25 dark:hover:bg-slate-700 dark:hover:text-slate-100 hover:text-slate-600"
:class="{
'bg-woot-50 dark:bg-slate-800 text-woot-500 hover:bg-woot-50':
isNotificationPanelActive,
@@ -23,34 +54,3 @@
</button>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
export default {
computed: {
...mapGetters({
accountId: 'getCurrentAccountId',
notificationMetadata: 'notifications/getMeta',
}),
unreadCount() {
if (!this.notificationMetadata.unreadCount) {
return '';
}
return this.notificationMetadata.unreadCount < 100
? `${this.notificationMetadata.unreadCount}`
: '99+';
},
isNotificationPanelActive() {
return this.$route.name === 'notifications_index';
},
},
methods: {
openNotificationPanel() {
if (this.$route.name !== 'notifications_index') {
this.$emit('open-notification-panel');
}
},
},
};
</script>

View File

@@ -1,110 +1,3 @@
<template>
<transition name="menu-slide">
<div
v-if="show"
v-on-clickaway="onClickAway"
class="left-3 rtl:left-auto rtl:right-3 bottom-16 w-64 absolute z-30 rounded-md shadow-xl bg-white dark:bg-slate-800 py-2 px-2 border border-slate-25 dark:border-slate-700"
:class="{ 'block visible': show }"
>
<availability-status />
<woot-dropdown-menu>
<woot-dropdown-item v-if="showChangeAccountOption">
<woot-button
variant="clear"
color-scheme="secondary"
size="small"
icon="arrow-swap"
@click="$emit('toggle-accounts')"
>
{{ $t('SIDEBAR_ITEMS.CHANGE_ACCOUNTS') }}
</woot-button>
</woot-dropdown-item>
<woot-dropdown-item v-if="globalConfig.chatwootInboxToken">
<woot-button
variant="clear"
color-scheme="secondary"
size="small"
icon="chat-help"
@click="$emit('show-support-chat-window')"
>
{{ $t('SIDEBAR_ITEMS.CONTACT_SUPPORT') }}
</woot-button>
</woot-dropdown-item>
<woot-dropdown-item>
<woot-button
variant="clear"
color-scheme="secondary"
size="small"
icon="keyboard"
@click="handleKeyboardHelpClick"
>
{{ $t('SIDEBAR_ITEMS.KEYBOARD_SHORTCUTS') }}
</woot-button>
</woot-dropdown-item>
<woot-dropdown-item>
<router-link
v-slot="{ href, isActive, navigate }"
:to="`/app/accounts/${accountId}/profile/settings`"
custom
>
<a
:href="href"
class="button small clear secondary bg-white dark:bg-slate-800 h-8"
:class="{ 'is-active': isActive }"
@click="e => handleProfileSettingClick(e, navigate)"
>
<fluent-icon icon="person" size="14" class="icon icon--font" />
<span class="button__content">
{{ $t('SIDEBAR_ITEMS.PROFILE_SETTINGS') }}
</span>
</a>
</router-link>
</woot-dropdown-item>
<woot-dropdown-item>
<woot-button
variant="clear"
color-scheme="secondary"
size="small"
icon="appearance"
@click="openAppearanceOptions"
>
{{ $t('SIDEBAR_ITEMS.APPEARANCE') }}
</woot-button>
</woot-dropdown-item>
<woot-dropdown-item v-if="currentUser.type === 'SuperAdmin'">
<a
href="/super_admin"
class="button small clear secondary bg-white dark:bg-slate-800 h-8"
target="_blank"
rel="noopener nofollow noreferrer"
@click="$emit('close')"
>
<fluent-icon
icon="content-settings"
size="14"
class="icon icon--font"
/>
<span class="button__content">
{{ $t('SIDEBAR_ITEMS.SUPER_ADMIN_CONSOLE') }}
</span>
</a>
</woot-dropdown-item>
<woot-dropdown-item>
<woot-button
variant="clear"
color-scheme="secondary"
size="small"
icon="power"
@click="logout"
>
{{ $t('SIDEBAR_ITEMS.LOGOUT') }}
</woot-button>
</woot-dropdown-item>
</woot-dropdown-menu>
</div>
</transition>
</template>
<script>
import { mapGetters } from 'vuex';
import Auth from '../../../api/auth';
@@ -145,7 +38,7 @@ export default {
navigate(e);
},
handleKeyboardHelpClick() {
this.$emit('key-shortcut-modal');
this.$emit('openKeyShortcutModal');
this.$emit('close');
},
logout() {
@@ -161,3 +54,110 @@ export default {
},
};
</script>
<template>
<transition name="menu-slide">
<div
v-if="show"
v-on-clickaway="onClickAway"
class="absolute z-30 w-64 px-2 py-2 bg-white border rounded-md shadow-xl left-3 rtl:left-auto rtl:right-3 bottom-16 dark:bg-slate-800 border-slate-25 dark:border-slate-700"
:class="{ 'block visible': show }"
>
<AvailabilityStatus />
<WootDropdownMenu>
<WootDropdownItem v-if="showChangeAccountOption">
<woot-button
variant="clear"
color-scheme="secondary"
size="small"
icon="arrow-swap"
@click="$emit('toggleAccounts')"
>
{{ $t('SIDEBAR_ITEMS.CHANGE_ACCOUNTS') }}
</woot-button>
</WootDropdownItem>
<WootDropdownItem v-if="globalConfig.chatwootInboxToken">
<woot-button
variant="clear"
color-scheme="secondary"
size="small"
icon="chat-help"
@click="$emit('showSupportChatWindow')"
>
{{ $t('SIDEBAR_ITEMS.CONTACT_SUPPORT') }}
</woot-button>
</WootDropdownItem>
<WootDropdownItem>
<woot-button
variant="clear"
color-scheme="secondary"
size="small"
icon="keyboard"
@click="handleKeyboardHelpClick"
>
{{ $t('SIDEBAR_ITEMS.KEYBOARD_SHORTCUTS') }}
</woot-button>
</WootDropdownItem>
<WootDropdownItem>
<router-link
v-slot="{ href, isActive, navigate }"
:to="`/app/accounts/${accountId}/profile/settings`"
custom
>
<a
:href="href"
class="h-8 bg-white button small clear secondary dark:bg-slate-800"
:class="{ 'is-active': isActive }"
@click="e => handleProfileSettingClick(e, navigate)"
>
<fluent-icon icon="person" size="14" class="icon icon--font" />
<span class="button__content">
{{ $t('SIDEBAR_ITEMS.PROFILE_SETTINGS') }}
</span>
</a>
</router-link>
</WootDropdownItem>
<WootDropdownItem>
<woot-button
variant="clear"
color-scheme="secondary"
size="small"
icon="appearance"
@click="openAppearanceOptions"
>
{{ $t('SIDEBAR_ITEMS.APPEARANCE') }}
</woot-button>
</WootDropdownItem>
<WootDropdownItem v-if="currentUser.type === 'SuperAdmin'">
<a
href="/super_admin"
class="h-8 bg-white button small clear secondary dark:bg-slate-800"
target="_blank"
rel="noopener nofollow noreferrer"
@click="$emit('close')"
>
<fluent-icon
icon="content-settings"
size="14"
class="icon icon--font"
/>
<span class="button__content">
{{ $t('SIDEBAR_ITEMS.SUPER_ADMIN_CONSOLE') }}
</span>
</a>
</WootDropdownItem>
<WootDropdownItem>
<woot-button
variant="clear"
color-scheme="secondary"
size="small"
icon="power"
@click="logout"
>
{{ $t('SIDEBAR_ITEMS.LOGOUT') }}
</woot-button>
</WootDropdownItem>
</WootDropdownMenu>
</div>
</transition>
</template>

View File

@@ -1,43 +1,3 @@
<template>
<div
class="h-full w-16 bg-white dark:bg-slate-900 border-r border-slate-50 dark:border-slate-800/50 rtl:border-l rtl:border-r-0 flex justify-between flex-col"
>
<div class="flex flex-col items-center">
<logo
:source="logoSource"
:name="installationName"
:account-id="accountId"
class="m-4 mb-10"
/>
<primary-nav-item
v-for="menuItem in menuItems"
:key="menuItem.toState"
:icon="menuItem.icon"
:name="menuItem.label"
:to="menuItem.toState"
:is-child-menu-active="menuItem.key === activeMenuItem"
/>
</div>
<div class="flex flex-col items-center justify-end pb-6">
<primary-nav-item
v-if="!isACustomBrandedInstance"
icon="book-open-globe"
name="DOCS"
:open-in-new-page="true"
:to="helpDocsURL"
/>
<notification-bell @open-notification-panel="openNotificationPanel" />
<agent-details @toggle-menu="toggleOptions" />
<options-menu
:show="showOptionsMenu"
@toggle-accounts="toggleAccountModal"
@show-support-chat-window="toggleSupportChatWindow"
@key-shortcut-modal="$emit('key-shortcut-modal')"
@close="toggleOptions"
/>
</div>
</div>
</template>
<script>
import Logo from './Logo.vue';
import PrimaryNavItem from './PrimaryNavItem.vue';
@@ -94,15 +54,56 @@ export default {
this.showOptionsMenu = !this.showOptionsMenu;
},
toggleAccountModal() {
this.$emit('toggle-accounts');
this.$emit('toggleAccounts');
},
toggleSupportChatWindow() {
window.$chatwoot.toggle();
},
openNotificationPanel() {
this.$track(ACCOUNT_EVENTS.OPENED_NOTIFICATIONS);
this.$emit('open-notification-panel');
this.$emit('openNotificationPanel');
},
},
};
</script>
<template>
<div
class="flex flex-col justify-between w-16 h-full bg-white border-r dark:bg-slate-900 border-slate-50 dark:border-slate-800/50 rtl:border-l rtl:border-r-0"
>
<div class="flex flex-col items-center">
<Logo
:source="logoSource"
:name="installationName"
:account-id="accountId"
class="m-4 mb-10"
/>
<PrimaryNavItem
v-for="menuItem in menuItems"
:key="menuItem.toState"
:icon="menuItem.icon"
:name="menuItem.label"
:to="menuItem.toState"
:is-child-menu-active="menuItem.key === activeMenuItem"
/>
</div>
<div class="flex flex-col items-center justify-end pb-6">
<PrimaryNavItem
v-if="!isACustomBrandedInstance"
icon="book-open-globe"
name="DOCS"
open-in-new-page
:to="helpDocsURL"
/>
<NotificationBell @openNotificationPanel="openNotificationPanel" />
<AgentDetails @toggleMenu="toggleOptions" />
<OptionsMenu
:show="showOptionsMenu"
@toggleAccounts="toggleAccountModal"
@showSupportChatWindow="toggleSupportChatWindow"
@openKeyShortcutModal="$emit('openKeyShortcutModal')"
@close="toggleOptions"
/>
</div>
</div>
</template>

View File

@@ -1,33 +1,3 @@
<template>
<router-link v-slot="{ href, isActive, navigate }" :to="to" custom>
<a
v-tooltip.right="$t(`SIDEBAR.${name}`)"
:href="href"
class="text-slate-700 dark:text-slate-100 w-10 h-10 my-2 flex items-center justify-center rounded-lg hover:bg-slate-25 dark:hover:bg-slate-700 dark:hover:text-slate-100 hover:text-slate-600 relative"
:class="{
'bg-woot-50 dark:bg-slate-800 text-woot-500 hover:bg-woot-50':
isActive || isChildMenuActive,
}"
:rel="openInNewPage ? 'noopener noreferrer nofollow' : undefined"
:target="openInNewPage ? '_blank' : undefined"
@click="navigate"
>
<fluent-icon
:icon="icon"
:class="{
'text-woot-500': isActive || isChildMenuActive,
}"
/>
<span class="sr-only">{{ name }}</span>
<span
v-if="count"
class="text-black-900 bg-yellow-500 absolute -top-1 -right-1"
>
{{ count }}
</span>
</a>
</router-link>
</template>
<script>
export default {
props: {
@@ -58,3 +28,34 @@ export default {
},
};
</script>
<template>
<router-link v-slot="{ href, isActive, navigate }" :to="to" custom>
<a
v-tooltip.right="$t(`SIDEBAR.${name}`)"
:href="href"
class="text-slate-700 dark:text-slate-100 w-10 h-10 my-2 flex items-center justify-center rounded-lg hover:bg-slate-25 dark:hover:bg-slate-700 dark:hover:text-slate-100 hover:text-slate-600 relative"
:class="{
'bg-woot-50 dark:bg-slate-800 text-woot-500 hover:bg-woot-50':
isActive || isChildMenuActive,
}"
:rel="openInNewPage ? 'noopener noreferrer nofollow' : undefined"
:target="openInNewPage ? '_blank' : undefined"
@click="navigate"
>
<fluent-icon
:icon="icon"
:class="{
'text-woot-500': isActive || isChildMenuActive,
}"
/>
<span class="sr-only">{{ name }}</span>
<span
v-if="count"
class="text-black-900 bg-yellow-500 absolute -top-1 -right-1"
>
{{ count }}
</span>
</a>
</router-link>
</template>

View File

@@ -1,28 +1,3 @@
<template>
<div
v-if="hasSecondaryMenu"
class="h-full overflow-auto w-48 flex flex-col bg-white dark:bg-slate-900 border-r dark:border-slate-800/50 rtl:border-r-0 rtl:border-l border-slate-50 text-sm px-2 pb-8"
>
<account-context @toggle-accounts="toggleAccountModal" />
<transition-group
name="menu-list"
tag="ul"
class="pt-2 list-none ml-0 mb-0"
>
<secondary-nav-item
v-for="menuItem in accessibleMenuItems"
:key="menuItem.toState"
:menu-item="menuItem"
/>
<secondary-nav-item
v-for="menuItem in additionalSecondaryMenuItems[menuConfig.parentNav]"
:key="menuItem.key"
:menu-item="menuItem"
@add-label="showAddLabelPopup"
/>
</transition-group>
</div>
</template>
<script>
import { frontendURL } from '../../../helper/URLHelper';
import SecondaryNavItem from './SecondaryNavItem.vue';
@@ -248,10 +223,10 @@ export default {
},
methods: {
showAddLabelPopup() {
this.$emit('add-label');
this.$emit('addLabel');
},
toggleAccountModal() {
this.$emit('toggle-accounts');
this.$emit('toggleAccounts');
},
showNewLink(featureFlag) {
return this.isFeatureEnabledonAccount(this.accountId, featureFlag);
@@ -259,3 +234,29 @@ export default {
},
};
</script>
<template>
<div
v-if="hasSecondaryMenu"
class="flex flex-col w-48 h-full px-2 pb-8 overflow-auto text-sm bg-white border-r dark:bg-slate-900 dark:border-slate-800/50 rtl:border-r-0 rtl:border-l border-slate-50"
>
<AccountContext @toggleAccounts="toggleAccountModal" />
<transition-group
name="menu-list"
tag="ul"
class="pt-2 mb-0 ml-0 list-none"
>
<SecondaryNavItem
v-for="menuItem in accessibleMenuItems"
:key="menuItem.toState"
:menu-item="menuItem"
/>
<SecondaryNavItem
v-for="menuItem in additionalSecondaryMenuItems[menuConfig.parentNav]"
:key="menuItem.key"
:menu-item="menuItem"
@addLabel="showAddLabelPopup"
/>
</transition-group>
</div>
</template>

View File

@@ -1,3 +1,55 @@
<script>
export default {
props: {
to: {
type: String,
default: '',
},
label: {
type: String,
default: '',
},
labelColor: {
type: String,
default: '',
},
shouldTruncate: {
type: Boolean,
default: false,
},
icon: {
type: String,
default: '',
},
warningIcon: {
type: String,
default: '',
},
showChildCount: {
type: Boolean,
default: false,
},
childItemCount: {
type: Number,
default: 0,
},
},
computed: {
showIcon() {
return {
'overflow-hidden whitespace-nowrap text-ellipsis': this.shouldTruncate,
};
},
isCountZero() {
return this.childItemCount === 0;
},
menuTitle() {
return this.shouldTruncate ? this.label : '';
},
},
};
</script>
<template>
<router-link
v-slot="{ href, isActive, navigate }"
@@ -78,54 +130,3 @@
</li>
</router-link>
</template>
<script>
export default {
props: {
to: {
type: String,
default: '',
},
label: {
type: String,
default: '',
},
labelColor: {
type: String,
default: '',
},
shouldTruncate: {
type: Boolean,
default: false,
},
icon: {
type: String,
default: '',
},
warningIcon: {
type: String,
default: '',
},
showChildCount: {
type: Boolean,
default: false,
},
childItemCount: {
type: Number,
default: 0,
},
},
computed: {
showIcon() {
return {
'overflow-hidden whitespace-nowrap text-ellipsis': this.shouldTruncate,
};
},
isCountZero() {
return this.childItemCount === 0;
},
menuTitle() {
return this.shouldTruncate ? this.label : '';
},
},
};
</script>

View File

@@ -1,97 +1,3 @@
<template>
<li v-show="isMenuItemVisible" class="mt-1">
<div v-if="hasSubMenu" class="flex justify-between">
<span
class="px-2 pt-1 my-2 text-sm font-semibold text-slate-700 dark:text-slate-200"
>
{{ $t(`SIDEBAR.${menuItem.label}`) }}
</span>
<div v-if="menuItem.showNewButton" class="flex items-center">
<woot-button
size="tiny"
variant="clear"
color-scheme="secondary"
icon="add"
class="p-0 ml-2"
@click="onClickOpen"
/>
</div>
</div>
<router-link
v-else
class="flex items-center p-2 m-0 text-sm font-medium leading-4 rounded-lg text-slate-700 dark:text-slate-100 hover:bg-slate-25 dark:hover:bg-slate-800"
:class="computedClass"
:to="menuItem && menuItem.toState"
>
<fluent-icon
:icon="menuItem.icon"
class="min-w-[1rem] mr-1.5 rtl:mr-0 rtl:ml-1.5"
size="14"
/>
{{ $t(`SIDEBAR.${menuItem.label}`) }}
<span
v-if="showChildCount(menuItem.count)"
class="px-1 py-0 mx-1 font-medium rounded-md text-xxs"
:class="{
'text-slate-300 dark:text-slate-600': isCountZero && !isActiveView,
'text-slate-600 dark:text-slate-50': !isCountZero && !isActiveView,
'bg-woot-75 dark:bg-woot-200 text-woot-600 dark:text-woot-600':
isActiveView,
'bg-slate-50 dark:bg-slate-700': !isActiveView,
}"
>
{{ `${menuItem.count}` }}
</span>
<span
v-if="menuItem.beta"
data-view-component="true"
label="Beta"
class="inline-block px-1 mx-1 font-medium leading-4 text-green-500 border border-green-400 rounded-lg text-xxs"
>
{{ $t('SIDEBAR.BETA') }}
</span>
</router-link>
<ul v-if="hasSubMenu" class="mb-0 ml-0 list-none">
<secondary-child-nav-item
v-for="child in menuItem.children"
:key="child.id"
:to="child.toState"
:label="child.label"
:label-color="child.color"
:should-truncate="child.truncateLabel"
:icon="computedInboxClass(child)"
:warning-icon="computedInboxErrorClass(child)"
:show-child-count="showChildCount(child.count)"
:child-item-count="child.count"
/>
<Policy :permissions="['administrator']">
<router-link
v-if="menuItem.newLink"
v-slot="{ href, navigate }"
:to="menuItem.toState"
custom
>
<li class="pl-1">
<a :href="href">
<woot-button
size="tiny"
variant="clear"
color-scheme="secondary"
icon="add"
:data-testid="menuItem.dataTestid"
@click="e => newLinkClick(e, navigate)"
>
{{ $t(`SIDEBAR.${menuItem.newLinkTag}`) }}
</woot-button>
</a>
</li>
</router-link>
</Policy>
</ul>
</li>
</template>
<script>
import { mapGetters } from 'vuex';
import { useAdmin } from 'dashboard/composables/useAdmin';
@@ -263,7 +169,7 @@ export default {
} else if (this.menuItem.showModalForNewItem) {
if (this.menuItem.modalName === 'AddLabel') {
e.preventDefault();
this.$emit('add-label');
this.$emit('addLabel');
}
}
},
@@ -279,3 +185,97 @@ export default {
},
};
</script>
<template>
<li v-show="isMenuItemVisible" class="mt-1">
<div v-if="hasSubMenu" class="flex justify-between">
<span
class="px-2 pt-1 my-2 text-sm font-semibold text-slate-700 dark:text-slate-200"
>
{{ $t(`SIDEBAR.${menuItem.label}`) }}
</span>
<div v-if="menuItem.showNewButton" class="flex items-center">
<woot-button
size="tiny"
variant="clear"
color-scheme="secondary"
icon="add"
class="p-0 ml-2"
@click="onClickOpen"
/>
</div>
</div>
<router-link
v-else
class="flex items-center p-2 m-0 text-sm font-medium leading-4 rounded-lg text-slate-700 dark:text-slate-100 hover:bg-slate-25 dark:hover:bg-slate-800"
:class="computedClass"
:to="menuItem && menuItem.toState"
>
<fluent-icon
:icon="menuItem.icon"
class="min-w-[1rem] mr-1.5 rtl:mr-0 rtl:ml-1.5"
size="14"
/>
{{ $t(`SIDEBAR.${menuItem.label}`) }}
<span
v-if="showChildCount(menuItem.count)"
class="px-1 py-0 mx-1 font-medium rounded-md text-xxs"
:class="{
'text-slate-300 dark:text-slate-600': isCountZero && !isActiveView,
'text-slate-600 dark:text-slate-50': !isCountZero && !isActiveView,
'bg-woot-75 dark:bg-woot-200 text-woot-600 dark:text-woot-600':
isActiveView,
'bg-slate-50 dark:bg-slate-700': !isActiveView,
}"
>
{{ `${menuItem.count}` }}
</span>
<span
v-if="menuItem.beta"
data-view-component="true"
label="Beta"
class="inline-block px-1 mx-1 font-medium leading-4 text-green-500 border border-green-400 rounded-lg text-xxs"
>
{{ $t('SIDEBAR.BETA') }}
</span>
</router-link>
<ul v-if="hasSubMenu" class="mb-0 ml-0 list-none">
<SecondaryChildNavItem
v-for="child in menuItem.children"
:key="child.id"
:to="child.toState"
:label="child.label"
:label-color="child.color"
:should-truncate="child.truncateLabel"
:icon="computedInboxClass(child)"
:warning-icon="computedInboxErrorClass(child)"
:show-child-count="showChildCount(child.count)"
:child-item-count="child.count"
/>
<Policy :permissions="['administrator']">
<router-link
v-if="menuItem.newLink"
v-slot="{ href, navigate }"
:to="menuItem.toState"
custom
>
<li class="pl-1">
<a :href="href">
<woot-button
size="tiny"
variant="clear"
color-scheme="secondary"
icon="add"
:data-testid="menuItem.dataTestid"
@click="e => newLinkClick(e, navigate)"
>
{{ $t(`SIDEBAR.${menuItem.newLinkTag}`) }}
</woot-button>
</a>
</li>
</router-link>
</Policy>
</ul>
</li>
</template>

View File

@@ -1,24 +1,3 @@
<template>
<div class="announcement-popup">
<span v-if="popupMessage" class="popup-content">
{{ popupMessage }}
<span v-if="routeText" class="route-url" @click="onClickOpenPath">
{{ routeText }}
</span>
</span>
<div v-if="hasCloseButton" class="popup-close">
<woot-button
v-if="hasCloseButton"
color-scheme="primary"
variant="link"
size="small"
@click="onClickClose"
>
{{ closeButtonText }}
</woot-button>
</div>
</div>
</template>
<script>
export default {
props: {
@@ -50,6 +29,28 @@ export default {
};
</script>
<template>
<div class="announcement-popup">
<span v-if="popupMessage" class="popup-content">
{{ popupMessage }}
<span v-if="routeText" class="route-url" @click="onClickOpenPath">
{{ routeText }}
</span>
</span>
<div v-if="hasCloseButton" class="popup-close">
<woot-button
v-if="hasCloseButton"
color-scheme="primary"
variant="link"
size="small"
@click="onClickClose"
>
{{ closeButtonText }}
</woot-button>
</div>
</div>
</template>
<style lang="scss">
.announcement-popup {
max-width: 15rem;

View File

@@ -1,45 +1,3 @@
<template>
<div
class="flex items-center justify-center h-12 gap-4 px-4 py-3 text-xs text-white banner dark:text-white"
:class="bannerClasses"
>
<span class="banner-message">
{{ bannerMessage }}
<a
v-if="hrefLink"
:href="hrefLink"
rel="noopener noreferrer nofollow"
target="_blank"
>
{{ hrefLinkText }}
</a>
</span>
<div class="actions">
<woot-button
v-if="hasActionButton"
size="tiny"
:icon="actionButtonIcon"
:variant="actionButtonVariant"
color-scheme="primary"
class-names="banner-action__button"
@click="onClick"
>
{{ actionButtonLabel }}
</woot-button>
<woot-button
v-if="hasCloseButton"
size="tiny"
:color-scheme="colorScheme"
icon="dismiss-circle"
class-names="banner-action__button"
@click="onClickClose"
>
{{ $t('GENERAL_SETTINGS.DISMISS') }}
</woot-button>
</div>
</div>
</template>
<script>
export default {
props: {
@@ -101,6 +59,48 @@ export default {
};
</script>
<template>
<div
class="flex items-center justify-center h-12 gap-4 px-4 py-3 text-xs text-white banner dark:text-white"
:class="bannerClasses"
>
<span class="banner-message">
{{ bannerMessage }}
<a
v-if="hrefLink"
:href="hrefLink"
rel="noopener noreferrer nofollow"
target="_blank"
>
{{ hrefLinkText }}
</a>
</span>
<div class="actions">
<woot-button
v-if="hasActionButton"
size="tiny"
:icon="actionButtonIcon"
:variant="actionButtonVariant"
color-scheme="primary"
class-names="banner-action__button"
@click="onClick"
>
{{ actionButtonLabel }}
</woot-button>
<woot-button
v-if="hasCloseButton"
size="tiny"
:color-scheme="colorScheme"
icon="dismiss-circle"
class-names="banner-action__button"
@click="onClickClose"
>
{{ $t('GENERAL_SETTINGS.DISMISS') }}
</woot-button>
</div>
</div>
</template>
<style lang="scss" scoped>
.banner {
&.primary {

View File

@@ -1,13 +1,3 @@
<template>
<div
class="fixed outline-none z-[9999] cursor-pointer"
:style="style"
tabindex="0"
@blur="$emit('close')"
>
<slot />
</div>
</template>
<script>
export default {
props: {
@@ -40,3 +30,14 @@ export default {
},
};
</script>
<template>
<div
class="fixed outline-none z-[9999] cursor-pointer"
:style="style"
tabindex="0"
@blur="$emit('close')"
>
<slot />
</div>
</template>

View File

@@ -31,6 +31,7 @@ import CalendarMonth from './components/CalendarMonth.vue';
import CalendarWeek from './components/CalendarWeek.vue';
import CalendarFooter from './components/CalendarFooter.vue';
const emit = defineEmits(['dateRangeChanged']);
const { LAST_7_DAYS, LAST_30_DAYS, CUSTOM_RANGE } = DATE_RANGE_TYPES;
const { START_CALENDAR, END_CALENDAR } = CALENDAR_TYPES;
const { WEEK, MONTH, YEAR } = CALENDAR_PERIODS;
@@ -54,8 +55,6 @@ const hoveredEndDate = ref(null);
const manualStartDate = ref(selectedStartDate.value);
const manualEndDate = ref(selectedEndDate.value);
const emit = defineEmits(['change']);
// Watcher will set the start and end dates based on the selected range
watch(selectedRange, newRange => {
if (newRange !== CUSTOM_RANGE) {
@@ -223,7 +222,7 @@ const emitDateRange = () => {
>
<CalendarDateRange
:selected-range="selectedRange"
@set-range="setDateRange"
@setRange="setDateRange"
/>
<div
class="flex flex-col w-[680px] ltr:border-l rtl:border-r border-slate-50 dark:border-slate-700/50"
@@ -265,15 +264,15 @@ const emitDateRange = () => {
:calendar-type="calendar"
:start-current-date="startCurrentDate"
:end-current-date="endCurrentDate"
@select-year="openCalendar($event, calendar, YEAR)"
@selectYear="openCalendar($event, calendar, YEAR)"
/>
<CalendarMonth
v-else-if="calendarViews[calendar] === MONTH"
:calendar-type="calendar"
:start-current-date="startCurrentDate"
:end-current-date="endCurrentDate"
@select-month="openCalendar($event, calendar)"
@set-view="setViewMode"
@selectMonth="openCalendar($event, calendar)"
@setView="setViewMode"
@prev="moveCalendar(calendar, 'prev', YEAR)"
@next="moveCalendar(calendar, 'next', YEAR)"
/>
@@ -287,9 +286,9 @@ const emitDateRange = () => {
:selected-end-date="selectedEndDate"
:selecting-end-date="selectingEndDate"
:hovered-end-date="hoveredEndDate"
@update-hovered-end-date="hoveredEndDate = $event"
@select-date="selectDate"
@set-view="setViewMode"
@updateHoveredEndDate="hoveredEndDate = $event"
@selectDate="selectDate"
@setView="setViewMode"
@prev="moveCalendar(calendar, 'prev')"
@next="moveCalendar(calendar, 'next')"
/>

View File

@@ -19,7 +19,7 @@ defineProps({
default: '',
},
});
const emit = defineEmits(['prev', 'next', 'set-view']);
const emit = defineEmits(['prev', 'next', 'setView']);
const { YEAR } = CALENDAR_PERIODS;
@@ -32,7 +32,7 @@ const onClickNext = type => {
};
const onClickSetView = (type, mode) => {
emit('set-view', type, mode);
emit('setView', type, mode);
};
</script>

View File

@@ -8,10 +8,10 @@ defineProps({
},
});
const emit = defineEmits(['set-range']);
const emit = defineEmits(['setRange']);
const setDateRange = range => {
emit('set-range', range);
emit('setRange', range);
};
</script>

View File

@@ -1,5 +1,5 @@
<script setup>
const emit = defineEmits(['clear', 'apply']);
const emit = defineEmits(['clear', 'clear']);
const onClickClear = () => {
emit('clear');

View File

@@ -18,6 +18,7 @@ const props = defineProps({
endCurrentDate: Date,
});
const emit = defineEmits(['selectMonth', 'prev', 'next', 'setView']);
const { START_CALENDAR } = CALENDAR_TYPES;
const { MONTH, YEAR } = CALENDAR_PERIODS;
@@ -33,10 +34,8 @@ const activeMonthIndex = computed(() => {
return getMonth(date);
});
const emit = defineEmits(['select-month', 'prev', 'next', 'set-view']);
const setViewMode = (type, mode) => {
emit('set-view', type, mode);
emit('setView', type, mode);
};
const onClickPrev = () => {
@@ -48,7 +47,7 @@ const onClickNext = () => {
};
const selectMonth = index => {
emit('select-month', index);
emit('selectMonth', index);
};
</script>
@@ -63,7 +62,7 @@ const selectMonth = index => {
MONTH
)
"
@set-view="setViewMode"
@setView="setViewMode"
@prev="onClickPrev"
@next="onClickNext"
/>

View File

@@ -31,22 +31,22 @@ const props = defineProps({
});
const emit = defineEmits([
'update-hovered-end-date',
'select-date',
'updateHoveredEndDate',
'selectDate',
'prev',
'next',
'set-view',
'setView',
]);
const { START_CALENDAR } = CALENDAR_TYPES;
const { MONTH } = CALENDAR_PERIODS;
const emitHoveredEndDate = day => {
emit('update-hovered-end-date', day);
emit('updateHoveredEndDate', day);
};
const emitSelectDate = day => {
emit('select-date', day);
emit('selectDate', day);
};
const onClickPrev = () => {
emit('prev');
@@ -57,7 +57,7 @@ const onClickNext = () => {
};
const setViewMode = (type, mode) => {
emit('set-view', type, mode);
emit('setView', type, mode);
};
const weeks = calendarType => {
@@ -139,7 +139,7 @@ const dayClasses = day => ({
"
@prev="onClickPrev"
@next="onClickNext"
@set-view="setViewMode"
@setView="setViewMode"
/>
<CalendarWeekLabel />
<div

View File

@@ -14,6 +14,8 @@ const props = defineProps({
endCurrentDate: Date,
});
const emit = defineEmits(['selectYear']);
const { START_CALENDAR } = CALENDAR_TYPES;
const calculateStartYear = date => {
@@ -52,10 +54,8 @@ const onClickNext = () => {
startYear.value = addYears(new Date(startYear.value, 0, 1), 10).getFullYear();
};
const emit = defineEmits(['select-year']);
const selectYear = year => {
emit('select-year', year);
emit('selectYear', year);
};
</script>

View File

@@ -12,6 +12,8 @@ const props = defineProps({
},
});
const emit = defineEmits(['open']);
const formatDateRange = computed(() => {
const startDate = props.selectedStartDate;
const endDate = props.selectedEndDate;
@@ -39,8 +41,6 @@ const activeDateRange = computed(
() => dateRanges.find(range => range.value === props.selectedRange).label
);
const emit = defineEmits(['open']);
const openDatePicker = () => {
emit('open');
};

View File

@@ -1,18 +1,3 @@
<template>
<div class="date-picker">
<date-picker
:range="true"
:confirm="true"
:clearable="false"
:editable="false"
:confirm-text="confirmText"
:placeholder="placeholder"
:value="value"
@change="handleChange"
/>
</div>
</template>
<script>
import DatePicker from 'vue2-datepicker';
export default {
@@ -38,3 +23,18 @@ export default {
},
};
</script>
<template>
<div class="date-picker">
<DatePicker
range
confirm
:clearable="false"
:editable="false"
:confirm-text="confirmText"
:placeholder="placeholder"
:value="value"
@change="handleChange"
/>
</div>
</template>

View File

@@ -1,19 +1,3 @@
<template>
<div class="date-picker">
<date-picker
type="datetime"
:confirm="true"
:clearable="false"
:editable="false"
:confirm-text="confirmText"
:placeholder="placeholder"
:value="value"
:disabled-date="disableBeforeToday"
@change="handleChange"
/>
</div>
</template>
<script>
import addDays from 'date-fns/addDays';
import DatePicker from 'vue2-datepicker';
@@ -45,3 +29,19 @@ export default {
},
};
</script>
<template>
<div class="date-picker">
<DatePicker
type="datetime"
confirm
:clearable="false"
:editable="false"
:confirm-text="confirmText"
:placeholder="placeholder"
:value="value"
:disabled-date="disableBeforeToday"
@change="handleChange"
/>
</div>
</template>

View File

@@ -14,6 +14,7 @@ defineProps({
},
});
</script>
<template>
<button
class="inline-flex relative items-center p-1.5 w-fit h-8 gap-1.5 rounded-lg hover:bg-slate-50 dark:hover:bg-slate-800 active:bg-slate-75 dark:active:bg-slate-800"

View File

@@ -6,6 +6,7 @@ defineProps({
},
});
</script>
<template>
<div
class="flex items-center justify-center h-10 text-sm text-slate-500 dark:text-slate-300"

View File

@@ -38,13 +38,13 @@ const props = defineProps({
},
});
const emits = defineEmits(['on-search']);
const emit = defineEmits(['onSearch']);
const searchTerm = ref('');
const onSearch = debounce(value => {
searchTerm.value = value;
emits('on-search', value);
emit('onSearch', value);
}, 300);
const filteredListItems = computed(() => {
@@ -71,13 +71,14 @@ const shouldShowEmptyState = computed(() => {
return !props.isLoading && isDropdownListEmpty.value;
});
</script>
<template>
<div
class="absolute z-20 w-40 bg-white border shadow dark:bg-slate-800 rounded-xl border-slate-50 dark:border-slate-700/50 max-h-[400px]"
@click.stop
>
<slot name="search">
<dropdown-search
<DropdownSearch
v-if="enableSearch"
:input-value="searchTerm"
:input-placeholder="inputPlaceholder"
@@ -87,15 +88,15 @@ const shouldShowEmptyState = computed(() => {
/>
</slot>
<slot name="listItem">
<dropdown-loading-state
<DropdownLoadingState
v-if="shouldShowLoadingState"
:message="loadingPlaceholder"
/>
<dropdown-empty-state
<DropdownEmptyState
v-else-if="shouldShowEmptyState"
:message="$t('REPORT.FILTER_ACTIONS.EMPTY_LIST')"
/>
<list-item-button
<ListItemButton
v-for="item in filteredListItems"
:key="item.id"
:is-active="isFilterActive(item.id)"

View File

@@ -10,6 +10,7 @@ defineProps({
},
});
</script>
<template>
<button
class="relative inline-flex items-center justify-start w-full p-3 border-0 rounded-none first:rounded-t-xl last:rounded-b-xl h-11 hover:bg-slate-50 dark:hover:bg-slate-700 active:bg-slate-75 dark:active:bg-slate-800"

View File

@@ -6,6 +6,7 @@ defineProps({
},
});
</script>
<template>
<div
class="flex items-center justify-center h-10 text-sm text-slate-500 dark:text-slate-300"

View File

@@ -14,6 +14,7 @@ defineProps({
},
});
</script>
<template>
<div
class="flex items-center justify-between h-10 min-h-[40px] sticky top-0 bg-white z-10 dark:bg-slate-800 gap-2 px-3 border-b rounded-t-xl border-slate-50 dark:border-slate-700"

View File

@@ -6,6 +6,7 @@ defineProps({
},
});
</script>
<template>
<div class="relative group w-[inherit] whitespace-normal z-20">
<fluent-icon

View File

@@ -1,32 +1,3 @@
<template>
<div
class="inline-flex ltr:mr-1 rtl:ml-1 mb-1"
:class="labelClass"
:style="labelStyle"
:title="description"
>
<span v-if="icon" class="label-action--button">
<fluent-icon :icon="icon" size="12" class="label--icon cursor-pointer" />
</span>
<span
v-if="['smooth', 'dashed'].includes(variant) && title && !icon"
:style="{ background: color }"
class="label-color-dot flex-shrink-0"
/>
<span v-if="!href" class="whitespace-nowrap text-ellipsis overflow-hidden">
{{ title }}
</span>
<a v-else :href="href" :style="anchorStyle">{{ title }}</a>
<button
v-if="showClose"
class="label-close--button p-0"
:style="{ color: textColor }"
@click="onClick"
>
<fluent-icon icon="dismiss" size="12" class="close--icon" />
</button>
</div>
</template>
<script>
import { getContrastingTextColor } from '@chatwoot/utils';
@@ -109,6 +80,36 @@ export default {
};
</script>
<template>
<div
class="inline-flex ltr:mr-1 rtl:ml-1 mb-1"
:class="labelClass"
:style="labelStyle"
:title="description"
>
<span v-if="icon" class="label-action--button">
<fluent-icon :icon="icon" size="12" class="label--icon cursor-pointer" />
</span>
<span
v-if="['smooth', 'dashed'].includes(variant) && title && !icon"
:style="{ background: color }"
class="label-color-dot flex-shrink-0"
/>
<span v-if="!href" class="whitespace-nowrap text-ellipsis overflow-hidden">
{{ title }}
</span>
<a v-else :href="href" :style="anchorStyle">{{ title }}</a>
<button
v-if="showClose"
class="label-close--button p-0"
:style="{ color: textColor }"
@click="onClick"
>
<fluent-icon icon="dismiss" size="12" class="close--icon" />
</button>
</div>
</template>
<style scoped lang="scss">
.label {
@apply items-center font-medium text-xs rounded-[4px] gap-1 p-1 bg-slate-50 dark:bg-slate-700 text-slate-800 dark:text-slate-100 border border-solid border-slate-75 dark:border-slate-600 h-6;

View File

@@ -1,3 +1,26 @@
<script>
export default {
props: {
heading: {
type: String,
default: '',
},
content: {
type: String,
default: '',
},
active: {
type: Boolean,
default: false,
},
src: {
type: String,
default: '',
},
},
};
</script>
<template>
<div
class="flex flex-col min-w-[15rem] max-h-[21.25rem] max-w-[23.75rem] rounded-md border border-solid border-slate-75 dark:border-slate-600"
@@ -13,7 +36,7 @@
active,
}"
>
<div class="items-center flex font-medium p-1 text-sm">{{ heading }}</div>
<div class="flex items-center p-1 text-sm font-medium">{{ heading }}</div>
<fluent-icon
v-if="active"
icon="checkmark-circle"
@@ -41,30 +64,3 @@
<slot v-else />
</div>
</template>
<script>
export default {
props: {
heading: {
type: String,
default: '',
},
content: {
type: String,
default: '',
},
active: {
type: Boolean,
default: false,
},
buttonText: {
type: String,
default: 'Active',
},
src: {
type: String,
default: '',
},
},
};
</script>

View File

@@ -1,16 +1,3 @@
<template>
<button
type="button"
class="toggle-button p-0"
:class="{ active: value, small: size === 'small' }"
role="switch"
:aria-checked="value.toString()"
@click="onClick"
>
<span aria-hidden="true" :class="{ active: value }" />
</button>
</template>
<script>
export default {
props: {
@@ -24,6 +11,20 @@ export default {
},
};
</script>
<template>
<button
type="button"
class="toggle-button p-0"
:class="{ active: value, small: size === 'small' }"
role="switch"
:aria-checked="value.toString()"
@click="onClick"
>
<span aria-hidden="true" :class="{ active: value }" />
</button>
</template>
<style lang="scss" scoped>
.toggle-button {
@apply bg-slate-200 dark:bg-slate-600;

View File

@@ -1,20 +1,3 @@
<template>
<li
:class="{
'tabs-title': true,
'is-active': active,
}"
>
<a @click="onTabClick">
{{ name }}
<div v-if="showBadge" class="badge min-w-[20px]">
<span>
{{ getItemCount }}
</span>
</div>
</a>
</li>
</template>
<script>
export default {
name: 'WootTabsItem',
@@ -61,3 +44,21 @@ export default {
},
};
</script>
<template>
<li
class="tabs-title"
:class="{
'is-active': active,
}"
>
<a @click="onTabClick">
{{ name }}
<div v-if="showBadge" class="badge min-w-[20px]">
<span>
{{ getItemCount }}
</span>
</div>
</a>
</li>
</template>

View File

@@ -1,16 +1,3 @@
<template>
<div
v-tooltip.top="{
content: tooltipText,
delay: { show: 1500, hide: 0 },
hideOnClick: true,
}"
class="ml-auto leading-4 text-xxs text-slate-500 dark:text-slate-500 hover:text-slate-900 dark:hover:text-slate-100"
>
<span>{{ `${createdAtTime}${lastActivityTime}` }}</span>
</div>
</template>
<script>
const MINUTE_IN_MILLI_SECONDS = 60000;
const HOUR_IN_MILLI_SECONDS = MINUTE_IN_MILLI_SECONDS * 60;
@@ -118,3 +105,16 @@ export default {
},
};
</script>
<template>
<div
v-tooltip.top="{
content: tooltipText,
delay: { show: 1500, hide: 0 },
hideOnClick: true,
}"
class="ml-auto leading-4 text-xxs text-slate-500 dark:text-slate-500 hover:text-slate-900 dark:hover:text-slate-100"
>
<span>{{ `${createdAtTime}${lastActivityTime}` }}</span>
</div>
</template>

View File

@@ -1,39 +1,3 @@
<template>
<transition-group
name="wizard-items"
tag="div"
class="wizard-box"
:class="classObject"
>
<div
v-for="item in items"
:key="item.route"
class="item"
:class="{ active: isActive(item), over: isOver(item) }"
>
<div class="flex items-center">
<h3
class="text-slate-800 dark:text-slate-100 text-base font-medium pl-6 overflow-hidden whitespace-nowrap mb-1.5 text-ellipsis leading-tight"
>
{{ item.title }}
</h3>
<span
v-if="isOver(item)"
class="text-green-500 dark:text-green-500 ml-1"
>
<fluent-icon icon="checkmark" />
</span>
</div>
<span class="step">
{{ items.indexOf(item) + 1 }}
</span>
<p class="text-slate-600 dark:text-slate-300 text-sm m-0 pl-6">
{{ item.body }}
</p>
</div>
</transition-group>
</template>
<script>
/* eslint no-console: 0 */
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
@@ -41,7 +5,6 @@ import globalConfigMixin from 'shared/mixins/globalConfigMixin';
export default {
mixins: [globalConfigMixin],
props: {
isFullwidth: Boolean,
items: {
type: Array,
default: () => [],
@@ -65,6 +28,43 @@ export default {
},
};
</script>
<template>
<transition-group
name="wizard-items"
tag="div"
class="wizard-box"
:class="classObject"
>
<div
v-for="item in items"
:key="item.route"
class="item"
:class="{ active: isActive(item), over: isOver(item) }"
>
<div class="flex items-center">
<h3
class="text-slate-800 dark:text-slate-100 text-base font-medium pl-6 overflow-hidden whitespace-nowrap mb-1.5 text-ellipsis leading-tight"
>
{{ item.title }}
</h3>
<span
v-if="isOver(item)"
class="ml-1 text-green-500 dark:text-green-500"
>
<fluent-icon icon="checkmark" />
</span>
</div>
<span class="step">
{{ items.indexOf(item) + 1 }}
</span>
<p class="pl-6 m-0 text-sm text-slate-600 dark:text-slate-300">
{{ item.body }}
</p>
</div>
</transition-group>
</template>
<style lang="scss" scoped>
.wizard-box {
.item {

View File

@@ -1,32 +1,3 @@
<template>
<button
class="button"
:type="type"
:class="buttonClasses"
:disabled="isDisabled || isLoading"
@click="handleClick"
>
<spinner
v-if="isLoading"
size="small"
:color-scheme="showDarkSpinner ? 'dark' : ''"
/>
<emoji-or-icon
v-else-if="icon || emoji"
class="icon"
:emoji="emoji"
:icon="icon"
:icon-size="iconSize"
/>
<span
v-if="$slots.default"
class="button__content"
:class="{ 'text-left rtl:text-right': size !== 'expanded' }"
>
<slot />
</span>
</button>
</template>
<script>
import Spinner from 'shared/components/Spinner.vue';
import EmojiOrIcon from 'shared/components/EmojiOrIcon.vue';
@@ -132,3 +103,33 @@ export default {
},
};
</script>
<template>
<button
class="button"
:type="type"
:class="buttonClasses"
:disabled="isDisabled || isLoading"
@click="handleClick"
>
<Spinner
v-if="isLoading"
size="small"
:color-scheme="showDarkSpinner ? 'dark' : ''"
/>
<EmojiOrIcon
v-else-if="icon || emoji"
class="icon"
:emoji="emoji"
:icon="icon"
:icon-size="iconSize"
/>
<span
v-if="$slots.default"
class="button__content"
:class="{ 'text-left rtl:text-right': size !== 'expanded' }"
>
<slot />
</span>
</button>
</template>

View File

@@ -1,38 +1,3 @@
<template>
<div v-if="!isFetchingAppIntegrations">
<div v-if="isAIIntegrationEnabled" class="relative">
<AIAssistanceCTAButton
v-if="shouldShowAIAssistCTAButton"
@click="openAIAssist"
/>
<woot-button
v-else
v-tooltip.top-end="$t('INTEGRATION_SETTINGS.OPEN_AI.AI_ASSIST')"
icon="wand"
color-scheme="secondary"
variant="smooth"
size="small"
@click="openAIAssist"
/>
<woot-modal
:show.sync="showAIAssistanceModal"
:on-close="hideAIAssistanceModal"
>
<AIAssistanceModal
:ai-option="aiOption"
@apply-text="insertText"
@close="hideAIAssistanceModal"
/>
</woot-modal>
</div>
<div v-else-if="shouldShowAIAssistCTAButtonForAdmin" class="relative">
<AIAssistanceCTAButton @click="openAICta" />
<woot-modal :show.sync="showAICtaModal" :on-close="hideAICtaModal">
<AICTAModal @close="hideAICtaModal" />
</woot-modal>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import { useAdmin } from 'dashboard/composables/useAdmin';
@@ -69,7 +34,6 @@ export default {
}),
computed: {
...mapGetters({
currentChat: 'getSelectedChat',
isAChatwootInstance: 'globalConfig/isAChatwootInstance',
}),
isAICTAModalDismissed() {
@@ -101,7 +65,7 @@ export default {
'$mod+KeyZ': {
action: () => {
if (this.initialMessage) {
this.$emit('replace-text', this.initialMessage);
this.$emit('replaceText', this.initialMessage);
this.initialMessage = '';
}
},
@@ -136,8 +100,44 @@ export default {
this.showAIAssistanceModal = true;
},
insertText(message) {
this.$emit('replace-text', message);
this.$emit('replaceText', message);
},
},
};
</script>
<template>
<div v-if="!isFetchingAppIntegrations">
<div v-if="isAIIntegrationEnabled" class="relative">
<AIAssistanceCTAButton
v-if="shouldShowAIAssistCTAButton"
@click="openAIAssist"
/>
<woot-button
v-else
v-tooltip.top-end="$t('INTEGRATION_SETTINGS.OPEN_AI.AI_ASSIST')"
icon="wand"
color-scheme="secondary"
variant="smooth"
size="small"
@click="openAIAssist"
/>
<woot-modal
:show.sync="showAIAssistanceModal"
:on-close="hideAIAssistanceModal"
>
<AIAssistanceModal
:ai-option="aiOption"
@applyText="insertText"
@close="hideAIAssistanceModal"
/>
</woot-modal>
</div>
<div v-else-if="shouldShowAIAssistCTAButtonForAdmin" class="relative">
<AIAssistanceCTAButton @click="openAICta" />
<woot-modal :show.sync="showAICtaModal" :on-close="hideAICtaModal">
<AICTAModal @close="hideAICtaModal" />
</woot-modal>
</div>
</div>
</template>

View File

@@ -1,3 +1,13 @@
<script>
export default {
methods: {
onClick() {
this.$emit('click');
},
},
};
</script>
<template>
<div class="relative">
<woot-button
@@ -19,15 +29,7 @@
/>
</div>
</template>
<script>
export default {
methods: {
onClick() {
this.$emit('click');
},
},
};
</script>
<style scoped>
@tailwind components;
@layer components {

View File

@@ -1,44 +1,4 @@
<template>
<div class="flex flex-col">
<woot-modal-header :header-title="headerTitle" />
<form
class="modal-content flex flex-col w-full"
@submit.prevent="applyText"
>
<div v-if="draftMessage" class="w-full">
<h4 class="text-base mt-1 text-slate-700 dark:text-slate-100">
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.ASSISTANCE_MODAL.DRAFT_TITLE') }}
</h4>
<p v-dompurify-html="formatMessage(draftMessage, false)" />
<h4 class="text-base mt-1 text-slate-700 dark:text-slate-100">
{{
$t('INTEGRATION_SETTINGS.OPEN_AI.ASSISTANCE_MODAL.GENERATED_TITLE')
}}
</h4>
</div>
<div>
<AILoader v-if="isGenerating" />
<p v-else v-dompurify-html="formatMessage(generatedContent, false)" />
</div>
<div class="flex flex-row justify-end gap-2 py-2 px-0 w-full">
<woot-button variant="clear" @click.prevent="onClose">
{{
$t('INTEGRATION_SETTINGS.OPEN_AI.ASSISTANCE_MODAL.BUTTONS.CANCEL')
}}
</woot-button>
<woot-button :disabled="!generatedContent">
{{
$t('INTEGRATION_SETTINGS.OPEN_AI.ASSISTANCE_MODAL.BUTTONS.APPLY')
}}
</woot-button>
</div>
</form>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
import AILoader from './AILoader.vue';
import aiMixin from 'dashboard/mixins/aiMixin';
@@ -62,9 +22,6 @@ export default {
};
},
computed: {
...mapGetters({
appIntegrations: 'integrations/getAppIntegrations',
}),
headerTitle() {
const translationKey = this.aiOption?.toUpperCase();
return translationKey
@@ -92,13 +49,52 @@ export default {
},
applyText() {
this.recordAnalytics(this.aiOption);
this.$emit('apply-text', this.generatedContent);
this.$emit('applyText', this.generatedContent);
this.onClose();
},
},
};
</script>
<template>
<div class="flex flex-col">
<woot-modal-header :header-title="headerTitle" />
<form
class="flex flex-col w-full modal-content"
@submit.prevent="applyText"
>
<div v-if="draftMessage" class="w-full">
<h4 class="mt-1 text-base text-slate-700 dark:text-slate-100">
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.ASSISTANCE_MODAL.DRAFT_TITLE') }}
</h4>
<p v-dompurify-html="formatMessage(draftMessage, false)" />
<h4 class="mt-1 text-base text-slate-700 dark:text-slate-100">
{{
$t('INTEGRATION_SETTINGS.OPEN_AI.ASSISTANCE_MODAL.GENERATED_TITLE')
}}
</h4>
</div>
<div>
<AILoader v-if="isGenerating" />
<p v-else v-dompurify-html="formatMessage(generatedContent, false)" />
</div>
<div class="flex flex-row justify-end w-full gap-2 px-0 py-2">
<woot-button variant="clear" @click.prevent="onClose">
{{
$t('INTEGRATION_SETTINGS.OPEN_AI.ASSISTANCE_MODAL.BUTTONS.CANCEL')
}}
</woot-button>
<woot-button :disabled="!generatedContent">
{{
$t('INTEGRATION_SETTINGS.OPEN_AI.ASSISTANCE_MODAL.BUTTONS.APPLY')
}}
</woot-button>
</div>
</form>
</div>
</template>
<style lang="scss" scoped>
.modal-content {
@apply pt-2 px-8 pb-8;

View File

@@ -1,45 +1,6 @@
<template>
<div class="flex-1 min-w-0 px-0">
<woot-modal-header
:header-title="$t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.TITLE')"
:header-content="$t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.DESC')"
/>
<form
class="flex flex-col flex-wrap modal-content"
@submit.prevent="finishOpenAI"
>
<div class="w-full mt-2">
<woot-input
v-model="value"
type="text"
:class="{ error: v$.value.$error }"
:placeholder="
$t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.KEY_PLACEHOLDER')
"
@blur="v$.value.$touch"
/>
</div>
<div class="flex flex-row justify-between w-full gap-2 px-0 py-2">
<woot-button variant="link" @click.prevent="openOpenAIDoc">
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.BUTTONS.NEED_HELP') }}
</woot-button>
<div class="flex items-center gap-1">
<woot-button variant="clear" @click.prevent="onDismiss">
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.BUTTONS.DISMISS') }}
</woot-button>
<woot-button :is-disabled="v$.value.$invalid">
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.BUTTONS.FINISH') }}
</woot-button>
</div>
</div>
</form>
</div>
</template>
<script>
import { useVuelidate } from '@vuelidate/core';
import { required } from '@vuelidate/validators';
import { mapGetters } from 'vuex';
import { useAlert } from 'dashboard/composables';
import { useUISettings } from 'dashboard/composables/useUISettings';
import aiMixin from 'dashboard/mixins/aiMixin';
@@ -63,11 +24,6 @@ export default {
required,
},
},
computed: {
...mapGetters({
appIntegrations: 'integrations/getAppIntegrations',
}),
},
methods: {
onClose() {
this.$emit('close');
@@ -113,3 +69,41 @@ export default {
},
};
</script>
<template>
<div class="flex-1 min-w-0 px-0">
<woot-modal-header
:header-title="$t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.TITLE')"
:header-content="$t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.DESC')"
/>
<form
class="flex flex-col flex-wrap modal-content"
@submit.prevent="finishOpenAI"
>
<div class="w-full mt-2">
<woot-input
v-model="value"
type="text"
:class="{ error: v$.value.$error }"
:placeholder="
$t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.KEY_PLACEHOLDER')
"
@blur="v$.value.$touch"
/>
</div>
<div class="flex flex-row justify-between w-full gap-2 px-0 py-2">
<woot-button variant="link" @click.prevent="openOpenAIDoc">
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.BUTTONS.NEED_HELP') }}
</woot-button>
<div class="flex items-center gap-1">
<woot-button variant="clear" @click.prevent="onDismiss">
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.BUTTONS.DISMISS') }}
</woot-button>
<woot-button :is-disabled="v$.value.$invalid">
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.BUTTONS.FINISH') }}
</woot-button>
</div>
</div>
</form>
</div>
</template>

View File

@@ -12,8 +12,6 @@
</div>
</template>
<script></script>
<style lang="scss" scoped>
.animation-container {
position: relative;

View File

@@ -1,3 +1,48 @@
<script setup>
import { computed } from 'vue';
import { formatBytes } from 'shared/helpers/FileHelper';
const props = defineProps({
attachments: {
type: Array,
default: () => [],
},
});
const emit = defineEmits(['removeAttachment']);
const nonRecordedAudioAttachments = computed(() => {
return props.attachments.filter(attachment => !attachment?.isRecordedAudio);
});
const recordedAudioAttachments = computed(() =>
props.attachments.filter(attachment => attachment.isRecordedAudio)
);
const onRemoveAttachment = itemIndex => {
emit(
'removeAttachment',
nonRecordedAudioAttachments.value
.filter((_, index) => index !== itemIndex)
.concat(recordedAudioAttachments.value)
);
};
const formatFileSize = file => {
const size = file.byte_size || file.size;
return formatBytes(size, 0);
};
const isTypeImage = file => {
const type = file.content_type || file.type;
return type.includes('image');
};
const fileName = file => {
return file.filename || file.name;
};
</script>
<template>
<div class="flex overflow-auto max-h-[12.5rem]">
<div
@@ -37,48 +82,3 @@
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { formatBytes } from 'shared/helpers/FileHelper';
const props = defineProps({
attachments: {
type: Array,
default: () => [],
},
});
const emits = defineEmits(['remove-attachment']);
const nonRecordedAudioAttachments = computed(() => {
return props.attachments.filter(attachment => !attachment?.isRecordedAudio);
});
const recordedAudioAttachments = computed(() =>
props.attachments.filter(attachment => attachment.isRecordedAudio)
);
const onRemoveAttachment = itemIndex => {
emits(
'remove-attachment',
nonRecordedAudioAttachments.value
.filter((_, index) => index !== itemIndex)
.concat(recordedAudioAttachments.value)
);
};
const formatFileSize = file => {
const size = file.byte_size || file.size;
return formatBytes(size, 0);
};
const isTypeImage = file => {
const type = file.content_type || file.type;
return type.includes('image');
};
const fileName = file => {
return file.filename || file.name;
};
</script>

View File

@@ -1,107 +1,3 @@
<template>
<div class="filter" :class="actionInputStyles">
<div class="filter-inputs">
<select
v-model="action_name"
class="action__question"
:class="{ 'full-width': !showActionInput }"
@change="resetAction()"
>
<option
v-for="attribute in actionTypes"
:key="attribute.key"
:value="attribute.key"
>
{{ attribute.label }}
</option>
</select>
<div v-if="showActionInput" class="filter__answer--wrap">
<div v-if="inputType" class="w-full">
<div
v-if="inputType === 'search_select'"
class="multiselect-wrap--small"
>
<multiselect
v-model="action_params"
track-by="id"
label="name"
:placeholder="$t('FORMS.MULTISELECT.SELECT')"
selected-label
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
deselect-label=""
:max-height="160"
:options="dropdownValues"
:allow-empty="false"
:option-height="104"
/>
</div>
<div
v-else-if="inputType === 'multi_select'"
class="multiselect-wrap--small"
>
<multiselect
v-model="action_params"
track-by="id"
label="name"
:placeholder="$t('FORMS.MULTISELECT.SELECT')"
:multiple="true"
selected-label
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
deselect-label=""
:max-height="160"
:options="dropdownValues"
:allow-empty="false"
:option-height="104"
/>
</div>
<input
v-else-if="inputType === 'email'"
v-model="action_params"
type="email"
class="answer--text-input"
placeholder="Enter email"
/>
<input
v-else-if="inputType === 'url'"
v-model="action_params"
type="url"
class="answer--text-input"
placeholder="Enter url"
/>
<automation-action-file-input
v-if="inputType === 'attachment'"
v-model="action_params"
:initial-file-name="initialFileName"
/>
</div>
</div>
<woot-button
v-if="!isMacro"
icon="dismiss"
variant="clear"
color-scheme="secondary"
@click="removeAction"
/>
</div>
<automation-action-team-message-input
v-if="inputType === 'team_message'"
v-model="action_params"
:teams="dropdownValues"
/>
<woot-message-editor
v-if="inputType === 'textarea'"
v-model="castMessageVmodel"
rows="4"
:enable-variables="true"
:placeholder="$t('AUTOMATION.ACTION.TEAM_MESSAGE_INPUT_PLACEHOLDER')"
class="action-message"
/>
<p v-if="errorMessage" class="filter-error">
{{ errorMessage }}
</p>
</div>
</template>
<script>
import AutomationActionTeamMessageInput from './AutomationActionTeamMessageInput.vue';
import AutomationActionFileInput from './AutomationFileInput.vue';
@@ -196,6 +92,110 @@ export default {
};
</script>
<template>
<div class="filter" :class="actionInputStyles">
<div class="filter-inputs">
<select
v-model="action_name"
class="action__question"
:class="{ 'full-width': !showActionInput }"
@change="resetAction()"
>
<option
v-for="attribute in actionTypes"
:key="attribute.key"
:value="attribute.key"
>
{{ attribute.label }}
</option>
</select>
<div v-if="showActionInput" class="filter__answer--wrap">
<div v-if="inputType" class="w-full">
<div
v-if="inputType === 'search_select'"
class="multiselect-wrap--small"
>
<multiselect
v-model="action_params"
track-by="id"
label="name"
:placeholder="$t('FORMS.MULTISELECT.SELECT')"
selected-label
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
deselect-label=""
:max-height="160"
:options="dropdownValues"
:allow-empty="false"
:option-height="104"
/>
</div>
<div
v-else-if="inputType === 'multi_select'"
class="multiselect-wrap--small"
>
<multiselect
v-model="action_params"
track-by="id"
label="name"
:placeholder="$t('FORMS.MULTISELECT.SELECT')"
multiple
selected-label
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
deselect-label=""
:max-height="160"
:options="dropdownValues"
:allow-empty="false"
:option-height="104"
/>
</div>
<input
v-else-if="inputType === 'email'"
v-model="action_params"
type="email"
class="answer--text-input"
:placeholder="$t('AUTOMATION.ACTION.EMAIL_INPUT_PLACEHOLDER')"
/>
<input
v-else-if="inputType === 'url'"
v-model="action_params"
type="url"
class="answer--text-input"
:placeholder="$t('AUTOMATION.ACTION.URL_INPUT_PLACEHOLDER')"
/>
<AutomationActionFileInput
v-if="inputType === 'attachment'"
v-model="action_params"
:initial-file-name="initialFileName"
/>
</div>
</div>
<woot-button
v-if="!isMacro"
icon="dismiss"
variant="clear"
color-scheme="secondary"
@click="removeAction"
/>
</div>
<AutomationActionTeamMessageInput
v-if="inputType === 'team_message'"
v-model="action_params"
:teams="dropdownValues"
/>
<WootMessageEditor
v-if="inputType === 'textarea'"
v-model="castMessageVmodel"
rows="4"
enable-variables
:placeholder="$t('AUTOMATION.ACTION.TEAM_MESSAGE_INPUT_PLACEHOLDER')"
class="action-message"
/>
<p v-if="errorMessage" class="filter-error">
{{ errorMessage }}
</p>
</div>
</template>
<style lang="scss" scoped>
.filter {
@apply bg-slate-50 dark:bg-slate-800 p-2 border border-solid border-slate-75 dark:border-slate-600 rounded-md mb-2;

View File

@@ -1,30 +1,3 @@
<template>
<div>
<div class="multiselect-wrap--small">
<multiselect
v-model="selectedTeams"
track-by="id"
label="name"
:placeholder="$t('AUTOMATION.ACTION.TEAM_DROPDOWN_PLACEHOLDER')"
:multiple="true"
selected-label
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
deselect-label=""
:max-height="160"
:options="teams"
:allow-empty="false"
@input="updateValue"
/>
<textarea
v-model="message"
rows="4"
:placeholder="$t('AUTOMATION.ACTION.TEAM_MESSAGE_INPUT_PLACEHOLDER')"
@input="updateValue"
/>
</div>
</div>
</template>
<script>
export default {
// The value types are dynamic, hence prop validation removed to work with our action schema
@@ -52,6 +25,33 @@ export default {
};
</script>
<template>
<div>
<div class="multiselect-wrap--small">
<multiselect
v-model="selectedTeams"
track-by="id"
label="name"
:placeholder="$t('AUTOMATION.ACTION.TEAM_DROPDOWN_PLACEHOLDER')"
multiple
selected-label
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
deselect-label=""
:max-height="160"
:options="teams"
:allow-empty="false"
@input="updateValue"
/>
<textarea
v-model="message"
rows="4"
:placeholder="$t('AUTOMATION.ACTION.TEAM_MESSAGE_INPUT_PLACEHOLDER')"
@input="updateValue"
/>
</div>
</div>
</template>
<style scoped>
.multiselect {
margin: var(--space-smaller) var(--space-zero);

View File

@@ -1,30 +1,3 @@
<template>
<label class="input-wrapper" :class="uploadState">
<input
v-if="uploadState !== 'processing'"
type="file"
name="attachment"
:class="uploadState === 'processing' ? 'disabled' : ''"
@change="onChangeFile"
/>
<spinner v-if="uploadState === 'processing'" />
<fluent-icon v-if="uploadState === 'idle'" icon="file-upload" />
<fluent-icon
v-if="uploadState === 'uploaded'"
icon="checkmark-circle"
type="outline"
class="success-icon"
/>
<fluent-icon
v-if="uploadState === 'failed'"
icon="dismiss-circle"
type="outline"
class="error-icon"
/>
<p class="file-button">{{ label }}</p>
</label>
</template>
<script>
import { useAlert } from 'dashboard/composables';
import Spinner from 'shared/components/Spinner.vue';
@@ -33,10 +6,6 @@ export default {
Spinner,
},
props: {
value: {
type: Array,
default: () => [],
},
initialFileName: {
type: String,
default: '',
@@ -77,6 +46,33 @@ export default {
};
</script>
<template>
<label class="input-wrapper" :class="uploadState">
<input
v-if="uploadState !== 'processing'"
type="file"
name="attachment"
:class="uploadState === 'processing' ? 'disabled' : ''"
@change="onChangeFile"
/>
<Spinner v-if="uploadState === 'processing'" />
<fluent-icon v-if="uploadState === 'idle'" icon="file-upload" />
<fluent-icon
v-if="uploadState === 'uploaded'"
icon="checkmark-circle"
type="outline"
class="success-icon"
/>
<fluent-icon
v-if="uploadState === 'failed'"
icon="dismiss-circle"
type="outline"
class="error-icon"
/>
<p class="file-button">{{ label }}</p>
</label>
</template>
<style scoped>
input[type='file'] {
@apply hidden;

View File

@@ -1,9 +1,3 @@
<template>
<div class="avatar-container" :style="style" aria-hidden="true">
<slot>{{ userInitial }}</slot>
</div>
</template>
<script>
export default {
name: 'Avatar',
@@ -38,6 +32,12 @@ export default {
};
</script>
<template>
<div class="avatar-container" :style="style" aria-hidden="true">
<slot>{{ userInitial }}</slot>
</div>
</template>
<style scoped>
@tailwind components;
@layer components {

View File

@@ -30,7 +30,7 @@ const buttonStyleClass = props.compact
<template>
<button
class="flex items-center font-normal p-0 cursor-pointer"
class="flex items-center p-0 font-normal cursor-pointer"
:class="buttonStyleClass"
@click.capture="goBack"
>

View File

@@ -1,11 +1,3 @@
<template>
<channel-selector
:class="{ inactive: !isActive }"
:title="channel.name"
:src="getChannelThumbnail()"
@click="onItemClick"
/>
</template>
<script>
import ChannelSelector from '../ChannelSelector.vue';
export default {
@@ -59,9 +51,18 @@ export default {
},
onItemClick() {
if (this.isActive) {
this.$emit('channel-item-click', this.channel.key);
this.$emit('channelItemClick', this.channel.key);
}
},
},
};
</script>
<template>
<ChannelSelector
:class="{ inactive: !isActive }"
:title="channel.name"
:src="getChannelThumbnail()"
@click="onItemClick"
/>
</template>

View File

@@ -1,13 +1,3 @@
<template>
<woot-tabs :index="activeTabIndex" @change="onTabChange">
<woot-tabs-item
v-for="item in items"
:key="item.key"
:name="item.name"
:count="item.count"
/>
</woot-tabs>
</template>
<script>
import wootConstants from 'dashboard/constants/globals';
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
@@ -51,3 +41,14 @@ export default {
},
};
</script>
<template>
<woot-tabs :index="activeTabIndex" @change="onTabChange">
<woot-tabs-item
v-for="item in items"
:key="item.key"
:name="item.name"
:count="item.count"
/>
</woot-tabs>
</template>

View File

@@ -1,21 +1,3 @@
<template>
<div class="colorpicker">
<div
class="colorpicker--selected"
:style="`background-color: ${value}`"
@click.prevent="toggleColorPicker"
/>
<chrome
v-if="isPickerOpen"
v-on-clickaway="closeTogglePicker"
:disable-alpha="true"
:value="value"
class="colorpicker--chrome"
@input="updateColor"
/>
</div>
</template>
<script>
import { Chrome } from 'vue-color';
@@ -50,6 +32,24 @@ export default {
};
</script>
<template>
<div class="colorpicker">
<div
class="colorpicker--selected"
:style="`background-color: ${value}`"
@click.prevent="toggleColorPicker"
/>
<Chrome
v-if="isPickerOpen"
v-on-clickaway="closeTogglePicker"
disable-alpha
:value="value"
class="colorpicker--chrome"
@input="updateColor"
/>
</div>
</template>
<style scoped lang="scss">
@import '~dashboard/assets/scss/variables';
@import '~dashboard/assets/scss/mixins';

View File

@@ -1,25 +1,3 @@
<template>
<div v-if="hasOpenedAtleastOnce" class="dashboard-app--container">
<div
v-for="(configItem, index) in config"
:key="index"
class="dashboard-app--list"
>
<loading-state
v-if="iframeLoading"
:message="$t('DASHBOARD_APPS.LOADING_MESSAGE')"
class="dashboard-app_loading-container"
/>
<iframe
v-if="configItem.type === 'frame' && configItem.url"
:id="getFrameId(index)"
:src="configItem.url"
@load="() => onIframeLoad(index)"
/>
</div>
</div>
</template>
<script>
import LoadingState from 'dashboard/components/widgets/LoadingState.vue';
export default {
@@ -102,6 +80,28 @@ export default {
};
</script>
<template>
<div v-if="hasOpenedAtleastOnce" class="dashboard-app--container">
<div
v-for="(configItem, index) in config"
:key="index"
class="dashboard-app--list"
>
<LoadingState
v-if="iframeLoading"
:message="$t('DASHBOARD_APPS.LOADING_MESSAGE')"
class="dashboard-app_loading-container"
/>
<iframe
v-if="configItem.type === 'frame' && configItem.url"
:id="getFrameId(index)"
:src="configItem.url"
@load="() => onIframeLoad(index)"
/>
</div>
</div>
</template>
<style scoped>
.dashboard-app--container,
.dashboard-app--list,

View File

@@ -1,3 +1,12 @@
<script>
export default {
props: {
title: { type: String, default: '' },
message: { type: String, default: '' },
},
};
</script>
<template>
<div class="empty-state py-16 px-1 ml-0 mr-0">
<h3
@@ -15,12 +24,3 @@
<slot />
</div>
</template>
<script>
export default {
props: {
title: { type: String, default: '' },
message: { type: String, default: '' },
},
};
</script>

View File

@@ -1,8 +1,3 @@
<template>
<div v-if="isFeatureEnabled">
<slot />
</div>
</template>
<script>
import { mapGetters } from 'vuex';
export default {
@@ -23,3 +18,9 @@ export default {
},
};
</script>
<template>
<div v-if="isFeatureEnabled">
<slot />
</div>
</template>

View File

@@ -1,147 +1,3 @@
<!-- eslint-disable vue/no-mutating-props -->
<template>
<div>
<div
class="rounded-md p-2 border border-solid"
:class="getInputErrorClass(errorMessage)"
>
<div class="flex">
<select
v-if="groupedFilters"
v-model="attributeKey"
class="bg-white max-w-[30%] dark:bg-slate-900 mb-0 mr-1 text-slate-800 dark:text-slate-100 border-slate-75 dark:border-slate-600"
@change="resetFilter()"
>
<optgroup
v-for="(group, i) in filterGroups"
:key="i"
:label="group.name"
>
<option
v-for="attribute in group.attributes"
:key="attribute.key"
:value="attribute.key"
>
{{ attribute.name }}
</option>
</optgroup>
</select>
<select
v-else
v-model="attributeKey"
class="bg-white max-w-[30%] dark:bg-slate-900 mb-0 mr-1 text-slate-800 dark:text-slate-100 border-slate-75 dark:border-slate-600"
@change="resetFilter()"
>
<option
v-for="attribute in filterAttributes"
:key="attribute.key"
:value="attribute.key"
:disabled="attribute.disabled"
>
{{ attribute.name }}
</option>
</select>
<select
v-model="filterOperator"
class="bg-white dark:bg-slate-900 max-w-[20%] mb-0 mr-1 text-slate-800 dark:text-slate-100 border-slate-75 dark:border-slate-600"
>
<option
v-for="(operator, o) in operators"
:key="o"
:value="operator.value"
>
{{ $t(`FILTER.OPERATOR_LABELS.${operator.value}`) }}
</option>
</select>
<div v-if="showUserInput" class="filter__answer--wrap mr-1 flex-grow">
<div
v-if="inputType === 'multi_select'"
class="multiselect-wrap--small"
>
<multiselect
v-model="values"
track-by="id"
label="name"
:placeholder="'Select'"
:multiple="true"
selected-label
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
deselect-label=""
:max-height="160"
:options="dropdownValues"
:allow-empty="false"
/>
</div>
<div
v-else-if="inputType === 'search_select'"
class="multiselect-wrap--small"
>
<multiselect
v-model="values"
track-by="id"
label="name"
:placeholder="'Select'"
selected-label
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
deselect-label=""
:max-height="160"
:options="dropdownValues"
:allow-empty="false"
:option-height="104"
/>
</div>
<div v-else-if="inputType === 'date'" class="multiselect-wrap--small">
<input
v-model="values"
type="date"
:editable="false"
class="mb-0 datepicker"
/>
</div>
<input
v-else
v-model="values"
type="text"
class="mb-0"
placeholder="Enter value"
/>
</div>
<woot-button
icon="dismiss"
variant="clear"
color-scheme="secondary"
@click="removeFilter"
/>
</div>
<p v-if="errorMessage" class="filter-error">
{{ errorMessage }}
</p>
</div>
<div
v-if="showQueryOperator"
class="flex items-center justify-center relative my-2.5 mx-0"
>
<hr
class="w-full absolute border-b border-solid border-slate-75 dark:border-slate-800"
/>
<select
v-model="query_operator"
class="bg-white dark:bg-slate-900 mb-0 w-auto relative text-slate-800 dark:text-slate-100 border-slate-75 dark:border-slate-600"
>
<option value="and">
{{ $t('FILTER.QUERY_DROPDOWN_LABELS.AND') }}
</option>
<option value="or">
{{ $t('FILTER.QUERY_DROPDOWN_LABELS.OR') }}
</option>
</select>
</div>
</div>
</template>
<script>
export default {
props: {
@@ -157,10 +13,6 @@ export default {
type: String,
default: 'plain_text',
},
dataType: {
type: String,
default: 'plain_text',
},
operators: {
type: Array,
default: () => [],
@@ -279,6 +131,151 @@ export default {
},
};
</script>
<!-- eslint-disable vue/no-mutating-props -->
<template>
<div>
<div
class="p-2 border border-solid rounded-md"
:class="getInputErrorClass(errorMessage)"
>
<div class="flex">
<select
v-if="groupedFilters"
v-model="attributeKey"
class="bg-white max-w-[30%] dark:bg-slate-900 mb-0 mr-1 text-slate-800 dark:text-slate-100 border-slate-75 dark:border-slate-600"
@change="resetFilter()"
>
<optgroup
v-for="(group, i) in filterGroups"
:key="i"
:label="group.name"
>
<option
v-for="attribute in group.attributes"
:key="attribute.key"
:value="attribute.key"
>
{{ attribute.name }}
</option>
</optgroup>
</select>
<select
v-else
v-model="attributeKey"
class="bg-white max-w-[30%] dark:bg-slate-900 mb-0 mr-1 text-slate-800 dark:text-slate-100 border-slate-75 dark:border-slate-600"
@change="resetFilter()"
>
<option
v-for="attribute in filterAttributes"
:key="attribute.key"
:value="attribute.key"
:disabled="attribute.disabled"
>
{{ attribute.name }}
</option>
</select>
<select
v-model="filterOperator"
class="bg-white dark:bg-slate-900 max-w-[20%] mb-0 mr-1 text-slate-800 dark:text-slate-100 border-slate-75 dark:border-slate-600"
>
<option
v-for="(operator, o) in operators"
:key="o"
:value="operator.value"
>
{{ $t(`FILTER.OPERATOR_LABELS.${operator.value}`) }}
</option>
</select>
<div v-if="showUserInput" class="flex-grow mr-1 filter__answer--wrap">
<div
v-if="inputType === 'multi_select'"
class="multiselect-wrap--small"
>
<multiselect
v-model="values"
track-by="id"
label="name"
placeholder="Select"
multiple
selected-label
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
deselect-label=""
:max-height="160"
:options="dropdownValues"
:allow-empty="false"
/>
</div>
<div
v-else-if="inputType === 'search_select'"
class="multiselect-wrap--small"
>
<multiselect
v-model="values"
track-by="id"
label="name"
placeholder="Select"
selected-label
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
deselect-label=""
:max-height="160"
:options="dropdownValues"
:allow-empty="false"
:option-height="104"
/>
</div>
<div v-else-if="inputType === 'date'" class="multiselect-wrap--small">
<input
v-model="values"
type="date"
:editable="false"
class="mb-0 datepicker"
/>
</div>
<input
v-else
v-model="values"
type="text"
class="mb-0"
:placeholder="$t('FILTER.INPUT_PLACEHOLDER')"
/>
</div>
<woot-button
icon="dismiss"
variant="clear"
color-scheme="secondary"
@click="removeFilter"
/>
</div>
<p v-if="errorMessage" class="filter-error">
{{ errorMessage }}
</p>
</div>
<div
v-if="showQueryOperator"
class="flex items-center justify-center relative my-2.5 mx-0"
>
<hr
class="absolute w-full border-b border-solid border-slate-75 dark:border-slate-800"
/>
<select
v-model="query_operator"
class="relative w-auto mb-0 bg-white dark:bg-slate-900 text-slate-800 dark:text-slate-100 border-slate-75 dark:border-slate-600"
>
<option value="and">
{{ $t('FILTER.QUERY_DROPDOWN_LABELS.AND') }}
</option>
<option value="or">
{{ $t('FILTER.QUERY_DROPDOWN_LABELS.OR') }}
</option>
</select>
</div>
</div>
</template>
<style lang="scss" scoped>
.filter__answer--wrap {
input {

View File

@@ -1,28 +1,3 @@
<template>
<div class="flex items-center h-[2.375rem] min-w-0 py-0 px-1">
<span
class="inline-flex rounded mr-1 rtl:ml-1 rtl:mr-0 bg-slate-25 dark:bg-slate-700 p-0.5 items-center flex-shrink-0 justify-center w-6 h-6"
>
<fluent-icon
:icon="computedInboxIcon"
size="14"
class="text-slate-800 dark:text-slate-200"
/>
</span>
<div class="flex flex-col w-full min-w-0 ml-1 mr-1">
<h5 class="option__title">
{{ name }}
</h5>
<p
class="option__body overflow-hidden whitespace-nowrap text-ellipsis"
:title="inboxIdentifier"
>
{{ inboxIdentifier || computedInboxType }}
</p>
</div>
</div>
</template>
<script>
import {
getInboxClassByType,
@@ -66,6 +41,31 @@ export default {
};
</script>
<template>
<div class="flex items-center h-[2.375rem] min-w-0 py-0 px-1">
<span
class="inline-flex rounded mr-1 rtl:ml-1 rtl:mr-0 bg-slate-25 dark:bg-slate-700 p-0.5 items-center flex-shrink-0 justify-center w-6 h-6"
>
<fluent-icon
:icon="computedInboxIcon"
size="14"
class="text-slate-800 dark:text-slate-200"
/>
</span>
<div class="flex flex-col w-full min-w-0 ml-1 mr-1">
<h5 class="option__title">
{{ name }}
</h5>
<p
class="option__body overflow-hidden whitespace-nowrap text-ellipsis"
:title="inboxIdentifier"
>
{{ inboxIdentifier || computedInboxType }}
</p>
</div>
</div>
</template>
<style lang="scss" scoped>
.option__body {
@apply inline-block text-slate-600 dark:text-slate-200 leading-[1.3] min-w-0 m-0;

View File

@@ -1,15 +1,3 @@
<template>
<div
class="inbox--name inline-flex items-center py-0.5 px-0 leading-3 whitespace-nowrap font-medium bg-none text-slate-600 dark:text-slate-500 text-xs my-0 mx-2.5"
>
<fluent-icon
class="mr-0.5 rtl:ml-0.5 rtl:mr-0"
:icon="computedInboxClass"
size="12"
/>
{{ inbox.name }}
</div>
</template>
<script>
import { getInboxClassByType } from 'dashboard/helper/inbox';
@@ -29,3 +17,16 @@ export default {
},
};
</script>
<template>
<div
class="inbox--name inline-flex items-center py-0.5 px-0 leading-3 whitespace-nowrap font-medium bg-none text-slate-600 dark:text-slate-500 text-xs my-0 mx-2.5"
>
<fluent-icon
class="mr-0.5 rtl:ml-0.5 rtl:mr-0"
:icon="computedInboxClass"
size="12"
/>
{{ inbox.name }}
</div>
</template>

View File

@@ -1,34 +1,3 @@
<template>
<div v-on-clickaway="closeDropdownLabel" class="label-wrap">
<add-label @add="toggleLabels" />
<woot-label
v-for="label in savedLabels"
:key="label.id"
:title="label.title"
:description="label.description"
:show-close="true"
:color="label.color"
variant="smooth"
@click="removeItem"
/>
<div class="dropdown-wrap">
<div
:class="{ 'dropdown-pane--open': showSearchDropdownLabel }"
class="dropdown-pane"
>
<label-dropdown
v-if="showSearchDropdownLabel"
:account-labels="allLabels"
:selected-labels="selectedLabels"
:allow-creation="isAdmin"
@add="addItem"
@remove="removeItem"
/>
</div>
</div>
</div>
</template>
<script>
import AddLabel from 'shared/components/ui/dropdown/AddLabel.vue';
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
@@ -107,6 +76,37 @@ export default {
};
</script>
<template>
<div v-on-clickaway="closeDropdownLabel" class="label-wrap">
<AddLabel @add="toggleLabels" />
<woot-label
v-for="label in savedLabels"
:key="label.id"
:title="label.title"
:description="label.description"
show-close
:color="label.color"
variant="smooth"
@click="removeItem"
/>
<div class="dropdown-wrap">
<div
:class="{ 'dropdown-pane--open': showSearchDropdownLabel }"
class="dropdown-pane"
>
<LabelDropdown
v-if="showSearchDropdownLabel"
:account-labels="allLabels"
:selected-labels="selectedLabels"
:allow-creation="isAdmin"
@add="addItem"
@remove="removeItem"
/>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.title-icon {
margin-right: var(--space-smaller);

View File

@@ -1,3 +1,11 @@
<script>
export default {
props: {
message: { type: String, default: '' },
},
};
</script>
<template>
<div class="flex items-center justify-center p-8">
<h6
@@ -10,10 +18,3 @@
</h6>
</div>
</template>
<script>
export default {
props: {
message: { type: String, default: '' },
},
};
</script>

View File

@@ -1,3 +1,18 @@
<script>
export default {
props: {
headerTitle: {
type: String,
default: '',
},
headerContent: {
type: String,
default: '',
},
},
};
</script>
<template>
<div
class="bg-slate-25 dark:bg-slate-900 pt-4 pb-0 px-8 border-b border-solid border-slate-50 dark:border-slate-800/50"
@@ -14,18 +29,3 @@
<slot />
</div>
</template>
<script>
export default {
props: {
headerTitle: {
type: String,
default: '',
},
headerContent: {
type: String,
default: '',
},
},
};
</script>

View File

@@ -1,15 +1,3 @@
<template>
<span>
{{ textToBeDisplayed }}
<button
v-if="text.length > limit"
class="show-more--button"
@click="toggleShowMore"
>
{{ buttonLabel }}
</button>
</span>
</template>
<script>
export default {
props: {
@@ -47,6 +35,20 @@ export default {
},
};
</script>
<template>
<span>
{{ textToBeDisplayed }}
<button
v-if="text.length > limit"
class="show-more--button"
@click="toggleShowMore"
>
{{ buttonLabel }}
</button>
</span>
</template>
<style scoped>
.show-more--button {
color: var(--w-500);

View File

@@ -1,24 +1,3 @@
<template>
<footer
v-if="isFooterVisible"
class="h-12 flex items-center justify-between px-6"
>
<table-footer-results
:first-index="firstIndex"
:last-index="lastIndex"
:total-count="totalCount"
/>
<table-footer-pagination
v-if="totalCount"
:current-page="currentPage"
:total-pages="totalPages"
:total-count="totalCount"
:page-size="pageSize"
@page-change="$emit('page-change', $event)"
/>
</footer>
</template>
<script setup>
import { computed } from 'vue';
import TableFooterResults from './TableFooterResults.vue';
@@ -46,3 +25,24 @@ const isFooterVisible = computed(
() => props.totalCount && !(firstIndex.value > props.totalCount)
);
</script>
<template>
<footer
v-if="isFooterVisible"
class="flex items-center justify-between h-12 px-6"
>
<TableFooterResults
:first-index="firstIndex"
:last-index="lastIndex"
:total-count="totalCount"
/>
<TableFooterPagination
v-if="totalCount"
:current-page="currentPage"
:total-pages="totalPages"
:total-count="totalCount"
:page-size="pageSize"
@pageChange="$emit('pageChange', $event)"
/>
</footer>
</template>

View File

@@ -1,5 +1,61 @@
<script setup>
import { computed } from 'vue';
// Props
const props = defineProps({
currentPage: {
type: Number,
default: 1,
},
totalPages: {
type: Number,
default: 0,
},
});
const emit = defineEmits(['pageChange']);
const hasLastPage = computed(
() => props.currentPage === props.totalPages || props.totalPages === 1
);
const hasFirstPage = computed(() => props.currentPage === 1);
const hasNextPage = computed(() => props.currentPage === props.totalPages);
const hasPrevPage = computed(() => props.currentPage === 1);
function buttonClass(hasPage) {
if (hasPage) {
return 'hover:!bg-slate-50 dark:hover:!bg-slate-800';
}
return 'dark:hover:!bg-slate-700/50';
}
function onPageChange(newPage) {
emit('pageChange', newPage);
}
const onNextPage = () => {
if (!onNextPage.value) {
onPageChange(props.currentPage + 1);
}
};
const onPrevPage = () => {
if (!hasPrevPage.value) {
onPageChange(props.currentPage - 1);
}
};
const onFirstPage = () => {
if (!hasFirstPage.value) {
onPageChange(1);
}
};
const onLastPage = () => {
if (!hasLastPage.value) {
onPageChange(props.totalPages);
}
};
</script>
<template>
<div class="flex items-center bg-slate-50 dark:bg-slate-800 h-8 rounded-lg">
<div class="flex items-center h-8 rounded-lg bg-slate-50 dark:bg-slate-800">
<woot-button
size="small"
variant="smooth"
@@ -16,7 +72,7 @@
:class="hasFirstPage && 'opacity-40'"
/>
</woot-button>
<div class="bg-slate-75 dark:bg-slate-700/50 w-px rounded-sm h-4" />
<div class="w-px h-4 rounded-sm bg-slate-75 dark:bg-slate-700/50" />
<woot-button
size="small"
variant="smooth"
@@ -35,7 +91,7 @@
</woot-button>
<div
class="flex px-3 items-center gap-3 tabular-nums bg-slate-50 dark:bg-slate-800 text-slate-700 dark:text-slate-100"
class="flex items-center gap-3 px-3 tabular-nums bg-slate-50 dark:bg-slate-800 text-slate-700 dark:text-slate-100"
>
<span class="text-sm text-slate-800 dark:text-slate-75">
{{ currentPage }}
@@ -61,7 +117,7 @@
:class="hasNextPage && 'opacity-40'"
/>
</woot-button>
<div class="bg-slate-75 dark:bg-slate-700/50 w-px rounded-sm h-4" />
<div class="w-px h-4 rounded-sm bg-slate-75 dark:bg-slate-700/50" />
<woot-button
size="small"
variant="smooth"
@@ -80,64 +136,3 @@
</woot-button>
</div>
</template>
<script setup>
import { computed } from 'vue';
// Props
const props = defineProps({
currentPage: {
type: Number,
default: 1,
},
totalCount: {
type: Number,
default: 0,
},
totalPages: {
type: Number,
default: 0,
},
});
const hasLastPage = computed(
() => props.currentPage === props.totalPages || props.totalPages === 1
);
const hasFirstPage = computed(() => props.currentPage === 1);
const hasNextPage = computed(() => props.currentPage === props.totalPages);
const hasPrevPage = computed(() => props.currentPage === 1);
const emit = defineEmits(['page-change']);
function buttonClass(hasPage) {
if (hasPage) {
return 'hover:!bg-slate-50 dark:hover:!bg-slate-800';
}
return 'dark:hover:!bg-slate-700/50';
}
function onPageChange(newPage) {
emit('page-change', newPage);
}
const onNextPage = () => {
if (!onNextPage.value) {
onPageChange(props.currentPage + 1);
}
};
const onPrevPage = () => {
if (!hasPrevPage.value) {
onPageChange(props.currentPage - 1);
}
};
const onFirstPage = () => {
if (!hasFirstPage.value) {
onPageChange(1);
}
};
const onLastPage = () => {
if (!hasLastPage.value) {
onPageChange(props.totalPages);
}
};
</script>

View File

@@ -1,15 +1,3 @@
<template>
<span class="text-sm text-slate-700 dark:text-slate-200 font-medium">
{{
$t('GENERAL.SHOWING_RESULTS', {
firstIndex,
lastIndex,
totalCount,
})
}}
</span>
</template>
<script setup>
defineProps({
firstIndex: {
@@ -26,3 +14,15 @@ defineProps({
},
});
</script>
<template>
<span class="text-sm text-slate-700 dark:text-slate-200 font-medium">
{{
$t('GENERAL.SHOWING_RESULTS', {
firstIndex,
lastIndex,
totalCount,
})
}}
</span>
</template>

View File

@@ -8,7 +8,6 @@ const props = defineProps({
label: {
type: String,
required: true,
default: '',
},
});
@@ -27,6 +26,7 @@ const spanClass = computed(() => {
return 'col-span-1';
});
</script>
<template>
<div
class="flex items-center px-0 py-2 text-xs font-medium text-left uppercase text-slate-700 dark:text-slate-100 rtl:text-right"

View File

@@ -1,40 +1,3 @@
<template>
<div
:class="thumbnailBoxClass"
:style="{ height: size, width: size }"
:title="title"
>
<!-- Using v-show instead of v-if to avoid flickering as v-if removes dom elements. -->
<slot>
<img
v-show="shouldShowImage"
:src="src"
draggable="false"
:class="thumbnailClass"
@load="onImgLoad"
@error="onImgError"
/>
<Avatar
v-show="!shouldShowImage"
:username="userNameWithoutEmoji"
:class="thumbnailClass"
:size="avatarSize"
/>
</slot>
<img
v-if="badgeSrc"
class="source-badge"
:style="badgeStyle"
:src="`/integrations/channels/badges/${badgeSrc}.png`"
alt="Badge"
/>
<div
v-if="showStatusIndicator"
:class="`source-badge user-online-status user-online-status--${status}`"
:style="statusStyle"
/>
</div>
</template>
<script>
/**
* Thumbnail Component
@@ -168,6 +131,44 @@ export default {
};
</script>
<template>
<div
:class="thumbnailBoxClass"
:style="{ height: size, width: size }"
:title="title"
>
<!-- Using v-show instead of v-if to avoid flickering as v-if removes dom elements. -->
<slot>
<img
v-show="shouldShowImage"
:src="src"
draggable="false"
:class="thumbnailClass"
@load="onImgLoad"
@error="onImgError"
/>
<Avatar
v-show="!shouldShowImage"
:username="userNameWithoutEmoji"
:class="thumbnailClass"
:size="avatarSize"
/>
</slot>
<img
v-if="badgeSrc"
class="source-badge"
:style="badgeStyle"
:src="`/integrations/channels/badges/${badgeSrc}.png`"
alt="Badge"
/>
<div
v-if="showStatusIndicator"
:class="`source-badge user-online-status user-online-status--${status}`"
:style="statusStyle"
/>
</div>
</template>
<style lang="scss" scoped>
.user-thumbnail-box {
flex: 0 0 auto;

View File

@@ -1,21 +1,3 @@
<template>
<div class="overlapping-thumbnails">
<thumbnail
v-for="user in usersList"
:key="user.id"
v-tooltip="user.name"
:title="user.name"
:src="user.thumbnail"
:username="user.name"
:has-border="true"
:size="size"
:class="`overlapping-thumbnail gap-${gap}`"
/>
<span v-if="showMoreThumbnailsCount" class="thumbnail-more-text">
{{ moreThumbnailsText }}
</span>
</div>
</template>
<script>
import Thumbnail from './Thumbnail.vue';
@@ -52,6 +34,25 @@ export default {
};
</script>
<template>
<div class="overlapping-thumbnails">
<Thumbnail
v-for="user in usersList"
:key="user.id"
v-tooltip="user.name"
:title="user.name"
:src="user.thumbnail"
:username="user.name"
has-border
:size="size"
:class="`overlapping-thumbnail gap-${gap}`"
/>
<span v-if="showMoreThumbnailsCount" class="thumbnail-more-text">
{{ moreThumbnailsText }}
</span>
</div>
</template>
<style lang="scss" scoped>
.overlapping-thumbnails {
display: flex;

View File

@@ -1,19 +1,3 @@
<template>
<div class="flex items-center gap-1.5 text-left">
<thumbnail
:src="user.thumbnail"
:size="size"
:username="user.name"
:status="user.availability_status"
/>
<h6
class="my-0 dark:text-slate-100 overflow-hidden whitespace-nowrap text-ellipsis text-capitalize"
:class="textClass"
>
{{ user.name }}
</h6>
</div>
</template>
<script>
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
@@ -37,3 +21,20 @@ export default {
},
};
</script>
<template>
<div class="flex items-center gap-1.5 text-left">
<Thumbnail
:src="user.thumbnail"
:size="size"
:username="user.name"
:status="user.availability_status"
/>
<h6
class="my-0 dark:text-slate-100 overflow-hidden whitespace-nowrap text-ellipsis text-capitalize"
:class="textClass"
>
{{ user.name }}
</h6>
</div>
</template>

View File

@@ -1,17 +1,3 @@
<template>
<woot-button
v-if="isVideoIntegrationEnabled"
v-tooltip.top-end="
$t('INTEGRATION_SETTINGS.DYTE.START_VIDEO_CALL_HELP_TEXT')
"
icon="video"
:is-loading="isLoading"
color-scheme="secondary"
variant="smooth"
size="small"
@click="onClick"
/>
</template>
<script>
import { mapGetters } from 'vuex';
import DyteAPI from 'dashboard/api/integrations/dyte';
@@ -54,3 +40,18 @@ export default {
},
};
</script>
<template>
<woot-button
v-if="isVideoIntegrationEnabled"
v-tooltip.top-end="
$t('INTEGRATION_SETTINGS.DYTE.START_VIDEO_CALL_HELP_TEXT')
"
icon="video"
:is-loading="isLoading"
color-scheme="secondary"
variant="smooth"
size="small"
@click="onClick"
/>
</template>

Some files were not shown because too many files have changed in this diff Show More