Compare commits

..
60 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
Shivam MishraandGitHub b52950ba5a fix: Update event name for reports filter (#10199) 2024-10-02 09:16:23 -07:00
PranavandGitHub a3b602290a feat: Upgrade vite to 5.4.8 to fix dependabot/186 (#10196)
Reference: https://github.com/chatwoot/chatwoot/pull/10195
2024-10-02 00:57:37 -07:00
Shivam MishraandGitHub fce4d5e26e feat: Show re-auth flag only for legacy Gmail and OAuth channels (#10189) 2024-10-02 00:51:02 -07:00
42f6621afb feat: Vite + vue 3 💚 (#10047)
Fixes https://github.com/chatwoot/chatwoot/issues/8436
Fixes https://github.com/chatwoot/chatwoot/issues/9767
Fixes https://github.com/chatwoot/chatwoot/issues/10156
Fixes https://github.com/chatwoot/chatwoot/issues/6031
Fixes https://github.com/chatwoot/chatwoot/issues/5696
Fixes https://github.com/chatwoot/chatwoot/issues/9250
Fixes https://github.com/chatwoot/chatwoot/issues/9762

---------

Co-authored-by: Pranav <pranavrajs@gmail.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2024-10-02 00:36:30 -07:00
1682 changed files with 23542 additions and 38919 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/**/*'
+18 -13
View File
@@ -2,16 +2,22 @@ module.exports = {
extends: [ extends: [
'airbnb-base/legacy', 'airbnb-base/legacy',
'prettier', 'prettier',
'plugin:vue/recommended', 'plugin:vue/vue3-recommended',
'plugin:storybook/recommended', 'plugin:vitest-globals/recommended',
'plugin:cypress/recommended',
], ],
overrides: [
{
files: ['**/*.spec.{j,t}s?(x)'],
env: {
'vitest-globals/env': true,
},
},
],
plugins: ['html', 'prettier'],
parserOptions: { parserOptions: {
parser: '@babel/eslint-parser', ecmaVersion: 'latest',
ecmaVersion: 2020,
sourceType: 'module', sourceType: 'module',
}, },
plugins: ['html', 'prettier', 'babel'],
rules: { rules: {
'prettier/prettier': ['error'], 'prettier/prettier': ['error'],
camelcase: 'off', camelcase: 'off',
@@ -210,13 +216,6 @@ module.exports = {
'import/extensions': ['off'], 'import/extensions': ['off'],
'no-console': 'error', 'no-console': 'error',
}, },
settings: {
'import/resolver': {
webpack: {
config: 'config/webpack/resolve.js',
},
},
},
env: { env: {
browser: true, browser: true,
node: true, node: true,
@@ -224,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
+38 -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,47 +41,49 @@ jobs:
options: --entrypoint redis-server options: --entrypoint redis-server
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: - uses: pnpm/action-setup@v4
ref: ${{ github.event.pull_request.head.ref }} with:
repository: ${{ github.event.pull_request.head.repo.full_name }} version: 9
ref: ${{ github.event.pull_request.head.ref }}
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: yarn cache: 'pnpm'
- name: yarn - name: Install pnpm dependencies
run: yarn install 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
- name: yarn check-files - name: Run frontend tests
run: yarn install --check-files run: pnpm run test:coverage
# Run rails tests # Run rails tests
- name: Run backend tests - name: Run backend tests
run: | run: |
bundle exec rspec --profile=10 --format documentation bundle exec rspec --profile=10 --format documentation
env: env:
NODE_OPTIONS: --openssl-legacy-provider NODE_OPTIONS: --openssl-legacy-provider
- name: Upload rails log folder - name: Upload rails log folder
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
if: always() if: always()
with: with:
name: rails-log-folder name: rails-log-folder
path: log 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
+15 -1
View File
@@ -32,6 +32,16 @@ master.key
public/uploads public/uploads
public/packs* public/packs*
public/assets/administrate*
public/assets/action*.js
public/assets/activestorage*.js
public/assets/trix*
public/assets/belongs_to*.js
public/assets/manifest*.js
public/assets/manifest*.js
public/assets/*.js.gz
public/assets/secretField*
public/assets/.sprockets-manifest-*.json
# VIM files # VIM files
*.swp *.swp
@@ -75,4 +85,8 @@ yalc.lock
yarn-debug.log* yarn-debug.log*
.yarn-integrity .yarn-integrity
/storybook-static # Vite Ruby
/public/vite*
# Vite uses dotenv and suggests to ignore local-only env files. See
# https://vitejs.dev/guide/env-and-mode.html#env-files
*.local
+8 -8
View File
@@ -1,11 +1,11 @@
#!/bin/sh # #!/bin/sh
. "$(dirname "$0")/_/husky.sh" # . "$(dirname "$0")/_/husky.sh"
# lint js and vue files # # lint js and vue files
npx --no-install lint-staged # npx --no-install lint-staged
# lint only staged ruby files # # lint only staged ruby files
git diff --name-only --cached | xargs ls -1 2>/dev/null | grep '\.rb$' | xargs bundle exec rubocop --force-exclusion -a # git diff --name-only --cached | xargs ls -1 2>/dev/null | grep '\.rb$' | xargs bundle exec rubocop --force-exclusion -a
# stage rubocop changes to files # # stage rubocop changes to files
git diff --name-only --cached | xargs git add # git diff --name-only --cached | xargs git add
-56
View File
@@ -1,56 +0,0 @@
const path = require('path');
const resolve = require('../config/webpack/resolve');
// Chatwoot's webpack.config.js
process.env.NODE_ENV = 'development';
const custom = require('../config/webpack/environment');
module.exports = {
stories: [
'../stories/**/*.stories.mdx',
'../app/javascript/**/*.stories.@(js|jsx|ts|tsx)',
],
addons: [
{
name: '@storybook/addon-docs',
options: {
vueDocgenOptions: {
alias: {
'@': path.resolve(__dirname, '../'),
},
},
},
},
'@storybook/addon-links',
'@storybook/addon-essentials',
{
/**
* Fix Storybook issue with PostCSS@8
* @see https://github.com/storybookjs/storybook/issues/12668#issuecomment-773958085
*/
name: '@storybook/addon-postcss',
options: {
postcssLoaderOptions: {
implementation: require('postcss'),
},
},
},
],
webpackFinal: config => {
const newConfig = {
...config,
resolve: {
...config.resolve,
modules: custom.resolvedModules.map(i => i.value),
},
};
newConfig.module.rules.push({
test: /\.scss$/,
use: ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader'],
include: path.resolve(__dirname, '../app/javascript'),
});
return newConfig;
},
};
-46
View File
@@ -1,46 +0,0 @@
import { addDecorator } from '@storybook/vue';
import Vue from 'vue';
import Vuex from 'vuex';
import VueI18n from 'vue-i18n';
import Multiselect from 'vue-multiselect';
import VueDOMPurifyHTML from 'vue-dompurify-html';
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon';
import WootUiKit from '../app/javascript/dashboard/components';
import i18n from '../app/javascript/dashboard/i18n';
import { domPurifyConfig } from 'shared/helpers/HTMLSanitizer';
import '../app/javascript/dashboard/assets/scss/storybook.scss';
Vue.use(VueI18n);
Vue.use(WootUiKit);
Vue.use(Vuex);
Vue.use(VueDOMPurifyHTML, domPurifyConfig);
Vue.component('multiselect', Multiselect);
Vue.component('fluent-icon', FluentIcon);
const store = new Vuex.Store({});
const i18nConfig = new VueI18n({
locale: 'en',
messages: i18n,
});
addDecorator(() => ({
template: '<story/>',
i18n: i18nConfig,
store,
beforeCreate: function () {
this.$root._i18n = this.$i18n;
},
}));
export const parameters = {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
};
+1 -3
View File
@@ -64,7 +64,7 @@ gem 'activerecord-import'
gem 'dotenv-rails', '>= 3.0.0' gem 'dotenv-rails', '>= 3.0.0'
gem 'foreman' gem 'foreman'
gem 'puma' gem 'puma'
gem 'webpacker' gem 'vite_rails'
# metrics on heroku # metrics on heroku
gem 'barnes' gem 'barnes'
@@ -204,8 +204,6 @@ group :development do
end end
group :test do group :test do
# Cypress in rails.
gem 'cypress-on-rails'
# fast cleaning of database # fast cleaning of database
gem 'database_cleaner' gem 'database_cleaner'
# mock http calls # mock http calls
+16 -17
View File
@@ -178,8 +178,6 @@ GEM
csv (3.3.0) csv (3.3.0)
csv-safe (3.3.1) csv-safe (3.3.1)
csv (~> 3.0) csv (~> 3.0)
cypress-on-rails (1.16.0)
rack
database_cleaner (2.0.2) database_cleaner (2.0.2)
database_cleaner-active_record (>= 2, < 3) database_cleaner-active_record (>= 2, < 3)
database_cleaner-active_record (2.1.0) database_cleaner-active_record (2.1.0)
@@ -222,6 +220,7 @@ GEM
railties (>= 6.1) railties (>= 6.1)
down (5.4.0) down (5.4.0)
addressable (~> 2.8) addressable (~> 2.8)
dry-cli (1.1.0)
ecma-re-validator (0.4.0) ecma-re-validator (0.4.0)
regexp_parser (~> 2.2) regexp_parser (~> 2.2)
elastic-apm (4.6.2) elastic-apm (4.6.2)
@@ -496,14 +495,14 @@ GEM
newrelic_rpm (9.6.0) newrelic_rpm (9.6.0)
base64 base64
nio4r (2.7.3) nio4r (2.7.3)
nokogiri (1.16.6) nokogiri (1.16.7)
mini_portile2 (~> 2.8.2) mini_portile2 (~> 2.8.2)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.16.6-arm64-darwin) nokogiri (1.16.7-arm64-darwin)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.16.6-x86_64-darwin) nokogiri (1.16.7-x86_64-darwin)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.16.6-x86_64-linux) nokogiri (1.16.7-x86_64-linux)
racc (~> 1.4) racc (~> 1.4)
oauth (1.1.0) oauth (1.1.0)
oauth-tty (~> 1.0, >= 1.0.1) oauth-tty (~> 1.0, >= 1.0.1)
@@ -557,7 +556,7 @@ GEM
pundit (2.3.0) pundit (2.3.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
raabro (1.4.0) raabro (1.4.0)
racc (1.8.0) racc (1.8.1)
rack (2.2.9) rack (2.2.9)
rack-attack (6.7.0) rack-attack (6.7.0)
rack (>= 1.0, < 4) rack (>= 1.0, < 4)
@@ -570,7 +569,7 @@ GEM
rack-protection (3.2.0) rack-protection (3.2.0)
base64 (>= 0.1.0) base64 (>= 0.1.0)
rack (~> 2.2, >= 2.2.4) rack (~> 2.2, >= 2.2.4)
rack-proxy (0.7.6) rack-proxy (0.7.7)
rack rack
rack-test (2.1.0) rack-test (2.1.0)
rack (>= 1.3) rack (>= 1.3)
@@ -709,7 +708,6 @@ GEM
activerecord (>= 4) activerecord (>= 4)
activesupport (>= 4) activesupport (>= 4)
selectize-rails (0.12.6) selectize-rails (0.12.6)
semantic_range (3.0.0)
sentry-rails (5.19.0) sentry-rails (5.19.0)
railties (>= 5.0) railties (>= 5.0)
sentry-ruby (~> 5.19.0) sentry-ruby (~> 5.19.0)
@@ -800,6 +798,13 @@ GEM
activemodel (>= 3.2) activemodel (>= 3.2)
mail (~> 2.5) mail (~> 2.5)
version_gem (1.1.4) version_gem (1.1.4)
vite_rails (3.0.17)
railties (>= 5.1, < 8)
vite_ruby (~> 3.0, >= 3.2.2)
vite_ruby (3.8.0)
dry-cli (>= 0.7, < 2)
rack-proxy (~> 0.6, >= 0.6.1)
zeitwerk (~> 2.2)
warden (1.2.9) warden (1.2.9)
rack (>= 2.0.9) rack (>= 2.0.9)
web-console (4.2.1) web-console (4.2.1)
@@ -814,11 +819,6 @@ GEM
addressable (>= 2.8.0) addressable (>= 2.8.0)
crack (>= 0.3.2) crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0) hashdiff (>= 0.4.0, < 2.0.0)
webpacker (5.4.4)
activesupport (>= 5.2)
rack-proxy (>= 0.6.1)
railties (>= 5.2)
semantic_range (>= 2.3.0)
webrick (1.8.2) webrick (1.8.2)
websocket-driver (0.7.6) websocket-driver (0.7.6)
websocket-extensions (>= 0.1.0) websocket-extensions (>= 0.1.0)
@@ -827,7 +827,7 @@ GEM
working_hours (1.4.1) working_hours (1.4.1)
activesupport (>= 3.2) activesupport (>= 3.2)
tzinfo tzinfo
zeitwerk (2.6.16) zeitwerk (2.6.17)
PLATFORMS PLATFORMS
arm64-darwin-20 arm64-darwin-20
@@ -862,7 +862,6 @@ DEPENDENCIES
climate_control climate_control
commonmarker commonmarker
csv-safe csv-safe
cypress-on-rails
database_cleaner database_cleaner
ddtrace ddtrace
debug (~> 1.8) debug (~> 1.8)
@@ -960,10 +959,10 @@ DEPENDENCIES
tzinfo-data tzinfo-data
uglifier uglifier
valid_email2 valid_email2
vite_rails
web-console (>= 4.2.1) web-console (>= 4.2.1)
web-push (>= 3.0.1) web-push (>= 3.0.1)
webmock webmock
webpacker
wisper (= 2.0.0) wisper (= 2.0.0)
working_hours working_hours
+1 -1
View File
@@ -1,4 +1,4 @@
backend: bin/rails s -p 3000 backend: bin/rails s -p 3000
frontend: export NODE_OPTIONS=--openssl-legacy-provider && bin/webpack-dev-server
# https://github.com/mperham/sidekiq/issues/3090#issuecomment-389748695 # https://github.com/mperham/sidekiq/issues/3090#issuecomment-389748695
worker: dotenv bundle exec sidekiq -C config/sidekiq.yml worker: dotenv bundle exec sidekiq -C config/sidekiq.yml
vite: bin/vite dev
+1 -1
View File
@@ -1,3 +1,3 @@
backend: RAILS_ENV=test bin/rails s -p 5050 backend: RAILS_ENV=test bin/rails s -p 5050
frontend: export NODE_OPTIONS=--openssl-legacy-provider && bin/webpack-dev-server vite: bin/vite dev
worker: RAILS_ENV=test dotenv bundle exec sidekiq -C config/sidekiq.yml worker: RAILS_ENV=test dotenv bundle exec sidekiq -C config/sidekiq.yml
+5 -2
View File
@@ -1,9 +1,12 @@
## 🚨 Note: This branch is unstable. For the stable branch's source code, please use the branch [3.x](https://github.com/chatwoot/chatwoot/tree/3.x)
<img src="https://user-images.githubusercontent.com/2246121/282256557-1570674b-d142-4198-9740-69404cc6a339.png#gh-light-mode-only" width="100%" alt="Chat dashboard dark mode"/> <img src="https://user-images.githubusercontent.com/2246121/282256557-1570674b-d142-4198-9740-69404cc6a339.png#gh-light-mode-only" width="100%" alt="Chat dashboard dark mode"/>
<img src="https://user-images.githubusercontent.com/2246121/282256632-87f6a01b-6467-4e0e-8a93-7bbf66d03a17.png#gh-dark-mode-only" width="100%" alt="Chat dashboard"/> <img src="https://user-images.githubusercontent.com/2246121/282256632-87f6a01b-6467-4e0e-8a93-7bbf66d03a17.png#gh-dark-mode-only" width="100%" alt="Chat dashboard"/>
___ ___
# Chatwoot # Chatwoot
Customer engagement suite, an open-source alternative to Intercom, Zendesk, Salesforce Service Cloud etc. Customer engagement suite, an open-source alternative to Intercom, Zendesk, Salesforce Service Cloud etc.
<p> <p>
@@ -98,7 +101,7 @@ Chatwoot now supports 1-Click deployment to DigitalOcean as a kubernetes app.
### Other deployment options ### Other deployment options
For other supported options, checkout our [deployment page](https://chatwoot.com/deploy). For other supported options, checkout our [deployment page](https://chatwoot.com/deploy).
## Security ## Security
+1 -1
View File
@@ -1 +1 @@
3.9.0 3.13.0
+1 -1
View File
@@ -1 +1 @@
2.8.0 3.0.0
-1
View File
@@ -2,5 +2,4 @@
//= link administrate/application.css //= link administrate/application.css
//= link administrate/application.js //= link administrate/application.js
//= link administrate-field-active_storage/application.css //= link administrate-field-active_storage/application.css
//= link dashboardChart.js
//= link secretField.js //= link secretField.js
-55
View File
@@ -1,55 +0,0 @@
// eslint-disable-next-line
function prepareData(data) {
var labels = [];
var dataSet = [];
data.forEach(item => {
labels.push(item[0]);
dataSet.push(item[1]);
});
return { labels, dataSet };
}
function getChartOptions() {
var fontFamily =
'PlusJakarta,-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif';
return {
responsive: true,
legend: { labels: { fontFamily } },
scales: {
xAxes: [
{
barPercentage: 1.26,
ticks: { fontFamily },
gridLines: { display: false },
},
],
yAxes: [
{
ticks: { fontFamily },
gridLines: { display: false },
},
],
},
};
}
// eslint-disable-next-line
function drawSuperAdminDashboard(data) {
var ctx = document.getElementById('dashboard-chart').getContext('2d');
var chartData = prepareData(data);
// eslint-disable-next-line
new Chart(ctx, {
type: 'bar',
data: {
labels: chartData.labels,
datasets: [
{
label: 'Conversations',
data: chartData.dataSet,
backgroundColor: '#1f93ff',
},
],
},
options: getChartOptions(),
});
}
@@ -86,8 +86,5 @@ $swift-ease-out-duration: .4s !default;
$swift-ease-out-timing-function: cubic-bezier(.25, .8, .25, 1) !default; $swift-ease-out-timing-function: cubic-bezier(.25, .8, .25, 1) !default;
$swift-ease-out: all $swift-ease-out-duration $swift-ease-out-timing-function !default; $swift-ease-out: all $swift-ease-out-duration $swift-ease-out-timing-function !default;
// Ionicons
$ionicons-font-path: '~ionicons/fonts';
// Transitions // Transitions
$transition-ease-in: all 0.250s ease-in; $transition-ease-in: all 0.250s ease-in;
+1 -1
View File
@@ -72,7 +72,7 @@ class DashboardController < ActionController::Base
@application_pack = if request.path.include?('/auth') || request.path.include?('/login') @application_pack = if request.path.include?('/auth') || request.path.include?('/login')
'v3app' 'v3app'
else else
'application' 'dashboard'
end end
end end
+49 -14
View File
@@ -1,6 +1,5 @@
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import router from '../dashboard/routes';
import AddAccountModal from '../dashboard/components/layout/sidebarComponents/AddAccountModal.vue'; import AddAccountModal from '../dashboard/components/layout/sidebarComponents/AddAccountModal.vue';
import LoadingState from './components/widgets/LoadingState.vue'; import LoadingState from './components/widgets/LoadingState.vue';
import NetworkNotification from './components/NetworkNotification.vue'; import NetworkNotification from './components/NetworkNotification.vue';
@@ -9,6 +8,8 @@ import UpgradeBanner from './components/app/UpgradeBanner.vue';
import PaymentPendingBanner from './components/app/PaymentPendingBanner.vue'; import PaymentPendingBanner from './components/app/PaymentPendingBanner.vue';
import PendingEmailVerificationBanner from './components/app/PendingEmailVerificationBanner.vue'; import PendingEmailVerificationBanner from './components/app/PendingEmailVerificationBanner.vue';
import vueActionCable from './helper/actionCable'; import vueActionCable from './helper/actionCable';
import { useRouter } from 'vue-router';
import { useStore } from 'dashboard/composables/store';
import WootSnackbarBox from './components/SnackbarContainer.vue'; import WootSnackbarBox from './components/SnackbarContainer.vue';
import { setColorTheme } from './helper/themeHelper'; import { setColorTheme } from './helper/themeHelper';
import { isOnOnboardingView } from 'v3/helpers/RouteHelper'; import { isOnOnboardingView } from 'v3/helpers/RouteHelper';
@@ -31,6 +32,12 @@ export default {
UpgradeBanner, UpgradeBanner,
PendingEmailVerificationBanner, PendingEmailVerificationBanner,
}, },
setup() {
const router = useRouter();
const store = useStore();
return { router, store };
},
data() { data() {
return { return {
showAddAccountModal: false, showAddAccountModal: false,
@@ -38,7 +45,6 @@ export default {
reconnectService: null, reconnectService: null,
}; };
}, },
computed: { computed: {
...mapGetters({ ...mapGetters({
getAccount: 'accounts/getAccount', getAccount: 'accounts/getAccount',
@@ -46,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;
@@ -64,17 +73,24 @@ 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.initializeColorTheme();
this.listenToThemeChanges();
this.setLocale(window.chatwootConfig.selectedLocale); this.setLocale(window.chatwootConfig.selectedLocale);
}, },
beforeDestroy() { async mounted() {
this.setLocale(window.chatwootConfig.selectedLocale);
this.initializeColorTheme();
this.listenToThemeChanges();
await this.$store.dispatch('accounts/get');
},
unmounted() {
if (this.reconnectService) { if (this.reconnectService) {
this.reconnectService.disconnect(); this.reconnectService.disconnect();
} }
@@ -88,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');
@@ -100,8 +116,9 @@ export default {
const { pubsub_token: pubsubToken } = this.currentUser || {}; const { pubsub_token: pubsubToken } = this.currentUser || {};
this.setLocale(locale); this.setLocale(locale);
this.latestChatwootVersion = latestChatwootVersion; this.latestChatwootVersion = latestChatwootVersion;
vueActionCable.init(pubsubToken); vueActionCable.init(this.store, pubsubToken);
this.reconnectService = new ReconnectService(this.$store, router); this.reconnectService = new ReconnectService(this.store, this.router);
window.reconnectService = this.reconnectService;
verifyServiceWorkerExistence(registration => verifyServiceWorkerExistence(registration =>
registration.pushManager.getSubscription().then(subscription => { registration.pushManager.getSubscription().then(subscription => {
@@ -129,9 +146,11 @@ export default {
<PaymentPendingBanner v-if="hideOnOnboardingView" /> <PaymentPendingBanner v-if="hideOnOnboardingView" />
<UpgradeBanner /> <UpgradeBanner />
</template> </template>
<transition name="fade" mode="out-in"> <router-view v-slot="{ Component }">
<router-view /> <transition name="fade" mode="out-in">
</transition> <component :is="Component" />
</transition>
</router-view>
<AddAccountModal :show="showAddAccountModal" :has-accounts="hasAccounts" /> <AddAccountModal :show="showAddAccountModal" :has-accounts="hasAccounts" />
<WootSnackbarBox /> <WootSnackbarBox />
<NetworkNotification /> <NetworkNotification />
@@ -141,6 +160,22 @@ export default {
<style lang="scss"> <style lang="scss">
@import './assets/scss/app'; @import './assets/scss/app';
.v-popper--theme-tooltip .v-popper__inner {
background: black !important;
font-size: 0.75rem;
padding: 4px 8px !important;
border-radius: 6px;
font-weight: 400;
}
.v-popper--theme-tooltip .v-popper__arrow-container {
display: none;
}
.multiselect__input {
margin-bottom: 0px !important;
}
</style> </style>
<style src="vue-multiselect/dist/vue-multiselect.min.css"></style> <style src="vue-multiselect/dist/vue-multiselect.css"></style>
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 76 KiB

@@ -0,0 +1,3 @@
<svg width="800" height="800" viewBox="0 0 800 800" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M800 0H600V200H400V400H200V600H0V800H200H400H600H800V600V400V200V0Z" fill="#2773E4" fill-opacity="0.42"/>
</svg>

After

Width:  |  Height:  |  Size: 262 B

@@ -0,0 +1,3 @@
<svg width="600" height="600" viewBox="0 0 600 600" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M200 0H0V200V400V600H200V400H400V200H600V0H400H200Z" fill="#2773E4" fill-opacity="0.42"/>
</svg>

After

Width:  |  Height:  |  Size: 246 B

@@ -31,23 +31,6 @@
transform: translateX($space-medium); transform: translateX($space-medium);
} }
.menu-list-enter-active,
.menu-list-leave-active {
transition: opacity 0.3s var(--ease-out-cubic),
transform 0.2s var(--ease-out-cubic);
}
.menu-list-leave-to {
opacity: 0;
position: absolute;
transform: translateX($space-small);
}
.menu-list-enter {
opacity: 0;
transform: translateX(-$space-small);
}
.slide-up-enter-active { .slide-up-enter-active {
transition: all 0.3s var(--ease-in-cubic); transition: all 0.3s var(--ease-in-cubic);
} }
@@ -65,7 +48,8 @@
.menu-slide-enter-active, .menu-slide-enter-active,
.menu-slide-leave-active { .menu-slide-leave-active {
transform: translateY(0); transform: translateY(0);
transition: transform 0.25s var(--ease-in-cubic), transition:
transform 0.25s var(--ease-in-cubic),
opacity 0.15s var(--ease-in-cubic); opacity 0.15s var(--ease-in-cubic);
} }
@@ -1,4 +1,4 @@
@import '~vue2-datepicker/scss/index'; @import 'vue-datepicker-next/scss/index';
.date-picker { .date-picker {
// To be removed one SLA reports date picker is created // To be removed one SLA reports date picker is created
@@ -1,4 +1,4 @@
@import '~dashboard/assets/scss/variables'; @import 'dashboard/assets/scss/variables';
.formulate-input { .formulate-input {
.formulate-input-errors { .formulate-input-errors {
@@ -1,10 +1,11 @@
// scss-lint:disable SpaceAfterPropertyColon // scss-lint:disable SpaceAfterPropertyColon
@import 'shared/assets/fonts/inter'; @import 'shared/assets/fonts/inter';
// Inter, // Inter,
html, html,
body { body {
font-family: font-family:
'PlusJakarta', 'Inter',
-apple-system, -apple-system,
system-ui, system-ui,
BlinkMacSystemFont, BlinkMacSystemFont,
@@ -23,7 +24,7 @@ body {
} }
.app-wrapper { .app-wrapper {
@apply h-full flex-grow-0 min-h-0 w-full; @apply h-screen flex-grow-0 min-h-0 w-full;
.button--fixed-top { .button--fixed-top {
@apply fixed ltr:right-2 rtl:left-2 top-2 flex flex-row; @apply fixed ltr:right-2 rtl:left-2 top-2 flex flex-row;
@@ -1,5 +1,5 @@
@import '~dashboard/assets/scss/variables'; @import 'dashboard/assets/scss/variables';
@import '~widget/assets/scss/mixins'; @import 'widget/assets/scss/mixins';
$spinner-before-border-color: rgba(255, 255, 255, 0.7); $spinner-before-border-color: rgba(255, 255, 255, 0.7);
@@ -89,9 +89,6 @@ $swift-ease-out-duration: .4s !default;
$swift-ease-out-function: cubic-bezier(0.37, 0, 0.63, 1) !default; $swift-ease-out-function: cubic-bezier(0.37, 0, 0.63, 1) !default;
$swift-ease-out: all $swift-ease-out-duration $swift-ease-out-function !default; $swift-ease-out: all $swift-ease-out-duration $swift-ease-out-function !default;
// Ionicons
$ionicons-font-path: '~ionicons/fonts';
// Transitions // Transitions
$transition-ease-in: all 0.250s ease-in; $transition-ease-in: all 0.250s ease-in;
@@ -2,7 +2,6 @@
@import 'tailwindcss/components'; @import 'tailwindcss/components';
@import 'tailwindcss/utilities'; @import 'tailwindcss/utilities';
@import 'shared/assets/fonts/plus-jakarta';
@import 'shared/assets/fonts/InterDisplay/inter-display'; @import 'shared/assets/fonts/InterDisplay/inter-display';
@import 'shared/assets/fonts/inter'; @import 'shared/assets/fonts/inter';
@@ -34,10 +33,13 @@
@import 'plugins/multiselect'; @import 'plugins/multiselect';
@import 'plugins/dropdown'; @import 'plugins/dropdown';
@import '~shared/assets/stylesheets/ionicons';
.tooltip { .tooltip {
@apply bg-slate-900 text-white py-1 px-2 z-40 text-xs rounded-md dark:bg-slate-200 dark:text-slate-900; @apply bg-slate-900 text-white py-1 px-2 z-40 text-xs rounded-md dark:bg-slate-200 dark:text-slate-900 max-w-96;
}
#app {
@apply h-full w-full;
} }
.hide { .hide {
@@ -45,7 +47,6 @@
} }
@layer base { @layer base {
// scss-lint:disable PropertySortOrder // scss-lint:disable PropertySortOrder
:root { :root {
--color-amber-25: 254 253 251; --color-amber-25: 254 253 251;
@@ -222,6 +222,7 @@
.multiselect__input { .multiselect__input {
@apply h-[2.875rem] min-h-[2.875rem]; @apply h-[2.875rem] min-h-[2.875rem];
margin-bottom: 0px !important;
} }
.multiselect__single { .multiselect__single {
@@ -1,39 +0,0 @@
@import 'shared/assets/fonts/inter';
@import 'shared/assets/fonts/plus-jakarta';
@import 'shared/assets/stylesheets/animations';
@import 'shared/assets/stylesheets/colors';
@import 'shared/assets/stylesheets/spacing';
@import 'shared/assets/stylesheets/font-size';
@import 'shared/assets/stylesheets/font-weights';
@import 'shared/assets/stylesheets/shadows';
@import 'shared/assets/stylesheets/border-radius';
@import 'variables';
@import 'vue-multiselect/dist/vue-multiselect.min.css';
@import '~shared/assets/stylesheets/ionicons';
@import 'mixins';
@import 'helper-classes';
@import 'typography';
@import 'layout';
@import 'animations';
@import 'widgets/buttons';
@import 'widgets/base';
@import 'plugins/multiselect';
@import 'widget/assets/scss/reset';
@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';
@import 'widget/assets/scss/utilities';
html,
body {
font-family: 'PlusJakarta', sans-serif;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
height: 100%;
}
@@ -1,30 +0,0 @@
import { action } from '@storybook/addon-actions';
import AccordionItemComponent from './AccordionItem';
export default {
title: 'Components/Generic/Accordion',
component: AccordionItemComponent,
argTypes: {
title: {
control: {
type: 'string',
},
},
},
};
const Template = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: { AccordionItem: AccordionItemComponent },
template: `
<accordion-item v-bind="$props" @click="onClick">
This is a sample content you can pass as a slot
</accordion-item>
`,
});
export const AccordionItem = Template.bind({});
AccordionItem.args = {
onClick: action('Added'),
title: 'Title of the accordion item',
};
@@ -1,32 +1,34 @@
<script> <script setup>
import EmojiOrIcon from 'shared/components/EmojiOrIcon.vue'; import EmojiOrIcon from 'shared/components/EmojiOrIcon.vue';
import { defineEmits } from 'vue';
export default { defineProps({
components: { title: {
EmojiOrIcon, type: String,
required: true,
}, },
props: { compact: {
title: { type: Boolean,
type: String, default: false,
required: true,
},
compact: {
type: Boolean,
default: false,
},
icon: {
type: String,
default: '',
},
emoji: {
type: String,
default: '',
},
isOpen: {
type: Boolean,
default: true,
},
}, },
icon: {
type: String,
default: '',
},
emoji: {
type: String,
default: '',
},
isOpen: {
type: Boolean,
default: true,
},
});
const emit = defineEmits(['toggle']);
const onToggle = () => {
emit('toggle');
}; };
</script> </script>
@@ -34,7 +36,7 @@ export default {
<div class="-mt-px text-sm"> <div class="-mt-px text-sm">
<button <button
class="flex items-center select-none w-full rounded-none bg-slate-50 dark:bg-slate-800 border border-l-0 border-r-0 border-solid m-0 border-slate-100 dark:border-slate-700/50 cursor-grab justify-between py-2 px-4 drag-handle" class="flex items-center select-none w-full rounded-none bg-slate-50 dark:bg-slate-800 border border-l-0 border-r-0 border-solid m-0 border-slate-100 dark:border-slate-700/50 cursor-grab justify-between py-2 px-4 drag-handle"
@click="$emit('click')" @click.stop="onToggle"
> >
<div class="flex justify-between mb-0.5"> <div class="flex justify-between mb-0.5">
<EmojiOrIcon class="inline-block w-5" :icon="icon" :emoji="emoji" /> <EmojiOrIcon class="inline-block w-5" :icon="icon" :emoji="emoji" />
@@ -16,7 +16,6 @@ export default {
<template> <template>
<button <button
class="bg-white dark:bg-slate-900 cursor-pointer flex flex-col justify-end transition-all duration-200 ease-in -m-px py-4 px-0 items-center border border-solid border-slate-25 dark:border-slate-800 hover:border-woot-500 dark:hover:border-woot-500 hover:shadow-md hover:z-50 disabled:opacity-60" class="bg-white dark:bg-slate-900 cursor-pointer flex flex-col justify-end transition-all duration-200 ease-in -m-px py-4 px-0 items-center border border-solid border-slate-25 dark:border-slate-800 hover:border-woot-500 dark:hover:border-woot-500 hover:shadow-md hover:z-50 disabled:opacity-60"
@click="$emit('click')"
> >
<img :src="src" :alt="title" class="w-1/2 my-4 mx-auto" /> <img :src="src" :alt="title" class="w-1/2 my-4 mx-auto" />
<h3 <h3
File diff suppressed because it is too large Load Diff
@@ -61,7 +61,7 @@ const hasAppliedFiltersOrActiveFolders = computed(() => {
</span> </span>
</div> </div>
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<div v-if="hasAppliedFilters && !hasActiveFolders"> <template v-if="hasAppliedFilters && !hasActiveFolders">
<woot-button <woot-button
v-tooltip.top-end="$t('FILTER.CUSTOM_VIEWS.ADD.SAVE_BUTTON')" v-tooltip.top-end="$t('FILTER.CUSTOM_VIEWS.ADD.SAVE_BUTTON')"
size="tiny" size="tiny"
@@ -78,8 +78,8 @@ const hasAppliedFiltersOrActiveFolders = computed(() => {
icon="dismiss-circle" icon="dismiss-circle"
@click="emit('resetFilters')" @click="emit('resetFilters')"
/> />
</div> </template>
<div v-if="hasActiveFolders"> <template v-if="hasActiveFolders">
<woot-button <woot-button
v-tooltip.top-end="$t('FILTER.CUSTOM_VIEWS.EDIT.EDIT_BUTTON')" v-tooltip.top-end="$t('FILTER.CUSTOM_VIEWS.EDIT.EDIT_BUTTON')"
size="tiny" size="tiny"
@@ -96,7 +96,7 @@ const hasAppliedFiltersOrActiveFolders = computed(() => {
icon="delete" icon="delete"
@click="emit('deleteFolders')" @click="emit('deleteFolders')"
/> />
</div> </template>
<woot-button <woot-button
v-else v-else
v-tooltip.right="$t('FILTER.TOOLTIP_LABEL')" v-tooltip.right="$t('FILTER.TOOLTIP_LABEL')"
@@ -108,7 +108,7 @@ const hasAppliedFiltersOrActiveFolders = computed(() => {
/> />
<ConversationBasicFilter <ConversationBasicFilter
v-if="!hasAppliedFiltersOrActiveFolders" v-if="!hasAppliedFiltersOrActiveFolders"
@changeFilter="onBasicFilterChange" @change-filter="onBasicFilterChange"
/> />
</div> </div>
</div> </div>
+23 -25
View File
@@ -1,5 +1,7 @@
<script> <script>
import 'highlight.js/styles/default.css'; import 'highlight.js/styles/default.css';
import 'highlight.js/lib/common';
import { copyTextToClipboard } from 'shared/helpers/clipboard'; import { copyTextToClipboard } from 'shared/helpers/clipboard';
import { useAlert } from 'dashboard/composables'; import { useAlert } from 'dashboard/composables';
@@ -28,14 +30,28 @@ export default {
return JSON.stringify({ return JSON.stringify({
title: this.codepenTitle, title: this.codepenTitle,
private: true, private: true,
[lang]: this.script, [lang]: this.scrubbedScript,
}); });
}, },
scrubbedScript() {
// remove trailing and leading extra lines and not spaces
const scrubbed = this.script.replace(/^\s*[\r\n]/gm, '');
const lines = scrubbed.split('\n');
// remove extra indentations
const minIndent = lines.reduce((min, line) => {
if (line.trim().length === 0) return min;
const indent = line.match(/^\s*/)[0].length;
return Math.min(min, indent);
}, Infinity);
return lines.map(line => line.slice(minIndent)).join('\n');
},
}, },
methods: { methods: {
async onCopy(e) { async onCopy(e) {
e.preventDefault(); e.preventDefault();
await copyTextToClipboard(this.script); await copyTextToClipboard(this.scrubbedScript);
useAlert(this.$t('COMPONENTS.CODE.COPY_SUCCESSFUL')); useAlert(this.$t('COMPONENTS.CODE.COPY_SUCCESSFUL'));
}, },
}, },
@@ -43,42 +59,24 @@ export default {
</script> </script>
<template> <template>
<div class="code--container"> <div class="relative text-left">
<div class="code--action-area"> <div class="top-1.5 absolute right-1.5 flex items-center gap-1">
<form <form
v-if="enableCodePen" v-if="enableCodePen"
class="code--codeopen-form" class="flex items-center"
action="https://codepen.io/pen/define" action="https://codepen.io/pen/define"
method="POST" method="POST"
target="_blank" target="_blank"
> >
<input type="hidden" name="data" :value="codepenScriptValue" /> <input type="hidden" name="data" :value="codepenScriptValue" />
<button type="submit" class="button secondary tiny"> <button type="submit" class="button secondary tiny">
{{ $t('COMPONENTS.CODE.CODEPEN') }} {{ $t('COMPONENTS.CODE.CODEPEN') }}
</button> </button>
</form> </form>
<button class="button secondary tiny" @click="onCopy"> <button type="button" class="button secondary tiny" @click="onCopy">
{{ $t('COMPONENTS.CODE.BUTTON_TEXT') }} {{ $t('COMPONENTS.CODE.BUTTON_TEXT') }}
</button> </button>
</div> </div>
<highlightjs v-if="script" :language="lang" :code="script" /> <highlightjs v-if="script" :language="lang" :code="scrubbedScript" />
</div> </div>
</template> </template>
<style lang="scss" scoped>
.code--container {
position: relative;
text-align: left;
.code--action-area {
top: var(--space-small);
position: absolute;
right: var(--space-small);
}
.code--codeopen-form {
display: inline-block;
}
}
</style>
@@ -14,6 +14,7 @@ export default {
'toggleContextMenu', 'toggleContextMenu',
'markAsUnread', 'markAsUnread',
'assignPriority', 'assignPriority',
'isConversationSelected',
], ],
props: { props: {
source: { source: {
@@ -36,10 +37,6 @@ export default {
type: [String, Number], type: [String, Number],
default: 0, default: 0,
}, },
isConversationSelected: {
type: Function,
default: () => {},
},
showAssignee: { showAssignee: {
type: Boolean, type: Boolean,
default: false, default: false,
@@ -59,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>
@@ -7,6 +7,7 @@ import HelperTextPopup from 'dashboard/components/ui/HelperTextPopup.vue';
import { isValidURL } from '../helper/URLHelper'; import { isValidURL } from '../helper/URLHelper';
import { getRegexp } from 'shared/helpers/Validators'; import { getRegexp } from 'shared/helpers/Validators';
import { useVuelidate } from '@vuelidate/core'; import { useVuelidate } from '@vuelidate/core';
import { emitter } from 'shared/helpers/mitt';
const DATE_FORMAT = 'yyyy-MM-dd'; const DATE_FORMAT = 'yyyy-MM-dd';
@@ -30,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() };
}, },
@@ -135,10 +137,10 @@ export default {
}, },
mounted() { mounted() {
this.editedValue = this.formattedValue; this.editedValue = this.formattedValue;
this.$emitter.on(BUS_EVENTS.FOCUS_CUSTOM_ATTRIBUTE, this.onFocusAttribute); emitter.on(BUS_EVENTS.FOCUS_CUSTOM_ATTRIBUTE, this.onFocusAttribute);
}, },
destroyed() { unmounted() {
this.$emitter.off(BUS_EVENTS.FOCUS_CUSTOM_ATTRIBUTE, this.onFocusAttribute); emitter.off(BUS_EVENTS.FOCUS_CUSTOM_ATTRIBUTE, this.onFocusAttribute);
}, },
methods: { methods: {
onFocusAttribute(focusAttributeKey) { onFocusAttribute(focusAttributeKey) {
@@ -321,7 +323,7 @@ export default {
'CUSTOM_ATTRIBUTES.FORM.ATTRIBUTE_TYPE.LIST.SEARCH_INPUT_PLACEHOLDER' 'CUSTOM_ATTRIBUTES.FORM.ATTRIBUTE_TYPE.LIST.SEARCH_INPUT_PLACEHOLDER'
) )
" "
@click="onUpdateListValue" @select="onUpdateListValue"
/> />
</div> </div>
</div> </div>
@@ -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 />
@@ -1,10 +1,11 @@
<script> <script>
import DatePicker from 'vue2-datepicker'; import DatePicker from 'vue-datepicker-next';
export default { 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="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>
@@ -1,32 +1,26 @@
<script> <script setup>
export default { import { ref, defineEmits } from 'vue';
props: { import { useIntersectionObserver } from '@vueuse/core';
options: {
type: Object, const { options } = defineProps({
default: () => ({ root: document, rootMargin: '100px 0 100px 0)' }), options: {
}, type: Object,
default: () => ({ root: document, rootMargin: '100px 0 100px 0)' }),
}, },
mounted() { });
this.intersectionObserver = null;
this.registerInfiniteLoader(); const emit = defineEmits(['observed']);
const observedElement = ref('');
useIntersectionObserver(
observedElement,
([{ isIntersecting }]) => {
if (isIntersecting) {
emit('observed');
}
}, },
beforeDestroy() { options
this.unobserveInfiniteLoadObserver(); );
},
methods: {
registerInfiniteLoader() {
this.intersectionObserver = new IntersectionObserver(entries => {
if (entries && entries[0].isIntersecting) {
this.$emit('observed');
}
}, this.options);
this.intersectionObserver.observe(this.$refs.observedElement);
},
unobserveInfiniteLoadObserver() {
this.intersectionObserver.unobserve(this.$refs.observedElement);
},
},
};
</script> </script>
<template> <template>
+63 -72
View File
@@ -1,78 +1,69 @@
<script> <script setup>
export default { // [TODO] Use Teleport to move the modal to the end of the body
props: { import { ref, computed, defineEmits, onMounted } from 'vue';
closeOnBackdropClick: { import { useEventListener } from '@vueuse/core';
type: Boolean,
default: true,
},
show: Boolean,
showCloseButton: {
type: Boolean,
default: true,
},
onClose: {
type: Function,
required: true,
},
fullWidth: {
type: Boolean,
default: false,
},
modalType: {
type: String,
default: 'centered',
},
size: {
type: String,
default: '',
},
},
data() {
return {
mousedDownOnBackdrop: false,
};
},
computed: {
modalClassName() {
const modalClassNameMap = {
centered: '',
'right-aligned': 'right-aligned',
};
return `modal-mask skip-context-menu ${ const { modalType, closeOnBackdropClick, onClose } = defineProps({
modalClassNameMap[this.modalType] || '' closeOnBackdropClick: { type: Boolean, default: true },
}`; showCloseButton: { type: Boolean, default: true },
}, onClose: { type: Function, required: true },
}, fullWidth: { type: Boolean, default: false },
mounted() { modalType: { type: String, default: 'centered' },
document.addEventListener('keydown', e => { size: { type: String, default: '' },
if (this.show && e.code === 'Escape') { });
this.onClose();
}
});
document.body.addEventListener('mouseup', this.onMouseUp); const emit = defineEmits(['close']);
}, const show = defineModel('show', { type: Boolean, default: false });
beforeDestroy() {
document.body.removeEventListener('mouseup', this.onMouseUp); const modalClassName = computed(() => {
}, const modalClassNameMap = {
methods: { centered: '',
handleMouseDown() { 'right-aligned': 'right-aligned',
this.mousedDownOnBackdrop = true; };
},
close() { return `modal-mask skip-context-menu ${modalClassNameMap[modalType] || ''}`;
this.onClose(); });
},
onMouseUp() { // [TODO] Revisit this logic to use outside click directive
if (this.mousedDownOnBackdrop) { const mousedDownOnBackdrop = ref(false);
this.mousedDownOnBackdrop = false;
if (this.closeOnBackdropClick) { const handleMouseDown = () => {
this.onClose(); mousedDownOnBackdrop.value = true;
}
}
},
},
}; };
const close = () => {
show.value = false;
emit('close');
onClose();
};
const onMouseUp = () => {
if (mousedDownOnBackdrop.value) {
mousedDownOnBackdrop.value = false;
if (closeOnBackdropClick) {
close();
}
}
};
const onKeydown = e => {
if (show.value && e.code === 'Escape') {
close();
e.stopPropagation();
}
};
useEventListener(document.body, 'mouseup', onMouseUp);
useEventListener(document, 'keydown', onKeydown);
onMounted(() => {
if (onClose && typeof onClose === 'function') {
// eslint-disable-next-line no-console
console.warn(
"[DEPRECATED] The 'onClose' prop is deprecated. Please use the 'close' event instead."
);
}
});
</script> </script>
<template> <template>
@@ -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 }}
@@ -1,7 +1,7 @@
<script setup> <script setup>
import { ref, computed, onBeforeUnmount } from 'vue'; import { ref, computed, onBeforeUnmount } from 'vue';
import { useI18n } from 'dashboard/composables/useI18n'; import { useI18n } from 'vue-i18n';
import { useRoute } from 'dashboard/composables/route'; import { useRoute } from 'vue-router';
import { useEmitter } from 'dashboard/composables/emitter'; import { useEmitter } from 'dashboard/composables/emitter';
import { BUS_EVENTS } from 'shared/constants/busEvents'; import { BUS_EVENTS } from 'shared/constants/busEvents';
import { import {
@@ -1,5 +1,6 @@
<script> <script>
import { BUS_EVENTS } from 'shared/constants/busEvents'; import { BUS_EVENTS } from 'shared/constants/busEvents';
import { emitter } from 'shared/helpers/mitt';
export default { export default {
props: { props: {
@@ -10,7 +11,7 @@ export default {
}, },
methods: { methods: {
onMenuItemClick() { onMenuItemClick() {
this.$emitter.emit(BUS_EVENTS.TOGGLE_SIDEMENU); emitter.emit(BUS_EVENTS.TOGGLE_SIDEMENU);
}, },
}, },
}; };
@@ -1,5 +1,6 @@
<script> <script>
import WootSnackbar from './Snackbar.vue'; import WootSnackbar from './Snackbar.vue';
import { emitter } from 'shared/helpers/mitt';
export default { export default {
components: { components: {
@@ -19,10 +20,10 @@ export default {
}, },
mounted() { mounted() {
this.$emitter.on('newToastMessage', this.onNewToastMessage); emitter.on('newToastMessage', this.onNewToastMessage);
}, },
beforeDestroy() { unmounted() {
this.$emitter.off('newToastMessage', this.onNewToastMessage); emitter.off('newToastMessage', this.onNewToastMessage);
}, },
methods: { methods: {
onNewToastMessage({ message, action }) { onNewToastMessage({ message, action }) {
@@ -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
@click="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
@click="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
@click="routeToBilling" @primary-action="routeToBilling"
/> />
</template> </template>
@@ -27,16 +27,17 @@ export default {
default: 'primary', default: 'primary',
}, },
}, },
methods: { created() {
onClick(e) { // eslint-disable-next-line
this.$emit('click', e); console.warn(
}, '[DEPRECATED] This component has been deprecated and will be removed soon. Please use v3/components/Form/Button.vue instead'
);
}, },
}; };
</script> </script>
<template> <template>
<button :type="type" class="button nice" :class="variant" @click="onClick"> <button :type="type" class="button nice" :class="variant">
<fluent-icon <fluent-icon
v-if="!isLoading && icon" v-if="!isLoading && icon"
class="icon" class="icon"
@@ -40,11 +40,6 @@ export default {
return `button nice gap-2 ${this.buttonClass || ' '}`; return `button nice gap-2 ${this.buttonClass || ' '}`;
}, },
}, },
methods: {
onClick() {
this.$emit('click');
},
},
}; };
</script> </script>
@@ -54,7 +49,6 @@ export default {
data-testid="submit_button" data-testid="submit_button"
:disabled="disabled" :disabled="disabled"
:class="computedClass" :class="computedClass"
@click="onClick"
> >
<fluent-icon v-if="!!iconClass" :icon="iconClass" class="icon" /> <fluent-icon v-if="!!iconClass" :icon="iconClass" class="icon" />
<span>{{ buttonText }}</span> <span>{{ buttonText }}</span>
@@ -2,7 +2,7 @@
import { ref, computed } from 'vue'; import { ref, computed } from 'vue';
import { useAlert } from 'dashboard/composables'; import { useAlert } from 'dashboard/composables';
import { useToggle } from '@vueuse/core'; import { useToggle } from '@vueuse/core';
import { useI18n } from 'dashboard/composables/useI18n'; import { useI18n } from 'vue-i18n';
import { useStore, useStoreGetters } from 'dashboard/composables/store'; import { useStore, useStoreGetters } from 'dashboard/composables/store';
import { useEmitter } from 'dashboard/composables/emitter'; import { useEmitter } from 'dashboard/composables/emitter';
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents'; import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
+16 -21
View File
@@ -1,45 +1,40 @@
// [NOTE][DEPRECATED] This method is to be deprecated, please do not add new components to this file.
/* eslint no-plusplus: 0 */ /* eslint no-plusplus: 0 */
import AvatarUploader from './widgets/forms/AvatarUploader.vue'; import AvatarUploader from './widgets/forms/AvatarUploader.vue';
import Bar from './widgets/chart/BarChart'; import Button from './ui/WootButton.vue';
import Button from './ui/WootButton'; import Code from './Code.vue';
import Code from './Code'; import ColorPicker from './widgets/ColorPicker.vue';
import ColorPicker from './widgets/ColorPicker';
import ConfirmDeleteModal from './widgets/modal/ConfirmDeleteModal.vue'; import ConfirmDeleteModal from './widgets/modal/ConfirmDeleteModal.vue';
import ConfirmModal from './widgets/modal/ConfirmationModal.vue'; import ConfirmModal from './widgets/modal/ConfirmationModal.vue';
import ContextMenu from './ui/ContextMenu.vue';
import DeleteModal from './widgets/modal/DeleteModal.vue'; import DeleteModal from './widgets/modal/DeleteModal.vue';
import DropdownItem from 'shared/components/ui/dropdown/DropdownItem'; import DropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
import DropdownMenu from 'shared/components/ui/dropdown/DropdownMenu'; import DropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
import FeatureToggle from './widgets/FeatureToggle'; import FeatureToggle from './widgets/FeatureToggle.vue';
import HorizontalBar from './widgets/chart/HorizontalBarChart';
import Input from './widgets/forms/Input.vue'; import Input from './widgets/forms/Input.vue';
import PhoneInput from './widgets/forms/PhoneInput.vue'; import PhoneInput from './widgets/forms/PhoneInput.vue';
import Label from './ui/Label'; import Label from './ui/Label.vue';
import LoadingState from './widgets/LoadingState'; import LoadingState from './widgets/LoadingState.vue';
import Modal from './Modal'; import ModalHeader from './ModalHeader.vue';
import ModalHeader from './ModalHeader'; import Modal from './Modal.vue';
import SidemenuIcon from './SidemenuIcon'; import SidemenuIcon from './SidemenuIcon.vue';
import Spinner from 'shared/components/Spinner'; import Spinner from 'shared/components/Spinner.vue';
import SubmitButton from './buttons/FormSubmitButton'; import SubmitButton from './buttons/FormSubmitButton.vue';
import Tabs from './ui/Tabs/Tabs'; import Tabs from './ui/Tabs/Tabs.vue';
import TabsItem from './ui/Tabs/TabsItem'; import TabsItem from './ui/Tabs/TabsItem.vue';
import Thumbnail from './widgets/Thumbnail.vue'; import Thumbnail from './widgets/Thumbnail.vue';
import DatePicker from './ui/DatePicker/DatePicker.vue'; import DatePicker from './ui/DatePicker/DatePicker.vue';
const WootUIKit = { const WootUIKit = {
AvatarUploader, AvatarUploader,
Bar,
Button, Button,
Code, Code,
ColorPicker, ColorPicker,
ConfirmDeleteModal, ConfirmDeleteModal,
ConfirmModal, ConfirmModal,
ContextMenu,
DeleteModal, DeleteModal,
DropdownItem, DropdownItem,
DropdownMenu, DropdownMenu,
FeatureToggle, FeatureToggle,
HorizontalBar,
Input, Input,
PhoneInput, PhoneInput,
Label, Label,
@@ -31,26 +31,29 @@ export default {
currentAccountId: 'getCurrentAccountId', currentAccountId: 'getCurrentAccountId',
currentUserAutoOffline: 'getCurrentUserAutoOffline', currentUserAutoOffline: 'getCurrentUserAutoOffline',
}), }),
statusList() {
return [
this.$t('PROFILE_SETTINGS.FORM.AVAILABILITY.STATUS.ONLINE'),
this.$t('PROFILE_SETTINGS.FORM.AVAILABILITY.STATUS.BUSY'),
this.$t('PROFILE_SETTINGS.FORM.AVAILABILITY.STATUS.OFFLINE'),
];
},
availabilityDisplayLabel() { availabilityDisplayLabel() {
const availabilityIndex = AVAILABILITY_STATUS_KEYS.findIndex( const availabilityIndex = AVAILABILITY_STATUS_KEYS.findIndex(
key => key === this.currentUserAvailability key => key === this.currentUserAvailability
); );
return this.$t('PROFILE_SETTINGS.FORM.AVAILABILITY.STATUSES_LIST')[ return this.statusList[availabilityIndex];
availabilityIndex
];
}, },
currentUserAvailability() { currentUserAvailability() {
return this.getCurrentUserAvailability; return this.getCurrentUserAvailability;
}, },
availabilityStatuses() { availabilityStatuses() {
return this.$t('PROFILE_SETTINGS.FORM.AVAILABILITY.STATUSES_LIST').map( return this.statusList.map((statusLabel, index) => ({
(statusLabel, index) => ({ label: statusLabel,
label: statusLabel, value: AVAILABILITY_STATUS_KEYS[index],
value: AVAILABILITY_STATUS_KEYS[index], disabled:
disabled: this.currentUserAvailability === AVAILABILITY_STATUS_KEYS[index],
this.currentUserAvailability === AVAILABILITY_STATUS_KEYS[index], }));
})
);
}, },
}, },
@@ -102,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" />
@@ -129,7 +132,7 @@ export default {
<woot-switch <woot-switch
size="small" size="small"
class="mx-1 mt-px mb-0" class="mx-1 mt-px mb-0"
:value="currentUserAutoOffline" :model-value="currentUserAutoOffline"
@input="updateAutoOffline" @input="updateAutoOffline"
/> />
</WootDropdownItem> </WootDropdownItem>
@@ -2,7 +2,7 @@
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { getSidebarItems } from './config/default-sidebar'; import { getSidebarItems } from './config/default-sidebar';
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents'; import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
import { useRoute, useRouter } from 'dashboard/composables/route'; import { useRoute, useRouter } from 'vue-router';
import PrimarySidebar from './sidebarComponents/Primary.vue'; import PrimarySidebar from './sidebarComponents/Primary.vue';
import SecondarySidebar from './sidebarComponents/Secondary.vue'; import SecondarySidebar from './sidebarComponents/Secondary.vue';
@@ -22,11 +22,14 @@ export default {
type: Boolean, type: Boolean,
default: true, default: true,
}, },
sidebarClassName: {
type: String,
default: '',
},
}, },
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;
}); });
@@ -159,6 +166,17 @@ export default {
) || {}; ) || {};
return activePrimaryMenu; return activePrimaryMenu;
}, },
hasSecondaryMenu() {
return (
this.activeSecondaryMenu.menuItems &&
this.activeSecondaryMenu.menuItems.length
);
},
hasSecondarySidebar() {
// if it is explicitly stated to show and it has secondary menu items to show
// showSecondarySidebar corresponds to the UI settings, indicating if the user has toggled it
return this.showSecondarySidebar && this.hasSecondaryMenu;
},
}, },
watch: { watch: {
@@ -206,13 +224,12 @@ 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="showSecondarySidebar" v-if="hasSecondarySidebar"
:class="sidebarClassName"
:account-id="accountId" :account-id="accountId"
:inboxes="inboxes" :inboxes="inboxes"
:labels="labels" :labels="labels"
@@ -221,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',
@@ -1,7 +1,7 @@
<script> <script>
import { useVuelidate } from '@vuelidate/core';
import { required, minLength } from '@vuelidate/validators'; import { required, minLength } from '@vuelidate/validators';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { useVuelidate } from '@vuelidate/core';
import { useAlert } from 'dashboard/composables'; import { useAlert } from 'dashboard/composables';
export default { export default {
@@ -15,6 +15,7 @@ export default {
default: true, default: true,
}, },
}, },
emits: ['closeAccountCreateModal'],
setup() { setup() {
return { v$: useVuelidate() }; return { v$: useVuelidate() };
}, },
@@ -23,11 +24,13 @@ export default {
accountName: '', accountName: '',
}; };
}, },
validations: { validations() {
accountName: { return {
required, accountName: {
minLength: minLength(1), required,
}, minLength: minLength(1),
},
};
}, },
computed: { computed: {
...mapGetters({ ...mapGetters({
@@ -76,7 +79,7 @@ export default {
<label :class="{ error: v$.accountName.$error }"> <label :class="{ error: v$.accountName.$error }">
{{ $t('CREATE_ACCOUNT.FORM.NAME.LABEL') }} {{ $t('CREATE_ACCOUNT.FORM.NAME.LABEL') }}
<input <input
v-model.trim="accountName" v-model="accountName"
type="text" type="text"
:placeholder="$t('CREATE_ACCOUNT.FORM.NAME.PLACEHOLDER')" :placeholder="$t('CREATE_ACCOUNT.FORM.NAME.PLACEHOLDER')"
@input="v$.accountName.$touch" @input="v$.accountName.$touch"
@@ -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',
@@ -7,6 +7,7 @@ import NotificationBell from './NotificationBell.vue';
import wootConstants from 'dashboard/constants/globals'; import wootConstants from 'dashboard/constants/globals';
import { frontendURL } from 'dashboard/helper/URLHelper'; import { frontendURL } from 'dashboard/helper/URLHelper';
import { ACCOUNT_EVENTS } from '../../../helper/AnalyticsHelper/events'; import { ACCOUNT_EVENTS } from '../../../helper/AnalyticsHelper/events';
import { useTrack } from 'dashboard/composables';
export default { export default {
components: { components: {
@@ -42,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() {
@@ -60,7 +65,7 @@ export default {
window.$chatwoot.toggle(); window.$chatwoot.toggle();
}, },
openNotificationPanel() { openNotificationPanel() {
this.$track(ACCOUNT_EVENTS.OPENED_NOTIFICATIONS); useTrack(ACCOUNT_EVENTS.OPENED_NOTIFICATIONS);
this.$emit('openNotificationPanel'); this.$emit('openNotificationPanel');
}, },
}, },
@@ -95,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,13 +49,11 @@ export default {
default: false, default: false,
}, },
}, },
emits: ['addLabel', 'toggleAccounts'],
computed: { computed: {
...mapGetters({ ...mapGetters({
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount', isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
}), }),
hasSecondaryMenu() {
return this.menuConfig.menuItems && this.menuConfig.menuItems.length;
},
contactCustomViews() { contactCustomViews() {
return this.customViews.filter(view => view.filter_type === 'contact'); return this.customViews.filter(view => view.filter_type === 'contact');
}, },
@@ -243,10 +241,9 @@ export default {
<template> <template>
<div <div
v-if="hasSecondaryMenu"
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"
@@ -261,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>
@@ -58,7 +58,7 @@ export default {
active-class="active" active-class="active"
> >
<li <li
class="font-medium h-7 my-1 hover:bg-slate-25 hover:text-bg-50 flex items-center px-2 rounded-md dark:hover:bg-slate-800" class="h-7 my-1 hover:bg-slate-25 hover:text-bg-50 flex items-center px-2 rounded-md dark:hover:bg-slate-800"
:class="{ :class="{
'bg-woot-25 dark:bg-slate-800': isActive, 'bg-woot-25 dark:bg-slate-800': isActive,
'text-ellipsis overflow-hidden whitespace-nowrap max-w-full': 'text-ellipsis overflow-hidden whitespace-nowrap max-w-full':
@@ -105,7 +105,7 @@ export default {
</span> </span>
<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 font-medium 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=" :class="
isCountZero isCountZero
? `text-slate-300 dark:text-slate-500` ? `text-slate-300 dark:text-slate-500`
@@ -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();
@@ -102,7 +103,7 @@ export default {
}, },
isInboxSettings() { isInboxSettings() {
return ( return (
this.$store.state.route.name === 'settings_inbox_show' && this.$route.name === 'settings_inbox_show' &&
this.menuItem.toStateName === 'settings_inbox_list' this.menuItem.toStateName === 'settings_inbox_list'
); );
}, },
@@ -208,7 +209,7 @@ export default {
</div> </div>
<router-link <router-link
v-else v-else
class="flex items-center p-2 m-0 text-sm font-medium leading-4 rounded-lg text-slate-700 dark:text-slate-100 hover:bg-slate-25 dark:hover:bg-slate-800" class="flex items-center p-2 m-0 text-sm leading-4 rounded-lg text-slate-700 dark:text-slate-100 hover:bg-slate-25 dark:hover:bg-slate-800"
:class="computedClass" :class="computedClass"
:to="menuItem && menuItem.toState" :to="menuItem && menuItem.toState"
> >
@@ -220,7 +221,7 @@ export default {
{{ $t(`SIDEBAR.${menuItem.label}`) }} {{ $t(`SIDEBAR.${menuItem.label}`) }}
<span <span
v-if="showChildCount(menuItem.count)" v-if="showChildCount(menuItem.count)"
class="px-1 py-0 mx-1 font-medium rounded-md text-xxs" class="px-1 py-0 mx-1 rounded-md text-xxs"
:class="{ :class="{
'text-slate-300 dark:text-slate-600': isCountZero && !isActiveView, 'text-slate-300 dark:text-slate-600': isCountZero && !isActiveView,
'text-slate-600 dark:text-slate-50': !isCountZero && !isActiveView, 'text-slate-600 dark:text-slate-50': !isCountZero && !isActiveView,
@@ -235,7 +236,7 @@ export default {
v-if="menuItem.beta" v-if="menuItem.beta"
data-view-component="true" data-view-component="true"
label="Beta" label="Beta"
class="inline-block px-1 mx-1 font-medium leading-4 text-green-500 border border-green-400 rounded-lg text-xxs" class="inline-block px-1 mx-1 leading-4 text-green-500 border border-green-400 rounded-lg text-xxs"
> >
{{ $t('SIDEBAR.BETA') }} {{ $t('SIDEBAR.BETA') }}
</span> </span>
@@ -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,28 +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 VTooltip from 'v-tooltip';
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: '',
@@ -30,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 VTooltip from 'v-tooltip';
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(VTooltip, {
defaultHtml: 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>
`; `;
@@ -0,0 +1,24 @@
<script setup>
import { useMapGetter } from 'dashboard/composables/store';
defineProps({
content: {
type: String,
default: '',
},
});
const isRTL = useMapGetter('accounts/isRTL');
</script>
<template>
<div
class="overflow-hidden whitespace-nowrap text-ellipsis"
:class="{ 'text-right': isRTL }"
>
<slot v-if="$slots.default || content">
<template v-if="content">{{ content }}</template>
</slot>
<span v-else class="text-slate-300 dark:text-slate-700"> --- </span>
</div>
</template>
@@ -0,0 +1,117 @@
<script setup>
import { computed } from 'vue';
const props = defineProps({
table: {
type: Object,
required: true,
},
});
const getFormattedPages = (start, end) => {
const formatter = new Intl.NumberFormat(navigator.language);
return Array.from({ length: end - start + 1 }, (_, i) =>
formatter.format(start + i)
);
};
const currentPage = computed(() => {
return props.table.getState().pagination.pageIndex + 1;
});
const totalPages = computed(() => {
return props.table.getPageCount();
});
const visiblePages = computed(() => {
if (totalPages.value <= 3) return getFormattedPages(1, totalPages.value);
if (currentPage.value === 1) return getFormattedPages(1, 3);
if (currentPage.value === totalPages.value) {
return getFormattedPages(totalPages.value - 2, totalPages.value);
}
return getFormattedPages(currentPage.value - 1, currentPage.value + 1);
});
const total = computed(() => {
return props.table.getRowCount();
});
const start = computed(() => {
const { pagination } = props.table.getState();
return pagination.pageIndex * pagination.pageSize + 1;
});
const end = computed(() => {
const { pagination } = props.table.getState();
return Math.min(
(pagination.pageIndex + 1) * pagination.pageSize,
total.value
);
});
</script>
<template>
<div class="flex items-center justify-between">
<div class="flex flex-1 items-center justify-between">
<div>
<p class="text-sm text-gray-700">
{{ $t('REPORT.PAGINATION.RESULTS', { start, end, total }) }}
</p>
</div>
<nav class="isolate inline-flex gap-1">
<woot-button
:disabled="!table.getCanPreviousPage()"
variant="clear"
class="size-8 flex items-center border border-slate-50"
color-scheme="secondary"
@click="table.setPageIndex(0)"
>
<span class="i-lucide-chevrons-left size-3" aria-hidden="true" />
</woot-button>
<woot-button
variant="clear"
class="size-8 flex items-center border border-slate-50"
color-scheme="secondary"
:disabled="!table.getCanPreviousPage()"
@click="table.previousPage()"
>
<span class="i-lucide-chevron-left size-3" aria-hidden="true" />
</woot-button>
<woot-button
v-for="page in visiblePages"
:key="page"
variant="clear"
class="size-8 flex items-center justify-center border text-xs leading-none text-center"
:class="page == currentPage ? 'border-woot-500' : 'border-slate-50'"
color-scheme="secondary"
@click="table.setPageIndex(page - 1)"
>
<div
class="text-center"
:class="{ 'text-woot-500': page == currentPage }"
>
{{ page }}
</div>
</woot-button>
<woot-button
:disabled="!table.getCanNextPage()"
variant="clear"
class="size-8 flex items-center border border-slate-50"
color-scheme="secondary"
@click="table.nextPage()"
>
<span class="i-lucide-chevron-right size-3" aria-hidden="true" />
</woot-button>
<woot-button
:disabled="!table.getCanNextPage()"
variant="clear"
class="size-8 flex items-center border border-slate-50"
color-scheme="secondary"
@click="table.setPageIndex(table.getPageCount() - 1)"
>
<span class="i-lucide-chevrons-right size-3" aria-hidden="true" />
</woot-button>
</nav>
</div>
</div>
</template>
@@ -0,0 +1,18 @@
<script setup>
defineProps({
header: {
type: Object,
required: true,
},
});
const sortIconMap = {
default: 'i-lucide-chevrons-up-down',
asc: 'i-lucide-chevron-up',
desc: 'i-lucide-chevron-down',
};
</script>
<template>
<span :class="sortIconMap[header.column.getIsSorted() || 'default']" />
</template>
@@ -0,0 +1,65 @@
<script setup>
import { FlexRender } from '@tanstack/vue-table';
import SortButton from './SortButton.vue';
defineProps({
table: {
type: Object,
required: true,
},
fixed: {
type: Boolean,
default: false,
},
});
</script>
<template>
<table :class="{ 'table-fixed': fixed }">
<thead
class="sticky top-0 z-10 border-b border-slate-50 dark:border-slate-800 bg-slate-25 dark:bg-slate-800"
>
<tr v-for="headerGroup in table.getHeaderGroups()" :key="headerGroup.id">
<th
v-for="header in headerGroup.headers"
:key="header.id"
:style="{
width: `${header.getSize()}px`,
}"
class="text-left py-3 px-5 dark:bg-slate-800 text-slate-800 dark:text-slate-200 font-normal text-xs"
@click="header.column.getCanSort() && header.column.toggleSorting()"
>
<div
v-if="!header.isPlaceholder"
class="flex place-items-center gap-1"
>
<FlexRender
:render="header.column.columnDef.header"
:props="header.getContext()"
/>
<SortButton v-if="header.column.getCanSort()" :header="header" />
</div>
</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-25 dark:divide-slate-900">
<tr
v-for="row in table.getRowModel().rows"
:key="row.id"
class="hover:bg-slate-25 dark:hover:bg-slate-800"
>
<td
v-for="cell in row.getVisibleCells()"
:key="cell.id"
class="py-2 px-5"
>
<FlexRender
:render="cell.column.columnDef.cell"
:props="cell.getContext()"
/>
</td>
</tr>
</tbody>
</table>
</template>
@@ -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];
@@ -50,7 +51,7 @@ export default {
}, },
methods: { methods: {
onClick(e) { onClick(e) {
this.$emit('click', e); this.$emit('primaryAction', e);
}, },
onClickClose(e) { onClickClose(e) {
this.$emit('close', e); this.$emit('close', e);
@@ -61,7 +62,7 @@ export default {
<template> <template>
<div <div
class="flex items-center justify-center h-12 gap-4 px-4 py-3 text-xs text-white banner dark:text-white" class="flex items-center justify-center h-12 gap-4 px-4 py-3 text-xs text-white banner dark:text-white woot-banner"
:class="bannerClasses" :class="bannerClasses"
> >
<span class="banner-message"> <span class="banner-message">
@@ -1,43 +1,38 @@
<script> <script setup>
export default { import { ref, computed, onMounted, nextTick, defineEmits } from 'vue';
props: {
x: { const { x, y } = defineProps({
type: Number, x: { type: Number, default: 0 },
default: 0, y: { type: Number, default: 0 },
}, });
y: { const emit = defineEmits(['close']);
type: Number,
default: 0, const left = ref(x);
}, const top = ref(y);
},
data() { const style = computed(() => ({
return { top: top.value + 'px',
left: this.x, left: left.value + 'px',
top: this.y, }));
show: false,
}; const target = ref();
}, onMounted(() => {
computed: { nextTick(() => {
style() { target.value.focus();
return { });
top: this.top + 'px', });
left: this.left + 'px',
};
},
},
mounted() {
this.$nextTick(() => this.$el.focus());
},
};
</script> </script>
<template> <template>
<div <Teleport to="body">
class="fixed outline-none z-[9999] cursor-pointer" <div
:style="style" ref="target"
tabindex="0" class="fixed outline-none z-[9999] cursor-pointer"
@blur="$emit('close')" :style="style"
> tabindex="0"
<slot /> @blur="emit('close')"
</div> >
<slot />
</div>
</Teleport>
</template> </template>
@@ -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
@@ -1,5 +1,5 @@
<script> <script>
import DatePicker from 'vue2-datepicker'; import DatePicker from 'vue-datepicker-next';
export default { export default {
components: { DatePicker }, components: { DatePicker },
props: { props: {
@@ -16,6 +16,7 @@ export default {
default: () => [], default: () => [],
}, },
}, },
emits: ['change'],
methods: { methods: {
handleChange(value) { handleChange(value) {
this.$emit('change', value); this.$emit('change', value);
@@ -1,6 +1,6 @@
<script> <script>
import addDays from 'date-fns/addDays'; import addDays from 'date-fns/addDays';
import DatePicker from 'vue2-datepicker'; import DatePicker from 'vue-datepicker-next';
export default { export default {
components: { DatePicker }, components: { DatePicker },
props: { props: {
@@ -17,6 +17,7 @@ export default {
default: [], default: [],
}, },
}, },
emits: ['change'],
methods: { methods: {
handleChange(value) { handleChange(value) {
@@ -18,7 +18,6 @@ defineProps({
<template> <template>
<button <button
class="inline-flex relative items-center p-1.5 w-fit h-8 gap-1.5 rounded-lg hover:bg-slate-50 dark:hover:bg-slate-800 active:bg-slate-75 dark:active:bg-slate-800" class="inline-flex relative items-center p-1.5 w-fit h-8 gap-1.5 rounded-lg hover:bg-slate-50 dark:hover:bg-slate-800 active:bg-slate-75 dark:active:bg-slate-800"
@click="$emit('click')"
> >
<slot name="leftIcon"> <slot name="leftIcon">
<fluent-icon <fluent-icon
@@ -38,15 +38,19 @@ const props = defineProps({
}, },
}); });
const emit = defineEmits(['onSearch']); const emit = defineEmits(['onSearch', 'select', 'removeFilter']);
const searchTerm = ref(''); const searchTerm = ref('');
const onSearch = debounce(value => { const debouncedEmit = debounce(value => {
searchTerm.value = value;
emit('onSearch', value); emit('onSearch', value);
}, 300); }, 300);
const onSearch = value => {
searchTerm.value = value;
debouncedEmit();
};
const filteredListItems = computed(() => { const filteredListItems = computed(() => {
if (!searchTerm.value) return props.listItems; if (!searchTerm.value) return props.listItems;
return picoSearch(props.listItems, searchTerm.value, ['name']); return picoSearch(props.listItems, searchTerm.value, ['name']);
@@ -80,11 +84,11 @@ 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"
@click="$emit('removeFilter')" @remove="$emit('removeFilter')"
/> />
</slot> </slot>
<slot name="listItem"> <slot name="listItem">
@@ -103,7 +107,7 @@ const shouldShowEmptyState = computed(() => {
:button-text="item.name" :button-text="item.name"
:icon="item.icon" :icon="item.icon"
:icon-color="item.iconColor" :icon-color="item.iconColor"
@click="$emit('click', item)" @click.stop.prevent="emit('select', item)"
/> />
</slot> </slot>
</div> </div>
@@ -22,10 +22,6 @@ defineProps({
<template> <template>
<button <button
class="relative inline-flex items-center justify-start w-full p-3 border-0 rounded-none first:rounded-t-xl last:rounded-b-xl h-11 hover:bg-slate-50 dark:hover:bg-slate-700 active:bg-slate-75 dark:active:bg-slate-800" class="relative inline-flex items-center justify-start w-full p-3 border-0 rounded-none first:rounded-t-xl last:rounded-b-xl h-11 hover:bg-slate-50 dark:hover:bg-slate-700 active:bg-slate-75 dark:active:bg-slate-800"
@click.stop.prevent="$emit('click')"
@mouseenter="$emit('mouseenter')"
@mouseleave="$emit('mouseleave')"
@focus="$emit('focus')"
> >
<div class="inline-flex items-center gap-3 overflow-hidden"> <div class="inline-flex items-center gap-3 overflow-hidden">
<fluent-icon <fluent-icon
@@ -1,9 +1,6 @@
<script setup> <script setup>
import { defineEmits, defineModel } from 'vue';
defineProps({ defineProps({
inputValue: {
type: String,
default: '',
},
inputPlaceholder: { inputPlaceholder: {
type: String, type: String,
default: '', default: '',
@@ -13,6 +10,13 @@ defineProps({
default: false, default: false,
}, },
}); });
const emit = defineEmits(['remove']);
const value = defineModel({
type: String,
default: '',
});
</script> </script>
<template> <template>
@@ -26,21 +30,20 @@ defineProps({
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"
class="!px-1 !py-1.5" class="!px-1 !py-1.5"
@click="$emit('click')" @click="emit('remove')"
> >
{{ $t('REPORT.FILTER_ACTIONS.CLEAR_FILTER') }} {{ $t('REPORT.FILTER_ACTIONS.CLEAR_FILTER') }}
</woot-button> </woot-button>
@@ -44,6 +44,7 @@ export default {
default: '', default: '',
}, },
}, },
emits: ['remove'],
computed: { computed: {
textColor() { textColor() {
if (this.variant === 'smooth') return ''; if (this.variant === 'smooth') return '';
@@ -74,7 +75,7 @@ export default {
}, },
methods: { methods: {
onClick() { onClick() {
this.$emit('click', this.title); this.$emit('remove', this.title);
}, },
}, },
}; };
@@ -1,12 +1,14 @@
<script> <script>
export default { export default {
props: { props: {
value: { 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('input', !this.value); this.$emit('update:modelValue', !this.modelValue);
this.$emit('input', !this.modelValue);
}, },
}, },
}; };
@@ -16,12 +18,12 @@ export default {
<button <button
type="button" type="button"
class="toggle-button p-0" class="toggle-button p-0"
:class="{ active: value, small: size === 'small' }" :class="{ active: modelValue, small: size === 'small' }"
role="switch" role="switch"
:aria-checked="value.toString()" :aria-checked="modelValue.toString()"
@click="onClick" @click="onClick"
> >
<span aria-hidden="true" :class="{ active: value }" /> <span aria-hidden="true" :class="{ active: modelValue }" />
</button> </button>
</template> </template>
@@ -1,91 +0,0 @@
export default {
name: 'WootTabs',
props: {
index: {
type: Number,
default: 0,
},
border: {
type: Boolean,
default: true,
},
},
data() {
return { hasScroll: false };
},
created() {
window.addEventListener('resize', this.computeScrollWidth);
},
beforeDestroy() {
window.removeEventListener('resize', this.computeScrollWidth);
},
mounted() {
this.computeScrollWidth();
},
methods: {
computeScrollWidth() {
const tabElement = this.$el.getElementsByClassName('tabs')[0];
this.hasScroll = tabElement.scrollWidth > tabElement.clientWidth;
},
onScrollClick(direction) {
const tabElement = this.$el.getElementsByClassName('tabs')[0];
let scrollPosition = tabElement.scrollLeft;
if (direction === 'left') {
scrollPosition -= 100;
} else {
scrollPosition += 100;
}
tabElement.scrollTo({
top: 0,
left: scrollPosition,
behavior: 'smooth',
});
},
createScrollButton(createElement, direction) {
if (!this.hasScroll) {
return false;
}
return createElement(
'button',
{
class: 'tabs--scroll-button button clear secondary button--only-icon',
on: { click: () => this.onScrollClick(direction) },
},
[
createElement('fluent-icon', {
props: { icon: `chevron-${direction}`, size: 16 },
}),
]
);
},
},
render(createElement) {
const Tabs = this.$slots.default
.filter(
node =>
node.componentOptions &&
node.componentOptions.tag === 'woot-tabs-item'
)
.map((node, index) => {
const data = node.componentOptions.propsData;
data.index = index;
return node;
});
const leftButton = this.createScrollButton(createElement, 'left');
const rightButton = this.createScrollButton(createElement, 'right');
return (
<div
class={{
'tabs--container--with-border': this.border,
'tabs--container': true,
}}
>
{leftButton}
<ul class={{ tabs: true, 'tabs--with-scroll': this.hasScroll }}>
{Tabs}
</ul>
{rightButton}
</div>
);
},
};
@@ -0,0 +1,89 @@
<script setup>
// [VITE] TODO: Test this component across different screen sizes and usages
import { ref, provide, onMounted, computed } from 'vue';
import { useEventListener } from '@vueuse/core';
const props = defineProps({
index: {
type: Number,
default: 0,
},
border: {
type: Boolean,
default: true,
},
});
const emit = defineEmits(['change']);
const hasScroll = ref(false);
// TODO: We may not this internalActiveIndex, we can use activeIndex directly
// But right I'll keep it and fix it when testing the rest of the codebase
const internalActiveIndex = ref(props.index);
// Create a proxy for activeIndex using computed
const activeIndex = computed({
get: () => internalActiveIndex.value,
set: newValue => {
internalActiveIndex.value = newValue;
emit('change', newValue);
},
});
provide('activeIndex', activeIndex);
provide('updateActiveIndex', index => {
activeIndex.value = index;
});
const computeScrollWidth = () => {
// TODO: use useElementSize from vueuse
const tabElement = document.querySelector('.tabs');
if (tabElement) {
hasScroll.value = tabElement.scrollWidth > tabElement.clientWidth;
}
};
const onScrollClick = direction => {
// TODO: use useElementSize from vueuse
const tabElement = document.querySelector('.tabs');
if (tabElement) {
let scrollPosition = tabElement.scrollLeft;
scrollPosition += direction === 'left' ? -100 : 100;
tabElement.scrollTo({
top: 0,
left: scrollPosition,
behavior: 'smooth',
});
}
};
useEventListener(window, 'resize', computeScrollWidth);
onMounted(() => {
computeScrollWidth();
});
</script>
<template>
<div
:class="{ 'tabs--container--with-border': border }"
class="tabs--container"
>
<button
v-if="hasScroll"
class="tabs--scroll-button button clear secondary button--only-icon"
@click="onScrollClick('left')"
>
<fluent-icon icon="chevron-left" :size="16" />
</button>
<ul :class="{ 'tabs--with-scroll': hasScroll }" class="tabs">
<slot />
</ul>
<button
v-if="hasScroll"
class="tabs--scroll-button button clear secondary button--only-icon"
@click="onScrollClick('right')"
>
<fluent-icon icon="chevron-right" :size="16" />
</button>
</div>
</template>
@@ -1,47 +1,40 @@
<script> <script setup>
export default { import { computed, inject } from 'vue';
name: 'WootTabsItem',
props: {
index: {
type: Number,
default: 0,
},
name: {
type: String,
required: true,
},
disabled: {
type: Boolean,
default: false,
},
count: {
type: Number,
default: 0,
},
showBadge: {
type: Boolean,
default: true,
},
},
computed: { const props = defineProps({
active() { index: {
return this.index === this.$parent.index; type: Number,
}, default: 0,
getItemCount() {
return this.count;
},
}, },
name: {
methods: { type: String,
onTabClick(event) { required: true,
event.preventDefault();
if (!this.disabled) {
this.$parent.$emit('change', this.index);
}
},
}, },
disabled: {
type: Boolean,
default: false,
},
count: {
type: Number,
default: 0,
},
showBadge: {
type: Boolean,
default: true,
},
});
const activeIndex = inject('activeIndex');
const updateActiveIndex = inject('updateActiveIndex');
const active = computed(() => props.index === activeIndex.value);
const getItemCount = computed(() => props.count);
const onTabClick = event => {
event.preventDefault();
if (!props.disabled) {
updateActiveIndex(props.index);
}
}; };
</script> </script>

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