Compare commits

..
Author SHA1 Message Date
Shivam Mishra 670ab2400a feat: disable matches filter 2025-03-27 16:31:52 +05:30
Shivam Mishra 4a800eb8d1 chore: get changes from dbc1b492 2025-03-27 16:23:12 +05:30
Shivam Mishra 1bf1075427 feat: disable get filtered conversations 2025-03-27 16:16:22 +05:30
241 changed files with 2962 additions and 6209 deletions
-3
View File
@@ -91,6 +91,3 @@ yarn-debug.log*
# Vite uses dotenv and suggests to ignore local-only env files. See
# https://vitejs.dev/guide/env-and-mode.html#env-files
*.local
# Claude.ai config file
CLAUDE.md
-1
View File
@@ -174,7 +174,6 @@ gem 'pgvector'
gem 'reverse_markdown'
gem 'iso-639'
gem 'ruby_llm', '1.1.0rc1'
gem 'ruby-openai'
gem 'shopify_api'
-8
View File
@@ -698,13 +698,6 @@ GEM
ruby2ruby (2.5.0)
ruby_parser (~> 3.1)
sexp_processor (~> 4.6)
ruby_llm (1.1.0rc1)
base64
event_stream_parser (~> 1)
faraday (~> 2)
faraday-multipart (~> 1)
faraday-retry (~> 2)
zeitwerk (~> 2)
ruby_parser (3.20.0)
sexp_processor (~> 4.16)
sass (3.7.4)
@@ -971,7 +964,6 @@ DEPENDENCIES
rubocop-rails
rubocop-rspec
ruby-openai
ruby_llm (= 1.1.0rc1)
scout_apm
scss_lint
seed_dump
@@ -1,21 +1,17 @@
class Api::V1::Accounts::Contacts::ConversationsController < Api::V1::Accounts::Contacts::BaseController
def index
# Start with all conversations for this contact
conversations = Current.account.conversations.includes(
@conversations = Current.account.conversations.includes(
:assignee, :contact, :inbox, :taggings
).where(contact_id: @contact.id)
).where(inbox_id: inbox_ids, contact_id: @contact.id).order(last_activity_at: :desc).limit(20)
end
# Apply permission-based filtering using the existing service
conversations = Conversations::PermissionFilterService.new(
conversations,
Current.user,
Current.account
).perform
private
# Only allow conversations from inboxes the user has access to
inbox_ids = Current.user.assigned_inboxes.pluck(:id)
conversations = conversations.where(inbox_id: inbox_ids)
@conversations = conversations.order(last_activity_at: :desc).limit(20)
def inbox_ids
if Current.user.administrator? || Current.user.agent?
Current.user.assigned_inboxes.pluck(:id)
else
[]
end
end
end
@@ -48,7 +48,7 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
end
def filter
result = ::Conversations::FilterService.new(params.permit!, current_user, current_account).perform
result = ::Conversations::FilterService.new(params.permit!, current_user).perform
@conversations = result[:conversations]
@conversations_count = result[:count]
rescue CustomExceptions::CustomFilter::InvalidAttribute,
@@ -1,30 +0,0 @@
class Api::V1::Accounts::Instagram::AuthorizationsController < Api::V1::Accounts::BaseController
include InstagramConcern
include Instagram::IntegrationHelper
before_action :check_authorization
def create
# https://developers.facebook.com/docs/instagram-platform/instagram-api-with-instagram-login/business-login#step-1--get-authorization
redirect_url = instagram_client.auth_code.authorize_url(
{
redirect_uri: "#{base_url}/instagram/callback",
scope: REQUIRED_SCOPES.join(','),
enable_fb_login: '0',
force_authentication: '1',
response_type: 'code',
state: generate_instagram_token(Current.account.id)
}
)
if redirect_url
render json: { success: true, url: redirect_url }
else
render json: { success: false }, status: :unprocessable_entity
end
end
private
def check_authorization
raise Pundit::NotAuthorizedError unless Current.account_user.administrator?
end
end
@@ -9,6 +9,11 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
@portals = Current.account.portals
end
def add_members
agents = Current.account.agents.where(id: portal_member_params[:member_ids])
@portal.members << agents
end
def show
@all_articles = @portal.articles
@articles = @all_articles.search(locale: params[:locale])
@@ -80,6 +85,10 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
{ channel_web_widget_id: inbox.channel.id }
end
def portal_member_params
params.require(:portal).permit(:account_id, member_ids: [])
end
def set_current_page
@current_page = params[:page] || 1
end
@@ -66,7 +66,9 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
end
def check_authorization
authorize :report, :view?
return if Current.account_user.administrator?
raise Pundit::NotAuthorizedError
end
def common_params
@@ -135,3 +137,5 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
V2::ReportBuilder.new(Current.account, conversation_params).conversation_metrics
end
end
Api::V2::Accounts::ReportsController.prepend_mod_with('Api::V2::Accounts::ReportsController')
@@ -1,75 +0,0 @@
module InstagramConcern
extend ActiveSupport::Concern
include HTTParty
def instagram_client
::OAuth2::Client.new(
client_id,
client_secret,
{
site: 'https://api.instagram.com',
authorize_url: 'https://api.instagram.com/oauth/authorize',
token_url: 'https://api.instagram.com/oauth/access_token',
auth_scheme: :request_body,
token_method: :post
}
)
end
private
def client_id
GlobalConfigService.load('INSTAGRAM_APP_ID', nil)
end
def client_secret
GlobalConfigService.load('INSTAGRAM_APP_SECRET', nil)
end
def exchange_for_long_lived_token(short_lived_token)
endpoint = 'https://graph.instagram.com/access_token'
params = {
grant_type: 'ig_exchange_token',
client_secret: client_secret,
access_token: short_lived_token,
client_id: client_id
}
make_api_request(endpoint, params, 'Failed to exchange token')
end
def fetch_instagram_user_details(access_token)
endpoint = 'https://graph.instagram.com/v22.0/me'
params = {
fields: 'id,username,user_id,name,profile_picture_url,account_type',
access_token: access_token
}
make_api_request(endpoint, params, 'Failed to fetch Instagram user details')
end
def make_api_request(endpoint, params, error_prefix)
response = HTTParty.get(
endpoint,
query: params,
headers: { 'Accept' => 'application/json' }
)
unless response.success?
Rails.logger.error "#{error_prefix}. Status: #{response.code}, Body: #{response.body}"
raise "#{error_prefix}: #{response.body}"
end
begin
JSON.parse(response.body)
rescue JSON::ParserError => e
ChatwootExceptionTracker.new(e).capture_exception
Rails.logger.error "Invalid JSON response: #{response.body}"
raise e
end
end
def base_url
ENV.fetch('FRONTEND_URL', 'http://localhost:3000')
end
end
@@ -1,163 +0,0 @@
class Instagram::CallbacksController < ApplicationController
include InstagramConcern
include Instagram::IntegrationHelper
def show
# Check if Instagram redirected with an error (user canceled authorization)
# See: https://developers.facebook.com/docs/instagram-platform/instagram-api-with-instagram-login/business-login#canceled-authorization
if params[:error].present?
handle_authorization_error
return
end
process_successful_authorization
rescue StandardError => e
handle_error(e)
end
private
# Process the authorization code and create inbox
def process_successful_authorization
@response = instagram_client.auth_code.get_token(
oauth_code,
redirect_uri: "#{base_url}/#{provider_name}/callback",
grant_type: 'authorization_code'
)
@long_lived_token_response = exchange_for_long_lived_token(@response.token)
inbox, already_exists = find_or_create_inbox
if already_exists
redirect_to app_instagram_inbox_settings_url(account_id: account_id, inbox_id: inbox.id)
else
redirect_to app_instagram_inbox_agents_url(account_id: account_id, inbox_id: inbox.id)
end
end
# Handle all errors that might occur during authorization
# https://developers.facebook.com/docs/instagram-platform/instagram-api-with-instagram-login/business-login#sample-rejected-response
def handle_error(error)
Rails.logger.error("Instagram Channel creation Error: #{error.message}")
ChatwootExceptionTracker.new(error).capture_exception
error_info = extract_error_info(error)
redirect_to_error_page(error_info)
end
# Extract error details from the exception
def extract_error_info(error)
if error.is_a?(OAuth2::Error)
begin
# Instagram returns JSON error response which we parse to extract error details
JSON.parse(error.message)
rescue JSON::ParseError
# Fall back to a generic OAuth error if JSON parsing fails
{ 'error_type' => 'OAuthException', 'code' => 400, 'error_message' => error.message }
end
else
# For other unexpected errors
{ 'error_type' => error.class.name, 'code' => 500, 'error_message' => error.message }
end
end
# Handles the case when a user denies permissions or cancels the authorization flow
# Error parameters are documented at:
# https://developers.facebook.com/docs/instagram-platform/instagram-api-with-instagram-login/business-login#canceled-authorization
def handle_authorization_error
error_info = {
'error_type' => params[:error] || 'authorization_error',
'code' => 400,
'error_message' => params[:error_description] || 'Authorization was denied'
}
Rails.logger.error("Instagram Authorization Error: #{error_info['error_message']}")
redirect_to_error_page(error_info)
end
# Centralized method to redirect to error page with appropriate parameters
# This ensures consistent error handling across different error scenarios
# Frontend will handle the error page based on the error_type
def redirect_to_error_page(error_info)
redirect_to app_new_instagram_inbox_url(
account_id: account_id,
error_type: error_info['error_type'],
code: error_info['code'],
error_message: error_info['error_message']
)
end
def find_or_create_inbox
user_details = fetch_instagram_user_details(@long_lived_token_response['access_token'])
channel_instagram = find_channel_by_instagram_id(user_details['user_id'].to_s)
channel_exists = channel_instagram.present?
if channel_instagram
update_channel(channel_instagram, user_details)
else
channel_instagram = create_channel_with_inbox(user_details)
end
# reauthorize channel, this code path only triggers when instagram auth is successful
# reauthorized will also update cache keys for the associated inbox
channel_instagram.reauthorized!
[channel_instagram.inbox, channel_exists]
end
def find_channel_by_instagram_id(instagram_id)
Channel::Instagram.find_by(instagram_id: instagram_id, account: account)
end
def update_channel(channel_instagram, user_details)
expires_at = Time.current + @long_lived_token_response['expires_in'].seconds
channel_instagram.update!(
access_token: @long_lived_token_response['access_token'],
expires_at: expires_at
)
# Update inbox name if username changed
channel_instagram.inbox.update!(name: user_details['username'])
channel_instagram
end
def create_channel_with_inbox(user_details)
ActiveRecord::Base.transaction do
expires_at = Time.current + @long_lived_token_response['expires_in'].seconds
channel_instagram = Channel::Instagram.create!(
access_token: @long_lived_token_response['access_token'],
instagram_id: user_details['user_id'].to_s,
account: account,
expires_at: expires_at
)
account.inboxes.create!(
account: account,
channel: channel_instagram,
name: user_details['username']
)
channel_instagram
end
end
def account_id
return unless params[:state]
verify_instagram_token(params[:state])
end
def oauth_code
params[:code]
end
def account
@account ||= Account.find(account_id)
end
def provider_name
'instagram'
end
end
@@ -43,8 +43,6 @@ class SuperAdmin::AppConfigsController < SuperAdmin::ApplicationController
['MAILER_INBOUND_EMAIL_DOMAIN']
when 'linear'
%w[LINEAR_CLIENT_ID LINEAR_CLIENT_SECRET]
when 'instagram'
%w[INSTAGRAM_APP_ID INSTAGRAM_APP_SECRET INSTAGRAM_VERIFY_TOKEN INSTAGRAM_API_VERSION ENABLE_INSTAGRAM_CHANNEL_HUMAN_AGENT]
else
%w[ENABLE_ACCOUNT_SIGNUP FIREBASE_PROJECT_ID FIREBASE_CREDENTIALS]
end
@@ -15,9 +15,6 @@ class Webhooks::InstagramController < ActionController::API
private
def valid_token?(token)
# Validates against both IG_VERIFY_TOKEN (Instagram channel via Facebook page) and
# INSTAGRAM_VERIFY_TOKEN (Instagram channel via direct Instagram login)
token == GlobalConfigService.load('IG_VERIFY_TOKEN', '') ||
token == GlobalConfigService.load('INSTAGRAM_VERIFY_TOKEN', '')
token == GlobalConfigService.load('IG_VERIFY_TOKEN', '')
end
end
+1 -2
View File
@@ -81,8 +81,7 @@ class AccountDashboard < Administrate::BaseDashboard
COLLECTION_FILTERS = {
active: ->(resources) { resources.where(status: :active) },
suspended: ->(resources) { resources.where(status: :suspended) },
recent: ->(resources) { resources.where('created_at > ?', 30.days.ago) },
marked_for_deletion: ->(resources) { resources.where("custom_attributes->>'marked_for_deletion_at' IS NOT NULL") }
recent: ->(resources) { resources.where('created_at > ?', 30.days.ago) }
}.freeze
# Overwrite this method to customize how accounts are displayed
+2 -12
View File
@@ -86,19 +86,9 @@ class ConversationFinder
@team = current_account.teams.find(params[:team_id]) if params[:team_id]
end
def find_conversation_by_inbox
@conversations = current_account.conversations
@conversations = @conversations.where(inbox_id: @inbox_ids) unless params[:inbox_id].blank? && @is_admin
end
def find_all_conversations
find_conversation_by_inbox
# Apply permission-based filtering
@conversations = Conversations::PermissionFilterService.new(
@conversations,
current_user,
current_account
).perform
@conversations = current_account.conversations
@conversations = @conversations.where(inbox_id: @inbox_ids) unless @is_admin
filter_by_conversation_type if params[:conversation_type]
@conversations
end
-4
View File
@@ -18,8 +18,4 @@ module BillingHelper
def non_web_inboxes(account)
account.inboxes.where.not(channel_type: Channel::WebWidget.to_s).count
end
def agents(account)
account.users.count
end
end
@@ -1,49 +0,0 @@
module Instagram::IntegrationHelper
REQUIRED_SCOPES = %w[instagram_business_basic instagram_business_manage_messages].freeze
# Generates a signed JWT token for Instagram integration
#
# @param account_id [Integer] The account ID to encode in the token
# @return [String, nil] The encoded JWT token or nil if client secret is missing
def generate_instagram_token(account_id)
return if client_secret.blank?
JWT.encode(token_payload(account_id), client_secret, 'HS256')
rescue StandardError => e
Rails.logger.error("Failed to generate Instagram token: #{e.message}")
nil
end
def token_payload(account_id)
{
sub: account_id,
iat: Time.current.to_i
}
end
# Verifies and decodes a Instagram JWT token
#
# @param token [String] The JWT token to verify
# @return [Integer, nil] The account ID from the token or nil if invalid
def verify_instagram_token(token)
return if token.blank? || client_secret.blank?
decode_token(token, client_secret)
end
private
def client_secret
@client_secret ||= GlobalConfigService.load('INSTAGRAM_APP_SECRET', nil)
end
def decode_token(token, secret)
JWT.decode(token, secret, true, {
algorithm: 'HS256',
verify_expiration: true
}).first['sub']
rescue StandardError => e
Rails.logger.error("Unexpected error verifying Instagram token: #{e.message}")
nil
end
end
+3
View File
@@ -4,6 +4,7 @@ import AddAccountModal from '../dashboard/components/layout/sidebarComponents/Ad
import LoadingState from './components/widgets/LoadingState.vue';
import NetworkNotification from './components/NetworkNotification.vue';
import UpdateBanner from './components/app/UpdateBanner.vue';
import UpgradeBanner from './components/app/UpgradeBanner.vue';
import PaymentPendingBanner from './components/app/PaymentPendingBanner.vue';
import PendingEmailVerificationBanner from './components/app/PendingEmailVerificationBanner.vue';
import vueActionCable from './helper/actionCable';
@@ -30,6 +31,7 @@ export default {
UpdateBanner,
PaymentPendingBanner,
WootSnackbarBox,
UpgradeBanner,
PendingEmailVerificationBanner,
},
setup() {
@@ -144,6 +146,7 @@ export default {
<template v-if="currentAccountId">
<PendingEmailVerificationBanner v-if="hideOnOnboardingView" />
<PaymentPendingBanner v-if="hideOnOnboardingView" />
<UpgradeBanner />
</template>
<router-view v-slot="{ Component }">
<transition name="fade" mode="out-in">
@@ -1,14 +0,0 @@
/* global axios */
import ApiClient from '../ApiClient';
class InstagramChannel extends ApiClient {
constructor() {
super('instagram', { accountScoped: true });
}
generateAuthorization(payload) {
return axios.post(`${this.url}/authorization`, payload);
}
}
export default new InstagramChannel();
@@ -17,12 +17,6 @@ class EnterpriseAccountAPI extends ApiClient {
getLimits() {
return axios.get(`${this.url}limits`);
}
toggleDeletion(action) {
return axios.post(`${this.url}toggle_deletion`, {
action_type: action,
});
}
}
export default new EnterpriseAccountAPI();
@@ -10,7 +10,6 @@ describe('#enterpriseAccountAPI', () => {
expect(accountAPI).toHaveProperty('update');
expect(accountAPI).toHaveProperty('delete');
expect(accountAPI).toHaveProperty('checkout');
expect(accountAPI).toHaveProperty('toggleDeletion');
});
describe('API calls', () => {
@@ -43,21 +42,5 @@ describe('#enterpriseAccountAPI', () => {
'/enterprise/api/v1/subscription'
);
});
it('#toggleDeletion with delete action', () => {
accountAPI.toggleDeletion('delete');
expect(axiosMock.post).toHaveBeenCalledWith(
'/enterprise/api/v1/toggle_deletion',
{ action_type: 'delete' }
);
});
it('#toggleDeletion with undelete action', () => {
accountAPI.toggleDeletion('undelete');
expect(axiosMock.post).toHaveBeenCalledWith(
'/enterprise/api/v1/toggle_deletion',
{ action_type: 'undelete' }
);
});
});
});
@@ -81,6 +81,28 @@
margin-left: var(--space-small);
}
}
// Conversation sidebar close button
.close-button--rtl {
transform: rotate(180deg);
}
// Resolve actions button
.resolve-actions {
.button-group .button:first-child {
border-bottom-left-radius: 0;
border-bottom-right-radius: var(--border-radius-normal);
border-top-left-radius: 0;
border-top-right-radius: var(--border-radius-normal);
}
.button-group .button:last-child {
border-bottom-left-radius: var(--border-radius-normal);
border-bottom-right-radius: 0;
border-top-left-radius: var(--border-radius-normal);
border-top-right-radius: 0;
}
}
}
// Conversation list
@@ -155,6 +177,71 @@
}
}
// Help center
.article-container .row--article-block {
td:last-child {
direction: initial;
}
}
.portal-popover__container .portal {
.actions-container {
margin-left: unset;
margin-right: var(--space-one);
}
}
.edit-article--container {
.header-right--wrap {
.button-group .button:first-child {
border-bottom-left-radius: 0;
border-bottom-right-radius: var(--border-radius-normal);
border-top-left-radius: 0;
border-top-right-radius: var(--border-radius-normal);
}
.button-group .button:last-child {
border-bottom-left-radius: var(--border-radius-normal);
border-bottom-right-radius: 0;
border-top-left-radius: var(--border-radius-normal);
border-top-right-radius: 0;
}
}
.header-left--wrap {
.back-button {
direction: initial;
}
}
.article--buttons {
.dropdown-pane {
left: 0;
position: absolute;
right: unset;
}
}
.sidebar-button {
transform: rotate(180deg);
}
}
.article-settings--container {
border-left: 0;
border-right: 1px solid var(--color-border-light);
flex-direction: row-reverse;
margin-left: 0;
margin-right: var(--space-normal);
padding-left: 0;
padding-right: var(--space-normal);
}
.category-list--container .header-left--wrap {
direction: initial;
justify-content: flex-end;
}
// Toggle switch
.toggle-button {
&.small {
@@ -177,6 +264,11 @@
}
}
// Widget builder
.widget-builder-container .widget-preview {
direction: initial;
}
// Modal
.modal-container {
text-align: right;
@@ -190,6 +282,7 @@
}
// Other changes
.colorpicker--chrome {
direction: initial;
}
@@ -198,6 +291,14 @@
direction: initial;
}
.contact--details .contact--bio {
direction: ltr;
}
.merge-contacts .child-contact-wrap {
direction: ltr;
}
.contact--form .input-group {
direction: initial;
}
@@ -29,6 +29,7 @@
@import 'rtl';
@import 'widgets/base';
@import 'widgets/buttons';
@import 'widgets/conversation-view';
@import 'widgets/tabs';
@import 'widgets/woot-tables';
@@ -40,12 +40,6 @@ dl:not(.reset-base) {
@apply mb-0;
}
// Button base
button {
font-family: inherit;
@apply inline-block text-center align-middle cursor-pointer text-sm m-0 py-1 px-2.5 transition-all duration-200 ease-in-out border-0 border-none rounded-lg disabled:opacity-50;
}
// Form elements
// -------------------------
label {
@@ -0,0 +1,228 @@
// scss-lint:disable SpaceAfterPropertyColon
// scss-lint:disable MergeableSelector
button {
font-family: inherit;
transition:
background-color 0.25s ease-out,
color 0.25s ease-out;
@apply inline-block items-center mb-0 text-center align-middle cursor-pointer text-sm mt-0 mx-0 py-1 px-2.5 border border-solid border-transparent dark:border-transparent rounded-[0.3125rem];
&:disabled,
&.disabled {
@apply opacity-40 cursor-not-allowed;
}
}
.button-group {
@apply mb-0 flex flex-nowrap items-stretch;
.button {
flex: 0 0 auto;
@apply m-0 text-sm rounded-none first:rounded-tl-[0.3125rem] first:rounded-bl-[0.3125rem] last:rounded-tr-[0.3125rem] last:rounded-br-[0.3125rem] rtl:space-x-reverse;
}
.button--only-icon {
@apply w-10 justify-center pl-0 pr-0;
}
}
.back-button {
@apply m-0;
}
.button {
@apply items-center bg-n-brand px-2.5 text-white dark:text-white inline-flex h-10 mb-0 gap-2 font-medium;
.button__content {
@apply w-full whitespace-nowrap overflow-hidden text-ellipsis;
img,
svg {
@apply inline-block;
}
}
&:hover:not(:disabled):not(.success):not(.alert):not(.warning):not(
.clear
):not(.smooth):not(.hollow) {
@apply bg-n-brand/80 dark:bg-n-brand/80;
}
&:disabled,
&.disabled {
@apply opacity-40 cursor-not-allowed;
}
&.success {
@apply bg-n-teal-9 text-white dark:text-white;
}
&.secondary {
@apply bg-n-solid-3 text-white dark:text-white;
}
&.primary {
@apply bg-n-brand text-white dark:text-white;
}
&.clear {
@apply text-n-blue-text dark:text-n-blue-text bg-transparent dark:bg-transparent;
}
&.alert {
@apply bg-n-ruby-9 text-white dark:text-white;
&.clear {
@apply bg-transparent dark:bg-transparent;
}
}
&.warning {
@apply bg-n-amber-9 text-white dark:text-white;
&.clear {
@apply bg-transparent dark:bg-transparent;
}
}
&.tiny {
@apply h-6 text-[10px];
}
&.small {
@apply h-8 text-xs;
}
.spinner {
@apply px-2 py-0;
}
// @TODDO - Remove after moving all buttons to woot-button
.icon + .button__content {
@apply w-auto;
}
&.expanded {
@apply flex justify-center text-center;
}
&.round {
@apply rounded-full;
}
// @TODO Use with link
&.compact {
@apply pb-0 pt-0;
}
&.hollow {
@apply border border-n-brand/40 bg-transparent text-n-blue-text hover:enabled:bg-n-brand/20;
&.secondary {
@apply text-n-slate-12 border-n-slate-5 hover:enabled:bg-n-slate-5;
}
&.success {
@apply text-n-teal-9 border-n-teal-8 hover:enabled:bg-n-teal-5;
}
&.alert {
@apply text-n-ruby-9 border-n-ruby-8 hover:enabled:bg-n-ruby-5;
}
&.warning {
@apply text-n-amber-9 border-n-amber-8 hover:enabled:bg-n-amber-5;
}
}
// Smooth style
&.smooth {
@apply bg-n-brand/10 dark:bg-n-brand/30 text-n-blue-text hover:enabled:bg-n-brand/20 dark:hover:enabled:bg-n-brand/40;
&.secondary {
@apply bg-n-slate-4 text-n-slate-11 hover:enabled:text-n-slate-11 hover:enabled:bg-n-slate-5;
}
&.success {
@apply bg-n-teal-4 text-n-teal-11 hover:enabled:text-n-teal-11 hover:enabled:bg-n-teal-5;
}
&.alert {
@apply bg-n-ruby-4 text-n-ruby-11 hover:enabled:text-n-ruby-11 hover:enabled:bg-n-ruby-5;
}
&.warning {
@apply bg-n-amber-4 text-n-amber-11 hover:enabled:text-n-amber-11 hover:enabled:bg-n-amber-5;
}
}
&.clear {
@apply text-n-blue-text hover:enabled:bg-n-brand/10 dark:hover:enabled:bg-n-brand/30;
&.secondary {
@apply text-n-slate-12 hover:enabled:bg-n-slate-4;
}
&.success {
@apply text-n-teal-10 hover:enabled:bg-n-teal-4;
}
&.alert {
@apply text-n-ruby-11 hover:enabled:bg-n-ruby-4;
}
&.warning {
@apply text-n-amber-11 hover:enabled:bg-n-amber-4;
}
&:active {
&.secondary {
@apply active:bg-n-slate-3 dark:active:bg-n-slate-7;
}
}
&:focus {
&.secondary {
@apply focus:bg-n-slate-4 dark:focus:bg-n-slate-6;
}
}
}
// Sizes
&.tiny {
@apply h-6;
}
&.small {
@apply h-8 pb-1 pt-1;
}
&.large {
@apply h-12;
}
&.button--only-icon {
@apply justify-center pl-0 pr-0 w-10;
&.tiny {
@apply w-6;
}
&.small {
@apply w-8;
}
&.large {
@apply w-12;
}
}
&.link {
@apply h-auto m-0 p-0;
&:hover {
@apply underline;
}
}
}
@@ -1,5 +1,3 @@
<!-- DEPRECIATED -->
<!-- TODO: Replace this banner component with NextBanner "app/javascript/dashboard/components-next/banner/Banner.vue" -->
<script setup>
import { computed } from 'vue';
@@ -103,7 +103,7 @@ export default {
{{ $t('FILTER.CUSTOM_VIEWS.ADD.TITLE') }}
</h3>
<form class="w-full grid gap-6" @submit.prevent="saveCustomViews">
<label :class="{ error: v$.name.$error }">
<div>
<input
v-model="name"
class="py-1.5 px-3 text-n-slate-12 bg-n-alpha-1 text-sm rounded-lg reset-base w-full"
@@ -116,14 +116,14 @@ export default {
>
{{ $t('FILTER.CUSTOM_VIEWS.ADD.ERROR_MESSAGE') }}
</span>
</label>
</div>
<div class="flex flex-row justify-end w-full gap-2">
<NextButton sm solid blue :disabled="isButtonDisabled">
{{ $t('FILTER.CUSTOM_VIEWS.ADD.SAVE_BUTTON') }}
</NextButton>
<NextButton faded slate sm @click.prevent="onClose">
{{ $t('FILTER.CUSTOM_VIEWS.ADD.CANCEL_BUTTON') }}
</NextButton>
<NextButton solid blue sm :disabled="isButtonDisabled">
{{ $t('FILTER.CUSTOM_VIEWS.ADD.SAVE_BUTTON') }}
</NextButton>
</div>
</form>
</div>
@@ -26,12 +26,6 @@ const showAccountSwitcher = computed(
() => userAccounts.value.length > 1 && currentAccount.value.name
);
const sortedCurrentUserAccounts = computed(() => {
return [...(currentUser.value.accounts || [])].sort((a, b) =>
a.name.localeCompare(b.name)
);
});
const onChangeAccount = newId => {
const accountUrl = `/app/accounts/${newId}/dashboard`;
window.location.href = accountUrl;
@@ -76,7 +70,7 @@ const emitNewAccount = () => {
<DropdownBody v-if="showAccountSwitcher" class="min-w-80 z-50">
<DropdownSection :title="t('SIDEBAR_ITEMS.SWITCH_ACCOUNT')">
<DropdownItem
v-for="account in sortedCurrentUserAccounts"
v-for="account in currentUser.accounts"
:id="`account-${account.id}`"
:key="account.id"
class="cursor-pointer"
@@ -35,7 +35,7 @@ const onToggle = () => {
<template>
<div class="text-sm">
<button
class="flex items-center select-none w-full rounded-lg bg-n-slate-2 outline outline-1 outline-n-weak m-0 cursor-grab justify-between py-2 px-4 drag-handle"
class="flex items-center select-none w-full rounded-lg bg-n-slate-2 border border-n-weak m-0 cursor-grab justify-between py-2 px-4 drag-handle"
:class="{ 'rounded-bl-none rounded-br-none': isOpen }"
@click.stop="onToggle"
>
@@ -55,7 +55,7 @@ const onToggle = () => {
</button>
<div
v-if="isOpen"
class="bg-n-background outline outline-1 outline-n-weak -mt-[-1px] border-t-0 rounded-br-lg rounded-bl-lg"
class="bg-n-background border border-n-weak dark:border-n-slate-2 border-t-0 rounded-br-lg rounded-bl-lg"
:class="compact ? 'p-0' : 'px-2 py-4'"
>
<slot />
@@ -61,7 +61,7 @@ import {
getUserPermissions,
filterItemsByPermission,
} from 'dashboard/helper/permissionsHelper.js';
import { matchesFilters } from '../store/modules/conversations/helpers/filterHelpers';
// import { matchesFilters } from '../store/modules/conversations/helpers/filterHelpers';
import { CONVERSATION_EVENTS } from '../helper/AnalyticsHelper/events';
import { ASSIGNEE_TYPE_TAB_PERMISSIONS } from 'dashboard/constants/permissions.js';
@@ -106,7 +106,7 @@ const advancedFilterTypes = ref(
);
const currentUser = useMapGetter('getCurrentUser');
const chatLists = useMapGetter('getFilteredConversations');
const chatLists = useMapGetter('getAllConversations');
const mineChatsList = useMapGetter('getMineChats');
const allChatList = useMapGetter('getAllStatusChats');
const unAssignedChatsList = useMapGetter('getUnAssignedChats');
@@ -326,12 +326,12 @@ const conversationList = computed(() => {
localConversationList = [...chatLists.value];
}
if (activeFolder.value) {
const { payload } = activeFolder.value.query;
localConversationList = localConversationList.filter(conversation => {
return matchesFilters(conversation, payload);
});
}
// if (activeFolder.value) {
// const { payload } = activeFolder.value.query;
// localConversationList = localConversationList.filter(conversation => {
// return matchesFilters(conversation, payload);
// });
// }
return localConversationList;
});
@@ -0,0 +1,64 @@
<script>
import 'highlight.js/styles/default.css';
import { copyTextToClipboard } from 'shared/helpers/clipboard';
import { useAlert } from 'dashboard/composables';
export default {
props: {
value: {
type: String,
default: '',
},
},
data() {
return {
masked: true,
};
},
methods: {
async onCopy(e) {
e.preventDefault();
await copyTextToClipboard(this.value);
useAlert(this.$t('COMPONENTS.CODE.COPY_SUCCESSFUL'));
},
toggleMasked() {
this.masked = !this.masked;
},
},
};
</script>
<template>
<div class="text--container">
<woot-button size="small" class="button--text" @click="onCopy">
{{ $t('COMPONENTS.CODE.BUTTON_TEXT') }}
</woot-button>
<woot-button
variant="clear"
size="small"
class="button--visibility"
color-scheme="secondary"
:icon="masked ? 'eye-show' : 'eye-hide'"
@click.prevent="toggleMasked"
/>
<highlightjs v-if="value" :code="masked ? '•'.repeat(10) : value" />
</div>
</template>
<style lang="scss" scoped>
.text--container {
position: relative;
text-align: left;
.button--text,
.button--visibility {
margin-top: 0;
position: absolute;
right: 0;
}
.button--visibility {
right: 60px;
}
}
</style>
@@ -3,16 +3,12 @@ import { FEATURE_FLAGS } from 'dashboard/featureFlags';
import { BUS_EVENTS } from 'shared/constants/busEvents';
import { mapGetters } from 'vuex';
import { emitter } from 'shared/helpers/mitt';
import NextButton from 'dashboard/components-next/button/Button.vue';
export default {
components: {
NextButton,
},
props: {
size: {
type: String,
default: 'sm',
default: 'small',
},
},
computed: {
@@ -37,13 +33,13 @@ export default {
<!-- eslint-disable-next-line vue/no-root-v-if -->
<template>
<NextButton
<woot-button
v-if="!hasNextSidebar"
ghost
slate
:size="size"
icon="i-lucide-menu"
class="-ml-3"
variant="clear"
color-scheme="secondary"
class="-ml-3 text-black-900 dark:text-slate-300"
icon="list"
@click="onMenuItemClick"
/>
</template>
@@ -35,7 +35,7 @@ export default {
color-scheme="alert"
:banner-message="bannerMessage"
:action-button-label="actionButtonMessage"
action-button-icon="i-lucide-mail"
action-button-icon="mail"
has-action-button
@primary-action="resendVerificationEmail"
/>
@@ -0,0 +1,52 @@
<script>
import Spinner from 'shared/components/Spinner.vue';
export default {
components: {
Spinner,
},
props: {
isLoading: {
type: Boolean,
default: false,
},
icon: {
type: String,
default: '',
},
buttonIconClass: {
type: String,
default: '',
},
type: {
type: String,
default: 'button',
},
variant: {
type: String,
default: 'primary',
},
},
created() {
if (import.meta.env.DEV) {
// eslint-disable-next-line
console.warn(
'[DEPRECATED] This component has been deprecated and will be removed soon. Please use v3/components/Form/Button.vue instead'
);
}
},
};
</script>
<template>
<button :type="type" class="button nice" :class="variant">
<fluent-icon
v-if="!isLoading && icon"
class="icon"
:class="buttonIconClass"
:icon="icon"
/>
<Spinner v-if="isLoading" />
<slot />
</button>
</template>
@@ -0,0 +1,66 @@
<script>
import Spinner from 'shared/components/Spinner.vue';
export default {
components: {
Spinner,
},
props: {
disabled: {
type: Boolean,
default: false,
},
loading: {
type: Boolean,
default: false,
},
buttonText: {
type: String,
default: '',
},
buttonClass: {
type: String,
default: '',
},
iconClass: {
type: String,
default: '',
},
spinnerClass: {
type: String,
default: '',
},
type: {
type: String,
default: 'submit',
},
},
computed: {
computedClass() {
return `button nice gap-2 ${this.buttonClass || ' '}`;
},
},
};
</script>
<template>
<button
:type="type"
data-testid="submit_button"
:disabled="disabled"
:class="computedClass"
>
<fluent-icon v-if="!!iconClass" :icon="iconClass" class="icon" />
<span>{{ buttonText }}</span>
<Spinner v-if="loading" class="ml-2" :color-scheme="spinnerClass" />
</button>
</template>
<style lang="scss" scoped>
button:disabled {
@apply bg-woot-100 dark:bg-woot-500/25 dark:text-slate-500 opacity-100;
&:hover {
@apply bg-woot-100 dark:bg-woot-500/25;
}
}
</style>
@@ -134,7 +134,7 @@ useEmitter(CMD_RESOLVE_CONVERSATION, onCmdResolveConversation);
<template>
<div class="relative flex items-center justify-end resolve-actions">
<div
class="rounded-lg shadow outline-1 outline"
class="rounded-lg shadow button-group outline-1 outline"
:class="!showOpenButton ? 'outline-n-container' : 'outline-transparent'"
>
<Button
@@ -1,6 +1,7 @@
// [NOTE][DEPRECATED] This method is to be deprecated, please do not add new components to this file.
/* eslint no-plusplus: 0 */
import AvatarUploader from './widgets/forms/AvatarUploader.vue';
import Button from './ui/WootButton.vue';
import Code from './Code.vue';
import ColorPicker from './widgets/ColorPicker.vue';
import ConfirmDeleteModal from './widgets/modal/ConfirmDeleteModal.vue';
@@ -17,6 +18,7 @@ import ModalHeader from './ModalHeader.vue';
import Modal from './Modal.vue';
import SidemenuIcon from './SidemenuIcon.vue';
import Spinner from 'shared/components/Spinner.vue';
import SubmitButton from './buttons/FormSubmitButton.vue';
import Tabs from './ui/Tabs/Tabs.vue';
import TabsItem from './ui/Tabs/TabsItem.vue';
import Thumbnail from './widgets/Thumbnail.vue';
@@ -24,6 +26,7 @@ import DatePicker from './ui/DatePicker/DatePicker.vue';
const WootUIKit = {
AvatarUploader,
Button,
Code,
ColorPicker,
ConfirmDeleteModal,
@@ -40,6 +43,7 @@ const WootUIKit = {
ModalHeader,
SidemenuIcon,
Spinner,
SubmitButton,
Tabs,
TabsItem,
Thumbnail,
@@ -7,7 +7,6 @@ import WootDropdownHeader from 'shared/components/ui/dropdown/DropdownHeader.vue
import WootDropdownDivider from 'shared/components/ui/dropdown/DropdownDivider.vue';
import AvailabilityStatusBadge from '../widgets/conversation/AvailabilityStatusBadge.vue';
import wootConstants from 'dashboard/constants/globals';
import NextButton from 'dashboard/components-next/button/Button.vue';
const { AVAILABILITY_STATUS_KEYS } = wootConstants;
@@ -18,7 +17,6 @@ export default {
WootDropdownMenu,
WootDropdownItem,
AvailabilityStatusBadge,
NextButton,
},
data() {
return {
@@ -103,21 +101,19 @@ export default {
:key="status.value"
class="flex items-baseline"
>
<NextButton
sm
:color="status.disabled ? 'blue' : 'slate'"
:variant="status.disabled ? 'faded' : 'ghost'"
class="status-change--dropdown-button !w-full !justify-start"
<woot-button
size="small"
:color-scheme="status.disabled ? '' : 'secondary'"
:variant="status.disabled ? 'smooth' : 'clear'"
class="status-change--dropdown-button"
@click="changeAvailabilityStatus(status.value)"
>
<AvailabilityStatusBadge :status="status.value" />
<span class="min-w-0 truncate font-medium text-xs">
{{ status.label }}
</span>
</NextButton>
{{ status.label }}
</woot-button>
</WootDropdownItem>
<WootDropdownDivider />
<WootDropdownItem class="flex items-center justify-between px-3 py-2 m-0">
<WootDropdownItem class="flex items-center justify-between p-2 m-0">
<div class="flex items-center">
<fluent-icon
v-tooltip.right-start="$t('SIDEBAR.SET_AUTO_OFFLINE.INFO_TEXT')"
@@ -127,7 +123,7 @@ export default {
/>
<span
class="mx-2 my-0 text-xs font-medium text-slate-600 dark:text-slate-100"
class="mx-1 my-0 text-xs font-medium text-slate-600 dark:text-slate-100"
>
{{ $t('SIDEBAR.SET_AUTO_OFFLINE.TEXT') }}
</span>
@@ -1,11 +1,7 @@
<script>
import { mapGetters } from 'vuex';
import NextButton from 'dashboard/components-next/button/Button.vue';
export default {
components: {
NextButton,
},
emits: ['toggleAccounts'],
data() {
return { showSwitchButton: false };
@@ -50,13 +46,14 @@ export default {
class="absolute top-0 right-0 flex items-center justify-end w-full h-full rounded-md ltr:overlay-shadow ltr:dark:overlay-shadow-dark rtl:rtl-overlay-shadow rtl:dark:rtl-overlay-shadow-dark"
>
<div class="mx-2 my-0">
<NextButton
ghost
xs
icon="i-lucide-arrow-right-left"
:label="$t('SIDEBAR.SWITCH')"
<woot-button
variant="clear"
size="tiny"
icon="arrow-swap"
@click="$emit('toggleAccounts')"
/>
>
{{ $t('SIDEBAR.SWITCH') }}
</woot-button>
</div>
</div>
</transition>
@@ -3,12 +3,8 @@ import { required, minLength } from '@vuelidate/validators';
import { mapGetters } from 'vuex';
import { useVuelidate } from '@vuelidate/core';
import { useAlert } from 'dashboard/composables';
import NextButton from 'dashboard/components-next/button/Button.vue';
export default {
components: {
NextButton,
},
props: {
show: {
type: Boolean,
@@ -90,24 +86,19 @@ export default {
/>
</label>
</div>
<div class="w-full flex justify-end gap-2 items-center">
<NextButton
faded
slate
type="reset"
:label="$t('CREATE_ACCOUNT.FORM.CANCEL')"
@click.prevent="() => $emit('closeAccountCreateModal')"
/>
<NextButton
type="submit"
:label="$t('CREATE_ACCOUNT.FORM.SUBMIT')"
:is-loading="uiFlags.isCreating"
:disabled="
v$.accountName.$invalid ||
v$.accountName.$invalid ||
uiFlags.isCreating
"
/>
<div class="w-full">
<div class="w-full">
<woot-submit-button
:disabled="
v$.accountName.$invalid ||
v$.accountName.$invalid ||
uiFlags.isCreating
"
:button-text="$t('CREATE_ACCOUNT.FORM.SUBMIT')"
:loading="uiFlags.isCreating"
button-class="large expanded"
/>
</div>
</div>
</form>
</div>
@@ -1,12 +1,10 @@
<script>
import { mapGetters } from 'vuex';
import Thumbnail from '../../widgets/Thumbnail.vue';
import NextButton from 'dashboard/components-next/button/Button.vue';
export default {
components: {
Thumbnail,
NextButton,
},
emits: ['toggleMenu'],
computed: {
@@ -27,10 +25,10 @@ export default {
</script>
<template>
<NextButton
<woot-button
v-tooltip.right="$t(`SIDEBAR.PROFILE_SETTINGS`)"
link
class="rounded-full"
variant="link"
class="flex items-center rounded-full"
@click="handleClick"
>
<Thumbnail
@@ -39,7 +37,6 @@ export default {
:status="statusOfAgent"
should-show-status-always
size="32px"
class="flex-shrink-0"
/>
</NextButton>
</woot-button>
</template>
@@ -5,14 +5,12 @@ import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
import AvailabilityStatus from 'dashboard/components/layout/AvailabilityStatus.vue';
import { FEATURE_FLAGS } from '../../../featureFlags';
import NextButton from 'dashboard/components-next/button/Button.vue';
export default {
components: {
WootDropdownMenu,
WootDropdownItem,
AvailabilityStatus,
NextButton,
},
props: {
show: {
@@ -84,46 +82,37 @@ export default {
<AvailabilityStatus />
<WootDropdownMenu>
<WootDropdownItem v-if="showChangeAccountOption">
<NextButton
ghost
sm
slate
icon="i-lucide-arrow-right-left"
class="!w-full !justify-start"
<woot-button
variant="clear"
color-scheme="secondary"
size="small"
icon="arrow-swap"
@click="$emit('toggleAccounts')"
>
<span class="min-w-0 truncate font-medium text-xs">
{{ $t('SIDEBAR_ITEMS.CHANGE_ACCOUNTS') }}
</span>
</NextButton>
{{ $t('SIDEBAR_ITEMS.CHANGE_ACCOUNTS') }}
</woot-button>
</WootDropdownItem>
<WootDropdownItem v-if="showChatSupport">
<NextButton
ghost
sm
slate
icon="i-lucide-message-circle-question"
class="!w-full !justify-start"
<woot-button
variant="clear"
color-scheme="secondary"
size="small"
icon="chat-help"
@click="$emit('showSupportChatWindow')"
>
<span class="min-w-0 truncate font-medium text-xs">
{{ $t('SIDEBAR_ITEMS.CONTACT_SUPPORT') }}
</span>
</NextButton>
{{ $t('SIDEBAR_ITEMS.CONTACT_SUPPORT') }}
</woot-button>
</WootDropdownItem>
<WootDropdownItem>
<NextButton
ghost
sm
slate
icon="i-lucide-keyboard"
class="!w-full !justify-start"
<woot-button
variant="clear"
color-scheme="secondary"
size="small"
icon="keyboard"
@click="handleKeyboardHelpClick"
>
<span class="min-w-0 truncate font-medium text-xs">
{{ $t('SIDEBAR_ITEMS.KEYBOARD_SHORTCUTS') }}
</span>
</NextButton>
{{ $t('SIDEBAR_ITEMS.KEYBOARD_SHORTCUTS') }}
</woot-button>
</WootDropdownItem>
<WootDropdownItem>
<router-link
@@ -133,70 +122,56 @@ export default {
>
<a
:href="href"
class="h-8 bg-white button small clear secondary dark:bg-slate-800"
:class="{ 'is-active': isActive }"
@click="e => handleProfileSettingClick(e, navigate)"
>
<NextButton
ghost
sm
slate
icon="i-lucide-circle-user"
class="!w-full !justify-start"
>
<span class="min-w-0 truncate font-medium text-xs">
{{ $t('SIDEBAR_ITEMS.PROFILE_SETTINGS') }}
</span>
</NextButton>
<fluent-icon icon="person" size="14" class="icon icon--font" />
<span class="button__content">
{{ $t('SIDEBAR_ITEMS.PROFILE_SETTINGS') }}
</span>
</a>
</router-link>
</WootDropdownItem>
<WootDropdownItem>
<NextButton
ghost
sm
slate
icon="i-lucide-sun-moon"
class="!w-full !justify-start"
<woot-button
variant="clear"
color-scheme="secondary"
size="small"
icon="appearance"
@click="openAppearanceOptions"
>
<span class="min-w-0 truncate font-medium text-xs">
{{ $t('SIDEBAR_ITEMS.APPEARANCE') }}
</span>
</NextButton>
{{ $t('SIDEBAR_ITEMS.APPEARANCE') }}
</woot-button>
</WootDropdownItem>
<WootDropdownItem v-if="currentUser.type === 'SuperAdmin'">
<a
href="/super_admin"
class="h-8 bg-white button small clear secondary dark:bg-slate-800"
target="_blank"
rel="noopener nofollow noreferrer"
@click="$emit('close')"
>
<NextButton
ghost
sm
slate
icon="i-lucide-layout-dashboard"
class="!w-full !justify-start"
>
<span class="min-w-0 truncate font-medium text-xs">
{{ $t('SIDEBAR_ITEMS.SUPER_ADMIN_CONSOLE') }}
</span>
</NextButton>
<fluent-icon
icon="content-settings"
size="14"
class="icon icon--font"
/>
<span class="button__content">
{{ $t('SIDEBAR_ITEMS.SUPER_ADMIN_CONSOLE') }}
</span>
</a>
</WootDropdownItem>
<WootDropdownItem>
<NextButton
ghost
sm
slate
icon="i-lucide-circle-power"
class="!w-full !justify-start"
<woot-button
variant="clear"
color-scheme="secondary"
size="small"
icon="power"
@click="logout"
>
<span class="min-w-0 truncate font-medium text-xs">
{{ $t('SIDEBAR_ITEMS.LOGOUT') }}
</span>
</NextButton>
{{ $t('SIDEBAR_ITEMS.LOGOUT') }}
</woot-button>
</WootDropdownItem>
</WootDropdownMenu>
</div>
@@ -13,10 +13,9 @@ import {
isOnUnattendedView,
} from '../../../store/modules/conversations/helpers/actionHelpers';
import Policy from '../../policy.vue';
import NextButton from 'dashboard/components-next/button/Button.vue';
export default {
components: { SecondaryChildNavItem, Policy, NextButton },
components: { SecondaryChildNavItem, Policy },
props: {
menuItem: {
type: Object,
@@ -206,7 +205,14 @@ export default {
{{ $t(`SIDEBAR.${menuItem.label}`) }}
</span>
<div v-if="menuItem.showNewButton" class="flex items-center">
<NextButton ghost xs slate icon="i-lucide-plus" @click="onClickOpen" />
<woot-button
size="tiny"
variant="clear"
color-scheme="secondary"
icon="add"
class="p-0 ml-2"
@click="onClickOpen"
/>
</div>
</div>
<router-link
@@ -266,15 +272,16 @@ export default {
>
<li class="pl-1">
<a :href="href">
<NextButton
ghost
xs
slate
icon="i-lucide-plus"
:label="$t(`SIDEBAR.${menuItem.newLinkTag}`)"
<woot-button
size="tiny"
variant="clear"
color-scheme="secondary"
icon="add"
:data-testid="menuItem.dataTestid"
@click="e => newLinkClick(e, navigate)"
/>
>
{{ $t(`SIDEBAR.${menuItem.newLinkTag}`) }}
</woot-button>
</a>
</li>
</router-link>
@@ -41,6 +41,7 @@ describe('AccountSelector', () => {
'fluent-icon': FluentIcon,
},
stubs: {
WootButton: { template: '<button />' },
// override global stub
WootModalHeader: false,
},
@@ -2,7 +2,7 @@ import { shallowMount } from '@vue/test-utils';
import { createStore } from 'vuex';
import AgentDetails from '../AgentDetails.vue';
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
import NextButton from 'dashboard/components-next/button/Button.vue';
import WootButton from 'dashboard/components/ui/WootButton.vue';
describe('AgentDetails', () => {
const currentUser = {
@@ -40,12 +40,12 @@ describe('AgentDetails', () => {
plugins: [store],
components: {
Thumbnail,
NextButton,
WootButton,
},
directives: {
tooltip: mockTooltipDirective, // Mocking the tooltip directive
},
stubs: { NextButton: { template: '<button><slot /></button>' } },
stubs: { WootButton: { template: '<button><slot /></button>' } },
},
});
});
@@ -1,7 +1,7 @@
import { mount } from '@vue/test-utils';
import { createStore } from 'vuex';
import AvailabilityStatus from '../AvailabilityStatus.vue';
import NextButton from 'dashboard/components-next/button/Button.vue';
import WootButton from 'dashboard/components/ui/WootButton.vue';
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
import WootDropdownHeader from 'shared/components/ui/dropdown/DropdownHeader.vue';
@@ -40,7 +40,7 @@ describe('AvailabilityStatus', () => {
global: {
plugins: [store],
components: {
NextButton,
WootButton,
WootDropdownItem,
WootDropdownMenu,
WootDropdownHeader,
@@ -22,7 +22,7 @@ const store = createStore({
describe('SidemenuIcon', () => {
test('matches snapshot', () => {
const wrapper = shallowMount(SidemenuIcon, {
stubs: { NextButton: { template: '<button><slot /></button>' } },
stubs: { WootButton: { template: '<button><slot /></button>' } },
global: { plugins: [store] },
});
expect(wrapper.vm).toBeTruthy();
@@ -2,11 +2,11 @@
exports[`SidemenuIcon > matches snapshot 1`] = `
<button
class="-ml-3"
ghost=""
icon="i-lucide-menu"
size="sm"
slate=""
class="-ml-3 text-black-900 dark:text-slate-300"
color-scheme="secondary"
icon="list"
size="small"
variant="clear"
>
@@ -1,10 +1,5 @@
<script>
import NextButton from 'dashboard/components-next/button/Button.vue';
export default {
components: {
NextButton,
},
props: {
bannerMessage: {
type: String,
@@ -24,7 +19,7 @@ export default {
},
actionButtonVariant: {
type: String,
default: 'faded',
default: '',
},
actionButtonLabel: {
type: String,
@@ -32,7 +27,7 @@ export default {
},
actionButtonIcon: {
type: String,
default: 'i-lucide-arrow-right',
default: 'arrow-right',
},
colorScheme: {
type: String,
@@ -53,18 +48,6 @@ export default {
}
return classList;
},
// TODO - Remove this method when we standardize
// the button color and variant names
getButtonColor() {
const colorMap = {
primary: 'blue',
secondary: 'blue',
alert: 'ruby',
warning: 'amber',
};
return colorMap[this.colorScheme] || 'blue';
},
},
methods: {
onClick(e) {
@@ -94,23 +77,27 @@ export default {
</a>
</span>
<div class="actions">
<NextButton
<woot-button
v-if="hasActionButton"
xs
size="tiny"
:icon="actionButtonIcon"
:variant="actionButtonVariant"
:color="getButtonColor"
:label="actionButtonLabel"
color-scheme="primary"
class-names="banner-action__button"
@click="onClick"
/>
<NextButton
>
{{ actionButtonLabel }}
</woot-button>
<woot-button
v-if="hasCloseButton"
xs
icon="i-lucide-circle-x"
:color="getButtonColor"
:label="$t('GENERAL_SETTINGS.DISMISS')"
size="tiny"
:color-scheme="colorScheme"
icon="dismiss-circle"
class-names="banner-action__button"
@click="onClickClose"
/>
>
{{ $t('GENERAL_SETTINGS.DISMISS') }}
</woot-button>
</div>
</div>
</template>
@@ -119,6 +106,13 @@ export default {
.banner {
&.primary {
@apply bg-woot-500 dark:bg-woot-500;
.banner-action__button {
@apply bg-woot-600 dark:bg-woot-600 border-none text-white;
&:hover {
@apply bg-woot-700 dark:bg-woot-700;
}
}
}
&.secondary {
@@ -130,6 +124,13 @@ export default {
&.alert {
@apply bg-n-ruby-3 text-n-ruby-12;
.banner-action__button {
@apply border-none text-n-ruby-12 bg-n-ruby-5;
&:hover {
@apply bg-n-ruby-4;
}
}
a {
@apply text-n-ruby-12;
@@ -145,12 +146,21 @@ export default {
&.gray {
@apply text-black-500 dark:text-black-500;
.banner-action__button {
@apply text-white dark:text-white;
}
}
a {
@apply ml-1 underline text-white dark:text-white text-xs;
}
.banner-action__button {
::v-deep .button__content {
@apply whitespace-nowrap;
}
}
.banner-message {
@apply flex items-center;
}
@@ -1,16 +1,14 @@
<script setup>
import Button from 'dashboard/components-next/button/Button.vue';
defineProps({
buttonText: {
type: String,
default: '',
},
trailingIcon: {
type: Boolean,
default: false,
rightIcon: {
type: String,
default: '',
},
icon: {
leftIcon: {
type: String,
default: '',
},
@@ -18,15 +16,32 @@ defineProps({
</script>
<template>
<Button
ghost
slate
sm
class="relative"
:icon="icon"
:trailing-icon="trailingIcon"
<button
class="inline-flex relative items-center p-1.5 w-fit h-8 gap-1.5 rounded-lg hover:bg-slate-50 dark:hover:bg-slate-800 active:bg-slate-75 dark:active:bg-slate-800"
>
<span class="min-w-0 truncate">{{ buttonText }}</span>
<slot name="leftIcon">
<fluent-icon
v-if="leftIcon"
:icon="leftIcon"
size="18"
class="flex-shrink-0 text-slate-900 dark:text-slate-50"
/>
</slot>
<span
v-if="buttonText"
class="text-sm font-medium truncate text-slate-900 dark:text-slate-50"
>
{{ buttonText }}
</span>
<slot name="rightIcon">
<fluent-icon
v-if="rightIcon"
:icon="rightIcon"
size="18"
class="flex-shrink-0 text-slate-900 dark:text-slate-50"
/>
</slot>
<slot name="dropdown" />
</Button>
</button>
</template>
@@ -8,7 +8,9 @@ defineProps({
</script>
<template>
<div class="flex items-center justify-center h-10 text-sm text-n-slate-11">
<div
class="flex items-center justify-center h-10 text-sm text-slate-500 dark:text-slate-300"
>
{{ message }}
</div>
</template>
@@ -78,7 +78,7 @@ const shouldShowEmptyState = computed(() => {
<template>
<div
class="absolute z-20 w-40 bg-n-solid-2 border-0 outline outline-1 outline-n-weak shadow rounded-xl max-h-[400px]"
class="absolute z-20 w-40 bg-white border shadow dark:bg-slate-800 rounded-xl border-slate-50 dark:border-slate-700/50 max-h-[400px]"
@click.stop
>
<slot name="search">
@@ -21,7 +21,7 @@ defineProps({
<template>
<button
class="relative inline-flex items-center justify-start w-full p-3 border-0 rounded-none first:rounded-t-xl last:rounded-b-xl h-11 hover:enabled:bg-n-alpha-2"
class="relative inline-flex items-center justify-start w-full p-3 border-0 rounded-none first:rounded-t-xl last:rounded-b-xl h-11 hover:bg-slate-50 dark:hover:bg-slate-700 active:bg-slate-75 dark:active:bg-slate-800"
>
<div class="inline-flex items-center gap-3 overflow-hidden">
<fluent-icon
@@ -30,14 +30,16 @@ defineProps({
size="18"
:style="{ color: iconColor }"
/>
<span class="text-sm font-medium truncate text-n-slate-12">
<span
class="text-sm font-medium truncate text-slate-900 dark:text-slate-50"
>
{{ buttonText }}
</span>
<fluent-icon
v-if="isActive"
icon="checkmark"
size="18"
class="flex-shrink-0 text-n-slate-12"
class="flex-shrink-0 text-slate-900 dark:text-slate-50"
/>
</div>
<slot name="dropdown" />
@@ -8,7 +8,9 @@ defineProps({
</script>
<template>
<div class="flex items-center justify-center h-10 text-sm text-n-slate-11">
<div
class="flex items-center justify-center h-10 text-sm text-slate-500 dark:text-slate-300"
>
{{ message }}
</div>
</template>
@@ -1,7 +1,5 @@
<script setup>
import { defineEmits, defineModel } from 'vue';
import NextButton from 'dashboard/components-next/button/Button.vue';
defineProps({
inputPlaceholder: {
type: String,
@@ -23,29 +21,31 @@ const value = defineModel({
<template>
<div
class="flex items-center justify-between h-10 min-h-[40px] sticky top-0 bg-n-solid-2 dark:bg-n-solid-2 z-10 gap-2 px-3 border-b rounded-t-xl border-n-weak"
class="flex items-center justify-between h-10 min-h-[40px] sticky top-0 bg-white z-10 dark:bg-slate-800 gap-2 px-3 border-b rounded-t-xl border-slate-50 dark:border-slate-700"
>
<div class="flex items-center w-full gap-2" @keyup.space.prevent>
<fluent-icon
icon="search"
size="16"
class="text-n-slate-11 flex-shrink-0"
class="text-slate-400 dark:text-slate-400 flex-shrink-0"
/>
<input
v-model="value"
:placeholder="inputPlaceholder"
type="text"
class="w-full mb-0 text-sm !outline-0 bg-transparent text-n-slate-12 placeholder:text-n-slate-10 reset-base"
class="w-full mb-0 text-sm bg-white dark:bg-slate-800 text-slate-800 dark:text-slate-75 reset-base"
/>
</div>
<!-- Clear filter button -->
<NextButton
<woot-button
v-if="!modelValue && showClearFilter"
faded
xs
class="flex-shrink-0"
:label="$t('REPORT.FILTER_ACTIONS.CLEAR_FILTER')"
size="small"
variant="clear"
color-scheme="primary"
class="!px-1 !py-1.5"
@click="emit('remove')"
/>
>
{{ $t('REPORT.FILTER_ACTIONS.CLEAR_FILTER') }}
</woot-button>
</div>
</template>
@@ -0,0 +1,129 @@
<script>
import Spinner from 'shared/components/Spinner.vue';
import EmojiOrIcon from 'shared/components/EmojiOrIcon.vue';
export default {
name: 'WootButton',
components: { EmojiOrIcon, Spinner },
props: {
type: {
type: String,
default: 'submit',
},
variant: {
type: String,
default: '',
},
size: {
type: String,
default: '',
},
icon: {
type: String,
default: '',
},
emoji: {
type: String,
default: '',
},
colorScheme: {
type: String,
default: 'primary',
},
classNames: {
type: [String, Object],
default: '',
},
isDisabled: {
type: Boolean,
default: false,
},
isLoading: {
type: Boolean,
default: false,
},
isExpanded: {
type: Boolean,
default: false,
},
},
computed: {
variantClasses() {
if (this.variant.includes('link')) {
return `clear ${this.variant}`;
}
return this.variant;
},
hasOnlyIcon() {
const hasEmojiOrIcon = this.emoji || this.icon;
return !this.$slots.default && hasEmojiOrIcon;
},
hasOnlyIconClasses() {
return this.hasOnlyIcon ? 'button--only-icon' : '';
},
buttonClasses() {
return [
this.variantClasses,
this.hasOnlyIconClasses,
this.size,
this.colorScheme,
this.classNames,
this.isDisabled ? 'disabled' : '',
this.isExpanded ? 'expanded' : '',
];
},
iconSize() {
switch (this.size) {
case 'tiny':
return 12;
case 'small':
return 14;
case 'medium':
return 16;
case 'large':
return 18;
default:
return 16;
}
},
showDarkSpinner() {
return (
this.colorScheme === 'secondary' ||
this.variant === 'clear' ||
this.variant === 'link' ||
this.variant === 'hollow'
);
},
},
};
</script>
<template>
<button
class="button"
:type="type"
:class="buttonClasses"
:disabled="isDisabled || isLoading"
>
<Spinner
v-if="isLoading"
size="small"
:color-scheme="showDarkSpinner ? 'dark' : ''"
/>
<EmojiOrIcon
v-else-if="icon || emoji"
class="icon"
:emoji="emoji"
:icon="icon"
:icon-size="iconSize"
/>
<span
v-if="$slots.default"
class="button__content"
:class="{ 'text-left rtl:text-right': size !== 'expanded' }"
>
<slot />
</span>
</button>
</template>
@@ -1,7 +1,7 @@
<script setup>
import { computed } from 'vue';
import NextButton from 'dashboard/components-next/button/Button.vue';
// Props
const props = defineProps({
currentPage: {
type: Number,
@@ -21,6 +21,13 @@ const hasFirstPage = computed(() => props.currentPage === 1);
const hasNextPage = computed(() => props.currentPage === props.totalPages);
const hasPrevPage = computed(() => props.currentPage === 1);
function buttonClass(hasPage) {
if (hasPage) {
return 'hover:!bg-slate-50 dark:hover:!bg-slate-800';
}
return 'dark:hover:!bg-slate-700/50';
}
function onPageChange(newPage) {
emit('pageChange', newPage);
}
@@ -48,61 +55,84 @@ const onLastPage = () => {
</script>
<template>
<div
class="flex items-center h-8 outline outline-1 outline-n-weak rounded-lg"
>
<NextButton
faded
sm
slate
icon="i-lucide-chevrons-left"
class="ltr:rounded-l-lg ltr:rounded-r-none rtl:rounded-r-lg rtl:rounded-l-none"
:disabled="hasFirstPage"
<div class="flex items-center h-8 rounded-lg bg-slate-50 dark:bg-slate-800">
<woot-button
size="small"
variant="smooth"
color-scheme="secondary"
:is-disabled="hasFirstPage"
class-names="dark:!bg-slate-800 !opacity-100 ltr:rounded-l-lg ltr:rounded-r-none rtl:rounded-r-lg rtl:rounded-l-none"
:class="buttonClass(hasFirstPage)"
@click="onFirstPage"
/>
<div class="flex items-center justify-center bg-n-slate-9/10 h-full">
<div class="w-px h-4 rounded-sm bg-n-strong" />
</div>
<NextButton
faded
sm
slate
icon="i-lucide-chevron-left"
class="rounded-none"
:disabled="hasPrevPage"
@click="onPrevPage"
/>
<div
class="flex items-center gap-3 px-3 tabular-nums bg-n-slate-9/10 h-full"
>
<span class="text-sm text-n-slate-12">
<fluent-icon
icon="chevrons-left"
size="20"
icon-lib="lucide"
:class="hasFirstPage && 'opacity-40'"
/>
</woot-button>
<div class="w-px h-4 rounded-sm bg-slate-75 dark:bg-slate-700/50" />
<woot-button
size="small"
variant="smooth"
color-scheme="secondary"
:is-disabled="hasPrevPage"
class-names="dark:!bg-slate-800 !opacity-100 rounded-none"
:class="buttonClass(hasPrevPage)"
@click="onPrevPage"
>
<fluent-icon
icon="chevron-left-single"
size="20"
icon-lib="lucide"
:class="hasPrevPage && 'opacity-40'"
/>
</woot-button>
<div
class="flex items-center gap-3 px-3 tabular-nums bg-slate-50 dark:bg-slate-800 text-slate-700 dark:text-slate-100"
>
<span class="text-sm text-slate-800 dark:text-slate-75">
{{ currentPage }}
</span>
<span class="text-n-slate-11">/</span>
<span class="text-sm text-n-slate-11">
<span class="text-slate-600 dark:text-slate-500">/</span>
<span class="text-sm text-slate-600 dark:text-slate-500">
{{ totalPages }}
</span>
</div>
<NextButton
faded
sm
slate
icon="i-lucide-chevron-right"
class="rounded-none"
:disabled="hasNextPage"
<woot-button
size="small"
variant="smooth"
color-scheme="secondary"
:is-disabled="hasNextPage"
class-names="dark:!bg-slate-800 !opacity-100 rounded-none"
:class="buttonClass(hasNextPage)"
@click="onNextPage"
/>
<div class="flex items-center justify-center bg-n-slate-9/10 h-full">
<div class="w-px h-4 rounded-sm bg-n-strong" />
</div>
<NextButton
faded
sm
slate
icon="i-lucide-chevrons-right"
class="ltr:rounded-r-lg ltr:rounded-l-none rtl:rounded-l-lg rtl:rounded-r-none"
:disabled="hasLastPage"
>
<fluent-icon
icon="chevron-right-single"
size="20"
icon-lib="lucide"
:class="hasNextPage && 'opacity-40'"
/>
</woot-button>
<div class="w-px h-4 rounded-sm bg-slate-75 dark:bg-slate-700/50" />
<woot-button
size="small"
variant="smooth"
color-scheme="secondary"
class-names="dark:!bg-slate-800 !opacity-100 ltr:rounded-r-lg ltr:rounded-l-none rtl:rounded-l-lg rtl:rounded-r-none"
:class="buttonClass(hasLastPage)"
:is-disabled="hasLastPage"
@click="onLastPage"
/>
>
<fluent-icon
icon="chevrons-right"
size="20"
icon-lib="lucide"
:class="hasLastPage && 'opacity-40'"
/>
</woot-button>
</div>
</template>
@@ -106,3 +106,43 @@ export default {
/>
</div>
</template>
<style lang="scss" scoped>
.button-group {
@apply flex border-0 p-0 m-0;
.button {
@apply text-sm font-medium py-2.5 px-4 m-0 relative z-10;
&.is-active {
@apply bg-white dark:bg-slate-900;
}
}
.button--reply {
@apply border-r rounded-none border-b-0 border-l-0 border-t-0 border-slate-50 dark:border-slate-700;
&:hover,
&:focus {
@apply border-r border-slate-50 dark:border-slate-700;
}
}
.button--note {
@apply border-l-0 rounded-none;
&.is-active {
@apply border-r border-b-0 bg-yellow-100 dark:bg-yellow-800 border-t-0 border-slate-50 dark:border-slate-700;
}
&:hover,
&:active {
@apply text-yellow-700 dark:text-yellow-700;
}
}
}
.button--note {
@apply text-yellow-600 dark:text-yellow-600 bg-transparent dark:bg-transparent;
}
</style>
@@ -2,12 +2,8 @@
import { useVuelidate } from '@vuelidate/core';
import { required, minLength, email } from '@vuelidate/validators';
import { useAlert } from 'dashboard/composables';
import NextButton from 'dashboard/components-next/button/Button.vue';
export default {
components: {
NextButton,
},
props: {
show: {
type: Boolean,
@@ -157,18 +153,13 @@ export default {
</div>
</div>
<div class="flex flex-row justify-end w-full gap-2 px-0 py-2">
<NextButton
faded
slate
type="reset"
:label="$t('EMAIL_TRANSCRIPT.CANCEL')"
@click.prevent="onCancel"
/>
<NextButton
type="submit"
:label="$t('EMAIL_TRANSCRIPT.SUBMIT')"
<woot-submit-button
:button-text="$t('EMAIL_TRANSCRIPT.SUBMIT')"
:disabled="!isFormValid"
/>
<button class="button clear" @click.prevent="onCancel">
{{ $t('EMAIL_TRANSCRIPT.CANCEL') }}
</button>
</div>
</form>
</div>
@@ -420,7 +420,6 @@ export default {
if (conversationId !== oldConversationId) {
this.setToDraft(oldConversationId, this.replyType);
this.getFromDraft();
this.resetRecorderAndClearAttachments();
}
},
message(updatedMessage) {
@@ -530,12 +529,6 @@ export default {
);
}
},
resetRecorderAndClearAttachments() {
// Reset audio recorder UI state
this.resetAudioRecorderInput();
// Reset attached files
this.attachedFiles = [];
},
saveDraft(conversationId, replyType) {
if (this.message || this.message === '') {
const key = `draft-${conversationId}-${replyType}`;
@@ -1074,7 +1067,7 @@ export default {
<template>
<Banner
v-if="showSelfAssignBanner"
action-button-variant="ghost"
action-button-variant="clear"
color-scheme="secondary"
class="banner--self-assign mx-2 mb-2 rounded-lg"
:banner-message="$t('CONVERSATION.NOT_ASSIGNED_TO_YOU')"
@@ -4,12 +4,8 @@ import {
DuplicateContactException,
ExceptionWithMessage,
} from 'shared/helpers/CustomErrors';
import NextButton from 'dashboard/components-next/button/Button.vue';
export default {
components: {
NextButton,
},
props: {
name: {
type: String,
@@ -95,12 +91,9 @@ export default {
</p>
</div>
<div v-if="formattedPhoneNumber" class="link-wrap">
<NextButton
ghost
xs
:label="$t('CONVERSATION.SAVE_CONTACT')"
@click.prevent="addContact"
/>
<woot-button variant="clear" size="small" @click.prevent="addContact">
{{ $t('CONVERSATION.SAVE_CONTACT') }}
</woot-button>
</div>
</div>
</template>
@@ -2,12 +2,8 @@
import DyteAPI from 'dashboard/api/integrations/dyte';
import { buildDyteURL } from 'shared/helpers/IntegrationHelper';
import { useAlert } from 'dashboard/composables';
import NextButton from 'dashboard/components-next/button/Button.vue';
export default {
components: {
NextButton,
},
props: {
messageId: {
type: Number,
@@ -45,25 +41,31 @@ export default {
<template>
<div>
<NextButton
blue
sm
icon="i-lucide-video"
:label="$t('INTEGRATION_SETTINGS.DYTE.CLICK_HERE_TO_JOIN')"
<woot-button
size="small"
variant="smooth"
color-scheme="secondary"
icon="video-add"
class="join-call-button"
:is-loading="isLoading"
@click="joinTheCall"
/>
>
{{ $t('INTEGRATION_SETTINGS.DYTE.CLICK_HERE_TO_JOIN') }}
</woot-button>
<div v-if="dyteAuthToken" class="video-call--container">
<iframe
:src="meetingLink"
allow="camera;microphone;fullscreen;display-capture;picture-in-picture;clipboard-write;"
/>
<NextButton
sm
class="mt-2"
:label="$t('INTEGRATION_SETTINGS.DYTE.LEAVE_THE_ROOM')"
<woot-button
size="small"
variant="smooth"
color-scheme="secondary"
class="join-call-button"
@click="leaveTheRoom"
/>
>
{{ $t('INTEGRATION_SETTINGS.DYTE.LEAVE_THE_ROOM') }}
</woot-button>
</div>
</div>
</template>
@@ -1,6 +1,6 @@
<script>
// components
import NextButton from 'dashboard/components-next/button/Button.vue';
import WootButton from '../../../ui/WootButton.vue';
import Avatar from '../../Avatar.vue';
// composables
@@ -19,7 +19,7 @@ export default {
name: 'LabelSuggestion',
components: {
Avatar,
NextButton,
WootButton,
},
props: {
suggestedLabels: {
@@ -185,44 +185,42 @@ export default {
"
/>
</button>
<NextButton
<WootButton
v-if="preparedLabels.length === 1"
v-tooltip.top="{
content: $t('LABEL_MGMT.SUGGESTIONS.TOOLTIP.DISMISS'),
delay: { show: 600, hide: 0 },
hideOnClick: true,
}"
faded
xs
icon="i-lucide-x"
class="flex-shrink-0"
:color="isHovered ? 'ruby' : 'blue'"
variant="smooth"
:color-scheme="isHovered ? 'alert' : 'primary'"
class="label--add"
icon="dismiss"
size="tiny"
@click="dismissSuggestions"
/>
</div>
<div
v-if="preparedLabels.length > 1"
class="inline-flex items-center gap-1"
>
<NextButton
xs
icon="i-lucide-plus"
class="flex-shrink-0"
:variant="selectedLabels.length === 0 ? 'faded' : 'solid'"
:label="addButtonText"
<div v-if="preparedLabels.length > 1">
<WootButton
:variant="selectedLabels.length === 0 ? 'smooth' : ''"
class="label--add"
icon="add"
size="tiny"
@click="addAllLabels"
/>
<NextButton
>
{{ addButtonText }}
</WootButton>
<WootButton
v-tooltip.top="{
content: $t('LABEL_MGMT.SUGGESTIONS.TOOLTIP.DISMISS'),
delay: { show: 600, hide: 0 },
hideOnClick: true,
}"
faded
xs
icon="i-lucide-x"
class="flex-shrink-0"
:color="isHovered ? 'ruby' : 'blue'"
:color-scheme="isHovered ? 'alert' : 'primary'"
variant="smooth"
class="label--add"
icon="dismiss"
size="tiny"
@click="dismissSuggestions"
/>
</div>
@@ -107,10 +107,9 @@ const linkIssue = async () => {
:class="shouldShowDropdown ? 'h-[256px]' : 'gap-2'"
>
<FilterButton
trailing-icon
icon="i-lucide-chevron-down"
right-icon="chevron-down"
:button-text="linkIssueTitle"
class="justify-between w-full h-[2.5rem] py-1.5 px-3 rounded-xl bg-n-alpha-black2 outline outline-1 outline-n-weak dark:outline-n-weak hover:outline-n-slate-6 dark:hover:outline-n-slate-6"
class="justify-between w-full h-[2.5rem] py-1.5 px-3 rounded-xl border border-slate-50 bg-slate-25 dark:border-slate-600 dark:bg-slate-900 hover:bg-slate-50 dark:hover:bg-slate-900/50"
@click="toggleDropdown"
>
<template v-if="shouldShowDropdown" #dropdown>
@@ -50,10 +50,9 @@ const selectedItemId = computed(() => selectedItem.value?.id || null);
<label class="w-full" :class="{ error: hasError }">
{{ label }}
<FilterButton
trailing-icon
icon="i-lucide-chevron-down"
right-icon="chevron-down"
:button-text="selectedItemName"
class="justify-between w-full h-[2.5rem] py-1.5 px-3 rounded-xl bg-n-alpha-black2 outline outline-1 outline-n-weak dark:outline-n-weak hover:outline-n-slate-6 dark:hover:outline-n-slate-6"
class="justify-between w-full h-[2.5rem] py-1.5 px-3 rounded-xl border border-slate-50 bg-slate-25 dark:border-slate-600 dark:bg-slate-900 hover:bg-slate-50 dark:hover:bg-slate-900/50"
@click="toggleDropdown"
>
<template v-if="shouldShowDropdown" #dropdown>
@@ -1,10 +1,5 @@
<script>
import NextButton from 'dashboard/components-next/button/Button.vue';
export default {
components: {
NextButton,
},
props: {
label: {
type: String,
@@ -54,13 +49,15 @@ export default {
:username="usernameAvatar"
/>
<div v-if="src && deleteAvatar" class="avatar-delete-btn">
<NextButton
outline
xs
ruby
:label="$t('INBOX_MGMT.DELETE.AVATAR_DELETE_BUTTON_TEXT')"
<woot-button
color-scheme="alert"
variant="hollow"
size="tiny"
type="button"
@click="onAvatarDelete"
/>
>
{{ $t('INBOX_MGMT.DELETE.AVATAR_DELETE_BUTTON_TEXT') }}
</woot-button>
</div>
<label>
<input
@@ -0,0 +1,29 @@
class ConversationMetaThrottleManager {
constructor() {
this.lastUpdatedTime = null;
}
shouldThrottle(threshold = 10000) {
if (!this.lastUpdatedTime) {
return false;
}
const currentTime = new Date().getTime();
const lastUpdatedTime = new Date(this.lastUpdatedTime).getTime();
if (currentTime - lastUpdatedTime < threshold) {
return true;
}
return false;
}
markUpdate() {
this.lastUpdatedTime = new Date();
}
reset() {
this.lastUpdatedTime = null;
}
}
export default new ConversationMetaThrottleManager();
-6
View File
@@ -9,7 +9,6 @@ export const INBOX_TYPES = {
TELEGRAM: 'Channel::Telegram',
LINE: 'Channel::Line',
SMS: 'Channel::Sms',
INSTAGRAM: 'Channel::Instagram',
};
const INBOX_ICON_MAP_FILL = {
@@ -21,7 +20,6 @@ const INBOX_ICON_MAP_FILL = {
[INBOX_TYPES.EMAIL]: 'i-ri-mail-fill',
[INBOX_TYPES.TELEGRAM]: 'i-ri-telegram-fill',
[INBOX_TYPES.LINE]: 'i-ri-line-fill',
[INBOX_TYPES.INSTAGRAM]: 'i-ri-instagram-fill',
};
const DEFAULT_ICON_FILL = 'i-ri-chat-1-fill';
@@ -35,7 +33,6 @@ const INBOX_ICON_MAP_LINE = {
[INBOX_TYPES.EMAIL]: 'i-ri-mail-line',
[INBOX_TYPES.TELEGRAM]: 'i-ri-telegram-line',
[INBOX_TYPES.LINE]: 'i-ri-line-line',
[INBOX_TYPES.INSTAGRAM]: 'i-ri-instagram-line',
};
const DEFAULT_ICON_LINE = 'i-ri-chat-1-line';
@@ -121,9 +118,6 @@ export const getInboxClassByType = (type, phoneNumber) => {
case INBOX_TYPES.LINE:
return 'brand-line';
case INBOX_TYPES.INSTAGRAM:
return 'brand-instagram';
default:
return 'chat';
}
@@ -0,0 +1,34 @@
import ConversationMetaThrottleManager from '../ConversationMetaThrottleManager';
describe('ConversationMetaThrottleManager', () => {
beforeEach(() => {
// Reset the lastUpdatedTime before each test
ConversationMetaThrottleManager.lastUpdatedTime = null;
});
describe('shouldThrottle', () => {
it('returns false when lastUpdatedTime is not set', () => {
expect(ConversationMetaThrottleManager.shouldThrottle()).toBe(false);
});
it('returns true when time difference is less than threshold', () => {
ConversationMetaThrottleManager.markUpdate();
expect(ConversationMetaThrottleManager.shouldThrottle()).toBe(true);
});
it('returns false when time difference is more than threshold', () => {
ConversationMetaThrottleManager.lastUpdatedTime = new Date(
Date.now() - 11000
);
expect(ConversationMetaThrottleManager.shouldThrottle()).toBe(false);
});
it('respects custom threshold value', () => {
ConversationMetaThrottleManager.lastUpdatedTime = new Date(
Date.now() - 5000
);
expect(ConversationMetaThrottleManager.shouldThrottle(3000)).toBe(false);
expect(ConversationMetaThrottleManager.shouldThrottle(6000)).toBe(true);
});
});
});
@@ -36,7 +36,7 @@ describe('#resolveActionName', () => {
expect(resolveActionName(MACRO_ACTION_TYPES[1].key)).not.toEqual(
MACRO_ACTION_TYPES[0].label
);
expect(resolveActionName('change_priority')).toEqual('CHANGE_PRIORITY'); // Translated
expect(resolveActionName('change_priority')).toEqual('Change Priority');
});
});
@@ -126,44 +126,6 @@
"ATLEAST_ONE_CONDITION_REQUIRED": "At least one condition is required",
"ATLEAST_ONE_ACTION_REQUIRED": "At least one action is required"
},
"NONE_OPTION": "None",
"EVENTS": {
"CONVERSATION_CREATED": "Conversation Created",
"CONVERSATION_UPDATED": "Conversation Updated",
"MESSAGE_CREATED": "Message Created",
"CONVERSATION_OPENED": "Conversation Opened"
},
"ACTIONS": {
"ASSIGN_AGENT": "Assign to Agent",
"ASSIGN_TEAM": "Assign a Team",
"ADD_LABEL": "Add a Label",
"REMOVE_LABEL": "Remove a Label",
"SEND_EMAIL_TO_TEAM": "Send an Email to Team",
"SEND_EMAIL_TRANSCRIPT": "Send an Email Transcript",
"MUTE_CONVERSATION": "Mute Conversation",
"SNOOZE_CONVERSATION": "Snooze Conversation",
"RESOLVE_CONVERSATION": "Resolve Conversation",
"SEND_WEBHOOK_EVENT": "Send Webhook Event",
"SEND_ATTACHMENT": "Send Attachment",
"SEND_MESSAGE": "Send a Message",
"CHANGE_PRIORITY": "Change Priority",
"ADD_SLA": "Add SLA"
},
"ATTRIBUTES": {
"MESSAGE_TYPE": "Message Type",
"MESSAGE_CONTAINS": "Message Contains",
"EMAIL": "Email",
"INBOX": "Inbox",
"CONVERSATION_LANGUAGE": "Conversation Language",
"PHONE_NUMBER": "Phone Number",
"STATUS": "Status",
"BROWSER_LANGUAGE": "Browser Language",
"MAIL_SUBJECT": "Email Subject",
"COUNTRY_NAME": "Country",
"REFERER_LINK": "Referrer Link",
"ASSIGNEE_NAME": "Assignee",
"TEAM_NAME": "Team",
"PRIORITY": "Priority"
}
"NONE_OPTION": "None"
}
}
@@ -1,11 +1,5 @@
{
"GENERAL_SETTINGS": {
"LIMIT_MESSAGES": {
"CONVERSATION": "You have exceeded the conversation limit. Hacker plan allows only 500 conversations.",
"INBOXES": "You have exceeded the inbox limit. Hacker plan only supports website live-chat. Additional inboxes like email, WhatsApp etc. require a paid plan.",
"AGENTS": "You have exceeded the agent limit. Hacker plan allows only 2 agents.",
"NON_ADMIN": "Please contact your administrator to upgrade the plan and continue using all features."
},
"TITLE": "Account settings",
"SUBMIT": "Update settings",
"BACK": "Back",
@@ -14,26 +8,6 @@
"ERROR": "Could not update settings, try again!",
"SUCCESS": "Successfully updated account settings"
},
"ACCOUNT_DELETE_SECTION": {
"TITLE": "Delete your Account",
"NOTE": "Once you delete your account, all your data will be deleted.",
"BUTTON_TEXT": "Delete Your Account",
"CONFIRM": {
"TITLE": "Delete Account",
"MESSAGE": "Deleting your Account is irreversible. Enter your account name below to confirm you want to permanently delete it.",
"BUTTON_TEXT": "Delete",
"DISMISS": "Cancel",
"PLACE_HOLDER": "Please type {accountName} to confirm"
},
"SUCCESS": "Account marked for deletion",
"FAILURE": "Could not delete account, try again!",
"SCHEDULED_DELETION": {
"TITLE": "Account Scheduled for Deletion",
"MESSAGE_MANUAL": "This account is scheduled for deletion on {deletionDate}. This was requested by an administrator. You can cancel the deletion before this date.",
"MESSAGE_INACTIVITY": "This account is scheduled for deletion on {deletionDate} due to account inactivity. You can cancel the deletion before this date.",
"CLEAR_BUTTON": "Cancel Scheduled Deletion"
}
},
"FORM": {
"ERROR": "Please fix form errors",
"GENERAL_SECTION": {
@@ -77,7 +51,6 @@
"UPDATE_CHATWOOT": "An update {latestChatwootVersion} for Chatwoot is available. Please update your instance.",
"LEARN_MORE": "Learn more",
"PAYMENT_PENDING": "Your payment is pending. Please update your payment information to continue using Chatwoot",
"UPGRADE": "Upgrade to continue using Chatwoot",
"LIMITS_UPGRADE": "Your account has exceeded the usage limits, please upgrade your plan to continue using Chatwoot",
"OPEN_BILLING": "Open billing"
},
@@ -43,14 +43,7 @@
"INBOX_NAME": "Inbox Name",
"ADD_NAME": "Add a name for your inbox",
"PICK_NAME": "Pick a Name for your Inbox",
"PICK_A_VALUE": "Pick a value",
"CREATE_INBOX": "Create Inbox"
},
"INSTAGRAM": {
"CONTINUE_WITH_INSTAGRAM": "Continue with Instagram",
"HELP": "To add your Instagram profile as a channel, you need to authenticate your Instagram Profile by clicking on 'Continue with Instagram' ",
"ERROR_MESSAGE": "There was an error connecting to Instagram, please try again",
"ERROR_AUTH": "There was an error connecting to Instagram, please try again"
"PICK_A_VALUE": "Pick a value"
},
"TWITTER": {
"HELP": "To add your Twitter profile as a channel, you need to authenticate your Twitter Profile by clicking on 'Sign in with Twitter' ",
@@ -760,8 +753,7 @@
"EMAIL": "Email",
"TELEGRAM": "Telegram",
"LINE": "Line",
"API": "API Channel",
"INSTAGRAM": "Instagram"
"API": "API Channel"
}
}
}
@@ -83,22 +83,6 @@
"ACTION_PARAMETERS_REQUIRED": "Action parameters are required",
"ATLEAST_ONE_CONDITION_REQUIRED": "At least one condition is required",
"ATLEAST_ONE_ACTION_REQUIRED": "At least one action is required"
},
"ACTIONS": {
"ASSIGN_TEAM": "Assign a Team",
"ASSIGN_AGENT": "Assign an Agent",
"ADD_LABEL": "Add a Label",
"REMOVE_LABEL": "Remove a Label",
"REMOVE_ASSIGNED_TEAM": "Remove Assigned Team",
"SEND_EMAIL_TRANSCRIPT": "Send an Email Transcript",
"MUTE_CONVERSATION": "Mute Conversation",
"SNOOZE_CONVERSATION": "Snooze Conversation",
"RESOLVE_CONVERSATION": "Resolve Conversation",
"SEND_ATTACHMENT": "Send Attachment",
"SEND_MESSAGE": "Send a Message",
"CHANGE_PRIORITY": "Change Priority",
"ADD_PRIVATE_NOTE": "Add a Private Note",
"SEND_WEBHOOK_EVENT": "Send Webhook Event"
}
}
}
@@ -387,8 +387,7 @@
"LABEL": "Company Name",
"PLACEHOLDER": "Wayne Enterprises"
},
"SUBMIT": "Submit",
"CANCEL": "Cancel"
"SUBMIT": "Submit"
}
},
"KEYBOARD_SHORTCUTS": {
@@ -0,0 +1,123 @@
<script>
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
import SocialIcons from 'dashboard/routes/dashboard/conversation/contact/SocialIcons.vue';
export default {
components: {
Thumbnail,
SocialIcons,
},
props: {
contact: {
type: Object,
default: () => ({}),
},
},
emits: ['edit', 'message'],
computed: {
additionalAttributes() {
return this.contact.additional_attributes || {};
},
socialProfiles() {
const {
social_profiles: socialProfiles,
screen_name: twitterScreenName,
} = this.additionalAttributes;
return { twitter: twitterScreenName, ...(socialProfiles || {}) };
},
company() {
const { company = {} } = this.contact;
return company;
},
},
methods: {
onEditClick() {
this.$emit('edit');
},
onNewMessageClick() {
this.$emit('message');
},
},
};
</script>
<template>
<div class="contact--intro">
<Thumbnail
:src="contact.thumbnail"
size="64px"
:username="contact.name"
:status="contact.availability_status"
/>
<div class="contact--details">
<h2 class="text-lg contact--name">
{{ contact.name }}
</h2>
<h3 class="text-base contact--work">
{{ contact.title }}
<i v-if="company.name" class="i-lucide-circle-minus" />
<span class="company-name">{{ company.name }}</span>
</h3>
<p v-if="additionalAttributes.description" class="contact--bio">
{{ additionalAttributes.description }}
</p>
<SocialIcons :social-profiles="socialProfiles" />
</div>
<div class="contact-actions">
<woot-button
class="new-message"
size="small expanded"
icon="ion-paper-airplane"
@click="onNewMessageClick"
>
{{ $t('CONTACT_PANEL.NEW_MESSAGE') }}
</woot-button>
<woot-button
variant="hollow"
size="small expanded"
icon="edit"
@click="onEditClick"
>
{{ $t('EDIT_CONTACT.BUTTON_LABEL') }}
</woot-button>
</div>
</div>
</template>
<style scoped lang="scss">
.contact--details {
margin-top: var(--space-small);
}
.contact--work {
color: var(--color-body);
.icon {
font-size: var(--font-size-nano);
vertical-align: middle;
}
}
.contact--name {
text-transform: capitalize;
font-weight: var(--font-weight-bold);
}
.contact--bio {
margin: var(--space-smaller) 0 0;
}
.button.new-message {
margin-right: var(--space-small);
}
.contact-actions {
display: flex;
align-items: center;
width: 100%;
margin-top: var(--space-small);
}
</style>
@@ -13,14 +13,12 @@ import {
} from '../../../helper/AnalyticsHelper/events';
import MenuItem from '../../../components/widgets/conversation/contextMenu/menuItem.vue';
import { useTrack } from 'dashboard/composables';
import NextButton from 'dashboard/components-next/button/Button.vue';
export default {
components: {
AddCannedModal,
MenuItem,
ContextMenu,
NextButton,
},
props: {
message: {
@@ -177,12 +175,12 @@ export default {
:confirm-text="$t('CONVERSATION.CONTEXT_MENU.DELETE_CONFIRMATION.DELETE')"
:reject-text="$t('CONVERSATION.CONTEXT_MENU.DELETE_CONFIRMATION.CANCEL')"
/>
<NextButton
<woot-button
v-if="!hideButton"
ghost
slate
sm
icon="i-lucide-ellipsis-vertical"
icon="more-vertical"
color-scheme="secondary"
variant="clear"
size="small"
class="invisible group-hover/context-menu:visible"
@click="handleOpen"
/>
@@ -1,6 +1,6 @@
<script>
import { defineAsyncComponent, ref } from 'vue';
import { mapGetters } from 'vuex';
import { defineAsyncComponent } from 'vue';
import NextSidebar from 'next/sidebar/Sidebar.vue';
import WootKeyShortcutModal from 'dashboard/components/widgets/modal/WootKeyShortcutModal.vue';
@@ -8,7 +8,6 @@ import AddAccountModal from 'dashboard/components/layout/sidebarComponents/AddAc
import AccountSelector from 'dashboard/components/layout/sidebarComponents/AccountSelector.vue';
import AddLabelModal from 'dashboard/routes/dashboard/settings/labels/AddLabel.vue';
import NotificationPanel from 'dashboard/routes/dashboard/notifications/components/NotificationPanel.vue';
import UpgradePage from 'dashboard/routes/dashboard/upgrade/UpgradePage.vue';
import { useUISettings } from 'dashboard/composables/useUISettings';
import { useAccount } from 'dashboard/composables/useAccount';
@@ -36,10 +35,8 @@ export default {
AccountSelector,
AddLabelModal,
NotificationPanel,
UpgradePage,
},
setup() {
const upgradePageRef = ref(null);
const { uiSettings, updateUISettings } = useUISettings();
const { accountId } = useAccount();
@@ -47,7 +44,6 @@ export default {
uiSettings,
updateUISettings,
accountId,
upgradePageRef,
};
},
data() {
@@ -68,16 +64,6 @@ export default {
currentRoute() {
return ' ';
},
showUpgradePage() {
return this.upgradePageRef?.shouldShowUpgradePage;
},
bypassUpgradePage() {
return [
'billing_settings_index',
'settings_inbox_list',
'agent_list',
].includes(this.$route.name);
},
isSidebarOpen() {
const { show_secondary_sidebar: showSecondarySidebar } = this.uiSettings;
return showSecondarySidebar;
@@ -211,25 +197,8 @@ export default {
@show-add-label-popup="showAddLabelPopup"
/>
<main class="flex flex-1 h-full min-h-0 px-0 overflow-hidden">
<UpgradePage
v-show="showUpgradePage"
ref="upgradePageRef"
:bypass-upgrade-page="bypassUpgradePage"
/>
<template v-if="!showUpgradePage">
<router-view />
<CommandBar />
<NotificationPanel
v-if="isNotificationPanel"
@close="closeNotificationPanel"
/>
<woot-modal
v-model:show="showAddLabelModal"
:on-close="hideAddLabelPopup"
>
<AddLabelModal @close="hideAddLabelPopup" />
</woot-modal>
</template>
<router-view />
<CommandBar />
<AccountSelector
:show-account-modal="showAccountModal"
@close-account-modal="toggleAccountModal"
@@ -244,6 +213,16 @@ export default {
@close="closeKeyShortcutModal"
@clickaway="closeKeyShortcutModal"
/>
<NotificationPanel
v-if="isNotificationPanel"
@close="closeNotificationPanel"
/>
<woot-modal
v-model:show="showAddLabelModal"
:on-close="hideAddLabelPopup"
>
<AddLabelModal @close="hideAddLabelPopup" />
</woot-modal>
</main>
</div>
</template>
@@ -1,7 +1,7 @@
<script>
import ConversationCard from 'dashboard/components/widgets/conversation/ConversationCard.vue';
import { mapGetters } from 'vuex';
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
import Spinner from 'shared/components/Spinner.vue';
export default {
components: {
@@ -47,25 +47,28 @@ export default {
</script>
<template>
<div v-if="!uiFlags.isFetching" class="">
<div v-if="!previousConversations.length" class="no-label-message px-4 p-3">
<span>
{{ $t('CONTACT_PANEL.CONVERSATIONS.NO_RECORDS_FOUND') }}
</span>
<div class="contact-conversation--panel">
<div v-if="!uiFlags.isFetching" class="contact-conversation__wrap">
<div
v-if="!previousConversations.length"
class="no-label-message px-4 p-3"
>
<span>
{{ $t('CONTACT_PANEL.CONVERSATIONS.NO_RECORDS_FOUND') }}
</span>
</div>
<div v-else class="contact-conversation--list">
<ConversationCard
v-for="conversation in previousConversations"
:key="conversation.id"
:chat="conversation"
:hide-inbox-name="false"
hide-thumbnail
class="compact"
/>
</div>
</div>
<div v-else class="contact-conversation--list">
<ConversationCard
v-for="conversation in previousConversations"
:key="conversation.id"
:chat="conversation"
:hide-inbox-name="false"
hide-thumbnail
class="compact"
/>
</div>
</div>
<div v-else class="flex items-center justify-center py-5">
<Spinner />
<Spinner v-else />
</div>
</template>
@@ -76,7 +79,6 @@ export default {
::v-deep .conversation {
@apply pr-0;
.conversation--details {
@apply pl-2;
}
@@ -9,14 +9,12 @@ import ConversationLabels from './labels/LabelBox.vue';
import { CONVERSATION_PRIORITY } from '../../../../shared/constants/messages';
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
import { useTrack } from 'dashboard/composables';
import NextButton from 'dashboard/components-next/button/Button.vue';
export default {
components: {
ContactDetailsItem,
MultiselectDropdown,
ConversationLabels,
NextButton,
},
props: {
conversationId: {
@@ -214,15 +212,15 @@ export default {
:title="$t('CONVERSATION_SIDEBAR.ASSIGNEE_LABEL')"
>
<template #button>
<NextButton
<woot-button
v-if="showSelfAssign"
link
xs
icon="i-lucide-arrow-right"
class="!gap-1"
:label="$t('CONVERSATION_SIDEBAR.SELF_ASSIGN')"
icon="arrow-right"
variant="link"
size="small"
@click="onSelfAssign"
/>
>
{{ $t('CONVERSATION_SIDEBAR.SELF_ASSIGN') }}
</woot-button>
</template>
</ContactDetailsItem>
<MultiselectDropdown
@@ -6,14 +6,12 @@ import { useAgentsList } from 'dashboard/composables/useAgentsList';
import ThumbnailGroup from 'dashboard/components/widgets/ThumbnailGroup.vue';
import MultiselectDropdownItems from 'shared/components/ui/MultiselectDropdownItems.vue';
import NextButton from 'dashboard/components-next/button/Button.vue';
export default {
components: {
Spinner,
ThumbnailGroup,
MultiselectDropdownItems,
NextButton,
},
props: {
conversationId: {
@@ -167,14 +165,13 @@ export default {
{{ $t('CONVERSATION_PARTICIPANTS.NO_PARTICIPANTS_TEXT') }}
</p>
</div>
<NextButton
<woot-button
v-tooltip.left="$t('CONVERSATION_PARTICIPANTS.ADD_PARTICIPANTS')"
slate
ghost
sm
icon="i-lucide-settings"
class="relative -top-1"
:title="$t('CONVERSATION_PARTICIPANTS.ADD_PARTICIPANTS')"
icon="settings"
size="tiny"
variant="smooth"
color-scheme="secondary"
@click="onOpenDropdown"
/>
</div>
@@ -191,15 +188,15 @@ export default {
>
{{ $t('CONVERSATION_PARTICIPANTS.YOU_ARE_WATCHING') }}
</p>
<NextButton
<woot-button
v-else
link
xs
icon="i-lucide-arrow-right"
class="!gap-1"
:label="$t('CONVERSATION_PARTICIPANTS.WATCH_CONVERSATION')"
icon="arrow-right"
variant="link"
size="small"
@click="onSelfAssign"
/>
>
{{ $t('CONVERSATION_PARTICIPANTS.WATCH_CONVERSATION') }}
</woot-button>
</div>
<div
v-on-clickaway="
@@ -216,7 +213,13 @@ export default {
>
{{ $t('CONVERSATION_PARTICIPANTS.ADD_PARTICIPANTS') }}
</h4>
<NextButton ghost slate xs icon="i-lucide-x" @click="onCloseDropdown" />
<woot-button
icon="dismiss"
size="tiny"
color-scheme="secondary"
variant="clear"
@click="onCloseDropdown"
/>
</div>
<MultiselectDropdownItems
:options="agentsList"
@@ -7,7 +7,6 @@ import { useUISettings } from 'dashboard/composables/useUISettings';
import Draggable from 'vuedraggable';
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
import MacroItem from './MacroItem.vue';
import NextButton from 'dashboard/components-next/button/Button.vue';
defineProps({
conversationId: {
@@ -72,13 +71,9 @@ onMounted(() => {
{{ $t('MACROS.LIST.404') }}
</p>
<router-link :to="accountScopedUrl('settings/macros')">
<NextButton
faded
xs
icon="i-lucide-plus"
class="mt-1"
:label="$t('MACROS.HEADER_BTN_TXT')"
/>
<woot-button variant="smooth" icon="add" size="tiny" class="mt-1">
{{ $t('MACROS.HEADER_BTN_TXT') }}
</woot-button>
</router-link>
</div>
<div
@@ -53,11 +53,9 @@ const closeMacroPreview = () => {
<template>
<div
class="relative flex items-center justify-between leading-4 rounded-md h-10 pl-3 pr-2"
class="relative flex items-center justify-between leading-4 rounded-md button secondary clear"
>
<span
class="overflow-hidden whitespace-nowrap text-ellipsis font-medium text-n-slate-12"
>
<span class="overflow-hidden whitespace-nowrap text-ellipsis">
{{ macro.name }}
</span>
<div class="flex items-center gap-1 justify-end">
@@ -65,7 +65,7 @@ const resolvedMacro = computed(() => {
class="absolute -left-[0.21875rem] top-[0.2734375rem] w-2 h-2 rounded-full bg-n-solid-1 border-2 border-solid border-n-weak dark:border-slate-600"
/>
<p class="mb-1 text-xs text-n-slate-11">
{{ $t(`MACROS.ACTIONS.${action.actionName}`) }}
{{ action.actionName }}
</p>
<p class="text-n-slate-12 text-sm">{{ action.actionValue }}</p>
</div>
@@ -9,12 +9,8 @@ import { useVuelidate } from '@vuelidate/core';
import countries from 'shared/constants/countries.js';
import { isPhoneNumberValid } from 'shared/helpers/Validators';
import parsePhoneNumber from 'libphonenumber-js';
import NextButton from 'dashboard/components-next/button/Button.vue';
export default {
components: {
NextButton,
},
props: {
contact: {
type: Object,
@@ -405,19 +401,16 @@ export default {
/>
</div>
</div>
<div class="flex flex-row justify-start w-full gap-2 px-0 py-2">
<NextButton
type="submit"
:label="$t('CONTACT_FORM.FORM.SUBMIT')"
:is-loading="inProgress"
/>
<NextButton
faded
slate
type="reset"
:label="$t('CONTACT_FORM.FORM.CANCEL')"
@click.prevent="onCancel"
/>
<div class="flex flex-row justify-end w-full gap-2 px-0 py-2">
<div class="w-full">
<woot-submit-button
:loading="inProgress"
:button-text="$t('CONTACT_FORM.FORM.SUBMIT')"
/>
<button class="button clear" @click.prevent="onCancel">
{{ $t('CONTACT_FORM.FORM.CANCEL') }}
</button>
</div>
</div>
</form>
</template>
@@ -2,12 +2,10 @@
import { useAlert } from 'dashboard/composables';
import EmojiOrIcon from 'shared/components/EmojiOrIcon.vue';
import { copyTextToClipboard } from 'shared/helpers/clipboard';
import NextButton from 'dashboard/components-next/button/Button.vue';
export default {
components: {
EmojiOrIcon,
NextButton,
},
props: {
href: {
@@ -64,13 +62,15 @@ export default {
<span v-else class="text-sm text-n-slate-11">
{{ $t('CONTACT_PANEL.NOT_AVAILABLE') }}
</span>
<NextButton
<woot-button
v-if="showCopy"
ghost
xs
slate
class="ltr:-ml-1 rtl:-mr-1"
icon="i-lucide-clipboard"
type="submit"
variant="clear"
size="tiny"
color-scheme="secondary"
icon="clipboard"
class-names="p-0"
@click="onCopy"
/>
</a>
@@ -9,7 +9,6 @@ import { useI18n } from 'vue-i18n';
import { useUISettings } from 'dashboard/composables/useUISettings';
import { copyTextToClipboard } from 'shared/helpers/clipboard';
import CustomAttribute from 'dashboard/components/CustomAttribute.vue';
import NextButton from 'dashboard/components-next/button/Button.vue';
const props = defineProps({
attributeType: {
@@ -319,16 +318,17 @@ const evenClass = [
{{ emptyStateMessage }}
</p>
<!-- Show more and show less buttons show it if the combinedElements length is greater than 5 -->
<div v-if="combinedElements.length > 5" class="flex items-center px-2 py-2">
<NextButton
ghost
xs
:icon="
showAllAttributes ? 'i-lucide-chevron-up' : 'i-lucide-chevron-down'
"
:label="toggleButtonText"
<div v-if="combinedElements.length > 5" class="flex px-2 py-2">
<woot-button
size="small"
:icon="showAllAttributes ? 'chevron-up' : 'chevron-down'"
variant="clear"
color-scheme="primary"
class="!px-2 hover:!bg-transparent dark:hover:!bg-transparent"
@click="onClickToggle"
/>
>
{{ toggleButtonText }}
</woot-button>
</div>
</div>
</template>
@@ -2,7 +2,6 @@
import { mapGetters } from 'vuex';
import SwitchLayout from './SwitchLayout.vue';
import { frontendURL } from 'dashboard/helper/URLHelper';
export default {
components: {
SwitchLayout,
@@ -38,7 +37,7 @@ export default {
class="flex px-4 pb-1 justify-between items-center flex-row gap-1 pt-2.5 border-b border-transparent"
>
<woot-sidemenu-icon
size="xs"
size="tiny"
class="relative top-0 ltr:-ml-1.5 rtl:-mr-1.5 flex-shrink-0 focus:!bg-n-solid-3 dark:!hover:bg-n-solid-2 hover:!bg-n-alpha-2"
/>
<router-link
@@ -0,0 +1,114 @@
<script>
import { useVuelidate } from '@vuelidate/core';
import { required, minLength } from '@vuelidate/validators';
import { useAlert } from 'dashboard/composables';
import { CONTACTS_EVENTS } from '../../../helper/AnalyticsHelper/events';
import { useTrack } from 'dashboard/composables';
export default {
props: {
filterType: {
type: Number,
default: 0,
},
customViewsQuery: {
type: Object,
default: () => {},
},
openLastSavedItem: {
type: Function,
default: () => {},
},
},
emits: ['close'],
setup() {
return { v$: useVuelidate() };
},
data() {
return {
show: true,
name: '',
};
},
computed: {
isButtonDisabled() {
return this.v$.name.$invalid;
},
},
validations: {
name: {
required,
minLength: minLength(1),
},
},
methods: {
onClose() {
this.$emit('close');
},
async saveCustomViews() {
this.v$.$touch();
if (this.v$.$invalid) {
return;
}
try {
await this.$store.dispatch('customViews/create', {
name: this.name,
filter_type: this.filterType,
query: this.customViewsQuery,
});
this.alertMessage =
this.filterType === 0
? this.$t('FILTER.CUSTOM_VIEWS.ADD.API_FOLDERS.SUCCESS_MESSAGE')
: this.$t('FILTER.CUSTOM_VIEWS.ADD.API_SEGMENTS.SUCCESS_MESSAGE');
this.onClose();
useTrack(CONTACTS_EVENTS.SAVE_FILTER, {
type: this.filterType === 0 ? 'folder' : 'segment',
});
} catch (error) {
const errorMessage = error?.message;
this.alertMessage =
errorMessage || this.filterType === 0
? errorMessage
: this.$t('FILTER.CUSTOM_VIEWS.ADD.API_SEGMENTS.ERROR_MESSAGE');
} finally {
useAlert(this.alertMessage);
}
this.openLastSavedItem();
},
},
};
</script>
<template>
<woot-modal v-model:show="show" :on-close="onClose">
<woot-modal-header :header-title="$t('FILTER.CUSTOM_VIEWS.ADD.TITLE')" />
<form class="w-full" @submit.prevent="saveCustomViews">
<div class="w-full">
<woot-input
v-model="name"
:label="$t('FILTER.CUSTOM_VIEWS.ADD.LABEL')"
type="text"
:error="
v$.name.$error ? $t('FILTER.CUSTOM_VIEWS.ADD.ERROR_MESSAGE') : ''
"
:class="{ error: v$.name.$error }"
:placeholder="$t('FILTER.CUSTOM_VIEWS.ADD.PLACEHOLDER')"
@blur="v$.name.$touch"
/>
<div class="flex flex-row justify-end w-full gap-2 px-0 py-2">
<woot-button :disabled="isButtonDisabled">
{{ $t('FILTER.CUSTOM_VIEWS.ADD.SAVE_BUTTON') }}
</woot-button>
<woot-button variant="clear" @click.prevent="onClose">
{{ $t('FILTER.CUSTOM_VIEWS.ADD.CANCEL_BUTTON') }}
</woot-button>
</div>
</div>
</form>
</woot-modal>
</template>
@@ -1,12 +1,7 @@
<script>
import { mapGetters } from 'vuex';
import wootConstants from 'dashboard/constants/globals';
import NextButton from 'dashboard/components-next/button/Button.vue';
export default {
components: {
NextButton,
},
data() {
return {
helpCenterDocsURL: wootConstants.HELP_CENTER_DOCS_URL,
@@ -91,15 +86,21 @@ export default {
v-if="isOnChatwootCloud"
class="flex flex-row gap-3 justify-start items-center sm:justify-center"
>
<NextButton
outline
:label="$t('HELP_CENTER.UPGRADE_PAGE.BUTTON.LEARN_MORE')"
<woot-button
size="medium"
variant="hollow"
color-scheme="primary"
@click="openHelpCenterDocs"
/>
<NextButton
:label="$t('HELP_CENTER.UPGRADE_PAGE.BUTTON.UPGRADE')"
>
{{ $t('HELP_CENTER.UPGRADE_PAGE.BUTTON.LEARN_MORE') }}
</woot-button>
<woot-button
size="medium"
color-scheme="primary"
@click="openBillingPage"
/>
>
{{ $t('HELP_CENTER.UPGRADE_PAGE.BUTTON.UPGRADE') }}
</woot-button>
</div>
</div>
<div
@@ -3,12 +3,10 @@ import { mapGetters } from 'vuex';
import NotificationPanelList from './NotificationPanelList.vue';
import { useTrack } from 'dashboard/composables';
import { ACCOUNT_EVENTS } from '../../../../helper/AnalyticsHelper/events';
import NextButton from 'dashboard/components-next/button/Button.vue';
export default {
components: {
NotificationPanelList,
NextButton,
},
emits: ['close'],
data() {
@@ -150,27 +148,28 @@ export default {
</span>
</div>
<div class="flex gap-2">
<NextButton
<woot-button
v-if="!noUnreadNotificationAvailable"
faded
xs
icon="i-lucide-list-check"
color-scheme="primary"
variant="smooth"
size="tiny"
:is-loading="uiFlags.isUpdating"
:label="$t('NOTIFICATIONS_PAGE.MARK_ALL_DONE')"
@click="onMarkAllDoneClick"
/>
<NextButton
faded
xs
slate
icon="i-lucide-settings"
>
{{ $t('NOTIFICATIONS_PAGE.MARK_ALL_DONE') }}
</woot-button>
<woot-button
color-scheme="secondary"
variant="smooth"
size="tiny"
icon="settings"
@click="openAudioNotificationSettings"
/>
<NextButton
ghost
xs
slate
icon="i-lucide-x"
<woot-button
color-scheme="secondary"
variant="link"
size="tiny"
icon="dismiss"
@click="closeNotificationPanel"
/>
</div>
@@ -187,18 +186,25 @@ export default {
class="flex items-center justify-between px-5 py-1"
>
<div class="flex">
<NextButton
ghost
slate
icon="i-lucide-chevrons-left"
:disabled="inFirstPage"
<woot-button
size="medium"
variant="clear"
color-scheme="secondary"
:is-disabled="inFirstPage"
@click="onClickFirstPage"
/>
<NextButton
ghost
slate
icon="i-lucide-chevron-left"
class="ltr:-ml-3 rtl:-mr-3"
>
<fluent-icon icon="chevron-left" size="16" />
<fluent-icon
icon="chevron-left"
size="16"
class="rtl:-mr-3 ltr:-ml-3"
/>
</woot-button>
<woot-button
color-scheme="secondary"
variant="clear"
size="medium"
icon="chevron-left"
:disabled="inFirstPage"
@click="onClickPreviousPage"
/>
@@ -207,21 +213,28 @@ export default {
{{ currentPage }} - {{ lastPage }}
</span>
<div class="flex">
<NextButton
ghost
slate
icon="i-lucide-chevron-right"
class="ltr:-mr-3 rtl:-ml-3"
<woot-button
color-scheme="secondary"
variant="clear"
size="medium"
icon="chevron-right"
:disabled="inLastPage"
@click="onClickNextPage"
/>
<NextButton
ghost
slate
icon="i-lucide-chevrons-right"
<woot-button
size="medium"
variant="clear"
color-scheme="secondary"
:disabled="inLastPage"
@click="onClickLastPage"
/>
>
<fluent-icon icon="chevron-right" size="16" />
<fluent-icon
icon="chevron-right"
size="16"
class="rtl:-mr-3 ltr:-ml-3"
/>
</woot-button>
</div>
</div>
<div v-else />
@@ -1,12 +1,10 @@
<script>
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
import { dynamicTime } from 'shared/helpers/timeHelper';
import NextButton from 'dashboard/components-next/button/Button.vue';
export default {
components: {
Thumbnail,
NextButton,
},
props: {
notificationItem: {
@@ -41,10 +39,11 @@ export default {
<template>
<div class="w-full">
<NextButton
link
slate
class="!w-full !h-auto"
<woot-button
size="expanded"
color-scheme="secondary"
variant="link"
class="w-full"
@click="onClickOpenNotification()"
>
<div
@@ -101,6 +100,6 @@ export default {
</span>
</div>
</div>
</NextButton>
</woot-button>
</div>
</template>
@@ -2,14 +2,12 @@
import Spinner from 'shared/components/Spinner.vue';
import EmptyState from 'dashboard/components/widgets/EmptyState.vue';
import NotificationPanelItem from './NotificationPanelItem.vue';
import NextButton from 'dashboard/components-next/button/Button.vue';
export default {
components: {
NotificationPanelItem,
Spinner,
EmptyState,
NextButton,
},
props: {
notifications: {
@@ -61,13 +59,16 @@ export default {
v-if="showEmptyResult"
:title="$t('NOTIFICATIONS_PAGE.UNREAD_NOTIFICATION.EMPTY_MESSAGE')"
/>
<NextButton
<woot-button
v-if="!isLoading && inLastPage"
ghost
class="!w-full mt-3"
:label="$t('NOTIFICATIONS_PAGE.UNREAD_NOTIFICATION.ALL_NOTIFICATIONS')"
size="expanded"
variant="clear"
color-scheme="primary"
class-names="mt-3"
@click="openNotificationPage"
/>
>
{{ $t('NOTIFICATIONS_PAGE.UNREAD_NOTIFICATION.ALL_NOTIFICATIONS') }}
</woot-button>
<div
v-if="isLoading"
class="flex items-center justify-center mx-2 my-12 text-sm font-medium"
@@ -4,14 +4,12 @@ import Spinner from 'shared/components/Spinner.vue';
import EmptyState from 'dashboard/components/widgets/EmptyState.vue';
import { dynamicTime } from 'shared/helpers/timeHelper';
import { mapGetters } from 'vuex';
import NextButton from 'dashboard/components-next/button/Button.vue';
export default {
components: {
Thumbnail,
Spinner,
EmptyState,
NextButton,
},
props: {
notifications: {
@@ -51,22 +49,17 @@ export default {
<template>
<section
class="flex-grow flex-shrink h-full px-4 py-8 overflow-hidden bg-n-background"
class="flex-grow flex-shrink h-full px-4 py-8 overflow-hidden bg-white dark:bg-slate-900"
>
<div class="flex w-full items-center justify-between gap-2 mb-4">
<h6 class="text-xl font-medium text-n-slate-12">
{{ $t('NOTIFICATIONS_PAGE.HEADER') }}
</h6>
<NextButton
v-if="notificationMetadata.unreadCount"
type="submit"
sm
:label="$t('NOTIFICATIONS_PAGE.MARK_ALL_DONE')"
:is-loading="isUpdating"
@click="onMarkAllDoneClick"
/>
</div>
<table class="woot-table notifications-table overflow-auto">
<woot-submit-button
v-if="notificationMetadata.unreadCount"
class="button nice success button--fixed-top"
:button-text="$t('NOTIFICATIONS_PAGE.MARK_ALL_DONE')"
:loading="isUpdating"
@click="onMarkAllDoneClick"
/>
<table class="woot-table notifications-table">
<tbody v-show="!isLoading">
<tr
v-for="notificationItem in notifications"
@@ -8,8 +8,8 @@ export const routes = [
path: frontendURL('accounts/:accountId/notifications'),
component: SettingsWrapper,
props: {
headerTitle: '',
icon: '',
headerTitle: 'NOTIFICATIONS_PAGE.HEADER',
icon: 'alert',
showNewButton: false,
showSidemenuIcon: false,
},
@@ -1,12 +1,10 @@
<script>
import { useAdmin } from 'dashboard/composables/useAdmin';
import BackButton from '../../../components/widgets/BackButton.vue';
import NextButton from 'dashboard/components-next/button/Button.vue';
export default {
components: {
BackButton,
NextButton,
},
props: {
headerTitle: {
@@ -72,14 +70,15 @@ export default {
{{ headerTitle }}
</span>
</h1>
<!-- TODO: Remove this when we are not using this -->
<router-link v-if="showNewButton && isAdmin" :to="buttonRoute">
<NextButton
teal
icon="i-lucide-circle-plus"
class="button--fixed-top"
:label="buttonText"
/>
<router-link
v-if="showNewButton && isAdmin"
:to="buttonRoute"
class="button success button--fixed-top rounded-[5px] flex gap-2"
>
<fluent-icon icon="add-circle" />
<span class="button__content">
{{ buttonText }}
</span>
</router-link>
</div>
</template>
@@ -11,15 +11,11 @@ import semver from 'semver';
import { getLanguageDirection } from 'dashboard/components/widgets/conversation/advancedFilterItems/languages';
import BaseSettingsHeader from '../components/BaseSettingsHeader.vue';
import V4Button from 'dashboard/components-next/button/Button.vue';
import WootConfirmDeleteModal from 'dashboard/components/widgets/modal/ConfirmDeleteModal.vue';
import NextButton from 'dashboard/components-next/button/Button.vue';
export default {
components: {
BaseSettingsHeader,
V4Button,
WootConfirmDeleteModal,
NextButton,
},
setup() {
const { updateUISettings } = useUISettings();
@@ -39,7 +35,6 @@ export default {
features: {},
autoResolveDuration: null,
latestChatwootVersion: null,
showDeletePopup: false,
};
},
validations: {
@@ -60,7 +55,6 @@ export default {
getAccount: 'accounts/getAccount',
uiFlags: 'accounts/getUIFlags',
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
isOnChatwootCloud: 'globalConfig/isOnChatwootCloud',
}),
showAutoResolutionConfig() {
return this.isFeatureEnabledonAccount(
@@ -107,34 +101,6 @@ export default {
getAccountId() {
return this.id.toString();
},
confirmPlaceHolderText() {
return `${this.$t(
'GENERAL_SETTINGS.ACCOUNT_DELETE_SECTION.CONFIRM.PLACE_HOLDER',
{
accountName: this.name,
}
)}`;
},
isMarkedForDeletion() {
const { custom_attributes = {} } = this.currentAccount;
return !!custom_attributes.marked_for_deletion_at;
},
markedForDeletionDate() {
const { custom_attributes = {} } = this.currentAccount;
if (!custom_attributes.marked_for_deletion_at) return null;
return new Date(custom_attributes.marked_for_deletion_at);
},
markedForDeletionReason() {
const { custom_attributes = {} } = this.currentAccount;
return custom_attributes.marked_for_deletion_reason || 'manual_deletion';
},
formattedDeletionDate() {
if (!this.markedForDeletionDate) return '';
return this.markedForDeletionDate.toLocaleString();
},
currentAccount() {
return this.getAccount(this.accountId) || {};
},
},
mounted() {
this.initializeAccount();
@@ -196,56 +162,6 @@ export default {
rtl_view: isRTLSupported,
});
},
// Delete Function
openDeletePopup() {
this.showDeletePopup = true;
},
closeDeletePopup() {
this.showDeletePopup = false;
},
async markAccountForDeletion() {
this.closeDeletePopup();
try {
// Use the enterprise API to toggle deletion with delete action
await this.$store.dispatch('accounts/toggleDeletion', {
action_type: 'delete',
});
// Refresh account data
await this.$store.dispatch('accounts/get');
useAlert(this.$t('GENERAL_SETTINGS.ACCOUNT_DELETE_SECTION.SUCCESS'));
} catch (error) {
// Handle error message
this.handleDeletionError(error);
}
},
handleDeletionError(error) {
const errorKey = error.response?.data?.error_key;
if (errorKey) {
useAlert(
this.$t(`GENERAL_SETTINGS.ACCOUNT_DELETE_SECTION.${errorKey}`)
);
return;
}
const message = error.response?.data?.message;
if (message) {
useAlert(message);
return;
}
useAlert(this.$t('GENERAL_SETTINGS.ACCOUNT_DELETE_SECTION.FAILURE'));
},
async clearDeletionMark() {
try {
// Use the enterprise API to toggle deletion with undelete action
await this.$store.dispatch('accounts/toggleDeletion', {
action_type: 'undelete',
});
// Refresh account data
await this.$store.dispatch('accounts/get');
useAlert(this.$t('GENERAL_SETTINGS.UPDATE.SUCCESS'));
} catch (error) {
useAlert(this.$t('GENERAL_SETTINGS.UPDATE.ERROR'));
}
},
},
};
</script>
@@ -259,7 +175,7 @@ export default {
</V4Button>
</template>
</BaseSettingsHeader>
<div class="flex-grow flex-shrink min-w-0 mt-3 overflow-auto">
<div class="flex-grow flex-shrink min-w-0 overflow-auto mt-3">
<form v-if="!uiFlags.isFetchingItem" @submit.prevent="updateAccount">
<div
class="flex flex-row border-b border-slate-25 dark:border-slate-800"
@@ -363,73 +279,6 @@ export default {
<woot-code :script="getAccountId" />
</div>
</div>
<div v-if="!uiFlags.isFetchingItem && isOnChatwootCloud">
<div
class="flex flex-row pt-4 mt-2 border-t border-slate-25 dark:border-slate-800 text-black-900 dark:text-slate-300"
>
<div
class="flex-grow-0 flex-shrink-0 flex-[25%] min-w-0 py-4 pr-6 pl-0"
>
<h4 class="text-lg font-medium text-black-900 dark:text-slate-200">
{{ $t('GENERAL_SETTINGS.ACCOUNT_DELETE_SECTION.TITLE') }}
</h4>
<p>
{{ $t('GENERAL_SETTINGS.ACCOUNT_DELETE_SECTION.NOTE') }}
</p>
</div>
<div class="p-4 flex-grow-0 flex-shrink-0 flex-[50%]">
<div v-if="isMarkedForDeletion">
<div
class="p-4 flex-grow-0 flex-shrink-0 flex-[50%] bg-red-50 dark:bg-red-900 rounded"
>
<p class="mb-4">
{{
$t(
`GENERAL_SETTINGS.ACCOUNT_DELETE_SECTION.SCHEDULED_DELETION.MESSAGE_${markedForDeletionReason === 'manual_deletion' ? 'MANUAL' : 'INACTIVITY'}`,
{
deletionDate: formattedDeletionDate,
}
)
}}
</p>
<NextButton
:label="
$t(
'GENERAL_SETTINGS.ACCOUNT_DELETE_SECTION.SCHEDULED_DELETION.CLEAR_BUTTON'
)
"
color="ruby"
:is-loading="uiFlags.isUpdating"
@click="clearDeletionMark"
/>
</div>
</div>
<div v-if="!isMarkedForDeletion">
<NextButton
:label="$t('GENERAL_SETTINGS.ACCOUNT_DELETE_SECTION.BUTTON_TEXT')"
color="ruby"
@click="openDeletePopup()"
/>
</div>
</div>
</div>
<WootConfirmDeleteModal
v-if="showDeletePopup"
v-model:show="showDeletePopup"
:title="$t('GENERAL_SETTINGS.ACCOUNT_DELETE_SECTION.CONFIRM.TITLE')"
:message="$t('GENERAL_SETTINGS.ACCOUNT_DELETE_SECTION.CONFIRM.MESSAGE')"
:confirm-text="
$t('GENERAL_SETTINGS.ACCOUNT_DELETE_SECTION.CONFIRM.BUTTON_TEXT')
"
:reject-text="
$t('GENERAL_SETTINGS.ACCOUNT_DELETE_SECTION.CONFIRM.DISMISS')
"
:confirm-value="name"
:confirm-place-holder-text="confirmPlaceHolderText"
@on-confirm="markAccountForDeletion"
@on-close="closeDeletePopup"
/>
</div>
<div class="p-4 text-sm text-center">
<div>{{ `v${globalConfig.appVersion}` }}</div>
<div v-if="hasAnUpdateAvailable && globalConfig.displayManifest">
@@ -82,6 +82,7 @@ export default {
data() {
return {
automationRuleEvent: AUTOMATION_RULE_EVENTS[0].key,
automationRuleEvents: AUTOMATION_RULE_EVENTS,
automationMutated: false,
show: true,
showDeleteConfirmationModal: false,
@@ -95,12 +96,6 @@ export default {
accountId: 'getCurrentAccountId',
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
}),
automationRuleEvents() {
return AUTOMATION_RULE_EVENTS.map(event => ({
...event,
value: this.$t(`AUTOMATION.EVENTS.${event.value}`),
}));
},
hasAutomationMutated() {
if (
this.automation.conditions[0].values ||
@@ -110,14 +105,10 @@ export default {
return false;
},
automationActionTypes() {
const actionTypes = this.isFeatureEnabled('sla')
const isSLAEnabled = this.isFeatureEnabled('sla');
return isSLAEnabled
? AUTOMATION_ACTION_TYPES
: AUTOMATION_ACTION_TYPES.filter(({ key }) => key !== 'add_sla');
return actionTypes.map(action => ({
...action,
label: this.$t(`AUTOMATION.ACTIONS.${action.label}`),
}));
: AUTOMATION_ACTION_TYPES.filter(action => action.key !== 'add_sla');
},
},
mounted() {
@@ -146,26 +137,6 @@ export default {
this.$emit('saveAutomation', automation, this.mode);
}
},
getTranslatedAttributes(type, event) {
return getAttributes(type, event).map(attribute => {
// Skip translation
// 1. If customAttributeType key is present then its rendering attributes from API
// 2. If contact_custom_attribute or conversation_custom_attribute is present then its rendering section title
const skipTranslation =
attribute.customAttributeType ||
[
'contact_custom_attribute',
'conversation_custom_attribute',
].includes(attribute.key);
return {
...attribute,
name: skipTranslation
? attribute.name
: this.$t(`AUTOMATION.ATTRIBUTES.${attribute.name}`),
};
});
},
},
};
</script>
@@ -233,7 +204,7 @@ export default {
:key="i"
v-model="automation.conditions[i]"
:filter-attributes="
getTranslatedAttributes(automationTypes, automation.event_name)
getAttributes(automationTypes, automation.event_name)
"
:input-type="
getInputType(
@@ -70,6 +70,7 @@ export default {
data() {
return {
automationRuleEvent: AUTOMATION_RULE_EVENTS[0].key,
automationRuleEvents: AUTOMATION_RULE_EVENTS,
automationMutated: false,
show: true,
showDeleteConfirmationModal: false,
@@ -83,12 +84,6 @@ export default {
accountId: 'getCurrentAccountId',
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
}),
automationRuleEvents() {
return AUTOMATION_RULE_EVENTS.map(event => ({
...event,
value: this.$t(`AUTOMATION.EVENTS.${event.value}`),
}));
},
hasAutomationMutated() {
if (
this.automation.conditions[0].values ||
@@ -98,14 +93,10 @@ export default {
return false;
},
automationActionTypes() {
const actionTypes = this.isFeatureEnabled('sla')
const isSLAEnabled = this.isFeatureEnabled('sla');
return isSLAEnabled
? AUTOMATION_ACTION_TYPES
: AUTOMATION_ACTION_TYPES.filter(({ key }) => key !== 'add_sla');
return actionTypes.map(action => ({
...action,
label: this.$t(`AUTOMATION.ACTIONS.${action.label}`),
}));
: AUTOMATION_ACTION_TYPES.filter(action => action.key !== 'add_sla');
},
},
mounted() {
@@ -136,26 +127,6 @@ export default {
this.$emit('saveAutomation', automation, this.mode);
}
},
getTranslatedAttributes(type, event) {
return getAttributes(type, event).map(attribute => {
// Skip translation
// 1. If customAttributeType key is present then its rendering attributes from API
// 2. If contact_custom_attribute or conversation_custom_attribute is present then its rendering section title
const skipTranslation =
attribute.customAttributeType ||
[
'contact_custom_attribute',
'conversation_custom_attribute',
].includes(attribute.key);
return {
...attribute,
name: skipTranslation
? attribute.name
: this.$t(`AUTOMATION.ATTRIBUTES.${attribute.name}`),
};
});
},
},
};
</script>
@@ -216,7 +187,7 @@ export default {
:key="i"
v-model="automation.conditions[i]"
:filter-attributes="
getTranslatedAttributes(automationTypes, automation.event_name)
getAttributes(automationTypes, automation.event_name)
"
:input-type="
getInputType(
@@ -10,37 +10,43 @@ export const AUTOMATIONS = {
conditions: [
{
key: 'message_type',
name: 'MESSAGE_TYPE',
name: 'Message Type',
attributeI18nKey: 'MESSAGE_TYPE',
inputType: 'search_select',
filterOperators: OPERATOR_TYPES_1,
},
{
key: 'content',
name: 'MESSAGE_CONTAINS',
name: 'Message Content',
attributeI18nKey: 'MESSAGE_CONTAINS',
inputType: 'comma_separated_plain_text',
filterOperators: OPERATOR_TYPES_2,
},
{
key: 'email',
name: 'EMAIL',
name: 'Email',
attributeI18nKey: 'EMAIL',
inputType: 'plain_text',
filterOperators: OPERATOR_TYPES_2,
},
{
key: 'inbox_id',
name: 'INBOX',
name: 'Inbox',
attributeI18nKey: 'INBOX',
inputType: 'multi_select',
filterOperators: OPERATOR_TYPES_1,
},
{
key: 'conversation_language',
name: 'CONVERSATION_LANGUAGE',
name: 'Conversation Language',
attributeI18nKey: 'CONVERSATION_LANGUAGE',
inputType: 'multi_select',
filterOperators: OPERATOR_TYPES_1,
},
{
key: 'phone_number',
name: 'PHONE_NUMBER',
name: 'Phone Number',
attributeI18nKey: 'PHONE_NUMBER',
inputType: 'plain_text',
filterOperators: OPERATOR_TYPES_6,
},
@@ -48,52 +54,64 @@ export const AUTOMATIONS = {
actions: [
{
key: 'assign_agent',
name: 'ASSIGN_AGENT',
name: 'Assign to agent',
attributeI18nKey: 'ASSIGN_AGENT',
},
{
key: 'assign_team',
name: 'ASSIGN_TEAM',
name: 'Assign a team',
attributeI18nKey: 'ASSIGN_TEAM',
},
{
key: 'add_label',
name: 'ADD_LABEL',
name: 'Add a label',
attributeI18nKey: 'ADD_LABEL',
},
{
key: 'remove_label',
name: 'REMOVE_LABEL',
name: 'Remove a label',
attributeI18nKey: 'REMOVE_LABEL',
},
{
key: 'send_email_to_team',
name: 'SEND_EMAIL_TO_TEAM',
name: 'Send an email to team',
attributeI18nKey: 'SEND_EMAIL_TO_TEAM',
},
{
key: 'send_message',
name: 'SEND_MESSAGE',
name: 'Send a message',
attributeI18nKey: 'SEND_MESSAGE',
},
{
key: 'send_email_transcript',
name: 'SEND_EMAIL_TRANSCRIPT',
name: 'Send an email transcript',
attributeI18nKey: 'SEND_EMAIL_TRANSCRIPT',
},
{
key: 'mute_conversation',
name: 'MUTE_CONVERSATION',
name: 'Mute conversation',
attributeI18nKey: 'MUTE_CONVERSATION',
},
{
key: 'snooze_conversation',
name: 'SNOOZE_CONVERSATION',
name: 'Snooze conversation',
attributeI18nKey: 'MUTE_CONVERSATION',
},
{
key: 'resolve_conversation',
name: 'RESOLVE_CONVERSATION',
name: 'Resolve conversation',
attributeI18nKey: 'RESOLVE_CONVERSATION',
},
{
key: 'send_webhook_event',
name: 'SEND_WEBHOOK_EVENT',
name: 'Send Webhook Event',
attributeI18nKey: 'SEND_WEBHOOK_EVENT',
},
{
key: 'send_attachment',
name: 'SEND_ATTACHMENT',
name: 'Send Attachment',
attributeI18nKey: 'SEND_ATTACHMENT',
},
],
},
@@ -101,61 +119,71 @@ export const AUTOMATIONS = {
conditions: [
{
key: 'status',
name: 'STATUS',
name: 'Status',
attributeI18nKey: 'STATUS',
inputType: 'multi_select',
filterOperators: OPERATOR_TYPES_1,
},
{
key: 'browser_language',
name: 'BROWSER_LANGUAGE',
name: 'Browser Language',
attributeI18nKey: 'BROWSER_LANGUAGE',
inputType: 'search_select',
filterOperators: OPERATOR_TYPES_1,
},
{
key: 'mail_subject',
name: 'MAIL_SUBJECT',
name: 'Email Subject',
attributeI18nKey: 'MAIL_SUBJECT',
inputType: 'plain_text',
filterOperators: OPERATOR_TYPES_2,
},
{
key: 'country_code',
name: 'COUNTRY_NAME',
name: 'Country',
attributeI18nKey: 'COUNTRY_NAME',
inputType: 'search_select',
filterOperators: OPERATOR_TYPES_1,
},
{
key: 'phone_number',
name: 'PHONE_NUMBER',
name: 'Phone Number',
attributeI18nKey: 'PHONE_NUMBER',
inputType: 'plain_text',
filterOperators: OPERATOR_TYPES_6,
},
{
key: 'referer',
name: 'REFERER_LINK',
name: 'Referrer Link',
attributeI18nKey: 'REFERER_LINK',
inputType: 'plain_text',
filterOperators: OPERATOR_TYPES_2,
},
{
key: 'email',
name: 'EMAIL',
name: 'Email',
attributeI18nKey: 'EMAIL',
inputType: 'plain_text',
filterOperators: OPERATOR_TYPES_2,
},
{
key: 'inbox_id',
name: 'INBOX',
name: 'Inbox',
attributeI18nKey: 'INBOX',
inputType: 'multi_select',
filterOperators: OPERATOR_TYPES_1,
},
{
key: 'conversation_language',
name: 'CONVERSATION_LANGUAGE',
name: 'Conversation Language',
attributeI18nKey: 'CONVERSATION_LANGUAGE',
inputType: 'multi_select',
filterOperators: OPERATOR_TYPES_1,
},
{
key: 'priority',
name: 'PRIORITY',
name: 'Priority',
attributeI18nKey: 'PRIORITY',
inputType: 'multi_select',
filterOperators: OPERATOR_TYPES_1,
},
@@ -163,47 +191,58 @@ export const AUTOMATIONS = {
actions: [
{
key: 'assign_agent',
name: 'ASSIGN_AGENT',
name: 'Assign to agent',
attributeI18nKey: 'ASSIGN_AGENT',
},
{
key: 'assign_team',
name: 'ASSIGN_TEAM',
name: 'Assign a team',
attributeI18nKey: 'ASSIGN_TEAM',
},
{
key: 'assign_agent',
name: 'ASSIGN_AGENT',
name: 'Assign an agent',
attributeI18nKey: 'ASSIGN_AGENT',
},
{
key: 'send_email_to_team',
name: 'SEND_EMAIL_TO_TEAM',
name: 'Send an email to team',
attributeI18nKey: 'SEND_EMAIL_TO_TEAM',
},
{
key: 'send_message',
name: 'SEND_MESSAGE',
name: 'Send a message',
attributeI18nKey: 'SEND_MESSAGE',
},
{
key: 'send_email_transcript',
name: 'SEND_EMAIL_TRANSCRIPT',
name: 'Send an email transcript',
attributeI18nKey: 'SEND_EMAIL_TRANSCRIPT',
},
{
key: 'mute_conversation',
name: 'MUTE_CONVERSATION',
name: 'Mute conversation',
attributeI18nKey: 'MUTE_CONVERSATION',
},
{
key: 'snooze_conversation',
name: 'SNOOZE_CONVERSATION',
name: 'Snooze conversation',
attributeI18nKey: 'MUTE_CONVERSATION',
},
{
key: 'resolve_conversation',
name: 'RESOLVE_CONVERSATION',
name: 'Resolve conversation',
attributeI18nKey: 'RESOLVE_CONVERSATION',
},
{
key: 'send_webhook_event',
name: 'SEND_WEBHOOK_EVENT',
name: 'Send Webhook Event',
attributeI18nKey: 'SEND_WEBHOOK_EVENT',
},
{
key: 'send_attachment',
name: 'SEND_ATTACHMENT',
name: 'Send Attachment',
attributeI18nKey: 'SEND_ATTACHMENT',
},
],
},
@@ -211,73 +250,85 @@ export const AUTOMATIONS = {
conditions: [
{
key: 'status',
name: 'STATUS',
name: 'Status',
attributeI18nKey: 'STATUS',
inputType: 'multi_select',
filterOperators: OPERATOR_TYPES_1,
},
{
key: 'browser_language',
name: 'BROWSER_LANGUAGE',
name: 'Browser Language',
attributeI18nKey: 'BROWSER_LANGUAGE',
inputType: 'search_select',
filterOperators: OPERATOR_TYPES_1,
},
{
key: 'mail_subject',
name: 'MAIL_SUBJECT',
name: 'Email Subject',
attributeI18nKey: 'MAIL_SUBJECT',
inputType: 'plain_text',
filterOperators: OPERATOR_TYPES_2,
},
{
key: 'country_code',
name: 'COUNTRY_NAME',
name: 'Country',
attributeI18nKey: 'COUNTRY_NAME',
inputType: 'search_select',
filterOperators: OPERATOR_TYPES_1,
},
{
key: 'referer',
name: 'REFERER_LINK',
name: 'Referrer Link',
attributeI18nKey: 'REFERER_LINK',
inputType: 'plain_text',
filterOperators: OPERATOR_TYPES_2,
},
{
key: 'phone_number',
name: 'PHONE_NUMBER',
name: 'Phone Number',
attributeI18nKey: 'PHONE_NUMBER',
inputType: 'plain_text',
filterOperators: OPERATOR_TYPES_6,
},
{
key: 'assignee_id',
name: 'ASSIGNEE_NAME',
name: 'Assignee',
attributeI18nKey: 'ASSIGNEE_NAME',
inputType: 'search_select',
filterOperators: OPERATOR_TYPES_3,
},
{
key: 'team_id',
name: 'TEAM_NAME',
name: 'Team',
attributeI18nKey: 'TEAM_NAME',
inputType: 'search_select',
filterOperators: OPERATOR_TYPES_3,
},
{
key: 'email',
name: 'EMAIL',
name: 'Email',
attributeI18nKey: 'EMAIL',
inputType: 'plain_text',
filterOperators: OPERATOR_TYPES_2,
},
{
key: 'inbox_id',
name: 'INBOX',
name: 'Inbox',
attributeI18nKey: 'INBOX',
inputType: 'multi_select',
filterOperators: OPERATOR_TYPES_1,
},
{
key: 'conversation_language',
name: 'CONVERSATION_LANGUAGE',
name: 'Conversation Language',
attributeI18nKey: 'CONVERSATION_LANGUAGE',
inputType: 'multi_select',
filterOperators: OPERATOR_TYPES_1,
},
{
key: 'priority',
name: 'PRIORITY',
name: 'Priority',
attributeI18nKey: 'PRIORITY',
inputType: 'multi_select',
filterOperators: OPERATOR_TYPES_1,
},
@@ -285,47 +336,58 @@ export const AUTOMATIONS = {
actions: [
{
key: 'assign_agent',
name: 'ASSIGN_AGENT',
name: 'Assign to agent',
attributeI18nKey: 'ASSIGN_AGENT',
},
{
key: 'assign_team',
name: 'ASSIGN_TEAM',
name: 'Assign a team',
attributeI18nKey: 'ASSIGN_TEAM',
},
{
key: 'assign_agent',
name: 'ASSIGN_AGENT',
name: 'Assign an agent',
attributeI18nKey: 'ASSIGN_AGENT',
},
{
key: 'send_email_to_team',
name: 'SEND_EMAIL_TO_TEAM',
name: 'Send an email to team',
attributeI18nKey: 'SEND_EMAIL_TO_TEAM',
},
{
key: 'send_message',
name: 'SEND_MESSAGE',
name: 'Send a message',
attributeI18nKey: 'SEND_MESSAGE',
},
{
key: 'send_email_transcript',
name: 'SEND_EMAIL_TRANSCRIPT',
name: 'Send an email transcript',
attributeI18nKey: 'SEND_EMAIL_TRANSCRIPT',
},
{
key: 'mute_conversation',
name: 'MUTE_CONVERSATION',
name: 'Mute conversation',
attributeI18nKey: 'MUTE_CONVERSATION',
},
{
key: 'snooze_conversation',
name: 'SNOOZE_CONVERSATION',
name: 'Snooze conversation',
attributeI18nKey: 'MUTE_CONVERSATION',
},
{
key: 'resolve_conversation',
name: 'RESOLVE_CONVERSATION',
name: 'Resolve conversation',
attributeI18nKey: 'RESOLVE_CONVERSATION',
},
{
key: 'send_webhook_event',
name: 'SEND_WEBHOOK_EVENT',
name: 'Send Webhook Event',
attributeI18nKey: 'SEND_WEBHOOK_EVENT',
},
{
key: 'send_attachment',
name: 'SEND_ATTACHMENT',
name: 'Send Attachment',
attributeI18nKey: 'SEND_ATTACHMENT',
},
],
},
@@ -333,67 +395,78 @@ export const AUTOMATIONS = {
conditions: [
{
key: 'browser_language',
name: 'BROWSER_LANGUAGE',
name: 'Browser Language',
attributeI18nKey: 'BROWSER_LANGUAGE',
inputType: 'search_select',
filterOperators: OPERATOR_TYPES_1,
},
{
key: 'email',
name: 'EMAIL',
name: 'Email',
attributeI18nKey: 'EMAIL',
inputType: 'plain_text',
filterOperators: OPERATOR_TYPES_2,
},
{
key: 'mail_subject',
name: 'MAIL_SUBJECT',
name: 'Email Subject',
attributeI18nKey: 'MAIL_SUBJECT',
inputType: 'plain_text',
filterOperators: OPERATOR_TYPES_2,
},
{
key: 'country_code',
name: 'COUNTRY_NAME',
name: 'Country',
attributeI18nKey: 'COUNTRY_NAME',
inputType: 'search_select',
filterOperators: OPERATOR_TYPES_1,
},
{
key: 'referer',
name: 'REFERER_LINK',
name: 'Referrer Link',
attributeI18nKey: 'REFERER_LINK',
inputType: 'plain_text',
filterOperators: OPERATOR_TYPES_2,
},
{
key: 'assignee_id',
name: 'ASSIGNEE_NAME',
name: 'Assignee',
attributeI18nKey: 'ASSIGNEE_NAME',
inputType: 'search_select',
filterOperators: OPERATOR_TYPES_3,
},
{
key: 'phone_number',
name: 'PHONE_NUMBER',
name: 'Phone Number',
attributeI18nKey: 'PHONE_NUMBER',
inputType: 'plain_text',
filterOperators: OPERATOR_TYPES_6,
},
{
key: 'team_id',
name: 'TEAM_NAME',
name: 'Team',
attributeI18nKey: 'TEAM_NAME',
inputType: 'search_select',
filterOperators: OPERATOR_TYPES_3,
},
{
key: 'inbox_id',
name: 'INBOX',
name: 'Inbox',
attributeI18nKey: 'INBOX',
inputType: 'multi_select',
filterOperators: OPERATOR_TYPES_1,
},
{
key: 'conversation_language',
name: 'CONVERSATION_LANGUAGE',
name: 'Conversation Language',
attributeI18nKey: 'CONVERSATION_LANGUAGE',
inputType: 'multi_select',
filterOperators: OPERATOR_TYPES_1,
},
{
key: 'priority',
name: 'PRIORITY',
name: 'Priority',
attributeI18nKey: 'PRIORITY',
inputType: 'multi_select',
filterOperators: OPERATOR_TYPES_1,
},
@@ -401,43 +474,53 @@ export const AUTOMATIONS = {
actions: [
{
key: 'assign_agent',
name: 'ASSIGN_AGENT',
name: 'Assign to agent',
attributeI18nKey: 'ASSIGN_AGENT',
},
{
key: 'assign_team',
name: 'ASSIGN_TEAM',
name: 'Assign a team',
attributeI18nKey: 'ASSIGN_TEAM',
},
{
key: 'assign_agent',
name: 'ASSIGN_AGENT',
name: 'Assign an agent',
attributeI18nKey: 'ASSIGN_AGENT',
},
{
key: 'send_email_to_team',
name: 'SEND_EMAIL_TO_TEAM',
name: 'Send an email to team',
attributeI18nKey: 'SEND_EMAIL_TO_TEAM',
},
{
key: 'send_message',
name: 'SEND_MESSAGE',
name: 'Send a message',
attributeI18nKey: 'SEND_MESSAGE',
},
{
key: 'send_email_transcript',
name: 'SEND_EMAIL_TRANSCRIPT',
name: 'Send an email transcript',
attributeI18nKey: 'SEND_EMAIL_TRANSCRIPT',
},
{
key: 'mute_conversation',
name: 'MUTE_CONVERSATION',
name: 'Mute conversation',
attributeI18nKey: 'MUTE_CONVERSATION',
},
{
key: 'snooze_conversation',
name: 'SNOOZE_CONVERSATION',
name: 'Snooze conversation',
attributeI18nKey: 'MUTE_CONVERSATION',
},
{
key: 'send_webhook_event',
name: 'SEND_WEBHOOK_EVENT',
name: 'Send Webhook Event',
attributeI18nKey: 'SEND_WEBHOOK_EVENT',
},
{
key: 'send_attachment',
name: 'SEND_ATTACHMENT',
name: 'Send Attachment',
attributeI18nKey: 'SEND_ATTACHMENT',
},
],
},
@@ -446,91 +529,91 @@ export const AUTOMATIONS = {
export const AUTOMATION_RULE_EVENTS = [
{
key: 'conversation_created',
value: 'CONVERSATION_CREATED',
value: 'Conversation Created',
},
{
key: 'conversation_updated',
value: 'CONVERSATION_UPDATED',
value: 'Conversation Updated',
},
{
key: 'message_created',
value: 'MESSAGE_CREATED',
value: 'Message Created',
},
{
key: 'conversation_opened',
value: 'CONVERSATION_OPENED',
value: 'Conversation Opened',
},
];
export const AUTOMATION_ACTION_TYPES = [
{
key: 'assign_agent',
label: 'ASSIGN_AGENT',
label: 'Assign to agent',
inputType: 'search_select',
},
{
key: 'assign_team',
label: 'ASSIGN_TEAM',
label: 'Assign a team',
inputType: 'search_select',
},
{
key: 'add_label',
label: 'ADD_LABEL',
label: 'Add a label',
inputType: 'multi_select',
},
{
key: 'remove_label',
label: 'REMOVE_LABEL',
label: 'Remove a label',
inputType: 'multi_select',
},
{
key: 'send_email_to_team',
label: 'SEND_EMAIL_TO_TEAM',
label: 'Send an email to team',
inputType: 'team_message',
},
{
key: 'send_email_transcript',
label: 'SEND_EMAIL_TRANSCRIPT',
label: 'Send an email transcript',
inputType: 'email',
},
{
key: 'mute_conversation',
label: 'MUTE_CONVERSATION',
label: 'Mute conversation',
inputType: null,
},
{
key: 'snooze_conversation',
label: 'SNOOZE_CONVERSATION',
label: 'Snooze conversation',
inputType: null,
},
{
key: 'resolve_conversation',
label: 'RESOLVE_CONVERSATION',
label: 'Resolve conversation',
inputType: null,
},
{
key: 'send_webhook_event',
label: 'SEND_WEBHOOK_EVENT',
label: 'Send Webhook Event',
inputType: 'url',
},
{
key: 'send_attachment',
label: 'SEND_ATTACHMENT',
label: 'Send Attachment',
inputType: 'attachment',
},
{
key: 'send_message',
label: 'SEND_MESSAGE',
label: 'Send a message',
inputType: 'textarea',
},
{
key: 'change_priority',
label: 'CHANGE_PRIORITY',
label: 'Change Priority',
inputType: 'search_select',
},
{
key: 'add_sla',
label: 'ADD_SLA',
label: 'Add SLA',
inputType: 'search_select',
},
];
@@ -9,7 +9,6 @@ import Sms from './channels/Sms.vue';
import Whatsapp from './channels/Whatsapp.vue';
import Line from './channels/Line.vue';
import Telegram from './channels/Telegram.vue';
import Instagram from './channels/Instagram.vue';
const channelViewList = {
facebook: Facebook,
@@ -21,7 +20,6 @@ const channelViewList = {
whatsapp: Whatsapp,
line: Line,
telegram: Telegram,
instagram: Instagram,
};
export default defineComponent({

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