Compare commits

..
Author SHA1 Message Date
Shivam Mishra 68ed5a4b4f chore: source account id directly 2024-10-08 18:52:34 +05:30
Shivam Mishra 7ecb993d06 chore: force init account 2024-10-08 18:49:26 +05:30
Shivam Mishra c00a157c03 chore: more log 2024-10-08 18:45:08 +05:30
Shivam Mishra 3750689bee chore: debug debug 2024-10-08 18:43:37 +05:30
Shivam Mishra 0511bf4ba6 chore: more debug 2024-10-08 18:34:06 +05:30
Shivam Mishra 9f50638a40 chore: more logs 2024-10-08 18:30:59 +05:30
Shivam Mishra f5ac272e3b chore: logs 2024-10-08 18:18:49 +05:30
Shivam Mishra 2ba7318817 chore: more try 2024-10-08 18:14:58 +05:30
Shivam Mishra 412303d1cd chore: try i18n 2024-10-08 15:20:35 +05:30
Shivam Mishra 47b9399208 feat: setup fallback 2024-10-08 14:01:57 +05:30
Shivam Mishra 0830ad78b6 chore: try 2024-10-08 13:53:58 +05:30
Shivam Mishra 0b8df762f9 chore: more debug 2024-10-08 12:38:03 +05:30
Shivam Mishra f24c87f9b2 chore: try 2024-10-08 12:28:48 +05:30
Shivam Mishra f62a4170b6 chore: debug 2024-10-08 12:20:14 +05:30
Shivam Mishra 2f090dbc61 Revert "chore: don't use async"
This reverts commit 9101e1e221.
2024-10-08 11:54:02 +05:30
Shivam Mishra ec17682d7a chore: move to created 2024-10-08 11:53:55 +05:30
Shivam Mishra 7861ce8c47 chore: revert iframe helper 2024-10-08 11:42:09 +05:30
Shivam Mishra e5de41fea4 chore: revert console.logs 2024-10-08 11:41:40 +05:30
Shivam Mishra adb0e94cbd chore: remove logs 2024-10-08 11:40:53 +05:30
Shivam Mishra 42fb083491 chore: debug 2024-10-08 11:31:33 +05:30
Shivam Mishra 9101e1e221 chore: don't use async 2024-10-08 11:18:13 +05:30
Shivam Mishra 91bac16c29 chore: add more error log 2024-10-08 11:00:21 +05:30
Shivam Mishra 97699f3087 chore: log errors 2024-10-08 10:59:42 +05:30
Shivam Mishra 1242cfd704 chore: add logs for iframhelper 2024-10-08 10:57:13 +05:30
Shivam Mishra 6963c668bf chore: add console logs 2024-10-08 10:49:56 +05:30
109 changed files with 213 additions and 5314 deletions
+6 -14
View File
@@ -12,20 +12,6 @@ module.exports = {
'vitest-globals/env': true,
},
},
{
files: ['**/*.story.vue'],
rules: {
'vue/no-undef-components': [
'error',
{
ignorePatterns: ['Variant', 'Story'],
},
],
// Story files can have static strings, it doesn't need to handle i18n always.
'vue/no-bare-strings-in-template': 'off',
'no-console': 'off',
},
},
],
plugins: ['html', 'prettier'],
parserOptions: {
@@ -237,5 +223,11 @@ module.exports = {
globals: {
bus: true,
vi: true,
// beforeEach: true,
// afterEach: true,
// test: true,
// describe: true,
// it: true,
// expect: true,
},
};
+56 -56
View File
@@ -33,70 +33,70 @@ GIT
GEM
remote: https://rubygems.org/
specs:
actioncable (7.0.8.5)
actionpack (= 7.0.8.5)
activesupport (= 7.0.8.5)
actioncable (7.0.8.4)
actionpack (= 7.0.8.4)
activesupport (= 7.0.8.4)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
actionmailbox (7.0.8.5)
actionpack (= 7.0.8.5)
activejob (= 7.0.8.5)
activerecord (= 7.0.8.5)
activestorage (= 7.0.8.5)
activesupport (= 7.0.8.5)
actionmailbox (7.0.8.4)
actionpack (= 7.0.8.4)
activejob (= 7.0.8.4)
activerecord (= 7.0.8.4)
activestorage (= 7.0.8.4)
activesupport (= 7.0.8.4)
mail (>= 2.7.1)
net-imap
net-pop
net-smtp
actionmailer (7.0.8.5)
actionpack (= 7.0.8.5)
actionview (= 7.0.8.5)
activejob (= 7.0.8.5)
activesupport (= 7.0.8.5)
actionmailer (7.0.8.4)
actionpack (= 7.0.8.4)
actionview (= 7.0.8.4)
activejob (= 7.0.8.4)
activesupport (= 7.0.8.4)
mail (~> 2.5, >= 2.5.4)
net-imap
net-pop
net-smtp
rails-dom-testing (~> 2.0)
actionpack (7.0.8.5)
actionview (= 7.0.8.5)
activesupport (= 7.0.8.5)
actionpack (7.0.8.4)
actionview (= 7.0.8.4)
activesupport (= 7.0.8.4)
rack (~> 2.0, >= 2.2.4)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (7.0.8.5)
actionpack (= 7.0.8.5)
activerecord (= 7.0.8.5)
activestorage (= 7.0.8.5)
activesupport (= 7.0.8.5)
actiontext (7.0.8.4)
actionpack (= 7.0.8.4)
activerecord (= 7.0.8.4)
activestorage (= 7.0.8.4)
activesupport (= 7.0.8.4)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
actionview (7.0.8.5)
activesupport (= 7.0.8.5)
actionview (7.0.8.4)
activesupport (= 7.0.8.4)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0)
active_record_query_trace (1.8)
activejob (7.0.8.5)
activesupport (= 7.0.8.5)
activejob (7.0.8.4)
activesupport (= 7.0.8.4)
globalid (>= 0.3.6)
activemodel (7.0.8.5)
activesupport (= 7.0.8.5)
activerecord (7.0.8.5)
activemodel (= 7.0.8.5)
activesupport (= 7.0.8.5)
activemodel (7.0.8.4)
activesupport (= 7.0.8.4)
activerecord (7.0.8.4)
activemodel (= 7.0.8.4)
activesupport (= 7.0.8.4)
activerecord-import (1.4.1)
activerecord (>= 4.2)
activestorage (7.0.8.5)
actionpack (= 7.0.8.5)
activejob (= 7.0.8.5)
activerecord (= 7.0.8.5)
activesupport (= 7.0.8.5)
activestorage (7.0.8.4)
actionpack (= 7.0.8.4)
activejob (= 7.0.8.4)
activerecord (= 7.0.8.4)
activesupport (= 7.0.8.4)
marcel (~> 1.0)
mini_mime (>= 1.1.0)
activesupport (7.0.8.5)
activesupport (7.0.8.4)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
@@ -373,7 +373,7 @@ GEM
mini_mime (>= 1.0.0)
multi_xml (>= 0.5.2)
httpclient (2.8.3)
i18n (1.14.6)
i18n (1.14.5)
concurrent-ruby (~> 1.0)
image_processing (1.12.2)
mini_magick (>= 4.9.5, < 5)
@@ -479,7 +479,7 @@ GEM
uri
net-http-persistent (4.0.2)
connection_pool (~> 2.2)
net-imap (0.4.17)
net-imap (0.4.14)
date
net-protocol
net-pop (0.1.2)
@@ -557,7 +557,7 @@ GEM
activesupport (>= 3.0.0)
raabro (1.4.0)
racc (1.8.1)
rack (2.2.10)
rack (2.2.9)
rack-attack (6.7.0)
rack (>= 1.0, < 4)
rack-contrib (2.5.0)
@@ -574,20 +574,20 @@ GEM
rack-test (2.1.0)
rack (>= 1.3)
rack-timeout (0.6.3)
rails (7.0.8.5)
actioncable (= 7.0.8.5)
actionmailbox (= 7.0.8.5)
actionmailer (= 7.0.8.5)
actionpack (= 7.0.8.5)
actiontext (= 7.0.8.5)
actionview (= 7.0.8.5)
activejob (= 7.0.8.5)
activemodel (= 7.0.8.5)
activerecord (= 7.0.8.5)
activestorage (= 7.0.8.5)
activesupport (= 7.0.8.5)
rails (7.0.8.4)
actioncable (= 7.0.8.4)
actionmailbox (= 7.0.8.4)
actionmailer (= 7.0.8.4)
actionpack (= 7.0.8.4)
actiontext (= 7.0.8.4)
actionview (= 7.0.8.4)
activejob (= 7.0.8.4)
activemodel (= 7.0.8.4)
activerecord (= 7.0.8.4)
activestorage (= 7.0.8.4)
activesupport (= 7.0.8.4)
bundler (>= 1.15.0)
railties (= 7.0.8.5)
railties (= 7.0.8.4)
rails-dom-testing (2.2.0)
activesupport (>= 5.0.0)
minitest
@@ -595,9 +595,9 @@ GEM
rails-html-sanitizer (1.6.0)
loofah (~> 2.21)
nokogiri (~> 1.14)
railties (7.0.8.5)
actionpack (= 7.0.8.5)
activesupport (= 7.0.8.5)
railties (7.0.8.4)
actionpack (= 7.0.8.4)
activesupport (= 7.0.8.4)
method_source
rake (>= 12.2)
thor (~> 1.0)
+2 -2
View File
@@ -6,7 +6,7 @@ RAILS_ENV ?= development
setup:
gem install bundler
bundle install
pnpm install
yarn install
db_create:
RAILS_ENV=$(RAILS_ENV) bundle exec rails db:create
@@ -30,7 +30,7 @@ server:
RAILS_ENV=$(RAILS_ENV) bundle exec rails server -b 0.0.0.0 -p 3000
burn:
bundle && pnpm install
bundle && yarn
run:
@if [ -f ./.overmind.sock ]; then \
@@ -1,15 +1,6 @@
class Google::CallbacksController < OauthCallbackController
include GoogleConcern
def find_channel_by_email
# find by imap_login first, and then by email
# this ensures the legacy users can migrate correctly even if inbox email address doesn't match
imap_channel = Channel::Email.find_by(imap_login: users_data['email'], account: account)
return imap_channel if imap_channel
Channel::Email.find_by(email: users_data['email'], account: account)
end
private
def provider_name
+1 -5
View File
@@ -25,7 +25,7 @@ class OauthCallbackController < ApplicationController
end
def find_or_create_inbox
channel_email = find_channel_by_email
channel_email = Channel::Email.find_by(email: users_data['email'], account: account)
# we need this value to know where to redirect on sucessful processing of the callback
channel_exists = channel_email.present?
@@ -39,10 +39,6 @@ class OauthCallbackController < ApplicationController
[channel_email.inbox, channel_exists]
end
def find_channel_by_email
Channel::Email.find_by(email: users_data['email'], account: account)
end
def update_channel(channel_email)
channel_email.update!({
imap_login: users_data['email'], imap_address: imap_address,
+19 -13
View File
@@ -13,7 +13,6 @@ import { useStore } from 'dashboard/composables/store';
import WootSnackbarBox from './components/SnackbarContainer.vue';
import { setColorTheme } from './helper/themeHelper';
import { isOnOnboardingView } from 'v3/helpers/RouteHelper';
import { useAccount } from 'dashboard/composables/useAccount';
import {
registerSubscription,
verifyServiceWorkerExistence,
@@ -36,9 +35,8 @@ export default {
setup() {
const router = useRouter();
const store = useStore();
const { accountId } = useAccount();
return { router, store, currentAccountId: accountId };
return { router, store };
},
data() {
return {
@@ -54,7 +52,11 @@ export default {
currentUser: 'getCurrentUser',
authUIFlags: 'getAuthUIFlags',
accountUIFlags: 'accounts/getUIFlags',
// currentAccountId: 'getCurrentAccountId',
}),
currentAccountId() {
return this.$route.params.accountId;
},
hasAccounts() {
const { accounts = [] } = this.currentUser || {};
return accounts.length > 0;
@@ -70,19 +72,23 @@ export default {
this.showAddAccountModal = true;
}
},
currentAccountId: {
immediate: true,
handler() {
if (this.currentAccountId) {
this.initializeAccount();
}
},
currentAccountId() {
console.log('THIS CALL', this.currentAccountId);
if (this.currentAccountId) {
this.initializeAccount();
} else {
console.log('NO ACCOUNT ID');
}
},
},
mounted() {
created() {
this.setLocale(window.chatwootConfig.selectedLocale);
},
async mounted() {
this.setLocale(window.chatwootConfig.selectedLocale);
this.initializeColorTheme();
this.listenToThemeChanges();
this.setLocale(window.chatwootConfig.selectedLocale);
await this.$store.dispatch('accounts/get');
},
unmounted() {
if (this.reconnectService) {
@@ -98,7 +104,7 @@ export default {
mql.onchange = e => setColorTheme(e.matches);
},
setLocale(locale) {
this.$root.$i18n.locale = locale;
this.$i18n.locale = locale;
},
async initializeAccount() {
await this.$store.dispatch('accounts/get');
+1 -151
View File
@@ -46,158 +46,8 @@
@apply hidden;
}
// scss-lint:disable PropertySortOrder
@layer base {
/* NEXT COLORS START */
:root {
/* slate */
--slate-1: 252 252 253;
--slate-2: 249 249 251;
--slate-3: 240 240 243;
--slate-4: 232 232 236;
--slate-5: 224 225 230;
--slate-6: 217 217 224;
--slate-7: 205 206 214;
--slate-8: 185 187 198;
--slate-9: 139 141 152;
--slate-10: 128 131 141;
--slate-11: 96 100 108;
--slate-12: 28 32 36;
--ruby-1: 255 252 253;
--ruby-2: 255 247 248;
--ruby-3: 254 234 237;
--ruby-4: 255 220 225;
--ruby-5: 255 206 214;
--ruby-6: 248 191 200;
--ruby-7: 239 172 184;
--ruby-8: 229 146 163;
--ruby-9: 229 70 102;
--ruby-10: 220 59 93;
--ruby-11: 202 36 77;
--ruby-12: 100 23 43;
--amber-1: 254 253 251;
--amber-2: 254 251 233;
--amber-3: 255 247 194;
--amber-4: 255 238 156;
--amber-5: 251 229 119;
--amber-6: 243 214 115;
--amber-7: 233 193 98;
--amber-8: 226 163 54;
--amber-9: 255 197 61;
--amber-10: 255 186 24;
--amber-11: 171 100 0;
--amber-12: 79 52 34;
--teal-1: 250 254 253;
--teal-2: 243 251 249;
--teal-3: 224 248 243;
--teal-4: 204 243 234;
--teal-5: 184 234 224;
--teal-6: 161 222 210;
--teal-7: 131 205 193;
--teal-8: 83 185 171;
--teal-9: 18 165 148;
--teal-10: 13 155 138;
--teal-11: 0 133 115;
--teal-12: 13 61 56;
--background-color: 248 248 248;
--solid-1: 255 255 255;
--solid-2: 252 252 252;
--solid-3: 255 255 255;
--solid-active: 250 251 251;
--solid-amber: 252 232 193;
--solid-blue: 218 236 255;
--white-alpha: 255 255 255 0.1;
--border-weak: 231 231 231;
--border-strong: 235 235 235;
--blue: 39 129 246;
/* alpha is added by default */
--alpha-1: 36, 38, 48, 0.06;
--alpha-2: 130, 134, 150, 0.12;
--alpha-3: 255, 255, 255, 0.9;
--black-alpha-1: 0, 0, 0, 0.12;
--black-alpha-2: 0, 0, 0, 0.04;
}
body.dark {
/* slate */
--slate-1: 17 17 19;
--slate-2: 24 25 27;
--slate-3: 33 34 37;
--slate-4: 39 42 45;
--slate-5: 46 49 53;
--slate-6: 54 58 63;
--slate-7: 67 72 78;
--slate-8: 90 97 105;
--slate-9: 105 110 119;
--slate-10: 119 123 132;
--slate-11: 176 180 186;
--slate-12: 237 238 240;
--ruby-1: 25 17 19;
--ruby-2: 30 21 23;
--ruby-3: 58 20 30;
--ruby-4: 78 19 37;
--ruby-5: 94 26 46;
--ruby-6: 111 37 57;
--ruby-7: 136 52 71;
--ruby-8: 179 68 90;
--ruby-9: 229 70 102;
--ruby-10: 236 90 114;
--ruby-11: 255 148 157;
--ruby-12: 254 210 225;
--amber-1: 22 18 12;
--amber-2: 29 24 15;
--amber-3: 48 32 8;
--amber-4: 63 39 0;
--amber-5: 77 48 0;
--amber-6: 92 61 5;
--amber-7: 113 79 25;
--amber-8: 143 100 36;
--amber-9: 255 197 61;
--amber-10: 255 214 10;
--amber-11: 255 202 22;
--amber-12: 255 231 179;
--teal-1: 13 21 20;
--teal-2: 17 28 27;
--teal-3: 13 45 42;
--teal-4: 2 59 55;
--teal-5: 8 72 67;
--teal-6: 20 87 80;
--teal-7: 28 105 97;
--teal-8: 32 126 115;
--teal-9: 18 165 148;
--teal-10: 14 179 158;
--teal-11: 11 216 182;
--teal-12: 173 240 221;
--background-color: 18 18 19;
--solid-1: 23 23 26;
--solid-2: 29 30 36;
--solid-3: 36 38 48;
--solid-amber: 42 37 30;
--solid-blue: 16 49 91;
--solid-active: 51 53 64;
--border-weak: 34 34 37;
--border-strong: 46 47 49;
--blue: 126 182 255;
/* alpha is added by default */
--alpha-1: 35, 37, 45, 0.8;
--alpha-2: 130, 134, 150, 0.15;
--alpha-3: 32, 33, 37, 0.9;
--black-alpha-1: 0, 0, 0, 0.3;
--black-alpha-2: 0, 0, 0, 0.2;
--white-alpha: 255, 255, 255, 0.1;
}
/* NEXT COLORS END */
// scss-lint:disable PropertySortOrder
:root {
--color-amber-25: 254 253 251;
--color-amber-50: 255 249 237;
@@ -1,16 +0,0 @@
<script setup>
const emit = defineEmits(['click']);
const handleClick = () => {
emit('click');
};
</script>
<template>
<div
class="relative flex flex-col w-full gap-3 px-6 py-5 group/cardLayout rounded-2xl bg-slate-25 dark:bg-slate-800/50"
@click="handleClick"
>
<slot name="header" />
<slot name="footer" />
</div>
</template>
@@ -1,47 +0,0 @@
<script setup>
defineProps({
title: {
type: String,
required: true,
},
subtitle: {
type: String,
required: true,
},
});
</script>
<template>
<section
class="relative flex flex-col items-center justify-center w-full h-full min-h-screen p-4 overflow-hidden"
>
<div
class="relative w-full max-w-[940px] mx-auto overflow-hidden h-full max-h-[448px]"
>
<div
class="w-full h-full space-y-4 overflow-y-hidden opacity-50 pointer-events-none"
>
<slot name="empty-state-item" />
</div>
<div
class="absolute inset-x-0 bottom-0 flex flex-col items-center justify-end w-full h-full pb-9 bg-gradient-to-t from-white dark:from-slate-900 to-transparent font-interDisplay"
>
<div class="flex flex-col items-center justify-center gap-6">
<div class="flex flex-col items-center justify-center gap-2">
<h2
class="text-3xl font-medium text-center text-slate-900 dark:text-white"
>
{{ title }}
</h2>
<p
class="max-w-lg text-base text-center text-slate-600 dark:text-slate-300"
>
{{ subtitle }}
</p>
</div>
<slot name="actions" />
</div>
</div>
</div>
</section>
</template>
@@ -1,56 +0,0 @@
<script setup>
import ArticleCard from './ArticleCard.vue';
const articles = [
{
title: "How to get an SSL certificate for your Help Center's custom domain",
status: 'draft',
updatedAt: '2 days ago',
author: 'Michael',
category: '⚡️ Marketing',
views: 400,
},
{
title: 'Setting up your first Help Center portal',
status: '',
updatedAt: '1 week ago',
author: 'John',
category: '🛠️ Development',
views: 1400,
},
{
title: 'Best practices for organizing your Help Center content',
status: 'archived',
updatedAt: '3 days ago',
author: 'Fernando',
category: '💰 Finance',
views: 4300,
},
];
</script>
<!-- eslint-disable vue/no-bare-strings-in-template -->
<!-- eslint-disable vue/no-undef-components -->
<template>
<Story
title="Components/HelpCenter/ArticleCard"
:layout="{ type: 'grid', width: '700px' }"
>
<Variant title="Article Card">
<div
v-for="(article, index) in articles"
:key="index"
class="px-20 py-4 bg-white dark:bg-slate-900"
>
<ArticleCard
:title="article.title"
:status="article.status"
:author="article.author"
:category="article.category"
:views="article.views"
:updated-at="article.updatedAt"
/>
</div>
</Variant>
</Story>
</template>
@@ -1,142 +0,0 @@
<script setup>
import { computed, ref } from 'vue';
import { OnClickOutside } from '@vueuse/components';
import CardLayout from 'dashboard/components-next/CardLayout.vue';
import DropdownMenu from 'dashboard/components-next/dropdown-menu/DropdownMenu.vue';
import Button from 'dashboard/components-next/button/Button.vue';
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
const props = defineProps({
title: {
type: String,
required: true,
},
status: {
type: String,
required: true,
},
author: {
type: String,
required: true,
},
category: {
type: String,
required: true,
},
views: {
type: Number,
required: true,
},
updatedAt: {
type: String,
required: true,
},
});
const isOpen = ref(false);
const menuItems = computed(() => {
const baseItems = [{ label: 'Delete', action: 'delete', icon: 'delete' }];
const menuOptions = {
archived: [
{ label: 'Publish', action: 'publish', icon: 'checkmark' },
{ label: 'Draft', action: 'draft', icon: 'draft' },
],
draft: [
{ label: 'Publish', action: 'publish', icon: 'checkmark' },
{ label: 'Archive', action: 'archive', icon: 'archive' },
],
'': [
// Empty string represents published status
{ label: 'Draft', action: 'draft', icon: 'draft' },
{ label: 'Archive', action: 'archive', icon: 'archive' },
],
};
return [...(menuOptions[props.status] || menuOptions['']), ...baseItems];
});
const statusTextColor = computed(() => {
switch (props.status) {
case 'archived':
return '!text-slate-600 dark:!text-slate-200';
case 'draft':
return '!text-amber-700 dark:!text-amber-400';
default:
return '!text-teal-700 dark:!text-teal-400';
}
});
const statusText = computed(() => {
switch (props.status) {
case 'archived':
return 'Archived';
case 'draft':
return 'Draft';
default:
return 'Published';
}
});
const handleAction = () => {
isOpen.value = false;
};
</script>
<!-- TODO: Add i18n -->
<!-- eslint-disable vue/no-bare-strings-in-template -->
<template>
<CardLayout>
<template #header>
<div class="flex justify-between gap-1">
<span class="text-base text-slate-900 dark:text-slate-50 line-clamp-1">
{{ title }}
</span>
<div class="relative group">
<Button
variant="ghost"
size="sm"
class="text-xs bg-slate-50 !font-normal group-hover:bg-slate-100/50 dark:group-hover:bg-slate-700/50 !h-6 dark:bg-slate-800 rounded-md border-0 !px-2 !py-0.5"
:label="statusText"
:class="statusTextColor"
@click="isOpen = !isOpen"
/>
<OnClickOutside @trigger="isOpen = false">
<DropdownMenu
v-if="isOpen"
:menu-items="menuItems"
class="right-0 mt-2 xl:left-0 top-full"
@action="handleAction"
/>
</OnClickOutside>
</div>
</div>
</template>
<template #footer>
<div class="flex items-center justify-between gap-4">
<div class="flex items-center gap-4">
<div class="flex items-center gap-1">
<div class="w-4 h-4 rounded-full bg-slate-100 dark:bg-slate-700" />
<span class="text-sm text-slate-500 dark:text-slate-400">
{{ author }}
</span>
</div>
<span
class="block text-sm whitespace-nowrap text-slate-500 dark:text-slate-400"
>
{{ category }}
</span>
<div
class="inline-flex items-center gap-1 text-slate-500 dark:text-slate-400 whitespace-nowrap"
>
<FluentIcon icon="eye-show" size="18" />
<span class="text-sm"> {{ views }} views </span>
</div>
</div>
<span class="text-sm text-slate-600 dark:text-slate-400 line-clamp-1">
{{ updatedAt }}
</span>
</div>
</template>
</CardLayout>
</template>
@@ -1,41 +0,0 @@
<script setup>
import CategoryCard from './CategoryCard.vue';
const categories = [
{
id: 'getting-started',
title: '🚀 Getting started',
description:
'Learn how to use Chatwoot effectively and make the most of its features to enhance customer support and engagement.',
articlesCount: '5',
},
{
id: 'marketing',
title: '📈 Marketing',
description: '',
articlesCount: '4',
},
];
</script>
<!-- eslint-disable vue/no-bare-strings-in-template -->
<!-- eslint-disable vue/no-undef-components -->
<template>
<Story
title="Components/HelpCenter/CategoryCard"
:layout="{ type: 'grid', width: '800px' }"
>
<Variant title="Category Card">
<div
v-for="(category, index) in categories"
:key="index"
class="px-20 py-4 bg-white dark:bg-slate-900"
>
<CategoryCard
:title="category.title"
:description="category.description"
:articles-count="category.articlesCount"
/>
</div>
</Variant>
</Story>
</template>
@@ -1,115 +0,0 @@
<script setup>
import { ref, computed } from 'vue';
import { OnClickOutside } from '@vueuse/components';
import CardLayout from 'dashboard/components-next/CardLayout.vue';
import Button from 'dashboard/components-next/button/Button.vue';
import DropdownMenu from 'dashboard/components-next/dropdown-menu/DropdownMenu.vue';
const props = defineProps({
id: {
type: String,
required: true,
},
title: {
type: String,
required: true,
},
articlesCount: {
type: String,
required: true,
},
description: {
type: String,
required: true,
},
});
const emit = defineEmits(['click']);
const isOpen = ref(false);
const menuItems = [
{
label: 'Edit',
action: 'edit',
icon: 'edit',
},
{
label: 'Delete',
action: 'delete',
icon: 'delete',
},
];
const description = computed(() => {
return props.description ? props.description : 'No description added';
});
const hasDescription = computed(() => {
return props.description.length > 0;
});
const handleClick = id => {
emit('click', id);
};
// eslint-disable-next-line no-unused-vars
const handleAction = action => {
// TODO: Implement action
};
</script>
<!-- TODO: Add i18n -->
<!-- eslint-disable vue/no-bare-strings-in-template -->
<template>
<CardLayout @click="handleClick(id)">
<template #header>
<div class="flex gap-2">
<div class="flex justify-between w-full">
<div class="flex items-center justify-start gap-2">
<span
class="text-base cursor-pointer group-hover/cardLayout:underline text-slate-900 dark:text-slate-50 line-clamp-1"
>
{{ title }}
</span>
<span
class="inline-flex items-center justify-center h-6 px-2 py-1 text-xs text-center border rounded-lg text-slate-500 w-fit border-slate-200 dark:border-slate-800 dark:text-slate-400"
>
{{ articlesCount }} articles
</span>
</div>
<div class="relative group" @click.stop>
<Button
variant="ghost"
size="icon"
icon="more-vertical"
class="w-8 z-60 group-hover:bg-slate-100 dark:group-hover:bg-slate-800"
@click="isOpen = !isOpen"
/>
<OnClickOutside @trigger="isOpen = false">
<DropdownMenu
v-if="isOpen"
:menu-items="menuItems"
class="right-0 mt-1 xl:left-0 top-full z-60"
@action="handleAction"
/>
</OnClickOutside>
</div>
</div>
</div>
</template>
<template #footer>
<span
class="text-sm line-clamp-3"
:class="
hasDescription
? 'text-slate-500 dark:text-slate-400'
: 'text-slate-400 dark:text-slate-700'
"
>
{{ description }}
</span>
</template>
</CardLayout>
</template>
@@ -1,16 +0,0 @@
<script setup>
import ArticleEmptyState from './ArticleEmptyState.vue';
</script>
<template>
<Story
title="Components/HelpCenter/EmptyState/ArticleEmptyState"
:layout="{ type: 'single', width: '1100px' }"
>
<Variant title="Article Empty State">
<div class="w-full h-full px-20 mx-auto bg-white dark:bg-slate-900">
<ArticleEmptyState />
</div>
</Variant>
</Story>
</template>
@@ -1,87 +0,0 @@
<script setup>
// import { ref } from 'vue';
import EmptyStateLayout from 'dashboard/components-next/EmptyStateLayout.vue';
import Button from 'dashboard/components-next/button/Button.vue';
import ArticleCard from 'dashboard/components-next/HelpCenter/ArticleCard/ArticleCard.vue';
// import AddLocaleDialog from 'dashboard/playground/HelpCenter/components/AddLocaleDialog.vue';
const articles = [
{
title: "How to get an SSL certificate for your Help Center's custom domain",
status: 'draft',
updatedAt: '2 days ago',
author: 'Michael',
category: '⚡️ Marketing',
views: 3400,
},
{
title: 'Setting up your first Help Center portal',
status: '',
updatedAt: '1 week ago',
author: 'John',
category: '🛠️ Development',
views: 400,
},
{
title: 'Best practices for organizing your Help Center content',
status: 'archived',
updatedAt: '3 days ago',
author: 'Fernando',
category: '💰 Finance',
views: 400,
},
{
title: 'Customizing the appearance of your Help Center',
status: '',
updatedAt: '5 days ago',
author: 'Jane',
category: '💰 Finance',
views: 400,
},
];
// const addLocaleDialogRef = ref(null);
// const openDialog = () => {
// addLocaleDialogRef.value.dialogRef.open();
// };
// const handleDialogConfirm = () => {
// // Add logic to create a new portal
// };
</script>
<!-- TODO: Add i18n -->
<!-- eslint-disable vue/no-bare-strings-in-template -->
<template>
<EmptyStateLayout
title="Write an article"
subtitle="Write a rich article, let's get started!"
>
<template #empty-state-item>
<div class="grid grid-cols-1 gap-4">
<ArticleCard
v-for="(article, index) in articles"
:key="`article-${index}`"
:title="article.title"
:status="article.status"
:updated-at="article.updatedAt"
:author="article.author"
:category="article.category"
:views="article.views"
/>
</div>
</template>
<template #actions>
<Button
variant="default"
label="New article"
icon="add"
@click="openDialog"
/>
<!-- <AddLocaleDialog
ref="addLocaleDialogRef"
@confirm="handleDialogConfirm"
/> -->
</template>
</EmptyStateLayout>
</template>
@@ -1,16 +0,0 @@
<script setup>
import PortalEmptyState from './PortalEmptyState.vue';
</script>
<template>
<Story
title="Components/HelpCenter/EmptyState/PortalEmptyState"
:layout="{ type: 'single', width: '1100px' }"
>
<Variant title="Portal Empty State">
<div class="w-full h-full px-20 mx-auto bg-white dark:bg-slate-900">
<PortalEmptyState />
</div>
</Variant>
</Story>
</template>
@@ -1,123 +0,0 @@
<script setup>
// import { ref } from 'vue';
import EmptyStateLayout from 'dashboard/components-next/EmptyStateLayout.vue';
import Button from 'dashboard/components-next/button/Button.vue';
import ArticleCard from 'dashboard/components-next/HelpCenter/ArticleCard/ArticleCard.vue';
import CategoryCard from 'dashboard/components-next/HelpCenter/CategoryCard/CategoryCard.vue';
import LocaleCard from 'dashboard/components-next/HelpCenter/LocaleCard/LocaleCard.vue';
// import CreatePortalDialog from 'dashboard/playground/HelpCenter/components/CreatePortalDialog.vue';
const articles = [
{
title: "How to get an SSL certificate for your Help Center's custom domain",
status: 'draft',
updatedAt: '2 days ago',
author: 'Michael',
category: '⚡️ Marketing',
views: 3400,
},
{
title: 'Setting up your first Help Center portal',
status: '',
updatedAt: '1 week ago',
author: 'John',
category: '🛠️ Development',
views: 400,
},
{
title: 'Best practices for organizing your Help Center content',
status: 'archived',
updatedAt: '3 days ago',
author: 'Fernando',
category: '💰 Finance',
views: 400,
},
{
title: 'Customizing the appearance of your Help Center',
status: '',
updatedAt: '5 days ago',
author: 'Jane',
category: '💰 Finance',
views: 400,
},
];
const categories = [
{
title: 'Getting Started',
description: 'Essential guides for new users',
articlesCount: '5',
},
{
title: 'Advanced Features',
description: 'In-depth tutorials for power users',
articlesCount: '8',
},
];
const locales = [
{ name: 'English', isDefault: true },
{ name: 'Spanish', isDefault: false },
{ name: 'Malayalam', isDefault: false },
];
// const createPortalDialogRef = ref(null);
// const openDialog = () => {
// createPortalDialogRef.value.dialogRef.open();
// };
// const handleDialogConfirm = () => {
// // Add logic to create a new portal
// };
</script>
<!-- TODO: Add i18n -->
<!-- eslint-disable vue/no-bare-strings-in-template -->
<template>
<EmptyStateLayout
title="Help Center"
subtitle="Create self-service portals to access articles and information. Streamline queries, enhance agent efficiency, and elevate customer support."
>
<template #empty-state-item>
<div class="grid grid-cols-2 gap-4">
<div class="space-y-4">
<ArticleCard
v-for="(article, index) in articles"
:key="`article-${index}`"
:title="article.title"
:status="article.status"
:updated-at="article.updatedAt"
:author="article.author"
:category="article.category"
:views="article.views"
/>
</div>
<div class="space-y-4">
<CategoryCard
v-for="(category, index) in categories"
:key="`category-${index}`"
:title="category.title"
:description="category.description"
:articles-count="category.articlesCount"
/>
<LocaleCard
v-for="(locale, index) in locales"
:key="`locale-${index}`"
:locale="locale.name"
:is-default="locale.isDefault"
/>
</div>
</div>
</template>
<template #actions>
<Button
variant="default"
label="Create Portal"
icon="add"
@click="openDialog"
/>
<!-- <CreatePortalDialog
ref="createPortalDialogRef"
@confirm="handleDialogConfirm"
/> -->
</template>
</EmptyStateLayout>
</template>
@@ -1,99 +0,0 @@
<script setup>
import { ref } from 'vue';
import { OnClickOutside } from '@vueuse/components';
import PaginationFooter from 'dashboard/components-next/pagination/PaginationFooter.vue';
import Button from 'dashboard/components-next/button/Button.vue';
import PortalSwitcher from 'dashboard/components-next/HelpCenter/PortalSwitcher/PortalSwitcher.vue';
defineProps({
header: {
type: String,
default: 'Chatwoot Help Center',
},
currentPage: {
type: Number,
default: 1,
},
totalItems: {
type: Number,
default: 100,
},
itemsPerPage: {
type: Number,
default: 25,
},
showHeaderTitle: {
type: Boolean,
default: true,
},
showPaginationFooter: {
type: Boolean,
default: true,
},
});
const emit = defineEmits(['update:currentPage']);
const showPortalSwitcher = ref(false);
const updateCurrentPage = page => {
emit('update:currentPage', page);
};
const togglePortalSwitcher = () => {
showPortalSwitcher.value = !showPortalSwitcher.value;
};
</script>
<template>
<section
class="flex flex-col w-full h-full overflow-hidden bg-white dark:bg-slate-900"
>
<header
class="sticky top-0 z-10 px-6 pb-3 bg-white lg:px-0 dark:bg-slate-900"
>
<div class="w-full max-w-[900px] mx-auto">
<div
v-if="showHeaderTitle"
class="flex items-center justify-start h-20 gap-2"
>
<span class="text-xl font-medium text-slate-900 dark:text-white">
{{ header }}
</span>
<div class="relative group">
<Button
icon="more-vertical"
variant="ghost"
size="sm"
class="group-hover:bg-slate-100 dark:group-hover:bg-slate-800"
@click="togglePortalSwitcher"
/>
<OnClickOutside @trigger="showPortalSwitcher = false">
<PortalSwitcher
v-if="showPortalSwitcher"
class="absolute left-0 top-9"
/>
</OnClickOutside>
</div>
</div>
<slot name="header-actions" />
</div>
</header>
<main class="flex-1 px-6 overflow-y-auto lg:px-0">
<div class="w-full max-w-[900px] mx-auto py-3">
<slot name="content" />
</div>
</main>
<footer
v-if="showPaginationFooter"
class="sticky bottom-0 z-10 px-4 pt-3 pb-4 bg-white dark:bg-slate-900"
>
<PaginationFooter
:current-page="currentPage"
:total-items="totalItems"
:items-per-page="itemsPerPage"
@update:current-page="updateCurrentPage"
/>
</footer>
</section>
</template>
@@ -1,29 +0,0 @@
<script setup>
import LocaleCard from './LocaleCard.vue';
const locales = [
{ name: 'English', isDefault: true, articleCount: 29, categoryCount: 5 },
{ name: 'Spanish', isDefault: false, articleCount: 29, categoryCount: 5 },
];
</script>
<!-- eslint-disable vue/no-bare-strings-in-template -->
<!-- eslint-disable vue/no-undef-components -->
<template>
<Story
title="Components/HelpCenter/LocaleCard"
:layout="{ type: 'grid', width: '800px' }"
>
<Variant title="Locale Card">
<div class="px-10 py-4 bg-white dark:bg-slate-900">
<div v-for="(locale, index) in locales" :key="index" class="px-20 py-2">
<LocaleCard
:locale="locale.name"
:is-default="locale.isDefault"
:article-count="locale.articleCount"
:category-count="locale.categoryCount"
/>
</div>
</div>
</Variant>
</Story>
</template>
@@ -1,103 +0,0 @@
<script setup>
import { ref } from 'vue';
import { OnClickOutside } from '@vueuse/components';
import CardLayout from 'dashboard/components-next/CardLayout.vue';
import Button from 'dashboard/components-next/button/Button.vue';
import DropdownMenu from 'dashboard/components-next/dropdown-menu/DropdownMenu.vue';
defineProps({
locale: {
type: String,
required: true,
},
isDefault: {
type: Boolean,
required: true,
},
articleCount: {
type: Number,
required: true,
},
categoryCount: {
type: Number,
required: true,
},
});
const isOpen = ref(false);
const menuItems = [
{
label: 'Make default',
action: 'default',
icon: 'star-emphasis',
},
{
label: 'Delete',
action: 'delete',
icon: 'delete',
},
];
// eslint-disable-next-line no-unused-vars
const handleAction = action => {
// TODO: Implement action
};
</script>
<!-- TODO: Add i18n -->
<!-- eslint-disable vue/no-bare-strings-in-template -->
<template>
<CardLayout class="ltr:pr-2 rtl:pl-2">
<template #header>
<div class="flex justify-between gap-2">
<div class="flex items-center justify-start gap-2">
<span
class="text-sm font-medium text-slate-900 dark:text-slate-50 line-clamp-1"
>
{{ locale }}
</span>
<span
v-if="isDefault"
class="bg-slate-100 dark:bg-slate-800 h-6 inline-flex items-center justify-center rounded-md text-xs border-px border-transparent text-woot-500 dark:text-woot-400 px-2 py-0.5"
>
Default
</span>
</div>
<div class="flex items-center justify-end gap-1">
<div class="flex items-center gap-4">
<span
class="text-sm text-slate-500 dark:text-slate-400 whitespace-nowrap"
>
{{ articleCount }} articles
</span>
<div class="w-px h-3 bg-slate-75 dark:bg-slate-800" />
<span
class="text-sm text-slate-500 dark:text-slate-400 whitespace-nowrap"
>
{{ categoryCount }} categories
</span>
</div>
<div class="relative group">
<Button
variant="ghost"
size="icon"
icon="more-vertical"
class="w-8 group-hover:bg-slate-100 dark:group-hover:bg-slate-800"
@click="isOpen = !isOpen"
/>
<OnClickOutside @trigger="isOpen = false">
<DropdownMenu
v-if="isOpen"
:menu-items="menuItems"
class="right-0 mt-1 xl:left-0 top-full z-60 min-w-[147px]"
@action="handleAction"
/>
</OnClickOutside>
</div>
</div>
</div>
</template>
</CardLayout>
</template>
@@ -1,25 +0,0 @@
<script setup>
import ArticleCard from 'dashboard/components-next/HelpCenter/ArticleCard/ArticleCard.vue';
defineProps({
articles: {
type: Array,
required: true,
},
});
</script>
<template>
<ul role="list" class="w-full h-full space-y-4">
<ArticleCard
v-for="article in articles"
:key="article.title"
:title="article.title"
:status="article.status"
:author="article.author"
:category="article.category"
:views="article.views"
:updated-at="article.updatedAt"
/>
</ul>
</template>
@@ -1,72 +0,0 @@
<script setup>
import ArticlesPage from './ArticlesPage.vue';
const articles = [
{
title: "How to get an SSL certificate for your Help Center's custom domain",
status: 'draft',
updatedAt: '2 days ago',
author: 'Michael',
category: '⚡️ Marketing',
views: 3400,
},
{
title: 'Setting up your first Help Center portal',
status: '',
updatedAt: '1 week ago',
author: 'John',
category: '🛠️ Development',
views: 400,
},
{
title: 'Best practices for organizing your Help Center content',
status: 'archived',
updatedAt: '3 days ago',
author: 'Fernando',
category: '💰 Finance',
views: 400,
},
{
title: 'Customizing the appearance of your Help Center',
status: '',
updatedAt: '5 days ago',
author: 'Jane',
category: '💰 Finance',
views: 400,
},
{
title: 'Best practices for organizing your Help Center content',
status: 'archived',
updatedAt: '3 days ago',
author: 'Fernando',
category: '💰 Finance',
views: 400,
},
{
title: 'Customizing the appearance of your Help Center',
status: '',
updatedAt: '5 days ago',
author: 'Jane',
category: '💰 Finance',
views: 400,
},
{
title: 'Best practices for organizing your Help Center content',
status: 'archived',
updatedAt: '3 days ago',
author: 'Fernando',
category: '💰 Finance',
views: 400,
},
];
</script>
<template>
<Story title="Pages/HelpCenter/ArticlesPage" :layout="{ type: 'single' }">
<Variant title="All Articles">
<div class="w-full min-h-screen bg-white dark:bg-slate-900">
<ArticlesPage :articles="articles" />
</div>
</Variant>
</Story>
</template>
@@ -1,74 +0,0 @@
<script setup>
import HelpCenterLayout from 'dashboard/components-next/HelpCenter/HelpCenterLayout.vue';
import TabBar from 'dashboard/components-next/tabbar/TabBar.vue';
import Button from 'dashboard/components-next/button/Button.vue';
import ArticleList from 'dashboard/components-next/HelpCenter/Pages/ArticlePage/ArticleList.vue';
defineProps({
articles: {
type: Array,
required: true,
},
});
const tabs = [
{ label: 'All articles', count: 24 },
{ label: 'Mine', count: 13 },
{ label: 'Draft', count: 5 },
{ label: 'Archived', count: 11 },
];
// TODO: remove comments
// eslint-disable-next-line no-unused-vars
const handleTabChange = tab => {
// TODO: Implement tab change logic
};
// eslint-disable-next-line no-unused-vars
const handlePageChange = page => {
// TODO: Implement page change logic
};
</script>
<template>
<HelpCenterLayout
:current-page="1"
:total-items="100"
:items-per-page="10"
@update:current-page="handlePageChange"
>
<template #header-actions>
<div class="flex items-end justify-between">
<div class="flex flex-col items-start w-full gap-2 lg:flex-row">
<TabBar
:tabs="tabs"
:initial-active-tab="1"
@tab-changed="handleTabChange"
/>
<div class="flex items-start justify-between w-full gap-2">
<div class="flex items-center gap-2">
<Button
label="English"
size="sm"
icon-position="right"
icon="chevron-lucide-down"
icon-lib="lucide"
variant="secondary"
/>
<Button
label="All categories"
size="sm"
icon-position="right"
icon="chevron-lucide-down"
icon-lib="lucide"
variant="secondary"
/>
</div>
<Button label="New article" icon="add" size="sm" />
</div>
</div>
</div>
</template>
<template #content>
<ArticleList :articles="articles" />
</template>
</HelpCenterLayout>
</template>
@@ -1,209 +0,0 @@
<script setup>
import CategoriesPage from './CategoriesPage.vue';
const categories = [
{
id: 'getting-started',
title: '🚀 Getting started',
description:
'Learn how to use Chatwoot effectively and make the most of its features to enhance customer support and engagement.',
articlesCount: '2',
articles: [
{
variant: 'Draft article',
title:
"How to get an SSL certificate for your Help Center's custom domain",
status: 'draft',
updatedAt: '2 days ago',
author: 'Michael',
category: '⚡️ Marketing',
views: 3400,
},
{
variant: 'Published article',
title: 'Setting up your first Help Center portal',
status: '',
updatedAt: '1 week ago',
author: 'John',
category: '🛠️ Development',
views: 400,
},
],
},
{
id: 'marketing',
title: 'Marketing',
description:
'Learn how to use Chatwoot effectively and make the most of its features to enhance customer support.',
articlesCount: '4',
articles: [
{
variant: 'Draft article',
title:
"How to get an SSL certificate for your Help Center's custom domain",
status: 'draft',
updatedAt: '2 days ago',
author: 'Michael',
category: '⚡️ Marketing',
views: 3400,
},
{
variant: 'Published article',
title: 'Setting up your first Help Center portal',
status: '',
updatedAt: '1 week ago',
author: 'John',
category: '🛠️ Development',
views: 400,
},
{
variant: 'Archived article',
title: 'Best practices for organizing your Help Center content',
status: 'archived',
updatedAt: '3 days ago',
author: 'Fernando',
category: '💰 Finance',
views: 400,
},
{
variant: 'Published article',
title: 'Customizing the appearance of your Help Center',
status: '',
updatedAt: '5 days ago',
author: 'Jane',
category: '💰 Finance',
views: 400,
},
],
},
{
id: 'development',
title: 'Development',
description: '',
articlesCount: '5',
articles: [
{
variant: 'Draft article',
title:
"How to get an SSL certificate for your Help Center's custom domain",
status: 'draft',
updatedAt: '2 days ago',
author: 'Michael',
category: '⚡️ Marketing',
views: 3400,
},
{
variant: 'Published article',
title: 'Setting up your first Help Center portal',
status: '',
updatedAt: '1 week ago',
author: 'John',
category: '🛠️ Development',
views: 400,
},
{
variant: 'Archived article',
title: 'Best practices for organizing your Help Center content',
status: 'archived',
updatedAt: '3 days ago',
author: 'Fernando',
category: '💰 Finance',
views: 400,
},
{
variant: 'Archived article',
title: 'Best practices for organizing your Help Center content',
status: 'archived',
updatedAt: '3 days ago',
author: 'Fernando',
category: '💰 Finance',
views: 400,
},
{
variant: 'Published article',
title: 'Customizing the appearance of your Help Center',
status: '',
updatedAt: '5 days ago',
author: 'Jane',
category: '💰 Finance',
views: 400,
},
],
},
{
id: 'roadmap',
title: '🛣️ Roadmap',
description:
'Learn how to use Chatwoot effectively and make the most of its features to enhance customer support and engagement.',
articlesCount: '3',
articles: [
{
variant: 'Draft article',
title:
"How to get an SSL certificate for your Help Center's custom domain",
status: 'draft',
updatedAt: '2 days ago',
author: 'Michael',
category: '⚡️ Marketing',
views: 3400,
},
{
variant: 'Published article',
title: 'Setting up your first Help Center portal',
status: '',
updatedAt: '1 week ago',
author: 'John',
category: '🛠️ Development',
views: 400,
},
{
variant: 'Published article',
title: 'Setting up your first Help Center portal',
status: '',
updatedAt: '1 week ago',
author: 'John',
category: '🛠️ Development',
views: 400,
},
],
},
{
id: 'finance',
title: '💰 Finance',
description:
'Learn how to use Chatwoot effectively and make the most of its features to enhance customer support and engagement.',
articlesCount: '2',
articles: [
{
variant: 'Draft article',
title:
"How to get an SSL certificate for your Help Center's custom domain",
status: 'draft',
updatedAt: '2 days ago',
author: 'Michael',
category: '⚡️ Marketing',
views: 3400,
},
{
variant: 'Published article',
title: 'Setting up your first Help Center portal',
status: '',
updatedAt: '1 week ago',
author: 'John',
category: '🛠️ Development',
views: 400,
},
],
},
];
</script>
<template>
<Story title="Pages/HelpCenter/CategoryPage" :layout="{ type: 'single' }">
<Variant title="All Categories">
<div class="w-full min-h-screen bg-white dark:bg-slate-900">
<CategoriesPage :categories="categories" />
</div>
</Variant>
</Story>
</template>
@@ -1,101 +0,0 @@
<script setup>
import { ref, computed } from 'vue';
// import { OnClickOutside } from '@vueuse/components';
import HelpCenterLayout from 'dashboard/components-next/HelpCenter/HelpCenterLayout.vue';
import Button from 'dashboard/components-next/button/Button.vue';
import Breadcrumb from 'dashboard/components-next/breadcrumb/Breadcrumb.vue';
import CategoryList from 'dashboard/components-next/HelpCenter/Pages/CategoryPage/CategoryList.vue';
import ArticleList from 'dashboard/components-next/HelpCenter/Pages/ArticlePage/ArticleList.vue';
// import EditCategory from 'dashboard/playground/HelpCenter/components/EditCategory.vue';
const props = defineProps({
categories: {
type: Array,
required: true,
},
});
const selectedCategory = ref(null);
// const showEditCategory = ref(false);
// const openEditCategory = () => {
// showEditCategory.value = true;
// };
// const closeEditCategory = () => {
// showEditCategory.value = false;
// };
const breadcrumbItems = computed(() => {
const items = [{ label: 'Categories (en-US)', link: '#' }];
if (selectedCategory.value) {
items.push({
label: selectedCategory.value.title,
count: selectedCategory.value.articles.length,
});
}
return items;
});
const openCategoryArticles = id => {
selectedCategory.value = props.categories.find(
category => category.id === id
);
};
const resetCategory = () => {
selectedCategory.value = null;
};
const displayedArticles = computed(() => {
return selectedCategory.value ? selectedCategory.value.articles : [];
});
</script>
<!-- eslint-disable vue/no-bare-strings-in-template -->
<template>
<HelpCenterLayout :show-pagination-footer="false">
<template #header-actions>
<div class="flex items-center justify-between">
<div v-if="!selectedCategory" class="flex items-center gap-4">
<Button
label="English"
size="sm"
icon-position="right"
icon="chevron-lucide-down"
icon-lib="lucide"
variant="secondary"
/>
<div
class="w-px h-3.5 rounded my-auto bg-slate-75 dark:bg-slate-800"
/>
<span class="text-sm font-medium text-slate-800 dark:text-slate-100">
{{ categories.length }} categories
</span>
</div>
<Breadcrumb v-else :items="breadcrumbItems" @click="resetCategory" />
<Button
v-if="!selectedCategory"
label="New category"
icon="add"
size="sm"
/>
<div v-else class="relative">
<Button
label="Edit category"
variant="secondary"
size="sm"
@click="openEditCategory"
/>
<!-- <OnClickOutside @trigger="closeEditCategory">
<EditCategory v-if="showEditCategory" @close="closeEditCategory" />
</OnClickOutside> -->
</div>
</div>
</template>
<template #content>
<CategoryList
v-if="!selectedCategory"
:categories="categories"
@click="openCategoryArticles"
/>
<ArticleList v-else :articles="displayedArticles" />
</template>
</HelpCenterLayout>
</template>
@@ -1,30 +0,0 @@
<script setup>
import CategoryCard from 'dashboard/components-next/HelpCenter/CategoryCard/CategoryCard.vue';
defineProps({
categories: {
type: Array,
required: true,
},
});
const emit = defineEmits(['click']);
const handleClick = id => {
emit('click', id);
};
</script>
<template>
<ul role="list" class="grid w-full h-full grid-cols-1 gap-4 md:grid-cols-2">
<CategoryCard
v-for="category in categories"
:id="category.id"
:key="category.title"
:title="category.title"
:description="category.description"
:articles-count="category.articlesCount"
@click="handleClick(category.id)"
/>
</ul>
</template>
@@ -1,146 +0,0 @@
<script setup>
import { computed } from 'vue';
import { debounce } from '@chatwoot/utils';
import { ARTICLE_EDITOR_MENU_OPTIONS } from 'dashboard/constants/editor';
import HelpCenterLayout from 'dashboard/components-next/HelpCenter/HelpCenterLayout.vue';
import Button from 'dashboard/components-next/button/Button.vue';
import TextArea from 'dashboard/components-next/textarea/TextArea.vue';
import FullEditor from 'dashboard/components/widgets/WootWriter/FullEditor.vue';
const { article } = defineProps({
article: {
type: Object,
default: () => ({}),
},
});
const emit = defineEmits(['saveArticle']);
const saveArticle = debounce(value => emit('saveArticle', value), 400, false);
const articleTitle = computed({
get: () => article.title,
set: title => {
saveArticle({ title });
},
});
const articleContent = computed({
get: () => article.content,
set: content => {
saveArticle({ content });
},
});
</script>
<!-- eslint-disable vue/no-bare-strings-in-template -->
<template>
<HelpCenterLayout :show-header-title="false" :show-pagination-footer="false">
<template #header-actions>
<div class="flex items-center justify-between h-20">
<Button
label="Back to articles"
icon="chevron-lucide-left"
icon-lib="lucide"
variant="link"
text-variant="info"
size="sm"
/>
<div class="flex items-center gap-4">
<span class="text-xs font-medium text-slate-500 dark:text-slate-400">
Saved
</span>
<div class="flex items-center gap-2">
<Button label="Preview" variant="secondary" size="sm" />
<Button
label="Publish"
icon="chevron-lucide-down"
icon-position="right"
icon-lib="lucide"
size="sm"
/>
</div>
</div>
</div>
</template>
<template #content>
<div class="flex flex-col gap-3 pl-4 mb-3 rtl:pr-3 rtl:pl-0">
<TextArea
v-model="articleTitle"
class="h-12"
custom-text-area-class="border-0 !text-[32px] !bg-transparent !py-0 !px-0 !h-auto !leading-[48px] !font-medium !tracking-[0.2px]"
placeholder="Title"
/>
<div class="flex items-center gap-4">
<div class="flex items-center gap-2">
<div class="w-5 h-5 rounded-full bg-slate-100 dark:bg-slate-700" />
<span class="text-sm text-slate-500 dark:text-slate-400">
John Doe
</span>
</div>
<div class="w-px h-3 bg-slate-50 dark:bg-slate-800" />
<Button
label="Uncategorized"
icon="play-shape"
variant="ghost"
class="!px-2 font-normal"
text-variant="info"
/>
<div class="w-px h-3 bg-slate-50 dark:bg-slate-800" />
<Button
label="More properties"
icon="add"
variant="ghost"
class="!px-2 font-normal"
/>
</div>
</div>
<FullEditor
v-model="articleContent"
class="py-0 pb-10 pl-4 rtl:pr-4 rtl:pl-0 h-fit"
placeholder="Write something"
:enabled-menu-options="ARTICLE_EDITOR_MENU_OPTIONS"
/>
</template>
</HelpCenterLayout>
</template>
<style lang="scss" scoped>
::v-deep {
.ProseMirror .empty-node::before {
@apply text-slate-200 dark:text-slate-500 text-base;
}
.ProseMirror-menubar-wrapper {
.ProseMirror-woot-style {
@apply min-h-[15rem] max-h-full;
}
}
.ProseMirror-menubar {
display: none; // Hide by default
}
.editor-root .has-selection {
.ProseMirror-menubar {
@apply h-8 rounded-lg !px-2 z-50 bg-slate-50 dark:bg-slate-800 items-center gap-4 ml-0 mb-0 shadow-md border border-slate-75 dark:border-slate-700/50;
display: flex;
top: var(--selection-top, auto) !important;
left: var(--selection-left, 0) !important;
width: fit-content !important;
position: absolute !important;
.ProseMirror-menuitem {
@apply mr-0;
.ProseMirror-icon {
@apply p-0 mt-1 !mr-0;
svg {
width: 20px !important;
height: 20px !important;
}
}
}
}
}
}
</style>
@@ -1,23 +0,0 @@
<script setup>
import LocaleCard from 'dashboard/components-next/HelpCenter/LocaleCard/LocaleCard.vue';
defineProps({
locales: {
type: Array,
required: true,
},
});
</script>
<template>
<ul role="list" class="w-full h-full space-y-4">
<LocaleCard
v-for="(locale, index) in locales"
:key="index"
:locale="locale.name"
:is-default="locale.isDefault"
:article-count="locale.articleCount"
:category-count="locale.categoryCount"
/>
</ul>
</template>
@@ -1,52 +0,0 @@
<script setup>
import LocalesPage from './LocalesPage.vue';
const locales = [
{
name: 'English (en-US)',
isDefault: true,
articleCount: 5,
categoryCount: 5,
},
{
name: 'Spanish (es-ES)',
isDefault: false,
articleCount: 20,
categoryCount: 10,
},
{
name: 'English (en-UK)',
isDefault: false,
articleCount: 15,
categoryCount: 7,
},
{
name: 'Malay (ms-MY)',
isDefault: false,
articleCount: 15,
categoryCount: 7,
},
{
name: 'Malayalam (ml-IN)',
isDefault: false,
articleCount: 10,
categoryCount: 5,
},
{
name: 'Hindi (hi-IN)',
isDefault: false,
articleCount: 15,
categoryCount: 7,
},
];
</script>
<template>
<Story title="Pages/HelpCenter/LocalePage" :layout="{ type: 'single' }">
<Variant title="All Locales">
<div class="w-full min-h-screen bg-white dark:bg-slate-900">
<LocalesPage :locales="locales" />
</div>
</Variant>
</Story>
</template>
@@ -1,47 +0,0 @@
<script setup>
import { computed } from 'vue';
import HelpCenterLayout from 'dashboard/components-next/HelpCenter/HelpCenterLayout.vue';
import Button from 'dashboard/components-next/button/Button.vue';
import LocaleList from 'dashboard/components-next/HelpCenter/Pages/LocalePage/LocaleList.vue';
const props = defineProps({
locales: {
type: Array,
required: true,
},
});
const localeCount = computed(() => props.locales?.length);
// TODO: remove comments
// eslint-disable-next-line no-unused-vars
const handleTabChange = tab => {
// TODO: Implement tab change logic
};
// eslint-disable-next-line no-unused-vars
const handlePageChange = page => {
// TODO: Implement page change logic
};
</script>
<template>
<HelpCenterLayout :show-pagination-footer="false">
<template #header-actions>
<div class="flex items-center justify-between">
<div class="flex items-center gap-4">
<span class="text-sm font-medium text-slate-800 dark:text-slate-100">
{{ $t('HELP_CENTER.LOCALES_PAGE.LOCALES_COUNT', localeCount) }}
</span>
</div>
<Button
:label="$t('HELP_CENTER.LOCALES_PAGE.NEW_LOCALE_BUTTON_TEXT')"
icon="add"
size="sm"
/>
</div>
</template>
<template #content>
<LocaleList :locales="locales" />
</template>
</HelpCenterLayout>
</template>
@@ -1,13 +0,0 @@
<script setup>
import PortalSettings from './PortalSettings.vue';
</script>
<template>
<Story title="Pages/HelpCenter/PortalSettings" :layout="{ type: 'single' }">
<Variant title="Default">
<div class="w-[1000px] min-h-screen bg-white dark:bg-slate-900">
<PortalSettings />
</div>
</Variant>
</Story>
</template>
@@ -1,113 +0,0 @@
<script setup>
import HelpCenterLayout from 'dashboard/components-next/HelpCenter/HelpCenterLayout.vue';
import Button from 'dashboard/components-next/button/Button.vue';
import Input from 'dashboard/components-next/input/Input.vue';
import InlineInput from 'dashboard/components-next/inline-input/InlineInput.vue';
import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
const handleUploadAvatar = () => {};
</script>
<!-- TODO: Add i18n -->
<!-- eslint-disable vue/no-bare-strings-in-template -->
<template>
<HelpCenterLayout :show-pagination-footer="false">
<template #content>
<div class="flex flex-col w-full gap-10 max-w-[640px] pt-2 pb-8">
<div class="flex flex-col w-full gap-4">
<div class="flex flex-col w-full gap-2">
<label
class="mb-0.5 text-sm font-medium text-gray-900 dark:text-gray-50"
>
Avatar
</label>
<Avatar
label="Avatar"
src="https://api.dicebear.com/9.x/avataaars/svg?seed=Amaya"
class="bg-ruby-300 dark:bg-ruby-400"
@upload="handleUploadAvatar"
/>
</div>
<div class="flex flex-col w-full gap-2">
<div class="flex justify-between w-full h-10 gap-2 py-1">
<label
class="text-sm font-medium whitespace-nowrap min-w-[100px] text-slate-900 dark:text-slate-50"
>
Name
</label>
<Input placeholder="Name" class="w-[432px]" />
</div>
<div class="flex justify-between w-full h-10 gap-2 py-1">
<label
class="text-sm font-medium whitespace-nowrap min-w-[100px] text-slate-900 dark:text-slate-50"
>
Header text
</label>
<Input placeholder="Header text" class="w-[432px]" />
</div>
<div class="flex justify-between w-full h-10 gap-2 py-1">
<label
class="text-sm font-medium whitespace-nowrap min-w-[100px] text-slate-900 dark:text-slate-50"
>
Page title
</label>
<Input placeholder="Page title" class="w-[432px]" />
</div>
<div class="flex justify-between w-full h-10 gap-2 py-1">
<label
class="text-sm font-medium whitespace-nowrap min-w-[100px] text-slate-900 dark:text-slate-50"
>
Widget color
</label>
</div>
<div class="flex justify-end w-full gap-2 py-2">
<Button label="Save changes" size="sm" />
</div>
</div>
<div class="w-full h-px bg-slate-50 dark:bg-slate-800/50" />
</div>
<div class="flex flex-col w-full gap-6">
<div class="flex flex-col w-full gap-6">
<h6 class="text-base font-medium text-slate-900 dark:text-slate-50">
Configuration
</h6>
<div class="flex flex-col w-full gap-4">
<div class="flex justify-between w-full gap-2 py-1">
<InlineInput
placeholder="Slug"
label="Slug:"
custom-label-class="min-w-[100px]"
custom-input-class="!w-[430px]"
/>
</div>
<div class="flex justify-between w-full gap-2 py-1">
<InlineInput
placeholder="Custom domain"
label="Custom domain:"
custom-label-class="min-w-[100px]"
custom-input-class="!w-[430px]"
/>
</div>
<div class="flex justify-between w-full gap-2 py-1">
<InlineInput
placeholder="Home page link"
label="Home page link:"
custom-label-class="min-w-[100px]"
custom-input-class="!w-[430px]"
/>
</div>
</div>
</div>
<div class="flex justify-end w-full gap-3 py-4">
<Button label="Edit configuration" size="sm" variant="secondary" />
<Button
label="Delete Test-Help Center"
size="sm"
variant="destructive"
/>
</div>
</div>
</div>
</template>
</HelpCenterLayout>
</template>
@@ -1,44 +0,0 @@
<script setup>
import PortalSwitcher from './PortalSwitcher.vue';
const portals = [
{
id: 1,
name: 'Chatwoot Help Center',
articles: 67,
domain: 'chatwoot.help',
slug: 'help-center',
},
{
id: 2,
name: 'Chatwoot Handbook',
articles: 42,
domain: 'chatwoot.help',
slug: 'handbook',
},
{
id: 3,
name: 'Developer Documentation',
articles: 89,
domain: 'dev.chatwoot.com',
slug: 'docs',
},
];
</script>
<template>
<Story
title="Components/HelpCenter/PortalSwitcher"
:layout="{ type: 'grid', width: '510px' }"
>
<Variant title="Portal Switcher">
<div class="h-[500px] p-4 bg-slate-100 dark:bg-slate-900">
<PortalSwitcher
:portals="portals"
header="Choose a Portal"
description="Select from available help center portals"
/>
</div>
</Variant>
</Story>
</template>
@@ -1,111 +0,0 @@
<script setup>
import { ref } from 'vue';
import Button from 'dashboard/components-next/button/Button.vue';
defineProps({
portals: {
type: Array,
default: () => [
{
id: 1,
name: 'Chatwoot Help Center',
articles: 67,
domain: 'chatwoot.help',
slug: 'help-center',
},
{
id: 2,
name: 'Chatwoot Handbook',
articles: 42,
domain: 'chatwoot.help',
slug: 'handbook',
},
],
},
header: {
type: String,
default: 'Portals',
},
description: {
type: String,
default: 'Create and manage multiple portals',
},
});
const selectedPortal = ref(1);
const handlePortalChange = id => {
selectedPortal.value = id;
};
</script>
<!-- TODO: Add i18n -->
<!-- eslint-disable vue/no-bare-strings-in-template -->
<template>
<div
class="pt-5 pb-3 bg-white z-50 dark:bg-slate-800 absolute w-[440px] rounded-xl shadow-md flex flex-col gap-4"
>
<div class="flex items-center justify-between gap-4 px-6 pb-2">
<div class="flex flex-col gap-1">
<h2 class="text-base font-medium text-slate-900 dark:text-slate-50">
{{ header }}
</h2>
<p class="text-sm text-slate-600 dark:text-slate-300">
{{ description }}
</p>
</div>
<Button label="New portal" variant="secondary" icon="add" size="sm" />
</div>
<div v-if="portals.length > 0" class="flex flex-col gap-3">
<template v-for="(portal, index) in portals" :key="portal.id">
<div class="flex flex-col gap-2 px-6 py-2">
<div class="flex items-center justify-between">
<div class="flex items-center">
<input
:id="portal.id"
v-model="selectedPortal"
type="radio"
:value="portal.id"
class="mr-3"
@change="handlePortalChange(portal.id)"
/>
<label
:for="portal.id"
class="text-sm font-medium text-slate-900 dark:text-slate-100"
>
{{ portal.name }}
</label>
</div>
<div class="w-4 h-4 rounded-full bg-slate-100 dark:bg-slate-700" />
</div>
<div class="inline-flex items-center gap-2 py-1 text-sm">
<span class="text-slate-600 dark:text-slate-400">
articles:
<span class="text-slate-800 dark:text-slate-200">
{{ portal.articles }}
</span>
</span>
<div class="w-px h-3 bg-slate-50 dark:bg-slate-700" />
<span class="text-slate-600 dark:text-slate-400">
domain:
<span class="text-slate-800 dark:text-slate-200">
{{ portal.domain }}
</span>
</span>
<div class="w-px h-3 bg-slate-50 dark:bg-slate-700" />
<span class="text-slate-600 dark:text-slate-400">
slug:
<span class="text-slate-800 dark:text-slate-200">
{{ portal.slug }}
</span>
</span>
</div>
</div>
<div
v-if="index < portals.length - 1 && portals.length > 1"
class="w-full h-px bg-slate-50 dark:bg-slate-700/50"
/>
</template>
</div>
</div>
</template>
@@ -1,36 +0,0 @@
<script setup>
import Avatar from './Avatar.vue';
</script>
<template>
<Story title="Components/Avatar" :layout="{ type: 'grid', width: '400' }">
<Variant title="Default">
<div class="p-4 bg-white dark:bg-slate-900">
<Avatar
src="https://api.dicebear.com/9.x/avataaars/svg?seed=Amaya"
class="bg-ruby-300 dark:bg-ruby-900"
/>
</div>
</Variant>
<Variant title="Different Sizes">
<div class="flex flex-wrap gap-4 p-4 bg-white dark:bg-slate-900">
<Avatar
src="https://api.dicebear.com/9.x/avataaars/svg?seed=Felix"
:size="48"
class="bg-green-300 dark:bg-green-900"
/>
<Avatar
:size="72"
src="https://api.dicebear.com/9.x/avataaars/svg?seed=Jade"
class="bg-indigo-300 dark:bg-indigo-900"
/>
<Avatar
src="https://api.dicebear.com/9.x/avataaars/svg?seed=Emery"
:size="96"
class="bg-woot-300 dark:bg-woot-900"
/>
</div>
</Variant>
</Story>
</template>
@@ -1,51 +0,0 @@
<script setup>
import { computed } from 'vue';
import Icon from '../icon/Icon.vue';
const props = defineProps({
src: {
type: String,
default: '',
},
size: {
type: Number,
default: 72,
},
});
const emit = defineEmits(['upload']);
const avatarSize = computed(() => `${props.size}px`);
const iconSize = computed(() => `${props.size / 2}px`);
const handleUploadAvatar = () => {
emit('upload');
};
</script>
<template>
<div
class="relative flex flex-col items-center gap-2 select-none rounded-xl group/avatar"
:style="{
width: avatarSize,
height: avatarSize,
}"
>
<img
v-if="src"
:src="props.src"
alt="avatar"
class="w-full h-full shadow-sm rounded-xl"
/>
<div
class="absolute inset-0 flex items-center justify-center invisible w-full h-full transition-all duration-500 ease-in-out opacity-0 rounded-xl dark:bg-slate-900/50 bg-slate-900/20 group-hover/avatar:visible group-hover/avatar:opacity-100"
@click="handleUploadAvatar"
>
<Icon
icon="i-lucide-upload"
:size="iconSize"
class="text-white dark:text-white"
/>
</div>
</div>
</template>
@@ -1,63 +0,0 @@
<script setup>
import { ref } from 'vue';
import Breadcrumb from './Breadcrumb.vue';
const singleItem = ref([{ label: 'Home', link: '#' }]);
const twoItems = ref([
{ label: 'Home', link: '#' },
{ label: 'Categories', link: '#' },
]);
const threeItems = ref([
{ label: 'Home', link: '#' },
{ label: 'Categories', link: '#' },
{ label: 'Marketing', count: 6 },
]);
const longBreadcrumb = ref([
{ label: 'Home', link: '#' },
{ label: 'Categories', link: '#' },
{ label: 'Marketing', link: '#' },
{ label: 'Digital', link: '#' },
{ label: 'Social Media', count: 12 },
]);
</script>
<!-- eslint-disable vue/no-bare-strings-in-template -->
<!-- eslint-disable vue/no-undef-components -->
<template>
<Story
title="Components/Breadcrumb"
:layout="{ type: 'grid', width: '800px' }"
>
<Variant title="Single Item">
<div class="w-full p-4 bg-white dark:bg-slate-900">
<Breadcrumb :items="singleItem" />
</div>
</Variant>
<Variant title="Two Items">
<div class="w-full p-4 bg-white dark:bg-slate-900">
<Breadcrumb :items="twoItems" />
</div>
</Variant>
<Variant title="Three Items with Count">
<div class="w-full p-4 bg-white dark:bg-slate-900">
<Breadcrumb :items="threeItems" count-label="articles" />
</div>
</Variant>
<Variant title="Long Breadcrumb">
<div class="w-full p-4 bg-white dark:bg-slate-900">
<Breadcrumb :items="longBreadcrumb" count-label="articles" />
</div>
</Variant>
<Variant title="RTL Support">
<div dir="rtl">
<div class="w-full p-4 bg-white dark:bg-slate-900">
<Breadcrumb :items="threeItems" count-label="articles" />
</div>
</div>
</Variant>
</Story>
</template>
@@ -1,60 +0,0 @@
<script setup>
import { defineProps } from 'vue';
import { useI18n } from 'vue-i18n';
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
defineProps({
items: {
type: Array,
required: true,
validator: value => {
return value.every(
item =>
typeof item.label === 'string' &&
(item.link === undefined || typeof item.link === 'string') &&
(item.count === undefined || typeof item.count === 'number')
);
},
},
countLabel: {
type: String,
default: '',
},
});
const { t } = useI18n();
</script>
<template>
<nav :aria-label="t('BREADCRUMB.ARIA_LABEL')" class="flex items-center h-8">
<ol class="flex items-center mb-0">
<li
v-for="(item, index) in items"
:key="index"
class="flex items-center gap-3"
>
<template v-if="index === items.length - 1">
<span class="text-sm text-slate-900 dark:text-slate-50">
{{
`${item.label}${item.count ? ` (${item.count} ${countLabel})` : ''}`
}}
</span>
</template>
<a
v-else
:href="item.link"
class="text-sm transition-colors duration-200 text-slate-300 dark:text-slate-500 hover:text-slate-700 dark:hover:text-slate-100"
>
{{ item.label }}
</a>
<FluentIcon
v-if="index < items.length - 1"
icon="chevron-lucide-right"
size="18"
icon-lib="lucide"
class="flex-shrink-0 text-slate-300 dark:text-slate-500 ltr:mr-3 rtl:mr-0 rtl:ml-3"
/>
</li>
</ol>
</nav>
</template>
@@ -1,126 +0,0 @@
<script setup>
import Button from './Button.vue';
</script>
<template>
<Story title="Components/Button" :layout="{ type: 'grid', width: '400' }">
<Variant title="Default">
<div class="p-4 bg-white dark:bg-slate-900">
<Button label="Default Button" />
</div>
</Variant>
<Variant title="Disabled">
<div class="flex flex-wrap gap-2 p-4 bg-white dark:bg-slate-900">
<Button label="Disabled" disabled />
<Button label="Disabled" variant="outline" disabled />
<Button label="Disabled" disabled icon="delete" variant="outline" />
<Button
label="Disabled"
disabled
icon="delete"
variant="destructive"
size="sm"
/>
<Button
label="Disabled"
disabled
icon="delete"
variant="ghost"
size="sm"
/>
<Button
label="Disabled"
disabled
icon="delete"
variant="link"
size="sm"
/>
</div>
</Variant>
<Variant title="Disabled with icon">
<div class="p-4 bg-white dark:bg-slate-900">
<Button label="Disabled Button" icon="emoji-add" disabled />
</div>
</Variant>
<Variant title="Different variant">
<div class="flex flex-wrap gap-2 p-4 bg-white dark:bg-slate-900">
<Button label="Default" variant="default" />
<Button label="Destructive" variant="destructive" />
<Button label="Outline" variant="outline" />
<Button label="Secondary" variant="secondary" />
<Button label="Ghost" variant="ghost" />
<Button label="Link" variant="link" />
</div>
</Variant>
<Variant title="Different variant with icon only">
<div class="flex flex-wrap gap-2 p-4 bg-white dark:bg-slate-900">
<Button icon="emoji-add" variant="default" />
<Button icon="emoji-add" variant="destructive" />
<Button icon="emoji-add" variant="outline" />
<Button icon="emoji-add" variant="secondary" />
<Button icon="emoji-add" variant="ghost" />
<Button icon="emoji-add" variant="link" />
</div>
</Variant>
<Variant title="Different size">
<div class="flex flex-wrap gap-2 p-4 bg-white dark:bg-slate-900">
<Button label="Default" />
<Button label="Large" size="lg" />
<Button label="Small" size="sm" />
</div>
</Variant>
<Variant title="Different text variant">
<div class="flex flex-wrap gap-2 p-4 bg-white dark:bg-slate-900">
<Button label="Default" text-variant="default" variant="outline" />
<Button label="Success" text-variant="success" variant="outline" />
<Button label="Warning" text-variant="warning" variant="outline" />
<Button label="Danger" text-variant="danger" variant="outline" />
<Button label="Info" text-variant="info" variant="outline" />
</div>
</Variant>
<Variant title="Button with left icon with different sizes and icon only">
<div class="flex flex-wrap gap-2 p-4 bg-white dark:bg-slate-900">
<Button label="Default" icon="emoji-add" icon-position="left" />
<Button
label="Default LG"
icon="emoji-add"
icon-position="left"
size="lg"
/>
<Button
label="Default SM"
icon="emoji-add"
icon-position="left"
size="sm"
/>
<Button icon="emoji-add" size="icon" />
</div>
</Variant>
<Variant title="Button with right icon with different sizes and icon only">
<div class="flex flex-wrap gap-2 p-4 bg-white dark:bg-slate-900">
<Button label="Default" icon="emoji-add" icon-position="right" />
<Button
label="Default LG"
icon="emoji-add"
icon-position="right"
size="lg"
/>
<Button
label="Default SM"
icon="emoji-add"
icon-position="right"
size="sm"
/>
<Button icon="emoji-add" size="icon" />
</div>
</Variant>
</Story>
</template>
@@ -1,130 +0,0 @@
<script setup>
import { computed } from 'vue';
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
const props = defineProps({
label: {
type: String,
default: '',
},
variant: {
type: String,
default: 'default',
validator: value =>
[
'default',
'destructive',
'outline',
'secondary',
'ghost',
'link',
].includes(value),
},
textVariant: {
type: String,
default: '',
validator: value =>
['', 'default', 'success', 'warning', 'danger', 'info'].includes(value),
},
size: {
type: String,
default: 'default',
validator: value => ['default', 'sm', 'lg', 'icon'].includes(value),
},
icon: {
type: String,
default: '',
},
iconPosition: {
type: String,
default: 'left',
validator: value => ['left', 'right'].includes(value),
},
iconLib: {
type: String,
default: 'fluent',
},
});
const emit = defineEmits(['click']);
const buttonVariants = {
variant: {
default:
'bg-woot-500 dark:bg-woot-500 text-white dark:text-white hover:bg-woot-600 dark:hover:bg-woot-600',
destructive:
'bg-ruby-700 dark:bg-ruby-700 text-white dark:text-white hover:bg-ruby-800 dark:hover:bg-ruby-800',
outline:
'border border-slate-200 dark:border-slate-700/50 hover:border-slate-300 dark:hover:border-slate-600',
secondary:
'bg-slate-50 text-slate-900 dark:bg-slate-700/50 dark:text-slate-100 hover:bg-slate-100 dark:hover:bg-slate-600',
ghost:
'text-slate-900 dark:text-slate-200 hover:bg-slate-100 dark:hover:bg-slate-800',
link: 'text-woot-500 underline-offset-4 hover:underline dark:hover:underline',
},
size: {
default: 'h-10 px-4 py-2',
sm: 'h-8 px-3',
lg: 'h-11 px-4',
icon: 'h-auto w-auto px-2',
},
text: {
default:
'!text-woot-500 dark:!text-woot-500 hover:!text-woot-600 dark:hover:!text-woot-600',
success:
'!text-green-500 dark:!text-green-500 hover:!text-green-600 dark:hover:!text-green-600',
warning:
'!text-amber-600 dark:!text-amber-600 hover:!text-amber-600 dark:hover:!text-amber-600',
danger:
'!text-ruby-700 dark:!text-ruby-700 hover:!text-ruby-800 dark:hover:!text-ruby-800',
info: '!text-slate-500 dark:!text-slate-400 hover:!text-slate-600 dark:hover:!text-slate-500',
},
};
const buttonClasses = computed(() => {
const classes = [
buttonVariants.variant[props.variant],
buttonVariants.size[props.size],
];
if (props.textVariant && buttonVariants.text[props.textVariant]) {
classes.push(buttonVariants.text[props.textVariant]);
}
return classes.join(' ');
});
const iconSize = computed(() => {
if (props.size === 'sm') return 16;
if (props.size === 'lg') return 20;
return 18;
});
const handleClick = () => {
emit('click');
};
</script>
<template>
<button
:class="buttonClasses"
class="inline-flex items-center justify-center h-10 min-w-0 gap-2 text-sm font-medium transition-all duration-200 ease-in-out rounded-lg disabled:cursor-not-allowed disabled:pointer-events-none disabled:opacity-50"
@click="handleClick"
>
<FluentIcon
v-if="icon && iconPosition === 'left'"
:icon="icon"
:size="iconSize"
:icon-lib="iconLib"
class="flex-shrink-0"
/>
<span v-if="label" class="min-w-0 truncate">{{ label }}</span>
<FluentIcon
v-if="icon && iconPosition === 'right'"
:icon="icon"
:size="iconSize"
:icon-lib="iconLib"
class="flex-shrink-0"
/>
</button>
</template>
@@ -1,30 +0,0 @@
<script setup>
import { ref } from 'vue';
import ComboBox from './ComboBox.vue';
const options = [
{ value: 1, label: 'Option 1' },
{ value: 2, label: 'Option 2' },
{ value: 3, label: 'Option 3' },
{ value: 4, label: 'Option 4' },
{ value: 5, label: 'Option 5' },
];
const selectedValue = ref('');
</script>
<template>
<Story title="Components/ComboBox" :layout="{ type: 'grid', width: '300px' }">
<Variant title="Default">
<div class="w-full p-4 bg-white h-80 dark:bg-slate-900">
<ComboBox v-model="selectedValue" :options="options" />
<p class="mt-2">Selected value: {{ selectedValue }}</p>
</div>
</Variant>
<Variant title="Disabled">
<div class="w-full p-4 bg-white h-80 dark:bg-slate-900">
<ComboBox :options="options" disabled />
</div>
</Variant>
</Story>
</template>
@@ -1,163 +0,0 @@
<script setup>
import { nextTick, ref, computed, watch } from 'vue';
import { onClickOutside } from '@vueuse/core';
import { useI18n } from 'vue-i18n';
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
import Button from 'dashboard/components-next/button/Button.vue';
const props = defineProps({
options: {
type: Array,
required: true,
validator: value =>
value.every(option => 'value' in option && 'label' in option),
},
placeholder: {
type: String,
default: '',
},
modelValue: {
type: [String, Number],
default: '',
},
disabled: {
type: Boolean,
default: false,
},
searchPlaceholder: {
type: String,
default: '',
},
emptyState: {
type: String,
default: '',
},
});
const emit = defineEmits(['update:modelValue']);
const { t } = useI18n();
const selectedValue = ref(props.modelValue);
const open = ref(false);
const search = ref('');
const searchInput = ref(null);
const comboboxRef = ref(null);
const filteredOptions = computed(() => {
const searchTerm = search.value.toLowerCase();
return props.options.filter(option =>
option.label.toLowerCase().includes(searchTerm)
);
});
const selectPlaceholder = computed(() => {
return props.placeholder || t('COMBOBOX.PLACEHOLDER');
});
const selectedLabel = computed(() => {
const selected = props.options.find(
option => option.value === selectedValue.value
);
return selected?.label ?? selectPlaceholder.value;
});
const selectOption = option => {
selectedValue.value = option.value;
emit('update:modelValue', option.value);
open.value = false;
search.value = '';
};
const toggleDropdown = () => {
open.value = !open.value;
if (open.value) {
search.value = '';
nextTick(() => searchInput.value.focus());
}
};
watch(
() => props.modelValue,
newValue => {
selectedValue.value = newValue;
}
);
onClickOutside(comboboxRef, () => {
open.value = false;
});
</script>
<template>
<div
ref="comboboxRef"
class="relative w-full"
:class="{
'cursor-not-allowed': disabled,
'group/combobox': !disabled,
}"
>
<Button
variant="outline"
:label="selectedLabel"
icon-position="right"
size="sm"
:disabled="disabled"
class="justify-between w-full text-slate-900 dark:text-slate-100 group-hover/combobox:border-slate-300 dark:group-hover/combobox:border-slate-600"
:icon="open ? 'chevron-up' : 'chevron-down'"
@click="toggleDropdown"
/>
<div
v-show="open"
class="absolute z-50 w-full mt-1 transition-opacity duration-200 bg-white border rounded-md shadow-lg border-slate-200 dark:bg-slate-900 dark:border-slate-700/50"
>
<div class="relative border-b border-slate-100 dark:border-slate-700/50">
<FluentIcon
icon="search"
:size="14"
class="absolute text-gray-400 dark:text-slate-500 top-3 left-3"
aria-hidden="true"
/>
<input
ref="searchInput"
v-model="search"
type="search"
:placeholder="searchPlaceholder || t('COMBOBOX.SEARCH_PLACEHOLDER')"
class="w-full py-2 pl-10 pr-2 text-sm bg-white border-none rounded-t-md dark:bg-slate-900 text-slate-900 dark:text-slate-50"
/>
</div>
<ul
class="py-1 overflow-auto max-h-60"
role="listbox"
:aria-activedescendant="selectedValue"
>
<li
v-for="option in filteredOptions"
:key="option.value"
class="flex items-center justify-between w-full gap-2 px-3 py-2 text-sm transition-colors duration-150 cursor-pointer hover:bg-slate-50 dark:hover:bg-slate-800/50"
:class="{
'bg-slate-50 dark:bg-slate-800/50': option.value === selectedValue,
}"
role="option"
:aria-selected="option.value === selectedValue"
@click="selectOption(option)"
>
<span :class="{ 'font-medium': option.value === selectedValue }">
{{ option.label }}
</span>
<FluentIcon
v-if="option.value === selectedValue"
icon="checkmark"
:size="16"
class="flex-shrink-0"
aria-hidden="true"
/>
</li>
<li
v-if="filteredOptions.length === 0"
class="px-3 py-2 text-sm text-slate-600 dark:text-slate-300"
>
{{ emptyState || t('COMBOBOX.EMPTY_STATE') }}
</li>
</ul>
</div>
</div>
</template>
@@ -1,81 +0,0 @@
<script setup>
import { ref } from 'vue';
import Dialog from './Dialog.vue';
import Button from 'dashboard/components-next/button/Button.vue';
import Input from 'dashboard/components-next/input/Input.vue';
const alertDialog = ref(null);
const editDialog = ref(null);
const confirmDialog = ref(null);
const openAlertDialog = () => {
alertDialog.value.open();
};
const openEditDialog = () => {
editDialog.value.open();
};
const openConfirmDialog = () => {
confirmDialog.value.open();
};
// eslint-disable-next-line no-unused-vars
const onConfirm = dialog => {};
</script>
<template>
<Story title="Components/Dialog" :layout="{ type: 'grid', width: '100%' }">
<Variant title="Alert Dialog">
<Button label="Open Alert Dialog" @click="openAlertDialog" />
<Dialog
ref="alertDialog"
type="alert"
title="Alert"
description="This is an alert message."
/>
</Variant>
<Variant title="Edit Dialog">
<Button label="Open Edit Dialog" @click="openEditDialog" />
<Dialog
ref="editDialog"
type="edit"
description="You can create a new portal here, by providing a name and a slug."
title="Create Portal"
confirm-button-label="Save"
@confirm="onConfirm()"
>
<template #form>
<div class="flex flex-col gap-6">
<Input
id="portal-name"
type="text"
placeholder="User Guide | Chatwoot"
label="Name"
message="This will be the name of your public facing portal"
/>
<Input
id="portal-slug"
type="text"
placeholder="user-guide"
label="Slug"
message="app.chatwoot.com/hc/my-portal/en-US/categories/my-slug"
/>
</div>
</template>
</Dialog>
</Variant>
<Variant title="Confirm Dialog">
<Button label="Open Confirm Dialog" @click="openConfirmDialog" />
<Dialog
ref="confirmDialog"
type="confirm"
title="Confirm Action"
description="Are you sure you want to perform this action?"
confirm-button-label="Yes, I'm sure"
cancel-button-label="No, cancel"
@confirm="onConfirm()"
/>
</Variant>
</Story>
</template>
@@ -1,109 +0,0 @@
<script setup>
import { ref } from 'vue';
import { onClickOutside } from '@vueuse/core';
import { useI18n } from 'vue-i18n';
import Button from 'dashboard/components-next/button/Button.vue';
defineProps({
type: {
type: String,
default: 'edit',
validator: value => ['alert', 'edit'].includes(value),
},
title: {
type: String,
required: true,
},
description: {
type: String,
default: '',
},
cancelButtonLabel: {
type: String,
default: '',
},
confirmButtonLabel: {
type: String,
default: '',
},
});
const emit = defineEmits(['confirm']);
const { t } = useI18n();
const dialogRef = ref(null);
const dialogContentRef = ref(null);
const open = () => {
dialogRef.value?.showModal();
};
const close = () => {
dialogRef.value?.close();
};
const confirm = () => {
emit('confirm');
};
defineExpose({ open });
onClickOutside(dialogContentRef, event => {
if (
dialogRef.value &&
dialogRef.value.open &&
event.target === dialogRef.value
) {
close();
}
});
</script>
<template>
<Teleport to="body">
<dialog
ref="dialogRef"
class="w-full max-w-lg overflow-visible shadow-xl bg-modal-backdrop-light dark:bg-modal-backdrop-dark rounded-xl"
@close="close"
>
<div
ref="dialogContentRef"
class="flex flex-col w-full h-auto gap-6 p-6 overflow-visible text-left align-middle transition-all duration-300 ease-in-out transform bg-white shadow-xl dark:bg-slate-800 rounded-xl"
@click.stop
>
<div class="flex flex-col gap-2">
<h3
class="text-base font-medium leading-6 text-gray-900 dark:text-white"
>
{{ title }}
</h3>
<p
v-if="description"
class="mb-0 text-sm text-slate-500 dark:text-slate-400"
>
{{ description }}
</p>
</div>
<slot name="form">
<!-- Form content will be injected here -->
</slot>
<div class="flex items-center justify-between w-full gap-3">
<Button
variant="secondary"
:label="cancelButtonLabel || t('DIALOG.BUTTONS.CANCEL')"
class="w-full"
size="sm"
@click="close"
/>
<Button
v-if="type !== 'alert'"
:variant="type === 'edit' ? 'default' : 'destructive'"
:label="confirmButtonLabel || t('DIALOG.BUTTONS.CONFIRM')"
class="w-full"
size="sm"
@click="confirm"
/>
</div>
</div>
</dialog>
</Teleport>
</template>
@@ -1,55 +0,0 @@
<script setup>
import { ref } from 'vue';
import DropdownMenu from './DropdownMenu.vue';
import Button from 'dashboard/components-next/button/Button.vue';
const menuItems = [
{ label: 'Edit', action: 'edit', icon: 'edit' },
{ label: 'Publish', action: 'publish', icon: 'checkmark' },
{ label: 'Archive', action: 'archive', icon: 'archive' },
{ label: 'Delete', action: 'delete', icon: 'delete' },
];
const isOpen = ref(false);
const toggleDropdown = () => {
isOpen.value = !isOpen.value;
};
const handleAction = () => {
isOpen.value = false;
};
</script>
<template>
<Story title="Components/DropdownMenu" :layout="{ type: 'grid', width: 300 }">
<Variant title="Default">
<div class="p-4 bg-white h-72 dark:bg-slate-900">
<div class="relative">
<Button label="Open Menu" size="sm" @click="toggleDropdown" />
<DropdownMenu
v-if="isOpen"
:menu-items="menuItems"
class="absolute left-0 top-10"
@action="handleAction"
/>
</div>
</div>
</Variant>
<Variant title="Always Open">
<div class="p-4 bg-white h-72 dark:bg-slate-900">
<DropdownMenu :menu-items="menuItems" @action="handleAction" />
</div>
</Variant>
<Variant title="Custom Items">
<div class="p-4 bg-white h-72 dark:bg-slate-900">
<DropdownMenu
:menu-items="[
{ label: 'Custom 1', action: 'custom1', icon: 'file-upload' },
{ label: 'Custom 2', action: 'custom2', icon: 'document' },
{ label: 'Danger', action: 'delete', icon: 'delete' },
]"
@action="handleAction"
/>
</div>
</Variant>
</Story>
</template>
@@ -1,35 +0,0 @@
<script setup>
import { defineProps, defineEmits } from 'vue';
import Button from 'dashboard/components-next/button/Button.vue';
defineProps({
menuItems: {
type: Array,
required: true,
},
});
const emit = defineEmits(['action']);
const handleAction = action => {
emit('action', action);
};
</script>
<template>
<div
class="bg-white dark:bg-slate-800 absolute rounded-xl z-50 py-3 px-1 gap-2 flex flex-col min-w-[136px] shadow-lg"
>
<Button
v-for="item in menuItems"
:key="item.action"
:label="item.label"
:icon="item.icon"
variant="ghost"
size="sm"
class="!justify-start w-full hover:bg-white dark:hover:bg-slate-800 z-60 font-normal"
:text-variant="item.action === 'delete' ? 'danger' : ''"
@click="handleAction(item.action)"
/>
</div>
</template>
@@ -1,17 +0,0 @@
<script setup>
import { h, isVNode } from 'vue';
const props = defineProps({
icon: { type: [String, Object, Function], required: true },
});
const renderIcon = () => {
if (!props.icon) return null;
if (typeof props.icon === 'function' || isVNode(props.icon)) {
return props.icon;
}
return h('span', { class: props.icon });
};
</script>
<template>
<component :is="renderIcon" />
</template>
@@ -1,82 +0,0 @@
<script setup>
import InlineInput from './InlineInput.vue';
</script>
<template>
<Story
title="Components/InlineInput"
:layout="{ type: 'grid', width: '400' }"
>
<Variant title="Default">
<div class="p-4 bg-white dark:bg-slate-800">
<InlineInput id="inline-input-1" placeholder="Default InlineInput" />
</div>
</Variant>
<Variant title="With Label">
<div class="p-4 bg-white dark:bg-slate-800">
<InlineInput
id="inline-input-2"
label="Username"
placeholder="Enter your username"
/>
</div>
</Variant>
<Variant title="Disabled">
<div class="p-4 bg-white dark:bg-slate-800">
<InlineInput
id="inline-input-3"
label="Disabled InlineInput"
placeholder="Can't type here"
disabled
/>
</div>
</Variant>
<Variant title="With Custom Classes">
<div class="flex flex-col gap-4 p-4 bg-white dark:bg-slate-800">
<InlineInput
id="inline-input-4"
label="Custom Input Class"
placeholder="Custom input style"
custom-input-class="placeholder:text-green-200 dark:placeholder:text-green-800"
/>
<InlineInput
id="inline-input-5"
label="Custom Label Class"
placeholder="Custom label style"
custom-label-class="text-green-600 dark:text-green-400"
/>
</div>
</Variant>
<Variant title="Different Types">
<div class="flex flex-col gap-4 p-4 bg-white dark:bg-slate-800">
<InlineInput
id="inline-input-6"
label="Text"
placeholder="Text input"
/>
<InlineInput
id="inline-input-7"
label="Number"
placeholder="Number input"
type="number"
/>
<InlineInput
id="inline-input-8"
label="Password"
placeholder="Password input"
type="password"
/>
<InlineInput
id="inline-input-9"
label="Email"
placeholder="Email input"
type="email"
/>
</div>
</Variant>
</Story>
</template>
@@ -1,65 +0,0 @@
<script setup>
defineProps({
modelValue: {
type: [String, Number],
default: '',
},
type: {
type: String,
default: 'text',
},
customInputClass: {
type: [String, Object, Array],
default: '',
},
customLabelClass: {
type: [String, Object, Array],
default: '',
},
placeholder: {
type: String,
default: '',
},
label: {
type: String,
default: '',
},
id: {
type: String,
default: '',
},
disabled: {
type: Boolean,
default: false,
},
});
defineEmits(['update:modelValue']);
</script>
<template>
<div
class="relative flex items-center justify-between w-full gap-2 whitespace-nowrap"
>
<label
v-if="label"
:for="id"
:class="customLabelClass"
class="mb-0.5 text-sm font-medium text-gray-900 dark:text-gray-50"
>
{{ label }}
</label>
<!-- Added prefix slot to allow adding custom labels to the input -->
<slot name="prefix" />
<input
:id="id"
:value="modelValue"
:type="type"
:placeholder="placeholder"
:disabled="disabled"
:class="customInputClass"
class="flex w-full reset-base text-sm h-6 !mb-0 border-0 rounded-lg bg-transparent dark:bg-transparent placeholder:text-slate-200 dark:placeholder:text-slate-500 disabled:cursor-not-allowed disabled:opacity-50 text-slate-900 dark:text-white transition-all duration-500 ease-in-out"
@input="$emit('update:modelValue', $event.target.value)"
/>
</div>
</template>
@@ -1,68 +0,0 @@
<script setup>
import Input from './Input.vue';
</script>
<template>
<Story title="Components/Input" :layout="{ type: 'grid', width: '400' }">
<Variant title="Default">
<div class="p-4 bg-white dark:bg-slate-800">
<Input placeholder="Default Input" />
</div>
</Variant>
<Variant title="With Label">
<div class="p-4 bg-white dark:bg-slate-800">
<Input label="Username" placeholder="Enter your username" />
</div>
</Variant>
<Variant title="Disabled">
<div class="p-4 bg-white dark:bg-slate-800">
<Input label="Disabled Input" placeholder="Can't type here" disabled />
</div>
</Variant>
<Variant title="With Message">
<div class="flex flex-col gap-4 p-4 bg-white dark:bg-slate-800">
<Input
label="Email"
placeholder="Enter your email"
message="We'll never share your email."
/>
<Input
label="Password"
type="password"
placeholder="Enter your password"
message="Password is incorrect"
message-type="error"
/>
<Input
label="Verification Code"
placeholder="Enter the code"
message="Code verified successfully"
message-type="success"
/>
</div>
</Variant>
<Variant title="Different Types">
<div class="flex flex-col gap-4 p-4 bg-white dark:bg-slate-800">
<Input label="Text" type="text" placeholder="Text input" />
<Input label="Number" type="number" placeholder="Number input" />
<Input label="Password" type="password" placeholder="Password input" />
<Input label="Email" type="email" placeholder="Email input" />
<Input label="Date" type="date" />
</div>
</Variant>
<Variant title="Custom Input Class">
<div class="p-4 bg-white dark:bg-slate-800">
<Input
label="Custom Style"
placeholder="Custom input class"
custom-input-class="border-yellow-500 dark:border-yellow-700"
/>
</div>
</Variant>
</Story>
</template>
@@ -1,92 +0,0 @@
<script setup>
import { computed } from 'vue';
const props = defineProps({
modelValue: {
type: [String, Number],
default: '',
},
type: {
type: String,
default: 'text',
},
customInputClass: {
type: [String, Object, Array],
default: '',
},
placeholder: {
type: String,
default: '',
},
label: {
type: String,
default: '',
},
id: {
type: String,
default: '',
},
message: {
type: String,
default: '',
},
disabled: {
type: Boolean,
default: false,
},
messageType: {
type: String,
default: 'info',
validator: value => ['info', 'error', 'success'].includes(value),
},
});
defineEmits(['update:modelValue']);
const messageClass = computed(() => {
switch (props.messageType) {
case 'error':
return 'text-red-500 dark:text-red-400';
case 'success':
return 'text-green-500 dark:text-green-400';
default:
return 'text-slate-500 dark:text-slate-400';
}
});
const inputBorderClass = computed(() => {
switch (props.messageType) {
case 'error':
return 'border-red-500 dark:border-red-400';
default:
return 'border-slate-100 dark:border-slate-700/50';
}
});
</script>
<template>
<div class="relative flex flex-col gap-1">
<label
v-if="label"
:for="id"
class="mb-0.5 text-sm font-medium text-gray-900 dark:text-gray-50"
>
{{ label }}
</label>
<!-- Added prefix slot to allow adding icons to the input -->
<slot name="prefix" />
<input
:id="id"
:value="modelValue"
:class="[customInputClass, inputBorderClass]"
:type="type"
:placeholder="placeholder"
:disabled="disabled"
class="flex w-full reset-base text-sm h-8 pl-3 pr-2 rtl:pr-3 rtl:pl-2 py-1.5 !mb-0 border rounded-lg focus:border-woot-500 dark:focus:border-woot-600 bg-white dark:bg-slate-900 file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-slate-200 dark:placeholder:text-slate-500 disabled:cursor-not-allowed disabled:opacity-50 text-slate-900 dark:text-white transition-all duration-500 ease-in-out"
@input="$emit('update:modelValue', $event.target.value)"
/>
<p
v-if="message"
class="mt-1 mb-0 text-xs transition-all duration-500 ease-in-out"
:class="messageClass"
>
{{ message }}
</p>
</div>
</template>
@@ -1,80 +0,0 @@
<script setup>
import { ref } from 'vue';
import PaginationFooter from './PaginationFooter.vue';
const createPaginationState = (initialPage, totalItems, itemsPerPage) => {
const currentPage = ref(initialPage);
const handlePageChange = newPage => {
currentPage.value = newPage;
};
return { currentPage, totalItems, itemsPerPage, handlePageChange };
};
const defaultState = createPaginationState(1, 100, 16);
const middlePageState = createPaginationState(3, 100, 16);
const lastPageState = createPaginationState(7, 100, 16);
const customItemsState = createPaginationState(2, 100, 10);
const singlePageState = createPaginationState(1, 10, 16);
</script>
<template>
<Story
title="Components/PaginationFooter"
:layout="{ type: 'grid', width: '957' }"
>
<Variant title="Default">
<div class="p-4 bg-white dark:bg-slate-900">
<PaginationFooter
:current-page="defaultState.currentPage.value"
:total-items="defaultState.totalItems"
:items-per-page="defaultState.itemsPerPage"
@update:current-page="defaultState.handlePageChange"
/>
</div>
</Variant>
<Variant title="Middle Page">
<div class="p-4 bg-white dark:bg-slate-900">
<PaginationFooter
:current-page="middlePageState.currentPage.value"
:total-items="middlePageState.totalItems"
:items-per-page="middlePageState.itemsPerPage"
@update:current-page="middlePageState.handlePageChange"
/>
</div>
</Variant>
<Variant title="Last Page">
<div class="p-4 bg-white dark:bg-slate-900">
<PaginationFooter
:current-page="lastPageState.currentPage.value"
:total-items="lastPageState.totalItems"
:items-per-page="lastPageState.itemsPerPage"
@update:current-page="lastPageState.handlePageChange"
/>
</div>
</Variant>
<Variant title="Custom Items Per Page">
<div class="p-4 bg-white dark:bg-slate-900">
<PaginationFooter
:current-page="customItemsState.currentPage.value"
:total-items="customItemsState.totalItems"
:items-per-page="customItemsState.itemsPerPage"
@update:current-page="customItemsState.handlePageChange"
/>
</div>
</Variant>
<Variant title="Single Page">
<div class="p-4 bg-white dark:bg-slate-900">
<PaginationFooter
:current-page="singlePageState.currentPage.value"
:total-items="singlePageState.totalItems"
:items-per-page="singlePageState.itemsPerPage"
@update:current-page="singlePageState.handlePageChange"
/>
</div>
</Variant>
</Story>
</template>
@@ -1,113 +0,0 @@
<script setup>
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import Button from 'dashboard/components-next/button/Button.vue';
const props = defineProps({
currentPage: {
type: Number,
required: true,
},
totalItems: {
type: Number,
required: true,
},
itemsPerPage: {
type: Number,
default: 16,
},
});
const emit = defineEmits(['update:currentPage']);
const { t } = useI18n();
const totalPages = computed(() =>
Math.ceil(props.totalItems / props.itemsPerPage)
);
const startItem = computed(
() => (props.currentPage - 1) * props.itemsPerPage + 1
);
const endItem = computed(() =>
Math.min(startItem.value + props.itemsPerPage - 1, props.totalItems)
);
const isFirstPage = computed(() => props.currentPage === 1);
const isLastPage = computed(() => props.currentPage === totalPages.value);
const changePage = newPage => {
if (newPage >= 1 && newPage <= totalPages.value) {
emit('update:currentPage', newPage);
}
};
const currentPageInformation = computed(() => {
return t('PAGINATION_FOOTER.SHOWING', {
startItem: startItem.value,
endItem: endItem.value,
totalItems: props.totalItems,
});
});
const pageInfo = computed(() => {
return t('PAGINATION_FOOTER.CURRENT_PAGE_INFO', {
currentPage: '',
totalPages: totalPages.value,
});
});
</script>
<template>
<div
class="flex justify-between h-12 w-full max-w-[957px] mx-auto bg-slate-25 dark:bg-slate-800/50 rounded-xl py-2 px-3 items-center"
>
<div class="flex items-center gap-3">
<span
class="min-w-0 text-sm font-normal line-clamp-1 text-slate-600 dark:text-slate-300"
>
{{ currentPageInformation }}
</span>
</div>
<div class="flex items-center gap-2">
<Button
icon="chevrons-lucide-left"
icon-lib="lucide"
variant="ghost"
size="sm"
:disabled="isFirstPage"
@click="changePage(1)"
/>
<Button
icon="chevron-lucide-left"
icon-lib="lucide"
variant="ghost"
size="sm"
:disabled="isFirstPage"
@click="changePage(currentPage - 1)"
/>
<div
class="inline-flex items-center gap-2 text-sm text-slate-500 dark:text-slate-400"
>
<span
class="px-3 tabular-nums py-0.5 bg-white dark:bg-slate-900 rounded-md"
>
{{ currentPage }}
</span>
<span>{{ pageInfo }}</span>
</div>
<Button
icon="chevron-lucide-right"
icon-lib="lucide"
variant="ghost"
size="sm"
:disabled="isLastPage"
@click="changePage(currentPage + 1)"
/>
<Button
icon="chevrons-lucide-right"
icon-lib="lucide"
variant="ghost"
size="sm"
:disabled="isLastPage"
@click="changePage(totalPages)"
/>
</div>
</div>
</template>
@@ -1,15 +0,0 @@
<script setup>
import Spinner from './Spinner.vue';
</script>
<template>
<Story title="Components/Spinner" :layout="{ type: 'grid', width: '400' }">
<Variant title="Default">
<Spinner size="24" class="text-n-slate-10" />
<Spinner size="32" class="text-n-ruby-10" />
<Spinner size="40" class="text-n-amber-10" />
<Spinner size="48" class="text-n-teal-10" />
<Spinner size="64" class="text-n-brand" />
</Variant>
</Story>
</template>
@@ -1,25 +0,0 @@
<script setup>
defineProps({
size: {
type: Number,
default: 24,
},
});
</script>
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
:width="size"
:height="size"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="8"
strokeLinecap="round"
strokeLinejoin="round"
class="animate-spin"
>
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
</svg>
</template>
@@ -1,61 +0,0 @@
<script setup>
import TabBar from './TabBar.vue';
const defaultTabs = [
{ label: 'All articles', count: 24 },
{ label: 'Mine', count: 13 },
{ label: 'Draft', count: 5 },
{ label: 'Archived', count: 11 },
{ label: 'Unpublished', count: 23 },
{ label: 'Published', count: 100 },
{ label: 'Scheduled', count: 49 },
];
const customTabs = [
{ label: 'Active', count: 10 },
{ label: 'Pending', count: 5 },
{ label: 'Completed', count: 15 },
];
const handleTabChanged = tab => {
console.log('Tab changed:', tab);
};
</script>
<template>
<Story title="Components/TabBar" :layout="{ type: 'grid', width: '920px' }">
<Variant title="Default">
<div class="p-4 bg-white dark:bg-slate-900">
<TabBar :tabs="defaultTabs" @tab-changed="handleTabChanged" />
</div>
</Variant>
<Variant title="Custom Tabs with Initial Tab">
<div class="p-4 bg-white dark:bg-slate-900">
<TabBar
:tabs="customTabs"
:initial-active-tab="1"
@tab-changed="handleTabChanged"
/>
</div>
</Variant>
<Variant title="No Counts">
<div class="p-4 bg-white dark:bg-slate-900">
<TabBar
:tabs="[{ label: 'Tab 1' }, { label: 'Tab 2' }, { label: 'Tab 3' }]"
@tab-changed="handleTabChanged"
/>
</div>
</Variant>
<Variant title="Single Tab">
<div class="p-4 bg-white dark:bg-slate-900">
<TabBar
:tabs="[{ label: 'Single Tab', count: 42 }]"
@tab-changed="handleTabChanged"
/>
</div>
</Variant>
</Story>
</template>
@@ -1,58 +0,0 @@
<script setup>
import { ref } from 'vue';
const props = defineProps({
initialActiveTab: {
type: Number,
default: 0,
},
tabs: {
type: Array,
required: true,
validator: value => {
return value.every(
tab => typeof tab.label === 'string' && typeof tab.count === 'number'
);
},
},
});
const emit = defineEmits(['tabChanged']);
const activeTab = ref(props.initialActiveTab);
const selectTab = index => {
activeTab.value = index;
emit('tabChanged', props.tabs[index]);
};
const showDivider = index => {
return (
// Show dividers after the active tab, but not after the last tab
(index > activeTab.value && index < props.tabs.length - 1) ||
// Show dividers before the active tab, but not immediately before it and not before the first tab
(index < activeTab.value - 1 && index > -1)
);
};
</script>
<template>
<div class="flex h-8 rounded-lg bg-slate-25 dark:bg-slate-800/50 w-fit">
<template v-for="(tab, index) in tabs" :key="index">
<button
class="relative px-4 truncate py-1.5 text-sm border-0 rounded-lg transition-colors duration-300 ease-in-out"
:class="[
activeTab === index
? 'text-woot-500 bg-woot-500/10 dark:bg-woot-500/10'
: 'text-slate-500 dark:text-slate-400 hover:text-woot-500 dark:hover:text-woot-400',
]"
@click="selectTab(index)"
>
{{ tab.label }} {{ tab.count ? `(${tab.count})` : '' }}
</button>
<div
class="w-px h-3.5 rounded my-auto transition-colors duration-300 ease-in-out"
:class="
showDivider(index)
? 'bg-slate-75 dark:bg-slate-800'
: 'bg-transparent dark:bg-transparent'
"
/>
</template>
</div>
</template>
@@ -1,67 +0,0 @@
<script setup>
import { ref } from 'vue';
import TextArea from './TextArea.vue';
const bio = ref('');
const description = ref('');
</script>
<template>
<Story title="Components/TextArea" :layout="{ type: 'grid', width: '400' }">
<Variant title="Default">
<div class="p-4 bg-white dark:bg-slate-800">
<TextArea placeholder="Default TextArea" />
</div>
</Variant>
<Variant title="With Label">
<div class="p-4 bg-white dark:bg-slate-800">
<TextArea label="Description" placeholder="Enter your description" />
</div>
</Variant>
<Variant title="Disabled">
<div class="p-4 bg-white dark:bg-slate-800">
<TextArea
label="Disabled TextArea"
placeholder="Can't type here"
disabled
/>
</div>
</Variant>
<Variant title="With Character Count">
<div class="p-4 bg-white dark:bg-slate-800">
<TextArea
v-model="bio"
label="Bio"
placeholder="Tell us about yourself"
:max-length="100"
show-character-count
/>
</div>
</Variant>
<Variant title="Custom Max Length">
<div class="p-4 bg-white dark:bg-slate-800">
<TextArea
v-model="description"
label="Long Description"
placeholder="Enter a longer description"
:max-length="500"
show-character-count
/>
</div>
</Variant>
<Variant title="Custom TextArea Class">
<div class="p-4 bg-white dark:bg-slate-800">
<TextArea
label="Custom Style"
placeholder="Custom textarea class"
custom-text-area-class="border-yellow-500 dark:border-yellow-700"
/>
</div>
</Variant>
</Story>
</template>
@@ -1,69 +0,0 @@
<script setup>
import { computed } from 'vue';
const props = defineProps({
modelValue: {
type: String,
default: '',
},
label: {
type: String,
default: '',
},
placeholder: {
type: String,
default: '',
},
maxLength: {
type: Number,
default: 200,
},
id: {
type: String,
default: '',
},
disabled: {
type: Boolean,
default: false,
},
customTextAreaClass: {
type: String,
default: '',
},
showCharacterCount: {
type: Boolean,
default: false,
},
});
defineEmits(['update:modelValue']);
const characterCount = computed(() => props.modelValue.length);
</script>
<template>
<div class="relative flex flex-col gap-1">
<label
v-if="label"
:for="id"
class="mb-0.5 text-sm font-medium text-gray-900 dark:text-gray-50"
>
{{ label }}
</label>
<textarea
:id="id"
:value="modelValue"
:placeholder="placeholder"
:maxlength="maxLength"
class="flex w-full reset-base text-sm h-24 px-3 pt-3 !mb-0 border rounded-lg focus:border-woot-500 dark:focus:border-woot-600 bg-white dark:bg-slate-900 placeholder:text-slate-200 dark:placeholder:text-slate-500 text-slate-900 dark:text-white transition-all duration-500 ease-in-out resize-none disabled:cursor-not-allowed disabled:opacity-50 disabled:bg-slate-25 dark:disabled:bg-slate-900"
:class="[customTextAreaClass, showCharacterCount ? 'pb-9' : 'pb-3']"
:disabled="disabled"
@input="$emit('update:modelValue', $event.target.value)"
/>
<div
v-if="showCharacterCount"
class="absolute flex items-center justify-between mt-1 bottom-3 ltr:right-3 rtl:left-3"
>
<span class="text-xs tabular-nums text-slate-300 dark:text-slate-600">
{{ characterCount }} / {{ maxLength }}
</span>
</div>
</div>
</template>
@@ -2,7 +2,6 @@
import { mapGetters } from 'vuex';
import { getSidebarItems } from './config/default-sidebar';
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
import { useAccount } from 'dashboard/composables/useAccount';
import { useRoute, useRouter } from 'vue-router';
import PrimarySidebar from './sidebarComponents/Primary.vue';
@@ -34,7 +33,6 @@ export default {
setup(props, { emit }) {
const route = useRoute();
const router = useRouter();
const { accountId } = useAccount();
const toggleKeyShortcutModal = () => {
emit('openKeyShortcutModal');
@@ -74,7 +72,6 @@ export default {
return {
toggleKeyShortcutModal,
accountId,
};
},
data() {
@@ -85,6 +82,7 @@ export default {
computed: {
...mapGetters({
accountId: 'getCurrentAccountId',
currentUser: 'getCurrentUser',
globalConfig: 'globalConfig/get',
inboxes: 'inboxes/getInboxes',
@@ -122,6 +120,7 @@ export default {
this.currentUser,
this.accountId
);
const menuItems = this.sideMenuConfig.primaryMenu;
return menuItems.filter(menuItem => {
const isAvailableForTheUser = hasPermissions(
@@ -132,6 +131,7 @@ export default {
if (!isAvailableForTheUser) {
return false;
}
if (
menuItem.alwaysVisibleOnChatwootInstances &&
!this.isACustomBrandedInstance
@@ -139,10 +139,12 @@ export default {
return true;
}
if (menuItem.featureFlag) {
return this.isFeatureEnabledonAccount(
const isEnabled = this.isFeatureEnabledonAccount(
this.accountId,
menuItem.featureFlag
);
return isEnabled;
}
return true;
});
@@ -50,6 +50,9 @@ export default {
showOptionsMenu: false,
};
},
mounted() {
console.log('MENU ITEMS', this.menuItems);
},
methods: {
frontendURL,
toggleOptions() {
@@ -90,17 +93,13 @@ export default {
/>
</div>
<div class="flex flex-col items-center justify-end pb-6">
<a
<PrimaryNavItem
v-if="!isACustomBrandedInstance"
v-tooltip.right="$t(`SIDEBAR.DOCS`)"
:href="helpDocsURL"
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"
rel="noopener noreferrer nofollow"
target="_blank"
>
<fluent-icon icon="book-open-globe" />
<span class="sr-only">{{ $t(`SIDEBAR.DOCS`) }}</span>
</a>
icon="book-open-globe"
name="DOCS"
open-in-new-page
:to="helpDocsURL"
/>
<NotificationBell @open-notification-panel="openNotificationPanel" />
<AgentDetails @toggle-menu="toggleOptions" />
<OptionsMenu
@@ -26,6 +26,13 @@ export default {
default: false,
},
},
mounted() {
console.log(
'MOUNTED: PRIMARY NAV ITEM',
this.name,
this.$t(`SIDEBAR.${this.name}`)
);
},
};
</script>
@@ -48,7 +48,7 @@ const debouncedEmit = debounce(value => {
const onSearch = value => {
searchTerm.value = value;
debouncedEmit(value);
debouncedEmit();
};
const filteredListItems = computed(() => {
@@ -59,7 +59,6 @@ export default {
data() {
return {
plugins: [imagePastePlugin(this.handleImageUpload)],
isTextSelected: false, // Tracks text selection and prevents unnecessary re-renders on mouse selection
};
},
watch: {
@@ -182,7 +181,6 @@ export default {
if (tx.docChanged) {
this.emitOnChange();
}
this.checkSelection(state);
},
handleDOMEvents: {
keyup: this.onKeyup,
@@ -228,56 +226,6 @@ export default {
onFocus() {
this.$emit('focus');
},
checkSelection(editorState) {
const { from, to } = editorState.selection;
// Check if there's a selection (from and to are different)
const hasSelection = from !== to;
// If the selection state is the same as the previous state, do nothing
if (hasSelection === this.isTextSelected) return;
// Update the selection state
this.isTextSelected = hasSelection;
const { editor } = this.$refs;
// Toggle the 'has-selection' class based on whether there's a selection
editor.classList.toggle('has-selection', hasSelection);
// If there's a selection, update the menubar position
if (hasSelection) this.setMenubarPosition(editorState);
},
setMenubarPosition(editorState) {
if (!editorState.selection) return;
// Get the start and end positions of the selection
const { from, to } = editorState.selection;
const { editor } = this.$refs;
// Get the editor's position relative to the viewport
const { left: editorLeft, top: editorTop } =
editor.getBoundingClientRect();
// Get the editor's width
const editorWidth = editor.offsetWidth;
const menubarWidth = 480; // Menubar width (adjust as needed (px))
// Get the end position of the selection
const { bottom: endBottom, right: endRight } = editorView.coordsAtPos(to);
// Get the start position of the selection
const { left: startLeft } = editorView.coordsAtPos(from);
// Calculate the top position for the menubar (10px below the selection)
const top = endBottom - editorTop + 10;
// Calculate the left position for the menubar
// This centers the menubar on the selection while keeping it within the editor's bounds
const left = Math.max(
0,
Math.min(
(startLeft + endRight) / 2 - editorLeft,
editorWidth - menubarWidth
)
);
// Set the CSS custom properties for positioning the menubar
editor.style.setProperty('--selection-top', `${top}px`);
editor.style.setProperty('--selection-left', `${left}px`);
},
},
};
</script>
@@ -311,7 +259,6 @@ export default {
}
.editor-root {
position: relative;
width: 100%;
}
@@ -1,5 +1,5 @@
<script>
import { Letter } from 'vue-letter';
import Letter from 'vue-letter';
import GalleryView from '../components/GalleryView.vue';
export default {
@@ -1,15 +1,14 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { ref } from 'vue';
import { describe, it, expect, vi } from 'vitest';
import { useAccount } from '../useAccount';
import { useRoute } from 'vue-router';
import { useStoreGetters } from 'dashboard/composables/store';
vi.mock('vue-router');
vi.mock('dashboard/composables/store');
describe('useAccount', () => {
beforeEach(() => {
useRoute.mockReturnValue({
params: {
accountId: 123,
},
useStoreGetters.mockReturnValue({
getCurrentAccountId: ref(123),
});
});
@@ -1,20 +1,18 @@
import { computed } from 'vue';
import { useRoute } from 'vue-router';
import { useStoreGetters } from 'dashboard/composables/store';
/**
* Composable for account-related operations.
* @returns {Object} An object containing account-related properties and methods.
*/
export function useAccount() {
const getters = useStoreGetters();
/**
* Computed property for the current account ID.
* @type {import('vue').ComputedRef<number>}
*/
const route = useRoute();
const accountId = computed(() => {
return Number(route.params.accountId);
});
const accountId = computed(() => getters.getCurrentAccountId.value);
/**
* Generates an account-scoped URL.
@@ -1,20 +0,0 @@
{
"PAGINATION_FOOTER": {
"SHOWING": "Showing {startItem} - {endItem} of {totalItems} items",
"CURRENT_PAGE_INFO": "{currentPage} of {totalPages} pages"
},
"COMBOBOX": {
"PLACEHOLDER": "Select an option...",
"EMPTY_STATE": "No results found.",
"SEARCH_PLACEHOLDER": "Search..."
},
"DIALOG": {
"BUTTONS": {
"CANCEL": "Cancel",
"CONFIRM": "Confirm"
}
},
"BREADCRUMB": {
"ARIA_LABEL": "Breadcrumb"
}
}
@@ -477,10 +477,6 @@
}
}
},
"LOADING": "Loading...",
"LOCALES_PAGE": {
"LOCALES_COUNT": "No locales available | {n} locale | {n} locales",
"NEW_LOCALE_BUTTON_TEXT": "New locale"
}
"LOADING": "Loading..."
}
}
@@ -1,4 +1,3 @@
import components from './components.json';
import advancedFilters from './advancedFilters.json';
import agentBots from './agentBots.json';
import agentMgmt from './agentMgmt.json';
@@ -37,7 +36,6 @@ import teamsSettings from './teamsSettings.json';
import whatsappTemplates from './whatsappTemplates.json';
export default {
...components,
...advancedFilters,
...agentBots,
...agentMgmt,
@@ -85,13 +85,9 @@ export default {
const {
social_profiles: socialProfiles,
screen_name: twitterScreenName,
social_telegram_user_name: telegramUsername,
} = this.additionalAttributes;
return {
twitter: twitterScreenName,
telegram: telegramUsername,
...(socialProfiles || {}),
};
return { twitter: twitterScreenName, ...(socialProfiles || {}) };
},
// Delete Modal
confirmDeleteMessage() {
@@ -14,7 +14,6 @@ export default {
{ key: 'linkedin', icon: 'linkedin', link: 'https://linkedin.com/' },
{ key: 'github', icon: 'github', link: 'https://github.com/' },
{ key: 'instagram', icon: 'instagram', link: 'https://instagram.com/' },
{ key: 'telegram', icon: 'telegram', link: 'https://t.me/' },
],
};
},
@@ -30,7 +29,7 @@ export default {
<!-- eslint-disable-next-line vue/no-root-v-if -->
<template>
<div v-if="availableProfiles.length" class="flex items-end gap-3 mx-0 my-2">
<div v-if="availableProfiles.length" class="flex items-end mx-0 my-2 gap-3">
<a
v-for="profile in availableProfiles"
:key="profile.key"
@@ -29,7 +29,7 @@ const showNewButton = computed(
button-route="new"
:icon="icon"
:header-title="t(headerTitle)"
:button-text="t(headerButtonText)"
:button-text="headerButtonText ? t(headerButtonText) : ''"
:show-back-button="showBackButton"
:back-url="backUrl"
:show-new-button="showNewButton"
@@ -5,7 +5,6 @@ import { mapGetters } from 'vuex';
import { useAlert } from 'dashboard/composables';
import { useUISettings } from 'dashboard/composables/useUISettings';
import { useConfig } from 'dashboard/composables/useConfig';
import { useAccount } from 'dashboard/composables/useAccount';
import { FEATURE_FLAGS } from '../../../../featureFlags';
import semver from 'semver';
import { getLanguageDirection } from 'dashboard/components/widgets/conversation/advancedFilterItems/languages';
@@ -14,10 +13,9 @@ export default {
setup() {
const { updateUISettings } = useUISettings();
const { enabledLanguages } = useConfig();
const { accountId } = useAccount();
const v$ = useVuelidate();
return { updateUISettings, v$, enabledLanguages, accountId };
return { updateUISettings, v$, enabledLanguages };
},
data() {
return {
@@ -48,6 +46,7 @@ export default {
globalConfig: 'globalConfig/get',
getAccount: 'accounts/getAccount',
uiFlags: 'accounts/getUIFlags',
accountId: 'getCurrentAccountId',
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
}),
showAutoResolutionConfig() {
@@ -96,8 +95,8 @@ export default {
return this.id.toString();
},
},
mounted() {
this.initializeAccount();
async mounted() {
await this.initializeAccount();
},
methods: {
async initializeAccount() {
@@ -113,7 +112,7 @@ export default {
latest_chatwoot_version: latestChatwootVersion,
} = this.getAccount(this.accountId);
this.$root.$i18n.locale = locale;
this.$i18n.locale = locale;
this.name = name;
this.locale = locale;
this.id = id;
@@ -141,7 +140,7 @@ export default {
support_email: this.supportEmail,
auto_resolve_duration: this.autoResolveDuration,
});
this.$root.$i18n.locale = this.locale;
this.$i18n.locale = this.locale;
this.getAccount(this.id).locale = this.locale;
this.updateDirectionView(this.locale);
useAlert(this.$t('GENERAL_SETTINGS.UPDATE.SUCCESS'));
@@ -47,7 +47,7 @@ const emailProviderList = computed(() => {
function onClick(emailProvider) {
if (emailProvider.isEnabled) {
provider.value = emailProvider.key;
this.provider = emailProvider.key;
}
}
</script>
@@ -1,5 +1,5 @@
<script setup>
import { ref, computed } from 'vue';
import { ref } from 'vue';
import InboxReconnectionRequired from '../../components/InboxReconnectionRequired.vue';
import googleClient from 'dashboard/api/channel/googleClient';
@@ -17,18 +17,11 @@ const { t } = useI18n();
const isRequestingAuthorization = ref(false);
const inboxEmail = computed(() => {
if (props.inbox.imap_login && props.inbox.imap_enabled) {
return props.inbox.imap_login;
}
return props.inbox.email;
});
async function requestAuthorization() {
try {
isRequestingAuthorization.value = true;
const response = await googleClient.generateAuthorization({
email: inboxEmail.value,
email: props.inbox.email,
});
const {
@@ -125,7 +125,7 @@ export default {
"
data-testid="app-title"
@input="v$.app.title.$touch"
@blur="v$.app.title.$touch"
@blur="v$.slug.$touch"
/>
<woot-input
v-model="app.content.url"
@@ -43,7 +43,11 @@ export const getters = {
return diffDays <= TRIAL_PERIOD_DAYS;
},
isFeatureEnabledonAccount: $state => (id, featureName) => {
console.log('----------------------------------------------------');
console.log('CHECK ENABLED for', featureName, id);
console.log($state, findRecordById($state, id));
const { features = {} } = findRecordById($state, id);
console.log('FEATURES', features, features[featureName]);
return features[featureName] || false;
},
};
@@ -1,5 +1,4 @@
import types from '../../mutation-types';
import * as Sentry from '@sentry/vue';
export const mutations = {
[types.SET_CONTACT_UI_FLAG]($state, data) {
@@ -49,23 +48,12 @@ export const mutations = {
[types.DELETE_CONTACT]: ($state, id) => {
const index = $state.sortOrder.findIndex(item => item === id);
$state.sortOrder.splice(index, 1);
delete $state.records[id];
$state.records[id] = null;
},
[types.UPDATE_CONTACTS_PRESENCE]: ($state, data) => {
Object.values($state.records).forEach(element => {
let availabilityStatus;
try {
availabilityStatus = data[element.id];
} catch (error) {
Sentry.setContext('contact is undefined', {
records: $state.records,
data: data,
});
Sentry.captureException(error);
return;
}
const availabilityStatus = data[element.id];
if (availabilityStatus) {
$state.records[element.id].availability_status = availabilityStatus;
} else {
@@ -33,7 +33,7 @@ export const mutations = {
// On reconnect, if there is existing notification with same primary_actor_id,
// it will be deleted and the new one will be added. So it will solve with duplicate notification
if (existingNotification) {
delete $state.records[existingNotification.id];
$state.records[existingNotification.id] = undefined;
}
$state.records[notification.id] = {
@@ -72,7 +72,7 @@ export const mutations = {
},
[types.DELETE_NOTIFICATION]($state, data) {
const { notification, unread_count: unreadCount, count } = data;
delete $state.records[notification.id];
$state.records[notification.id] = undefined;
$state.meta.unreadCount = unreadCount;
$state.meta.count = count;
},
@@ -83,7 +83,7 @@ export const mutations = {
[types.DELETE_READ_NOTIFICATIONS]: $state => {
Object.values($state.records).forEach(item => {
if (item.read_at) {
delete $state.records[item.id];
$state.records[item.id] = undefined;
}
});
},
@@ -1,21 +0,0 @@
@import 'dashboard/assets/scss/app';
*,
::before,
::after {
--_histoire-color-primary-50: 235 245 255;
--_histoire-color-primary-100: 194 225 255;
--_histoire-color-primary-200: 153 206 255;
--_histoire-color-primary-300: 112 186 255;
--_histoire-color-primary-400: 71 166 255;
--_histoire-color-primary-500: 31 147 255;
--_histoire-color-primary-600: 25 118 204;
--_histoire-color-primary-700: 19 88 153;
--_histoire-color-primary-800: 12 59 102;
--_histoire-color-primary-900: 6 29 51;
}
html,
body {
font-family: 'Inter', sans-serif;
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="512px" height="512px" viewBox="0 0 512 512" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 59.1 (86144) - https://sketch.com -->
<title>woot-log</title>
<desc>Created with Sketch.</desc>
<g id="Logo" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="woot-log" fill-rule="nonzero">
<circle id="Oval" fill="#47A7F6" cx="256" cy="256" r="256"></circle>
<path d="M362.807947,368.807947 L244.122956,368.807947 C178.699407,368.807947 125.456954,315.561812 125.456954,250.12177 C125.456954,184.703089 178.699407,131.456954 244.124143,131.456954 C309.565494,131.456954 362.807947,184.703089 362.807947,250.12177 L362.807947,368.807947 Z" id="Fill-1" stroke="#FFFFFF" stroke-width="6" fill="#FFFFFF"></path>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 916 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

+4 -3
View File
@@ -10,6 +10,7 @@ import Multiselect from 'vue-multiselect';
import { plugin, defaultConfig } from '@formkit/vue';
import WootSwitch from 'components/ui/Switch.vue';
import WootWizard from 'components/ui/Wizard.vue';
import { sync } from 'vuex-router-sync';
import FloatingVue from 'floating-vue';
import WootUiKit from 'dashboard/components';
import App from 'dashboard/App.vue';
@@ -17,7 +18,6 @@ import i18nMessages from 'dashboard/i18n';
import createAxios from 'dashboard/helper/APIHelper';
import commonHelpers, { isJSONValid } from 'dashboard/helper/commons';
import { sync } from 'vuex-router-sync';
import router, { initalizeRouter } from 'dashboard/routes';
import store from 'dashboard/store';
import constants from 'dashboard/constants/globals';
@@ -39,11 +39,10 @@ import 'floating-vue/dist/style.css';
const i18n = createI18n({
legacy: false, // https://github.com/intlify/vue-i18n/issues/1902
locale: 'en',
fallbackLocale: 'en',
messages: i18nMessages,
});
sync(store, router);
const app = createApp(App);
app.use(i18n);
app.use(store);
@@ -99,9 +98,11 @@ app.component('fluent-icon', FluentIcon);
app.directive('resize', vResizeObserver);
app.directive('on-clickaway', onClickaway);
sync(store, router);
// load common helpers into js
commonHelpers();
window.WOOT_STORE = store;
window.dashboardApp = app;
window.WootConstants = constants;
window.axios = createAxios(axios);
// [VITE] Disabled this we don't need it, we can use `useEmitter` directly
+1
View File
@@ -25,6 +25,7 @@ import {
const i18n = createI18n({
legacy: false, // https://github.com/intlify/vue-i18n/issues/1902
locale: 'en',
fallbackLocale: 'en',
messages: i18nMessages,
});
-14
View File
@@ -1,14 +0,0 @@
import './design-system/histoire.scss';
import { defineSetupVue3 } from '@histoire/plugin-vue';
import i18nMessages from 'dashboard/i18n';
import { createI18n } from 'vue-i18n';
const i18n = createI18n({
legacy: false, // https://github.com/intlify/vue-i18n/issues/1902
locale: 'en',
messages: i18nMessages,
});
export const setupVue3 = defineSetupVue3(({ app }) => {
app.use(i18n);
});
@@ -285,14 +285,5 @@
"M9.60364 9.20645C9.60364 8.67008 10.0385 8.23523 10.5749 8.23523C11.1113 8.23523 11.5461 8.67008 11.5461 9.20645V11.4511C11.5461 11.9875 11.1113 12.4223 10.5749 12.4223C10.0385 12.4223 9.60364 11.9875 9.60364 11.4511V9.20645Z",
"M17.1442 5.57049C13.5275 5.06019 10.5793 5.04007 6.88135 5.56825C5.9466 5.70176 5.32812 5.79197 4.85654 5.92976C4.41928 6.05757 4.17061 6.20994 3.96492 6.43984C3.539 6.91583 3.48286 7.45419 3.4248 9.33184C3.36775 11.1772 3.48076 12.831 3.69481 14.6918C3.80887 15.6834 3.88736 16.3526 4.01268 16.8613C4.13155 17.3439 4.27532 17.6034 4.47513 17.802C4.67654 18.0023 4.93467 18.1435 5.40841 18.2581C5.90952 18.3793 6.56702 18.4526 7.5442 18.5592C10.7045 18.904 13.0702 18.9022 16.2423 18.561C17.2313 18.4546 17.8995 18.3813 18.4081 18.2609C18.8913 18.1465 19.1511 18.0063 19.3497 17.8118C19.5442 17.6213 19.6928 17.3587 19.8217 16.852C19.9561 16.3234 20.0476 15.624 20.18 14.5966C20.4162 12.7633 20.5863 11.1533 20.5929 9.3896C20.5999 7.50391 20.5613 6.96737 20.1306 6.46971C19.9226 6.22932 19.6696 6.0713 19.2224 5.93968C18.7395 5.79754 18.1042 5.70594 17.1442 5.57049ZM6.65555 3.98715C10.5078 3.43695 13.6072 3.45849 17.3674 3.98902L17.4224 3.99678C18.3127 4.12235 19.0648 4.22844 19.6733 4.40753C20.33 4.60078 20.8792 4.89417 21.3382 5.4245C22.2041 6.42482 22.1984 7.6117 22.1909 9.18858C22.1905 9.25686 22.1902 9.32584 22.19 9.3956C22.183 11.2604 22.0026 12.949 21.764 14.8006L21.7577 14.8496C21.6332 15.8159 21.5307 16.6121 21.3695 17.2458C21.2 17.9121 20.9467 18.4833 20.4672 18.9529C19.9919 19.4183 19.4302 19.6602 18.776 19.8151C18.1582 19.9613 17.3895 20.044 16.4629 20.1436L16.4131 20.149C13.1283 20.5023 10.6472 20.5043 7.37097 20.1469L7.32043 20.1414C6.40679 20.0417 5.64604 19.9587 5.03292 19.8104C4.38112 19.6527 3.82317 19.406 3.34911 18.9347C2.87346 18.4618 2.62363 17.8999 2.46191 17.2433C2.30938 16.6241 2.22071 15.8531 2.11393 14.9246L2.10815 14.8743C1.88863 12.9659 1.76823 11.23 1.82845 9.28246C1.83063 9.2118 1.83272 9.14191 1.83479 9.07281C1.8816 7.50776 1.91671 6.33374 2.7747 5.37486C3.22992 4.86612 3.76798 4.58399 4.40853 4.39678C5.00257 4.22316 5.73505 4.11858 6.60207 3.99479C6.61981 3.99225 6.63764 3.9897 6.65555 3.98715Z"
],
"scan-person-outline": "M5.25 3.5A1.75 1.75 0 0 0 3.5 5.25v3a.75.75 0 0 1-1.5 0v-3A3.25 3.25 0 0 1 5.25 2h3a.75.75 0 0 1 0 1.5zm0 17a1.75 1.75 0 0 1-1.75-1.75v-3a.75.75 0 0 0-1.5 0v3A3.25 3.25 0 0 0 5.25 22h3a.75.75 0 0 0 .707-1l-.005-.015a.75.75 0 0 0-.702-.485zM20.5 5.25a1.75 1.75 0 0 0-1.75-1.75h-3a.75.75 0 0 1 0-1.5h3A3.25 3.25 0 0 1 22 5.25v3a.75.75 0 0 1-1.5 0zM18.75 20.5a1.75 1.75 0 0 0 1.75-1.75v-3a.75.75 0 0 1 1.5 0v3A3.25 3.25 0 0 1 18.75 22h-3a.75.75 0 0 1 0-1.5zM6.5 18.616q0 .465.258.884H5.25a1 1 0 0 1-.129-.011A3.1 3.1 0 0 1 5 18.616v-.366A2.25 2.25 0 0 1 7.25 16h9.5A2.25 2.25 0 0 1 19 18.25v.366c0 .31-.047.601-.132.875a1 1 0 0 1-.118.009h-1.543a1.56 1.56 0 0 0 .293-.884v-.366a.75.75 0 0 0-.75-.75h-9.5a.75.75 0 0 0-.75.75zm8.25-8.866a2.75 2.75 0 1 0-5.5 0a2.75 2.75 0 0 0 5.5 0m1.5 0a4.25 4.25 0 1 1-8.5 0a4.25 4.25 0 0 1 8.5 0",
"play-shape-outline": "M3.7 11q-.575 0-.862-.488t-.013-.987l3.3-5.95q.275-.5.875-.5t.875.5l3.3 5.95q.275.5-.012.988T10.3 11zM7 21q-1.65 0-2.825-1.175T3 17q0-1.675 1.175-2.838T7 13q1.65 0 2.825 1.175T11 17q0 1.65-1.175 2.825T7 21m0-2q.825 0 1.412-.587T9 17q0-.825-.587-1.412T7 15q-.825 0-1.412.588T5 17q0 .825.588 1.413T7 19M5.4 9h3.2L7 6.125zM14 21q-.425 0-.712-.288T13 20v-6q0-.425.288-.712T14 13h6q.425 0 .713.288T21 14v6q0 .425-.288.713T20 21zm1-2h4v-4h-4zm.6-12.5l-1.9-1.9q-.275-.275-.275-.7t.275-.7q.275-.275.7-.275t.7.275L17 5.1l1.9-1.9q.275-.275.7-.275t.7.275q.275.275.275.7t-.275.7l-1.9 1.9l1.9 1.9q.275.275.275.7t-.275.7q-.275.275-.7.275t-.7-.275L17 7.9l-1.9 1.9q-.275.275-.7.275t-.7-.275q-.275-.275-.275-.7t.275-.7z",
"upload-lucide-outline": "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4m14-7l-5-5l-5 5m5-5v12",
"chevrons-lucide-left-outline": "m11 17l-5-5l5-5m7 10l-5-5l5-5",
"chevrons-lucide-right-outline": "m6 17l5-5l-5-5m7 10l5-5l-5-5",
"chevron-lucide-left-outline": "m15 18l-6-6l6-6",
"chevron-lucide-right-outline": "m9 18l6-6l-6-6",
"chevron-lucide-up-outline": "m18 15l-6-6l-6 6",
"chevron-lucide-down-outline": "m6 9l6 6l6-6"
"scan-person-outline": "M5.25 3.5A1.75 1.75 0 0 0 3.5 5.25v3a.75.75 0 0 1-1.5 0v-3A3.25 3.25 0 0 1 5.25 2h3a.75.75 0 0 1 0 1.5zm0 17a1.75 1.75 0 0 1-1.75-1.75v-3a.75.75 0 0 0-1.5 0v3A3.25 3.25 0 0 0 5.25 22h3a.75.75 0 0 0 .707-1l-.005-.015a.75.75 0 0 0-.702-.485zM20.5 5.25a1.75 1.75 0 0 0-1.75-1.75h-3a.75.75 0 0 1 0-1.5h3A3.25 3.25 0 0 1 22 5.25v3a.75.75 0 0 1-1.5 0zM18.75 20.5a1.75 1.75 0 0 0 1.75-1.75v-3a.75.75 0 0 1 1.5 0v3A3.25 3.25 0 0 1 18.75 22h-3a.75.75 0 0 1 0-1.5zM6.5 18.616q0 .465.258.884H5.25a1 1 0 0 1-.129-.011A3.1 3.1 0 0 1 5 18.616v-.366A2.25 2.25 0 0 1 7.25 16h9.5A2.25 2.25 0 0 1 19 18.25v.366c0 .31-.047.601-.132.875a1 1 0 0 1-.118.009h-1.543a1.56 1.56 0 0 0 .293-.884v-.366a.75.75 0 0 0-.75-.75h-9.5a.75.75 0 0 0-.75.75zm8.25-8.866a2.75 2.75 0 1 0-5.5 0a2.75 2.75 0 0 0 5.5 0m1.5 0a4.25 4.25 0 1 1-8.5 0a4.25 4.25 0 0 1 8.5 0"
}
+1 -1
View File
@@ -120,7 +120,7 @@ export default {
}
},
setLocale(locale) {
this.$root.$i18n.locale = locale || 'en';
this.$i18n.locale = locale || 'en';
},
},
};
+1 -1
View File
@@ -31,7 +31,7 @@ export default {
};
},
setLocale(locale) {
this.$root.$i18n.locale = locale;
this.$i18n.locale = locale;
},
},
};
+30 -2
View File
@@ -66,6 +66,33 @@ export default {
mounted() {
const { websiteToken, locale, widgetColor } = window.chatwootWebChannel;
this.setLocale(locale);
setTimeout(() => {
console.log('SET LOCALE AGAIN 100');
this.setLocale(locale);
}, 100);
setTimeout(() => {
console.log('SET LOCALE AGAIN 200');
this.setLocale(locale);
}, 200);
setTimeout(() => {
console.log('SET LOCALE AGAIN 300');
this.setLocale(locale);
}, 300);
setTimeout(() => {
console.log('SET LOCALE AGAIN 400');
this.setLocale(locale);
}, 400);
setTimeout(() => {
console.log('SET LOCALE AGAIN 500');
this.setLocale(locale);
}, 500);
setTimeout(() => {
console.log('SET LOCALE YET AGAIN');
this.setLocale(locale);
}, 5000);
this.setWidgetColor(widgetColor);
setHeader(window.authToken);
if (this.isIFrame) {
@@ -121,6 +148,7 @@ export default {
},
setLocale(localeWithVariation) {
if (!localeWithVariation) return;
console.log('SETTING LOCALE HAHAHAHAHA!', localeWithVariation);
const { enabledLanguages } = window.chatwootWebChannel;
const localeWithoutVariation = localeWithVariation.split('_')[0];
const hasLocaleWithoutVariation = enabledLanguages.some(
@@ -131,9 +159,9 @@ export default {
);
if (hasLocaleWithVariation) {
this.$root.$i18n.locale = localeWithVariation;
this.$i18n.locale = localeWithVariation;
} else if (hasLocaleWithoutVariation) {
this.$root.$i18n.locale = localeWithoutVariation;
this.$i18n.locale = localeWithoutVariation;
}
},
registerUnreadEvents() {
@@ -60,12 +60,7 @@ export default {
chatwootWebChannel: { websiteToken },
authToken,
} = window;
popoutChatWindow(
origin,
websiteToken,
this.$root.$i18n.locale,
authToken
);
popoutChatWindow(origin, websiteToken, this.$i18n.locale, authToken);
},
closeWindow() {
if (IFrameHelper.isIFrame()) {
@@ -37,7 +37,7 @@ export default {
},
data() {
return {
locale: this.$root.$i18n.locale,
locale: this.$i18n.locale,
hasErrorInPhoneInput: false,
message: '',
formValues: {},
@@ -75,7 +75,7 @@ export default {
messageId: this.messageId,
});
} catch (error) {
// Ignore error
console.log(error);
} finally {
this.isUpdating = false;
}
@@ -41,7 +41,7 @@ export default {
);
this.dyteAuthToken = authToken;
} catch (error) {
// Ignore Error for now
console.log(error);
} finally {
this.isLoading = false;
}
@@ -31,7 +31,7 @@ export const actions = {
const { data } = await ContactsAPI.get();
commit(SET_CURRENT_USER, data);
} catch (error) {
// Ignore error
console.log(error);
}
},
update: async ({ dispatch }, { user }) => {
@@ -39,7 +39,7 @@ export const actions = {
await ContactsAPI.update(user);
dispatch('get');
} catch (error) {
// Ignore error
console.log(error);
}
},
setUser: async ({ dispatch }, { identifier, user: userObject }) => {
@@ -91,14 +91,14 @@ export const actions = {
try {
await ContactsAPI.setCustomAttributes(customAttributes);
} catch (error) {
// Ignore error
console.log(error);
}
},
deleteCustomAttribute: async (_, customAttribute) => {
try {
await ContactsAPI.deleteCustomAttribute(customAttribute);
} catch (error) {
// Ignore error
console.log(error);
}
},
};
@@ -25,7 +25,7 @@ export const actions = {
// Emit event to notify that conversation is created and show the chat screen
emitter.emit(ON_CONVERSATION_CREATED);
} catch (error) {
// Ignore error
console.log(error);
} finally {
commit('setConversationUIFlag', { isCreating: false });
}
@@ -133,7 +133,7 @@ export const actions = {
commit('conversation/setMetaUserLastSeenAt', lastSeen, { root: true });
commit('setMissingMessagesInConversation', updatedConversation);
} catch (error) {
// IgnoreError
console.log(error);
}
},
@@ -158,7 +158,7 @@ export const actions = {
try {
await toggleTyping(data);
} catch (error) {
// IgnoreError
console.log(error);
}
},
@@ -172,7 +172,7 @@ export const actions = {
commit('setMetaUserLastSeenAt', lastSeen);
await setUserLastSeenAt({ lastSeen });
} catch (error) {
// IgnoreError
console.log(error);
}
},
@@ -184,7 +184,7 @@ export const actions = {
try {
await setCustomAttributes(customAttributes);
} catch (error) {
// IgnoreError
console.log(error);
}
},
@@ -192,7 +192,7 @@ export const actions = {
try {
await deleteCustomAttribute(customAttribute);
} catch (error) {
// IgnoreError
console.log(error);
}
},
};
@@ -22,7 +22,7 @@ export const actions = {
commit(SET_CONVERSATION_ATTRIBUTES, data);
commit('conversation/setMetaUserLastSeenAt', lastSeen, { root: true });
} catch (error) {
// Ignore error
console.log(error);
}
},
update({ commit }, data) {
@@ -9,14 +9,14 @@ export const actions = {
try {
await conversationLabels.create(label);
} catch (error) {
// Ignore error
console.log(error);
}
},
destroy: async (_, label) => {
try {
await conversationLabels.destroy(label);
} catch (error) {
// Ignore error
console.log(error);
}
},
};
@@ -5,7 +5,7 @@ const actions = {
try {
await events.create(name);
} catch (error) {
// Ignore error
console.log(error);
}
},
};

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