Compare commits
256 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
98c289dc3e | ||
![]() |
3e91765472 | ||
![]() |
1bf23055df | ||
![]() |
2af337be10 | ||
![]() |
dbb6c0a074 | ||
![]() |
8c88344170 | ||
![]() |
6a78254701 | ||
![]() |
26ada8b342 | ||
![]() |
3c6bd2c8fd | ||
![]() |
2c2c47d7fd | ||
![]() |
34f7405689 | ||
![]() |
3ebfb3a140 | ||
![]() |
2dfe38ae4d | ||
![]() |
ca88eb55f4 | ||
![]() |
d1a26e80f4 | ||
![]() |
022d0b0ea3 | ||
![]() |
5541d9e00b | ||
![]() |
38587b3aa1 | ||
![]() |
4d2b7c37a0 | ||
![]() |
aaacf9d4d2 | ||
![]() |
82d3398932 | ||
![]() |
9106f6278d | ||
![]() |
f8e6308caf | ||
![]() |
72fcaa739c | ||
![]() |
9292653bf9 | ||
![]() |
2a1a38f986 | ||
![]() |
2972319026 | ||
![]() |
26e05de642 | ||
![]() |
8222a47154 | ||
![]() |
9d78f0d6c6 | ||
![]() |
86958278cd | ||
![]() |
823c836906 | ||
![]() |
6200559123 | ||
![]() |
7dc790a7e0 | ||
![]() |
431e2931c4 | ||
![]() |
52ea201070 | ||
![]() |
779bcf5e0d | ||
![]() |
6064aad38f | ||
![]() |
caa45d1d92 | ||
![]() |
f1d1bb84fd | ||
![]() |
01cc3d7c9c | ||
![]() |
89cfc5bbf3 | ||
![]() |
a82b9991b3 | ||
![]() |
06434bc655 | ||
![]() |
b9fd1d88ea | ||
![]() |
8004f67efe | ||
![]() |
87ef39ad9c | ||
![]() |
c3b6e1a732 | ||
![]() |
c9cae01cb4 | ||
![]() |
613fb0b064 | ||
![]() |
0b5c82ad5f | ||
![]() |
c8ec397c79 | ||
![]() |
a08099bbcc | ||
![]() |
e35638588a | ||
![]() |
3083f74d45 | ||
![]() |
85b52a1d3f | ||
![]() |
c94ba16565 | ||
![]() |
0cad3bed71 | ||
![]() |
edcbd53425 | ||
![]() |
a397f01692 | ||
![]() |
4755031e1d | ||
![]() |
fc9fc5a661 | ||
![]() |
b05d06a28a | ||
![]() |
8813c77907 | ||
![]() |
8ea0660862 | ||
![]() |
606fc9046a | ||
![]() |
e593e516b8 | ||
![]() |
b5f8524167 | ||
![]() |
b765e17457 | ||
![]() |
db37bfea06 | ||
![]() |
16bfd68d95 | ||
![]() |
33aacb3401 | ||
![]() |
47676c3cce | ||
![]() |
9bfbd528ef | ||
![]() |
e85f998a08 | ||
![]() |
66044a0dc3 | ||
![]() |
9b9c019de0 | ||
![]() |
86e0ff76c5 | ||
![]() |
abbb6ac676 | ||
![]() |
42b466bda2 | ||
![]() |
956837ded5 | ||
![]() |
f0ef497005 | ||
![]() |
8e2da837d4 | ||
![]() |
e7f1a9ab4d | ||
![]() |
826a735cdb | ||
![]() |
38ab3c36db | ||
![]() |
b5f7be0cd2 | ||
![]() |
efceaec950 | ||
![]() |
6aba352e0d | ||
![]() |
9eb861a3b7 | ||
![]() |
3184c8964d | ||
![]() |
865346223b | ||
![]() |
4f82859bba | ||
![]() |
47c90e2085 | ||
![]() |
7352b928da | ||
![]() |
5febdde938 | ||
![]() |
d39ace5a6b | ||
![]() |
e2059cfc5b | ||
![]() |
2e42821c48 | ||
![]() |
16d59f4bb0 | ||
![]() |
0d9ed0674b | ||
![]() |
6ff0c93659 | ||
![]() |
20406dce01 | ||
![]() |
b50890d1b5 | ||
![]() |
48373628a1 | ||
![]() |
479d88a480 | ||
![]() |
526722dffa | ||
![]() |
a23974d8b9 | ||
![]() |
894234e777 | ||
![]() |
dc3ee3464b | ||
![]() |
4bb5e812ba | ||
![]() |
8bd5ba187a | ||
![]() |
c121b44df4 | ||
![]() |
4c43330b15 | ||
![]() |
8b659de73d | ||
![]() |
936c2ec7e2 | ||
![]() |
9c0cce0392 | ||
![]() |
86ca7f4a8d | ||
![]() |
6cfd594d85 | ||
![]() |
e1190fd9bf | ||
![]() |
f2753df8df | ||
![]() |
00e06e5139 | ||
![]() |
be516a5ea6 | ||
![]() |
f8d9a27d7a | ||
![]() |
92724576af | ||
![]() |
a0606d36f6 | ||
![]() |
2073a23d5c | ||
![]() |
b20f5e5cef | ||
![]() |
3b09840d39 | ||
![]() |
2aa99ee137 | ||
![]() |
89d7e4ead6 | ||
![]() |
12cd15b6ad | ||
![]() |
352558dd11 | ||
![]() |
bedb2cab63 | ||
![]() |
abe439594e | ||
![]() |
bcde84b5b5 | ||
![]() |
06e2219110 | ||
![]() |
c3ec1d4f8a | ||
![]() |
d54392cb53 | ||
![]() |
3a71fe3260 | ||
![]() |
af020f446e | ||
![]() |
6823b04e5b | ||
![]() |
8d5a9a9daa | ||
![]() |
c3426929d7 | ||
![]() |
4a299a9441 | ||
![]() |
782165478b | ||
![]() |
95cc55d043 | ||
![]() |
a274a1702a | ||
![]() |
4d0b302802 | ||
![]() |
a8561cd798 | ||
![]() |
fa73b5290c | ||
![]() |
a3277b45af | ||
![]() |
10d86fbb35 | ||
![]() |
22d5703b92 | ||
![]() |
1fb1be3ddc | ||
![]() |
bce0bb8acb | ||
![]() |
ceaffe862a | ||
![]() |
199f462af4 | ||
![]() |
c542d2e0ff | ||
![]() |
6bc34db932 | ||
![]() |
0343acdb7e | ||
![]() |
f740727177 | ||
![]() |
3de8f256cb | ||
![]() |
2e7ab484bd | ||
![]() |
e34e975776 | ||
![]() |
a1ce188dab | ||
![]() |
2f7a16ae16 | ||
![]() |
71ca530292 | ||
![]() |
e19c6d5671 | ||
![]() |
2423def8e8 | ||
![]() |
1c44e43c43 | ||
![]() |
444809cc68 | ||
![]() |
20b4a91122 | ||
![]() |
73f5595762 | ||
![]() |
704554d453 | ||
![]() |
706ab872f3 | ||
![]() |
252eda14c6 | ||
![]() |
ce3730d640 | ||
![]() |
db53af91e7 | ||
![]() |
a6960dc2d3 | ||
![]() |
e310230f62 | ||
![]() |
1f271356ca | ||
![]() |
bf4338ef9e | ||
![]() |
a533f43fbf | ||
![]() |
8f4944fda0 | ||
![]() |
d2fd05ee4e | ||
![]() |
ee520bdf98 | ||
![]() |
6c048626d0 | ||
![]() |
0c8f744c33 | ||
![]() |
fca629a32a | ||
![]() |
1b5a335f93 | ||
![]() |
242de0b3f9 | ||
![]() |
1bdd59f025 | ||
![]() |
32d885a19b | ||
![]() |
6c160ccad5 | ||
![]() |
9b5c0de0ea | ||
![]() |
38776906ab | ||
![]() |
5f4b6f2ce4 | ||
![]() |
7419e413f4 | ||
![]() |
0b5a956e05 | ||
![]() |
2c8ded98aa | ||
![]() |
d727093538 | ||
![]() |
5bd5395d31 | ||
![]() |
779f815f8e | ||
![]() |
ce7d9be633 | ||
![]() |
788b766179 | ||
![]() |
0a9ea6e272 | ||
![]() |
b668723313 | ||
![]() |
bd445216e9 | ||
![]() |
cd4c1ef27e | ||
![]() |
8b0e95ece8 | ||
![]() |
7b1630b468 | ||
![]() |
c76aed6d5e | ||
![]() |
8df7547043 | ||
![]() |
beedfc47bf | ||
![]() |
9ea43a2678 | ||
![]() |
705d06ac3c | ||
![]() |
7b54990ae6 | ||
![]() |
4f0360c7a2 | ||
![]() |
57fcb79d71 | ||
![]() |
1819041f5a | ||
![]() |
74e03f9beb | ||
![]() |
6b47c7b43d | ||
![]() |
fcb9a9ab0c | ||
![]() |
83eee7df91 | ||
![]() |
543854eaa8 | ||
![]() |
336c09e072 | ||
![]() |
c1c57fb2cd | ||
![]() |
8e5d8fcdaf | ||
![]() |
d2d7355e2d | ||
![]() |
913224ad84 | ||
![]() |
b463ce5b1a | ||
![]() |
111016fe4c | ||
![]() |
cc4ef14faa | ||
![]() |
2d871a1ed5 | ||
![]() |
c556e9c694 | ||
![]() |
d28502b917 | ||
![]() |
1761100c77 | ||
![]() |
3a1e521b4c | ||
![]() |
06c9aad7b3 | ||
![]() |
eb8e348ec1 | ||
![]() |
54d0055e86 | ||
![]() |
3b7cae19e6 | ||
![]() |
6b80afaf50 | ||
![]() |
99de8f4500 | ||
![]() |
a773ad7d08 | ||
![]() |
97583e410c | ||
![]() |
678a0af962 | ||
![]() |
bc23c69605 | ||
![]() |
e585b227f1 | ||
![]() |
cb4dd7e633 | ||
![]() |
8af27d861b | ||
![]() |
59b31615ed | ||
![]() |
44f498be6d | ||
![]() |
1ea289e8b7 | ||
![]() |
a680b08251 |
1537 changed files with 59355 additions and 11315 deletions
|
@ -19,6 +19,7 @@ defaults: &defaults
|
||||||
- COVERAGE: true
|
- COVERAGE: true
|
||||||
- LOG_LEVEL: warn
|
- LOG_LEVEL: warn
|
||||||
parallelism: 4
|
parallelism: 4
|
||||||
|
resource_class: large
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
@ -122,9 +123,11 @@ jobs:
|
||||||
mkdir -p coverage
|
mkdir -p coverage
|
||||||
~/tmp/cc-test-reporter before-build
|
~/tmp/cc-test-reporter before-build
|
||||||
TESTFILES=$(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings)
|
TESTFILES=$(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings)
|
||||||
bundle exec rspec --profile 10 \
|
bundle exec rspec --format progress \
|
||||||
--out test-results/rspec/rspec.xml \
|
--format RspecJunitFormatter \
|
||||||
|
--out ~/tmp/test-results/rspec.xml \
|
||||||
-- ${TESTFILES}
|
-- ${TESTFILES}
|
||||||
|
no_output_timeout: 30m
|
||||||
- run:
|
- run:
|
||||||
name: Code Climate Test Coverage
|
name: Code Climate Test Coverage
|
||||||
command: |
|
command: |
|
||||||
|
@ -137,7 +140,7 @@ jobs:
|
||||||
~/tmp/cc-test-reporter before-build
|
~/tmp/cc-test-reporter before-build
|
||||||
TESTFILES=$(circleci tests glob **/specs/*.spec.js | circleci tests split --split-by=timings)
|
TESTFILES=$(circleci tests glob **/specs/*.spec.js | circleci tests split --split-by=timings)
|
||||||
yarn test:coverage --profile 10 \
|
yarn test:coverage --profile 10 \
|
||||||
--out test-results/frontend_specs/rspec.xml \
|
--out ~/tmp/test-results/yarn.xml \
|
||||||
-- ${TESTFILES}
|
-- ${TESTFILES}
|
||||||
- run:
|
- run:
|
||||||
name: Code Climate Test Coverage
|
name: Code Climate Test Coverage
|
||||||
|
|
|
@ -54,3 +54,5 @@ exclude_patterns:
|
||||||
- 'app/javascript/widget/i18n/index.js'
|
- 'app/javascript/widget/i18n/index.js'
|
||||||
- 'app/javascript/survey/i18n/index.js'
|
- 'app/javascript/survey/i18n/index.js'
|
||||||
- 'app/javascript/shared/constants/locales.js'
|
- 'app/javascript/shared/constants/locales.js'
|
||||||
|
- 'app/javascript/dashboard/helper/specs/macrosFixtures.js'
|
||||||
|
- 'app/javascript/dashboard/routes/dashboard/settings/macros/constants.js'
|
||||||
|
|
|
@ -7,8 +7,8 @@ end_of_line = lf
|
||||||
charset = utf-8
|
charset = utf-8
|
||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
indent_style = spaces
|
indent_style = space
|
||||||
tab_width = 2
|
tab_width = 2
|
||||||
|
|
||||||
[{*.{rb,erb,js,coffee,json,yml,css,scss,sh,markdown,md,html}]
|
[*.{rb,erb,js,coffee,json,yml,css,scss,sh,markdown,md,html}]
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
|
|
14
.env.example
14
.env.example
|
@ -3,6 +3,8 @@ SECRET_KEY_BASE=replace_with_lengthy_secure_hex
|
||||||
|
|
||||||
# Replace with the URL you are planning to use for your app
|
# Replace with the URL you are planning to use for your app
|
||||||
FRONTEND_URL=http://0.0.0.0:3000
|
FRONTEND_URL=http://0.0.0.0:3000
|
||||||
|
# To use a dedicated URL for help center pages
|
||||||
|
# HELPCENTER_URL=http://0.0.0.0:3000
|
||||||
|
|
||||||
# If the variable is set, all non-authenticated pages would fallback to the default locale.
|
# If the variable is set, all non-authenticated pages would fallback to the default locale.
|
||||||
# Whenever a new account is created, the default language will be DEFAULT_LOCALE instead of en
|
# Whenever a new account is created, the default language will be DEFAULT_LOCALE instead of en
|
||||||
|
@ -32,6 +34,11 @@ REDIS_SENTINELS=
|
||||||
# You can find list of master using "SENTINEL masters" command
|
# You can find list of master using "SENTINEL masters" command
|
||||||
REDIS_SENTINEL_MASTER_NAME=
|
REDIS_SENTINEL_MASTER_NAME=
|
||||||
|
|
||||||
|
# By default Chatwoot will pass REDIS_PASSWORD as the password value for sentinels
|
||||||
|
# Use the following environment variable to customize passwords for sentinels.
|
||||||
|
# Use empty string if sentinels are configured with out passwords
|
||||||
|
# REDIS_SENTINEL_PASSWORD=
|
||||||
|
|
||||||
# Redis premium breakage in heroku fix
|
# Redis premium breakage in heroku fix
|
||||||
# enable the following configuration
|
# enable the following configuration
|
||||||
# ref: https://github.com/chatwoot/chatwoot/issues/2420
|
# ref: https://github.com/chatwoot/chatwoot/issues/2420
|
||||||
|
@ -49,13 +56,14 @@ RAILS_MAX_THREADS=5
|
||||||
|
|
||||||
# The email from which all outgoing emails are sent
|
# The email from which all outgoing emails are sent
|
||||||
# could user either `email@yourdomain.com` or `BrandName <email@yourdomain.com>`
|
# could user either `email@yourdomain.com` or `BrandName <email@yourdomain.com>`
|
||||||
MAILER_SENDER_EMAIL="Chatwoot <accounts@chatwoot.com>"
|
MAILER_SENDER_EMAIL=Chatwoot <accounts@chatwoot.com>
|
||||||
|
|
||||||
#SMTP domain key is set up for HELO checking
|
#SMTP domain key is set up for HELO checking
|
||||||
SMTP_DOMAIN=chatwoot.com
|
SMTP_DOMAIN=chatwoot.com
|
||||||
# the default value is set "mailhog" and is used by docker-compose for development environments,
|
# Set the value to "mailhog" if using docker-compose for development environments,
|
||||||
# Set the value as "localhost" or your SMTP address in other environments
|
# Set the value as "localhost" or your SMTP address in other environments
|
||||||
SMTP_ADDRESS=mailhog
|
# If SMTP_ADDRESS is empty, Chatwoot would try to use sendmail(postfix)
|
||||||
|
SMTP_ADDRESS=
|
||||||
SMTP_PORT=1025
|
SMTP_PORT=1025
|
||||||
SMTP_USERNAME=
|
SMTP_USERNAME=
|
||||||
SMTP_PASSWORD=
|
SMTP_PASSWORD=
|
||||||
|
|
46
.github/ISSUE_TEMPLATE/bug_report.md
vendored
46
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
@ -6,6 +6,7 @@ labels: 'Bug'
|
||||||
assignees: ''
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Describe the bug**
|
**Describe the bug**
|
||||||
|
|
||||||
A clear and concise description of what the bug is.
|
A clear and concise description of what the bug is.
|
||||||
|
@ -16,11 +17,11 @@ Steps to reproduce the behavior:
|
||||||
1. Go to '...'
|
1. Go to '...'
|
||||||
2. Click on '....'
|
2. Click on '....'
|
||||||
3. Scroll down to '....'
|
3. Scroll down to '....'
|
||||||
4. See error
|
4. See the error
|
||||||
|
|
||||||
**Expected behavior**
|
**Expected behavior**
|
||||||
|
|
||||||
A clear and concise description of what you expected to happen.
|
Share a clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
**Screenshots**
|
**Screenshots**
|
||||||
|
|
||||||
|
@ -28,27 +29,50 @@ If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
**Browser logs**
|
**Browser logs**
|
||||||
|
|
||||||
Share the browser logs to debug the issue further
|
Share the browser logs to debug the issue further.
|
||||||
|
|
||||||
**Server logs**
|
**Server logs**
|
||||||
|
|
||||||
Share the server logs to debug the issue further
|
Share the server logs to debug the issue further.
|
||||||
|
|
||||||
**Environment**
|
**Environment**
|
||||||
|
|
||||||
Describe whether you are using Chatwoot Cloud (app.chatwoot.com) or a self hosted installation of Chatwoot. If you are using a self hosted installation of Chatwoot describe the type of deployment (Docker/Linux VM installation/Heroku)
|
Describe whether you are using Chatwoot Cloud (app.chatwoot.com) or a self-hosted installation of Chatwoot. If you are using a self-hosted installation of Chatwoot, describe the type of deployment (Docker/Linux VM installation/Heroku/Kubernetes/Other).
|
||||||
|
|
||||||
**Desktop (please complete the following information):**
|
- [ ] app.chatwoot.com (Chatwoot Cloud)
|
||||||
- OS: [e.g. iOS]
|
- [ ] Self-hosted
|
||||||
- Browser [e.g. chrome, safari]
|
- - [ ] Linux VM
|
||||||
|
- - [ ] Docker
|
||||||
|
- - [ ] Kubernetes
|
||||||
|
- - [ ] Heroku
|
||||||
|
- - [ ] Other (Please specify)
|
||||||
|
|
||||||
|
|
||||||
|
**Desktop (please complete the following information)** (If applicable)
|
||||||
|
- OS: [e.g. Linux, Windows, MacOS]
|
||||||
|
- Browser [e.g. chrome, firefox, safari]
|
||||||
- Version [e.g. 22]
|
- Version [e.g. 22]
|
||||||
|
|
||||||
**Smartphone (please complete the following information):**
|
**Smartphone (please complete the following information)** (If applicable)
|
||||||
- Device: [e.g. iPhone6]
|
- Device: [e.g. iPhone6, Pixel7]
|
||||||
- OS: [e.g. iOS8.1]
|
- OS: [e.g. iOS8.1]
|
||||||
- Browser [e.g. stock browser, safari]
|
- Browser [e.g. stock browser, firefox, safari]
|
||||||
- Version [e.g. 22]
|
- Version [e.g. 22]
|
||||||
|
|
||||||
|
**Docker** (If applicable)
|
||||||
|
|
||||||
|
Please share the output of the following.
|
||||||
|
- `docker version`
|
||||||
|
- `docker info`
|
||||||
|
- `docker-compose version`
|
||||||
|
|
||||||
|
**Cloud Provider** (If applicable)
|
||||||
|
- [ ] AWS
|
||||||
|
- [ ] GCP
|
||||||
|
- [ ] Azure
|
||||||
|
- [ ] DigitalOcean
|
||||||
|
- [ ] Others
|
||||||
|
|
||||||
**Additional context**
|
**Additional context**
|
||||||
|
|
||||||
Add any other context about the problem here.
|
Add any other context about the problem here.
|
||||||
|
|
9
.github/PULL_REQUEST_TEMPLATE.md
vendored
9
.github/PULL_REQUEST_TEMPLATE.md
vendored
|
@ -2,8 +2,7 @@
|
||||||
|
|
||||||
## Description
|
## Description
|
||||||
|
|
||||||
Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change.
|
Please include a summary of the change and issue(s) fixed. Also, mention relevant motivation, context, and any dependencies that this change requires.
|
||||||
|
|
||||||
Fixes # (issue)
|
Fixes # (issue)
|
||||||
|
|
||||||
## Type of change
|
## Type of change
|
||||||
|
@ -12,18 +11,18 @@ Please delete options that are not relevant.
|
||||||
|
|
||||||
- [ ] Bug fix (non-breaking change which fixes an issue)
|
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||||
- [ ] New feature (non-breaking change which adds functionality)
|
- [ ] New feature (non-breaking change which adds functionality)
|
||||||
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
- [ ] Breaking change (fix or feature that would cause existing functionality not to work as expected)
|
||||||
- [ ] This change requires a documentation update
|
- [ ] This change requires a documentation update
|
||||||
|
|
||||||
## How Has This Been Tested?
|
## How Has This Been Tested?
|
||||||
|
|
||||||
Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration
|
Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration.
|
||||||
|
|
||||||
|
|
||||||
## Checklist:
|
## Checklist:
|
||||||
|
|
||||||
- [ ] My code follows the style guidelines of this project
|
- [ ] My code follows the style guidelines of this project
|
||||||
- [ ] I have performed a self-review of my own code
|
- [ ] I have performed a self-review of my code
|
||||||
- [ ] I have commented on my code, particularly in hard-to-understand areas
|
- [ ] I have commented on my code, particularly in hard-to-understand areas
|
||||||
- [ ] I have made corresponding changes to the documentation
|
- [ ] I have made corresponding changes to the documentation
|
||||||
- [ ] My changes generate no new warnings
|
- [ ] My changes generate no new warnings
|
||||||
|
|
16
.github/workflows/nightly_installer.yml
vendored
16
.github/workflows/nightly_installer.yml
vendored
|
@ -23,6 +23,8 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
wget https://get.chatwoot.app/linux/install.sh
|
wget https://get.chatwoot.app/linux/install.sh
|
||||||
chmod +x install.sh
|
chmod +x install.sh
|
||||||
|
#fix for postgtres not starting automatically in gh action env
|
||||||
|
sed -i '/function configure_db() {/a sudo service postgresql start' install.sh
|
||||||
|
|
||||||
- name: create input file
|
- name: create input file
|
||||||
run: |
|
run: |
|
||||||
|
@ -33,20 +35,6 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
sudo ./install.sh --install < input
|
sudo ./install.sh --install < input
|
||||||
|
|
||||||
# temp fix for postgresql not starting
|
|
||||||
# automatically in gh action env
|
|
||||||
- name: start postgresql service
|
|
||||||
if: always()
|
|
||||||
run: |
|
|
||||||
sudo service postgresql start
|
|
||||||
|
|
||||||
#re-running the installer again
|
|
||||||
- name: Run the installer again
|
|
||||||
if: always()
|
|
||||||
run: |
|
|
||||||
sudo ./install.sh --install < input
|
|
||||||
|
|
||||||
|
|
||||||
# disabling http verify for now as http
|
# disabling http verify for now as http
|
||||||
# access to port 3000 fails in gh action env
|
# access to port 3000 fails in gh action env
|
||||||
# - name: Verify
|
# - name: Verify
|
||||||
|
|
1
.github/workflows/publish_foss_docker.yml
vendored
1
.github/workflows/publish_foss_docker.yml
vendored
|
@ -58,5 +58,6 @@ jobs:
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: docker/Dockerfile
|
file: docker/Dockerfile
|
||||||
|
platforms: linux/amd64
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ env.DOCKER_TAG }}
|
tags: ${{ env.DOCKER_TAG }}
|
||||||
|
|
7
.gitignore
vendored
7
.gitignore
vendored
|
@ -39,9 +39,6 @@ public/packs*
|
||||||
*.un~
|
*.un~
|
||||||
.jest-cache
|
.jest-cache
|
||||||
|
|
||||||
#VS Code files
|
|
||||||
.vscode
|
|
||||||
|
|
||||||
# ignore jetbrains IDE files
|
# ignore jetbrains IDE files
|
||||||
.idea
|
.idea
|
||||||
|
|
||||||
|
@ -62,4 +59,6 @@ package-lock.json
|
||||||
test/cypress/videos/*
|
test/cypress/videos/*
|
||||||
|
|
||||||
/config/master.key
|
/config/master.key
|
||||||
/config/*.enc
|
/config/*.enc
|
||||||
|
|
||||||
|
.vscode/settings.json
|
||||||
|
|
32
.vscode/extensions.json
vendored
Normal file
32
.vscode/extensions.json
vendored
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
// Spell check
|
||||||
|
"streetsidesoftware.code-spell-checker",
|
||||||
|
// Better Comments
|
||||||
|
"aaron-bond.better-comments",
|
||||||
|
// Rails Test Runner
|
||||||
|
"davidpallinder.rails-test-runner",
|
||||||
|
// Eslint
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
// Auto Close Tag
|
||||||
|
"formulahendry.auto-close-tag",
|
||||||
|
// Auto Rename Tag
|
||||||
|
"formulahendry.auto-rename-tag",
|
||||||
|
// Hight light colors
|
||||||
|
"naumovs.color-highlight",
|
||||||
|
// GitLens
|
||||||
|
"eamodio.gitlens",
|
||||||
|
// Ruby
|
||||||
|
"rebornix.ruby",
|
||||||
|
// Vue
|
||||||
|
"octref.vetur",
|
||||||
|
// Prettier
|
||||||
|
"esbenp.prettier-vscode",
|
||||||
|
// Dot Env
|
||||||
|
"mikestead.dotenv",
|
||||||
|
// HTML CSS Support
|
||||||
|
"ecmel.vscode-html-css",
|
||||||
|
// Tailwind CSS Intellisense
|
||||||
|
"bradlc.vscode-tailwindcss",
|
||||||
|
]
|
||||||
|
}
|
1
.vscode/settings.json
vendored
Normal file
1
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{}
|
11
Gemfile
11
Gemfile
|
@ -4,7 +4,7 @@ ruby '3.0.4'
|
||||||
|
|
||||||
##-- base gems for rails --##
|
##-- base gems for rails --##
|
||||||
gem 'rack-cors', require: 'rack/cors'
|
gem 'rack-cors', require: 'rack/cors'
|
||||||
gem 'rails', '~>6.1'
|
gem 'rails', '~> 6.1', '>= 6.1.6.1'
|
||||||
# Reduces boot times through caching; required in config/boot.rb
|
# Reduces boot times through caching; required in config/boot.rb
|
||||||
gem 'bootsnap', require: false
|
gem 'bootsnap', require: false
|
||||||
|
|
||||||
|
@ -56,7 +56,7 @@ gem 'activerecord-import'
|
||||||
gem 'dotenv-rails'
|
gem 'dotenv-rails'
|
||||||
gem 'foreman'
|
gem 'foreman'
|
||||||
gem 'puma'
|
gem 'puma'
|
||||||
gem 'webpacker', '~> 5.x'
|
gem 'webpacker', '~> 5.4', '>= 5.4.3'
|
||||||
# metrics on heroku
|
# metrics on heroku
|
||||||
gem 'barnes'
|
gem 'barnes'
|
||||||
|
|
||||||
|
@ -94,7 +94,7 @@ gem 'ddtrace'
|
||||||
gem 'elastic-apm'
|
gem 'elastic-apm'
|
||||||
gem 'newrelic_rpm'
|
gem 'newrelic_rpm'
|
||||||
gem 'scout_apm'
|
gem 'scout_apm'
|
||||||
gem 'sentry-rails', '~> 5.3'
|
gem 'sentry-rails', '~> 5.3', '>= 5.3.1'
|
||||||
gem 'sentry-ruby', '~> 5.3'
|
gem 'sentry-ruby', '~> 5.3'
|
||||||
gem 'sentry-sidekiq', '~> 5.3'
|
gem 'sentry-sidekiq', '~> 5.3'
|
||||||
|
|
||||||
|
@ -135,8 +135,6 @@ gem 'stripe'
|
||||||
## to populate db with sample data
|
## to populate db with sample data
|
||||||
gem 'faker'
|
gem 'faker'
|
||||||
|
|
||||||
gem 'ruby-saml', '= 1.11'
|
|
||||||
|
|
||||||
group :production, :staging do
|
group :production, :staging do
|
||||||
# we dont want request timing out in development while using byebug
|
# we dont want request timing out in development while using byebug
|
||||||
gem 'rack-timeout'
|
gem 'rack-timeout'
|
||||||
|
@ -176,7 +174,8 @@ group :development, :test do
|
||||||
gem 'listen'
|
gem 'listen'
|
||||||
gem 'mock_redis'
|
gem 'mock_redis'
|
||||||
gem 'pry-rails'
|
gem 'pry-rails'
|
||||||
gem 'rspec-rails', '~> 5.0.0'
|
gem 'rspec_junit_formatter'
|
||||||
|
gem 'rspec-rails', '~> 5.0.3'
|
||||||
gem 'rubocop', require: false
|
gem 'rubocop', require: false
|
||||||
gem 'rubocop-performance', require: false
|
gem 'rubocop-performance', require: false
|
||||||
gem 'rubocop-rails', require: false
|
gem 'rubocop-rails', require: false
|
||||||
|
|
38
Gemfile.lock
38
Gemfile.lock
|
@ -135,7 +135,7 @@ GEM
|
||||||
byebug (11.1.3)
|
byebug (11.1.3)
|
||||||
climate_control (1.1.1)
|
climate_control (1.1.1)
|
||||||
coderay (1.1.3)
|
coderay (1.1.3)
|
||||||
commonmarker (0.23.5)
|
commonmarker (0.23.6)
|
||||||
concurrent-ruby (1.1.10)
|
concurrent-ruby (1.1.10)
|
||||||
connection_pool (2.2.5)
|
connection_pool (2.2.5)
|
||||||
crack (0.4.5)
|
crack (0.4.5)
|
||||||
|
@ -286,9 +286,9 @@ GEM
|
||||||
google-cloud-core (~> 1.6)
|
google-cloud-core (~> 1.6)
|
||||||
googleauth (>= 0.16.2, < 2.a)
|
googleauth (>= 0.16.2, < 2.a)
|
||||||
mini_mime (~> 1.0)
|
mini_mime (~> 1.0)
|
||||||
google-protobuf (3.21.2)
|
google-protobuf (3.21.7)
|
||||||
google-protobuf (3.21.2-x86_64-darwin)
|
google-protobuf (3.21.7-x86_64-darwin)
|
||||||
google-protobuf (3.21.2-x86_64-linux)
|
google-protobuf (3.21.7-x86_64-linux)
|
||||||
googleapis-common-protos (1.3.12)
|
googleapis-common-protos (1.3.12)
|
||||||
google-protobuf (~> 3.14)
|
google-protobuf (~> 3.14)
|
||||||
googleapis-common-protos-types (~> 1.2)
|
googleapis-common-protos-types (~> 1.2)
|
||||||
|
@ -398,7 +398,7 @@ GEM
|
||||||
llhttp-ffi (0.4.0)
|
llhttp-ffi (0.4.0)
|
||||||
ffi-compiler (~> 1.0)
|
ffi-compiler (~> 1.0)
|
||||||
rake (~> 13.0)
|
rake (~> 13.0)
|
||||||
loofah (2.18.0)
|
loofah (2.19.1)
|
||||||
crass (~> 1.0.2)
|
crass (~> 1.0.2)
|
||||||
nokogiri (>= 1.5.9)
|
nokogiri (>= 1.5.9)
|
||||||
mail (2.7.1)
|
mail (2.7.1)
|
||||||
|
@ -427,14 +427,14 @@ GEM
|
||||||
netrc (0.11.0)
|
netrc (0.11.0)
|
||||||
newrelic_rpm (8.9.0)
|
newrelic_rpm (8.9.0)
|
||||||
nio4r (2.5.8)
|
nio4r (2.5.8)
|
||||||
nokogiri (1.13.8)
|
nokogiri (1.13.10)
|
||||||
mini_portile2 (~> 2.8.0)
|
mini_portile2 (~> 2.8.0)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.13.8-arm64-darwin)
|
nokogiri (1.13.10-arm64-darwin)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.13.8-x86_64-darwin)
|
nokogiri (1.13.10-x86_64-darwin)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.13.8-x86_64-linux)
|
nokogiri (1.13.10-x86_64-linux)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
oauth (0.5.10)
|
oauth (0.5.10)
|
||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
|
@ -459,7 +459,7 @@ GEM
|
||||||
pundit (2.2.0)
|
pundit (2.2.0)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
raabro (1.4.0)
|
raabro (1.4.0)
|
||||||
racc (1.6.0)
|
racc (1.6.1)
|
||||||
rack (2.2.4)
|
rack (2.2.4)
|
||||||
rack-attack (6.6.1)
|
rack-attack (6.6.1)
|
||||||
rack (>= 1.0, < 3)
|
rack (>= 1.0, < 3)
|
||||||
|
@ -488,8 +488,8 @@ GEM
|
||||||
rails-dom-testing (2.0.3)
|
rails-dom-testing (2.0.3)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
nokogiri (>= 1.6)
|
nokogiri (>= 1.6)
|
||||||
rails-html-sanitizer (1.4.3)
|
rails-html-sanitizer (1.4.4)
|
||||||
loofah (~> 2.3)
|
loofah (~> 2.19, >= 2.19.1)
|
||||||
railties (6.1.6.1)
|
railties (6.1.6.1)
|
||||||
actionpack (= 6.1.6.1)
|
actionpack (= 6.1.6.1)
|
||||||
activesupport (= 6.1.6.1)
|
activesupport (= 6.1.6.1)
|
||||||
|
@ -536,6 +536,8 @@ GEM
|
||||||
rspec-mocks (~> 3.10)
|
rspec-mocks (~> 3.10)
|
||||||
rspec-support (~> 3.10)
|
rspec-support (~> 3.10)
|
||||||
rspec-support (3.11.0)
|
rspec-support (3.11.0)
|
||||||
|
rspec_junit_formatter (0.6.0)
|
||||||
|
rspec-core (>= 2, < 4, != 2.12.0)
|
||||||
rubocop (1.31.2)
|
rubocop (1.31.2)
|
||||||
json (~> 2.3)
|
json (~> 2.3)
|
||||||
parallel (~> 1.10)
|
parallel (~> 1.10)
|
||||||
|
@ -558,8 +560,6 @@ GEM
|
||||||
rubocop-rspec (2.12.1)
|
rubocop-rspec (2.12.1)
|
||||||
rubocop (~> 1.31)
|
rubocop (~> 1.31)
|
||||||
ruby-progressbar (1.11.0)
|
ruby-progressbar (1.11.0)
|
||||||
ruby-saml (1.11.0)
|
|
||||||
nokogiri (>= 1.5.10)
|
|
||||||
ruby-vips (2.1.4)
|
ruby-vips (2.1.4)
|
||||||
ffi (~> 1.12)
|
ffi (~> 1.12)
|
||||||
ruby2_keywords (0.0.5)
|
ruby2_keywords (0.0.5)
|
||||||
|
@ -765,20 +765,20 @@ DEPENDENCIES
|
||||||
rack-attack
|
rack-attack
|
||||||
rack-cors
|
rack-cors
|
||||||
rack-timeout
|
rack-timeout
|
||||||
rails (~> 6.1)
|
rails (~> 6.1, >= 6.1.6.1)
|
||||||
redis
|
redis
|
||||||
redis-namespace
|
redis-namespace
|
||||||
responders
|
responders
|
||||||
rest-client
|
rest-client
|
||||||
rspec-rails (~> 5.0.0)
|
rspec-rails (~> 5.0.3)
|
||||||
|
rspec_junit_formatter
|
||||||
rubocop
|
rubocop
|
||||||
rubocop-performance
|
rubocop-performance
|
||||||
rubocop-rails
|
rubocop-rails
|
||||||
rubocop-rspec
|
rubocop-rspec
|
||||||
ruby-saml (= 1.11)
|
|
||||||
scout_apm
|
scout_apm
|
||||||
seed_dump
|
seed_dump
|
||||||
sentry-rails (~> 5.3)
|
sentry-rails (~> 5.3, >= 5.3.1)
|
||||||
sentry-ruby (~> 5.3)
|
sentry-ruby (~> 5.3)
|
||||||
sentry-sidekiq (~> 5.3)
|
sentry-sidekiq (~> 5.3)
|
||||||
shoulda-matchers
|
shoulda-matchers
|
||||||
|
@ -799,7 +799,7 @@ DEPENDENCIES
|
||||||
valid_email2
|
valid_email2
|
||||||
web-console
|
web-console
|
||||||
webmock
|
webmock
|
||||||
webpacker (~> 5.x)
|
webpacker (~> 5.4, >= 5.4.3)
|
||||||
webpush
|
webpush
|
||||||
wisper (= 2.0.0)
|
wisper (= 2.0.0)
|
||||||
working_hours
|
working_hours
|
||||||
|
|
14
app.json
14
app.json
|
@ -41,16 +41,24 @@
|
||||||
"formation": {
|
"formation": {
|
||||||
"web": {
|
"web": {
|
||||||
"quantity": 1,
|
"quantity": 1,
|
||||||
"size": "FREE"
|
"size": "basic"
|
||||||
},
|
},
|
||||||
"worker": {
|
"worker": {
|
||||||
"quantity": 1,
|
"quantity": 1,
|
||||||
"size": "FREE"
|
"size": "basic"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"stack": "heroku-20",
|
"stack": "heroku-20",
|
||||||
"image": "heroku/ruby",
|
"image": "heroku/ruby",
|
||||||
"addons": [ "heroku-redis", "heroku-postgresql"],
|
"addons": [
|
||||||
|
{
|
||||||
|
"plan": "heroku-redis:mini"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"plan": "heroku-postgresql:mini"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"stack": "heroku-20",
|
||||||
"buildpacks": [
|
"buildpacks": [
|
||||||
{
|
{
|
||||||
"url": "heroku/ruby"
|
"url": "heroku/ruby"
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
|
# This Builder will create a contact inbox with specified attributes. If the contact inbox already exists, it will be returned.
|
||||||
|
# For Specific Channels like whatsapp, email etc . it smartly generated appropriate the source id when none is provided.
|
||||||
|
|
||||||
class ContactInboxBuilder
|
class ContactInboxBuilder
|
||||||
pattr_initialize [:contact_id!, :inbox_id!, :source_id]
|
pattr_initialize [:contact, :inbox, :source_id, { hmac_verified: false }]
|
||||||
|
|
||||||
def perform
|
def perform
|
||||||
@contact = Contact.find(contact_id)
|
@source_id ||= generate_source_id
|
||||||
@inbox = @contact.account.inboxes.find(inbox_id)
|
create_contact_inbox if source_id.present?
|
||||||
return unless ['Channel::TwilioSms', 'Channel::Sms', 'Channel::Email', 'Channel::Api', 'Channel::Whatsapp'].include? @inbox.channel_type
|
|
||||||
|
|
||||||
source_id = @source_id || generate_source_id
|
|
||||||
create_contact_inbox(source_id) if source_id.present?
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -19,23 +18,37 @@ class ContactInboxBuilder
|
||||||
when 'Channel::Whatsapp'
|
when 'Channel::Whatsapp'
|
||||||
wa_source_id
|
wa_source_id
|
||||||
when 'Channel::Email'
|
when 'Channel::Email'
|
||||||
@contact.email
|
email_source_id
|
||||||
when 'Channel::Sms'
|
when 'Channel::Sms'
|
||||||
@contact.phone_number
|
phone_source_id
|
||||||
when 'Channel::Api'
|
when 'Channel::Api', 'Channel::WebWidget'
|
||||||
SecureRandom.uuid
|
SecureRandom.uuid
|
||||||
|
else
|
||||||
|
raise "Unsupported operation for this channel: #{@inbox.channel_type}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def email_source_id
|
||||||
|
raise ActionController::ParameterMissing, 'contact email' unless @contact.email
|
||||||
|
|
||||||
|
@contact.email
|
||||||
|
end
|
||||||
|
|
||||||
|
def phone_source_id
|
||||||
|
raise ActionController::ParameterMissing, 'contact phone number' unless @contact.phone_number
|
||||||
|
|
||||||
|
@contact.phone_number
|
||||||
|
end
|
||||||
|
|
||||||
def wa_source_id
|
def wa_source_id
|
||||||
return unless @contact.phone_number
|
raise ActionController::ParameterMissing, 'contact phone number' unless @contact.phone_number
|
||||||
|
|
||||||
# whatsapp doesn't want the + in e164 format
|
# whatsapp doesn't want the + in e164 format
|
||||||
"#{@contact.phone_number}.delete('+')"
|
@contact.phone_number.delete('+').to_s
|
||||||
end
|
end
|
||||||
|
|
||||||
def twilio_source_id
|
def twilio_source_id
|
||||||
return unless @contact.phone_number
|
raise ActionController::ParameterMissing, 'contact phone number' unless @contact.phone_number
|
||||||
|
|
||||||
case @inbox.channel.medium
|
case @inbox.channel.medium
|
||||||
when 'sms'
|
when 'sms'
|
||||||
|
@ -45,11 +58,11 @@ class ContactInboxBuilder
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_contact_inbox(source_id)
|
def create_contact_inbox
|
||||||
::ContactInbox.find_or_create_by!(
|
::ContactInbox.create_with(hmac_verified: hmac_verified || false).find_or_create_by!(
|
||||||
contact_id: @contact.id,
|
contact_id: @contact.id,
|
||||||
inbox_id: @inbox.id,
|
inbox_id: @inbox.id,
|
||||||
source_id: source_id
|
source_id: @source_id
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,25 +1,47 @@
|
||||||
class ContactBuilder
|
# This Builder will create a contact and contact inbox with specified attributes.
|
||||||
pattr_initialize [:source_id!, :inbox!, :contact_attributes!, :hmac_verified]
|
# If an existing identified contact exisits, it will be returned.
|
||||||
|
# for contact inbox logic it uses the contact inbox builder
|
||||||
|
|
||||||
|
class ContactInboxWithContactBuilder
|
||||||
|
pattr_initialize [:inbox!, :contact_attributes!, :source_id, :hmac_verified]
|
||||||
|
|
||||||
def perform
|
def perform
|
||||||
contact_inbox = inbox.contact_inboxes.find_by(source_id: source_id)
|
find_or_create_contact_and_contact_inbox
|
||||||
return contact_inbox if contact_inbox
|
# in case of race conditions where contact is created by another thread
|
||||||
|
# we will try to find the contact and create a contact inbox
|
||||||
|
rescue ActiveRecord::RecordNotUnique
|
||||||
|
find_or_create_contact_and_contact_inbox
|
||||||
|
end
|
||||||
|
|
||||||
build_contact_inbox
|
def find_or_create_contact_and_contact_inbox
|
||||||
|
@contact_inbox = inbox.contact_inboxes.find_by(source_id: source_id) if source_id.present?
|
||||||
|
return @contact_inbox if @contact_inbox
|
||||||
|
|
||||||
|
ActiveRecord::Base.transaction(requires_new: true) do
|
||||||
|
build_contact_with_contact_inbox
|
||||||
|
update_contact_avatar(@contact) unless @contact.avatar.attached?
|
||||||
|
@contact_inbox
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def build_contact_with_contact_inbox
|
||||||
|
@contact = find_contact || create_contact
|
||||||
|
@contact_inbox = create_contact_inbox
|
||||||
|
end
|
||||||
|
|
||||||
def account
|
def account
|
||||||
@account ||= inbox.account
|
@account ||= inbox.account
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_contact_inbox(contact)
|
def create_contact_inbox
|
||||||
::ContactInbox.create_with(hmac_verified: hmac_verified || false).find_or_create_by!(
|
ContactInboxBuilder.new(
|
||||||
contact_id: contact.id,
|
contact: @contact,
|
||||||
inbox_id: inbox.id,
|
inbox: @inbox,
|
||||||
source_id: source_id
|
source_id: @source_id,
|
||||||
)
|
hmac_verified: hmac_verified
|
||||||
|
).perform
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_contact_avatar(contact)
|
def update_contact_avatar(contact)
|
||||||
|
@ -61,16 +83,4 @@ class ContactBuilder
|
||||||
|
|
||||||
account.contacts.find_by(phone_number: phone_number)
|
account.contacts.find_by(phone_number: phone_number)
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_contact_inbox
|
|
||||||
ActiveRecord::Base.transaction do
|
|
||||||
contact = find_contact || create_contact
|
|
||||||
contact_inbox = create_contact_inbox(contact)
|
|
||||||
update_contact_avatar(contact)
|
|
||||||
contact_inbox
|
|
||||||
rescue StandardError => e
|
|
||||||
Rails.logger.error e
|
|
||||||
raise e
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
40
app/builders/conversation_builder.rb
Normal file
40
app/builders/conversation_builder.rb
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
class ConversationBuilder
|
||||||
|
pattr_initialize [:params!, :contact_inbox!]
|
||||||
|
|
||||||
|
def perform
|
||||||
|
look_up_exising_conversation || create_new_conversation
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def look_up_exising_conversation
|
||||||
|
return unless @contact_inbox.inbox.lock_to_single_conversation?
|
||||||
|
|
||||||
|
@contact_inbox.conversations.last
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_new_conversation
|
||||||
|
::Conversation.create!(conversation_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
def conversation_params
|
||||||
|
additional_attributes = params[:additional_attributes]&.permit! || {}
|
||||||
|
custom_attributes = params[:custom_attributes]&.permit! || {}
|
||||||
|
status = params[:status].present? ? { status: params[:status] } : {}
|
||||||
|
|
||||||
|
# TODO: temporary fallback for the old bot status in conversation, we will remove after couple of releases
|
||||||
|
# commenting this out to see if there are any errors, if not we can remove this in subsequent releases
|
||||||
|
# status = { status: 'pending' } if status[:status] == 'bot'
|
||||||
|
{
|
||||||
|
account_id: @contact_inbox.inbox.account_id,
|
||||||
|
inbox_id: @contact_inbox.inbox_id,
|
||||||
|
contact_id: @contact_inbox.contact_id,
|
||||||
|
contact_inbox_id: @contact_inbox.id,
|
||||||
|
additional_attributes: additional_attributes,
|
||||||
|
custom_attributes: custom_attributes,
|
||||||
|
snoozed_until: params[:snoozed_until],
|
||||||
|
assignee_id: params[:assignee_id],
|
||||||
|
team_id: params[:team_id]
|
||||||
|
}.merge(status)
|
||||||
|
end
|
||||||
|
end
|
|
@ -22,10 +22,9 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
|
||||||
return if @inbox.channel.reauthorization_required?
|
return if @inbox.channel.reauthorization_required?
|
||||||
|
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
build_contact
|
build_contact_inbox
|
||||||
build_message
|
build_message
|
||||||
end
|
end
|
||||||
ensure_contact_avatar
|
|
||||||
rescue Koala::Facebook::AuthenticationError
|
rescue Koala::Facebook::AuthenticationError
|
||||||
@inbox.channel.authorization_error!
|
@inbox.channel.authorization_error!
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
|
@ -35,15 +34,12 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def contact
|
def build_contact_inbox
|
||||||
@contact ||= @inbox.contact_inboxes.find_by(source_id: @sender_id)&.contact
|
@contact_inbox = ::ContactInboxWithContactBuilder.new(
|
||||||
end
|
source_id: @sender_id,
|
||||||
|
inbox: @inbox,
|
||||||
def build_contact
|
contact_attributes: contact_params
|
||||||
return if contact.present?
|
).perform
|
||||||
|
|
||||||
@contact = Contact.create!(contact_params.except(:remote_avatar_url))
|
|
||||||
@contact_inbox = ContactInbox.find_or_create_by!(contact: contact, inbox: @inbox, source_id: @sender_id)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_message
|
def build_message
|
||||||
|
@ -54,19 +50,11 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def ensure_contact_avatar
|
|
||||||
return if contact_params[:remote_avatar_url].blank?
|
|
||||||
return if @contact.avatar.attached?
|
|
||||||
|
|
||||||
Avatar::AvatarFromUrlJob.perform_later(@contact, contact_params[:remote_avatar_url])
|
|
||||||
end
|
|
||||||
|
|
||||||
def conversation
|
def conversation
|
||||||
@conversation ||= Conversation.find_by(conversation_params) || build_conversation
|
@conversation ||= Conversation.find_by(conversation_params) || build_conversation
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_conversation
|
def build_conversation
|
||||||
@contact_inbox ||= contact.contact_inboxes.find_by!(source_id: @sender_id)
|
|
||||||
Conversation.create!(conversation_params.merge(
|
Conversation.create!(conversation_params.merge(
|
||||||
contact_inbox_id: @contact_inbox.id
|
contact_inbox_id: @contact_inbox.id
|
||||||
))
|
))
|
||||||
|
@ -94,7 +82,7 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
|
||||||
{
|
{
|
||||||
account_id: @inbox.account_id,
|
account_id: @inbox.account_id,
|
||||||
inbox_id: @inbox.id,
|
inbox_id: @inbox.id,
|
||||||
contact_id: contact.id
|
contact_id: @contact_inbox.contact_id
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -105,7 +93,7 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
|
||||||
message_type: @message_type,
|
message_type: @message_type,
|
||||||
content: response.content,
|
content: response.content,
|
||||||
source_id: response.identifier,
|
source_id: response.identifier,
|
||||||
sender: @outgoing_echo ? nil : contact
|
sender: @outgoing_echo ? nil : @contact_inbox.contact
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -113,7 +101,7 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
|
||||||
{
|
{
|
||||||
name: "#{result['first_name'] || 'John'} #{result['last_name'] || 'Doe'}",
|
name: "#{result['first_name'] || 'John'} #{result['last_name'] || 'Doe'}",
|
||||||
account_id: @inbox.account_id,
|
account_id: @inbox.account_id,
|
||||||
remote_avatar_url: result['profile_pic'] || ''
|
avatar_url: result['profile_pic']
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -72,6 +72,7 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
|
||||||
|
|
||||||
def build_message
|
def build_message
|
||||||
return if @outgoing_echo && already_sent_from_chatwoot?
|
return if @outgoing_echo && already_sent_from_chatwoot?
|
||||||
|
return if message_content.blank? && all_unsupported_files?
|
||||||
|
|
||||||
@message = conversation.messages.create!(message_params)
|
@message = conversation.messages.create!(message_params)
|
||||||
|
|
||||||
|
@ -117,6 +118,13 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
|
||||||
cw_message.present?
|
cw_message.present?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def all_unsupported_files?
|
||||||
|
return if attachments.empty?
|
||||||
|
|
||||||
|
attachments_type = attachments.pluck(:type).uniq.first
|
||||||
|
unsupported_file_type?(attachments_type)
|
||||||
|
end
|
||||||
|
|
||||||
### Sample response
|
### Sample response
|
||||||
# {
|
# {
|
||||||
# "object": "instagram",
|
# "object": "instagram",
|
||||||
|
|
|
@ -35,7 +35,13 @@ class Messages::MessageBuilder
|
||||||
file: uploaded_attachment
|
file: uploaded_attachment
|
||||||
)
|
)
|
||||||
|
|
||||||
attachment.file_type = file_type(uploaded_attachment&.content_type) if uploaded_attachment.is_a?(ActionDispatch::Http::UploadedFile)
|
attachment.file_type = if uploaded_attachment.is_a?(String)
|
||||||
|
file_type_by_signed_id(
|
||||||
|
uploaded_attachment
|
||||||
|
)
|
||||||
|
else
|
||||||
|
file_type(uploaded_attachment&.content_type)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,8 @@ class Messages::Messenger::MessageBuilder
|
||||||
include ::FileTypeHelper
|
include ::FileTypeHelper
|
||||||
|
|
||||||
def process_attachment(attachment)
|
def process_attachment(attachment)
|
||||||
return if attachment['type'].to_sym == :template
|
# This check handles very rare case if there are multiple files to attach with only one usupported file
|
||||||
|
return if unsupported_file_type?(attachment['type'])
|
||||||
|
|
||||||
attachment_obj = @message.attachments.new(attachment_params(attachment).except(:remote_file_url))
|
attachment_obj = @message.attachments.new(attachment_params(attachment).except(:remote_file_url))
|
||||||
attachment_obj.save!
|
attachment_obj.save!
|
||||||
|
@ -45,6 +46,7 @@ class Messages::Messenger::MessageBuilder
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_attachment_file_type(attachment)
|
def update_attachment_file_type(attachment)
|
||||||
|
return if @message.reload.attachments.blank?
|
||||||
return unless attachment.file_type == 'share' || attachment.file_type == 'story_mention'
|
return unless attachment.file_type == 'share' || attachment.file_type == 'story_mention'
|
||||||
|
|
||||||
attachment.file_type = file_type(attachment.file&.content_type)
|
attachment.file_type = file_type(attachment.file&.content_type)
|
||||||
|
@ -61,6 +63,7 @@ class Messages::Messenger::MessageBuilder
|
||||||
story_sender = result['from']['username']
|
story_sender = result['from']['username']
|
||||||
message.content_attributes[:story_sender] = story_sender
|
message.content_attributes[:story_sender] = story_sender
|
||||||
message.content_attributes[:story_id] = story_id
|
message.content_attributes[:story_id] = story_id
|
||||||
|
message.content_attributes[:image_type] = 'story_mention'
|
||||||
message.content = I18n.t('conversations.messages.instagram_story_content', story_sender: story_sender)
|
message.content = I18n.t('conversations.messages.instagram_story_content', story_sender: story_sender)
|
||||||
message.save!
|
message.save!
|
||||||
end
|
end
|
||||||
|
@ -73,6 +76,7 @@ class Messages::Messenger::MessageBuilder
|
||||||
raise
|
raise
|
||||||
rescue Koala::Facebook::ClientError => e
|
rescue Koala::Facebook::ClientError => e
|
||||||
# The exception occurs when we are trying fetch the deleted story or blocked story.
|
# The exception occurs when we are trying fetch the deleted story or blocked story.
|
||||||
|
@message.attachments.destroy_all
|
||||||
@message.update(content: I18n.t('conversations.messages.instagram_deleted_story_content'))
|
@message.update(content: I18n.t('conversations.messages.instagram_deleted_story_content'))
|
||||||
Rails.logger.error e
|
Rails.logger.error e
|
||||||
{}
|
{}
|
||||||
|
@ -80,4 +84,10 @@ class Messages::Messenger::MessageBuilder
|
||||||
ChatwootExceptionTracker.new(e, account: @inbox.account).capture_exception
|
ChatwootExceptionTracker.new(e, account: @inbox.account).capture_exception
|
||||||
{}
|
{}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def unsupported_file_type?(attachment_type)
|
||||||
|
[:template, :unsupported_type].include? attachment_type.to_sym
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -25,7 +25,7 @@ class NotificationSubscriptionBuilder
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_identifier_subscription
|
def build_identifier_subscription
|
||||||
@identifier_subscription = user.notification_subscriptions.create(params.merge(identifier: identifier))
|
@identifier_subscription = user.notification_subscriptions.create!(params.merge(identifier: identifier))
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_identifier_subscription
|
def update_identifier_subscription
|
||||||
|
|
|
@ -5,9 +5,10 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
|
||||||
before_action :set_current_page, only: [:index]
|
before_action :set_current_page, only: [:index]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@articles_count = @portal.articles.count
|
@portal_articles = @portal.articles
|
||||||
@articles = @portal.articles
|
@all_articles = @portal_articles.search(list_params)
|
||||||
@articles = @articles.search(list_params) if list_params.present?
|
@articles_count = @all_articles.count
|
||||||
|
@articles = @all_articles.page(@current_page)
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
|
@ -37,13 +38,13 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def portal
|
def portal
|
||||||
@portal ||= Current.account.portals.find_by(slug: params[:portal_id])
|
@portal ||= Current.account.portals.find_by!(slug: params[:portal_id])
|
||||||
end
|
end
|
||||||
|
|
||||||
def article_params
|
def article_params
|
||||||
params.require(:article).permit(
|
params.require(:article).permit(
|
||||||
:title, :content, :description, :position, :category_id, :author_id, :associated_article_id, :status, meta: [:title, :description,
|
:title, :slug, :content, :description, :position, :category_id, :author_id, :associated_article_id, :status, meta: [:title, :description,
|
||||||
{ tags: [] }]
|
{ tags: [] }]
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -49,7 +49,7 @@ class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseCont
|
||||||
def clone
|
def clone
|
||||||
automation_rule = Current.account.automation_rules.find_by(id: params[:automation_rule_id])
|
automation_rule = Current.account.automation_rules.find_by(id: params[:automation_rule_id])
|
||||||
new_rule = automation_rule.dup
|
new_rule = automation_rule.dup
|
||||||
new_rule.save
|
new_rule.save!
|
||||||
@automation_rule = new_rule
|
@automation_rule = new_rule
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ class Api::V1::Accounts::CategoriesController < Api::V1::Accounts::BaseControlle
|
||||||
before_action :set_current_page, only: [:index]
|
before_action :set_current_page, only: [:index]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
|
@current_locale = params[:locale]
|
||||||
@categories = @portal.categories.search(params)
|
@categories = @portal.categories.search(params)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -44,7 +44,7 @@ class Api::V1::Accounts::Channels::TwilioChannelsController < Api::V1::Accounts:
|
||||||
phone_number: phone_number,
|
phone_number: phone_number,
|
||||||
medium: medium
|
medium: medium
|
||||||
)
|
)
|
||||||
@inbox = Current.account.inboxes.create(
|
@inbox = Current.account.inboxes.create!(
|
||||||
name: permitted_params[:name],
|
name: permitted_params[:name],
|
||||||
channel: @twilio_channel
|
channel: @twilio_channel
|
||||||
)
|
)
|
||||||
|
|
|
@ -2,8 +2,11 @@ class Api::V1::Accounts::Contacts::ContactInboxesController < Api::V1::Accounts:
|
||||||
before_action :ensure_inbox, only: [:create]
|
before_action :ensure_inbox, only: [:create]
|
||||||
|
|
||||||
def create
|
def create
|
||||||
source_id = params[:source_id] || SecureRandom.uuid
|
@contact_inbox = ContactInboxBuilder.new(
|
||||||
@contact_inbox = ContactInbox.create!(contact: @contact, inbox: @inbox, source_id: source_id)
|
contact: @contact,
|
||||||
|
inbox: @inbox,
|
||||||
|
source_id: params[:source_id]
|
||||||
|
).perform
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -2,7 +2,7 @@ class Api::V1::Accounts::Contacts::ConversationsController < Api::V1::Accounts::
|
||||||
def index
|
def index
|
||||||
@conversations = Current.account.conversations.includes(
|
@conversations = Current.account.conversations.includes(
|
||||||
:assignee, :contact, :inbox, :taggings
|
:assignee, :contact, :inbox, :taggings
|
||||||
).where(inbox_id: inbox_ids, contact_id: @contact.id)
|
).where(inbox_id: inbox_ids, contact_id: @contact.id).order(id: :desc).limit(20)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -134,8 +134,11 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||||
return if params[:inbox_id].blank?
|
return if params[:inbox_id].blank?
|
||||||
|
|
||||||
inbox = Current.account.inboxes.find(params[:inbox_id])
|
inbox = Current.account.inboxes.find(params[:inbox_id])
|
||||||
source_id = params[:source_id] || SecureRandom.uuid
|
ContactInboxBuilder.new(
|
||||||
ContactInbox.create(contact: @contact, inbox: inbox, source_id: source_id)
|
contact: @contact,
|
||||||
|
inbox: inbox,
|
||||||
|
source_id: params[:source_id]
|
||||||
|
).perform
|
||||||
end
|
end
|
||||||
|
|
||||||
def permitted_params
|
def permitted_params
|
||||||
|
|
|
@ -3,7 +3,7 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
||||||
include DateRangeHelper
|
include DateRangeHelper
|
||||||
|
|
||||||
before_action :conversation, except: [:index, :meta, :search, :create, :filter]
|
before_action :conversation, except: [:index, :meta, :search, :create, :filter]
|
||||||
before_action :contact_inbox, only: [:create]
|
before_action :inbox, :contact, :contact_inbox, only: [:create]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
result = conversation_finder.perform
|
result = conversation_finder.perform
|
||||||
|
@ -24,7 +24,7 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
||||||
|
|
||||||
def create
|
def create
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
@conversation = ::Conversation.create!(conversation_params)
|
@conversation = ConversationBuilder.new(params: params, contact_inbox: @contact_inbox).perform
|
||||||
Messages::MessageBuilder.new(Current.user, @conversation, params[:message]).perform if params[:message].present?
|
Messages::MessageBuilder.new(Current.user, @conversation, params[:message]).perform if params[:message].present?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -75,10 +75,13 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_last_seen
|
def update_last_seen
|
||||||
# rubocop:disable Rails/SkipsModelValidations
|
update_last_seen_on_conversation(DateTime.now.utc, assignee?)
|
||||||
@conversation.update_column(:agent_last_seen_at, DateTime.now.utc)
|
end
|
||||||
@conversation.update_column(:assignee_last_seen_at, DateTime.now.utc) if assignee?
|
|
||||||
# rubocop:enable Rails/SkipsModelValidations
|
def unread
|
||||||
|
last_incoming_message = @conversation.messages.incoming.last
|
||||||
|
last_seen_at = last_incoming_message.created_at - 1.second if last_incoming_message.present?
|
||||||
|
update_last_seen_on_conversation(last_seen_at, true)
|
||||||
end
|
end
|
||||||
|
|
||||||
def custom_attributes
|
def custom_attributes
|
||||||
|
@ -88,9 +91,18 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def update_last_seen_on_conversation(last_seen_at, update_assignee)
|
||||||
|
# rubocop:disable Rails/SkipsModelValidations
|
||||||
|
@conversation.update_column(:agent_last_seen_at, last_seen_at)
|
||||||
|
@conversation.update_column(:assignee_last_seen_at, last_seen_at) if update_assignee.present?
|
||||||
|
# rubocop:enable Rails/SkipsModelValidations
|
||||||
|
end
|
||||||
|
|
||||||
def set_conversation_status
|
def set_conversation_status
|
||||||
status = params[:status] == 'bot' ? 'pending' : params[:status]
|
# TODO: temporary fallback for the old bot status in conversation, we will remove after couple of releases
|
||||||
@conversation.status = status
|
# commenting this out to see if there are any errors, if not we can remove this in subsequent releases
|
||||||
|
# status = params[:status] == 'bot' ? 'pending' : params[:status]
|
||||||
|
@conversation.status = params[:status]
|
||||||
@conversation.snoozed_until = parse_date_time(params[:snoozed_until].to_s) if params[:snoozed_until]
|
@conversation.snoozed_until = parse_date_time(params[:snoozed_until].to_s) if params[:snoozed_until]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -109,51 +121,44 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
||||||
authorize @conversation.inbox, :show?
|
authorize @conversation.inbox, :show?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def inbox
|
||||||
|
return if params[:inbox_id].blank?
|
||||||
|
|
||||||
|
@inbox = Current.account.inboxes.find(params[:inbox_id])
|
||||||
|
authorize @inbox, :show?
|
||||||
|
end
|
||||||
|
|
||||||
|
def contact
|
||||||
|
return if params[:contact_id].blank?
|
||||||
|
|
||||||
|
@contact = Current.account.contacts.find(params[:contact_id])
|
||||||
|
end
|
||||||
|
|
||||||
def contact_inbox
|
def contact_inbox
|
||||||
@contact_inbox = build_contact_inbox
|
@contact_inbox = build_contact_inbox
|
||||||
|
|
||||||
|
# fallback for the old case where we do look up only using source id
|
||||||
|
# In future we need to change this and make sure we do look up on combination of inbox_id and source_id
|
||||||
|
# and deprecate the support of passing only source_id as the param
|
||||||
@contact_inbox ||= ::ContactInbox.find_by!(source_id: params[:source_id])
|
@contact_inbox ||= ::ContactInbox.find_by!(source_id: params[:source_id])
|
||||||
authorize @contact_inbox.inbox, :show?
|
authorize @contact_inbox.inbox, :show?
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_contact_inbox
|
def build_contact_inbox
|
||||||
return if params[:contact_id].blank? || params[:inbox_id].blank?
|
return if @inbox.blank? || @contact.blank?
|
||||||
|
|
||||||
inbox = Current.account.inboxes.find(params[:inbox_id])
|
|
||||||
authorize inbox, :show?
|
|
||||||
|
|
||||||
ContactInboxBuilder.new(
|
ContactInboxBuilder.new(
|
||||||
contact_id: params[:contact_id],
|
contact: @contact,
|
||||||
inbox_id: inbox.id,
|
inbox: @inbox,
|
||||||
source_id: params[:source_id]
|
source_id: params[:source_id]
|
||||||
).perform
|
).perform
|
||||||
end
|
end
|
||||||
|
|
||||||
def conversation_params
|
|
||||||
additional_attributes = params[:additional_attributes]&.permit! || {}
|
|
||||||
custom_attributes = params[:custom_attributes]&.permit! || {}
|
|
||||||
status = params[:status].present? ? { status: params[:status] } : {}
|
|
||||||
|
|
||||||
# TODO: temporary fallback for the old bot status in conversation, we will remove after couple of releases
|
|
||||||
status = { status: 'pending' } if status[:status] == 'bot'
|
|
||||||
{
|
|
||||||
account_id: Current.account.id,
|
|
||||||
inbox_id: @contact_inbox.inbox_id,
|
|
||||||
contact_id: @contact_inbox.contact_id,
|
|
||||||
contact_inbox_id: @contact_inbox.id,
|
|
||||||
additional_attributes: additional_attributes,
|
|
||||||
custom_attributes: custom_attributes,
|
|
||||||
snoozed_until: params[:snoozed_until],
|
|
||||||
assignee_id: params[:assignee_id],
|
|
||||||
team_id: params[:team_id]
|
|
||||||
}.merge(status)
|
|
||||||
end
|
|
||||||
|
|
||||||
def conversation_finder
|
def conversation_finder
|
||||||
@conversation_finder ||= ConversationFinder.new(current_user, params)
|
@conversation_finder ||= ConversationFinder.new(Current.user, params)
|
||||||
end
|
end
|
||||||
|
|
||||||
def assignee?
|
def assignee?
|
||||||
@conversation.assignee_id? && current_user == @conversation.assignee
|
@conversation.assignee_id? && Current.user == @conversation.assignee
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -22,7 +22,7 @@ class Api::V1::Accounts::CsatSurveyResponsesController < Api::V1::Accounts::Base
|
||||||
def download
|
def download
|
||||||
response.headers['Content-Type'] = 'text/csv'
|
response.headers['Content-Type'] = 'text/csv'
|
||||||
response.headers['Content-Disposition'] = 'attachment; filename=csat_report.csv'
|
response.headers['Content-Disposition'] = 'attachment; filename=csat_report.csv'
|
||||||
render layout: false, template: 'api/v1/accounts/csat_survey_responses/download.csv.erb', format: 'csv'
|
render layout: false, template: 'api/v1/accounts/csat_survey_responses/download', formats: [:csv]
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -113,7 +113,8 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||||
|
|
||||||
def inbox_attributes
|
def inbox_attributes
|
||||||
[:name, :avatar, :greeting_enabled, :greeting_message, :enable_email_collect, :csat_survey_enabled,
|
[:name, :avatar, :greeting_enabled, :greeting_message, :enable_email_collect, :csat_survey_enabled,
|
||||||
:enable_auto_assignment, :working_hours_enabled, :out_of_office_message, :timezone, :allow_messages_after_resolved]
|
:enable_auto_assignment, :working_hours_enabled, :out_of_office_message, :timezone, :allow_messages_after_resolved,
|
||||||
|
:lock_to_single_conversation]
|
||||||
end
|
end
|
||||||
|
|
||||||
def permitted_params(channel_attributes = [])
|
def permitted_params(channel_attributes = [])
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
class Api::V1::Accounts::MacrosController < Api::V1::Accounts::BaseController
|
class Api::V1::Accounts::MacrosController < Api::V1::Accounts::BaseController
|
||||||
before_action :check_authorization
|
|
||||||
before_action :fetch_macro, only: [:show, :update, :destroy, :execute]
|
before_action :fetch_macro, only: [:show, :update, :destroy, :execute]
|
||||||
|
before_action :check_authorization, only: [:show, :update, :destroy, :execute]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@macros = Macro.with_visibility(current_user, params)
|
@macros = Macro.with_visibility(current_user, params)
|
||||||
|
@ -14,19 +14,34 @@ class Api::V1::Accounts::MacrosController < Api::V1::Accounts::BaseController
|
||||||
render json: { error: @macro.errors.messages }, status: :unprocessable_entity and return unless @macro.valid?
|
render json: { error: @macro.errors.messages }, status: :unprocessable_entity and return unless @macro.valid?
|
||||||
|
|
||||||
@macro.save!
|
@macro.save!
|
||||||
|
process_attachments
|
||||||
|
@macro
|
||||||
end
|
end
|
||||||
|
|
||||||
def show; end
|
def show
|
||||||
|
head :not_found if @macro.nil?
|
||||||
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@macro.destroy!
|
@macro.destroy!
|
||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def attach_file
|
||||||
|
file_blob = ActiveStorage::Blob.create_and_upload!(
|
||||||
|
key: nil,
|
||||||
|
io: params[:attachment].tempfile,
|
||||||
|
filename: params[:attachment].original_filename,
|
||||||
|
content_type: params[:attachment].content_type
|
||||||
|
)
|
||||||
|
render json: { blob_key: file_blob.key, blob_id: file_blob.id }
|
||||||
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
@macro.update!(macros_with_user)
|
@macro.update!(macros_with_user)
|
||||||
@macro.set_visibility(current_user, permitted_params)
|
@macro.set_visibility(current_user, permitted_params)
|
||||||
|
process_attachments
|
||||||
@macro.save!
|
@macro.save!
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Rails.logger.error e
|
Rails.logger.error e
|
||||||
|
@ -40,6 +55,19 @@ class Api::V1::Accounts::MacrosController < Api::V1::Accounts::BaseController
|
||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def process_attachments
|
||||||
|
actions = @macro.actions.filter_map { |k, _v| k if k['action_name'] == 'send_attachment' }
|
||||||
|
return if actions.blank?
|
||||||
|
|
||||||
|
actions.each do |action|
|
||||||
|
blob_id = action['action_params']
|
||||||
|
blob = ActiveStorage::Blob.find_by(id: blob_id)
|
||||||
|
@macro.files.attach(blob)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def permitted_params
|
def permitted_params
|
||||||
params.permit(
|
params.permit(
|
||||||
:name, :account_id, :visibility,
|
:name, :account_id, :visibility,
|
||||||
|
@ -54,4 +82,8 @@ class Api::V1::Accounts::MacrosController < Api::V1::Accounts::BaseController
|
||||||
def fetch_macro
|
def fetch_macro
|
||||||
@macro = Current.account.macros.find_by(id: params[:id])
|
@macro = Current.account.macros.find_by(id: params[:id])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def check_authorization
|
||||||
|
authorize(@macro) if @macro.present?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -14,10 +14,14 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
|
||||||
@portal.members << agents
|
@portal.members << agents
|
||||||
end
|
end
|
||||||
|
|
||||||
def show; end
|
def show
|
||||||
|
@all_articles = @portal.articles
|
||||||
|
@articles = @all_articles.search(locale: params[:locale])
|
||||||
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@portal = Current.account.portals.build(portal_params)
|
@portal = Current.account.portals.build(portal_params)
|
||||||
|
@portal.custom_domain = parsed_custom_domain
|
||||||
@portal.save!
|
@portal.save!
|
||||||
process_attached_logo
|
process_attached_logo
|
||||||
end
|
end
|
||||||
|
@ -25,6 +29,7 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
|
||||||
def update
|
def update
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
@portal.update!(portal_params) if params[:portal].present?
|
@portal.update!(portal_params) if params[:portal].present?
|
||||||
|
# @portal.custom_domain = parsed_custom_domain
|
||||||
process_attached_logo
|
process_attached_logo
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Rails.logger.error e
|
Rails.logger.error e
|
||||||
|
@ -70,4 +75,9 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
|
||||||
def set_current_page
|
def set_current_page
|
||||||
@current_page = params[:page] || 1
|
@current_page = params[:page] || 1
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def parsed_custom_domain
|
||||||
|
domain = URI.parse(@portal.custom_domain)
|
||||||
|
domain.is_a?(URI::HTTP) ? domain.host : @portal.custom_domain
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -24,7 +24,7 @@ class Api::V1::AccountsController < Api::BaseController
|
||||||
).perform
|
).perform
|
||||||
if @user
|
if @user
|
||||||
send_auth_headers(@user)
|
send_auth_headers(@user)
|
||||||
render 'api/v1/accounts/create.json', locals: { resource: @user }
|
render 'api/v1/accounts/create', format: :json, locals: { resource: @user }
|
||||||
else
|
else
|
||||||
render_error_response(CustomExceptions::Account::SignupFailed.new({}))
|
render_error_response(CustomExceptions::Account::SignupFailed.new({}))
|
||||||
end
|
end
|
||||||
|
@ -32,7 +32,7 @@ class Api::V1::AccountsController < Api::BaseController
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@latest_chatwoot_version = ::Redis::Alfred.get(::Redis::Alfred::LATEST_CHATWOOT_VERSION)
|
@latest_chatwoot_version = ::Redis::Alfred.get(::Redis::Alfred::LATEST_CHATWOOT_VERSION)
|
||||||
render 'api/v1/accounts/show.json'
|
render 'api/v1/accounts/show', format: :json
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
|
|
|
@ -9,7 +9,7 @@ class Api::V1::NotificationSubscriptionsController < Api::BaseController
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
notification_subscription = NotificationSubscription.where(["subscription_attributes->>'push_token' = ?", params[:push_token]]).first
|
notification_subscription = NotificationSubscription.where(["subscription_attributes->>'push_token' = ?", params[:push_token]]).first
|
||||||
notification_subscription.destroy!
|
notification_subscription.destroy! if notification_subscription.present?
|
||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -18,10 +18,19 @@ class Api::V1::ProfilesController < Api::BaseController
|
||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def auto_offline
|
||||||
|
@user.account_users.find_by!(account_id: auto_offline_params[:account_id]).update!(auto_offline: auto_offline_params[:auto_offline] || false)
|
||||||
|
end
|
||||||
|
|
||||||
def availability
|
def availability
|
||||||
@user.account_users.find_by!(account_id: availability_params[:account_id]).update!(availability: availability_params[:availability])
|
@user.account_users.find_by!(account_id: availability_params[:account_id]).update!(availability: availability_params[:availability])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def set_active_account
|
||||||
|
@user.account_users.find_by(account_id: profile_params[:account_id]).update(active_at: Time.now.utc)
|
||||||
|
head :ok
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_user
|
def set_user
|
||||||
|
@ -32,6 +41,10 @@ class Api::V1::ProfilesController < Api::BaseController
|
||||||
params.require(:profile).permit(:account_id, :availability)
|
params.require(:profile).permit(:account_id, :availability)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def auto_offline_params
|
||||||
|
params.require(:profile).permit(:account_id, :auto_offline)
|
||||||
|
end
|
||||||
|
|
||||||
def profile_params
|
def profile_params
|
||||||
params.require(:profile).permit(
|
params.require(:profile).permit(
|
||||||
:email,
|
:email,
|
||||||
|
@ -39,6 +52,7 @@ class Api::V1::ProfilesController < Api::BaseController
|
||||||
:display_name,
|
:display_name,
|
||||||
:avatar,
|
:avatar,
|
||||||
:message_signature,
|
:message_signature,
|
||||||
|
:account_id,
|
||||||
ui_settings: {}
|
ui_settings: {}
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
|
@ -50,7 +50,9 @@ class Api::V1::Widget::BaseController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def contact_name
|
def contact_name
|
||||||
params[:contact][:name] || contact_email.split('@')[0] if contact_email.present?
|
return if @contact.email.present? || @contact.phone_number.present? || @contact.identifier.present?
|
||||||
|
|
||||||
|
permitted_params.dig(:contact, :name) || (contact_email.split('@')[0] if contact_email.present?)
|
||||||
end
|
end
|
||||||
|
|
||||||
def contact_phone_number
|
def contact_phone_number
|
||||||
|
|
|
@ -9,7 +9,7 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
process_update_contact
|
process_update_contact
|
||||||
@conversation = create_conversation
|
@conversation = create_conversation
|
||||||
conversation.messages.create(message_params)
|
conversation.messages.create!(message_params)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -17,7 +17,8 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
||||||
@contact = ContactIdentifyAction.new(
|
@contact = ContactIdentifyAction.new(
|
||||||
contact: @contact,
|
contact: @contact,
|
||||||
params: { email: contact_email, phone_number: contact_phone_number, name: contact_name },
|
params: { email: contact_email, phone_number: contact_phone_number, name: contact_name },
|
||||||
retain_original_contact_name: true
|
retain_original_contact_name: true,
|
||||||
|
discard_invalid_attrs: true
|
||||||
).perform
|
).perform
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -59,7 +60,7 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
||||||
|
|
||||||
unless conversation.resolved?
|
unless conversation.resolved?
|
||||||
conversation.status = :resolved
|
conversation.status = :resolved
|
||||||
conversation.save
|
conversation.save!
|
||||||
end
|
end
|
||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
|
@ -14,22 +14,22 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
|
||||||
|
|
||||||
def agents
|
def agents
|
||||||
@report_data = generate_agents_report
|
@report_data = generate_agents_report
|
||||||
generate_csv('agents_report', 'api/v2/accounts/reports/agents.csv.erb')
|
generate_csv('agents_report', 'api/v2/accounts/reports/agents')
|
||||||
end
|
end
|
||||||
|
|
||||||
def inboxes
|
def inboxes
|
||||||
@report_data = generate_inboxes_report
|
@report_data = generate_inboxes_report
|
||||||
generate_csv('inboxes_report', 'api/v2/accounts/reports/inboxes.csv.erb')
|
generate_csv('inboxes_report', 'api/v2/accounts/reports/inboxes')
|
||||||
end
|
end
|
||||||
|
|
||||||
def labels
|
def labels
|
||||||
@report_data = generate_labels_report
|
@report_data = generate_labels_report
|
||||||
generate_csv('labels_report', 'api/v2/accounts/reports/labels.csv.erb')
|
generate_csv('labels_report', 'api/v2/accounts/reports/labels')
|
||||||
end
|
end
|
||||||
|
|
||||||
def teams
|
def teams
|
||||||
@report_data = generate_teams_report
|
@report_data = generate_teams_report
|
||||||
generate_csv('teams_report', 'api/v2/accounts/reports/teams.csv.erb')
|
generate_csv('teams_report', 'api/v2/accounts/reports/teams')
|
||||||
end
|
end
|
||||||
|
|
||||||
def conversations
|
def conversations
|
||||||
|
@ -43,7 +43,7 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
|
||||||
def generate_csv(filename, template)
|
def generate_csv(filename, template)
|
||||||
response.headers['Content-Type'] = 'text/csv'
|
response.headers['Content-Type'] = 'text/csv'
|
||||||
response.headers['Content-Disposition'] = "attachment; filename=#{filename}.csv"
|
response.headers['Content-Disposition'] = "attachment; filename=#{filename}.csv"
|
||||||
render layout: false, template: template, format: 'csv'
|
render layout: false, template: template, formats: [:csv]
|
||||||
end
|
end
|
||||||
|
|
||||||
def check_authorization
|
def check_authorization
|
||||||
|
|
|
@ -13,6 +13,8 @@ module RequestExceptionHandler
|
||||||
render_not_found_error('Resource could not be found')
|
render_not_found_error('Resource could not be found')
|
||||||
rescue Pundit::NotAuthorizedError
|
rescue Pundit::NotAuthorizedError
|
||||||
render_unauthorized('You are not authorized to do this action')
|
render_unauthorized('You are not authorized to do this action')
|
||||||
|
rescue ActionController::ParameterMissing => e
|
||||||
|
render_could_not_create_error(e.message)
|
||||||
ensure
|
ensure
|
||||||
# to address the thread variable leak issues in Puma/Thin webserver
|
# to address the thread variable leak issues in Puma/Thin webserver
|
||||||
Current.reset
|
Current.reset
|
||||||
|
|
|
@ -4,8 +4,7 @@ class DashboardController < ActionController::Base
|
||||||
before_action :set_global_config
|
before_action :set_global_config
|
||||||
around_action :switch_locale
|
around_action :switch_locale
|
||||||
before_action :ensure_installation_onboarding, only: [:index]
|
before_action :ensure_installation_onboarding, only: [:index]
|
||||||
before_action :redirect_to_custom_domain_page
|
before_action :render_hc_if_custom_domain, only: [:index]
|
||||||
before_action :redirect_to_saml_login
|
|
||||||
|
|
||||||
layout 'vueapp'
|
layout 'vueapp'
|
||||||
|
|
||||||
|
@ -17,8 +16,7 @@ class DashboardController < ActionController::Base
|
||||||
@global_config = GlobalConfig.get(
|
@global_config = GlobalConfig.get(
|
||||||
'LOGO', 'LOGO_THUMBNAIL',
|
'LOGO', 'LOGO_THUMBNAIL',
|
||||||
'INSTALLATION_NAME',
|
'INSTALLATION_NAME',
|
||||||
'WIDGET_BRAND_URL',
|
'WIDGET_BRAND_URL', 'TERMS_URL',
|
||||||
'TERMS_URL',
|
|
||||||
'PRIVACY_URL',
|
'PRIVACY_URL',
|
||||||
'DISPLAY_MANIFEST',
|
'DISPLAY_MANIFEST',
|
||||||
'CREATE_NEW_ACCOUNT_FROM_DASHBOARD',
|
'CREATE_NEW_ACCOUNT_FROM_DASHBOARD',
|
||||||
|
@ -26,12 +24,12 @@ class DashboardController < ActionController::Base
|
||||||
'API_CHANNEL_NAME',
|
'API_CHANNEL_NAME',
|
||||||
'API_CHANNEL_THUMBNAIL',
|
'API_CHANNEL_THUMBNAIL',
|
||||||
'ANALYTICS_TOKEN',
|
'ANALYTICS_TOKEN',
|
||||||
'ANALYTICS_HOST',
|
|
||||||
'DIRECT_UPLOADS_ENABLED',
|
'DIRECT_UPLOADS_ENABLED',
|
||||||
'HCAPTCHA_SITE_KEY',
|
'HCAPTCHA_SITE_KEY',
|
||||||
'LOGOUT_REDIRECT_LINK',
|
'LOGOUT_REDIRECT_LINK',
|
||||||
'DISABLE_USER_PROFILE_UPDATE',
|
'DISABLE_USER_PROFILE_UPDATE',
|
||||||
'DEPLOYMENT_ENV'
|
'DEPLOYMENT_ENV',
|
||||||
|
'CSML_EDITOR_HOST'
|
||||||
).merge(app_config)
|
).merge(app_config)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -39,17 +37,15 @@ class DashboardController < ActionController::Base
|
||||||
redirect_to '/installation/onboarding' if ::Redis::Alfred.get(::Redis::Alfred::CHATWOOT_INSTALLATION_ONBOARDING)
|
redirect_to '/installation/onboarding' if ::Redis::Alfred.get(::Redis::Alfred::CHATWOOT_INSTALLATION_ONBOARDING)
|
||||||
end
|
end
|
||||||
|
|
||||||
def redirect_to_custom_domain_page
|
def render_hc_if_custom_domain
|
||||||
custom_domain = request.host
|
domain = request.host
|
||||||
portal = Portal.find_by(custom_domain: custom_domain)
|
return if domain == URI.parse(ENV.fetch('FRONTEND_URL', '')).host
|
||||||
|
|
||||||
return unless portal
|
@portal = Portal.find_by(custom_domain: domain)
|
||||||
|
return unless @portal
|
||||||
|
|
||||||
redirect_to "/hc/#{portal.slug}"
|
@locale = @portal.default_locale
|
||||||
end
|
render 'public/api/v1/portals/show', layout: 'portal', portal: @portal and return
|
||||||
|
|
||||||
def redirect_to_saml_login
|
|
||||||
redirect_to '/saml' and return unless current_user
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def app_config
|
def app_config
|
||||||
|
@ -62,29 +58,4 @@ class DashboardController < ActionController::Base
|
||||||
IS_ENTERPRISE: ChatwootApp.enterprise?
|
IS_ENTERPRISE: ChatwootApp.enterprise?
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def saml_settings
|
|
||||||
settings = OneLogin::RubySaml::Settings.new
|
|
||||||
|
|
||||||
settings.assertion_consumer_service_url = "http://#{request.host}/saml/consume"
|
|
||||||
settings.sp_entity_id = "http://#{request.host}/saml/metadata"
|
|
||||||
settings.idp_entity_id = 'https://app.onelogin.com/saml/metadata/1835014'
|
|
||||||
settings.idp_sso_target_url = 'https://app.onelogin.com/trust/saml2/http-post/sso/1835014'
|
|
||||||
settings.idp_slo_target_url = 'https://app.onelogin.com/trust/saml2/http-redirect/slo/1835014'
|
|
||||||
settings.name_identifier_format = 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'
|
|
||||||
|
|
||||||
# Optional for most SAML IdPs
|
|
||||||
settings.authn_context = 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport'
|
|
||||||
# or as an array
|
|
||||||
settings.authn_context = [
|
|
||||||
'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport',
|
|
||||||
'urn:oasis:names:tc:SAML:2.0:ac:classes:Password'
|
|
||||||
]
|
|
||||||
|
|
||||||
# Optional bindings (defaults to Redirect for logout POST for ACS)
|
|
||||||
settings.single_logout_service_binding = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect' # or :post, :redirect
|
|
||||||
settings.assertion_consumer_service_binding = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST' # or :post, :redirect
|
|
||||||
|
|
||||||
settings
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -14,7 +14,7 @@ class DeviseOverrides::ConfirmationsController < Devise::ConfirmationsController
|
||||||
|
|
||||||
def render_confirmation_success
|
def render_confirmation_success
|
||||||
send_auth_headers(@confirmable)
|
send_auth_headers(@confirmable)
|
||||||
render partial: 'devise/auth.json', locals: { resource: @confirmable }
|
render partial: 'devise/auth', formats: [:json], locals: { resource: @confirmable }
|
||||||
end
|
end
|
||||||
|
|
||||||
def render_confirmation_error
|
def render_confirmation_error
|
||||||
|
|
|
@ -11,7 +11,7 @@ class DeviseOverrides::PasswordsController < Devise::PasswordsController
|
||||||
@recoverable = User.find_by(reset_password_token: reset_password_token)
|
@recoverable = User.find_by(reset_password_token: reset_password_token)
|
||||||
if @recoverable && reset_password_and_confirmation(@recoverable)
|
if @recoverable && reset_password_and_confirmation(@recoverable)
|
||||||
send_auth_headers(@recoverable)
|
send_auth_headers(@recoverable)
|
||||||
render partial: 'devise/auth.json', locals: { resource: @recoverable }
|
render partial: 'devise/auth', formats: [:json], locals: { resource: @recoverable }
|
||||||
else
|
else
|
||||||
render json: { message: 'Invalid token', redirect_url: '/' }, status: :unprocessable_entity
|
render json: { message: 'Invalid token', redirect_url: '/' }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
|
@ -16,14 +16,14 @@ class DeviseOverrides::SessionsController < ::DeviseTokenAuth::SessionsControlle
|
||||||
end
|
end
|
||||||
|
|
||||||
def render_create_success
|
def render_create_success
|
||||||
render partial: 'devise/auth.json', locals: { resource: @resource }
|
render partial: 'devise/auth', formats: [:json], locals: { resource: @resource }
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def authenticate_resource_with_sso_token
|
def authenticate_resource_with_sso_token
|
||||||
@token = @resource.create_token
|
@token = @resource.create_token
|
||||||
@resource.save
|
@resource.save!
|
||||||
|
|
||||||
sign_in(:user, @resource, store: false, bypass: false)
|
sign_in(:user, @resource, store: false, bypass: false)
|
||||||
# invalidate the token after the user is signed in
|
# invalidate the token after the user is signed in
|
||||||
|
|
|
@ -2,7 +2,7 @@ class DeviseOverrides::TokenValidationsController < ::DeviseTokenAuth::TokenVali
|
||||||
def validate_token
|
def validate_token
|
||||||
# @resource will have been set by set_user_by_token concern
|
# @resource will have been set by set_user_by_token concern
|
||||||
if @resource
|
if @resource
|
||||||
render 'devise/token.json'
|
render 'devise/token', formats: [:json]
|
||||||
else
|
else
|
||||||
render_validate_token_error
|
render_validate_token_error
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,18 +1,16 @@
|
||||||
class Platform::Api::V1::AccountsController < PlatformController
|
class Platform::Api::V1::AccountsController < PlatformController
|
||||||
def create
|
def create
|
||||||
@resource = Account.new(account_params)
|
@resource = Account.create!(account_params)
|
||||||
@resource.save!
|
update_resource_features
|
||||||
@platform_app.platform_app_permissibles.find_or_create_by(permissible: @resource)
|
@platform_app.platform_app_permissibles.find_or_create_by(permissible: @resource)
|
||||||
render json: @resource
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show; end
|
||||||
render json: @resource
|
|
||||||
end
|
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@resource.update!(account_params)
|
@resource.assign_attributes(account_params)
|
||||||
render json: @resource
|
update_resource_features
|
||||||
|
@resource.save!
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
|
@ -27,6 +25,18 @@ class Platform::Api::V1::AccountsController < PlatformController
|
||||||
end
|
end
|
||||||
|
|
||||||
def account_params
|
def account_params
|
||||||
params.permit(:name, :locale)
|
permitted_params.except(:features)
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_resource_features
|
||||||
|
return if permitted_params[:features].blank?
|
||||||
|
|
||||||
|
permitted_params[:features].each do |key, value|
|
||||||
|
value.present? ? @resource.enable_features(key) : @resource.disable_features(key)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def permitted_params
|
||||||
|
params.permit(:name, :locale, :domain, :support_email, :status, features: {}, limits: {}, custom_attributes: {})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -51,6 +51,6 @@ class Platform::Api::V1::UsersController < PlatformController
|
||||||
end
|
end
|
||||||
|
|
||||||
def user_params
|
def user_params
|
||||||
params.permit(:name, :email, :password, custom_attributes: {})
|
params.permit(:name, :display_name, :email, :password, custom_attributes: {})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,10 +4,10 @@ class Public::Api::V1::Inboxes::ContactsController < Public::Api::V1::InboxesCon
|
||||||
|
|
||||||
def create
|
def create
|
||||||
source_id = params[:source_id] || SecureRandom.uuid
|
source_id = params[:source_id] || SecureRandom.uuid
|
||||||
@contact_inbox = ::ContactBuilder.new(
|
@contact_inbox = ::ContactInboxWithContactBuilder.new(
|
||||||
source_id: source_id,
|
source_id: source_id,
|
||||||
inbox: @inbox_channel.inbox,
|
inbox: @inbox_channel.inbox,
|
||||||
contact_attributes: permitted_params.except(:identifier, :identifier_hash)
|
contact_attributes: permitted_params.except(:identifier_hash)
|
||||||
).perform
|
).perform
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -3,9 +3,15 @@ class Public::Api::V1::InboxesController < PublicController
|
||||||
before_action :set_contact_inbox
|
before_action :set_contact_inbox
|
||||||
before_action :set_conversation
|
before_action :set_conversation
|
||||||
|
|
||||||
|
def show
|
||||||
|
@inbox_channel = ::Channel::Api.find_by!(identifier: params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_inbox_channel
|
def set_inbox_channel
|
||||||
|
return if params[:inbox_id].blank?
|
||||||
|
|
||||||
@inbox_channel = ::Channel::Api.find_by!(identifier: params[:inbox_id])
|
@inbox_channel = ::Channel::Api.find_by!(identifier: params[:inbox_id])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
class Public::Api::V1::Portals::ArticlesController < PublicController
|
class Public::Api::V1::Portals::ArticlesController < PublicController
|
||||||
before_action :ensure_custom_domain_request, only: [:show, :index]
|
before_action :ensure_custom_domain_request, only: [:show, :index]
|
||||||
before_action :set_portal
|
before_action :portal
|
||||||
before_action :set_category
|
before_action :set_category, except: [:index]
|
||||||
before_action :set_article, only: [:show]
|
before_action :set_article, only: [:show]
|
||||||
|
layout 'portal'
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@articles = @portal.articles
|
@articles = @portal.articles
|
||||||
|
@ -15,17 +16,24 @@ class Public::Api::V1::Portals::ArticlesController < PublicController
|
||||||
|
|
||||||
def set_article
|
def set_article
|
||||||
@article = @category.articles.find(params[:id])
|
@article = @category.articles.find(params[:id])
|
||||||
|
@parsed_content = render_article_content(@article.content)
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_category
|
def set_category
|
||||||
@category = @portal.categories.find_by!(slug: params[:category_slug])
|
@category = @portal.categories.find_by!(slug: params[:category_slug]) if params[:category_slug].present?
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_portal
|
def portal
|
||||||
@portal = @portals.find_by!(slug: params[:slug], archived: false)
|
@portal ||= Portal.find_by!(slug: params[:slug], archived: false)
|
||||||
end
|
end
|
||||||
|
|
||||||
def list_params
|
def list_params
|
||||||
params.permit(:query)
|
params.permit(:query)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def render_article_content(content)
|
||||||
|
# rubocop:disable Rails/OutputSafety
|
||||||
|
CommonMarker.render_html(content).html_safe
|
||||||
|
# rubocop:enable Rails/OutputSafety
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
class Public::Api::V1::Portals::CategoriesController < PublicController
|
class Public::Api::V1::Portals::CategoriesController < PublicController
|
||||||
before_action :ensure_custom_domain_request, only: [:show, :index]
|
before_action :ensure_custom_domain_request, only: [:show, :index]
|
||||||
before_action :set_portal
|
before_action :portal
|
||||||
before_action :set_category, only: [:show]
|
before_action :set_category, only: [:show]
|
||||||
|
layout 'portal'
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@categories = @portal.categories
|
@categories = @portal.categories
|
||||||
|
@ -12,10 +13,10 @@ class Public::Api::V1::Portals::CategoriesController < PublicController
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_category
|
def set_category
|
||||||
@category = @portal.categories.find_by!(locale: params[:locale])
|
@category = @portal.categories.find_by!(locale: params[:locale], slug: params[:category_slug])
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_portal
|
def portal
|
||||||
@portal = @portals.find_by!(slug: params[:slug], archived: false)
|
@portal ||= Portal.find_by!(slug: params[:slug], archived: false)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,12 +1,21 @@
|
||||||
class Public::Api::V1::PortalsController < PublicController
|
class Public::Api::V1::PortalsController < PublicController
|
||||||
before_action :ensure_custom_domain_request, only: [:show]
|
before_action :ensure_custom_domain_request, only: [:show]
|
||||||
before_action :set_portal
|
before_action :portal
|
||||||
|
before_action :redirect_to_portal_with_locale, only: [:show]
|
||||||
|
layout 'portal'
|
||||||
|
|
||||||
def show; end
|
def show; end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_portal
|
def portal
|
||||||
@portal = @portals.find_by!(slug: params[:slug], archived: false)
|
@portal ||= Portal.find_by!(slug: params[:slug], archived: false)
|
||||||
|
@locale = params[:locale] || @portal.default_locale
|
||||||
|
end
|
||||||
|
|
||||||
|
def redirect_to_portal_with_locale
|
||||||
|
return if params[:locale].present?
|
||||||
|
|
||||||
|
redirect_to "/hc/#{@portal.slug}/#{@portal.default_locale}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,14 +7,15 @@ class PublicController < ActionController::Base
|
||||||
private
|
private
|
||||||
|
|
||||||
def ensure_custom_domain_request
|
def ensure_custom_domain_request
|
||||||
custom_domain = request.host
|
domain = request.host
|
||||||
|
|
||||||
@portals = ::Portal.where(custom_domain: custom_domain)
|
return if [URI.parse(ENV.fetch('FRONTEND_URL', '')).host, URI.parse(ENV.fetch('HELPCENTER_URL', '')).host].include?(domain)
|
||||||
|
|
||||||
return if @portals.present?
|
@portal = ::Portal.find_by(custom_domain: domain)
|
||||||
|
return if @portal.present?
|
||||||
|
|
||||||
render json: {
|
render json: {
|
||||||
error: "Domain: #{custom_domain} is not registered with us. \
|
error: "Domain: #{domain} is not registered with us. \
|
||||||
Please send us an email at support@chatwoot.com with the custom domain name and account API key"
|
Please send us an email at support@chatwoot.com with the custom domain name and account API key"
|
||||||
}, status: :unauthorized and return
|
}, status: :unauthorized and return
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,106 +0,0 @@
|
||||||
# This controller expects you to use the URLs /saml/init and /saml/consume in your OneLogin application.
|
|
||||||
class SamlController < ApplicationController
|
|
||||||
# skip_before_action :verify_authenticity_token, :only => [:consume]
|
|
||||||
layout 'vueapp'
|
|
||||||
include SsoAuthenticatable
|
|
||||||
|
|
||||||
def index
|
|
||||||
request = OneLogin::RubySaml::Authrequest.new
|
|
||||||
redirect_to(request.create(saml_settings))
|
|
||||||
end
|
|
||||||
|
|
||||||
def consume
|
|
||||||
response = OneLogin::RubySaml::Response.new(params['SAMLResponse'])
|
|
||||||
response.settings = saml_settings
|
|
||||||
# We validate the SAML Response and check if the user already exists in the system
|
|
||||||
if response.is_valid?
|
|
||||||
# authorize_success, log the user
|
|
||||||
find_the_resource(response.nameid)
|
|
||||||
create_session_and_assign_token
|
|
||||||
|
|
||||||
encoded_email = ERB::Util.url_encode(@resource.email)
|
|
||||||
redirect_to "https://db7b-103-51-75-84.in.ngrok.io/app/login?email=#{encoded_email}&sso_auth_token=#{@resource.generate_sso_auth_token}"
|
|
||||||
else
|
|
||||||
Rails.logger.error "Response Invalid. Errors: #{response.errors}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def metadata
|
|
||||||
settings = saml_settings
|
|
||||||
meta = OneLogin::RubySaml::Metadata.new
|
|
||||||
render xml: meta.generate(settings, true)
|
|
||||||
end
|
|
||||||
|
|
||||||
def logout
|
|
||||||
# If we're given a logout request, handle it in the IdP logout initiated method
|
|
||||||
idp_logout_request
|
|
||||||
end
|
|
||||||
|
|
||||||
# Method to handle IdP initiated logouts
|
|
||||||
def idp_logout_request
|
|
||||||
settings = saml_settings
|
|
||||||
logout_request = OneLogin::RubySaml::SloLogoutrequest.new(params[:SAMLRequest], settings: settings)
|
|
||||||
unless logout_request.is_valid?
|
|
||||||
error_msg = "IdP initiated LogoutRequest was not valid!. Errors: #{logout_request.errors}"
|
|
||||||
Rails.logger.error error_msg
|
|
||||||
render inline: error_msg
|
|
||||||
end
|
|
||||||
Rails.logger.info "IdP initiated Logout for #{logout_request.nameid}"
|
|
||||||
|
|
||||||
# Actually log out this session
|
|
||||||
reset_session
|
|
||||||
|
|
||||||
logout_response = OneLogin::RubySaml::SloLogoutresponse.new.create(settings, logout_request.id, nil, RelayState: params[:RelayState])
|
|
||||||
redirect_to logout_response
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def find_the_resource(email)
|
|
||||||
@resource = User.find_by(email: email)
|
|
||||||
end
|
|
||||||
|
|
||||||
def create_session_and_assign_token
|
|
||||||
create_and_assign_token
|
|
||||||
sign_in(:user, @resource, store: true, bypass: false)
|
|
||||||
end
|
|
||||||
|
|
||||||
def create_and_assign_token
|
|
||||||
if @resource.respond_to?(:with_lock)
|
|
||||||
@resource.with_lock do
|
|
||||||
@token = @resource.create_token
|
|
||||||
@resource.save!
|
|
||||||
end
|
|
||||||
else
|
|
||||||
@token = @resource.create_token
|
|
||||||
@resource.save!
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def render_create_success
|
|
||||||
render partial: 'devise/auth.json', locals: { resource: @resource }
|
|
||||||
end
|
|
||||||
|
|
||||||
def saml_settings
|
|
||||||
settings = OneLogin::RubySaml::Settings.new
|
|
||||||
|
|
||||||
settings.soft = true
|
|
||||||
|
|
||||||
settings.assertion_consumer_service_url = 'https://db7b-103-51-75-84.in.ngrok.io/saml/consume'
|
|
||||||
settings.sp_entity_id = 'https://db7b-103-51-75-84.in.ngrok.io/saml/metadata'
|
|
||||||
|
|
||||||
settings.idp_entity_id = 'https://app.onelogin.com/saml2'
|
|
||||||
settings.idp_sso_target_url = 'https://chatwoot-dev.onelogin.com/trust/saml2/http-post/sso/de789d10-0617-44e9-8fd6-9d798809cfbf'
|
|
||||||
settings.idp_slo_target_url = 'https://chatwoot-dev.onelogin.com/trust/saml2/http-redirect/slo/1861655'
|
|
||||||
|
|
||||||
settings.name_identifier_format = 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'
|
|
||||||
settings.idp_cert_fingerprint = 'FD:17:5E:81:F8:F5:88:EF:21:AB:94:44:3E:4A:C4:72:94:E2:63:AE'
|
|
||||||
settings.idp_cert_fingerprint_algorithm = 'http://www.w3.org/2000/09/xmldsig#sha1'
|
|
||||||
|
|
||||||
# Optional bindings (defaults to Redirect for logout POST for ACS)
|
|
||||||
settings.single_logout_service_binding = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect' # or :post, :redirect
|
|
||||||
settings.assertion_consumer_service_binding = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST' # or :post, :redirect
|
|
||||||
|
|
||||||
settings
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -36,6 +36,7 @@ class SuperAdmin::AccountsController < SuperAdmin::ApplicationController
|
||||||
def resource_params
|
def resource_params
|
||||||
permitted_params = super
|
permitted_params = super
|
||||||
permitted_params[:limits] = permitted_params[:limits].to_h.compact
|
permitted_params[:limits] = permitted_params[:limits].to_h.compact
|
||||||
|
permitted_params[:selected_feature_flags] = params[:enabled_features].keys.map(&:to_sym) if params[:enabled_features].present?
|
||||||
permitted_params
|
permitted_params
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -44,12 +44,12 @@ class Twitter::CallbacksController < Twitter::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_inbox
|
def create_inbox
|
||||||
twitter_profile = account.twitter_profiles.create(
|
twitter_profile = account.twitter_profiles.create!(
|
||||||
twitter_access_token: parsed_body['oauth_token'],
|
twitter_access_token: parsed_body['oauth_token'],
|
||||||
twitter_access_token_secret: parsed_body['oauth_token_secret'],
|
twitter_access_token_secret: parsed_body['oauth_token_secret'],
|
||||||
profile_id: parsed_body['user_id']
|
profile_id: parsed_body['user_id']
|
||||||
)
|
)
|
||||||
account.inboxes.create(
|
account.inboxes.create!(
|
||||||
name: parsed_body['screen_name'],
|
name: parsed_body['screen_name'],
|
||||||
channel: twitter_profile
|
channel: twitter_profile
|
||||||
)
|
)
|
||||||
|
|
|
@ -8,7 +8,15 @@ class AccountDashboard < Administrate::BaseDashboard
|
||||||
# which determines how the attribute is displayed
|
# which determines how the attribute is displayed
|
||||||
# on pages throughout the dashboard.
|
# on pages throughout the dashboard.
|
||||||
|
|
||||||
enterprise_attribute_types = ChatwootApp.enterprise? ? { limits: Enterprise::AccountLimitsField } : {}
|
enterprise_attribute_types = if ChatwootApp.enterprise?
|
||||||
|
{
|
||||||
|
limits: Enterprise::AccountLimitsField,
|
||||||
|
all_features: Enterprise::AccountFeaturesField
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{}
|
||||||
|
end
|
||||||
|
|
||||||
ATTRIBUTE_TYPES = {
|
ATTRIBUTE_TYPES = {
|
||||||
id: Field::Number,
|
id: Field::Number,
|
||||||
name: Field::String,
|
name: Field::String,
|
||||||
|
@ -37,7 +45,7 @@ class AccountDashboard < Administrate::BaseDashboard
|
||||||
|
|
||||||
# SHOW_PAGE_ATTRIBUTES
|
# SHOW_PAGE_ATTRIBUTES
|
||||||
# an array of attributes that will be displayed on the model's show page.
|
# an array of attributes that will be displayed on the model's show page.
|
||||||
enterprise_show_page_attributes = ChatwootApp.enterprise? ? %i[limits] : []
|
enterprise_show_page_attributes = ChatwootApp.enterprise? ? %i[limits all_features] : []
|
||||||
SHOW_PAGE_ATTRIBUTES = (%i[
|
SHOW_PAGE_ATTRIBUTES = (%i[
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
|
@ -52,7 +60,7 @@ class AccountDashboard < Administrate::BaseDashboard
|
||||||
# FORM_ATTRIBUTES
|
# FORM_ATTRIBUTES
|
||||||
# an array of attributes that will be displayed
|
# an array of attributes that will be displayed
|
||||||
# on the model's form (`new` and `edit`) pages.
|
# on the model's form (`new` and `edit`) pages.
|
||||||
enterprise_form_attributes = ChatwootApp.enterprise? ? %i[limits] : []
|
enterprise_form_attributes = ChatwootApp.enterprise? ? %i[limits all_features] : []
|
||||||
FORM_ATTRIBUTES = (%i[
|
FORM_ATTRIBUTES = (%i[
|
||||||
name
|
name
|
||||||
locale
|
locale
|
||||||
|
|
7
app/fields/enterprise/account_features_field.rb
Normal file
7
app/fields/enterprise/account_features_field.rb
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
require 'administrate/field/base'
|
||||||
|
|
||||||
|
class Enterprise::AccountFeaturesField < Administrate::Field::Base
|
||||||
|
def to_s
|
||||||
|
data
|
||||||
|
end
|
||||||
|
end
|
|
@ -56,7 +56,6 @@ class ConversationFinder
|
||||||
filter_by_team if @team
|
filter_by_team if @team
|
||||||
filter_by_labels if params[:labels]
|
filter_by_labels if params[:labels]
|
||||||
filter_by_query if params[:q]
|
filter_by_query if params[:q]
|
||||||
filter_by_reply_status
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_inboxes
|
def set_inboxes
|
||||||
|
@ -76,12 +75,9 @@ class ConversationFinder
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_all_conversations
|
def find_all_conversations
|
||||||
if params[:conversation_type] == 'mention'
|
@conversations = current_account.conversations.where(inbox_id: @inbox_ids)
|
||||||
conversation_ids = current_account.mentions.where(user: current_user).pluck(:conversation_id)
|
filter_by_conversation_type if params[:conversation_type]
|
||||||
@conversations = current_account.conversations.where(id: conversation_ids)
|
@conversations
|
||||||
else
|
|
||||||
@conversations = current_account.conversations.where(inbox_id: @inbox_ids)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def filter_by_assignee_type
|
def filter_by_assignee_type
|
||||||
|
@ -96,8 +92,15 @@ class ConversationFinder
|
||||||
@conversations
|
@conversations
|
||||||
end
|
end
|
||||||
|
|
||||||
def filter_by_reply_status
|
def filter_by_conversation_type
|
||||||
@conversations = @conversations.where(first_reply_created_at: nil) if params[:reply_status] == 'unattended'
|
case @params[:conversation_type]
|
||||||
|
when 'mention'
|
||||||
|
conversation_ids = current_account.mentions.where(user: current_user).pluck(:conversation_id)
|
||||||
|
@conversations = @conversations.where(id: conversation_ids)
|
||||||
|
when 'unattended'
|
||||||
|
@conversations = @conversations.where(first_reply_created_at: nil)
|
||||||
|
end
|
||||||
|
@conversations
|
||||||
end
|
end
|
||||||
|
|
||||||
def filter_by_query
|
def filter_by_query
|
||||||
|
|
|
@ -21,7 +21,9 @@ class MessageFinder
|
||||||
end
|
end
|
||||||
|
|
||||||
def current_messages
|
def current_messages
|
||||||
if @params[:before].present?
|
if @params[:after].present?
|
||||||
|
messages.reorder('created_at asc').where('id >= ?', @params[:before].to_i).limit(20)
|
||||||
|
elsif @params[:before].present?
|
||||||
messages.reorder('created_at desc').where('id < ?', @params[:before].to_i).limit(20).reverse
|
messages.reorder('created_at desc').where('id < ?', @params[:before].to_i).limit(20).reverse
|
||||||
else
|
else
|
||||||
messages.reorder('created_at desc').limit(20).reverse
|
messages.reorder('created_at desc').limit(20).reverse
|
||||||
|
|
|
@ -8,6 +8,12 @@ module FileTypeHelper
|
||||||
:file
|
:file
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Used in case of DIRECT_UPLOADS_ENABLED=true
|
||||||
|
def file_type_by_signed_id(signed_id)
|
||||||
|
blob = ActiveStorage::Blob.find_signed(signed_id)
|
||||||
|
file_type(blob&.content_type)
|
||||||
|
end
|
||||||
|
|
||||||
def image_file?(content_type)
|
def image_file?(content_type)
|
||||||
[
|
[
|
||||||
'image/jpeg',
|
'image/jpeg',
|
||||||
|
|
|
@ -87,6 +87,9 @@ export default {
|
||||||
},
|
},
|
||||||
async initializeAccount() {
|
async initializeAccount() {
|
||||||
await this.$store.dispatch('accounts/get');
|
await this.$store.dispatch('accounts/get');
|
||||||
|
this.$store.dispatch('setActiveAccount', {
|
||||||
|
accountId: this.currentAccountId,
|
||||||
|
});
|
||||||
const {
|
const {
|
||||||
locale,
|
locale,
|
||||||
latest_chatwoot_version: latestChatwootVersion,
|
latest_chatwoot_version: latestChatwootVersion,
|
||||||
|
|
9
app/javascript/dashboard/api/agentBots.js
Normal file
9
app/javascript/dashboard/api/agentBots.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import ApiClient from './ApiClient';
|
||||||
|
|
||||||
|
class AgentBotsAPI extends ApiClient {
|
||||||
|
constructor() {
|
||||||
|
super('agent_bots', { accountScoped: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new AgentBotsAPI();
|
|
@ -144,7 +144,22 @@ export default {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
updateAutoOffline(accountId, autoOffline = false) {
|
||||||
|
return axios.post(endPoints('autoOffline').url, {
|
||||||
|
profile: { account_id: accountId, auto_offline: autoOffline },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
deleteAvatar() {
|
deleteAvatar() {
|
||||||
return axios.delete(endPoints('deleteAvatar').url);
|
return axios.delete(endPoints('deleteAvatar').url);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setActiveAccount({ accountId }) {
|
||||||
|
const urlData = endPoints('setActiveAccount');
|
||||||
|
return axios.put(urlData.url, {
|
||||||
|
profile: {
|
||||||
|
account_id: accountId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -16,6 +16,9 @@ const endPoints = {
|
||||||
availabilityUpdate: {
|
availabilityUpdate: {
|
||||||
url: '/api/v1/profile/availability',
|
url: '/api/v1/profile/availability',
|
||||||
},
|
},
|
||||||
|
autoOffline: {
|
||||||
|
url: '/api/v1/profile/auto_offline',
|
||||||
|
},
|
||||||
logout: {
|
logout: {
|
||||||
url: 'auth/sign_out',
|
url: 'auth/sign_out',
|
||||||
},
|
},
|
||||||
|
@ -40,6 +43,10 @@ const endPoints = {
|
||||||
deleteAvatar: {
|
deleteAvatar: {
|
||||||
url: '/api/v1/profile/avatar',
|
url: '/api/v1/profile/avatar',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setActiveAccount: {
|
||||||
|
url: '/api/v1/profile/set_active_account',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default page => {
|
export default page => {
|
||||||
|
|
|
@ -7,8 +7,8 @@ class CategoriesAPI extends PortalsAPI {
|
||||||
super('categories', { accountScoped: true });
|
super('categories', { accountScoped: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
get({ portalSlug }) {
|
get({ portalSlug, locale }) {
|
||||||
return axios.get(`${this.url}/${portalSlug}/categories`);
|
return axios.get(`${this.url}/${portalSlug}/categories?locale=${locale}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
create({ portalSlug, categoryObj }) {
|
create({ portalSlug, categoryObj }) {
|
||||||
|
|
|
@ -6,6 +6,10 @@ class PortalsAPI extends ApiClient {
|
||||||
super('portals', { accountScoped: true });
|
super('portals', { accountScoped: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getPortal({ portalSlug, locale }) {
|
||||||
|
return axios.get(`${this.url}/${portalSlug}?locale=${locale}`);
|
||||||
|
}
|
||||||
|
|
||||||
updatePortal({ portalSlug, portalObj }) {
|
updatePortal({ portalSlug, portalObj }) {
|
||||||
return axios.patch(`${this.url}/${portalSlug}`, portalObj);
|
return axios.patch(`${this.url}/${portalSlug}`, portalObj);
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,6 +68,10 @@ class ConversationApi extends ApiClient {
|
||||||
return axios.post(`${this.url}/${id}/update_last_seen`);
|
return axios.post(`${this.url}/${id}/update_last_seen`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
markMessagesUnread({ id }) {
|
||||||
|
return axios.post(`${this.url}/${id}/unread`);
|
||||||
|
}
|
||||||
|
|
||||||
toggleTyping({ conversationId, status, isPrivate }) {
|
toggleTyping({ conversationId, status, isPrivate }) {
|
||||||
return axios.post(`${this.url}/${conversationId}/toggle_typing_status`, {
|
return axios.post(`${this.url}/${conversationId}/toggle_typing_status`, {
|
||||||
typing_status: status,
|
typing_status: status,
|
||||||
|
@ -105,6 +109,16 @@ class ConversationApi extends ApiClient {
|
||||||
custom_attributes: customAttributes,
|
custom_attributes: customAttributes,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fetchParticipants(conversationId) {
|
||||||
|
return axios.get(`${this.url}/${conversationId}/participants`);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateParticipants({ conversationId, userIds }) {
|
||||||
|
return axios.patch(`${this.url}/${conversationId}/participants`, {
|
||||||
|
user_ids: userIds,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new ConversationApi();
|
export default new ConversationApi();
|
||||||
|
|
|
@ -13,6 +13,16 @@ class Inboxes extends ApiClient {
|
||||||
deleteInboxAvatar(inboxId) {
|
deleteInboxAvatar(inboxId) {
|
||||||
return axios.delete(`${this.url}/${inboxId}/avatar`);
|
return axios.delete(`${this.url}/${inboxId}/avatar`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getAgentBot(inboxId) {
|
||||||
|
return axios.get(`${this.url}/${inboxId}/agent_bot`);
|
||||||
|
}
|
||||||
|
|
||||||
|
setAgentBot(inboxId, botId) {
|
||||||
|
return axios.post(`${this.url}/${inboxId}/set_agent_bot`, {
|
||||||
|
agent_bot: botId,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new Inboxes();
|
export default new Inboxes();
|
||||||
|
|
16
app/javascript/dashboard/api/macros.js
Normal file
16
app/javascript/dashboard/api/macros.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
/* global axios */
|
||||||
|
import ApiClient from './ApiClient';
|
||||||
|
|
||||||
|
class MacrosAPI extends ApiClient {
|
||||||
|
constructor() {
|
||||||
|
super('macros', { accountScoped: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
executeMacro({ macroId, conversationIds }) {
|
||||||
|
return axios.post(`${this.url}/${macroId}/execute`, {
|
||||||
|
conversation_ids: conversationIds,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new MacrosAPI();
|
13
app/javascript/dashboard/api/specs/agentBots.spec.js
Normal file
13
app/javascript/dashboard/api/specs/agentBots.spec.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import AgentBotsAPI from '../agentBots';
|
||||||
|
import ApiClient from '../ApiClient';
|
||||||
|
|
||||||
|
describe('#AgentBotsAPI', () => {
|
||||||
|
it('creates correct instance', () => {
|
||||||
|
expect(AgentBotsAPI).toBeInstanceOf(ApiClient);
|
||||||
|
expect(AgentBotsAPI).toHaveProperty('get');
|
||||||
|
expect(AgentBotsAPI).toHaveProperty('show');
|
||||||
|
expect(AgentBotsAPI).toHaveProperty('create');
|
||||||
|
expect(AgentBotsAPI).toHaveProperty('update');
|
||||||
|
expect(AgentBotsAPI).toHaveProperty('delete');
|
||||||
|
});
|
||||||
|
});
|
|
@ -11,6 +11,8 @@ describe('#InboxesAPI', () => {
|
||||||
expect(inboxesAPI).toHaveProperty('update');
|
expect(inboxesAPI).toHaveProperty('update');
|
||||||
expect(inboxesAPI).toHaveProperty('delete');
|
expect(inboxesAPI).toHaveProperty('delete');
|
||||||
expect(inboxesAPI).toHaveProperty('getCampaigns');
|
expect(inboxesAPI).toHaveProperty('getCampaigns');
|
||||||
|
expect(inboxesAPI).toHaveProperty('getAgentBot');
|
||||||
|
expect(inboxesAPI).toHaveProperty('setAgentBot');
|
||||||
});
|
});
|
||||||
describeWithAPIMock('API calls', context => {
|
describeWithAPIMock('API calls', context => {
|
||||||
it('#getCampaigns', () => {
|
it('#getCampaigns', () => {
|
||||||
|
|
14
app/javascript/dashboard/api/specs/macros.spec.js
Normal file
14
app/javascript/dashboard/api/specs/macros.spec.js
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import macros from '../macros';
|
||||||
|
import ApiClient from '../ApiClient';
|
||||||
|
|
||||||
|
describe('#macrosAPI', () => {
|
||||||
|
it('creates correct instance', () => {
|
||||||
|
expect(macros).toBeInstanceOf(ApiClient);
|
||||||
|
expect(macros).toHaveProperty('get');
|
||||||
|
expect(macros).toHaveProperty('create');
|
||||||
|
expect(macros).toHaveProperty('update');
|
||||||
|
expect(macros).toHaveProperty('delete');
|
||||||
|
expect(macros).toHaveProperty('show');
|
||||||
|
expect(macros.url).toBe('/api/v1/macros');
|
||||||
|
});
|
||||||
|
});
|
6
app/javascript/dashboard/api/testimonials.js
Normal file
6
app/javascript/dashboard/api/testimonials.js
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
/* global axios */
|
||||||
|
import wootConstants from 'dashboard/constants';
|
||||||
|
|
||||||
|
export const getTestimonialContent = () => {
|
||||||
|
return axios.get(wootConstants.TESTIMONIAL_URL);
|
||||||
|
};
|
|
@ -74,8 +74,8 @@ Tahoma,
|
||||||
Arial,
|
Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
$body-antialiased: true;
|
$body-antialiased: true;
|
||||||
$global-margin: $space-one;
|
$global-margin: $space-small;
|
||||||
$global-padding: $space-one;
|
$global-padding: $space-micro;
|
||||||
$global-weight-normal: normal;
|
$global-weight-normal: normal;
|
||||||
$global-weight-bold: bold;
|
$global-weight-bold: bold;
|
||||||
$global-radius: 0;
|
$global-radius: 0;
|
||||||
|
|
|
@ -20,6 +20,24 @@
|
||||||
|
|
||||||
@include foundation-everything($flex: true);
|
@include foundation-everything($flex: true);
|
||||||
|
|
||||||
|
@include foundation-prototype-text-utilities;
|
||||||
|
@include foundation-prototype-text-transformation;
|
||||||
|
@include foundation-prototype-text-decoration;
|
||||||
|
@include foundation-prototype-font-styling;
|
||||||
|
@include foundation-prototype-list-style-type;
|
||||||
|
@include foundation-prototype-rounded;
|
||||||
|
@include foundation-prototype-bordered;
|
||||||
|
@include foundation-prototype-shadow;
|
||||||
|
@include foundation-prototype-separator;
|
||||||
|
@include foundation-prototype-overflow;
|
||||||
|
@include foundation-prototype-display;
|
||||||
|
@include foundation-prototype-position;
|
||||||
|
@include foundation-prototype-border-box;
|
||||||
|
@include foundation-prototype-border-none;
|
||||||
|
@include foundation-prototype-sizing;
|
||||||
|
@include foundation-prototype-spacing;
|
||||||
|
|
||||||
|
|
||||||
@import 'typography';
|
@import 'typography';
|
||||||
@import 'layout';
|
@import 'layout';
|
||||||
@import 'animations';
|
@import 'animations';
|
||||||
|
|
|
@ -113,9 +113,22 @@ $default-button-height: 4.0rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.clear {
|
&.clear {
|
||||||
|
color: var(--w-700);
|
||||||
|
|
||||||
|
&.secondary {
|
||||||
|
color: var(--s-700)
|
||||||
|
}
|
||||||
|
|
||||||
|
&.success {
|
||||||
|
color: var(--g-700)
|
||||||
|
}
|
||||||
|
|
||||||
|
&.alert {
|
||||||
|
color: var(--r-700)
|
||||||
|
}
|
||||||
|
|
||||||
&.warning {
|
&.warning {
|
||||||
color: var(--y-600);
|
color: var(--y-700)
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
@ -142,10 +155,20 @@ $default-button-height: 4.0rem;
|
||||||
// Sizes
|
// Sizes
|
||||||
&.tiny {
|
&.tiny {
|
||||||
height: var(--space-medium);
|
height: var(--space-medium);
|
||||||
|
|
||||||
|
.icon+.button__content {
|
||||||
|
padding-left: var(--space-micro);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.small {
|
&.small {
|
||||||
height: var(--space-large);
|
height: var(--space-large);
|
||||||
|
padding-bottom: var(--space-smaller);
|
||||||
|
padding-top: var(--space-smaller);
|
||||||
|
|
||||||
|
.icon+.button__content {
|
||||||
|
padding-left: var(--space-smaller);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.large {
|
&.large {
|
||||||
|
@ -175,6 +198,10 @@ $default-button-height: 4.0rem;
|
||||||
height: auto;
|
height: auto;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,15 +14,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal--close {
|
.modal--close {
|
||||||
border-radius: 50%;
|
|
||||||
color: $color-heading;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: $font-size-big;
|
|
||||||
line-height: $space-normal;
|
|
||||||
padding: $space-normal;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: $space-micro;
|
right: $space-small;
|
||||||
top: $space-micro;
|
top: $space-small;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: $color-background;
|
background: $color-background;
|
||||||
|
|
|
@ -59,12 +59,8 @@
|
||||||
|
|
||||||
.hamburger--menu {
|
.hamburger--menu {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: none;
|
display: block;
|
||||||
margin-right: $space-normal;
|
margin-right: $space-normal;
|
||||||
|
|
||||||
@media screen and (max-width: 1200px) {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.header--icon {
|
.header--icon {
|
||||||
|
|
|
@ -102,6 +102,7 @@
|
||||||
@assign-agent="onAssignAgent"
|
@assign-agent="onAssignAgent"
|
||||||
@update-conversations="onUpdateConversations"
|
@update-conversations="onUpdateConversations"
|
||||||
@assign-labels="onAssignLabels"
|
@assign-labels="onAssignLabels"
|
||||||
|
@assign-team="onAssignTeamsForBulk"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
ref="activeConversation"
|
ref="activeConversation"
|
||||||
|
@ -125,6 +126,7 @@
|
||||||
@assign-label="onAssignLabels"
|
@assign-label="onAssignLabels"
|
||||||
@update-conversation-status="toggleConversationStatus"
|
@update-conversation-status="toggleConversationStatus"
|
||||||
@context-menu-toggle="onContextMenuToggle"
|
@context-menu-toggle="onContextMenuToggle"
|
||||||
|
@mark-as-unread="markAsUnread"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div v-if="chatListLoading" class="text-center">
|
<div v-if="chatListLoading" class="text-center">
|
||||||
|
@ -184,6 +186,11 @@ import {
|
||||||
hasPressedAltAndJKey,
|
hasPressedAltAndJKey,
|
||||||
hasPressedAltAndKKey,
|
hasPressedAltAndKKey,
|
||||||
} from 'shared/helpers/KeyboardHelpers';
|
} from 'shared/helpers/KeyboardHelpers';
|
||||||
|
import { conversationListPageURL } from '../helper/URLHelper';
|
||||||
|
import {
|
||||||
|
isOnMentionsView,
|
||||||
|
isOnUnattendedView,
|
||||||
|
} from '../store/modules/conversations/helpers/actionHelpers';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
@ -332,14 +339,15 @@ export default {
|
||||||
status: this.activeStatus,
|
status: this.activeStatus,
|
||||||
page: this.currentPage + 1,
|
page: this.currentPage + 1,
|
||||||
labels: this.label ? [this.label] : undefined,
|
labels: this.label ? [this.label] : undefined,
|
||||||
teamId: this.teamId ? this.teamId : undefined,
|
teamId: this.teamId || undefined,
|
||||||
conversationType: this.conversationType
|
conversationType: this.conversationType || undefined,
|
||||||
? this.conversationType
|
|
||||||
: undefined,
|
|
||||||
folders: this.hasActiveFolders ? this.savedFoldersValue : undefined,
|
folders: this.hasActiveFolders ? this.savedFoldersValue : undefined,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
pageTitle() {
|
pageTitle() {
|
||||||
|
if (this.hasAppliedFilters) {
|
||||||
|
return this.$t('CHAT_LIST.TAB_HEADING');
|
||||||
|
}
|
||||||
if (this.inbox.name) {
|
if (this.inbox.name) {
|
||||||
return this.inbox.name;
|
return this.inbox.name;
|
||||||
}
|
}
|
||||||
|
@ -352,6 +360,9 @@ export default {
|
||||||
if (this.conversationType === 'mention') {
|
if (this.conversationType === 'mention') {
|
||||||
return this.$t('CHAT_LIST.MENTION_HEADING');
|
return this.$t('CHAT_LIST.MENTION_HEADING');
|
||||||
}
|
}
|
||||||
|
if (this.conversationType === 'unattended') {
|
||||||
|
return this.$t('CHAT_LIST.UNATTENDED_HEADING');
|
||||||
|
}
|
||||||
if (this.hasActiveFolders) {
|
if (this.hasActiveFolders) {
|
||||||
return this.activeFolder.name;
|
return this.activeFolder.name;
|
||||||
}
|
}
|
||||||
|
@ -431,9 +442,6 @@ export default {
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onApplyFilter(payload) {
|
onApplyFilter(payload) {
|
||||||
if (this.$route.name !== 'home') {
|
|
||||||
this.$router.push({ name: 'home' });
|
|
||||||
}
|
|
||||||
this.resetBulkActions();
|
this.resetBulkActions();
|
||||||
this.foldersQuery = filterQueryGenerator(payload);
|
this.foldersQuery = filterQueryGenerator(payload);
|
||||||
this.$store.dispatch('conversationPage/reset');
|
this.$store.dispatch('conversationPage/reset');
|
||||||
|
@ -636,6 +644,35 @@ export default {
|
||||||
this.showAlert(this.$t('BULK_ACTION.ASSIGN_FAILED'));
|
this.showAlert(this.$t('BULK_ACTION.ASSIGN_FAILED'));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async markAsUnread(conversationId) {
|
||||||
|
try {
|
||||||
|
await this.$store.dispatch('markMessagesUnread', {
|
||||||
|
id: conversationId,
|
||||||
|
});
|
||||||
|
const {
|
||||||
|
params: { accountId, inbox_id: inboxId, label, teamId },
|
||||||
|
name,
|
||||||
|
} = this.$route;
|
||||||
|
let conversationType = '';
|
||||||
|
if (isOnMentionsView({ route: { name } })) {
|
||||||
|
conversationType = 'mention';
|
||||||
|
} else if (isOnUnattendedView({ route: { name } })) {
|
||||||
|
conversationType = 'unattended';
|
||||||
|
}
|
||||||
|
this.$router.push(
|
||||||
|
conversationListPageURL({
|
||||||
|
accountId,
|
||||||
|
conversationType: conversationType,
|
||||||
|
customViewId: this.foldersId,
|
||||||
|
inboxId,
|
||||||
|
label,
|
||||||
|
teamId,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
// Ignore error
|
||||||
|
}
|
||||||
|
},
|
||||||
async onAssignTeam(team, conversationId = null) {
|
async onAssignTeam(team, conversationId = null) {
|
||||||
try {
|
try {
|
||||||
await this.$store.dispatch('assignTeam', {
|
await this.$store.dispatch('assignTeam', {
|
||||||
|
@ -685,6 +722,21 @@ export default {
|
||||||
this.showAlert(this.$t('BULK_ACTION.LABELS.ASSIGN_FAILED'));
|
this.showAlert(this.$t('BULK_ACTION.LABELS.ASSIGN_FAILED'));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async onAssignTeamsForBulk(team) {
|
||||||
|
try {
|
||||||
|
await this.$store.dispatch('bulkActions/process', {
|
||||||
|
type: 'Conversation',
|
||||||
|
ids: this.selectedConversations,
|
||||||
|
fields: {
|
||||||
|
team_id: team.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.selectedConversations = [];
|
||||||
|
this.showAlert(this.$t('BULK_ACTION.TEAMS.ASSIGN_SUCCESFUL'));
|
||||||
|
} catch (err) {
|
||||||
|
this.showAlert(this.$t('BULK_ACTION.TEAMS.ASSIGN_FAILED'));
|
||||||
|
}
|
||||||
|
},
|
||||||
async onUpdateConversations(status) {
|
async onUpdateConversations(status) {
|
||||||
try {
|
try {
|
||||||
await this.$store.dispatch('bulkActions/process', {
|
await this.$store.dispatch('bulkActions/process', {
|
||||||
|
|
|
@ -7,9 +7,13 @@
|
||||||
@click="onBackDropClick"
|
@click="onBackDropClick"
|
||||||
>
|
>
|
||||||
<div :class="modalContainerClassName" @click.stop>
|
<div :class="modalContainerClassName" @click.stop>
|
||||||
<button class="modal--close" @click="close">
|
<woot-button
|
||||||
<fluent-icon icon="dismiss" />
|
color-scheme="secondary"
|
||||||
</button>
|
icon="dismiss"
|
||||||
|
variant="clear"
|
||||||
|
class="modal--close"
|
||||||
|
@click="close"
|
||||||
|
/>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
<template>
|
<template>
|
||||||
<button @click="onMenuItemClick">
|
<woot-button
|
||||||
<fluent-icon class="hamburger--menu" icon="list" />
|
size="small"
|
||||||
</button>
|
variant="clear"
|
||||||
|
color-scheme="secondary"
|
||||||
|
icon="list"
|
||||||
|
class="toggle-sidebar"
|
||||||
|
@click="onMenuItemClick"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
@ -16,13 +21,8 @@ export default {
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.hamburger--menu {
|
.toggle-sidebar {
|
||||||
cursor: pointer;
|
margin-right: var(--space-small);
|
||||||
display: none;
|
margin-left: var(--space-minus-small);
|
||||||
margin-right: var(--space-normal);
|
|
||||||
|
|
||||||
@media screen and (max-width: 1200px) {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -5,9 +5,12 @@ import Button from './ui/WootButton';
|
||||||
import Code from './Code';
|
import Code from './Code';
|
||||||
import ColorPicker from './widgets/ColorPicker';
|
import ColorPicker from './widgets/ColorPicker';
|
||||||
import ConfirmDeleteModal from './widgets/modal/ConfirmDeleteModal.vue';
|
import ConfirmDeleteModal from './widgets/modal/ConfirmDeleteModal.vue';
|
||||||
|
import ConfirmModal from './widgets/modal/ConfirmationModal.vue';
|
||||||
|
import ContextMenu from './ui/ContextMenu.vue';
|
||||||
import DeleteModal from './widgets/modal/DeleteModal.vue';
|
import DeleteModal from './widgets/modal/DeleteModal.vue';
|
||||||
import DropdownItem from 'shared/components/ui/dropdown/DropdownItem';
|
import DropdownItem from 'shared/components/ui/dropdown/DropdownItem';
|
||||||
import DropdownMenu from 'shared/components/ui/dropdown/DropdownMenu';
|
import DropdownMenu from 'shared/components/ui/dropdown/DropdownMenu';
|
||||||
|
import FeatureToggle from './widgets/FeatureToggle';
|
||||||
import HorizontalBar from './widgets/chart/HorizontalBarChart';
|
import HorizontalBar from './widgets/chart/HorizontalBarChart';
|
||||||
import Input from './widgets/forms/Input.vue';
|
import Input from './widgets/forms/Input.vue';
|
||||||
import Label from './ui/Label';
|
import Label from './ui/Label';
|
||||||
|
@ -21,8 +24,6 @@ import SubmitButton from './buttons/FormSubmitButton';
|
||||||
import Tabs from './ui/Tabs/Tabs';
|
import Tabs from './ui/Tabs/Tabs';
|
||||||
import TabsItem from './ui/Tabs/TabsItem';
|
import TabsItem from './ui/Tabs/TabsItem';
|
||||||
import Thumbnail from './widgets/Thumbnail.vue';
|
import Thumbnail from './widgets/Thumbnail.vue';
|
||||||
import ConfirmModal from './widgets/modal/ConfirmationModal.vue';
|
|
||||||
import ContextMenu from './ui/ContextMenu.vue';
|
|
||||||
|
|
||||||
const WootUIKit = {
|
const WootUIKit = {
|
||||||
AvatarUploader,
|
AvatarUploader,
|
||||||
|
@ -31,9 +32,12 @@ const WootUIKit = {
|
||||||
Code,
|
Code,
|
||||||
ColorPicker,
|
ColorPicker,
|
||||||
ConfirmDeleteModal,
|
ConfirmDeleteModal,
|
||||||
|
ConfirmModal,
|
||||||
|
ContextMenu,
|
||||||
DeleteModal,
|
DeleteModal,
|
||||||
DropdownItem,
|
DropdownItem,
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
|
FeatureToggle,
|
||||||
HorizontalBar,
|
HorizontalBar,
|
||||||
Input,
|
Input,
|
||||||
Label,
|
Label,
|
||||||
|
@ -47,8 +51,6 @@ const WootUIKit = {
|
||||||
Tabs,
|
Tabs,
|
||||||
TabsItem,
|
TabsItem,
|
||||||
Thumbnail,
|
Thumbnail,
|
||||||
ConfirmModal,
|
|
||||||
ContextMenu,
|
|
||||||
install(Vue) {
|
install(Vue) {
|
||||||
const keys = Object.keys(this);
|
const keys = Object.keys(this);
|
||||||
keys.pop(); // remove 'install' from keys
|
keys.pop(); // remove 'install' from keys
|
||||||
|
|
|
@ -18,12 +18,35 @@
|
||||||
</woot-button>
|
</woot-button>
|
||||||
</woot-dropdown-item>
|
</woot-dropdown-item>
|
||||||
<woot-dropdown-divider />
|
<woot-dropdown-divider />
|
||||||
|
<woot-dropdown-item class="auto-offline--toggle">
|
||||||
|
<div class="info-wrap">
|
||||||
|
<fluent-icon
|
||||||
|
v-tooltip.right-start="$t('SIDEBAR.SET_AUTO_OFFLINE.INFO_TEXT')"
|
||||||
|
icon="info"
|
||||||
|
size="14"
|
||||||
|
class="info-icon"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span class="auto-offline--text">
|
||||||
|
{{ $t('SIDEBAR.SET_AUTO_OFFLINE.TEXT') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<woot-switch
|
||||||
|
size="small"
|
||||||
|
class="auto-offline--switch"
|
||||||
|
:value="currentUserAutoOffline"
|
||||||
|
@input="updateAutoOffline"
|
||||||
|
/>
|
||||||
|
</woot-dropdown-item>
|
||||||
|
<woot-dropdown-divider />
|
||||||
</woot-dropdown-menu>
|
</woot-dropdown-menu>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import { mixin as clickaway } from 'vue-clickaway';
|
import { mixin as clickaway } from 'vue-clickaway';
|
||||||
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem';
|
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem';
|
||||||
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu';
|
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu';
|
||||||
import WootDropdownHeader from 'shared/components/ui/dropdown/DropdownHeader';
|
import WootDropdownHeader from 'shared/components/ui/dropdown/DropdownHeader';
|
||||||
|
@ -41,7 +64,7 @@ export default {
|
||||||
AvailabilityStatusBadge,
|
AvailabilityStatusBadge,
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [clickaway],
|
mixins: [clickaway, alertMixin],
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -54,6 +77,7 @@ export default {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
getCurrentUserAvailability: 'getCurrentUserAvailability',
|
getCurrentUserAvailability: 'getCurrentUserAvailability',
|
||||||
currentAccountId: 'getCurrentAccountId',
|
currentAccountId: 'getCurrentAccountId',
|
||||||
|
currentUserAutoOffline: 'getCurrentUserAutoOffline',
|
||||||
}),
|
}),
|
||||||
availabilityDisplayLabel() {
|
availabilityDisplayLabel() {
|
||||||
const availabilityIndex = AVAILABILITY_STATUS_KEYS.findIndex(
|
const availabilityIndex = AVAILABILITY_STATUS_KEYS.findIndex(
|
||||||
|
@ -85,21 +109,30 @@ export default {
|
||||||
closeStatusMenu() {
|
closeStatusMenu() {
|
||||||
this.isStatusMenuOpened = false;
|
this.isStatusMenuOpened = false;
|
||||||
},
|
},
|
||||||
|
updateAutoOffline(autoOffline) {
|
||||||
|
this.$store.dispatch('updateAutoOffline', {
|
||||||
|
accountId: this.currentAccountId,
|
||||||
|
autoOffline,
|
||||||
|
});
|
||||||
|
},
|
||||||
changeAvailabilityStatus(availability) {
|
changeAvailabilityStatus(availability) {
|
||||||
const accountId = this.currentAccountId;
|
|
||||||
if (this.isUpdating) {
|
if (this.isUpdating) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.isUpdating = true;
|
this.isUpdating = true;
|
||||||
this.$store
|
try {
|
||||||
.dispatch('updateAvailability', {
|
this.$store.dispatch('updateAvailability', {
|
||||||
availability: availability,
|
availability,
|
||||||
account_id: accountId,
|
account_id: this.currentAccountId,
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
this.isUpdating = false;
|
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
this.showAlert(
|
||||||
|
this.$t('PROFILE_SETTINGS.FORM.AVAILABILITY.SET_AVAILABILITY_ERROR')
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
this.isUpdating = false;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -143,4 +176,32 @@ export default {
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.auto-offline--toggle {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: var(--space-smaller) 0 var(--space-smaller) var(--space-small);
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
.info-wrap {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-icon {
|
||||||
|
margin-top: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auto-offline--switch {
|
||||||
|
margin: -1px var(--space-micro) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auto-offline--text {
|
||||||
|
margin: 0 var(--space-smaller);
|
||||||
|
font-size: var(--font-size-mini);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
color: var(--s-700);
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -73,13 +73,14 @@ export default {
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
currentUser: 'getCurrentUser',
|
|
||||||
globalConfig: 'globalConfig/get',
|
|
||||||
isACustomBrandedInstance: 'globalConfig/isACustomBrandedInstance',
|
|
||||||
isOnChatwootCloud: 'globalConfig/isOnChatwootCloud',
|
|
||||||
inboxes: 'inboxes/getInboxes',
|
|
||||||
accountId: 'getCurrentAccountId',
|
accountId: 'getCurrentAccountId',
|
||||||
currentRole: 'getCurrentRole',
|
currentRole: 'getCurrentRole',
|
||||||
|
currentUser: 'getCurrentUser',
|
||||||
|
globalConfig: 'globalConfig/get',
|
||||||
|
inboxes: 'inboxes/getInboxes',
|
||||||
|
isACustomBrandedInstance: 'globalConfig/isACustomBrandedInstance',
|
||||||
|
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
|
||||||
|
isOnChatwootCloud: 'globalConfig/isOnChatwootCloud',
|
||||||
labels: 'labels/getLabelsOnSidebar',
|
labels: 'labels/getLabelsOnSidebar',
|
||||||
teams: 'teams/getMyTeams',
|
teams: 'teams/getMyTeams',
|
||||||
}),
|
}),
|
||||||
|
@ -108,9 +109,21 @@ export default {
|
||||||
},
|
},
|
||||||
primaryMenuItems() {
|
primaryMenuItems() {
|
||||||
const menuItems = this.sideMenuConfig.primaryMenu;
|
const menuItems = this.sideMenuConfig.primaryMenu;
|
||||||
return menuItems.filter(menuItem =>
|
return menuItems.filter(menuItem => {
|
||||||
menuItem.roles.includes(this.currentRole)
|
const isAvailableForTheUser = menuItem.roles.includes(this.currentRole);
|
||||||
);
|
|
||||||
|
if (!isAvailableForTheUser) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (menuItem.featureFlag) {
|
||||||
|
return this.isFeatureEnabledonAccount(
|
||||||
|
this.accountId,
|
||||||
|
menuItem.featureFlag
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
activeSecondaryMenu() {
|
activeSecondaryMenu() {
|
||||||
const { secondaryMenu } = this.sideMenuConfig;
|
const { secondaryMenu } = this.sideMenuConfig;
|
||||||
|
@ -285,8 +298,6 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
.secondary-menu .nested.vertical.menu {
|
.secondary-menu .nested.vertical.menu {
|
||||||
overflow-y: auto;
|
|
||||||
height: 100%;
|
|
||||||
margin-left: var(--space-small);
|
margin-left: var(--space-small);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -16,6 +16,8 @@ const conversations = accountId => ({
|
||||||
'conversation_through_mentions',
|
'conversation_through_mentions',
|
||||||
'folder_conversations',
|
'folder_conversations',
|
||||||
'conversations_through_folders',
|
'conversations_through_folders',
|
||||||
|
'conversation_unattended',
|
||||||
|
'conversation_through_unattended',
|
||||||
],
|
],
|
||||||
menuItems: [
|
menuItems: [
|
||||||
{
|
{
|
||||||
|
@ -33,6 +35,13 @@ const conversations = accountId => ({
|
||||||
toState: frontendURL(`accounts/${accountId}/mentions/conversations`),
|
toState: frontendURL(`accounts/${accountId}/mentions/conversations`),
|
||||||
toStateName: 'conversation_mentions',
|
toStateName: 'conversation_mentions',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
icon: 'mail-unread',
|
||||||
|
label: 'UNATTENDED_CONVERSATIONS',
|
||||||
|
key: 'conversation_unattended',
|
||||||
|
toState: frontendURL(`accounts/${accountId}/unattended/conversations`),
|
||||||
|
toStateName: 'conversation_unattended',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { FEATURE_FLAGS } from '../../../../featureFlags';
|
||||||
import { frontendURL } from '../../../../helper/URLHelper';
|
import { frontendURL } from '../../../../helper/URLHelper';
|
||||||
|
|
||||||
const primaryMenuItems = accountId => [
|
const primaryMenuItems = accountId => [
|
||||||
|
@ -13,6 +14,7 @@ const primaryMenuItems = accountId => [
|
||||||
icon: 'book-contacts',
|
icon: 'book-contacts',
|
||||||
key: 'contacts',
|
key: 'contacts',
|
||||||
label: 'CONTACTS',
|
label: 'CONTACTS',
|
||||||
|
featureFlag: FEATURE_FLAGS.CRM,
|
||||||
toState: frontendURL(`accounts/${accountId}/contacts`),
|
toState: frontendURL(`accounts/${accountId}/contacts`),
|
||||||
toStateName: 'contacts_dashboard',
|
toStateName: 'contacts_dashboard',
|
||||||
roles: ['administrator', 'agent'],
|
roles: ['administrator', 'agent'],
|
||||||
|
@ -21,6 +23,7 @@ const primaryMenuItems = accountId => [
|
||||||
icon: 'arrow-trending-lines',
|
icon: 'arrow-trending-lines',
|
||||||
key: 'reports',
|
key: 'reports',
|
||||||
label: 'REPORTS',
|
label: 'REPORTS',
|
||||||
|
featureFlag: FEATURE_FLAGS.REPORTS,
|
||||||
toState: frontendURL(`accounts/${accountId}/reports`),
|
toState: frontendURL(`accounts/${accountId}/reports`),
|
||||||
toStateName: 'settings_account_reports',
|
toStateName: 'settings_account_reports',
|
||||||
roles: ['administrator'],
|
roles: ['administrator'],
|
||||||
|
@ -29,10 +32,20 @@ const primaryMenuItems = accountId => [
|
||||||
icon: 'megaphone',
|
icon: 'megaphone',
|
||||||
key: 'campaigns',
|
key: 'campaigns',
|
||||||
label: 'CAMPAIGNS',
|
label: 'CAMPAIGNS',
|
||||||
|
featureFlag: FEATURE_FLAGS.CAMPAIGNS,
|
||||||
toState: frontendURL(`accounts/${accountId}/campaigns`),
|
toState: frontendURL(`accounts/${accountId}/campaigns`),
|
||||||
toStateName: 'settings_account_campaigns',
|
toStateName: 'settings_account_campaigns',
|
||||||
roles: ['administrator'],
|
roles: ['administrator'],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
icon: 'library',
|
||||||
|
key: 'helpcenter',
|
||||||
|
label: 'HELP_CENTER.TITLE',
|
||||||
|
featureFlag: FEATURE_FLAGS.HELP_CENTER,
|
||||||
|
toState: frontendURL(`accounts/${accountId}/portals`),
|
||||||
|
toStateName: 'default_portal_articles',
|
||||||
|
roles: ['administrator'],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
icon: 'settings',
|
icon: 'settings',
|
||||||
key: 'settings',
|
key: 'settings',
|
||||||
|
|
|
@ -1,45 +1,58 @@
|
||||||
|
import { FEATURE_FLAGS } from '../../../../featureFlags';
|
||||||
import { frontendURL } from '../../../../helper/URLHelper';
|
import { frontendURL } from '../../../../helper/URLHelper';
|
||||||
|
|
||||||
const settings = accountId => ({
|
const settings = accountId => ({
|
||||||
parentNav: 'settings',
|
parentNav: 'settings',
|
||||||
routes: [
|
routes: [
|
||||||
|
'agent_bots',
|
||||||
'agent_list',
|
'agent_list',
|
||||||
'canned_list',
|
|
||||||
'labels_list',
|
|
||||||
'settings_inbox',
|
|
||||||
'attributes_list',
|
'attributes_list',
|
||||||
'settings_inbox_new',
|
'automation_list',
|
||||||
'settings_inbox_list',
|
'billing_settings_index',
|
||||||
'settings_inbox_show',
|
'canned_list',
|
||||||
'settings_inboxes_page_channel',
|
|
||||||
'settings_inboxes_add_agents',
|
|
||||||
'settings_inbox_finish',
|
|
||||||
'settings_integrations',
|
|
||||||
'settings_integrations_webhook',
|
|
||||||
'settings_integrations_integration',
|
|
||||||
'settings_applications',
|
|
||||||
'settings_integrations_dashboard_apps',
|
|
||||||
'settings_applications_webhook',
|
|
||||||
'settings_applications_integration',
|
|
||||||
'general_settings',
|
|
||||||
'general_settings_index',
|
'general_settings_index',
|
||||||
|
'general_settings',
|
||||||
|
'labels_list',
|
||||||
|
'macros_edit',
|
||||||
|
'macros_new',
|
||||||
|
'macros_wrapper',
|
||||||
|
'settings_applications_integration',
|
||||||
|
'settings_applications_webhook',
|
||||||
|
'settings_applications',
|
||||||
|
'settings_inbox_finish',
|
||||||
|
'settings_inbox_list',
|
||||||
|
'settings_inbox_new',
|
||||||
|
'settings_inbox_show',
|
||||||
|
'settings_inbox',
|
||||||
|
'settings_inboxes_add_agents',
|
||||||
|
'settings_inboxes_page_channel',
|
||||||
|
'settings_integrations_dashboard_apps',
|
||||||
|
'settings_integrations_integration',
|
||||||
|
'settings_integrations_webhook',
|
||||||
|
'settings_integrations',
|
||||||
|
'settings_teams_add_agents',
|
||||||
|
'settings_teams_edit_finish',
|
||||||
|
'settings_teams_edit_members',
|
||||||
|
'settings_teams_edit',
|
||||||
|
'settings_teams_finish',
|
||||||
'settings_teams_list',
|
'settings_teams_list',
|
||||||
'settings_teams_new',
|
'settings_teams_new',
|
||||||
'settings_teams_add_agents',
|
|
||||||
'settings_teams_finish',
|
|
||||||
'settings_teams_edit',
|
|
||||||
'settings_teams_edit_members',
|
|
||||||
'settings_teams_edit_finish',
|
|
||||||
'billing_settings_index',
|
|
||||||
'automation_list',
|
|
||||||
],
|
],
|
||||||
menuItems: [
|
menuItems: [
|
||||||
|
{
|
||||||
|
icon: 'briefcase',
|
||||||
|
label: 'ACCOUNT_SETTINGS',
|
||||||
|
hasSubMenu: false,
|
||||||
|
toState: frontendURL(`accounts/${accountId}/settings/general`),
|
||||||
|
toStateName: 'general_settings_index',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
icon: 'people',
|
icon: 'people',
|
||||||
label: 'AGENTS',
|
label: 'AGENTS',
|
||||||
hasSubMenu: false,
|
hasSubMenu: false,
|
||||||
toState: frontendURL(`accounts/${accountId}/settings/agents/list`),
|
toState: frontendURL(`accounts/${accountId}/settings/agents/list`),
|
||||||
toStateName: 'agent_list',
|
toStateName: 'agent_list',
|
||||||
|
featureFlag: FEATURE_FLAGS.AGENT_MANAGEMENT,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'people-team',
|
icon: 'people-team',
|
||||||
|
@ -47,6 +60,7 @@ const settings = accountId => ({
|
||||||
hasSubMenu: false,
|
hasSubMenu: false,
|
||||||
toState: frontendURL(`accounts/${accountId}/settings/teams/list`),
|
toState: frontendURL(`accounts/${accountId}/settings/teams/list`),
|
||||||
toStateName: 'settings_teams_list',
|
toStateName: 'settings_teams_list',
|
||||||
|
featureFlag: FEATURE_FLAGS.TEAM_MANAGEMENT,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'mail-inbox-all',
|
icon: 'mail-inbox-all',
|
||||||
|
@ -54,6 +68,7 @@ const settings = accountId => ({
|
||||||
hasSubMenu: false,
|
hasSubMenu: false,
|
||||||
toState: frontendURL(`accounts/${accountId}/settings/inboxes/list`),
|
toState: frontendURL(`accounts/${accountId}/settings/inboxes/list`),
|
||||||
toStateName: 'settings_inbox_list',
|
toStateName: 'settings_inbox_list',
|
||||||
|
featureFlag: FEATURE_FLAGS.INBOX_MANAGEMENT,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'tag',
|
icon: 'tag',
|
||||||
|
@ -61,6 +76,7 @@ const settings = accountId => ({
|
||||||
hasSubMenu: false,
|
hasSubMenu: false,
|
||||||
toState: frontendURL(`accounts/${accountId}/settings/labels/list`),
|
toState: frontendURL(`accounts/${accountId}/settings/labels/list`),
|
||||||
toStateName: 'labels_list',
|
toStateName: 'labels_list',
|
||||||
|
featureFlag: FEATURE_FLAGS.LABELS,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'code',
|
icon: 'code',
|
||||||
|
@ -70,13 +86,35 @@ const settings = accountId => ({
|
||||||
`accounts/${accountId}/settings/custom-attributes/list`
|
`accounts/${accountId}/settings/custom-attributes/list`
|
||||||
),
|
),
|
||||||
toStateName: 'attributes_list',
|
toStateName: 'attributes_list',
|
||||||
|
featureFlag: FEATURE_FLAGS.CUSTOM_ATTRIBUTES,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'automation',
|
icon: 'automation',
|
||||||
label: 'AUTOMATION',
|
label: 'AUTOMATION',
|
||||||
|
beta: true,
|
||||||
hasSubMenu: false,
|
hasSubMenu: false,
|
||||||
toState: frontendURL(`accounts/${accountId}/settings/automation/list`),
|
toState: frontendURL(`accounts/${accountId}/settings/automation/list`),
|
||||||
toStateName: 'automation_list',
|
toStateName: 'automation_list',
|
||||||
|
featureFlag: FEATURE_FLAGS.AUTOMATIONS,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'bot',
|
||||||
|
label: 'AGENT_BOTS',
|
||||||
|
beta: true,
|
||||||
|
hasSubMenu: false,
|
||||||
|
globalConfigFlag: 'csmlEditorHost',
|
||||||
|
toState: frontendURL(`accounts/${accountId}/settings/agent-bots`),
|
||||||
|
toStateName: 'agent_bots',
|
||||||
|
featureFlag: FEATURE_FLAGS.AGENT_BOTS,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'flash-settings',
|
||||||
|
label: 'MACROS',
|
||||||
|
hasSubMenu: false,
|
||||||
|
toState: frontendURL(`accounts/${accountId}/settings/macros`),
|
||||||
|
toStateName: 'macros_wrapper',
|
||||||
|
beta: true,
|
||||||
|
featureFlag: FEATURE_FLAGS.MACROS,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'chat-multiple',
|
icon: 'chat-multiple',
|
||||||
|
@ -86,6 +124,7 @@ const settings = accountId => ({
|
||||||
`accounts/${accountId}/settings/canned-response/list`
|
`accounts/${accountId}/settings/canned-response/list`
|
||||||
),
|
),
|
||||||
toStateName: 'canned_list',
|
toStateName: 'canned_list',
|
||||||
|
featureFlag: FEATURE_FLAGS.CANNED_RESPONSES,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'flash-on',
|
icon: 'flash-on',
|
||||||
|
@ -93,6 +132,7 @@ const settings = accountId => ({
|
||||||
hasSubMenu: false,
|
hasSubMenu: false,
|
||||||
toState: frontendURL(`accounts/${accountId}/settings/integrations`),
|
toState: frontendURL(`accounts/${accountId}/settings/integrations`),
|
||||||
toStateName: 'settings_integrations',
|
toStateName: 'settings_integrations',
|
||||||
|
featureFlag: FEATURE_FLAGS.INTEGRATIONS,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'star-emphasis',
|
icon: 'star-emphasis',
|
||||||
|
@ -100,6 +140,7 @@ const settings = accountId => ({
|
||||||
hasSubMenu: false,
|
hasSubMenu: false,
|
||||||
toState: frontendURL(`accounts/${accountId}/settings/applications`),
|
toState: frontendURL(`accounts/${accountId}/settings/applications`),
|
||||||
toStateName: 'settings_applications',
|
toStateName: 'settings_applications',
|
||||||
|
featureFlag: FEATURE_FLAGS.INTEGRATIONS,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'credit-card-person',
|
icon: 'credit-card-person',
|
||||||
|
@ -109,13 +150,6 @@ const settings = accountId => ({
|
||||||
toStateName: 'billing_settings_index',
|
toStateName: 'billing_settings_index',
|
||||||
showOnlyOnCloud: true,
|
showOnlyOnCloud: true,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
icon: 'settings',
|
|
||||||
label: 'ACCOUNT_SETTINGS',
|
|
||||||
hasSubMenu: false,
|
|
||||||
toState: frontendURL(`accounts/${accountId}/settings/general`),
|
|
||||||
toStateName: 'general_settings_index',
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -8,25 +8,33 @@
|
||||||
:header-title="$t('SIDEBAR_ITEMS.CHANGE_ACCOUNTS')"
|
:header-title="$t('SIDEBAR_ITEMS.CHANGE_ACCOUNTS')"
|
||||||
:header-content="$t('SIDEBAR_ITEMS.SELECTOR_SUBTITLE')"
|
:header-content="$t('SIDEBAR_ITEMS.SELECTOR_SUBTITLE')"
|
||||||
/>
|
/>
|
||||||
<div
|
<div class="account-selector--wrap">
|
||||||
v-for="account in currentUser.accounts"
|
<div
|
||||||
:key="account.id"
|
v-for="account in currentUser.accounts"
|
||||||
class="account-selector"
|
:key="account.id"
|
||||||
>
|
class="account-selector"
|
||||||
<a :href="`/app/accounts/${account.id}/dashboard`">
|
>
|
||||||
<fluent-icon
|
<button
|
||||||
v-if="account.id === accountId"
|
class="button expanded clear link"
|
||||||
class="selected--account"
|
@click="onChangeAccount(account.id)"
|
||||||
icon="checkmark-circle"
|
>
|
||||||
type="solid"
|
<span class="button__content">
|
||||||
size="24"
|
<label :for="account.name" class="account-details--wrap">
|
||||||
/>
|
<div class="account--name">{{ account.name }}</div>
|
||||||
<label :for="account.name" class="account--details">
|
<div class="account--role">{{ account.role }}</div>
|
||||||
<div class="account--name">{{ account.name }}</div>
|
</label>
|
||||||
<div class="account--role">{{ account.role }}</div>
|
</span>
|
||||||
</label>
|
<fluent-icon
|
||||||
</a>
|
v-show="account.id === accountId"
|
||||||
|
class="selected--account"
|
||||||
|
icon="checkmark-circle"
|
||||||
|
type="solid"
|
||||||
|
size="24"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="globalConfig.createNewAccountFromDashboard"
|
v-if="globalConfig.createNewAccountFromDashboard"
|
||||||
class="modal-footer delete-item"
|
class="modal-footer delete-item"
|
||||||
|
@ -58,5 +66,40 @@ export default {
|
||||||
globalConfig: 'globalConfig/get',
|
globalConfig: 'globalConfig/get',
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
methods: {
|
||||||
|
onChangeAccount(accountId) {
|
||||||
|
const accountUrl = `/app/accounts/${accountId}/dashboard`;
|
||||||
|
window.location.href = accountUrl;
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.account-selector--wrap {
|
||||||
|
margin-top: var(--space-normal);
|
||||||
|
}
|
||||||
|
.account-selector {
|
||||||
|
padding-top: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
.button {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: var(--space-one) var(--space-normal);
|
||||||
|
.account-details--wrap {
|
||||||
|
text-align: left;
|
||||||
|
.account--name {
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: var(--font-size-medium);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account--role {
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: var(--font-size-mini);
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -61,6 +61,24 @@
|
||||||
</a>
|
</a>
|
||||||
</router-link>
|
</router-link>
|
||||||
</woot-dropdown-item>
|
</woot-dropdown-item>
|
||||||
|
<woot-dropdown-item v-if="currentUser.type === 'SuperAdmin'">
|
||||||
|
<a
|
||||||
|
href="/super_admin"
|
||||||
|
class="button small clear secondary"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener nofollow noreferrer"
|
||||||
|
@click="$emit('close')"
|
||||||
|
>
|
||||||
|
<fluent-icon
|
||||||
|
icon="content-settings"
|
||||||
|
size="14"
|
||||||
|
class="icon icon--font"
|
||||||
|
/>
|
||||||
|
<span class="button__content">
|
||||||
|
{{ $t('SIDEBAR_ITEMS.SUPER_ADMIN_CONSOLE') }}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</woot-dropdown-item>
|
||||||
<woot-dropdown-item>
|
<woot-dropdown-item>
|
||||||
<woot-button
|
<woot-button
|
||||||
variant="clear"
|
variant="clear"
|
||||||
|
@ -135,7 +153,7 @@ export default {
|
||||||
.dropdown-pane {
|
.dropdown-pane {
|
||||||
left: var(--space-slab);
|
left: var(--space-slab);
|
||||||
bottom: var(--space-larger);
|
bottom: var(--space-larger);
|
||||||
min-width: 16.8rem;
|
min-width: 22rem;
|
||||||
z-index: var(--z-index-much-higher);
|
z-index: var(--z-index-low);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -20,6 +20,8 @@
|
||||||
import { frontendURL } from '../../../helper/URLHelper';
|
import { frontendURL } from '../../../helper/URLHelper';
|
||||||
import SecondaryNavItem from './SecondaryNavItem.vue';
|
import SecondaryNavItem from './SecondaryNavItem.vue';
|
||||||
import AccountContext from './AccountContext.vue';
|
import AccountContext from './AccountContext.vue';
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import { FEATURE_FLAGS } from '../../../featureFlags';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
@ -61,6 +63,9 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
|
||||||
|
}),
|
||||||
hasSecondaryMenu() {
|
hasSecondaryMenu() {
|
||||||
return this.menuConfig.menuItems && this.menuConfig.menuItems.length;
|
return this.menuConfig.menuItems && this.menuConfig.menuItems.length;
|
||||||
},
|
},
|
||||||
|
@ -89,7 +94,7 @@ export default {
|
||||||
icon: 'folder',
|
icon: 'folder',
|
||||||
label: 'INBOXES',
|
label: 'INBOXES',
|
||||||
hasSubMenu: true,
|
hasSubMenu: true,
|
||||||
newLink: true,
|
newLink: this.showNewLink(FEATURE_FLAGS.INBOX_MANAGEMENT),
|
||||||
newLinkTag: 'NEW_INBOX',
|
newLinkTag: 'NEW_INBOX',
|
||||||
key: 'inbox',
|
key: 'inbox',
|
||||||
toState: frontendURL(`accounts/${this.accountId}/settings/inboxes/new`),
|
toState: frontendURL(`accounts/${this.accountId}/settings/inboxes/new`),
|
||||||
|
@ -117,7 +122,7 @@ export default {
|
||||||
icon: 'number-symbol',
|
icon: 'number-symbol',
|
||||||
label: 'LABELS',
|
label: 'LABELS',
|
||||||
hasSubMenu: true,
|
hasSubMenu: true,
|
||||||
newLink: true,
|
newLink: this.showNewLink(FEATURE_FLAGS.TEAM_MANAGEMENT),
|
||||||
newLinkTag: 'NEW_LABEL',
|
newLinkTag: 'NEW_LABEL',
|
||||||
key: 'label',
|
key: 'label',
|
||||||
toState: frontendURL(`accounts/${this.accountId}/settings/labels`),
|
toState: frontendURL(`accounts/${this.accountId}/settings/labels`),
|
||||||
|
@ -141,7 +146,7 @@ export default {
|
||||||
label: 'TAGGED_WITH',
|
label: 'TAGGED_WITH',
|
||||||
hasSubMenu: true,
|
hasSubMenu: true,
|
||||||
key: 'label',
|
key: 'label',
|
||||||
newLink: true,
|
newLink: this.showNewLink(FEATURE_FLAGS.TEAM_MANAGEMENT),
|
||||||
newLinkTag: 'NEW_LABEL',
|
newLinkTag: 'NEW_LABEL',
|
||||||
toState: frontendURL(`accounts/${this.accountId}/settings/labels`),
|
toState: frontendURL(`accounts/${this.accountId}/settings/labels`),
|
||||||
toStateName: 'labels_list',
|
toStateName: 'labels_list',
|
||||||
|
@ -163,7 +168,7 @@ export default {
|
||||||
icon: 'people-team',
|
icon: 'people-team',
|
||||||
label: 'TEAMS',
|
label: 'TEAMS',
|
||||||
hasSubMenu: true,
|
hasSubMenu: true,
|
||||||
newLink: true,
|
newLink: this.showNewLink(FEATURE_FLAGS.TEAM_MANAGEMENT),
|
||||||
newLinkTag: 'NEW_TEAM',
|
newLinkTag: 'NEW_TEAM',
|
||||||
key: 'team',
|
key: 'team',
|
||||||
toState: frontendURL(`accounts/${this.accountId}/settings/teams/new`),
|
toState: frontendURL(`accounts/${this.accountId}/settings/teams/new`),
|
||||||
|
@ -238,6 +243,9 @@ export default {
|
||||||
toggleAccountModal() {
|
toggleAccountModal() {
|
||||||
this.$emit('toggle-accounts');
|
this.$emit('toggle-accounts');
|
||||||
},
|
},
|
||||||
|
showNewLink(featureFlag) {
|
||||||
|
return this.isFeatureEnabledonAccount(this.accountId, featureFlag);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -245,20 +253,15 @@ export default {
|
||||||
@import '~dashboard/assets/scss/woot';
|
@import '~dashboard/assets/scss/woot';
|
||||||
|
|
||||||
.secondary-menu {
|
.secondary-menu {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
background: var(--white);
|
background: var(--white);
|
||||||
border-right: 1px solid var(--s-50);
|
border-right: 1px solid var(--s-50);
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 20rem;
|
width: 20rem;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
|
position: unset;
|
||||||
@include breakpoint(xlarge down) {
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
|
|
||||||
@include breakpoint(xlarge up) {
|
|
||||||
position: unset;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
|
@ -267,7 +270,6 @@ export default {
|
||||||
.menu {
|
.menu {
|
||||||
padding: var(--space-small);
|
padding: var(--space-small);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
height: 94%;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
:class="{ 'text-truncate': shouldTruncate }"
|
:class="{ 'text-truncate': shouldTruncate }"
|
||||||
>
|
>
|
||||||
{{ label }}
|
{{ label }}
|
||||||
<span v-if="isHelpCenterSidebar && childItemCount" class="count-view">
|
<span v-if="showChildCount" class="count-view">
|
||||||
{{ childItemCount }}
|
{{ childItemCount }}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
@ -76,7 +76,7 @@ export default {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
isHelpCenterSidebar: {
|
showChildCount: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
@ -112,6 +112,7 @@ $label-badge-size: var(--space-slab);
|
||||||
padding: var(--space-smaller) var(--space-smaller);
|
padding: var(--space-smaller) var(--space-smaller);
|
||||||
margin: var(--space-smaller) 0;
|
margin: var(--space-smaller) 0;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
line-height: 1.2;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: var(--s-25);
|
background: var(--s-25);
|
||||||
|
@ -127,11 +128,14 @@ $label-badge-size: var(--space-slab);
|
||||||
color: var(--w-500);
|
color: var(--w-500);
|
||||||
border-color: var(--w-25);
|
border-color: var(--w-25);
|
||||||
}
|
}
|
||||||
|
&.is-active .count-view {
|
||||||
|
background: var(--w-75);
|
||||||
|
color: var(--w-500);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-label {
|
.menu-label {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
line-height: var(--space-two);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.inbox-icon {
|
.inbox-icon {
|
||||||
|
@ -175,10 +179,6 @@ $label-badge-size: var(--space-slab);
|
||||||
font-weight: var(--font-weight-bold);
|
font-weight: var(--font-weight-bold);
|
||||||
margin-left: var(--space-smaller);
|
margin-left: var(--space-smaller);
|
||||||
padding: var(--space-zero) var(--space-smaller);
|
padding: var(--space-zero) var(--space-smaller);
|
||||||
|
line-height: var(--font-size-small);
|
||||||
&.is-active {
|
|
||||||
background: var(--w-50);
|
|
||||||
color: var(--w-500);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,19 +1,18 @@
|
||||||
<template>
|
<template>
|
||||||
<li class="sidebar-item">
|
<li v-show="isMenuItemVisible" class="sidebar-item">
|
||||||
<div v-if="hasSubMenu" class="secondary-menu--wrap">
|
<div v-if="hasSubMenu" class="secondary-menu--wrap">
|
||||||
<span class="secondary-menu--header fs-small">
|
<span class="secondary-menu--header fs-small">
|
||||||
{{ $t(`SIDEBAR.${menuItem.label}`) }}
|
{{ $t(`SIDEBAR.${menuItem.label}`) }}
|
||||||
</span>
|
</span>
|
||||||
<div v-if="isHelpCenterSidebar" class="submenu-icons">
|
<div v-if="menuItem.showNewButton" class="submenu-icons">
|
||||||
<woot-button
|
<woot-button
|
||||||
size="tiny"
|
size="tiny"
|
||||||
variant="clear"
|
variant="clear"
|
||||||
color-scheme="secondary"
|
color-scheme="secondary"
|
||||||
|
icon="add"
|
||||||
class="submenu-icon"
|
class="submenu-icon"
|
||||||
@click="onClickOpen"
|
@click="onClickOpen"
|
||||||
>
|
/>
|
||||||
<fluent-icon icon="add" size="16" />
|
|
||||||
</woot-button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<router-link
|
<router-link
|
||||||
|
@ -28,15 +27,11 @@
|
||||||
size="14"
|
size="14"
|
||||||
/>
|
/>
|
||||||
{{ $t(`SIDEBAR.${menuItem.label}`) }}
|
{{ $t(`SIDEBAR.${menuItem.label}`) }}
|
||||||
<span
|
<span v-if="showChildCount(menuItem.count)" class="count-view">
|
||||||
v-if="isHelpCenterSidebar"
|
|
||||||
class="count-view"
|
|
||||||
:class="computedClass"
|
|
||||||
>
|
|
||||||
{{ `${menuItem.count}` }}
|
{{ `${menuItem.count}` }}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
v-if="menuItem.label === 'AUTOMATION'"
|
v-if="menuItem.beta"
|
||||||
data-view-component="true"
|
data-view-component="true"
|
||||||
label="Beta"
|
label="Beta"
|
||||||
class="beta"
|
class="beta"
|
||||||
|
@ -55,7 +50,7 @@
|
||||||
:should-truncate="child.truncateLabel"
|
:should-truncate="child.truncateLabel"
|
||||||
:icon="computedInboxClass(child)"
|
:icon="computedInboxClass(child)"
|
||||||
:warning-icon="computedInboxErrorClass(child)"
|
:warning-icon="computedInboxErrorClass(child)"
|
||||||
:is-help-center-sidebar="isHelpCenterSidebar"
|
:show-child-count="showChildCount(child.count)"
|
||||||
:child-item-count="child.count"
|
:child-item-count="child.count"
|
||||||
/>
|
/>
|
||||||
<router-link
|
<router-link
|
||||||
|
@ -64,10 +59,10 @@
|
||||||
:to="menuItem.toState"
|
:to="menuItem.toState"
|
||||||
custom
|
custom
|
||||||
>
|
>
|
||||||
<li>
|
<li class="menu-item--new">
|
||||||
<a
|
<a
|
||||||
:href="href"
|
:href="href"
|
||||||
class="button small clear menu-item--new secondary"
|
class="button small link clear secondary"
|
||||||
:class="{ 'is-active': isActive }"
|
:class="{ 'is-active': isActive }"
|
||||||
@click="e => newLinkClick(e, navigate)"
|
@click="e => newLinkClick(e, navigate)"
|
||||||
>
|
>
|
||||||
|
@ -78,9 +73,6 @@
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</router-link>
|
</router-link>
|
||||||
<p v-if="isHelpCenterSidebar && isCategoryEmpty" class="empty-text">
|
|
||||||
{{ $t('SIDEBAR.HELP_CENTER.CATEGORY_EMPTY_MESSAGE') }}
|
|
||||||
</p>
|
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
|
@ -95,6 +87,10 @@ import {
|
||||||
} from 'dashboard/helper/inbox';
|
} from 'dashboard/helper/inbox';
|
||||||
|
|
||||||
import SecondaryChildNavItem from './SecondaryChildNavItem';
|
import SecondaryChildNavItem from './SecondaryChildNavItem';
|
||||||
|
import {
|
||||||
|
isOnMentionsView,
|
||||||
|
isOnUnattendedView,
|
||||||
|
} from '../../../store/modules/conversations/helpers/actionHelpers';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: { SecondaryChildNavItem },
|
components: { SecondaryChildNavItem },
|
||||||
|
@ -104,33 +100,54 @@ export default {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => ({}),
|
default: () => ({}),
|
||||||
},
|
},
|
||||||
isHelpCenterSidebar: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
isCategoryEmpty: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({ activeInbox: 'getSelectedInbox' }),
|
...mapGetters({
|
||||||
|
activeInbox: 'getSelectedInbox',
|
||||||
|
accountId: 'getCurrentAccountId',
|
||||||
|
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
|
||||||
|
globalConfig: 'globalConfig/get',
|
||||||
|
}),
|
||||||
hasSubMenu() {
|
hasSubMenu() {
|
||||||
return !!this.menuItem.children;
|
return !!this.menuItem.children;
|
||||||
},
|
},
|
||||||
isInboxConversation() {
|
isMenuItemVisible() {
|
||||||
|
if (this.menuItem.globalConfigFlag) {
|
||||||
|
return !!this.globalConfig[this.menuItem.globalConfigFlag];
|
||||||
|
}
|
||||||
|
if (this.menuItem.featureFlag) {
|
||||||
|
return this.isFeatureEnabledonAccount(
|
||||||
|
this.accountId,
|
||||||
|
this.menuItem.featureFlag
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
isAllConversations() {
|
||||||
return (
|
return (
|
||||||
this.$store.state.route.name === 'inbox_conversation' &&
|
this.$store.state.route.name === 'inbox_conversation' &&
|
||||||
this.menuItem.toStateName === 'home'
|
this.menuItem.toStateName === 'home'
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
isMentions() {
|
||||||
|
return (
|
||||||
|
isOnMentionsView({ route: this.$route }) &&
|
||||||
|
this.menuItem.toStateName === 'conversation_mentions'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
isUnattended() {
|
||||||
|
return (
|
||||||
|
isOnUnattendedView({ route: this.$route }) &&
|
||||||
|
this.menuItem.toStateName === 'conversation_unattended'
|
||||||
|
);
|
||||||
|
},
|
||||||
isTeamsSettings() {
|
isTeamsSettings() {
|
||||||
return (
|
return (
|
||||||
this.$store.state.route.name === 'settings_teams_edit' &&
|
this.$store.state.route.name === 'settings_teams_edit' &&
|
||||||
this.menuItem.toStateName === 'settings_teams_list'
|
this.menuItem.toStateName === 'settings_teams_list'
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
isInboxsSettings() {
|
isInboxSettings() {
|
||||||
return (
|
return (
|
||||||
this.$store.state.route.name === 'settings_inbox_show' &&
|
this.$store.state.route.name === 'settings_inbox_show' &&
|
||||||
this.menuItem.toStateName === 'settings_inbox_list'
|
this.menuItem.toStateName === 'settings_inbox_list'
|
||||||
|
@ -148,19 +165,25 @@ export default {
|
||||||
this.menuItem.toStateName === 'settings_applications'
|
this.menuItem.toStateName === 'settings_applications'
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
isArticlesView() {
|
isCurrentRoute() {
|
||||||
return this.$store.state.route.name === this.menuItem.toStateName;
|
return this.$store.state.route.name.includes(this.menuItem.toStateName);
|
||||||
},
|
},
|
||||||
|
|
||||||
computedClass() {
|
computedClass() {
|
||||||
// If active Inbox is present
|
// If active inbox is present, do not highlight conversations
|
||||||
// donot highlight conversations
|
|
||||||
if (this.activeInbox) return ' ';
|
if (this.activeInbox) return ' ';
|
||||||
|
if (
|
||||||
|
this.isAllConversations ||
|
||||||
|
this.isMentions ||
|
||||||
|
this.isUnattended ||
|
||||||
|
this.isCurrentRoute
|
||||||
|
) {
|
||||||
|
return 'is-active';
|
||||||
|
}
|
||||||
if (this.hasSubMenu) {
|
if (this.hasSubMenu) {
|
||||||
if (
|
if (
|
||||||
this.isInboxConversation ||
|
|
||||||
this.isTeamsSettings ||
|
this.isTeamsSettings ||
|
||||||
this.isInboxsSettings ||
|
this.isInboxSettings ||
|
||||||
this.isIntegrationsSettings ||
|
this.isIntegrationsSettings ||
|
||||||
this.isApplicationsSettings
|
this.isApplicationsSettings
|
||||||
) {
|
) {
|
||||||
|
@ -168,12 +191,7 @@ export default {
|
||||||
}
|
}
|
||||||
return ' ';
|
return ' ';
|
||||||
}
|
}
|
||||||
if (this.isHelpCenterSidebar) {
|
|
||||||
if (this.isArticlesView) {
|
|
||||||
return 'is-active';
|
|
||||||
}
|
|
||||||
return ' ';
|
|
||||||
}
|
|
||||||
return '';
|
return '';
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -204,11 +222,14 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
showItem(item) {
|
showItem(item) {
|
||||||
return this.isAdmin && item.newLink !== undefined;
|
return this.isAdmin && !!item.newLink;
|
||||||
},
|
},
|
||||||
onClickOpen() {
|
onClickOpen() {
|
||||||
this.$emit('open');
|
this.$emit('open');
|
||||||
},
|
},
|
||||||
|
showChildCount(count) {
|
||||||
|
return Number.isInteger(count);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -264,6 +285,11 @@ export default {
|
||||||
color: var(--w-500);
|
color: var(--w-500);
|
||||||
border-color: var(--w-25);
|
border-color: var(--w-25);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.is-active .count-view {
|
||||||
|
background: var(--w-75);
|
||||||
|
color: var(--w-600);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.secondary-menu--icon {
|
.secondary-menu--icon {
|
||||||
|
@ -293,22 +319,19 @@ export default {
|
||||||
top: -1px;
|
top: -1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-item .button.menu-item--new {
|
.sidebar-item .menu-item--new {
|
||||||
display: inline-flex;
|
padding: var(--space-small) 0;
|
||||||
height: var(--space-medium);
|
|
||||||
margin: var(--space-smaller) 0;
|
|
||||||
padding: var(--space-smaller);
|
|
||||||
color: var(--s-500);
|
|
||||||
|
|
||||||
&:hover {
|
.button {
|
||||||
color: var(--w-500);
|
display: inline-flex;
|
||||||
|
color: var(--s-500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.beta {
|
.beta {
|
||||||
padding-right: var(--space-smaller) !important;
|
padding-right: var(--space-smaller) !important;
|
||||||
padding-left: var(--space-smaller) !important;
|
padding-left: var(--space-smaller) !important;
|
||||||
margin-left: var(--space-half) !important;
|
margin-left: var(--space-smaller) !important;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-size: var(--font-size-micro);
|
font-size: var(--font-size-micro);
|
||||||
font-weight: var(--font-weight-medium);
|
font-weight: var(--font-weight-medium);
|
||||||
|
@ -327,11 +350,6 @@ export default {
|
||||||
font-weight: var(--font-weight-bold);
|
font-weight: var(--font-weight-bold);
|
||||||
margin-left: var(--space-smaller);
|
margin-left: var(--space-smaller);
|
||||||
padding: var(--space-zero) var(--space-smaller);
|
padding: var(--space-zero) var(--space-smaller);
|
||||||
|
|
||||||
&.is-active {
|
|
||||||
background: var(--w-50);
|
|
||||||
color: var(--w-500);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.submenu-icons {
|
.submenu-icons {
|
||||||
|
@ -343,10 +361,4 @@ export default {
|
||||||
margin-left: var(--space-small);
|
margin-left: var(--space-small);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-text {
|
|
||||||
color: var(--s-500);
|
|
||||||
font-size: var(--font-size-small);
|
|
||||||
margin: var(--space-smaller);
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`SidemenuIcon matches snapshot 1`] = `
|
exports[`SidemenuIcon matches snapshot 1`] = `
|
||||||
<button>
|
<woot-button
|
||||||
<fluent-icon
|
class="toggle-sidebar"
|
||||||
class="hamburger--menu"
|
color-scheme="secondary"
|
||||||
icon="list"
|
icon="list"
|
||||||
/>
|
size="small"
|
||||||
</button>
|
variant="clear"
|
||||||
|
/>
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
<fluent-icon :icon="icon" size="12" class="label--icon" />
|
<fluent-icon :icon="icon" size="12" class="label--icon" />
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
v-if="variant === 'smooth'"
|
v-if="variant === 'smooth' && title && !icon"
|
||||||
:style="{ background: color }"
|
:style="{ background: color }"
|
||||||
class="label-color-dot"
|
class="label-color-dot"
|
||||||
/>
|
/>
|
||||||
|
@ -117,14 +117,16 @@ export default {
|
||||||
height: var(--space-medium);
|
height: var(--space-medium);
|
||||||
|
|
||||||
&.small {
|
&.small {
|
||||||
font-size: var(--font-size-micro);
|
font-size: var(--font-size-mini);
|
||||||
padding: var(--space-micro) var(--space-smaller);
|
padding: var(--space-micro) var(--space-smaller);
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
letter-spacing: 0.15px;
|
height: var(--space-two);
|
||||||
}
|
}
|
||||||
|
|
||||||
.label--icon {
|
.label--icon {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.label-color-dot {
|
||||||
margin-right: var(--space-smaller);
|
margin-right: var(--space-smaller);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -199,8 +201,8 @@ export default {
|
||||||
|
|
||||||
&.smooth {
|
&.smooth {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 1px solid var(--s-75);
|
border: 1px solid var(--s-100);
|
||||||
color: var(--s-800);
|
color: var(--s-700);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -221,14 +223,22 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
.label-action--button {
|
.label-action--button {
|
||||||
margin-bottom: var(--space-minus-micro);
|
display: flex;
|
||||||
|
margin-right: var(--space-smaller);
|
||||||
}
|
}
|
||||||
|
|
||||||
.label-color-dot {
|
.label-color-dot {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: var(--space-one);
|
width: var(--space-slab);
|
||||||
height: var(--space-one);
|
height: var(--space-slab);
|
||||||
border-radius: var(--border-radius-small);
|
border-radius: var(--border-radius-small);
|
||||||
margin-right: var(--space-smaller);
|
margin-right: var(--space-smaller);
|
||||||
|
box-shadow: var(--shadow-small);
|
||||||
|
}
|
||||||
|
.label.small .label-color-dot {
|
||||||
|
width: var(--space-small);
|
||||||
|
height: var(--space-small);
|
||||||
|
border-radius: var(--border-radius-small);
|
||||||
|
box-shadow: var(--shadow-small);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
113
app/javascript/dashboard/components/ui/PreviewCard.vue
Normal file
113
app/javascript/dashboard/components/ui/PreviewCard.vue
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
<template>
|
||||||
|
<div class="preview-card--wrap" :class="{ activecard: active }">
|
||||||
|
<div class="header--wrap" :class="{ active: active }">
|
||||||
|
<div class="heading-wrap text-block-title">{{ heading }}</div>
|
||||||
|
<fluent-icon
|
||||||
|
v-if="active"
|
||||||
|
icon="checkmark-circle"
|
||||||
|
type="solid"
|
||||||
|
size="24"
|
||||||
|
class="checkmark"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="content-wrap">
|
||||||
|
{{ content }}
|
||||||
|
</div>
|
||||||
|
<div class="image-wrap">
|
||||||
|
<img :src="src" class="image" :class="{ activeimage: active }" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
heading: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
active: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
buttonText: {
|
||||||
|
type: String,
|
||||||
|
default: 'Active',
|
||||||
|
},
|
||||||
|
src: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.preview-card--wrap {
|
||||||
|
border-radius: var(--border-radius-normal);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
max-height: 34rem;
|
||||||
|
max-width: 38rem;
|
||||||
|
min-width: 24rem;
|
||||||
|
|
||||||
|
.header--wrap {
|
||||||
|
background: var(--s-50);
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
border-top-left-radius: var(--border-radius-normal);
|
||||||
|
border-top-right-radius: var(--border-radius-normal);
|
||||||
|
display: flex;
|
||||||
|
height: 4rem;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: var(--space-small);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active {
|
||||||
|
background: var(--w-50);
|
||||||
|
border-bottom: 1px solid var(--w-75);
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading-wrap {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
padding: var(--space-smaller);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkmark {
|
||||||
|
color: var(--w-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-wrap {
|
||||||
|
color: var(--s-700);
|
||||||
|
font-size: var(--font-size-mini);
|
||||||
|
line-height: 1.4;
|
||||||
|
padding: var(--space-slab) var(--space-slab) 0 var(--space-slab);
|
||||||
|
text-align: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-wrap {
|
||||||
|
padding: var(--space-slab);
|
||||||
|
}
|
||||||
|
|
||||||
|
.image {
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--border-radius-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.activeimage {
|
||||||
|
border: 1px solid var(--w-75);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.activecard {
|
||||||
|
background: var(--w-25);
|
||||||
|
border: 1px solid var(--w-300);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -2,7 +2,7 @@
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="toggle-button"
|
class="toggle-button"
|
||||||
:class="{ active: value }"
|
:class="{ active: value, small: size === 'small' }"
|
||||||
role="switch"
|
role="switch"
|
||||||
:aria-checked="value.toString()"
|
:aria-checked="value.toString()"
|
||||||
@click="onClick"
|
@click="onClick"
|
||||||
|
@ -15,6 +15,7 @@
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
value: { type: Boolean, default: false },
|
value: { type: Boolean, default: false },
|
||||||
|
size: { type: String, default: '' },
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onClick() {
|
onClick() {
|
||||||
|
@ -45,6 +46,20 @@ export default {
|
||||||
background-color: var(--w-500);
|
background-color: var(--w-500);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.small {
|
||||||
|
width: 22px;
|
||||||
|
height: 14px;
|
||||||
|
|
||||||
|
span {
|
||||||
|
height: var(--space-one);
|
||||||
|
width: var(--space-one);
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
transform: translate(var(--space-small), var(--space-zero));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
span {
|
span {
|
||||||
--space-one-point-five: 1.5rem;
|
--space-one-point-five: 1.5rem;
|
||||||
background-color: var(--white);
|
background-color: var(--white);
|
||||||
|
|
|
@ -1,17 +1,15 @@
|
||||||
<template>
|
<template>
|
||||||
<span class="time-ago">
|
<span class="time-ago">
|
||||||
<span> {{ timeAgo }}</span>
|
<span>{{ timeAgo }}</span>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const ZERO = 0;
|
|
||||||
const MINUTE_IN_MILLI_SECONDS = 60000;
|
const MINUTE_IN_MILLI_SECONDS = 60000;
|
||||||
const HOUR_IN_MILLI_SECONDS = MINUTE_IN_MILLI_SECONDS * 60;
|
const HOUR_IN_MILLI_SECONDS = MINUTE_IN_MILLI_SECONDS * 60;
|
||||||
const DAY_IN_MILLI_SECONDS = HOUR_IN_MILLI_SECONDS * 24;
|
const DAY_IN_MILLI_SECONDS = HOUR_IN_MILLI_SECONDS * 24;
|
||||||
|
|
||||||
import timeMixin from 'dashboard/mixins/time';
|
import timeMixin from 'dashboard/mixins/time';
|
||||||
import { differenceInMilliseconds } from 'date-fns';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'TimeAgo',
|
name: 'TimeAgo',
|
||||||
|
@ -28,51 +26,40 @@ export default {
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
timeAgo: '',
|
timeAgo: this.dynamicTime(this.timestamp),
|
||||||
timer: null,
|
timer: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
watch: {
|
||||||
|
timestamp() {
|
||||||
|
this.timeAgo = this.dynamicTime(this.timestamp);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
if (this.isAutoRefreshEnabled) {
|
||||||
|
this.createTimer();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
clearTimeout(this.timer);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
createTimer() {
|
||||||
|
this.timer = setTimeout(() => {
|
||||||
|
this.timeAgo = this.dynamicTime(this.timestamp);
|
||||||
|
this.createTimer();
|
||||||
|
}, this.refreshTime());
|
||||||
|
},
|
||||||
refreshTime() {
|
refreshTime() {
|
||||||
const timeDiff = differenceInMilliseconds(
|
const timeDiff = Date.now() - this.timestamp * 1000;
|
||||||
new Date(),
|
|
||||||
new Date(this.timestamp * 1000)
|
|
||||||
);
|
|
||||||
if (timeDiff > DAY_IN_MILLI_SECONDS) {
|
if (timeDiff > DAY_IN_MILLI_SECONDS) {
|
||||||
return DAY_IN_MILLI_SECONDS;
|
return DAY_IN_MILLI_SECONDS;
|
||||||
}
|
}
|
||||||
if (timeDiff > HOUR_IN_MILLI_SECONDS) {
|
if (timeDiff > HOUR_IN_MILLI_SECONDS) {
|
||||||
return HOUR_IN_MILLI_SECONDS;
|
return HOUR_IN_MILLI_SECONDS;
|
||||||
}
|
}
|
||||||
if (timeDiff > MINUTE_IN_MILLI_SECONDS) {
|
|
||||||
return MINUTE_IN_MILLI_SECONDS;
|
return MINUTE_IN_MILLI_SECONDS;
|
||||||
}
|
|
||||||
return ZERO;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.timeAgo = this.dynamicTime(this.timestamp);
|
|
||||||
if (this.isAutoRefreshEnabled) {
|
|
||||||
this.createTimer();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
beforeDestroy() {
|
|
||||||
this.clearTimer();
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
createTimer() {
|
|
||||||
const refreshTime = this.refreshTime;
|
|
||||||
if (refreshTime > ZERO) {
|
|
||||||
this.timer = setTimeout(() => {
|
|
||||||
this.timeAgo = this.dynamicTime(this.timestamp);
|
|
||||||
this.createTimer();
|
|
||||||
}, refreshTime);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
clearTimer() {
|
|
||||||
if (this.timer) {
|
|
||||||
clearTimeout(this.timer);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,8 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div class="filter" :class="actionInputStyles">
|
||||||
class="filter"
|
|
||||||
:class="{ error: v.action_params.$dirty && v.action_params.$error }"
|
|
||||||
>
|
|
||||||
<div class="filter-inputs">
|
<div class="filter-inputs">
|
||||||
<select
|
<select
|
||||||
v-model="action_name"
|
v-model="action_name"
|
||||||
|
@ -21,14 +18,32 @@
|
||||||
<div v-if="showActionInput" class="filter__answer--wrap">
|
<div v-if="showActionInput" class="filter__answer--wrap">
|
||||||
<div v-if="inputType">
|
<div v-if="inputType">
|
||||||
<div
|
<div
|
||||||
v-if="inputType === 'multi_select'"
|
v-if="inputType === 'search_select'"
|
||||||
class="multiselect-wrap--small"
|
class="multiselect-wrap--small"
|
||||||
>
|
>
|
||||||
<multiselect
|
<multiselect
|
||||||
v-model="action_params"
|
v-model="action_params"
|
||||||
track-by="id"
|
track-by="id"
|
||||||
label="name"
|
label="name"
|
||||||
:placeholder="'Select'"
|
:placeholder="$t('FORMS.MULTISELECT.SELECT')"
|
||||||
|
selected-label
|
||||||
|
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
|
||||||
|
deselect-label=""
|
||||||
|
:max-height="160"
|
||||||
|
:options="dropdownValues"
|
||||||
|
:allow-empty="false"
|
||||||
|
:option-height="104"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="inputType === 'multi_select'"
|
||||||
|
class="multiselect-wrap--small"
|
||||||
|
>
|
||||||
|
<multiselect
|
||||||
|
v-model="action_params"
|
||||||
|
track-by="id"
|
||||||
|
label="name"
|
||||||
|
:placeholder="$t('FORMS.MULTISELECT.SELECT')"
|
||||||
:multiple="true"
|
:multiple="true"
|
||||||
selected-label
|
selected-label
|
||||||
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
|
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
|
||||||
|
@ -36,6 +51,7 @@
|
||||||
:max-height="160"
|
:max-height="160"
|
||||||
:options="dropdownValues"
|
:options="dropdownValues"
|
||||||
:allow-empty="false"
|
:allow-empty="false"
|
||||||
|
:option-height="104"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
|
@ -60,6 +76,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<woot-button
|
<woot-button
|
||||||
|
v-if="!isMacro"
|
||||||
icon="dismiss"
|
icon="dismiss"
|
||||||
variant="clear"
|
variant="clear"
|
||||||
color-scheme="secondary"
|
color-scheme="secondary"
|
||||||
|
@ -120,6 +137,10 @@ export default {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
|
isMacro: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
action_name: {
|
action_name: {
|
||||||
|
@ -146,6 +167,12 @@ export default {
|
||||||
return this.actionTypes.find(action => action.key === this.action_name)
|
return this.actionTypes.find(action => action.key === this.action_name)
|
||||||
.inputType;
|
.inputType;
|
||||||
},
|
},
|
||||||
|
actionInputStyles() {
|
||||||
|
return {
|
||||||
|
'has-error': this.v.action_params.$dirty && this.v.action_params.$error,
|
||||||
|
'is-a-macro': this.isMacro,
|
||||||
|
};
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
removeAction() {
|
removeAction() {
|
||||||
|
@ -165,9 +192,21 @@ export default {
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
border-radius: var(--border-radius-medium);
|
border-radius: var(--border-radius-medium);
|
||||||
margin-bottom: var(--space-small);
|
margin-bottom: var(--space-small);
|
||||||
|
|
||||||
|
&.is-a-macro {
|
||||||
|
margin-bottom: 0;
|
||||||
|
background: var(--white);
|
||||||
|
padding: var(--space-zero);
|
||||||
|
border: unset;
|
||||||
|
border-radius: unset;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter.error {
|
.no-margin-bottom {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter.has-error {
|
||||||
background: var(--r-50);
|
background: var(--r-50);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -240,6 +279,6 @@ export default {
|
||||||
margin-bottom: var(--space-zero);
|
margin-bottom: var(--space-zero);
|
||||||
}
|
}
|
||||||
.action-message {
|
.action-message {
|
||||||
margin: var(--space-small) 0 0;
|
margin: var(--space-small) var(--space-zero) var(--space-zero);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue