Compare commits
78
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be68927a52 | ||
|
|
1fd77fdec2 | ||
|
|
992cf6fd4d | ||
|
|
b3ac1dcd03 | ||
|
|
80caf25962 | ||
|
|
bf035114fe | ||
|
|
990b58fe62 | ||
|
|
52271dc984 | ||
|
|
52d6f2a369 | ||
|
|
a35c3e4c06 | ||
|
|
344e8d5016 | ||
|
|
3e27e28848 | ||
|
|
38af08534c | ||
|
|
ee1ea9576b | ||
|
|
26ea87a6cb | ||
|
|
7e8fe78ecd | ||
|
|
f3176afc1c | ||
|
|
c4ba925ae7 | ||
|
|
1f0b56b96e | ||
|
|
e7177364d4 | ||
|
|
f726dc2419 | ||
|
|
5891fd6f49 | ||
|
|
b9864fe1f6 | ||
|
|
5f97a2fd7b | ||
|
|
74fc9c0321 | ||
|
|
9898ccee9e | ||
|
|
eabdfc8168 | ||
|
|
9bd5f15450 | ||
|
|
aea1585fa3 | ||
|
|
254d5dcf9a | ||
|
|
b4c4f328b2 | ||
|
|
a509ef826a | ||
|
|
2180edc14a | ||
|
|
de2a35b617 | ||
|
|
40577e6a50 | ||
|
|
368d7c4608 | ||
|
|
f1f1ce644c | ||
|
|
38f16ba677 | ||
|
|
e7b01d80b3 | ||
|
|
ec9a82a017 | ||
|
|
cdd3b73fc9 | ||
|
|
610495123e | ||
|
|
cb65d615ea | ||
|
|
d8da1f5bf3 | ||
|
|
6829328182 | ||
|
|
6cc69f444b | ||
|
|
f89ed56258 | ||
|
|
7c5bb343c6 | ||
|
|
170ea7691f | ||
|
|
606adffeeb | ||
|
|
e9c1c61fe4 | ||
|
|
78ebdbbbd8 | ||
|
|
978f4c431a | ||
|
|
4b2ebb8877 | ||
|
|
c4c1f3eb63 | ||
|
|
829142c808 | ||
|
|
3a71829b46 | ||
|
|
0974aea300 | ||
|
|
9fb0dfa4a7 | ||
|
|
8bbb8ba5a4 | ||
|
|
a8aefa0c73 | ||
|
|
b536f35fa7 | ||
|
|
0d721bc898 | ||
|
|
c29a08f0ca | ||
|
|
66cfef9298 | ||
|
|
109aaa2341 | ||
|
|
ecff66146a | ||
|
|
21366e1c3b | ||
|
|
406a470c81 | ||
|
|
6c6aaf573c | ||
|
|
487209574c | ||
|
|
7f1671c083 | ||
|
|
b00261d7c2 | ||
|
|
fcb91ab88a | ||
|
|
59f7c8aa55 | ||
|
|
cd2c58726f | ||
|
|
03d0688cc2 | ||
|
|
b75ea7a762 |
@@ -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
|
||||
|
||||
+2
-2
@@ -644,7 +644,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)
|
||||
@@ -935,7 +935,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)
|
||||
|
||||
@@ -7,6 +7,7 @@ class Messages::MessageBuilder
|
||||
@private = params[:private] || false
|
||||
@conversation = conversation
|
||||
@user = user
|
||||
@account = conversation.account
|
||||
@message_type = params[:message_type] || 'outgoing'
|
||||
@attachments = params[:attachments]
|
||||
@automation_rule = content_attributes&.dig(:automation_rule_id)
|
||||
@@ -20,6 +21,9 @@ class Messages::MessageBuilder
|
||||
@message = @conversation.messages.build(message_params)
|
||||
process_attachments
|
||||
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)
|
||||
@message.save!
|
||||
@message
|
||||
end
|
||||
@@ -92,6 +96,14 @@ class Messages::MessageBuilder
|
||||
@message.content_attributes[:to_emails] = to_emails
|
||||
end
|
||||
|
||||
def process_email_content
|
||||
return unless should_process_email_content?
|
||||
|
||||
@message.content_attributes ||= {}
|
||||
email_attributes = build_email_attributes
|
||||
@message.content_attributes[:email] = email_attributes
|
||||
end
|
||||
|
||||
def process_email_string(email_string)
|
||||
return [] if email_string.blank?
|
||||
|
||||
@@ -153,4 +165,71 @@ class Messages::MessageBuilder
|
||||
source_id: @params[:source_id]
|
||||
}.merge(external_created_at).merge(automation_rule_id).merge(campaign_id).merge(template_params)
|
||||
end
|
||||
|
||||
def email_inbox?
|
||||
@conversation.inbox&.inbox_type == 'Email'
|
||||
end
|
||||
|
||||
def should_process_email_content?
|
||||
email_inbox? && !@private && @message.content.present?
|
||||
end
|
||||
|
||||
def build_email_attributes
|
||||
email_attributes = ensure_indifferent_access(@message.content_attributes[:email] || {})
|
||||
normalized_content = normalize_email_body(@message.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
|
||||
|
||||
def build_html_content(normalized_content)
|
||||
html_content = ensure_indifferent_access(@message.content_attributes.dig(:email, :html_content) || {})
|
||||
rendered_html = render_email_html(normalized_content)
|
||||
html_content[:full] = rendered_html
|
||||
html_content[:reply] = rendered_html
|
||||
html_content
|
||||
end
|
||||
|
||||
def build_text_content(normalized_content)
|
||||
text_content = ensure_indifferent_access(@message.content_attributes.dig(:email, :text_content) || {})
|
||||
text_content[:full] = normalized_content
|
||||
text_content[:reply] = normalized_content
|
||||
text_content
|
||||
end
|
||||
|
||||
def ensure_indifferent_access(hash)
|
||||
return {} if hash.blank?
|
||||
|
||||
hash.respond_to?(:with_indifferent_access) ? hash.with_indifferent_access : hash
|
||||
end
|
||||
|
||||
def normalize_email_body(content)
|
||||
content.to_s.gsub("\r\n", "\n")
|
||||
end
|
||||
|
||||
def render_email_html(content)
|
||||
return '' if content.blank?
|
||||
|
||||
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
|
||||
|
||||
@@ -17,8 +17,8 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||
before_action :set_include_contact_inboxes, only: [:index, :active, :search, :filter, :show, :update]
|
||||
|
||||
def index
|
||||
@contacts_count = resolved_contacts.count
|
||||
@contacts = fetch_contacts(resolved_contacts)
|
||||
@contacts_count = @contacts.total_count
|
||||
end
|
||||
|
||||
def search
|
||||
@@ -29,8 +29,8 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||
OR contacts.additional_attributes->>\'company_name\' ILIKE :search',
|
||||
search: "%#{params[:q].strip}%"
|
||||
)
|
||||
@contacts_count = contacts.count
|
||||
@contacts = fetch_contacts(contacts)
|
||||
@contacts_count = @contacts.total_count
|
||||
end
|
||||
|
||||
def import
|
||||
@@ -55,8 +55,8 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||
def active
|
||||
contacts = Current.account.contacts.where(id: ::OnlineStatusTracker
|
||||
.get_available_contact_ids(Current.account.id))
|
||||
@contacts_count = contacts.count
|
||||
@contacts = fetch_contacts(contacts)
|
||||
@contacts_count = @contacts.total_count
|
||||
end
|
||||
|
||||
def show; end
|
||||
@@ -133,13 +133,14 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||
end
|
||||
|
||||
def fetch_contacts(contacts)
|
||||
contacts_with_avatar = filtrate(contacts)
|
||||
.includes([{ avatar_attachment: [:blob] }])
|
||||
.page(@current_page).per(RESULTS_PER_PAGE)
|
||||
# Build includes hash to avoid separate query when contact_inboxes are needed
|
||||
includes_hash = { avatar_attachment: [:blob] }
|
||||
includes_hash[:contact_inboxes] = { inbox: :channel } if @include_contact_inboxes
|
||||
|
||||
return contacts_with_avatar.includes([{ contact_inboxes: [:inbox] }]) if @include_contact_inboxes
|
||||
|
||||
contacts_with_avatar
|
||||
filtrate(contacts)
|
||||
.includes(includes_hash)
|
||||
.page(@current_page)
|
||||
.per(RESULTS_PER_PAGE)
|
||||
end
|
||||
|
||||
def build_contact_inbox
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -4,7 +4,8 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||
before_action :fetch_agent_bot, only: [:set_agent_bot]
|
||||
before_action :validate_limit, only: [:create]
|
||||
# we are already handling the authorization in fetch inbox
|
||||
before_action :check_authorization, except: [:show]
|
||||
before_action :check_authorization, except: [:show, :health]
|
||||
before_action :validate_whatsapp_cloud_channel, only: [:health]
|
||||
|
||||
def index
|
||||
@inboxes = policy_scope(Current.account.inboxes.order_by_name.includes(:channel, { avatar_attachment: [:blob] }))
|
||||
@@ -78,6 +79,14 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||
render status: :internal_server_error, json: { error: e.message }
|
||||
end
|
||||
|
||||
def health
|
||||
health_data = Whatsapp::HealthService.new(@inbox.channel).fetch_health_status
|
||||
render json: health_data
|
||||
rescue StandardError => e
|
||||
Rails.logger.error "[INBOX HEALTH] Error fetching health data: #{e.message}"
|
||||
render json: { error: e.message }, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def fetch_inbox
|
||||
@@ -89,6 +98,12 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||
@agent_bot = AgentBot.find(params[:agent_bot]) if params[:agent_bot]
|
||||
end
|
||||
|
||||
def validate_whatsapp_cloud_channel
|
||||
return if @inbox.channel.is_a?(Channel::Whatsapp) && @inbox.channel.provider == 'whatsapp_cloud'
|
||||
|
||||
render json: { error: 'Health data only available for WhatsApp Cloud API channels' }, status: :bad_request
|
||||
end
|
||||
|
||||
def create_channel
|
||||
return unless allowed_channel_types.include?(permitted_params[:channel][:type])
|
||||
|
||||
|
||||
@@ -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();
|
||||
@@ -0,0 +1,14 @@
|
||||
/* global axios */
|
||||
import ApiClient from './ApiClient';
|
||||
|
||||
class InboxHealthAPI extends ApiClient {
|
||||
constructor() {
|
||||
super('inboxes', { accountScoped: true });
|
||||
}
|
||||
|
||||
getHealthStatus(inboxId) {
|
||||
return axios.get(`${this.url}/${inboxId}/health`);
|
||||
}
|
||||
}
|
||||
|
||||
export default new InboxHealthAPI();
|
||||
+14
-3
@@ -148,10 +148,21 @@ const isAnyDropdownActive = computed(() => {
|
||||
|
||||
const handleContactSearch = value => {
|
||||
showContactsDropdown.value = true;
|
||||
emit('searchContacts', {
|
||||
keys: ['email', 'phone_number', 'name'],
|
||||
query: value,
|
||||
const query = typeof value === 'string' ? value.trim() : '';
|
||||
const hasAlphabet = Array.from(query).some(char => {
|
||||
const lower = char.toLowerCase();
|
||||
const upper = char.toUpperCase();
|
||||
return lower !== upper;
|
||||
});
|
||||
const isEmailLike = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(query);
|
||||
|
||||
const keys = ['email', 'phone_number', 'name'].filter(key => {
|
||||
if (key === 'phone_number' && hasAlphabet) return false;
|
||||
if (key === 'name' && isEmailLike) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
emit('searchContacts', { keys, query: value });
|
||||
};
|
||||
|
||||
const handleDropdownUpdate = (type, value) => {
|
||||
|
||||
@@ -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
|
||||
|
||||
+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 ResponseCard from 'dashboard/components-next/captain/assistant/ResponseCard.vue';
|
||||
@@ -6,6 +7,7 @@ import FeatureSpotlight from 'dashboard/components-next/feature-spotlight/Featur
|
||||
import { responsesList } 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/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
|
||||
|
||||
+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>
|
||||
@@ -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"
|
||||
|
||||
@@ -5,9 +5,9 @@ import { sanitizeTextForRender } from '@chatwoot/utils';
|
||||
import { allowedCssProperties } from 'lettersanitizer';
|
||||
|
||||
import Icon from 'next/icon/Icon.vue';
|
||||
import { EmailQuoteExtractor } from './removeReply.js';
|
||||
import BaseBubble from 'next/message/bubbles/Base.vue';
|
||||
import { EmailQuoteExtractor } from 'dashboard/helper/emailQuoteExtractor.js';
|
||||
import FormattedContent from 'next/message/bubbles/Text/FormattedContent.vue';
|
||||
import BaseBubble from 'next/message/bubbles/Base.vue';
|
||||
import AttachmentChips from 'next/message/chips/AttachmentChips.vue';
|
||||
import EmailMeta from './EmailMeta.vue';
|
||||
import TranslationToggle from 'dashboard/components-next/message/TranslationToggle.vue';
|
||||
@@ -47,6 +47,13 @@ const originalEmailHtml = computed(
|
||||
originalEmailText.value
|
||||
);
|
||||
|
||||
const hasEmailContent = computed(() => {
|
||||
return (
|
||||
contentAttributes?.value?.email?.textContent?.full ||
|
||||
contentAttributes?.value?.email?.htmlContent?.full
|
||||
);
|
||||
});
|
||||
|
||||
const messageContent = computed(() => {
|
||||
// If translations exist and we're showing translations (not original)
|
||||
if (hasTranslations.value && !renderOriginal.value) {
|
||||
@@ -137,7 +144,7 @@ const handleSeeOriginal = () => {
|
||||
</button>
|
||||
</div>
|
||||
<FormattedContent
|
||||
v-if="isOutgoing && content"
|
||||
v-if="isOutgoing && content && !hasEmailContent"
|
||||
class="text-n-slate-12"
|
||||
:content="messageContent"
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
+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>
|
||||
@@ -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>
|
||||
@@ -118,6 +118,14 @@ export default {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
showQuotedReplyToggle: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
quotedReplyEnabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: [
|
||||
'replaceText',
|
||||
@@ -125,6 +133,7 @@ export default {
|
||||
'toggleEditor',
|
||||
'selectWhatsappTemplate',
|
||||
'selectContentTemplate',
|
||||
'toggleQuotedReply',
|
||||
],
|
||||
setup() {
|
||||
const { setSignatureFlagForInbox, fetchSignatureFlagFromUISettings } =
|
||||
@@ -249,6 +258,11 @@ export default {
|
||||
isFetchingAppIntegrations() {
|
||||
return this.uiFlags.isFetching;
|
||||
},
|
||||
quotedReplyToggleTooltip() {
|
||||
return this.quotedReplyEnabled
|
||||
? this.$t('CONVERSATION.REPLYBOX.QUOTED_REPLY.DISABLE_TOOLTIP')
|
||||
: this.$t('CONVERSATION.REPLYBOX.QUOTED_REPLY.ENABLE_TOOLTIP');
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
ActiveStorage.start();
|
||||
@@ -339,6 +353,16 @@ export default {
|
||||
sm
|
||||
@click="toggleMessageSignature"
|
||||
/>
|
||||
<NextButton
|
||||
v-if="showQuotedReplyToggle"
|
||||
v-tooltip.top-end="quotedReplyToggleTooltip"
|
||||
icon="i-ph-quotes"
|
||||
:variant="quotedReplyEnabled ? 'solid' : 'faded'"
|
||||
color="slate"
|
||||
sm
|
||||
:aria-pressed="quotedReplyEnabled"
|
||||
@click="$emit('toggleQuotedReply')"
|
||||
/>
|
||||
<NextButton
|
||||
v-if="enableWhatsAppTemplates"
|
||||
v-tooltip.top-end="$t('CONVERSATION.FOOTER.WHATSAPP_TEMPLATES')"
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
const props = defineProps({
|
||||
quotedEmailText: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
previewText: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['toggle']);
|
||||
|
||||
const { t } = useI18n();
|
||||
const { formatMessage } = useMessageFormatter();
|
||||
|
||||
const isExpanded = ref(false);
|
||||
|
||||
const formattedQuotedEmailText = computed(() => {
|
||||
if (!props.quotedEmailText) {
|
||||
return '';
|
||||
}
|
||||
return formatMessage(props.quotedEmailText, false, false, true);
|
||||
});
|
||||
|
||||
const toggleExpand = () => {
|
||||
isExpanded.value = !isExpanded.value;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mt-2">
|
||||
<div
|
||||
class="relative rounded-md px-3 py-2 text-xs text-n-slate-12 bg-n-slate-3 dark:bg-n-solid-3"
|
||||
>
|
||||
<div class="absolute top-2 right-2 z-10 flex items-center gap-1">
|
||||
<NextButton
|
||||
v-tooltip="
|
||||
isExpanded
|
||||
? t('CONVERSATION.REPLYBOX.QUOTED_REPLY.COLLAPSE')
|
||||
: t('CONVERSATION.REPLYBOX.QUOTED_REPLY.EXPAND')
|
||||
"
|
||||
ghost
|
||||
slate
|
||||
xs
|
||||
:icon="isExpanded ? 'i-lucide-minimize' : 'i-lucide-maximize'"
|
||||
@click="toggleExpand"
|
||||
/>
|
||||
<NextButton
|
||||
v-tooltip="t('CONVERSATION.REPLYBOX.QUOTED_REPLY.REMOVE_PREVIEW')"
|
||||
ghost
|
||||
slate
|
||||
xs
|
||||
icon="i-lucide-x"
|
||||
@click="emit('toggle')"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-dompurify-html="formattedQuotedEmailText"
|
||||
class="w-full max-w-none break-words prose prose-sm dark:prose-invert cursor-pointer ltr:pr-8 rtl:pl-8"
|
||||
:class="{
|
||||
'line-clamp-1': !isExpanded,
|
||||
'max-h-60 overflow-y-auto': isExpanded,
|
||||
}"
|
||||
:title="previewText"
|
||||
@click="toggleExpand"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -5,6 +5,7 @@ import { useAlert } from 'dashboard/composables';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import { useTrack } from 'dashboard/composables';
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||
|
||||
import CannedResponse from './CannedResponse.vue';
|
||||
import ReplyToMessage from './ReplyToMessage.vue';
|
||||
@@ -16,6 +17,7 @@ import ReplyBottomPanel from 'dashboard/components/widgets/WootWriter/ReplyBotto
|
||||
import ArticleSearchPopover from 'dashboard/routes/dashboard/helpcenter/components/ArticleSearch/SearchPopover.vue';
|
||||
import MessageSignatureMissingAlert from './MessageSignatureMissingAlert.vue';
|
||||
import ReplyBoxBanner from './ReplyBoxBanner.vue';
|
||||
import QuotedEmailPreview from './QuotedEmailPreview.vue';
|
||||
import { REPLY_EDITOR_MODES } from 'dashboard/components/widgets/WootWriter/constants';
|
||||
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
|
||||
import AudioRecorder from 'dashboard/components/widgets/WootWriter/AudioRecorder.vue';
|
||||
@@ -32,6 +34,12 @@ import { MESSAGE_MAX_LENGTH } from 'shared/helpers/MessageTypeHelper';
|
||||
import inboxMixin, { INBOX_FEATURES } from 'shared/mixins/inboxMixin';
|
||||
import { trimContent, debounce, getRecipients } from '@chatwoot/utils';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import {
|
||||
extractQuotedEmailText,
|
||||
buildQuotedEmailHeader,
|
||||
truncatePreviewText,
|
||||
appendQuotedTextToMessage,
|
||||
} from 'dashboard/helper/quotedEmailHelper';
|
||||
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
|
||||
import fileUploadMixin from 'dashboard/mixins/fileUploadMixin';
|
||||
import {
|
||||
@@ -65,6 +73,7 @@ export default {
|
||||
ContentTemplates,
|
||||
WhatsappTemplates,
|
||||
WootMessageEditor,
|
||||
QuotedEmailPreview,
|
||||
},
|
||||
mixins: [inboxMixin, fileUploadMixin, keyboardEventListenerMixins],
|
||||
props: {
|
||||
@@ -80,6 +89,8 @@ export default {
|
||||
updateUISettings,
|
||||
isEditorHotKeyEnabled,
|
||||
fetchSignatureFlagFromUISettings,
|
||||
setQuotedReplyFlagForInbox,
|
||||
fetchQuotedReplyFlagFromUISettings,
|
||||
} = useUISettings();
|
||||
|
||||
const replyEditor = useTemplateRef('replyEditor');
|
||||
@@ -89,6 +100,8 @@ export default {
|
||||
updateUISettings,
|
||||
isEditorHotKeyEnabled,
|
||||
fetchSignatureFlagFromUISettings,
|
||||
setQuotedReplyFlagForInbox,
|
||||
fetchQuotedReplyFlagFromUISettings,
|
||||
replyEditor,
|
||||
};
|
||||
},
|
||||
@@ -130,6 +143,8 @@ export default {
|
||||
currentUser: 'getCurrentUser',
|
||||
lastEmail: 'getLastEmailInSelectedChat',
|
||||
globalConfig: 'globalConfig/get',
|
||||
accountId: 'getCurrentAccountId',
|
||||
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
|
||||
}),
|
||||
currentContact() {
|
||||
return this.$store.getters['contacts/getContact'](
|
||||
@@ -367,6 +382,51 @@ export default {
|
||||
const { slug = '' } = portal;
|
||||
return slug;
|
||||
},
|
||||
isQuotedEmailReplyEnabled() {
|
||||
return this.isFeatureEnabledonAccount(
|
||||
this.accountId,
|
||||
FEATURE_FLAGS.QUOTED_EMAIL_REPLY
|
||||
);
|
||||
},
|
||||
quotedReplyPreference() {
|
||||
if (!this.isAnEmailChannel || !this.isQuotedEmailReplyEnabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !!this.fetchQuotedReplyFlagFromUISettings(this.channelType);
|
||||
},
|
||||
lastEmailWithQuotedContent() {
|
||||
if (!this.isAnEmailChannel) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const lastEmail = this.lastEmail;
|
||||
if (!lastEmail || lastEmail.private) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return lastEmail;
|
||||
},
|
||||
quotedEmailText() {
|
||||
return extractQuotedEmailText(this.lastEmailWithQuotedContent);
|
||||
},
|
||||
quotedEmailPreviewText() {
|
||||
return truncatePreviewText(this.quotedEmailText, 80);
|
||||
},
|
||||
shouldShowQuotedReplyToggle() {
|
||||
return (
|
||||
this.isAnEmailChannel &&
|
||||
!this.isOnPrivateNote &&
|
||||
this.isQuotedEmailReplyEnabled
|
||||
);
|
||||
},
|
||||
shouldShowQuotedPreview() {
|
||||
return (
|
||||
this.shouldShowQuotedReplyToggle &&
|
||||
this.quotedReplyPreference &&
|
||||
!!this.quotedEmailText
|
||||
);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
currentChat(conversation, oldConversation) {
|
||||
@@ -516,6 +576,36 @@ export default {
|
||||
);
|
||||
}
|
||||
},
|
||||
toggleQuotedReply() {
|
||||
if (!this.isAnEmailChannel) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextValue = !this.quotedReplyPreference;
|
||||
this.setQuotedReplyFlagForInbox(this.channelType, nextValue);
|
||||
},
|
||||
shouldIncludeQuotedEmail() {
|
||||
return (
|
||||
this.isQuotedEmailReplyEnabled &&
|
||||
this.quotedReplyPreference &&
|
||||
this.shouldShowQuotedReplyToggle &&
|
||||
!!this.quotedEmailText
|
||||
);
|
||||
},
|
||||
getMessageWithQuotedEmailText(message) {
|
||||
if (!this.shouldIncludeQuotedEmail()) {
|
||||
return message;
|
||||
}
|
||||
|
||||
const quotedText = this.quotedEmailText || '';
|
||||
const header = buildQuotedEmailHeader(
|
||||
this.lastEmailWithQuotedContent,
|
||||
this.currentContact,
|
||||
this.inbox
|
||||
);
|
||||
|
||||
return appendQuotedTextToMessage(message, quotedText, header);
|
||||
},
|
||||
resetRecorderAndClearAttachments() {
|
||||
// Reset audio recorder UI state
|
||||
this.resetAudioRecorderInput();
|
||||
@@ -965,9 +1055,11 @@ export default {
|
||||
return multipleMessagePayload;
|
||||
},
|
||||
getMessagePayload(message) {
|
||||
const messageWithQuote = this.getMessageWithQuotedEmailText(message);
|
||||
|
||||
let messagePayload = {
|
||||
conversationId: this.currentChat.id,
|
||||
message,
|
||||
message: messageWithQuote,
|
||||
private: this.isPrivate,
|
||||
sender: this.sender,
|
||||
};
|
||||
@@ -995,7 +1087,6 @@ export default {
|
||||
if (this.toEmails && !this.isOnPrivateNote) {
|
||||
messagePayload.toEmails = this.toEmails;
|
||||
}
|
||||
|
||||
return messagePayload;
|
||||
},
|
||||
setCcEmails(value) {
|
||||
@@ -1160,6 +1251,12 @@ export default {
|
||||
@toggle-variables-menu="toggleVariablesMenu"
|
||||
@clear-selection="clearEditorSelection"
|
||||
/>
|
||||
<QuotedEmailPreview
|
||||
v-if="shouldShowQuotedPreview"
|
||||
:quoted-email-text="quotedEmailText"
|
||||
:preview-text="quotedEmailPreviewText"
|
||||
@toggle="toggleQuotedReply"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="hasAttachments && !showAudioRecorderEditor"
|
||||
@@ -1195,6 +1292,8 @@ export default {
|
||||
:show-editor-toggle="isAPIInbox && !isOnPrivateNote"
|
||||
:show-emoji-picker="showEmojiPicker"
|
||||
:show-file-upload="showFileUpload"
|
||||
:show-quoted-reply-toggle="shouldShowQuotedReplyToggle"
|
||||
:quoted-reply-enabled="quotedReplyPreference"
|
||||
:toggle-audio-recorder-play-pause="toggleAudioRecorderPlayPause"
|
||||
:toggle-audio-recorder="toggleAudioRecorder"
|
||||
:toggle-emoji-picker="toggleEmojiPicker"
|
||||
@@ -1206,6 +1305,7 @@ export default {
|
||||
@toggle-editor="toggleRichContentEditor"
|
||||
@replace-text="replaceText"
|
||||
@toggle-insert-article="toggleInsertArticle"
|
||||
@toggle-quoted-reply="toggleQuotedReply"
|
||||
/>
|
||||
<WhatsappTemplates
|
||||
:inbox-id="inbox.id"
|
||||
|
||||
@@ -13,6 +13,7 @@ const getUISettingsMock = ref({
|
||||
conversation_sidebar_items_order: DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
|
||||
contact_sidebar_items_order: DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER,
|
||||
editor_message_key: 'enter',
|
||||
channel_email_quoted_reply_enabled: true,
|
||||
});
|
||||
|
||||
vi.mock('dashboard/composables/store', () => ({
|
||||
@@ -37,6 +38,7 @@ describe('useUISettings', () => {
|
||||
DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
|
||||
contact_sidebar_items_order: DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER,
|
||||
editor_message_key: 'enter',
|
||||
channel_email_quoted_reply_enabled: true,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -51,6 +53,7 @@ describe('useUISettings', () => {
|
||||
DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
|
||||
contact_sidebar_items_order: DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER,
|
||||
editor_message_key: 'enter',
|
||||
channel_email_quoted_reply_enabled: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -65,6 +68,7 @@ describe('useUISettings', () => {
|
||||
DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
|
||||
contact_sidebar_items_order: DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER,
|
||||
editor_message_key: 'enter',
|
||||
channel_email_quoted_reply_enabled: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -100,6 +104,7 @@ describe('useUISettings', () => {
|
||||
contact_sidebar_items_order: DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER,
|
||||
email_signature_enabled: true,
|
||||
editor_message_key: 'enter',
|
||||
channel_email_quoted_reply_enabled: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -109,6 +114,26 @@ describe('useUISettings', () => {
|
||||
expect(fetchSignatureFlagFromUISettings('email')).toBe(undefined);
|
||||
});
|
||||
|
||||
it('sets quoted reply flag for inbox correctly', () => {
|
||||
const { setQuotedReplyFlagForInbox } = useUISettings();
|
||||
setQuotedReplyFlagForInbox('Channel::Email', false);
|
||||
expect(mockDispatch).toHaveBeenCalledWith('updateUISettings', {
|
||||
uiSettings: {
|
||||
is_ct_labels_open: true,
|
||||
conversation_sidebar_items_order:
|
||||
DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
|
||||
contact_sidebar_items_order: DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER,
|
||||
editor_message_key: 'enter',
|
||||
channel_email_quoted_reply_enabled: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('fetches quoted reply flag from UI settings correctly', () => {
|
||||
const { fetchQuotedReplyFlagFromUISettings } = useUISettings();
|
||||
expect(fetchQuotedReplyFlagFromUISettings('Channel::Email')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns correct value for isEditorHotKeyEnabled when editor_message_key is configured', () => {
|
||||
getUISettingsMock.value.enter_to_send_enabled = false;
|
||||
const { isEditorHotKeyEnabled } = useUISettings();
|
||||
|
||||
@@ -87,6 +87,13 @@ const setSignatureFlagForInbox = (channelType, value, updateUISettings) => {
|
||||
updateUISettings({ [`${slugifiedChannel}_signature_enabled`]: value });
|
||||
};
|
||||
|
||||
const setQuotedReplyFlagForInbox = (channelType, value, updateUISettings) => {
|
||||
if (!channelType) return;
|
||||
|
||||
const slugifiedChannel = slugifyChannel(channelType);
|
||||
updateUISettings({ [`${slugifiedChannel}_quoted_reply_enabled`]: value });
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches the signature flag for a specific channel type from UI settings.
|
||||
* @param {string} channelType - The type of the channel.
|
||||
@@ -100,6 +107,13 @@ const fetchSignatureFlagFromUISettings = (channelType, uiSettings) => {
|
||||
return uiSettings.value[`${slugifiedChannel}_signature_enabled`];
|
||||
};
|
||||
|
||||
const fetchQuotedReplyFlagFromUISettings = (channelType, uiSettings) => {
|
||||
if (!channelType) return false;
|
||||
|
||||
const slugifiedChannel = slugifyChannel(channelType);
|
||||
return uiSettings.value[`${slugifiedChannel}_quoted_reply_enabled`];
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if a specific editor hotkey is enabled.
|
||||
* @param {string} key - The key to check.
|
||||
@@ -147,6 +161,10 @@ export function useUISettings() {
|
||||
setSignatureFlagForInbox(channelType, value, updateUISettings),
|
||||
fetchSignatureFlagFromUISettings: channelType =>
|
||||
fetchSignatureFlagFromUISettings(channelType, uiSettings),
|
||||
setQuotedReplyFlagForInbox: (channelType, value) =>
|
||||
setQuotedReplyFlagForInbox(channelType, value, updateUISettings),
|
||||
fetchQuotedReplyFlagFromUISettings: channelType =>
|
||||
fetchQuotedReplyFlagFromUISettings(channelType, uiSettings),
|
||||
isEditorHotKeyEnabled: key => isEditorHotKeyEnabled(key, uiSettings),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ export const FEATURE_FLAGS = {
|
||||
CONTACT_CHATWOOT_SUPPORT_TEAM: 'contact_chatwoot_support_team',
|
||||
CAPTAIN_V2: 'captain_integration_v2',
|
||||
SAML: 'saml',
|
||||
QUOTED_EMAIL_REPLY: 'quoted_email_reply',
|
||||
};
|
||||
|
||||
export const PREMIUM_FEATURES = [
|
||||
@@ -48,6 +49,5 @@ export const PREMIUM_FEATURES = [
|
||||
FEATURE_FLAGS.CUSTOM_ROLES,
|
||||
FEATURE_FLAGS.AUDIT_LOGS,
|
||||
FEATURE_FLAGS.HELP_CENTER,
|
||||
FEATURE_FLAGS.CAPTAIN_V2,
|
||||
FEATURE_FLAGS.SAML,
|
||||
];
|
||||
|
||||
+30
@@ -10,6 +10,8 @@ const QUOTE_INDICATORS = [
|
||||
'[class*="Quote"]',
|
||||
];
|
||||
|
||||
const BLOCKQUOTE_FALLBACK_SELECTOR = 'blockquote';
|
||||
|
||||
// Regex patterns for quote identification
|
||||
const QUOTE_PATTERNS = [
|
||||
/On .* wrote:/i,
|
||||
@@ -36,6 +38,8 @@ export class EmailQuoteExtractor {
|
||||
});
|
||||
});
|
||||
|
||||
this.removeTrailingBlockquote(tempDiv);
|
||||
|
||||
// Remove text-based quotes
|
||||
const textNodeQuotes = this.findTextNodeQuotes(tempDiv);
|
||||
textNodeQuotes.forEach(el => {
|
||||
@@ -62,6 +66,10 @@ export class EmailQuoteExtractor {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.findTrailingBlockquote(tempDiv)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for text-based quotes
|
||||
const textNodeQuotes = this.findTextNodeQuotes(tempDiv);
|
||||
return textNodeQuotes.length > 0;
|
||||
@@ -123,4 +131,26 @@ export class EmailQuoteExtractor {
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove fallback blockquote if it is the last top-level element.
|
||||
* @param {Element} rootElement - Root element containing the HTML
|
||||
*/
|
||||
static removeTrailingBlockquote(rootElement) {
|
||||
const trailingBlockquote = this.findTrailingBlockquote(rootElement);
|
||||
trailingBlockquote?.remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* Locate a fallback blockquote that is the last top-level element.
|
||||
* @param {Element} rootElement - Root element containing the HTML
|
||||
* @returns {Element|null} The trailing blockquote element if present
|
||||
*/
|
||||
static findTrailingBlockquote(rootElement) {
|
||||
const lastElement = rootElement.lastElementChild;
|
||||
if (lastElement?.matches?.(BLOCKQUOTE_FALLBACK_SELECTOR)) {
|
||||
return lastElement;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -68,13 +68,17 @@ export const registerSubscription = (onSuccess = () => {}) => {
|
||||
.then(() => {
|
||||
onSuccess();
|
||||
})
|
||||
.catch(() => {
|
||||
.catch(error => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Push subscription registration failed:', error);
|
||||
useAlert('This browser does not support desktop notification');
|
||||
});
|
||||
};
|
||||
|
||||
export const requestPushPermissions = ({ onSuccess }) => {
|
||||
if (!('Notification' in window)) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('Notification is not supported');
|
||||
useAlert('This browser does not support desktop notification');
|
||||
} else if (Notification.permission === 'granted') {
|
||||
registerSubscription(onSuccess);
|
||||
|
||||
@@ -0,0 +1,332 @@
|
||||
import { format, parseISO, isValid as isValidDate } from 'date-fns';
|
||||
|
||||
/**
|
||||
* Extracts plain text from HTML content
|
||||
* @param {string} html - HTML content to convert
|
||||
* @returns {string} Plain text content
|
||||
*/
|
||||
export const extractPlainTextFromHtml = html => {
|
||||
if (!html) {
|
||||
return '';
|
||||
}
|
||||
if (typeof document === 'undefined') {
|
||||
return html.replace(/<[^>]*>/g, ' ');
|
||||
}
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = html;
|
||||
return tempDiv.textContent || tempDiv.innerText || '';
|
||||
};
|
||||
|
||||
/**
|
||||
* Extracts sender name from email message
|
||||
* @param {Object} lastEmail - Last email message object
|
||||
* @param {Object} contact - Contact object
|
||||
* @returns {string} Sender name
|
||||
*/
|
||||
export const getEmailSenderName = (lastEmail, contact) => {
|
||||
const senderName = lastEmail?.sender?.name;
|
||||
if (senderName && senderName.trim()) {
|
||||
return senderName.trim();
|
||||
}
|
||||
|
||||
const contactName = contact?.name;
|
||||
return contactName && contactName.trim() ? contactName.trim() : '';
|
||||
};
|
||||
|
||||
/**
|
||||
* Extracts sender email from email message
|
||||
* @param {Object} lastEmail - Last email message object
|
||||
* @param {Object} contact - Contact object
|
||||
* @returns {string} Sender email address
|
||||
*/
|
||||
export const getEmailSenderEmail = (lastEmail, contact) => {
|
||||
const senderEmail = lastEmail?.sender?.email;
|
||||
if (senderEmail && senderEmail.trim()) {
|
||||
return senderEmail.trim();
|
||||
}
|
||||
|
||||
const contentAttributes =
|
||||
lastEmail?.contentAttributes || lastEmail?.content_attributes || {};
|
||||
const emailMeta = contentAttributes.email || {};
|
||||
|
||||
if (Array.isArray(emailMeta.from) && emailMeta.from.length > 0) {
|
||||
const fromAddress = emailMeta.from[0];
|
||||
if (fromAddress && fromAddress.trim()) {
|
||||
return fromAddress.trim();
|
||||
}
|
||||
}
|
||||
|
||||
const contactEmail = contact?.email;
|
||||
return contactEmail && contactEmail.trim() ? contactEmail.trim() : '';
|
||||
};
|
||||
|
||||
/**
|
||||
* Extracts date from email message
|
||||
* @param {Object} lastEmail - Last email message object
|
||||
* @returns {Date|null} Email date
|
||||
*/
|
||||
export const getEmailDate = lastEmail => {
|
||||
const contentAttributes =
|
||||
lastEmail?.contentAttributes || lastEmail?.content_attributes || {};
|
||||
const emailMeta = contentAttributes.email || {};
|
||||
|
||||
if (emailMeta.date) {
|
||||
const parsedDate = parseISO(emailMeta.date);
|
||||
if (isValidDate(parsedDate)) {
|
||||
return parsedDate;
|
||||
}
|
||||
}
|
||||
|
||||
const createdAt = lastEmail?.created_at;
|
||||
if (createdAt) {
|
||||
const timestamp = Number(createdAt);
|
||||
if (!Number.isNaN(timestamp)) {
|
||||
const milliseconds = timestamp > 1e12 ? timestamp : timestamp * 1000;
|
||||
const derivedDate = new Date(milliseconds);
|
||||
if (!Number.isNaN(derivedDate.getTime())) {
|
||||
return derivedDate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats date for quoted email header
|
||||
* @param {Date} date - Date to format
|
||||
* @returns {string} Formatted date string
|
||||
*/
|
||||
export const formatQuotedEmailDate = date => {
|
||||
try {
|
||||
return format(date, "EEE, MMM d, yyyy 'at' p");
|
||||
} catch (error) {
|
||||
const fallbackDate = new Date(date);
|
||||
if (!Number.isNaN(fallbackDate.getTime())) {
|
||||
return format(fallbackDate, "EEE, MMM d, yyyy 'at' p");
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
/**
|
||||
* Extracts inbox email address from last email message
|
||||
* @param {Object} lastEmail - Last email message object
|
||||
* @param {Object} inbox - Inbox object
|
||||
* @returns {string} Inbox email address
|
||||
*/
|
||||
export const getInboxEmail = (lastEmail, inbox) => {
|
||||
const contentAttributes =
|
||||
lastEmail?.contentAttributes || lastEmail?.content_attributes || {};
|
||||
const emailMeta = contentAttributes.email || {};
|
||||
|
||||
if (Array.isArray(emailMeta.to) && emailMeta.to.length > 0) {
|
||||
const toAddress = emailMeta.to[0];
|
||||
if (toAddress && toAddress.trim()) {
|
||||
return toAddress.trim();
|
||||
}
|
||||
}
|
||||
|
||||
const inboxEmail = inbox?.email;
|
||||
return inboxEmail && inboxEmail.trim() ? inboxEmail.trim() : '';
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds quoted email header from contact (for incoming messages)
|
||||
* @param {Object} lastEmail - Last email message object
|
||||
* @param {Object} contact - Contact object
|
||||
* @returns {string} Formatted header string
|
||||
*/
|
||||
export const buildQuotedEmailHeaderFromContact = (lastEmail, contact) => {
|
||||
if (!lastEmail) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const quotedDate = getEmailDate(lastEmail);
|
||||
const senderEmail = getEmailSenderEmail(lastEmail, contact);
|
||||
|
||||
if (!quotedDate || !senderEmail) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const formattedDate = formatQuotedEmailDate(quotedDate);
|
||||
if (!formattedDate) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const senderName = getEmailSenderName(lastEmail, contact);
|
||||
const hasName = !!senderName;
|
||||
const contactLabel = hasName
|
||||
? `${senderName} <${senderEmail}>`
|
||||
: `<${senderEmail}>`;
|
||||
|
||||
return `On ${formattedDate} ${contactLabel} wrote:`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds quoted email header from inbox (for outgoing messages)
|
||||
* @param {Object} lastEmail - Last email message object
|
||||
* @param {Object} inbox - Inbox object
|
||||
* @returns {string} Formatted header string
|
||||
*/
|
||||
export const buildQuotedEmailHeaderFromInbox = (lastEmail, inbox) => {
|
||||
if (!lastEmail) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const quotedDate = getEmailDate(lastEmail);
|
||||
const inboxEmail = getInboxEmail(lastEmail, inbox);
|
||||
|
||||
if (!quotedDate || !inboxEmail) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const formattedDate = formatQuotedEmailDate(quotedDate);
|
||||
if (!formattedDate) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const inboxName = inbox?.name;
|
||||
const hasName = !!inboxName;
|
||||
const inboxLabel = hasName
|
||||
? `${inboxName} <${inboxEmail}>`
|
||||
: `<${inboxEmail}>`;
|
||||
|
||||
return `On ${formattedDate} ${inboxLabel} wrote:`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds quoted email header based on message type
|
||||
* @param {Object} lastEmail - Last email message object
|
||||
* @param {Object} contact - Contact object
|
||||
* @param {Object} inbox - Inbox object
|
||||
* @returns {string} Formatted header string
|
||||
*/
|
||||
export const buildQuotedEmailHeader = (lastEmail, contact, inbox) => {
|
||||
if (!lastEmail) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// MESSAGE_TYPE.OUTGOING = 1, MESSAGE_TYPE.INCOMING = 0
|
||||
const isOutgoing = lastEmail.message_type === 1;
|
||||
|
||||
if (isOutgoing) {
|
||||
return buildQuotedEmailHeaderFromInbox(lastEmail, inbox);
|
||||
}
|
||||
|
||||
return buildQuotedEmailHeaderFromContact(lastEmail, contact);
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats text as markdown blockquote
|
||||
* @param {string} text - Text to format
|
||||
* @param {string} header - Optional header to prepend
|
||||
* @returns {string} Formatted blockquote
|
||||
*/
|
||||
export const formatQuotedTextAsBlockquote = (text, header = '') => {
|
||||
const normalizedLines = text
|
||||
? String(text).replace(/\r\n/g, '\n').split('\n')
|
||||
: [];
|
||||
|
||||
if (!header && !normalizedLines.length) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const quotedLines = [];
|
||||
|
||||
if (header) {
|
||||
quotedLines.push(`> ${header}`);
|
||||
quotedLines.push('>');
|
||||
}
|
||||
|
||||
normalizedLines.forEach(line => {
|
||||
const trimmedLine = line.trimEnd();
|
||||
quotedLines.push(trimmedLine ? `> ${trimmedLine}` : '>');
|
||||
});
|
||||
|
||||
return quotedLines.join('\n');
|
||||
};
|
||||
|
||||
/**
|
||||
* Extracts quoted email text from last email message
|
||||
* @param {Object} lastEmail - Last email message object
|
||||
* @returns {string} Quoted email text
|
||||
*/
|
||||
export const extractQuotedEmailText = lastEmail => {
|
||||
if (!lastEmail) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const contentAttributes =
|
||||
lastEmail.contentAttributes || lastEmail.content_attributes || {};
|
||||
const emailContent = contentAttributes.email || {};
|
||||
const textContent = emailContent.textContent || emailContent.text_content;
|
||||
|
||||
if (textContent?.reply) {
|
||||
return textContent.reply;
|
||||
}
|
||||
if (textContent?.full) {
|
||||
return textContent.full;
|
||||
}
|
||||
|
||||
const htmlContent = emailContent.htmlContent || emailContent.html_content;
|
||||
if (htmlContent?.reply) {
|
||||
return extractPlainTextFromHtml(htmlContent.reply);
|
||||
}
|
||||
if (htmlContent?.full) {
|
||||
return extractPlainTextFromHtml(htmlContent.full);
|
||||
}
|
||||
|
||||
const fallbackContent =
|
||||
lastEmail.content || lastEmail.processed_message_content || '';
|
||||
|
||||
return fallbackContent;
|
||||
};
|
||||
|
||||
/**
|
||||
* Truncates text for preview display
|
||||
* @param {string} text - Text to truncate
|
||||
* @param {number} maxLength - Maximum length (default: 80)
|
||||
* @returns {string} Truncated text
|
||||
*/
|
||||
export const truncatePreviewText = (text, maxLength = 80) => {
|
||||
const preview = text.trim().replace(/\s+/g, ' ');
|
||||
if (!preview) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (preview.length <= maxLength) {
|
||||
return preview;
|
||||
}
|
||||
return `${preview.slice(0, maxLength - 3)}...`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Appends quoted text to message
|
||||
* @param {string} message - Original message
|
||||
* @param {string} quotedText - Text to quote
|
||||
* @param {string} header - Quote header
|
||||
* @returns {string} Message with quoted text appended
|
||||
*/
|
||||
export const appendQuotedTextToMessage = (message, quotedText, header) => {
|
||||
const baseMessage = message ? String(message) : '';
|
||||
const quotedBlock = formatQuotedTextAsBlockquote(quotedText, header);
|
||||
|
||||
if (!quotedBlock) {
|
||||
return baseMessage;
|
||||
}
|
||||
|
||||
if (!baseMessage) {
|
||||
return quotedBlock;
|
||||
}
|
||||
|
||||
let separator = '\n\n';
|
||||
if (baseMessage.endsWith('\n\n')) {
|
||||
separator = '';
|
||||
} else if (baseMessage.endsWith('\n')) {
|
||||
separator = '\n';
|
||||
}
|
||||
|
||||
return `${baseMessage}${separator}${quotedBlock}`;
|
||||
};
|
||||
@@ -0,0 +1,99 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { EmailQuoteExtractor } from '../emailQuoteExtractor.js';
|
||||
|
||||
const SAMPLE_EMAIL_HTML = `
|
||||
<p>method</p>
|
||||
<blockquote>
|
||||
<p>On Mon, Sep 29, 2025 at 5:18 PM John <a href="mailto:shivam@chatwoot.com">shivam@chatwoot.com</a> wrote:</p>
|
||||
<p>Hi</p>
|
||||
<blockquote>
|
||||
<p>On Mon, Sep 29, 2025 at 5:17 PM Shivam Mishra <a href="mailto:shivam@chatwoot.com">shivam@chatwoot.com</a> wrote:</p>
|
||||
<p>Yes, it is.</p>
|
||||
<p>On Mon, Sep 29, 2025 at 5:16 PM John from Shaneforwoot < shaneforwoot@gmail.com> wrote:</p>
|
||||
<blockquote>
|
||||
<p>Hey</p>
|
||||
<p>On Mon, Sep 29, 2025 at 4:59 PM John shivam@chatwoot.com wrote:</p>
|
||||
<p>This is another quoted quoted text reply</p>
|
||||
<p>This is nice</p>
|
||||
<p>On Mon, Sep 29, 2025 at 4:21 PM John from Shaneforwoot < > shaneforwoot@gmail.com> wrote:</p>
|
||||
<p>Hey there, this is a reply from Chatwoot, notice the quoted text</p>
|
||||
<p>Hey there</p>
|
||||
<p>This is an email text, enjoy reading this</p>
|
||||
<p>-- Shivam Mishra, Chatwoot</p>
|
||||
</blockquote>
|
||||
</blockquote>
|
||||
</blockquote>
|
||||
`;
|
||||
|
||||
const EMAIL_WITH_SIGNATURE = `
|
||||
<p>Latest reply here.</p>
|
||||
<p>Thanks,</p>
|
||||
<p>Jane Doe</p>
|
||||
<blockquote>
|
||||
<p>On Mon, Sep 22, Someone wrote:</p>
|
||||
<p>Previous reply content</p>
|
||||
</blockquote>
|
||||
`;
|
||||
|
||||
const EMAIL_WITH_FOLLOW_UP_CONTENT = `
|
||||
<blockquote>
|
||||
<p>Inline quote that should stay</p>
|
||||
</blockquote>
|
||||
<p>Internal note follows</p>
|
||||
<p>Regards,</p>
|
||||
`;
|
||||
|
||||
describe('EmailQuoteExtractor', () => {
|
||||
it('removes blockquote-based quotes from the email body', () => {
|
||||
const cleanedHtml = EmailQuoteExtractor.extractQuotes(SAMPLE_EMAIL_HTML);
|
||||
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = cleanedHtml;
|
||||
|
||||
expect(container.querySelectorAll('blockquote').length).toBe(0);
|
||||
expect(container.textContent?.trim()).toBe('method');
|
||||
expect(container.textContent).not.toContain(
|
||||
'On Mon, Sep 29, 2025 at 5:18 PM'
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps blockquote fallback when it is not the last top-level element', () => {
|
||||
const cleanedHtml = EmailQuoteExtractor.extractQuotes(
|
||||
EMAIL_WITH_FOLLOW_UP_CONTENT
|
||||
);
|
||||
|
||||
const container = document.createElement('div');
|
||||
container.innerHTML = cleanedHtml;
|
||||
|
||||
expect(container.querySelector('blockquote')).not.toBeNull();
|
||||
expect(container.lastElementChild?.tagName).toBe('P');
|
||||
});
|
||||
|
||||
it('detects quote indicators in nested blockquotes', () => {
|
||||
const result = EmailQuoteExtractor.hasQuotes(SAMPLE_EMAIL_HTML);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('does not flag blockquotes that are followed by other elements', () => {
|
||||
expect(EmailQuoteExtractor.hasQuotes(EMAIL_WITH_FOLLOW_UP_CONTENT)).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
it('returns false when no quote indicators are present', () => {
|
||||
const html = '<p>Plain content</p>';
|
||||
expect(EmailQuoteExtractor.hasQuotes(html)).toBe(false);
|
||||
});
|
||||
|
||||
it('removes trailing blockquotes while preserving trailing signatures', () => {
|
||||
const cleanedHtml = EmailQuoteExtractor.extractQuotes(EMAIL_WITH_SIGNATURE);
|
||||
|
||||
expect(cleanedHtml).toContain('<p>Thanks,</p>');
|
||||
expect(cleanedHtml).toContain('<p>Jane Doe</p>');
|
||||
expect(cleanedHtml).not.toContain('<blockquote');
|
||||
});
|
||||
|
||||
it('detects quotes for trailing blockquotes even when signatures follow text', () => {
|
||||
expect(EmailQuoteExtractor.hasQuotes(EMAIL_WITH_SIGNATURE)).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,441 @@
|
||||
import {
|
||||
extractPlainTextFromHtml,
|
||||
getEmailSenderName,
|
||||
getEmailSenderEmail,
|
||||
getEmailDate,
|
||||
formatQuotedEmailDate,
|
||||
getInboxEmail,
|
||||
buildQuotedEmailHeader,
|
||||
buildQuotedEmailHeaderFromContact,
|
||||
buildQuotedEmailHeaderFromInbox,
|
||||
formatQuotedTextAsBlockquote,
|
||||
extractQuotedEmailText,
|
||||
truncatePreviewText,
|
||||
appendQuotedTextToMessage,
|
||||
} from '../quotedEmailHelper';
|
||||
|
||||
describe('quotedEmailHelper', () => {
|
||||
describe('extractPlainTextFromHtml', () => {
|
||||
it('returns empty string for null or undefined', () => {
|
||||
expect(extractPlainTextFromHtml(null)).toBe('');
|
||||
expect(extractPlainTextFromHtml(undefined)).toBe('');
|
||||
});
|
||||
|
||||
it('strips HTML tags and returns plain text', () => {
|
||||
const html = '<p>Hello <strong>world</strong></p>';
|
||||
const result = extractPlainTextFromHtml(html);
|
||||
expect(result).toBe('Hello world');
|
||||
});
|
||||
|
||||
it('handles complex HTML structure', () => {
|
||||
const html = '<div><p>Line 1</p><p>Line 2</p></div>';
|
||||
const result = extractPlainTextFromHtml(html);
|
||||
expect(result).toContain('Line 1');
|
||||
expect(result).toContain('Line 2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getEmailSenderName', () => {
|
||||
it('returns sender name from lastEmail', () => {
|
||||
const lastEmail = { sender: { name: 'John Doe' } };
|
||||
const result = getEmailSenderName(lastEmail, {});
|
||||
expect(result).toBe('John Doe');
|
||||
});
|
||||
|
||||
it('returns contact name if sender name not available', () => {
|
||||
const lastEmail = { sender: {} };
|
||||
const contact = { name: 'Jane Smith' };
|
||||
const result = getEmailSenderName(lastEmail, contact);
|
||||
expect(result).toBe('Jane Smith');
|
||||
});
|
||||
|
||||
it('returns empty string if neither available', () => {
|
||||
const result = getEmailSenderName({}, {});
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('trims whitespace from names', () => {
|
||||
const lastEmail = { sender: { name: ' John Doe ' } };
|
||||
const result = getEmailSenderName(lastEmail, {});
|
||||
expect(result).toBe('John Doe');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getEmailSenderEmail', () => {
|
||||
it('returns sender email from lastEmail', () => {
|
||||
const lastEmail = { sender: { email: 'john@example.com' } };
|
||||
const result = getEmailSenderEmail(lastEmail, {});
|
||||
expect(result).toBe('john@example.com');
|
||||
});
|
||||
|
||||
it('returns email from contentAttributes if sender email not available', () => {
|
||||
const lastEmail = {
|
||||
contentAttributes: {
|
||||
email: { from: ['jane@example.com'] },
|
||||
},
|
||||
};
|
||||
const result = getEmailSenderEmail(lastEmail, {});
|
||||
expect(result).toBe('jane@example.com');
|
||||
});
|
||||
|
||||
it('returns contact email as fallback', () => {
|
||||
const lastEmail = {};
|
||||
const contact = { email: 'contact@example.com' };
|
||||
const result = getEmailSenderEmail(lastEmail, contact);
|
||||
expect(result).toBe('contact@example.com');
|
||||
});
|
||||
|
||||
it('trims whitespace from emails', () => {
|
||||
const lastEmail = { sender: { email: ' john@example.com ' } };
|
||||
const result = getEmailSenderEmail(lastEmail, {});
|
||||
expect(result).toBe('john@example.com');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getEmailDate', () => {
|
||||
it('returns parsed date from email metadata', () => {
|
||||
const lastEmail = {
|
||||
contentAttributes: {
|
||||
email: { date: '2024-01-15T10:30:00Z' },
|
||||
},
|
||||
};
|
||||
const result = getEmailDate(lastEmail);
|
||||
expect(result).toBeInstanceOf(Date);
|
||||
});
|
||||
|
||||
it('returns date from created_at timestamp', () => {
|
||||
const lastEmail = { created_at: 1705318200 };
|
||||
const result = getEmailDate(lastEmail);
|
||||
expect(result).toBeInstanceOf(Date);
|
||||
});
|
||||
|
||||
it('handles millisecond timestamps', () => {
|
||||
const lastEmail = { created_at: 1705318200000 };
|
||||
const result = getEmailDate(lastEmail);
|
||||
expect(result).toBeInstanceOf(Date);
|
||||
});
|
||||
|
||||
it('returns null if no valid date found', () => {
|
||||
const result = getEmailDate({});
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatQuotedEmailDate', () => {
|
||||
it('formats date correctly', () => {
|
||||
const date = new Date('2024-01-15T10:30:00Z');
|
||||
const result = formatQuotedEmailDate(date);
|
||||
expect(result).toMatch(/Mon, Jan 15, 2024 at/);
|
||||
});
|
||||
|
||||
it('returns empty string for invalid date', () => {
|
||||
const result = formatQuotedEmailDate('invalid');
|
||||
expect(result).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getInboxEmail', () => {
|
||||
it('returns email from contentAttributes.email.to', () => {
|
||||
const lastEmail = {
|
||||
contentAttributes: {
|
||||
email: { to: ['inbox@example.com'] },
|
||||
},
|
||||
};
|
||||
const result = getInboxEmail(lastEmail, {});
|
||||
expect(result).toBe('inbox@example.com');
|
||||
});
|
||||
|
||||
it('returns inbox email as fallback', () => {
|
||||
const lastEmail = {};
|
||||
const inbox = { email: 'support@example.com' };
|
||||
const result = getInboxEmail(lastEmail, inbox);
|
||||
expect(result).toBe('support@example.com');
|
||||
});
|
||||
|
||||
it('returns empty string if no email found', () => {
|
||||
expect(getInboxEmail({}, {})).toBe('');
|
||||
});
|
||||
|
||||
it('trims whitespace from emails', () => {
|
||||
const lastEmail = {
|
||||
contentAttributes: {
|
||||
email: { to: [' inbox@example.com '] },
|
||||
},
|
||||
};
|
||||
const result = getInboxEmail(lastEmail, {});
|
||||
expect(result).toBe('inbox@example.com');
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildQuotedEmailHeaderFromContact', () => {
|
||||
it('builds complete header with name and email', () => {
|
||||
const lastEmail = {
|
||||
sender: { name: 'John Doe', email: 'john@example.com' },
|
||||
contentAttributes: {
|
||||
email: { date: '2024-01-15T10:30:00Z' },
|
||||
},
|
||||
};
|
||||
const result = buildQuotedEmailHeaderFromContact(lastEmail, {});
|
||||
expect(result).toContain('John Doe');
|
||||
expect(result).toContain('john@example.com');
|
||||
expect(result).toContain('wrote:');
|
||||
});
|
||||
|
||||
it('builds header without name if not available', () => {
|
||||
const lastEmail = {
|
||||
sender: { email: 'john@example.com' },
|
||||
contentAttributes: {
|
||||
email: { date: '2024-01-15T10:30:00Z' },
|
||||
},
|
||||
};
|
||||
const result = buildQuotedEmailHeaderFromContact(lastEmail, {});
|
||||
expect(result).toContain('<john@example.com>');
|
||||
expect(result).not.toContain('undefined');
|
||||
});
|
||||
|
||||
it('returns empty string if missing required data', () => {
|
||||
expect(buildQuotedEmailHeaderFromContact(null, {})).toBe('');
|
||||
expect(buildQuotedEmailHeaderFromContact({}, {})).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildQuotedEmailHeaderFromInbox', () => {
|
||||
it('builds complete header with inbox name and email', () => {
|
||||
const lastEmail = {
|
||||
contentAttributes: {
|
||||
email: {
|
||||
date: '2024-01-15T10:30:00Z',
|
||||
to: ['support@example.com'],
|
||||
},
|
||||
},
|
||||
};
|
||||
const inbox = { name: 'Support Team', email: 'support@example.com' };
|
||||
const result = buildQuotedEmailHeaderFromInbox(lastEmail, inbox);
|
||||
expect(result).toContain('Support Team');
|
||||
expect(result).toContain('support@example.com');
|
||||
expect(result).toContain('wrote:');
|
||||
});
|
||||
|
||||
it('builds header without name if not available', () => {
|
||||
const lastEmail = {
|
||||
contentAttributes: {
|
||||
email: {
|
||||
date: '2024-01-15T10:30:00Z',
|
||||
to: ['inbox@example.com'],
|
||||
},
|
||||
},
|
||||
};
|
||||
const inbox = { email: 'inbox@example.com' };
|
||||
const result = buildQuotedEmailHeaderFromInbox(lastEmail, inbox);
|
||||
expect(result).toContain('<inbox@example.com>');
|
||||
expect(result).not.toContain('undefined');
|
||||
});
|
||||
|
||||
it('returns empty string if missing required data', () => {
|
||||
expect(buildQuotedEmailHeaderFromInbox(null, {})).toBe('');
|
||||
expect(buildQuotedEmailHeaderFromInbox({}, {})).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildQuotedEmailHeader', () => {
|
||||
it('uses inbox email for outgoing messages (message_type: 1)', () => {
|
||||
const lastEmail = {
|
||||
message_type: 1,
|
||||
contentAttributes: {
|
||||
email: {
|
||||
date: '2024-01-15T10:30:00Z',
|
||||
to: ['support@example.com'],
|
||||
},
|
||||
},
|
||||
};
|
||||
const inbox = { name: 'Support', email: 'support@example.com' };
|
||||
const contact = { name: 'John Doe', email: 'john@example.com' };
|
||||
const result = buildQuotedEmailHeader(lastEmail, contact, inbox);
|
||||
expect(result).toContain('Support');
|
||||
expect(result).toContain('support@example.com');
|
||||
expect(result).not.toContain('John Doe');
|
||||
});
|
||||
|
||||
it('uses contact email for incoming messages (message_type: 0)', () => {
|
||||
const lastEmail = {
|
||||
message_type: 0,
|
||||
sender: { name: 'Jane Smith', email: 'jane@example.com' },
|
||||
contentAttributes: {
|
||||
email: { date: '2024-01-15T10:30:00Z' },
|
||||
},
|
||||
};
|
||||
const inbox = { name: 'Support', email: 'support@example.com' };
|
||||
const contact = { name: 'Jane Smith', email: 'jane@example.com' };
|
||||
const result = buildQuotedEmailHeader(lastEmail, contact, inbox);
|
||||
expect(result).toContain('Jane Smith');
|
||||
expect(result).toContain('jane@example.com');
|
||||
expect(result).not.toContain('Support');
|
||||
});
|
||||
|
||||
it('returns empty string if missing required data', () => {
|
||||
expect(buildQuotedEmailHeader(null, {}, {})).toBe('');
|
||||
expect(buildQuotedEmailHeader({}, {}, {})).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatQuotedTextAsBlockquote', () => {
|
||||
it('formats single line text', () => {
|
||||
const result = formatQuotedTextAsBlockquote('Hello world');
|
||||
expect(result).toBe('> Hello world');
|
||||
});
|
||||
|
||||
it('formats multi-line text', () => {
|
||||
const text = 'Line 1\nLine 2\nLine 3';
|
||||
const result = formatQuotedTextAsBlockquote(text);
|
||||
expect(result).toBe('> Line 1\n> Line 2\n> Line 3');
|
||||
});
|
||||
|
||||
it('includes header if provided', () => {
|
||||
const result = formatQuotedTextAsBlockquote('Hello', 'Header text');
|
||||
expect(result).toContain('> Header text');
|
||||
expect(result).toContain('>\n> Hello');
|
||||
});
|
||||
|
||||
it('handles empty lines correctly', () => {
|
||||
const text = 'Line 1\n\nLine 3';
|
||||
const result = formatQuotedTextAsBlockquote(text);
|
||||
expect(result).toBe('> Line 1\n>\n> Line 3');
|
||||
});
|
||||
|
||||
it('returns empty string for empty input', () => {
|
||||
expect(formatQuotedTextAsBlockquote('')).toBe('');
|
||||
expect(formatQuotedTextAsBlockquote('', '')).toBe('');
|
||||
});
|
||||
|
||||
it('handles Windows line endings', () => {
|
||||
const text = 'Line 1\r\nLine 2';
|
||||
const result = formatQuotedTextAsBlockquote(text);
|
||||
expect(result).toBe('> Line 1\n> Line 2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractQuotedEmailText', () => {
|
||||
it('extracts text from textContent.reply', () => {
|
||||
const lastEmail = {
|
||||
contentAttributes: {
|
||||
email: { textContent: { reply: 'Reply text' } },
|
||||
},
|
||||
};
|
||||
const result = extractQuotedEmailText(lastEmail);
|
||||
expect(result).toBe('Reply text');
|
||||
});
|
||||
|
||||
it('falls back to textContent.full', () => {
|
||||
const lastEmail = {
|
||||
contentAttributes: {
|
||||
email: { textContent: { full: 'Full text' } },
|
||||
},
|
||||
};
|
||||
const result = extractQuotedEmailText(lastEmail);
|
||||
expect(result).toBe('Full text');
|
||||
});
|
||||
|
||||
it('extracts from htmlContent and converts to plain text', () => {
|
||||
const lastEmail = {
|
||||
contentAttributes: {
|
||||
email: { htmlContent: { reply: '<p>HTML reply</p>' } },
|
||||
},
|
||||
};
|
||||
const result = extractQuotedEmailText(lastEmail);
|
||||
expect(result).toBe('HTML reply');
|
||||
});
|
||||
|
||||
it('uses fallback content if structured content not available', () => {
|
||||
const lastEmail = { content: 'Fallback content' };
|
||||
const result = extractQuotedEmailText(lastEmail);
|
||||
expect(result).toBe('Fallback content');
|
||||
});
|
||||
|
||||
it('returns empty string for null or missing email', () => {
|
||||
expect(extractQuotedEmailText(null)).toBe('');
|
||||
expect(extractQuotedEmailText({})).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('truncatePreviewText', () => {
|
||||
it('returns full text if under max length', () => {
|
||||
const text = 'Short text';
|
||||
const result = truncatePreviewText(text, 80);
|
||||
expect(result).toBe('Short text');
|
||||
});
|
||||
|
||||
it('truncates text exceeding max length', () => {
|
||||
const text = 'A'.repeat(100);
|
||||
const result = truncatePreviewText(text, 80);
|
||||
expect(result).toHaveLength(80);
|
||||
expect(result).toContain('...');
|
||||
});
|
||||
|
||||
it('collapses multiple spaces', () => {
|
||||
const text = 'Text with spaces';
|
||||
const result = truncatePreviewText(text);
|
||||
expect(result).toBe('Text with spaces');
|
||||
});
|
||||
|
||||
it('trims whitespace', () => {
|
||||
const text = ' Text with spaces ';
|
||||
const result = truncatePreviewText(text);
|
||||
expect(result).toBe('Text with spaces');
|
||||
});
|
||||
|
||||
it('returns empty string for empty input', () => {
|
||||
expect(truncatePreviewText('')).toBe('');
|
||||
expect(truncatePreviewText(' ')).toBe('');
|
||||
});
|
||||
|
||||
it('uses default max length of 80', () => {
|
||||
const text = 'A'.repeat(100);
|
||||
const result = truncatePreviewText(text);
|
||||
expect(result).toHaveLength(80);
|
||||
});
|
||||
});
|
||||
|
||||
describe('appendQuotedTextToMessage', () => {
|
||||
it('appends quoted text to message', () => {
|
||||
const message = 'My reply';
|
||||
const quotedText = 'Original message';
|
||||
const header = 'On date sender wrote:';
|
||||
const result = appendQuotedTextToMessage(message, quotedText, header);
|
||||
|
||||
expect(result).toContain('My reply');
|
||||
expect(result).toContain('> On date sender wrote:');
|
||||
expect(result).toContain('> Original message');
|
||||
});
|
||||
|
||||
it('returns only quoted text if message is empty', () => {
|
||||
const result = appendQuotedTextToMessage('', 'Quoted', 'Header');
|
||||
expect(result).toContain('> Header');
|
||||
expect(result).toContain('> Quoted');
|
||||
expect(result).not.toContain('\n\n\n');
|
||||
});
|
||||
|
||||
it('returns message if no quoted text', () => {
|
||||
const result = appendQuotedTextToMessage('Message', '', '');
|
||||
expect(result).toBe('Message');
|
||||
});
|
||||
|
||||
it('handles proper spacing with double newline', () => {
|
||||
const result = appendQuotedTextToMessage('Message', 'Quoted', 'Header');
|
||||
expect(result).toContain('Message\n\n>');
|
||||
});
|
||||
|
||||
it('does not add extra newlines if message already ends with newlines', () => {
|
||||
const result = appendQuotedTextToMessage(
|
||||
'Message\n\n',
|
||||
'Quoted',
|
||||
'Header'
|
||||
);
|
||||
expect(result).not.toContain('\n\n\n');
|
||||
});
|
||||
|
||||
it('adds single newline if message ends with one newline', () => {
|
||||
const result = appendQuotedTextToMessage('Message\n', 'Quoted', 'Header');
|
||||
expect(result).toContain('Message\n\n>');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -227,6 +227,13 @@
|
||||
"YES": "Send",
|
||||
"CANCEL": "Cancel"
|
||||
}
|
||||
},
|
||||
"QUOTED_REPLY": {
|
||||
"ENABLE_TOOLTIP": "Include quoted email thread",
|
||||
"DISABLE_TOOLTIP": "Don't include quoted email thread",
|
||||
"REMOVE_PREVIEW": "Remove quoted email thread",
|
||||
"COLLAPSE": "Collapse preview",
|
||||
"EXPAND": "Expand preview"
|
||||
}
|
||||
},
|
||||
"VISIBLE_TO_AGENTS": "Private Note: Only visible to you and your team",
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
"PLACEHOLDER": "Search",
|
||||
"EMPTY_STATE": "No results found"
|
||||
},
|
||||
"CLOSE": "Close"
|
||||
"CLOSE": "Close",
|
||||
"BETA": "Beta",
|
||||
"BETA_DESCRIPTION": "This feature is in beta and may change as we improve it."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
@@ -618,6 +676,11 @@
|
||||
"SETTINGS_POPUP": {
|
||||
"MESSENGER_HEADING": "Messenger Script",
|
||||
"MESSENGER_SUB_HEAD": "Place this button inside your body tag",
|
||||
"ALLOWED_DOMAINS": {
|
||||
"TITLE": "Allowed Domains",
|
||||
"SUBTITLE": "Add wildcard or regular domains separated by commas (leave blank to allow all), e.g. *.chatwoot.dev, chatwoot.com.",
|
||||
"PLACEHOLDER": "Enter domains separated by commas (eg: *.chatwoot.dev, chatwoot.com)"
|
||||
},
|
||||
"INBOX_AGENTS": "Agents",
|
||||
"INBOX_AGENTS_SUB_TEXT": "Add or remove agents from this inbox",
|
||||
"AGENT_ASSIGNMENT": "Conversation Assignment",
|
||||
|
||||
@@ -752,6 +752,115 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"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",
|
||||
"ADD_NEW": "Create new FAQ",
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"CREATE_NEW_ACCOUNT": "Create a new account",
|
||||
"SUBMIT": "Login",
|
||||
"SAML": {
|
||||
"LABEL": "Log in via SSO",
|
||||
"LABEL": "Login via SSO",
|
||||
"TITLE": "Initiate Single Sign-on (SSO)",
|
||||
"SUBTITLE": "Enter your work email to access your organization",
|
||||
"BACK_TO_LOGIN": "Login via Password",
|
||||
@@ -34,7 +34,7 @@
|
||||
},
|
||||
"SUBMIT": "Continue with SSO",
|
||||
"API": {
|
||||
"ERROR_MESSAGE": "SSO authentication failed"
|
||||
"ERROR_MESSAGE": "SSO authentication failed. Please check your credentials and try again."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -27,15 +27,20 @@
|
||||
"LABEL": "Password",
|
||||
"PLACEHOLDER": "Password",
|
||||
"ERROR": "Password is too short.",
|
||||
"IS_INVALID_PASSWORD": "Password should contain atleast 1 uppercase letter, 1 lowercase letter, 1 number and 1 special character."
|
||||
"IS_INVALID_PASSWORD": "Password should contain atleast 1 uppercase letter, 1 lowercase letter, 1 number and 1 special character.",
|
||||
"REQUIREMENTS_LENGTH": "At least 6 characters long",
|
||||
"REQUIREMENTS_UPPERCASE": "At least one uppercase letter",
|
||||
"REQUIREMENTS_LOWERCASE": "At least one lowercase letter",
|
||||
"REQUIREMENTS_NUMBER": "At least one number",
|
||||
"REQUIREMENTS_SPECIAL": "At least one special character"
|
||||
},
|
||||
"CONFIRM_PASSWORD": {
|
||||
"LABEL": "Confirm password",
|
||||
"PLACEHOLDER": "Confirm password",
|
||||
"ERROR": "Password doesnot match."
|
||||
"ERROR": "Passwords do not match."
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Registration Successfull",
|
||||
"SUCCESS_MESSAGE": "Registration Successful",
|
||||
"ERROR_MESSAGE": "Could not connect to Woot server. Please try again."
|
||||
},
|
||||
"SUBMIT": "Create account",
|
||||
|
||||
@@ -227,6 +227,13 @@
|
||||
"YES": "إرسال",
|
||||
"CANCEL": "إلغاء"
|
||||
}
|
||||
},
|
||||
"QUOTED_REPLY": {
|
||||
"ENABLE_TOOLTIP": "Include quoted email thread",
|
||||
"DISABLE_TOOLTIP": "Don't include quoted email thread",
|
||||
"REMOVE_PREVIEW": "Remove quoted email thread",
|
||||
"COLLAPSE": "Collapse preview",
|
||||
"EXPAND": "Expand preview"
|
||||
}
|
||||
},
|
||||
"VISIBLE_TO_AGENTS": "ملاحظة خاصة: مرئية فقط لك ولأعضاء فريقك",
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
"PLACEHOLDER": "بحث",
|
||||
"EMPTY_STATE": "لم يتم العثور على النتائج"
|
||||
},
|
||||
"CLOSE": "أغلق"
|
||||
"CLOSE": "أغلق",
|
||||
"BETA": "تجريبي",
|
||||
"BETA_DESCRIPTION": "This feature is in beta and may change as we improve it."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "الخصائص",
|
||||
@@ -618,6 +676,11 @@
|
||||
"SETTINGS_POPUP": {
|
||||
"MESSENGER_HEADING": "كود \"الماسنجر\"",
|
||||
"MESSENGER_SUB_HEAD": "ضع هذا الكود داخل وسم الـ body في موقعك",
|
||||
"ALLOWED_DOMAINS": {
|
||||
"TITLE": "Allowed Domains",
|
||||
"SUBTITLE": "Add wildcard or regular domains separated by commas (leave blank to allow all), e.g. *.chatwoot.dev, chatwoot.com.",
|
||||
"PLACEHOLDER": "Enter domains separated by commas (eg: *.chatwoot.dev, chatwoot.com)"
|
||||
},
|
||||
"INBOX_AGENTS": "وكيل الدعم",
|
||||
"INBOX_AGENTS_SUB_TEXT": "إضافة أو إزالة وكلاء من صندوق الوارد هذا",
|
||||
"AGENT_ASSIGNMENT": "تعيين المحادثة",
|
||||
|
||||
@@ -752,6 +752,115 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"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",
|
||||
"ADD_NEW": "Create new FAQ",
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"CREATE_NEW_ACCOUNT": "إنشاء حساب جديد",
|
||||
"SUBMIT": "تسجيل الدخول",
|
||||
"SAML": {
|
||||
"LABEL": "Log in via SSO",
|
||||
"LABEL": "Login via SSO",
|
||||
"TITLE": "Initiate Single Sign-on (SSO)",
|
||||
"SUBTITLE": "Enter your work email to access your organization",
|
||||
"BACK_TO_LOGIN": "Login via Password",
|
||||
@@ -34,7 +34,7 @@
|
||||
},
|
||||
"SUBMIT": "Continue with SSO",
|
||||
"API": {
|
||||
"ERROR_MESSAGE": "SSO authentication failed"
|
||||
"ERROR_MESSAGE": "SSO authentication failed. Please check your credentials and try again."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "الروبوتات",
|
||||
|
||||
@@ -27,15 +27,20 @@
|
||||
"LABEL": "كلمة المرور",
|
||||
"PLACEHOLDER": "كلمة المرور",
|
||||
"ERROR": "كلمة المرور قصيرة جداً",
|
||||
"IS_INVALID_PASSWORD": "يجب أن تحتوي كلمة المرور على الأقل على حرف كبير واحد وحرف صغير واحد ورقم واحد وحرف خاص واحد"
|
||||
"IS_INVALID_PASSWORD": "يجب أن تحتوي كلمة المرور على الأقل على حرف كبير واحد وحرف صغير واحد ورقم واحد وحرف خاص واحد",
|
||||
"REQUIREMENTS_LENGTH": "At least 6 characters long",
|
||||
"REQUIREMENTS_UPPERCASE": "At least one uppercase letter",
|
||||
"REQUIREMENTS_LOWERCASE": "At least one lowercase letter",
|
||||
"REQUIREMENTS_NUMBER": "At least one number",
|
||||
"REQUIREMENTS_SPECIAL": "At least one special character"
|
||||
},
|
||||
"CONFIRM_PASSWORD": {
|
||||
"LABEL": "تأكيد كلمة المرور",
|
||||
"PLACEHOLDER": "تأكيد كلمة المرور",
|
||||
"ERROR": "كلمة المرور غير متطابقة"
|
||||
"ERROR": "كلمة المرور غير متطابقة."
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "تم التسجيل بنجاح",
|
||||
"SUCCESS_MESSAGE": "Registration Successful",
|
||||
"ERROR_MESSAGE": "تعذر الاتصال بالخادم، الرجاء المحاولة مرة أخرى لاحقاً"
|
||||
},
|
||||
"SUBMIT": "إرسال",
|
||||
|
||||
@@ -227,6 +227,13 @@
|
||||
"YES": "Send",
|
||||
"CANCEL": "Cancel"
|
||||
}
|
||||
},
|
||||
"QUOTED_REPLY": {
|
||||
"ENABLE_TOOLTIP": "Include quoted email thread",
|
||||
"DISABLE_TOOLTIP": "Don't include quoted email thread",
|
||||
"REMOVE_PREVIEW": "Remove quoted email thread",
|
||||
"COLLAPSE": "Collapse preview",
|
||||
"EXPAND": "Expand preview"
|
||||
}
|
||||
},
|
||||
"VISIBLE_TO_AGENTS": "Private Note: Only visible to you and your team",
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
"PLACEHOLDER": "Search",
|
||||
"EMPTY_STATE": "No results found"
|
||||
},
|
||||
"CLOSE": "Close"
|
||||
"CLOSE": "Close",
|
||||
"BETA": "Beta",
|
||||
"BETA_DESCRIPTION": "This feature is in beta and may change as we improve it."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
@@ -618,6 +676,11 @@
|
||||
"SETTINGS_POPUP": {
|
||||
"MESSENGER_HEADING": "Messenger Script",
|
||||
"MESSENGER_SUB_HEAD": "Place this button inside your body tag",
|
||||
"ALLOWED_DOMAINS": {
|
||||
"TITLE": "Allowed Domains",
|
||||
"SUBTITLE": "Add wildcard or regular domains separated by commas (leave blank to allow all), e.g. *.chatwoot.dev, chatwoot.com.",
|
||||
"PLACEHOLDER": "Enter domains separated by commas (eg: *.chatwoot.dev, chatwoot.com)"
|
||||
},
|
||||
"INBOX_AGENTS": "Agents",
|
||||
"INBOX_AGENTS_SUB_TEXT": "Add or remove agents from this inbox",
|
||||
"AGENT_ASSIGNMENT": "Conversation Assignment",
|
||||
|
||||
@@ -752,6 +752,115 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"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",
|
||||
"ADD_NEW": "Create new FAQ",
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"CREATE_NEW_ACCOUNT": "Create a new account",
|
||||
"SUBMIT": "Login",
|
||||
"SAML": {
|
||||
"LABEL": "Log in via SSO",
|
||||
"LABEL": "Login via SSO",
|
||||
"TITLE": "Initiate Single Sign-on (SSO)",
|
||||
"SUBTITLE": "Enter your work email to access your organization",
|
||||
"BACK_TO_LOGIN": "Login via Password",
|
||||
@@ -34,7 +34,7 @@
|
||||
},
|
||||
"SUBMIT": "Continue with SSO",
|
||||
"API": {
|
||||
"ERROR_MESSAGE": "SSO authentication failed"
|
||||
"ERROR_MESSAGE": "SSO authentication failed. Please check your credentials and try again."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -27,15 +27,20 @@
|
||||
"LABEL": "Password",
|
||||
"PLACEHOLDER": "Password",
|
||||
"ERROR": "Password is too short.",
|
||||
"IS_INVALID_PASSWORD": "Password should contain atleast 1 uppercase letter, 1 lowercase letter, 1 number and 1 special character."
|
||||
"IS_INVALID_PASSWORD": "Password should contain atleast 1 uppercase letter, 1 lowercase letter, 1 number and 1 special character.",
|
||||
"REQUIREMENTS_LENGTH": "At least 6 characters long",
|
||||
"REQUIREMENTS_UPPERCASE": "At least one uppercase letter",
|
||||
"REQUIREMENTS_LOWERCASE": "At least one lowercase letter",
|
||||
"REQUIREMENTS_NUMBER": "At least one number",
|
||||
"REQUIREMENTS_SPECIAL": "At least one special character"
|
||||
},
|
||||
"CONFIRM_PASSWORD": {
|
||||
"LABEL": "Confirm password",
|
||||
"PLACEHOLDER": "Confirm password",
|
||||
"ERROR": "Password doesnot match."
|
||||
"ERROR": "Passwords do not match."
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Registration Successfull",
|
||||
"SUCCESS_MESSAGE": "Registration Successful",
|
||||
"ERROR_MESSAGE": "Could not connect to Woot server. Please try again."
|
||||
},
|
||||
"SUBMIT": "Create account",
|
||||
|
||||
@@ -227,6 +227,13 @@
|
||||
"YES": "Send",
|
||||
"CANCEL": "Отмени"
|
||||
}
|
||||
},
|
||||
"QUOTED_REPLY": {
|
||||
"ENABLE_TOOLTIP": "Include quoted email thread",
|
||||
"DISABLE_TOOLTIP": "Don't include quoted email thread",
|
||||
"REMOVE_PREVIEW": "Remove quoted email thread",
|
||||
"COLLAPSE": "Collapse preview",
|
||||
"EXPAND": "Expand preview"
|
||||
}
|
||||
},
|
||||
"VISIBLE_TO_AGENTS": "Private Note: Only visible to you and your team",
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
"PLACEHOLDER": "Търсене",
|
||||
"EMPTY_STATE": "Няма намерени резултати"
|
||||
},
|
||||
"CLOSE": "Close"
|
||||
"CLOSE": "Close",
|
||||
"BETA": "Beta",
|
||||
"BETA_DESCRIPTION": "This feature is in beta and may change as we improve it."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
@@ -618,6 +676,11 @@
|
||||
"SETTINGS_POPUP": {
|
||||
"MESSENGER_HEADING": "Messenger Script",
|
||||
"MESSENGER_SUB_HEAD": "Place this button inside your body tag",
|
||||
"ALLOWED_DOMAINS": {
|
||||
"TITLE": "Allowed Domains",
|
||||
"SUBTITLE": "Add wildcard or regular domains separated by commas (leave blank to allow all), e.g. *.chatwoot.dev, chatwoot.com.",
|
||||
"PLACEHOLDER": "Enter domains separated by commas (eg: *.chatwoot.dev, chatwoot.com)"
|
||||
},
|
||||
"INBOX_AGENTS": "Агенти",
|
||||
"INBOX_AGENTS_SUB_TEXT": "Add or remove agents from this inbox",
|
||||
"AGENT_ASSIGNMENT": "Conversation Assignment",
|
||||
|
||||
@@ -752,6 +752,115 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"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",
|
||||
"ADD_NEW": "Create new FAQ",
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"CREATE_NEW_ACCOUNT": "Create new account",
|
||||
"SUBMIT": "Login",
|
||||
"SAML": {
|
||||
"LABEL": "Log in via SSO",
|
||||
"LABEL": "Login via SSO",
|
||||
"TITLE": "Initiate Single Sign-on (SSO)",
|
||||
"SUBTITLE": "Enter your work email to access your organization",
|
||||
"BACK_TO_LOGIN": "Login via Password",
|
||||
@@ -34,7 +34,7 @@
|
||||
},
|
||||
"SUBMIT": "Continue with SSO",
|
||||
"API": {
|
||||
"ERROR_MESSAGE": "SSO authentication failed"
|
||||
"ERROR_MESSAGE": "SSO authentication failed. Please check your credentials and try again."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -27,15 +27,20 @@
|
||||
"LABEL": "Password",
|
||||
"PLACEHOLDER": "Password",
|
||||
"ERROR": "Password is too short",
|
||||
"IS_INVALID_PASSWORD": "Password should contain atleast 1 uppercase letter, 1 lowercase letter, 1 number and 1 special character"
|
||||
"IS_INVALID_PASSWORD": "Password should contain atleast 1 uppercase letter, 1 lowercase letter, 1 number and 1 special character",
|
||||
"REQUIREMENTS_LENGTH": "At least 6 characters long",
|
||||
"REQUIREMENTS_UPPERCASE": "At least one uppercase letter",
|
||||
"REQUIREMENTS_LOWERCASE": "At least one lowercase letter",
|
||||
"REQUIREMENTS_NUMBER": "At least one number",
|
||||
"REQUIREMENTS_SPECIAL": "At least one special character"
|
||||
},
|
||||
"CONFIRM_PASSWORD": {
|
||||
"LABEL": "Confirm Password",
|
||||
"PLACEHOLDER": "Confirm Password",
|
||||
"ERROR": "Password doesnot match"
|
||||
"ERROR": "Passwords do not match."
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Registration Successfull",
|
||||
"SUCCESS_MESSAGE": "Registration Successful",
|
||||
"ERROR_MESSAGE": "Не можа да се свърже с Woot сървър. Моля, опитайте отново по-късно"
|
||||
},
|
||||
"SUBMIT": "Create account",
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
{
|
||||
"AGENT_BOTS": {
|
||||
"HEADER": "Bots",
|
||||
"LOADING_EDITOR": "Loading editor...",
|
||||
"DESCRIPTION": "Agent Bots are like the most fabulous members of your team. They can handle the small stuff, so you can focus on the stuff that matters. Give them a try. You can manage your bots from this page or create new ones using the 'Add Bot' button.",
|
||||
"LEARN_MORE": "Learn about agent bots",
|
||||
"GLOBAL_BOT": "System bot",
|
||||
"GLOBAL_BOT_BADGE": "System",
|
||||
"AVATAR": {
|
||||
"SUCCESS_DELETE": "Bot avatar deleted successfully",
|
||||
"ERROR_DELETE": "Error deleting bot avatar, please try again"
|
||||
},
|
||||
"BOT_CONFIGURATION": {
|
||||
"TITLE": "Select an agent bot",
|
||||
"DESC": "Assign an Agent Bot to your inbox. They can handle initial conversations and transfer them to a live agent when necessary.",
|
||||
"SUBMIT": "Update",
|
||||
"DISCONNECT": "Disconnect bot",
|
||||
"SUCCESS_MESSAGE": "Successfully updated the agent bot.",
|
||||
"DISCONNECTED_SUCCESS_MESSAGE": "Successfully disconnected the agent bot.",
|
||||
"ERROR_MESSAGE": "Could not update the agent bot. Please try again.",
|
||||
"DISCONNECTED_ERROR_MESSAGE": "Could not disconnect the agent bot. Please try again.",
|
||||
"SELECT_PLACEHOLDER": "Select bot"
|
||||
},
|
||||
"ADD": {
|
||||
"TITLE": "Add Bot",
|
||||
"CANCEL_BUTTON_TEXT": "Cancel",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Bot added successfully.",
|
||||
"ERROR_MESSAGE": "Could not add bot. Please try again later."
|
||||
}
|
||||
},
|
||||
"LIST": {
|
||||
"404": "No bots found. You can create a bot by clicking the 'Add Bot' button.",
|
||||
"LOADING": "Fetching bots...",
|
||||
"TABLE_HEADER": {
|
||||
"DETAILS": "Bot Details",
|
||||
"URL": "Webhook URL"
|
||||
}
|
||||
},
|
||||
"DELETE": {
|
||||
"BUTTON_TEXT": "Delete",
|
||||
"TITLE": "Delete bot",
|
||||
"CONFIRM": {
|
||||
"TITLE": "Confirm Deletion",
|
||||
"MESSAGE": "Are you sure you want to delete {name}?",
|
||||
"YES": "Yes, Delete",
|
||||
"NO": "No, Keep"
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Bot deleted successfully.",
|
||||
"ERROR_MESSAGE": "Could not delete bot. Please try again."
|
||||
}
|
||||
},
|
||||
"EDIT": {
|
||||
"BUTTON_TEXT": "Edit",
|
||||
"TITLE": "Edit bot",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Bot updated successfully.",
|
||||
"ERROR_MESSAGE": "Could not update bot. Please try again."
|
||||
}
|
||||
},
|
||||
"ACCESS_TOKEN": {
|
||||
"TITLE": "Access Token",
|
||||
"DESCRIPTION": "Copy the access token and save it securely",
|
||||
"COPY_SUCCESSFUL": "Access token copied to clipboard",
|
||||
"RESET_SUCCESS": "Access token regenerated successfully",
|
||||
"RESET_ERROR": "Unable to regenerate access token. Please try again"
|
||||
},
|
||||
"FORM": {
|
||||
"AVATAR": {
|
||||
"LABEL": "Bot avatar"
|
||||
},
|
||||
"NAME": {
|
||||
"LABEL": "Bot name",
|
||||
"PLACEHOLDER": "Enter bot name",
|
||||
"REQUIRED": "Bot name is required"
|
||||
},
|
||||
"DESCRIPTION": {
|
||||
"LABEL": "Description",
|
||||
"PLACEHOLDER": "What does this bot do?"
|
||||
},
|
||||
"WEBHOOK_URL": {
|
||||
"LABEL": "Webhook URL",
|
||||
"PLACEHOLDER": "https://example.com/webhook",
|
||||
"REQUIRED": "Webhook URL is required"
|
||||
},
|
||||
"ERRORS": {
|
||||
"NAME": "Bot name is required",
|
||||
"URL": "Webhook URL is required",
|
||||
"VALID_URL": "Please enter a valid URL starting with http:// or https://"
|
||||
},
|
||||
"CANCEL": "Cancel",
|
||||
"CREATE": "Create Bot",
|
||||
"UPDATE": "Update Bot"
|
||||
},
|
||||
"WEBHOOK": {
|
||||
"DESCRIPTION": "Configure a webhook bot to integrate with your custom services. The bot will receive and process events from conversations and can respond to them."
|
||||
},
|
||||
"TYPES": {
|
||||
"WEBHOOK": "Webhook bot"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
{
|
||||
"AGENT_MGMT": {
|
||||
"HEADER": "Agents",
|
||||
"HEADER_BTN_TXT": "Add Agent",
|
||||
"LOADING": "Fetching Agent List",
|
||||
"DESCRIPTION": "An agent is a member of your customer support team who can view and respond to user messages. The list below shows all the agents in your account.",
|
||||
"LEARN_MORE": "Learn about user roles",
|
||||
"AGENT_TYPES": {
|
||||
"ADMINISTRATOR": "Administrator",
|
||||
"AGENT": "Agent"
|
||||
},
|
||||
"LIST": {
|
||||
"404": "There are no agents associated to this account",
|
||||
"TITLE": "Manage agents in your team",
|
||||
"DESC": "You can add/remove agents to/in your team.",
|
||||
"NAME": "Name",
|
||||
"EMAIL": "EMAIL",
|
||||
"STATUS": "Status",
|
||||
"ACTIONS": "Actions",
|
||||
"VERIFIED": "Verified",
|
||||
"VERIFICATION_PENDING": "Verification Pending",
|
||||
"AVAILABLE_CUSTOM_ROLE": "Available custom role permissions"
|
||||
},
|
||||
"ADD": {
|
||||
"TITLE": "Add agent to your team",
|
||||
"DESC": "You can add people who will be able to handle support for your inboxes.",
|
||||
"CANCEL_BUTTON_TEXT": "Cancel",
|
||||
"FORM": {
|
||||
"NAME": {
|
||||
"LABEL": "Agent Name",
|
||||
"PLACEHOLDER": "Please enter a name of the agent"
|
||||
},
|
||||
"AGENT_TYPE": {
|
||||
"LABEL": "Role",
|
||||
"PLACEHOLDER": "Please select a role",
|
||||
"ERROR": "Role is required"
|
||||
},
|
||||
"EMAIL": {
|
||||
"LABEL": "Email Address",
|
||||
"PLACEHOLDER": "Please enter an email address of the agent"
|
||||
},
|
||||
"SUBMIT": "Add Agent"
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Agent added successfully",
|
||||
"EXIST_MESSAGE": "Agent email already in use, Please try another email address",
|
||||
"ERROR_MESSAGE": "Could not connect to Woot Server, Please try again later"
|
||||
}
|
||||
},
|
||||
"DELETE": {
|
||||
"BUTTON_TEXT": "Delete",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Agent deleted successfully",
|
||||
"ERROR_MESSAGE": "Could not connect to Woot Server, Please try again later"
|
||||
},
|
||||
"CONFIRM": {
|
||||
"TITLE": "Confirm Deletion",
|
||||
"MESSAGE": "Are you sure to delete ",
|
||||
"YES": "Yes, Delete ",
|
||||
"NO": "No, Keep "
|
||||
}
|
||||
},
|
||||
"EDIT": {
|
||||
"TITLE": "Edit agent",
|
||||
"FORM": {
|
||||
"NAME": {
|
||||
"LABEL": "Agent Name",
|
||||
"PLACEHOLDER": "Please enter a name of the agent"
|
||||
},
|
||||
"AGENT_TYPE": {
|
||||
"LABEL": "Role",
|
||||
"PLACEHOLDER": "Please select a role",
|
||||
"ERROR": "Role is required"
|
||||
},
|
||||
"EMAIL": {
|
||||
"LABEL": "Email Address",
|
||||
"PLACEHOLDER": "Please enter an email address of the agent"
|
||||
},
|
||||
"AGENT_AVAILABILITY": {
|
||||
"LABEL": "Availability",
|
||||
"PLACEHOLDER": "Please select an availability status",
|
||||
"ERROR": "Availability is required"
|
||||
},
|
||||
"SUBMIT": "Edit Agent"
|
||||
},
|
||||
"BUTTON_TEXT": "Edit",
|
||||
"CANCEL_BUTTON_TEXT": "Cancel",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Agent updated successfully",
|
||||
"ERROR_MESSAGE": "Could not connect to Woot Server, Please try again later"
|
||||
},
|
||||
"PASSWORD_RESET": {
|
||||
"ADMIN_RESET_BUTTON": "Reset Password",
|
||||
"ADMIN_SUCCESS_MESSAGE": "An email with reset password instructions has been sent to the agent",
|
||||
"SUCCESS_MESSAGE": "Agent password reset successfully",
|
||||
"ERROR_MESSAGE": "Could not connect to Woot Server, Please try again later"
|
||||
}
|
||||
},
|
||||
"SEARCH": {
|
||||
"NO_RESULTS": "No results found."
|
||||
},
|
||||
"MULTI_SELECTOR": {
|
||||
"PLACEHOLDER": "None",
|
||||
"TITLE": {
|
||||
"AGENT": "Select agent",
|
||||
"TEAM": "Select team"
|
||||
},
|
||||
"LIST": {
|
||||
"NONE": "None"
|
||||
},
|
||||
"SEARCH": {
|
||||
"NO_RESULTS": {
|
||||
"AGENT": "No agents found",
|
||||
"TEAM": "No teams found"
|
||||
},
|
||||
"PLACEHOLDER": {
|
||||
"AGENT": "Search agents",
|
||||
"TEAM": "Search teams",
|
||||
"INPUT": "Search for agents"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
{
|
||||
"ATTRIBUTES_MGMT": {
|
||||
"HEADER": "Custom Attributes",
|
||||
"HEADER_BTN_TXT": "Add Custom Attribute",
|
||||
"LOADING": "Fetching custom attributes",
|
||||
"DESCRIPTION": "A custom attribute tracks additional details about your contacts or conversations—such as the subscription plan or the date of their first purchase. You can add different types of custom attributes, such as text, lists, or numbers, to capture the specific information you need.",
|
||||
"LEARN_MORE": "Learn more about custom attributes",
|
||||
"ATTRIBUTE_MODELS": {
|
||||
"CONVERSATION": "Conversation",
|
||||
"CONTACT": "Contact"
|
||||
},
|
||||
"ATTRIBUTE_TYPES": {
|
||||
"TEXT": "Text",
|
||||
"NUMBER": "Number",
|
||||
"LINK": "Link",
|
||||
"DATE": "Date",
|
||||
"LIST": "List",
|
||||
"CHECKBOX": "Checkbox"
|
||||
},
|
||||
"ADD": {
|
||||
"TITLE": "Add Custom Attribute",
|
||||
"SUBMIT": "Create",
|
||||
"CANCEL_BUTTON_TEXT": "Cancel",
|
||||
"FORM": {
|
||||
"NAME": {
|
||||
"LABEL": "Display Name",
|
||||
"PLACEHOLDER": "Enter custom attribute display name",
|
||||
"ERROR": "Name is required"
|
||||
},
|
||||
"DESC": {
|
||||
"LABEL": "Description",
|
||||
"PLACEHOLDER": "Enter custom attribute description",
|
||||
"ERROR": "Description is required"
|
||||
},
|
||||
"MODEL": {
|
||||
"LABEL": "Applies to",
|
||||
"PLACEHOLDER": "Please select one",
|
||||
"ERROR": "Model is required"
|
||||
},
|
||||
"TYPE": {
|
||||
"LABEL": "Type",
|
||||
"PLACEHOLDER": "Please select a type",
|
||||
"ERROR": "Type is required",
|
||||
"LIST": {
|
||||
"LABEL": "List Values",
|
||||
"PLACEHOLDER": "Please enter value and press enter key",
|
||||
"ERROR": "Must have at least one value"
|
||||
}
|
||||
},
|
||||
"KEY": {
|
||||
"LABEL": "Key",
|
||||
"PLACEHOLDER": "Enter custom attribute key",
|
||||
"ERROR": "Key is required",
|
||||
"IN_VALID": "Invalid key"
|
||||
},
|
||||
"REGEX_PATTERN": {
|
||||
"LABEL": "Regex Pattern",
|
||||
"PLACEHOLDER": "Please enter custom attribute regex pattern. (Optional)"
|
||||
},
|
||||
"REGEX_CUE": {
|
||||
"LABEL": "Regex Cue",
|
||||
"PLACEHOLDER": "Please enter regex pattern hint. (Optional)"
|
||||
},
|
||||
"ENABLE_REGEX": {
|
||||
"LABEL": "Enable regex validation"
|
||||
}
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Custom Attribute added successfully!",
|
||||
"ERROR_MESSAGE": "Could not create a Custom Attribute. Please try again later."
|
||||
}
|
||||
},
|
||||
"DELETE": {
|
||||
"BUTTON_TEXT": "Delete",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Custom Attribute deleted successfully.",
|
||||
"ERROR_MESSAGE": "Couldn't delete the custom attribute. Try again."
|
||||
},
|
||||
"CONFIRM": {
|
||||
"TITLE": "Are you sure want to delete - {attributeName}",
|
||||
"PLACE_HOLDER": "Please type {attributeName} to confirm",
|
||||
"MESSAGE": "Deleting will remove the custom attribute",
|
||||
"YES": "Delete ",
|
||||
"NO": "Cancel"
|
||||
}
|
||||
},
|
||||
"EDIT": {
|
||||
"TITLE": "Edit Custom Attribute",
|
||||
"UPDATE_BUTTON_TEXT": "Update",
|
||||
"TYPE": {
|
||||
"LIST": {
|
||||
"LABEL": "List Values",
|
||||
"PLACEHOLDER": "Please enter values and press enter key"
|
||||
}
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Custom Attribute updated successfully",
|
||||
"ERROR_MESSAGE": "There was an error updating custom attribute, please try again"
|
||||
}
|
||||
},
|
||||
"TABS": {
|
||||
"HEADER": "Custom Attributes",
|
||||
"CONVERSATION": "Conversation",
|
||||
"CONTACT": "Contact"
|
||||
},
|
||||
"LIST": {
|
||||
"TABLE_HEADER": {
|
||||
"NAME": "Name",
|
||||
"DESCRIPTION": "Description",
|
||||
"TYPE": "Type",
|
||||
"KEY": "Key"
|
||||
},
|
||||
"BUTTONS": {
|
||||
"EDIT": "Edit",
|
||||
"DELETE": "Delete"
|
||||
},
|
||||
"EMPTY_RESULT": {
|
||||
"404": "There are no custom attributes created",
|
||||
"NOT_FOUND": "There are no custom attributes configured"
|
||||
},
|
||||
"REGEX_PATTERN": {
|
||||
"LABEL": "Regex Pattern",
|
||||
"PLACEHOLDER": "Please enter custom attribute regex pattern. (Optional)"
|
||||
},
|
||||
"REGEX_CUE": {
|
||||
"LABEL": "Regex Cue",
|
||||
"PLACEHOLDER": "Please enter regex pattern hint. (Optional)"
|
||||
},
|
||||
"ENABLE_REGEX": {
|
||||
"LABEL": "Enable regex validation"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"AUDIT_LOGS": {
|
||||
"HEADER": "Audit Logs",
|
||||
"HEADER_BTN_TXT": "Add Audit Logs",
|
||||
"LOADING": "Fetching Audit Logs",
|
||||
"DESCRIPTION": "Audit Logs maintain a record of activities in your account, allowing you to track and audit your account, team, or services.",
|
||||
"LEARN_MORE": "Learn more about audit logs",
|
||||
"SEARCH_404": "There are no items matching this query",
|
||||
"SIDEBAR_TXT": "<p><b>Audit Logs</b> </p><p> Audit Logs are trails for events and actions in a Chatwoot System. </p>",
|
||||
"LIST": {
|
||||
"404": "There are no Audit Logs available in this account.",
|
||||
"TITLE": "Manage Audit Logs",
|
||||
"DESC": "Audit Logs are trails for events and actions in a Chatwoot System.",
|
||||
"TABLE_HEADER": {
|
||||
"ACTIVITY": "Activity",
|
||||
"TIME": "Time",
|
||||
"IP_ADDRESS": "IP Address"
|
||||
}
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "AuditLogs retrieved successfully",
|
||||
"ERROR_MESSAGE": "Could not connect to Woot Server, Please try again later"
|
||||
},
|
||||
"DEFAULT_USER": "System",
|
||||
"AUTOMATION_RULE": {
|
||||
"ADD": "{agentName} created a new automation rule (#{id})",
|
||||
"EDIT": "{agentName} updated an automation rule (#{id})",
|
||||
"DELETE": "{agentName} deleted an automation rule (#{id})"
|
||||
},
|
||||
"ACCOUNT_USER": {
|
||||
"ADD": "{agentName} invited {invitee} to the account as an {role}",
|
||||
"EDIT": {
|
||||
"SELF": "{agentName} changed their {attributes} to {values}",
|
||||
"OTHER": "{agentName} changed {attributes} of {user} to {values}",
|
||||
"DELETED": "{agentName} changed {attributes} of a deleted user to {values}"
|
||||
}
|
||||
},
|
||||
"INBOX": {
|
||||
"ADD": "{agentName} created a new inbox (#{id})",
|
||||
"EDIT": "{agentName} updated an inbox (#{id})",
|
||||
"DELETE": "{agentName} deleted an inbox (#{id})"
|
||||
},
|
||||
"WEBHOOK": {
|
||||
"ADD": "{agentName} created a new webhook (#{id})",
|
||||
"EDIT": "{agentName} updated a webhook (#{id})",
|
||||
"DELETE": "{agentName} deleted a webhook (#{id})"
|
||||
},
|
||||
"USER_ACTION": {
|
||||
"SIGN_IN": "{agentName} signed in",
|
||||
"SIGN_OUT": "{agentName} signed out"
|
||||
},
|
||||
"TEAM": {
|
||||
"ADD": "{agentName} created a new team (#{id})",
|
||||
"EDIT": "{agentName} updated a team (#{id})",
|
||||
"DELETE": "{agentName} deleted a team (#{id})"
|
||||
},
|
||||
"MACRO": {
|
||||
"ADD": "{agentName} created a new macro (#{id})",
|
||||
"EDIT": "{agentName} updated a macro (#{id})",
|
||||
"DELETE": "{agentName} deleted a macro (#{id})"
|
||||
},
|
||||
"INBOX_MEMBER": {
|
||||
"ADD": "{agentName} added {user} to the inbox(#{inbox_id})",
|
||||
"REMOVE": "{agentName} removed {user} from the inbox(#{inbox_id})"
|
||||
},
|
||||
"TEAM_MEMBER": {
|
||||
"ADD": "{agentName} added {user} to the team(#{team_id})",
|
||||
"REMOVE": "{agentName} removed {user} from the team(#{team_id})"
|
||||
},
|
||||
"ACCOUNT": {
|
||||
"EDIT": "{agentName} updated the account configuration (#{id})"
|
||||
},
|
||||
"CONVERSATION": {
|
||||
"DELETE": "{agentName} deleted conversation #{id}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
{
|
||||
"AUTOMATION": {
|
||||
"HEADER": "Automation",
|
||||
"DESCRIPTION": "Automation can replace and streamline existing processes that require manual effort, such as adding labels and assigning conversations to the most suitable agent. This allows the team to focus on their strengths while reducing time spent on routine tasks.",
|
||||
"LEARN_MORE": "Learn more about automation",
|
||||
"HEADER_BTN_TXT": "Add Automation Rule",
|
||||
"LOADING": "Fetching automation rules",
|
||||
"ADD": {
|
||||
"TITLE": "Add Automation Rule",
|
||||
"SUBMIT": "Create",
|
||||
"CANCEL_BUTTON_TEXT": "Cancel",
|
||||
"FORM": {
|
||||
"NAME": {
|
||||
"LABEL": "Rule Name",
|
||||
"PLACEHOLDER": "Enter rule name",
|
||||
"ERROR": "Name is required"
|
||||
},
|
||||
"DESC": {
|
||||
"LABEL": "Description",
|
||||
"PLACEHOLDER": "Enter rule description",
|
||||
"ERROR": "Description is required"
|
||||
},
|
||||
"EVENT": {
|
||||
"LABEL": "Event",
|
||||
"PLACEHOLDER": "Please select one",
|
||||
"ERROR": "Event is required"
|
||||
},
|
||||
"CONDITIONS": {
|
||||
"LABEL": "Conditions"
|
||||
},
|
||||
"ACTIONS": {
|
||||
"LABEL": "Actions"
|
||||
}
|
||||
},
|
||||
"CONDITION_BUTTON_LABEL": "Add Condition",
|
||||
"ACTION_BUTTON_LABEL": "Add Action",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Automation rule added successfully",
|
||||
"ERROR_MESSAGE": "Could not able to create a automation rule, Please try again later"
|
||||
}
|
||||
},
|
||||
"LIST": {
|
||||
"TABLE_HEADER": {
|
||||
"NAME": "Name",
|
||||
"DESCRIPTION": "Description",
|
||||
"ACTIVE": "Active",
|
||||
"CREATED_ON": "Created on"
|
||||
},
|
||||
"404": "No automation rules found"
|
||||
},
|
||||
"DELETE": {
|
||||
"TITLE": "Delete Automation Rule",
|
||||
"SUBMIT": "Delete",
|
||||
"CANCEL_BUTTON_TEXT": "Cancel",
|
||||
"CONFIRM": {
|
||||
"TITLE": "Confirm Deletion",
|
||||
"MESSAGE": "Are you sure to delete ",
|
||||
"YES": "Yes, Delete ",
|
||||
"NO": "No, Keep "
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Automation rule deleted successfully",
|
||||
"ERROR_MESSAGE": "Could not able to delete a automation rule, Please try again later"
|
||||
}
|
||||
},
|
||||
"EDIT": {
|
||||
"TITLE": "Edit Automation Rule",
|
||||
"SUBMIT": "Update",
|
||||
"CANCEL_BUTTON_TEXT": "Cancel",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Automation rule updated successfully",
|
||||
"ERROR_MESSAGE": "Could not update automation rule, Please try again later"
|
||||
}
|
||||
},
|
||||
"CLONE": {
|
||||
"TOOLTIP": "Clone",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Automation cloned successfully",
|
||||
"ERROR_MESSAGE": "Could not clone automation rule, Please try again later"
|
||||
}
|
||||
},
|
||||
"FORM": {
|
||||
"EDIT": "Edit",
|
||||
"CREATE": "Create",
|
||||
"DELETE": "Delete",
|
||||
"CANCEL": "Cancel",
|
||||
"RESET_MESSAGE": "Changing event type will reset the conditions and events you have added below"
|
||||
},
|
||||
"CONDITION": {
|
||||
"DELETE_MESSAGE": "You need to have atleast one condition to save",
|
||||
"CONTACT_CUSTOM_ATTR_LABEL": "Contact Custom Attributes",
|
||||
"CONVERSATION_CUSTOM_ATTR_LABEL": "Conversation Custom Attributes"
|
||||
},
|
||||
"ACTION": {
|
||||
"DELETE_MESSAGE": "You need to have atleast one action to save",
|
||||
"TEAM_MESSAGE_INPUT_PLACEHOLDER": "Enter your message here",
|
||||
"TEAM_DROPDOWN_PLACEHOLDER": "Select teams",
|
||||
"EMAIL_INPUT_PLACEHOLDER": "Enter email",
|
||||
"URL_INPUT_PLACEHOLDER": "Enter URL"
|
||||
},
|
||||
"TOGGLE": {
|
||||
"ACTIVATION_TITLE": "Activate Automation Rule",
|
||||
"DEACTIVATION_TITLE": "Deactivate Automation Rule",
|
||||
"ACTIVATION_DESCRIPTION": "This action will activate the automation rule '{automationName}'. Are you sure you want to proceed?",
|
||||
"DEACTIVATION_DESCRIPTION": "This action will deactivate the automation rule '{automationName}'. Are you sure you want to proceed?",
|
||||
"ACTIVATION_SUCCESFUL": "Automation Rule Activated Successfully",
|
||||
"DEACTIVATION_SUCCESFUL": "Automation Rule Deactivated Successfully",
|
||||
"ACTIVATION_ERROR": "Could not Activate Automation, Please try again later",
|
||||
"DEACTIVATION_ERROR": "Could not Deactivate Automation, Please try again later",
|
||||
"CONFIRMATION_LABEL": "Yes",
|
||||
"CANCEL_LABEL": "No"
|
||||
},
|
||||
"ATTACHMENT": {
|
||||
"UPLOAD_ERROR": "Could not upload attachment, Please try again",
|
||||
"LABEL_IDLE": "Upload Attachment",
|
||||
"LABEL_UPLOADING": "Uploading...",
|
||||
"LABEL_UPLOADED": "Successfully Uploaded",
|
||||
"LABEL_UPLOAD_FAILED": "Upload Failed"
|
||||
},
|
||||
"ERRORS": {
|
||||
"ATTRIBUTE_KEY_REQUIRED": "Attribute key is required",
|
||||
"FILTER_OPERATOR_REQUIRED": "Filter operator is required",
|
||||
"VALUE_REQUIRED": "Value is required",
|
||||
"VALUE_MUST_BE_BETWEEN_1_AND_998": "Value must be between 1 and 998",
|
||||
"ACTION_PARAMETERS_REQUIRED": "Action parameters are required",
|
||||
"ATLEAST_ONE_CONDITION_REQUIRED": "At least one condition is required",
|
||||
"ATLEAST_ONE_ACTION_REQUIRED": "At least one action is required"
|
||||
},
|
||||
"NONE_OPTION": "None",
|
||||
"EVENTS": {
|
||||
"CONVERSATION_CREATED": "Conversation Created",
|
||||
"CONVERSATION_UPDATED": "Conversation Updated",
|
||||
"MESSAGE_CREATED": "Message Created",
|
||||
"CONVERSATION_RESOLVED": "Conversation Resolved",
|
||||
"CONVERSATION_OPENED": "Conversation Opened"
|
||||
},
|
||||
"ACTIONS": {
|
||||
"ASSIGN_AGENT": "Assign to Agent",
|
||||
"ASSIGN_TEAM": "Assign a Team",
|
||||
"ADD_LABEL": "Add a Label",
|
||||
"REMOVE_LABEL": "Remove a Label",
|
||||
"SEND_EMAIL_TO_TEAM": "Send an Email to Team",
|
||||
"SEND_EMAIL_TRANSCRIPT": "Send an Email Transcript",
|
||||
"MUTE_CONVERSATION": "Mute Conversation",
|
||||
"SNOOZE_CONVERSATION": "Snooze Conversation",
|
||||
"RESOLVE_CONVERSATION": "Resolve Conversation",
|
||||
"SEND_WEBHOOK_EVENT": "Send Webhook Event",
|
||||
"SEND_ATTACHMENT": "Send Attachment",
|
||||
"SEND_MESSAGE": "Send a Message",
|
||||
"ADD_PRIVATE_NOTE": "Add a Private Note",
|
||||
"CHANGE_PRIORITY": "Change Priority",
|
||||
"ADD_SLA": "Add SLA",
|
||||
"OPEN_CONVERSATION": "Open conversation"
|
||||
},
|
||||
"MESSAGE_TYPES": {
|
||||
"INCOMING": "Incoming Message",
|
||||
"OUTGOING": "Outgoing Message"
|
||||
},
|
||||
"PRIORITY_TYPES": {
|
||||
"NONE": "None",
|
||||
"LOW": "Low",
|
||||
"MEDIUM": "Medium",
|
||||
"HIGH": "High",
|
||||
"URGENT": "Urgent"
|
||||
},
|
||||
"ATTRIBUTES": {
|
||||
"MESSAGE_TYPE": "Message Type",
|
||||
"MESSAGE_CONTAINS": "Message Contains",
|
||||
"EMAIL": "Email",
|
||||
"INBOX": "Inbox",
|
||||
"CONVERSATION_LANGUAGE": "Conversation Language",
|
||||
"PHONE_NUMBER": "Phone Number",
|
||||
"STATUS": "Status",
|
||||
"BROWSER_LANGUAGE": "Browser Language",
|
||||
"MAIL_SUBJECT": "Email Subject",
|
||||
"COUNTRY_NAME": "Country",
|
||||
"REFERER_LINK": "Referrer Link",
|
||||
"ASSIGNEE_NAME": "Assignee",
|
||||
"TEAM_NAME": "Team",
|
||||
"PRIORITY": "Priority",
|
||||
"LABELS": "Labels"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"BULK_ACTION": {
|
||||
"CONVERSATIONS_SELECTED": "{conversationCount} conversations selected",
|
||||
"AGENT_SELECT_LABEL": "Select agent",
|
||||
"ASSIGN_CONFIRMATION_LABEL": "Are you sure to assign {conversationCount} {conversationLabel} to",
|
||||
"UNASSIGN_CONFIRMATION_LABEL": "Are you sure to unassign {conversationCount} {conversationLabel}?",
|
||||
"GO_BACK_LABEL": "Go back",
|
||||
"ASSIGN_LABEL": "Assign",
|
||||
"YES": "Yes",
|
||||
"SEARCH_INPUT_PLACEHOLDER": "Search",
|
||||
"ASSIGN_AGENT_TOOLTIP": "Assign agent",
|
||||
"ASSIGN_TEAM_TOOLTIP": "Assign team",
|
||||
"ASSIGN_SUCCESFUL": "Conversations assigned successfully.",
|
||||
"ASSIGN_FAILED": "Failed to assign conversations. Please try again.",
|
||||
"RESOLVE_SUCCESFUL": "Conversations resolved successfully.",
|
||||
"RESOLVE_FAILED": "Failed to resolve conversations. Please try again.",
|
||||
"ALL_CONVERSATIONS_SELECTED_ALERT": "Conversations visible on this page are only selected.",
|
||||
"AGENT_LIST_LOADING": "Loading agents",
|
||||
"UPDATE": {
|
||||
"CHANGE_STATUS": "Change status",
|
||||
"SNOOZE_UNTIL": "Snooze",
|
||||
"UPDATE_SUCCESFUL": "Conversation status updated successfully.",
|
||||
"UPDATE_FAILED": "Failed to update conversations. Please try again."
|
||||
},
|
||||
"LABELS": {
|
||||
"ASSIGN_LABELS": "Assign labels",
|
||||
"NO_LABELS_FOUND": "No labels found for",
|
||||
"ASSIGN_SELECTED_LABELS": "Assign selected labels",
|
||||
"ASSIGN_SUCCESFUL": "Labels assigned successfully.",
|
||||
"ASSIGN_FAILED": "Failed to assign labels. Please try again."
|
||||
},
|
||||
"TEAMS": {
|
||||
"TEAM_SELECT_LABEL": "Select team",
|
||||
"NONE": "None",
|
||||
"NO_TEAMS_AVAILABLE": "There are no teams added to this account yet.",
|
||||
"ASSIGN_SELECTED_TEAMS": "Assign selected team.",
|
||||
"ASSIGN_SUCCESFUL": "Teams assigned successfully.",
|
||||
"ASSIGN_FAILED": "Failed to assign team. Please try again."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
{
|
||||
"CAMPAIGN": {
|
||||
"LIVE_CHAT": {
|
||||
"HEADER_TITLE": "Live chat campaigns",
|
||||
"NEW_CAMPAIGN": "Create campaign",
|
||||
"CARD": {
|
||||
"STATUS": {
|
||||
"ENABLED": "Enabled",
|
||||
"DISABLED": "Disabled"
|
||||
},
|
||||
"CAMPAIGN_DETAILS": {
|
||||
"SENT_BY": "Sent by",
|
||||
"BOT": "Bot",
|
||||
"FROM": "from",
|
||||
"URL": "URL:"
|
||||
}
|
||||
},
|
||||
"EMPTY_STATE": {
|
||||
"TITLE": "No live chat campaigns are available",
|
||||
"SUBTITLE": "Connect with your customers using proactive messages. Click 'Create campaign' to get started."
|
||||
},
|
||||
"CREATE": {
|
||||
"TITLE": "Create a live chat campaign",
|
||||
"CANCEL_BUTTON_TEXT": "Cancel",
|
||||
"CREATE_BUTTON_TEXT": "Create",
|
||||
"FORM": {
|
||||
"TITLE": {
|
||||
"LABEL": "Title",
|
||||
"PLACEHOLDER": "Please enter the title of campaign",
|
||||
"ERROR": "Title is required"
|
||||
},
|
||||
"MESSAGE": {
|
||||
"LABEL": "Message",
|
||||
"PLACEHOLDER": "Please enter the message of campaign",
|
||||
"ERROR": "Message is required"
|
||||
},
|
||||
"INBOX": {
|
||||
"LABEL": "Select Inbox",
|
||||
"PLACEHOLDER": "Select Inbox",
|
||||
"ERROR": "Inbox is required"
|
||||
},
|
||||
"SENT_BY": {
|
||||
"LABEL": "Sent by",
|
||||
"PLACEHOLDER": "Please select sender",
|
||||
"ERROR": "Sender is required"
|
||||
},
|
||||
"END_POINT": {
|
||||
"LABEL": "URL",
|
||||
"PLACEHOLDER": "Please enter the URL",
|
||||
"ERROR": "Please enter a valid URL"
|
||||
},
|
||||
"TIME_ON_PAGE": {
|
||||
"LABEL": "Time on page(Seconds)",
|
||||
"PLACEHOLDER": "Please enter the time",
|
||||
"ERROR": "Time on page is required"
|
||||
},
|
||||
"OTHER_PREFERENCES": {
|
||||
"TITLE": "Other preferences",
|
||||
"ENABLED": "Enable campaign",
|
||||
"TRIGGER_ONLY_BUSINESS_HOURS": "Trigger only during business hours"
|
||||
},
|
||||
"BUTTONS": {
|
||||
"CREATE": "Create",
|
||||
"CANCEL": "Cancel"
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Live chat campaign created successfully",
|
||||
"ERROR_MESSAGE": "There was an error. Please try again."
|
||||
}
|
||||
}
|
||||
},
|
||||
"EDIT": {
|
||||
"TITLE": "Edit live chat campaign",
|
||||
"FORM": {
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Live chat campaign updated successfully",
|
||||
"ERROR_MESSAGE": "There was an error. Please try again."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"SMS": {
|
||||
"HEADER_TITLE": "SMS campaigns",
|
||||
"NEW_CAMPAIGN": "Create campaign",
|
||||
"EMPTY_STATE": {
|
||||
"TITLE": "No SMS campaigns are available",
|
||||
"SUBTITLE": "Launch an SMS campaign to reach your customers directly. Send offers or make announcements with ease. Click 'Create campaign' to get started."
|
||||
},
|
||||
"CARD": {
|
||||
"STATUS": {
|
||||
"COMPLETED": "Completed",
|
||||
"SCHEDULED": "Scheduled"
|
||||
},
|
||||
"CAMPAIGN_DETAILS": {
|
||||
"SENT_FROM": "Sent from",
|
||||
"ON": "on"
|
||||
}
|
||||
},
|
||||
"CREATE": {
|
||||
"TITLE": "Create SMS campaign",
|
||||
"CANCEL_BUTTON_TEXT": "Cancel",
|
||||
"CREATE_BUTTON_TEXT": "Create",
|
||||
"FORM": {
|
||||
"TITLE": {
|
||||
"LABEL": "Title",
|
||||
"PLACEHOLDER": "Please enter the title of campaign",
|
||||
"ERROR": "Title is required"
|
||||
},
|
||||
"MESSAGE": {
|
||||
"LABEL": "Message",
|
||||
"PLACEHOLDER": "Please enter the message of campaign",
|
||||
"ERROR": "Message is required"
|
||||
},
|
||||
"INBOX": {
|
||||
"LABEL": "Select Inbox",
|
||||
"PLACEHOLDER": "Select Inbox",
|
||||
"ERROR": "Inbox is required"
|
||||
},
|
||||
"AUDIENCE": {
|
||||
"LABEL": "Audience",
|
||||
"PLACEHOLDER": "Select the customer labels",
|
||||
"ERROR": "Audience is required"
|
||||
},
|
||||
"SCHEDULED_AT": {
|
||||
"LABEL": "Scheduled time",
|
||||
"PLACEHOLDER": "Please select the time",
|
||||
"ERROR": "Scheduled time is required"
|
||||
},
|
||||
"BUTTONS": {
|
||||
"CREATE": "Create",
|
||||
"CANCEL": "Cancel"
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "SMS campaign created successfully",
|
||||
"ERROR_MESSAGE": "There was an error. Please try again."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"WHATSAPP": {
|
||||
"HEADER_TITLE": "WhatsApp campaigns",
|
||||
"NEW_CAMPAIGN": "Create campaign",
|
||||
"EMPTY_STATE": {
|
||||
"TITLE": "No WhatsApp campaigns are available",
|
||||
"SUBTITLE": "Launch a WhatsApp campaign to reach your customers directly. Send offers or make announcements with ease. Click 'Create campaign' to get started."
|
||||
},
|
||||
"CARD": {
|
||||
"STATUS": {
|
||||
"COMPLETED": "Completed",
|
||||
"SCHEDULED": "Scheduled"
|
||||
},
|
||||
"CAMPAIGN_DETAILS": {
|
||||
"SENT_FROM": "Sent from",
|
||||
"ON": "on"
|
||||
}
|
||||
},
|
||||
"CREATE": {
|
||||
"TITLE": "Create WhatsApp campaign",
|
||||
"CANCEL_BUTTON_TEXT": "Cancel",
|
||||
"CREATE_BUTTON_TEXT": "Create",
|
||||
"FORM": {
|
||||
"TITLE": {
|
||||
"LABEL": "Title",
|
||||
"PLACEHOLDER": "Please enter the title of campaign",
|
||||
"ERROR": "Title is required"
|
||||
},
|
||||
"INBOX": {
|
||||
"LABEL": "Select Inbox",
|
||||
"PLACEHOLDER": "Select Inbox",
|
||||
"ERROR": "Inbox is required"
|
||||
},
|
||||
"TEMPLATE": {
|
||||
"LABEL": "WhatsApp Template",
|
||||
"PLACEHOLDER": "Select a template",
|
||||
"INFO": "Select a template to use for this campaign.",
|
||||
"ERROR": "Template is required",
|
||||
"PREVIEW_TITLE": "Process {templateName}",
|
||||
"LANGUAGE": "Language",
|
||||
"CATEGORY": "Category",
|
||||
"VARIABLES_LABEL": "Variables",
|
||||
"VARIABLE_PLACEHOLDER": "Enter value for {variable}"
|
||||
},
|
||||
"AUDIENCE": {
|
||||
"LABEL": "Audience",
|
||||
"PLACEHOLDER": "Select the customer labels",
|
||||
"ERROR": "Audience is required"
|
||||
},
|
||||
"SCHEDULED_AT": {
|
||||
"LABEL": "Scheduled time",
|
||||
"PLACEHOLDER": "Please select the time",
|
||||
"ERROR": "Scheduled time is required"
|
||||
},
|
||||
"BUTTONS": {
|
||||
"CREATE": "Create",
|
||||
"CANCEL": "Cancel"
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "WhatsApp campaign created successfully",
|
||||
"ERROR_MESSAGE": "There was an error. Please try again."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"CONFIRM_DELETE": {
|
||||
"TITLE": "Are you sure to delete?",
|
||||
"DESCRIPTION": "The delete action is permanent and cannot be reversed.",
|
||||
"CONFIRM": "Delete",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Campaign deleted successfully",
|
||||
"ERROR_MESSAGE": "There was an error. Please try again."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user