Compare commits

...
21 Commits
Author SHA1 Message Date
Shivam Mishra ce276f888a fix: remove error loader 2024-10-04 18:09:28 +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
851 changed files with 17406 additions and 31831 deletions
+2 -20
View File
@@ -1,17 +1,6 @@
module.exports = {
extends: [
'airbnb-base/legacy',
'prettier',
'plugin:vue/recommended',
'plugin:storybook/recommended',
'plugin:cypress/recommended',
],
parserOptions: {
parser: '@babel/eslint-parser',
ecmaVersion: 2020,
sourceType: 'module',
},
plugins: ['html', 'prettier', 'babel'],
extends: ['airbnb-base/legacy', 'prettier', 'plugin:vue/vue3-recommended'],
plugins: ['html', 'prettier'],
rules: {
'prettier/prettier': ['error'],
camelcase: 'off',
@@ -210,13 +199,6 @@ module.exports = {
'import/extensions': ['off'],
'no-console': 'error',
},
settings: {
'import/resolver': {
webpack: {
config: 'config/webpack/resolve.js',
},
},
},
env: {
browser: true,
node: true,
+5 -6
View File
@@ -42,7 +42,9 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
ref: ${{ github.event.pull_request.head.ref }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
@@ -53,10 +55,10 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: 20
cache: yarn
cache: 'pnpm'
- name: yarn
run: yarn install
- name: Install pnpm dependencies
run: pnpm i
- name: Strip enterprise code
run: |
@@ -69,9 +71,6 @@ jobs:
- name: Seed database
run: bundle exec rake db:schema:load
- name: yarn check-files
run: yarn install --check-files
# Run rails tests
- name: Run backend tests
run: |
+7 -3
View File
@@ -49,13 +49,17 @@ jobs:
with:
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
with:
node-version: 20
cache: yarn
cache: pnpm
- name: yarn
run: yarn install
- name: pnpm
run: pnpm install
- name: Create database
run: bundle exec rake db:create
+15 -5
View File
@@ -19,23 +19,33 @@ jobs:
with:
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
with:
node-version: 20
cache: 'yarn'
cache: 'pnpm'
- name: yarn
run: yarn install
- name: pnpm
run: pnpm install
- name: Strip enterprise code
run: |
rm -rf enterprise
rm -rf spec/enterprise
- name: setup env
run: |
cp .env.example .env
- name: Run asset compile
run: bundle exec rake assets:precompile
env:
NODE_OPTIONS: --openssl-legacy-provider
RAILS_ENV: production
- name: Size Check
run: yarn run size
run: pnpm run size
+15 -1
View File
@@ -32,6 +32,16 @@ master.key
public/uploads
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
*.swp
@@ -75,4 +85,8 @@ yalc.lock
yarn-debug.log*
.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
. "$(dirname "$0")/_/husky.sh"
# #!/bin/sh
# . "$(dirname "$0")/_/husky.sh"
# lint js and vue files
npx --no-install lint-staged
# # lint js and vue files
# npx --no-install lint-staged
# 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
# # 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
# stage rubocop changes to files
git diff --name-only --cached | xargs git add
# # stage rubocop changes to files
# 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 'foreman'
gem 'puma'
gem 'webpacker'
gem 'vite_rails'
# metrics on heroku
gem 'barnes'
@@ -204,8 +204,6 @@ group :development do
end
group :test do
# Cypress in rails.
gem 'cypress-on-rails'
# fast cleaning of database
gem 'database_cleaner'
# mock http calls
+16 -17
View File
@@ -178,8 +178,6 @@ GEM
csv (3.3.0)
csv-safe (3.3.1)
csv (~> 3.0)
cypress-on-rails (1.16.0)
rack
database_cleaner (2.0.2)
database_cleaner-active_record (>= 2, < 3)
database_cleaner-active_record (2.1.0)
@@ -222,6 +220,7 @@ GEM
railties (>= 6.1)
down (5.4.0)
addressable (~> 2.8)
dry-cli (1.1.0)
ecma-re-validator (0.4.0)
regexp_parser (~> 2.2)
elastic-apm (4.6.2)
@@ -496,14 +495,14 @@ GEM
newrelic_rpm (9.6.0)
base64
nio4r (2.7.3)
nokogiri (1.16.6)
nokogiri (1.16.7)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
nokogiri (1.16.6-arm64-darwin)
nokogiri (1.16.7-arm64-darwin)
racc (~> 1.4)
nokogiri (1.16.6-x86_64-darwin)
nokogiri (1.16.7-x86_64-darwin)
racc (~> 1.4)
nokogiri (1.16.6-x86_64-linux)
nokogiri (1.16.7-x86_64-linux)
racc (~> 1.4)
oauth (1.1.0)
oauth-tty (~> 1.0, >= 1.0.1)
@@ -557,7 +556,7 @@ GEM
pundit (2.3.0)
activesupport (>= 3.0.0)
raabro (1.4.0)
racc (1.8.0)
racc (1.8.1)
rack (2.2.9)
rack-attack (6.7.0)
rack (>= 1.0, < 4)
@@ -570,7 +569,7 @@ GEM
rack-protection (3.2.0)
base64 (>= 0.1.0)
rack (~> 2.2, >= 2.2.4)
rack-proxy (0.7.6)
rack-proxy (0.7.7)
rack
rack-test (2.1.0)
rack (>= 1.3)
@@ -709,7 +708,6 @@ GEM
activerecord (>= 4)
activesupport (>= 4)
selectize-rails (0.12.6)
semantic_range (3.0.0)
sentry-rails (5.19.0)
railties (>= 5.0)
sentry-ruby (~> 5.19.0)
@@ -800,6 +798,13 @@ GEM
activemodel (>= 3.2)
mail (~> 2.5)
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)
rack (>= 2.0.9)
web-console (4.2.1)
@@ -814,11 +819,6 @@ GEM
addressable (>= 2.8.0)
crack (>= 0.3.2)
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)
websocket-driver (0.7.6)
websocket-extensions (>= 0.1.0)
@@ -827,7 +827,7 @@ GEM
working_hours (1.4.1)
activesupport (>= 3.2)
tzinfo
zeitwerk (2.6.16)
zeitwerk (2.6.17)
PLATFORMS
arm64-darwin-20
@@ -862,7 +862,6 @@ DEPENDENCIES
climate_control
commonmarker
csv-safe
cypress-on-rails
database_cleaner
ddtrace
debug (~> 1.8)
@@ -960,10 +959,10 @@ DEPENDENCIES
tzinfo-data
uglifier
valid_email2
vite_rails
web-console (>= 4.2.1)
web-push (>= 3.0.1)
webmock
webpacker
wisper (= 2.0.0)
working_hours
+1 -1
View File
@@ -1,4 +1,4 @@
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
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
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
+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/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.
<p>
@@ -98,7 +101,7 @@ Chatwoot now supports 1-Click deployment to DigitalOcean as a kubernetes app.
### 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
+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.js
//= link administrate-field-active_storage/application.css
//= link dashboardChart.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: all $swift-ease-out-duration $swift-ease-out-timing-function !default;
// Ionicons
$ionicons-font-path: '~ionicons/fonts';
// Transitions
$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')
'v3app'
else
'application'
'dashboard'
end
end
+34 -9
View File
@@ -1,6 +1,5 @@
<script>
import { mapGetters } from 'vuex';
import router from '../dashboard/routes';
import AddAccountModal from '../dashboard/components/layout/sidebarComponents/AddAccountModal.vue';
import LoadingState from './components/widgets/LoadingState.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 PendingEmailVerificationBanner from './components/app/PendingEmailVerificationBanner.vue';
import vueActionCable from './helper/actionCable';
import { useRouter } from 'vue-router';
import { useStore } from 'dashboard/composables/store';
import WootSnackbarBox from './components/SnackbarContainer.vue';
import { setColorTheme } from './helper/themeHelper';
import { isOnOnboardingView } from 'v3/helpers/RouteHelper';
@@ -31,6 +32,12 @@ export default {
UpgradeBanner,
PendingEmailVerificationBanner,
},
setup() {
const router = useRouter();
const store = useStore();
return { router, store };
},
data() {
return {
showAddAccountModal: false,
@@ -38,7 +45,6 @@ export default {
reconnectService: null,
};
},
computed: {
...mapGetters({
getAccount: 'accounts/getAccount',
@@ -74,7 +80,7 @@ export default {
this.listenToThemeChanges();
this.setLocale(window.chatwootConfig.selectedLocale);
},
beforeDestroy() {
unmounted() {
if (this.reconnectService) {
this.reconnectService.disconnect();
}
@@ -100,8 +106,9 @@ export default {
const { pubsub_token: pubsubToken } = this.currentUser || {};
this.setLocale(locale);
this.latestChatwootVersion = latestChatwootVersion;
vueActionCable.init(pubsubToken);
this.reconnectService = new ReconnectService(this.$store, router);
vueActionCable.init(this.store, pubsubToken);
this.reconnectService = new ReconnectService(this.store, this.router);
window.reconnectService = this.reconnectService;
verifyServiceWorkerExistence(registration =>
registration.pushManager.getSubscription().then(subscription => {
@@ -129,9 +136,11 @@ export default {
<PaymentPendingBanner v-if="hideOnOnboardingView" />
<UpgradeBanner />
</template>
<transition name="fade" mode="out-in">
<router-view />
</transition>
<router-view v-slot="{ Component }">
<transition name="fade" mode="out-in">
<component :is="Component" />
</transition>
</router-view>
<AddAccountModal :show="showAddAccountModal" :has-accounts="hasAccounts" />
<WootSnackbarBox />
<NetworkNotification />
@@ -141,6 +150,22 @@ export default {
<style lang="scss">
@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 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);
}
.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 {
transition: all 0.3s var(--ease-in-cubic);
}
@@ -65,7 +48,8 @@
.menu-slide-enter-active,
.menu-slide-leave-active {
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);
}
@@ -1,4 +1,4 @@
@import '~vue2-datepicker/scss/index';
@import 'vue-datepicker-next/scss/index';
.date-picker {
// 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-errors {
@@ -1,10 +1,11 @@
// scss-lint:disable SpaceAfterPropertyColon
@import 'shared/assets/fonts/inter';
// Inter,
html,
body {
font-family:
'PlusJakarta',
'Inter',
-apple-system,
system-ui,
BlinkMacSystemFont,
@@ -23,7 +24,7 @@ body {
}
.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 {
@apply fixed ltr:right-2 rtl:left-2 top-2 flex flex-row;
@@ -1,5 +1,5 @@
@import '~dashboard/assets/scss/variables';
@import '~widget/assets/scss/mixins';
@import 'dashboard/assets/scss/variables';
@import 'widget/assets/scss/mixins';
$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: all $swift-ease-out-duration $swift-ease-out-function !default;
// Ionicons
$ionicons-font-path: '~ionicons/fonts';
// Transitions
$transition-ease-in: all 0.250s ease-in;
@@ -2,7 +2,6 @@
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';
@import 'shared/assets/fonts/plus-jakarta';
@import 'shared/assets/fonts/InterDisplay/inter-display';
@import 'shared/assets/fonts/inter';
@@ -34,10 +33,13 @@
@import 'plugins/multiselect';
@import 'plugins/dropdown';
@import '~shared/assets/stylesheets/ionicons';
.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 {
@@ -45,7 +47,6 @@
}
@layer base {
// scss-lint:disable PropertySortOrder
:root {
--color-amber-25: 254 253 251;
@@ -222,6 +222,7 @@
.multiselect__input {
@apply h-[2.875rem] min-h-[2.875rem];
margin-bottom: 0px !important;
}
.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 { defineEmits } from 'vue';
export default {
components: {
EmojiOrIcon,
defineProps({
title: {
type: String,
required: true,
},
props: {
title: {
type: String,
required: true,
},
compact: {
type: Boolean,
default: false,
},
icon: {
type: String,
default: '',
},
emoji: {
type: String,
default: '',
},
isOpen: {
type: Boolean,
default: true,
},
compact: {
type: Boolean,
default: false,
},
icon: {
type: String,
default: '',
},
emoji: {
type: String,
default: '',
},
isOpen: {
type: Boolean,
default: true,
},
});
const emit = defineEmits(['toggle']);
const onToggle = () => {
emit('toggle');
};
</script>
@@ -34,7 +36,7 @@ export default {
<div class="-mt-px text-sm">
<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"
@click="$emit('click')"
@click.stop="onToggle"
>
<div class="flex justify-between mb-0.5">
<EmojiOrIcon class="inline-block w-5" :icon="icon" :emoji="emoji" />
@@ -16,7 +16,6 @@ export default {
<template>
<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"
@click="$emit('click')"
>
<img :src="src" :alt="title" class="w-1/2 my-4 mx-auto" />
<h3
File diff suppressed because it is too large Load Diff
@@ -61,7 +61,7 @@ const hasAppliedFiltersOrActiveFolders = computed(() => {
</span>
</div>
<div class="flex items-center gap-1">
<div v-if="hasAppliedFilters && !hasActiveFolders">
<template v-if="hasAppliedFilters && !hasActiveFolders">
<woot-button
v-tooltip.top-end="$t('FILTER.CUSTOM_VIEWS.ADD.SAVE_BUTTON')"
size="tiny"
@@ -78,8 +78,8 @@ const hasAppliedFiltersOrActiveFolders = computed(() => {
icon="dismiss-circle"
@click="emit('resetFilters')"
/>
</div>
<div v-if="hasActiveFolders">
</template>
<template v-if="hasActiveFolders">
<woot-button
v-tooltip.top-end="$t('FILTER.CUSTOM_VIEWS.EDIT.EDIT_BUTTON')"
size="tiny"
@@ -96,7 +96,7 @@ const hasAppliedFiltersOrActiveFolders = computed(() => {
icon="delete"
@click="emit('deleteFolders')"
/>
</div>
</template>
<woot-button
v-else
v-tooltip.right="$t('FILTER.TOOLTIP_LABEL')"
@@ -108,7 +108,7 @@ const hasAppliedFiltersOrActiveFolders = computed(() => {
/>
<ConversationBasicFilter
v-if="!hasAppliedFiltersOrActiveFolders"
@changeFilter="onBasicFilterChange"
@change-filter="onBasicFilterChange"
/>
</div>
</div>
+23 -25
View File
@@ -1,5 +1,7 @@
<script>
import 'highlight.js/styles/default.css';
import 'highlight.js/lib/common';
import { copyTextToClipboard } from 'shared/helpers/clipboard';
import { useAlert } from 'dashboard/composables';
@@ -28,14 +30,28 @@ export default {
return JSON.stringify({
title: this.codepenTitle,
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: {
async onCopy(e) {
e.preventDefault();
await copyTextToClipboard(this.script);
await copyTextToClipboard(this.scrubbedScript);
useAlert(this.$t('COMPONENTS.CODE.COPY_SUCCESSFUL'));
},
},
@@ -43,42 +59,24 @@ export default {
</script>
<template>
<div class="code--container">
<div class="code--action-area">
<div class="relative text-left">
<div class="top-1.5 absolute right-1.5 flex items-center gap-1">
<form
v-if="enableCodePen"
class="code--codeopen-form"
class="flex items-center"
action="https://codepen.io/pen/define"
method="POST"
target="_blank"
>
<input type="hidden" name="data" :value="codepenScriptValue" />
<button type="submit" class="button secondary tiny">
{{ $t('COMPONENTS.CODE.CODEPEN') }}
</button>
</form>
<button class="button secondary tiny" @click="onCopy">
<button type="button" class="button secondary tiny" @click="onCopy">
{{ $t('COMPONENTS.CODE.BUTTON_TEXT') }}
</button>
</div>
<highlightjs v-if="script" :language="lang" :code="script" />
<highlightjs v-if="script" :language="lang" :code="scrubbedScript" />
</div>
</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',
'markAsUnread',
'assignPriority',
'isConversationSelected',
],
props: {
source: {
@@ -36,10 +37,6 @@ export default {
type: [String, Number],
default: 0,
},
isConversationSelected: {
type: Function,
default: () => {},
},
showAssignee: {
type: Boolean,
default: false,
@@ -59,14 +56,14 @@ export default {
:selected="isConversationSelected(source.id)"
:show-assignee="showAssignee"
enable-context-menu
@selectConversation="selectConversation"
@deSelectConversation="deSelectConversation"
@assignAgent="assignAgent"
@assignTeam="assignTeam"
@assignLabel="assignLabels"
@updateConversationStatus="updateConversationStatus"
@contextMenuToggle="toggleContextMenu"
@markAsUnread="markAsUnread"
@assignPriority="assignPriority"
@select-conversation="selectConversation"
@de-select-conversation="deSelectConversation"
@assign-agent="assignAgent"
@assign-team="assignTeam"
@assign-label="assignLabels"
@update-conversation-status="updateConversationStatus"
@context-menu-toggle="toggleContextMenu"
@mark-as-unread="markAsUnread"
@assign-priority="assignPriority"
/>
</template>
@@ -7,6 +7,7 @@ import HelperTextPopup from 'dashboard/components/ui/HelperTextPopup.vue';
import { isValidURL } from '../helper/URLHelper';
import { getRegexp } from 'shared/helpers/Validators';
import { useVuelidate } from '@vuelidate/core';
import { emitter } from 'shared/helpers/mitt';
const DATE_FORMAT = 'yyyy-MM-dd';
@@ -30,6 +31,7 @@ export default {
attributeKey: { type: String, required: true },
contactId: { type: Number, default: null },
},
emits: ['update', 'delete', 'copy'],
setup() {
return { v$: useVuelidate() };
},
@@ -135,10 +137,10 @@ export default {
},
mounted() {
this.editedValue = this.formattedValue;
this.$emitter.on(BUS_EVENTS.FOCUS_CUSTOM_ATTRIBUTE, this.onFocusAttribute);
emitter.on(BUS_EVENTS.FOCUS_CUSTOM_ATTRIBUTE, this.onFocusAttribute);
},
destroyed() {
this.$emitter.off(BUS_EVENTS.FOCUS_CUSTOM_ATTRIBUTE, this.onFocusAttribute);
unmounted() {
emitter.off(BUS_EVENTS.FOCUS_CUSTOM_ATTRIBUTE, this.onFocusAttribute);
},
methods: {
onFocusAttribute(focusAttributeKey) {
@@ -321,7 +323,7 @@ export default {
'CUSTOM_ATTRIBUTES.FORM.ATTRIBUTE_TYPE.LIST.SEARCH_INPUT_PLACEHOLDER'
)
"
@click="onUpdateListValue"
@select="onUpdateListValue"
/>
</div>
</div>
@@ -18,6 +18,7 @@ const shouldShowContent = computed(
);
</script>
<!-- eslint-disable-next-line vue/no-root-v-if -->
<template>
<div v-if="shouldShowContent">
<slot />
@@ -1,10 +1,11 @@
<script>
import DatePicker from 'vue2-datepicker';
import DatePicker from 'vue-datepicker-next';
export default {
components: {
DatePicker,
},
emits: ['close', 'chooseTime'],
data() {
return {
@@ -43,15 +44,18 @@ export default {
<template>
<div class="flex flex-col">
<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
v-model="snoozeTime"
v-model:value="snoozeTime"
type="datetime"
inline
input-class="mx-input reset-base"
:lang="lang"
:disabled-date="disabledDate"
:disabled-time="disabledTime"
:popup-style="{ width: '100%' }"
/>
<div class="flex flex-row justify-end w-full gap-2 px-0 py-2">
<woot-button variant="clear" @click.prevent="onClose">
@@ -64,9 +68,3 @@ export default {
</form>
</div>
</template>
<style lang="scss" scoped>
.modal-content {
@apply pt-2 px-5 pb-6;
}
</style>
@@ -1,32 +1,26 @@
<script>
export default {
props: {
options: {
type: Object,
default: () => ({ root: document, rootMargin: '100px 0 100px 0)' }),
},
<script setup>
import { ref, defineEmits } from 'vue';
import { useIntersectionObserver } from '@vueuse/core';
const { options } = defineProps({
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() {
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);
},
},
};
options
);
</script>
<template>
+63 -72
View File
@@ -1,78 +1,69 @@
<script>
export default {
props: {
closeOnBackdropClick: {
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',
};
<script setup>
// [TODO] Use Teleport to move the modal to the end of the body
import { ref, computed, defineEmits, onMounted } from 'vue';
import { useEventListener } from '@vueuse/core';
return `modal-mask skip-context-menu ${
modalClassNameMap[this.modalType] || ''
}`;
},
},
mounted() {
document.addEventListener('keydown', e => {
if (this.show && e.code === 'Escape') {
this.onClose();
}
});
const { modalType, closeOnBackdropClick, onClose } = defineProps({
closeOnBackdropClick: { type: Boolean, default: true },
showCloseButton: { type: Boolean, default: true },
onClose: { type: Function, required: true },
fullWidth: { type: Boolean, default: false },
modalType: { type: String, default: 'centered' },
size: { type: String, default: '' },
});
document.body.addEventListener('mouseup', this.onMouseUp);
},
beforeDestroy() {
document.body.removeEventListener('mouseup', this.onMouseUp);
},
methods: {
handleMouseDown() {
this.mousedDownOnBackdrop = true;
},
close() {
this.onClose();
},
onMouseUp() {
if (this.mousedDownOnBackdrop) {
this.mousedDownOnBackdrop = false;
if (this.closeOnBackdropClick) {
this.onClose();
}
}
},
},
const emit = defineEmits(['close']);
const show = defineModel('show', { type: Boolean, default: false });
const modalClassName = computed(() => {
const modalClassNameMap = {
centered: '',
'right-aligned': 'right-aligned',
};
return `modal-mask skip-context-menu ${modalClassNameMap[modalType] || ''}`;
});
// [TODO] Revisit this logic to use outside click directive
const mousedDownOnBackdrop = ref(false);
const handleMouseDown = () => {
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>
<template>
@@ -1,7 +1,7 @@
<script setup>
import { ref, computed, onBeforeUnmount } from 'vue';
import { useI18n } from 'dashboard/composables/useI18n';
import { useRoute } from 'dashboard/composables/route';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import { useEmitter } from 'dashboard/composables/emitter';
import { BUS_EVENTS } from 'shared/constants/busEvents';
import {
@@ -1,5 +1,6 @@
<script>
import { BUS_EVENTS } from 'shared/constants/busEvents';
import { emitter } from 'shared/helpers/mitt';
export default {
props: {
@@ -10,7 +11,7 @@ export default {
},
methods: {
onMenuItemClick() {
this.$emitter.emit(BUS_EVENTS.TOGGLE_SIDEMENU);
emitter.emit(BUS_EVENTS.TOGGLE_SIDEMENU);
},
},
};
@@ -1,5 +1,6 @@
<script>
import WootSnackbar from './Snackbar.vue';
import { emitter } from 'shared/helpers/mitt';
export default {
components: {
@@ -19,10 +20,10 @@ export default {
},
mounted() {
this.$emitter.on('newToastMessage', this.onNewToastMessage);
emitter.on('newToastMessage', this.onNewToastMessage);
},
beforeDestroy() {
this.$emitter.off('newToastMessage', this.onNewToastMessage);
unmounted() {
emitter.off('newToastMessage', this.onNewToastMessage);
},
methods: {
onNewToastMessage({ message, action }) {
@@ -79,6 +79,7 @@ export default {
};
</script>
<!-- eslint-disable-next-line vue/no-root-v-if -->
<template>
<Banner
v-if="shouldShowBanner"
@@ -86,6 +87,6 @@ export default {
:banner-message="bannerMessage"
:action-button-label="actionButtonMessage"
has-action-button
@click="routeToBilling"
@primary-action="routeToBilling"
/>
</template>
@@ -28,6 +28,7 @@ export default {
};
</script>
<!-- eslint-disable-next-line vue/no-root-v-if -->
<template>
<Banner
v-if="shouldShowBanner"
@@ -36,6 +37,6 @@ export default {
:action-button-label="actionButtonMessage"
action-button-icon="mail"
has-action-button
@click="resendVerificationEmail"
@primary-action="resendVerificationEmail"
/>
</template>
@@ -67,6 +67,7 @@ export default {
};
</script>
<!-- eslint-disable-next-line vue/no-root-v-if -->
<template>
<Banner
v-if="shouldShowBanner"
@@ -81,6 +81,7 @@ export default {
};
</script>
<!-- eslint-disable-next-line vue/no-root-v-if -->
<template>
<Banner
v-if="shouldShowBanner"
@@ -88,6 +89,6 @@ export default {
:banner-message="bannerMessage"
:action-button-label="actionButtonMessage"
has-action-button
@click="routeToBilling"
@primary-action="routeToBilling"
/>
</template>
@@ -27,16 +27,17 @@ export default {
default: 'primary',
},
},
methods: {
onClick(e) {
this.$emit('click', e);
},
created() {
// eslint-disable-next-line
console.warn(
'[DEPRECATED] This component has been deprecated and will be removed soon. Please use v3/components/Form/Button.vue instead'
);
},
};
</script>
<template>
<button :type="type" class="button nice" :class="variant" @click="onClick">
<button :type="type" class="button nice" :class="variant">
<fluent-icon
v-if="!isLoading && icon"
class="icon"
@@ -40,11 +40,6 @@ export default {
return `button nice gap-2 ${this.buttonClass || ' '}`;
},
},
methods: {
onClick() {
this.$emit('click');
},
},
};
</script>
@@ -54,7 +49,6 @@ export default {
data-testid="submit_button"
:disabled="disabled"
:class="computedClass"
@click="onClick"
>
<fluent-icon v-if="!!iconClass" :icon="iconClass" class="icon" />
<span>{{ buttonText }}</span>
@@ -2,7 +2,7 @@
import { ref, computed } from 'vue';
import { useAlert } from 'dashboard/composables';
import { useToggle } from '@vueuse/core';
import { useI18n } from 'dashboard/composables/useI18n';
import { useI18n } from 'vue-i18n';
import { useStore, useStoreGetters } from 'dashboard/composables/store';
import { useEmitter } from 'dashboard/composables/emitter';
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 */
import AvatarUploader from './widgets/forms/AvatarUploader.vue';
import Bar from './widgets/chart/BarChart';
import Button from './ui/WootButton';
import Code from './Code';
import ColorPicker from './widgets/ColorPicker';
import Button from './ui/WootButton.vue';
import Code from './Code.vue';
import ColorPicker from './widgets/ColorPicker.vue';
import ConfirmDeleteModal from './widgets/modal/ConfirmDeleteModal.vue';
import ConfirmModal from './widgets/modal/ConfirmationModal.vue';
import ContextMenu from './ui/ContextMenu.vue';
import DeleteModal from './widgets/modal/DeleteModal.vue';
import DropdownItem from 'shared/components/ui/dropdown/DropdownItem';
import DropdownMenu from 'shared/components/ui/dropdown/DropdownMenu';
import FeatureToggle from './widgets/FeatureToggle';
import HorizontalBar from './widgets/chart/HorizontalBarChart';
import DropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
import DropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
import FeatureToggle from './widgets/FeatureToggle.vue';
import Input from './widgets/forms/Input.vue';
import PhoneInput from './widgets/forms/PhoneInput.vue';
import Label from './ui/Label';
import LoadingState from './widgets/LoadingState';
import Modal from './Modal';
import ModalHeader from './ModalHeader';
import SidemenuIcon from './SidemenuIcon';
import Spinner from 'shared/components/Spinner';
import SubmitButton from './buttons/FormSubmitButton';
import Tabs from './ui/Tabs/Tabs';
import TabsItem from './ui/Tabs/TabsItem';
import Label from './ui/Label.vue';
import LoadingState from './widgets/LoadingState.vue';
import ModalHeader from './ModalHeader.vue';
import Modal from './Modal.vue';
import SidemenuIcon from './SidemenuIcon.vue';
import Spinner from 'shared/components/Spinner.vue';
import SubmitButton from './buttons/FormSubmitButton.vue';
import Tabs from './ui/Tabs/Tabs.vue';
import TabsItem from './ui/Tabs/TabsItem.vue';
import Thumbnail from './widgets/Thumbnail.vue';
import DatePicker from './ui/DatePicker/DatePicker.vue';
const WootUIKit = {
AvatarUploader,
Bar,
Button,
Code,
ColorPicker,
ConfirmDeleteModal,
ConfirmModal,
ContextMenu,
DeleteModal,
DropdownItem,
DropdownMenu,
FeatureToggle,
HorizontalBar,
Input,
PhoneInput,
Label,
@@ -31,26 +31,29 @@ export default {
currentAccountId: 'getCurrentAccountId',
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() {
const availabilityIndex = AVAILABILITY_STATUS_KEYS.findIndex(
key => key === this.currentUserAvailability
);
return this.$t('PROFILE_SETTINGS.FORM.AVAILABILITY.STATUSES_LIST')[
availabilityIndex
];
return this.statusList[availabilityIndex];
},
currentUserAvailability() {
return this.getCurrentUserAvailability;
},
availabilityStatuses() {
return this.$t('PROFILE_SETTINGS.FORM.AVAILABILITY.STATUSES_LIST').map(
(statusLabel, index) => ({
label: statusLabel,
value: AVAILABILITY_STATUS_KEYS[index],
disabled:
this.currentUserAvailability === AVAILABILITY_STATUS_KEYS[index],
})
);
return this.statusList.map((statusLabel, index) => ({
label: statusLabel,
value: AVAILABILITY_STATUS_KEYS[index],
disabled:
this.currentUserAvailability === AVAILABILITY_STATUS_KEYS[index],
}));
},
},
@@ -129,7 +132,7 @@ export default {
<woot-switch
size="small"
class="mx-1 mt-px mb-0"
:value="currentUserAutoOffline"
:model-value="currentUserAutoOffline"
@input="updateAutoOffline"
/>
</WootDropdownItem>
@@ -2,7 +2,7 @@
import { mapGetters } from 'vuex';
import { getSidebarItems } from './config/default-sidebar';
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 SecondarySidebar from './sidebarComponents/Secondary.vue';
@@ -22,11 +22,14 @@ export default {
type: Boolean,
default: true,
},
sidebarClassName: {
type: String,
default: '',
},
},
emits: [
'toggleAccountModal',
'showAddLabelPopup',
'openNotificationPanel',
'closeKeyShortcutModal',
'openKeyShortcutModal',
],
setup(props, { emit }) {
const route = useRoute();
const router = useRouter();
@@ -159,6 +162,17 @@ export default {
) || {};
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: {
@@ -206,13 +220,12 @@ export default {
:account-id="accountId"
:menu-items="primaryMenuItems"
:active-menu-item="activePrimaryMenu.key"
@toggleAccounts="toggleAccountModal"
@openKeyShortcutModal="toggleKeyShortcutModal"
@openNotificationPanel="openNotificationPanel"
@toggle-accounts="toggleAccountModal"
@open-key-shortcut-modal="toggleKeyShortcutModal"
@open-notification-panel="openNotificationPanel"
/>
<SecondarySidebar
v-if="showSecondarySidebar"
:class="sidebarClassName"
v-if="hasSecondarySidebar"
:account-id="accountId"
:inboxes="inboxes"
:labels="labels"
@@ -221,8 +234,8 @@ export default {
:menu-config="activeSecondaryMenu"
:current-user="currentUser"
:is-on-chatwoot-cloud="isOnChatwootCloud"
@addLabel="showAddLabelPopup"
@toggleAccounts="toggleAccountModal"
@add-label="showAddLabelPopup"
@toggle-accounts="toggleAccountModal"
/>
</aside>
</template>
@@ -2,6 +2,7 @@
import { mapGetters } from 'vuex';
export default {
emits: ['toggleAccounts'],
data() {
return { showSwitchButton: false };
},
@@ -25,6 +26,7 @@ export default {
};
</script>
<!-- eslint-disable-next-line vue/no-root-v-if -->
<template>
<div
v-if="showShowCurrentAccountContext"
@@ -7,7 +7,7 @@ export default {
default: true,
},
},
emits: ['closeAccountModal', 'showCreateAccountModal'],
computed: {
...mapGetters({
accountId: 'getCurrentAccountId',
@@ -1,7 +1,7 @@
<script>
import { useVuelidate } from '@vuelidate/core';
import { required, minLength } from '@vuelidate/validators';
import { mapGetters } from 'vuex';
import { useVuelidate } from '@vuelidate/core';
import { useAlert } from 'dashboard/composables';
export default {
@@ -15,6 +15,7 @@ export default {
default: true,
},
},
emits: ['closeAccountCreateModal'],
setup() {
return { v$: useVuelidate() };
},
@@ -23,11 +24,13 @@ export default {
accountName: '',
};
},
validations: {
accountName: {
required,
minLength: minLength(1),
},
validations() {
return {
accountName: {
required,
minLength: minLength(1),
},
};
},
computed: {
...mapGetters({
@@ -76,7 +79,7 @@ export default {
<label :class="{ error: v$.accountName.$error }">
{{ $t('CREATE_ACCOUNT.FORM.NAME.LABEL') }}
<input
v-model.trim="accountName"
v-model="accountName"
type="text"
:placeholder="$t('CREATE_ACCOUNT.FORM.NAME.PLACEHOLDER')"
@input="v$.accountName.$touch"
@@ -6,6 +6,7 @@ export default {
components: {
Thumbnail,
},
emits: ['toggleMenu'],
computed: {
...mapGetters({
currentUser: 'getCurrentUser',
@@ -2,6 +2,8 @@
import { mapGetters } from 'vuex';
export default {
emits: ['openNotificationPanel'],
computed: {
...mapGetters({
notificationMetadata: 'notifications/getMeta',
@@ -17,6 +17,12 @@ export default {
default: false,
},
},
emits: [
'close',
'openKeyShortcutModal',
'toggleAccounts',
'showSupportChatWindow',
],
computed: {
...mapGetters({
currentUser: 'getCurrentUser',
@@ -7,6 +7,7 @@ import NotificationBell from './NotificationBell.vue';
import wootConstants from 'dashboard/constants/globals';
import { frontendURL } from 'dashboard/helper/URLHelper';
import { ACCOUNT_EVENTS } from '../../../helper/AnalyticsHelper/events';
import { useTrack } from 'dashboard/composables';
export default {
components: {
@@ -42,6 +43,7 @@ export default {
default: '',
},
},
emits: ['toggleAccounts', 'openNotificationPanel', 'openKeyShortcutModal'],
data() {
return {
helpDocsURL: wootConstants.DOCS_URL,
@@ -60,7 +62,7 @@ export default {
window.$chatwoot.toggle();
},
openNotificationPanel() {
this.$track(ACCOUNT_EVENTS.OPENED_NOTIFICATIONS);
useTrack(ACCOUNT_EVENTS.OPENED_NOTIFICATIONS);
this.$emit('openNotificationPanel');
},
},
@@ -95,13 +97,13 @@ export default {
open-in-new-page
:to="helpDocsURL"
/>
<NotificationBell @openNotificationPanel="openNotificationPanel" />
<AgentDetails @toggleMenu="toggleOptions" />
<NotificationBell @open-notification-panel="openNotificationPanel" />
<AgentDetails @toggle-menu="toggleOptions" />
<OptionsMenu
:show="showOptionsMenu"
@toggleAccounts="toggleAccountModal"
@showSupportChatWindow="toggleSupportChatWindow"
@openKeyShortcutModal="$emit('openKeyShortcutModal')"
@toggle-accounts="toggleAccountModal"
@show-support-chat-window="toggleSupportChatWindow"
@open-key-shortcut-modal="$emit('openKeyShortcutModal')"
@close="toggleOptions"
/>
</div>
@@ -49,13 +49,11 @@ export default {
default: false,
},
},
emits: ['addLabel', 'toggleAccounts'],
computed: {
...mapGetters({
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
}),
hasSecondaryMenu() {
return this.menuConfig.menuItems && this.menuConfig.menuItems.length;
},
contactCustomViews() {
return this.customViews.filter(view => view.filter_type === 'contact');
},
@@ -243,10 +241,9 @@ export default {
<template>
<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"
>
<AccountContext @toggleAccounts="toggleAccountModal" />
<AccountContext @toggle-accounts="toggleAccountModal" />
<transition-group
name="menu-list"
tag="ul"
@@ -261,7 +258,7 @@ export default {
v-for="menuItem in additionalSecondaryMenuItems[menuConfig.parentNav]"
:key="menuItem.key"
:menu-item="menuItem"
@addLabel="showAddLabelPopup"
@add-label="showAddLabelPopup"
/>
</transition-group>
</div>
@@ -58,7 +58,7 @@ export default {
active-class="active"
>
<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="{
'bg-woot-25 dark:bg-slate-800': isActive,
'text-ellipsis overflow-hidden whitespace-nowrap max-w-full':
@@ -105,7 +105,7 @@ export default {
</span>
<span
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="
isCountZero
? `text-slate-300 dark:text-slate-500`
@@ -22,6 +22,7 @@ export default {
default: () => ({}),
},
},
emits: ['addLabel', 'open'],
setup() {
const { isAdmin } = useAdmin();
const { isEnterprise } = useConfig();
@@ -102,7 +103,7 @@ export default {
},
isInboxSettings() {
return (
this.$store.state.route.name === 'settings_inbox_show' &&
this.$route.name === 'settings_inbox_show' &&
this.menuItem.toStateName === 'settings_inbox_list'
);
},
@@ -208,7 +209,7 @@ export default {
</div>
<router-link
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"
:to="menuItem && menuItem.toState"
>
@@ -220,7 +221,7 @@ export default {
{{ $t(`SIDEBAR.${menuItem.label}`) }}
<span
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="{
'text-slate-300 dark:text-slate-600': isCountZero && !isActiveView,
'text-slate-600 dark:text-slate-50': !isCountZero && !isActiveView,
@@ -235,7 +236,7 @@ export default {
v-if="menuItem.beta"
data-view-component="true"
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') }}
</span>
@@ -2,7 +2,6 @@ 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';
@@ -2,7 +2,7 @@ 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 FloatingVue from 'floating-vue';
import WootButton from 'dashboard/components/ui/WootButton.vue';
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
@@ -14,8 +14,8 @@ import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
import i18n from 'dashboard/i18n';
const localVue = createLocalVue();
localVue.use(VTooltip, {
defaultHtml: false,
localVue.use(FloatingVue, {
html: false,
});
localVue.use(Vuex);
localVue.use(VueI18n);
@@ -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: '',
},
},
emits: ['open', 'close'],
methods: {
onClickOpenPath() {
this.$emit('open');
@@ -38,6 +38,7 @@ export default {
default: false,
},
},
emits: ['primaryAction', 'close'],
computed: {
bannerClasses() {
const classList = [this.colorScheme];
@@ -50,7 +51,7 @@ export default {
},
methods: {
onClick(e) {
this.$emit('click', e);
this.$emit('primaryAction', e);
},
onClickClose(e) {
this.$emit('close', e);
@@ -61,7 +62,7 @@ export default {
<template>
<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"
>
<span class="banner-message">
@@ -1,43 +1,38 @@
<script>
export default {
props: {
x: {
type: Number,
default: 0,
},
y: {
type: Number,
default: 0,
},
},
data() {
return {
left: this.x,
top: this.y,
show: false,
};
},
computed: {
style() {
return {
top: this.top + 'px',
left: this.left + 'px',
};
},
},
mounted() {
this.$nextTick(() => this.$el.focus());
},
};
<script setup>
import { ref, computed, onMounted, nextTick, defineEmits } from 'vue';
const { x, y } = defineProps({
x: { type: Number, default: 0 },
y: { type: Number, default: 0 },
});
const emit = defineEmits(['close']);
const left = ref(x);
const top = ref(y);
const style = computed(() => ({
top: top.value + 'px',
left: left.value + 'px',
}));
const target = ref();
onMounted(() => {
nextTick(() => {
target.value.focus();
});
});
</script>
<template>
<div
class="fixed outline-none z-[9999] cursor-pointer"
:style="style"
tabindex="0"
@blur="$emit('close')"
>
<slot />
</div>
<Teleport to="body">
<div
ref="target"
class="fixed outline-none z-[9999] cursor-pointer"
:style="style"
tabindex="0"
@blur="emit('close')"
>
<slot />
</div>
</Teleport>
</template>
@@ -222,7 +222,7 @@ const emitDateRange = () => {
>
<CalendarDateRange
:selected-range="selectedRange"
@setRange="setDateRange"
@set-range="setDateRange"
/>
<div
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"
:start-current-date="startCurrentDate"
:end-current-date="endCurrentDate"
@selectYear="openCalendar($event, calendar, YEAR)"
@select-year="openCalendar($event, calendar, YEAR)"
/>
<CalendarMonth
v-else-if="calendarViews[calendar] === MONTH"
:calendar-type="calendar"
:start-current-date="startCurrentDate"
:end-current-date="endCurrentDate"
@selectMonth="openCalendar($event, calendar)"
@setView="setViewMode"
@select-month="openCalendar($event, calendar)"
@set-view="setViewMode"
@prev="moveCalendar(calendar, 'prev', YEAR)"
@next="moveCalendar(calendar, 'next', YEAR)"
/>
@@ -286,9 +286,9 @@ const emitDateRange = () => {
:selected-end-date="selectedEndDate"
:selecting-end-date="selectingEndDate"
:hovered-end-date="hoveredEndDate"
@updateHoveredEndDate="hoveredEndDate = $event"
@selectDate="selectDate"
@setView="setViewMode"
@update-hovered-end-date="hoveredEndDate = $event"
@select-date="selectDate"
@set-view="setViewMode"
@prev="moveCalendar(calendar, 'prev')"
@next="moveCalendar(calendar, 'next')"
/>
@@ -1,5 +1,5 @@
<script setup>
const emit = defineEmits(['clear', 'clear']);
const emit = defineEmits(['clear', 'change']);
const onClickClear = () => {
emit('clear');
@@ -62,7 +62,7 @@ const selectMonth = index => {
MONTH
)
"
@setView="setViewMode"
@set-view="setViewMode"
@prev="onClickPrev"
@next="onClickNext"
/>
@@ -139,7 +139,7 @@ const dayClasses = day => ({
"
@prev="onClickPrev"
@next="onClickNext"
@setView="setViewMode"
@set-view="setViewMode"
/>
<CalendarWeekLabel />
<div
@@ -1,5 +1,5 @@
<script>
import DatePicker from 'vue2-datepicker';
import DatePicker from 'vue-datepicker-next';
export default {
components: { DatePicker },
props: {
@@ -16,6 +16,7 @@ export default {
default: () => [],
},
},
emits: ['change'],
methods: {
handleChange(value) {
this.$emit('change', value);
@@ -1,6 +1,6 @@
<script>
import addDays from 'date-fns/addDays';
import DatePicker from 'vue2-datepicker';
import DatePicker from 'vue-datepicker-next';
export default {
components: { DatePicker },
props: {
@@ -17,6 +17,7 @@ export default {
default: [],
},
},
emits: ['change'],
methods: {
handleChange(value) {
@@ -18,7 +18,6 @@ defineProps({
<template>
<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"
@click="$emit('click')"
>
<slot name="leftIcon">
<fluent-icon
@@ -38,15 +38,19 @@ const props = defineProps({
},
});
const emit = defineEmits(['onSearch']);
const emit = defineEmits(['onSearch', 'select', 'removeFilter']);
const searchTerm = ref('');
const onSearch = debounce(value => {
searchTerm.value = value;
const debouncedEmit = debounce(value => {
emit('onSearch', value);
}, 300);
const onSearch = value => {
searchTerm.value = value;
debouncedEmit();
};
const filteredListItems = computed(() => {
if (!searchTerm.value) return props.listItems;
return picoSearch(props.listItems, searchTerm.value, ['name']);
@@ -84,7 +88,7 @@ const shouldShowEmptyState = computed(() => {
:input-placeholder="inputPlaceholder"
:show-clear-filter="showClearFilter"
@input="onSearch"
@click="$emit('removeFilter')"
@remove="$emit('removeFilter')"
/>
</slot>
<slot name="listItem">
@@ -103,7 +107,7 @@ const shouldShowEmptyState = computed(() => {
:button-text="item.name"
:icon="item.icon"
:icon-color="item.iconColor"
@click="$emit('click', item)"
@click.stop.prevent="emit('select', item)"
/>
</slot>
</div>
@@ -22,10 +22,6 @@ defineProps({
<template>
<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"
@click.stop.prevent="$emit('click')"
@mouseenter="$emit('mouseenter')"
@mouseleave="$emit('mouseleave')"
@focus="$emit('focus')"
>
<div class="inline-flex items-center gap-3 overflow-hidden">
<fluent-icon
@@ -1,4 +1,5 @@
<script setup>
import { defineEmits } from 'vue';
defineProps({
inputValue: {
type: String,
@@ -13,6 +14,8 @@ defineProps({
default: false,
},
});
const emit = defineEmits(['input', 'remove']);
</script>
<template>
@@ -30,7 +33,7 @@ defineProps({
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)"
@input="emit('input', $event.target.value)"
/>
</div>
<!-- Clear filter button -->
@@ -40,7 +43,7 @@ defineProps({
variant="clear"
color-scheme="primary"
class="!px-1 !py-1.5"
@click="$emit('click')"
@click="emit('remove')"
>
{{ $t('REPORT.FILTER_ACTIONS.CLEAR_FILTER') }}
</woot-button>
@@ -44,6 +44,7 @@ export default {
default: '',
},
},
emits: ['remove'],
computed: {
textColor() {
if (this.variant === 'smooth') return '';
@@ -74,7 +75,7 @@ export default {
},
methods: {
onClick() {
this.$emit('click', this.title);
this.$emit('remove', this.title);
},
},
};
@@ -1,12 +1,14 @@
<script>
export default {
props: {
value: { type: Boolean, default: false },
modelValue: { type: Boolean, default: false },
size: { type: String, default: '' },
},
emits: ['update:modelValue', 'input'],
methods: {
onClick() {
this.$emit('input', !this.value);
this.$emit('update:modelValue', !this.modelValue);
this.$emit('input', !this.modelValue);
},
},
};
@@ -16,12 +18,12 @@ export default {
<button
type="button"
class="toggle-button p-0"
:class="{ active: value, small: size === 'small' }"
:class="{ active: modelValue, small: size === 'small' }"
role="switch"
:aria-checked="value.toString()"
:aria-checked="modelValue.toString()"
@click="onClick"
>
<span aria-hidden="true" :class="{ active: value }" />
<span aria-hidden="true" :class="{ active: modelValue }" />
</button>
</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>
export default {
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,
},
},
<script setup>
import { computed, inject } from 'vue';
computed: {
active() {
return this.index === this.$parent.index;
},
getItemCount() {
return this.count;
},
const props = defineProps({
index: {
type: Number,
default: 0,
},
methods: {
onTabClick(event) {
event.preventDefault();
if (!this.disabled) {
this.$parent.$emit('change', this.index);
}
},
name: {
type: String,
required: true,
},
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>
@@ -80,7 +80,7 @@ export default {
this.createTimer();
}
},
beforeDestroy() {
unmounted() {
clearTimeout(this.timer);
},
methods: {
@@ -96,11 +96,6 @@ export default {
);
},
},
methods: {
handleClick(evt) {
this.$emit('click', evt);
},
},
};
</script>
@@ -110,7 +105,6 @@ export default {
:type="type"
:class="buttonClasses"
:disabled="isDisabled || isLoading"
@click="handleClick"
>
<Spinner
v-if="isLoading"
@@ -1,46 +0,0 @@
import { action } from '@storybook/addon-actions';
import WootAnnouncementPopup from '../AnnouncementPopup.vue';
export default {
title: 'Components/Popup/Announcement Popup',
argTypes: {
popupMessage: {
defaultValue:
'Now a new key shortcut (⌘ + ↵) is available to send messages. You can enable it in the',
control: {
type: 'text',
},
},
routeText: {
defaultValue: 'profile settings',
control: {
type: 'text',
},
},
hasCloseButton: {
defaultValue: true,
control: {
type: 'boolean',
},
},
closeButtonText: {
defaultValue: 'Got it',
control: {
type: 'text',
},
},
},
};
const Template = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: { WootAnnouncementPopup },
template:
'<woot-announcement-popup v-bind="$props" @open="onClickOpenPath" @close="onClickClose"></woot-announcement-popup>',
});
export const AnnouncementPopup = Template.bind({});
AnnouncementPopup.args = {
onClickOpenPath: action('opened path'),
onClickClose: action('closed the popup'),
};
@@ -1,50 +0,0 @@
import { action } from '@storybook/addon-actions';
import WootButton from '../WootButton.vue';
export default {
title: 'Components/Button',
component: WootButton,
argTypes: {
colorScheme: {
control: {
type: 'select',
options: ['primary', 'secondary', 'success', 'alert', 'warning'],
},
},
size: {
control: {
type: 'select',
options: ['tiny', 'small', 'medium', 'large', 'expanded'],
},
},
variant: {
control: {
type: 'select',
options: ['hollow', 'clear'],
},
},
isLoading: {
control: {
type: 'boolean',
},
},
isDisabled: {
control: {
type: 'boolean',
},
},
},
};
const Template = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: { WootButton },
template:
'<woot-button v-bind="$props" @click="onClick">{{label}}</woot-button>',
});
export const Primary = Template.bind({});
Primary.args = {
label: 'New message',
onClick: action('Hello'),
};
@@ -1,38 +0,0 @@
import { action } from '@storybook/addon-actions';
import WootDateRangePicker from '../DateRangePicker.vue';
export default {
title: 'Components/Date Picker/Date Range Picker',
argTypes: {
confirmText: {
defaultValue: 'Apply',
control: {
type: 'text',
},
},
placeholder: {
defaultValue: 'Select date range',
control: {
type: 'text',
},
},
value: {
control: {
type: 'text',
},
},
},
};
const Template = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: { WootDateRangePicker },
template:
'<woot-date-range-picker v-bind="$props" @change="onChange"></woot-date-range-picker>',
});
export const DateRangePicker = Template.bind({});
DateRangePicker.args = {
onChange: action('applied'),
value: new Date(),
};
@@ -1,38 +0,0 @@
import { action } from '@storybook/addon-actions';
import WootDateTimePicker from '../DateTimePicker.vue';
export default {
title: 'Components/Date Picker/Date Time Picker',
argTypes: {
confirmText: {
defaultValue: 'Apply',
control: {
type: 'text',
},
},
placeholder: {
defaultValue: 'Select date time',
control: {
type: 'text',
},
},
value: {
control: {
type: 'text',
},
},
},
};
const Template = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: { WootDateTimePicker },
template:
'<woot-date-time-picker v-bind="$props" @change="onChange"></woot-date-time-picker>',
});
export const DateTimePicker = Template.bind({});
DateTimePicker.args = {
onChange: action('applied'),
value: new Date(),
};
@@ -1,64 +0,0 @@
import { action } from '@storybook/addon-actions';
export default {
title: 'Components/Label',
argTypes: {
title: {
defaultValue: 'sales',
control: {
type: 'text',
},
},
colorScheme: {
control: {
type: 'select',
options: ['primary', 'secondary', 'success', 'alert', 'warning'],
},
},
description: {
defaultValue: 'label',
control: {
type: 'text',
},
},
href: {
control: {
type: 'text',
},
},
bgColor: {
defaultValue: '#a83262',
control: {
type: 'text',
},
},
small: {
defaultValue: false,
control: {
type: 'boolean',
},
},
showClose: {
defaultValue: false,
control: {
type: 'boolean',
},
},
icon: {
defaultValue: 'ion-close',
control: {
type: 'text',
},
},
},
};
const Template = (args, { argTypes }) => ({
props: Object.keys(argTypes),
template: '<woot-label v-bind="$props" @click="onClick"></woot-label>',
});
export const DefaultLabel = Template.bind({});
DefaultLabel.args = {
onClick: action('clicked'),
};
@@ -1,30 +0,0 @@
import TimeAgo from 'dashboard/components/ui/TimeAgo';
export default {
title: 'Components/TimeAgo',
component: TimeAgo,
argTypes: {
isAutoRefreshEnabled: {
control: {
type: 'boolean',
},
},
timestamp: {
control: {
type: 'text, date, number',
},
},
},
};
const Template = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: { TimeAgo },
template: '<time-ago v-bind="$props"></time-ago>',
});
export const TimeAgoView = Template.bind({});
TimeAgoView.args = {
timestamp: 1549843200,
isAutoRefreshEnabled: false,
};
@@ -9,6 +9,7 @@ import AICTAModal from './AICTAModal.vue';
import AIAssistanceModal from './AIAssistanceModal.vue';
import { CMD_AI_ASSIST } from 'dashboard/helper/commandbar/events';
import AIAssistanceCTAButton from './AIAssistanceCTAButton.vue';
import { emitter } from 'shared/helpers/mitt';
export default {
components: {
@@ -16,6 +17,7 @@ export default {
AICTAModal,
AIAssistanceCTAButton,
},
emits: ['replaceText'],
setup(props, { emit }) {
const { uiSettings, updateUISettings } = useUISettings();
@@ -80,7 +82,7 @@ export default {
},
mounted() {
this.$emitter.on(CMD_AI_ASSIST, this.onAIAssist);
emitter.on(CMD_AI_ASSIST, this.onAIAssist);
this.initializeMessage(this.draftMessage);
},
@@ -124,7 +126,7 @@ export default {
<div v-if="isAIIntegrationEnabled" class="relative">
<AIAssistanceCTAButton
v-if="shouldShowAIAssistCTAButton"
@click="openAIAssist"
@open="openAIAssist"
/>
<woot-button
v-else
@@ -136,19 +138,19 @@ export default {
@click="openAIAssist"
/>
<woot-modal
:show.sync="showAIAssistanceModal"
v-model:show="showAIAssistanceModal"
:on-close="hideAIAssistanceModal"
>
<AIAssistanceModal
:ai-option="aiOption"
@applyText="insertText"
@apply-text="insertText"
@close="hideAIAssistanceModal"
/>
</woot-modal>
</div>
<div v-else-if="shouldShowAIAssistCTAButtonForAdmin" class="relative">
<AIAssistanceCTAButton @click="openAICta" />
<woot-modal :show.sync="showAICtaModal" :on-close="hideAICtaModal">
<woot-modal v-model:show="showAICtaModal" :on-close="hideAICtaModal">
<AICTAModal @close="hideAICtaModal" />
</woot-modal>
</div>

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