Compare commits

..
Author SHA1 Message Date
Shivam Mishra b83c7c8c39 feat: update message 2024-07-26 14:09:05 +05:30
Shivam Mishra 496e78b5d3 feat: add console.warn for action cable ignore 2024-07-26 14:08:53 +05:30
6694db093f feat: Update the design for dashboard_apps (#9840)
This PR migrates the dashboard apps page to the new layout and includes
the following updates:

- Create a compact design for the back button
- Add a back button to the settings header
- Reduce letter-spacing on the description
- Fix mobile styles
- Migrate the layout of dashboard apps/index to new layouts


Note: I've moved all feature help URLs from features.yml to the frontend. This change prevents features.yml from becoming bloated due to frontend modifications.

---------

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2024-07-25 16:26:00 -07:00
0331815cc5 feat: Integration with Captain (alpha) (#9834)
- Integration with captain (alpha)

Co-authored-by: Pranav <pranavrajs@gmail.com>
2024-07-25 14:24:04 -07:00
Chatwoot BotandGitHub 027a540bbd chore: Update translations from Crowdin (#9835) 2024-07-25 11:58:22 -07:00
Sivin VargheseandGitHub 239c61095a fix: Show add/import/export buttons in mobile view (#9761) 2024-07-25 14:08:32 +05:30
e9d47d31c7 chore: [Snyk] Fix for 1 vulnerabilities (#9832)
Update dependencies to mitigate vulnerabilities 

Co-authored-by: snyk-bot <snyk-bot@snyk.io>
2024-07-24 16:42:39 -07:00
8b1d98af52 feat: Update the design for integration page (#9825)
Combine integrations and applications page into one page. 

<img width="1182" alt="Screenshot 2024-07-23 at 3 30 51 PM"
src="https://github.com/user-attachments/assets/50920a6f-606f-44b3-b1e4-641046a14444">

Major changes:

- The app enabled?, active? checks are all moved to backend.
- The dashboard_apps integration is also now part of the apps.yml file.
- Updated the header design for the new settings pages.
- Merged the folders integrationapps and integrations. 
- Updated the copy to match the size of the card and provide clear
instruction.
- Only the list page is updated in this PR, rest of the pages are yet to
be migrated.


| Integration | Verified | 
| -- | -- |
| Dashboard Apps | ✅ | 
| Dyte | ✅ | 
| Slack | ✅ | 
| Webhooks | ✅ | 
| Dialogflow | ✅ | 
| Google Translate | ✅ | 
| OpenAI | ✅ | 
| Linear | ✅ |

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2024-07-24 16:35:40 -07:00
Sojan JoseandGitHub 8e2b329202 feat: Render instagram reels in Chatwoot (#9829)
- Previously we were ignoring the reels shared over Instagram messages.
This PR will render the reels with in Chatwoot.

followup : we need to render reels in a better interface so that it is
clearly denoted to the user that its an Instagram reel
2024-07-24 12:58:12 -07:00
Sivin VargheseandGitHub 71073a06ff fix: Broken connect to facebook page layout (#9830)
# Pull Request Template

## Description

This PR will fix the layout issue in connecting to the Facebook page
layout.

Fixes
https://linear.app/chatwoot/issue/CW-3489/connect-facebook-page-layout-is-broken

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

**Before**
<img width="1264" alt="image"
src="https://github.com/user-attachments/assets/2b9e412b-f172-4184-b45b-8c97333c3e51">


**After**
<img width="1264" alt="image"
src="https://github.com/user-attachments/assets/035f3468-f611-43ce-a058-fb8eafa1a45d">




## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
2024-07-24 16:45:17 +05:30
PranavandGitHub 39d20b197d chore: Update the integration icons, logic for enabled/active attributes for the integration (#9828)
This PR would update the logos for the integrations (also add the dark mode variants to be used in the future) and updates the logic for enabled / active attributes in the apps.
2024-07-23 17:45:53 -07:00
Sivin VargheseandGitHub fb99ba7b40 feat: Rewrite uiSettings mixin to a composable (#9819) 2024-07-23 21:27:22 +05:30
79aa5a5d7f feat: Replace alertMixin usage with useAlert (#9793)
# Pull Request Template

## Description

This PR will replace the usage of `alertMixin` from the code base with
the `useAlert` composable.

Fixes
https://linear.app/chatwoot/issue/CW-3462/replace-alertmixin-usage-with-usealert

## Type of change

- [x] Breaking change (fix or feature that would cause existing
functionality not to work as expected)

## How Has This Been Tested?

Please refer this issue description

https://linear.app/chatwoot/issue/CW-3462/replace-alertmixin-usage-with-usealert


## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

---------

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2024-07-23 16:41:11 +05:30
Sivin VargheseandGitHub 10ee773aac feat: Rewrite labels/validationMixin mixin to a helper (#9818) 2024-07-22 21:41:05 +05:30
Sivin VargheseandGitHub 81d7c51a84 feat: Replace contentTypeMixin within the component (#9802)
# Pull Request Template

## Description

This PR will completely remove the `contentTypeMixin` and be used in the
component as it is.

Fixes
https://linear.app/chatwoot/issue/CW-3465/rewrite-contenttypemixin-mixin-as-a-helper

## Type of change

- [x] New feature (non-breaking change which adds functionality)

## How Has This Been Tested?

Take a look at this component
`app/javascript/dashboard/components/widgets/conversation/Message.vue`


## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
2024-07-22 13:08:04 +05:30
79381b08cc feat: Move timeMixin to a helper (#9799)
# Pull Request Template

## Description

This PR will replace the usage of `timeMixin` with `timeHelper`

Fixes
https://linear.app/chatwoot/issue/CW-3451/move-time-mixin-to-a-helper

## Type of change

- [x] New feature (non-breaking change which adds functionality)

## How Has This Been Tested?

Please refer to this issue description.
https://linear.app/chatwoot/issue/CW-3451/move-time-mixin-to-a-helper

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

---------

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2024-07-22 13:07:29 +05:30
Sivin VargheseandGitHub 84c380c8c4 feat: Replace SLA validationMixin within the component (#9804)
# Pull Request Template

## Description

This PR will completely remove the SLA `validationMixin` and be used in
the component as it is with a name change.

Fixes
https://linear.app/chatwoot/issue/CW-3456/rewrite-sla-validationmixin-mixin-to-a-composable

## Type of change

- [x] New feature (non-breaking change which adds functionality)

## How Has This Been Tested?

Take a look at these components
1. `dashboard/routes/dashboard/settings/sla/AddSLA.vue` **(Not used)**
2. `dashboard/routes/dashboard/settings/sla/SlaForm.vue`
3. `dashboard/routes/dashboard/settings/sla/SlaTimeInput.vue`


## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
2024-07-22 13:05:55 +05:30
cb0642564c feat: add promise based loader for FB script (#9780)
![CleanShot 2024-07-16 at 11 10
40@2x](https://github.com/user-attachments/assets/8b938968-5f80-4a19-95fb-e00e1dbd7526)

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2024-07-22 11:32:05 +05:30
23e30fcb1a feat: Delete bulkActionsMixin (#9800)
# Pull Request Template

## Description

This PR will remove the `bulkActionsMixin` usage. Seems like it is not
used anywhere.

Fixes https://linear.app/chatwoot/issue/CW-3453/delete-bulkactionsmixin

## Type of change

- [x] New feature (non-breaking change which adds functionality)


## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2024-07-19 11:14:56 +05:30
Sojan JoseandGitHub ae8619142f chore: Update dependencies to fix security issues (#9801)
- update dependencies to fix security issues
2024-07-18 20:08:26 -07:00
Vishnu NarayananandGitHub 499408ea6f fix: add restart policy for rails/sidekiq containers (#9797)
- Add `restart:always` policy for rails and sidekiq containers in the
production compose file

Fixes #9501
Fixes
https://linear.app/chatwoot/issue/PR-1099/missing-restart-always-at-docker-compose-file
2024-07-18 18:33:03 +05:30
Sojan dc245814fe Merge branch 'release/3.11.0' into develop 2024-07-16 19:23:13 -07:00
Sojan 6c52a0638d Bump version to 3.11.0 2024-07-16 19:22:11 -07:00
64a9a46b28 fix: Docker compose build of unable to select packages (#9783)
Fixes #9782

Co-authored-by: grape <grape@auro.gg>
2024-07-16 09:51:56 +05:30
Sivin VargheseandGitHub 30d4f6c2f5 fix: TypeError cannot read properties of undefined (reading 'status') (#9774)
# Pull Request Template

## Description

**Issue**
The error `Cannot read properties of undefined (reading 'status')`
occurs because the `error.response.status` is accessed without checking
if `error.response` is present.

I can't able to reproduce this issue.

**Solution**
To resolve this issue, I added a check to ensure that this
`error.response` is defined before accessing the `error.response.status`

Fixes
https://linear.app/chatwoot/issue/CW-3322/typeerror-cannot-read-properties-of-undefined-reading-status

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
2024-07-16 07:02:02 +05:30
Sivin VargheseandGitHub 5909c0f8b7 fix: TypeError cannot read properties of null (reading 'file_type') (#9778)
# Pull Request Template

## Description

**Cases**

## Type of change


- [x] Bug fix (non-breaking change which fixes an issue)



## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
2024-07-16 07:01:48 +05:30
Sivin VargheseandGitHub 554388bff3 fix: TypeError cannot read property 'focus' of undefined (#9765) 2024-07-15 19:33:16 +05:30
Sivin VargheseandGitHub ebf3b46f0d fix: TypeError cannot read properties of undefined (reading '$store') (#9777)
This issue is an extremely narrow edge case that does not affect the app functionality. It occurs here

1. `executeRecurringNotification` is the method that sets a timeout of 30 seconds and pings as required
2. This method is triggered when one of the following events happen `click`, `touchstart`, `keypress`, `keydown`
3. The class in question is initalized before the Vue app is mounted in `application.js`, the vue app is mounted on `window.onload` event
4. In a case where the Vue app is not created (i.e. `window.onload` event is not fired) within 30 seconds of a user input, this error occurs.
2024-07-15 18:50:43 +05:30
Shivam MishraandGitHub 5cfac31f93 fix: return if user is not present (#9768)
Fixes
https://chatwoot-p3.sentry.io/share/issue/aa6a3c0accb345e984ed80eaddc3efd5/

In the case where the user is redirected to the page from a redirect
like google login, the analytics helper breaks. This PR fixes it by
checking if the user is present.

This does not break the user email tracking since the user is redirected
anyway and the `AnalyticsHelper` is reinitialised
2024-07-15 17:42:35 +05:30
Sivin VargheseandGitHub 787ac3134d fix: TypeError cannot read properties of undefined (reading 'data') (#9766)
# Pull Request Template

## Description

**Issue**
The error `Cannot read properties of undefined (reading 'data')` occurs
because the `error.response.data` is accessed without checking if
`error.response` is present.

I can able to reproduce this issue.

**Solution**
To resolve this issue, I can add a check to ensure that this
`error.response` is defined before accessing the `error.response.data`

Fixes
https://linear.app/chatwoot/issue/CW-3406/typeerror-cannot-read-properties-of-undefined-reading-data

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)


## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
2024-07-12 09:21:40 +05:30
Sivin VargheseandGitHub 2a06c85f1a fix: Message editor fails to render markdown headings (#9748) 2024-07-11 12:23:27 +05:30
761 changed files with 16209 additions and 6867 deletions
+2
View File
@@ -257,3 +257,5 @@ AZURE_APP_SECRET=
# Set to true if you want to remove stale contact inboxes
# contact_inboxes with no conversation older than 90 days will be removed
# REMOVE_STALE_CONTACT_INBOX_JOB_STATUS=false
# CAPTAIN_API_URL=http://localhost:3001/api
+2 -2
View File
@@ -111,9 +111,9 @@ gem 'elastic-apm', require: false
gem 'newrelic_rpm', require: false
gem 'newrelic-sidekiq-metrics', '>= 1.6.2', require: false
gem 'scout_apm', require: false
gem 'sentry-rails', '>= 5.18.1', require: false
gem 'sentry-rails', '>= 5.18.2', require: false
gem 'sentry-ruby', require: false
gem 'sentry-sidekiq', '>= 5.18.1', require: false
gem 'sentry-sidekiq', '>= 5.18.2', require: false
##-- background job processing --##
gem 'sidekiq', '>= 7.3.0'
+70 -29
View File
@@ -103,8 +103,8 @@ GEM
tzinfo (~> 2.0)
acts-as-taggable-on (9.0.1)
activerecord (>= 6.0, < 7.1)
addressable (2.8.4)
public_suffix (>= 2.0.2, < 6.0)
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
administrate (0.20.1)
actionpack (>= 6.0, < 8.0)
actionview (>= 6.0, < 8.0)
@@ -148,7 +148,7 @@ GEM
barnes (0.0.9)
multi_json (~> 1)
statsd-ruby (~> 1.1)
base64 (0.1.1)
base64 (0.2.0)
bcrypt (3.1.20)
bigdecimal (3.1.8)
bindex (0.8.1)
@@ -171,7 +171,8 @@ GEM
commonmarker (0.23.10)
concurrent-ruby (1.3.3)
connection_pool (2.4.1)
crack (0.4.5)
crack (1.0.0)
bigdecimal
rexml
crass (1.0.6)
csv (3.3.0)
@@ -242,9 +243,8 @@ GEM
railties (>= 5.0.0)
faker (3.2.0)
i18n (>= 1.8.11, < 2)
faraday (2.7.4)
faraday-net_http (>= 2.0, < 3.1)
ruby2_keywords (>= 0.0.4)
faraday (2.9.0)
faraday-net_http (>= 2.0, < 3.2)
faraday-follow_redirects (0.3.0)
faraday (>= 1, < 3)
faraday-mashify (0.1.1)
@@ -252,7 +252,8 @@ GEM
hashie
faraday-multipart (1.0.4)
multipart-post (~> 2)
faraday-net_http (3.0.2)
faraday-net_http (3.1.0)
net-http
faraday-net_http_persistent (2.1.0)
faraday (~> 2.5)
net-http-persistent (~> 4.0)
@@ -321,7 +322,10 @@ GEM
google-cloud-translate-v3 (0.6.0)
gapic-common (>= 0.17.1, < 2.a)
google-cloud-errors (~> 1.0)
google-protobuf (3.25.3)
google-protobuf (3.25.3-arm64-darwin)
google-protobuf (3.25.3-x86_64-darwin)
google-protobuf (3.25.3-x86_64-linux)
googleapis-common-protos (1.4.0)
google-protobuf (~> 3.14)
googleapis-common-protos-types (~> 1.2)
@@ -337,7 +341,16 @@ GEM
signet (>= 0.16, < 2.a)
groupdate (6.2.1)
activesupport (>= 5.2)
grpc (1.63.0-arm64-darwin)
grpc (1.62.0)
google-protobuf (~> 3.25)
googleapis-common-protos-types (~> 1.0)
grpc (1.62.0-arm64-darwin)
google-protobuf (~> 3.25)
googleapis-common-protos-types (~> 1.0)
grpc (1.62.0-x86_64-darwin)
google-protobuf (~> 3.25)
googleapis-common-protos-types (~> 1.0)
grpc (1.62.0-x86_64-linux)
google-protobuf (~> 3.25)
googleapis-common-protos-types (~> 1.0)
haikunator (1.1.1)
@@ -346,7 +359,7 @@ GEM
ruby2ruby (~> 2.4)
ruby_parser (~> 3.10)
hana (1.3.7)
hashdiff (1.0.1)
hashdiff (1.1.0)
hashie (5.0.0)
http (5.1.1)
addressable (~> 2.8)
@@ -361,7 +374,7 @@ GEM
mini_mime (>= 1.0.0)
multi_xml (>= 0.5.2)
httpclient (2.8.3)
i18n (1.14.1)
i18n (1.14.5)
concurrent-ruby (~> 1.0)
image_processing (1.12.2)
mini_magick (>= 4.9.5, < 5)
@@ -385,7 +398,8 @@ GEM
hana (~> 1.3)
regexp_parser (~> 2.0)
uri_template (~> 0.7)
jwt (2.7.0)
jwt (2.8.1)
base64
kaminari (1.2.2)
activesupport (>= 4.1.0)
kaminari-actionview (= 1.2.2)
@@ -409,8 +423,15 @@ GEM
letter_opener (1.8.1)
launchy (>= 2.2, < 3)
libdatadog (7.0.0.1.0)
libdatadog (7.0.0.1.0-x86_64-linux)
libddwaf (1.14.0.0.0)
ffi (~> 1.0)
libddwaf (1.14.0.0.0-arm64-darwin)
ffi (~> 1.0)
libddwaf (1.14.0.0.0-x86_64-darwin)
ffi (~> 1.0)
libddwaf (1.14.0.0.0-x86_64-linux)
ffi (~> 1.0)
line-bot-api (1.28.0)
liquid (5.4.0)
listen (3.8.0)
@@ -439,12 +460,13 @@ GEM
meta_request (0.8.2)
rack-contrib (>= 1.1, < 3)
railties (>= 3.0.0, < 8)
method_source (1.0.0)
method_source (1.1.0)
mime-types (3.4.1)
mime-types-data (~> 3.2015)
mime-types-data (3.2023.0218.1)
mini_magick (4.12.0)
mini_mime (1.1.5)
mini_portile2 (2.8.7)
minitest (5.24.1)
mock_redis (0.36.0)
ruby2_keywords
@@ -454,6 +476,8 @@ GEM
multipart-post (2.3.0)
neighbor (0.2.3)
activerecord (>= 5.2)
net-http (0.4.1)
uri
net-http-persistent (4.0.2)
connection_pool (~> 2.2)
net-imap (0.4.12)
@@ -472,8 +496,15 @@ GEM
newrelic_rpm (9.6.0)
base64
nio4r (2.7.3)
nokogiri (1.16.6)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
nokogiri (1.16.6-arm64-darwin)
racc (~> 1.4)
nokogiri (1.16.6-x86_64-darwin)
racc (~> 1.4)
nokogiri (1.16.6-x86_64-linux)
racc (~> 1.4)
oauth (1.1.0)
oauth-tty (~> 1.0, >= 1.0.1)
snaky_hash (~> 2.0)
@@ -520,7 +551,7 @@ GEM
method_source (~> 1.0)
pry-rails (0.3.9)
pry (>= 0.10.4)
public_suffix (5.0.1)
public_suffix (6.0.0)
puma (6.4.2)
nio4r (~> 2.0)
pundit (2.3.0)
@@ -536,7 +567,8 @@ GEM
rack (>= 2.0.0)
rack-mini-profiler (3.2.0)
rack (>= 1.2.0)
rack-protection (3.1.0)
rack-protection (3.2.0)
base64 (>= 0.1.0)
rack (~> 2.2, >= 2.2.4)
rack-proxy (0.7.6)
rack
@@ -572,7 +604,7 @@ GEM
thor (~> 1.0)
zeitwerk (~> 2.5)
rainbow (3.1.1)
rake (13.1.0)
rake (13.2.1)
rb-fsevent (0.11.2)
rb-inotify (0.10.1)
ffi (~> 1.0)
@@ -602,7 +634,7 @@ GEM
retriable (3.1.2)
reverse_markdown (2.1.1)
nokogiri
rexml (3.3.1)
rexml (3.3.2)
strscan
rspec-core (3.13.0)
rspec-support (~> 3.13.0)
@@ -678,14 +710,14 @@ GEM
activesupport (>= 4)
selectize-rails (0.12.6)
semantic_range (3.0.0)
sentry-rails (5.18.1)
sentry-rails (5.18.2)
railties (>= 5.0)
sentry-ruby (~> 5.18.1)
sentry-ruby (5.18.1)
sentry-ruby (~> 5.18.2)
sentry-ruby (5.18.2)
bigdecimal
concurrent-ruby (~> 1.0, >= 1.0.2)
sentry-sidekiq (5.18.1)
sentry-ruby (~> 5.18.1)
sentry-sidekiq (5.18.2)
sentry-ruby (~> 5.18.2)
sidekiq (>= 3.0)
sexp_processor (4.17.0)
shoulda-matchers (5.3.0)
@@ -737,7 +769,7 @@ GEM
strscan (3.1.0)
telephone_number (1.4.20)
test-prof (1.2.1)
thor (1.3.0)
thor (1.3.1)
tilt (2.3.0)
time_diff (0.3.0)
activesupport
@@ -762,11 +794,12 @@ GEM
unf_ext (0.0.8.2)
unicode-display_width (2.4.2)
uniform_notifier (1.16.0)
uri (0.13.0)
uri_template (0.7.0)
valid_email2 (4.0.6)
activemodel (>= 3.2)
mail (~> 2.5)
version_gem (1.1.3)
version_gem (1.1.4)
warden (1.2.9)
rack (>= 2.0.9)
web-console (4.2.1)
@@ -777,7 +810,7 @@ GEM
web-push (3.0.1)
jwt (~> 2.0)
openssl (~> 3.0)
webmock (3.18.1)
webmock (3.23.1)
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
@@ -797,7 +830,15 @@ GEM
zeitwerk (2.6.16)
PLATFORMS
arm64-darwin-23
arm64-darwin-20
arm64-darwin-21
arm64-darwin-22
ruby
x86_64-darwin-18
x86_64-darwin-20
x86_64-darwin-21
x86_64-darwin-22
x86_64-linux
DEPENDENCIES
active_record_query_trace
@@ -898,9 +939,9 @@ DEPENDENCIES
scout_apm
scss_lint
seed_dump
sentry-rails (>= 5.18.1)
sentry-rails (>= 5.18.2)
sentry-ruby
sentry-sidekiq (>= 5.18.1)
sentry-sidekiq (>= 5.18.2)
shoulda-matchers
sidekiq (>= 7.3.0)
sidekiq-cron (>= 1.12.0)
@@ -930,4 +971,4 @@ RUBY VERSION
ruby 3.3.3p89
BUNDLED WITH
2.5.14
2.5.16
@@ -27,7 +27,7 @@ class Messages::Messenger::MessageBuilder
file_type = attachment['type'].to_sym
params = { file_type: file_type, account_id: @message.account_id }
if [:image, :file, :audio, :video, :share, :story_mention].include? file_type
if [:image, :file, :audio, :video, :share, :story_mention, :ig_reel].include? file_type
params.merge!(file_type_params(attachment))
elsif file_type == :location
params.merge!(location_params(attachment))
@@ -1,30 +0,0 @@
class V2::BaseConversationReportBuilder
pattr_initialize :account, :params
private
AVG_METRICS = %w[avg_first_response_time avg_resolution_time reply_time].freeze
COUNT_METRICS = %w[
conversations_count
incoming_messages_count
outgoing_messages_count
resolutions_count
bot_resolutions_count
bot_handoffs_count
].freeze
def builder_class(metric)
case metric
when *AVG_METRICS
V2::Reports::Timeseries::AverageReportBuilder
when *COUNT_METRICS
V2::Reports::Timeseries::CountReportBuilder
end
end
def log_invalid_metric
Rails.logger.error "ReportBuilder: Invalid metric - #{params[:metric]}"
{}
end
end
@@ -1,30 +0,0 @@
class V2::ConversationMetricBuilder < V2::BaseConversationReportBuilder
def summary
{
conversations_count: count('conversations_count'),
incoming_messages_count: count('incoming_messages_count'),
outgoing_messages_count: count('outgoing_messages_count'),
avg_first_response_time: count('avg_first_response_time'),
avg_resolution_time: count('avg_resolution_time'),
resolutions_count: count('resolutions_count'),
reply_time: count('reply_time')
}
end
def bot_summary
{
bot_resolutions_count: count('bot_resolutions_count'),
bot_handoffs_count: count('bot_handoffs_count')
}
end
private
def count(metric)
builder_class(metric).new(account, builder_params(metric)).aggregate_value
end
def builder_params(metric)
params.merge({ metric: metric })
end
end
@@ -1,25 +0,0 @@
class V2::ConversationReportBuilder < V2::BaseConversationReportBuilder
include DateRangeHelper
pattr_initialize :account, :params
def timeseries
perform_action(:timeseries)
end
def aggregate_value
perform_action(:aggregate_value)
end
private
def perform_action(method_name)
return builder.new(account, params).public_send(method_name) if builder.present?
log_invalid_metric
end
def builder
builder_class(params[:metric])
end
end
@@ -3,7 +3,6 @@ class V2::Reports::AgentSummaryBuilder < V2::Reports::BaseSummaryBuilder
def build
set_grouped_conversations_count
set_grouped_resolved_conversations_count
set_grouped_avg_reply_time
set_grouped_avg_first_response_time
set_grouped_avg_resolution_time
@@ -28,15 +27,6 @@ class V2::Reports::AgentSummaryBuilder < V2::Reports::BaseSummaryBuilder
@grouped_avg_reply_time = get_grouped_average(reporting_events.where(name: 'reply_time'))
end
def set_grouped_resolved_conversations_count
@grouped_resolved_conversations_count = reporting_events
.where(name: 'conversation_resolved')
.select(group_by_key, :conversation_id)
.distinct
.group(group_by_key)
.count(:conversation_id)
end
def group_by_key
:user_id
end
@@ -47,14 +37,12 @@ class V2::Reports::AgentSummaryBuilder < V2::Reports::BaseSummaryBuilder
def prepare_report
account.account_users.each_with_object([]) do |account_user, arr|
user_id = account_user.user_id
arr << {
id: user_id,
conversations_count: @grouped_conversations_count[user_id],
resolved_conversations_count: @grouped_resolved_conversations_count[user_id],
avg_resolution_time: @grouped_avg_resolution_time[user_id],
avg_first_response_time: @grouped_avg_first_response_time[user_id],
avg_reply_time: @grouped_avg_reply_time[user_id]
id: account_user.user_id,
conversations_count: @grouped_conversations_count[account_user.user_id],
avg_resolution_time: @grouped_avg_resolution_time[account_user.user_id],
avg_first_response_time: @grouped_avg_first_response_time[account_user.user_id],
avg_reply_time: @grouped_avg_reply_time[account_user.user_id]
}
end
end
@@ -1,65 +0,0 @@
class V2::Reports::InboxSummaryBuilder < V2::Reports::BaseSummaryBuilder
pattr_initialize [:account!, :params!]
def build
set_grouped_conversations_count
set_grouped_resolved_conversations_count
set_grouped_avg_reply_time
set_grouped_avg_first_response_time
set_grouped_avg_resolution_time
prepare_report
end
private
def set_grouped_conversations_count
@grouped_conversations_count = Current.account.conversations.where(created_at: range).group('inbox_id').count
end
def set_grouped_avg_resolution_time
@grouped_avg_resolution_time = get_grouped_average(reporting_events.where(name: 'conversation_resolved'))
end
def set_grouped_avg_first_response_time
@grouped_avg_first_response_time = get_grouped_average(reporting_events.where(name: 'first_response'))
end
def set_grouped_avg_reply_time
@grouped_avg_reply_time = get_grouped_average(reporting_events.where(name: 'reply_time'))
end
def set_grouped_resolved_conversations_count
@grouped_resolved_conversations_count = reporting_events
.where(name: 'conversation_resolved')
.select(group_by_key, :conversation_id)
.distinct
.group(group_by_key)
.count(:conversation_id)
end
def group_by_key
:inbox_id
end
def reporting_events
@reporting_events ||= Current.account.reporting_events.where(created_at: range)
end
def prepare_report
account.inboxes.each_with_object([]) do |inbox, arr|
inbox_id = inbox.id
arr << {
id: inbox_id,
conversations_count: @grouped_conversations_count[inbox_id],
resolved_conversations_count: @grouped_resolved_conversations_count[inbox_id],
avg_resolution_time: @grouped_avg_resolution_time[inbox_id],
avg_first_response_time: @grouped_avg_first_response_time[inbox_id],
avg_reply_time: @grouped_avg_reply_time[inbox_id]
}
end
end
def average_value_key
ActiveModel::Type::Boolean.new.cast(params[:business_hours]).present? ? :value_in_business_hours : :value
end
end
@@ -3,7 +3,6 @@ class V2::Reports::TeamSummaryBuilder < V2::Reports::BaseSummaryBuilder
def build
set_grouped_conversations_count
set_grouped_resolved_conversations_count
set_grouped_avg_reply_time
set_grouped_avg_first_response_time
set_grouped_avg_resolution_time
@@ -28,15 +27,6 @@ class V2::Reports::TeamSummaryBuilder < V2::Reports::BaseSummaryBuilder
@grouped_avg_reply_time = get_grouped_average(reporting_events.where(name: 'reply_time'))
end
def set_grouped_resolved_conversations_count
@grouped_resolved_conversations_count = reporting_events
.where(name: 'conversation_resolved')
.select(group_by_key, :conversation_id)
.distinct
.group(group_by_key)
.count(:conversation_id)
end
def reporting_events
@reporting_events ||= Current.account.reporting_events.where(created_at: range).joins(:conversation)
end
@@ -50,7 +40,6 @@ class V2::Reports::TeamSummaryBuilder < V2::Reports::BaseSummaryBuilder
arr << {
id: team.id,
conversations_count: @grouped_conversations_count[team.id],
resolved_conversations_count: @grouped_resolved_conversations_count[team.id],
avg_resolution_time: @grouped_avg_resolution_time[team.id],
avg_first_response_time: @grouped_avg_first_response_time[team.id],
avg_reply_time: @grouped_avg_reply_time[team.id]
@@ -10,7 +10,7 @@ class Api::V1::Accounts::Integrations::AppsController < Api::V1::Accounts::BaseC
private
def fetch_apps
@apps = Integrations::App.all.select(&:active?)
@apps = Integrations::App.all.select { |app| app.active?(Current.account) }
end
def fetch_app
@@ -1,57 +0,0 @@
class Api::V2::Accounts::LiveReportsController < Api::V1::Accounts::BaseController
before_action :load_conversations, only: [:conversation_metrics, :grouped_conversation_metrics]
before_action :set_group_scope, only: [:grouped_conversation_metrics]
def conversation_metrics
render json: {
open: @conversations.open.count,
unattended: @conversations.open.unattended.count,
unassigned: @conversations.open.unassigned.count
}
end
def grouped_conversation_metrics
count_by_group = @conversations.open.group(@group_scope).count
unattended_by_group = @conversations.open.unattended.group(@group_scope).count
unassigned_by_group = @conversations.open.unassigned.group(@group_scope).count
group_metrics = count_by_group.map do |group_id, count|
metric = {
open: count,
unattended: unattended_by_group[group_id] || 0,
unassigned: unassigned_by_group[group_id] || 0
}
metric[@group_scope] = group_id
metric
end
render json: group_metrics
end
private
def set_group_scope
render json: { error: 'invalid group_by' }, status: :unprocessable_entity and return unless %w[
team_id
assignee_id
].include?(permitted_params[:group_by])
@group_scope = permitted_params[:group_by]
end
def team
return unless permitted_params[:team_id]
@team ||= Current.account.teams.find(permitted_params[:team_id])
end
def load_conversations
scope = Current.account.conversations
scope = scope.where(team_id: team.id) if team.present?
@conversations = scope
end
def permitted_params
params.permit(:team_id, :group_by)
end
end
@@ -1,6 +1,6 @@
class Api::V2::Accounts::SummaryReportsController < Api::V1::Accounts::BaseController
before_action :check_authorization
before_action :prepare_builder_params, only: [:agent, :team, :inbox]
before_action :prepare_builder_params, only: [:agent, :team]
def agent
render_report_with(V2::Reports::AgentSummaryBuilder)
@@ -10,10 +10,6 @@ class Api::V2::Accounts::SummaryReportsController < Api::V1::Accounts::BaseContr
render_report_with(V2::Reports::TeamSummaryBuilder)
end
def inbox
render_report_with(V2::Reports::InboxSummaryBuilder)
end
private
def check_authorization
@@ -94,8 +94,7 @@ module Api::V2::Accounts::HeatmapHelper
end
def since_timestamp(date)
number_of_days = params[:days_before].present? ? params[:days_before].to_i.days : 6.days
(date - number_of_days).to_i.to_s
(date - 6.days).to_i.to_s
end
def until_timestamp(date)
@@ -1,20 +0,0 @@
/* global axios */
import ApiClient from './ApiClient';
class LiveReportsAPI extends ApiClient {
constructor() {
super('live_reports', { accountScoped: true, apiVersion: 'v2' });
}
getConversationMetric(params = {}) {
return axios.get(`${this.url}/conversation_metrics`, { params });
}
getGroupedConversations({ groupBy } = { groupBy: 'assignee_id' }) {
return axios.get(`${this.url}/grouped_conversation_metrics`, {
params: { group_by: groupBy },
});
}
}
export default new LiveReportsAPI();
+2 -2
View File
@@ -61,9 +61,9 @@ class ReportsAPI extends ApiClient {
});
}
getConversationTrafficCSV({ daysBefore = 6 } = {}) {
getConversationTrafficCSV() {
return axios.get(`${this.url}/conversation_traffic`, {
params: { timezone_offset: getTimeOffset(), days_before: daysBefore },
params: { timezone_offset: getTimeOffset() },
});
}
@@ -1,40 +0,0 @@
/* global axios */
import ApiClient from './ApiClient';
class SummaryReportsAPI extends ApiClient {
constructor() {
super('summary_reports', { accountScoped: true, apiVersion: 'v2' });
}
getTeamReports({ since, until, businessHours } = {}) {
return axios.get(`${this.url}/team`, {
params: {
since,
until,
business_hours: businessHours,
},
});
}
getAgentReports({ since, until, businessHours } = {}) {
return axios.get(`${this.url}/agent`, {
params: {
since,
until,
business_hours: businessHours,
},
});
}
getInboxReports({ since, until, businessHours } = {}) {
return axios.get(`${this.url}/inbox`, {
params: {
since,
until,
business_hours: businessHours,
},
});
}
}
export default new SummaryReportsAPI();
@@ -3,6 +3,9 @@
@import 'tailwindcss/utilities';
@import 'shared/assets/fonts/plus-jakarta';
@import 'shared/assets/fonts/InterDisplay/inter-display';
@import 'shared/assets/fonts/inter';
@import 'shared/assets/stylesheets/animations';
@import 'shared/assets/stylesheets/colors';
@import 'shared/assets/stylesheets/spacing';
@@ -42,6 +45,7 @@
}
@layer base {
// scss-lint:disable PropertySortOrder
:root {
--color-amber-25: 254 253 251;
@@ -213,6 +217,7 @@
--color-orange-800: 204 78 0;
--color-orange-900: 88 45 29;
}
// scss-lint:disable QualifyingElement
body.dark {
--color-amber-25: 31 19 0;
@@ -1,6 +1,10 @@
table {
@apply border-spacing-0 text-sm w-full;
}
.woot-table {
thead {
th {
@apply font-semibold tracking-[1px] text-left px-2.5 uppercase text-slate-900 dark:text-slate-200;
@@ -16,9 +20,7 @@ table {
@apply p-2.5 text-slate-700 dark:text-slate-100;
}
}
}
.woot-table {
tr {
.show-if-hover {
transition: opacity 0.2s $swift-ease-out-function;
@@ -116,13 +116,14 @@
<script>
import { mapGetters } from 'vuex';
import { useUISettings } from 'dashboard/composables/useUISettings';
import { useAlert } from 'dashboard/composables';
import VirtualList from 'vue-virtual-scroll-list';
import ChatListHeader from './ChatListHeader.vue';
import ConversationAdvancedFilter from './widgets/conversation/ConversationAdvancedFilter.vue';
import ChatTypeTabs from './widgets/ChatTypeTabs.vue';
import ConversationItem from './ConversationItem.vue';
import timeMixin from '../mixins/time';
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
import conversationMixin from '../mixins/conversations';
import wootConstants from 'dashboard/constants/globals';
@@ -131,9 +132,7 @@ import filterQueryGenerator from '../helper/filterQueryGenerator.js';
import AddCustomViews from 'dashboard/routes/dashboard/customviews/AddCustomViews.vue';
import DeleteCustomViews from 'dashboard/routes/dashboard/customviews/DeleteCustomViews.vue';
import ConversationBulkActions from './widgets/conversation/conversationBulkActions/Index.vue';
import alertMixin from 'shared/mixins/alertMixin';
import filterMixin from 'shared/mixins/filterMixin';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import languages from 'dashboard/components/widgets/conversation/advancedFilterItems/languages';
import countries from 'shared/constants/countries';
import { generateValuesForEditCustomViews } from 'dashboard/helper/customViewsHelper';
@@ -158,14 +157,7 @@ export default {
IntersectionObserver,
VirtualList,
},
mixins: [
timeMixin,
conversationMixin,
keyboardEventListenerMixins,
alertMixin,
filterMixin,
uiSettingsMixin,
],
mixins: [conversationMixin, keyboardEventListenerMixins, filterMixin],
provide() {
return {
// Actions to be performed on virtual list item and context menu.
@@ -210,6 +202,13 @@ export default {
type: Boolean,
},
},
setup() {
const { uiSettings } = useUISettings();
return {
uiSettings,
};
},
data() {
return {
activeAssigneeTab: wootConstants.ASSIGNEE_TYPE.ME,
@@ -812,7 +811,7 @@ export default {
});
this.$store.dispatch('bulkActions/clearSelectedConversationIds');
if (conversationId) {
this.showAlert(
useAlert(
this.$t(
'CONVERSATION.CARD_CONTEXT_MENU.API.AGENT_ASSIGNMENT.SUCCESFUL',
{
@@ -822,10 +821,10 @@ export default {
)
);
} else {
this.showAlert(this.$t('BULK_ACTION.ASSIGN_SUCCESFUL'));
useAlert(this.$t('BULK_ACTION.ASSIGN_SUCCESFUL'));
}
} catch (err) {
this.showAlert(this.$t('BULK_ACTION.ASSIGN_FAILED'));
useAlert(this.$t('BULK_ACTION.ASSIGN_FAILED'));
}
},
async assignPriority(priority, conversationId = null) {
@@ -840,7 +839,7 @@ export default {
newValue: priority,
from: 'Context menu',
});
this.showAlert(
useAlert(
this.$t('CONVERSATION.PRIORITY.CHANGE_PRIORITY.SUCCESSFUL', {
priority,
conversationId,
@@ -883,7 +882,7 @@ export default {
conversationId,
teamId: team.id,
});
this.showAlert(
useAlert(
this.$t(
'CONVERSATION.CARD_CONTEXT_MENU.API.TEAM_ASSIGNMENT.SUCCESFUL',
{
@@ -893,7 +892,7 @@ export default {
)
);
} catch (error) {
this.showAlert(
useAlert(
this.$t('CONVERSATION.CARD_CONTEXT_MENU.API.TEAM_ASSIGNMENT.FAILED')
);
}
@@ -910,7 +909,7 @@ export default {
});
this.$store.dispatch('bulkActions/clearSelectedConversationIds');
if (conversationId) {
this.showAlert(
useAlert(
this.$t(
'CONVERSATION.CARD_CONTEXT_MENU.API.LABEL_ASSIGNMENT.SUCCESFUL',
{
@@ -920,10 +919,10 @@ export default {
)
);
} else {
this.showAlert(this.$t('BULK_ACTION.LABELS.ASSIGN_SUCCESFUL'));
useAlert(this.$t('BULK_ACTION.LABELS.ASSIGN_SUCCESFUL'));
}
} catch (err) {
this.showAlert(this.$t('BULK_ACTION.LABELS.ASSIGN_FAILED'));
useAlert(this.$t('BULK_ACTION.LABELS.ASSIGN_FAILED'));
}
},
async onAssignTeamsForBulk(team) {
@@ -936,9 +935,9 @@ export default {
},
});
this.$store.dispatch('bulkActions/clearSelectedConversationIds');
this.showAlert(this.$t('BULK_ACTION.TEAMS.ASSIGN_SUCCESFUL'));
useAlert(this.$t('BULK_ACTION.TEAMS.ASSIGN_SUCCESFUL'));
} catch (err) {
this.showAlert(this.$t('BULK_ACTION.TEAMS.ASSIGN_FAILED'));
useAlert(this.$t('BULK_ACTION.TEAMS.ASSIGN_FAILED'));
}
},
async onUpdateConversations(status, snoozedUntil) {
@@ -952,9 +951,9 @@ export default {
snoozed_until: snoozedUntil,
});
this.$store.dispatch('bulkActions/clearSelectedConversationIds');
this.showAlert(this.$t('BULK_ACTION.UPDATE.UPDATE_SUCCESFUL'));
useAlert(this.$t('BULK_ACTION.UPDATE.UPDATE_SUCCESFUL'));
} catch (err) {
this.showAlert(this.$t('BULK_ACTION.UPDATE.UPDATE_FAILED'));
useAlert(this.$t('BULK_ACTION.UPDATE.UPDATE_FAILED'));
}
},
toggleConversationStatus(conversationId, status, snoozedUntil) {
@@ -965,7 +964,7 @@ export default {
snoozedUntil,
})
.then(() => {
this.showAlert(this.$t('CONVERSATION.CHANGE_STATUS'));
useAlert(this.$t('CONVERSATION.CHANGE_STATUS'));
this.isLoading = false;
});
},
+2 -3
View File
@@ -25,10 +25,9 @@
<script>
import 'highlight.js/styles/default.css';
import { copyTextToClipboard } from 'shared/helpers/clipboard';
import alertMixin from 'shared/mixins/alertMixin';
import { useAlert } from 'dashboard/composables';
export default {
mixins: [alertMixin],
props: {
script: {
type: String,
@@ -61,7 +60,7 @@ export default {
async onCopy(e) {
e.preventDefault();
await copyTextToClipboard(this.script);
this.showAlert(this.$t('COMPONENTS.CODE.COPY_SUCCESSFUL'));
useAlert(this.$t('COMPONENTS.CODE.COPY_SUCCESSFUL'));
},
},
};
@@ -0,0 +1,25 @@
<script setup>
import { useStoreGetters } from 'dashboard/composables/store';
import { computed } from 'vue';
const props = defineProps({
showOnCustomBrandedInstance: {
type: Boolean,
default: true,
},
});
const getters = useStoreGetters();
const isACustomBrandedInstance =
getters['globalConfig/isACustomBrandedInstance'];
const shouldShowContent = computed(
() => props.showOnCustomBrandedInstance || !isACustomBrandedInstance.value
);
</script>
<template>
<div v-if="shouldShowContent">
<slot />
</div>
</template>
@@ -18,10 +18,9 @@
<script>
import 'highlight.js/styles/default.css';
import { copyTextToClipboard } from 'shared/helpers/clipboard';
import alertMixin from 'shared/mixins/alertMixin';
import { useAlert } from 'dashboard/composables';
export default {
mixins: [alertMixin],
props: {
value: {
type: String,
@@ -37,7 +36,7 @@ export default {
async onCopy(e) {
e.preventDefault();
await copyTextToClipboard(this.value);
this.showAlert(this.$t('COMPONENTS.CODE.COPY_SUCCESSFUL'));
useAlert(this.$t('COMPONENTS.CODE.COPY_SUCCESSFUL'));
},
toggleMasked() {
this.masked = !this.masked;
@@ -15,13 +15,11 @@
<script>
import WootSnackbar from './Snackbar.vue';
import alertMixin from 'shared/mixins/alertMixin';
export default {
components: {
WootSnackbar,
},
mixins: [alertMixin],
props: {
duration: {
type: Number,
@@ -42,7 +40,7 @@ export default {
this.$emitter.off('newToastMessage', this.onNewToastMessage);
},
methods: {
onNewToastMessage(message, action) {
onNewToastMessage({ message, action }) {
this.snackMessages.push({
key: new Date().getTime(),
message,
@@ -10,9 +10,9 @@
</template>
<script>
import Banner from 'dashboard/components/ui/Banner.vue';
import { mapGetters } from 'vuex';
import adminMixin from 'dashboard/mixins/isAdmin';
import { useAdmin } from 'dashboard/composables/useAdmin';
import Banner from 'dashboard/components/ui/Banner.vue';
import accountMixin from 'dashboard/mixins/account';
const EMPTY_SUBSCRIPTION_INFO = {
@@ -22,7 +22,13 @@ const EMPTY_SUBSCRIPTION_INFO = {
export default {
components: { Banner },
mixins: [adminMixin, accountMixin],
mixins: [accountMixin],
setup() {
const { isAdmin } = useAdmin();
return {
isAdmin,
};
},
computed: {
...mapGetters({
isOnChatwootCloud: 'globalConfig/isOnChatwootCloud',
@@ -14,11 +14,11 @@
import Banner from 'dashboard/components/ui/Banner.vue';
import { mapGetters } from 'vuex';
import accountMixin from 'dashboard/mixins/account';
import alertMixin from 'shared/mixins/alertMixin';
import { useAlert } from 'dashboard/composables';
export default {
components: { Banner },
mixins: [accountMixin, alertMixin],
mixins: [accountMixin],
computed: {
...mapGetters({
currentUser: 'getCurrentUser',
@@ -36,7 +36,7 @@ export default {
methods: {
resendVerificationEmail() {
this.$store.dispatch('resendConfirmation');
this.showAlert(this.$t('APP_GLOBAL.EMAIL_VERIFICATION_SENT'));
useAlert(this.$t('APP_GLOBAL.EMAIL_VERIFICATION_SENT'));
},
},
};
@@ -14,15 +14,20 @@ import Banner from 'dashboard/components/ui/Banner.vue';
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
import { LocalStorage } from 'shared/helpers/localStorage';
import { mapGetters } from 'vuex';
import adminMixin from 'dashboard/mixins/isAdmin';
import { useAdmin } from 'dashboard/composables/useAdmin';
import { hasAnUpdateAvailable } from './versionCheckHelper';
export default {
components: { Banner },
mixins: [adminMixin],
props: {
latestChatwootVersion: { type: String, default: '' },
},
setup() {
const { isAdmin } = useAdmin();
return {
isAdmin,
};
},
data() {
return { userDismissedBanner: false };
},
@@ -12,13 +12,12 @@
<script>
import Banner from 'dashboard/components/ui/Banner.vue';
import { mapGetters } from 'vuex';
import adminMixin from 'dashboard/mixins/isAdmin';
import accountMixin from 'dashboard/mixins/account';
import { differenceInDays } from 'date-fns';
export default {
components: { Banner },
mixins: [adminMixin, accountMixin],
mixins: [accountMixin],
data() {
return { conversationMeta: {} };
},
@@ -78,7 +78,7 @@
<script>
import { mapGetters } from 'vuex';
import alertMixin from 'shared/mixins/alertMixin';
import { useAlert } from 'dashboard/composables';
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
@@ -94,7 +94,7 @@ export default {
WootDropdownItem,
WootDropdownMenu,
},
mixins: [alertMixin, keyboardEventListenerMixins],
mixins: [keyboardEventListenerMixins],
props: { conversationId: { type: [String, Number], required: true } },
data() {
return {
@@ -209,7 +209,7 @@ export default {
snoozedUntil,
})
.then(() => {
this.showAlert(this.$t('CONVERSATION.CHANGE_STATUS'));
useAlert(this.$t('CONVERSATION.CHANGE_STATUS'));
this.isLoading = false;
});
},
@@ -18,7 +18,7 @@
</woot-button>
</woot-dropdown-item>
<woot-dropdown-divider />
<woot-dropdown-item class="m-0 flex items-center justify-between p-2">
<woot-dropdown-item class="flex items-center justify-between p-2 m-0">
<div class="flex items-center">
<fluent-icon
v-tooltip.right-start="$t('SIDEBAR.SET_AUTO_OFFLINE.INFO_TEXT')"
@@ -28,7 +28,7 @@
/>
<span
class="my-0 mx-1 text-xs font-medium text-slate-600 dark:text-slate-100"
class="mx-1 my-0 text-xs font-medium text-slate-600 dark:text-slate-100"
>
{{ $t('SIDEBAR.SET_AUTO_OFFLINE.TEXT') }}
</span>
@@ -36,7 +36,7 @@
<woot-switch
size="small"
class="mt-px mx-1 mb-0"
class="mx-1 mt-px mb-0"
:value="currentUserAutoOffline"
@input="updateAutoOffline"
/>
@@ -47,7 +47,7 @@
<script>
import { mapGetters } from 'vuex';
import alertMixin from 'shared/mixins/alertMixin';
import { useAlert } from 'dashboard/composables';
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
import WootDropdownHeader from 'shared/components/ui/dropdown/DropdownHeader.vue';
@@ -65,9 +65,6 @@ export default {
WootDropdownItem,
AvailabilityStatusBadge,
},
mixins: [alertMixin],
data() {
return {
isStatusMenuOpened: false,
@@ -129,7 +126,7 @@ export default {
account_id: this.currentAccountId,
});
} catch (error) {
this.showAlert(
useAlert(
this.$t('PROFILE_SETTINGS.FORM.AVAILABILITY.SET_AVAILABILITY_ERROR')
);
} finally {
@@ -30,9 +30,7 @@
<script>
import { mapGetters } from 'vuex';
import adminMixin from '../../mixins/isAdmin';
import { getSidebarItems } from './config/default-sidebar';
import alertMixin from 'shared/mixins/alertMixin';
import PrimarySidebar from './sidebarComponents/Primary.vue';
import SecondarySidebar from './sidebarComponents/Secondary.vue';
@@ -45,7 +43,7 @@ export default {
PrimarySidebar,
SecondarySidebar,
},
mixins: [adminMixin, alertMixin, keyboardEventListenerMixins],
mixins: [keyboardEventListenerMixins],
props: {
showSecondarySidebar: {
type: Boolean,
@@ -163,17 +163,6 @@ const settings = accountId => ({
permissions: ['administrator'],
},
toState: frontendURL(`accounts/${accountId}/settings/integrations`),
toStateName: 'settings_integrations',
featureFlag: FEATURE_FLAGS.INTEGRATIONS,
},
{
icon: 'star-emphasis',
label: 'APPLICATIONS',
hasSubMenu: false,
meta: {
permissions: ['administrator'],
},
toState: frontendURL(`accounts/${accountId}/settings/applications`),
toStateName: 'settings_applications',
featureFlag: FEATURE_FLAGS.INTEGRATIONS,
},
@@ -51,10 +51,9 @@
<script>
import { required, minLength } from 'vuelidate/lib/validators';
import { mapGetters } from 'vuex';
import alertMixin from 'shared/mixins/alertMixin';
import { useAlert } from 'dashboard/composables';
export default {
mixins: [alertMixin],
props: {
show: {
type: Boolean,
@@ -88,13 +87,13 @@ export default {
account_name: this.accountName,
});
this.$emit('close-account-create-modal');
this.showAlert(this.$t('CREATE_ACCOUNT.API.SUCCESS_MESSAGE'));
useAlert(this.$t('CREATE_ACCOUNT.API.SUCCESS_MESSAGE'));
window.location = `/app/accounts/${account_id}/dashboard`;
} catch (error) {
if (error.response.status === 422) {
this.showAlert(this.$t('CREATE_ACCOUNT.API.EXIST_MESSAGE'));
useAlert(this.$t('CREATE_ACCOUNT.API.EXIST_MESSAGE'));
} else {
this.showAlert(this.$t('CREATE_ACCOUNT.API.ERROR_MESSAGE'));
useAlert(this.$t('CREATE_ACCOUNT.API.ERROR_MESSAGE'));
}
}
},
@@ -94,8 +94,7 @@
<script>
import { mapGetters } from 'vuex';
import adminMixin from '../../../mixins/isAdmin';
import { useAdmin } from 'dashboard/composables/useAdmin';
import configMixin from 'shared/mixins/configMixin';
import {
getInboxClassByType,
@@ -111,13 +110,19 @@ import Policy from '../../policy.vue';
export default {
components: { SecondaryChildNavItem, Policy },
mixins: [adminMixin, configMixin],
mixins: [configMixin],
props: {
menuItem: {
type: Object,
default: () => ({}),
},
},
setup() {
const { isAdmin } = useAdmin();
return {
isAdmin,
};
},
computed: {
...mapGetters({
activeInbox: 'getSelectedInbox',
@@ -5,7 +5,7 @@
delay: { show: 1500, hide: 0 },
hideOnClick: true,
}"
class="text-xxs text-slate-500 dark:text-slate-500 leading-4 ml-auto hover:text-slate-900 dark:hover:text-slate-100"
class="ml-auto leading-4 text-xxs text-slate-500 dark:text-slate-500 hover:text-slate-900 dark:hover:text-slate-100"
>
<span>{{ `${createdAtTime} • ${lastActivityTime}` }}</span>
</div>
@@ -16,11 +16,14 @@ const MINUTE_IN_MILLI_SECONDS = 60000;
const HOUR_IN_MILLI_SECONDS = MINUTE_IN_MILLI_SECONDS * 60;
const DAY_IN_MILLI_SECONDS = HOUR_IN_MILLI_SECONDS * 24;
import timeMixin from 'dashboard/mixins/time';
import {
dynamicTime,
dateFormat,
shortTimestamp,
} from 'shared/helpers/timeHelper';
export default {
name: 'TimeAgo',
mixins: [timeMixin],
props: {
isAutoRefreshEnabled: {
type: Boolean,
@@ -37,17 +40,17 @@ export default {
},
data() {
return {
lastActivityAtTimeAgo: this.dynamicTime(this.lastActivityTimestamp),
createdAtTimeAgo: this.dynamicTime(this.createdAtTimestamp),
lastActivityAtTimeAgo: dynamicTime(this.lastActivityTimestamp),
createdAtTimeAgo: dynamicTime(this.createdAtTimestamp),
timer: null,
};
},
computed: {
lastActivityTime() {
return this.shortTimestamp(this.lastActivityAtTimeAgo);
return shortTimestamp(this.lastActivityAtTimeAgo);
},
createdAtTime() {
return this.shortTimestamp(this.createdAtTimeAgo);
return shortTimestamp(this.createdAtTimeAgo);
},
createdAt() {
const createdTimeDiff = Date.now() - this.createdAtTimestamp * 1000;
@@ -56,9 +59,9 @@ export default {
? `${this.$t('CHAT_LIST.CHAT_TIME_STAMP.CREATED.LATEST')} ${
this.createdAtTimeAgo
}`
: `${this.$t(
'CHAT_LIST.CHAT_TIME_STAMP.CREATED.OLDEST'
)} ${this.dateFormat(this.createdAtTimestamp)}`;
: `${this.$t('CHAT_LIST.CHAT_TIME_STAMP.CREATED.OLDEST')} ${dateFormat(
this.createdAtTimestamp
)}`;
},
lastActivity() {
const lastActivityTimeDiff =
@@ -70,7 +73,7 @@ export default {
}`
: `${this.$t(
'CHAT_LIST.CHAT_TIME_STAMP.LAST_ACTIVITY.NOT_ACTIVE'
)} ${this.dateFormat(this.lastActivityTimestamp)}`;
)} ${dateFormat(this.lastActivityTimestamp)}`;
},
tooltipText() {
return `${this.createdAt}
@@ -79,10 +82,10 @@ export default {
},
watch: {
lastActivityTimestamp() {
this.lastActivityAtTimeAgo = this.dynamicTime(this.lastActivityTimestamp);
this.lastActivityAtTimeAgo = dynamicTime(this.lastActivityTimestamp);
},
createdAtTimestamp() {
this.createdAtTimeAgo = this.dynamicTime(this.createdAtTimestamp);
this.createdAtTimeAgo = dynamicTime(this.createdAtTimestamp);
},
},
mounted() {
@@ -96,10 +99,8 @@ export default {
methods: {
createTimer() {
this.timer = setTimeout(() => {
this.lastActivityAtTimeAgo = this.dynamicTime(
this.lastActivityTimestamp
);
this.createdAtTimeAgo = this.dynamicTime(this.createdAtTimestamp);
this.lastActivityAtTimeAgo = dynamicTime(this.lastActivityTimestamp);
this.createdAtTimeAgo = dynamicTime(this.createdAtTimestamp);
this.createTimer();
}, this.refreshTime());
},
@@ -35,13 +35,13 @@
</template>
<script>
import { mapGetters } from 'vuex';
import { useAdmin } from 'dashboard/composables/useAdmin';
import { useUISettings } from 'dashboard/composables/useUISettings';
import AICTAModal from './AICTAModal.vue';
import AIAssistanceModal from './AIAssistanceModal.vue';
import adminMixin from 'dashboard/mixins/aiMixin';
import aiMixin from 'dashboard/mixins/isAdmin';
import aiMixin from 'dashboard/mixins/aiMixin';
import { CMD_AI_ASSIST } from 'dashboard/routes/dashboard/commands/commandBarBusEvents';
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import AIAssistanceCTAButton from './AIAssistanceCTAButton.vue';
export default {
@@ -50,7 +50,17 @@ export default {
AICTAModal,
AIAssistanceCTAButton,
},
mixins: [aiMixin, keyboardEventListenerMixins, adminMixin, uiSettingsMixin],
mixins: [aiMixin, keyboardEventListenerMixins],
setup() {
const { uiSettings, updateUISettings } = useUISettings();
const { isAdmin } = useAdmin();
return {
uiSettings,
updateUISettings,
isAdmin,
};
},
data: () => ({
showAIAssistanceModal: false,
showAICtaModal: false,
@@ -1,14 +1,14 @@
<template>
<div class="px-0 min-w-0 flex-1">
<div class="flex-1 min-w-0 px-0">
<woot-modal-header
:header-title="$t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.TITLE')"
:header-content="$t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.DESC')"
/>
<form
class="flex flex-wrap flex-col modal-content"
class="flex flex-col flex-wrap modal-content"
@submit.prevent="finishOpenAI"
>
<div class="mt-2 w-full">
<div class="w-full mt-2">
<woot-input
v-model="value"
type="text"
@@ -19,7 +19,7 @@
@blur="$v.value.$touch"
/>
</div>
<div class="flex flex-row justify-between gap-2 py-2 px-0 w-full">
<div class="flex flex-row justify-between w-full gap-2 px-0 py-2">
<woot-button variant="link" @click.prevent="openOpenAIDoc">
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.BUTTONS.NEED_HELP') }}
</woot-button>
@@ -39,13 +39,20 @@
<script>
import { required } from 'vuelidate/lib/validators';
import { mapGetters } from 'vuex';
import { useAlert } from 'dashboard/composables';
import { useUISettings } from 'dashboard/composables/useUISettings';
import aiMixin from 'dashboard/mixins/aiMixin';
import alertMixin from 'shared/mixins/alertMixin';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import { OPEN_AI_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
export default {
mixins: [aiMixin, alertMixin, uiSettingsMixin],
mixins: [aiMixin],
setup() {
const { updateUISettings } = useUISettings();
return {
updateUISettings,
};
},
data() {
return {
value: '',
@@ -67,7 +74,7 @@ export default {
},
onDismiss() {
this.showAlert(
useAlert(
this.$t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.DISMISS_MESSAGE')
);
this.updateUISettings({
@@ -97,7 +104,7 @@ export default {
this.alertMessage =
errorMessage || this.$t('INTEGRATION_APPS.ADD.API.ERROR_MESSAGE');
} finally {
this.showAlert(this.alertMessage);
useAlert(this.alertMessage);
}
},
openOpenAIDoc() {
@@ -26,13 +26,12 @@
</template>
<script>
import { useAlert } from 'dashboard/composables';
import Spinner from 'shared/components/Spinner.vue';
import alertMixin from 'shared/mixins/alertMixin';
export default {
components: {
Spinner,
},
mixins: [alertMixin],
props: {
value: {
type: Array,
@@ -71,7 +70,7 @@ export default {
} catch (error) {
this.uploadState = 'failed';
this.label = this.$t('AUTOMATION.ATTACHMENT.LABEL_UPLOAD_FAILED');
this.showAlert(this.$t('AUTOMATION.ATTACHMENT.UPLOAD_ERROR'));
useAlert(this.$t('AUTOMATION.ATTACHMENT.UPLOAD_ERROR'));
}
},
},
@@ -1,34 +1,40 @@
<script setup>
import router from '../../routes/index';
const props = defineProps({
backUrl: {
type: [String, Object],
default: '',
},
buttonLabel: {
type: String,
default: '',
},
compact: {
type: Boolean,
default: false,
},
});
const goBack = () => {
if (props.backUrl !== '') {
router.push(props.backUrl);
} else {
router.go(-1);
}
};
const buttonStyleClass = props.compact
? 'text-sm text-slate-600 dark:text-slate-300'
: 'text-base text-woot-500 dark:text-woot-500';
</script>
<template>
<button
class="header-section flex items-center text-base font-normal mr-4 ml-2 p-0 cursor-pointer text-woot-500 dark:text-woot-500"
class="flex items-center font-normal p-0 cursor-pointer"
:class="buttonStyleClass"
@click.capture="goBack"
>
<fluent-icon icon="chevron-left" />
<fluent-icon icon="chevron-left" class="-ml-1" />
{{ buttonLabel || $t('GENERAL_SETTINGS.BACK') }}
</button>
</template>
<script>
import router from '../../routes/index';
export default {
props: {
backUrl: {
type: [String, Object],
default: '',
},
buttonLabel: {
type: String,
default: '',
},
},
methods: {
goBack() {
if (this.backUrl !== '') {
router.push(this.backUrl);
} else {
router.go(-1);
}
},
},
};
</script>
@@ -33,7 +33,7 @@
import AddLabel from 'shared/components/ui/dropdown/AddLabel.vue';
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
import LabelDropdown from 'shared/components/ui/label/LabelDropdown.vue';
import adminMixin from 'dashboard/mixins/isAdmin';
import { useAdmin } from 'dashboard/composables/useAdmin';
export default {
components: {
@@ -41,7 +41,7 @@ export default {
LabelDropdown,
},
mixins: [adminMixin, keyboardEventListenerMixins],
mixins: [keyboardEventListenerMixins],
props: {
allLabels: {
@@ -54,6 +54,13 @@ export default {
},
},
setup() {
const { isAdmin } = useAdmin();
return {
isAdmin,
};
},
data() {
return {
showSearchDropdownLabel: false,
@@ -15,10 +15,9 @@
<script>
import { mapGetters } from 'vuex';
import DyteAPI from 'dashboard/api/integrations/dyte';
import alertMixin from 'shared/mixins/alertMixin';
import { useAlert } from 'dashboard/composables';
export default {
mixins: [alertMixin],
props: {
conversationId: {
type: Number,
@@ -47,7 +46,7 @@ export default {
try {
await DyteAPI.createAMeeting(this.conversationId);
} catch (error) {
this.showAlert(this.$t('INTEGRATION_SETTINGS.DYTE.CREATE_ERROR'));
useAlert(this.$t('INTEGRATION_SETTINGS.DYTE.CREATE_ERROR'));
} finally {
this.isLoading = false;
}
@@ -11,7 +11,7 @@ import 'videojs-record/dist/css/videojs.record.css';
import videojs from 'video.js';
import alertMixin from '../../../../shared/mixins/alertMixin';
import { useAlert } from 'dashboard/composables';
import Recorder from 'opus-recorder';
@@ -50,7 +50,6 @@ const RECORDER_CONFIG = {
export default {
name: 'WootAudioRecorder',
mixins: [alertMixin],
props: {
audioRecordFormat: {
type: String,
@@ -188,14 +187,10 @@ export default {
deviceErrorName?.includes('notallowederror') ||
deviceErrorName?.includes('permissiondeniederror')
) {
this.showAlert(
this.$t('CONVERSATION.REPLYBOX.TIP_AUDIORECORDER_PERMISSION')
);
useAlert(this.$t('CONVERSATION.REPLYBOX.TIP_AUDIORECORDER_PERMISSION'));
this.fireStateRecorderChanged('notallowederror');
} else {
this.showAlert(
this.$t('CONVERSATION.REPLYBOX.TIP_AUDIORECORDER_ERROR')
);
useAlert(this.$t('CONVERSATION.REPLYBOX.TIP_AUDIORECORDER_ERROR'));
}
},
formatTimeProgress() {
@@ -80,8 +80,7 @@ import {
hasPressedCommandAndEnter,
} from 'shared/helpers/KeyboardHelpers';
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import { isEditorHotKeyEnabled } from 'dashboard/mixins/uiSettings';
import { useUISettings } from 'dashboard/composables/useUISettings';
import {
replaceVariablesInMessage,
createTypingIndicator,
@@ -89,7 +88,7 @@ import {
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
import { uploadFile } from 'dashboard/helper/uploadHelper';
import alertMixin from 'shared/mixins/alertMixin';
import { useAlert } from 'dashboard/composables';
import {
MESSAGE_EDITOR_MENU_OPTIONS,
MESSAGE_EDITOR_IMAGE_RESIZES,
@@ -119,7 +118,7 @@ const createState = (
export default {
name: 'WootMessageEditor',
components: { TagAgents, CannedResponse, VariableList },
mixins: [keyboardEventListenerMixins, uiSettingsMixin, alertMixin],
mixins: [keyboardEventListenerMixins],
props: {
value: { type: String, default: '' },
editorId: { type: String, default: '' },
@@ -139,6 +138,19 @@ export default {
channelType: { type: String, default: '' },
showImageResizeToolbar: { type: Boolean, default: false }, // A kill switch to show or hide the image toolbar
},
setup() {
const {
uiSettings,
isEditorHotKeyEnabled,
fetchSignatureFlagFromUISettings,
} = useUISettings();
return {
uiSettings,
isEditorHotKeyEnabled,
fetchSignatureFlagFromUISettings,
};
},
data() {
return {
typingIndicator: createTypingIndicator(
@@ -278,7 +290,7 @@ export default {
// this is considered the source of truth, we watch this property
// on change, we toggle the signature in the editor
if (this.allowSignature && !this.isPrivate && this.channelType) {
return this.fetchSignatureFlagFromUiSettings(this.channelType);
return this.fetchSignatureFlagFromUISettings(this.channelType);
}
return false;
@@ -521,10 +533,10 @@ export default {
}
},
isEnterToSendEnabled() {
return isEditorHotKeyEnabled(this.uiSettings, 'enter');
return this.isEditorHotKeyEnabled('enter');
},
isCmdPlusEnterToSendEnabled() {
return isEditorHotKeyEnabled(this.uiSettings, 'cmd_enter');
return this.isEditorHotKeyEnabled('cmd_enter');
},
getKeyboardEvents() {
return {
@@ -611,7 +623,7 @@ export default {
if (checkFileSizeLimit(file, MAXIMUM_FILE_UPLOAD_SIZE)) {
this.uploadImageToStorage(file);
} else {
this.showAlert(
useAlert(
this.$t(
'PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.IMAGE_UPLOAD_SIZE_ERROR',
{
@@ -629,13 +641,13 @@ export default {
if (fileUrl) {
this.onImageInsertInEditor(fileUrl);
}
this.showAlert(
useAlert(
this.$t(
'PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.IMAGE_UPLOAD_SUCCESS'
)
);
} catch (error) {
this.showAlert(
useAlert(
this.$t(
'PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.IMAGE_UPLOAD_ERROR'
)
@@ -24,9 +24,9 @@ import {
Selection,
} from '@chatwoot/prosemirror-schema';
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
import alertMixin from 'shared/mixins/alertMixin';
import { useAlert } from 'dashboard/composables';
import { useUISettings } from 'dashboard/composables/useUISettings';
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
const MAXIMUM_FILE_UPLOAD_SIZE = 4; // in MB
const createState = (
@@ -51,13 +51,21 @@ const createState = (
};
export default {
mixins: [keyboardEventListenerMixins, uiSettingsMixin, alertMixin],
mixins: [keyboardEventListenerMixins],
props: {
value: { type: String, default: '' },
editorId: { type: String, default: '' },
placeholder: { type: String, default: '' },
enabledMenuOptions: { type: Array, default: () => [] },
},
setup() {
const { uiSettings, updateUISettings } = useUISettings();
return {
uiSettings,
updateUISettings,
};
},
data() {
return {
editorView: null,
@@ -108,7 +116,7 @@ export default {
if (checkFileSizeLimit(file, MAXIMUM_FILE_UPLOAD_SIZE)) {
this.uploadImageToStorage(file);
} else {
this.showAlert(
useAlert(
this.$t('HELP_CENTER.ARTICLE_EDITOR.IMAGE_UPLOAD.ERROR_FILE_SIZE', {
size: MAXIMUM_FILE_UPLOAD_SIZE,
})
@@ -127,13 +135,9 @@ export default {
if (fileUrl) {
this.onImageUploadStart(fileUrl);
}
this.showAlert(
this.$t('HELP_CENTER.ARTICLE_EDITOR.IMAGE_UPLOAD.SUCCESS')
);
useAlert(this.$t('HELP_CENTER.ARTICLE_EDITOR.IMAGE_UPLOAD.SUCCESS'));
} catch (error) {
this.showAlert(
this.$t('HELP_CENTER.ARTICLE_EDITOR.IMAGE_UPLOAD.ERROR')
);
useAlert(this.$t('HELP_CENTER.ARTICLE_EDITOR.IMAGE_UPLOAD.ERROR'));
}
},
onImageUploadStart(fileUrl) {
@@ -134,10 +134,10 @@
</template>
<script>
import { useUISettings } from 'dashboard/composables/useUISettings';
import FileUpload from 'vue-upload-component';
import * as ActiveStorage from 'activestorage';
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import inboxMixin from 'shared/mixins/inboxMixin';
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
import {
@@ -153,7 +153,7 @@ import { mapGetters } from 'vuex';
export default {
name: 'ReplyBottomPanel',
components: { FileUpload, VideoCallButton, AIAssistanceButton },
mixins: [keyboardEventListenerMixins, uiSettingsMixin, inboxMixin],
mixins: [keyboardEventListenerMixins, inboxMixin],
props: {
mode: {
type: String,
@@ -248,6 +248,15 @@ export default {
required: true,
},
},
setup() {
const { setSignatureFlagForInbox, fetchSignatureFlagFromUISettings } =
useUISettings();
return {
setSignatureFlagForInbox,
fetchSignatureFlagFromUISettings,
};
},
computed: {
...mapGetters({
accountId: 'getCurrentAccountId',
@@ -320,7 +329,7 @@ export default {
},
sendWithSignature() {
// channelType is sourced from inboxMixin
return this.fetchSignatureFlagFromUiSettings(this.channelType);
return this.fetchSignatureFlagFromUISettings(this.channelType);
},
signatureToggleTooltip() {
return this.sendWithSignature
@@ -79,7 +79,7 @@
</template>
<script>
import alertMixin from 'shared/mixins/alertMixin';
import { useAlert } from 'dashboard/composables';
import { required, requiredIf } from 'vuelidate/lib/validators';
import FilterInputBox from '../FilterInput/Index.vue';
import languages from './advancedFilterItems/languages';
@@ -94,7 +94,7 @@ export default {
components: {
FilterInputBox,
},
mixins: [alertMixin, filterMixin],
mixins: [filterMixin],
props: {
onClose: {
type: Function,
@@ -341,7 +341,7 @@ export default {
},
removeFilter(index) {
if (this.appliedFilters.length <= 1) {
this.showAlert(this.$t('FILTER.FILTER_DELETE_ERROR'));
useAlert(this.$t('FILTER.FILTER_DELETE_ERROR'));
} else {
this.appliedFilters.splice(index, 1);
}
@@ -12,10 +12,10 @@
<div
v-if="showActionsDropdown"
v-on-clickaway="closeDropdown"
class="dropdown-pane dropdown-pane--open mt-1 right-0 basic-filter"
class="right-0 mt-1 dropdown-pane dropdown-pane--open basic-filter"
>
<div class="items-center flex justify-between last:mt-4">
<span class="text-slate-800 dark:text-slate-100 text-xs font-medium">{{
<div class="flex items-center justify-between last:mt-4">
<span class="text-xs font-medium text-slate-800 dark:text-slate-100">{{
$t('CHAT_LIST.CHAT_SORT.STATUS')
}}</span>
<filter-item
@@ -26,8 +26,8 @@
@onChangeFilter="onChangeFilter"
/>
</div>
<div class="items-center flex justify-between last:mt-4">
<span class="text-slate-800 dark:text-slate-100 text-xs font-medium">{{
<div class="flex items-center justify-between last:mt-4">
<span class="text-xs font-medium text-slate-800 dark:text-slate-100">{{
$t('CHAT_LIST.CHAT_SORT.ORDER_BY')
}}</span>
<filter-item
@@ -46,13 +46,19 @@
import wootConstants from 'dashboard/constants/globals';
import { mapGetters } from 'vuex';
import FilterItem from './FilterItem.vue';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import { useUISettings } from 'dashboard/composables/useUISettings';
export default {
components: {
FilterItem,
},
mixins: [uiSettingsMixin],
setup() {
const { updateUISettings } = useUISettings();
return {
updateUISettings,
};
},
data() {
return {
showActionsDropdown: false,
@@ -120,13 +120,11 @@ import { mapGetters } from 'vuex';
import Thumbnail from '../Thumbnail.vue';
import MessagePreview from './MessagePreview.vue';
import conversationMixin from '../../../mixins/conversations';
import timeMixin from '../../../mixins/time';
import router from '../../../routes';
import { frontendURL, conversationUrl } from '../../../helper/URLHelper';
import InboxName from '../InboxName.vue';
import inboxMixin from 'shared/mixins/inboxMixin';
import ConversationContextMenu from './contextMenu/Index.vue';
import alertMixin from 'shared/mixins/alertMixin';
import TimeAgo from 'dashboard/components/ui/TimeAgo.vue';
import CardLabels from './conversationCardComponents/CardLabels.vue';
import PriorityMark from './PriorityMark.vue';
@@ -143,8 +141,7 @@ export default {
PriorityMark,
SLACardLabel,
},
mixins: [inboxMixin, timeMixin, conversationMixin, alertMixin],
mixins: [inboxMixin, conversationMixin],
props: {
activeLabel: {
type: String,
@@ -1,7 +1,7 @@
<!-- eslint-disable vue/no-mutating-props -->
<template>
<woot-modal :show.sync="show" :on-close="onCancel">
<div class="h-auto overflow-auto flex flex-col">
<div class="flex flex-col h-auto overflow-auto">
<woot-modal-header
:header-title="$t('EMAIL_TRANSCRIPT.TITLE')"
:header-content="$t('EMAIL_TRANSCRIPT.DESC')"
@@ -61,7 +61,7 @@
</label>
</div>
</div>
<div class="flex flex-row justify-end gap-2 py-2 px-0 w-full">
<div class="flex flex-row justify-end w-full gap-2 px-0 py-2">
<woot-submit-button
:button-text="$t('EMAIL_TRANSCRIPT.SUBMIT')"
:disabled="!isFormValid"
@@ -77,9 +77,8 @@
<script>
import { required, minLength, email } from 'vuelidate/lib/validators';
import alertMixin from 'shared/mixins/alertMixin';
import { useAlert } from 'dashboard/composables';
export default {
mixins: [alertMixin],
props: {
show: {
type: Boolean,
@@ -142,10 +141,10 @@ export default {
email: this.selectedEmailAddress,
conversationId: this.currentChat.id,
});
this.showAlert(this.$t('EMAIL_TRANSCRIPT.SEND_EMAIL_SUCCESS'));
useAlert(this.$t('EMAIL_TRANSCRIPT.SEND_EMAIL_SUCCESS'));
this.onCancel();
} catch (error) {
this.showAlert(this.$t('EMAIL_TRANSCRIPT.SEND_EMAIL_ERROR'));
useAlert(this.$t('EMAIL_TRANSCRIPT.SEND_EMAIL_ERROR'));
} finally {
this.isSubmitting = false;
}
@@ -35,7 +35,7 @@
</template>
<script>
import { mapGetters } from 'vuex';
import adminMixin from 'dashboard/mixins/isAdmin';
import { useAdmin } from 'dashboard/composables/useAdmin';
import accountMixin from 'dashboard/mixins/account';
import OnboardingView from '../OnboardingView.vue';
import EmptyStateMessage from './EmptyStateMessage.vue';
@@ -45,13 +45,19 @@ export default {
OnboardingView,
EmptyStateMessage,
},
mixins: [accountMixin, adminMixin],
mixins: [accountMixin],
props: {
isOnExpandedLayout: {
type: Boolean,
default: false,
},
},
setup() {
const { isAdmin } = useAdmin();
return {
isAdmin,
};
},
computed: {
...mapGetters({
currentChat: 'getSelectedChat',
@@ -127,7 +127,7 @@
</div>
<div
v-if="shouldShowContextMenu"
class="context-menu-wrap invisible group-hover:visible"
class="invisible context-menu-wrap group-hover:visible"
>
<context-menu
v-if="isBubble && !isMessageDeleted"
@@ -157,8 +157,7 @@ import ContextMenu from 'dashboard/modules/conversations/components/MessageConte
import InstagramStory from './bubble/InstagramStory.vue';
import InstagramStoryReply from './bubble/InstagramStoryReply.vue';
import Spinner from 'shared/components/Spinner.vue';
import alertMixin from 'shared/mixins/alertMixin';
import contentTypeMixin from 'shared/mixins/contentTypeMixin';
import { CONTENT_TYPES } from 'shared/constants/contentType';
import { MESSAGE_TYPE, MESSAGE_STATUS } from 'shared/constants/messages';
import { generateBotMessageContent } from './helpers/botMessageContentHelper';
import { BUS_EVENTS } from 'shared/constants/busEvents';
@@ -166,6 +165,7 @@ import { ACCOUNT_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
import { LocalStorage } from 'shared/helpers/localStorage';
import { getDayDifferenceFromNow } from 'shared/helpers/DateHelper';
import * as Sentry from '@sentry/browser';
export default {
components: {
@@ -183,7 +183,7 @@ export default {
InstagramStoryReply,
Spinner,
},
mixins: [alertMixin, messageFormatterMixin, contentTypeMixin],
mixins: [messageFormatterMixin],
props: {
data: {
type: Object,
@@ -481,6 +481,9 @@ export default {
}
return '';
},
isEmailContentType() {
return this.contentType === CONTENT_TYPES.INCOMING_EMAIL;
},
},
watch: {
data() {
@@ -498,19 +501,35 @@ export default {
},
methods: {
isAttachmentImageVideoAudio(fileType) {
return ['image', 'audio', 'video', 'story_mention'].includes(fileType);
return ['image', 'audio', 'video', 'story_mention', 'ig_reel'].includes(
fileType
);
},
hasMediaAttachment(type) {
if (this.hasAttachments && this.data.attachments.length > 0) {
const { attachments = [{}] } = this.data;
const { file_type: fileType } = attachments[0];
return fileType === type && !this.hasMediaLoadError;
return this.compareMessageFileType(this.data, type);
}
if (this.storyReply) {
return true;
}
return false;
},
compareMessageFileType(messageData, type) {
try {
const { attachments = [{}] } = messageData;
const { file_type: fileType } = attachments[0];
return fileType === type && !this.hasMediaLoadError;
} catch (err) {
Sentry.setContext('attachment-parsing-error', {
messageData,
type,
hasMediaLoadError: this.hasMediaLoadError,
});
Sentry.captureException(err);
return false;
}
},
handleContextMenuClick() {
this.showContextMenu = !this.showContextMenu;
},
@@ -1,5 +1,5 @@
<template>
<div class="flex actions--container relative items-center gap-2">
<div class="relative flex items-center gap-2 actions--container">
<woot-button
v-if="!currentChat.muted"
v-tooltip="$t('CONTACT_PANEL.MUTE_CONTACT')"
@@ -37,7 +37,7 @@
</template>
<script>
import { mapGetters } from 'vuex';
import alertMixin from 'shared/mixins/alertMixin';
import { useAlert } from 'dashboard/composables';
import EmailTranscriptModal from './EmailTranscriptModal.vue';
import ResolveAction from '../../buttons/ResolveAction.vue';
import {
@@ -51,7 +51,6 @@ export default {
EmailTranscriptModal,
ResolveAction,
},
mixins: [alertMixin],
data() {
return {
showEmailActionsModal: false,
@@ -73,11 +72,11 @@ export default {
methods: {
mute() {
this.$store.dispatch('muteConversation', this.currentChat.id);
this.showAlert(this.$t('CONTACT_PANEL.MUTED_SUCCESS'));
useAlert(this.$t('CONTACT_PANEL.MUTED_SUCCESS'));
},
unmute() {
this.$store.dispatch('unmuteConversation', this.currentChat.id);
this.showAlert(this.$t('CONTACT_PANEL.UNMUTED_SUCCESS'));
useAlert(this.$t('CONTACT_PANEL.UNMUTED_SUCCESS'));
},
toggleEmailActionsModal() {
this.showEmailActionsModal = !this.showEmailActionsModal;
@@ -153,7 +153,8 @@
<script>
import { mapGetters } from 'vuex';
import alertMixin from 'shared/mixins/alertMixin';
import { useAlert } from 'dashboard/composables';
import { useUISettings } from 'dashboard/composables/useUISettings';
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
import CannedResponse from './CannedResponse.vue';
@@ -180,10 +181,8 @@ import {
import WhatsappTemplates from './WhatsappTemplates/Modal.vue';
import { MESSAGE_MAX_LENGTH } from 'shared/helpers/MessageTypeHelper';
import inboxMixin, { INBOX_FEATURES } from 'shared/mixins/inboxMixin';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import { trimContent, debounce } from '@chatwoot/utils';
import wootConstants from 'dashboard/constants/globals';
import { isEditorHotKeyEnabled } from 'dashboard/mixins/uiSettings';
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
import rtlMixin from 'shared/mixins/rtlMixin';
import fileUploadMixin from 'dashboard/mixins/fileUploadMixin';
@@ -218,8 +217,6 @@ export default {
},
mixins: [
inboxMixin,
uiSettingsMixin,
alertMixin,
messageFormatterMixin,
rtlMixin,
fileUploadMixin,
@@ -231,6 +228,21 @@ export default {
default: false,
},
},
setup() {
const {
uiSettings,
updateUISettings,
isEditorHotKeyEnabled,
fetchSignatureFlagFromUISettings,
} = useUISettings();
return {
uiSettings,
updateUISettings,
isEditorHotKeyEnabled,
fetchSignatureFlagFromUISettings,
};
},
data() {
return {
message: '',
@@ -310,7 +322,7 @@ export default {
agentId,
})
.then(() => {
this.showAlert(this.$t('CONVERSATION.CHANGE_AGENT'));
useAlert(this.$t('CONVERSATION.CHANGE_AGENT'));
});
},
},
@@ -406,7 +418,7 @@ export default {
if (this.isPrivate) {
sendMessageText = this.$t('CONVERSATION.REPLYBOX.CREATE');
}
const keyLabel = isEditorHotKeyEnabled(this.uiSettings, 'cmd_enter')
const keyLabel = this.isEditorHotKeyEnabled('cmd_enter')
? '(⌘ + ↵)'
: '(↵)';
return `${sendMessageText} ${keyLabel}`;
@@ -479,7 +491,7 @@ export default {
return !!this.signatureToApply;
},
sendWithSignature() {
return this.fetchSignatureFlagFromUiSettings(this.channelType);
return this.fetchSignatureFlagFromUISettings(this.channelType);
},
editorMessageKey() {
const { editor_message_key: isEnabled } = this.uiSettings;
@@ -748,7 +760,7 @@ export default {
!this.showCannedMenu &&
!this.showVariablesMenu &&
this.isFocused &&
isEditorHotKeyEnabled(this.uiSettings, selectedKey)
this.isEditorHotKeyEnabled(selectedKey)
);
},
onPaste(e) {
@@ -880,7 +892,7 @@ export default {
} catch (error) {
const errorMessage =
error?.response?.data?.error || this.$t('CONVERSATION.MESSAGE_ERROR');
this.showAlert(errorMessage);
useAlert(errorMessage);
}
},
async onSendWhatsAppReply(messagePayload) {
@@ -77,10 +77,10 @@
import { MESSAGE_TYPE, MESSAGE_STATUS } from 'shared/constants/messages';
import inboxMixin from 'shared/mixins/inboxMixin';
import { mapGetters } from 'vuex';
import timeMixin from '../../../../mixins/time';
import { messageTimestamp } from 'shared/helpers/timeHelper';
export default {
mixins: [inboxMixin, timeMixin],
mixins: [inboxMixin],
props: {
sender: {
type: Object,
@@ -159,7 +159,7 @@ export default {
return MESSAGE_STATUS.SENT === this.messageStatus;
},
readableTime() {
return this.messageTimestamp(this.createdAt, 'LLL d, h:mm a');
return messageTimestamp(this.createdAt, 'LLL d, h:mm a');
},
screenName() {
const { additional_attributes: additionalAttributes = {} } =
@@ -17,14 +17,13 @@
</template>
<script>
import { useAlert } from 'dashboard/composables';
import {
DuplicateContactException,
ExceptionWithMessage,
} from 'shared/helpers/CustomErrors';
import alertMixin from 'shared/mixins/alertMixin';
export default {
mixins: [alertMixin],
props: {
name: {
type: String,
@@ -52,18 +51,18 @@ export default {
'contacts/create',
this.getContactObject()
);
this.showAlert(this.$t('CONTACT_FORM.SUCCESS_MESSAGE'));
useAlert(this.$t('CONTACT_FORM.SUCCESS_MESSAGE'));
}
this.openContactNewTab(contact.id);
} catch (error) {
if (error instanceof DuplicateContactException) {
if (error.data.includes('phone_number')) {
this.showAlert(this.$t('CONTACT_FORM.FORM.PHONE_NUMBER.DUPLICATE'));
useAlert(this.$t('CONTACT_FORM.FORM.PHONE_NUMBER.DUPLICATE'));
}
} else if (error instanceof ExceptionWithMessage) {
this.showAlert(error.data);
useAlert(error.data);
} else {
this.showAlert(this.$t('CONTACT_FORM.ERROR_MESSAGE'));
useAlert(this.$t('CONTACT_FORM.ERROR_MESSAGE'));
}
}
},
@@ -40,6 +40,7 @@ const ALLOWED_FILE_TYPES = {
IMAGE: 'image',
VIDEO: 'video',
AUDIO: 'audio',
IG_REEL: 'ig_reel',
};
export default {
@@ -66,7 +67,10 @@ export default {
return this.attachment.file_type === ALLOWED_FILE_TYPES.IMAGE;
},
isVideo() {
return this.attachment.file_type === ALLOWED_FILE_TYPES.VIDEO;
return (
this.attachment.file_type === ALLOWED_FILE_TYPES.VIDEO ||
this.attachment.file_type === ALLOWED_FILE_TYPES.IG_REEL
);
},
isAudio() {
return this.attachment.file_type === ALLOWED_FILE_TYPES.AUDIO;
@@ -31,10 +31,9 @@
<script>
import DyteAPI from 'dashboard/api/integrations/dyte';
import { buildDyteURL } from 'shared/helpers/IntegrationHelper';
import alertMixin from 'shared/mixins/alertMixin';
import { useAlert } from 'dashboard/composables';
export default {
mixins: [alertMixin],
props: {
messageId: {
type: Number,
@@ -61,7 +60,7 @@ export default {
await DyteAPI.addParticipantToMeeting(this.messageId);
this.dyteAuthToken = authToken;
} catch (err) {
this.showAlert(this.$t('INTEGRATION_SETTINGS.DYTE.JOIN_ERROR'));
useAlert(this.$t('INTEGRATION_SETTINGS.DYTE.JOIN_ERROR'));
} finally {
this.isLoading = false;
}
@@ -12,7 +12,7 @@
@click="onClose"
>
<div
class="bg-white dark:bg-slate-900 z-10 flex items-center justify-between w-full h-16 px-6 py-2"
class="z-10 flex items-center justify-between w-full h-16 px-6 py-2 bg-white dark:bg-slate-900"
@click.stop
>
<div
@@ -167,7 +167,7 @@
/>
</div>
</div>
<div class="flex items-center justify-center w-full h-16 px-6 py-2 z-10">
<div class="z-10 flex items-center justify-center w-full h-16 px-6 py-2">
<div
class="items-center rounded-sm flex font-semibold justify-center min-w-[5rem] p-1 bg-slate-25 dark:bg-slate-800 text-slate-600 dark:text-slate-200 text-sm"
>
@@ -182,13 +182,14 @@
<script>
import { mapGetters } from 'vuex';
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
import timeMixin from 'dashboard/mixins/time';
import { messageTimestamp } from 'shared/helpers/timeHelper';
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
const ALLOWED_FILE_TYPES = {
IMAGE: 'image',
VIDEO: 'video',
IG_REEL: 'ig_reel',
AUDIO: 'audio',
};
@@ -199,7 +200,7 @@ export default {
components: {
Thumbnail,
},
mixins: [keyboardEventListenerMixins, timeMixin],
mixins: [keyboardEventListenerMixins],
props: {
show: {
type: Boolean,
@@ -236,13 +237,16 @@ export default {
readableTime() {
const { created_at: createdAt } = this.activeAttachment;
if (!createdAt) return '';
return this.messageTimestamp(createdAt, 'LLL d yyyy, h:mm a') || '';
return messageTimestamp(createdAt, 'LLL d yyyy, h:mm a') || '';
},
isImage() {
return this.activeFileType === ALLOWED_FILE_TYPES.IMAGE;
},
isVideo() {
return this.activeFileType === ALLOWED_FILE_TYPES.VIDEO;
return (
this.activeFileType === ALLOWED_FILE_TYPES.VIDEO ||
this.activeFileType === ALLOWED_FILE_TYPES.IG_REEL
);
},
isAudio() {
return this.activeFileType === ALLOWED_FILE_TYPES.AUDIO;
@@ -85,11 +85,11 @@ describe('MoveActions', () => {
it('shows alert', async () => {
await moreActions.find('button:first-child').trigger('click');
expect(emitter.emit).toBeCalledWith(
'newToastMessage',
'This contact is blocked successfully. You will not be notified of any future conversations.',
undefined
);
expect(emitter.emit).toBeCalledWith('newToastMessage', {
message:
'This contact is blocked successfully. You will not be notified of any future conversations.',
action: null,
});
});
});
@@ -111,11 +111,10 @@ describe('MoveActions', () => {
it('shows alert', async () => {
await moreActions.find('button:first-child').trigger('click');
expect(emitter.emit).toBeCalledWith(
'newToastMessage',
'This contact is unblocked successfully.',
undefined
);
expect(emitter.emit).toBeCalledWith('newToastMessage', {
message: 'This contact is unblocked successfully.',
action: null,
});
});
});
});
+12 -3
View File
@@ -1,6 +1,10 @@
import { getCurrentInstance } from 'vue';
import { emitter } from 'shared/helpers/mitt';
/**
* Custom hook to track events
* @returns {Function} The track function
*/
export const useTrack = () => {
const vm = getCurrentInstance();
if (!vm) throw new Error('must be called in setup');
@@ -8,6 +12,11 @@ export const useTrack = () => {
return vm.proxy.$track;
};
export function useAlert(message, action) {
emitter.emit('newToastMessage', message, action);
}
/**
* Emits a toast message event using a global emitter.
* @param {string} message - The message to be displayed in the toast.
* @param {Object|null} action - Optional callback function or object to execute.
*/
export const useAlert = (message, action = null) => {
emitter.emit('newToastMessage', { message, action });
};
@@ -0,0 +1,51 @@
import { getCurrentInstance } from 'vue';
import { emitter } from 'shared/helpers/mitt';
import { useTrack, useAlert } from '../index';
vi.mock('vue', () => ({
getCurrentInstance: vi.fn(),
}));
vi.mock('shared/helpers/mitt', () => ({
emitter: {
emit: vi.fn(),
},
}));
describe('useTrack', () => {
it('should return $track from the current instance proxy', () => {
const mockProxy = { $track: vi.fn() };
getCurrentInstance.mockReturnValue({ proxy: mockProxy });
const track = useTrack();
expect(track).toBe(mockProxy.$track);
});
it('should throw an error if called outside of setup', () => {
getCurrentInstance.mockReturnValue(null);
expect(useTrack).toThrowError('must be called in setup');
});
});
describe('useAlert', () => {
it('should emit a newToastMessage event with the provided message and action', () => {
const message = 'Toast message';
const action = {
type: 'link',
to: '/app/accounts/1/conversations/1',
message: 'Navigate',
};
useAlert(message, action);
expect(emitter.emit).toHaveBeenCalledWith('newToastMessage', {
message,
action,
});
});
it('should emit a newToastMessage event with the provided message and no action if action is null', () => {
const message = 'Toast message';
useAlert(message);
expect(emitter.emit).toHaveBeenCalledWith('newToastMessage', {
message,
action: null,
});
});
});
@@ -0,0 +1,52 @@
import { ref } from 'vue';
import { useAdmin } from '../useAdmin';
import { useStoreGetters } from 'dashboard/composables/store';
vi.mock('dashboard/composables/store');
describe('useAdmin', () => {
it('returns true if the current user is an administrator', () => {
useStoreGetters.mockReturnValue({
getCurrentRole: ref('administrator'),
});
const { isAdmin } = useAdmin();
expect(isAdmin.value).toBe(true);
});
it('returns false if the current user is not an administrator', () => {
useStoreGetters.mockReturnValue({
getCurrentRole: ref('user'),
});
const { isAdmin } = useAdmin();
expect(isAdmin.value).toBe(false);
});
it('returns false if the current user role is null', () => {
useStoreGetters.mockReturnValue({
getCurrentRole: ref(null),
});
const { isAdmin } = useAdmin();
expect(isAdmin.value).toBe(false);
});
it('returns false if the current user role is undefined', () => {
useStoreGetters.mockReturnValue({
getCurrentRole: ref(undefined),
});
const { isAdmin } = useAdmin();
expect(isAdmin.value).toBe(false);
});
it('returns false if the current user role is an empty string', () => {
useStoreGetters.mockReturnValue({
getCurrentRole: ref(''),
});
const { isAdmin } = useAdmin();
expect(isAdmin.value).toBe(false);
});
});
@@ -0,0 +1,138 @@
import { ref } from 'vue';
import {
useUISettings,
DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER,
} from 'dashboard/composables/useUISettings';
// Mocking the store composables
const mockDispatch = vi.fn();
const getUISettingsMock = ref({
is_ct_labels_open: true,
conversation_sidebar_items_order: DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
contact_sidebar_items_order: DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER,
editor_message_key: 'enter',
});
vi.mock('dashboard/composables/store', () => ({
useStoreGetters: () => ({
getUISettings: getUISettingsMock,
}),
useStore: () => ({
dispatch: mockDispatch,
}),
}));
describe('useUISettings', () => {
beforeEach(() => {
mockDispatch.mockClear();
});
it('returns uiSettings', () => {
const { uiSettings } = useUISettings();
expect(uiSettings.value).toEqual({
is_ct_labels_open: true,
conversation_sidebar_items_order:
DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
contact_sidebar_items_order: DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER,
editor_message_key: 'enter',
});
});
it('updates UI settings correctly', () => {
const { updateUISettings } = useUISettings();
updateUISettings({ enter_to_send_enabled: true });
expect(mockDispatch).toHaveBeenCalledWith('updateUISettings', {
uiSettings: {
enter_to_send_enabled: true,
is_ct_labels_open: true,
conversation_sidebar_items_order:
DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
contact_sidebar_items_order: DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER,
editor_message_key: 'enter',
},
});
});
it('toggles sidebar UI state correctly', () => {
const { toggleSidebarUIState } = useUISettings();
toggleSidebarUIState('is_ct_labels_open');
expect(mockDispatch).toHaveBeenCalledWith('updateUISettings', {
uiSettings: {
is_ct_labels_open: false,
conversation_sidebar_items_order:
DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
contact_sidebar_items_order: DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER,
editor_message_key: 'enter',
},
});
});
it('returns correct conversation sidebar items order', () => {
const { conversationSidebarItemsOrder } = useUISettings();
expect(conversationSidebarItemsOrder.value).toEqual(
DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER
);
});
it('returns correct contact sidebar items order', () => {
const { contactSidebarItemsOrder } = useUISettings();
expect(contactSidebarItemsOrder.value).toEqual(
DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER
);
});
it('returns correct value for isContactSidebarItemOpen', () => {
const { isContactSidebarItemOpen } = useUISettings();
expect(isContactSidebarItemOpen('is_ct_labels_open')).toBe(true);
expect(isContactSidebarItemOpen('non_existent_key')).toBe(false);
});
it('sets signature flag for inbox correctly', () => {
const { setSignatureFlagForInbox } = useUISettings();
setSignatureFlagForInbox('email', true);
expect(mockDispatch).toHaveBeenCalledWith('updateUISettings', {
uiSettings: {
is_ct_labels_open: true,
conversation_sidebar_items_order:
DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
contact_sidebar_items_order: DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER,
email_signature_enabled: true,
editor_message_key: 'enter',
},
});
});
it('fetches signature flag from UI settings correctly', () => {
const { fetchSignatureFlagFromUISettings } = useUISettings();
expect(fetchSignatureFlagFromUISettings('email')).toBe(undefined);
});
it('returns correct value for isEditorHotKeyEnabled when editor_message_key is configured', () => {
getUISettingsMock.value.enter_to_send_enabled = false;
const { isEditorHotKeyEnabled } = useUISettings();
expect(isEditorHotKeyEnabled('enter')).toBe(true);
expect(isEditorHotKeyEnabled('cmd_enter')).toBe(false);
});
it('returns correct value for isEditorHotKeyEnabled when editor_message_key is not configured', () => {
getUISettingsMock.value.editor_message_key = undefined;
const { isEditorHotKeyEnabled } = useUISettings();
expect(isEditorHotKeyEnabled('enter')).toBe(false);
expect(isEditorHotKeyEnabled('cmd_enter')).toBe(true);
});
it('handles non-existent keys', () => {
const {
isContactSidebarItemOpen,
fetchSignatureFlagFromUISettings,
isEditorHotKeyEnabled,
} = useUISettings();
expect(isContactSidebarItemOpen('non_existent_key')).toBe(false);
expect(fetchSignatureFlagFromUISettings('non_existent_key')).toBe(
undefined
);
expect(isEditorHotKeyEnabled('non_existent_key')).toBe(false);
});
});
@@ -0,0 +1,17 @@
import { computed } from 'vue';
import { useStoreGetters } from 'dashboard/composables/store';
/**
* Composable to determine if the current user is an administrator.
* @returns {Boolean} - True if the current user is an administrator, false otherwise.
*/
export function useAdmin() {
const getters = useStoreGetters();
const currentUserRole = computed(() => getters.getCurrentRole.value);
const isAdmin = computed(() => currentUserRole.value === 'administrator');
return {
isAdmin,
};
}
@@ -0,0 +1,149 @@
import { computed } from 'vue';
import { useStore, useStoreGetters } from 'dashboard/composables/store';
export const DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER = Object.freeze([
{ name: 'conversation_actions' },
{ name: 'macros' },
{ name: 'conversation_info' },
{ name: 'contact_attributes' },
{ name: 'previous_conversation' },
{ name: 'conversation_participants' },
]);
export const DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER = Object.freeze([
{ name: 'contact_attributes' },
{ name: 'contact_labels' },
{ name: 'previous_conversation' },
]);
/**
* Slugifies the channel name.
* Replaces spaces, hyphens, and double colons with underscores.
* @param {string} name - The channel name to slugify.
* @returns {string} The slugified channel name.
*/
const slugifyChannel = name =>
name?.toLowerCase().replace(' ', '_').replace('-', '_').replace('::', '_');
/**
* Computes the order of items in the conversation sidebar, using defaults if not present.
* @param {Object} uiSettings - Reactive UI settings object.
* @returns {Array} Ordered list of sidebar items.
*/
const useConversationSidebarItemsOrder = uiSettings => {
return computed(() => {
const { conversation_sidebar_items_order: itemsOrder } = uiSettings.value;
// If the sidebar order is not set, use the default order.
if (!itemsOrder) {
return DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER;
}
// Create a copy of itemsOrder to avoid mutating the original store object.
const itemsOrderCopy = [...itemsOrder];
// If the sidebar order doesn't have the new elements, then add them to the list.
DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER.forEach(item => {
if (!itemsOrderCopy.find(i => i.name === item.name)) {
itemsOrderCopy.push(item);
}
});
return itemsOrderCopy;
});
};
/**
* Computes the order of items in the contact sidebar,using defaults if not present.
* @param {Object} uiSettings - Reactive UI settings object.
* @returns {Array} Ordered list of sidebar items.
*/
const useContactSidebarItemsOrder = uiSettings => {
return computed(() => {
const { contact_sidebar_items_order: itemsOrder } = uiSettings.value;
return itemsOrder || DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER;
});
};
/**
* Toggles the open state of a sidebar item.
* @param {string} key - The key of the sidebar item to toggle.
* @param {Object} uiSettings - Reactive UI settings object.
* @param {Function} updateUISettings - Function to update UI settings.
*/
const toggleSidebarUIState = (key, uiSettings, updateUISettings) => {
updateUISettings({ [key]: !uiSettings.value[key] });
};
/**
* Sets the signature flag for a specific channel type in the inbox settings.
* @param {string} channelType - The type of the channel.
* @param {boolean} value - The value to set for the signature enabled flag.
* @param {Function} updateUISettings - Function to update UI settings.
*/
const setSignatureFlagForInbox = (channelType, value, updateUISettings) => {
if (!channelType) return;
const slugifiedChannel = slugifyChannel(channelType);
updateUISettings({ [`${slugifiedChannel}_signature_enabled`]: value });
};
/**
* Fetches the signature flag for a specific channel type from UI settings.
* @param {string} channelType - The type of the channel.
* @param {Object} uiSettings - Reactive UI settings object.
* @returns {boolean} The value of the signature enabled flag.
*/
const fetchSignatureFlagFromUISettings = (channelType, uiSettings) => {
if (!channelType) return false;
const slugifiedChannel = slugifyChannel(channelType);
return uiSettings.value[`${slugifiedChannel}_signature_enabled`];
};
/**
* Checks if a specific editor hotkey is enabled.
* @param {string} key - The key to check.
* @param {Object} uiSettings - Reactive UI settings object.
* @returns {boolean} True if the hotkey is enabled, otherwise false.
*/
const isEditorHotKeyEnabled = (key, uiSettings) => {
const {
editor_message_key: editorMessageKey,
enter_to_send_enabled: enterToSendEnabled,
} = uiSettings.value || {};
if (!editorMessageKey) {
return key === (enterToSendEnabled ? 'enter' : 'cmd_enter');
}
return editorMessageKey === key;
};
/**
* Main composable function for managing UI settings.
* @returns {Object} An object containing reactive properties and methods for UI settings management.
*/
export function useUISettings() {
const getters = useStoreGetters();
const store = useStore();
const uiSettings = computed(() => getters.getUISettings.value);
const updateUISettings = (settings = {}) => {
store.dispatch('updateUISettings', {
uiSettings: {
...uiSettings.value,
...settings,
},
});
};
return {
uiSettings,
updateUISettings,
conversationSidebarItemsOrder: useConversationSidebarItemsOrder(uiSettings),
contactSidebarItemsOrder: useContactSidebarItemsOrder(uiSettings),
isContactSidebarItemOpen: key => !!uiSettings.value[key],
toggleSidebarUIState: key =>
toggleSidebarUIState(key, uiSettings, updateUISettings),
setSignatureFlagForInbox: (channelType, value) =>
setSignatureFlagForInbox(channelType, value, updateUISettings),
fetchSignatureFlagFromUISettings: channelType =>
fetchSignatureFlagFromUISettings(channelType, uiSettings),
isEditorHotKeyEnabled: key => isEditorHotKeyEnabled(key, uiSettings),
};
}
@@ -38,9 +38,10 @@ export class AnalyticsHelper {
* @param {Object} user - User object
*/
identify(user) {
if (!this.analytics) {
if (!this.analytics || !user) {
return;
}
this.user = user;
this.analytics.identify(this.user.email, {
userId: this.user.id,
@@ -52,6 +52,10 @@ class DashboardAudioNotificationHelper {
};
executeRecurringNotification = () => {
if (!window.WOOT || !window.WOOT.$store) {
this.clearSetTimeout();
return;
}
const mineConversation = window.WOOT.$store.getters.getMineChats({
assigneeType: 'me',
status: 'open',
@@ -0,0 +1,124 @@
const SCRIPT_TYPE = 'text/javascript';
const DATA_LOADED_ATTR = 'data-loaded';
const SCRIPT_PROPERTIES = [
'defer',
'crossOrigin',
'noModule',
'referrerPolicy',
'id',
];
/**
* Custom error class for script loading failures.
* @extends Error
*/
class ScriptLoaderError extends Error {
/**
* Creates a new ScriptLoaderError.
* @param {string} src - The source URL of the script that failed to load.
* @param {string} message - The error message.
*/
constructor(src, message = 'Failed to load script') {
super(message);
this.name = 'ScriptLoaderError';
this.src = src;
}
/**
* Gets detailed error information.
* @returns {string} A string containing the error details.
*/
getErrorDetails() {
return `Failed to load script from source: ${this.src}`;
}
}
/**
* Creates a new script element with the specified attributes.
* @param {string} src - The source URL of the script.
* @param {Object} options - Options for configuring the script element.
* @param {string} [options.type='text/javascript'] - The type of the script.
* @param {boolean} [options.async=true] - Whether the script should load asynchronously.
* @param {boolean} [options.defer] - Whether the script execution should be deferred.
* @param {string} [options.crossOrigin] - The CORS setting for the script.
* @param {boolean} [options.noModule] - Whether the script should not be treated as a JavaScript module.
* @param {string} [options.referrerPolicy] - The referrer policy for the script.
* @param {string} [options.id] - The id attribute for the script element.
* @param {Object} [options.attrs] - Additional attributes to set on the script element.
* @returns {HTMLScriptElement} The created script element.
*/
const createScriptElement = (src, options) => {
const el = document.createElement('script');
el.type = options.type || SCRIPT_TYPE;
el.async = options.async !== false;
el.src = src;
SCRIPT_PROPERTIES.forEach(property => {
if (property in options) {
el[property] = options[property];
}
});
Object.entries(options.attrs || {}).forEach(([name, value]) =>
el.setAttribute(name, value)
);
return el;
};
/**
* Finds an existing script element with the specified source URL.
* @param {string} src - The source URL to search for.
* @returns {HTMLScriptElement|null} The found script element, or null if not found.
*/
const findExistingScript = src => {
return document.querySelector(`script[src="${src}"]`);
};
/**
* Loads a script asynchronously and returns a promise.
* @param {string} src - The source URL of the script to load.
* @param {Object} options - Options for configuring the script element.
* @param {string} [options.type='text/javascript'] - The type of the script.
* @param {boolean} [options.async=true] - Whether the script should load asynchronously.
* @param {boolean} [options.defer] - Whether the script execution should be deferred.
* @param {string} [options.crossOrigin] - The CORS setting for the script.
* @param {boolean} [options.noModule] - Whether the script should not be treated as a JavaScript module.
* @param {string} [options.referrerPolicy] - The referrer policy for the script.
* @param {string} [options.id] - The id attribute for the script element.
* @param {Object} [options.attrs] - Additional attributes to set on the script element.
* @returns {Promise<HTMLScriptElement|boolean>} A promise that resolves with the loaded script element,
* or false if the script couldn't be loaded.
* @throws {ScriptLoaderError} If the script fails to load.
*/
export async function loadScript(src, options) {
if (typeof window === 'undefined' || !window.document) {
return Promise.resolve(false);
}
return new Promise((resolve, reject) => {
if (typeof src !== 'string' || src.trim() === '') {
reject(new Error('Invalid source URL provided'));
return;
}
let el = findExistingScript(src);
if (!el) {
el = createScriptElement(src, options);
document.head.appendChild(el);
} else if (el.hasAttribute(DATA_LOADED_ATTR)) {
resolve(el);
return;
}
const handleError = () => reject(new ScriptLoaderError(src));
el.addEventListener('error', handleError);
el.addEventListener('abort', handleError);
el.addEventListener('load', () => {
el.setAttribute(DATA_LOADED_ATTR, 'true');
resolve(el);
});
});
}
@@ -1,4 +1,20 @@
const FEATURE_HELP_URLS = {
channel_email: 'https://chwt.app/hc/email',
channel_facebook: 'https://chwt.app/hc/fb',
help_center: 'https://chwt.app/hc/help-center',
agent_bots: 'https://chwt.app/hc/agent-bots',
team_management: 'https://chwt.app/hc/teams',
labels: 'https://chwt.app/hc/labels',
custom_attributes: 'https://chwt.app/hc/custom-attributes',
canned_responses: 'https://chwt.app/hc/canned',
integrations: 'https://chwt.app/hc/integrations',
campaigns: 'https://chwt.app/hc/campaigns',
reports: 'https://chwt.app/hc/reports',
message_reply_to: 'https://chwt.app/hc/reply-to',
sla: 'https://chwt.app/hc/sla',
dashboard_apps: 'https://chwt.app/hc/dashboard-apps',
};
export function getHelpUrlForFeature(featureName) {
const { helpUrls } = window.chatwootConfig;
return helpUrls[featureName];
return FEATURE_HELP_URLS[featureName];
}
@@ -1,7 +1,7 @@
/* eslint-disable no-console */
import NotificationSubscriptions from '../api/notificationSubscription';
import auth from '../api/auth';
import { emitter } from 'shared/helpers/mitt';
import { useAlert } from 'dashboard/composables';
export const verifyServiceWorkerExistence = (callback = () => {}) => {
if (!('serviceWorker' in navigator)) {
@@ -69,19 +69,13 @@ export const registerSubscription = (onSuccess = () => {}) => {
onSuccess();
})
.catch(() => {
emitter.emit(
'newToastMessage',
'This browser does not support desktop notification'
);
useAlert('This browser does not support desktop notification');
});
};
export const requestPushPermissions = ({ onSuccess }) => {
if (!('Notification' in window)) {
emitter.emit(
'newToastMessage',
'This browser does not support desktop notification'
);
useAlert('This browser does not support desktop notification');
} else if (Notification.permission === 'granted') {
registerSubscription(onSuccess);
} else if (Notification.permission !== 'denied') {
@@ -0,0 +1,95 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { loadScript } from '../DOMHelpers';
import { JSDOM } from 'jsdom';
describe('loadScript', () => {
let dom;
let window;
let document;
beforeEach(() => {
dom = new JSDOM('<!DOCTYPE html><html><head></head><body></body></html>', {
url: 'http://localhost',
});
window = dom.window;
document = window.document;
global.document = document;
});
afterEach(() => {
vi.restoreAllMocks();
delete global.document;
});
it('should load a script successfully', async () => {
const src = 'https://example.com/script.js';
const loadPromise = loadScript(src, {});
// Simulate successful script load
setTimeout(() => {
const script = document.querySelector(`script[src="${src}"]`);
if (script) {
script.dispatchEvent(new window.Event('load'));
}
}, 0);
const script = await loadPromise;
expect(script).toBeTruthy();
expect(script.getAttribute('src')).toBe(src);
expect(script.getAttribute('data-loaded')).toBe('true');
});
it('should not load a script if document is not available', async () => {
delete global.document;
const result = await loadScript('https://example.com/script.js', {});
expect(result).toBe(false);
});
it('should use an existing script if already present', async () => {
const src = 'https://example.com/existing-script.js';
const existingScript = document.createElement('script');
existingScript.src = src;
existingScript.setAttribute('data-loaded', 'true');
document.head.appendChild(existingScript);
const script = await loadScript(src, {});
expect(script).toBe(existingScript);
});
it('should set custom attributes on the script element', async () => {
const src = 'https://example.com/custom-script.js';
const options = {
type: 'module',
async: false,
defer: true,
crossOrigin: 'anonymous',
noModule: true,
referrerPolicy: 'origin',
id: 'custom-script',
attrs: { 'data-custom': 'value' },
};
const loadPromise = loadScript(src, options);
// Simulate successful script load
setTimeout(() => {
const script = document.querySelector(`script[src="${src}"]`);
if (script) {
script.dispatchEvent(new window.Event('load'));
}
}, 0);
const script = await loadPromise;
expect(script.type).toBe('module');
expect(script.async).toBe(false);
expect(script.defer).toBe(true);
expect(script.crossOrigin).toBe('anonymous');
expect(script.noModule).toBe(true);
expect(script.referrerPolicy).toBe('origin');
expect(script.id).toBe('custom-script');
expect(script.getAttribute('data-custom')).toBe('value');
});
});
@@ -33,7 +33,7 @@
"NONE": "None",
"NO_TEAMS_AVAILABLE": "There are no teams added to this account yet.",
"ASSIGN_SELECTED_TEAMS": "Assign selected team.",
"ASSIGN_SUCCESFUL": "Teams assiged successfully.",
"ASSIGN_SUCCESFUL": "Teams assigned successfully.",
"ASSIGN_FAILED": "Failed to assign team. Please try again."
}
}
@@ -95,7 +95,9 @@
},
"NETWORK": {
"NOTIFICATION": {
"OFFLINE": "Offline"
"OFFLINE": "Offline",
"RECONNECTING": "Reconnecting...",
"RECONNECT_SUCCESS": "Reconnected"
},
"BUTTON": {
"REFRESH": "Refresh"
@@ -154,7 +156,7 @@
"UNTIL_TOMORROW": "Until tomorrow",
"UNTIL_NEXT_MONTH": "Until next month",
"AN_HOUR_FROM_NOW": "Until an hour from now",
"CUSTOM": "Custom...",
"UNTIL_CUSTOM_TIME": "Custom...",
"CHANGE_APPEARANCE": "Change Appearance",
"LIGHT_MODE": "Light",
"DARK_MODE": "Dark",
@@ -2,6 +2,8 @@
"INBOX_MGMT": {
"HEADER": "Inboxes",
"SIDEBAR_TXT": "<p><b>Inbox</b></p> <p> When you connect a website or a facebook Page to Chatwoot, it is called an <b>Inbox</b>. You can have unlimited inboxes in your Chatwoot account. </p><p> Click on <b>Add Inbox</b> to connect a website or a Facebook Page. </p><p> In the Dashboard, you can see all the conversations from all your inboxes in a single place and respond to them under the `Conversations` tab. </p><p> You can also see conversations specific to an inbox by clicking on the inbox name on the left pane of the dashboard. </p>",
"RECONNECTION_REQUIRED": "Your inbox is disconnected. You won't receive new messages until you reauthorize it.",
"CLICK_TO_RECONNECT": "Click here to reconnect.",
"LIST": {
"404": "There are no inboxes attached to this account."
},
@@ -364,6 +366,7 @@
"TITLE": "Microsoft Email",
"DESCRIPTION": "Click on the Sign in with Microsoft button to get started. You will redirected to the email sign in page. Once you accept the requested permissions, you would be redirected back to the inbox creation step.",
"EMAIL_PLACEHOLDER": "Enter email address",
"SIGN_IN": "Sign in with Microsoft",
"HELP": "To add your Microsoft account as a channel, you need to authenticate your Microsoft account by clicking on 'Sign in with Microsoft' ",
"ERROR_MESSAGE": "There was an error connecting to Microsoft, please try again"
}
@@ -209,6 +209,87 @@
"API_SUCCESS": "Dashboard app deleted successfully",
"API_ERROR": "We couldn't delete the app. Please try again later"
}
},
"LINEAR": {
"ADD_OR_LINK_BUTTON": "Create/Link Linear Issue",
"LOADING": "Fetching linear issues...",
"LOADING_ERROR": "There was an error fetching the linear issues, please try again",
"CREATE": "Create",
"LINK": {
"SEARCH": "Search issues",
"SELECT": "Select issue",
"TITLE": "Link",
"EMPTY_LIST": "No linear issues found",
"LOADING": "Loading",
"ERROR": "There was an error fetching the linear issues, please try again",
"LINK_SUCCESS": "Issue linked successfully",
"LINK_ERROR": "There was an error linking the issue, please try again",
"LINK_TITLE": "Conversation (#%{conversationId}) with %{name}"
},
"ADD_OR_LINK": {
"TITLE": "Create/link linear issue",
"DESCRIPTION": "Create Linear issues from conversations, or link existing ones for seamless tracking.",
"FORM": {
"TITLE": {
"LABEL": "Title",
"PLACEHOLDER": "Enter title",
"REQUIRED_ERROR": "Title is required"
},
"DESCRIPTION": {
"LABEL": "Description",
"PLACEHOLDER": "Enter description"
},
"TEAM": {
"LABEL": "Team",
"PLACEHOLDER": "Select team",
"SEARCH": "Search team",
"REQUIRED_ERROR": "Team is required"
},
"ASSIGNEE": {
"LABEL": "Assignee",
"PLACEHOLDER": "Select assignee",
"SEARCH": "Search assignee"
},
"PRIORITY": {
"LABEL": "Priority",
"PLACEHOLDER": "Select priority",
"SEARCH": "Search priority"
},
"LABEL": {
"LABEL": "Label",
"PLACEHOLDER": "Select label",
"SEARCH": "Search label"
},
"STATUS": {
"LABEL": "Status",
"PLACEHOLDER": "Select status",
"SEARCH": "Search status"
},
"PROJECT": {
"LABEL": "Project",
"PLACEHOLDER": "Select project",
"SEARCH": "Search project"
}
},
"CREATE": "Create",
"CANCEL": "Cancel",
"CREATE_SUCCESS": "Issue created successfully",
"CREATE_ERROR": "There was an error creating the issue, please try again",
"LOADING_TEAM_ERROR": "There was an error fetching the teams, please try again",
"LOADING_TEAM_ENTITIES_ERROR": "There was an error fetching the team entities, please try again"
},
"ISSUE": {
"STATUS": "Status",
"PRIORITY": "Priority",
"ASSIGNEE": "Assignee",
"LABELS": "Labels",
"CREATED_AT": "Created at %{createdAt}"
},
"UNLINK": {
"TITLE": "Unlink",
"SUCCESS": "Issue unlinked successfully",
"ERROR": "There was an error unlinking the issue, please try again"
}
}
}
}
@@ -278,7 +278,7 @@
"SLA": "SLA",
"BETA": "Beta",
"REPORTS_OVERVIEW": "Overview",
"FACEBOOK_REAUTHORIZE": "Your Facebook connection has expired, please reconnect your Facebook page to continue services",
"REAUTHORIZE": "Your inbox connection has expired, please reconnect\n to continue receiving and sending messages",
"HELP_CENTER": {
"TITLE": "Help Center",
"ALL_ARTICLES": "All Articles",
@@ -1,17 +1,17 @@
{
"FILTER": {
"TITLE": "تصفية المحادثات",
"SUBTITLE": "Add your filters below and hit 'Apply filters' to cut through the chat clutter.",
"EDIT_CUSTOM_FILTER": "Edit Folder",
"CUSTOM_VIEWS_SUBTITLE": "Add or remove filters and update your folder.",
"ADD_NEW_FILTER": "Add filter",
"FILTER_DELETE_ERROR": "Oops, looks like we can't save nothing! Please add at least one filter to save it.",
"SUBTITLE": "أضف عوامل التصفية أدناه واضغط على 'تطبيق الفلترة' لتجنب الفوضى في المحادثات.",
"EDIT_CUSTOM_FILTER": "تحرير المجلد",
"CUSTOM_VIEWS_SUBTITLE": "أضف أو أزل الفلاتر وقم بتحديث المجلد الخاص بك.",
"ADD_NEW_FILTER": "إضافة فلتر",
"FILTER_DELETE_ERROR": "عذرًا، يبدو أننا لا نستطيع حفظ أي شيء! يرجى إضافة فلتر واحد على الأقل لحفظه.",
"SUBMIT_BUTTON_LABEL": "تطبيق عامل التصفية",
"UPDATE_BUTTON_LABEL": "Update folder",
"UPDATE_BUTTON_LABEL": "تحديث المجلد",
"CANCEL_BUTTON_LABEL": "إلغاء",
"CLEAR_BUTTON_LABEL": "Clear filters",
"FOLDER_LABEL": "Folder Name",
"FOLDER_QUERY_LABEL": "Folder Query",
"CLEAR_BUTTON_LABEL": "مسح الفلاتر",
"FOLDER_LABEL": "اسم المجلد",
"FOLDER_QUERY_LABEL": "استعلام المجلد",
"EMPTY_VALUE_ERROR": "القيمة مطلوبة.",
"TOOLTIP_LABEL": "تصفية المحادثات",
"QUERY_DROPDOWN_LABELS": {
@@ -36,15 +36,15 @@
},
"ATTRIBUTES": {
"STATUS": "الحالة",
"ASSIGNEE_NAME": "Assignee name",
"ASSIGNEE_NAME": "اسم المستلم",
"INBOX_NAME": "اسم صندوق الوارد",
"TEAM_NAME": "اسم الفريق",
"CONVERSATION_IDENTIFIER": "Conversation identifier",
"CAMPAIGN_NAME": "Campaign name",
"CONVERSATION_IDENTIFIER": "معرف المحادثة",
"CAMPAIGN_NAME": "اسم الحملة",
"LABELS": "الوسوم",
"BROWSER_LANGUAGE": "Browser language",
"BROWSER_LANGUAGE": "لغة المتصفح",
"PRIORITY": "الأولوية",
"COUNTRY_NAME": "Country name",
"COUNTRY_NAME": "اسم الدولة",
"REFERER_LINK": "رابط المرجع",
"CUSTOM_ATTRIBUTE_LIST": "القائمة",
"CUSTOM_ATTRIBUTE_TEXT": "النص",
@@ -55,15 +55,15 @@
"LAST_ACTIVITY": "النشاط الأخير"
},
"GROUPS": {
"STANDARD_FILTERS": "Standard filters",
"ADDITIONAL_FILTERS": "Additional filters",
"CUSTOM_ATTRIBUTES": "Custom attributes"
"STANDARD_FILTERS": "الفلاتر القياسية",
"ADDITIONAL_FILTERS": "الفلاتر الإضافية",
"CUSTOM_ATTRIBUTES": "الخصائص المخصصة"
},
"CUSTOM_VIEWS": {
"ADD": {
"TITLE": "هل تريد حفظ هذا الفلتر؟",
"LABEL": "تسمية هذا الفلتر",
"PLACEHOLDER": "Name your filter to refer it later.",
"PLACEHOLDER": "قم بتسمية الفلتر لتتمكن من الرجوع إليه لاحقًا.",
"ERROR_MESSAGE": "الاسم مطلوب.",
"SAVE_BUTTON": "حفظ الفلتر",
"CANCEL_BUTTON": "إلغاء",
@@ -77,7 +77,7 @@
}
},
"EDIT": {
"EDIT_BUTTON": "Edit folder"
"EDIT_BUTTON": "تحرير المجلد"
},
"DELETE": {
"DELETE_BUTTON": "حذف الفلتر",
@@ -85,7 +85,7 @@
"CONFIRM": {
"TITLE": "تأكيد الحذف",
"MESSAGE": "هل أنت متأكد من حذف الفلتر ",
"YES": "Yes, delete",
"YES": "نعم، احذف",
"NO": "لا، احتفظ به"
}
},
@@ -1,73 +1,73 @@
{
"AGENT_BOTS": {
"HEADER": "Bots",
"LOADING_EDITOR": "Loading editor...",
"HEADER_BTN_TXT": "Add bot configuration",
"SIDEBAR_TXT": "<p><b>Agent Bots</b> <p>Agent Bots are like the most fabulous members of your team. They can handle the small stuff, so you can focus on the stuff that matters. Give them a try.</p> <p> You can manage your bots from this page or create new ones using the 'Add bot configuraton' button.</p> <p> Open the <a href=\"https://www.chatwoot.com/hc/chatwoot-user-guide-cloud-version/articles/1677497472-how-to-use-agent-bots\" target=\"blank\">Agent bots handbook</a> in another tab for a helping hand.</p>",
"HEADER": "البوتات",
"LOADING_EDITOR": "جارٍ تحميل المحرر...",
"HEADER_BTN_TXT": "إضافة تكوين بوت",
"SIDEBAR_TXT": "<p><b>البوتات</b> <p>البوتات هي مثل الأعضاء الأكثر روعة في فريقك. يمكنهم التعامل مع الأمور الصغيرة، حتى تتمكن من التركيز على الأمور التي تهمك. جربهم.</p> <p> يمكنك إدارة البوتات الخاصة بك من هذه الصفحة أو إنشاء بوتات جديدة باستخدام زر 'إضافة تكوين بوت'.</p> <p> افتح <a href=\"https://www.chatwoot.com/hc/chatwoot-user-guide-cloud-version/articles/1677497472-how-to-use-agent-bots\" target=\"blank\">دليل البوتات</a> في علامة تبويب أخرى للحصول على مساعدة.</p>",
"CSML_BOT_EDITOR": {
"NAME": {
"LABEL": "Bot name",
"PLACEHOLDER": "Name your bot.",
"ERROR": "Bot name is required."
"LABEL": "اسم البوت",
"PLACEHOLDER": "قم بتسمية البوت الخاص بك.",
"ERROR": "اسم البوت مطلوب."
},
"DESCRIPTION": {
"LABEL": "Bot description",
"PLACEHOLDER": "What does this bot do?"
"LABEL": "وصف البوت",
"PLACEHOLDER": "ماذا يفعل هذا البوت؟"
},
"BOT_CONFIG": {
"ERROR": "Please enter your CSML bot configuration above.",
"API_ERROR": "Your CSML configuration is invalid. Please fix it and try again."
"ERROR": "يرجى إدخال تكوين بوت CSML الخاص بك أعلاه.",
"API_ERROR": "تكوين CSML الخاص بك غير صالح. يرجى إصلاحه والمحاولة مرة أخرى."
},
"SUBMIT": "Validate and save"
"SUBMIT": "التحقق والحفظ"
},
"BOT_CONFIGURATION": {
"TITLE": "Select an agent bot",
"DESC": "Assign an Agent Bot to your inbox. They can handle initial conversations and transfer them to a live agent when necessary.",
"TITLE": "اختر بوت",
"DESC": "قم بتعيين بوت لصندوق الوارد الخاص بك. يمكنهم التعامل مع المحادثات الأولية ونقلها إلى وكيل مباشر عند الضرورة.",
"SUBMIT": "تحديث",
"DISCONNECT": "Disconnect bot",
"SUCCESS_MESSAGE": "Successfully updated the agent bot.",
"DISCONNECTED_SUCCESS_MESSAGE": "Successfully disconnected the agent bot.",
"ERROR_MESSAGE": "Could not update the agent bot. Please try again.",
"DISCONNECTED_ERROR_MESSAGE": "Could not disconnect the agent bot. Please try again.",
"SELECT_PLACEHOLDER": "Select bot"
"DISCONNECT": "فصل البوت",
"SUCCESS_MESSAGE": "تم تحديث البوت بنجاح.",
"DISCONNECTED_SUCCESS_MESSAGE": "تم فصل البوت بنجاح.",
"ERROR_MESSAGE": "تعذر تحديث البوت. يرجى المحاولة مرة أخرى.",
"DISCONNECTED_ERROR_MESSAGE": "تعذر فصل البوت. يرجى المحاولة مرة أخرى.",
"SELECT_PLACEHOLDER": "اختر بوت"
},
"ADD": {
"TITLE": "Configure new bot",
"TITLE": "تكوين بوت جديد",
"CANCEL_BUTTON_TEXT": "إلغاء",
"API": {
"SUCCESS_MESSAGE": "Bot added successfully.",
"ERROR_MESSAGE": "Could not add bot. Please try again later."
"SUCCESS_MESSAGE": "تمت إضافة البوت بنجاح.",
"ERROR_MESSAGE": "تعذر إضافة البوت. يرجى المحاولة مرة أخرى لاحقًا."
}
},
"LIST": {
"404": "No bots found. You can create a bot by clicking the 'Configure new bot' button ↗",
"LOADING": "Fetching bots...",
"TYPE": "Bot type"
"404": "لم يتم العثور على أي بوتات. يمكنك إنشاء بوت بالنقر على زر 'تكوين بوت جديد' ↗",
"LOADING": "جارٍ جلب البوتات...",
"TYPE": "نوع البوت"
},
"DELETE": {
"BUTTON_TEXT": "حذف",
"TITLE": "Delete bot",
"TITLE": "حذف البوت",
"SUBMIT": "حذف",
"CANCEL_BUTTON_TEXT": "إلغاء",
"DESCRIPTION": "Are you sure you want to delete this bot? This action is irreversible.",
"DESCRIPTION": "هل أنت متأكد أنك تريد حذف هذا البوت؟ هذا الإجراء لا يمكن التراجع عنه.",
"API": {
"SUCCESS_MESSAGE": "Bot deleted successfully.",
"ERROR_MESSAGE": "Could not delete bot. Please try again."
"SUCCESS_MESSAGE": "تم حذف البوت بنجاح.",
"ERROR_MESSAGE": "تعذر حذف البوت. يرجى المحاولة مرة أخرى."
}
},
"EDIT": {
"BUTTON_TEXT": "تعديل",
"LOADING": "Fetching bots...",
"TITLE": "Edit bot",
"LOADING": "جارٍ جلب البوتات...",
"TITLE": "تعديل البوت",
"CANCEL_BUTTON_TEXT": "إلغاء",
"API": {
"SUCCESS_MESSAGE": "Bot updated successfully.",
"ERROR_MESSAGE": "Could not update bot. Please try again."
"SUCCESS_MESSAGE": "تم تحديث البوت بنجاح.",
"ERROR_MESSAGE": "تعذر تحديث البوت. يرجى المحاولة مرة أخرى."
}
},
"TYPES": {
"WEBHOOK": "Webhook bot",
"CSML": "CSML bot"
"WEBHOOK": "بوت Webhook",
"CSML": "بوت CSML"
}
}
}
@@ -41,15 +41,15 @@
"IN_VALID": "مفتاح غير صالح"
},
"REGEX_PATTERN": {
"LABEL": "Regex Pattern",
"PLACEHOLDER": "Please enter custom attribute regex pattern. (Optional)"
"LABEL": "نمط Regex",
"PLACEHOLDER": "الرجاء إدخال سمة مخصصة ريمكس (اختياري)"
},
"REGEX_CUE": {
"LABEL": "Regex Cue",
"PLACEHOLDER": "Please enter regex pattern hint. (Optional)"
"PLACEHOLDER": "الرجاء إدخال تلميح نمط regex (اختياري)"
},
"ENABLE_REGEX": {
"LABEL": "Enable regex validation"
"LABEL": "تمكين التحقق من صحة regex"
}
},
"API": {
@@ -106,15 +106,15 @@
"NOT_FOUND": "لا توجد سمات مخصصة تم تكوينها"
},
"REGEX_PATTERN": {
"LABEL": "Regex Pattern",
"PLACEHOLDER": "Please enter custom attribute regex pattern. (Optional)"
"LABEL": "نمط Regex",
"PLACEHOLDER": "الرجاء إدخال سمة مخصصة ريمكس (اختياري)"
},
"REGEX_CUE": {
"LABEL": "Regex Cue",
"PLACEHOLDER": "Please enter regex pattern hint. (Optional)"
"PLACEHOLDER": "الرجاء إدخال تلميح نمط regex (اختياري)"
},
"ENABLE_REGEX": {
"LABEL": "Enable regex validation"
"LABEL": "تمكين التحقق من صحة regex"
}
}
}
@@ -1,71 +1,71 @@
{
"AUDIT_LOGS": {
"HEADER": "Audit Logs",
"HEADER_BTN_TXT": "Add Audit Logs",
"LOADING": "Fetching Audit Logs",
"HEADER": "سجلات التدقيق",
"HEADER_BTN_TXT": "إضافة سجلات التدقيق",
"LOADING": "جارٍ جلب سجلات التدقيق",
"SEARCH_404": "لا توجد عناصر مطابقة لهذا الاستعلام",
"SIDEBAR_TXT": "<p><b>Audit Logs</b> </p><p> Audit Logs are trails for events and actions in a Chatwoot System. </p>",
"SIDEBAR_TXT": "<p><b>سجلات التدقيق</b> </p><p> سجلات التدقيق هي مسارات للأحداث والإجراءات في نظام Chatwoot. </p>",
"LIST": {
"404": "There are no Audit Logs available in this account.",
"TITLE": "Manage Audit Logs",
"DESC": "Audit Logs are trails for events and actions in a Chatwoot System.",
"404": "لا توجد سجلات تدقيق متاحة في هذا الحساب.",
"TITLE": "إدارة سجلات التدقيق",
"DESC": "سجلات التدقيق هي مسارات للأحداث والإجراءات في نظام Chatwoot.",
"TABLE_HEADER": [
"User",
"Action",
"المستخدم",
"الإجراء",
"عنوان IP"
]
},
"API": {
"SUCCESS_MESSAGE": "AuditLogs retrieved successfully",
"SUCCESS_MESSAGE": "تم استرجاع سجلات التدقيق بنجاح",
"ERROR_MESSAGE": "تعذر الاتصال بالخادم، الرجاء المحاولة مرة أخرى لاحقاً"
},
"DEFAULT_USER": "System",
"DEFAULT_USER": "النظام",
"AUTOMATION_RULE": {
"ADD": "%{agentName} created a new automation rule (#%{id})",
"EDIT": "%{agentName} updated an automation rule (#%{id})",
"DELETE": "%{agentName} deleted an automation rule (#%{id})"
"ADD": "%{agentName} أنشأ قاعدة أتمتة جديدة (#%{id})",
"EDIT": "%{agentName} قام بتحديث قاعدة أتمتة (#%{id})",
"DELETE": "%{agentName} حذف قاعدة أتمتة (#%{id})"
},
"ACCOUNT_USER": {
"ADD": "%{agentName} invited %{invitee} to the account as an %{role}",
"ADD": "%{agentName} دعا %{invitee} إلى الحساب كـ %{role}",
"EDIT": {
"SELF": "%{agentName} changed their %{attributes} to %{values}",
"OTHER": "%{agentName} changed %{attributes} of %{user} to %{values}"
"SELF": "%{agentName} غير %{attributes} الخاصة به إلى %{values}",
"OTHER": "%{agentName} غير %{attributes} لـ %{user} إلى %{values}"
}
},
"INBOX": {
"ADD": "%{agentName} created a new inbox (#%{id})",
"EDIT": "%{agentName} updated an inbox (#%{id})",
"DELETE": "%{agentName} deleted an inbox (#%{id})"
"ADD": "%{agentName} أنشأ صندوق وارد جديد (#%{id})",
"EDIT": "%{agentName} قام بتحديث صندوق الوارد (#%{id})",
"DELETE": "%{agentName} حذف صندوق الوارد (#%{id})"
},
"WEBHOOK": {
"ADD": "%{agentName} created a new webhook (#%{id})",
"EDIT": "%{agentName} updated a webhook (#%{id})",
"DELETE": "%{agentName} deleted a webhook (#%{id})"
"ADD": "%{agentName} أنشأ Webhook جديد (#%{id})",
"EDIT": "%{agentName} قام بتحديث Webhook (#%{id})",
"DELETE": "%{agentName} حذف Webhook (#%{id})"
},
"USER_ACTION": {
"SIGN_IN": "%{agentName} signed in",
"SIGN_OUT": "%{agentName} signed out"
"SIGN_IN": "%{agentName} قام بتسجيل الدخول",
"SIGN_OUT": "%{agentName} قام بتسجيل الخروج"
},
"TEAM": {
"ADD": "%{agentName} created a new team (#%{id})",
"EDIT": "%{agentName} updated a team (#%{id})",
"DELETE": "%{agentName} deleted a team (#%{id})"
"ADD": "%{agentName} أنشأ فريق جديد (#%{id})",
"EDIT": "%{agentName} قام بتحديث الفريق (#%{id})",
"DELETE": "%{agentName} حذف الفريق (#%{id})"
},
"MACRO": {
"ADD": "%{agentName} created a new macro (#%{id})",
"EDIT": "%{agentName} updated a macro (#%{id})",
"DELETE": "%{agentName} deleted a macro (#%{id})"
"ADD": "%{agentName} أنشأ ماكرو جديد (#%{id})",
"EDIT": "%{agentName} قام بتحديث ماكرو (#%{id})",
"DELETE": "%{agentName} حذف ماكرو (#%{id})"
},
"INBOX_MEMBER": {
"ADD": "%{agentName} added %{user} to the inbox(#%{inbox_id})",
"REMOVE": "%{agentName} removed %{user} from the inbox(#%{inbox_id})"
"ADD": "%{agentName} أضاف %{user} إلى صندوق الوارد (#%{inbox_id})",
"REMOVE": "%{agentName} أزال %{user} من صندوق الوارد (#%{inbox_id})"
},
"TEAM_MEMBER": {
"ADD": "%{agentName} added %{user} to the team(#%{team_id})",
"REMOVE": "%{agentName} removed %{user} from the team(#%{team_id})"
"ADD": "%{agentName} أضاف %{user} إلى الفريق (#%{team_id})",
"REMOVE": "%{agentName} أزال %{user} من الفريق (#%{team_id})"
},
"ACCOUNT": {
"EDIT": "%{agentName} updated the account configuration (#%{id})"
"EDIT": "%{agentName} قام بتحديث إعدادات الحساب (#%{id})"
}
}
}
@@ -10,23 +10,23 @@
"ASSIGN_AGENT_TOOLTIP": "تعيين وكيل",
"ASSIGN_TEAM_TOOLTIP": "تعيين فريق",
"ASSIGN_SUCCESFUL": "تم تعيين المحادثات بنجاح.",
"ASSIGN_FAILED": "Failed to assign conversations. Please try again.",
"ASSIGN_FAILED": "فشل في تعيين المحادثات، الرجاء المحاولة مرة أخرى.",
"RESOLVE_SUCCESFUL": "تم تسوية المحادثات بنجاح.",
"RESOLVE_FAILED": "Failed to resolve conversations. Please try again.",
"RESOLVE_FAILED": "فشل في حل المحادثات، يرجى المحاولة مرة أخرى.",
"ALL_CONVERSATIONS_SELECTED_ALERT": "المحادثات المرئية في هذه الصفحة هي المحددة فقط.",
"AGENT_LIST_LOADING": "جاري تحميل الوكلاء",
"UPDATE": {
"CHANGE_STATUS": "تغيير الحالة",
"SNOOZE_UNTIL": "غفوة",
"UPDATE_SUCCESFUL": "تم تحديث حالة المحادثة بنجاح.",
"UPDATE_FAILED": "Failed to update conversations. Please try again."
"UPDATE_FAILED": "فشل تحديث المحادثات، الرجاء المحاولة مرة أخرى."
},
"LABELS": {
"ASSIGN_LABELS": "Assign labels",
"ASSIGN_LABELS": "إضافة وسم",
"NO_LABELS_FOUND": "لم يتم العثور على تسميات لـ",
"ASSIGN_SELECTED_LABELS": "تعيين التسميات المحددة",
"ASSIGN_SUCCESFUL": "تم تعيين التسميات بنجاح.",
"ASSIGN_FAILED": "Failed to assign labels. Please try again."
"ASSIGN_FAILED": "فشل في تعيين التسميات ، الرجاء المحاولة مرة أخرى."
},
"TEAMS": {
"TEAM_SELECT_LABEL": "اختيار فريق",
@@ -34,7 +34,7 @@
"NO_TEAMS_AVAILABLE": "لا توجد فرق مضافة إلى هذا الحساب حتى الآن.",
"ASSIGN_SELECTED_TEAMS": "تعيين فريق محدد.",
"ASSIGN_SUCCESFUL": "تم تعيين الفرق بنجاح.",
"ASSIGN_FAILED": "Failed to assign team. Please try again."
"ASSIGN_FAILED": "فشل تعيين الفريق، الرجاء المحاولة مرة أخرى."
}
}
}
@@ -4,11 +4,11 @@
"HEADER_BTN_TXT": "إضافة رد جاهز",
"LOADING": "جاري جلب الردود الجاهزة...",
"SEARCH_404": "لا توجد عناصر مطابقة لهذا الاستعلام.",
"SIDEBAR_TXT": "<p><b>Canned Responses</b> </p><p> Canned Responses are pre-written reply templates that help you quickly respond to a conversation. To insert a canned response during a chat, agents can type a short code preceded by a '/' character. </p><p> You can manage your canned responses from this page or create new ones using the \"Add canned response\" button.</p><p>Open the <a target=\"_blank\" href=\"https://www.chatwoot.com/hc/chatwoot-user-guide-cloud-version/articles/1677501325-how-to-create-saved-reply-templates-with-canned-responses\">Canned Responses handbook</a> in another tab for a helping hand.</p><p>Also, check out the all-new <a href=\"https://www.chatwoot.com/tools/canned-responses-library\" target=\"_blank\">Canned Responses Library</a>.</p>",
"SIDEBAR_TXT": "<p><b>الردود المسبقة</b> </p></p><p> الردود المسبقة هي قوالب رد مكتوبة مسبقاً تساعدك على الرد بسرعة على محادثة. لإدراج استجابة مسبقة أثناء الدردشة، يمكن للوكلاء كتابة رمز قصير يسبقه حرف '/'. </p><p> يمكنك إدارة ردودك المعلبة من هذه الصفحة أو إنشاء استجابات جديدة باستخدام زر \"إضافة استجابة مسبقة\".</p><p>افتح دليل الردود المسبقة <a target=\"_blank\" href=\"https://www.chatwoot.com/hc/chatwoot-user-guide-cloud-version/articles/1677501325-how-to-create-saved-reply-templates-with-canned-responses\"></a> في علامة تبويب أخرى للحصول على يد المساعدة.</p><p>أيضا، تحقق من كل المكتبة <a href=\"https://www.chatwoot.com/tools/canned-responses-library\" target=\"_blank\">الردود المسبقة</a>.</p>",
"LIST": {
"404": "لا توجد ردود جاهزة متوفرة في هذا الحساب.",
"TITLE": "إدارة الردود الجاهزة",
"DESC": "Canned Responses are predefined reply templates which can be used to quickly send out replies to conversations.",
"DESC": "الردود الجاهزة هي قوالب رسائل معدة مسبقاً يمكن استخدامها لتسريع كتابة الردود في المحادثات.",
"TABLE_HEADER": [
"كود مختصر",
"المحتوى",
@@ -17,7 +17,7 @@
},
"ADD": {
"TITLE": "إضافة رد جاهز",
"DESC": "Canned Responses are predefined reply templates which can be used to quickly send out replies to conversations.",
"DESC": "الردود الجاهزة هي قوالب رسائل معدة مسبقاً يمكن استخدامها لتسريع كتابة الردود في المحادثات.",
"CANCEL_BUTTON_TEXT": "إلغاء",
"FORM": {
"SHORT_CODE": {
@@ -59,22 +59,22 @@
"TEXT": "اخر نشاط: الأحدث أولا"
},
"created_at_desc": {
"TEXT": "Created at: Newest first"
"TEXT": "تم إنشاؤها في: الأحدث أولاً"
},
"created_at_asc": {
"TEXT": "Created at: Oldest first"
"TEXT": "تم إنشاؤها في: أقدم أولاً"
},
"priority_desc": {
"TEXT": "Priority: Highest first"
"TEXT": "الأولوية: الأعلى أولا"
},
"priority_asc": {
"TEXT": "Priority: Lowest first"
"TEXT": "الأولوية: الأقل أولاً"
},
"waiting_since_asc": {
"TEXT": "Pending Response: Longest first"
"TEXT": "الرد المعلق: الأطول أولا"
},
"waiting_since_desc": {
"TEXT": "Pending Response: Shortest first"
"TEXT": "الرد المعلق: أقصر أولاً"
}
},
"ATTACHMENTS": {
@@ -110,7 +110,7 @@
"LABEL": "تم إنشاؤها في"
},
"LAST_USER_MESSAGE_AT": {
"NAME": "Last user message at",
"NAME": "آخر رسالة للمستخدم في",
"LABEL": "أخر رسالة"
}
}
@@ -34,14 +34,14 @@
"TITLE": "اضافة تصنيف جديد",
"PLACEHOLDER": "ابحث عن تصنيفات",
"NO_RESULT": "لم يتم العثور على تصنيفات",
"CREATE_LABEL": "Create new label"
"CREATE_LABEL": "إنشاء تسمية جديدة"
}
},
"MERGE_CONTACT": "دمج جهة الاتصال",
"CONTACT_ACTIONS": "إجراءات جهات الاتصال",
"MUTE_CONTACT": "احجب جهة الاتصال",
"UNMUTE_CONTACT": "إلغاء حجب جهة الاتصال",
"MUTED_SUCCESS": "This contact is blocked successfully. You will not be notified of any future conversations.",
"MUTED_SUCCESS": "تم حظر جهة الاتصال هذه بنجاح، لن يتم إعلامك بأي محادثات في المستقبل.",
"UNMUTED_SUCCESS": "تم إلغاء حجب جهة الاتصال بنجاح.",
"SEND_TRANSCRIPT": "إرسال النص",
"EDIT_LABEL": "تعديل",
@@ -72,7 +72,7 @@
"SUBMIT": "استيراد",
"CANCEL": "إلغاء"
},
"SUCCESS_MESSAGE": "You will be notified via email when the import is complete.",
"SUCCESS_MESSAGE": "سيتم إعلامك عبر البريد الإلكتروني عند اكتمال الاستيراد.",
"ERROR_MESSAGE": "حدث خطأ، الرجاء المحاولة مرة أخرى"
},
"EXPORT_CONTACTS": {
@@ -83,8 +83,8 @@
"ERROR_MESSAGE": "حدث خطأ، الرجاء المحاولة مرة أخرى",
"CONFIRM": {
"TITLE": "تصدير جهات الاتصال",
"MESSAGE": "Are you sure you want to export all contacts?",
"FILTERED_MESSAGE": "Are you sure you want to export all the filtered contacts?",
"MESSAGE": "هل أنت متأكد من أنك تريد تصدير جميع جهات الاتصال؟",
"FILTERED_MESSAGE": "هل أنت متأكد من أنك تريد تصدير جميع جهات الاتصال التي تم تصفيتها؟",
"YES": "نعم, قم بالتصدير",
"NO": "نعم, إلغاء"
}
@@ -138,7 +138,7 @@
"LABEL": "رقم الهاتف",
"HELP": "يجب ان يحتوى رقم الهاتف على كود دولتك تسبقها علامة +\nمثال: +20101243567",
"ERROR": "يجب ان تكون خانة رقم الهاتف إما فارغة او مكتملة مع رمز الدولة",
"DIAL_CODE_ERROR": "Please select a dial code from the list",
"DIAL_CODE_ERROR": "الرجاء تحديد رمز الاتصال من القائمة",
"DUPLICATE": "رقم الهاتف هذا مستخدم لجهة اتصال أخرى."
},
"LOCATION": {
@@ -213,8 +213,8 @@
"ERROR": "لا يمكن أن تكون الرسالة فارغة"
},
"ATTACHMENTS": {
"SELECT": "Choose files",
"HELP_TEXT": "Drag and drop files here or choose files to attach"
"SELECT": "اختر الملفات",
"HELP_TEXT": "قم بسحب وإسقاط الملفات هنا أو اختر ملفات لإرفاقها"
},
"SUBMIT": "إرسال الرسالة",
"CANCEL": "إلغاء",
@@ -231,7 +231,7 @@
"FILTER_CONTACTS": "فلترة",
"FILTER_CONTACTS_SAVE": "حفظ الفلتر",
"FILTER_CONTACTS_DELETE": "حذف الفلتر",
"FILTER_CONTACTS_EDIT": "Edit segment",
"FILTER_CONTACTS_EDIT": "تحرير الجزء",
"LIST": {
"LOADING_MESSAGE": "جاري تحميل جهات الاتصال...",
"404": "لا توجد جهات اتصال تطابق بحثك 🔍",
@@ -298,8 +298,8 @@
"BUTTON": "إضافة سمة خاصة",
"NOT_AVAILABLE": "لا توجد سمات مخصصة متاحة لجهة الاتصال هذه.",
"COPY_SUCCESSFUL": "تم النسخ إلى الحافظة بنجاح",
"SHOW_MORE": "Show all attributes",
"SHOW_LESS": "Show less attributes",
"SHOW_MORE": "إظهار كافة السمات",
"SHOW_LESS": "عرض سمات أقل",
"ACTIONS": {
"COPY": "نسخ السمة",
"DELETE": "حذف السمة",
@@ -350,7 +350,7 @@
"VALIDATIONS": {
"REQUIRED": "القيمة الصالحة مطلوبة",
"INVALID_URL": "عنوان URL غير صالح",
"INVALID_INPUT": "Invalid Input"
"INVALID_INPUT": "مدخلات غير صالح"
}
},
"MERGE_CONTACTS": {
@@ -2,18 +2,18 @@
"CONTACTS_FILTER": {
"TITLE": "تصفية جهات الاتصال",
"SUBTITLE": "إضافة فلاتر أدناه واضغط على 'إرسال' لتصفية جهات الاتصال.",
"EDIT_CUSTOM_SEGMENT": "Edit Segment",
"CUSTOM_VIEWS_SUBTITLE": "Add or remove filters and update your segment.",
"EDIT_CUSTOM_SEGMENT": "تحرير الجزء",
"CUSTOM_VIEWS_SUBTITLE": "أضف أو أزل الفلاتر وقم بتحديث المجلد الخاص بك.",
"ADD_NEW_FILTER": "إضافة فلتر",
"CLEAR_ALL_FILTERS": "مسح جميع الفلاتر",
"FILTER_DELETE_ERROR": "يجب ان يكون لديك فلتر واحد على الاقل",
"SUBMIT_BUTTON_LABEL": "إرسال",
"UPDATE_BUTTON_LABEL": "Update Segment",
"UPDATE_BUTTON_LABEL": "تحديث الجزء",
"CANCEL_BUTTON_LABEL": "إلغاء",
"CLEAR_BUTTON_LABEL": "مسح الفلاتر",
"EMPTY_VALUE_ERROR": "القيمة مطلوبة",
"SEGMENT_LABEL": "Segment Name",
"SEGMENT_QUERY_LABEL": "Segment Query",
"SEGMENT_LABEL": "اسم القسم",
"SEGMENT_QUERY_LABEL": "استعلام الجزء",
"TOOLTIP_LABEL": "تصفية جهات الاتصال",
"QUERY_DROPDOWN_LABELS": {
"AND": "و",
@@ -45,7 +45,7 @@
"CREATED_AT": "تم إنشاؤها في",
"LAST_ACTIVITY": "آخر نشاط",
"REFERER_LINK": "رابط المرجع",
"BLOCKED": "Blocked"
"BLOCKED": "محظور"
},
"GROUPS": {
"STANDARD_FILTERS": "الفلاتر القياسية",
@@ -13,8 +13,8 @@
"NO_INBOX_AGENT": "يبدو أنه لم يتم إسنادك لأي قنوات تواصل بعد. الرجاء التواصل مع المدير لإضافتك لصناديق الوارد الخاصة بقنوات التواصل",
"SEARCH_MESSAGES": "البحث عن رسائل في المحادثات",
"EMPTY_STATE": {
"CMD_BAR": "to open command menu",
"KEYBOARD_SHORTCUTS": "to view keyboard shortcuts"
"CMD_BAR": "لفتح قائمة الأوامر",
"KEYBOARD_SHORTCUTS": "لعرض اختصارات لوحة المفاتيح"
},
"SEARCH": {
"TITLE": "البحث في الرسائل",
@@ -40,16 +40,16 @@
"UNKNOWN_FILE_TYPE": "ملف غير معروف",
"SAVE_CONTACT": "حفظ",
"UPLOADING_ATTACHMENTS": "جاري تحميل المرفقات...",
"REPLIED_TO_STORY": "Replied to your story",
"UNSUPPORTED_MESSAGE": "This message is unsupported.",
"UNSUPPORTED_MESSAGE_FACEBOOK": "This message is unsupported. You can view this message on the Facebook Messenger app.",
"UNSUPPORTED_MESSAGE_INSTAGRAM": "This message is unsupported. You can view this message on the Instagram app.",
"REPLIED_TO_STORY": "رد على قصتك",
"UNSUPPORTED_MESSAGE": "هذه الرسالة غير مدعومة.",
"UNSUPPORTED_MESSAGE_FACEBOOK": "هذه الرسالة غير مدعومة، يمكنك مشاهدة هذه الرسالة على تطبيق فيسبوك (Messenger).",
"UNSUPPORTED_MESSAGE_INSTAGRAM": "هذه الرسالة غير مدعومة، يمكنك عرض هذه الرسالة على تطبيق Instagram.",
"SUCCESS_DELETE_MESSAGE": "تم حذف الرسالة بنجاح",
"FAIL_DELETE_MESSSAGE": "تعذر حذف الرسالة! حاول مرة أخرى",
"NO_RESPONSE": "لا توجد استجابة",
"RATING_TITLE": "التقييم",
"FEEDBACK_TITLE": "الملاحظات",
"REPLY_MESSAGE_NOT_FOUND": "Message not available",
"REPLY_MESSAGE_NOT_FOUND": "الرسالة غير متوفرة",
"CARD": {
"SHOW_LABELS": "إظهار السمات",
"HIDE_LABELS": "إخفاء السمات"
@@ -61,7 +61,7 @@
"OPEN": "المزيد",
"CLOSE": "أغلق",
"DETAILS": "التفاصيل",
"SNOOZED_UNTIL": "Snoozed until",
"SNOOZED_UNTIL": "غفوة حتى",
"SNOOZED_UNTIL_TOMORROW": "غفوة حتى الغد",
"SNOOZED_UNTIL_NEXT_WEEK": "غفوة حتى الأسبوع القادم",
"SNOOZED_UNTIL_NEXT_REPLY": "غفوة حتى الرد التالي",
@@ -69,8 +69,8 @@
"FRT": "FRT {status}",
"NRT": "NRT {status}",
"RT": "RT {status}",
"MISSED": "missed",
"DUE": "due"
"MISSED": "مفقود",
"DUE": "مستحق"
}
},
"RESOLVE_DROPDOWN": {
@@ -92,17 +92,17 @@
"TITLE": "الأولوية",
"OPTIONS": {
"NONE": "لا شيء",
"URGENT": "Urgent",
"HIGH": "High",
"MEDIUM": "Medium",
"LOW": "Low"
"URGENT": "عاجل",
"HIGH": "عالية",
"MEDIUM": "متوسطة",
"LOW": "منخفضة"
},
"CHANGE_PRIORITY": {
"SELECT_PLACEHOLDER": "لا شيء",
"INPUT_PLACEHOLDER": "Select priority",
"INPUT_PLACEHOLDER": "تحديد الأولوية",
"NO_RESULTS": "لم يتم العثور على النتائج",
"SUCCESSFUL": "Changed priority of conversation id %{conversationId} to %{priority}",
"FAILED": "Couldn't change priority. Please try again."
"SUCCESSFUL": "تغيير أولوية معرف المحادثة %{conversationId} إلى %{priority}",
"FAILED": "تعذر تغيير الأولوية، الرجاء المحاولة مرة أخرى."
}
},
"CARD_CONTEXT_MENU": {
@@ -150,8 +150,8 @@
"SEND": "إرسال",
"CREATE": "إضافة ملاحظة",
"INSERT_READ_MORE": "اقرأ المزيد",
"DISMISS_REPLY": "Dismiss reply",
"REPLYING_TO": "Replying to:",
"DISMISS_REPLY": "تجاهل الرد",
"REPLYING_TO": "الرد على:",
"TIP_FORMAT_ICON": "عرض محرر النصوص",
"TIP_EMOJI_ICON": "إظهار قائمة الرموز التعبيرية",
"TIP_ATTACH_ICON": "إرفاق الملفات",
@@ -163,7 +163,7 @@
"STOP_AUDIO_RECORDING": "إيقاف التسجيل الصوتي",
"": "",
"EMAIL_HEAD": {
"TO": "TO",
"TO": "إلى",
"ADD_BCC": "إضافة bcc",
"CC": {
"LABEL": "CC",
@@ -206,15 +206,15 @@
},
"CONTEXT_MENU": {
"COPY": "نسخ",
"REPLY_TO": "Reply to this message",
"REPLY_TO": "الرد على هذه الرسالة",
"DELETE": "حذف",
"CREATE_A_CANNED_RESPONSE": "إضافة إلى الردود السريعة",
"TRANSLATE": "ترجم",
"COPY_PERMALINK": "Copy link to the message",
"LINK_COPIED": "Message URL copied to the clipboard",
"COPY_PERMALINK": "نسخ الرابط إلى الرسالة",
"LINK_COPIED": "تم نسخ عنوان URL للرسالة إلى الحافظة",
"DELETE_CONFIRMATION": {
"TITLE": "Are you sure you want to delete this message?",
"MESSAGE": "You cannot undo this action",
"TITLE": "هل أنت متأكد من أنك تريد حذف هذه الرسالة؟",
"MESSAGE": "لا يمكنك التراجع عن هذا الإجراء",
"DELETE": "حذف",
"CANCEL": "إلغاء"
}
@@ -1,13 +1,13 @@
{
"DATE_PICKER": {
"APPLY_BUTTON": "تطبيق",
"CLEAR_BUTTON": "Clear",
"CLEAR_BUTTON": "مسح",
"DATE_RANGE_INPUT": {
"START": "Start Date",
"END": "End Date"
"START": "تاريخ البدء",
"END": "تاريخ الانتهاء"
},
"DATE_RANGE_OPTIONS": {
"TITLE": "DATE RANGE",
"TITLE": "نطاق التاريخ",
"LAST_7_DAYS": "آخر 7 أيام",
"LAST_30_DAYS": "آخر 30 يوماً",
"LAST_3_MONTHS": "آخر 3 أشهر",
@@ -50,8 +50,8 @@
},
"UPDATE_CHATWOOT": "يتوفر تحديث %{latestChatwootVersion} لـ Chatwoot. الرجاء التحديث.",
"LEARN_MORE": "اعرف المزيد",
"PAYMENT_PENDING": "Your payment is pending. Please update your payment information to continue using Chatwoot",
"LIMITS_UPGRADE": "Your account has exceeded the usage limits, please upgrade your plan to continue using Chatwoot",
"PAYMENT_PENDING": "الدفعة الخاصة بك معلقة. الرجاء تحديث معلومات الدفع الخاصة بك للاستمرار في استخدام Chatwoot",
"LIMITS_UPGRADE": "لقد تجاوز حسابك حدود الاستخدام، يرجى ترقية خطتك للاستمرار في استخدام Chatwoot",
"OPEN_BILLING": "فتح الفواتير"
},
"FORMS": {
@@ -88,14 +88,16 @@
"assigned_conversation_new_message": "رسالة جديدة",
"participating_conversation_new_message": "رسالة جديدة",
"conversation_mention": "إشارة",
"sla_missed_first_response": "SLA Missed",
"sla_missed_next_response": "SLA Missed",
"sla_missed_resolution": "SLA Missed"
"sla_missed_first_response": "فشل اتفاقية مستوى الخدمة",
"sla_missed_next_response": "فشل اتفاقية مستوى الخدمة",
"sla_missed_resolution": "فشل اتفاقية مستوى الخدمة"
}
},
"NETWORK": {
"NOTIFICATION": {
"OFFLINE": "غير متصل"
"OFFLINE": "غير متصل",
"RECONNECTING": "Reconnecting...",
"RECONNECT_SUCCESS": "Reconnected"
},
"BUTTON": {
"REFRESH": "تحديث"
@@ -107,7 +109,7 @@
"GENERAL": "عام",
"REPORTS": "التقارير",
"CONVERSATION": "المحادثات",
"BULK_ACTIONS": "Bulk Actions",
"BULK_ACTIONS": "الإجراءات الشاملة",
"CHANGE_ASSIGNEE": "تغيير المحال إليه",
"CHANGE_PRIORITY": "تغيير الأولوية",
"CHANGE_TEAM": "تغيير الفريق",
@@ -115,7 +117,7 @@
"ADD_LABEL": "إضافة تسمية إلى المحادثة",
"REMOVE_LABEL": "إزالة التسمية من المحادثة",
"SETTINGS": "الإعدادات",
"AI_ASSIST": "AI Assist",
"AI_ASSIST": "مساعدة AI",
"APPEARANCE": "مظهر",
"SNOOZE_NOTIFICATION": "تأجيل التنبيهات"
},
@@ -139,7 +141,7 @@
"GO_TO_NOTIFICATIONS": "الذهاب إلى الإشعارات",
"ADD_LABELS_TO_CONVERSATION": "إضافة تسمية إلى المحادثة",
"ASSIGN_AN_AGENT": "تعيين وكيل",
"AI_ASSIST": "AI Assist",
"AI_ASSIST": "مساعدة AI",
"ASSIGN_PRIORITY": "تعيين الأولوية",
"ASSIGN_A_TEAM": "تعيين فريق",
"MUTE_CONVERSATION": "كتم المحادثة",
@@ -154,9 +156,9 @@
"UNTIL_TOMORROW": "حتى الغد",
"UNTIL_NEXT_MONTH": "حتي الشهر القادم",
"AN_HOUR_FROM_NOW": "حتي ساعة من الأن",
"CUSTOM": "مخصص...",
"UNTIL_CUSTOM_TIME": "مخصص...",
"CHANGE_APPEARANCE": "تغيير المظهر",
"LIGHT_MODE": "Light",
"LIGHT_MODE": "فاتح",
"DARK_MODE": "مظلم",
"SYSTEM_MODE": "نظام",
"SNOOZE_NOTIFICATION": "تأجيل التنبيهات"
@@ -456,7 +456,7 @@
"BACK_RESULTS": "Back to results"
},
"UPGRADE_PAGE": {
"TITLE": "Help Center",
"TITLE": "مركز المساعدة",
"DESCRIPTION": "Create user-friendly self-service portals. Help your users to access the articles and get support 24/7. Upgrade your subscription to enable this feature.",
"SELF_HOSTED_DESCRIPTION": "Create user-friendly self-service portals. Help your users to access the articles and get support 24/7. Please contact your administrator to enable this feature.",
"BUTTON": {
@@ -2,63 +2,63 @@
"INBOX": {
"LIST": {
"TITLE": "صندوق الوارد",
"DISPLAY_DROPDOWN": "Display",
"LOADING": "Fetching notifications",
"404": "There are no active notifications in this group.",
"NO_NOTIFICATIONS": "No notifications",
"NOTE": "Notifications from all subscribed inboxes",
"NO_MESSAGES_AVAILABLE": "Oops! Not able to fetch messages",
"SNOOZED_UNTIL": "Snoozed until",
"DISPLAY_DROPDOWN": "عرض",
"LOADING": "جارٍ جلب الإشعارات",
"404": "لا توجد إشعارات نشطة في هذه المجموعة.",
"NO_NOTIFICATIONS": "لا توجد إشعارات",
"NOTE": "الإشعارات من جميع صناديق الوارد المشترك بها",
"NO_MESSAGES_AVAILABLE": "عذرًا! لا يمكن جلب الرسائل",
"SNOOZED_UNTIL": "غفوة حتى",
"SNOOZED_UNTIL_TOMORROW": "غفوة حتى الغد",
"SNOOZED_UNTIL_NEXT_WEEK": "غفوة حتى الأسبوع القادم"
},
"ACTION_HEADER": {
"SNOOZE": "Snooze notification",
"DELETE": "Delete notification",
"SNOOZE": "غفوة الإشعار",
"DELETE": "حذف الإشعار",
"BACK": "العودة"
},
"TYPES": {
"CONVERSATION_MENTION": "You have been mentioned in a conversation",
"CONVERSATION_CREATION": "New conversation created",
"CONVERSATION_ASSIGNMENT": "A conversation has been assigned to you",
"ASSIGNED_CONVERSATION_NEW_MESSAGE": "New message in an assigned conversation",
"PARTICIPATING_CONVERSATION_NEW_MESSAGE": "New message in a conversation you are participating in",
"SLA_MISSED_FIRST_RESPONSE": "SLA target first response missed for conversation",
"SLA_MISSED_NEXT_RESPONSE": "SLA target next response missed for conversation",
"SLA_MISSED_RESOLUTION": "SLA target resolution missed for conversation"
"CONVERSATION_MENTION": "تم ذكرك في محادثة",
"CONVERSATION_CREATION": "تم إنشاء محادثة جديدة",
"CONVERSATION_ASSIGNMENT": "تم تعيين محادثة لك",
"ASSIGNED_CONVERSATION_NEW_MESSAGE": "رسالة جديدة في محادثة معينة لك",
"PARTICIPATING_CONVERSATION_NEW_MESSAGE": "رسالة جديدة في محادثة تشارك فيها",
"SLA_MISSED_FIRST_RESPONSE": "تم تجاوز الهدف الأول للرد للمحادثة",
"SLA_MISSED_NEXT_RESPONSE": "تم تجاوز الهدف التالي للرد للمحادثة",
"SLA_MISSED_RESOLUTION": "تم تجاوز الهدف النهائي للحل للمحادثة"
},
"MENU_ITEM": {
"MARK_AS_READ": "Mark as read",
"MARK_AS_READ": "وضع علامة كمقروء",
"MARK_AS_UNREAD": "وضع علامة كغير مقروء",
"SNOOZE": "غفوة",
"DELETE": "حذف",
"MARK_ALL_READ": "تحديد الكل كمقروء",
"DELETE_ALL": "Delete all",
"DELETE_ALL_READ": "Delete all read"
"DELETE_ALL": "حذف الكل",
"DELETE_ALL_READ": "حذف كل المقروء"
},
"DISPLAY_MENU": {
"SORT": "Sort",
"DISPLAY": "Display :",
"SORT": "فرز",
"DISPLAY": "عرض :",
"SORT_OPTIONS": {
"NEWEST": "Newest",
"OLDEST": "Oldest",
"NEWEST": "الأحدث",
"OLDEST": "الأقدم",
"PRIORITY": "الأولوية"
},
"DISPLAY_OPTIONS": {
"SNOOZED": "غفوة",
"READ": "قرائة",
"READ": "قراءة",
"LABELS": "الوسوم",
"CONVERSATION_ID": "Conversation ID"
"CONVERSATION_ID": "معرف المحادثة"
}
},
"ALERTS": {
"MARK_AS_READ": "Notification marked as read",
"MARK_AS_UNREAD": "Notification marked as unread",
"SNOOZE": "Notification snoozed",
"DELETE": "Notification deleted",
"MARK_ALL_READ": "All notifications marked as read",
"DELETE_ALL": "All notifications deleted",
"DELETE_ALL_READ": "All read notifications deleted"
"MARK_AS_READ": "تم وضع علامة الإشعار كمقروء",
"MARK_AS_UNREAD": "تم وضع علامة الإشعار كغير مقروء",
"SNOOZE": "تم تأجيل الإشعار",
"DELETE": "تم حذف الإشعار",
"MARK_ALL_READ": "تم وضع علامة كل الإشعارات كمقروء",
"DELETE_ALL": "تم حذف كل الإشعارات",
"DELETE_ALL_READ": "تم حذف كل الإشعارات المقروءة"
}
}
}
@@ -2,6 +2,8 @@
"INBOX_MGMT": {
"HEADER": "قنوات التواصل",
"SIDEBAR_TXT": "<p><b>قنوات التواصل</b></p> <p> عند ربطك لموقع ويب أو صفحة فيسبوك إلى Chatwoot، يتم تسميتها <b>قناة تواصل</b>. يمكنك إنشاء قنوات تواصل غير محدودة من مختلف الأنواع في حساب Chatwoot الخاص بك. </p><p> انقر فوق <b>إضافة قناة تواصل</b> لربط موقع الويب أو صفحة فيسبوك الخاصة بك. </p><p> من لوحة الإدارة، يمكنك رؤية جميع المحادثات من جميع صناديق الوارد الخاصة بك والرد عليها من مكان موّحد عبر الضغط على علامة التبويب \"المحادثات\". </p><p> يمكنك أيضًا مشاهدة المحادثات الخاصة بصندوق وارد معين بالنقر على اسم صندوق الوارد على الجزء الجانبي من لوحة الإدارة. </p>",
"RECONNECTION_REQUIRED": "Your inbox is disconnected. You won't receive new messages until you reauthorize it.",
"CLICK_TO_RECONNECT": "Click here to reconnect.",
"LIST": {
"404": "لا توجد صناديق وارد لقنوات تواصل مرتبطة بهذا الحساب."
},
@@ -364,6 +366,7 @@
"TITLE": "Microsoft Email",
"DESCRIPTION": "Click on the Sign in with Microsoft button to get started. You will redirected to the email sign in page. Once you accept the requested permissions, you would be redirected back to the inbox creation step.",
"EMAIL_PLACEHOLDER": "Enter email address",
"SIGN_IN": "Sign in with Microsoft",
"HELP": "To add your Microsoft account as a channel, you need to authenticate your Microsoft account by clicking on 'Sign in with Microsoft' ",
"ERROR_MESSAGE": "There was an error connecting to Microsoft, please try again"
}
@@ -518,7 +521,7 @@
"UPDATE_PRE_CHAT_FORM_SETTINGS": "Update Pre Chat Form Settings"
},
"HELP_CENTER": {
"LABEL": "Help Center",
"LABEL": "مركز المساعدة",
"PLACEHOLDER": "Select Help Center",
"SELECT_PLACEHOLDER": "Select Help Center",
"REMOVE": "Remove Help Center",
@@ -209,6 +209,87 @@
"API_SUCCESS": "تم حذف تطبيق لوحة التحكم بنجاح",
"API_ERROR": "لم نتمكن من حذف التطبيق. الرجاء المحاولة مرة أخرى لاحقاً"
}
},
"LINEAR": {
"ADD_OR_LINK_BUTTON": "Create/Link Linear Issue",
"LOADING": "Fetching linear issues...",
"LOADING_ERROR": "There was an error fetching the linear issues, please try again",
"CREATE": "إنشاء",
"LINK": {
"SEARCH": "Search issues",
"SELECT": "Select issue",
"TITLE": "الرابط",
"EMPTY_LIST": "No linear issues found",
"LOADING": "جار التحميل",
"ERROR": "There was an error fetching the linear issues, please try again",
"LINK_SUCCESS": "Issue linked successfully",
"LINK_ERROR": "There was an error linking the issue, please try again",
"LINK_TITLE": "Conversation (#%{conversationId}) with %{name}"
},
"ADD_OR_LINK": {
"TITLE": "Create/link linear issue",
"DESCRIPTION": "Create Linear issues from conversations, or link existing ones for seamless tracking.",
"FORM": {
"TITLE": {
"LABEL": "العنوان",
"PLACEHOLDER": "Enter title",
"REQUIRED_ERROR": "العنوان مطلوب"
},
"DESCRIPTION": {
"LABEL": "الوصف",
"PLACEHOLDER": "Enter description"
},
"TEAM": {
"LABEL": "الفريق",
"PLACEHOLDER": "اختيار فريق",
"SEARCH": "Search team",
"REQUIRED_ERROR": "Team is required"
},
"ASSIGNEE": {
"LABEL": "Assignee",
"PLACEHOLDER": "Select assignee",
"SEARCH": "Search assignee"
},
"PRIORITY": {
"LABEL": "الأولوية",
"PLACEHOLDER": "تحديد الأولوية",
"SEARCH": "Search priority"
},
"LABEL": {
"LABEL": "الوسم",
"PLACEHOLDER": "Select label",
"SEARCH": "Search label"
},
"STATUS": {
"LABEL": "الحالة",
"PLACEHOLDER": "Select status",
"SEARCH": "Search status"
},
"PROJECT": {
"LABEL": "Project",
"PLACEHOLDER": "Select project",
"SEARCH": "Search project"
}
},
"CREATE": "إنشاء",
"CANCEL": "إلغاء",
"CREATE_SUCCESS": "Issue created successfully",
"CREATE_ERROR": "There was an error creating the issue, please try again",
"LOADING_TEAM_ERROR": "There was an error fetching the teams, please try again",
"LOADING_TEAM_ENTITIES_ERROR": "There was an error fetching the team entities, please try again"
},
"ISSUE": {
"STATUS": "الحالة",
"PRIORITY": "الأولوية",
"ASSIGNEE": "Assignee",
"LABELS": "الوسوم",
"CREATED_AT": "Created at %{createdAt}"
},
"UNLINK": {
"TITLE": "Unlink",
"SUCCESS": "Issue unlinked successfully",
"ERROR": "There was an error unlinking the issue, please try again"
}
}
}
}
@@ -517,7 +517,7 @@
"DOWNLOAD_SLA_REPORTS": "Download SLA reports",
"DOWNLOAD_FAILED": "Failed to download SLA Reports",
"DROPDOWN": {
"ADD_FIlTER": "Add filter",
"ADD_FIlTER": "إضافة فلتر",
"CLEAR_ALL": "Clear all",
"CLEAR_FILTER": "Clear filter",
"EMPTY_LIST": "لم يتم العثور على النتائج",
@@ -278,9 +278,9 @@
"SLA": "SLA",
"BETA": "تجريبي",
"REPORTS_OVERVIEW": "نظرة عامة",
"FACEBOOK_REAUTHORIZE": "انتهت صلاحية اتصال الفيسبوك الخاص بك، يرجى إعادة الاتصال بصفحة الفيسبوك الخاصة بك لمواصلة الخدمات",
"REAUTHORIZE": "Your inbox connection has expired, please reconnect\n to continue receiving and sending messages",
"HELP_CENTER": {
"TITLE": "Help Center",
"TITLE": "مركز المساعدة",
"ALL_ARTICLES": "جميع المقالات",
"MY_ARTICLES": "مقالاتي",
"DRAFT": "مسودة",
@@ -33,7 +33,7 @@
"NONE": "Нито един",
"NO_TEAMS_AVAILABLE": "There are no teams added to this account yet.",
"ASSIGN_SELECTED_TEAMS": "Assign selected team.",
"ASSIGN_SUCCESFUL": "Teams assiged successfully.",
"ASSIGN_SUCCESFUL": "Teams assigned successfully.",
"ASSIGN_FAILED": "Failed to assign team. Please try again."
}
}
@@ -95,7 +95,9 @@
},
"NETWORK": {
"NOTIFICATION": {
"OFFLINE": "Offline"
"OFFLINE": "Offline",
"RECONNECTING": "Reconnecting...",
"RECONNECT_SUCCESS": "Reconnected"
},
"BUTTON": {
"REFRESH": "Refresh"
@@ -154,7 +156,7 @@
"UNTIL_TOMORROW": "Until tomorrow",
"UNTIL_NEXT_MONTH": "Until next month",
"AN_HOUR_FROM_NOW": "Until an hour from now",
"CUSTOM": "Custom...",
"UNTIL_CUSTOM_TIME": "Custom...",
"CHANGE_APPEARANCE": "Change Appearance",
"LIGHT_MODE": "Light",
"DARK_MODE": "Dark",
@@ -2,6 +2,8 @@
"INBOX_MGMT": {
"HEADER": "Inboxes",
"SIDEBAR_TXT": "<p><b>Inbox</b></p> <p> When you connect a website or a facebook Page to Chatwoot, it is called an <b>Inbox</b>. You can have unlimited inboxes in your Chatwoot account. </p><p> Click on <b>Add Inbox</b> to connect a website or a Facebook Page. </p><p> In the Dashboard, you can see all the conversations from all your inboxes in a single place and respond to them under the `Conversations` tab. </p><p> You can also see conversations specific to an inbox by clicking on the inbox name on the left pane of the dashboard. </p>",
"RECONNECTION_REQUIRED": "Your inbox is disconnected. You won't receive new messages until you reauthorize it.",
"CLICK_TO_RECONNECT": "Click here to reconnect.",
"LIST": {
"404": "There are no inboxes attached to this account."
},
@@ -364,6 +366,7 @@
"TITLE": "Microsoft Email",
"DESCRIPTION": "Click on the Sign in with Microsoft button to get started. You will redirected to the email sign in page. Once you accept the requested permissions, you would be redirected back to the inbox creation step.",
"EMAIL_PLACEHOLDER": "Enter email address",
"SIGN_IN": "Sign in with Microsoft",
"HELP": "To add your Microsoft account as a channel, you need to authenticate your Microsoft account by clicking on 'Sign in with Microsoft' ",
"ERROR_MESSAGE": "There was an error connecting to Microsoft, please try again"
}
@@ -209,6 +209,87 @@
"API_SUCCESS": "Dashboard app deleted successfully",
"API_ERROR": "We couldn't delete the app. Please try again later"
}
},
"LINEAR": {
"ADD_OR_LINK_BUTTON": "Create/Link Linear Issue",
"LOADING": "Fetching linear issues...",
"LOADING_ERROR": "There was an error fetching the linear issues, please try again",
"CREATE": "Създаване",
"LINK": {
"SEARCH": "Search issues",
"SELECT": "Select issue",
"TITLE": "Link",
"EMPTY_LIST": "No linear issues found",
"LOADING": "Loading",
"ERROR": "There was an error fetching the linear issues, please try again",
"LINK_SUCCESS": "Issue linked successfully",
"LINK_ERROR": "There was an error linking the issue, please try again",
"LINK_TITLE": "Conversation (#%{conversationId}) with %{name}"
},
"ADD_OR_LINK": {
"TITLE": "Create/link linear issue",
"DESCRIPTION": "Create Linear issues from conversations, or link existing ones for seamless tracking.",
"FORM": {
"TITLE": {
"LABEL": "Title",
"PLACEHOLDER": "Enter title",
"REQUIRED_ERROR": "Title is required"
},
"DESCRIPTION": {
"LABEL": "Описание",
"PLACEHOLDER": "Enter description"
},
"TEAM": {
"LABEL": "Team",
"PLACEHOLDER": "Изберете екип",
"SEARCH": "Search team",
"REQUIRED_ERROR": "Team is required"
},
"ASSIGNEE": {
"LABEL": "Assignee",
"PLACEHOLDER": "Select assignee",
"SEARCH": "Search assignee"
},
"PRIORITY": {
"LABEL": "Priority",
"PLACEHOLDER": "Select priority",
"SEARCH": "Search priority"
},
"LABEL": {
"LABEL": "Label",
"PLACEHOLDER": "Select label",
"SEARCH": "Search label"
},
"STATUS": {
"LABEL": "Статус",
"PLACEHOLDER": "Select status",
"SEARCH": "Search status"
},
"PROJECT": {
"LABEL": "Project",
"PLACEHOLDER": "Select project",
"SEARCH": "Search project"
}
},
"CREATE": "Създаване",
"CANCEL": "Отмени",
"CREATE_SUCCESS": "Issue created successfully",
"CREATE_ERROR": "There was an error creating the issue, please try again",
"LOADING_TEAM_ERROR": "There was an error fetching the teams, please try again",
"LOADING_TEAM_ENTITIES_ERROR": "There was an error fetching the team entities, please try again"
},
"ISSUE": {
"STATUS": "Статус",
"PRIORITY": "Priority",
"ASSIGNEE": "Assignee",
"LABELS": "Етикети",
"CREATED_AT": "Created at %{createdAt}"
},
"UNLINK": {
"TITLE": "Unlink",
"SUCCESS": "Issue unlinked successfully",
"ERROR": "There was an error unlinking the issue, please try again"
}
}
}
}
@@ -278,7 +278,7 @@
"SLA": "SLA",
"BETA": "Beta",
"REPORTS_OVERVIEW": "Overview",
"FACEBOOK_REAUTHORIZE": "Your Facebook connection has expired, please reconnect your Facebook page to continue services",
"REAUTHORIZE": "Your inbox connection has expired, please reconnect\n to continue receiving and sending messages",
"HELP_CENTER": {
"TITLE": "Help Center",
"ALL_ARTICLES": "All Articles",
@@ -2,67 +2,67 @@
"AGENT_BOTS": {
"HEADER": "Bots",
"LOADING_EDITOR": "S'està carregant l'editor...",
"HEADER_BTN_TXT": "Add bot configuration",
"SIDEBAR_TXT": "<p><b>Agent Bots</b> <p>Agent Bots are like the most fabulous members of your team. They can handle the small stuff, so you can focus on the stuff that matters. Give them a try.</p> <p> You can manage your bots from this page or create new ones using the 'Add bot configuraton' button.</p> <p> Open the <a href=\"https://www.chatwoot.com/hc/chatwoot-user-guide-cloud-version/articles/1677497472-how-to-use-agent-bots\" target=\"blank\">Agent bots handbook</a> in another tab for a helping hand.</p>",
"HEADER_BTN_TXT": "Afegir configuració del bot",
"SIDEBAR_TXT": "<p><b>Bots d'Agent</b> <p>Bots d'Agent són com els membres més fabulosos del teu equip. Poden gestionar les coses petites, de manera que us podeu centrar en les coses importants. Prova-ls.</p> <p> Pots gestionar els teus bots des d'aquesta pàgina o crear-ne de nous mitjançant el botó \"Afegeix una configuració de bot\".</p> <p> Obre el <a href=\"https:/ /www.chatwoot.com/hc/chatwoot-user-guide-cloud-version/articles/1677497472-how-to-use-agent-bots\" target=\"blank\">Manual de robots d'agent</a> en una altra pestanya per a un cop de mà.</p>",
"CSML_BOT_EDITOR": {
"NAME": {
"LABEL": "Bot name",
"PLACEHOLDER": "Name your bot.",
"ERROR": "Bot name is required."
"LABEL": "Nom del bot",
"PLACEHOLDER": "Anomena el teu bot.",
"ERROR": "El nom del bot és obligatori."
},
"DESCRIPTION": {
"LABEL": "Bot description",
"PLACEHOLDER": "What does this bot do?"
"LABEL": "Descripció del bot",
"PLACEHOLDER": "Què fa aquest bot?"
},
"BOT_CONFIG": {
"ERROR": "Please enter your CSML bot configuration above.",
"API_ERROR": "Your CSML configuration is invalid. Please fix it and try again."
"ERROR": "Introdueix la configuració del bot CSML més amunt.",
"API_ERROR": "La vostra configuració CSML no és vàlida. Arregla-ho i torna-ho a provar."
},
"SUBMIT": "Validate and save"
"SUBMIT": "Valida i desa"
},
"BOT_CONFIGURATION": {
"TITLE": "Select an agent bot",
"DESC": "Assign an Agent Bot to your inbox. They can handle initial conversations and transfer them to a live agent when necessary.",
"TITLE": "Selecciona un bot d'agent",
"DESC": "Assigna un bot d'agent a la teva safata d'entrada. Poden gestionar les converses inicials i transferir-les a un agent en directe quan sigui necessari.",
"SUBMIT": "Actualitza",
"DISCONNECT": "Disconnect bot",
"SUCCESS_MESSAGE": "Successfully updated the agent bot.",
"DISCONNECTED_SUCCESS_MESSAGE": "Successfully disconnected the agent bot.",
"ERROR_MESSAGE": "Could not update the agent bot. Please try again.",
"DISCONNECTED_ERROR_MESSAGE": "Could not disconnect the agent bot. Please try again.",
"SELECT_PLACEHOLDER": "Select bot"
"DISCONNECT": "Desconnecta el bot",
"SUCCESS_MESSAGE": "S'ha actualitzat correctament el bot de l'agent.",
"DISCONNECTED_SUCCESS_MESSAGE": "S'ha desconnectat correctament el bot de l'agent.",
"ERROR_MESSAGE": "No s'ha pogut actualitzar el bot de l'agent. Torneu-ho a provar.",
"DISCONNECTED_ERROR_MESSAGE": "No s'ha pogut desconnectar el bot de l'agent. Torneu-ho a provar.",
"SELECT_PLACEHOLDER": "Selecciona el bot"
},
"ADD": {
"TITLE": "Configure new bot",
"TITLE": "Configura el nou bot",
"CANCEL_BUTTON_TEXT": "Cancel·la",
"API": {
"SUCCESS_MESSAGE": "Bot added successfully.",
"ERROR_MESSAGE": "Could not add bot. Please try again later."
"SUCCESS_MESSAGE": "Bot afegit correctament.",
"ERROR_MESSAGE": "No s'ha pogut afegir el bot. Torneu-ho a provar més endavant."
}
},
"LIST": {
"404": "No bots found. You can create a bot by clicking the 'Configure new bot' button ↗",
"LOADING": "Fetching bots...",
"TYPE": "Bot type"
"404": "No s'han trobat bots. Pots crear un bot fent clic al botó \"Configura un bot nou\" ↗",
"LOADING": "S'estan obtenint bots...",
"TYPE": "Tipus de bot"
},
"DELETE": {
"BUTTON_TEXT": "Esborrar",
"TITLE": "Delete bot",
"TITLE": "Suprimeix el bot",
"SUBMIT": "Esborrar",
"CANCEL_BUTTON_TEXT": "Cancel·la",
"DESCRIPTION": "Are you sure you want to delete this bot? This action is irreversible.",
"DESCRIPTION": "Estàs segur que vols suprimir aquest bot? Aquesta acció és irreversible.",
"API": {
"SUCCESS_MESSAGE": "Bot deleted successfully.",
"ERROR_MESSAGE": "Could not delete bot. Please try again."
"SUCCESS_MESSAGE": "S'ha esborrat el bot correctament.",
"ERROR_MESSAGE": "No s'ha pogut eliminar el bot. Torneu-ho a provar més endavant."
}
},
"EDIT": {
"BUTTON_TEXT": "Edita",
"LOADING": "Fetching bots...",
"TITLE": "Edit bot",
"LOADING": "S'estan obtenint bots...",
"TITLE": "Edita el bot",
"CANCEL_BUTTON_TEXT": "Cancel·la",
"API": {
"SUCCESS_MESSAGE": "Bot updated successfully.",
"ERROR_MESSAGE": "Could not update bot. Please try again."
"SUCCESS_MESSAGE": "Bot actualitzat correctament.",
"ERROR_MESSAGE": "No s'ha pogut actualitzar el bot. Torneu-ho a provar."
}
},
"TYPES": {

Some files were not shown because too many files have changed in this diff Show More