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
248 changed files with 2502 additions and 2184 deletions
+10 -9
View File
@@ -103,8 +103,8 @@ GEM
tzinfo (~> 2.0)
acts-as-taggable-on (9.0.1)
activerecord (>= 6.0, < 7.1)
addressable (2.8.4)
public_suffix (>= 2.0.2, < 6.0)
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
administrate (0.20.1)
actionpack (>= 6.0, < 8.0)
actionview (>= 6.0, < 8.0)
@@ -171,7 +171,8 @@ GEM
commonmarker (0.23.10)
concurrent-ruby (1.3.3)
connection_pool (2.4.1)
crack (0.4.5)
crack (1.0.0)
bigdecimal
rexml
crass (1.0.6)
csv (3.3.0)
@@ -358,7 +359,7 @@ GEM
ruby2ruby (~> 2.4)
ruby_parser (~> 3.10)
hana (1.3.7)
hashdiff (1.0.1)
hashdiff (1.1.0)
hashie (5.0.0)
http (5.1.1)
addressable (~> 2.8)
@@ -550,7 +551,7 @@ GEM
method_source (~> 1.0)
pry-rails (0.3.9)
pry (>= 0.10.4)
public_suffix (5.0.1)
public_suffix (6.0.0)
puma (6.4.2)
nio4r (~> 2.0)
pundit (2.3.0)
@@ -633,8 +634,8 @@ GEM
retriable (3.1.2)
reverse_markdown (2.1.1)
nokogiri
rexml (3.2.8)
strscan (>= 3.0.9)
rexml (3.3.2)
strscan
rspec-core (3.13.0)
rspec-support (~> 3.13.0)
rspec-expectations (3.13.1)
@@ -809,7 +810,7 @@ GEM
web-push (3.0.1)
jwt (~> 2.0)
openssl (~> 3.0)
webmock (3.18.1)
webmock (3.23.1)
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
@@ -970,4 +971,4 @@ RUBY VERSION
ruby 3.3.3p89
BUNDLED WITH
2.5.14
2.5.16
@@ -116,13 +116,14 @@
<script>
import { mapGetters } from 'vuex';
import { useUISettings } from 'dashboard/composables/useUISettings';
import { useAlert } from 'dashboard/composables';
import VirtualList from 'vue-virtual-scroll-list';
import ChatListHeader from './ChatListHeader.vue';
import ConversationAdvancedFilter from './widgets/conversation/ConversationAdvancedFilter.vue';
import ChatTypeTabs from './widgets/ChatTypeTabs.vue';
import ConversationItem from './ConversationItem.vue';
import timeMixin from '../mixins/time';
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
import conversationMixin from '../mixins/conversations';
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 DeleteCustomViews from 'dashboard/routes/dashboard/customviews/DeleteCustomViews.vue';
import ConversationBulkActions from './widgets/conversation/conversationBulkActions/Index.vue';
import alertMixin from 'shared/mixins/alertMixin';
import filterMixin from 'shared/mixins/filterMixin';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import languages from 'dashboard/components/widgets/conversation/advancedFilterItems/languages';
import countries from 'shared/constants/countries';
import { generateValuesForEditCustomViews } from 'dashboard/helper/customViewsHelper';
@@ -158,14 +157,7 @@ export default {
IntersectionObserver,
VirtualList,
},
mixins: [
timeMixin,
conversationMixin,
keyboardEventListenerMixins,
alertMixin,
filterMixin,
uiSettingsMixin,
],
mixins: [conversationMixin, keyboardEventListenerMixins, filterMixin],
provide() {
return {
// Actions to be performed on virtual list item and context menu.
@@ -210,6 +202,13 @@ export default {
type: Boolean,
},
},
setup() {
const { uiSettings } = useUISettings();
return {
uiSettings,
};
},
data() {
return {
activeAssigneeTab: wootConstants.ASSIGNEE_TYPE.ME,
@@ -812,7 +811,7 @@ export default {
});
this.$store.dispatch('bulkActions/clearSelectedConversationIds');
if (conversationId) {
this.showAlert(
useAlert(
this.$t(
'CONVERSATION.CARD_CONTEXT_MENU.API.AGENT_ASSIGNMENT.SUCCESFUL',
{
@@ -822,10 +821,10 @@ export default {
)
);
} else {
this.showAlert(this.$t('BULK_ACTION.ASSIGN_SUCCESFUL'));
useAlert(this.$t('BULK_ACTION.ASSIGN_SUCCESFUL'));
}
} catch (err) {
this.showAlert(this.$t('BULK_ACTION.ASSIGN_FAILED'));
useAlert(this.$t('BULK_ACTION.ASSIGN_FAILED'));
}
},
async assignPriority(priority, conversationId = null) {
@@ -840,7 +839,7 @@ export default {
newValue: priority,
from: 'Context menu',
});
this.showAlert(
useAlert(
this.$t('CONVERSATION.PRIORITY.CHANGE_PRIORITY.SUCCESSFUL', {
priority,
conversationId,
@@ -883,7 +882,7 @@ export default {
conversationId,
teamId: team.id,
});
this.showAlert(
useAlert(
this.$t(
'CONVERSATION.CARD_CONTEXT_MENU.API.TEAM_ASSIGNMENT.SUCCESFUL',
{
@@ -893,7 +892,7 @@ export default {
)
);
} catch (error) {
this.showAlert(
useAlert(
this.$t('CONVERSATION.CARD_CONTEXT_MENU.API.TEAM_ASSIGNMENT.FAILED')
);
}
@@ -910,7 +909,7 @@ export default {
});
this.$store.dispatch('bulkActions/clearSelectedConversationIds');
if (conversationId) {
this.showAlert(
useAlert(
this.$t(
'CONVERSATION.CARD_CONTEXT_MENU.API.LABEL_ASSIGNMENT.SUCCESFUL',
{
@@ -920,10 +919,10 @@ export default {
)
);
} else {
this.showAlert(this.$t('BULK_ACTION.LABELS.ASSIGN_SUCCESFUL'));
useAlert(this.$t('BULK_ACTION.LABELS.ASSIGN_SUCCESFUL'));
}
} catch (err) {
this.showAlert(this.$t('BULK_ACTION.LABELS.ASSIGN_FAILED'));
useAlert(this.$t('BULK_ACTION.LABELS.ASSIGN_FAILED'));
}
},
async onAssignTeamsForBulk(team) {
@@ -936,9 +935,9 @@ export default {
},
});
this.$store.dispatch('bulkActions/clearSelectedConversationIds');
this.showAlert(this.$t('BULK_ACTION.TEAMS.ASSIGN_SUCCESFUL'));
useAlert(this.$t('BULK_ACTION.TEAMS.ASSIGN_SUCCESFUL'));
} catch (err) {
this.showAlert(this.$t('BULK_ACTION.TEAMS.ASSIGN_FAILED'));
useAlert(this.$t('BULK_ACTION.TEAMS.ASSIGN_FAILED'));
}
},
async onUpdateConversations(status, snoozedUntil) {
@@ -952,9 +951,9 @@ export default {
snoozed_until: snoozedUntil,
});
this.$store.dispatch('bulkActions/clearSelectedConversationIds');
this.showAlert(this.$t('BULK_ACTION.UPDATE.UPDATE_SUCCESFUL'));
useAlert(this.$t('BULK_ACTION.UPDATE.UPDATE_SUCCESFUL'));
} catch (err) {
this.showAlert(this.$t('BULK_ACTION.UPDATE.UPDATE_FAILED'));
useAlert(this.$t('BULK_ACTION.UPDATE.UPDATE_FAILED'));
}
},
toggleConversationStatus(conversationId, status, snoozedUntil) {
@@ -965,7 +964,7 @@ export default {
snoozedUntil,
})
.then(() => {
this.showAlert(this.$t('CONVERSATION.CHANGE_STATUS'));
useAlert(this.$t('CONVERSATION.CHANGE_STATUS'));
this.isLoading = false;
});
},
+2 -3
View File
@@ -25,10 +25,9 @@
<script>
import 'highlight.js/styles/default.css';
import { copyTextToClipboard } from 'shared/helpers/clipboard';
import alertMixin from 'shared/mixins/alertMixin';
import { useAlert } from 'dashboard/composables';
export default {
mixins: [alertMixin],
props: {
script: {
type: String,
@@ -61,7 +60,7 @@ export default {
async onCopy(e) {
e.preventDefault();
await copyTextToClipboard(this.script);
this.showAlert(this.$t('COMPONENTS.CODE.COPY_SUCCESSFUL'));
useAlert(this.$t('COMPONENTS.CODE.COPY_SUCCESSFUL'));
},
},
};
@@ -18,10 +18,9 @@
<script>
import 'highlight.js/styles/default.css';
import { copyTextToClipboard } from 'shared/helpers/clipboard';
import alertMixin from 'shared/mixins/alertMixin';
import { useAlert } from 'dashboard/composables';
export default {
mixins: [alertMixin],
props: {
value: {
type: String,
@@ -37,7 +36,7 @@ export default {
async onCopy(e) {
e.preventDefault();
await copyTextToClipboard(this.value);
this.showAlert(this.$t('COMPONENTS.CODE.COPY_SUCCESSFUL'));
useAlert(this.$t('COMPONENTS.CODE.COPY_SUCCESSFUL'));
},
toggleMasked() {
this.masked = !this.masked;
@@ -15,13 +15,11 @@
<script>
import WootSnackbar from './Snackbar.vue';
import alertMixin from 'shared/mixins/alertMixin';
export default {
components: {
WootSnackbar,
},
mixins: [alertMixin],
props: {
duration: {
type: Number,
@@ -42,7 +40,7 @@ export default {
this.$emitter.off('newToastMessage', this.onNewToastMessage);
},
methods: {
onNewToastMessage(message, action) {
onNewToastMessage({ message, action }) {
this.snackMessages.push({
key: new Date().getTime(),
message,
@@ -10,9 +10,9 @@
</template>
<script>
import Banner from 'dashboard/components/ui/Banner.vue';
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';
const EMPTY_SUBSCRIPTION_INFO = {
@@ -22,7 +22,13 @@ const EMPTY_SUBSCRIPTION_INFO = {
export default {
components: { Banner },
mixins: [adminMixin, accountMixin],
mixins: [accountMixin],
setup() {
const { isAdmin } = useAdmin();
return {
isAdmin,
};
},
computed: {
...mapGetters({
isOnChatwootCloud: 'globalConfig/isOnChatwootCloud',
@@ -14,11 +14,11 @@
import Banner from 'dashboard/components/ui/Banner.vue';
import { mapGetters } from 'vuex';
import accountMixin from 'dashboard/mixins/account';
import alertMixin from 'shared/mixins/alertMixin';
import { useAlert } from 'dashboard/composables';
export default {
components: { Banner },
mixins: [accountMixin, alertMixin],
mixins: [accountMixin],
computed: {
...mapGetters({
currentUser: 'getCurrentUser',
@@ -36,7 +36,7 @@ export default {
methods: {
resendVerificationEmail() {
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 { LocalStorage } from 'shared/helpers/localStorage';
import { mapGetters } from 'vuex';
import adminMixin from 'dashboard/mixins/isAdmin';
import { useAdmin } from 'dashboard/composables/useAdmin';
import { hasAnUpdateAvailable } from './versionCheckHelper';
export default {
components: { Banner },
mixins: [adminMixin],
props: {
latestChatwootVersion: { type: String, default: '' },
},
setup() {
const { isAdmin } = useAdmin();
return {
isAdmin,
};
},
data() {
return { userDismissedBanner: false };
},
@@ -12,13 +12,12 @@
<script>
import Banner from 'dashboard/components/ui/Banner.vue';
import { mapGetters } from 'vuex';
import adminMixin from 'dashboard/mixins/isAdmin';
import accountMixin from 'dashboard/mixins/account';
import { differenceInDays } from 'date-fns';
export default {
components: { Banner },
mixins: [adminMixin, accountMixin],
mixins: [accountMixin],
data() {
return { conversationMeta: {} };
},
@@ -78,7 +78,7 @@
<script>
import { mapGetters } from 'vuex';
import alertMixin from 'shared/mixins/alertMixin';
import { useAlert } from 'dashboard/composables';
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
@@ -94,7 +94,7 @@ export default {
WootDropdownItem,
WootDropdownMenu,
},
mixins: [alertMixin, keyboardEventListenerMixins],
mixins: [keyboardEventListenerMixins],
props: { conversationId: { type: [String, Number], required: true } },
data() {
return {
@@ -209,7 +209,7 @@ export default {
snoozedUntil,
})
.then(() => {
this.showAlert(this.$t('CONVERSATION.CHANGE_STATUS'));
useAlert(this.$t('CONVERSATION.CHANGE_STATUS'));
this.isLoading = false;
});
},
@@ -18,7 +18,7 @@
</woot-button>
</woot-dropdown-item>
<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">
<fluent-icon
v-tooltip.right-start="$t('SIDEBAR.SET_AUTO_OFFLINE.INFO_TEXT')"
@@ -28,7 +28,7 @@
/>
<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') }}
</span>
@@ -36,7 +36,7 @@
<woot-switch
size="small"
class="mt-px mx-1 mb-0"
class="mx-1 mt-px mb-0"
:value="currentUserAutoOffline"
@input="updateAutoOffline"
/>
@@ -47,7 +47,7 @@
<script>
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 WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
import WootDropdownHeader from 'shared/components/ui/dropdown/DropdownHeader.vue';
@@ -65,9 +65,6 @@ export default {
WootDropdownItem,
AvailabilityStatusBadge,
},
mixins: [alertMixin],
data() {
return {
isStatusMenuOpened: false,
@@ -129,7 +126,7 @@ export default {
account_id: this.currentAccountId,
});
} catch (error) {
this.showAlert(
useAlert(
this.$t('PROFILE_SETTINGS.FORM.AVAILABILITY.SET_AVAILABILITY_ERROR')
);
} finally {
@@ -30,9 +30,7 @@
<script>
import { mapGetters } from 'vuex';
import adminMixin from '../../mixins/isAdmin';
import { getSidebarItems } from './config/default-sidebar';
import alertMixin from 'shared/mixins/alertMixin';
import PrimarySidebar from './sidebarComponents/Primary.vue';
import SecondarySidebar from './sidebarComponents/Secondary.vue';
@@ -45,7 +43,7 @@ export default {
PrimarySidebar,
SecondarySidebar,
},
mixins: [adminMixin, alertMixin, keyboardEventListenerMixins],
mixins: [keyboardEventListenerMixins],
props: {
showSecondarySidebar: {
type: Boolean,
@@ -51,10 +51,9 @@
<script>
import { required, minLength } from 'vuelidate/lib/validators';
import { mapGetters } from 'vuex';
import alertMixin from 'shared/mixins/alertMixin';
import { useAlert } from 'dashboard/composables';
export default {
mixins: [alertMixin],
props: {
show: {
type: Boolean,
@@ -88,13 +87,13 @@ export default {
account_name: this.accountName,
});
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`;
} catch (error) {
if (error.response.status === 422) {
this.showAlert(this.$t('CREATE_ACCOUNT.API.EXIST_MESSAGE'));
useAlert(this.$t('CREATE_ACCOUNT.API.EXIST_MESSAGE'));
} else {
this.showAlert(this.$t('CREATE_ACCOUNT.API.ERROR_MESSAGE'));
useAlert(this.$t('CREATE_ACCOUNT.API.ERROR_MESSAGE'));
}
}
},
@@ -94,8 +94,7 @@
<script>
import { mapGetters } from 'vuex';
import adminMixin from '../../../mixins/isAdmin';
import { useAdmin } from 'dashboard/composables/useAdmin';
import configMixin from 'shared/mixins/configMixin';
import {
getInboxClassByType,
@@ -111,13 +110,19 @@ import Policy from '../../policy.vue';
export default {
components: { SecondaryChildNavItem, Policy },
mixins: [adminMixin, configMixin],
mixins: [configMixin],
props: {
menuItem: {
type: Object,
default: () => ({}),
},
},
setup() {
const { isAdmin } = useAdmin();
return {
isAdmin,
};
},
computed: {
...mapGetters({
activeInbox: 'getSelectedInbox',
@@ -5,7 +5,7 @@
delay: { show: 1500, hide: 0 },
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>
</div>
@@ -16,11 +16,14 @@ const MINUTE_IN_MILLI_SECONDS = 60000;
const HOUR_IN_MILLI_SECONDS = MINUTE_IN_MILLI_SECONDS * 60;
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 {
name: 'TimeAgo',
mixins: [timeMixin],
props: {
isAutoRefreshEnabled: {
type: Boolean,
@@ -37,17 +40,17 @@ export default {
},
data() {
return {
lastActivityAtTimeAgo: this.dynamicTime(this.lastActivityTimestamp),
createdAtTimeAgo: this.dynamicTime(this.createdAtTimestamp),
lastActivityAtTimeAgo: dynamicTime(this.lastActivityTimestamp),
createdAtTimeAgo: dynamicTime(this.createdAtTimestamp),
timer: null,
};
},
computed: {
lastActivityTime() {
return this.shortTimestamp(this.lastActivityAtTimeAgo);
return shortTimestamp(this.lastActivityAtTimeAgo);
},
createdAtTime() {
return this.shortTimestamp(this.createdAtTimeAgo);
return shortTimestamp(this.createdAtTimeAgo);
},
createdAt() {
const createdTimeDiff = Date.now() - this.createdAtTimestamp * 1000;
@@ -56,9 +59,9 @@ export default {
? `${this.$t('CHAT_LIST.CHAT_TIME_STAMP.CREATED.LATEST')} ${
this.createdAtTimeAgo
}`
: `${this.$t(
'CHAT_LIST.CHAT_TIME_STAMP.CREATED.OLDEST'
)} ${this.dateFormat(this.createdAtTimestamp)}`;
: `${this.$t('CHAT_LIST.CHAT_TIME_STAMP.CREATED.OLDEST')} ${dateFormat(
this.createdAtTimestamp
)}`;
},
lastActivity() {
const lastActivityTimeDiff =
@@ -70,7 +73,7 @@ export default {
}`
: `${this.$t(
'CHAT_LIST.CHAT_TIME_STAMP.LAST_ACTIVITY.NOT_ACTIVE'
)} ${this.dateFormat(this.lastActivityTimestamp)}`;
)} ${dateFormat(this.lastActivityTimestamp)}`;
},
tooltipText() {
return `${this.createdAt}
@@ -79,10 +82,10 @@ export default {
},
watch: {
lastActivityTimestamp() {
this.lastActivityAtTimeAgo = this.dynamicTime(this.lastActivityTimestamp);
this.lastActivityAtTimeAgo = dynamicTime(this.lastActivityTimestamp);
},
createdAtTimestamp() {
this.createdAtTimeAgo = this.dynamicTime(this.createdAtTimestamp);
this.createdAtTimeAgo = dynamicTime(this.createdAtTimestamp);
},
},
mounted() {
@@ -96,10 +99,8 @@ export default {
methods: {
createTimer() {
this.timer = setTimeout(() => {
this.lastActivityAtTimeAgo = this.dynamicTime(
this.lastActivityTimestamp
);
this.createdAtTimeAgo = this.dynamicTime(this.createdAtTimestamp);
this.lastActivityAtTimeAgo = dynamicTime(this.lastActivityTimestamp);
this.createdAtTimeAgo = dynamicTime(this.createdAtTimestamp);
this.createTimer();
}, this.refreshTime());
},
@@ -35,13 +35,13 @@
</template>
<script>
import { mapGetters } from 'vuex';
import { useAdmin } from 'dashboard/composables/useAdmin';
import { useUISettings } from 'dashboard/composables/useUISettings';
import AICTAModal from './AICTAModal.vue';
import AIAssistanceModal from './AIAssistanceModal.vue';
import adminMixin from 'dashboard/mixins/aiMixin';
import aiMixin from 'dashboard/mixins/isAdmin';
import aiMixin from 'dashboard/mixins/aiMixin';
import { CMD_AI_ASSIST } from 'dashboard/routes/dashboard/commands/commandBarBusEvents';
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import AIAssistanceCTAButton from './AIAssistanceCTAButton.vue';
export default {
@@ -50,7 +50,17 @@ export default {
AICTAModal,
AIAssistanceCTAButton,
},
mixins: [aiMixin, keyboardEventListenerMixins, adminMixin, uiSettingsMixin],
mixins: [aiMixin, keyboardEventListenerMixins],
setup() {
const { uiSettings, updateUISettings } = useUISettings();
const { isAdmin } = useAdmin();
return {
uiSettings,
updateUISettings,
isAdmin,
};
},
data: () => ({
showAIAssistanceModal: false,
showAICtaModal: false,
@@ -1,14 +1,14 @@
<template>
<div class="px-0 min-w-0 flex-1">
<div class="flex-1 min-w-0 px-0">
<woot-modal-header
:header-title="$t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.TITLE')"
:header-content="$t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.DESC')"
/>
<form
class="flex flex-wrap flex-col modal-content"
class="flex flex-col flex-wrap modal-content"
@submit.prevent="finishOpenAI"
>
<div class="mt-2 w-full">
<div class="w-full mt-2">
<woot-input
v-model="value"
type="text"
@@ -19,7 +19,7 @@
@blur="$v.value.$touch"
/>
</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">
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.BUTTONS.NEED_HELP') }}
</woot-button>
@@ -39,13 +39,20 @@
<script>
import { required } from 'vuelidate/lib/validators';
import { mapGetters } from 'vuex';
import { useAlert } from 'dashboard/composables';
import { useUISettings } from 'dashboard/composables/useUISettings';
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';
export default {
mixins: [aiMixin, alertMixin, uiSettingsMixin],
mixins: [aiMixin],
setup() {
const { updateUISettings } = useUISettings();
return {
updateUISettings,
};
},
data() {
return {
value: '',
@@ -67,7 +74,7 @@ export default {
},
onDismiss() {
this.showAlert(
useAlert(
this.$t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.DISMISS_MESSAGE')
);
this.updateUISettings({
@@ -97,7 +104,7 @@ export default {
this.alertMessage =
errorMessage || this.$t('INTEGRATION_APPS.ADD.API.ERROR_MESSAGE');
} finally {
this.showAlert(this.alertMessage);
useAlert(this.alertMessage);
}
},
openOpenAIDoc() {
@@ -26,13 +26,12 @@
</template>
<script>
import { useAlert } from 'dashboard/composables';
import Spinner from 'shared/components/Spinner.vue';
import alertMixin from 'shared/mixins/alertMixin';
export default {
components: {
Spinner,
},
mixins: [alertMixin],
props: {
value: {
type: Array,
@@ -71,7 +70,7 @@ export default {
} catch (error) {
this.uploadState = '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 keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
import LabelDropdown from 'shared/components/ui/label/LabelDropdown.vue';
import adminMixin from 'dashboard/mixins/isAdmin';
import { useAdmin } from 'dashboard/composables/useAdmin';
export default {
components: {
@@ -41,7 +41,7 @@ export default {
LabelDropdown,
},
mixins: [adminMixin, keyboardEventListenerMixins],
mixins: [keyboardEventListenerMixins],
props: {
allLabels: {
@@ -54,6 +54,13 @@ export default {
},
},
setup() {
const { isAdmin } = useAdmin();
return {
isAdmin,
};
},
data() {
return {
showSearchDropdownLabel: false,
@@ -15,10 +15,9 @@
<script>
import { mapGetters } from 'vuex';
import DyteAPI from 'dashboard/api/integrations/dyte';
import alertMixin from 'shared/mixins/alertMixin';
import { useAlert } from 'dashboard/composables';
export default {
mixins: [alertMixin],
props: {
conversationId: {
type: Number,
@@ -47,7 +46,7 @@ export default {
try {
await DyteAPI.createAMeeting(this.conversationId);
} catch (error) {
this.showAlert(this.$t('INTEGRATION_SETTINGS.DYTE.CREATE_ERROR'));
useAlert(this.$t('INTEGRATION_SETTINGS.DYTE.CREATE_ERROR'));
} finally {
this.isLoading = false;
}
@@ -11,7 +11,7 @@ import 'videojs-record/dist/css/videojs.record.css';
import videojs from 'video.js';
import alertMixin from '../../../../shared/mixins/alertMixin';
import { useAlert } from 'dashboard/composables';
import Recorder from 'opus-recorder';
@@ -50,7 +50,6 @@ const RECORDER_CONFIG = {
export default {
name: 'WootAudioRecorder',
mixins: [alertMixin],
props: {
audioRecordFormat: {
type: String,
@@ -188,14 +187,10 @@ export default {
deviceErrorName?.includes('notallowederror') ||
deviceErrorName?.includes('permissiondeniederror')
) {
this.showAlert(
this.$t('CONVERSATION.REPLYBOX.TIP_AUDIORECORDER_PERMISSION')
);
useAlert(this.$t('CONVERSATION.REPLYBOX.TIP_AUDIORECORDER_PERMISSION'));
this.fireStateRecorderChanged('notallowederror');
} else {
this.showAlert(
this.$t('CONVERSATION.REPLYBOX.TIP_AUDIORECORDER_ERROR')
);
useAlert(this.$t('CONVERSATION.REPLYBOX.TIP_AUDIORECORDER_ERROR'));
}
},
formatTimeProgress() {
@@ -80,8 +80,7 @@ import {
hasPressedCommandAndEnter,
} from 'shared/helpers/KeyboardHelpers';
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import { isEditorHotKeyEnabled } from 'dashboard/mixins/uiSettings';
import { useUISettings } from 'dashboard/composables/useUISettings';
import {
replaceVariablesInMessage,
createTypingIndicator,
@@ -89,7 +88,7 @@ import {
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
import { uploadFile } from 'dashboard/helper/uploadHelper';
import alertMixin from 'shared/mixins/alertMixin';
import { useAlert } from 'dashboard/composables';
import {
MESSAGE_EDITOR_MENU_OPTIONS,
MESSAGE_EDITOR_IMAGE_RESIZES,
@@ -119,7 +118,7 @@ const createState = (
export default {
name: 'WootMessageEditor',
components: { TagAgents, CannedResponse, VariableList },
mixins: [keyboardEventListenerMixins, uiSettingsMixin, alertMixin],
mixins: [keyboardEventListenerMixins],
props: {
value: { type: String, default: '' },
editorId: { type: String, default: '' },
@@ -139,6 +138,19 @@ export default {
channelType: { type: String, default: '' },
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() {
return {
typingIndicator: createTypingIndicator(
@@ -278,7 +290,7 @@ export default {
// this is considered the source of truth, we watch this property
// on change, we toggle the signature in the editor
if (this.allowSignature && !this.isPrivate && this.channelType) {
return this.fetchSignatureFlagFromUiSettings(this.channelType);
return this.fetchSignatureFlagFromUISettings(this.channelType);
}
return false;
@@ -521,10 +533,10 @@ export default {
}
},
isEnterToSendEnabled() {
return isEditorHotKeyEnabled(this.uiSettings, 'enter');
return this.isEditorHotKeyEnabled('enter');
},
isCmdPlusEnterToSendEnabled() {
return isEditorHotKeyEnabled(this.uiSettings, 'cmd_enter');
return this.isEditorHotKeyEnabled('cmd_enter');
},
getKeyboardEvents() {
return {
@@ -611,7 +623,7 @@ export default {
if (checkFileSizeLimit(file, MAXIMUM_FILE_UPLOAD_SIZE)) {
this.uploadImageToStorage(file);
} else {
this.showAlert(
useAlert(
this.$t(
'PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.IMAGE_UPLOAD_SIZE_ERROR',
{
@@ -629,13 +641,13 @@ export default {
if (fileUrl) {
this.onImageInsertInEditor(fileUrl);
}
this.showAlert(
useAlert(
this.$t(
'PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.IMAGE_UPLOAD_SUCCESS'
)
);
} catch (error) {
this.showAlert(
useAlert(
this.$t(
'PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.IMAGE_UPLOAD_ERROR'
)
@@ -24,9 +24,9 @@ import {
Selection,
} from '@chatwoot/prosemirror-schema';
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 uiSettingsMixin from 'dashboard/mixins/uiSettings';
const MAXIMUM_FILE_UPLOAD_SIZE = 4; // in MB
const createState = (
@@ -51,13 +51,21 @@ const createState = (
};
export default {
mixins: [keyboardEventListenerMixins, uiSettingsMixin, alertMixin],
mixins: [keyboardEventListenerMixins],
props: {
value: { type: String, default: '' },
editorId: { type: String, default: '' },
placeholder: { type: String, default: '' },
enabledMenuOptions: { type: Array, default: () => [] },
},
setup() {
const { uiSettings, updateUISettings } = useUISettings();
return {
uiSettings,
updateUISettings,
};
},
data() {
return {
editorView: null,
@@ -108,7 +116,7 @@ export default {
if (checkFileSizeLimit(file, MAXIMUM_FILE_UPLOAD_SIZE)) {
this.uploadImageToStorage(file);
} else {
this.showAlert(
useAlert(
this.$t('HELP_CENTER.ARTICLE_EDITOR.IMAGE_UPLOAD.ERROR_FILE_SIZE', {
size: MAXIMUM_FILE_UPLOAD_SIZE,
})
@@ -127,13 +135,9 @@ export default {
if (fileUrl) {
this.onImageUploadStart(fileUrl);
}
this.showAlert(
this.$t('HELP_CENTER.ARTICLE_EDITOR.IMAGE_UPLOAD.SUCCESS')
);
useAlert(this.$t('HELP_CENTER.ARTICLE_EDITOR.IMAGE_UPLOAD.SUCCESS'));
} catch (error) {
this.showAlert(
this.$t('HELP_CENTER.ARTICLE_EDITOR.IMAGE_UPLOAD.ERROR')
);
useAlert(this.$t('HELP_CENTER.ARTICLE_EDITOR.IMAGE_UPLOAD.ERROR'));
}
},
onImageUploadStart(fileUrl) {
@@ -134,10 +134,10 @@
</template>
<script>
import { useUISettings } from 'dashboard/composables/useUISettings';
import FileUpload from 'vue-upload-component';
import * as ActiveStorage from 'activestorage';
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import inboxMixin from 'shared/mixins/inboxMixin';
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
import {
@@ -153,7 +153,7 @@ import { mapGetters } from 'vuex';
export default {
name: 'ReplyBottomPanel',
components: { FileUpload, VideoCallButton, AIAssistanceButton },
mixins: [keyboardEventListenerMixins, uiSettingsMixin, inboxMixin],
mixins: [keyboardEventListenerMixins, inboxMixin],
props: {
mode: {
type: String,
@@ -248,6 +248,15 @@ export default {
required: true,
},
},
setup() {
const { setSignatureFlagForInbox, fetchSignatureFlagFromUISettings } =
useUISettings();
return {
setSignatureFlagForInbox,
fetchSignatureFlagFromUISettings,
};
},
computed: {
...mapGetters({
accountId: 'getCurrentAccountId',
@@ -320,7 +329,7 @@ export default {
},
sendWithSignature() {
// channelType is sourced from inboxMixin
return this.fetchSignatureFlagFromUiSettings(this.channelType);
return this.fetchSignatureFlagFromUISettings(this.channelType);
},
signatureToggleTooltip() {
return this.sendWithSignature
@@ -79,7 +79,7 @@
</template>
<script>
import alertMixin from 'shared/mixins/alertMixin';
import { useAlert } from 'dashboard/composables';
import { required, requiredIf } from 'vuelidate/lib/validators';
import FilterInputBox from '../FilterInput/Index.vue';
import languages from './advancedFilterItems/languages';
@@ -89,12 +89,13 @@ import { filterAttributeGroups } from './advancedFilterItems';
import filterMixin from 'shared/mixins/filterMixin';
import * as OPERATORS from 'dashboard/components/widgets/FilterInput/FilterOperatorTypes.js';
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
import cloneObject from 'dashboard/helpers/clone';
export default {
components: {
FilterInputBox,
},
mixins: [alertMixin, filterMixin],
mixins: [filterMixin],
props: {
onClose: {
type: Function,
@@ -341,7 +342,7 @@ export default {
},
removeFilter(index) {
if (this.appliedFilters.length <= 1) {
this.showAlert(this.$t('FILTER.FILTER_DELETE_ERROR'));
useAlert(this.$t('FILTER.FILTER_DELETE_ERROR'));
} else {
this.appliedFilters.splice(index, 1);
}
@@ -351,7 +352,7 @@ export default {
if (this.$v.$invalid) return;
this.$store.dispatch(
'setConversationFilters',
JSON.parse(JSON.stringify(this.appliedFilters))
cloneObject(this.appliedFilters)
);
this.$emit('applyFilter', this.appliedFilters);
this.$track(CONVERSATION_EVENTS.APPLY_FILTER, {
@@ -12,10 +12,10 @@
<div
v-if="showActionsDropdown"
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">
<span class="text-slate-800 dark:text-slate-100 text-xs font-medium">{{
<div class="flex items-center justify-between last:mt-4">
<span class="text-xs font-medium text-slate-800 dark:text-slate-100">{{
$t('CHAT_LIST.CHAT_SORT.STATUS')
}}</span>
<filter-item
@@ -26,8 +26,8 @@
@onChangeFilter="onChangeFilter"
/>
</div>
<div class="items-center flex justify-between last:mt-4">
<span class="text-slate-800 dark:text-slate-100 text-xs font-medium">{{
<div class="flex items-center justify-between last:mt-4">
<span class="text-xs font-medium text-slate-800 dark:text-slate-100">{{
$t('CHAT_LIST.CHAT_SORT.ORDER_BY')
}}</span>
<filter-item
@@ -46,13 +46,19 @@
import wootConstants from 'dashboard/constants/globals';
import { mapGetters } from 'vuex';
import FilterItem from './FilterItem.vue';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import { useUISettings } from 'dashboard/composables/useUISettings';
export default {
components: {
FilterItem,
},
mixins: [uiSettingsMixin],
setup() {
const { updateUISettings } = useUISettings();
return {
updateUISettings,
};
},
data() {
return {
showActionsDropdown: false,
@@ -120,13 +120,11 @@ import { mapGetters } from 'vuex';
import Thumbnail from '../Thumbnail.vue';
import MessagePreview from './MessagePreview.vue';
import conversationMixin from '../../../mixins/conversations';
import timeMixin from '../../../mixins/time';
import router from '../../../routes';
import { frontendURL, conversationUrl } from '../../../helper/URLHelper';
import InboxName from '../InboxName.vue';
import inboxMixin from 'shared/mixins/inboxMixin';
import ConversationContextMenu from './contextMenu/Index.vue';
import alertMixin from 'shared/mixins/alertMixin';
import TimeAgo from 'dashboard/components/ui/TimeAgo.vue';
import CardLabels from './conversationCardComponents/CardLabels.vue';
import PriorityMark from './PriorityMark.vue';
@@ -143,8 +141,7 @@ export default {
PriorityMark,
SLACardLabel,
},
mixins: [inboxMixin, timeMixin, conversationMixin, alertMixin],
mixins: [inboxMixin, conversationMixin],
props: {
activeLabel: {
type: String,
@@ -1,7 +1,7 @@
<!-- eslint-disable vue/no-mutating-props -->
<template>
<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
:header-title="$t('EMAIL_TRANSCRIPT.TITLE')"
:header-content="$t('EMAIL_TRANSCRIPT.DESC')"
@@ -61,7 +61,7 @@
</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">
<woot-submit-button
:button-text="$t('EMAIL_TRANSCRIPT.SUBMIT')"
:disabled="!isFormValid"
@@ -77,9 +77,8 @@
<script>
import { required, minLength, email } from 'vuelidate/lib/validators';
import alertMixin from 'shared/mixins/alertMixin';
import { useAlert } from 'dashboard/composables';
export default {
mixins: [alertMixin],
props: {
show: {
type: Boolean,
@@ -142,10 +141,10 @@ export default {
email: this.selectedEmailAddress,
conversationId: this.currentChat.id,
});
this.showAlert(this.$t('EMAIL_TRANSCRIPT.SEND_EMAIL_SUCCESS'));
useAlert(this.$t('EMAIL_TRANSCRIPT.SEND_EMAIL_SUCCESS'));
this.onCancel();
} catch (error) {
this.showAlert(this.$t('EMAIL_TRANSCRIPT.SEND_EMAIL_ERROR'));
useAlert(this.$t('EMAIL_TRANSCRIPT.SEND_EMAIL_ERROR'));
} finally {
this.isSubmitting = false;
}
@@ -35,7 +35,7 @@
</template>
<script>
import { mapGetters } from 'vuex';
import adminMixin from 'dashboard/mixins/isAdmin';
import { useAdmin } from 'dashboard/composables/useAdmin';
import accountMixin from 'dashboard/mixins/account';
import OnboardingView from '../OnboardingView.vue';
import EmptyStateMessage from './EmptyStateMessage.vue';
@@ -45,13 +45,19 @@ export default {
OnboardingView,
EmptyStateMessage,
},
mixins: [accountMixin, adminMixin],
mixins: [accountMixin],
props: {
isOnExpandedLayout: {
type: Boolean,
default: false,
},
},
setup() {
const { isAdmin } = useAdmin();
return {
isAdmin,
};
},
computed: {
...mapGetters({
currentChat: 'getSelectedChat',
@@ -157,8 +157,7 @@ import ContextMenu from 'dashboard/modules/conversations/components/MessageConte
import InstagramStory from './bubble/InstagramStory.vue';
import InstagramStoryReply from './bubble/InstagramStoryReply.vue';
import Spinner from 'shared/components/Spinner.vue';
import alertMixin from 'shared/mixins/alertMixin';
import contentTypeMixin from 'shared/mixins/contentTypeMixin';
import { CONTENT_TYPES } from 'shared/constants/contentType';
import { MESSAGE_TYPE, MESSAGE_STATUS } from 'shared/constants/messages';
import { generateBotMessageContent } from './helpers/botMessageContentHelper';
import { BUS_EVENTS } from 'shared/constants/busEvents';
@@ -184,7 +183,7 @@ export default {
InstagramStoryReply,
Spinner,
},
mixins: [alertMixin, messageFormatterMixin, contentTypeMixin],
mixins: [messageFormatterMixin],
props: {
data: {
type: Object,
@@ -482,6 +481,9 @@ export default {
}
return '';
},
isEmailContentType() {
return this.contentType === CONTENT_TYPES.INCOMING_EMAIL;
},
},
watch: {
data() {
@@ -1,5 +1,5 @@
<template>
<div class="flex actions--container relative items-center gap-2">
<div class="relative flex items-center gap-2 actions--container">
<woot-button
v-if="!currentChat.muted"
v-tooltip="$t('CONTACT_PANEL.MUTE_CONTACT')"
@@ -37,7 +37,7 @@
</template>
<script>
import { mapGetters } from 'vuex';
import alertMixin from 'shared/mixins/alertMixin';
import { useAlert } from 'dashboard/composables';
import EmailTranscriptModal from './EmailTranscriptModal.vue';
import ResolveAction from '../../buttons/ResolveAction.vue';
import {
@@ -51,7 +51,6 @@ export default {
EmailTranscriptModal,
ResolveAction,
},
mixins: [alertMixin],
data() {
return {
showEmailActionsModal: false,
@@ -73,11 +72,11 @@ export default {
methods: {
mute() {
this.$store.dispatch('muteConversation', this.currentChat.id);
this.showAlert(this.$t('CONTACT_PANEL.MUTED_SUCCESS'));
useAlert(this.$t('CONTACT_PANEL.MUTED_SUCCESS'));
},
unmute() {
this.$store.dispatch('unmuteConversation', this.currentChat.id);
this.showAlert(this.$t('CONTACT_PANEL.UNMUTED_SUCCESS'));
useAlert(this.$t('CONTACT_PANEL.UNMUTED_SUCCESS'));
},
toggleEmailActionsModal() {
this.showEmailActionsModal = !this.showEmailActionsModal;
@@ -153,7 +153,8 @@
<script>
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 CannedResponse from './CannedResponse.vue';
@@ -180,10 +181,8 @@ import {
import WhatsappTemplates from './WhatsappTemplates/Modal.vue';
import { MESSAGE_MAX_LENGTH } from 'shared/helpers/MessageTypeHelper';
import inboxMixin, { INBOX_FEATURES } from 'shared/mixins/inboxMixin';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import { trimContent, debounce } from '@chatwoot/utils';
import wootConstants from 'dashboard/constants/globals';
import { isEditorHotKeyEnabled } from 'dashboard/mixins/uiSettings';
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
import rtlMixin from 'shared/mixins/rtlMixin';
import fileUploadMixin from 'dashboard/mixins/fileUploadMixin';
@@ -218,8 +217,6 @@ export default {
},
mixins: [
inboxMixin,
uiSettingsMixin,
alertMixin,
messageFormatterMixin,
rtlMixin,
fileUploadMixin,
@@ -231,6 +228,21 @@ export default {
default: false,
},
},
setup() {
const {
uiSettings,
updateUISettings,
isEditorHotKeyEnabled,
fetchSignatureFlagFromUISettings,
} = useUISettings();
return {
uiSettings,
updateUISettings,
isEditorHotKeyEnabled,
fetchSignatureFlagFromUISettings,
};
},
data() {
return {
message: '',
@@ -310,7 +322,7 @@ export default {
agentId,
})
.then(() => {
this.showAlert(this.$t('CONVERSATION.CHANGE_AGENT'));
useAlert(this.$t('CONVERSATION.CHANGE_AGENT'));
});
},
},
@@ -406,7 +418,7 @@ export default {
if (this.isPrivate) {
sendMessageText = this.$t('CONVERSATION.REPLYBOX.CREATE');
}
const keyLabel = isEditorHotKeyEnabled(this.uiSettings, 'cmd_enter')
const keyLabel = this.isEditorHotKeyEnabled('cmd_enter')
? '(⌘ + ↵)'
: '(↵)';
return `${sendMessageText} ${keyLabel}`;
@@ -479,7 +491,7 @@ export default {
return !!this.signatureToApply;
},
sendWithSignature() {
return this.fetchSignatureFlagFromUiSettings(this.channelType);
return this.fetchSignatureFlagFromUISettings(this.channelType);
},
editorMessageKey() {
const { editor_message_key: isEnabled } = this.uiSettings;
@@ -748,7 +760,7 @@ export default {
!this.showCannedMenu &&
!this.showVariablesMenu &&
this.isFocused &&
isEditorHotKeyEnabled(this.uiSettings, selectedKey)
this.isEditorHotKeyEnabled(selectedKey)
);
},
onPaste(e) {
@@ -880,7 +892,7 @@ export default {
} catch (error) {
const errorMessage =
error?.response?.data?.error || this.$t('CONVERSATION.MESSAGE_ERROR');
this.showAlert(errorMessage);
useAlert(errorMessage);
}
},
async onSendWhatsAppReply(messagePayload) {
@@ -77,10 +77,10 @@
import { MESSAGE_TYPE, MESSAGE_STATUS } from 'shared/constants/messages';
import inboxMixin from 'shared/mixins/inboxMixin';
import { mapGetters } from 'vuex';
import timeMixin from '../../../../mixins/time';
import { messageTimestamp } from 'shared/helpers/timeHelper';
export default {
mixins: [inboxMixin, timeMixin],
mixins: [inboxMixin],
props: {
sender: {
type: Object,
@@ -159,7 +159,7 @@ export default {
return MESSAGE_STATUS.SENT === this.messageStatus;
},
readableTime() {
return this.messageTimestamp(this.createdAt, 'LLL d, h:mm a');
return messageTimestamp(this.createdAt, 'LLL d, h:mm a');
},
screenName() {
const { additional_attributes: additionalAttributes = {} } =
@@ -17,14 +17,13 @@
</template>
<script>
import { useAlert } from 'dashboard/composables';
import {
DuplicateContactException,
ExceptionWithMessage,
} from 'shared/helpers/CustomErrors';
import alertMixin from 'shared/mixins/alertMixin';
export default {
mixins: [alertMixin],
props: {
name: {
type: String,
@@ -52,18 +51,18 @@ export default {
'contacts/create',
this.getContactObject()
);
this.showAlert(this.$t('CONTACT_FORM.SUCCESS_MESSAGE'));
useAlert(this.$t('CONTACT_FORM.SUCCESS_MESSAGE'));
}
this.openContactNewTab(contact.id);
} catch (error) {
if (error instanceof DuplicateContactException) {
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) {
this.showAlert(error.data);
useAlert(error.data);
} else {
this.showAlert(this.$t('CONTACT_FORM.ERROR_MESSAGE'));
useAlert(this.$t('CONTACT_FORM.ERROR_MESSAGE'));
}
}
},
@@ -31,10 +31,9 @@
<script>
import DyteAPI from 'dashboard/api/integrations/dyte';
import { buildDyteURL } from 'shared/helpers/IntegrationHelper';
import alertMixin from 'shared/mixins/alertMixin';
import { useAlert } from 'dashboard/composables';
export default {
mixins: [alertMixin],
props: {
messageId: {
type: Number,
@@ -61,7 +60,7 @@ export default {
await DyteAPI.addParticipantToMeeting(this.messageId);
this.dyteAuthToken = authToken;
} catch (err) {
this.showAlert(this.$t('INTEGRATION_SETTINGS.DYTE.JOIN_ERROR'));
useAlert(this.$t('INTEGRATION_SETTINGS.DYTE.JOIN_ERROR'));
} finally {
this.isLoading = false;
}
@@ -12,7 +12,7 @@
@click="onClose"
>
<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
>
<div
@@ -167,7 +167,7 @@
/>
</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
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>
import { mapGetters } from 'vuex';
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';
@@ -199,7 +199,7 @@ export default {
components: {
Thumbnail,
},
mixins: [keyboardEventListenerMixins, timeMixin],
mixins: [keyboardEventListenerMixins],
props: {
show: {
type: Boolean,
@@ -236,7 +236,7 @@ export default {
readableTime() {
const { created_at: createdAt } = this.activeAttachment;
if (!createdAt) return '';
return this.messageTimestamp(createdAt, 'LLL d yyyy, h:mm a') || '';
return messageTimestamp(createdAt, 'LLL d yyyy, h:mm a') || '';
},
isImage() {
return this.activeFileType === ALLOWED_FILE_TYPES.IMAGE;
@@ -85,11 +85,11 @@ describe('MoveActions', () => {
it('shows alert', async () => {
await moreActions.find('button:first-child').trigger('click');
expect(emitter.emit).toBeCalledWith(
'newToastMessage',
'This contact is blocked successfully. You will not be notified of any future conversations.',
undefined
);
expect(emitter.emit).toBeCalledWith('newToastMessage', {
message:
'This contact is blocked successfully. You will not be notified of any future conversations.',
action: null,
});
});
});
@@ -111,11 +111,10 @@ describe('MoveActions', () => {
it('shows alert', async () => {
await moreActions.find('button:first-child').trigger('click');
expect(emitter.emit).toBeCalledWith(
'newToastMessage',
'This contact is unblocked successfully.',
undefined
);
expect(emitter.emit).toBeCalledWith('newToastMessage', {
message: 'This contact is unblocked successfully.',
action: null,
});
});
});
});
+12 -3
View File
@@ -1,6 +1,10 @@
import { getCurrentInstance } from 'vue';
import { emitter } from 'shared/helpers/mitt';
/**
* Custom hook to track events
* @returns {Function} The track function
*/
export const useTrack = () => {
const vm = getCurrentInstance();
if (!vm) throw new Error('must be called in setup');
@@ -8,6 +12,11 @@ export const useTrack = () => {
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 => {
return arr.every(elem => typeof elem === 'string');
};
@@ -27,7 +29,7 @@ const generatePayloadForObject = item => {
};
const generatePayload = data => {
const actions = JSON.parse(JSON.stringify(data));
const actions = cloneObject(data);
let payload = actions.map(item => {
if (Array.isArray(item.action_params)) {
item.action_params = formatArray(item.action_params);
@@ -5,6 +5,8 @@ import {
} from 'dashboard/routes/dashboard/settings/automation/operators';
import filterQueryGenerator from './filterQueryGenerator';
import actionQueryGenerator from './actionQueryGenerator';
import cloneObject from 'dashboard/helpers/clone';
const MESSAGE_CONDITION_VALUES = [
{
id: 'incoming',
@@ -255,7 +257,7 @@ export const getStandardAttributeInputType = (automationTypes, event, key) => {
};
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 = filterQueryGenerator(automation.conditions).payload;
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 => {
return item.values[0]?.id ? item.values.map(val => val.id) : item.values;
};
@@ -21,7 +23,7 @@ const generateValues = item => {
const generatePayload = data => {
// 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 => {
// 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
@@ -1,7 +1,7 @@
/* eslint-disable no-console */
import NotificationSubscriptions from '../api/notificationSubscription';
import auth from '../api/auth';
import { emitter } from 'shared/helpers/mitt';
import { useAlert } from 'dashboard/composables';
export const verifyServiceWorkerExistence = (callback = () => {}) => {
if (!('serviceWorker' in navigator)) {
@@ -69,19 +69,13 @@ export const registerSubscription = (onSuccess = () => {}) => {
onSuccess();
})
.catch(() => {
emitter.emit(
'newToastMessage',
'This browser does not support desktop notification'
);
useAlert('This browser does not support desktop notification');
});
};
export const requestPushPermissions = ({ onSuccess }) => {
if (!('Notification' in window)) {
emitter.emit(
'newToastMessage',
'This browser does not support desktop notification'
);
useAlert('This browser does not support desktop notification');
} else if (Notification.permission === 'granted') {
registerSubscription(onSuccess);
} 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": {
"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_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>.",
+2 -3
View File
@@ -1,10 +1,9 @@
import { mapGetters } from 'vuex';
import { useAlert } from 'dashboard/composables';
import { OPEN_AI_EVENTS } from '../helper/AnalyticsHelper/events';
import OpenAPI from '../api/integrations/openapi';
import alertMixin from 'shared/mixins/alertMixin';
export default {
mixins: [alertMixin],
mounted() {
this.fetchIntegrationsIfRequired();
},
@@ -101,7 +100,7 @@ export default {
} = result;
return generatedMessage;
} catch (error) {
this.showAlert(this.$t('INTEGRATION_SETTINGS.OPEN_AI.GENERATE_ERROR'));
useAlert(this.$t('INTEGRATION_SETTINGS.OPEN_AI.GENERATE_ERROR'));
return '';
}
},
@@ -17,6 +17,7 @@ import {
generateCustomAttributes,
} from 'dashboard/helper/automationHelper';
import { mapGetters } from 'vuex';
import { useAlert } from 'dashboard/composables';
export default {
computed: {
@@ -138,14 +139,14 @@ export default {
},
removeFilter(index) {
if (this.automation.conditions.length <= 1) {
this.showAlert(this.$t('AUTOMATION.CONDITION.DELETE_MESSAGE'));
useAlert(this.$t('AUTOMATION.CONDITION.DELETE_MESSAGE'));
} else {
this.automation.conditions.splice(index, 1);
}
},
removeAction(index) {
if (this.automation.actions.length <= 1) {
this.showAlert(this.$t('AUTOMATION.ACTION.DELETE_MESSAGE'));
useAlert(this.$t('AUTOMATION.ACTION.DELETE_MESSAGE'));
} else {
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 {
MAXIMUM_FILE_UPLOAD_SIZE,
MAXIMUM_FILE_UPLOAD_SIZE_TWILIO_SMS_CHANNEL,
@@ -38,13 +39,13 @@ export default {
upload.create((error, blob) => {
if (error) {
this.showAlert(error);
useAlert(error);
} else {
this.attachFile({ file, blob });
}
});
} else {
this.showAlert(
useAlert(
this.$t('CONVERSATION.FILE_SIZE_LIMIT', {
MAXIMUM_SUPPORTED_FILE_UPLOAD_SIZE,
})
@@ -61,7 +62,7 @@ export default {
if (checkFileSizeLimit(file, MAXIMUM_SUPPORTED_FILE_UPLOAD_SIZE)) {
this.attachFile({ file });
} else {
this.showAlert(
useAlert(
this.$t('CONVERSATION.FILE_SIZE_LIMIT', {
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 { useAlert } from 'dashboard/composables';
import fileUploadMixin from 'dashboard/mixins/fileUploadMixin';
vi.mock('shared/helpers/FileHelper', () => ({
checkFileSizeLimit: vi.fn(),
@@ -11,6 +12,10 @@ vi.mock('activestorage', () => ({
})),
}));
vi.mock('dashboard/composables', () => ({
useAlert: vi.fn(),
}));
describe('FileUploadMixin', () => {
let vm;
@@ -53,9 +58,8 @@ describe('FileUploadMixin', () => {
it('shows an alert if the file size exceeds the maximum limit', () => {
const fakeFile = { size: 999999999 };
vm.showAlert = vi.fn();
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', () => {
const fakeFile = { size: 999999999 };
vm.showAlert = vi.fn();
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>
<script>
import alertMixin from 'shared/mixins/alertMixin';
import { useAlert } from 'dashboard/composables';
import MergeContact from 'dashboard/modules/contact/components/MergeContact.vue';
import ContactAPI from 'dashboard/api/contacts';
@@ -29,7 +29,6 @@ import { CONTACTS_EVENTS } from '../../helper/AnalyticsHelper/events';
export default {
components: { MergeContact },
mixins: [alertMixin],
props: {
primaryContact: {
type: Object,
@@ -68,7 +67,7 @@ export default {
contact => contact.id !== this.primaryContact.id
);
} catch (error) {
this.showAlert(this.$t('MERGE_CONTACTS.SEARCH.ERROR_MESSAGE'));
useAlert(this.$t('MERGE_CONTACTS.SEARCH.ERROR_MESSAGE'));
} finally {
this.isSearching = false;
}
@@ -80,10 +79,10 @@ export default {
childId: this.primaryContact.id,
parentId: parentContactId,
});
this.showAlert(this.$t('MERGE_CONTACTS.FORM.SUCCESS_MESSAGE'));
useAlert(this.$t('MERGE_CONTACTS.FORM.SUCCESS_MESSAGE'));
this.onClose();
} 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')"
: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
:is-disabled="$v.attributeName.$invalid || isCreating"
:is-loading="isCreating"
@@ -38,14 +38,12 @@
<script>
import Modal from 'dashboard/components/Modal.vue';
import alertMixin from 'shared/mixins/alertMixin';
import { required, minLength } from 'vuelidate/lib/validators';
export default {
components: {
Modal,
},
mixins: [alertMixin],
props: {
show: {
type: Boolean,
@@ -57,7 +57,7 @@
</span>
</div>
</div>
<div class="multiselect-wrap--medium flex">
<div class="flex multiselect-wrap--medium">
<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"
>
@@ -102,7 +102,7 @@
:primary-contact-name="primaryContact.name"
: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">
{{ $t('MERGE_CONTACTS.FORM.CANCEL') }}
</woot-button>
@@ -114,7 +114,6 @@
</template>
<script>
import alertMixin from 'shared/mixins/alertMixin';
import { required } from 'vuelidate/lib/validators';
import MergeContactSummary from 'dashboard/modules/contact/components/MergeContactSummary.vue';
@@ -122,7 +121,6 @@ import ContactDropdownItem from './ContactDropdownItem.vue';
export default {
components: { MergeContactSummary, ContactDropdownItem },
mixins: [alertMixin],
props: {
primaryContact: {
type: Object,
@@ -104,7 +104,7 @@
</div>
</template>
<script>
import alertMixin from 'shared/mixins/alertMixin';
import { useAlert } from 'dashboard/composables';
import { mapGetters } from 'vuex';
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
import AddCannedModal from 'dashboard/routes/dashboard/settings/canned/AddCanned.vue';
@@ -123,7 +123,7 @@ export default {
TranslateModal,
MenuItem,
},
mixins: [alertMixin, messageFormatterMixin],
mixins: [messageFormatterMixin],
props: {
message: {
type: Object,
@@ -183,12 +183,12 @@ export default {
await copyTextToClipboard(
`${fullConversationURL}?messageId=${this.messageId}`
);
this.showAlert(this.$t('CONVERSATION.CONTEXT_MENU.LINK_COPIED'));
useAlert(this.$t('CONVERSATION.CONTEXT_MENU.LINK_COPIED'));
this.handleClose();
},
async handleCopy() {
await copyTextToClipboard(this.plainTextContent);
this.showAlert(this.$t('CONTACT_PANEL.COPY_SUCCESSFUL'));
useAlert(this.$t('CONTACT_PANEL.COPY_SUCCESSFUL'));
this.handleClose();
},
showCannedResponseModal() {
@@ -233,10 +233,10 @@ export default {
conversationId: this.conversationId,
messageId: this.messageId,
});
this.showAlert(this.$t('CONVERSATION.SUCCESS_DELETE_MESSAGE'));
useAlert(this.$t('CONVERSATION.SUCCESS_DELETE_MESSAGE'));
this.handleClose();
} catch (error) {
this.showAlert(this.$t('CONVERSATION.FAIL_DELETE_MESSSAGE'));
useAlert(this.$t('CONVERSATION.FAIL_DELETE_MESSSAGE'));
}
},
closeDeleteModal() {
@@ -1,8 +1,8 @@
<template>
<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">
<thumbnail
:title="noteAuthorName"
@@ -22,7 +22,7 @@
</span>
</div>
</div>
<div class="actions flex invisible">
<div class="flex invisible actions">
<woot-button
v-tooltip="$t('NOTES.CONTENT_HEADER.DELETE')"
variant="smooth"
@@ -45,22 +45,22 @@
</div>
<p
v-dompurify-html="formatMessage(note || '')"
class="note__content mt-4"
class="mt-4 note__content"
/>
</div>
</template>
<script>
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
import timeMixin from 'dashboard/mixins/time';
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
import { dynamicTime } from 'shared/helpers/timeHelper';
export default {
components: {
Thumbnail,
},
mixins: [timeMixin, messageFormatterMixin],
mixins: [messageFormatterMixin],
props: {
id: {
@@ -87,7 +87,7 @@ export default {
},
computed: {
readableTime() {
return this.dynamicTime(this.createdAt);
return dynamicTime(this.createdAt);
},
noteAuthor() {
return this.user || {};
@@ -27,7 +27,7 @@
</h5>
<h5
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>
{{ email }}
@@ -40,14 +40,13 @@
<script>
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';
export default {
components: {
InboxName,
},
mixins: [timeMixin],
props: {
id: {
type: Number,
@@ -90,7 +89,7 @@ export default {
);
},
createdAtTime() {
return this.dynamicTime(this.createdAt);
return dynamicTime(this.createdAt);
},
},
};
@@ -1,6 +1,6 @@
<template>
<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
:route="currentRoute"
@@ -11,7 +11,7 @@
@close-key-shortcut-modal="closeKeyShortcutModal"
@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 />
<command-bar />
<account-selector
@@ -47,7 +47,7 @@ import AddAccountModal from 'dashboard/components/layout/sidebarComponents/AddAc
import AccountSelector from 'dashboard/components/layout/sidebarComponents/AccountSelector.vue';
import AddLabelModal from 'dashboard/routes/dashboard/settings/labels/AddLabel.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';
const CommandBar = () => import('./commands/commandbar.vue');
@@ -61,7 +61,14 @@ export default {
AddLabelModal,
NotificationPanel,
},
mixins: [uiSettingsMixin],
setup() {
const { uiSettings, updateUISettings } = useUISettings();
return {
uiSettings,
updateUISettings,
};
},
data() {
return {
showAccountModal: false,
@@ -21,12 +21,10 @@ import appearanceHotKeys from './appearanceHotKeys';
import agentMixin from 'dashboard/mixins/agentMixin';
import conversationLabelMixin from 'dashboard/mixins/conversation/labelMixin';
import conversationTeamMixin from 'dashboard/mixins/conversation/teamMixin';
import adminMixin from 'dashboard/mixins/isAdmin';
import { GENERAL_EVENTS } from '../../../helper/AnalyticsHelper/events';
export default {
mixins: [
adminMixin,
agentMixin,
conversationHotKeysMixin,
bulkActionsHotKeysMixin,
@@ -17,6 +17,7 @@ import {
} from './CommandBarIcons';
import { frontendURL } from '../../../helper/URLHelper';
import { mapGetters } from 'vuex';
import { useAdmin } from 'dashboard/composables/useAdmin';
import { FEATURE_FLAGS } from '../../../featureFlags';
const GO_TO_COMMANDS = [
@@ -172,6 +173,12 @@ const GO_TO_COMMANDS = [
];
export default {
setup() {
const { isAdmin } = useAdmin();
return {
isAdmin,
};
},
computed: {
...mapGetters({
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 CustomAttributes from 'dashboard/routes/dashboard/conversation/customAttributes/CustomAttributes.vue';
import draggable from 'vuedraggable';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import { useUISettings } from 'dashboard/composables/useUISettings';
export default {
components: {
@@ -97,7 +97,6 @@ export default {
CustomAttributes,
draggable,
},
mixins: [uiSettingsMixin],
props: {
contact: {
type: Object,
@@ -116,6 +115,21 @@ export default {
default: true,
},
},
setup() {
const {
updateUISettings,
isContactSidebarItemOpen,
contactSidebarItemsOrder,
toggleSidebarUIState,
} = useUISettings();
return {
updateUISettings,
isContactSidebarItemOpen,
contactSidebarItemsOrder,
toggleSidebarUIState,
};
},
data() {
return {
dragEnabled: true,
@@ -9,12 +9,11 @@
<script>
import { mapGetters } from 'vuex';
import { useAlert } from 'dashboard/composables';
import LabelSelector from 'dashboard/components/widgets/LabelSelector.vue';
import alertMixin from 'shared/mixins/alertMixin';
export default {
components: { LabelSelector },
mixins: [alertMixin],
props: {
contactId: {
type: [String, Number],
@@ -59,7 +58,7 @@ export default {
labels: selectedLabels,
});
} catch (error) {
this.showAlert(this.$t('CONTACT_PANEL.LABELS.CONTACT.ERROR'));
useAlert(this.$t('CONTACT_PANEL.LABELS.CONTACT.ERROR'));
}
},
@@ -89,7 +89,7 @@
</template>
<script>
import alertMixin from 'shared/mixins/alertMixin';
import { useAlert } from 'dashboard/composables';
import { required } from 'vuelidate/lib/validators';
import FilterInputBox from '../../../../components/widgets/FilterInput/Index.vue';
import countries from 'shared/constants/countries.js';
@@ -98,11 +98,13 @@ import { filterAttributeGroups } from '../contactFilterItems';
import filterMixin from 'shared/mixins/filterMixin';
import * as OPERATORS from 'dashboard/components/widgets/FilterInput/FilterOperatorTypes.js';
import { CONTACTS_EVENTS } from '../../../../helper/AnalyticsHelper/events';
import cloneObject from 'dashboard/helpers/clone';
export default {
components: {
FilterInputBox,
},
mixins: [alertMixin, filterMixin],
mixins: [filterMixin],
props: {
onClose: {
type: Function,
@@ -308,7 +310,7 @@ export default {
},
removeFilter(index) {
if (this.appliedFilters.length <= 1) {
this.showAlert(this.$t('CONTACTS_FILTER.FILTER_DELETE_ERROR'));
useAlert(this.$t('CONTACTS_FILTER.FILTER_DELETE_ERROR'));
} else {
this.appliedFilters.splice(index, 1);
}
@@ -318,7 +320,7 @@ export default {
if (this.$v.$invalid) return;
this.$store.dispatch(
'contacts/setContactFilters',
JSON.parse(JSON.stringify(this.appliedFilters))
cloneObject(this.appliedFilters)
);
this.$emit('applyFilter', this.appliedFilters);
this.$track(CONTACTS_EVENTS.APPLY_FILTER, {
@@ -1,6 +1,6 @@
<template>
<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
:fixed-header="true"
@@ -20,7 +20,7 @@
v-else-if="!isLoading && !contacts.length"
: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 />
<span>{{ $t('CONTACTS_PAGE.LIST.LOADING_MESSAGE') }}</span>
</div>
@@ -34,7 +34,7 @@ import { getCountryFlag } from 'dashboard/helper/flag';
import Spinner from 'shared/components/Spinner.vue';
import Thumbnail from 'dashboard/components/widgets/Thumbnail.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 FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
@@ -44,7 +44,7 @@ export default {
Spinner,
VeTable,
},
mixins: [timeMixin, rtlMixin],
mixins: [rtlMixin],
props: {
contacts: {
type: Array,
@@ -105,9 +105,9 @@ export default {
countryCode: additional.country_code,
conversationsCount: item.conversations_count || '---',
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}
/>
<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
to={`/app/accounts/${this.$route.params.accountId}/contacts/${row.id}`}
class="user-name"
@@ -81,6 +81,7 @@
<script>
import { mapGetters } from 'vuex';
import { useAlert } from 'dashboard/composables';
import ContactsHeader from './Header.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 DeleteCustomViews from 'dashboard/routes/dashboard/customviews/DeleteCustomViews.vue';
import { CONTACTS_EVENTS } from '../../../../helper/AnalyticsHelper/events';
import alertMixin from 'shared/mixins/alertMixin';
import countries from 'shared/constants/countries.js';
import { generateValuesForEditCustomViews } from 'dashboard/helper/customViewsHelper';
@@ -113,7 +113,6 @@ export default {
AddCustomViews,
DeleteCustomViews,
},
mixins: [alertMixin],
props: {
label: { type: String, default: '' },
segmentsId: {
@@ -404,11 +403,9 @@ export default {
...query,
label: this.label,
});
this.showAlert(this.$t('EXPORT_CONTACTS.SUCCESS_MESSAGE'));
useAlert(this.$t('EXPORT_CONTACTS.SUCCESS_MESSAGE'));
} catch (error) {
this.showAlert(
error.message || this.$t('EXPORT_CONTACTS.ERROR_MESSAGE')
);
useAlert(error.message || this.$t('EXPORT_CONTACTS.ERROR_MESSAGE'));
}
},
setParamsForEditSegmentModal() {
@@ -125,10 +125,9 @@
<script>
import { mapGetters } from 'vuex';
import adminMixin from 'dashboard/mixins/isAdmin';
import { useAdmin } from 'dashboard/composables/useAdmin';
export default {
mixins: [adminMixin],
props: {
headerTitle: {
type: String,
@@ -143,6 +142,12 @@ export default {
default: 0,
},
},
setup() {
const { isAdmin } = useAdmin();
return {
isAdmin,
};
},
data() {
return {
showCreateModal: false,
@@ -1,6 +1,6 @@
<template>
<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')">
<p>
{{ $t('IMPORT_CONTACTS.DESC') }}
@@ -22,7 +22,7 @@
/>
</label>
</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">
<woot-button
:disabled="uiFlags.isCreating || !file"
@@ -42,16 +42,15 @@
</template>
<script>
import Modal from '../../../../components/Modal.vue';
import { mapGetters } from 'vuex';
import alertMixin from 'shared/mixins/alertMixin';
import { useAlert } from 'dashboard/composables';
import { CONTACTS_EVENTS } from '../../../../helper/AnalyticsHelper/events';
import Modal from '../../../../components/Modal.vue';
export default {
components: {
Modal,
},
mixins: [alertMixin],
props: {
onClose: {
type: Function,
@@ -81,12 +80,10 @@ export default {
if (!this.file) return;
await this.$store.dispatch('contacts/import', this.file);
this.onClose();
this.showAlert(this.$t('IMPORT_CONTACTS.SUCCESS_MESSAGE'));
useAlert(this.$t('IMPORT_CONTACTS.SUCCESS_MESSAGE'));
this.$track(CONTACTS_EVENTS.IMPORT_SUCCESS);
} catch (error) {
this.showAlert(
error.message || this.$t('IMPORT_CONTACTS.ERROR_MESSAGE')
);
useAlert(error.message || this.$t('IMPORT_CONTACTS.ERROR_MESSAGE'));
this.$track(CONTACTS_EVENTS.IMPORT_FAILURE);
}
},
@@ -28,10 +28,8 @@
</template>
<script>
import timeMixin from 'dashboard/mixins/time';
import { dynamicTime } from 'shared/helpers/timeHelper';
export default {
mixins: [timeMixin],
props: {
eventType: {
type: String,
@@ -53,7 +51,7 @@ export default {
computed: {
readableTime() {
return this.dynamicTime(this.timeStamp);
return dynamicTime(this.timeStamp);
},
},
@@ -133,7 +133,7 @@
<script>
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 ContactConversations from './ContactConversations.vue';
import ConversationAction from './ConversationAction.vue';
@@ -143,7 +143,6 @@ import ContactInfo from './contact/ContactInfo.vue';
import ConversationInfo from './ConversationInfo.vue';
import CustomAttributes from './customAttributes/CustomAttributes.vue';
import draggable from 'vuedraggable';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import MacrosList from './Macros/List.vue';
export default {
components: {
@@ -157,7 +156,6 @@ export default {
draggable,
MacrosList,
},
mixins: [alertMixin, uiSettingsMixin],
props: {
conversationId: {
type: [Number, String],
@@ -172,6 +170,21 @@ export default {
default: () => {},
},
},
setup() {
const {
updateUISettings,
isContactSidebarItemOpen,
conversationSidebarItemsOrder,
toggleSidebarUIState,
} = useUISettings();
return {
updateUISettings,
isContactSidebarItemOpen,
conversationSidebarItemsOrder,
toggleSidebarUIState,
};
},
data() {
return {
dragEnabled: true,
@@ -82,7 +82,7 @@
<script>
import { mapGetters } from 'vuex';
import alertMixin from 'shared/mixins/alertMixin';
import { useAlert } from 'dashboard/composables';
import ContactDetailsItem from './ContactDetailsItem.vue';
import MultiselectDropdown from 'shared/components/ui/MultiselectDropdown.vue';
import ConversationLabels from './labels/LabelBox.vue';
@@ -97,7 +97,7 @@ export default {
MultiselectDropdown,
ConversationLabels,
},
mixins: [agentMixin, alertMixin, teamMixin],
mixins: [agentMixin, teamMixin],
props: {
conversationId: {
type: [Number, String],
@@ -157,7 +157,7 @@ export default {
agentId,
})
.then(() => {
this.showAlert(this.$t('CONVERSATION.CHANGE_AGENT'));
useAlert(this.$t('CONVERSATION.CHANGE_AGENT'));
});
},
},
@@ -172,7 +172,7 @@ export default {
this.$store
.dispatch('assignTeam', { conversationId, teamId })
.then(() => {
this.showAlert(this.$t('CONVERSATION.CHANGE_TEAM'));
useAlert(this.$t('CONVERSATION.CHANGE_TEAM'));
});
},
},
@@ -201,7 +201,7 @@ export default {
newValue: priority,
from: 'Conversation Sidebar',
});
this.showAlert(
useAlert(
this.$t('CONVERSATION.PRIORITY.CHANGE_PRIORITY.SUCCESSFUL', {
priority: priorityItem.name,
conversationId,
@@ -79,7 +79,7 @@
<script>
import Spinner from 'shared/components/Spinner.vue';
import alertMixin from 'shared/mixins/alertMixin';
import { useAlert } from 'dashboard/composables';
import { mapGetters } from 'vuex';
import agentMixin from 'dashboard/mixins/agentMixin';
import ThumbnailGroup from 'dashboard/components/widgets/ThumbnailGroup.vue';
@@ -91,7 +91,7 @@ export default {
ThumbnailGroup,
MultiselectDropdownItems,
},
mixins: [alertMixin, agentMixin],
mixins: [agentMixin],
props: {
conversationId: {
type: [Number, String],
@@ -197,7 +197,7 @@ export default {
error?.message ||
this.$t('CONVERSATION_PARTICIPANTS.API.ERROR_MESSAGE');
} finally {
this.showAlert(alertMessage);
useAlert(alertMessage);
}
this.fetchParticipants();
},
@@ -1,5 +1,5 @@
<template>
<section class="conversation-page bg-white dark:bg-slate-900">
<section class="bg-white conversation-page dark:bg-slate-900">
<chat-list
:show-conversation-list="showConversationList"
:conversation-inbox="inboxId"
@@ -36,13 +36,13 @@
<script>
import { mapGetters } from 'vuex';
import { useAlert } from 'dashboard/composables';
import { useUISettings } from 'dashboard/composables/useUISettings';
import { getUnixTime } from 'date-fns';
import ChatList from '../../../components/ChatList.vue';
import ConversationBox from '../../../components/widgets/conversation/ConversationBox.vue';
import PopOverSearch from './search/PopOverSearch.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 { BUS_EVENTS } from 'shared/constants/busEvents';
import { CMD_SNOOZE_CONVERSATION } from 'dashboard/routes/dashboard/commands/commandBarBusEvents';
@@ -55,7 +55,6 @@ export default {
PopOverSearch,
CustomSnoozeModal,
},
mixins: [uiSettingsMixin, alertMixin],
props: {
inboxId: {
type: [String, Number],
@@ -82,6 +81,14 @@ export default {
default: 0,
},
},
setup() {
const { uiSettings, updateUISettings } = useUISettings();
return {
uiSettings,
updateUISettings,
};
},
data() {
return {
showSearchModal: false,
@@ -235,7 +242,7 @@ export default {
})
.then(() => {
this.$store.dispatch('setContextMenuChatId', null);
this.showAlert(this.$t('CONVERSATION.CHANGE_STATUS'));
useAlert(this.$t('CONVERSATION.CHANGE_STATUS'));
});
},
hideCustomSnoozeModal() {
@@ -3,7 +3,7 @@
<span class="overflow-hidden whitespace-nowrap text-ellipsis">{{
macro.name
}}</span>
<div class="macros-actions flex items-center gap-1">
<div class="flex items-center gap-1 macros-actions">
<woot-button
v-tooltip.left-start="$t('MACROS.EXECUTE.PREVIEW')"
size="tiny"
@@ -33,7 +33,7 @@
</template>
<script>
import alertMixin from 'shared/mixins/alertMixin';
import { useAlert } from 'dashboard/composables';
import MacroPreview from './MacroPreview.vue';
import { CONVERSATION_EVENTS } from '../../../../helper/AnalyticsHelper/events';
@@ -41,7 +41,6 @@ export default {
components: {
MacroPreview,
},
mixins: [alertMixin],
props: {
macro: {
type: Object,
@@ -67,9 +66,9 @@ export default {
conversationIds: [this.conversationId],
});
this.$track(CONVERSATION_EVENTS.EXECUTED_A_MACRO);
this.showAlert(this.$t('MACROS.EXECUTE.EXECUTED_SUCCESSFULLY'));
useAlert(this.$t('MACROS.EXECUTE.EXECUTED_SUCCESSFULLY'));
} catch (error) {
this.showAlert(this.$t('MACROS.ERROR'));
useAlert(this.$t('MACROS.ERROR'));
} finally {
this.isExecuting = false;
}
@@ -150,7 +150,7 @@
</template>
<script>
import alertMixin from 'shared/mixins/alertMixin';
import { useAlert } from 'dashboard/composables';
import {
DuplicateContactException,
ExceptionWithMessage,
@@ -161,7 +161,6 @@ import { isPhoneNumberValid } from 'shared/helpers/Validators';
import parsePhoneNumber from 'libphonenumber-js';
export default {
mixins: [alertMixin],
props: {
contact: {
type: Object,
@@ -375,20 +374,18 @@ export default {
try {
await this.onSubmit(this.getContactObject());
this.onSuccess();
this.showAlert(this.$t('CONTACT_FORM.SUCCESS_MESSAGE'));
useAlert(this.$t('CONTACT_FORM.SUCCESS_MESSAGE'));
} catch (error) {
if (error instanceof DuplicateContactException) {
if (error.data.includes('email')) {
this.showAlert(
this.$t('CONTACT_FORM.FORM.EMAIL_ADDRESS.DUPLICATE')
);
useAlert(this.$t('CONTACT_FORM.FORM.EMAIL_ADDRESS.DUPLICATE'));
} 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) {
this.showAlert(error.data);
useAlert(error.data);
} else {
this.showAlert(this.$t('CONTACT_FORM.ERROR_MESSAGE'));
useAlert(this.$t('CONTACT_FORM.ERROR_MESSAGE'));
}
}
},
@@ -400,15 +397,13 @@ export default {
try {
if (this.contact && this.contact.id) {
await this.$store.dispatch('contacts/deleteAvatar', this.contact.id);
this.showAlert(
this.$t('CONTACT_FORM.DELETE_AVATAR.API.SUCCESS_MESSAGE')
);
useAlert(this.$t('CONTACT_FORM.DELETE_AVATAR.API.SUCCESS_MESSAGE'));
}
this.avatarFile = null;
this.avatarUrl = '';
this.activeDialCode = '';
} catch (error) {
this.showAlert(
useAlert(
error.message
? error.message
: this.$t('CONTACT_FORM.DELETE_AVATAR.API.ERROR_MESSAGE')
@@ -1,7 +1,7 @@
<template>
<div class="relative items-center p-4 bg-white dark:bg-slate-900 w-full">
<div class="text-left rtl:text-right flex flex-col gap-2 w-full">
<div class="flex justify-between flex-row">
<div class="relative items-center w-full p-4 bg-white dark:bg-slate-900">
<div class="flex flex-col w-full gap-2 text-left rtl:text-right">
<div class="flex flex-row justify-between">
<thumbnail
v-if="showAvatar"
:src="contact.thumbnail"
@@ -18,9 +18,9 @@
</div>
<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
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 }}
</h3>
@@ -55,7 +55,7 @@
<p v-if="additionalAttributes.description" class="break-words mb-0.5">
{{ additionalAttributes.description }}
</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
:href="contact.email ? `mailto:${contact.email}` : ''"
:value="contact.email"
@@ -166,17 +166,16 @@
</div>
</template>
<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 Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
import SocialIcons from './SocialIcons.vue';
import EditContact from './EditContact.vue';
import NewConversation from './NewConversation.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 { BUS_EVENTS } from 'shared/constants/busEvents';
import {
@@ -194,7 +193,6 @@ export default {
NewConversation,
ContactMergeModal,
},
mixins: [alertMixin, adminMixin, timeMixin],
props: {
contact: {
type: Object,
@@ -217,6 +215,12 @@ export default {
default: 'chevron-right',
},
},
setup() {
const { isAdmin } = useAdmin();
return {
isAdmin,
};
},
data() {
return {
showEditModal: false,
@@ -260,6 +264,7 @@ export default {
},
},
methods: {
dynamicTime,
toggleMergeModal() {
this.showMergeModal = !this.showMergeModal;
},
@@ -300,7 +305,7 @@ export default {
try {
await this.$store.dispatch('contacts/delete', id);
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)) {
this.$router.push({
@@ -316,7 +321,7 @@ export default {
});
}
} catch (error) {
this.showAlert(
useAlert(
error.message
? error.message
: this.$t('DELETE_CONTACT.API.ERROR_MESSAGE')
@@ -1,5 +1,5 @@
<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
v-if="href"
:href="href"
@@ -9,16 +9,16 @@
:icon="icon"
:emoji="emoji"
icon-size="14"
class="ltr:ml-1 rtl:mr-1 flex-shrink-0"
class="flex-shrink-0 ltr:ml-1 rtl:mr-1"
/>
<span
v-if="value"
class="overflow-hidden whitespace-nowrap text-ellipsis text-sm"
class="overflow-hidden text-sm whitespace-nowrap text-ellipsis"
:title="value"
>
{{ value }}
</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')
}}</span>
@@ -42,22 +42,22 @@
:icon="icon"
:emoji="emoji"
icon-size="14"
class="ltr:ml-1 rtl:mr-1 flex-shrink-0"
class="flex-shrink-0 ltr:ml-1 rtl:mr-1"
/>
<span
v-if="value"
class="overflow-hidden whitespace-nowrap text-ellipsis text-sm"
class="overflow-hidden text-sm whitespace-nowrap text-ellipsis"
>
{{ value }}
</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')
}}</span>
</div>
</div>
</template>
<script>
import alertMixin from 'shared/mixins/alertMixin';
import { useAlert } from 'dashboard/composables';
import EmojiOrIcon from 'shared/components/EmojiOrIcon.vue';
import { copyTextToClipboard } from 'shared/helpers/clipboard';
@@ -65,7 +65,6 @@ export default {
components: {
EmojiOrIcon,
},
mixins: [alertMixin],
props: {
href: {
type: String,
@@ -92,7 +91,7 @@ export default {
async onCopy(e) {
e.preventDefault();
await copyTextToClipboard(this.value);
this.showAlert(this.$t('CONTACT_PANEL.COPY_SUCCESSFUL'));
useAlert(this.$t('CONTACT_PANEL.COPY_SUCCESSFUL'));
},
},
};
@@ -1,5 +1,5 @@
<template>
<form class="conversation--form w-full" @submit.prevent="onFormSubmit">
<form class="w-full conversation--form" @submit.prevent="onFormSubmit">
<div
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"
@@ -9,7 +9,7 @@
</p>
</div>
<div v-else>
<div class="gap-2 flex flex-row">
<div class="flex flex-row gap-2">
<div class="w-[50%]">
<label>
{{ $t('NEW_CONVERSATION.FORM.INBOX.LABEL') }}
@@ -69,7 +69,7 @@
:status="contact.availability_status"
/>
<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 }}
</h4>
@@ -128,7 +128,7 @@
v-if="isSignatureEnabledForInbox && !messageSignature"
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
v-tooltip.top-end="signatureToggleTooltip"
icon="signature"
@@ -191,7 +191,7 @@
{{ $t('NEW_CONVERSATION.FORM.ATTACHMENTS.SELECT') }}
</woot-button>
<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') }}
</span>
@@ -213,7 +213,7 @@
<div
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">
{{ $t('NEW_CONVERSATION.FORM.CANCEL') }}
@@ -226,7 +226,7 @@
<transition v-if="isEmailOrWebWidgetInbox" name="modal-fade">
<div
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" />
<h4 class="text-2xl break-words text-slate-600 dark:text-slate-200">
@@ -239,6 +239,8 @@
<script>
import { mapGetters } from 'vuex';
import { useAlert } from 'dashboard/composables';
import { useUISettings } from 'dashboard/composables/useUISettings';
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.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 InboxDropdownItem from 'dashboard/components/widgets/InboxDropdownItem.vue';
import WhatsappTemplates from './WhatsappTemplates.vue';
import alertMixin from 'shared/mixins/alertMixin';
import { INBOX_TYPES } from 'shared/mixins/inboxMixin';
import { ExceptionWithMessage } from 'shared/helpers/CustomErrors';
import { getInboxSource } from 'dashboard/helper/inbox';
@@ -260,7 +261,6 @@ import {
appendSignature,
removeSignature,
} from 'dashboard/helper/editorHelper';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
export default {
components: {
@@ -274,7 +274,7 @@ export default {
AttachmentPreview,
MessageSignatureMissingAlert,
},
mixins: [alertMixin, uiSettingsMixin, inboxMixin, fileUploadMixin],
mixins: [inboxMixin, fileUploadMixin],
props: {
contact: {
type: Object,
@@ -289,6 +289,15 @@ export default {
default: '',
},
},
setup() {
const { fetchSignatureFlagFromUISettings, setSignatureFlagForInbox } =
useUISettings();
return {
fetchSignatureFlagFromUISettings,
setSignatureFlagForInbox,
};
},
data() {
return {
name: '',
@@ -323,7 +332,7 @@ export default {
messageSignature: 'getMessageSignature',
}),
sendWithSignature() {
return this.fetchSignatureFlagFromUiSettings(this.channelType);
return this.fetchSignatureFlagFromUISettings(this.channelType);
},
signatureToApply() {
return this.messageSignature;
@@ -515,15 +524,12 @@ export default {
message: this.$t('NEW_CONVERSATION.FORM.GO_TO_CONVERSATION'),
};
this.onSuccess();
this.showAlert(
this.$t('NEW_CONVERSATION.FORM.SUCCESS_MESSAGE'),
action
);
useAlert(this.$t('NEW_CONVERSATION.FORM.SUCCESS_MESSAGE'), action);
} catch (error) {
if (error instanceof ExceptionWithMessage) {
this.showAlert(error.data);
useAlert(error.data);
} else {
this.showAlert(this.$t('NEW_CONVERSATION.FORM.ERROR_MESSAGE'));
useAlert(this.$t('NEW_CONVERSATION.FORM.ERROR_MESSAGE'));
}
}
},
@@ -41,17 +41,17 @@
</template>
<script>
import CustomAttribute from 'dashboard/components/CustomAttribute.vue';
import alertMixin from 'shared/mixins/alertMixin';
import attributeMixin from 'dashboard/mixins/attributeMixin';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import { useAlert } from 'dashboard/composables';
import { useUISettings } from 'dashboard/composables/useUISettings';
import { copyTextToClipboard } from 'shared/helpers/clipboard';
import CustomAttribute from 'dashboard/components/CustomAttribute.vue';
import attributeMixin from 'dashboard/mixins/attributeMixin';
export default {
components: {
CustomAttribute,
},
mixins: [alertMixin, attributeMixin, uiSettingsMixin],
mixins: [attributeMixin],
props: {
attributeType: {
type: String,
@@ -71,6 +71,14 @@ export default {
default: '',
},
},
setup() {
const { uiSettings, updateUISettings } = useUISettings();
return {
uiSettings,
updateUISettings,
};
},
data() {
return {
showAllAttributes: false,
@@ -141,12 +149,12 @@ export default {
custom_attributes: updatedAttributes,
});
}
this.showAlert(this.$t('CUSTOM_ATTRIBUTES.FORM.UPDATE.SUCCESS'));
useAlert(this.$t('CUSTOM_ATTRIBUTES.FORM.UPDATE.SUCCESS'));
} catch (error) {
const errorMessage =
error?.response?.message ||
this.$t('CUSTOM_ATTRIBUTES.FORM.UPDATE.ERROR');
this.showAlert(errorMessage);
useAlert(errorMessage);
}
},
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) {
const errorMessage =
error?.response?.message ||
this.$t('CUSTOM_ATTRIBUTES.FORM.DELETE.ERROR');
this.showAlert(errorMessage);
useAlert(errorMessage);
}
},
async onCopy(attributeValue) {
await copyTextToClipboard(attributeValue);
this.showAlert(this.$t('CUSTOM_ATTRIBUTES.COPY_SUCCESSFUL'));
useAlert(this.$t('CUSTOM_ATTRIBUTES.COPY_SUCCESSFUL'));
},
},
};
@@ -45,10 +45,10 @@
<script>
import { mapGetters } from 'vuex';
import { useAdmin } from 'dashboard/composables/useAdmin';
import Spinner from 'shared/components/Spinner.vue';
import LabelDropdown from 'shared/components/ui/label/LabelDropdown.vue';
import AddLabel from 'shared/components/ui/dropdown/AddLabel.vue';
import adminMixin from 'dashboard/mixins/isAdmin';
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
import conversationLabelMixin from 'dashboard/mixins/conversation/labelMixin';
@@ -59,14 +59,19 @@ export default {
AddLabel,
},
mixins: [conversationLabelMixin, adminMixin, keyboardEventListenerMixins],
mixins: [conversationLabelMixin, keyboardEventListenerMixins],
props: {
conversationId: {
type: Number,
required: true,
},
},
setup() {
const { isAdmin } = useAdmin();
return {
isAdmin,
};
},
data() {
return {
selectedLabels: [],
@@ -9,7 +9,7 @@
/>
<router-link
: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">
<fluent-icon
@@ -19,7 +19,7 @@
/>
</div>
<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') }}
</p>
@@ -34,7 +34,6 @@
<script>
import { mapGetters } from 'vuex';
import timeMixin from '../../../../mixins/time';
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
import SwitchLayout from './SwitchLayout.vue';
import { frontendURL } from 'dashboard/helper/URLHelper';
@@ -49,7 +48,7 @@ export default {
},
},
},
mixins: [timeMixin, messageFormatterMixin],
mixins: [messageFormatterMixin],
props: {
isOnExpandedLayout: {
type: Boolean,
@@ -30,11 +30,10 @@
<script>
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';
export default {
mixins: [alertMixin],
props: {
filterType: {
type: Number,
@@ -101,7 +100,7 @@ export default {
? errorMessage
: this.$t('FILTER.CUSTOM_VIEWS.ADD.API_SEGMENTS.ERROR_MESSAGE');
} finally {
this.showAlert(this.alertMessage);
useAlert(this.alertMessage);
}
this.openLastSavedItem();
},
@@ -16,10 +16,9 @@
</template>
<script>
import alertMixin from 'shared/mixins/alertMixin';
import { useAlert } from 'dashboard/composables';
import { CONTACTS_EVENTS } from '../../../helper/AnalyticsHelper/events';
export default {
mixins: [alertMixin],
props: {
showDeletePopup: {
type: Boolean,
@@ -79,7 +78,7 @@ export default {
const filterType = this.activeCustomViews;
await this.$store.dispatch('customViews/delete', { id, filterType });
this.closeDeletePopup();
this.showAlert(
useAlert(
this.activeFilterType === 0
? this.$t('FILTER.CUSTOM_VIEWS.DELETE.API_FOLDERS.SUCCESS_MESSAGE')
: this.$t('FILTER.CUSTOM_VIEWS.DELETE.API_SEGMENTS.SUCCESS_MESSAGE')
@@ -94,7 +93,7 @@ export default {
: this.$t(
'FILTER.CUSTOM_VIEWS.DELETE.API_SEGMENTS.SUCCESS_MESSAGE'
);
this.showAlert(errorMessage);
useAlert(errorMessage);
}
this.openLastItemAfterDelete();
},
@@ -24,7 +24,7 @@
</label>
<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">
{{ $t('HELP_CENTER.PORTAL.ADD_LOCALE.BUTTONS.CANCEL') }}
</woot-button>
@@ -39,16 +39,15 @@
</template>
<script>
import Modal from 'dashboard/components/Modal.vue';
import alertMixin from 'shared/mixins/alertMixin';
import { useAlert } from 'dashboard/composables';
import { required } from 'vuelidate/lib/validators';
import allLocales from 'shared/constants/locales.js';
import { PORTALS_EVENTS } from '../../../../helper/AnalyticsHelper/events';
import Modal from 'dashboard/components/Modal.vue';
export default {
components: {
Modal,
},
mixins: [alertMixin],
props: {
show: {
type: Boolean,
@@ -120,7 +119,7 @@ export default {
error?.message ||
this.$t('HELP_CENTER.PORTAL.ADD_LOCALE.API.ERROR_MESSAGE');
} finally {
this.showAlert(this.alertMessage);
useAlert(this.alertMessage);
this.isUpdating = false;
}
},
@@ -1,12 +1,12 @@
<template>
<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
v-if="showDragIcon"
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"
/>
<div class="flex flex-col truncate">
@@ -18,7 +18,7 @@
{{ title }}
</h6>
</router-link>
<div class="flex gap-1 items-center">
<div class="flex items-center gap-1">
<Thumbnail
v-if="author"
:src="author.thumbnail"
@@ -39,7 +39,7 @@
class="text-woot-300 dark:text-woot-300"
/>
</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 }}
</span>
</div>
@@ -60,7 +60,7 @@
:title="formattedViewCount"
>
{{ readableViewCount }}
<span class="lg:hidden ml-1">
<span class="ml-1 lg:hidden">
{{ ` ${$t('HELP_CENTER.TABLE.HEADERS.READ_COUNT')}` }}
</span>
</span>
@@ -74,7 +74,7 @@
/>
</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 }}
</span>
@@ -82,7 +82,7 @@
</template>
<script>
import timeMixin from 'dashboard/mixins/time';
import { dynamicTime } from 'shared/helpers/timeHelper';
import portalMixin from '../mixins/portalMixin';
import { frontendURL } from 'dashboard/helper/URLHelper';
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
@@ -91,7 +91,7 @@ export default {
components: {
Thumbnail,
},
mixins: [timeMixin, portalMixin],
mixins: [portalMixin],
props: {
showDragIcon: {
type: Boolean,
@@ -131,7 +131,7 @@ export default {
computed: {
lastUpdatedAt() {
return this.dynamicTime(this.updatedAt);
return dynamicTime(this.updatedAt);
},
formattedViewCount() {
return Number(this.views || 0).toLocaleString('en');
@@ -1,17 +1,17 @@
<template>
<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"
>
<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 }}
</h4>
<div class="flex content-between items-center gap-0.5 w-full">
<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 }}
{{ ` / ` }}
@@ -42,12 +42,11 @@
</template>
<script>
import { useAlert } from 'dashboard/composables';
import { copyTextToClipboard } from 'shared/helpers/clipboard';
import alertMixin from 'shared/mixins/alertMixin';
export default {
name: 'ArticleSearchResultItem',
mixins: [alertMixin],
props: {
id: {
type: Number,
@@ -86,7 +85,7 @@ export default {
async handleCopy(e) {
e.stopPropagation();
await copyTextToClipboard(this.url);
this.showAlert(this.$t('CONTACT_PANEL.COPY_SUCCESSFUL'));
useAlert(this.$t('CONTACT_PANEL.COPY_SUCCESSFUL'));
},
},
};
@@ -1,6 +1,6 @@
<template>
<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
v-on-clickaway="onClose"
@@ -34,6 +34,7 @@
<script>
import { debounce } from '@chatwoot/utils';
import { useAlert } from 'dashboard/composables';
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
import SearchHeader from './Header.vue';
@@ -42,7 +43,6 @@ import ArticleView from './ArticleView.vue';
import ArticlesAPI from 'dashboard/api/helpCenter/articles';
import { buildPortalArticleURL } from 'dashboard/helper/portalHelper';
import portalMixin from '../../mixins/portalMixin';
import alertMixin from 'shared/mixins/alertMixin';
export default {
name: 'ArticleSearchPopover',
@@ -51,7 +51,7 @@ export default {
SearchResults,
ArticleView,
},
mixins: [portalMixin, alertMixin, keyboardEventListenerMixins],
mixins: [portalMixin, keyboardEventListenerMixins],
props: {
selectedPortalSlug: {
type: String,
@@ -145,9 +145,7 @@ export default {
const article = this.activeArticle(id || this.activeId);
this.$emit('insert', article);
this.showAlert(
this.$t('HELP_CENTER.ARTICLE_SEARCH.SUCCESS_ARTICLE_INSERTED')
);
useAlert(this.$t('HELP_CENTER.ARTICLE_SEARCH.SUCCESS_ARTICLE_INSERTED'));
this.onClose();
},
getKeyboardEvents() {
@@ -108,14 +108,13 @@
</template>
<script>
import alertMixin from 'shared/mixins/alertMixin';
import { useAlert } from 'dashboard/composables';
import wootConstants from 'dashboard/constants/globals';
import { PORTALS_EVENTS } from '../../../../../helper/AnalyticsHelper/events';
const { ARTICLE_STATUS_TYPES } = wootConstants;
export default {
mixins: [alertMixin],
props: {
isSidebarOpen: {
type: Boolean,
@@ -200,7 +199,7 @@ export default {
this.alertMessage =
error?.message || this.statusUpdateErrorMessage(status);
} finally {
this.showAlert(this.alertMessage);
useAlert(this.alertMessage);
}
},
statusUpdateSuccessMessage(status) {

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