Compare commits

..
397 changed files with 5767 additions and 8927 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 285 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 966 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 934 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

-3
View File
@@ -91,6 +91,3 @@ yarn-debug.log*
# Vite uses dotenv and suggests to ignore local-only env files. See # Vite uses dotenv and suggests to ignore local-only env files. See
# https://vitejs.dev/guide/env-and-mode.html#env-files # https://vitejs.dev/guide/env-and-mode.html#env-files
*.local *.local
# Claude.ai config file
CLAUDE.md
+4 -4
View File
@@ -501,14 +501,14 @@ GEM
newrelic_rpm (9.6.0) newrelic_rpm (9.6.0)
base64 base64
nio4r (2.7.3) nio4r (2.7.3)
nokogiri (1.18.4) nokogiri (1.18.3)
mini_portile2 (~> 2.8.2) mini_portile2 (~> 2.8.2)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.18.4-arm64-darwin) nokogiri (1.18.3-arm64-darwin)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.18.4-x86_64-darwin) nokogiri (1.18.3-x86_64-darwin)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.18.4-x86_64-linux-gnu) nokogiri (1.18.3-x86_64-linux-gnu)
racc (~> 1.4) racc (~> 1.4)
oauth (1.1.0) oauth (1.1.0)
oauth-tty (~> 1.0, >= 1.0.1) oauth-tty (~> 1.0, >= 1.0.1)
+46 -63
View File
@@ -1,11 +1,22 @@
<img src="./.github/screenshots/header.png#gh-light-mode-only" width="100%" alt="Header light mode"/> ## 🚨 Note: This branch is unstable. For the stable branch's source code, please use the branch [3.x](https://github.com/chatwoot/chatwoot/tree/3.x)
<img src="./.github/screenshots/header-dark.png#gh-dark-mode-only" width="100%" alt="Header dark mode"/>
<img src="https://user-images.githubusercontent.com/2246121/282256557-1570674b-d142-4198-9740-69404cc6a339.png#gh-light-mode-only" width="100%" alt="Chat dashboard dark mode"/>
<img src="https://user-images.githubusercontent.com/2246121/282256632-87f6a01b-6467-4e0e-8a93-7bbf66d03a17.png#gh-dark-mode-only" width="100%" alt="Chat dashboard"/>
___ ___
# Chatwoot # Chatwoot
The modern customer support platform, an open-source alternative to Intercom, Zendesk, Salesforce Service Cloud etc. Customer engagement suite, an open-source alternative to Intercom, Zendesk, Salesforce Service Cloud etc.
<p>
<a href="https://heroku.com/deploy?template=https://github.com/chatwoot/chatwoot/tree/master" alt="Deploy to Heroku">
<img width="150" alt="Deploy" src="https://www.herokucdn.com/deploy/button.svg"/>
</a>
<a href="https://marketplace.digitalocean.com/apps/chatwoot?refcode=f2238426a2a8" alt="Deploy to DigitalOcean">
<img width="200" alt="Deploy to DO" src="https://www.deploytodo.com/do-btn-blue.svg"/>
</a>
</p>
<p> <p>
<a href="https://codeclimate.com/github/chatwoot/chatwoot/maintainability"><img src="https://api.codeclimate.com/v1/badges/e6e3f66332c91e5a4c0c/maintainability" alt="Maintainability"></a> <a href="https://codeclimate.com/github/chatwoot/chatwoot/maintainability"><img src="https://api.codeclimate.com/v1/badges/e6e3f66332c91e5a4c0c/maintainability" alt="Maintainability"></a>
@@ -20,71 +31,41 @@ The modern customer support platform, an open-source alternative to Intercom, Ze
<a href="https://artifacthub.io/packages/helm/chatwoot/chatwoot"><img src="https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/artifact-hub" alt="Artifact HUB"></a> <a href="https://artifacthub.io/packages/helm/chatwoot/chatwoot"><img src="https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/artifact-hub" alt="Artifact HUB"></a>
</p> </p>
<img src="https://user-images.githubusercontent.com/2246121/282255783-ee8a50c9-f42d-4752-8201-2d59965a663d.png#gh-light-mode-only" width="100%" alt="Chat dashboard dark mode"/>
<img src="https://user-images.githubusercontent.com/2246121/282255784-3d1994ec-d895-4ff5-ac68-d819987e1869.png#gh-dark-mode-only" width="100%" alt="Chat dashboard"/>
<p> Chatwoot is an open-source, self-hosted customer engagement suite. Chatwoot lets you view and manage your customer data, communicate with them irrespective of which medium they use, and re-engage them based on their profile.
<a href="https://heroku.com/deploy?template=https://github.com/chatwoot/chatwoot/tree/master" alt="Deploy to Heroku">
<img width="150" alt="Deploy" src="https://www.herokucdn.com/deploy/button.svg"/>
</a>
<a href="https://marketplace.digitalocean.com/apps/chatwoot?refcode=f2238426a2a8" alt="Deploy to DigitalOcean">
<img width="200" alt="Deploy to DO" src="https://www.deploytodo.com/do-btn-blue.svg"/>
</a>
</p>
<img src="./.github/screenshots/dashboard.png#gh-light-mode-only" width="100%" alt="Chat dashboard dark mode"/> ## Features
<img src="./.github/screenshots/dashboard-dark.png#gh-dark-mode-only" width="100%" alt="Chat dashboard"/>
--- Chatwoot supports the following conversation channels:
Chatwoot is the modern, open-source, and self-hosted customer support platform designed to help businesses deliver exceptional customer support experience. Built for scale and flexibility, Chatwoot gives you full control over your customer data while providing powerful tools to manage conversations across channels. - **Website**: Talk to your customers using our live chat widget and make use of our SDK to identify a user and provide contextual support.
- **Facebook**: Connect your Facebook pages and start replying to the direct messages to your page.
- **Instagram**: Connect your Instagram profile and start replying to the direct messages.
- **Twitter**: Connect your Twitter profiles and reply to direct messages or the tweets where you are mentioned.
- **Telegram**: Connect your Telegram bot and reply to your customers right from a single dashboard.
- **WhatsApp**: Connect your WhatsApp business account and manage the conversation in Chatwoot.
- **Line**: Connect your Line account and manage the conversations in Chatwoot.
- **SMS**: Connect your Twilio SMS account and reply to the SMS queries in Chatwoot.
- **API Channel**: Build custom communication channels using our API channel.
- **Email**: Forward all your email queries to Chatwoot and view it in our integrated dashboard.
### ✨ Captain AI Agent for Support And more.
Supercharge your support with Captain, Chatwoots AI agent. Captain helps automate responses, handle common queries, and reduce agent workload—ensuring customers get instant, accurate answers. With Captain, your team can focus on complex conversations while routine questions are resolved automatically. Read more about Captain [here](https://chwt.app/captain-docs). Other features include:
### 💬 Omnichannel Support Desk
Chatwoot centralizes all customer conversations into one powerful inbox, no matter where your customers reach out from. It supports live chat on your website, email, Facebook, Instagram, Twitter, WhatsApp, Telegram, Line, SMS etc.
### 📚 Help center portal
Publish help articles, FAQs, and guides through the built-in Help Center Portal. Enable customers to find answers on their own, reduce repetitive queries, and keep your support team focused on more complex issues.
### 🗂️ Other features
#### Collaboration & Productivity
- Private Notes and @mentions for internal team discussions.
- Labels to organize and categorize conversations.
- Keyboard Shortcuts and a Command Bar for quick navigation.
- Canned Responses to reply faster to frequently asked questions.
- Auto-Assignment to route conversations based on agent availability.
- Multi-lingual Support to serve customers in multiple languages.
- Custom Views and Filters for better inbox organization.
- Business Hours and Auto-Responders to manage response expectations.
- Teams and Automation tools for scaling support workflows.
- Agent Capacity Management to balance workload across the team.
#### Customer Data & Segmentation
- Contact Management with profiles and interaction history.
- Contact Segments and Notes for targeted communication.
- Campaigns to proactively engage customers.
- Custom Attributes for storing additional customer data.
- Pre-Chat Forms to collect user information before starting conversations.
#### Integrations
- Slack Integration to manage conversations directly from Slack.
- Dialogflow Integration for chatbot automation.
- Dashboard Apps to embed internal tools within Chatwoot.
- Shopify Integration to view and manage customer orders right within Chatwoot.
- Use Google Translate to translate messages from your customers in realtime.
- Create and manage Linear tickets within Chatwoot.
#### Reports & Insights
- Live View of ongoing conversations for real-time monitoring.
- Conversation, Agent, Inbox, Label, and Team Reports for operational visibility.
- CSAT Reports to measure customer satisfaction.
- Downloadable Reports for offline analysis and reporting.
- **CRM**: Save all your customer information right inside Chatwoot, use contact notes to log emails, phone calls, or meeting notes.
- **Custom Attributes**: Define custom attribute attributes to store information about a contact or a conversation and extend the product to match your workflow.
- **Shared multi-brand inboxes**: Manage multiple brands or pages using a shared inbox.
- **Private notes**: Use @mentions and private notes to communicate internally about a conversation.
- **Canned responses (Saved replies)**: Improve the response rate by adding saved replies for frequently asked questions.
- **Conversation Labels**: Use conversation labels to create custom workflows.
- **Auto assignment**: Chatwoot intelligently assigns a ticket to the agents who have access to the inbox depending on their availability and load.
- **Conversation continuity**: If the user has provided an email address through the chat widget, Chatwoot will send an email to the customer under the agent name so that the user can continue the conversation over the email.
- **Multi-lingual support**: Chatwoot supports 10+ languages.
- **Powerful API & Webhooks**: Extend the capability of the software using Chatwoots webhooks and APIs.
- **Integrations**: Chatwoot natively integrates with Slack right now. Manage your conversations in Slack without logging into the dashboard.
## Documentation ## Documentation
@@ -126,11 +107,13 @@ For other supported options, checkout our [deployment page](https://chatwoot.com
Looking to report a vulnerability? Please refer our [SECURITY.md](./SECURITY.md) file. Looking to report a vulnerability? Please refer our [SECURITY.md](./SECURITY.md) file.
## Community
## Community? Questions? Support ?
If you need help or just want to hang out, come, say hi on our [Discord](https://discord.gg/cJXdrwS) server. If you need help or just want to hang out, come, say hi on our [Discord](https://discord.gg/cJXdrwS) server.
## Contributors
## Contributors ✨
Thanks goes to all these [wonderful people](https://www.chatwoot.com/docs/contributors): Thanks goes to all these [wonderful people](https://www.chatwoot.com/docs/contributors):
-35
View File
@@ -64,40 +64,5 @@ class ContactInboxBuilder
inbox_id: @inbox.id, inbox_id: @inbox.id,
source_id: @source_id source_id: @source_id
) )
rescue ActiveRecord::RecordNotUnique
Rails.logger.info("[ContactInboxBuilder] RecordNotUnique #{@source_id} #{@contact.id} #{@inbox.id}")
update_old_contact_inbox
retry
end
def update_old_contact_inbox
# The race condition occurs when theres a contact inbox with the
# same source ID but linked to a different contact. This can happen
# if the agent updates the contacts email or phone number, or
# if the contact is merged with another.
#
# We update the old contact inbox source_id to a random value to
# avoid disrupting the current flow. However, the root cause of
# this issue is a flaw in the contact inbox model design.
# Contact inbox is essentially tracking a session and is not
# needed for non-live chat channels.
raise ActiveRecord::RecordNotUnique unless allowed_channels?
contact_inbox = ::ContactInbox.find_by(inbox_id: @inbox.id, source_id: @source_id)
return if contact_inbox.blank?
contact_inbox.update!(source_id: new_source_id)
end
def new_source_id
if @inbox.whatsapp? || @inbox.sms? || @inbox.twilio?
"whatsapp:#{@source_id}#{rand(100)}"
else
"#{rand(10)}#{@source_id}"
end
end
def allowed_channels?
@inbox.email? || @inbox.sms? || @inbox.twilio? || @inbox.whatsapp?
end end
end end
@@ -1,21 +1,17 @@
class Api::V1::Accounts::Contacts::ConversationsController < Api::V1::Accounts::Contacts::BaseController class Api::V1::Accounts::Contacts::ConversationsController < Api::V1::Accounts::Contacts::BaseController
def index def index
# Start with all conversations for this contact @conversations = Current.account.conversations.includes(
conversations = Current.account.conversations.includes(
:assignee, :contact, :inbox, :taggings :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 private
conversations = Conversations::PermissionFilterService.new(
conversations,
Current.user,
Current.account
).perform
# Only allow conversations from inboxes the user has access to def inbox_ids
inbox_ids = Current.user.assigned_inboxes.pluck(:id) if Current.user.administrator? || Current.user.agent?
conversations = conversations.where(inbox_id: inbox_ids) Current.user.assigned_inboxes.pluck(:id)
else
@conversations = conversations.order(last_activity_at: :desc).limit(20) []
end
end end
end end
@@ -48,7 +48,7 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
end end
def filter 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 = result[:conversations]
@conversations_count = result[:count] @conversations_count = result[:count]
rescue CustomExceptions::CustomFilter::InvalidAttribute, 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
@@ -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 -1
View File
@@ -15,7 +15,7 @@ class DashboardController < ActionController::Base
private private
def ensure_html_format def ensure_html_format
head :not_acceptable unless request.format.html? head :not_acceptable if request.format.json?
end end
def set_global_config def set_global_config
@@ -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'] ['MAILER_INBOUND_EMAIL_DOMAIN']
when 'linear' when 'linear'
%w[LINEAR_CLIENT_ID LINEAR_CLIENT_SECRET] %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 else
%w[ENABLE_ACCOUNT_SIGNUP FIREBASE_PROJECT_ID FIREBASE_CREDENTIALS] %w[ENABLE_ACCOUNT_SIGNUP FIREBASE_PROJECT_ID FIREBASE_CREDENTIALS]
end end
@@ -15,9 +15,6 @@ class Webhooks::InstagramController < ActionController::API
private private
def valid_token?(token) def valid_token?(token)
# Validates against both IG_VERIFY_TOKEN (Instagram channel via Facebook page) and token == GlobalConfigService.load('IG_VERIFY_TOKEN', '')
# INSTAGRAM_VERIFY_TOKEN (Instagram channel via direct Instagram login)
token == GlobalConfigService.load('IG_VERIFY_TOKEN', '') ||
token == GlobalConfigService.load('INSTAGRAM_VERIFY_TOKEN', '')
end end
end end
+1 -2
View File
@@ -81,8 +81,7 @@ class AccountDashboard < Administrate::BaseDashboard
COLLECTION_FILTERS = { COLLECTION_FILTERS = {
active: ->(resources) { resources.where(status: :active) }, active: ->(resources) { resources.where(status: :active) },
suspended: ->(resources) { resources.where(status: :suspended) }, suspended: ->(resources) { resources.where(status: :suspended) },
recent: ->(resources) { resources.where('created_at > ?', 30.days.ago) }, recent: ->(resources) { resources.where('created_at > ?', 30.days.ago) }
marked_for_deletion: ->(resources) { resources.where("custom_attributes->>'marked_for_deletion_at' IS NOT NULL") }
}.freeze }.freeze
# Overwrite this method to customize how accounts are displayed # Overwrite this method to customize how accounts are displayed
+1 -13
View File
@@ -32,7 +32,6 @@ class ConversationFinder
def initialize(current_user, params) def initialize(current_user, params)
@current_user = current_user @current_user = current_user
@current_account = current_user.account @current_account = current_user.account
@is_admin = current_account.account_users.find_by(user_id: current_user.id)&.administrator?
@params = params @params = params
end end
@@ -86,19 +85,8 @@ class ConversationFinder
@team = current_account.teams.find(params[:team_id]) if params[:team_id] @team = current_account.teams.find(params[:team_id]) if params[:team_id]
end 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 def find_all_conversations
find_conversation_by_inbox @conversations = current_account.conversations.where(inbox_id: @inbox_ids)
# Apply permission-based filtering
@conversations = Conversations::PermissionFilterService.new(
@conversations,
current_user,
current_account
).perform
filter_by_conversation_type if params[:conversation_type] filter_by_conversation_type if params[:conversation_type]
@conversations @conversations
end end
-4
View File
@@ -18,8 +18,4 @@ module BillingHelper
def non_web_inboxes(account) def non_web_inboxes(account)
account.inboxes.where.not(channel_type: Channel::WebWidget.to_s).count account.inboxes.where.not(channel_type: Channel::WebWidget.to_s).count
end end
def agents(account)
account.users.count
end
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 LoadingState from './components/widgets/LoadingState.vue';
import NetworkNotification from './components/NetworkNotification.vue'; import NetworkNotification from './components/NetworkNotification.vue';
import UpdateBanner from './components/app/UpdateBanner.vue'; import UpdateBanner from './components/app/UpdateBanner.vue';
import UpgradeBanner from './components/app/UpgradeBanner.vue';
import PaymentPendingBanner from './components/app/PaymentPendingBanner.vue'; import PaymentPendingBanner from './components/app/PaymentPendingBanner.vue';
import PendingEmailVerificationBanner from './components/app/PendingEmailVerificationBanner.vue'; import PendingEmailVerificationBanner from './components/app/PendingEmailVerificationBanner.vue';
import vueActionCable from './helper/actionCable'; import vueActionCable from './helper/actionCable';
@@ -30,6 +31,7 @@ export default {
UpdateBanner, UpdateBanner,
PaymentPendingBanner, PaymentPendingBanner,
WootSnackbarBox, WootSnackbarBox,
UpgradeBanner,
PendingEmailVerificationBanner, PendingEmailVerificationBanner,
}, },
setup() { setup() {
@@ -144,6 +146,7 @@ export default {
<template v-if="currentAccountId"> <template v-if="currentAccountId">
<PendingEmailVerificationBanner v-if="hideOnOnboardingView" /> <PendingEmailVerificationBanner v-if="hideOnOnboardingView" />
<PaymentPendingBanner v-if="hideOnOnboardingView" /> <PaymentPendingBanner v-if="hideOnOnboardingView" />
<UpgradeBanner />
</template> </template>
<router-view v-slot="{ Component }"> <router-view v-slot="{ Component }">
<transition name="fade" mode="out-in"> <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() { getLimits() {
return axios.get(`${this.url}limits`); return axios.get(`${this.url}limits`);
} }
toggleDeletion(action) {
return axios.post(`${this.url}toggle_deletion`, {
action_type: action,
});
}
} }
export default new EnterpriseAccountAPI(); export default new EnterpriseAccountAPI();
@@ -10,7 +10,6 @@ describe('#enterpriseAccountAPI', () => {
expect(accountAPI).toHaveProperty('update'); expect(accountAPI).toHaveProperty('update');
expect(accountAPI).toHaveProperty('delete'); expect(accountAPI).toHaveProperty('delete');
expect(accountAPI).toHaveProperty('checkout'); expect(accountAPI).toHaveProperty('checkout');
expect(accountAPI).toHaveProperty('toggleDeletion');
}); });
describe('API calls', () => { describe('API calls', () => {
@@ -43,21 +42,5 @@ describe('#enterpriseAccountAPI', () => {
'/enterprise/api/v1/subscription' '/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' }
);
});
}); });
}); });
@@ -4,6 +4,7 @@
@apply inline-block h-6 py-0 px-6 relative align-middle w-6; @apply inline-block h-6 py-0 px-6 relative align-middle w-6;
&.message { &.message {
@include normal-shadow;
@apply bg-white dark:bg-slate-800 rounded-full left-0 my-3 mx-auto p-4 top-0; @apply bg-white dark:bg-slate-800 rounded-full left-0 my-3 mx-auto p-4 top-0;
&::before { &::before {
@@ -1,7 +1,79 @@
@import 'dashboard/assets/scss/variables'; @import 'dashboard/assets/scss/variables';
@import 'widget/assets/scss/mixins';
$spinner-before-border-color: rgba(255, 255, 255, 0.7); $spinner-before-border-color: rgba(255, 255, 255, 0.7);
//borders
@mixin border-nil() {
border-color: transparent;
border: 0;
}
@mixin thin-border($color) {
border: 1px solid $color;
}
@mixin custom-border-bottom($size, $color) {
border-bottom: $size solid $color;
}
@mixin custom-border-top($size, $color) {
border-top: $size solid $color;
}
@mixin border-normal() {
@apply border border-slate-50 dark:border-slate-700;
}
@mixin border-normal-left() {
@apply border-l border-slate-50 dark:border-slate-700;
}
@mixin border-normal-top() {
@apply border-t border-slate-50 dark:border-slate-700;
}
@mixin border-normal-right() {
@apply border-r border-slate-50 dark:border-slate-700;
}
@mixin border-normal-bottom() {
@apply border-b border-slate-50 dark:border-slate-700;
}
@mixin border-light() {
@apply border border-slate-25 dark:border-slate-700;
}
@mixin border-light-left() {
@apply border-l border-slate-25 dark:border-slate-700;
}
@mixin border-light-top() {
@apply border-t border-slate-25 dark:border-slate-700;
}
@mixin border-light-right() {
@apply border-r border-slate-25 dark:border-slate-700;
}
@mixin border-light-bottom() {
@apply border-b border-slate-25 dark:border-slate-700;
}
// background
@mixin background-gray() {
background: $color-background;
}
@mixin background-light() {
@apply bg-slate-50 dark:bg-slate-800;
}
@mixin background-white() {
@apply bg-white dark:bg-slate-900;
}
// input form // input form
@mixin ghost-input() { @mixin ghost-input() {
box-shadow: none; box-shadow: none;
@@ -15,6 +87,65 @@ $spinner-before-border-color: rgba(255, 255, 255, 0.7);
} }
} }
// flex-layout
@mixin space-between() {
display: flex;
justify-content: space-between;
}
@mixin space-between-column() {
@include space-between;
flex-direction: column;
}
@mixin space-between-row() {
@include space-between;
flex-direction: row;
}
@mixin flex-shrink() {
flex: 0 0 auto;
max-width: 100%;
}
@mixin flex-weight($value) {
// Grab flex-grow for older browsers.
$flex-grow: nth($value, 1);
// 2009
@include prefixer(box-flex, $flex-grow, webkit moz spec);
// 2011 (IE 10), 2012
@include prefixer(flex, $value, webkit moz ms spec);
}
// full height
@mixin full-height() {
height: 100%;
}
@mixin round-corner() {
border-radius: 1000px;
}
@mixin scroll-on-hover() {
overflow: hidden;
&:hover {
overflow-y: auto;
}
}
@mixin horizontal-scroll() {
overflow-y: auto;
}
@mixin elegant-card() {
@include normal-shadow;
border-radius: $space-small;
}
@mixin color-spinner() { @mixin color-spinner() {
@keyframes spinner { @keyframes spinner {
to { to {
@@ -99,3 +230,17 @@ $spinner-before-border-color: rgba(255, 255, 255, 0.7);
border-left: $size solid transparent; border-left: $size solid transparent;
} }
} }
@mixin text-ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
@mixin three-column-grid($column-one-width: 16rem,
$column-three-width: 16rem) {
width: 100%;
height: 100%;
display: grid;
grid-template-columns: minmax($column-one-width, 6fr) 10fr minmax($column-three-width, 6fr);
}
@@ -81,6 +81,28 @@
margin-left: var(--space-small); 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 // 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 switch
.toggle-button { .toggle-button {
&.small { &.small {
@@ -177,6 +264,11 @@
} }
} }
// Widget builder
.widget-builder-container .widget-preview {
direction: initial;
}
// Modal // Modal
.modal-container { .modal-container {
text-align: right; text-align: right;
@@ -190,6 +282,7 @@
} }
// Other changes // Other changes
.colorpicker--chrome { .colorpicker--chrome {
direction: initial; direction: initial;
} }
@@ -198,6 +291,14 @@
direction: initial; direction: initial;
} }
.contact--details .contact--bio {
direction: ltr;
}
.merge-contacts .child-contact-wrap {
direction: ltr;
}
.contact--form .input-group { .contact--form .input-group {
direction: initial; direction: initial;
} }
@@ -29,6 +29,7 @@
@import 'rtl'; @import 'rtl';
@import 'widgets/base'; @import 'widgets/base';
@import 'widgets/buttons';
@import 'widgets/conversation-view'; @import 'widgets/conversation-view';
@import 'widgets/tabs'; @import 'widgets/tabs';
@import 'widgets/woot-tables'; @import 'widgets/woot-tables';
@@ -40,12 +40,6 @@ dl:not(.reset-base) {
@apply mb-0; @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 // Form elements
// ------------------------- // -------------------------
label { 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;
}
}
}
@@ -55,13 +55,13 @@ defineExpose({ dialogRef, contactsFormRef, onSuccess });
@click="closeDialog" @click="closeDialog"
/> />
<Button <Button
type="submit"
:label=" :label="
t('CONTACTS_LAYOUT.HEADER.ACTIONS.CONTACT_CREATION.SAVE_CONTACT') t('CONTACTS_LAYOUT.HEADER.ACTIONS.CONTACT_CREATION.SAVE_CONTACT')
" "
color="blue" color="blue"
:disabled="contactsFormRef?.isFormInvalid" :disabled="contactsFormRef?.isFormInvalid"
:is-loading="isCreatingContact" :is-loading="isCreatingContact"
@click="handleDialogConfirm"
/> />
</div> </div>
</template> </template>
@@ -1,5 +1,3 @@
<!-- DEPRECIATED -->
<!-- TODO: Replace this banner component with NextBanner "app/javascript/dashboard/components-next/banner/Banner.vue" -->
<script setup> <script setup>
import { computed } from 'vue'; import { computed } from 'vue';
@@ -190,7 +190,7 @@ const STYLE_CONFIG = {
const variantClasses = computed(() => { const variantClasses = computed(() => {
const variantMap = { const variantMap = {
ghost: `${STYLE_CONFIG.colors[computedColor.value].ghost}`, ghost: `${STYLE_CONFIG.colors[computedColor.value].ghost}`,
link: `${STYLE_CONFIG.colors[computedColor.value].link} p-0 font-medium underline-offset-2`, link: `${STYLE_CONFIG.colors[computedColor.value].link} p-0 font-medium underline-offset-4`,
outline: STYLE_CONFIG.colors[computedColor.value].outline, outline: STYLE_CONFIG.colors[computedColor.value].outline,
faded: STYLE_CONFIG.colors[computedColor.value].faded, faded: STYLE_CONFIG.colors[computedColor.value].faded,
solid: STYLE_CONFIG.colors[computedColor.value].solid, solid: STYLE_CONFIG.colors[computedColor.value].solid,
@@ -103,7 +103,7 @@ export default {
{{ $t('FILTER.CUSTOM_VIEWS.ADD.TITLE') }} {{ $t('FILTER.CUSTOM_VIEWS.ADD.TITLE') }}
</h3> </h3>
<form class="w-full grid gap-6" @submit.prevent="saveCustomViews"> <form class="w-full grid gap-6" @submit.prevent="saveCustomViews">
<label :class="{ error: v$.name.$error }"> <div>
<input <input
v-model="name" 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" 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') }} {{ $t('FILTER.CUSTOM_VIEWS.ADD.ERROR_MESSAGE') }}
</span> </span>
</label> </div>
<div class="flex flex-row justify-end w-full gap-2"> <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"> <NextButton faded slate sm @click.prevent="onClose">
{{ $t('FILTER.CUSTOM_VIEWS.ADD.CANCEL_BUTTON') }} {{ $t('FILTER.CUSTOM_VIEWS.ADD.CANCEL_BUTTON') }}
</NextButton> </NextButton>
<NextButton solid blue sm :disabled="isButtonDisabled">
{{ $t('FILTER.CUSTOM_VIEWS.ADD.SAVE_BUTTON') }}
</NextButton>
</div> </div>
</form> </form>
</div> </div>
@@ -26,12 +26,6 @@ const showAccountSwitcher = computed(
() => userAccounts.value.length > 1 && currentAccount.value.name () => 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 onChangeAccount = newId => {
const accountUrl = `/app/accounts/${newId}/dashboard`; const accountUrl = `/app/accounts/${newId}/dashboard`;
window.location.href = accountUrl; window.location.href = accountUrl;
@@ -76,7 +70,7 @@ const emitNewAccount = () => {
<DropdownBody v-if="showAccountSwitcher" class="min-w-80 z-50"> <DropdownBody v-if="showAccountSwitcher" class="min-w-80 z-50">
<DropdownSection :title="t('SIDEBAR_ITEMS.SWITCH_ACCOUNT')"> <DropdownSection :title="t('SIDEBAR_ITEMS.SWITCH_ACCOUNT')">
<DropdownItem <DropdownItem
v-for="account in sortedCurrentUserAccounts" v-for="account in currentUser.accounts"
:id="`account-${account.id}`" :id="`account-${account.id}`"
:key="account.id" :key="account.id"
class="cursor-pointer" class="cursor-pointer"
@@ -35,7 +35,7 @@ const onToggle = () => {
<template> <template>
<div class="text-sm"> <div class="text-sm">
<button <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 }" :class="{ 'rounded-bl-none rounded-br-none': isOpen }"
@click.stop="onToggle" @click.stop="onToggle"
> >
@@ -55,7 +55,7 @@ const onToggle = () => {
</button> </button>
<div <div
v-if="isOpen" 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'" :class="compact ? 'p-0' : 'px-2 py-4'"
> >
<slot /> <slot />
@@ -61,7 +61,6 @@ import {
getUserPermissions, getUserPermissions,
filterItemsByPermission, filterItemsByPermission,
} from 'dashboard/helper/permissionsHelper.js'; } from 'dashboard/helper/permissionsHelper.js';
import { matchesFilters } from '../store/modules/conversations/helpers/filterHelpers';
import { CONVERSATION_EVENTS } from '../helper/AnalyticsHelper/events'; import { CONVERSATION_EVENTS } from '../helper/AnalyticsHelper/events';
import { ASSIGNEE_TYPE_TAB_PERMISSIONS } from 'dashboard/constants/permissions.js'; import { ASSIGNEE_TYPE_TAB_PERMISSIONS } from 'dashboard/constants/permissions.js';
@@ -106,7 +105,7 @@ const advancedFilterTypes = ref(
); );
const currentUser = useMapGetter('getCurrentUser'); const currentUser = useMapGetter('getCurrentUser');
const chatLists = useMapGetter('getFilteredConversations'); const chatLists = useMapGetter('getAllConversations');
const mineChatsList = useMapGetter('getMineChats'); const mineChatsList = useMapGetter('getMineChats');
const allChatList = useMapGetter('getAllStatusChats'); const allChatList = useMapGetter('getAllStatusChats');
const unAssignedChatsList = useMapGetter('getUnAssignedChats'); const unAssignedChatsList = useMapGetter('getUnAssignedChats');
@@ -325,14 +324,6 @@ const conversationList = computed(() => {
} else { } else {
localConversationList = [...chatLists.value]; localConversationList = [...chatLists.value];
} }
if (activeFolder.value) {
const { payload } = activeFolder.value.query;
localConversationList = localConversationList.filter(conversation => {
return matchesFilters(conversation, payload);
});
}
return localConversationList; return localConversationList;
}); });
@@ -469,12 +460,6 @@ function setParamsForEditFolderModal() {
campaigns: campaigns.value, campaigns: campaigns.value,
languages: languages, languages: languages,
countries: countries, countries: countries,
priority: [
{ id: 'low', name: t('CONVERSATION.PRIORITY.OPTIONS.LOW') },
{ id: 'medium', name: t('CONVERSATION.PRIORITY.OPTIONS.MEDIUM') },
{ id: 'high', name: t('CONVERSATION.PRIORITY.OPTIONS.HIGH') },
{ id: 'urgent', name: t('CONVERSATION.PRIORITY.OPTIONS.URGENT') },
],
filterTypes: advancedFilterTypes.value, filterTypes: advancedFilterTypes.value,
allCustomAttributes: conversationCustomAttributes.value, allCustomAttributes: conversationCustomAttributes.value,
}; };
@@ -9,15 +9,12 @@ import { getRegexp } from 'shared/helpers/Validators';
import { useVuelidate } from '@vuelidate/core'; import { useVuelidate } from '@vuelidate/core';
import { emitter } from 'shared/helpers/mitt'; import { emitter } from 'shared/helpers/mitt';
import NextButton from 'dashboard/components-next/button/Button.vue';
const DATE_FORMAT = 'yyyy-MM-dd'; const DATE_FORMAT = 'yyyy-MM-dd';
export default { export default {
components: { components: {
MultiselectDropdown, MultiselectDropdown,
HelperTextPopup, HelperTextPopup,
NextButton,
}, },
props: { props: {
label: { type: String, required: true }, label: { type: String, required: true },
@@ -222,13 +219,14 @@ export default {
class="mt-0.5" class="mt-0.5"
/> />
</span> </span>
<NextButton <woot-button
v-if="showActions && value" v-if="showActions && value"
v-tooltip.left="$t('CUSTOM_ATTRIBUTES.ACTIONS.DELETE')" v-tooltip.left="$t('CUSTOM_ATTRIBUTES.ACTIONS.DELETE')"
slate variant="link"
sm size="medium"
link color-scheme="secondary"
icon="i-lucide-trash-2" icon="delete"
class-names="flex justify-end !w-fit"
@click="onDelete" @click="onDelete"
/> />
</div> </div>
@@ -248,10 +246,10 @@ export default {
@keyup.enter="onUpdate" @keyup.enter="onUpdate"
/> />
<div> <div>
<NextButton <woot-button
sm size="small"
icon="i-lucide-check" icon="checkmark"
class="ltr:rounded-l-none rtl:rounded-r-none h-[34px]" class="ltr:rounded-l-none rtl:rounded-r-none"
@click="onUpdate" @click="onUpdate"
/> />
</div> </div>
@@ -283,27 +281,25 @@ export default {
> >
{{ displayValue || '---' }} {{ displayValue || '---' }}
</p> </p>
<div <div class="flex max-w-[2rem] gap-1 ml-1 rtl:mr-1 rtl:ml-0">
class="flex items-center max-w-[2rem] gap-1 ml-1 rtl:mr-1 rtl:ml-0" <woot-button
>
<NextButton
v-if="showActions && value" v-if="showActions && value"
v-tooltip="$t('CUSTOM_ATTRIBUTES.ACTIONS.COPY')" v-tooltip="$t('CUSTOM_ATTRIBUTES.ACTIONS.COPY')"
xs variant="link"
slate size="small"
ghost color-scheme="secondary"
icon="i-lucide-clipboard" icon="clipboard"
class="hidden group-hover:flex flex-shrink-0" class-names="hidden group-hover:flex !w-6 flex-shrink-0"
@click="onCopy" @click="onCopy"
/> />
<NextButton <woot-button
v-if="showActions" v-if="showActions"
v-tooltip.right="$t('CUSTOM_ATTRIBUTES.ACTIONS.EDIT')" v-tooltip.right="$t('CUSTOM_ATTRIBUTES.ACTIONS.EDIT')"
xs variant="link"
slate size="small"
ghost color-scheme="secondary"
icon="i-lucide-pen" icon="edit"
class="hidden group-hover:flex flex-shrink-0" class-names="hidden group-hover:flex !w-6 flex-shrink-0"
@click="onEdit" @click="onEdit"
/> />
</div> </div>
@@ -1,11 +1,9 @@
<script> <script>
import DatePicker from 'vue-datepicker-next'; import DatePicker from 'vue-datepicker-next';
import NextButton from 'dashboard/components-next/button/Button.vue';
export default { export default {
components: { components: {
DatePicker, DatePicker,
NextButton,
}, },
emits: ['close', 'chooseTime'], emits: ['close', 'chooseTime'],
@@ -54,23 +52,18 @@ export default {
v-model:value="snoozeTime" v-model:value="snoozeTime"
type="datetime" type="datetime"
inline inline
input-class="mx-input " input-class="mx-input reset-base"
:lang="lang" :lang="lang"
:disabled-date="disabledDate" :disabled-date="disabledDate"
:disabled-time="disabledTime" :disabled-time="disabledTime"
/> />
<div class="flex flex-row justify-end w-full gap-2 px-0 py-2"> <div class="flex flex-row justify-end w-full gap-2 px-0 py-2">
<NextButton <woot-button variant="clear" @click.prevent="onClose">
faded {{ $t('CONVERSATION.CUSTOM_SNOOZE.CANCEL') }}
slate </woot-button>
type="reset" <woot-button>
:label="$t('CONVERSATION.CUSTOM_SNOOZE.CANCEL')" {{ $t('CONVERSATION.CUSTOM_SNOOZE.APPLY') }}
@click.prevent="onClose" </woot-button>
/>
<NextButton
type="submit"
:label="$t('CONVERSATION.CUSTOM_SNOOZE.APPLY')"
/>
</div> </div>
</form> </form>
</div> </div>
@@ -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>
@@ -2,7 +2,6 @@
// [TODO] Use Teleport to move the modal to the end of the body // [TODO] Use Teleport to move the modal to the end of the body
import { ref, computed, defineEmits, onMounted } from 'vue'; import { ref, computed, defineEmits, onMounted } from 'vue';
import { useEventListener } from '@vueuse/core'; import { useEventListener } from '@vueuse/core';
import Button from 'dashboard/components-next/button/Button.vue';
const { modalType, closeOnBackdropClick, onClose } = defineProps({ const { modalType, closeOnBackdropClick, onClose } = defineProps({
closeOnBackdropClick: { type: Boolean, default: true }, closeOnBackdropClick: { type: Boolean, default: true },
@@ -86,11 +85,11 @@ onMounted(() => {
@mouse.stop @mouse.stop
@mousedown="event => event.stopPropagation()" @mousedown="event => event.stopPropagation()"
> >
<Button <woot-button
v-if="showCloseButton" v-if="showCloseButton"
ghost color-scheme="secondary"
slate icon="dismiss"
icon="i-lucide-x" variant="clear"
class="absolute z-10 ltr:right-2 rtl:left-2 top-2" class="absolute z-10 ltr:right-2 rtl:left-2 top-2"
@click="close" @click="close"
/> />
@@ -11,8 +11,6 @@ import {
} from 'dashboard/helper/routeHelpers'; } from 'dashboard/helper/routeHelpers';
import { useEventListener } from '@vueuse/core'; import { useEventListener } from '@vueuse/core';
import Button from 'dashboard/components-next/button/Button.vue';
const { t } = useI18n(); const { t } = useI18n();
const route = useRoute(); const route = useRoute();
@@ -109,31 +107,34 @@ onBeforeUnmount(() => {
<template> <template>
<transition name="network-notification-fade" tag="div"> <transition name="network-notification-fade" tag="div">
<div v-show="showNotification" class="fixed z-50 top-2 left-2 group"> <div v-show="showNotification" class="fixed z-50 top-4 left-2 group">
<div <div
class="relative flex items-center justify-between w-full px-2 py-1 bg-n-amber-4 dark:bg-n-amber-8 rounded-lg shadow-lg" class="relative flex items-center justify-between w-full px-2 py-1 bg-yellow-200 rounded-lg shadow-lg dark:bg-yellow-700"
> >
<fluent-icon :icon="iconName" class="text-n-amber-12" size="18" /> <fluent-icon
<span class="px-2 text-xs font-medium tracking-wide text-n-amber-12"> :icon="iconName"
class="text-yellow-700/50 dark:text-yellow-50"
size="18"
/>
<span
class="px-2 text-xs font-medium tracking-wide text-yellow-700/70 dark:text-yellow-50"
>
{{ bannerText }} {{ bannerText }}
</span> </span>
<Button <woot-button
v-if="canRefresh" v-if="canRefresh"
ghost
sm
amber
icon="i-lucide-refresh-ccw"
:title="$t('NETWORK.BUTTON.REFRESH')" :title="$t('NETWORK.BUTTON.REFRESH')"
class="!text-n-amber-12 dark:!text-n-amber-9" variant="clear"
size="small"
color-scheme="warning"
icon="arrow-clockwise"
@click="refreshPage" @click="refreshPage"
/> />
<woot-button
<Button variant="clear"
ghost size="small"
sm color-scheme="warning"
amber icon="dismiss"
icon="i-lucide-x"
class="!text-n-amber-12 dark:!text-n-amber-9"
@click="closeNotification" @click="closeNotification"
/> />
</div> </div>
@@ -3,16 +3,12 @@ import { FEATURE_FLAGS } from 'dashboard/featureFlags';
import { BUS_EVENTS } from 'shared/constants/busEvents'; import { BUS_EVENTS } from 'shared/constants/busEvents';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { emitter } from 'shared/helpers/mitt'; import { emitter } from 'shared/helpers/mitt';
import NextButton from 'dashboard/components-next/button/Button.vue';
export default { export default {
components: {
NextButton,
},
props: { props: {
size: { size: {
type: String, type: String,
default: 'sm', default: 'small',
}, },
}, },
computed: { computed: {
@@ -37,13 +33,13 @@ export default {
<!-- eslint-disable-next-line vue/no-root-v-if --> <!-- eslint-disable-next-line vue/no-root-v-if -->
<template> <template>
<NextButton <woot-button
v-if="!hasNextSidebar" v-if="!hasNextSidebar"
ghost
slate
:size="size" :size="size"
icon="i-lucide-menu" variant="clear"
class="-ml-3" color-scheme="secondary"
class="-ml-3 text-black-900 dark:text-slate-300"
icon="list"
@click="onMenuItemClick" @click="onMenuItemClick"
/> />
</template> </template>
@@ -35,7 +35,7 @@ export default {
color-scheme="alert" color-scheme="alert"
:banner-message="bannerMessage" :banner-message="bannerMessage"
:action-button-label="actionButtonMessage" :action-button-label="actionButtonMessage"
action-button-icon="i-lucide-mail" action-button-icon="mail"
has-action-button has-action-button
@primary-action="resendVerificationEmail" @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> <template>
<div class="relative flex items-center justify-end resolve-actions"> <div class="relative flex items-center justify-end resolve-actions">
<div <div
class="rounded-lg shadow outline-1 outline" class="rounded-lg shadow button-group outline-1 outline"
:class="!showOpenButton ? 'outline-n-container' : 'outline-transparent'" :class="!showOpenButton ? 'outline-n-container' : 'outline-transparent'"
> >
<Button <Button
@@ -1,6 +1,7 @@
// [NOTE][DEPRECATED] This method is to be deprecated, please do not add new components to this file. // [NOTE][DEPRECATED] This method is to be deprecated, please do not add new components to this file.
/* eslint no-plusplus: 0 */ /* eslint no-plusplus: 0 */
import AvatarUploader from './widgets/forms/AvatarUploader.vue'; import AvatarUploader from './widgets/forms/AvatarUploader.vue';
import Button from './ui/WootButton.vue';
import Code from './Code.vue'; import Code from './Code.vue';
import ColorPicker from './widgets/ColorPicker.vue'; import ColorPicker from './widgets/ColorPicker.vue';
import ConfirmDeleteModal from './widgets/modal/ConfirmDeleteModal.vue'; import ConfirmDeleteModal from './widgets/modal/ConfirmDeleteModal.vue';
@@ -17,6 +18,7 @@ import ModalHeader from './ModalHeader.vue';
import Modal from './Modal.vue'; import Modal from './Modal.vue';
import SidemenuIcon from './SidemenuIcon.vue'; import SidemenuIcon from './SidemenuIcon.vue';
import Spinner from 'shared/components/Spinner.vue'; import Spinner from 'shared/components/Spinner.vue';
import SubmitButton from './buttons/FormSubmitButton.vue';
import Tabs from './ui/Tabs/Tabs.vue'; import Tabs from './ui/Tabs/Tabs.vue';
import TabsItem from './ui/Tabs/TabsItem.vue'; import TabsItem from './ui/Tabs/TabsItem.vue';
import Thumbnail from './widgets/Thumbnail.vue'; import Thumbnail from './widgets/Thumbnail.vue';
@@ -24,6 +26,7 @@ import DatePicker from './ui/DatePicker/DatePicker.vue';
const WootUIKit = { const WootUIKit = {
AvatarUploader, AvatarUploader,
Button,
Code, Code,
ColorPicker, ColorPicker,
ConfirmDeleteModal, ConfirmDeleteModal,
@@ -40,6 +43,7 @@ const WootUIKit = {
ModalHeader, ModalHeader,
SidemenuIcon, SidemenuIcon,
Spinner, Spinner,
SubmitButton,
Tabs, Tabs,
TabsItem, TabsItem,
Thumbnail, Thumbnail,
@@ -7,7 +7,6 @@ import WootDropdownHeader from 'shared/components/ui/dropdown/DropdownHeader.vue
import WootDropdownDivider from 'shared/components/ui/dropdown/DropdownDivider.vue'; import WootDropdownDivider from 'shared/components/ui/dropdown/DropdownDivider.vue';
import AvailabilityStatusBadge from '../widgets/conversation/AvailabilityStatusBadge.vue'; import AvailabilityStatusBadge from '../widgets/conversation/AvailabilityStatusBadge.vue';
import wootConstants from 'dashboard/constants/globals'; import wootConstants from 'dashboard/constants/globals';
import NextButton from 'dashboard/components-next/button/Button.vue';
const { AVAILABILITY_STATUS_KEYS } = wootConstants; const { AVAILABILITY_STATUS_KEYS } = wootConstants;
@@ -18,7 +17,6 @@ export default {
WootDropdownMenu, WootDropdownMenu,
WootDropdownItem, WootDropdownItem,
AvailabilityStatusBadge, AvailabilityStatusBadge,
NextButton,
}, },
data() { data() {
return { return {
@@ -103,21 +101,19 @@ export default {
:key="status.value" :key="status.value"
class="flex items-baseline" class="flex items-baseline"
> >
<NextButton <woot-button
sm size="small"
:color="status.disabled ? 'blue' : 'slate'" :color-scheme="status.disabled ? '' : 'secondary'"
:variant="status.disabled ? 'faded' : 'ghost'" :variant="status.disabled ? 'smooth' : 'clear'"
class="status-change--dropdown-button !w-full !justify-start" class="status-change--dropdown-button"
@click="changeAvailabilityStatus(status.value)" @click="changeAvailabilityStatus(status.value)"
> >
<AvailabilityStatusBadge :status="status.value" /> <AvailabilityStatusBadge :status="status.value" />
<span class="min-w-0 truncate font-medium text-xs"> {{ status.label }}
{{ status.label }} </woot-button>
</span>
</NextButton>
</WootDropdownItem> </WootDropdownItem>
<WootDropdownDivider /> <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"> <div class="flex items-center">
<fluent-icon <fluent-icon
v-tooltip.right-start="$t('SIDEBAR.SET_AUTO_OFFLINE.INFO_TEXT')" v-tooltip.right-start="$t('SIDEBAR.SET_AUTO_OFFLINE.INFO_TEXT')"
@@ -127,7 +123,7 @@ export default {
/> />
<span <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') }} {{ $t('SIDEBAR.SET_AUTO_OFFLINE.TEXT') }}
</span> </span>
@@ -1,11 +1,7 @@
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import NextButton from 'dashboard/components-next/button/Button.vue';
export default { export default {
components: {
NextButton,
},
emits: ['toggleAccounts'], emits: ['toggleAccounts'],
data() { data() {
return { showSwitchButton: false }; 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" 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"> <div class="mx-2 my-0">
<NextButton <woot-button
ghost variant="clear"
xs size="tiny"
icon="i-lucide-arrow-right-left" icon="arrow-swap"
:label="$t('SIDEBAR.SWITCH')"
@click="$emit('toggleAccounts')" @click="$emit('toggleAccounts')"
/> >
{{ $t('SIDEBAR.SWITCH') }}
</woot-button>
</div> </div>
</div> </div>
</transition> </transition>
@@ -3,12 +3,8 @@ import { required, minLength } from '@vuelidate/validators';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { useVuelidate } from '@vuelidate/core'; import { useVuelidate } from '@vuelidate/core';
import { useAlert } from 'dashboard/composables'; import { useAlert } from 'dashboard/composables';
import NextButton from 'dashboard/components-next/button/Button.vue';
export default { export default {
components: {
NextButton,
},
props: { props: {
show: { show: {
type: Boolean, type: Boolean,
@@ -90,24 +86,19 @@ export default {
/> />
</label> </label>
</div> </div>
<div class="w-full flex justify-end gap-2 items-center"> <div class="w-full">
<NextButton <div class="w-full">
faded <woot-submit-button
slate :disabled="
type="reset" v$.accountName.$invalid ||
:label="$t('CREATE_ACCOUNT.FORM.CANCEL')" v$.accountName.$invalid ||
@click.prevent="() => $emit('closeAccountCreateModal')" uiFlags.isCreating
/> "
<NextButton :button-text="$t('CREATE_ACCOUNT.FORM.SUBMIT')"
type="submit" :loading="uiFlags.isCreating"
:label="$t('CREATE_ACCOUNT.FORM.SUBMIT')" button-class="large expanded"
:is-loading="uiFlags.isCreating" />
:disabled=" </div>
v$.accountName.$invalid ||
v$.accountName.$invalid ||
uiFlags.isCreating
"
/>
</div> </div>
</form> </form>
</div> </div>
@@ -1,12 +1,10 @@
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import Thumbnail from '../../widgets/Thumbnail.vue'; import Thumbnail from '../../widgets/Thumbnail.vue';
import NextButton from 'dashboard/components-next/button/Button.vue';
export default { export default {
components: { components: {
Thumbnail, Thumbnail,
NextButton,
}, },
emits: ['toggleMenu'], emits: ['toggleMenu'],
computed: { computed: {
@@ -27,10 +25,10 @@ export default {
</script> </script>
<template> <template>
<NextButton <woot-button
v-tooltip.right="$t(`SIDEBAR.PROFILE_SETTINGS`)" v-tooltip.right="$t(`SIDEBAR.PROFILE_SETTINGS`)"
link variant="link"
class="rounded-full" class="flex items-center rounded-full"
@click="handleClick" @click="handleClick"
> >
<Thumbnail <Thumbnail
@@ -39,7 +37,6 @@ export default {
:status="statusOfAgent" :status="statusOfAgent"
should-show-status-always should-show-status-always
size="32px" size="32px"
class="flex-shrink-0"
/> />
</NextButton> </woot-button>
</template> </template>
@@ -5,14 +5,12 @@ import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue'; import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
import AvailabilityStatus from 'dashboard/components/layout/AvailabilityStatus.vue'; import AvailabilityStatus from 'dashboard/components/layout/AvailabilityStatus.vue';
import { FEATURE_FLAGS } from '../../../featureFlags'; import { FEATURE_FLAGS } from '../../../featureFlags';
import NextButton from 'dashboard/components-next/button/Button.vue';
export default { export default {
components: { components: {
WootDropdownMenu, WootDropdownMenu,
WootDropdownItem, WootDropdownItem,
AvailabilityStatus, AvailabilityStatus,
NextButton,
}, },
props: { props: {
show: { show: {
@@ -84,46 +82,37 @@ export default {
<AvailabilityStatus /> <AvailabilityStatus />
<WootDropdownMenu> <WootDropdownMenu>
<WootDropdownItem v-if="showChangeAccountOption"> <WootDropdownItem v-if="showChangeAccountOption">
<NextButton <woot-button
ghost variant="clear"
sm color-scheme="secondary"
slate size="small"
icon="i-lucide-arrow-right-left" icon="arrow-swap"
class="!w-full !justify-start"
@click="$emit('toggleAccounts')" @click="$emit('toggleAccounts')"
> >
<span class="min-w-0 truncate font-medium text-xs"> {{ $t('SIDEBAR_ITEMS.CHANGE_ACCOUNTS') }}
{{ $t('SIDEBAR_ITEMS.CHANGE_ACCOUNTS') }} </woot-button>
</span>
</NextButton>
</WootDropdownItem> </WootDropdownItem>
<WootDropdownItem v-if="showChatSupport"> <WootDropdownItem v-if="showChatSupport">
<NextButton <woot-button
ghost variant="clear"
sm color-scheme="secondary"
slate size="small"
icon="i-lucide-message-circle-question" icon="chat-help"
class="!w-full !justify-start"
@click="$emit('showSupportChatWindow')" @click="$emit('showSupportChatWindow')"
> >
<span class="min-w-0 truncate font-medium text-xs"> {{ $t('SIDEBAR_ITEMS.CONTACT_SUPPORT') }}
{{ $t('SIDEBAR_ITEMS.CONTACT_SUPPORT') }} </woot-button>
</span>
</NextButton>
</WootDropdownItem> </WootDropdownItem>
<WootDropdownItem> <WootDropdownItem>
<NextButton <woot-button
ghost variant="clear"
sm color-scheme="secondary"
slate size="small"
icon="i-lucide-keyboard" icon="keyboard"
class="!w-full !justify-start"
@click="handleKeyboardHelpClick" @click="handleKeyboardHelpClick"
> >
<span class="min-w-0 truncate font-medium text-xs"> {{ $t('SIDEBAR_ITEMS.KEYBOARD_SHORTCUTS') }}
{{ $t('SIDEBAR_ITEMS.KEYBOARD_SHORTCUTS') }} </woot-button>
</span>
</NextButton>
</WootDropdownItem> </WootDropdownItem>
<WootDropdownItem> <WootDropdownItem>
<router-link <router-link
@@ -133,70 +122,56 @@ export default {
> >
<a <a
:href="href" :href="href"
class="h-8 bg-white button small clear secondary dark:bg-slate-800"
:class="{ 'is-active': isActive }" :class="{ 'is-active': isActive }"
@click="e => handleProfileSettingClick(e, navigate)" @click="e => handleProfileSettingClick(e, navigate)"
> >
<NextButton <fluent-icon icon="person" size="14" class="icon icon--font" />
ghost <span class="button__content">
sm {{ $t('SIDEBAR_ITEMS.PROFILE_SETTINGS') }}
slate </span>
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>
</a> </a>
</router-link> </router-link>
</WootDropdownItem> </WootDropdownItem>
<WootDropdownItem> <WootDropdownItem>
<NextButton <woot-button
ghost variant="clear"
sm color-scheme="secondary"
slate size="small"
icon="i-lucide-sun-moon" icon="appearance"
class="!w-full !justify-start"
@click="openAppearanceOptions" @click="openAppearanceOptions"
> >
<span class="min-w-0 truncate font-medium text-xs"> {{ $t('SIDEBAR_ITEMS.APPEARANCE') }}
{{ $t('SIDEBAR_ITEMS.APPEARANCE') }} </woot-button>
</span>
</NextButton>
</WootDropdownItem> </WootDropdownItem>
<WootDropdownItem v-if="currentUser.type === 'SuperAdmin'"> <WootDropdownItem v-if="currentUser.type === 'SuperAdmin'">
<a <a
href="/super_admin" href="/super_admin"
class="h-8 bg-white button small clear secondary dark:bg-slate-800"
target="_blank" target="_blank"
rel="noopener nofollow noreferrer" rel="noopener nofollow noreferrer"
@click="$emit('close')" @click="$emit('close')"
> >
<NextButton <fluent-icon
ghost icon="content-settings"
sm size="14"
slate class="icon icon--font"
icon="i-lucide-layout-dashboard" />
class="!w-full !justify-start" <span class="button__content">
> {{ $t('SIDEBAR_ITEMS.SUPER_ADMIN_CONSOLE') }}
<span class="min-w-0 truncate font-medium text-xs"> </span>
{{ $t('SIDEBAR_ITEMS.SUPER_ADMIN_CONSOLE') }}
</span>
</NextButton>
</a> </a>
</WootDropdownItem> </WootDropdownItem>
<WootDropdownItem> <WootDropdownItem>
<NextButton <woot-button
ghost variant="clear"
sm color-scheme="secondary"
slate size="small"
icon="i-lucide-circle-power" icon="power"
class="!w-full !justify-start"
@click="logout" @click="logout"
> >
<span class="min-w-0 truncate font-medium text-xs"> {{ $t('SIDEBAR_ITEMS.LOGOUT') }}
{{ $t('SIDEBAR_ITEMS.LOGOUT') }} </woot-button>
</span>
</NextButton>
</WootDropdownItem> </WootDropdownItem>
</WootDropdownMenu> </WootDropdownMenu>
</div> </div>
@@ -13,10 +13,9 @@ import {
isOnUnattendedView, isOnUnattendedView,
} from '../../../store/modules/conversations/helpers/actionHelpers'; } from '../../../store/modules/conversations/helpers/actionHelpers';
import Policy from '../../policy.vue'; import Policy from '../../policy.vue';
import NextButton from 'dashboard/components-next/button/Button.vue';
export default { export default {
components: { SecondaryChildNavItem, Policy, NextButton }, components: { SecondaryChildNavItem, Policy },
props: { props: {
menuItem: { menuItem: {
type: Object, type: Object,
@@ -206,7 +205,14 @@ export default {
{{ $t(`SIDEBAR.${menuItem.label}`) }} {{ $t(`SIDEBAR.${menuItem.label}`) }}
</span> </span>
<div v-if="menuItem.showNewButton" class="flex items-center"> <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>
</div> </div>
<router-link <router-link
@@ -266,15 +272,16 @@ export default {
> >
<li class="pl-1"> <li class="pl-1">
<a :href="href"> <a :href="href">
<NextButton <woot-button
ghost size="tiny"
xs variant="clear"
slate color-scheme="secondary"
icon="i-lucide-plus" icon="add"
:label="$t(`SIDEBAR.${menuItem.newLinkTag}`)"
:data-testid="menuItem.dataTestid" :data-testid="menuItem.dataTestid"
@click="e => newLinkClick(e, navigate)" @click="e => newLinkClick(e, navigate)"
/> >
{{ $t(`SIDEBAR.${menuItem.newLinkTag}`) }}
</woot-button>
</a> </a>
</li> </li>
</router-link> </router-link>
@@ -41,6 +41,7 @@ describe('AccountSelector', () => {
'fluent-icon': FluentIcon, 'fluent-icon': FluentIcon,
}, },
stubs: { stubs: {
WootButton: { template: '<button />' },
// override global stub // override global stub
WootModalHeader: false, WootModalHeader: false,
}, },
@@ -2,7 +2,7 @@ import { shallowMount } from '@vue/test-utils';
import { createStore } from 'vuex'; import { createStore } from 'vuex';
import AgentDetails from '../AgentDetails.vue'; import AgentDetails from '../AgentDetails.vue';
import Thumbnail from 'dashboard/components/widgets/Thumbnail.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', () => { describe('AgentDetails', () => {
const currentUser = { const currentUser = {
@@ -40,12 +40,12 @@ describe('AgentDetails', () => {
plugins: [store], plugins: [store],
components: { components: {
Thumbnail, Thumbnail,
NextButton, WootButton,
}, },
directives: { directives: {
tooltip: mockTooltipDirective, // Mocking the tooltip directive 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 { mount } from '@vue/test-utils';
import { createStore } from 'vuex'; import { createStore } from 'vuex';
import AvailabilityStatus from '../AvailabilityStatus.vue'; 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 WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue'; import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
import WootDropdownHeader from 'shared/components/ui/dropdown/DropdownHeader.vue'; import WootDropdownHeader from 'shared/components/ui/dropdown/DropdownHeader.vue';
@@ -40,7 +40,7 @@ describe('AvailabilityStatus', () => {
global: { global: {
plugins: [store], plugins: [store],
components: { components: {
NextButton, WootButton,
WootDropdownItem, WootDropdownItem,
WootDropdownMenu, WootDropdownMenu,
WootDropdownHeader, WootDropdownHeader,
@@ -22,7 +22,7 @@ const store = createStore({
describe('SidemenuIcon', () => { describe('SidemenuIcon', () => {
test('matches snapshot', () => { test('matches snapshot', () => {
const wrapper = shallowMount(SidemenuIcon, { const wrapper = shallowMount(SidemenuIcon, {
stubs: { NextButton: { template: '<button><slot /></button>' } }, stubs: { WootButton: { template: '<button><slot /></button>' } },
global: { plugins: [store] }, global: { plugins: [store] },
}); });
expect(wrapper.vm).toBeTruthy(); expect(wrapper.vm).toBeTruthy();
@@ -2,11 +2,11 @@
exports[`SidemenuIcon > matches snapshot 1`] = ` exports[`SidemenuIcon > matches snapshot 1`] = `
<button <button
class="-ml-3" class="-ml-3 text-black-900 dark:text-slate-300"
ghost="" color-scheme="secondary"
icon="i-lucide-menu" icon="list"
size="sm" size="small"
slate="" variant="clear"
> >
@@ -0,0 +1,90 @@
<script>
export default {
props: {
popupMessage: {
type: String,
default: '',
},
routeText: {
type: String,
default: '',
},
hasCloseButton: {
type: Boolean,
default: true,
},
closeButtonText: {
type: String,
default: '',
},
},
emits: ['open', 'close'],
methods: {
onClickOpenPath() {
this.$emit('open');
},
onClickClose(e) {
this.$emit('close', e);
},
},
};
</script>
<template>
<div class="announcement-popup">
<span v-if="popupMessage" class="popup-content">
{{ popupMessage }}
<span v-if="routeText" class="route-url" @click="onClickOpenPath">
{{ routeText }}
</span>
</span>
<div v-if="hasCloseButton" class="popup-close">
<woot-button
v-if="hasCloseButton"
color-scheme="primary"
variant="link"
size="small"
@click="onClickClose"
>
{{ closeButtonText }}
</woot-button>
</div>
</div>
</template>
<style lang="scss">
.announcement-popup {
max-width: 15rem;
min-width: 10rem;
display: flex;
position: absolute;
flex-direction: column;
align-items: flex-start;
height: fit-content;
background: var(--white);
padding: 0 var(--space-normal);
z-index: var(--z-index-much-higher);
box-shadow: var(--b-200) 4px 4px 16px 4px;
border-radius: var(--border-radius-normal);
.popup-content {
font-size: var(--font-size-mini);
color: var(--s-700);
padding: var(--space-one) 0;
.route-url {
font-size: var(--font-size-mini);
color: var(--s-600);
cursor: pointer;
text-decoration: underline;
}
}
.popup-close {
width: 100%;
display: flex;
align-items: center;
padding: var(--space-one) 0;
border-top: 1px solid var(--color-border-light);
}
}
</style>
@@ -1,10 +1,5 @@
<script> <script>
import NextButton from 'dashboard/components-next/button/Button.vue';
export default { export default {
components: {
NextButton,
},
props: { props: {
bannerMessage: { bannerMessage: {
type: String, type: String,
@@ -24,7 +19,7 @@ export default {
}, },
actionButtonVariant: { actionButtonVariant: {
type: String, type: String,
default: 'faded', default: '',
}, },
actionButtonLabel: { actionButtonLabel: {
type: String, type: String,
@@ -32,7 +27,7 @@ export default {
}, },
actionButtonIcon: { actionButtonIcon: {
type: String, type: String,
default: 'i-lucide-arrow-right', default: 'arrow-right',
}, },
colorScheme: { colorScheme: {
type: String, type: String,
@@ -53,18 +48,6 @@ export default {
} }
return classList; 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: { methods: {
onClick(e) { onClick(e) {
@@ -94,23 +77,27 @@ export default {
</a> </a>
</span> </span>
<div class="actions"> <div class="actions">
<NextButton <woot-button
v-if="hasActionButton" v-if="hasActionButton"
xs size="tiny"
:icon="actionButtonIcon" :icon="actionButtonIcon"
:variant="actionButtonVariant" :variant="actionButtonVariant"
:color="getButtonColor" color-scheme="primary"
:label="actionButtonLabel" class-names="banner-action__button"
@click="onClick" @click="onClick"
/> >
<NextButton {{ actionButtonLabel }}
</woot-button>
<woot-button
v-if="hasCloseButton" v-if="hasCloseButton"
xs size="tiny"
icon="i-lucide-circle-x" :color-scheme="colorScheme"
:color="getButtonColor" icon="dismiss-circle"
:label="$t('GENERAL_SETTINGS.DISMISS')" class-names="banner-action__button"
@click="onClickClose" @click="onClickClose"
/> >
{{ $t('GENERAL_SETTINGS.DISMISS') }}
</woot-button>
</div> </div>
</div> </div>
</template> </template>
@@ -119,6 +106,13 @@ export default {
.banner { .banner {
&.primary { &.primary {
@apply bg-woot-500 dark:bg-woot-500; @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 { &.secondary {
@@ -130,6 +124,13 @@ export default {
&.alert { &.alert {
@apply bg-n-ruby-3 text-n-ruby-12; @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 { a {
@apply text-n-ruby-12; @apply text-n-ruby-12;
@@ -145,12 +146,21 @@ export default {
&.gray { &.gray {
@apply text-black-500 dark:text-black-500; @apply text-black-500 dark:text-black-500;
.banner-action__button {
@apply text-white dark:text-white;
}
} }
a { a {
@apply ml-1 underline text-white dark:text-white text-xs; @apply ml-1 underline text-white dark:text-white text-xs;
} }
.banner-action__button {
::v-deep .button__content {
@apply whitespace-nowrap;
}
}
.banner-message { .banner-message {
@apply flex items-center; @apply flex items-center;
} }
@@ -1,16 +1,14 @@
<script setup> <script setup>
import Button from 'dashboard/components-next/button/Button.vue';
defineProps({ defineProps({
buttonText: { buttonText: {
type: String, type: String,
default: '', default: '',
}, },
trailingIcon: { rightIcon: {
type: Boolean, type: String,
default: false, default: '',
}, },
icon: { leftIcon: {
type: String, type: String,
default: '', default: '',
}, },
@@ -18,15 +16,32 @@ defineProps({
</script> </script>
<template> <template>
<Button <button
ghost 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"
slate
sm
class="relative"
:icon="icon"
:trailing-icon="trailingIcon"
> >
<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" /> <slot name="dropdown" />
</Button> </button>
</template> </template>
@@ -8,7 +8,9 @@ defineProps({
</script> </script>
<template> <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 }} {{ message }}
</div> </div>
</template> </template>
@@ -78,7 +78,7 @@ const shouldShowEmptyState = computed(() => {
<template> <template>
<div <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 @click.stop
> >
<slot name="search"> <slot name="search">
@@ -21,7 +21,7 @@ defineProps({
<template> <template>
<button <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"> <div class="inline-flex items-center gap-3 overflow-hidden">
<fluent-icon <fluent-icon
@@ -30,14 +30,16 @@ defineProps({
size="18" size="18"
:style="{ color: iconColor }" :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 }} {{ buttonText }}
</span> </span>
<fluent-icon <fluent-icon
v-if="isActive" v-if="isActive"
icon="checkmark" icon="checkmark"
size="18" size="18"
class="flex-shrink-0 text-n-slate-12" class="flex-shrink-0 text-slate-900 dark:text-slate-50"
/> />
</div> </div>
<slot name="dropdown" /> <slot name="dropdown" />
@@ -8,7 +8,9 @@ defineProps({
</script> </script>
<template> <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 }} {{ message }}
</div> </div>
</template> </template>
@@ -1,7 +1,5 @@
<script setup> <script setup>
import { defineEmits, defineModel } from 'vue'; import { defineEmits, defineModel } from 'vue';
import NextButton from 'dashboard/components-next/button/Button.vue';
defineProps({ defineProps({
inputPlaceholder: { inputPlaceholder: {
type: String, type: String,
@@ -23,29 +21,31 @@ const value = defineModel({
<template> <template>
<div <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> <div class="flex items-center w-full gap-2" @keyup.space.prevent>
<fluent-icon <fluent-icon
icon="search" icon="search"
size="16" size="16"
class="text-n-slate-11 flex-shrink-0" class="text-slate-400 dark:text-slate-400 flex-shrink-0"
/> />
<input <input
v-model="value" v-model="value"
:placeholder="inputPlaceholder" :placeholder="inputPlaceholder"
type="text" 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> </div>
<!-- Clear filter button --> <!-- Clear filter button -->
<NextButton <woot-button
v-if="!modelValue && showClearFilter" v-if="!modelValue && showClearFilter"
faded size="small"
xs variant="clear"
class="flex-shrink-0" color-scheme="primary"
:label="$t('REPORT.FILTER_ACTIONS.CLEAR_FILTER')" class="!px-1 !py-1.5"
@click="emit('remove')" @click="emit('remove')"
/> >
{{ $t('REPORT.FILTER_ACTIONS.CLEAR_FILTER') }}
</woot-button>
</div> </div>
</template> </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>
@@ -2,12 +2,10 @@
import { useMessageFormatter } from 'shared/composables/useMessageFormatter'; import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
import { useAI } from 'dashboard/composables/useAI'; import { useAI } from 'dashboard/composables/useAI';
import AILoader from './AILoader.vue'; import AILoader from './AILoader.vue';
import NextButton from 'dashboard/components-next/button/Button.vue';
export default { export default {
components: { components: {
AILoader, AILoader,
NextButton,
}, },
props: { props: {
aiOption: { aiOption: {
@@ -86,22 +84,16 @@ export default {
</div> </div>
<div class="flex flex-row justify-end w-full gap-2 px-0 py-2"> <div class="flex flex-row justify-end w-full gap-2 px-0 py-2">
<NextButton <woot-button variant="clear" @click.prevent="onClose">
faded {{
slate
type="reset"
:label="
$t('INTEGRATION_SETTINGS.OPEN_AI.ASSISTANCE_MODAL.BUTTONS.CANCEL') $t('INTEGRATION_SETTINGS.OPEN_AI.ASSISTANCE_MODAL.BUTTONS.CANCEL')
" }}
@click.prevent="onClose" </woot-button>
/> <woot-button :disabled="!generatedContent">
<NextButton {{
type="submit"
:disabled="!generatedContent"
:label="
$t('INTEGRATION_SETTINGS.OPEN_AI.ASSISTANCE_MODAL.BUTTONS.APPLY') $t('INTEGRATION_SETTINGS.OPEN_AI.ASSISTANCE_MODAL.BUTTONS.APPLY')
" }}
/> </woot-button>
</div> </div>
</form> </form>
</div> </div>
@@ -6,12 +6,7 @@ import { useUISettings } from 'dashboard/composables/useUISettings';
import { useAI } from 'dashboard/composables/useAI'; import { useAI } from 'dashboard/composables/useAI';
import { OPEN_AI_EVENTS } from 'dashboard/helper/AnalyticsHelper/events'; import { OPEN_AI_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
import NextButton from 'dashboard/components-next/button/Button.vue';
export default { export default {
components: {
NextButton,
},
emits: ['close'], emits: ['close'],
setup() { setup() {
@@ -99,30 +94,16 @@ export default {
/> />
</div> </div>
<div class="flex flex-row justify-between w-full gap-2 px-0 py-2"> <div class="flex flex-row justify-between w-full gap-2 px-0 py-2">
<NextButton <woot-button variant="link" @click.prevent="openOpenAIDoc">
ghost {{ $t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.BUTTONS.NEED_HELP') }}
type="button" </woot-button>
class="!px-3"
:label="
$t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.BUTTONS.NEED_HELP')
"
@click.prevent="openOpenAIDoc"
/>
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<NextButton <woot-button variant="clear" @click.prevent="onDismiss">
faded {{ $t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.BUTTONS.DISMISS') }}
slate </woot-button>
type="reset" <woot-button :is-disabled="v$.value.$invalid">
:label=" {{ $t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.BUTTONS.FINISH') }}
$t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.BUTTONS.DISMISS') </woot-button>
"
@click.prevent="onDismiss"
/>
<NextButton
type="submit"
:disabled="v$.value.$invalid"
:label="$t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.BUTTONS.FINISH')"
/>
</div> </div>
</div> </div>
</form> </form>
@@ -2,8 +2,6 @@
import { computed } from 'vue'; import { computed } from 'vue';
import { formatBytes } from 'shared/helpers/FileHelper'; import { formatBytes } from 'shared/helpers/FileHelper';
import Button from 'dashboard/components-next/button/Button.vue';
const props = defineProps({ const props = defineProps({
attachments: { attachments: {
type: Array, type: Array,
@@ -75,11 +73,9 @@ const fileName = file => {
</span> </span>
</div> </div>
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
<Button <woot-button
ghost class="!w-6 !h-6 text-sm rounded-md hover:bg-slate-50 dark:hover:bg-slate-800 clear secondary"
slate icon="dismiss"
xs
icon="i-lucide-x"
@click="onRemoveAttachment(index)" @click="onRemoveAttachment(index)"
/> />
</div> </div>
@@ -2,14 +2,11 @@
import AutomationActionTeamMessageInput from './AutomationActionTeamMessageInput.vue'; import AutomationActionTeamMessageInput from './AutomationActionTeamMessageInput.vue';
import AutomationActionFileInput from './AutomationFileInput.vue'; import AutomationActionFileInput from './AutomationFileInput.vue';
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue'; import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
import NextButton from 'dashboard/components-next/button/Button.vue';
export default { export default {
components: { components: {
AutomationActionTeamMessageInput, AutomationActionTeamMessageInput,
AutomationActionFileInput, AutomationActionFileInput,
WootMessageEditor, WootMessageEditor,
NextButton,
}, },
props: { props: {
modelValue: { modelValue: {
@@ -175,11 +172,11 @@ export default {
/> />
</div> </div>
</div> </div>
<NextButton <woot-button
v-if="!isMacro" v-if="!isMacro"
icon="i-lucide-x" icon="dismiss"
slate variant="clear"
ghost color-scheme="secondary"
class="flex-shrink-0" class="flex-shrink-0"
@click="removeAction" @click="removeAction"
/> />
@@ -247,13 +247,7 @@ export default {
:placeholder="$t('FILTER.INPUT_PLACEHOLDER')" :placeholder="$t('FILTER.INPUT_PLACEHOLDER')"
/> />
</div> </div>
<NextButton <NextButton icon="i-lucide-x" slate ghost @click="removeFilter" />
icon="i-lucide-x"
slate
ghost
class="flex-shrink-0"
@click="removeFilter"
/>
</div> </div>
<p v-if="errorMessage" class="filter-error"> <p v-if="errorMessage" class="filter-error">
{{ errorMessage }} {{ errorMessage }}
@@ -1,7 +1,7 @@
<script setup> <script setup>
import { computed } from 'vue'; import { computed } from 'vue';
import NextButton from 'dashboard/components-next/button/Button.vue';
// Props
const props = defineProps({ const props = defineProps({
currentPage: { currentPage: {
type: Number, type: Number,
@@ -21,6 +21,13 @@ const hasFirstPage = computed(() => props.currentPage === 1);
const hasNextPage = computed(() => props.currentPage === props.totalPages); const hasNextPage = computed(() => props.currentPage === props.totalPages);
const hasPrevPage = computed(() => props.currentPage === 1); 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) { function onPageChange(newPage) {
emit('pageChange', newPage); emit('pageChange', newPage);
} }
@@ -48,61 +55,84 @@ const onLastPage = () => {
</script> </script>
<template> <template>
<div <div class="flex items-center h-8 rounded-lg bg-slate-50 dark:bg-slate-800">
class="flex items-center h-8 outline outline-1 outline-n-weak rounded-lg" <woot-button
> size="small"
<NextButton variant="smooth"
faded color-scheme="secondary"
sm :is-disabled="hasFirstPage"
slate class-names="dark:!bg-slate-800 !opacity-100 ltr:rounded-l-lg ltr:rounded-r-none rtl:rounded-r-lg rtl:rounded-l-none"
icon="i-lucide-chevrons-left" :class="buttonClass(hasFirstPage)"
class="ltr:rounded-l-lg ltr:rounded-r-none rtl:rounded-r-lg rtl:rounded-l-none"
:disabled="hasFirstPage"
@click="onFirstPage" @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 }} {{ currentPage }}
</span> </span>
<span class="text-n-slate-11">/</span> <span class="text-slate-600 dark:text-slate-500">/</span>
<span class="text-sm text-n-slate-11"> <span class="text-sm text-slate-600 dark:text-slate-500">
{{ totalPages }} {{ totalPages }}
</span> </span>
</div> </div>
<NextButton <woot-button
faded size="small"
sm variant="smooth"
slate color-scheme="secondary"
icon="i-lucide-chevron-right" :is-disabled="hasNextPage"
class="rounded-none" class-names="dark:!bg-slate-800 !opacity-100 rounded-none"
:disabled="hasNextPage" :class="buttonClass(hasNextPage)"
@click="onNextPage" @click="onNextPage"
/> >
<div class="flex items-center justify-center bg-n-slate-9/10 h-full"> <fluent-icon
<div class="w-px h-4 rounded-sm bg-n-strong" /> icon="chevron-right-single"
</div> size="20"
<NextButton icon-lib="lucide"
faded :class="hasNextPage && 'opacity-40'"
sm />
slate </woot-button>
icon="i-lucide-chevrons-right" <div class="w-px h-4 rounded-sm bg-slate-75 dark:bg-slate-700/50" />
class="ltr:rounded-r-lg ltr:rounded-l-none rtl:rounded-l-lg rtl:rounded-r-none" <woot-button
:disabled="hasLastPage" 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" @click="onLastPage"
/> >
<fluent-icon
icon="chevrons-right"
size="20"
icon-lib="lucide"
:class="hasLastPage && 'opacity-40'"
/>
</woot-button>
</div> </div>
</template> </template>
@@ -164,6 +164,11 @@ export default {
'is-note-mode': this.isNote, 'is-note-mode': this.isNote,
}; };
}, },
buttonClass() {
return {
warning: this.isNote,
};
},
showAttachButton() { showAttachButton() {
return this.showFileUpload || this.isNote; return this.showFileUpload || this.isNote;
}, },
@@ -362,15 +367,14 @@ export default {
/> />
</div> </div>
<div class="right-wrap"> <div class="right-wrap">
<NextButton <woot-button
:label="sendButtonText" size="small"
type="submit" :class-names="buttonClass"
sm :is-disabled="isSendDisabled"
:color="isNote ? 'amber' : 'blue'"
:disabled="isSendDisabled"
class="flex-shrink-0"
@click="onSend" @click="onSend"
/> >
{{ sendButtonText }}
</woot-button>
</div> </div>
</div> </div>
</template> </template>
@@ -106,3 +106,43 @@ export default {
/> />
</div> </div>
</template> </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>
@@ -13,8 +13,6 @@ import { snoozedReopenTime } from 'dashboard/helper/snoozeHelpers';
import { FEATURE_FLAGS } from 'dashboard/featureFlags'; import { FEATURE_FLAGS } from 'dashboard/featureFlags';
import Linear from './linear/index.vue'; import Linear from './linear/index.vue';
import NextButton from 'dashboard/components-next/button/Button.vue';
export default { export default {
components: { components: {
BackButton, BackButton,
@@ -23,7 +21,6 @@ export default {
Thumbnail, Thumbnail,
SLACardLabel, SLACardLabel,
Linear, Linear,
NextButton,
}, },
mixins: [inboxMixin], mixins: [inboxMixin],
props: { props: {
@@ -157,13 +154,16 @@ export default {
<div <div
class="flex flex-row items-center max-w-full gap-1 p-0 m-0 w-fit" class="flex flex-row items-center max-w-full gap-1 p-0 m-0 w-fit"
> >
<NextButton link slate @click.prevent="$emit('contactPanelToggle')"> <woot-button
<span variant="link"
class="text-base font-medium truncate leading-tight text-n-slate-12" color-scheme="secondary"
> class="[&>span]:overflow-hidden [&>span]:whitespace-nowrap [&>span]:text-ellipsis min-w-0"
@click.prevent="$emit('contactPanelToggle')"
>
<span class="text-base font-medium leading-tight text-n-slate-12">
{{ currentContact.name }} {{ currentContact.name }}
</span> </span>
</NextButton> </woot-button>
<fluent-icon <fluent-icon
v-if="!isHMACVerified" v-if="!isHMACVerified"
v-tooltip="$t('CONVERSATION.UNVERIFIED_SESSION')" v-tooltip="$t('CONVERSATION.UNVERIFIED_SESSION')"
@@ -180,13 +180,14 @@ export default {
<span v-if="isSnoozed" class="font-medium text-n-amber-10"> <span v-if="isSnoozed" class="font-medium text-n-amber-10">
{{ snoozedDisplayText }} {{ snoozedDisplayText }}
</span> </span>
<NextButton <woot-button
link class="p-0"
xs size="small"
blue variant="link"
:label="contactPanelToggleText"
@click="$emit('contactPanelToggle')" @click="$emit('contactPanelToggle')"
/> >
{{ contactPanelToggleText }}
</woot-button>
</div> </div>
</div> </div>
</div> </div>
@@ -2,12 +2,8 @@
import { useVuelidate } from '@vuelidate/core'; import { useVuelidate } from '@vuelidate/core';
import { required, minLength, email } from '@vuelidate/validators'; import { required, minLength, email } from '@vuelidate/validators';
import { useAlert } from 'dashboard/composables'; import { useAlert } from 'dashboard/composables';
import NextButton from 'dashboard/components-next/button/Button.vue';
export default { export default {
components: {
NextButton,
},
props: { props: {
show: { show: {
type: Boolean, type: Boolean,
@@ -157,18 +153,13 @@ export default {
</div> </div>
</div> </div>
<div class="flex flex-row justify-end w-full gap-2 px-0 py-2"> <div class="flex flex-row justify-end w-full gap-2 px-0 py-2">
<NextButton <woot-submit-button
faded :button-text="$t('EMAIL_TRANSCRIPT.SUBMIT')"
slate
type="reset"
:label="$t('EMAIL_TRANSCRIPT.CANCEL')"
@click.prevent="onCancel"
/>
<NextButton
type="submit"
:label="$t('EMAIL_TRANSCRIPT.SUBMIT')"
:disabled="!isFormValid" :disabled="!isFormValid"
/> />
<button class="button clear" @click.prevent="onCancel">
{{ $t('EMAIL_TRANSCRIPT.CANCEL') }}
</button>
</div> </div>
</form> </form>
</div> </div>
@@ -25,8 +25,6 @@ import * as Sentry from '@sentry/vue';
import { useTrack } from 'dashboard/composables'; import { useTrack } from 'dashboard/composables';
import { emitter } from 'shared/helpers/mitt'; import { emitter } from 'shared/helpers/mitt';
import NextButton from 'dashboard/components-next/button/Button.vue';
export default { export default {
components: { components: {
BubbleActions, BubbleActions,
@@ -42,7 +40,6 @@ export default {
InstagramStory, InstagramStory,
InstagramStoryReply, InstagramStoryReply,
Spinner, Spinner,
NextButton,
}, },
props: { props: {
data: { data: {
@@ -455,12 +452,12 @@ export default {
v-if="isFailed && !hasOneDayPassed && !isAnEmailInbox" v-if="isFailed && !hasOneDayPassed && !isAnEmailInbox"
class="message-failed--alert" class="message-failed--alert"
> >
<NextButton <woot-button
v-tooltip.top-end="$t('CONVERSATION.TRY_AGAIN')" v-tooltip.top-end="$t('CONVERSATION.TRY_AGAIN')"
ghost size="tiny"
xs color-scheme="alert"
ruby variant="clear"
icon="i-lucide-refresh-ccw" icon="arrow-clockwise"
@click="retrySendMessage" @click="retrySendMessage"
/> />
</div> </div>
@@ -663,10 +660,10 @@ export default {
} }
&.is-failed { &.is-failed {
@apply bg-n-ruby-4 dark:bg-n-ruby-4 text-n-slate-12; @apply bg-red-200 dark:bg-red-200;
.message-text--metadata .time { .message-text--metadata .time {
@apply text-n-ruby-12 dark:text-n-ruby-12; @apply text-red-50 dark:text-red-50;
} }
} }
} }
@@ -727,7 +724,7 @@ li.right {
} }
.wrap.is-failed { .wrap.is-failed {
@apply flex items-end ltr:ml-auto rtl:mr-auto; @apply flex items-end ml-auto;
} }
} }
@@ -1,11 +1,10 @@
<script setup> <script>
import { useRouter } from 'vue-router'; export default {
import Button from 'dashboard/components-next/button/Button.vue'; methods: {
openProfileSettings() {
const router = useRouter(); return this.$router.push({ name: 'profile_settings_index' });
},
const openProfileSettings = () => { },
return router.push({ name: 'profile_settings_index' });
}; };
</script> </script>
@@ -15,12 +14,13 @@ const openProfileSettings = () => {
> >
<p class="w-fit !m-0"> <p class="w-fit !m-0">
{{ $t('CONVERSATION.FOOTER.MESSAGE_SIGNATURE_NOT_CONFIGURED') }} {{ $t('CONVERSATION.FOOTER.MESSAGE_SIGNATURE_NOT_CONFIGURED') }}
<woot-button
<Button color-scheme="primary"
link variant="link"
:label="$t('CONVERSATION.FOOTER.CLICK_HERE')"
@click="openProfileSettings" @click="openProfileSettings"
/> >
{{ $t('CONVERSATION.FOOTER.CLICK_HERE') }}
</woot-button>
</p> </p>
</div> </div>
</template> </template>
@@ -37,8 +37,6 @@ import wootConstants from 'dashboard/constants/globals';
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage'; import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
import { FEATURE_FLAGS } from '../../../featureFlags'; import { FEATURE_FLAGS } from '../../../featureFlags';
import NextButton from 'dashboard/components-next/button/Button.vue';
export default { export default {
components: { components: {
Message, Message,
@@ -46,7 +44,6 @@ export default {
ReplyBox, ReplyBox,
Banner, Banner,
ConversationLabelSuggestion, ConversationLabelSuggestion,
NextButton,
}, },
mixins: [inboxMixin], mixins: [inboxMixin],
props: { props: {
@@ -491,15 +488,13 @@ export default {
:href-link-text="replyWindowLinkText" :href-link-text="replyWindowLinkText"
/> />
<div class="flex justify-end"> <div class="flex justify-end">
<NextButton <woot-button
faded variant="smooth"
xs size="tiny"
slate color-scheme="secondary"
class="!rounded-r-none rtl:rotate-180 !rounded-2xl !fixed z-10" class="box-border fixed z-10 bg-white border border-r-0 border-solid rounded-bl-calc rtl:rotate-180 rounded-tl-calc border-n-weak"
:icon="
isContactPanelOpen ? 'i-ph-caret-right-fill' : 'i-ph-caret-left-fill'
"
:class="isInboxView ? 'top-52 md:top-40' : 'top-32'" :class="isInboxView ? 'top-52 md:top-40' : 'top-32'"
:icon="isRightOrLeftIcon"
@click="onToggleContactPanel" @click="onToggleContactPanel"
/> />
</div> </div>
@@ -619,6 +614,20 @@ export default {
</div> </div>
</template> </template>
<style scoped>
@tailwind components;
@layer components {
.rounded-bl-calc {
border-bottom-left-radius: calc(1.5rem + 1px);
}
.rounded-tl-calc {
border-top-left-radius: calc(1.5rem + 1px);
}
}
</style>
<style scoped lang="scss"> <style scoped lang="scss">
.modal-mask { .modal-mask {
@apply absolute; @apply absolute;
@@ -420,7 +420,6 @@ export default {
if (conversationId !== oldConversationId) { if (conversationId !== oldConversationId) {
this.setToDraft(oldConversationId, this.replyType); this.setToDraft(oldConversationId, this.replyType);
this.getFromDraft(); this.getFromDraft();
this.resetRecorderAndClearAttachments();
} }
}, },
message(updatedMessage) { message(updatedMessage) {
@@ -530,12 +529,6 @@ export default {
); );
} }
}, },
resetRecorderAndClearAttachments() {
// Reset audio recorder UI state
this.resetAudioRecorderInput();
// Reset attached files
this.attachedFiles = [];
},
saveDraft(conversationId, replyType) { saveDraft(conversationId, replyType) {
if (this.message || this.message === '') { if (this.message || this.message === '') {
const key = `draft-${conversationId}-${replyType}`; const key = `draft-${conversationId}-${replyType}`;
@@ -1074,7 +1067,7 @@ export default {
<template> <template>
<Banner <Banner
v-if="showSelfAssignBanner" v-if="showSelfAssignBanner"
action-button-variant="ghost" action-button-variant="clear"
color-scheme="secondary" color-scheme="secondary"
class="banner--self-assign mx-2 mb-2 rounded-lg" class="banner--self-assign mx-2 mb-2 rounded-lg"
:banner-message="$t('CONVERSATION.NOT_ASSIGNED_TO_YOU')" :banner-message="$t('CONVERSATION.NOT_ASSIGNED_TO_YOU')"
@@ -1,20 +1,21 @@
<script setup> <script>
import MessagePreview from 'dashboard/components/widgets/conversation/MessagePreview.vue'; import MessagePreview from 'dashboard/components/widgets/conversation/MessagePreview.vue';
import Button from 'dashboard/components-next/button/Button.vue';
defineProps({ export default {
message: { components: { MessagePreview },
type: Object, props: {
required: true, message: {
type: Object,
required: true,
},
}, },
}); emits: ['dismiss'],
};
const emit = defineEmits(['dismiss']);
</script> </script>
<template> <template>
<div <div
class="reply-editor bg-n-slate-9/10 rounded-md py-1 pl-2 pr-1 text-xs tracking-wide mt-2 flex items-center gap-1.5 -mx-2" class="reply-editor bg-slate-50 dark:bg-slate-800 rounded-md py-1 pl-2 pr-1 text-xs tracking-wide mt-2 flex items-center gap-1.5 -mx-2"
> >
<fluent-icon class="flex-shrink-0 icon" icon="arrow-reply" size="14" /> <fluent-icon class="flex-shrink-0 icon" icon="arrow-reply" size="14" />
<div class="flex-grow gap-1 mt-px text-xs truncate"> <div class="flex-grow gap-1 mt-px text-xs truncate">
@@ -26,13 +27,14 @@ const emit = defineEmits(['dismiss']);
class="inline" class="inline"
/> />
</div> </div>
<Button <woot-button
v-tooltip="$t('CONVERSATION.REPLYBOX.DISMISS_REPLY')" v-tooltip="$t('CONVERSATION.REPLYBOX.DISMISS_REPLY')"
ghost color-scheme="secondary"
xs icon="dismiss"
slate variant="clear"
icon="i-lucide-x" size="tiny"
@click.stop="emit('dismiss')" class="flex-shrink-0"
@click.stop="$emit('dismiss')"
/> />
</div> </div>
</template> </template>
@@ -12,12 +12,7 @@ import { ref, computed, onMounted } from 'vue';
import { useVuelidate } from '@vuelidate/core'; import { useVuelidate } from '@vuelidate/core';
import { requiredIf } from '@vuelidate/validators'; import { requiredIf } from '@vuelidate/validators';
import NextButton from 'dashboard/components-next/button/Button.vue';
export default { export default {
components: {
NextButton,
},
props: { props: {
template: { template: {
type: Object, type: Object,
@@ -119,47 +114,78 @@ export default {
readonly readonly
class="template-input" class="template-input"
/> />
<div v-if="variables" class="p-2.5"> <div v-if="variables" class="template__variables-container">
<p class="text-sm font-semibold mb-2.5"> <p class="variables-label">
{{ $t('WHATSAPP_TEMPLATES.PARSER.VARIABLES_LABEL') }} {{ $t('WHATSAPP_TEMPLATES.PARSER.VARIABLES_LABEL') }}
</p> </p>
<div <div
v-for="(variable, key) in processedParams" v-for="(variable, key) in processedParams"
:key="key" :key="key"
class="items-center flex mb-2.5" class="template__variable-item"
> >
<span <span class="variable-label">
class="bg-n-alpha-black2 text-n-slate-12 inline-block rounded-md text-xs py-2.5 px-6"
>
{{ key }} {{ key }}
</span> </span>
<woot-input <woot-input
v-model="processedParams[key]" v-model="processedParams[key]"
type="text" type="text"
class="flex-1 text-sm ml-2.5" class="variable-input"
:styles="{ marginBottom: 0 }" :styles="{ marginBottom: 0 }"
/> />
</div> </div>
<p <p v-if="v$.$dirty && v$.$invalid" class="error">
v-if="v$.$dirty && v$.$invalid"
class="bg-n-ruby-9/20 rounded-md text-n-ruby-9 p-2.5 text-center"
>
{{ $t('WHATSAPP_TEMPLATES.PARSER.FORM_ERROR_MESSAGE') }} {{ $t('WHATSAPP_TEMPLATES.PARSER.FORM_ERROR_MESSAGE') }}
</p> </p>
</div> </div>
<footer class="flex justify-end gap-2"> <footer>
<NextButton <woot-button variant="smooth" @click="resetTemplate">
faded {{ $t('WHATSAPP_TEMPLATES.PARSER.GO_BACK_LABEL') }}
slate </woot-button>
type="reset" <woot-button type="button" @click="sendMessage">
:label="$t('WHATSAPP_TEMPLATES.PARSER.GO_BACK_LABEL')" {{ $t('WHATSAPP_TEMPLATES.PARSER.SEND_MESSAGE_LABEL') }}
@click="resetTemplate" </woot-button>
/>
<NextButton
type="button"
:label="$t('WHATSAPP_TEMPLATES.PARSER.SEND_MESSAGE_LABEL')"
@click="sendMessage"
/>
</footer> </footer>
</div> </div>
</template> </template>
<style scoped lang="scss">
.template__variables-container {
@apply p-2.5;
}
.variables-label {
@apply text-sm font-semibold mb-2.5;
}
.template__variable-item {
@apply items-center flex mb-2.5;
.label {
@apply text-xs;
}
.variable-input {
@apply flex-1 text-sm ml-2.5;
}
.variable-label {
@apply bg-slate-75 dark:bg-slate-700 text-slate-700 dark:text-slate-100 inline-block rounded-md text-xs py-2.5 px-6;
}
}
footer {
@apply flex justify-end;
button {
@apply ml-2.5;
}
}
.error {
@apply bg-red-100 dark:bg-red-100 rounded-md text-red-800 dark:text-red-800 p-2.5 text-center;
}
.template-input {
@apply bg-slate-25 dark:bg-slate-900 text-slate-700 dark:text-slate-100;
}
</style>
@@ -43,56 +43,48 @@ export default {
<template> <template>
<div class="w-full"> <div class="w-full">
<div <div class="gap-1 templates__list-search">
class="gap-1 bg-n-alpha-black2 items-center flex mb-2.5 py-0 px-2.5 rounded-lg outline outline-1 outline-n-weak hover:outline-n-slate-6 dark:hover:outline-n-slate-6 focus-within:outline-n-brand dark:focus-within:outline-n-brand" <fluent-icon icon="search" class="search-icon" size="16" />
>
<fluent-icon icon="search" class="text-n-slate-12" size="16" />
<input <input
v-model="query" v-model="query"
type="search" type="search"
:placeholder="$t('WHATSAPP_TEMPLATES.PICKER.SEARCH_PLACEHOLDER')" :placeholder="$t('WHATSAPP_TEMPLATES.PICKER.SEARCH_PLACEHOLDER')"
class="reset-base w-full h-9 bg-transparent text-n-slate-12 !text-sm !outline-0" class="templates__search-input"
/> />
</div> </div>
<div <div class="template__list-container">
class="bg-n-background outline-n-container outline outline-1 rounded-lg max-h-[18.75rem] overflow-y-auto p-2.5"
>
<div v-for="(template, i) in filteredTemplateMessages" :key="template.id"> <div v-for="(template, i) in filteredTemplateMessages" :key="template.id">
<button <button
class="rounded-lg cursor-pointer block p-2.5 text-left w-full hover:bg-n-alpha-2 dark:hover:bg-n-solid-2" class="template__list-item"
@click="$emit('onSelect', template)" @click="$emit('onSelect', template)"
> >
<div> <div>
<div class="flex items-center justify-between mb-2.5"> <div class="flex items-center justify-between mb-2.5">
<p class="text-sm"> <p class="label-title">
{{ template.name }} {{ template.name }}
</p> </p>
<span <span
class="inline-block px-2 py-1 text-xs leading-none bg-n-slate-3 rounded-lg cursor-default text-n-slate-12" class="inline-block px-2 py-1 text-xs leading-none bg-white rounded-sm cursor-default dark:bg-slate-700 text-slate-800 dark:text-slate-100"
> >
{{ $t('WHATSAPP_TEMPLATES.PICKER.LABELS.LANGUAGE') }} : {{ $t('WHATSAPP_TEMPLATES.PICKER.LABELS.LANGUAGE') }} :
{{ template.language }} {{ template.language }}
</span> </span>
</div> </div>
<div> <div>
<p class="font-medium"> <p class="strong">
{{ $t('WHATSAPP_TEMPLATES.PICKER.LABELS.TEMPLATE_BODY') }} {{ $t('WHATSAPP_TEMPLATES.PICKER.LABELS.TEMPLATE_BODY') }}
</p> </p>
<p class="label-body">{{ getTemplatebody(template) }}</p> <p class="label-body">{{ getTemplatebody(template) }}</p>
</div> </div>
<div class="mt-5"> <div class="label-category">
<p class="font-medium"> <p class="strong">
{{ $t('WHATSAPP_TEMPLATES.PICKER.LABELS.CATEGORY') }} {{ $t('WHATSAPP_TEMPLATES.PICKER.LABELS.CATEGORY') }}
</p> </p>
<p>{{ template.category }}</p> <p>{{ template.category }}</p>
</div> </div>
</div> </div>
</button> </button>
<hr <hr v-if="i != filteredTemplateMessages.length - 1" :key="`hr-${i}`" />
v-if="i != filteredTemplateMessages.length - 1"
:key="`hr-${i}`"
class="border-b border-solid border-n-weak my-2.5 mx-auto max-w-[95%]"
/>
</div> </div>
<div v-if="!filteredTemplateMessages.length"> <div v-if="!filteredTemplateMessages.length">
<p> <p>
@@ -105,7 +97,46 @@ export default {
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
.label-body { .templates__list-search {
font-family: monospace; @apply items-center flex bg-slate-25 dark:bg-slate-900 mb-2.5 py-0 px-2.5 rounded-md border border-solid border-slate-100 dark:border-slate-700;
.search-icon {
@apply text-slate-400 dark:text-slate-300;
}
.templates__search-input {
@apply bg-transparent border-0 text-xs h-9 m-0;
}
}
.template__list-container {
@apply bg-slate-25 dark:bg-slate-900 rounded-md max-h-[18.75rem] overflow-y-auto p-2.5;
.template__list-item {
@apply rounded-lg cursor-pointer block p-2.5 text-left w-full hover:bg-woot-50 dark:hover:bg-slate-800;
.label-title {
@apply text-sm;
}
.label-category {
@apply mt-5;
span {
@apply text-sm font-semibold;
}
}
.label-body {
font-family: monospace;
}
}
}
.strong {
@apply text-xs font-semibold;
}
hr {
@apply border-b border-solid border-slate-100 dark:border-slate-700 my-2.5 mx-auto max-w-[95%];
} }
</style> </style>
@@ -22,14 +22,6 @@ const filterTypes = [
filterOperators: OPERATOR_TYPES_2, filterOperators: OPERATOR_TYPES_2,
attributeModel: 'standard', attributeModel: 'standard',
}, },
{
attributeKey: 'priority',
attributeI18nKey: 'PRIORITY',
inputType: 'multi_select',
dataType: 'text',
filterOperators: OPERATOR_TYPES_1,
attributeModel: 'standard',
},
{ {
attributeKey: 'inbox_id', attributeKey: 'inbox_id',
attributeI18nKey: 'INBOX_NAME', attributeI18nKey: 'INBOX_NAME',
@@ -4,12 +4,8 @@ import {
DuplicateContactException, DuplicateContactException,
ExceptionWithMessage, ExceptionWithMessage,
} from 'shared/helpers/CustomErrors'; } from 'shared/helpers/CustomErrors';
import NextButton from 'dashboard/components-next/button/Button.vue';
export default { export default {
components: {
NextButton,
},
props: { props: {
name: { name: {
type: String, type: String,
@@ -95,12 +91,9 @@ export default {
</p> </p>
</div> </div>
<div v-if="formattedPhoneNumber" class="link-wrap"> <div v-if="formattedPhoneNumber" class="link-wrap">
<NextButton <woot-button variant="clear" size="small" @click.prevent="addContact">
ghost {{ $t('CONVERSATION.SAVE_CONTACT') }}
xs </woot-button>
:label="$t('CONVERSATION.SAVE_CONTACT')"
@click.prevent="addContact"
/>
</div> </div>
</div> </div>
</template> </template>
@@ -2,12 +2,8 @@
import DyteAPI from 'dashboard/api/integrations/dyte'; import DyteAPI from 'dashboard/api/integrations/dyte';
import { buildDyteURL } from 'shared/helpers/IntegrationHelper'; import { buildDyteURL } from 'shared/helpers/IntegrationHelper';
import { useAlert } from 'dashboard/composables'; import { useAlert } from 'dashboard/composables';
import NextButton from 'dashboard/components-next/button/Button.vue';
export default { export default {
components: {
NextButton,
},
props: { props: {
messageId: { messageId: {
type: Number, type: Number,
@@ -45,25 +41,31 @@ export default {
<template> <template>
<div> <div>
<NextButton <woot-button
blue size="small"
sm variant="smooth"
icon="i-lucide-video" color-scheme="secondary"
:label="$t('INTEGRATION_SETTINGS.DYTE.CLICK_HERE_TO_JOIN')" icon="video-add"
class="join-call-button"
:is-loading="isLoading" :is-loading="isLoading"
@click="joinTheCall" @click="joinTheCall"
/> >
{{ $t('INTEGRATION_SETTINGS.DYTE.CLICK_HERE_TO_JOIN') }}
</woot-button>
<div v-if="dyteAuthToken" class="video-call--container"> <div v-if="dyteAuthToken" class="video-call--container">
<iframe <iframe
:src="meetingLink" :src="meetingLink"
allow="camera;microphone;fullscreen;display-capture;picture-in-picture;clipboard-write;" allow="camera;microphone;fullscreen;display-capture;picture-in-picture;clipboard-write;"
/> />
<NextButton <woot-button
sm size="small"
class="mt-2" variant="smooth"
:label="$t('INTEGRATION_SETTINGS.DYTE.LEAVE_THE_ROOM')" color-scheme="secondary"
class="join-call-button"
@click="leaveTheRoom" @click="leaveTheRoom"
/> >
{{ $t('INTEGRATION_SETTINGS.DYTE.LEAVE_THE_ROOM') }}
</woot-button>
</div> </div>
</div> </div>
</template> </template>
@@ -3,7 +3,6 @@ import { ref, computed } from 'vue';
import wootConstants from 'dashboard/constants/globals'; import wootConstants from 'dashboard/constants/globals';
import SLAEventItem from './SLAEventItem.vue'; import SLAEventItem from './SLAEventItem.vue';
import Button from 'dashboard/components-next/button/Button.vue';
const props = defineProps({ const props = defineProps({
slaMissedEvents: { slaMissedEvents: {
@@ -61,19 +60,20 @@ const toggleShowAllNRT = () => {
v-if="shouldShowMoreNRTButton" v-if="shouldShowMoreNRTButton"
class="flex flex-col items-end w-full" class="flex flex-col items-end w-full"
> >
<Button <woot-button
link size="small"
xs :icon="!shouldShowAllNrts ? 'plus-sign' : ''"
slate variant="link"
class="hover:!no-underline" color-scheme="secondary"
:icon="!shouldShowAllNrts ? 'i-lucide-plus' : ''" class="hover:!no-underline !gap-1 hover:!bg-transparent dark:hover:!bg-transparent"
:label=" @click="toggleShowAllNRT"
>
{{
shouldShowAllNrts shouldShowAllNrts
? $t('SLA.EVENTS.HIDE', { count: nrtMisses.length }) ? $t('SLA.EVENTS.HIDE', { count: nrtMisses.length })
: $t('SLA.EVENTS.SHOW_MORE', { count: nrtMisses.length }) : $t('SLA.EVENTS.SHOW_MORE', { count: nrtMisses.length })
" }}
@click="toggleShowAllNRT" </woot-button>
/>
</div> </div>
</template> </template>
</SLAEventItem> </SLAEventItem>
@@ -1,6 +1,6 @@
<script> <script>
// components // components
import NextButton from 'dashboard/components-next/button/Button.vue'; import WootButton from '../../../ui/WootButton.vue';
import Avatar from '../../Avatar.vue'; import Avatar from '../../Avatar.vue';
// composables // composables
@@ -19,7 +19,7 @@ export default {
name: 'LabelSuggestion', name: 'LabelSuggestion',
components: { components: {
Avatar, Avatar,
NextButton, WootButton,
}, },
props: { props: {
suggestedLabels: { suggestedLabels: {
@@ -185,44 +185,42 @@ export default {
" "
/> />
</button> </button>
<NextButton <WootButton
v-if="preparedLabels.length === 1" v-if="preparedLabels.length === 1"
v-tooltip.top="{ v-tooltip.top="{
content: $t('LABEL_MGMT.SUGGESTIONS.TOOLTIP.DISMISS'), content: $t('LABEL_MGMT.SUGGESTIONS.TOOLTIP.DISMISS'),
delay: { show: 600, hide: 0 }, delay: { show: 600, hide: 0 },
hideOnClick: true, hideOnClick: true,
}" }"
faded variant="smooth"
xs :color-scheme="isHovered ? 'alert' : 'primary'"
icon="i-lucide-x" class="label--add"
class="flex-shrink-0" icon="dismiss"
:color="isHovered ? 'ruby' : 'blue'" size="tiny"
@click="dismissSuggestions" @click="dismissSuggestions"
/> />
</div> </div>
<div <div v-if="preparedLabels.length > 1">
v-if="preparedLabels.length > 1" <WootButton
class="inline-flex items-center gap-1" :variant="selectedLabels.length === 0 ? 'smooth' : ''"
> class="label--add"
<NextButton icon="add"
xs size="tiny"
icon="i-lucide-plus"
class="flex-shrink-0"
:variant="selectedLabels.length === 0 ? 'faded' : 'solid'"
:label="addButtonText"
@click="addAllLabels" @click="addAllLabels"
/> >
<NextButton {{ addButtonText }}
</WootButton>
<WootButton
v-tooltip.top="{ v-tooltip.top="{
content: $t('LABEL_MGMT.SUGGESTIONS.TOOLTIP.DISMISS'), content: $t('LABEL_MGMT.SUGGESTIONS.TOOLTIP.DISMISS'),
delay: { show: 600, hide: 0 }, delay: { show: 600, hide: 0 },
hideOnClick: true, hideOnClick: true,
}" }"
faded :color-scheme="isHovered ? 'alert' : 'primary'"
xs variant="smooth"
icon="i-lucide-x" class="label--add"
class="flex-shrink-0" icon="dismiss"
:color="isHovered ? 'ruby' : 'blue'" size="tiny"
@click="dismissSuggestions" @click="dismissSuggestions"
/> />
</div> </div>
@@ -2,13 +2,11 @@
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue'; import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
import Spinner from 'shared/components/Spinner.vue'; import Spinner from 'shared/components/Spinner.vue';
import NextButton from 'dashboard/components-next/button/Button.vue';
export default { export default {
components: { components: {
Thumbnail, Thumbnail,
Spinner, Spinner,
NextButton,
}, },
props: { props: {
selectedInboxes: { selectedInboxes: {
@@ -96,7 +94,13 @@ export default {
</div> </div>
<div class="flex items-center justify-between header"> <div class="flex items-center justify-between header">
<span>{{ $t('BULK_ACTION.AGENT_SELECT_LABEL') }}</span> <span>{{ $t('BULK_ACTION.AGENT_SELECT_LABEL') }}</span>
<NextButton ghost xs slate icon="i-lucide-x" @click="onClose" /> <woot-button
size="tiny"
variant="clear"
color-scheme="secondary"
icon="dismiss"
@click="onClose"
/>
</div> </div>
<div class="container"> <div class="container">
<div <div
@@ -117,7 +121,7 @@ export default {
v-model="query" v-model="query"
type="search" type="search"
:placeholder="$t('BULK_ACTION.SEARCH_INPUT_PLACEHOLDER')" :placeholder="$t('BULK_ACTION.SEARCH_INPUT_PLACEHOLDER')"
class="reset-base !outline-0 !text-sm agent--search_input" class="agent--search_input"
/> />
</div> </div>
</li> </li>
@@ -157,21 +161,21 @@ export default {
}} }}
</p> </p>
<div class="agent-confirmation-actions"> <div class="agent-confirmation-actions">
<NextButton <woot-button
faded color-scheme="primary"
sm variant="smooth"
slate
type="reset"
:label="$t('BULK_ACTION.GO_BACK_LABEL')"
@click="goBack" @click="goBack"
/> >
<NextButton {{ $t('BULK_ACTION.GO_BACK_LABEL') }}
sm </woot-button>
type="submit" <woot-button
:label="$t('BULK_ACTION.YES')" color-scheme="primary"
variant="flat"
:is-loading="uiFlags.isUpdating" :is-loading="uiFlags.isUpdating"
@click="submit" @click="submit"
/> >
{{ $t('BULK_ACTION.YES') }}
</woot-button>
</div> </div>
</div> </div>
</div> </div>
@@ -1,11 +1,7 @@
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import NextButton from 'dashboard/components-next/button/Button.vue';
export default { export default {
components: {
NextButton,
},
emits: ['update', 'close', 'assign'], emits: ['update', 'close', 'assign'],
data() { data() {
return { return {
@@ -44,7 +40,13 @@ export default {
</div> </div>
<div class="flex items-center justify-between header"> <div class="flex items-center justify-between header">
<span>{{ $t('BULK_ACTION.LABELS.ASSIGN_LABELS') }}</span> <span>{{ $t('BULK_ACTION.LABELS.ASSIGN_LABELS') }}</span>
<NextButton ghost xs slate icon="i-lucide-x" @click="onClose" /> <woot-button
size="tiny"
variant="clear"
color-scheme="secondary"
icon="dismiss"
@click="onClose"
/>
</div> </div>
<div class="labels-list"> <div class="labels-list">
<header class="labels-list__header"> <header class="labels-list__header">
@@ -56,7 +58,7 @@ export default {
v-model="query" v-model="query"
type="search" type="search"
:placeholder="$t('BULK_ACTION.SEARCH_INPUT_PLACEHOLDER')" :placeholder="$t('BULK_ACTION.SEARCH_INPUT_PLACEHOLDER')"
class="reset-base !outline-0 !text-sm label--search_input" class="label--search_input"
/> />
</div> </div>
</header> </header>
@@ -89,13 +91,15 @@ export default {
</li> </li>
</ul> </ul>
<footer class="labels-list__footer"> <footer class="labels-list__footer">
<NextButton <woot-button
sm size="small"
type="submit" is-expanded
:label="$t('BULK_ACTION.LABELS.ASSIGN_SELECTED_LABELS')" color-scheme="primary"
:disabled="!selectedLabels.length" :disabled="!selectedLabels.length"
@click="$emit('assign', selectedLabels)" @click="$emit('assign', selectedLabels)"
/> >
<span>{{ $t('BULK_ACTION.LABELS.ASSIGN_SELECTED_LABELS') }}</span>
</woot-button>
</footer> </footer>
</div> </div>
</div> </div>
@@ -1,11 +1,6 @@
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import NextButton from 'dashboard/components-next/button/Button.vue';
export default { export default {
components: {
NextButton,
},
emits: ['assignTeam', 'close'], emits: ['assignTeam', 'close'],
data() { data() {
@@ -45,7 +40,13 @@ export default {
</div> </div>
<div class="flex items-center justify-between header"> <div class="flex items-center justify-between header">
<span>{{ $t('BULK_ACTION.TEAMS.TEAM_SELECT_LABEL') }}</span> <span>{{ $t('BULK_ACTION.TEAMS.TEAM_SELECT_LABEL') }}</span>
<NextButton ghost xs slate icon="i-lucide-x" @click="onClose" /> <woot-button
size="tiny"
variant="clear"
color-scheme="secondary"
icon="dismiss"
@click="onClose"
/>
</div> </div>
<div class="container"> <div class="container">
<div class="team__list-container"> <div class="team__list-container">
@@ -59,7 +60,7 @@ export default {
v-model="query" v-model="query"
type="search" type="search"
:placeholder="$t('BULK_ACTION.SEARCH_INPUT_PLACEHOLDER')" :placeholder="$t('BULK_ACTION.SEARCH_INPUT_PLACEHOLDER')"
class="reset-base !outline-0 !text-sm agent--search_input" class="agent--search_input"
/> />
</div> </div>
</li> </li>
@@ -4,7 +4,6 @@ import { ref } from 'vue';
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue'; import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue'; import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
import Button from 'dashboard/components-next/button/Button.vue';
const props = defineProps({ const props = defineProps({
showResolve: { showResolve: {
@@ -26,9 +25,9 @@ const emit = defineEmits(['update', 'close']);
const { t } = useI18n(); const { t } = useI18n();
const actions = ref([ const actions = ref([
{ icon: 'i-lucide-check', key: 'resolved' }, { icon: 'checkmark', key: 'resolved' },
{ icon: 'i-lucide-redo', key: 'open' }, { icon: 'arrow-redo', key: 'open' },
{ icon: 'i-lucide-alarm-clock', key: 'snoozed' }, { icon: 'send-clock', key: 'snoozed' },
]); ]);
const updateConversations = key => { const updateConversations = key => {
@@ -86,21 +85,28 @@ const actionLabel = key => {
<span class="text-sm font-medium text-slate-600 dark:text-slate-100"> <span class="text-sm font-medium text-slate-600 dark:text-slate-100">
{{ $t('BULK_ACTION.UPDATE.CHANGE_STATUS') }} {{ $t('BULK_ACTION.UPDATE.CHANGE_STATUS') }}
</span> </span>
<Button ghost xs slate icon="i-lucide-x" @click="onClose" /> <woot-button
size="tiny"
variant="clear"
color-scheme="secondary"
icon="dismiss"
@click="onClose"
/>
</div> </div>
<div class="px-2.5 pt-0 pb-2.5"> <div class="px-2.5 pt-0 pb-2.5">
<WootDropdownMenu class="m-0 list-none"> <WootDropdownMenu class="m-0 list-none">
<template v-for="action in actions"> <template v-for="action in actions">
<WootDropdownItem v-if="showAction(action.key)" :key="action.key"> <WootDropdownItem v-if="showAction(action.key)" :key="action.key">
<Button <woot-button
ghost variant="clear"
sm color-scheme="secondary"
slate size="small"
class="!w-full ltr:!justify-start rtl:!justify-end"
:icon="action.icon" :icon="action.icon"
:label="actionLabel(action.key)" class="hover:!bg-n-slate-3 dark:hover:!bg-n-solid-3"
@click="updateConversations(action.key)" @click="updateConversations(action.key)"
/> >
{{ actionLabel(action.key) }}
</woot-button>
</WootDropdownItem> </WootDropdownItem>
</template> </template>
</WootDropdownMenu> </WootDropdownMenu>
@@ -9,7 +9,6 @@ import validations from './validations';
import { parseLinearAPIErrorResponse } from 'dashboard/store/utils/api'; import { parseLinearAPIErrorResponse } from 'dashboard/store/utils/api';
import SearchableDropdown from './SearchableDropdown.vue'; import SearchableDropdown from './SearchableDropdown.vue';
import { LINEAR_EVENTS } from 'dashboard/helper/AnalyticsHelper/events'; import { LINEAR_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
import Button from 'dashboard/components-next/button/Button.vue';
const props = defineProps({ const props = defineProps({
conversationId: { conversationId: {
@@ -249,20 +248,20 @@ onMounted(getTeams);
/> />
</div> </div>
<div class="flex items-center justify-end w-full gap-2 mt-8"> <div class="flex items-center justify-end w-full gap-2 mt-8">
<Button <woot-button
faded class="px-4 rounded-xl button clear outline-woot-200/50 outline"
slate
type="reset"
:label="$t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.CANCEL')"
@click.prevent="onClose" @click.prevent="onClose"
/> >
<Button {{ $t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.CANCEL') }}
type="submit" </woot-button>
:label="$t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.CREATE')" <woot-button
:disabled="isSubmitDisabled" :is-disabled="isSubmitDisabled"
class="px-4 rounded-xl"
:is-loading="isCreating" :is-loading="isCreating"
@click.prevent="createIssue" @click.prevent="createIssue"
/> >
{{ $t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.CREATE') }}
</woot-button>
</div> </div>
</div> </div>
</template> </template>
@@ -1,7 +1,5 @@
<script setup> <script setup>
import { inject } from 'vue'; import { inject } from 'vue';
import Button from 'dashboard/components-next/button/Button.vue';
const props = defineProps({ const props = defineProps({
identifier: { identifier: {
type: String, type: String,
@@ -39,24 +37,30 @@ const openIssue = () => {
/> />
<span class="text-xs font-medium text-ash-900">{{ identifier }}</span> <span class="text-xs font-medium text-ash-900">{{ identifier }}</span>
</div> </div>
<div class="flex items-center gap-1"> <div class="flex items-center gap-0.5">
<Button <woot-button
ghost variant="clear"
xs color-scheme="secondary"
slate class="h-[24px]"
icon="i-lucide-unlink"
class="!transition-none"
:is-loading="isUnlinking" :is-loading="isUnlinking"
@click="unlinkIssue" @click="unlinkIssue"
/> >
<Button <fluent-icon
ghost v-if="!isUnlinking"
xs icon="unlink"
slate size="12"
class="!transition-none" type="outline"
icon="i-lucide-arrow-up-right" icon-lib="lucide"
/>
</woot-button>
<woot-button
variant="clear"
class="h-[24px]"
color-scheme="secondary"
@click="openIssue" @click="openIssue"
/> >
<fluent-icon icon="arrow-up-right" size="14" />
</woot-button>
</div> </div>
</div> </div>
</template> </template>
@@ -8,7 +8,6 @@ import FilterButton from 'dashboard/components/ui/Dropdown/DropdownButton.vue';
import FilterListDropdown from 'dashboard/components/ui/Dropdown/DropdownList.vue'; import FilterListDropdown from 'dashboard/components/ui/Dropdown/DropdownList.vue';
import { parseLinearAPIErrorResponse } from 'dashboard/store/utils/api'; import { parseLinearAPIErrorResponse } from 'dashboard/store/utils/api';
import { LINEAR_EVENTS } from 'dashboard/helper/AnalyticsHelper/events'; import { LINEAR_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
import Button from 'dashboard/components-next/button/Button.vue';
const props = defineProps({ const props = defineProps({
conversationId: { conversationId: {
@@ -107,10 +106,9 @@ const linkIssue = async () => {
:class="shouldShowDropdown ? 'h-[256px]' : 'gap-2'" :class="shouldShowDropdown ? 'h-[256px]' : 'gap-2'"
> >
<FilterButton <FilterButton
trailing-icon right-icon="chevron-down"
icon="i-lucide-chevron-down"
:button-text="linkIssueTitle" :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" @click="toggleDropdown"
> >
<template v-if="shouldShowDropdown" #dropdown> <template v-if="shouldShowDropdown" #dropdown>
@@ -131,20 +129,20 @@ const linkIssue = async () => {
</template> </template>
</FilterButton> </FilterButton>
<div class="flex items-center justify-end w-full gap-2 mt-2"> <div class="flex items-center justify-end w-full gap-2 mt-2">
<Button <woot-button
faded class="px-4 rounded-xl button clear outline-woot-200/50 outline"
slate
type="reset"
:label="$t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.CANCEL')"
@click.prevent="onClose" @click.prevent="onClose"
/> >
<Button {{ $t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.CANCEL') }}
type="submit" </woot-button>
:label="$t('INTEGRATION_SETTINGS.LINEAR.LINK.TITLE')" <woot-button
:disabled="isSubmitDisabled" :is-disabled="isSubmitDisabled"
class="px-4 rounded-xl"
:is-loading="isLinking" :is-loading="isLinking"
@click.prevent="linkIssue" @click.prevent="linkIssue"
/> >
{{ $t('INTEGRATION_SETTINGS.LINEAR.LINK.TITLE') }}
</woot-button>
</div> </div>
</div> </div>
</template> </template>
@@ -50,10 +50,9 @@ const selectedItemId = computed(() => selectedItem.value?.id || null);
<label class="w-full" :class="{ error: hasError }"> <label class="w-full" :class="{ error: hasError }">
{{ label }} {{ label }}
<FilterButton <FilterButton
trailing-icon right-icon="chevron-down"
icon="i-lucide-chevron-down"
:button-text="selectedItemName" :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" @click="toggleDropdown"
> >
<template v-if="shouldShowDropdown" #dropdown> <template v-if="shouldShowDropdown" #dropdown>
@@ -9,7 +9,6 @@ import Issue from './Issue.vue';
import { useTrack } from 'dashboard/composables'; import { useTrack } from 'dashboard/composables';
import { LINEAR_EVENTS } from 'dashboard/helper/AnalyticsHelper/events'; import { LINEAR_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
import { parseLinearAPIErrorResponse } from 'dashboard/store/utils/api'; import { parseLinearAPIErrorResponse } from 'dashboard/store/utils/api';
import Button from 'dashboard/components-next/button/Button.vue';
const props = defineProps({ const props = defineProps({
conversationId: { conversationId: {
@@ -101,25 +100,23 @@ onMounted(() => {
<template> <template>
<div class="relative" :class="{ group: linkedIssue }"> <div class="relative" :class="{ group: linkedIssue }">
<Button <woot-button
v-on-clickaway="closeIssue" v-on-clickaway="closeIssue"
v-tooltip="tooltipText" v-tooltip="tooltipText"
sm variant="clear"
ghost color-scheme="secondary"
slate
class="!gap-1"
@click="openIssue" @click="openIssue"
> >
<fluent-icon <fluent-icon
icon="linear" icon="linear"
size="19" size="19"
class="text-[#5E6AD2] flex-shrink-0" class="text-[#5E6AD2]"
view-box="0 0 19 19" view-box="0 0 19 19"
/> />
<span v-if="linkedIssue" class="text-xs font-medium text-n-slate-11"> <span v-if="linkedIssue" class="text-xs font-medium text-ash-800">
{{ linkedIssue.issue.identifier }} {{ linkedIssue.issue.identifier }}
</span> </span>
</Button> </woot-button>
<Issue <Issue
v-if="linkedIssue" v-if="linkedIssue"
:issue="linkedIssue.issue" :issue="linkedIssue.issue"

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