Compare commits

...
Author SHA1 Message Date
Shivam Mishra 68ed5a4b4f chore: source account id directly 2024-10-08 18:52:34 +05:30
Shivam Mishra 7ecb993d06 chore: force init account 2024-10-08 18:49:26 +05:30
Shivam Mishra c00a157c03 chore: more log 2024-10-08 18:45:08 +05:30
Shivam Mishra 3750689bee chore: debug debug 2024-10-08 18:43:37 +05:30
Shivam Mishra 0511bf4ba6 chore: more debug 2024-10-08 18:34:06 +05:30
Shivam Mishra 9f50638a40 chore: more logs 2024-10-08 18:30:59 +05:30
Shivam Mishra f5ac272e3b chore: logs 2024-10-08 18:18:49 +05:30
Shivam Mishra 2ba7318817 chore: more try 2024-10-08 18:14:58 +05:30
Shivam Mishra 412303d1cd chore: try i18n 2024-10-08 15:20:35 +05:30
Shivam Mishra 47b9399208 feat: setup fallback 2024-10-08 14:01:57 +05:30
Shivam Mishra 0830ad78b6 chore: try 2024-10-08 13:53:58 +05:30
Shivam Mishra 0b8df762f9 chore: more debug 2024-10-08 12:38:03 +05:30
Shivam Mishra f24c87f9b2 chore: try 2024-10-08 12:28:48 +05:30
Shivam Mishra f62a4170b6 chore: debug 2024-10-08 12:20:14 +05:30
Shivam Mishra 2f090dbc61 Revert "chore: don't use async"
This reverts commit 9101e1e221.
2024-10-08 11:54:02 +05:30
Shivam Mishra ec17682d7a chore: move to created 2024-10-08 11:53:55 +05:30
Shivam Mishra 7861ce8c47 chore: revert iframe helper 2024-10-08 11:42:09 +05:30
Shivam Mishra e5de41fea4 chore: revert console.logs 2024-10-08 11:41:40 +05:30
Shivam Mishra adb0e94cbd chore: remove logs 2024-10-08 11:40:53 +05:30
Shivam Mishra 42fb083491 chore: debug 2024-10-08 11:31:33 +05:30
Shivam Mishra 9101e1e221 chore: don't use async 2024-10-08 11:18:13 +05:30
Shivam Mishra 91bac16c29 chore: add more error log 2024-10-08 11:00:21 +05:30
Shivam Mishra 97699f3087 chore: log errors 2024-10-08 10:59:42 +05:30
Shivam Mishra 1242cfd704 chore: add logs for iframhelper 2024-10-08 10:57:13 +05:30
Shivam Mishra 6963c668bf chore: add console logs 2024-10-08 10:49:56 +05:30
Shivam MishraandGitHub a8c12ffb25 chore: update interpolation syntax for i18n files (#10198)
There were two warnings showing up on new teams page

1. `errorMessage` prop was getting invalid value, this was because were
short circuting the error message using `&&`, fixed it by using ternary
operator
2. `vue-i18n` has deprecated [rails
syntax](https://vue-i18n.intlify.dev/guide/essentials/syntax#rails-i18n-format),
fixed that warning by removing `%` before `{}` for interpolation

> Note: the `vue-i18n` deprecation needs to be handled, but we can do it
later
2024-10-07 11:12:40 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
97d4ce65c4 chore(deps): bump braces from 3.0.2 to 3.0.3 (#10238)
Bumps [braces](https://github.com/micromatch/braces) from 3.0.2 to
3.0.3.
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/micromatch/braces/commit/74b2db2938fad48a2ea54a9c8bf27a37a62c350d"><code>74b2db2</code></a>
3.0.3</li>
<li><a
href="https://github.com/micromatch/braces/commit/88f1429a0f47e1dd3813de35211fc97ffda27f9e"><code>88f1429</code></a>
update eslint. lint, fix unit tests.</li>
<li><a
href="https://github.com/micromatch/braces/commit/415d660c3002d1ab7e63dbf490c9851da80596ff"><code>415d660</code></a>
Snyk js braces 6838727 (<a
href="https://redirect.github.com/micromatch/braces/issues/40">#40</a>)</li>
<li><a
href="https://github.com/micromatch/braces/commit/190510f79db1adf21d92798b0bb6fccc1f72c9d6"><code>190510f</code></a>
fix tests, skip 1 test in test/braces.expand</li>
<li><a
href="https://github.com/micromatch/braces/commit/716eb9f12d820b145a831ad678618731927e8856"><code>716eb9f</code></a>
readme bump</li>
<li><a
href="https://github.com/micromatch/braces/commit/a5851e57f45c3431a94d83fc565754bc10f5bbc3"><code>a5851e5</code></a>
Merge pull request <a
href="https://redirect.github.com/micromatch/braces/issues/37">#37</a>
from coderaiser/fix/vulnerability</li>
<li><a
href="https://github.com/micromatch/braces/commit/2092bd1fb108d2c59bd62e243b70ad98db961538"><code>2092bd1</code></a>
feature: braces: add maxSymbols (<a
href="https://github.com/micromatch/braces/issues/">https://github.com/micromatch/braces/issues/</a>...</li>
<li><a
href="https://github.com/micromatch/braces/commit/9f5b4cf47329351bcb64287223ffb6ecc9a5e6d3"><code>9f5b4cf</code></a>
fix: vulnerability (<a
href="https://security.snyk.io/vuln/SNYK-JS-BRACES-6838727">https://security.snyk.io/vuln/SNYK-JS-BRACES-6838727</a>)</li>
<li><a
href="https://github.com/micromatch/braces/commit/98414f9f1fabe021736e26836d8306d5de747e0d"><code>98414f9</code></a>
remove funding file</li>
<li><a
href="https://github.com/micromatch/braces/commit/665ab5d561c017a38ba7aafd92cc6655b91d8c14"><code>665ab5d</code></a>
update keepEscaping doc (<a
href="https://redirect.github.com/micromatch/braces/issues/27">#27</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/micromatch/braces/compare/3.0.2...3.0.3">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=braces&package-manager=npm_and_yarn&previous-version=3.0.2&new-version=3.0.3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/chatwoot/chatwoot/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-07 11:12:02 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
ea787fecd8 chore(deps): bump dset from 3.1.3 to 3.1.4 (#10239)
Bumps [dset](https://github.com/lukeed/dset) from 3.1.3 to 3.1.4.
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/lukeed/dset/commit/05b1ec0f8cebe3b5d5667285bc7da90f38a7215e"><code>05b1ec0</code></a>
3.1.4</li>
<li><a
href="https://github.com/lukeed/dset/commit/16d6154e085bef01e99f01330e5a421a7f098afa"><code>16d6154</code></a>
fix: prevent <strong>proto</strong> assignment via implicit string</li>
<li>See full diff in <a
href="https://github.com/lukeed/dset/compare/v3.1.3...v3.1.4">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=dset&package-manager=npm_and_yarn&previous-version=3.1.3&new-version=3.1.4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/chatwoot/chatwoot/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-07 11:11:36 -07:00
ee02923ace chore: fix circleci on vite build (#10214)
- Switch to pnpm based build
- Switch circleci from docker to machine to have more memory
- Fix frontend and backend tests

Fixes
https://linear.app/chatwoot/issue/CW-3610/fix-circle-ci-for-vite-build
---------

Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
Co-authored-by: Pranav <pranavrajs@gmail.com>
Co-authored-by: Pranav <pranav@chatwoot.com>
2024-10-07 15:27:41 +05:30
0677d8763d fix: Update the reply box to handle play, pause callbacks from WaveSurfer (#10223)
- Implemented custom @play, @pause methods to update the state of the recording. Once the recording is finished the button icon changes from stop button to play/pause button.
- Removes the console error undefined hasAudio

Fixes https://linear.app/chatwoot/issue/CW-3609/audio-recorder-issue

---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2024-10-05 00:17:11 -07:00
Sivin VargheseandGitHub 095aaf3de6 fix: Issues with drag and drop attachments to the editor (#10235) 2024-10-05 00:16:42 -07:00
PranavandGitHub b7aea99f44 fix: Add webhook string to the zh_CN to fix missing webhook feature (#10234)
The integrations are displayed based on whether the integration object
is available on the API. Since there are chances where empty objects can
be returned, the check for the presence was done based on the
"integration.name". The name was empty (this should be investigated) in
the zh_CN locale, which led to the feature being disabled for a user.

In this PR, I just added the string which should enable the feature. In
the followups, I will add a better check for the presence of the
integration object.
2024-10-04 17:50:38 -07:00
Sivin VargheseandGitHub d0bf66fe7a feat: Update widget builder to support dark mode (#10230) 2024-10-04 15:53:31 -07:00
Shivam MishraandGitHub 490fc4eaba fix: Remove the usage of asyncComponent in emoji picker (#10232)
This PR has the following fixes

1. preview of article inside the iframe, earlier this didn't work because the iframe didn't get the URL correctly. We fix that by passing the URL is a query instead
2. Emoji picker caused a weird redirect, this was only happening when the chunk was loaded async, this PR changes it to use regular loading instead
2024-10-04 08:10:27 -07:00
Vishnu NarayananandGitHub e6e0f7de5a fix: Add missing 'one' translations for time units in agent-reports (#10228) 2024-10-04 08:08:25 -07:00
d2eda8bd08 fix: Update inconsistent behaviour of create and edit automation rules (#10197)
When Vue 3 is used with options API, any assignment to `this.<something>` is converted to a Proxy before assignment. This is fine as long as we are in the options context, problem arises when we access this in a `composable` any mutations on the object doesn't behave correctly as expected, this PR fixes that by moving the `automation` object inside the composable and using it in the options.

> Another option to fix such an issue is to make the object non-reactive, like done in places where we have `editorView`, but that wasn't viable here

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2024-10-04 08:06:36 -07:00
55bf05d2d4 fix: Use arrow function to bind methods to class (#10215)
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
2024-10-04 08:05:34 -07:00
Shivam MishraandGitHub 5b22af6af8 fix: Update input event bindings to use blur events (#10216) 2024-10-04 08:04:56 -07:00
9338bc1391 fix: emit events across the app (#10227)
This PR makes the following changes

1. Update v-model bindings for components using the old `value` prop and `input` event method
2. Remove components that were not used anywhere

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2024-10-04 08:03:41 -07:00
Shivam MishraandGitHub 88a16b8e96 fix: submit pre chat triggering native submit event (#10224) 2024-10-04 20:31:25 +05:30
Vishnu NarayananandGitHub 8c54d7f794 feat: support vite build for linux installations(cwctl) (#10231)
- Switch to `pnpm` from `yarn`

Changelog
----
- add support for `pnpm` `vite` build for chatwoot 4.0
- for new installations, install pnpm and vite
- for existing installations, remove `node_modules` and install `pnpm`

Note: `yarn` is not removed when upgrading existing installations. If
you want to rollback to an older version of Chatwoot(pre 4.0),

```
sudo -i -u chatwoot
cd chatwoot
git checkout <tag> # tag is the version of chatwoot you want to rollback to
rm -rf node_modules # remove deps installed via pnpm

# Update dependencies
bundle
yarn

# Recompile the assets
rake assets:precompile RAILS_ENV=production

# Migrate the database schema
RAILS_ENV=production bundle exec rake db:migrate
exit
```
2024-10-04 16:50:44 +05:30
Shivam MishraandGitHub bd88bfb0fe fix: emits for FormSelect and PhoneInput component (#10226) 2024-10-04 15:09:42 +05:30
Vishnu NarayananandGitHub 83100b8f60 feat: support vite build for docker based installations (#10225)
- Switch to `pnpm` from `yarn` for docker builds
2024-10-04 14:20:56 +05:30
PranavandGitHub 0f26351fd3 fix: Update card labels button with a new one to fix the CSS issue (#10222) 2024-10-04 00:00:35 -07:00
Sojan JoseandGitHub 6d053e5816 chore: Move line attachment creation to a single db commit (#10194)
Similar to the work we did with the legacy code of other channels like Twilio, we need to move the message creation and attachment creation to be under a single db commit for the line channel. Otherwise, the emitted webhook events for message creation will miss the attachment payload.


Ref: https://github.com/orgs/chatwoot/discussions/7546#discussioncomment-10814495
Ref: https://github.com/chatwoot/chatwoot/pull/10167
2024-10-03 22:13:41 -07:00
Shivam MishraandGitHub bbb8e57fee fix: Update the styles for the datepicker in custom snooze modal (#10207) 2024-10-03 22:06:51 -07:00
Shivam MishraandGitHub 5eac95732b fix: Update translation payload for audit logs (#10217)
Vue i18n would automatically merge arrays previously, it does not do so now. This PR fixes it by cleaning up the payload before passing it for translation
2024-10-03 22:04:26 -07:00
b3262597c1 fix: vue 3 followup fixes (#10213)
Fixes: CW-3602,  CW-3606, CW-3605, CW-3601, CW-3603, CW-3600, CW-3598

-
[CW-3602](https://linear.app/chatwoot/issue/CW-3602/chat-list-infinite-loader-fetching-only-odd-numbered-pages)
Chat list pagination broken
-
[CW-3606](https://linear.app/chatwoot/issue/CW-3606/saving-greeting-message-is-not-working-in-inbox-settings)
Greetings message not getting saved
-
[CW-3605](https://linear.app/chatwoot/issue/CW-3605/copy-and-paste-image-attachment-not-working-in-widget)
Paste not working on widget
-
[CW-3601](https://linear.app/chatwoot/issue/CW-3601/edit-category-is-not-working-properly)
Edit category not updating
-
[CW-3603](https://linear.app/chatwoot/issue/CW-3603/delete-filter-is-not-working)
Delete filter modal not toggling
-
[CW-3600](https://linear.app/chatwoot/issue/CW-3600/portal-editor-is-not-working-properly)
Portal editor events were flaky
-
[CW-3598](https://linear.app/chatwoot/issue/CW-3598/rearrange-of-pre-chat-form-fields-throws-an-error)
Prechat form re-order bug

---------

Co-authored-by: Vishnu Narayanan <iamwishnu@gmail.com>
2024-10-03 19:59:07 +05:30
Shivam MishraandGitHub 701135df92 fix: vitest resolution in vite.config [CW-3587] (#10204)
The ruby plugin conflicted with vitest resolution, this PR fixes it. We
will need to separate out the vite config for this
2024-10-03 19:54:57 +05:30
Vishnu NarayananandGitHub fdc3e370ef fix: gh actions for vite build (#10212)
Fixes https://linear.app/chatwoot/issue/CW-3604/fix-gh-actions-for-vite-build

- [x] response bot spec
- [x] chatwoot ce spec
- [x] size limit check
2024-10-03 16:25:48 +05:30
Shivam MishraandGitHub f14edd5242 fix: initOnEvents not removed [CW-3594] (#10200)
The `initOnEvents` was used to get the notification sound file and
trigger the 30 second loop, but since the function was replaced to using
class syntax, the removeEvent listener was not working. This PR fixes it
by reverting to the old syntax but moving it inside the constructor
instead and also adding a `once: true` to ensure it is always removed
automatically
2024-10-03 15:03:44 +05:30
Shivam MishraandGitHub 578dce81a1 fix: suggestions not triggering directly (#10211) 2024-10-03 15:02:56 +05:30
Shivam MishraandGitHub c51a458c25 style: apply fixes for eslint issues [cw-3590] (#10210)
These fixes are all auto generated and can be merged directly

Fixes the following issues

1. Event used on components should be hypenated
2. Attribute orders in components
3. Use `unmounted` instead of `destroyed`
4. Add explicit `emits` declarations for components, autofixed [using
this
script](https://gist.github.com/scmmishra/6f549109b96400006bb69bbde392eddf)


We ignore the top level v-if for now, we will fix it later
2024-10-03 15:02:12 +05:30
Shivam MishraandGitHub b8d0252511 feat: remove usage of .sync and define explicitly emits (#10209)
References

- https://v3-migration.vuejs.org/breaking-changes/v-model
-
https://v3-migration.vuejs.org/breaking-changes/v-on-native-modifier-removed.html
2024-10-03 12:44:18 +05:30
Shivam MishraandGitHub edc1fe2363 fix: eslint issues on CC & BCC email head [CW-3586] (#10203)
The previous usage was flagged because of outdated eslint issue, this PR
fixes it

![CleanShot 2024-10-03 at 08 46
34@2x](https://github.com/user-attachments/assets/52a9de6b-1349-48dc-850d-40e641df69b4)

![CleanShot 2024-10-03 at 08 46
54@2x](https://github.com/user-attachments/assets/7b0972dd-0cbe-4d59-b077-7f4946d53766)
2024-10-03 11:59:30 +05:30
Shivam MishraandGitHub 0491ffc94e fix: signup form broken in production (#10206)
`vue-i18n` has a new [linked message
format](https://vue-i18n.intlify.dev/guide/essentials/syntax#linked-messages),
this however conflicts with raw usage an email address like
`hey@chatwoot.com`. The way to solve this as prescribed by then is to
use [literal
interpolation](https://vue-i18n.intlify.dev/guide/essentials/syntax#literal-interpolation),
like `hey{'@'}chatwoot.com`.

This PR does that for the entire translation files
2024-10-03 11:23:30 +05:30
1247 changed files with 7920 additions and 8052 deletions
+113 -133
View File
@@ -1,84 +1,87 @@
# Ruby CircleCI 2.0 configuration file version: 2.1
# orbs:
# Check https://circleci.com/docs/2.0/language-ruby/ for more details node: circleci/node@6.1.0
#
version: 2
defaults: &defaults defaults: &defaults
working_directory: ~/build working_directory: ~/build
docker: machine:
# specify the version you desire here image: ubuntu-2204:2024.05.1
- image: cimg/ruby:3.3.3-browsers
# Specify service dependencies here if necessary
# CircleCI maintains a library of pre-built images
# documented at https://circleci.com/docs/2.0/circleci-images/
- image: cimg/postgres:15.3
- image: cimg/redis:6.2.6
environment:
- RAILS_LOG_TO_STDOUT: false
- COVERAGE: true
- LOG_LEVEL: warn
parallelism: 4
resource_class: large resource_class: large
environment:
RAILS_LOG_TO_STDOUT: false
COVERAGE: true
LOG_LEVEL: warn
parallelism: 4
jobs: jobs:
build: build:
<<: *defaults <<: *defaults
steps: steps:
- checkout - checkout
- node/install:
node-version: '20.12'
- node/install-pnpm
- node/install-packages:
pkg-manager: pnpm
override-ci-command: pnpm i
- run: node --version
- run: pnpm --version
- run: - run:
name: Configure Bundler name: Install System Dependencies
command: | command: |
echo 'export BUNDLER_VERSION=$(cat Gemfile.lock | tail -1 | tr -d " ")' >> $BASH_ENV sudo apt-get update
source $BASH_ENV DEBIAN_FRONTEND=noninteractive sudo apt-get install -y \
libpq-dev \
redis-server \
postgresql \
build-essential \
git \
curl \
libssl-dev \
zlib1g-dev \
libreadline-dev \
libyaml-dev \
openjdk-11-jdk \
jq \
software-properties-common \
ca-certificates \
imagemagick \
libxml2-dev \
libxslt1-dev \
file \
g++ \
gcc \
autoconf \
gnupg2 \
patch \
ruby-dev \
liblzma-dev \
libgmp-dev \
libncurses5-dev \
libffi-dev \
libgdbm6 \
libgdbm-dev \
libvips
- run:
name: Install RVM and Ruby 3.3.3
command: |
sudo apt-get install -y gpg
gpg --keyserver hkp://keyserver.ubuntu.com --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB
\curl -sSL https://get.rvm.io | bash -s stable
echo 'source ~/.rvm/scripts/rvm' >> $BASH_ENV
source ~/.rvm/scripts/rvm
rvm install "3.3.3"
rvm use 3.3.3 --default
gem install bundler gem install bundler
- run: - run:
name: Which bundler? name: Install Application Dependencies
command: bundle -v
- run:
name: Swap node versions
command: | command: |
set +e source ~/.rvm/scripts/rvm
wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash bundle install
export NVM_DIR="$HOME/.nvm" # pnpm install
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"
nvm install v20
echo 'export NVM_DIR="$HOME/.nvm"' >> $BASH_ENV
echo '[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"' >> $BASH_ENV
# Run bundler
# Load installed gems from cache if possible, bundle install then save cache
# Multiple caches are used to increase the chance of a cache hit
- restore_cache:
keys:
- chatwoot-bundle-{{ .Environment.CACHE_VERSION }}-v20220524-{{ checksum "Gemfile.lock" }}
- run: bundle install --frozen --path ~/.bundle
- save_cache:
paths:
- ~/.bundle
key: chatwoot-bundle-{{ .Environment.CACHE_VERSION }}-v20220524-{{ checksum "Gemfile.lock" }}
# Only necessary if app uses webpacker or yarn in some other way
- restore_cache:
keys:
- chatwoot-yarn-{{ .Environment.CACHE_VERSION }}-{{ checksum "yarn.lock" }}
- chatwoot-yarn-
- run:
name: yarn
command: yarn install --frozen-lockfile --cache-folder ~/.cache/yarn
# Store yarn / webpacker cache
- save_cache:
key: chatwoot-yarn-{{ .Environment.CACHE_VERSION }}-{{ checksum "yarn.lock" }}
paths:
- ~/.cache/yarn
- run: - run:
name: Download cc-test-reporter name: Download cc-test-reporter
@@ -86,12 +89,8 @@ jobs:
mkdir -p ~/tmp mkdir -p ~/tmp
curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ~/tmp/cc-test-reporter curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ~/tmp/cc-test-reporter
chmod +x ~/tmp/cc-test-reporter chmod +x ~/tmp/cc-test-reporter
- persist_to_workspace:
root: ~/tmp
paths:
- cc-test-reporter
# verify swagger specification # Swagger verification
- run: - run:
name: Verify swagger API specification name: Verify swagger API specification
command: | command: |
@@ -104,45 +103,62 @@ jobs:
curl -L https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/6.3.0/openapi-generator-cli-6.3.0.jar > ~/tmp/openapi-generator-cli-6.3.0.jar curl -L https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/6.3.0/openapi-generator-cli-6.3.0.jar > ~/tmp/openapi-generator-cli-6.3.0.jar
java -jar ~/tmp/openapi-generator-cli-6.3.0.jar validate -i swagger/swagger.json java -jar ~/tmp/openapi-generator-cli-6.3.0.jar validate -i swagger/swagger.json
# Database setup # we remove the FRONTED_URL from the .env before running the tests
- run: bundle exec rake db:create - run:
- run: bundle exec rake db:schema:load name: Database Setup and Configure Environment Variables
command: |
pg_pass=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 15 ; echo '')
sed -i "s/REPLACE_WITH_PASSWORD/${pg_pass}/g" ${PWD}/.circleci/setup_chatwoot.sql
chmod 644 ${PWD}/.circleci/setup_chatwoot.sql
mv ${PWD}/.circleci/setup_chatwoot.sql /tmp/
sudo -i -u postgres psql -f /tmp/setup_chatwoot.sql
cp .env.example .env
sed -i '/^FRONTEND_URL/d' .env
sed -i -e '/REDIS_URL/ s/=.*/=redis:\/\/localhost:6379/' .env
sed -i -e '/POSTGRES_HOST/ s/=.*/=localhost/' .env
sed -i -e '/POSTGRES_USERNAME/ s/=.*/=chatwoot/' .env
sed -i -e "/POSTGRES_PASSWORD/ s/=.*/=$pg_pass/" .env
echo -en "\nINSTALLATION_ENV=circleci" >> ".env"
# Database setup
- run:
name: Run DB migrations
command: bundle exec rails db:chatwoot_prepare
# Bundle audit
- run: - run:
name: Bundle audit name: Bundle audit
command: bundle exec bundle audit update && bundle exec bundle audit check -v command: bundle exec bundle audit update && bundle exec bundle audit check -v
# Rubocop linting
- run: - run:
name: Rubocop name: Rubocop
command: bundle exec rubocop command: bundle exec rubocop
# - run: # ESLint linting
# name: Brakeman
# command: bundle exec brakeman
- run: - run:
name: eslint name: eslint
command: yarn run eslint command: pnpm run eslint
# Run frontend tests
- run: - run:
name: Run frontend tests name: Run frontend tests
command: | command: |
mkdir -p ~/tmp/test-results/frontend_specs mkdir -p ~/build/coverage/frontend
~/tmp/cc-test-reporter before-build ~/tmp/cc-test-reporter before-build
yarn test:coverage pnpm run test:coverage
- run:
name: Code Climate Test Coverage
command: |
~/tmp/cc-test-reporter format-coverage -t lcov -o "coverage/codeclimate.frontend_$CIRCLE_NODE_INDEX.json"
# Run rails tests - run:
name: Code Climate Test Coverage (Frontend)
command: |
~/tmp/cc-test-reporter format-coverage -t lcov -o "~/build/coverage/frontend/codeclimate.frontend_$CIRCLE_NODE_INDEX.json"
# Run backend tests
- run: - run:
name: Run backend tests name: Run backend tests
command: | command: |
mkdir -p ~/tmp/test-results/rspec mkdir -p ~/tmp/test-results/rspec
mkdir -p ~/tmp/test-artifacts mkdir -p ~/tmp/test-artifacts
mkdir -p coverage mkdir -p ~/build/coverage/backend
~/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 --format progress \ bundle exec rspec --format progress \
@@ -150,54 +166,18 @@ jobs:
--out ~/tmp/test-results/rspec.xml \ --out ~/tmp/test-results/rspec.xml \
-- ${TESTFILES} -- ${TESTFILES}
no_output_timeout: 30m no_output_timeout: 30m
- run: - run:
name: Code Climate Test Coverage name: Code Climate Test Coverage (Backend)
command: | command: |
~/tmp/cc-test-reporter format-coverage -t simplecov -o "coverage/codeclimate.$CIRCLE_NODE_INDEX.json" ~/tmp/cc-test-reporter format-coverage -t simplecov -o "~/build/coverage/backend/codeclimate.$CIRCLE_NODE_INDEX.json"
- run:
name: List coverage directory contents
command: |
ls -R ~/build/coverage
- persist_to_workspace: - persist_to_workspace:
root: coverage root: ~/build
paths: paths:
- codeclimate.*.json - coverage
# collect reports
- store_test_results:
path: ~/tmp/test-results
- store_artifacts:
path: ~/tmp/test-artifacts
- store_artifacts:
path: log
upload-coverage:
working_directory: ~/build
docker:
# specify the version you desire here
- image: circleci/ruby:3.0.2-node-browsers
environment:
- CC_TEST_REPORTER_ID: caf26a895e937974a90860cfadfded20891cfd1373a5aaafb3f67406ab9d433f
steps:
- attach_workspace:
at: ~/build
- run:
name: Download cc-test-reporter
command: |
mkdir -p ~/tmp
curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ~/tmp/cc-test-reporter
chmod +x ~/tmp/cc-test-reporter
- persist_to_workspace:
root: ~/tmp
paths:
- cc-test-reporter
- run:
name: Upload coverage results to Code Climate
command: |
~/tmp/cc-test-reporter sum-coverage --output - codeclimate.*.json | ~/tmp/cc-test-reporter upload-coverage --debug --input -
workflows:
version: 2
commit:
jobs:
- build
- upload-coverage:
requires:
- build
+11
View File
@@ -0,0 +1,11 @@
CREATE USER chatwoot CREATEDB;
ALTER USER chatwoot PASSWORD 'REPLACE_WITH_PASSWORD';
ALTER ROLE chatwoot SUPERUSER;
UPDATE pg_database SET datistemplate = FALSE WHERE datname = 'template1';
DROP DATABASE template1;
CREATE DATABASE template1 WITH TEMPLATE = template0 ENCODING = 'UNICODE';
UPDATE pg_database SET datistemplate = TRUE WHERE datname = 'template1';
\c template1;
VACUUM FREEZE;
+2 -1
View File
@@ -27,7 +27,8 @@ checks:
threshold: 50 threshold: 50
exclude_patterns: exclude_patterns:
- 'spec/' - 'spec/'
- '**/specs/' - '**/specs/**/**'
- '**/spec/**/**'
- 'db/*' - 'db/*'
- 'bin/**/*' - 'bin/**/*'
- 'db/**/*' - 'db/**/*'
+24 -1
View File
@@ -1,6 +1,23 @@
module.exports = { module.exports = {
extends: ['airbnb-base/legacy', 'prettier', 'plugin:vue/recommended'], extends: [
'airbnb-base/legacy',
'prettier',
'plugin:vue/vue3-recommended',
'plugin:vitest-globals/recommended',
],
overrides: [
{
files: ['**/*.spec.{j,t}s?(x)'],
env: {
'vitest-globals/env': true,
},
},
],
plugins: ['html', 'prettier'], plugins: ['html', 'prettier'],
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
},
rules: { rules: {
'prettier/prettier': ['error'], 'prettier/prettier': ['error'],
camelcase: 'off', camelcase: 'off',
@@ -206,5 +223,11 @@ module.exports = {
globals: { globals: {
bus: true, bus: true,
vi: true, vi: true,
// beforeEach: true,
// afterEach: true,
// test: true,
// describe: true,
// it: true,
// expect: true,
}, },
}; };
+43
View File
@@ -0,0 +1,43 @@
name: Frontend Lint & Test
on:
push:
branches:
- develop
pull_request:
branches:
- develop
jobs:
test:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.ref }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
- uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- uses: pnpm/action-setup@v4
with:
version: 9.3.0
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install pnpm dependencies
run: pnpm install --frozen-lockfile
- name: Run eslint
run: pnpm run eslint
- name: Run frontend tests with coverage
run: |
mkdir -p coverage
pnpm run test:coverage
+39 -36
View File
@@ -21,7 +21,7 @@ jobs:
image: postgres:15.3 image: postgres:15.3
env: env:
POSTGRES_USER: postgres POSTGRES_USER: postgres
POSTGRES_PASSWORD: "" POSTGRES_PASSWORD: ''
POSTGRES_DB: postgres POSTGRES_DB: postgres
POSTGRES_HOST_AUTH_METHOD: trust POSTGRES_HOST_AUTH_METHOD: trust
ports: ports:
@@ -41,46 +41,49 @@ jobs:
options: --entrypoint redis-server options: --entrypoint redis-server
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v4
with: with:
version: 9 version: 9
ref: ${{ github.event.pull_request.head.ref }} ref: ${{ github.event.pull_request.head.ref }}
repository: ${{ github.event.pull_request.head.repo.full_name }} repository: ${{ github.event.pull_request.head.repo.full_name }}
- uses: ruby/setup-ruby@v1 - uses: ruby/setup-ruby@v1
with: with:
bundler-cache: true # runs 'bundle install' and caches installed gems automatically bundler-cache: true # runs 'bundle install' and caches installed gems automatically
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 20 node-version: 20
cache: 'pnpm' cache: 'pnpm'
- name: Install pnpm dependencies - name: Install pnpm dependencies
run: pnpm i run: pnpm i
- name: Strip enterprise code - name: Strip enterprise code
run: | run: |
rm -rf enterprise rm -rf enterprise
rm -rf spec/enterprise rm -rf spec/enterprise
- name: Create database - name: Create database
run: bundle exec rake db:create run: bundle exec rake db:create
- name: Seed database - name: Seed database
run: bundle exec rake db:schema:load run: bundle exec rake db:schema:load
# Run rails tests - name: Run frontend tests
- name: Run backend tests run: pnpm run test:coverage
run: |
bundle exec rspec --profile=10 --format documentation
env:
NODE_OPTIONS: --openssl-legacy-provider
- name: Upload rails log folder # Run rails tests
uses: actions/upload-artifact@v4 - name: Run backend tests
if: always() run: |
with: bundle exec rspec --profile=10 --format documentation
name: rails-log-folder env:
path: log NODE_OPTIONS: --openssl-legacy-provider
- name: Upload rails log folder
uses: actions/upload-artifact@v4
if: always()
with:
name: rails-log-folder
path: log
+7 -3
View File
@@ -49,13 +49,17 @@ jobs:
with: with:
bundler-cache: true # runs 'bundle install' and caches installed gems automatically bundler-cache: true # runs 'bundle install' and caches installed gems automatically
- uses: pnpm/action-setup@v2
with:
version: 9.3.0
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 20 node-version: 20
cache: yarn cache: pnpm
- name: yarn - name: pnpm
run: yarn install run: pnpm install
- name: Create database - name: Create database
run: bundle exec rake db:create run: bundle exec rake db:create
+15 -5
View File
@@ -19,23 +19,33 @@ jobs:
with: with:
bundler-cache: true # runs 'bundle install' and caches installed gems automatically bundler-cache: true # runs 'bundle install' and caches installed gems automatically
- uses: pnpm/action-setup@v2
with:
version: 9.3.0
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 20 node-version: 20
cache: 'yarn' cache: 'pnpm'
- name: yarn - name: pnpm
run: yarn install run: pnpm install
- name: Strip enterprise code - name: Strip enterprise code
run: | run: |
rm -rf enterprise rm -rf enterprise
rm -rf spec/enterprise rm -rf spec/enterprise
- name: setup env
run: |
cp .env.example .env
- name: Run asset compile - name: Run asset compile
run: bundle exec rake assets:precompile run: bundle exec rake assets:precompile
env: env:
NODE_OPTIONS: --openssl-legacy-provider RAILS_ENV: production
- name: Size Check - name: Size Check
run: yarn run size run: pnpm run size
+1 -1
View File
@@ -1 +1 @@
3.9.0 3.13.0
+1 -1
View File
@@ -1 +1 @@
2.8.0 3.0.0
+14 -4
View File
@@ -52,8 +52,11 @@ export default {
currentUser: 'getCurrentUser', currentUser: 'getCurrentUser',
authUIFlags: 'getAuthUIFlags', authUIFlags: 'getAuthUIFlags',
accountUIFlags: 'accounts/getUIFlags', accountUIFlags: 'accounts/getUIFlags',
currentAccountId: 'getCurrentAccountId', // currentAccountId: 'getCurrentAccountId',
}), }),
currentAccountId() {
return this.$route.params.accountId;
},
hasAccounts() { hasAccounts() {
const { accounts = [] } = this.currentUser || {}; const { accounts = [] } = this.currentUser || {};
return accounts.length > 0; return accounts.length > 0;
@@ -70,15 +73,22 @@ export default {
} }
}, },
currentAccountId() { currentAccountId() {
console.log('THIS CALL', this.currentAccountId);
if (this.currentAccountId) { if (this.currentAccountId) {
this.initializeAccount(); this.initializeAccount();
} else {
console.log('NO ACCOUNT ID');
} }
}, },
}, },
mounted() { created() {
this.setLocale(window.chatwootConfig.selectedLocale);
},
async mounted() {
this.setLocale(window.chatwootConfig.selectedLocale);
this.initializeColorTheme(); this.initializeColorTheme();
this.listenToThemeChanges(); this.listenToThemeChanges();
this.setLocale(window.chatwootConfig.selectedLocale); await this.$store.dispatch('accounts/get');
}, },
unmounted() { unmounted() {
if (this.reconnectService) { if (this.reconnectService) {
@@ -94,7 +104,7 @@ export default {
mql.onchange = e => setColorTheme(e.matches); mql.onchange = e => setColorTheme(e.matches);
}, },
setLocale(locale) { setLocale(locale) {
this.$root.$i18n.locale = locale; this.$i18n.locale = locale;
}, },
async initializeAccount() { async initializeAccount() {
await this.$store.dispatch('accounts/get'); await this.$store.dispatch('accounts/get');
@@ -537,12 +537,6 @@ function loadMoreConversations() {
return; return;
} }
// Increment the current page
store.dispatch('conversationPage/setCurrentPage', {
filter: currentPageFilterKey.value,
page: currentFiltersPage.value + 1,
});
if (!hasAppliedFiltersOrActiveFolders.value) { if (!hasAppliedFiltersOrActiveFolders.value) {
fetchConversations(); fetchConversations();
} else if (hasActiveFolders.value) { } else if (hasActiveFolders.value) {
@@ -769,11 +763,11 @@ watch(conversationFilters, (newVal, oldVal) => {
:has-applied-filters="hasAppliedFilters" :has-applied-filters="hasAppliedFilters"
:has-active-folders="hasActiveFolders" :has-active-folders="hasActiveFolders"
:active-status="activeStatus" :active-status="activeStatus"
@addFolders="onClickOpenAddFoldersModal" @add-folders="onClickOpenAddFoldersModal"
@deleteFolders="onClickOpenDeleteFoldersModal" @delete-folders="onClickOpenDeleteFoldersModal"
@filtersModal="onToggleAdvanceFiltersModal" @filters-modal="onToggleAdvanceFiltersModal"
@resetFilters="resetAndFetchData" @reset-filters="resetAndFetchData"
@basicFilterChange="onBasicFilterChange" @basic-filter-change="onBasicFilterChange"
/> />
<AddCustomViews <AddCustomViews
@@ -785,7 +779,7 @@ watch(conversationFilters, (newVal, oldVal) => {
<DeleteCustomViews <DeleteCustomViews
v-if="showDeleteFoldersModal" v-if="showDeleteFoldersModal"
:show-delete-popup.sync="showDeleteFoldersModal" v-model:show="showDeleteFoldersModal"
:active-custom-view="activeFolder" :active-custom-view="activeFolder"
:custom-views-id="foldersId" :custom-views-id="foldersId"
:open-last-item-after-delete="openLastItemAfterDeleteInFolder" :open-last-item-after-delete="openLastItemAfterDeleteInFolder"
@@ -796,7 +790,7 @@ watch(conversationFilters, (newVal, oldVal) => {
v-if="!hasAppliedFiltersOrActiveFolders" v-if="!hasAppliedFiltersOrActiveFolders"
:items="assigneeTabItems" :items="assigneeTabItems"
:active-tab="activeAssigneeTab" :active-tab="activeAssigneeTab"
@chatTabChange="updateAssigneeTab" @chat-tab-change="updateAssigneeTab"
/> />
<p <p
@@ -813,11 +807,11 @@ watch(conversationFilters, (newVal, oldVal) => {
:show-open-action="allSelectedConversationsStatus('open')" :show-open-action="allSelectedConversationsStatus('open')"
:show-resolved-action="allSelectedConversationsStatus('resolved')" :show-resolved-action="allSelectedConversationsStatus('resolved')"
:show-snoozed-action="allSelectedConversationsStatus('snoozed')" :show-snoozed-action="allSelectedConversationsStatus('snoozed')"
@selectAllConversations="toggleSelectAll" @select-all-conversations="toggleSelectAll"
@assignAgent="onAssignAgent" @assign-agent="onAssignAgent"
@updateConversations="onUpdateConversations" @update-conversations="onUpdateConversations"
@assignLabels="onAssignLabels" @assign-labels="onAssignLabels"
@assignTeam="onAssignTeamsForBulk" @assign-team="onAssignTeamsForBulk"
/> />
<div <div
ref="conversationListRef" ref="conversationListRef"
@@ -854,8 +848,8 @@ watch(conversationFilters, (newVal, oldVal) => {
:folders-id="foldersId" :folders-id="foldersId"
:conversation-type="conversationType" :conversation-type="conversationType"
:show-assignee="showAssigneeInConversationCard" :show-assignee="showAssigneeInConversationCard"
@selectConversation="selectConversation" @select-conversation="selectConversation"
@deSelectConversation="deSelectConversation" @de-select-conversation="deSelectConversation"
/> />
</DynamicScrollerItem> </DynamicScrollerItem>
</template> </template>
@@ -878,7 +872,7 @@ watch(conversationFilters, (newVal, oldVal) => {
</DynamicScroller> </DynamicScroller>
</div> </div>
<woot-modal <woot-modal
:show.sync="showAdvancedFilters" v-model:show="showAdvancedFilters"
:on-close="closeAdvanceFiltersModal" :on-close="closeAdvanceFiltersModal"
size="medium" size="medium"
> >
@@ -889,8 +883,8 @@ watch(conversationFilters, (newVal, oldVal) => {
:active-folder-name="activeFolderName" :active-folder-name="activeFolderName"
:on-close="closeAdvanceFiltersModal" :on-close="closeAdvanceFiltersModal"
:is-folder-view="hasActiveFolders" :is-folder-view="hasActiveFolders"
@applyFilter="onApplyFilter" @apply-filter="onApplyFilter"
@updateFolder="onUpdateSavedFilter" @update-folder="onUpdateSavedFilter"
/> />
</woot-modal> </woot-modal>
</div> </div>
@@ -108,7 +108,7 @@ const hasAppliedFiltersOrActiveFolders = computed(() => {
/> />
<ConversationBasicFilter <ConversationBasicFilter
v-if="!hasAppliedFiltersOrActiveFolders" v-if="!hasAppliedFiltersOrActiveFolders"
@changeFilter="onBasicFilterChange" @change-filter="onBasicFilterChange"
/> />
</div> </div>
</div> </div>
@@ -56,14 +56,14 @@ export default {
:selected="isConversationSelected(source.id)" :selected="isConversationSelected(source.id)"
:show-assignee="showAssignee" :show-assignee="showAssignee"
enable-context-menu enable-context-menu
@selectConversation="selectConversation" @select-conversation="selectConversation"
@deSelectConversation="deSelectConversation" @de-select-conversation="deSelectConversation"
@assignAgent="assignAgent" @assign-agent="assignAgent"
@assignTeam="assignTeam" @assign-team="assignTeam"
@assignLabel="assignLabels" @assign-label="assignLabels"
@updateConversationStatus="updateConversationStatus" @update-conversation-status="updateConversationStatus"
@contextMenuToggle="toggleContextMenu" @context-menu-toggle="toggleContextMenu"
@markAsUnread="markAsUnread" @mark-as-unread="markAsUnread"
@assignPriority="assignPriority" @assign-priority="assignPriority"
/> />
</template> </template>
@@ -31,6 +31,7 @@ export default {
attributeKey: { type: String, required: true }, attributeKey: { type: String, required: true },
contactId: { type: Number, default: null }, contactId: { type: Number, default: null },
}, },
emits: ['update', 'delete', 'copy'],
setup() { setup() {
return { v$: useVuelidate() }; return { v$: useVuelidate() };
}, },
@@ -138,7 +139,7 @@ export default {
this.editedValue = this.formattedValue; this.editedValue = this.formattedValue;
emitter.on(BUS_EVENTS.FOCUS_CUSTOM_ATTRIBUTE, this.onFocusAttribute); emitter.on(BUS_EVENTS.FOCUS_CUSTOM_ATTRIBUTE, this.onFocusAttribute);
}, },
destroyed() { unmounted() {
emitter.off(BUS_EVENTS.FOCUS_CUSTOM_ATTRIBUTE, this.onFocusAttribute); emitter.off(BUS_EVENTS.FOCUS_CUSTOM_ATTRIBUTE, this.onFocusAttribute);
}, },
methods: { methods: {
@@ -18,6 +18,7 @@ const shouldShowContent = computed(
); );
</script> </script>
<!-- eslint-disable-next-line vue/no-root-v-if -->
<template> <template>
<div v-if="shouldShowContent"> <div v-if="shouldShowContent">
<slot /> <slot />
@@ -5,6 +5,7 @@ export default {
components: { components: {
DatePicker, DatePicker,
}, },
emits: ['close', 'chooseTime'],
data() { data() {
return { return {
@@ -43,15 +44,18 @@ export default {
<template> <template>
<div class="flex flex-col"> <div class="flex flex-col">
<woot-modal-header :header-title="$t('CONVERSATION.CUSTOM_SNOOZE.TITLE')" /> <woot-modal-header :header-title="$t('CONVERSATION.CUSTOM_SNOOZE.TITLE')" />
<form class="modal-content" @submit.prevent="chooseTime"> <form
class="modal-content w-full pt-2 px-5 pb-6"
@submit.prevent="chooseTime"
>
<DatePicker <DatePicker
v-model:value="snoozeTime" v-model:value="snoozeTime"
type="datetime" type="datetime"
inline inline
input-class="mx-input reset-base"
:lang="lang" :lang="lang"
:disabled-date="disabledDate" :disabled-date="disabledDate"
:disabled-time="disabledTime" :disabled-time="disabledTime"
:popup-style="{ width: '100%' }"
/> />
<div class="flex flex-row justify-end w-full gap-2 px-0 py-2"> <div class="flex flex-row justify-end w-full gap-2 px-0 py-2">
<woot-button variant="clear" @click.prevent="onClose"> <woot-button variant="clear" @click.prevent="onClose">
@@ -64,9 +68,3 @@ export default {
</form> </form>
</div> </div>
</template> </template>
<style lang="scss" scoped>
.modal-content {
@apply pt-2 px-5 pb-6;
}
</style>
@@ -3,9 +3,8 @@
import { ref, computed, defineEmits, onMounted } from 'vue'; import { ref, computed, defineEmits, onMounted } from 'vue';
import { useEventListener } from '@vueuse/core'; import { useEventListener } from '@vueuse/core';
const { show, modalType, closeOnBackdropClick, onClose } = defineProps({ const { modalType, closeOnBackdropClick, onClose } = defineProps({
closeOnBackdropClick: { type: Boolean, default: true }, closeOnBackdropClick: { type: Boolean, default: true },
show: Boolean,
showCloseButton: { type: Boolean, default: true }, showCloseButton: { type: Boolean, default: true },
onClose: { type: Function, required: true }, onClose: { type: Function, required: true },
fullWidth: { type: Boolean, default: false }, fullWidth: { type: Boolean, default: false },
@@ -14,6 +13,7 @@ const { show, modalType, closeOnBackdropClick, onClose } = defineProps({
}); });
const emit = defineEmits(['close']); const emit = defineEmits(['close']);
const show = defineModel('show', { type: Boolean, default: false });
const modalClassName = computed(() => { const modalClassName = computed(() => {
const modalClassNameMap = { const modalClassNameMap = {
@@ -32,6 +32,7 @@ const handleMouseDown = () => {
}; };
const close = () => { const close = () => {
show.value = false;
emit('close'); emit('close');
onClose(); onClose();
}; };
@@ -40,14 +41,14 @@ const onMouseUp = () => {
if (mousedDownOnBackdrop.value) { if (mousedDownOnBackdrop.value) {
mousedDownOnBackdrop.value = false; mousedDownOnBackdrop.value = false;
if (closeOnBackdropClick) { if (closeOnBackdropClick) {
onClose(); close();
} }
} }
}; };
const onKeydown = e => { const onKeydown = e => {
if (show && e.code === 'Escape') { if (show.value && e.code === 'Escape') {
onClose(); close();
e.stopPropagation(); e.stopPropagation();
} }
}; };
@@ -27,14 +27,14 @@ export default {
<div class="flex flex-col items-start px-8 pt-8 pb-0"> <div class="flex flex-col items-start px-8 pt-8 pb-0">
<img v-if="headerImage" :src="headerImage" alt="No image" /> <img v-if="headerImage" :src="headerImage" alt="No image" />
<h2 <h2
ref="modalHeaderTitle" data-test-id="modal-header-title"
class="text-base font-semibold leading-6 text-slate-800 dark:text-slate-50" class="text-base font-semibold leading-6 text-slate-800 dark:text-slate-50"
> >
{{ headerTitle }} {{ headerTitle }}
</h2> </h2>
<p <p
v-if="headerContent" v-if="headerContent"
ref="modalHeaderContent" data-test-id="modal-header-content"
class="w-full mt-2 text-sm leading-5 break-words text-slate-600 dark:text-slate-300" class="w-full mt-2 text-sm leading-5 break-words text-slate-600 dark:text-slate-300"
> >
{{ headerContent }} {{ headerContent }}
@@ -79,6 +79,7 @@ export default {
}; };
</script> </script>
<!-- eslint-disable-next-line vue/no-root-v-if -->
<template> <template>
<Banner <Banner
v-if="shouldShowBanner" v-if="shouldShowBanner"
@@ -86,6 +87,6 @@ export default {
:banner-message="bannerMessage" :banner-message="bannerMessage"
:action-button-label="actionButtonMessage" :action-button-label="actionButtonMessage"
has-action-button has-action-button
@primaryAction="routeToBilling" @primary-action="routeToBilling"
/> />
</template> </template>
@@ -28,6 +28,7 @@ export default {
}; };
</script> </script>
<!-- eslint-disable-next-line vue/no-root-v-if -->
<template> <template>
<Banner <Banner
v-if="shouldShowBanner" v-if="shouldShowBanner"
@@ -36,6 +37,6 @@ export default {
:action-button-label="actionButtonMessage" :action-button-label="actionButtonMessage"
action-button-icon="mail" action-button-icon="mail"
has-action-button has-action-button
@primaryAction="resendVerificationEmail" @primary-action="resendVerificationEmail"
/> />
</template> </template>
@@ -67,6 +67,7 @@ export default {
}; };
</script> </script>
<!-- eslint-disable-next-line vue/no-root-v-if -->
<template> <template>
<Banner <Banner
v-if="shouldShowBanner" v-if="shouldShowBanner"
@@ -81,6 +81,7 @@ export default {
}; };
</script> </script>
<!-- eslint-disable-next-line vue/no-root-v-if -->
<template> <template>
<Banner <Banner
v-if="shouldShowBanner" v-if="shouldShowBanner"
@@ -88,6 +89,6 @@ export default {
:banner-message="bannerMessage" :banner-message="bannerMessage"
:action-button-label="actionButtonMessage" :action-button-label="actionButtonMessage"
has-action-button has-action-button
@primaryAction="routeToBilling" @primary-action="routeToBilling"
/> />
</template> </template>
@@ -105,7 +105,7 @@ export default {
size="small" size="small"
:color-scheme="status.disabled ? '' : 'secondary'" :color-scheme="status.disabled ? '' : 'secondary'"
:variant="status.disabled ? 'smooth' : 'clear'" :variant="status.disabled ? 'smooth' : 'clear'"
class-names="status-change--dropdown-button" class="status-change--dropdown-button"
@click="changeAvailabilityStatus(status.value)" @click="changeAvailabilityStatus(status.value)"
> >
<AvailabilityStatusBadge :status="status.value" /> <AvailabilityStatusBadge :status="status.value" />
@@ -22,11 +22,14 @@ export default {
type: Boolean, type: Boolean,
default: true, default: true,
}, },
hasBanner: {
type: Boolean,
default: false,
},
}, },
emits: [
'toggleAccountModal',
'showAddLabelPopup',
'openNotificationPanel',
'closeKeyShortcutModal',
'openKeyShortcutModal',
],
setup(props, { emit }) { setup(props, { emit }) {
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
@@ -117,6 +120,7 @@ export default {
this.currentUser, this.currentUser,
this.accountId this.accountId
); );
const menuItems = this.sideMenuConfig.primaryMenu; const menuItems = this.sideMenuConfig.primaryMenu;
return menuItems.filter(menuItem => { return menuItems.filter(menuItem => {
const isAvailableForTheUser = hasPermissions( const isAvailableForTheUser = hasPermissions(
@@ -127,6 +131,7 @@ export default {
if (!isAvailableForTheUser) { if (!isAvailableForTheUser) {
return false; return false;
} }
if ( if (
menuItem.alwaysVisibleOnChatwootInstances && menuItem.alwaysVisibleOnChatwootInstances &&
!this.isACustomBrandedInstance !this.isACustomBrandedInstance
@@ -134,10 +139,12 @@ export default {
return true; return true;
} }
if (menuItem.featureFlag) { if (menuItem.featureFlag) {
return this.isFeatureEnabledonAccount( const isEnabled = this.isFeatureEnabledonAccount(
this.accountId, this.accountId,
menuItem.featureFlag menuItem.featureFlag
); );
return isEnabled;
} }
return true; return true;
}); });
@@ -217,9 +224,9 @@ export default {
:account-id="accountId" :account-id="accountId"
:menu-items="primaryMenuItems" :menu-items="primaryMenuItems"
:active-menu-item="activePrimaryMenu.key" :active-menu-item="activePrimaryMenu.key"
@toggleAccounts="toggleAccountModal" @toggle-accounts="toggleAccountModal"
@openKeyShortcutModal="toggleKeyShortcutModal" @open-key-shortcut-modal="toggleKeyShortcutModal"
@openNotificationPanel="openNotificationPanel" @open-notification-panel="openNotificationPanel"
/> />
<SecondarySidebar <SecondarySidebar
v-if="hasSecondarySidebar" v-if="hasSecondarySidebar"
@@ -231,8 +238,8 @@ export default {
:menu-config="activeSecondaryMenu" :menu-config="activeSecondaryMenu"
:current-user="currentUser" :current-user="currentUser"
:is-on-chatwoot-cloud="isOnChatwootCloud" :is-on-chatwoot-cloud="isOnChatwootCloud"
@addLabel="showAddLabelPopup" @add-label="showAddLabelPopup"
@toggleAccounts="toggleAccountModal" @toggle-accounts="toggleAccountModal"
/> />
</aside> </aside>
</template> </template>
@@ -2,6 +2,7 @@
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
export default { export default {
emits: ['toggleAccounts'],
data() { data() {
return { showSwitchButton: false }; return { showSwitchButton: false };
}, },
@@ -25,6 +26,7 @@ export default {
}; };
</script> </script>
<!-- eslint-disable-next-line vue/no-root-v-if -->
<template> <template>
<div <div
v-if="showShowCurrentAccountContext" v-if="showShowCurrentAccountContext"
@@ -7,7 +7,7 @@ export default {
default: true, default: true,
}, },
}, },
emits: ['closeAccountModal', 'showCreateAccountModal'],
computed: { computed: {
...mapGetters({ ...mapGetters({
accountId: 'getCurrentAccountId', accountId: 'getCurrentAccountId',
@@ -15,6 +15,7 @@ export default {
default: true, default: true,
}, },
}, },
emits: ['closeAccountCreateModal'],
setup() { setup() {
return { v$: useVuelidate() }; return { v$: useVuelidate() };
}, },
@@ -6,6 +6,7 @@ export default {
components: { components: {
Thumbnail, Thumbnail,
}, },
emits: ['toggleMenu'],
computed: { computed: {
...mapGetters({ ...mapGetters({
currentUser: 'getCurrentUser', currentUser: 'getCurrentUser',
@@ -2,6 +2,8 @@
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
export default { export default {
emits: ['openNotificationPanel'],
computed: { computed: {
...mapGetters({ ...mapGetters({
notificationMetadata: 'notifications/getMeta', notificationMetadata: 'notifications/getMeta',
@@ -17,6 +17,12 @@ export default {
default: false, default: false,
}, },
}, },
emits: [
'close',
'openKeyShortcutModal',
'toggleAccounts',
'showSupportChatWindow',
],
computed: { computed: {
...mapGetters({ ...mapGetters({
currentUser: 'getCurrentUser', currentUser: 'getCurrentUser',
@@ -43,12 +43,16 @@ export default {
default: '', default: '',
}, },
}, },
emits: ['toggleAccounts', 'openNotificationPanel', 'openKeyShortcutModal'],
data() { data() {
return { return {
helpDocsURL: wootConstants.DOCS_URL, helpDocsURL: wootConstants.DOCS_URL,
showOptionsMenu: false, showOptionsMenu: false,
}; };
}, },
mounted() {
console.log('MENU ITEMS', this.menuItems);
},
methods: { methods: {
frontendURL, frontendURL,
toggleOptions() { toggleOptions() {
@@ -96,13 +100,13 @@ export default {
open-in-new-page open-in-new-page
:to="helpDocsURL" :to="helpDocsURL"
/> />
<NotificationBell @openNotificationPanel="openNotificationPanel" /> <NotificationBell @open-notification-panel="openNotificationPanel" />
<AgentDetails @toggleMenu="toggleOptions" /> <AgentDetails @toggle-menu="toggleOptions" />
<OptionsMenu <OptionsMenu
:show="showOptionsMenu" :show="showOptionsMenu"
@toggleAccounts="toggleAccountModal" @toggle-accounts="toggleAccountModal"
@showSupportChatWindow="toggleSupportChatWindow" @show-support-chat-window="toggleSupportChatWindow"
@openKeyShortcutModal="$emit('openKeyShortcutModal')" @open-key-shortcut-modal="$emit('openKeyShortcutModal')"
@close="toggleOptions" @close="toggleOptions"
/> />
</div> </div>
@@ -26,6 +26,13 @@ export default {
default: false, default: false,
}, },
}, },
mounted() {
console.log(
'MOUNTED: PRIMARY NAV ITEM',
this.name,
this.$t(`SIDEBAR.${this.name}`)
);
},
}; };
</script> </script>
@@ -49,6 +49,7 @@ export default {
default: false, default: false,
}, },
}, },
emits: ['addLabel', 'toggleAccounts'],
computed: { computed: {
...mapGetters({ ...mapGetters({
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount', isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
@@ -242,7 +243,7 @@ export default {
<div <div
class="flex flex-col w-48 h-full px-2 pb-8 overflow-auto text-sm bg-white border-r dark:bg-slate-900 dark:border-slate-800/50 rtl:border-r-0 rtl:border-l border-slate-50" class="flex flex-col w-48 h-full px-2 pb-8 overflow-auto text-sm bg-white border-r dark:bg-slate-900 dark:border-slate-800/50 rtl:border-r-0 rtl:border-l border-slate-50"
> >
<AccountContext @toggleAccounts="toggleAccountModal" /> <AccountContext @toggle-accounts="toggleAccountModal" />
<transition-group <transition-group
name="menu-list" name="menu-list"
tag="ul" tag="ul"
@@ -257,7 +258,7 @@ export default {
v-for="menuItem in additionalSecondaryMenuItems[menuConfig.parentNav]" v-for="menuItem in additionalSecondaryMenuItems[menuConfig.parentNav]"
:key="menuItem.key" :key="menuItem.key"
:menu-item="menuItem" :menu-item="menuItem"
@addLabel="showAddLabelPopup" @add-label="showAddLabelPopup"
/> />
</transition-group> </transition-group>
</div> </div>
@@ -106,10 +106,11 @@ export default {
<span <span
v-if="showChildCount" v-if="showChildCount"
class="bg-slate-50 dark:bg-slate-700 rounded-full min-w-[18px] justify-center items-center flex text-xxs mx-1 py-0 px-1" class="bg-slate-50 dark:bg-slate-700 rounded-full min-w-[18px] justify-center items-center flex text-xxs mx-1 py-0 px-1"
:class="isCountZero :class="
isCountZero
? `text-slate-300 dark:text-slate-500` ? `text-slate-300 dark:text-slate-500`
: `text-slate-700 dark:text-slate-50` : `text-slate-700 dark:text-slate-50`
" "
> >
{{ childItemCount }} {{ childItemCount }}
</span> </span>
@@ -22,6 +22,7 @@ export default {
default: () => ({}), default: () => ({}),
}, },
}, },
emits: ['addLabel', 'open'],
setup() { setup() {
const { isAdmin } = useAdmin(); const { isAdmin } = useAdmin();
const { isEnterprise } = useConfig(); const { isEnterprise } = useConfig();
@@ -1,79 +1,62 @@
import { mount } from '@vue/test-utils';
import { createStore } from 'vuex';
import AccountSelector from '../AccountSelector.vue'; import AccountSelector from '../AccountSelector.vue';
import { createLocalVue, mount } from '@vue/test-utils';
import Vuex from 'vuex';
import VueI18n from 'vue-i18n';
import i18n from 'dashboard/i18n';
import WootModal from 'dashboard/components/Modal.vue'; import WootModal from 'dashboard/components/Modal.vue';
import WootModalHeader from 'dashboard/components/ModalHeader.vue'; import WootModalHeader from 'dashboard/components/ModalHeader.vue';
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue'; import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
const localVue = createLocalVue(); const store = createStore({
localVue.component('woot-modal', WootModal); modules: {
localVue.component('woot-modal-header', WootModalHeader); auth: {
localVue.component('fluent-icon', FluentIcon); namespaced: false,
getters: {
localVue.use(Vuex); getCurrentAccountId: () => 1,
localVue.use(VueI18n); getCurrentUser: () => ({
accounts: [
const i18nConfig = new VueI18n({ { id: 1, name: 'Chatwoot', role: 'administrator' },
locale: 'en', { id: 2, name: 'GitX', role: 'agent' },
messages: i18n, ],
}),
},
},
globalConfig: {
namespaced: true,
getters: {
get: () => ({ createNewAccountFromDashboard: false }),
},
},
},
}); });
describe('accountSelctor', () => { describe('AccountSelector', () => {
let accountSelector = null; let accountSelector = null;
const currentUser = {
accounts: [
{
id: 1,
name: 'Chatwoot',
role: 'administrator',
},
{
id: 2,
name: 'GitX',
role: 'agent',
},
],
};
let actions = null;
let modules = null;
beforeEach(() => { beforeEach(() => {
actions = {};
modules = {
auth: {
getters: {
getCurrentAccountId: () => 1,
getCurrentUser: () => currentUser,
},
},
globalConfig: {
getters: {
'globalConfig/get': () => ({ createNewAccountFromDashboard: false }),
},
},
};
let store = new Vuex.Store({ actions, modules });
accountSelector = mount(AccountSelector, { accountSelector = mount(AccountSelector, {
store, global: {
localVue, plugins: [store],
i18n: i18nConfig, components: {
propsData: { showAccountModal: true }, 'woot-modal': WootModal,
stubs: { WootButton: { template: '<button />' } }, 'woot-modal-header': WootModalHeader,
'fluent-icon': FluentIcon,
},
stubs: {
WootButton: { template: '<button />' },
// override global stub
WootModalHeader: false,
},
},
props: { showAccountModal: true },
}); });
}); });
it('title and sub title exist', () => { it('title and sub title exist', () => {
const headerComponent = accountSelector.findComponent(WootModalHeader); const headerComponent = accountSelector.findComponent(WootModalHeader);
const title = headerComponent.findComponent({ ref: 'modalHeaderTitle' }); const title = headerComponent.find('[data-test-id="modal-header-title"]');
expect(title.text()).toBe('Switch Account'); expect(title.text()).toBe('Switch Account');
const content = headerComponent.findComponent({ const content = headerComponent.find(
ref: 'modalHeaderContent', '[data-test-id="modal-header-content"]'
}); );
expect(content.text()).toBe('Select an account from the following list'); expect(content.text()).toBe('Select an account from the following list');
}); });
@@ -1,27 +1,10 @@
import { shallowMount } from '@vue/test-utils';
import { createStore } from 'vuex';
import AgentDetails from '../AgentDetails.vue'; import AgentDetails from '../AgentDetails.vue';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
import VueI18n from 'vue-i18n';
import i18n from 'dashboard/i18n';
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue'; import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
import WootButton from 'dashboard/components/ui/WootButton.vue'; import WootButton from 'dashboard/components/ui/WootButton.vue';
const localVue = createLocalVue();
localVue.use(Vuex);
localVue.use(VueI18n);
localVue.component('thumbnail', Thumbnail);
localVue.component('woot-button', WootButton);
localVue.component('woot-button', WootButton);
localVue.use(VTooltip, {
defaultHtml: false,
});
const i18nConfig = new VueI18n({ describe('AgentDetails', () => {
locale: 'en',
messages: i18n,
});
describe('agentDetails', () => {
const currentUser = { const currentUser = {
name: 'Neymar Junior', name: 'Neymar Junior',
avatar_url: '', avatar_url: '',
@@ -29,37 +12,46 @@ describe('agentDetails', () => {
}; };
const currentRole = 'agent'; const currentRole = 'agent';
let store = null; let store = null;
let actions = null;
let modules = null;
let agentDetails = null; let agentDetails = null;
beforeEach(() => { const mockTooltipDirective = {
actions = {}; mounted: (el, binding) => {
// You can mock the behavior here if necessary
el.setAttribute('data-tooltip', binding.value || '');
},
};
modules = { beforeEach(() => {
auth: { store = createStore({
getters: { modules: {
getCurrentUser: () => currentUser, auth: {
getCurrentRole: () => currentRole, namespaced: false,
getCurrentUserAvailability: () => currentUser.availability_status, getters: {
getCurrentUser: () => currentUser,
getCurrentRole: () => currentRole,
getCurrentUserAvailability: () => currentUser.availability_status,
},
}, },
}, },
};
store = new Vuex.Store({
actions,
modules,
}); });
agentDetails = shallowMount(AgentDetails, { agentDetails = shallowMount(AgentDetails, {
store, global: {
localVue, plugins: [store],
i18n: i18nConfig, components: {
Thumbnail,
WootButton,
},
directives: {
tooltip: mockTooltipDirective, // Mocking the tooltip directive
},
stubs: { WootButton: { template: '<button><slot /></button>' } },
},
}); });
}); });
it(' the agent status', () => { it('shows the correct agent status', () => {
expect(agentDetails.find('thumbnail-stub').vm.status).toBe('online'); expect(agentDetails.findComponent(Thumbnail).vm.status).toBe('online');
}); });
it('agent thumbnail exists', () => { it('agent thumbnail exists', () => {
@@ -1,20 +1,7 @@
import NotificationBell from '../NotificationBell.vue'; import { shallowMount } from '@vue/test-utils';
import { createLocalVue, shallowMount } from '@vue/test-utils'; import { createStore } from 'vuex';
import Vuex from 'vuex';
import VueI18n from 'vue-i18n';
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue'; import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
import NotificationBell from '../NotificationBell.vue';
import i18n from 'dashboard/i18n';
const localVue = createLocalVue();
localVue.use(Vuex);
localVue.use(VueI18n);
localVue.component('fluent-icon', FluentIcon);
const i18nConfig = new VueI18n({
locale: 'en',
messages: i18n,
});
const $route = { const $route = {
name: 'notifications_index', name: 'notifications_index',
@@ -33,43 +20,51 @@ describe('notificationBell', () => {
}; };
modules = { modules = {
auth: { auth: {
namespaced: false,
getters: { getters: {
getCurrentAccountId: () => accountId, getCurrentAccountId: () => accountId,
}, },
}, },
notifications: { notifications: {
namespaced: false,
getters: { getters: {
'notifications/getMeta': () => notificationMetadata, 'notifications/getMeta': () => notificationMetadata,
}, },
}, },
}; };
store = new Vuex.Store({ store = createStore({
actions, actions,
modules, modules,
}); });
}); });
it('it should return unread count 19 ', () => { it('it should return unread count 19', () => {
const wrapper = shallowMount(NotificationBell, { const wrapper = shallowMount(NotificationBell, {
localVue, global: {
i18n: i18nConfig, plugins: [store],
store, mocks: {
mocks: { $route,
$route, },
components: {
'fluent-icon': FluentIcon,
},
}, },
}); });
expect(wrapper.vm.unreadCount).toBe('19'); expect(wrapper.vm.unreadCount).toBe('19');
}); });
it('it should return unread count 99+ ', async () => { it('it should return unread count 99+', async () => {
notificationMetadata.unreadCount = 100; notificationMetadata.unreadCount = 100;
const wrapper = shallowMount(NotificationBell, { const wrapper = shallowMount(NotificationBell, {
localVue, global: {
i18n: i18nConfig, plugins: [store],
store, mocks: {
mocks: { $route,
$route, },
components: {
'fluent-icon': FluentIcon,
},
}, },
}); });
expect(wrapper.vm.unreadCount).toBe('99+'); expect(wrapper.vm.unreadCount).toBe('99+');
@@ -77,11 +72,14 @@ describe('notificationBell', () => {
it('isNotificationPanelActive', async () => { it('isNotificationPanelActive', async () => {
const notificationBell = shallowMount(NotificationBell, { const notificationBell = shallowMount(NotificationBell, {
store, global: {
localVue, plugins: [store],
i18n: i18nConfig, mocks: {
mocks: { $route,
$route, },
components: {
'fluent-icon': FluentIcon,
},
}, },
}); });
@@ -1,9 +1,6 @@
import { mount } from '@vue/test-utils';
import { createStore } from 'vuex';
import AvailabilityStatus from '../AvailabilityStatus.vue'; import AvailabilityStatus from '../AvailabilityStatus.vue';
import { createLocalVue, mount } from '@vue/test-utils';
import Vuex from 'vuex';
import VueI18n from 'vue-i18n';
import FloatingVue from 'floating-vue';
import WootButton from 'dashboard/components/ui/WootButton.vue'; import WootButton from 'dashboard/components/ui/WootButton.vue';
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue'; import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue'; import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
@@ -11,70 +8,64 @@ import WootDropdownHeader from 'shared/components/ui/dropdown/DropdownHeader.vue
import WootDropdownDivider from 'shared/components/ui/dropdown/DropdownDivider.vue'; import WootDropdownDivider from 'shared/components/ui/dropdown/DropdownDivider.vue';
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue'; import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
import i18n from 'dashboard/i18n';
const localVue = createLocalVue();
localVue.use(FloatingVue, {
html: false,
});
localVue.use(Vuex);
localVue.use(VueI18n);
localVue.component('woot-button', WootButton);
localVue.component('woot-dropdown-header', WootDropdownHeader);
localVue.component('woot-dropdown-menu', WootDropdownMenu);
localVue.component('woot-dropdown-divider', WootDropdownDivider);
localVue.component('woot-dropdown-item', WootDropdownItem);
localVue.component('fluent-icon', FluentIcon);
const i18nConfig = new VueI18n({ locale: 'en', messages: i18n });
describe('AvailabilityStatus', () => { describe('AvailabilityStatus', () => {
const currentAvailability = 'online'; const currentAvailability = 'online';
const currentAccountId = '1'; const currentAccountId = '1';
const currentUserAutoOffline = false; const currentUserAutoOffline = false;
let store = null; let store = null;
let actions = null; let actions = null;
let modules = null;
let availabilityStatus = null;
beforeEach(() => { beforeEach(() => {
actions = { actions = {
updateAvailability: vi.fn(() => { updateAvailability: vi.fn(() => Promise.resolve()),
return Promise.resolve();
}),
}; };
modules = { store = createStore({
auth: { modules: {
getters: { auth: {
getCurrentUserAvailability: () => currentAvailability, namespaced: false,
getCurrentAccountId: () => currentAccountId, getters: {
getCurrentUserAutoOffline: () => currentUserAutoOffline, getCurrentUserAvailability: () => currentAvailability,
getCurrentAccountId: () => currentAccountId,
getCurrentUserAutoOffline: () => currentUserAutoOffline,
},
}, },
}, },
}; actions,
store = new Vuex.Store({ actions, modules });
availabilityStatus = mount(AvailabilityStatus, {
store,
localVue,
i18n: i18nConfig,
stubs: { WootSwitch: { template: '<button />' } },
}); });
}); });
it('dispatches an action when user changes status', async () => { it('dispatches an action when user changes status', async () => {
await availabilityStatus; const wrapper = mount(AvailabilityStatus, {
availabilityStatus global: {
.findAll('.status-change--dropdown-button') plugins: [store],
.at(2) components: {
.trigger('click'); WootButton,
WootDropdownItem,
WootDropdownMenu,
WootDropdownHeader,
WootDropdownDivider,
FluentIcon,
},
stubs: {
WootSwitch: { template: '<button />' },
},
},
});
expect(actions.updateAvailability).toBeCalledWith( // Ensure that the dropdown menu is opened
expect.any(Object), await wrapper.vm.openStatusMenu();
{ availability: 'offline', account_id: currentAccountId },
undefined // Simulate the user clicking the 3rd button (offline status)
); const buttons = wrapper.findAll('.status-change--dropdown-button');
expect(buttons.length).toBeGreaterThan(0); // Ensure buttons exist
await buttons[2].trigger('click');
expect(actions.updateAvailability).toHaveBeenCalledTimes(1);
expect(actions.updateAvailability.mock.calls[0][1]).toEqual({
availability: 'offline',
account_id: currentAccountId,
});
}); });
}); });
@@ -7,5 +7,8 @@ exports[`SidemenuIcon > matches snapshot 1`] = `
icon="list" icon="list"
size="small" size="small"
variant="clear" variant="clear"
/> >
</button>
`; `;
@@ -18,6 +18,7 @@ export default {
default: '', default: '',
}, },
}, },
emits: ['open', 'close'],
methods: { methods: {
onClickOpenPath() { onClickOpenPath() {
this.$emit('open'); this.$emit('open');
@@ -38,6 +38,7 @@ export default {
default: false, default: false,
}, },
}, },
emits: ['primaryAction', 'close'],
computed: { computed: {
bannerClasses() { bannerClasses() {
const classList = [this.colorScheme]; const classList = [this.colorScheme];
@@ -222,7 +222,7 @@ const emitDateRange = () => {
> >
<CalendarDateRange <CalendarDateRange
:selected-range="selectedRange" :selected-range="selectedRange"
@setRange="setDateRange" @set-range="setDateRange"
/> />
<div <div
class="flex flex-col w-[680px] ltr:border-l rtl:border-r border-slate-50 dark:border-slate-700/50" class="flex flex-col w-[680px] ltr:border-l rtl:border-r border-slate-50 dark:border-slate-700/50"
@@ -264,15 +264,15 @@ const emitDateRange = () => {
:calendar-type="calendar" :calendar-type="calendar"
:start-current-date="startCurrentDate" :start-current-date="startCurrentDate"
:end-current-date="endCurrentDate" :end-current-date="endCurrentDate"
@selectYear="openCalendar($event, calendar, YEAR)" @select-year="openCalendar($event, calendar, YEAR)"
/> />
<CalendarMonth <CalendarMonth
v-else-if="calendarViews[calendar] === MONTH" v-else-if="calendarViews[calendar] === MONTH"
:calendar-type="calendar" :calendar-type="calendar"
:start-current-date="startCurrentDate" :start-current-date="startCurrentDate"
:end-current-date="endCurrentDate" :end-current-date="endCurrentDate"
@selectMonth="openCalendar($event, calendar)" @select-month="openCalendar($event, calendar)"
@setView="setViewMode" @set-view="setViewMode"
@prev="moveCalendar(calendar, 'prev', YEAR)" @prev="moveCalendar(calendar, 'prev', YEAR)"
@next="moveCalendar(calendar, 'next', YEAR)" @next="moveCalendar(calendar, 'next', YEAR)"
/> />
@@ -286,9 +286,9 @@ const emitDateRange = () => {
:selected-end-date="selectedEndDate" :selected-end-date="selectedEndDate"
:selecting-end-date="selectingEndDate" :selecting-end-date="selectingEndDate"
:hovered-end-date="hoveredEndDate" :hovered-end-date="hoveredEndDate"
@updateHoveredEndDate="hoveredEndDate = $event" @update-hovered-end-date="hoveredEndDate = $event"
@selectDate="selectDate" @select-date="selectDate"
@setView="setViewMode" @set-view="setViewMode"
@prev="moveCalendar(calendar, 'prev')" @prev="moveCalendar(calendar, 'prev')"
@next="moveCalendar(calendar, 'next')" @next="moveCalendar(calendar, 'next')"
/> />
@@ -1,5 +1,5 @@
<script setup> <script setup>
const emit = defineEmits(['clear', 'clear']); const emit = defineEmits(['clear', 'change']);
const onClickClear = () => { const onClickClear = () => {
emit('clear'); emit('clear');
@@ -62,7 +62,7 @@ const selectMonth = index => {
MONTH MONTH
) )
" "
@setView="setViewMode" @set-view="setViewMode"
@prev="onClickPrev" @prev="onClickPrev"
@next="onClickNext" @next="onClickNext"
/> />
@@ -139,7 +139,7 @@ const dayClasses = day => ({
" "
@prev="onClickPrev" @prev="onClickPrev"
@next="onClickNext" @next="onClickNext"
@setView="setViewMode" @set-view="setViewMode"
/> />
<CalendarWeekLabel /> <CalendarWeekLabel />
<div <div
@@ -16,6 +16,7 @@ export default {
default: () => [], default: () => [],
}, },
}, },
emits: ['change'],
methods: { methods: {
handleChange(value) { handleChange(value) {
this.$emit('change', value); this.$emit('change', value);
@@ -17,6 +17,7 @@ export default {
default: [], default: [],
}, },
}, },
emits: ['change'],
methods: { methods: {
handleChange(value) { handleChange(value) {
@@ -38,7 +38,7 @@ const props = defineProps({
}, },
}); });
const emit = defineEmits(['onSearch', 'select']); const emit = defineEmits(['onSearch', 'select', 'removeFilter']);
const searchTerm = ref(''); const searchTerm = ref('');
@@ -84,10 +84,10 @@ const shouldShowEmptyState = computed(() => {
<slot name="search"> <slot name="search">
<DropdownSearch <DropdownSearch
v-if="enableSearch" v-if="enableSearch"
:input-value="searchTerm" v-model="searchTerm"
:input-placeholder="inputPlaceholder" :input-placeholder="inputPlaceholder"
:show-clear-filter="showClearFilter" :show-clear-filter="showClearFilter"
@input="onSearch" @update:model-value="onSearch"
@remove="$emit('removeFilter')" @remove="$emit('removeFilter')"
/> />
</slot> </slot>
@@ -1,10 +1,6 @@
<script setup> <script setup>
import { defineEmits } from 'vue'; import { defineEmits, defineModel } from 'vue';
defineProps({ defineProps({
inputValue: {
type: String,
default: '',
},
inputPlaceholder: { inputPlaceholder: {
type: String, type: String,
default: '', default: '',
@@ -15,7 +11,12 @@ defineProps({
}, },
}); });
const emit = defineEmits(['input', 'remove']); const emit = defineEmits(['remove']);
const value = defineModel({
type: String,
default: '',
});
</script> </script>
<template> <template>
@@ -29,16 +30,15 @@ const emit = defineEmits(['input', 'remove']);
class="text-slate-400 dark:text-slate-400 flex-shrink-0" class="text-slate-400 dark:text-slate-400 flex-shrink-0"
/> />
<input <input
v-model="value"
:placeholder="inputPlaceholder"
type="text" type="text"
class="w-full mb-0 text-sm bg-white dark:bg-slate-800 text-slate-800 dark:text-slate-75 reset-base" class="w-full mb-0 text-sm bg-white dark:bg-slate-800 text-slate-800 dark:text-slate-75 reset-base"
:placeholder="inputPlaceholder"
:value="inputValue"
@input="emit('input', $event.target.value)"
/> />
</div> </div>
<!-- Clear filter button --> <!-- Clear filter button -->
<woot-button <woot-button
v-if="!inputValue && showClearFilter" v-if="!modelValue && showClearFilter"
size="small" size="small"
variant="clear" variant="clear"
color-scheme="primary" color-scheme="primary"
@@ -44,6 +44,7 @@ export default {
default: '', default: '',
}, },
}, },
emits: ['remove'],
computed: { computed: {
textColor() { textColor() {
if (this.variant === 'smooth') return ''; if (this.variant === 'smooth') return '';
@@ -4,6 +4,7 @@ export default {
modelValue: { type: Boolean, default: false }, modelValue: { type: Boolean, default: false },
size: { type: String, default: '' }, size: { type: String, default: '' },
}, },
emits: ['update:modelValue', 'input'],
methods: { methods: {
onClick() { onClick() {
this.$emit('update:modelValue', !this.modelValue); this.$emit('update:modelValue', !this.modelValue);
@@ -17,6 +17,7 @@ export default {
AICTAModal, AICTAModal,
AIAssistanceCTAButton, AIAssistanceCTAButton,
}, },
emits: ['replaceText'],
setup(props, { emit }) { setup(props, { emit }) {
const { uiSettings, updateUISettings } = useUISettings(); const { uiSettings, updateUISettings } = useUISettings();
@@ -137,19 +138,19 @@ export default {
@click="openAIAssist" @click="openAIAssist"
/> />
<woot-modal <woot-modal
:show.sync="showAIAssistanceModal" v-model:show="showAIAssistanceModal"
:on-close="hideAIAssistanceModal" :on-close="hideAIAssistanceModal"
> >
<AIAssistanceModal <AIAssistanceModal
:ai-option="aiOption" :ai-option="aiOption"
@applyText="insertText" @apply-text="insertText"
@close="hideAIAssistanceModal" @close="hideAIAssistanceModal"
/> />
</woot-modal> </woot-modal>
</div> </div>
<div v-else-if="shouldShowAIAssistCTAButtonForAdmin" class="relative"> <div v-else-if="shouldShowAIAssistCTAButtonForAdmin" class="relative">
<AIAssistanceCTAButton @click="openAICta" /> <AIAssistanceCTAButton @click="openAICta" />
<woot-modal :show.sync="showAICtaModal" :on-close="hideAICtaModal"> <woot-modal v-model:show="showAICtaModal" :on-close="hideAICtaModal">
<AICTAModal @close="hideAICtaModal" /> <AICTAModal @close="hideAICtaModal" />
</woot-modal> </woot-modal>
</div> </div>
@@ -1,5 +1,7 @@
<script> <script>
export default { export default {
emits: ['open'],
methods: { methods: {
onClick() { onClick() {
this.$emit('open'); this.$emit('open');
@@ -13,6 +13,7 @@ export default {
required: true, required: true,
}, },
}, },
emits: ['close', 'applyText'],
setup() { setup() {
const { formatMessage } = useMessageFormatter(); const { formatMessage } = useMessageFormatter();
const { draftMessage, processEvent, recordAnalytics } = useAI(); const { draftMessage, processEvent, recordAnalytics } = useAI();
@@ -7,6 +7,8 @@ import { useAI } from 'dashboard/composables/useAI';
import { OPEN_AI_EVENTS } from 'dashboard/helper/AnalyticsHelper/events'; import { OPEN_AI_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
export default { export default {
emits: ['close'],
setup() { setup() {
const { updateUISettings } = useUISettings(); const { updateUISettings } = useUISettings();
const { recordAnalytics } = useAI(); const { recordAnalytics } = useAI();
@@ -38,6 +38,7 @@ export default {
default: false, default: false,
}, },
}, },
emits: ['update:modelValue', 'input', 'removeAction', 'resetAction'],
computed: { computed: {
action_name: { action_name: {
get() { get() {
@@ -1,8 +1,10 @@
<script> <script>
export default { export default {
// The value types are dynamic, hence prop validation removed to work with our action schema props: {
// eslint-disable-next-line vue/require-prop-types teams: { type: Array, required: true },
props: ['teams', 'value'], modelValue: { type: Object, required: true },
},
emits: ['update:modelValue'],
data() { data() {
return { return {
selectedTeams: [], selectedTeams: [],
@@ -10,13 +12,13 @@ export default {
}; };
}, },
mounted() { mounted() {
const { team_ids: teamIds } = this.value; const { team_ids: teamIds } = this.modelValue;
this.selectedTeams = teamIds; this.selectedTeams = teamIds;
this.message = this.value.message; this.message = this.modelValue.message;
}, },
methods: { methods: {
updateValue() { updateValue() {
this.$emit('input', { this.$emit('update:modelValue', {
team_ids: this.selectedTeams.map(team => team.id), team_ids: this.selectedTeams.map(team => team.id),
message: this.message, message: this.message,
}); });
@@ -11,6 +11,7 @@ export default {
default: '', default: '',
}, },
}, },
emits: ['update:modelValue'],
data() { data() {
return { return {
uploadState: 'idle', uploadState: 'idle',
@@ -12,6 +12,7 @@ export default {
required: true, required: true,
}, },
}, },
emits: ['channelItemClick'],
computed: { computed: {
hasFbConfigured() { hasFbConfigured() {
return window.chatwootConfig?.fbAppId; return window.chatwootConfig?.fbAppId;
@@ -11,6 +11,7 @@ export default {
default: '', default: '',
}, },
}, },
emits: ['update:modelValue'],
data() { data() {
return { return {
isPickerOpen: false, isPickerOpen: false,
@@ -45,7 +46,7 @@ export default {
disable-alpha disable-alpha
:model-value="modelValue" :model-value="modelValue"
class="colorpicker--chrome" class="colorpicker--chrome"
@update:modelValue="updateColor" @update:model-value="updateColor"
/> />
</div> </div>
</template> </template>
@@ -80,6 +80,7 @@ export default {
}; };
</script> </script>
<!-- eslint-disable-next-line vue/no-root-v-if -->
<template> <template>
<div v-if="hasOpenedAtleastOnce" class="dashboard-app--container"> <div v-if="hasOpenedAtleastOnce" class="dashboard-app--container">
<div <div
@@ -19,6 +19,7 @@ export default {
}; };
</script> </script>
<!-- eslint-disable-next-line vue/no-root-v-if -->
<template> <template>
<div v-if="isFeatureEnabled"> <div v-if="isFeatureEnabled">
<slot /> <slot />
@@ -47,6 +47,7 @@ export default {
default: '', default: '',
}, },
}, },
emits: ['update:modelValue', 'removeFilter', 'resetFilter'],
computed: { computed: {
attributeKey: { attributeKey: {
get() { get() {
@@ -5,7 +5,7 @@ export default {
props: { props: {
inbox: { inbox: {
type: Object, type: Object,
default: () => { }, default: () => {},
}, },
}, },
computed: { computed: {
@@ -16,6 +16,8 @@ const props = defineProps({
default: 0, default: 0,
}, },
}); });
const emit = defineEmits(['pageChange']);
const totalPages = computed(() => Math.ceil(props.totalCount / props.pageSize)); const totalPages = computed(() => Math.ceil(props.totalCount / props.pageSize));
const firstIndex = computed(() => props.pageSize * (props.currentPage - 1) + 1); const firstIndex = computed(() => props.pageSize * (props.currentPage - 1) + 1);
const lastIndex = computed(() => const lastIndex = computed(() =>
@@ -26,6 +28,7 @@ const isFooterVisible = computed(
); );
</script> </script>
<!-- eslint-disable-next-line vue/no-root-v-if -->
<template> <template>
<footer <footer
v-if="isFooterVisible" v-if="isFooterVisible"
@@ -42,7 +45,7 @@ const isFooterVisible = computed(
:total-pages="totalPages" :total-pages="totalPages"
:total-count="totalCount" :total-count="totalCount"
:page-size="pageSize" :page-size="pageSize"
@pageChange="$emit('pageChange', $event)" @page-change="emit('pageChange', $event)"
/> />
</footer> </footer>
</template> </template>
@@ -41,6 +41,7 @@ export default {
}; };
</script> </script>
<!-- eslint-disable-next-line vue/no-root-v-if -->
<template> <template>
<woot-button <woot-button
v-if="isVideoIntegrationEnabled" v-if="isVideoIntegrationEnabled"
@@ -13,7 +13,12 @@ const props = defineProps({
}, },
}); });
const emit = defineEmits(['recorderProgressChanged', 'finishRecord']); const emit = defineEmits([
'recorderProgressChanged',
'finishRecord',
'pause',
'play',
]);
const waveformContainer = ref(null); const waveformContainer = ref(null);
const wavesurfer = ref(null); const wavesurfer = ref(null);
@@ -47,6 +52,9 @@ const initWaveSurfer = () => {
], ],
}); });
wavesurfer.value.on('pause', () => emit('pause'));
wavesurfer.value.on('play', () => emit('play'));
record.value = wavesurfer.value.plugins[0]; record.value = wavesurfer.value.plugins[0];
wavesurfer.value.on('finish', () => { wavesurfer.value.on('finish', () => {
@@ -106,11 +114,9 @@ onUnmounted(() => {
} }
}); });
defineExpose({ playPause, stopRecording }); defineExpose({ playPause, stopRecording, record });
</script> </script>
<template> <template>
<div class="w-full"> <div ref="waveformContainer" class="w-full p-1" />
<div ref="waveformContainer" />
</div>
</template> </template>
@@ -176,7 +176,7 @@ const editorMenuOptions = computed(() => {
function createSuggestionPlugin({ function createSuggestionPlugin({
trigger, trigger,
minChars = 1, minChars = 0,
showMenu, showMenu,
searchTerm, searchTerm,
isAllowed = () => true, isAllowed = () => true,
@@ -47,6 +47,7 @@ export default {
placeholder: { type: String, default: '' }, placeholder: { type: String, default: '' },
enabledMenuOptions: { type: Array, default: () => [] }, enabledMenuOptions: { type: Array, default: () => [] },
}, },
emits: ['blur', 'input', 'update:modelValue', 'keyup', 'focus', 'keydown'],
setup() { setup() {
const { uiSettings, updateUISettings } = useUISettings(); const { uiSettings, updateUISettings } = useUISettings();
@@ -60,17 +61,9 @@ export default {
plugins: [imagePastePlugin(this.handleImageUpload)], plugins: [imagePastePlugin(this.handleImageUpload)],
}; };
}, },
computed: {
contentFromEditor() {
if (editorView) {
return ArticleMarkdownSerializer.serialize(editorView.state.doc);
}
return '';
},
},
watch: { watch: {
modelValue(newValue = '') { modelValue(newValue = '') {
if (newValue !== this.contentFromEditor) { if (newValue !== this.contentFromEditor()) {
this.reloadState(); this.reloadState();
} }
}, },
@@ -95,6 +88,12 @@ export default {
this.focusEditorInputField(); this.focusEditorInputField();
}, },
methods: { methods: {
contentFromEditor() {
if (editorView) {
return ArticleMarkdownSerializer.serialize(editorView.state.doc);
}
return '';
},
openFileBrowser() { openFileBrowser() {
this.$refs.imageUploadInput.click(); this.$refs.imageUploadInput.click();
}, },
@@ -212,8 +211,8 @@ export default {
editorView.focus(); editorView.focus();
}, },
emitOnChange() { emitOnChange() {
this.$emit('update:modelValue', this.contentFromEditor); this.$emit('update:modelValue', this.contentFromEditor());
this.$emit('input', this.contentFromEditor); this.$emit('input', this.contentFromEditor());
}, },
onKeyup() { onKeyup() {
this.$emit('keyup'); this.$emit('keyup');
@@ -1,4 +1,5 @@
<script> <script>
import { ref } from 'vue';
import { useUISettings } from 'dashboard/composables/useUISettings'; import { useUISettings } from 'dashboard/composables/useUISettings';
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents'; import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
import FileUpload from 'vue-upload-component'; import FileUpload from 'vue-upload-component';
@@ -26,7 +27,7 @@ export default {
}, },
onSend: { onSend: {
type: Function, type: Function,
default: () => { }, default: () => {},
}, },
sendButtonText: { sendButtonText: {
type: String, type: String,
@@ -34,7 +35,7 @@ export default {
}, },
recordingAudioDurationText: { recordingAudioDurationText: {
type: String, type: String,
default: '', default: '00:00',
}, },
// inbox prop is used in /mixins/inboxMixin, // inbox prop is used in /mixins/inboxMixin,
// remove this props when refactoring to composable if not needed // remove this props when refactoring to composable if not needed
@@ -53,19 +54,19 @@ export default {
}, },
onFileUpload: { onFileUpload: {
type: Function, type: Function,
default: () => { }, default: () => {},
}, },
toggleEmojiPicker: { toggleEmojiPicker: {
type: Function, type: Function,
default: () => { }, default: () => {},
}, },
toggleAudioRecorder: { toggleAudioRecorder: {
type: Function, type: Function,
default: () => { }, default: () => {},
}, },
toggleAudioRecorderPlayPause: { toggleAudioRecorderPlayPause: {
type: Function, type: Function,
default: () => { }, default: () => {},
}, },
isRecordingAudio: { isRecordingAudio: {
type: Boolean, type: Boolean,
@@ -112,10 +113,18 @@ export default {
required: true, required: true,
}, },
}, },
emits: [
'replaceText',
'toggleInsertArticle',
'toggleEditor',
'selectWhatsappTemplate',
],
setup() { setup() {
const { setSignatureFlagForInbox, fetchSignatureFlagFromUISettings } = const { setSignatureFlagForInbox, fetchSignatureFlagFromUISettings } =
useUISettings(); useUISettings();
const uploadRef = ref(false);
const keyboardEvents = { const keyboardEvents = {
'Alt+KeyA': { 'Alt+KeyA': {
action: () => { action: () => {
@@ -137,6 +146,7 @@ export default {
return { return {
setSignatureFlagForInbox, setSignatureFlagForInbox,
fetchSignatureFlagFromUISettings, fetchSignatureFlagFromUISettings,
uploadRef,
}; };
}, },
computed: { computed: {
@@ -166,9 +176,9 @@ export default {
return false; return false;
} }
// Disable audio recorder for safari browser as recording is not supported // Disable audio recorder for safari browser as recording is not supported
const isSafari = /^((?!chrome|android|crios|fxios).)*safari/i.test( // const isSafari = /^((?!chrome|android|crios|fxios).)*safari/i.test(
navigator.userAgent // navigator.userAgent
); // );
return ( return (
this.isFeatureEnabledonAccount( this.isFeatureEnabledonAccount(
@@ -249,13 +259,13 @@ export default {
v-tooltip.top-end="$t('CONVERSATION.REPLYBOX.TIP_EMOJI_ICON')" v-tooltip.top-end="$t('CONVERSATION.REPLYBOX.TIP_EMOJI_ICON')"
:title="$t('CONVERSATION.REPLYBOX.TIP_EMOJI_ICON')" :title="$t('CONVERSATION.REPLYBOX.TIP_EMOJI_ICON')"
icon="emoji" icon="emoji"
emoji="😊"
color-scheme="secondary" color-scheme="secondary"
variant="smooth" variant="smooth"
size="small" size="small"
@click="toggleEmojiPicker" @click="toggleEmojiPicker"
/> />
<FileUpload <FileUpload
ref="uploadRef"
v-tooltip.top-end="$t('CONVERSATION.REPLYBOX.TIP_ATTACH_ICON')" v-tooltip.top-end="$t('CONVERSATION.REPLYBOX.TIP_ATTACH_ICON')"
input-id="conversationAttachment" input-id="conversationAttachment"
:size="4096 * 4096" :size="4096 * 4096"
@@ -274,7 +284,6 @@ export default {
class-names="button--upload" class-names="button--upload"
:title="$t('CONVERSATION.REPLYBOX.TIP_ATTACH_ICON')" :title="$t('CONVERSATION.REPLYBOX.TIP_ATTACH_ICON')"
icon="attach" icon="attach"
emoji="📎"
color-scheme="secondary" color-scheme="secondary"
variant="smooth" variant="smooth"
size="small" size="small"
@@ -284,7 +293,6 @@ export default {
v-if="showAudioRecorderButton" v-if="showAudioRecorderButton"
v-tooltip.top-end="$t('CONVERSATION.REPLYBOX.TIP_AUDIORECORDER_ICON')" v-tooltip.top-end="$t('CONVERSATION.REPLYBOX.TIP_AUDIORECORDER_ICON')"
:icon="!isRecordingAudio ? 'microphone' : 'microphone-off'" :icon="!isRecordingAudio ? 'microphone' : 'microphone-off'"
emoji="🎤"
:color-scheme="!isRecordingAudio ? 'secondary' : 'alert'" :color-scheme="!isRecordingAudio ? 'secondary' : 'alert'"
variant="smooth" variant="smooth"
size="small" size="small"
@@ -294,7 +302,6 @@ export default {
v-if="showEditorToggle" v-if="showEditorToggle"
v-tooltip.top-end="$t('CONVERSATION.REPLYBOX.TIP_FORMAT_ICON')" v-tooltip.top-end="$t('CONVERSATION.REPLYBOX.TIP_FORMAT_ICON')"
icon="quote" icon="quote"
emoji="🖊️"
color-scheme="secondary" color-scheme="secondary"
variant="smooth" variant="smooth"
size="small" size="small"
@@ -303,7 +310,6 @@ export default {
<woot-button <woot-button
v-if="showAudioPlayStopButton" v-if="showAudioPlayStopButton"
:icon="audioRecorderPlayStopIcon" :icon="audioRecorderPlayStopIcon"
emoji="🎤"
color-scheme="secondary" color-scheme="secondary"
variant="smooth" variant="smooth"
size="small" size="small"
@@ -340,11 +346,11 @@ export default {
:conversation-id="conversationId" :conversation-id="conversationId"
:is-private-note="isOnPrivateNote" :is-private-note="isOnPrivateNote"
:message="message" :message="message"
@replaceText="replaceText" @replace-text="replaceText"
/> />
<transition name="modal-fade"> <transition name="modal-fade">
<div <div
v-show="$refs.uploadRef && $refs.uploadRef.dropActive" v-show="uploadRef && uploadRef.dropActive"
class="fixed top-0 bottom-0 left-0 right-0 z-20 flex flex-col items-center justify-center w-full h-full gap-2 text-slate-900 dark:text-slate-50 bg-modal-backdrop-light dark:bg-modal-backdrop-dark" class="fixed top-0 bottom-0 left-0 right-0 z-20 flex flex-col items-center justify-center w-full h-full gap-2 text-slate-900 dark:text-slate-50 bg-modal-backdrop-light dark:bg-modal-backdrop-dark"
> >
<fluent-icon icon="cloud-backup" size="40" /> <fluent-icon icon="cloud-backup" size="40" />
@@ -22,6 +22,7 @@ export default {
default: false, default: false,
}, },
}, },
emits: ['setReplyMode', 'togglePopout'],
setup(props, { emit }) { setup(props, { emit }) {
const setReplyMode = mode => { const setReplyMode = mode => {
emit('setReplyMode', mode); emit('setReplyMode', mode);
@@ -48,7 +48,7 @@ onMounted(() => {
v-if="items.length" v-if="items.length"
type="emoji" type="emoji"
:items="items" :items="items"
@mentionSelect="handleMentionClick" @mention-select="handleMentionClick"
> >
<template #default="{ item, selected }"> <template #default="{ item, selected }">
<span <span
@@ -1,6 +1,7 @@
import lamejs from '@breezystack/lamejs'; import lamejs from '@breezystack/lamejs';
const writeString = (view, offset, string) => { const writeString = (view, offset, string) => {
// eslint-disable-next-line no-plusplus
for (let i = 0; i < string.length; i++) { for (let i = 0; i < string.length; i++) {
view.setUint8(offset + i, string.charCodeAt(i)); view.setUint8(offset + i, string.charCodeAt(i));
} }
@@ -28,7 +29,9 @@ const bufferToWav = async (buffer, numChannels, sampleRate) => {
// WAV Data // WAV Data
const offset = 44; const offset = 44;
// eslint-disable-next-line no-plusplus
for (let i = 0; i < buffer.length; i++) { for (let i = 0; i < buffer.length; i++) {
// eslint-disable-next-line no-plusplus
for (let channel = 0; channel < numChannels; channel++) { for (let channel = 0; channel < numChannels; channel++) {
const sample = Math.max( const sample = Math.max(
-1, -1,
@@ -10,6 +10,7 @@ export default {
default: '', default: '',
}, },
}, },
emits: ['replace'],
computed: { computed: {
...mapGetters({ ...mapGetters({
cannedMessages: 'getCannedResponses', cannedMessages: 'getCannedResponses',
@@ -46,6 +47,6 @@ export default {
<MentionBox <MentionBox
v-if="items.length" v-if="items.length"
:items="items" :items="items"
@mentionSelect="handleMentionClick" @mention-select="handleMentionClick"
/> />
</template> </template>
@@ -37,6 +37,7 @@ export default {
default: false, default: false,
}, },
}, },
emits: ['applyFilter', 'updateFolder'],
setup() { setup() {
const { setFilterAttributes } = useFilter({ const { setFilterAttributes } = useFilter({
filteri18nKey: 'FILTER', filteri18nKey: 'FILTER',
@@ -363,8 +364,8 @@ export default {
? $t(`CONTACTS_FILTER.ERRORS.VALUE_REQUIRED`) ? $t(`CONTACTS_FILTER.ERRORS.VALUE_REQUIRED`)
: '' : ''
" "
@resetFilter="resetFilter(i, appliedFilters[i])" @reset-filter="resetFilter(i, appliedFilters[i])"
@removeFilter="removeFilter(i)" @remove-filter="removeFilter(i)"
/> />
<div class="mt-4"> <div class="mt-4">
<woot-button <woot-button
@@ -27,6 +27,7 @@ export default {
components: { components: {
FilterItem, FilterItem,
}, },
emits: ['changeFilter'],
setup() { setup() {
const { updateUISettings } = useUISettings(); const { updateUISettings } = useUISettings();
@@ -107,7 +108,7 @@ export default {
:selected-value="chatStatus" :selected-value="chatStatus"
:items="chatStatusItems" :items="chatStatusItems"
path-prefix="CHAT_LIST.CHAT_STATUS_FILTER_ITEMS" path-prefix="CHAT_LIST.CHAT_STATUS_FILTER_ITEMS"
@onChangeFilter="onChangeFilter" @on-change-filter="onChangeFilter"
/> />
</div> </div>
<div class="flex items-center justify-between last:mt-4"> <div class="flex items-center justify-between last:mt-4">
@@ -119,7 +120,7 @@ export default {
:selected-value="sortFilter" :selected-value="sortFilter"
:items="chatSortItems" :items="chatSortItems"
path-prefix="CHAT_LIST.SORT_ORDER_ITEMS" path-prefix="CHAT_LIST.SORT_ORDER_ITEMS"
@onChangeFilter="onChangeFilter" @on-change-filter="onChangeFilter"
/> />
</div> </div>
</div> </div>
@@ -34,6 +34,7 @@ export default {
default: true, default: true,
}, },
}, },
emits: ['contactPanelToggle'],
data() { data() {
return { activeIndex: 0 }; return { activeIndex: 0 };
}, },
@@ -103,7 +104,7 @@ export default {
:is-inbox-view="isInboxView" :is-inbox-view="isInboxView"
:is-contact-panel-open="isContactPanelOpen" :is-contact-panel-open="isContactPanelOpen"
:show-back-button="isOnExpandedLayout && !isInboxView" :show-back-button="isOnExpandedLayout && !isInboxView"
@contactPanelToggle="onToggleContactPanel" @contact-panel-toggle="onToggleContactPanel"
/> />
<woot-tabs <woot-tabs
v-if="dashboardApps.length && currentChat.id" v-if="dashboardApps.length && currentChat.id"
@@ -128,7 +129,7 @@ export default {
:inbox-id="inboxId" :inbox-id="inboxId"
:is-inbox-view="isInboxView" :is-inbox-view="isInboxView"
:is-contact-panel-open="isContactPanelOpen" :is-contact-panel-open="isContactPanelOpen"
@contactPanelToggle="onToggleContactPanel" @contact-panel-toggle="onToggleContactPanel"
/> />
<EmptyState <EmptyState
v-if="!currentChat.id && !isInboxView" v-if="!currentChat.id && !isInboxView"
@@ -69,6 +69,15 @@ export default {
default: false, default: false,
}, },
}, },
emits: [
'contextMenuToggle',
'assignAgent',
'assignLabel',
'assignTeam',
'markAsUnread',
'assignPriority',
'updateConversationStatus',
],
data() { data() {
return { return {
hovered: false, hovered: false,
@@ -336,12 +345,12 @@ export default {
:priority="chat.priority" :priority="chat.priority"
:chat-id="chat.id" :chat-id="chat.id"
:has-unread-messages="hasUnread" :has-unread-messages="hasUnread"
@updateConversation="onUpdateConversation" @update-conversation="onUpdateConversation"
@assignAgent="onAssignAgent" @assign-agent="onAssignAgent"
@assignLabel="onAssignLabel" @assign-label="onAssignLabel"
@assignTeam="onAssignTeam" @assign-team="onAssignTeam"
@markAsUnread="markAsUnread" @mark-as-unread="markAsUnread"
@assignPriority="assignPriority" @assign-priority="assignPriority"
/> />
</ContextMenu> </ContextMenu>
</div> </div>
@@ -41,6 +41,7 @@ export default {
default: false, default: false,
}, },
}, },
emits: ['contactPanelToggle'],
setup(props, { emit }) { setup(props, { emit }) {
const keyboardEvents = { const keyboardEvents = {
'Alt+KeyO': { 'Alt+KeyO': {
@@ -2,6 +2,7 @@
import { useVuelidate } from '@vuelidate/core'; import { useVuelidate } from '@vuelidate/core';
import { required, minLength, email } from '@vuelidate/validators'; import { required, minLength, email } from '@vuelidate/validators';
import { useAlert } from 'dashboard/composables'; import { useAlert } from 'dashboard/composables';
export default { export default {
props: { props: {
show: { show: {
@@ -13,6 +14,7 @@ export default {
default: () => ({}), default: () => ({}),
}, },
}, },
emits: ['cancel', 'update:show'],
setup() { setup() {
return { v$: useVuelidate() }; return { v$: useVuelidate() };
}, },
@@ -31,6 +33,14 @@ export default {
}, },
}, },
computed: { computed: {
localShow: {
get() {
return this.show;
},
set(value) {
this.$emit('update:show', value);
},
},
sentToOtherEmailAddress() { sentToOtherEmailAddress() {
return this.selectedType === 'other_email_address'; return this.selectedType === 'other_email_address';
}, },
@@ -80,9 +90,8 @@ export default {
}; };
</script> </script>
<!-- eslint-disable vue/no-mutating-props -->
<template> <template>
<woot-modal :show.sync="show" :on-close="onCancel"> <woot-modal v-model:show="localShow" :on-close="onCancel">
<div class="flex flex-col h-auto overflow-auto"> <div class="flex flex-col h-auto overflow-auto">
<woot-modal-header <woot-modal-header
:header-title="$t('EMAIL_TRANSCRIPT.TITLE')" :header-title="$t('EMAIL_TRANSCRIPT.TITLE')"
@@ -18,6 +18,7 @@ export default {
required: true, required: true,
}, },
}, },
emits: ['onChangeFilter'],
data() { data() {
return { return {
activeValue: this.selectedValue, activeValue: this.selectedValue,
@@ -439,6 +439,7 @@ export default {
}; };
</script> </script>
<!-- eslint-disable-next-line vue/no-root-v-if -->
<template> <template>
<li <li
v-if="shouldRenderMessage" v-if="shouldRenderMessage"
@@ -579,7 +580,7 @@ export default {
:message="data" :message="data"
@open="openContextMenu" @open="openContextMenu"
@close="closeContextMenu" @close="closeContextMenu"
@replyTo="handleReplyTo" @reply-to="handleReplyTo"
/> />
</div> </div>
</li> </li>
@@ -52,6 +52,7 @@ export default {
default: false, default: false,
}, },
}, },
emits: ['contactPanelToggle'],
setup() { setup() {
const isPopOutReplyBox = ref(false); const isPopOutReplyBox = ref(false);
const { isEnterprise } = useConfig(); const { isEnterprise } = useConfig();
@@ -477,6 +478,7 @@ export default {
</div> </div>
<ul class="conversation-panel"> <ul class="conversation-panel">
<transition name="slide-up"> <transition name="slide-up">
<!-- eslint-disable-next-line vue/require-toggle-inside-transition -->
<li class="min-h-[4rem]"> <li class="min-h-[4rem]">
<span v-if="shouldShowSpinner" class="spinner message" /> <span v-if="shouldShowSpinner" class="spinner message" />
</li> </li>
@@ -547,9 +549,9 @@ export default {
</div> </div>
</div> </div>
<ReplyBox <ReplyBox
v-model:popout-reply-box="isPopOutReplyBox"
:conversation-id="currentChat.id" :conversation-id="currentChat.id"
:popout-reply-box.sync="isPopOutReplyBox" @toggle-popout="showPopOutReplyBox"
@togglePopout="showPopOutReplyBox"
/> />
</div> </div>
</div> </div>
@@ -28,7 +28,7 @@ export default {
emitter.on(CMD_UNMUTE_CONVERSATION, this.unmute); emitter.on(CMD_UNMUTE_CONVERSATION, this.unmute);
emitter.on(CMD_SEND_TRANSCRIPT, this.toggleEmailActionsModal); emitter.on(CMD_SEND_TRANSCRIPT, this.toggleEmailActionsModal);
}, },
destroyed() { unmounted() {
emitter.off(CMD_MUTE_CONVERSATION, this.mute); emitter.off(CMD_MUTE_CONVERSATION, this.mute);
emitter.off(CMD_UNMUTE_CONVERSATION, this.unmute); emitter.off(CMD_UNMUTE_CONVERSATION, this.unmute);
emitter.off(CMD_SEND_TRANSCRIPT, this.toggleEmailActionsModal); emitter.off(CMD_SEND_TRANSCRIPT, this.toggleEmailActionsModal);
@@ -29,6 +29,7 @@ export default {
}; };
</script> </script>
<!-- eslint-disable-next-line vue/no-root-v-if -->
<template> <template>
<span <span
v-if="priority" v-if="priority"
@@ -1,5 +1,6 @@
<script> <script>
import { defineAsyncComponent } from 'vue'; // [TODO] The popout events are needlessly complex and should be simplified
import { defineAsyncComponent, defineModel } from 'vue';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { useAlert } from 'dashboard/composables'; import { useAlert } from 'dashboard/composables';
import { useUISettings } from 'dashboard/composables/useUISettings'; import { useUISettings } from 'dashboard/composables/useUISettings';
@@ -18,7 +19,7 @@ import MessageSignatureMissingAlert from './MessageSignatureMissingAlert.vue';
import Banner from 'dashboard/components/ui/Banner.vue'; import Banner from 'dashboard/components/ui/Banner.vue';
import { REPLY_EDITOR_MODES } from 'dashboard/components/widgets/WootWriter/constants'; import { REPLY_EDITOR_MODES } from 'dashboard/components/widgets/WootWriter/constants';
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue'; import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
import WootAudioRecorder from 'dashboard/components/widgets/WootWriter/AudioRecorder.vue'; import AudioRecorder from 'dashboard/components/widgets/WootWriter/AudioRecorder.vue';
import { AUDIO_FORMATS } from 'shared/constants/messages'; import { AUDIO_FORMATS } from 'shared/constants/messages';
import { BUS_EVENTS } from 'shared/constants/busEvents'; import { BUS_EVENTS } from 'shared/constants/busEvents';
import { import {
@@ -49,28 +50,23 @@ const EmojiInput = defineAsyncComponent(
export default { export default {
components: { components: {
EmojiInput,
CannedResponse,
ReplyToMessage,
ResizableTextArea,
AttachmentPreview,
ReplyTopPanel,
ReplyEmailHead,
ReplyBottomPanel,
WootMessageEditor,
WootAudioRecorder,
Banner,
WhatsappTemplates,
MessageSignatureMissingAlert,
ArticleSearchPopover, ArticleSearchPopover,
AttachmentPreview,
AudioRecorder,
Banner,
CannedResponse,
EmojiInput,
MessageSignatureMissingAlert,
ReplyBottomPanel,
ReplyEmailHead,
ReplyToMessage,
ReplyTopPanel,
ResizableTextArea,
WhatsappTemplates,
WootMessageEditor,
}, },
mixins: [inboxMixin, fileUploadMixin, keyboardEventListenerMixins], mixins: [inboxMixin, fileUploadMixin, keyboardEventListenerMixins],
props: { emits: ['update:popoutReplyBox', 'togglePopout'],
popoutReplyBox: {
type: Boolean,
default: false,
},
},
setup() { setup() {
const { const {
uiSettings, uiSettings,
@@ -79,8 +75,14 @@ export default {
fetchSignatureFlagFromUISettings, fetchSignatureFlagFromUISettings,
} = useUISettings(); } = useUISettings();
const popoutReplyBox = defineModel('popoutReplyBox', {
type: Boolean,
default: false,
});
return { return {
uiSettings, uiSettings,
popoutReplyBox,
updateUISettings, updateUISettings,
isEditorHotKeyEnabled, isEditorHotKeyEnabled,
fetchSignatureFlagFromUISettings, fetchSignatureFlagFromUISettings,
@@ -113,6 +115,7 @@ export default {
showVariablesMenu: false, showVariablesMenu: false,
newConversationModalActive: false, newConversationModalActive: false,
showArticleSearchPopover: false, showArticleSearchPopover: false,
hasRecordedAudio: false,
}; };
}, },
computed: { computed: {
@@ -277,12 +280,6 @@ export default {
hasAttachments() { hasAttachments() {
return this.attachedFiles.length; return this.attachedFiles.length;
}, },
hasRecordedAudio() {
return (
this.$refs.audioRecorderInput &&
this.$refs.audioRecorderInput.hasAudio()
);
},
isRichEditorEnabled() { isRichEditorEnabled() {
return this.isAWebWidgetInbox || this.isAnEmailChannel; return this.isAWebWidgetInbox || this.isAnEmailChannel;
}, },
@@ -362,7 +359,7 @@ export default {
return AUDIO_FORMATS.MP3; return AUDIO_FORMATS.MP3;
} }
if (this.isAPIInbox) { if (this.isAPIInbox) {
return AUDIO_FORMATS.OGG; return AUDIO_FORMATS.MP3;
} }
return AUDIO_FORMATS.WAV; return AUDIO_FORMATS.WAV;
}, },
@@ -463,12 +460,10 @@ export default {
this.onNewConversationModalActive this.onNewConversationModalActive
); );
}, },
destroyed() { unmounted() {
document.removeEventListener('paste', this.onPaste); document.removeEventListener('paste', this.onPaste);
document.removeEventListener('keydown', this.handleKeyEvents); document.removeEventListener('keydown', this.handleKeyEvents);
emitter.off(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE, this.fetchAndSetReplyTo); emitter.off(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE, this.fetchAndSetReplyTo);
},
unmounted() {
emitter.off( emitter.off(
BUS_EVENTS.NEW_CONVERSATION_MODAL, BUS_EVENTS.NEW_CONVERSATION_MODAL,
this.onNewConversationModalActive this.onNewConversationModalActive
@@ -803,19 +798,14 @@ export default {
this.attachedFiles = []; this.attachedFiles = [];
this.isRecordingAudio = false; this.isRecordingAudio = false;
this.resetReplyToMessage(); this.resetReplyToMessage();
this.resetAudioRecorderInput();
}, },
clearEmailField() { clearEmailField() {
this.ccEmails = ''; this.ccEmails = '';
this.bccEmails = ''; this.bccEmails = '';
this.toEmails = ''; this.toEmails = '';
}, },
clearRecorder() {
this.isRecordingAudio = false;
// Only clear the recorded audio when we click toggle button.
this.attachedFiles = this.attachedFiles.filter(
file => !file?.isRecordedAudio
);
},
toggleEmojiPicker() { toggleEmojiPicker() {
this.showEmojiPicker = !this.showEmojiPicker; this.showEmojiPicker = !this.showEmojiPicker;
}, },
@@ -823,17 +813,18 @@ export default {
this.isRecordingAudio = !this.isRecordingAudio; this.isRecordingAudio = !this.isRecordingAudio;
this.isRecorderAudioStopped = !this.isRecordingAudio; this.isRecorderAudioStopped = !this.isRecordingAudio;
if (!this.isRecordingAudio) { if (!this.isRecordingAudio) {
this.clearRecorder(); this.resetAudioRecorderInput();
} }
}, },
toggleAudioRecorderPlayPause() { toggleAudioRecorderPlayPause() {
if (this.isRecordingAudio) { if (!this.isRecordingAudio) {
if (!this.isRecorderAudioStopped) { return;
this.isRecorderAudioStopped = true; }
this.$refs.audioRecorderInput.stopRecording(); if (!this.isRecorderAudioStopped) {
} else if (this.isRecorderAudioStopped) { this.isRecorderAudioStopped = true;
this.$refs.audioRecorderInput.playPause(); this.$refs.audioRecorderInput.stopRecording();
} } else if (this.isRecorderAudioStopped) {
this.$refs.audioRecorderInput.playPause();
} }
}, },
hideEmojiPicker() { hideEmojiPicker() {
@@ -860,13 +851,9 @@ export default {
onRecordProgressChanged(duration) { onRecordProgressChanged(duration) {
this.recordingAudioDurationText = duration; this.recordingAudioDurationText = duration;
}, },
onStateRecorderChanged(state) {
this.recordingAudioState = state;
if (state && 'notallowederror'.includes(state)) {
this.toggleAudioRecorder();
}
},
onFinishRecorder(file) { onFinishRecorder(file) {
this.recordingAudioState = 'stopped';
this.hasRecordedAudio = true;
// Added a new key isRecordedAudio to the file to find it's and recorded audio // Added a new key isRecordedAudio to the file to find it's and recorded audio
// Because to filter and show only non recorded audio and other attachments // Because to filter and show only non recorded audio and other attachments
const autoRecordedFile = { const autoRecordedFile = {
@@ -1067,6 +1054,16 @@ export default {
toggleInsertArticle() { toggleInsertArticle() {
this.showArticleSearchPopover = !this.showArticleSearchPopover; this.showArticleSearchPopover = !this.showArticleSearchPopover;
}, },
resetAudioRecorderInput() {
this.recordingAudioDurationText = '00:00';
this.isRecordingAudio = false;
this.recordingAudioState = '';
this.hasRecordedAudio = false;
// Only clear the recorded audio when we click toggle button.
this.attachedFiles = this.attachedFiles.filter(
file => !file?.isRecordedAudio
);
},
}, },
}; };
</script> </script>
@@ -1081,15 +1078,15 @@ export default {
:banner-message="$t('CONVERSATION.NOT_ASSIGNED_TO_YOU')" :banner-message="$t('CONVERSATION.NOT_ASSIGNED_TO_YOU')"
has-action-button has-action-button
:action-button-label="$t('CONVERSATION.ASSIGN_TO_ME')" :action-button-label="$t('CONVERSATION.ASSIGN_TO_ME')"
@primaryAction="onClickSelfAssign" @primary-action="onClickSelfAssign"
/> />
<ReplyTopPanel <ReplyTopPanel
:mode="replyType" :mode="replyType"
:is-message-length-reaching-threshold="isMessageLengthReachingThreshold" :is-message-length-reaching-threshold="isMessageLengthReachingThreshold"
:characters-remaining="charactersRemaining" :characters-remaining="charactersRemaining"
:popout-reply-box="popoutReplyBox" :popout-reply-box="popoutReplyBox"
@setReplyMode="setReplyMode" @set-reply-mode="setReplyMode"
@togglePopout="$emit('togglePopout')" @toggle-popout="$emit('togglePopout')"
/> />
<ArticleSearchPopover <ArticleSearchPopover
v-if="showArticleSearchPopover && connectedPortalSlug" v-if="showArticleSearchPopover && connectedPortalSlug"
@@ -1122,13 +1119,14 @@ export default {
v-model:bcc-emails="bccEmails" v-model:bcc-emails="bccEmails"
v-model:to-emails="toEmails" v-model:to-emails="toEmails"
/> />
<WootAudioRecorder <AudioRecorder
v-if="showAudioRecorderEditor" v-if="showAudioRecorderEditor"
ref="audioRecorderInput" ref="audioRecorderInput"
:audio-record-format="audioRecordFormat" :audio-record-format="audioRecordFormat"
@recorderProgressChanged="onRecordProgressChanged" @recorder-progress-changed="onRecordProgressChanged"
@stateRecorderChanged="onStateRecorderChanged" @finish-record="onFinishRecorder"
@finishRecord="onFinishRecorder" @play="recordingAudioState = 'playing'"
@pause="recordingAudioState = 'paused'"
/> />
<ResizableTextArea <ResizableTextArea
v-else-if="!showRichContentEditor" v-else-if="!showRichContentEditor"
@@ -1140,8 +1138,8 @@ export default {
:signature="signatureToApply" :signature="signatureToApply"
allow-signature allow-signature
:send-with-signature="sendWithSignature" :send-with-signature="sendWithSignature"
@typingOff="onTypingOff" @typing-off="onTypingOff"
@typingOn="onTypingOn" @typing-on="onTypingOn"
@focus="onFocus" @focus="onFocus"
@blur="onBlur" @blur="onBlur"
/> />
@@ -1159,21 +1157,25 @@ export default {
:signature="signatureToApply" :signature="signatureToApply"
allow-signature allow-signature
:channel-type="channelType" :channel-type="channelType"
@typingOff="onTypingOff" @typing-off="onTypingOff"
@typingOn="onTypingOn" @typing-on="onTypingOn"
@focus="onFocus" @focus="onFocus"
@blur="onBlur" @blur="onBlur"
@toggleUserMention="toggleUserMention" @toggle-user-mention="toggleUserMention"
@toggleCannedMenu="toggleCannedMenu" @toggle-canned-menu="toggleCannedMenu"
@toggleVariablesMenu="toggleVariablesMenu" @toggle-variables-menu="toggleVariablesMenu"
@clearSelection="clearEditorSelection" @clear-selection="clearEditorSelection"
/> />
</div> </div>
<div v-if="hasAttachments" class="attachment-preview-box" @paste="onPaste"> <div
v-if="hasAttachments && !showAudioRecorderEditor"
class="attachment-preview-box"
@paste="onPaste"
>
<AttachmentPreview <AttachmentPreview
class="flex-col mt-4" class="flex-col mt-4"
:attachments="attachedFiles" :attachments="attachedFiles"
@removeAttachment="removeAttachment" @remove-attachment="removeAttachment"
/> />
</div> </div>
<MessageSignatureMissingAlert <MessageSignatureMissingAlert
@@ -1203,16 +1205,16 @@ export default {
:message="message" :message="message"
:portal-slug="connectedPortalSlug" :portal-slug="connectedPortalSlug"
:new-conversation-modal-active="newConversationModalActive" :new-conversation-modal-active="newConversationModalActive"
@selectWhatsappTemplate="openWhatsappTemplateModal" @select-whatsapp-template="openWhatsappTemplateModal"
@toggleEditor="toggleRichContentEditor" @toggle-editor="toggleRichContentEditor"
@replaceText="replaceText" @replace-text="replaceText"
@toggleInsertArticle="toggleInsertArticle" @toggle-insert-article="toggleInsertArticle"
/> />
<WhatsappTemplates <WhatsappTemplates
:inbox-id="inbox.id" :inbox-id="inbox.id"
:show="showWhatsAppTemplatesModal" :show="showWhatsAppTemplatesModal"
@close="hideWhatsappTemplatesModal" @close="hideWhatsappTemplatesModal"
@onSend="onSendWhatsAppReply" @on-send="onSendWhatsAppReply"
@cancel="hideWhatsappTemplatesModal" @cancel="hideWhatsappTemplatesModal"
/> />
@@ -17,6 +17,7 @@ export default {
default: '', default: '',
}, },
}, },
emits: ['update:bccEmails', 'update:ccEmails', 'update:toEmails'],
setup() { setup() {
return { v$: useVuelidate() }; return { v$: useVuelidate() };
}, },
@@ -9,6 +9,7 @@ export default {
required: true, required: true,
}, },
}, },
emits: ['dismiss'],
}; };
</script> </script>
@@ -11,6 +11,7 @@ export default {
default: '', default: '',
}, },
}, },
emits: ['selectVariable'],
computed: { computed: {
...mapGetters({ ...mapGetters({
customAttributes: 'attributes/getAttributes', customAttributes: 'attributes/getAttributes',
@@ -62,7 +63,7 @@ export default {
v-if="items.length" v-if="items.length"
type="variable" type="variable"
:items="items" :items="items"
@mentionSelect="handleVariableClick" @mention-select="handleVariableClick"
/> />
</template> </template>
@@ -7,21 +7,30 @@ export default {
TemplateParser, TemplateParser,
}, },
props: { props: {
show: {
type: Boolean,
default: false,
},
inboxId: { inboxId: {
type: Number, type: Number,
default: undefined, default: undefined,
}, },
show: {
type: Boolean,
default: true,
},
}, },
emits: ['onSend', 'cancel', 'update:show'],
data() { data() {
return { return {
selectedWaTemplate: null, selectedWaTemplate: null,
}; };
}, },
computed: { computed: {
localShow: {
get() {
return this.show;
},
set(value) {
this.$emit('update:show', value);
},
},
modalHeaderContent() { modalHeaderContent() {
return this.selectedWaTemplate return this.selectedWaTemplate
? this.$t('WHATSAPP_TEMPLATES.MODAL.TEMPLATE_SELECTED_SUBTITLE', { ? this.$t('WHATSAPP_TEMPLATES.MODAL.TEMPLATE_SELECTED_SUBTITLE', {
@@ -47,9 +56,8 @@ export default {
}; };
</script> </script>
<!-- eslint-disable vue/no-mutating-props -->
<template> <template>
<woot-modal :show.sync="show" :on-close="onClose" size="modal-big"> <woot-modal v-model:show="localShow" :on-close="onClose" size="modal-big">
<woot-modal-header <woot-modal-header
:header-title="$t('WHATSAPP_TEMPLATES.MODAL.TITLE')" :header-title="$t('WHATSAPP_TEMPLATES.MODAL.TITLE')"
:header-content="modalHeaderContent" :header-content="modalHeaderContent"
@@ -58,13 +66,13 @@ export default {
<TemplatesPicker <TemplatesPicker
v-if="!selectedWaTemplate" v-if="!selectedWaTemplate"
:inbox-id="inboxId" :inbox-id="inboxId"
@onSelect="pickTemplate" @on-select="pickTemplate"
/> />
<TemplateParser <TemplateParser
v-else v-else
:template="selectedWaTemplate" :template="selectedWaTemplate"
@resetTemplate="onResetTemplate" @reset-template="onResetTemplate"
@sendMessage="onSendMessage" @send-message="onSendMessage"
/> />
</div> </div>
</woot-modal> </woot-modal>
@@ -19,6 +19,7 @@ export default {
default: () => ({}), default: () => ({}),
}, },
}, },
emits: ['sendMessage', 'resetTemplate'],
setup(props, { emit }) { setup(props, { emit }) {
const processVariable = str => { const processVariable = str => {
return str.replace(/{{|}}/g, ''); return str.replace(/{{|}}/g, '');
@@ -9,6 +9,7 @@ export default {
default: undefined, default: undefined,
}, },
}, },
emits: ['onSelect'],
data() { data() {
return { return {
query: '', query: '',
@@ -7,6 +7,7 @@ export default {
required: true, required: true,
}, },
}, },
emits: ['error'],
data() { data() {
return { return {
show: false, show: false,
@@ -26,7 +27,7 @@ export default {
<template> <template>
<div class="image message-text__wrap"> <div class="image message-text__wrap">
<img :src="url" @click="onClick" @error="$emit('error')" /> <img :src="url" @click="onClick" @error="$emit('error')" />
<woot-modal full-width :show.sync="show" :on-close="onClose"> <woot-modal v-model:show="show" full-width :on-close="onClose">
<img :src="url" class="modal-image skip-context-menu" /> <img :src="url" class="modal-image skip-context-menu" />
</woot-modal> </woot-modal>
</div> </div>
@@ -21,6 +21,7 @@ export default {
required: true, required: true,
}, },
}, },
emits: ['error'],
data() { data() {
return { return {
show: false, show: false,
@@ -116,7 +117,7 @@ export default {
</audio> </audio>
<GalleryView <GalleryView
v-if="show" v-if="show"
:show.sync="show" v-model:show="show"
:attachment="attachment" :attachment="attachment"
:all-attachments="filteredCurrentChatAttachments" :all-attachments="filteredCurrentChatAttachments"
@error="onImgError" @error="onImgError"
@@ -15,6 +15,7 @@ export default {
default: '', default: '',
}, },
}, },
emits: ['error'],
data() { data() {
return { return {
hasImgStoryError: false, hasImgStoryError: false,
@@ -31,6 +31,7 @@ export default {
}; };
</script> </script>
<!-- eslint-disable-next-line vue/no-root-v-if -->
<template> <template>
<DyteVideoCall <DyteVideoCall
v-if="showDyteIntegration" v-if="showDyteIntegration"
@@ -45,6 +45,7 @@ export default {
}; };
</script> </script>
<!-- eslint-disable-next-line vue/no-root-v-if -->
<template> <template>
<div <div
v-if="showHead" v-if="showHead"
@@ -106,7 +106,7 @@ export default {
</button> </button>
<GalleryView <GalleryView
v-if="showGalleryViewer" v-if="showGalleryViewer"
:show.sync="showGalleryViewer" v-model:show="showGalleryViewer"
:attachment="attachment" :attachment="attachment"
:all-attachments="availableAttachments" :all-attachments="availableAttachments"
@error="onClose" @error="onClose"

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