Merge branch 'develop' into feat/ui-lib
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import AddAccountModal from '../dashboard/components/layout/sidebarComponents/AddAccountModal.vue';
|
||||
import AddAccountModal from './components/app/AddAccountModal.vue';
|
||||
import LoadingState from './components/widgets/LoadingState.vue';
|
||||
import NetworkNotification from './components/NetworkNotification.vue';
|
||||
import UpdateBanner from './components/app/UpdateBanner.vue';
|
||||
@@ -136,8 +136,7 @@ export default {
|
||||
<div
|
||||
v-if="!authUIFlags.isFetching && !accountUIFlags.isFetchingItem"
|
||||
id="app"
|
||||
class="flex-grow-0 w-full h-full min-h-0 app-wrapper"
|
||||
:class="{ 'app-rtl--wrapper': isRTL }"
|
||||
class="flex flex-col w-full h-screen min-h-0"
|
||||
:dir="isRTL ? 'rtl' : 'ltr'"
|
||||
>
|
||||
<UpdateBanner :latest-chatwoot-version="latestChatwootVersion" />
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/* global axios */
|
||||
import ApiClient from '../ApiClient';
|
||||
|
||||
class CaptainScenarios extends ApiClient {
|
||||
constructor() {
|
||||
super('captain/assistants', { accountScoped: true });
|
||||
}
|
||||
|
||||
get({ assistantId, page = 1, searchKey } = {}) {
|
||||
return axios.get(`${this.url}/${assistantId}/scenarios`, {
|
||||
params: { page, searchKey },
|
||||
});
|
||||
}
|
||||
|
||||
show({ assistantId, id }) {
|
||||
return axios.get(`${this.url}/${assistantId}/scenarios/${id}`);
|
||||
}
|
||||
|
||||
create({ assistantId, ...data } = {}) {
|
||||
return axios.post(`${this.url}/${assistantId}/scenarios`, {
|
||||
scenario: data,
|
||||
});
|
||||
}
|
||||
|
||||
update({ assistantId, id }, data = {}) {
|
||||
return axios.put(`${this.url}/${assistantId}/scenarios/${id}`, {
|
||||
scenario: data,
|
||||
});
|
||||
}
|
||||
|
||||
delete({ assistantId, id }) {
|
||||
return axios.delete(`${this.url}/${assistantId}/scenarios/${id}`);
|
||||
}
|
||||
}
|
||||
|
||||
export default new CaptainScenarios();
|
||||
@@ -0,0 +1,16 @@
|
||||
/* global axios */
|
||||
import ApiClient from '../ApiClient';
|
||||
|
||||
class CaptainTools extends ApiClient {
|
||||
constructor() {
|
||||
super('captain/assistants/tools', { accountScoped: true });
|
||||
}
|
||||
|
||||
get(params = {}) {
|
||||
return axios.get(this.url, {
|
||||
params,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new CaptainTools();
|
||||
@@ -0,0 +1,21 @@
|
||||
/* global axios */
|
||||
import ApiClient from '../ApiClient';
|
||||
|
||||
class WhatsappChannel extends ApiClient {
|
||||
constructor() {
|
||||
super('whatsapp', { accountScoped: true });
|
||||
}
|
||||
|
||||
createEmbeddedSignup(params) {
|
||||
return axios.post(`${this.baseUrl()}/whatsapp/authorization`, params);
|
||||
}
|
||||
|
||||
reauthorizeWhatsApp({ inboxId, ...params }) {
|
||||
return axios.post(`${this.baseUrl()}/whatsapp/authorization`, {
|
||||
...params,
|
||||
inbox_id: inboxId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new WhatsappChannel();
|
||||
@@ -21,6 +21,14 @@ class PortalsAPI extends ApiClient {
|
||||
deleteLogo(portalSlug) {
|
||||
return axios.delete(`${this.url}/${portalSlug}/logo`);
|
||||
}
|
||||
|
||||
sendCnameInstructions(portalSlug, email) {
|
||||
return axios.post(`${this.url}/${portalSlug}/send_instructions`, { email });
|
||||
}
|
||||
|
||||
sslStatus(portalSlug) {
|
||||
return axios.get(`${this.url}/${portalSlug}/ssl_status`);
|
||||
}
|
||||
}
|
||||
|
||||
export default PortalsAPI;
|
||||
|
||||
@@ -35,6 +35,10 @@ class Inboxes extends BaseClass {
|
||||
agent_bot: botId,
|
||||
});
|
||||
}
|
||||
|
||||
syncTemplates(inboxId) {
|
||||
return axios.post(`${this.url}/${inboxId}/sync_templates`);
|
||||
}
|
||||
}
|
||||
|
||||
export default new Inboxes();
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
/* global axios */
|
||||
import ApiClient from './ApiClient';
|
||||
|
||||
class NotionOAuthClient extends ApiClient {
|
||||
constructor() {
|
||||
super('notion', { accountScoped: true });
|
||||
}
|
||||
|
||||
generateAuthorization() {
|
||||
return axios.post(`${this.url}/authorization`);
|
||||
}
|
||||
}
|
||||
|
||||
export default new NotionOAuthClient();
|
||||
@@ -12,6 +12,7 @@ describe('#InboxesAPI', () => {
|
||||
expect(inboxesAPI).toHaveProperty('getCampaigns');
|
||||
expect(inboxesAPI).toHaveProperty('getAgentBot');
|
||||
expect(inboxesAPI).toHaveProperty('setAgentBot');
|
||||
expect(inboxesAPI).toHaveProperty('syncTemplates');
|
||||
});
|
||||
|
||||
describe('API calls', () => {
|
||||
@@ -40,5 +41,12 @@ describe('#InboxesAPI', () => {
|
||||
inboxesAPI.deleteInboxAvatar(2);
|
||||
expect(axiosMock.delete).toHaveBeenCalledWith('/api/v1/inboxes/2/avatar');
|
||||
});
|
||||
|
||||
it('#syncTemplates', () => {
|
||||
inboxesAPI.syncTemplates(2);
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/inboxes/2/sync_templates'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
.slide-fade-enter-active {
|
||||
transition: all 0.3s var(--ease-in-cubic);
|
||||
}
|
||||
|
||||
.slide-fade-leave-active {
|
||||
transition: all 0.3s var(--ease-out-cubic);
|
||||
}
|
||||
|
||||
.slide-fade-enter,
|
||||
.slide-fade-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(10px);
|
||||
}
|
||||
|
||||
.slide-fade-enter {
|
||||
transform: translateX($space-micro);
|
||||
}
|
||||
|
||||
.slide-fade-leave-to {
|
||||
transform: translateX($space-medium);
|
||||
}
|
||||
|
||||
.conversations-list-enter-active,
|
||||
.conversations-list-leave-active {
|
||||
transition: all 0.25s var(--ease-out-cubic);
|
||||
}
|
||||
|
||||
.conversations-list-enter,
|
||||
.conversations-list-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX($space-medium);
|
||||
}
|
||||
|
||||
.slide-up-enter-active {
|
||||
transition: all 0.3s var(--ease-in-cubic);
|
||||
}
|
||||
|
||||
.slide-up-leave-active {
|
||||
transition: all 0.3s var(--ease-out-cubic);
|
||||
}
|
||||
|
||||
.slide-up-enter,
|
||||
.slide-up-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-$space-medium);
|
||||
}
|
||||
|
||||
.menu-slide-enter-active,
|
||||
.menu-slide-leave-active {
|
||||
transform: translateY(0);
|
||||
transition:
|
||||
transform 0.25s var(--ease-in-cubic),
|
||||
opacity 0.15s var(--ease-in-cubic);
|
||||
}
|
||||
|
||||
.menu-slide-enter,
|
||||
.menu-slide-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY($space-small);
|
||||
}
|
||||
|
||||
.toast-fade-enter-active {
|
||||
transition: all 0.3s var(--ease-in-sine);
|
||||
}
|
||||
|
||||
.toast-fade-leave-active {
|
||||
transition: all 0.1s var(--ease-out-sine);
|
||||
}
|
||||
|
||||
.toast-fade-enter,
|
||||
.toast-fade-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-$space-small);
|
||||
}
|
||||
|
||||
.modal-fade-enter-active {
|
||||
transition: all 0.3s var(--ease-in-sine);
|
||||
}
|
||||
|
||||
.modal-fade-leave-active {
|
||||
transition: all 0.1s var(--ease-out-sine);
|
||||
}
|
||||
|
||||
.modal-fade-enter,
|
||||
.modal-fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.network-notification-fade-enter-active {
|
||||
transition: all 0.1s var(--ease-in-sine);
|
||||
}
|
||||
|
||||
.network-notification-fade-leave-active {
|
||||
transition: all 0.1s var(--ease-out-sine);
|
||||
}
|
||||
|
||||
.network-notification-fade-enter,
|
||||
.network-notification-fade-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-$space-small);
|
||||
}
|
||||
+5
@@ -201,3 +201,8 @@ code {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Table
|
||||
table {
|
||||
@apply border-spacing-0 text-sm w-full;
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
@import 'dashboard/assets/scss/variables';
|
||||
|
||||
.formulate-input {
|
||||
.formulate-input-errors {
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.formulate-input-error {
|
||||
color: var(--r-400);
|
||||
display: block;
|
||||
font-size: var(--font-size-small);
|
||||
font-weight: $font-weight-normal;
|
||||
margin-bottom: $space-one;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.integration-hooks {
|
||||
.formulate-input[data-type='checkbox'] {
|
||||
.formulate-input-wrapper {
|
||||
@apply flex;
|
||||
|
||||
.formulate-input-element {
|
||||
@apply pr-2;
|
||||
|
||||
input {
|
||||
@apply mb-0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.formulate-input-element-decorator {
|
||||
@apply hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
// loader class
|
||||
.spinner {
|
||||
@include color-spinner();
|
||||
@apply inline-block h-6 py-0 px-6 relative align-middle w-6;
|
||||
|
||||
&.message {
|
||||
@apply bg-white dark:bg-slate-800 rounded-full left-0 my-3 mx-auto p-4 top-0;
|
||||
|
||||
&::before {
|
||||
@apply -ml-3 -mt-3;
|
||||
}
|
||||
}
|
||||
|
||||
&.small {
|
||||
@apply h-4 w-4;
|
||||
|
||||
&::before {
|
||||
@apply h-4 -mt-2 w-4;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
// scss-lint:disable SpaceAfterPropertyColon
|
||||
@import 'shared/assets/fonts/inter';
|
||||
|
||||
// Inter,
|
||||
html,
|
||||
body {
|
||||
font-family:
|
||||
'Inter',
|
||||
-apple-system,
|
||||
system-ui,
|
||||
BlinkMacSystemFont,
|
||||
'Segoe UI',
|
||||
Roboto,
|
||||
'Helvetica Neue',
|
||||
Tahoma,
|
||||
Arial,
|
||||
sans-serif !important;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.app-wrapper {
|
||||
@apply h-screen flex-grow-0 min-h-0 w-full;
|
||||
|
||||
.button--fixed-top {
|
||||
@apply fixed ltr:right-2 rtl:left-2 top-2 flex flex-row;
|
||||
}
|
||||
}
|
||||
|
||||
.banner + .app-wrapper {
|
||||
// Reduce the height of the dashboard to make room for the banner.
|
||||
// And causing the top right green-action button to be pushed down when scrolling.
|
||||
@apply h-[calc(100%-48px)];
|
||||
|
||||
.button--fixed-top {
|
||||
@apply top-14;
|
||||
}
|
||||
|
||||
.off-canvas-content {
|
||||
.button--fixed-top {
|
||||
@apply top-2;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
@import 'dashboard/assets/scss/variables';
|
||||
|
||||
$spinner-before-border-color: rgba(255, 255, 255, 0.7);
|
||||
|
||||
// input form
|
||||
@mixin ghost-input() {
|
||||
box-shadow: none;
|
||||
border-color: transparent;
|
||||
|
||||
&:active,
|
||||
&:hover,
|
||||
&:focus {
|
||||
border-color: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin color-spinner() {
|
||||
@keyframes spinner {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
&::before {
|
||||
animation: spinner .9s linear infinite;
|
||||
border: 2px solid $spinner-before-border-color;
|
||||
border-radius: 50%;
|
||||
border-top-color: lighten($color-woot, 10%);
|
||||
box-sizing: border-box;
|
||||
content: '';
|
||||
height: $space-medium;
|
||||
left: 50%;
|
||||
margin-left: -$space-one;
|
||||
margin-top: -$space-one;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: $space-medium;
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// arrows
|
||||
// --------------------------------------------------------
|
||||
// $direction: top, left, right, bottom, top-left, top-right, bottom-left, bottom-right
|
||||
// $color: hex, rgb or rbga
|
||||
// $size: px or em
|
||||
// @example
|
||||
// .element{
|
||||
// @include arrow(top, #000, 50px);
|
||||
// }
|
||||
@mixin arrow($direction, $color, $size) {
|
||||
display: block;
|
||||
height: 0;
|
||||
width: 0;
|
||||
content: '';
|
||||
|
||||
@if $direction == 'top' {
|
||||
border-bottom: $size solid $color;
|
||||
border-left: $size solid transparent;
|
||||
border-right: $size solid transparent;
|
||||
}
|
||||
|
||||
@else if $direction == 'right' {
|
||||
border-bottom: $size solid transparent;
|
||||
border-left: $size solid $color;
|
||||
border-top: $size solid transparent;
|
||||
}
|
||||
|
||||
@else if $direction == 'bottom' {
|
||||
border-left: $size solid transparent;
|
||||
border-right: $size solid transparent;
|
||||
border-top: $size solid $color;
|
||||
}
|
||||
|
||||
@else if $direction == 'left' {
|
||||
border-bottom: $size solid transparent;
|
||||
border-right: $size solid $color;
|
||||
border-top: $size solid transparent;
|
||||
}
|
||||
|
||||
@else if $direction == 'top-left' {
|
||||
border-right: $size solid transparent;
|
||||
border-top: $size solid $color;
|
||||
}
|
||||
|
||||
@else if $direction == 'top-right' {
|
||||
border-left: $size solid transparent;
|
||||
border-top: $size solid $color;
|
||||
}
|
||||
|
||||
@else if $direction == 'bottom-left' {
|
||||
border-bottom: $size solid $color;
|
||||
border-right: $size solid transparent;
|
||||
}
|
||||
|
||||
@else if $direction == 'bottom-right' {
|
||||
border-bottom: $size solid $color;
|
||||
border-left: $size solid transparent;
|
||||
}
|
||||
}
|
||||
@@ -1,204 +0,0 @@
|
||||
.app-rtl--wrapper {
|
||||
direction: rtl;
|
||||
|
||||
// Woot Tabs
|
||||
.tabs-title {
|
||||
&:first-child {
|
||||
margin-left: var(--space-small);
|
||||
margin-right: unset;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-left: unset;
|
||||
margin-right: var(--space-small);
|
||||
}
|
||||
}
|
||||
|
||||
// woot tables
|
||||
table,
|
||||
thead,
|
||||
th {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
// Table footer
|
||||
.footer {
|
||||
.page-meta {
|
||||
direction: initial;
|
||||
}
|
||||
}
|
||||
|
||||
// Wizard box
|
||||
.wizard-box {
|
||||
direction: initial;
|
||||
}
|
||||
|
||||
// Conversation details
|
||||
.conversation-details-wrap {
|
||||
.conversation-panel {
|
||||
// Message text
|
||||
.text-content {
|
||||
p {
|
||||
unicode-bidi: plaintext;
|
||||
}
|
||||
|
||||
ul {
|
||||
padding-left: unset;
|
||||
padding-right: var(--space-two);
|
||||
}
|
||||
|
||||
li {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
// Message items and actions
|
||||
li {
|
||||
&.right {
|
||||
.sender--info {
|
||||
padding: var(--space-small) var(--space-smaller)
|
||||
var(--space-smaller) 0;
|
||||
}
|
||||
|
||||
.context-menu-wrap {
|
||||
margin-left: 0;
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Conversation footer
|
||||
.conversation-footer {
|
||||
.preview-item {
|
||||
direction: initial;
|
||||
}
|
||||
}
|
||||
|
||||
// Custom attributes section in conversation sidebar
|
||||
.conversation-sidebar-wrap .checkbox-wrap {
|
||||
.checkbox {
|
||||
margin-left: var(--space-small);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Conversation list
|
||||
.conversations-list-wrap {
|
||||
border-right: 0;
|
||||
|
||||
.conversation {
|
||||
.conversation--meta {
|
||||
left: $space-normal;
|
||||
right: unset;
|
||||
|
||||
.unread {
|
||||
margin-left: unset;
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.assignee-label {
|
||||
margin-left: 0;
|
||||
margin-right: var(--space-one);
|
||||
}
|
||||
|
||||
.show-more--button {
|
||||
margin: unset;
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
// Basic filter dropdown
|
||||
.basic-filter {
|
||||
left: 0;
|
||||
right: unset;
|
||||
}
|
||||
|
||||
// Bulk actions
|
||||
.bulk-action__container {
|
||||
.triangle {
|
||||
left: var(--triangle-position);
|
||||
right: unset;
|
||||
}
|
||||
|
||||
.bulk-action__agents {
|
||||
left: var(--space-small);
|
||||
right: unset;
|
||||
}
|
||||
|
||||
.labels-container {
|
||||
left: var(--space-small);
|
||||
right: unset;
|
||||
|
||||
.label-checkbox {
|
||||
margin: 0 0 0 var(--space-one);
|
||||
}
|
||||
}
|
||||
|
||||
.actions-container {
|
||||
left: var(--space-small);
|
||||
right: unset;
|
||||
}
|
||||
|
||||
.bulk-action__teams {
|
||||
left: var(--space-small);
|
||||
right: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Contact notes
|
||||
.card.note-wrap {
|
||||
.time-stamp {
|
||||
unicode-bidi: plaintext;
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle switch
|
||||
.toggle-button {
|
||||
&.small {
|
||||
span {
|
||||
&.active {
|
||||
transform: translate(var(--space-minus-small), var(--space-zero));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
--minus-space-one-point-five: -0.9375rem;
|
||||
|
||||
&.active {
|
||||
transform: translate(
|
||||
var(--minus-space-one-point-five),
|
||||
var(--space-zero)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Modal
|
||||
.modal-container {
|
||||
text-align: right;
|
||||
|
||||
.modal-footer {
|
||||
button {
|
||||
margin-left: 0;
|
||||
margin-right: var(--space-small);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Other changes
|
||||
.colorpicker--chrome {
|
||||
direction: initial;
|
||||
}
|
||||
|
||||
.mention--box {
|
||||
direction: initial;
|
||||
}
|
||||
|
||||
.contact--form .input-group {
|
||||
direction: initial;
|
||||
}
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
// Font sizes
|
||||
$font-size-nano: 0.5rem;
|
||||
$font-size-micro: 0.675rem;
|
||||
$font-size-mini: 0.75rem;
|
||||
$font-size-small: 0.875rem;
|
||||
$font-size-default: 1rem;
|
||||
$font-size-medium: 1.125rem;
|
||||
$font-size-large: 1.375rem;
|
||||
$font-size-big: 1.5rem;
|
||||
$font-size-bigger: 1.75rem;
|
||||
$font-size-mega: 2.125rem;
|
||||
$font-size-giga: 2.5rem;
|
||||
|
||||
// spaces
|
||||
$zero: 0;
|
||||
$space-micro: 0.125rem;
|
||||
$space-smaller: 0.25rem;
|
||||
$space-small: 0.5rem;
|
||||
$space-one: 0.675rem;
|
||||
$space-slab: 0.75rem;
|
||||
$space-normal: 1rem;
|
||||
$space-two: 1.25rem;
|
||||
$space-medium: 1.5rem;
|
||||
$space-large: 2rem;
|
||||
$space-larger: 3rem;
|
||||
$space-jumbo: 4rem;
|
||||
$space-mega: 6.25rem;
|
||||
|
||||
// font-weight
|
||||
$font-weight-feather: 100;
|
||||
$font-weight-light: 300;
|
||||
$font-weight-normal: 400;
|
||||
$font-weight-medium: 500;
|
||||
$font-weight-bold: 600;
|
||||
$font-weight-black: 700;
|
||||
|
||||
//Navbar
|
||||
$nav-bar-width: 14.375rem;
|
||||
$header-height: 3.5rem;
|
||||
|
||||
$woot-logo-padding: $space-large $space-two;
|
||||
|
||||
// Colors
|
||||
$color-woot: #1f93ff;
|
||||
$color-gray: #6e6f73;
|
||||
$color-light-gray: #999a9b;
|
||||
|
||||
$color-border: var(--s-75);
|
||||
$color-border-light: var(--s-50);
|
||||
$color-border-dark: var(--s-100);
|
||||
|
||||
$color-background: var(--s-50);
|
||||
$color-background-light: var(--s-25);
|
||||
|
||||
$color-white: #fff;
|
||||
$color-body: #3c4858;
|
||||
$color-heading: #1f2d3d;
|
||||
$color-extra-light-blue: #f5f7f9;
|
||||
|
||||
$primary-color: $color-woot;
|
||||
$secondary-color: #5d7592;
|
||||
$success-color: #44ce4b;
|
||||
$warning-color: #ffc532;
|
||||
$alert-color: #ff382d;
|
||||
|
||||
$masked-bg: rgba(0, 0, 0, .4);
|
||||
|
||||
// Color-palettes
|
||||
|
||||
$color-primary-light: #c7e3ff;
|
||||
$color-primary-dark: darken($color-woot, 20%);
|
||||
|
||||
// Thumbnail
|
||||
$thumbnail-radius: 2.5rem;
|
||||
|
||||
// chat-header
|
||||
$conv-header-height: 2.5rem;
|
||||
|
||||
// Inbox List
|
||||
|
||||
$inbox-thumb-size: 3rem;
|
||||
|
||||
|
||||
// Snackbar default
|
||||
$woot-snackbar-bg: #323232;
|
||||
$woot-snackbar-button: #ffeb3b;
|
||||
|
||||
$swift-ease-out-duration: .4s !default;
|
||||
$swift-ease-out-function: cubic-bezier(0.37, 0, 0.63, 1) !default;
|
||||
$swift-ease-out: all $swift-ease-out-duration $swift-ease-out-function !default;
|
||||
|
||||
// Transitions
|
||||
$transition-ease-in: all 0.250s ease-in;
|
||||
|
||||
:root {
|
||||
--dashboard-app-tabs-height: 2.4375rem;
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
// scss-lint:disable SpaceAfterPropertyColon
|
||||
@import 'tailwindcss/base';
|
||||
@import 'tailwindcss/components';
|
||||
@import 'tailwindcss/utilities';
|
||||
@@ -8,54 +9,42 @@
|
||||
// Next Colors
|
||||
@import 'next-colors';
|
||||
|
||||
@import 'shared/assets/stylesheets/animations';
|
||||
@import 'shared/assets/stylesheets/colors';
|
||||
@import 'shared/assets/stylesheets/spacing';
|
||||
@import 'shared/assets/stylesheets/font-size';
|
||||
@import 'shared/assets/stylesheets/font-weights';
|
||||
@import 'shared/assets/stylesheets/shadows';
|
||||
@import 'shared/assets/stylesheets/border-radius';
|
||||
@import 'shared/assets/stylesheets/z-index';
|
||||
|
||||
@import 'variables';
|
||||
|
||||
@import 'mixins';
|
||||
@import 'helper-classes';
|
||||
@import 'formulate';
|
||||
@import 'date-picker';
|
||||
|
||||
@import 'layout';
|
||||
@import 'animations';
|
||||
@import 'rtl';
|
||||
|
||||
@import 'widgets/base';
|
||||
@import 'widgets/conversation-view';
|
||||
@import 'widgets/tabs';
|
||||
@import 'widgets/woot-tables';
|
||||
// Base styles for elements
|
||||
@import 'base';
|
||||
|
||||
// Plugins
|
||||
@import 'plugins/multiselect';
|
||||
@import 'plugins/dropdown';
|
||||
@import 'plugins/date-picker';
|
||||
|
||||
html,
|
||||
body {
|
||||
font-family:
|
||||
'Inter',
|
||||
-apple-system,
|
||||
system-ui,
|
||||
BlinkMacSystemFont,
|
||||
'Segoe UI',
|
||||
Roboto,
|
||||
'Helvetica Neue',
|
||||
Tahoma,
|
||||
Arial,
|
||||
sans-serif !important;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
@apply bg-slate-900 text-white py-1 px-2 z-40 text-xs rounded-md dark:bg-slate-200 dark:text-slate-900 max-w-96;
|
||||
@apply bg-n-solid-2 text-n-slate-12 py-1 px-2 z-40 text-xs rounded-md max-w-96;
|
||||
}
|
||||
|
||||
#app {
|
||||
@apply h-full w-full;
|
||||
}
|
||||
|
||||
.hide {
|
||||
@apply hidden;
|
||||
}
|
||||
|
||||
.n-blue-border {
|
||||
@apply border-n-blue-border;
|
||||
}
|
||||
|
||||
.n-blue-text {
|
||||
@apply text-n-blue-text;
|
||||
}
|
||||
|
||||
.custom-dashed-border {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100%25' height='100%25'%3E%3Crect x='0' y='0' width='100%25' height='100%25' fill='none' rx='16' ry='16' stroke='%23E2E3E7' stroke-width='2' stroke-dasharray='6, 8' stroke-dashoffset='0' stroke-linecap='round'/%3E%3C/svg%3E");
|
||||
background-position: center;
|
||||
@@ -67,351 +56,6 @@
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100%25' height='100%25'%3E%3Crect x='0' y='0' width='100%25' height='100%25' fill='none' rx='16' ry='16' stroke='%23343434' stroke-width='2' stroke-dasharray='6, 8' stroke-dashoffset='0' stroke-linecap='round'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
// scss-lint:disable PropertySortOrder
|
||||
@layer base {
|
||||
:root {
|
||||
--color-amber-25: 254 253 251;
|
||||
--color-amber-50: 255 249 237;
|
||||
--color-amber-75: 255 243 208;
|
||||
--color-amber-100: 255 236 183;
|
||||
--color-amber-200: 255 224 161;
|
||||
--color-amber-300: 245 208 140;
|
||||
--color-amber-400: 228 187 120;
|
||||
--color-amber-500: 214 163 92;
|
||||
--color-amber-600: 214 163 92;
|
||||
--color-amber-700: 255 186 26;
|
||||
--color-amber-800: 145 89 48;
|
||||
--color-amber-900: 79 52 34;
|
||||
|
||||
--color-ash-100: 235 235 239;
|
||||
--color-ash-200: 228 228 233;
|
||||
--color-ash-25: 252 252 253;
|
||||
--color-ash-300: 221 221 227;
|
||||
--color-ash-400: 211 212 219;
|
||||
--color-ash-50: 249 249 251;
|
||||
--color-ash-500: 185 187 198;
|
||||
--color-ash-600: 139 141 152;
|
||||
--color-ash-700: 126 128 138;
|
||||
--color-ash-75: 242 242 245;
|
||||
--color-ash-800: 96 100 108;
|
||||
--color-ash-900: 28 32 36;
|
||||
|
||||
--color-primary-25: 251 253 255;
|
||||
--color-primary-50: 245 249 255;
|
||||
--color-primary-75: 233 243 255;
|
||||
--color-primary-100: 218 236 255;
|
||||
--color-primary-200: 201 226 255;
|
||||
--color-primary-300: 181 213 255;
|
||||
--color-primary-400: 155 195 252;
|
||||
--color-primary-500: 117 171 247;
|
||||
--color-primary-600: 39 129 246;
|
||||
--color-primary-700: 16 115 233;
|
||||
--color-primary-800: 8 109 224;
|
||||
--color-primary-900: 11 50 101;
|
||||
|
||||
--color-ruby-100: 255 220 225;
|
||||
--color-ruby-200: 255 206 214;
|
||||
--color-ruby-25: 255 252 253;
|
||||
--color-ruby-300: 248 191 200;
|
||||
--color-ruby-400: 239 172 184;
|
||||
--color-ruby-50: 255 247 248;
|
||||
--color-ruby-500: 229 146 163;
|
||||
--color-ruby-600: 229 70 102;
|
||||
--color-ruby-700: 220 59 93;
|
||||
--color-ruby-75: 254 234 237;
|
||||
--color-ruby-800: 202 36 77;
|
||||
--color-ruby-900: 100 23 43;
|
||||
|
||||
--color-teal-100: 224 248 243;
|
||||
--color-teal-200: 204 243 234;
|
||||
--color-teal-25: 250 254 253;
|
||||
--color-teal-300: 184 234 224;
|
||||
--color-teal-400: 161 222 210;
|
||||
--color-teal-50: 243 251 249;
|
||||
--color-teal-500: 83 185 171;
|
||||
--color-teal-600: 18 165 148;
|
||||
--color-teal-700: 13 155 138;
|
||||
--color-teal-75: 236 249 255;
|
||||
--color-teal-800: 0 133 115;
|
||||
--color-teal-900: 13 61 56;
|
||||
|
||||
--color-green-25: 251 254 252;
|
||||
--color-green-50: 244 251 246;
|
||||
--color-green-75: 230 246 235;
|
||||
--color-green-100: 214 241 223;
|
||||
--color-green-200: 196 232 209;
|
||||
--color-green-300: 173 221 192;
|
||||
--color-green-400: 142 206 170;
|
||||
--color-green-500: 91 185 139;
|
||||
--color-green-600: 48 164 108;
|
||||
--color-green-700: 43 154 102;
|
||||
--color-green-800: 33 131 88;
|
||||
--color-green-900: 25 59 45;
|
||||
|
||||
--color-mint-25: 249 254 253;
|
||||
--color-mint-50: 242 251 249;
|
||||
--color-mint-75: 221 249 242;
|
||||
--color-mint-100: 200 244 233;
|
||||
--color-mint-200: 179 236 222;
|
||||
--color-mint-300: 156 224 208;
|
||||
--color-mint-400: 126 207 189;
|
||||
--color-mint-500: 76 187 165;
|
||||
--color-mint-600: 134 234 212;
|
||||
--color-mint-700: 125 224 203;
|
||||
--color-mint-800: 2 120 100;
|
||||
--color-mint-900: 22 67 60;
|
||||
|
||||
--color-sky-25: 249 254 255;
|
||||
--color-sky-50: 241 250 253;
|
||||
--color-sky-75: 225 246 253;
|
||||
--color-sky-100: 209 240 250;
|
||||
--color-sky-200: 190 231 245;
|
||||
--color-sky-300: 169 218 237;
|
||||
--color-sky-400: 141 202 227;
|
||||
--color-sky-500: 96 179 215;
|
||||
--color-sky-600: 124 226 254;
|
||||
--color-sky-700: 116 218 248;
|
||||
--color-sky-800: 0 116 158;
|
||||
--color-sky-900: 29 62 86;
|
||||
|
||||
--color-indigo-25: 253 253 254;
|
||||
--color-indigo-50: 247 249 255;
|
||||
--color-indigo-75: 237 242 254;
|
||||
--color-indigo-100: 225 233 255;
|
||||
--color-indigo-200: 210 222 255;
|
||||
--color-indigo-300: 193 208 255;
|
||||
--color-indigo-400: 171 189 249;
|
||||
--color-indigo-500: 141 164 239;
|
||||
--color-indigo-600: 62 99 221;
|
||||
--color-indigo-700: 51 88 212;
|
||||
--color-indigo-800: 58 91 199;
|
||||
--color-indigo-900: 31 45 92;
|
||||
|
||||
--color-iris-25: 253 253 255;
|
||||
--color-iris-50: 248 248 255;
|
||||
--color-iris-75: 240 241 254;
|
||||
--color-iris-100: 230 231 255;
|
||||
--color-iris-200: 218 220 255;
|
||||
--color-iris-300: 203 205 255;
|
||||
--color-iris-400: 184 186 248;
|
||||
--color-iris-500: 155 158 240;
|
||||
--color-iris-600: 91 91 214;
|
||||
--color-iris-700: 81 81 205;
|
||||
--color-iris-800: 87 83 198;
|
||||
--color-iris-900: 39 41 98;
|
||||
|
||||
--color-violet-25: 253 252 254;
|
||||
--color-violet-50: 250 248 255;
|
||||
--color-violet-75: 244 240 254;
|
||||
--color-violet-100: 235 228 255;
|
||||
--color-violet-200: 225 217 255;
|
||||
--color-violet-300: 212 202 254;
|
||||
--color-violet-400: 194 181 245;
|
||||
--color-violet-500: 170 153 236;
|
||||
--color-violet-600: 110 86 207;
|
||||
--color-violet-700: 101 77 196;
|
||||
--color-violet-800: 101 80 185;
|
||||
--color-violet-900: 47 38 95;
|
||||
|
||||
--color-pink-25: 255 252 254;
|
||||
--color-pink-50: 254 247 251;
|
||||
--color-pink-75: 254 233 245;
|
||||
--color-pink-100: 251 220 239;
|
||||
--color-pink-200: 246 206 231;
|
||||
--color-pink-300: 239 191 221;
|
||||
--color-pink-400: 231 172 208;
|
||||
--color-pink-500: 221 147 194;
|
||||
--color-pink-600: 214 64 159;
|
||||
--color-pink-700: 207 56 151;
|
||||
--color-pink-800: 194 41 138;
|
||||
--color-pink-900: 101 18 73;
|
||||
|
||||
--color-orange-25: 254 252 251;
|
||||
--color-orange-50: 255 247 237;
|
||||
--color-orange-75: 255 239 214;
|
||||
--color-orange-100: 255 223 181;
|
||||
--color-orange-200: 255 209 154;
|
||||
--color-orange-300: 255 193 130;
|
||||
--color-orange-400: 245 174 115;
|
||||
--color-orange-500: 236 148 85;
|
||||
--color-orange-600: 247 107 21;
|
||||
--color-orange-700: 239 95 0;
|
||||
--color-orange-800: 204 78 0;
|
||||
--color-orange-900: 88 45 29;
|
||||
}
|
||||
|
||||
// scss-lint:disable QualifyingElement
|
||||
body.dark {
|
||||
--color-amber-25: 31 19 0;
|
||||
--color-amber-50: 37 24 4;
|
||||
--color-amber-75: 48 32 11;
|
||||
--color-amber-100: 57 39 15;
|
||||
--color-amber-200: 67 46 18;
|
||||
--color-amber-300: 83 57 22;
|
||||
--color-amber-400: 111 77 29;
|
||||
--color-amber-500: 169 118 42;
|
||||
--color-amber-600: 169 118 42;
|
||||
--color-amber-700: 255 203 71;
|
||||
--color-amber-800: 255 204 77;
|
||||
--color-amber-900: 255 231 179;
|
||||
|
||||
--color-ash-100: 46 48 53;
|
||||
--color-ash-200: 53 55 60;
|
||||
--color-ash-25: 24 24 26;
|
||||
--color-ash-300: 60 63 68;
|
||||
--color-ash-400: 70 75 80;
|
||||
--color-ash-50: 27 27 31;
|
||||
--color-ash-500: 90 97 101;
|
||||
--color-ash-600: 105 110 119;
|
||||
--color-ash-700: 120 127 133;
|
||||
--color-ash-75: 39 40 45;
|
||||
--color-ash-800: 173 177 184;
|
||||
--color-ash-900: 237 238 240;
|
||||
|
||||
--color-primary-25: 10 17 28;
|
||||
--color-primary-50: 15 24 38;
|
||||
--color-primary-75: 15 39 72;
|
||||
--color-primary-100: 10 49 99;
|
||||
--color-primary-200: 18 61 117;
|
||||
--color-primary-300: 29 74 134;
|
||||
--color-primary-400: 40 89 156;
|
||||
--color-primary-500: 48 106 186;
|
||||
--color-primary-600: 39 129 246;
|
||||
--color-primary-700: 21 116 231;
|
||||
--color-primary-800: 126 182 255;
|
||||
--color-primary-900: 205 227 255;
|
||||
|
||||
--color-ruby-100: 78 19 37;
|
||||
--color-ruby-200: 94 26 46;
|
||||
--color-ruby-25: 25 17 19;
|
||||
--color-ruby-300: 111 37 57;
|
||||
--color-ruby-400: 136 52 71;
|
||||
--color-ruby-50: 30 21 23;
|
||||
--color-ruby-500: 179 68 90;
|
||||
--color-ruby-600: 229 70 102;
|
||||
--color-ruby-700: 236 90 114;
|
||||
--color-ruby-75: 58 20 30;
|
||||
--color-ruby-800: 255 148 157;
|
||||
--color-ruby-900: 254 210 225;
|
||||
|
||||
--color-teal-100: 2 59 55;
|
||||
--color-teal-200: 8 72 67;
|
||||
--color-teal-25: 13 21 20;
|
||||
--color-teal-300: 28 105 97;
|
||||
--color-teal-400: 28 105 97;
|
||||
--color-teal-50: 17 28 27;
|
||||
--color-teal-500: 32 126 115;
|
||||
--color-teal-600: 41 163 131;
|
||||
--color-teal-700: 14 179 158;
|
||||
--color-teal-75: 13 45 42;
|
||||
--color-teal-800: 11 216 182;
|
||||
--color-teal-900: 173 240 221;
|
||||
|
||||
--color-green-25: 14 21 18;
|
||||
--color-green-50: 18 27 23;
|
||||
--color-green-75: 19 45 33;
|
||||
--color-green-100: 17 59 41;
|
||||
--color-green-200: 23 73 51;
|
||||
--color-green-300: 32 87 62;
|
||||
--color-green-400: 40 104 74;
|
||||
--color-green-500: 47 124 87;
|
||||
--color-green-600: 48 164 108;
|
||||
--color-green-700: 51 176 116;
|
||||
--color-green-800: 61 214 140;
|
||||
--color-green-900: 177 241 203;
|
||||
|
||||
--color-mint-25: 14 21 21;
|
||||
--color-mint-50: 15 27 27;
|
||||
--color-mint-75: 9 44 43;
|
||||
--color-mint-100: 0 58 56;
|
||||
--color-mint-200: 0 71 68;
|
||||
--color-mint-300: 16 86 80;
|
||||
--color-mint-400: 30 104 95;
|
||||
--color-mint-500: 39 127 112;
|
||||
--color-mint-600: 134 234 212;
|
||||
--color-mint-700: 168 245 229;
|
||||
--color-mint-800: 88 213 186;
|
||||
--color-mint-900: 196 245 225;
|
||||
|
||||
--color-sky-25: 14 21 21;
|
||||
--color-sky-50: 15 27 27;
|
||||
--color-sky-75: 9 44 43;
|
||||
--color-sky-100: 0 58 56;
|
||||
--color-sky-200: 0 71 68;
|
||||
--color-sky-300: 16 86 80;
|
||||
--color-sky-400: 30 104 95;
|
||||
--color-sky-500: 39 127 112;
|
||||
--color-sky-600: 134 234 212;
|
||||
--color-sky-700: 168 245 229;
|
||||
--color-sky-800: 88 213 186;
|
||||
--color-sky-900: 196 245 225;
|
||||
|
||||
--color-indigo-25: 17 19 31;
|
||||
--color-indigo-50: 20 23 38;
|
||||
--color-indigo-75: 24 36 73;
|
||||
--color-indigo-100: 29 46 98;
|
||||
--color-indigo-200: 37 57 116;
|
||||
--color-indigo-300: 48 67 132;
|
||||
--color-indigo-400: 58 79 151;
|
||||
--color-indigo-500: 67 93 177;
|
||||
--color-indigo-600: 62 99 221;
|
||||
--color-indigo-700: 84 114 228;
|
||||
--color-indigo-800: 158 177 255;
|
||||
--color-indigo-900: 214 225 255;
|
||||
|
||||
--color-iris-25: 19 19 30;
|
||||
--color-iris-50: 23 22 37;
|
||||
--color-iris-75: 32 34 72;
|
||||
--color-iris-100: 38 42 101;
|
||||
--color-iris-200: 48 51 116;
|
||||
--color-iris-300: 61 62 130;
|
||||
--color-iris-400: 74 74 149;
|
||||
--color-iris-500: 89 88 177;
|
||||
--color-iris-600: 91 91 214;
|
||||
--color-iris-700: 110 106 222;
|
||||
--color-iris-800: 177 169 255;
|
||||
--color-iris-900: 224 223 254;
|
||||
|
||||
--color-violet-25: 20 18 31;
|
||||
--color-violet-50: 27 21 37;
|
||||
--color-violet-75: 41 31 67;
|
||||
--color-violet-100: 51 37 91;
|
||||
--color-violet-200: 60 46 105;
|
||||
--color-violet-300: 71 56 118;
|
||||
--color-violet-400: 86 70 139;
|
||||
--color-violet-500: 105 88 173;
|
||||
--color-violet-600: 110 86 207;
|
||||
--color-violet-700: 125 102 217;
|
||||
--color-violet-800: 186 167 255;
|
||||
--color-violet-900: 226 221 254;
|
||||
|
||||
--color-pink-25: 25 17 23;
|
||||
--color-pink-50: 33 18 29;
|
||||
--color-pink-75: 55 23 47;
|
||||
--color-pink-100: 75 20 61;
|
||||
--color-pink-200: 89 28 71;
|
||||
--color-pink-300: 105 41 85;
|
||||
--color-pink-400: 131 56 105;
|
||||
--color-pink-500: 168 72 133;
|
||||
--color-pink-600: 214 64 159;
|
||||
--color-pink-700: 222 81 168;
|
||||
--color-pink-800: 255 141 204;
|
||||
--color-pink-900: 253 209 234;
|
||||
--color-orange-25: 23 18 14;
|
||||
--color-orange-50: 30 22 15;
|
||||
--color-orange-75: 51 30 11;
|
||||
--color-orange-100: 70 33 0;
|
||||
--color-orange-200: 86 40 0;
|
||||
--color-orange-300: 102 53 12;
|
||||
--color-orange-400: 126 69 29;
|
||||
--color-orange-500: 163 88 41;
|
||||
--color-orange-600: 247 107 21;
|
||||
--color-orange-700: 255 128 31;
|
||||
--color-orange-800: 255 160 87;
|
||||
--color-orange-900: 255 224 194;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
/* Hide scrollbar for Chrome, Safari and Opera */
|
||||
.no-scrollbar::-webkit-scrollbar {
|
||||
|
||||
+8
-8
@@ -30,11 +30,11 @@
|
||||
|
||||
.mx-input:disabled,
|
||||
.mx-input[readonly] {
|
||||
@apply bg-white dark:bg-slate-900 cursor-pointer;
|
||||
@apply bg-n-background cursor-pointer;
|
||||
}
|
||||
|
||||
.mx-icon-calendar {
|
||||
@apply dark:text-slate-500;
|
||||
@apply text-n-slate-10;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,17 +43,17 @@
|
||||
|
||||
.cell {
|
||||
&.disabled {
|
||||
@apply bg-slate-25 dark:bg-slate-900 text-slate-200 dark:text-slate-300;
|
||||
@apply bg-n-slate-2 dark:bg-n-background text-n-slate-10;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&.hover-in-range,
|
||||
&.in-range {
|
||||
@apply bg-slate-75 dark:bg-slate-700 text-slate-900 dark:text-slate-100;
|
||||
@apply bg-n-slate-3 dark:bg-n-solid-3 text-n-slate-12;
|
||||
}
|
||||
}
|
||||
|
||||
.mx-calendar+.mx-calendar {
|
||||
.mx-calendar + .mx-calendar {
|
||||
@apply border-l border-n-weak;
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
}
|
||||
|
||||
.mx-time {
|
||||
@apply border-0 bg-white dark:bg-slate-800;
|
||||
@apply border-0 bg-n-background dark:bg-n-solid-2;
|
||||
|
||||
.mx-time-header {
|
||||
@apply border-0;
|
||||
@@ -70,11 +70,11 @@
|
||||
|
||||
.mx-time-item {
|
||||
&.disabled {
|
||||
@apply bg-slate-25 dark:bg-slate-900;
|
||||
@apply bg-n-slate-2 dark:bg-n-background;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@apply bg-slate-75 dark:bg-slate-700;
|
||||
@apply bg-n-slate-3 dark:bg-n-solid-3;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
.dropdown-pane {
|
||||
@apply border rounded-lg hidden relative invisible shadow-lg border-n-strong dark:border-n-strong box-content p-2 w-fit z-[9999];
|
||||
|
||||
&.dropdown-pane--open {
|
||||
@apply bg-n-alpha-3 backdrop-blur-[100px] absolute block visible;
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,10 @@
|
||||
@apply mb-4;
|
||||
}
|
||||
|
||||
&.invalid .multiselect__tags {
|
||||
@apply border-0 outline outline-1 outline-n-ruby-8 dark:outline-n-ruby-8 hover:outline-n-ruby-9 dark:hover:outline-n-ruby-9 disabled:outline-n-ruby-8 dark:disabled:outline-n-ruby-8;
|
||||
}
|
||||
|
||||
&.multiselect--disabled {
|
||||
@apply opacity-50 rounded-lg cursor-not-allowed pointer-events-auto;
|
||||
|
||||
@@ -124,8 +128,7 @@
|
||||
}
|
||||
|
||||
.multiselect__input {
|
||||
@include ghost-input;
|
||||
@apply text-sm h-[2.875rem] mb-0 p-0;
|
||||
@apply text-sm h-[2.875rem] mb-0 p-0 shadow-none border-transparent hover:border-transparent hover:shadow-none focus:border-transparent focus:shadow-none active:border-transparent active:shadow-none;
|
||||
}
|
||||
|
||||
.multiselect__single {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
// to be removed
|
||||
@@ -1 +0,0 @@
|
||||
// to be removed
|
||||
@@ -1,261 +0,0 @@
|
||||
// scss-lint:disable MergeableSelector
|
||||
|
||||
@tailwind utilities;
|
||||
@layer utilities {
|
||||
.custom-gradient {
|
||||
background-image: linear-gradient(
|
||||
-180deg,
|
||||
transparent 3%,
|
||||
rgb(76 81 85) 130%
|
||||
);
|
||||
}
|
||||
|
||||
.bubble-with-types {
|
||||
@apply py-2 text-sm font-normal bg-woot-500 dark:bg-woot-500 relative px-4 m-0 text-white dark:text-white;
|
||||
|
||||
.message-text__wrap {
|
||||
@apply relative;
|
||||
|
||||
.link {
|
||||
@apply text-white dark:text-white underline;
|
||||
}
|
||||
}
|
||||
|
||||
.image,
|
||||
.video {
|
||||
@apply cursor-pointer relative;
|
||||
|
||||
.modal-container {
|
||||
@apply text-center;
|
||||
}
|
||||
|
||||
.modal-image {
|
||||
@apply max-h-[76vh] max-w-[76vw];
|
||||
}
|
||||
|
||||
.modal-video {
|
||||
@apply max-h-[76vh] max-w-[76vw];
|
||||
}
|
||||
|
||||
&::before {
|
||||
@apply custom-gradient bottom-0 h-[20%] content-[''] left-0 absolute w-full opacity-80;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.conversation-panel {
|
||||
@apply flex-shrink flex-grow basis-px flex flex-col overflow-y-auto relative h-full m-0 pb-4;
|
||||
}
|
||||
|
||||
.conversation-panel > li {
|
||||
@apply flex flex-shrink-0 flex-grow-0 flex-auto max-w-full mt-0 mr-0 mb-1 ml-0 relative first:mt-auto last:mb-0;
|
||||
|
||||
&.unread--toast {
|
||||
+ .right {
|
||||
@apply mb-1;
|
||||
}
|
||||
|
||||
+ .left {
|
||||
@apply mb-0;
|
||||
}
|
||||
|
||||
span {
|
||||
@apply shadow-lg rounded-full bg-woot-500 dark:bg-woot-500 text-white dark:text-white text-xs font-medium my-2.5 mx-auto px-2.5 py-1.5;
|
||||
}
|
||||
}
|
||||
|
||||
.bubble {
|
||||
@apply bubble-with-types text-left break-words;
|
||||
|
||||
.aplayer {
|
||||
@apply shadow-none;
|
||||
font-family: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
&.left {
|
||||
.bubble {
|
||||
@apply rounded-r-lg rounded-l mr-auto break-words;
|
||||
|
||||
&:not(.is-unsupported) {
|
||||
@apply border border-slate-50 dark:border-slate-700 bg-white dark:bg-slate-700 text-black-900 dark:text-slate-50;
|
||||
}
|
||||
|
||||
&.is-image {
|
||||
@apply rounded-lg;
|
||||
}
|
||||
|
||||
.link {
|
||||
@apply text-woot-600 dark:text-woot-600;
|
||||
}
|
||||
|
||||
.file {
|
||||
.attachment-name {
|
||||
@apply text-slate-700 dark:text-woot-300;
|
||||
}
|
||||
|
||||
.icon-wrap {
|
||||
@apply text-woot-600 dark:text-woot-600;
|
||||
}
|
||||
|
||||
.download {
|
||||
@apply text-woot-600 dark:text-woot-600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+ .right {
|
||||
@apply mt-2.5;
|
||||
|
||||
.bubble {
|
||||
@apply rounded-tr-lg;
|
||||
}
|
||||
}
|
||||
|
||||
+ .unread--toast {
|
||||
+ .right {
|
||||
@apply mt-2.5;
|
||||
|
||||
.bubble {
|
||||
@apply rounded-tr-lg;
|
||||
}
|
||||
}
|
||||
|
||||
+ .left {
|
||||
@apply mt-0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.right {
|
||||
@apply justify-end;
|
||||
|
||||
.wrap {
|
||||
@apply flex items-end mr-4 text-right;
|
||||
|
||||
.sender--info {
|
||||
@apply pt-2 pb-1 pr-0 pl-2;
|
||||
}
|
||||
}
|
||||
|
||||
.bubble {
|
||||
@apply ml-auto break-words rounded-l-lg rounded-r;
|
||||
|
||||
&.is-private {
|
||||
@apply text-black-900 dark:text-white relative border border-solid bg-yellow-100 dark:bg-yellow-700 border-yellow-200 dark:border-yellow-600/25;
|
||||
|
||||
blockquote {
|
||||
@apply border-slate-400 dark:border-slate-400 text-slate-800 dark:text-slate-300;
|
||||
|
||||
p {
|
||||
@apply text-slate-600 dark:text-slate-300;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-image {
|
||||
@apply rounded-lg;
|
||||
|
||||
.message__mail-head {
|
||||
@apply px-4 py-2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+ .left {
|
||||
@apply mt-2.5;
|
||||
|
||||
.bubble {
|
||||
@apply rounded-tl-lg;
|
||||
}
|
||||
}
|
||||
|
||||
+ .unread--toast {
|
||||
+ .left {
|
||||
@apply rounded-lg;
|
||||
|
||||
.bubble {
|
||||
@apply rounded-tl-lg;
|
||||
}
|
||||
}
|
||||
|
||||
+ .right {
|
||||
@apply mt-0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.center {
|
||||
@apply items-center justify-center;
|
||||
}
|
||||
|
||||
.wrap {
|
||||
max-width: Min(31rem, 84%);
|
||||
@apply my-0 mx-4;
|
||||
|
||||
.sender--name {
|
||||
@apply text-xs mb-1;
|
||||
}
|
||||
}
|
||||
|
||||
.sender--thumbnail {
|
||||
@apply h-3 mr-3 mt-0.5 w-3 rounded-full;
|
||||
}
|
||||
|
||||
.activity-wrap {
|
||||
@apply flex justify-center text-sm my-1 mx-0 py-1 pr-0.5 pl-2.5 bg-slate-50 dark:bg-slate-600 text-slate-800 dark:text-slate-100 rounded-md border border-slate-100 dark:border-slate-600 border-solid;
|
||||
|
||||
.is-text {
|
||||
@apply inline-flex items-center text-start 2xl:flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.activity-wrap .message-text__wrap {
|
||||
.text-content p {
|
||||
@apply mb-0;
|
||||
}
|
||||
}
|
||||
|
||||
.conversation-footer {
|
||||
@apply flex relative flex-col;
|
||||
}
|
||||
|
||||
.left .bubble .text-content {
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
@apply text-slate-800 dark:text-slate-100;
|
||||
}
|
||||
|
||||
a {
|
||||
@apply text-woot-500 dark:text-woot-500 underline;
|
||||
}
|
||||
|
||||
p:last-child {
|
||||
@apply mb-0;
|
||||
}
|
||||
}
|
||||
|
||||
.right .bubble .text-content {
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
@apply text-white dark:text-white;
|
||||
}
|
||||
|
||||
a {
|
||||
@apply text-white dark:text-white underline;
|
||||
}
|
||||
|
||||
p:last-child {
|
||||
@apply mb-0;
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
.tabs--container {
|
||||
@apply flex;
|
||||
}
|
||||
|
||||
.tabs--container--with-border {
|
||||
@apply border-b border-b-n-weak;
|
||||
}
|
||||
|
||||
.tabs--container--compact.tab--chat-type {
|
||||
.tabs-title {
|
||||
a {
|
||||
@apply py-2 text-sm;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tabs {
|
||||
@apply border-r-0 border-l-0 border-t-0 flex min-w-[6.25rem] py-0 px-4 list-none mb-0;
|
||||
}
|
||||
|
||||
.tabs--with-scroll {
|
||||
@apply overflow-hidden py-0 px-1;
|
||||
max-width: calc(100% - 64px);
|
||||
}
|
||||
|
||||
.tabs--scroll-button {
|
||||
@apply items-center rounded-none cursor-pointer flex h-auto justify-center min-w-[2rem];
|
||||
}
|
||||
|
||||
// Tab chat type
|
||||
.tab--chat-type {
|
||||
@apply flex;
|
||||
|
||||
.tabs-title {
|
||||
a {
|
||||
@apply text-base font-medium py-3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tabs-title {
|
||||
@apply flex-shrink-0 my-0 mx-2;
|
||||
|
||||
.badge {
|
||||
@apply bg-n-alpha-black2 dark:bg-n-solid-3 rounded-md text-n-slate-11 h-5 flex items-center justify-center text-xxs font-semibold my-0 mx-1 px-1 py-0;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
@apply ml-0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
@apply mr-0;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
a {
|
||||
@apply text-n-slate-12;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
@apply flex items-center flex-row border-b py-2.5 select-none cursor-pointer border-transparent text-n-slate-11 text-sm top-[1px] relative;
|
||||
transition: border-color 0.15s $swift-ease-out-function;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
a {
|
||||
@apply border-b border-n-brand text-n-blue-text;
|
||||
}
|
||||
|
||||
.badge {
|
||||
@apply bg-n-brand/10 dark:bg-n-brand/20 text-n-blue-text;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
table {
|
||||
@apply border-spacing-0 text-sm w-full;
|
||||
|
||||
|
||||
}
|
||||
|
||||
.woot-table {
|
||||
thead {
|
||||
th {
|
||||
@apply font-semibold tracking-[1px] text-left px-2.5 uppercase text-slate-900 dark:text-slate-200;
|
||||
}
|
||||
}
|
||||
|
||||
tbody {
|
||||
tr {
|
||||
@apply border-b border-slate-50 dark:border-slate-800/30;
|
||||
}
|
||||
|
||||
td {
|
||||
@apply p-2.5 text-slate-700 dark:text-slate-100;
|
||||
}
|
||||
}
|
||||
|
||||
tr {
|
||||
.show-if-hover {
|
||||
transition: opacity 0.2s $swift-ease-out-function;
|
||||
@apply opacity-0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.show-if-hover {
|
||||
@apply opacity-100;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.agent-name {
|
||||
@apply block font-medium capitalize;
|
||||
}
|
||||
|
||||
.woot-thumbnail {
|
||||
@apply rounded-full h-[3.125rem] w-[3.125rem];
|
||||
}
|
||||
|
||||
.button-wrapper {
|
||||
@apply flex justify-start flex-row min-w-[12.5rem] gap-1;
|
||||
}
|
||||
|
||||
.button {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.ve-table {
|
||||
.ve-table-container.ve-table-border-around {
|
||||
@apply border-slate-200 dark:border-slate-700;
|
||||
}
|
||||
|
||||
.ve-table-content {
|
||||
.ve-table-header .ve-table-header-tr .ve-table-header-th {
|
||||
@apply bg-slate-50 dark:bg-slate-800 text-slate-800 dark:text-slate-100 border-slate-100 dark:border-slate-700/50;
|
||||
}
|
||||
|
||||
.ve-table-body .ve-table-body-tr .ve-table-body-td {
|
||||
@apply bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-100 border-slate-75 dark:border-slate-800;
|
||||
}
|
||||
|
||||
.ve-table-body.ve-table-row-hover .ve-table-body-tr:hover td {
|
||||
@apply bg-slate-50 dark:bg-slate-700 text-slate-800 dark:text-slate-100;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table-pagination {
|
||||
.ve-pagination-total {
|
||||
@apply text-slate-600 dark:text-slate-200;
|
||||
}
|
||||
|
||||
.ve-pagination-goto {
|
||||
@apply text-slate-600 dark:text-slate-200;
|
||||
|
||||
.ve-pagination-goto-input {
|
||||
@apply bg-white dark:bg-slate-900 text-slate-600 dark:text-slate-200;
|
||||
}
|
||||
}
|
||||
|
||||
.ve-pagination-li {
|
||||
@apply bg-white dark:bg-slate-900 text-slate-600 dark:text-slate-200 border-slate-75 dark:border-slate-700;
|
||||
}
|
||||
}
|
||||
@@ -76,8 +76,8 @@ const campaignStatus = computed(() => {
|
||||
const inboxName = computed(() => props.inbox?.name || '');
|
||||
|
||||
const inboxIcon = computed(() => {
|
||||
const { phone_number: phoneNumber, channel_type: type } = props.inbox;
|
||||
return getInboxIconByType(type, phoneNumber);
|
||||
const { medium, channel_type: type } = props.inbox;
|
||||
return getInboxIconByType(type, medium);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
<script setup>
|
||||
import { ONE_OFF_CAMPAIGN_EMPTY_STATE_CONTENT } from './CampaignEmptyStateContent';
|
||||
|
||||
import EmptyStateLayout from 'dashboard/components-next/EmptyStateLayout.vue';
|
||||
import CampaignCard from 'dashboard/components-next/Campaigns/CampaignCard/CampaignCard.vue';
|
||||
|
||||
defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
subtitle: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EmptyStateLayout :title="title" :subtitle="subtitle">
|
||||
<template #empty-state-item>
|
||||
<div class="flex flex-col gap-4 p-px">
|
||||
<CampaignCard
|
||||
v-for="campaign in ONE_OFF_CAMPAIGN_EMPTY_STATE_CONTENT"
|
||||
:key="campaign.id"
|
||||
:title="campaign.title"
|
||||
:message="campaign.message"
|
||||
:is-enabled="campaign.enabled"
|
||||
:status="campaign.campaign_status"
|
||||
:sender="campaign.sender"
|
||||
:inbox="campaign.inbox"
|
||||
:scheduled-at="campaign.scheduled_at"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</EmptyStateLayout>
|
||||
</template>
|
||||
+2
-2
@@ -40,9 +40,9 @@ const handleSubmit = campaignDetails => {
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="w-[25rem] z-50 min-w-0 absolute top-10 ltr:right-0 rtl:left-0 bg-n-alpha-3 backdrop-blur-[100px] p-6 rounded-xl border border-slate-50 dark:border-slate-900 shadow-md flex flex-col gap-6 max-h-[85vh] overflow-y-auto"
|
||||
class="w-[25rem] z-50 min-w-0 absolute top-10 ltr:right-0 rtl:left-0 bg-n-alpha-3 backdrop-blur-[100px] p-6 rounded-xl border border-n-weak shadow-md flex flex-col gap-6 max-h-[85vh] overflow-y-auto"
|
||||
>
|
||||
<h3 class="text-base font-medium text-slate-900 dark:text-slate-50">
|
||||
<h3 class="text-base font-medium text-n-slate-12">
|
||||
{{ t(`CAMPAIGN.LIVE_CHAT.CREATE.TITLE`) }}
|
||||
</h3>
|
||||
<LiveChatCampaignForm
|
||||
|
||||
+1
-1
@@ -306,7 +306,7 @@ defineExpose({ prepareCampaignDetails, isSubmitDisabled });
|
||||
variant="faded"
|
||||
color="slate"
|
||||
:label="t('CAMPAIGN.LIVE_CHAT.CREATE.FORM.BUTTONS.CANCEL')"
|
||||
class="w-full bg-n-alpha-2 n-blue-text hover:bg-n-alpha-3"
|
||||
class="w-full bg-n-alpha-2 text-n-blue-text hover:bg-n-alpha-3"
|
||||
@click="handleCancel"
|
||||
/>
|
||||
<Button
|
||||
|
||||
+2
-2
@@ -39,9 +39,9 @@ const handleClose = () => emit('close');
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="w-[25rem] z-50 min-w-0 absolute top-10 ltr:right-0 rtl:left-0 bg-n-alpha-3 backdrop-blur-[100px] p-6 rounded-xl border border-slate-50 dark:border-slate-900 shadow-md flex flex-col gap-6"
|
||||
class="w-[25rem] z-50 min-w-0 absolute top-10 ltr:right-0 rtl:left-0 bg-n-alpha-3 backdrop-blur-[100px] p-6 rounded-xl border border-n-weak shadow-md flex flex-col gap-6"
|
||||
>
|
||||
<h3 class="text-base font-medium text-slate-900 dark:text-slate-50">
|
||||
<h3 class="text-base font-medium text-n-slate-12">
|
||||
{{ t(`CAMPAIGN.SMS.CREATE.TITLE`) }}
|
||||
</h3>
|
||||
<SMSCampaignForm @submit="handleSubmit" @cancel="handleClose" />
|
||||
|
||||
+1
-1
@@ -174,7 +174,7 @@ const handleSubmit = async () => {
|
||||
color="slate"
|
||||
type="button"
|
||||
:label="t('CAMPAIGN.SMS.CREATE.FORM.BUTTONS.CANCEL')"
|
||||
class="w-full bg-n-alpha-2 n-blue-text hover:bg-n-alpha-3"
|
||||
class="w-full bg-n-alpha-2 text-n-blue-text hover:bg-n-alpha-3"
|
||||
@click="handleCancel"
|
||||
/>
|
||||
<Button
|
||||
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
<script setup>
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useStore } from 'dashboard/composables/store';
|
||||
import { useAlert, useTrack } from 'dashboard/composables';
|
||||
import { CAMPAIGN_TYPES } from 'shared/constants/campaign.js';
|
||||
import { CAMPAIGNS_EVENTS } from 'dashboard/helper/AnalyticsHelper/events.js';
|
||||
|
||||
import WhatsAppCampaignForm from 'dashboard/components-next/Campaigns/Pages/CampaignPage/WhatsAppCampaign/WhatsAppCampaignForm.vue';
|
||||
|
||||
const emit = defineEmits(['close']);
|
||||
|
||||
const store = useStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
const addCampaign = async campaignDetails => {
|
||||
try {
|
||||
await store.dispatch('campaigns/create', campaignDetails);
|
||||
|
||||
useTrack(CAMPAIGNS_EVENTS.CREATE_CAMPAIGN, {
|
||||
type: CAMPAIGN_TYPES.ONE_OFF,
|
||||
});
|
||||
|
||||
useAlert(t('CAMPAIGN.WHATSAPP.CREATE.FORM.API.SUCCESS_MESSAGE'));
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error?.response?.message ||
|
||||
t('CAMPAIGN.WHATSAPP.CREATE.FORM.API.ERROR_MESSAGE');
|
||||
useAlert(errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = campaignDetails => {
|
||||
addCampaign(campaignDetails);
|
||||
};
|
||||
|
||||
const handleClose = () => emit('close');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="w-[25rem] z-50 min-w-0 absolute top-10 ltr:right-0 rtl:left-0 bg-n-alpha-3 backdrop-blur-[100px] rounded-xl border border-n-weak shadow-md max-h-[80vh] overflow-y-auto"
|
||||
>
|
||||
<div class="p-6 flex flex-col gap-6">
|
||||
<h3 class="text-base font-medium text-n-slate-12 flex-shrink-0">
|
||||
{{ t(`CAMPAIGN.WHATSAPP.CREATE.TITLE`) }}
|
||||
</h3>
|
||||
<WhatsAppCampaignForm @submit="handleSubmit" @cancel="handleClose" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
+266
@@ -0,0 +1,266 @@
|
||||
<script setup>
|
||||
import { reactive, computed, watch, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { required, minLength } from '@vuelidate/validators';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
|
||||
import Input from 'dashboard/components-next/input/Input.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import ComboBox from 'dashboard/components-next/combobox/ComboBox.vue';
|
||||
import TagMultiSelectComboBox from 'dashboard/components-next/combobox/TagMultiSelectComboBox.vue';
|
||||
import WhatsAppTemplateParser from 'dashboard/components-next/whatsapp/WhatsAppTemplateParser.vue';
|
||||
|
||||
const emit = defineEmits(['submit', 'cancel']);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const formState = {
|
||||
uiFlags: useMapGetter('campaigns/getUIFlags'),
|
||||
labels: useMapGetter('labels/getLabels'),
|
||||
inboxes: useMapGetter('inboxes/getWhatsAppInboxes'),
|
||||
getFilteredWhatsAppTemplates: useMapGetter(
|
||||
'inboxes/getFilteredWhatsAppTemplates'
|
||||
),
|
||||
};
|
||||
|
||||
const initialState = {
|
||||
title: '',
|
||||
inboxId: null,
|
||||
templateId: null,
|
||||
scheduledAt: null,
|
||||
selectedAudience: [],
|
||||
};
|
||||
|
||||
const state = reactive({ ...initialState });
|
||||
const templateParserRef = ref(null);
|
||||
|
||||
const rules = {
|
||||
title: { required, minLength: minLength(1) },
|
||||
inboxId: { required },
|
||||
templateId: { required },
|
||||
scheduledAt: { required },
|
||||
selectedAudience: { required },
|
||||
};
|
||||
|
||||
const v$ = useVuelidate(rules, state);
|
||||
|
||||
const isCreating = computed(() => formState.uiFlags.value.isCreating);
|
||||
|
||||
const currentDateTime = computed(() => {
|
||||
// Added to disable the scheduled at field from being set to the current time
|
||||
const now = new Date();
|
||||
const localTime = new Date(now.getTime() - now.getTimezoneOffset() * 60000);
|
||||
return localTime.toISOString().slice(0, 16);
|
||||
});
|
||||
|
||||
const mapToOptions = (items, valueKey, labelKey) =>
|
||||
items?.map(item => ({
|
||||
value: item[valueKey],
|
||||
label: item[labelKey],
|
||||
})) ?? [];
|
||||
|
||||
const audienceList = computed(() =>
|
||||
mapToOptions(formState.labels.value, 'id', 'title')
|
||||
);
|
||||
|
||||
const inboxOptions = computed(() =>
|
||||
mapToOptions(formState.inboxes.value, 'id', 'name')
|
||||
);
|
||||
|
||||
const templateOptions = computed(() => {
|
||||
if (!state.inboxId) return [];
|
||||
const templates = formState.getFilteredWhatsAppTemplates.value(state.inboxId);
|
||||
return templates.map(template => {
|
||||
// Create a more user-friendly label from template name
|
||||
const friendlyName = template.name
|
||||
.replace(/_/g, ' ')
|
||||
.replace(/\b\w/g, l => l.toUpperCase());
|
||||
|
||||
return {
|
||||
value: template.id,
|
||||
label: `${friendlyName} (${template.language || 'en'})`,
|
||||
template: template,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
const selectedTemplate = computed(() => {
|
||||
if (!state.templateId) return null;
|
||||
return templateOptions.value.find(option => option.value === state.templateId)
|
||||
?.template;
|
||||
});
|
||||
|
||||
const getErrorMessage = (field, errorKey) => {
|
||||
const baseKey = 'CAMPAIGN.WHATSAPP.CREATE.FORM';
|
||||
return v$.value[field].$error ? t(`${baseKey}.${errorKey}.ERROR`) : '';
|
||||
};
|
||||
|
||||
const formErrors = computed(() => ({
|
||||
title: getErrorMessage('title', 'TITLE'),
|
||||
inbox: getErrorMessage('inboxId', 'INBOX'),
|
||||
template: getErrorMessage('templateId', 'TEMPLATE'),
|
||||
scheduledAt: getErrorMessage('scheduledAt', 'SCHEDULED_AT'),
|
||||
audience: getErrorMessage('selectedAudience', 'AUDIENCE'),
|
||||
}));
|
||||
|
||||
const hasRequiredTemplateParams = computed(() => {
|
||||
return templateParserRef.value?.v$?.$invalid === false || true;
|
||||
});
|
||||
|
||||
const isSubmitDisabled = computed(
|
||||
() => v$.value.$invalid || !hasRequiredTemplateParams.value
|
||||
);
|
||||
|
||||
const formatToUTCString = localDateTime =>
|
||||
localDateTime ? new Date(localDateTime).toISOString() : null;
|
||||
|
||||
const resetState = () => {
|
||||
Object.assign(state, initialState);
|
||||
v$.value.$reset();
|
||||
};
|
||||
|
||||
const handleCancel = () => emit('cancel');
|
||||
|
||||
const prepareCampaignDetails = () => {
|
||||
// Find the selected template to get its content
|
||||
const currentTemplate = selectedTemplate.value;
|
||||
const parserData = templateParserRef.value;
|
||||
|
||||
// Extract template content - this should be the template message body
|
||||
const templateContent = parserData?.renderedTemplate || '';
|
||||
|
||||
// Prepare template_params object with the same structure as used in contacts
|
||||
const templateParams = {
|
||||
name: currentTemplate?.name || '',
|
||||
namespace: currentTemplate?.namespace || '',
|
||||
category: currentTemplate?.category || 'UTILITY',
|
||||
language: currentTemplate?.language || 'en_US',
|
||||
processed_params: parserData?.processedParams || {},
|
||||
};
|
||||
|
||||
return {
|
||||
title: state.title,
|
||||
message: templateContent,
|
||||
template_params: templateParams,
|
||||
inbox_id: state.inboxId,
|
||||
scheduled_at: formatToUTCString(state.scheduledAt),
|
||||
audience: state.selectedAudience?.map(id => ({
|
||||
id,
|
||||
type: 'Label',
|
||||
})),
|
||||
};
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const isFormValid = await v$.value.$validate();
|
||||
if (!isFormValid) return;
|
||||
|
||||
emit('submit', prepareCampaignDetails());
|
||||
resetState();
|
||||
handleCancel();
|
||||
};
|
||||
|
||||
// Reset template selection when inbox changes
|
||||
watch(
|
||||
() => state.inboxId,
|
||||
() => {
|
||||
state.templateId = null;
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form class="flex flex-col gap-4" @submit.prevent="handleSubmit">
|
||||
<Input
|
||||
v-model="state.title"
|
||||
:label="t('CAMPAIGN.WHATSAPP.CREATE.FORM.TITLE.LABEL')"
|
||||
:placeholder="t('CAMPAIGN.WHATSAPP.CREATE.FORM.TITLE.PLACEHOLDER')"
|
||||
:message="formErrors.title"
|
||||
:message-type="formErrors.title ? 'error' : 'info'"
|
||||
/>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="inbox" class="mb-0.5 text-sm font-medium text-n-slate-12">
|
||||
{{ t('CAMPAIGN.WHATSAPP.CREATE.FORM.INBOX.LABEL') }}
|
||||
</label>
|
||||
<ComboBox
|
||||
id="inbox"
|
||||
v-model="state.inboxId"
|
||||
:options="inboxOptions"
|
||||
:has-error="!!formErrors.inbox"
|
||||
:placeholder="t('CAMPAIGN.WHATSAPP.CREATE.FORM.INBOX.PLACEHOLDER')"
|
||||
:message="formErrors.inbox"
|
||||
class="[&>div>button]:bg-n-alpha-black2 [&>div>button:not(.focused)]:dark:outline-n-weak [&>div>button:not(.focused)]:hover:!outline-n-slate-6"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="template" class="mb-0.5 text-sm font-medium text-n-slate-12">
|
||||
{{ t('CAMPAIGN.WHATSAPP.CREATE.FORM.TEMPLATE.LABEL') }}
|
||||
</label>
|
||||
<ComboBox
|
||||
id="template"
|
||||
v-model="state.templateId"
|
||||
:options="templateOptions"
|
||||
:has-error="!!formErrors.template"
|
||||
:placeholder="t('CAMPAIGN.WHATSAPP.CREATE.FORM.TEMPLATE.PLACEHOLDER')"
|
||||
:message="formErrors.template"
|
||||
class="[&>div>button]:bg-n-alpha-black2 [&>div>button:not(.focused)]:dark:outline-n-weak [&>div>button:not(.focused)]:hover:!outline-n-slate-6"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-n-slate-11">
|
||||
{{ t('CAMPAIGN.WHATSAPP.CREATE.FORM.TEMPLATE.INFO') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Template Parser -->
|
||||
<WhatsAppTemplateParser
|
||||
v-if="selectedTemplate"
|
||||
ref="templateParserRef"
|
||||
:template="selectedTemplate"
|
||||
/>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="audience" class="mb-0.5 text-sm font-medium text-n-slate-12">
|
||||
{{ t('CAMPAIGN.WHATSAPP.CREATE.FORM.AUDIENCE.LABEL') }}
|
||||
</label>
|
||||
<TagMultiSelectComboBox
|
||||
v-model="state.selectedAudience"
|
||||
:options="audienceList"
|
||||
:label="t('CAMPAIGN.WHATSAPP.CREATE.FORM.AUDIENCE.LABEL')"
|
||||
:placeholder="t('CAMPAIGN.WHATSAPP.CREATE.FORM.AUDIENCE.PLACEHOLDER')"
|
||||
:has-error="!!formErrors.audience"
|
||||
:message="formErrors.audience"
|
||||
class="[&>div>button]:bg-n-alpha-black2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
v-model="state.scheduledAt"
|
||||
:label="t('CAMPAIGN.WHATSAPP.CREATE.FORM.SCHEDULED_AT.LABEL')"
|
||||
type="datetime-local"
|
||||
:min="currentDateTime"
|
||||
:placeholder="t('CAMPAIGN.WHATSAPP.CREATE.FORM.SCHEDULED_AT.PLACEHOLDER')"
|
||||
:message="formErrors.scheduledAt"
|
||||
:message-type="formErrors.scheduledAt ? 'error' : 'info'"
|
||||
/>
|
||||
|
||||
<div class="flex gap-3 justify-between items-center w-full">
|
||||
<Button
|
||||
variant="faded"
|
||||
color="slate"
|
||||
type="button"
|
||||
:label="t('CAMPAIGN.WHATSAPP.CREATE.FORM.BUTTONS.CANCEL')"
|
||||
class="w-full bg-n-alpha-2 text-n-blue-text hover:bg-n-alpha-3"
|
||||
@click="handleCancel"
|
||||
/>
|
||||
<Button
|
||||
:label="t('CAMPAIGN.WHATSAPP.CREATE.FORM.BUTTONS.CREATE')"
|
||||
class="w-full"
|
||||
type="submit"
|
||||
:is-loading="isCreating"
|
||||
:disabled="isCreating || isSubmitDisabled"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
@@ -98,6 +98,7 @@ const onClickViewDetails = () => emit('showContact', props.id);
|
||||
:src="thumbnail"
|
||||
:size="48"
|
||||
:status="availabilityStatus"
|
||||
hide-offline-status
|
||||
rounded-full
|
||||
/>
|
||||
<div class="flex flex-col gap-0.5 flex-1">
|
||||
|
||||
@@ -218,10 +218,13 @@ const resetForm = () => {
|
||||
Object.assign(state, defaultState);
|
||||
};
|
||||
|
||||
watch(() => props.contactData, prepareStateBasedOnProps, {
|
||||
immediate: true,
|
||||
deep: true,
|
||||
});
|
||||
watch(
|
||||
() => props.contactData?.id,
|
||||
id => {
|
||||
if (id) prepareStateBasedOnProps();
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// Expose state to parent component for avatar upload
|
||||
defineExpose({
|
||||
|
||||
@@ -130,7 +130,7 @@ const onMergeContacts = async () => {
|
||||
variant="faded"
|
||||
color="slate"
|
||||
:label="t('CONTACTS_LAYOUT.SIDEBAR.MERGE.BUTTONS.CANCEL')"
|
||||
class="w-full bg-n-alpha-2 n-blue-text hover:bg-n-alpha-3"
|
||||
class="w-full bg-n-alpha-2 text-n-blue-text hover:bg-n-alpha-3"
|
||||
@click="resetState"
|
||||
/>
|
||||
<Button
|
||||
|
||||
+2
-2
@@ -48,8 +48,8 @@ const inbox = computed(() => props.stateInbox);
|
||||
const inboxName = computed(() => inbox.value?.name);
|
||||
|
||||
const inboxIcon = computed(() => {
|
||||
const { phoneNumber, channelType } = inbox.value;
|
||||
return getInboxIconByType(channelType, phoneNumber);
|
||||
const { channelType, medium } = inbox.value;
|
||||
return getInboxIconByType(channelType, medium);
|
||||
});
|
||||
|
||||
const lastActivityAt = computed(() => {
|
||||
|
||||
@@ -20,6 +20,7 @@ const props = defineProps({
|
||||
enableVariables: { type: Boolean, default: false },
|
||||
enableCannedResponses: { type: Boolean, default: true },
|
||||
enabledMenuOptions: { type: Array, default: () => [] },
|
||||
enableCaptainTools: { type: Boolean, default: false },
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
@@ -35,7 +36,7 @@ const messageClass = computed(() => {
|
||||
case 'error':
|
||||
return 'text-n-ruby-9 dark:text-n-ruby-9';
|
||||
case 'success':
|
||||
return 'text-green-500 dark:text-green-400';
|
||||
return 'text-n-teal-10 dark:text-n-teal-10';
|
||||
default:
|
||||
return 'text-n-slate-11 dark:text-n-slate-11';
|
||||
}
|
||||
@@ -98,6 +99,7 @@ watch(
|
||||
:enable-variables="enableVariables"
|
||||
:enable-canned-responses="enableCannedResponses"
|
||||
:enabled-menu-options="enabledMenuOptions"
|
||||
:enable-captain-tools="enableCaptainTools"
|
||||
@input="handleInput"
|
||||
@focus="handleFocus"
|
||||
@blur="handleBlur"
|
||||
|
||||
@@ -35,12 +35,12 @@ defineProps({
|
||||
<div class="flex flex-col items-center justify-center gap-6">
|
||||
<div class="flex flex-col items-center justify-center gap-3">
|
||||
<h2
|
||||
class="text-3xl font-medium text-center text-slate-900 dark:text-white font-interDisplay"
|
||||
class="text-3xl font-medium text-center text-n-slate-12 font-interDisplay"
|
||||
>
|
||||
{{ title }}
|
||||
</h2>
|
||||
<p
|
||||
class="max-w-xl text-base text-center text-slate-600 dark:text-slate-300 font-interDisplay tracking-[0.3px]"
|
||||
class="max-w-xl text-base text-center text-n-slate-11 font-interDisplay tracking-[0.3px]"
|
||||
>
|
||||
{{ subtitle }}
|
||||
</p>
|
||||
|
||||
+1
-1
@@ -70,7 +70,7 @@ const category = {
|
||||
<div
|
||||
v-for="(article, index) in articles"
|
||||
:key="index"
|
||||
class="px-20 py-4 bg-white dark:bg-slate-900"
|
||||
class="px-20 py-4 bg-n-background"
|
||||
>
|
||||
<ArticleCard
|
||||
:id="article.id"
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ const categories = [
|
||||
<div
|
||||
v-for="(category, index) in categories"
|
||||
:key="index"
|
||||
class="px-20 py-4 bg-white dark:bg-slate-900"
|
||||
class="px-20 py-4 bg-n-background"
|
||||
>
|
||||
<CategoryCard
|
||||
:id="category.id"
|
||||
|
||||
@@ -121,11 +121,7 @@ const handleAction = ({ action, value }) => {
|
||||
</div>
|
||||
<span
|
||||
class="text-sm line-clamp-3"
|
||||
:class="
|
||||
hasDescription
|
||||
? 'text-slate-500 dark:text-slate-400'
|
||||
: 'text-slate-400 dark:text-slate-700'
|
||||
"
|
||||
:class="hasDescription ? 'text-n-slate-11' : 'text-n-slate-9'"
|
||||
>
|
||||
{{ description }}
|
||||
</span>
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ import ArticleEmptyState from './ArticleEmptyState.vue';
|
||||
: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">
|
||||
<div class="w-full h-full px-20 mx-auto bg-n-background">
|
||||
<ArticleEmptyState />
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ import PortalEmptyState from './PortalEmptyState.vue';
|
||||
: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">
|
||||
<div class="w-full h-full px-20 mx-auto bg-n-background">
|
||||
<PortalEmptyState />
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
@@ -14,7 +14,7 @@ const locales = [
|
||||
:layout="{ type: 'grid', width: '800px' }"
|
||||
>
|
||||
<Variant title="Locale Card">
|
||||
<div class="px-10 py-4 bg-white dark:bg-slate-900">
|
||||
<div class="px-10 py-4 bg-n-background">
|
||||
<div v-for="(locale, index) in locales" :key="index" class="px-20 py-2">
|
||||
<LocaleCard
|
||||
:locale="locale.name"
|
||||
|
||||
@@ -55,9 +55,7 @@ const handleAction = ({ action, value }) => {
|
||||
<CardLayout>
|
||||
<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"
|
||||
>
|
||||
<span class="text-sm font-medium text-n-slate-12 line-clamp-1">
|
||||
{{ locale }} ({{ localeCode }})
|
||||
</span>
|
||||
<span
|
||||
@@ -69,9 +67,7 @@ const handleAction = ({ action, value }) => {
|
||||
</div>
|
||||
<div class="flex items-center justify-end gap-4">
|
||||
<div class="flex items-center gap-4">
|
||||
<span
|
||||
class="text-sm text-slate-500 dark:text-slate-400 whitespace-nowrap"
|
||||
>
|
||||
<span class="text-sm text-n-slate-11 whitespace-nowrap">
|
||||
{{
|
||||
$t(
|
||||
'HELP_CENTER.LOCALES_PAGE.LOCALE_CARD.ARTICLES_COUNT',
|
||||
@@ -79,10 +75,8 @@ const handleAction = ({ action, value }) => {
|
||||
)
|
||||
}}
|
||||
</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"
|
||||
>
|
||||
<div class="w-px h-3 bg-n-weak" />
|
||||
<span class="text-sm text-n-slate-11 whitespace-nowrap">
|
||||
{{
|
||||
$t(
|
||||
'HELP_CENTER.LOCALES_PAGE.LOCALE_CARD.CATEGORIES_COUNT',
|
||||
|
||||
+6
-2
@@ -146,7 +146,7 @@ const previewArticle = () => {
|
||||
<style lang="scss" scoped>
|
||||
::v-deep {
|
||||
.ProseMirror .empty-node::before {
|
||||
@apply text-slate-200 dark:text-slate-500 text-base;
|
||||
@apply text-n-slate-10 text-base;
|
||||
}
|
||||
|
||||
.ProseMirror-menubar-wrapper {
|
||||
@@ -161,7 +161,7 @@ const previewArticle = () => {
|
||||
|
||||
.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;
|
||||
@apply h-8 rounded-lg !px-2 z-50 bg-n-solid-3 items-center gap-4 ml-0 mb-0 shadow-md outline outline-1 outline-n-weak;
|
||||
display: flex;
|
||||
top: var(--selection-top, auto) !important;
|
||||
left: var(--selection-left, 0) !important;
|
||||
@@ -180,6 +180,10 @@ const previewArticle = () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ProseMirror-menu-active {
|
||||
@apply bg-n-slate-3;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -206,7 +206,7 @@ onMounted(() => {
|
||||
/>
|
||||
</OnClickOutside>
|
||||
</div>
|
||||
<div class="w-px h-3 bg-slate-50 dark:bg-slate-800" />
|
||||
<div class="w-px h-3 bg-n-weak" />
|
||||
<div class="relative">
|
||||
<OnClickOutside @trigger="openCategoryList = false">
|
||||
<Button
|
||||
@@ -239,7 +239,7 @@ onMounted(() => {
|
||||
</OnClickOutside>
|
||||
</div>
|
||||
|
||||
<div class="w-px h-3 bg-slate-50 dark:bg-slate-800" />
|
||||
<div class="w-px h-3 bg-n-weak" />
|
||||
<div class="relative">
|
||||
<OnClickOutside @trigger="openProperties = false">
|
||||
<Button
|
||||
|
||||
+1
-1
@@ -128,7 +128,7 @@ const updateArticleStatus = async ({ value }) => {
|
||||
<div class="flex items-center gap-4">
|
||||
<span
|
||||
v-if="isUpdating || isSaved"
|
||||
class="text-xs font-medium transition-all duration-300 text-slate-500 dark:text-slate-400"
|
||||
class="text-xs font-medium transition-all duration-300 text-n-slate-11"
|
||||
>
|
||||
{{ statusText }}
|
||||
</span>
|
||||
|
||||
+1
-1
@@ -64,7 +64,7 @@ const articles = [
|
||||
<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">
|
||||
<div class="w-full min-h-screen bg-n-background">
|
||||
<ArticlesPage :articles="articles" />
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
+1
-1
@@ -201,7 +201,7 @@ const categories = [
|
||||
<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">
|
||||
<div class="w-full min-h-screen bg-n-background">
|
||||
<CategoriesPage :categories="categories" />
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
+2
-2
@@ -90,9 +90,9 @@ const handleCategory = async formData => {
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="w-[25rem] absolute top-10 ltr:right-0 rtl:left-0 bg-n-alpha-3 backdrop-blur-[100px] p-6 rounded-xl border border-slate-50 dark:border-slate-900 shadow-md flex flex-col gap-6"
|
||||
class="w-[25rem] absolute top-10 ltr:right-0 rtl:left-0 bg-n-alpha-3 backdrop-blur-[100px] p-6 rounded-xl border border-n-weak shadow-md flex flex-col gap-6"
|
||||
>
|
||||
<h3 class="text-base font-medium text-slate-900 dark:text-slate-50">
|
||||
<h3 class="text-base font-medium text-n-slate-12">
|
||||
{{
|
||||
t(
|
||||
`HELP_CENTER.CATEGORY_PAGE.CATEGORY_DIALOG.HEADER.${mode.toUpperCase()}`
|
||||
|
||||
+1
-1
@@ -246,7 +246,7 @@ defineExpose({ state, isSubmitDisabled });
|
||||
variant="faded"
|
||||
color="slate"
|
||||
:label="t('HELP_CENTER.CATEGORY_PAGE.CATEGORY_DIALOG.BUTTONS.CANCEL')"
|
||||
class="w-full bg-n-alpha-2 n-blue-text hover:bg-n-alpha-3"
|
||||
class="w-full bg-n-alpha-2 text-n-blue-text hover:bg-n-alpha-3"
|
||||
@click="handleCancel"
|
||||
/>
|
||||
<Button
|
||||
|
||||
+2
-4
@@ -147,10 +147,8 @@ const handleBreadcrumbClick = () => {
|
||||
/>
|
||||
</OnClickOutside>
|
||||
</div>
|
||||
<div class="w-px h-3.5 rounded my-auto bg-slate-75 dark:bg-slate-800" />
|
||||
<span
|
||||
class="min-w-0 text-sm font-medium truncate text-slate-800 dark:text-slate-100"
|
||||
>
|
||||
<div class="w-px h-3.5 rounded my-auto bg-n-weak" />
|
||||
<span class="min-w-0 text-sm font-medium truncate text-n-slate-12">
|
||||
{{
|
||||
t('HELP_CENTER.CATEGORY_PAGE.CATEGORY_HEADER.CATEGORIES_COUNT', {
|
||||
n: categoriesCount,
|
||||
|
||||
+1
-1
@@ -44,7 +44,7 @@ const locales = [
|
||||
<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">
|
||||
<div class="w-full min-h-screen bg-n-background">
|
||||
<LocalesPage :locales="locales" />
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ const localeCount = computed(() => props.locales?.length);
|
||||
<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">
|
||||
<span class="text-sm font-medium text-n-slate-12">
|
||||
{{ $t('HELP_CENTER.LOCALES_PAGE.LOCALES_COUNT', localeCount) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
+26
-1
@@ -1,6 +1,9 @@
|
||||
<script setup>
|
||||
import { ref, reactive, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { helpers } from '@vuelidate/validators';
|
||||
import { isValidDomain } from '@chatwoot/utils';
|
||||
|
||||
import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
|
||||
import Input from 'dashboard/components-next/input/Input.vue';
|
||||
@@ -26,6 +29,20 @@ const formState = reactive({
|
||||
customDomain: props.customDomain,
|
||||
});
|
||||
|
||||
const rules = {
|
||||
customDomain: {
|
||||
isValidDomain: helpers.withMessage(
|
||||
() =>
|
||||
t(
|
||||
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.DIALOG.FORMAT_ERROR'
|
||||
),
|
||||
isValidDomain
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
const v$ = useVuelidate(rules, formState);
|
||||
|
||||
watch(
|
||||
() => props.customDomain,
|
||||
newVal => {
|
||||
@@ -33,7 +50,10 @@ watch(
|
||||
}
|
||||
);
|
||||
|
||||
const handleDialogConfirm = () => {
|
||||
const handleDialogConfirm = async () => {
|
||||
const isFormCorrect = await v$.value.$validate();
|
||||
if (!isFormCorrect) return;
|
||||
|
||||
emit('addCustomDomain', formState.customDomain);
|
||||
};
|
||||
|
||||
@@ -67,6 +87,11 @@ defineExpose({ dialogRef });
|
||||
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.DIALOG.PLACEHOLDER'
|
||||
)
|
||||
"
|
||||
:message="
|
||||
v$.customDomain.$error ? v$.customDomain.$errors[0].$message : ''
|
||||
"
|
||||
:message-type="v$.customDomain.$error ? 'error' : 'info'"
|
||||
@blur="v$.customDomain.$touch()"
|
||||
/>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
+138
-37
@@ -1,9 +1,15 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { reactive, computed, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { copyTextToClipboard } from 'shared/helpers/clipboard';
|
||||
import { getHostNameFromURL } from 'dashboard/helper/URLHelper';
|
||||
import { email, required } from '@vuelidate/validators';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
|
||||
import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
|
||||
import Input from 'dashboard/components-next/input/Input.vue';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
const props = defineProps({
|
||||
customDomain: {
|
||||
@@ -12,10 +18,20 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['confirm']);
|
||||
const emit = defineEmits(['send', 'close']);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const state = reactive({
|
||||
email: '',
|
||||
});
|
||||
|
||||
const validationRules = {
|
||||
email: { email, required },
|
||||
};
|
||||
|
||||
const v$ = useVuelidate(validationRules, state);
|
||||
|
||||
const domain = computed(() => {
|
||||
const { hostURL, helpCenterURL } = window?.chatwootConfig || {};
|
||||
return getHostNameFromURL(helpCenterURL) || getHostNameFromURL(hostURL) || '';
|
||||
@@ -25,10 +41,34 @@ const subdomainCNAME = computed(
|
||||
() => `${props.customDomain} CNAME ${domain.value}`
|
||||
);
|
||||
|
||||
const handleCopy = async e => {
|
||||
e.stopPropagation();
|
||||
await copyTextToClipboard(subdomainCNAME.value);
|
||||
useAlert(
|
||||
t(
|
||||
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.DNS_CONFIGURATION_DIALOG.COPY'
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const dialogRef = ref(null);
|
||||
|
||||
const handleDialogConfirm = () => {
|
||||
emit('confirm');
|
||||
const resetForm = () => {
|
||||
v$.value.$reset();
|
||||
state.email = '';
|
||||
};
|
||||
|
||||
const onClose = () => {
|
||||
resetForm();
|
||||
emit('close');
|
||||
};
|
||||
|
||||
const handleSend = async () => {
|
||||
const isFormCorrect = await v$.value.$validate();
|
||||
if (!isFormCorrect) return;
|
||||
|
||||
emit('send', state.email);
|
||||
onClose();
|
||||
};
|
||||
|
||||
defineExpose({ dialogRef });
|
||||
@@ -37,42 +77,103 @@ defineExpose({ dialogRef });
|
||||
<template>
|
||||
<Dialog
|
||||
ref="dialogRef"
|
||||
:title="
|
||||
t(
|
||||
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.DNS_CONFIGURATION_DIALOG.HEADER'
|
||||
)
|
||||
"
|
||||
:confirm-button-label="
|
||||
t(
|
||||
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.DNS_CONFIGURATION_DIALOG.CONFIRM_BUTTON_LABEL'
|
||||
)
|
||||
"
|
||||
:show-cancel-button="false"
|
||||
@confirm="handleDialogConfirm"
|
||||
:show-confirm-button="false"
|
||||
@close="resetForm"
|
||||
>
|
||||
<template #description>
|
||||
<p class="mb-0 text-sm text-n-slate-12">
|
||||
{{
|
||||
t(
|
||||
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.DNS_CONFIGURATION_DIALOG.DESCRIPTION'
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
</template>
|
||||
<NextButton
|
||||
icon="i-lucide-x"
|
||||
sm
|
||||
ghost
|
||||
slate
|
||||
class="flex-shrink-0 absolute top-2 ltr:right-2 rtl:left-2"
|
||||
@click="onClose"
|
||||
/>
|
||||
<div class="flex flex-col gap-6 divide-y divide-n-strong">
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="flex flex-col gap-2 ltr:pr-10 rtl:pl-10">
|
||||
<h3 class="text-base font-medium leading-6 text-n-slate-12">
|
||||
{{
|
||||
t(
|
||||
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.DNS_CONFIGURATION_DIALOG.HEADER'
|
||||
)
|
||||
}}
|
||||
</h3>
|
||||
<p class="mb-0 text-sm text-n-slate-12">
|
||||
{{
|
||||
t(
|
||||
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.DNS_CONFIGURATION_DIALOG.DESCRIPTION'
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 w-full">
|
||||
<span
|
||||
class="min-h-10 px-3 py-2.5 inline-flex items-center w-full text-sm bg-transparent border rounded-lg text-n-slate-11 border-n-strong"
|
||||
>
|
||||
{{ subdomainCNAME }}
|
||||
</span>
|
||||
<NextButton
|
||||
faded
|
||||
slate
|
||||
type="button"
|
||||
icon="i-lucide-copy"
|
||||
class="flex-shrink-0"
|
||||
@click="handleCopy"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-6">
|
||||
<span
|
||||
class="h-10 px-3 py-2.5 text-sm select-none bg-transparent border rounded-lg text-n-slate-11 border-n-strong"
|
||||
>
|
||||
{{ subdomainCNAME }}
|
||||
</span>
|
||||
<p class="text-sm text-n-slate-12">
|
||||
{{
|
||||
t(
|
||||
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.DNS_CONFIGURATION_DIALOG.HELP_TEXT'
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<div class="flex flex-col gap-6 pt-6">
|
||||
<div class="flex flex-col gap-2 ltr:pr-10 rtl:pl-10">
|
||||
<h3 class="text-base font-medium leading-6 text-n-slate-12">
|
||||
{{
|
||||
t(
|
||||
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.DNS_CONFIGURATION_DIALOG.SEND_INSTRUCTIONS.HEADER'
|
||||
)
|
||||
}}
|
||||
</h3>
|
||||
<p class="mb-0 text-sm text-n-slate-12">
|
||||
{{
|
||||
t(
|
||||
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.DNS_CONFIGURATION_DIALOG.SEND_INSTRUCTIONS.DESCRIPTION'
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
<form
|
||||
class="flex items-start gap-3 w-full"
|
||||
@submit.prevent="handleSend"
|
||||
>
|
||||
<Input
|
||||
v-model="state.email"
|
||||
:placeholder="
|
||||
t(
|
||||
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.DNS_CONFIGURATION_DIALOG.SEND_INSTRUCTIONS.PLACEHOLDER'
|
||||
)
|
||||
"
|
||||
:message="
|
||||
v$.email.$error
|
||||
? t(
|
||||
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.DNS_CONFIGURATION_DIALOG.SEND_INSTRUCTIONS.ERROR'
|
||||
)
|
||||
: ''
|
||||
"
|
||||
:message-type="v$.email.$error ? 'error' : 'info'"
|
||||
class="w-full"
|
||||
@blur="v$.email.$touch()"
|
||||
/>
|
||||
<NextButton
|
||||
:label="
|
||||
t(
|
||||
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.DNS_CONFIGURATION_DIALOG.SEND_INSTRUCTIONS.SEND_BUTTON'
|
||||
)
|
||||
"
|
||||
type="submit"
|
||||
class="flex-shrink-0"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
+13
-11
@@ -7,8 +7,8 @@ import { useStore, useStoreGetters } from 'dashboard/composables/store';
|
||||
import { uploadFile } from 'dashboard/helper/uploadHelper';
|
||||
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { required, minLength, helpers } from '@vuelidate/validators';
|
||||
import { shouldBeUrl, isValidSlug } from 'shared/helpers/Validators';
|
||||
import { required, minLength, helpers, url } from '@vuelidate/validators';
|
||||
import { isValidSlug } from 'shared/helpers/Validators';
|
||||
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import Input from 'dashboard/components-next/input/Input.vue';
|
||||
@@ -71,7 +71,7 @@ const rules = {
|
||||
isValidSlug
|
||||
),
|
||||
},
|
||||
homePageLink: { shouldBeUrl },
|
||||
homePageLink: { url },
|
||||
};
|
||||
|
||||
const v$ = useVuelidate(rules, state);
|
||||
@@ -211,7 +211,7 @@ const handleAvatarDelete = () => {
|
||||
class="grid items-start justify-between w-full gap-2 grid-cols-[200px,1fr]"
|
||||
>
|
||||
<label
|
||||
class="text-sm font-medium whitespace-nowrap py-2.5 text-slate-900 dark:text-slate-50"
|
||||
class="text-sm font-medium whitespace-nowrap py-2.5 text-n-slate-12"
|
||||
>
|
||||
{{ t('HELP_CENTER.PORTAL_SETTINGS.FORM.NAME.LABEL') }}
|
||||
</label>
|
||||
@@ -229,7 +229,7 @@ const handleAvatarDelete = () => {
|
||||
class="grid items-start justify-between w-full gap-2 grid-cols-[200px,1fr]"
|
||||
>
|
||||
<label
|
||||
class="text-sm font-medium whitespace-nowrap py-2.5 text-slate-900 dark:text-slate-50"
|
||||
class="text-sm font-medium whitespace-nowrap py-2.5 text-n-slate-12"
|
||||
>
|
||||
{{ t('HELP_CENTER.PORTAL_SETTINGS.FORM.HEADER_TEXT.LABEL') }}
|
||||
</label>
|
||||
@@ -245,7 +245,7 @@ const handleAvatarDelete = () => {
|
||||
class="grid items-start justify-between w-full gap-2 grid-cols-[200px,1fr]"
|
||||
>
|
||||
<label
|
||||
class="text-sm font-medium whitespace-nowrap text-slate-900 py-2.5 dark:text-slate-50"
|
||||
class="text-sm font-medium whitespace-nowrap text-n-slate-12 py-2.5"
|
||||
>
|
||||
{{ t('HELP_CENTER.PORTAL_SETTINGS.FORM.PAGE_TITLE.LABEL') }}
|
||||
</label>
|
||||
@@ -261,7 +261,7 @@ const handleAvatarDelete = () => {
|
||||
class="grid items-start justify-between w-full gap-2 grid-cols-[200px,1fr]"
|
||||
>
|
||||
<label
|
||||
class="text-sm font-medium whitespace-nowrap text-slate-900 py-2.5 dark:text-slate-50"
|
||||
class="text-sm font-medium whitespace-nowrap text-n-slate-12 py-2.5"
|
||||
>
|
||||
{{ t('HELP_CENTER.PORTAL_SETTINGS.FORM.HOME_PAGE_LINK.LABEL') }}
|
||||
</label>
|
||||
@@ -281,7 +281,7 @@ const handleAvatarDelete = () => {
|
||||
class="grid items-start justify-between w-full gap-2 grid-cols-[200px,1fr]"
|
||||
>
|
||||
<label
|
||||
class="text-sm font-medium whitespace-nowrap py-2.5 text-slate-900 dark:text-slate-50"
|
||||
class="text-sm font-medium whitespace-nowrap py-2.5 text-n-slate-12"
|
||||
>
|
||||
{{ t('HELP_CENTER.PORTAL_SETTINGS.FORM.SLUG.LABEL') }}
|
||||
</label>
|
||||
@@ -299,7 +299,7 @@ const handleAvatarDelete = () => {
|
||||
class="grid items-start justify-between w-full gap-2 grid-cols-[200px,1fr]"
|
||||
>
|
||||
<label
|
||||
class="text-sm font-medium whitespace-nowrap py-2.5 text-slate-900 dark:text-slate-50"
|
||||
class="text-sm font-medium whitespace-nowrap py-2.5 text-n-slate-12"
|
||||
>
|
||||
{{ t('HELP_CENTER.PORTAL_SETTINGS.FORM.LIVE_CHAT_WIDGET.LABEL') }}
|
||||
</label>
|
||||
@@ -315,9 +315,11 @@ const handleAvatarDelete = () => {
|
||||
class="[&>div>button:not(.focused)]:!outline-n-weak"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-start justify-between w-full gap-2">
|
||||
<div
|
||||
class="grid items-start justify-between w-full gap-2 grid-cols-[200px,1fr]"
|
||||
>
|
||||
<label
|
||||
class="text-sm font-medium whitespace-nowrap py-2.5 text-slate-900 dark:text-slate-50"
|
||||
class="text-sm font-medium whitespace-nowrap py-2.5 text-n-slate-12"
|
||||
>
|
||||
{{ t('HELP_CENTER.PORTAL_SETTINGS.FORM.BRAND_COLOR.LABEL') }}
|
||||
</label>
|
||||
|
||||
+159
-23
@@ -1,6 +1,7 @@
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useAccount } from 'dashboard/composables/useAccount';
|
||||
|
||||
import AddCustomDomainDialog from 'dashboard/components-next/HelpCenter/Pages/PortalSettingsPage/AddCustomDomainDialog.vue';
|
||||
import DNSConfigurationDialog from 'dashboard/components-next/HelpCenter/Pages/PortalSettingsPage/DNSConfigurationDialog.vue';
|
||||
@@ -11,11 +12,52 @@ const props = defineProps({
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
isFetchingStatus: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['updatePortalConfiguration']);
|
||||
const emit = defineEmits([
|
||||
'updatePortalConfiguration',
|
||||
'refreshStatus',
|
||||
'sendCnameInstructions',
|
||||
]);
|
||||
|
||||
const SSL_STATUS = {
|
||||
LIVE: ['active', 'staging_active'],
|
||||
PENDING: [
|
||||
'provisioned',
|
||||
'pending',
|
||||
'initializing',
|
||||
'pending_validation',
|
||||
'pending_deployment',
|
||||
'pending_issuance',
|
||||
'holding_deployment',
|
||||
'holding_validation',
|
||||
'pending_expiration',
|
||||
'pending_cleanup',
|
||||
'pending_deletion',
|
||||
'staging_deployment',
|
||||
'backup_issued',
|
||||
],
|
||||
ERROR: [
|
||||
'blocked',
|
||||
'inactive',
|
||||
'moved',
|
||||
'expired',
|
||||
'deleted',
|
||||
'timed_out_initializing',
|
||||
'timed_out_validation',
|
||||
'timed_out_issuance',
|
||||
'timed_out_deployment',
|
||||
'timed_out_deletion',
|
||||
'deactivating',
|
||||
],
|
||||
};
|
||||
|
||||
const { t } = useI18n();
|
||||
const { isOnChatwootCloud } = useAccount();
|
||||
|
||||
const addCustomDomainDialogRef = ref(null);
|
||||
const dnsConfigurationDialogRef = ref(null);
|
||||
@@ -25,6 +67,45 @@ const customDomainAddress = computed(
|
||||
() => props.activePortal?.custom_domain || ''
|
||||
);
|
||||
|
||||
const sslSettings = computed(() => props.activePortal?.ssl_settings || {});
|
||||
const verificationErrors = computed(
|
||||
() => sslSettings.value.verification_errors || ''
|
||||
);
|
||||
|
||||
const isLive = computed(() =>
|
||||
SSL_STATUS.LIVE.includes(sslSettings.value.status)
|
||||
);
|
||||
const isPending = computed(() =>
|
||||
SSL_STATUS.PENDING.includes(sslSettings.value.status)
|
||||
);
|
||||
const isError = computed(() =>
|
||||
SSL_STATUS.ERROR.includes(sslSettings.value.status)
|
||||
);
|
||||
|
||||
const statusText = computed(() => {
|
||||
if (isLive.value)
|
||||
return t(
|
||||
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.STATUS.LIVE'
|
||||
);
|
||||
if (isPending.value)
|
||||
return t(
|
||||
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.STATUS.PENDING'
|
||||
);
|
||||
if (isError.value)
|
||||
return t(
|
||||
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.STATUS.ERROR'
|
||||
);
|
||||
return '';
|
||||
});
|
||||
|
||||
const statusColors = computed(() => {
|
||||
if (isLive.value)
|
||||
return { text: 'text-n-teal-11', bubble: 'outline-n-teal-6 bg-n-teal-9' };
|
||||
if (isError.value)
|
||||
return { text: 'text-n-ruby-11', bubble: 'outline-n-ruby-6 bg-n-ruby-9' };
|
||||
return { text: 'text-n-amber-11', bubble: 'outline-n-amber-6 bg-n-amber-9' };
|
||||
});
|
||||
|
||||
const updatePortalConfiguration = customDomain => {
|
||||
const portal = {
|
||||
id: props.activePortal?.id,
|
||||
@@ -42,6 +123,17 @@ const closeDNSConfigurationDialog = () => {
|
||||
updatedDomainAddress.value = '';
|
||||
dnsConfigurationDialogRef.value.dialogRef.close();
|
||||
};
|
||||
|
||||
const onClickRefreshSSLStatus = () => {
|
||||
emit('refreshStatus');
|
||||
};
|
||||
|
||||
const onClickSend = email => {
|
||||
emit('sendCnameInstructions', {
|
||||
portalSlug: props.activePortal?.slug,
|
||||
email,
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -63,33 +155,76 @@ const closeDNSConfigurationDialog = () => {
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-col w-full gap-4">
|
||||
<div class="flex justify-between w-full gap-2">
|
||||
<div
|
||||
v-if="customDomainAddress"
|
||||
class="flex items-center w-full h-8 gap-4"
|
||||
>
|
||||
<label class="text-sm font-medium text-n-slate-12">
|
||||
<div class="flex items-center justify-between w-full gap-2">
|
||||
<div v-if="customDomainAddress" class="flex flex-col gap-1">
|
||||
<div class="flex items-center w-full h-8 gap-4">
|
||||
<label class="text-sm font-medium text-n-slate-12">
|
||||
{{
|
||||
t(
|
||||
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.LABEL'
|
||||
)
|
||||
}}
|
||||
</label>
|
||||
<span class="text-sm text-n-slate-12">
|
||||
{{ customDomainAddress }}
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
v-if="!isLive && isOnChatwootCloud"
|
||||
class="text-sm text-n-slate-11"
|
||||
>
|
||||
{{
|
||||
t(
|
||||
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.LABEL'
|
||||
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.STATUS_DESCRIPTION'
|
||||
)
|
||||
}}
|
||||
</label>
|
||||
<span class="text-sm text-n-slate-12">
|
||||
{{ customDomainAddress }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-end w-full">
|
||||
<Button
|
||||
v-if="customDomainAddress"
|
||||
color="slate"
|
||||
:label="
|
||||
t(
|
||||
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.EDIT_BUTTON'
|
||||
)
|
||||
"
|
||||
@click="addCustomDomainDialogRef.dialogRef.open()"
|
||||
/>
|
||||
<div class="flex items-center">
|
||||
<div v-if="customDomainAddress" class="flex items-center gap-3">
|
||||
<div
|
||||
v-if="statusText && isOnChatwootCloud"
|
||||
v-tooltip="verificationErrors"
|
||||
class="flex items-center gap-3 flex-shrink-0"
|
||||
>
|
||||
<span
|
||||
class="size-1.5 rounded-full outline outline-2 block flex-shrink-0"
|
||||
:class="statusColors.bubble"
|
||||
/>
|
||||
<span
|
||||
:class="statusColors.text"
|
||||
class="text-sm leading-[16px] font-medium"
|
||||
>
|
||||
{{ statusText }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="statusText && isOnChatwootCloud"
|
||||
class="w-px h-3 bg-n-weak"
|
||||
/>
|
||||
<Button
|
||||
slate
|
||||
sm
|
||||
link
|
||||
:label="
|
||||
t(
|
||||
'HELP_CENTER.PORTAL_SETTINGS.CONFIGURATION_FORM.CUSTOM_DOMAIN.EDIT_BUTTON'
|
||||
)
|
||||
"
|
||||
class="hover:!no-underline flex-shrink-0"
|
||||
@click="addCustomDomainDialogRef.dialogRef.open()"
|
||||
/>
|
||||
<div v-if="isOnChatwootCloud" class="w-px h-3 bg-n-weak" />
|
||||
<Button
|
||||
v-if="isOnChatwootCloud"
|
||||
slate
|
||||
sm
|
||||
link
|
||||
icon="i-lucide-refresh-ccw"
|
||||
:class="isFetchingStatus && 'animate-spin'"
|
||||
@click="onClickRefreshSSLStatus"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
v-else
|
||||
:label="
|
||||
@@ -112,7 +247,8 @@ const closeDNSConfigurationDialog = () => {
|
||||
<DNSConfigurationDialog
|
||||
ref="dnsConfigurationDialogRef"
|
||||
:custom-domain="updatedDomainAddress || customDomainAddress"
|
||||
@confirm="closeDNSConfigurationDialog"
|
||||
@close="closeDNSConfigurationDialog"
|
||||
@send="onClickSend"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ import PortalSettings from './PortalSettings.vue';
|
||||
<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">
|
||||
<div class="w-[1000px] min-h-screen bg-n-background">
|
||||
<PortalSettings />
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
+16
-2
@@ -26,6 +26,8 @@ const emit = defineEmits([
|
||||
'updatePortal',
|
||||
'updatePortalConfiguration',
|
||||
'deletePortal',
|
||||
'refreshStatus',
|
||||
'sendCnameInstructions',
|
||||
]);
|
||||
|
||||
const { t } = useI18n();
|
||||
@@ -36,6 +38,7 @@ const confirmDeletePortalDialogRef = ref(null);
|
||||
const currentPortalSlug = computed(() => route.params.portalSlug);
|
||||
|
||||
const isSwitchingPortal = useMapGetter('portals/isSwitchingPortal');
|
||||
const isFetchingSSLStatus = useMapGetter('portals/isFetchingSSLStatus');
|
||||
|
||||
const activePortal = computed(() => {
|
||||
return props.portals?.find(portal => portal.slug === currentPortalSlug.value);
|
||||
@@ -53,6 +56,14 @@ const handleUpdatePortalConfiguration = portal => {
|
||||
emit('updatePortalConfiguration', portal);
|
||||
};
|
||||
|
||||
const fetchSSLStatus = () => {
|
||||
emit('refreshStatus');
|
||||
};
|
||||
|
||||
const handleSendCnameInstructions = payload => {
|
||||
emit('sendCnameInstructions', payload);
|
||||
};
|
||||
|
||||
const openConfirmDeletePortalDialog = () => {
|
||||
confirmDeletePortalDialogRef.value.dialogRef.open();
|
||||
};
|
||||
@@ -81,13 +92,16 @@ const handleDeletePortal = () => {
|
||||
:is-fetching="isFetching"
|
||||
@update-portal="handleUpdatePortal"
|
||||
/>
|
||||
<div class="w-full h-px bg-slate-50 dark:bg-slate-800/50" />
|
||||
<div class="w-full h-px bg-n-weak" />
|
||||
<PortalConfigurationSettings
|
||||
:active-portal="activePortal"
|
||||
:is-fetching="isFetching"
|
||||
:is-fetching-status="isFetchingSSLStatus"
|
||||
@update-portal-configuration="handleUpdatePortalConfiguration"
|
||||
@refresh-status="fetchSSLStatus"
|
||||
@send-cname-instructions="handleSendCnameInstructions"
|
||||
/>
|
||||
<div class="w-full h-px bg-slate-50 dark:bg-slate-800/50" />
|
||||
<div class="w-full h-px bg-n-weak" />
|
||||
<div class="flex items-end justify-between w-full gap-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<h6 class="text-base font-medium text-n-slate-12">
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ const portals = [
|
||||
:layout="{ type: 'grid', width: '510px' }"
|
||||
>
|
||||
<Variant title="Portal Switcher">
|
||||
<div class="h-[500px] p-4 bg-slate-100 dark:bg-slate-900">
|
||||
<div class="h-[500px] p-4 bg-n-slate-2 dark:bg-n-background">
|
||||
<PortalSwitcher
|
||||
:portals="portals"
|
||||
header="Choose a Portal"
|
||||
|
||||
+2
-2
@@ -100,7 +100,7 @@ const redirectToPortalHomePage = () => {
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<h2
|
||||
class="text-base font-medium cursor-pointer text-slate-900 dark:text-slate-50 w-fit hover:underline"
|
||||
class="text-base font-medium cursor-pointer text-n-slate-12 w-fit hover:underline"
|
||||
@click="redirectToPortalHomePage"
|
||||
>
|
||||
{{ t('HELP_CENTER.PORTAL_SWITCHER.PORTALS') }}
|
||||
@@ -115,7 +115,7 @@ const redirectToPortalHomePage = () => {
|
||||
@click="onClickPreviewPortal"
|
||||
/>
|
||||
</div>
|
||||
<p class="text-sm text-slate-600 dark:text-slate-300">
|
||||
<p class="text-sm text-n-slate-11">
|
||||
{{ t('HELP_CENTER.PORTAL_SWITCHER.CREATE_PORTAL') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -49,8 +49,8 @@ const isUnread = computed(() => !props.inboxItem?.readAt);
|
||||
const inbox = computed(() => props.stateInbox);
|
||||
|
||||
const inboxIcon = computed(() => {
|
||||
const { phoneNumber, channelType } = inbox.value;
|
||||
return getInboxIconByType(channelType, phoneNumber);
|
||||
const { channelType, medium } = inbox.value;
|
||||
return getInboxIconByType(channelType, medium);
|
||||
});
|
||||
|
||||
const hasSlaThreshold = computed(() => {
|
||||
@@ -63,11 +63,12 @@ const lastActivityAt = computed(() => {
|
||||
});
|
||||
|
||||
const menuItems = computed(() => [
|
||||
{ key: 'delete', label: t('INBOX.MENU_ITEM.DELETE') },
|
||||
{
|
||||
key: isUnread.value ? 'mark_as_read' : 'mark_as_unread',
|
||||
icon: isUnread.value ? 'mail' : 'mail-unread',
|
||||
label: t(`INBOX.MENU_ITEM.MARK_AS_${isUnread.value ? 'READ' : 'UNREAD'}`),
|
||||
},
|
||||
{ key: 'delete', icon: 'delete', label: t('INBOX.MENU_ITEM.DELETE') },
|
||||
]);
|
||||
|
||||
const messageClasses = computed(() => ({
|
||||
@@ -153,7 +154,7 @@ onBeforeMount(contextMenuActions.close);
|
||||
<template>
|
||||
<div
|
||||
role="button"
|
||||
class="flex flex-col w-full gap-2 p-3 transition-all duration-300 ease-in-out cursor-pointer"
|
||||
class="flex flex-col w-full gap-1 p-3 transition-all duration-300 ease-in-out cursor-pointer"
|
||||
@contextmenu="contextMenuActions.open($event)"
|
||||
@click="emit('click')"
|
||||
>
|
||||
@@ -232,7 +233,7 @@ onBeforeMount(contextMenuActions.close);
|
||||
class="flex-shrink-0 text-n-slate-11 size-2.5"
|
||||
/>
|
||||
</div>
|
||||
<span class="text-sm text-n-slate-10">
|
||||
<span class="text-xs text-n-slate-10">
|
||||
{{ lastActivityAt }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { ref, computed, onMounted, watch } from 'vue';
|
||||
import { useStore, useMapGetter } from 'dashboard/composables/store';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useWindowSize } from '@vueuse/core';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import { vOnClickOutside } from '@vueuse/components';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
@@ -15,6 +16,7 @@ import {
|
||||
processContactableInboxes,
|
||||
mergeInboxDetails,
|
||||
} from 'dashboard/components-next/NewConversation/helpers/composeConversationHelper';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
|
||||
import ComposeNewConversationForm from 'dashboard/components-next/NewConversation/components/ComposeNewConversationForm.vue';
|
||||
|
||||
@@ -37,9 +39,16 @@ const emit = defineEmits(['close']);
|
||||
|
||||
const store = useStore();
|
||||
const { t } = useI18n();
|
||||
const { width: windowWidth } = useWindowSize();
|
||||
|
||||
const { fetchSignatureFlagFromUISettings } = useUISettings();
|
||||
|
||||
const isSmallScreen = computed(
|
||||
() => windowWidth.value < wootConstants.SMALL_SCREEN_BREAKPOINT
|
||||
);
|
||||
|
||||
const viewInModal = computed(() => props.isModal || isSmallScreen.value);
|
||||
|
||||
const contacts = ref([]);
|
||||
const selectedContact = ref(null);
|
||||
const targetInbox = ref(null);
|
||||
@@ -67,7 +76,7 @@ const directUploadsEnabled = computed(
|
||||
const activeContact = computed(() => contactById.value(props.contactId));
|
||||
|
||||
const composePopoverClass = computed(() => {
|
||||
if (props.isModal) return '';
|
||||
if (viewInModal.value) return '';
|
||||
|
||||
return props.alignPosition === 'right'
|
||||
? 'absolute ltr:left-0 ltr:right-[unset] rtl:right-0 rtl:left-[unset]'
|
||||
@@ -179,14 +188,18 @@ const toggle = () => {
|
||||
|
||||
watch(
|
||||
activeContact,
|
||||
() => {
|
||||
if (activeContact.value && props.contactId) {
|
||||
const contactInboxes = activeContact.value?.contactInboxes || [];
|
||||
(currentContact, previousContact) => {
|
||||
if (currentContact && props.contactId) {
|
||||
// Reset on contact change
|
||||
if (currentContact?.id !== previousContact?.id) clearSelectedContact();
|
||||
|
||||
// First process the contactable inboxes to get the right structure
|
||||
const processedInboxes = processContactableInboxes(contactInboxes);
|
||||
const processedInboxes = processContactableInboxes(
|
||||
currentContact.contactInboxes || []
|
||||
);
|
||||
// Then Merge processedInboxes with the inboxes list
|
||||
selectedContact.value = {
|
||||
...activeContact.value,
|
||||
...currentContact,
|
||||
contactInboxes: mergeInboxDetails(processedInboxes, inboxesList.value),
|
||||
};
|
||||
}
|
||||
@@ -202,7 +215,7 @@ const handleClickOutside = () => {
|
||||
};
|
||||
|
||||
const onModalBackdropClick = () => {
|
||||
if (!props.isModal) return;
|
||||
if (!viewInModal.value) return;
|
||||
handleClickOutside();
|
||||
};
|
||||
|
||||
@@ -231,7 +244,7 @@ useKeyboardEvents(keyboardEvents);
|
||||
]"
|
||||
class="relative"
|
||||
:class="{
|
||||
'z-40': showComposeNewConversation,
|
||||
'z-50': showComposeNewConversation && !viewInModal,
|
||||
}"
|
||||
>
|
||||
<slot
|
||||
@@ -243,12 +256,12 @@ useKeyboardEvents(keyboardEvents);
|
||||
v-if="showComposeNewConversation"
|
||||
:class="{
|
||||
'fixed z-50 bg-n-alpha-black1 backdrop-blur-[4px] flex items-start pt-[clamp(3rem,15vh,12rem)] justify-center inset-0':
|
||||
isModal,
|
||||
viewInModal,
|
||||
}"
|
||||
@click.self="onModalBackdropClick"
|
||||
>
|
||||
<ComposeNewConversationForm
|
||||
:class="[{ 'mt-2': !isModal }, composePopoverClass]"
|
||||
:class="[{ 'mt-2': !viewInModal }, composePopoverClass]"
|
||||
:contacts="contacts"
|
||||
:contact-id="contactId"
|
||||
:is-loading="isSearching"
|
||||
|
||||
+5
-3
@@ -25,6 +25,7 @@ const props = defineProps({
|
||||
hasNoInbox: { type: Boolean, default: false },
|
||||
isDropdownActive: { type: Boolean, default: false },
|
||||
messageSignature: { type: String, default: '' },
|
||||
inboxId: { type: Number, default: null },
|
||||
});
|
||||
|
||||
const emit = defineEmits([
|
||||
@@ -150,9 +151,10 @@ useKeyboardEvents(keyboardEvents);
|
||||
<div
|
||||
class="flex items-center justify-between w-full h-[3.25rem] gap-2 px-4 py-3"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex gap-2 items-center">
|
||||
<WhatsAppOptions
|
||||
v-if="isWhatsappInbox"
|
||||
:inbox-id="inboxId"
|
||||
:message-templates="messageTemplates"
|
||||
@send-message="emit('sendWhatsappMessage', $event)"
|
||||
/>
|
||||
@@ -170,7 +172,7 @@ useKeyboardEvents(keyboardEvents);
|
||||
/>
|
||||
<EmojiInput
|
||||
v-if="isEmojiPickerOpen"
|
||||
class="left-0 top-full mt-1.5"
|
||||
class="ltr:left-0 rtl:right-0 top-full mt-1.5"
|
||||
:on-click="onClickInsertEmoji"
|
||||
/>
|
||||
</div>
|
||||
@@ -206,7 +208,7 @@ useKeyboardEvents(keyboardEvents);
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex gap-2 items-center">
|
||||
<Button
|
||||
:label="t('COMPOSE_NEW_CONVERSATION.FORM.ACTION_BUTTONS.DISCARD')"
|
||||
variant="faded"
|
||||
|
||||
+1
-1
@@ -57,7 +57,7 @@ const removeAttachment = id => {
|
||||
variant="ghost"
|
||||
icon="i-lucide-trash"
|
||||
color="slate"
|
||||
class="absolute top-1 right-1 !w-5 !h-5 transition-opacity duration-150 ease-in-out opacity-0 group-hover/image:opacity-100"
|
||||
class="absolute top-1 ltr:right-1 rtl:left-1 !w-5 !h-5 transition-opacity duration-150 ease-in-out opacity-0 group-hover/image:opacity-100"
|
||||
@click="removeAttachment(attachment.resource.id)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
+2
-1
@@ -265,7 +265,7 @@ const handleSendWhatsappMessage = async ({ message, templateParams }) => {
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="w-[42rem] divide-y divide-n-strong overflow-visible transition-all duration-300 ease-in-out top-full justify-between flex flex-col bg-n-alpha-3 border border-n-strong shadow-sm backdrop-blur-[100px] rounded-xl"
|
||||
class="w-[42rem] divide-y divide-n-strong overflow-visible transition-all duration-300 ease-in-out top-full justify-between flex flex-col bg-n-alpha-3 border border-n-strong shadow-sm backdrop-blur-[100px] rounded-xl min-w-0"
|
||||
>
|
||||
<ContactSelector
|
||||
:contacts="contacts"
|
||||
@@ -336,6 +336,7 @@ const handleSendWhatsappMessage = async ({ message, templateParams }) => {
|
||||
:is-loading="isCreating"
|
||||
:disable-send-button="isCreating"
|
||||
:has-selected-inbox="!!targetInbox"
|
||||
:inbox-id="targetInbox?.id"
|
||||
:has-no-inbox="showNoInboxAlert"
|
||||
:is-dropdown-active="isAnyDropdownActive"
|
||||
:message-signature="messageSignature"
|
||||
|
||||
+1
-1
@@ -83,7 +83,7 @@ const targetInboxLabel = computed(() => {
|
||||
<DropdownMenu
|
||||
v-if="contactableInboxesList?.length > 0 && showInboxesDropdown"
|
||||
:menu-items="contactableInboxesList"
|
||||
class="left-0 z-[100] top-8 overflow-y-auto max-h-60 w-fit max-w-sm dark:!outline-n-slate-5"
|
||||
class="ltr:left-0 rtl:right-0 z-[100] top-8 overflow-y-auto max-h-60 w-fit max-w-sm dark:!outline-n-slate-5"
|
||||
@action="emit('handleInboxAction', $event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
+19
-26
@@ -1,23 +1,25 @@
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
|
||||
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import WhatsappTemplateParser from './WhatsappTemplateParser.vue';
|
||||
import WhatsappTemplate from './WhatsappTemplate.vue';
|
||||
|
||||
const props = defineProps({
|
||||
messageTemplates: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
inboxId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['sendMessage']);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
// TODO: Remove this when we support all formats
|
||||
const formatsToRemove = ['DOCUMENT', 'IMAGE', 'VIDEO'];
|
||||
const getFilteredWhatsAppTemplates = useMapGetter(
|
||||
'inboxes/getFilteredWhatsAppTemplates'
|
||||
);
|
||||
|
||||
const searchQuery = ref('');
|
||||
const selectedTemplate = ref(null);
|
||||
@@ -25,19 +27,7 @@ const selectedTemplate = ref(null);
|
||||
const showTemplatesMenu = ref(false);
|
||||
|
||||
const whatsAppTemplateMessages = computed(() => {
|
||||
// Add null check and ensure it's an array
|
||||
const templates = Array.isArray(props.messageTemplates)
|
||||
? props.messageTemplates
|
||||
: [];
|
||||
|
||||
// TODO: Remove the last filter when we support all formats
|
||||
return templates
|
||||
.filter(template => template?.status?.toLowerCase() === 'approved')
|
||||
.filter(template => {
|
||||
return template?.components?.every(component => {
|
||||
return !formatsToRemove.includes(component.format);
|
||||
});
|
||||
});
|
||||
return getFilteredWhatsAppTemplates.value(props.inboxId);
|
||||
});
|
||||
|
||||
const filteredTemplates = computed(() => {
|
||||
@@ -84,10 +74,13 @@ const handleSendMessage = template => {
|
||||
/>
|
||||
<div
|
||||
v-if="showTemplatesMenu"
|
||||
class="absolute top-full mt-1.5 max-h-96 overflow-y-auto left-0 flex flex-col gap-2 p-4 items-center w-[21.875rem] h-auto bg-n-solid-2 border border-n-strong shadow-sm rounded-lg"
|
||||
class="absolute top-full mt-1.5 max-h-96 overflow-y-auto ltr:left-0 rtl:right-0 flex flex-col gap-2 p-4 items-center w-[21.875rem] h-auto bg-n-solid-2 border border-n-strong shadow-sm rounded-lg"
|
||||
>
|
||||
<div class="relative w-full">
|
||||
<span class="absolute i-lucide-search size-3.5 top-2 left-3" />
|
||||
<Icon
|
||||
icon="i-lucide-search"
|
||||
class="absolute size-3.5 top-2 ltr:left-3 rtl:right-3"
|
||||
/>
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="search"
|
||||
@@ -96,13 +89,13 @@ const handleSendMessage = template => {
|
||||
'COMPOSE_NEW_CONVERSATION.FORM.WHATSAPP_OPTIONS.SEARCH_PLACEHOLDER'
|
||||
)
|
||||
"
|
||||
class="w-full h-8 py-2 pl-10 pr-2 text-sm reset-base outline-none border-none rounded-lg bg-n-alpha-black2 dark:bg-n-solid-1 text-n-slate-12"
|
||||
class="w-full h-8 py-2 ltr:pl-10 rtl:pr-10 ltr:pr-2 rtl:pl-2 text-sm reset-base outline-none border-none rounded-lg bg-n-alpha-black2 dark:bg-n-solid-1 text-n-slate-12"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-for="template in filteredTemplates"
|
||||
:key="template.id"
|
||||
class="flex flex-col w-full gap-2 p-2 rounded-lg cursor-pointer dark:hover:bg-n-alpha-3 hover:bg-n-alpha-1"
|
||||
class="flex flex-col gap-2 p-2 w-full rounded-lg cursor-pointer dark:hover:bg-n-alpha-3 hover:bg-n-alpha-1"
|
||||
@click="handleTemplateClick(template)"
|
||||
>
|
||||
<span class="text-sm text-n-slate-12">{{ template.name }}</span>
|
||||
@@ -111,12 +104,12 @@ const handleSendMessage = template => {
|
||||
</p>
|
||||
</div>
|
||||
<template v-if="filteredTemplates.length === 0">
|
||||
<p class="w-full pt-2 text-sm text-n-slate-11">
|
||||
<p class="pt-2 w-full text-sm text-n-slate-11">
|
||||
{{ t('COMPOSE_NEW_CONVERSATION.FORM.WHATSAPP_OPTIONS.EMPTY_STATE') }}
|
||||
</p>
|
||||
</template>
|
||||
</div>
|
||||
<WhatsappTemplateParser
|
||||
<WhatsappTemplate
|
||||
v-if="selectedTemplate"
|
||||
:template="selectedTemplate"
|
||||
@send-message="handleSendMessage"
|
||||
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
<script setup>
|
||||
import WhatsAppTemplateParser from 'dashboard/components-next/whatsapp/WhatsAppTemplateParser.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
defineProps({
|
||||
template: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['sendMessage', 'back']);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const handleSendMessage = payload => {
|
||||
emit('sendMessage', payload);
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
emit('back');
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="absolute top-full mt-1.5 max-h-[30rem] overflow-y-auto ltr:left-0 rtl:right-0 flex flex-col gap-4 px-4 pt-6 pb-5 items-start w-[28.75rem] h-auto bg-n-solid-2 border border-n-strong shadow-sm rounded-lg"
|
||||
>
|
||||
<div class="w-full">
|
||||
<WhatsAppTemplateParser
|
||||
:template="template"
|
||||
@send-message="handleSendMessage"
|
||||
@back="handleBack"
|
||||
>
|
||||
<template #actions="{ sendMessage, goBack, disabled }">
|
||||
<div class="flex gap-3 justify-between items-end w-full h-14">
|
||||
<Button
|
||||
:label="
|
||||
t(
|
||||
'COMPOSE_NEW_CONVERSATION.FORM.WHATSAPP_OPTIONS.TEMPLATE_PARSER.BACK'
|
||||
)
|
||||
"
|
||||
color="slate"
|
||||
variant="faded"
|
||||
class="w-full font-medium"
|
||||
@click="goBack"
|
||||
/>
|
||||
<Button
|
||||
:label="
|
||||
t(
|
||||
'COMPOSE_NEW_CONVERSATION.FORM.WHATSAPP_OPTIONS.TEMPLATE_PARSER.SEND_MESSAGE'
|
||||
)
|
||||
"
|
||||
class="w-full font-medium"
|
||||
:disabled="disabled"
|
||||
@click="sendMessage"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</WhatsAppTemplateParser>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
+3
-2
@@ -106,7 +106,7 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="absolute top-full mt-1.5 max-h-[30rem] overflow-y-auto left-0 flex flex-col gap-4 px-4 pt-6 pb-5 items-start w-[28.75rem] h-auto bg-n-solid-2 border border-n-strong shadow-sm rounded-lg"
|
||||
class="absolute top-full mt-1.5 max-h-[30rem] overflow-y-auto ltr:left-0 rtl:right-0 flex flex-col gap-4 px-4 pt-6 pb-5 items-start w-[28.75rem] h-auto bg-n-solid-2 border border-n-strong shadow-sm rounded-lg"
|
||||
>
|
||||
<span class="text-sm text-n-slate-12">
|
||||
{{
|
||||
@@ -138,7 +138,8 @@ onMounted(() => {
|
||||
class="flex items-center w-full gap-2"
|
||||
>
|
||||
<span
|
||||
class="flex items-center h-8 text-sm min-w-6 ltr:text-left rtl:text-right text-n-slate-10"
|
||||
class="block h-8 text-sm min-w-6 text-start truncate text-n-slate-10 leading-8"
|
||||
:title="key"
|
||||
>
|
||||
{{ key }}
|
||||
</span>
|
||||
|
||||
+2
-1
@@ -36,10 +36,11 @@ const transformInbox = ({
|
||||
email,
|
||||
channelType,
|
||||
phoneNumber,
|
||||
medium,
|
||||
...rest
|
||||
}) => ({
|
||||
id,
|
||||
icon: getInboxIconByType(channelType, phoneNumber, 'line'),
|
||||
icon: getInboxIconByType(channelType, medium, 'line'),
|
||||
label: generateLabelForContactableInboxesList({
|
||||
name,
|
||||
email,
|
||||
|
||||
@@ -5,7 +5,7 @@ import Avatar from './Avatar.vue';
|
||||
<template>
|
||||
<Story title="Components/Avatar" :layout="{ type: 'grid', width: '400' }">
|
||||
<Variant title="Default">
|
||||
<div class="flex p-4 space-x-4 bg-white dark:bg-slate-900">
|
||||
<div class="flex p-4 space-x-4 bg-n-background">
|
||||
<Avatar
|
||||
name=""
|
||||
src="https://api.dicebear.com/9.x/thumbs/svg?seed=Amaya"
|
||||
@@ -16,7 +16,7 @@ import Avatar from './Avatar.vue';
|
||||
</Variant>
|
||||
|
||||
<Variant title="Different Shapes">
|
||||
<div class="gap-4 p-4 bg-white dark:bg-slate-900">
|
||||
<div class="gap-4 p-4 bg-n-background">
|
||||
<Avatar
|
||||
src="https://api.dicebear.com/9.x/thumbs/svg?seed=Amaya"
|
||||
name=""
|
||||
@@ -34,7 +34,7 @@ import Avatar from './Avatar.vue';
|
||||
</Variant>
|
||||
|
||||
<Variant title="Different Sizes">
|
||||
<div class="flex flex-wrap gap-4 p-4 bg-white dark:bg-slate-900">
|
||||
<div class="flex flex-wrap gap-4 p-4 bg-n-background">
|
||||
<Avatar
|
||||
src="https://api.dicebear.com/9.x/avataaars/svg?seed=Felix"
|
||||
:size="48"
|
||||
@@ -57,7 +57,7 @@ import Avatar from './Avatar.vue';
|
||||
</Variant>
|
||||
|
||||
<Variant title="With Status">
|
||||
<div class="flex flex-wrap gap-4 p-4 bg-white dark:bg-slate-900">
|
||||
<div class="flex flex-wrap gap-4 p-4 bg-n-background">
|
||||
<Avatar
|
||||
src="https://api.dicebear.com/9.x/thumbs/svg?seed=Felix"
|
||||
status="online"
|
||||
@@ -77,7 +77,7 @@ import Avatar from './Avatar.vue';
|
||||
</Variant>
|
||||
|
||||
<Variant title="With Custom Icon">
|
||||
<div class="flex flex-wrap gap-4 p-4 bg-white dark:bg-slate-900">
|
||||
<div class="flex flex-wrap gap-4 p-4 bg-n-background">
|
||||
<Avatar name="Custom Icon" icon-name="i-lucide-user" :size="48" />
|
||||
<Avatar
|
||||
name="Custom Industry"
|
||||
@@ -88,7 +88,7 @@ import Avatar from './Avatar.vue';
|
||||
</Variant>
|
||||
|
||||
<Variant title="Upload States">
|
||||
<div class="flex flex-wrap gap-4 p-4 bg-white dark:bg-slate-900">
|
||||
<div class="flex flex-wrap gap-4 p-4 bg-n-background">
|
||||
<!-- Empty state with upload -->
|
||||
<Avatar name="Upload New" allow-upload :size="48" />
|
||||
|
||||
@@ -103,7 +103,7 @@ import Avatar from './Avatar.vue';
|
||||
</Variant>
|
||||
|
||||
<Variant title="Name Initials">
|
||||
<div class="flex flex-wrap gap-4 p-4 bg-white dark:bg-slate-900">
|
||||
<div class="flex flex-wrap gap-4 p-4 bg-n-background">
|
||||
<Avatar name="Catherine" :size="48" />
|
||||
<Avatar name="John Doe" :size="48" />
|
||||
<Avatar name="Rose Doe John" :size="48" />
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useI18n } from 'vue-i18n';
|
||||
import { removeEmoji } from 'shared/helpers/emoji';
|
||||
|
||||
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||
import ChannelIcon from 'dashboard/components-next/icon/ChannelIcon.vue';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
|
||||
const props = defineProps({
|
||||
@@ -33,10 +34,18 @@ const props = defineProps({
|
||||
validator: value =>
|
||||
!value || wootConstants.AVAILABILITY_STATUS_KEYS.includes(value),
|
||||
},
|
||||
inbox: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
iconName: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
hideOfflineStatus: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['upload', 'delete']);
|
||||
@@ -66,11 +75,11 @@ const AVATAR_COLORS = {
|
||||
default: { bg: '#E8E8E8', text: '#60646C' },
|
||||
};
|
||||
|
||||
const STATUS_CLASSES = {
|
||||
const STATUS_CLASSES = computed(() => ({
|
||||
online: 'bg-n-teal-10',
|
||||
busy: 'bg-n-amber-10',
|
||||
offline: 'bg-n-slate-10',
|
||||
};
|
||||
...(props.hideOfflineStatus ? {} : { offline: 'bg-n-slate-10' }),
|
||||
}));
|
||||
|
||||
const showDefaultAvatar = computed(() => !props.src && !props.name);
|
||||
|
||||
@@ -174,21 +183,31 @@ watch(
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span class="relative inline-flex group/avatar z-0" :style="containerStyles">
|
||||
<span
|
||||
class="relative inline-flex group/avatar z-0 flex-shrink-0"
|
||||
:style="containerStyles"
|
||||
>
|
||||
<!-- Status Badge -->
|
||||
<slot name="badge" :size="size">
|
||||
<div
|
||||
v-if="status"
|
||||
v-if="status && STATUS_CLASSES[status]"
|
||||
class="absolute z-20 border rounded-full border-n-slate-3"
|
||||
:style="badgeStyles"
|
||||
:class="STATUS_CLASSES[status]"
|
||||
/>
|
||||
<div
|
||||
v-if="inbox && !(status && STATUS_CLASSES[status])"
|
||||
:style="badgeStyles"
|
||||
class="absolute z-20 flex items-center justify-center rounded-full bg-n-solid-1 border border-transparent flex-shrink-0"
|
||||
>
|
||||
<ChannelIcon :inbox="inbox" class="w-full h-full text-n-slate-11" />
|
||||
</div>
|
||||
</slot>
|
||||
|
||||
<!-- Delete Avatar Button -->
|
||||
<div
|
||||
v-if="src && allowUpload"
|
||||
class="absolute z-20 flex items-center justify-center invisible w-6 h-6 transition-all duration-300 ease-in-out opacity-0 cursor-pointer outline outline-1 outline-n-container -top-2 -right-2 rounded-xl bg-n-solid-3 group-hover/avatar:visible group-hover/avatar:opacity-100"
|
||||
class="absolute z-20 flex items-center justify-center invisible w-6 h-6 transition-all duration-300 ease-in-out opacity-0 cursor-pointer outline outline-1 outline-n-container -top-2 ltr:-right-2 rtl:-left-2 rounded-xl bg-n-solid-3 group-hover/avatar:visible group-hover/avatar:opacity-100"
|
||||
@click="handleDismiss"
|
||||
>
|
||||
<Icon icon="i-lucide-x" class="text-n-slate-11 size-4" />
|
||||
@@ -232,31 +251,40 @@ watch(
|
||||
<!-- Fallback Icon if no name or image -->
|
||||
<Icon
|
||||
v-else
|
||||
v-tooltip.top-start="t('THUMBNAIL.AUTHOR.NOT_AVAILABLE')"
|
||||
:title="t('THUMBNAIL.AUTHOR.NOT_AVAILABLE')"
|
||||
icon="i-lucide-user"
|
||||
:style="iconStyles"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- Upload Overlay and Input -->
|
||||
<div
|
||||
v-if="allowUpload"
|
||||
class="absolute inset-0 z-10 flex items-center justify-center invisible w-full h-full transition-all duration-300 ease-in-out opacity-0 rounded-xl bg-n-alpha-black1 group-hover/avatar:visible group-hover/avatar:opacity-100"
|
||||
@click="handleUploadAvatar"
|
||||
<slot
|
||||
v-if="allowUpload || $slots.overlay"
|
||||
name="overlay"
|
||||
:size="size"
|
||||
:handle-upload="handleUploadAvatar"
|
||||
:file-input-ref="fileInput"
|
||||
:handle-image-upload="handleImageUpload"
|
||||
>
|
||||
<Icon
|
||||
icon="i-lucide-upload"
|
||||
class="text-white"
|
||||
:style="{ width: `${size / 2}px`, height: `${size / 2}px` }"
|
||||
/>
|
||||
<input
|
||||
ref="fileInput"
|
||||
type="file"
|
||||
accept="image/png, image/jpeg, image/jpg, image/gif, image/webp"
|
||||
class="hidden"
|
||||
@change="handleImageUpload"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="absolute inset-0 z-10 flex items-center justify-center invisible w-full h-full transition-all duration-300 ease-in-out opacity-0 rounded-xl bg-n-alpha-black1 group-hover/avatar:visible group-hover/avatar:opacity-100"
|
||||
@click="handleUploadAvatar"
|
||||
>
|
||||
<Icon
|
||||
icon="i-lucide-upload"
|
||||
class="text-white"
|
||||
:style="{ width: `${size / 2}px`, height: `${size / 2}px` }"
|
||||
/>
|
||||
<input
|
||||
v-if="allowUpload"
|
||||
ref="fileInput"
|
||||
type="file"
|
||||
accept="image/png, image/jpeg, image/jpg, image/gif, image/webp"
|
||||
class="hidden"
|
||||
@change="handleImageUpload"
|
||||
/>
|
||||
</div>
|
||||
</slot>
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
@@ -29,32 +29,32 @@ const longBreadcrumb = ref([
|
||||
:layout="{ type: 'grid', width: '800px' }"
|
||||
>
|
||||
<Variant title="Single Item">
|
||||
<div class="w-full p-4 bg-white dark:bg-slate-900">
|
||||
<div class="w-full p-4 bg-n-background">
|
||||
<Breadcrumb :items="singleItem" />
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
<Variant title="Two Items">
|
||||
<div class="w-full p-4 bg-white dark:bg-slate-900">
|
||||
<div class="w-full p-4 bg-n-background">
|
||||
<Breadcrumb :items="twoItems" />
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
<Variant title="Three Items with Count">
|
||||
<div class="w-full p-4 bg-white dark:bg-slate-900">
|
||||
<div class="w-full p-4 bg-n-background">
|
||||
<Breadcrumb :items="threeItems" count-label="articles" />
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
<Variant title="Long Breadcrumb">
|
||||
<div class="w-full p-4 bg-white dark:bg-slate-900">
|
||||
<div class="w-full p-4 bg-n-background">
|
||||
<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">
|
||||
<div class="w-full p-4 bg-n-background">
|
||||
<Breadcrumb :items="threeItems" count-label="articles" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -15,8 +15,8 @@ const emit = defineEmits(['click']);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const onClick = event => {
|
||||
emit('click', event);
|
||||
const onClick = (item, index) => {
|
||||
emit('click', item, index);
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -24,24 +24,25 @@ const onClick = event => {
|
||||
<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">
|
||||
<Icon
|
||||
v-if="index > 0"
|
||||
icon="i-lucide-chevron-right"
|
||||
class="flex-shrink-0 mx-2 size-4 text-n-slate-11 dark:text-n-slate-11"
|
||||
/>
|
||||
|
||||
<!-- Render as button for all except the last item -->
|
||||
<button
|
||||
v-if="index === 0"
|
||||
v-if="index !== items.length - 1"
|
||||
class="inline-flex items-center justify-center min-w-0 gap-2 p-0 text-sm font-medium transition-all duration-200 ease-in-out border-0 rounded-lg text-n-slate-11 hover:text-n-slate-12 outline-transparent max-w-56"
|
||||
@click="onClick"
|
||||
@click="onClick(item, index)"
|
||||
>
|
||||
<span class="min-w-0 truncate">{{ item.label }}</span>
|
||||
</button>
|
||||
<template v-else>
|
||||
<Icon
|
||||
icon="i-lucide-chevron-right"
|
||||
class="flex-shrink-0 mx-2 size-4 text-n-slate-11 dark:text-n-slate-11"
|
||||
/>
|
||||
<span
|
||||
class="text-sm truncate text-slate-900 dark:text-slate-50 max-w-56"
|
||||
>
|
||||
{{ item.emoji ? item.emoji : '' }} {{ item.label }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<!-- The last breadcrumb item is plain text -->
|
||||
<span v-else class="text-sm truncate text-n-slate-12 max-w-56">
|
||||
{{ item.emoji ? item.emoji : '' }} {{ item.label }}
|
||||
</span>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
@@ -11,7 +11,7 @@ const SIZES = ['default', 'sm', 'lg'];
|
||||
<Story title="Components/Button" :layout="{ type: 'grid', width: '800px' }">
|
||||
<!-- Basic Variants -->
|
||||
<Variant title="Basic Variants">
|
||||
<div class="flex flex-wrap gap-2 p-4 bg-white dark:bg-slate-900">
|
||||
<div class="flex flex-wrap gap-2 p-4 bg-n-background">
|
||||
<Button
|
||||
v-for="variant in VARIANTS"
|
||||
:key="variant"
|
||||
@@ -23,7 +23,7 @@ const SIZES = ['default', 'sm', 'lg'];
|
||||
|
||||
<!-- Colors -->
|
||||
<Variant title="Color Variants">
|
||||
<div class="flex flex-wrap gap-2 p-4 bg-white dark:bg-slate-900">
|
||||
<div class="flex flex-wrap gap-2 p-4 bg-n-background">
|
||||
<Button
|
||||
v-for="color in COLORS"
|
||||
:key="color"
|
||||
@@ -35,16 +35,14 @@ const SIZES = ['default', 'sm', 'lg'];
|
||||
|
||||
<!-- Sizes -->
|
||||
<Variant title="Size Variants">
|
||||
<div
|
||||
class="flex flex-wrap items-center gap-2 p-4 bg-white dark:bg-slate-900"
|
||||
>
|
||||
<div class="flex flex-wrap items-center gap-2 p-4 bg-n-background">
|
||||
<Button v-for="size in SIZES" :key="size" :label="size" :size="size" />
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
<!-- Icons -->
|
||||
<Variant title="Icons">
|
||||
<div class="flex flex-wrap gap-2 p-4 bg-white dark:bg-slate-900">
|
||||
<div class="flex flex-wrap gap-2 p-4 bg-n-background">
|
||||
<Button label="Leading Icon" icon="i-lucide-plus" />
|
||||
<Button label="Trailing Icon" icon="i-lucide-plus" trailing-icon />
|
||||
<Button icon="i-lucide-plus" />
|
||||
@@ -53,7 +51,7 @@ const SIZES = ['default', 'sm', 'lg'];
|
||||
|
||||
<!-- Loading State -->
|
||||
<Variant title="Loading State">
|
||||
<div class="flex flex-wrap gap-2 p-4 bg-white dark:bg-slate-900">
|
||||
<div class="flex flex-wrap gap-2 p-4 bg-n-background">
|
||||
<Button label="Loading" is-loading />
|
||||
<Button label="Loading" variant="outline" is-loading />
|
||||
<Button is-loading icon="i-lucide-plus" />
|
||||
@@ -62,7 +60,7 @@ const SIZES = ['default', 'sm', 'lg'];
|
||||
|
||||
<!-- Disabled State -->
|
||||
<Variant title="Disabled State">
|
||||
<div class="flex flex-wrap gap-2 p-4 bg-white dark:bg-slate-900">
|
||||
<div class="flex flex-wrap gap-2 p-4 bg-n-background">
|
||||
<Button label="Disabled" disabled />
|
||||
<Button label="Disabled Outline" variant="outline" disabled />
|
||||
<Button label="Disabled Icon" icon="delete" disabled />
|
||||
@@ -78,7 +76,7 @@ const SIZES = ['default', 'sm', 'lg'];
|
||||
|
||||
<!-- Color Combinations -->
|
||||
<Variant title="Color & Variant Combinations">
|
||||
<div class="flex flex-wrap gap-2 p-4 bg-white dark:bg-slate-900">
|
||||
<div class="flex flex-wrap gap-2 p-4 bg-n-background">
|
||||
<template v-for="color in COLORS" :key="color">
|
||||
<Button
|
||||
v-for="variant in VARIANTS"
|
||||
@@ -93,7 +91,7 @@ const SIZES = ['default', 'sm', 'lg'];
|
||||
|
||||
<!-- Icon Positions -->
|
||||
<Variant title="Icon Positions & Sizes">
|
||||
<div class="flex flex-wrap gap-2 p-4 bg-white dark:bg-slate-900">
|
||||
<div class="flex flex-wrap gap-2 p-4 bg-n-background">
|
||||
<template v-for="size in SIZES" :key="size">
|
||||
<Button
|
||||
:label="`${size} Leading`"
|
||||
@@ -113,7 +111,7 @@ const SIZES = ['default', 'sm', 'lg'];
|
||||
|
||||
<!-- Ghost & Link Variants -->
|
||||
<Variant title="Ghost & Link Variants">
|
||||
<div class="flex flex-wrap gap-2 p-4 bg-white dark:bg-slate-900">
|
||||
<div class="flex flex-wrap gap-2 p-4 bg-n-background">
|
||||
<Button label="Ghost Button" variant="ghost" color="slate" />
|
||||
<Button
|
||||
label="Ghost with Icon"
|
||||
|
||||
@@ -15,7 +15,7 @@ const incrementCount = () => {
|
||||
:layout="{ type: 'grid', width: '400px' }"
|
||||
>
|
||||
<Variant title="Basic">
|
||||
<div class="grid gap-2 p-4 bg-white dark:bg-slate-900">
|
||||
<div class="grid gap-2 p-4 bg-n-background">
|
||||
<p>{{ count }}</p>
|
||||
<ConfirmButton
|
||||
label="Delete"
|
||||
@@ -26,7 +26,7 @@ const incrementCount = () => {
|
||||
</Variant>
|
||||
|
||||
<Variant title="Color Change">
|
||||
<div class="grid gap-2 p-4 bg-white dark:bg-slate-900">
|
||||
<div class="grid gap-2 p-4 bg-n-background">
|
||||
<p>{{ count }}</p>
|
||||
<ConfirmButton
|
||||
label="Archive"
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
<script setup>
|
||||
import { useRouter } from 'vue-router';
|
||||
import PaginationFooter from 'dashboard/components-next/pagination/PaginationFooter.vue';
|
||||
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
|
||||
import Breadcrumb from 'dashboard/components-next/breadcrumb/Breadcrumb.vue';
|
||||
|
||||
defineProps({
|
||||
isFetching: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isEmpty: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
currentPage: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
totalCount: {
|
||||
type: Number,
|
||||
default: 100,
|
||||
},
|
||||
itemsPerPage: {
|
||||
type: Number,
|
||||
default: 25,
|
||||
},
|
||||
showPaginationFooter: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
breadcrumbItems: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:currentPage']);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const handlePageChange = event => {
|
||||
emit('update:currentPage', event);
|
||||
};
|
||||
|
||||
const handleBreadcrumbClick = item => {
|
||||
router.push({
|
||||
name: item.routeName,
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section
|
||||
class="mt-4 px-10 flex flex-col w-full h-screen overflow-y-auto bg-n-background"
|
||||
>
|
||||
<div class="max-w-[60rem] mx-auto flex flex-col w-full h-full mb-4">
|
||||
<header class="mb-7 sticky top-0 z-10 bg-n-background">
|
||||
<Breadcrumb :items="breadcrumbItems" @click="handleBreadcrumbClick" />
|
||||
</header>
|
||||
<main class="flex gap-16 w-full flex-1 pb-16">
|
||||
<section
|
||||
v-if="$slots.body || $slots.emptyState || isFetching"
|
||||
class="flex flex-col w-full"
|
||||
>
|
||||
<div
|
||||
v-if="isFetching"
|
||||
class="flex items-center justify-center py-10 text-n-slate-11"
|
||||
>
|
||||
<Spinner />
|
||||
</div>
|
||||
<div v-else-if="isEmpty">
|
||||
<slot name="emptyState" />
|
||||
</div>
|
||||
<slot v-else name="body" />
|
||||
</section>
|
||||
<section v-if="$slots.controls" class="flex w-full">
|
||||
<slot name="controls" />
|
||||
</section>
|
||||
</main>
|
||||
<footer v-if="showPaginationFooter" class="sticky bottom-0 z-10 pb-4">
|
||||
<PaginationFooter
|
||||
:current-page="currentPage"
|
||||
:total-items="totalCount"
|
||||
:items-per-page="itemsPerPage"
|
||||
@update:current-page="handlePageChange"
|
||||
/>
|
||||
</footer>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
<script setup>
|
||||
import AddNewRulesDialog from './AddNewRulesDialog.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story
|
||||
title="Captain/Assistant/AddNewRulesDialog"
|
||||
:layout="{ type: 'grid', width: '800px' }"
|
||||
>
|
||||
<Variant title="Default">
|
||||
<div class="px-4 py-4 bg-n-background h-[200px]">
|
||||
<AddNewRulesDialog
|
||||
button-label="Add a guardrail"
|
||||
placeholder="Type in another guardrail..."
|
||||
confirm-label="Create"
|
||||
cancel-label="Cancel"
|
||||
/>
|
||||
</div>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
@@ -0,0 +1,81 @@
|
||||
<script setup>
|
||||
import { useToggle } from '@vueuse/core';
|
||||
import { vOnClickOutside } from '@vueuse/components';
|
||||
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import InlineInput from 'dashboard/components-next/inline-input/InlineInput.vue';
|
||||
|
||||
defineProps({
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
buttonLabel: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
confirmLabel: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
cancelLabel: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['add']);
|
||||
|
||||
const modelValue = defineModel({
|
||||
type: String,
|
||||
default: '',
|
||||
});
|
||||
|
||||
const [showPopover, togglePopover] = useToggle();
|
||||
const onClickAdd = () => {
|
||||
if (!modelValue.value?.trim()) return;
|
||||
emit('add', modelValue.value.trim());
|
||||
modelValue.value = '';
|
||||
togglePopover(false);
|
||||
};
|
||||
|
||||
const onClickCancel = () => {
|
||||
togglePopover(false);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-on-click-outside="() => togglePopover(false)"
|
||||
class="inline-flex relative"
|
||||
>
|
||||
<Button
|
||||
:label="buttonLabel"
|
||||
sm
|
||||
slate
|
||||
class="flex-shrink-0"
|
||||
@click="togglePopover(!showPopover)"
|
||||
/>
|
||||
<div
|
||||
v-if="showPopover"
|
||||
class="absolute w-[26.5rem] top-9 z-50 ltr:left-0 rtl:right-0 flex flex-col gap-5 bg-n-alpha-3 backdrop-blur-[100px] p-4 rounded-xl border border-n-weak shadow-md"
|
||||
>
|
||||
<InlineInput
|
||||
v-model="modelValue"
|
||||
:placeholder="placeholder"
|
||||
@keyup.enter="onClickAdd"
|
||||
/>
|
||||
<div class="flex gap-2 justify-between">
|
||||
<Button
|
||||
:label="cancelLabel"
|
||||
sm
|
||||
link
|
||||
slate
|
||||
class="h-10 hover:!no-underline"
|
||||
@click="onClickCancel"
|
||||
/>
|
||||
<Button :label="confirmLabel" sm @click="onClickAdd" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,19 @@
|
||||
<script setup>
|
||||
import AddNewRulesInput from './AddNewRulesInput.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story
|
||||
title="Captain/Assistant/AddNewRulesInput"
|
||||
:layout="{ type: 'grid', width: '800px' }"
|
||||
>
|
||||
<Variant title="Default">
|
||||
<div class="px-6 py-4 bg-n-background">
|
||||
<AddNewRulesInput
|
||||
placeholder="Type in another response guideline..."
|
||||
label="Add and save (↵)"
|
||||
/>
|
||||
</div>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
@@ -0,0 +1,51 @@
|
||||
<script setup>
|
||||
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import InlineInput from 'dashboard/components-next/inline-input/InlineInput.vue';
|
||||
|
||||
defineProps({
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['add']);
|
||||
|
||||
const modelValue = defineModel({
|
||||
type: String,
|
||||
default: '',
|
||||
});
|
||||
|
||||
const onClickAdd = () => {
|
||||
if (!modelValue.value?.trim()) return;
|
||||
emit('add', modelValue.value.trim());
|
||||
modelValue.value = '';
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex py-3 ltr:pl-3 h-16 rtl:pr-3 ltr:pr-4 rtl:pl-4 items-center gap-3 rounded-xl bg-n-solid-2 outline-1 outline outline-n-container"
|
||||
>
|
||||
<Icon icon="i-lucide-plus" class="text-n-slate-10 size-5 flex-shrink-0" />
|
||||
|
||||
<InlineInput
|
||||
v-model="modelValue"
|
||||
:placeholder="placeholder"
|
||||
@keyup.enter="onClickAdd"
|
||||
/>
|
||||
<Button
|
||||
:label="label"
|
||||
ghost
|
||||
xs
|
||||
slate
|
||||
class="!text-sm !text-n-slate-11 flex-shrink-0"
|
||||
@click="onClickAdd"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,155 @@
|
||||
<script setup>
|
||||
import { computed, reactive } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useToggle } from '@vueuse/core';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { vOnClickOutside } from '@vueuse/components';
|
||||
import { required, minLength } from '@vuelidate/validators';
|
||||
|
||||
import Input from 'dashboard/components-next/input/Input.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import TextArea from 'dashboard/components-next/textarea/TextArea.vue';
|
||||
import Editor from 'dashboard/components-next/Editor/Editor.vue';
|
||||
|
||||
const emit = defineEmits(['add']);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const [showPopover, togglePopover] = useToggle();
|
||||
|
||||
const state = reactive({
|
||||
id: '',
|
||||
title: '',
|
||||
description: '',
|
||||
instruction: '',
|
||||
});
|
||||
|
||||
const rules = {
|
||||
title: { required, minLength: minLength(1) },
|
||||
description: { required },
|
||||
instruction: { required },
|
||||
};
|
||||
|
||||
const v$ = useVuelidate(rules, state);
|
||||
|
||||
const titleError = computed(() =>
|
||||
v$.value.title.$error
|
||||
? t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.TITLE.ERROR')
|
||||
: ''
|
||||
);
|
||||
|
||||
const descriptionError = computed(() =>
|
||||
v$.value.description.$error
|
||||
? t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.DESCRIPTION.ERROR')
|
||||
: ''
|
||||
);
|
||||
|
||||
const instructionError = computed(() =>
|
||||
v$.value.instruction.$error
|
||||
? t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.INSTRUCTION.ERROR')
|
||||
: ''
|
||||
);
|
||||
|
||||
const resetState = () => {
|
||||
Object.assign(state, {
|
||||
id: '',
|
||||
title: '',
|
||||
description: '',
|
||||
instruction: '',
|
||||
});
|
||||
};
|
||||
|
||||
const onClickAdd = async () => {
|
||||
v$.value.$touch();
|
||||
if (v$.value.$invalid) return;
|
||||
|
||||
await emit('add', state);
|
||||
resetState();
|
||||
togglePopover(false);
|
||||
};
|
||||
|
||||
const onClickCancel = () => {
|
||||
togglePopover(false);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-on-click-outside="() => togglePopover(false)"
|
||||
class="inline-flex relative"
|
||||
>
|
||||
<Button
|
||||
:label="t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.CREATE')"
|
||||
sm
|
||||
slate
|
||||
class="flex-shrink-0"
|
||||
@click="togglePopover(!showPopover)"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="showPopover"
|
||||
class="w-[31.25rem] absolute top-10 ltr:left-0 rtl:right-0 bg-n-alpha-3 backdrop-blur-[100px] p-6 rounded-xl border border-n-weak shadow-md flex flex-col gap-6 z-50"
|
||||
>
|
||||
<h3 class="text-base font-medium text-n-slate-12">
|
||||
{{ t(`CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.TITLE`) }}
|
||||
</h3>
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<Input
|
||||
v-model="state.title"
|
||||
:label="t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.TITLE.LABEL')"
|
||||
:placeholder="
|
||||
t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.TITLE.PLACEHOLDER')
|
||||
"
|
||||
:message="titleError"
|
||||
:message-type="titleError ? 'error' : 'info'"
|
||||
/>
|
||||
|
||||
<TextArea
|
||||
v-model="state.description"
|
||||
:label="
|
||||
t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.DESCRIPTION.LABEL')
|
||||
"
|
||||
:placeholder="
|
||||
t(
|
||||
'CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.DESCRIPTION.PLACEHOLDER'
|
||||
)
|
||||
"
|
||||
:message="descriptionError"
|
||||
:message-type="descriptionError ? 'error' : 'info'"
|
||||
show-character-count
|
||||
/>
|
||||
<Editor
|
||||
v-model="state.instruction"
|
||||
:label="
|
||||
t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.INSTRUCTION.LABEL')
|
||||
"
|
||||
:placeholder="
|
||||
t(
|
||||
'CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.INSTRUCTION.PLACEHOLDER'
|
||||
)
|
||||
"
|
||||
:message="instructionError"
|
||||
:message-type="instructionError ? 'error' : 'info'"
|
||||
:show-character-count="false"
|
||||
enable-captain-tools
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between w-full gap-3">
|
||||
<Button
|
||||
variant="faded"
|
||||
color="slate"
|
||||
:label="t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.CANCEL')"
|
||||
class="w-full bg-n-alpha-2 !text-n-blue-text hover:bg-n-alpha-3"
|
||||
@click="onClickCancel"
|
||||
/>
|
||||
<Button
|
||||
:label="t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.CREATE')"
|
||||
class="w-full"
|
||||
@click="onClickAdd"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -12,7 +12,7 @@ import { assistantsList } from 'dashboard/components-next/captain/pageComponents
|
||||
<div
|
||||
v-for="(assistant, index) in assistantsList"
|
||||
:key="index"
|
||||
class="px-20 py-4 bg-white dark:bg-slate-900"
|
||||
class="px-20 py-4 bg-n-background"
|
||||
>
|
||||
<AssistantCard
|
||||
:id="assistant.id"
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import Checkbox from 'dashboard/components-next/checkbox/Checkbox.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
const props = defineProps({
|
||||
allItems: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
selectAllLabel: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
selectedCountLabel: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
deleteLabel: {
|
||||
type: String,
|
||||
default: 'Delete',
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['bulkDelete']);
|
||||
|
||||
const modelValue = defineModel({
|
||||
type: Set,
|
||||
default: () => new Set(),
|
||||
});
|
||||
|
||||
const selectedCount = computed(() => modelValue.value.size);
|
||||
const totalCount = computed(() => props.allItems.length);
|
||||
|
||||
const hasSelected = computed(() => selectedCount.value > 0);
|
||||
const isIndeterminate = computed(
|
||||
() => hasSelected.value && selectedCount.value < totalCount.value
|
||||
);
|
||||
const allSelected = computed(
|
||||
() => totalCount.value > 0 && selectedCount.value === totalCount.value
|
||||
);
|
||||
|
||||
const bulkCheckboxState = computed({
|
||||
get: () => allSelected.value,
|
||||
set: shouldSelectAll => {
|
||||
const newSelectedIds = shouldSelectAll
|
||||
? new Set(props.allItems.map(item => item.id))
|
||||
: new Set();
|
||||
modelValue.value = newSelectedIds;
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<transition
|
||||
name="slide-fade"
|
||||
enter-active-class="transition-all duration-300 ease-out"
|
||||
enter-from-class="opacity-0 transform ltr:-translate-x-4 rtl:translate-x-4"
|
||||
enter-to-class="opacity-100 transform translate-x-0"
|
||||
leave-active-class="hidden opacity-0"
|
||||
>
|
||||
<div
|
||||
v-if="hasSelected"
|
||||
class="flex items-center gap-3 py-1 ltr:pl-3 rtl:pr-3 ltr:pr-4 rtl:pl-4 rounded-lg bg-n-solid-2 outline outline-1 outline-n-container shadow"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<Checkbox
|
||||
v-model="bulkCheckboxState"
|
||||
:indeterminate="isIndeterminate"
|
||||
/>
|
||||
<span class="text-sm font-medium text-n-slate-12 tabular-nums">
|
||||
{{ selectAllLabel }}
|
||||
</span>
|
||||
</div>
|
||||
<span class="text-sm text-n-slate-10 tabular-nums">
|
||||
{{ selectedCountLabel }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="h-4 w-px bg-n-strong" />
|
||||
<div class="flex items-center gap-3">
|
||||
<slot name="actions" :selected-count="selectedCount">
|
||||
<Button
|
||||
:label="deleteLabel"
|
||||
sm
|
||||
ruby
|
||||
ghost
|
||||
class="!px-1.5"
|
||||
icon="i-lucide-trash"
|
||||
@click="emit('bulkDelete')"
|
||||
/>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="flex items-center gap-3">
|
||||
<slot name="default-actions" />
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
@@ -12,7 +12,7 @@ import { documentsList } from 'dashboard/components-next/captain/pageComponents/
|
||||
<div
|
||||
v-for="(doc, index) in documentsList"
|
||||
:key="index"
|
||||
class="px-20 py-4 bg-white dark:bg-slate-900"
|
||||
class="px-20 py-4 bg-n-background"
|
||||
>
|
||||
<DocumentCard
|
||||
:id="doc.id"
|
||||
|
||||
@@ -12,7 +12,7 @@ import { inboxes } from 'dashboard/components-next/captain/pageComponents/emptyS
|
||||
<div
|
||||
v-for="inbox in inboxes"
|
||||
:key="inbox.id"
|
||||
class="px-20 py-4 bg-white dark:bg-slate-900"
|
||||
class="px-20 py-4 bg-n-background"
|
||||
>
|
||||
<InboxCard :id="inbox.id" :inbox="inbox" />
|
||||
</div>
|
||||
|
||||
@@ -57,9 +57,10 @@ const menuItems = computed(() => [
|
||||
},
|
||||
]);
|
||||
|
||||
const icon = computed(() =>
|
||||
getInboxIconByType(props.inbox.channel_type, '', 'outline')
|
||||
);
|
||||
const icon = computed(() => {
|
||||
const { medium, channel_type: type } = props.inbox;
|
||||
return getInboxIconByType(type, medium, 'outline');
|
||||
});
|
||||
|
||||
const handleAction = ({ action, value }) => {
|
||||
toggleDropdown(false);
|
||||
|
||||
@@ -12,7 +12,7 @@ import { responsesList } from 'dashboard/components-next/captain/pageComponents/
|
||||
<div
|
||||
v-for="(response, index) in responsesList"
|
||||
:key="index"
|
||||
class="px-20 py-4 bg-white dark:bg-slate-900"
|
||||
class="px-20 py-4 bg-n-background"
|
||||
>
|
||||
<ResponseCard
|
||||
:id="response.id"
|
||||
|
||||
@@ -55,6 +55,10 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showMenu: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['action', 'navigate', 'select', 'hover']);
|
||||
@@ -130,7 +134,7 @@ const handleDocumentableClick = () => {
|
||||
<span class="text-base text-n-slate-12 line-clamp-1">
|
||||
{{ question }}
|
||||
</span>
|
||||
<div v-if="!compact" class="flex items-center gap-2">
|
||||
<div v-if="!compact && showMenu" class="flex items-center gap-2">
|
||||
<Policy
|
||||
v-on-clickaway="() => toggleDropdown(false)"
|
||||
:permissions="['administrator']"
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<script setup>
|
||||
import RuleCard from './RuleCard.vue';
|
||||
|
||||
const sampleRules = [
|
||||
{ id: 1, content: 'Block sensitive personal information', selectable: true },
|
||||
{ id: 2, content: 'Reject offensive language', selectable: true },
|
||||
{ id: 3, content: 'Deflect legal or medical advice', selectable: true },
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story
|
||||
title="Captain/Assistant/RuleCard"
|
||||
:layout="{ type: 'grid', width: '800px' }"
|
||||
>
|
||||
<Variant title="Selectable List">
|
||||
<div class="flex flex-col gap-4 px-20 py-4 bg-n-background">
|
||||
<RuleCard
|
||||
v-for="rule in sampleRules"
|
||||
:id="rule.id"
|
||||
:key="rule.id"
|
||||
:content="rule.content"
|
||||
:selectable="rule.selectable"
|
||||
@select="id => console.log('Selected rule', id)"
|
||||
@edit="id => console.log('Edit', id)"
|
||||
@delete="id => console.log('Delete', id)"
|
||||
/>
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
<Variant title="Non-Selectable">
|
||||
<div class="flex flex-col gap-4 px-20 py-4 bg-n-background">
|
||||
<RuleCard id="4" content="Replies should be friendly and clear." />
|
||||
</div>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
@@ -0,0 +1,93 @@
|
||||
<script setup>
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import CardLayout from 'dashboard/components-next/CardLayout.vue';
|
||||
import Checkbox from 'dashboard/components-next/checkbox/Checkbox.vue';
|
||||
import InlineInput from 'dashboard/components-next/inline-input/InlineInput.vue';
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
content: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
selectable: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isSelected: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['select', 'hover', 'edit', 'delete']);
|
||||
|
||||
const modelValue = computed({
|
||||
get: () => props.isSelected,
|
||||
set: () => emit('select', props.id),
|
||||
});
|
||||
|
||||
const isEditing = ref(false);
|
||||
const editedContent = ref(props.content);
|
||||
|
||||
// Local content to display to avoid flicker until parent prop updates on inline edit
|
||||
const localContent = ref(props.content);
|
||||
|
||||
// Keeps localContent in sync when parent updates content prop
|
||||
watch(
|
||||
() => props.content,
|
||||
newVal => {
|
||||
localContent.value = newVal;
|
||||
}
|
||||
);
|
||||
|
||||
const startEdit = () => {
|
||||
isEditing.value = true;
|
||||
editedContent.value = props.content;
|
||||
};
|
||||
|
||||
const saveEdit = () => {
|
||||
isEditing.value = false;
|
||||
// Update local content
|
||||
localContent.value = editedContent.value;
|
||||
emit('edit', { id: props.id, content: editedContent.value });
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CardLayout
|
||||
selectable
|
||||
class="relative [&>div]:!py-5 [&>div]:ltr:!pr-4 [&>div]:rtl:!pl-4"
|
||||
layout="row"
|
||||
@mouseenter="emit('hover', true)"
|
||||
@mouseleave="emit('hover', false)"
|
||||
>
|
||||
<div v-show="selectable" class="absolute top-6 ltr:left-3 rtl:right-3">
|
||||
<Checkbox v-model="modelValue" />
|
||||
</div>
|
||||
<InlineInput
|
||||
v-if="isEditing"
|
||||
v-model="editedContent"
|
||||
focus-on-mount
|
||||
@keyup.enter="saveEdit"
|
||||
/>
|
||||
<span v-else class="flex items-center gap-2 text-sm text-n-slate-12">
|
||||
{{ localContent }}
|
||||
</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button icon="i-lucide-pen" slate xs ghost @click="startEdit" />
|
||||
<span class="w-px h-4 bg-n-weak" />
|
||||
<Button
|
||||
icon="i-lucide-trash"
|
||||
slate
|
||||
xs
|
||||
ghost
|
||||
@click="emit('delete', id)"
|
||||
/>
|
||||
</div>
|
||||
</CardLayout>
|
||||
</template>
|
||||
@@ -0,0 +1,45 @@
|
||||
<script setup>
|
||||
import ScenariosCard from './ScenariosCard.vue';
|
||||
|
||||
const sampleScenarios = [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Refund Order',
|
||||
description: 'User requests a refund for a recent purchase.',
|
||||
instruction:
|
||||
'Gather order details and reason for refund. Use [Order Search](tool://order_search) then submit with [Refund Payment](tool://refund_payment).',
|
||||
tools: ['order_search', 'refund_payment'],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Bug Report',
|
||||
description: 'Customer reports a bug in the mobile app.',
|
||||
instruction:
|
||||
'Ask for reproduction steps and environment. Check [Known Issues](tool://known_issues) then create ticket with [Create Bug Report](tool://bug_report_create).',
|
||||
tools: ['known_issues', 'bug_report_create'],
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story
|
||||
title="Captain/Assistant/ScenariosCard"
|
||||
:layout="{ type: 'grid', width: '800px' }"
|
||||
>
|
||||
<Variant title="Default">
|
||||
<div
|
||||
v-for="scenario in sampleScenarios"
|
||||
:key="scenario.id"
|
||||
class="px-4 py-4 bg-n-background"
|
||||
>
|
||||
<ScenariosCard
|
||||
:id="scenario.id"
|
||||
:title="scenario.title"
|
||||
:description="scenario.description"
|
||||
:instruction="scenario.instruction"
|
||||
:tools="scenario.tools"
|
||||
/>
|
||||
</div>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
@@ -0,0 +1,218 @@
|
||||
<script setup>
|
||||
import { computed, h, reactive } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useToggle } from '@vueuse/core';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { required, minLength } from '@vuelidate/validators';
|
||||
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||
import Input from 'dashboard/components-next/input/Input.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import TextArea from 'dashboard/components-next/textarea/TextArea.vue';
|
||||
import Editor from 'dashboard/components-next/Editor/Editor.vue';
|
||||
import CardLayout from 'dashboard/components-next/CardLayout.vue';
|
||||
import Checkbox from 'dashboard/components-next/checkbox/Checkbox.vue';
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
instruction: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
tools: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
selectable: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isSelected: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['select', 'hover', 'delete', 'update']);
|
||||
|
||||
const { t } = useI18n();
|
||||
const { formatMessage } = useMessageFormatter();
|
||||
|
||||
const modelValue = computed({
|
||||
get: () => props.isSelected,
|
||||
set: () => emit('select', props.id),
|
||||
});
|
||||
|
||||
const state = reactive({
|
||||
id: '',
|
||||
title: '',
|
||||
description: '',
|
||||
instruction: '',
|
||||
});
|
||||
|
||||
const [isEditing, toggleEditing] = useToggle();
|
||||
|
||||
const startEdit = () => {
|
||||
Object.assign(state, {
|
||||
id: props.id,
|
||||
title: props.title,
|
||||
description: props.description,
|
||||
instruction: props.instruction,
|
||||
tools: props.tools,
|
||||
});
|
||||
toggleEditing(true);
|
||||
};
|
||||
|
||||
const rules = {
|
||||
title: { required, minLength: minLength(1) },
|
||||
description: { required },
|
||||
instruction: { required },
|
||||
};
|
||||
|
||||
const v$ = useVuelidate(rules, state);
|
||||
|
||||
const titleError = computed(() =>
|
||||
v$.value.title.$error
|
||||
? t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.TITLE.ERROR')
|
||||
: ''
|
||||
);
|
||||
|
||||
const descriptionError = computed(() =>
|
||||
v$.value.description.$error
|
||||
? t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.DESCRIPTION.ERROR')
|
||||
: ''
|
||||
);
|
||||
|
||||
const onClickUpdate = () => {
|
||||
v$.value.$touch();
|
||||
if (v$.value.$invalid) return;
|
||||
emit('update', { ...state });
|
||||
toggleEditing(false);
|
||||
};
|
||||
|
||||
const instructionError = computed(() =>
|
||||
v$.value.instruction.$error
|
||||
? t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.INSTRUCTION.ERROR')
|
||||
: ''
|
||||
);
|
||||
|
||||
const LINK_INSTRUCTION_CLASS =
|
||||
'[&_a[href^="tool://"]]:text-n-iris-11 [&_a:not([href^="tool://"])]:text-n-slate-12 [&_a]:pointer-events-none [&_a]:cursor-default';
|
||||
|
||||
const renderInstruction = instruction => () =>
|
||||
h('p', {
|
||||
class: `text-sm text-n-slate-12 py-4 mb-0 [&_ol]:list-decimal ${LINK_INSTRUCTION_CLASS}`,
|
||||
innerHTML: instruction,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CardLayout
|
||||
selectable
|
||||
class="relative [&>div]:!py-4"
|
||||
:class="{
|
||||
'[&>div]:ltr:!pr-4 [&>div]:rtl:!pl-4': !isEditing,
|
||||
'[&>div]:ltr:!pr-10 [&>div]:rtl:!pl-10': isEditing,
|
||||
}"
|
||||
layout="row"
|
||||
@mouseenter="emit('hover', true)"
|
||||
@mouseleave="emit('hover', false)"
|
||||
>
|
||||
<div
|
||||
v-show="selectable && !isEditing"
|
||||
class="absolute top-[1.125rem] ltr:left-3 rtl:right-3"
|
||||
>
|
||||
<Checkbox v-model="modelValue" />
|
||||
</div>
|
||||
|
||||
<div v-if="!isEditing" class="flex flex-col w-full">
|
||||
<div class="flex items-start justify-between w-full gap-2">
|
||||
<div class="flex flex-col items-start">
|
||||
<span class="text-sm text-n-slate-12 font-medium">{{ title }}</span>
|
||||
<span class="text-sm text-n-slate-11 mt-2">
|
||||
{{ description }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- <Button label="Test" slate xs ghost class="!text-sm" />
|
||||
<span class="w-px h-4 bg-n-weak" /> -->
|
||||
<Button icon="i-lucide-pen" slate xs ghost @click="startEdit" />
|
||||
<span class="w-px h-4 bg-n-weak" />
|
||||
<Button
|
||||
icon="i-lucide-trash"
|
||||
slate
|
||||
xs
|
||||
ghost
|
||||
@click="emit('delete', id)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<component :is="renderInstruction(formatMessage(instruction, false))" />
|
||||
<span class="text-sm text-n-slate-11 font-medium mb-1">
|
||||
{{ t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.SUGGESTED.TOOLS_USED') }}
|
||||
{{ tools?.map(tool => `@${tool}`).join(', ') }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-else class="overflow-hidden flex flex-col gap-4 w-full">
|
||||
<Input
|
||||
v-model="state.title"
|
||||
:label="t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.TITLE.LABEL')"
|
||||
:placeholder="
|
||||
t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.TITLE.PLACEHOLDER')
|
||||
"
|
||||
:message="titleError"
|
||||
:message-type="titleError ? 'error' : 'info'"
|
||||
/>
|
||||
|
||||
<TextArea
|
||||
v-model="state.description"
|
||||
:label="
|
||||
t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.DESCRIPTION.LABEL')
|
||||
"
|
||||
:placeholder="
|
||||
t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.DESCRIPTION.PLACEHOLDER')
|
||||
"
|
||||
:message="descriptionError"
|
||||
:message-type="descriptionError ? 'error' : 'info'"
|
||||
show-character-count
|
||||
/>
|
||||
<Editor
|
||||
v-model="state.instruction"
|
||||
:label="
|
||||
t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.INSTRUCTION.LABEL')
|
||||
"
|
||||
:placeholder="
|
||||
t('CAPTAIN.ASSISTANTS.SCENARIOS.ADD.NEW.FORM.INSTRUCTION.PLACEHOLDER')
|
||||
"
|
||||
:message="instructionError"
|
||||
:message-type="instructionError ? 'error' : 'info'"
|
||||
:show-character-count="false"
|
||||
enable-captain-tools
|
||||
/>
|
||||
<div class="flex items-center gap-3">
|
||||
<Button
|
||||
faded
|
||||
slate
|
||||
sm
|
||||
:label="t('CAPTAIN.ASSISTANTS.SCENARIOS.UPDATE.CANCEL')"
|
||||
@click="toggleEditing(false)"
|
||||
/>
|
||||
<Button
|
||||
sm
|
||||
:label="t('CAPTAIN.ASSISTANTS.SCENARIOS.UPDATE.UPDATE')"
|
||||
@click="onClickUpdate"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardLayout>
|
||||
</template>
|
||||
@@ -0,0 +1,46 @@
|
||||
<script setup>
|
||||
import SuggestedRules from './SuggestedRules.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
const guidelinesExample = [
|
||||
{
|
||||
content:
|
||||
'Block queries that share or request sensitive personal information (e.g. phone numbers, passwords).',
|
||||
},
|
||||
{
|
||||
content:
|
||||
'Reject queries that include offensive, discriminatory, or threatening language.',
|
||||
},
|
||||
{
|
||||
content:
|
||||
'Deflect when the assistant is asked for legal or medical diagnosis or treatment.',
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story
|
||||
title="Captain/Assistant/SuggestedRules"
|
||||
:layout="{ type: 'grid', width: '800px' }"
|
||||
>
|
||||
<Variant title="Suggested Rules List">
|
||||
<div class="px-20 py-4 bg-n-background">
|
||||
<SuggestedRules
|
||||
title="Example response guidelines"
|
||||
:items="guidelinesExample"
|
||||
>
|
||||
<template #default="{ item }">
|
||||
<span class="text-sm text-n-slate-12">{{ item.content }}</span>
|
||||
<Button
|
||||
label="Add this"
|
||||
ghost
|
||||
xs
|
||||
slate
|
||||
class="!text-sm !text-n-slate-11 flex-shrink-0"
|
||||
/>
|
||||
</template>
|
||||
</SuggestedRules>
|
||||
</div>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
@@ -0,0 +1,63 @@
|
||||
<script setup>
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['add', 'close']);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const onAddClick = () => {
|
||||
emit('add');
|
||||
};
|
||||
|
||||
const onClickClose = () => {
|
||||
emit('close');
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col items-start self-stretch rounded-xl w-full overflow-hidden border border-dashed border-n-strong"
|
||||
>
|
||||
<div class="flex items-center justify-between w-full gap-3 px-4 pb-1 pt-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<h5 class="text-sm font-medium text-n-slate-11">{{ title }}</h5>
|
||||
<span class="h-3 w-px bg-n-weak" />
|
||||
<Button
|
||||
:label="t('CAPTAIN.ASSISTANTS.GUARDRAILS.ADD.SUGGESTED.ADD')"
|
||||
ghost
|
||||
xs
|
||||
slate
|
||||
class="!text-sm !text-n-slate-11 flex-shrink-0"
|
||||
@click="onAddClick"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
ghost
|
||||
xs
|
||||
slate
|
||||
icon="i-lucide-x"
|
||||
class="!text-sm !text-n-slate-11 flex-shrink-0"
|
||||
@click="onClickClose"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-col items-start divide-y divide-n-strong divide-dashed w-full"
|
||||
>
|
||||
<div v-for="item in items" :key="item.content" class="w-full px-4 py-4">
|
||||
<slot :item="item" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,37 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import ToolsDropdown from './ToolsDropdown.vue';
|
||||
|
||||
const items = [
|
||||
{
|
||||
id: 'order_search',
|
||||
title: 'Order Search',
|
||||
description: 'Lookup orders by customer ID, email, or order number',
|
||||
},
|
||||
{
|
||||
id: 'refund_payment',
|
||||
title: 'Refund Payment',
|
||||
description: 'Initiates a refund on a specific payment',
|
||||
},
|
||||
{
|
||||
id: 'fetch_customer',
|
||||
title: 'Fetch Customer',
|
||||
description: 'Pulls customer details (email, tags, last seen, etc.)',
|
||||
},
|
||||
];
|
||||
|
||||
const selectedIndex = ref(0);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story
|
||||
title="Captain/Assistant/ToolsDropdown"
|
||||
:layout="{ type: 'grid', width: '600px' }"
|
||||
>
|
||||
<Variant title="Default">
|
||||
<div class="relative h-80 bg-n-background p-4">
|
||||
<ToolsDropdown :items="items" :selected-index="selectedIndex" />
|
||||
</div>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user