Compare commits

..
Author SHA1 Message Date
Muhsin KelothandGitHub 3b723ecccc Merge branch 'develop' into refactor/structured-clone 2024-07-24 19:18:38 +05:30
Sivin VargheseandGitHub 71073a06ff fix: Broken connect to facebook page layout (#9830)
# Pull Request Template

## Description

This PR will fix the layout issue in connecting to the Facebook page
layout.

Fixes
https://linear.app/chatwoot/issue/CW-3489/connect-facebook-page-layout-is-broken

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

**Before**
<img width="1264" alt="image"
src="https://github.com/user-attachments/assets/2b9e412b-f172-4184-b45b-8c97333c3e51">


**After**
<img width="1264" alt="image"
src="https://github.com/user-attachments/assets/035f3468-f611-43ce-a058-fb8eafa1a45d">




## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
2024-07-24 16:45:17 +05:30
Muhsin KelothandGitHub 168938500f Merge branch 'develop' into refactor/structured-clone 2024-07-24 12:19:15 +05:30
PranavandGitHub 39d20b197d chore: Update the integration icons, logic for enabled/active attributes for the integration (#9828)
This PR would update the logos for the integrations (also add the dark mode variants to be used in the future) and updates the logic for enabled / active attributes in the apps.
2024-07-23 17:45:53 -07:00
Sivin VargheseandGitHub fb99ba7b40 feat: Rewrite uiSettings mixin to a composable (#9819) 2024-07-23 21:27:22 +05:30
79aa5a5d7f feat: Replace alertMixin usage with useAlert (#9793)
# Pull Request Template

## Description

This PR will replace the usage of `alertMixin` from the code base with
the `useAlert` composable.

Fixes
https://linear.app/chatwoot/issue/CW-3462/replace-alertmixin-usage-with-usealert

## Type of change

- [x] Breaking change (fix or feature that would cause existing
functionality not to work as expected)

## How Has This Been Tested?

Please refer this issue description

https://linear.app/chatwoot/issue/CW-3462/replace-alertmixin-usage-with-usealert


## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

---------

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2024-07-23 16:41:11 +05:30
Muhsin KelothandGitHub 2bb7d1e4b2 Merge branch 'develop' into refactor/structured-clone 2024-07-23 13:12:45 +05:30
Sivin VargheseandGitHub 10ee773aac feat: Rewrite labels/validationMixin mixin to a helper (#9818) 2024-07-22 21:41:05 +05:30
Shivam Mishra c5c3b74728 refactor: use structuredClone instead of JSON stringify and parse 2024-07-22 16:09:05 +05:30
Sivin VargheseandGitHub 81d7c51a84 feat: Replace contentTypeMixin within the component (#9802)
# Pull Request Template

## Description

This PR will completely remove the `contentTypeMixin` and be used in the
component as it is.

Fixes
https://linear.app/chatwoot/issue/CW-3465/rewrite-contenttypemixin-mixin-as-a-helper

## Type of change

- [x] New feature (non-breaking change which adds functionality)

## How Has This Been Tested?

Take a look at this component
`app/javascript/dashboard/components/widgets/conversation/Message.vue`


## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
2024-07-22 13:08:04 +05:30
79381b08cc feat: Move timeMixin to a helper (#9799)
# Pull Request Template

## Description

This PR will replace the usage of `timeMixin` with `timeHelper`

Fixes
https://linear.app/chatwoot/issue/CW-3451/move-time-mixin-to-a-helper

## Type of change

- [x] New feature (non-breaking change which adds functionality)

## How Has This Been Tested?

Please refer to this issue description.
https://linear.app/chatwoot/issue/CW-3451/move-time-mixin-to-a-helper

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

---------

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2024-07-22 13:07:29 +05:30
Sivin VargheseandGitHub 84c380c8c4 feat: Replace SLA validationMixin within the component (#9804)
# Pull Request Template

## Description

This PR will completely remove the SLA `validationMixin` and be used in
the component as it is with a name change.

Fixes
https://linear.app/chatwoot/issue/CW-3456/rewrite-sla-validationmixin-mixin-to-a-composable

## Type of change

- [x] New feature (non-breaking change which adds functionality)

## How Has This Been Tested?

Take a look at these components
1. `dashboard/routes/dashboard/settings/sla/AddSLA.vue` **(Not used)**
2. `dashboard/routes/dashboard/settings/sla/SlaForm.vue`
3. `dashboard/routes/dashboard/settings/sla/SlaTimeInput.vue`


## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
2024-07-22 13:05:55 +05:30
cb0642564c feat: add promise based loader for FB script (#9780)
![CleanShot 2024-07-16 at 11 10
40@2x](https://github.com/user-attachments/assets/8b938968-5f80-4a19-95fb-e00e1dbd7526)

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2024-07-22 11:32:05 +05:30
23e30fcb1a feat: Delete bulkActionsMixin (#9800)
# Pull Request Template

## Description

This PR will remove the `bulkActionsMixin` usage. Seems like it is not
used anywhere.

Fixes https://linear.app/chatwoot/issue/CW-3453/delete-bulkactionsmixin

## Type of change

- [x] New feature (non-breaking change which adds functionality)


## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2024-07-19 11:14:56 +05:30
Sojan JoseandGitHub ae8619142f chore: Update dependencies to fix security issues (#9801)
- update dependencies to fix security issues
2024-07-18 20:08:26 -07:00
Vishnu NarayananandGitHub 499408ea6f fix: add restart policy for rails/sidekiq containers (#9797)
- Add `restart:always` policy for rails and sidekiq containers in the
production compose file

Fixes #9501
Fixes
https://linear.app/chatwoot/issue/PR-1099/missing-restart-always-at-docker-compose-file
2024-07-18 18:33:03 +05:30
Sojan dc245814fe Merge branch 'release/3.11.0' into develop 2024-07-16 19:23:13 -07:00
253 changed files with 2504 additions and 2227 deletions
+10 -9
View File
@@ -103,8 +103,8 @@ GEM
tzinfo (~> 2.0) tzinfo (~> 2.0)
acts-as-taggable-on (9.0.1) acts-as-taggable-on (9.0.1)
activerecord (>= 6.0, < 7.1) activerecord (>= 6.0, < 7.1)
addressable (2.8.4) addressable (2.8.7)
public_suffix (>= 2.0.2, < 6.0) public_suffix (>= 2.0.2, < 7.0)
administrate (0.20.1) administrate (0.20.1)
actionpack (>= 6.0, < 8.0) actionpack (>= 6.0, < 8.0)
actionview (>= 6.0, < 8.0) actionview (>= 6.0, < 8.0)
@@ -171,7 +171,8 @@ GEM
commonmarker (0.23.10) commonmarker (0.23.10)
concurrent-ruby (1.3.3) concurrent-ruby (1.3.3)
connection_pool (2.4.1) connection_pool (2.4.1)
crack (0.4.5) crack (1.0.0)
bigdecimal
rexml rexml
crass (1.0.6) crass (1.0.6)
csv (3.3.0) csv (3.3.0)
@@ -358,7 +359,7 @@ GEM
ruby2ruby (~> 2.4) ruby2ruby (~> 2.4)
ruby_parser (~> 3.10) ruby_parser (~> 3.10)
hana (1.3.7) hana (1.3.7)
hashdiff (1.0.1) hashdiff (1.1.0)
hashie (5.0.0) hashie (5.0.0)
http (5.1.1) http (5.1.1)
addressable (~> 2.8) addressable (~> 2.8)
@@ -550,7 +551,7 @@ GEM
method_source (~> 1.0) method_source (~> 1.0)
pry-rails (0.3.9) pry-rails (0.3.9)
pry (>= 0.10.4) pry (>= 0.10.4)
public_suffix (5.0.1) public_suffix (6.0.0)
puma (6.4.2) puma (6.4.2)
nio4r (~> 2.0) nio4r (~> 2.0)
pundit (2.3.0) pundit (2.3.0)
@@ -633,8 +634,8 @@ GEM
retriable (3.1.2) retriable (3.1.2)
reverse_markdown (2.1.1) reverse_markdown (2.1.1)
nokogiri nokogiri
rexml (3.2.8) rexml (3.3.2)
strscan (>= 3.0.9) strscan
rspec-core (3.13.0) rspec-core (3.13.0)
rspec-support (~> 3.13.0) rspec-support (~> 3.13.0)
rspec-expectations (3.13.1) rspec-expectations (3.13.1)
@@ -809,7 +810,7 @@ GEM
web-push (3.0.1) web-push (3.0.1)
jwt (~> 2.0) jwt (~> 2.0)
openssl (~> 3.0) openssl (~> 3.0)
webmock (3.18.1) webmock (3.23.1)
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)
@@ -970,4 +971,4 @@ RUBY VERSION
ruby 3.3.3p89 ruby 3.3.3p89
BUNDLED WITH BUNDLED WITH
2.5.14 2.5.16
@@ -116,13 +116,14 @@
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { useUISettings } from 'dashboard/composables/useUISettings';
import { useAlert } from 'dashboard/composables';
import VirtualList from 'vue-virtual-scroll-list'; import VirtualList from 'vue-virtual-scroll-list';
import ChatListHeader from './ChatListHeader.vue'; import ChatListHeader from './ChatListHeader.vue';
import ConversationAdvancedFilter from './widgets/conversation/ConversationAdvancedFilter.vue'; import ConversationAdvancedFilter from './widgets/conversation/ConversationAdvancedFilter.vue';
import ChatTypeTabs from './widgets/ChatTypeTabs.vue'; import ChatTypeTabs from './widgets/ChatTypeTabs.vue';
import ConversationItem from './ConversationItem.vue'; import ConversationItem from './ConversationItem.vue';
import timeMixin from '../mixins/time';
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins'; import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
import conversationMixin from '../mixins/conversations'; import conversationMixin from '../mixins/conversations';
import wootConstants from 'dashboard/constants/globals'; import wootConstants from 'dashboard/constants/globals';
@@ -131,9 +132,7 @@ import filterQueryGenerator from '../helper/filterQueryGenerator.js';
import AddCustomViews from 'dashboard/routes/dashboard/customviews/AddCustomViews.vue'; import AddCustomViews from 'dashboard/routes/dashboard/customviews/AddCustomViews.vue';
import DeleteCustomViews from 'dashboard/routes/dashboard/customviews/DeleteCustomViews.vue'; import DeleteCustomViews from 'dashboard/routes/dashboard/customviews/DeleteCustomViews.vue';
import ConversationBulkActions from './widgets/conversation/conversationBulkActions/Index.vue'; import ConversationBulkActions from './widgets/conversation/conversationBulkActions/Index.vue';
import alertMixin from 'shared/mixins/alertMixin';
import filterMixin from 'shared/mixins/filterMixin'; import filterMixin from 'shared/mixins/filterMixin';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import languages from 'dashboard/components/widgets/conversation/advancedFilterItems/languages'; import languages from 'dashboard/components/widgets/conversation/advancedFilterItems/languages';
import countries from 'shared/constants/countries'; import countries from 'shared/constants/countries';
import { generateValuesForEditCustomViews } from 'dashboard/helper/customViewsHelper'; import { generateValuesForEditCustomViews } from 'dashboard/helper/customViewsHelper';
@@ -158,14 +157,7 @@ export default {
IntersectionObserver, IntersectionObserver,
VirtualList, VirtualList,
}, },
mixins: [ mixins: [conversationMixin, keyboardEventListenerMixins, filterMixin],
timeMixin,
conversationMixin,
keyboardEventListenerMixins,
alertMixin,
filterMixin,
uiSettingsMixin,
],
provide() { provide() {
return { return {
// Actions to be performed on virtual list item and context menu. // Actions to be performed on virtual list item and context menu.
@@ -210,6 +202,13 @@ export default {
type: Boolean, type: Boolean,
}, },
}, },
setup() {
const { uiSettings } = useUISettings();
return {
uiSettings,
};
},
data() { data() {
return { return {
activeAssigneeTab: wootConstants.ASSIGNEE_TYPE.ME, activeAssigneeTab: wootConstants.ASSIGNEE_TYPE.ME,
@@ -812,7 +811,7 @@ export default {
}); });
this.$store.dispatch('bulkActions/clearSelectedConversationIds'); this.$store.dispatch('bulkActions/clearSelectedConversationIds');
if (conversationId) { if (conversationId) {
this.showAlert( useAlert(
this.$t( this.$t(
'CONVERSATION.CARD_CONTEXT_MENU.API.AGENT_ASSIGNMENT.SUCCESFUL', 'CONVERSATION.CARD_CONTEXT_MENU.API.AGENT_ASSIGNMENT.SUCCESFUL',
{ {
@@ -822,10 +821,10 @@ export default {
) )
); );
} else { } else {
this.showAlert(this.$t('BULK_ACTION.ASSIGN_SUCCESFUL')); useAlert(this.$t('BULK_ACTION.ASSIGN_SUCCESFUL'));
} }
} catch (err) { } catch (err) {
this.showAlert(this.$t('BULK_ACTION.ASSIGN_FAILED')); useAlert(this.$t('BULK_ACTION.ASSIGN_FAILED'));
} }
}, },
async assignPriority(priority, conversationId = null) { async assignPriority(priority, conversationId = null) {
@@ -840,7 +839,7 @@ export default {
newValue: priority, newValue: priority,
from: 'Context menu', from: 'Context menu',
}); });
this.showAlert( useAlert(
this.$t('CONVERSATION.PRIORITY.CHANGE_PRIORITY.SUCCESSFUL', { this.$t('CONVERSATION.PRIORITY.CHANGE_PRIORITY.SUCCESSFUL', {
priority, priority,
conversationId, conversationId,
@@ -883,7 +882,7 @@ export default {
conversationId, conversationId,
teamId: team.id, teamId: team.id,
}); });
this.showAlert( useAlert(
this.$t( this.$t(
'CONVERSATION.CARD_CONTEXT_MENU.API.TEAM_ASSIGNMENT.SUCCESFUL', 'CONVERSATION.CARD_CONTEXT_MENU.API.TEAM_ASSIGNMENT.SUCCESFUL',
{ {
@@ -893,7 +892,7 @@ export default {
) )
); );
} catch (error) { } catch (error) {
this.showAlert( useAlert(
this.$t('CONVERSATION.CARD_CONTEXT_MENU.API.TEAM_ASSIGNMENT.FAILED') this.$t('CONVERSATION.CARD_CONTEXT_MENU.API.TEAM_ASSIGNMENT.FAILED')
); );
} }
@@ -910,7 +909,7 @@ export default {
}); });
this.$store.dispatch('bulkActions/clearSelectedConversationIds'); this.$store.dispatch('bulkActions/clearSelectedConversationIds');
if (conversationId) { if (conversationId) {
this.showAlert( useAlert(
this.$t( this.$t(
'CONVERSATION.CARD_CONTEXT_MENU.API.LABEL_ASSIGNMENT.SUCCESFUL', 'CONVERSATION.CARD_CONTEXT_MENU.API.LABEL_ASSIGNMENT.SUCCESFUL',
{ {
@@ -920,10 +919,10 @@ export default {
) )
); );
} else { } else {
this.showAlert(this.$t('BULK_ACTION.LABELS.ASSIGN_SUCCESFUL')); useAlert(this.$t('BULK_ACTION.LABELS.ASSIGN_SUCCESFUL'));
} }
} catch (err) { } catch (err) {
this.showAlert(this.$t('BULK_ACTION.LABELS.ASSIGN_FAILED')); useAlert(this.$t('BULK_ACTION.LABELS.ASSIGN_FAILED'));
} }
}, },
async onAssignTeamsForBulk(team) { async onAssignTeamsForBulk(team) {
@@ -936,9 +935,9 @@ export default {
}, },
}); });
this.$store.dispatch('bulkActions/clearSelectedConversationIds'); this.$store.dispatch('bulkActions/clearSelectedConversationIds');
this.showAlert(this.$t('BULK_ACTION.TEAMS.ASSIGN_SUCCESFUL')); useAlert(this.$t('BULK_ACTION.TEAMS.ASSIGN_SUCCESFUL'));
} catch (err) { } catch (err) {
this.showAlert(this.$t('BULK_ACTION.TEAMS.ASSIGN_FAILED')); useAlert(this.$t('BULK_ACTION.TEAMS.ASSIGN_FAILED'));
} }
}, },
async onUpdateConversations(status, snoozedUntil) { async onUpdateConversations(status, snoozedUntil) {
@@ -952,9 +951,9 @@ export default {
snoozed_until: snoozedUntil, snoozed_until: snoozedUntil,
}); });
this.$store.dispatch('bulkActions/clearSelectedConversationIds'); this.$store.dispatch('bulkActions/clearSelectedConversationIds');
this.showAlert(this.$t('BULK_ACTION.UPDATE.UPDATE_SUCCESFUL')); useAlert(this.$t('BULK_ACTION.UPDATE.UPDATE_SUCCESFUL'));
} catch (err) { } catch (err) {
this.showAlert(this.$t('BULK_ACTION.UPDATE.UPDATE_FAILED')); useAlert(this.$t('BULK_ACTION.UPDATE.UPDATE_FAILED'));
} }
}, },
toggleConversationStatus(conversationId, status, snoozedUntil) { toggleConversationStatus(conversationId, status, snoozedUntil) {
@@ -965,7 +964,7 @@ export default {
snoozedUntil, snoozedUntil,
}) })
.then(() => { .then(() => {
this.showAlert(this.$t('CONVERSATION.CHANGE_STATUS')); useAlert(this.$t('CONVERSATION.CHANGE_STATUS'));
this.isLoading = false; this.isLoading = false;
}); });
}, },
+2 -3
View File
@@ -25,10 +25,9 @@
<script> <script>
import 'highlight.js/styles/default.css'; import 'highlight.js/styles/default.css';
import { copyTextToClipboard } from 'shared/helpers/clipboard'; import { copyTextToClipboard } from 'shared/helpers/clipboard';
import alertMixin from 'shared/mixins/alertMixin'; import { useAlert } from 'dashboard/composables';
export default { export default {
mixins: [alertMixin],
props: { props: {
script: { script: {
type: String, type: String,
@@ -61,7 +60,7 @@ export default {
async onCopy(e) { async onCopy(e) {
e.preventDefault(); e.preventDefault();
await copyTextToClipboard(this.script); await copyTextToClipboard(this.script);
this.showAlert(this.$t('COMPONENTS.CODE.COPY_SUCCESSFUL')); useAlert(this.$t('COMPONENTS.CODE.COPY_SUCCESSFUL'));
}, },
}, },
}; };
@@ -18,10 +18,9 @@
<script> <script>
import 'highlight.js/styles/default.css'; import 'highlight.js/styles/default.css';
import { copyTextToClipboard } from 'shared/helpers/clipboard'; import { copyTextToClipboard } from 'shared/helpers/clipboard';
import alertMixin from 'shared/mixins/alertMixin'; import { useAlert } from 'dashboard/composables';
export default { export default {
mixins: [alertMixin],
props: { props: {
value: { value: {
type: String, type: String,
@@ -37,7 +36,7 @@ export default {
async onCopy(e) { async onCopy(e) {
e.preventDefault(); e.preventDefault();
await copyTextToClipboard(this.value); await copyTextToClipboard(this.value);
this.showAlert(this.$t('COMPONENTS.CODE.COPY_SUCCESSFUL')); useAlert(this.$t('COMPONENTS.CODE.COPY_SUCCESSFUL'));
}, },
toggleMasked() { toggleMasked() {
this.masked = !this.masked; this.masked = !this.masked;
@@ -15,13 +15,11 @@
<script> <script>
import WootSnackbar from './Snackbar.vue'; import WootSnackbar from './Snackbar.vue';
import alertMixin from 'shared/mixins/alertMixin';
export default { export default {
components: { components: {
WootSnackbar, WootSnackbar,
}, },
mixins: [alertMixin],
props: { props: {
duration: { duration: {
type: Number, type: Number,
@@ -42,7 +40,7 @@ export default {
this.$emitter.off('newToastMessage', this.onNewToastMessage); this.$emitter.off('newToastMessage', this.onNewToastMessage);
}, },
methods: { methods: {
onNewToastMessage(message, action) { onNewToastMessage({ message, action }) {
this.snackMessages.push({ this.snackMessages.push({
key: new Date().getTime(), key: new Date().getTime(),
message, message,
@@ -10,9 +10,9 @@
</template> </template>
<script> <script>
import Banner from 'dashboard/components/ui/Banner.vue';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import adminMixin from 'dashboard/mixins/isAdmin'; import { useAdmin } from 'dashboard/composables/useAdmin';
import Banner from 'dashboard/components/ui/Banner.vue';
import accountMixin from 'dashboard/mixins/account'; import accountMixin from 'dashboard/mixins/account';
const EMPTY_SUBSCRIPTION_INFO = { const EMPTY_SUBSCRIPTION_INFO = {
@@ -22,7 +22,13 @@ const EMPTY_SUBSCRIPTION_INFO = {
export default { export default {
components: { Banner }, components: { Banner },
mixins: [adminMixin, accountMixin], mixins: [accountMixin],
setup() {
const { isAdmin } = useAdmin();
return {
isAdmin,
};
},
computed: { computed: {
...mapGetters({ ...mapGetters({
isOnChatwootCloud: 'globalConfig/isOnChatwootCloud', isOnChatwootCloud: 'globalConfig/isOnChatwootCloud',
@@ -14,11 +14,11 @@
import Banner from 'dashboard/components/ui/Banner.vue'; import Banner from 'dashboard/components/ui/Banner.vue';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import accountMixin from 'dashboard/mixins/account'; import accountMixin from 'dashboard/mixins/account';
import alertMixin from 'shared/mixins/alertMixin'; import { useAlert } from 'dashboard/composables';
export default { export default {
components: { Banner }, components: { Banner },
mixins: [accountMixin, alertMixin], mixins: [accountMixin],
computed: { computed: {
...mapGetters({ ...mapGetters({
currentUser: 'getCurrentUser', currentUser: 'getCurrentUser',
@@ -36,7 +36,7 @@ export default {
methods: { methods: {
resendVerificationEmail() { resendVerificationEmail() {
this.$store.dispatch('resendConfirmation'); this.$store.dispatch('resendConfirmation');
this.showAlert(this.$t('APP_GLOBAL.EMAIL_VERIFICATION_SENT')); useAlert(this.$t('APP_GLOBAL.EMAIL_VERIFICATION_SENT'));
}, },
}, },
}; };
@@ -14,15 +14,20 @@ import Banner from 'dashboard/components/ui/Banner.vue';
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage'; import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
import { LocalStorage } from 'shared/helpers/localStorage'; import { LocalStorage } from 'shared/helpers/localStorage';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import adminMixin from 'dashboard/mixins/isAdmin'; import { useAdmin } from 'dashboard/composables/useAdmin';
import { hasAnUpdateAvailable } from './versionCheckHelper'; import { hasAnUpdateAvailable } from './versionCheckHelper';
export default { export default {
components: { Banner }, components: { Banner },
mixins: [adminMixin],
props: { props: {
latestChatwootVersion: { type: String, default: '' }, latestChatwootVersion: { type: String, default: '' },
}, },
setup() {
const { isAdmin } = useAdmin();
return {
isAdmin,
};
},
data() { data() {
return { userDismissedBanner: false }; return { userDismissedBanner: false };
}, },
@@ -12,13 +12,12 @@
<script> <script>
import Banner from 'dashboard/components/ui/Banner.vue'; import Banner from 'dashboard/components/ui/Banner.vue';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import adminMixin from 'dashboard/mixins/isAdmin';
import accountMixin from 'dashboard/mixins/account'; import accountMixin from 'dashboard/mixins/account';
import { differenceInDays } from 'date-fns'; import { differenceInDays } from 'date-fns';
export default { export default {
components: { Banner }, components: { Banner },
mixins: [adminMixin, accountMixin], mixins: [accountMixin],
data() { data() {
return { conversationMeta: {} }; return { conversationMeta: {} };
}, },
@@ -78,7 +78,7 @@
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import alertMixin from 'shared/mixins/alertMixin'; import { useAlert } from 'dashboard/composables';
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins'; import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
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';
@@ -94,7 +94,7 @@ export default {
WootDropdownItem, WootDropdownItem,
WootDropdownMenu, WootDropdownMenu,
}, },
mixins: [alertMixin, keyboardEventListenerMixins], mixins: [keyboardEventListenerMixins],
props: { conversationId: { type: [String, Number], required: true } }, props: { conversationId: { type: [String, Number], required: true } },
data() { data() {
return { return {
@@ -209,7 +209,7 @@ export default {
snoozedUntil, snoozedUntil,
}) })
.then(() => { .then(() => {
this.showAlert(this.$t('CONVERSATION.CHANGE_STATUS')); useAlert(this.$t('CONVERSATION.CHANGE_STATUS'));
this.isLoading = false; this.isLoading = false;
}); });
}, },
@@ -18,7 +18,7 @@
</woot-button> </woot-button>
</woot-dropdown-item> </woot-dropdown-item>
<woot-dropdown-divider /> <woot-dropdown-divider />
<woot-dropdown-item class="m-0 flex items-center justify-between p-2"> <woot-dropdown-item class="flex items-center justify-between p-2 m-0">
<div class="flex items-center"> <div class="flex items-center">
<fluent-icon <fluent-icon
v-tooltip.right-start="$t('SIDEBAR.SET_AUTO_OFFLINE.INFO_TEXT')" v-tooltip.right-start="$t('SIDEBAR.SET_AUTO_OFFLINE.INFO_TEXT')"
@@ -28,7 +28,7 @@
/> />
<span <span
class="my-0 mx-1 text-xs font-medium text-slate-600 dark:text-slate-100" class="mx-1 my-0 text-xs font-medium text-slate-600 dark:text-slate-100"
> >
{{ $t('SIDEBAR.SET_AUTO_OFFLINE.TEXT') }} {{ $t('SIDEBAR.SET_AUTO_OFFLINE.TEXT') }}
</span> </span>
@@ -36,7 +36,7 @@
<woot-switch <woot-switch
size="small" size="small"
class="mt-px mx-1 mb-0" class="mx-1 mt-px mb-0"
:value="currentUserAutoOffline" :value="currentUserAutoOffline"
@input="updateAutoOffline" @input="updateAutoOffline"
/> />
@@ -47,7 +47,7 @@
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import alertMixin from 'shared/mixins/alertMixin'; import { useAlert } from 'dashboard/composables';
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';
import WootDropdownHeader from 'shared/components/ui/dropdown/DropdownHeader.vue'; import WootDropdownHeader from 'shared/components/ui/dropdown/DropdownHeader.vue';
@@ -65,9 +65,6 @@ export default {
WootDropdownItem, WootDropdownItem,
AvailabilityStatusBadge, AvailabilityStatusBadge,
}, },
mixins: [alertMixin],
data() { data() {
return { return {
isStatusMenuOpened: false, isStatusMenuOpened: false,
@@ -129,7 +126,7 @@ export default {
account_id: this.currentAccountId, account_id: this.currentAccountId,
}); });
} catch (error) { } catch (error) {
this.showAlert( useAlert(
this.$t('PROFILE_SETTINGS.FORM.AVAILABILITY.SET_AVAILABILITY_ERROR') this.$t('PROFILE_SETTINGS.FORM.AVAILABILITY.SET_AVAILABILITY_ERROR')
); );
} finally { } finally {
@@ -30,9 +30,7 @@
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import adminMixin from '../../mixins/isAdmin';
import { getSidebarItems } from './config/default-sidebar'; import { getSidebarItems } from './config/default-sidebar';
import alertMixin from 'shared/mixins/alertMixin';
import PrimarySidebar from './sidebarComponents/Primary.vue'; import PrimarySidebar from './sidebarComponents/Primary.vue';
import SecondarySidebar from './sidebarComponents/Secondary.vue'; import SecondarySidebar from './sidebarComponents/Secondary.vue';
@@ -45,7 +43,7 @@ export default {
PrimarySidebar, PrimarySidebar,
SecondarySidebar, SecondarySidebar,
}, },
mixins: [adminMixin, alertMixin, keyboardEventListenerMixins], mixins: [keyboardEventListenerMixins],
props: { props: {
showSecondarySidebar: { showSecondarySidebar: {
type: Boolean, type: Boolean,
@@ -51,10 +51,9 @@
<script> <script>
import { required, minLength } from 'vuelidate/lib/validators'; import { required, minLength } from 'vuelidate/lib/validators';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import alertMixin from 'shared/mixins/alertMixin'; import { useAlert } from 'dashboard/composables';
export default { export default {
mixins: [alertMixin],
props: { props: {
show: { show: {
type: Boolean, type: Boolean,
@@ -88,13 +87,13 @@ export default {
account_name: this.accountName, account_name: this.accountName,
}); });
this.$emit('close-account-create-modal'); this.$emit('close-account-create-modal');
this.showAlert(this.$t('CREATE_ACCOUNT.API.SUCCESS_MESSAGE')); useAlert(this.$t('CREATE_ACCOUNT.API.SUCCESS_MESSAGE'));
window.location = `/app/accounts/${account_id}/dashboard`; window.location = `/app/accounts/${account_id}/dashboard`;
} catch (error) { } catch (error) {
if (error.response.status === 422) { if (error.response.status === 422) {
this.showAlert(this.$t('CREATE_ACCOUNT.API.EXIST_MESSAGE')); useAlert(this.$t('CREATE_ACCOUNT.API.EXIST_MESSAGE'));
} else { } else {
this.showAlert(this.$t('CREATE_ACCOUNT.API.ERROR_MESSAGE')); useAlert(this.$t('CREATE_ACCOUNT.API.ERROR_MESSAGE'));
} }
} }
}, },
@@ -94,8 +94,7 @@
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { useAdmin } from 'dashboard/composables/useAdmin';
import adminMixin from '../../../mixins/isAdmin';
import configMixin from 'shared/mixins/configMixin'; import configMixin from 'shared/mixins/configMixin';
import { import {
getInboxClassByType, getInboxClassByType,
@@ -111,13 +110,19 @@ import Policy from '../../policy.vue';
export default { export default {
components: { SecondaryChildNavItem, Policy }, components: { SecondaryChildNavItem, Policy },
mixins: [adminMixin, configMixin], mixins: [configMixin],
props: { props: {
menuItem: { menuItem: {
type: Object, type: Object,
default: () => ({}), default: () => ({}),
}, },
}, },
setup() {
const { isAdmin } = useAdmin();
return {
isAdmin,
};
},
computed: { computed: {
...mapGetters({ ...mapGetters({
activeInbox: 'getSelectedInbox', activeInbox: 'getSelectedInbox',
@@ -5,7 +5,7 @@
delay: { show: 1500, hide: 0 }, delay: { show: 1500, hide: 0 },
hideOnClick: true, hideOnClick: true,
}" }"
class="text-xxs text-slate-500 dark:text-slate-500 leading-4 ml-auto hover:text-slate-900 dark:hover:text-slate-100" class="ml-auto leading-4 text-xxs text-slate-500 dark:text-slate-500 hover:text-slate-900 dark:hover:text-slate-100"
> >
<span>{{ `${createdAtTime} • ${lastActivityTime}` }}</span> <span>{{ `${createdAtTime} • ${lastActivityTime}` }}</span>
</div> </div>
@@ -16,11 +16,14 @@ const MINUTE_IN_MILLI_SECONDS = 60000;
const HOUR_IN_MILLI_SECONDS = MINUTE_IN_MILLI_SECONDS * 60; const HOUR_IN_MILLI_SECONDS = MINUTE_IN_MILLI_SECONDS * 60;
const DAY_IN_MILLI_SECONDS = HOUR_IN_MILLI_SECONDS * 24; const DAY_IN_MILLI_SECONDS = HOUR_IN_MILLI_SECONDS * 24;
import timeMixin from 'dashboard/mixins/time'; import {
dynamicTime,
dateFormat,
shortTimestamp,
} from 'shared/helpers/timeHelper';
export default { export default {
name: 'TimeAgo', name: 'TimeAgo',
mixins: [timeMixin],
props: { props: {
isAutoRefreshEnabled: { isAutoRefreshEnabled: {
type: Boolean, type: Boolean,
@@ -37,17 +40,17 @@ export default {
}, },
data() { data() {
return { return {
lastActivityAtTimeAgo: this.dynamicTime(this.lastActivityTimestamp), lastActivityAtTimeAgo: dynamicTime(this.lastActivityTimestamp),
createdAtTimeAgo: this.dynamicTime(this.createdAtTimestamp), createdAtTimeAgo: dynamicTime(this.createdAtTimestamp),
timer: null, timer: null,
}; };
}, },
computed: { computed: {
lastActivityTime() { lastActivityTime() {
return this.shortTimestamp(this.lastActivityAtTimeAgo); return shortTimestamp(this.lastActivityAtTimeAgo);
}, },
createdAtTime() { createdAtTime() {
return this.shortTimestamp(this.createdAtTimeAgo); return shortTimestamp(this.createdAtTimeAgo);
}, },
createdAt() { createdAt() {
const createdTimeDiff = Date.now() - this.createdAtTimestamp * 1000; const createdTimeDiff = Date.now() - this.createdAtTimestamp * 1000;
@@ -56,9 +59,9 @@ export default {
? `${this.$t('CHAT_LIST.CHAT_TIME_STAMP.CREATED.LATEST')} ${ ? `${this.$t('CHAT_LIST.CHAT_TIME_STAMP.CREATED.LATEST')} ${
this.createdAtTimeAgo this.createdAtTimeAgo
}` }`
: `${this.$t( : `${this.$t('CHAT_LIST.CHAT_TIME_STAMP.CREATED.OLDEST')} ${dateFormat(
'CHAT_LIST.CHAT_TIME_STAMP.CREATED.OLDEST' this.createdAtTimestamp
)} ${this.dateFormat(this.createdAtTimestamp)}`; )}`;
}, },
lastActivity() { lastActivity() {
const lastActivityTimeDiff = const lastActivityTimeDiff =
@@ -70,7 +73,7 @@ export default {
}` }`
: `${this.$t( : `${this.$t(
'CHAT_LIST.CHAT_TIME_STAMP.LAST_ACTIVITY.NOT_ACTIVE' 'CHAT_LIST.CHAT_TIME_STAMP.LAST_ACTIVITY.NOT_ACTIVE'
)} ${this.dateFormat(this.lastActivityTimestamp)}`; )} ${dateFormat(this.lastActivityTimestamp)}`;
}, },
tooltipText() { tooltipText() {
return `${this.createdAt} return `${this.createdAt}
@@ -79,10 +82,10 @@ export default {
}, },
watch: { watch: {
lastActivityTimestamp() { lastActivityTimestamp() {
this.lastActivityAtTimeAgo = this.dynamicTime(this.lastActivityTimestamp); this.lastActivityAtTimeAgo = dynamicTime(this.lastActivityTimestamp);
}, },
createdAtTimestamp() { createdAtTimestamp() {
this.createdAtTimeAgo = this.dynamicTime(this.createdAtTimestamp); this.createdAtTimeAgo = dynamicTime(this.createdAtTimestamp);
}, },
}, },
mounted() { mounted() {
@@ -96,10 +99,8 @@ export default {
methods: { methods: {
createTimer() { createTimer() {
this.timer = setTimeout(() => { this.timer = setTimeout(() => {
this.lastActivityAtTimeAgo = this.dynamicTime( this.lastActivityAtTimeAgo = dynamicTime(this.lastActivityTimestamp);
this.lastActivityTimestamp this.createdAtTimeAgo = dynamicTime(this.createdAtTimestamp);
);
this.createdAtTimeAgo = this.dynamicTime(this.createdAtTimestamp);
this.createTimer(); this.createTimer();
}, this.refreshTime()); }, this.refreshTime());
}, },
@@ -35,13 +35,13 @@
</template> </template>
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { useAdmin } from 'dashboard/composables/useAdmin';
import { useUISettings } from 'dashboard/composables/useUISettings';
import AICTAModal from './AICTAModal.vue'; import AICTAModal from './AICTAModal.vue';
import AIAssistanceModal from './AIAssistanceModal.vue'; import AIAssistanceModal from './AIAssistanceModal.vue';
import adminMixin from 'dashboard/mixins/aiMixin'; import aiMixin from 'dashboard/mixins/aiMixin';
import aiMixin from 'dashboard/mixins/isAdmin';
import { CMD_AI_ASSIST } from 'dashboard/routes/dashboard/commands/commandBarBusEvents'; import { CMD_AI_ASSIST } from 'dashboard/routes/dashboard/commands/commandBarBusEvents';
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins'; import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import AIAssistanceCTAButton from './AIAssistanceCTAButton.vue'; import AIAssistanceCTAButton from './AIAssistanceCTAButton.vue';
export default { export default {
@@ -50,7 +50,17 @@ export default {
AICTAModal, AICTAModal,
AIAssistanceCTAButton, AIAssistanceCTAButton,
}, },
mixins: [aiMixin, keyboardEventListenerMixins, adminMixin, uiSettingsMixin], mixins: [aiMixin, keyboardEventListenerMixins],
setup() {
const { uiSettings, updateUISettings } = useUISettings();
const { isAdmin } = useAdmin();
return {
uiSettings,
updateUISettings,
isAdmin,
};
},
data: () => ({ data: () => ({
showAIAssistanceModal: false, showAIAssistanceModal: false,
showAICtaModal: false, showAICtaModal: false,
@@ -1,14 +1,14 @@
<template> <template>
<div class="px-0 min-w-0 flex-1"> <div class="flex-1 min-w-0 px-0">
<woot-modal-header <woot-modal-header
:header-title="$t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.TITLE')" :header-title="$t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.TITLE')"
:header-content="$t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.DESC')" :header-content="$t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.DESC')"
/> />
<form <form
class="flex flex-wrap flex-col modal-content" class="flex flex-col flex-wrap modal-content"
@submit.prevent="finishOpenAI" @submit.prevent="finishOpenAI"
> >
<div class="mt-2 w-full"> <div class="w-full mt-2">
<woot-input <woot-input
v-model="value" v-model="value"
type="text" type="text"
@@ -19,7 +19,7 @@
@blur="$v.value.$touch" @blur="$v.value.$touch"
/> />
</div> </div>
<div class="flex flex-row justify-between gap-2 py-2 px-0 w-full"> <div class="flex flex-row justify-between w-full gap-2 px-0 py-2">
<woot-button variant="link" @click.prevent="openOpenAIDoc"> <woot-button variant="link" @click.prevent="openOpenAIDoc">
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.BUTTONS.NEED_HELP') }} {{ $t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.BUTTONS.NEED_HELP') }}
</woot-button> </woot-button>
@@ -39,13 +39,20 @@
<script> <script>
import { required } from 'vuelidate/lib/validators'; import { required } from 'vuelidate/lib/validators';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { useAlert } from 'dashboard/composables';
import { useUISettings } from 'dashboard/composables/useUISettings';
import aiMixin from 'dashboard/mixins/aiMixin'; import aiMixin from 'dashboard/mixins/aiMixin';
import alertMixin from 'shared/mixins/alertMixin';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import { OPEN_AI_EVENTS } from 'dashboard/helper/AnalyticsHelper/events'; import { OPEN_AI_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
export default { export default {
mixins: [aiMixin, alertMixin, uiSettingsMixin], mixins: [aiMixin],
setup() {
const { updateUISettings } = useUISettings();
return {
updateUISettings,
};
},
data() { data() {
return { return {
value: '', value: '',
@@ -67,7 +74,7 @@ export default {
}, },
onDismiss() { onDismiss() {
this.showAlert( useAlert(
this.$t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.DISMISS_MESSAGE') this.$t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.DISMISS_MESSAGE')
); );
this.updateUISettings({ this.updateUISettings({
@@ -97,7 +104,7 @@ export default {
this.alertMessage = this.alertMessage =
errorMessage || this.$t('INTEGRATION_APPS.ADD.API.ERROR_MESSAGE'); errorMessage || this.$t('INTEGRATION_APPS.ADD.API.ERROR_MESSAGE');
} finally { } finally {
this.showAlert(this.alertMessage); useAlert(this.alertMessage);
} }
}, },
openOpenAIDoc() { openOpenAIDoc() {
@@ -26,13 +26,12 @@
</template> </template>
<script> <script>
import { useAlert } from 'dashboard/composables';
import Spinner from 'shared/components/Spinner.vue'; import Spinner from 'shared/components/Spinner.vue';
import alertMixin from 'shared/mixins/alertMixin';
export default { export default {
components: { components: {
Spinner, Spinner,
}, },
mixins: [alertMixin],
props: { props: {
value: { value: {
type: Array, type: Array,
@@ -71,7 +70,7 @@ export default {
} catch (error) { } catch (error) {
this.uploadState = 'failed'; this.uploadState = 'failed';
this.label = this.$t('AUTOMATION.ATTACHMENT.LABEL_UPLOAD_FAILED'); this.label = this.$t('AUTOMATION.ATTACHMENT.LABEL_UPLOAD_FAILED');
this.showAlert(this.$t('AUTOMATION.ATTACHMENT.UPLOAD_ERROR')); useAlert(this.$t('AUTOMATION.ATTACHMENT.UPLOAD_ERROR'));
} }
}, },
}, },
@@ -33,7 +33,7 @@
import AddLabel from 'shared/components/ui/dropdown/AddLabel.vue'; import AddLabel from 'shared/components/ui/dropdown/AddLabel.vue';
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins'; import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
import LabelDropdown from 'shared/components/ui/label/LabelDropdown.vue'; import LabelDropdown from 'shared/components/ui/label/LabelDropdown.vue';
import adminMixin from 'dashboard/mixins/isAdmin'; import { useAdmin } from 'dashboard/composables/useAdmin';
export default { export default {
components: { components: {
@@ -41,7 +41,7 @@ export default {
LabelDropdown, LabelDropdown,
}, },
mixins: [adminMixin, keyboardEventListenerMixins], mixins: [keyboardEventListenerMixins],
props: { props: {
allLabels: { allLabels: {
@@ -54,6 +54,13 @@ export default {
}, },
}, },
setup() {
const { isAdmin } = useAdmin();
return {
isAdmin,
};
},
data() { data() {
return { return {
showSearchDropdownLabel: false, showSearchDropdownLabel: false,
@@ -15,10 +15,9 @@
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import DyteAPI from 'dashboard/api/integrations/dyte'; import DyteAPI from 'dashboard/api/integrations/dyte';
import alertMixin from 'shared/mixins/alertMixin'; import { useAlert } from 'dashboard/composables';
export default { export default {
mixins: [alertMixin],
props: { props: {
conversationId: { conversationId: {
type: Number, type: Number,
@@ -47,7 +46,7 @@ export default {
try { try {
await DyteAPI.createAMeeting(this.conversationId); await DyteAPI.createAMeeting(this.conversationId);
} catch (error) { } catch (error) {
this.showAlert(this.$t('INTEGRATION_SETTINGS.DYTE.CREATE_ERROR')); useAlert(this.$t('INTEGRATION_SETTINGS.DYTE.CREATE_ERROR'));
} finally { } finally {
this.isLoading = false; this.isLoading = false;
} }
@@ -11,7 +11,7 @@ import 'videojs-record/dist/css/videojs.record.css';
import videojs from 'video.js'; import videojs from 'video.js';
import alertMixin from '../../../../shared/mixins/alertMixin'; import { useAlert } from 'dashboard/composables';
import Recorder from 'opus-recorder'; import Recorder from 'opus-recorder';
@@ -50,7 +50,6 @@ const RECORDER_CONFIG = {
export default { export default {
name: 'WootAudioRecorder', name: 'WootAudioRecorder',
mixins: [alertMixin],
props: { props: {
audioRecordFormat: { audioRecordFormat: {
type: String, type: String,
@@ -188,14 +187,10 @@ export default {
deviceErrorName?.includes('notallowederror') || deviceErrorName?.includes('notallowederror') ||
deviceErrorName?.includes('permissiondeniederror') deviceErrorName?.includes('permissiondeniederror')
) { ) {
this.showAlert( useAlert(this.$t('CONVERSATION.REPLYBOX.TIP_AUDIORECORDER_PERMISSION'));
this.$t('CONVERSATION.REPLYBOX.TIP_AUDIORECORDER_PERMISSION')
);
this.fireStateRecorderChanged('notallowederror'); this.fireStateRecorderChanged('notallowederror');
} else { } else {
this.showAlert( useAlert(this.$t('CONVERSATION.REPLYBOX.TIP_AUDIORECORDER_ERROR'));
this.$t('CONVERSATION.REPLYBOX.TIP_AUDIORECORDER_ERROR')
);
} }
}, },
formatTimeProgress() { formatTimeProgress() {
@@ -80,8 +80,7 @@ import {
hasPressedCommandAndEnter, hasPressedCommandAndEnter,
} from 'shared/helpers/KeyboardHelpers'; } from 'shared/helpers/KeyboardHelpers';
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins'; import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
import uiSettingsMixin from 'dashboard/mixins/uiSettings'; import { useUISettings } from 'dashboard/composables/useUISettings';
import { isEditorHotKeyEnabled } from 'dashboard/mixins/uiSettings';
import { import {
replaceVariablesInMessage, replaceVariablesInMessage,
createTypingIndicator, createTypingIndicator,
@@ -89,7 +88,7 @@ import {
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events'; import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
import { checkFileSizeLimit } from 'shared/helpers/FileHelper'; import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
import { uploadFile } from 'dashboard/helper/uploadHelper'; import { uploadFile } from 'dashboard/helper/uploadHelper';
import alertMixin from 'shared/mixins/alertMixin'; import { useAlert } from 'dashboard/composables';
import { import {
MESSAGE_EDITOR_MENU_OPTIONS, MESSAGE_EDITOR_MENU_OPTIONS,
MESSAGE_EDITOR_IMAGE_RESIZES, MESSAGE_EDITOR_IMAGE_RESIZES,
@@ -119,7 +118,7 @@ const createState = (
export default { export default {
name: 'WootMessageEditor', name: 'WootMessageEditor',
components: { TagAgents, CannedResponse, VariableList }, components: { TagAgents, CannedResponse, VariableList },
mixins: [keyboardEventListenerMixins, uiSettingsMixin, alertMixin], mixins: [keyboardEventListenerMixins],
props: { props: {
value: { type: String, default: '' }, value: { type: String, default: '' },
editorId: { type: String, default: '' }, editorId: { type: String, default: '' },
@@ -139,6 +138,19 @@ export default {
channelType: { type: String, default: '' }, channelType: { type: String, default: '' },
showImageResizeToolbar: { type: Boolean, default: false }, // A kill switch to show or hide the image toolbar showImageResizeToolbar: { type: Boolean, default: false }, // A kill switch to show or hide the image toolbar
}, },
setup() {
const {
uiSettings,
isEditorHotKeyEnabled,
fetchSignatureFlagFromUISettings,
} = useUISettings();
return {
uiSettings,
isEditorHotKeyEnabled,
fetchSignatureFlagFromUISettings,
};
},
data() { data() {
return { return {
typingIndicator: createTypingIndicator( typingIndicator: createTypingIndicator(
@@ -278,7 +290,7 @@ export default {
// this is considered the source of truth, we watch this property // this is considered the source of truth, we watch this property
// on change, we toggle the signature in the editor // on change, we toggle the signature in the editor
if (this.allowSignature && !this.isPrivate && this.channelType) { if (this.allowSignature && !this.isPrivate && this.channelType) {
return this.fetchSignatureFlagFromUiSettings(this.channelType); return this.fetchSignatureFlagFromUISettings(this.channelType);
} }
return false; return false;
@@ -521,10 +533,10 @@ export default {
} }
}, },
isEnterToSendEnabled() { isEnterToSendEnabled() {
return isEditorHotKeyEnabled(this.uiSettings, 'enter'); return this.isEditorHotKeyEnabled('enter');
}, },
isCmdPlusEnterToSendEnabled() { isCmdPlusEnterToSendEnabled() {
return isEditorHotKeyEnabled(this.uiSettings, 'cmd_enter'); return this.isEditorHotKeyEnabled('cmd_enter');
}, },
getKeyboardEvents() { getKeyboardEvents() {
return { return {
@@ -611,7 +623,7 @@ export default {
if (checkFileSizeLimit(file, MAXIMUM_FILE_UPLOAD_SIZE)) { if (checkFileSizeLimit(file, MAXIMUM_FILE_UPLOAD_SIZE)) {
this.uploadImageToStorage(file); this.uploadImageToStorage(file);
} else { } else {
this.showAlert( useAlert(
this.$t( this.$t(
'PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.IMAGE_UPLOAD_SIZE_ERROR', 'PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.IMAGE_UPLOAD_SIZE_ERROR',
{ {
@@ -629,13 +641,13 @@ export default {
if (fileUrl) { if (fileUrl) {
this.onImageInsertInEditor(fileUrl); this.onImageInsertInEditor(fileUrl);
} }
this.showAlert( useAlert(
this.$t( this.$t(
'PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.IMAGE_UPLOAD_SUCCESS' 'PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.IMAGE_UPLOAD_SUCCESS'
) )
); );
} catch (error) { } catch (error) {
this.showAlert( useAlert(
this.$t( this.$t(
'PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.IMAGE_UPLOAD_ERROR' 'PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.IMAGE_UPLOAD_ERROR'
) )
@@ -24,9 +24,9 @@ import {
Selection, Selection,
} from '@chatwoot/prosemirror-schema'; } from '@chatwoot/prosemirror-schema';
import { checkFileSizeLimit } from 'shared/helpers/FileHelper'; import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
import alertMixin from 'shared/mixins/alertMixin'; import { useAlert } from 'dashboard/composables';
import { useUISettings } from 'dashboard/composables/useUISettings';
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins'; import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
const MAXIMUM_FILE_UPLOAD_SIZE = 4; // in MB const MAXIMUM_FILE_UPLOAD_SIZE = 4; // in MB
const createState = ( const createState = (
@@ -51,13 +51,21 @@ const createState = (
}; };
export default { export default {
mixins: [keyboardEventListenerMixins, uiSettingsMixin, alertMixin], mixins: [keyboardEventListenerMixins],
props: { props: {
value: { type: String, default: '' }, value: { type: String, default: '' },
editorId: { type: String, default: '' }, editorId: { type: String, default: '' },
placeholder: { type: String, default: '' }, placeholder: { type: String, default: '' },
enabledMenuOptions: { type: Array, default: () => [] }, enabledMenuOptions: { type: Array, default: () => [] },
}, },
setup() {
const { uiSettings, updateUISettings } = useUISettings();
return {
uiSettings,
updateUISettings,
};
},
data() { data() {
return { return {
editorView: null, editorView: null,
@@ -108,7 +116,7 @@ export default {
if (checkFileSizeLimit(file, MAXIMUM_FILE_UPLOAD_SIZE)) { if (checkFileSizeLimit(file, MAXIMUM_FILE_UPLOAD_SIZE)) {
this.uploadImageToStorage(file); this.uploadImageToStorage(file);
} else { } else {
this.showAlert( useAlert(
this.$t('HELP_CENTER.ARTICLE_EDITOR.IMAGE_UPLOAD.ERROR_FILE_SIZE', { this.$t('HELP_CENTER.ARTICLE_EDITOR.IMAGE_UPLOAD.ERROR_FILE_SIZE', {
size: MAXIMUM_FILE_UPLOAD_SIZE, size: MAXIMUM_FILE_UPLOAD_SIZE,
}) })
@@ -127,13 +135,9 @@ export default {
if (fileUrl) { if (fileUrl) {
this.onImageUploadStart(fileUrl); this.onImageUploadStart(fileUrl);
} }
this.showAlert( useAlert(this.$t('HELP_CENTER.ARTICLE_EDITOR.IMAGE_UPLOAD.SUCCESS'));
this.$t('HELP_CENTER.ARTICLE_EDITOR.IMAGE_UPLOAD.SUCCESS')
);
} catch (error) { } catch (error) {
this.showAlert( useAlert(this.$t('HELP_CENTER.ARTICLE_EDITOR.IMAGE_UPLOAD.ERROR'));
this.$t('HELP_CENTER.ARTICLE_EDITOR.IMAGE_UPLOAD.ERROR')
);
} }
}, },
onImageUploadStart(fileUrl) { onImageUploadStart(fileUrl) {
@@ -134,10 +134,10 @@
</template> </template>
<script> <script>
import { useUISettings } from 'dashboard/composables/useUISettings';
import FileUpload from 'vue-upload-component'; import FileUpload from 'vue-upload-component';
import * as ActiveStorage from 'activestorage'; import * as ActiveStorage from 'activestorage';
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins'; import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import inboxMixin from 'shared/mixins/inboxMixin'; import inboxMixin from 'shared/mixins/inboxMixin';
import { FEATURE_FLAGS } from 'dashboard/featureFlags'; import { FEATURE_FLAGS } from 'dashboard/featureFlags';
import { import {
@@ -153,7 +153,7 @@ import { mapGetters } from 'vuex';
export default { export default {
name: 'ReplyBottomPanel', name: 'ReplyBottomPanel',
components: { FileUpload, VideoCallButton, AIAssistanceButton }, components: { FileUpload, VideoCallButton, AIAssistanceButton },
mixins: [keyboardEventListenerMixins, uiSettingsMixin, inboxMixin], mixins: [keyboardEventListenerMixins, inboxMixin],
props: { props: {
mode: { mode: {
type: String, type: String,
@@ -248,6 +248,15 @@ export default {
required: true, required: true,
}, },
}, },
setup() {
const { setSignatureFlagForInbox, fetchSignatureFlagFromUISettings } =
useUISettings();
return {
setSignatureFlagForInbox,
fetchSignatureFlagFromUISettings,
};
},
computed: { computed: {
...mapGetters({ ...mapGetters({
accountId: 'getCurrentAccountId', accountId: 'getCurrentAccountId',
@@ -320,7 +329,7 @@ export default {
}, },
sendWithSignature() { sendWithSignature() {
// channelType is sourced from inboxMixin // channelType is sourced from inboxMixin
return this.fetchSignatureFlagFromUiSettings(this.channelType); return this.fetchSignatureFlagFromUISettings(this.channelType);
}, },
signatureToggleTooltip() { signatureToggleTooltip() {
return this.sendWithSignature return this.sendWithSignature
@@ -79,7 +79,7 @@
</template> </template>
<script> <script>
import alertMixin from 'shared/mixins/alertMixin'; import { useAlert } from 'dashboard/composables';
import { required, requiredIf } from 'vuelidate/lib/validators'; import { required, requiredIf } from 'vuelidate/lib/validators';
import FilterInputBox from '../FilterInput/Index.vue'; import FilterInputBox from '../FilterInput/Index.vue';
import languages from './advancedFilterItems/languages'; import languages from './advancedFilterItems/languages';
@@ -89,12 +89,13 @@ import { filterAttributeGroups } from './advancedFilterItems';
import filterMixin from 'shared/mixins/filterMixin'; import filterMixin from 'shared/mixins/filterMixin';
import * as OPERATORS from 'dashboard/components/widgets/FilterInput/FilterOperatorTypes.js'; import * as OPERATORS from 'dashboard/components/widgets/FilterInput/FilterOperatorTypes.js';
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events'; import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
import cloneObject from 'dashboard/helpers/clone';
export default { export default {
components: { components: {
FilterInputBox, FilterInputBox,
}, },
mixins: [alertMixin, filterMixin], mixins: [filterMixin],
props: { props: {
onClose: { onClose: {
type: Function, type: Function,
@@ -341,7 +342,7 @@ export default {
}, },
removeFilter(index) { removeFilter(index) {
if (this.appliedFilters.length <= 1) { if (this.appliedFilters.length <= 1) {
this.showAlert(this.$t('FILTER.FILTER_DELETE_ERROR')); useAlert(this.$t('FILTER.FILTER_DELETE_ERROR'));
} else { } else {
this.appliedFilters.splice(index, 1); this.appliedFilters.splice(index, 1);
} }
@@ -351,7 +352,7 @@ export default {
if (this.$v.$invalid) return; if (this.$v.$invalid) return;
this.$store.dispatch( this.$store.dispatch(
'setConversationFilters', 'setConversationFilters',
JSON.parse(JSON.stringify(this.appliedFilters)) cloneObject(this.appliedFilters)
); );
this.$emit('applyFilter', this.appliedFilters); this.$emit('applyFilter', this.appliedFilters);
this.$track(CONVERSATION_EVENTS.APPLY_FILTER, { this.$track(CONVERSATION_EVENTS.APPLY_FILTER, {
@@ -12,10 +12,10 @@
<div <div
v-if="showActionsDropdown" v-if="showActionsDropdown"
v-on-clickaway="closeDropdown" v-on-clickaway="closeDropdown"
class="dropdown-pane dropdown-pane--open mt-1 right-0 basic-filter" class="right-0 mt-1 dropdown-pane dropdown-pane--open basic-filter"
> >
<div class="items-center flex justify-between last:mt-4"> <div class="flex items-center justify-between last:mt-4">
<span class="text-slate-800 dark:text-slate-100 text-xs font-medium">{{ <span class="text-xs font-medium text-slate-800 dark:text-slate-100">{{
$t('CHAT_LIST.CHAT_SORT.STATUS') $t('CHAT_LIST.CHAT_SORT.STATUS')
}}</span> }}</span>
<filter-item <filter-item
@@ -26,8 +26,8 @@
@onChangeFilter="onChangeFilter" @onChangeFilter="onChangeFilter"
/> />
</div> </div>
<div class="items-center flex justify-between last:mt-4"> <div class="flex items-center justify-between last:mt-4">
<span class="text-slate-800 dark:text-slate-100 text-xs font-medium">{{ <span class="text-xs font-medium text-slate-800 dark:text-slate-100">{{
$t('CHAT_LIST.CHAT_SORT.ORDER_BY') $t('CHAT_LIST.CHAT_SORT.ORDER_BY')
}}</span> }}</span>
<filter-item <filter-item
@@ -46,13 +46,19 @@
import wootConstants from 'dashboard/constants/globals'; import wootConstants from 'dashboard/constants/globals';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import FilterItem from './FilterItem.vue'; import FilterItem from './FilterItem.vue';
import uiSettingsMixin from 'dashboard/mixins/uiSettings'; import { useUISettings } from 'dashboard/composables/useUISettings';
export default { export default {
components: { components: {
FilterItem, FilterItem,
}, },
mixins: [uiSettingsMixin], setup() {
const { updateUISettings } = useUISettings();
return {
updateUISettings,
};
},
data() { data() {
return { return {
showActionsDropdown: false, showActionsDropdown: false,
@@ -120,13 +120,11 @@ import { mapGetters } from 'vuex';
import Thumbnail from '../Thumbnail.vue'; import Thumbnail from '../Thumbnail.vue';
import MessagePreview from './MessagePreview.vue'; import MessagePreview from './MessagePreview.vue';
import conversationMixin from '../../../mixins/conversations'; import conversationMixin from '../../../mixins/conversations';
import timeMixin from '../../../mixins/time';
import router from '../../../routes'; import router from '../../../routes';
import { frontendURL, conversationUrl } from '../../../helper/URLHelper'; import { frontendURL, conversationUrl } from '../../../helper/URLHelper';
import InboxName from '../InboxName.vue'; import InboxName from '../InboxName.vue';
import inboxMixin from 'shared/mixins/inboxMixin'; import inboxMixin from 'shared/mixins/inboxMixin';
import ConversationContextMenu from './contextMenu/Index.vue'; import ConversationContextMenu from './contextMenu/Index.vue';
import alertMixin from 'shared/mixins/alertMixin';
import TimeAgo from 'dashboard/components/ui/TimeAgo.vue'; import TimeAgo from 'dashboard/components/ui/TimeAgo.vue';
import CardLabels from './conversationCardComponents/CardLabels.vue'; import CardLabels from './conversationCardComponents/CardLabels.vue';
import PriorityMark from './PriorityMark.vue'; import PriorityMark from './PriorityMark.vue';
@@ -143,8 +141,7 @@ export default {
PriorityMark, PriorityMark,
SLACardLabel, SLACardLabel,
}, },
mixins: [inboxMixin, conversationMixin],
mixins: [inboxMixin, timeMixin, conversationMixin, alertMixin],
props: { props: {
activeLabel: { activeLabel: {
type: String, type: String,
@@ -1,7 +1,7 @@
<!-- eslint-disable vue/no-mutating-props --> <!-- eslint-disable vue/no-mutating-props -->
<template> <template>
<woot-modal :show.sync="show" :on-close="onCancel"> <woot-modal :show.sync="show" :on-close="onCancel">
<div class="h-auto overflow-auto flex flex-col"> <div class="flex flex-col h-auto overflow-auto">
<woot-modal-header <woot-modal-header
:header-title="$t('EMAIL_TRANSCRIPT.TITLE')" :header-title="$t('EMAIL_TRANSCRIPT.TITLE')"
:header-content="$t('EMAIL_TRANSCRIPT.DESC')" :header-content="$t('EMAIL_TRANSCRIPT.DESC')"
@@ -61,7 +61,7 @@
</label> </label>
</div> </div>
</div> </div>
<div class="flex flex-row justify-end gap-2 py-2 px-0 w-full"> <div class="flex flex-row justify-end w-full gap-2 px-0 py-2">
<woot-submit-button <woot-submit-button
:button-text="$t('EMAIL_TRANSCRIPT.SUBMIT')" :button-text="$t('EMAIL_TRANSCRIPT.SUBMIT')"
:disabled="!isFormValid" :disabled="!isFormValid"
@@ -77,9 +77,8 @@
<script> <script>
import { required, minLength, email } from 'vuelidate/lib/validators'; import { required, minLength, email } from 'vuelidate/lib/validators';
import alertMixin from 'shared/mixins/alertMixin'; import { useAlert } from 'dashboard/composables';
export default { export default {
mixins: [alertMixin],
props: { props: {
show: { show: {
type: Boolean, type: Boolean,
@@ -142,10 +141,10 @@ export default {
email: this.selectedEmailAddress, email: this.selectedEmailAddress,
conversationId: this.currentChat.id, conversationId: this.currentChat.id,
}); });
this.showAlert(this.$t('EMAIL_TRANSCRIPT.SEND_EMAIL_SUCCESS')); useAlert(this.$t('EMAIL_TRANSCRIPT.SEND_EMAIL_SUCCESS'));
this.onCancel(); this.onCancel();
} catch (error) { } catch (error) {
this.showAlert(this.$t('EMAIL_TRANSCRIPT.SEND_EMAIL_ERROR')); useAlert(this.$t('EMAIL_TRANSCRIPT.SEND_EMAIL_ERROR'));
} finally { } finally {
this.isSubmitting = false; this.isSubmitting = false;
} }
@@ -35,7 +35,7 @@
</template> </template>
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import adminMixin from 'dashboard/mixins/isAdmin'; import { useAdmin } from 'dashboard/composables/useAdmin';
import accountMixin from 'dashboard/mixins/account'; import accountMixin from 'dashboard/mixins/account';
import OnboardingView from '../OnboardingView.vue'; import OnboardingView from '../OnboardingView.vue';
import EmptyStateMessage from './EmptyStateMessage.vue'; import EmptyStateMessage from './EmptyStateMessage.vue';
@@ -45,13 +45,19 @@ export default {
OnboardingView, OnboardingView,
EmptyStateMessage, EmptyStateMessage,
}, },
mixins: [accountMixin, adminMixin], mixins: [accountMixin],
props: { props: {
isOnExpandedLayout: { isOnExpandedLayout: {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
}, },
setup() {
const { isAdmin } = useAdmin();
return {
isAdmin,
};
},
computed: { computed: {
...mapGetters({ ...mapGetters({
currentChat: 'getSelectedChat', currentChat: 'getSelectedChat',
@@ -157,8 +157,7 @@ import ContextMenu from 'dashboard/modules/conversations/components/MessageConte
import InstagramStory from './bubble/InstagramStory.vue'; import InstagramStory from './bubble/InstagramStory.vue';
import InstagramStoryReply from './bubble/InstagramStoryReply.vue'; import InstagramStoryReply from './bubble/InstagramStoryReply.vue';
import Spinner from 'shared/components/Spinner.vue'; import Spinner from 'shared/components/Spinner.vue';
import alertMixin from 'shared/mixins/alertMixin'; import { CONTENT_TYPES } from 'shared/constants/contentType';
import contentTypeMixin from 'shared/mixins/contentTypeMixin';
import { MESSAGE_TYPE, MESSAGE_STATUS } from 'shared/constants/messages'; import { MESSAGE_TYPE, MESSAGE_STATUS } from 'shared/constants/messages';
import { generateBotMessageContent } from './helpers/botMessageContentHelper'; import { generateBotMessageContent } from './helpers/botMessageContentHelper';
import { BUS_EVENTS } from 'shared/constants/busEvents'; import { BUS_EVENTS } from 'shared/constants/busEvents';
@@ -184,7 +183,7 @@ export default {
InstagramStoryReply, InstagramStoryReply,
Spinner, Spinner,
}, },
mixins: [alertMixin, messageFormatterMixin, contentTypeMixin], mixins: [messageFormatterMixin],
props: { props: {
data: { data: {
type: Object, type: Object,
@@ -482,6 +481,9 @@ export default {
} }
return ''; return '';
}, },
isEmailContentType() {
return this.contentType === CONTENT_TYPES.INCOMING_EMAIL;
},
}, },
watch: { watch: {
data() { data() {
@@ -1,5 +1,5 @@
<template> <template>
<div class="flex actions--container relative items-center gap-2"> <div class="relative flex items-center gap-2 actions--container">
<woot-button <woot-button
v-if="!currentChat.muted" v-if="!currentChat.muted"
v-tooltip="$t('CONTACT_PANEL.MUTE_CONTACT')" v-tooltip="$t('CONTACT_PANEL.MUTE_CONTACT')"
@@ -37,7 +37,7 @@
</template> </template>
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import alertMixin from 'shared/mixins/alertMixin'; import { useAlert } from 'dashboard/composables';
import EmailTranscriptModal from './EmailTranscriptModal.vue'; import EmailTranscriptModal from './EmailTranscriptModal.vue';
import ResolveAction from '../../buttons/ResolveAction.vue'; import ResolveAction from '../../buttons/ResolveAction.vue';
import { import {
@@ -51,7 +51,6 @@ export default {
EmailTranscriptModal, EmailTranscriptModal,
ResolveAction, ResolveAction,
}, },
mixins: [alertMixin],
data() { data() {
return { return {
showEmailActionsModal: false, showEmailActionsModal: false,
@@ -73,11 +72,11 @@ export default {
methods: { methods: {
mute() { mute() {
this.$store.dispatch('muteConversation', this.currentChat.id); this.$store.dispatch('muteConversation', this.currentChat.id);
this.showAlert(this.$t('CONTACT_PANEL.MUTED_SUCCESS')); useAlert(this.$t('CONTACT_PANEL.MUTED_SUCCESS'));
}, },
unmute() { unmute() {
this.$store.dispatch('unmuteConversation', this.currentChat.id); this.$store.dispatch('unmuteConversation', this.currentChat.id);
this.showAlert(this.$t('CONTACT_PANEL.UNMUTED_SUCCESS')); useAlert(this.$t('CONTACT_PANEL.UNMUTED_SUCCESS'));
}, },
toggleEmailActionsModal() { toggleEmailActionsModal() {
this.showEmailActionsModal = !this.showEmailActionsModal; this.showEmailActionsModal = !this.showEmailActionsModal;
@@ -153,7 +153,8 @@
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import alertMixin from 'shared/mixins/alertMixin'; import { useAlert } from 'dashboard/composables';
import { useUISettings } from 'dashboard/composables/useUISettings';
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins'; import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
import CannedResponse from './CannedResponse.vue'; import CannedResponse from './CannedResponse.vue';
@@ -180,10 +181,8 @@ import {
import WhatsappTemplates from './WhatsappTemplates/Modal.vue'; import WhatsappTemplates from './WhatsappTemplates/Modal.vue';
import { MESSAGE_MAX_LENGTH } from 'shared/helpers/MessageTypeHelper'; import { MESSAGE_MAX_LENGTH } from 'shared/helpers/MessageTypeHelper';
import inboxMixin, { INBOX_FEATURES } from 'shared/mixins/inboxMixin'; import inboxMixin, { INBOX_FEATURES } from 'shared/mixins/inboxMixin';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import { trimContent, debounce } from '@chatwoot/utils'; import { trimContent, debounce } from '@chatwoot/utils';
import wootConstants from 'dashboard/constants/globals'; import wootConstants from 'dashboard/constants/globals';
import { isEditorHotKeyEnabled } from 'dashboard/mixins/uiSettings';
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events'; import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
import rtlMixin from 'shared/mixins/rtlMixin'; import rtlMixin from 'shared/mixins/rtlMixin';
import fileUploadMixin from 'dashboard/mixins/fileUploadMixin'; import fileUploadMixin from 'dashboard/mixins/fileUploadMixin';
@@ -218,8 +217,6 @@ export default {
}, },
mixins: [ mixins: [
inboxMixin, inboxMixin,
uiSettingsMixin,
alertMixin,
messageFormatterMixin, messageFormatterMixin,
rtlMixin, rtlMixin,
fileUploadMixin, fileUploadMixin,
@@ -231,6 +228,21 @@ export default {
default: false, default: false,
}, },
}, },
setup() {
const {
uiSettings,
updateUISettings,
isEditorHotKeyEnabled,
fetchSignatureFlagFromUISettings,
} = useUISettings();
return {
uiSettings,
updateUISettings,
isEditorHotKeyEnabled,
fetchSignatureFlagFromUISettings,
};
},
data() { data() {
return { return {
message: '', message: '',
@@ -310,7 +322,7 @@ export default {
agentId, agentId,
}) })
.then(() => { .then(() => {
this.showAlert(this.$t('CONVERSATION.CHANGE_AGENT')); useAlert(this.$t('CONVERSATION.CHANGE_AGENT'));
}); });
}, },
}, },
@@ -406,7 +418,7 @@ export default {
if (this.isPrivate) { if (this.isPrivate) {
sendMessageText = this.$t('CONVERSATION.REPLYBOX.CREATE'); sendMessageText = this.$t('CONVERSATION.REPLYBOX.CREATE');
} }
const keyLabel = isEditorHotKeyEnabled(this.uiSettings, 'cmd_enter') const keyLabel = this.isEditorHotKeyEnabled('cmd_enter')
? '(⌘ + ↵)' ? '(⌘ + ↵)'
: '(↵)'; : '(↵)';
return `${sendMessageText} ${keyLabel}`; return `${sendMessageText} ${keyLabel}`;
@@ -479,7 +491,7 @@ export default {
return !!this.signatureToApply; return !!this.signatureToApply;
}, },
sendWithSignature() { sendWithSignature() {
return this.fetchSignatureFlagFromUiSettings(this.channelType); return this.fetchSignatureFlagFromUISettings(this.channelType);
}, },
editorMessageKey() { editorMessageKey() {
const { editor_message_key: isEnabled } = this.uiSettings; const { editor_message_key: isEnabled } = this.uiSettings;
@@ -748,7 +760,7 @@ export default {
!this.showCannedMenu && !this.showCannedMenu &&
!this.showVariablesMenu && !this.showVariablesMenu &&
this.isFocused && this.isFocused &&
isEditorHotKeyEnabled(this.uiSettings, selectedKey) this.isEditorHotKeyEnabled(selectedKey)
); );
}, },
onPaste(e) { onPaste(e) {
@@ -880,7 +892,7 @@ export default {
} catch (error) { } catch (error) {
const errorMessage = const errorMessage =
error?.response?.data?.error || this.$t('CONVERSATION.MESSAGE_ERROR'); error?.response?.data?.error || this.$t('CONVERSATION.MESSAGE_ERROR');
this.showAlert(errorMessage); useAlert(errorMessage);
} }
}, },
async onSendWhatsAppReply(messagePayload) { async onSendWhatsAppReply(messagePayload) {
@@ -77,10 +77,10 @@
import { MESSAGE_TYPE, MESSAGE_STATUS } from 'shared/constants/messages'; import { MESSAGE_TYPE, MESSAGE_STATUS } from 'shared/constants/messages';
import inboxMixin from 'shared/mixins/inboxMixin'; import inboxMixin from 'shared/mixins/inboxMixin';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import timeMixin from '../../../../mixins/time'; import { messageTimestamp } from 'shared/helpers/timeHelper';
export default { export default {
mixins: [inboxMixin, timeMixin], mixins: [inboxMixin],
props: { props: {
sender: { sender: {
type: Object, type: Object,
@@ -159,7 +159,7 @@ export default {
return MESSAGE_STATUS.SENT === this.messageStatus; return MESSAGE_STATUS.SENT === this.messageStatus;
}, },
readableTime() { readableTime() {
return this.messageTimestamp(this.createdAt, 'LLL d, h:mm a'); return messageTimestamp(this.createdAt, 'LLL d, h:mm a');
}, },
screenName() { screenName() {
const { additional_attributes: additionalAttributes = {} } = const { additional_attributes: additionalAttributes = {} } =
@@ -17,14 +17,13 @@
</template> </template>
<script> <script>
import { useAlert } from 'dashboard/composables';
import { import {
DuplicateContactException, DuplicateContactException,
ExceptionWithMessage, ExceptionWithMessage,
} from 'shared/helpers/CustomErrors'; } from 'shared/helpers/CustomErrors';
import alertMixin from 'shared/mixins/alertMixin';
export default { export default {
mixins: [alertMixin],
props: { props: {
name: { name: {
type: String, type: String,
@@ -52,18 +51,18 @@ export default {
'contacts/create', 'contacts/create',
this.getContactObject() this.getContactObject()
); );
this.showAlert(this.$t('CONTACT_FORM.SUCCESS_MESSAGE')); useAlert(this.$t('CONTACT_FORM.SUCCESS_MESSAGE'));
} }
this.openContactNewTab(contact.id); this.openContactNewTab(contact.id);
} catch (error) { } catch (error) {
if (error instanceof DuplicateContactException) { if (error instanceof DuplicateContactException) {
if (error.data.includes('phone_number')) { if (error.data.includes('phone_number')) {
this.showAlert(this.$t('CONTACT_FORM.FORM.PHONE_NUMBER.DUPLICATE')); useAlert(this.$t('CONTACT_FORM.FORM.PHONE_NUMBER.DUPLICATE'));
} }
} else if (error instanceof ExceptionWithMessage) { } else if (error instanceof ExceptionWithMessage) {
this.showAlert(error.data); useAlert(error.data);
} else { } else {
this.showAlert(this.$t('CONTACT_FORM.ERROR_MESSAGE')); useAlert(this.$t('CONTACT_FORM.ERROR_MESSAGE'));
} }
} }
}, },
@@ -31,10 +31,9 @@
<script> <script>
import DyteAPI from 'dashboard/api/integrations/dyte'; import DyteAPI from 'dashboard/api/integrations/dyte';
import { buildDyteURL } from 'shared/helpers/IntegrationHelper'; import { buildDyteURL } from 'shared/helpers/IntegrationHelper';
import alertMixin from 'shared/mixins/alertMixin'; import { useAlert } from 'dashboard/composables';
export default { export default {
mixins: [alertMixin],
props: { props: {
messageId: { messageId: {
type: Number, type: Number,
@@ -61,7 +60,7 @@ export default {
await DyteAPI.addParticipantToMeeting(this.messageId); await DyteAPI.addParticipantToMeeting(this.messageId);
this.dyteAuthToken = authToken; this.dyteAuthToken = authToken;
} catch (err) { } catch (err) {
this.showAlert(this.$t('INTEGRATION_SETTINGS.DYTE.JOIN_ERROR')); useAlert(this.$t('INTEGRATION_SETTINGS.DYTE.JOIN_ERROR'));
} finally { } finally {
this.isLoading = false; this.isLoading = false;
} }
@@ -12,7 +12,7 @@
@click="onClose" @click="onClose"
> >
<div <div
class="bg-white dark:bg-slate-900 z-10 flex items-center justify-between w-full h-16 px-6 py-2" class="z-10 flex items-center justify-between w-full h-16 px-6 py-2 bg-white dark:bg-slate-900"
@click.stop @click.stop
> >
<div <div
@@ -167,7 +167,7 @@
/> />
</div> </div>
</div> </div>
<div class="flex items-center justify-center w-full h-16 px-6 py-2 z-10"> <div class="z-10 flex items-center justify-center w-full h-16 px-6 py-2">
<div <div
class="items-center rounded-sm flex font-semibold justify-center min-w-[5rem] p-1 bg-slate-25 dark:bg-slate-800 text-slate-600 dark:text-slate-200 text-sm" class="items-center rounded-sm flex font-semibold justify-center min-w-[5rem] p-1 bg-slate-25 dark:bg-slate-800 text-slate-600 dark:text-slate-200 text-sm"
> >
@@ -182,7 +182,7 @@
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins'; import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
import timeMixin from 'dashboard/mixins/time'; import { messageTimestamp } from 'shared/helpers/timeHelper';
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue'; import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
@@ -199,7 +199,7 @@ export default {
components: { components: {
Thumbnail, Thumbnail,
}, },
mixins: [keyboardEventListenerMixins, timeMixin], mixins: [keyboardEventListenerMixins],
props: { props: {
show: { show: {
type: Boolean, type: Boolean,
@@ -236,7 +236,7 @@ export default {
readableTime() { readableTime() {
const { created_at: createdAt } = this.activeAttachment; const { created_at: createdAt } = this.activeAttachment;
if (!createdAt) return ''; if (!createdAt) return '';
return this.messageTimestamp(createdAt, 'LLL d yyyy, h:mm a') || ''; return messageTimestamp(createdAt, 'LLL d yyyy, h:mm a') || '';
}, },
isImage() { isImage() {
return this.activeFileType === ALLOWED_FILE_TYPES.IMAGE; return this.activeFileType === ALLOWED_FILE_TYPES.IMAGE;
@@ -85,11 +85,11 @@ describe('MoveActions', () => {
it('shows alert', async () => { it('shows alert', async () => {
await moreActions.find('button:first-child').trigger('click'); await moreActions.find('button:first-child').trigger('click');
expect(emitter.emit).toBeCalledWith( expect(emitter.emit).toBeCalledWith('newToastMessage', {
'newToastMessage', message:
'This contact is blocked successfully. You will not be notified of any future conversations.', 'This contact is blocked successfully. You will not be notified of any future conversations.',
undefined action: null,
); });
}); });
}); });
@@ -111,11 +111,10 @@ describe('MoveActions', () => {
it('shows alert', async () => { it('shows alert', async () => {
await moreActions.find('button:first-child').trigger('click'); await moreActions.find('button:first-child').trigger('click');
expect(emitter.emit).toBeCalledWith( expect(emitter.emit).toBeCalledWith('newToastMessage', {
'newToastMessage', message: 'This contact is unblocked successfully.',
'This contact is unblocked successfully.', action: null,
undefined });
);
}); });
}); });
}); });
+12 -3
View File
@@ -1,6 +1,10 @@
import { getCurrentInstance } from 'vue'; import { getCurrentInstance } from 'vue';
import { emitter } from 'shared/helpers/mitt'; import { emitter } from 'shared/helpers/mitt';
/**
* Custom hook to track events
* @returns {Function} The track function
*/
export const useTrack = () => { export const useTrack = () => {
const vm = getCurrentInstance(); const vm = getCurrentInstance();
if (!vm) throw new Error('must be called in setup'); if (!vm) throw new Error('must be called in setup');
@@ -8,6 +12,11 @@ export const useTrack = () => {
return vm.proxy.$track; return vm.proxy.$track;
}; };
export function useAlert(message, action) { /**
emitter.emit('newToastMessage', message, action); * Emits a toast message event using a global emitter.
} * @param {string} message - The message to be displayed in the toast.
* @param {Object|null} action - Optional callback function or object to execute.
*/
export const useAlert = (message, action = null) => {
emitter.emit('newToastMessage', { message, action });
};
@@ -0,0 +1,51 @@
import { getCurrentInstance } from 'vue';
import { emitter } from 'shared/helpers/mitt';
import { useTrack, useAlert } from '../index';
vi.mock('vue', () => ({
getCurrentInstance: vi.fn(),
}));
vi.mock('shared/helpers/mitt', () => ({
emitter: {
emit: vi.fn(),
},
}));
describe('useTrack', () => {
it('should return $track from the current instance proxy', () => {
const mockProxy = { $track: vi.fn() };
getCurrentInstance.mockReturnValue({ proxy: mockProxy });
const track = useTrack();
expect(track).toBe(mockProxy.$track);
});
it('should throw an error if called outside of setup', () => {
getCurrentInstance.mockReturnValue(null);
expect(useTrack).toThrowError('must be called in setup');
});
});
describe('useAlert', () => {
it('should emit a newToastMessage event with the provided message and action', () => {
const message = 'Toast message';
const action = {
type: 'link',
to: '/app/accounts/1/conversations/1',
message: 'Navigate',
};
useAlert(message, action);
expect(emitter.emit).toHaveBeenCalledWith('newToastMessage', {
message,
action,
});
});
it('should emit a newToastMessage event with the provided message and no action if action is null', () => {
const message = 'Toast message';
useAlert(message);
expect(emitter.emit).toHaveBeenCalledWith('newToastMessage', {
message,
action: null,
});
});
});
@@ -0,0 +1,52 @@
import { ref } from 'vue';
import { useAdmin } from '../useAdmin';
import { useStoreGetters } from 'dashboard/composables/store';
vi.mock('dashboard/composables/store');
describe('useAdmin', () => {
it('returns true if the current user is an administrator', () => {
useStoreGetters.mockReturnValue({
getCurrentRole: ref('administrator'),
});
const { isAdmin } = useAdmin();
expect(isAdmin.value).toBe(true);
});
it('returns false if the current user is not an administrator', () => {
useStoreGetters.mockReturnValue({
getCurrentRole: ref('user'),
});
const { isAdmin } = useAdmin();
expect(isAdmin.value).toBe(false);
});
it('returns false if the current user role is null', () => {
useStoreGetters.mockReturnValue({
getCurrentRole: ref(null),
});
const { isAdmin } = useAdmin();
expect(isAdmin.value).toBe(false);
});
it('returns false if the current user role is undefined', () => {
useStoreGetters.mockReturnValue({
getCurrentRole: ref(undefined),
});
const { isAdmin } = useAdmin();
expect(isAdmin.value).toBe(false);
});
it('returns false if the current user role is an empty string', () => {
useStoreGetters.mockReturnValue({
getCurrentRole: ref(''),
});
const { isAdmin } = useAdmin();
expect(isAdmin.value).toBe(false);
});
});
@@ -0,0 +1,138 @@
import { ref } from 'vue';
import {
useUISettings,
DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER,
} from 'dashboard/composables/useUISettings';
// Mocking the store composables
const mockDispatch = vi.fn();
const getUISettingsMock = ref({
is_ct_labels_open: true,
conversation_sidebar_items_order: DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
contact_sidebar_items_order: DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER,
editor_message_key: 'enter',
});
vi.mock('dashboard/composables/store', () => ({
useStoreGetters: () => ({
getUISettings: getUISettingsMock,
}),
useStore: () => ({
dispatch: mockDispatch,
}),
}));
describe('useUISettings', () => {
beforeEach(() => {
mockDispatch.mockClear();
});
it('returns uiSettings', () => {
const { uiSettings } = useUISettings();
expect(uiSettings.value).toEqual({
is_ct_labels_open: true,
conversation_sidebar_items_order:
DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
contact_sidebar_items_order: DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER,
editor_message_key: 'enter',
});
});
it('updates UI settings correctly', () => {
const { updateUISettings } = useUISettings();
updateUISettings({ enter_to_send_enabled: true });
expect(mockDispatch).toHaveBeenCalledWith('updateUISettings', {
uiSettings: {
enter_to_send_enabled: true,
is_ct_labels_open: true,
conversation_sidebar_items_order:
DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
contact_sidebar_items_order: DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER,
editor_message_key: 'enter',
},
});
});
it('toggles sidebar UI state correctly', () => {
const { toggleSidebarUIState } = useUISettings();
toggleSidebarUIState('is_ct_labels_open');
expect(mockDispatch).toHaveBeenCalledWith('updateUISettings', {
uiSettings: {
is_ct_labels_open: false,
conversation_sidebar_items_order:
DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
contact_sidebar_items_order: DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER,
editor_message_key: 'enter',
},
});
});
it('returns correct conversation sidebar items order', () => {
const { conversationSidebarItemsOrder } = useUISettings();
expect(conversationSidebarItemsOrder.value).toEqual(
DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER
);
});
it('returns correct contact sidebar items order', () => {
const { contactSidebarItemsOrder } = useUISettings();
expect(contactSidebarItemsOrder.value).toEqual(
DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER
);
});
it('returns correct value for isContactSidebarItemOpen', () => {
const { isContactSidebarItemOpen } = useUISettings();
expect(isContactSidebarItemOpen('is_ct_labels_open')).toBe(true);
expect(isContactSidebarItemOpen('non_existent_key')).toBe(false);
});
it('sets signature flag for inbox correctly', () => {
const { setSignatureFlagForInbox } = useUISettings();
setSignatureFlagForInbox('email', true);
expect(mockDispatch).toHaveBeenCalledWith('updateUISettings', {
uiSettings: {
is_ct_labels_open: true,
conversation_sidebar_items_order:
DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
contact_sidebar_items_order: DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER,
email_signature_enabled: true,
editor_message_key: 'enter',
},
});
});
it('fetches signature flag from UI settings correctly', () => {
const { fetchSignatureFlagFromUISettings } = useUISettings();
expect(fetchSignatureFlagFromUISettings('email')).toBe(undefined);
});
it('returns correct value for isEditorHotKeyEnabled when editor_message_key is configured', () => {
getUISettingsMock.value.enter_to_send_enabled = false;
const { isEditorHotKeyEnabled } = useUISettings();
expect(isEditorHotKeyEnabled('enter')).toBe(true);
expect(isEditorHotKeyEnabled('cmd_enter')).toBe(false);
});
it('returns correct value for isEditorHotKeyEnabled when editor_message_key is not configured', () => {
getUISettingsMock.value.editor_message_key = undefined;
const { isEditorHotKeyEnabled } = useUISettings();
expect(isEditorHotKeyEnabled('enter')).toBe(false);
expect(isEditorHotKeyEnabled('cmd_enter')).toBe(true);
});
it('handles non-existent keys', () => {
const {
isContactSidebarItemOpen,
fetchSignatureFlagFromUISettings,
isEditorHotKeyEnabled,
} = useUISettings();
expect(isContactSidebarItemOpen('non_existent_key')).toBe(false);
expect(fetchSignatureFlagFromUISettings('non_existent_key')).toBe(
undefined
);
expect(isEditorHotKeyEnabled('non_existent_key')).toBe(false);
});
});
@@ -0,0 +1,17 @@
import { computed } from 'vue';
import { useStoreGetters } from 'dashboard/composables/store';
/**
* Composable to determine if the current user is an administrator.
* @returns {Boolean} - True if the current user is an administrator, false otherwise.
*/
export function useAdmin() {
const getters = useStoreGetters();
const currentUserRole = computed(() => getters.getCurrentRole.value);
const isAdmin = computed(() => currentUserRole.value === 'administrator');
return {
isAdmin,
};
}
@@ -0,0 +1,149 @@
import { computed } from 'vue';
import { useStore, useStoreGetters } from 'dashboard/composables/store';
export const DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER = Object.freeze([
{ name: 'conversation_actions' },
{ name: 'macros' },
{ name: 'conversation_info' },
{ name: 'contact_attributes' },
{ name: 'previous_conversation' },
{ name: 'conversation_participants' },
]);
export const DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER = Object.freeze([
{ name: 'contact_attributes' },
{ name: 'contact_labels' },
{ name: 'previous_conversation' },
]);
/**
* Slugifies the channel name.
* Replaces spaces, hyphens, and double colons with underscores.
* @param {string} name - The channel name to slugify.
* @returns {string} The slugified channel name.
*/
const slugifyChannel = name =>
name?.toLowerCase().replace(' ', '_').replace('-', '_').replace('::', '_');
/**
* Computes the order of items in the conversation sidebar, using defaults if not present.
* @param {Object} uiSettings - Reactive UI settings object.
* @returns {Array} Ordered list of sidebar items.
*/
const useConversationSidebarItemsOrder = uiSettings => {
return computed(() => {
const { conversation_sidebar_items_order: itemsOrder } = uiSettings.value;
// If the sidebar order is not set, use the default order.
if (!itemsOrder) {
return DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER;
}
// Create a copy of itemsOrder to avoid mutating the original store object.
const itemsOrderCopy = [...itemsOrder];
// If the sidebar order doesn't have the new elements, then add them to the list.
DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER.forEach(item => {
if (!itemsOrderCopy.find(i => i.name === item.name)) {
itemsOrderCopy.push(item);
}
});
return itemsOrderCopy;
});
};
/**
* Computes the order of items in the contact sidebar,using defaults if not present.
* @param {Object} uiSettings - Reactive UI settings object.
* @returns {Array} Ordered list of sidebar items.
*/
const useContactSidebarItemsOrder = uiSettings => {
return computed(() => {
const { contact_sidebar_items_order: itemsOrder } = uiSettings.value;
return itemsOrder || DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER;
});
};
/**
* Toggles the open state of a sidebar item.
* @param {string} key - The key of the sidebar item to toggle.
* @param {Object} uiSettings - Reactive UI settings object.
* @param {Function} updateUISettings - Function to update UI settings.
*/
const toggleSidebarUIState = (key, uiSettings, updateUISettings) => {
updateUISettings({ [key]: !uiSettings.value[key] });
};
/**
* Sets the signature flag for a specific channel type in the inbox settings.
* @param {string} channelType - The type of the channel.
* @param {boolean} value - The value to set for the signature enabled flag.
* @param {Function} updateUISettings - Function to update UI settings.
*/
const setSignatureFlagForInbox = (channelType, value, updateUISettings) => {
if (!channelType) return;
const slugifiedChannel = slugifyChannel(channelType);
updateUISettings({ [`${slugifiedChannel}_signature_enabled`]: value });
};
/**
* Fetches the signature flag for a specific channel type from UI settings.
* @param {string} channelType - The type of the channel.
* @param {Object} uiSettings - Reactive UI settings object.
* @returns {boolean} The value of the signature enabled flag.
*/
const fetchSignatureFlagFromUISettings = (channelType, uiSettings) => {
if (!channelType) return false;
const slugifiedChannel = slugifyChannel(channelType);
return uiSettings.value[`${slugifiedChannel}_signature_enabled`];
};
/**
* Checks if a specific editor hotkey is enabled.
* @param {string} key - The key to check.
* @param {Object} uiSettings - Reactive UI settings object.
* @returns {boolean} True if the hotkey is enabled, otherwise false.
*/
const isEditorHotKeyEnabled = (key, uiSettings) => {
const {
editor_message_key: editorMessageKey,
enter_to_send_enabled: enterToSendEnabled,
} = uiSettings.value || {};
if (!editorMessageKey) {
return key === (enterToSendEnabled ? 'enter' : 'cmd_enter');
}
return editorMessageKey === key;
};
/**
* Main composable function for managing UI settings.
* @returns {Object} An object containing reactive properties and methods for UI settings management.
*/
export function useUISettings() {
const getters = useStoreGetters();
const store = useStore();
const uiSettings = computed(() => getters.getUISettings.value);
const updateUISettings = (settings = {}) => {
store.dispatch('updateUISettings', {
uiSettings: {
...uiSettings.value,
...settings,
},
});
};
return {
uiSettings,
updateUISettings,
conversationSidebarItemsOrder: useConversationSidebarItemsOrder(uiSettings),
contactSidebarItemsOrder: useContactSidebarItemsOrder(uiSettings),
isContactSidebarItemOpen: key => !!uiSettings.value[key],
toggleSidebarUIState: key =>
toggleSidebarUIState(key, uiSettings, updateUISettings),
setSignatureFlagForInbox: (channelType, value) =>
setSignatureFlagForInbox(channelType, value, updateUISettings),
fetchSignatureFlagFromUISettings: channelType =>
fetchSignatureFlagFromUISettings(channelType, uiSettings),
isEditorHotKeyEnabled: key => isEditorHotKeyEnabled(key, uiSettings),
};
}
@@ -0,0 +1,124 @@
const SCRIPT_TYPE = 'text/javascript';
const DATA_LOADED_ATTR = 'data-loaded';
const SCRIPT_PROPERTIES = [
'defer',
'crossOrigin',
'noModule',
'referrerPolicy',
'id',
];
/**
* Custom error class for script loading failures.
* @extends Error
*/
class ScriptLoaderError extends Error {
/**
* Creates a new ScriptLoaderError.
* @param {string} src - The source URL of the script that failed to load.
* @param {string} message - The error message.
*/
constructor(src, message = 'Failed to load script') {
super(message);
this.name = 'ScriptLoaderError';
this.src = src;
}
/**
* Gets detailed error information.
* @returns {string} A string containing the error details.
*/
getErrorDetails() {
return `Failed to load script from source: ${this.src}`;
}
}
/**
* Creates a new script element with the specified attributes.
* @param {string} src - The source URL of the script.
* @param {Object} options - Options for configuring the script element.
* @param {string} [options.type='text/javascript'] - The type of the script.
* @param {boolean} [options.async=true] - Whether the script should load asynchronously.
* @param {boolean} [options.defer] - Whether the script execution should be deferred.
* @param {string} [options.crossOrigin] - The CORS setting for the script.
* @param {boolean} [options.noModule] - Whether the script should not be treated as a JavaScript module.
* @param {string} [options.referrerPolicy] - The referrer policy for the script.
* @param {string} [options.id] - The id attribute for the script element.
* @param {Object} [options.attrs] - Additional attributes to set on the script element.
* @returns {HTMLScriptElement} The created script element.
*/
const createScriptElement = (src, options) => {
const el = document.createElement('script');
el.type = options.type || SCRIPT_TYPE;
el.async = options.async !== false;
el.src = src;
SCRIPT_PROPERTIES.forEach(property => {
if (property in options) {
el[property] = options[property];
}
});
Object.entries(options.attrs || {}).forEach(([name, value]) =>
el.setAttribute(name, value)
);
return el;
};
/**
* Finds an existing script element with the specified source URL.
* @param {string} src - The source URL to search for.
* @returns {HTMLScriptElement|null} The found script element, or null if not found.
*/
const findExistingScript = src => {
return document.querySelector(`script[src="${src}"]`);
};
/**
* Loads a script asynchronously and returns a promise.
* @param {string} src - The source URL of the script to load.
* @param {Object} options - Options for configuring the script element.
* @param {string} [options.type='text/javascript'] - The type of the script.
* @param {boolean} [options.async=true] - Whether the script should load asynchronously.
* @param {boolean} [options.defer] - Whether the script execution should be deferred.
* @param {string} [options.crossOrigin] - The CORS setting for the script.
* @param {boolean} [options.noModule] - Whether the script should not be treated as a JavaScript module.
* @param {string} [options.referrerPolicy] - The referrer policy for the script.
* @param {string} [options.id] - The id attribute for the script element.
* @param {Object} [options.attrs] - Additional attributes to set on the script element.
* @returns {Promise<HTMLScriptElement|boolean>} A promise that resolves with the loaded script element,
* or false if the script couldn't be loaded.
* @throws {ScriptLoaderError} If the script fails to load.
*/
export async function loadScript(src, options) {
if (typeof window === 'undefined' || !window.document) {
return Promise.resolve(false);
}
return new Promise((resolve, reject) => {
if (typeof src !== 'string' || src.trim() === '') {
reject(new Error('Invalid source URL provided'));
return;
}
let el = findExistingScript(src);
if (!el) {
el = createScriptElement(src, options);
document.head.appendChild(el);
} else if (el.hasAttribute(DATA_LOADED_ATTR)) {
resolve(el);
return;
}
const handleError = () => reject(new ScriptLoaderError(src));
el.addEventListener('error', handleError);
el.addEventListener('abort', handleError);
el.addEventListener('load', () => {
el.setAttribute(DATA_LOADED_ATTR, 'true');
resolve(el);
});
});
}
@@ -1,3 +1,5 @@
import cloneObject from 'dashboard/helpers/clone';
const allElementsString = arr => { const allElementsString = arr => {
return arr.every(elem => typeof elem === 'string'); return arr.every(elem => typeof elem === 'string');
}; };
@@ -27,7 +29,7 @@ const generatePayloadForObject = item => {
}; };
const generatePayload = data => { const generatePayload = data => {
const actions = JSON.parse(JSON.stringify(data)); const actions = cloneObject(data);
let payload = actions.map(item => { let payload = actions.map(item => {
if (Array.isArray(item.action_params)) { if (Array.isArray(item.action_params)) {
item.action_params = formatArray(item.action_params); item.action_params = formatArray(item.action_params);
@@ -5,6 +5,8 @@ import {
} from 'dashboard/routes/dashboard/settings/automation/operators'; } from 'dashboard/routes/dashboard/settings/automation/operators';
import filterQueryGenerator from './filterQueryGenerator'; import filterQueryGenerator from './filterQueryGenerator';
import actionQueryGenerator from './actionQueryGenerator'; import actionQueryGenerator from './actionQueryGenerator';
import cloneObject from 'dashboard/helpers/clone';
const MESSAGE_CONDITION_VALUES = [ const MESSAGE_CONDITION_VALUES = [
{ {
id: 'incoming', id: 'incoming',
@@ -255,7 +257,7 @@ export const getStandardAttributeInputType = (automationTypes, event, key) => {
}; };
export const generateAutomationPayload = payload => { export const generateAutomationPayload = payload => {
const automation = JSON.parse(JSON.stringify(payload)); const automation = cloneObject(payload);
automation.conditions[automation.conditions.length - 1].query_operator = null; automation.conditions[automation.conditions.length - 1].query_operator = null;
automation.conditions = filterQueryGenerator(automation.conditions).payload; automation.conditions = filterQueryGenerator(automation.conditions).payload;
automation.actions = actionQueryGenerator(automation.actions); automation.actions = actionQueryGenerator(automation.actions);
+33
View File
@@ -0,0 +1,33 @@
/**
* Creates a deep clone of the provided object.
*
* This function attempts to use the `structuredClone` method if available.
* If `structuredClone` is not supported, it falls back to using
* `JSON.parse(JSON.stringify())`.
*
* @param {*} obj - The object to be cloned. Can be of any type.
* @returns {*} A deep clone of the input object.
*
* @throws {TypeError} If the object contains values that JSON cannot serialize
* (e.g., functions, undefined) when falling back to
* JSON methods.
*
* @example
* const original = { a: 1, b: { c: 2 } };
* const clone = cloneObject(original);
* console.log(clone); // { a: 1, b: { c: 2 } }
* console.log(original === clone); // false
*/
export function cloneObject(obj) {
if (typeof structuredClone === 'function') {
return structuredClone(obj);
}
// The JSON method doesn't handle all JavaScript types correctly and may cause unexpected behavior.
// At the moment structuredClone has good adoption across browsers https://caniuse.com/mdn-api_structuredclone
// and is the preferred method for cloning objects.
//
// We can consider implementing a more robust fallback method in the future if we find users are running into issues.
// Ref: https://github.com/lukeed/klona
return JSON.parse(JSON.stringify(obj));
}
@@ -1,3 +1,5 @@
import cloneObject from 'dashboard/helpers/clone';
const setArrayValues = item => { const setArrayValues = item => {
return item.values[0]?.id ? item.values.map(val => val.id) : item.values; return item.values[0]?.id ? item.values.map(val => val.id) : item.values;
}; };
@@ -21,7 +23,7 @@ const generateValues = item => {
const generatePayload = data => { const generatePayload = data => {
// Make a copy of data to avoid vue data reactivity issues // Make a copy of data to avoid vue data reactivity issues
const filters = JSON.parse(JSON.stringify(data)); const filters = cloneObject(data);
let payload = filters.map(item => { let payload = filters.map(item => {
// If item key is content, we will split it using comma and return as array // If item key is content, we will split it using comma and return as array
// FIX ME: Make this generic option instead of using the key directly here // FIX ME: Make this generic option instead of using the key directly here
@@ -1,7 +1,7 @@
/* eslint-disable no-console */ /* eslint-disable no-console */
import NotificationSubscriptions from '../api/notificationSubscription'; import NotificationSubscriptions from '../api/notificationSubscription';
import auth from '../api/auth'; import auth from '../api/auth';
import { emitter } from 'shared/helpers/mitt'; import { useAlert } from 'dashboard/composables';
export const verifyServiceWorkerExistence = (callback = () => {}) => { export const verifyServiceWorkerExistence = (callback = () => {}) => {
if (!('serviceWorker' in navigator)) { if (!('serviceWorker' in navigator)) {
@@ -69,19 +69,13 @@ export const registerSubscription = (onSuccess = () => {}) => {
onSuccess(); onSuccess();
}) })
.catch(() => { .catch(() => {
emitter.emit( useAlert('This browser does not support desktop notification');
'newToastMessage',
'This browser does not support desktop notification'
);
}); });
}; };
export const requestPushPermissions = ({ onSuccess }) => { export const requestPushPermissions = ({ onSuccess }) => {
if (!('Notification' in window)) { if (!('Notification' in window)) {
emitter.emit( useAlert('This browser does not support desktop notification');
'newToastMessage',
'This browser does not support desktop notification'
);
} else if (Notification.permission === 'granted') { } else if (Notification.permission === 'granted') {
registerSubscription(onSuccess); registerSubscription(onSuccess);
} else if (Notification.permission !== 'denied') { } else if (Notification.permission !== 'denied') {
@@ -0,0 +1,95 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { loadScript } from '../DOMHelpers';
import { JSDOM } from 'jsdom';
describe('loadScript', () => {
let dom;
let window;
let document;
beforeEach(() => {
dom = new JSDOM('<!DOCTYPE html><html><head></head><body></body></html>', {
url: 'http://localhost',
});
window = dom.window;
document = window.document;
global.document = document;
});
afterEach(() => {
vi.restoreAllMocks();
delete global.document;
});
it('should load a script successfully', async () => {
const src = 'https://example.com/script.js';
const loadPromise = loadScript(src, {});
// Simulate successful script load
setTimeout(() => {
const script = document.querySelector(`script[src="${src}"]`);
if (script) {
script.dispatchEvent(new window.Event('load'));
}
}, 0);
const script = await loadPromise;
expect(script).toBeTruthy();
expect(script.getAttribute('src')).toBe(src);
expect(script.getAttribute('data-loaded')).toBe('true');
});
it('should not load a script if document is not available', async () => {
delete global.document;
const result = await loadScript('https://example.com/script.js', {});
expect(result).toBe(false);
});
it('should use an existing script if already present', async () => {
const src = 'https://example.com/existing-script.js';
const existingScript = document.createElement('script');
existingScript.src = src;
existingScript.setAttribute('data-loaded', 'true');
document.head.appendChild(existingScript);
const script = await loadScript(src, {});
expect(script).toBe(existingScript);
});
it('should set custom attributes on the script element', async () => {
const src = 'https://example.com/custom-script.js';
const options = {
type: 'module',
async: false,
defer: true,
crossOrigin: 'anonymous',
noModule: true,
referrerPolicy: 'origin',
id: 'custom-script',
attrs: { 'data-custom': 'value' },
};
const loadPromise = loadScript(src, options);
// Simulate successful script load
setTimeout(() => {
const script = document.querySelector(`script[src="${src}"]`);
if (script) {
script.dispatchEvent(new window.Event('load'));
}
}, 0);
const script = await loadPromise;
expect(script.type).toBe('module');
expect(script.async).toBe(false);
expect(script.defer).toBe(true);
expect(script.crossOrigin).toBe('anonymous');
expect(script.noModule).toBe(true);
expect(script.referrerPolicy).toBe('origin');
expect(script.id).toBe('custom-script');
expect(script.getAttribute('data-custom')).toBe('value');
});
});
@@ -0,0 +1,61 @@
import { describe, it, expect, vi } from 'vitest';
import { cloneObject } from '../clone'; // Update this path
describe('cloneObject', () => {
it('should clone a simple object', () => {
const original = { a: 1, b: 'string', c: true };
const cloned = cloneObject(original);
expect(cloned).toEqual(original);
expect(cloned).not.toBe(original);
});
it('should clone a nested object', () => {
const original = { a: 1, b: { c: 2, d: { e: 3 } } };
const cloned = cloneObject(original);
expect(cloned).toEqual(original);
expect(cloned.b).not.toBe(original.b);
expect(cloned.b.d).not.toBe(original.b.d);
});
it('should clone an array', () => {
const original = [1, 2, [3, 4]];
const cloned = cloneObject(original);
expect(cloned).toEqual(original);
expect(cloned).not.toBe(original);
expect(cloned[2]).not.toBe(original[2]);
});
it('should clone a Date object', () => {
const original = new Date();
const cloned = cloneObject(original);
expect(cloned).toEqual(original);
expect(cloned).not.toBe(original);
});
it('should use structuredClone when available', () => {
const structuredCloneSpy = vi.fn(x => x);
global.structuredClone = structuredCloneSpy;
const obj = { a: 1 };
cloneObject(obj);
expect(structuredCloneSpy).toHaveBeenCalledWith(obj);
delete global.structuredClone;
});
it('should fall back to JSON methods when structuredClone is not available', () => {
const jsonParseSpy = vi.spyOn(JSON, 'parse');
const jsonStringifySpy = vi.spyOn(JSON, 'stringify');
const original = { a: 1 };
global.structuredClone = undefined;
cloneObject(original);
expect(jsonStringifySpy).toHaveBeenCalledWith(original);
expect(jsonParseSpy).toHaveBeenCalled();
jsonParseSpy.mockRestore();
jsonStringifySpy.mockRestore();
});
});
@@ -379,6 +379,7 @@
}, },
"DETAILS": { "DETAILS": {
"LOADING_FB": "Authenticating you with Facebook...", "LOADING_FB": "Authenticating you with Facebook...",
"ERROR_FB_LOADING": "Error loading Facebook SDK. Please disable any ad-blockers and try again from a different browser.",
"ERROR_FB_AUTH": "Something went wrong, Please refresh page...", "ERROR_FB_AUTH": "Something went wrong, Please refresh page...",
"ERROR_FB_UNAUTHORIZED": "You're not authorized to perform this action. ", "ERROR_FB_UNAUTHORIZED": "You're not authorized to perform this action. ",
"ERROR_FB_UNAUTHORIZED_HELP": "Please ensure you have access to the Facebook page with full control. You can read more about Facebook roles <a href=\" https://www.facebook.com/help/187316341316631\">here</a>.", "ERROR_FB_UNAUTHORIZED_HELP": "Please ensure you have access to the Facebook page with full control. You can read more about Facebook roles <a href=\" https://www.facebook.com/help/187316341316631\">here</a>.",
+2 -3
View File
@@ -1,10 +1,9 @@
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { useAlert } from 'dashboard/composables';
import { OPEN_AI_EVENTS } from '../helper/AnalyticsHelper/events'; import { OPEN_AI_EVENTS } from '../helper/AnalyticsHelper/events';
import OpenAPI from '../api/integrations/openapi'; import OpenAPI from '../api/integrations/openapi';
import alertMixin from 'shared/mixins/alertMixin';
export default { export default {
mixins: [alertMixin],
mounted() { mounted() {
this.fetchIntegrationsIfRequired(); this.fetchIntegrationsIfRequired();
}, },
@@ -101,7 +100,7 @@ export default {
} = result; } = result;
return generatedMessage; return generatedMessage;
} catch (error) { } catch (error) {
this.showAlert(this.$t('INTEGRATION_SETTINGS.OPEN_AI.GENERATE_ERROR')); useAlert(this.$t('INTEGRATION_SETTINGS.OPEN_AI.GENERATE_ERROR'));
return ''; return '';
} }
}, },
@@ -17,6 +17,7 @@ import {
generateCustomAttributes, generateCustomAttributes,
} from 'dashboard/helper/automationHelper'; } from 'dashboard/helper/automationHelper';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { useAlert } from 'dashboard/composables';
export default { export default {
computed: { computed: {
@@ -138,14 +139,14 @@ export default {
}, },
removeFilter(index) { removeFilter(index) {
if (this.automation.conditions.length <= 1) { if (this.automation.conditions.length <= 1) {
this.showAlert(this.$t('AUTOMATION.CONDITION.DELETE_MESSAGE')); useAlert(this.$t('AUTOMATION.CONDITION.DELETE_MESSAGE'));
} else { } else {
this.automation.conditions.splice(index, 1); this.automation.conditions.splice(index, 1);
} }
}, },
removeAction(index) { removeAction(index) {
if (this.automation.actions.length <= 1) { if (this.automation.actions.length <= 1) {
this.showAlert(this.$t('AUTOMATION.ACTION.DELETE_MESSAGE')); useAlert(this.$t('AUTOMATION.ACTION.DELETE_MESSAGE'));
} else { } else {
this.automation.actions.splice(index, 1); this.automation.actions.splice(index, 1);
} }
@@ -1,15 +0,0 @@
export default {
props: {
trianglePosition: {
type: String,
default: '0',
},
},
computed: {
cssVars() {
return {
'--triangle-position': this.trianglePosition + 'rem',
};
},
},
};
@@ -1,3 +1,4 @@
import { useAlert } from 'dashboard/composables';
import { import {
MAXIMUM_FILE_UPLOAD_SIZE, MAXIMUM_FILE_UPLOAD_SIZE,
MAXIMUM_FILE_UPLOAD_SIZE_TWILIO_SMS_CHANNEL, MAXIMUM_FILE_UPLOAD_SIZE_TWILIO_SMS_CHANNEL,
@@ -38,13 +39,13 @@ export default {
upload.create((error, blob) => { upload.create((error, blob) => {
if (error) { if (error) {
this.showAlert(error); useAlert(error);
} else { } else {
this.attachFile({ file, blob }); this.attachFile({ file, blob });
} }
}); });
} else { } else {
this.showAlert( useAlert(
this.$t('CONVERSATION.FILE_SIZE_LIMIT', { this.$t('CONVERSATION.FILE_SIZE_LIMIT', {
MAXIMUM_SUPPORTED_FILE_UPLOAD_SIZE, MAXIMUM_SUPPORTED_FILE_UPLOAD_SIZE,
}) })
@@ -61,7 +62,7 @@ export default {
if (checkFileSizeLimit(file, MAXIMUM_SUPPORTED_FILE_UPLOAD_SIZE)) { if (checkFileSizeLimit(file, MAXIMUM_SUPPORTED_FILE_UPLOAD_SIZE)) {
this.attachFile({ file }); this.attachFile({ file });
} else { } else {
this.showAlert( useAlert(
this.$t('CONVERSATION.FILE_SIZE_LIMIT', { this.$t('CONVERSATION.FILE_SIZE_LIMIT', {
MAXIMUM_SUPPORTED_FILE_UPLOAD_SIZE, MAXIMUM_SUPPORTED_FILE_UPLOAD_SIZE,
}) })
@@ -1,12 +0,0 @@
import { mapGetters } from 'vuex';
export default {
computed: {
...mapGetters({
currentUserRole: 'getCurrentRole',
}),
isAdmin() {
return this.currentUserRole === 'administrator';
},
},
};
@@ -1,19 +0,0 @@
import { mount } from '@vue/test-utils';
import bulkActionsMixin from '../bulkActionsMixin';
describe('bulkActionsMixin', () => {
it('returns the prop and computed values for triangle position:', async () => {
const Component = {
render() {},
title: 'MyComponent',
mixins: [bulkActionsMixin],
};
const wrapper = mount(Component);
await wrapper.setProps({
trianglePosition: '10',
});
expect(wrapper.props().trianglePosition).toEqual('10');
expect(wrapper.vm.cssVars).toEqual({
'--triangle-position': '10rem',
});
});
});
@@ -1,5 +1,6 @@
import fileUploadMixin from 'dashboard/mixins/fileUploadMixin';
import Vue from 'vue'; import Vue from 'vue';
import { useAlert } from 'dashboard/composables';
import fileUploadMixin from 'dashboard/mixins/fileUploadMixin';
vi.mock('shared/helpers/FileHelper', () => ({ vi.mock('shared/helpers/FileHelper', () => ({
checkFileSizeLimit: vi.fn(), checkFileSizeLimit: vi.fn(),
@@ -11,6 +12,10 @@ vi.mock('activestorage', () => ({
})), })),
})); }));
vi.mock('dashboard/composables', () => ({
useAlert: vi.fn(),
}));
describe('FileUploadMixin', () => { describe('FileUploadMixin', () => {
let vm; let vm;
@@ -53,9 +58,8 @@ describe('FileUploadMixin', () => {
it('shows an alert if the file size exceeds the maximum limit', () => { it('shows an alert if the file size exceeds the maximum limit', () => {
const fakeFile = { size: 999999999 }; const fakeFile = { size: 999999999 };
vm.showAlert = vi.fn();
vm.onDirectFileUpload(fakeFile); vm.onDirectFileUpload(fakeFile);
expect(vm.showAlert).toHaveBeenCalledWith(expect.any(String)); expect(useAlert).toHaveBeenCalledWith(expect.any(String));
}); });
}); });
@@ -67,9 +71,8 @@ describe('FileUploadMixin', () => {
it('shows an alert if the file size exceeds the maximum limit', () => { it('shows an alert if the file size exceeds the maximum limit', () => {
const fakeFile = { size: 999999999 }; const fakeFile = { size: 999999999 };
vm.showAlert = vi.fn();
vm.onIndirectFileUpload(fakeFile); vm.onIndirectFileUpload(fakeFile);
expect(vm.showAlert).toHaveBeenCalledWith(expect.any(String)); expect(useAlert).toHaveBeenCalledWith(expect.any(String));
}); });
}); });
}); });
@@ -1,28 +0,0 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import isAdminMixin from '../isAdmin';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('isAdminMixin', () => {
let getters;
let store;
beforeEach(() => {
getters = {
getCurrentRole: () => 'administrator',
};
store = new Vuex.Store({ getters });
});
it('set accountId properly', () => {
const Component = {
render() {},
title: 'TestComponent',
mixins: [isAdminMixin],
};
const wrapper = shallowMount(Component, { store, localVue });
expect(wrapper.vm.isAdmin).toBe(true);
});
});
@@ -1,128 +0,0 @@
import TimeMixin from '../time';
describe('#messageStamp', () => {
it('returns correct value', () => {
expect(TimeMixin.methods.messageStamp(1612971343)).toEqual('3:35 PM');
expect(TimeMixin.methods.messageStamp(1612971343, 'LLL d, h:mm a')).toEqual(
'Feb 10, 3:35 PM'
);
});
});
describe('#messageTimestamp', () => {
beforeEach(() => {
vi.useFakeTimers('modern');
const mockDate = new Date(2023, 4, 5);
vi.setSystemTime(mockDate);
});
afterEach(() => {
vi.useRealTimers();
});
it('should return the message date in the specified format if the message was sent in the current year', () => {
expect(TimeMixin.methods.messageTimestamp(1680777464)).toEqual(
'Apr 6, 2023'
);
});
it('should return the message date and time in a different format if the message was sent in a different year', () => {
expect(TimeMixin.methods.messageTimestamp(1612971343)).toEqual(
'Feb 10 2021, 3:35 PM'
);
});
});
describe('#dynamicTime', () => {
it('returns correct value', () => {
Date.now = vi.fn(() => new Date(Date.UTC(2023, 1, 14)).valueOf());
expect(TimeMixin.methods.dynamicTime(1612971343)).toEqual(
'about 2 years ago'
);
});
});
describe('#dateFormat', () => {
it('returns correct value', () => {
expect(TimeMixin.methods.dateFormat(1612971343)).toEqual('Feb 10, 2021');
expect(TimeMixin.methods.dateFormat(1612971343, 'LLL d, yyyy')).toEqual(
'Feb 10, 2021'
);
});
});
describe('#shortTimestamp', () => {
// Test cases when withAgo is false or not provided
it('returns correct value without ago', () => {
expect(TimeMixin.methods.shortTimestamp('less than a minute ago')).toEqual(
'now'
);
expect(TimeMixin.methods.shortTimestamp('1 minute ago')).toEqual('1m');
expect(TimeMixin.methods.shortTimestamp('12 minutes ago')).toEqual('12m');
expect(TimeMixin.methods.shortTimestamp('a minute ago')).toEqual('1m');
expect(TimeMixin.methods.shortTimestamp('an hour ago')).toEqual('1h');
expect(TimeMixin.methods.shortTimestamp('1 hour ago')).toEqual('1h');
expect(TimeMixin.methods.shortTimestamp('2 hours ago')).toEqual('2h');
expect(TimeMixin.methods.shortTimestamp('1 day ago')).toEqual('1d');
expect(TimeMixin.methods.shortTimestamp('a day ago')).toEqual('1d');
expect(TimeMixin.methods.shortTimestamp('3 days ago')).toEqual('3d');
expect(TimeMixin.methods.shortTimestamp('a month ago')).toEqual('1mo');
expect(TimeMixin.methods.shortTimestamp('1 month ago')).toEqual('1mo');
expect(TimeMixin.methods.shortTimestamp('2 months ago')).toEqual('2mo');
expect(TimeMixin.methods.shortTimestamp('a year ago')).toEqual('1y');
expect(TimeMixin.methods.shortTimestamp('1 year ago')).toEqual('1y');
expect(TimeMixin.methods.shortTimestamp('4 years ago')).toEqual('4y');
});
// Test cases when withAgo is true
it('returns correct value with ago', () => {
expect(
TimeMixin.methods.shortTimestamp('less than a minute ago', true)
).toEqual('now');
expect(TimeMixin.methods.shortTimestamp('1 minute ago', true)).toEqual(
'1m ago'
);
expect(TimeMixin.methods.shortTimestamp('12 minutes ago', true)).toEqual(
'12m ago'
);
expect(TimeMixin.methods.shortTimestamp('a minute ago', true)).toEqual(
'1m ago'
);
expect(TimeMixin.methods.shortTimestamp('an hour ago', true)).toEqual(
'1h ago'
);
expect(TimeMixin.methods.shortTimestamp('1 hour ago', true)).toEqual(
'1h ago'
);
expect(TimeMixin.methods.shortTimestamp('2 hours ago', true)).toEqual(
'2h ago'
);
expect(TimeMixin.methods.shortTimestamp('1 day ago', true)).toEqual(
'1d ago'
);
expect(TimeMixin.methods.shortTimestamp('a day ago', true)).toEqual(
'1d ago'
);
expect(TimeMixin.methods.shortTimestamp('3 days ago', true)).toEqual(
'3d ago'
);
expect(TimeMixin.methods.shortTimestamp('a month ago', true)).toEqual(
'1mo ago'
);
expect(TimeMixin.methods.shortTimestamp('1 month ago', true)).toEqual(
'1mo ago'
);
expect(TimeMixin.methods.shortTimestamp('2 months ago', true)).toEqual(
'2mo ago'
);
expect(TimeMixin.methods.shortTimestamp('a year ago', true)).toEqual(
'1y ago'
);
expect(TimeMixin.methods.shortTimestamp('1 year ago', true)).toEqual(
'1y ago'
);
expect(TimeMixin.methods.shortTimestamp('4 years ago', true)).toEqual(
'4y ago'
);
});
});
@@ -1,169 +0,0 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import uiSettingsMixin, {
DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER,
isEditorHotKeyEnabled,
} from '../uiSettings';
import Vuex from 'vuex';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('uiSettingsMixin', () => {
let getters;
let actions;
let store;
beforeEach(() => {
actions = { updateUISettings: vi.fn(), toggleSidebarUIState: vi.fn() };
getters = {
getUISettings: () => ({
enter_to_send_enabled: false,
is_ct_labels_open: true,
conversation_sidebar_items_order:
DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
contact_sidebar_items_order: DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER,
}),
};
store = new Vuex.Store({ actions, getters });
});
it('returns uiSettings', () => {
const Component = {
render() {},
title: 'TestComponent',
mixins: [uiSettingsMixin],
};
const wrapper = shallowMount(Component, { store, localVue });
expect(wrapper.vm.uiSettings).toEqual({
enter_to_send_enabled: false,
is_ct_labels_open: true,
conversation_sidebar_items_order:
DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
contact_sidebar_items_order: DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER,
});
});
describe('#updateUISettings', () => {
it('dispatches store actions correctly', () => {
const Component = {
render() {},
title: 'TestComponent',
mixins: [uiSettingsMixin],
};
const wrapper = shallowMount(Component, { store, localVue });
wrapper.vm.updateUISettings({ enter_to_send_enabled: true });
expect(actions.updateUISettings).toHaveBeenCalledWith(
expect.anything(),
{
uiSettings: {
enter_to_send_enabled: true,
is_ct_labels_open: true,
conversation_sidebar_items_order:
DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
contact_sidebar_items_order: DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER,
},
},
undefined
);
});
});
describe('#toggleSidebarUIState', () => {
it('dispatches store actions correctly', () => {
const Component = {
render() {},
title: 'TestComponent',
mixins: [uiSettingsMixin],
};
const wrapper = shallowMount(Component, { store, localVue });
wrapper.vm.toggleSidebarUIState('is_ct_labels_open');
expect(actions.updateUISettings).toHaveBeenCalledWith(
expect.anything(),
{
uiSettings: {
enter_to_send_enabled: false,
is_ct_labels_open: false,
conversation_sidebar_items_order:
DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
contact_sidebar_items_order: DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER,
},
},
undefined
);
});
});
describe('#isContactSidebarItemOpen', () => {
it('returns correct values', () => {
const Component = {
render() {},
title: 'TestComponent',
mixins: [uiSettingsMixin],
};
const wrapper = shallowMount(Component, { store, localVue });
expect(wrapper.vm.isContactSidebarItemOpen('is_ct_labels_open')).toEqual(
true
);
expect(
wrapper.vm.isContactSidebarItemOpen('is_ct_prev_conv_open')
).toEqual(false);
});
});
describe('#conversationSidebarItemsOrder', () => {
it('returns correct values', () => {
const Component = {
render() {},
title: 'TestComponent',
mixins: [uiSettingsMixin],
};
const wrapper = shallowMount(Component, { store, localVue });
expect(wrapper.vm.conversationSidebarItemsOrder).toEqual([
{ name: 'conversation_actions' },
{ name: 'macros' },
{ name: 'conversation_info' },
{ name: 'contact_attributes' },
{ name: 'previous_conversation' },
{ name: 'conversation_participants' },
]);
});
});
describe('#contactSidebarItemsOrder', () => {
it('returns correct values', () => {
const Component = {
render() {},
title: 'TestComponent',
mixins: [uiSettingsMixin],
};
const wrapper = shallowMount(Component, { store, localVue });
expect(wrapper.vm.contactSidebarItemsOrder).toEqual([
{ name: 'contact_attributes' },
{ name: 'contact_labels' },
{ name: 'previous_conversation' },
]);
});
});
});
describe('isEditorHotKeyEnabled', () => {
it('returns true if hot key is not configured and enter to send flag is true', () => {
expect(
isEditorHotKeyEnabled({ enter_to_send_enabled: true }, 'enter')
).toEqual(true);
expect(
isEditorHotKeyEnabled({ enter_to_send_enabled: true }, 'cmd_enter')
).toEqual(false);
expect(isEditorHotKeyEnabled({}, 'cmd_enter')).toEqual(true);
expect(isEditorHotKeyEnabled({}, 'enter')).toEqual(false);
});
it('returns correct value if hot key is configured', () => {
expect(
isEditorHotKeyEnabled({ editor_message_key: 'enter' }, 'enter')
).toEqual(true);
expect(
isEditorHotKeyEnabled({ editor_message_key: 'cmd_enter' }, 'enter')
).toEqual(false);
});
});
-64
View File
@@ -1,64 +0,0 @@
import {
format,
isSameYear,
fromUnixTime,
formatDistanceToNow,
} from 'date-fns';
export default {
methods: {
messageStamp(time, dateFormat = 'h:mm a') {
const unixTime = fromUnixTime(time);
return format(unixTime, dateFormat);
},
messageTimestamp(time, dateFormat = 'MMM d, yyyy') {
const messageTime = fromUnixTime(time);
const now = new Date();
const messageDate = format(messageTime, dateFormat);
if (!isSameYear(messageTime, now)) {
return format(messageTime, 'LLL d y, h:mm a');
}
return messageDate;
},
dynamicTime(time) {
const unixTime = fromUnixTime(time);
return formatDistanceToNow(unixTime, { addSuffix: true });
},
dateFormat(time, dateFormat = 'MMM d, yyyy') {
const unixTime = fromUnixTime(time);
return format(unixTime, dateFormat);
},
shortTimestamp(time, withAgo = false) {
// This function takes a time string and converts it to a short time string
// with the following format: 1m, 1h, 1d, 1mo, 1y
// The function also takes an optional boolean parameter withAgo
// which will add the word "ago" to the end of the time string
const suffix = withAgo ? ' ago' : '';
const timeMappings = {
'less than a minute ago': 'now',
'a minute ago': `1m${suffix}`,
'an hour ago': `1h${suffix}`,
'a day ago': `1d${suffix}`,
'a month ago': `1mo${suffix}`,
'a year ago': `1y${suffix}`,
};
// Check if the time string is one of the specific cases
if (timeMappings[time]) {
return timeMappings[time];
}
const convertToShortTime = time
.replace(/about|over|almost|/g, '')
.replace(' minute ago', `m${suffix}`)
.replace(' minutes ago', `m${suffix}`)
.replace(' hour ago', `h${suffix}`)
.replace(' hours ago', `h${suffix}`)
.replace(' day ago', `d${suffix}`)
.replace(' days ago', `d${suffix}`)
.replace(' month ago', `mo${suffix}`)
.replace(' months ago', `mo${suffix}`)
.replace(' year ago', `y${suffix}`)
.replace(' years ago', `y${suffix}`);
return convertToShortTime;
},
},
};
@@ -1,87 +0,0 @@
import { mapGetters } from 'vuex';
export const DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER = [
{ name: 'conversation_actions' },
{ name: 'macros' },
{ name: 'conversation_info' },
{ name: 'contact_attributes' },
{ name: 'previous_conversation' },
{ name: 'conversation_participants' },
];
export const DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER = [
{ name: 'contact_attributes' },
{ name: 'contact_labels' },
{ name: 'previous_conversation' },
];
const slugifyChannel = name =>
name?.toLowerCase().replace(' ', '_').replace('-', '_').replace('::', '_');
export const isEditorHotKeyEnabled = (uiSettings, key) => {
const {
editor_message_key: editorMessageKey,
enter_to_send_enabled: enterToSendEnabled,
} = uiSettings || {};
if (!editorMessageKey) {
if (enterToSendEnabled) {
return key === 'enter';
}
return key === 'cmd_enter';
}
return editorMessageKey === key;
};
export default {
computed: {
...mapGetters({ uiSettings: 'getUISettings' }),
conversationSidebarItemsOrder() {
const { conversation_sidebar_items_order: itemsOrder } = this.uiSettings;
// If the sidebar order is not set, use the default order.
if (!itemsOrder) {
return DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER;
}
// If the sidebar order doesn't have the new elements, then add them to the list.
DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER.forEach(item => {
if (!itemsOrder.find(i => i.name === item.name)) {
itemsOrder.push(item);
}
});
return itemsOrder;
},
contactSidebarItemsOrder() {
const { contact_sidebar_items_order: itemsOrder } = this.uiSettings;
return itemsOrder || DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER;
},
},
methods: {
updateUISettings(uiSettings = {}) {
this.$store.dispatch('updateUISettings', {
uiSettings: {
...this.uiSettings,
...uiSettings,
},
});
},
isContactSidebarItemOpen(key) {
const { [key]: isOpen } = this.uiSettings;
return !!isOpen;
},
toggleSidebarUIState(key) {
this.updateUISettings({ [key]: !this.isContactSidebarItemOpen(key) });
},
setSignatureFlagForInbox(channelType, value) {
if (!channelType) return;
channelType = slugifyChannel(channelType);
this.updateUISettings({
[`${channelType}_signature_enabled`]: value,
});
},
fetchSignatureFlagFromUiSettings(channelType) {
if (!channelType) return false;
channelType = slugifyChannel(channelType);
return this.uiSettings[`${channelType}_signature_enabled`];
},
},
};
@@ -19,7 +19,7 @@
</template> </template>
<script> <script>
import alertMixin from 'shared/mixins/alertMixin'; import { useAlert } from 'dashboard/composables';
import MergeContact from 'dashboard/modules/contact/components/MergeContact.vue'; import MergeContact from 'dashboard/modules/contact/components/MergeContact.vue';
import ContactAPI from 'dashboard/api/contacts'; import ContactAPI from 'dashboard/api/contacts';
@@ -29,7 +29,6 @@ import { CONTACTS_EVENTS } from '../../helper/AnalyticsHelper/events';
export default { export default {
components: { MergeContact }, components: { MergeContact },
mixins: [alertMixin],
props: { props: {
primaryContact: { primaryContact: {
type: Object, type: Object,
@@ -68,7 +67,7 @@ export default {
contact => contact.id !== this.primaryContact.id contact => contact.id !== this.primaryContact.id
); );
} catch (error) { } catch (error) {
this.showAlert(this.$t('MERGE_CONTACTS.SEARCH.ERROR_MESSAGE')); useAlert(this.$t('MERGE_CONTACTS.SEARCH.ERROR_MESSAGE'));
} finally { } finally {
this.isSearching = false; this.isSearching = false;
} }
@@ -80,10 +79,10 @@ export default {
childId: this.primaryContact.id, childId: this.primaryContact.id,
parentId: parentContactId, parentId: parentContactId,
}); });
this.showAlert(this.$t('MERGE_CONTACTS.FORM.SUCCESS_MESSAGE')); useAlert(this.$t('MERGE_CONTACTS.FORM.SUCCESS_MESSAGE'));
this.onClose(); this.onClose();
} catch (error) { } catch (error) {
this.showAlert(this.$t('MERGE_CONTACTS.FORM.ERROR_MESSAGE')); useAlert(this.$t('MERGE_CONTACTS.FORM.ERROR_MESSAGE'));
} }
}, },
}, },
@@ -21,7 +21,7 @@
:label="$t('CUSTOM_ATTRIBUTES.FORM.VALUE.LABEL')" :label="$t('CUSTOM_ATTRIBUTES.FORM.VALUE.LABEL')"
:placeholder="$t('CUSTOM_ATTRIBUTES.FORM.VALUE.PLACEHOLDER')" :placeholder="$t('CUSTOM_ATTRIBUTES.FORM.VALUE.PLACEHOLDER')"
/> />
<div class="flex justify-end items-center py-2 px-0 gap-2"> <div class="flex items-center justify-end gap-2 px-0 py-2">
<woot-button <woot-button
:is-disabled="$v.attributeName.$invalid || isCreating" :is-disabled="$v.attributeName.$invalid || isCreating"
:is-loading="isCreating" :is-loading="isCreating"
@@ -38,14 +38,12 @@
<script> <script>
import Modal from 'dashboard/components/Modal.vue'; import Modal from 'dashboard/components/Modal.vue';
import alertMixin from 'shared/mixins/alertMixin';
import { required, minLength } from 'vuelidate/lib/validators'; import { required, minLength } from 'vuelidate/lib/validators';
export default { export default {
components: { components: {
Modal, Modal,
}, },
mixins: [alertMixin],
props: { props: {
show: { show: {
type: Boolean, type: Boolean,
@@ -57,7 +57,7 @@
</span> </span>
</div> </div>
</div> </div>
<div class="multiselect-wrap--medium flex"> <div class="flex multiselect-wrap--medium">
<div <div
class="w-8 relative text-base text-slate-100 dark:text-slate-600 after:content-[''] after:h-12 after:w-0 after:left-4 after:absolute after:border-l after:border-solid after:border-slate-100 after:dark:border-slate-600 before:content-[''] before:h-0 before:w-4 before:left-4 before:top-12 before:absolute before:border-b before:border-solid before:border-slate-100 before:dark:border-slate-600" class="w-8 relative text-base text-slate-100 dark:text-slate-600 after:content-[''] after:h-12 after:w-0 after:left-4 after:absolute after:border-l after:border-solid after:border-slate-100 after:dark:border-slate-600 before:content-[''] before:h-0 before:w-4 before:left-4 before:top-12 before:absolute before:border-b before:border-solid before:border-slate-100 before:dark:border-slate-600"
> >
@@ -102,7 +102,7 @@
:primary-contact-name="primaryContact.name" :primary-contact-name="primaryContact.name"
:parent-contact-name="parentContactName" :parent-contact-name="parentContactName"
/> />
<div class="mt-6 flex gap-2 justify-end"> <div class="flex justify-end gap-2 mt-6">
<woot-button variant="clear" @click.prevent="onCancel"> <woot-button variant="clear" @click.prevent="onCancel">
{{ $t('MERGE_CONTACTS.FORM.CANCEL') }} {{ $t('MERGE_CONTACTS.FORM.CANCEL') }}
</woot-button> </woot-button>
@@ -114,7 +114,6 @@
</template> </template>
<script> <script>
import alertMixin from 'shared/mixins/alertMixin';
import { required } from 'vuelidate/lib/validators'; import { required } from 'vuelidate/lib/validators';
import MergeContactSummary from 'dashboard/modules/contact/components/MergeContactSummary.vue'; import MergeContactSummary from 'dashboard/modules/contact/components/MergeContactSummary.vue';
@@ -122,7 +121,6 @@ import ContactDropdownItem from './ContactDropdownItem.vue';
export default { export default {
components: { MergeContactSummary, ContactDropdownItem }, components: { MergeContactSummary, ContactDropdownItem },
mixins: [alertMixin],
props: { props: {
primaryContact: { primaryContact: {
type: Object, type: Object,
@@ -104,7 +104,7 @@
</div> </div>
</template> </template>
<script> <script>
import alertMixin from 'shared/mixins/alertMixin'; import { useAlert } from 'dashboard/composables';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin'; import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
import AddCannedModal from 'dashboard/routes/dashboard/settings/canned/AddCanned.vue'; import AddCannedModal from 'dashboard/routes/dashboard/settings/canned/AddCanned.vue';
@@ -123,7 +123,7 @@ export default {
TranslateModal, TranslateModal,
MenuItem, MenuItem,
}, },
mixins: [alertMixin, messageFormatterMixin], mixins: [messageFormatterMixin],
props: { props: {
message: { message: {
type: Object, type: Object,
@@ -183,12 +183,12 @@ export default {
await copyTextToClipboard( await copyTextToClipboard(
`${fullConversationURL}?messageId=${this.messageId}` `${fullConversationURL}?messageId=${this.messageId}`
); );
this.showAlert(this.$t('CONVERSATION.CONTEXT_MENU.LINK_COPIED')); useAlert(this.$t('CONVERSATION.CONTEXT_MENU.LINK_COPIED'));
this.handleClose(); this.handleClose();
}, },
async handleCopy() { async handleCopy() {
await copyTextToClipboard(this.plainTextContent); await copyTextToClipboard(this.plainTextContent);
this.showAlert(this.$t('CONTACT_PANEL.COPY_SUCCESSFUL')); useAlert(this.$t('CONTACT_PANEL.COPY_SUCCESSFUL'));
this.handleClose(); this.handleClose();
}, },
showCannedResponseModal() { showCannedResponseModal() {
@@ -233,10 +233,10 @@ export default {
conversationId: this.conversationId, conversationId: this.conversationId,
messageId: this.messageId, messageId: this.messageId,
}); });
this.showAlert(this.$t('CONVERSATION.SUCCESS_DELETE_MESSAGE')); useAlert(this.$t('CONVERSATION.SUCCESS_DELETE_MESSAGE'));
this.handleClose(); this.handleClose();
} catch (error) { } catch (error) {
this.showAlert(this.$t('CONVERSATION.FAIL_DELETE_MESSSAGE')); useAlert(this.$t('CONVERSATION.FAIL_DELETE_MESSSAGE'));
} }
}, },
closeDeleteModal() { closeDeleteModal() {
@@ -1,8 +1,8 @@
<template> <template>
<div <div
class="flex flex-col mb-2 p-4 border border-solid border-slate-75 dark:border-slate-700 overflow-hidden rounded-md flex-grow shadow-sm bg-white dark:bg-slate-900 text-slate-700 dark:text-slate-100 note-wrap" class="flex flex-col flex-grow p-4 mb-2 overflow-hidden bg-white border border-solid rounded-md shadow-sm border-slate-75 dark:border-slate-700 dark:bg-slate-900 text-slate-700 dark:text-slate-100 note-wrap"
> >
<div class="flex justify-between items-end text-xs gap-1"> <div class="flex items-end justify-between gap-1 text-xs">
<div class="flex items-center"> <div class="flex items-center">
<thumbnail <thumbnail
:title="noteAuthorName" :title="noteAuthorName"
@@ -22,7 +22,7 @@
</span> </span>
</div> </div>
</div> </div>
<div class="actions flex invisible"> <div class="flex invisible actions">
<woot-button <woot-button
v-tooltip="$t('NOTES.CONTENT_HEADER.DELETE')" v-tooltip="$t('NOTES.CONTENT_HEADER.DELETE')"
variant="smooth" variant="smooth"
@@ -45,22 +45,22 @@
</div> </div>
<p <p
v-dompurify-html="formatMessage(note || '')" v-dompurify-html="formatMessage(note || '')"
class="note__content mt-4" class="mt-4 note__content"
/> />
</div> </div>
</template> </template>
<script> <script>
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue'; import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
import timeMixin from 'dashboard/mixins/time';
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin'; import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
import { dynamicTime } from 'shared/helpers/timeHelper';
export default { export default {
components: { components: {
Thumbnail, Thumbnail,
}, },
mixins: [timeMixin, messageFormatterMixin], mixins: [messageFormatterMixin],
props: { props: {
id: { id: {
@@ -87,7 +87,7 @@ export default {
}, },
computed: { computed: {
readableTime() { readableTime() {
return this.dynamicTime(this.createdAt); return dynamicTime(this.createdAt);
}, },
noteAuthor() { noteAuthor() {
return this.user || {}; return this.user || {};
@@ -27,7 +27,7 @@
</h5> </h5>
<h5 <h5
v-if="email" v-if="email"
class="text-sm email text-slate-700 dark:text-slate-200 overflow-hidden whitespace-nowrap text-ellipsis" class="overflow-hidden text-sm email text-slate-700 dark:text-slate-200 whitespace-nowrap text-ellipsis"
> >
<span class="pre-text">{{ $t('SEARCH.EMAIL') }}:</span> <span class="pre-text">{{ $t('SEARCH.EMAIL') }}:</span>
{{ email }} {{ email }}
@@ -40,14 +40,13 @@
<script> <script>
import { frontendURL } from 'dashboard/helper/URLHelper.js'; import { frontendURL } from 'dashboard/helper/URLHelper.js';
import timeMixin from 'dashboard/mixins/time'; import { dynamicTime } from 'shared/helpers/timeHelper';
import InboxName from 'dashboard/components/widgets/InboxName.vue'; import InboxName from 'dashboard/components/widgets/InboxName.vue';
export default { export default {
components: { components: {
InboxName, InboxName,
}, },
mixins: [timeMixin],
props: { props: {
id: { id: {
type: Number, type: Number,
@@ -90,7 +89,7 @@ export default {
); );
}, },
createdAtTime() { createdAtTime() {
return this.dynamicTime(this.createdAt); return dynamicTime(this.createdAt);
}, },
}, },
}; };
@@ -1,6 +1,6 @@
<template> <template>
<div <div
class="app-wrapper h-full flex-grow-0 min-h-0 w-full max-w-full ml-auto mr-auto flex flex-wrap dark:text-slate-300" class="flex flex-wrap flex-grow-0 w-full h-full max-w-full min-h-0 ml-auto mr-auto app-wrapper dark:text-slate-300"
> >
<sidebar <sidebar
:route="currentRoute" :route="currentRoute"
@@ -11,7 +11,7 @@
@close-key-shortcut-modal="closeKeyShortcutModal" @close-key-shortcut-modal="closeKeyShortcutModal"
@show-add-label-popup="showAddLabelPopup" @show-add-label-popup="showAddLabelPopup"
/> />
<section class="flex h-full min-h-0 overflow-hidden flex-1 px-0"> <section class="flex flex-1 h-full min-h-0 px-0 overflow-hidden">
<router-view /> <router-view />
<command-bar /> <command-bar />
<account-selector <account-selector
@@ -47,7 +47,7 @@ import AddAccountModal from 'dashboard/components/layout/sidebarComponents/AddAc
import AccountSelector from 'dashboard/components/layout/sidebarComponents/AccountSelector.vue'; import AccountSelector from 'dashboard/components/layout/sidebarComponents/AccountSelector.vue';
import AddLabelModal from 'dashboard/routes/dashboard/settings/labels/AddLabel.vue'; import AddLabelModal from 'dashboard/routes/dashboard/settings/labels/AddLabel.vue';
import NotificationPanel from 'dashboard/routes/dashboard/notifications/components/NotificationPanel.vue'; import NotificationPanel from 'dashboard/routes/dashboard/notifications/components/NotificationPanel.vue';
import uiSettingsMixin from 'dashboard/mixins/uiSettings'; import { useUISettings } from 'dashboard/composables/useUISettings';
import wootConstants from 'dashboard/constants/globals'; import wootConstants from 'dashboard/constants/globals';
const CommandBar = () => import('./commands/commandbar.vue'); const CommandBar = () => import('./commands/commandbar.vue');
@@ -61,7 +61,14 @@ export default {
AddLabelModal, AddLabelModal,
NotificationPanel, NotificationPanel,
}, },
mixins: [uiSettingsMixin], setup() {
const { uiSettings, updateUISettings } = useUISettings();
return {
uiSettings,
updateUISettings,
};
},
data() { data() {
return { return {
showAccountModal: false, showAccountModal: false,
@@ -21,12 +21,10 @@ import appearanceHotKeys from './appearanceHotKeys';
import agentMixin from 'dashboard/mixins/agentMixin'; import agentMixin from 'dashboard/mixins/agentMixin';
import conversationLabelMixin from 'dashboard/mixins/conversation/labelMixin'; import conversationLabelMixin from 'dashboard/mixins/conversation/labelMixin';
import conversationTeamMixin from 'dashboard/mixins/conversation/teamMixin'; import conversationTeamMixin from 'dashboard/mixins/conversation/teamMixin';
import adminMixin from 'dashboard/mixins/isAdmin';
import { GENERAL_EVENTS } from '../../../helper/AnalyticsHelper/events'; import { GENERAL_EVENTS } from '../../../helper/AnalyticsHelper/events';
export default { export default {
mixins: [ mixins: [
adminMixin,
agentMixin, agentMixin,
conversationHotKeysMixin, conversationHotKeysMixin,
bulkActionsHotKeysMixin, bulkActionsHotKeysMixin,
@@ -17,6 +17,7 @@ import {
} from './CommandBarIcons'; } from './CommandBarIcons';
import { frontendURL } from '../../../helper/URLHelper'; import { frontendURL } from '../../../helper/URLHelper';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { useAdmin } from 'dashboard/composables/useAdmin';
import { FEATURE_FLAGS } from '../../../featureFlags'; import { FEATURE_FLAGS } from '../../../featureFlags';
const GO_TO_COMMANDS = [ const GO_TO_COMMANDS = [
@@ -172,6 +173,12 @@ const GO_TO_COMMANDS = [
]; ];
export default { export default {
setup() {
const { isAdmin } = useAdmin();
return {
isAdmin,
};
},
computed: { computed: {
...mapGetters({ ...mapGetters({
accountId: 'getCurrentAccountId', accountId: 'getCurrentAccountId',
@@ -86,7 +86,7 @@ import ContactInfo from 'dashboard/routes/dashboard/conversation/contact/Contact
import ContactLabel from 'dashboard/routes/dashboard/contacts/components/ContactLabels.vue'; import ContactLabel from 'dashboard/routes/dashboard/contacts/components/ContactLabels.vue';
import CustomAttributes from 'dashboard/routes/dashboard/conversation/customAttributes/CustomAttributes.vue'; import CustomAttributes from 'dashboard/routes/dashboard/conversation/customAttributes/CustomAttributes.vue';
import draggable from 'vuedraggable'; import draggable from 'vuedraggable';
import uiSettingsMixin from 'dashboard/mixins/uiSettings'; import { useUISettings } from 'dashboard/composables/useUISettings';
export default { export default {
components: { components: {
@@ -97,7 +97,6 @@ export default {
CustomAttributes, CustomAttributes,
draggable, draggable,
}, },
mixins: [uiSettingsMixin],
props: { props: {
contact: { contact: {
type: Object, type: Object,
@@ -116,6 +115,21 @@ export default {
default: true, default: true,
}, },
}, },
setup() {
const {
updateUISettings,
isContactSidebarItemOpen,
contactSidebarItemsOrder,
toggleSidebarUIState,
} = useUISettings();
return {
updateUISettings,
isContactSidebarItemOpen,
contactSidebarItemsOrder,
toggleSidebarUIState,
};
},
data() { data() {
return { return {
dragEnabled: true, dragEnabled: true,
@@ -9,12 +9,11 @@
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { useAlert } from 'dashboard/composables';
import LabelSelector from 'dashboard/components/widgets/LabelSelector.vue'; import LabelSelector from 'dashboard/components/widgets/LabelSelector.vue';
import alertMixin from 'shared/mixins/alertMixin';
export default { export default {
components: { LabelSelector }, components: { LabelSelector },
mixins: [alertMixin],
props: { props: {
contactId: { contactId: {
type: [String, Number], type: [String, Number],
@@ -59,7 +58,7 @@ export default {
labels: selectedLabels, labels: selectedLabels,
}); });
} catch (error) { } catch (error) {
this.showAlert(this.$t('CONTACT_PANEL.LABELS.CONTACT.ERROR')); useAlert(this.$t('CONTACT_PANEL.LABELS.CONTACT.ERROR'));
} }
}, },
@@ -89,7 +89,7 @@
</template> </template>
<script> <script>
import alertMixin from 'shared/mixins/alertMixin'; import { useAlert } from 'dashboard/composables';
import { required } from 'vuelidate/lib/validators'; import { required } from 'vuelidate/lib/validators';
import FilterInputBox from '../../../../components/widgets/FilterInput/Index.vue'; import FilterInputBox from '../../../../components/widgets/FilterInput/Index.vue';
import countries from 'shared/constants/countries.js'; import countries from 'shared/constants/countries.js';
@@ -98,11 +98,13 @@ import { filterAttributeGroups } from '../contactFilterItems';
import filterMixin from 'shared/mixins/filterMixin'; import filterMixin from 'shared/mixins/filterMixin';
import * as OPERATORS from 'dashboard/components/widgets/FilterInput/FilterOperatorTypes.js'; import * as OPERATORS from 'dashboard/components/widgets/FilterInput/FilterOperatorTypes.js';
import { CONTACTS_EVENTS } from '../../../../helper/AnalyticsHelper/events'; import { CONTACTS_EVENTS } from '../../../../helper/AnalyticsHelper/events';
import cloneObject from 'dashboard/helpers/clone';
export default { export default {
components: { components: {
FilterInputBox, FilterInputBox,
}, },
mixins: [alertMixin, filterMixin], mixins: [filterMixin],
props: { props: {
onClose: { onClose: {
type: Function, type: Function,
@@ -308,7 +310,7 @@ export default {
}, },
removeFilter(index) { removeFilter(index) {
if (this.appliedFilters.length <= 1) { if (this.appliedFilters.length <= 1) {
this.showAlert(this.$t('CONTACTS_FILTER.FILTER_DELETE_ERROR')); useAlert(this.$t('CONTACTS_FILTER.FILTER_DELETE_ERROR'));
} else { } else {
this.appliedFilters.splice(index, 1); this.appliedFilters.splice(index, 1);
} }
@@ -318,7 +320,7 @@ export default {
if (this.$v.$invalid) return; if (this.$v.$invalid) return;
this.$store.dispatch( this.$store.dispatch(
'contacts/setContactFilters', 'contacts/setContactFilters',
JSON.parse(JSON.stringify(this.appliedFilters)) cloneObject(this.appliedFilters)
); );
this.$emit('applyFilter', this.appliedFilters); this.$emit('applyFilter', this.appliedFilters);
this.$track(CONTACTS_EVENTS.APPLY_FILTER, { this.$track(CONTACTS_EVENTS.APPLY_FILTER, {
@@ -1,6 +1,6 @@
<template> <template>
<section <section
class="contacts-table-wrap bg-white dark:bg-slate-900 flex-1 h-full overflow-hidden -mt-1" class="flex-1 h-full -mt-1 overflow-hidden bg-white contacts-table-wrap dark:bg-slate-900"
> >
<ve-table <ve-table
:fixed-header="true" :fixed-header="true"
@@ -20,7 +20,7 @@
v-else-if="!isLoading && !contacts.length" v-else-if="!isLoading && !contacts.length"
:title="$t('CONTACTS_PAGE.LIST.NO_CONTACTS')" :title="$t('CONTACTS_PAGE.LIST.NO_CONTACTS')"
/> />
<div v-if="isLoading" class="items-center flex text-base justify-center"> <div v-if="isLoading" class="flex items-center justify-center text-base">
<spinner /> <spinner />
<span>{{ $t('CONTACTS_PAGE.LIST.LOADING_MESSAGE') }}</span> <span>{{ $t('CONTACTS_PAGE.LIST.LOADING_MESSAGE') }}</span>
</div> </div>
@@ -34,7 +34,7 @@ import { getCountryFlag } from 'dashboard/helper/flag';
import Spinner from 'shared/components/Spinner.vue'; import Spinner from 'shared/components/Spinner.vue';
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue'; import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
import EmptyState from 'dashboard/components/widgets/EmptyState.vue'; import EmptyState from 'dashboard/components/widgets/EmptyState.vue';
import timeMixin from 'dashboard/mixins/time'; import { dynamicTime } from 'shared/helpers/timeHelper';
import rtlMixin from 'shared/mixins/rtlMixin'; import rtlMixin from 'shared/mixins/rtlMixin';
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue'; import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
@@ -44,7 +44,7 @@ export default {
Spinner, Spinner,
VeTable, VeTable,
}, },
mixins: [timeMixin, rtlMixin], mixins: [rtlMixin],
props: { props: {
contacts: { contacts: {
type: Array, type: Array,
@@ -105,9 +105,9 @@ export default {
countryCode: additional.country_code, countryCode: additional.country_code,
conversationsCount: item.conversations_count || '---', conversationsCount: item.conversations_count || '---',
last_activity_at: lastActivityAt last_activity_at: lastActivityAt
? this.dynamicTime(lastActivityAt) ? dynamicTime(lastActivityAt)
: '---', : '---',
created_at: createdAt ? this.dynamicTime(createdAt) : '---', created_at: createdAt ? dynamicTime(createdAt) : '---',
}; };
}); });
}, },
@@ -134,7 +134,7 @@ export default {
status={row.availability_status} status={row.availability_status}
/> />
<div class="user-block"> <div class="user-block">
<h6 class="text-base overflow-hidden whitespace-nowrap text-ellipsis"> <h6 class="overflow-hidden text-base whitespace-nowrap text-ellipsis">
<router-link <router-link
to={`/app/accounts/${this.$route.params.accountId}/contacts/${row.id}`} to={`/app/accounts/${this.$route.params.accountId}/contacts/${row.id}`}
class="user-name" class="user-name"
@@ -81,6 +81,7 @@
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { useAlert } from 'dashboard/composables';
import ContactsHeader from './Header.vue'; import ContactsHeader from './Header.vue';
import ContactsTable from './ContactsTable.vue'; import ContactsTable from './ContactsTable.vue';
@@ -94,7 +95,6 @@ import filterQueryGenerator from '../../../../helper/filterQueryGenerator';
import AddCustomViews from 'dashboard/routes/dashboard/customviews/AddCustomViews.vue'; import AddCustomViews from 'dashboard/routes/dashboard/customviews/AddCustomViews.vue';
import DeleteCustomViews from 'dashboard/routes/dashboard/customviews/DeleteCustomViews.vue'; import DeleteCustomViews from 'dashboard/routes/dashboard/customviews/DeleteCustomViews.vue';
import { CONTACTS_EVENTS } from '../../../../helper/AnalyticsHelper/events'; import { CONTACTS_EVENTS } from '../../../../helper/AnalyticsHelper/events';
import alertMixin from 'shared/mixins/alertMixin';
import countries from 'shared/constants/countries.js'; import countries from 'shared/constants/countries.js';
import { generateValuesForEditCustomViews } from 'dashboard/helper/customViewsHelper'; import { generateValuesForEditCustomViews } from 'dashboard/helper/customViewsHelper';
@@ -113,7 +113,6 @@ export default {
AddCustomViews, AddCustomViews,
DeleteCustomViews, DeleteCustomViews,
}, },
mixins: [alertMixin],
props: { props: {
label: { type: String, default: '' }, label: { type: String, default: '' },
segmentsId: { segmentsId: {
@@ -404,11 +403,9 @@ export default {
...query, ...query,
label: this.label, label: this.label,
}); });
this.showAlert(this.$t('EXPORT_CONTACTS.SUCCESS_MESSAGE')); useAlert(this.$t('EXPORT_CONTACTS.SUCCESS_MESSAGE'));
} catch (error) { } catch (error) {
this.showAlert( useAlert(error.message || this.$t('EXPORT_CONTACTS.ERROR_MESSAGE'));
error.message || this.$t('EXPORT_CONTACTS.ERROR_MESSAGE')
);
} }
}, },
setParamsForEditSegmentModal() { setParamsForEditSegmentModal() {
@@ -125,10 +125,9 @@
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import adminMixin from 'dashboard/mixins/isAdmin'; import { useAdmin } from 'dashboard/composables/useAdmin';
export default { export default {
mixins: [adminMixin],
props: { props: {
headerTitle: { headerTitle: {
type: String, type: String,
@@ -143,6 +142,12 @@ export default {
default: 0, default: 0,
}, },
}, },
setup() {
const { isAdmin } = useAdmin();
return {
isAdmin,
};
},
data() { data() {
return { return {
showCreateModal: false, showCreateModal: false,
@@ -1,6 +1,6 @@
<template> <template>
<modal :show.sync="show" :on-close="onClose"> <modal :show.sync="show" :on-close="onClose">
<div class="h-auto overflow-auto flex flex-col"> <div class="flex flex-col h-auto overflow-auto">
<woot-modal-header :header-title="$t('IMPORT_CONTACTS.TITLE')"> <woot-modal-header :header-title="$t('IMPORT_CONTACTS.TITLE')">
<p> <p>
{{ $t('IMPORT_CONTACTS.DESC') }} {{ $t('IMPORT_CONTACTS.DESC') }}
@@ -22,7 +22,7 @@
/> />
</label> </label>
</div> </div>
<div class="flex flex-row justify-end gap-2 py-2 px-0 w-full"> <div class="flex flex-row justify-end w-full gap-2 px-0 py-2">
<div class="w-full"> <div class="w-full">
<woot-button <woot-button
:disabled="uiFlags.isCreating || !file" :disabled="uiFlags.isCreating || !file"
@@ -42,16 +42,15 @@
</template> </template>
<script> <script>
import Modal from '../../../../components/Modal.vue';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import alertMixin from 'shared/mixins/alertMixin'; import { useAlert } from 'dashboard/composables';
import { CONTACTS_EVENTS } from '../../../../helper/AnalyticsHelper/events'; import { CONTACTS_EVENTS } from '../../../../helper/AnalyticsHelper/events';
import Modal from '../../../../components/Modal.vue';
export default { export default {
components: { components: {
Modal, Modal,
}, },
mixins: [alertMixin],
props: { props: {
onClose: { onClose: {
type: Function, type: Function,
@@ -81,12 +80,10 @@ export default {
if (!this.file) return; if (!this.file) return;
await this.$store.dispatch('contacts/import', this.file); await this.$store.dispatch('contacts/import', this.file);
this.onClose(); this.onClose();
this.showAlert(this.$t('IMPORT_CONTACTS.SUCCESS_MESSAGE')); useAlert(this.$t('IMPORT_CONTACTS.SUCCESS_MESSAGE'));
this.$track(CONTACTS_EVENTS.IMPORT_SUCCESS); this.$track(CONTACTS_EVENTS.IMPORT_SUCCESS);
} catch (error) { } catch (error) {
this.showAlert( useAlert(error.message || this.$t('IMPORT_CONTACTS.ERROR_MESSAGE'));
error.message || this.$t('IMPORT_CONTACTS.ERROR_MESSAGE')
);
this.$track(CONTACTS_EVENTS.IMPORT_FAILURE); this.$track(CONTACTS_EVENTS.IMPORT_FAILURE);
} }
}, },
@@ -28,10 +28,8 @@
</template> </template>
<script> <script>
import timeMixin from 'dashboard/mixins/time'; import { dynamicTime } from 'shared/helpers/timeHelper';
export default { export default {
mixins: [timeMixin],
props: { props: {
eventType: { eventType: {
type: String, type: String,
@@ -53,7 +51,7 @@ export default {
computed: { computed: {
readableTime() { readableTime() {
return this.dynamicTime(this.timeStamp); return dynamicTime(this.timeStamp);
}, },
}, },
@@ -133,7 +133,7 @@
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import alertMixin from 'shared/mixins/alertMixin'; import { useUISettings } from 'dashboard/composables/useUISettings';
import AccordionItem from 'dashboard/components/Accordion/AccordionItem.vue'; import AccordionItem from 'dashboard/components/Accordion/AccordionItem.vue';
import ContactConversations from './ContactConversations.vue'; import ContactConversations from './ContactConversations.vue';
import ConversationAction from './ConversationAction.vue'; import ConversationAction from './ConversationAction.vue';
@@ -143,7 +143,6 @@ import ContactInfo from './contact/ContactInfo.vue';
import ConversationInfo from './ConversationInfo.vue'; import ConversationInfo from './ConversationInfo.vue';
import CustomAttributes from './customAttributes/CustomAttributes.vue'; import CustomAttributes from './customAttributes/CustomAttributes.vue';
import draggable from 'vuedraggable'; import draggable from 'vuedraggable';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import MacrosList from './Macros/List.vue'; import MacrosList from './Macros/List.vue';
export default { export default {
components: { components: {
@@ -157,7 +156,6 @@ export default {
draggable, draggable,
MacrosList, MacrosList,
}, },
mixins: [alertMixin, uiSettingsMixin],
props: { props: {
conversationId: { conversationId: {
type: [Number, String], type: [Number, String],
@@ -172,6 +170,21 @@ export default {
default: () => {}, default: () => {},
}, },
}, },
setup() {
const {
updateUISettings,
isContactSidebarItemOpen,
conversationSidebarItemsOrder,
toggleSidebarUIState,
} = useUISettings();
return {
updateUISettings,
isContactSidebarItemOpen,
conversationSidebarItemsOrder,
toggleSidebarUIState,
};
},
data() { data() {
return { return {
dragEnabled: true, dragEnabled: true,
@@ -82,7 +82,7 @@
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import alertMixin from 'shared/mixins/alertMixin'; import { useAlert } from 'dashboard/composables';
import ContactDetailsItem from './ContactDetailsItem.vue'; import ContactDetailsItem from './ContactDetailsItem.vue';
import MultiselectDropdown from 'shared/components/ui/MultiselectDropdown.vue'; import MultiselectDropdown from 'shared/components/ui/MultiselectDropdown.vue';
import ConversationLabels from './labels/LabelBox.vue'; import ConversationLabels from './labels/LabelBox.vue';
@@ -97,7 +97,7 @@ export default {
MultiselectDropdown, MultiselectDropdown,
ConversationLabels, ConversationLabels,
}, },
mixins: [agentMixin, alertMixin, teamMixin], mixins: [agentMixin, teamMixin],
props: { props: {
conversationId: { conversationId: {
type: [Number, String], type: [Number, String],
@@ -157,7 +157,7 @@ export default {
agentId, agentId,
}) })
.then(() => { .then(() => {
this.showAlert(this.$t('CONVERSATION.CHANGE_AGENT')); useAlert(this.$t('CONVERSATION.CHANGE_AGENT'));
}); });
}, },
}, },
@@ -172,7 +172,7 @@ export default {
this.$store this.$store
.dispatch('assignTeam', { conversationId, teamId }) .dispatch('assignTeam', { conversationId, teamId })
.then(() => { .then(() => {
this.showAlert(this.$t('CONVERSATION.CHANGE_TEAM')); useAlert(this.$t('CONVERSATION.CHANGE_TEAM'));
}); });
}, },
}, },
@@ -201,7 +201,7 @@ export default {
newValue: priority, newValue: priority,
from: 'Conversation Sidebar', from: 'Conversation Sidebar',
}); });
this.showAlert( useAlert(
this.$t('CONVERSATION.PRIORITY.CHANGE_PRIORITY.SUCCESSFUL', { this.$t('CONVERSATION.PRIORITY.CHANGE_PRIORITY.SUCCESSFUL', {
priority: priorityItem.name, priority: priorityItem.name,
conversationId, conversationId,
@@ -79,7 +79,7 @@
<script> <script>
import Spinner from 'shared/components/Spinner.vue'; import Spinner from 'shared/components/Spinner.vue';
import alertMixin from 'shared/mixins/alertMixin'; import { useAlert } from 'dashboard/composables';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import agentMixin from 'dashboard/mixins/agentMixin'; import agentMixin from 'dashboard/mixins/agentMixin';
import ThumbnailGroup from 'dashboard/components/widgets/ThumbnailGroup.vue'; import ThumbnailGroup from 'dashboard/components/widgets/ThumbnailGroup.vue';
@@ -91,7 +91,7 @@ export default {
ThumbnailGroup, ThumbnailGroup,
MultiselectDropdownItems, MultiselectDropdownItems,
}, },
mixins: [alertMixin, agentMixin], mixins: [agentMixin],
props: { props: {
conversationId: { conversationId: {
type: [Number, String], type: [Number, String],
@@ -197,7 +197,7 @@ export default {
error?.message || error?.message ||
this.$t('CONVERSATION_PARTICIPANTS.API.ERROR_MESSAGE'); this.$t('CONVERSATION_PARTICIPANTS.API.ERROR_MESSAGE');
} finally { } finally {
this.showAlert(alertMessage); useAlert(alertMessage);
} }
this.fetchParticipants(); this.fetchParticipants();
}, },
@@ -1,5 +1,5 @@
<template> <template>
<section class="conversation-page bg-white dark:bg-slate-900"> <section class="bg-white conversation-page dark:bg-slate-900">
<chat-list <chat-list
:show-conversation-list="showConversationList" :show-conversation-list="showConversationList"
:conversation-inbox="inboxId" :conversation-inbox="inboxId"
@@ -36,13 +36,13 @@
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { useAlert } from 'dashboard/composables';
import { useUISettings } from 'dashboard/composables/useUISettings';
import { getUnixTime } from 'date-fns'; import { getUnixTime } from 'date-fns';
import ChatList from '../../../components/ChatList.vue'; import ChatList from '../../../components/ChatList.vue';
import ConversationBox from '../../../components/widgets/conversation/ConversationBox.vue'; import ConversationBox from '../../../components/widgets/conversation/ConversationBox.vue';
import PopOverSearch from './search/PopOverSearch.vue'; import PopOverSearch from './search/PopOverSearch.vue';
import CustomSnoozeModal from 'dashboard/components/CustomSnoozeModal.vue'; import CustomSnoozeModal from 'dashboard/components/CustomSnoozeModal.vue';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import alertMixin from 'shared/mixins/alertMixin';
import wootConstants from 'dashboard/constants/globals'; import wootConstants from 'dashboard/constants/globals';
import { BUS_EVENTS } from 'shared/constants/busEvents'; import { BUS_EVENTS } from 'shared/constants/busEvents';
import { CMD_SNOOZE_CONVERSATION } from 'dashboard/routes/dashboard/commands/commandBarBusEvents'; import { CMD_SNOOZE_CONVERSATION } from 'dashboard/routes/dashboard/commands/commandBarBusEvents';
@@ -55,7 +55,6 @@ export default {
PopOverSearch, PopOverSearch,
CustomSnoozeModal, CustomSnoozeModal,
}, },
mixins: [uiSettingsMixin, alertMixin],
props: { props: {
inboxId: { inboxId: {
type: [String, Number], type: [String, Number],
@@ -82,6 +81,14 @@ export default {
default: 0, default: 0,
}, },
}, },
setup() {
const { uiSettings, updateUISettings } = useUISettings();
return {
uiSettings,
updateUISettings,
};
},
data() { data() {
return { return {
showSearchModal: false, showSearchModal: false,
@@ -235,7 +242,7 @@ export default {
}) })
.then(() => { .then(() => {
this.$store.dispatch('setContextMenuChatId', null); this.$store.dispatch('setContextMenuChatId', null);
this.showAlert(this.$t('CONVERSATION.CHANGE_STATUS')); useAlert(this.$t('CONVERSATION.CHANGE_STATUS'));
}); });
}, },
hideCustomSnoozeModal() { hideCustomSnoozeModal() {
@@ -3,7 +3,7 @@
<span class="overflow-hidden whitespace-nowrap text-ellipsis">{{ <span class="overflow-hidden whitespace-nowrap text-ellipsis">{{
macro.name macro.name
}}</span> }}</span>
<div class="macros-actions flex items-center gap-1"> <div class="flex items-center gap-1 macros-actions">
<woot-button <woot-button
v-tooltip.left-start="$t('MACROS.EXECUTE.PREVIEW')" v-tooltip.left-start="$t('MACROS.EXECUTE.PREVIEW')"
size="tiny" size="tiny"
@@ -33,7 +33,7 @@
</template> </template>
<script> <script>
import alertMixin from 'shared/mixins/alertMixin'; import { useAlert } from 'dashboard/composables';
import MacroPreview from './MacroPreview.vue'; import MacroPreview from './MacroPreview.vue';
import { CONVERSATION_EVENTS } from '../../../../helper/AnalyticsHelper/events'; import { CONVERSATION_EVENTS } from '../../../../helper/AnalyticsHelper/events';
@@ -41,7 +41,6 @@ export default {
components: { components: {
MacroPreview, MacroPreview,
}, },
mixins: [alertMixin],
props: { props: {
macro: { macro: {
type: Object, type: Object,
@@ -67,9 +66,9 @@ export default {
conversationIds: [this.conversationId], conversationIds: [this.conversationId],
}); });
this.$track(CONVERSATION_EVENTS.EXECUTED_A_MACRO); this.$track(CONVERSATION_EVENTS.EXECUTED_A_MACRO);
this.showAlert(this.$t('MACROS.EXECUTE.EXECUTED_SUCCESSFULLY')); useAlert(this.$t('MACROS.EXECUTE.EXECUTED_SUCCESSFULLY'));
} catch (error) { } catch (error) {
this.showAlert(this.$t('MACROS.ERROR')); useAlert(this.$t('MACROS.ERROR'));
} finally { } finally {
this.isExecuting = false; this.isExecuting = false;
} }
@@ -150,7 +150,7 @@
</template> </template>
<script> <script>
import alertMixin from 'shared/mixins/alertMixin'; import { useAlert } from 'dashboard/composables';
import { import {
DuplicateContactException, DuplicateContactException,
ExceptionWithMessage, ExceptionWithMessage,
@@ -161,7 +161,6 @@ import { isPhoneNumberValid } from 'shared/helpers/Validators';
import parsePhoneNumber from 'libphonenumber-js'; import parsePhoneNumber from 'libphonenumber-js';
export default { export default {
mixins: [alertMixin],
props: { props: {
contact: { contact: {
type: Object, type: Object,
@@ -375,20 +374,18 @@ export default {
try { try {
await this.onSubmit(this.getContactObject()); await this.onSubmit(this.getContactObject());
this.onSuccess(); this.onSuccess();
this.showAlert(this.$t('CONTACT_FORM.SUCCESS_MESSAGE')); useAlert(this.$t('CONTACT_FORM.SUCCESS_MESSAGE'));
} catch (error) { } catch (error) {
if (error instanceof DuplicateContactException) { if (error instanceof DuplicateContactException) {
if (error.data.includes('email')) { if (error.data.includes('email')) {
this.showAlert( useAlert(this.$t('CONTACT_FORM.FORM.EMAIL_ADDRESS.DUPLICATE'));
this.$t('CONTACT_FORM.FORM.EMAIL_ADDRESS.DUPLICATE')
);
} else if (error.data.includes('phone_number')) { } else if (error.data.includes('phone_number')) {
this.showAlert(this.$t('CONTACT_FORM.FORM.PHONE_NUMBER.DUPLICATE')); useAlert(this.$t('CONTACT_FORM.FORM.PHONE_NUMBER.DUPLICATE'));
} }
} else if (error instanceof ExceptionWithMessage) { } else if (error instanceof ExceptionWithMessage) {
this.showAlert(error.data); useAlert(error.data);
} else { } else {
this.showAlert(this.$t('CONTACT_FORM.ERROR_MESSAGE')); useAlert(this.$t('CONTACT_FORM.ERROR_MESSAGE'));
} }
} }
}, },
@@ -400,15 +397,13 @@ export default {
try { try {
if (this.contact && this.contact.id) { if (this.contact && this.contact.id) {
await this.$store.dispatch('contacts/deleteAvatar', this.contact.id); await this.$store.dispatch('contacts/deleteAvatar', this.contact.id);
this.showAlert( useAlert(this.$t('CONTACT_FORM.DELETE_AVATAR.API.SUCCESS_MESSAGE'));
this.$t('CONTACT_FORM.DELETE_AVATAR.API.SUCCESS_MESSAGE')
);
} }
this.avatarFile = null; this.avatarFile = null;
this.avatarUrl = ''; this.avatarUrl = '';
this.activeDialCode = ''; this.activeDialCode = '';
} catch (error) { } catch (error) {
this.showAlert( useAlert(
error.message error.message
? error.message ? error.message
: this.$t('CONTACT_FORM.DELETE_AVATAR.API.ERROR_MESSAGE') : this.$t('CONTACT_FORM.DELETE_AVATAR.API.ERROR_MESSAGE')
@@ -1,7 +1,7 @@
<template> <template>
<div class="relative items-center p-4 bg-white dark:bg-slate-900 w-full"> <div class="relative items-center w-full p-4 bg-white dark:bg-slate-900">
<div class="text-left rtl:text-right flex flex-col gap-2 w-full"> <div class="flex flex-col w-full gap-2 text-left rtl:text-right">
<div class="flex justify-between flex-row"> <div class="flex flex-row justify-between">
<thumbnail <thumbnail
v-if="showAvatar" v-if="showAvatar"
:src="contact.thumbnail" :src="contact.thumbnail"
@@ -18,9 +18,9 @@
</div> </div>
<div class="flex flex-col items-start gap-1.5 min-w-0 w-full"> <div class="flex flex-col items-start gap-1.5 min-w-0 w-full">
<div v-if="showAvatar" class="flex items-start gap-2 min-w-0 w-full"> <div v-if="showAvatar" class="flex items-start w-full min-w-0 gap-2">
<h3 <h3
class="flex-shrink min-w-0 text-base text-slate-800 dark:text-slate-100 capitalize my-0 max-w-full break-words" class="flex-shrink max-w-full min-w-0 my-0 text-base capitalize break-words text-slate-800 dark:text-slate-100"
> >
{{ contact.name }} {{ contact.name }}
</h3> </h3>
@@ -55,7 +55,7 @@
<p v-if="additionalAttributes.description" class="break-words mb-0.5"> <p v-if="additionalAttributes.description" class="break-words mb-0.5">
{{ additionalAttributes.description }} {{ additionalAttributes.description }}
</p> </p>
<div class="flex flex-col gap-2 items-start w-full"> <div class="flex flex-col items-start w-full gap-2">
<contact-info-row <contact-info-row
:href="contact.email ? `mailto:${contact.email}` : ''" :href="contact.email ? `mailto:${contact.email}` : ''"
:value="contact.email" :value="contact.email"
@@ -166,17 +166,16 @@
</div> </div>
</template> </template>
<script> <script>
import timeMixin from 'dashboard/mixins/time'; import { mapGetters } from 'vuex';
import { useAlert } from 'dashboard/composables';
import { dynamicTime } from 'shared/helpers/timeHelper';
import { useAdmin } from 'dashboard/composables/useAdmin';
import ContactInfoRow from './ContactInfoRow.vue'; import ContactInfoRow from './ContactInfoRow.vue';
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue'; import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
import SocialIcons from './SocialIcons.vue'; import SocialIcons from './SocialIcons.vue';
import EditContact from './EditContact.vue'; import EditContact from './EditContact.vue';
import NewConversation from './NewConversation.vue'; import NewConversation from './NewConversation.vue';
import ContactMergeModal from 'dashboard/modules/contact/ContactMergeModal.vue'; import ContactMergeModal from 'dashboard/modules/contact/ContactMergeModal.vue';
import alertMixin from 'shared/mixins/alertMixin';
import adminMixin from '../../../../mixins/isAdmin';
import { mapGetters } from 'vuex';
import { getCountryFlag } from 'dashboard/helper/flag'; import { getCountryFlag } from 'dashboard/helper/flag';
import { BUS_EVENTS } from 'shared/constants/busEvents'; import { BUS_EVENTS } from 'shared/constants/busEvents';
import { import {
@@ -194,7 +193,6 @@ export default {
NewConversation, NewConversation,
ContactMergeModal, ContactMergeModal,
}, },
mixins: [alertMixin, adminMixin, timeMixin],
props: { props: {
contact: { contact: {
type: Object, type: Object,
@@ -217,6 +215,12 @@ export default {
default: 'chevron-right', default: 'chevron-right',
}, },
}, },
setup() {
const { isAdmin } = useAdmin();
return {
isAdmin,
};
},
data() { data() {
return { return {
showEditModal: false, showEditModal: false,
@@ -260,6 +264,7 @@ export default {
}, },
}, },
methods: { methods: {
dynamicTime,
toggleMergeModal() { toggleMergeModal() {
this.showMergeModal = !this.showMergeModal; this.showMergeModal = !this.showMergeModal;
}, },
@@ -300,7 +305,7 @@ export default {
try { try {
await this.$store.dispatch('contacts/delete', id); await this.$store.dispatch('contacts/delete', id);
this.$emit('panel-close'); this.$emit('panel-close');
this.showAlert(this.$t('DELETE_CONTACT.API.SUCCESS_MESSAGE')); useAlert(this.$t('DELETE_CONTACT.API.SUCCESS_MESSAGE'));
if (isAConversationRoute(this.$route.name)) { if (isAConversationRoute(this.$route.name)) {
this.$router.push({ this.$router.push({
@@ -316,7 +321,7 @@ export default {
}); });
} }
} catch (error) { } catch (error) {
this.showAlert( useAlert(
error.message error.message
? error.message ? error.message
: this.$t('DELETE_CONTACT.API.ERROR_MESSAGE') : this.$t('DELETE_CONTACT.API.ERROR_MESSAGE')
@@ -1,5 +1,5 @@
<template> <template>
<div class="ltr:-ml-1 rtl:-mr-1 h-5 w-full"> <div class="w-full h-5 ltr:-ml-1 rtl:-mr-1">
<a <a
v-if="href" v-if="href"
:href="href" :href="href"
@@ -9,16 +9,16 @@
:icon="icon" :icon="icon"
:emoji="emoji" :emoji="emoji"
icon-size="14" icon-size="14"
class="ltr:ml-1 rtl:mr-1 flex-shrink-0" class="flex-shrink-0 ltr:ml-1 rtl:mr-1"
/> />
<span <span
v-if="value" v-if="value"
class="overflow-hidden whitespace-nowrap text-ellipsis text-sm" class="overflow-hidden text-sm whitespace-nowrap text-ellipsis"
:title="value" :title="value"
> >
{{ value }} {{ value }}
</span> </span>
<span v-else class="text-slate-300 dark:text-slate-600 text-sm">{{ <span v-else class="text-sm text-slate-300 dark:text-slate-600">{{
$t('CONTACT_PANEL.NOT_AVAILABLE') $t('CONTACT_PANEL.NOT_AVAILABLE')
}}</span> }}</span>
@@ -42,22 +42,22 @@
:icon="icon" :icon="icon"
:emoji="emoji" :emoji="emoji"
icon-size="14" icon-size="14"
class="ltr:ml-1 rtl:mr-1 flex-shrink-0" class="flex-shrink-0 ltr:ml-1 rtl:mr-1"
/> />
<span <span
v-if="value" v-if="value"
class="overflow-hidden whitespace-nowrap text-ellipsis text-sm" class="overflow-hidden text-sm whitespace-nowrap text-ellipsis"
> >
{{ value }} {{ value }}
</span> </span>
<span v-else class="text-slate-300 dark:text-slate-600 text-sm">{{ <span v-else class="text-sm text-slate-300 dark:text-slate-600">{{
$t('CONTACT_PANEL.NOT_AVAILABLE') $t('CONTACT_PANEL.NOT_AVAILABLE')
}}</span> }}</span>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import alertMixin from 'shared/mixins/alertMixin'; import { useAlert } from 'dashboard/composables';
import EmojiOrIcon from 'shared/components/EmojiOrIcon.vue'; import EmojiOrIcon from 'shared/components/EmojiOrIcon.vue';
import { copyTextToClipboard } from 'shared/helpers/clipboard'; import { copyTextToClipboard } from 'shared/helpers/clipboard';
@@ -65,7 +65,6 @@ export default {
components: { components: {
EmojiOrIcon, EmojiOrIcon,
}, },
mixins: [alertMixin],
props: { props: {
href: { href: {
type: String, type: String,
@@ -92,7 +91,7 @@ export default {
async onCopy(e) { async onCopy(e) {
e.preventDefault(); e.preventDefault();
await copyTextToClipboard(this.value); await copyTextToClipboard(this.value);
this.showAlert(this.$t('CONTACT_PANEL.COPY_SUCCESSFUL')); useAlert(this.$t('CONTACT_PANEL.COPY_SUCCESSFUL'));
}, },
}, },
}; };
@@ -1,5 +1,5 @@
<template> <template>
<form class="conversation--form w-full" @submit.prevent="onFormSubmit"> <form class="w-full conversation--form" @submit.prevent="onFormSubmit">
<div <div
v-if="showNoInboxAlert" v-if="showNoInboxAlert"
class="relative mx-0 mt-0 mb-2.5 p-2 rounded-none text-sm border border-solid border-yellow-500 dark:border-yellow-700 bg-yellow-200/60 dark:bg-yellow-200/20 text-slate-700 dark:text-yellow-400" class="relative mx-0 mt-0 mb-2.5 p-2 rounded-none text-sm border border-solid border-yellow-500 dark:border-yellow-700 bg-yellow-200/60 dark:bg-yellow-200/20 text-slate-700 dark:text-yellow-400"
@@ -9,7 +9,7 @@
</p> </p>
</div> </div>
<div v-else> <div v-else>
<div class="gap-2 flex flex-row"> <div class="flex flex-row gap-2">
<div class="w-[50%]"> <div class="w-[50%]">
<label> <label>
{{ $t('NEW_CONVERSATION.FORM.INBOX.LABEL') }} {{ $t('NEW_CONVERSATION.FORM.INBOX.LABEL') }}
@@ -69,7 +69,7 @@
:status="contact.availability_status" :status="contact.availability_status"
/> />
<h4 <h4
class="m-0 ml-2 mr-2 text-slate-700 dark:text-slate-100 text-sm" class="m-0 ml-2 mr-2 text-sm text-slate-700 dark:text-slate-100"
> >
{{ contact.name }} {{ contact.name }}
</h4> </h4>
@@ -128,7 +128,7 @@
v-if="isSignatureEnabledForInbox && !messageSignature" v-if="isSignatureEnabledForInbox && !messageSignature"
class="!mx-0 mb-1" class="!mx-0 mb-1"
/> />
<div v-if="isAnEmailInbox" class="mb-3 mt-px"> <div v-if="isAnEmailInbox" class="mt-px mb-3">
<woot-button <woot-button
v-tooltip.top-end="signatureToggleTooltip" v-tooltip.top-end="signatureToggleTooltip"
icon="signature" icon="signature"
@@ -191,7 +191,7 @@
{{ $t('NEW_CONVERSATION.FORM.ATTACHMENTS.SELECT') }} {{ $t('NEW_CONVERSATION.FORM.ATTACHMENTS.SELECT') }}
</woot-button> </woot-button>
<span <span
class="text-slate-500 ltr:ml-1 rtl:mr-1 font-medium text-xs dark:text-slate-400" class="text-xs font-medium text-slate-500 ltr:ml-1 rtl:mr-1 dark:text-slate-400"
> >
{{ $t('NEW_CONVERSATION.FORM.ATTACHMENTS.HELP_TEXT') }} {{ $t('NEW_CONVERSATION.FORM.ATTACHMENTS.HELP_TEXT') }}
</span> </span>
@@ -213,7 +213,7 @@
<div <div
v-if="!hasWhatsappTemplates" v-if="!hasWhatsappTemplates"
class="flex flex-row justify-end gap-2 py-2 px-0 w-full" class="flex flex-row justify-end w-full gap-2 px-0 py-2"
> >
<button class="button clear" @click.prevent="onCancel"> <button class="button clear" @click.prevent="onCancel">
{{ $t('NEW_CONVERSATION.FORM.CANCEL') }} {{ $t('NEW_CONVERSATION.FORM.CANCEL') }}
@@ -226,7 +226,7 @@
<transition v-if="isEmailOrWebWidgetInbox" name="modal-fade"> <transition v-if="isEmailOrWebWidgetInbox" name="modal-fade">
<div <div
v-show="$refs.uploadAttachment && $refs.uploadAttachment.dropActive" v-show="$refs.uploadAttachment && $refs.uploadAttachment.dropActive"
class="flex top-0 bottom-0 z-30 gap-2 right-0 left-0 items-center justify-center flex-col absolute w-full h-full bg-white/80 dark:bg-slate-700/80" class="absolute top-0 bottom-0 left-0 right-0 z-30 flex flex-col items-center justify-center w-full h-full gap-2 bg-white/80 dark:bg-slate-700/80"
> >
<fluent-icon icon="cloud-backup" size="40" /> <fluent-icon icon="cloud-backup" size="40" />
<h4 class="text-2xl break-words text-slate-600 dark:text-slate-200"> <h4 class="text-2xl break-words text-slate-600 dark:text-slate-200">
@@ -239,6 +239,8 @@
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { useAlert } from 'dashboard/composables';
import { useUISettings } from 'dashboard/composables/useUISettings';
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue'; import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue'; import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
import ReplyEmailHead from 'dashboard/components/widgets/conversation/ReplyEmailHead.vue'; import ReplyEmailHead from 'dashboard/components/widgets/conversation/ReplyEmailHead.vue';
@@ -246,7 +248,6 @@ import CannedResponse from 'dashboard/components/widgets/conversation/CannedResp
import MessageSignatureMissingAlert from 'dashboard/components/widgets/conversation/MessageSignatureMissingAlert'; import MessageSignatureMissingAlert from 'dashboard/components/widgets/conversation/MessageSignatureMissingAlert';
import InboxDropdownItem from 'dashboard/components/widgets/InboxDropdownItem.vue'; import InboxDropdownItem from 'dashboard/components/widgets/InboxDropdownItem.vue';
import WhatsappTemplates from './WhatsappTemplates.vue'; import WhatsappTemplates from './WhatsappTemplates.vue';
import alertMixin from 'shared/mixins/alertMixin';
import { INBOX_TYPES } from 'shared/mixins/inboxMixin'; import { INBOX_TYPES } from 'shared/mixins/inboxMixin';
import { ExceptionWithMessage } from 'shared/helpers/CustomErrors'; import { ExceptionWithMessage } from 'shared/helpers/CustomErrors';
import { getInboxSource } from 'dashboard/helper/inbox'; import { getInboxSource } from 'dashboard/helper/inbox';
@@ -260,7 +261,6 @@ import {
appendSignature, appendSignature,
removeSignature, removeSignature,
} from 'dashboard/helper/editorHelper'; } from 'dashboard/helper/editorHelper';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
export default { export default {
components: { components: {
@@ -274,7 +274,7 @@ export default {
AttachmentPreview, AttachmentPreview,
MessageSignatureMissingAlert, MessageSignatureMissingAlert,
}, },
mixins: [alertMixin, uiSettingsMixin, inboxMixin, fileUploadMixin], mixins: [inboxMixin, fileUploadMixin],
props: { props: {
contact: { contact: {
type: Object, type: Object,
@@ -289,6 +289,15 @@ export default {
default: '', default: '',
}, },
}, },
setup() {
const { fetchSignatureFlagFromUISettings, setSignatureFlagForInbox } =
useUISettings();
return {
fetchSignatureFlagFromUISettings,
setSignatureFlagForInbox,
};
},
data() { data() {
return { return {
name: '', name: '',
@@ -323,7 +332,7 @@ export default {
messageSignature: 'getMessageSignature', messageSignature: 'getMessageSignature',
}), }),
sendWithSignature() { sendWithSignature() {
return this.fetchSignatureFlagFromUiSettings(this.channelType); return this.fetchSignatureFlagFromUISettings(this.channelType);
}, },
signatureToApply() { signatureToApply() {
return this.messageSignature; return this.messageSignature;
@@ -515,15 +524,12 @@ export default {
message: this.$t('NEW_CONVERSATION.FORM.GO_TO_CONVERSATION'), message: this.$t('NEW_CONVERSATION.FORM.GO_TO_CONVERSATION'),
}; };
this.onSuccess(); this.onSuccess();
this.showAlert( useAlert(this.$t('NEW_CONVERSATION.FORM.SUCCESS_MESSAGE'), action);
this.$t('NEW_CONVERSATION.FORM.SUCCESS_MESSAGE'),
action
);
} catch (error) { } catch (error) {
if (error instanceof ExceptionWithMessage) { if (error instanceof ExceptionWithMessage) {
this.showAlert(error.data); useAlert(error.data);
} else { } else {
this.showAlert(this.$t('NEW_CONVERSATION.FORM.ERROR_MESSAGE')); useAlert(this.$t('NEW_CONVERSATION.FORM.ERROR_MESSAGE'));
} }
} }
}, },
@@ -41,17 +41,17 @@
</template> </template>
<script> <script>
import CustomAttribute from 'dashboard/components/CustomAttribute.vue'; import { useAlert } from 'dashboard/composables';
import alertMixin from 'shared/mixins/alertMixin'; import { useUISettings } from 'dashboard/composables/useUISettings';
import attributeMixin from 'dashboard/mixins/attributeMixin';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import { copyTextToClipboard } from 'shared/helpers/clipboard'; import { copyTextToClipboard } from 'shared/helpers/clipboard';
import CustomAttribute from 'dashboard/components/CustomAttribute.vue';
import attributeMixin from 'dashboard/mixins/attributeMixin';
export default { export default {
components: { components: {
CustomAttribute, CustomAttribute,
}, },
mixins: [alertMixin, attributeMixin, uiSettingsMixin], mixins: [attributeMixin],
props: { props: {
attributeType: { attributeType: {
type: String, type: String,
@@ -71,6 +71,14 @@ export default {
default: '', default: '',
}, },
}, },
setup() {
const { uiSettings, updateUISettings } = useUISettings();
return {
uiSettings,
updateUISettings,
};
},
data() { data() {
return { return {
showAllAttributes: false, showAllAttributes: false,
@@ -141,12 +149,12 @@ export default {
custom_attributes: updatedAttributes, custom_attributes: updatedAttributes,
}); });
} }
this.showAlert(this.$t('CUSTOM_ATTRIBUTES.FORM.UPDATE.SUCCESS')); useAlert(this.$t('CUSTOM_ATTRIBUTES.FORM.UPDATE.SUCCESS'));
} catch (error) { } catch (error) {
const errorMessage = const errorMessage =
error?.response?.message || error?.response?.message ||
this.$t('CUSTOM_ATTRIBUTES.FORM.UPDATE.ERROR'); this.$t('CUSTOM_ATTRIBUTES.FORM.UPDATE.ERROR');
this.showAlert(errorMessage); useAlert(errorMessage);
} }
}, },
async onDelete(key) { async onDelete(key) {
@@ -164,17 +172,17 @@ export default {
}); });
} }
this.showAlert(this.$t('CUSTOM_ATTRIBUTES.FORM.DELETE.SUCCESS')); useAlert(this.$t('CUSTOM_ATTRIBUTES.FORM.DELETE.SUCCESS'));
} catch (error) { } catch (error) {
const errorMessage = const errorMessage =
error?.response?.message || error?.response?.message ||
this.$t('CUSTOM_ATTRIBUTES.FORM.DELETE.ERROR'); this.$t('CUSTOM_ATTRIBUTES.FORM.DELETE.ERROR');
this.showAlert(errorMessage); useAlert(errorMessage);
} }
}, },
async onCopy(attributeValue) { async onCopy(attributeValue) {
await copyTextToClipboard(attributeValue); await copyTextToClipboard(attributeValue);
this.showAlert(this.$t('CUSTOM_ATTRIBUTES.COPY_SUCCESSFUL')); useAlert(this.$t('CUSTOM_ATTRIBUTES.COPY_SUCCESSFUL'));
}, },
}, },
}; };
@@ -45,10 +45,10 @@
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { useAdmin } from 'dashboard/composables/useAdmin';
import Spinner from 'shared/components/Spinner.vue'; import Spinner from 'shared/components/Spinner.vue';
import LabelDropdown from 'shared/components/ui/label/LabelDropdown.vue'; import LabelDropdown from 'shared/components/ui/label/LabelDropdown.vue';
import AddLabel from 'shared/components/ui/dropdown/AddLabel.vue'; import AddLabel from 'shared/components/ui/dropdown/AddLabel.vue';
import adminMixin from 'dashboard/mixins/isAdmin';
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins'; import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
import conversationLabelMixin from 'dashboard/mixins/conversation/labelMixin'; import conversationLabelMixin from 'dashboard/mixins/conversation/labelMixin';
@@ -59,14 +59,19 @@ export default {
AddLabel, AddLabel,
}, },
mixins: [conversationLabelMixin, adminMixin, keyboardEventListenerMixins], mixins: [conversationLabelMixin, keyboardEventListenerMixins],
props: { props: {
conversationId: { conversationId: {
type: Number, type: Number,
required: true, required: true,
}, },
}, },
setup() {
const { isAdmin } = useAdmin();
return {
isAdmin,
};
},
data() { data() {
return { return {
selectedLabels: [], selectedLabels: [],
@@ -9,7 +9,7 @@
/> />
<router-link <router-link
:to="searchUrl" :to="searchUrl"
class="search-link flex-1 items-center gap-1 text-left h-6 rtl:mr-3 rtl:text-right rounded-md px-2 py-0 bg-slate-25 dark:bg-slate-800 inline-flex" class="inline-flex items-center flex-1 h-6 gap-1 px-2 py-0 text-left rounded-md search-link rtl:mr-3 rtl:text-right bg-slate-25 dark:bg-slate-800"
> >
<div class="flex"> <div class="flex">
<fluent-icon <fluent-icon
@@ -19,7 +19,7 @@
/> />
</div> </div>
<p <p
class="search--label mb-0 overflow-hidden whitespace-nowrap text-ellipsis text-sm text-slate-800 dark:text-slate-200" class="mb-0 overflow-hidden text-sm search--label whitespace-nowrap text-ellipsis text-slate-800 dark:text-slate-200"
> >
{{ $t('CONVERSATION.SEARCH_MESSAGES') }} {{ $t('CONVERSATION.SEARCH_MESSAGES') }}
</p> </p>
@@ -34,7 +34,6 @@
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import timeMixin from '../../../../mixins/time';
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin'; import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
import SwitchLayout from './SwitchLayout.vue'; import SwitchLayout from './SwitchLayout.vue';
import { frontendURL } from 'dashboard/helper/URLHelper'; import { frontendURL } from 'dashboard/helper/URLHelper';
@@ -49,7 +48,7 @@ export default {
}, },
}, },
}, },
mixins: [timeMixin, messageFormatterMixin], mixins: [messageFormatterMixin],
props: { props: {
isOnExpandedLayout: { isOnExpandedLayout: {
type: Boolean, type: Boolean,
@@ -30,11 +30,10 @@
<script> <script>
import { required, minLength } from 'vuelidate/lib/validators'; import { required, minLength } from 'vuelidate/lib/validators';
import alertMixin from 'shared/mixins/alertMixin'; import { useAlert } from 'dashboard/composables';
import { CONTACTS_EVENTS } from '../../../helper/AnalyticsHelper/events'; import { CONTACTS_EVENTS } from '../../../helper/AnalyticsHelper/events';
export default { export default {
mixins: [alertMixin],
props: { props: {
filterType: { filterType: {
type: Number, type: Number,
@@ -101,7 +100,7 @@ export default {
? errorMessage ? errorMessage
: this.$t('FILTER.CUSTOM_VIEWS.ADD.API_SEGMENTS.ERROR_MESSAGE'); : this.$t('FILTER.CUSTOM_VIEWS.ADD.API_SEGMENTS.ERROR_MESSAGE');
} finally { } finally {
this.showAlert(this.alertMessage); useAlert(this.alertMessage);
} }
this.openLastSavedItem(); this.openLastSavedItem();
}, },
@@ -16,10 +16,9 @@
</template> </template>
<script> <script>
import alertMixin from 'shared/mixins/alertMixin'; import { useAlert } from 'dashboard/composables';
import { CONTACTS_EVENTS } from '../../../helper/AnalyticsHelper/events'; import { CONTACTS_EVENTS } from '../../../helper/AnalyticsHelper/events';
export default { export default {
mixins: [alertMixin],
props: { props: {
showDeletePopup: { showDeletePopup: {
type: Boolean, type: Boolean,
@@ -79,7 +78,7 @@ export default {
const filterType = this.activeCustomViews; const filterType = this.activeCustomViews;
await this.$store.dispatch('customViews/delete', { id, filterType }); await this.$store.dispatch('customViews/delete', { id, filterType });
this.closeDeletePopup(); this.closeDeletePopup();
this.showAlert( useAlert(
this.activeFilterType === 0 this.activeFilterType === 0
? this.$t('FILTER.CUSTOM_VIEWS.DELETE.API_FOLDERS.SUCCESS_MESSAGE') ? this.$t('FILTER.CUSTOM_VIEWS.DELETE.API_FOLDERS.SUCCESS_MESSAGE')
: this.$t('FILTER.CUSTOM_VIEWS.DELETE.API_SEGMENTS.SUCCESS_MESSAGE') : this.$t('FILTER.CUSTOM_VIEWS.DELETE.API_SEGMENTS.SUCCESS_MESSAGE')
@@ -94,7 +93,7 @@ export default {
: this.$t( : this.$t(
'FILTER.CUSTOM_VIEWS.DELETE.API_SEGMENTS.SUCCESS_MESSAGE' 'FILTER.CUSTOM_VIEWS.DELETE.API_SEGMENTS.SUCCESS_MESSAGE'
); );
this.showAlert(errorMessage); useAlert(errorMessage);
} }
this.openLastItemAfterDelete(); this.openLastItemAfterDelete();
}, },
@@ -24,7 +24,7 @@
</label> </label>
<div class="w-full"> <div class="w-full">
<div class="flex flex-row justify-end gap-2 py-2 px-0 w-full"> <div class="flex flex-row justify-end w-full gap-2 px-0 py-2">
<woot-button class="button clear" @click.prevent="onClose"> <woot-button class="button clear" @click.prevent="onClose">
{{ $t('HELP_CENTER.PORTAL.ADD_LOCALE.BUTTONS.CANCEL') }} {{ $t('HELP_CENTER.PORTAL.ADD_LOCALE.BUTTONS.CANCEL') }}
</woot-button> </woot-button>
@@ -39,16 +39,15 @@
</template> </template>
<script> <script>
import Modal from 'dashboard/components/Modal.vue'; import { useAlert } from 'dashboard/composables';
import alertMixin from 'shared/mixins/alertMixin';
import { required } from 'vuelidate/lib/validators'; import { required } from 'vuelidate/lib/validators';
import allLocales from 'shared/constants/locales.js'; import allLocales from 'shared/constants/locales.js';
import { PORTALS_EVENTS } from '../../../../helper/AnalyticsHelper/events'; import { PORTALS_EVENTS } from '../../../../helper/AnalyticsHelper/events';
import Modal from 'dashboard/components/Modal.vue';
export default { export default {
components: { components: {
Modal, Modal,
}, },
mixins: [alertMixin],
props: { props: {
show: { show: {
type: Boolean, type: Boolean,
@@ -120,7 +119,7 @@ export default {
error?.message || error?.message ||
this.$t('HELP_CENTER.PORTAL.ADD_LOCALE.API.ERROR_MESSAGE'); this.$t('HELP_CENTER.PORTAL.ADD_LOCALE.API.ERROR_MESSAGE');
} finally { } finally {
this.showAlert(this.alertMessage); useAlert(this.alertMessage);
this.isUpdating = false; this.isUpdating = false;
} }
}, },
@@ -1,12 +1,12 @@
<template> <template>
<div <div
class="text-slate-700 dark:text-slate-100 last:border-b-0 bg-white dark:bg-slate-900 my-0 -mx-4 grid grid-cols-1 lg:grid-cols-12 gap-4 border-b border-slate-50 dark:border-slate-800 px-6 py-3" class="grid grid-cols-1 gap-4 px-6 py-3 my-0 -mx-4 bg-white border-b text-slate-700 dark:text-slate-100 last:border-b-0 dark:bg-slate-900 lg:grid-cols-12 border-slate-50 dark:border-slate-800"
> >
<span class="items-start flex gap-2 col-span-6 text-left"> <span class="flex items-start col-span-6 gap-2 text-left">
<fluent-icon <fluent-icon
v-if="showDragIcon" v-if="showDragIcon"
size="20" size="20"
class="block cursor-move flex-shrink-0 h-4 mt-1 w-4 text-slate-200 dark:text-slate-700 hover:text-slate-400 hover:dark:text-slate-200" class="flex-shrink-0 block w-4 h-4 mt-1 cursor-move text-slate-200 dark:text-slate-700 hover:text-slate-400 hover:dark:text-slate-200"
icon="grab-handle" icon="grab-handle"
/> />
<div class="flex flex-col truncate"> <div class="flex flex-col truncate">
@@ -18,7 +18,7 @@
{{ title }} {{ title }}
</h6> </h6>
</router-link> </router-link>
<div class="flex gap-1 items-center"> <div class="flex items-center gap-1">
<Thumbnail <Thumbnail
v-if="author" v-if="author"
:src="author.thumbnail" :src="author.thumbnail"
@@ -39,7 +39,7 @@
class="text-woot-300 dark:text-woot-300" class="text-woot-300 dark:text-woot-300"
/> />
</div> </div>
<span class="font-normal text-slate-700 dark:text-slate-200 text-sm"> <span class="text-sm font-normal text-slate-700 dark:text-slate-200">
{{ articleAuthorName }} {{ articleAuthorName }}
</span> </span>
</div> </div>
@@ -60,7 +60,7 @@
:title="formattedViewCount" :title="formattedViewCount"
> >
{{ readableViewCount }} {{ readableViewCount }}
<span class="lg:hidden ml-1"> <span class="ml-1 lg:hidden">
{{ ` ${$t('HELP_CENTER.TABLE.HEADERS.READ_COUNT')}` }} {{ ` ${$t('HELP_CENTER.TABLE.HEADERS.READ_COUNT')}` }}
</span> </span>
</span> </span>
@@ -74,7 +74,7 @@
/> />
</span> </span>
<span <span
class="flex items-center justify-end col-span-2 first-letter:uppercase text-slate-700 dark:text-slate-100 text-xs" class="flex items-center justify-end col-span-2 text-xs first-letter:uppercase text-slate-700 dark:text-slate-100"
> >
{{ lastUpdatedAt }} {{ lastUpdatedAt }}
</span> </span>
@@ -82,7 +82,7 @@
</template> </template>
<script> <script>
import timeMixin from 'dashboard/mixins/time'; import { dynamicTime } from 'shared/helpers/timeHelper';
import portalMixin from '../mixins/portalMixin'; import portalMixin from '../mixins/portalMixin';
import { frontendURL } from 'dashboard/helper/URLHelper'; import { frontendURL } from 'dashboard/helper/URLHelper';
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue'; import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
@@ -91,7 +91,7 @@ export default {
components: { components: {
Thumbnail, Thumbnail,
}, },
mixins: [timeMixin, portalMixin], mixins: [portalMixin],
props: { props: {
showDragIcon: { showDragIcon: {
type: Boolean, type: Boolean,
@@ -131,7 +131,7 @@ export default {
computed: { computed: {
lastUpdatedAt() { lastUpdatedAt() {
return this.dynamicTime(this.updatedAt); return dynamicTime(this.updatedAt);
}, },
formattedViewCount() { formattedViewCount() {
return Number(this.views || 0).toLocaleString('en'); return Number(this.views || 0).toLocaleString('en');
@@ -1,17 +1,17 @@
<template> <template>
<button <button
class="flex flex-col gap-1 bg-white dark:bg-slate-900 hover:bg-slate-25 hover:dark:bg-slate-800 rounded-md py-1 px-2 w-full group border border-transparent border-solid focus:outline-none focus:bg-slate-25 focus:border-slate-500 dark:focus:border-slate-400 dark:focus:bg-slate-800 cursor-pointer" class="flex flex-col w-full gap-1 px-2 py-1 bg-white border border-transparent border-solid rounded-md cursor-pointer dark:bg-slate-900 hover:bg-slate-25 hover:dark:bg-slate-800 group focus:outline-none focus:bg-slate-25 focus:border-slate-500 dark:focus:border-slate-400 dark:focus:bg-slate-800"
@click="handlePreview" @click="handlePreview"
> >
<h4 <h4
class="text-sm ltr:text-left rtl:text-right w-full mb-0 text-slate-900 dark:text-slate-25 -mx-1 rounded-sm hover:underline group-hover:underline" class="w-full mb-0 -mx-1 text-sm rounded-sm ltr:text-left rtl:text-right text-slate-900 dark:text-slate-25 hover:underline group-hover:underline"
> >
{{ title }} {{ title }}
</h4> </h4>
<div class="flex content-between items-center gap-0.5 w-full"> <div class="flex content-between items-center gap-0.5 w-full">
<p <p
class="text-sm ltr:text-left rtl:text-right text-slate-600 dark:text-slate-300 mb-0 w-full" class="w-full mb-0 text-sm ltr:text-left rtl:text-right text-slate-600 dark:text-slate-300"
> >
{{ locale }} {{ locale }}
{{ ` / ` }} {{ ` / ` }}
@@ -42,12 +42,11 @@
</template> </template>
<script> <script>
import { useAlert } from 'dashboard/composables';
import { copyTextToClipboard } from 'shared/helpers/clipboard'; import { copyTextToClipboard } from 'shared/helpers/clipboard';
import alertMixin from 'shared/mixins/alertMixin';
export default { export default {
name: 'ArticleSearchResultItem', name: 'ArticleSearchResultItem',
mixins: [alertMixin],
props: { props: {
id: { id: {
type: Number, type: Number,
@@ -86,7 +85,7 @@ export default {
async handleCopy(e) { async handleCopy(e) {
e.stopPropagation(); e.stopPropagation();
await copyTextToClipboard(this.url); await copyTextToClipboard(this.url);
this.showAlert(this.$t('CONTACT_PANEL.COPY_SUCCESSFUL')); useAlert(this.$t('CONTACT_PANEL.COPY_SUCCESSFUL'));
}, },
}, },
}; };
@@ -1,6 +1,6 @@
<template> <template>
<div <div
class="fixed flex items-center justify-center w-screen h-screen bg-modal-backdrop-light dark:bg-modal-backdrop-dark top-0 left-0 z-50" class="fixed top-0 left-0 z-50 flex items-center justify-center w-screen h-screen bg-modal-backdrop-light dark:bg-modal-backdrop-dark"
> >
<div <div
v-on-clickaway="onClose" v-on-clickaway="onClose"
@@ -34,6 +34,7 @@
<script> <script>
import { debounce } from '@chatwoot/utils'; import { debounce } from '@chatwoot/utils';
import { useAlert } from 'dashboard/composables';
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins'; import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
import SearchHeader from './Header.vue'; import SearchHeader from './Header.vue';
@@ -42,7 +43,6 @@ import ArticleView from './ArticleView.vue';
import ArticlesAPI from 'dashboard/api/helpCenter/articles'; import ArticlesAPI from 'dashboard/api/helpCenter/articles';
import { buildPortalArticleURL } from 'dashboard/helper/portalHelper'; import { buildPortalArticleURL } from 'dashboard/helper/portalHelper';
import portalMixin from '../../mixins/portalMixin'; import portalMixin from '../../mixins/portalMixin';
import alertMixin from 'shared/mixins/alertMixin';
export default { export default {
name: 'ArticleSearchPopover', name: 'ArticleSearchPopover',
@@ -51,7 +51,7 @@ export default {
SearchResults, SearchResults,
ArticleView, ArticleView,
}, },
mixins: [portalMixin, alertMixin, keyboardEventListenerMixins], mixins: [portalMixin, keyboardEventListenerMixins],
props: { props: {
selectedPortalSlug: { selectedPortalSlug: {
type: String, type: String,
@@ -145,9 +145,7 @@ export default {
const article = this.activeArticle(id || this.activeId); const article = this.activeArticle(id || this.activeId);
this.$emit('insert', article); this.$emit('insert', article);
this.showAlert( useAlert(this.$t('HELP_CENTER.ARTICLE_SEARCH.SUCCESS_ARTICLE_INSERTED'));
this.$t('HELP_CENTER.ARTICLE_SEARCH.SUCCESS_ARTICLE_INSERTED')
);
this.onClose(); this.onClose();
}, },
getKeyboardEvents() { getKeyboardEvents() {
@@ -108,14 +108,13 @@
</template> </template>
<script> <script>
import alertMixin from 'shared/mixins/alertMixin'; import { useAlert } from 'dashboard/composables';
import wootConstants from 'dashboard/constants/globals'; import wootConstants from 'dashboard/constants/globals';
import { PORTALS_EVENTS } from '../../../../../helper/AnalyticsHelper/events'; import { PORTALS_EVENTS } from '../../../../../helper/AnalyticsHelper/events';
const { ARTICLE_STATUS_TYPES } = wootConstants; const { ARTICLE_STATUS_TYPES } = wootConstants;
export default { export default {
mixins: [alertMixin],
props: { props: {
isSidebarOpen: { isSidebarOpen: {
type: Boolean, type: Boolean,
@@ -200,7 +199,7 @@ export default {
this.alertMessage = this.alertMessage =
error?.message || this.statusUpdateErrorMessage(status); error?.message || this.statusUpdateErrorMessage(status);
} finally { } finally {
this.showAlert(this.alertMessage); useAlert(this.alertMessage);
} }
}, },
statusUpdateSuccessMessage(status) { statusUpdateSuccessMessage(status) {

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