Compare commits
183
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d9348883ed | ||
|
|
aeb708a053 | ||
|
|
ce8f40debb | ||
|
|
5ca8aec7c3 | ||
|
|
4b8b33c4fd | ||
|
|
e3df65ef09 | ||
|
|
063e9654bc | ||
|
|
b7035de400 | ||
|
|
156220e0e2 | ||
|
|
cec1a64d6f | ||
|
|
8b0caa6909 | ||
|
|
051eeae8ec | ||
|
|
d51cea9d6b | ||
|
|
56160bfc63 | ||
|
|
abca836f0c | ||
|
|
64640092aa | ||
|
|
d478f6e42e | ||
|
|
2dcd071dec | ||
|
|
ea34071553 | ||
|
|
ce4d52926c | ||
|
|
ba8df900e3 | ||
|
|
fb86b1354a | ||
|
|
ab28802d5a | ||
|
|
797714f5aa | ||
|
|
b2cc5d113e | ||
|
|
4eca7382f8 | ||
|
|
0fe60d137e | ||
|
|
f6ec3f3a72 | ||
|
|
9b75d9bd1b | ||
|
|
ec6c3b3571 | ||
|
|
48ba273730 | ||
|
|
d51148951f | ||
|
|
79bb3d0ef3 | ||
|
|
7418882a4f | ||
|
|
d38bd5139e | ||
|
|
fcc6973245 | ||
|
|
5491ca2470 | ||
|
|
2590c57893 | ||
|
|
f225e2fcd8 | ||
|
|
b626e0cbf1 | ||
|
|
eb4f93b821 | ||
|
|
4a0c61ddb7 | ||
|
|
72391f9c36 | ||
|
|
dc4dfe60e4 | ||
|
|
d3b4c7225a | ||
|
|
35f42e71f6 | ||
|
|
eb04882e5a | ||
|
|
f89d9a4401 | ||
|
|
e8ae73230d | ||
|
|
40c75941f5 | ||
|
|
3a4aa5f837 | ||
|
|
bfb2d2b2bb | ||
|
|
aa0f36245f | ||
|
|
8f83d03354 | ||
|
|
643f7415c1 | ||
|
|
ebee38d4a5 | ||
|
|
16ac2dba17 | ||
|
|
e511527f32 | ||
|
|
d9b840f161 | ||
|
|
1fbdd68222 | ||
|
|
ef54f07d5b | ||
|
|
bd19d596b3 | ||
|
|
6a0878f893 | ||
|
|
cdececcc67 | ||
|
|
ffdcee7d86 | ||
|
|
05c4ee099b | ||
|
|
e771d99552 | ||
|
|
643316e19a | ||
|
|
a0cc8e2f3f | ||
|
|
fb0f5cd696 | ||
|
|
6211406681 | ||
|
|
76ff8bdd35 | ||
|
|
358a3924b8 | ||
|
|
6b87d6784e | ||
|
|
faaf67129e | ||
|
|
10cd5da09d | ||
|
|
ce5a859a4f | ||
|
|
32e0915a20 | ||
|
|
159c810117 | ||
|
|
ce400a36d7 | ||
|
|
d20de25da1 | ||
|
|
c31d693add | ||
|
|
94985bbc77 | ||
|
|
65559b47fd | ||
|
|
40608f4494 | ||
|
|
31497d9c63 | ||
|
|
f31d9a3e76 | ||
|
|
eb94eac9a2 | ||
|
|
52f673e56c | ||
|
|
4aa567ee3d | ||
|
|
79082bea58 | ||
|
|
b9f68fc839 | ||
|
|
e10ef03490 | ||
|
|
a35c3e4c06 | ||
|
|
6f9f2c32c9 | ||
|
|
24606ab5ef | ||
|
|
344e8d5016 | ||
|
|
3e27e28848 | ||
|
|
38af08534c | ||
|
|
dd54cf5af3 | ||
|
|
b91f8012cd | ||
|
|
f999112671 | ||
|
|
be37c5b80c | ||
|
|
abeb938bfd | ||
|
|
81c1ab73e6 | ||
|
|
1689d5cf5c | ||
|
|
3970149b06 | ||
|
|
9c33ce50b3 | ||
|
|
36a2412f42 | ||
|
|
ee1ea9576b | ||
|
|
26ea87a6cb | ||
|
|
7e8fe78ecd | ||
|
|
f3176afc1c | ||
|
|
c4ba925ae7 | ||
|
|
1f0b56b96e | ||
|
|
3ba4340042 | ||
|
|
e7177364d4 | ||
|
|
f726dc2419 | ||
|
|
411048715d | ||
|
|
9a7a2486e4 | ||
|
|
39af360a18 | ||
|
|
5891fd6f49 | ||
|
|
b9864fe1f6 | ||
|
|
5f97a2fd7b | ||
|
|
74fc9c0321 | ||
|
|
9898ccee9e | ||
|
|
eabdfc8168 | ||
|
|
9bd5f15450 | ||
|
|
aea1585fa3 | ||
|
|
254d5dcf9a | ||
|
|
b4c4f328b2 | ||
|
|
a509ef826a | ||
|
|
ac1743514b | ||
|
|
2180edc14a | ||
|
|
de2a35b617 | ||
|
|
40577e6a50 | ||
|
|
368d7c4608 | ||
|
|
c1df37b41d | ||
|
|
7c3b07c507 | ||
|
|
f1f1ce644c | ||
|
|
38f16ba677 | ||
|
|
e7b01d80b3 | ||
|
|
ec9a82a017 | ||
|
|
62203da528 | ||
|
|
cdd3b73fc9 | ||
|
|
029294e16b | ||
|
|
2c095bed86 | ||
|
|
7c6508dd04 | ||
|
|
cef23891af | ||
|
|
610495123e | ||
|
|
19a851c01d | ||
|
|
a22e11c810 | ||
|
|
efe6e64dcd | ||
|
|
d6f5c627eb | ||
|
|
cb65d615ea | ||
|
|
51b4a8f9fc | ||
|
|
d8da1f5bf3 | ||
|
|
1268b2cf55 | ||
|
|
6829328182 | ||
|
|
6cc69f444b | ||
|
|
f89ed56258 | ||
|
|
7c5bb343c6 | ||
|
|
125ae5523a | ||
|
|
59f60936a7 | ||
|
|
216a2c0e73 | ||
|
|
708296a746 | ||
|
|
b9168664f7 | ||
|
|
170ea7691f | ||
|
|
606adffeeb | ||
|
|
e9c1c61fe4 | ||
|
|
78ebdbbbd8 | ||
|
|
01cd7b878d | ||
|
|
acb2816826 | ||
|
|
978f4c431a | ||
|
|
4b2ebb8877 | ||
|
|
c4c1f3eb63 | ||
|
|
829142c808 | ||
|
|
3a71829b46 | ||
|
|
0974aea300 | ||
|
|
9fb0dfa4a7 | ||
|
|
8bbb8ba5a4 | ||
|
|
a8aefa0c73 | ||
|
|
b536f35fa7 |
@@ -256,6 +256,8 @@ AZURE_APP_SECRET=
|
||||
## Change these values to fine tune performance
|
||||
# control the concurrency setting of sidekiq
|
||||
# SIDEKIQ_CONCURRENCY=10
|
||||
# Enable verbose logging each time a job is dequeued in Sidekiq
|
||||
# ENABLE_SIDEKIQ_DEQUEUE_LOGGER=false
|
||||
|
||||
|
||||
# AI powered features
|
||||
|
||||
@@ -70,6 +70,7 @@ jobs:
|
||||
spec/services/mfa/authentication_service_spec.rb \
|
||||
spec/requests/api/v1/profile/mfa_controller_spec.rb \
|
||||
spec/controllers/devise_overrides/sessions_controller_spec.rb \
|
||||
spec/models/application_record_external_credentials_encryption_spec.rb \
|
||||
--profile=10 \
|
||||
--format documentation
|
||||
env:
|
||||
|
||||
+2
-2
@@ -23,7 +23,7 @@ Metrics/MethodLength:
|
||||
- 'enterprise/lib/captain/agent.rb'
|
||||
|
||||
RSpec/ExampleLength:
|
||||
Max: 25
|
||||
Max: 50
|
||||
|
||||
Style/Documentation:
|
||||
Enabled: false
|
||||
@@ -336,4 +336,4 @@ FactoryBot/RedundantFactoryOption:
|
||||
Enabled: false
|
||||
|
||||
FactoryBot/FactoryAssociationWithStrategy:
|
||||
Enabled: false
|
||||
Enabled: false
|
||||
|
||||
@@ -21,6 +21,7 @@ gem 'telephone_number'
|
||||
gem 'time_diff'
|
||||
gem 'tzinfo-data'
|
||||
gem 'valid_email2'
|
||||
gem 'email-provider-info'
|
||||
# compress javascript config.assets.js_compressor
|
||||
gem 'uglifier'
|
||||
##-- used for single column multiple binary flags in notification settings/feature flagging --##
|
||||
@@ -158,7 +159,7 @@ gem 'working_hours'
|
||||
gem 'pg_search'
|
||||
|
||||
# Subscriptions, Billing
|
||||
gem 'stripe'
|
||||
gem 'stripe', '17.2.0.pre.alpha.2'
|
||||
|
||||
## - helper gems --##
|
||||
## to populate db with sample data
|
||||
|
||||
+8
-6
@@ -270,6 +270,7 @@ GEM
|
||||
concurrent-ruby (~> 1.0)
|
||||
http (>= 3.0)
|
||||
ruby2_keywords
|
||||
email-provider-info (0.0.1)
|
||||
email_reply_trimmer (0.1.13)
|
||||
erubi (1.13.0)
|
||||
et-orbi (1.2.11)
|
||||
@@ -594,7 +595,7 @@ GEM
|
||||
oj (3.16.10)
|
||||
bigdecimal (>= 3.0)
|
||||
ostruct (>= 0.2)
|
||||
omniauth (2.1.3)
|
||||
omniauth (2.1.4)
|
||||
hashie (>= 3.4.6)
|
||||
logger
|
||||
rack (>= 2.2.3)
|
||||
@@ -644,7 +645,7 @@ GEM
|
||||
activesupport (>= 3.0.0)
|
||||
raabro (1.4.0)
|
||||
racc (1.8.1)
|
||||
rack (3.2.0)
|
||||
rack (3.2.3)
|
||||
rack-attack (6.7.0)
|
||||
rack (>= 1.0, < 4)
|
||||
rack-contrib (2.5.0)
|
||||
@@ -653,7 +654,7 @@ GEM
|
||||
rack (>= 2.0.0)
|
||||
rack-mini-profiler (3.2.0)
|
||||
rack (>= 1.2.0)
|
||||
rack-protection (4.1.1)
|
||||
rack-protection (4.2.1)
|
||||
base64 (>= 0.1.0)
|
||||
logger (>= 1.6.0)
|
||||
rack (>= 3.0.0, < 4)
|
||||
@@ -901,7 +902,7 @@ GEM
|
||||
squasher (0.7.2)
|
||||
stackprof (0.2.25)
|
||||
statsd-ruby (1.5.0)
|
||||
stripe (8.5.0)
|
||||
stripe (17.2.0.pre.alpha.2)
|
||||
telephone_number (1.4.20)
|
||||
test-prof (1.2.1)
|
||||
thor (1.4.0)
|
||||
@@ -935,7 +936,7 @@ GEM
|
||||
unicode-emoji (~> 4.0, >= 4.0.4)
|
||||
unicode-emoji (4.0.4)
|
||||
uniform_notifier (1.17.0)
|
||||
uri (1.0.3)
|
||||
uri (1.0.4)
|
||||
uri_template (0.7.0)
|
||||
valid_email2 (5.2.6)
|
||||
activemodel (>= 3.2)
|
||||
@@ -1016,6 +1017,7 @@ DEPENDENCIES
|
||||
dotenv-rails (>= 3.0.0)
|
||||
down
|
||||
elastic-apm
|
||||
email-provider-info
|
||||
email_reply_trimmer
|
||||
facebook-messenger
|
||||
factory_bot_rails (>= 6.4.3)
|
||||
@@ -1108,7 +1110,7 @@ DEPENDENCIES
|
||||
spring-watcher-listen
|
||||
squasher
|
||||
stackprof
|
||||
stripe
|
||||
stripe (= 17.2.0.pre.alpha.2)
|
||||
telephone_number
|
||||
test-prof
|
||||
tidewave
|
||||
|
||||
@@ -36,6 +36,10 @@
|
||||
"REDIS_OPENSSL_VERIFY_MODE":{
|
||||
"description": "OpenSSL verification mode for Redis connections. ref https://help.heroku.com/HC0F8CUS/redis-connection-issues",
|
||||
"value": "none"
|
||||
},
|
||||
"NODE_OPTIONS": {
|
||||
"description": "Increase V8 heap for Vite build to avoid OOM",
|
||||
"value": "--max-old-space-size=4096"
|
||||
}
|
||||
},
|
||||
"formation": {
|
||||
|
||||
@@ -23,7 +23,7 @@ class Messages::MessageBuilder
|
||||
process_emails
|
||||
# When the message has no quoted content, it will just be rendered as a regular message
|
||||
# The frontend is equipped to handle this case
|
||||
process_email_content if @account.feature_enabled?(:quoted_email_reply)
|
||||
process_email_content
|
||||
@message.save!
|
||||
@message
|
||||
end
|
||||
@@ -178,7 +178,13 @@ class Messages::MessageBuilder
|
||||
email_attributes = ensure_indifferent_access(@message.content_attributes[:email] || {})
|
||||
normalized_content = normalize_email_body(@message.content)
|
||||
|
||||
email_attributes[:html_content] = build_html_content(normalized_content)
|
||||
# Use custom HTML content if provided, otherwise generate from message content
|
||||
email_attributes[:html_content] = if custom_email_content_provided?
|
||||
build_custom_html_content
|
||||
else
|
||||
build_html_content(normalized_content)
|
||||
end
|
||||
|
||||
email_attributes[:text_content] = build_text_content(normalized_content)
|
||||
email_attributes
|
||||
end
|
||||
@@ -213,4 +219,17 @@ class Messages::MessageBuilder
|
||||
|
||||
ChatwootMarkdownRenderer.new(content).render_message.to_s
|
||||
end
|
||||
|
||||
def custom_email_content_provided?
|
||||
@params[:email_html_content].present?
|
||||
end
|
||||
|
||||
def build_custom_html_content
|
||||
html_content = ensure_indifferent_access(@message.content_attributes.dig(:email, :html_content) || {})
|
||||
|
||||
html_content[:full] = @params[:email_html_content]
|
||||
html_content[:reply] = @params[:email_html_content]
|
||||
|
||||
html_content
|
||||
end
|
||||
end
|
||||
|
||||
@@ -10,10 +10,28 @@ class V2::Reports::BaseSummaryBuilder
|
||||
|
||||
def load_data
|
||||
@conversations_count = fetch_conversations_count
|
||||
@resolved_count = fetch_resolved_count
|
||||
@avg_resolution_time = fetch_average_time('conversation_resolved')
|
||||
@avg_first_response_time = fetch_average_time('first_response')
|
||||
@avg_reply_time = fetch_average_time('reply_time')
|
||||
load_reporting_events_data
|
||||
end
|
||||
|
||||
def load_reporting_events_data
|
||||
# Extract the column name for indexing (e.g., 'conversations.team_id' -> 'team_id')
|
||||
index_key = group_by_key.to_s.split('.').last
|
||||
|
||||
results = reporting_events
|
||||
.select(
|
||||
"#{group_by_key} as #{index_key}",
|
||||
"COUNT(CASE WHEN name = 'conversation_resolved' THEN 1 END) as resolved_count",
|
||||
"AVG(CASE WHEN name = 'conversation_resolved' THEN #{average_value_key} END) as avg_resolution_time",
|
||||
"AVG(CASE WHEN name = 'first_response' THEN #{average_value_key} END) as avg_first_response_time",
|
||||
"AVG(CASE WHEN name = 'reply_time' THEN #{average_value_key} END) as avg_reply_time"
|
||||
)
|
||||
.group(group_by_key)
|
||||
.index_by { |record| record.public_send(index_key) }
|
||||
|
||||
@resolved_count = results.transform_values(&:resolved_count)
|
||||
@avg_resolution_time = results.transform_values(&:avg_resolution_time)
|
||||
@avg_first_response_time = results.transform_values(&:avg_first_response_time)
|
||||
@avg_reply_time = results.transform_values(&:avg_reply_time)
|
||||
end
|
||||
|
||||
def reporting_events
|
||||
@@ -24,14 +42,6 @@ class V2::Reports::BaseSummaryBuilder
|
||||
# Override this method
|
||||
end
|
||||
|
||||
def fetch_average_time(event_name)
|
||||
get_grouped_average(reporting_events.where(name: event_name))
|
||||
end
|
||||
|
||||
def fetch_resolved_count
|
||||
reporting_events.where(name: 'conversation_resolved').group(group_by_key).count
|
||||
end
|
||||
|
||||
def group_by_key
|
||||
# Override this method
|
||||
end
|
||||
@@ -40,10 +50,6 @@ class V2::Reports::BaseSummaryBuilder
|
||||
# Override this method
|
||||
end
|
||||
|
||||
def get_grouped_average(events)
|
||||
events.group(group_by_key).average(average_value_key)
|
||||
end
|
||||
|
||||
def average_value_key
|
||||
ActiveModel::Type::Boolean.new.cast(params[:business_hours]).present? ? :value_in_business_hours : :value
|
||||
end
|
||||
|
||||
@@ -13,10 +13,7 @@ class V2::Reports::InboxSummaryBuilder < V2::Reports::BaseSummaryBuilder
|
||||
|
||||
def load_data
|
||||
@conversations_count = fetch_conversations_count
|
||||
@resolved_count = fetch_resolved_count
|
||||
@avg_resolution_time = fetch_average_time('conversation_resolved')
|
||||
@avg_first_response_time = fetch_average_time('first_response')
|
||||
@avg_reply_time = fetch_average_time('reply_time')
|
||||
load_reporting_events_data
|
||||
end
|
||||
|
||||
def fetch_conversations_count
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
class Api::V1::Accounts::BulkActionsController < Api::V1::Accounts::BaseController
|
||||
before_action :type_matches?
|
||||
|
||||
def create
|
||||
if type_matches?
|
||||
::BulkActionsJob.perform_later(
|
||||
account: @current_account,
|
||||
user: current_user,
|
||||
params: permitted_params
|
||||
)
|
||||
case normalized_type
|
||||
when 'Conversation'
|
||||
enqueue_conversation_job
|
||||
head :ok
|
||||
when 'Contact'
|
||||
check_authorization_for_contact_action
|
||||
enqueue_contact_job
|
||||
head :ok
|
||||
else
|
||||
render json: { success: false }, status: :unprocessable_entity
|
||||
@@ -16,11 +15,54 @@ class Api::V1::Accounts::BulkActionsController < Api::V1::Accounts::BaseControll
|
||||
|
||||
private
|
||||
|
||||
def type_matches?
|
||||
['Conversation'].include?(params[:type])
|
||||
def normalized_type
|
||||
params[:type].to_s.camelize
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:type, :snoozed_until, ids: [], fields: [:status, :assignee_id, :team_id], labels: [add: [], remove: []])
|
||||
def enqueue_conversation_job
|
||||
::BulkActionsJob.perform_later(
|
||||
account: @current_account,
|
||||
user: current_user,
|
||||
params: conversation_params
|
||||
)
|
||||
end
|
||||
|
||||
def enqueue_contact_job
|
||||
Contacts::BulkActionJob.perform_later(
|
||||
@current_account.id,
|
||||
current_user.id,
|
||||
contact_params
|
||||
)
|
||||
end
|
||||
|
||||
def delete_contact_action?
|
||||
params[:action_name] == 'delete'
|
||||
end
|
||||
|
||||
def check_authorization_for_contact_action
|
||||
authorize(Contact, :destroy?) if delete_contact_action?
|
||||
end
|
||||
|
||||
def conversation_params
|
||||
# TODO: Align conversation payloads with the `{ action_name, action_attributes }`
|
||||
# and then remove this method in favor of a common params method.
|
||||
base = params.permit(
|
||||
:snoozed_until,
|
||||
fields: [:status, :assignee_id, :team_id]
|
||||
)
|
||||
append_common_bulk_attributes(base)
|
||||
end
|
||||
|
||||
def contact_params
|
||||
# TODO: remove this method in favor of a common params method.
|
||||
# once legacy conversation payloads are migrated.
|
||||
append_common_bulk_attributes({})
|
||||
end
|
||||
|
||||
def append_common_bulk_attributes(base_params)
|
||||
# NOTE: Conversation payloads historically diverged per action. Going forward we
|
||||
# want all objects to share a common contract: `{ action_name, action_attributes }`
|
||||
common = params.permit(:type, :action_name, ids: [], labels: [add: [], remove: []])
|
||||
base_params.merge(common)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,6 +5,6 @@ class Api::V1::Accounts::Conversations::BaseController < Api::V1::Accounts::Base
|
||||
|
||||
def conversation
|
||||
@conversation ||= Current.account.conversations.find_by!(display_id: params[:conversation_id])
|
||||
authorize @conversation.inbox, :show?
|
||||
authorize @conversation, :show?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -160,7 +160,7 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
||||
|
||||
def conversation
|
||||
@conversation ||= Current.account.conversations.find_by!(display_id: params[:id])
|
||||
authorize @conversation.inbox, :show?
|
||||
authorize @conversation, :show?
|
||||
end
|
||||
|
||||
def inbox
|
||||
|
||||
@@ -22,7 +22,7 @@ class Api::V1::Accounts::Integrations::DyteController < Api::V1::Accounts::BaseC
|
||||
private
|
||||
|
||||
def authorize_request
|
||||
authorize @conversation.inbox, :show?
|
||||
authorize @conversation, :show?
|
||||
end
|
||||
|
||||
def render_response(response)
|
||||
|
||||
@@ -14,6 +14,7 @@ module AccessTokenAuthHelper
|
||||
ensure_access_token
|
||||
render_unauthorized('Invalid Access Token') && return if @access_token.blank?
|
||||
|
||||
# NOTE: This ensures that current_user is set and available for the rest of the controller actions
|
||||
@resource = @access_token.owner
|
||||
Current.user = @resource if allowed_current_user_type?(@resource)
|
||||
end
|
||||
|
||||
@@ -19,6 +19,19 @@ class DeviseOverrides::OmniauthCallbacksController < DeviseTokenAuth::OmniauthCa
|
||||
redirect_to login_page_url(email: encoded_email, sso_auth_token: @resource.generate_sso_auth_token)
|
||||
end
|
||||
|
||||
def sign_in_user_on_mobile
|
||||
@resource.skip_confirmation! if confirmable_enabled?
|
||||
|
||||
# once the resource is found and verified
|
||||
# we can just send them to the login page again with the SSO params
|
||||
# that will log them in
|
||||
encoded_email = ERB::Util.url_encode(@resource.email)
|
||||
params = { email: encoded_email, sso_auth_token: @resource.generate_sso_auth_token }.to_query
|
||||
|
||||
mobile_deep_link_base = GlobalConfigService.load('MOBILE_DEEP_LINK_BASE', 'chatwootapp')
|
||||
redirect_to "#{mobile_deep_link_base}://auth/saml?#{params}", allow_other_host: true
|
||||
end
|
||||
|
||||
def sign_up_user
|
||||
return redirect_to login_page_url(error: 'no-account-found') unless account_signup_allowed?
|
||||
return redirect_to login_page_url(error: 'business-account-only') unless validate_signup_email_is_business_domain?
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/* global axios */
|
||||
import ApiClient from '../ApiClient';
|
||||
|
||||
class CaptainCustomTools extends ApiClient {
|
||||
constructor() {
|
||||
super('captain/custom_tools', { accountScoped: true });
|
||||
}
|
||||
|
||||
get({ page = 1, searchKey } = {}) {
|
||||
return axios.get(this.url, {
|
||||
params: { page, searchKey },
|
||||
});
|
||||
}
|
||||
|
||||
show(id) {
|
||||
return axios.get(`${this.url}/${id}`);
|
||||
}
|
||||
|
||||
create(data = {}) {
|
||||
return axios.post(this.url, {
|
||||
custom_tool: data,
|
||||
});
|
||||
}
|
||||
|
||||
update(id, data = {}) {
|
||||
return axios.put(`${this.url}/${id}`, {
|
||||
custom_tool: data,
|
||||
});
|
||||
}
|
||||
|
||||
delete(id) {
|
||||
return axios.delete(`${this.url}/${id}`);
|
||||
}
|
||||
}
|
||||
|
||||
export default new CaptainCustomTools();
|
||||
@@ -0,0 +1,16 @@
|
||||
import axios from 'axios';
|
||||
import ApiClient from './ApiClient';
|
||||
import { CHANGELOG_API_URL } from 'shared/constants/links';
|
||||
|
||||
class ChangelogApi extends ApiClient {
|
||||
constructor() {
|
||||
super('changelog', { apiVersion: 'v1' });
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
fetchFromHub() {
|
||||
return axios.get(CHANGELOG_API_URL);
|
||||
}
|
||||
}
|
||||
|
||||
export default new ChangelogApi();
|
||||
@@ -23,6 +23,35 @@ class EnterpriseAccountAPI extends ApiClient {
|
||||
action_type: action,
|
||||
});
|
||||
}
|
||||
|
||||
// V2 Billing APIs
|
||||
creditGrants() {
|
||||
return axios.get(`${this.url}credit_grants`);
|
||||
}
|
||||
|
||||
v2PricingPlans() {
|
||||
return axios.get(`${this.url}v2_pricing_plans`);
|
||||
}
|
||||
|
||||
v2TopupOptions() {
|
||||
return axios.get(`${this.url}v2_topup_options`);
|
||||
}
|
||||
|
||||
v2Topup(data) {
|
||||
return axios.post(`${this.url}v2_topup`, data);
|
||||
}
|
||||
|
||||
v2Subscribe(data) {
|
||||
return axios.post(`${this.url}v2_subscribe`, data);
|
||||
}
|
||||
|
||||
cancelSubscription(data = {}) {
|
||||
return axios.post(`${this.url}cancel_subscription`, data);
|
||||
}
|
||||
|
||||
changePricingPlan(data) {
|
||||
return axios.post(`${this.url}change_pricing_plan`, data);
|
||||
}
|
||||
}
|
||||
|
||||
export default new EnterpriseAccountAPI();
|
||||
|
||||
+38
-35
@@ -5,6 +5,7 @@ import { useToggle } from '@vueuse/core';
|
||||
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import ConfirmContactDeleteDialog from 'dashboard/components-next/Contacts/ContactsForm/ConfirmContactDeleteDialog.vue';
|
||||
import Policy from 'dashboard/components/policy.vue';
|
||||
|
||||
defineProps({
|
||||
selectedContact: {
|
||||
@@ -24,42 +25,44 @@ const openConfirmDeleteContactDialog = () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col items-start border-t border-n-strong px-6 py-5">
|
||||
<Button
|
||||
:label="t('CONTACTS_LAYOUT.DETAILS.DELETE_CONTACT')"
|
||||
sm
|
||||
link
|
||||
slate
|
||||
class="hover:!no-underline text-n-slate-12"
|
||||
icon="i-lucide-chevron-down"
|
||||
trailing-icon
|
||||
@click="toggleDeleteSection()"
|
||||
/>
|
||||
<Policy :permissions="['administrator']">
|
||||
<div class="flex flex-col items-start border-t border-n-strong px-6 py-5">
|
||||
<Button
|
||||
:label="t('CONTACTS_LAYOUT.DETAILS.DELETE_CONTACT')"
|
||||
sm
|
||||
link
|
||||
slate
|
||||
class="hover:!no-underline text-n-slate-12"
|
||||
icon="i-lucide-chevron-down"
|
||||
trailing-icon
|
||||
@click="toggleDeleteSection()"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="transition-all duration-300 ease-in-out grid w-full overflow-hidden"
|
||||
:class="
|
||||
showDeleteSection
|
||||
? 'grid-rows-[1fr] opacity-100 mt-2'
|
||||
: 'grid-rows-[0fr] opacity-0 mt-0'
|
||||
"
|
||||
>
|
||||
<div class="overflow-hidden min-h-0">
|
||||
<span class="inline-flex text-n-slate-11 text-sm items-center gap-1">
|
||||
{{ t('CONTACTS_LAYOUT.CARD.DELETE_CONTACT.MESSAGE') }}
|
||||
<Button
|
||||
:label="t('CONTACTS_LAYOUT.CARD.DELETE_CONTACT.BUTTON')"
|
||||
sm
|
||||
ruby
|
||||
link
|
||||
@click="openConfirmDeleteContactDialog()"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="transition-all duration-300 ease-in-out grid w-full overflow-hidden"
|
||||
:class="
|
||||
showDeleteSection
|
||||
? 'grid-rows-[1fr] opacity-100 mt-2'
|
||||
: 'grid-rows-[0fr] opacity-0 mt-0'
|
||||
"
|
||||
>
|
||||
<div class="overflow-hidden min-h-0">
|
||||
<span class="inline-flex text-n-slate-11 text-sm items-center gap-1">
|
||||
{{ t('CONTACTS_LAYOUT.CARD.DELETE_CONTACT.MESSAGE') }}
|
||||
<Button
|
||||
:label="t('CONTACTS_LAYOUT.CARD.DELETE_CONTACT.BUTTON')"
|
||||
sm
|
||||
ruby
|
||||
link
|
||||
@click="openConfirmDeleteContactDialog()"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ConfirmContactDeleteDialog
|
||||
ref="confirmDeleteContactDialogRef"
|
||||
:selected-contact="selectedContact"
|
||||
/>
|
||||
<ConfirmContactDeleteDialog
|
||||
ref="confirmDeleteContactDialogRef"
|
||||
:selected-contact="selectedContact"
|
||||
/>
|
||||
</Policy>
|
||||
</template>
|
||||
|
||||
@@ -8,6 +8,7 @@ import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
|
||||
import Flag from 'dashboard/components-next/flag/Flag.vue';
|
||||
import ContactDeleteSection from 'dashboard/components-next/Contacts/ContactsCard/ContactDeleteSection.vue';
|
||||
import Checkbox from 'dashboard/components-next/checkbox/Checkbox.vue';
|
||||
import countries from 'shared/constants/countries';
|
||||
|
||||
const props = defineProps({
|
||||
@@ -20,9 +21,17 @@ const props = defineProps({
|
||||
availabilityStatus: { type: String, default: null },
|
||||
isExpanded: { type: Boolean, default: false },
|
||||
isUpdating: { type: Boolean, default: false },
|
||||
selectable: { type: Boolean, default: false },
|
||||
isSelected: { type: Boolean, default: false },
|
||||
});
|
||||
|
||||
const emit = defineEmits(['toggle', 'updateContact', 'showContact']);
|
||||
const emit = defineEmits([
|
||||
'toggle',
|
||||
'updateContact',
|
||||
'showContact',
|
||||
'select',
|
||||
'avatarHover',
|
||||
]);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
@@ -88,111 +97,148 @@ const onClickExpand = () => {
|
||||
};
|
||||
|
||||
const onClickViewDetails = () => emit('showContact', props.id);
|
||||
|
||||
const toggleSelect = checked => {
|
||||
emit('select', checked);
|
||||
};
|
||||
|
||||
const handleAvatarHover = isHovered => {
|
||||
emit('avatarHover', isHovered);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CardLayout :key="id" layout="row">
|
||||
<div class="flex items-center justify-start flex-1 gap-4">
|
||||
<Avatar
|
||||
:name="name"
|
||||
:src="thumbnail"
|
||||
:size="48"
|
||||
:status="availabilityStatus"
|
||||
hide-offline-status
|
||||
rounded-full
|
||||
/>
|
||||
<div class="flex flex-col gap-0.5 flex-1">
|
||||
<div class="flex flex-wrap items-center gap-x-4 gap-y-1">
|
||||
<span class="text-base font-medium truncate text-n-slate-12">
|
||||
{{ name }}
|
||||
</span>
|
||||
<span class="inline-flex items-center gap-1">
|
||||
<span
|
||||
v-if="additionalAttributes?.companyName"
|
||||
class="i-ph-building-light size-4 text-n-slate-10 mb-0.5"
|
||||
/>
|
||||
<span
|
||||
v-if="additionalAttributes?.companyName"
|
||||
class="text-sm truncate text-n-slate-11"
|
||||
>
|
||||
{{ additionalAttributes.companyName }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center justify-start gap-x-3 gap-y-1">
|
||||
<div v-if="email" class="truncate max-w-72" :title="email">
|
||||
<span class="text-sm text-n-slate-11">
|
||||
{{ email }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="email" class="w-px h-3 truncate bg-n-slate-6" />
|
||||
<span v-if="phoneNumber" class="text-sm truncate text-n-slate-11">
|
||||
{{ phoneNumber }}
|
||||
</span>
|
||||
<div v-if="phoneNumber" class="w-px h-3 truncate bg-n-slate-6" />
|
||||
<span
|
||||
v-if="countryDetails"
|
||||
class="inline-flex items-center gap-2 text-sm truncate text-n-slate-11"
|
||||
<div class="relative">
|
||||
<CardLayout
|
||||
:key="id"
|
||||
layout="row"
|
||||
:class="{
|
||||
'outline-n-weak !bg-n-slate-3 dark:!bg-n-solid-3': isSelected,
|
||||
}"
|
||||
>
|
||||
<div class="flex items-center justify-start flex-1 gap-4">
|
||||
<div
|
||||
class="relative"
|
||||
@mouseenter="handleAvatarHover(true)"
|
||||
@mouseleave="handleAvatarHover(false)"
|
||||
>
|
||||
<Avatar
|
||||
:name="name"
|
||||
:src="thumbnail"
|
||||
:size="48"
|
||||
:status="availabilityStatus"
|
||||
hide-offline-status
|
||||
rounded-full
|
||||
>
|
||||
<Flag :country="countryDetails.countryCode" class="size-3.5" />
|
||||
{{ formattedLocation }}
|
||||
</span>
|
||||
<div v-if="countryDetails" class="w-px h-3 truncate bg-n-slate-6" />
|
||||
<Button
|
||||
:label="t('CONTACTS_LAYOUT.CARD.VIEW_DETAILS')"
|
||||
variant="link"
|
||||
size="xs"
|
||||
@click="onClickViewDetails"
|
||||
/>
|
||||
<template v-if="selectable" #overlay="{ size }">
|
||||
<label
|
||||
class="flex items-center justify-center rounded-full cursor-pointer absolute inset-0 z-10 backdrop-blur-[2px] border border-n-weak"
|
||||
:style="{ width: `${size}px`, height: `${size}px` }"
|
||||
@click.stop
|
||||
>
|
||||
<Checkbox
|
||||
:model-value="isSelected"
|
||||
@change="event => toggleSelect(event.target.checked)"
|
||||
/>
|
||||
</label>
|
||||
</template>
|
||||
</Avatar>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
icon="i-lucide-chevron-down"
|
||||
variant="ghost"
|
||||
color="slate"
|
||||
size="xs"
|
||||
:class="{ 'rotate-180': isExpanded }"
|
||||
@click="onClickExpand"
|
||||
/>
|
||||
|
||||
<template #after>
|
||||
<div
|
||||
class="transition-all duration-500 ease-in-out grid overflow-hidden"
|
||||
:class="
|
||||
isExpanded
|
||||
? 'grid-rows-[1fr] opacity-100'
|
||||
: 'grid-rows-[0fr] opacity-0'
|
||||
"
|
||||
>
|
||||
<div class="overflow-hidden">
|
||||
<div class="flex flex-col gap-6 p-6 border-t border-n-strong">
|
||||
<ContactsForm
|
||||
ref="contactsFormRef"
|
||||
:contact-data="contactData"
|
||||
@update="handleFormUpdate"
|
||||
/>
|
||||
<div>
|
||||
<Button
|
||||
:label="
|
||||
t('CONTACTS_LAYOUT.CARD.EDIT_DETAILS_FORM.UPDATE_BUTTON')
|
||||
"
|
||||
size="sm"
|
||||
:is-loading="isUpdating"
|
||||
:disabled="isUpdating || isFormInvalid"
|
||||
@click="handleUpdateContact"
|
||||
<div class="flex flex-col gap-0.5 flex-1">
|
||||
<div class="flex flex-wrap items-center gap-x-4 gap-y-1">
|
||||
<span class="text-base font-medium truncate text-n-slate-12">
|
||||
{{ name }}
|
||||
</span>
|
||||
<span class="inline-flex items-center gap-1">
|
||||
<span
|
||||
v-if="additionalAttributes?.companyName"
|
||||
class="i-ph-building-light size-4 text-n-slate-10 mb-0.5"
|
||||
/>
|
||||
<span
|
||||
v-if="additionalAttributes?.companyName"
|
||||
class="text-sm truncate text-n-slate-11"
|
||||
>
|
||||
{{ additionalAttributes.companyName }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-wrap items-center justify-start gap-x-3 gap-y-1"
|
||||
>
|
||||
<div v-if="email" class="truncate max-w-72" :title="email">
|
||||
<span class="text-sm text-n-slate-11">
|
||||
{{ email }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="email" class="w-px h-3 truncate bg-n-slate-6" />
|
||||
<span v-if="phoneNumber" class="text-sm truncate text-n-slate-11">
|
||||
{{ phoneNumber }}
|
||||
</span>
|
||||
<div v-if="phoneNumber" class="w-px h-3 truncate bg-n-slate-6" />
|
||||
<span
|
||||
v-if="countryDetails"
|
||||
class="inline-flex items-center gap-2 text-sm truncate text-n-slate-11"
|
||||
>
|
||||
<Flag :country="countryDetails.countryCode" class="size-3.5" />
|
||||
{{ formattedLocation }}
|
||||
</span>
|
||||
<div v-if="countryDetails" class="w-px h-3 truncate bg-n-slate-6" />
|
||||
<Button
|
||||
:label="t('CONTACTS_LAYOUT.CARD.VIEW_DETAILS')"
|
||||
variant="link"
|
||||
size="xs"
|
||||
@click="onClickViewDetails"
|
||||
/>
|
||||
</div>
|
||||
<ContactDeleteSection
|
||||
:selected-contact="{
|
||||
id: props.id,
|
||||
name: props.name,
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</CardLayout>
|
||||
|
||||
<Button
|
||||
icon="i-lucide-chevron-down"
|
||||
variant="ghost"
|
||||
color="slate"
|
||||
size="xs"
|
||||
:class="{ 'rotate-180': isExpanded }"
|
||||
@click="onClickExpand"
|
||||
/>
|
||||
|
||||
<template #after>
|
||||
<div
|
||||
class="transition-all duration-500 ease-in-out grid overflow-hidden"
|
||||
:class="
|
||||
isExpanded
|
||||
? 'grid-rows-[1fr] opacity-100'
|
||||
: 'grid-rows-[0fr] opacity-0'
|
||||
"
|
||||
>
|
||||
<div class="overflow-hidden">
|
||||
<div class="flex flex-col gap-6 p-6 border-t border-n-strong">
|
||||
<ContactsForm
|
||||
ref="contactsFormRef"
|
||||
:contact-data="contactData"
|
||||
@update="handleFormUpdate"
|
||||
/>
|
||||
<div>
|
||||
<Button
|
||||
:label="
|
||||
t('CONTACTS_LAYOUT.CARD.EDIT_DETAILS_FORM.UPDATE_BUTTON')
|
||||
"
|
||||
size="sm"
|
||||
:is-loading="isUpdating"
|
||||
:disabled="isUpdating || isFormInvalid"
|
||||
@click="handleUpdateContact"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ContactDeleteSection
|
||||
:selected-contact="{
|
||||
id: props.id,
|
||||
name: props.name,
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</CardLayout>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -10,6 +10,7 @@ import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import ContactLabels from 'dashboard/components-next/Contacts/ContactLabels/ContactLabels.vue';
|
||||
import ContactsForm from 'dashboard/components-next/Contacts/ContactsForm/ContactsForm.vue';
|
||||
import ConfirmContactDeleteDialog from 'dashboard/components-next/Contacts/ContactsForm/ConfirmContactDeleteDialog.vue';
|
||||
import Policy from 'dashboard/components/policy.vue';
|
||||
|
||||
const props = defineProps({
|
||||
selectedContact: {
|
||||
@@ -174,27 +175,29 @@ const handleAvatarDelete = async () => {
|
||||
@click="updateContact"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-col items-start w-full gap-4 pt-6 border-t border-n-strong"
|
||||
>
|
||||
<div class="flex flex-col gap-2">
|
||||
<h6 class="text-base font-medium text-n-slate-12">
|
||||
{{ t('CONTACTS_LAYOUT.DETAILS.DELETE_CONTACT') }}
|
||||
</h6>
|
||||
<span class="text-sm text-n-slate-11">
|
||||
{{ t('CONTACTS_LAYOUT.DETAILS.DELETE_CONTACT_DESCRIPTION') }}
|
||||
</span>
|
||||
<Policy :permissions="['administrator']">
|
||||
<div
|
||||
class="flex flex-col items-start w-full gap-4 pt-6 border-t border-n-strong"
|
||||
>
|
||||
<div class="flex flex-col gap-2">
|
||||
<h6 class="text-base font-medium text-n-slate-12">
|
||||
{{ t('CONTACTS_LAYOUT.DETAILS.DELETE_CONTACT') }}
|
||||
</h6>
|
||||
<span class="text-sm text-n-slate-11">
|
||||
{{ t('CONTACTS_LAYOUT.DETAILS.DELETE_CONTACT_DESCRIPTION') }}
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
:label="t('CONTACTS_LAYOUT.DETAILS.DELETE_CONTACT')"
|
||||
color="ruby"
|
||||
@click="openConfirmDeleteContactDialog"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
:label="t('CONTACTS_LAYOUT.DETAILS.DELETE_CONTACT')"
|
||||
color="ruby"
|
||||
@click="openConfirmDeleteContactDialog"
|
||||
<ConfirmContactDeleteDialog
|
||||
ref="confirmDeleteContactDialogRef"
|
||||
:selected-contact="selectedContact"
|
||||
@go-to-contacts-list="emit('goToContactsList')"
|
||||
/>
|
||||
</div>
|
||||
<ConfirmContactDeleteDialog
|
||||
ref="confirmDeleteContactDialogRef"
|
||||
:selected-contact="selectedContact"
|
||||
@go-to-contacts-list="emit('goToContactsList')"
|
||||
/>
|
||||
</Policy>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -10,7 +10,15 @@ import {
|
||||
} from 'shared/helpers/CustomErrors';
|
||||
import ContactsCard from 'dashboard/components-next/Contacts/ContactsCard/ContactsCard.vue';
|
||||
|
||||
defineProps({ contacts: { type: Array, required: true } });
|
||||
const props = defineProps({
|
||||
contacts: { type: Array, required: true },
|
||||
selectedContactIds: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['toggleContact']);
|
||||
|
||||
const { t } = useI18n();
|
||||
const store = useStore();
|
||||
@@ -20,6 +28,9 @@ const route = useRoute();
|
||||
const uiFlags = useMapGetter('contacts/getUIFlags');
|
||||
const isUpdating = computed(() => uiFlags.value.isUpdating);
|
||||
const expandedCardId = ref(null);
|
||||
const hoveredAvatarId = ref(null);
|
||||
|
||||
const selectedIdsSet = computed(() => new Set(props.selectedContactIds || []));
|
||||
|
||||
const updateContact = async updatedData => {
|
||||
try {
|
||||
@@ -58,25 +69,43 @@ const onClickViewDetails = async id => {
|
||||
const toggleExpanded = id => {
|
||||
expandedCardId.value = expandedCardId.value === id ? null : id;
|
||||
};
|
||||
|
||||
const isSelected = id => selectedIdsSet.value.has(id);
|
||||
|
||||
const shouldShowSelection = id => {
|
||||
return hoveredAvatarId.value === id || isSelected(id);
|
||||
};
|
||||
|
||||
const handleSelect = (id, value) => {
|
||||
emit('toggleContact', { id, value });
|
||||
};
|
||||
|
||||
const handleAvatarHover = (id, isHovered) => {
|
||||
hoveredAvatarId.value = isHovered ? id : null;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-4 px-6 pt-4 pb-6">
|
||||
<ContactsCard
|
||||
v-for="contact in contacts"
|
||||
:id="contact.id"
|
||||
:key="contact.id"
|
||||
:name="contact.name"
|
||||
:email="contact.email"
|
||||
:thumbnail="contact.thumbnail"
|
||||
:phone-number="contact.phoneNumber"
|
||||
:additional-attributes="contact.additionalAttributes"
|
||||
:availability-status="contact.availabilityStatus"
|
||||
:is-expanded="expandedCardId === contact.id"
|
||||
:is-updating="isUpdating"
|
||||
@toggle="toggleExpanded(contact.id)"
|
||||
@update-contact="updateContact"
|
||||
@show-contact="onClickViewDetails"
|
||||
/>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div v-for="contact in contacts" :key="contact.id" class="relative">
|
||||
<ContactsCard
|
||||
:id="contact.id"
|
||||
:name="contact.name"
|
||||
:email="contact.email"
|
||||
:thumbnail="contact.thumbnail"
|
||||
:phone-number="contact.phoneNumber"
|
||||
:additional-attributes="contact.additionalAttributes"
|
||||
:availability-status="contact.availabilityStatus"
|
||||
:is-expanded="expandedCardId === contact.id"
|
||||
:is-updating="isUpdating"
|
||||
:selectable="shouldShowSelection(contact.id)"
|
||||
:is-selected="isSelected(contact.id)"
|
||||
@toggle="toggleExpanded(contact.id)"
|
||||
@update-contact="updateContact"
|
||||
@show-contact="onClickViewDetails"
|
||||
@select="value => handleSelect(contact.id, value)"
|
||||
@avatar-hover="value => handleAvatarHover(contact.id, value)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup>
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import ButtonGroup from 'dashboard/components-next/buttonGroup/ButtonGroup.vue';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import { computed } from 'vue';
|
||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||
@@ -55,17 +56,17 @@ useKeyboardEvents(keyboardEvents);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col justify-center items-center absolute top-36 xl:top-24 ltr:right-2 rtl:left-2 bg-n-solid-2 border border-n-weak rounded-full gap-2 p-1"
|
||||
<ButtonGroup
|
||||
class="flex flex-col justify-center items-center absolute top-36 xl:top-24 ltr:right-2 rtl:left-2 bg-n-solid-2/90 backdrop-blur-lg border border-n-weak/50 rounded-full gap-1.5 p-1.5 shadow-sm transition-shadow duration-200 hover:shadow"
|
||||
>
|
||||
<Button
|
||||
v-tooltip.top="$t('CONVERSATION.SIDEBAR.CONTACT')"
|
||||
ghost
|
||||
slate
|
||||
sm
|
||||
class="!rounded-full"
|
||||
class="!rounded-full transition-all duration-[250ms] ease-out active:!scale-95 active:!brightness-105 active:duration-75"
|
||||
:class="{
|
||||
'bg-n-alpha-2': isContactSidebarOpen,
|
||||
'bg-n-alpha-2 active:shadow-sm': isContactSidebarOpen,
|
||||
}"
|
||||
icon="i-ph-user-bold"
|
||||
@click="handleConversationSidebarToggle"
|
||||
@@ -75,13 +76,14 @@ useKeyboardEvents(keyboardEvents);
|
||||
v-tooltip.bottom="$t('CONVERSATION.SIDEBAR.COPILOT')"
|
||||
ghost
|
||||
slate
|
||||
class="!rounded-full"
|
||||
:class="{
|
||||
'bg-n-alpha-2 !text-n-iris-9': isCopilotPanelOpen,
|
||||
}"
|
||||
sm
|
||||
class="!rounded-full transition-all duration-[250ms] ease-out active:!scale-95 active:duration-75"
|
||||
:class="{
|
||||
'bg-n-alpha-2 !text-n-iris-9 active:!brightness-105 active:shadow-sm':
|
||||
isCopilotPanelOpen,
|
||||
}"
|
||||
icon="i-woot-captain"
|
||||
@click="handleCopilotSidebarToggle"
|
||||
/>
|
||||
</div>
|
||||
</ButtonGroup>
|
||||
</template>
|
||||
|
||||
@@ -21,6 +21,10 @@ const props = defineProps({
|
||||
enableCannedResponses: { type: Boolean, default: true },
|
||||
enabledMenuOptions: { type: Array, default: () => [] },
|
||||
enableCaptainTools: { type: Boolean, default: false },
|
||||
signature: { type: String, default: '' },
|
||||
allowSignature: { type: Boolean, default: false },
|
||||
sendWithSignature: { type: Boolean, default: false },
|
||||
channelType: { type: String, default: '' },
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
@@ -100,6 +104,10 @@ watch(
|
||||
:enable-canned-responses="enableCannedResponses"
|
||||
:enabled-menu-options="enabledMenuOptions"
|
||||
:enable-captain-tools="enableCaptainTools"
|
||||
:signature="signature"
|
||||
:allow-signature="allowSignature"
|
||||
:send-with-signature="sendWithSignature"
|
||||
:channel-type="channelType"
|
||||
@input="handleInput"
|
||||
@focus="handleFocus"
|
||||
@blur="handleBlur"
|
||||
|
||||
@@ -14,6 +14,10 @@ defineProps({
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
showBackdrop: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -25,14 +29,24 @@ defineProps({
|
||||
class="relative w-full max-w-[60rem] mx-auto overflow-hidden h-full max-h-[28rem]"
|
||||
>
|
||||
<div
|
||||
v-if="showBackdrop"
|
||||
class="w-full h-full space-y-4 overflow-y-hidden opacity-50 pointer-events-none"
|
||||
>
|
||||
<slot name="empty-state-item" />
|
||||
</div>
|
||||
<div
|
||||
class="absolute inset-x-0 bottom-0 flex flex-col items-center justify-end w-full h-full pb-20 bg-gradient-to-t from-n-background from-25% dark:from-n-background to-transparent"
|
||||
class="flex flex-col items-center justify-end w-full h-full pb-20"
|
||||
:class="{
|
||||
'absolute inset-x-0 bottom-0 bg-gradient-to-t from-n-background from-25% dark:from-n-background to-transparent':
|
||||
showBackdrop,
|
||||
}"
|
||||
>
|
||||
<div class="flex flex-col items-center justify-center gap-6">
|
||||
<div
|
||||
class="flex flex-col items-center justify-center gap-6"
|
||||
:class="{
|
||||
'mt-48': !showBackdrop,
|
||||
}"
|
||||
>
|
||||
<div class="flex flex-col items-center justify-center gap-3">
|
||||
<h2
|
||||
class="text-3xl font-medium text-center text-n-slate-12 font-interDisplay"
|
||||
@@ -40,6 +54,7 @@ defineProps({
|
||||
{{ title }}
|
||||
</h2>
|
||||
<p
|
||||
v-if="subtitle"
|
||||
class="max-w-xl text-base text-center text-n-slate-11 font-interDisplay tracking-[0.3px]"
|
||||
>
|
||||
{{ subtitle }}
|
||||
|
||||
+5
-2
@@ -14,6 +14,7 @@ import {
|
||||
} from 'dashboard/helper/portalHelper';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
|
||||
import ButtonGroup from 'dashboard/components-next/buttonGroup/ButtonGroup.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import DropdownMenu from 'dashboard/components-next/dropdown-menu/DropdownMenu.vue';
|
||||
|
||||
@@ -140,11 +141,12 @@ const updateArticleStatus = async ({ value }) => {
|
||||
:disabled="!articleId"
|
||||
@click="previewArticle"
|
||||
/>
|
||||
<div class="flex items-center">
|
||||
<ButtonGroup class="flex items-center">
|
||||
<Button
|
||||
:label="t('HELP_CENTER.EDIT_ARTICLE_PAGE.HEADER.PUBLISH')"
|
||||
size="sm"
|
||||
class="ltr:rounded-r-none rtl:rounded-l-none"
|
||||
no-animation
|
||||
:is-loading="isArticlePublishing"
|
||||
:disabled="
|
||||
status === ARTICLE_STATUSES.PUBLISHED ||
|
||||
@@ -159,6 +161,7 @@ const updateArticleStatus = async ({ value }) => {
|
||||
icon="i-lucide-chevron-down"
|
||||
size="sm"
|
||||
:disabled="!articleId"
|
||||
no-animation
|
||||
class="ltr:rounded-l-none rtl:rounded-r-none"
|
||||
@click.stop="showArticleActionMenu = !showArticleActionMenu"
|
||||
/>
|
||||
@@ -170,7 +173,7 @@ const updateArticleStatus = async ({ value }) => {
|
||||
/>
|
||||
</OnClickOutside>
|
||||
</div>
|
||||
</div>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -92,7 +92,6 @@ const setSignature = () => {
|
||||
|
||||
const toggleMessageSignature = () => {
|
||||
setSignatureFlagForInbox(props.channelType, !sendWithSignature.value);
|
||||
setSignature();
|
||||
};
|
||||
|
||||
// Added this watch to dynamically set signature on target inbox change.
|
||||
|
||||
+14
-8
@@ -199,16 +199,20 @@ const handleInboxAction = ({ value, action, ...rest }) => {
|
||||
state.attachedFiles = [];
|
||||
};
|
||||
|
||||
const removeTargetInbox = value => {
|
||||
v$.value.$reset();
|
||||
// Remove the signature from message content
|
||||
// Based on the Advance Editor (used in isEmailOrWebWidget) and Plain editor(all other inboxes except WhatsApp)
|
||||
if (props.sendWithSignature) {
|
||||
const signatureToRemove = inboxTypes.value.isEmailOrWebWidget
|
||||
? props.messageSignature
|
||||
: extractTextFromMarkdown(props.messageSignature);
|
||||
const removeSignatureFromMessage = () => {
|
||||
// Always remove the signature from message content when inbox/contact is removed
|
||||
// to ensure no leftover signature content remains
|
||||
const signatureToRemove = inboxTypes.value.isEmailOrWebWidget
|
||||
? props.messageSignature
|
||||
: extractTextFromMarkdown(props.messageSignature);
|
||||
if (signatureToRemove) {
|
||||
state.message = removeSignature(state.message, signatureToRemove);
|
||||
}
|
||||
};
|
||||
|
||||
const removeTargetInbox = value => {
|
||||
v$.value.$reset();
|
||||
removeSignatureFromMessage();
|
||||
emit('updateTargetInbox', value);
|
||||
state.attachedFiles = [];
|
||||
};
|
||||
@@ -216,6 +220,7 @@ const removeTargetInbox = value => {
|
||||
const clearSelectedContact = () => {
|
||||
emit('clearSelectedContact');
|
||||
state.attachedFiles = [];
|
||||
removeSignatureFromMessage();
|
||||
};
|
||||
|
||||
const onClickInsertEmoji = emoji => {
|
||||
@@ -354,6 +359,7 @@ const shouldShowMessageEditor = computed(() => {
|
||||
:is-email-or-web-widget-inbox="inboxTypes.isEmailOrWebWidget"
|
||||
:has-errors="validationStates.isMessageInvalid"
|
||||
:has-attachments="state.attachedFiles.length > 0"
|
||||
:channel-type="inboxChannelType"
|
||||
/>
|
||||
|
||||
<AttachmentPreviews
|
||||
|
||||
@@ -17,6 +17,7 @@ const props = defineProps({
|
||||
hasAttachments: { type: Boolean, default: false },
|
||||
sendWithSignature: { type: Boolean, default: false },
|
||||
messageSignature: { type: String, default: '' },
|
||||
channelType: { type: String, default: '' },
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
@@ -90,6 +91,10 @@ const replaceText = async message => {
|
||||
"
|
||||
enable-variables
|
||||
:show-character-count="false"
|
||||
:signature="messageSignature"
|
||||
allow-signature
|
||||
:send-with-signature="sendWithSignature"
|
||||
:channel-type="channelType"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
|
||||
@@ -36,6 +36,7 @@ const props = defineProps({
|
||||
icon: { type: [String, Object, Function], default: '' },
|
||||
trailingIcon: { type: Boolean, default: false },
|
||||
isLoading: { type: Boolean, default: false },
|
||||
noAnimation: { type: Boolean, default: false },
|
||||
});
|
||||
|
||||
const slots = useSlots();
|
||||
@@ -179,12 +180,18 @@ const STYLE_CONFIG = {
|
||||
md: 'text-sm font-medium',
|
||||
lg: 'text-base',
|
||||
},
|
||||
clickAnimation: {
|
||||
xs: 'active:enabled:scale-[0.97]',
|
||||
sm: 'active:enabled:scale-[0.97]',
|
||||
md: 'active:enabled:scale-[0.98]',
|
||||
lg: 'active:enabled:scale-[0.98]',
|
||||
},
|
||||
justify: {
|
||||
start: 'justify-start',
|
||||
center: 'justify-center',
|
||||
end: 'justify-end',
|
||||
},
|
||||
base: 'inline-flex items-center min-w-0 gap-2 transition-all duration-200 ease-in-out border-0 rounded-lg outline-1 outline disabled:opacity-50',
|
||||
base: 'inline-flex items-center min-w-0 gap-2 transition-all duration-100 ease-out border-0 rounded-lg outline-1 outline disabled:opacity-50',
|
||||
};
|
||||
|
||||
const variantClasses = computed(() => {
|
||||
@@ -221,6 +228,12 @@ const linkButtonClasses = computed(() => {
|
||||
|
||||
return classes.join(' ');
|
||||
});
|
||||
|
||||
const animationClasses = computed(() => {
|
||||
return props.noAnimation
|
||||
? ''
|
||||
: STYLE_CONFIG.clickAnimation[computedSize.value];
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -230,6 +243,7 @@ const linkButtonClasses = computed(() => {
|
||||
[STYLE_CONFIG.base]: true,
|
||||
[isLink ? linkButtonClasses : buttonClasses]: true,
|
||||
[STYLE_CONFIG.fontSize[computedSize]]: true,
|
||||
[animationClasses]: true,
|
||||
[STYLE_CONFIG.justify[computedJustify]]: true,
|
||||
'flex-row-reverse': trailingIcon && !isIconOnly,
|
||||
}"
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
noAnimation: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="
|
||||
noAnimation
|
||||
? ''
|
||||
: 'has-[button:not(:disabled):active]:scale-[0.98] transition-transform duration-150 ease-out'
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
@@ -114,6 +114,7 @@ const handlePageChange = event => {
|
||||
<slot name="action" />
|
||||
</div>
|
||||
</div>
|
||||
<slot name="subHeader" />
|
||||
</div>
|
||||
</header>
|
||||
<main class="flex-1 px-6 overflow-y-auto">
|
||||
|
||||
@@ -64,20 +64,23 @@ const bulkCheckboxState = computed({
|
||||
class="flex items-center gap-3 py-1 ltr:pl-3 rtl:pr-3 ltr:pr-4 rtl:pl-4 rounded-lg bg-n-solid-2 outline outline-1 outline-n-container shadow"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<div class="flex items-center gap-1.5 min-w-0">
|
||||
<Checkbox
|
||||
v-model="bulkCheckboxState"
|
||||
:indeterminate="isIndeterminate"
|
||||
/>
|
||||
<span class="text-sm font-medium text-n-slate-12 tabular-nums">
|
||||
<span
|
||||
class="text-sm font-medium truncate text-n-slate-12 tabular-nums"
|
||||
>
|
||||
{{ selectAllLabel }}
|
||||
</span>
|
||||
</div>
|
||||
<span class="text-sm text-n-slate-10 tabular-nums">
|
||||
<span class="text-sm text-n-slate-10 truncate tabular-nums">
|
||||
{{ selectedCountLabel }}
|
||||
</span>
|
||||
<div class="h-4 w-px bg-n-strong" />
|
||||
<slot name="secondary-actions" />
|
||||
</div>
|
||||
<div class="h-4 w-px bg-n-strong" />
|
||||
<div class="flex items-center gap-3">
|
||||
<slot name="actions" :selected-count="selectedCount">
|
||||
<Button
|
||||
|
||||
@@ -9,6 +9,7 @@ import DropdownMenu from 'dashboard/components-next/dropdown-menu/DropdownMenu.v
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import Checkbox from 'dashboard/components-next/checkbox/Checkbox.vue';
|
||||
import Policy from 'dashboard/components/policy.vue';
|
||||
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
@@ -59,6 +60,10 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
showActions: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['action', 'navigate', 'select', 'hover']);
|
||||
@@ -159,73 +164,116 @@ const handleDocumentableClick = () => {
|
||||
<span class="text-n-slate-11 text-sm line-clamp-5">
|
||||
{{ answer }}
|
||||
</span>
|
||||
<div v-if="!compact" class="items-center justify-between hidden lg:flex">
|
||||
<div class="inline-flex items-center">
|
||||
<span
|
||||
class="text-sm shrink-0 truncate text-n-slate-11 inline-flex items-center gap-1"
|
||||
>
|
||||
<i class="i-woot-captain" />
|
||||
{{ assistant?.name || '' }}
|
||||
</span>
|
||||
<div
|
||||
v-if="documentable"
|
||||
class="shrink-0 text-sm text-n-slate-11 inline-flex line-clamp-1 gap-1 ml-3"
|
||||
>
|
||||
<div
|
||||
v-if="!compact"
|
||||
class="flex items-start justify-between flex-col-reverse md:flex-row gap-3"
|
||||
>
|
||||
<Policy v-if="showActions" :permissions="['administrator']">
|
||||
<div class="flex items-center gap-2 sm:gap-5 w-full">
|
||||
<Button
|
||||
v-if="status === 'pending'"
|
||||
:label="$t('CAPTAIN.RESPONSES.OPTIONS.APPROVE')"
|
||||
icon="i-lucide-circle-check-big"
|
||||
sm
|
||||
link
|
||||
class="hover:!no-underline"
|
||||
@click="
|
||||
handleAssistantAction({ action: 'approve', value: 'approve' })
|
||||
"
|
||||
/>
|
||||
<Button
|
||||
:label="$t('CAPTAIN.RESPONSES.OPTIONS.EDIT_RESPONSE')"
|
||||
icon="i-lucide-pencil-line"
|
||||
sm
|
||||
slate
|
||||
link
|
||||
class="hover:!no-underline"
|
||||
@click="
|
||||
handleAssistantAction({
|
||||
action: 'edit',
|
||||
value: 'edit',
|
||||
})
|
||||
"
|
||||
/>
|
||||
<Button
|
||||
:label="$t('CAPTAIN.RESPONSES.OPTIONS.DELETE_RESPONSE')"
|
||||
icon="i-lucide-trash"
|
||||
sm
|
||||
ruby
|
||||
link
|
||||
class="hover:!no-underline"
|
||||
@click="
|
||||
handleAssistantAction({ action: 'delete', value: 'delete' })
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</Policy>
|
||||
<div
|
||||
class="flex items-center gap-3"
|
||||
:class="{ 'justify-between w-full': !showActions }"
|
||||
>
|
||||
<div class="inline-flex items-center gap-3 min-w-0">
|
||||
<span
|
||||
v-if="documentable.type === 'Captain::Document'"
|
||||
class="inline-flex items-center gap-1 truncate over"
|
||||
v-if="status === 'approved'"
|
||||
class="text-sm shrink-0 truncate text-n-slate-11 inline-flex items-center gap-1"
|
||||
>
|
||||
<i class="i-ph-files-light text-base" />
|
||||
<span class="max-w-96 truncate" :title="documentable.name">
|
||||
<Icon icon="i-woot-captain" class="size-3.5" />
|
||||
{{ assistant?.name || '' }}
|
||||
</span>
|
||||
<div
|
||||
v-if="documentable"
|
||||
class="text-sm text-n-slate-11 grid grid-cols-[auto_1fr] items-center gap-1 min-w-0"
|
||||
>
|
||||
<Icon
|
||||
v-if="documentable.type === 'Captain::Document'"
|
||||
icon="i-ph-files-light"
|
||||
class="size-3.5"
|
||||
/>
|
||||
<Icon
|
||||
v-else-if="documentable.type === 'User'"
|
||||
icon="i-ph-user-circle-plus"
|
||||
class="size-3.5"
|
||||
/>
|
||||
<Icon
|
||||
v-else-if="documentable.type === 'Conversation'"
|
||||
icon="i-ph-chat-circle-dots"
|
||||
class="size-3.5"
|
||||
/>
|
||||
<span
|
||||
v-if="documentable.type === 'Captain::Document'"
|
||||
class="truncate"
|
||||
:title="documentable.name"
|
||||
>
|
||||
{{ documentable.name }}
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
v-if="documentable.type === 'User'"
|
||||
class="inline-flex items-center gap-1"
|
||||
>
|
||||
<i class="i-ph-user-circle-plus text-base" />
|
||||
<span
|
||||
class="max-w-96 truncate"
|
||||
v-else-if="documentable.type === 'User'"
|
||||
class="truncate"
|
||||
:title="documentable.available_name"
|
||||
>
|
||||
{{ documentable.available_name }}
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
v-else-if="documentable.type === 'Conversation'"
|
||||
class="inline-flex items-center gap-1 group cursor-pointer"
|
||||
role="button"
|
||||
@click="handleDocumentableClick"
|
||||
>
|
||||
<i class="i-ph-chat-circle-dots text-base" />
|
||||
<span class="group-hover:underline">
|
||||
<span
|
||||
v-else-if="documentable.type === 'Conversation'"
|
||||
class="hover:underline truncate cursor-pointer"
|
||||
role="button"
|
||||
@click="handleDocumentableClick"
|
||||
>
|
||||
{{
|
||||
t(`CAPTAIN.RESPONSES.DOCUMENTABLE.CONVERSATION`, {
|
||||
id: documentable.display_id,
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
</span>
|
||||
<span v-else />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="status !== 'approved'"
|
||||
class="shrink-0 text-sm text-n-slate-11 line-clamp-1 inline-flex items-center gap-1 ml-3"
|
||||
class="shrink-0 text-sm text-n-slate-11 line-clamp-1 inline-flex items-center gap-1"
|
||||
>
|
||||
<i
|
||||
class="i-ph-stack text-base"
|
||||
:title="t('CAPTAIN.RESPONSES.STATUS.TITLE')"
|
||||
/>
|
||||
{{ t(`CAPTAIN.RESPONSES.STATUS.${status.toUpperCase()}`) }}
|
||||
<Icon icon="i-ph-calendar-dot" class="size-3.5" />
|
||||
{{ timestamp }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="shrink-0 text-sm text-n-slate-11 line-clamp-1 inline-flex items-center gap-1 ml-3"
|
||||
>
|
||||
<i class="i-ph-calendar-dot" />
|
||||
{{ timestamp }}
|
||||
</div>
|
||||
</div>
|
||||
</CardLayout>
|
||||
</template>
|
||||
|
||||
@@ -10,6 +10,10 @@ const props = defineProps({
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
translationKey: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
entity: {
|
||||
type: Object,
|
||||
required: true,
|
||||
@@ -25,7 +29,9 @@ const emit = defineEmits(['deleteSuccess']);
|
||||
const { t } = useI18n();
|
||||
const store = useStore();
|
||||
const deleteDialogRef = ref(null);
|
||||
const i18nKey = computed(() => props.type.toUpperCase());
|
||||
const i18nKey = computed(() => {
|
||||
return props.translationKey || props.type.toUpperCase();
|
||||
});
|
||||
|
||||
const deleteEntity = async payload => {
|
||||
if (!payload) return;
|
||||
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
<script setup>
|
||||
import { defineModel, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import Input from 'dashboard/components-next/input/Input.vue';
|
||||
|
||||
const props = defineProps({
|
||||
authType: {
|
||||
type: String,
|
||||
required: true,
|
||||
validator: value => ['none', 'bearer', 'basic', 'api_key'].includes(value),
|
||||
},
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const authConfig = defineModel('authConfig', {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.authType,
|
||||
() => {
|
||||
authConfig.value = {};
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-2">
|
||||
<Input
|
||||
v-if="authType === 'bearer'"
|
||||
v-model="authConfig.token"
|
||||
:label="t('CAPTAIN.CUSTOM_TOOLS.FORM.AUTH_CONFIG.BEARER_TOKEN')"
|
||||
:placeholder="
|
||||
t('CAPTAIN.CUSTOM_TOOLS.FORM.AUTH_CONFIG.BEARER_TOKEN_PLACEHOLDER')
|
||||
"
|
||||
/>
|
||||
<template v-else-if="authType === 'basic'">
|
||||
<Input
|
||||
v-model="authConfig.username"
|
||||
:label="t('CAPTAIN.CUSTOM_TOOLS.FORM.AUTH_CONFIG.USERNAME')"
|
||||
:placeholder="
|
||||
t('CAPTAIN.CUSTOM_TOOLS.FORM.AUTH_CONFIG.USERNAME_PLACEHOLDER')
|
||||
"
|
||||
/>
|
||||
<Input
|
||||
v-model="authConfig.password"
|
||||
type="password"
|
||||
:label="t('CAPTAIN.CUSTOM_TOOLS.FORM.AUTH_CONFIG.PASSWORD')"
|
||||
:placeholder="
|
||||
t('CAPTAIN.CUSTOM_TOOLS.FORM.AUTH_CONFIG.PASSWORD_PLACEHOLDER')
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="authType === 'api_key'">
|
||||
<Input
|
||||
v-model="authConfig.name"
|
||||
:label="t('CAPTAIN.CUSTOM_TOOLS.FORM.AUTH_CONFIG.API_KEY')"
|
||||
:placeholder="
|
||||
t('CAPTAIN.CUSTOM_TOOLS.FORM.AUTH_CONFIG.API_KEY_PLACEHOLDER')
|
||||
"
|
||||
/>
|
||||
<Input
|
||||
v-model="authConfig.key"
|
||||
:label="t('CAPTAIN.CUSTOM_TOOLS.FORM.AUTH_CONFIG.API_VALUE')"
|
||||
:placeholder="
|
||||
t('CAPTAIN.CUSTOM_TOOLS.FORM.AUTH_CONFIG.API_VALUE_PLACEHOLDER')
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { useStore } from 'dashboard/composables/store';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { parseAPIErrorResponse } from 'dashboard/store/utils/api';
|
||||
|
||||
import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
|
||||
import CustomToolForm from './CustomToolForm.vue';
|
||||
|
||||
const props = defineProps({
|
||||
selectedTool: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'create',
|
||||
validator: value => ['create', 'edit'].includes(value),
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['close']);
|
||||
const { t } = useI18n();
|
||||
const store = useStore();
|
||||
|
||||
const dialogRef = ref(null);
|
||||
|
||||
const updateTool = toolDetails =>
|
||||
store.dispatch('captainCustomTools/update', {
|
||||
id: props.selectedTool.id,
|
||||
...toolDetails,
|
||||
});
|
||||
|
||||
const i18nKey = computed(
|
||||
() => `CAPTAIN.CUSTOM_TOOLS.${props.type.toUpperCase()}`
|
||||
);
|
||||
|
||||
const createTool = toolDetails =>
|
||||
store.dispatch('captainCustomTools/create', toolDetails);
|
||||
|
||||
const handleSubmit = async updatedTool => {
|
||||
try {
|
||||
if (props.type === 'edit') {
|
||||
await updateTool(updatedTool);
|
||||
} else {
|
||||
await createTool(updatedTool);
|
||||
}
|
||||
useAlert(t(`${i18nKey.value}.SUCCESS_MESSAGE`));
|
||||
dialogRef.value.close();
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
parseAPIErrorResponse(error) || t(`${i18nKey.value}.ERROR_MESSAGE`);
|
||||
useAlert(errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
emit('close');
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
dialogRef.value.close();
|
||||
};
|
||||
|
||||
defineExpose({ dialogRef });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog
|
||||
ref="dialogRef"
|
||||
width="2xl"
|
||||
:title="$t(`${i18nKey}.TITLE`)"
|
||||
:description="$t('CAPTAIN.CUSTOM_TOOLS.FORM_DESCRIPTION')"
|
||||
:show-cancel-button="false"
|
||||
:show-confirm-button="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<CustomToolForm
|
||||
:mode="type"
|
||||
:tool="selectedTool"
|
||||
@submit="handleSubmit"
|
||||
@cancel="handleCancel"
|
||||
/>
|
||||
<template #footer />
|
||||
</Dialog>
|
||||
</template>
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useToggle } from '@vueuse/core';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { dynamicTime } from 'shared/helpers/timeHelper';
|
||||
|
||||
import CardLayout from 'dashboard/components-next/CardLayout.vue';
|
||||
import DropdownMenu from 'dashboard/components-next/dropdown-menu/DropdownMenu.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import Policy from 'dashboard/components/policy.vue';
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
authType: {
|
||||
type: String,
|
||||
default: 'none',
|
||||
},
|
||||
updatedAt: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
createdAt: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['action']);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const [showActionsDropdown, toggleDropdown] = useToggle();
|
||||
|
||||
const menuItems = computed(() => [
|
||||
{
|
||||
label: t('CAPTAIN.CUSTOM_TOOLS.OPTIONS.EDIT_TOOL'),
|
||||
value: 'edit',
|
||||
action: 'edit',
|
||||
icon: 'i-lucide-pencil-line',
|
||||
},
|
||||
{
|
||||
label: t('CAPTAIN.CUSTOM_TOOLS.OPTIONS.DELETE_TOOL'),
|
||||
value: 'delete',
|
||||
action: 'delete',
|
||||
icon: 'i-lucide-trash',
|
||||
},
|
||||
]);
|
||||
|
||||
const timestamp = computed(() =>
|
||||
dynamicTime(props.updatedAt || props.createdAt)
|
||||
);
|
||||
|
||||
const handleAction = ({ action, value }) => {
|
||||
toggleDropdown(false);
|
||||
emit('action', { action, value, id: props.id });
|
||||
};
|
||||
|
||||
const authTypeLabel = computed(() => {
|
||||
return t(
|
||||
`CAPTAIN.CUSTOM_TOOLS.FORM.AUTH_TYPES.${props.authType.toUpperCase()}`
|
||||
);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CardLayout class="relative">
|
||||
<div class="flex relative justify-between w-full gap-1">
|
||||
<span class="text-base text-n-slate-12 line-clamp-1 font-medium">
|
||||
{{ title }}
|
||||
</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<Policy
|
||||
v-on-clickaway="() => toggleDropdown(false)"
|
||||
:permissions="['administrator']"
|
||||
class="relative flex items-center group"
|
||||
>
|
||||
<Button
|
||||
icon="i-lucide-ellipsis-vertical"
|
||||
color="slate"
|
||||
size="xs"
|
||||
class="rounded-md group-hover:bg-n-alpha-2"
|
||||
@click="toggleDropdown()"
|
||||
/>
|
||||
<DropdownMenu
|
||||
v-if="showActionsDropdown"
|
||||
:menu-items="menuItems"
|
||||
class="mt-1 ltr:right-0 rtl:right-0 top-full"
|
||||
@action="handleAction($event)"
|
||||
/>
|
||||
</Policy>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between w-full gap-4">
|
||||
<div class="flex items-center gap-3 flex-1">
|
||||
<span
|
||||
v-if="description"
|
||||
class="text-sm truncate text-n-slate-11 flex-1"
|
||||
>
|
||||
{{ description }}
|
||||
</span>
|
||||
<span
|
||||
v-if="authType !== 'none'"
|
||||
class="text-sm shrink-0 text-n-slate-11 inline-flex items-center gap-1"
|
||||
>
|
||||
<i class="i-lucide-lock text-base" />
|
||||
{{ authTypeLabel }}
|
||||
</span>
|
||||
</div>
|
||||
<span class="text-sm text-n-slate-11 line-clamp-1 shrink-0">
|
||||
{{ timestamp }}
|
||||
</span>
|
||||
</div>
|
||||
</CardLayout>
|
||||
</template>
|
||||
+271
@@ -0,0 +1,271 @@
|
||||
<script setup>
|
||||
import { reactive, computed, useTemplateRef, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { required } from '@vuelidate/validators';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
|
||||
import Input from 'dashboard/components-next/input/Input.vue';
|
||||
import TextArea from 'dashboard/components-next/textarea/TextArea.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import ComboBox from 'dashboard/components-next/combobox/ComboBox.vue';
|
||||
import ParamRow from './ParamRow.vue';
|
||||
import AuthConfig from './AuthConfig.vue';
|
||||
|
||||
const props = defineProps({
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'create',
|
||||
validator: value => ['create', 'edit'].includes(value),
|
||||
},
|
||||
tool: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['submit', 'cancel']);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const formState = {
|
||||
uiFlags: useMapGetter('captainCustomTools/getUIFlags'),
|
||||
};
|
||||
|
||||
const initialState = {
|
||||
title: '',
|
||||
description: '',
|
||||
endpoint_url: '',
|
||||
http_method: 'GET',
|
||||
request_template: '',
|
||||
response_template: '',
|
||||
auth_type: 'none',
|
||||
auth_config: {},
|
||||
param_schema: [],
|
||||
};
|
||||
|
||||
const state = reactive({ ...initialState });
|
||||
|
||||
// Populate form when in edit mode
|
||||
watch(
|
||||
() => props.tool,
|
||||
newTool => {
|
||||
if (props.mode === 'edit' && newTool && newTool.id) {
|
||||
state.title = newTool.title || '';
|
||||
state.description = newTool.description || '';
|
||||
state.endpoint_url = newTool.endpoint_url || '';
|
||||
state.http_method = newTool.http_method || 'GET';
|
||||
state.request_template = newTool.request_template || '';
|
||||
state.response_template = newTool.response_template || '';
|
||||
state.auth_type = newTool.auth_type || 'none';
|
||||
state.auth_config = newTool.auth_config || {};
|
||||
state.param_schema = newTool.param_schema || [];
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const DEFAULT_PARAM = {
|
||||
name: '',
|
||||
type: 'string',
|
||||
description: '',
|
||||
required: false,
|
||||
};
|
||||
|
||||
const validationRules = {
|
||||
title: { required },
|
||||
endpoint_url: { required },
|
||||
http_method: { required },
|
||||
auth_type: { required },
|
||||
};
|
||||
|
||||
const httpMethodOptions = computed(() => [
|
||||
{ value: 'GET', label: 'GET' },
|
||||
{ value: 'POST', label: 'POST' },
|
||||
]);
|
||||
|
||||
const authTypeOptions = computed(() => [
|
||||
{ value: 'none', label: t('CAPTAIN.CUSTOM_TOOLS.FORM.AUTH_TYPES.NONE') },
|
||||
{ value: 'bearer', label: t('CAPTAIN.CUSTOM_TOOLS.FORM.AUTH_TYPES.BEARER') },
|
||||
{ value: 'basic', label: t('CAPTAIN.CUSTOM_TOOLS.FORM.AUTH_TYPES.BASIC') },
|
||||
{
|
||||
value: 'api_key',
|
||||
label: t('CAPTAIN.CUSTOM_TOOLS.FORM.AUTH_TYPES.API_KEY'),
|
||||
},
|
||||
]);
|
||||
|
||||
const v$ = useVuelidate(validationRules, state);
|
||||
|
||||
const isLoading = computed(() =>
|
||||
props.mode === 'edit'
|
||||
? formState.uiFlags.value.updatingItem
|
||||
: formState.uiFlags.value.creatingItem
|
||||
);
|
||||
|
||||
const getErrorMessage = (field, errorKey) => {
|
||||
return v$.value[field].$error
|
||||
? t(`CAPTAIN.CUSTOM_TOOLS.FORM.${errorKey}.ERROR`)
|
||||
: '';
|
||||
};
|
||||
|
||||
const formErrors = computed(() => ({
|
||||
title: getErrorMessage('title', 'TITLE'),
|
||||
endpoint_url: getErrorMessage('endpoint_url', 'ENDPOINT_URL'),
|
||||
}));
|
||||
|
||||
const paramsRef = useTemplateRef('paramsRef');
|
||||
|
||||
const isParamsValid = () => {
|
||||
if (!paramsRef.value || paramsRef.value.length === 0) {
|
||||
return true;
|
||||
}
|
||||
return paramsRef.value.every(param => param.validate());
|
||||
};
|
||||
|
||||
const removeParam = index => {
|
||||
state.param_schema.splice(index, 1);
|
||||
};
|
||||
|
||||
const addParam = () => {
|
||||
state.param_schema.push({ ...DEFAULT_PARAM });
|
||||
};
|
||||
|
||||
const handleCancel = () => emit('cancel');
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const isFormValid = await v$.value.$validate();
|
||||
if (!isFormValid || !isParamsValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
emit('submit', state);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form
|
||||
class="flex flex-col px-4 -mx-4 gap-4 max-h-[calc(100vh-200px)] overflow-y-scroll"
|
||||
@submit.prevent="handleSubmit"
|
||||
>
|
||||
<Input
|
||||
v-model="state.title"
|
||||
:label="t('CAPTAIN.CUSTOM_TOOLS.FORM.TITLE.LABEL')"
|
||||
:placeholder="t('CAPTAIN.CUSTOM_TOOLS.FORM.TITLE.PLACEHOLDER')"
|
||||
:message="formErrors.title"
|
||||
:message-type="formErrors.title ? 'error' : 'info'"
|
||||
/>
|
||||
|
||||
<TextArea
|
||||
v-model="state.description"
|
||||
:label="t('CAPTAIN.CUSTOM_TOOLS.FORM.DESCRIPTION.LABEL')"
|
||||
:placeholder="t('CAPTAIN.CUSTOM_TOOLS.FORM.DESCRIPTION.PLACEHOLDER')"
|
||||
:rows="2"
|
||||
/>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<div class="flex flex-col gap-1 w-28">
|
||||
<label class="mb-0.5 text-sm font-medium text-n-slate-12">
|
||||
{{ t('CAPTAIN.CUSTOM_TOOLS.FORM.HTTP_METHOD.LABEL') }}
|
||||
</label>
|
||||
<ComboBox
|
||||
v-model="state.http_method"
|
||||
:options="httpMethodOptions"
|
||||
class="[&>div>button]:bg-n-alpha-black2 [&_li]:font-mono [&_button]:font-mono [&>div>button]:outline-offset-[-1px]"
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
v-model="state.endpoint_url"
|
||||
:label="t('CAPTAIN.CUSTOM_TOOLS.FORM.ENDPOINT_URL.LABEL')"
|
||||
:placeholder="t('CAPTAIN.CUSTOM_TOOLS.FORM.ENDPOINT_URL.PLACEHOLDER')"
|
||||
:message="formErrors.endpoint_url"
|
||||
:message-type="formErrors.endpoint_url ? 'error' : 'info'"
|
||||
class="flex-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<label class="mb-0.5 text-sm font-medium text-n-slate-12">
|
||||
{{ t('CAPTAIN.CUSTOM_TOOLS.FORM.AUTH_TYPE.LABEL') }}
|
||||
</label>
|
||||
<ComboBox
|
||||
v-model="state.auth_type"
|
||||
:options="authTypeOptions"
|
||||
class="[&>div>button]:bg-n-alpha-black2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<AuthConfig
|
||||
v-model:auth-config="state.auth_config"
|
||||
:auth-type="state.auth_type"
|
||||
/>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="text-sm font-medium text-n-slate-12">
|
||||
{{ t('CAPTAIN.CUSTOM_TOOLS.FORM.PARAMETERS.LABEL') }}
|
||||
</label>
|
||||
<p class="text-xs text-n-slate-11 -mt-1">
|
||||
{{ t('CAPTAIN.CUSTOM_TOOLS.FORM.PARAMETERS.HELP_TEXT') }}
|
||||
</p>
|
||||
<ul v-if="state.param_schema.length > 0" class="grid gap-2 list-none">
|
||||
<ParamRow
|
||||
v-for="(param, index) in state.param_schema"
|
||||
:key="index"
|
||||
ref="paramsRef"
|
||||
v-model:name="param.name"
|
||||
v-model:type="param.type"
|
||||
v-model:description="param.description"
|
||||
v-model:required="param.required"
|
||||
@remove="removeParam(index)"
|
||||
/>
|
||||
</ul>
|
||||
<Button
|
||||
type="button"
|
||||
sm
|
||||
ghost
|
||||
blue
|
||||
icon="i-lucide-plus"
|
||||
:label="t('CAPTAIN.CUSTOM_TOOLS.FORM.ADD_PARAMETER')"
|
||||
@click="addParam"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<TextArea
|
||||
v-if="state.http_method === 'POST'"
|
||||
v-model="state.request_template"
|
||||
:label="t('CAPTAIN.CUSTOM_TOOLS.FORM.REQUEST_TEMPLATE.LABEL')"
|
||||
:placeholder="t('CAPTAIN.CUSTOM_TOOLS.FORM.REQUEST_TEMPLATE.PLACEHOLDER')"
|
||||
:rows="4"
|
||||
class="[&_textarea]:font-mono"
|
||||
/>
|
||||
|
||||
<TextArea
|
||||
v-model="state.response_template"
|
||||
:label="t('CAPTAIN.CUSTOM_TOOLS.FORM.RESPONSE_TEMPLATE.LABEL')"
|
||||
:placeholder="
|
||||
t('CAPTAIN.CUSTOM_TOOLS.FORM.RESPONSE_TEMPLATE.PLACEHOLDER')
|
||||
"
|
||||
:rows="4"
|
||||
class="[&_textarea]:font-mono"
|
||||
/>
|
||||
|
||||
<div class="flex gap-3 justify-between items-center w-full">
|
||||
<Button
|
||||
type="button"
|
||||
variant="faded"
|
||||
color="slate"
|
||||
:label="t('CAPTAIN.FORM.CANCEL')"
|
||||
class="w-full bg-n-alpha-2 text-n-blue-text hover:bg-n-alpha-3"
|
||||
@click="handleCancel"
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
:label="
|
||||
t(mode === 'edit' ? 'CAPTAIN.FORM.EDIT' : 'CAPTAIN.FORM.CREATE')
|
||||
"
|
||||
class="w-full"
|
||||
:is-loading="isLoading"
|
||||
:disabled="isLoading"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
<script setup>
|
||||
import { computed, defineModel, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import Input from 'dashboard/components-next/input/Input.vue';
|
||||
import ComboBox from 'dashboard/components-next/combobox/ComboBox.vue';
|
||||
import Checkbox from 'dashboard/components-next/checkbox/Checkbox.vue';
|
||||
|
||||
const emit = defineEmits(['remove']);
|
||||
const { t } = useI18n();
|
||||
const showErrors = ref(false);
|
||||
|
||||
const name = defineModel('name', {
|
||||
type: String,
|
||||
required: true,
|
||||
});
|
||||
|
||||
const type = defineModel('type', {
|
||||
type: String,
|
||||
required: true,
|
||||
});
|
||||
|
||||
const description = defineModel('description', {
|
||||
type: String,
|
||||
default: '',
|
||||
});
|
||||
|
||||
const required = defineModel('required', {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
});
|
||||
|
||||
const paramTypeOptions = computed(() => [
|
||||
{ value: 'string', label: t('CAPTAIN.CUSTOM_TOOLS.FORM.PARAM_TYPES.STRING') },
|
||||
{ value: 'number', label: t('CAPTAIN.CUSTOM_TOOLS.FORM.PARAM_TYPES.NUMBER') },
|
||||
{
|
||||
value: 'boolean',
|
||||
label: t('CAPTAIN.CUSTOM_TOOLS.FORM.PARAM_TYPES.BOOLEAN'),
|
||||
},
|
||||
{ value: 'array', label: t('CAPTAIN.CUSTOM_TOOLS.FORM.PARAM_TYPES.ARRAY') },
|
||||
{ value: 'object', label: t('CAPTAIN.CUSTOM_TOOLS.FORM.PARAM_TYPES.OBJECT') },
|
||||
]);
|
||||
|
||||
const validationError = computed(() => {
|
||||
if (!name.value || name.value.trim() === '') {
|
||||
return 'PARAM_NAME_REQUIRED';
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
watch([name, type, description, required], () => {
|
||||
showErrors.value = false;
|
||||
});
|
||||
|
||||
const validate = () => {
|
||||
showErrors.value = true;
|
||||
return !validationError.value;
|
||||
};
|
||||
|
||||
defineExpose({ validate });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<li class="list-none">
|
||||
<div
|
||||
class="flex items-start gap-2 p-3 rounded-lg border border-n-weak bg-n-alpha-2"
|
||||
:class="{
|
||||
'animate-wiggle border-n-ruby-9': showErrors && validationError,
|
||||
}"
|
||||
>
|
||||
<div class="flex flex-col flex-1 gap-3">
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<Input
|
||||
v-model="name"
|
||||
:placeholder="t('CAPTAIN.CUSTOM_TOOLS.FORM.PARAM_NAME.PLACEHOLDER')"
|
||||
class="col-span-2"
|
||||
/>
|
||||
<ComboBox
|
||||
v-model="type"
|
||||
:options="paramTypeOptions"
|
||||
:placeholder="t('CAPTAIN.CUSTOM_TOOLS.FORM.PARAM_TYPE.PLACEHOLDER')"
|
||||
class="[&>div>button]:bg-n-alpha-black2"
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
v-model="description"
|
||||
:placeholder="
|
||||
t('CAPTAIN.CUSTOM_TOOLS.FORM.PARAM_DESCRIPTION.PLACEHOLDER')
|
||||
"
|
||||
/>
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<Checkbox v-model="required" />
|
||||
<span class="text-sm text-n-slate-11">
|
||||
{{ t('CAPTAIN.CUSTOM_TOOLS.FORM.PARAM_REQUIRED.LABEL') }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<Button
|
||||
solid
|
||||
slate
|
||||
icon="i-lucide-trash"
|
||||
class="flex-shrink-0"
|
||||
@click.stop="emit('remove')"
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
v-if="showErrors && validationError"
|
||||
class="block mt-1 text-sm text-n-ruby-11"
|
||||
>
|
||||
{{ t(`CAPTAIN.CUSTOM_TOOLS.FORM.ERRORS.${validationError}`) }}
|
||||
</span>
|
||||
</li>
|
||||
</template>
|
||||
+3
@@ -1,4 +1,5 @@
|
||||
<script setup>
|
||||
import { useAccount } from 'dashboard/composables/useAccount';
|
||||
import EmptyStateLayout from 'dashboard/components-next/EmptyStateLayout.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import AssistantCard from 'dashboard/components-next/captain/assistant/AssistantCard.vue';
|
||||
@@ -6,6 +7,7 @@ import FeatureSpotlight from 'dashboard/components-next/feature-spotlight/Featur
|
||||
import { assistantsList } from 'dashboard/components-next/captain/pageComponents/emptyStates/captainEmptyStateContent.js';
|
||||
|
||||
const emit = defineEmits(['click']);
|
||||
const { isOnChatwootCloud } = useAccount();
|
||||
|
||||
const onClick = () => {
|
||||
emit('click');
|
||||
@@ -20,6 +22,7 @@ const onClick = () => {
|
||||
fallback-thumbnail-dark="/assets/images/dashboard/captain/assistant-dark.svg"
|
||||
learn-more-url="https://chwt.app/captain-assistant"
|
||||
class="mb-8"
|
||||
:hide-actions="!isOnChatwootCloud"
|
||||
/>
|
||||
<EmptyStateLayout
|
||||
:title="$t('CAPTAIN.ASSISTANTS.EMPTY_STATE.TITLE')"
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
<script setup>
|
||||
import EmptyStateLayout from 'dashboard/components-next/EmptyStateLayout.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
const emit = defineEmits(['click']);
|
||||
|
||||
const onClick = () => {
|
||||
emit('click');
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EmptyStateLayout
|
||||
:title="$t('CAPTAIN.CUSTOM_TOOLS.EMPTY_STATE.TITLE')"
|
||||
:subtitle="$t('CAPTAIN.CUSTOM_TOOLS.EMPTY_STATE.SUBTITLE')"
|
||||
:action-perms="['administrator']"
|
||||
>
|
||||
<template #empty-state-item>
|
||||
<div class="min-h-[600px]" />
|
||||
</template>
|
||||
<template #actions>
|
||||
<Button
|
||||
:label="$t('CAPTAIN.CUSTOM_TOOLS.ADD_NEW')"
|
||||
icon="i-lucide-plus"
|
||||
@click="onClick"
|
||||
/>
|
||||
</template>
|
||||
</EmptyStateLayout>
|
||||
</template>
|
||||
+3
@@ -1,4 +1,5 @@
|
||||
<script setup>
|
||||
import { useAccount } from 'dashboard/composables/useAccount';
|
||||
import EmptyStateLayout from 'dashboard/components-next/EmptyStateLayout.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import DocumentCard from 'dashboard/components-next/captain/assistant/DocumentCard.vue';
|
||||
@@ -6,6 +7,7 @@ import FeatureSpotlight from 'dashboard/components-next/feature-spotlight/Featur
|
||||
import { documentsList } from 'dashboard/components-next/captain/pageComponents/emptyStates/captainEmptyStateContent.js';
|
||||
|
||||
const emit = defineEmits(['click']);
|
||||
const { isOnChatwootCloud } = useAccount();
|
||||
|
||||
const onClick = () => {
|
||||
emit('click');
|
||||
@@ -19,6 +21,7 @@ const onClick = () => {
|
||||
fallback-thumbnail="/assets/images/dashboard/captain/document-light.svg"
|
||||
fallback-thumbnail-dark="/assets/images/dashboard/captain/document-dark.svg"
|
||||
learn-more-url="https://chwt.app/captain-document"
|
||||
:hide-actions="!isOnChatwootCloud"
|
||||
class="mb-8"
|
||||
/>
|
||||
<EmptyStateLayout
|
||||
|
||||
+50
-9
@@ -1,32 +1,63 @@
|
||||
<script setup>
|
||||
import { useAccount } from 'dashboard/composables/useAccount';
|
||||
import EmptyStateLayout from 'dashboard/components-next/EmptyStateLayout.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import ResponseCard from 'dashboard/components-next/captain/assistant/ResponseCard.vue';
|
||||
import FeatureSpotlight from 'dashboard/components-next/feature-spotlight/FeatureSpotlight.vue';
|
||||
import { responsesList } from 'dashboard/components-next/captain/pageComponents/emptyStates/captainEmptyStateContent.js';
|
||||
|
||||
const emit = defineEmits(['click']);
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
variant: {
|
||||
type: String,
|
||||
default: 'approved',
|
||||
validator: value => ['approved', 'pending'].includes(value),
|
||||
},
|
||||
hasActiveFilters: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['click', 'clearFilters']);
|
||||
|
||||
const isApproved = computed(() => props.variant === 'approved');
|
||||
const isPending = computed(() => props.variant === 'pending');
|
||||
|
||||
const { isOnChatwootCloud } = useAccount();
|
||||
|
||||
const onClick = () => {
|
||||
emit('click');
|
||||
};
|
||||
|
||||
const onClearFilters = () => {
|
||||
emit('clearFilters');
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FeatureSpotlight
|
||||
v-if="isApproved"
|
||||
:title="$t('CAPTAIN.RESPONSES.EMPTY_STATE.FEATURE_SPOTLIGHT.TITLE')"
|
||||
:note="$t('CAPTAIN.RESPONSES.EMPTY_STATE.FEATURE_SPOTLIGHT.NOTE')"
|
||||
fallback-thumbnail="/assets/images/dashboard/captain/faqs-light.svg"
|
||||
fallback-thumbnail-dark="/assets/images/dashboard/captain/faqs-dark.svg"
|
||||
learn-more-url="https://chwt.app/captain-faq"
|
||||
:hide-actions="!isOnChatwootCloud"
|
||||
class="mb-8"
|
||||
/>
|
||||
<EmptyStateLayout
|
||||
:title="$t('CAPTAIN.RESPONSES.EMPTY_STATE.TITLE')"
|
||||
:subtitle="$t('CAPTAIN.RESPONSES.EMPTY_STATE.SUBTITLE')"
|
||||
:title="
|
||||
isPending
|
||||
? $t('CAPTAIN.RESPONSES.EMPTY_STATE.NO_PENDING_TITLE')
|
||||
: $t('CAPTAIN.RESPONSES.EMPTY_STATE.TITLE')
|
||||
"
|
||||
:subtitle="isApproved ? $t('CAPTAIN.RESPONSES.EMPTY_STATE.SUBTITLE') : ''"
|
||||
:action-perms="['administrator']"
|
||||
:show-backdrop="isApproved"
|
||||
>
|
||||
<template #empty-state-item>
|
||||
<template v-if="isApproved" #empty-state-item>
|
||||
<div class="grid grid-cols-1 gap-4 p-px overflow-hidden">
|
||||
<ResponseCard
|
||||
v-for="(response, index) in responsesList.slice(0, 5)"
|
||||
@@ -42,11 +73,21 @@ const onClick = () => {
|
||||
</div>
|
||||
</template>
|
||||
<template #actions>
|
||||
<Button
|
||||
:label="$t('CAPTAIN.RESPONSES.ADD_NEW')"
|
||||
icon="i-lucide-plus"
|
||||
@click="onClick"
|
||||
/>
|
||||
<div class="flex flex-col items-center gap-3">
|
||||
<Button
|
||||
v-if="isApproved"
|
||||
:label="$t('CAPTAIN.RESPONSES.ADD_NEW')"
|
||||
icon="i-lucide-plus"
|
||||
@click="onClick"
|
||||
/>
|
||||
<Button
|
||||
v-else-if="isPending && hasActiveFilters"
|
||||
:label="$t('CAPTAIN.RESPONSES.EMPTY_STATE.CLEAR_SEARCH')"
|
||||
variant="link"
|
||||
size="sm"
|
||||
@click="onClearFilters"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</EmptyStateLayout>
|
||||
</template>
|
||||
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import GroupedStackedChangelogCard from './GroupedStackedChangelogCard.vue';
|
||||
|
||||
const sampleCards = [
|
||||
{
|
||||
id: 'chatwoot-captain',
|
||||
title: 'Chatwoot Captain',
|
||||
meta_title: 'Chatwoot Captain',
|
||||
meta_description:
|
||||
'Watch how our latest feature can transform your workflow with powerful automation tools.',
|
||||
slug: 'chatwoot-captain',
|
||||
feature_image:
|
||||
'https://www.chatwoot.com/images/captain/captain_thumbnail.jpg',
|
||||
},
|
||||
{
|
||||
id: 'smart-routing',
|
||||
title: 'Smart Routing Forms',
|
||||
meta_title: 'Smart Routing Forms',
|
||||
meta_description:
|
||||
'Screen bookers with intelligent forms and route them to the right team member.',
|
||||
slug: 'smart-routing',
|
||||
feature_image: 'https://www.chatwoot.com/images/dashboard-dark.webp',
|
||||
},
|
||||
{
|
||||
id: 'instant-meetings',
|
||||
title: 'Instant Meetings',
|
||||
meta_title: 'Instant Meetings',
|
||||
meta_description: 'Start instant meetings directly from shared links.',
|
||||
slug: 'instant-meetings',
|
||||
feature_image:
|
||||
'https://images.unsplash.com/photo-1587614382346-4ec70e388b28?w=600',
|
||||
},
|
||||
{
|
||||
id: 'analytics',
|
||||
title: 'Advanced Analytics',
|
||||
meta_title: 'Advanced Analytics',
|
||||
meta_description:
|
||||
'Track meeting performance, conversion, and response rates in one place.',
|
||||
slug: 'analytics',
|
||||
feature_image:
|
||||
'https://images.unsplash.com/photo-1522202176988-66273c2fd55f?w=500',
|
||||
},
|
||||
{
|
||||
id: 'team-collaboration',
|
||||
title: 'Team Collaboration',
|
||||
meta_title: 'Team Collaboration',
|
||||
meta_description:
|
||||
'Coordinate with your team seamlessly using shared availability.',
|
||||
slug: 'team-collaboration',
|
||||
feature_image:
|
||||
'https://images.unsplash.com/photo-1522202176988-66273c2fd55f?w=400',
|
||||
},
|
||||
];
|
||||
|
||||
const visibleCards = ref([...sampleCards]);
|
||||
const currentIndex = ref(0);
|
||||
const dismissingCards = ref([]);
|
||||
|
||||
const handleReadMore = slug => {
|
||||
console.log(`Read more: ${slug}`);
|
||||
};
|
||||
|
||||
const handleDismiss = slug => {
|
||||
dismissingCards.value.push(slug);
|
||||
setTimeout(() => {
|
||||
const idx = visibleCards.value.findIndex(c => c.slug === slug);
|
||||
if (idx !== -1) visibleCards.value.splice(idx, 1);
|
||||
dismissingCards.value = dismissingCards.value.filter(s => s !== slug);
|
||||
if (currentIndex.value >= visibleCards.value.length) currentIndex.value = 0;
|
||||
}, 200);
|
||||
};
|
||||
|
||||
const handleImgClick = data => {
|
||||
currentIndex.value = data.index;
|
||||
console.log(`Card clicked: ${visibleCards.value[data.index].title}`);
|
||||
};
|
||||
|
||||
const resetDemo = () => {
|
||||
visibleCards.value = [...sampleCards];
|
||||
currentIndex.value = 0;
|
||||
dismissingCards.value = [];
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story
|
||||
title="Components/ChangelogCard/GroupedStackedChangelogCard"
|
||||
:layout="{ type: 'grid', width: '320px' }"
|
||||
>
|
||||
<Variant title="Interactive Demo">
|
||||
<div class="p-4 bg-n-solid-2 rounded-md mx-auto w-64 h-[400px]">
|
||||
<GroupedStackedChangelogCard
|
||||
:posts="visibleCards"
|
||||
:current-index="currentIndex"
|
||||
:is-active="currentIndex === 0"
|
||||
:dismissing-slugs="dismissingCards"
|
||||
class="min-h-[270px]"
|
||||
@read-more="handleReadMore"
|
||||
@dismiss="handleDismiss"
|
||||
@img-click="handleImgClick"
|
||||
/>
|
||||
|
||||
<button
|
||||
class="mt-3 px-3 py-1 text-xs font-medium bg-n-brand text-white rounded hover:bg-n-brand/80 transition"
|
||||
@click="resetDemo"
|
||||
>
|
||||
<!-- eslint-disable-next-line @intlify/vue-i18n/no-raw-text -->
|
||||
{{ 'Reset Cards' }}
|
||||
</button>
|
||||
</div>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import StackedChangelogCard from './StackedChangelogCard.vue';
|
||||
|
||||
const props = defineProps({
|
||||
posts: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
currentIndex: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
dismissingSlugs: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['readMore', 'dismiss', 'imgClick']);
|
||||
|
||||
const stackedPosts = computed(() => props.posts?.slice(0, 5));
|
||||
|
||||
const isPostDismissing = post => props.dismissingSlugs.includes(post.slug);
|
||||
|
||||
const handleReadMore = post => emit('readMore', post.slug);
|
||||
const handleDismiss = post => emit('dismiss', post.slug);
|
||||
const handlePostClick = (post, index) => {
|
||||
if (index === props.currentIndex && !isPostDismissing(post)) {
|
||||
emit('imgClick', { slug: post.slug, index });
|
||||
}
|
||||
};
|
||||
|
||||
const getCardClasses = index => {
|
||||
const pos =
|
||||
(index - props.currentIndex + stackedPosts.value.length) %
|
||||
stackedPosts.value.length;
|
||||
const base =
|
||||
'relative transition-all duration-500 ease-out col-start-1 row-start-1';
|
||||
|
||||
const layers = [
|
||||
'z-50 scale-100 translate-y-0 opacity-100',
|
||||
'z-40 scale-[0.95] -translate-y-3 opacity-90',
|
||||
'z-30 scale-[0.9] -translate-y-6 opacity-70',
|
||||
'z-20 scale-[0.85] -translate-y-9 opacity-50',
|
||||
'z-10 scale-[0.8] -translate-y-12 opacity-30',
|
||||
];
|
||||
|
||||
return pos < layers.length
|
||||
? `${base} ${layers[pos]}`
|
||||
: `${base} opacity-0 scale-75 -translate-y-16`;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="overflow-hidden">
|
||||
<div class="relative grid grid-cols-1 pt-8 pb-1 px-2">
|
||||
<div
|
||||
v-for="(post, index) in stackedPosts"
|
||||
:key="post.slug || index"
|
||||
:class="getCardClasses(index)"
|
||||
>
|
||||
<StackedChangelogCard
|
||||
:card="post"
|
||||
:is-active="index === currentIndex"
|
||||
:is-dismissing="isPostDismissing(post)"
|
||||
@read-more="handleReadMore(post)"
|
||||
@dismiss="handleDismiss(post)"
|
||||
@img-click="handlePostClick(post, index)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
<script setup>
|
||||
import StackedChangelogCard from './StackedChangelogCard.vue';
|
||||
|
||||
const imageCards = {
|
||||
id: 'chatwoot-captain',
|
||||
title: 'Chatwoot Captain',
|
||||
meta_title: 'Chatwoot Captain',
|
||||
meta_description:
|
||||
'Watch how our latest feature can transform your workflow with powerful automation tools.',
|
||||
slug: 'chatwoot-captain',
|
||||
feature_image:
|
||||
'https://www.chatwoot.com/images/captain/captain_thumbnail.jpg',
|
||||
};
|
||||
|
||||
const handleReadMore = () => {
|
||||
console.log(`Read more: ${imageCards.title}`);
|
||||
};
|
||||
|
||||
const handleDismiss = () => {
|
||||
console.log(`Dismissed: ${imageCards.title}`);
|
||||
};
|
||||
|
||||
const handleImgClick = () => {
|
||||
console.log(`Card clicked: ${imageCards.title}`);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story
|
||||
title="Components/ChangelogCard/StackedChangelogCard"
|
||||
:layout="{ type: 'grid', width: '260px' }"
|
||||
>
|
||||
<Variant title="Single Card - With Image">
|
||||
<div class="p-3 bg-n-solid-2 w-56">
|
||||
<StackedChangelogCard
|
||||
:card="imageCards"
|
||||
is-active
|
||||
:is-dismissing="false"
|
||||
@read-more="handleReadMore(imageCards)"
|
||||
@dismiss="handleDismiss(imageCards)"
|
||||
@img-click="handleImgClick(imageCards)"
|
||||
/>
|
||||
</div>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
@@ -0,0 +1,119 @@
|
||||
<script setup>
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
defineProps({
|
||||
card: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isDismissing: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['readMore', 'dismiss', 'imgClick']);
|
||||
|
||||
const handleReadMore = () => {
|
||||
emit('readMore');
|
||||
};
|
||||
|
||||
const handleDismiss = () => {
|
||||
emit('dismiss');
|
||||
};
|
||||
|
||||
const handleImgClick = () => {
|
||||
emit('imgClick');
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
data-testid="changelog-card"
|
||||
class="flex flex-col justify-between p-3 w-full rounded-lg border shadow-sm transition-all duration-200 border-n-weak bg-n-background text-n-slate-12"
|
||||
:class="{
|
||||
'animate-fade-out pointer-events-none': isDismissing,
|
||||
'hover:shadow': isActive,
|
||||
}"
|
||||
>
|
||||
<div>
|
||||
<h5
|
||||
:title="card.meta_title"
|
||||
class="mb-1 text-sm font-semibold line-clamp-1 text-n-slate-12"
|
||||
>
|
||||
{{ card.meta_title }}
|
||||
</h5>
|
||||
<p
|
||||
:title="card.meta_description"
|
||||
class="mb-0 text-xs leading-relaxed text-n-slate-11 line-clamp-2"
|
||||
>
|
||||
{{ card.meta_description }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="card.feature_image"
|
||||
class="block overflow-hidden my-3 rounded-md border border-n-weak/40"
|
||||
>
|
||||
<img
|
||||
:src="card.feature_image"
|
||||
:alt="`${card.title} preview image`"
|
||||
class="object-cover w-full h-24 rounded-md cursor-pointer"
|
||||
loading="lazy"
|
||||
@click.stop="handleImgClick"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="block overflow-hidden my-3 rounded-md border border-n-weak/40"
|
||||
>
|
||||
<img
|
||||
:src="card.feature_image"
|
||||
:alt="`${card.title} preview image`"
|
||||
class="object-cover w-full h-24 rounded-md cursor-pointer"
|
||||
loading="lazy"
|
||||
@click.stop="handleImgClick"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center mt-1">
|
||||
<Button
|
||||
label="Read more"
|
||||
color="slate"
|
||||
link
|
||||
sm
|
||||
class="text-xs font-normal hover:!no-underline"
|
||||
@click.stop="handleReadMore"
|
||||
/>
|
||||
<Button
|
||||
label="Dismiss"
|
||||
color="slate"
|
||||
link
|
||||
sm
|
||||
class="text-xs font-normal hover:!no-underline"
|
||||
@click.stop="handleDismiss"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@keyframes fade-out {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fade-out {
|
||||
animation: fade-out 0.2s ease-out forwards;
|
||||
}
|
||||
</style>
|
||||
@@ -96,6 +96,7 @@ watch(
|
||||
:label="selectedLabel"
|
||||
trailing-icon
|
||||
:disabled="disabled"
|
||||
no-animation
|
||||
class="justify-between w-full !px-3 !py-2.5 text-n-slate-12 font-normal group-hover/combobox:border-n-slate-6 focus:outline-n-brand"
|
||||
:class="{
|
||||
focused: open,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { computed } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import ButtonGroup from 'dashboard/components-next/buttonGroup/ButtonGroup.vue';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||
@@ -53,14 +54,17 @@ const toggleSidebar = () => {
|
||||
v-if="showCopilotLauncher"
|
||||
class="fixed bottom-4 ltr:right-4 rtl:left-4 z-50"
|
||||
>
|
||||
<div class="rounded-full bg-n-alpha-2 p-1">
|
||||
<ButtonGroup
|
||||
class="rounded-full bg-n-alpha-2 backdrop-blur-lg p-1 shadow hover:shadow-md"
|
||||
>
|
||||
<Button
|
||||
icon="i-woot-captain"
|
||||
class="!rounded-full !bg-n-solid-3 dark:!bg-n-alpha-2 !text-n-slate-12 text-xl"
|
||||
no-animation
|
||||
class="!rounded-full !bg-n-solid-3 dark:!bg-n-alpha-2 !text-n-slate-12 text-xl transition-all duration-200 ease-out hover:brightness-110"
|
||||
lg
|
||||
@click="toggleSidebar"
|
||||
/>
|
||||
</div>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
<template v-else />
|
||||
</template>
|
||||
|
||||
@@ -10,6 +10,7 @@ defineProps({
|
||||
fallbackThumbnail: { type: String, default: '' },
|
||||
fallbackThumbnailDark: { type: String, default: '' },
|
||||
learnMoreUrl: { type: String, default: '' },
|
||||
hideActions: { type: Boolean, default: false },
|
||||
});
|
||||
|
||||
const imageError = ref(false);
|
||||
@@ -65,7 +66,7 @@ const openLink = link => {
|
||||
<div class="flex flex-col flex-1 gap-3 ltr:pr-8 rtl:pl-8">
|
||||
<p v-if="note" class="text-n-slate-12 text-sm mb-0">{{ note }}</p>
|
||||
|
||||
<div class="flex gap-3">
|
||||
<div v-if="!hideActions" class="flex gap-3">
|
||||
<slot name="actions">
|
||||
<Button
|
||||
v-if="videoUrl"
|
||||
|
||||
+2
-1
@@ -13,6 +13,7 @@ defineProps({
|
||||
fallbackThumbnail: { type: String, default: '' },
|
||||
fallbackThumbnailDark: { type: String, default: '' },
|
||||
learnMoreUrl: { type: String, default: '' },
|
||||
hideActions: { type: Boolean, default: false },
|
||||
});
|
||||
|
||||
const imageError = ref(false);
|
||||
@@ -92,7 +93,7 @@ const openLink = link => {
|
||||
{{ note }}
|
||||
</p>
|
||||
|
||||
<div class="flex gap-3 justify-between w-full">
|
||||
<div v-if="!hideActions" class="flex gap-3 justify-between w-full">
|
||||
<slot name="actions">
|
||||
<Button
|
||||
v-if="videoUrl"
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<script setup>
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
defineProps({
|
||||
message: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="text-n-slate-12 max-w-80 flex flex-col gap-2.5">
|
||||
<div class="p-3 bg-n-alpha-2 rounded-xl">
|
||||
<span
|
||||
v-dompurify-html="message.content"
|
||||
class="prose prose-bubble font-medium text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<Button label="Call us" slate class="!text-n-blue-text w-full" />
|
||||
<Button
|
||||
label="Visit our website"
|
||||
slate
|
||||
class="!text-n-blue-text w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,32 @@
|
||||
<script setup>
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
defineProps({
|
||||
message: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="bg-n-alpha-2 divide-y divide-n-strong text-n-slate-12 rounded-xl max-w-80"
|
||||
>
|
||||
<div class="px-3 py-2.5">
|
||||
<img :src="message.image_url" class="max-h-44 rounded-lg w-full" />
|
||||
<div class="pt-2.5 flex flex-col gap-2">
|
||||
<h6 class="font-semibold">{{ message.title }}</h6>
|
||||
<span
|
||||
v-dompurify-html="message.content"
|
||||
class="prose prose-bubble text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-3 flex items-center justify-center">
|
||||
<Button label="Call us to order" link class="hover:!no-underline" />
|
||||
</div>
|
||||
<div class="p-3 flex items-center justify-center">
|
||||
<Button label="Visit our store" link class="hover:!no-underline" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,25 @@
|
||||
<script setup>
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
defineProps({
|
||||
message: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="bg-n-alpha-2 divide-y divide-n-strong text-n-slate-12 rounded-xl max-w-80"
|
||||
>
|
||||
<div class="p-3">
|
||||
<span
|
||||
v-dompurify-html="message.content"
|
||||
class="prose prose-bubble font-medium text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div class="p-3 flex items-center justify-center">
|
||||
<Button label="See options" link class="hover:!no-underline" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
message: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="bg-n-alpha-2 text-n-slate-12 rounded-xl flex flex-col gap-2.5 p-3 max-w-80"
|
||||
>
|
||||
<img :src="message.image_url" class="max-h-44 rounded-lg w-full" />
|
||||
<span
|
||||
v-dompurify-html="message.content"
|
||||
class="prose prose-bubble font-medium text-sm"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,68 @@
|
||||
<script setup>
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
defineProps({
|
||||
message: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="bg-n-alpha-2 divide-y divide-n-strong text-n-slate-12 rounded-xl max-w-80"
|
||||
>
|
||||
<div class="p-3">
|
||||
<span
|
||||
v-dompurify-html="message.content"
|
||||
class="prose prose-bubble font-medium text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div class="p-3 flex items-center justify-center">
|
||||
<Button label="No, that will be all" link class="hover:!no-underline">
|
||||
<template #icon>
|
||||
<svg
|
||||
width="15"
|
||||
height="15"
|
||||
viewBox="0 0 15 15"
|
||||
fill="none"
|
||||
class="stroke-n-blue-text"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M.667 6.654 5.315.667v3.326c7.968 0 8.878 6.46 8.656 10.007l-.005-.027c-.334-1.79-.474-4.658-8.65-4.658v3.327z"
|
||||
stroke-width="1.333"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
</Button>
|
||||
</div>
|
||||
<div class="p-3 flex items-center justify-center">
|
||||
<Button
|
||||
label="I want to talk to an agents"
|
||||
link
|
||||
class="hover:!no-underline"
|
||||
>
|
||||
<template #icon>
|
||||
<svg
|
||||
width="15"
|
||||
height="15"
|
||||
viewBox="0 0 15 15"
|
||||
fill="none"
|
||||
class="stroke-n-blue-text"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M.667 6.654 5.315.667v3.326c7.968 0 8.878 6.46 8.656 10.007l-.005-.027c-.334-1.79-.474-4.658-8.65-4.658v3.327z"
|
||||
stroke-width="1.333"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,14 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
message: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bg-n-alpha-2 text-n-slate-12 rounded-xl p-3 max-w-80">
|
||||
<span v-dompurify-html="message.content" class="prose prose-bubble" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -47,6 +47,7 @@ const isReel = computed(() => {
|
||||
'max-w-48': isReel,
|
||||
'max-w-full': !isReel,
|
||||
}"
|
||||
@click.stop
|
||||
@error="handleError"
|
||||
/>
|
||||
</div>
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
<script setup>
|
||||
import CallToAction from '../../bubbles/Template/CallToAction.vue';
|
||||
|
||||
const message = {
|
||||
content:
|
||||
'We have super cool products going live! Pre-order and customize products. Contact us for more details',
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story
|
||||
title="Components/Message Bubbles/Template/CallToAction"
|
||||
:layout="{ type: 'grid', width: '600px' }"
|
||||
>
|
||||
<Variant title="Call To Action">
|
||||
<div class="p-4 bg-n-background rounded-lg w-full min-w-4xl">
|
||||
<CallToAction :message="message" />
|
||||
</div>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
@@ -0,0 +1,23 @@
|
||||
<script setup>
|
||||
import Card from '../../bubbles/Template/Card.vue';
|
||||
|
||||
const message = {
|
||||
title: 'Two in one cake (1 pound)',
|
||||
content: 'Customize your order for special occasions',
|
||||
image_url:
|
||||
'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=500&h=300&fit=crop',
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story
|
||||
title="Components/Message Bubbles/Template/Card"
|
||||
:layout="{ type: 'grid', width: '600px' }"
|
||||
>
|
||||
<Variant title="Card">
|
||||
<div class="p-4 bg-n-background rounded-lg w-full min-w-4xl">
|
||||
<Card :message="message" />
|
||||
</div>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
<script setup>
|
||||
import ListPicker from '../../bubbles/Template/ListPicker.vue';
|
||||
|
||||
const message = {
|
||||
content: `Hey there! Thanks for reaching out to us. Could you let us know
|
||||
what you need to help us better assist you? `,
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story
|
||||
title="Components/Message Bubbles/Template/ListPicker"
|
||||
:layout="{ type: 'grid', width: '600px' }"
|
||||
>
|
||||
<Variant title="ListPicker">
|
||||
<div class="p-4 bg-n-background rounded-lg w-full min-w-4xl">
|
||||
<ListPicker :message="message" />
|
||||
</div>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
@@ -0,0 +1,23 @@
|
||||
<script setup>
|
||||
import Media from '../../bubbles/Template/Media.vue';
|
||||
|
||||
const message = {
|
||||
content:
|
||||
'Welcome to our Diwali sale! Get flat 50% off on select items. Hurry now!',
|
||||
image_url:
|
||||
'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=500&h=300&fit=crop',
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story
|
||||
title="Components/Message Bubbles/Template/Media"
|
||||
:layout="{ type: 'grid', width: '600px' }"
|
||||
>
|
||||
<Variant title="Image Media">
|
||||
<div class="p-4 bg-n-background rounded-lg w-full min-w-4xl">
|
||||
<Media :message="message" />
|
||||
</div>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
<script setup>
|
||||
import QuickReply from '../../bubbles/Template/QuickReply.vue';
|
||||
|
||||
const message = {
|
||||
content: `Hey there! Thanks for reaching out to us. Could you let us know
|
||||
what you need to help us better assist you?`,
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story
|
||||
title="Components/Message Bubbles/Template/QuickReply"
|
||||
:layout="{ type: 'grid', width: '600px' }"
|
||||
>
|
||||
<Variant title="Quick Replies">
|
||||
<div class="p-4 bg-n-background rounded-lg w-full min-w-4xl">
|
||||
<QuickReply :message="message" />
|
||||
</div>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script setup>
|
||||
import Text from '../../bubbles/Template/Text.vue';
|
||||
|
||||
const message = {
|
||||
content: 'Hello John, how may we assist you?',
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story
|
||||
title="Components/Message Bubbles/Template/Text"
|
||||
:layout="{ type: 'grid', width: '600px' }"
|
||||
>
|
||||
<Variant title="Default Text">
|
||||
<div class="p-4 bg-n-background rounded-lg w-full min-w-4xl">
|
||||
<Text :message="message" />
|
||||
</div>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
@@ -0,0 +1,61 @@
|
||||
<script setup>
|
||||
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||
|
||||
defineProps({
|
||||
color: {
|
||||
type: String,
|
||||
default: 'blue',
|
||||
validator: value => ['ruby', 'blue', 'teal', 'amber'].includes(value),
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: 'i-lucide-info',
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
message: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const colorClasses = {
|
||||
ruby: 'outline-n-ruby-4 bg-n-ruby-3 text-n-ruby-11',
|
||||
blue: 'outline-n-blue-4 bg-n-blue-3 text-n-blue-11',
|
||||
teal: 'outline-n-teal-4 bg-n-teal-3 text-n-teal-11',
|
||||
amber: 'outline-n-amber-4 bg-n-amber-3 text-n-amber-11',
|
||||
};
|
||||
|
||||
const titleColorClasses = {
|
||||
ruby: 'text-n-ruby-11',
|
||||
blue: 'text-n-blue-11',
|
||||
teal: 'text-n-teal-11',
|
||||
amber: 'text-n-amber-11',
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="p-4 rounded-xl outline outline-1 -outline-offset-1"
|
||||
:class="colorClasses[color]"
|
||||
>
|
||||
<div class="flex gap-3 items-start">
|
||||
<Icon :icon="icon" class="flex-shrink-0 mt-0.5" />
|
||||
<div class="flex-1 space-y-1">
|
||||
<h4
|
||||
v-if="title"
|
||||
class="text-sm font-semibold"
|
||||
:class="titleColorClasses[color]"
|
||||
>
|
||||
{{ title }}
|
||||
</h4>
|
||||
<p v-if="message" class="mb-0 text-sm">
|
||||
{{ message }}
|
||||
</p>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -2,6 +2,7 @@
|
||||
import { computed } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import ButtonGroup from 'dashboard/components-next/buttonGroup/ButtonGroup.vue';
|
||||
|
||||
defineProps({
|
||||
isMobileSidebarOpen: {
|
||||
@@ -45,14 +46,17 @@ const toggleSidebar = () => {
|
||||
},
|
||||
]"
|
||||
>
|
||||
<div class="rounded-full bg-n-alpha-2 p-1">
|
||||
<ButtonGroup
|
||||
class="rounded-full bg-n-alpha-2 backdrop-blur-lg p-1 shadow hover:shadow-md"
|
||||
>
|
||||
<Button
|
||||
icon="i-lucide-menu"
|
||||
class="!rounded-full !bg-n-solid-3 dark:!bg-n-alpha-2 !text-n-slate-12 text-xl"
|
||||
no-animation
|
||||
class="!rounded-full !bg-n-solid-3 dark:!bg-n-alpha-2 !text-n-slate-12 text-xl transition-all duration-200 ease-out hover:brightness-110"
|
||||
lg
|
||||
@click="toggleSidebar"
|
||||
/>
|
||||
</div>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
<template v-else />
|
||||
</template>
|
||||
|
||||
@@ -13,6 +13,7 @@ import { vOnClickOutside } from '@vueuse/components';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import SidebarGroup from './SidebarGroup.vue';
|
||||
import SidebarProfileMenu from './SidebarProfileMenu.vue';
|
||||
import SidebarChangelogCard from './SidebarChangelogCard.vue';
|
||||
import ChannelLeaf from './ChannelLeaf.vue';
|
||||
import SidebarAccountSwitcher from './SidebarAccountSwitcher.vue';
|
||||
import Logo from 'next/icon/Logo.vue';
|
||||
@@ -32,11 +33,15 @@ const emit = defineEmits([
|
||||
'closeMobileSidebar',
|
||||
]);
|
||||
|
||||
const { accountScopedRoute } = useAccount();
|
||||
const { accountScopedRoute, isOnChatwootCloud } = useAccount();
|
||||
const store = useStore();
|
||||
const searchShortcut = useKbd([`$mod`, 'k']);
|
||||
const { t } = useI18n();
|
||||
|
||||
const isACustomBrandedInstance = useMapGetter(
|
||||
'globalConfig/isACustomBrandedInstance'
|
||||
);
|
||||
|
||||
const toggleShortcutModalFn = show => {
|
||||
if (show) {
|
||||
emit('openKeyShortcutModal');
|
||||
@@ -232,6 +237,11 @@ const menuItems = computed(() => {
|
||||
label: t('SIDEBAR.CAPTAIN_RESPONSES'),
|
||||
to: accountScopedRoute('captain_responses_index'),
|
||||
},
|
||||
{
|
||||
name: 'Tools',
|
||||
label: t('SIDEBAR.CAPTAIN_TOOLS'),
|
||||
to: accountScopedRoute('captain_tools_index'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -527,20 +537,20 @@ const menuItems = computed(() => {
|
||||
]"
|
||||
>
|
||||
<section class="grid gap-2 mt-2 mb-4">
|
||||
<div class="flex items-center min-w-0 gap-2 px-2">
|
||||
<div class="grid flex-shrink-0 size-6 place-content-center">
|
||||
<div class="flex gap-2 items-center px-2 min-w-0">
|
||||
<div class="grid flex-shrink-0 place-content-center size-6">
|
||||
<Logo class="size-4" />
|
||||
</div>
|
||||
<div class="flex-shrink-0 w-px h-3 bg-n-strong" />
|
||||
<SidebarAccountSwitcher
|
||||
class="flex-grow min-w-0 -mx-1"
|
||||
class="flex-grow -mx-1 min-w-0"
|
||||
@show-create-account-modal="emit('showCreateAccountModal')"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex gap-2 px-2">
|
||||
<RouterLink
|
||||
:to="{ name: 'search' }"
|
||||
class="flex items-center w-full gap-2 px-2 py-1 rounded-lg h-7 outline outline-1 outline-n-weak bg-n-solid-3 dark:bg-n-black/30"
|
||||
class="flex gap-2 items-center px-2 py-1 w-full h-7 rounded-lg outline outline-1 outline-n-weak bg-n-solid-3 dark:bg-n-black/30"
|
||||
>
|
||||
<span class="flex-shrink-0 i-lucide-search size-4 text-n-slate-11" />
|
||||
<span class="flex-grow text-left">
|
||||
@@ -565,7 +575,7 @@ const menuItems = computed(() => {
|
||||
</ComposeConversation>
|
||||
</div>
|
||||
</section>
|
||||
<nav class="grid flex-grow gap-2 px-2 pb-5 overflow-y-scroll no-scrollbar">
|
||||
<nav class="grid overflow-y-scroll flex-grow gap-2 px-2 pb-5 no-scrollbar">
|
||||
<ul class="flex flex-col gap-1.5 m-0 list-none">
|
||||
<SidebarGroup
|
||||
v-for="item in menuItems"
|
||||
@@ -575,11 +585,21 @@ const menuItems = computed(() => {
|
||||
</ul>
|
||||
</nav>
|
||||
<section
|
||||
class="p-1 border-t border-n-weak shadow-[0px_-2px_4px_0px_rgba(27,28,29,0.02)] flex-shrink-0 flex justify-between gap-2 items-center"
|
||||
class="flex flex-col flex-shrink-0 relative gap-1 justify-between items-center"
|
||||
>
|
||||
<SidebarProfileMenu
|
||||
@open-key-shortcut-modal="emit('openKeyShortcutModal')"
|
||||
<div
|
||||
class="pointer-events-none absolute inset-x-0 -top-[31px] h-8 bg-gradient-to-t from-n-solid-2 to-transparent"
|
||||
/>
|
||||
<SidebarChangelogCard
|
||||
v-if="isOnChatwootCloud && !isACustomBrandedInstance"
|
||||
/>
|
||||
<div
|
||||
class="p-1 flex-shrink-0 flex w-full justify-between z-10 gap-2 items-center border-t border-n-weak shadow-[0px_-2px_4px_0px_rgba(27,28,29,0.02)]"
|
||||
>
|
||||
<SidebarProfileMenu
|
||||
@open-key-shortcut-modal="emit('openKeyShortcutModal')"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import GroupedStackedChangelogCard from 'dashboard/components-next/changelog-card/GroupedStackedChangelogCard.vue';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import changelogAPI from 'dashboard/api/changelog';
|
||||
|
||||
const MAX_DISMISSED_SLUGS = 5;
|
||||
|
||||
const { uiSettings, updateUISettings } = useUISettings();
|
||||
const posts = ref([]);
|
||||
const currentIndex = ref(0);
|
||||
const dismissingCards = ref([]);
|
||||
const isLoading = ref(false);
|
||||
|
||||
// Get current dismissed slugs from ui_settings
|
||||
const dismissedSlugs = computed(() => {
|
||||
return uiSettings.value.changelog_dismissed_slugs || [];
|
||||
});
|
||||
|
||||
// Get un dismissed posts - these are the changelog posts that should be shown
|
||||
const unDismissedPosts = computed(() => {
|
||||
return posts.value.filter(post => !dismissedSlugs.value.includes(post.slug));
|
||||
});
|
||||
|
||||
// Fetch changelog posts from API
|
||||
const fetchChangelog = async () => {
|
||||
isLoading.value = true;
|
||||
|
||||
try {
|
||||
const response = await changelogAPI.fetchFromHub();
|
||||
posts.value = response.data.posts || [];
|
||||
|
||||
// Clean up dismissed slugs - remove any that are no longer in the current feed
|
||||
const currentSlugs = posts.value.map(post => post.slug);
|
||||
const cleanedDismissedSlugs = dismissedSlugs.value.filter(slug =>
|
||||
currentSlugs.includes(slug)
|
||||
);
|
||||
|
||||
// Update ui_settings if cleanup occurred
|
||||
if (cleanedDismissedSlugs.length !== dismissedSlugs.value.length) {
|
||||
updateUISettings({
|
||||
changelog_dismissed_slugs: cleanedDismissedSlugs,
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch (err) {
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Dismiss a changelog post
|
||||
const dismissPost = slug => {
|
||||
const currentDismissed = [...dismissedSlugs.value];
|
||||
|
||||
// Add new slug if not already present
|
||||
if (!currentDismissed.includes(slug)) {
|
||||
currentDismissed.push(slug);
|
||||
|
||||
// Keep only the most recent MAX_DISMISSED_SLUGS entries
|
||||
if (currentDismissed.length > MAX_DISMISSED_SLUGS) {
|
||||
currentDismissed.shift(); // Remove oldest entry
|
||||
}
|
||||
|
||||
updateUISettings({
|
||||
changelog_dismissed_slugs: currentDismissed,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleDismiss = slug => {
|
||||
dismissingCards.value.push(slug);
|
||||
setTimeout(() => {
|
||||
dismissPost(slug);
|
||||
dismissingCards.value = dismissingCards.value.filter(s => s !== slug);
|
||||
if (currentIndex.value >= unDismissedPosts.value.length)
|
||||
currentIndex.value = 0;
|
||||
}, 200);
|
||||
};
|
||||
|
||||
const handleReadMore = () => {
|
||||
const currentPost = unDismissedPosts.value[currentIndex.value];
|
||||
if (currentPost?.slug) {
|
||||
window.open(`https://www.chatwoot.com/blog/${currentPost.slug}`, '_blank');
|
||||
}
|
||||
};
|
||||
|
||||
const handleImgClick = ({ index }) => {
|
||||
currentIndex.value = index;
|
||||
handleReadMore();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchChangelog();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<GroupedStackedChangelogCard
|
||||
v-if="unDismissedPosts.length > 0"
|
||||
:posts="unDismissedPosts"
|
||||
:current-index="currentIndex"
|
||||
:dismissing-slugs="dismissingCards"
|
||||
class="min-h-[240px] z-10"
|
||||
@read-more="handleReadMore"
|
||||
@dismiss="handleDismiss"
|
||||
@img-click="handleImgClick"
|
||||
/>
|
||||
<template v-else />
|
||||
</template>
|
||||
@@ -1,5 +1,7 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { computed, ref, onMounted, nextTick } from 'vue';
|
||||
import { useResizeObserver } from '@vueuse/core';
|
||||
|
||||
const props = defineProps({
|
||||
initialActiveTab: {
|
||||
type: Number,
|
||||
@@ -22,6 +24,32 @@ const emit = defineEmits(['tabChanged']);
|
||||
|
||||
const activeTab = computed(() => props.initialActiveTab);
|
||||
|
||||
const tabRefs = ref([]);
|
||||
const indicatorStyle = ref({});
|
||||
const enableTransition = ref(false);
|
||||
|
||||
const activeElement = computed(() => tabRefs.value[activeTab.value]);
|
||||
|
||||
const updateIndicator = () => {
|
||||
if (!activeElement.value) return;
|
||||
|
||||
indicatorStyle.value = {
|
||||
left: `${activeElement.value.offsetLeft}px`,
|
||||
width: `${activeElement.value.offsetWidth}px`,
|
||||
};
|
||||
};
|
||||
|
||||
useResizeObserver(activeElement, () => {
|
||||
if (enableTransition.value || !activeElement.value) updateIndicator();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
updateIndicator();
|
||||
nextTick(() => {
|
||||
enableTransition.value = true;
|
||||
});
|
||||
});
|
||||
|
||||
const selectTab = index => {
|
||||
emit('tabChanged', props.tabs[index]);
|
||||
};
|
||||
@@ -37,20 +65,30 @@ const showDivider = index => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center h-8 rounded-lg bg-n-alpha-1 w-fit">
|
||||
<div
|
||||
class="relative flex items-center h-8 rounded-lg bg-n-alpha-1 w-fit transition-all duration-200 ease-out has-[button:active]:scale-[1.01]"
|
||||
>
|
||||
<div
|
||||
class="absolute rounded-lg bg-n-solid-active shadow-sm pointer-events-none h-8 outline-1 outline outline-n-container inset-y-0"
|
||||
:class="{ 'transition-all duration-300 ease-out': enableTransition }"
|
||||
:style="indicatorStyle"
|
||||
/>
|
||||
|
||||
<template v-for="(tab, index) in tabs" :key="index">
|
||||
<button
|
||||
class="relative px-4 truncate py-1.5 text-sm border-0 outline-1 outline rounded-lg transition-colors duration-300 ease-in-out hover:text-n-brand"
|
||||
:ref="el => (tabRefs[index] = el)"
|
||||
class="relative z-10 px-4 truncate py-1.5 text-sm border-0 outline-1 outline-transparent rounded-lg transition-all duration-200 ease-out hover:text-n-brand active:scale-[1.02]"
|
||||
:class="[
|
||||
activeTab === index
|
||||
? 'text-n-blue-text bg-n-solid-active outline-n-container dark:outline-transparent'
|
||||
: 'text-n-slate-10 outline-transparent h-8',
|
||||
? 'text-n-blue-text scale-100'
|
||||
: 'text-n-slate-10 scale-[0.98]',
|
||||
]"
|
||||
@click="selectTab(index)"
|
||||
>
|
||||
{{ tab.label }} {{ tab.count ? `(${tab.count})` : '' }}
|
||||
</button>
|
||||
<div
|
||||
v-if="index < tabs.length - 1"
|
||||
class="w-px h-3.5 rounded my-auto transition-colors duration-300 ease-in-out"
|
||||
:class="
|
||||
showDivider(index)
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
CMD_RESOLVE_CONVERSATION,
|
||||
} from 'dashboard/helper/commandbar/events';
|
||||
|
||||
import ButtonGroup from 'dashboard/components-next/buttonGroup/ButtonGroup.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
const store = useStore();
|
||||
@@ -133,7 +134,7 @@ useEmitter(CMD_RESOLVE_CONVERSATION, onCmdResolveConversation);
|
||||
|
||||
<template>
|
||||
<div class="relative flex items-center justify-end resolve-actions">
|
||||
<div
|
||||
<ButtonGroup
|
||||
class="rounded-lg shadow outline-1 outline flex-shrink-0"
|
||||
:class="!showOpenButton ? 'outline-n-container' : 'outline-transparent'"
|
||||
>
|
||||
@@ -142,6 +143,7 @@ useEmitter(CMD_RESOLVE_CONVERSATION, onCmdResolveConversation);
|
||||
:label="t('CONVERSATION.HEADER.RESOLVE_ACTION')"
|
||||
size="sm"
|
||||
color="slate"
|
||||
no-animation
|
||||
class="ltr:rounded-r-none rtl:rounded-l-none !outline-0"
|
||||
:is-loading="isLoading"
|
||||
@click="onCmdResolveConversation"
|
||||
@@ -151,6 +153,7 @@ useEmitter(CMD_RESOLVE_CONVERSATION, onCmdResolveConversation);
|
||||
:label="t('CONVERSATION.HEADER.REOPEN_ACTION')"
|
||||
size="sm"
|
||||
color="slate"
|
||||
no-animation
|
||||
class="ltr:rounded-r-none rtl:rounded-l-none !outline-0"
|
||||
:is-loading="isLoading"
|
||||
@click="onCmdOpenConversation"
|
||||
@@ -160,6 +163,7 @@ useEmitter(CMD_RESOLVE_CONVERSATION, onCmdResolveConversation);
|
||||
:label="t('CONVERSATION.HEADER.OPEN_ACTION')"
|
||||
size="sm"
|
||||
color="slate"
|
||||
no-animation
|
||||
:is-loading="isLoading"
|
||||
@click="onCmdOpenConversation"
|
||||
/>
|
||||
@@ -169,12 +173,13 @@ useEmitter(CMD_RESOLVE_CONVERSATION, onCmdResolveConversation);
|
||||
icon="i-lucide-chevron-down"
|
||||
:disabled="isLoading"
|
||||
size="sm"
|
||||
no-animation
|
||||
class="ltr:rounded-l-none rtl:rounded-r-none !outline-0"
|
||||
color="slate"
|
||||
trailing-icon
|
||||
@click="openDropdown"
|
||||
/>
|
||||
</div>
|
||||
</ButtonGroup>
|
||||
<div
|
||||
v-if="showActionsDropdown"
|
||||
v-on-clickaway="closeDropdown"
|
||||
|
||||
@@ -302,7 +302,16 @@ function isBodyEmpty(content) {
|
||||
}
|
||||
|
||||
function handleEmptyBodyWithSignature() {
|
||||
const { schema, tr } = state;
|
||||
const { schema, tr, doc } = state;
|
||||
|
||||
const isEmptyParagraph = node =>
|
||||
node && node.type === schema.nodes.paragraph && node.content.size === 0;
|
||||
|
||||
// Check if empty paragraph already exists to prevent duplicates when toggling signatures
|
||||
if (isEmptyParagraph(doc.firstChild)) {
|
||||
focusEditorInputField('start');
|
||||
return;
|
||||
}
|
||||
|
||||
// create a paragraph node and
|
||||
// start a transaction to append it at the end
|
||||
|
||||
@@ -59,7 +59,7 @@ const translateValue = computed(() => {
|
||||
|
||||
<template>
|
||||
<button
|
||||
class="flex items-center w-auto h-8 p-1 transition-all border rounded-full bg-n-alpha-2 group relative duration-300 ease-in-out z-0"
|
||||
class="flex items-center w-auto h-8 p-1 transition-all border rounded-full bg-n-alpha-2 group relative duration-300 ease-in-out z-0 active:scale-[0.995] active:duration-75"
|
||||
:disabled="disabled"
|
||||
:class="{
|
||||
'cursor-not-allowed': disabled,
|
||||
|
||||
+92
-159
@@ -1,100 +1,129 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
import { vOnClickOutside } from '@vueuse/components';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NextButton,
|
||||
},
|
||||
emits: ['update', 'close', 'assign'],
|
||||
data() {
|
||||
return {
|
||||
query: '',
|
||||
selectedLabels: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({ labels: 'labels/getLabels' }),
|
||||
filteredLabels() {
|
||||
return this.labels.filter(label =>
|
||||
label.title.toLowerCase().includes(this.query.toLowerCase())
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
isLabelSelected(label) {
|
||||
return this.selectedLabels.includes(label);
|
||||
},
|
||||
assignLabels(key) {
|
||||
this.$emit('update', key);
|
||||
},
|
||||
onClose() {
|
||||
this.$emit('close');
|
||||
},
|
||||
},
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
import Input from 'dashboard/components-next/input/Input.vue';
|
||||
|
||||
const emit = defineEmits(['close', 'assign']);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const labels = useMapGetter('labels/getLabels');
|
||||
|
||||
const query = ref('');
|
||||
const selectedLabels = ref([]);
|
||||
|
||||
const filteredLabels = computed(() => {
|
||||
if (!query.value) return labels.value;
|
||||
return labels.value.filter(label =>
|
||||
label.title.toLowerCase().includes(query.value.toLowerCase())
|
||||
);
|
||||
});
|
||||
|
||||
const hasLabels = computed(() => labels.value.length > 0);
|
||||
const hasFilteredLabels = computed(() => filteredLabels.value.length > 0);
|
||||
|
||||
const isLabelSelected = label => {
|
||||
return selectedLabels.value.includes(label);
|
||||
};
|
||||
|
||||
const onClose = () => {
|
||||
emit('close');
|
||||
};
|
||||
|
||||
const handleAssign = () => {
|
||||
if (selectedLabels.value.length > 0) {
|
||||
emit('assign', selectedLabels.value);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-on-clickaway="onClose" class="labels-container">
|
||||
<div
|
||||
v-on-click-outside="onClose"
|
||||
class="absolute ltr:right-2 rtl:left-2 top-12 origin-top-right z-20 w-60 bg-n-alpha-3 backdrop-blur-[100px] border-n-weak rounded-lg border border-solid shadow-md"
|
||||
role="dialog"
|
||||
aria-labelledby="label-dialog-title"
|
||||
>
|
||||
<div class="triangle">
|
||||
<svg height="12" viewBox="0 0 24 12" width="24">
|
||||
<path d="M20 12l-8-8-12 12" fill-rule="evenodd" stroke-width="1px" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex items-center justify-between header">
|
||||
<span>{{ $t('BULK_ACTION.LABELS.ASSIGN_LABELS') }}</span>
|
||||
<div class="flex items-center justify-between p-2.5">
|
||||
<span class="text-sm font-medium">{{
|
||||
t('BULK_ACTION.LABELS.ASSIGN_LABELS')
|
||||
}}</span>
|
||||
<NextButton ghost xs slate icon="i-lucide-x" @click="onClose" />
|
||||
</div>
|
||||
<div class="labels-list">
|
||||
<header class="labels-list__header">
|
||||
<div
|
||||
class="flex items-center justify-between h-8 gap-2 label-list-search"
|
||||
>
|
||||
<fluent-icon icon="search" class="search-icon" size="16" />
|
||||
<input
|
||||
v-model="query"
|
||||
type="search"
|
||||
:placeholder="$t('BULK_ACTION.SEARCH_INPUT_PLACEHOLDER')"
|
||||
class="reset-base !outline-0 !text-sm label--search_input"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col max-h-60 min-h-0">
|
||||
<header class="py-2 px-2.5">
|
||||
<Input
|
||||
v-model="query"
|
||||
type="search"
|
||||
:placeholder="t('BULK_ACTION.SEARCH_INPUT_PLACEHOLDER')"
|
||||
icon-left="i-lucide-search"
|
||||
size="sm"
|
||||
class="w-full"
|
||||
:aria-label="t('BULK_ACTION.SEARCH_INPUT_PLACEHOLDER')"
|
||||
/>
|
||||
</header>
|
||||
<ul class="labels-list__body">
|
||||
<ul
|
||||
v-if="hasLabels"
|
||||
class="flex-1 overflow-y-auto m-0 list-none"
|
||||
role="listbox"
|
||||
:aria-label="t('BULK_ACTION.LABELS.ASSIGN_LABELS')"
|
||||
>
|
||||
<li v-if="!hasFilteredLabels" class="p-2 text-center">
|
||||
<span class="text-sm text-n-slate-11">{{
|
||||
t('BULK_ACTION.LABELS.NO_LABELS_FOUND')
|
||||
}}</span>
|
||||
</li>
|
||||
<li
|
||||
v-for="label in filteredLabels"
|
||||
:key="label.id"
|
||||
class="label__list-item"
|
||||
class="my-1 mx-0 py-0 px-2.5"
|
||||
role="option"
|
||||
:aria-selected="isLabelSelected(label.title)"
|
||||
>
|
||||
<label
|
||||
class="item"
|
||||
:class="{ 'label-selected': isLabelSelected(label.title) }"
|
||||
class="items-center rounded-md cursor-pointer flex py-1 px-2.5 hover:bg-n-slate-3 dark:hover:bg-n-solid-3 has-[:checked]:bg-n-slate-2"
|
||||
>
|
||||
<input
|
||||
v-model="selectedLabels"
|
||||
type="checkbox"
|
||||
:value="label.title"
|
||||
class="label-checkbox"
|
||||
class="my-0 ltr:mr-2.5 rtl:ml-2.5"
|
||||
:aria-label="label.title"
|
||||
/>
|
||||
<span
|
||||
class="overflow-hidden label-title whitespace-nowrap text-ellipsis"
|
||||
class="overflow-hidden flex-grow w-full text-sm whitespace-nowrap text-ellipsis"
|
||||
>
|
||||
{{ label.title }}
|
||||
</span>
|
||||
<span
|
||||
class="label-pill"
|
||||
class="rounded-md h-3 w-3 flex-shrink-0 border border-solid border-n-weak"
|
||||
:style="{ backgroundColor: label.color }"
|
||||
/>
|
||||
</label>
|
||||
</li>
|
||||
</ul>
|
||||
<footer class="labels-list__footer">
|
||||
<div v-else class="p-2 text-center">
|
||||
<span class="text-sm text-n-slate-11">{{
|
||||
t('CONTACTS_BULK_ACTIONS.NO_LABELS_FOUND')
|
||||
}}</span>
|
||||
</div>
|
||||
<footer class="p-2">
|
||||
<NextButton
|
||||
sm
|
||||
type="submit"
|
||||
:label="$t('BULK_ACTION.LABELS.ASSIGN_SELECTED_LABELS')"
|
||||
class="w-full"
|
||||
:label="t('BULK_ACTION.LABELS.ASSIGN_SELECTED_LABELS')"
|
||||
:disabled="!selectedLabels.length"
|
||||
@click="$emit('assign', selectedLabels)"
|
||||
@click="handleAssign"
|
||||
/>
|
||||
</footer>
|
||||
</div>
|
||||
@@ -102,107 +131,11 @@ export default {
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.labels-list {
|
||||
@apply flex flex-col max-h-[15rem] min-h-[auto];
|
||||
.triangle {
|
||||
@apply block z-10 absolute text-left -top-3 ltr:right-[--triangle-position] rtl:left-[--triangle-position];
|
||||
|
||||
.labels-list__header {
|
||||
@apply bg-n-alpha-3 backdrop-blur-[100px] py-0 px-2.5;
|
||||
svg path {
|
||||
@apply fill-n-alpha-3 backdrop-blur-[100px] stroke-n-weak;
|
||||
}
|
||||
|
||||
.labels-list__body {
|
||||
@apply flex-1 overflow-y-auto py-2.5 mx-0;
|
||||
}
|
||||
|
||||
.labels-list__footer {
|
||||
@apply p-2;
|
||||
|
||||
button {
|
||||
@apply w-full;
|
||||
|
||||
.button__content {
|
||||
@apply text-center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.label-list-search {
|
||||
@apply bg-n-alpha-black2 py-0 px-2.5 border border-solid border-n-strong rounded-md;
|
||||
|
||||
.search-icon {
|
||||
@apply text-n-slate-10;
|
||||
}
|
||||
|
||||
.label--search_input {
|
||||
@apply border-0 text-xs m-0 dark:bg-transparent bg-transparent h-[unset] w-full;
|
||||
}
|
||||
}
|
||||
|
||||
.labels-container {
|
||||
@apply absolute ltr:right-2 rtl:left-2 top-12 origin-top-right w-auto z-20 max-w-[15rem] min-w-[15rem] bg-n-alpha-3 backdrop-blur-[100px] border-n-weak rounded-lg border border-solid shadow-md;
|
||||
|
||||
.header {
|
||||
@apply p-2.5;
|
||||
|
||||
span {
|
||||
@apply text-sm font-medium;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
@apply max-h-[15rem] overflow-y-auto;
|
||||
|
||||
.label__list-container {
|
||||
@apply h-full;
|
||||
}
|
||||
}
|
||||
|
||||
.triangle {
|
||||
@apply block z-10 absolute text-left -top-3 ltr:right-[--triangle-position] rtl:left-[--triangle-position];
|
||||
|
||||
svg path {
|
||||
@apply fill-n-alpha-3 backdrop-blur-[100px] stroke-n-weak;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
@apply m-0 list-none;
|
||||
}
|
||||
|
||||
.labels-placeholder {
|
||||
@apply p-2;
|
||||
}
|
||||
|
||||
.label__list-item {
|
||||
@apply my-1 mx-0 py-0 px-2.5;
|
||||
|
||||
.item {
|
||||
@apply items-center rounded-md cursor-pointer flex py-1 px-2.5 hover:bg-n-slate-3 dark:hover:bg-n-solid-3;
|
||||
|
||||
&.label-selected {
|
||||
@apply bg-n-slate-2;
|
||||
}
|
||||
|
||||
span {
|
||||
@apply text-sm;
|
||||
}
|
||||
|
||||
.label-checkbox {
|
||||
@apply my-0 ltr:mr-2.5 rtl:ml-2.5;
|
||||
}
|
||||
|
||||
.label-title {
|
||||
@apply flex-grow w-full;
|
||||
}
|
||||
|
||||
.label-pill {
|
||||
@apply rounded-md h-3 w-3 flex-shrink-0 border border-solid border-n-weak;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-container {
|
||||
@apply bg-n-alpha-3 backdrop-blur-[100px] py-0 px-2.5 sticky top-0 z-20;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -41,6 +41,7 @@ export const FEATURE_FLAGS = {
|
||||
CAPTAIN_V2: 'captain_integration_v2',
|
||||
SAML: 'saml',
|
||||
QUOTED_EMAIL_REPLY: 'quoted_email_reply',
|
||||
STRIPE_V2: 'stripe_v2',
|
||||
};
|
||||
|
||||
export const PREMIUM_FEATURES = [
|
||||
@@ -49,6 +50,5 @@ export const PREMIUM_FEATURES = [
|
||||
FEATURE_FLAGS.CUSTOM_ROLES,
|
||||
FEATURE_FLAGS.AUDIT_LOGS,
|
||||
FEATURE_FLAGS.HELP_CENTER,
|
||||
FEATURE_FLAGS.CAPTAIN_V2,
|
||||
FEATURE_FLAGS.SAML,
|
||||
];
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
},
|
||||
"LABELS": {
|
||||
"ASSIGN_LABELS": "Assign labels",
|
||||
"NO_LABELS_FOUND": "No labels found for",
|
||||
"NO_LABELS_FOUND": "No labels found",
|
||||
"ASSIGN_SELECTED_LABELS": "Assign selected labels",
|
||||
"ASSIGN_SUCCESFUL": "Labels assigned successfully.",
|
||||
"ASSIGN_FAILED": "Failed to assign labels. Please try again."
|
||||
|
||||
@@ -571,6 +571,16 @@
|
||||
"ACTIVE_EMPTY_STATE_TITLE": "በአሁኑ ጊዜ ንቁ እውቂያዎች የሉም 🌙"
|
||||
}
|
||||
},
|
||||
"CONTACTS_BULK_ACTIONS": {
|
||||
"ASSIGN_LABELS": "Assign Labels",
|
||||
"ASSIGN_LABELS_SUCCESS": "Labels assigned successfully.",
|
||||
"ASSIGN_LABELS_FAILED": "Failed to assign labels",
|
||||
"DESCRIPTION": "Select the labels you want to add to the selected contacts.",
|
||||
"NO_LABELS_FOUND": "No labels available yet.",
|
||||
"SELECTED_COUNT": "{count} selected",
|
||||
"CLEAR_SELECTION": "Clear selection",
|
||||
"SELECT_ALL": "Select all ({count})"
|
||||
},
|
||||
"COMPOSE_NEW_CONVERSATION": {
|
||||
"CONTACT_SEARCH": {
|
||||
"ERROR_MESSAGE": "We couldn’t complete the search. Please try again."
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
"LEARN_MORE": "Learn more about inboxes",
|
||||
"RECONNECTION_REQUIRED": "Your inbox is disconnected. You won't receive new messages until you reauthorize it.",
|
||||
"CLICK_TO_RECONNECT": "Click here to reconnect.",
|
||||
"WHATSAPP_REGISTRATION_INCOMPLETE": "Your WhatsApp Business registration isn’t complete. Please check your display name status in Meta Business Manager before reconnecting.",
|
||||
"COMPLETE_REGISTRATION": "Complete Registration",
|
||||
"LIST": {
|
||||
"404": "There are no inboxes attached to this account."
|
||||
},
|
||||
@@ -605,8 +607,64 @@
|
||||
"BUSINESS_HOURS": "Business Hours",
|
||||
"WIDGET_BUILDER": "Widget Builder",
|
||||
"BOT_CONFIGURATION": "Bot Configuration",
|
||||
"ACCOUNT_HEALTH": "Account Health",
|
||||
"CSAT": "CSAT"
|
||||
},
|
||||
"ACCOUNT_HEALTH": {
|
||||
"TITLE": "Manage your WhatsApp account",
|
||||
"DESCRIPTION": "Review your WhatsApp account status, messaging limits, and quality. Update settings or resolve issues if needed",
|
||||
"GO_TO_SETTINGS": "Go to Meta Business Manager",
|
||||
"NO_DATA": "Health data is not available",
|
||||
"FIELDS": {
|
||||
"DISPLAY_PHONE_NUMBER": {
|
||||
"LABEL": "Display phone number",
|
||||
"TOOLTIP": "Phone number displayed to customers"
|
||||
},
|
||||
"VERIFIED_NAME": {
|
||||
"LABEL": "Business name",
|
||||
"TOOLTIP": "Business name verified by WhatsApp"
|
||||
},
|
||||
"DISPLAY_NAME_STATUS": {
|
||||
"LABEL": "Display name status",
|
||||
"TOOLTIP": "Status of your business name verification"
|
||||
},
|
||||
"QUALITY_RATING": {
|
||||
"LABEL": "Quality rating",
|
||||
"TOOLTIP": "WhatsApp quality rating for your account"
|
||||
},
|
||||
"MESSAGING_LIMIT_TIER": {
|
||||
"LABEL": "Messaging limit tier",
|
||||
"TOOLTIP": "Daily messaging limit for your account"
|
||||
},
|
||||
"ACCOUNT_MODE": {
|
||||
"LABEL": "Account mode",
|
||||
"TOOLTIP": "Current operating mode of your WhatsApp account"
|
||||
}
|
||||
},
|
||||
"VALUES": {
|
||||
"TIERS": {
|
||||
"TIER_250": "250 customers per 24h",
|
||||
"TIER_1000": "1K customers per 24h",
|
||||
"TIER_1K": "1K customers per 24h",
|
||||
"TIER_10K": "10K customers per 24h",
|
||||
"TIER_100K": "100K customers per 24h",
|
||||
"TIER_UNLIMITED": "Unlimited customers per 24h",
|
||||
"UNKNOWN": "Rating not available"
|
||||
},
|
||||
"STATUSES": {
|
||||
"APPROVED": "Approved",
|
||||
"PENDING_REVIEW": "Pending Review",
|
||||
"AVAILABLE_WITHOUT_REVIEW": "Available Without Review",
|
||||
"REJECTED": "Rejected",
|
||||
"DECLINED": "Declined",
|
||||
"NON_EXISTS": "Non exists"
|
||||
},
|
||||
"MODES": {
|
||||
"SANDBOX": "Sandbox",
|
||||
"LIVE": "Live"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SETTINGS": "Settings",
|
||||
"FEATURES": {
|
||||
"LABEL": "Features",
|
||||
|
||||
@@ -752,8 +752,118 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"CUSTOM_TOOLS": {
|
||||
"HEADER": "Tools",
|
||||
"ADD_NEW": "Create a new tool",
|
||||
"EMPTY_STATE": {
|
||||
"TITLE": "No custom tools available",
|
||||
"SUBTITLE": "Create custom tools to connect your assistant with external APIs and services, enabling it to fetch data and perform actions on your behalf.",
|
||||
"FEATURE_SPOTLIGHT": {
|
||||
"TITLE": "Custom Tools",
|
||||
"NOTE": "Custom tools allow your assistant to interact with external APIs and services. Create tools to fetch data, perform actions, or integrate with your existing systems to enhance your assistant's capabilities."
|
||||
}
|
||||
},
|
||||
"FORM_DESCRIPTION": "Configure your custom tool to connect with external APIs",
|
||||
"OPTIONS": {
|
||||
"EDIT_TOOL": "Edit tool",
|
||||
"DELETE_TOOL": "Delete tool"
|
||||
},
|
||||
"CREATE": {
|
||||
"TITLE": "Create Custom Tool",
|
||||
"SUCCESS_MESSAGE": "Custom tool created successfully",
|
||||
"ERROR_MESSAGE": "Failed to create custom tool"
|
||||
},
|
||||
"EDIT": {
|
||||
"TITLE": "Edit Custom Tool",
|
||||
"SUCCESS_MESSAGE": "Custom tool updated successfully",
|
||||
"ERROR_MESSAGE": "Failed to update custom tool"
|
||||
},
|
||||
"DELETE": {
|
||||
"TITLE": "Delete Custom Tool",
|
||||
"DESCRIPTION": "Are you sure you want to delete this custom tool? This action cannot be undone.",
|
||||
"CONFIRM": "Yes, delete",
|
||||
"SUCCESS_MESSAGE": "Custom tool deleted successfully",
|
||||
"ERROR_MESSAGE": "Failed to delete custom tool"
|
||||
},
|
||||
"FORM": {
|
||||
"TITLE": {
|
||||
"LABEL": "Tool Name",
|
||||
"PLACEHOLDER": "Order Lookup",
|
||||
"ERROR": "Tool name is required"
|
||||
},
|
||||
"DESCRIPTION": {
|
||||
"LABEL": "Description",
|
||||
"PLACEHOLDER": "Looks up order details by order ID"
|
||||
},
|
||||
"HTTP_METHOD": {
|
||||
"LABEL": "Method"
|
||||
},
|
||||
"ENDPOINT_URL": {
|
||||
"LABEL": "Endpoint URL",
|
||||
"PLACEHOLDER": "https://api.example.com/orders/{'{{'} order_id {'}}'}",
|
||||
"ERROR": "Valid URL is required"
|
||||
},
|
||||
"AUTH_TYPE": {
|
||||
"LABEL": "Authentication Type"
|
||||
},
|
||||
"AUTH_TYPES": {
|
||||
"NONE": "None",
|
||||
"BEARER": "Bearer Token",
|
||||
"BASIC": "Basic Auth",
|
||||
"API_KEY": "API Key"
|
||||
},
|
||||
"AUTH_CONFIG": {
|
||||
"BEARER_TOKEN": "Bearer Token",
|
||||
"BEARER_TOKEN_PLACEHOLDER": "Enter your bearer token",
|
||||
"USERNAME": "Username",
|
||||
"USERNAME_PLACEHOLDER": "Enter username",
|
||||
"PASSWORD": "Password",
|
||||
"PASSWORD_PLACEHOLDER": "Enter password",
|
||||
"API_KEY": "Header Name",
|
||||
"API_KEY_PLACEHOLDER": "X-API-Key",
|
||||
"API_VALUE": "Header Value",
|
||||
"API_VALUE_PLACEHOLDER": "Enter API key value"
|
||||
},
|
||||
"PARAMETERS": {
|
||||
"LABEL": "Parameters",
|
||||
"HELP_TEXT": "Define the parameters that will be extracted from user queries"
|
||||
},
|
||||
"ADD_PARAMETER": "Add Parameter",
|
||||
"PARAM_NAME": {
|
||||
"PLACEHOLDER": "Parameter name (e.g., order_id)"
|
||||
},
|
||||
"PARAM_TYPE": {
|
||||
"PLACEHOLDER": "Type"
|
||||
},
|
||||
"PARAM_TYPES": {
|
||||
"STRING": "String",
|
||||
"NUMBER": "Number",
|
||||
"BOOLEAN": "Boolean",
|
||||
"ARRAY": "Array",
|
||||
"OBJECT": "Object"
|
||||
},
|
||||
"PARAM_DESCRIPTION": {
|
||||
"PLACEHOLDER": "Description of the parameter"
|
||||
},
|
||||
"PARAM_REQUIRED": {
|
||||
"LABEL": "Required"
|
||||
},
|
||||
"REQUEST_TEMPLATE": {
|
||||
"LABEL": "Request Body Template (Optional)",
|
||||
"PLACEHOLDER": "{'{'}\n \"order_id\": \"{'{{'} order_id {'}}'}\"\n{'}'}"
|
||||
},
|
||||
"RESPONSE_TEMPLATE": {
|
||||
"LABEL": "Response Template (Optional)",
|
||||
"PLACEHOLDER": "Order {'{{'} order_id {'}}'} status: {'{{'} status {'}}'}"
|
||||
},
|
||||
"ERRORS": {
|
||||
"PARAM_NAME_REQUIRED": "Parameter name is required"
|
||||
}
|
||||
}
|
||||
},
|
||||
"RESPONSES": {
|
||||
"HEADER": "FAQs",
|
||||
"PENDING_FAQS": "Pending FAQs",
|
||||
"ADD_NEW": "Create new FAQ",
|
||||
"DOCUMENTABLE": {
|
||||
"CONVERSATION": "Conversation #{id}"
|
||||
@@ -793,6 +903,10 @@
|
||||
"APPROVED": "Approved",
|
||||
"ALL": "All"
|
||||
},
|
||||
"PENDING_BANNER": {
|
||||
"TITLE": "Captain has found some FAQs your customers were looking for.",
|
||||
"ACTION": "Click here to review"
|
||||
},
|
||||
"FORM_DESCRIPTION": "Add a question and its corresponding answer to the knowledge base and select the assistant it should be associated with.",
|
||||
"CREATE": {
|
||||
"TITLE": "Add an FAQ",
|
||||
@@ -823,13 +937,15 @@
|
||||
"APPROVE_SUCCESS_MESSAGE": "The FAQ was marked as approved"
|
||||
},
|
||||
"OPTIONS": {
|
||||
"APPROVE": "Mark as approved",
|
||||
"EDIT_RESPONSE": "Edit FAQ",
|
||||
"DELETE_RESPONSE": "Delete FAQ"
|
||||
"APPROVE": "Approve",
|
||||
"EDIT_RESPONSE": "Edit",
|
||||
"DELETE_RESPONSE": "Delete"
|
||||
},
|
||||
"EMPTY_STATE": {
|
||||
"TITLE": "No FAQs Found",
|
||||
"NO_PENDING_TITLE": "There are no more pending FAQs to review",
|
||||
"SUBTITLE": "FAQs help your assistant provide quick and accurate answers to questions from your customers. They can be generated automatically from your content or can be added manually.",
|
||||
"CLEAR_SEARCH": "Clear active filters",
|
||||
"FEATURE_SPOTLIGHT": {
|
||||
"TITLE": "Captain FAQ",
|
||||
"NOTE": "Captain FAQs detects common customer questions—whether missing from your knowledge base or frequently asked—and generates relevant FAQs to improve support. You can review each suggestion and decide whether to approve or reject it."
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
},
|
||||
"DATE_RANGE_OPTIONS": {
|
||||
"LAST_7_DAYS": "Last 7 days",
|
||||
"LAST_14_DAYS": "Last 14 days",
|
||||
"LAST_30_DAYS": "Last 30 days",
|
||||
"LAST_3_MONTHS": "Last 3 months",
|
||||
"LAST_6_MONTHS": "Last 6 months",
|
||||
@@ -266,6 +267,8 @@
|
||||
"NO_ENOUGH_DATA": "We've not received enough data points to generate report, Please try again later.",
|
||||
"DOWNLOAD_INBOX_REPORTS": "Download inbox reports",
|
||||
"FILTER_DROPDOWN_LABEL": "Select Inbox",
|
||||
"ALL_INBOXES": "All Inboxes",
|
||||
"SEARCH_INBOX": "Search Inbox",
|
||||
"METRICS": {
|
||||
"CONVERSATIONS": {
|
||||
"NAME": "Conversations",
|
||||
@@ -467,6 +470,13 @@
|
||||
"CONVERSATIONS": "{count} conversations",
|
||||
"DOWNLOAD_REPORT": "Download report"
|
||||
},
|
||||
"RESOLUTION_HEATMAP": {
|
||||
"HEADER": "Resolutions",
|
||||
"NO_CONVERSATIONS": "No conversations",
|
||||
"CONVERSATION": "{count} conversation",
|
||||
"CONVERSATIONS": "{count} conversations",
|
||||
"DOWNLOAD_REPORT": "Download report"
|
||||
},
|
||||
"AGENT_CONVERSATIONS": {
|
||||
"HEADER": "Conversations by agents",
|
||||
"LOADING_MESSAGE": "Loading agent metrics...",
|
||||
|
||||
@@ -304,6 +304,7 @@
|
||||
"CAPTAIN_ASSISTANTS": "Assistants",
|
||||
"CAPTAIN_DOCUMENTS": "Documents",
|
||||
"CAPTAIN_RESPONSES": "FAQs",
|
||||
"CAPTAIN_TOOLS": "Tools",
|
||||
"HOME": "Home",
|
||||
"AGENTS": "Agents",
|
||||
"AGENT_BOTS": "Bots",
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
},
|
||||
"LABELS": {
|
||||
"ASSIGN_LABELS": "إضافة وسم",
|
||||
"NO_LABELS_FOUND": "لم يتم العثور على تسميات لـ",
|
||||
"NO_LABELS_FOUND": "لم يتم العثور على تصنيفات",
|
||||
"ASSIGN_SELECTED_LABELS": "تعيين التسميات المحددة",
|
||||
"ASSIGN_SUCCESFUL": "تم تعيين التسميات بنجاح.",
|
||||
"ASSIGN_FAILED": "فشل في تعيين التسميات ، الرجاء المحاولة مرة أخرى."
|
||||
|
||||
@@ -571,6 +571,16 @@
|
||||
"ACTIVE_EMPTY_STATE_TITLE": "No contacts are active at the moment 🌙"
|
||||
}
|
||||
},
|
||||
"CONTACTS_BULK_ACTIONS": {
|
||||
"ASSIGN_LABELS": "تعيين التسميات",
|
||||
"ASSIGN_LABELS_SUCCESS": "تم تعيين التسميات بنجاح.",
|
||||
"ASSIGN_LABELS_FAILED": "Failed to assign labels",
|
||||
"DESCRIPTION": "Select the labels you want to add to the selected contacts.",
|
||||
"NO_LABELS_FOUND": "No labels available yet.",
|
||||
"SELECTED_COUNT": "{count} selected",
|
||||
"CLEAR_SELECTION": "Clear selection",
|
||||
"SELECT_ALL": "Select all ({count})"
|
||||
},
|
||||
"COMPOSE_NEW_CONVERSATION": {
|
||||
"CONTACT_SEARCH": {
|
||||
"ERROR_MESSAGE": "We couldn’t complete the search. Please try again."
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
"LEARN_MORE": "تعلم المزيد عن صناديق البريد",
|
||||
"RECONNECTION_REQUIRED": "Your inbox is disconnected. You won't receive new messages until you reauthorize it.",
|
||||
"CLICK_TO_RECONNECT": "Click here to reconnect.",
|
||||
"WHATSAPP_REGISTRATION_INCOMPLETE": "Your WhatsApp Business registration isn’t complete. Please check your display name status in Meta Business Manager before reconnecting.",
|
||||
"COMPLETE_REGISTRATION": "Complete Registration",
|
||||
"LIST": {
|
||||
"404": "لا توجد صناديق وارد لقنوات تواصل مرتبطة بهذا الحساب."
|
||||
},
|
||||
@@ -605,8 +607,64 @@
|
||||
"BUSINESS_HOURS": "ساعات العمل",
|
||||
"WIDGET_BUILDER": "منشئ اللايف شات",
|
||||
"BOT_CONFIGURATION": "اعدادات البوت",
|
||||
"ACCOUNT_HEALTH": "Account Health",
|
||||
"CSAT": "تقييم رضاء العملاء"
|
||||
},
|
||||
"ACCOUNT_HEALTH": {
|
||||
"TITLE": "Manage your WhatsApp account",
|
||||
"DESCRIPTION": "Review your WhatsApp account status, messaging limits, and quality. Update settings or resolve issues if needed",
|
||||
"GO_TO_SETTINGS": "Go to Meta Business Manager",
|
||||
"NO_DATA": "Health data is not available",
|
||||
"FIELDS": {
|
||||
"DISPLAY_PHONE_NUMBER": {
|
||||
"LABEL": "Display phone number",
|
||||
"TOOLTIP": "Phone number displayed to customers"
|
||||
},
|
||||
"VERIFIED_NAME": {
|
||||
"LABEL": "Business name",
|
||||
"TOOLTIP": "Business name verified by WhatsApp"
|
||||
},
|
||||
"DISPLAY_NAME_STATUS": {
|
||||
"LABEL": "Display name status",
|
||||
"TOOLTIP": "Status of your business name verification"
|
||||
},
|
||||
"QUALITY_RATING": {
|
||||
"LABEL": "Quality rating",
|
||||
"TOOLTIP": "WhatsApp quality rating for your account"
|
||||
},
|
||||
"MESSAGING_LIMIT_TIER": {
|
||||
"LABEL": "Messaging limit tier",
|
||||
"TOOLTIP": "Daily messaging limit for your account"
|
||||
},
|
||||
"ACCOUNT_MODE": {
|
||||
"LABEL": "Account mode",
|
||||
"TOOLTIP": "Current operating mode of your WhatsApp account"
|
||||
}
|
||||
},
|
||||
"VALUES": {
|
||||
"TIERS": {
|
||||
"TIER_250": "250 customers per 24h",
|
||||
"TIER_1000": "1K customers per 24h",
|
||||
"TIER_1K": "1K customers per 24h",
|
||||
"TIER_10K": "10K customers per 24h",
|
||||
"TIER_100K": "100K customers per 24h",
|
||||
"TIER_UNLIMITED": "Unlimited customers per 24h",
|
||||
"UNKNOWN": "Rating not available"
|
||||
},
|
||||
"STATUSES": {
|
||||
"APPROVED": "Approved",
|
||||
"PENDING_REVIEW": "Pending Review",
|
||||
"AVAILABLE_WITHOUT_REVIEW": "Available Without Review",
|
||||
"REJECTED": "Rejected",
|
||||
"DECLINED": "Declined",
|
||||
"NON_EXISTS": "Non exists"
|
||||
},
|
||||
"MODES": {
|
||||
"SANDBOX": "Sandbox",
|
||||
"LIVE": "مباشر"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SETTINGS": "الإعدادات",
|
||||
"FEATURES": {
|
||||
"LABEL": "الخصائص",
|
||||
|
||||
@@ -752,8 +752,118 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"CUSTOM_TOOLS": {
|
||||
"HEADER": "Tools",
|
||||
"ADD_NEW": "Create a new tool",
|
||||
"EMPTY_STATE": {
|
||||
"TITLE": "No custom tools available",
|
||||
"SUBTITLE": "Create custom tools to connect your assistant with external APIs and services, enabling it to fetch data and perform actions on your behalf.",
|
||||
"FEATURE_SPOTLIGHT": {
|
||||
"TITLE": "Custom Tools",
|
||||
"NOTE": "Custom tools allow your assistant to interact with external APIs and services. Create tools to fetch data, perform actions, or integrate with your existing systems to enhance your assistant's capabilities."
|
||||
}
|
||||
},
|
||||
"FORM_DESCRIPTION": "Configure your custom tool to connect with external APIs",
|
||||
"OPTIONS": {
|
||||
"EDIT_TOOL": "Edit tool",
|
||||
"DELETE_TOOL": "Delete tool"
|
||||
},
|
||||
"CREATE": {
|
||||
"TITLE": "Create Custom Tool",
|
||||
"SUCCESS_MESSAGE": "Custom tool created successfully",
|
||||
"ERROR_MESSAGE": "Failed to create custom tool"
|
||||
},
|
||||
"EDIT": {
|
||||
"TITLE": "Edit Custom Tool",
|
||||
"SUCCESS_MESSAGE": "Custom tool updated successfully",
|
||||
"ERROR_MESSAGE": "Failed to update custom tool"
|
||||
},
|
||||
"DELETE": {
|
||||
"TITLE": "Delete Custom Tool",
|
||||
"DESCRIPTION": "Are you sure you want to delete this custom tool? This action cannot be undone.",
|
||||
"CONFIRM": "نعم، احذف",
|
||||
"SUCCESS_MESSAGE": "Custom tool deleted successfully",
|
||||
"ERROR_MESSAGE": "Failed to delete custom tool"
|
||||
},
|
||||
"FORM": {
|
||||
"TITLE": {
|
||||
"LABEL": "Tool Name",
|
||||
"PLACEHOLDER": "Order Lookup",
|
||||
"ERROR": "Tool name is required"
|
||||
},
|
||||
"DESCRIPTION": {
|
||||
"LABEL": "الوصف",
|
||||
"PLACEHOLDER": "Looks up order details by order ID"
|
||||
},
|
||||
"HTTP_METHOD": {
|
||||
"LABEL": "Method"
|
||||
},
|
||||
"ENDPOINT_URL": {
|
||||
"LABEL": "Endpoint URL",
|
||||
"PLACEHOLDER": "https://api.example.com/orders/{'{{'} order_id {'}}'}",
|
||||
"ERROR": "Valid URL is required"
|
||||
},
|
||||
"AUTH_TYPE": {
|
||||
"LABEL": "Authentication Type"
|
||||
},
|
||||
"AUTH_TYPES": {
|
||||
"NONE": "لا شيء",
|
||||
"BEARER": "Bearer Token",
|
||||
"BASIC": "Basic Auth",
|
||||
"API_KEY": "مفتاح API"
|
||||
},
|
||||
"AUTH_CONFIG": {
|
||||
"BEARER_TOKEN": "Bearer Token",
|
||||
"BEARER_TOKEN_PLACEHOLDER": "Enter your bearer token",
|
||||
"USERNAME": "Username",
|
||||
"USERNAME_PLACEHOLDER": "Enter username",
|
||||
"PASSWORD": "كلمة المرور",
|
||||
"PASSWORD_PLACEHOLDER": "Enter password",
|
||||
"API_KEY": "Header Name",
|
||||
"API_KEY_PLACEHOLDER": "X-API-Key",
|
||||
"API_VALUE": "Header Value",
|
||||
"API_VALUE_PLACEHOLDER": "Enter API key value"
|
||||
},
|
||||
"PARAMETERS": {
|
||||
"LABEL": "Parameters",
|
||||
"HELP_TEXT": "Define the parameters that will be extracted from user queries"
|
||||
},
|
||||
"ADD_PARAMETER": "Add Parameter",
|
||||
"PARAM_NAME": {
|
||||
"PLACEHOLDER": "Parameter name (e.g., order_id)"
|
||||
},
|
||||
"PARAM_TYPE": {
|
||||
"PLACEHOLDER": "النوع"
|
||||
},
|
||||
"PARAM_TYPES": {
|
||||
"STRING": "String",
|
||||
"NUMBER": "العدد",
|
||||
"BOOLEAN": "Boolean",
|
||||
"ARRAY": "Array",
|
||||
"OBJECT": "Object"
|
||||
},
|
||||
"PARAM_DESCRIPTION": {
|
||||
"PLACEHOLDER": "Description of the parameter"
|
||||
},
|
||||
"PARAM_REQUIRED": {
|
||||
"LABEL": "مطلوب"
|
||||
},
|
||||
"REQUEST_TEMPLATE": {
|
||||
"LABEL": "Request Body Template (Optional)",
|
||||
"PLACEHOLDER": "{'{'}\n \"order_id\": \"{'{{'} order_id {'}}'}\"\n{'}'}"
|
||||
},
|
||||
"RESPONSE_TEMPLATE": {
|
||||
"LABEL": "Response Template (Optional)",
|
||||
"PLACEHOLDER": "Order {'{{'} order_id {'}}'} status: {'{{'} status {'}}'}"
|
||||
},
|
||||
"ERRORS": {
|
||||
"PARAM_NAME_REQUIRED": "Parameter name is required"
|
||||
}
|
||||
}
|
||||
},
|
||||
"RESPONSES": {
|
||||
"HEADER": "FAQs",
|
||||
"PENDING_FAQS": "Pending FAQs",
|
||||
"ADD_NEW": "Create new FAQ",
|
||||
"DOCUMENTABLE": {
|
||||
"CONVERSATION": "Conversation #{id}"
|
||||
@@ -793,6 +903,10 @@
|
||||
"APPROVED": "Approved",
|
||||
"ALL": "الكل"
|
||||
},
|
||||
"PENDING_BANNER": {
|
||||
"TITLE": "Captain has found some FAQs your customers were looking for.",
|
||||
"ACTION": "Click here to review"
|
||||
},
|
||||
"FORM_DESCRIPTION": "Add a question and its corresponding answer to the knowledge base and select the assistant it should be associated with.",
|
||||
"CREATE": {
|
||||
"TITLE": "Add an FAQ",
|
||||
@@ -823,13 +937,15 @@
|
||||
"APPROVE_SUCCESS_MESSAGE": "The FAQ was marked as approved"
|
||||
},
|
||||
"OPTIONS": {
|
||||
"APPROVE": "Mark as approved",
|
||||
"EDIT_RESPONSE": "Edit FAQ",
|
||||
"DELETE_RESPONSE": "Delete FAQ"
|
||||
"APPROVE": "Approve",
|
||||
"EDIT_RESPONSE": "تعديل",
|
||||
"DELETE_RESPONSE": "حذف"
|
||||
},
|
||||
"EMPTY_STATE": {
|
||||
"TITLE": "No FAQs Found",
|
||||
"NO_PENDING_TITLE": "There are no more pending FAQs to review",
|
||||
"SUBTITLE": "FAQs help your assistant provide quick and accurate answers to questions from your customers. They can be generated automatically from your content or can be added manually.",
|
||||
"CLEAR_SEARCH": "Clear active filters",
|
||||
"FEATURE_SPOTLIGHT": {
|
||||
"TITLE": "Captain FAQ",
|
||||
"NOTE": "Captain FAQs detects common customer questions—whether missing from your knowledge base or frequently asked—and generates relevant FAQs to improve support. You can review each suggestion and decide whether to approve or reject it."
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
},
|
||||
"DATE_RANGE_OPTIONS": {
|
||||
"LAST_7_DAYS": "آخر 7 أيام",
|
||||
"LAST_14_DAYS": "آخر 14 يوماً",
|
||||
"LAST_30_DAYS": "آخر 30 يوماً",
|
||||
"LAST_3_MONTHS": "آخر 3 أشهر",
|
||||
"LAST_6_MONTHS": "آخر 6 أشهر",
|
||||
@@ -266,6 +267,8 @@
|
||||
"NO_ENOUGH_DATA": "لم يتم جمع بيانات بقدر كافي لإنشاء التقرير، الرجاء المحاولة مرة أخرى لاحقاً.",
|
||||
"DOWNLOAD_INBOX_REPORTS": "تحميل تقارير صندوق الوارد",
|
||||
"FILTER_DROPDOWN_LABEL": "اختر صندوق الوارد",
|
||||
"ALL_INBOXES": "All Inboxes",
|
||||
"SEARCH_INBOX": "Search Inbox",
|
||||
"METRICS": {
|
||||
"CONVERSATIONS": {
|
||||
"NAME": "المحادثات",
|
||||
@@ -467,6 +470,13 @@
|
||||
"CONVERSATIONS": "{count} conversations",
|
||||
"DOWNLOAD_REPORT": "تنزيل التقرير"
|
||||
},
|
||||
"RESOLUTION_HEATMAP": {
|
||||
"HEADER": "Resolutions",
|
||||
"NO_CONVERSATIONS": "No conversations",
|
||||
"CONVERSATION": "{count} conversation",
|
||||
"CONVERSATIONS": "{count} conversations",
|
||||
"DOWNLOAD_REPORT": "تنزيل التقرير"
|
||||
},
|
||||
"AGENT_CONVERSATIONS": {
|
||||
"HEADER": "المحادثات من قبل الوكلاء",
|
||||
"LOADING_MESSAGE": "جاري تحميل مقاييس الوكيل...",
|
||||
|
||||
@@ -304,6 +304,7 @@
|
||||
"CAPTAIN_ASSISTANTS": "Assistants",
|
||||
"CAPTAIN_DOCUMENTS": "Documents",
|
||||
"CAPTAIN_RESPONSES": "FAQs",
|
||||
"CAPTAIN_TOOLS": "Tools",
|
||||
"HOME": "الرئيسية",
|
||||
"AGENTS": "وكيل الدعم",
|
||||
"AGENT_BOTS": "الروبوتات",
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
},
|
||||
"LABELS": {
|
||||
"ASSIGN_LABELS": "Assign labels",
|
||||
"NO_LABELS_FOUND": "No labels found for",
|
||||
"NO_LABELS_FOUND": "No labels found",
|
||||
"ASSIGN_SELECTED_LABELS": "Assign selected labels",
|
||||
"ASSIGN_SUCCESFUL": "Labels assigned successfully.",
|
||||
"ASSIGN_FAILED": "Failed to assign labels. Please try again."
|
||||
|
||||
@@ -571,6 +571,16 @@
|
||||
"ACTIVE_EMPTY_STATE_TITLE": "No contacts are active at the moment 🌙"
|
||||
}
|
||||
},
|
||||
"CONTACTS_BULK_ACTIONS": {
|
||||
"ASSIGN_LABELS": "Assign Labels",
|
||||
"ASSIGN_LABELS_SUCCESS": "Labels assigned successfully.",
|
||||
"ASSIGN_LABELS_FAILED": "Failed to assign labels",
|
||||
"DESCRIPTION": "Select the labels you want to add to the selected contacts.",
|
||||
"NO_LABELS_FOUND": "No labels available yet.",
|
||||
"SELECTED_COUNT": "{count} selected",
|
||||
"CLEAR_SELECTION": "Clear selection",
|
||||
"SELECT_ALL": "Select all ({count})"
|
||||
},
|
||||
"COMPOSE_NEW_CONVERSATION": {
|
||||
"CONTACT_SEARCH": {
|
||||
"ERROR_MESSAGE": "We couldn’t complete the search. Please try again."
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
"LEARN_MORE": "Learn more about inboxes",
|
||||
"RECONNECTION_REQUIRED": "Your inbox is disconnected. You won't receive new messages until you reauthorize it.",
|
||||
"CLICK_TO_RECONNECT": "Click here to reconnect.",
|
||||
"WHATSAPP_REGISTRATION_INCOMPLETE": "Your WhatsApp Business registration isn’t complete. Please check your display name status in Meta Business Manager before reconnecting.",
|
||||
"COMPLETE_REGISTRATION": "Complete Registration",
|
||||
"LIST": {
|
||||
"404": "There are no inboxes attached to this account."
|
||||
},
|
||||
@@ -605,8 +607,64 @@
|
||||
"BUSINESS_HOURS": "Business Hours",
|
||||
"WIDGET_BUILDER": "Widget Builder",
|
||||
"BOT_CONFIGURATION": "Bot Configuration",
|
||||
"ACCOUNT_HEALTH": "Account Health",
|
||||
"CSAT": "CSAT"
|
||||
},
|
||||
"ACCOUNT_HEALTH": {
|
||||
"TITLE": "Manage your WhatsApp account",
|
||||
"DESCRIPTION": "Review your WhatsApp account status, messaging limits, and quality. Update settings or resolve issues if needed",
|
||||
"GO_TO_SETTINGS": "Go to Meta Business Manager",
|
||||
"NO_DATA": "Health data is not available",
|
||||
"FIELDS": {
|
||||
"DISPLAY_PHONE_NUMBER": {
|
||||
"LABEL": "Display phone number",
|
||||
"TOOLTIP": "Phone number displayed to customers"
|
||||
},
|
||||
"VERIFIED_NAME": {
|
||||
"LABEL": "Business name",
|
||||
"TOOLTIP": "Business name verified by WhatsApp"
|
||||
},
|
||||
"DISPLAY_NAME_STATUS": {
|
||||
"LABEL": "Display name status",
|
||||
"TOOLTIP": "Status of your business name verification"
|
||||
},
|
||||
"QUALITY_RATING": {
|
||||
"LABEL": "Quality rating",
|
||||
"TOOLTIP": "WhatsApp quality rating for your account"
|
||||
},
|
||||
"MESSAGING_LIMIT_TIER": {
|
||||
"LABEL": "Messaging limit tier",
|
||||
"TOOLTIP": "Daily messaging limit for your account"
|
||||
},
|
||||
"ACCOUNT_MODE": {
|
||||
"LABEL": "Account mode",
|
||||
"TOOLTIP": "Current operating mode of your WhatsApp account"
|
||||
}
|
||||
},
|
||||
"VALUES": {
|
||||
"TIERS": {
|
||||
"TIER_250": "250 customers per 24h",
|
||||
"TIER_1000": "1K customers per 24h",
|
||||
"TIER_1K": "1K customers per 24h",
|
||||
"TIER_10K": "10K customers per 24h",
|
||||
"TIER_100K": "100K customers per 24h",
|
||||
"TIER_UNLIMITED": "Unlimited customers per 24h",
|
||||
"UNKNOWN": "Rating not available"
|
||||
},
|
||||
"STATUSES": {
|
||||
"APPROVED": "Approved",
|
||||
"PENDING_REVIEW": "Pending Review",
|
||||
"AVAILABLE_WITHOUT_REVIEW": "Available Without Review",
|
||||
"REJECTED": "Rejected",
|
||||
"DECLINED": "Declined",
|
||||
"NON_EXISTS": "Non exists"
|
||||
},
|
||||
"MODES": {
|
||||
"SANDBOX": "Sandbox",
|
||||
"LIVE": "Live"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SETTINGS": "Settings",
|
||||
"FEATURES": {
|
||||
"LABEL": "Features",
|
||||
|
||||
@@ -752,8 +752,118 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"CUSTOM_TOOLS": {
|
||||
"HEADER": "Tools",
|
||||
"ADD_NEW": "Create a new tool",
|
||||
"EMPTY_STATE": {
|
||||
"TITLE": "No custom tools available",
|
||||
"SUBTITLE": "Create custom tools to connect your assistant with external APIs and services, enabling it to fetch data and perform actions on your behalf.",
|
||||
"FEATURE_SPOTLIGHT": {
|
||||
"TITLE": "Custom Tools",
|
||||
"NOTE": "Custom tools allow your assistant to interact with external APIs and services. Create tools to fetch data, perform actions, or integrate with your existing systems to enhance your assistant's capabilities."
|
||||
}
|
||||
},
|
||||
"FORM_DESCRIPTION": "Configure your custom tool to connect with external APIs",
|
||||
"OPTIONS": {
|
||||
"EDIT_TOOL": "Edit tool",
|
||||
"DELETE_TOOL": "Delete tool"
|
||||
},
|
||||
"CREATE": {
|
||||
"TITLE": "Create Custom Tool",
|
||||
"SUCCESS_MESSAGE": "Custom tool created successfully",
|
||||
"ERROR_MESSAGE": "Failed to create custom tool"
|
||||
},
|
||||
"EDIT": {
|
||||
"TITLE": "Edit Custom Tool",
|
||||
"SUCCESS_MESSAGE": "Custom tool updated successfully",
|
||||
"ERROR_MESSAGE": "Failed to update custom tool"
|
||||
},
|
||||
"DELETE": {
|
||||
"TITLE": "Delete Custom Tool",
|
||||
"DESCRIPTION": "Are you sure you want to delete this custom tool? This action cannot be undone.",
|
||||
"CONFIRM": "Yes, delete",
|
||||
"SUCCESS_MESSAGE": "Custom tool deleted successfully",
|
||||
"ERROR_MESSAGE": "Failed to delete custom tool"
|
||||
},
|
||||
"FORM": {
|
||||
"TITLE": {
|
||||
"LABEL": "Tool Name",
|
||||
"PLACEHOLDER": "Order Lookup",
|
||||
"ERROR": "Tool name is required"
|
||||
},
|
||||
"DESCRIPTION": {
|
||||
"LABEL": "Description",
|
||||
"PLACEHOLDER": "Looks up order details by order ID"
|
||||
},
|
||||
"HTTP_METHOD": {
|
||||
"LABEL": "Method"
|
||||
},
|
||||
"ENDPOINT_URL": {
|
||||
"LABEL": "Endpoint URL",
|
||||
"PLACEHOLDER": "https://api.example.com/orders/{'{{'} order_id {'}}'}",
|
||||
"ERROR": "Valid URL is required"
|
||||
},
|
||||
"AUTH_TYPE": {
|
||||
"LABEL": "Authentication Type"
|
||||
},
|
||||
"AUTH_TYPES": {
|
||||
"NONE": "None",
|
||||
"BEARER": "Bearer Token",
|
||||
"BASIC": "Basic Auth",
|
||||
"API_KEY": "API Key"
|
||||
},
|
||||
"AUTH_CONFIG": {
|
||||
"BEARER_TOKEN": "Bearer Token",
|
||||
"BEARER_TOKEN_PLACEHOLDER": "Enter your bearer token",
|
||||
"USERNAME": "Username",
|
||||
"USERNAME_PLACEHOLDER": "Enter username",
|
||||
"PASSWORD": "Password",
|
||||
"PASSWORD_PLACEHOLDER": "Enter password",
|
||||
"API_KEY": "Header Name",
|
||||
"API_KEY_PLACEHOLDER": "X-API-Key",
|
||||
"API_VALUE": "Header Value",
|
||||
"API_VALUE_PLACEHOLDER": "Enter API key value"
|
||||
},
|
||||
"PARAMETERS": {
|
||||
"LABEL": "Parameters",
|
||||
"HELP_TEXT": "Define the parameters that will be extracted from user queries"
|
||||
},
|
||||
"ADD_PARAMETER": "Add Parameter",
|
||||
"PARAM_NAME": {
|
||||
"PLACEHOLDER": "Parameter name (e.g., order_id)"
|
||||
},
|
||||
"PARAM_TYPE": {
|
||||
"PLACEHOLDER": "Type"
|
||||
},
|
||||
"PARAM_TYPES": {
|
||||
"STRING": "String",
|
||||
"NUMBER": "Number",
|
||||
"BOOLEAN": "Boolean",
|
||||
"ARRAY": "Array",
|
||||
"OBJECT": "Object"
|
||||
},
|
||||
"PARAM_DESCRIPTION": {
|
||||
"PLACEHOLDER": "Description of the parameter"
|
||||
},
|
||||
"PARAM_REQUIRED": {
|
||||
"LABEL": "Required"
|
||||
},
|
||||
"REQUEST_TEMPLATE": {
|
||||
"LABEL": "Request Body Template (Optional)",
|
||||
"PLACEHOLDER": "{'{'}\n \"order_id\": \"{'{{'} order_id {'}}'}\"\n{'}'}"
|
||||
},
|
||||
"RESPONSE_TEMPLATE": {
|
||||
"LABEL": "Response Template (Optional)",
|
||||
"PLACEHOLDER": "Order {'{{'} order_id {'}}'} status: {'{{'} status {'}}'}"
|
||||
},
|
||||
"ERRORS": {
|
||||
"PARAM_NAME_REQUIRED": "Parameter name is required"
|
||||
}
|
||||
}
|
||||
},
|
||||
"RESPONSES": {
|
||||
"HEADER": "FAQs",
|
||||
"PENDING_FAQS": "Pending FAQs",
|
||||
"ADD_NEW": "Create new FAQ",
|
||||
"DOCUMENTABLE": {
|
||||
"CONVERSATION": "Conversation #{id}"
|
||||
@@ -793,6 +903,10 @@
|
||||
"APPROVED": "Approved",
|
||||
"ALL": "All"
|
||||
},
|
||||
"PENDING_BANNER": {
|
||||
"TITLE": "Captain has found some FAQs your customers were looking for.",
|
||||
"ACTION": "Click here to review"
|
||||
},
|
||||
"FORM_DESCRIPTION": "Add a question and its corresponding answer to the knowledge base and select the assistant it should be associated with.",
|
||||
"CREATE": {
|
||||
"TITLE": "Add an FAQ",
|
||||
@@ -823,13 +937,15 @@
|
||||
"APPROVE_SUCCESS_MESSAGE": "The FAQ was marked as approved"
|
||||
},
|
||||
"OPTIONS": {
|
||||
"APPROVE": "Mark as approved",
|
||||
"EDIT_RESPONSE": "Edit FAQ",
|
||||
"DELETE_RESPONSE": "Delete FAQ"
|
||||
"APPROVE": "Approve",
|
||||
"EDIT_RESPONSE": "Edit",
|
||||
"DELETE_RESPONSE": "Delete"
|
||||
},
|
||||
"EMPTY_STATE": {
|
||||
"TITLE": "No FAQs Found",
|
||||
"NO_PENDING_TITLE": "There are no more pending FAQs to review",
|
||||
"SUBTITLE": "FAQs help your assistant provide quick and accurate answers to questions from your customers. They can be generated automatically from your content or can be added manually.",
|
||||
"CLEAR_SEARCH": "Clear active filters",
|
||||
"FEATURE_SPOTLIGHT": {
|
||||
"TITLE": "Captain FAQ",
|
||||
"NOTE": "Captain FAQs detects common customer questions—whether missing from your knowledge base or frequently asked—and generates relevant FAQs to improve support. You can review each suggestion and decide whether to approve or reject it."
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
},
|
||||
"DATE_RANGE_OPTIONS": {
|
||||
"LAST_7_DAYS": "Last 7 days",
|
||||
"LAST_14_DAYS": "Last 14 days",
|
||||
"LAST_30_DAYS": "Last 30 days",
|
||||
"LAST_3_MONTHS": "Last 3 months",
|
||||
"LAST_6_MONTHS": "Last 6 months",
|
||||
@@ -266,6 +267,8 @@
|
||||
"NO_ENOUGH_DATA": "We've not received enough data points to generate report, Please try again later.",
|
||||
"DOWNLOAD_INBOX_REPORTS": "Download inbox reports",
|
||||
"FILTER_DROPDOWN_LABEL": "Select Inbox",
|
||||
"ALL_INBOXES": "All Inboxes",
|
||||
"SEARCH_INBOX": "Search Inbox",
|
||||
"METRICS": {
|
||||
"CONVERSATIONS": {
|
||||
"NAME": "Conversations",
|
||||
@@ -467,6 +470,13 @@
|
||||
"CONVERSATIONS": "{count} conversations",
|
||||
"DOWNLOAD_REPORT": "Download report"
|
||||
},
|
||||
"RESOLUTION_HEATMAP": {
|
||||
"HEADER": "Resolutions",
|
||||
"NO_CONVERSATIONS": "No conversations",
|
||||
"CONVERSATION": "{count} conversation",
|
||||
"CONVERSATIONS": "{count} conversations",
|
||||
"DOWNLOAD_REPORT": "Download report"
|
||||
},
|
||||
"AGENT_CONVERSATIONS": {
|
||||
"HEADER": "Conversations by agents",
|
||||
"LOADING_MESSAGE": "Loading agent metrics...",
|
||||
|
||||
@@ -304,6 +304,7 @@
|
||||
"CAPTAIN_ASSISTANTS": "Assistants",
|
||||
"CAPTAIN_DOCUMENTS": "Documents",
|
||||
"CAPTAIN_RESPONSES": "FAQs",
|
||||
"CAPTAIN_TOOLS": "Tools",
|
||||
"HOME": "Home",
|
||||
"AGENTS": "Agents",
|
||||
"AGENT_BOTS": "Bots",
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
},
|
||||
"LABELS": {
|
||||
"ASSIGN_LABELS": "Assign labels",
|
||||
"NO_LABELS_FOUND": "No labels found for",
|
||||
"NO_LABELS_FOUND": "Няма намерени етикети",
|
||||
"ASSIGN_SELECTED_LABELS": "Assign selected labels",
|
||||
"ASSIGN_SUCCESFUL": "Labels assigned successfully.",
|
||||
"ASSIGN_FAILED": "Failed to assign labels. Please try again."
|
||||
|
||||
@@ -571,6 +571,16 @@
|
||||
"ACTIVE_EMPTY_STATE_TITLE": "No contacts are active at the moment 🌙"
|
||||
}
|
||||
},
|
||||
"CONTACTS_BULK_ACTIONS": {
|
||||
"ASSIGN_LABELS": "Assign Labels",
|
||||
"ASSIGN_LABELS_SUCCESS": "Labels assigned successfully.",
|
||||
"ASSIGN_LABELS_FAILED": "Failed to assign labels",
|
||||
"DESCRIPTION": "Select the labels you want to add to the selected contacts.",
|
||||
"NO_LABELS_FOUND": "No labels available yet.",
|
||||
"SELECTED_COUNT": "{count} selected",
|
||||
"CLEAR_SELECTION": "Clear selection",
|
||||
"SELECT_ALL": "Select all ({count})"
|
||||
},
|
||||
"COMPOSE_NEW_CONVERSATION": {
|
||||
"CONTACT_SEARCH": {
|
||||
"ERROR_MESSAGE": "We couldn’t complete the search. Please try again."
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
"LEARN_MORE": "Learn more about inboxes",
|
||||
"RECONNECTION_REQUIRED": "Your inbox is disconnected. You won't receive new messages until you reauthorize it.",
|
||||
"CLICK_TO_RECONNECT": "Click here to reconnect.",
|
||||
"WHATSAPP_REGISTRATION_INCOMPLETE": "Your WhatsApp Business registration isn’t complete. Please check your display name status in Meta Business Manager before reconnecting.",
|
||||
"COMPLETE_REGISTRATION": "Complete Registration",
|
||||
"LIST": {
|
||||
"404": "There are no inboxes attached to this account."
|
||||
},
|
||||
@@ -605,8 +607,64 @@
|
||||
"BUSINESS_HOURS": "Business Hours",
|
||||
"WIDGET_BUILDER": "Widget Builder",
|
||||
"BOT_CONFIGURATION": "Bot Configuration",
|
||||
"ACCOUNT_HEALTH": "Account Health",
|
||||
"CSAT": "CSAT"
|
||||
},
|
||||
"ACCOUNT_HEALTH": {
|
||||
"TITLE": "Manage your WhatsApp account",
|
||||
"DESCRIPTION": "Review your WhatsApp account status, messaging limits, and quality. Update settings or resolve issues if needed",
|
||||
"GO_TO_SETTINGS": "Go to Meta Business Manager",
|
||||
"NO_DATA": "Health data is not available",
|
||||
"FIELDS": {
|
||||
"DISPLAY_PHONE_NUMBER": {
|
||||
"LABEL": "Display phone number",
|
||||
"TOOLTIP": "Phone number displayed to customers"
|
||||
},
|
||||
"VERIFIED_NAME": {
|
||||
"LABEL": "Business name",
|
||||
"TOOLTIP": "Business name verified by WhatsApp"
|
||||
},
|
||||
"DISPLAY_NAME_STATUS": {
|
||||
"LABEL": "Display name status",
|
||||
"TOOLTIP": "Status of your business name verification"
|
||||
},
|
||||
"QUALITY_RATING": {
|
||||
"LABEL": "Quality rating",
|
||||
"TOOLTIP": "WhatsApp quality rating for your account"
|
||||
},
|
||||
"MESSAGING_LIMIT_TIER": {
|
||||
"LABEL": "Messaging limit tier",
|
||||
"TOOLTIP": "Daily messaging limit for your account"
|
||||
},
|
||||
"ACCOUNT_MODE": {
|
||||
"LABEL": "Account mode",
|
||||
"TOOLTIP": "Current operating mode of your WhatsApp account"
|
||||
}
|
||||
},
|
||||
"VALUES": {
|
||||
"TIERS": {
|
||||
"TIER_250": "250 customers per 24h",
|
||||
"TIER_1000": "1K customers per 24h",
|
||||
"TIER_1K": "1K customers per 24h",
|
||||
"TIER_10K": "10K customers per 24h",
|
||||
"TIER_100K": "100K customers per 24h",
|
||||
"TIER_UNLIMITED": "Unlimited customers per 24h",
|
||||
"UNKNOWN": "Rating not available"
|
||||
},
|
||||
"STATUSES": {
|
||||
"APPROVED": "Approved",
|
||||
"PENDING_REVIEW": "Pending Review",
|
||||
"AVAILABLE_WITHOUT_REVIEW": "Available Without Review",
|
||||
"REJECTED": "Rejected",
|
||||
"DECLINED": "Declined",
|
||||
"NON_EXISTS": "Non exists"
|
||||
},
|
||||
"MODES": {
|
||||
"SANDBOX": "Sandbox",
|
||||
"LIVE": "Live"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SETTINGS": "Settings",
|
||||
"FEATURES": {
|
||||
"LABEL": "Features",
|
||||
|
||||
@@ -752,8 +752,118 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"CUSTOM_TOOLS": {
|
||||
"HEADER": "Tools",
|
||||
"ADD_NEW": "Create a new tool",
|
||||
"EMPTY_STATE": {
|
||||
"TITLE": "No custom tools available",
|
||||
"SUBTITLE": "Create custom tools to connect your assistant with external APIs and services, enabling it to fetch data and perform actions on your behalf.",
|
||||
"FEATURE_SPOTLIGHT": {
|
||||
"TITLE": "Custom Tools",
|
||||
"NOTE": "Custom tools allow your assistant to interact with external APIs and services. Create tools to fetch data, perform actions, or integrate with your existing systems to enhance your assistant's capabilities."
|
||||
}
|
||||
},
|
||||
"FORM_DESCRIPTION": "Configure your custom tool to connect with external APIs",
|
||||
"OPTIONS": {
|
||||
"EDIT_TOOL": "Edit tool",
|
||||
"DELETE_TOOL": "Delete tool"
|
||||
},
|
||||
"CREATE": {
|
||||
"TITLE": "Create Custom Tool",
|
||||
"SUCCESS_MESSAGE": "Custom tool created successfully",
|
||||
"ERROR_MESSAGE": "Failed to create custom tool"
|
||||
},
|
||||
"EDIT": {
|
||||
"TITLE": "Edit Custom Tool",
|
||||
"SUCCESS_MESSAGE": "Custom tool updated successfully",
|
||||
"ERROR_MESSAGE": "Failed to update custom tool"
|
||||
},
|
||||
"DELETE": {
|
||||
"TITLE": "Delete Custom Tool",
|
||||
"DESCRIPTION": "Are you sure you want to delete this custom tool? This action cannot be undone.",
|
||||
"CONFIRM": "Yes, delete",
|
||||
"SUCCESS_MESSAGE": "Custom tool deleted successfully",
|
||||
"ERROR_MESSAGE": "Failed to delete custom tool"
|
||||
},
|
||||
"FORM": {
|
||||
"TITLE": {
|
||||
"LABEL": "Tool Name",
|
||||
"PLACEHOLDER": "Order Lookup",
|
||||
"ERROR": "Tool name is required"
|
||||
},
|
||||
"DESCRIPTION": {
|
||||
"LABEL": "Описание",
|
||||
"PLACEHOLDER": "Looks up order details by order ID"
|
||||
},
|
||||
"HTTP_METHOD": {
|
||||
"LABEL": "Method"
|
||||
},
|
||||
"ENDPOINT_URL": {
|
||||
"LABEL": "Endpoint URL",
|
||||
"PLACEHOLDER": "https://api.example.com/orders/{'{{'} order_id {'}}'}",
|
||||
"ERROR": "Valid URL is required"
|
||||
},
|
||||
"AUTH_TYPE": {
|
||||
"LABEL": "Authentication Type"
|
||||
},
|
||||
"AUTH_TYPES": {
|
||||
"NONE": "None",
|
||||
"BEARER": "Bearer Token",
|
||||
"BASIC": "Basic Auth",
|
||||
"API_KEY": "API Key"
|
||||
},
|
||||
"AUTH_CONFIG": {
|
||||
"BEARER_TOKEN": "Bearer Token",
|
||||
"BEARER_TOKEN_PLACEHOLDER": "Enter your bearer token",
|
||||
"USERNAME": "Username",
|
||||
"USERNAME_PLACEHOLDER": "Enter username",
|
||||
"PASSWORD": "Password",
|
||||
"PASSWORD_PLACEHOLDER": "Enter password",
|
||||
"API_KEY": "Header Name",
|
||||
"API_KEY_PLACEHOLDER": "X-API-Key",
|
||||
"API_VALUE": "Header Value",
|
||||
"API_VALUE_PLACEHOLDER": "Enter API key value"
|
||||
},
|
||||
"PARAMETERS": {
|
||||
"LABEL": "Parameters",
|
||||
"HELP_TEXT": "Define the parameters that will be extracted from user queries"
|
||||
},
|
||||
"ADD_PARAMETER": "Add Parameter",
|
||||
"PARAM_NAME": {
|
||||
"PLACEHOLDER": "Parameter name (e.g., order_id)"
|
||||
},
|
||||
"PARAM_TYPE": {
|
||||
"PLACEHOLDER": "Тип"
|
||||
},
|
||||
"PARAM_TYPES": {
|
||||
"STRING": "String",
|
||||
"NUMBER": "Number",
|
||||
"BOOLEAN": "Boolean",
|
||||
"ARRAY": "Array",
|
||||
"OBJECT": "Object"
|
||||
},
|
||||
"PARAM_DESCRIPTION": {
|
||||
"PLACEHOLDER": "Description of the parameter"
|
||||
},
|
||||
"PARAM_REQUIRED": {
|
||||
"LABEL": "Required"
|
||||
},
|
||||
"REQUEST_TEMPLATE": {
|
||||
"LABEL": "Request Body Template (Optional)",
|
||||
"PLACEHOLDER": "{'{'}\n \"order_id\": \"{'{{'} order_id {'}}'}\"\n{'}'}"
|
||||
},
|
||||
"RESPONSE_TEMPLATE": {
|
||||
"LABEL": "Response Template (Optional)",
|
||||
"PLACEHOLDER": "Order {'{{'} order_id {'}}'} status: {'{{'} status {'}}'}"
|
||||
},
|
||||
"ERRORS": {
|
||||
"PARAM_NAME_REQUIRED": "Parameter name is required"
|
||||
}
|
||||
}
|
||||
},
|
||||
"RESPONSES": {
|
||||
"HEADER": "FAQs",
|
||||
"PENDING_FAQS": "Pending FAQs",
|
||||
"ADD_NEW": "Create new FAQ",
|
||||
"DOCUMENTABLE": {
|
||||
"CONVERSATION": "Conversation #{id}"
|
||||
@@ -793,6 +903,10 @@
|
||||
"APPROVED": "Approved",
|
||||
"ALL": "Всички"
|
||||
},
|
||||
"PENDING_BANNER": {
|
||||
"TITLE": "Captain has found some FAQs your customers were looking for.",
|
||||
"ACTION": "Click here to review"
|
||||
},
|
||||
"FORM_DESCRIPTION": "Add a question and its corresponding answer to the knowledge base and select the assistant it should be associated with.",
|
||||
"CREATE": {
|
||||
"TITLE": "Add an FAQ",
|
||||
@@ -823,13 +937,15 @@
|
||||
"APPROVE_SUCCESS_MESSAGE": "The FAQ was marked as approved"
|
||||
},
|
||||
"OPTIONS": {
|
||||
"APPROVE": "Mark as approved",
|
||||
"EDIT_RESPONSE": "Edit FAQ",
|
||||
"DELETE_RESPONSE": "Delete FAQ"
|
||||
"APPROVE": "Approve",
|
||||
"EDIT_RESPONSE": "Редактирай",
|
||||
"DELETE_RESPONSE": "Изтрий"
|
||||
},
|
||||
"EMPTY_STATE": {
|
||||
"TITLE": "No FAQs Found",
|
||||
"NO_PENDING_TITLE": "There are no more pending FAQs to review",
|
||||
"SUBTITLE": "FAQs help your assistant provide quick and accurate answers to questions from your customers. They can be generated automatically from your content or can be added manually.",
|
||||
"CLEAR_SEARCH": "Clear active filters",
|
||||
"FEATURE_SPOTLIGHT": {
|
||||
"TITLE": "Captain FAQ",
|
||||
"NOTE": "Captain FAQs detects common customer questions—whether missing from your knowledge base or frequently asked—and generates relevant FAQs to improve support. You can review each suggestion and decide whether to approve or reject it."
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
},
|
||||
"DATE_RANGE_OPTIONS": {
|
||||
"LAST_7_DAYS": "Last 7 days",
|
||||
"LAST_14_DAYS": "Last 14 days",
|
||||
"LAST_30_DAYS": "Last 30 days",
|
||||
"LAST_3_MONTHS": "Last 3 months",
|
||||
"LAST_6_MONTHS": "Last 6 months",
|
||||
@@ -266,6 +267,8 @@
|
||||
"NO_ENOUGH_DATA": "We've not received enough data points to generate report, Please try again later.",
|
||||
"DOWNLOAD_INBOX_REPORTS": "Download inbox reports",
|
||||
"FILTER_DROPDOWN_LABEL": "Select Inbox",
|
||||
"ALL_INBOXES": "All Inboxes",
|
||||
"SEARCH_INBOX": "Search Inbox",
|
||||
"METRICS": {
|
||||
"CONVERSATIONS": {
|
||||
"NAME": "Разговори",
|
||||
@@ -467,6 +470,13 @@
|
||||
"CONVERSATIONS": "{count} conversations",
|
||||
"DOWNLOAD_REPORT": "Download report"
|
||||
},
|
||||
"RESOLUTION_HEATMAP": {
|
||||
"HEADER": "Resolutions",
|
||||
"NO_CONVERSATIONS": "No conversations",
|
||||
"CONVERSATION": "{count} conversation",
|
||||
"CONVERSATIONS": "{count} conversations",
|
||||
"DOWNLOAD_REPORT": "Download report"
|
||||
},
|
||||
"AGENT_CONVERSATIONS": {
|
||||
"HEADER": "Conversations by agents",
|
||||
"LOADING_MESSAGE": "Loading agent metrics...",
|
||||
|
||||
@@ -304,6 +304,7 @@
|
||||
"CAPTAIN_ASSISTANTS": "Assistants",
|
||||
"CAPTAIN_DOCUMENTS": "Documents",
|
||||
"CAPTAIN_RESPONSES": "FAQs",
|
||||
"CAPTAIN_TOOLS": "Tools",
|
||||
"HOME": "Home",
|
||||
"AGENTS": "Агенти",
|
||||
"AGENT_BOTS": "Bots",
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
{
|
||||
"FILTER": {
|
||||
"TITLE": "Filter conversations",
|
||||
"SUBTITLE": "Add your filters below and hit 'Apply filters' to cut through the chat clutter.",
|
||||
"EDIT_CUSTOM_FILTER": "Edit Folder",
|
||||
"CUSTOM_VIEWS_SUBTITLE": "Add or remove filters and update your folder.",
|
||||
"ADD_NEW_FILTER": "Add filter",
|
||||
"FILTER_DELETE_ERROR": "Oops, looks like we can't save nothing! Please add at least one filter to save it.",
|
||||
"SUBMIT_BUTTON_LABEL": "Apply filters",
|
||||
"UPDATE_BUTTON_LABEL": "Update folder",
|
||||
"CANCEL_BUTTON_LABEL": "Cancel",
|
||||
"CLEAR_BUTTON_LABEL": "Clear filters",
|
||||
"FOLDER_LABEL": "Folder Name",
|
||||
"FOLDER_QUERY_LABEL": "Folder Query",
|
||||
"EMPTY_VALUE_ERROR": "Value is required.",
|
||||
"TOOLTIP_LABEL": "Filter conversations",
|
||||
"QUERY_DROPDOWN_LABELS": {
|
||||
"AND": "AND",
|
||||
"OR": "OR"
|
||||
},
|
||||
"INPUT_PLACEHOLDER": "Enter value",
|
||||
"OPERATOR_LABELS": {
|
||||
"equal_to": "Equal to",
|
||||
"not_equal_to": "Not equal to",
|
||||
"does_not_contain": "Does not contain",
|
||||
"is_present": "Is present",
|
||||
"is_not_present": "Is not present",
|
||||
"is_greater_than": "Is greater than",
|
||||
"is_less_than": "Is lesser than",
|
||||
"days_before": "Is x days before",
|
||||
"starts_with": "Starts with",
|
||||
"equalTo": "Equal to",
|
||||
"notEqualTo": "Not equal to",
|
||||
"contains": "Contains",
|
||||
"doesNotContain": "Does not contain",
|
||||
"isPresent": "Is present",
|
||||
"isNotPresent": "Is not present",
|
||||
"isGreaterThan": "Is greater than",
|
||||
"isLessThan": "Is lesser than",
|
||||
"daysBefore": "Is x days before",
|
||||
"startsWith": "Starts with"
|
||||
},
|
||||
"ATTRIBUTE_LABELS": {
|
||||
"TRUE": "True",
|
||||
"FALSE": "False"
|
||||
},
|
||||
"ATTRIBUTES": {
|
||||
"STATUS": "Status",
|
||||
"ASSIGNEE_NAME": "Assignee name",
|
||||
"INBOX_NAME": "Inbox name",
|
||||
"TEAM_NAME": "Team name",
|
||||
"CONVERSATION_IDENTIFIER": "Conversation identifier",
|
||||
"CAMPAIGN_NAME": "Campaign name",
|
||||
"LABELS": "Labels",
|
||||
"BROWSER_LANGUAGE": "Browser language",
|
||||
"PRIORITY": "Priority",
|
||||
"COUNTRY_NAME": "Country name",
|
||||
"REFERER_LINK": "Referer link",
|
||||
"CUSTOM_ATTRIBUTE_LIST": "List",
|
||||
"CUSTOM_ATTRIBUTE_TEXT": "Text",
|
||||
"CUSTOM_ATTRIBUTE_NUMBER": "Number",
|
||||
"CUSTOM_ATTRIBUTE_LINK": "Link",
|
||||
"CUSTOM_ATTRIBUTE_CHECKBOX": "Checkbox",
|
||||
"CREATED_AT": "Created at",
|
||||
"LAST_ACTIVITY": "Last activity"
|
||||
},
|
||||
"ERRORS": {
|
||||
"VALUE_REQUIRED": "Value is required",
|
||||
"ATTRIBUTE_KEY_REQUIRED": "Attribute key is required",
|
||||
"FILTER_OPERATOR_REQUIRED": "Filter operator is required",
|
||||
"VALUE_MUST_BE_BETWEEN_1_AND_998": "Value must be between 1 and 998"
|
||||
},
|
||||
"GROUPS": {
|
||||
"STANDARD_FILTERS": "Standard filters",
|
||||
"ADDITIONAL_FILTERS": "Additional filters",
|
||||
"CUSTOM_ATTRIBUTES": "Custom attributes"
|
||||
},
|
||||
"CUSTOM_VIEWS": {
|
||||
"ADD": {
|
||||
"TITLE": "Do you want to save this filter?",
|
||||
"LABEL": "Name this filter",
|
||||
"PLACEHOLDER": "Name your filter to refer it later.",
|
||||
"ERROR_MESSAGE": "Name is required.",
|
||||
"SAVE_BUTTON": "Save filter",
|
||||
"CANCEL_BUTTON": "Cancel",
|
||||
"API_FOLDERS": {
|
||||
"SUCCESS_MESSAGE": "Folder created successfully.",
|
||||
"ERROR_MESSAGE": "Error while creating folder."
|
||||
},
|
||||
"API_SEGMENTS": {
|
||||
"SUCCESS_MESSAGE": "Segment created successfully.",
|
||||
"ERROR_MESSAGE": "Error while creating segment."
|
||||
}
|
||||
},
|
||||
"EDIT": {
|
||||
"EDIT_BUTTON": "Edit folder"
|
||||
},
|
||||
"DELETE": {
|
||||
"DELETE_BUTTON": "Delete filter",
|
||||
"MODAL": {
|
||||
"CONFIRM": {
|
||||
"TITLE": "Confirm deletion",
|
||||
"MESSAGE": "Are you sure to delete the filter ",
|
||||
"YES": "Yes, delete",
|
||||
"NO": "No, keep it"
|
||||
}
|
||||
},
|
||||
"API_FOLDERS": {
|
||||
"SUCCESS_MESSAGE": "Folder deleted successfully.",
|
||||
"ERROR_MESSAGE": "Error while deleting folder."
|
||||
},
|
||||
"API_SEGMENTS": {
|
||||
"SUCCESS_MESSAGE": "Segment deleted successfully.",
|
||||
"ERROR_MESSAGE": "Error while deleting segment."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user