Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
977deceb49 |
Binary file not shown.
|
After Width: | Height: | Size: 285 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 966 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 934 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 40 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 45 KiB |
@@ -1,11 +1,22 @@
|
||||
<img src="./.github/screenshots/header.png#gh-light-mode-only" width="100%" alt="Header light mode"/>
|
||||
<img src="./.github/screenshots/header-dark.png#gh-dark-mode-only" width="100%" alt="Header dark mode"/>
|
||||
## 🚨 Note: This branch is unstable. For the stable branch's source code, please use the branch [3.x](https://github.com/chatwoot/chatwoot/tree/3.x)
|
||||
|
||||
|
||||
<img src="https://user-images.githubusercontent.com/2246121/282256557-1570674b-d142-4198-9740-69404cc6a339.png#gh-light-mode-only" width="100%" alt="Chat dashboard dark mode"/>
|
||||
<img src="https://user-images.githubusercontent.com/2246121/282256632-87f6a01b-6467-4e0e-8a93-7bbf66d03a17.png#gh-dark-mode-only" width="100%" alt="Chat dashboard"/>
|
||||
|
||||
___
|
||||
|
||||
# Chatwoot
|
||||
|
||||
The modern customer support platform, an open-source alternative to Intercom, Zendesk, Salesforce Service Cloud etc.
|
||||
Customer engagement suite, an open-source alternative to Intercom, Zendesk, Salesforce Service Cloud etc.
|
||||
<p>
|
||||
<a href="https://heroku.com/deploy?template=https://github.com/chatwoot/chatwoot/tree/master" alt="Deploy to Heroku">
|
||||
<img width="150" alt="Deploy" src="https://www.herokucdn.com/deploy/button.svg"/>
|
||||
</a>
|
||||
<a href="https://marketplace.digitalocean.com/apps/chatwoot?refcode=f2238426a2a8" alt="Deploy to DigitalOcean">
|
||||
<img width="200" alt="Deploy to DO" src="https://www.deploytodo.com/do-btn-blue.svg"/>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<a href="https://codeclimate.com/github/chatwoot/chatwoot/maintainability"><img src="https://api.codeclimate.com/v1/badges/e6e3f66332c91e5a4c0c/maintainability" alt="Maintainability"></a>
|
||||
@@ -20,71 +31,41 @@ The modern customer support platform, an open-source alternative to Intercom, Ze
|
||||
<a href="https://artifacthub.io/packages/helm/chatwoot/chatwoot"><img src="https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/artifact-hub" alt="Artifact HUB"></a>
|
||||
</p>
|
||||
|
||||
<img src="https://user-images.githubusercontent.com/2246121/282255783-ee8a50c9-f42d-4752-8201-2d59965a663d.png#gh-light-mode-only" width="100%" alt="Chat dashboard dark mode"/>
|
||||
<img src="https://user-images.githubusercontent.com/2246121/282255784-3d1994ec-d895-4ff5-ac68-d819987e1869.png#gh-dark-mode-only" width="100%" alt="Chat dashboard"/>
|
||||
|
||||
<p>
|
||||
<a href="https://heroku.com/deploy?template=https://github.com/chatwoot/chatwoot/tree/master" alt="Deploy to Heroku">
|
||||
<img width="150" alt="Deploy" src="https://www.herokucdn.com/deploy/button.svg"/>
|
||||
</a>
|
||||
<a href="https://marketplace.digitalocean.com/apps/chatwoot?refcode=f2238426a2a8" alt="Deploy to DigitalOcean">
|
||||
<img width="200" alt="Deploy to DO" src="https://www.deploytodo.com/do-btn-blue.svg"/>
|
||||
</a>
|
||||
</p>
|
||||
Chatwoot is an open-source, self-hosted customer engagement suite. Chatwoot lets you view and manage your customer data, communicate with them irrespective of which medium they use, and re-engage them based on their profile.
|
||||
|
||||
<img src="./.github/screenshots/dashboard.png#gh-light-mode-only" width="100%" alt="Chat dashboard dark mode"/>
|
||||
<img src="./.github/screenshots/dashboard-dark.png#gh-dark-mode-only" width="100%" alt="Chat dashboard"/>
|
||||
## Features
|
||||
|
||||
---
|
||||
Chatwoot supports the following conversation channels:
|
||||
|
||||
Chatwoot is the modern, open-source, and self-hosted customer support platform designed to help businesses deliver exceptional customer support experience. Built for scale and flexibility, Chatwoot gives you full control over your customer data while providing powerful tools to manage conversations across channels.
|
||||
- **Website**: Talk to your customers using our live chat widget and make use of our SDK to identify a user and provide contextual support.
|
||||
- **Facebook**: Connect your Facebook pages and start replying to the direct messages to your page.
|
||||
- **Instagram**: Connect your Instagram profile and start replying to the direct messages.
|
||||
- **Twitter**: Connect your Twitter profiles and reply to direct messages or the tweets where you are mentioned.
|
||||
- **Telegram**: Connect your Telegram bot and reply to your customers right from a single dashboard.
|
||||
- **WhatsApp**: Connect your WhatsApp business account and manage the conversation in Chatwoot.
|
||||
- **Line**: Connect your Line account and manage the conversations in Chatwoot.
|
||||
- **SMS**: Connect your Twilio SMS account and reply to the SMS queries in Chatwoot.
|
||||
- **API Channel**: Build custom communication channels using our API channel.
|
||||
- **Email**: Forward all your email queries to Chatwoot and view it in our integrated dashboard.
|
||||
|
||||
### ✨ Captain – AI Agent for Support
|
||||
And more.
|
||||
|
||||
Supercharge your support with Captain, Chatwoot’s AI agent. Captain helps automate responses, handle common queries, and reduce agent workload—ensuring customers get instant, accurate answers. With Captain, your team can focus on complex conversations while routine questions are resolved automatically. Read more about Captain [here](https://chwt.app/captain-docs).
|
||||
|
||||
### 💬 Omnichannel Support Desk
|
||||
|
||||
Chatwoot centralizes all customer conversations into one powerful inbox, no matter where your customers reach out from. It supports live chat on your website, email, Facebook, Instagram, Twitter, WhatsApp, Telegram, Line, SMS etc.
|
||||
|
||||
### 📚 Help center portal
|
||||
|
||||
Publish help articles, FAQs, and guides through the built-in Help Center Portal. Enable customers to find answers on their own, reduce repetitive queries, and keep your support team focused on more complex issues.
|
||||
|
||||
### 🗂️ Other features
|
||||
|
||||
#### Collaboration & Productivity
|
||||
|
||||
- Private Notes and @mentions for internal team discussions.
|
||||
- Labels to organize and categorize conversations.
|
||||
- Keyboard Shortcuts and a Command Bar for quick navigation.
|
||||
- Canned Responses to reply faster to frequently asked questions.
|
||||
- Auto-Assignment to route conversations based on agent availability.
|
||||
- Multi-lingual Support to serve customers in multiple languages.
|
||||
- Custom Views and Filters for better inbox organization.
|
||||
- Business Hours and Auto-Responders to manage response expectations.
|
||||
- Teams and Automation tools for scaling support workflows.
|
||||
- Agent Capacity Management to balance workload across the team.
|
||||
|
||||
#### Customer Data & Segmentation
|
||||
- Contact Management with profiles and interaction history.
|
||||
- Contact Segments and Notes for targeted communication.
|
||||
- Campaigns to proactively engage customers.
|
||||
- Custom Attributes for storing additional customer data.
|
||||
- Pre-Chat Forms to collect user information before starting conversations.
|
||||
|
||||
#### Integrations
|
||||
- Slack Integration to manage conversations directly from Slack.
|
||||
- Dialogflow Integration for chatbot automation.
|
||||
- Dashboard Apps to embed internal tools within Chatwoot.
|
||||
- Shopify Integration to view and manage customer orders right within Chatwoot.
|
||||
- Use Google Translate to translate messages from your customers in realtime.
|
||||
- Create and manage Linear tickets within Chatwoot.
|
||||
|
||||
#### Reports & Insights
|
||||
- Live View of ongoing conversations for real-time monitoring.
|
||||
- Conversation, Agent, Inbox, Label, and Team Reports for operational visibility.
|
||||
- CSAT Reports to measure customer satisfaction.
|
||||
- Downloadable Reports for offline analysis and reporting.
|
||||
Other features include:
|
||||
|
||||
- **CRM**: Save all your customer information right inside Chatwoot, use contact notes to log emails, phone calls, or meeting notes.
|
||||
- **Custom Attributes**: Define custom attribute attributes to store information about a contact or a conversation and extend the product to match your workflow.
|
||||
- **Shared multi-brand inboxes**: Manage multiple brands or pages using a shared inbox.
|
||||
- **Private notes**: Use @mentions and private notes to communicate internally about a conversation.
|
||||
- **Canned responses (Saved replies)**: Improve the response rate by adding saved replies for frequently asked questions.
|
||||
- **Conversation Labels**: Use conversation labels to create custom workflows.
|
||||
- **Auto assignment**: Chatwoot intelligently assigns a ticket to the agents who have access to the inbox depending on their availability and load.
|
||||
- **Conversation continuity**: If the user has provided an email address through the chat widget, Chatwoot will send an email to the customer under the agent name so that the user can continue the conversation over the email.
|
||||
- **Multi-lingual support**: Chatwoot supports 10+ languages.
|
||||
- **Powerful API & Webhooks**: Extend the capability of the software using Chatwoot’s webhooks and APIs.
|
||||
- **Integrations**: Chatwoot natively integrates with Slack right now. Manage your conversations in Slack without logging into the dashboard.
|
||||
|
||||
## Documentation
|
||||
|
||||
@@ -126,11 +107,13 @@ For other supported options, checkout our [deployment page](https://chatwoot.com
|
||||
|
||||
Looking to report a vulnerability? Please refer our [SECURITY.md](./SECURITY.md) file.
|
||||
|
||||
## Community
|
||||
|
||||
## Community? Questions? Support ?
|
||||
|
||||
If you need help or just want to hang out, come, say hi on our [Discord](https://discord.gg/cJXdrwS) server.
|
||||
|
||||
## Contributors
|
||||
|
||||
## Contributors ✨
|
||||
|
||||
Thanks goes to all these [wonderful people](https://www.chatwoot.com/docs/contributors):
|
||||
|
||||
|
||||
@@ -12,11 +12,50 @@ class ContactInboxBuilder
|
||||
private
|
||||
|
||||
def generate_source_id
|
||||
ContactInbox::SourceIdService.new(
|
||||
contact: @contact,
|
||||
channel_type: @inbox.channel_type,
|
||||
medium: @inbox.channel.try(:medium)
|
||||
).generate
|
||||
case @inbox.channel_type
|
||||
when 'Channel::TwilioSms'
|
||||
twilio_source_id
|
||||
when 'Channel::Whatsapp'
|
||||
wa_source_id
|
||||
when 'Channel::Email'
|
||||
email_source_id
|
||||
when 'Channel::Sms'
|
||||
phone_source_id
|
||||
when 'Channel::Api', 'Channel::WebWidget'
|
||||
SecureRandom.uuid
|
||||
else
|
||||
raise "Unsupported operation for this channel: #{@inbox.channel_type}"
|
||||
end
|
||||
end
|
||||
|
||||
def email_source_id
|
||||
raise ActionController::ParameterMissing, 'contact email' unless @contact.email
|
||||
|
||||
@contact.email
|
||||
end
|
||||
|
||||
def phone_source_id
|
||||
raise ActionController::ParameterMissing, 'contact phone number' unless @contact.phone_number
|
||||
|
||||
@contact.phone_number
|
||||
end
|
||||
|
||||
def wa_source_id
|
||||
raise ActionController::ParameterMissing, 'contact phone number' unless @contact.phone_number
|
||||
|
||||
# whatsapp doesn't want the + in e164 format
|
||||
@contact.phone_number.delete('+').to_s
|
||||
end
|
||||
|
||||
def twilio_source_id
|
||||
raise ActionController::ParameterMissing, 'contact phone number' unless @contact.phone_number
|
||||
|
||||
case @inbox.channel.medium
|
||||
when 'sms'
|
||||
@contact.phone_number
|
||||
when 'whatsapp'
|
||||
"whatsapp:#{@contact.phone_number}"
|
||||
end
|
||||
end
|
||||
|
||||
def create_contact_inbox
|
||||
@@ -25,40 +64,5 @@ class ContactInboxBuilder
|
||||
inbox_id: @inbox.id,
|
||||
source_id: @source_id
|
||||
)
|
||||
rescue ActiveRecord::RecordNotUnique
|
||||
Rails.logger.info("[ContactInboxBuilder] RecordNotUnique #{@source_id} #{@contact.id} #{@inbox.id}")
|
||||
update_old_contact_inbox
|
||||
retry
|
||||
end
|
||||
|
||||
def update_old_contact_inbox
|
||||
# The race condition occurs when there’s a contact inbox with the
|
||||
# same source ID but linked to a different contact. This can happen
|
||||
# if the agent updates the contact’s email or phone number, or
|
||||
# if the contact is merged with another.
|
||||
#
|
||||
# We update the old contact inbox source_id to a random value to
|
||||
# avoid disrupting the current flow. However, the root cause of
|
||||
# this issue is a flaw in the contact inbox model design.
|
||||
# Contact inbox is essentially tracking a session and is not
|
||||
# needed for non-live chat channels.
|
||||
raise ActiveRecord::RecordNotUnique unless allowed_channels?
|
||||
|
||||
contact_inbox = ::ContactInbox.find_by(inbox_id: @inbox.id, source_id: @source_id)
|
||||
return if contact_inbox.blank?
|
||||
|
||||
contact_inbox.update!(source_id: new_source_id)
|
||||
end
|
||||
|
||||
def new_source_id
|
||||
if @inbox.whatsapp? || @inbox.sms? || @inbox.twilio?
|
||||
"#{@source_id}#{rand(100)}"
|
||||
else
|
||||
"#{rand(10)}#{@source_id}"
|
||||
end
|
||||
end
|
||||
|
||||
def allowed_channels?
|
||||
@inbox.email? || @inbox.sms? || @inbox.twilio? || @inbox.whatsapp?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -9,8 +9,6 @@ class Api::V1::Accounts::Contacts::ContactInboxesController < Api::V1::Accounts:
|
||||
source_id: params[:source_id],
|
||||
hmac_verified: hmac_verified?
|
||||
).perform
|
||||
rescue ArgumentError => e
|
||||
render json: { error: e.message }, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -15,7 +15,7 @@ class DashboardController < ActionController::Base
|
||||
private
|
||||
|
||||
def ensure_html_format
|
||||
head :not_acceptable unless request.format.html?
|
||||
head :not_acceptable if request.format.json?
|
||||
end
|
||||
|
||||
def set_global_config
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
@apply inline-block h-6 py-0 px-6 relative align-middle w-6;
|
||||
|
||||
&.message {
|
||||
@include normal-shadow;
|
||||
@apply bg-white dark:bg-slate-800 rounded-full left-0 my-3 mx-auto p-4 top-0;
|
||||
|
||||
&::before {
|
||||
|
||||
@@ -1,7 +1,79 @@
|
||||
@import 'dashboard/assets/scss/variables';
|
||||
@import 'widget/assets/scss/mixins';
|
||||
|
||||
$spinner-before-border-color: rgba(255, 255, 255, 0.7);
|
||||
|
||||
//borders
|
||||
@mixin border-nil() {
|
||||
border-color: transparent;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
@mixin thin-border($color) {
|
||||
border: 1px solid $color;
|
||||
}
|
||||
|
||||
@mixin custom-border-bottom($size, $color) {
|
||||
border-bottom: $size solid $color;
|
||||
}
|
||||
|
||||
@mixin custom-border-top($size, $color) {
|
||||
border-top: $size solid $color;
|
||||
}
|
||||
|
||||
@mixin border-normal() {
|
||||
@apply border border-slate-50 dark:border-slate-700;
|
||||
}
|
||||
|
||||
@mixin border-normal-left() {
|
||||
@apply border-l border-slate-50 dark:border-slate-700;
|
||||
}
|
||||
|
||||
@mixin border-normal-top() {
|
||||
@apply border-t border-slate-50 dark:border-slate-700;
|
||||
}
|
||||
|
||||
@mixin border-normal-right() {
|
||||
@apply border-r border-slate-50 dark:border-slate-700;
|
||||
}
|
||||
|
||||
@mixin border-normal-bottom() {
|
||||
@apply border-b border-slate-50 dark:border-slate-700;
|
||||
}
|
||||
|
||||
@mixin border-light() {
|
||||
@apply border border-slate-25 dark:border-slate-700;
|
||||
}
|
||||
|
||||
@mixin border-light-left() {
|
||||
@apply border-l border-slate-25 dark:border-slate-700;
|
||||
}
|
||||
|
||||
@mixin border-light-top() {
|
||||
@apply border-t border-slate-25 dark:border-slate-700;
|
||||
}
|
||||
|
||||
@mixin border-light-right() {
|
||||
@apply border-r border-slate-25 dark:border-slate-700;
|
||||
}
|
||||
|
||||
@mixin border-light-bottom() {
|
||||
@apply border-b border-slate-25 dark:border-slate-700;
|
||||
}
|
||||
|
||||
// background
|
||||
@mixin background-gray() {
|
||||
background: $color-background;
|
||||
}
|
||||
|
||||
@mixin background-light() {
|
||||
@apply bg-slate-50 dark:bg-slate-800;
|
||||
}
|
||||
|
||||
@mixin background-white() {
|
||||
@apply bg-white dark:bg-slate-900;
|
||||
}
|
||||
|
||||
// input form
|
||||
@mixin ghost-input() {
|
||||
box-shadow: none;
|
||||
@@ -15,6 +87,65 @@ $spinner-before-border-color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
}
|
||||
|
||||
// flex-layout
|
||||
@mixin space-between() {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
@mixin space-between-column() {
|
||||
@include space-between;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@mixin space-between-row() {
|
||||
@include space-between;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
@mixin flex-shrink() {
|
||||
flex: 0 0 auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
@mixin flex-weight($value) {
|
||||
// Grab flex-grow for older browsers.
|
||||
$flex-grow: nth($value, 1);
|
||||
|
||||
// 2009
|
||||
@include prefixer(box-flex, $flex-grow, webkit moz spec);
|
||||
|
||||
// 2011 (IE 10), 2012
|
||||
@include prefixer(flex, $value, webkit moz ms spec);
|
||||
}
|
||||
|
||||
// full height
|
||||
@mixin full-height() {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@mixin round-corner() {
|
||||
border-radius: 1000px;
|
||||
}
|
||||
|
||||
@mixin scroll-on-hover() {
|
||||
overflow: hidden;
|
||||
|
||||
&:hover {
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@mixin horizontal-scroll() {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
@mixin elegant-card() {
|
||||
@include normal-shadow;
|
||||
border-radius: $space-small;
|
||||
}
|
||||
|
||||
@mixin color-spinner() {
|
||||
@keyframes spinner {
|
||||
to {
|
||||
@@ -99,3 +230,17 @@ $spinner-before-border-color: rgba(255, 255, 255, 0.7);
|
||||
border-left: $size solid transparent;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin text-ellipsis {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@mixin three-column-grid($column-one-width: 16rem,
|
||||
$column-three-width: 16rem) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: minmax($column-one-width, 6fr) 10fr minmax($column-three-width, 6fr);
|
||||
}
|
||||
|
||||
+1
-1
@@ -55,13 +55,13 @@ defineExpose({ dialogRef, contactsFormRef, onSuccess });
|
||||
@click="closeDialog"
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
:label="
|
||||
t('CONTACTS_LAYOUT.HEADER.ACTIONS.CONTACT_CREATION.SAVE_CONTACT')
|
||||
"
|
||||
color="blue"
|
||||
:disabled="contactsFormRef?.isFormInvalid"
|
||||
:is-loading="isCreatingContact"
|
||||
@click="handleDialogConfirm"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
-5
@@ -25,11 +25,6 @@ export const generateLabelForContactableInboxesList = ({
|
||||
channelType === INBOX_TYPES.TWILIO ||
|
||||
channelType === INBOX_TYPES.WHATSAPP
|
||||
) {
|
||||
// Handled separately for Twilio Inbox where phone number is not mandatory.
|
||||
// You can send message to a contact with Messaging Service Id.
|
||||
if (!phoneNumber) {
|
||||
return name;
|
||||
}
|
||||
return `${name} (${phoneNumber})`;
|
||||
}
|
||||
return name;
|
||||
|
||||
+6
-13
@@ -8,8 +8,8 @@ vi.mock('dashboard/api/contacts');
|
||||
describe('composeConversationHelper', () => {
|
||||
describe('generateLabelForContactableInboxesList', () => {
|
||||
const contact = {
|
||||
name: 'Priority Inbox',
|
||||
email: 'hello@example.com',
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
phoneNumber: '+1234567890',
|
||||
};
|
||||
|
||||
@@ -19,7 +19,7 @@ describe('composeConversationHelper', () => {
|
||||
...contact,
|
||||
channelType: INBOX_TYPES.EMAIL,
|
||||
})
|
||||
).toBe('Priority Inbox (hello@example.com)');
|
||||
).toBe('John Doe (john@example.com)');
|
||||
});
|
||||
|
||||
it('generates label for twilio inbox', () => {
|
||||
@@ -28,14 +28,7 @@ describe('composeConversationHelper', () => {
|
||||
...contact,
|
||||
channelType: INBOX_TYPES.TWILIO,
|
||||
})
|
||||
).toBe('Priority Inbox (+1234567890)');
|
||||
|
||||
expect(
|
||||
helpers.generateLabelForContactableInboxesList({
|
||||
name: 'Priority Inbox',
|
||||
channelType: INBOX_TYPES.TWILIO,
|
||||
})
|
||||
).toBe('Priority Inbox');
|
||||
).toBe('John Doe (+1234567890)');
|
||||
});
|
||||
|
||||
it('generates label for whatsapp inbox', () => {
|
||||
@@ -44,7 +37,7 @@ describe('composeConversationHelper', () => {
|
||||
...contact,
|
||||
channelType: INBOX_TYPES.WHATSAPP,
|
||||
})
|
||||
).toBe('Priority Inbox (+1234567890)');
|
||||
).toBe('John Doe (+1234567890)');
|
||||
});
|
||||
|
||||
it('generates label for other inbox types', () => {
|
||||
@@ -53,7 +46,7 @@ describe('composeConversationHelper', () => {
|
||||
...contact,
|
||||
channelType: 'Channel::Api',
|
||||
})
|
||||
).toBe('Priority Inbox');
|
||||
).toBe('John Doe');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -190,7 +190,7 @@ const STYLE_CONFIG = {
|
||||
const variantClasses = computed(() => {
|
||||
const variantMap = {
|
||||
ghost: `${STYLE_CONFIG.colors[computedColor.value].ghost}`,
|
||||
link: `${STYLE_CONFIG.colors[computedColor.value].link} p-0 font-medium underline-offset-2`,
|
||||
link: `${STYLE_CONFIG.colors[computedColor.value].link} p-0 font-medium underline-offset-4`,
|
||||
outline: STYLE_CONFIG.colors[computedColor.value].outline,
|
||||
faded: STYLE_CONFIG.colors[computedColor.value].faded,
|
||||
solid: STYLE_CONFIG.colors[computedColor.value].solid,
|
||||
|
||||
@@ -9,15 +9,12 @@ import { getRegexp } from 'shared/helpers/Validators';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
const DATE_FORMAT = 'yyyy-MM-dd';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MultiselectDropdown,
|
||||
HelperTextPopup,
|
||||
NextButton,
|
||||
},
|
||||
props: {
|
||||
label: { type: String, required: true },
|
||||
@@ -222,13 +219,14 @@ export default {
|
||||
class="mt-0.5"
|
||||
/>
|
||||
</span>
|
||||
<NextButton
|
||||
<woot-button
|
||||
v-if="showActions && value"
|
||||
v-tooltip.left="$t('CUSTOM_ATTRIBUTES.ACTIONS.DELETE')"
|
||||
slate
|
||||
sm
|
||||
link
|
||||
icon="i-lucide-trash-2"
|
||||
variant="link"
|
||||
size="medium"
|
||||
color-scheme="secondary"
|
||||
icon="delete"
|
||||
class-names="flex justify-end !w-fit"
|
||||
@click="onDelete"
|
||||
/>
|
||||
</div>
|
||||
@@ -248,10 +246,10 @@ export default {
|
||||
@keyup.enter="onUpdate"
|
||||
/>
|
||||
<div>
|
||||
<NextButton
|
||||
sm
|
||||
icon="i-lucide-check"
|
||||
class="ltr:rounded-l-none rtl:rounded-r-none h-[34px]"
|
||||
<woot-button
|
||||
size="small"
|
||||
icon="checkmark"
|
||||
class="ltr:rounded-l-none rtl:rounded-r-none"
|
||||
@click="onUpdate"
|
||||
/>
|
||||
</div>
|
||||
@@ -283,27 +281,25 @@ export default {
|
||||
>
|
||||
{{ displayValue || '---' }}
|
||||
</p>
|
||||
<div
|
||||
class="flex items-center max-w-[2rem] gap-1 ml-1 rtl:mr-1 rtl:ml-0"
|
||||
>
|
||||
<NextButton
|
||||
<div class="flex max-w-[2rem] gap-1 ml-1 rtl:mr-1 rtl:ml-0">
|
||||
<woot-button
|
||||
v-if="showActions && value"
|
||||
v-tooltip="$t('CUSTOM_ATTRIBUTES.ACTIONS.COPY')"
|
||||
xs
|
||||
slate
|
||||
ghost
|
||||
icon="i-lucide-clipboard"
|
||||
class="hidden group-hover:flex flex-shrink-0"
|
||||
variant="link"
|
||||
size="small"
|
||||
color-scheme="secondary"
|
||||
icon="clipboard"
|
||||
class-names="hidden group-hover:flex !w-6 flex-shrink-0"
|
||||
@click="onCopy"
|
||||
/>
|
||||
<NextButton
|
||||
<woot-button
|
||||
v-if="showActions"
|
||||
v-tooltip.right="$t('CUSTOM_ATTRIBUTES.ACTIONS.EDIT')"
|
||||
xs
|
||||
slate
|
||||
ghost
|
||||
icon="i-lucide-pen"
|
||||
class="hidden group-hover:flex flex-shrink-0"
|
||||
variant="link"
|
||||
size="small"
|
||||
color-scheme="secondary"
|
||||
icon="edit"
|
||||
class-names="hidden group-hover:flex !w-6 flex-shrink-0"
|
||||
@click="onEdit"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
<script>
|
||||
import DatePicker from 'vue-datepicker-next';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DatePicker,
|
||||
NextButton,
|
||||
},
|
||||
emits: ['close', 'chooseTime'],
|
||||
|
||||
@@ -54,23 +52,18 @@ export default {
|
||||
v-model:value="snoozeTime"
|
||||
type="datetime"
|
||||
inline
|
||||
input-class="mx-input "
|
||||
input-class="mx-input reset-base"
|
||||
:lang="lang"
|
||||
:disabled-date="disabledDate"
|
||||
:disabled-time="disabledTime"
|
||||
/>
|
||||
<div class="flex flex-row justify-end w-full gap-2 px-0 py-2">
|
||||
<NextButton
|
||||
faded
|
||||
slate
|
||||
type="reset"
|
||||
:label="$t('CONVERSATION.CUSTOM_SNOOZE.CANCEL')"
|
||||
@click.prevent="onClose"
|
||||
/>
|
||||
<NextButton
|
||||
type="submit"
|
||||
:label="$t('CONVERSATION.CUSTOM_SNOOZE.APPLY')"
|
||||
/>
|
||||
<woot-button variant="clear" @click.prevent="onClose">
|
||||
{{ $t('CONVERSATION.CUSTOM_SNOOZE.CANCEL') }}
|
||||
</woot-button>
|
||||
<woot-button>
|
||||
{{ $t('CONVERSATION.CUSTOM_SNOOZE.APPLY') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// [TODO] Use Teleport to move the modal to the end of the body
|
||||
import { ref, computed, defineEmits, onMounted } from 'vue';
|
||||
import { useEventListener } from '@vueuse/core';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
const { modalType, closeOnBackdropClick, onClose } = defineProps({
|
||||
closeOnBackdropClick: { type: Boolean, default: true },
|
||||
@@ -86,11 +85,11 @@ onMounted(() => {
|
||||
@mouse.stop
|
||||
@mousedown="event => event.stopPropagation()"
|
||||
>
|
||||
<Button
|
||||
<woot-button
|
||||
v-if="showCloseButton"
|
||||
ghost
|
||||
slate
|
||||
icon="i-lucide-x"
|
||||
color-scheme="secondary"
|
||||
icon="dismiss"
|
||||
variant="clear"
|
||||
class="absolute z-10 ltr:right-2 rtl:left-2 top-2"
|
||||
@click="close"
|
||||
/>
|
||||
|
||||
@@ -11,8 +11,6 @@ import {
|
||||
} from 'dashboard/helper/routeHelpers';
|
||||
import { useEventListener } from '@vueuse/core';
|
||||
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
const route = useRoute();
|
||||
|
||||
@@ -109,31 +107,34 @@ onBeforeUnmount(() => {
|
||||
|
||||
<template>
|
||||
<transition name="network-notification-fade" tag="div">
|
||||
<div v-show="showNotification" class="fixed z-50 top-2 left-2 group">
|
||||
<div v-show="showNotification" class="fixed z-50 top-4 left-2 group">
|
||||
<div
|
||||
class="relative flex items-center justify-between w-full px-2 py-1 bg-n-amber-4 dark:bg-n-amber-8 rounded-lg shadow-lg"
|
||||
class="relative flex items-center justify-between w-full px-2 py-1 bg-yellow-200 rounded-lg shadow-lg dark:bg-yellow-700"
|
||||
>
|
||||
<fluent-icon :icon="iconName" class="text-n-amber-12" size="18" />
|
||||
<span class="px-2 text-xs font-medium tracking-wide text-n-amber-12">
|
||||
<fluent-icon
|
||||
:icon="iconName"
|
||||
class="text-yellow-700/50 dark:text-yellow-50"
|
||||
size="18"
|
||||
/>
|
||||
<span
|
||||
class="px-2 text-xs font-medium tracking-wide text-yellow-700/70 dark:text-yellow-50"
|
||||
>
|
||||
{{ bannerText }}
|
||||
</span>
|
||||
<Button
|
||||
<woot-button
|
||||
v-if="canRefresh"
|
||||
ghost
|
||||
sm
|
||||
amber
|
||||
icon="i-lucide-refresh-ccw"
|
||||
:title="$t('NETWORK.BUTTON.REFRESH')"
|
||||
class="!text-n-amber-12 dark:!text-n-amber-9"
|
||||
variant="clear"
|
||||
size="small"
|
||||
color-scheme="warning"
|
||||
icon="arrow-clockwise"
|
||||
@click="refreshPage"
|
||||
/>
|
||||
|
||||
<Button
|
||||
ghost
|
||||
sm
|
||||
amber
|
||||
icon="i-lucide-x"
|
||||
class="!text-n-amber-12 dark:!text-n-amber-9"
|
||||
<woot-button
|
||||
variant="clear"
|
||||
size="small"
|
||||
color-scheme="warning"
|
||||
icon="dismiss"
|
||||
@click="closeNotification"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
popupMessage: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
routeText: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
hasCloseButton: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
closeButtonText: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
emits: ['open', 'close'],
|
||||
methods: {
|
||||
onClickOpenPath() {
|
||||
this.$emit('open');
|
||||
},
|
||||
onClickClose(e) {
|
||||
this.$emit('close', e);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="announcement-popup">
|
||||
<span v-if="popupMessage" class="popup-content">
|
||||
{{ popupMessage }}
|
||||
<span v-if="routeText" class="route-url" @click="onClickOpenPath">
|
||||
{{ routeText }}
|
||||
</span>
|
||||
</span>
|
||||
<div v-if="hasCloseButton" class="popup-close">
|
||||
<woot-button
|
||||
v-if="hasCloseButton"
|
||||
color-scheme="primary"
|
||||
variant="link"
|
||||
size="small"
|
||||
@click="onClickClose"
|
||||
>
|
||||
{{ closeButtonText }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.announcement-popup {
|
||||
max-width: 15rem;
|
||||
min-width: 10rem;
|
||||
display: flex;
|
||||
position: absolute;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
height: fit-content;
|
||||
background: var(--white);
|
||||
padding: 0 var(--space-normal);
|
||||
z-index: var(--z-index-much-higher);
|
||||
box-shadow: var(--b-200) 4px 4px 16px 4px;
|
||||
border-radius: var(--border-radius-normal);
|
||||
|
||||
.popup-content {
|
||||
font-size: var(--font-size-mini);
|
||||
color: var(--s-700);
|
||||
padding: var(--space-one) 0;
|
||||
.route-url {
|
||||
font-size: var(--font-size-mini);
|
||||
color: var(--s-600);
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.popup-close {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: var(--space-one) 0;
|
||||
border-top: 1px solid var(--color-border-light);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -2,12 +2,10 @@
|
||||
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||
import { useAI } from 'dashboard/composables/useAI';
|
||||
import AILoader from './AILoader.vue';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AILoader,
|
||||
NextButton,
|
||||
},
|
||||
props: {
|
||||
aiOption: {
|
||||
@@ -86,22 +84,16 @@ export default {
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row justify-end w-full gap-2 px-0 py-2">
|
||||
<NextButton
|
||||
faded
|
||||
slate
|
||||
type="reset"
|
||||
:label="
|
||||
<woot-button variant="clear" @click.prevent="onClose">
|
||||
{{
|
||||
$t('INTEGRATION_SETTINGS.OPEN_AI.ASSISTANCE_MODAL.BUTTONS.CANCEL')
|
||||
"
|
||||
@click.prevent="onClose"
|
||||
/>
|
||||
<NextButton
|
||||
type="submit"
|
||||
:disabled="!generatedContent"
|
||||
:label="
|
||||
}}
|
||||
</woot-button>
|
||||
<woot-button :disabled="!generatedContent">
|
||||
{{
|
||||
$t('INTEGRATION_SETTINGS.OPEN_AI.ASSISTANCE_MODAL.BUTTONS.APPLY')
|
||||
"
|
||||
/>
|
||||
}}
|
||||
</woot-button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -6,12 +6,7 @@ import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import { useAI } from 'dashboard/composables/useAI';
|
||||
import { OPEN_AI_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NextButton,
|
||||
},
|
||||
emits: ['close'],
|
||||
|
||||
setup() {
|
||||
@@ -99,30 +94,16 @@ export default {
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-row justify-between w-full gap-2 px-0 py-2">
|
||||
<NextButton
|
||||
ghost
|
||||
type="button"
|
||||
class="!px-3"
|
||||
:label="
|
||||
$t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.BUTTONS.NEED_HELP')
|
||||
"
|
||||
@click.prevent="openOpenAIDoc"
|
||||
/>
|
||||
<woot-button variant="link" @click.prevent="openOpenAIDoc">
|
||||
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.BUTTONS.NEED_HELP') }}
|
||||
</woot-button>
|
||||
<div class="flex items-center gap-1">
|
||||
<NextButton
|
||||
faded
|
||||
slate
|
||||
type="reset"
|
||||
:label="
|
||||
$t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.BUTTONS.DISMISS')
|
||||
"
|
||||
@click.prevent="onDismiss"
|
||||
/>
|
||||
<NextButton
|
||||
type="submit"
|
||||
:disabled="v$.value.$invalid"
|
||||
:label="$t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.BUTTONS.FINISH')"
|
||||
/>
|
||||
<woot-button variant="clear" @click.prevent="onDismiss">
|
||||
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.BUTTONS.DISMISS') }}
|
||||
</woot-button>
|
||||
<woot-button :is-disabled="v$.value.$invalid">
|
||||
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.BUTTONS.FINISH') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
import { computed } from 'vue';
|
||||
import { formatBytes } from 'shared/helpers/FileHelper';
|
||||
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
const props = defineProps({
|
||||
attachments: {
|
||||
type: Array,
|
||||
@@ -75,11 +73,9 @@ const fileName = file => {
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-center">
|
||||
<Button
|
||||
ghost
|
||||
slate
|
||||
xs
|
||||
icon="i-lucide-x"
|
||||
<woot-button
|
||||
class="!w-6 !h-6 text-sm rounded-md hover:bg-slate-50 dark:hover:bg-slate-800 clear secondary"
|
||||
icon="dismiss"
|
||||
@click="onRemoveAttachment(index)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -2,14 +2,11 @@
|
||||
import AutomationActionTeamMessageInput from './AutomationActionTeamMessageInput.vue';
|
||||
import AutomationActionFileInput from './AutomationFileInput.vue';
|
||||
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AutomationActionTeamMessageInput,
|
||||
AutomationActionFileInput,
|
||||
WootMessageEditor,
|
||||
NextButton,
|
||||
},
|
||||
props: {
|
||||
modelValue: {
|
||||
@@ -175,11 +172,11 @@ export default {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<NextButton
|
||||
<woot-button
|
||||
v-if="!isMacro"
|
||||
icon="i-lucide-x"
|
||||
slate
|
||||
ghost
|
||||
icon="dismiss"
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
class="flex-shrink-0"
|
||||
@click="removeAction"
|
||||
/>
|
||||
|
||||
@@ -247,13 +247,7 @@ export default {
|
||||
:placeholder="$t('FILTER.INPUT_PLACEHOLDER')"
|
||||
/>
|
||||
</div>
|
||||
<NextButton
|
||||
icon="i-lucide-x"
|
||||
slate
|
||||
ghost
|
||||
class="flex-shrink-0"
|
||||
@click="removeFilter"
|
||||
/>
|
||||
<NextButton icon="i-lucide-x" slate ghost @click="removeFilter" />
|
||||
</div>
|
||||
<p v-if="errorMessage" class="filter-error">
|
||||
{{ errorMessage }}
|
||||
|
||||
@@ -164,6 +164,11 @@ export default {
|
||||
'is-note-mode': this.isNote,
|
||||
};
|
||||
},
|
||||
buttonClass() {
|
||||
return {
|
||||
warning: this.isNote,
|
||||
};
|
||||
},
|
||||
showAttachButton() {
|
||||
return this.showFileUpload || this.isNote;
|
||||
},
|
||||
@@ -362,15 +367,14 @@ export default {
|
||||
/>
|
||||
</div>
|
||||
<div class="right-wrap">
|
||||
<NextButton
|
||||
:label="sendButtonText"
|
||||
type="submit"
|
||||
sm
|
||||
:color="isNote ? 'amber' : 'blue'"
|
||||
:disabled="isSendDisabled"
|
||||
class="flex-shrink-0"
|
||||
<woot-button
|
||||
size="small"
|
||||
:class-names="buttonClass"
|
||||
:is-disabled="isSendDisabled"
|
||||
@click="onSend"
|
||||
/>
|
||||
>
|
||||
{{ sendButtonText }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -13,8 +13,6 @@ import { snoozedReopenTime } from 'dashboard/helper/snoozeHelpers';
|
||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||
import Linear from './linear/index.vue';
|
||||
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
BackButton,
|
||||
@@ -23,7 +21,6 @@ export default {
|
||||
Thumbnail,
|
||||
SLACardLabel,
|
||||
Linear,
|
||||
NextButton,
|
||||
},
|
||||
mixins: [inboxMixin],
|
||||
props: {
|
||||
@@ -157,13 +154,16 @@ export default {
|
||||
<div
|
||||
class="flex flex-row items-center max-w-full gap-1 p-0 m-0 w-fit"
|
||||
>
|
||||
<NextButton link slate @click.prevent="$emit('contactPanelToggle')">
|
||||
<span
|
||||
class="text-base font-medium truncate leading-tight text-n-slate-12"
|
||||
>
|
||||
<woot-button
|
||||
variant="link"
|
||||
color-scheme="secondary"
|
||||
class="[&>span]:overflow-hidden [&>span]:whitespace-nowrap [&>span]:text-ellipsis min-w-0"
|
||||
@click.prevent="$emit('contactPanelToggle')"
|
||||
>
|
||||
<span class="text-base font-medium leading-tight text-n-slate-12">
|
||||
{{ currentContact.name }}
|
||||
</span>
|
||||
</NextButton>
|
||||
</woot-button>
|
||||
<fluent-icon
|
||||
v-if="!isHMACVerified"
|
||||
v-tooltip="$t('CONVERSATION.UNVERIFIED_SESSION')"
|
||||
@@ -180,13 +180,14 @@ export default {
|
||||
<span v-if="isSnoozed" class="font-medium text-n-amber-10">
|
||||
{{ snoozedDisplayText }}
|
||||
</span>
|
||||
<NextButton
|
||||
link
|
||||
xs
|
||||
blue
|
||||
:label="contactPanelToggleText"
|
||||
<woot-button
|
||||
class="p-0"
|
||||
size="small"
|
||||
variant="link"
|
||||
@click="$emit('contactPanelToggle')"
|
||||
/>
|
||||
>
|
||||
{{ contactPanelToggleText }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -25,8 +25,6 @@ import * as Sentry from '@sentry/vue';
|
||||
import { useTrack } from 'dashboard/composables';
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
BubbleActions,
|
||||
@@ -42,7 +40,6 @@ export default {
|
||||
InstagramStory,
|
||||
InstagramStoryReply,
|
||||
Spinner,
|
||||
NextButton,
|
||||
},
|
||||
props: {
|
||||
data: {
|
||||
@@ -455,12 +452,12 @@ export default {
|
||||
v-if="isFailed && !hasOneDayPassed && !isAnEmailInbox"
|
||||
class="message-failed--alert"
|
||||
>
|
||||
<NextButton
|
||||
<woot-button
|
||||
v-tooltip.top-end="$t('CONVERSATION.TRY_AGAIN')"
|
||||
ghost
|
||||
xs
|
||||
ruby
|
||||
icon="i-lucide-refresh-ccw"
|
||||
size="tiny"
|
||||
color-scheme="alert"
|
||||
variant="clear"
|
||||
icon="arrow-clockwise"
|
||||
@click="retrySendMessage"
|
||||
/>
|
||||
</div>
|
||||
@@ -663,10 +660,10 @@ export default {
|
||||
}
|
||||
|
||||
&.is-failed {
|
||||
@apply bg-n-ruby-4 dark:bg-n-ruby-4 text-n-slate-12;
|
||||
@apply bg-red-200 dark:bg-red-200;
|
||||
|
||||
.message-text--metadata .time {
|
||||
@apply text-n-ruby-12 dark:text-n-ruby-12;
|
||||
@apply text-red-50 dark:text-red-50;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -727,7 +724,7 @@ li.right {
|
||||
}
|
||||
|
||||
.wrap.is-failed {
|
||||
@apply flex items-end ltr:ml-auto rtl:mr-auto;
|
||||
@apply flex items-end ml-auto;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+13
-13
@@ -1,11 +1,10 @@
|
||||
<script setup>
|
||||
import { useRouter } from 'vue-router';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const openProfileSettings = () => {
|
||||
return router.push({ name: 'profile_settings_index' });
|
||||
<script>
|
||||
export default {
|
||||
methods: {
|
||||
openProfileSettings() {
|
||||
return this.$router.push({ name: 'profile_settings_index' });
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -15,12 +14,13 @@ const openProfileSettings = () => {
|
||||
>
|
||||
<p class="w-fit !m-0">
|
||||
{{ $t('CONVERSATION.FOOTER.MESSAGE_SIGNATURE_NOT_CONFIGURED') }}
|
||||
|
||||
<Button
|
||||
link
|
||||
:label="$t('CONVERSATION.FOOTER.CLICK_HERE')"
|
||||
<woot-button
|
||||
color-scheme="primary"
|
||||
variant="link"
|
||||
@click="openProfileSettings"
|
||||
/>
|
||||
>
|
||||
{{ $t('CONVERSATION.FOOTER.CLICK_HERE') }}
|
||||
</woot-button>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -37,8 +37,6 @@ import wootConstants from 'dashboard/constants/globals';
|
||||
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
|
||||
import { FEATURE_FLAGS } from '../../../featureFlags';
|
||||
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Message,
|
||||
@@ -46,7 +44,6 @@ export default {
|
||||
ReplyBox,
|
||||
Banner,
|
||||
ConversationLabelSuggestion,
|
||||
NextButton,
|
||||
},
|
||||
mixins: [inboxMixin],
|
||||
props: {
|
||||
@@ -491,15 +488,13 @@ export default {
|
||||
:href-link-text="replyWindowLinkText"
|
||||
/>
|
||||
<div class="flex justify-end">
|
||||
<NextButton
|
||||
faded
|
||||
xs
|
||||
slate
|
||||
class="!rounded-r-none rtl:rotate-180 !rounded-2xl !fixed z-10"
|
||||
:icon="
|
||||
isContactPanelOpen ? 'i-ph-caret-right-fill' : 'i-ph-caret-left-fill'
|
||||
"
|
||||
<woot-button
|
||||
variant="smooth"
|
||||
size="tiny"
|
||||
color-scheme="secondary"
|
||||
class="box-border fixed z-10 bg-white border border-r-0 border-solid rounded-bl-calc rtl:rotate-180 rounded-tl-calc border-n-weak"
|
||||
:class="isInboxView ? 'top-52 md:top-40' : 'top-32'"
|
||||
:icon="isRightOrLeftIcon"
|
||||
@click="onToggleContactPanel"
|
||||
/>
|
||||
</div>
|
||||
@@ -619,6 +614,20 @@ export default {
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@tailwind components;
|
||||
|
||||
@layer components {
|
||||
.rounded-bl-calc {
|
||||
border-bottom-left-radius: calc(1.5rem + 1px);
|
||||
}
|
||||
|
||||
.rounded-tl-calc {
|
||||
border-top-left-radius: calc(1.5rem + 1px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.modal-mask {
|
||||
@apply absolute;
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
<script setup>
|
||||
<script>
|
||||
import MessagePreview from 'dashboard/components/widgets/conversation/MessagePreview.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
defineProps({
|
||||
message: {
|
||||
type: Object,
|
||||
required: true,
|
||||
export default {
|
||||
components: { MessagePreview },
|
||||
props: {
|
||||
message: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['dismiss']);
|
||||
emits: ['dismiss'],
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="reply-editor bg-n-slate-9/10 rounded-md py-1 pl-2 pr-1 text-xs tracking-wide mt-2 flex items-center gap-1.5 -mx-2"
|
||||
class="reply-editor bg-slate-50 dark:bg-slate-800 rounded-md py-1 pl-2 pr-1 text-xs tracking-wide mt-2 flex items-center gap-1.5 -mx-2"
|
||||
>
|
||||
<fluent-icon class="flex-shrink-0 icon" icon="arrow-reply" size="14" />
|
||||
<div class="flex-grow gap-1 mt-px text-xs truncate">
|
||||
@@ -26,13 +27,14 @@ const emit = defineEmits(['dismiss']);
|
||||
class="inline"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
<woot-button
|
||||
v-tooltip="$t('CONVERSATION.REPLYBOX.DISMISS_REPLY')"
|
||||
ghost
|
||||
xs
|
||||
slate
|
||||
icon="i-lucide-x"
|
||||
@click.stop="emit('dismiss')"
|
||||
color-scheme="secondary"
|
||||
icon="dismiss"
|
||||
variant="clear"
|
||||
size="tiny"
|
||||
class="flex-shrink-0"
|
||||
@click.stop="$emit('dismiss')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
+55
-29
@@ -12,12 +12,7 @@ import { ref, computed, onMounted } from 'vue';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { requiredIf } from '@vuelidate/validators';
|
||||
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NextButton,
|
||||
},
|
||||
props: {
|
||||
template: {
|
||||
type: Object,
|
||||
@@ -119,47 +114,78 @@ export default {
|
||||
readonly
|
||||
class="template-input"
|
||||
/>
|
||||
<div v-if="variables" class="p-2.5">
|
||||
<p class="text-sm font-semibold mb-2.5">
|
||||
<div v-if="variables" class="template__variables-container">
|
||||
<p class="variables-label">
|
||||
{{ $t('WHATSAPP_TEMPLATES.PARSER.VARIABLES_LABEL') }}
|
||||
</p>
|
||||
<div
|
||||
v-for="(variable, key) in processedParams"
|
||||
:key="key"
|
||||
class="items-center flex mb-2.5"
|
||||
class="template__variable-item"
|
||||
>
|
||||
<span
|
||||
class="bg-n-alpha-black2 text-n-slate-12 inline-block rounded-md text-xs py-2.5 px-6"
|
||||
>
|
||||
<span class="variable-label">
|
||||
{{ key }}
|
||||
</span>
|
||||
<woot-input
|
||||
v-model="processedParams[key]"
|
||||
type="text"
|
||||
class="flex-1 text-sm ml-2.5"
|
||||
class="variable-input"
|
||||
:styles="{ marginBottom: 0 }"
|
||||
/>
|
||||
</div>
|
||||
<p
|
||||
v-if="v$.$dirty && v$.$invalid"
|
||||
class="bg-n-ruby-9/20 rounded-md text-n-ruby-9 p-2.5 text-center"
|
||||
>
|
||||
<p v-if="v$.$dirty && v$.$invalid" class="error">
|
||||
{{ $t('WHATSAPP_TEMPLATES.PARSER.FORM_ERROR_MESSAGE') }}
|
||||
</p>
|
||||
</div>
|
||||
<footer class="flex justify-end gap-2">
|
||||
<NextButton
|
||||
faded
|
||||
slate
|
||||
type="reset"
|
||||
:label="$t('WHATSAPP_TEMPLATES.PARSER.GO_BACK_LABEL')"
|
||||
@click="resetTemplate"
|
||||
/>
|
||||
<NextButton
|
||||
type="button"
|
||||
:label="$t('WHATSAPP_TEMPLATES.PARSER.SEND_MESSAGE_LABEL')"
|
||||
@click="sendMessage"
|
||||
/>
|
||||
<footer>
|
||||
<woot-button variant="smooth" @click="resetTemplate">
|
||||
{{ $t('WHATSAPP_TEMPLATES.PARSER.GO_BACK_LABEL') }}
|
||||
</woot-button>
|
||||
<woot-button type="button" @click="sendMessage">
|
||||
{{ $t('WHATSAPP_TEMPLATES.PARSER.SEND_MESSAGE_LABEL') }}
|
||||
</woot-button>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.template__variables-container {
|
||||
@apply p-2.5;
|
||||
}
|
||||
|
||||
.variables-label {
|
||||
@apply text-sm font-semibold mb-2.5;
|
||||
}
|
||||
|
||||
.template__variable-item {
|
||||
@apply items-center flex mb-2.5;
|
||||
|
||||
.label {
|
||||
@apply text-xs;
|
||||
}
|
||||
|
||||
.variable-input {
|
||||
@apply flex-1 text-sm ml-2.5;
|
||||
}
|
||||
|
||||
.variable-label {
|
||||
@apply bg-slate-75 dark:bg-slate-700 text-slate-700 dark:text-slate-100 inline-block rounded-md text-xs py-2.5 px-6;
|
||||
}
|
||||
}
|
||||
|
||||
footer {
|
||||
@apply flex justify-end;
|
||||
|
||||
button {
|
||||
@apply ml-2.5;
|
||||
}
|
||||
}
|
||||
|
||||
.error {
|
||||
@apply bg-red-100 dark:bg-red-100 rounded-md text-red-800 dark:text-red-800 p-2.5 text-center;
|
||||
}
|
||||
|
||||
.template-input {
|
||||
@apply bg-slate-25 dark:bg-slate-900 text-slate-700 dark:text-slate-100;
|
||||
}
|
||||
</style>
|
||||
|
||||
+52
-21
@@ -43,56 +43,48 @@ export default {
|
||||
|
||||
<template>
|
||||
<div class="w-full">
|
||||
<div
|
||||
class="gap-1 bg-n-alpha-black2 items-center flex mb-2.5 py-0 px-2.5 rounded-lg outline outline-1 outline-n-weak hover:outline-n-slate-6 dark:hover:outline-n-slate-6 focus-within:outline-n-brand dark:focus-within:outline-n-brand"
|
||||
>
|
||||
<fluent-icon icon="search" class="text-n-slate-12" size="16" />
|
||||
<div class="gap-1 templates__list-search">
|
||||
<fluent-icon icon="search" class="search-icon" size="16" />
|
||||
<input
|
||||
v-model="query"
|
||||
type="search"
|
||||
:placeholder="$t('WHATSAPP_TEMPLATES.PICKER.SEARCH_PLACEHOLDER')"
|
||||
class="reset-base w-full h-9 bg-transparent text-n-slate-12 !text-sm !outline-0"
|
||||
class="templates__search-input"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="bg-n-background outline-n-container outline outline-1 rounded-lg max-h-[18.75rem] overflow-y-auto p-2.5"
|
||||
>
|
||||
<div class="template__list-container">
|
||||
<div v-for="(template, i) in filteredTemplateMessages" :key="template.id">
|
||||
<button
|
||||
class="rounded-lg cursor-pointer block p-2.5 text-left w-full hover:bg-n-alpha-2 dark:hover:bg-n-solid-2"
|
||||
class="template__list-item"
|
||||
@click="$emit('onSelect', template)"
|
||||
>
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-2.5">
|
||||
<p class="text-sm">
|
||||
<p class="label-title">
|
||||
{{ template.name }}
|
||||
</p>
|
||||
<span
|
||||
class="inline-block px-2 py-1 text-xs leading-none bg-n-slate-3 rounded-lg cursor-default text-n-slate-12"
|
||||
class="inline-block px-2 py-1 text-xs leading-none bg-white rounded-sm cursor-default dark:bg-slate-700 text-slate-800 dark:text-slate-100"
|
||||
>
|
||||
{{ $t('WHATSAPP_TEMPLATES.PICKER.LABELS.LANGUAGE') }} :
|
||||
{{ template.language }}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-medium">
|
||||
<p class="strong">
|
||||
{{ $t('WHATSAPP_TEMPLATES.PICKER.LABELS.TEMPLATE_BODY') }}
|
||||
</p>
|
||||
<p class="label-body">{{ getTemplatebody(template) }}</p>
|
||||
</div>
|
||||
<div class="mt-5">
|
||||
<p class="font-medium">
|
||||
<div class="label-category">
|
||||
<p class="strong">
|
||||
{{ $t('WHATSAPP_TEMPLATES.PICKER.LABELS.CATEGORY') }}
|
||||
</p>
|
||||
<p>{{ template.category }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<hr
|
||||
v-if="i != filteredTemplateMessages.length - 1"
|
||||
:key="`hr-${i}`"
|
||||
class="border-b border-solid border-n-weak my-2.5 mx-auto max-w-[95%]"
|
||||
/>
|
||||
<hr v-if="i != filteredTemplateMessages.length - 1" :key="`hr-${i}`" />
|
||||
</div>
|
||||
<div v-if="!filteredTemplateMessages.length">
|
||||
<p>
|
||||
@@ -105,7 +97,46 @@ export default {
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.label-body {
|
||||
font-family: monospace;
|
||||
.templates__list-search {
|
||||
@apply items-center flex bg-slate-25 dark:bg-slate-900 mb-2.5 py-0 px-2.5 rounded-md border border-solid border-slate-100 dark:border-slate-700;
|
||||
|
||||
.search-icon {
|
||||
@apply text-slate-400 dark:text-slate-300;
|
||||
}
|
||||
|
||||
.templates__search-input {
|
||||
@apply bg-transparent border-0 text-xs h-9 m-0;
|
||||
}
|
||||
}
|
||||
.template__list-container {
|
||||
@apply bg-slate-25 dark:bg-slate-900 rounded-md max-h-[18.75rem] overflow-y-auto p-2.5;
|
||||
|
||||
.template__list-item {
|
||||
@apply rounded-lg cursor-pointer block p-2.5 text-left w-full hover:bg-woot-50 dark:hover:bg-slate-800;
|
||||
|
||||
.label-title {
|
||||
@apply text-sm;
|
||||
}
|
||||
|
||||
.label-category {
|
||||
@apply mt-5;
|
||||
|
||||
span {
|
||||
@apply text-sm font-semibold;
|
||||
}
|
||||
}
|
||||
|
||||
.label-body {
|
||||
font-family: monospace;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.strong {
|
||||
@apply text-xs font-semibold;
|
||||
}
|
||||
|
||||
hr {
|
||||
@apply border-b border-solid border-slate-100 dark:border-slate-700 my-2.5 mx-auto max-w-[95%];
|
||||
}
|
||||
</style>
|
||||
|
||||
+11
-11
@@ -3,7 +3,6 @@ import { ref, computed } from 'vue';
|
||||
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import SLAEventItem from './SLAEventItem.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
const props = defineProps({
|
||||
slaMissedEvents: {
|
||||
@@ -61,19 +60,20 @@ const toggleShowAllNRT = () => {
|
||||
v-if="shouldShowMoreNRTButton"
|
||||
class="flex flex-col items-end w-full"
|
||||
>
|
||||
<Button
|
||||
link
|
||||
xs
|
||||
slate
|
||||
class="hover:!no-underline"
|
||||
:icon="!shouldShowAllNrts ? 'i-lucide-plus' : ''"
|
||||
:label="
|
||||
<woot-button
|
||||
size="small"
|
||||
:icon="!shouldShowAllNrts ? 'plus-sign' : ''"
|
||||
variant="link"
|
||||
color-scheme="secondary"
|
||||
class="hover:!no-underline !gap-1 hover:!bg-transparent dark:hover:!bg-transparent"
|
||||
@click="toggleShowAllNRT"
|
||||
>
|
||||
{{
|
||||
shouldShowAllNrts
|
||||
? $t('SLA.EVENTS.HIDE', { count: nrtMisses.length })
|
||||
: $t('SLA.EVENTS.SHOW_MORE', { count: nrtMisses.length })
|
||||
"
|
||||
@click="toggleShowAllNRT"
|
||||
/>
|
||||
}}
|
||||
</woot-button>
|
||||
</div>
|
||||
</template>
|
||||
</SLAEventItem>
|
||||
|
||||
+20
-16
@@ -2,13 +2,11 @@
|
||||
import { mapGetters } from 'vuex';
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Thumbnail,
|
||||
Spinner,
|
||||
NextButton,
|
||||
},
|
||||
props: {
|
||||
selectedInboxes: {
|
||||
@@ -96,7 +94,13 @@ export default {
|
||||
</div>
|
||||
<div class="flex items-center justify-between header">
|
||||
<span>{{ $t('BULK_ACTION.AGENT_SELECT_LABEL') }}</span>
|
||||
<NextButton ghost xs slate icon="i-lucide-x" @click="onClose" />
|
||||
<woot-button
|
||||
size="tiny"
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
icon="dismiss"
|
||||
@click="onClose"
|
||||
/>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div
|
||||
@@ -117,7 +121,7 @@ export default {
|
||||
v-model="query"
|
||||
type="search"
|
||||
:placeholder="$t('BULK_ACTION.SEARCH_INPUT_PLACEHOLDER')"
|
||||
class="reset-base !outline-0 !text-sm agent--search_input"
|
||||
class="agent--search_input"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
@@ -157,21 +161,21 @@ export default {
|
||||
}}
|
||||
</p>
|
||||
<div class="agent-confirmation-actions">
|
||||
<NextButton
|
||||
faded
|
||||
sm
|
||||
slate
|
||||
type="reset"
|
||||
:label="$t('BULK_ACTION.GO_BACK_LABEL')"
|
||||
<woot-button
|
||||
color-scheme="primary"
|
||||
variant="smooth"
|
||||
@click="goBack"
|
||||
/>
|
||||
<NextButton
|
||||
sm
|
||||
type="submit"
|
||||
:label="$t('BULK_ACTION.YES')"
|
||||
>
|
||||
{{ $t('BULK_ACTION.GO_BACK_LABEL') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
color-scheme="primary"
|
||||
variant="flat"
|
||||
:is-loading="uiFlags.isUpdating"
|
||||
@click="submit"
|
||||
/>
|
||||
>
|
||||
{{ $t('BULK_ACTION.YES') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+15
-11
@@ -1,11 +1,7 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NextButton,
|
||||
},
|
||||
emits: ['update', 'close', 'assign'],
|
||||
data() {
|
||||
return {
|
||||
@@ -44,7 +40,13 @@ export default {
|
||||
</div>
|
||||
<div class="flex items-center justify-between header">
|
||||
<span>{{ $t('BULK_ACTION.LABELS.ASSIGN_LABELS') }}</span>
|
||||
<NextButton ghost xs slate icon="i-lucide-x" @click="onClose" />
|
||||
<woot-button
|
||||
size="tiny"
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
icon="dismiss"
|
||||
@click="onClose"
|
||||
/>
|
||||
</div>
|
||||
<div class="labels-list">
|
||||
<header class="labels-list__header">
|
||||
@@ -56,7 +58,7 @@ export default {
|
||||
v-model="query"
|
||||
type="search"
|
||||
:placeholder="$t('BULK_ACTION.SEARCH_INPUT_PLACEHOLDER')"
|
||||
class="reset-base !outline-0 !text-sm label--search_input"
|
||||
class="label--search_input"
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
@@ -89,13 +91,15 @@ export default {
|
||||
</li>
|
||||
</ul>
|
||||
<footer class="labels-list__footer">
|
||||
<NextButton
|
||||
sm
|
||||
type="submit"
|
||||
:label="$t('BULK_ACTION.LABELS.ASSIGN_SELECTED_LABELS')"
|
||||
<woot-button
|
||||
size="small"
|
||||
is-expanded
|
||||
color-scheme="primary"
|
||||
:disabled="!selectedLabels.length"
|
||||
@click="$emit('assign', selectedLabels)"
|
||||
/>
|
||||
>
|
||||
<span>{{ $t('BULK_ACTION.LABELS.ASSIGN_SELECTED_LABELS') }}</span>
|
||||
</woot-button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+8
-7
@@ -1,11 +1,6 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NextButton,
|
||||
},
|
||||
emits: ['assignTeam', 'close'],
|
||||
|
||||
data() {
|
||||
@@ -45,7 +40,13 @@ export default {
|
||||
</div>
|
||||
<div class="flex items-center justify-between header">
|
||||
<span>{{ $t('BULK_ACTION.TEAMS.TEAM_SELECT_LABEL') }}</span>
|
||||
<NextButton ghost xs slate icon="i-lucide-x" @click="onClose" />
|
||||
<woot-button
|
||||
size="tiny"
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
icon="dismiss"
|
||||
@click="onClose"
|
||||
/>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="team__list-container">
|
||||
@@ -59,7 +60,7 @@ export default {
|
||||
v-model="query"
|
||||
type="search"
|
||||
:placeholder="$t('BULK_ACTION.SEARCH_INPUT_PLACEHOLDER')"
|
||||
class="reset-base !outline-0 !text-sm agent--search_input"
|
||||
class="agent--search_input"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
+18
-12
@@ -4,7 +4,6 @@ import { ref } from 'vue';
|
||||
|
||||
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
|
||||
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
const props = defineProps({
|
||||
showResolve: {
|
||||
@@ -26,9 +25,9 @@ const emit = defineEmits(['update', 'close']);
|
||||
const { t } = useI18n();
|
||||
|
||||
const actions = ref([
|
||||
{ icon: 'i-lucide-check', key: 'resolved' },
|
||||
{ icon: 'i-lucide-redo', key: 'open' },
|
||||
{ icon: 'i-lucide-alarm-clock', key: 'snoozed' },
|
||||
{ icon: 'checkmark', key: 'resolved' },
|
||||
{ icon: 'arrow-redo', key: 'open' },
|
||||
{ icon: 'send-clock', key: 'snoozed' },
|
||||
]);
|
||||
|
||||
const updateConversations = key => {
|
||||
@@ -86,21 +85,28 @@ const actionLabel = key => {
|
||||
<span class="text-sm font-medium text-slate-600 dark:text-slate-100">
|
||||
{{ $t('BULK_ACTION.UPDATE.CHANGE_STATUS') }}
|
||||
</span>
|
||||
<Button ghost xs slate icon="i-lucide-x" @click="onClose" />
|
||||
<woot-button
|
||||
size="tiny"
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
icon="dismiss"
|
||||
@click="onClose"
|
||||
/>
|
||||
</div>
|
||||
<div class="px-2.5 pt-0 pb-2.5">
|
||||
<WootDropdownMenu class="m-0 list-none">
|
||||
<template v-for="action in actions">
|
||||
<WootDropdownItem v-if="showAction(action.key)" :key="action.key">
|
||||
<Button
|
||||
ghost
|
||||
sm
|
||||
slate
|
||||
class="!w-full ltr:!justify-start rtl:!justify-end"
|
||||
<woot-button
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
size="small"
|
||||
:icon="action.icon"
|
||||
:label="actionLabel(action.key)"
|
||||
class="hover:!bg-n-slate-3 dark:hover:!bg-n-solid-3"
|
||||
@click="updateConversations(action.key)"
|
||||
/>
|
||||
>
|
||||
{{ actionLabel(action.key) }}
|
||||
</woot-button>
|
||||
</WootDropdownItem>
|
||||
</template>
|
||||
</WootDropdownMenu>
|
||||
|
||||
@@ -9,7 +9,6 @@ import validations from './validations';
|
||||
import { parseLinearAPIErrorResponse } from 'dashboard/store/utils/api';
|
||||
import SearchableDropdown from './SearchableDropdown.vue';
|
||||
import { LINEAR_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
const props = defineProps({
|
||||
conversationId: {
|
||||
@@ -249,20 +248,20 @@ onMounted(getTeams);
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center justify-end w-full gap-2 mt-8">
|
||||
<Button
|
||||
faded
|
||||
slate
|
||||
type="reset"
|
||||
:label="$t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.CANCEL')"
|
||||
<woot-button
|
||||
class="px-4 rounded-xl button clear outline-woot-200/50 outline"
|
||||
@click.prevent="onClose"
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
:label="$t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.CREATE')"
|
||||
:disabled="isSubmitDisabled"
|
||||
>
|
||||
{{ $t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.CANCEL') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
:is-disabled="isSubmitDisabled"
|
||||
class="px-4 rounded-xl"
|
||||
:is-loading="isCreating"
|
||||
@click.prevent="createIssue"
|
||||
/>
|
||||
>
|
||||
{{ $t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.CREATE') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
<script setup>
|
||||
import { inject } from 'vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
const props = defineProps({
|
||||
identifier: {
|
||||
type: String,
|
||||
@@ -39,24 +37,30 @@ const openIssue = () => {
|
||||
/>
|
||||
<span class="text-xs font-medium text-ash-900">{{ identifier }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<Button
|
||||
ghost
|
||||
xs
|
||||
slate
|
||||
icon="i-lucide-unlink"
|
||||
class="!transition-none"
|
||||
<div class="flex items-center gap-0.5">
|
||||
<woot-button
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
class="h-[24px]"
|
||||
:is-loading="isUnlinking"
|
||||
@click="unlinkIssue"
|
||||
/>
|
||||
<Button
|
||||
ghost
|
||||
xs
|
||||
slate
|
||||
class="!transition-none"
|
||||
icon="i-lucide-arrow-up-right"
|
||||
>
|
||||
<fluent-icon
|
||||
v-if="!isUnlinking"
|
||||
icon="unlink"
|
||||
size="12"
|
||||
type="outline"
|
||||
icon-lib="lucide"
|
||||
/>
|
||||
</woot-button>
|
||||
<woot-button
|
||||
variant="clear"
|
||||
class="h-[24px]"
|
||||
color-scheme="secondary"
|
||||
@click="openIssue"
|
||||
/>
|
||||
>
|
||||
<fluent-icon icon="arrow-up-right" size="14" />
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -8,7 +8,6 @@ import FilterButton from 'dashboard/components/ui/Dropdown/DropdownButton.vue';
|
||||
import FilterListDropdown from 'dashboard/components/ui/Dropdown/DropdownList.vue';
|
||||
import { parseLinearAPIErrorResponse } from 'dashboard/store/utils/api';
|
||||
import { LINEAR_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
const props = defineProps({
|
||||
conversationId: {
|
||||
@@ -130,20 +129,20 @@ const linkIssue = async () => {
|
||||
</template>
|
||||
</FilterButton>
|
||||
<div class="flex items-center justify-end w-full gap-2 mt-2">
|
||||
<Button
|
||||
faded
|
||||
slate
|
||||
type="reset"
|
||||
:label="$t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.CANCEL')"
|
||||
<woot-button
|
||||
class="px-4 rounded-xl button clear outline-woot-200/50 outline"
|
||||
@click.prevent="onClose"
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
:label="$t('INTEGRATION_SETTINGS.LINEAR.LINK.TITLE')"
|
||||
:disabled="isSubmitDisabled"
|
||||
>
|
||||
{{ $t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.CANCEL') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
:is-disabled="isSubmitDisabled"
|
||||
class="px-4 rounded-xl"
|
||||
:is-loading="isLinking"
|
||||
@click.prevent="linkIssue"
|
||||
/>
|
||||
>
|
||||
{{ $t('INTEGRATION_SETTINGS.LINEAR.LINK.TITLE') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -9,7 +9,6 @@ import Issue from './Issue.vue';
|
||||
import { useTrack } from 'dashboard/composables';
|
||||
import { LINEAR_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||
import { parseLinearAPIErrorResponse } from 'dashboard/store/utils/api';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
const props = defineProps({
|
||||
conversationId: {
|
||||
@@ -101,25 +100,23 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<div class="relative" :class="{ group: linkedIssue }">
|
||||
<Button
|
||||
<woot-button
|
||||
v-on-clickaway="closeIssue"
|
||||
v-tooltip="tooltipText"
|
||||
sm
|
||||
ghost
|
||||
slate
|
||||
class="!gap-1"
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
@click="openIssue"
|
||||
>
|
||||
<fluent-icon
|
||||
icon="linear"
|
||||
size="19"
|
||||
class="text-[#5E6AD2] flex-shrink-0"
|
||||
class="text-[#5E6AD2]"
|
||||
view-box="0 0 19 19"
|
||||
/>
|
||||
<span v-if="linkedIssue" class="text-xs font-medium text-n-slate-11">
|
||||
<span v-if="linkedIssue" class="text-xs font-medium text-ash-800">
|
||||
{{ linkedIssue.issue.identifier }}
|
||||
</span>
|
||||
</Button>
|
||||
</woot-button>
|
||||
<Issue
|
||||
v-if="linkedIssue"
|
||||
:issue="linkedIssue.issue"
|
||||
|
||||
@@ -3,12 +3,9 @@ import { required } from '@vuelidate/validators';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import Modal from '../../Modal.vue';
|
||||
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Modal,
|
||||
NextButton,
|
||||
},
|
||||
props: {
|
||||
show: { type: Boolean, default: false },
|
||||
@@ -69,20 +66,13 @@ export default {
|
||||
:placeholder="confirmPlaceHolderText"
|
||||
@blur="v$.value.$touch"
|
||||
/>
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<NextButton
|
||||
faded
|
||||
slate
|
||||
type="reset"
|
||||
:label="rejectText"
|
||||
@click.prevent="closeModal"
|
||||
/>
|
||||
<NextButton
|
||||
ruby
|
||||
type="submit"
|
||||
:label="confirmText"
|
||||
:disabled="v$.value.$invalid"
|
||||
/>
|
||||
<div class="button-wrapper">
|
||||
<woot-button color-scheme="alert" :is-disabled="v$.value.$invalid">
|
||||
{{ confirmText }}
|
||||
</woot-button>
|
||||
<woot-button class="clear" @click.prevent="closeModal">
|
||||
{{ rejectText }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
<script>
|
||||
import Modal from '../../Modal.vue';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Modal,
|
||||
NextButton,
|
||||
},
|
||||
props: {
|
||||
title: {
|
||||
@@ -57,8 +55,12 @@ export default {
|
||||
<div class="h-auto overflow-auto flex flex-col">
|
||||
<woot-modal-header :header-title="title" :header-content="description" />
|
||||
<div class="flex flex-row justify-end gap-2 py-4 px-6 w-full">
|
||||
<NextButton faded type="reset" :label="cancelLabel" @click="cancel" />
|
||||
<NextButton type="submit" :label="confirmLabel" @click="confirm" />
|
||||
<woot-button variant="clear" @click="cancel">
|
||||
{{ cancelLabel }}
|
||||
</woot-button>
|
||||
<woot-button @click="confirm">
|
||||
{{ confirmLabel }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script setup>
|
||||
import Modal from '../../Modal.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
defineProps({
|
||||
onClose: { type: Function, default: () => {} },
|
||||
@@ -23,8 +22,23 @@ const show = defineModel('show', { type: Boolean, default: false });
|
||||
:header-content-value="messageValue"
|
||||
/>
|
||||
<div class="flex items-center justify-end gap-2 p-8">
|
||||
<Button faded slate type="reset" :label="rejectText" @click="onClose" />
|
||||
<Button ruby type="submit" :label="confirmText" @click="onConfirm" />
|
||||
<woot-button variant="clear" class="action-button" @click="onClose">
|
||||
{{ rejectText }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
color-scheme="alert"
|
||||
class="action-button"
|
||||
variant="smooth"
|
||||
@click="onConfirm"
|
||||
>
|
||||
{{ confirmText }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.action-button {
|
||||
max-width: var(--space-giga);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -288,32 +288,32 @@
|
||||
"SEARCH_PLACEHOLDER": "Search...",
|
||||
"MESSAGE_BUTTON": "Messagem",
|
||||
"SEND_MESSAGE": "Enviar mensagem",
|
||||
"BLOCK_CONTACT": "Bloquear contato",
|
||||
"UNBLOCK_CONTACT": "Desbloquear contato",
|
||||
"BLOCK_CONTACT": "Block contact",
|
||||
"UNBLOCK_CONTACT": "Unblock contact",
|
||||
"BREADCRUMB": {
|
||||
"CONTACTS": "Contactos"
|
||||
},
|
||||
"ACTIONS": {
|
||||
"CONTACT_CREATION": {
|
||||
"ADD_CONTACT": "Editar contato",
|
||||
"EXPORT_CONTACT": "Exportar contatos",
|
||||
"IMPORT_CONTACT": "Importar contatos",
|
||||
"SAVE_CONTACT": "Salvar contato",
|
||||
"ADD_CONTACT": "Add contact",
|
||||
"EXPORT_CONTACT": "Export contacts",
|
||||
"IMPORT_CONTACT": "Import contacts",
|
||||
"SAVE_CONTACT": "Save contact",
|
||||
"EMAIL_ADDRESS_DUPLICATE": "O e-mail inserido já está a ser utilizado por outro contacto.",
|
||||
"PHONE_NUMBER_DUPLICATE": "Este número já está a ser usado por outro contacto.",
|
||||
"SUCCESS_MESSAGE": "Contacto guardado com sucesso",
|
||||
"ERROR_MESSAGE": "Não foi possível o contato. Por favor, tente mais tarde."
|
||||
"ERROR_MESSAGE": "Unable to save contact. Please try again later."
|
||||
},
|
||||
"BLOCK_SUCCESS_MESSAGE": "Este contato foi bloqueado com sucesso",
|
||||
"BLOCK_ERROR_MESSAGE": "Não foi possível bloquear o contato. Por favor, tente mais tarde.",
|
||||
"BLOCK_SUCCESS_MESSAGE": "This contact is blocked successfully",
|
||||
"BLOCK_ERROR_MESSAGE": "Unable to block contact. Please try again later.",
|
||||
"UNBLOCK_SUCCESS_MESSAGE": "Este contacto foi desbloqueado",
|
||||
"UNBLOCK_ERROR_MESSAGE": "Não foi possível bloquear o contato. Por favor, tente mais tarde.",
|
||||
"UNBLOCK_ERROR_MESSAGE": "Unable to unblock contact. Please try again later.",
|
||||
"IMPORT_CONTACT": {
|
||||
"TITLE": "Importar contatos",
|
||||
"TITLE": "Import contacts",
|
||||
"DESCRIPTION": "Importar contactos através de um ficheiro CSV.",
|
||||
"DOWNLOAD_LABEL": "Descarregar uma amostra CSV.",
|
||||
"LABEL": "Ficheiro CSV:",
|
||||
"CHOOSE_FILE": "Escolher arquivo",
|
||||
"CHOOSE_FILE": "Choose file",
|
||||
"CHANGE": "Trocar",
|
||||
"CANCEL": "Cancelar",
|
||||
"IMPORT": "Importar",
|
||||
@@ -321,8 +321,8 @@
|
||||
"ERROR_MESSAGE": "Ocorreu um erro, por favor, tente novamente"
|
||||
},
|
||||
"EXPORT_CONTACT": {
|
||||
"TITLE": "Exportar contatos",
|
||||
"DESCRIPTION": "Exporte rapidamente um arquivo csv com detalhes completos dos seus contatos",
|
||||
"TITLE": "Export contacts",
|
||||
"DESCRIPTION": "Quickly export a csv file with comprehensive details of your contacts",
|
||||
"CONFIRM": "Exportar",
|
||||
"SUCCESS_MESSAGE": "Exportação em progresso. Será notificado via e-mail quando o ficheiro de exportação estiver pronto para descarregar.",
|
||||
"ERROR_MESSAGE": "Ocorreu um erro, por favor, tente novamente"
|
||||
@@ -408,44 +408,44 @@
|
||||
"TITLE": "Editar detalhes do contacto",
|
||||
"FORM": {
|
||||
"FIRST_NAME": {
|
||||
"PLACEHOLDER": "Digite o primeiro nome"
|
||||
"PLACEHOLDER": "Enter the first name"
|
||||
},
|
||||
"LAST_NAME": {
|
||||
"PLACEHOLDER": "Digite o sobrenome"
|
||||
"PLACEHOLDER": "Enter the last name"
|
||||
},
|
||||
"EMAIL_ADDRESS": {
|
||||
"PLACEHOLDER": "Insira um endereço de e-mail",
|
||||
"PLACEHOLDER": "Enter the email address",
|
||||
"DUPLICATE": "O e-mail inserido já está a ser utilizado por outro contacto."
|
||||
},
|
||||
"PHONE_NUMBER": {
|
||||
"PLACEHOLDER": "Digite número de telefone",
|
||||
"PLACEHOLDER": "Enter the phone number",
|
||||
"DUPLICATE": "Este número já está a ser usado por outro contacto."
|
||||
},
|
||||
"CITY": {
|
||||
"PLACEHOLDER": "Escreva o nome da cidade"
|
||||
},
|
||||
"COUNTRY": {
|
||||
"PLACEHOLDER": "Selecionar País"
|
||||
"PLACEHOLDER": "Select country"
|
||||
},
|
||||
"BIO": {
|
||||
"PLACEHOLDER": "Digite descrição"
|
||||
"PLACEHOLDER": "Enter the bio"
|
||||
},
|
||||
"COMPANY_NAME": {
|
||||
"PLACEHOLDER": "Insira o nome da empresa"
|
||||
}
|
||||
},
|
||||
"UPDATE_BUTTON": "Atualizar contato",
|
||||
"SUCCESS_MESSAGE": "Contato atualizado com sucesso",
|
||||
"ERROR_MESSAGE": "Não foi possível atualizar o contato. Por favor, tente mais tarde."
|
||||
"UPDATE_BUTTON": "Update contact",
|
||||
"SUCCESS_MESSAGE": "Contact updated successfully",
|
||||
"ERROR_MESSAGE": "Unable to update contact. Please try again later."
|
||||
},
|
||||
"SOCIAL_MEDIA": {
|
||||
"TITLE": "Editar redes sociais",
|
||||
"TITLE": "Edit social links",
|
||||
"FORM": {
|
||||
"FACEBOOK": {
|
||||
"PLACEHOLDER": "Adicionar Facebook"
|
||||
"PLACEHOLDER": "Add Facebook"
|
||||
},
|
||||
"GITHUB": {
|
||||
"PLACEHOLDER": "Adicionar Github"
|
||||
"PLACEHOLDER": "Add Github"
|
||||
},
|
||||
"INSTAGRAM": {
|
||||
"PLACEHOLDER": "Add Instagram"
|
||||
@@ -543,43 +543,43 @@
|
||||
"PLACEHOLDER": "Adicionar nota",
|
||||
"WROTE": "escreveu",
|
||||
"YOU": "Você",
|
||||
"SAVE": "Salvar nota",
|
||||
"EMPTY_STATE": "Não existem notas associadas a este contato. Você pode adicionar uma nota digitando a caixa acima."
|
||||
"SAVE": "Save note",
|
||||
"EMPTY_STATE": "There are no notes associated to this contact. You can add a note by typing in the box above."
|
||||
}
|
||||
},
|
||||
"EMPTY_STATE": {
|
||||
"TITLE": "Nenhum contato encontrado nesta conta",
|
||||
"SUBTITLE": "Para adicionar novos contatos, clique no botão abaixo",
|
||||
"BUTTON_LABEL": "Editar contato",
|
||||
"TITLE": "No contacts found in this account",
|
||||
"SUBTITLE": "Start adding new contacts by clicking on the button below",
|
||||
"BUTTON_LABEL": "Add contact",
|
||||
"SEARCH_EMPTY_STATE_TITLE": "Nenhum contacto corresponde à sua pesquisa 🔍",
|
||||
"LIST_EMPTY_STATE_TITLE": "Não há contatos disponíveis nesta visualização 📋"
|
||||
"LIST_EMPTY_STATE_TITLE": "No contacts available in this view 📋"
|
||||
}
|
||||
},
|
||||
"COMPOSE_NEW_CONVERSATION": {
|
||||
"CONTACT_SEARCH": {
|
||||
"ERROR_MESSAGE": "Não foi possível concluir a pesquisa. Por favor, tente novamente."
|
||||
"ERROR_MESSAGE": "We couldn’t complete the search. Please try again."
|
||||
},
|
||||
"FORM": {
|
||||
"GO_TO_CONVERSATION": "Ver",
|
||||
"SUCCESS_MESSAGE": "Mensagem enviada com sucesso!",
|
||||
"ERROR_MESSAGE": "Ocorreu um erro ao criar a conversa. Tente novamente mais tarde.",
|
||||
"NO_INBOX_ALERT": "Não há caixas de entrada disponíveis para iniciar uma conversa com este contato.",
|
||||
"SUCCESS_MESSAGE": "The message was sent successfully!",
|
||||
"ERROR_MESSAGE": "An error occurred while creating the conversation. Please try again later.",
|
||||
"NO_INBOX_ALERT": "There are no available inboxes to start a conversation with this contact.",
|
||||
"CONTACT_SELECTOR": {
|
||||
"LABEL": "Para:",
|
||||
"TAG_INPUT_PLACEHOLDER": "Procurar por um contato com o nome, e-mail ou número de telefone",
|
||||
"CONTACT_CREATING": "Criando contato..."
|
||||
"TAG_INPUT_PLACEHOLDER": "Search for a contact with name, email or phone number",
|
||||
"CONTACT_CREATING": "Creating contact..."
|
||||
},
|
||||
"INBOX_SELECTOR": {
|
||||
"LABEL": "Via:",
|
||||
"BUTTON": "Mostrar caixas de entrada"
|
||||
"BUTTON": "Show inboxes"
|
||||
},
|
||||
"EMAIL_OPTIONS": {
|
||||
"SUBJECT_LABEL": "Assunto :",
|
||||
"SUBJECT_PLACEHOLDER": "Insira seu assunto do e-mail aqui",
|
||||
"SUBJECT_PLACEHOLDER": "Enter your email subject here",
|
||||
"CC_LABEL": "Cc:",
|
||||
"CC_PLACEHOLDER": "Procurar um contato com seu endereço de e-mail",
|
||||
"CC_PLACEHOLDER": "Search for a contact with their email address",
|
||||
"BCC_LABEL": "Bcc:",
|
||||
"BCC_PLACEHOLDER": "Procurar um contato com seu endereço de e-mail",
|
||||
"BCC_PLACEHOLDER": "Search for a contact with their email address",
|
||||
"BCC_BUTTON": "Bcc"
|
||||
},
|
||||
"MESSAGE_EDITOR": {
|
||||
|
||||
@@ -42,6 +42,6 @@
|
||||
},
|
||||
"FEATURE_SPOTLIGHT": {
|
||||
"LEARN_MORE": "Saiba mais",
|
||||
"WATCH_VIDEO": "Assista ao vídeo"
|
||||
"WATCH_VIDEO": "Watch video"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,25 +296,25 @@
|
||||
"CONTACT_ATTRIBUTES": "Atributos do contato",
|
||||
"PREVIOUS_CONVERSATION": "Conversas anteriores",
|
||||
"MACROS": "Macros",
|
||||
"SHOPIFY_ORDERS": "Pedidos Shopify"
|
||||
"SHOPIFY_ORDERS": "Shopify Orders"
|
||||
},
|
||||
"SHOPIFY": {
|
||||
"ORDER_ID": "Pedido #{id}",
|
||||
"ERROR": "Erro ao carregar pedidos",
|
||||
"NO_SHOPIFY_ORDERS": "Nenhum pedido encontrado",
|
||||
"ORDER_ID": "Order #{id}",
|
||||
"ERROR": "Error loading orders",
|
||||
"NO_SHOPIFY_ORDERS": "No orders found",
|
||||
"FINANCIAL_STATUS": {
|
||||
"PENDING": "Pendentes",
|
||||
"AUTHORIZED": "Autorizado",
|
||||
"PARTIALLY_PAID": "Parcialmente pago",
|
||||
"PAID": "Pago",
|
||||
"PARTIALLY_REFUNDED": "Parcialmente reembolsado",
|
||||
"REFUNDED": "Reembolsado",
|
||||
"VOIDED": "Anulado"
|
||||
"AUTHORIZED": "Authorized",
|
||||
"PARTIALLY_PAID": "Partially Paid",
|
||||
"PAID": "Paid",
|
||||
"PARTIALLY_REFUNDED": "Partially Refunded",
|
||||
"REFUNDED": "Refunded",
|
||||
"VOIDED": "Voided"
|
||||
},
|
||||
"FULFILLMENT_STATUS": {
|
||||
"FULFILLED": "Processado",
|
||||
"PARTIALLY_FULFILLED": "Parcialmente processado",
|
||||
"UNFULFILLED": "Não processado"
|
||||
"FULFILLED": "Fulfilled",
|
||||
"PARTIALLY_FULFILLED": "Partially Fulfilled",
|
||||
"UNFULFILLED": "Unfulfilled"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2,18 +2,18 @@
|
||||
"INTEGRATION_SETTINGS": {
|
||||
"SHOPIFY": {
|
||||
"DELETE": {
|
||||
"TITLE": "Excluir integração com Shopify",
|
||||
"MESSAGE": "Tem certeza de que deseja excluir a integração com Shopify?"
|
||||
"TITLE": "Delete Shopify Integration",
|
||||
"MESSAGE": "Are you sure you want to delete the Shopify integration?"
|
||||
},
|
||||
"STORE_URL": {
|
||||
"TITLE": "Conectar loja Shopify",
|
||||
"LABEL": "URL da loja",
|
||||
"PLACEHOLDER": "sua-loja.myshopify.com",
|
||||
"HELP": "Insira a URL myshopify.com da sua loja Shopify",
|
||||
"TITLE": "Connect Shopify Store",
|
||||
"LABEL": "Store URL",
|
||||
"PLACEHOLDER": "your-store.myshopify.com",
|
||||
"HELP": "Enter your Shopify store's myshopify.com URL",
|
||||
"CANCEL": "Cancelar",
|
||||
"SUBMIT": "Conectar loja"
|
||||
"SUBMIT": "Connect Store"
|
||||
},
|
||||
"ERROR": "Ocorreu um erro ao conectar com o Shopify. Por favor, tente novamente ou entre em contato com o suporte se o problema persistir."
|
||||
"ERROR": "There was an error connecting to Shopify. Please try again or contact support if the issue persists."
|
||||
},
|
||||
"HEADER": "Integrações",
|
||||
"DESCRIPTION": "Chatwoot se integra com várias ferramentas e serviços para melhorar a eficiência da sua equipe. Explore a lista abaixo para configurar seus aplicativos favoritos.",
|
||||
@@ -325,8 +325,8 @@
|
||||
}
|
||||
},
|
||||
"CAPTAIN": {
|
||||
"NAME": "Captain",
|
||||
"HEADER_KNOW_MORE": "Saiba mais",
|
||||
"NAME": "Capitão",
|
||||
"HEADER_KNOW_MORE": "Know more",
|
||||
"COPILOT": {
|
||||
"SEND_MESSAGE": "Enviar mensagem...",
|
||||
"LOADER": "Capitão está pensando",
|
||||
@@ -408,8 +408,8 @@
|
||||
"TITLE": "Não há assistentes disponíveis",
|
||||
"SUBTITLE": "Crie um assistente para fornecer respostas rápidas e precisas aos seus usuários. Ele pode aprender com seus artigos de ajuda e conversas passadas.",
|
||||
"FEATURE_SPOTLIGHT": {
|
||||
"TITLE": "Assistente Captain",
|
||||
"NOTE": "O Assistente Capitão interage diretamente com os clientes, aprende com seus documentos de ajuda e conversas passadas, e fornece respostas instantâneas e precisas. Ele lida com as consultas iniciais, oferecendo soluções rápidas antes de transferir para um agente quando necessário."
|
||||
"TITLE": "Captain Assistant",
|
||||
"NOTE": "Captain Assistant engages directly with customers, learns from your help docs and past conversations, and delivers instant, accurate responses. It handles the initial queries, providing quick resolutions before transferring to an agent when needed."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -453,8 +453,8 @@
|
||||
"TITLE": "Nenhum documento disponível",
|
||||
"SUBTITLE": "Os documentos são usados pelo seu assistente para gerar perguntas frequentes. Pode importar documentos para fornecer um contexto para seu assistente.",
|
||||
"FEATURE_SPOTLIGHT": {
|
||||
"TITLE": "Documentos Captain",
|
||||
"NOTE": "Um documento no Captain serve como um recurso de conhecimento para o assistente. Ao conectar sua central de ajuda ou guias, o Captain pode analisar o conteúdo e fornecer respostas precisas para as perguntas dos clientes."
|
||||
"TITLE": "Captain Document",
|
||||
"NOTE": "A document in Captain serves as a knowledge resource for the assistant. By connecting your help center or guides, Captain can analyze the content and provide accurate responses for customer inquiries."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -534,8 +534,8 @@
|
||||
"TITLE": "Nenhuma FAQ encontrada",
|
||||
"SUBTITLE": "Perguntas Frequentes ajudam seu assistente a fornecer respostas rápidas e precisas para perguntas de seus clientes. Eles podem ser gerados automaticamente a partir do seu conteúdo ou podem ser adicionados manualmente.",
|
||||
"FEATURE_SPOTLIGHT": {
|
||||
"TITLE": "Perguntas Frequentes do Captain",
|
||||
"NOTE": "As Perguntas Frequentes do Captain detectam dúvidas comuns dos clientes — sejam elas ausentes da sua base de conhecimento ou frequentemente feitas — e geram respostas relevantes para melhorar o suporte. Você pode revisar cada sugestão e decidir se deseja aprová-la ou rejeitá-la."
|
||||
"TITLE": "Captain FAQ",
|
||||
"NOTE": "Captain FAQs detects common customer questions—whether missing from your knowledge base or frequently asked—and generates relevant FAQs to improve support. You can review each suggestion and decide whether to approve or reject it."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -120,7 +120,7 @@
|
||||
},
|
||||
"PAGINATION": {
|
||||
"RESULTS": "Exibindo {start} — {end} de {total} resultados",
|
||||
"PER_PAGE_TEMPLATE": "{size} / página"
|
||||
"PER_PAGE_TEMPLATE": "{size} / page"
|
||||
}
|
||||
},
|
||||
"AGENT_REPORTS": {
|
||||
|
||||
@@ -364,7 +364,7 @@
|
||||
"BUTTON_TXT": "Comprar mais créditos",
|
||||
"DOCUMENTS": "Documentos",
|
||||
"RESPONSES": "Respostas",
|
||||
"UPGRADE": "O Captain não está disponível no plano gratuito, faça o upgrade para ter acesso aos assistentes, copilot e muito mais."
|
||||
"UPGRADE": "O capitão não está disponível no plano gratuito, faça o upgrade para ter acesso aos assistentes, co-piloto e muito mais."
|
||||
},
|
||||
"CHAT_WITH_US": {
|
||||
"TITLE": "Precisa de ajuda?",
|
||||
|
||||
@@ -4,10 +4,9 @@ import { useVuelidate } from '@vuelidate/core';
|
||||
|
||||
import MergeContactSummary from 'dashboard/modules/contact/components/MergeContactSummary.vue';
|
||||
import ContactDropdownItem from './ContactDropdownItem.vue';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
export default {
|
||||
components: { MergeContactSummary, ContactDropdownItem, NextButton },
|
||||
components: { MergeContactSummary, ContactDropdownItem },
|
||||
props: {
|
||||
primaryContact: {
|
||||
type: Object,
|
||||
@@ -174,18 +173,12 @@ export default {
|
||||
:parent-contact-name="parentContactName"
|
||||
/>
|
||||
<div class="flex justify-end gap-2 mt-6">
|
||||
<NextButton
|
||||
faded
|
||||
slate
|
||||
type="reset"
|
||||
:label="$t('MERGE_CONTACTS.FORM.CANCEL')"
|
||||
@click.prevent="onCancel"
|
||||
/>
|
||||
<NextButton
|
||||
type="submit"
|
||||
:is-loading="isMerging"
|
||||
:label="$t('MERGE_CONTACTS.FORM.SUBMIT')"
|
||||
/>
|
||||
<woot-button variant="clear" @click.prevent="onCancel">
|
||||
{{ $t('MERGE_CONTACTS.FORM.CANCEL') }}
|
||||
</woot-button>
|
||||
<woot-button type="submit" :is-loading="isMerging">
|
||||
{{ $t('MERGE_CONTACTS.FORM.SUBMIT') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
+52
-54
@@ -1,50 +1,47 @@
|
||||
<script setup>
|
||||
<script>
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { copyTextToClipboard } from 'shared/helpers/clipboard';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
export default {
|
||||
name: 'ArticleSearchResultItem',
|
||||
props: {
|
||||
id: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: 'Untitled',
|
||||
},
|
||||
url: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
category: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
locale: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: 'Untitled',
|
||||
emits: ['insert', 'preview'],
|
||||
methods: {
|
||||
handleInsert(e) {
|
||||
e.stopPropagation();
|
||||
this.$emit('insert', this.id);
|
||||
},
|
||||
handlePreview(e) {
|
||||
e.stopPropagation();
|
||||
this.$emit('preview', this.id);
|
||||
},
|
||||
async handleCopy(e) {
|
||||
e.stopPropagation();
|
||||
await copyTextToClipboard(this.url);
|
||||
useAlert(this.$t('CONTACT_PANEL.COPY_SUCCESSFUL'));
|
||||
},
|
||||
},
|
||||
url: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
category: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
locale: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['insert', 'preview']);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const handleInsert = e => {
|
||||
e.stopPropagation();
|
||||
emit('insert', props.id);
|
||||
};
|
||||
|
||||
const handlePreview = e => {
|
||||
e.stopPropagation();
|
||||
emit('preview', props.id);
|
||||
};
|
||||
|
||||
const handleCopy = async e => {
|
||||
e.stopPropagation();
|
||||
await copyTextToClipboard(props.url);
|
||||
useAlert(t('CONTACT_PANEL.COPY_SUCCESSFUL'));
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -68,23 +65,24 @@ const handleCopy = async e => {
|
||||
{{ category || $t('HELP_CENTER.ARTICLE_SEARCH_RESULT.UNCATEGORIZED') }}
|
||||
</p>
|
||||
<div class="flex gap-0.5">
|
||||
<Button
|
||||
<woot-button
|
||||
:title="$t('HELP_CENTER.ARTICLE_SEARCH_RESULT.COPY_LINK')"
|
||||
faded
|
||||
slate
|
||||
xs
|
||||
type="reset"
|
||||
icon="i-lucide-copy"
|
||||
variant="hollow"
|
||||
color-scheme="secondary"
|
||||
size="tiny"
|
||||
icon="copy"
|
||||
class="invisible group-hover:visible"
|
||||
@click="handleCopy"
|
||||
/>
|
||||
<Button
|
||||
xs
|
||||
faded
|
||||
slate
|
||||
:label="$t('HELP_CENTER.ARTICLE_SEARCH_RESULT.INSERT_ARTICLE')"
|
||||
<woot-button
|
||||
class="insert-button"
|
||||
variant="smooth"
|
||||
color-scheme="secondary"
|
||||
size="tiny"
|
||||
@click="handleInsert"
|
||||
/>
|
||||
>
|
||||
{{ $t('HELP_CENTER.ARTICLE_SEARCH_RESULT.INSERT_ARTICLE') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
+45
-38
@@ -1,37 +1,42 @@
|
||||
<script setup>
|
||||
<script>
|
||||
import IframeLoader from 'shared/components/IframeLoader.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
defineProps({
|
||||
url: {
|
||||
type: String,
|
||||
default: '',
|
||||
export default {
|
||||
name: 'ArticleView',
|
||||
components: {
|
||||
IframeLoader,
|
||||
},
|
||||
props: {
|
||||
url: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
emits: ['back', 'insert'],
|
||||
methods: {
|
||||
onBack(e) {
|
||||
e.stopPropagation();
|
||||
this.$emit('back');
|
||||
},
|
||||
onInsert(e) {
|
||||
e.stopPropagation();
|
||||
this.$emit('insert');
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['back', 'insert']);
|
||||
|
||||
const onBack = e => {
|
||||
e.stopPropagation();
|
||||
emit('back');
|
||||
};
|
||||
|
||||
const onInsert = e => {
|
||||
e.stopPropagation();
|
||||
emit('insert');
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full w-full flex flex-col flex-1 overflow-hidden">
|
||||
<div class="py-1">
|
||||
<Button
|
||||
link
|
||||
xs
|
||||
:label="$t('HELP_CENTER.ARTICLE_SEARCH.BACK_RESULTS')"
|
||||
icon="i-lucide-chevron-left"
|
||||
<woot-button
|
||||
variant="link"
|
||||
size="small"
|
||||
icon="chevron-left"
|
||||
@click="onBack"
|
||||
/>
|
||||
>
|
||||
{{ $t('HELP_CENTER.ARTICLE_SEARCH.BACK_RESULTS') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
<div class="-ml-4 h-full overflow-y-auto">
|
||||
<div class="w-full h-full min-h-0">
|
||||
@@ -40,22 +45,24 @@ const onInsert = e => {
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-2 py-2">
|
||||
<Button
|
||||
faded
|
||||
slate
|
||||
sm
|
||||
type="reset"
|
||||
icon="i-lucide-chevron-left"
|
||||
:label="$t('HELP_CENTER.ARTICLE_SEARCH.BACK')"
|
||||
<woot-button
|
||||
variant="hollow"
|
||||
size="small"
|
||||
is-expanded
|
||||
color-scheme="secondary"
|
||||
icon="chevron-left"
|
||||
@click="onBack"
|
||||
/>
|
||||
<Button
|
||||
sm
|
||||
type="submit"
|
||||
icon="i-lucide-arrow-down-to-dot"
|
||||
:label="$t('HELP_CENTER.ARTICLE_SEARCH.INSERT_ARTICLE')"
|
||||
>
|
||||
{{ $t('HELP_CENTER.ARTICLE_SEARCH.BACK') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
size="small"
|
||||
is-expanded
|
||||
icon="arrow-download"
|
||||
@click="onInsert"
|
||||
/>
|
||||
>
|
||||
{{ $t('HELP_CENTER.ARTICLE_SEARCH.INSERT_ARTICLE') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
+7
-2
@@ -1,7 +1,6 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
defineProps({
|
||||
title: {
|
||||
@@ -50,7 +49,13 @@ useKeyboardEvents(keyboardEvents);
|
||||
<h3 class="text-base text-slate-900 dark:text-slate-25">
|
||||
{{ title }}
|
||||
</h3>
|
||||
<Button ghost xs slate icon="i-lucide-x" @click="onClose" />
|
||||
<woot-button
|
||||
variant="clear"
|
||||
size="tiny"
|
||||
color-scheme="secondary"
|
||||
icon="dismiss"
|
||||
@click="onClose"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="relative">
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
@import 'tailwindcss/utilities';
|
||||
|
||||
@import 'widget/assets/scss/reset';
|
||||
@import 'widget/assets/scss/variables';
|
||||
@import 'widget/assets/scss/buttons';
|
||||
@import 'widget/assets/scss/mixins';
|
||||
@import 'widget/assets/scss/forms';
|
||||
@import 'shared/assets/fonts/InterDisplay/inter-display';
|
||||
|
||||
html,
|
||||
@@ -14,6 +18,7 @@ body {
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
|
||||
|
||||
// Taking these utils from tailwind 3.x.x, need to remove once we upgrade
|
||||
.scroll-mt-24 {
|
||||
scroll-margin-top: 6rem;
|
||||
|
||||
@@ -1,19 +1,3 @@
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 100;
|
||||
font-display: swap;
|
||||
src: url('shared/assets/fonts/Inter/Inter-Thin.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
src: url('shared/assets/fonts/Inter/Inter-Light.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
@@ -22,14 +6,6 @@
|
||||
src: url('shared/assets/fonts/Inter/Inter-Regular.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('shared/assets/fonts/Inter/Inter-Italic.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
@@ -37,19 +13,3 @@
|
||||
font-display: swap;
|
||||
src: url('shared/assets/fonts/Inter/Inter-Medium.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
src: url('shared/assets/fonts/Inter/Inter-SemiBold.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url('shared/assets/fonts/Inter/Inter-Bold.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@@ -53,10 +53,10 @@ export default {
|
||||
:href="brandRedirectURL"
|
||||
rel="noreferrer noopener nofollow"
|
||||
target="_blank"
|
||||
class="branding--link text-n-slate-11 hover:text-n-slate-12 cursor-pointer text-xs inline-flex grayscale-[1] hover:grayscale-0 hover:opacity-100 opacity-90 no-underline justify-center items-center leading-3"
|
||||
class="branding--link justify-center items-center leading-3"
|
||||
>
|
||||
<img
|
||||
class="ltr:mr-1 rtl:ml-1 max-w-3 max-h-3"
|
||||
class="branding--image"
|
||||
:alt="globalConfig.brandName"
|
||||
:src="globalConfig.logoThumbnail"
|
||||
/>
|
||||
@@ -67,3 +67,29 @@ export default {
|
||||
</div>
|
||||
<div v-else class="p-3" />
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import 'widget/assets/scss/variables.scss';
|
||||
|
||||
.branding--image {
|
||||
margin-right: $space-smaller;
|
||||
max-width: $space-slab;
|
||||
max-height: $space-slab;
|
||||
}
|
||||
|
||||
.branding--link {
|
||||
color: $color-light-gray;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
filter: grayscale(1);
|
||||
font-size: $font-size-small;
|
||||
opacity: 0.9;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
filter: grayscale(0);
|
||||
opacity: 1;
|
||||
color: $color-gray;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -25,7 +25,7 @@ export default {
|
||||
computed: {
|
||||
buttonClassName() {
|
||||
let className =
|
||||
'text-white py-3 px-4 rounded-lg shadow-sm leading-4 cursor-pointer disabled:opacity-50';
|
||||
'text-white py-3 px-4 rounded shadow-sm leading-4 cursor-pointer disabled:opacity-50';
|
||||
if (this.type === 'clear') {
|
||||
className = 'flex mx-auto mt-4 text-xs leading-3 w-auto text-black-600';
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ export default {
|
||||
<button
|
||||
v-else
|
||||
:key="action.payload"
|
||||
class="action-button button !bg-n-background dark:!bg-n-alpha-black1 text-n-brand"
|
||||
class="action-button button"
|
||||
:style="{ borderColor: widgetColor, color: widgetColor }"
|
||||
@click="onClick"
|
||||
>
|
||||
@@ -66,7 +66,17 @@ export default {
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import 'widget/assets/scss/variables.scss';
|
||||
|
||||
.action-button {
|
||||
@apply items-center rounded-lg flex font-medium justify-center mt-1 p-0 w-full;
|
||||
align-items: center;
|
||||
border-radius: $space-micro;
|
||||
display: flex;
|
||||
font-weight: $font-weight-medium;
|
||||
justify-content: center;
|
||||
margin-top: $space-smaller;
|
||||
max-height: 34px;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script>
|
||||
import CardButton from 'shared/components/CardButton.vue';
|
||||
import { useDarkMode } from 'widget/composables/useDarkMode';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -23,27 +24,71 @@ export default {
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { getThemeClass } = useDarkMode();
|
||||
return { getThemeClass };
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="card-message chat-bubble agent bg-n-background dark:bg-n-solid-3 max-w-56 rounded-lg overflow-hidden"
|
||||
class="card-message chat-bubble agent"
|
||||
:class="getThemeClass('bg-white', 'dark:bg-slate-700')"
|
||||
>
|
||||
<img
|
||||
class="w-full object-contain max-h-[150px] rounded-[5px]"
|
||||
:src="mediaUrl"
|
||||
/>
|
||||
<img class="media" :src="mediaUrl" />
|
||||
<div class="card-body">
|
||||
<h4
|
||||
class="!text-base !font-medium !mt-1 !mb-1 !leading-[1.5] text-n-slate-12"
|
||||
class="title"
|
||||
:class="getThemeClass('text-black-900', 'dark:text-slate-50')"
|
||||
>
|
||||
{{ title }}
|
||||
</h4>
|
||||
<p class="!mb-1 text-n-slate-11">
|
||||
<p
|
||||
class="body"
|
||||
:class="getThemeClass('text-black-700', 'dark:text-slate-100')"
|
||||
>
|
||||
{{ description }}
|
||||
</p>
|
||||
<CardButton v-for="action in actions" :key="action.id" :action="action" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import 'widget/assets/scss/variables.scss';
|
||||
@import 'dashboard/assets/scss/mixins.scss';
|
||||
|
||||
.card-message {
|
||||
max-width: 220px;
|
||||
padding: $space-small;
|
||||
border-radius: $space-small;
|
||||
overflow: hidden;
|
||||
|
||||
.title {
|
||||
font-size: $font-size-default;
|
||||
font-weight: $font-weight-medium;
|
||||
margin-top: $space-smaller;
|
||||
margin-bottom: $space-smaller;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.body {
|
||||
margin-bottom: $space-smaller;
|
||||
}
|
||||
|
||||
.media {
|
||||
@include border-light;
|
||||
width: 100%;
|
||||
object-fit: contain;
|
||||
max-height: 150px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.action-button + .action-button {
|
||||
background: $color-white;
|
||||
@include thin-border($color-woot);
|
||||
color: $color-woot;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { getContrastingTextColor } from '@chatwoot/utils';
|
||||
import { useDarkMode } from 'widget/composables/useDarkMode';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
@@ -18,6 +19,10 @@ export default {
|
||||
},
|
||||
},
|
||||
emits: ['submit'],
|
||||
setup() {
|
||||
const { getThemeClass } = useDarkMode();
|
||||
return { getThemeClass };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
formValues: {},
|
||||
@@ -31,6 +36,10 @@ export default {
|
||||
textColor() {
|
||||
return getContrastingTextColor(this.widgetColor);
|
||||
},
|
||||
inputColor() {
|
||||
return `${this.getThemeClass('bg-white', 'dark:bg-slate-600')}
|
||||
${this.getThemeClass('text-black-900', 'dark:text-slate-50')}`;
|
||||
},
|
||||
isFormValid() {
|
||||
return this.items.reduce((acc, { name }) => {
|
||||
return !!this.formValues[name] && acc;
|
||||
@@ -74,23 +83,25 @@ export default {
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="form chat-bubble agent w-full p-4 bg-n-background dark:bg-n-solid-3"
|
||||
class="form chat-bubble agent"
|
||||
:class="getThemeClass('bg-white', 'dark:bg-slate-700')"
|
||||
>
|
||||
<form @submit.prevent="onSubmit">
|
||||
<div
|
||||
v-for="item in items"
|
||||
:key="item.key"
|
||||
class="pb-2 w-full"
|
||||
class="form-block"
|
||||
:class="{
|
||||
'has-submitted': hasSubmitted,
|
||||
}"
|
||||
>
|
||||
<label class="text-n-slate-12">
|
||||
{{ item.label }}
|
||||
</label>
|
||||
<label :class="getThemeClass('text-black-900', 'dark:text-slate-50')">{{
|
||||
item.label
|
||||
}}</label>
|
||||
<input
|
||||
v-if="item.type === 'email'"
|
||||
v-model="formValues[item.name]"
|
||||
:class="inputColor"
|
||||
:type="item.type"
|
||||
:pattern="item.regex"
|
||||
:title="item.title"
|
||||
@@ -102,6 +113,7 @@ export default {
|
||||
<input
|
||||
v-else-if="item.type === 'text'"
|
||||
v-model="formValues[item.name]"
|
||||
:class="inputColor"
|
||||
:required="item.required && 'required'"
|
||||
:pattern="item.pattern"
|
||||
:title="item.title"
|
||||
@@ -113,6 +125,7 @@ export default {
|
||||
<textarea
|
||||
v-else-if="item.type === 'text_area'"
|
||||
v-model="formValues[item.name]"
|
||||
:class="inputColor"
|
||||
:required="item.required && 'required'"
|
||||
:title="item.title"
|
||||
:name="item.name"
|
||||
@@ -122,6 +135,7 @@ export default {
|
||||
<select
|
||||
v-else-if="item.type === 'select'"
|
||||
v-model="formValues[item.name]"
|
||||
:class="inputColor"
|
||||
:required="item.required && 'required'"
|
||||
>
|
||||
<option
|
||||
@@ -154,31 +168,87 @@ export default {
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import 'widget/assets/scss/variables.scss';
|
||||
|
||||
.form {
|
||||
padding: $space-normal;
|
||||
width: 80%;
|
||||
|
||||
.form-block {
|
||||
width: 90%;
|
||||
padding-bottom: $space-small;
|
||||
}
|
||||
|
||||
label {
|
||||
@apply block font-medium py-1 px-0 capitalize;
|
||||
}
|
||||
|
||||
.button {
|
||||
@apply text-sm rounded-lg;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
@apply text-n-ruby-9 mt-1 hidden;
|
||||
display: block;
|
||||
font-weight: $font-weight-medium;
|
||||
padding: $space-smaller 0;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
input,
|
||||
textarea,
|
||||
textarea {
|
||||
border-radius: $space-smaller;
|
||||
border: 1px solid $color-border;
|
||||
display: block;
|
||||
font-family: inherit;
|
||||
font-size: $font-size-default;
|
||||
line-height: 1.5;
|
||||
padding: $space-one;
|
||||
width: 100%;
|
||||
|
||||
&:disabled {
|
||||
background: $color-background-light;
|
||||
}
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: none;
|
||||
}
|
||||
|
||||
select {
|
||||
@apply dark:bg-n-alpha-black1;
|
||||
width: 110%;
|
||||
padding: $space-smaller;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
border: 1px solid $color-border;
|
||||
border-radius: $space-smaller;
|
||||
font-family: inherit;
|
||||
font-size: $space-normal;
|
||||
font-weight: normal;
|
||||
line-height: 1.5;
|
||||
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' version='1.1' width='32' height='24' viewBox='0 0 32 24'><polygon points='0,0 32,0 16,24' style='fill: rgb%28110, 111, 115%29'></polygon></svg>");
|
||||
background-origin: content-box;
|
||||
background-position: right -1.6rem center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 9px 6px;
|
||||
padding-right: 2.4rem;
|
||||
}
|
||||
|
||||
.button {
|
||||
font-size: $font-size-default;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
display: none;
|
||||
margin-top: $space-smaller;
|
||||
color: $color-error;
|
||||
}
|
||||
|
||||
.has-submitted {
|
||||
input:invalid,
|
||||
textarea:invalid {
|
||||
@apply outline-n-ruby-8 dark:outline-n-ruby-8 hover:outline-n-ruby-9 dark:hover:outline-n-ruby-9;
|
||||
input:invalid {
|
||||
border: 1px solid $color-error;
|
||||
}
|
||||
input:invalid + .error-message,
|
||||
|
||||
input:invalid + .error-message {
|
||||
display: block;
|
||||
}
|
||||
|
||||
textarea:invalid {
|
||||
border: 1px solid $color-error;
|
||||
}
|
||||
|
||||
textarea:invalid + .error-message {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -40,16 +40,35 @@ export default {
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import 'widget/assets/scss/variables.scss';
|
||||
|
||||
.option {
|
||||
@apply rounded-[5rem] border border-solid border-n-brand ltr:float-left rtl:float-right m-1 max-w-full;
|
||||
border-radius: $space-jumbo;
|
||||
border: 1px solid $color-woot;
|
||||
float: left;
|
||||
margin: $space-smaller;
|
||||
max-width: 100%;
|
||||
|
||||
.option-button {
|
||||
@apply bg-transparent border-0 cursor-pointer h-auto leading-normal ltr:text-left rtl:text-right whitespace-normal rounded-[2rem] min-h-[2.5rem];
|
||||
background: transparent;
|
||||
border-radius: $space-large;
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
height: auto;
|
||||
line-height: 1.5;
|
||||
min-height: $space-two * 2;
|
||||
text-align: left;
|
||||
white-space: normal;
|
||||
|
||||
span {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
> .icon {
|
||||
margin-right: $space-smaller;
|
||||
font-size: $font-size-medium;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -43,24 +43,66 @@ export default {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="chat-bubble agent max-w-64 !py-2 !px-4 rounded-lg overflow-hidden mt-1 bg-n-background dark:bg-n-solid-3"
|
||||
>
|
||||
<h4 class="text-n-slate-12 text-sm font-normal my-1 leading-[1.5]">
|
||||
<div
|
||||
v-dompurify-html="formatMessage(title, false)"
|
||||
class="text-n-slate-12"
|
||||
/>
|
||||
</h4>
|
||||
<ul v-if="!hideFields" class="w-full">
|
||||
<ChatOption
|
||||
v-for="option in options"
|
||||
:key="option.id"
|
||||
:action="option"
|
||||
:is-selected="isSelected(option)"
|
||||
class="list-none p-0"
|
||||
@option-select="onClick"
|
||||
/>
|
||||
</ul>
|
||||
<div class="options-message chat-bubble agent bg-white dark:bg-slate-700">
|
||||
<div class="card-body">
|
||||
<h4 class="title text-black-900 dark:text-slate-50">
|
||||
<div
|
||||
v-dompurify-html="formatMessage(title, false)"
|
||||
class="message-content text-black-900 dark:text-slate-50"
|
||||
/>
|
||||
</h4>
|
||||
<ul
|
||||
v-if="!hideFields"
|
||||
class="options"
|
||||
:class="{ 'has-selected': !!selected }"
|
||||
>
|
||||
<ChatOption
|
||||
v-for="option in options"
|
||||
:key="option.id"
|
||||
:action="option"
|
||||
:is-selected="isSelected(option)"
|
||||
@option-select="onClick"
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import 'dashboard/assets/scss/variables.scss';
|
||||
|
||||
.has-selected {
|
||||
.option-button:not(.is-selected) {
|
||||
color: $color-light-gray;
|
||||
cursor: initial;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import 'widget/assets/scss/variables.scss';
|
||||
|
||||
.options-message {
|
||||
max-width: 17rem;
|
||||
padding: $space-small $space-normal;
|
||||
border-radius: $space-small;
|
||||
overflow: hidden;
|
||||
|
||||
.title {
|
||||
font-size: $font-size-default;
|
||||
font-weight: $font-weight-normal;
|
||||
margin-top: $space-smaller;
|
||||
margin-bottom: $space-smaller;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.options {
|
||||
width: 100%;
|
||||
|
||||
> li {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { mapGetters } from 'vuex';
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
import { CSAT_RATINGS } from 'shared/constants/messages';
|
||||
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
|
||||
import { useDarkMode } from 'widget/composables/useDarkMode';
|
||||
import { getContrastingTextColor } from '@chatwoot/utils';
|
||||
|
||||
export default {
|
||||
@@ -20,6 +21,10 @@ export default {
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { getThemeClass } = useDarkMode();
|
||||
return { getThemeClass };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
email: '',
|
||||
@@ -41,6 +46,10 @@ export default {
|
||||
isButtonDisabled() {
|
||||
return !(this.selectedRating && this.feedback);
|
||||
},
|
||||
inputColor() {
|
||||
return `${this.getThemeClass('bg-white', 'dark:bg-slate-600')}
|
||||
${this.getThemeClass('text-black-900', 'dark:text-slate-50')}`;
|
||||
},
|
||||
textColor() {
|
||||
return getContrastingTextColor(this.widgetColor);
|
||||
},
|
||||
@@ -98,13 +107,17 @@ export default {
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="customer-satisfaction w-full bg-n-background dark:bg-n-solid-3 shadow-[0_0.25rem_6px_rgba(50,50,93,0.08),0_1px_3px_rgba(0,0,0,0.05)] ltr:rounded-bl-[0.25rem] rtl:rounded-br-[0.25rem] rounded-lg inline-block leading-[1.5] mt-1 border-t-2 border-t-n-brand border-solid"
|
||||
class="customer-satisfaction"
|
||||
:class="getThemeClass('bg-white', 'dark:bg-slate-700')"
|
||||
:style="{ borderColor: widgetColor }"
|
||||
>
|
||||
<h6 class="text-n-slate-12 text-sm font-medium pt-5 px-2.5 text-center">
|
||||
<h6
|
||||
class="title"
|
||||
:class="getThemeClass('text-slate-900', 'dark:text-slate-50')"
|
||||
>
|
||||
{{ title }}
|
||||
</h6>
|
||||
<div class="ratings flex justify-around py-5 px-4">
|
||||
<div class="ratings">
|
||||
<button
|
||||
v-for="rating in ratings"
|
||||
:key="rating.key"
|
||||
@@ -116,11 +129,13 @@ export default {
|
||||
</div>
|
||||
<form
|
||||
v-if="!isFeedbackSubmitted"
|
||||
class="feedback-form flex"
|
||||
class="feedback-form"
|
||||
@submit.prevent="onSubmit()"
|
||||
>
|
||||
<input
|
||||
v-model="feedback"
|
||||
class="form-input"
|
||||
:class="inputColor"
|
||||
:placeholder="$t('CSAT.PLACEHOLDER')"
|
||||
@keydown.enter="onSubmit"
|
||||
/>
|
||||
@@ -141,35 +156,80 @@ export default {
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'widget/assets/scss/variables.scss';
|
||||
@import 'widget/assets/scss/mixins.scss';
|
||||
|
||||
.customer-satisfaction {
|
||||
@include light-shadow;
|
||||
|
||||
border-bottom-left-radius: $space-smaller;
|
||||
border-radius: $space-small;
|
||||
border-top: $space-micro solid $color-woot;
|
||||
color: $color-body;
|
||||
display: inline-block;
|
||||
line-height: 1.5;
|
||||
margin-top: $space-smaller;
|
||||
width: 80%;
|
||||
|
||||
.title {
|
||||
font-size: $font-size-default;
|
||||
font-weight: $font-weight-medium;
|
||||
padding: $space-two $space-one 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ratings {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
padding: $space-two $space-normal;
|
||||
|
||||
.emoji-button {
|
||||
@apply shadow-none grayscale text-2xl outline-none transition-all duration-200;
|
||||
box-shadow: none;
|
||||
filter: grayscale(100%);
|
||||
font-size: $font-size-big;
|
||||
outline: none;
|
||||
|
||||
&.selected,
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
@apply grayscale-0 scale-[1.32];
|
||||
filter: grayscale(0%);
|
||||
transform: scale(1.32);
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
@apply cursor-not-allowed opacity-50 pointer-events-none;
|
||||
cursor: default;
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.feedback-form {
|
||||
display: flex;
|
||||
|
||||
input {
|
||||
@apply h-10 dark:bg-n-alpha-black1 rtl:rounded-tl-[0] rtl:rounded-tr-[0] ltr:rounded-tr-[0] ltr:rounded-tl-[0] rtl:rounded-bl-[0] ltr:rounded-br-[0] ltr:rounded-bl-[0.25rem] rtl:rounded-br-[0.25rem] rounded-lg p-2.5 w-full focus:ring-0 focus:outline-n-brand;
|
||||
border-bottom-right-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-left-radius: $space-small;
|
||||
border: 0;
|
||||
border-top: 1px solid $color-border;
|
||||
padding: $space-one;
|
||||
width: 100%;
|
||||
|
||||
&::placeholder {
|
||||
@apply text-n-slate-10;
|
||||
color: $color-light-gray;
|
||||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
@apply rtl:rounded-tr-[0] rtl:rounded-tl-[0] appearance-none ltr:rounded-tl-[0] ltr:rounded-tr-[0] rtl:rounded-br-[0] ltr:rounded-bl-[0] rounded-lg h-auto ltr:-ml-px rtl:-mr-px text-xl;
|
||||
appearance: none;
|
||||
border-bottom-left-radius: 0;
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-right-radius: $space-small;
|
||||
font-size: $font-size-large;
|
||||
height: auto;
|
||||
margin-left: -1px;
|
||||
|
||||
.spinner {
|
||||
display: block;
|
||||
@@ -180,4 +240,10 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.customer-satisfaction .feedback-form input {
|
||||
border-top: 1px solid var(--b-500);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script>
|
||||
import { formatDate } from 'shared/helpers/DateHelper';
|
||||
import { useDarkMode } from 'widget/composables/useDarkMode';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
@@ -8,6 +9,10 @@ export default {
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { getThemeClass } = useDarkMode();
|
||||
return { getThemeClass };
|
||||
},
|
||||
computed: {
|
||||
formattedDate() {
|
||||
return formatDate({
|
||||
@@ -22,8 +27,40 @@ export default {
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="text-sm text-n-slate-11 h-[50px] leading-[50px] relative text-center w-full before:content-[''] before:h-px before:absolute before:top-6 before:w-[calc((100%-120px)/2)] before:bg-n-slate-4 before:dark:bg-n-slate-6 before:left-0 after:content-[''] after:h-px after:absolute after:top-6 after:w-[calc((100%-120px)/2)] after:bg-n-slate-4 after:dark:bg-n-slate-6 after:right-0"
|
||||
class="date--separator"
|
||||
:class="getThemeClass('text-slate-700', 'dark:text-slate-200')"
|
||||
>
|
||||
{{ formattedDate }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'widget/assets/scss/variables';
|
||||
|
||||
.date--separator {
|
||||
font-size: $font-size-default;
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.date--separator::before,
|
||||
.date--separator::after {
|
||||
background-color: $color-border;
|
||||
content: '';
|
||||
height: 1px;
|
||||
position: absolute;
|
||||
top: 24px;
|
||||
width: calc((100% - 120px) / 2);
|
||||
}
|
||||
|
||||
.date--separator::before {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.date--separator::after {
|
||||
right: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -11,10 +11,6 @@ export default {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
isRtl: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -22,32 +18,6 @@ export default {
|
||||
showEmptyState: !this.url,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
isRtl: {
|
||||
immediate: true,
|
||||
handler(value) {
|
||||
this.$nextTick(() => {
|
||||
const iframeElement = this.$el.querySelector('iframe');
|
||||
if (iframeElement) {
|
||||
iframeElement.onload = () => {
|
||||
try {
|
||||
const iframeDocument =
|
||||
iframeElement.contentDocument ||
|
||||
(iframeElement.contentWindow &&
|
||||
iframeElement.contentWindow.document);
|
||||
|
||||
if (iframeDocument) {
|
||||
iframeDocument.documentElement.dir = value ? 'rtl' : 'ltr';
|
||||
}
|
||||
} catch (e) {
|
||||
// error
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleIframeLoad() {
|
||||
// Once loaded, the loading state is hidden
|
||||
|
||||
@@ -35,41 +35,82 @@ export default {
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@keyframes spinner {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
@import 'widget/assets/scss/variables.scss';
|
||||
|
||||
@mixin color-spinner() {
|
||||
@keyframes spinner {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: $space-medium;
|
||||
height: $space-medium;
|
||||
margin-top: -$space-one;
|
||||
margin-left: -$space-one;
|
||||
border-radius: 50%;
|
||||
border: 2px solid rgba(255, 255, 255, 0.8);
|
||||
border-top-color: rgba(255, 255, 255, 0.3);
|
||||
animation: spinner 0.9s linear infinite;
|
||||
}
|
||||
}
|
||||
|
||||
.spinner {
|
||||
@apply relative inline-block w-6 h-6 align-middle;
|
||||
|
||||
&:before {
|
||||
@apply border-n-slate-10 border-2 border-solid content-[''] box-border absolute top-[50%] left-[50%] rounded-full border-t-n-strong -ml-2.5 -mt-2.5 w-6 h-6 animate-[spinner_0.9s_linear_infinite];
|
||||
}
|
||||
@include color-spinner();
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: $space-medium;
|
||||
height: $space-medium;
|
||||
padding: $zero $space-medium;
|
||||
vertical-align: middle;
|
||||
|
||||
&.message {
|
||||
@apply p-2.5 top-0 left-0 mx-auto my-0 mt-3 bg-n-background rounded-[2rem];
|
||||
padding: $space-one;
|
||||
top: 0;
|
||||
left: 0;
|
||||
margin: 0 auto;
|
||||
margin-top: $space-slab;
|
||||
background: $color-white;
|
||||
border-radius: $space-large;
|
||||
|
||||
&:before {
|
||||
@apply -mt-3 -ml-3;
|
||||
margin-top: -$space-slab;
|
||||
margin-left: -$space-slab;
|
||||
}
|
||||
}
|
||||
|
||||
&.small {
|
||||
@apply w-4 h-4;
|
||||
width: $space-normal;
|
||||
height: $space-normal;
|
||||
|
||||
&:before {
|
||||
@apply w-4 h-4 -mt-2;
|
||||
width: $space-normal;
|
||||
height: $space-normal;
|
||||
margin-top: -$space-small;
|
||||
}
|
||||
}
|
||||
|
||||
&.tiny {
|
||||
@apply w-2.5 h-2.5 py-0 px-1;
|
||||
width: $space-one;
|
||||
height: $space-one;
|
||||
padding: 0 $space-smaller;
|
||||
|
||||
&:before {
|
||||
@apply w-2.5 h-2.5 -mt-1.5;
|
||||
width: $space-one;
|
||||
height: $space-one;
|
||||
margin-top: -$space-small + $space-micro;
|
||||
}
|
||||
}
|
||||
|
||||
&.dark::before {
|
||||
border-color: rgba(0, 0, 0, 0.7);
|
||||
border-top-color: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,8 +2,18 @@
|
||||
|
||||
exports[`DateSeparator > date separator snapshot 1`] = `
|
||||
<div
|
||||
class="text-sm text-n-slate-11 h-[50px] leading-[50px] relative text-center w-full before:content-[''] before:h-px before:absolute before:top-6 before:w-[calc((100%-120px)/2)] before:bg-n-slate-4 before:dark:bg-n-slate-6 before:left-0 after:content-[''] after:h-px after:absolute after:top-6 after:w-[calc((100%-120px)/2)] after:bg-n-slate-4 after:dark:bg-n-slate-6 after:right-0"
|
||||
class="date--separator text-slate-700"
|
||||
data-v-b24b73fa=""
|
||||
>
|
||||
Nov 18, 2019
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`dateSeparator > date separator snapshot 1`] = `
|
||||
<div
|
||||
class="date--separator text-slate-700"
|
||||
data-v-b24b73fa=""
|
||||
>
|
||||
Nov 18, 2019
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -1,20 +1,30 @@
|
||||
<script setup>
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
<script>
|
||||
export default {
|
||||
emits: ['add'],
|
||||
|
||||
const emit = defineEmits(['add']);
|
||||
|
||||
const addLabel = () => {
|
||||
emit('add');
|
||||
methods: {
|
||||
addLabel() {
|
||||
this.$emit('add');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Button
|
||||
faded
|
||||
xs
|
||||
icon="i-lucide-plus"
|
||||
class="mb-0.5 ltr:mr-0.5 rtl:ml-0.5 !rounded-[4px]"
|
||||
:label="$t('CONTACT_PANEL.LABELS.CONVERSATION.ADD_BUTTON')"
|
||||
<woot-button
|
||||
variant="smooth"
|
||||
class="label--add"
|
||||
icon="add"
|
||||
size="tiny"
|
||||
@click="addLabel"
|
||||
/>
|
||||
>
|
||||
{{ $t('CONTACT_PANEL.LABELS.CONVERSATION.ADD_BUTTON') }}
|
||||
</woot-button>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.label--add {
|
||||
margin-bottom: var(--space-micro);
|
||||
margin-right: var(--space-micro);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,11 +2,15 @@
|
||||
@import 'tailwindcss/components';
|
||||
@import 'tailwindcss/utilities';
|
||||
@import 'widget/assets/scss/reset';
|
||||
@import 'widget/assets/scss/variables';
|
||||
@import 'widget/assets/scss/buttons';
|
||||
@import 'widget/assets/scss/mixins';
|
||||
@import 'widget/assets/scss/forms';
|
||||
@import 'shared/assets/fonts/widget_fonts';
|
||||
|
||||
html,
|
||||
body {
|
||||
font-family: 'Inter', -apple-system, system-ui, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Tahoma, Arial, sans-serif;
|
||||
font-family: $font-family;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
height: 100%;
|
||||
|
||||
@@ -180,7 +180,9 @@ export default {
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import 'widget/assets/scss/variables.scss';
|
||||
|
||||
.logo {
|
||||
max-height: 3rem;
|
||||
max-height: $space-larger;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -6,7 +6,6 @@ import { IFrameHelper, RNHelper } from 'widget/helpers/utils';
|
||||
import configMixin from './mixins/configMixin';
|
||||
import availabilityMixin from 'widget/mixins/availability';
|
||||
import { getLocale } from './helpers/urlParamsHelper';
|
||||
import { getLanguageDirection } from 'dashboard/components/widgets/conversation/advancedFilterItems/languages';
|
||||
import { isEmptyObject } from 'widget/helpers/utils';
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
import routerMixin from './mixins/routerMixin';
|
||||
@@ -58,22 +57,11 @@ export default {
|
||||
isRNWebView() {
|
||||
return RNHelper.isRNWebView();
|
||||
},
|
||||
isRTL() {
|
||||
return this.$root.$i18n.locale
|
||||
? getLanguageDirection(this.$root.$i18n.locale)
|
||||
: false;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
activeCampaign() {
|
||||
this.setCampaignView();
|
||||
},
|
||||
isRTL: {
|
||||
immediate: true,
|
||||
handler(value) {
|
||||
document.documentElement.dir = value ? 'rtl' : 'ltr';
|
||||
},
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
const { websiteToken, locale, widgetColor } = window.chatwootWebChannel;
|
||||
@@ -347,7 +335,7 @@ export default {
|
||||
<template>
|
||||
<div
|
||||
v-if="!conversationSize && isFetchingList"
|
||||
class="flex items-center justify-center flex-1 h-full bg-n-background"
|
||||
class="flex items-center justify-center flex-1 h-full bg-black-25"
|
||||
:class="{ dark: prefersDarkMode }"
|
||||
>
|
||||
<Spinner size="" />
|
||||
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
$button-border-width: 1px;
|
||||
// Buttons
|
||||
.button {
|
||||
appearance: none;
|
||||
background: $color-primary;
|
||||
border: $button-border-width solid $color-primary;
|
||||
border-radius: $border-radius;
|
||||
color: $color-white;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-size: $font-size-default;
|
||||
height: $space-two * 2;
|
||||
line-height: $line-height;
|
||||
outline: none;
|
||||
padding: $space-smaller $space-normal;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
transition: background .2s, border .2s, box-shadow .2s, color .2s;
|
||||
user-select: none;
|
||||
vertical-align: middle;
|
||||
white-space: nowrap;
|
||||
|
||||
&:focus,
|
||||
&:hover {
|
||||
background: lighten($color-primary, 7%);
|
||||
border-color: $color-primary;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&:active,
|
||||
&.active {
|
||||
background: $color-primary;
|
||||
border-color: darken($color-primary, 5%);
|
||||
color: lighten($color-primary, 20%);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&[disabled],
|
||||
&:disabled,
|
||||
&.disabled {
|
||||
cursor: default;
|
||||
opacity: .5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&.small {
|
||||
font-size: $font-size-small;
|
||||
height: $space-medium;
|
||||
padding: $space-smaller $space-slab;
|
||||
}
|
||||
|
||||
&.large {
|
||||
font-size: $font-size-medium;
|
||||
height: $space-larger;
|
||||
padding: $space-small $space-medium;
|
||||
}
|
||||
|
||||
&.block {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&.transparent {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
&.compact {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
// scss-lint:disable PropertySortOrder DeclarationOrder QualifyingElement
|
||||
$form-border-width: 1px;
|
||||
$input-height: $space-two * 2;
|
||||
|
||||
.form-input {
|
||||
@include placeholder {
|
||||
color: $color-gray;
|
||||
}
|
||||
|
||||
appearance: none;
|
||||
background: $color-white;
|
||||
border: 1px solid $color-border;
|
||||
border-radius: $border-radius;
|
||||
box-sizing: border-box;
|
||||
color: $color-body;
|
||||
display: block;
|
||||
font-family: $font-family;
|
||||
font-size: $font-size-medium;
|
||||
height: $input-height;
|
||||
line-height: 1.5;
|
||||
max-width: 100%;
|
||||
outline: none;
|
||||
padding: $space-smaller;
|
||||
position: relative;
|
||||
transition: background .2s,
|
||||
border .2s,
|
||||
box-shadow .2s,
|
||||
color .2s;
|
||||
width: 100%;
|
||||
|
||||
&:focus {
|
||||
border-color: $color-primary;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: $color-gray;
|
||||
}
|
||||
|
||||
// Input sizes
|
||||
&.small {
|
||||
font-size: $font-size-small;
|
||||
height: $space-large;
|
||||
padding: $space-small $space-one;
|
||||
}
|
||||
|
||||
&.default {
|
||||
font-size: $font-size-default;
|
||||
height: $space-medium;
|
||||
padding: $space-smaller $space-slab;
|
||||
}
|
||||
|
||||
&.large {
|
||||
font-size: $font-size-medium;
|
||||
height: $space-larger;
|
||||
padding: $space-slab $space-two;
|
||||
}
|
||||
|
||||
&.input-inline {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
// Input types
|
||||
&[type="file"] {
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
// Form element: Textarea
|
||||
textarea.form-input {
|
||||
font-family: $font-family;
|
||||
|
||||
@include placeholder {
|
||||
color: $color-light-gray;
|
||||
}
|
||||
|
||||
&,
|
||||
&.large,
|
||||
&.small {
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
// scss-lint:disable PseudoElement SpaceBeforeBrace VendorPrefix
|
||||
$shadow-color-1: rgba(50, 50, 93, 0.08);
|
||||
$shadow-color-2: rgba(0, 0, 0, 0.07);
|
||||
$shadow-color-3: rgba(50, 50, 93, 0.08);
|
||||
$shadow-color-4: rgba(0, 0, 0, 0.05);
|
||||
|
||||
$color-shadow-medium: rgba(50, 50, 93, 0.08);
|
||||
$color-shadow-light: rgba(50, 50, 93, 0.04);
|
||||
$color-shadow-large: rgba(50, 50, 93, 0.25);
|
||||
$color-shadow-outline: rgba(66, 153, 225, 0.5);
|
||||
|
||||
@mixin normal-shadow {
|
||||
box-shadow: 0 $space-small $space-normal $shadow-color-1,
|
||||
0 $space-smaller $space-slab $shadow-color-2;
|
||||
}
|
||||
|
||||
@mixin light-shadow {
|
||||
box-shadow: 0 $space-smaller 6px $shadow-color-3, 0 1px 3px $shadow-color-4;
|
||||
}
|
||||
|
||||
@mixin placeholder {
|
||||
&::-webkit-input-placeholder {
|
||||
@content;
|
||||
}
|
||||
|
||||
&:-moz-placeholder {
|
||||
@content;
|
||||
}
|
||||
|
||||
&::-moz-placeholder {
|
||||
@content;
|
||||
}
|
||||
|
||||
&:-ms-input-placeholder {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin shadow {
|
||||
box-shadow: 0 1px 10px 2px $color-shadow-medium,
|
||||
0 1px 5px 1px $color-shadow-light;
|
||||
}
|
||||
|
||||
@mixin shadow-medium {
|
||||
box-shadow: 0 4px 24px 4px $color-shadow-medium,
|
||||
0 2px 16px 2px $color-shadow-light;
|
||||
}
|
||||
|
||||
@mixin shadow-large {
|
||||
box-shadow: 0 10px 15px -16px $color-shadow-medium,
|
||||
0 4px 6px -8px $color-shadow-light;
|
||||
}
|
||||
|
||||
@mixin shadow-big {
|
||||
box-shadow: 0 20px 25px -20px $color-shadow-medium,
|
||||
0 10px 10px -10px $color-shadow-light;
|
||||
}
|
||||
|
||||
@mixin shadow-mega {
|
||||
box-shadow: 0 25px 50px -12px $color-shadow-big;
|
||||
}
|
||||
|
||||
@mixin shadow-inner {
|
||||
box-shadow: inset 0 2px 4px 0 $color-shadow-light;
|
||||
}
|
||||
|
||||
@mixin shadow-outline {
|
||||
box-shadow: 0 0 0 3px $color-shadow-outline;
|
||||
}
|
||||
|
||||
@mixin shadow-none {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
@mixin button-size {
|
||||
min-height: $space-large;
|
||||
min-width: $space-large;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
.icon-button {
|
||||
@include button-size;
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
// Font sizes
|
||||
$font-size-micro: 0.5rem;
|
||||
$font-size-mini: 0.625rem;
|
||||
$font-size-small: 0.75rem;
|
||||
$font-size-default: 0.875rem;
|
||||
$font-size-medium: 1rem;
|
||||
$font-size-large: 1.25rem;
|
||||
$font-size-big: 1.5rem;
|
||||
$font-size-bigger: 2rem;
|
||||
$font-size-mega: 2.5rem;
|
||||
$font-size-giga: 3.5rem;
|
||||
|
||||
// spaces
|
||||
$zero: 0;
|
||||
$space-micro: 0.125rem;
|
||||
$space-smaller: 0.25rem;
|
||||
$space-small: 0.5rem;
|
||||
$space-one: 0.625rem;
|
||||
$space-slab: 0.75rem;
|
||||
$space-normal: 1rem;
|
||||
$space-two: 1.25rem;
|
||||
$space-medium: 1.5rem;
|
||||
$space-large: 2rem;
|
||||
$space-larger: 3rem;
|
||||
$space-big: 4rem;
|
||||
$space-jumbo: 5rem;
|
||||
$space-mega: 6.25rem;
|
||||
|
||||
$border-radius-small: 0.1875rem;
|
||||
$border-radius-normal: 0.3125rem;
|
||||
|
||||
// font-weight
|
||||
$font-weight-feather: 100;
|
||||
$font-weight-light: 300;
|
||||
$font-weight-normal: 400;
|
||||
$font-weight-medium: 500;
|
||||
$font-weight-bold: 600;
|
||||
$font-weight-black: 700;
|
||||
|
||||
// Colors
|
||||
$color-woot: #1f93ff;
|
||||
$color-primary: $color-woot;
|
||||
$color-gray: #6e6f73;
|
||||
$color-light-gray: #999a9b;
|
||||
$color-border: #e0e6ed;
|
||||
$color-border-transparent: rgba(224, 230, 237, 0.5);
|
||||
$color-border-light: #f0f4f5;
|
||||
$color-border-dark: #cad0d4;
|
||||
$color-background: #f4f6fb;
|
||||
$color-background-light: #fafafa;
|
||||
$color-white: #fff;
|
||||
$color-body: #3c4858;
|
||||
$color-heading: #1f2d3d;
|
||||
$color-error: #ff382d;
|
||||
$color-success: #44ce4b;
|
||||
|
||||
// Color-palettes
|
||||
|
||||
$color-primary-light: #c7e3ff;
|
||||
$color-primary-dark: darken($color-woot, 20%);
|
||||
|
||||
// Snackbar default
|
||||
$woot-snackbar-bg: #323232;
|
||||
$woot-snackbar-button: #ffeb3b;
|
||||
|
||||
$swift-ease-out-duration: 0.4s !default;
|
||||
$swift-ease-out-timing-function: cubic-bezier(0.25, 0.8, 0.25, 1) !default;
|
||||
$swift-ease-out: all $swift-ease-out-duration $swift-ease-out-timing-function !default;
|
||||
|
||||
$border-radius: 0.1875px;
|
||||
$line-height: 1;
|
||||
$footer-height: 11.2rem;
|
||||
$header-expanded-height: $space-medium * 10;
|
||||
|
||||
$font-family: 'Inter',
|
||||
-apple-system,
|
||||
system-ui,
|
||||
BlinkMacSystemFont,
|
||||
'Segoe UI',
|
||||
Roboto,
|
||||
'Helvetica Neue',
|
||||
Tahoma,
|
||||
Arial,
|
||||
sans-serif;
|
||||
|
||||
// Break points
|
||||
$break-point-medium: 667px;
|
||||
@@ -4,18 +4,28 @@
|
||||
|
||||
.conversation-wrap {
|
||||
.agent-message {
|
||||
@apply items-end flex flex-row justify-start mt-0 ltr:mr-0 rtl:mr-2 mb-0.5 ltr:ml-2 rtl:ml-0 max-w-[88%];
|
||||
align-items: flex-end;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
margin: 0 0 $space-micro $space-small;
|
||||
max-width: 88%;
|
||||
|
||||
.avatar-wrap {
|
||||
@apply flex-shrink-0 h-6 w-6;
|
||||
flex-shrink: 0;
|
||||
height: $space-medium;
|
||||
width: $space-medium;
|
||||
|
||||
.user-thumbnail-box {
|
||||
@apply -mt-8;
|
||||
margin-top: -$space-large;
|
||||
}
|
||||
}
|
||||
|
||||
.message-wrap {
|
||||
@apply flex-grow flex-shrink-0 ltr:ml-2 rtl:mr-2 max-w-[90%];
|
||||
flex-grow: 1;
|
||||
flex-shrink: 0;
|
||||
margin-left: $space-small;
|
||||
max-width: 90%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +42,10 @@
|
||||
}
|
||||
|
||||
.agent-name {
|
||||
@apply text-xs font-medium my-2 ltr:pl-0.5 rtl:pr-0.5;
|
||||
font-size: $font-size-small;
|
||||
font-weight: $font-weight-medium;
|
||||
margin: $space-small 0;
|
||||
padding-left: $space-micro;
|
||||
}
|
||||
|
||||
.has-attachment {
|
||||
@@ -43,127 +56,146 @@
|
||||
}
|
||||
|
||||
&.has-text {
|
||||
@apply mt-1;
|
||||
margin-top: $space-smaller;
|
||||
}
|
||||
}
|
||||
|
||||
.agent-message-wrap {
|
||||
+ .agent-message-wrap {
|
||||
@apply mt-0.5;
|
||||
margin-top: $space-micro;
|
||||
|
||||
.agent-message .chat-bubble {
|
||||
@apply ltr:rounded-tl-[0.25rem] rtl:rounded-tr-[0.25rem];
|
||||
border-top-left-radius: $space-smaller;
|
||||
}
|
||||
}
|
||||
|
||||
+ .user-message-wrap {
|
||||
@apply mt-4;
|
||||
margin-top: $space-normal;
|
||||
}
|
||||
|
||||
&.has-response + .user-message-wrap {
|
||||
@apply mt-0.5;
|
||||
margin-top: $space-micro;
|
||||
|
||||
.chat-bubble {
|
||||
@apply ltr:rounded-tr-[0.25rem] rtl:rounded-tl-[0.25rem];
|
||||
border-top-right-radius: $space-smaller;
|
||||
}
|
||||
}
|
||||
|
||||
&.has-response + .agent-message-wrap {
|
||||
@apply mt-4;
|
||||
margin-top: $space-normal;
|
||||
}
|
||||
}
|
||||
|
||||
.user-message {
|
||||
@apply flex items-end flex-row justify-end max-w-[85%] ltr:text-right rtl:text-left mt-0 ltr:ml-auto rtl:mr-auto ltr:mr-1 rtl:ml-1 mb-0.5;
|
||||
align-items: flex-end;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
margin: 0 $space-smaller $space-micro auto;
|
||||
max-width: 85%;
|
||||
text-align: right;
|
||||
|
||||
.message-wrap {
|
||||
@apply max-w-full ltr:mr-2 rtl:ml-2;
|
||||
margin-right: $space-small;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.in-progress {
|
||||
.in-progress,
|
||||
.is-failed {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.is-failed {
|
||||
@apply flex items-end flex-row-reverse;
|
||||
align-items: flex-end;
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
|
||||
.chat-bubble.user {
|
||||
@apply bg-n-ruby-9 dark:bg-n-ruby-9 #{!important};
|
||||
background: $color-error !important;
|
||||
// TODO: Remove the important
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.user.has-attachment {
|
||||
.icon-wrap {
|
||||
@apply text-white;
|
||||
color: $color-white;
|
||||
}
|
||||
|
||||
.download {
|
||||
@apply text-white;
|
||||
color: $color-white;
|
||||
}
|
||||
}
|
||||
|
||||
.user-message-wrap {
|
||||
+ .user-message-wrap {
|
||||
@apply mt-0.5;
|
||||
margin-top: $space-micro;
|
||||
|
||||
.user-message .chat-bubble {
|
||||
@apply ltr:rounded-tr-[0.25rem] rtl:rounded-tl-[0.25rem];
|
||||
border-top-right-radius: $space-smaller;
|
||||
}
|
||||
}
|
||||
|
||||
+ .agent-message-wrap {
|
||||
@apply mt-4;
|
||||
margin-top: $space-normal;
|
||||
}
|
||||
}
|
||||
|
||||
p:not(:last-child) {
|
||||
@apply mb-4;
|
||||
margin-bottom: $space-normal;
|
||||
}
|
||||
}
|
||||
|
||||
.unread-messages {
|
||||
@apply flex flex-col flex-nowrap mt-0 overflow-y-auto w-full pb-2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
margin-top: 0;
|
||||
overflow-y: auto;
|
||||
padding-bottom: $space-small;
|
||||
width: 100%;
|
||||
|
||||
.chat-bubble-wrap {
|
||||
@apply mb-1;
|
||||
margin-bottom: $space-smaller;
|
||||
|
||||
&:first-child {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.chat-bubble {
|
||||
@apply border border-solid border-n-slate-5 dark:border-n-slate-11/50 text-n-black;
|
||||
border: 1px solid $color-border-dark;
|
||||
}
|
||||
|
||||
+ .chat-bubble-wrap {
|
||||
.chat-bubble {
|
||||
@apply ltr:rounded-tl-[0.25rem] rtl:rounded-tr-[0.25rem];
|
||||
border-top-left-radius: $space-smaller;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child .chat-bubble {
|
||||
@apply ltr:rounded-bl-[1.25rem] rtl:rounded-br-[1.25rem];
|
||||
border-bottom-left-radius: $space-two;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-widget-right .unread-wrap {
|
||||
@apply ltr:text-right rtl:text-left overflow-hidden;
|
||||
overflow: hidden;
|
||||
text-align: right;
|
||||
|
||||
.chat-bubble-wrap {
|
||||
.chat-bubble {
|
||||
@apply ltr:rounded-br-[0.25rem] rtl:rounded-bl-[0.25rem] rounded-[1.25rem];
|
||||
border-bottom-right-radius: $space-smaller;
|
||||
border-radius: $space-two;
|
||||
}
|
||||
|
||||
+ .chat-bubble-wrap {
|
||||
.chat-bubble {
|
||||
@apply ltr:rounded-tr-[0.25rem] rtl:rounded-tl-[0.25rem];
|
||||
border-top-right-radius: $space-smaller;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child .chat-bubble {
|
||||
@apply ltr:rounded-br-[1.25rem] rtl:rounded-bl-[1.25rem];
|
||||
border-bottom-right-radius: $space-two;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,8 +205,15 @@
|
||||
}
|
||||
|
||||
.chat-bubble {
|
||||
@apply shadow-[0_0.25rem_6px_rgba(50,50,93,0.08),0_1px_3px_rgba(0,0,0,0.05)] rounded-[1.25rem] inline-block text-sm leading-[1.5] max-w-full ltr:text-left rtl:text-right py-3 px-4 text-white;
|
||||
|
||||
@include light-shadow;
|
||||
border-radius: $space-two;
|
||||
color: $color-white;
|
||||
display: inline-block;
|
||||
font-size: $font-size-default;
|
||||
line-height: 1.5;
|
||||
max-width: 100%;
|
||||
padding: $space-slab $space-normal;
|
||||
text-align: left;
|
||||
word-break: break-word;
|
||||
|
||||
:not([audio]) {
|
||||
@@ -182,7 +221,7 @@
|
||||
}
|
||||
|
||||
> a {
|
||||
@apply text-n-brand;
|
||||
color: $color-primary;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
@@ -191,18 +230,19 @@
|
||||
}
|
||||
|
||||
&.user {
|
||||
@apply ltr:rounded-br-[0.25rem] rtl:rounded-bl-[0.25rem];
|
||||
border-bottom-right-radius: $space-smaller;
|
||||
|
||||
> a {
|
||||
@apply text-white;
|
||||
color: $color-white;
|
||||
}
|
||||
}
|
||||
|
||||
&.agent {
|
||||
@apply ltr:rounded-bl-[0.25rem] rtl:rounded-br-[0.25rem] text-n-slate-12;
|
||||
border-bottom-left-radius: $space-smaller;
|
||||
color: $color-body;
|
||||
|
||||
.link {
|
||||
@apply text-n-brand;
|
||||
color: $color-woot;
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,12 +5,20 @@
|
||||
@import 'tailwindcss/base';
|
||||
@import 'tailwindcss/components';
|
||||
@import 'tailwindcss/utilities';
|
||||
@import 'variables';
|
||||
@import 'buttons';
|
||||
@import 'mixins';
|
||||
@import 'forms';
|
||||
@import 'utilities';
|
||||
@import 'shared/assets/fonts/widget_fonts';
|
||||
@import 'views/conversation';
|
||||
|
||||
html,
|
||||
body {
|
||||
@apply antialiased h-full bg-n-background;
|
||||
font-family: $font-family;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.is-mobile {
|
||||
@@ -35,15 +43,19 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
ul {
|
||||
list-style: disc;
|
||||
@apply ltr:pl-3 rtl:pr-3;
|
||||
padding-left: $space-slab;
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style: decimal;
|
||||
@apply ltr:pl-4 rtl:pr-4;
|
||||
padding-left: $space-normal;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,316 +82,3 @@ body {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
@apply block font-medium py-1 px-0 capitalize;
|
||||
}
|
||||
|
||||
input:not(.reset-base),
|
||||
textarea:not(.reset-base) {
|
||||
font-family: inherit;
|
||||
@apply rounded-lg box-border bg-n-background dark:bg-n-alpha-2 border-none outline outline-1 outline-offset-[-1px] outline-n-weak block text-base leading-[1.5] p-2.5 w-full text-n-slate-12 focus:outline-n-brand focus:ring-1 focus:ring-n-brand;
|
||||
|
||||
&:disabled {
|
||||
@apply opacity-40 cursor-not-allowed;
|
||||
}
|
||||
|
||||
&:placeholder-shown {
|
||||
@apply text-ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: none;
|
||||
}
|
||||
|
||||
select:not(.reset-base) {
|
||||
@apply bg-n-background dark:bg-n-alpha-2 w-full p-2.5 border-none outline outline-1 outline-offset-[-1px] outline-n-weak rounded-lg text-n-slate-12 text-base ltr:pr-10 rtl:pl-10 font-normal ltr:bg-[right_-1.6rem_center] rtl:bg-[left_-1.6rem_center] focus:outline-n-brand focus:ring-1 focus:ring-n-brand;
|
||||
|
||||
-moz-appearance: none;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' version='1.1' width='32' height='24' viewBox='0 0 32 24'><polygon points='0,0 32,0 16,24' style='fill: rgb%28110, 111, 115%29'></polygon></svg>");
|
||||
background-origin: content-box;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 9px 6px;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
p code {
|
||||
@apply bg-n-slate-3 dark:bg-n-alpha-2 text-n-slate-11 text-sm inline-block rounded py-px px-1;
|
||||
}
|
||||
|
||||
pre {
|
||||
@apply bg-n-slate-3 dark:bg-n-alpha-2 text-n-slate-11 overflow-y-auto rounded-md p-2 mt-1 mb-2 block leading-[1.5] whitespace-pre-wrap;
|
||||
|
||||
code {
|
||||
@apply bg-transparent text-n-slate-11 p-0 text-sm;
|
||||
}
|
||||
}
|
||||
|
||||
blockquote {
|
||||
@apply ltr:border-l-4 rtl:border-r-4 border-n-slate-3 dark:border-n-alpha-2 border-solid my-1 px-0 text-n-slate-11 py-1 ltr:pr-2 rtl:pr-4 ltr:pl-4 rtl:pl-2;
|
||||
}
|
||||
|
||||
.button {
|
||||
@apply appearance-none bg-n-brand border border-solid border-n-brand text-white cursor-pointer inline-block text-sm h-10 leading-none outline-none outline-0 py-1 px-4 text-center no-underline select-none align-middle whitespace-nowrap;
|
||||
|
||||
&:focus,
|
||||
&:hover {
|
||||
@apply no-underline border-n-brand brightness-110;
|
||||
}
|
||||
|
||||
&:active,
|
||||
&.active {
|
||||
@apply no-underline border-n-brand brightness-125;
|
||||
}
|
||||
|
||||
&[disabled],
|
||||
&:disabled,
|
||||
&.disabled {
|
||||
cursor: default;
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&.small {
|
||||
@apply text-xs h-6 py-1 px-3;
|
||||
}
|
||||
|
||||
&.large {
|
||||
@apply text-base h-12 py-2 px-6;
|
||||
}
|
||||
|
||||
&.block {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&.transparent {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
&.compact {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// scss-lint:disable PropertySortOrder
|
||||
@layer base {
|
||||
// NEXT COLORS START
|
||||
:root {
|
||||
// slate
|
||||
--slate-1: 252 252 253;
|
||||
--slate-2: 249 249 251;
|
||||
--slate-3: 240 240 243;
|
||||
--slate-4: 232 232 236;
|
||||
--slate-5: 224 225 230;
|
||||
--slate-6: 217 217 224;
|
||||
--slate-7: 205 206 214;
|
||||
--slate-8: 185 187 198;
|
||||
--slate-9: 139 141 152;
|
||||
--slate-10: 128 131 141;
|
||||
--slate-11: 96 100 108;
|
||||
--slate-12: 28 32 36;
|
||||
|
||||
// iris
|
||||
--iris-1: 253 253 255;
|
||||
--iris-2: 248 248 255;
|
||||
--iris-3: 240 241 254;
|
||||
--iris-4: 230 231 255;
|
||||
--iris-5: 218 220 255;
|
||||
--iris-6: 203 205 255;
|
||||
--iris-7: 184 186 248;
|
||||
--iris-8: 155 158 240;
|
||||
--iris-9: 91 91 214;
|
||||
--iris-10: 81 81 205;
|
||||
--iris-11: 87 83 198;
|
||||
--iris-12: 39 41 98;
|
||||
|
||||
// ruby
|
||||
--ruby-1: 255 252 253;
|
||||
--ruby-2: 255 247 248;
|
||||
--ruby-3: 254 234 237;
|
||||
--ruby-4: 255 220 225;
|
||||
--ruby-5: 255 206 214;
|
||||
--ruby-6: 248 191 200;
|
||||
--ruby-7: 239 172 184;
|
||||
--ruby-8: 229 146 163;
|
||||
--ruby-9: 229 70 102;
|
||||
--ruby-10: 220 59 93;
|
||||
--ruby-11: 202 36 77;
|
||||
--ruby-12: 100 23 43;
|
||||
|
||||
// amber
|
||||
--amber-1: 254 253 251;
|
||||
--amber-2: 254 251 233;
|
||||
--amber-3: 255 247 194;
|
||||
--amber-4: 255 238 156;
|
||||
--amber-5: 251 229 119;
|
||||
--amber-6: 243 214 115;
|
||||
--amber-7: 233 193 98;
|
||||
--amber-8: 226 163 54;
|
||||
--amber-9: 255 197 61;
|
||||
--amber-10: 255 186 24;
|
||||
--amber-11: 171 100 0;
|
||||
--amber-12: 79 52 34;
|
||||
|
||||
// teal
|
||||
--teal-1: 250 254 253;
|
||||
--teal-2: 243 251 249;
|
||||
--teal-3: 224 248 243;
|
||||
--teal-4: 204 243 234;
|
||||
--teal-5: 184 234 224;
|
||||
--teal-6: 161 222 210;
|
||||
--teal-7: 131 205 193;
|
||||
--teal-8: 83 185 171;
|
||||
--teal-9: 18 165 148;
|
||||
--teal-10: 13 155 138;
|
||||
--teal-11: 0 133 115;
|
||||
--teal-12: 13 61 56;
|
||||
|
||||
// gray
|
||||
--gray-1: 252 252 252;
|
||||
--gray-2: 249 249 249;
|
||||
--gray-3: 240 240 240;
|
||||
--gray-4: 232 232 232;
|
||||
--gray-5: 224 224 224;
|
||||
--gray-6: 217 217 217;
|
||||
--gray-7: 206 206 206;
|
||||
--gray-8: 187 187 187;
|
||||
--gray-9: 141 141 141;
|
||||
--gray-10: 131 131 131;
|
||||
--gray-11: 100 100 100;
|
||||
--gray-12: 32 32 32;
|
||||
|
||||
--background-color: 253 253 253;
|
||||
--text-blue: 8 109 224;
|
||||
--border-container: 236 236 236;
|
||||
--border-strong: 235 235 235;
|
||||
--border-weak: 234 234 234;
|
||||
--solid-1: 255 255 255;
|
||||
--solid-2: 255 255 255;
|
||||
--solid-3: 255 255 255;
|
||||
--solid-active: 255 255 255;
|
||||
--solid-amber: 252 232 193;
|
||||
--solid-blue: 218 236 255;
|
||||
--solid-iris: 230 231 255;
|
||||
|
||||
--alpha-1: 67, 67, 67, 0.06;
|
||||
--alpha-2: 201, 202, 207, 0.15;
|
||||
--alpha-3: 255, 255, 255, 0.96;
|
||||
--black-alpha-1: 0, 0, 0, 0.12;
|
||||
--black-alpha-2: 0, 0, 0, 0.04;
|
||||
--border-blue: 39, 129, 246, 0.5;
|
||||
--white-alpha: 255, 255, 255, 0.8;
|
||||
}
|
||||
|
||||
.dark {
|
||||
// slate
|
||||
--slate-1: 17 17 19;
|
||||
--slate-2: 24 25 27;
|
||||
--slate-3: 33 34 37;
|
||||
--slate-4: 39 42 45;
|
||||
--slate-5: 46 49 53;
|
||||
--slate-6: 54 58 63;
|
||||
--slate-7: 67 72 78;
|
||||
--slate-8: 90 97 105;
|
||||
--slate-9: 105 110 119;
|
||||
--slate-10: 119 123 132;
|
||||
--slate-11: 176 180 186;
|
||||
--slate-12: 237 238 240;
|
||||
|
||||
// iris
|
||||
--iris-1: 19 19 30;
|
||||
--iris-2: 23 22 37;
|
||||
--iris-3: 32 34 72;
|
||||
--iris-4: 38 42 101;
|
||||
--iris-5: 48 51 116;
|
||||
--iris-6: 61 62 130;
|
||||
--iris-7: 74 74 149;
|
||||
--iris-8: 89 88 177;
|
||||
--iris-9: 91 91 214;
|
||||
--iris-10: 84 114 228;
|
||||
--iris-11: 158 177 255;
|
||||
--iris-12: 224 223 254;
|
||||
|
||||
// ruby
|
||||
--ruby-1: 25 17 19;
|
||||
--ruby-2: 30 21 23;
|
||||
--ruby-3: 58 20 30;
|
||||
--ruby-4: 78 19 37;
|
||||
--ruby-5: 94 26 46;
|
||||
--ruby-6: 111 37 57;
|
||||
--ruby-7: 136 52 71;
|
||||
--ruby-8: 179 68 90;
|
||||
--ruby-9: 229 70 102;
|
||||
--ruby-10: 236 90 114;
|
||||
--ruby-11: 255 148 157;
|
||||
--ruby-12: 254 210 225;
|
||||
|
||||
// amber
|
||||
--amber-1: 22 18 12;
|
||||
--amber-2: 29 24 15;
|
||||
--amber-3: 48 32 8;
|
||||
--amber-4: 63 39 0;
|
||||
--amber-5: 77 48 0;
|
||||
--amber-6: 92 61 5;
|
||||
--amber-7: 113 79 25;
|
||||
--amber-8: 143 100 36;
|
||||
--amber-9: 255 197 61;
|
||||
--amber-10: 255 214 10;
|
||||
--amber-11: 255 202 22;
|
||||
--amber-12: 255 231 179;
|
||||
|
||||
// teal
|
||||
--teal-1: 13 21 20;
|
||||
--teal-2: 17 28 27;
|
||||
--teal-3: 13 45 42;
|
||||
--teal-4: 2 59 55;
|
||||
--teal-5: 8 72 67;
|
||||
--teal-6: 20 87 80;
|
||||
--teal-7: 28 105 97;
|
||||
--teal-8: 32 126 115;
|
||||
--teal-9: 18 165 148;
|
||||
--teal-10: 14 179 158;
|
||||
--teal-11: 11 216 182;
|
||||
--teal-12: 173 240 221;
|
||||
|
||||
// gray
|
||||
--gray-1: 17 17 17;
|
||||
--gray-2: 25 25 25;
|
||||
--gray-3: 34 34 34;
|
||||
--gray-4: 42 42 42;
|
||||
--gray-5: 49 49 49;
|
||||
--gray-6: 58 58 58;
|
||||
--gray-7: 72 72 72;
|
||||
--gray-8: 96 96 96;
|
||||
--gray-9: 110 110 110;
|
||||
--gray-10: 123 123 123;
|
||||
--gray-11: 180 180 180;
|
||||
--gray-12: 238 238 238;
|
||||
|
||||
--background-color: 18 18 19;
|
||||
--border-strong: 52 52 52;
|
||||
--border-weak: 38 38 42;
|
||||
--solid-1: 23 23 26;
|
||||
--solid-2: 29 30 36;
|
||||
--solid-3: 44 45 54;
|
||||
--solid-active: 53 57 66;
|
||||
--solid-amber: 42 37 30;
|
||||
--solid-blue: 16 49 91;
|
||||
--solid-iris: 38 42 101;
|
||||
--text-blue: 126 182 255;
|
||||
|
||||
--alpha-1: 36, 36, 36, 0.8;
|
||||
--alpha-2: 139, 147, 182, 0.15;
|
||||
--alpha-3: 36, 38, 45, 0.9;
|
||||
--black-alpha-1: 0, 0, 0, 0.3;
|
||||
--black-alpha-2: 0, 0, 0, 0.2;
|
||||
--border-blue: 39, 129, 246, 0.5;
|
||||
--border-container: 236, 236, 236, 0;
|
||||
--white-alpha: 255, 255, 255, 0.1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { MESSAGE_TYPE } from 'widget/helpers/constants';
|
||||
import configMixin from '../mixins/configMixin';
|
||||
import messageMixin from '../mixins/messageMixin';
|
||||
import { isASubmittedFormMessage } from 'shared/helpers/MessageTypeHelper';
|
||||
import { useDarkMode } from 'widget/composables/useDarkMode';
|
||||
import ReplyToChip from 'widget/components/ReplyToChip.vue';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
@@ -38,6 +39,12 @@ export default {
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { getThemeClass } = useDarkMode();
|
||||
return {
|
||||
getThemeClass,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
hasImageError: false,
|
||||
@@ -176,15 +183,8 @@ export default {
|
||||
<div v-if="hasReplyTo" class="flex mt-2 mb-1 text-xs">
|
||||
<ReplyToChip :reply-to="replyTo" />
|
||||
</div>
|
||||
<div class="flex w-full gap-1">
|
||||
<div
|
||||
class="space-y-2"
|
||||
:class="{
|
||||
'w-full':
|
||||
contentType === 'form' &&
|
||||
!messageContentAttributes?.submitted_values,
|
||||
}"
|
||||
>
|
||||
<div class="flex gap-1">
|
||||
<div class="space-y-2">
|
||||
<AgentMessageBubble
|
||||
v-if="shouldDisplayAgentMessage"
|
||||
:content-type="contentType"
|
||||
@@ -195,8 +195,10 @@ export default {
|
||||
/>
|
||||
<div
|
||||
v-if="hasAttachments"
|
||||
class="space-y-2 chat-bubble has-attachment agent bg-n-background dark:bg-n-solid-3"
|
||||
:class="wrapClass"
|
||||
class="space-y-2 chat-bubble has-attachment agent"
|
||||
:class="
|
||||
(wrapClass, getThemeClass('bg-white', 'dark:bg-slate-700'))
|
||||
"
|
||||
>
|
||||
<div
|
||||
v-for="attachment in message.attachments"
|
||||
@@ -217,11 +219,7 @@ export default {
|
||||
@error="onVideoLoadError"
|
||||
/>
|
||||
|
||||
<audio
|
||||
v-else-if="attachment.file_type === 'audio'"
|
||||
controls
|
||||
class="h-10 dark:invert"
|
||||
>
|
||||
<audio v-else-if="attachment.file_type === 'audio'" controls>
|
||||
<source :src="attachment.data_url" />
|
||||
</audio>
|
||||
<FileBubble v-else :url="attachment.data_url" />
|
||||
@@ -238,7 +236,8 @@ export default {
|
||||
<p
|
||||
v-if="message.showAvatar || hasRecordedResponse"
|
||||
v-dompurify-html="agentName"
|
||||
class="agent-name text-n-slate-11"
|
||||
class="agent-name"
|
||||
:class="getThemeClass('text-slate-700', 'dark:text-slate-200')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,7 @@ import ChatOptions from 'shared/components/ChatOptions.vue';
|
||||
import ChatArticle from './template/Article.vue';
|
||||
import EmailInput from './template/EmailInput.vue';
|
||||
import CustomerSatisfaction from 'shared/components/CustomerSatisfaction.vue';
|
||||
import { useDarkMode } from 'widget/composables/useDarkMode';
|
||||
import IntegrationCard from './template/IntegrationCard.vue';
|
||||
|
||||
export default {
|
||||
@@ -32,11 +33,13 @@ export default {
|
||||
setup() {
|
||||
const { formatMessage, getPlainText, truncateMessage, highlightContent } =
|
||||
useMessageFormatter();
|
||||
const { getThemeClass } = useDarkMode();
|
||||
return {
|
||||
formatMessage,
|
||||
getPlainText,
|
||||
truncateMessage,
|
||||
highlightContent,
|
||||
getThemeClass,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -95,11 +98,12 @@ export default {
|
||||
v-if="
|
||||
!isCards && !isOptions && !isForm && !isArticle && !isCards && !isCSAT
|
||||
"
|
||||
class="chat-bubble agent bg-n-background dark:bg-n-solid-3 text-n-slate-12"
|
||||
class="chat-bubble agent"
|
||||
:class="getThemeClass('bg-white', 'dark:bg-slate-700 has-dark-mode')"
|
||||
>
|
||||
<div
|
||||
v-dompurify-html="formatMessage(message, false)"
|
||||
class="message-content text-n-slate-12"
|
||||
class="message-content text-slate-900 dark:text-slate-50"
|
||||
/>
|
||||
<EmailInput
|
||||
v-if="isTemplateEmail"
|
||||
|
||||
@@ -1,30 +1,47 @@
|
||||
<script>
|
||||
import { useDarkMode } from 'widget/composables/useDarkMode';
|
||||
export default {
|
||||
name: 'AgentTypingBubble',
|
||||
setup() {
|
||||
const { getThemeClass } = useDarkMode();
|
||||
return { getThemeClass };
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="agent-message-wrap sticky bottom-1">
|
||||
<div class="agent-message-wrap">
|
||||
<div class="agent-message">
|
||||
<div class="avatar-wrap" />
|
||||
<div class="message-wrap mt-2">
|
||||
<div
|
||||
class="chat-bubble agent typing-bubble bg-n-background dark:bg-n-solid-3"
|
||||
class="typing-bubble chat-bubble agent"
|
||||
:class="getThemeClass('bg-white', 'dark:bg-slate-700')"
|
||||
>
|
||||
<img
|
||||
src="assets/images/typing.gif"
|
||||
alt="Agent is typing a message"
|
||||
class="!w-full"
|
||||
/>
|
||||
<img src="assets/images/typing.gif" alt="Agent is typing a message" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style lang="scss" scoped>
|
||||
@import 'widget/assets/scss/variables.scss';
|
||||
|
||||
.agent-message-wrap {
|
||||
position: sticky;
|
||||
bottom: $space-smaller;
|
||||
}
|
||||
|
||||
.typing-bubble {
|
||||
@apply max-w-[2.4rem] p-2 ltr:rounded-bl-[1.25rem] rtl:rounded-br-[1.25rem] ltr:rounded-tl-lg rtl:rounded-tr-lg;
|
||||
max-width: $space-normal * 2.4;
|
||||
padding: $space-small;
|
||||
border-bottom-left-radius: $space-two;
|
||||
border-top-left-radius: $space-small;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<div class="py-4 space-y-4 bg-white dark:bg-slate-700">
|
||||
<div class="space-y-2 animate-pulse">
|
||||
<div class="h-6 bg-slate-100 dark:bg-slate-500 rounded w-2/5" />
|
||||
</div>
|
||||
<div class="space-y-2 animate-pulse">
|
||||
<div class="h-4 bg-slate-100 dark:bg-slate-500 rounded" />
|
||||
<div class="h-4 bg-slate-100 dark:bg-slate-500 rounded" />
|
||||
<div class="h-4 bg-slate-100 dark:bg-slate-500 rounded" />
|
||||
</div>
|
||||
<div class="space-y-2 animate-pulse">
|
||||
<div class="h-4 bg-slate-100 dark:bg-slate-500 rounded w-1/5" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,54 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import ArticleList from './ArticleList.vue';
|
||||
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
|
||||
|
||||
export default {
|
||||
components: { FluentIcon, ArticleList },
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
articles: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
emits: ['view', 'viewAll'],
|
||||
computed: {
|
||||
...mapGetters({ widgetColor: 'appConfig/getWidgetColor' }),
|
||||
},
|
||||
methods: {
|
||||
onArticleClick(link) {
|
||||
this.$emit('view', link);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h3 class="mb-0 text-sm font-medium text-slate-800 dark:text-slate-50">
|
||||
{{ title }}
|
||||
</h3>
|
||||
<ArticleList :articles="articles" @select-article="onArticleClick" />
|
||||
<button
|
||||
class="inline-flex items-center justify-between px-2 py-1 -ml-2 text-sm font-medium leading-6 rounded-md text-slate-800 dark:text-slate-50 hover:bg-slate-25 dark:hover:bg-slate-800 see-articles"
|
||||
:style="{ color: widgetColor }"
|
||||
@click="$emit('viewAll')"
|
||||
>
|
||||
<span class="pr-2 text-sm">{{ $t('PORTAL.VIEW_ALL_ARTICLES') }}</span>
|
||||
<FluentIcon icon="arrow-right" size="14" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.see-articles {
|
||||
color: var(--brand-textButtonClear);
|
||||
svg {
|
||||
color: var(--brand-textButtonClear);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,27 @@
|
||||
<script>
|
||||
import CategoryCard from './ArticleCategoryCard.vue';
|
||||
export default {
|
||||
components: { CategoryCard },
|
||||
props: {
|
||||
articles: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
emits: ['view', 'viewAll'],
|
||||
methods: {
|
||||
onArticleClick(link) {
|
||||
this.$emit('view', link);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CategoryCard
|
||||
:title="$t('PORTAL.POPULAR_ARTICLES')"
|
||||
:articles="articles.slice(0, 6)"
|
||||
@view-all="$emit('viewAll')"
|
||||
@view="onArticleClick"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,36 @@
|
||||
<script>
|
||||
import ArticleListItem from './ArticleListItem.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ArticleListItem,
|
||||
},
|
||||
props: {
|
||||
articles: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
emits: ['selectArticle'],
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
methods: {
|
||||
onClick(link) {
|
||||
this.$emit('selectArticle', link);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ul role="list" class="py-2">
|
||||
<ArticleListItem
|
||||
v-for="article in articles"
|
||||
:key="article.slug"
|
||||
:link="article.link"
|
||||
:title="article.title"
|
||||
@select-article="onClick"
|
||||
/>
|
||||
</ul>
|
||||
</template>
|
||||
@@ -0,0 +1,41 @@
|
||||
<script>
|
||||
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
|
||||
|
||||
export default {
|
||||
components: { FluentIcon },
|
||||
props: {
|
||||
link: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
emits: ['selectArticle'],
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
methods: {
|
||||
onClick() {
|
||||
this.$emit('selectArticle', this.link);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<li
|
||||
class="py-1 flex items-center justify-between -mx-1 px-1 hover:bg-slate-25 dark:hover:bg-slate-600 rounded cursor-pointer text-slate-700 dark:text-slate-50 dark:hover:text-slate-25 hover:text-slate-900"
|
||||
role="button"
|
||||
@click="onClick"
|
||||
>
|
||||
<button class="underline-offset-2 text-sm leading-6 text-left">
|
||||
{{ title }}
|
||||
</button>
|
||||
<span class="pl-1 arrow">
|
||||
<FluentIcon icon="arrow-right" size="14" />
|
||||
</span>
|
||||
</li>
|
||||
</template>
|
||||
@@ -0,0 +1,52 @@
|
||||
<script>
|
||||
import { debounce } from '@chatwoot/utils';
|
||||
export default {
|
||||
props: {
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
emits: ['search'],
|
||||
data() {
|
||||
return {
|
||||
searchQuery: '',
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
handleInput: debounce(
|
||||
() => {
|
||||
this.$emit('search', this.searchQuery);
|
||||
},
|
||||
500,
|
||||
true
|
||||
),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative flex items-center">
|
||||
<div
|
||||
class="absolute inset-y-0 left-0 flex items-center px-2 py-2 text-slate-500"
|
||||
>
|
||||
<fluent-icon icon="search" size="14" />
|
||||
</div>
|
||||
<input
|
||||
id="search"
|
||||
v-model="searchQuery"
|
||||
:placeholder="placeholder"
|
||||
type="text"
|
||||
name="search"
|
||||
class="block w-full h-8 px-2 pl-6 pr-1 text-sm border rounded-md focus-visible:outline-none text-slate-800 border-slate-100 bg-slate-75 placeholder:text-slate-400 focus:ring focus:border-woot-500 focus:ring-woot-200 hover:border-woot-200"
|
||||
@input="handleInput"
|
||||
/>
|
||||
<div class="absolute inset-y-0 right-0 flex py-1.5 pr-1.5">
|
||||
<kbd
|
||||
class="inline-flex items-center px-1 font-sans border rounded border-slate-200 text-xxs text-slate-400"
|
||||
>
|
||||
{{ '⌘K' }}
|
||||
</kbd>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,27 @@
|
||||
<script>
|
||||
import GroupedAvatars from 'widget/components/GroupedAvatars.vue';
|
||||
|
||||
export default {
|
||||
name: 'AvailableAgents',
|
||||
components: { GroupedAvatars },
|
||||
props: {
|
||||
agents: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
users() {
|
||||
return this.agents.slice(0, 4).map(agent => ({
|
||||
id: agent.id,
|
||||
avatar: agent.avatar_url,
|
||||
name: agent.name,
|
||||
}));
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<GroupedAvatars :users="users" />
|
||||
</template>
|
||||
@@ -33,15 +33,21 @@ export default {
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import 'widget/assets/scss/variables.scss';
|
||||
|
||||
.banner {
|
||||
@apply text-white text-sm font-semibold p-3 text-center;
|
||||
color: $color-white;
|
||||
font-size: $font-size-default;
|
||||
font-weight: $font-weight-bold;
|
||||
padding: $space-slab;
|
||||
text-align: center;
|
||||
|
||||
&.success {
|
||||
@apply bg-n-teal-9;
|
||||
background: $color-success;
|
||||
}
|
||||
|
||||
&.error {
|
||||
@apply bg-n-ruby-9;
|
||||
background: $color-error;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -147,7 +147,7 @@ export default {
|
||||
}"
|
||||
@input-file="onFileUpload"
|
||||
>
|
||||
<button class="min-h-8 min-w-8 flex items-center justify-center">
|
||||
<button class="icon-button flex items-center justify-center">
|
||||
<FluentIcon v-if="!isUploading.image" icon="attach" />
|
||||
<Spinner v-if="isUploading" size="small" />
|
||||
</button>
|
||||
|
||||
@@ -156,3 +156,23 @@ export default {
|
||||
</CustomButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import 'widget/assets/scss/variables.scss';
|
||||
|
||||
.branding {
|
||||
align-items: center;
|
||||
color: $color-body;
|
||||
display: flex;
|
||||
font-size: $font-size-default;
|
||||
justify-content: center;
|
||||
padding: $space-one;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
|
||||
img {
|
||||
margin-right: $space-small;
|
||||
max-width: $space-two;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -4,6 +4,7 @@ import nextAvailabilityTime from 'widget/mixins/nextAvailabilityTime';
|
||||
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
|
||||
import HeaderActions from './HeaderActions.vue';
|
||||
import routerMixin from 'widget/mixins/routerMixin';
|
||||
import { useDarkMode } from 'widget/composables/useDarkMode';
|
||||
|
||||
export default {
|
||||
name: 'ChatHeader',
|
||||
@@ -34,6 +35,10 @@ export default {
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { getThemeClass } = useDarkMode();
|
||||
return { getThemeClass };
|
||||
},
|
||||
computed: {
|
||||
isOnline() {
|
||||
const { workingHoursEnabled } = this.channelConfig;
|
||||
@@ -54,32 +59,43 @@ export default {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header class="flex justify-between w-full p-5 bg-n-background gap-2">
|
||||
<header
|
||||
class="flex justify-between w-full p-5"
|
||||
:class="getThemeClass('bg-white', 'dark:bg-slate-900')"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<button
|
||||
v-if="showBackButton"
|
||||
class="px-2 ltr:-ml-3 rtl:-mr-3"
|
||||
class="px-2 -ml-3"
|
||||
@click="onBackButtonClick"
|
||||
>
|
||||
<FluentIcon icon="chevron-left" size="24" class="text-n-slate-12" />
|
||||
<FluentIcon
|
||||
icon="chevron-left"
|
||||
size="24"
|
||||
:class="getThemeClass('text-black-900', 'dark:text-slate-50')"
|
||||
/>
|
||||
</button>
|
||||
<img
|
||||
v-if="avatarUrl"
|
||||
class="w-8 h-8 ltr:mr-3 rtl:ml-3 rounded-full"
|
||||
class="w-8 h-8 mr-3 rounded-full"
|
||||
:src="avatarUrl"
|
||||
alt="avatar"
|
||||
/>
|
||||
<div class="flex flex-col gap-1">
|
||||
<div>
|
||||
<div
|
||||
class="flex items-center text-base font-medium leading-4 text-n-slate-12"
|
||||
class="flex items-center text-base font-medium leading-4"
|
||||
:class="getThemeClass('text-black-900', 'dark:text-slate-50')"
|
||||
>
|
||||
<span v-dompurify-html="title" class="ltr:mr-1 rtl:ml-1" />
|
||||
<span v-dompurify-html="title" class="mr-1" />
|
||||
<div
|
||||
:class="`h-2 w-2 rounded-full
|
||||
${isOnline ? 'bg-green-500' : 'hidden'}`"
|
||||
/>
|
||||
</div>
|
||||
<div class="text-xs leading-3 text-n-slate-11">
|
||||
<div
|
||||
class="mt-1 text-xs leading-3"
|
||||
:class="getThemeClass('text-black-700', 'dark:text-slate-400')"
|
||||
>
|
||||
{{ replyWaitMessage }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,36 +1,45 @@
|
||||
<script setup>
|
||||
<script>
|
||||
import HeaderActions from './HeaderActions.vue';
|
||||
import { computed } from 'vue';
|
||||
import { useDarkMode } from 'widget/composables/useDarkMode';
|
||||
|
||||
const props = defineProps({
|
||||
avatarUrl: {
|
||||
type: String,
|
||||
default: '',
|
||||
export default {
|
||||
name: 'ChatHeaderExpanded',
|
||||
components: {
|
||||
HeaderActions,
|
||||
},
|
||||
introHeading: {
|
||||
type: String,
|
||||
default: '',
|
||||
props: {
|
||||
avatarUrl: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
introHeading: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
introBody: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
showPopoutButton: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
introBody: {
|
||||
type: String,
|
||||
default: '',
|
||||
setup() {
|
||||
const { getThemeClass } = useDarkMode();
|
||||
return { getThemeClass };
|
||||
},
|
||||
showPopoutButton: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const containerClasses = computed(() => [
|
||||
props.avatarUrl ? 'justify-between' : 'justify-end',
|
||||
]);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header
|
||||
class="header-expanded pt-6 pb-4 px-5 relative box-border w-full bg-transparent"
|
||||
>
|
||||
<div class="flex items-start" :class="containerClasses">
|
||||
<div
|
||||
class="flex items-start"
|
||||
:class="[avatarUrl ? 'justify-between' : 'justify-end']"
|
||||
>
|
||||
<img
|
||||
v-if="avatarUrl"
|
||||
class="h-12 rounded-full"
|
||||
@@ -44,11 +53,13 @@ const containerClasses = computed(() => [
|
||||
</div>
|
||||
<h2
|
||||
v-dompurify-html="introHeading"
|
||||
class="mt-4 text-2xl mb-1.5 font-medium text-n-slate-12"
|
||||
class="mt-4 text-2xl mb-1.5 font-medium"
|
||||
:class="getThemeClass('text-slate-900', 'dark:text-slate-50')"
|
||||
/>
|
||||
<p
|
||||
v-dompurify-html="introBody"
|
||||
class="text-lg leading-normal text-n-slate-11"
|
||||
class="text-base leading-normal"
|
||||
:class="getThemeClass('text-slate-700', 'dark:text-slate-200')"
|
||||
/>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
@@ -6,6 +6,7 @@ import ChatSendButton from 'widget/components/ChatSendButton.vue';
|
||||
import configMixin from '../mixins/configMixin';
|
||||
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
|
||||
import ResizableTextArea from 'shared/components/ResizableTextArea.vue';
|
||||
import { useDarkMode } from 'widget/composables/useDarkMode';
|
||||
|
||||
import EmojiInput from 'shared/components/emoji/EmojiInput.vue';
|
||||
|
||||
@@ -29,6 +30,10 @@ export default {
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { getThemeClass } = useDarkMode();
|
||||
return { getThemeClass };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
userInput: '',
|
||||
@@ -48,6 +53,18 @@ export default {
|
||||
showSendButton() {
|
||||
return this.userInput.length > 0;
|
||||
},
|
||||
inputColor() {
|
||||
return `${this.getThemeClass('bg-white', 'dark:bg-slate-600')}
|
||||
${this.getThemeClass('text-black-900', 'dark:text-slate-50')}`;
|
||||
},
|
||||
emojiIconColor() {
|
||||
return this.showEmojiPicker
|
||||
? `text-woot-500 ${this.getThemeClass(
|
||||
'text-black-900',
|
||||
'dark:text-slate-100'
|
||||
)}`
|
||||
: `${this.getThemeClass('text-black-900', 'dark:text-slate-100')}`;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
isWidgetOpen(isWidgetOpen) {
|
||||
@@ -116,11 +133,8 @@ export default {
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="items-center flex ltr:pl-3 rtl:pr-3 ltr:pr-2 rtl:pl-2 rounded-[7px] transition-all duration-200 bg-n-background !shadow-[0_0_0_1px,0_0_2px_3px]"
|
||||
:class="{
|
||||
'!shadow-n-brand dark:!shadow-n-brand': isFocused,
|
||||
'!shadow-n-strong dark:!shadow-n-strong': !isFocused,
|
||||
}"
|
||||
class="chat-message--input is-focused"
|
||||
:class="getThemeClass('bg-white ', 'dark:bg-slate-600')"
|
||||
@keydown.esc="hideEmojiPicker"
|
||||
>
|
||||
<ResizableTextArea
|
||||
@@ -130,32 +144,26 @@ export default {
|
||||
:rows="1"
|
||||
:aria-label="$t('CHAT_PLACEHOLDER')"
|
||||
:placeholder="$t('CHAT_PLACEHOLDER')"
|
||||
class="user-message-input reset-base"
|
||||
class="form-input user-message-input is-focused"
|
||||
:class="inputColor"
|
||||
@typing-off="onTypingOff"
|
||||
@typing-on="onTypingOn"
|
||||
@focus="onFocus"
|
||||
@blur="onBlur"
|
||||
/>
|
||||
<div class="flex items-center ltr:pl-2 rtl:pr-2">
|
||||
<div class="button-wrap">
|
||||
<ChatAttachmentButton
|
||||
v-if="showAttachment"
|
||||
class="text-n-slate-12"
|
||||
:class="getThemeClass('text-black-900', 'dark:text-slate-100')"
|
||||
:on-attach="onSendAttachment"
|
||||
/>
|
||||
<button
|
||||
v-if="hasEmojiPickerEnabled"
|
||||
class="flex items-center justify-center min-h-8 min-w-8"
|
||||
class="flex items-center justify-center icon-button"
|
||||
:aria-label="$t('EMOJI.ARIA_LABEL')"
|
||||
@click="toggleEmojiPicker"
|
||||
>
|
||||
<FluentIcon
|
||||
icon="emoji"
|
||||
class="transition-all duration-150"
|
||||
:class="{
|
||||
'text-n-slate-12': !showEmojiPicker,
|
||||
'text-n-brand': showEmojiPicker,
|
||||
}"
|
||||
/>
|
||||
<FluentIcon icon="emoji" :class="emojiIconColor" />
|
||||
</button>
|
||||
<EmojiInput
|
||||
v-if="showEmojiPicker"
|
||||
@@ -173,11 +181,46 @@ export default {
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import 'widget/assets/scss/variables.scss';
|
||||
@import 'widget/assets/scss/mixins.scss';
|
||||
|
||||
.chat-message--input {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
padding: 0 $space-small 0 $space-slab;
|
||||
border-radius: 7px;
|
||||
|
||||
&.is-focused {
|
||||
box-shadow:
|
||||
0 0 0 1px $color-woot,
|
||||
0 0 2px 3px $color-primary-light;
|
||||
}
|
||||
}
|
||||
|
||||
.emoji-dialog {
|
||||
@apply max-w-full ltr:right-5 rtl:right-[unset] rtl:left-5 -top-[302px] before:ltr:right-2.5 before:rtl:right-[unset] before:rtl:left-2.5;
|
||||
right: 20px;
|
||||
top: -302px;
|
||||
max-width: 100%;
|
||||
|
||||
&::before {
|
||||
right: $space-one;
|
||||
}
|
||||
}
|
||||
|
||||
.button-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: $space-small;
|
||||
}
|
||||
|
||||
.user-message-input {
|
||||
@apply border-none outline-none w-full placeholder:text-n-slate-10 resize-none h-8 min-h-8 max-h-60 py-1 px-0 my-2 bg-n-background text-n-slate-12 transition-all duration-200;
|
||||
border: 0;
|
||||
height: $space-large;
|
||||
min-height: $space-large;
|
||||
max-height: 2.4 * $space-mega;
|
||||
resize: none;
|
||||
padding: $space-smaller 0;
|
||||
margin-top: $space-small;
|
||||
margin-bottom: $space-small;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -53,3 +53,56 @@ export default {
|
||||
max-width: 90%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
@import 'widget/assets/scss/variables.scss';
|
||||
|
||||
.chat-bubble .message-content,
|
||||
.chat-bubble.user {
|
||||
p code {
|
||||
background-color: var(--s-75);
|
||||
display: inline-block;
|
||||
line-height: 1;
|
||||
|
||||
border-radius: $border-radius-small;
|
||||
padding: $space-smaller;
|
||||
}
|
||||
|
||||
pre {
|
||||
overflow-y: auto;
|
||||
background-color: var(--s-75);
|
||||
border-color: var(--s-75);
|
||||
color: var(--s-800);
|
||||
border-radius: $border-radius-normal;
|
||||
padding: $space-small;
|
||||
margin-top: $space-smaller;
|
||||
margin-bottom: $space-small;
|
||||
display: block;
|
||||
line-height: 1.7;
|
||||
white-space: pre-wrap;
|
||||
|
||||
code {
|
||||
background-color: transparent;
|
||||
color: var(--s-800);
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: $space-micro solid var(--s-75);
|
||||
color: var(--s-800);
|
||||
padding: $space-smaller $space-small;
|
||||
margin: $space-smaller 0;
|
||||
padding: $space-small $space-small 0 $space-normal;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.chat-bubble.agent.has-dark-mode {
|
||||
blockquote {
|
||||
border-color: var(--s-200);
|
||||
color: var(--s-50);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -28,7 +28,7 @@ export default {
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="disabled"
|
||||
class="min-h-8 min-w-8 flex items-center justify-center ml-1"
|
||||
class="icon-button flex items-center justify-center ml-1"
|
||||
>
|
||||
<FluentIcon v-if="!loading" icon="send" :style="`color: ${color}`" />
|
||||
<Spinner v-else size="small" />
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
<script>
|
||||
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
|
||||
|
||||
export default {
|
||||
components: { FluentIcon },
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
default: 'Continue your chat',
|
||||
},
|
||||
content: {
|
||||
type: String,
|
||||
default: 'Chat with us',
|
||||
},
|
||||
unreadCount: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
emits: ['continue'],
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full justify-between items-center rounded-md ring-1 ring-inset ring-slate-50 px-2 py-2 text-sm text-slate-700 bg-slate-25 hover:bg-slate-50 dark:text-white dark:bg-slate-800 dark:ring-slate-900 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-woot-600 group"
|
||||
@click="$emit('continue')"
|
||||
>
|
||||
<div
|
||||
class="w-10 h-10 rounded-md bg-slate-75 dark:bg-slate-700 text-lg flex items-center justify-center flex-shrink-0"
|
||||
>
|
||||
<FluentIcon
|
||||
icon="chat"
|
||||
size="16"
|
||||
class="text-slate-600 dark:text-slate-400"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="text-left flex flex-col justify-start flex-grow max-w-[calc(100%-80px)] mx-2 group-hover:opacity-75"
|
||||
>
|
||||
<h5 class="font-medium text-slate-900 dark:text-white">
|
||||
{{ title }}
|
||||
</h5>
|
||||
<p class="h-4 leading-4 flex items-center gap-1">
|
||||
<span
|
||||
v-if="unreadCount > 0"
|
||||
class="inline-flex items-center justify-center rounded-full bg-green-200 px-1 min-w-[16px] leading-4 text-xxs font-medium text-green-700 mr-0.5"
|
||||
>
|
||||
{{ unreadCount }}
|
||||
</span>
|
||||
<span
|
||||
v-dompurify-html="content"
|
||||
class="leading-4 h-4 text-ellipsis overflow-hidden whitespace-nowrap dark:text-slate-25"
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
<div class="w-8 h-10 flex items-center justify-center">
|
||||
<FluentIcon icon="chevron-right" />
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
||||
@@ -122,6 +122,9 @@ export default {
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import 'widget/assets/scss/variables.scss';
|
||||
@import 'widget/assets/scss/mixins.scss';
|
||||
|
||||
.conversation--container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -140,7 +143,7 @@ export default {
|
||||
|
||||
.conversation-wrap {
|
||||
flex: 1;
|
||||
@apply px-2 pt-8 pb-2;
|
||||
padding: $space-large $space-small $space-small $space-small;
|
||||
}
|
||||
|
||||
.message--loader {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user