Compare commits

...
93 Commits
Author SHA1 Message Date
Sojan 2605f24539 Merge branch 'release/2.3.2' 2022-03-23 19:06:45 +05:30
Sojan dd76dd4204 Bump version to 2.3.2 2022-03-23 19:05:34 +05:30
86b4183bde fix: Add multiple file paste support and fix for bugs (#4066)
- Add multiple files paste support.
- Fixes showing file name in the editor field when we paste the file from finder.
- Fixes showing the image in the advance editor when we paste the image as an attachment from the clipboard.

Fixes: #4036

Co-authored-by: Nithin David <1277421+nithindavid@users.noreply.github.com>
2022-03-23 18:25:57 +05:30
Jordan BroughandGitHub 4a21633a2b chore: Use bang version of "find_or_create_by!" in Platform UsersController (#4248) 2022-03-23 16:09:10 +05:30
Sojan JoseandGitHub f8c46341f6 chore: Disable invalid contact resolved activity messages (#4253)
Fixes the bug which triggered "Contact resolved conversation" activity messages when snoozed conversations where reopened
2022-03-23 16:08:42 +05:30
Pranav Raj SandGitHub 771cb5b9fc fix: Update scroll event name to fix auto-scroll (#4251)
Fixes #3622
2022-03-23 13:27:50 +05:30
Sojan JoseandGitHub f43018e017 chore: New translations automation.json (Italian) (#4231) 2022-03-22 12:42:10 +05:30
Sojan JoseandGitHub 467f3b9191 chore: Disable fetching new emails after mailbox error (#4176)
- Disabled email fetch job if credentials for the channel isn't working
- notify customers when the email channel isn't working

fixes: https://github.com/chatwoot/chatwoot/issues/4174
2022-03-22 12:14:17 +05:30
Muhsin KelothandGitHub 715400f7ab fix: Disable pre-chat fields for identified users (#4234) 2022-03-22 01:28:22 +05:30
Tejaswini ChileandGitHub 7b4cac746a feat: enhancement to un-send instagram message (#4120) 2022-03-21 17:10:15 +05:30
Sojan JoseandGitHub 6169d172d8 chore: Update translations from Crowdin (#4217) 2022-03-21 13:15:33 +05:30
Tejaswini ChileandGitHub e06a6a7c55 Automation enhancement (#4087) 2022-03-21 13:12:27 +05:30
c608ca37e0 fix: Contact conversations are showing in the chat list (#4078)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2022-03-21 11:46:43 +05:30
Pranav Raj SandGitHub 6097f4c122 fix: Update survey locale based on the account (#4207) 2022-03-20 13:07:36 +05:30
Fayaz AhmedandGitHub 8372ae418e System default monospace fonts for code elem (#4222) 2022-03-19 20:56:29 +05:30
Sojan JoseandGitHub 71391d982b chore: filter .xml files for Bandwidth SMS (#4206)
MMS sent from an ATT or Verizon phone number seems to be appending a SMIL file in .xml format for MMS.
2022-03-18 19:00:36 +05:30
b77fad52fb fix: Add missing contact information to fix invalid activity messages (#4195)
Co-authored-by: Tejaswini <tejaswini@chatwoot.com>
2022-03-18 13:43:02 +05:30
Sojan JoseandGitHub 40f31cb0a3 chore: Update translations (#4208) 2022-03-18 11:24:22 +05:30
7f8a199b29 chore: Update translations (#4099)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2022-03-17 20:02:32 +05:30
Muhsin KelothandGitHub 23fe8847d8 feat: Notification action cable event (#4154) 2022-03-17 18:56:36 +05:30
Sojan 430547e026 Merge branch 'hotfix/2.3.1' into develop 2022-03-17 16:55:00 +05:30
Sojan d0702363f4 Merge branch 'hotfix/2.3.1' 2022-03-17 16:54:49 +05:30
Sojan d6d0c79618 Bump version to 2.3.1 2022-03-17 16:53:36 +05:30
Sojan JoseandGitHub 9f8e442333 chore: Improve n+1 queries (#4202)
Fixes the n+1 queries that get triggered while accessing agents endpoint
2022-03-17 15:57:32 +05:30
Pranav Raj SandGitHub 4a3ddad02f fix: Reduce the height of the unread container while campaign is active (#4185) 2022-03-16 14:45:57 +05:30
Tejaswini ChileandGitHub 207a03155e Feat: authenticate direct upload (#4160) 2022-03-16 13:54:18 +05:30
Sojan 796a7805db Merge branch 'release/2.3.0' into develop 2022-03-15 23:04:09 +05:30
Sojan a50ea9c88a Merge branch 'release/2.3.0' 2022-03-15 23:03:59 +05:30
Sojan 711251560b Bump version to 2.3.0 2022-03-15 22:59:54 +05:30
c4837cd7ac feat: End conversation from widget (#3660)
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: Fayaz Ahmed <15716057+fayazara@users.noreply.github.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com>
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2022-03-15 22:07:30 +05:30
Pranav Raj SandGitHub 4b748e2c8c fix: Delete content_attributes in message destroy API (#4184) 2022-03-15 22:01:16 +05:30
8c8c5a77c8 feat: Show banner message if Chatwoot update available (#3999)
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com>
Co-authored-by: Fayaz Ahmed <15716057+fayazara@users.noreply.github.com>
Co-authored-by: Pranav <pranav@chatwoot.com>
2022-03-15 21:09:14 +05:30
Sojan JoseandGitHub 02dd5ecfab chore: 360Dialog attachments fix (#4173)
fixes: #3701
2022-03-15 17:42:21 +05:30
Muhsin KelothandGitHub f405572e5c Revert "feat: Custom fields in pre-chat form (#4135)" (#4182)
This reverts commit e730804b48.
2022-03-15 16:54:33 +05:30
Sojan JoseandGitHub 25876993ff fix: NoMethodError: undefined method `id' for nil:NilClass (#4172)
fixes: #4171
2022-03-15 14:30:33 +05:30
b3ba8b9513 fix: Could not able to create label (#4161)
* fixes: Could not able to create label

* review fixes

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2022-03-14 20:43:56 +05:30
Fayaz AhmedandGitHub 4a2452173e feat: Added support for Created At, Last Activity At filters in the UI (#4031) 2022-03-14 20:24:53 +05:30
Aswin Dev P.SandGitHub c62d74a01d feat: Display trends in report metrics (#4144) 2022-03-14 18:15:27 +05:30
Sivin VargheseandGitHub 5edf0f2bbe fix: Fix modal responsiveness (#4149) 2022-03-14 18:14:16 +05:30
Fayaz AhmedandGitHub dd1fe4f93a fix: Handle unsanitized markup in DOM (#4110) 2022-03-14 18:13:21 +05:30
Muhsin KelothandGitHub e730804b48 feat: Custom fields in pre-chat form (#4135)
Fixes: #2744
2022-03-14 15:06:56 +05:30
Sivin VargheseandGitHub 578414d788 feat: Adds ability to delete contact without having to type name (#4155) 2022-03-11 17:00:55 +05:30
Sivin VargheseandGitHub 619441ce1c fixes: Showing vue warning in the console (#4152)
* fixes: Showing vue warn with PR#4100

* Minor fixes
2022-03-10 22:06:59 +05:30
Nithin David ThomasandGitHub 3c3b6f90c7 fix: Add base_url to the audio notification file (#4116) 2022-03-10 20:40:47 +05:30
Tejaswini ChileandGitHub 647efa12e7 fix: Attach instagram images with file type story_mentions (#4100) 2022-03-10 20:27:30 +05:30
b3545f42f1 fixes: Contact search is skipping the '+' sign for email. (#4142)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2022-03-10 11:15:41 +05:30
Pranav Raj SandGitHub fb03b6f434 chore: Upgrade rails to 6.1.4.7 to fix (#4143)
Fixes CVE-2022-21831
2022-03-10 11:01:40 +05:30
Pranav Raj SandGitHub 1aa7d6f7d8 fix: Update brand name in mailers (#4123) 2022-03-08 09:22:57 +05:30
giquieuandGitHub 96b719017b feat: Ability to send voice message to channel (#4064)
Created the possibility to send audio as an attachment via the dashboard.
The channels that can send audio are the same channels that can send any type of attachment.
Used RecordRTC (https://github.com/muaz-khan/RecordRTC) to capture the audio and Wavesurfer (https://github.com/katspaugh/wavesurfer.js) to display the audio waves.
RecordRTC can be used to record videos if necessary.

Fixes #1973
2022-03-04 19:43:07 +05:30
Aswin Dev P.SandGitHub b94e67f5d7 feat: Add agents filter in CSAT reports (#4106)
* add agents filter in csat reports
2022-03-04 03:49:26 -08:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Pranav Raj S
c76b588850 chore(deps): bump marked from 2.0.3 to 4.0.10 (#3771)
* chore(deps): bump marked from 2.0.3 to 4.0.10

Bumps [marked](https://github.com/markedjs/marked) from 2.0.3 to 4.0.10.
- [Release notes](https://github.com/markedjs/marked/releases)
- [Changelog](https://github.com/markedjs/marked/blob/master/.releaserc.json)
- [Commits](https://github.com/markedjs/marked/compare/v2.0.3...v4.0.10)

---
updated-dependencies:
- dependency-name: marked
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

* use {marked}

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2022-03-04 14:53:40 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Muhsin KelothNithin David Thomas
a28b911132 chore(deps): bump url-parse from 1.5.7 to 1.5.10 (#4076)
Bumps [url-parse](https://github.com/unshiftio/url-parse) from 1.5.7 to 1.5.10.
- [Release notes](https://github.com/unshiftio/url-parse/releases)
- [Commits](https://github.com/unshiftio/url-parse/compare/1.5.7...1.5.10)

---
updated-dependencies:
- dependency-name: url-parse
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com>
2022-03-04 10:58:05 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
6e4ae1a1bc chore(deps): bump commonmarker from 0.23.2 to 0.23.4 (#4108)
Bumps [commonmarker](https://github.com/gjtorikian/commonmarker) from 0.23.2 to 0.23.4.
- [Release notes](https://github.com/gjtorikian/commonmarker/releases)
- [Changelog](https://github.com/gjtorikian/commonmarker/blob/main/CHANGELOG.md)
- [Commits](https://github.com/gjtorikian/commonmarker/compare/v0.23.2...v0.23.4)

---
updated-dependencies:
- dependency-name: commonmarker
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-03-04 10:57:22 +05:30
Pranav Raj SandGitHub 9583a2dbad chore: Pass sso_account_id to select the account during SSO Login (#4103) 2022-03-03 20:49:51 +05:30
Sojan JoseandGitHub 11adfd2384 chore: remove unnecessary dependent destroy (#4104) 2022-03-03 13:02:02 +05:30
Pranav Raj SandGitHub 28d102f526 fix: Show new message screen instead of input field (#4102) 2022-03-03 12:36:31 +05:30
3d7ca61481 Show email signature with maximum height 8vh, fixes #4021 (#4063)
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Fayaz Ahmed <15716057+fayazara@users.noreply.github.com>
Co-authored-by: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com>
2022-03-02 15:28:56 +05:30
Sojan JoseandGitHub 7b9e4982cf Feat: Support MMS in SMS Channel ( Bandwidth ) (#4046)
Ability to send and receive MMS to bandwidth channel

fixes: #3961
2022-03-02 15:09:56 +05:30
4d458c2184 feat: Support respective input types in custom attributes - filters (#4032)
* Support differnt input types in custom attributes

* Handle list type custom attributes

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: Vishnu Narayanan <vishnu@chatwoot.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2022-03-02 14:16:42 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
0582d15eb5 chore(deps): bump image_processing from 1.12.1 to 1.12.2 (#4095)
Bumps [image_processing](https://github.com/janko/image_processing) from 1.12.1 to 1.12.2.
- [Release notes](https://github.com/janko/image_processing/releases)
- [Changelog](https://github.com/janko/image_processing/blob/master/CHANGELOG.md)
- [Commits](https://github.com/janko/image_processing/compare/v1.12.1...v1.12.2)

---
updated-dependencies:
- dependency-name: image_processing
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-03-02 10:31:37 +05:30
915fe3897c chore: Add Github in contact social profiles (#4086)
* Add github in social links

* Code cleanup

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2022-03-01 14:33:26 +05:30
Muhsin KelothandGitHub f08d1b35d0 feat: Toggle automation status (#3991) 2022-03-01 14:14:23 +05:30
a3cb26a317 fix: AudioContext warning when loading widget on Chrome (#3956)
* fix: AudioContext warning when loading widget on Chrome

* minor fixes

* Minor fixes

* adds event on document

* Play audio from parent window through SDK

* Adds notification to dashboard

Co-authored-by: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com>
Co-authored-by: Vishnu Narayanan <vishnu@chatwoot.com>
2022-02-28 21:43:24 +05:30
eee89bf0d8 feat: Show cc from last email on reply editor (#3983)
* Adds last emails to reply editor

* Fixes bug in reply box

* Adds test cases

* Prevents private notes having cc bcc data

* Prevents private notes having cc bcc data

* Init reply head with values

* fix broken tests

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: Fayaz Ahmed <15716057+fayazara@users.noreply.github.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2022-02-28 21:42:50 +05:30
Muhsin KelothandGitHub 1de18391b4 fix: Handle invalid country code in contact details (#4081) 2022-02-28 19:48:13 +05:30
Muhsin KelothandGitHub bb16780278 chore: Upgrade utils to 0.0.5 (#4082) 2022-02-28 18:50:29 +05:30
4260441f8c Chore: clean up Reporting Events (#4044)
Tech debt clean up

Fixes #4057

Co-authored-by: Aswin Dev P S <aswin@chatwoot.com>
2022-02-28 18:16:12 +05:30
12c0be002e feat: Format time in reports (#4072)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2022-02-28 16:54:43 +05:30
Muhsin KelothandGitHub aff14b697f chore: Add webhook URL validation (#4080) 2022-02-28 15:44:02 +05:30
Pranav Raj SandGitHub 87a6266ddc fix: Add resetTriggered flag to fix reset loop (#4079) 2022-02-28 14:53:11 +05:30
Sojan cebb1bf871 Merge branch 'release/2.2.1' into develop 2022-02-28 14:12:36 +05:30
Sojan ed2772dd59 Merge branch 'release/2.2.1' 2022-02-28 14:12:22 +05:30
Sojan 4f19fa763c Bump version to 2.2.1 2022-02-28 14:04:40 +05:30
Muhsin KelothandGitHub 46c1734ba1 feat: Add additional attributes in setUser method (#3958) 2022-02-28 12:10:55 +05:30
9b615f11f1 fix: Consider timezone in the reports (#4027)
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2022-02-28 10:56:24 +05:30
Sojan JoseandGitHub 4ca66c1195 fix: Automation loop on conversation_update (#4071) 2022-02-25 18:01:21 +05:30
Pranav Raj SandGitHub cc74bebc0f feat: Display Account context in the UI (#4069) 2022-02-25 16:36:36 +05:30
2c8a3ef3b2 fix: Message signature going with other channels message (#4024)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2022-02-25 16:36:10 +05:30
Pranav Raj SandGitHub 6c94768bdb feat: Add flat design for widget (#4065) 2022-02-25 16:18:18 +05:30
Nithin David ThomasandGitHub dcecbf4b80 Fix: show empty emails in chat (#4030) 2022-02-24 21:33:16 -08:00
Vishnu NarayananandGitHub d590b0ebc4 fix: nokogiri gem build failure in docker (#4060)
tar (child): xz: Cannot exec: No such file or directory¶

Starting in v1.13.2, the source archive used for libxml2 and libxslt is compressed with xz (previous versions were compressed with gzip. As a result, when compiling from source, your system will need to have xz installed in order to extract the source code for these libraries.
2022-02-23 23:27:19 +05:30
Pranav Raj SandGitHub 85f19fa25a chore: Show error messages from server in agent APIs (#4055) 2022-02-23 20:18:20 +05:30
Tejaswini ChileandGitHub 1ca1b4d36b feat: bulk actions to update conversation objects (#3934)
Added the endpoints for bulk updating conversation objects

Fixes: #3845 #3940 #3943
2022-02-23 16:53:36 +05:30
Sivin VargheseandGitHub 9059f5906a fix: Throwing error when click load more conversation (#4049) 2022-02-23 16:48:53 +05:30
Sojan JoseandGitHub eb0da2026f chore: Bundle update (#4038)
- address the nokogiri security issues
2022-02-22 16:06:04 +05:30
Aswin Dev P.SandGitHub e348db1e37 Fix: add option to choose 24 hour working slot (#4018)
* Add option to choose 24 hour working slot

* fix spec

* add check to update open hour and close hour for open all day

* update 24 hour working slot change in widget side

* add validation to check open_all_day and closed_all_day true at the same time
2022-02-22 01:28:49 -08:00
Muhsin KelothandGitHub 7ba24b90c4 feat: Add chatwoot:error sdk event (#3998) 2022-02-21 09:40:11 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
3b015d4dd8 chore(deps): bump url-parse from 1.5.3 to 1.5.7 (#4016)
Bumps [url-parse](https://github.com/unshiftio/url-parse) from 1.5.3 to 1.5.7.
- [Release notes](https://github.com/unshiftio/url-parse/releases)
- [Commits](https://github.com/unshiftio/url-parse/compare/1.5.3...1.5.7)

---
updated-dependencies:
- dependency-name: url-parse
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-02-20 11:30:19 +05:30
ea44a32758 feat: Add hCaptcha for public forms (#4017)
- added hCaptcha based verification for chatwoot signups

Co-authored-by: Sojan <sojan@pepalo.com>
2022-02-18 20:02:50 +05:30
Muhsin KelothandGitHub 80d83b401c feat: Show refresh modal on action cable disconnect (#4010) 2022-02-18 11:09:43 +05:30
Sivin VargheseandGitHub ea3e011d3f fix: Auto scroll to bottom when agent sends a message (#3764) 2022-02-17 20:42:13 +05:30
Sivin VargheseandGitHub 2586c8e7e5 fix: Lock icon colour issue in private messages (#4009)
* fix: Lock icon colour issue in private messages

* minor fixes
2022-02-17 18:58:53 +05:30
Sojan b9025a98d3 Merge branch 'release/2.2.0' into develop 2022-02-15 23:41:56 +05:30
813 changed files with 13831 additions and 2689 deletions
+3
View File
@@ -50,3 +50,6 @@ exclude_patterns:
- 'app/javascript/dashboard/routes/dashboard/settings/automation/constants.js'
- 'app/javascript/dashboard/components/widgets/FilterInput/FilterOperatorTypes.js'
- 'app/javascript/dashboard/routes/dashboard/settings/reports/constants.js'
- 'app/javascript/dashboard/i18n/index.js'
- 'app/javascript/widget/i18n/index.js'
- 'app/javascript/survey/i18n/index.js'
+2 -2
View File
@@ -29,8 +29,8 @@ module.exports = {
'vue/html-self-closing': 'off',
"vue/no-v-html": 'off',
'vue/singleline-html-element-content-newline': 'off',
'import/extensions': ['off']
'import/extensions': ['off'],
'no-console': 'error'
},
settings: {
'import/resolver': {
+12
View File
@@ -16,6 +16,7 @@ Metrics/ClassLength:
- 'app/models/message.rb'
- 'app/builders/messages/facebook/message_builder.rb'
- 'app/controllers/api/v1/accounts/contacts_controller.rb'
- 'app/listeners/action_cable_listener.rb'
RSpec/ExampleLength:
Max: 25
Style/Documentation:
@@ -26,6 +27,8 @@ Style/FrozenStringLiteralComment:
Enabled: false
Style/SymbolArray:
Enabled: false
Style/OpenStructUse:
Enabled: false
Style/OptionalBooleanParameter:
Exclude:
- 'app/services/email_templates/db_resolver_service.rb'
@@ -68,14 +71,20 @@ Rails/ApplicationController:
- 'app/controllers/platform_controller.rb'
- 'app/controllers/public_controller.rb'
- 'app/controllers/survey/responses_controller.rb'
Rails/CompactBlank:
Enabled: false
Rails/EnvironmentVariableAccess:
Enabled: false
Rails/TimeZoneAssignment:
Enabled: false
Rails/RedundantPresenceValidationOnBelongsTo:
Enabled: false
Style/ClassAndModuleChildren:
EnforcedStyle: compact
Exclude:
- 'config/application.rb'
Style/MapToHash:
Enabled: false
RSpec/NestedGroups:
Enabled: true
Max: 4
@@ -83,6 +92,8 @@ RSpec/MessageSpies:
Enabled: false
RSpec/StubbedMock:
Enabled: false
RSpec/FactoryBot/SyntaxMethods:
Enabled: false
Naming/VariableNumber:
Enabled: false
Metrics/MethodLength:
@@ -119,6 +130,7 @@ Rails/ReversibleMigration:
- 'db/migrate/20191020085608_rename_old_tables.rb'
- 'db/migrate/20191126185833_update_user_invite_foreign_key.rb'
- 'db/migrate/20191130164019_add_template_type_to_messages.rb'
- 'db/migrate/20210513083044_remove_not_null_from_webhook_url_channel_api.rb'
Rails/BulkChangeTable:
Exclude:
- 'db/migrate/20161025070152_removechannelsfrommodels.rb'
+1 -1
View File
@@ -42,7 +42,7 @@ gem 'down', '~> 5.0'
gem 'aws-sdk-s3', require: false
gem 'azure-storage-blob', require: false
gem 'google-cloud-storage', require: false
gem 'image_processing'
gem 'image_processing', '~> 1.12.2'
##-- gems for database --#
gem 'groupdate'
+201 -200
View File
@@ -9,73 +9,73 @@ GIT
GEM
remote: https://rubygems.org/
specs:
actioncable (6.1.4.6)
actionpack (= 6.1.4.6)
activesupport (= 6.1.4.6)
actioncable (6.1.4.7)
actionpack (= 6.1.4.7)
activesupport (= 6.1.4.7)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
actionmailbox (6.1.4.6)
actionpack (= 6.1.4.6)
activejob (= 6.1.4.6)
activerecord (= 6.1.4.6)
activestorage (= 6.1.4.6)
activesupport (= 6.1.4.6)
actionmailbox (6.1.4.7)
actionpack (= 6.1.4.7)
activejob (= 6.1.4.7)
activerecord (= 6.1.4.7)
activestorage (= 6.1.4.7)
activesupport (= 6.1.4.7)
mail (>= 2.7.1)
actionmailer (6.1.4.6)
actionpack (= 6.1.4.6)
actionview (= 6.1.4.6)
activejob (= 6.1.4.6)
activesupport (= 6.1.4.6)
actionmailer (6.1.4.7)
actionpack (= 6.1.4.7)
actionview (= 6.1.4.7)
activejob (= 6.1.4.7)
activesupport (= 6.1.4.7)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
actionpack (6.1.4.6)
actionview (= 6.1.4.6)
activesupport (= 6.1.4.6)
actionpack (6.1.4.7)
actionview (= 6.1.4.7)
activesupport (= 6.1.4.7)
rack (~> 2.0, >= 2.0.9)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (6.1.4.6)
actionpack (= 6.1.4.6)
activerecord (= 6.1.4.6)
activestorage (= 6.1.4.6)
activesupport (= 6.1.4.6)
actiontext (6.1.4.7)
actionpack (= 6.1.4.7)
activerecord (= 6.1.4.7)
activestorage (= 6.1.4.7)
activesupport (= 6.1.4.7)
nokogiri (>= 1.8.5)
actionview (6.1.4.6)
activesupport (= 6.1.4.6)
actionview (6.1.4.7)
activesupport (= 6.1.4.7)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0)
active_record_query_trace (1.8)
activejob (6.1.4.6)
activesupport (= 6.1.4.6)
activejob (6.1.4.7)
activesupport (= 6.1.4.7)
globalid (>= 0.3.6)
activemodel (6.1.4.6)
activesupport (= 6.1.4.6)
activerecord (6.1.4.6)
activemodel (= 6.1.4.6)
activesupport (= 6.1.4.6)
activerecord-import (1.2.0)
activerecord (>= 3.2)
activestorage (6.1.4.6)
actionpack (= 6.1.4.6)
activejob (= 6.1.4.6)
activerecord (= 6.1.4.6)
activesupport (= 6.1.4.6)
activemodel (6.1.4.7)
activesupport (= 6.1.4.7)
activerecord (6.1.4.7)
activemodel (= 6.1.4.7)
activesupport (= 6.1.4.7)
activerecord-import (1.3.0)
activerecord (>= 4.2)
activestorage (6.1.4.7)
actionpack (= 6.1.4.7)
activejob (= 6.1.4.7)
activerecord (= 6.1.4.7)
activesupport (= 6.1.4.7)
marcel (~> 1.0.0)
mini_mime (>= 1.1.0)
activesupport (6.1.4.6)
activesupport (6.1.4.7)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
tzinfo (~> 2.0)
zeitwerk (~> 2.3)
acts-as-taggable-on (8.1.0)
activerecord (>= 5.0, < 6.2)
acts-as-taggable-on (9.0.1)
activerecord (>= 6.0, < 7.1)
addressable (2.8.0)
public_suffix (>= 2.0.2, < 5.0)
administrate (0.16.0)
administrate (0.17.0)
actionpack (>= 5.0)
actionview (>= 5.0)
activerecord (>= 5.0)
@@ -85,46 +85,46 @@ GEM
momentjs-rails (~> 2.8)
sassc-rails (~> 2.1)
selectize-rails (~> 0.6)
annotate (3.1.1)
activerecord (>= 3.2, < 7.0)
annotate (3.2.0)
activerecord (>= 3.2, < 8.0)
rake (>= 10.4, < 14.0)
ast (2.4.2)
attr_extras (6.2.4)
attr_extras (6.2.5)
aws-eventstream (1.2.0)
aws-partitions (1.513.0)
aws-sdk-core (3.121.1)
aws-partitions (1.556.0)
aws-sdk-core (3.126.2)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.239.0)
aws-partitions (~> 1, >= 1.525.0)
aws-sigv4 (~> 1.1)
jmespath (~> 1.0)
aws-sdk-kms (1.49.0)
aws-sdk-core (~> 3, >= 3.120.0)
aws-sdk-kms (1.54.0)
aws-sdk-core (~> 3, >= 3.126.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.103.0)
aws-sdk-core (~> 3, >= 3.120.0)
aws-sdk-s3 (1.112.0)
aws-sdk-core (~> 3, >= 3.126.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.4)
aws-sigv4 (1.4.0)
aws-eventstream (~> 1, >= 1.0.2)
azure-storage-blob (2.0.1)
azure-storage-blob (2.0.3)
azure-storage-common (~> 2.0)
nokogiri (~> 1.11.0.rc2)
azure-storage-common (2.0.2)
nokogiri (~> 1, >= 1.10.8)
azure-storage-common (2.0.4)
faraday (~> 1.0)
faraday_middleware (~> 1.0.0.rc1)
faraday_middleware (~> 1.0, >= 1.0.0.rc1)
net-http-persistent (~> 4.0)
nokogiri (~> 1.11.0.rc2)
nokogiri (~> 1, >= 1.10.8)
barnes (0.0.9)
multi_json (~> 1)
statsd-ruby (~> 1.1)
bcrypt (3.1.16)
bindex (0.8.1)
bootsnap (1.9.1)
msgpack (~> 1.0)
brakeman (5.1.1)
bootsnap (1.10.3)
msgpack (~> 1.2)
brakeman (5.2.1)
browser (5.3.1)
builder (3.2.4)
bullet (6.1.5)
bullet (7.0.1)
activesupport (>= 3.0.0)
uniform_notifier (~> 1.11)
bundle-audit (0.1.0)
@@ -135,13 +135,13 @@ GEM
byebug (11.1.3)
climate_control (1.0.1)
coderay (1.1.3)
commonmarker (0.23.2)
commonmarker (0.23.4)
concurrent-ruby (1.1.9)
connection_pool (2.2.5)
crack (0.4.5)
rexml
crass (1.0.6)
cypress-on-rails (1.11.0)
cypress-on-rails (1.12.1)
rack
database_cleaner (2.0.1)
database_cleaner-active_record (~> 2.0.0)
@@ -151,11 +151,12 @@ GEM
database_cleaner-core (2.0.1)
datetime_picker_rails (0.0.7)
momentjs-rails (>= 2.8.1)
ddtrace (0.53.0)
ffi (~> 1.0)
ddtrace (0.54.2)
debase-ruby_core_source (<= 0.10.14)
msgpack
debase-ruby_core_source (0.10.14)
declarative (0.0.20)
devise (4.8.0)
devise (4.8.1)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
railties (>= 4.1.0)
@@ -165,7 +166,7 @@ GEM
bcrypt (~> 3.0)
devise (> 3.5.2, < 5)
rails (>= 4.2.0, < 6.2)
diff-lcs (1.4.4)
diff-lcs (1.5.0)
digest-crc (0.6.4)
rake (>= 12.0.0, < 14.0.0)
docile (1.4.0)
@@ -175,14 +176,14 @@ GEM
dotenv-rails (2.7.6)
dotenv (= 2.7.6)
railties (>= 3.2)
down (5.2.4)
down (5.3.0)
addressable (~> 2.8)
ecma-re-validator (0.3.0)
regexp_parser (~> 2.0)
ecma-re-validator (0.4.0)
regexp_parser (~> 2.2)
email_reply_trimmer (0.1.13)
erubi (1.10.0)
erubis (2.7.0)
et-orbi (1.2.5)
et-orbi (1.2.6)
tzinfo
execjs (2.8.1)
facebook-messenger (2.0.1)
@@ -197,11 +198,11 @@ GEM
i18n (>= 1.6, < 2)
faraday (1.0.1)
multipart-post (>= 1.2, < 3)
faraday_middleware (1.0.0)
faraday_middleware (1.2.0)
faraday (~> 1.0)
fcm (1.0.3)
fcm (1.0.5)
faraday (~> 1)
ffi (1.15.4)
ffi (1.15.5)
flag_shih_tzu (0.3.23)
flay (2.12.1)
erubis (~> 2.7.0)
@@ -218,11 +219,11 @@ GEM
googleapis-common-protos-types (>= 1.0.4, < 2.0)
googleauth (~> 0.9)
grpc (~> 1.25)
geocoder (1.7.0)
gli (2.20.1)
geocoder (1.7.3)
gli (2.21.0)
globalid (1.0.0)
activesupport (>= 5.0)
google-apis-core (0.4.1)
google-apis-core (0.4.2)
addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.16.2, < 2.a)
httpclient (>= 2.8.1, < 3.a)
@@ -231,9 +232,9 @@ GEM
retriable (>= 2.0, < 4.a)
rexml
webrick
google-apis-iamcredentials_v1 (0.7.0)
google-apis-iamcredentials_v1 (0.10.0)
google-apis-core (>= 0.4, < 2.a)
google-apis-storage_v1 (0.8.0)
google-apis-storage_v1 (0.11.0)
google-apis-core (>= 0.4, < 2.a)
google-cloud-core (1.6.0)
google-cloud-env (~> 1.0)
@@ -247,22 +248,22 @@ GEM
google-cloud-env (1.5.0)
faraday (>= 0.17.3, < 2.0)
google-cloud-errors (1.2.0)
google-cloud-storage (1.34.1)
addressable (~> 2.5)
google-cloud-storage (1.36.1)
addressable (~> 2.8)
digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1)
google-apis-storage_v1 (~> 0.1)
google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0)
google-protobuf (3.19.2)
google-protobuf (3.19.2-x86_64-darwin)
google-protobuf (3.19.2-x86_64-linux)
google-protobuf (3.19.4)
google-protobuf (3.19.4-x86_64-darwin)
google-protobuf (3.19.4-x86_64-linux)
googleapis-common-protos (1.3.12)
google-protobuf (~> 3.14)
googleapis-common-protos-types (~> 1.2)
grpc (~> 1.27)
googleapis-common-protos-types (1.2.0)
googleapis-common-protos-types (1.3.0)
google-protobuf (~> 3.14)
googleauth (0.17.1)
faraday (>= 0.17.3, < 2.0)
@@ -271,25 +272,25 @@ GEM
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
signet (~> 0.15)
groupdate (5.2.2)
activesupport (>= 5)
grpc (1.41.0)
google-protobuf (~> 3.17)
groupdate (6.0.1)
activesupport (>= 5.2)
grpc (1.43.1)
google-protobuf (~> 3.18)
googleapis-common-protos-types (~> 1.0)
grpc (1.41.0-universal-darwin)
google-protobuf (~> 3.17)
grpc (1.43.1-universal-darwin)
google-protobuf (~> 3.18)
googleapis-common-protos-types (~> 1.0)
grpc (1.41.0-x86_64-linux)
google-protobuf (~> 3.17)
grpc (1.43.1-x86_64-linux)
google-protobuf (~> 3.18)
googleapis-common-protos-types (~> 1.0)
haikunator (1.1.1)
hairtrigger (0.2.24)
activerecord (>= 5.0, < 7)
hairtrigger (0.2.25)
activerecord (>= 5.0, < 8)
ruby2ruby (~> 2.4)
ruby_parser (~> 3.10)
hana (1.3.7)
hashdiff (1.0.1)
hashie (4.1.0)
hashie (5.0.0)
hkdf (0.3.0)
html2text (0.2.1)
nokogiri (~> 1.6)
@@ -300,50 +301,52 @@ GEM
mime-types (~> 3.0)
multi_xml (>= 0.5.2)
httpclient (2.8.3)
i18n (1.9.1)
i18n (1.10.0)
concurrent-ruby (~> 1.0)
image_processing (1.12.1)
image_processing (1.12.2)
mini_magick (>= 4.9.5, < 5)
ruby-vips (>= 2.0.17, < 3)
jbuilder (2.11.2)
jbuilder (2.11.5)
actionview (>= 5.0.0)
activesupport (>= 5.0.0)
jmespath (1.4.0)
jmespath (1.6.0)
jquery-rails (4.4.0)
rails-dom-testing (>= 1, < 3)
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
json (2.5.1)
json_refs (0.1.6)
json (2.6.1)
json_refs (0.1.7)
hana
json_schemer (0.2.18)
json_schemer (0.2.19)
ecma-re-validator (~> 0.3)
hana (~> 1.3)
regexp_parser (~> 2.0)
uri_template (~> 0.7)
jwt (2.3.0)
kaminari (1.2.1)
kaminari (1.2.2)
activesupport (>= 4.1.0)
kaminari-actionview (= 1.2.1)
kaminari-activerecord (= 1.2.1)
kaminari-core (= 1.2.1)
kaminari-actionview (1.2.1)
kaminari-actionview (= 1.2.2)
kaminari-activerecord (= 1.2.2)
kaminari-core (= 1.2.2)
kaminari-actionview (1.2.2)
actionview
kaminari-core (= 1.2.1)
kaminari-activerecord (1.2.1)
kaminari-core (= 1.2.2)
kaminari-activerecord (1.2.2)
activerecord
kaminari-core (= 1.2.1)
kaminari-core (1.2.1)
koala (3.0.0)
kaminari-core (= 1.2.2)
kaminari-core (1.2.2)
koala (3.1.0)
addressable
faraday
faraday (< 2)
json (>= 1.8)
rexml
launchy (2.5.0)
addressable (~> 2.7)
letter_opener (1.7.0)
launchy (~> 2.2)
line-bot-api (1.22.0)
line-bot-api (1.23.0)
liquid (5.1.0)
listen (3.7.0)
listen (3.7.1)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
loofah (2.14.0)
@@ -355,43 +358,43 @@ GEM
maxminddb (0.1.22)
memoist (0.16.2)
method_source (1.0.0)
mime-types (3.3.1)
mime-types (3.4.1)
mime-types-data (~> 3.2015)
mime-types-data (3.2021.0901)
mime-types-data (3.2022.0105)
mini_magick (4.11.0)
mini_mime (1.1.2)
mini_portile2 (2.5.3)
mini_portile2 (2.8.0)
minitest (5.15.0)
mock_redis (0.29.0)
mock_redis (0.30.0)
ruby2_keywords
momentjs-rails (2.20.1)
momentjs-rails (2.29.1.1)
railties (>= 3.1)
msgpack (1.4.2)
msgpack (1.4.5)
multi_json (1.15.0)
multi_xml (0.6.0)
multipart-post (2.1.1)
net-http-persistent (4.0.1)
connection_pool (~> 2.2)
netrc (0.11.0)
newrelic_rpm (8.0.0)
newrelic_rpm (8.4.0)
nio4r (2.5.8)
nokogiri (1.11.7)
mini_portile2 (~> 2.5.0)
nokogiri (1.13.3)
mini_portile2 (~> 2.8.0)
racc (~> 1.4)
nokogiri (1.11.7-arm64-darwin)
nokogiri (1.13.3-arm64-darwin)
racc (~> 1.4)
nokogiri (1.11.7-x86_64-darwin)
nokogiri (1.13.3-x86_64-darwin)
racc (~> 1.4)
nokogiri (1.11.7-x86_64-linux)
nokogiri (1.13.3-x86_64-linux)
racc (~> 1.4)
oauth (0.5.6)
oauth (0.5.8)
orm_adapter (0.5.0)
os (1.1.1)
os (1.1.4)
parallel (1.21.0)
parser (3.0.2.0)
parser (3.1.1.0)
ast (~> 2.4.1)
path_expander (1.1.0)
pg (1.2.3)
pg (1.3.2)
procore-sift (0.16.0)
rails (> 4.2.0)
pry (0.14.1)
@@ -402,55 +405,55 @@ GEM
public_suffix (4.0.6)
puma (5.6.2)
nio4r (~> 2.0)
pundit (2.1.1)
pundit (2.2.0)
activesupport (>= 3.0.0)
raabro (1.4.0)
racc (1.6.0)
rack (2.2.3)
rack-attack (6.5.0)
rack-attack (6.6.0)
rack (>= 1.0, < 3)
rack-cors (1.1.1)
rack (>= 2.0.0)
rack-proxy (0.7.0)
rack-proxy (0.7.2)
rack
rack-test (1.1.0)
rack (>= 1.0, < 3)
rack-timeout (0.6.0)
rails (6.1.4.6)
actioncable (= 6.1.4.6)
actionmailbox (= 6.1.4.6)
actionmailer (= 6.1.4.6)
actionpack (= 6.1.4.6)
actiontext (= 6.1.4.6)
actionview (= 6.1.4.6)
activejob (= 6.1.4.6)
activemodel (= 6.1.4.6)
activerecord (= 6.1.4.6)
activestorage (= 6.1.4.6)
activesupport (= 6.1.4.6)
rails (6.1.4.7)
actioncable (= 6.1.4.7)
actionmailbox (= 6.1.4.7)
actionmailer (= 6.1.4.7)
actionpack (= 6.1.4.7)
actiontext (= 6.1.4.7)
actionview (= 6.1.4.7)
activejob (= 6.1.4.7)
activemodel (= 6.1.4.7)
activerecord (= 6.1.4.7)
activestorage (= 6.1.4.7)
activesupport (= 6.1.4.7)
bundler (>= 1.15.0)
railties (= 6.1.4.6)
railties (= 6.1.4.7)
sprockets-rails (>= 2.0.0)
rails-dom-testing (2.0.3)
activesupport (>= 4.2.0)
nokogiri (>= 1.6)
rails-html-sanitizer (1.4.2)
loofah (~> 2.3)
railties (6.1.4.6)
actionpack (= 6.1.4.6)
activesupport (= 6.1.4.6)
railties (6.1.4.7)
actionpack (= 6.1.4.7)
activesupport (= 6.1.4.7)
method_source
rake (>= 0.13)
thor (~> 1.0)
rainbow (3.0.0)
rainbow (3.1.1)
rake (13.0.6)
rb-fsevent (0.11.0)
rb-fsevent (0.11.1)
rb-inotify (0.10.1)
ffi (~> 1.0)
redis (4.5.1)
redis (4.6.0)
redis-namespace (1.8.1)
redis (>= 3.0.4)
regexp_parser (2.1.1)
regexp_parser (2.2.1)
representable (3.1.1)
declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0)
@@ -465,19 +468,19 @@ GEM
netrc (~> 0.8)
retriable (3.1.2)
rexml (3.2.5)
rspec (3.10.0)
rspec-core (~> 3.10.0)
rspec-expectations (~> 3.10.0)
rspec-mocks (~> 3.10.0)
rspec-core (3.10.1)
rspec-support (~> 3.10.0)
rspec-expectations (3.10.1)
rspec (3.11.0)
rspec-core (~> 3.11.0)
rspec-expectations (~> 3.11.0)
rspec-mocks (~> 3.11.0)
rspec-core (3.11.0)
rspec-support (~> 3.11.0)
rspec-expectations (3.11.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.10.0)
rspec-mocks (3.10.2)
rspec-support (~> 3.11.0)
rspec-mocks (3.11.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.10.0)
rspec-rails (5.0.2)
rspec-support (~> 3.11.0)
rspec-rails (5.0.3)
actionpack (>= 5.2)
activesupport (>= 5.2)
railties (>= 5.2)
@@ -485,36 +488,36 @@ GEM
rspec-expectations (~> 3.10)
rspec-mocks (~> 3.10)
rspec-support (~> 3.10)
rspec-support (3.10.2)
rubocop (1.22.1)
rspec-support (3.11.0)
rubocop (1.25.1)
parallel (~> 1.10)
parser (>= 3.0.0.0)
parser (>= 3.1.0.0)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0)
rexml
rubocop-ast (>= 1.12.0, < 2.0)
rubocop-ast (>= 1.15.1, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 3.0)
rubocop-ast (1.12.0)
parser (>= 3.0.1.1)
rubocop-performance (1.11.5)
rubocop-ast (1.16.0)
parser (>= 3.1.1.0)
rubocop-performance (1.13.2)
rubocop (>= 1.7.0, < 2.0)
rubocop-ast (>= 0.4.0)
rubocop-rails (2.12.3)
rubocop-rails (2.13.2)
activesupport (>= 4.2.0)
rack (>= 1.1)
rubocop (>= 1.7.0, < 2.0)
rubocop-rspec (2.5.0)
rubocop-rspec (2.8.0)
rubocop (~> 1.19)
ruby-progressbar (1.11.0)
ruby-vips (2.1.3)
ruby-vips (2.1.4)
ffi (~> 1.12)
ruby2_keywords (0.0.5)
ruby2ruby (2.4.4)
ruby_parser (~> 3.1)
sexp_processor (~> 4.6)
ruby_parser (3.17.0)
sexp_processor (~> 4.15, >= 4.15.1)
ruby_parser (3.18.1)
sexp_processor (~> 4.16)
sassc (2.4.0)
ffi (~> 1.9)
sassc-rails (2.1.2)
@@ -523,30 +526,28 @@ GEM
sprockets (> 3.0)
sprockets-rails
tilt
scout_apm (4.1.2)
scout_apm (5.1.1)
parser
seed_dump (3.3.1)
activerecord (>= 4)
activesupport (>= 4)
selectize-rails (0.12.6)
semantic_range (3.0.0)
sentry-rails (4.7.3)
sentry-rails (5.1.0)
railties (>= 5.0)
sentry-ruby-core (~> 4.7.0)
sentry-ruby (4.7.3)
sentry-ruby-core (~> 5.1.0)
sentry-ruby (5.1.0)
concurrent-ruby (~> 1.0, >= 1.0.2)
faraday (>= 1.0)
sentry-ruby-core (= 4.7.3)
sentry-ruby-core (4.7.3)
sentry-ruby-core (= 5.1.0)
sentry-ruby-core (5.1.0)
concurrent-ruby
faraday
sentry-sidekiq (4.7.3)
sentry-ruby-core (~> 4.7.0)
sentry-sidekiq (5.1.0)
sentry-ruby-core (~> 5.1.0)
sidekiq (>= 3.0)
sexp_processor (4.15.3)
shoulda-matchers (5.0.0)
sexp_processor (4.16.0)
shoulda-matchers (5.1.0)
activesupport (>= 5.2.0)
sidekiq (6.4.0)
sidekiq (6.4.1)
connection_pool (>= 2.2.2)
rack (~> 2.0)
redis (>= 4.2.0)
@@ -563,7 +564,7 @@ GEM
json (>= 1.8, < 3)
simplecov-html (~> 0.10.0)
simplecov-html (0.10.2)
slack-ruby-client (0.17.0)
slack-ruby-client (1.0.0)
faraday (>= 1.0)
faraday_middleware
gli
@@ -573,7 +574,7 @@ GEM
spring-watcher-listen (2.0.1)
listen (>= 2.7, < 4.0)
spring (>= 1.2, < 3.0)
sprockets (4.0.2)
sprockets (4.0.3)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
sprockets-rails (3.4.2)
@@ -582,13 +583,13 @@ GEM
sprockets (>= 3.0.0)
squasher (0.6.2)
statsd-ruby (1.5.0)
telephone_number (1.4.12)
telephone_number (1.4.13)
thor (1.2.1)
tilt (2.0.10)
time_diff (0.3.0)
activesupport
i18n
trailblazer-option (0.1.1)
trailblazer-option (0.1.2)
twilio-ruby (5.32.0)
faraday (~> 1.0.0)
jwt (>= 1.5, <= 2.5)
@@ -597,7 +598,7 @@ GEM
oauth
tzinfo (2.0.4)
concurrent-ruby (~> 1.0)
tzinfo-data (1.2021.3)
tzinfo-data (1.2021.5)
tzinfo (>= 1.0.0)
uber (0.1.0)
uglifier (4.2.0)
@@ -608,12 +609,12 @@ GEM
unicode-display_width (2.1.0)
uniform_notifier (1.14.2)
uri_template (0.7.0)
valid_email2 (4.0.0)
valid_email2 (4.0.3)
activemodel (>= 3.2)
mail (~> 2.5)
warden (1.2.9)
rack (>= 2.0.9)
web-console (4.1.0)
web-console (4.2.0)
actionview (>= 6.0.0)
activemodel (>= 6.0.0)
bindex (>= 0.4.0)
@@ -687,7 +688,7 @@ DEPENDENCIES
hairtrigger
hashie
html2text
image_processing
image_processing (~> 1.12.2)
jbuilder
json_refs
json_schemer
@@ -750,4 +751,4 @@ RUBY VERSION
ruby 3.0.2p107
BUNDLED WITH
2.2.25
2.3.8
+13 -2
View File
@@ -52,12 +52,11 @@ class ContactIdentifyAction
end
def update_contact
custom_attributes = params[:custom_attributes] ? @contact.custom_attributes.merge(params[:custom_attributes]) : @contact.custom_attributes
# blank identifier or email will throw unique index error
# TODO: replace reject { |_k, v| v.blank? } with compact_blank when rails is upgraded
@contact.update!(params.slice(:name, :email, :identifier, :phone_number).reject do |_k, v|
v.blank?
end.merge({ custom_attributes: custom_attributes }))
end.merge({ custom_attributes: custom_attributes, additional_attributes: additional_attributes }))
ContactAvatarJob.perform_later(@contact, params[:avatar_url]) if params[:avatar_url].present?
end
@@ -68,4 +67,16 @@ class ContactIdentifyAction
mergee_contact: merge_contact
).perform
end
def custom_attributes
params[:custom_attributes] ? @contact.custom_attributes.deep_merge(params[:custom_attributes].stringify_keys) : @contact.custom_attributes
end
def additional_attributes
if params[:additional_attributes]
@contact.additional_attributes.deep_merge(params[:additional_attributes].stringify_keys)
else
@contact.additional_attributes
end
end
end
@@ -1,10 +1,14 @@
class Messages::Messenger::MessageBuilder
include ::FileTypeHelper
def process_attachment(attachment)
return if attachment['type'].to_sym == :template
attachment_obj = @message.attachments.new(attachment_params(attachment).except(:remote_file_url))
attachment_obj.save!
attach_file(attachment_obj, attachment_params(attachment)[:remote_file_url]) if attachment_params(attachment)[:remote_file_url]
fetch_story_link(attachment_obj) if attachment_obj.file_type == 'story_mention'
update_attachment_file_type(attachment_obj)
end
def attach_file(attachment, file_url)
@@ -22,7 +26,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].include? file_type
if [:image, :file, :audio, :video, :share, :story_mention].include? file_type
params.merge!(file_type_params(attachment))
elsif file_type == :location
params.merge!(location_params(attachment))
@@ -39,4 +43,31 @@ class Messages::Messenger::MessageBuilder
remote_file_url: attachment['payload']['url']
}
end
def update_attachment_file_type(attachment)
return unless attachment.file_type == 'share' || attachment.file_type == 'story_mention'
attachment.file_type = file_type(attachment.file&.content_type)
attachment.save!
end
def fetch_story_link(attachment)
message = attachment.message
begin
k = Koala::Facebook::API.new(@inbox.channel.page_access_token) if @inbox.facebook?
result = k.get_object(message.source_id, fields: %w[story from]) || {}
rescue Koala::Facebook::AuthenticationError
@inbox.channel.authorization_error!
raise
rescue StandardError => e
result = {}
Sentry.capture_exception(e)
end
story_id = result['story']['mention']['id']
story_sender = result['from']['username']
message.content_attributes[:story_sender] = story_sender
message.content_attributes[:story_id] = story_id
message.content = I18n.t('conversations.messages.instagram_story_content', story_sender: story_sender)
message.save!
end
end
+32 -31
View File
@@ -7,6 +7,9 @@ class V2::ReportBuilder
def initialize(account, params)
@account = account
@params = params
timezone_offset = (params[:timezone_offset] || 0).to_f
@timezone = ActiveSupport::TimeZone[timezone_offset]&.name
end
def timeseries
@@ -64,60 +67,58 @@ class V2::ReportBuilder
@team ||= account.teams.find(params[:id])
end
def get_grouped_values(object_scope)
object_scope.group_by_period(
params[:group_by] || DEFAULT_GROUP_BY,
:created_at,
default_value: 0,
range: range,
permit: %w[day week month year],
time_zone: @timezone
)
end
def conversations_count
scope.conversations
.group_by_period(params[:group_by] || DEFAULT_GROUP_BY,
:created_at, range: range, default_value: 0, permit: %w[day week month year])
.count
(get_grouped_values scope.conversations).count
end
def incoming_messages_count
scope.messages.incoming.unscope(:order)
.group_by_period(params[:group_by] || DEFAULT_GROUP_BY,
:created_at, range: range, default_value: 0, permit: %w[day week month year])
.count
(get_grouped_values scope.messages.incoming.unscope(:order)).count
end
def outgoing_messages_count
scope.messages.outgoing.unscope(:order)
.group_by_period(params[:group_by] || DEFAULT_GROUP_BY,
:created_at, range: range, default_value: 0, permit: %w[day week month year])
.count
(get_grouped_values scope.messages.outgoing.unscope(:order)).count
end
def resolutions_count
scope.conversations
.resolved
.group_by_period(params[:group_by] || DEFAULT_GROUP_BY,
:created_at, range: range, default_value: 0, permit: %w[day week month year])
.count
(get_grouped_values scope.conversations.resolved).count
end
def avg_first_response_time
scope.events
.where(name: 'first_response')
.group_by_day(:created_at, range: range, default_value: 0)
.average(:value)
(get_grouped_values scope.reporting_events.where(name: 'first_response')).average(:value)
end
def avg_resolution_time
scope.events.where(name: 'conversation_resolved')
.group_by_day(:created_at, range: range, default_value: 0)
.average(:value)
(get_grouped_values scope.reporting_events.where(name: 'conversation_resolved')).average(:value)
end
# Taking average of average is not too accurate
# https://en.wikipedia.org/wiki/Simpson's_paradox
# TODO: Will optimize this later
def avg_resolution_time_summary
return 0 if avg_resolution_time.values.empty?
avg_rt = scope.reporting_events
.where(name: 'conversation_resolved', created_at: range)
.average(:value)
(avg_resolution_time.values.sum / avg_resolution_time.values.length)
return 0 if avg_rt.blank?
avg_rt
end
def avg_first_response_time_summary
return 0 if avg_first_response_time.values.empty?
avg_frt = scope.reporting_events
.where(name: 'first_response', created_at: range)
.average(:value)
(avg_first_response_time.values.sum / avg_first_response_time.values.length)
return 0 if avg_frt.blank?
avg_frt
end
end
+7
View File
@@ -1,5 +1,8 @@
class RoomChannel < ApplicationCable::Channel
def subscribed
# TODO: should we only do ensure stream if current account is present?
# for now going ahead with guard clauses in update_subscription and broadcast_presence
ensure_stream
current_user
current_account
@@ -15,6 +18,8 @@ class RoomChannel < ApplicationCable::Channel
private
def broadcast_presence
return if @current_account.blank?
data = { account_id: @current_account.id, users: ::OnlineStatusTracker.get_available_users(@current_account.id) }
data[:contacts] = ::OnlineStatusTracker.get_available_contacts(@current_account.id) if @current_user.is_a? User
ActionCable.server.broadcast(@pubsub_token, { event: 'presence.update', data: data })
@@ -26,6 +31,8 @@ class RoomChannel < ApplicationCable::Channel
end
def update_subscription
return if @current_account.blank?
::OnlineStatusTracker.update_presence(@current_account.id, @current_user.class.name, @current_user.id)
end
@@ -68,7 +68,7 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
end
def agents
@agents ||= Current.account.users.order_by_full_name.includes({ avatar_attachment: [:blob] })
@agents ||= Current.account.users.order_by_full_name.includes(:account_users, { avatar_attachment: [:blob] })
end
def validate_limit
@@ -3,7 +3,7 @@ class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseCont
before_action :fetch_automation_rule, only: [:show, :update, :destroy, :clone]
def index
@automation_rules = Current.account.automation_rules.active
@automation_rules = Current.account.automation_rules
end
def create
@@ -32,9 +32,9 @@ class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseCont
def automation_rules_permit
params.permit(
:name, :description, :event_name, :account_id,
:name, :description, :event_name, :account_id, :active,
conditions: [:attribute_key, :filter_operator, :query_operator, { values: [] }],
actions: [:action_name, { action_params: [] }]
actions: [:action_name, { action_params: [{}] }]
)
end
@@ -1,32 +1,6 @@
class Api::V1::Accounts::BaseController < Api::BaseController
include SwitchLocale
include EnsureCurrentAccountHelper
before_action :current_account
around_action :switch_locale_using_account_locale
private
def current_account
@current_account ||= ensure_current_account
Current.account = @current_account
end
def ensure_current_account
account = Account.find(params[:account_id])
if current_user
account_accessible_for_user?(account)
elsif @resource.is_a?(AgentBot)
account_accessible_for_bot?(account)
end
account
end
def account_accessible_for_user?(account)
@current_account_user = account.account_users.find_by(user_id: current_user.id)
Current.account_user = @current_account_user
render_unauthorized('You are not authorized to access this account') unless @current_account_user
end
def account_accessible_for_bot?(account)
render_unauthorized('You are not authorized to access this account') unless @resource.agent_bot_inboxes.find_by(account_id: account.id)
end
end
@@ -0,0 +1,26 @@
class Api::V1::Accounts::BulkActionsController < Api::V1::Accounts::BaseController
before_action :type_matches?
def create
if type_matches?
::BulkActionsJob.perform_later(
account: @current_account,
user: current_user,
params: permitted_params
)
head :ok
else
render json: { success: false }, status: :unprocessable_entity
end
end
private
def type_matches?
['Conversation'].include?(params[:type])
end
def permitted_params
params.permit(:type, ids: [], fields: [:status, :assignee_id, :team_id], labels: [add: [], remove: []])
end
end
@@ -1,4 +1,5 @@
class Api::V1::Accounts::Conversations::BaseController < Api::V1::Accounts::BaseController
include EnsureCurrentAccountHelper
before_action :conversation
private
@@ -0,0 +1,17 @@
class Api::V1::Accounts::Conversations::DirectUploadsController < ActiveStorage::DirectUploadsController
include EnsureCurrentAccountHelper
before_action :current_account
before_action :conversation
def create
return if @conversation.nil? || @current_account.nil?
super
end
private
def conversation
@conversation ||= Current.account.conversations.find_by(display_id: params[:conversation_id])
end
end
@@ -13,7 +13,7 @@ class Api::V1::Accounts::Conversations::MessagesController < Api::V1::Accounts::
def destroy
ActiveRecord::Base.transaction do
message.update!(content: I18n.t('conversations.messages.deleted'), deleted: true)
message.update!(content: I18n.t('conversations.messages.deleted'), content_attributes: { deleted: true })
message.attachments.destroy_all
end
end
@@ -30,8 +30,7 @@ class Api::V1::Accounts::CsatSurveyResponsesController < Api::V1::Accounts::Base
def set_csat_survey_responses
@csat_survey_responses = filtrate(
Current.account.csat_survey_responses.includes([:conversation, :assigned_agent, :contact])
)
@csat_survey_responses = @csat_survey_responses.where(created_at: range) if range.present?
).filter_by_created_at(range).filter_by_assigned_agent_id(params[:user_ids])
end
def set_current_page_surveys
@@ -48,7 +48,10 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
# Inbox update doesn't necessarily need channel attributes
return if permitted_params(channel_attributes)[:channel].blank?
validate_email_channel(channel_attributes) if @inbox.inbox_type == 'Email'
if @inbox.inbox_type == 'Email'
validate_email_channel(channel_attributes)
@inbox.channel.reauthorized!
end
@inbox.channel.update!(permitted_params(channel_attributes)[:channel])
update_channel_feature_flags
@@ -138,7 +141,7 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
end
def get_channel_attributes(channel_type)
if channel_type.constantize.const_defined?('EDITABLE_ATTRS')
if channel_type.constantize.const_defined?(:EDITABLE_ATTRS)
channel_type.constantize::EDITABLE_ATTRS.presence
else
[]
@@ -4,6 +4,7 @@ class Api::V1::AccountsController < Api::BaseController
skip_before_action :authenticate_user!, :set_current_user, :handle_with_exception,
only: [:create], raise: false
before_action :check_signup_enabled, only: [:create]
before_action :validate_captcha, only: [:create]
before_action :fetch_account, except: [:create]
before_action :check_authorization, except: [:create]
@@ -58,6 +59,10 @@ class Api::V1::AccountsController < Api::BaseController
raise ActionController::RoutingError, 'Not Found' if GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'false') == 'false'
end
def validate_captcha
raise ActionController::InvalidAuthenticityToken, 'Invalid Captcha' unless ChatwootCaptcha.new(params[:h_captcha_client_response]).valid?
end
def pundit_user
{
user: current_user,
@@ -1,5 +1,6 @@
class Api::V1::Widget::BaseController < ApplicationController
include SwitchLocale
include WebsiteTokenHelper
before_action :set_web_widget
before_action :set_contact
@@ -19,23 +20,6 @@ class Api::V1::Widget::BaseController < ApplicationController
@conversation ||= conversations.last
end
def auth_token_params
@auth_token_params ||= ::Widget::TokenService.new(token: request.headers['X-Auth-Token']).decode_token
end
def set_web_widget
@web_widget = ::Channel::WebWidget.find_by!(website_token: permitted_params[:website_token])
@current_account = @web_widget.account
end
def set_contact
@contact_inbox = @web_widget.inbox.contact_inboxes.find_by(
source_id: auth_token_params[:source_id]
)
@contact = @contact_inbox&.contact
raise ActiveRecord::RecordNotFound unless @contact
end
def create_conversation
::Conversation.create!(conversation_params)
end
@@ -94,10 +78,6 @@ class Api::V1::Widget::BaseController < ApplicationController
{ timestamp: permitted_params[:message][:timestamp] }
end
def permitted_params
params.permit(:website_token)
end
def message_params
{
account_id: conversation.account_id,
@@ -46,6 +46,7 @@ class Api::V1::Widget::ContactsController < Api::V1::Widget::BaseController
end
def permitted_params
params.permit(:website_token, :identifier, :identifier_hash, :email, :name, :avatar_url, :phone_number, custom_attributes: {})
params.permit(:website_token, :identifier, :identifier_hash, :email, :name, :avatar_url, :phone_number, custom_attributes: {},
additional_attributes: {})
end
end
@@ -44,6 +44,15 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
head :ok
end
def toggle_status
head :not_found && return if conversation.nil?
unless conversation.resolved?
conversation.status = :resolved
conversation.save
end
head :ok
end
private
def trigger_typing_event(event)
@@ -0,0 +1,11 @@
class Api::V1::Widget::DirectUploadsController < ActiveStorage::DirectUploadsController
include WebsiteTokenHelper
before_action :set_web_widget
before_action :set_contact
def create
return if @contact.nil? || @current_account.nil?
super
end
end
@@ -41,12 +41,22 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
raise Pundit::NotAuthorizedError unless Current.account_user.administrator?
end
def summary_params
def current_summary_params
{
type: params[:type].to_sym,
since: params[:since],
until: params[:until],
id: params[:id],
since: range[:current][:since],
until: range[:current][:until],
group_by: params[:group_by]
}
end
def previous_summary_params
{
type: params[:type].to_sym,
id: params[:id],
since: range[:previous][:since],
until: range[:previous][:until],
group_by: params[:group_by]
}
end
@@ -58,12 +68,27 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
since: params[:since],
until: params[:until],
id: params[:id],
group_by: params[:group_by]
group_by: params[:group_by],
timezone_offset: params[:timezone_offset]
}
end
def range
{
current: {
since: params[:since],
until: params[:until]
},
previous: {
since: (params[:since].to_i - (params[:until].to_i - params[:since].to_i)).to_s,
until: params[:since]
}
}
end
def summary_metrics
builder = V2::ReportBuilder.new(Current.account, summary_params)
builder.summary
summary = V2::ReportBuilder.new(Current.account, current_summary_params).summary
summary[:previous] = V2::ReportBuilder.new(Current.account, previous_summary_params).summary
summary
end
end
@@ -0,0 +1,28 @@
module EnsureCurrentAccountHelper
private
def current_account
@current_account ||= ensure_current_account
Current.account = @current_account
end
def ensure_current_account
account = Account.find(params[:account_id])
if current_user
account_accessible_for_user?(account)
elsif @resource.is_a?(AgentBot)
account_accessible_for_bot?(account)
end
account
end
def account_accessible_for_user?(account)
@current_account_user = account.account_users.find_by(user_id: current_user.id)
Current.account_user = @current_account_user
render_unauthorized('You are not authorized to access this account') unless @current_account_user
end
def account_accessible_for_bot?(account)
render_unauthorized('You are not authorized to access this account') unless @resource.agent_bot_inboxes.find_by(account_id: account.id)
end
end
@@ -0,0 +1,24 @@
module WebsiteTokenHelper
def auth_token_params
@auth_token_params ||= ::Widget::TokenService.new(token: request.headers['X-Auth-Token']).decode_token
end
def set_web_widget
@web_widget = ::Channel::WebWidget.find_by!(website_token: permitted_params[:website_token])
@current_account = @web_widget.account
end
def set_contact
@contact_inbox = @web_widget.inbox.contact_inboxes.find_by(
source_id: auth_token_params[:source_id]
)
@contact = @contact_inbox&.contact
raise ActiveRecord::RecordNotFound unless @contact
Current.contact = @contact
end
def permitted_params
params.permit(:website_token)
end
end
+4 -1
View File
@@ -26,7 +26,10 @@ class DashboardController < ActionController::Base
'API_CHANNEL_THUMBNAIL',
'ANALYTICS_TOKEN',
'ANALYTICS_HOST',
'DIRECT_UPLOADS_ENABLED'
'DIRECT_UPLOADS_ENABLED',
'HCAPTCHA_SITE_KEY',
'LOGOUT_REDIRECT_LINK',
'DISABLE_USER_PROFILE_UPDATE'
).merge(app_config)
end
@@ -9,7 +9,7 @@ class Platform::Api::V1::UsersController < PlatformController
@resource = (User.find_by(email: user_params[:email]) || User.new(user_params))
@resource.save!
@resource.confirm
@platform_app.platform_app_permissibles.find_or_create_by(permissible: @resource)
@platform_app.platform_app_permissibles.find_or_create_by!(permissible: @resource)
end
def login
+23 -3
View File
@@ -1,5 +1,8 @@
class WidgetTestsController < ActionController::Base
before_action :set_web_widget
before_action :ensure_web_widget
before_action :ensure_widget_position
before_action :ensure_widget_type
before_action :ensure_widget_style
def index
render
@@ -7,7 +10,24 @@ class WidgetTestsController < ActionController::Base
private
def set_web_widget
@web_widget = Channel::WebWidget.first
def ensure_widget_style
@widget_style = params[:widget_style] || 'standard'
end
def ensure_widget_position
@widget_position = params[:position] || 'left'
end
def ensure_widget_type
@widget_type = params[:type] || 'expanded_bubble'
end
def inbox_id
@inbox_id ||= params[:inbox_id] || Channel::WebWidget.first.inbox.id
end
def ensure_web_widget
@inbox = Inbox.find(inbox_id)
@web_widget = @inbox.channel
end
end
+1 -1
View File
@@ -12,10 +12,10 @@ class AsyncDispatcher < BaseDispatcher
[
CampaignListener.instance,
CsatSurveyListener.instance,
EventListener.instance,
HookListener.instance,
InstallationWebhookListener.instance,
NotificationListener.instance,
ReportingEventListener.instance,
WebhookListener.instance,
AutomationRuleListener.instance
]
-2
View File
@@ -1,2 +0,0 @@
module Api::V1::ReportsHelper
end
+5 -3
View File
@@ -3,7 +3,7 @@ module FileTypeHelper
def file_type(content_type)
return :image if image_file?(content_type)
return :video if video_file?(content_type)
return :audio if content_type.include?('audio/')
return :audio if content_type&.include?('audio/')
:file
end
@@ -14,7 +14,8 @@ module FileTypeHelper
'image/png',
'image/gif',
'image/bmp',
'image/webp'
'image/webp',
'image'
].include?(content_type)
end
@@ -23,7 +24,8 @@ module FileTypeHelper
'video/ogg',
'video/mp4',
'video/webm',
'video/quicktime'
'video/quicktime',
'video'
].include?(content_type)
end
end
+18 -4
View File
@@ -1,5 +1,6 @@
<template>
<div id="app" class="app-wrapper app-root">
<update-banner :latest-chatwoot-version="latestChatwootVersion" />
<transition name="fade" mode="out-in">
<router-view></router-view>
</transition>
@@ -13,24 +14,27 @@
</template>
<script>
import { accountIdFromPathname } from './helper/URLHelper';
import { mapGetters } from 'vuex';
import AddAccountModal from '../dashboard/components/layout/sidebarComponents/AddAccountModal';
import WootSnackbarBox from './components/SnackbarContainer';
import NetworkNotification from './components/NetworkNotification';
import { accountIdFromPathname } from './helper/URLHelper';
import UpdateBanner from './components/app/UpdateBanner.vue';
import WootSnackbarBox from './components/SnackbarContainer';
export default {
name: 'App',
components: {
WootSnackbarBox,
AddAccountModal,
NetworkNotification,
UpdateBanner,
WootSnackbarBox,
},
data() {
return {
showAddAccountModal: false,
latestChatwootVersion: null,
};
},
@@ -38,6 +42,7 @@ export default {
...mapGetters({
getAccount: 'accounts/getAccount',
currentUser: 'getCurrentUser',
globalConfig: 'globalConfig/get',
}),
hasAccounts() {
return (
@@ -72,8 +77,12 @@ export default {
if (accountId) {
await this.$store.dispatch('accounts/get');
const { locale } = this.getAccount(accountId);
const {
locale,
latest_chatwoot_version: latestChatwootVersion,
} = this.getAccount(accountId);
this.setLocale(locale);
this.latestChatwootVersion = latestChatwootVersion;
}
},
},
@@ -82,6 +91,11 @@ export default {
<style lang="scss">
@import './assets/scss/app';
.update-banner {
height: var(--space-larger);
align-items: center;
font-size: var(--font-size-small) !important;
}
</style>
<style src="vue-multiselect/dist/vue-multiselect.min.css"></style>
+2 -1
View File
@@ -13,7 +13,7 @@ export default {
.post('auth/sign_in', creds)
.then(response => {
setAuthCredentials(response);
resolve();
resolve(response.data);
})
.catch(error => {
reject(error.response);
@@ -30,6 +30,7 @@ export default {
user_full_name: creds.fullName.trim(),
email: creds.email,
password: creds.password,
h_captcha_client_response: creds.hCaptchaClientResponse,
})
.then(response => {
setAuthCredentials(response);
+10 -4
View File
@@ -6,15 +6,21 @@ class CSATReportsAPI extends ApiClient {
super('csat_survey_responses', { accountScoped: true });
}
get({ page, from, to } = {}) {
get({ page, from, to, user_ids } = {}) {
return axios.get(this.url, {
params: { page, since: from, until: to, sort: '-created_at' },
params: {
page,
since: from,
until: to,
sort: '-created_at',
user_ids,
},
});
}
getMetrics({ from, to } = {}) {
getMetrics({ from, to, user_ids } = {}) {
return axios.get(`${this.url}/metrics`, {
params: { since: from, until: to },
params: { since: from, until: to, user_ids },
});
}
}
+18 -2
View File
@@ -1,6 +1,8 @@
/* global axios */
import ApiClient from './ApiClient';
const getTimeOffset = () => -new Date().getTimezoneOffset() / 60;
class ReportsAPI extends ApiClient {
constructor() {
super('reports', { accountScoped: true, apiVersion: 'v2' });
@@ -8,13 +10,27 @@ class ReportsAPI extends ApiClient {
getReports(metric, since, until, type = 'account', id, group_by) {
return axios.get(`${this.url}`, {
params: { metric, since, until, type, id, group_by },
params: {
metric,
since,
until,
type,
id,
group_by,
timezone_offset: getTimeOffset(),
},
});
}
getSummary(since, until, type = 'account', id, group_by) {
return axios.get(`${this.url}/summary`, {
params: { since, until, type, id, group_by },
params: {
since,
until,
type,
id,
group_by,
},
});
}
@@ -27,6 +27,7 @@ describe('#Reports API', () => {
since: 1621103400,
until: 1621621800,
type: 'account',
timezone_offset: -0,
},
});
});
@@ -26,7 +26,8 @@
code {
border: 0;
font-family: 'Monaco', Verdana;
font-family: 'ui-monospace', 'SFMono-Regular', 'Menlo', 'Monaco', 'Consolas',
'"Liberation Mono"', '"Courier New"', 'monospace';
font-size: $font-size-mini;
&.hljs {
@@ -55,7 +56,6 @@ code {
padding-right: var(--space-normal);
}
.badge {
border-radius: var(--border-radius-normal);
}
@@ -10,10 +10,24 @@ body {
.app-wrapper {
@include full-height;
flex-grow: 0;
min-height: 0;
width: 100%;
}
.app-root {
.banner + .app-wrapper {
.button--fixed-right-top {
top: 5.6 * $space-one;
}
.off-canvas-content {
.button--fixed-right-top {
top: $space-small;
}
}
}
is-closed .app-root {
@include flex;
flex-direction: column;
}
@@ -21,6 +35,7 @@ body {
.app-content {
@include flex;
@include full-height;
min-height: 0;
overflow: hidden;
}
@@ -54,3 +54,9 @@
.text-y-800 {
color: var(--y-800);
}
.text-ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
@@ -20,9 +20,28 @@
color: $color-heading;
}
.metric-wrap {
align-items: baseline;
display: flex;
}
.metric {
font-size: $font-size-bigger;
font-size: $font-size-big;
font-weight: $font-weight-feather;
margin-top: $space-smaller;
}
.metric-trend {
font-size: $font-size-small;
margin-left: $space-small;
}
.metric-up {
color: $success-color;
}
.metric-down {
color: $alert-color;
}
.desc {
@@ -219,7 +219,7 @@ export default {
folders: 'customViews/getCustomViews',
}),
hasAppliedFilters() {
return this.appliedFilters.length;
return this.appliedFilters.length !== 0;
},
hasActiveFolders() {
return this.activeFolder && this.foldersId !== 0;
@@ -460,7 +460,8 @@ export default {
if (this.hasActiveFolders) {
const payload = this.activeFolder.query;
this.fetchSavedFilteredConversations(payload);
} else {
}
if (this.hasAppliedFilters) {
this.fetchFilteredConversations(this.appliedFilters);
}
},
@@ -30,6 +30,7 @@
<script>
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
import { mapGetters } from 'vuex';
import { BUS_EVENTS } from 'shared/constants/busEvents';
export default {
mixins: [globalConfigMixin],
@@ -46,6 +47,9 @@ export default {
mounted() {
window.addEventListener('offline', this.updateOnlineStatus);
window.bus.$on(BUS_EVENTS.WEBSOCKET_DISCONNECT, () => {
this.updateOnlineStatus({ type: 'offline' });
});
},
beforeDestroy() {
@@ -0,0 +1,74 @@
<template>
<banner
v-if="shouldShowBanner"
class="update-banner"
color-scheme="primary"
:banner-message="bannerMessage"
href-link="https://github.com/chatwoot/chatwoot/releases"
:href-link-text="$t('GENERAL_SETTINGS.LEARN_MORE')"
has-close-button
@close="dismissUpdateBanner"
/>
</template>
<script>
import Banner from 'dashboard/components/ui/Banner.vue';
import LocalStorage from '../../helper/localStorage';
import { mapGetters } from 'vuex';
import adminMixin from 'dashboard/mixins/isAdmin';
const semver = require('semver');
const dismissedUpdates = new LocalStorage('dismissedUpdates');
export default {
components: {
Banner,
},
mixins: [adminMixin],
props: {
latestChatwootVersion: {
type: String,
default: '',
},
},
computed: {
...mapGetters({ globalConfig: 'globalConfig/get' }),
hasAnUpdateAvailable() {
if (!semver.valid(this.latestChatwootVersion)) {
return false;
}
return semver.lt(
this.globalConfig.appVersion,
this.latestChatwootVersion
);
},
bannerMessage() {
return this.$t('GENERAL_SETTINGS.UPDATE_CHATWOOT', {
latestChatwootVersion: this.latestChatwootVersion,
});
},
shouldShowBanner() {
return (
this.globalConfig.displayManifest &&
this.hasAnUpdateAvailable &&
!this.isVersionNotificationDismissed(this.latestChatwootVersion) &&
this.isAdmin
);
},
},
methods: {
isVersionNotificationDismissed(version) {
return dismissedUpdates.get().includes(version);
},
dismissUpdateBanner() {
let updatedDismissedItems = dismissedUpdates.get();
if (updatedDismissedItems instanceof Array) {
updatedDismissedItems.push(this.latestChatwootVersion);
} else {
updatedDismissedItems = [this.latestChatwootVersion];
}
dismissedUpdates.store(updatedDismissedItems);
this.latestChatwootVersion = this.globalConfig.appVersion;
},
},
};
</script>
@@ -21,6 +21,7 @@ import SubmitButton from './buttons/FormSubmitButton';
import Tabs from './ui/Tabs/Tabs';
import TabsItem from './ui/Tabs/TabsItem';
import Thumbnail from './widgets/Thumbnail.vue';
import ConfirmModal from './widgets/modal/ConfirmationModal.vue';
const WootUIKit = {
AvatarUploader,
@@ -45,6 +46,7 @@ const WootUIKit = {
Tabs,
TabsItem,
Thumbnail,
ConfirmModal,
install(Vue) {
const keys = Object.keys(this);
keys.pop(); // remove 'install' from keys
@@ -19,23 +19,6 @@
:current-role="currentRole"
@add-label="showAddLabelPopup"
/>
<woot-key-shortcut-modal
v-if="showShortcutModal"
@close="closeKeyShortcutModal"
@clickaway="closeKeyShortcutModal"
/>
<account-selector
:show-account-modal="showAccountModal"
@close-account-modal="toggleAccountModal"
@show-create-account-modal="openCreateAccountModal"
/>
<add-account-modal
:show="showCreateAccountModal"
@close-account-create-modal="closeCreateAccountModal"
/>
<woot-modal :show.sync="showAddLabelModal" :on-close="hideAddLabelPopup">
<add-label-modal @close="hideAddLabelPopup" />
</woot-modal>
</aside>
</template>
@@ -46,12 +29,8 @@ import adminMixin from '../../mixins/isAdmin';
import { getSidebarItems } from './config/default-sidebar';
import alertMixin from 'shared/mixins/alertMixin';
import AccountSelector from './sidebarComponents/AccountSelector.vue';
import AddAccountModal from './sidebarComponents/AddAccountModal.vue';
import AddLabelModal from '../../routes/dashboard/settings/labels/AddLabel';
import PrimarySidebar from './sidebarComponents/Primary';
import SecondarySidebar from './sidebarComponents/Secondary';
import WootKeyShortcutModal from 'components/widgets/modal/WootKeyShortcutModal';
import {
hasPressedAltAndCKey,
hasPressedAltAndRKey,
@@ -65,21 +44,13 @@ import router from '../../routes';
export default {
components: {
AccountSelector,
AddAccountModal,
AddLabelModal,
PrimarySidebar,
SecondarySidebar,
WootKeyShortcutModal,
},
mixins: [adminMixin, alertMixin, eventListenerMixins],
data() {
return {
showOptionsMenu: false,
showAccountModal: false,
showCreateAccountModal: false,
showAddLabelModal: false,
showShortcutModal: false,
};
},
@@ -162,10 +133,10 @@ export default {
}
},
toggleKeyShortcutModal() {
this.showShortcutModal = true;
this.$emit('open-key-shortcut-modal');
},
closeKeyShortcutModal() {
this.showShortcutModal = false;
this.$emit('close-key-shortcut-modal');
},
handleKeyEvents(e) {
if (hasPressedCommandAndForwardSlash(e)) {
@@ -200,20 +171,10 @@ export default {
window.$chatwoot.toggle();
},
toggleAccountModal() {
this.showAccountModal = !this.showAccountModal;
},
openCreateAccountModal() {
this.showAccountModal = false;
this.showCreateAccountModal = true;
},
closeCreateAccountModal() {
this.showCreateAccountModal = false;
this.$emit('toggle-account-modal');
},
showAddLabelPopup() {
this.showAddLabelModal = true;
},
hideAddLabelPopup() {
this.showAddLabelModal = false;
this.$emit('show-add-label-popup');
},
},
};
@@ -223,6 +184,8 @@ export default {
.woot-sidebar {
background: var(--white);
display: flex;
min-height: 0;
height: 100%;
}
</style>
@@ -0,0 +1,36 @@
<template>
<div v-if="showShowCurrentAccountContext" class="account-context--group">
{{ $t('SIDEBAR.CURRENTLY_VIEWING_ACCOUNT') }}
<p class="account-context--name text-ellipsis">
{{ account.name }}
</p>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
export default {
computed: {
...mapGetters({
account: 'getCurrentAccount',
userAccounts: 'getUserAccounts',
}),
showShowCurrentAccountContext() {
return this.userAccounts.length > 1 && this.account.name;
},
},
};
</script>
<style scoped lang="scss">
.account-context--group {
border-radius: var(--border-radius-normal);
border: 1px solid var(--color-border);
font-size: var(--font-size-mini);
padding: var(--space-small);
margin-bottom: var(--space-small);
.account-context--name {
font-weight: var(--font-weight-medium);
margin-bottom: 0;
}
}
</style>
@@ -93,7 +93,7 @@ export default {
width: var(--space-jumbo);
border-right: 1px solid var(--s-50);
box-sizing: content-box;
height: 100vh;
height: 100%;
flex-shrink: 0;
}
@@ -1,5 +1,6 @@
<template>
<div v-if="hasSecondaryMenu" class="main-nav secondary-menu">
<account-context />
<transition-group name="menu-list" tag="ul" class="menu vertical">
<secondary-nav-item
v-for="menuItem in accessibleMenuItems"
@@ -18,9 +19,11 @@
<script>
import { frontendURL } from '../../../helper/URLHelper';
import SecondaryNavItem from './SecondaryNavItem.vue';
import AccountContext from './AccountContext.vue';
export default {
components: {
AccountContext,
SecondaryNavItem,
},
props: {
@@ -222,7 +225,7 @@ export default {
.secondary-menu {
background: var(--white);
border-right: 1px solid var(--s-50);
height: 100vh;
height: 100%;
width: 19rem;
flex-shrink: 0;
overflow: hidden;
@@ -20,7 +20,8 @@
data-view-component="true"
label="Beta"
class="beta"
>Beta
>
{{ $t('SIDEBAR.BETA') }}
</span>
</router-link>
@@ -233,7 +234,7 @@ export default {
padding-left: var(--space-smaller) !important;
margin-left: var(--space-half) !important;
display: inline-block;
font-size: var(--font-size-mini);
font-size: var(--font-size-micro);
font-weight: var(--font-weight-medium);
line-height: 18px;
border: 1px solid transparent;
@@ -1,6 +1,6 @@
<template>
<div class="banner" :class="bannerClasses">
<span>
<span class="banner-message">
{{ bannerMessage }}
<a
v-if="hrefLink"
@@ -26,7 +26,7 @@
v-if="hasCloseButton"
size="small"
variant="link"
color-scheme="warning"
color-scheme="secondary"
icon="dismiss-circle"
class-names="banner-action__button"
@click="onClickClose"
@@ -92,8 +92,19 @@ export default {
justify-content: center;
position: sticky;
&.primary {
background: var(--w-500);
.banner-action__button {
color: var(--white);
}
}
&.secondary {
background: var(--s-300);
background: var(--s-200);
color: var(--s-800);
a {
color: var(--s-800);
}
}
&.alert {
@@ -110,9 +121,13 @@ export default {
&.gray {
background: var(--b-500);
.banner-action__button {
color: var(--white);
}
}
a {
margin-left: var(--space-smaller);
text-decoration: underline;
color: var(--white);
font-size: var(--font-size-mini);
@@ -125,5 +140,10 @@ export default {
white-space: nowrap;
}
}
.banner-message {
display: flex;
align-items: center;
}
}
</style>
@@ -1,5 +1,5 @@
<template>
<div>
<div class="preview-item__wrap">
<div
v-for="(attachment, index) in attachments"
:key="attachment.id"
@@ -19,12 +19,13 @@
</span>
</div>
<div class="file-size-wrap">
<span class="item">
<span class="item text-truncate">
{{ formatFileSize(attachment.resource) }}
</span>
</div>
<div class="remove-file-wrap">
<woot-button
v-if="!isTypeAudio(attachment.resource)"
class="remove--attachment clear secondary"
icon="dismiss"
@click="() => onRemoveAttachment(index)"
@@ -58,6 +59,10 @@ export default {
const type = file.content_type || file.type;
return type.includes('image');
},
isTypeAudio(file) {
const type = file.content_type || file.type;
return type.includes('audio');
},
fileName(file) {
return file.filename || file.name;
},
@@ -65,15 +70,23 @@ export default {
};
</script>
<style lang="scss" scoped>
.preview-item__wrap {
display: flex;
flex-direction: column;
overflow: auto;
margin-top: var(--space-normal);
max-height: 20rem;
}
.preview-item {
display: flex;
padding: var(--space-slab) 0 0;
background: var(--color-background-light);
background: var(--b-50);
border-radius: var(--border-radius-normal);
width: fit-content;
width: 24rem;
padding: var(--space-smaller);
margin-top: var(--space-normal);
margin-bottom: var(--space-one);
}
.thumb-wrap {
@@ -109,6 +122,7 @@ export default {
> .item {
margin: 0;
overflow: hidden;
font-size: var(--font-size-mini);
font-weight: var(--font-weight-medium);
}
@@ -119,7 +133,8 @@ export default {
}
.file-name-wrap {
max-width: 50%;
max-width: 60%;
min-width: 50%;
overflow: hidden;
text-overflow: ellipsis;
margin-left: var(--space-small);
@@ -69,7 +69,22 @@ export const OPERATOR_TYPES_4 = [
label: 'Is greater than',
},
{
value: 'is_lesser_than',
label: 'Is lesser than',
value: 'is_less_than',
label: 'Is less than',
},
];
export const OPERATOR_TYPES_5 = [
{
value: 'is_greater_than',
label: 'Is greater than',
},
{
value: 'is_less_than',
label: 'Is less than',
},
{
value: 'days_before',
label: 'Is x days before',
},
];
@@ -0,0 +1,15 @@
import {
OPERATOR_TYPES_1,
OPERATOR_TYPES_2,
OPERATOR_TYPES_3,
OPERATOR_TYPES_4,
} from './FilterOperatorTypes';
describe('#filterOperators', () => {
it('Matches the correct Operators', () => {
expect(OPERATOR_TYPES_1).toMatchObject(OPERATOR_TYPES_1);
expect(OPERATOR_TYPES_2).toMatchObject(OPERATOR_TYPES_2);
expect(OPERATOR_TYPES_3).toMatchObject(OPERATOR_TYPES_3);
expect(OPERATOR_TYPES_4).toMatchObject(OPERATOR_TYPES_4);
});
});
@@ -141,6 +141,10 @@ export default {
type: String,
default: 'plain_text',
},
dataType: {
type: String,
default: 'plain_text',
},
operators: {
type: Array,
default: () => [],
@@ -7,9 +7,12 @@
<h3 class="heading">
{{ heading }}
</h3>
<h4 class="metric">
{{ point }}
</h4>
<div class="metric-wrap">
<h4 class="metric">
{{ point }}
</h4>
<span v-if="trend !== 0" :class="trendClass">{{ trendValue }}</span>
</div>
<p class="desc">
{{ desc }}
</p>
@@ -20,10 +23,27 @@ export default {
props: {
heading: { type: String, default: '' },
point: { type: [Number, String], default: '' },
trend: { type: Number, default: null },
index: { type: Number, default: null },
desc: { type: String, default: '' },
selected: Boolean,
onClick: { type: Function, default: () => {} },
},
computed: {
trendClass() {
if (this.trend > 0) {
return 'metric-trend metric-up';
}
return 'metric-trend metric-down';
},
trendValue() {
if (this.trend > 0) {
return `+${this.trend}%`;
}
return `${this.trend}%`;
},
},
};
</script>
@@ -0,0 +1,223 @@
<template>
<div class="audio-wave-wrapper">
<div id="audio-wave"></div>
</div>
</template>
<script>
import WaveSurfer from 'wavesurfer.js';
import MicrophonePlugin from 'wavesurfer.js/dist/plugin/wavesurfer.microphone.js';
import RecordRTC from 'recordrtc';
import inboxMixin from '../../../../shared/mixins/inboxMixin';
import alertMixin from '../../../../shared/mixins/alertMixin';
WaveSurfer.microphone = MicrophonePlugin;
export default {
name: 'WootAudioRecorder',
mixins: [inboxMixin, alertMixin],
data() {
return {
wavesurfer: false,
recorder: false,
recordingInterval: false,
recordingDateStarted: new Date().getTime(),
timeDuration: '00:00',
initialTimeDuration: '00:00',
options: {
container: '#audio-wave',
backend: 'WebAudio',
interact: true,
cursorWidth: 1,
plugins: [
WaveSurfer.microphone.create({
bufferSize: 4096,
numberOfInputChannels: 1,
numberOfOutputChannels: 1,
constraints: {
video: false,
audio: true,
},
}),
],
},
optionsRecorder: {
type: 'audio',
mimeType: 'audio/wav',
disableLogs: true,
recorderType: RecordRTC.StereoAudioRecorder,
sampleRate: 44100,
numberOfAudioChannels: 2,
checkForInactiveTracks: true,
bufferSize: 4096,
},
};
},
computed: {
isRecording() {
if (this.recorder) {
return this.recorder.getState() === 'recording';
}
return false;
},
},
mounted() {
this.wavesurfer = WaveSurfer.create(this.options);
this.wavesurfer.on('play', this.playingRecorder);
this.wavesurfer.on('pause', this.pausedRecorder);
this.wavesurfer.microphone.on('deviceReady', this.startRecording);
this.wavesurfer.microphone.on('deviceError', this.deviceError);
this.wavesurfer.microphone.start();
this.fireStateRecorderTimerChanged(this.initialTimeDuration);
},
beforeDestroy() {
if (this.recorder) {
this.recorder.destroy();
}
if (this.wavesurfer) {
this.wavesurfer.destroy();
}
},
methods: {
startRecording(stream) {
this.recorder = RecordRTC(stream, this.optionsRecorder);
this.recorder.onStateChanged = this.onStateRecorderChanged;
this.recorder.startRecording();
},
stopAudioRecording() {
if (this.isRecording) {
this.recorder.stopRecording(() => {
this.wavesurfer.microphone.stopDevice();
this.wavesurfer.loadBlob(this.recorder.getBlob());
this.wavesurfer.stop();
this.fireRecorderBlob(this.getAudioFile());
});
}
},
getAudioFile() {
if (this.hasAudio()) {
return new File([this.recorder.getBlob()], this.getAudioFileName(), {
type: 'audio/wav',
});
}
return false;
},
hasAudio() {
return !(this.isRecording || this.wavesurfer.isPlaying());
},
playingRecorder() {
this.fireStateRecorderChanged('playing');
},
pausedRecorder() {
this.fireStateRecorderChanged('paused');
},
deviceError(err) {
if (
err?.name &&
(err.name.toLowerCase().includes('notallowederror') ||
err.name.toLowerCase().includes('permissiondeniederror'))
) {
this.showAlert(
this.$t('CONVERSATION.REPLYBOX.TIP_AUDIORECORDER_PERMISSION')
);
this.fireStateRecorderChanged('notallowederror');
} else {
this.showAlert(
this.$t('CONVERSATION.REPLYBOX.TIP_AUDIORECORDER_ERROR')
);
}
},
onStateRecorderChanged(state) {
// recording stopped inactive destroyed
switch (state) {
case 'recording':
this.timerDurationChanged();
break;
case 'stopped':
this.timerDurationChanged();
break;
default:
break;
}
this.fireStateRecorderChanged(state);
},
timerDurationChanged() {
if (this.isRecording) {
this.recordingInterval = setInterval(() => {
this.calculateTimeDuration(
(new Date().getTime() - this.recordingDateStarted) / 1000
);
this.fireStateRecorderTimerChanged(this.timeDuration);
}, 1000);
} else {
clearInterval(this.recordingInterval);
}
},
calculateTimeDuration(secs) {
let hr = Math.floor(secs / 3600);
let min = Math.floor((secs - hr * 3600) / 60);
let sec = Math.floor(secs - hr * 3600 - min * 60);
if (min < 10) {
min = '0' + min;
}
if (sec < 10) {
sec = '0' + sec;
}
if (hr <= 0) {
this.timeDuration = min + ':' + sec;
} else {
if (hr < 10) {
hr = '0' + hr;
}
this.timeDuration = hr + ':' + min + ':' + sec;
}
},
playPause() {
this.wavesurfer.playPause();
},
fireRecorderBlob(blob) {
this.$emit('recorder-blob', {
name: blob.name,
type: blob.type,
size: blob.size,
file: blob,
});
},
fireStateRecorderChanged(state) {
this.$emit('state-recorder-changed', state);
},
fireStateRecorderTimerChanged(duration) {
this.$emit('state-recorder-timer-changed', duration);
},
getAudioFileName() {
const d = new Date();
return `audio-${d.getFullYear()}-${d.getMonth()}-${d.getDate()}-${this.getRandomString()}.wav`;
},
getRandomString() {
if (
window.crypto &&
window.crypto.getRandomValues &&
navigator.userAgent.indexOf('Safari') === -1
) {
let a = window.crypto.getRandomValues(new Uint32Array(3));
let token = '';
for (let i = 0, l = a.length; i < l; i += 1) {
token += a[i].toString(36);
}
return token.toLowerCase();
}
return (Math.random() * new Date().getTime())
.toString(36)
.replace(/\./g, '');
},
},
};
</script>
<style lang="scss">
.audio-wave-wrapper {
min-height: 8rem;
max-height: 12rem;
overflow: hidden;
}
</style>
@@ -186,6 +186,12 @@ export default {
blur: () => {
this.onBlur();
},
paste: (view, event) => {
const data = event.clipboardData.files;
if (data.length > 0) {
event.preventDefault();
}
},
},
});
this.focusEditorInputField();
@@ -11,7 +11,6 @@
size="small"
@click="toggleEmojiPicker"
/>
<!-- ensure the same validations for attachment types are implemented in backend models as well -->
<file-upload
ref="upload"
@@ -49,6 +48,27 @@
:title="$t('CONVERSATION.REPLYBOX.TIP_FORMAT_ICON')"
@click="toggleFormatMode"
/>
<woot-button
v-if="showAudioRecorderButton"
:icon="!isRecordingAudio ? 'microphone' : 'microphone-off'"
emoji="🎤"
:color-scheme="!isRecordingAudio ? 'secondary' : 'alert'"
variant="smooth"
size="small"
:title="$t('CONVERSATION.REPLYBOX.TIP_AUDIORECORDER_ICON')"
@click="toggleAudioRecorder"
/>
<woot-button
v-if="showAudioPlayStopButton"
:icon="audioRecorderPlayStopIcon"
emoji="🎤"
color-scheme="secondary"
variant="smooth"
size="small"
@click="toggleAudioRecorderPlayPause"
>
<span>{{ recordingAudioDurationText }}</span>
</woot-button>
<woot-button
v-if="showMessageSignatureButton"
v-tooltip.top-end="signatureToggleTooltip"
@@ -110,7 +130,7 @@ import { ALLOWED_FILE_TYPES } from 'shared/constants/messages';
import { REPLY_EDITOR_MODES } from './constants';
export default {
name: 'ReplyTopPanel',
name: 'ReplyBottomPanel',
components: { FileUpload },
mixins: [eventListenerMixins, uiSettingsMixin, inboxMixin],
props: {
@@ -126,6 +146,10 @@ export default {
type: String,
default: '',
},
recordingAudioDurationText: {
type: String,
default: '',
},
inbox: {
type: Object,
default: () => ({}),
@@ -134,6 +158,10 @@ export default {
type: Boolean,
default: false,
},
showAudioRecorder: {
type: Boolean,
default: false,
},
onFileUpload: {
type: Function,
default: () => {},
@@ -146,6 +174,22 @@ export default {
type: Function,
default: () => {},
},
toggleAudioRecorder: {
type: Function,
default: () => {},
},
toggleAudioRecorderPlayPause: {
type: Function,
default: () => {},
},
isRecordingAudio: {
type: Boolean,
default: false,
},
recordingAudioState: {
type: String,
default: '',
},
isSendDisabled: {
type: Boolean,
default: false,
@@ -192,9 +236,28 @@ export default {
showAttachButton() {
return this.showFileUpload || this.isNote;
},
showAudioRecorderButton() {
return this.showAudioRecorder;
},
showAudioPlayStopButton() {
return this.showAudioRecorder && this.isRecordingAudio;
},
allowedFileTypes() {
return ALLOWED_FILE_TYPES;
},
audioRecorderPlayStopIcon() {
switch (this.recordingAudioState) {
// playing paused recording stopped inactive destroyed
case 'playing':
return 'microphone-pause';
case 'paused':
return 'microphone-play';
case 'stopped':
return 'microphone-play';
default:
return 'microphone-stop';
}
},
showMessageSignatureButton() {
return !this.isPrivate && this.isAnEmailChannel;
},
@@ -10,7 +10,12 @@
:key="i"
v-model="appliedFilters[i]"
:filter-groups="filterGroups"
:input-type="getInputType(appliedFilters[i].attribute_key)"
:input-type="
getInputType(
appliedFilters[i].attribute_key,
appliedFilters[i].filter_operator
)
"
:operators="getOperators(appliedFilters[i].attribute_key)"
:dropdown-values="getDropdownValues(appliedFilters[i].attribute_key)"
:show-query-operator="i !== appliedFilters.length - 1"
@@ -56,6 +61,7 @@ import { mapGetters } from 'vuex';
import { filterAttributeGroups } from './advancedFilterItems';
import filterMixin from 'shared/mixins/filterMixin';
import * as OPERATORS from 'dashboard/components/widgets/FilterInput/FilterOperatorTypes.js';
export default {
components: {
FilterInputBox,
@@ -76,6 +82,12 @@ export default {
required,
$each: {
values: {
ensureBetween0to999(value, prop) {
if (prop.filter_operator === 'days_before') {
return parseInt(value, 10) > 0 && parseInt(value, 10) < 999;
}
return true;
},
required: requiredIf(prop => {
return !(
prop.filter_operator === 'is_present' ||
@@ -141,6 +153,12 @@ export default {
switch (key) {
case 'date':
return 'date';
case 'text':
return 'plain_text';
case 'list':
return 'search_select';
case 'checkbox':
return 'search_select';
default:
return 'plain_text';
}
@@ -149,7 +167,9 @@ export default {
const type = this.filterTypes.find(filter => filter.attributeKey === key);
return type.attributeModel;
},
getInputType(key) {
getInputType(key, operator) {
if (key === 'created_at' || key === 'last_activity_at')
if (operator === 'days_before') return 'plain_text';
const type = this.filterTypes.find(filter => filter.attributeKey === key);
return type.inputType;
},
@@ -159,6 +179,47 @@ export default {
},
getDropdownValues(type) {
const statusFilters = this.$t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS');
const allCustomAttributes = this.$store.getters[
'attributes/getAttributesByModel'
](this.attributeModel);
const isCustomAttributeCheckbox = allCustomAttributes.find(attr => {
return (
attr.attribute_key === type &&
attr.attribute_display_type === 'checkbox'
);
});
if (isCustomAttributeCheckbox) {
return [
{
id: true,
name: this.$t('FILTER.ATTRIBUTE_LABELS.TRUE'),
},
{
id: false,
name: this.$t('FILTER.ATTRIBUTE_LABELS.FALSE'),
},
];
}
const isCustomAttributeList = allCustomAttributes.find(attr => {
return (
attr.attribute_key === type && attr.attribute_display_type === 'list'
);
});
if (isCustomAttributeList) {
return allCustomAttributes
.find(attr => attr.attribute_key === type)
.attribute_values.map(item => {
return {
id: item,
name: item,
};
});
}
switch (type) {
case 'status':
return [
@@ -100,7 +100,8 @@ export default {
display: flex;
background: var(--color-background-light);
margin: 0;
height: calc(100vh - var(--space-jumbo));
height: 100%;
min-height: 0;
}
.conversation-sidebar-wrap {
@@ -1,5 +1,8 @@
<template>
<li v-if="hasAttachments || data.content" :class="alignBubble">
<li
v-if="hasAttachments || data.content || isEmailContentType"
:class="alignBubble"
>
<div :class="wrapClass">
<div v-tooltip.top-start="messageToolTip" :class="bubbleClass">
<bubble-mail-head
@@ -47,7 +50,10 @@
<bubble-actions
:id="data.id"
:sender="data.sender"
:story-sender="storySender"
:story-id="storyId"
:is-a-tweet="isATweet"
:has-instagram-story="hasInstagramStory"
:is-email="isEmailContentType"
:is-private="data.private"
:message-type="data.message_type"
@@ -143,6 +149,10 @@ export default {
type: Boolean,
default: false,
},
hasInstagramStory: {
type: Boolean,
default: false,
},
},
data() {
return {
@@ -206,6 +216,12 @@ export default {
sender() {
return this.data.sender || {};
},
storySender() {
return this.contentAttributes.story_sender || null;
},
storyId() {
return this.contentAttributes.story_id || null;
},
contentType() {
const {
data: { content_type: contentType },
@@ -47,6 +47,7 @@
class="message--read"
:data="message"
:is-a-tweet="isATweet"
:has-instagram-story="hasInstagramStory"
/>
<li v-show="getUnreadCount != 0" class="unread--toast">
<span class="text-uppercase">
@@ -64,6 +65,7 @@
class="message--unread"
:data="message"
:is-a-tweet="isATweet"
:has-instagram-story="hasInstagramStory"
/>
</ul>
<div
@@ -215,6 +217,10 @@ export default {
return this.conversationType === 'tweet';
},
hasInstagramStory() {
return this.conversationType === 'instagram_direct_message';
},
selectedTweet() {
if (this.selectedTweetId) {
const { messages = [] } = this.getMessages;
@@ -65,7 +65,7 @@
</div>
<div class="features-item">
<h2 class="block-title">
<span class="emoji">🏷</span>{{ $t('ONBOARDING.LABELS.TITLE') }}
<span class="emoji">🔖</span>{{ $t('ONBOARDING.LABELS.TITLE') }}
</h2>
<p class="intro-body">
{{ $t('ONBOARDING.LABELS.DESCRIPTION') }}
@@ -33,8 +33,15 @@
:cc-emails.sync="ccEmails"
:bcc-emails.sync="bccEmails"
/>
<woot-audio-recorder
v-if="showAudioRecorderEditor"
ref="audioRecorderInput"
@state-recorder-timer-changed="onStateRecorderTimerChanged"
@state-recorder-changed="onStateRecorderChanged"
@recorder-blob="onRecorderBlob"
/>
<resizable-text-area
v-if="!showRichContentEditor"
v-else-if="!showRichContentEditor"
ref="messageInput"
v-model="message"
class="input"
@@ -67,11 +74,21 @@
/>
</div>
<div
v-if="showMessageSignature"
v-if="isSignatureEnabledForInbox"
v-tooltip="$t('CONVERSATION.FOOTER.MESSAGE_SIGN_TOOLTIP')"
class="message-signature-wrap"
>
<p class="message-signature" v-html="formatMessage(messageSignature)" />
<p
v-if="isSignatureAvailable"
class="message-signature"
v-html="formatMessage(messageSignature)"
/>
<p v-else class="message-signature">
{{ $t('CONVERSATION.FOOTER.MESSAGE_SIGNATURE_NOT_CONFIGURED') }}
<router-link :to="profilePath">
{{ $t('CONVERSATION.FOOTER.CLICK_HERE') }}
</router-link>
</p>
</div>
<reply-bottom-panel
:mode="replyType"
@@ -79,10 +96,16 @@
:send-button-text="replyButtonLabel"
:on-file-upload="onFileUpload"
:show-file-upload="showFileUpload"
:show-audio-recorder="showAudioRecorder"
:toggle-emoji-picker="toggleEmojiPicker"
:toggle-audio-recorder="toggleAudioRecorder"
:toggle-audio-recorder-play-pause="toggleAudioRecorderPlayPause"
:show-emoji-picker="showEmojiPicker"
:on-send="sendMessage"
:is-send-disabled="isReplyButtonDisabled"
:recording-audio-duration-text="recordingAudioDuration"
:recording-audio-state="recordingAudioState"
:is-recording-audio="isRecordingAudio"
:set-format-mode="setFormatMode"
:is-on-private-note="isOnPrivateNote"
:is-format-mode="showRichContentEditor"
@@ -109,6 +132,7 @@ import ReplyBottomPanel from 'dashboard/components/widgets/WootWriter/ReplyBotto
import Banner from 'dashboard/components/ui/Banner.vue';
import { REPLY_EDITOR_MODES } from 'dashboard/components/widgets/WootWriter/constants';
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor';
import WootAudioRecorder from 'dashboard/components/widgets/WootWriter/AudioRecorder';
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
import { MAXIMUM_FILE_UPLOAD_SIZE } from 'shared/constants/messages';
@@ -124,6 +148,7 @@ import { MESSAGE_MAX_LENGTH } from 'shared/helpers/MessageTypeHelper';
import inboxMixin from 'shared/mixins/inboxMixin';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import { DirectUpload } from 'activestorage';
import { frontendURL } from '../../../helper/URLHelper';
export default {
components: {
@@ -135,6 +160,7 @@ export default {
ReplyEmailHead,
ReplyBottomPanel,
WootMessageEditor,
WootAudioRecorder,
Banner,
},
mixins: [
@@ -165,6 +191,9 @@ export default {
showEmojiPicker: false,
showMentions: false,
attachedFiles: [],
isRecordingAudio: false,
recordingAudioState: '',
recordingAudioDuration: '',
isUploading: false,
replyType: REPLY_EDITOR_MODES.REPLY,
mentionSearchKey: '',
@@ -179,7 +208,9 @@ export default {
currentChat: 'getSelectedChat',
messageSignature: 'getMessageSignature',
currentUser: 'getCurrentUser',
lastEmail: 'getLastEmailInSelectedChat',
globalConfig: 'globalConfig/get',
accountId: 'getCurrentAccountId',
}),
showRichContentEditor() {
@@ -257,7 +288,7 @@ export default {
return true;
}
if (this.hasAttachments) return false;
if (this.hasAttachments || this.hasRecordedAudio) return false;
return (
this.isMessageEmpty ||
@@ -280,7 +311,7 @@ export default {
if (this.isAWhatsappChannel) {
return MESSAGE_MAX_LENGTH.TWILIO_WHATSAPP;
}
if (this.isATwilioSMSChannel) {
if (this.isASmsInbox) {
return MESSAGE_MAX_LENGTH.TWILIO_SMS;
}
if (this.isATwitterInbox) {
@@ -297,7 +328,7 @@ export default {
this.isAWhatsappChannel ||
this.isAPIInbox ||
this.isAnEmailChannel ||
this.isATwilioSMSChannel ||
this.isASmsInbox ||
this.isATelegramChannel
);
},
@@ -319,9 +350,21 @@ export default {
hasAttachments() {
return this.attachedFiles.length;
},
hasRecordedAudio() {
return (
this.$refs.audioRecorderInput &&
this.$refs.audioRecorderInput.hasAudio()
);
},
isRichEditorEnabled() {
return this.isAWebWidgetInbox || this.isAnEmailChannel;
},
showAudioRecorder() {
return !this.isOnPrivateNote && this.showFileUpload;
},
showAudioRecorderEditor() {
return this.showAudioRecorder && this.isRecordingAudio;
},
isOnPrivateNote() {
return this.replyType === REPLY_EDITOR_MODES.NOTE;
},
@@ -350,13 +393,19 @@ export default {
enableMultipleFileUpload() {
return this.isAnEmailChannel || this.isAWebWidgetInbox || this.isAPIInbox;
},
showMessageSignature() {
isSignatureEnabledForInbox() {
return !this.isPrivate && this.isAnEmailChannel && this.sendWithSignature;
},
isSignatureAvailable() {
return !!this.messageSignature;
},
sendWithSignature() {
const { send_with_signature: isEnabled } = this.uiSettings;
return isEnabled;
},
profilePath() {
return frontendURL(`accounts/${this.accountId}/profile/settings`);
},
},
watch: {
currentChat(conversation) {
@@ -370,6 +419,8 @@ export default {
} else {
this.replyType = REPLY_EDITOR_MODES.NOTE;
}
this.setCCEmailFromLastChat();
},
message(updatedMessage) {
this.hasSlashCommand =
@@ -391,6 +442,8 @@ export default {
// working even if input/textarea is focussed.
document.addEventListener('keydown', this.handleKeyEvents);
document.addEventListener('paste', this.onPaste);
this.setCCEmailFromLastChat();
},
destroyed() {
document.removeEventListener('keydown', this.handleKeyEvents);
@@ -399,12 +452,16 @@ export default {
methods: {
onPaste(e) {
const data = e.clipboardData.files;
if (!this.showRichContentEditor && data.length !== 0) {
this.$refs.messageInput.$el.blur();
}
if (!data.length || !data[0]) {
return;
}
const file = data[0];
const { name, type, size } = file;
this.onFileUpload({ name, type, size, file });
data.forEach(file => {
const { name, type, size } = file;
this.onFileUpload({ name, type, size, file: file });
});
},
toggleUserMention(currentMentionState) {
this.hasUserMention = currentMentionState;
@@ -469,7 +526,7 @@ export default {
}
if (!this.showMentions) {
let newMessage = this.message;
if (this.sendWithSignature && this.messageSignature) {
if (this.isSignatureEnabledForInbox && this.messageSignature) {
newMessage += '\n\n' + this.messageSignature;
}
const messagePayload = this.getMessagePayload(newMessage);
@@ -479,7 +536,7 @@ export default {
'createPendingMessageAndSend',
messagePayload
);
this.$emit(BUS_EVENTS.SCROLL_TO_MESSAGE);
bus.$emit(BUS_EVENTS.SCROLL_TO_MESSAGE);
} catch (error) {
const errorMessage =
error?.response?.data?.error ||
@@ -500,6 +557,9 @@ export default {
if (canReply || this.isAWhatsappChannel) this.replyType = mode;
if (this.showRichContentEditor) {
if (this.isRecordingAudio) {
this.toggleAudioRecorder();
}
return;
}
this.$nextTick(() => this.$refs.messageInput.focus());
@@ -512,10 +572,26 @@ export default {
this.attachedFiles = [];
this.ccEmails = '';
this.bccEmails = '';
this.isRecordingAudio = false;
},
toggleEmojiPicker() {
this.showEmojiPicker = !this.showEmojiPicker;
},
toggleAudioRecorder() {
this.isRecordingAudio = !this.isRecordingAudio;
this.isRecorderAudioStopped = !this.isRecordingAudio;
if (!this.isRecordingAudio) {
this.clearMessage();
}
},
toggleAudioRecorderPlayPause() {
if (this.isRecordingAudio && !this.isRecorderAudioStopped) {
this.isRecorderAudioStopped = true;
this.$refs.audioRecorderInput.stopAudioRecording();
} else if (this.isRecordingAudio && this.isRecorderAudioStopped) {
this.$refs.audioRecorderInput.playPause();
}
},
hideEmojiPicker() {
if (this.showEmojiPicker) {
this.toggleEmojiPicker();
@@ -536,6 +612,20 @@ export default {
onFocus() {
this.isFocused = true;
},
onStateRecorderTimerChanged(time) {
this.recordingAudioDuration = time;
},
onStateRecorderChanged(state) {
this.recordingAudioState = state;
if (state.includes('notallowederror')) {
this.toggleAudioRecorder();
}
},
onRecorderBlob(file) {
if (file) {
this.onFileUpload(file);
}
},
toggleTyping(status) {
const conversationId = this.currentChat.id;
const isPrivate = this.isPrivate;
@@ -559,10 +649,17 @@ export default {
if (checkFileSizeLimit(file, MAXIMUM_FILE_UPLOAD_SIZE)) {
const upload = new DirectUpload(
file.file,
'/rails/active_storage/direct_uploads',
null,
file.file.name
`/api/v1/accounts/${this.accountId}/conversations/${this.currentChat.id}/direct_uploads`,
{
directUploadWillCreateBlobWithXHR: xhr => {
xhr.setRequestHeader(
'api_access_token',
this.currentUser.access_token
);
},
}
);
upload.create((error, blob) => {
if (error) {
this.showAlert(error);
@@ -632,11 +729,11 @@ export default {
});
}
if (this.ccEmails) {
if (this.ccEmails && !this.isOnPrivateNote) {
messagePayload.ccEmails = this.ccEmails;
}
if (this.bccEmails) {
if (this.bccEmails && !this.isOnPrivateNote) {
messagePayload.bccEmails = this.bccEmails;
}
@@ -649,6 +746,17 @@ export default {
this.bccEmails = value.bccEmails;
this.ccEmails = value.ccEmails;
},
setCCEmailFromLastChat() {
if (this.lastEmail) {
const {
content_attributes: { email: emailAttributes = {} },
} = this.lastEmail;
const cc = emailAttributes.cc || [];
const bcc = emailAttributes.bcc || [];
this.ccEmails = cc.join(', ');
this.bccEmails = bcc.join(', ');
}
},
},
};
</script>
@@ -666,6 +774,8 @@ export default {
justify-content: space-between;
border: 1px dashed var(--s-100);
border-radius: var(--border-radius-small);
max-height: 8vh;
overflow: auto;
&:hover {
background: var(--s-25);
@@ -713,4 +823,12 @@ export default {
filter: drop-shadow(0px 4px 4px rgba(0, 0, 0, 0.08));
}
}
.message-signature {
margin-bottom: 0;
::v-deep p:last-child {
margin-bottom: 0;
}
}
</style>
@@ -83,6 +83,10 @@ export default {
}
},
},
mounted() {
this.ccEmailsVal = this.ccEmails;
this.bccEmailsVal = this.bccEmails;
},
validations: {
ccEmailsVal: {
hasValidEmails(value) {
@@ -0,0 +1,9 @@
import defaultFilters from './index';
import { filterAttributeGroups } from './index';
describe('#filterItems', () => {
it('Matches the correct filterItems', () => {
expect(defaultFilters).toMatchObject(defaultFilters);
expect(filterAttributeGroups).toMatchObject(filterAttributeGroups);
});
});
@@ -2,6 +2,7 @@ import {
OPERATOR_TYPES_1,
OPERATOR_TYPES_2,
OPERATOR_TYPES_3,
OPERATOR_TYPES_5,
} from '../../FilterInput/FilterOperatorTypes';
const filterTypes = [
@@ -85,6 +86,30 @@ const filterTypes = [
filterOperators: OPERATOR_TYPES_3,
attributeModel: 'additional',
},
{
attributeKey: 'created_at',
attributeI18nKey: 'CREATED_AT',
inputType: 'date',
dataType: 'text',
filterOperators: OPERATOR_TYPES_5,
attributeModel: 'standard',
},
{
attributeKey: 'last_activity_at',
attributeI18nKey: 'LAST_ACTIVITY',
inputType: 'date',
dataType: 'text',
filterOperators: OPERATOR_TYPES_5,
attributeModel: 'standard',
},
{
attributeKey: 'referer',
attributeI18nKey: 'REFERER_LINK',
inputType: 'plain_text',
dataType: 'text',
filterOperators: OPERATOR_TYPES_5,
attributeModel: 'standard',
},
];
export const filterAttributeGroups = [
@@ -120,6 +145,14 @@ export const filterAttributeGroups = [
key: 'labels',
i18nKey: 'LABELS',
},
{
key: 'created_at',
i18nKey: 'CREATED_AT',
},
{
key: 'last_activity_at',
i18nKey: 'LAST_ACTIVITY',
},
],
},
{
@@ -19,20 +19,35 @@
v-if="isPrivate"
v-tooltip.top-start="$t('CONVERSATION.VISIBLE_TO_AGENTS')"
icon="lock-closed"
class="action--icon"
class="action--icon lock--icon--private"
size="16"
@mouseenter="isHovered = true"
@mouseleave="isHovered = false"
/>
<button @click="onTweetReply">
<button
v-if="isATweet && (isIncoming || isOutgoing) && sourceId"
@click="onTweetReply"
>
<fluent-icon
v-if="isATweet && (isIncoming || isOutgoing) && sourceId"
v-tooltip.top-start="$t('CHAT_LIST.REPLY_TO_TWEET')"
icon="arrow-reply"
class="action--icon cursor-pointer"
size="16"
/>
</button>
<a
v-if="hasInstagramStory && (isIncoming || isOutgoing) && linkToStory"
:href="linkToStory"
target="_blank"
rel="noopener noreferrer nofollow"
>
<fluent-icon
v-tooltip.top-start="$t('CHAT_LIST.LINK_TO_STORY')"
icon="open"
class="action--icon cursor-pointer"
size="16"
/>
</a>
<a
v-if="isATweet && (isOutgoing || isIncoming) && linkToTweet"
:href="linkToTweet"
@@ -65,6 +80,14 @@ export default {
type: String,
default: '',
},
storySender: {
type: String,
default: '',
},
storyId: {
type: String,
default: '',
},
isEmail: {
type: Boolean,
default: true,
@@ -77,6 +100,10 @@ export default {
type: Boolean,
default: true,
},
hasInstagramStory: {
type: Boolean,
default: true,
},
messageType: {
type: Number,
default: 1,
@@ -117,6 +144,13 @@ export default {
return `https://twitter.com/${screenName ||
this.inbox.name}/status/${sourceId}`;
},
linkToStory() {
if (!this.storyId || !this.storySender) {
return '';
}
const { storySender, storyId } = this;
return `https://www.instagram.com/stories/${storySender}/${storyId}`;
},
showSentIndicator() {
return this.isOutgoing && this.sourceId && this.isAnEmailChannel;
},
@@ -141,6 +175,10 @@ export default {
.action--icon {
color: var(--white);
}
.lock--icon--private {
color: var(--s-400);
}
}
}
@@ -205,7 +243,7 @@ export default {
.is-private {
.message-text--metadata {
align-items: flex-end;
align-items: center;
.time {
color: var(--s-400);
@@ -70,7 +70,7 @@ export default {
return this.emailAttributes.subject || '';
},
showHead() {
return this.toMails || this.ccMails || this.bccMails;
return this.toMails || this.ccMails || this.bccMails || this.fromMail;
},
},
};
@@ -0,0 +1,75 @@
<template>
<modal :show.sync="show" :on-close="cancel">
<div class="column content-box">
<woot-modal-header :header-title="title"> </woot-modal-header>
<div class="row modal-content">
<div class="medium-12 columns">
<p>
{{ description }}
</p>
</div>
<div class="modal-footer">
<div class="medium-12 columns">
<woot-button @click="confirm">
{{ confirmLabel }}
</woot-button>
<button class="button clear" @click="cancel">
{{ cancelLabel }}
</button>
</div>
</div>
</div>
</div>
</modal>
</template>
<script>
import Modal from '../../Modal';
export default {
components: {
Modal,
},
props: {
title: {
type: String,
default: 'This is a title',
},
description: {
type: String,
default: 'This is your description',
},
confirmLabel: {
type: String,
default: 'Yes',
},
cancelLabel: {
type: String,
default: 'No',
},
},
data: () => ({
show: false,
resolvePromise: undefined,
rejectPromise: undefined,
}),
methods: {
showConfirmation() {
this.show = true;
return new Promise((resolve, reject) => {
this.resolvePromise = resolve;
this.rejectPromise = reject;
});
},
confirm() {
this.resolvePromise(true);
this.show = false;
},
cancel() {
this.resolvePromise(false);
this.show = false;
},
},
};
</script>
@@ -1,10 +1,22 @@
import queryString from 'query-string';
import { DEFAULT_REDIRECT_URL } from '../constants';
export const frontendURL = (path, params) => {
const stringifiedParams = params ? `?${queryString.stringify(params)}` : '';
return `/app/${path}${stringifiedParams}`;
};
export const getLoginRedirectURL = (ssoAccountId, user) => {
const { accounts = [] } = user || {};
const ssoAccount = accounts.find(
account => account.id === Number(ssoAccountId)
);
if (ssoAccount) {
return frontendURL(`accounts/${ssoAccountId}/dashboard`);
}
return DEFAULT_REDIRECT_URL;
};
export const conversationUrl = ({
accountId,
activeInbox,
@@ -0,0 +1,17 @@
class LocalStorage {
constructor(key) {
this.key = key;
}
store(allItems) {
localStorage.setItem(this.key, JSON.stringify(allItems));
localStorage.setItem(this.key + ':ts', Date.now());
}
get() {
let stored = localStorage.getItem(this.key);
return JSON.parse(stored) || [];
}
}
export default LocalStorage;
@@ -3,6 +3,7 @@ import {
conversationUrl,
accountIdFromPathname,
isValidURL,
getLoginRedirectURL,
} from '../URLHelper';
describe('#URL Helpers', () => {
@@ -58,4 +59,24 @@ describe('#URL Helpers', () => {
expect(isValidURL('alert.window')).toBe(false);
});
});
describe('getLoginRedirectURL', () => {
it('should return correct Account URL if account id is present', () => {
expect(
getLoginRedirectURL('7500', {
accounts: [{ id: 7500, name: 'Test Account 7500' }],
})
).toBe('/app/accounts/7500/dashboard');
});
it('should return default URL if account id is not present', () => {
expect(getLoginRedirectURL('7500', {})).toBe('/app/');
expect(
getLoginRedirectURL('7500', {
accounts: [{ id: '7501', name: 'Test Account 7501' }],
})
).toBe('/app/');
expect(getLoginRedirectURL('7500', null)).toBe('/app/');
});
});
});
@@ -21,7 +21,12 @@
"is_present": "موجود",
"is_not_present": "غير موجود",
"is_greater_than": "هو أكبر من",
"is_lesser_than": "هو أقل من"
"is_less_than": "هو أقل من",
"days_before": "قبل x أيام"
},
"ATTRIBUTE_LABELS": {
"TRUE": "صحيح",
"FALSE": "خاطئ"
},
"ATTRIBUTES": {
"STATUS": "الحالة",
@@ -38,7 +43,9 @@
"CUSTOM_ATTRIBUTE_TEXT": "النص",
"CUSTOM_ATTRIBUTE_NUMBER": "العدد",
"CUSTOM_ATTRIBUTE_LINK": "الرابط",
"CUSTOM_ATTRIBUTE_CHECKBOX": "مربع"
"CUSTOM_ATTRIBUTE_CHECKBOX": "مربع",
"CREATED_AT": "تم إنشاؤها في",
"LAST_ACTIVITY": "آخر نشاط"
},
"GROUPS": {
"STANDARD_FILTERS": "الفلاتر القياسية",
@@ -64,7 +64,7 @@
},
"EDIT": {
"TITLE": "تعديل قاعدة الأتمتة",
"SUBMIT": عديل",
"SUBMIT": حديث",
"CANCEL_BUTTON_TEXT": "إلغاء",
"API": {
"SUCCESS_MESSAGE": "تم تحديث قاعدة الأتمتة بنجاح",
@@ -84,6 +84,24 @@
"DELETE": "حذف",
"CANCEL": "إلغاء",
"RESET_MESSAGE": "تغيير نوع الحدث سوف يعيد تعيين الشروط والأحداث التي أضفتها أدناه"
},
"CONDITION": {
"DELETE_MESSAGE": "يجب أن يكون لديك على الأقل شرط واحد للحفظ"
},
"ACTION": {
"DELETE_MESSAGE": "يجب أن يكون لديك على الأقل شرط واحد للحفظ"
},
"TOGGLE": {
"ACTIVATION_TITLE": "تفعيل قاعدة الأتمتة",
"DEACTIVATION_TITLE": "تغطيل قاعدة الأتمتة",
"ACTIVATION_DESCRIPTION": "سيؤدي هذا الإجراء إلى تنشيط قاعدة الأتمتة '{automationName}'. هل أنت متأكد من أنك تريد المتابعة؟",
"DEACTIVATION_DESCRIPTION": "سيؤدي هذا الإجراء إلى إلغاء تنشيط قاعدة الأتمتة '{automationName}'. هل أنت متأكد من أنك تريد المتابعة؟",
"ACTIVATION_SUCCESFUL": "تم تفعيل قاعدة الأتمتة بنجاح",
"DEACTIVATION_SUCCESFUL": "تم تعطيل قاعدة الأتمتة بنجاح",
"ACTIVATION_ERROR": "تعذر تنشيط قاعدة الأتمتة، الرجاء المحاولة مرة أخرى لاحقاً",
"DEACTIVATION_ERROR": "تعذر إلغاء تنشيط قاعدة الأتمتة، الرجاء المحاولة مرة أخرى لاحقاً",
"CONFIRMATION_LABEL": "نعم",
"CANCEL_LABEL": "لا"
}
}
}
@@ -76,6 +76,7 @@
"RECEIVED_VIA_EMAIL": "تم تلقيه عبر البريد الإلكتروني",
"VIEW_TWEET_IN_TWITTER": "عرض التغريدة في تويتر",
"REPLY_TO_TWEET": "الرد على هذه التغريدة",
"LINK_TO_STORY": "الذهاب إلى قصة الإنستقرام",
"SENT": "Sent successfully",
"NO_MESSAGES": "لا توجد رسائل",
"NO_CONTENT": "لم يتم العثور على محتوى",
@@ -77,9 +77,8 @@
"CONFIRM": {
"TITLE": "تأكيد الحذف",
"MESSAGE": "هل أنت متأكد من الحذف ",
"PLACE_HOLDER": "الرجاء كتابة {contactName} للتأكيد",
"YES": "نعم، احذف ",
"NO": "لا، احتفظ "
"YES": "نعم، احذف",
"NO": "لا، احتفظ"
},
"API": {
"SUCCESS_MESSAGE": "تم حذف جهة الاتصال بنجاح",
@@ -22,7 +22,8 @@
"is_present": "موجود",
"is_not_present": "غير موجود",
"is_greater_than": "هو أكبر من",
"is_lesser_than": "هو أقل من"
"is_lesser_than": "هو أقل من",
"days_before": "قبل x أيام"
},
"ATTRIBUTES": {
"NAME": "الاسم",
@@ -35,7 +36,9 @@
"CUSTOM_ATTRIBUTE_TEXT": "النص",
"CUSTOM_ATTRIBUTE_NUMBER": "العدد",
"CUSTOM_ATTRIBUTE_LINK": "الرابط",
"CUSTOM_ATTRIBUTE_CHECKBOX": "مربع"
"CUSTOM_ATTRIBUTE_CHECKBOX": "مربع",
"CREATED_AT": "تم إنشاؤها في",
"LAST_ACTIVITY": "آخر نشاط"
},
"GROUPS": {
"STANDARD_FILTERS": "الفلاتر القياسية",
@@ -57,11 +57,13 @@
}
},
"FOOTER": {
"MESSAGE_SIGN_TOOLTIP": "Message signature",
"ENABLE_SIGN_TOOLTIP": "Enable signature",
"DISABLE_SIGN_TOOLTIP": "Disable signature",
"MESSAGE_SIGN_TOOLTIP": "توقيع الرسالة",
"ENABLE_SIGN_TOOLTIP": "تمكين التوقيع",
"DISABLE_SIGN_TOOLTIP": "تعطيل التوقيع",
"MSG_INPUT": "زر Shift + Enter لإضافة سطر جديد. ابدأ بزر / للاختيار من الردود السريعة.",
"PRIVATE_MSG_INPUT": "زر Shift + Enter لإضافة سطر جديد. سيكون هذا مرئياً للموظفين فقط"
"PRIVATE_MSG_INPUT": "زر Shift + Enter لإضافة سطر جديد. سيكون هذا مرئياً للموظفين فقط",
"MESSAGE_SIGNATURE_NOT_CONFIGURED": "لم يتم تكوين توقيع الرسالة، الرجاء تكوينه في إعدادات الملف الشخصي.",
"CLICK_HERE": "انقر هنا للتحديث"
},
"REPLYBOX": {
"REPLY": "إضافة رد",
@@ -72,8 +74,14 @@
"TIP_FORMAT_ICON": "عرض محرر النصوص",
"TIP_EMOJI_ICON": "إظهار قائمة الرموز التعبيرية",
"TIP_ATTACH_ICON": "إرفاق الملفات",
"TIP_AUDIORECORDER_ICON": "تسجيل الصوت",
"TIP_AUDIORECORDER_PERMISSION": "السماح بالوصول إلى الصوت",
"TIP_AUDIORECORDER_ERROR": "تعذر فتح الصوت",
"ENTER_TO_SEND": "زر الإدخل للإرسال",
"DRAG_DROP": "اسحب و أسقط هنا للإرفاق",
"START_AUDIO_RECORDING": "بدء التسجيل الصوتي",
"STOP_AUDIO_RECORDING": "إيقاف التسجيل الصوتي",
"": "",
"EMAIL_HEAD": {
"ADD_BCC": "إضافة bcc",
"CC": {
@@ -47,7 +47,8 @@
"CUSTOM_EMAIL_DOMAIN_ENABLED": "يمكنك تلقي رسائل البريد الإلكتروني في النطاق المخصص الخاص بك الآن."
}
},
"UPDATE_CHATWOOT": "يتوفر تحديث %{latestChatwootVersion} لـ Chatwoot. الرجاء التحديث."
"UPDATE_CHATWOOT": "يتوفر تحديث %{latestChatwootVersion} لـ Chatwoot. الرجاء التحديث.",
"LEARN_MORE": "اعرف المزيد"
},
"FORMS": {
"MULTISELECT": {
@@ -462,7 +462,8 @@
"HOURS": "ساعات",
"VALIDATION_ERROR": "يجب أن يكون وقت البدء قبل وقت الإغلاق.",
"CHOOSE": "اختر"
}
},
"ALL_DAY": "جميع الأيام"
},
"IMAP": {
"TITLE": "IMAP",
@@ -59,7 +59,56 @@
"CUSTOM_DATE_RANGE": {
"CONFIRM": "تطبيق",
"PLACEHOLDER": "اختر نطاق المدة"
}
},
"GROUP_BY_FILTER_DROPDOWN_LABEL": "تجميع بواسطة",
"GROUP_BY_DAY_OPTIONS": [
{
"id": 1,
"groupBy": "اليوم"
}
],
"GROUP_BY_WEEK_OPTIONS": [
{
"id": 1,
"groupBy": "اليوم"
},
{
"id": 2,
"groupBy": "الأسبوع"
}
],
"GROUP_BY_MONTH_OPTIONS": [
{
"id": 1,
"groupBy": "اليوم"
},
{
"id": 2,
"groupBy": "الأسبوع"
},
{
"id": 3,
"groupBy": "الشهر"
}
],
"GROUP_BY_YEAR_OPTIONS": [
{
"id": 1,
"groupBy": "اليوم"
},
{
"id": 2,
"groupBy": "الأسبوع"
},
{
"id": 3,
"groupBy": "الشهر"
},
{
"id": 4,
"groupBy": "السنة"
}
]
},
"AGENT_REPORTS": {
"HEADER": "نظرة عامة للوكلاء",
@@ -316,6 +365,11 @@
"CSAT_REPORTS": {
"HEADER": "تقارير CSAT",
"NO_RECORDS": "لا توجد ردود متوفرة على الدراسة الاستقصائية CSAT.",
"FILTERS": {
"AGENTS": {
"PLACEHOLDER": "اختر الوكلاء"
}
},
"TABLE": {
"HEADER": {
"CONTACT_NAME": "جهات الاتصال",
@@ -20,16 +20,16 @@
"NOTE": "عنوان بريدك الإلكتروني هو المعرف الخاص بك الذي ستستخدمه لتسجيل الدخول."
},
"MESSAGE_SIGNATURE_SECTION": {
"TITLE": "Personal message signature",
"NOTE": "Create a personal message signature that would be added to all the messages you send from the platform. Use the rich content editor to create a highly personalised signature.",
"BTN_TEXT": "Save message signature",
"API_ERROR": "Couldn't save signature! Try again",
"API_SUCCESS": "Signature saved successfully"
"TITLE": "توقيع الرسالة الشخصية",
"NOTE": "إنشاء توقيع رسالة شخصية يتم إضافتها إلى جميع الرسائل التي ترسلها من المنصة. استخدم محرر المحتوى الغني لإنشاء توقيع شديد التخصيص.",
"BTN_TEXT": "حفظ توقيع الرسالة",
"API_ERROR": "تعذر إرسال الرسالة! حاول مرة أخرى",
"API_SUCCESS": "تم حفظ التوقيع بنجاح"
},
"MESSAGE_SIGNATURE": {
"LABEL": "Message Signature",
"ERROR": "Message Signature cannot be empty",
"PLACEHOLDER": "Insert your personal message signature here."
"LABEL": "توقيع الرسالة",
"ERROR": "توقيع الرسالة لا يمكن أن يكون فارغاً",
"PLACEHOLDER": "أدخل توقيع رسالتك الشخصية هنا."
},
"PASSWORD_SECTION": {
"TITLE": "كلمة المرور",
@@ -146,6 +146,7 @@
}
},
"SIDEBAR": {
"CURRENTLY_VIEWING_ACCOUNT": "مشاهدة حاليا:",
"CONVERSATIONS": "المحادثات",
"ALL_CONVERSATIONS": "كل المحادثات",
"MENTIONED_CONVERSATIONS": "الإشارات",
@@ -181,7 +182,8 @@
"REPORTS_LABEL": "الوسوم",
"REPORTS_INBOX": "صندوق الوارد",
"REPORTS_TEAM": "الفريق",
"SET_AVAILABILITY_TITLE": "تعيين نفسك كـ"
"SET_AVAILABILITY_TITLE": "تعيين نفسك كـ",
"BETA": "تجريبي"
},
"CREATE_ACCOUNT": {
"NO_ACCOUNT_WARNING": "أوه! لم نتمكن من العثور على الحساب. الرجاء إنشاء حساب جديد للمتابعة.",
@@ -21,7 +21,12 @@
"is_present": "Присъства",
"is_not_present": "Не присъства",
"is_greater_than": "Is greater than",
"is_lesser_than": "Is lesser than"
"is_less_than": "Is lesser than",
"days_before": "Is x days before"
},
"ATTRIBUTE_LABELS": {
"TRUE": "True",
"FALSE": "False"
},
"ATTRIBUTES": {
"STATUS": "Статус",
@@ -38,7 +43,9 @@
"CUSTOM_ATTRIBUTE_TEXT": "Text",
"CUSTOM_ATTRIBUTE_NUMBER": "Number",
"CUSTOM_ATTRIBUTE_LINK": "Link",
"CUSTOM_ATTRIBUTE_CHECKBOX": "Checkbox"
"CUSTOM_ATTRIBUTE_CHECKBOX": "Checkbox",
"CREATED_AT": "Created At",
"LAST_ACTIVITY": "Последна активност"
},
"GROUPS": {
"STANDARD_FILTERS": "Standard Filters",
@@ -1,6 +1,6 @@
{
"AUTOMATION": {
"HEADER": "Автоматизация",
"HEADER": "Automations",
"HEADER_BTN_TXT": "Добавяне правило за автоматизация",
"LOADING": "Fetching automation rules",
"SIDEBAR_TXT": "<p><b>Automation Rules</b> <p>Automation can replace and automate existing processes that require manual effort. You can do many things with automation, including adding labels and assigning conversation to the best agent. So the team focuses on what they do best and spends more little time on manual tasks.</p>",
@@ -64,7 +64,7 @@
},
"EDIT": {
"TITLE": "Edit Automation Rule",
"SUBMIT": "Редактирай",
"SUBMIT": "Обновяване",
"CANCEL_BUTTON_TEXT": "Отмени",
"API": {
"SUCCESS_MESSAGE": "Automation rule updated successfully",
@@ -84,6 +84,24 @@
"DELETE": "Изтрий",
"CANCEL": "Отмени",
"RESET_MESSAGE": "Changing event type will reset the conditions and events you have added below"
},
"CONDITION": {
"DELETE_MESSAGE": "You need to have atleast one condition to save"
},
"ACTION": {
"DELETE_MESSAGE": "You need to have atleast one action to save"
},
"TOGGLE": {
"ACTIVATION_TITLE": "Activate Automation Rule",
"DEACTIVATION_TITLE": "Deactivate Automation Rule",
"ACTIVATION_DESCRIPTION": "This action will activate the automation rule '{automationName}'. Are you sure you want to proceed?",
"DEACTIVATION_DESCRIPTION": "This action will deactivate the automation rule '{automationName}'. Are you sure you want to proceed?",
"ACTIVATION_SUCCESFUL": "Automation Rule Activated Successfully",
"DEACTIVATION_SUCCESFUL": "Automation Rule Deactivated Successfully",
"ACTIVATION_ERROR": "Could not Activate Automation, Please try again later",
"DEACTIVATION_ERROR": "Could not Deactivate Automation, Please try again later",
"CONFIRMATION_LABEL": "Yes",
"CANCEL_LABEL": "No"
}
}
}
@@ -76,6 +76,7 @@
"RECEIVED_VIA_EMAIL": "Получено чрез имейл",
"VIEW_TWEET_IN_TWITTER": "Виж туита в Twitter",
"REPLY_TO_TWEET": "Отговори на този туит",
"LINK_TO_STORY": "Go to instagram story",
"SENT": "Успено изпратено",
"NO_MESSAGES": "Няма съобщения",
"NO_CONTENT": "Няма налично съдържание",
@@ -77,9 +77,8 @@
"CONFIRM": {
"TITLE": "Потвърди изтриването",
"MESSAGE": "Сигурни ли сте за изтриването ",
"PLACE_HOLDER": "Моля, въведете {contactName} за потвърждение",
"YES": "Да, изтрий ",
"NO": "Не, запази "
"YES": "Да, изтрий",
"NO": "Не, запази"
},
"API": {
"SUCCESS_MESSAGE": "Контакта е изтрит успешно",
@@ -22,7 +22,8 @@
"is_present": "Присъства",
"is_not_present": "Не присъства",
"is_greater_than": "Is greater than",
"is_lesser_than": "Is lesser than"
"is_lesser_than": "Is lesser than",
"days_before": "Is x days before"
},
"ATTRIBUTES": {
"NAME": "Име",
@@ -35,7 +36,9 @@
"CUSTOM_ATTRIBUTE_TEXT": "Text",
"CUSTOM_ATTRIBUTE_NUMBER": "Number",
"CUSTOM_ATTRIBUTE_LINK": "Link",
"CUSTOM_ATTRIBUTE_CHECKBOX": "Checkbox"
"CUSTOM_ATTRIBUTE_CHECKBOX": "Checkbox",
"CREATED_AT": "Created At",
"LAST_ACTIVITY": "Последна активност"
},
"GROUPS": {
"STANDARD_FILTERS": "Standard Filters",
@@ -61,7 +61,9 @@
"ENABLE_SIGN_TOOLTIP": "Enable signature",
"DISABLE_SIGN_TOOLTIP": "Disable signature",
"MSG_INPUT": "Shift + enter for new line. Start with '/' to select a Canned Response.",
"PRIVATE_MSG_INPUT": "Shift + enter for new line. This will be visible only to Agents"
"PRIVATE_MSG_INPUT": "Shift + enter for new line. This will be visible only to Agents",
"MESSAGE_SIGNATURE_NOT_CONFIGURED": "Message signature is not configured, please configure it in profile settings.",
"CLICK_HERE": "Click here to update"
},
"REPLYBOX": {
"REPLY": "Reply",
@@ -72,8 +74,14 @@
"TIP_FORMAT_ICON": "Show rich text editor",
"TIP_EMOJI_ICON": "Show emoji selector",
"TIP_ATTACH_ICON": "Attach files",
"TIP_AUDIORECORDER_ICON": "Record audio",
"TIP_AUDIORECORDER_PERMISSION": "Allow access to audio",
"TIP_AUDIORECORDER_ERROR": "Could not open the audio",
"ENTER_TO_SEND": "Enter to send",
"DRAG_DROP": "Drag and drop here to attach",
"START_AUDIO_RECORDING": "Start audio recording",
"STOP_AUDIO_RECORDING": "Stop audio recording",
"": "",
"EMAIL_HEAD": {
"ADD_BCC": "Add bcc",
"CC": {
@@ -47,7 +47,8 @@
"CUSTOM_EMAIL_DOMAIN_ENABLED": "You can receive emails in your custom domain now."
}
},
"UPDATE_CHATWOOT": "An update %{latestChatwootVersion} for Chatwoot is available. Please update your instance."
"UPDATE_CHATWOOT": "An update %{latestChatwootVersion} for Chatwoot is available. Please update your instance.",
"LEARN_MORE": "Learn more"
},
"FORMS": {
"MULTISELECT": {
@@ -462,7 +462,8 @@
"HOURS": "hours",
"VALIDATION_ERROR": "Starting time should be before closing time.",
"CHOOSE": "Choose"
}
},
"ALL_DAY": "All-Day"
},
"IMAP": {
"TITLE": "IMAP",
@@ -59,7 +59,56 @@
"CUSTOM_DATE_RANGE": {
"CONFIRM": "Apply",
"PLACEHOLDER": "Select date range"
}
},
"GROUP_BY_FILTER_DROPDOWN_LABEL": "Group By",
"GROUP_BY_DAY_OPTIONS": [
{
"id": 1,
"groupBy": "Day"
}
],
"GROUP_BY_WEEK_OPTIONS": [
{
"id": 1,
"groupBy": "Day"
},
{
"id": 2,
"groupBy": "Week"
}
],
"GROUP_BY_MONTH_OPTIONS": [
{
"id": 1,
"groupBy": "Day"
},
{
"id": 2,
"groupBy": "Week"
},
{
"id": 3,
"groupBy": "Month"
}
],
"GROUP_BY_YEAR_OPTIONS": [
{
"id": 1,
"groupBy": "Day"
},
{
"id": 2,
"groupBy": "Week"
},
{
"id": 3,
"groupBy": "Month"
},
{
"id": 4,
"groupBy": "Year"
}
]
},
"AGENT_REPORTS": {
"HEADER": "Agents Overview",
@@ -316,6 +365,11 @@
"CSAT_REPORTS": {
"HEADER": "CSAT Reports",
"NO_RECORDS": "There are no CSAT survey responses available.",
"FILTERS": {
"AGENTS": {
"PLACEHOLDER": "Choose Agents"
}
},
"TABLE": {
"HEADER": {
"CONTACT_NAME": "Contact",
@@ -146,6 +146,7 @@
}
},
"SIDEBAR": {
"CURRENTLY_VIEWING_ACCOUNT": "Currently viewing:",
"CONVERSATIONS": "Разговори",
"ALL_CONVERSATIONS": "All Conversations",
"MENTIONED_CONVERSATIONS": "Споменавания",
@@ -181,7 +182,8 @@
"REPORTS_LABEL": "Labels",
"REPORTS_INBOX": "Входяща кутия",
"REPORTS_TEAM": "Team",
"SET_AVAILABILITY_TITLE": "Set yourself as"
"SET_AVAILABILITY_TITLE": "Set yourself as",
"BETA": "Beta"
},
"CREATE_ACCOUNT": {
"NO_ACCOUNT_WARNING": "Uh oh! We could not find any Chatwoot accounts. Please create a new account to continue.",
@@ -21,7 +21,12 @@
"is_present": "Is present",
"is_not_present": "Is not present",
"is_greater_than": "Is greater than",
"is_lesser_than": "Is lesser than"
"is_less_than": "Is lesser than",
"days_before": "Is x days before"
},
"ATTRIBUTE_LABELS": {
"TRUE": "True",
"FALSE": "False"
},
"ATTRIBUTES": {
"STATUS": "Estat",
@@ -38,7 +43,9 @@
"CUSTOM_ATTRIBUTE_TEXT": "Text",
"CUSTOM_ATTRIBUTE_NUMBER": "Number",
"CUSTOM_ATTRIBUTE_LINK": "Link",
"CUSTOM_ATTRIBUTE_CHECKBOX": "Checkbox"
"CUSTOM_ATTRIBUTE_CHECKBOX": "Checkbox",
"CREATED_AT": "Created At",
"LAST_ACTIVITY": "Last Activity"
},
"GROUPS": {
"STANDARD_FILTERS": "Standard Filters",
@@ -1,6 +1,6 @@
{
"AUTOMATION": {
"HEADER": "Automation",
"HEADER": "Automations",
"HEADER_BTN_TXT": "Add Automation Rule",
"LOADING": "Fetching automation rules",
"SIDEBAR_TXT": "<p><b>Automation Rules</b> <p>Automation can replace and automate existing processes that require manual effort. You can do many things with automation, including adding labels and assigning conversation to the best agent. So the team focuses on what they do best and spends more little time on manual tasks.</p>",
@@ -64,7 +64,7 @@
},
"EDIT": {
"TITLE": "Edit Automation Rule",
"SUBMIT": "Edita",
"SUBMIT": "Actualitza",
"CANCEL_BUTTON_TEXT": "Cancel·la",
"API": {
"SUCCESS_MESSAGE": "Automation rule updated successfully",
@@ -84,6 +84,24 @@
"DELETE": "Esborrar",
"CANCEL": "Cancel·la",
"RESET_MESSAGE": "Changing event type will reset the conditions and events you have added below"
},
"CONDITION": {
"DELETE_MESSAGE": "You need to have atleast one condition to save"
},
"ACTION": {
"DELETE_MESSAGE": "You need to have atleast one action to save"
},
"TOGGLE": {
"ACTIVATION_TITLE": "Activate Automation Rule",
"DEACTIVATION_TITLE": "Deactivate Automation Rule",
"ACTIVATION_DESCRIPTION": "This action will activate the automation rule '{automationName}'. Are you sure you want to proceed?",
"DEACTIVATION_DESCRIPTION": "This action will deactivate the automation rule '{automationName}'. Are you sure you want to proceed?",
"ACTIVATION_SUCCESFUL": "Automation Rule Activated Successfully",
"DEACTIVATION_SUCCESFUL": "Automation Rule Deactivated Successfully",
"ACTIVATION_ERROR": "Could not Activate Automation, Please try again later",
"DEACTIVATION_ERROR": "Could not Deactivate Automation, Please try again later",
"CONFIRMATION_LABEL": "Si",
"CANCEL_LABEL": "No"
}
}
}
@@ -76,6 +76,7 @@
"RECEIVED_VIA_EMAIL": "Rebut per correu electrònic",
"VIEW_TWEET_IN_TWITTER": "Veure el tuit a Twitter",
"REPLY_TO_TWEET": "Respon a aquest tuit",
"LINK_TO_STORY": "Go to instagram story",
"SENT": "Sent successfully",
"NO_MESSAGES": "Cap Missatge",
"NO_CONTENT": "No content available",
@@ -77,9 +77,8 @@
"CONFIRM": {
"TITLE": "Confirma l'esborrat",
"MESSAGE": "N'estas segur? ",
"PLACE_HOLDER": "Please type {contactName} to confirm",
"YES": "Si, esborra ",
"NO": "No, segueix "
"YES": "Si, esborra",
"NO": "No, segueix"
},
"API": {
"SUCCESS_MESSAGE": "Contact deleted successfully",
@@ -22,7 +22,8 @@
"is_present": "Is present",
"is_not_present": "Is not present",
"is_greater_than": "Is greater than",
"is_lesser_than": "Is lesser than"
"is_lesser_than": "Is lesser than",
"days_before": "Is x days before"
},
"ATTRIBUTES": {
"NAME": "Nom",
@@ -35,7 +36,9 @@
"CUSTOM_ATTRIBUTE_TEXT": "Text",
"CUSTOM_ATTRIBUTE_NUMBER": "Number",
"CUSTOM_ATTRIBUTE_LINK": "Link",
"CUSTOM_ATTRIBUTE_CHECKBOX": "Checkbox"
"CUSTOM_ATTRIBUTE_CHECKBOX": "Checkbox",
"CREATED_AT": "Created At",
"LAST_ACTIVITY": "Last Activity"
},
"GROUPS": {
"STANDARD_FILTERS": "Standard Filters",
@@ -61,7 +61,9 @@
"ENABLE_SIGN_TOOLTIP": "Enable signature",
"DISABLE_SIGN_TOOLTIP": "Disable signature",
"MSG_INPUT": "Shift + enter per a una línia nova. Comença amb '/' per seleccionar una resposta predeterminada.",
"PRIVATE_MSG_INPUT": "Shift + enter per una línia nova. Això serà visible només per als Agents"
"PRIVATE_MSG_INPUT": "Shift + enter per una línia nova. Això serà visible només per als Agents",
"MESSAGE_SIGNATURE_NOT_CONFIGURED": "Message signature is not configured, please configure it in profile settings.",
"CLICK_HERE": "Click here to update"
},
"REPLYBOX": {
"REPLY": "Respon",
@@ -72,8 +74,14 @@
"TIP_FORMAT_ICON": "Mostra l'editor de text enriquit",
"TIP_EMOJI_ICON": "Mostra la selecció d'emoticones",
"TIP_ATTACH_ICON": "Ajuntar fitxers",
"TIP_AUDIORECORDER_ICON": "Record audio",
"TIP_AUDIORECORDER_PERMISSION": "Allow access to audio",
"TIP_AUDIORECORDER_ERROR": "Could not open the audio",
"ENTER_TO_SEND": "Intro per enviar",
"DRAG_DROP": "Drag and drop here to attach",
"START_AUDIO_RECORDING": "Start audio recording",
"STOP_AUDIO_RECORDING": "Stop audio recording",
"": "",
"EMAIL_HEAD": {
"ADD_BCC": "Add bcc",
"CC": {

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