Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
859ab6777c |
@@ -94,8 +94,6 @@ yarn-debug.log*
|
|||||||
.vscode
|
.vscode
|
||||||
.claude/settings.local.json
|
.claude/settings.local.json
|
||||||
.cursor
|
.cursor
|
||||||
.codex/
|
|
||||||
.claude/
|
|
||||||
CLAUDE.local.md
|
CLAUDE.local.md
|
||||||
|
|
||||||
# Histoire deployment
|
# Histoire deployment
|
||||||
@@ -103,4 +101,3 @@ CLAUDE.local.md
|
|||||||
.histoire
|
.histoire
|
||||||
.pnpm-store/*
|
.pnpm-store/*
|
||||||
local/
|
local/
|
||||||
Procfile.worktree
|
|
||||||
|
|||||||
@@ -4,11 +4,6 @@
|
|||||||
|
|
||||||
- **Setup**: `bundle install && pnpm install`
|
- **Setup**: `bundle install && pnpm install`
|
||||||
- **Run Dev**: `pnpm dev` or `overmind start -f ./Procfile.dev`
|
- **Run Dev**: `pnpm dev` or `overmind start -f ./Procfile.dev`
|
||||||
- **Seed Local Test Data**: `bundle exec rails db:seed` (quickly populates minimal data for standard feature verification)
|
|
||||||
- **Seed Search Test Data**: `bundle exec rails search:setup_test_data` (bulk fixture generation for search/performance/manual load scenarios)
|
|
||||||
- **Seed Account Sample Data (richer test data)**: `Seeders::AccountSeeder` is available as an internal utility and is exposed through Super Admin `Accounts#seed`, but can be used directly in dev workflows too:
|
|
||||||
- UI path: Super Admin → Accounts → Seed (enqueues `Internal::SeedAccountJob`).
|
|
||||||
- CLI path: `bundle exec rails runner "Internal::SeedAccountJob.perform_now(Account.find(<id>))"` (or call `Seeders::AccountSeeder.new(account: Account.find(<id>)).perform!` directly).
|
|
||||||
- **Lint JS/Vue**: `pnpm eslint` / `pnpm eslint:fix`
|
- **Lint JS/Vue**: `pnpm eslint` / `pnpm eslint:fix`
|
||||||
- **Lint Ruby**: `bundle exec rubocop -a`
|
- **Lint Ruby**: `bundle exec rubocop -a`
|
||||||
- **Test JS**: `pnpm test` or `pnpm test:watch`
|
- **Test JS**: `pnpm test` or `pnpm test:watch`
|
||||||
@@ -55,13 +50,6 @@
|
|||||||
- Prefer `with_modified_env` (from spec helpers) over stubbing `ENV` directly in specs
|
- Prefer `with_modified_env` (from spec helpers) over stubbing `ENV` directly in specs
|
||||||
- Specs in parallel/reloading environments: prefer comparing `error.class.name` over constant class equality when asserting raised errors
|
- Specs in parallel/reloading environments: prefer comparing `error.class.name` over constant class equality when asserting raised errors
|
||||||
|
|
||||||
## Codex Worktree Workflow
|
|
||||||
|
|
||||||
- Use a separate git worktree + branch per task to keep changes isolated.
|
|
||||||
- Keep Codex-specific local setup under `.codex/` and use `Procfile.worktree` for worktree process orchestration.
|
|
||||||
- The setup workflow in `.codex/environments/environment.toml` should dynamically generate per-worktree DB/port values (Rails, Vite, Redis DB index) to avoid collisions.
|
|
||||||
- Start each worktree with its own Overmind socket/title so multiple instances can run at the same time.
|
|
||||||
|
|
||||||
## Commit Messages
|
## Commit Messages
|
||||||
|
|
||||||
- Prefer Conventional Commits: `type(scope): subject` (scope optional)
|
- Prefer Conventional Commits: `type(scope): subject` (scope optional)
|
||||||
@@ -98,7 +86,3 @@ Practical checklist for any change impacting core logic or public APIs
|
|||||||
- When renaming/moving shared code, mirror the change in `enterprise/` to prevent drift.
|
- When renaming/moving shared code, mirror the change in `enterprise/` to prevent drift.
|
||||||
- Tests: Add Enterprise-specific specs under `spec/enterprise`, mirroring OSS spec layout where applicable.
|
- Tests: Add Enterprise-specific specs under `spec/enterprise`, mirroring OSS spec layout where applicable.
|
||||||
- When modifying existing OSS features for Enterprise-only behavior, add an Enterprise module (via `prepend_mod_with`/`include_mod_with`) instead of editing OSS files directly—especially for policies, controllers, and services. For Enterprise-exclusive features, place code directly under `enterprise/`.
|
- When modifying existing OSS features for Enterprise-only behavior, add an Enterprise module (via `prepend_mod_with`/`include_mod_with`) instead of editing OSS files directly—especially for policies, controllers, and services. For Enterprise-exclusive features, place code directly under `enterprise/`.
|
||||||
|
|
||||||
## Branding / White-labeling note
|
|
||||||
|
|
||||||
- For user-facing strings that currently contain "Chatwoot" but should adapt to branded/self-hosted installs, prefer applying `replaceInstallationName` from `shared/composables/useBranding` in the UI layer (for example tooltip and suggestion labels) instead of adding hardcoded brand-specific copy.
|
|
||||||
|
|||||||
@@ -191,14 +191,12 @@ gem 'reverse_markdown'
|
|||||||
|
|
||||||
gem 'iso-639'
|
gem 'iso-639'
|
||||||
gem 'ruby-openai'
|
gem 'ruby-openai'
|
||||||
gem 'ai-agents'
|
gem 'ai-agents', '>= 0.7.0'
|
||||||
|
|
||||||
# TODO: Move this gem as a dependency of ai-agents
|
# TODO: Move this gem as a dependency of ai-agents
|
||||||
gem 'ruby_llm', '>= 1.8.2'
|
gem 'ruby_llm', '>= 1.8.2'
|
||||||
gem 'ruby_llm-schema'
|
gem 'ruby_llm-schema'
|
||||||
|
|
||||||
gem 'cld3', '~> 3.7'
|
|
||||||
|
|
||||||
# OpenTelemetry for LLM observability
|
# OpenTelemetry for LLM observability
|
||||||
gem 'opentelemetry-sdk'
|
gem 'opentelemetry-sdk'
|
||||||
gem 'opentelemetry-exporter-otlp'
|
gem 'opentelemetry-exporter-otlp'
|
||||||
|
|||||||
+20
-22
@@ -126,8 +126,8 @@ GEM
|
|||||||
jbuilder (~> 2)
|
jbuilder (~> 2)
|
||||||
rails (>= 4.2, < 7.2)
|
rails (>= 4.2, < 7.2)
|
||||||
selectize-rails (~> 0.6)
|
selectize-rails (~> 0.6)
|
||||||
ai-agents (0.9.0)
|
ai-agents (0.7.0)
|
||||||
ruby_llm (~> 1.9.1)
|
ruby_llm (~> 1.8.2)
|
||||||
annotaterb (4.20.0)
|
annotaterb (4.20.0)
|
||||||
activerecord (>= 6.0.0)
|
activerecord (>= 6.0.0)
|
||||||
activesupport (>= 6.0.0)
|
activesupport (>= 6.0.0)
|
||||||
@@ -186,7 +186,6 @@ GEM
|
|||||||
byebug (11.1.3)
|
byebug (11.1.3)
|
||||||
childprocess (5.1.0)
|
childprocess (5.1.0)
|
||||||
logger (~> 1.5)
|
logger (~> 1.5)
|
||||||
cld3 (3.7.0)
|
|
||||||
climate_control (1.2.0)
|
climate_control (1.2.0)
|
||||||
coderay (1.1.3)
|
coderay (1.1.3)
|
||||||
commonmarker (0.23.10)
|
commonmarker (0.23.10)
|
||||||
@@ -298,7 +297,7 @@ GEM
|
|||||||
railties (>= 5.0.0)
|
railties (>= 5.0.0)
|
||||||
faker (3.2.0)
|
faker (3.2.0)
|
||||||
i18n (>= 1.8.11, < 2)
|
i18n (>= 1.8.11, < 2)
|
||||||
faraday (2.14.1)
|
faraday (2.13.1)
|
||||||
faraday-net_http (>= 2.0, < 3.5)
|
faraday-net_http (>= 2.0, < 3.5)
|
||||||
json
|
json
|
||||||
logger
|
logger
|
||||||
@@ -309,12 +308,12 @@ GEM
|
|||||||
hashie
|
hashie
|
||||||
faraday-multipart (1.0.4)
|
faraday-multipart (1.0.4)
|
||||||
multipart-post (~> 2)
|
multipart-post (~> 2)
|
||||||
faraday-net_http (3.4.2)
|
faraday-net_http (3.4.0)
|
||||||
net-http (~> 0.5)
|
net-http (>= 0.5.0)
|
||||||
faraday-net_http_persistent (2.1.0)
|
faraday-net_http_persistent (2.1.0)
|
||||||
faraday (~> 2.5)
|
faraday (~> 2.5)
|
||||||
net-http-persistent (~> 4.0)
|
net-http-persistent (~> 4.0)
|
||||||
faraday-retry (2.4.0)
|
faraday-retry (2.2.1)
|
||||||
faraday (~> 2.0)
|
faraday (~> 2.0)
|
||||||
faraday_middleware-aws-sigv4 (1.0.1)
|
faraday_middleware-aws-sigv4 (1.0.1)
|
||||||
aws-sigv4 (~> 1.0)
|
aws-sigv4 (~> 1.0)
|
||||||
@@ -465,7 +464,7 @@ GEM
|
|||||||
rails-dom-testing (>= 1, < 3)
|
rails-dom-testing (>= 1, < 3)
|
||||||
railties (>= 4.2.0)
|
railties (>= 4.2.0)
|
||||||
thor (>= 0.14, < 2.0)
|
thor (>= 0.14, < 2.0)
|
||||||
json (2.18.1)
|
json (2.13.2)
|
||||||
json_refs (0.1.8)
|
json_refs (0.1.8)
|
||||||
hana
|
hana
|
||||||
json_schemer (0.2.24)
|
json_schemer (0.2.24)
|
||||||
@@ -540,7 +539,7 @@ GEM
|
|||||||
net-imap
|
net-imap
|
||||||
net-pop
|
net-pop
|
||||||
net-smtp
|
net-smtp
|
||||||
marcel (1.1.0)
|
marcel (1.0.4)
|
||||||
maxminddb (0.1.22)
|
maxminddb (0.1.22)
|
||||||
meta_request (0.8.5)
|
meta_request (0.8.5)
|
||||||
rack-contrib (>= 1.1, < 3)
|
rack-contrib (>= 1.1, < 3)
|
||||||
@@ -559,12 +558,12 @@ GEM
|
|||||||
multi_json (1.15.0)
|
multi_json (1.15.0)
|
||||||
multi_xml (0.8.0)
|
multi_xml (0.8.0)
|
||||||
bigdecimal (>= 3.1, < 5)
|
bigdecimal (>= 3.1, < 5)
|
||||||
multipart-post (2.4.1)
|
multipart-post (2.3.0)
|
||||||
mutex_m (0.3.0)
|
mutex_m (0.3.0)
|
||||||
neighbor (0.2.3)
|
neighbor (0.2.3)
|
||||||
activerecord (>= 5.2)
|
activerecord (>= 5.2)
|
||||||
net-http (0.9.1)
|
net-http (0.6.0)
|
||||||
uri (>= 0.11.1)
|
uri
|
||||||
net-http-persistent (4.0.2)
|
net-http-persistent (4.0.2)
|
||||||
connection_pool (~> 2.2)
|
connection_pool (~> 2.2)
|
||||||
net-imap (0.4.20)
|
net-imap (0.4.20)
|
||||||
@@ -583,14 +582,14 @@ GEM
|
|||||||
newrelic_rpm (9.6.0)
|
newrelic_rpm (9.6.0)
|
||||||
base64
|
base64
|
||||||
nio4r (2.7.3)
|
nio4r (2.7.3)
|
||||||
nokogiri (1.19.1)
|
nokogiri (1.18.9)
|
||||||
mini_portile2 (~> 2.8.2)
|
mini_portile2 (~> 2.8.2)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.19.1-arm64-darwin)
|
nokogiri (1.18.9-arm64-darwin)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.19.1-x86_64-darwin)
|
nokogiri (1.18.9-x86_64-darwin)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.19.1-x86_64-linux-gnu)
|
nokogiri (1.18.9-x86_64-linux-gnu)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
oauth (1.1.0)
|
oauth (1.1.0)
|
||||||
oauth-tty (~> 1.0, >= 1.0.1)
|
oauth-tty (~> 1.0, >= 1.0.1)
|
||||||
@@ -677,7 +676,7 @@ GEM
|
|||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
raabro (1.4.0)
|
raabro (1.4.0)
|
||||||
racc (1.8.1)
|
racc (1.8.1)
|
||||||
rack (3.2.5)
|
rack (3.2.3)
|
||||||
rack-attack (6.7.0)
|
rack-attack (6.7.0)
|
||||||
rack (>= 1.0, < 4)
|
rack (>= 1.0, < 4)
|
||||||
rack-contrib (2.5.0)
|
rack-contrib (2.5.0)
|
||||||
@@ -825,7 +824,7 @@ GEM
|
|||||||
ruby2ruby (2.5.0)
|
ruby2ruby (2.5.0)
|
||||||
ruby_parser (~> 3.1)
|
ruby_parser (~> 3.1)
|
||||||
sexp_processor (~> 4.6)
|
sexp_processor (~> 4.6)
|
||||||
ruby_llm (1.9.2)
|
ruby_llm (1.8.2)
|
||||||
base64
|
base64
|
||||||
event_stream_parser (~> 1)
|
event_stream_parser (~> 1)
|
||||||
faraday (>= 1.10.0)
|
faraday (>= 1.10.0)
|
||||||
@@ -969,7 +968,7 @@ GEM
|
|||||||
unicode-emoji (~> 4.0, >= 4.0.4)
|
unicode-emoji (~> 4.0, >= 4.0.4)
|
||||||
unicode-emoji (4.0.4)
|
unicode-emoji (4.0.4)
|
||||||
uniform_notifier (1.17.0)
|
uniform_notifier (1.17.0)
|
||||||
uri (1.1.1)
|
uri (1.0.4)
|
||||||
uri_template (0.7.0)
|
uri_template (0.7.0)
|
||||||
valid_email2 (5.2.6)
|
valid_email2 (5.2.6)
|
||||||
activemodel (>= 3.2)
|
activemodel (>= 3.2)
|
||||||
@@ -1004,7 +1003,7 @@ GEM
|
|||||||
working_hours (1.4.1)
|
working_hours (1.4.1)
|
||||||
activesupport (>= 3.2)
|
activesupport (>= 3.2)
|
||||||
tzinfo
|
tzinfo
|
||||||
zeitwerk (2.7.4)
|
zeitwerk (2.6.17)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
arm64-darwin-20
|
arm64-darwin-20
|
||||||
@@ -1024,7 +1023,7 @@ DEPENDENCIES
|
|||||||
administrate (>= 0.20.1)
|
administrate (>= 0.20.1)
|
||||||
administrate-field-active_storage (>= 1.0.3)
|
administrate-field-active_storage (>= 1.0.3)
|
||||||
administrate-field-belongs_to_search (>= 0.9.0)
|
administrate-field-belongs_to_search (>= 0.9.0)
|
||||||
ai-agents
|
ai-agents (>= 0.7.0)
|
||||||
annotaterb
|
annotaterb
|
||||||
attr_extras
|
attr_extras
|
||||||
audited (~> 5.4, >= 5.4.1)
|
audited (~> 5.4, >= 5.4.1)
|
||||||
@@ -1038,7 +1037,6 @@ DEPENDENCIES
|
|||||||
bullet
|
bullet
|
||||||
bundle-audit
|
bundle-audit
|
||||||
byebug
|
byebug
|
||||||
cld3 (~> 3.7)
|
|
||||||
climate_control
|
climate_control
|
||||||
commonmarker
|
commonmarker
|
||||||
csv-safe
|
csv-safe
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
4.11.0
|
4.10.1
|
||||||
|
|||||||
@@ -70,7 +70,6 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
|||||||
|
|
||||||
def transcript
|
def transcript
|
||||||
render json: { error: 'email param missing' }, status: :unprocessable_entity and return if params[:email].blank?
|
render json: { error: 'email param missing' }, status: :unprocessable_entity and return if params[:email].blank?
|
||||||
return render_payment_required('Email transcript is not available on your plan') unless @conversation.account.email_transcript_enabled?
|
|
||||||
return head :too_many_requests unless @conversation.account.within_email_rate_limit?
|
return head :too_many_requests unless @conversation.account.within_email_rate_limit?
|
||||||
|
|
||||||
ConversationReplyMailer.with(account: @conversation.account).conversation_transcript(@conversation, params[:email])&.deliver_later
|
ConversationReplyMailer.with(account: @conversation.account).conversation_transcript(@conversation, params[:email])&.deliver_later
|
||||||
|
|||||||
@@ -35,9 +35,7 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def transcript
|
def transcript
|
||||||
return head :too_many_requests if conversation.blank?
|
return head :too_many_requests unless conversation.present? && conversation.account.within_email_rate_limit?
|
||||||
return head :payment_required unless conversation.account.email_transcript_enabled?
|
|
||||||
return head :too_many_requests unless conversation.account.within_email_rate_limit?
|
|
||||||
|
|
||||||
send_transcript_email
|
send_transcript_email
|
||||||
head :ok
|
head :ok
|
||||||
|
|||||||
@@ -6,8 +6,12 @@ class DeviseOverrides::PasswordsController < Devise::PasswordsController
|
|||||||
|
|
||||||
def create
|
def create
|
||||||
@user = User.from_email(params[:email])
|
@user = User.from_email(params[:email])
|
||||||
@user&.send_reset_password_instructions
|
if @user
|
||||||
build_response(I18n.t('messages.reset_password'), 200)
|
@user.send_reset_password_instructions
|
||||||
|
build_response(I18n.t('messages.reset_password_success'), 200)
|
||||||
|
else
|
||||||
|
build_response(I18n.t('messages.reset_password_failure'), 404)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
class Webhooks::ShopifyController < ActionController::API
|
|
||||||
before_action :verify_hmac!
|
|
||||||
|
|
||||||
def events
|
|
||||||
case request.headers['X-Shopify-Topic']
|
|
||||||
when 'shop/redact'
|
|
||||||
handle_shop_redact
|
|
||||||
end
|
|
||||||
|
|
||||||
head :ok
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def verify_hmac!
|
|
||||||
secret = GlobalConfigService.load('SHOPIFY_CLIENT_SECRET', nil)
|
|
||||||
return head :unauthorized if secret.blank?
|
|
||||||
|
|
||||||
data = request.body.read
|
|
||||||
request.body.rewind
|
|
||||||
|
|
||||||
hmac_header = request.headers['X-Shopify-Hmac-SHA256']
|
|
||||||
return head :unauthorized if hmac_header.blank?
|
|
||||||
|
|
||||||
computed = Base64.strict_encode64(OpenSSL::HMAC.digest('SHA256', secret, data))
|
|
||||||
return head :unauthorized unless ActiveSupport::SecurityUtils.secure_compare(computed, hmac_header)
|
|
||||||
end
|
|
||||||
|
|
||||||
def handle_shop_redact
|
|
||||||
shop_domain = params[:shop_domain]
|
|
||||||
return if shop_domain.blank?
|
|
||||||
|
|
||||||
Integrations::Hook.where(app_id: 'shopify', reference_id: shop_domain).destroy_all
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -164,4 +164,10 @@ export default {
|
|||||||
.v-popper--theme-tooltip .v-popper__arrow-container {
|
.v-popper--theme-tooltip .v-popper__arrow-container {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.multiselect__input {
|
||||||
|
margin-bottom: 0px !important;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style src="vue-multiselect/dist/vue-multiselect.css"></style>
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ textarea {
|
|||||||
// Field base styles (Input, TextArea, Select)
|
// Field base styles (Input, TextArea, Select)
|
||||||
@layer components {
|
@layer components {
|
||||||
.field-base {
|
.field-base {
|
||||||
@apply block box-border w-full transition-colors duration-[0.25s] ease-[ease-in-out] focus:outline-n-brand dark:focus:outline-n-brand appearance-none mx-0 mt-0 mb-4 py-2 px-3 rounded-lg text-sm font-normal bg-n-alpha-black2 placeholder:text-n-slate-10 dark:placeholder:text-n-slate-10 text-n-slate-12 border-none outline outline-1 outline-n-weak dark:outline-n-weak hover:outline-n-slate-6 dark:hover:outline-n-slate-6;
|
@apply block box-border w-full transition-colors duration-[0.25s] ease-[ease-in-out] focus:outline-n-brand dark:focus:outline-n-brand appearance-none mx-0 mt-0 mb-4 py-2 px-3 rounded-lg text-base font-normal bg-n-alpha-black2 placeholder:text-n-slate-10 dark:placeholder:text-n-slate-10 text-n-slate-12 border-none outline outline-1 outline-n-weak dark:outline-n-weak hover:outline-n-slate-6 dark:hover:outline-n-slate-6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.field-disabled {
|
.field-disabled {
|
||||||
@@ -78,7 +78,7 @@ textarea {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$form-input-selector: "input[type]:not([type='file']):not([type='checkbox']):not([type='radio']):not([type='range']):not([type='button']):not([type='submit']):not([type='reset']):not([type='color']):not([type='image']):not([type='hidden']):not(.reset-base):not(.no-margin)";
|
$form-input-selector: "input[type]:not([type='file']):not([type='checkbox']):not([type='radio']):not([type='range']):not([type='button']):not([type='submit']):not([type='reset']):not([type='color']):not([type='image']):not([type='hidden']):not(.reset-base):not(.multiselect__input):not(.no-margin)";
|
||||||
|
|
||||||
#{$form-input-selector} {
|
#{$form-input-selector} {
|
||||||
@apply field-base h-10;
|
@apply field-base h-10;
|
||||||
@@ -92,7 +92,7 @@ $form-input-selector: "input[type]:not([type='file']):not([type='checkbox']):not
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type='file'] {
|
input[type='file']:not(.multiselect__input) {
|
||||||
@apply leading-[1.15] mb-4 border-0 bg-transparent text-sm;
|
@apply leading-[1.15] mb-4 border-0 bg-transparent text-sm;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,6 +126,13 @@ label:has(.help-text) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Error handling
|
||||||
|
.has-multi-select-error {
|
||||||
|
div.multiselect {
|
||||||
|
@apply mb-1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// FormKit support
|
// FormKit support
|
||||||
.formkit-outer[data-invalid='true'] {
|
.formkit-outer[data-invalid='true'] {
|
||||||
#{$form-input-selector},
|
#{$form-input-selector},
|
||||||
@@ -143,7 +150,9 @@ label:has(.help-text) {
|
|||||||
#{$form-input-selector},
|
#{$form-input-selector},
|
||||||
input:not([type]),
|
input:not([type]),
|
||||||
textarea,
|
textarea,
|
||||||
select {
|
select,
|
||||||
|
.multiselect > .multiselect__tags,
|
||||||
|
.multiselect:not(.no-margin) {
|
||||||
@apply field-error;
|
@apply field-error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
@import 'base';
|
@import 'base';
|
||||||
|
|
||||||
// Plugins
|
// Plugins
|
||||||
|
@import 'plugins/multiselect';
|
||||||
@import 'plugins/date-picker';
|
@import 'plugins/date-picker';
|
||||||
|
|
||||||
html,
|
html,
|
||||||
@@ -65,84 +66,4 @@ body {
|
|||||||
-ms-overflow-style: none; /* IE and Edge */
|
-ms-overflow-style: none; /* IE and Edge */
|
||||||
scrollbar-width: none; /* Firefox */
|
scrollbar-width: none; /* Firefox */
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* ============================================================================
|
|
||||||
* TYPOGRAPHY UTILITIES
|
|
||||||
* ============================================================================
|
|
||||||
*
|
|
||||||
* | Class | Use Case |
|
|
||||||
* |--------------------|----------------------------------------------------|
|
|
||||||
* | .text-body-main | <p>, <span>, general body text |
|
|
||||||
* | .text-body-para | <p> for paragraphs, larger text blocks |
|
|
||||||
* | .text-heading-1 | <h1>, page titles, panel headers |
|
|
||||||
* | .text-heading-2 | <h2>, section headings, card titles |
|
|
||||||
* | .text-heading-3 | <h3>, card headings, breadcrumbs, subsections |
|
|
||||||
* | .text-label | <label>, form labels, field names |
|
|
||||||
* | .text-label-small | <small>, footnotes, tags, badges, captions |
|
|
||||||
* | .text-button | <button>, standard button text |
|
|
||||||
* | .text-button-small | <button>, small/compact button text |
|
|
||||||
*/
|
|
||||||
|
|
||||||
/* body-text-main: Main text style for general body text */
|
|
||||||
.text-body-main {
|
|
||||||
@apply font-inter text-sm font-420;
|
|
||||||
line-height: 21px; /* 150% */
|
|
||||||
letter-spacing: -0.28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* body-text-paragraph: For paragraphs or larger blocks of text */
|
|
||||||
.text-body-para {
|
|
||||||
@apply font-inter text-sm font-420;
|
|
||||||
line-height: 21px; /* 150% */
|
|
||||||
letter-spacing: -0.21px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* heading-1: Large heading for pages and panels */
|
|
||||||
.text-heading-1 {
|
|
||||||
@apply font-inter text-lg font-520;
|
|
||||||
line-height: 24px; /* 133.333% */
|
|
||||||
letter-spacing: -0.27px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* heading-2: Secondary heading for sections */
|
|
||||||
.text-heading-2 {
|
|
||||||
@apply font-inter text-base font-medium;
|
|
||||||
line-height: 24px; /* 133.333% */
|
|
||||||
letter-spacing: -0.27px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* heading-3: For card headings, breadcrumbs, subsections */
|
|
||||||
.text-heading-3 {
|
|
||||||
@apply font-inter text-sm font-medium;
|
|
||||||
line-height: 21px; /* 150% */
|
|
||||||
letter-spacing: -0.27px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* label: Standard label text for form fields */
|
|
||||||
.text-label {
|
|
||||||
@apply font-inter text-sm font-medium;
|
|
||||||
line-height: 21px; /* 150% */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* label-small: Smallest font for labels, footnotes, tags */
|
|
||||||
.text-label-small {
|
|
||||||
@apply font-inter text-xs font-440;
|
|
||||||
line-height: 16px; /* 133.333% */
|
|
||||||
letter-spacing: -0.24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* button-text: Text for standard size buttons */
|
|
||||||
.text-button {
|
|
||||||
@apply font-inter text-sm font-460;
|
|
||||||
line-height: 21px; /* 150% */
|
|
||||||
letter-spacing: -0.28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* button-text-small: Text for smaller buttons */
|
|
||||||
.text-button-small {
|
|
||||||
@apply font-inter text-xs font-440;
|
|
||||||
line-height: 18px; /* 150% */
|
|
||||||
letter-spacing: -0.24px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,273 @@
|
|||||||
|
@mixin label-multiselect-hover {
|
||||||
|
&::after {
|
||||||
|
@apply text-n-brand;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
@apply bg-n-slate-3;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
@apply text-n-blue-11;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect {
|
||||||
|
&:not(.no-margin) {
|
||||||
|
@apply mb-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.invalid .multiselect__tags {
|
||||||
|
@apply border-0 outline outline-1 outline-n-ruby-8 dark:outline-n-ruby-8 hover:outline-n-ruby-9 dark:hover:outline-n-ruby-9 disabled:outline-n-ruby-8 dark:disabled:outline-n-ruby-8;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.multiselect--disabled {
|
||||||
|
@apply opacity-50 rounded-lg cursor-not-allowed pointer-events-auto;
|
||||||
|
|
||||||
|
.multiselect__select {
|
||||||
|
@apply cursor-not-allowed bg-transparent rounded-lg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect--active {
|
||||||
|
> .multiselect__tags {
|
||||||
|
@apply outline-n-blue-border;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect__select {
|
||||||
|
@apply min-h-[2.875rem] p-0 right-0 top-0;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
@apply right-0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect__content-wrapper {
|
||||||
|
@apply bg-n-alpha-black2 text-n-slate-12 backdrop-blur-[100px] border-0 border-none outline outline-1 outline-n-weak rounded-b-lg;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect__content {
|
||||||
|
@apply max-w-full;
|
||||||
|
|
||||||
|
.multiselect__option {
|
||||||
|
@apply text-sm font-normal flex justify-between items-center;
|
||||||
|
|
||||||
|
span {
|
||||||
|
@apply inline-block overflow-hidden text-ellipsis whitespace-nowrap w-fit;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
@apply mb-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
@apply bottom-0 flex items-center justify-center text-center relative px-1 leading-tight;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.multiselect__option--highlight {
|
||||||
|
@apply bg-n-alpha-black2 text-n-slate-12;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.multiselect__option--highlight:hover {
|
||||||
|
@apply bg-n-brand/10 text-n-slate-12;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
@apply bg-transparent text-center text-n-slate-12;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.multiselect__option--highlight::after {
|
||||||
|
@apply bg-transparent text-n-slate-12;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.multiselect__option--selected {
|
||||||
|
@apply bg-n-brand/20 text-n-slate-12;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
@apply bg-transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.multiselect__option--highlight:hover {
|
||||||
|
@apply bg-transparent;
|
||||||
|
|
||||||
|
&::after:hover {
|
||||||
|
@apply text-n-slate-12 bg-transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect__tags {
|
||||||
|
@apply bg-n-alpha-black2 border-0 grid items-center w-full border-none outline-1 outline outline-n-weak hover:outline-n-slate-6 m-0 min-h-[2.875rem] rounded-lg pt-0;
|
||||||
|
|
||||||
|
input {
|
||||||
|
@apply border-0 border-none bg-transparent dark:bg-transparent text-n-slate-12 placeholder:text-n-slate-10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect__spinner {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect__tags-wrap {
|
||||||
|
@apply inline-block leading-none mt-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect__placeholder {
|
||||||
|
@apply text-n-slate-10 font-normal pt-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect__tag {
|
||||||
|
@apply bg-n-alpha-white mt-1 text-n-slate-12 pr-6 pl-2.5 py-1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect__tag-icon {
|
||||||
|
@include label-multiselect-hover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect__input {
|
||||||
|
@apply text-sm h-[2.875rem] mb-0 p-0 shadow-none border-transparent hover:border-transparent hover:shadow-none focus:border-transparent focus:shadow-none active:border-transparent active:shadow-none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect__single {
|
||||||
|
@apply bg-transparent text-n-slate-12 inline-block mb-0 py-3 px-2.5 overflow-hidden whitespace-nowrap text-ellipsis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-labels-wrap {
|
||||||
|
&.has-edited,
|
||||||
|
&:hover {
|
||||||
|
.multiselect {
|
||||||
|
@apply cursor-pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect {
|
||||||
|
> .multiselect__select {
|
||||||
|
@apply invisible;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .multiselect__tags {
|
||||||
|
@apply outline-transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.multiselect--active > .multiselect__tags {
|
||||||
|
@apply outline-n-blue-border;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-wrap--small {
|
||||||
|
// To be removed one SLA reports date picker is created
|
||||||
|
&.tiny {
|
||||||
|
.multiselect.no-margin {
|
||||||
|
@apply min-h-[32px];
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect__select {
|
||||||
|
@apply min-h-[32px] h-8;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
@apply top-[60%];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect__tags {
|
||||||
|
@apply min-h-[32px] max-h-[32px];
|
||||||
|
|
||||||
|
.multiselect__single {
|
||||||
|
@apply pt-1 pb-1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect__tags,
|
||||||
|
.multiselect__input,
|
||||||
|
.multiselect {
|
||||||
|
@apply text-n-slate-12 rounded-lg text-sm min-h-[2.5rem];
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect__input {
|
||||||
|
@apply h-[2.375rem] min-h-[2.375rem];
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect__single {
|
||||||
|
@apply items-center flex m-0 text-sm max-h-[2.375rem] bg-transparent text-n-slate-12 py-3 px-0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect__placeholder {
|
||||||
|
@apply m-0 py-2 px-0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect__tag {
|
||||||
|
@apply py-[6px] my-[1px];
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect__select {
|
||||||
|
@apply min-h-[2.5rem];
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect--disabled .multiselect__current,
|
||||||
|
.multiselect--disabled .multiselect__select {
|
||||||
|
@apply bg-transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect--disabled {
|
||||||
|
background-color: rgba(var(--black-alpha-2)) !important;
|
||||||
|
|
||||||
|
.multiselect__tags {
|
||||||
|
@apply hover:outline-n-weak;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect--active {
|
||||||
|
.multiselect__select::before {
|
||||||
|
@apply top-[62%];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect__select::before {
|
||||||
|
top: 60% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-wrap--medium {
|
||||||
|
.multiselect__tags,
|
||||||
|
.multiselect__input {
|
||||||
|
@apply items-center flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect__tags,
|
||||||
|
.multiselect__input,
|
||||||
|
.multiselect {
|
||||||
|
@apply bg-n-alpha-black2 text-n-slate-12 text-sm h-12 min-h-[3rem];
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect__input {
|
||||||
|
@apply h-[2.875rem] min-h-[2.875rem];
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect__single {
|
||||||
|
@apply items-center flex m-0 text-sm py-1 px-0.5 bg-transparent text-n-slate-12;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect__placeholder {
|
||||||
|
@apply m-0 py-1 px-0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect__select {
|
||||||
|
@apply min-h-[3rem];
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect--disabled .multiselect__current,
|
||||||
|
.multiselect--disabled .multiselect__select {
|
||||||
|
@apply bg-transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect__tags-wrap {
|
||||||
|
@apply flex-shrink-0;
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -49,7 +49,7 @@ const handleFetchUsers = () => {
|
|||||||
<div class="flex flex-col gap-2 relative justify-between w-full">
|
<div class="flex flex-col gap-2 relative justify-between w-full">
|
||||||
<div class="flex items-center gap-3 justify-between w-full">
|
<div class="flex items-center gap-3 justify-between w-full">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<h3 class="text-heading-2 text-n-slate-12 line-clamp-1">
|
<h3 class="text-base font-medium text-n-slate-12 line-clamp-1">
|
||||||
{{ name }}
|
{{ name }}
|
||||||
</h3>
|
</h3>
|
||||||
<CardPopover
|
<CardPopover
|
||||||
@@ -78,7 +78,7 @@ const handleFetchUsers = () => {
|
|||||||
<Button icon="i-lucide-trash" sm slate ghost @click="handleDelete" />
|
<Button icon="i-lucide-trash" sm slate ghost @click="handleDelete" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-n-slate-11 text-body-para line-clamp-1 mb-0 py-1">
|
<p class="text-n-slate-11 text-sm line-clamp-1 mb-0 py-1">
|
||||||
{{ description }}
|
{{ description }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+3
-3
@@ -20,7 +20,7 @@ const handleClick = () => {
|
|||||||
<CardLayout class="[&>div]:px-5 cursor-pointer" @click="handleClick">
|
<CardLayout class="[&>div]:px-5 cursor-pointer" @click="handleClick">
|
||||||
<div class="flex flex-col items-start gap-2">
|
<div class="flex flex-col items-start gap-2">
|
||||||
<div class="flex justify-between w-full items-center">
|
<div class="flex justify-between w-full items-center">
|
||||||
<h3 class="text-n-slate-12 text-heading-2">{{ title }}</h3>
|
<h3 class="text-n-slate-12 text-base font-medium">{{ title }}</h3>
|
||||||
<Button
|
<Button
|
||||||
xs
|
xs
|
||||||
slate
|
slate
|
||||||
@@ -29,14 +29,14 @@ const handleClick = () => {
|
|||||||
@click.stop="handleClick"
|
@click.stop="handleClick"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-n-slate-11 text-body-para mb-0">{{ description }}</p>
|
<p class="text-n-slate-11 text-sm mb-0">{{ description }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul class="flex flex-col items-start gap-3 mt-3">
|
<ul class="flex flex-col items-start gap-3 mt-3">
|
||||||
<li
|
<li
|
||||||
v-for="feature in features"
|
v-for="feature in features"
|
||||||
:key="feature.id"
|
:key="feature.id"
|
||||||
class="flex items-center gap-3 text-body-para"
|
class="flex items-center gap-3 text-sm"
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
:icon="feature.icon"
|
:icon="feature.icon"
|
||||||
|
|||||||
+3
@@ -39,6 +39,7 @@ const policyA = withCount({
|
|||||||
description: 'Distributes conversations evenly among available agents',
|
description: 'Distributes conversations evenly among available agents',
|
||||||
assignmentOrder: 'round_robin',
|
assignmentOrder: 'round_robin',
|
||||||
conversationPriority: 'high',
|
conversationPriority: 'high',
|
||||||
|
enabled: true,
|
||||||
inboxes: [mockInboxes[0], mockInboxes[1]],
|
inboxes: [mockInboxes[0], mockInboxes[1]],
|
||||||
isFetchingInboxes: false,
|
isFetchingInboxes: false,
|
||||||
});
|
});
|
||||||
@@ -49,6 +50,7 @@ const policyB = withCount({
|
|||||||
description: 'Assigns based on capacity and workload',
|
description: 'Assigns based on capacity and workload',
|
||||||
assignmentOrder: 'capacity_based',
|
assignmentOrder: 'capacity_based',
|
||||||
conversationPriority: 'medium',
|
conversationPriority: 'medium',
|
||||||
|
enabled: true,
|
||||||
inboxes: [mockInboxes[2], mockInboxes[3]],
|
inboxes: [mockInboxes[2], mockInboxes[3]],
|
||||||
isFetchingInboxes: false,
|
isFetchingInboxes: false,
|
||||||
});
|
});
|
||||||
@@ -59,6 +61,7 @@ const emptyPolicy = withCount({
|
|||||||
description: 'Policy with no assigned inboxes',
|
description: 'Policy with no assigned inboxes',
|
||||||
assignmentOrder: 'manual',
|
assignmentOrder: 'manual',
|
||||||
conversationPriority: 'low',
|
conversationPriority: 'low',
|
||||||
|
enabled: false,
|
||||||
inboxes: [],
|
inboxes: [],
|
||||||
isFetchingInboxes: false,
|
isFetchingInboxes: false,
|
||||||
});
|
});
|
||||||
|
|||||||
+35
-14
@@ -15,6 +15,7 @@ const props = defineProps({
|
|||||||
assignmentOrder: { type: String, default: '' },
|
assignmentOrder: { type: String, default: '' },
|
||||||
conversationPriority: { type: String, default: '' },
|
conversationPriority: { type: String, default: '' },
|
||||||
assignedInboxCount: { type: Number, default: 0 },
|
assignedInboxCount: { type: Number, default: 0 },
|
||||||
|
enabled: { type: Boolean, default: false },
|
||||||
inboxes: { type: Array, default: () => [] },
|
inboxes: { type: Array, default: () => [] },
|
||||||
isFetchingInboxes: { type: Boolean, default: false },
|
isFetchingInboxes: { type: Boolean, default: false },
|
||||||
});
|
});
|
||||||
@@ -60,19 +61,39 @@ const handleFetchInboxes = () => {
|
|||||||
<div class="flex flex-col gap-2 relative justify-between w-full">
|
<div class="flex flex-col gap-2 relative justify-between w-full">
|
||||||
<div class="flex items-center gap-3 justify-between w-full">
|
<div class="flex items-center gap-3 justify-between w-full">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<h3 class="text-heading-2 text-n-slate-12 line-clamp-1">
|
<h3 class="text-base font-medium text-n-slate-12 line-clamp-1">
|
||||||
{{ name }}
|
{{ name }}
|
||||||
</h3>
|
</h3>
|
||||||
<CardPopover
|
<div class="flex items-center gap-2">
|
||||||
:title="
|
<div class="flex items-center rounded-md bg-n-alpha-2 h-6 px-2">
|
||||||
t('ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.INDEX.CARD.POPOVER')
|
<span
|
||||||
"
|
class="text-xs"
|
||||||
icon="i-lucide-inbox"
|
:class="enabled ? 'text-n-teal-11' : 'text-n-slate-12'"
|
||||||
:count="assignedInboxCount"
|
>
|
||||||
:items="inboxes"
|
{{
|
||||||
:is-fetching="isFetchingInboxes"
|
enabled
|
||||||
@fetch="handleFetchInboxes"
|
? t(
|
||||||
/>
|
'ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.INDEX.CARD.ACTIVE'
|
||||||
|
)
|
||||||
|
: t(
|
||||||
|
'ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.INDEX.CARD.INACTIVE'
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<CardPopover
|
||||||
|
:title="
|
||||||
|
t(
|
||||||
|
'ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.INDEX.CARD.POPOVER'
|
||||||
|
)
|
||||||
|
"
|
||||||
|
icon="i-lucide-inbox"
|
||||||
|
:count="assignedInboxCount"
|
||||||
|
:items="inboxes"
|
||||||
|
:is-fetching="isFetchingInboxes"
|
||||||
|
@fetch="handleFetchInboxes"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Button
|
<Button
|
||||||
@@ -89,18 +110,18 @@ const handleFetchInboxes = () => {
|
|||||||
<Button icon="i-lucide-trash" sm slate ghost @click="handleDelete" />
|
<Button icon="i-lucide-trash" sm slate ghost @click="handleDelete" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-n-slate-11 text-body-para line-clamp-1 mb-0 py-1">
|
<p class="text-n-slate-11 text-sm line-clamp-1 mb-0 py-1">
|
||||||
{{ description }}
|
{{ description }}
|
||||||
</p>
|
</p>
|
||||||
<div class="flex items-center gap-3 py-1.5">
|
<div class="flex items-center gap-3 py-1.5">
|
||||||
<span v-if="order" class="text-n-slate-11 text-body-para">
|
<span v-if="order" class="text-n-slate-11 text-sm">
|
||||||
{{
|
{{
|
||||||
`${t('ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.INDEX.CARD.ORDER')}:`
|
`${t('ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.INDEX.CARD.ORDER')}:`
|
||||||
}}
|
}}
|
||||||
<span class="text-n-slate-12">{{ order }}</span>
|
<span class="text-n-slate-12">{{ order }}</span>
|
||||||
</span>
|
</span>
|
||||||
<div v-if="order" class="w-px h-3 bg-n-strong" />
|
<div v-if="order" class="w-px h-3 bg-n-strong" />
|
||||||
<span v-if="priority" class="text-n-slate-11 text-body-para">
|
<span v-if="priority" class="text-n-slate-11 text-sm">
|
||||||
{{
|
{{
|
||||||
`${t('ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.INDEX.CARD.PRIORITY')}:`
|
`${t('ASSIGNMENT_POLICY.AGENT_ASSIGNMENT_POLICY.INDEX.CARD.PRIORITY')}:`
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -19,15 +19,11 @@ defineProps({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['delete', 'navigate']);
|
const emit = defineEmits(['delete']);
|
||||||
|
|
||||||
const handleDelete = itemId => {
|
const handleDelete = itemId => {
|
||||||
emit('delete', itemId);
|
emit('delete', itemId);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleNavigate = item => {
|
|
||||||
emit('navigate', item);
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -51,11 +47,7 @@ const handleNavigate = item => {
|
|||||||
:key="item.id"
|
:key="item.id"
|
||||||
class="grid grid-cols-4 items-center gap-3 min-w-0 w-full justify-between h-[3.25rem] ltr:pr-2 rtl:pl-2"
|
class="grid grid-cols-4 items-center gap-3 min-w-0 w-full justify-between h-[3.25rem] ltr:pr-2 rtl:pl-2"
|
||||||
>
|
>
|
||||||
<button
|
<div class="flex items-center gap-2 col-span-2">
|
||||||
type="button"
|
|
||||||
class="flex items-center gap-2 col-span-2 hover:bg-n-alpha-1 dark:hover:bg-n-alpha-2 rounded-lg py-1 px-1.5 -ml-1.5 transition-colors cursor-pointer group"
|
|
||||||
@click="handleNavigate(item)"
|
|
||||||
>
|
|
||||||
<Icon
|
<Icon
|
||||||
v-if="item.icon"
|
v-if="item.icon"
|
||||||
:icon="item.icon"
|
:icon="item.icon"
|
||||||
@@ -69,16 +61,10 @@ const handleNavigate = item => {
|
|||||||
:size="20"
|
:size="20"
|
||||||
rounded-full
|
rounded-full
|
||||||
/>
|
/>
|
||||||
<span
|
<span class="text-sm text-n-slate-12 truncate min-w-0">
|
||||||
class="text-sm text-n-slate-12 truncate min-w-0 group-hover:text-n-blue-11 dark:group-hover:text-n-blue-10 transition-colors"
|
|
||||||
>
|
|
||||||
{{ item.name }}
|
{{ item.name }}
|
||||||
</span>
|
</span>
|
||||||
<Icon
|
</div>
|
||||||
icon="i-lucide-external-link"
|
|
||||||
class="size-3.5 text-n-slate-10 opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="flex items-start gap-2 col-span-1">
|
<div class="flex items-start gap-2 col-span-1">
|
||||||
<span
|
<span
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
import { computed, ref, onMounted } from 'vue';
|
import { computed, ref, onMounted } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import AddDataDropdown from 'dashboard/components-next/AssignmentPolicy/components/AddDataDropdown.vue';
|
import AddDataDropdown from 'dashboard/components-next/AssignmentPolicy/components/AddDataDropdown.vue';
|
||||||
import LabelItem from 'dashboard/components-next/label/LabelItem.vue';
|
import LabelItem from 'dashboard/components-next/Label/LabelItem.vue';
|
||||||
import DurationInput from 'dashboard/components-next/input/DurationInput.vue';
|
import DurationInput from 'dashboard/components-next/input/DurationInput.vue';
|
||||||
import { DURATION_UNITS } from 'dashboard/components-next/input/constants';
|
import { DURATION_UNITS } from 'dashboard/components-next/input/constants';
|
||||||
|
|
||||||
|
|||||||
+4
-18
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted } from 'vue';
|
import { ref, onMounted } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import Input from 'dashboard/components-next/input/Input.vue';
|
import Input from 'dashboard/components-next/input/Input.vue';
|
||||||
import DurationInput from 'dashboard/components-next/input/DurationInput.vue';
|
import DurationInput from 'dashboard/components-next/input/DurationInput.vue';
|
||||||
@@ -15,9 +15,6 @@ const fairDistributionLimit = defineModel('fairDistributionLimit', {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// The model value is in seconds (for the backend/DB)
|
|
||||||
// DurationInput works in minutes internally
|
|
||||||
// We need to convert between seconds and minutes
|
|
||||||
const fairDistributionWindow = defineModel('fairDistributionWindow', {
|
const fairDistributionWindow = defineModel('fairDistributionWindow', {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 3600,
|
default: 3600,
|
||||||
@@ -28,17 +25,6 @@ const fairDistributionWindow = defineModel('fairDistributionWindow', {
|
|||||||
|
|
||||||
const windowUnit = ref(DURATION_UNITS.MINUTES);
|
const windowUnit = ref(DURATION_UNITS.MINUTES);
|
||||||
|
|
||||||
// Convert seconds to minutes for DurationInput
|
|
||||||
const windowInMinutes = computed({
|
|
||||||
get() {
|
|
||||||
return Math.floor((fairDistributionWindow.value || 0) / 60);
|
|
||||||
},
|
|
||||||
set(minutes) {
|
|
||||||
fairDistributionWindow.value = minutes * 60;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Detect unit based on minutes (converted from seconds)
|
|
||||||
const detectUnit = minutes => {
|
const detectUnit = minutes => {
|
||||||
const m = Number(minutes) || 0;
|
const m = Number(minutes) || 0;
|
||||||
if (m === 0) return DURATION_UNITS.MINUTES;
|
if (m === 0) return DURATION_UNITS.MINUTES;
|
||||||
@@ -48,7 +34,7 @@ const detectUnit = minutes => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
windowUnit.value = detectUnit(windowInMinutes.value);
|
windowUnit.value = detectUnit(fairDistributionWindow.value);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -87,9 +73,9 @@ onMounted(() => {
|
|||||||
<div
|
<div
|
||||||
class="flex items-center gap-2 flex-1 [&>select]:!bg-n-alpha-2 [&>select]:!outline-none [&>select]:hover:brightness-110"
|
class="flex items-center gap-2 flex-1 [&>select]:!bg-n-alpha-2 [&>select]:!outline-none [&>select]:hover:brightness-110"
|
||||||
>
|
>
|
||||||
<!-- allow 10 mins to 999 days (in minutes) -->
|
<!-- allow 10 mins to 999 days -->
|
||||||
<DurationInput
|
<DurationInput
|
||||||
v-model:model-value="windowInMinutes"
|
v-model:model-value="fairDistributionWindow"
|
||||||
v-model:unit="windowUnit"
|
v-model:unit="windowUnit"
|
||||||
:min="10"
|
:min="10"
|
||||||
:max="1438560"
|
:max="1438560"
|
||||||
|
|||||||
+22
-41
@@ -1,6 +1,4 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import Label from 'dashboard/components-next/label/Label.vue';
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
id: {
|
id: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -18,24 +16,12 @@ const props = defineProps({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
disabled: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
disabledLabel: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
disabledMessage: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['select']);
|
const emit = defineEmits(['select']);
|
||||||
|
|
||||||
const handleChange = () => {
|
const handleChange = () => {
|
||||||
if (!props.isActive && !props.disabled) {
|
if (!props.isActive) {
|
||||||
emit('select', props.id);
|
emit('select', props.id);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -43,37 +29,32 @@ const handleChange = () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="cursor-pointer rounded-xl outline outline-1 p-4 transition-all duration-200 bg-n-solid-1 py-4 ltr:pl-4 rtl:pr-4 ltr:pr-6 rtl:pl-6"
|
class="relative cursor-pointer rounded-xl outline outline-1 p-4 transition-all duration-200 bg-n-solid-1 py-4 ltr:pl-4 rtl:pr-4 ltr:pr-6 rtl:pl-6"
|
||||||
:class="[
|
:class="[
|
||||||
disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer',
|
isActive ? 'outline-n-blue-9' : 'outline-n-weak hover:outline-n-strong',
|
||||||
isActive ? 'outline-n-blue-9' : 'outline-n-weak',
|
|
||||||
!disabled && !isActive ? 'hover:outline-n-strong' : '',
|
|
||||||
]"
|
]"
|
||||||
@click="handleChange"
|
@click="handleChange"
|
||||||
>
|
>
|
||||||
<div class="flex flex-col gap-2 items-start">
|
<div class="absolute top-4 right-4">
|
||||||
<div class="flex items-center justify-between w-full gap-3">
|
<input
|
||||||
<div class="flex items-center gap-2">
|
:id="`${id}`"
|
||||||
<h3 class="text-heading-3 text-n-slate-12">
|
:checked="isActive"
|
||||||
{{ label }}
|
:value="id"
|
||||||
</h3>
|
:name="id"
|
||||||
<Label v-if="disabled" :label="disabledLabel" color="amber" compact />
|
type="radio"
|
||||||
</div>
|
class="h-4 w-4 border-n-slate-6 text-n-brand focus:ring-n-brand focus:ring-offset-0"
|
||||||
<input
|
@change="handleChange"
|
||||||
:id="`${id}`"
|
/>
|
||||||
:checked="isActive"
|
</div>
|
||||||
:value="id"
|
|
||||||
:name="id"
|
<!-- Content -->
|
||||||
:disabled="disabled"
|
<div class="flex flex-col gap-3 items-start">
|
||||||
type="radio"
|
<h3 class="text-sm font-medium text-n-slate-12">
|
||||||
class="h-4 w-4 border-n-slate-6 text-n-brand focus:ring-n-brand focus:ring-offset-0 flex-shrink-0"
|
{{ label }}
|
||||||
@change="handleChange"
|
</h3>
|
||||||
/>
|
<p class="text-sm text-n-slate-11">
|
||||||
</div>
|
{{ description }}
|
||||||
<p class="text-body-main text-n-slate-11">
|
|
||||||
{{ disabled && disabledMessage ? disabledMessage : description }}
|
|
||||||
</p>
|
</p>
|
||||||
<slot />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
+4
@@ -6,6 +6,7 @@ const policyName = ref('Round Robin Policy');
|
|||||||
const description = ref(
|
const description = ref(
|
||||||
'Distributes conversations evenly among available agents'
|
'Distributes conversations evenly among available agents'
|
||||||
);
|
);
|
||||||
|
const enabled = ref(true);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -18,10 +19,13 @@ const description = ref(
|
|||||||
<BaseInfo
|
<BaseInfo
|
||||||
v-model:policy-name="policyName"
|
v-model:policy-name="policyName"
|
||||||
v-model:description="description"
|
v-model:description="description"
|
||||||
|
v-model:enabled="enabled"
|
||||||
name-label="Policy Name"
|
name-label="Policy Name"
|
||||||
name-placeholder="Enter policy name"
|
name-placeholder="Enter policy name"
|
||||||
description-label="Description"
|
description-label="Description"
|
||||||
description-placeholder="Enter policy description"
|
description-placeholder="Enter policy description"
|
||||||
|
status-label="Status"
|
||||||
|
status-placeholder="Active"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Variant>
|
</Variant>
|
||||||
|
|||||||
@@ -21,10 +21,10 @@ const handleButtonClick = () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="flex flex-col w-full h-full overflow-hidden bg-n-surface-1">
|
<section class="flex flex-col w-full h-full overflow-hidden bg-n-surface-1">
|
||||||
<header class="sticky top-0 z-10 px-6">
|
<header class="sticky top-0 z-10 px-6 lg:px-0">
|
||||||
<div class="w-full max-w-5xl mx-auto">
|
<div class="w-full max-w-[60rem] mx-auto">
|
||||||
<div class="flex items-center justify-between w-full h-20 gap-2">
|
<div class="flex items-center justify-between w-full h-20 gap-2">
|
||||||
<span class="text-heading-1 text-n-slate-12">
|
<span class="text-xl font-medium text-n-slate-12">
|
||||||
{{ headerTitle }}
|
{{ headerTitle }}
|
||||||
</span>
|
</span>
|
||||||
<div
|
<div
|
||||||
@@ -43,8 +43,8 @@ const handleButtonClick = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main class="flex-1 px-6 overflow-y-auto">
|
<main class="flex-1 px-6 overflow-y-auto lg:px-0">
|
||||||
<div class="w-full max-w-5xl mx-auto py-4">
|
<div class="w-full max-w-[60rem] mx-auto py-4">
|
||||||
<slot name="default" />
|
<slot name="default" />
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const handleClick = () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="flex flex-col w-full outline-1 outline outline-n-container -outline-offset-1 group/cardLayout rounded-xl bg-n-solid-2"
|
class="flex flex-col w-full outline-1 outline outline-n-container group/cardLayout rounded-xl bg-n-solid-2"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex w-full gap-3 py-5"
|
class="flex w-full gap-3 py-5"
|
||||||
|
|||||||
+3
-3
@@ -15,11 +15,11 @@ const emit = defineEmits(['search', 'update:sort']);
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<header class="sticky top-0 z-10 px-6">
|
<header class="sticky top-0 z-10">
|
||||||
<div
|
<div
|
||||||
class="flex items-start sm:items-center justify-between w-full py-6 gap-2 mx-auto max-w-5xl"
|
class="flex items-start sm:items-center justify-between w-full py-6 px-6 gap-2 mx-auto max-w-[60rem]"
|
||||||
>
|
>
|
||||||
<span class="text-heading-1 truncate text-n-slate-12">
|
<span class="text-xl font-medium truncate text-n-slate-12">
|
||||||
{{ headerTitle }}
|
{{ headerTitle }}
|
||||||
</span>
|
</span>
|
||||||
<div class="flex items-center flex-row flex-shrink-0 gap-2">
|
<div class="flex items-center flex-row flex-shrink-0 gap-2">
|
||||||
|
|||||||
@@ -32,18 +32,17 @@ const updateCurrentPage = page => {
|
|||||||
@search="emit('search', $event)"
|
@search="emit('search', $event)"
|
||||||
@update:sort="emit('update:sort', $event)"
|
@update:sort="emit('update:sort', $event)"
|
||||||
/>
|
/>
|
||||||
<main class="flex-1 px-6 overflow-y-auto">
|
<main class="flex-1 overflow-y-auto">
|
||||||
<div class="w-full mx-auto max-w-5xl py-4">
|
<div class="w-full mx-auto max-w-[60rem]">
|
||||||
<slot name="default" />
|
<slot name="default" />
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<footer v-if="showPaginationFooter" class="sticky bottom-0 z-0">
|
<footer v-if="showPaginationFooter" class="sticky bottom-0 z-0 px-4 pb-4">
|
||||||
<PaginationFooter
|
<PaginationFooter
|
||||||
current-page-info="COMPANIES_LAYOUT.PAGINATION_FOOTER.SHOWING"
|
current-page-info="COMPANIES_LAYOUT.PAGINATION_FOOTER.SHOWING"
|
||||||
:current-page="currentPage"
|
:current-page="currentPage"
|
||||||
:total-items="totalItems"
|
:total-items="totalItems"
|
||||||
:items-per-page="25"
|
:items-per-page="25"
|
||||||
class="max-w-[67rem]"
|
|
||||||
@update:current-page="updateCurrentPage"
|
@update:current-page="updateCurrentPage"
|
||||||
/>
|
/>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { computed, watch, onMounted, ref } from 'vue';
|
|||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import { useMapGetter, useStore } from 'dashboard/composables/store';
|
import { useMapGetter, useStore } from 'dashboard/composables/store';
|
||||||
|
|
||||||
import LabelItem from 'dashboard/components-next/label/LabelItem.vue';
|
import LabelItem from 'dashboard/components-next/Label/LabelItem.vue';
|
||||||
import AddLabel from 'dashboard/components-next/label/AddLabel.vue';
|
import AddLabel from 'dashboard/components-next/Label/AddLabel.vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
contactId: {
|
contactId: {
|
||||||
|
|||||||
@@ -32,9 +32,9 @@ const emit = defineEmits([
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<header class="sticky top-0 z-10 px-6">
|
<header class="sticky top-0 z-10">
|
||||||
<div
|
<div
|
||||||
class="flex items-start sm:items-center justify-between w-full py-6 gap-2 mx-auto max-w-5xl"
|
class="flex items-start sm:items-center justify-between w-full py-6 px-6 gap-2 mx-auto max-w-[60rem]"
|
||||||
>
|
>
|
||||||
<span class="text-xl font-medium truncate text-n-slate-12">
|
<span class="text-xl font-medium truncate text-n-slate-12">
|
||||||
{{ headerTitle }}
|
{{ headerTitle }}
|
||||||
|
|||||||
+2
-1
@@ -68,7 +68,8 @@ const hasActiveSegments = computed(
|
|||||||
);
|
);
|
||||||
const activeSegmentName = computed(() => props.activeSegment?.name);
|
const activeSegmentName = computed(() => props.activeSegment?.name);
|
||||||
|
|
||||||
const openCreateNewContactDialog = () => {
|
const openCreateNewContactDialog = async () => {
|
||||||
|
await createNewContactDialogRef.value?.contactsFormRef.resetValidation();
|
||||||
createNewContactDialogRef.value?.dialogRef.open();
|
createNewContactDialogRef.value?.dialogRef.open();
|
||||||
};
|
};
|
||||||
const openContactImportDialog = () =>
|
const openContactImportDialog = () =>
|
||||||
|
|||||||
+1
-1
@@ -62,7 +62,7 @@ const activeFilterQueryData = computed(() => {
|
|||||||
t('CONTACTS_LAYOUT.FILTER.ACTIVE_FILTERS.CLEAR_FILTERS')
|
t('CONTACTS_LAYOUT.FILTER.ACTIVE_FILTERS.CLEAR_FILTERS')
|
||||||
"
|
"
|
||||||
:show-clear-button="!hasActiveSegments"
|
:show-clear-button="!hasActiveSegments"
|
||||||
class="max-w-5xl"
|
class="max-w-[60rem] px-6"
|
||||||
@open-filter="emit('openFilter')"
|
@open-filter="emit('openFilter')"
|
||||||
@clear-filters="emit('clearFilters')"
|
@clear-filters="emit('clearFilters')"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -98,8 +98,8 @@ const showPagination = computed(() => {
|
|||||||
@apply-filter="emit('applyFilter', $event)"
|
@apply-filter="emit('applyFilter', $event)"
|
||||||
@clear-filters="emit('clearFilters')"
|
@clear-filters="emit('clearFilters')"
|
||||||
/>
|
/>
|
||||||
<main class="flex-1 overflow-y-auto px-6">
|
<main class="flex-1 overflow-y-auto">
|
||||||
<div class="w-full mx-auto max-w-5xl">
|
<div class="w-full mx-auto max-w-[60rem]">
|
||||||
<ContactsActiveFiltersPreview
|
<ContactsActiveFiltersPreview
|
||||||
v-if="showActiveFiltersPreview"
|
v-if="showActiveFiltersPreview"
|
||||||
:active-segment="activeSegment"
|
:active-segment="activeSegment"
|
||||||
@@ -114,12 +114,11 @@ const showPagination = computed(() => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<footer v-if="showPagination" class="sticky bottom-0 z-0">
|
<footer v-if="showPagination" class="sticky bottom-0 z-0 px-4 pb-4">
|
||||||
<PaginationFooter
|
<PaginationFooter
|
||||||
current-page-info="CONTACTS_LAYOUT.PAGINATION_FOOTER.SHOWING"
|
current-page-info="CONTACTS_LAYOUT.PAGINATION_FOOTER.SHOWING"
|
||||||
:current-page="currentPage"
|
:current-page="currentPage"
|
||||||
:total-items="totalItems"
|
:total-items="totalItems"
|
||||||
class="max-w-[67rem]"
|
|
||||||
:items-per-page="itemsPerPage"
|
:items-per-page="itemsPerPage"
|
||||||
@update:current-page="updateCurrentPage"
|
@update:current-page="updateCurrentPage"
|
||||||
/>
|
/>
|
||||||
|
|||||||
+45
-46
@@ -1,10 +1,8 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue';
|
|
||||||
|
|
||||||
import Button from 'dashboard/components-next/button/Button.vue';
|
import Button from 'dashboard/components-next/button/Button.vue';
|
||||||
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||||
import Label from 'dashboard/components-next/label/Label.vue';
|
|
||||||
import AttributeBadge from 'dashboard/components-next/CustomAttributes/AttributeBadge.vue';
|
import AttributeBadge from 'dashboard/components-next/CustomAttributes/AttributeBadge.vue';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
attribute: {
|
attribute: {
|
||||||
@@ -20,7 +18,7 @@ const props = defineProps({
|
|||||||
const emit = defineEmits(['edit', 'delete']);
|
const emit = defineEmits(['edit', 'delete']);
|
||||||
|
|
||||||
const iconByType = {
|
const iconByType = {
|
||||||
text: 'i-lucide-menu',
|
text: 'i-lucide-align-justify',
|
||||||
checkbox: 'i-lucide-circle-check-big',
|
checkbox: 'i-lucide-circle-check-big',
|
||||||
list: 'i-lucide-list',
|
list: 'i-lucide-list',
|
||||||
date: 'i-lucide-calendar',
|
date: 'i-lucide-calendar',
|
||||||
@@ -30,60 +28,61 @@ const iconByType = {
|
|||||||
|
|
||||||
const attributeIcon = computed(() => {
|
const attributeIcon = computed(() => {
|
||||||
const typeKey = props.attribute.type?.toLowerCase();
|
const typeKey = props.attribute.type?.toLowerCase();
|
||||||
return iconByType[typeKey] || 'i-lucide-menu';
|
return iconByType[typeKey] || 'i-lucide-align-justify';
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col py-4 min-w-0">
|
<div
|
||||||
<div class="flex justify-between flex-row items-center gap-4 min-w-0">
|
class="flex flex-col gap-2 p-4 bg-n-solid-1 rounded-2xl outline outline-1 outline-n-container"
|
||||||
<div class="flex items-center gap-4 min-w-0">
|
>
|
||||||
<div
|
<div class="flex flex-wrap gap-2 justify-between items-center">
|
||||||
class="flex items-center flex-shrink-0 size-10 justify-center rounded-xl outline outline-1 outline-n-weak -outline-offset-1"
|
<div class="flex flex-wrap gap-2 items-center min-w-0">
|
||||||
>
|
<h4 class="text-sm font-medium truncate text-n-slate-12">
|
||||||
<Icon :icon="attributeIcon" class="size-4 text-n-slate-11" />
|
{{ attribute.label }}
|
||||||
</div>
|
</h4>
|
||||||
<div class="flex flex-col gap-1.5 items-start min-w-0 overflow-hidden">
|
<div class="w-px h-3 bg-n-strong" />
|
||||||
<div class="flex items-center gap-2 min-w-0">
|
<div class="flex gap-2 items-center text-sm text-n-slate-11">
|
||||||
<h4 class="text-heading-3 truncate text-n-slate-12 min-w-0">
|
<div class="flex items-center gap-1.5 text-n-slate-11">
|
||||||
{{ attribute.label }}
|
<Icon :icon="attributeIcon" class="size-4" />
|
||||||
</h4>
|
<span class="text-sm">{{ attribute.type }}</span>
|
||||||
<div class="flex items-center gap-1.5">
|
|
||||||
<Label :label="attribute.type" compact />
|
|
||||||
<AttributeBadge
|
|
||||||
v-for="badge in badges"
|
|
||||||
:key="badge.type"
|
|
||||||
:type="badge.type"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-[auto_1fr] items-center gap-1.5">
|
<div class="w-px h-3 bg-n-weak" />
|
||||||
<Icon icon="i-lucide-key-round" class="size-3.5 text-n-slate-11" />
|
<div class="flex items-center gap-1.5 text-n-slate-11">
|
||||||
<div class="flex items-center gap-2 min-w-0">
|
<Icon icon="i-lucide-key-round" class="size-4" />
|
||||||
<span class="text-body-main text-n-slate-11 truncate">
|
<span class="line-clamp-1 text-sm">{{ attribute.value }}</span>
|
||||||
{{ attribute.value }}
|
|
||||||
</span>
|
|
||||||
<template
|
|
||||||
v-if="attribute.attribute_description || attribute.description"
|
|
||||||
>
|
|
||||||
<div class="w-px h-3 rounded-lg bg-n-weak flex-shrink-0" />
|
|
||||||
<span class="text-body-main text-n-slate-11 truncate">
|
|
||||||
{{ attribute.attribute_description || attribute.description }}
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-3 justify-end flex-shrink-0">
|
<div class="flex gap-2 items-center">
|
||||||
|
<AttributeBadge
|
||||||
|
v-for="badge in badges"
|
||||||
|
:key="badge.type"
|
||||||
|
:type="badge.type"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="badges.length > 0"
|
||||||
|
class="w-px h-3 bg-n-strong ltr:ml-1.5 rtl:mr-1.5"
|
||||||
|
/>
|
||||||
<Button
|
<Button
|
||||||
icon="i-woot-edit-pen"
|
icon="i-lucide-pencil-line"
|
||||||
slate
|
size="sm"
|
||||||
sm
|
color="slate"
|
||||||
|
ghost
|
||||||
@click="emit('edit', attribute)"
|
@click="emit('edit', attribute)"
|
||||||
/>
|
/>
|
||||||
<Button icon="i-woot-bin" slate sm @click="emit('delete', attribute)" />
|
<div class="w-px h-3 bg-n-strong" />
|
||||||
|
<Button
|
||||||
|
icon="i-lucide-trash"
|
||||||
|
size="sm"
|
||||||
|
color="slate"
|
||||||
|
ghost
|
||||||
|
@click="emit('delete', attribute)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="mb-0 text-sm text-n-slate-11">
|
||||||
|
{{ attribute.attribute_description || attribute.description || '' }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
+3
-5
@@ -35,20 +35,18 @@ const handleDelete = () => {
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex justify-between items-center px-4 py-3 w-full">
|
<div class="flex justify-between items-center px-4 py-3 w-full">
|
||||||
<div class="flex gap-3 items-center">
|
<div class="flex gap-3 items-center">
|
||||||
<h5 class="text-heading-3 text-n-slate-12 line-clamp-1">
|
<h5 class="text-sm font-medium text-n-slate-12 line-clamp-1">
|
||||||
{{ attribute.label }}
|
{{ attribute.label }}
|
||||||
</h5>
|
</h5>
|
||||||
<div class="w-px h-2.5 bg-n-slate-5" />
|
<div class="w-px h-2.5 bg-n-slate-5" />
|
||||||
<div class="flex gap-1.5 items-center">
|
<div class="flex gap-1.5 items-center">
|
||||||
<Icon :icon="attributeIcon" class="size-4 text-n-slate-11" />
|
<Icon :icon="attributeIcon" class="size-4 text-n-slate-11" />
|
||||||
<span class="text-body-para text-n-slate-11">{{ attribute.type }}</span>
|
<span class="text-sm text-n-slate-11">{{ attribute.type }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-px h-2.5 bg-n-slate-5" />
|
<div class="w-px h-2.5 bg-n-slate-5" />
|
||||||
<div class="flex gap-1.5 items-center">
|
<div class="flex gap-1.5 items-center">
|
||||||
<Icon icon="i-lucide-key-round" class="size-4 text-n-slate-11" />
|
<Icon icon="i-lucide-key-round" class="size-4 text-n-slate-11" />
|
||||||
<span class="text-body-para text-n-slate-11">{{
|
<span class="text-sm text-n-slate-11">{{ attribute.value }}</span>
|
||||||
attribute.value
|
|
||||||
}}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2 items-center">
|
<div class="flex gap-2 items-center">
|
||||||
|
|||||||
+2
-2
@@ -129,10 +129,10 @@ const handleDelete = attribute => {
|
|||||||
<div class="flex flex-col gap-2 items-start px-5 py-4">
|
<div class="flex flex-col gap-2 items-start px-5 py-4">
|
||||||
<div class="flex justify-between items-center w-full">
|
<div class="flex justify-between items-center w-full">
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<h3 class="text-heading-2 text-n-slate-12">
|
<h3 class="text-base font-medium text-n-slate-12">
|
||||||
{{ $t('CONVERSATION_WORKFLOW.REQUIRED_ATTRIBUTES.TITLE') }}
|
{{ $t('CONVERSATION_WORKFLOW.REQUIRED_ATTRIBUTES.TITLE') }}
|
||||||
</h3>
|
</h3>
|
||||||
<p class="mb-0 text-body-para text-n-slate-11">
|
<p class="mb-0 text-sm text-n-slate-11">
|
||||||
{{ $t('CONVERSATION_WORKFLOW.REQUIRED_ATTRIBUTES.DESCRIPTION') }}
|
{{ $t('CONVERSATION_WORKFLOW.REQUIRED_ATTRIBUTES.DESCRIPTION') }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||||
import Label from 'dashboard/components-next/label/Label.vue';
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
type: {
|
type: {
|
||||||
@@ -19,13 +18,11 @@ const attributeConfig = {
|
|||||||
colorClass: 'text-n-blue-11',
|
colorClass: 'text-n-blue-11',
|
||||||
icon: 'i-lucide-message-circle',
|
icon: 'i-lucide-message-circle',
|
||||||
labelKey: 'ATTRIBUTES_MGMT.BADGES.PRE_CHAT',
|
labelKey: 'ATTRIBUTES_MGMT.BADGES.PRE_CHAT',
|
||||||
color: 'slate',
|
|
||||||
},
|
},
|
||||||
resolution: {
|
resolution: {
|
||||||
colorClass: 'text-n-teal-11',
|
colorClass: 'text-n-teal-11',
|
||||||
icon: 'i-lucide-circle-check-big',
|
icon: 'i-lucide-circle-check-big',
|
||||||
labelKey: 'ATTRIBUTES_MGMT.BADGES.RESOLUTION',
|
labelKey: 'ATTRIBUTES_MGMT.BADGES.RESOLUTION',
|
||||||
color: 'slate',
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const config = computed(
|
const config = computed(
|
||||||
@@ -34,9 +31,12 @@ const config = computed(
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Label :label="t(config.labelKey)" :color="config.color" compact>
|
<div
|
||||||
<template #icon>
|
class="flex gap-1 justify-center items-center px-1.5 py-1 rounded-md shadow outline-1 outline outline-n-container bg-n-solid-2"
|
||||||
<Icon :icon="config.icon" class="size-3.5 text-n-slate-12" />
|
>
|
||||||
</template>
|
<Icon :icon="config.icon" class="size-4" :class="config.colorClass" />
|
||||||
</Label>
|
<span class="text-xs" :class="config.colorClass">{{
|
||||||
|
t(config.labelKey)
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ defineProps({
|
|||||||
class="relative flex flex-col items-center justify-center w-full h-full overflow-hidden"
|
class="relative flex flex-col items-center justify-center w-full h-full overflow-hidden"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="relative w-full max-w-5xl mx-auto overflow-hidden h-full max-h-[28rem]"
|
class="relative w-full max-w-[60rem] mx-auto overflow-hidden h-full max-h-[28rem]"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="showBackdrop"
|
v-if="showBackdrop"
|
||||||
|
|||||||
@@ -59,8 +59,8 @@ const togglePortalSwitcher = () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="flex flex-col w-full h-full overflow-hidden bg-n-surface-1">
|
<section class="flex flex-col w-full h-full overflow-hidden bg-n-surface-1">
|
||||||
<header class="sticky top-0 z-10 px-6 pb-3">
|
<header class="sticky top-0 z-10 px-6 pb-3 lg:px-0">
|
||||||
<div class="w-full max-w-5xl mx-auto">
|
<div class="w-full max-w-[60rem] mx-auto lg:px-6">
|
||||||
<div
|
<div
|
||||||
v-if="showHeaderTitle"
|
v-if="showHeaderTitle"
|
||||||
class="flex items-center justify-start h-20 gap-2"
|
class="flex items-center justify-start h-20 gap-2"
|
||||||
@@ -95,17 +95,16 @@ const togglePortalSwitcher = () => {
|
|||||||
<slot name="header-actions" />
|
<slot name="header-actions" />
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main class="flex-1 px-6 overflow-y-auto">
|
<main class="flex-1 px-6 overflow-y-auto lg:px-0">
|
||||||
<div class="w-full max-w-5xl mx-auto py-3">
|
<div class="w-full max-w-[60rem] mx-auto py-3 lg:px-6">
|
||||||
<slot name="content" />
|
<slot name="content" />
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<footer v-if="showPaginationFooter" class="sticky bottom-0 z-10">
|
<footer v-if="showPaginationFooter" class="sticky bottom-0 z-10 px-4 pb-4">
|
||||||
<PaginationFooter
|
<PaginationFooter
|
||||||
:current-page="currentPage"
|
:current-page="currentPage"
|
||||||
:total-items="totalItems"
|
:total-items="totalItems"
|
||||||
:items-per-page="itemsPerPage"
|
:items-per-page="itemsPerPage"
|
||||||
class="max-w-[67rem]"
|
|
||||||
@update:current-page="updateCurrentPage"
|
@update:current-page="updateCurrentPage"
|
||||||
/>
|
/>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
-1
@@ -62,7 +62,6 @@ const onCreate = async () => {
|
|||||||
from: route.name,
|
from: route.name,
|
||||||
});
|
});
|
||||||
|
|
||||||
selectedLocale.value = '';
|
|
||||||
dialogRef.value?.close();
|
dialogRef.value?.close();
|
||||||
useAlert(
|
useAlert(
|
||||||
t('HELP_CENTER.LOCALES_PAGE.ADD_LOCALE_DIALOG.API.SUCCESS_MESSAGE')
|
t('HELP_CENTER.LOCALES_PAGE.ADD_LOCALE_DIALOG.API.SUCCESS_MESSAGE')
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { reactive, ref, computed, onMounted, watch } from 'vue';
|
import { ref, computed, onMounted, watch } from 'vue';
|
||||||
import { useStore, useMapGetter } from 'dashboard/composables/store';
|
import { useStore, useMapGetter } from 'dashboard/composables/store';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useWindowSize } from '@vueuse/core';
|
import { useWindowSize } from '@vueuse/core';
|
||||||
@@ -59,23 +59,6 @@ const isFetchingInboxes = ref(false);
|
|||||||
const isSearching = ref(false);
|
const isSearching = ref(false);
|
||||||
const showComposeNewConversation = ref(false);
|
const showComposeNewConversation = ref(false);
|
||||||
|
|
||||||
const formState = reactive({
|
|
||||||
message: '',
|
|
||||||
subject: '',
|
|
||||||
ccEmails: '',
|
|
||||||
bccEmails: '',
|
|
||||||
attachedFiles: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
const clearFormState = () => {
|
|
||||||
Object.assign(formState, {
|
|
||||||
subject: '',
|
|
||||||
ccEmails: '',
|
|
||||||
bccEmails: '',
|
|
||||||
attachedFiles: [],
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const contactById = useMapGetter('contacts/getContactById');
|
const contactById = useMapGetter('contacts/getContactById');
|
||||||
const contactsUiFlags = useMapGetter('contacts/getUIFlags');
|
const contactsUiFlags = useMapGetter('contacts/getUIFlags');
|
||||||
const currentUser = useMapGetter('getCurrentUser');
|
const currentUser = useMapGetter('getCurrentUser');
|
||||||
@@ -157,14 +140,12 @@ const handleSelectedContact = async ({ value, action, ...rest }) => {
|
|||||||
|
|
||||||
const handleTargetInbox = inbox => {
|
const handleTargetInbox = inbox => {
|
||||||
targetInbox.value = inbox;
|
targetInbox.value = inbox;
|
||||||
if (!inbox) clearFormState();
|
|
||||||
resetContacts();
|
resetContacts();
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearSelectedContact = () => {
|
const clearSelectedContact = () => {
|
||||||
selectedContact.value = null;
|
selectedContact.value = null;
|
||||||
targetInbox.value = null;
|
targetInbox.value = null;
|
||||||
clearFormState();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const closeCompose = () => {
|
const closeCompose = () => {
|
||||||
@@ -179,12 +160,6 @@ const closeCompose = () => {
|
|||||||
emit('close');
|
emit('close');
|
||||||
};
|
};
|
||||||
|
|
||||||
const discardCompose = () => {
|
|
||||||
clearFormState();
|
|
||||||
formState.message = '';
|
|
||||||
closeCompose();
|
|
||||||
};
|
|
||||||
|
|
||||||
const createConversation = async ({ payload, isFromWhatsApp }) => {
|
const createConversation = async ({ payload, isFromWhatsApp }) => {
|
||||||
try {
|
try {
|
||||||
const data = await store.dispatch('contactConversations/create', {
|
const data = await store.dispatch('contactConversations/create', {
|
||||||
@@ -196,7 +171,7 @@ const createConversation = async ({ payload, isFromWhatsApp }) => {
|
|||||||
to: `/app/accounts/${data.account_id}/conversations/${data.id}`,
|
to: `/app/accounts/${data.account_id}/conversations/${data.id}`,
|
||||||
message: t('COMPOSE_NEW_CONVERSATION.FORM.GO_TO_CONVERSATION'),
|
message: t('COMPOSE_NEW_CONVERSATION.FORM.GO_TO_CONVERSATION'),
|
||||||
};
|
};
|
||||||
discardCompose();
|
closeCompose();
|
||||||
useAlert(t('COMPOSE_NEW_CONVERSATION.FORM.SUCCESS_MESSAGE'), action);
|
useAlert(t('COMPOSE_NEW_CONVERSATION.FORM.SUCCESS_MESSAGE'), action);
|
||||||
return true; // Return success
|
return true; // Return success
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -218,11 +193,7 @@ watch(
|
|||||||
(currentContact, previousContact) => {
|
(currentContact, previousContact) => {
|
||||||
if (currentContact && props.contactId) {
|
if (currentContact && props.contactId) {
|
||||||
// Reset on contact change
|
// Reset on contact change
|
||||||
if (currentContact?.id !== previousContact?.id) {
|
if (currentContact?.id !== previousContact?.id) clearSelectedContact();
|
||||||
clearSelectedContact();
|
|
||||||
clearFormState();
|
|
||||||
formState.message = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
// First process the contactable inboxes to get the right structure
|
// First process the contactable inboxes to get the right structure
|
||||||
const processedInboxes = processContactableInboxes(
|
const processedInboxes = processContactableInboxes(
|
||||||
@@ -294,7 +265,6 @@ useKeyboardEvents(keyboardEvents);
|
|||||||
@click.self="onModalBackdropClick"
|
@click.self="onModalBackdropClick"
|
||||||
>
|
>
|
||||||
<ComposeNewConversationForm
|
<ComposeNewConversationForm
|
||||||
:form-state="formState"
|
|
||||||
:class="[{ 'mt-2': !viewInModal }, composePopoverClass]"
|
:class="[{ 'mt-2': !viewInModal }, composePopoverClass]"
|
||||||
:contacts="contacts"
|
:contacts="contacts"
|
||||||
:contact-id="contactId"
|
:contact-id="contactId"
|
||||||
@@ -315,7 +285,7 @@ useKeyboardEvents(keyboardEvents);
|
|||||||
@update-target-inbox="handleTargetInbox"
|
@update-target-inbox="handleTargetInbox"
|
||||||
@clear-selected-contact="clearSelectedContact"
|
@clear-selected-contact="clearSelectedContact"
|
||||||
@create-conversation="createConversation"
|
@create-conversation="createConversation"
|
||||||
@discard="discardCompose"
|
@discard="closeCompose"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+3
-4
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed } from 'vue';
|
import { reactive, ref, computed } from 'vue';
|
||||||
import { useVuelidate } from '@vuelidate/core';
|
import { useVuelidate } from '@vuelidate/core';
|
||||||
import { required, requiredIf } from '@vuelidate/validators';
|
import { required, requiredIf } from '@vuelidate/validators';
|
||||||
import { INBOX_TYPES } from 'dashboard/helper/inbox';
|
import { INBOX_TYPES } from 'dashboard/helper/inbox';
|
||||||
@@ -37,7 +37,6 @@ const props = defineProps({
|
|||||||
contactsUiFlags: { type: Object, default: null },
|
contactsUiFlags: { type: Object, default: null },
|
||||||
messageSignature: { type: String, default: '' },
|
messageSignature: { type: String, default: '' },
|
||||||
sendWithSignature: { type: Boolean, default: false },
|
sendWithSignature: { type: Boolean, default: false },
|
||||||
formState: { type: Object, required: true },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits([
|
const emit = defineEmits([
|
||||||
@@ -58,13 +57,13 @@ const showBccEmailsDropdown = ref(false);
|
|||||||
|
|
||||||
const isCreating = computed(() => props.contactConversationsUiFlags.isCreating);
|
const isCreating = computed(() => props.contactConversationsUiFlags.isCreating);
|
||||||
|
|
||||||
const state = props.formState || {
|
const state = reactive({
|
||||||
message: '',
|
message: '',
|
||||||
subject: '',
|
subject: '',
|
||||||
ccEmails: '',
|
ccEmails: '',
|
||||||
bccEmails: '',
|
bccEmails: '',
|
||||||
attachedFiles: [],
|
attachedFiles: [],
|
||||||
};
|
});
|
||||||
|
|
||||||
const inboxTypes = computed(() => ({
|
const inboxTypes = computed(() => ({
|
||||||
isEmail: props.targetInbox?.channelType === INBOX_TYPES.EMAIL,
|
isEmail: props.targetInbox?.channelType === INBOX_TYPES.EMAIL,
|
||||||
|
|||||||
@@ -95,7 +95,6 @@ const inputClass = computed(() => {
|
|||||||
:show-dropdown="showCcEmailsDropdown"
|
:show-dropdown="showCcEmailsDropdown"
|
||||||
:is-loading="isLoading"
|
:is-loading="isLoading"
|
||||||
type="email"
|
type="email"
|
||||||
allow-create
|
|
||||||
class="flex-1 min-h-7"
|
class="flex-1 min-h-7"
|
||||||
@focus="emit('updateDropdown', 'cc', true)"
|
@focus="emit('updateDropdown', 'cc', true)"
|
||||||
@input="emit('searchCcEmails', $event)"
|
@input="emit('searchCcEmails', $event)"
|
||||||
@@ -128,7 +127,6 @@ const inputClass = computed(() => {
|
|||||||
:show-dropdown="showBccEmailsDropdown"
|
:show-dropdown="showBccEmailsDropdown"
|
||||||
:is-loading="isLoading"
|
:is-loading="isLoading"
|
||||||
type="email"
|
type="email"
|
||||||
allow-create
|
|
||||||
class="flex-1 min-h-7"
|
class="flex-1 min-h-7"
|
||||||
focus-on-mount
|
focus-on-mount
|
||||||
@focus="emit('updateDropdown', 'bcc', true)"
|
@focus="emit('updateDropdown', 'bcc', true)"
|
||||||
|
|||||||
+1
-1
@@ -70,7 +70,7 @@ describe('composeConversationHelper', () => {
|
|||||||
const result = helpers.buildContactableInboxesList(inboxes);
|
const result = helpers.buildContactableInboxesList(inboxes);
|
||||||
expect(result[0]).toMatchObject({
|
expect(result[0]).toMatchObject({
|
||||||
id: 1,
|
id: 1,
|
||||||
icon: 'i-woot-mail',
|
icon: 'i-ri-mail-line',
|
||||||
label: 'Email Inbox (support@example.com)',
|
label: 'Email Inbox (support@example.com)',
|
||||||
action: 'inbox',
|
action: 'inbox',
|
||||||
value: 1,
|
value: 1,
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { useToggle } from '@vueuse/core';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
title: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
defaultOpen: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const [isOpen, toggle] = useToggle(props.defaultOpen);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
v-bind="$attrs"
|
|
||||||
class="flex items-center justify-between w-full cursor-pointer pb-2 pt-4 border-t border-n-weak"
|
|
||||||
@click="toggle()"
|
|
||||||
>
|
|
||||||
<span class="text-heading-2 text-n-slate-12 w-full">
|
|
||||||
{{ title }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div v-if="isOpen" class="w-full space-y-4 pt-4 mb-4">
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
defineProps({
|
|
||||||
label: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
helpText: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="w-full py-2 mb-2 [interpolate-size:allow-keywords]">
|
|
||||||
<div
|
|
||||||
class="grid grid-cols-1 lg:grid-cols-8 gap-1.5 lg:gap-4 items-start lg:items-center"
|
|
||||||
>
|
|
||||||
<label class="text-heading-3 text-n-slate-12 col-span-1 lg:col-span-2">
|
|
||||||
{{ label }}
|
|
||||||
</label>
|
|
||||||
<div class="col-span-1 lg:col-span-6">
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="helpText" class="grid grid-cols-1 lg:grid-cols-8">
|
|
||||||
<div class="col-span-1 lg:col-span-2 invisible" />
|
|
||||||
<p
|
|
||||||
v-if="helpText"
|
|
||||||
class="mt-1.5 col-span-1 lg:col-span-6 text-label-small text-n-slate-11 ltr:ml-1 rtl:mr-1"
|
|
||||||
>
|
|
||||||
{{ helpText }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<slot name="extra" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import ToggleSwitch from 'dashboard/components-next/switch/Switch.vue';
|
|
||||||
|
|
||||||
defineProps({
|
|
||||||
header: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
compact: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const modelValue = defineModel({ type: Boolean, default: false });
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
class="flex flex-col items-start outline outline-1 -outline-offset-1 outline-n-weak rounded-xl [interpolate-size:allow-keywords]"
|
|
||||||
>
|
|
||||||
<div class="flex flex-col gap-1 items-start w-full px-4 py-3">
|
|
||||||
<div class="flex items-center gap-3 w-full justify-between">
|
|
||||||
<span class="text-heading-3 text-n-slate-12">
|
|
||||||
{{ header }}
|
|
||||||
</span>
|
|
||||||
<ToggleSwitch v-model="modelValue" />
|
|
||||||
</div>
|
|
||||||
<span v-if="description" class="text-body-main text-n-slate-11">
|
|
||||||
{{ description }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="$slots.editor"
|
|
||||||
class="w-full border-t border-n-weak"
|
|
||||||
:class="{ 'p-0': compact, 'px-4 pb-4 pt-2': !compact }"
|
|
||||||
>
|
|
||||||
<slot name="editor" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -112,19 +112,6 @@ const containerStyles = computed(() => ({
|
|||||||
height: `${props.size}px`,
|
height: `${props.size}px`,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const borderRadiusClass = computed(() => {
|
|
||||||
if (props.roundedFull) {
|
|
||||||
return 'rounded-full';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Approximates 25% of size
|
|
||||||
if (props.size <= 16) return 'rounded'; // 4px
|
|
||||||
if (props.size <= 24) return 'rounded-md'; // 6px
|
|
||||||
if (props.size <= 32) return 'rounded-lg'; // 8px
|
|
||||||
if (props.size <= 48) return 'rounded-xl'; // 12px
|
|
||||||
return 'rounded-2xl'; // 16px
|
|
||||||
});
|
|
||||||
|
|
||||||
const avatarStyles = computed(() => ({
|
const avatarStyles = computed(() => ({
|
||||||
...containerStyles.value,
|
...containerStyles.value,
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
@@ -197,7 +184,7 @@ watch(
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<span
|
<span
|
||||||
class="relative inline-flex group/avatar z-0 flex-shrink-0 align-middle"
|
class="relative inline-flex group/avatar z-0 flex-shrink-0"
|
||||||
:style="containerStyles"
|
:style="containerStyles"
|
||||||
>
|
>
|
||||||
<!-- Status Badge -->
|
<!-- Status Badge -->
|
||||||
@@ -229,9 +216,9 @@ watch(
|
|||||||
<!-- Avatar Container -->
|
<!-- Avatar Container -->
|
||||||
<span
|
<span
|
||||||
role="img"
|
role="img"
|
||||||
class="relative inline-flex items-center justify-center object-cover overflow-hidden font-medium outline outline-1 -outline-offset-1 outline-[rgb(0_0_0_/_0.03)] dark:outline-[rgb(255_255_255_/_0.04)]"
|
class="relative inline-flex items-center justify-center object-cover overflow-hidden font-medium"
|
||||||
:class="[
|
:class="[
|
||||||
borderRadiusClass,
|
roundedFull ? 'rounded-full' : 'rounded-xl',
|
||||||
{
|
{
|
||||||
'dark:!bg-[var(--dark-bg)] dark:!text-[var(--dark-text)]':
|
'dark:!bg-[var(--dark-bg)] dark:!text-[var(--dark-text)]':
|
||||||
!showDefaultAvatar && (!src || !isImageValid),
|
!showDefaultAvatar && (!src || !isImageValid),
|
||||||
@@ -280,8 +267,7 @@ watch(
|
|||||||
:handle-image-upload="handleImageUpload"
|
:handle-image-upload="handleImageUpload"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="absolute inset-0 z-10 flex items-center justify-center invisible w-full h-full transition-all duration-300 ease-in-out opacity-0 bg-n-alpha-black1 group-hover/avatar:visible group-hover/avatar:opacity-100"
|
class="absolute inset-0 z-10 flex items-center justify-center invisible w-full h-full transition-all duration-300 ease-in-out opacity-0 rounded-xl bg-n-alpha-black1 group-hover/avatar:visible group-hover/avatar:opacity-100"
|
||||||
:class="borderRadiusClass"
|
|
||||||
@click="handleUploadAvatar"
|
@click="handleUploadAvatar"
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ const handleCreateAssistant = () => {
|
|||||||
<template>
|
<template>
|
||||||
<section class="flex flex-col w-full h-full overflow-hidden bg-n-surface-1">
|
<section class="flex flex-col w-full h-full overflow-hidden bg-n-surface-1">
|
||||||
<header class="sticky top-0 z-10 px-6">
|
<header class="sticky top-0 z-10 px-6">
|
||||||
<div class="w-full max-w-5xl mx-auto">
|
<div class="w-full max-w-[60rem] mx-auto">
|
||||||
<div
|
<div
|
||||||
class="flex items-start lg:items-center justify-between w-full py-6 lg:py-0 lg:h-20 gap-4 lg:gap-2 flex-col lg:flex-row"
|
class="flex items-start lg:items-center justify-between w-full py-6 lg:py-0 lg:h-20 gap-4 lg:gap-2 flex-col lg:flex-row"
|
||||||
>
|
>
|
||||||
@@ -140,9 +140,7 @@ const handleCreateAssistant = () => {
|
|||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
icon="i-lucide-chevron-down"
|
icon="i-lucide-chevron-down"
|
||||||
:variant="
|
variant="ghost"
|
||||||
showAssistantSwitcherDropdown ? 'faded' : 'ghost'
|
|
||||||
"
|
|
||||||
color="slate"
|
color="slate"
|
||||||
size="xs"
|
size="xs"
|
||||||
:disabled="isFetchingAssistants"
|
:disabled="isFetchingAssistants"
|
||||||
@@ -206,7 +204,7 @@ const handleCreateAssistant = () => {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main class="flex-1 px-6 overflow-y-auto">
|
<main class="flex-1 px-6 overflow-y-auto">
|
||||||
<div class="w-full max-w-5xl h-full mx-auto py-4">
|
<div class="w-full max-w-[60rem] h-full mx-auto py-4">
|
||||||
<slot v-if="!showPaywall" name="controls" />
|
<slot v-if="!showPaywall" name="controls" />
|
||||||
<div
|
<div
|
||||||
v-if="isFetching"
|
v-if="isFetching"
|
||||||
@@ -224,12 +222,11 @@ const handleCreateAssistant = () => {
|
|||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<footer v-if="showPaginationFooter" class="sticky bottom-0 z-10">
|
<footer v-if="showPaginationFooter" class="sticky bottom-0 z-10 px-4 pb-4">
|
||||||
<PaginationFooter
|
<PaginationFooter
|
||||||
:current-page="currentPage"
|
:current-page="currentPage"
|
||||||
:total-items="totalCount"
|
:total-items="totalCount"
|
||||||
:items-per-page="itemsPerPage"
|
:items-per-page="itemsPerPage"
|
||||||
class="max-w-[67rem]"
|
|
||||||
@update:current-page="handlePageChange"
|
@update:current-page="handlePageChange"
|
||||||
/>
|
/>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const openBilling = () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="w-full max-w-5xl mx-auto h-full max-h-[448px] grid place-content-center"
|
class="w-full max-w-[60rem] mx-auto h-full max-h-[448px] grid place-content-center"
|
||||||
>
|
>
|
||||||
<BasePaywallModal
|
<BasePaywallModal
|
||||||
class="mx-auto"
|
class="mx-auto"
|
||||||
|
|||||||
@@ -56,13 +56,8 @@ const selectedLabel = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const selectOption = option => {
|
const selectOption = option => {
|
||||||
if (selectedValue.value === option.value) {
|
selectedValue.value = option.value;
|
||||||
selectedValue.value = '';
|
emit('update:modelValue', option.value);
|
||||||
emit('update:modelValue', '');
|
|
||||||
} else {
|
|
||||||
selectedValue.value = option.value;
|
|
||||||
emit('update:modelValue', option.value);
|
|
||||||
}
|
|
||||||
open.value = false;
|
open.value = false;
|
||||||
search.value = '';
|
search.value = '';
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -79,8 +79,8 @@ defineExpose({
|
|||||||
:aria-multiselectable="multiple"
|
:aria-multiselectable="multiple"
|
||||||
>
|
>
|
||||||
<li
|
<li
|
||||||
v-for="(option, index) in options"
|
v-for="option in options"
|
||||||
:key="`${option.value}-${index}`"
|
:key="option.value"
|
||||||
class="flex items-center justify-between w-full gap-2 px-3 py-2 text-sm transition-colors duration-150 cursor-pointer hover:bg-n-alpha-2"
|
class="flex items-center justify-between w-full gap-2 px-3 py-2 text-sm transition-colors duration-150 cursor-pointer hover:bg-n-alpha-2"
|
||||||
:class="{
|
:class="{
|
||||||
'bg-n-alpha-2': isSelected(option),
|
'bg-n-alpha-2': isSelected(option),
|
||||||
|
|||||||
@@ -53,11 +53,6 @@ const props = defineProps({
|
|||||||
default: 'lg',
|
default: 'lg',
|
||||||
validator: value => ['3xl', '2xl', 'xl', 'lg', 'md', 'sm'].includes(value),
|
validator: value => ['3xl', '2xl', 'xl', 'lg', 'md', 'sm'].includes(value),
|
||||||
},
|
},
|
||||||
position: {
|
|
||||||
type: String,
|
|
||||||
default: 'center',
|
|
||||||
validator: value => ['center', 'top'].includes(value),
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['confirm', 'close']);
|
const emit = defineEmits(['confirm', 'close']);
|
||||||
@@ -66,7 +61,6 @@ const { t } = useI18n();
|
|||||||
|
|
||||||
const dialogRef = ref(null);
|
const dialogRef = ref(null);
|
||||||
const dialogContentRef = ref(null);
|
const dialogContentRef = ref(null);
|
||||||
const isOpen = ref(false);
|
|
||||||
|
|
||||||
const maxWidthClass = computed(() => {
|
const maxWidthClass = computed(() => {
|
||||||
const classesMap = {
|
const classesMap = {
|
||||||
@@ -81,19 +75,13 @@ const maxWidthClass = computed(() => {
|
|||||||
return classesMap[props.width] ?? 'max-w-md';
|
return classesMap[props.width] ?? 'max-w-md';
|
||||||
});
|
});
|
||||||
|
|
||||||
const positionClass = computed(() =>
|
|
||||||
props.position === 'top' ? 'dialog-position-top' : ''
|
|
||||||
);
|
|
||||||
|
|
||||||
const open = () => {
|
const open = () => {
|
||||||
isOpen.value = true;
|
|
||||||
dialogRef.value?.showModal();
|
dialogRef.value?.showModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
const close = () => {
|
const close = () => {
|
||||||
emit('close');
|
emit('close');
|
||||||
dialogRef.value?.close();
|
dialogRef.value?.close();
|
||||||
isOpen.value = false;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirm = () => {
|
const confirm = () => {
|
||||||
@@ -110,7 +98,6 @@ defineExpose({ open, close });
|
|||||||
class="w-full transition-all duration-300 ease-in-out shadow-xl rounded-xl"
|
class="w-full transition-all duration-300 ease-in-out shadow-xl rounded-xl"
|
||||||
:class="[
|
:class="[
|
||||||
maxWidthClass,
|
maxWidthClass,
|
||||||
positionClass,
|
|
||||||
overflowYAuto ? 'overflow-y-auto' : 'overflow-visible',
|
overflowYAuto ? 'overflow-y-auto' : 'overflow-visible',
|
||||||
]"
|
]"
|
||||||
@close="close"
|
@close="close"
|
||||||
@@ -118,7 +105,7 @@ defineExpose({ open, close });
|
|||||||
<OnClickOutside @trigger="close">
|
<OnClickOutside @trigger="close">
|
||||||
<form
|
<form
|
||||||
ref="dialogContentRef"
|
ref="dialogContentRef"
|
||||||
class="flex flex-col w-full h-auto gap-6 p-6 overflow-visible text-start align-middle transition-all duration-300 ease-in-out transform bg-n-alpha-3 backdrop-blur-[100px] shadow-xl rounded-xl"
|
class="flex flex-col w-full h-auto gap-6 p-6 overflow-visible text-left align-middle transition-all duration-300 ease-in-out transform bg-n-alpha-3 backdrop-blur-[100px] shadow-xl rounded-xl"
|
||||||
@submit.prevent="confirm"
|
@submit.prevent="confirm"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
@@ -132,7 +119,7 @@ defineExpose({ open, close });
|
|||||||
</p>
|
</p>
|
||||||
</slot>
|
</slot>
|
||||||
</div>
|
</div>
|
||||||
<slot v-if="isOpen" />
|
<slot />
|
||||||
<!-- Dialog content will be injected here -->
|
<!-- Dialog content will be injected here -->
|
||||||
<slot name="footer">
|
<slot name="footer">
|
||||||
<div
|
<div
|
||||||
@@ -169,9 +156,4 @@ defineExpose({ open, close });
|
|||||||
dialog::backdrop {
|
dialog::backdrop {
|
||||||
@apply bg-n-alpha-black1 backdrop-blur-[4px];
|
@apply bg-n-alpha-black1 backdrop-blur-[4px];
|
||||||
}
|
}
|
||||||
|
|
||||||
.dialog-position-top {
|
|
||||||
margin-top: clamp(2rem, 5vh, 5rem);
|
|
||||||
margin-bottom: auto;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -4,10 +4,6 @@ defineProps({
|
|||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
height: {
|
|
||||||
type: String,
|
|
||||||
default: 'max-h-96',
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -19,10 +15,7 @@ defineProps({
|
|||||||
>
|
>
|
||||||
{{ title }}
|
{{ title }}
|
||||||
</div>
|
</div>
|
||||||
<ul
|
<ul class="gap-2 grid reset-base list-none px-2 max-h-96 overflow-y-auto">
|
||||||
class="gap-2 grid reset-base list-none px-2 overflow-y-auto"
|
|
||||||
:class="height"
|
|
||||||
>
|
|
||||||
<slot />
|
<slot />
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+1
-1
@@ -50,7 +50,7 @@ const openLink = link => {
|
|||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<section
|
<section
|
||||||
class="absolute top-full mt-6 ltr:left-0 rtl:right-0 outline outline-1 outline-n-weak bg-n-alpha-3 backdrop-blur-[100px] rounded-xl p-4 w-80 z-20"
|
class="absolute top-full mt-6 ltr:left-0 rtl:right-0 outline outline-1 outline-n-weak bg-n-alpha-3 backdrop-blur-[100px] rounded-xl p-4 w-80"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="absolute -top-[0.77rem] ltr:left-12 rtl:right-12 w-6 h-6 ltr:rotate-45 rtl:-rotate-45 rtl:rounded-tr ltr:rounded-tl rtl:border-r ltr:border-l border-t border-n-weak bg-n-alpha-3 z-10"
|
class="absolute -top-[0.77rem] ltr:left-12 rtl:right-12 w-6 h-6 ltr:rotate-45 rtl:-rotate-45 rtl:rounded-tr ltr:rounded-tl rtl:border-r ltr:border-l border-t border-n-weak bg-n-alpha-3 z-10"
|
||||||
|
|||||||
@@ -50,12 +50,12 @@ const currentFilter = computed(() =>
|
|||||||
);
|
);
|
||||||
|
|
||||||
const getOperator = (filter, selectedOperator) => {
|
const getOperator = (filter, selectedOperator) => {
|
||||||
const operatorFromOptions = filter?.filterOperators?.find(
|
const operatorFromOptions = filter.filterOperators.find(
|
||||||
operator => operator.value === selectedOperator
|
operator => operator.value === selectedOperator
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!operatorFromOptions) {
|
if (!operatorFromOptions) {
|
||||||
return filter?.filterOperators?.[0];
|
return filter.filterOperators[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
return operatorFromOptions;
|
return operatorFromOptions;
|
||||||
@@ -138,11 +138,7 @@ const validate = () => {
|
|||||||
return !validationError.value;
|
return !validationError.value;
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetValidation = () => {
|
defineExpose({ validate });
|
||||||
showErrors.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
defineExpose({ validate, resetValidation });
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -170,20 +166,18 @@ defineExpose({ validate, resetValidation });
|
|||||||
<FilterSelect
|
<FilterSelect
|
||||||
v-model="filterOperator"
|
v-model="filterOperator"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
:options="currentFilter?.filterOperators"
|
:options="currentFilter.filterOperators"
|
||||||
/>
|
/>
|
||||||
<template v-if="currentOperator?.hasInput">
|
<template v-if="currentOperator.hasInput">
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
v-if="inputType === 'multiSelect'"
|
v-if="inputType === 'multiSelect'"
|
||||||
v-model="values"
|
v-model="values"
|
||||||
:options="currentFilter.options"
|
:options="currentFilter.options"
|
||||||
dropdown-max-height="max-h-72"
|
|
||||||
/>
|
/>
|
||||||
<SingleSelect
|
<SingleSelect
|
||||||
v-else-if="inputType === 'searchSelect'"
|
v-else-if="inputType === 'searchSelect'"
|
||||||
v-model="values"
|
v-model="values"
|
||||||
:options="currentFilter.options"
|
:options="currentFilter.options"
|
||||||
dropdown-max-height="max-h-64"
|
|
||||||
/>
|
/>
|
||||||
<SingleSelect
|
<SingleSelect
|
||||||
v-else-if="inputType === 'booleanSelect'"
|
v-else-if="inputType === 'booleanSelect'"
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ const { height } = useWindowSize();
|
|||||||
const { height: dropdownHeight } = useElementBounding(dropdownRef);
|
const { height: dropdownHeight } = useElementBounding(dropdownRef);
|
||||||
|
|
||||||
const selectedOption = computed(() => {
|
const selectedOption = computed(() => {
|
||||||
return props.options?.find(o => o.value === selected.value) || {};
|
return props.options.find(o => o.value === selected.value) || {};
|
||||||
});
|
});
|
||||||
|
|
||||||
const iconToRender = computed(() => {
|
const iconToRender = computed(() => {
|
||||||
@@ -87,25 +87,18 @@ const updateSelected = newValue => {
|
|||||||
</template>
|
</template>
|
||||||
<DropdownBody
|
<DropdownBody
|
||||||
ref="dropdownRef"
|
ref="dropdownRef"
|
||||||
class="min-w-56 z-50"
|
class="min-w-48 z-50"
|
||||||
:class="dropdownPosition"
|
:class="dropdownPosition"
|
||||||
strong
|
strong
|
||||||
>
|
>
|
||||||
<DropdownSection class="[&>ul]:max-h-72">
|
<DropdownSection class="[&>ul]:max-h-80">
|
||||||
<template v-for="option in options" :key="option.value">
|
<DropdownItem
|
||||||
<li
|
v-for="option in options"
|
||||||
v-if="option.disabled"
|
:key="option.value"
|
||||||
class="px-2 py-1.5 text-xs font-medium text-n-slate-10 select-none"
|
:label="option.label"
|
||||||
>
|
:icon="option.icon"
|
||||||
{{ option.label }}
|
@click="updateSelected(option.value)"
|
||||||
</li>
|
/>
|
||||||
<DropdownItem
|
|
||||||
v-else
|
|
||||||
:label="option.label"
|
|
||||||
:icon="option.icon"
|
|
||||||
@click="updateSelected(option.value)"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</DropdownSection>
|
</DropdownSection>
|
||||||
</DropdownBody>
|
</DropdownBody>
|
||||||
</DropdownContainer>
|
</DropdownContainer>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import DropdownSection from 'next/dropdown-menu/base/DropdownSection.vue';
|
|||||||
import DropdownBody from 'next/dropdown-menu/base/DropdownBody.vue';
|
import DropdownBody from 'next/dropdown-menu/base/DropdownBody.vue';
|
||||||
import DropdownItem from 'next/dropdown-menu/base/DropdownItem.vue';
|
import DropdownItem from 'next/dropdown-menu/base/DropdownItem.vue';
|
||||||
|
|
||||||
const { options, maxChips, dropdownMaxHeight } = defineProps({
|
const { options, maxChips } = defineProps({
|
||||||
options: {
|
options: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true,
|
required: true,
|
||||||
@@ -17,10 +17,6 @@ const { options, maxChips, dropdownMaxHeight } = defineProps({
|
|||||||
type: Number,
|
type: Number,
|
||||||
default: 3,
|
default: 3,
|
||||||
},
|
},
|
||||||
dropdownMaxHeight: {
|
|
||||||
type: String,
|
|
||||||
default: 'max-h-80',
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
@@ -127,7 +123,7 @@ const toggleOption = option => {
|
|||||||
</Button>
|
</Button>
|
||||||
</template>
|
</template>
|
||||||
<DropdownBody class="top-0 min-w-48 z-50" strong>
|
<DropdownBody class="top-0 min-w-48 z-50" strong>
|
||||||
<DropdownSection :height="dropdownMaxHeight">
|
<DropdownSection class="[&>ul]:max-h-80">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
v-for="option in options"
|
v-for="option in options"
|
||||||
:key="option.id"
|
:key="option.id"
|
||||||
|
|||||||
@@ -12,12 +12,10 @@ import DropdownItem from 'next/dropdown-menu/base/DropdownItem.vue';
|
|||||||
const {
|
const {
|
||||||
options,
|
options,
|
||||||
disableSearch,
|
disableSearch,
|
||||||
disableDeselect,
|
|
||||||
placeholderIcon,
|
placeholderIcon,
|
||||||
placeholder,
|
placeholder,
|
||||||
placeholderTrailingIcon,
|
placeholderTrailingIcon,
|
||||||
searchPlaceholder,
|
searchPlaceholder,
|
||||||
dropdownMaxHeight,
|
|
||||||
} = defineProps({
|
} = defineProps({
|
||||||
options: {
|
options: {
|
||||||
type: Array,
|
type: Array,
|
||||||
@@ -43,14 +41,6 @@ const {
|
|||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
dropdownMaxHeight: {
|
|
||||||
type: String,
|
|
||||||
default: 'max-h-80',
|
|
||||||
},
|
|
||||||
disableDeselect: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
@@ -73,8 +63,6 @@ const selectedItem = computed(() => {
|
|||||||
const optionToSearch = Array.isArray(selected.value)
|
const optionToSearch = Array.isArray(selected.value)
|
||||||
? selected.value[0]
|
? selected.value[0]
|
||||||
: selected.value;
|
: selected.value;
|
||||||
|
|
||||||
if (!optionToSearch) return null;
|
|
||||||
// extract the selected item from the options array
|
// extract the selected item from the options array
|
||||||
// this ensures that options like icon is also included
|
// this ensures that options like icon is also included
|
||||||
return options.find(option => option.id === optionToSearch.id);
|
return options.find(option => option.id === optionToSearch.id);
|
||||||
@@ -89,7 +77,7 @@ const toggleSelected = option => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (selected.value && selected.value.id === optionToToggle.id) {
|
if (selected.value && selected.value.id === optionToToggle.id) {
|
||||||
if (!disableDeselect) selected.value = null;
|
selected.value = null;
|
||||||
} else {
|
} else {
|
||||||
selected.value = optionToToggle;
|
selected.value = optionToToggle;
|
||||||
}
|
}
|
||||||
@@ -136,7 +124,7 @@ const toggleSelected = option => {
|
|||||||
:placeholder="searchPlaceholder || t('COMBOBOX.SEARCH_PLACEHOLDER')"
|
:placeholder="searchPlaceholder || t('COMBOBOX.SEARCH_PLACEHOLDER')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<DropdownSection :height="dropdownMaxHeight">
|
<DropdownSection class="[&>ul]:max-h-80">
|
||||||
<template v-if="searchResults.length">
|
<template v-if="searchResults.length">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
v-for="option in searchResults"
|
v-for="option in searchResults"
|
||||||
|
|||||||
@@ -9,12 +9,12 @@ export function useChannelIcon(inbox) {
|
|||||||
'Channel::Sms': 'i-woot-sms',
|
'Channel::Sms': 'i-woot-sms',
|
||||||
'Channel::Telegram': 'i-woot-telegram',
|
'Channel::Telegram': 'i-woot-telegram',
|
||||||
'Channel::TwilioSms': 'i-woot-sms',
|
'Channel::TwilioSms': 'i-woot-sms',
|
||||||
'Channel::TwitterProfile': 'i-woot-x',
|
'Channel::TwitterProfile': 'i-ri-twitter-x-fill',
|
||||||
'Channel::WebWidget': 'i-woot-website',
|
'Channel::WebWidget': 'i-woot-website',
|
||||||
'Channel::Whatsapp': 'i-woot-whatsapp',
|
'Channel::Whatsapp': 'i-woot-whatsapp',
|
||||||
'Channel::Instagram': 'i-woot-instagram',
|
'Channel::Instagram': 'i-woot-instagram',
|
||||||
'Channel::Tiktok': 'i-woot-tiktok',
|
'Channel::Tiktok': 'i-woot-tiktok',
|
||||||
'Channel::Voice': 'i-woot-voice',
|
'Channel::Voice': 'i-ri-phone-fill',
|
||||||
};
|
};
|
||||||
|
|
||||||
const providerIconMap = {
|
const providerIconMap = {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ describe('useChannelIcon', () => {
|
|||||||
it('returns correct icon for Voice channel', () => {
|
it('returns correct icon for Voice channel', () => {
|
||||||
const inbox = { channel_type: 'Channel::Voice' };
|
const inbox = { channel_type: 'Channel::Voice' };
|
||||||
const { value: icon } = useChannelIcon(inbox);
|
const { value: icon } = useChannelIcon(inbox);
|
||||||
expect(icon).toBe('i-woot-voice');
|
expect(icon).toBe('i-ri-phone-fill');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns correct icon for Line channel', () => {
|
it('returns correct icon for Line channel', () => {
|
||||||
@@ -46,7 +46,7 @@ describe('useChannelIcon', () => {
|
|||||||
it('returns correct icon for Twitter channel', () => {
|
it('returns correct icon for Twitter channel', () => {
|
||||||
const inbox = { channel_type: 'Channel::TwitterProfile' };
|
const inbox = { channel_type: 'Channel::TwitterProfile' };
|
||||||
const { value: icon } = useChannelIcon(inbox);
|
const { value: icon } = useChannelIcon(inbox);
|
||||||
expect(icon).toBe('i-woot-x');
|
expect(icon).toBe('i-ri-twitter-x-fill');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns correct icon for WebWidget channel', () => {
|
it('returns correct icon for WebWidget channel', () => {
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ onMounted(() => {
|
|||||||
<label
|
<label
|
||||||
v-if="label"
|
v-if="label"
|
||||||
:for="uniqueId"
|
:for="uniqueId"
|
||||||
class="mb-0.5 text-heading-3 text-n-slate-12"
|
class="mb-0.5 text-sm font-medium text-n-slate-12"
|
||||||
>
|
>
|
||||||
{{ label }}
|
{{ label }}
|
||||||
</label>
|
</label>
|
||||||
@@ -145,7 +145,7 @@ onMounted(() => {
|
|||||||
/>
|
/>
|
||||||
<p
|
<p
|
||||||
v-if="message"
|
v-if="message"
|
||||||
class="min-w-0 mt-1 mb-0 text-label-small truncate transition-all duration-500 ease-in-out"
|
class="min-w-0 mt-1 mb-0 text-xs truncate transition-all duration-500 ease-in-out"
|
||||||
:class="messageClass"
|
:class="messageClass"
|
||||||
>
|
>
|
||||||
{{ message }}
|
{{ message }}
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { computed } from 'vue';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
label: {
|
|
||||||
type: [Object, String],
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
compact: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
color: {
|
|
||||||
type: String,
|
|
||||||
default: 'slate',
|
|
||||||
validator: value =>
|
|
||||||
['slate', 'amber', 'teal', 'ruby', 'blue', 'iris'].includes(value),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const COLOR_CLASSES = {
|
|
||||||
slate: 'bg-n-label-color outline-n-label-border text-n-slate-12',
|
|
||||||
amber: 'bg-n-amber-2 outline-n-amber-4 text-n-amber-11',
|
|
||||||
teal: 'bg-n-teal-2 outline-n-teal-4 text-n-teal-11',
|
|
||||||
ruby: 'bg-n-ruby-2 outline-n-ruby-4 text-n-ruby-11',
|
|
||||||
blue: 'bg-n-blue-2 outline-n-blue-4 text-n-blue-11',
|
|
||||||
iris: 'bg-n-iris-2 outline-n-iris-4 text-n-iris-11',
|
|
||||||
};
|
|
||||||
|
|
||||||
const isStringLabel = computed(() => typeof props.label === 'string');
|
|
||||||
|
|
||||||
const labelTitle = computed(() => {
|
|
||||||
return isStringLabel.value ? props.label : props.label?.title;
|
|
||||||
});
|
|
||||||
|
|
||||||
const labelDescription = computed(() => {
|
|
||||||
return (!isStringLabel.value && props.label?.description) || '';
|
|
||||||
});
|
|
||||||
|
|
||||||
const labelColor = computed(() => {
|
|
||||||
return isStringLabel.value ? null : props.label.color;
|
|
||||||
});
|
|
||||||
|
|
||||||
const colorClasses = computed(() => COLOR_CLASSES[props.color]);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
:title="labelDescription"
|
|
||||||
class="rounded-lg -outline-offset-1 outline outline-1 inline-flex items-center flex-shrink-0"
|
|
||||||
:class="[
|
|
||||||
colorClasses,
|
|
||||||
compact ? 'px-1.5 h-6 gap-1 rounded-md' : 'px-2.5 h-8 gap-1.5 rounded-lg',
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
v-if="labelColor"
|
|
||||||
class="rounded-sm flex-shrink-0"
|
|
||||||
:class="compact ? 'size-1.5' : 'size-2'"
|
|
||||||
:style="{ background: labelColor }"
|
|
||||||
/>
|
|
||||||
<slot v-else name="icon" />
|
|
||||||
<span
|
|
||||||
class="whitespace-nowrap"
|
|
||||||
:class="compact ? 'text-label-small' : 'text-label !font-420'"
|
|
||||||
>
|
|
||||||
{{ labelTitle }}
|
|
||||||
</span>
|
|
||||||
<slot name="action" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -43,7 +43,6 @@ import VoiceCallBubble from './bubbles/VoiceCall.vue';
|
|||||||
|
|
||||||
import MessageError from './MessageError.vue';
|
import MessageError from './MessageError.vue';
|
||||||
import ContextMenu from 'dashboard/modules/conversations/components/MessageContextMenu.vue';
|
import ContextMenu from 'dashboard/modules/conversations/components/MessageContextMenu.vue';
|
||||||
import { useBranding } from 'shared/composables/useBranding';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} Attachment
|
* @typedef {Object} Attachment
|
||||||
@@ -144,7 +143,6 @@ const { t } = useI18n();
|
|||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const inboxGetter = useMapGetter('inboxes/getInbox');
|
const inboxGetter = useMapGetter('inboxes/getInbox');
|
||||||
const inbox = computed(() => inboxGetter.value(props.inboxId) || {});
|
const inbox = computed(() => inboxGetter.value(props.inboxId) || {});
|
||||||
const { replaceInstallationName } = useBranding();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Computes the message variant based on props
|
* Computes the message variant based on props
|
||||||
@@ -391,17 +389,13 @@ const shouldRenderMessage = computed(() => {
|
|||||||
const isUnsupported = props.contentAttributes?.isUnsupported;
|
const isUnsupported = props.contentAttributes?.isUnsupported;
|
||||||
const isAnIntegrationMessage =
|
const isAnIntegrationMessage =
|
||||||
props.contentType === CONTENT_TYPES.INTEGRATIONS;
|
props.contentType === CONTENT_TYPES.INTEGRATIONS;
|
||||||
const isFailedMessage = props.status === MESSAGE_STATUS.FAILED;
|
|
||||||
const hasExternalError = !!props.contentAttributes?.externalError;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
hasAttachments ||
|
hasAttachments ||
|
||||||
props.content ||
|
props.content ||
|
||||||
isEmailContentType ||
|
isEmailContentType ||
|
||||||
isUnsupported ||
|
isUnsupported ||
|
||||||
isAnIntegrationMessage ||
|
isAnIntegrationMessage
|
||||||
isFailedMessage ||
|
|
||||||
hasExternalError
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -478,7 +472,7 @@ const avatarInfo = computed(() => {
|
|||||||
|
|
||||||
const avatarTooltip = computed(() => {
|
const avatarTooltip = computed(() => {
|
||||||
if (props.contentAttributes?.externalEcho) {
|
if (props.contentAttributes?.externalEcho) {
|
||||||
return replaceInstallationName(t('CONVERSATION.NATIVE_APP_ADVISORY'));
|
return t('CONVERSATION.NATIVE_APP_ADVISORY');
|
||||||
}
|
}
|
||||||
if (avatarInfo.value.name === '') return '';
|
if (avatarInfo.value.name === '') return '';
|
||||||
return `${t('CONVERSATION.SENT_BY')} ${avatarInfo.value.name}`;
|
return `${t('CONVERSATION.SENT_BY')} ${avatarInfo.value.name}`;
|
||||||
|
|||||||
@@ -12,16 +12,11 @@ defineProps({
|
|||||||
|
|
||||||
const emit = defineEmits(['retry']);
|
const emit = defineEmits(['retry']);
|
||||||
|
|
||||||
const { orientation, status, createdAt, content, attachments } =
|
const { orientation, status, createdAt } = useMessageContext();
|
||||||
useMessageContext();
|
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
const canRetry = computed(() => {
|
const canRetry = computed(() => !hasOneDayPassed(createdAt.value));
|
||||||
const hasContent = content.value !== null;
|
|
||||||
const hasAttachments = attachments.value && attachments.value.length > 0;
|
|
||||||
return !hasOneDayPassed(createdAt.value) && (hasContent || hasAttachments);
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -71,10 +71,10 @@ const pageInfo = computed(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="flex justify-between h-[3.375rem] w-full border-t border-n-weak mx-auto bg-n-surface-1 py-3 px-6 items-center before:absolute before:inset-x-0 before:-top-4 before:bg-gradient-to-t before:from-n-surface-1 before:from-0% before:to-transparent before:h-4 before:pointer-events-none"
|
class="flex justify-between h-12 w-full max-w-[calc(60rem-3px)] outline outline-n-container outline-1 -outline-offset-1 mx-auto bg-n-solid-2 rounded-xl py-2 ltr:pl-4 rtl:pr-4 ltr:pr-3 rtl:pl-3 items-center before:absolute before:inset-x-0 before:-top-4 before:bg-gradient-to-t before:from-n-surface-1 before:from-10% before:dark:from-0% before:to-transparent before:h-4 before:pointer-events-none"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<span class="min-w-0 text-body-main line-clamp-1 text-n-slate-11">
|
<span class="min-w-0 text-sm font-normal line-clamp-1 text-n-slate-11">
|
||||||
{{ currentPageInformation }}
|
{{ currentPageInformation }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -97,13 +97,11 @@ const pageInfo = computed(() => {
|
|||||||
:disabled="isFirstPage"
|
:disabled="isFirstPage"
|
||||||
@click="changePage(currentPage - 1)"
|
@click="changePage(currentPage - 1)"
|
||||||
/>
|
/>
|
||||||
<div class="inline-flex items-center gap-2 text-sm">
|
<div class="inline-flex items-center gap-2 text-sm text-n-slate-11">
|
||||||
<span
|
<span class="px-3 tabular-nums py-0.5 bg-n-alpha-black2 rounded-md">
|
||||||
class="px-3 tabular-nums py-0.5 font-420 bg-n-input-background text-body-main text-n-slate-12 rounded-md"
|
|
||||||
>
|
|
||||||
{{ formatFullNumber(currentPage) }}
|
{{ formatFullNumber(currentPage) }}
|
||||||
</span>
|
</span>
|
||||||
<span class="truncate text-body-main text-n-slate-11">
|
<span class="truncate">
|
||||||
{{ pageInfo }}
|
{{ pageInfo }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,97 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
|
||||||
|
|
||||||
defineProps({
|
|
||||||
options: {
|
|
||||||
type: Array,
|
|
||||||
default: () => [],
|
|
||||||
validator: options =>
|
|
||||||
options.every(
|
|
||||||
opt => typeof opt === 'object' && 'value' in opt && 'label' in opt
|
|
||||||
),
|
|
||||||
},
|
|
||||||
groups: {
|
|
||||||
type: Array,
|
|
||||||
default: () => [],
|
|
||||||
validator: groups =>
|
|
||||||
groups.every(
|
|
||||||
group =>
|
|
||||||
'label' in group &&
|
|
||||||
Array.isArray(group.options) &&
|
|
||||||
group.options.every(opt => 'value' in opt && 'label' in opt)
|
|
||||||
),
|
|
||||||
},
|
|
||||||
placeholder: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
disabled: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
error: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const modelValue = defineModel({
|
|
||||||
type: [String, Number, Boolean],
|
|
||||||
default: '',
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="w-fit relative">
|
|
||||||
<select
|
|
||||||
v-model="modelValue"
|
|
||||||
:disabled="disabled"
|
|
||||||
class="appearance-none bg-none rounded-lg border-0 outline-1 outline -outline-offset-1 transition-all duration-200 bg-n-surface-1 !mb-0 py-2 px-3 pr-10 text-sm"
|
|
||||||
:class="{
|
|
||||||
'outline-n-weak hover:outline-n-slate-6 focus:outline-n-blue-9':
|
|
||||||
!error && !disabled,
|
|
||||||
'outline-n-red-9 focus:outline-n-red-9': error && !disabled,
|
|
||||||
'outline-n-weak bg-n-slate-2 cursor-not-allowed opacity-60': disabled,
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<option v-if="placeholder" value="" disabled>
|
|
||||||
{{ placeholder }}
|
|
||||||
</option>
|
|
||||||
<template v-if="groups.length">
|
|
||||||
<optgroup
|
|
||||||
v-for="group in groups"
|
|
||||||
:key="group.label"
|
|
||||||
:label="group.label"
|
|
||||||
>
|
|
||||||
<option
|
|
||||||
v-for="option in group.options"
|
|
||||||
:key="option.value"
|
|
||||||
:value="option.value"
|
|
||||||
:disabled="option.disabled"
|
|
||||||
>
|
|
||||||
{{ option.label }}
|
|
||||||
</option>
|
|
||||||
</optgroup>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<option
|
|
||||||
v-for="option in options"
|
|
||||||
:key="option.value"
|
|
||||||
:value="option.value"
|
|
||||||
:disabled="option.disabled"
|
|
||||||
>
|
|
||||||
{{ option.label }}
|
|
||||||
</option>
|
|
||||||
</template>
|
|
||||||
</select>
|
|
||||||
<div
|
|
||||||
class="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none"
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
icon="i-lucide-chevron-down"
|
|
||||||
class="size-4 text-n-slate-11"
|
|
||||||
:class="{ 'opacity-50': disabled }"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -25,8 +25,8 @@ const reauthorizationRequired = computed(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<span class="size-4 grid place-content-center rounded-full">
|
<span class="size-5 grid place-content-center rounded-full bg-n-alpha-2">
|
||||||
<ChannelIcon :inbox="inbox" class="size-4" />
|
<ChannelIcon :inbox="inbox" class="size-3" />
|
||||||
</span>
|
</span>
|
||||||
<div class="flex-1 truncate min-w-0">{{ label }}</div>
|
<div class="flex-1 truncate min-w-0">{{ label }}</div>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { useStore } from 'vuex';
|
|||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useSidebarKeyboardShortcuts } from './useSidebarKeyboardShortcuts';
|
import { useSidebarKeyboardShortcuts } from './useSidebarKeyboardShortcuts';
|
||||||
import { vOnClickOutside } from '@vueuse/components';
|
import { vOnClickOutside } from '@vueuse/components';
|
||||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
|
||||||
import { useWindowSize, useEventListener } from '@vueuse/core';
|
import { useWindowSize, useEventListener } from '@vueuse/core';
|
||||||
import { emitter } from 'shared/helpers/mitt';
|
import { emitter } from 'shared/helpers/mitt';
|
||||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||||
@@ -51,18 +50,6 @@ const isRTL = useMapGetter('accounts/isRTL');
|
|||||||
const { width: windowWidth } = useWindowSize();
|
const { width: windowWidth } = useWindowSize();
|
||||||
const isMobile = computed(() => windowWidth.value < 768);
|
const isMobile = computed(() => windowWidth.value < 768);
|
||||||
|
|
||||||
const accountId = useMapGetter('getCurrentAccountId');
|
|
||||||
const isFeatureEnabledonAccount = useMapGetter(
|
|
||||||
'accounts/isFeatureEnabledonAccount'
|
|
||||||
);
|
|
||||||
|
|
||||||
const hasAdvancedAssignment = computed(() => {
|
|
||||||
return isFeatureEnabledonAccount.value(
|
|
||||||
accountId.value,
|
|
||||||
FEATURE_FLAGS.ADVANCED_ASSIGNMENT
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const toggleShortcutModalFn = show => {
|
const toggleShortcutModalFn = show => {
|
||||||
if (show) {
|
if (show) {
|
||||||
emit('openKeyShortcutModal');
|
emit('openKeyShortcutModal');
|
||||||
@@ -286,7 +273,7 @@ const menuItems = computed(() => {
|
|||||||
children: sortedInboxes.value.map(inbox => ({
|
children: sortedInboxes.value.map(inbox => ({
|
||||||
name: `${inbox.name}-${inbox.id}`,
|
name: `${inbox.name}-${inbox.id}`,
|
||||||
label: inbox.name,
|
label: inbox.name,
|
||||||
icon: h(ChannelIcon, { inbox, class: 'size-[16px]' }),
|
icon: h(ChannelIcon, { inbox, class: 'size-[12px]' }),
|
||||||
to: accountScopedRoute('inbox_dashboard', { inbox_id: inbox.id }),
|
to: accountScopedRoute('inbox_dashboard', { inbox_id: inbox.id }),
|
||||||
component: leafProps =>
|
component: leafProps =>
|
||||||
h(ChannelLeaf, {
|
h(ChannelLeaf, {
|
||||||
@@ -595,49 +582,18 @@ const menuItems = computed(() => {
|
|||||||
name: 'Settings Teams',
|
name: 'Settings Teams',
|
||||||
label: t('SIDEBAR.TEAMS'),
|
label: t('SIDEBAR.TEAMS'),
|
||||||
icon: 'i-lucide-users',
|
icon: 'i-lucide-users',
|
||||||
activeOn: [
|
|
||||||
'settings_teams_list',
|
|
||||||
'settings_teams_new',
|
|
||||||
'settings_teams_finish',
|
|
||||||
'settings_teams_add_agents',
|
|
||||||
'settings_teams_show',
|
|
||||||
'settings_teams_edit',
|
|
||||||
'settings_teams_edit_members',
|
|
||||||
'settings_teams_edit_finish',
|
|
||||||
],
|
|
||||||
to: accountScopedRoute('settings_teams_list'),
|
to: accountScopedRoute('settings_teams_list'),
|
||||||
},
|
},
|
||||||
...(hasAdvancedAssignment.value
|
{
|
||||||
? [
|
name: 'Settings Agent Assignment',
|
||||||
{
|
label: t('SIDEBAR.AGENT_ASSIGNMENT'),
|
||||||
name: 'Settings Agent Assignment',
|
icon: 'i-lucide-user-cog',
|
||||||
label: t('SIDEBAR.AGENT_ASSIGNMENT'),
|
to: accountScopedRoute('assignment_policy_index'),
|
||||||
icon: 'i-lucide-user-cog',
|
},
|
||||||
activeOn: [
|
|
||||||
'assignment_policy_index',
|
|
||||||
'agent_assignment_policy_index',
|
|
||||||
'agent_assignment_policy_create',
|
|
||||||
'agent_assignment_policy_edit',
|
|
||||||
'agent_capacity_policy_index',
|
|
||||||
'agent_capacity_policy_create',
|
|
||||||
'agent_capacity_policy_edit',
|
|
||||||
],
|
|
||||||
to: accountScopedRoute('assignment_policy_index'),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
{
|
{
|
||||||
name: 'Settings Inboxes',
|
name: 'Settings Inboxes',
|
||||||
label: t('SIDEBAR.INBOXES'),
|
label: t('SIDEBAR.INBOXES'),
|
||||||
icon: 'i-lucide-inbox',
|
icon: 'i-lucide-inbox',
|
||||||
activeOn: [
|
|
||||||
'settings_inbox_list',
|
|
||||||
'settings_inbox_show',
|
|
||||||
'settings_inbox_new',
|
|
||||||
'settings_inbox_finish',
|
|
||||||
'settings_inboxes_page_channel',
|
|
||||||
'settings_inboxes_add_agents',
|
|
||||||
],
|
|
||||||
to: accountScopedRoute('settings_inbox_list'),
|
to: accountScopedRoute('settings_inbox_list'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -730,7 +686,7 @@ const menuItems = computed(() => {
|
|||||||
closeMobileSidebar,
|
closeMobileSidebar,
|
||||||
{ ignore: ['#mobile-sidebar-launcher'] },
|
{ ignore: ['#mobile-sidebar-launcher'] },
|
||||||
]"
|
]"
|
||||||
class="bg-n-background flex flex-col text-sm pb-px fixed top-0 ltr:left-0 rtl:right-0 h-full z-40 w-[200px] md:w-auto md:relative md:flex-shrink-0 md:ltr:translate-x-0 md:rtl:translate-x-0 ltr:border-r rtl:border-l border-n-weak"
|
class="bg-n-background flex flex-col text-sm pb-0.5 fixed top-0 ltr:left-0 rtl:right-0 h-full z-40 w-[200px] md:w-auto md:relative md:flex-shrink-0 md:ltr:translate-x-0 md:rtl:translate-x-0 ltr:border-r rtl:border-l border-n-weak"
|
||||||
:class="[
|
:class="[
|
||||||
{
|
{
|
||||||
'shadow-lg md:shadow-none': isMobileSidebarOpen,
|
'shadow-lg md:shadow-none': isMobileSidebarOpen,
|
||||||
@@ -851,7 +807,7 @@ const menuItems = computed(() => {
|
|||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
class="px-1 py-1.5 flex-shrink-0 flex w-full z-50 gap-2 items-center border-t border-n-weak shadow-[0px_-2px_4px_0px_rgba(27,28,29,0.02)]"
|
class="p-1 flex-shrink-0 flex w-full z-50 gap-2 items-center border-t border-n-weak shadow-[0px_-2px_4px_0px_rgba(27,28,29,0.02)]"
|
||||||
:class="isEffectivelyCollapsed ? 'justify-center' : 'justify-between'"
|
:class="isEffectivelyCollapsed ? 'justify-center' : 'justify-between'"
|
||||||
>
|
>
|
||||||
<SidebarProfileMenu
|
<SidebarProfileMenu
|
||||||
|
|||||||
@@ -44,10 +44,8 @@ const shouldRenderComponent = computed(() => {
|
|||||||
:active
|
:active
|
||||||
/>
|
/>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<span v-if="icon" class="size-4 grid place-content-center rounded-full">
|
<Icon v-if="icon" :icon="icon" class="size-4 inline-block" />
|
||||||
<Icon :icon="icon" class="size-4 inline-block" />
|
<div class="flex-1 truncate min-w-0">{{ label }}</div>
|
||||||
</span>
|
|
||||||
<div class="flex-1 truncate min-w-0 text-sm">{{ label }}</div>
|
|
||||||
</template>
|
</template>
|
||||||
</component>
|
</component>
|
||||||
</Policy>
|
</Policy>
|
||||||
|
|||||||
@@ -19,24 +19,20 @@ const updateValue = () => {
|
|||||||
<template>
|
<template>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="group relative h-4 rounded-full w-7 flex-shrink-0 select-none focus:outline-none focus:ring-1 focus:ring-n-brand focus:ring-offset-n-slate-2 focus:ring-offset-2 transition-colors duration-200 ease-in-out"
|
class="relative h-4 transition-colors duration-200 ease-in-out rounded-full w-7 focus:outline-none focus:ring-1 focus:ring-n-brand focus:ring-offset-n-slate-2 focus:ring-offset-2 flex-shrink-0"
|
||||||
:class="modelValue ? 'bg-n-brand' : 'bg-n-slate-6'"
|
:class="modelValue ? 'bg-n-brand' : 'bg-n-slate-6 disabled:bg-n-slate-6/60'"
|
||||||
role="switch"
|
role="switch"
|
||||||
:aria-checked="modelValue"
|
:aria-checked="modelValue"
|
||||||
@click="updateValue"
|
@click="updateValue"
|
||||||
>
|
>
|
||||||
<span class="sr-only">{{ t('SWITCH.TOGGLE') }}</span>
|
<span class="sr-only">{{ t('SWITCH.TOGGLE') }}</span>
|
||||||
<span
|
<span
|
||||||
class="absolute top-1/2 ltr:left-0.5 rtl:right-0.5 -translate-y-1/2 transition-transform duration-[350ms] ease-[cubic-bezier(0.34,1.56,0.64,1)]"
|
class="absolute top-0.5 left-0.5 h-3 w-3 transform rounded-full shadow-sm transition-transform duration-200 ease-out"
|
||||||
:class="
|
:class="
|
||||||
modelValue
|
modelValue
|
||||||
? 'ltr:translate-x-3 rtl:-translate-x-3 group-active:ltr:translate-x-[6px] rtl:group-active:-translate-x-[6px]'
|
? 'translate-x-3 bg-n-background'
|
||||||
: 'ltr:translate-x-0 rtl:translate-x-0'
|
: 'translate-x-0 bg-n-background'
|
||||||
"
|
"
|
||||||
>
|
/>
|
||||||
<span
|
|
||||||
class="block h-3 w-3 rounded-full bg-n-background shadow-md transition-[width] duration-[180ms] ease-in-out group-active:w-[18px]"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,175 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { ref } from 'vue';
|
|
||||||
import { BaseTable, BaseTableRow, BaseTableCell } from './index';
|
|
||||||
import Button from '../button/Button.vue';
|
|
||||||
import Avatar from '../avatar/Avatar.vue';
|
|
||||||
import ToggleSwitch from '../switch/Switch.vue';
|
|
||||||
|
|
||||||
const automationData = ref([
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: 'Welcome Message',
|
|
||||||
description: 'Send welcome message to new contacts',
|
|
||||||
active: true,
|
|
||||||
createdOn: 'Apr 21, 2022',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: 'Auto-assign to Sales',
|
|
||||||
description: 'Automatically assign sales conversations to sales team',
|
|
||||||
active: false,
|
|
||||||
createdOn: 'May 15, 2022',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
name: 'Tag Premium Users',
|
|
||||||
description: 'Add premium tag to conversations from premium users',
|
|
||||||
active: true,
|
|
||||||
createdOn: 'Jun 10, 2022',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
const agentData = ref([
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: 'John Doe',
|
|
||||||
email: 'john@example.com',
|
|
||||||
role: 'Administrator',
|
|
||||||
verified: true,
|
|
||||||
avatarUrl: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: 'Jane Smith',
|
|
||||||
email: 'jane@example.com',
|
|
||||||
role: 'Agent',
|
|
||||||
verified: true,
|
|
||||||
avatarUrl: '',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
const emptyData = ref([]);
|
|
||||||
|
|
||||||
const headers = ['Name', 'Active', 'Created on', 'Actions'];
|
|
||||||
const agentHeaders = ['Agent', 'Role', 'Verification', 'Actions'];
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Story title="Components/Table" :layout="{ type: 'grid', width: '100%' }">
|
|
||||||
<!-- Basic Table -->
|
|
||||||
<Variant title="Basic Table">
|
|
||||||
<div class="p-4 bg-n-surface-1">
|
|
||||||
<BaseTable :headers="headers" :items="automationData">
|
|
||||||
<template #row="{ items }">
|
|
||||||
<BaseTableRow
|
|
||||||
v-for="automation in items"
|
|
||||||
:key="automation.id"
|
|
||||||
:item="automation"
|
|
||||||
>
|
|
||||||
<template #default>
|
|
||||||
<BaseTableCell>
|
|
||||||
<div class="flex items-center gap-2 min-w-0 max-w-full">
|
|
||||||
<span
|
|
||||||
class="text-body-main text-n-slate-12 truncate min-w-0 flex-1"
|
|
||||||
>
|
|
||||||
{{ automation.name }}
|
|
||||||
</span>
|
|
||||||
<div class="w-px h-3 rounded-lg bg-n-weak flex-shrink-0" />
|
|
||||||
<span
|
|
||||||
class="text-body-main text-n-slate-11 truncate min-w-0 flex-1"
|
|
||||||
>
|
|
||||||
{{ automation.description }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</BaseTableCell>
|
|
||||||
|
|
||||||
<BaseTableCell>
|
|
||||||
<div class="flex justify-center">
|
|
||||||
<ToggleSwitch v-model="automation.active" />
|
|
||||||
</div>
|
|
||||||
</BaseTableCell>
|
|
||||||
|
|
||||||
<BaseTableCell>
|
|
||||||
<span
|
|
||||||
class="text-body-main text-n-slate-12 whitespace-nowrap"
|
|
||||||
>
|
|
||||||
{{ automation.createdOn }}
|
|
||||||
</span>
|
|
||||||
</BaseTableCell>
|
|
||||||
|
|
||||||
<BaseTableCell align="end" class="w-24">
|
|
||||||
<div class="flex gap-3 justify-end flex-shrink-0">
|
|
||||||
<Button icon="i-woot-edit-pen" slate sm />
|
|
||||||
<Button icon="i-woot-bin" slate sm />
|
|
||||||
<Button icon="i-woot-clone" slate sm />
|
|
||||||
</div>
|
|
||||||
</BaseTableCell>
|
|
||||||
</template>
|
|
||||||
</BaseTableRow>
|
|
||||||
</template>
|
|
||||||
</BaseTable>
|
|
||||||
</div>
|
|
||||||
</Variant>
|
|
||||||
|
|
||||||
<!-- Table with Avatars -->
|
|
||||||
<Variant title="Table with Avatars">
|
|
||||||
<div class="p-4 bg-n-surface-1">
|
|
||||||
<BaseTable :headers="agentHeaders" :items="agentData">
|
|
||||||
<template #row="{ items }">
|
|
||||||
<BaseTableRow v-for="agent in items" :key="agent.id" :item="agent">
|
|
||||||
<template #default>
|
|
||||||
<BaseTableCell>
|
|
||||||
<div class="flex items-center gap-3 min-w-0">
|
|
||||||
<Avatar :user="agent" :size="40" class="flex-shrink-0" />
|
|
||||||
<div class="flex flex-col min-w-0">
|
|
||||||
<span class="text-body-main text-n-slate-12 truncate">
|
|
||||||
{{ agent.name }}
|
|
||||||
</span>
|
|
||||||
<span class="text-body-main text-n-slate-11 truncate">
|
|
||||||
{{ agent.email }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</BaseTableCell>
|
|
||||||
|
|
||||||
<BaseTableCell>
|
|
||||||
<span
|
|
||||||
class="text-body-main text-n-slate-12 whitespace-nowrap"
|
|
||||||
>
|
|
||||||
{{ agent.role }}
|
|
||||||
</span>
|
|
||||||
</BaseTableCell>
|
|
||||||
|
|
||||||
<BaseTableCell>
|
|
||||||
<span
|
|
||||||
class="text-body-main text-n-slate-12 whitespace-nowrap"
|
|
||||||
>
|
|
||||||
{{ agent.verified ? 'Verified' : 'Pending' }}
|
|
||||||
</span>
|
|
||||||
</BaseTableCell>
|
|
||||||
|
|
||||||
<BaseTableCell align="end" class="w-24">
|
|
||||||
<div class="flex gap-3 justify-end flex-shrink-0">
|
|
||||||
<Button icon="i-woot-edit-pen" slate sm />
|
|
||||||
<Button icon="i-woot-bin" slate sm />
|
|
||||||
</div>
|
|
||||||
</BaseTableCell>
|
|
||||||
</template>
|
|
||||||
</BaseTableRow>
|
|
||||||
</template>
|
|
||||||
</BaseTable>
|
|
||||||
</div>
|
|
||||||
</Variant>
|
|
||||||
|
|
||||||
<!-- Empty State -->
|
|
||||||
<Variant title="Empty State">
|
|
||||||
<div class="p-4 bg-n-surface-1">
|
|
||||||
<BaseTable
|
|
||||||
:headers="headers"
|
|
||||||
:items="emptyData"
|
|
||||||
no-data-message="No automation rules found"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Variant>
|
|
||||||
</Story>
|
|
||||||
</template>
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { computed } from 'vue';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
headers: {
|
|
||||||
type: Array,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
items: {
|
|
||||||
type: Array,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
noDataMessage: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
loading: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const hasHeaderSlot = computed(() => !!props.headers.length);
|
|
||||||
const showHeaders = computed(
|
|
||||||
() => hasHeaderSlot.value && props.items.length > 0
|
|
||||||
);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="w-full">
|
|
||||||
<table class="min-w-full table-auto divide-y divide-n-weak">
|
|
||||||
<thead v-if="showHeaders" class="border-t border-n-weak">
|
|
||||||
<tr>
|
|
||||||
<th
|
|
||||||
v-for="(header, index) in headers"
|
|
||||||
:key="index"
|
|
||||||
class="py-4 ltr:pr-4 rtl:pl-4 text-start text-heading-3 text-n-slate-12 capitalize"
|
|
||||||
>
|
|
||||||
<slot :name="`header-${index}`" :header="header">
|
|
||||||
{{ header }}
|
|
||||||
</slot>
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody class="divide-y divide-n-weak text-n-slate-11">
|
|
||||||
<template v-if="items.length">
|
|
||||||
<slot name="row" :items="items" />
|
|
||||||
</template>
|
|
||||||
<tr v-else-if="noDataMessage && !loading">
|
|
||||||
<td
|
|
||||||
:colspan="headers.length || 1"
|
|
||||||
class="py-20 text-center text-body-main !text-base text-n-slate-11"
|
|
||||||
>
|
|
||||||
{{ noDataMessage }}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
defineProps({
|
|
||||||
align: {
|
|
||||||
type: String,
|
|
||||||
default: 'start',
|
|
||||||
validator: value => ['start', 'center', 'end'].includes(value),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<td
|
|
||||||
class="py-3 ltr:pr-4 rtl:pl-4 text-body-main"
|
|
||||||
:class="{
|
|
||||||
'text-start': align === 'start',
|
|
||||||
'text-center': align === 'center',
|
|
||||||
'text-end': align === 'end',
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<slot />
|
|
||||||
</td>
|
|
||||||
</template>
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
defineProps({
|
|
||||||
item: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<tr>
|
|
||||||
<slot :item="item" />
|
|
||||||
</tr>
|
|
||||||
</template>
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export { default as BaseTable } from './BaseTable.vue';
|
|
||||||
export { default as BaseTableRow } from './BaseTableRow.vue';
|
|
||||||
export { default as BaseTableCell } from './BaseTableCell.vue';
|
|
||||||
@@ -36,11 +36,6 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
focusOnMount: { type: Boolean, default: false },
|
focusOnMount: { type: Boolean, default: false },
|
||||||
allowCreate: { type: Boolean, default: false },
|
allowCreate: { type: Boolean, default: false },
|
||||||
// Skip label-based dedup when the consumer already filters menuItems by ID.
|
|
||||||
// Prevents removing all same-name items when one is selected (e.g. duplicate agent names).
|
|
||||||
skipLabelDedup: { type: Boolean, default: false },
|
|
||||||
// When false, the dropdown won't auto-open on mount; it opens only on click/focus.
|
|
||||||
autoOpenDropdown: { type: Boolean, default: true },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits([
|
const emit = defineEmits([
|
||||||
@@ -61,7 +56,7 @@ const modelValue = defineModel({
|
|||||||
const tagInputRef = ref(null);
|
const tagInputRef = ref(null);
|
||||||
const tags = ref(props.modelValue);
|
const tags = ref(props.modelValue);
|
||||||
const newTag = ref('');
|
const newTag = ref('');
|
||||||
const isFocused = ref(props.autoOpenDropdown);
|
const isFocused = ref(true);
|
||||||
|
|
||||||
const rules = computed(() => getValidationRules(props.type));
|
const rules = computed(() => getValidationRules(props.type));
|
||||||
const v$ = useVuelidate(rules, { newTag });
|
const v$ = useVuelidate(rules, { newTag });
|
||||||
@@ -79,11 +74,11 @@ const showInput = computed(() =>
|
|||||||
const showDropdownMenu = computed(() =>
|
const showDropdownMenu = computed(() =>
|
||||||
props.mode === MODE.SINGLE && tags.value.length >= 1
|
props.mode === MODE.SINGLE && tags.value.length >= 1
|
||||||
? false
|
? false
|
||||||
: props.showDropdown && isFocused.value
|
: props.showDropdown
|
||||||
);
|
);
|
||||||
|
|
||||||
const filteredMenuItems = computed(() => {
|
const filteredMenuItems = computed(() =>
|
||||||
const items = buildTagMenuItems({
|
buildTagMenuItems({
|
||||||
mode: props.mode,
|
mode: props.mode,
|
||||||
tags: tags.value,
|
tags: tags.value,
|
||||||
menuItems: props.menuItems,
|
menuItems: props.menuItems,
|
||||||
@@ -91,14 +86,8 @@ const filteredMenuItems = computed(() => {
|
|||||||
isLoading: props.isLoading,
|
isLoading: props.isLoading,
|
||||||
type: props.type,
|
type: props.type,
|
||||||
isNewTagInValidType: isNewTagInValidType.value,
|
isNewTagInValidType: isNewTagInValidType.value,
|
||||||
allowCreate: props.allowCreate,
|
})
|
||||||
skipLabelDedup: props.skipLabelDedup,
|
);
|
||||||
});
|
|
||||||
if (props.type !== INPUT_TYPES.TEXT) return items;
|
|
||||||
const query = newTag.value?.trim()?.toLowerCase();
|
|
||||||
if (!query) return items;
|
|
||||||
return items.filter(item => item.label?.toLowerCase().includes(query));
|
|
||||||
});
|
|
||||||
|
|
||||||
const emitDataOnAdd = value => {
|
const emitDataOnAdd = value => {
|
||||||
const matchingMenuItem = findMatchingMenuItem(props.menuItems, value);
|
const matchingMenuItem = findMatchingMenuItem(props.menuItems, value);
|
||||||
@@ -123,13 +112,10 @@ const addTag = async () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isValidatedType = [INPUT_TYPES.EMAIL, INPUT_TYPES.TEL].includes(
|
if (
|
||||||
props.type
|
[INPUT_TYPES.EMAIL, INPUT_TYPES.TEL].includes(props.type) ||
|
||||||
);
|
props.allowCreate
|
||||||
|
) {
|
||||||
if (!isValidatedType && !props.allowCreate && props.showDropdown) return;
|
|
||||||
|
|
||||||
if (isValidatedType || props.allowCreate) {
|
|
||||||
if (!(await v$.value.$validate())) return;
|
if (!(await v$.value.$validate())) return;
|
||||||
emitDataOnAdd(trimmedTag);
|
emitDataOnAdd(trimmedTag);
|
||||||
}
|
}
|
||||||
@@ -139,31 +125,28 @@ const addTag = async () => {
|
|||||||
const removeTag = index => {
|
const removeTag = index => {
|
||||||
tags.value.splice(index, 1);
|
tags.value.splice(index, 1);
|
||||||
modelValue.value = tags.value;
|
modelValue.value = tags.value;
|
||||||
emit('remove', index);
|
emit('remove');
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDropdownAction = async ({
|
const handleDropdownAction = async ({
|
||||||
email: emailAddress,
|
email: emailAddress,
|
||||||
phoneNumber,
|
phoneNumber,
|
||||||
label,
|
|
||||||
...rest
|
...rest
|
||||||
}) => {
|
}) => {
|
||||||
if (props.mode === MODE.SINGLE && tags.value.length >= 1) return;
|
if (props.mode === MODE.SINGLE && tags.value.length >= 1) return;
|
||||||
if (!props.showDropdown) return;
|
if (!props.showDropdown) return;
|
||||||
|
|
||||||
const isEmail = props.type === INPUT_TYPES.EMAIL;
|
const isEmail = props.type === 'email';
|
||||||
const tagValue = isEmail ? emailAddress : phoneNumber || label;
|
newTag.value = isEmail ? emailAddress : phoneNumber;
|
||||||
|
|
||||||
if (isEmail || props.type === INPUT_TYPES.TEL) {
|
if (!(await v$.value.$validate())) return;
|
||||||
newTag.value = tagValue;
|
|
||||||
if (!(await v$.value.$validate())) return;
|
|
||||||
}
|
|
||||||
|
|
||||||
emit(
|
const payload = isEmail
|
||||||
'add',
|
? { email: emailAddress, ...rest }
|
||||||
isEmail ? { email: emailAddress, ...rest } : { phoneNumber, label, ...rest }
|
: { phoneNumber, ...rest };
|
||||||
);
|
|
||||||
updateValueAndFocus(tagValue);
|
emit('add', payload);
|
||||||
|
updateValueAndFocus(emailAddress);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFocus = () => {
|
const handleFocus = () => {
|
||||||
@@ -180,7 +163,7 @@ const handleKeydown = event => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleClickOutside = () => {
|
const handleClickOutside = () => {
|
||||||
isFocused.value = false;
|
if (tags.value.length) isFocused.value = false;
|
||||||
emit('onClickOutside');
|
emit('onClickOutside');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -261,71 +261,6 @@ describe('tagInputHelper', () => {
|
|||||||
});
|
});
|
||||||
expect(result).toEqual(menuItems);
|
expect(result).toEqual(menuItems);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not create suggestion when allowCreate is false', () => {
|
|
||||||
const result = buildTagMenuItems({
|
|
||||||
...baseParams,
|
|
||||||
type: INPUT_TYPES.EMAIL,
|
|
||||||
newTag: 'test@example.com',
|
|
||||||
allowCreate: false,
|
|
||||||
});
|
|
||||||
expect(result).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('still returns available menu items when allowCreate is false', () => {
|
|
||||||
const menuItems = [
|
|
||||||
{ label: 'Agent 1', value: '1' },
|
|
||||||
{ label: 'Agent 2', value: '2' },
|
|
||||||
];
|
|
||||||
const result = buildTagMenuItems({
|
|
||||||
...baseParams,
|
|
||||||
menuItems,
|
|
||||||
newTag: 'Agent',
|
|
||||||
allowCreate: false,
|
|
||||||
});
|
|
||||||
expect(result).toEqual(menuItems);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('creates new item when allowCreate is true (default)', () => {
|
|
||||||
const result = buildTagMenuItems({
|
|
||||||
...baseParams,
|
|
||||||
type: INPUT_TYPES.EMAIL,
|
|
||||||
newTag: 'test@example.com',
|
|
||||||
allowCreate: true,
|
|
||||||
});
|
|
||||||
expect(result).toHaveLength(1);
|
|
||||||
expect(result[0]).toMatchObject({
|
|
||||||
label: 'test@example.com',
|
|
||||||
action: 'create',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('skips label dedup when skipLabelDedup is true', () => {
|
|
||||||
const menuItems = [
|
|
||||||
{ label: 'HDMA', value: 1 },
|
|
||||||
{ label: 'HDMA', value: 2 },
|
|
||||||
];
|
|
||||||
const result = buildTagMenuItems({
|
|
||||||
...baseParams,
|
|
||||||
tags: ['HDMA'],
|
|
||||||
menuItems,
|
|
||||||
skipLabelDedup: true,
|
|
||||||
});
|
|
||||||
expect(result).toEqual(menuItems);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('filters by label when skipLabelDedup is false (default)', () => {
|
|
||||||
const menuItems = [
|
|
||||||
{ label: 'HDMA', value: 1 },
|
|
||||||
{ label: 'HDMA', value: 2 },
|
|
||||||
];
|
|
||||||
const result = buildTagMenuItems({
|
|
||||||
...baseParams,
|
|
||||||
tags: ['HDMA'],
|
|
||||||
menuItems,
|
|
||||||
});
|
|
||||||
expect(result).toEqual([]);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('canAddTag', () => {
|
describe('canAddTag', () => {
|
||||||
|
|||||||
@@ -84,29 +84,25 @@ export const buildTagMenuItems = ({
|
|||||||
isLoading,
|
isLoading,
|
||||||
type,
|
type,
|
||||||
isNewTagInValidType,
|
isNewTagInValidType,
|
||||||
allowCreate = true,
|
|
||||||
skipLabelDedup = false,
|
|
||||||
}) => {
|
}) => {
|
||||||
if (mode === MODE.SINGLE && tags.length >= 1) return [];
|
if (mode === MODE.SINGLE && tags.length >= 1) return [];
|
||||||
|
|
||||||
const availableMenuItems = skipLabelDedup
|
const availableMenuItems = menuItems.filter(
|
||||||
? menuItems
|
item => !tags.includes(item.label)
|
||||||
: menuItems.filter(item => !tags.includes(item.label));
|
);
|
||||||
|
|
||||||
// Show typed value as suggestion only if:
|
// Show typed value as suggestion only if:
|
||||||
// 1. There's a value being typed
|
// 1. There's a value being typed
|
||||||
// 2. The value isn't already in the tags
|
// 2. The value isn't already in the tags
|
||||||
// 3. Validation passes (email/phone) and There are no menu items available
|
// 3. Validation passes (email/phone) and There are no menu items available
|
||||||
// 4. allowCreate is enabled
|
|
||||||
const trimmedNewTag = newTag?.trim();
|
const trimmedNewTag = newTag?.trim();
|
||||||
const shouldShowCreateSuggestion =
|
const shouldShowTypedValue =
|
||||||
allowCreate &&
|
|
||||||
trimmedNewTag &&
|
trimmedNewTag &&
|
||||||
!tags.includes(trimmedNewTag) &&
|
!tags.includes(trimmedNewTag) &&
|
||||||
!isLoading &&
|
!isLoading &&
|
||||||
!availableMenuItems.length;
|
!availableMenuItems.length;
|
||||||
|
|
||||||
if (shouldShowCreateSuggestion) {
|
if (shouldShowTypedValue) {
|
||||||
const { isValid, formattedValue } = validateAndFormatNewTag(
|
const { isValid, formattedValue } = validateAndFormatNewTag(
|
||||||
trimmedNewTag,
|
trimmedNewTag,
|
||||||
type,
|
type,
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
year: {
|
year: {
|
||||||
type: [Number, String],
|
type: [Number, String],
|
||||||
default: '',
|
required: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ defineProps({
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="isComingSoon"
|
v-if="isComingSoon"
|
||||||
class="absolute inset-0 flex items-center justify-center backdrop-blur-[2px] rounded-2xl bg-gradient-to-br from-n-surface-1/90 via-n-surface-1/70 to-n-surface-1/95 cursor-not-allowed"
|
class="absolute inset-0 flex items-center justify-center backdrop-blur-[2px] rounded-2xl bg-gradient-to-br from-n-background/90 via-n-background/70 to-n-background/95 cursor-not-allowed"
|
||||||
>
|
>
|
||||||
<span class="text-n-slate-12 font-medium text-sm">
|
<span class="text-n-slate-12 font-medium text-sm">
|
||||||
{{ $t('CHANNEL_SELECTOR.COMING_SOON') }} 🚀
|
{{ $t('CHANNEL_SELECTOR.COMING_SOON') }} 🚀
|
||||||
|
|||||||
@@ -145,7 +145,6 @@ const {
|
|||||||
isConversationSelected,
|
isConversationSelected,
|
||||||
onAssignAgent,
|
onAssignAgent,
|
||||||
onAssignLabels,
|
onAssignLabels,
|
||||||
onRemoveLabels,
|
|
||||||
onAssignTeamsForBulk,
|
onAssignTeamsForBulk,
|
||||||
onUpdateConversations,
|
onUpdateConversations,
|
||||||
} = useBulkActions();
|
} = useBulkActions();
|
||||||
@@ -860,7 +859,6 @@ provide('deSelectConversation', deSelectConversation);
|
|||||||
provide('assignAgent', onAssignAgent);
|
provide('assignAgent', onAssignAgent);
|
||||||
provide('assignTeam', onAssignTeam);
|
provide('assignTeam', onAssignTeam);
|
||||||
provide('assignLabels', onAssignLabels);
|
provide('assignLabels', onAssignLabels);
|
||||||
provide('removeLabels', onRemoveLabels);
|
|
||||||
provide('updateConversationStatus', handleResolveConversation);
|
provide('updateConversationStatus', handleResolveConversation);
|
||||||
provide('toggleContextMenu', onContextMenuToggle);
|
provide('toggleContextMenu', onContextMenuToggle);
|
||||||
provide('markAsUnread', markAsUnread);
|
provide('markAsUnread', markAsUnread);
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ export default {
|
|||||||
'assignAgent',
|
'assignAgent',
|
||||||
'assignTeam',
|
'assignTeam',
|
||||||
'assignLabels',
|
'assignLabels',
|
||||||
'removeLabels',
|
|
||||||
'updateConversationStatus',
|
'updateConversationStatus',
|
||||||
'toggleContextMenu',
|
'toggleContextMenu',
|
||||||
'markAsUnread',
|
'markAsUnread',
|
||||||
@@ -64,7 +63,6 @@ export default {
|
|||||||
@assign-agent="assignAgent"
|
@assign-agent="assignAgent"
|
||||||
@assign-team="assignTeam"
|
@assign-team="assignTeam"
|
||||||
@assign-label="assignLabels"
|
@assign-label="assignLabels"
|
||||||
@remove-label="removeLabels"
|
|
||||||
@update-conversation-status="updateConversationStatus"
|
@update-conversation-status="updateConversationStatus"
|
||||||
@context-menu-toggle="toggleContextMenu"
|
@context-menu-toggle="toggleContextMenu"
|
||||||
@mark-as-unread="markAsUnread"
|
@mark-as-unread="markAsUnread"
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col items-start w-full gap-6">
|
||||||
|
<div class="flex flex-col w-full gap-4">
|
||||||
|
<h4 v-if="title" class="text-lg font-medium text-n-slate-12">
|
||||||
|
{{ title }}
|
||||||
|
</h4>
|
||||||
|
<div class="flex flex-row items-center justify-between">
|
||||||
|
<div class="flex-grow h-px bg-n-weak" />
|
||||||
|
</div>
|
||||||
|
<p v-if="description" class="mb-0 text-sm font-normal text-n-slate-12">
|
||||||
|
{{ description }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col w-full gap-6">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -3,9 +3,6 @@ import AutomationActionTeamMessageInput from './AutomationActionTeamMessageInput
|
|||||||
import AutomationActionFileInput from './AutomationFileInput.vue';
|
import AutomationActionFileInput from './AutomationFileInput.vue';
|
||||||
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
|
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
|
||||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||||
import SingleSelect from 'dashboard/components-next/filter/inputs/SingleSelect.vue';
|
|
||||||
import MultiSelect from 'dashboard/components-next/filter/inputs/MultiSelect.vue';
|
|
||||||
import NextInput from 'dashboard/components-next/input/Input.vue';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@@ -13,9 +10,6 @@ export default {
|
|||||||
AutomationActionFileInput,
|
AutomationActionFileInput,
|
||||||
WootMessageEditor,
|
WootMessageEditor,
|
||||||
NextButton,
|
NextButton,
|
||||||
SingleSelect,
|
|
||||||
MultiSelect,
|
|
||||||
NextInput,
|
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
modelValue: {
|
modelValue: {
|
||||||
@@ -46,10 +40,6 @@ export default {
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
dropdownMaxHeight: {
|
|
||||||
type: String,
|
|
||||||
default: 'max-h-80',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
emits: ['update:modelValue', 'input', 'removeAction', 'resetAction'],
|
emits: ['update:modelValue', 'input', 'removeAction', 'resetAction'],
|
||||||
computed: {
|
computed: {
|
||||||
@@ -79,21 +69,11 @@ export default {
|
|||||||
return this.actionTypes.find(action => action.key === this.action_name)
|
return this.actionTypes.find(action => action.key === this.action_name)
|
||||||
.inputType;
|
.inputType;
|
||||||
},
|
},
|
||||||
actionNameAsSelectModel: {
|
actionInputStyles() {
|
||||||
get() {
|
return {
|
||||||
if (!this.action_name) return null;
|
'has-error': this.errorMessage,
|
||||||
const found = this.actionTypes.find(a => a.key === this.action_name);
|
'is-a-macro': this.isMacro,
|
||||||
return found ? { id: found.key, name: found.label } : null;
|
};
|
||||||
},
|
|
||||||
set(value) {
|
|
||||||
this.action_name = value?.id || value;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
actionTypesAsOptions() {
|
|
||||||
return this.actionTypes.map(a => ({ id: a.key, name: a.label }));
|
|
||||||
},
|
|
||||||
isVerticalLayout() {
|
|
||||||
return ['team_message', 'textarea'].includes(this.inputType);
|
|
||||||
},
|
},
|
||||||
castMessageVmodel: {
|
castMessageVmodel: {
|
||||||
get() {
|
get() {
|
||||||
@@ -114,89 +94,203 @@ export default {
|
|||||||
resetAction() {
|
resetAction() {
|
||||||
this.$emit('resetAction');
|
this.$emit('resetAction');
|
||||||
},
|
},
|
||||||
onActionNameChange(value) {
|
|
||||||
this.actionNameAsSelectModel = value;
|
|
||||||
this.resetAction();
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<li class="list-none py-2 first:pt-0 last:pb-0">
|
<div class="filter" :class="actionInputStyles">
|
||||||
<div
|
<div class="filter-inputs">
|
||||||
class="flex flex-col gap-2"
|
<select
|
||||||
:class="{ 'animate-wiggle': errorMessage }"
|
v-model="action_name"
|
||||||
>
|
class="action__question"
|
||||||
<div class="flex items-center gap-2">
|
:class="{ 'full-width': !showActionInput }"
|
||||||
<SingleSelect
|
@change="resetAction()"
|
||||||
:model-value="actionNameAsSelectModel"
|
>
|
||||||
:options="actionTypesAsOptions"
|
<option
|
||||||
:dropdown-max-height="dropdownMaxHeight"
|
v-for="attribute in actionTypes"
|
||||||
disable-deselect
|
:key="attribute.key"
|
||||||
class="flex-shrink-0"
|
:value="attribute.key"
|
||||||
@update:model-value="onActionNameChange"
|
>
|
||||||
/>
|
{{ attribute.label }}
|
||||||
<template v-if="showActionInput && !isVerticalLayout">
|
</option>
|
||||||
<SingleSelect
|
</select>
|
||||||
|
<div v-if="showActionInput" class="filter__answer--wrap">
|
||||||
|
<div v-if="inputType" class="w-full">
|
||||||
|
<div
|
||||||
v-if="inputType === 'search_select'"
|
v-if="inputType === 'search_select'"
|
||||||
v-model="action_params"
|
class="multiselect-wrap--small"
|
||||||
:options="dropdownValues"
|
>
|
||||||
:dropdown-max-height="dropdownMaxHeight"
|
<multiselect
|
||||||
/>
|
v-model="action_params"
|
||||||
<MultiSelect
|
track-by="id"
|
||||||
|
label="name"
|
||||||
|
:placeholder="$t('FORMS.MULTISELECT.SELECT')"
|
||||||
|
selected-label
|
||||||
|
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
|
||||||
|
deselect-label=""
|
||||||
|
:max-height="160"
|
||||||
|
:options="dropdownValues"
|
||||||
|
:allow-empty="false"
|
||||||
|
:option-height="104"
|
||||||
|
>
|
||||||
|
<template #noOptions>
|
||||||
|
{{ $t('FORMS.MULTISELECT.NO_OPTIONS') }}
|
||||||
|
</template>
|
||||||
|
</multiselect>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
v-else-if="inputType === 'multi_select'"
|
v-else-if="inputType === 'multi_select'"
|
||||||
v-model="action_params"
|
class="multiselect-wrap--small"
|
||||||
:options="dropdownValues"
|
>
|
||||||
:dropdown-max-height="dropdownMaxHeight"
|
<multiselect
|
||||||
/>
|
v-model="action_params"
|
||||||
<NextInput
|
track-by="id"
|
||||||
|
label="name"
|
||||||
|
:placeholder="$t('FORMS.MULTISELECT.SELECT')"
|
||||||
|
multiple
|
||||||
|
selected-label
|
||||||
|
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
|
||||||
|
deselect-label=""
|
||||||
|
:max-height="160"
|
||||||
|
:options="dropdownValues"
|
||||||
|
:allow-empty="false"
|
||||||
|
:option-height="104"
|
||||||
|
>
|
||||||
|
<template #noOptions>
|
||||||
|
{{ $t('FORMS.MULTISELECT.NO_OPTIONS') }}
|
||||||
|
</template>
|
||||||
|
</multiselect>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
v-else-if="inputType === 'email'"
|
v-else-if="inputType === 'email'"
|
||||||
v-model="action_params"
|
v-model="action_params"
|
||||||
type="email"
|
type="email"
|
||||||
size="sm"
|
class="answer--text-input"
|
||||||
:placeholder="$t('AUTOMATION.ACTION.EMAIL_INPUT_PLACEHOLDER')"
|
:placeholder="$t('AUTOMATION.ACTION.EMAIL_INPUT_PLACEHOLDER')"
|
||||||
/>
|
/>
|
||||||
<NextInput
|
<input
|
||||||
v-else-if="inputType === 'url'"
|
v-else-if="inputType === 'url'"
|
||||||
v-model="action_params"
|
v-model="action_params"
|
||||||
type="url"
|
type="url"
|
||||||
size="sm"
|
class="answer--text-input"
|
||||||
:placeholder="$t('AUTOMATION.ACTION.URL_INPUT_PLACEHOLDER')"
|
:placeholder="$t('AUTOMATION.ACTION.URL_INPUT_PLACEHOLDER')"
|
||||||
/>
|
/>
|
||||||
<AutomationActionFileInput
|
<AutomationActionFileInput
|
||||||
v-else-if="inputType === 'attachment'"
|
v-if="inputType === 'attachment'"
|
||||||
v-model="action_params"
|
v-model="action_params"
|
||||||
:initial-file-name="initialFileName"
|
:initial-file-name="initialFileName"
|
||||||
/>
|
/>
|
||||||
</template>
|
</div>
|
||||||
<NextButton
|
|
||||||
v-if="!isMacro"
|
|
||||||
sm
|
|
||||||
solid
|
|
||||||
slate
|
|
||||||
icon="i-lucide-trash"
|
|
||||||
class="flex-shrink-0"
|
|
||||||
@click="removeAction"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<AutomationActionTeamMessageInput
|
<NextButton
|
||||||
v-if="inputType === 'team_message'"
|
v-if="!isMacro"
|
||||||
v-model="action_params"
|
icon="i-lucide-x"
|
||||||
:teams="dropdownValues"
|
slate
|
||||||
:dropdown-max-height="dropdownMaxHeight"
|
ghost
|
||||||
/>
|
class="flex-shrink-0"
|
||||||
<WootMessageEditor
|
@click="removeAction"
|
||||||
v-if="inputType === 'textarea'"
|
|
||||||
v-model="castMessageVmodel"
|
|
||||||
rows="4"
|
|
||||||
enable-variables
|
|
||||||
:placeholder="$t('AUTOMATION.ACTION.TEAM_MESSAGE_INPUT_PLACEHOLDER')"
|
|
||||||
class="[&_.ProseMirror-menubar]:hidden px-3 py-1 bg-n-alpha-1 rounded-lg outline outline-1 outline-n-weak dark:outline-n-strong"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span v-if="errorMessage" class="text-sm text-n-ruby-11">
|
<AutomationActionTeamMessageInput
|
||||||
|
v-if="inputType === 'team_message'"
|
||||||
|
v-model="action_params"
|
||||||
|
:teams="dropdownValues"
|
||||||
|
/>
|
||||||
|
<WootMessageEditor
|
||||||
|
v-if="inputType === 'textarea'"
|
||||||
|
v-model="castMessageVmodel"
|
||||||
|
rows="4"
|
||||||
|
enable-variables
|
||||||
|
:placeholder="$t('AUTOMATION.ACTION.TEAM_MESSAGE_INPUT_PLACEHOLDER')"
|
||||||
|
class="action-message"
|
||||||
|
/>
|
||||||
|
<p v-if="errorMessage" class="filter-error">
|
||||||
{{ errorMessage }}
|
{{ errorMessage }}
|
||||||
</span>
|
</p>
|
||||||
</li>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.filter {
|
||||||
|
@apply bg-n-background p-2 border border-solid border-n-strong dark:border-n-strong rounded-lg mb-2;
|
||||||
|
|
||||||
|
&.is-a-macro {
|
||||||
|
@apply mb-0 bg-n-background dark:bg-n-solid-1 p-0 border-0 rounded-none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-margin-bottom {
|
||||||
|
@apply mb-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter.has-error {
|
||||||
|
@apply bg-n-ruby-8/20 border-n-ruby-5 dark:border-n-ruby-5;
|
||||||
|
|
||||||
|
&.is-a-macro {
|
||||||
|
@apply bg-transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-inputs {
|
||||||
|
@apply flex gap-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-error {
|
||||||
|
@apply text-n-ruby-9 dark:text-n-ruby-9 block my-1 mx-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action__question,
|
||||||
|
.filter__operator {
|
||||||
|
@apply mb-0 mr-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action__question {
|
||||||
|
@apply max-w-[50%];
|
||||||
|
}
|
||||||
|
|
||||||
|
.action__question.full-width {
|
||||||
|
@apply max-w-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter__answer--wrap {
|
||||||
|
@apply max-w-[50%] flex-grow mr-1 flex w-full items-center justify-start;
|
||||||
|
|
||||||
|
input {
|
||||||
|
@apply mb-0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.filter__answer {
|
||||||
|
&.answer--text-input {
|
||||||
|
@apply mb-0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter__join-operator-wrap {
|
||||||
|
@apply relative z-20 m-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter__join-operator {
|
||||||
|
@apply flex items-center justify-center relative my-2.5 mx-0;
|
||||||
|
|
||||||
|
.operator__line {
|
||||||
|
@apply absolute w-full border-b border-solid border-n-weak;
|
||||||
|
}
|
||||||
|
|
||||||
|
.operator__select {
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
@apply relative w-auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect {
|
||||||
|
@apply mb-0;
|
||||||
|
}
|
||||||
|
.action-message {
|
||||||
|
@apply mt-2 mx-0 mb-0;
|
||||||
|
}
|
||||||
|
// Prosemirror does not have a native way of hiding the menu bar, hence
|
||||||
|
::v-deep .ProseMirror-menubar {
|
||||||
|
@apply hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,14 +1,8 @@
|
|||||||
<script>
|
<script>
|
||||||
import MultiSelect from 'dashboard/components-next/filter/inputs/MultiSelect.vue';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
|
||||||
MultiSelect,
|
|
||||||
},
|
|
||||||
props: {
|
props: {
|
||||||
teams: { type: Array, required: true },
|
teams: { type: Array, required: true },
|
||||||
modelValue: { type: Object, required: true },
|
modelValue: { type: Object, required: true },
|
||||||
dropdownMaxHeight: { type: String, default: 'max-h-80' },
|
|
||||||
},
|
},
|
||||||
emits: ['update:modelValue'],
|
emits: ['update:modelValue'],
|
||||||
data() {
|
data() {
|
||||||
@@ -18,9 +12,9 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
const { team_ids: teamIds, message } = this.modelValue || {};
|
const { team_ids: teamIds } = this.modelValue;
|
||||||
this.selectedTeams = teamIds || [];
|
this.selectedTeams = teamIds;
|
||||||
this.message = message || '';
|
this.message = this.modelValue.message;
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
updateValue() {
|
updateValue() {
|
||||||
@@ -34,19 +28,37 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col gap-2">
|
<div>
|
||||||
<MultiSelect
|
<div class="multiselect-wrap--small flex flex-col gap-1 mt-1">
|
||||||
v-model="selectedTeams"
|
<multiselect
|
||||||
:options="teams"
|
v-model="selectedTeams"
|
||||||
:dropdown-max-height="dropdownMaxHeight"
|
track-by="id"
|
||||||
@update:model-value="updateValue"
|
label="name"
|
||||||
/>
|
:placeholder="$t('AUTOMATION.ACTION.TEAM_DROPDOWN_PLACEHOLDER')"
|
||||||
<textarea
|
multiple
|
||||||
v-model="message"
|
selected-label
|
||||||
class="mb-0 !text-sm"
|
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
|
||||||
rows="4"
|
deselect-label=""
|
||||||
:placeholder="$t('AUTOMATION.ACTION.TEAM_MESSAGE_INPUT_PLACEHOLDER')"
|
:max-height="160"
|
||||||
@input="updateValue"
|
:options="teams"
|
||||||
/>
|
:allow-empty="false"
|
||||||
|
@update:model-value="updateValue"
|
||||||
|
/>
|
||||||
|
<textarea
|
||||||
|
v-model="message"
|
||||||
|
rows="4"
|
||||||
|
:placeholder="$t('AUTOMATION.ACTION.TEAM_MESSAGE_INPUT_PLACEHOLDER')"
|
||||||
|
@input="updateValue"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.multiselect {
|
||||||
|
margin: 0.25rem 0;
|
||||||
|
}
|
||||||
|
textarea {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ input[type='file'] {
|
|||||||
@apply hidden;
|
@apply hidden;
|
||||||
}
|
}
|
||||||
.input-wrapper {
|
.input-wrapper {
|
||||||
@apply flex h-8 bg-n-background py-1 px-2 items-center text-xs cursor-pointer rounded-lg border border-dashed border-n-strong;
|
@apply flex h-9 bg-n-background py-1 px-2 items-center text-xs cursor-pointer rounded-sm border border-dashed border-n-strong;
|
||||||
}
|
}
|
||||||
.success-icon {
|
.success-icon {
|
||||||
@apply text-n-teal-9 mr-2;
|
@apply text-n-teal-9 mr-2;
|
||||||
|
|||||||
@@ -0,0 +1,305 @@
|
|||||||
|
<script>
|
||||||
|
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'FilterInput',
|
||||||
|
components: {
|
||||||
|
NextButton,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
modelValue: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {},
|
||||||
|
},
|
||||||
|
filterAttributes: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
inputType: {
|
||||||
|
type: String,
|
||||||
|
default: 'plain_text',
|
||||||
|
},
|
||||||
|
operators: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
dropdownValues: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
showQueryOperator: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
showUserInput: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
groupedFilters: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
filterGroups: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
customAttributeType: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
errorMessage: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: ['update:modelValue', 'removeFilter', 'resetFilter'],
|
||||||
|
computed: {
|
||||||
|
attributeKey: {
|
||||||
|
get() {
|
||||||
|
if (!this.modelValue) return null;
|
||||||
|
return this.modelValue.attribute_key;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
const payload = this.modelValue || {};
|
||||||
|
this.$emit('update:modelValue', { ...payload, attribute_key: value });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
filterOperator: {
|
||||||
|
get() {
|
||||||
|
if (!this.modelValue) return null;
|
||||||
|
return this.modelValue.filter_operator;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
const payload = this.modelValue || {};
|
||||||
|
this.$emit('update:modelValue', { ...payload, filter_operator: value });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
values: {
|
||||||
|
get() {
|
||||||
|
if (!this.modelValue) return null;
|
||||||
|
return this.modelValue.values;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
const payload = this.modelValue || {};
|
||||||
|
this.$emit('update:modelValue', { ...payload, values: value });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
query_operator: {
|
||||||
|
get() {
|
||||||
|
if (!this.modelValue) return null;
|
||||||
|
return this.modelValue.query_operator;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
const payload = this.modelValue || {};
|
||||||
|
this.$emit('update:modelValue', { ...payload, query_operator: value });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
custom_attribute_type: {
|
||||||
|
get() {
|
||||||
|
if (!this.customAttributeType) return '';
|
||||||
|
return this.customAttributeType;
|
||||||
|
},
|
||||||
|
set() {
|
||||||
|
const payload = this.modelValue || {};
|
||||||
|
this.$emit('update:modelValue', {
|
||||||
|
...payload,
|
||||||
|
custom_attribute_type: this.customAttributeType,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
customAttributeType: {
|
||||||
|
handler(value) {
|
||||||
|
if (
|
||||||
|
value === 'conversation_attribute' ||
|
||||||
|
value === 'contact_attribute'
|
||||||
|
) {
|
||||||
|
// eslint-disable-next-line vue/no-mutating-props
|
||||||
|
this.modelValue.custom_attribute_type = this.customAttributeType;
|
||||||
|
// eslint-disable-next-line vue/no-mutating-props
|
||||||
|
} else this.modelValue.custom_attribute_type = '';
|
||||||
|
},
|
||||||
|
immediate: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
removeFilter() {
|
||||||
|
this.$emit('removeFilter');
|
||||||
|
},
|
||||||
|
resetFilter() {
|
||||||
|
this.$emit('resetFilter');
|
||||||
|
},
|
||||||
|
getInputErrorClass(errorMessage) {
|
||||||
|
return errorMessage
|
||||||
|
? 'bg-n-ruby-8/20 border-n-ruby-5 dark:border-n-ruby-5'
|
||||||
|
: 'bg-n-background border-n-weak dark:border-n-weak';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- eslint-disable vue/no-mutating-props -->
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="p-2 border border-solid rounded-lg"
|
||||||
|
:class="getInputErrorClass(errorMessage)"
|
||||||
|
>
|
||||||
|
<div class="flex gap-1">
|
||||||
|
<select
|
||||||
|
v-if="groupedFilters"
|
||||||
|
v-model="attributeKey"
|
||||||
|
class="max-w-[30%] mb-0 mr-1"
|
||||||
|
@change="resetFilter()"
|
||||||
|
>
|
||||||
|
<optgroup
|
||||||
|
v-for="(group, i) in filterGroups"
|
||||||
|
:key="i"
|
||||||
|
:label="group.name"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-for="attribute in group.attributes"
|
||||||
|
:key="attribute.key"
|
||||||
|
:value="attribute.key"
|
||||||
|
:selected="true"
|
||||||
|
>
|
||||||
|
{{ attribute.name }}
|
||||||
|
</option>
|
||||||
|
</optgroup>
|
||||||
|
</select>
|
||||||
|
<select
|
||||||
|
v-else
|
||||||
|
v-model="attributeKey"
|
||||||
|
class="max-w-[30%] mb-0 mr-1"
|
||||||
|
@change="resetFilter()"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-for="attribute in filterAttributes"
|
||||||
|
:key="attribute.key"
|
||||||
|
:value="attribute.key"
|
||||||
|
:disabled="attribute.disabled"
|
||||||
|
>
|
||||||
|
{{ attribute.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select v-model="filterOperator" class="max-w-[20%] mb-0 mr-1">
|
||||||
|
<option
|
||||||
|
v-for="(operator, o) in operators"
|
||||||
|
:key="o"
|
||||||
|
:value="operator.value"
|
||||||
|
>
|
||||||
|
{{ $t(`FILTER.OPERATOR_LABELS.${operator.value}`) }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<div v-if="showUserInput" class="flex-grow mr-1 filter__answer--wrap">
|
||||||
|
<div
|
||||||
|
v-if="inputType === 'multi_select'"
|
||||||
|
class="multiselect-wrap--small"
|
||||||
|
>
|
||||||
|
<multiselect
|
||||||
|
v-model="values"
|
||||||
|
track-by="id"
|
||||||
|
label="name"
|
||||||
|
:placeholder="$t('FORMS.MULTISELECT.SELECT')"
|
||||||
|
multiple
|
||||||
|
selected-label
|
||||||
|
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
|
||||||
|
deselect-label=""
|
||||||
|
:max-height="160"
|
||||||
|
:options="dropdownValues"
|
||||||
|
:allow-empty="false"
|
||||||
|
>
|
||||||
|
<template #noOptions>
|
||||||
|
{{ $t('FORMS.MULTISELECT.NO_OPTIONS') }}
|
||||||
|
</template>
|
||||||
|
</multiselect>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="inputType === 'search_select'"
|
||||||
|
class="multiselect-wrap--small"
|
||||||
|
>
|
||||||
|
<multiselect
|
||||||
|
v-model="values"
|
||||||
|
track-by="id"
|
||||||
|
label="name"
|
||||||
|
:placeholder="$t('FORMS.MULTISELECT.SELECT')"
|
||||||
|
selected-label
|
||||||
|
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
|
||||||
|
deselect-label=""
|
||||||
|
:max-height="160"
|
||||||
|
:options="dropdownValues"
|
||||||
|
:allow-empty="false"
|
||||||
|
:option-height="104"
|
||||||
|
>
|
||||||
|
<template #noOptions>
|
||||||
|
{{ $t('FORMS.MULTISELECT.NO_OPTIONS') }}
|
||||||
|
</template>
|
||||||
|
</multiselect>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="inputType === 'date'" class="multiselect-wrap--small">
|
||||||
|
<input
|
||||||
|
v-model="values"
|
||||||
|
type="date"
|
||||||
|
:editable="false"
|
||||||
|
class="!mb-0 datepicker"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
v-else
|
||||||
|
v-model="values"
|
||||||
|
type="text"
|
||||||
|
class="!mb-0"
|
||||||
|
:placeholder="$t('FILTER.INPUT_PLACEHOLDER')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<NextButton
|
||||||
|
icon="i-lucide-x"
|
||||||
|
slate
|
||||||
|
ghost
|
||||||
|
class="flex-shrink-0"
|
||||||
|
@click="removeFilter"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p v-if="errorMessage" class="filter-error">
|
||||||
|
{{ errorMessage }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="showQueryOperator"
|
||||||
|
class="flex items-center justify-center relative my-2.5 mx-0"
|
||||||
|
>
|
||||||
|
<hr class="absolute w-full border-b border-solid border-n-weak" />
|
||||||
|
<select
|
||||||
|
v-model="query_operator"
|
||||||
|
class="relative w-auto mb-0 bg-n-background text-n-slate-12 border-n-weak"
|
||||||
|
>
|
||||||
|
<option value="and">
|
||||||
|
{{ $t('FILTER.QUERY_DROPDOWN_LABELS.AND') }}
|
||||||
|
</option>
|
||||||
|
<option value="or">
|
||||||
|
{{ $t('FILTER.QUERY_DROPDOWN_LABELS.OR') }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.filter__answer--wrap {
|
||||||
|
input {
|
||||||
|
@apply bg-n-background mb-0 text-n-slate-12 border-n-weak;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-error {
|
||||||
|
@apply text-n-ruby-9 dark:text-n-ruby-9 block my-1 mx-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect {
|
||||||
|
@apply mb-0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -10,9 +10,12 @@ defineProps({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :title="inbox.name" class="flex items-center gap-0.5 min-w-0">
|
<div class="flex items-center text-n-slate-11 text-xs min-w-0">
|
||||||
<ChannelIcon :inbox="inbox" class="size-4 flex-shrink-0 text-n-slate-11" />
|
<ChannelIcon
|
||||||
<span class="truncate text-label-small text-n-slate-11">
|
:inbox="inbox"
|
||||||
|
class="size-3 ltr:mr-1 rtl:ml-1 flex-shrink-0"
|
||||||
|
/>
|
||||||
|
<span class="truncate">
|
||||||
{{ inbox.name }}
|
{{ inbox.name }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ defineProps({
|
|||||||
<h6
|
<h6
|
||||||
class="flex items-center gap-3 text-base text-center w-100 text-n-slate-11"
|
class="flex items-center gap-3 text-base text-center w-100 text-n-slate-11"
|
||||||
>
|
>
|
||||||
<span class="text-body-main !text-base text-n-slate-12">
|
<span class="text-base font-medium text-n-slate-12">
|
||||||
{{ message }}
|
{{ message }}
|
||||||
</span>
|
</span>
|
||||||
<Spinner class="text-n-brand" />
|
<Spinner class="text-n-brand" />
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export default {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="border-b border-solid border-n-weak/60">
|
<div class="border-b border-solid border-n-weak/60">
|
||||||
<div class="max-w-7xl w-full mx-auto pt-4 pb-0 px-6">
|
<div class="max-w-6xl w-full mx-auto pt-4 pb-0 px-8">
|
||||||
<h2 class="text-2xl text-n-slate-12 mb-1 font-medium">
|
<h2 class="text-2xl text-n-slate-12 mb-1 font-medium">
|
||||||
{{ headerTitle }}
|
{{ headerTitle }}
|
||||||
</h2>
|
</h2>
|
||||||
|
|||||||
@@ -28,10 +28,7 @@ import { useAlert } from 'dashboard/composables';
|
|||||||
import { vOnClickOutside } from '@vueuse/components';
|
import { vOnClickOutside } from '@vueuse/components';
|
||||||
|
|
||||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||||
import {
|
import { CONVERSATION_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||||
CONVERSATION_EVENTS,
|
|
||||||
CAPTAIN_EVENTS,
|
|
||||||
} from 'dashboard/helper/AnalyticsHelper/events';
|
|
||||||
import { MESSAGE_EDITOR_IMAGE_RESIZES } from 'dashboard/constants/editor';
|
import { MESSAGE_EDITOR_IMAGE_RESIZES } from 'dashboard/constants/editor';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -89,7 +86,6 @@ const props = defineProps({
|
|||||||
// are triggered except when this flag is true
|
// are triggered except when this flag is true
|
||||||
allowSignature: { type: Boolean, default: false },
|
allowSignature: { type: Boolean, default: false },
|
||||||
channelType: { type: String, default: '' },
|
channelType: { type: String, default: '' },
|
||||||
conversationId: { type: Number, default: null },
|
|
||||||
medium: { type: String, default: '' },
|
medium: { type: String, default: '' },
|
||||||
showImageResizeToolbar: { type: Boolean, default: false }, // A kill switch to show or hide the image toolbar
|
showImageResizeToolbar: { type: Boolean, default: false }, // A kill switch to show or hide the image toolbar
|
||||||
focusOnMount: { type: Boolean, default: true },
|
focusOnMount: { type: Boolean, default: true },
|
||||||
@@ -400,14 +396,7 @@ function openFileBrowser() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleCopilotClick() {
|
function handleCopilotClick() {
|
||||||
const isOpening = !showSelectionMenu.value;
|
showSelectionMenu.value = !showSelectionMenu.value;
|
||||||
if (isOpening) {
|
|
||||||
useTrack(CAPTAIN_EVENTS.EDITOR_AI_MENU_OPENED, {
|
|
||||||
conversationId: props.conversationId,
|
|
||||||
entryPoint: 'inline',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
showSelectionMenu.value = isOpening;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleClickOutside(event) {
|
function handleClickOutside(event) {
|
||||||
@@ -830,13 +819,7 @@ useEmitter(BUS_EVENTS.INSERT_INTO_RICH_EDITOR, insertContentIntoEditor);
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div ref="editorRoot" class="relative w-full">
|
||||||
ref="editorRoot"
|
|
||||||
class="relative w-full"
|
|
||||||
:class="{
|
|
||||||
'opacity-50 cursor-not-allowed pointer-events-none': disabled,
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<TagAgents
|
<TagAgents
|
||||||
v-if="showUserMentions && isPrivate"
|
v-if="showUserMentions && isPrivate"
|
||||||
:search-key="mentionSearchKey"
|
:search-key="mentionSearchKey"
|
||||||
|
|||||||
@@ -122,10 +122,6 @@ export default {
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
isEditorDisabled: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
emits: [
|
emits: [
|
||||||
'replaceText',
|
'replaceText',
|
||||||
@@ -134,7 +130,7 @@ export default {
|
|||||||
'selectContentTemplate',
|
'selectContentTemplate',
|
||||||
'toggleQuotedReply',
|
'toggleQuotedReply',
|
||||||
],
|
],
|
||||||
setup(props) {
|
setup() {
|
||||||
const { setSignatureFlagForInbox, fetchSignatureFlagFromUISettings } =
|
const { setSignatureFlagForInbox, fetchSignatureFlagFromUISettings } =
|
||||||
useUISettings();
|
useUISettings();
|
||||||
|
|
||||||
@@ -143,9 +139,6 @@ export default {
|
|||||||
const keyboardEvents = {
|
const keyboardEvents = {
|
||||||
'$mod+Alt+KeyA': {
|
'$mod+Alt+KeyA': {
|
||||||
action: () => {
|
action: () => {
|
||||||
// Skip if editor is disabled (e.g., WhatsApp 24-hour window expired)
|
|
||||||
if (props.isEditorDisabled) return;
|
|
||||||
|
|
||||||
// TODO: This is really hacky, we need to replace the file picker component with
|
// TODO: This is really hacky, we need to replace the file picker component with
|
||||||
// a custom one, where the logic and the component markup is isolated.
|
// a custom one, where the logic and the component markup is isolated.
|
||||||
// Once we have the custom component, we can remove the hacky logic below.
|
// Once we have the custom component, we can remove the hacky logic below.
|
||||||
@@ -153,7 +146,7 @@ export default {
|
|||||||
const uploadTriggerButton = document.querySelector(
|
const uploadTriggerButton = document.querySelector(
|
||||||
'#conversationAttachment'
|
'#conversationAttachment'
|
||||||
);
|
);
|
||||||
if (uploadTriggerButton) uploadTriggerButton.click();
|
uploadTriggerButton.click();
|
||||||
},
|
},
|
||||||
allowOnFocusedInput: true,
|
allowOnFocusedInput: true,
|
||||||
},
|
},
|
||||||
@@ -184,11 +177,9 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
showAttachButton() {
|
showAttachButton() {
|
||||||
if (this.isEditorDisabled) return false;
|
|
||||||
return this.showFileUpload || this.isNote;
|
return this.showFileUpload || this.isNote;
|
||||||
},
|
},
|
||||||
showAudioRecorderButton() {
|
showAudioRecorderButton() {
|
||||||
if (this.isEditorDisabled) return false;
|
|
||||||
if (this.isALineChannel) {
|
if (this.isALineChannel) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -206,7 +197,6 @@ export default {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
showAudioPlayStopButton() {
|
showAudioPlayStopButton() {
|
||||||
if (this.isEditorDisabled) return false;
|
|
||||||
return this.showAudioRecorder && this.isRecordingAudio;
|
return this.showAudioRecorder && this.isRecordingAudio;
|
||||||
},
|
},
|
||||||
isInstagramDM() {
|
isInstagramDM() {
|
||||||
@@ -246,7 +236,6 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
showMessageSignatureButton() {
|
showMessageSignatureButton() {
|
||||||
if (this.isEditorDisabled) return false;
|
|
||||||
return !this.isOnPrivateNote;
|
return !this.isOnPrivateNote;
|
||||||
},
|
},
|
||||||
sendWithSignature() {
|
sendWithSignature() {
|
||||||
@@ -291,7 +280,6 @@ export default {
|
|||||||
<div class="flex justify-between p-3" :class="wrapClass">
|
<div class="flex justify-between p-3" :class="wrapClass">
|
||||||
<div class="left-wrap">
|
<div class="left-wrap">
|
||||||
<NextButton
|
<NextButton
|
||||||
v-if="!isEditorDisabled"
|
|
||||||
v-tooltip.top-end="$t('CONVERSATION.REPLYBOX.TIP_EMOJI_ICON')"
|
v-tooltip.top-end="$t('CONVERSATION.REPLYBOX.TIP_EMOJI_ICON')"
|
||||||
icon="i-ph-smiley-sticker"
|
icon="i-ph-smiley-sticker"
|
||||||
slate
|
slate
|
||||||
@@ -300,7 +288,6 @@ export default {
|
|||||||
@click="toggleEmojiPicker"
|
@click="toggleEmojiPicker"
|
||||||
/>
|
/>
|
||||||
<FileUpload
|
<FileUpload
|
||||||
v-if="showAttachButton"
|
|
||||||
ref="uploadRef"
|
ref="uploadRef"
|
||||||
v-tooltip.top-end="$t('CONVERSATION.REPLYBOX.TIP_ATTACH_ICON')"
|
v-tooltip.top-end="$t('CONVERSATION.REPLYBOX.TIP_ATTACH_ICON')"
|
||||||
input-id="conversationAttachment"
|
input-id="conversationAttachment"
|
||||||
|
|||||||
@@ -2,10 +2,8 @@
|
|||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
|
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
|
||||||
import { useCaptain } from 'dashboard/composables/useCaptain';
|
import { useCaptain } from 'dashboard/composables/useCaptain';
|
||||||
import { useTrack } from 'dashboard/composables';
|
|
||||||
import { vOnClickOutside } from '@vueuse/components';
|
import { vOnClickOutside } from '@vueuse/components';
|
||||||
import { REPLY_EDITOR_MODES, CHAR_LENGTH_WARNING } from './constants';
|
import { REPLY_EDITOR_MODES, CHAR_LENGTH_WARNING } from './constants';
|
||||||
import { CAPTAIN_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
|
||||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||||
import EditorModeToggle from './EditorModeToggle.vue';
|
import EditorModeToggle from './EditorModeToggle.vue';
|
||||||
import CopilotMenuBar from './CopilotMenuBar.vue';
|
import CopilotMenuBar from './CopilotMenuBar.vue';
|
||||||
@@ -33,14 +31,6 @@ export default {
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
isEditorDisabled: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
conversationId: {
|
|
||||||
type: Number,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
isMessageLengthReachingThreshold: {
|
isMessageLengthReachingThreshold: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: () => false,
|
default: () => false,
|
||||||
@@ -79,14 +69,7 @@ export default {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const toggleCopilotMenu = () => {
|
const toggleCopilotMenu = () => {
|
||||||
const isOpening = !showCopilotMenu.value;
|
showCopilotMenu.value = !showCopilotMenu.value;
|
||||||
if (isOpening) {
|
|
||||||
useTrack(CAPTAIN_EVENTS.EDITOR_AI_MENU_OPENED, {
|
|
||||||
conversationId: props.conversationId,
|
|
||||||
entryPoint: 'top_panel',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
showCopilotMenu.value = isOpening;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClickOutside = () => {
|
const handleClickOutside = () => {
|
||||||
@@ -161,7 +144,7 @@ export default {
|
|||||||
<div class="relative">
|
<div class="relative">
|
||||||
<NextButton
|
<NextButton
|
||||||
ghost
|
ghost
|
||||||
:disabled="disabled || isEditorDisabled"
|
:disabled="disabled"
|
||||||
:class="{
|
:class="{
|
||||||
'text-n-violet-9 hover:enabled:!bg-n-violet-3': !showCopilotMenu,
|
'text-n-violet-9 hover:enabled:!bg-n-violet-3': !showCopilotMenu,
|
||||||
'text-n-violet-9 bg-n-violet-3': showCopilotMenu,
|
'text-n-violet-9 bg-n-violet-3': showCopilotMenu,
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ const emit = defineEmits([
|
|||||||
'contextMenuToggle',
|
'contextMenuToggle',
|
||||||
'assignAgent',
|
'assignAgent',
|
||||||
'assignLabel',
|
'assignLabel',
|
||||||
'removeLabel',
|
|
||||||
'assignTeam',
|
'assignTeam',
|
||||||
'markAsUnread',
|
'markAsUnread',
|
||||||
'markAsRead',
|
'markAsRead',
|
||||||
@@ -204,10 +203,7 @@ const onAssignAgent = agent => {
|
|||||||
|
|
||||||
const onAssignLabel = label => {
|
const onAssignLabel = label => {
|
||||||
emit('assignLabel', [label.title], [props.chat.id]);
|
emit('assignLabel', [label.title], [props.chat.id]);
|
||||||
};
|
closeContextMenu();
|
||||||
|
|
||||||
const onRemoveLabel = label => {
|
|
||||||
emit('removeLabel', [label.title], [props.chat.id]);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onAssignTeam = team => {
|
const onAssignTeam = team => {
|
||||||
@@ -383,13 +379,11 @@ const deleteConversation = () => {
|
|||||||
:priority="chat.priority"
|
:priority="chat.priority"
|
||||||
:chat-id="chat.id"
|
:chat-id="chat.id"
|
||||||
:has-unread-messages="hasUnread"
|
:has-unread-messages="hasUnread"
|
||||||
:conversation-labels="chat.labels"
|
|
||||||
:conversation-url="conversationPath"
|
:conversation-url="conversationPath"
|
||||||
:allowed-options="allowedContextMenuOptions"
|
:allowed-options="allowedContextMenuOptions"
|
||||||
@update-conversation="onUpdateConversation"
|
@update-conversation="onUpdateConversation"
|
||||||
@assign-agent="onAssignAgent"
|
@assign-agent="onAssignAgent"
|
||||||
@assign-label="onAssignLabel"
|
@assign-label="onAssignLabel"
|
||||||
@remove-label="onRemoveLabel"
|
|
||||||
@assign-team="onAssignTeam"
|
@assign-team="onAssignTeam"
|
||||||
@mark-as-unread="markAsUnread"
|
@mark-as-unread="markAsUnread"
|
||||||
@mark-as-read="markAsRead"
|
@mark-as-read="markAsRead"
|
||||||
|
|||||||
@@ -41,10 +41,7 @@ import {
|
|||||||
truncatePreviewText,
|
truncatePreviewText,
|
||||||
appendQuotedTextToMessage,
|
appendQuotedTextToMessage,
|
||||||
} from 'dashboard/helper/quotedEmailHelper';
|
} from 'dashboard/helper/quotedEmailHelper';
|
||||||
import {
|
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
|
||||||
CONVERSATION_EVENTS,
|
|
||||||
CAPTAIN_EVENTS,
|
|
||||||
} from '../../../helper/AnalyticsHelper/events';
|
|
||||||
import fileUploadMixin from 'dashboard/mixins/fileUploadMixin';
|
import fileUploadMixin from 'dashboard/mixins/fileUploadMixin';
|
||||||
import {
|
import {
|
||||||
appendSignature,
|
appendSignature,
|
||||||
@@ -139,7 +136,6 @@ export default {
|
|||||||
newConversationModalActive: false,
|
newConversationModalActive: false,
|
||||||
showArticleSearchPopover: false,
|
showArticleSearchPopover: false,
|
||||||
hasRecordedAudio: false,
|
hasRecordedAudio: false,
|
||||||
copilotAcceptedMessages: {},
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -199,11 +195,6 @@ export default {
|
|||||||
return this.$store.getters['inboxes/getInbox'](this.inboxId);
|
return this.$store.getters['inboxes/getInbox'](this.inboxId);
|
||||||
},
|
},
|
||||||
messagePlaceHolder() {
|
messagePlaceHolder() {
|
||||||
if (this.isEditorDisabled) {
|
|
||||||
return this.isAWhatsAppChannel
|
|
||||||
? this.$t('CONVERSATION.FOOTER.MESSAGING_RESTRICTED_WHATSAPP')
|
|
||||||
: this.$t('CONVERSATION.FOOTER.MESSAGING_RESTRICTED');
|
|
||||||
}
|
|
||||||
return this.isPrivate
|
return this.isPrivate
|
||||||
? this.$t('CONVERSATION.FOOTER.PRIVATE_MSG_INPUT')
|
? this.$t('CONVERSATION.FOOTER.PRIVATE_MSG_INPUT')
|
||||||
: this.$t('CONVERSATION.FOOTER.MSG_INPUT');
|
: this.$t('CONVERSATION.FOOTER.MSG_INPUT');
|
||||||
@@ -215,7 +206,6 @@ export default {
|
|||||||
return this.maxLength - this.message.length;
|
return this.maxLength - this.message.length;
|
||||||
},
|
},
|
||||||
isReplyButtonDisabled() {
|
isReplyButtonDisabled() {
|
||||||
if (this.isEditorDisabled) return true;
|
|
||||||
if (this.isATwitterInbox) return true;
|
if (this.isATwitterInbox) return true;
|
||||||
if (this.hasAttachments || this.hasRecordedAudio) return false;
|
if (this.hasAttachments || this.hasRecordedAudio) return false;
|
||||||
|
|
||||||
@@ -424,13 +414,6 @@ export default {
|
|||||||
isDefaultEditorMode() {
|
isDefaultEditorMode() {
|
||||||
return !this.showAudioRecorderEditor && !this.copilot.isActive.value;
|
return !this.showAudioRecorderEditor && !this.copilot.isActive.value;
|
||||||
},
|
},
|
||||||
isEditorDisabled() {
|
|
||||||
return (
|
|
||||||
this.isAWhatsAppChannel &&
|
|
||||||
!this.isOnPrivateNote &&
|
|
||||||
!this.currentChat.can_reply
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
currentChat(conversation, oldConversation) {
|
currentChat(conversation, oldConversation) {
|
||||||
@@ -525,24 +508,6 @@ export default {
|
|||||||
emitter.off(CMD_AI_ASSIST, this.executeCopilotAction);
|
emitter.off(CMD_AI_ASSIST, this.executeCopilotAction);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
getDraftKey(
|
|
||||||
conversationId = this.conversationIdByRoute,
|
|
||||||
replyType = this.replyType
|
|
||||||
) {
|
|
||||||
return `draft-${conversationId}-${replyType}`;
|
|
||||||
},
|
|
||||||
getCopilotAcceptedMessage(replyType = this.replyType) {
|
|
||||||
const key = this.getDraftKey(this.conversationIdByRoute, replyType);
|
|
||||||
return this.copilotAcceptedMessages[key] || '';
|
|
||||||
},
|
|
||||||
setCopilotAcceptedMessage(message, replyType = this.replyType) {
|
|
||||||
const key = this.getDraftKey(this.conversationIdByRoute, replyType);
|
|
||||||
this.copilotAcceptedMessages[key] = trimContent(message || '');
|
|
||||||
},
|
|
||||||
clearCopilotAcceptedMessage(replyType = this.replyType) {
|
|
||||||
const key = this.getDraftKey(this.conversationIdByRoute, replyType);
|
|
||||||
delete this.copilotAcceptedMessages[key];
|
|
||||||
},
|
|
||||||
handleInsert(article) {
|
handleInsert(article) {
|
||||||
const { url, title } = article;
|
const { url, title } = article;
|
||||||
// Removing empty lines from the title
|
// Removing empty lines from the title
|
||||||
@@ -594,7 +559,7 @@ export default {
|
|||||||
},
|
},
|
||||||
saveDraft(conversationId, replyType) {
|
saveDraft(conversationId, replyType) {
|
||||||
if (this.message || this.message === '') {
|
if (this.message || this.message === '') {
|
||||||
const key = this.getDraftKey(conversationId, replyType);
|
const key = `draft-${conversationId}-${replyType}`;
|
||||||
const draftToSave = trimContent(this.message || '');
|
const draftToSave = trimContent(this.message || '');
|
||||||
|
|
||||||
this.$store.dispatch('draftMessages/set', {
|
this.$store.dispatch('draftMessages/set', {
|
||||||
@@ -609,7 +574,7 @@ export default {
|
|||||||
},
|
},
|
||||||
getFromDraft() {
|
getFromDraft() {
|
||||||
if (this.conversationIdByRoute) {
|
if (this.conversationIdByRoute) {
|
||||||
const key = this.getDraftKey();
|
const key = `draft-${this.conversationIdByRoute}-${this.replyType}`;
|
||||||
const messageFromStore =
|
const messageFromStore =
|
||||||
this.$store.getters['draftMessages/get'](key) || '';
|
this.$store.getters['draftMessages/get'](key) || '';
|
||||||
|
|
||||||
@@ -632,7 +597,7 @@ export default {
|
|||||||
},
|
},
|
||||||
removeFromDraft() {
|
removeFromDraft() {
|
||||||
if (this.conversationIdByRoute) {
|
if (this.conversationIdByRoute) {
|
||||||
const key = this.getDraftKey();
|
const key = `draft-${this.conversationIdByRoute}-${this.replyType}`;
|
||||||
this.$store.dispatch('draftMessages/delete', { key });
|
this.$store.dispatch('draftMessages/delete', { key });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -690,9 +655,6 @@ export default {
|
|||||||
// Don't handle paste if compose new conversation modal is open
|
// Don't handle paste if compose new conversation modal is open
|
||||||
if (this.newConversationModalActive) return;
|
if (this.newConversationModalActive) return;
|
||||||
|
|
||||||
// Don't handle paste if editor is disabled
|
|
||||||
if (this.isEditorDisabled) return;
|
|
||||||
|
|
||||||
// Filter valid files (non-zero size)
|
// Filter valid files (non-zero size)
|
||||||
Array.from(e.clipboardData.files)
|
Array.from(e.clipboardData.files)
|
||||||
.filter(file => file.size > 0)
|
.filter(file => file.size > 0)
|
||||||
@@ -746,7 +708,6 @@ export default {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!this.showMentions) {
|
if (!this.showMentions) {
|
||||||
const copilotAcceptedMessage = this.getCopilotAcceptedMessage();
|
|
||||||
const isOnWhatsApp =
|
const isOnWhatsApp =
|
||||||
this.isATwilioWhatsAppChannel ||
|
this.isATwilioWhatsAppChannel ||
|
||||||
this.isAWhatsAppCloudChannel ||
|
this.isAWhatsAppCloudChannel ||
|
||||||
@@ -756,17 +717,10 @@ export default {
|
|||||||
// This can create duplicate messages in Chatwoot. To prevent this issue, we'll handle text and attachments as separate messages.
|
// This can create duplicate messages in Chatwoot. To prevent this issue, we'll handle text and attachments as separate messages.
|
||||||
const isOnInstagram = this.isAnInstagramChannel;
|
const isOnInstagram = this.isAnInstagramChannel;
|
||||||
if ((isOnWhatsApp || isOnInstagram) && !this.isPrivate) {
|
if ((isOnWhatsApp || isOnInstagram) && !this.isPrivate) {
|
||||||
this.sendMessageAsMultipleMessages(
|
this.sendMessageAsMultipleMessages(this.message);
|
||||||
this.message,
|
|
||||||
copilotAcceptedMessage
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
const messagePayload = this.getMessagePayload(this.message);
|
const messagePayload = this.getMessagePayload(this.message);
|
||||||
this.sendMessage(
|
this.sendMessage(messagePayload);
|
||||||
messagePayload,
|
|
||||||
this.message,
|
|
||||||
copilotAcceptedMessage
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.isPrivate) {
|
if (!this.isPrivate) {
|
||||||
@@ -778,53 +732,13 @@ export default {
|
|||||||
this.$emit('update:popOutReplyBox', false);
|
this.$emit('update:popOutReplyBox', false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
sendMessageAsMultipleMessages(message, copilotAcceptedMessage = '') {
|
sendMessageAsMultipleMessages(message) {
|
||||||
const messages = this.getMultipleMessagesPayload(message);
|
const messages = this.getMultipleMessagesPayload(message);
|
||||||
messages.forEach(messagePayload => {
|
messages.forEach(messagePayload => {
|
||||||
this.sendMessage(
|
this.sendMessage(messagePayload);
|
||||||
messagePayload,
|
|
||||||
messagePayload.message || '',
|
|
||||||
copilotAcceptedMessage
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
sendMessageAnalyticsData(
|
sendMessageAnalyticsData(isPrivate) {
|
||||||
isPrivate,
|
|
||||||
{ editorMessage = '', copilotAcceptedMessage = '' } = {}
|
|
||||||
) {
|
|
||||||
const normalizeForComparison = message => {
|
|
||||||
let normalizedMessage = message || '';
|
|
||||||
|
|
||||||
if (this.sendWithSignature && this.messageSignature && !isPrivate) {
|
|
||||||
const effectiveChannelType = getEffectiveChannelType(
|
|
||||||
this.channelType,
|
|
||||||
this.inbox?.medium || ''
|
|
||||||
);
|
|
||||||
normalizedMessage = removeSignature(
|
|
||||||
normalizedMessage,
|
|
||||||
this.messageSignature,
|
|
||||||
effectiveChannelType
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return trimContent(normalizedMessage);
|
|
||||||
};
|
|
||||||
|
|
||||||
const normalizedAcceptedMessage = normalizeForComparison(
|
|
||||||
copilotAcceptedMessage
|
|
||||||
);
|
|
||||||
const normalizedEditorMessage = normalizeForComparison(editorMessage);
|
|
||||||
|
|
||||||
if (normalizedAcceptedMessage && normalizedEditorMessage) {
|
|
||||||
useTrack(CAPTAIN_EVENTS.AI_ASSISTED_MESSAGE_SENT, {
|
|
||||||
conversationId: this.conversationIdByRoute,
|
|
||||||
channelType: this.channelType,
|
|
||||||
editedBeforeSend:
|
|
||||||
normalizedAcceptedMessage !== normalizedEditorMessage,
|
|
||||||
isPrivate,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Analytics data for message signature is enabled or not in channels
|
// Analytics data for message signature is enabled or not in channels
|
||||||
return isPrivate
|
return isPrivate
|
||||||
? useTrack(CONVERSATION_EVENTS.SENT_PRIVATE_NOTE)
|
? useTrack(CONVERSATION_EVENTS.SENT_PRIVATE_NOTE)
|
||||||
@@ -858,11 +772,7 @@ export default {
|
|||||||
this.confirmOnSendReply();
|
this.confirmOnSendReply();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async sendMessage(
|
async sendMessage(messagePayload) {
|
||||||
messagePayload,
|
|
||||||
editorMessage = '',
|
|
||||||
copilotAcceptedMessage = ''
|
|
||||||
) {
|
|
||||||
try {
|
try {
|
||||||
await this.$store.dispatch(
|
await this.$store.dispatch(
|
||||||
'createPendingMessageAndSend',
|
'createPendingMessageAndSend',
|
||||||
@@ -871,10 +781,7 @@ export default {
|
|||||||
emitter.emit(BUS_EVENTS.SCROLL_TO_MESSAGE);
|
emitter.emit(BUS_EVENTS.SCROLL_TO_MESSAGE);
|
||||||
emitter.emit(BUS_EVENTS.MESSAGE_SENT);
|
emitter.emit(BUS_EVENTS.MESSAGE_SENT);
|
||||||
this.removeFromDraft();
|
this.removeFromDraft();
|
||||||
this.sendMessageAnalyticsData(messagePayload.private, {
|
this.sendMessageAnalyticsData(messagePayload.private);
|
||||||
editorMessage,
|
|
||||||
copilotAcceptedMessage,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
error?.response?.data?.error || this.$t('CONVERSATION.MESSAGE_ERROR');
|
error?.response?.data?.error || this.$t('CONVERSATION.MESSAGE_ERROR');
|
||||||
@@ -948,7 +855,6 @@ export default {
|
|||||||
},
|
},
|
||||||
clearMessage() {
|
clearMessage() {
|
||||||
this.message = '';
|
this.message = '';
|
||||||
this.clearCopilotAcceptedMessage();
|
|
||||||
if (this.sendWithSignature && !this.isPrivate) {
|
if (this.sendWithSignature && !this.isPrivate) {
|
||||||
// if signature is enabled, append it to the message
|
// if signature is enabled, append it to the message
|
||||||
const effectiveChannelType = getEffectiveChannelType(
|
const effectiveChannelType = getEffectiveChannelType(
|
||||||
@@ -1213,9 +1119,7 @@ export default {
|
|||||||
this.$emit('update:popOutReplyBox', !this.popOutReplyBox);
|
this.$emit('update:popOutReplyBox', !this.popOutReplyBox);
|
||||||
},
|
},
|
||||||
onSubmitCopilotReply() {
|
onSubmitCopilotReply() {
|
||||||
const acceptedMessage = this.copilot.accept();
|
this.message = this.copilot.accept();
|
||||||
this.message = acceptedMessage;
|
|
||||||
this.setCopilotAcceptedMessage(acceptedMessage);
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -1226,13 +1130,11 @@ export default {
|
|||||||
<div ref="replyEditor" class="reply-box" :class="replyBoxClass">
|
<div ref="replyEditor" class="reply-box" :class="replyBoxClass">
|
||||||
<ReplyTopPanel
|
<ReplyTopPanel
|
||||||
:mode="replyType"
|
:mode="replyType"
|
||||||
:conversation-id="conversationId"
|
|
||||||
:is-reply-restricted="isReplyRestricted"
|
:is-reply-restricted="isReplyRestricted"
|
||||||
:disabled="
|
:disabled="
|
||||||
(copilot.isActive.value && copilot.isButtonDisabled.value) ||
|
(copilot.isActive.value && copilot.isButtonDisabled.value) ||
|
||||||
showAudioRecorderEditor
|
showAudioRecorderEditor
|
||||||
"
|
"
|
||||||
:is-editor-disabled="isEditorDisabled"
|
|
||||||
:is-message-length-reaching-threshold="isMessageLengthReachingThreshold"
|
:is-message-length-reaching-threshold="isMessageLengthReachingThreshold"
|
||||||
:characters-remaining="charactersRemaining"
|
:characters-remaining="charactersRemaining"
|
||||||
:popout-reply-box="popOutReplyBox"
|
:popout-reply-box="popOutReplyBox"
|
||||||
@@ -1302,14 +1204,12 @@ export default {
|
|||||||
<WootMessageEditor
|
<WootMessageEditor
|
||||||
v-else-if="!showAudioRecorderEditor"
|
v-else-if="!showAudioRecorderEditor"
|
||||||
v-model="message"
|
v-model="message"
|
||||||
:conversation-id="conversationId"
|
|
||||||
:editor-id="editorStateId"
|
:editor-id="editorStateId"
|
||||||
class="input popover-prosemirror-menu"
|
class="input popover-prosemirror-menu"
|
||||||
:is-private="isOnPrivateNote"
|
:is-private="isOnPrivateNote"
|
||||||
:placeholder="messagePlaceHolder"
|
:placeholder="messagePlaceHolder"
|
||||||
:update-selection-with="updateEditorSelectionWith"
|
:update-selection-with="updateEditorSelectionWith"
|
||||||
:min-height="4"
|
:min-height="4"
|
||||||
:disabled="isEditorDisabled"
|
|
||||||
enable-variables
|
enable-variables
|
||||||
:variables="messageVariables"
|
:variables="messageVariables"
|
||||||
:signature="messageSignature"
|
:signature="messageSignature"
|
||||||
@@ -1385,7 +1285,6 @@ export default {
|
|||||||
:is-recording-audio="isRecordingAudio"
|
:is-recording-audio="isRecordingAudio"
|
||||||
:is-send-disabled="isReplyButtonDisabled"
|
:is-send-disabled="isReplyButtonDisabled"
|
||||||
:is-note="isPrivate"
|
:is-note="isPrivate"
|
||||||
:is-editor-disabled="isEditorDisabled"
|
|
||||||
:on-file-upload="onFileUpload"
|
:on-file-upload="onFileUpload"
|
||||||
:on-send="onSendReply"
|
:on-send="onSendReply"
|
||||||
:conversation-type="conversationType"
|
:conversation-type="conversationType"
|
||||||
|
|||||||
@@ -31,10 +31,7 @@ const assignedAgent = computed({
|
|||||||
},
|
},
|
||||||
set(agent) {
|
set(agent) {
|
||||||
const agentId = agent ? agent.id : null;
|
const agentId = agent ? agent.id : null;
|
||||||
store.dispatch('setCurrentChatAssignee', {
|
store.dispatch('setCurrentChatAssignee', agent);
|
||||||
conversationId: currentChat.value?.id,
|
|
||||||
assignee: agent,
|
|
||||||
});
|
|
||||||
store.dispatch('assignAgent', {
|
store.dispatch('assignAgent', {
|
||||||
conversationId: currentChat.value?.id,
|
conversationId: currentChat.value?.id,
|
||||||
agentId,
|
agentId,
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user