Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
daacd6eda5 | ||
|
|
a4617b2dcb | ||
|
|
63041d743d | ||
|
|
687f7fd33a | ||
|
|
8b7c9173e2 |
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 |
@@ -173,7 +173,6 @@ gem 'pgvector'
|
||||
# Convert Website HTML to Markdown
|
||||
gem 'reverse_markdown'
|
||||
|
||||
gem 'iso-639'
|
||||
gem 'ruby-openai'
|
||||
|
||||
gem 'shopify_api'
|
||||
|
||||
@@ -378,8 +378,6 @@ GEM
|
||||
io-console (0.6.0)
|
||||
irb (1.7.2)
|
||||
reline (>= 0.3.6)
|
||||
iso-639 (0.3.8)
|
||||
csv
|
||||
jbuilder (2.11.5)
|
||||
actionview (>= 5.0.0)
|
||||
activesupport (>= 5.0.0)
|
||||
@@ -915,7 +913,6 @@ DEPENDENCIES
|
||||
hashie
|
||||
html2text
|
||||
image_processing
|
||||
iso-639
|
||||
jbuilder
|
||||
json_refs
|
||||
json_schemer
|
||||
|
||||
@@ -1,11 +1,22 @@
|
||||
<img src="./.github/screenshots/header.png#gh-light-mode-only" width="100%" alt="Header light mode"/>
|
||||
<img src="./.github/screenshots/header-dark.png#gh-dark-mode-only" width="100%" alt="Header dark 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="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
|
||||
|
||||
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>
|
||||
<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>
|
||||
</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>
|
||||
<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>
|
||||
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.
|
||||
|
||||
<img src="./.github/screenshots/dashboard.png#gh-light-mode-only" width="100%" alt="Chat dashboard dark mode"/>
|
||||
<img src="./.github/screenshots/dashboard-dark.png#gh-dark-mode-only" width="100%" alt="Chat dashboard"/>
|
||||
## Features
|
||||
|
||||
---
|
||||
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, Chatwoot’s 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).
|
||||
|
||||
### 💬 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.
|
||||
Other features include:
|
||||
|
||||
- **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 Chatwoot’s webhooks and APIs.
|
||||
- **Integrations**: Chatwoot natively integrates with Slack right now. Manage your conversations in Slack without logging into the dashboard.
|
||||
|
||||
## 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.
|
||||
|
||||
## 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.
|
||||
|
||||
## Contributors
|
||||
|
||||
## Contributors ✨
|
||||
|
||||
Thanks goes to all these [wonderful people](https://www.chatwoot.com/docs/contributors):
|
||||
|
||||
|
||||
@@ -12,11 +12,50 @@ class ContactInboxBuilder
|
||||
private
|
||||
|
||||
def generate_source_id
|
||||
ContactInbox::SourceIdService.new(
|
||||
contact: @contact,
|
||||
channel_type: @inbox.channel_type,
|
||||
medium: @inbox.channel.try(:medium)
|
||||
).generate
|
||||
case @inbox.channel_type
|
||||
when 'Channel::TwilioSms'
|
||||
twilio_source_id
|
||||
when 'Channel::Whatsapp'
|
||||
wa_source_id
|
||||
when 'Channel::Email'
|
||||
email_source_id
|
||||
when 'Channel::Sms'
|
||||
phone_source_id
|
||||
when 'Channel::Api', 'Channel::WebWidget'
|
||||
SecureRandom.uuid
|
||||
else
|
||||
raise "Unsupported operation for this channel: #{@inbox.channel_type}"
|
||||
end
|
||||
end
|
||||
|
||||
def email_source_id
|
||||
raise ActionController::ParameterMissing, 'contact email' unless @contact.email
|
||||
|
||||
@contact.email
|
||||
end
|
||||
|
||||
def phone_source_id
|
||||
raise ActionController::ParameterMissing, 'contact phone number' unless @contact.phone_number
|
||||
|
||||
@contact.phone_number
|
||||
end
|
||||
|
||||
def wa_source_id
|
||||
raise ActionController::ParameterMissing, 'contact phone number' unless @contact.phone_number
|
||||
|
||||
# whatsapp doesn't want the + in e164 format
|
||||
@contact.phone_number.delete('+').to_s
|
||||
end
|
||||
|
||||
def twilio_source_id
|
||||
raise ActionController::ParameterMissing, 'contact phone number' unless @contact.phone_number
|
||||
|
||||
case @inbox.channel.medium
|
||||
when 'sms'
|
||||
@contact.phone_number
|
||||
when 'whatsapp'
|
||||
"whatsapp:#{@contact.phone_number}"
|
||||
end
|
||||
end
|
||||
|
||||
def create_contact_inbox
|
||||
@@ -25,40 +64,5 @@ class ContactInboxBuilder
|
||||
inbox_id: @inbox.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 there’s a contact inbox with the
|
||||
# same source ID but linked to a different contact. This can happen
|
||||
# if the agent updates the contact’s 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?
|
||||
"#{@source_id}#{rand(100)}"
|
||||
else
|
||||
"#{rand(10)}#{@source_id}"
|
||||
end
|
||||
end
|
||||
|
||||
def allowed_channels?
|
||||
@inbox.email? || @inbox.sms? || @inbox.twilio? || @inbox.whatsapp?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -9,8 +9,6 @@ class Api::V1::Accounts::Contacts::ContactInboxesController < Api::V1::Accounts:
|
||||
source_id: params[:source_id],
|
||||
hmac_verified: hmac_verified?
|
||||
).perform
|
||||
rescue ArgumentError => e
|
||||
render json: { error: e.message }, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
module Database::ConnectionDiagnostics
|
||||
extend ActiveSupport::Concern
|
||||
include Database::QueryInspection
|
||||
include Database::QueryLogging
|
||||
|
||||
private
|
||||
|
||||
def log_connection_pool_stats(connection_pool)
|
||||
{
|
||||
pool_size: connection_pool.size,
|
||||
active_connections: connection_pool.connections.count(&:in_use?),
|
||||
total_connections: connection_pool.connections.count,
|
||||
waiting_threads: connection_pool.num_waiting_in_queue,
|
||||
checkout_timeout: connection_pool.checkout_timeout
|
||||
}
|
||||
end
|
||||
|
||||
def connection_status_sql
|
||||
<<~SQL.squish
|
||||
SELECT count(*) as connection_count, state#{' '}
|
||||
FROM pg_stat_activity#{' '}
|
||||
GROUP BY state;
|
||||
SQL
|
||||
end
|
||||
|
||||
def fetch_connection_diagnostics
|
||||
{
|
||||
active_queries: fetch_active_queries,
|
||||
locked_queries: fetch_locked_queries,
|
||||
connection_status: ActiveRecord::Base.connection.execute(connection_status_sql).to_a
|
||||
}
|
||||
rescue StandardError => e
|
||||
Rails.logger.error "Error fetching active query data: #{e.message}"
|
||||
{ active_queries: [], locked_queries: [] }
|
||||
end
|
||||
|
||||
def caller_info(exception)
|
||||
{
|
||||
process_id: Process.pid,
|
||||
thread_id: Thread.current.object_id,
|
||||
backtrace: exception.backtrace&.first(15) || []
|
||||
}
|
||||
end
|
||||
|
||||
def log_timeout_error(exception, connection_info)
|
||||
Rails.logger.error "ActiveRecord::ConnectionTimeoutError: #{exception.message}"
|
||||
Rails.logger.error "Connection Pool Stats: #{connection_info.except(:active_queries, :locked_queries).inspect}"
|
||||
log_active_queries(connection_info[:active_queries])
|
||||
log_locked_queries(connection_info[:locked_queries])
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,91 @@
|
||||
module Database::QueryInspection
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
private
|
||||
|
||||
def active_queries_sql
|
||||
<<~SQL.squish
|
||||
SELECT pid,#{' '}
|
||||
now() - pg_stat_activity.query_start AS duration,
|
||||
query,
|
||||
state,
|
||||
wait_event_type,
|
||||
wait_event,
|
||||
backend_type,
|
||||
application_name,
|
||||
client_addr,
|
||||
usename
|
||||
FROM pg_stat_activity#{' '}
|
||||
WHERE state <> 'idle'
|
||||
AND query NOT ILIKE '%pg_stat_activity%'
|
||||
ORDER BY duration DESC;
|
||||
SQL
|
||||
end
|
||||
|
||||
def process_active_query_row(row)
|
||||
{
|
||||
pid: row['pid'],
|
||||
duration: row['duration'].to_s,
|
||||
state: row['state'],
|
||||
query: row['query'],
|
||||
wait_event_type: row['wait_event_type'],
|
||||
wait_event: row['wait_event'],
|
||||
backend_type: row['backend_type'],
|
||||
application_name: row['application_name'],
|
||||
client_addr: row['client_addr'],
|
||||
username: row['usename']
|
||||
}
|
||||
end
|
||||
|
||||
def fetch_active_queries
|
||||
query_data = ActiveRecord::Base.connection.execute(active_queries_sql)
|
||||
query_data.map { |row| process_active_query_row(row) }
|
||||
end
|
||||
|
||||
def locked_queries_sql
|
||||
<<~SQL.squish
|
||||
SELECT blocked_locks.pid AS blocked_pid,
|
||||
blocked_activity.usename AS blocked_user,
|
||||
blocking_locks.pid AS blocking_pid,
|
||||
blocking_activity.usename AS blocking_user,
|
||||
blocked_activity.query AS blocked_statement,
|
||||
blocking_activity.query AS blocking_statement,
|
||||
now() - blocking_activity.query_start AS blocking_duration
|
||||
FROM pg_catalog.pg_locks blocked_locks
|
||||
JOIN pg_catalog.pg_stat_activity blocked_activity ON blocked_activity.pid = blocked_locks.pid
|
||||
JOIN pg_catalog.pg_locks blocking_locks#{' '}
|
||||
ON blocking_locks.locktype = blocked_locks.locktype
|
||||
AND blocking_locks.DATABASE IS NOT DISTINCT FROM blocked_locks.DATABASE
|
||||
AND blocking_locks.relation IS NOT DISTINCT FROM blocked_locks.relation
|
||||
AND blocking_locks.page IS NOT DISTINCT FROM blocked_locks.page
|
||||
AND blocking_locks.tuple IS NOT DISTINCT FROM blocked_locks.tuple
|
||||
AND blocking_locks.virtualxid IS NOT DISTINCT FROM blocked_locks.virtualxid
|
||||
AND blocking_locks.transactionid IS NOT DISTINCT FROM blocked_locks.transactionid
|
||||
AND blocking_locks.classid IS NOT DISTINCT FROM blocked_locks.classid
|
||||
AND blocking_locks.objid IS NOT DISTINCT FROM blocked_locks.objid
|
||||
AND blocking_locks.objsubid IS NOT DISTINCT FROM blocked_locks.objsubid
|
||||
AND blocking_locks.pid != blocked_locks.pid
|
||||
JOIN pg_catalog.pg_stat_activity blocking_activity ON blocking_activity.pid = blocking_locks.pid
|
||||
WHERE NOT blocked_locks.granted;
|
||||
SQL
|
||||
end
|
||||
|
||||
def process_lock_row(row)
|
||||
{
|
||||
blocked_pid: row['blocked_pid'],
|
||||
blocked_user: row['blocked_user'],
|
||||
blocking_pid: row['blocking_pid'],
|
||||
blocking_user: row['blocking_user'],
|
||||
blocked_statement: row['blocked_statement'],
|
||||
blocking_statement: row['blocking_statement'],
|
||||
blocking_duration: row['blocking_duration'].to_s
|
||||
}
|
||||
end
|
||||
|
||||
def fetch_locked_queries
|
||||
lock_data = ActiveRecord::Base.connection.execute(locked_queries_sql)
|
||||
return [] if lock_data.count.zero?
|
||||
|
||||
lock_data.map { |row| process_lock_row(row) }
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,33 @@
|
||||
module Database::QueryLogging
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
private
|
||||
|
||||
def log_query_details(query_info, index)
|
||||
Rails.logger.error "Query ##{index + 1} [PID: #{query_info[:pid]}] [Duration: #{query_info[:duration]}] [State: #{query_info[:state]}]:"
|
||||
Rails.logger.error "App: #{query_info[:application_name]} User: #{query_info[:username]} Client: #{query_info[:client_addr]}"
|
||||
Rails.logger.error "Waiting: #{query_info[:wait_event_type]} / #{query_info[:wait_event]}" if query_info[:wait_event_type].present?
|
||||
Rails.logger.error query_info[:query]
|
||||
end
|
||||
|
||||
def log_active_queries(active_queries)
|
||||
if active_queries.any?
|
||||
Rails.logger.error "Active Database Queries (#{active_queries.count}):"
|
||||
active_queries.each_with_index { |query_info, index| log_query_details(query_info, index) }
|
||||
else
|
||||
Rails.logger.error 'No active queries found or unable to retrieve query information'
|
||||
end
|
||||
end
|
||||
|
||||
def log_locked_queries(locked_queries)
|
||||
return if locked_queries.blank?
|
||||
|
||||
Rails.logger.error "Locked Queries (#{locked_queries.count}):"
|
||||
locked_queries.each_with_index do |lock_info, index|
|
||||
Rails.logger.error "Lock ##{index + 1}: PID #{lock_info[:blocked_pid]} blocked by PID #{lock_info[:blocking_pid]}"
|
||||
Rails.logger.error "Duration: #{lock_info[:blocking_duration]}"
|
||||
Rails.logger.error "Blocked query: #{lock_info[:blocked_statement]}"
|
||||
Rails.logger.error "Blocking query: #{lock_info[:blocking_statement]}"
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,8 +1,10 @@
|
||||
module RequestExceptionHandler
|
||||
extend ActiveSupport::Concern
|
||||
include Database::ConnectionDiagnostics
|
||||
|
||||
included do
|
||||
rescue_from ActiveRecord::RecordInvalid, with: :render_record_invalid
|
||||
rescue_from ActiveRecord::ConnectionTimeoutError, with: :handle_connection_timeout
|
||||
end
|
||||
|
||||
private
|
||||
@@ -18,11 +20,36 @@ module RequestExceptionHandler
|
||||
rescue ActionController::ParameterMissing => e
|
||||
log_handled_error(e)
|
||||
render_could_not_create_error(e.message)
|
||||
rescue ActiveRecord::ConnectionTimeoutError => e
|
||||
handle_connection_timeout(e)
|
||||
ensure
|
||||
# to address the thread variable leak issues in Puma/Thin webserver
|
||||
Current.reset
|
||||
end
|
||||
|
||||
def handle_connection_timeout(exception)
|
||||
connection_pool = ActiveRecord::Base.connection_pool
|
||||
connection_info = log_connection_pool_stats(connection_pool)
|
||||
|
||||
# Gather diagnostic info
|
||||
diagnostics = fetch_connection_diagnostics
|
||||
connection_info.merge!(diagnostics)
|
||||
connection_info[:caller_info] = caller_info(exception)
|
||||
|
||||
# Log error details
|
||||
log_timeout_error(exception, connection_info)
|
||||
|
||||
# Report to exception tracker
|
||||
ChatwootExceptionTracker.new(
|
||||
exception,
|
||||
user: Current.user,
|
||||
account: Current.account,
|
||||
additional_context: { connection_info: connection_info }
|
||||
).capture_exception
|
||||
|
||||
render_service_unavailable('Database connection timeout. Please try again later.')
|
||||
end
|
||||
|
||||
def render_unauthorized(message)
|
||||
render json: { error: message }, status: :unauthorized
|
||||
end
|
||||
@@ -43,6 +70,10 @@ module RequestExceptionHandler
|
||||
render json: { error: message }, status: :internal_server_error
|
||||
end
|
||||
|
||||
def render_service_unavailable(message)
|
||||
render json: { error: message }, status: :service_unavailable
|
||||
end
|
||||
|
||||
def render_record_invalid(exception)
|
||||
log_handled_error(exception)
|
||||
render json: {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
@apply inline-block h-6 py-0 px-6 relative align-middle w-6;
|
||||
|
||||
&.message {
|
||||
@include normal-shadow;
|
||||
@apply bg-white dark:bg-slate-800 rounded-full left-0 my-3 mx-auto p-4 top-0;
|
||||
|
||||
&::before {
|
||||
|
||||
@@ -1,7 +1,79 @@
|
||||
@import 'dashboard/assets/scss/variables';
|
||||
@import 'widget/assets/scss/mixins';
|
||||
|
||||
$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
|
||||
@mixin ghost-input() {
|
||||
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() {
|
||||
@keyframes spinner {
|
||||
to {
|
||||
@@ -99,3 +230,17 @@ $spinner-before-border-color: rgba(255, 255, 255, 0.7);
|
||||
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);
|
||||
}
|
||||
|
||||
+1
-1
@@ -55,13 +55,13 @@ defineExpose({ dialogRef, contactsFormRef, onSuccess });
|
||||
@click="closeDialog"
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
:label="
|
||||
t('CONTACTS_LAYOUT.HEADER.ACTIONS.CONTACT_CREATION.SAVE_CONTACT')
|
||||
"
|
||||
color="blue"
|
||||
:disabled="contactsFormRef?.isFormInvalid"
|
||||
:is-loading="isCreatingContact"
|
||||
@click="handleDialogConfirm"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
-5
@@ -25,11 +25,6 @@ export const generateLabelForContactableInboxesList = ({
|
||||
channelType === INBOX_TYPES.TWILIO ||
|
||||
channelType === INBOX_TYPES.WHATSAPP
|
||||
) {
|
||||
// Handled separately for Twilio Inbox where phone number is not mandatory.
|
||||
// You can send message to a contact with Messaging Service Id.
|
||||
if (!phoneNumber) {
|
||||
return name;
|
||||
}
|
||||
return `${name} (${phoneNumber})`;
|
||||
}
|
||||
return name;
|
||||
|
||||
+6
-13
@@ -8,8 +8,8 @@ vi.mock('dashboard/api/contacts');
|
||||
describe('composeConversationHelper', () => {
|
||||
describe('generateLabelForContactableInboxesList', () => {
|
||||
const contact = {
|
||||
name: 'Priority Inbox',
|
||||
email: 'hello@example.com',
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
phoneNumber: '+1234567890',
|
||||
};
|
||||
|
||||
@@ -19,7 +19,7 @@ describe('composeConversationHelper', () => {
|
||||
...contact,
|
||||
channelType: INBOX_TYPES.EMAIL,
|
||||
})
|
||||
).toBe('Priority Inbox (hello@example.com)');
|
||||
).toBe('John Doe (john@example.com)');
|
||||
});
|
||||
|
||||
it('generates label for twilio inbox', () => {
|
||||
@@ -28,14 +28,7 @@ describe('composeConversationHelper', () => {
|
||||
...contact,
|
||||
channelType: INBOX_TYPES.TWILIO,
|
||||
})
|
||||
).toBe('Priority Inbox (+1234567890)');
|
||||
|
||||
expect(
|
||||
helpers.generateLabelForContactableInboxesList({
|
||||
name: 'Priority Inbox',
|
||||
channelType: INBOX_TYPES.TWILIO,
|
||||
})
|
||||
).toBe('Priority Inbox');
|
||||
).toBe('John Doe (+1234567890)');
|
||||
});
|
||||
|
||||
it('generates label for whatsapp inbox', () => {
|
||||
@@ -44,7 +37,7 @@ describe('composeConversationHelper', () => {
|
||||
...contact,
|
||||
channelType: INBOX_TYPES.WHATSAPP,
|
||||
})
|
||||
).toBe('Priority Inbox (+1234567890)');
|
||||
).toBe('John Doe (+1234567890)');
|
||||
});
|
||||
|
||||
it('generates label for other inbox types', () => {
|
||||
@@ -53,7 +46,7 @@ describe('composeConversationHelper', () => {
|
||||
...contact,
|
||||
channelType: 'Channel::Api',
|
||||
})
|
||||
).toBe('Priority Inbox');
|
||||
).toBe('John Doe');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -190,7 +190,7 @@ const STYLE_CONFIG = {
|
||||
const variantClasses = computed(() => {
|
||||
const variantMap = {
|
||||
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,
|
||||
faded: STYLE_CONFIG.colors[computedColor.value].faded,
|
||||
solid: STYLE_CONFIG.colors[computedColor.value].solid,
|
||||
|
||||
@@ -13,9 +13,6 @@ const attachment = computed(() => {
|
||||
|
||||
<template>
|
||||
<BaseBubble class="bg-transparent" data-bubble-name="audio">
|
||||
<AudioChip
|
||||
:attachment="attachment"
|
||||
class="p-2 text-n-slate-12 skip-context-menu"
|
||||
/>
|
||||
<AudioChip :attachment="attachment" class="p-2 text-n-slate-12" />
|
||||
</BaseBubble>
|
||||
</template>
|
||||
|
||||
@@ -56,7 +56,6 @@ const downloadAttachment = async () => {
|
||||
</div>
|
||||
<div v-else class="relative group rounded-lg overflow-hidden">
|
||||
<img
|
||||
class="skip-context-menu"
|
||||
:src="attachment.dataUrl"
|
||||
:width="attachment.width"
|
||||
:height="attachment.height"
|
||||
@@ -64,9 +63,8 @@ const downloadAttachment = async () => {
|
||||
@error="handleError"
|
||||
/>
|
||||
<div
|
||||
class="inset-0 p-2 pointer-events-none absolute bg-gradient-to-tl from-n-slate-12/30 dark:from-n-slate-1/50 via-transparent to-transparent hidden group-hover:flex"
|
||||
/>
|
||||
<div class="absolute right-2 bottom-2 hidden group-hover:flex gap-2">
|
||||
class="inset-0 p-2 absolute bg-gradient-to-tl from-n-slate-12/30 dark:from-n-slate-1/50 via-transparent to-transparent hidden group-hover:flex items-end justify-end gap-1.5"
|
||||
>
|
||||
<Button xs solid slate icon="i-lucide-expand" class="opacity-60" />
|
||||
<Button
|
||||
xs
|
||||
|
||||
@@ -41,13 +41,13 @@ const onVideoLoadError = () => {
|
||||
<div v-if="content" v-dompurify-html="formattedContent" class="mb-2" />
|
||||
<img
|
||||
v-if="!hasImgStoryError"
|
||||
class="rounded-lg max-w-80 skip-context-menu"
|
||||
class="rounded-lg max-w-80"
|
||||
:src="attachment.dataUrl"
|
||||
@error="onImageLoadError"
|
||||
/>
|
||||
<video
|
||||
v-else-if="!hasVideoStoryError"
|
||||
class="rounded-lg max-w-80 skip-context-menu"
|
||||
class="rounded-lg max-w-80"
|
||||
controls
|
||||
:src="attachment.dataUrl"
|
||||
@error="onVideoLoadError"
|
||||
|
||||
@@ -35,13 +35,13 @@ const isReel = computed(() => {
|
||||
<div class="relative group rounded-lg overflow-hidden">
|
||||
<div
|
||||
v-if="isReel"
|
||||
class="absolute p-2 flex items-start justify-end right-0 pointer-events-none"
|
||||
class="absolute p-2 flex items-start justify-end right-0"
|
||||
>
|
||||
<Icon icon="i-lucide-instagram" class="text-white shadow-lg" />
|
||||
</div>
|
||||
<video
|
||||
controls
|
||||
class="rounded-lg skip-context-menu"
|
||||
class="rounded-lg"
|
||||
:src="attachment.dataUrl"
|
||||
:class="{
|
||||
'max-w-48': isReel,
|
||||
|
||||
@@ -36,7 +36,7 @@ const handleError = () => {
|
||||
</div>
|
||||
<img
|
||||
v-else
|
||||
class="object-cover w-full h-full skip-context-menu"
|
||||
class="object-cover w-full h-full"
|
||||
:src="attachment.dataUrl"
|
||||
@error="handleError"
|
||||
/>
|
||||
|
||||
@@ -9,15 +9,12 @@ import { getRegexp } from 'shared/helpers/Validators';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
const DATE_FORMAT = 'yyyy-MM-dd';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MultiselectDropdown,
|
||||
HelperTextPopup,
|
||||
NextButton,
|
||||
},
|
||||
props: {
|
||||
label: { type: String, required: true },
|
||||
@@ -222,13 +219,14 @@ export default {
|
||||
class="mt-0.5"
|
||||
/>
|
||||
</span>
|
||||
<NextButton
|
||||
<woot-button
|
||||
v-if="showActions && value"
|
||||
v-tooltip.left="$t('CUSTOM_ATTRIBUTES.ACTIONS.DELETE')"
|
||||
slate
|
||||
sm
|
||||
link
|
||||
icon="i-lucide-trash-2"
|
||||
variant="link"
|
||||
size="medium"
|
||||
color-scheme="secondary"
|
||||
icon="delete"
|
||||
class-names="flex justify-end !w-fit"
|
||||
@click="onDelete"
|
||||
/>
|
||||
</div>
|
||||
@@ -248,10 +246,10 @@ export default {
|
||||
@keyup.enter="onUpdate"
|
||||
/>
|
||||
<div>
|
||||
<NextButton
|
||||
sm
|
||||
icon="i-lucide-check"
|
||||
class="ltr:rounded-l-none rtl:rounded-r-none h-[34px]"
|
||||
<woot-button
|
||||
size="small"
|
||||
icon="checkmark"
|
||||
class="ltr:rounded-l-none rtl:rounded-r-none"
|
||||
@click="onUpdate"
|
||||
/>
|
||||
</div>
|
||||
@@ -283,27 +281,25 @@ export default {
|
||||
>
|
||||
{{ displayValue || '---' }}
|
||||
</p>
|
||||
<div
|
||||
class="flex items-center max-w-[2rem] gap-1 ml-1 rtl:mr-1 rtl:ml-0"
|
||||
>
|
||||
<NextButton
|
||||
<div class="flex max-w-[2rem] gap-1 ml-1 rtl:mr-1 rtl:ml-0">
|
||||
<woot-button
|
||||
v-if="showActions && value"
|
||||
v-tooltip="$t('CUSTOM_ATTRIBUTES.ACTIONS.COPY')"
|
||||
xs
|
||||
slate
|
||||
ghost
|
||||
icon="i-lucide-clipboard"
|
||||
class="hidden group-hover:flex flex-shrink-0"
|
||||
variant="link"
|
||||
size="small"
|
||||
color-scheme="secondary"
|
||||
icon="clipboard"
|
||||
class-names="hidden group-hover:flex !w-6 flex-shrink-0"
|
||||
@click="onCopy"
|
||||
/>
|
||||
<NextButton
|
||||
<woot-button
|
||||
v-if="showActions"
|
||||
v-tooltip.right="$t('CUSTOM_ATTRIBUTES.ACTIONS.EDIT')"
|
||||
xs
|
||||
slate
|
||||
ghost
|
||||
icon="i-lucide-pen"
|
||||
class="hidden group-hover:flex flex-shrink-0"
|
||||
variant="link"
|
||||
size="small"
|
||||
color-scheme="secondary"
|
||||
icon="edit"
|
||||
class-names="hidden group-hover:flex !w-6 flex-shrink-0"
|
||||
@click="onEdit"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
<script>
|
||||
import DatePicker from 'vue-datepicker-next';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DatePicker,
|
||||
NextButton,
|
||||
},
|
||||
emits: ['close', 'chooseTime'],
|
||||
|
||||
@@ -54,23 +52,18 @@ export default {
|
||||
v-model:value="snoozeTime"
|
||||
type="datetime"
|
||||
inline
|
||||
input-class="mx-input "
|
||||
input-class="mx-input reset-base"
|
||||
:lang="lang"
|
||||
:disabled-date="disabledDate"
|
||||
:disabled-time="disabledTime"
|
||||
/>
|
||||
<div class="flex flex-row justify-end w-full gap-2 px-0 py-2">
|
||||
<NextButton
|
||||
faded
|
||||
slate
|
||||
type="reset"
|
||||
:label="$t('CONVERSATION.CUSTOM_SNOOZE.CANCEL')"
|
||||
@click.prevent="onClose"
|
||||
/>
|
||||
<NextButton
|
||||
type="submit"
|
||||
:label="$t('CONVERSATION.CUSTOM_SNOOZE.APPLY')"
|
||||
/>
|
||||
<woot-button variant="clear" @click.prevent="onClose">
|
||||
{{ $t('CONVERSATION.CUSTOM_SNOOZE.CANCEL') }}
|
||||
</woot-button>
|
||||
<woot-button>
|
||||
{{ $t('CONVERSATION.CUSTOM_SNOOZE.APPLY') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// [TODO] Use Teleport to move the modal to the end of the body
|
||||
import { ref, computed, defineEmits, onMounted } from 'vue';
|
||||
import { useEventListener } from '@vueuse/core';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
const { modalType, closeOnBackdropClick, onClose } = defineProps({
|
||||
closeOnBackdropClick: { type: Boolean, default: true },
|
||||
@@ -86,11 +85,11 @@ onMounted(() => {
|
||||
@mouse.stop
|
||||
@mousedown="event => event.stopPropagation()"
|
||||
>
|
||||
<Button
|
||||
<woot-button
|
||||
v-if="showCloseButton"
|
||||
ghost
|
||||
slate
|
||||
icon="i-lucide-x"
|
||||
color-scheme="secondary"
|
||||
icon="dismiss"
|
||||
variant="clear"
|
||||
class="absolute z-10 ltr:right-2 rtl:left-2 top-2"
|
||||
@click="close"
|
||||
/>
|
||||
|
||||
@@ -11,8 +11,6 @@ import {
|
||||
} from 'dashboard/helper/routeHelpers';
|
||||
import { useEventListener } from '@vueuse/core';
|
||||
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
const route = useRoute();
|
||||
|
||||
@@ -109,31 +107,34 @@ onBeforeUnmount(() => {
|
||||
|
||||
<template>
|
||||
<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
|
||||
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" />
|
||||
<span class="px-2 text-xs font-medium tracking-wide text-n-amber-12">
|
||||
<fluent-icon
|
||||
: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 }}
|
||||
</span>
|
||||
<Button
|
||||
<woot-button
|
||||
v-if="canRefresh"
|
||||
ghost
|
||||
sm
|
||||
amber
|
||||
icon="i-lucide-refresh-ccw"
|
||||
: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"
|
||||
/>
|
||||
|
||||
<Button
|
||||
ghost
|
||||
sm
|
||||
amber
|
||||
icon="i-lucide-x"
|
||||
class="!text-n-amber-12 dark:!text-n-amber-9"
|
||||
<woot-button
|
||||
variant="clear"
|
||||
size="small"
|
||||
color-scheme="warning"
|
||||
icon="dismiss"
|
||||
@click="closeNotification"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
@@ -2,12 +2,10 @@
|
||||
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||
import { useAI } from 'dashboard/composables/useAI';
|
||||
import AILoader from './AILoader.vue';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AILoader,
|
||||
NextButton,
|
||||
},
|
||||
props: {
|
||||
aiOption: {
|
||||
@@ -86,22 +84,16 @@ export default {
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row justify-end w-full gap-2 px-0 py-2">
|
||||
<NextButton
|
||||
faded
|
||||
slate
|
||||
type="reset"
|
||||
:label="
|
||||
<woot-button variant="clear" @click.prevent="onClose">
|
||||
{{
|
||||
$t('INTEGRATION_SETTINGS.OPEN_AI.ASSISTANCE_MODAL.BUTTONS.CANCEL')
|
||||
"
|
||||
@click.prevent="onClose"
|
||||
/>
|
||||
<NextButton
|
||||
type="submit"
|
||||
:disabled="!generatedContent"
|
||||
:label="
|
||||
}}
|
||||
</woot-button>
|
||||
<woot-button :disabled="!generatedContent">
|
||||
{{
|
||||
$t('INTEGRATION_SETTINGS.OPEN_AI.ASSISTANCE_MODAL.BUTTONS.APPLY')
|
||||
"
|
||||
/>
|
||||
}}
|
||||
</woot-button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -6,12 +6,7 @@ import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import { useAI } from 'dashboard/composables/useAI';
|
||||
import { OPEN_AI_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NextButton,
|
||||
},
|
||||
emits: ['close'],
|
||||
|
||||
setup() {
|
||||
@@ -99,30 +94,16 @@ export default {
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-row justify-between w-full gap-2 px-0 py-2">
|
||||
<NextButton
|
||||
ghost
|
||||
type="button"
|
||||
class="!px-3"
|
||||
:label="
|
||||
$t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.BUTTONS.NEED_HELP')
|
||||
"
|
||||
@click.prevent="openOpenAIDoc"
|
||||
/>
|
||||
<woot-button variant="link" @click.prevent="openOpenAIDoc">
|
||||
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.BUTTONS.NEED_HELP') }}
|
||||
</woot-button>
|
||||
<div class="flex items-center gap-1">
|
||||
<NextButton
|
||||
faded
|
||||
slate
|
||||
type="reset"
|
||||
:label="
|
||||
$t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.BUTTONS.DISMISS')
|
||||
"
|
||||
@click.prevent="onDismiss"
|
||||
/>
|
||||
<NextButton
|
||||
type="submit"
|
||||
:disabled="v$.value.$invalid"
|
||||
:label="$t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.BUTTONS.FINISH')"
|
||||
/>
|
||||
<woot-button variant="clear" @click.prevent="onDismiss">
|
||||
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.BUTTONS.DISMISS') }}
|
||||
</woot-button>
|
||||
<woot-button :is-disabled="v$.value.$invalid">
|
||||
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.BUTTONS.FINISH') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
import { computed } from 'vue';
|
||||
import { formatBytes } from 'shared/helpers/FileHelper';
|
||||
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
const props = defineProps({
|
||||
attachments: {
|
||||
type: Array,
|
||||
@@ -75,11 +73,9 @@ const fileName = file => {
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-center">
|
||||
<Button
|
||||
ghost
|
||||
slate
|
||||
xs
|
||||
icon="i-lucide-x"
|
||||
<woot-button
|
||||
class="!w-6 !h-6 text-sm rounded-md hover:bg-slate-50 dark:hover:bg-slate-800 clear secondary"
|
||||
icon="dismiss"
|
||||
@click="onRemoveAttachment(index)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -2,14 +2,11 @@
|
||||
import AutomationActionTeamMessageInput from './AutomationActionTeamMessageInput.vue';
|
||||
import AutomationActionFileInput from './AutomationFileInput.vue';
|
||||
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AutomationActionTeamMessageInput,
|
||||
AutomationActionFileInput,
|
||||
WootMessageEditor,
|
||||
NextButton,
|
||||
},
|
||||
props: {
|
||||
modelValue: {
|
||||
@@ -175,11 +172,11 @@ export default {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<NextButton
|
||||
<woot-button
|
||||
v-if="!isMacro"
|
||||
icon="i-lucide-x"
|
||||
slate
|
||||
ghost
|
||||
icon="dismiss"
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
class="flex-shrink-0"
|
||||
@click="removeAction"
|
||||
/>
|
||||
|
||||
@@ -32,7 +32,7 @@ const buttonStyleClass = props.compact ? 'text-sm' : 'text-base';
|
||||
:class="buttonStyleClass"
|
||||
@click.capture="goBack"
|
||||
>
|
||||
<i class="i-lucide-chevron-left -ml-1 text-lg" />
|
||||
<fluent-icon icon="chevron-left" class="-ml-1" />
|
||||
{{ buttonLabel || $t('GENERAL_SETTINGS.BACK') }}
|
||||
</button>
|
||||
</template>
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
<script>
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
export default {
|
||||
name: 'FilterInput',
|
||||
components: {
|
||||
NextButton,
|
||||
},
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Object,
|
||||
@@ -247,11 +242,10 @@ export default {
|
||||
:placeholder="$t('FILTER.INPUT_PLACEHOLDER')"
|
||||
/>
|
||||
</div>
|
||||
<NextButton
|
||||
icon="i-lucide-x"
|
||||
slate
|
||||
ghost
|
||||
class="flex-shrink-0"
|
||||
<woot-button
|
||||
icon="dismiss"
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
@click="removeFilter"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -164,6 +164,11 @@ export default {
|
||||
'is-note-mode': this.isNote,
|
||||
};
|
||||
},
|
||||
buttonClass() {
|
||||
return {
|
||||
warning: this.isNote,
|
||||
};
|
||||
},
|
||||
showAttachButton() {
|
||||
return this.showFileUpload || this.isNote;
|
||||
},
|
||||
@@ -362,15 +367,14 @@ export default {
|
||||
/>
|
||||
</div>
|
||||
<div class="right-wrap">
|
||||
<NextButton
|
||||
:label="sendButtonText"
|
||||
type="submit"
|
||||
sm
|
||||
:color="isNote ? 'amber' : 'blue'"
|
||||
:disabled="isSendDisabled"
|
||||
class="flex-shrink-0"
|
||||
<woot-button
|
||||
size="small"
|
||||
:class-names="buttonClass"
|
||||
:is-disabled="isSendDisabled"
|
||||
@click="onSend"
|
||||
/>
|
||||
>
|
||||
{{ sendButtonText }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -13,8 +13,6 @@ import { snoozedReopenTime } from 'dashboard/helper/snoozeHelpers';
|
||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||
import Linear from './linear/index.vue';
|
||||
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
BackButton,
|
||||
@@ -23,7 +21,6 @@ export default {
|
||||
Thumbnail,
|
||||
SLACardLabel,
|
||||
Linear,
|
||||
NextButton,
|
||||
},
|
||||
mixins: [inboxMixin],
|
||||
props: {
|
||||
@@ -157,13 +154,16 @@ export default {
|
||||
<div
|
||||
class="flex flex-row items-center max-w-full gap-1 p-0 m-0 w-fit"
|
||||
>
|
||||
<NextButton link slate @click.prevent="$emit('contactPanelToggle')">
|
||||
<span
|
||||
class="text-base font-medium truncate leading-tight text-n-slate-12"
|
||||
>
|
||||
<woot-button
|
||||
variant="link"
|
||||
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 }}
|
||||
</span>
|
||||
</NextButton>
|
||||
</woot-button>
|
||||
<fluent-icon
|
||||
v-if="!isHMACVerified"
|
||||
v-tooltip="$t('CONVERSATION.UNVERIFIED_SESSION')"
|
||||
@@ -180,13 +180,14 @@ export default {
|
||||
<span v-if="isSnoozed" class="font-medium text-n-amber-10">
|
||||
{{ snoozedDisplayText }}
|
||||
</span>
|
||||
<NextButton
|
||||
link
|
||||
xs
|
||||
blue
|
||||
:label="contactPanelToggleText"
|
||||
<woot-button
|
||||
class="p-0"
|
||||
size="small"
|
||||
variant="link"
|
||||
@click="$emit('contactPanelToggle')"
|
||||
/>
|
||||
>
|
||||
{{ contactPanelToggleText }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -25,8 +25,6 @@ import * as Sentry from '@sentry/vue';
|
||||
import { useTrack } from 'dashboard/composables';
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
BubbleActions,
|
||||
@@ -42,7 +40,6 @@ export default {
|
||||
InstagramStory,
|
||||
InstagramStoryReply,
|
||||
Spinner,
|
||||
NextButton,
|
||||
},
|
||||
props: {
|
||||
data: {
|
||||
@@ -455,12 +452,12 @@ export default {
|
||||
v-if="isFailed && !hasOneDayPassed && !isAnEmailInbox"
|
||||
class="message-failed--alert"
|
||||
>
|
||||
<NextButton
|
||||
<woot-button
|
||||
v-tooltip.top-end="$t('CONVERSATION.TRY_AGAIN')"
|
||||
ghost
|
||||
xs
|
||||
ruby
|
||||
icon="i-lucide-refresh-ccw"
|
||||
size="tiny"
|
||||
color-scheme="alert"
|
||||
variant="clear"
|
||||
icon="arrow-clockwise"
|
||||
@click="retrySendMessage"
|
||||
/>
|
||||
</div>
|
||||
@@ -663,10 +660,10 @@ export default {
|
||||
}
|
||||
|
||||
&.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 {
|
||||
@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 {
|
||||
@apply flex items-end ltr:ml-auto rtl:mr-auto;
|
||||
@apply flex items-end ml-auto;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+13
-13
@@ -1,11 +1,10 @@
|
||||
<script setup>
|
||||
import { useRouter } from 'vue-router';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const openProfileSettings = () => {
|
||||
return router.push({ name: 'profile_settings_index' });
|
||||
<script>
|
||||
export default {
|
||||
methods: {
|
||||
openProfileSettings() {
|
||||
return this.$router.push({ name: 'profile_settings_index' });
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -15,12 +14,13 @@ const openProfileSettings = () => {
|
||||
>
|
||||
<p class="w-fit !m-0">
|
||||
{{ $t('CONVERSATION.FOOTER.MESSAGE_SIGNATURE_NOT_CONFIGURED') }}
|
||||
|
||||
<Button
|
||||
link
|
||||
:label="$t('CONVERSATION.FOOTER.CLICK_HERE')"
|
||||
<woot-button
|
||||
color-scheme="primary"
|
||||
variant="link"
|
||||
@click="openProfileSettings"
|
||||
/>
|
||||
>
|
||||
{{ $t('CONVERSATION.FOOTER.CLICK_HERE') }}
|
||||
</woot-button>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -37,8 +37,6 @@ import wootConstants from 'dashboard/constants/globals';
|
||||
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
|
||||
import { FEATURE_FLAGS } from '../../../featureFlags';
|
||||
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Message,
|
||||
@@ -46,7 +44,6 @@ export default {
|
||||
ReplyBox,
|
||||
Banner,
|
||||
ConversationLabelSuggestion,
|
||||
NextButton,
|
||||
},
|
||||
mixins: [inboxMixin],
|
||||
props: {
|
||||
@@ -491,15 +488,13 @@ export default {
|
||||
:href-link-text="replyWindowLinkText"
|
||||
/>
|
||||
<div class="flex justify-end">
|
||||
<NextButton
|
||||
faded
|
||||
xs
|
||||
slate
|
||||
class="!rounded-r-none rtl:rotate-180 !rounded-2xl !fixed z-10"
|
||||
:icon="
|
||||
isContactPanelOpen ? 'i-ph-caret-right-fill' : 'i-ph-caret-left-fill'
|
||||
"
|
||||
<woot-button
|
||||
variant="smooth"
|
||||
size="tiny"
|
||||
color-scheme="secondary"
|
||||
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"
|
||||
:class="isInboxView ? 'top-52 md:top-40' : 'top-32'"
|
||||
:icon="isRightOrLeftIcon"
|
||||
@click="onToggleContactPanel"
|
||||
/>
|
||||
</div>
|
||||
@@ -619,6 +614,20 @@ export default {
|
||||
</div>
|
||||
</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">
|
||||
.modal-mask {
|
||||
@apply absolute;
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
<script setup>
|
||||
<script>
|
||||
import MessagePreview from 'dashboard/components/widgets/conversation/MessagePreview.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
defineProps({
|
||||
message: {
|
||||
type: Object,
|
||||
required: true,
|
||||
export default {
|
||||
components: { MessagePreview },
|
||||
props: {
|
||||
message: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['dismiss']);
|
||||
emits: ['dismiss'],
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<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" />
|
||||
<div class="flex-grow gap-1 mt-px text-xs truncate">
|
||||
@@ -26,13 +27,14 @@ const emit = defineEmits(['dismiss']);
|
||||
class="inline"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
<woot-button
|
||||
v-tooltip="$t('CONVERSATION.REPLYBOX.DISMISS_REPLY')"
|
||||
ghost
|
||||
xs
|
||||
slate
|
||||
icon="i-lucide-x"
|
||||
@click.stop="emit('dismiss')"
|
||||
color-scheme="secondary"
|
||||
icon="dismiss"
|
||||
variant="clear"
|
||||
size="tiny"
|
||||
class="flex-shrink-0"
|
||||
@click.stop="$emit('dismiss')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
+55
-29
@@ -12,12 +12,7 @@ import { ref, computed, onMounted } from 'vue';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { requiredIf } from '@vuelidate/validators';
|
||||
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NextButton,
|
||||
},
|
||||
props: {
|
||||
template: {
|
||||
type: Object,
|
||||
@@ -119,47 +114,78 @@ export default {
|
||||
readonly
|
||||
class="template-input"
|
||||
/>
|
||||
<div v-if="variables" class="p-2.5">
|
||||
<p class="text-sm font-semibold mb-2.5">
|
||||
<div v-if="variables" class="template__variables-container">
|
||||
<p class="variables-label">
|
||||
{{ $t('WHATSAPP_TEMPLATES.PARSER.VARIABLES_LABEL') }}
|
||||
</p>
|
||||
<div
|
||||
v-for="(variable, key) in processedParams"
|
||||
:key="key"
|
||||
class="items-center flex mb-2.5"
|
||||
class="template__variable-item"
|
||||
>
|
||||
<span
|
||||
class="bg-n-alpha-black2 text-n-slate-12 inline-block rounded-md text-xs py-2.5 px-6"
|
||||
>
|
||||
<span class="variable-label">
|
||||
{{ key }}
|
||||
</span>
|
||||
<woot-input
|
||||
v-model="processedParams[key]"
|
||||
type="text"
|
||||
class="flex-1 text-sm ml-2.5"
|
||||
class="variable-input"
|
||||
:styles="{ marginBottom: 0 }"
|
||||
/>
|
||||
</div>
|
||||
<p
|
||||
v-if="v$.$dirty && v$.$invalid"
|
||||
class="bg-n-ruby-9/20 rounded-md text-n-ruby-9 p-2.5 text-center"
|
||||
>
|
||||
<p v-if="v$.$dirty && v$.$invalid" class="error">
|
||||
{{ $t('WHATSAPP_TEMPLATES.PARSER.FORM_ERROR_MESSAGE') }}
|
||||
</p>
|
||||
</div>
|
||||
<footer class="flex justify-end gap-2">
|
||||
<NextButton
|
||||
faded
|
||||
slate
|
||||
type="reset"
|
||||
:label="$t('WHATSAPP_TEMPLATES.PARSER.GO_BACK_LABEL')"
|
||||
@click="resetTemplate"
|
||||
/>
|
||||
<NextButton
|
||||
type="button"
|
||||
:label="$t('WHATSAPP_TEMPLATES.PARSER.SEND_MESSAGE_LABEL')"
|
||||
@click="sendMessage"
|
||||
/>
|
||||
<footer>
|
||||
<woot-button variant="smooth" @click="resetTemplate">
|
||||
{{ $t('WHATSAPP_TEMPLATES.PARSER.GO_BACK_LABEL') }}
|
||||
</woot-button>
|
||||
<woot-button type="button" @click="sendMessage">
|
||||
{{ $t('WHATSAPP_TEMPLATES.PARSER.SEND_MESSAGE_LABEL') }}
|
||||
</woot-button>
|
||||
</footer>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
+52
-21
@@ -43,56 +43,48 @@ export default {
|
||||
|
||||
<template>
|
||||
<div class="w-full">
|
||||
<div
|
||||
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="text-n-slate-12" size="16" />
|
||||
<div class="gap-1 templates__list-search">
|
||||
<fluent-icon icon="search" class="search-icon" size="16" />
|
||||
<input
|
||||
v-model="query"
|
||||
type="search"
|
||||
: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
|
||||
class="bg-n-background outline-n-container outline outline-1 rounded-lg max-h-[18.75rem] overflow-y-auto p-2.5"
|
||||
>
|
||||
<div class="template__list-container">
|
||||
<div v-for="(template, i) in filteredTemplateMessages" :key="template.id">
|
||||
<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)"
|
||||
>
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-2.5">
|
||||
<p class="text-sm">
|
||||
<p class="label-title">
|
||||
{{ template.name }}
|
||||
</p>
|
||||
<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') }} :
|
||||
{{ template.language }}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-medium">
|
||||
<p class="strong">
|
||||
{{ $t('WHATSAPP_TEMPLATES.PICKER.LABELS.TEMPLATE_BODY') }}
|
||||
</p>
|
||||
<p class="label-body">{{ getTemplatebody(template) }}</p>
|
||||
</div>
|
||||
<div class="mt-5">
|
||||
<p class="font-medium">
|
||||
<div class="label-category">
|
||||
<p class="strong">
|
||||
{{ $t('WHATSAPP_TEMPLATES.PICKER.LABELS.CATEGORY') }}
|
||||
</p>
|
||||
<p>{{ template.category }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<hr
|
||||
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%]"
|
||||
/>
|
||||
<hr v-if="i != filteredTemplateMessages.length - 1" :key="`hr-${i}`" />
|
||||
</div>
|
||||
<div v-if="!filteredTemplateMessages.length">
|
||||
<p>
|
||||
@@ -105,7 +97,46 @@ export default {
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.label-body {
|
||||
font-family: monospace;
|
||||
.templates__list-search {
|
||||
@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>
|
||||
|
||||
+169
-171
@@ -166,190 +166,188 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<woot-modal
|
||||
v-model:show="show"
|
||||
full-width
|
||||
:show-close-button="false"
|
||||
:on-close="onClose"
|
||||
<woot-modal
|
||||
v-model:show="show"
|
||||
full-width
|
||||
:show-close-button="false"
|
||||
:on-close="onClose"
|
||||
>
|
||||
<div
|
||||
class="bg-n-background flex flex-col h-[inherit] w-[inherit] overflow-hidden select-none"
|
||||
@click="onClose"
|
||||
>
|
||||
<div
|
||||
class="bg-n-background flex flex-col h-[inherit] w-[inherit] overflow-hidden select-none"
|
||||
@click="onClose"
|
||||
<header
|
||||
class="z-10 flex items-center justify-between w-full h-16 px-6 py-2 bg-n-background border-b border-n-weak"
|
||||
@click.stop
|
||||
>
|
||||
<header
|
||||
class="z-10 flex items-center justify-between w-full h-16 px-6 py-2 bg-n-background border-b border-n-weak"
|
||||
@click.stop
|
||||
<div
|
||||
v-if="senderDetails"
|
||||
class="flex items-center min-w-[15rem] shrink-0"
|
||||
>
|
||||
<div
|
||||
v-if="senderDetails"
|
||||
class="flex items-center min-w-[15rem] shrink-0"
|
||||
>
|
||||
<Thumbnail
|
||||
v-if="senderDetails.avatar"
|
||||
:username="senderDetails.name"
|
||||
:src="senderDetails.avatar"
|
||||
class="flex-shrink-0"
|
||||
/>
|
||||
<div class="flex flex-col ml-2 rtl:ml-0 rtl:mr-2 overflow-hidden">
|
||||
<h3 class="text-base leading-5 m-0 font-medium">
|
||||
<span
|
||||
class="overflow-hidden text-n-slate-12 whitespace-nowrap text-ellipsis"
|
||||
>
|
||||
{{ senderDetails.name }}
|
||||
</span>
|
||||
</h3>
|
||||
<Thumbnail
|
||||
v-if="senderDetails.avatar"
|
||||
:username="senderDetails.name"
|
||||
:src="senderDetails.avatar"
|
||||
class="flex-shrink-0"
|
||||
/>
|
||||
<div class="flex flex-col ml-2 rtl:ml-0 rtl:mr-2 overflow-hidden">
|
||||
<h3 class="text-base leading-5 m-0 font-medium">
|
||||
<span
|
||||
class="text-xs text-n-slate-11 whitespace-nowrap text-ellipsis"
|
||||
class="overflow-hidden text-n-slate-12 whitespace-nowrap text-ellipsis"
|
||||
>
|
||||
{{ readableTime }}
|
||||
{{ senderDetails.name }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex-1 mx-2 px-2 truncate text-sm font-medium text-center text-n-slate-12"
|
||||
>
|
||||
<span v-dompurify-html="fileNameFromDataUrl" class="truncate" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 ml-2 shrink-0">
|
||||
<NextButton
|
||||
v-if="isImage"
|
||||
icon="i-lucide-zoom-in"
|
||||
slate
|
||||
ghost
|
||||
@click="onZoom(0.1)"
|
||||
/>
|
||||
<NextButton
|
||||
v-if="isImage"
|
||||
icon="i-lucide-zoom-out"
|
||||
slate
|
||||
ghost
|
||||
@click="onZoom(-0.1)"
|
||||
/>
|
||||
<NextButton
|
||||
v-if="isImage"
|
||||
icon="i-lucide-rotate-ccw"
|
||||
slate
|
||||
ghost
|
||||
@click="onRotate('counter-clockwise')"
|
||||
/>
|
||||
<NextButton
|
||||
v-if="isImage"
|
||||
icon="i-lucide-rotate-cw"
|
||||
slate
|
||||
ghost
|
||||
@click="onRotate('clockwise')"
|
||||
/>
|
||||
<NextButton
|
||||
icon="i-lucide-download"
|
||||
slate
|
||||
ghost
|
||||
:is-loading="isDownloading"
|
||||
:disabled="isDownloading"
|
||||
@click="onClickDownload"
|
||||
/>
|
||||
<NextButton icon="i-lucide-x" slate ghost @click="onClose" />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="flex items-stretch flex-1 h-full overflow-hidden">
|
||||
<div class="flex items-center justify-center w-16 shrink-0">
|
||||
<NextButton
|
||||
v-if="hasMoreThanOneAttachment"
|
||||
icon="i-lucide-chevron-left"
|
||||
class="z-10"
|
||||
blue
|
||||
faded
|
||||
lg
|
||||
:disabled="activeImageIndex === 0"
|
||||
@click.stop="
|
||||
onClickChangeAttachment(
|
||||
allAttachments[activeImageIndex - 1],
|
||||
activeImageIndex - 1
|
||||
)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 flex items-center justify-center overflow-hidden">
|
||||
<div
|
||||
v-if="isImage"
|
||||
:style="imageWrapperStyle"
|
||||
class="flex items-center justify-center origin-center"
|
||||
:class="{
|
||||
// Adjust dimensions when rotated 90/270 degrees to maintain visibility
|
||||
// and prevent image from overflowing container in different aspect ratios
|
||||
'w-[calc(100dvh-8rem)] h-[calc(100dvw-7rem)]':
|
||||
activeImageRotation % 180 !== 0,
|
||||
'size-full': activeImageRotation % 180 === 0,
|
||||
}"
|
||||
</h3>
|
||||
<span
|
||||
class="text-xs text-n-slate-11 whitespace-nowrap text-ellipsis"
|
||||
>
|
||||
<img
|
||||
ref="imageRef"
|
||||
:key="activeAttachment.message_id"
|
||||
:src="activeAttachment.data_url"
|
||||
:style="imageStyle"
|
||||
class="max-h-full max-w-full object-contain duration-100 ease-in-out transform select-none"
|
||||
@click.stop
|
||||
@dblclick.stop="onDoubleClickZoomImage"
|
||||
@wheel.prevent.stop="onWheelImageZoom"
|
||||
@mousemove="onMouseMove"
|
||||
@mouseleave="onMouseLeave"
|
||||
/>
|
||||
</div>
|
||||
{{ readableTime }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<video
|
||||
v-if="isVideo"
|
||||
<div
|
||||
class="flex-1 mx-2 px-2 truncate text-sm font-medium text-center text-n-slate-12"
|
||||
>
|
||||
<span v-dompurify-html="fileNameFromDataUrl" class="truncate" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 ml-2 shrink-0">
|
||||
<NextButton
|
||||
v-if="isImage"
|
||||
icon="i-lucide-zoom-in"
|
||||
slate
|
||||
ghost
|
||||
@click="onZoom(0.1)"
|
||||
/>
|
||||
<NextButton
|
||||
v-if="isImage"
|
||||
icon="i-lucide-zoom-out"
|
||||
slate
|
||||
ghost
|
||||
@click="onZoom(-0.1)"
|
||||
/>
|
||||
<NextButton
|
||||
v-if="isImage"
|
||||
icon="i-lucide-rotate-ccw"
|
||||
slate
|
||||
ghost
|
||||
@click="onRotate('counter-clockwise')"
|
||||
/>
|
||||
<NextButton
|
||||
v-if="isImage"
|
||||
icon="i-lucide-rotate-cw"
|
||||
slate
|
||||
ghost
|
||||
@click="onRotate('clockwise')"
|
||||
/>
|
||||
<NextButton
|
||||
icon="i-lucide-download"
|
||||
slate
|
||||
ghost
|
||||
:is-loading="isDownloading"
|
||||
:disabled="isDownloading"
|
||||
@click="onClickDownload"
|
||||
/>
|
||||
<NextButton icon="i-lucide-x" slate ghost @click="onClose" />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="flex items-stretch flex-1 h-full overflow-hidden">
|
||||
<div class="flex items-center justify-center w-16 shrink-0">
|
||||
<NextButton
|
||||
v-if="hasMoreThanOneAttachment"
|
||||
icon="i-lucide-chevron-left"
|
||||
class="z-10"
|
||||
blue
|
||||
faded
|
||||
lg
|
||||
:disabled="activeImageIndex === 0"
|
||||
@click.stop="
|
||||
onClickChangeAttachment(
|
||||
allAttachments[activeImageIndex - 1],
|
||||
activeImageIndex - 1
|
||||
)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 flex items-center justify-center overflow-hidden">
|
||||
<div
|
||||
v-if="isImage"
|
||||
:style="imageWrapperStyle"
|
||||
class="flex items-center justify-center origin-center"
|
||||
:class="{
|
||||
// Adjust dimensions when rotated 90/270 degrees to maintain visibility
|
||||
// and prevent image from overflowing container in different aspect ratios
|
||||
'w-[calc(100dvh-8rem)] h-[calc(100dvw-7rem)]':
|
||||
activeImageRotation % 180 !== 0,
|
||||
'size-full': activeImageRotation % 180 === 0,
|
||||
}"
|
||||
>
|
||||
<img
|
||||
ref="imageRef"
|
||||
:key="activeAttachment.message_id"
|
||||
:src="activeAttachment.data_url"
|
||||
controls
|
||||
playsInline
|
||||
class="max-h-full max-w-full object-contain"
|
||||
:style="imageStyle"
|
||||
class="max-h-full max-w-full object-contain duration-100 ease-in-out transform select-none"
|
||||
@click.stop
|
||||
/>
|
||||
|
||||
<audio
|
||||
v-if="isAudio"
|
||||
:key="activeAttachment.message_id"
|
||||
controls
|
||||
class="w-full max-w-md"
|
||||
@click.stop
|
||||
>
|
||||
<source :src="`${activeAttachment.data_url}?t=${Date.now()}`" />
|
||||
</audio>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-center w-16 shrink-0">
|
||||
<NextButton
|
||||
v-if="hasMoreThanOneAttachment"
|
||||
icon="i-lucide-chevron-right"
|
||||
class="z-10"
|
||||
blue
|
||||
faded
|
||||
lg
|
||||
:disabled="activeImageIndex === allAttachments.length - 1"
|
||||
@click.stop="
|
||||
onClickChangeAttachment(
|
||||
allAttachments[activeImageIndex + 1],
|
||||
activeImageIndex + 1
|
||||
)
|
||||
"
|
||||
@dblclick.stop="onDoubleClickZoomImage"
|
||||
@wheel.prevent.stop="onWheelImageZoom"
|
||||
@mousemove="onMouseMove"
|
||||
@mouseleave="onMouseLeave"
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer
|
||||
class="z-10 flex items-center justify-center h-12 border-t border-n-weak"
|
||||
>
|
||||
<div
|
||||
class="rounded-md flex items-center justify-center px-3 py-1 bg-n-slate-3 text-n-slate-12 text-sm font-medium"
|
||||
<video
|
||||
v-if="isVideo"
|
||||
:key="activeAttachment.message_id"
|
||||
:src="activeAttachment.data_url"
|
||||
controls
|
||||
playsInline
|
||||
class="max-h-full max-w-full object-contain"
|
||||
@click.stop
|
||||
/>
|
||||
|
||||
<audio
|
||||
v-if="isAudio"
|
||||
:key="activeAttachment.message_id"
|
||||
controls
|
||||
class="w-full max-w-md"
|
||||
@click.stop
|
||||
>
|
||||
{{ `${activeImageIndex + 1} / ${allAttachments.length}` }}
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</woot-modal>
|
||||
</Teleport>
|
||||
<source :src="`${activeAttachment.data_url}?t=${Date.now()}`" />
|
||||
</audio>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-center w-16 shrink-0">
|
||||
<NextButton
|
||||
v-if="hasMoreThanOneAttachment"
|
||||
icon="i-lucide-chevron-right"
|
||||
class="z-10"
|
||||
blue
|
||||
faded
|
||||
lg
|
||||
:disabled="activeImageIndex === allAttachments.length - 1"
|
||||
@click.stop="
|
||||
onClickChangeAttachment(
|
||||
allAttachments[activeImageIndex + 1],
|
||||
activeImageIndex + 1
|
||||
)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer
|
||||
class="z-10 flex items-center justify-center h-12 border-t border-n-weak"
|
||||
>
|
||||
<div
|
||||
class="rounded-md flex items-center justify-center px-3 py-1 bg-n-slate-3 text-n-slate-12 text-sm font-medium"
|
||||
>
|
||||
{{ `${activeImageIndex + 1} / ${allAttachments.length}` }}
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</woot-modal>
|
||||
</template>
|
||||
|
||||
+11
-11
@@ -3,7 +3,6 @@ import { ref, computed } from 'vue';
|
||||
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import SLAEventItem from './SLAEventItem.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
const props = defineProps({
|
||||
slaMissedEvents: {
|
||||
@@ -61,19 +60,20 @@ const toggleShowAllNRT = () => {
|
||||
v-if="shouldShowMoreNRTButton"
|
||||
class="flex flex-col items-end w-full"
|
||||
>
|
||||
<Button
|
||||
link
|
||||
xs
|
||||
slate
|
||||
class="hover:!no-underline"
|
||||
:icon="!shouldShowAllNrts ? 'i-lucide-plus' : ''"
|
||||
:label="
|
||||
<woot-button
|
||||
size="small"
|
||||
:icon="!shouldShowAllNrts ? 'plus-sign' : ''"
|
||||
variant="link"
|
||||
color-scheme="secondary"
|
||||
class="hover:!no-underline !gap-1 hover:!bg-transparent dark:hover:!bg-transparent"
|
||||
@click="toggleShowAllNRT"
|
||||
>
|
||||
{{
|
||||
shouldShowAllNrts
|
||||
? $t('SLA.EVENTS.HIDE', { count: nrtMisses.length })
|
||||
: $t('SLA.EVENTS.SHOW_MORE', { count: nrtMisses.length })
|
||||
"
|
||||
@click="toggleShowAllNRT"
|
||||
/>
|
||||
}}
|
||||
</woot-button>
|
||||
</div>
|
||||
</template>
|
||||
</SLAEventItem>
|
||||
|
||||
+20
-16
@@ -2,13 +2,11 @@
|
||||
import { mapGetters } from 'vuex';
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Thumbnail,
|
||||
Spinner,
|
||||
NextButton,
|
||||
},
|
||||
props: {
|
||||
selectedInboxes: {
|
||||
@@ -96,7 +94,13 @@ export default {
|
||||
</div>
|
||||
<div class="flex items-center justify-between header">
|
||||
<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 class="container">
|
||||
<div
|
||||
@@ -117,7 +121,7 @@ export default {
|
||||
v-model="query"
|
||||
type="search"
|
||||
:placeholder="$t('BULK_ACTION.SEARCH_INPUT_PLACEHOLDER')"
|
||||
class="reset-base !outline-0 !text-sm agent--search_input"
|
||||
class="agent--search_input"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
@@ -157,21 +161,21 @@ export default {
|
||||
}}
|
||||
</p>
|
||||
<div class="agent-confirmation-actions">
|
||||
<NextButton
|
||||
faded
|
||||
sm
|
||||
slate
|
||||
type="reset"
|
||||
:label="$t('BULK_ACTION.GO_BACK_LABEL')"
|
||||
<woot-button
|
||||
color-scheme="primary"
|
||||
variant="smooth"
|
||||
@click="goBack"
|
||||
/>
|
||||
<NextButton
|
||||
sm
|
||||
type="submit"
|
||||
:label="$t('BULK_ACTION.YES')"
|
||||
>
|
||||
{{ $t('BULK_ACTION.GO_BACK_LABEL') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
color-scheme="primary"
|
||||
variant="flat"
|
||||
:is-loading="uiFlags.isUpdating"
|
||||
@click="submit"
|
||||
/>
|
||||
>
|
||||
{{ $t('BULK_ACTION.YES') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+15
-11
@@ -1,11 +1,7 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NextButton,
|
||||
},
|
||||
emits: ['update', 'close', 'assign'],
|
||||
data() {
|
||||
return {
|
||||
@@ -44,7 +40,13 @@ export default {
|
||||
</div>
|
||||
<div class="flex items-center justify-between header">
|
||||
<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 class="labels-list">
|
||||
<header class="labels-list__header">
|
||||
@@ -56,7 +58,7 @@ export default {
|
||||
v-model="query"
|
||||
type="search"
|
||||
:placeholder="$t('BULK_ACTION.SEARCH_INPUT_PLACEHOLDER')"
|
||||
class="reset-base !outline-0 !text-sm label--search_input"
|
||||
class="label--search_input"
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
@@ -89,13 +91,15 @@ export default {
|
||||
</li>
|
||||
</ul>
|
||||
<footer class="labels-list__footer">
|
||||
<NextButton
|
||||
sm
|
||||
type="submit"
|
||||
:label="$t('BULK_ACTION.LABELS.ASSIGN_SELECTED_LABELS')"
|
||||
<woot-button
|
||||
size="small"
|
||||
is-expanded
|
||||
color-scheme="primary"
|
||||
:disabled="!selectedLabels.length"
|
||||
@click="$emit('assign', selectedLabels)"
|
||||
/>
|
||||
>
|
||||
<span>{{ $t('BULK_ACTION.LABELS.ASSIGN_SELECTED_LABELS') }}</span>
|
||||
</woot-button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+8
-7
@@ -1,11 +1,6 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NextButton,
|
||||
},
|
||||
emits: ['assignTeam', 'close'],
|
||||
|
||||
data() {
|
||||
@@ -45,7 +40,13 @@ export default {
|
||||
</div>
|
||||
<div class="flex items-center justify-between header">
|
||||
<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 class="container">
|
||||
<div class="team__list-container">
|
||||
@@ -59,7 +60,7 @@ export default {
|
||||
v-model="query"
|
||||
type="search"
|
||||
:placeholder="$t('BULK_ACTION.SEARCH_INPUT_PLACEHOLDER')"
|
||||
class="reset-base !outline-0 !text-sm agent--search_input"
|
||||
class="agent--search_input"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
+18
-12
@@ -4,7 +4,6 @@ import { ref } from 'vue';
|
||||
|
||||
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
|
||||
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
const props = defineProps({
|
||||
showResolve: {
|
||||
@@ -26,9 +25,9 @@ const emit = defineEmits(['update', 'close']);
|
||||
const { t } = useI18n();
|
||||
|
||||
const actions = ref([
|
||||
{ icon: 'i-lucide-check', key: 'resolved' },
|
||||
{ icon: 'i-lucide-redo', key: 'open' },
|
||||
{ icon: 'i-lucide-alarm-clock', key: 'snoozed' },
|
||||
{ icon: 'checkmark', key: 'resolved' },
|
||||
{ icon: 'arrow-redo', key: 'open' },
|
||||
{ icon: 'send-clock', key: 'snoozed' },
|
||||
]);
|
||||
|
||||
const updateConversations = key => {
|
||||
@@ -86,21 +85,28 @@ const actionLabel = key => {
|
||||
<span class="text-sm font-medium text-slate-600 dark:text-slate-100">
|
||||
{{ $t('BULK_ACTION.UPDATE.CHANGE_STATUS') }}
|
||||
</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 class="px-2.5 pt-0 pb-2.5">
|
||||
<WootDropdownMenu class="m-0 list-none">
|
||||
<template v-for="action in actions">
|
||||
<WootDropdownItem v-if="showAction(action.key)" :key="action.key">
|
||||
<Button
|
||||
ghost
|
||||
sm
|
||||
slate
|
||||
class="!w-full ltr:!justify-start rtl:!justify-end"
|
||||
<woot-button
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
size="small"
|
||||
:icon="action.icon"
|
||||
:label="actionLabel(action.key)"
|
||||
class="hover:!bg-n-slate-3 dark:hover:!bg-n-solid-3"
|
||||
@click="updateConversations(action.key)"
|
||||
/>
|
||||
>
|
||||
{{ actionLabel(action.key) }}
|
||||
</woot-button>
|
||||
</WootDropdownItem>
|
||||
</template>
|
||||
</WootDropdownMenu>
|
||||
|
||||
@@ -9,7 +9,6 @@ import validations from './validations';
|
||||
import { parseLinearAPIErrorResponse } from 'dashboard/store/utils/api';
|
||||
import SearchableDropdown from './SearchableDropdown.vue';
|
||||
import { LINEAR_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
const props = defineProps({
|
||||
conversationId: {
|
||||
@@ -249,20 +248,20 @@ onMounted(getTeams);
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center justify-end w-full gap-2 mt-8">
|
||||
<Button
|
||||
faded
|
||||
slate
|
||||
type="reset"
|
||||
:label="$t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.CANCEL')"
|
||||
<woot-button
|
||||
class="px-4 rounded-xl button clear outline-woot-200/50 outline"
|
||||
@click.prevent="onClose"
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
:label="$t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.CREATE')"
|
||||
:disabled="isSubmitDisabled"
|
||||
>
|
||||
{{ $t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.CANCEL') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
:is-disabled="isSubmitDisabled"
|
||||
class="px-4 rounded-xl"
|
||||
:is-loading="isCreating"
|
||||
@click.prevent="createIssue"
|
||||
/>
|
||||
>
|
||||
{{ $t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.CREATE') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
<script setup>
|
||||
import { inject } from 'vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
const props = defineProps({
|
||||
identifier: {
|
||||
type: String,
|
||||
@@ -39,24 +37,30 @@ const openIssue = () => {
|
||||
/>
|
||||
<span class="text-xs font-medium text-ash-900">{{ identifier }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<Button
|
||||
ghost
|
||||
xs
|
||||
slate
|
||||
icon="i-lucide-unlink"
|
||||
class="!transition-none"
|
||||
<div class="flex items-center gap-0.5">
|
||||
<woot-button
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
class="h-[24px]"
|
||||
:is-loading="isUnlinking"
|
||||
@click="unlinkIssue"
|
||||
/>
|
||||
<Button
|
||||
ghost
|
||||
xs
|
||||
slate
|
||||
class="!transition-none"
|
||||
icon="i-lucide-arrow-up-right"
|
||||
>
|
||||
<fluent-icon
|
||||
v-if="!isUnlinking"
|
||||
icon="unlink"
|
||||
size="12"
|
||||
type="outline"
|
||||
icon-lib="lucide"
|
||||
/>
|
||||
</woot-button>
|
||||
<woot-button
|
||||
variant="clear"
|
||||
class="h-[24px]"
|
||||
color-scheme="secondary"
|
||||
@click="openIssue"
|
||||
/>
|
||||
>
|
||||
<fluent-icon icon="arrow-up-right" size="14" />
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -8,7 +8,6 @@ import FilterButton from 'dashboard/components/ui/Dropdown/DropdownButton.vue';
|
||||
import FilterListDropdown from 'dashboard/components/ui/Dropdown/DropdownList.vue';
|
||||
import { parseLinearAPIErrorResponse } from 'dashboard/store/utils/api';
|
||||
import { LINEAR_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
const props = defineProps({
|
||||
conversationId: {
|
||||
@@ -130,20 +129,20 @@ const linkIssue = async () => {
|
||||
</template>
|
||||
</FilterButton>
|
||||
<div class="flex items-center justify-end w-full gap-2 mt-2">
|
||||
<Button
|
||||
faded
|
||||
slate
|
||||
type="reset"
|
||||
:label="$t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.CANCEL')"
|
||||
<woot-button
|
||||
class="px-4 rounded-xl button clear outline-woot-200/50 outline"
|
||||
@click.prevent="onClose"
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
:label="$t('INTEGRATION_SETTINGS.LINEAR.LINK.TITLE')"
|
||||
:disabled="isSubmitDisabled"
|
||||
>
|
||||
{{ $t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.CANCEL') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
:is-disabled="isSubmitDisabled"
|
||||
class="px-4 rounded-xl"
|
||||
:is-loading="isLinking"
|
||||
@click.prevent="linkIssue"
|
||||
/>
|
||||
>
|
||||
{{ $t('INTEGRATION_SETTINGS.LINEAR.LINK.TITLE') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -9,7 +9,6 @@ import Issue from './Issue.vue';
|
||||
import { useTrack } from 'dashboard/composables';
|
||||
import { LINEAR_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||
import { parseLinearAPIErrorResponse } from 'dashboard/store/utils/api';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
const props = defineProps({
|
||||
conversationId: {
|
||||
@@ -101,25 +100,23 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<div class="relative" :class="{ group: linkedIssue }">
|
||||
<Button
|
||||
<woot-button
|
||||
v-on-clickaway="closeIssue"
|
||||
v-tooltip="tooltipText"
|
||||
sm
|
||||
ghost
|
||||
slate
|
||||
class="!gap-1"
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
@click="openIssue"
|
||||
>
|
||||
<fluent-icon
|
||||
icon="linear"
|
||||
size="19"
|
||||
class="text-[#5E6AD2] flex-shrink-0"
|
||||
class="text-[#5E6AD2]"
|
||||
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 }}
|
||||
</span>
|
||||
</Button>
|
||||
</woot-button>
|
||||
<Issue
|
||||
v-if="linkedIssue"
|
||||
:issue="linkedIssue.issue"
|
||||
|
||||
@@ -3,12 +3,9 @@ import { required } from '@vuelidate/validators';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import Modal from '../../Modal.vue';
|
||||
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Modal,
|
||||
NextButton,
|
||||
},
|
||||
props: {
|
||||
show: { type: Boolean, default: false },
|
||||
@@ -69,20 +66,13 @@ export default {
|
||||
:placeholder="confirmPlaceHolderText"
|
||||
@blur="v$.value.$touch"
|
||||
/>
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<NextButton
|
||||
faded
|
||||
slate
|
||||
type="reset"
|
||||
:label="rejectText"
|
||||
@click.prevent="closeModal"
|
||||
/>
|
||||
<NextButton
|
||||
ruby
|
||||
type="submit"
|
||||
:label="confirmText"
|
||||
:disabled="v$.value.$invalid"
|
||||
/>
|
||||
<div class="button-wrapper">
|
||||
<woot-button color-scheme="alert" :is-disabled="v$.value.$invalid">
|
||||
{{ confirmText }}
|
||||
</woot-button>
|
||||
<woot-button class="clear" @click.prevent="closeModal">
|
||||
{{ rejectText }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
<script>
|
||||
import Modal from '../../Modal.vue';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Modal,
|
||||
NextButton,
|
||||
},
|
||||
props: {
|
||||
title: {
|
||||
@@ -57,8 +55,12 @@ export default {
|
||||
<div class="h-auto overflow-auto flex flex-col">
|
||||
<woot-modal-header :header-title="title" :header-content="description" />
|
||||
<div class="flex flex-row justify-end gap-2 py-4 px-6 w-full">
|
||||
<NextButton faded type="reset" :label="cancelLabel" @click="cancel" />
|
||||
<NextButton type="submit" :label="confirmLabel" @click="confirm" />
|
||||
<woot-button variant="clear" @click="cancel">
|
||||
{{ cancelLabel }}
|
||||
</woot-button>
|
||||
<woot-button @click="confirm">
|
||||
{{ confirmLabel }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script setup>
|
||||
import Modal from '../../Modal.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
defineProps({
|
||||
onClose: { type: Function, default: () => {} },
|
||||
@@ -23,8 +22,23 @@ const show = defineModel('show', { type: Boolean, default: false });
|
||||
:header-content-value="messageValue"
|
||||
/>
|
||||
<div class="flex items-center justify-end gap-2 p-8">
|
||||
<Button faded slate type="reset" :label="rejectText" @click="onClose" />
|
||||
<Button ruby type="submit" :label="confirmText" @click="onConfirm" />
|
||||
<woot-button variant="clear" class="action-button" @click="onClose">
|
||||
{{ rejectText }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
color-scheme="alert"
|
||||
class="action-button"
|
||||
variant="smooth"
|
||||
@click="onConfirm"
|
||||
>
|
||||
{{ confirmText }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.action-button {
|
||||
max-width: var(--space-giga);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -295,27 +295,7 @@
|
||||
"CONVERSATION_INFO": "Conversation Information",
|
||||
"CONTACT_ATTRIBUTES": "Contact Attributes",
|
||||
"PREVIOUS_CONVERSATION": "Previous Conversations",
|
||||
"MACROS": "Macros",
|
||||
"SHOPIFY_ORDERS": "Shopify Orders"
|
||||
},
|
||||
"SHOPIFY": {
|
||||
"ORDER_ID": "Order #{id}",
|
||||
"ERROR": "Error loading orders",
|
||||
"NO_SHOPIFY_ORDERS": "No orders found",
|
||||
"FINANCIAL_STATUS": {
|
||||
"PENDING": "Pending",
|
||||
"AUTHORIZED": "Authorized",
|
||||
"PARTIALLY_PAID": "Partially Paid",
|
||||
"PAID": "Paid",
|
||||
"PARTIALLY_REFUNDED": "Partially Refunded",
|
||||
"REFUNDED": "Refunded",
|
||||
"VOIDED": "Voided"
|
||||
},
|
||||
"FULFILLMENT_STATUS": {
|
||||
"FULFILLED": "Fulfilled",
|
||||
"PARTIALLY_FULFILLED": "Partially Fulfilled",
|
||||
"UNFULFILLED": "Unfulfilled"
|
||||
}
|
||||
"MACROS": "Macros"
|
||||
}
|
||||
},
|
||||
"CONVERSATION_CUSTOM_ATTRIBUTES": {
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
{
|
||||
"INTEGRATION_SETTINGS": {
|
||||
"SHOPIFY": {
|
||||
"DELETE": {
|
||||
"TITLE": "Delete Shopify Integration",
|
||||
"MESSAGE": "Are you sure you want to delete the Shopify integration?"
|
||||
},
|
||||
"STORE_URL": {
|
||||
"TITLE": "Connect Shopify Store",
|
||||
"LABEL": "Store URL",
|
||||
"PLACEHOLDER": "your-store.myshopify.com",
|
||||
"HELP": "Enter your Shopify store's myshopify.com URL",
|
||||
"CANCEL": "Cancel",
|
||||
"SUBMIT": "Connect Store"
|
||||
},
|
||||
"ERROR": "There was an error connecting to Shopify. Please try again or contact support if the issue persists."
|
||||
},
|
||||
"HEADER": "Integrations",
|
||||
"DESCRIPTION": "Chatwoot integrates with multiple tools and services to improve your team's efficiency. Explore the list below to configure your favorite apps.",
|
||||
"LEARN_MORE": "Learn more about integrations",
|
||||
|
||||
@@ -119,8 +119,7 @@
|
||||
"EMPTY_LIST": "No results found"
|
||||
},
|
||||
"PAGINATION": {
|
||||
"RESULTS": "Showing {start} to {end} of {total} results",
|
||||
"PER_PAGE_TEMPLATE": "{size} / page"
|
||||
"RESULTS": "Showing {start} to {end} of {total} results"
|
||||
}
|
||||
},
|
||||
"AGENT_REPORTS": {
|
||||
|
||||
@@ -295,27 +295,7 @@
|
||||
"CONVERSATION_INFO": "معلومات المحادثة",
|
||||
"CONTACT_ATTRIBUTES": "سمات جهة الاتصال",
|
||||
"PREVIOUS_CONVERSATION": "المحادثات السابقة",
|
||||
"MACROS": "ماكروس",
|
||||
"SHOPIFY_ORDERS": "Shopify Orders"
|
||||
},
|
||||
"SHOPIFY": {
|
||||
"ORDER_ID": "Order #{id}",
|
||||
"ERROR": "Error loading orders",
|
||||
"NO_SHOPIFY_ORDERS": "No orders found",
|
||||
"FINANCIAL_STATUS": {
|
||||
"PENDING": "معلق",
|
||||
"AUTHORIZED": "Authorized",
|
||||
"PARTIALLY_PAID": "Partially Paid",
|
||||
"PAID": "Paid",
|
||||
"PARTIALLY_REFUNDED": "Partially Refunded",
|
||||
"REFUNDED": "Refunded",
|
||||
"VOIDED": "Voided"
|
||||
},
|
||||
"FULFILLMENT_STATUS": {
|
||||
"FULFILLED": "Fulfilled",
|
||||
"PARTIALLY_FULFILLED": "Partially Fulfilled",
|
||||
"UNFULFILLED": "Unfulfilled"
|
||||
}
|
||||
"MACROS": "ماكروس"
|
||||
}
|
||||
},
|
||||
"CONVERSATION_CUSTOM_ATTRIBUTES": {
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
{
|
||||
"INTEGRATION_SETTINGS": {
|
||||
"SHOPIFY": {
|
||||
"DELETE": {
|
||||
"TITLE": "Delete Shopify Integration",
|
||||
"MESSAGE": "Are you sure you want to delete the Shopify integration?"
|
||||
},
|
||||
"STORE_URL": {
|
||||
"TITLE": "Connect Shopify Store",
|
||||
"LABEL": "Store URL",
|
||||
"PLACEHOLDER": "your-store.myshopify.com",
|
||||
"HELP": "Enter your Shopify store's myshopify.com URL",
|
||||
"CANCEL": "إلغاء",
|
||||
"SUBMIT": "Connect Store"
|
||||
},
|
||||
"ERROR": "There was an error connecting to Shopify. Please try again or contact support if the issue persists."
|
||||
},
|
||||
"HEADER": "خيارات الربط",
|
||||
"DESCRIPTION": "Chatwoot تتكامل مع أدوات وخدمات متعددة لتحسين كفاءة فريقك. استكشف القائمة أدناه لتكوين تطبيقاتك المفضلة.",
|
||||
"LEARN_MORE": "معرفة المزيد عن التكاملات",
|
||||
|
||||
@@ -119,8 +119,7 @@
|
||||
"EMPTY_LIST": "لم يتم العثور على النتائج"
|
||||
},
|
||||
"PAGINATION": {
|
||||
"RESULTS": "Showing {start} to {end} of {total} results",
|
||||
"PER_PAGE_TEMPLATE": "{size} / page"
|
||||
"RESULTS": "Showing {start} to {end} of {total} results"
|
||||
}
|
||||
},
|
||||
"AGENT_REPORTS": {
|
||||
|
||||
@@ -295,27 +295,7 @@
|
||||
"CONVERSATION_INFO": "Conversation Information",
|
||||
"CONTACT_ATTRIBUTES": "Contact Attributes",
|
||||
"PREVIOUS_CONVERSATION": "Previous Conversations",
|
||||
"MACROS": "Macros",
|
||||
"SHOPIFY_ORDERS": "Shopify Orders"
|
||||
},
|
||||
"SHOPIFY": {
|
||||
"ORDER_ID": "Order #{id}",
|
||||
"ERROR": "Error loading orders",
|
||||
"NO_SHOPIFY_ORDERS": "No orders found",
|
||||
"FINANCIAL_STATUS": {
|
||||
"PENDING": "Pending",
|
||||
"AUTHORIZED": "Authorized",
|
||||
"PARTIALLY_PAID": "Partially Paid",
|
||||
"PAID": "Paid",
|
||||
"PARTIALLY_REFUNDED": "Partially Refunded",
|
||||
"REFUNDED": "Refunded",
|
||||
"VOIDED": "Voided"
|
||||
},
|
||||
"FULFILLMENT_STATUS": {
|
||||
"FULFILLED": "Fulfilled",
|
||||
"PARTIALLY_FULFILLED": "Partially Fulfilled",
|
||||
"UNFULFILLED": "Unfulfilled"
|
||||
}
|
||||
"MACROS": "Macros"
|
||||
}
|
||||
},
|
||||
"CONVERSATION_CUSTOM_ATTRIBUTES": {
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
{
|
||||
"INTEGRATION_SETTINGS": {
|
||||
"SHOPIFY": {
|
||||
"DELETE": {
|
||||
"TITLE": "Delete Shopify Integration",
|
||||
"MESSAGE": "Are you sure you want to delete the Shopify integration?"
|
||||
},
|
||||
"STORE_URL": {
|
||||
"TITLE": "Connect Shopify Store",
|
||||
"LABEL": "Store URL",
|
||||
"PLACEHOLDER": "your-store.myshopify.com",
|
||||
"HELP": "Enter your Shopify store's myshopify.com URL",
|
||||
"CANCEL": "Cancel",
|
||||
"SUBMIT": "Connect Store"
|
||||
},
|
||||
"ERROR": "There was an error connecting to Shopify. Please try again or contact support if the issue persists."
|
||||
},
|
||||
"HEADER": "Integrations",
|
||||
"DESCRIPTION": "Chatwoot integrates with multiple tools and services to improve your team's efficiency. Explore the list below to configure your favorite apps.",
|
||||
"LEARN_MORE": "Learn more about integrations",
|
||||
|
||||
@@ -119,8 +119,7 @@
|
||||
"EMPTY_LIST": "No results found"
|
||||
},
|
||||
"PAGINATION": {
|
||||
"RESULTS": "Showing {start} to {end} of {total} results",
|
||||
"PER_PAGE_TEMPLATE": "{size} / page"
|
||||
"RESULTS": "Showing {start} to {end} of {total} results"
|
||||
}
|
||||
},
|
||||
"AGENT_REPORTS": {
|
||||
|
||||
@@ -295,27 +295,7 @@
|
||||
"CONVERSATION_INFO": "Conversation Information",
|
||||
"CONTACT_ATTRIBUTES": "Contact Attributes",
|
||||
"PREVIOUS_CONVERSATION": "Предишни разговори",
|
||||
"MACROS": "Macros",
|
||||
"SHOPIFY_ORDERS": "Shopify Orders"
|
||||
},
|
||||
"SHOPIFY": {
|
||||
"ORDER_ID": "Order #{id}",
|
||||
"ERROR": "Error loading orders",
|
||||
"NO_SHOPIFY_ORDERS": "No orders found",
|
||||
"FINANCIAL_STATUS": {
|
||||
"PENDING": "Предстоящ",
|
||||
"AUTHORIZED": "Authorized",
|
||||
"PARTIALLY_PAID": "Partially Paid",
|
||||
"PAID": "Paid",
|
||||
"PARTIALLY_REFUNDED": "Partially Refunded",
|
||||
"REFUNDED": "Refunded",
|
||||
"VOIDED": "Voided"
|
||||
},
|
||||
"FULFILLMENT_STATUS": {
|
||||
"FULFILLED": "Fulfilled",
|
||||
"PARTIALLY_FULFILLED": "Partially Fulfilled",
|
||||
"UNFULFILLED": "Unfulfilled"
|
||||
}
|
||||
"MACROS": "Macros"
|
||||
}
|
||||
},
|
||||
"CONVERSATION_CUSTOM_ATTRIBUTES": {
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
{
|
||||
"INTEGRATION_SETTINGS": {
|
||||
"SHOPIFY": {
|
||||
"DELETE": {
|
||||
"TITLE": "Delete Shopify Integration",
|
||||
"MESSAGE": "Are you sure you want to delete the Shopify integration?"
|
||||
},
|
||||
"STORE_URL": {
|
||||
"TITLE": "Connect Shopify Store",
|
||||
"LABEL": "Store URL",
|
||||
"PLACEHOLDER": "your-store.myshopify.com",
|
||||
"HELP": "Enter your Shopify store's myshopify.com URL",
|
||||
"CANCEL": "Отмени",
|
||||
"SUBMIT": "Connect Store"
|
||||
},
|
||||
"ERROR": "There was an error connecting to Shopify. Please try again or contact support if the issue persists."
|
||||
},
|
||||
"HEADER": "Integrations",
|
||||
"DESCRIPTION": "Chatwoot integrates with multiple tools and services to improve your team's efficiency. Explore the list below to configure your favorite apps.",
|
||||
"LEARN_MORE": "Learn more about integrations",
|
||||
|
||||
@@ -119,8 +119,7 @@
|
||||
"EMPTY_LIST": "Няма намерени резултати"
|
||||
},
|
||||
"PAGINATION": {
|
||||
"RESULTS": "Showing {start} to {end} of {total} results",
|
||||
"PER_PAGE_TEMPLATE": "{size} / page"
|
||||
"RESULTS": "Showing {start} to {end} of {total} results"
|
||||
}
|
||||
},
|
||||
"AGENT_REPORTS": {
|
||||
|
||||
@@ -295,27 +295,7 @@
|
||||
"CONVERSATION_INFO": "Informació de la conversa",
|
||||
"CONTACT_ATTRIBUTES": "Atributs de contacte",
|
||||
"PREVIOUS_CONVERSATION": "Converses prèvies",
|
||||
"MACROS": "Macros",
|
||||
"SHOPIFY_ORDERS": "Shopify Orders"
|
||||
},
|
||||
"SHOPIFY": {
|
||||
"ORDER_ID": "Order #{id}",
|
||||
"ERROR": "Error loading orders",
|
||||
"NO_SHOPIFY_ORDERS": "No orders found",
|
||||
"FINANCIAL_STATUS": {
|
||||
"PENDING": "Pendent",
|
||||
"AUTHORIZED": "Authorized",
|
||||
"PARTIALLY_PAID": "Partially Paid",
|
||||
"PAID": "Paid",
|
||||
"PARTIALLY_REFUNDED": "Partially Refunded",
|
||||
"REFUNDED": "Refunded",
|
||||
"VOIDED": "Voided"
|
||||
},
|
||||
"FULFILLMENT_STATUS": {
|
||||
"FULFILLED": "Fulfilled",
|
||||
"PARTIALLY_FULFILLED": "Partially Fulfilled",
|
||||
"UNFULFILLED": "Unfulfilled"
|
||||
}
|
||||
"MACROS": "Macros"
|
||||
}
|
||||
},
|
||||
"CONVERSATION_CUSTOM_ATTRIBUTES": {
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
{
|
||||
"INTEGRATION_SETTINGS": {
|
||||
"SHOPIFY": {
|
||||
"DELETE": {
|
||||
"TITLE": "Delete Shopify Integration",
|
||||
"MESSAGE": "Are you sure you want to delete the Shopify integration?"
|
||||
},
|
||||
"STORE_URL": {
|
||||
"TITLE": "Connect Shopify Store",
|
||||
"LABEL": "Store URL",
|
||||
"PLACEHOLDER": "your-store.myshopify.com",
|
||||
"HELP": "Enter your Shopify store's myshopify.com URL",
|
||||
"CANCEL": "Cancel·la",
|
||||
"SUBMIT": "Connect Store"
|
||||
},
|
||||
"ERROR": "There was an error connecting to Shopify. Please try again or contact support if the issue persists."
|
||||
},
|
||||
"HEADER": "Integracions",
|
||||
"DESCRIPTION": "Chatwoot integrates with multiple tools and services to improve your team's efficiency. Explore the list below to configure your favorite apps.",
|
||||
"LEARN_MORE": "Learn more about integrations",
|
||||
|
||||
@@ -119,8 +119,7 @@
|
||||
"EMPTY_LIST": "No s'ha trobat agents"
|
||||
},
|
||||
"PAGINATION": {
|
||||
"RESULTS": "Showing {start} to {end} of {total} results",
|
||||
"PER_PAGE_TEMPLATE": "{size} / page"
|
||||
"RESULTS": "Showing {start} to {end} of {total} results"
|
||||
}
|
||||
},
|
||||
"AGENT_REPORTS": {
|
||||
|
||||
@@ -295,27 +295,7 @@
|
||||
"CONVERSATION_INFO": "Informace o konverzaci",
|
||||
"CONTACT_ATTRIBUTES": "Atributy kontaktu",
|
||||
"PREVIOUS_CONVERSATION": "Předchozí konverzace",
|
||||
"MACROS": "Macros",
|
||||
"SHOPIFY_ORDERS": "Shopify Orders"
|
||||
},
|
||||
"SHOPIFY": {
|
||||
"ORDER_ID": "Order #{id}",
|
||||
"ERROR": "Error loading orders",
|
||||
"NO_SHOPIFY_ORDERS": "No orders found",
|
||||
"FINANCIAL_STATUS": {
|
||||
"PENDING": "Čekající",
|
||||
"AUTHORIZED": "Authorized",
|
||||
"PARTIALLY_PAID": "Partially Paid",
|
||||
"PAID": "Paid",
|
||||
"PARTIALLY_REFUNDED": "Partially Refunded",
|
||||
"REFUNDED": "Refunded",
|
||||
"VOIDED": "Voided"
|
||||
},
|
||||
"FULFILLMENT_STATUS": {
|
||||
"FULFILLED": "Fulfilled",
|
||||
"PARTIALLY_FULFILLED": "Partially Fulfilled",
|
||||
"UNFULFILLED": "Unfulfilled"
|
||||
}
|
||||
"MACROS": "Macros"
|
||||
}
|
||||
},
|
||||
"CONVERSATION_CUSTOM_ATTRIBUTES": {
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
{
|
||||
"INTEGRATION_SETTINGS": {
|
||||
"SHOPIFY": {
|
||||
"DELETE": {
|
||||
"TITLE": "Delete Shopify Integration",
|
||||
"MESSAGE": "Are you sure you want to delete the Shopify integration?"
|
||||
},
|
||||
"STORE_URL": {
|
||||
"TITLE": "Connect Shopify Store",
|
||||
"LABEL": "Store URL",
|
||||
"PLACEHOLDER": "your-store.myshopify.com",
|
||||
"HELP": "Enter your Shopify store's myshopify.com URL",
|
||||
"CANCEL": "Zrušit",
|
||||
"SUBMIT": "Connect Store"
|
||||
},
|
||||
"ERROR": "There was an error connecting to Shopify. Please try again or contact support if the issue persists."
|
||||
},
|
||||
"HEADER": "Integrace",
|
||||
"DESCRIPTION": "Chatwoot integrates with multiple tools and services to improve your team's efficiency. Explore the list below to configure your favorite apps.",
|
||||
"LEARN_MORE": "Learn more about integrations",
|
||||
|
||||
@@ -119,8 +119,7 @@
|
||||
"EMPTY_LIST": "Žádné výsledky"
|
||||
},
|
||||
"PAGINATION": {
|
||||
"RESULTS": "Showing {start} to {end} of {total} results",
|
||||
"PER_PAGE_TEMPLATE": "{size} / page"
|
||||
"RESULTS": "Showing {start} to {end} of {total} results"
|
||||
}
|
||||
},
|
||||
"AGENT_REPORTS": {
|
||||
|
||||
@@ -295,27 +295,7 @@
|
||||
"CONVERSATION_INFO": "Samtale Information",
|
||||
"CONTACT_ATTRIBUTES": "Kontakt Attributter",
|
||||
"PREVIOUS_CONVERSATION": "Tidligere Samtaler",
|
||||
"MACROS": "Macros",
|
||||
"SHOPIFY_ORDERS": "Shopify Orders"
|
||||
},
|
||||
"SHOPIFY": {
|
||||
"ORDER_ID": "Order #{id}",
|
||||
"ERROR": "Error loading orders",
|
||||
"NO_SHOPIFY_ORDERS": "No orders found",
|
||||
"FINANCIAL_STATUS": {
|
||||
"PENDING": "Afventer",
|
||||
"AUTHORIZED": "Authorized",
|
||||
"PARTIALLY_PAID": "Partially Paid",
|
||||
"PAID": "Paid",
|
||||
"PARTIALLY_REFUNDED": "Partially Refunded",
|
||||
"REFUNDED": "Refunded",
|
||||
"VOIDED": "Voided"
|
||||
},
|
||||
"FULFILLMENT_STATUS": {
|
||||
"FULFILLED": "Fulfilled",
|
||||
"PARTIALLY_FULFILLED": "Partially Fulfilled",
|
||||
"UNFULFILLED": "Unfulfilled"
|
||||
}
|
||||
"MACROS": "Macros"
|
||||
}
|
||||
},
|
||||
"CONVERSATION_CUSTOM_ATTRIBUTES": {
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
{
|
||||
"INTEGRATION_SETTINGS": {
|
||||
"SHOPIFY": {
|
||||
"DELETE": {
|
||||
"TITLE": "Delete Shopify Integration",
|
||||
"MESSAGE": "Are you sure you want to delete the Shopify integration?"
|
||||
},
|
||||
"STORE_URL": {
|
||||
"TITLE": "Connect Shopify Store",
|
||||
"LABEL": "Store URL",
|
||||
"PLACEHOLDER": "your-store.myshopify.com",
|
||||
"HELP": "Enter your Shopify store's myshopify.com URL",
|
||||
"CANCEL": "Annuller",
|
||||
"SUBMIT": "Connect Store"
|
||||
},
|
||||
"ERROR": "There was an error connecting to Shopify. Please try again or contact support if the issue persists."
|
||||
},
|
||||
"HEADER": "Integrationer",
|
||||
"DESCRIPTION": "Chatwoot integrates with multiple tools and services to improve your team's efficiency. Explore the list below to configure your favorite apps.",
|
||||
"LEARN_MORE": "Learn more about integrations",
|
||||
|
||||
@@ -119,8 +119,7 @@
|
||||
"EMPTY_LIST": "Ingen resultater fundet"
|
||||
},
|
||||
"PAGINATION": {
|
||||
"RESULTS": "Showing {start} to {end} of {total} results",
|
||||
"PER_PAGE_TEMPLATE": "{size} / page"
|
||||
"RESULTS": "Showing {start} to {end} of {total} results"
|
||||
}
|
||||
},
|
||||
"AGENT_REPORTS": {
|
||||
|
||||
@@ -295,27 +295,7 @@
|
||||
"CONVERSATION_INFO": "Konversationsinformationen",
|
||||
"CONTACT_ATTRIBUTES": "Kontakt-Attribute",
|
||||
"PREVIOUS_CONVERSATION": "Vorherige Konversationen",
|
||||
"MACROS": "Makros",
|
||||
"SHOPIFY_ORDERS": "Shopify Orders"
|
||||
},
|
||||
"SHOPIFY": {
|
||||
"ORDER_ID": "Order #{id}",
|
||||
"ERROR": "Error loading orders",
|
||||
"NO_SHOPIFY_ORDERS": "No orders found",
|
||||
"FINANCIAL_STATUS": {
|
||||
"PENDING": "Ausstehend",
|
||||
"AUTHORIZED": "Authorized",
|
||||
"PARTIALLY_PAID": "Partially Paid",
|
||||
"PAID": "Paid",
|
||||
"PARTIALLY_REFUNDED": "Partially Refunded",
|
||||
"REFUNDED": "Refunded",
|
||||
"VOIDED": "Voided"
|
||||
},
|
||||
"FULFILLMENT_STATUS": {
|
||||
"FULFILLED": "Fulfilled",
|
||||
"PARTIALLY_FULFILLED": "Partially Fulfilled",
|
||||
"UNFULFILLED": "Unfulfilled"
|
||||
}
|
||||
"MACROS": "Makros"
|
||||
}
|
||||
},
|
||||
"CONVERSATION_CUSTOM_ATTRIBUTES": {
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
{
|
||||
"INTEGRATION_SETTINGS": {
|
||||
"SHOPIFY": {
|
||||
"DELETE": {
|
||||
"TITLE": "Delete Shopify Integration",
|
||||
"MESSAGE": "Are you sure you want to delete the Shopify integration?"
|
||||
},
|
||||
"STORE_URL": {
|
||||
"TITLE": "Connect Shopify Store",
|
||||
"LABEL": "Store URL",
|
||||
"PLACEHOLDER": "your-store.myshopify.com",
|
||||
"HELP": "Enter your Shopify store's myshopify.com URL",
|
||||
"CANCEL": "Stornieren",
|
||||
"SUBMIT": "Connect Store"
|
||||
},
|
||||
"ERROR": "There was an error connecting to Shopify. Please try again or contact support if the issue persists."
|
||||
},
|
||||
"HEADER": "Integrationen",
|
||||
"DESCRIPTION": "Chatwoot integriert sich mit mehreren Tools und Diensten, um die Effizienz Ihres Teams zu verbessern. Erkunden Sie die folgende Liste, um Ihre Lieblingsapps zu konfigurieren.",
|
||||
"LEARN_MORE": "Mehr über Integrationen erfahren",
|
||||
|
||||
@@ -119,8 +119,7 @@
|
||||
"EMPTY_LIST": "Keine Ergebnisse gefunden"
|
||||
},
|
||||
"PAGINATION": {
|
||||
"RESULTS": "Showing {start} to {end} of {total} results",
|
||||
"PER_PAGE_TEMPLATE": "{size} / page"
|
||||
"RESULTS": "Showing {start} to {end} of {total} results"
|
||||
}
|
||||
},
|
||||
"AGENT_REPORTS": {
|
||||
|
||||
@@ -295,27 +295,7 @@
|
||||
"CONVERSATION_INFO": "Πληροφορίες Συνομιλίας",
|
||||
"CONTACT_ATTRIBUTES": "Ιδιότητες Επαφής",
|
||||
"PREVIOUS_CONVERSATION": "Προηγούμενες συνομιλίες",
|
||||
"MACROS": "Μακροεντολές",
|
||||
"SHOPIFY_ORDERS": "Shopify Orders"
|
||||
},
|
||||
"SHOPIFY": {
|
||||
"ORDER_ID": "Order #{id}",
|
||||
"ERROR": "Error loading orders",
|
||||
"NO_SHOPIFY_ORDERS": "No orders found",
|
||||
"FINANCIAL_STATUS": {
|
||||
"PENDING": "Εκκρεμεί",
|
||||
"AUTHORIZED": "Authorized",
|
||||
"PARTIALLY_PAID": "Partially Paid",
|
||||
"PAID": "Paid",
|
||||
"PARTIALLY_REFUNDED": "Partially Refunded",
|
||||
"REFUNDED": "Refunded",
|
||||
"VOIDED": "Voided"
|
||||
},
|
||||
"FULFILLMENT_STATUS": {
|
||||
"FULFILLED": "Fulfilled",
|
||||
"PARTIALLY_FULFILLED": "Partially Fulfilled",
|
||||
"UNFULFILLED": "Unfulfilled"
|
||||
}
|
||||
"MACROS": "Μακροεντολές"
|
||||
}
|
||||
},
|
||||
"CONVERSATION_CUSTOM_ATTRIBUTES": {
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
{
|
||||
"INTEGRATION_SETTINGS": {
|
||||
"SHOPIFY": {
|
||||
"DELETE": {
|
||||
"TITLE": "Delete Shopify Integration",
|
||||
"MESSAGE": "Are you sure you want to delete the Shopify integration?"
|
||||
},
|
||||
"STORE_URL": {
|
||||
"TITLE": "Connect Shopify Store",
|
||||
"LABEL": "Store URL",
|
||||
"PLACEHOLDER": "your-store.myshopify.com",
|
||||
"HELP": "Enter your Shopify store's myshopify.com URL",
|
||||
"CANCEL": "Άκυρο",
|
||||
"SUBMIT": "Connect Store"
|
||||
},
|
||||
"ERROR": "There was an error connecting to Shopify. Please try again or contact support if the issue persists."
|
||||
},
|
||||
"HEADER": "Ενοποιήσεις",
|
||||
"DESCRIPTION": "Chatwoot integrates with multiple tools and services to improve your team's efficiency. Explore the list below to configure your favorite apps.",
|
||||
"LEARN_MORE": "Learn more about integrations",
|
||||
|
||||
@@ -119,8 +119,7 @@
|
||||
"EMPTY_LIST": "Δεν βρέθηκαν αποτελέσματα"
|
||||
},
|
||||
"PAGINATION": {
|
||||
"RESULTS": "Showing {start} to {end} of {total} results",
|
||||
"PER_PAGE_TEMPLATE": "{size} / page"
|
||||
"RESULTS": "Showing {start} to {end} of {total} results"
|
||||
}
|
||||
},
|
||||
"AGENT_REPORTS": {
|
||||
|
||||
@@ -295,27 +295,7 @@
|
||||
"CONVERSATION_INFO": "Información de la conversación",
|
||||
"CONTACT_ATTRIBUTES": "Atributos de contacto",
|
||||
"PREVIOUS_CONVERSATION": "Conversaciones anteriores",
|
||||
"MACROS": "Macros",
|
||||
"SHOPIFY_ORDERS": "Shopify Orders"
|
||||
},
|
||||
"SHOPIFY": {
|
||||
"ORDER_ID": "Order #{id}",
|
||||
"ERROR": "Error loading orders",
|
||||
"NO_SHOPIFY_ORDERS": "No orders found",
|
||||
"FINANCIAL_STATUS": {
|
||||
"PENDING": "Pendientes",
|
||||
"AUTHORIZED": "Authorized",
|
||||
"PARTIALLY_PAID": "Partially Paid",
|
||||
"PAID": "Paid",
|
||||
"PARTIALLY_REFUNDED": "Partially Refunded",
|
||||
"REFUNDED": "Refunded",
|
||||
"VOIDED": "Voided"
|
||||
},
|
||||
"FULFILLMENT_STATUS": {
|
||||
"FULFILLED": "Fulfilled",
|
||||
"PARTIALLY_FULFILLED": "Partially Fulfilled",
|
||||
"UNFULFILLED": "Unfulfilled"
|
||||
}
|
||||
"MACROS": "Macros"
|
||||
}
|
||||
},
|
||||
"CONVERSATION_CUSTOM_ATTRIBUTES": {
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
{
|
||||
"INTEGRATION_SETTINGS": {
|
||||
"SHOPIFY": {
|
||||
"DELETE": {
|
||||
"TITLE": "Delete Shopify Integration",
|
||||
"MESSAGE": "Are you sure you want to delete the Shopify integration?"
|
||||
},
|
||||
"STORE_URL": {
|
||||
"TITLE": "Connect Shopify Store",
|
||||
"LABEL": "Store URL",
|
||||
"PLACEHOLDER": "your-store.myshopify.com",
|
||||
"HELP": "Enter your Shopify store's myshopify.com URL",
|
||||
"CANCEL": "Cancelar",
|
||||
"SUBMIT": "Connect Store"
|
||||
},
|
||||
"ERROR": "There was an error connecting to Shopify. Please try again or contact support if the issue persists."
|
||||
},
|
||||
"HEADER": "Integraciones",
|
||||
"DESCRIPTION": "Chatwoot se integra con múltiples herramientas y servicios para mejorar la eficiencia de tu equipo. Explora la lista de abajo para configurar tus aplicaciones favoritas.",
|
||||
"LEARN_MORE": "Más información acerca de integraciones",
|
||||
|
||||
@@ -119,8 +119,7 @@
|
||||
"EMPTY_LIST": "No se encontraron resultados"
|
||||
},
|
||||
"PAGINATION": {
|
||||
"RESULTS": "Mostrando {start} a {end} de {total} resultados",
|
||||
"PER_PAGE_TEMPLATE": "{size} / page"
|
||||
"RESULTS": "Mostrando {start} a {end} de {total} resultados"
|
||||
}
|
||||
},
|
||||
"AGENT_REPORTS": {
|
||||
|
||||
@@ -295,27 +295,7 @@
|
||||
"CONVERSATION_INFO": "اطلاعات مکالمه",
|
||||
"CONTACT_ATTRIBUTES": "ویژگیهای تماس",
|
||||
"PREVIOUS_CONVERSATION": "گفتگوهای قبلی",
|
||||
"MACROS": "ماکروها",
|
||||
"SHOPIFY_ORDERS": "Shopify Orders"
|
||||
},
|
||||
"SHOPIFY": {
|
||||
"ORDER_ID": "Order #{id}",
|
||||
"ERROR": "Error loading orders",
|
||||
"NO_SHOPIFY_ORDERS": "No orders found",
|
||||
"FINANCIAL_STATUS": {
|
||||
"PENDING": "در انتظار",
|
||||
"AUTHORIZED": "Authorized",
|
||||
"PARTIALLY_PAID": "Partially Paid",
|
||||
"PAID": "Paid",
|
||||
"PARTIALLY_REFUNDED": "Partially Refunded",
|
||||
"REFUNDED": "Refunded",
|
||||
"VOIDED": "Voided"
|
||||
},
|
||||
"FULFILLMENT_STATUS": {
|
||||
"FULFILLED": "Fulfilled",
|
||||
"PARTIALLY_FULFILLED": "Partially Fulfilled",
|
||||
"UNFULFILLED": "Unfulfilled"
|
||||
}
|
||||
"MACROS": "ماکروها"
|
||||
}
|
||||
},
|
||||
"CONVERSATION_CUSTOM_ATTRIBUTES": {
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
{
|
||||
"INTEGRATION_SETTINGS": {
|
||||
"SHOPIFY": {
|
||||
"DELETE": {
|
||||
"TITLE": "Delete Shopify Integration",
|
||||
"MESSAGE": "Are you sure you want to delete the Shopify integration?"
|
||||
},
|
||||
"STORE_URL": {
|
||||
"TITLE": "Connect Shopify Store",
|
||||
"LABEL": "Store URL",
|
||||
"PLACEHOLDER": "your-store.myshopify.com",
|
||||
"HELP": "Enter your Shopify store's myshopify.com URL",
|
||||
"CANCEL": "انصراف",
|
||||
"SUBMIT": "Connect Store"
|
||||
},
|
||||
"ERROR": "There was an error connecting to Shopify. Please try again or contact support if the issue persists."
|
||||
},
|
||||
"HEADER": "برنامههای تلفیق شده",
|
||||
"DESCRIPTION": "Chatwoot integrates with multiple tools and services to improve your team's efficiency. Explore the list below to configure your favorite apps.",
|
||||
"LEARN_MORE": "Learn more about integrations",
|
||||
|
||||
@@ -119,8 +119,7 @@
|
||||
"EMPTY_LIST": "نتیجهای یافت نشد"
|
||||
},
|
||||
"PAGINATION": {
|
||||
"RESULTS": "Showing {start} to {end} of {total} results",
|
||||
"PER_PAGE_TEMPLATE": "{size} / page"
|
||||
"RESULTS": "Showing {start} to {end} of {total} results"
|
||||
}
|
||||
},
|
||||
"AGENT_REPORTS": {
|
||||
|
||||
@@ -295,27 +295,7 @@
|
||||
"CONVERSATION_INFO": "Keskustelun Tiedot",
|
||||
"CONTACT_ATTRIBUTES": "Yhteystiedon määritteet",
|
||||
"PREVIOUS_CONVERSATION": "Edelliset keskustelut",
|
||||
"MACROS": "Macros",
|
||||
"SHOPIFY_ORDERS": "Shopify Orders"
|
||||
},
|
||||
"SHOPIFY": {
|
||||
"ORDER_ID": "Order #{id}",
|
||||
"ERROR": "Error loading orders",
|
||||
"NO_SHOPIFY_ORDERS": "No orders found",
|
||||
"FINANCIAL_STATUS": {
|
||||
"PENDING": "Odottava",
|
||||
"AUTHORIZED": "Authorized",
|
||||
"PARTIALLY_PAID": "Partially Paid",
|
||||
"PAID": "Paid",
|
||||
"PARTIALLY_REFUNDED": "Partially Refunded",
|
||||
"REFUNDED": "Refunded",
|
||||
"VOIDED": "Voided"
|
||||
},
|
||||
"FULFILLMENT_STATUS": {
|
||||
"FULFILLED": "Fulfilled",
|
||||
"PARTIALLY_FULFILLED": "Partially Fulfilled",
|
||||
"UNFULFILLED": "Unfulfilled"
|
||||
}
|
||||
"MACROS": "Macros"
|
||||
}
|
||||
},
|
||||
"CONVERSATION_CUSTOM_ATTRIBUTES": {
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
{
|
||||
"INTEGRATION_SETTINGS": {
|
||||
"SHOPIFY": {
|
||||
"DELETE": {
|
||||
"TITLE": "Delete Shopify Integration",
|
||||
"MESSAGE": "Are you sure you want to delete the Shopify integration?"
|
||||
},
|
||||
"STORE_URL": {
|
||||
"TITLE": "Connect Shopify Store",
|
||||
"LABEL": "Store URL",
|
||||
"PLACEHOLDER": "your-store.myshopify.com",
|
||||
"HELP": "Enter your Shopify store's myshopify.com URL",
|
||||
"CANCEL": "Peruuta",
|
||||
"SUBMIT": "Connect Store"
|
||||
},
|
||||
"ERROR": "There was an error connecting to Shopify. Please try again or contact support if the issue persists."
|
||||
},
|
||||
"HEADER": "Integraatiot",
|
||||
"DESCRIPTION": "Chatwoot integrates with multiple tools and services to improve your team's efficiency. Explore the list below to configure your favorite apps.",
|
||||
"LEARN_MORE": "Learn more about integrations",
|
||||
|
||||
@@ -119,8 +119,7 @@
|
||||
"EMPTY_LIST": "Tuloksia ei löytynyt"
|
||||
},
|
||||
"PAGINATION": {
|
||||
"RESULTS": "Showing {start} to {end} of {total} results",
|
||||
"PER_PAGE_TEMPLATE": "{size} / page"
|
||||
"RESULTS": "Showing {start} to {end} of {total} results"
|
||||
}
|
||||
},
|
||||
"AGENT_REPORTS": {
|
||||
|
||||
@@ -295,27 +295,7 @@
|
||||
"CONVERSATION_INFO": "Informations de la conversation",
|
||||
"CONTACT_ATTRIBUTES": "Attributs du contact",
|
||||
"PREVIOUS_CONVERSATION": "Conversations précédentes",
|
||||
"MACROS": "Macros",
|
||||
"SHOPIFY_ORDERS": "Shopify Orders"
|
||||
},
|
||||
"SHOPIFY": {
|
||||
"ORDER_ID": "Order #{id}",
|
||||
"ERROR": "Error loading orders",
|
||||
"NO_SHOPIFY_ORDERS": "No orders found",
|
||||
"FINANCIAL_STATUS": {
|
||||
"PENDING": "En attente",
|
||||
"AUTHORIZED": "Authorized",
|
||||
"PARTIALLY_PAID": "Partially Paid",
|
||||
"PAID": "Paid",
|
||||
"PARTIALLY_REFUNDED": "Partially Refunded",
|
||||
"REFUNDED": "Refunded",
|
||||
"VOIDED": "Voided"
|
||||
},
|
||||
"FULFILLMENT_STATUS": {
|
||||
"FULFILLED": "Fulfilled",
|
||||
"PARTIALLY_FULFILLED": "Partially Fulfilled",
|
||||
"UNFULFILLED": "Unfulfilled"
|
||||
}
|
||||
"MACROS": "Macros"
|
||||
}
|
||||
},
|
||||
"CONVERSATION_CUSTOM_ATTRIBUTES": {
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
{
|
||||
"INTEGRATION_SETTINGS": {
|
||||
"SHOPIFY": {
|
||||
"DELETE": {
|
||||
"TITLE": "Delete Shopify Integration",
|
||||
"MESSAGE": "Are you sure you want to delete the Shopify integration?"
|
||||
},
|
||||
"STORE_URL": {
|
||||
"TITLE": "Connect Shopify Store",
|
||||
"LABEL": "Store URL",
|
||||
"PLACEHOLDER": "your-store.myshopify.com",
|
||||
"HELP": "Enter your Shopify store's myshopify.com URL",
|
||||
"CANCEL": "Annuler",
|
||||
"SUBMIT": "Connect Store"
|
||||
},
|
||||
"ERROR": "There was an error connecting to Shopify. Please try again or contact support if the issue persists."
|
||||
},
|
||||
"HEADER": "Intégrations",
|
||||
"DESCRIPTION": "Chatwoot integrates with multiple tools and services to improve your team's efficiency. Explore the list below to configure your favorite apps.",
|
||||
"LEARN_MORE": "Learn more about integrations",
|
||||
|
||||
@@ -119,8 +119,7 @@
|
||||
"EMPTY_LIST": "Aucun résultat trouvé"
|
||||
},
|
||||
"PAGINATION": {
|
||||
"RESULTS": "Showing {start} to {end} of {total} results",
|
||||
"PER_PAGE_TEMPLATE": "{size} / page"
|
||||
"RESULTS": "Showing {start} to {end} of {total} results"
|
||||
}
|
||||
},
|
||||
"AGENT_REPORTS": {
|
||||
|
||||
@@ -295,27 +295,7 @@
|
||||
"CONVERSATION_INFO": "מידע על שיחה",
|
||||
"CONTACT_ATTRIBUTES": "תכונות יצירת קשר",
|
||||
"PREVIOUS_CONVERSATION": "שיחות קודמות",
|
||||
"MACROS": "מאקרו",
|
||||
"SHOPIFY_ORDERS": "Shopify Orders"
|
||||
},
|
||||
"SHOPIFY": {
|
||||
"ORDER_ID": "Order #{id}",
|
||||
"ERROR": "Error loading orders",
|
||||
"NO_SHOPIFY_ORDERS": "No orders found",
|
||||
"FINANCIAL_STATUS": {
|
||||
"PENDING": "ממתין ל",
|
||||
"AUTHORIZED": "Authorized",
|
||||
"PARTIALLY_PAID": "Partially Paid",
|
||||
"PAID": "Paid",
|
||||
"PARTIALLY_REFUNDED": "Partially Refunded",
|
||||
"REFUNDED": "Refunded",
|
||||
"VOIDED": "Voided"
|
||||
},
|
||||
"FULFILLMENT_STATUS": {
|
||||
"FULFILLED": "Fulfilled",
|
||||
"PARTIALLY_FULFILLED": "Partially Fulfilled",
|
||||
"UNFULFILLED": "Unfulfilled"
|
||||
}
|
||||
"MACROS": "מאקרו"
|
||||
}
|
||||
},
|
||||
"CONVERSATION_CUSTOM_ATTRIBUTES": {
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
{
|
||||
"INTEGRATION_SETTINGS": {
|
||||
"SHOPIFY": {
|
||||
"DELETE": {
|
||||
"TITLE": "Delete Shopify Integration",
|
||||
"MESSAGE": "Are you sure you want to delete the Shopify integration?"
|
||||
},
|
||||
"STORE_URL": {
|
||||
"TITLE": "Connect Shopify Store",
|
||||
"LABEL": "Store URL",
|
||||
"PLACEHOLDER": "your-store.myshopify.com",
|
||||
"HELP": "Enter your Shopify store's myshopify.com URL",
|
||||
"CANCEL": "ביטול",
|
||||
"SUBMIT": "Connect Store"
|
||||
},
|
||||
"ERROR": "There was an error connecting to Shopify. Please try again or contact support if the issue persists."
|
||||
},
|
||||
"HEADER": "אינטגרציות",
|
||||
"DESCRIPTION": "Chatwoot integrates with multiple tools and services to improve your team's efficiency. Explore the list below to configure your favorite apps.",
|
||||
"LEARN_MORE": "Learn more about integrations",
|
||||
|
||||
@@ -119,8 +119,7 @@
|
||||
"EMPTY_LIST": "לא נמצאו תוצאות"
|
||||
},
|
||||
"PAGINATION": {
|
||||
"RESULTS": "Showing {start} to {end} of {total} results",
|
||||
"PER_PAGE_TEMPLATE": "{size} / page"
|
||||
"RESULTS": "Showing {start} to {end} of {total} results"
|
||||
}
|
||||
},
|
||||
"AGENT_REPORTS": {
|
||||
|
||||
@@ -295,27 +295,7 @@
|
||||
"CONVERSATION_INFO": "Conversation Information",
|
||||
"CONTACT_ATTRIBUTES": "Contact Attributes",
|
||||
"PREVIOUS_CONVERSATION": "Previous Conversations",
|
||||
"MACROS": "Macros",
|
||||
"SHOPIFY_ORDERS": "Shopify Orders"
|
||||
},
|
||||
"SHOPIFY": {
|
||||
"ORDER_ID": "Order #{id}",
|
||||
"ERROR": "Error loading orders",
|
||||
"NO_SHOPIFY_ORDERS": "No orders found",
|
||||
"FINANCIAL_STATUS": {
|
||||
"PENDING": "Pending",
|
||||
"AUTHORIZED": "Authorized",
|
||||
"PARTIALLY_PAID": "Partially Paid",
|
||||
"PAID": "Paid",
|
||||
"PARTIALLY_REFUNDED": "Partially Refunded",
|
||||
"REFUNDED": "Refunded",
|
||||
"VOIDED": "Voided"
|
||||
},
|
||||
"FULFILLMENT_STATUS": {
|
||||
"FULFILLED": "Fulfilled",
|
||||
"PARTIALLY_FULFILLED": "Partially Fulfilled",
|
||||
"UNFULFILLED": "Unfulfilled"
|
||||
}
|
||||
"MACROS": "Macros"
|
||||
}
|
||||
},
|
||||
"CONVERSATION_CUSTOM_ATTRIBUTES": {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user