Merge branch 'develop' into fix/show-error-on-clipboard-errors
This commit is contained in:
@@ -1,11 +0,0 @@
|
||||
import App from './App';
|
||||
import '../../test-matchers';
|
||||
|
||||
describe(`App component`, () => {
|
||||
it(`should be a component`, () => {
|
||||
// Arrange
|
||||
// Act
|
||||
expect(App).toBeVueComponent('App');
|
||||
// Assert
|
||||
});
|
||||
});
|
||||
@@ -15,10 +15,10 @@ describe('#enterpriseAccountAPI', () => {
|
||||
describe('API calls', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
post: vi.fn(() => Promise.resolve()),
|
||||
get: vi.fn(() => Promise.resolve()),
|
||||
patch: vi.fn(() => Promise.resolve()),
|
||||
delete: vi.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -15,10 +15,10 @@ describe('#accountAPI', () => {
|
||||
describe('API calls', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
post: vi.fn(() => Promise.resolve()),
|
||||
get: vi.fn(() => Promise.resolve()),
|
||||
patch: vi.fn(() => Promise.resolve()),
|
||||
delete: vi.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -10,10 +10,10 @@ describe('#ContactsAPI', () => {
|
||||
describe('API calls', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
post: vi.fn(() => Promise.resolve()),
|
||||
get: vi.fn(() => Promise.resolve()),
|
||||
patch: vi.fn(() => Promise.resolve()),
|
||||
delete: vi.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -14,7 +14,7 @@ describe('#AgentAPI', () => {
|
||||
describe('API calls', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
post: vi.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -14,10 +14,10 @@ describe('#PortalAPI', () => {
|
||||
describe('API calls', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
post: vi.fn(() => Promise.resolve()),
|
||||
get: vi.fn(() => Promise.resolve()),
|
||||
patch: vi.fn(() => Promise.resolve()),
|
||||
delete: vi.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -44,10 +44,10 @@ describe('#PortalAPI', () => {
|
||||
describe('API calls', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
post: vi.fn(() => Promise.resolve()),
|
||||
get: vi.fn(() => Promise.resolve()),
|
||||
patch: vi.fn(() => Promise.resolve()),
|
||||
delete: vi.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -71,10 +71,10 @@ describe('#PortalAPI', () => {
|
||||
describe('API calls', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
post: vi.fn(() => Promise.resolve()),
|
||||
get: vi.fn(() => Promise.resolve()),
|
||||
patch: vi.fn(() => Promise.resolve()),
|
||||
delete: vi.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -98,10 +98,10 @@ describe('#PortalAPI', () => {
|
||||
describe('API calls', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
post: vi.fn(() => Promise.resolve()),
|
||||
get: vi.fn(() => Promise.resolve()),
|
||||
patch: vi.fn(() => Promise.resolve()),
|
||||
delete: vi.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -129,10 +129,10 @@ describe('#PortalAPI', () => {
|
||||
describe('API calls', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
post: vi.fn(() => Promise.resolve()),
|
||||
get: vi.fn(() => Promise.resolve()),
|
||||
patch: vi.fn(() => Promise.resolve()),
|
||||
delete: vi.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -4,10 +4,10 @@ describe('#AssignableAgentsAPI', () => {
|
||||
describe('API calls', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
post: vi.fn(() => Promise.resolve()),
|
||||
get: vi.fn(() => Promise.resolve()),
|
||||
patch: vi.fn(() => Promise.resolve()),
|
||||
delete: vi.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -13,10 +13,10 @@ describe('#FBChannel', () => {
|
||||
describe('API calls', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
post: vi.fn(() => Promise.resolve()),
|
||||
get: vi.fn(() => Promise.resolve()),
|
||||
patch: vi.fn(() => Promise.resolve()),
|
||||
delete: vi.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -17,10 +17,10 @@ describe('#ContactsAPI', () => {
|
||||
describe('API calls', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
post: vi.fn(() => Promise.resolve()),
|
||||
get: vi.fn(() => Promise.resolve()),
|
||||
patch: vi.fn(() => Promise.resolve()),
|
||||
delete: vi.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -16,10 +16,10 @@ describe('#ConversationApi', () => {
|
||||
describe('API calls', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
post: vi.fn(() => Promise.resolve()),
|
||||
get: vi.fn(() => Promise.resolve()),
|
||||
patch: vi.fn(() => Promise.resolve()),
|
||||
delete: vi.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -11,10 +11,10 @@ describe('#Reports API', () => {
|
||||
describe('API calls', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
post: vi.fn(() => Promise.resolve()),
|
||||
get: vi.fn(() => Promise.resolve()),
|
||||
patch: vi.fn(() => Promise.resolve()),
|
||||
delete: vi.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -24,10 +24,10 @@ describe('#ConversationAPI', () => {
|
||||
describe('API calls', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
post: vi.fn(() => Promise.resolve()),
|
||||
get: vi.fn(() => Promise.resolve()),
|
||||
patch: vi.fn(() => Promise.resolve()),
|
||||
delete: vi.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -15,10 +15,10 @@ describe('#ConversationAPI', () => {
|
||||
describe('API calls', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
post: vi.fn(() => Promise.resolve()),
|
||||
get: vi.fn(() => Promise.resolve()),
|
||||
patch: vi.fn(() => Promise.resolve()),
|
||||
delete: vi.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -17,10 +17,10 @@ describe('#InboxesAPI', () => {
|
||||
describe('API calls', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
post: vi.fn(() => Promise.resolve()),
|
||||
get: vi.fn(() => Promise.resolve()),
|
||||
patch: vi.fn(() => Promise.resolve()),
|
||||
delete: vi.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -18,10 +18,10 @@ describe('#integrationAPI', () => {
|
||||
describe('API calls', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
post: vi.fn(() => Promise.resolve()),
|
||||
get: vi.fn(() => Promise.resolve()),
|
||||
patch: vi.fn(() => Promise.resolve()),
|
||||
delete: vi.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -11,10 +11,10 @@ describe('#accountAPI', () => {
|
||||
describe('createAMeeting', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
post: vi.fn(() => Promise.resolve()),
|
||||
get: vi.fn(() => Promise.resolve()),
|
||||
patch: vi.fn(() => Promise.resolve()),
|
||||
delete: vi.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -39,10 +39,10 @@ describe('#accountAPI', () => {
|
||||
describe('addParticipantToMeeting', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
post: vi.fn(() => Promise.resolve()),
|
||||
get: vi.fn(() => Promise.resolve()),
|
||||
patch: vi.fn(() => Promise.resolve()),
|
||||
delete: vi.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -16,10 +16,10 @@ describe('#linearAPI', () => {
|
||||
describe('getTeams', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
post: vi.fn(() => Promise.resolve()),
|
||||
get: vi.fn(() => Promise.resolve()),
|
||||
patch: vi.fn(() => Promise.resolve()),
|
||||
delete: vi.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -41,10 +41,10 @@ describe('#linearAPI', () => {
|
||||
describe('getTeamEntities', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
post: vi.fn(() => Promise.resolve()),
|
||||
get: vi.fn(() => Promise.resolve()),
|
||||
patch: vi.fn(() => Promise.resolve()),
|
||||
delete: vi.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -66,10 +66,10 @@ describe('#linearAPI', () => {
|
||||
describe('createIssue', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
post: vi.fn(() => Promise.resolve()),
|
||||
get: vi.fn(() => Promise.resolve()),
|
||||
patch: vi.fn(() => Promise.resolve()),
|
||||
delete: vi.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -96,10 +96,10 @@ describe('#linearAPI', () => {
|
||||
describe('link_issue', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
post: vi.fn(() => Promise.resolve()),
|
||||
get: vi.fn(() => Promise.resolve()),
|
||||
patch: vi.fn(() => Promise.resolve()),
|
||||
delete: vi.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -125,10 +125,10 @@ describe('#linearAPI', () => {
|
||||
describe('getLinkedIssue', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
post: vi.fn(() => Promise.resolve()),
|
||||
get: vi.fn(() => Promise.resolve()),
|
||||
patch: vi.fn(() => Promise.resolve()),
|
||||
delete: vi.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -150,10 +150,10 @@ describe('#linearAPI', () => {
|
||||
describe('unlinkIssue', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
post: vi.fn(() => Promise.resolve()),
|
||||
get: vi.fn(() => Promise.resolve()),
|
||||
patch: vi.fn(() => Promise.resolve()),
|
||||
delete: vi.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -178,10 +178,10 @@ describe('#linearAPI', () => {
|
||||
describe('searchIssues', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
post: vi.fn(() => Promise.resolve()),
|
||||
get: vi.fn(() => Promise.resolve()),
|
||||
patch: vi.fn(() => Promise.resolve()),
|
||||
delete: vi.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -13,10 +13,10 @@ describe('#NotificationAPI', () => {
|
||||
describe('API calls', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
post: vi.fn(() => Promise.resolve()),
|
||||
get: vi.fn(() => Promise.resolve()),
|
||||
patch: vi.fn(() => Promise.resolve()),
|
||||
delete: vi.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -20,10 +20,10 @@ describe('#Reports API', () => {
|
||||
describe('API calls', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
post: vi.fn(() => Promise.resolve()),
|
||||
get: vi.fn(() => Promise.resolve()),
|
||||
patch: vi.fn(() => Promise.resolve()),
|
||||
delete: vi.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -12,10 +12,10 @@ describe('#SLAReports API', () => {
|
||||
describe('API calls', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
post: vi.fn(() => Promise.resolve()),
|
||||
get: vi.fn(() => Promise.resolve()),
|
||||
patch: vi.fn(() => Promise.resolve()),
|
||||
delete: vi.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -16,10 +16,10 @@ describe('#TeamsAPI', () => {
|
||||
describe('API calls', () => {
|
||||
const originalAxios = window.axios;
|
||||
const axiosMock = {
|
||||
post: jest.fn(() => Promise.resolve()),
|
||||
get: jest.fn(() => Promise.resolve()),
|
||||
patch: jest.fn(() => Promise.resolve()),
|
||||
delete: jest.fn(() => Promise.resolve()),
|
||||
post: vi.fn(() => Promise.resolve()),
|
||||
get: vi.fn(() => Promise.resolve()),
|
||||
patch: vi.fn(() => Promise.resolve()),
|
||||
delete: vi.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -111,15 +111,6 @@
|
||||
@updateFolder="onUpdateSavedFilter"
|
||||
/>
|
||||
</woot-modal>
|
||||
<woot-modal
|
||||
:show.sync="showCustomSnoozeModal"
|
||||
:on-close="hideCustomSnoozeModal"
|
||||
>
|
||||
<custom-snooze-modal
|
||||
@close="hideCustomSnoozeModal"
|
||||
@choose-time="chooseSnoozeTime"
|
||||
/>
|
||||
</woot-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -131,7 +122,6 @@ 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';
|
||||
@@ -152,10 +142,6 @@ import {
|
||||
isOnUnattendedView,
|
||||
} from '../store/modules/conversations/helpers/actionHelpers';
|
||||
import { CONVERSATION_EVENTS } from '../helper/AnalyticsHelper/events';
|
||||
import { CMD_SNOOZE_CONVERSATION } from 'dashboard/routes/dashboard/commands/commandBarBusEvents';
|
||||
import { findSnoozeTime } from 'dashboard/helper/snoozeHelpers';
|
||||
import { getUnixTime } from 'date-fns';
|
||||
import CustomSnoozeModal from 'dashboard/components/CustomSnoozeModal.vue';
|
||||
import IntersectionObserver from './IntersectionObserver.vue';
|
||||
|
||||
export default {
|
||||
@@ -170,10 +156,8 @@ export default {
|
||||
ConversationBulkActions,
|
||||
IntersectionObserver,
|
||||
VirtualList,
|
||||
CustomSnoozeModal,
|
||||
},
|
||||
mixins: [
|
||||
timeMixin,
|
||||
conversationMixin,
|
||||
keyboardEventListenerMixins,
|
||||
alertMixin,
|
||||
@@ -247,7 +231,6 @@ export default {
|
||||
root: this.$refs.conversationList,
|
||||
rootMargin: '100px 0px 100px 0px',
|
||||
},
|
||||
showCustomSnoozeModal: false,
|
||||
|
||||
itemComponent: ConversationItem,
|
||||
// virtualListExtraProps is to pass the props to the conversationItem component.
|
||||
@@ -283,7 +266,6 @@ export default {
|
||||
campaigns: 'campaigns/getAllCampaigns',
|
||||
labels: 'labels/getLabels',
|
||||
selectedConversations: 'bulkActions/getSelectedConversationIds',
|
||||
contextMenuChatId: 'getContextMenuChatId',
|
||||
}),
|
||||
hasAppliedFilters() {
|
||||
return this.appliedFilters.length !== 0;
|
||||
@@ -517,11 +499,6 @@ export default {
|
||||
this.$emitter.on('fetch_conversation_stats', () => {
|
||||
this.$store.dispatch('conversationStats/get', this.conversationFilters);
|
||||
});
|
||||
|
||||
this.$emitter.on(CMD_SNOOZE_CONVERSATION, this.onCmdSnoozeConversation);
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.$emitter.off(CMD_SNOOZE_CONVERSATION, this.onCmdSnoozeConversation);
|
||||
},
|
||||
methods: {
|
||||
updateVirtualListProps(key, value) {
|
||||
@@ -999,43 +976,6 @@ export default {
|
||||
onContextMenuToggle(state) {
|
||||
this.isContextMenuOpen = state;
|
||||
},
|
||||
onCmdSnoozeConversation(snoozeType) {
|
||||
if (snoozeType === wootConstants.SNOOZE_OPTIONS.UNTIL_CUSTOM_TIME) {
|
||||
this.showCustomSnoozeModal = true;
|
||||
} else {
|
||||
this.toggleStatus(
|
||||
wootConstants.STATUS_TYPE.SNOOZED,
|
||||
findSnoozeTime(snoozeType) || null
|
||||
);
|
||||
}
|
||||
},
|
||||
chooseSnoozeTime(customSnoozeTime) {
|
||||
this.showCustomSnoozeModal = false;
|
||||
if (customSnoozeTime) {
|
||||
this.toggleStatus(
|
||||
wootConstants.STATUS_TYPE.SNOOZED,
|
||||
getUnixTime(customSnoozeTime)
|
||||
);
|
||||
}
|
||||
},
|
||||
toggleStatus(status, snoozedUntil) {
|
||||
this.$store
|
||||
.dispatch('toggleStatus', {
|
||||
conversationId: this.currentChat?.id || this.contextMenuChatId,
|
||||
status,
|
||||
snoozedUntil,
|
||||
})
|
||||
.then(() => {
|
||||
this.$store.dispatch('setContextMenuChatId', null);
|
||||
this.showAlert(this.$t('CONVERSATION.CHANGE_STATUS'));
|
||||
});
|
||||
},
|
||||
hideCustomSnoozeModal() {
|
||||
// if we select custom snooze and then the custom snooze modal is open
|
||||
// Then if the custom snooze modal is closed and set the context menu chat id to null
|
||||
this.$store.dispatch('setContextMenuChatId', null);
|
||||
this.showCustomSnoozeModal = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
:teams="teams"
|
||||
:custom-views="customViews"
|
||||
:menu-config="activeSecondaryMenu"
|
||||
:current-role="currentRole"
|
||||
:current-user="currentUser"
|
||||
:is-on-chatwoot-cloud="isOnChatwootCloud"
|
||||
@add-label="showAddLabelPopup"
|
||||
@toggle-accounts="toggleAccountModal"
|
||||
@@ -37,7 +37,8 @@ import alertMixin from 'shared/mixins/alertMixin';
|
||||
import PrimarySidebar from './sidebarComponents/Primary.vue';
|
||||
import SecondarySidebar from './sidebarComponents/Secondary.vue';
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
import router from '../../routes';
|
||||
import router, { routesWithPermissions } from '../../routes';
|
||||
import { hasPermissions } from '../../helper/permissionsHelper';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -98,9 +99,13 @@ export default {
|
||||
return getSidebarItems(this.accountId);
|
||||
},
|
||||
primaryMenuItems() {
|
||||
const userPermissions = this.currentUser.permissions;
|
||||
const menuItems = this.sideMenuConfig.primaryMenu;
|
||||
return menuItems.filter(menuItem => {
|
||||
const isAvailableForTheUser = menuItem.roles.includes(this.currentRole);
|
||||
const isAvailableForTheUser = hasPermissions(
|
||||
routesWithPermissions[menuItem.toStateName],
|
||||
userPermissions
|
||||
);
|
||||
|
||||
if (!isAvailableForTheUser) {
|
||||
return false;
|
||||
|
||||
@@ -9,7 +9,6 @@ const primaryMenuItems = accountId => [
|
||||
featureFlag: FEATURE_FLAGS.INBOX_VIEW,
|
||||
toState: frontendURL(`accounts/${accountId}/inbox-view`),
|
||||
toStateName: 'inbox_view',
|
||||
roles: ['administrator', 'agent'],
|
||||
},
|
||||
{
|
||||
icon: 'chat',
|
||||
@@ -17,7 +16,6 @@ const primaryMenuItems = accountId => [
|
||||
label: 'CONVERSATIONS',
|
||||
toState: frontendURL(`accounts/${accountId}/dashboard`),
|
||||
toStateName: 'home',
|
||||
roles: ['administrator', 'agent'],
|
||||
},
|
||||
{
|
||||
icon: 'book-contacts',
|
||||
@@ -26,7 +24,6 @@ const primaryMenuItems = accountId => [
|
||||
featureFlag: FEATURE_FLAGS.CRM,
|
||||
toState: frontendURL(`accounts/${accountId}/contacts`),
|
||||
toStateName: 'contacts_dashboard',
|
||||
roles: ['administrator', 'agent'],
|
||||
},
|
||||
{
|
||||
icon: 'arrow-trending-lines',
|
||||
@@ -34,8 +31,7 @@ const primaryMenuItems = accountId => [
|
||||
label: 'REPORTS',
|
||||
featureFlag: FEATURE_FLAGS.REPORTS,
|
||||
toState: frontendURL(`accounts/${accountId}/reports`),
|
||||
toStateName: 'settings_account_reports',
|
||||
roles: ['administrator'],
|
||||
toStateName: 'account_overview_reports',
|
||||
},
|
||||
{
|
||||
icon: 'megaphone',
|
||||
@@ -44,7 +40,6 @@ const primaryMenuItems = accountId => [
|
||||
featureFlag: FEATURE_FLAGS.CAMPAIGNS,
|
||||
toState: frontendURL(`accounts/${accountId}/campaigns`),
|
||||
toStateName: 'ongoing_campaigns',
|
||||
roles: ['administrator'],
|
||||
},
|
||||
{
|
||||
icon: 'library',
|
||||
@@ -54,7 +49,6 @@ const primaryMenuItems = accountId => [
|
||||
alwaysVisibleOnChatwootInstances: true,
|
||||
toState: frontendURL(`accounts/${accountId}/portals`),
|
||||
toStateName: 'default_portal_articles',
|
||||
roles: ['administrator'],
|
||||
},
|
||||
{
|
||||
icon: 'settings',
|
||||
@@ -62,7 +56,6 @@ const primaryMenuItems = accountId => [
|
||||
label: 'SETTINGS',
|
||||
toState: frontendURL(`accounts/${accountId}/settings`),
|
||||
toStateName: 'settings_home',
|
||||
roles: ['administrator', 'agent'],
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ const settings = accountId => ({
|
||||
'settings_inbox_list',
|
||||
'settings_inbox_new',
|
||||
'settings_inbox_show',
|
||||
'settings_inbox',
|
||||
'settings_inboxes_add_agents',
|
||||
'settings_inboxes_page_channel',
|
||||
'settings_integrations_dashboard_apps',
|
||||
@@ -46,6 +45,9 @@ const settings = accountId => ({
|
||||
icon: 'briefcase',
|
||||
label: 'ACCOUNT_SETTINGS',
|
||||
hasSubMenu: false,
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
toState: frontendURL(`accounts/${accountId}/settings/general`),
|
||||
toStateName: 'general_settings_index',
|
||||
},
|
||||
@@ -53,6 +55,9 @@ const settings = accountId => ({
|
||||
icon: 'people',
|
||||
label: 'AGENTS',
|
||||
hasSubMenu: false,
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
toState: frontendURL(`accounts/${accountId}/settings/agents/list`),
|
||||
toStateName: 'agent_list',
|
||||
featureFlag: FEATURE_FLAGS.AGENT_MANAGEMENT,
|
||||
@@ -61,6 +66,9 @@ const settings = accountId => ({
|
||||
icon: 'people-team',
|
||||
label: 'TEAMS',
|
||||
hasSubMenu: false,
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
toState: frontendURL(`accounts/${accountId}/settings/teams/list`),
|
||||
toStateName: 'settings_teams_list',
|
||||
featureFlag: FEATURE_FLAGS.TEAM_MANAGEMENT,
|
||||
@@ -69,6 +77,9 @@ const settings = accountId => ({
|
||||
icon: 'mail-inbox-all',
|
||||
label: 'INBOXES',
|
||||
hasSubMenu: false,
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
toState: frontendURL(`accounts/${accountId}/settings/inboxes/list`),
|
||||
toStateName: 'settings_inbox_list',
|
||||
featureFlag: FEATURE_FLAGS.INBOX_MANAGEMENT,
|
||||
@@ -77,6 +88,9 @@ const settings = accountId => ({
|
||||
icon: 'tag',
|
||||
label: 'LABELS',
|
||||
hasSubMenu: false,
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
toState: frontendURL(`accounts/${accountId}/settings/labels/list`),
|
||||
toStateName: 'labels_list',
|
||||
featureFlag: FEATURE_FLAGS.LABELS,
|
||||
@@ -85,6 +99,9 @@ const settings = accountId => ({
|
||||
icon: 'code',
|
||||
label: 'CUSTOM_ATTRIBUTES',
|
||||
hasSubMenu: false,
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
toState: frontendURL(
|
||||
`accounts/${accountId}/settings/custom-attributes/list`
|
||||
),
|
||||
@@ -95,6 +112,9 @@ const settings = accountId => ({
|
||||
icon: 'automation',
|
||||
label: 'AUTOMATION',
|
||||
hasSubMenu: false,
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
toState: frontendURL(`accounts/${accountId}/settings/automation/list`),
|
||||
toStateName: 'automation_list',
|
||||
featureFlag: FEATURE_FLAGS.AUTOMATIONS,
|
||||
@@ -103,6 +123,9 @@ const settings = accountId => ({
|
||||
icon: 'bot',
|
||||
label: 'AGENT_BOTS',
|
||||
hasSubMenu: false,
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
globalConfigFlag: 'csmlEditorHost',
|
||||
toState: frontendURL(`accounts/${accountId}/settings/agent-bots`),
|
||||
toStateName: 'agent_bots',
|
||||
@@ -112,6 +135,9 @@ const settings = accountId => ({
|
||||
icon: 'flash-settings',
|
||||
label: 'MACROS',
|
||||
hasSubMenu: false,
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
toState: frontendURL(`accounts/${accountId}/settings/macros`),
|
||||
toStateName: 'macros_wrapper',
|
||||
featureFlag: FEATURE_FLAGS.MACROS,
|
||||
@@ -120,6 +146,9 @@ const settings = accountId => ({
|
||||
icon: 'chat-multiple',
|
||||
label: 'CANNED_RESPONSES',
|
||||
hasSubMenu: false,
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
toState: frontendURL(
|
||||
`accounts/${accountId}/settings/canned-response/list`
|
||||
),
|
||||
@@ -130,6 +159,9 @@ const settings = accountId => ({
|
||||
icon: 'flash-on',
|
||||
label: 'INTEGRATIONS',
|
||||
hasSubMenu: false,
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
toState: frontendURL(`accounts/${accountId}/settings/integrations`),
|
||||
toStateName: 'settings_integrations',
|
||||
featureFlag: FEATURE_FLAGS.INTEGRATIONS,
|
||||
@@ -138,6 +170,9 @@ const settings = accountId => ({
|
||||
icon: 'star-emphasis',
|
||||
label: 'APPLICATIONS',
|
||||
hasSubMenu: false,
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
toState: frontendURL(`accounts/${accountId}/settings/applications`),
|
||||
toStateName: 'settings_applications',
|
||||
featureFlag: FEATURE_FLAGS.INTEGRATIONS,
|
||||
@@ -146,6 +181,9 @@ const settings = accountId => ({
|
||||
icon: 'key',
|
||||
label: 'AUDIT_LOGS',
|
||||
hasSubMenu: false,
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
toState: frontendURL(`accounts/${accountId}/settings/audit-log/list`),
|
||||
toStateName: 'auditlogs_list',
|
||||
isEnterpriseOnly: true,
|
||||
@@ -156,6 +194,9 @@ const settings = accountId => ({
|
||||
icon: 'document-list-clock',
|
||||
label: 'SLA',
|
||||
hasSubMenu: false,
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
toState: frontendURL(`accounts/${accountId}/settings/sla/list`),
|
||||
toStateName: 'sla_list',
|
||||
isEnterpriseOnly: true,
|
||||
@@ -166,6 +207,9 @@ const settings = accountId => ({
|
||||
icon: 'credit-card-person',
|
||||
label: 'BILLING',
|
||||
hasSubMenu: false,
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
toState: frontendURL(`accounts/${accountId}/settings/billing`),
|
||||
toStateName: 'billing_settings_index',
|
||||
showOnlyOnCloud: true,
|
||||
|
||||
@@ -29,6 +29,8 @@ import SecondaryNavItem from './SecondaryNavItem.vue';
|
||||
import AccountContext from './AccountContext.vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { FEATURE_FLAGS } from '../../../featureFlags';
|
||||
import { hasPermissions } from '../../../helper/permissionsHelper';
|
||||
import { routesWithPermissions } from '../../../routes';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -60,9 +62,9 @@ export default {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
currentRole: {
|
||||
type: String,
|
||||
default: '',
|
||||
currentUser: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
isOnChatwootCloud: {
|
||||
type: Boolean,
|
||||
@@ -80,16 +82,16 @@ export default {
|
||||
return this.customViews.filter(view => view.filter_type === 'contact');
|
||||
},
|
||||
accessibleMenuItems() {
|
||||
if (!this.currentRole) {
|
||||
return [];
|
||||
}
|
||||
const menuItemsFilteredByRole = this.menuConfig.menuItems.filter(
|
||||
menuItem =>
|
||||
window.roleWiseRoutes[this.currentRole].indexOf(
|
||||
menuItem.toStateName
|
||||
) > -1
|
||||
const menuItemsFilteredByPermissions = this.menuConfig.menuItems.filter(
|
||||
menuItem => {
|
||||
const { permissions: userPermissions = [] } = this.currentUser;
|
||||
return hasPermissions(
|
||||
routesWithPermissions[menuItem.toStateName],
|
||||
userPermissions
|
||||
);
|
||||
}
|
||||
);
|
||||
return menuItemsFilteredByRole.filter(item => {
|
||||
return menuItemsFilteredByPermissions.filter(item => {
|
||||
if (item.showOnlyOnCloud) {
|
||||
return this.isOnChatwootCloud;
|
||||
}
|
||||
|
||||
@@ -65,27 +65,29 @@
|
||||
:show-child-count="showChildCount(child.count)"
|
||||
:child-item-count="child.count"
|
||||
/>
|
||||
<router-link
|
||||
v-if="showItem(menuItem)"
|
||||
v-slot="{ href, navigate }"
|
||||
:to="menuItem.toState"
|
||||
custom
|
||||
>
|
||||
<li class="pl-1">
|
||||
<a :href="href">
|
||||
<woot-button
|
||||
size="tiny"
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
icon="add"
|
||||
:data-testid="menuItem.dataTestid"
|
||||
@click="e => newLinkClick(e, navigate)"
|
||||
>
|
||||
{{ $t(`SIDEBAR.${menuItem.newLinkTag}`) }}
|
||||
</woot-button>
|
||||
</a>
|
||||
</li>
|
||||
</router-link>
|
||||
<Policy :permissions="['administrator']">
|
||||
<router-link
|
||||
v-if="menuItem.newLink"
|
||||
v-slot="{ href, navigate }"
|
||||
:to="menuItem.toState"
|
||||
custom
|
||||
>
|
||||
<li class="pl-1">
|
||||
<a :href="href">
|
||||
<woot-button
|
||||
size="tiny"
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
icon="add"
|
||||
:data-testid="menuItem.dataTestid"
|
||||
@click="e => newLinkClick(e, navigate)"
|
||||
>
|
||||
{{ $t(`SIDEBAR.${menuItem.newLinkTag}`) }}
|
||||
</woot-button>
|
||||
</a>
|
||||
</li>
|
||||
</router-link>
|
||||
</Policy>
|
||||
</ul>
|
||||
</li>
|
||||
</template>
|
||||
@@ -105,9 +107,10 @@ import {
|
||||
isOnMentionsView,
|
||||
isOnUnattendedView,
|
||||
} from '../../../store/modules/conversations/helpers/actionHelpers';
|
||||
import Policy from '../../policy.vue';
|
||||
|
||||
export default {
|
||||
components: { SecondaryChildNavItem },
|
||||
components: { SecondaryChildNavItem, Policy },
|
||||
mixins: [adminMixin, configMixin],
|
||||
props: {
|
||||
menuItem: {
|
||||
|
||||
+4
-4
@@ -1,12 +1,12 @@
|
||||
import AccountSelector from '../AccountSelector';
|
||||
import AccountSelector from '../AccountSelector.vue';
|
||||
import { createLocalVue, mount } from '@vue/test-utils';
|
||||
import Vuex from 'vuex';
|
||||
import VueI18n from 'vue-i18n';
|
||||
|
||||
import i18n from 'dashboard/i18n';
|
||||
import WootModal from 'dashboard/components/Modal';
|
||||
import WootModalHeader from 'dashboard/components/ModalHeader';
|
||||
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon';
|
||||
import WootModal from 'dashboard/components/Modal.vue';
|
||||
import WootModalHeader from 'dashboard/components/ModalHeader.vue';
|
||||
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.component('woot-modal', WootModal);
|
||||
|
||||
+3
-3
@@ -1,12 +1,12 @@
|
||||
import AgentDetails from '../AgentDetails';
|
||||
import AgentDetails from '../AgentDetails.vue';
|
||||
import { createLocalVue, shallowMount } from '@vue/test-utils';
|
||||
import Vuex from 'vuex';
|
||||
import VueI18n from 'vue-i18n';
|
||||
import VTooltip from 'v-tooltip';
|
||||
|
||||
import i18n from 'dashboard/i18n';
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail';
|
||||
import WootButton from 'dashboard/components/ui/WootButton';
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
import WootButton from 'dashboard/components/ui/WootButton.vue';
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Vuex);
|
||||
localVue.use(VueI18n);
|
||||
|
||||
+4
-2
@@ -1,13 +1,15 @@
|
||||
import NotificationBell from '../NotificationBell';
|
||||
import NotificationBell from '../NotificationBell.vue';
|
||||
import { createLocalVue, shallowMount } from '@vue/test-utils';
|
||||
import Vuex from 'vuex';
|
||||
import VueI18n from 'vue-i18n';
|
||||
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
|
||||
|
||||
import i18n from 'dashboard/i18n';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Vuex);
|
||||
localVue.use(VueI18n);
|
||||
localVue.component('fluent-icon', FluentIcon);
|
||||
|
||||
const i18nConfig = new VueI18n({
|
||||
locale: 'en',
|
||||
@@ -27,7 +29,7 @@ describe('notificationBell', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
actions = {
|
||||
showNotification: jest.fn(),
|
||||
showNotification: vi.fn(),
|
||||
};
|
||||
modules = {
|
||||
auth: {
|
||||
|
||||
@@ -2,15 +2,21 @@ import AvailabilityStatus from '../AvailabilityStatus.vue';
|
||||
import { createLocalVue, mount } from '@vue/test-utils';
|
||||
import Vuex from 'vuex';
|
||||
import VueI18n from 'vue-i18n';
|
||||
import VTooltip from 'v-tooltip';
|
||||
|
||||
import WootButton from 'dashboard/components/ui/WootButton.vue';
|
||||
import 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';
|
||||
import WootDropdownDivider from 'shared/components/ui/dropdown/DropdownDivider.vue';
|
||||
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
|
||||
|
||||
import WootButton from 'dashboard/components/ui/WootButton';
|
||||
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem';
|
||||
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu';
|
||||
import WootDropdownHeader from 'shared/components/ui/dropdown/DropdownHeader';
|
||||
import WootDropdownDivider from 'shared/components/ui/dropdown/DropdownDivider';
|
||||
import i18n from 'dashboard/i18n';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(VTooltip, {
|
||||
defaultHtml: false,
|
||||
});
|
||||
localVue.use(Vuex);
|
||||
localVue.use(VueI18n);
|
||||
localVue.component('woot-button', WootButton);
|
||||
@@ -18,12 +24,14 @@ localVue.component('woot-dropdown-header', WootDropdownHeader);
|
||||
localVue.component('woot-dropdown-menu', WootDropdownMenu);
|
||||
localVue.component('woot-dropdown-divider', WootDropdownDivider);
|
||||
localVue.component('woot-dropdown-item', WootDropdownItem);
|
||||
localVue.component('fluent-icon', FluentIcon);
|
||||
|
||||
const i18nConfig = new VueI18n({ locale: 'en', messages: i18n });
|
||||
|
||||
describe('AvailabilityStatus', () => {
|
||||
const currentAvailability = 'online';
|
||||
const currentAccountId = '1';
|
||||
const currentUserAutoOffline = false;
|
||||
let store = null;
|
||||
let actions = null;
|
||||
let modules = null;
|
||||
@@ -31,7 +39,7 @@ describe('AvailabilityStatus', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
actions = {
|
||||
updateAvailability: jest.fn(() => {
|
||||
updateAvailability: vi.fn(() => {
|
||||
return Promise.resolve();
|
||||
}),
|
||||
};
|
||||
@@ -41,6 +49,7 @@ describe('AvailabilityStatus', () => {
|
||||
getters: {
|
||||
getCurrentUserAvailability: () => currentAvailability,
|
||||
getCurrentAccountId: () => currentAccountId,
|
||||
getCurrentUserAutoOffline: () => currentUserAutoOffline,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<script setup>
|
||||
import { useStoreGetters } from 'dashboard/composables/store';
|
||||
import { computed } from 'vue';
|
||||
import { hasPermissions } from '../helper/permissionsHelper';
|
||||
const props = defineProps({
|
||||
permissions: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const getters = useStoreGetters();
|
||||
const user = getters.getCurrentUser.value;
|
||||
const hasPermission = computed(() =>
|
||||
hasPermissions(props.permissions, user.permissions)
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="hasPermission">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,5 +1,5 @@
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import SidemenuIcon from '../SidemenuIcon';
|
||||
import SidemenuIcon from '../SidemenuIcon.vue';
|
||||
|
||||
describe('SidemenuIcon', () => {
|
||||
test('matches snapshot', () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`SidemenuIcon matches snapshot 1`] = `
|
||||
exports[`SidemenuIcon > matches snapshot 1`] = `
|
||||
<button
|
||||
class="-ml-3 text-black-900 dark:text-slate-300"
|
||||
color-scheme="secondary"
|
||||
|
||||
+7
-7
@@ -18,11 +18,11 @@ describe('Date formatting functions', () => {
|
||||
const testDate = new Date(2020, 4, 15); // May 15, 2020
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(navigator, 'language', 'get').mockReturnValue('en-US');
|
||||
vi.spyOn(navigator, 'language', 'get').mockReturnValue('en-US');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('returns the correct month name from a date', () => {
|
||||
@@ -39,7 +39,7 @@ describe('Date formatting functions', () => {
|
||||
});
|
||||
|
||||
it('returns the correct date format for the current locale en-IN', () => {
|
||||
jest.spyOn(navigator, 'language', 'get').mockReturnValue('en-IN');
|
||||
vi.spyOn(navigator, 'language', 'get').mockReturnValue('en-IN');
|
||||
const expected = 'dd/MM/yyyy';
|
||||
expect(getIntlDateFormatForLocale()).toBe(expected);
|
||||
});
|
||||
@@ -231,13 +231,13 @@ describe('isHoveringNextDayInRange', () => {
|
||||
describe('getActiveDateRange', () => {
|
||||
const currentDate = new Date(2020, 5, 15, 12, 0); // May 15, 2020, at noon
|
||||
|
||||
beforeAll(() => {
|
||||
beforeEach(() => {
|
||||
// Mocking the current date to ensure consistency in tests
|
||||
jest.useFakeTimers().setSystemTime(currentDate.getTime());
|
||||
vi.useFakeTimers().setSystemTime(currentDate.getTime());
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('returns the correct range for "last7days"', () => {
|
||||
|
||||
@@ -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());
|
||||
},
|
||||
|
||||
@@ -120,7 +120,6 @@ 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';
|
||||
@@ -144,7 +143,7 @@ export default {
|
||||
SLACardLabel,
|
||||
},
|
||||
|
||||
mixins: [inboxMixin, timeMixin, conversationMixin, alertMixin],
|
||||
mixins: [inboxMixin, conversationMixin, alertMixin],
|
||||
props: {
|
||||
activeLabel: {
|
||||
type: String,
|
||||
|
||||
@@ -127,7 +127,7 @@
|
||||
</div>
|
||||
<div
|
||||
v-if="shouldShowContextMenu"
|
||||
class="context-menu-wrap invisible group-hover:visible"
|
||||
class="invisible context-menu-wrap group-hover:visible"
|
||||
>
|
||||
<context-menu
|
||||
v-if="isBubble && !isMessageDeleted"
|
||||
@@ -158,7 +158,7 @@ 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';
|
||||
@@ -166,6 +166,7 @@ import { ACCOUNT_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
|
||||
import { LocalStorage } from 'shared/helpers/localStorage';
|
||||
import { getDayDifferenceFromNow } from 'shared/helpers/DateHelper';
|
||||
import * as Sentry from '@sentry/browser';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -183,7 +184,7 @@ export default {
|
||||
InstagramStoryReply,
|
||||
Spinner,
|
||||
},
|
||||
mixins: [alertMixin, messageFormatterMixin, contentTypeMixin],
|
||||
mixins: [alertMixin, messageFormatterMixin],
|
||||
props: {
|
||||
data: {
|
||||
type: Object,
|
||||
@@ -481,6 +482,9 @@ export default {
|
||||
}
|
||||
return '';
|
||||
},
|
||||
isEmailContentType() {
|
||||
return this.contentType === CONTENT_TYPES.INCOMING_EMAIL;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
data() {
|
||||
@@ -502,15 +506,29 @@ export default {
|
||||
},
|
||||
hasMediaAttachment(type) {
|
||||
if (this.hasAttachments && this.data.attachments.length > 0) {
|
||||
const { attachments = [{}] } = this.data;
|
||||
const { file_type: fileType } = attachments[0];
|
||||
return fileType === type && !this.hasMediaLoadError;
|
||||
return this.compareMessageFileType(this.data, type);
|
||||
}
|
||||
if (this.storyReply) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
compareMessageFileType(messageData, type) {
|
||||
try {
|
||||
const { attachments = [{}] } = messageData;
|
||||
const { file_type: fileType } = attachments[0];
|
||||
return fileType === type && !this.hasMediaLoadError;
|
||||
} catch (err) {
|
||||
Sentry.setContext('attachment-parsing-error', {
|
||||
messageData,
|
||||
type,
|
||||
hasMediaLoadError: this.hasMediaLoadError,
|
||||
});
|
||||
|
||||
Sentry.captureException(err);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
handleContextMenuClick() {
|
||||
this.showContextMenu = !this.showContextMenu;
|
||||
},
|
||||
|
||||
@@ -197,7 +197,7 @@ import {
|
||||
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
|
||||
import { LocalStorage } from 'shared/helpers/localStorage';
|
||||
|
||||
const EmojiInput = () => import('shared/components/emoji/EmojiInput');
|
||||
const EmojiInput = () => import('shared/components/emoji/EmojiInput.vue');
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
||||
@@ -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 = {} } =
|
||||
|
||||
@@ -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;
|
||||
|
||||
+12
-12
@@ -2,16 +2,16 @@ import { createLocalVue, mount } from '@vue/test-utils';
|
||||
import Vuex from 'vuex';
|
||||
import VueI18n from 'vue-i18n';
|
||||
import VTooltip from 'v-tooltip';
|
||||
import Button from 'dashboard/components/buttons/Button';
|
||||
import Button from 'dashboard/components/buttons/Button.vue';
|
||||
import i18n from 'dashboard/i18n';
|
||||
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon';
|
||||
import MoreActions from '../MoreActions';
|
||||
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
|
||||
import MoreActions from '../MoreActions.vue';
|
||||
|
||||
jest.mock('shared/helpers/mitt', () => ({
|
||||
vi.mock('shared/helpers/mitt', () => ({
|
||||
emitter: {
|
||||
emit: jest.fn(),
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
emit: vi.fn(),
|
||||
on: vi.fn(),
|
||||
off: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -26,9 +26,9 @@ localVue.component('fluent-icon', FluentIcon);
|
||||
localVue.component('woot-button', Button);
|
||||
|
||||
localVue.prototype.$emitter = {
|
||||
emit: jest.fn(),
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
emit: vi.fn(),
|
||||
on: vi.fn(),
|
||||
off: vi.fn(),
|
||||
};
|
||||
|
||||
const i18nConfig = new VueI18n({ locale: 'en', messages: i18n });
|
||||
@@ -49,8 +49,8 @@ describe('MoveActions', () => {
|
||||
currentChat,
|
||||
};
|
||||
|
||||
muteConversation = jest.fn(() => Promise.resolve());
|
||||
unmuteConversation = jest.fn(() => Promise.resolve());
|
||||
muteConversation = vi.fn(() => Promise.resolve());
|
||||
unmuteConversation = vi.fn(() => Promise.resolve());
|
||||
|
||||
modules = {
|
||||
conversations: { actions: { muteConversation, unmuteConversation } },
|
||||
|
||||
+4
-4
@@ -8,7 +8,7 @@ const buildComponent = ({ data = {}, methods = {} }) => ({
|
||||
data() {
|
||||
return { ...data, selectedIndex: 0, items: [1, 2, 3] };
|
||||
},
|
||||
methods: { ...methods, onSelect: jest.fn(), adjustScroll: jest.fn() },
|
||||
methods: { ...methods, onSelect: vi.fn(), adjustScroll: vi.fn() },
|
||||
mixins: [keyboardEventListenerMixins],
|
||||
});
|
||||
|
||||
@@ -21,7 +21,7 @@ describe('mentionSelectionKeyboardMixin', () => {
|
||||
});
|
||||
|
||||
it('ArrowUp and Control+KeyP update selectedIndex correctly', () => {
|
||||
const preventDefault = jest.fn();
|
||||
const preventDefault = vi.fn();
|
||||
const keyboardEvents = wrapper.vm.getKeyboardEvents();
|
||||
|
||||
if (keyboardEvents && keyboardEvents.ArrowUp) {
|
||||
@@ -39,7 +39,7 @@ describe('mentionSelectionKeyboardMixin', () => {
|
||||
});
|
||||
|
||||
it('ArrowDown and Control+KeyN update selectedIndex correctly', () => {
|
||||
const preventDefault = jest.fn();
|
||||
const preventDefault = vi.fn();
|
||||
const keyboardEvents = wrapper.vm.getKeyboardEvents();
|
||||
|
||||
if (keyboardEvents && keyboardEvents.ArrowDown) {
|
||||
@@ -57,7 +57,7 @@ describe('mentionSelectionKeyboardMixin', () => {
|
||||
});
|
||||
|
||||
it('Enter key triggers onSelect method', () => {
|
||||
const preventDefault = jest.fn();
|
||||
const preventDefault = vi.fn();
|
||||
const keyboardEvents = wrapper.vm.getKeyboardEvents();
|
||||
|
||||
if (keyboardEvents && keyboardEvents.Enter) {
|
||||
|
||||
@@ -2,17 +2,17 @@ import { shallowMount } from '@vue/test-utils';
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
import { useEmitter } from '../emitter';
|
||||
|
||||
jest.mock('shared/helpers/mitt', () => ({
|
||||
vi.mock('shared/helpers/mitt', () => ({
|
||||
emitter: {
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
on: vi.fn(),
|
||||
off: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('useEmitter', () => {
|
||||
let wrapper;
|
||||
const eventName = 'my-event';
|
||||
const callback = jest.fn();
|
||||
const callback = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = shallowMount({
|
||||
@@ -29,10 +29,6 @@ describe('useEmitter', () => {
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should add an event listener on mount', () => {
|
||||
expect(emitter.on).toHaveBeenCalledWith(eventName, callback);
|
||||
});
|
||||
|
||||
@@ -38,9 +38,10 @@ export class AnalyticsHelper {
|
||||
* @param {Object} user - User object
|
||||
*/
|
||||
identify(user) {
|
||||
if (!this.analytics) {
|
||||
if (!this.analytics || !user) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.user = user;
|
||||
this.analytics.identify(this.user.email, {
|
||||
userId: this.user.id,
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import helperObject, { AnalyticsHelper } from '../';
|
||||
|
||||
jest.mock('@june-so/analytics-next', () => ({
|
||||
vi.mock('@june-so/analytics-next', () => ({
|
||||
AnalyticsBrowser: {
|
||||
load: () => [
|
||||
{
|
||||
identify: jest.fn(),
|
||||
track: jest.fn(),
|
||||
page: jest.fn(),
|
||||
group: jest.fn(),
|
||||
identify: vi.fn(),
|
||||
track: vi.fn(),
|
||||
page: vi.fn(),
|
||||
group: vi.fn(),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -40,7 +40,7 @@ describe('AnalyticsHelper', () => {
|
||||
|
||||
describe('identify', () => {
|
||||
beforeEach(() => {
|
||||
analyticsHelper.analytics = { identify: jest.fn(), group: jest.fn() };
|
||||
analyticsHelper.analytics = { identify: vi.fn(), group: vi.fn() };
|
||||
});
|
||||
|
||||
it('should call identify on analytics browser with correct arguments', () => {
|
||||
@@ -87,7 +87,7 @@ describe('AnalyticsHelper', () => {
|
||||
|
||||
describe('track', () => {
|
||||
beforeEach(() => {
|
||||
analyticsHelper.analytics = { track: jest.fn() };
|
||||
analyticsHelper.analytics = { track: vi.fn() };
|
||||
analyticsHelper.user = { id: '123' };
|
||||
});
|
||||
|
||||
@@ -118,7 +118,7 @@ describe('AnalyticsHelper', () => {
|
||||
|
||||
describe('page', () => {
|
||||
beforeEach(() => {
|
||||
analyticsHelper.analytics = { page: jest.fn() };
|
||||
analyticsHelper.analytics = { page: vi.fn() };
|
||||
});
|
||||
|
||||
it('should call the analytics.page method with the correct arguments', () => {
|
||||
|
||||
@@ -2,34 +2,35 @@ import Vue from 'vue';
|
||||
import plugin from '../plugin';
|
||||
import analyticsHelper from '../index';
|
||||
|
||||
vi.spyOn(analyticsHelper, 'init');
|
||||
vi.spyOn(analyticsHelper, 'track');
|
||||
|
||||
describe('Vue Analytics Plugin', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(analyticsHelper, 'init');
|
||||
jest.spyOn(analyticsHelper, 'track');
|
||||
Vue.use(plugin);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetModules();
|
||||
jest.clearAllMocks();
|
||||
it('should call the init method on analyticsHelper once during plugin installation', () => {
|
||||
expect(analyticsHelper.init).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should call the init method on the analyticsHelper', () => {
|
||||
expect(analyticsHelper.init).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should add the analyticsHelper to the Vue prototype', () => {
|
||||
it('should add the analyticsHelper to the Vue prototype as $analytics', () => {
|
||||
expect(Vue.prototype.$analytics).toBe(analyticsHelper);
|
||||
});
|
||||
|
||||
it('should add the track method to the Vue prototype', () => {
|
||||
it('should add a track method to the Vue prototype as $track', () => {
|
||||
expect(typeof Vue.prototype.$track).toBe('function');
|
||||
Vue.prototype.$track('eventName');
|
||||
expect(analyticsHelper.track).toHaveBeenCalledWith('eventName');
|
||||
expect(analyticsHelper.track)
|
||||
.toHaveBeenCalledTimes(1)
|
||||
.toHaveBeenCalledWith('eventName');
|
||||
});
|
||||
|
||||
it('should call the track method on the analyticsHelper when $track is called', () => {
|
||||
Vue.prototype.$track('eventName');
|
||||
expect(analyticsHelper.track).toHaveBeenCalledWith('eventName');
|
||||
it('should call the track method on analyticsHelper with the correct event name when $track is called', () => {
|
||||
const eventName = 'testEvent';
|
||||
Vue.prototype.$track(eventName);
|
||||
expect(analyticsHelper.track)
|
||||
.toHaveBeenCalledTimes(1)
|
||||
.toHaveBeenCalledWith(eventName);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -52,6 +52,10 @@ class DashboardAudioNotificationHelper {
|
||||
};
|
||||
|
||||
executeRecurringNotification = () => {
|
||||
if (!window.WOOT || !window.WOOT.$store) {
|
||||
this.clearSetTimeout();
|
||||
return;
|
||||
}
|
||||
const mineConversation = window.WOOT.$store.getters.getMineChats({
|
||||
assigneeType: 'me',
|
||||
status: 'open',
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -9,6 +9,11 @@ import {
|
||||
|
||||
const MAX_DISCONNECT_SECONDS = 10800;
|
||||
|
||||
// The disconnect delay threshold is added to account for delays in identifying
|
||||
// disconnections (for example, the websocket disconnection takes up to 3 seconds)
|
||||
// while fetching the latest updated conversations or messages.
|
||||
const DISCONNECT_DELAY_THRESHOLD = 15;
|
||||
|
||||
class ReconnectService {
|
||||
constructor(store, router) {
|
||||
this.store = store;
|
||||
@@ -47,7 +52,8 @@ class ReconnectService {
|
||||
fetchConversations = async () => {
|
||||
await this.store.dispatch('updateChatListFilters', {
|
||||
page: null,
|
||||
updatedWithin: this.getSecondsSinceDisconnect(),
|
||||
updatedWithin:
|
||||
this.getSecondsSinceDisconnect() + DISCONNECT_DELAY_THRESHOLD,
|
||||
});
|
||||
await this.store.dispatch('fetchAllConversations');
|
||||
// Reset the updatedWithin in the store chat list filter after fetching conversations when the user is reconnected
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
export const hasPermissions = (
|
||||
requiredPermissions = [],
|
||||
availablePermissions = []
|
||||
) => {
|
||||
return requiredPermissions.some(permission =>
|
||||
availablePermissions.includes(permission)
|
||||
);
|
||||
};
|
||||
|
||||
const isPermissionsPresentInRoute = route =>
|
||||
route.meta && route.meta.permissions;
|
||||
|
||||
export const buildPermissionsFromRouter = (routes = []) =>
|
||||
routes.reduce((acc, route) => {
|
||||
if (route.name) {
|
||||
if (!isPermissionsPresentInRoute(route)) {
|
||||
// eslint-disable-next-line
|
||||
console.error(route);
|
||||
throw new Error(
|
||||
"The route doesn't have the required permissions defined"
|
||||
);
|
||||
}
|
||||
acc[route.name] = route.meta.permissions;
|
||||
}
|
||||
|
||||
if (route.children) {
|
||||
acc = {
|
||||
...acc,
|
||||
...buildPermissionsFromRouter(route.children),
|
||||
};
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
@@ -1,19 +1,16 @@
|
||||
import { hasPermissions } from './permissionsHelper';
|
||||
|
||||
// eslint-disable-next-line default-param-last
|
||||
export const getCurrentAccount = ({ accounts } = {}, accountId) => {
|
||||
return accounts.find(account => account.id === accountId);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line default-param-last
|
||||
export const getUserRole = ({ accounts } = {}, accountId) => {
|
||||
const currentAccount = getCurrentAccount({ accounts }, accountId) || {};
|
||||
return currentAccount.role || null;
|
||||
export const routeIsAccessibleFor = (route, userPermissions = []) => {
|
||||
const { meta: { permissions: routePermissions = [] } = {} } = route;
|
||||
return hasPermissions(routePermissions, userPermissions);
|
||||
};
|
||||
|
||||
export const routeIsAccessibleFor = (route, role, roleWiseRoutes) => {
|
||||
return roleWiseRoutes[role].includes(route);
|
||||
};
|
||||
|
||||
const validateActiveAccountRoutes = (to, user, roleWiseRoutes) => {
|
||||
const validateActiveAccountRoutes = (to, user) => {
|
||||
// If the current account is active, then check for the route permissions
|
||||
const accountDashboardURL = `accounts/${to.params.accountId}/dashboard`;
|
||||
|
||||
@@ -22,15 +19,13 @@ const validateActiveAccountRoutes = (to, user, roleWiseRoutes) => {
|
||||
return accountDashboardURL;
|
||||
}
|
||||
|
||||
const userRole = getUserRole(user, Number(to.params.accountId));
|
||||
const isAccessible = routeIsAccessibleFor(to.name, userRole, roleWiseRoutes);
|
||||
const isAccessible = routeIsAccessibleFor(to, user.permissions);
|
||||
// If the route is not accessible for the user, return to dashboard screen
|
||||
return isAccessible ? null : accountDashboardURL;
|
||||
};
|
||||
|
||||
export const validateLoggedInRoutes = (to, user, roleWiseRoutes) => {
|
||||
export const validateLoggedInRoutes = (to, user) => {
|
||||
const currentAccount = getCurrentAccount(user, Number(to.params.accountId));
|
||||
|
||||
// If current account is missing, either user does not have
|
||||
// access to the account or the account is deleted, return to login screen
|
||||
if (!currentAccount) {
|
||||
@@ -40,7 +35,7 @@ export const validateLoggedInRoutes = (to, user, roleWiseRoutes) => {
|
||||
const isCurrentAccountActive = currentAccount.status === 'active';
|
||||
|
||||
if (isCurrentAccountActive) {
|
||||
return validateActiveAccountRoutes(to, user, roleWiseRoutes);
|
||||
return validateActiveAccountRoutes(to, user);
|
||||
}
|
||||
|
||||
// If the current account is not active, then redirect the user to the suspended screen
|
||||
|
||||
@@ -4,7 +4,7 @@ describe('DataManager', () => {
|
||||
const accountId = 'test-account';
|
||||
let dataManager;
|
||||
|
||||
beforeAll(async () => {
|
||||
beforeEach(async () => {
|
||||
dataManager = new DataManager(accountId);
|
||||
await dataManager.initDb();
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -8,26 +8,26 @@ import {
|
||||
} from 'dashboard/helper/routeHelpers';
|
||||
import ReconnectService from 'dashboard/helper/ReconnectService';
|
||||
|
||||
jest.mock('shared/helpers/mitt', () => ({
|
||||
vi.mock('shared/helpers/mitt', () => ({
|
||||
emitter: {
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
emit: jest.fn(),
|
||||
on: vi.fn(),
|
||||
off: vi.fn(),
|
||||
emit: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('date-fns', () => ({
|
||||
differenceInSeconds: jest.fn(),
|
||||
vi.mock('date-fns', () => ({
|
||||
differenceInSeconds: vi.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('dashboard/helper/routeHelpers', () => ({
|
||||
isAConversationRoute: jest.fn(),
|
||||
isAInboxViewRoute: jest.fn(),
|
||||
isNotificationRoute: jest.fn(),
|
||||
vi.mock('dashboard/helper/routeHelpers', () => ({
|
||||
isAConversationRoute: vi.fn(),
|
||||
isAInboxViewRoute: vi.fn(),
|
||||
isNotificationRoute: vi.fn(),
|
||||
}));
|
||||
|
||||
const storeMock = {
|
||||
dispatch: jest.fn(),
|
||||
dispatch: vi.fn(),
|
||||
getters: {
|
||||
getAppliedConversationFiltersQuery: [],
|
||||
'customViews/getActiveConversationFolder': { query: {} },
|
||||
@@ -46,17 +46,17 @@ describe('ReconnectService', () => {
|
||||
let reconnectService;
|
||||
|
||||
beforeEach(() => {
|
||||
window.addEventListener = jest.fn();
|
||||
window.removeEventListener = jest.fn();
|
||||
window.addEventListener = vi.fn();
|
||||
window.removeEventListener = vi.fn();
|
||||
Object.defineProperty(window, 'location', {
|
||||
configurable: true,
|
||||
value: { reload: jest.fn() },
|
||||
value: { reload: vi.fn() },
|
||||
});
|
||||
reconnectService = new ReconnectService(storeMock, routerMock);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
@@ -102,7 +102,7 @@ describe('ReconnectService', () => {
|
||||
expect(reconnectService.getSecondsSinceDisconnect()).toBe(0);
|
||||
});
|
||||
|
||||
it('should return the number of seconds since disconnect', () => {
|
||||
it('should return the number of seconds + threshold since disconnect', () => {
|
||||
reconnectService.disconnectTime = new Date();
|
||||
differenceInSeconds.mockReturnValue(100);
|
||||
expect(reconnectService.getSecondsSinceDisconnect()).toBe(100);
|
||||
@@ -111,7 +111,7 @@ describe('ReconnectService', () => {
|
||||
|
||||
describe('handleOnlineEvent', () => {
|
||||
it('should reload the page if disconnected for more than 3 hours', () => {
|
||||
reconnectService.getSecondsSinceDisconnect = jest
|
||||
reconnectService.getSecondsSinceDisconnect = vi
|
||||
.fn()
|
||||
.mockReturnValue(10801);
|
||||
reconnectService.handleOnlineEvent();
|
||||
@@ -119,7 +119,7 @@ describe('ReconnectService', () => {
|
||||
});
|
||||
|
||||
it('should not reload the page if disconnected for less than 3 hours', () => {
|
||||
reconnectService.getSecondsSinceDisconnect = jest
|
||||
reconnectService.getSecondsSinceDisconnect = vi
|
||||
.fn()
|
||||
.mockReturnValue(10799);
|
||||
reconnectService.handleOnlineEvent();
|
||||
@@ -128,22 +128,27 @@ describe('ReconnectService', () => {
|
||||
});
|
||||
|
||||
describe('fetchConversations', () => {
|
||||
it('should dispatch updateChatListFilters and fetchAllConversations', async () => {
|
||||
reconnectService.getSecondsSinceDisconnect = jest
|
||||
.fn()
|
||||
.mockReturnValue(100);
|
||||
it('should update the filters with disconnected time and the threshold', async () => {
|
||||
reconnectService.getSecondsSinceDisconnect = vi.fn().mockReturnValue(100);
|
||||
await reconnectService.fetchConversations();
|
||||
expect(storeMock.dispatch).toHaveBeenCalledWith('updateChatListFilters', {
|
||||
page: null,
|
||||
updatedWithin: 100,
|
||||
updatedWithin: 115,
|
||||
});
|
||||
});
|
||||
|
||||
it('should dispatch updateChatListFilters and fetchAllConversations', async () => {
|
||||
reconnectService.getSecondsSinceDisconnect = vi.fn().mockReturnValue(100);
|
||||
await reconnectService.fetchConversations();
|
||||
expect(storeMock.dispatch).toHaveBeenCalledWith('updateChatListFilters', {
|
||||
page: null,
|
||||
updatedWithin: 115,
|
||||
});
|
||||
expect(storeMock.dispatch).toHaveBeenCalledWith('fetchAllConversations');
|
||||
});
|
||||
|
||||
it('should dispatch updateChatListFilters and reset updatedWithin', async () => {
|
||||
reconnectService.getSecondsSinceDisconnect = jest
|
||||
.fn()
|
||||
.mockReturnValue(100);
|
||||
reconnectService.getSecondsSinceDisconnect = vi.fn().mockReturnValue(100);
|
||||
await reconnectService.fetchConversations();
|
||||
expect(storeMock.dispatch).toHaveBeenCalledWith('updateChatListFilters', {
|
||||
updatedWithin: null,
|
||||
@@ -173,7 +178,7 @@ describe('ReconnectService', () => {
|
||||
},
|
||||
],
|
||||
};
|
||||
const spy = jest.spyOn(
|
||||
const spy = vi.spyOn(
|
||||
reconnectService,
|
||||
'fetchFilteredOrSavedConversations'
|
||||
);
|
||||
@@ -191,7 +196,7 @@ describe('ReconnectService', () => {
|
||||
query: null,
|
||||
};
|
||||
|
||||
const spy = jest.spyOn(reconnectService, 'fetchConversations');
|
||||
const spy = vi.spyOn(reconnectService, 'fetchConversations');
|
||||
|
||||
await reconnectService.fetchConversationsOnReconnect();
|
||||
|
||||
@@ -204,7 +209,7 @@ describe('ReconnectService', () => {
|
||||
query: { test: 'activeFolderQuery' },
|
||||
};
|
||||
|
||||
const spy = jest.spyOn(
|
||||
const spy = vi.spyOn(
|
||||
reconnectService,
|
||||
'fetchFilteredOrSavedConversations'
|
||||
);
|
||||
@@ -270,11 +275,11 @@ describe('ReconnectService', () => {
|
||||
describe('handleRouteSpecificFetch', () => {
|
||||
it('should fetch conversations and messages if current route is a conversation route', async () => {
|
||||
isAConversationRoute.mockReturnValue(true);
|
||||
const spyConversations = jest.spyOn(
|
||||
const spyConversations = vi.spyOn(
|
||||
reconnectService,
|
||||
'fetchConversationsOnReconnect'
|
||||
);
|
||||
const spyMessages = jest.spyOn(
|
||||
const spyMessages = vi.spyOn(
|
||||
reconnectService,
|
||||
'fetchConversationMessagesOnReconnect'
|
||||
);
|
||||
@@ -285,14 +290,14 @@ describe('ReconnectService', () => {
|
||||
|
||||
it('should fetch notifications if current route is an inbox view route', async () => {
|
||||
isAInboxViewRoute.mockReturnValue(true);
|
||||
const spy = jest.spyOn(reconnectService, 'fetchNotificationsOnReconnect');
|
||||
const spy = vi.spyOn(reconnectService, 'fetchNotificationsOnReconnect');
|
||||
await reconnectService.handleRouteSpecificFetch();
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should fetch notifications if current route is a notification route', async () => {
|
||||
isNotificationRoute.mockReturnValue(true);
|
||||
const spy = jest.spyOn(reconnectService, 'fetchNotificationsOnReconnect');
|
||||
const spy = vi.spyOn(reconnectService, 'fetchNotificationsOnReconnect');
|
||||
await reconnectService.handleRouteSpecificFetch();
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
@@ -320,7 +325,7 @@ describe('ReconnectService', () => {
|
||||
|
||||
describe('onDisconnect', () => {
|
||||
it('should set disconnectTime and call setConversationLastMessageId', () => {
|
||||
reconnectService.setConversationLastMessageId = jest.fn();
|
||||
reconnectService.setConversationLastMessageId = vi.fn();
|
||||
reconnectService.onDisconnect();
|
||||
expect(reconnectService.disconnectTime).toBeInstanceOf(Date);
|
||||
expect(reconnectService.setConversationLastMessageId).toHaveBeenCalled();
|
||||
@@ -329,8 +334,8 @@ describe('ReconnectService', () => {
|
||||
|
||||
describe('onReconnect', () => {
|
||||
it('should handle route-specific fetch, revalidate caches, and emit WEBSOCKET_RECONNECT_COMPLETED event', async () => {
|
||||
reconnectService.handleRouteSpecificFetch = jest.fn();
|
||||
reconnectService.revalidateCaches = jest.fn();
|
||||
reconnectService.handleRouteSpecificFetch = vi.fn();
|
||||
reconnectService.revalidateCaches = vi.fn();
|
||||
await reconnectService.onReconnect();
|
||||
expect(reconnectService.handleRouteSpecificFetch).toHaveBeenCalled();
|
||||
expect(reconnectService.revalidateCaches).toHaveBeenCalled();
|
||||
|
||||
@@ -36,16 +36,14 @@ describe('#createPendingMessage', () => {
|
||||
message: 'hi',
|
||||
};
|
||||
it('returns the pending message with expected new keys', () => {
|
||||
expect(createPendingMessage(message)).toHaveProperty(
|
||||
'content',
|
||||
'id',
|
||||
'status',
|
||||
'echo_id',
|
||||
'status',
|
||||
'created_at',
|
||||
'message_type',
|
||||
'conversation_id'
|
||||
);
|
||||
expect(createPendingMessage(message)).toMatchObject({
|
||||
content: expect.anything(),
|
||||
id: expect.anything(),
|
||||
status: expect.anything(),
|
||||
echo_id: expect.anything(),
|
||||
created_at: expect.anything(),
|
||||
message_type: expect.anything(),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the pending message with status progress', () => {
|
||||
@@ -61,23 +59,20 @@ describe('#createPendingMessage', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the pending message with attachmnet key if file is passed', () => {
|
||||
it('returns the pending message with attachment key if file is passed', () => {
|
||||
const messageWithFile = {
|
||||
message: 'hi',
|
||||
file: {},
|
||||
};
|
||||
expect(createPendingMessage(messageWithFile)).toHaveProperty(
|
||||
'content',
|
||||
'id',
|
||||
'status',
|
||||
'echo_id',
|
||||
'status',
|
||||
'created_at',
|
||||
'message_type',
|
||||
'conversation_id',
|
||||
'attachments',
|
||||
'private'
|
||||
);
|
||||
expect(createPendingMessage(messageWithFile)).toMatchObject({
|
||||
content: expect.anything(),
|
||||
id: expect.anything(),
|
||||
status: expect.anything(),
|
||||
echo_id: expect.anything(),
|
||||
created_at: expect.anything(),
|
||||
message_type: expect.anything(),
|
||||
attachments: [{ id: expect.anything() }],
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the pending message to have one attachment', () => {
|
||||
|
||||
@@ -19,19 +19,19 @@ describe('resize directive', () => {
|
||||
beforeEach(() => {
|
||||
el = document.createElement('div');
|
||||
binding = {
|
||||
value: jest.fn(),
|
||||
value: vi.fn(),
|
||||
};
|
||||
observer = {
|
||||
observe: jest.fn(),
|
||||
unobserve: jest.fn(),
|
||||
disconnect: jest.fn(),
|
||||
observe: vi.fn(),
|
||||
unobserve: vi.fn(),
|
||||
disconnect: vi.fn(),
|
||||
};
|
||||
window.ResizeObserver = ResizeObserverMock;
|
||||
jest.spyOn(window, 'ResizeObserver').mockImplementation(() => observer);
|
||||
vi.spyOn(window, 'ResizeObserver').mockImplementation(() => observer);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should create ResizeObserver on bind', () => {
|
||||
@@ -44,7 +44,7 @@ describe('resize directive', () => {
|
||||
it('should call callback on observer callback', () => {
|
||||
el = document.createElement('div');
|
||||
binding = {
|
||||
value: jest.fn(),
|
||||
value: vi.fn(),
|
||||
};
|
||||
|
||||
resize.bind(el, binding);
|
||||
|
||||
@@ -283,17 +283,17 @@ describe('findNodeToInsertImage', () => {
|
||||
mockEditorState = {
|
||||
selection: {
|
||||
$from: {
|
||||
node: jest.fn(() => ({})),
|
||||
node: vi.fn(() => ({})),
|
||||
},
|
||||
from: 0,
|
||||
},
|
||||
schema: {
|
||||
nodes: {
|
||||
image: {
|
||||
create: jest.fn(attrs => ({ type: { name: 'image' }, attrs })),
|
||||
create: vi.fn(attrs => ({ type: { name: 'image' }, attrs })),
|
||||
},
|
||||
paragraph: {
|
||||
create: jest.fn((_, node) => ({
|
||||
create: vi.fn((_, node) => ({
|
||||
type: { name: 'paragraph' },
|
||||
content: [node],
|
||||
})),
|
||||
@@ -381,11 +381,11 @@ describe('setURLWithQueryAndSize', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
selectedNode = {
|
||||
setAttribute: jest.fn(),
|
||||
setAttribute: vi.fn(),
|
||||
};
|
||||
|
||||
const tr = {
|
||||
setNodeMarkup: jest.fn().mockReturnValue({
|
||||
setNodeMarkup: vi.fn().mockReturnValue({
|
||||
docChanged: true,
|
||||
}),
|
||||
};
|
||||
@@ -397,7 +397,7 @@ describe('setURLWithQueryAndSize', () => {
|
||||
|
||||
editorView = {
|
||||
state,
|
||||
dispatch: jest.fn(),
|
||||
dispatch: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -420,7 +420,7 @@ describe('setURLWithQueryAndSize', () => {
|
||||
});
|
||||
|
||||
it('does not update the editor view if the document has not changed', () => {
|
||||
editorView.state.tr.setNodeMarkup = jest.fn().mockReturnValue({
|
||||
editorView.state.tr.setNodeMarkup = vi.fn().mockReturnValue({
|
||||
docChanged: false,
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import {
|
||||
buildPermissionsFromRouter,
|
||||
hasPermissions,
|
||||
} from '../permissionsHelper';
|
||||
|
||||
describe('hasPermissions', () => {
|
||||
it('returns true if permission is present', () => {
|
||||
expect(
|
||||
hasPermissions(['contact_manage'], ['team_manage', 'contact_manage'])
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true if permission is not present', () => {
|
||||
expect(
|
||||
hasPermissions(['contact_manage'], ['team_manage', 'user_manage'])
|
||||
).toBe(false);
|
||||
expect(hasPermissions()).toBe(false);
|
||||
expect(hasPermissions([])).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildPermissionsFromRouter', () => {
|
||||
it('returns a valid object when routes have permissions defined', () => {
|
||||
expect(
|
||||
buildPermissionsFromRouter([
|
||||
{
|
||||
path: 'agent',
|
||||
name: 'agent_list',
|
||||
meta: { permissions: ['agent_admin'] },
|
||||
},
|
||||
{
|
||||
path: 'inbox',
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'inbox_list',
|
||||
meta: { permissions: ['inbox_admin'] },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'conversations',
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
children: [
|
||||
{
|
||||
path: 'attachments',
|
||||
name: 'attachments_list',
|
||||
meta: { permissions: ['conversation_admin'] },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
).toEqual({
|
||||
agent_list: ['agent_admin'],
|
||||
inbox_list: ['inbox_admin'],
|
||||
attachments_list: ['conversation_admin'],
|
||||
});
|
||||
});
|
||||
|
||||
it('throws an error if a named routed does not have permissions defined', () => {
|
||||
expect(() => {
|
||||
buildPermissionsFromRouter([
|
||||
{
|
||||
path: 'agent',
|
||||
name: 'agent_list',
|
||||
},
|
||||
]);
|
||||
}).toThrow("The route doesn't have the required permissions defined");
|
||||
|
||||
expect(() => {
|
||||
buildPermissionsFromRouter([
|
||||
{
|
||||
path: 'agent',
|
||||
name: 'agent_list',
|
||||
meta: {},
|
||||
},
|
||||
]);
|
||||
}).toThrow("The route doesn't have the required permissions defined");
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,6 @@
|
||||
import {
|
||||
getConversationDashboardRoute,
|
||||
getCurrentAccount,
|
||||
getUserRole,
|
||||
isAConversationRoute,
|
||||
routeIsAccessibleFor,
|
||||
validateLoggedInRoutes,
|
||||
@@ -15,24 +14,11 @@ describe('#getCurrentAccount', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getUserRole', () => {
|
||||
it('should return the current role', () => {
|
||||
expect(
|
||||
getUserRole({ accounts: [{ id: 1, role: 'administrator' }] }, 1)
|
||||
).toEqual('administrator');
|
||||
expect(getUserRole({ accounts: [] }, 1)).toEqual(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#routeIsAccessibleFor', () => {
|
||||
it('should return the correct access', () => {
|
||||
const roleWiseRoutes = { agent: ['conversations'], admin: ['billing'] };
|
||||
expect(routeIsAccessibleFor('billing', 'agent', roleWiseRoutes)).toEqual(
|
||||
false
|
||||
);
|
||||
expect(routeIsAccessibleFor('billing', 'admin', roleWiseRoutes)).toEqual(
|
||||
true
|
||||
);
|
||||
let route = { meta: { permissions: ['administrator'] } };
|
||||
expect(routeIsAccessibleFor(route, ['agent'])).toEqual(false);
|
||||
expect(routeIsAccessibleFor(route, ['administrator'])).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -40,11 +26,7 @@ describe('#validateLoggedInRoutes', () => {
|
||||
describe('when account access is missing', () => {
|
||||
it('should return the login route', () => {
|
||||
expect(
|
||||
validateLoggedInRoutes(
|
||||
{ params: { accountId: 1 } },
|
||||
{ accounts: [] },
|
||||
{}
|
||||
)
|
||||
validateLoggedInRoutes({ params: { accountId: 1 } }, { accounts: [] })
|
||||
).toEqual(`app/login`);
|
||||
});
|
||||
});
|
||||
@@ -53,9 +35,12 @@ describe('#validateLoggedInRoutes', () => {
|
||||
it('return suspended route', () => {
|
||||
expect(
|
||||
validateLoggedInRoutes(
|
||||
{ name: 'conversations', params: { accountId: 1 } },
|
||||
{ accounts: [{ id: 1, role: 'agent', status: 'suspended' }] },
|
||||
{ agent: ['conversations'] }
|
||||
{
|
||||
name: 'conversations',
|
||||
params: { accountId: 1 },
|
||||
meta: { permissions: ['agent'] },
|
||||
},
|
||||
{ accounts: [{ id: 1, role: 'agent', status: 'suspended' }] }
|
||||
)
|
||||
).toEqual(`accounts/1/suspended`);
|
||||
});
|
||||
@@ -65,9 +50,22 @@ describe('#validateLoggedInRoutes', () => {
|
||||
it('returns null (no action required)', () => {
|
||||
expect(
|
||||
validateLoggedInRoutes(
|
||||
{ name: 'conversations', params: { accountId: 1 } },
|
||||
{ accounts: [{ id: 1, role: 'agent', status: 'active' }] },
|
||||
{ agent: ['conversations'] }
|
||||
{
|
||||
name: 'conversations',
|
||||
params: { accountId: 1 },
|
||||
meta: { permissions: ['agent'] },
|
||||
},
|
||||
{
|
||||
permissions: ['agent'],
|
||||
accounts: [
|
||||
{
|
||||
id: 1,
|
||||
role: 'agent',
|
||||
permissions: ['agent'],
|
||||
status: 'active',
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
).toEqual(null);
|
||||
});
|
||||
@@ -76,9 +74,12 @@ describe('#validateLoggedInRoutes', () => {
|
||||
it('returns dashboard url', () => {
|
||||
expect(
|
||||
validateLoggedInRoutes(
|
||||
{ name: 'conversations', params: { accountId: 1 } },
|
||||
{ accounts: [{ id: 1, role: 'agent', status: 'active' }] },
|
||||
{ admin: ['conversations'], agent: [] }
|
||||
{
|
||||
name: 'billing',
|
||||
params: { accountId: 1 },
|
||||
meta: { permissions: ['administrator'] },
|
||||
},
|
||||
{ accounts: [{ id: 1, role: 'agent', status: 'active' }] }
|
||||
)
|
||||
).toEqual(`accounts/1/dashboard`);
|
||||
});
|
||||
@@ -88,8 +89,7 @@ describe('#validateLoggedInRoutes', () => {
|
||||
expect(
|
||||
validateLoggedInRoutes(
|
||||
{ name: 'account_suspended', params: { accountId: 1 } },
|
||||
{ accounts: [{ id: 1, role: 'agent', status: 'active' }] },
|
||||
{ agent: ['account_suspended'] }
|
||||
{ accounts: [{ id: 1, role: 'agent', status: 'active' }] }
|
||||
)
|
||||
).toEqual(`accounts/1/dashboard`);
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { setColorTheme } from 'dashboard/helper/themeHelper.js';
|
||||
import { LocalStorage } from 'shared/helpers/localStorage';
|
||||
|
||||
jest.mock('shared/helpers/localStorage');
|
||||
vi.mock('shared/helpers/localStorage');
|
||||
|
||||
describe('setColorTheme', () => {
|
||||
it('should set body class to dark if selectedColorScheme is dark', () => {
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { uploadFile } from '../uploadHelper';
|
||||
import axios from 'axios';
|
||||
|
||||
// Mocking axios using jest-mock-axios
|
||||
global.axios = axios;
|
||||
jest.mock('axios');
|
||||
vi.mock('axios');
|
||||
|
||||
describe('#Upload Helpers', () => {
|
||||
afterEach(() => {
|
||||
|
||||
@@ -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>.",
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
export default {
|
||||
props: {
|
||||
trianglePosition: {
|
||||
type: String,
|
||||
default: '0',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
cssVars() {
|
||||
return {
|
||||
'--triangle-position': this.trianglePosition + 'rem',
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -4,8 +4,8 @@ import Vuex from 'vuex';
|
||||
import OpenAPI from '../../api/integrations/openapi';
|
||||
import { LocalStorage } from '../../../shared/helpers/localStorage';
|
||||
|
||||
jest.mock('../../api/integrations/openapi');
|
||||
jest.mock('../../../shared/helpers/localStorage');
|
||||
vi.mock('../../api/integrations/openapi');
|
||||
vi.mock('../../../shared/helpers/localStorage');
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Vuex);
|
||||
@@ -18,12 +18,12 @@ describe('aiMixin', () => {
|
||||
let actions;
|
||||
|
||||
beforeEach(() => {
|
||||
OpenAPI.processEvent = jest.fn();
|
||||
LocalStorage.set = jest.fn();
|
||||
LocalStorage.get = jest.fn();
|
||||
OpenAPI.processEvent = vi.fn();
|
||||
LocalStorage.set = vi.fn();
|
||||
LocalStorage.get = vi.fn();
|
||||
|
||||
actions = {
|
||||
['integrations/get']: jest.fn(),
|
||||
['integrations/get']: vi.fn(),
|
||||
};
|
||||
|
||||
getters = {
|
||||
@@ -63,20 +63,20 @@ describe('aiMixin', () => {
|
||||
localVue,
|
||||
});
|
||||
|
||||
const dispatchSpy = jest.spyOn(wrapper.vm.$store, 'dispatch');
|
||||
const dispatchSpy = vi.spyOn(wrapper.vm.$store, 'dispatch');
|
||||
await wrapper.vm.fetchIntegrationsIfRequired();
|
||||
expect(dispatchSpy).toHaveBeenCalledWith('integrations/get');
|
||||
});
|
||||
|
||||
it('does not fetch integrations', async () => {
|
||||
const dispatchSpy = jest.spyOn(wrapper.vm.$store, 'dispatch');
|
||||
const dispatchSpy = vi.spyOn(wrapper.vm.$store, 'dispatch');
|
||||
await wrapper.vm.fetchIntegrationsIfRequired();
|
||||
expect(dispatchSpy).not.toHaveBeenCalledWith('integrations/get');
|
||||
expect(wrapper.vm.isAIIntegrationEnabled).toBeTruthy();
|
||||
});
|
||||
|
||||
it('fetches label suggestions', async () => {
|
||||
const processEventSpy = jest.spyOn(OpenAPI, 'processEvent');
|
||||
const processEventSpy = vi.spyOn(OpenAPI, 'processEvent');
|
||||
await wrapper.vm.fetchLabelSuggestions({
|
||||
conversationId: '123',
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ describe('attributeMixin', () => {
|
||||
let store;
|
||||
|
||||
beforeEach(() => {
|
||||
actions = { updateUISettings: jest.fn(), toggleSidebarUIState: jest.fn() };
|
||||
actions = { updateUISettings: vi.fn(), toggleSidebarUIState: vi.fn() };
|
||||
getters = {
|
||||
getSelectedChat: () => ({
|
||||
id: 7165,
|
||||
|
||||
@@ -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,13 +1,13 @@
|
||||
import fileUploadMixin from 'dashboard/mixins/fileUploadMixin';
|
||||
import Vue from 'vue';
|
||||
|
||||
jest.mock('shared/helpers/FileHelper', () => ({
|
||||
checkFileSizeLimit: jest.fn(),
|
||||
vi.mock('shared/helpers/FileHelper', () => ({
|
||||
checkFileSizeLimit: vi.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('activestorage', () => ({
|
||||
DirectUpload: jest.fn().mockImplementation(() => ({
|
||||
create: jest.fn(),
|
||||
vi.mock('activestorage', () => ({
|
||||
DirectUpload: vi.fn().mockImplementation(() => ({
|
||||
create: vi.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
@@ -27,20 +27,20 @@ describe('FileUploadMixin', () => {
|
||||
vm.currentUser = {
|
||||
access_token: 'token',
|
||||
};
|
||||
vm.$t = jest.fn(message => message);
|
||||
vm.showAlert = jest.fn();
|
||||
vm.attachFile = jest.fn();
|
||||
vm.$t = vi.fn(message => message);
|
||||
vm.showAlert = vi.fn();
|
||||
vm.attachFile = vi.fn();
|
||||
});
|
||||
|
||||
it('should call onDirectFileUpload when direct uploads are enabled', () => {
|
||||
vm.onDirectFileUpload = jest.fn();
|
||||
vm.onDirectFileUpload = vi.fn();
|
||||
vm.onFileUpload({});
|
||||
expect(vm.onDirectFileUpload).toHaveBeenCalledWith({});
|
||||
});
|
||||
|
||||
it('should call onIndirectFileUpload when direct uploads are disabled', () => {
|
||||
vm.globalConfig.directUploadsEnabled = false;
|
||||
vm.onIndirectFileUpload = jest.fn();
|
||||
vm.onIndirectFileUpload = vi.fn();
|
||||
vm.onFileUpload({});
|
||||
expect(vm.onIndirectFileUpload).toHaveBeenCalledWith({});
|
||||
});
|
||||
@@ -53,7 +53,7 @@ describe('FileUploadMixin', () => {
|
||||
|
||||
it('shows an alert if the file size exceeds the maximum limit', () => {
|
||||
const fakeFile = { size: 999999999 };
|
||||
vm.showAlert = jest.fn();
|
||||
vm.showAlert = vi.fn();
|
||||
vm.onDirectFileUpload(fakeFile);
|
||||
expect(vm.showAlert).toHaveBeenCalledWith(expect.any(String));
|
||||
});
|
||||
@@ -67,7 +67,7 @@ describe('FileUploadMixin', () => {
|
||||
|
||||
it('shows an alert if the file size exceeds the maximum limit', () => {
|
||||
const fakeFile = { size: 999999999 };
|
||||
vm.showAlert = jest.fn();
|
||||
vm.showAlert = vi.fn();
|
||||
vm.onIndirectFileUpload(fakeFile);
|
||||
expect(vm.showAlert).toHaveBeenCalledWith(expect.any(String));
|
||||
});
|
||||
|
||||
@@ -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(() => {
|
||||
jest.useFakeTimers('modern');
|
||||
|
||||
const mockDate = new Date(2023, 4, 5);
|
||||
jest.setSystemTime(mockDate);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.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 = jest.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'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -14,7 +14,7 @@ describe('uiSettingsMixin', () => {
|
||||
let store;
|
||||
|
||||
beforeEach(() => {
|
||||
actions = { updateUISettings: jest.fn(), toggleSidebarUIState: jest.fn() };
|
||||
actions = { updateUISettings: vi.fn(), toggleSidebarUIState: vi.fn() };
|
||||
getters = {
|
||||
getUISettings: () => ({
|
||||
enter_to_send_enabled: false,
|
||||
|
||||
@@ -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,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);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -7,7 +7,9 @@ export const routes = [
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/search'),
|
||||
name: 'search',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: SearchView,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -41,7 +41,6 @@
|
||||
|
||||
<script>
|
||||
import Sidebar from '../../components/layout/Sidebar.vue';
|
||||
import CommandBar from './commands/commandbar.vue';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
import WootKeyShortcutModal from 'dashboard/components/widgets/modal/WootKeyShortcutModal.vue';
|
||||
import AddAccountModal from 'dashboard/components/layout/sidebarComponents/AddAccountModal.vue';
|
||||
@@ -50,6 +49,7 @@ import AddLabelModal from 'dashboard/routes/dashboard/settings/labels/AddLabel.v
|
||||
import NotificationPanel from 'dashboard/routes/dashboard/notifications/components/NotificationPanel.vue';
|
||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
const CommandBar = () => import('./commands/commandbar.vue');
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
+3
-3
@@ -15,7 +15,7 @@ describe('AddReminder', () => {
|
||||
});
|
||||
|
||||
it('tests resetValue', () => {
|
||||
const resetValue = jest.spyOn(wrapper.vm, 'resetValue');
|
||||
const resetValue = vi.spyOn(wrapper.vm, 'resetValue');
|
||||
wrapper.vm.content = 'test';
|
||||
wrapper.vm.date = '08/11/2022';
|
||||
wrapper.vm.resetValue();
|
||||
@@ -25,7 +25,7 @@ describe('AddReminder', () => {
|
||||
});
|
||||
|
||||
it('tests optionSelected', () => {
|
||||
const optionSelected = jest.spyOn(wrapper.vm, 'optionSelected');
|
||||
const optionSelected = vi.spyOn(wrapper.vm, 'optionSelected');
|
||||
wrapper.vm.label = '';
|
||||
wrapper.vm.optionSelected({ target: { value: 'test' } });
|
||||
expect(wrapper.vm.label).toEqual('test');
|
||||
@@ -33,7 +33,7 @@ describe('AddReminder', () => {
|
||||
});
|
||||
|
||||
it('tests onAdd', () => {
|
||||
const onAdd = jest.spyOn(wrapper.vm, 'onAdd');
|
||||
const onAdd = vi.spyOn(wrapper.vm, 'onAdd');
|
||||
wrapper.vm.label = 'label';
|
||||
wrapper.vm.content = 'content';
|
||||
wrapper.vm.date = '08/11/2022';
|
||||
|
||||
@@ -7,13 +7,17 @@ export const routes = [
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/contacts'),
|
||||
name: 'contacts_dashboard',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ContactsView,
|
||||
},
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/contacts/custom_view/:id'),
|
||||
name: 'contacts_segments_dashboard',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ContactsView,
|
||||
props: route => {
|
||||
return { segmentsId: route.params.id };
|
||||
@@ -22,7 +26,9 @@ export const routes = [
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/labels/:label/contacts'),
|
||||
name: 'contacts_labels_dashboard',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ContactsView,
|
||||
props: route => {
|
||||
return { label: route.params.label };
|
||||
@@ -31,7 +37,9 @@ export const routes = [
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/contacts/:contactId'),
|
||||
name: 'contact_profile_dashboard',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ContactManageView,
|
||||
props: route => {
|
||||
return { contactId: route.params.contactId };
|
||||
|
||||
@@ -22,25 +22,40 @@
|
||||
:is-on-expanded-layout="isOnExpandedLayout"
|
||||
@contact-panel-toggle="onToggleContactPanel"
|
||||
/>
|
||||
<woot-modal
|
||||
:show.sync="showCustomSnoozeModal"
|
||||
:on-close="hideCustomSnoozeModal"
|
||||
>
|
||||
<custom-snooze-modal
|
||||
@close="hideCustomSnoozeModal"
|
||||
@choose-time="chooseSnoozeTime"
|
||||
/>
|
||||
</woot-modal>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
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 { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
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';
|
||||
import { findSnoozeTime } from 'dashboard/helper/snoozeHelpers';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ChatList,
|
||||
ConversationBox,
|
||||
PopOverSearch,
|
||||
CustomSnoozeModal,
|
||||
},
|
||||
mixins: [uiSettingsMixin],
|
||||
mixins: [uiSettingsMixin, alertMixin],
|
||||
props: {
|
||||
inboxId: {
|
||||
type: [String, Number],
|
||||
@@ -70,12 +85,14 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
showSearchModal: false,
|
||||
showCustomSnoozeModal: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
chatList: 'getAllConversations',
|
||||
currentChat: 'getSelectedChat',
|
||||
contextMenuChatId: 'getContextMenuChatId',
|
||||
}),
|
||||
showConversationList() {
|
||||
return this.isOnExpandedLayout ? !this.conversationId : true;
|
||||
@@ -112,6 +129,10 @@ export default {
|
||||
this.$watch('chatList.length', () => {
|
||||
this.setActiveChat();
|
||||
});
|
||||
this.$emitter.on(CMD_SNOOZE_CONVERSATION, this.onCmdSnoozeConversation);
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.$emitter.off(CMD_SNOOZE_CONVERSATION, this.onCmdSnoozeConversation);
|
||||
},
|
||||
|
||||
methods: {
|
||||
@@ -186,6 +207,43 @@ export default {
|
||||
closeSearch() {
|
||||
this.showSearchModal = false;
|
||||
},
|
||||
onCmdSnoozeConversation(snoozeType) {
|
||||
if (snoozeType === wootConstants.SNOOZE_OPTIONS.UNTIL_CUSTOM_TIME) {
|
||||
this.showCustomSnoozeModal = true;
|
||||
} else {
|
||||
this.toggleStatus(
|
||||
wootConstants.STATUS_TYPE.SNOOZED,
|
||||
findSnoozeTime(snoozeType) || null
|
||||
);
|
||||
}
|
||||
},
|
||||
chooseSnoozeTime(customSnoozeTime) {
|
||||
this.showCustomSnoozeModal = false;
|
||||
if (customSnoozeTime) {
|
||||
this.toggleStatus(
|
||||
wootConstants.STATUS_TYPE.SNOOZED,
|
||||
getUnixTime(customSnoozeTime)
|
||||
);
|
||||
}
|
||||
},
|
||||
toggleStatus(status, snoozedUntil) {
|
||||
this.$store
|
||||
.dispatch('toggleStatus', {
|
||||
conversationId: this.currentChat?.id || this.contextMenuChatId,
|
||||
status,
|
||||
snoozedUntil,
|
||||
})
|
||||
.then(() => {
|
||||
this.$store.dispatch('setContextMenuChatId', null);
|
||||
this.showAlert(this.$t('CONVERSATION.CHANGE_STATUS'));
|
||||
});
|
||||
},
|
||||
hideCustomSnoozeModal() {
|
||||
// if we select custom snooze and the custom snooze modal is open
|
||||
// Then if the custom snooze modal is closed then set the context menu chat id to null
|
||||
this.$store.dispatch('setContextMenuChatId', null);
|
||||
this.showCustomSnoozeModal = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -117,7 +117,7 @@
|
||||
/>
|
||||
|
||||
<div class="w-full">
|
||||
<label> Social Profiles </label>
|
||||
<label>{{ $t('CONTACTS_PAGE.LIST.TABLE_HEADER.SOCIAL_PROFILES') }}</label>
|
||||
<div
|
||||
v-for="socialProfile in socialProfileKeys"
|
||||
:key="socialProfile.key"
|
||||
@@ -130,7 +130,7 @@
|
||||
</span>
|
||||
<input
|
||||
v-model="socialProfileUserNames[socialProfile.key]"
|
||||
class="input-group-field ltr:rounded-l-none rtl:rounded-r-none !mb-0"
|
||||
class="input-group-field ltr:!rounded-l-none rtl:rounded-r-none !mb-0"
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -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,7 +166,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import timeMixin from 'dashboard/mixins/time';
|
||||
import { dynamicTime } from 'shared/helpers/timeHelper';
|
||||
import ContactInfoRow from './ContactInfoRow.vue';
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
import SocialIcons from './SocialIcons.vue';
|
||||
@@ -194,7 +194,7 @@ export default {
|
||||
NewConversation,
|
||||
ContactMergeModal,
|
||||
},
|
||||
mixins: [alertMixin, adminMixin, timeMixin],
|
||||
mixins: [alertMixin, adminMixin],
|
||||
props: {
|
||||
contact: {
|
||||
type: Object,
|
||||
@@ -260,6 +260,7 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
dynamicTime,
|
||||
toggleMergeModal() {
|
||||
this.showMergeModal = !this.showMergeModal;
|
||||
},
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
/* eslint arrow-body-style: 0 */
|
||||
import { frontendURL } from '../../../helper/URLHelper';
|
||||
const ConversationView = () => import('./ConversationView');
|
||||
const ConversationView = () => import('./ConversationView.vue');
|
||||
|
||||
export default {
|
||||
routes: [
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/dashboard'),
|
||||
name: 'home',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ConversationView,
|
||||
props: () => {
|
||||
return { inboxId: 0 };
|
||||
@@ -16,7 +18,9 @@ export default {
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/conversations/:conversation_id'),
|
||||
name: 'inbox_conversation',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ConversationView,
|
||||
props: route => {
|
||||
return { inboxId: 0, conversationId: route.params.conversation_id };
|
||||
@@ -25,7 +29,9 @@ export default {
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/inbox/:inbox_id'),
|
||||
name: 'inbox_dashboard',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ConversationView,
|
||||
props: route => {
|
||||
return { inboxId: route.params.inbox_id };
|
||||
@@ -36,7 +42,9 @@ export default {
|
||||
'accounts/:accountId/inbox/:inbox_id/conversations/:conversation_id'
|
||||
),
|
||||
name: 'conversation_through_inbox',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ConversationView,
|
||||
props: route => {
|
||||
return {
|
||||
@@ -48,7 +56,9 @@ export default {
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/label/:label'),
|
||||
name: 'label_conversations',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ConversationView,
|
||||
props: route => ({ label: route.params.label }),
|
||||
},
|
||||
@@ -57,7 +67,9 @@ export default {
|
||||
'accounts/:accountId/label/:label/conversations/:conversation_id'
|
||||
),
|
||||
name: 'conversations_through_label',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ConversationView,
|
||||
props: route => ({
|
||||
conversationId: route.params.conversation_id,
|
||||
@@ -67,7 +79,9 @@ export default {
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/team/:teamId'),
|
||||
name: 'team_conversations',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ConversationView,
|
||||
props: route => ({ teamId: route.params.teamId }),
|
||||
},
|
||||
@@ -76,7 +90,9 @@ export default {
|
||||
'accounts/:accountId/team/:teamId/conversations/:conversationId'
|
||||
),
|
||||
name: 'conversations_through_team',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ConversationView,
|
||||
props: route => ({
|
||||
conversationId: route.params.conversationId,
|
||||
@@ -86,7 +102,9 @@ export default {
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/custom_view/:id'),
|
||||
name: 'folder_conversations',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ConversationView,
|
||||
props: route => ({ foldersId: route.params.id }),
|
||||
},
|
||||
@@ -95,7 +113,9 @@ export default {
|
||||
'accounts/:accountId/custom_view/:id/conversations/:conversation_id'
|
||||
),
|
||||
name: 'conversations_through_folders',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ConversationView,
|
||||
props: route => ({
|
||||
conversationId: route.params.conversation_id,
|
||||
@@ -105,7 +125,9 @@ export default {
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/mentions/conversations'),
|
||||
name: 'conversation_mentions',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ConversationView,
|
||||
props: () => ({ conversationType: 'mention' }),
|
||||
},
|
||||
@@ -114,7 +136,9 @@ export default {
|
||||
'accounts/:accountId/mentions/conversations/:conversationId'
|
||||
),
|
||||
name: 'conversation_through_mentions',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ConversationView,
|
||||
props: route => ({
|
||||
conversationId: route.params.conversationId,
|
||||
@@ -124,7 +148,9 @@ export default {
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/unattended/conversations'),
|
||||
name: 'conversation_unattended',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ConversationView,
|
||||
props: () => ({ conversationType: 'unattended' }),
|
||||
},
|
||||
@@ -133,7 +159,9 @@ export default {
|
||||
'accounts/:accountId/unattended/conversations/:conversationId'
|
||||
),
|
||||
name: 'conversation_through_unattended',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ConversationView,
|
||||
props: route => ({
|
||||
conversationId: route.params.conversationId,
|
||||
@@ -143,7 +171,9 @@ export default {
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/participating/conversations'),
|
||||
name: 'conversation_participating',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ConversationView,
|
||||
props: () => ({ conversationType: 'participating' }),
|
||||
},
|
||||
@@ -152,7 +182,9 @@ export default {
|
||||
'accounts/:accountId/participating/conversations/:conversationId'
|
||||
),
|
||||
name: 'conversation_through_participating',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ConversationView,
|
||||
props: route => ({
|
||||
conversationId: route.params.conversationId,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -28,7 +28,9 @@ export default {
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/suspended'),
|
||||
name: 'account_suspended',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: Suspended,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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');
|
||||
|
||||
+3
-2
@@ -59,13 +59,12 @@
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import UpgradePage from './UpgradePage';
|
||||
import UpgradePage from './UpgradePage.vue';
|
||||
import { frontendURL } from '../../../../helper/URLHelper';
|
||||
import Sidebar from 'dashboard/components/layout/Sidebar.vue';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
import PortalPopover from '../components/PortalPopover.vue';
|
||||
import HelpCenterSidebar from '../components/Sidebar/Sidebar.vue';
|
||||
import CommandBar from 'dashboard/routes/dashboard/commands/commandbar.vue';
|
||||
import WootKeyShortcutModal from 'dashboard/components/widgets/modal/WootKeyShortcutModal.vue';
|
||||
import AccountSelector from 'dashboard/components/layout/sidebarComponents/AccountSelector.vue';
|
||||
import NotificationPanel from 'dashboard/routes/dashboard/notifications/components/NotificationPanel.vue';
|
||||
@@ -73,6 +72,8 @@ import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||
import portalMixin from '../mixins/portalMixin';
|
||||
import AddCategory from '../pages/categories/AddCategory.vue';
|
||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||
const CommandBar = () =>
|
||||
import('dashboard/routes/dashboard/commands/commandbar.vue');
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
||||
@@ -1,42 +1,48 @@
|
||||
import HelpCenterLayout from './components/HelpCenterLayout';
|
||||
import HelpCenterLayout from './components/HelpCenterLayout.vue';
|
||||
import { getPortalRoute } from './helpers/routeHelper';
|
||||
|
||||
const ListAllPortals = () => import('./pages/portals/ListAllPortals');
|
||||
const NewPortal = () => import('./pages/portals/NewPortal');
|
||||
const ListAllPortals = () => import('./pages/portals/ListAllPortals.vue');
|
||||
const NewPortal = () => import('./pages/portals/NewPortal.vue');
|
||||
|
||||
const EditPortal = () => import('./pages/portals/EditPortal');
|
||||
const EditPortalBasic = () => import('./pages/portals/EditPortalBasic');
|
||||
const EditPortal = () => import('./pages/portals/EditPortal.vue');
|
||||
const EditPortalBasic = () => import('./pages/portals/EditPortalBasic.vue');
|
||||
const EditPortalCustomization = () =>
|
||||
import('./pages/portals/EditPortalCustomization');
|
||||
import('./pages/portals/EditPortalCustomization.vue');
|
||||
const EditPortalLocales = () => import('./pages/portals/EditPortalLocales.vue');
|
||||
const ShowPortal = () => import('./pages/portals/ShowPortal');
|
||||
const PortalDetails = () => import('./pages/portals/PortalDetails');
|
||||
const PortalCustomization = () => import('./pages/portals/PortalCustomization');
|
||||
const ShowPortal = () => import('./pages/portals/ShowPortal.vue');
|
||||
const PortalDetails = () => import('./pages/portals/PortalDetails.vue');
|
||||
const PortalCustomization = () =>
|
||||
import('./pages/portals/PortalCustomization.vue');
|
||||
const PortalSettingsFinish = () =>
|
||||
import('./pages/portals/PortalSettingsFinish');
|
||||
import('./pages/portals/PortalSettingsFinish.vue');
|
||||
|
||||
const ListAllCategories = () => import('./pages/categories/ListAllCategories');
|
||||
const NewCategory = () => import('./pages/categories/NewCategory');
|
||||
const EditCategory = () => import('./pages/categories/EditCategory');
|
||||
const ListAllCategories = () =>
|
||||
import('./pages/categories/ListAllCategories.vue');
|
||||
const NewCategory = () => import('./pages/categories/NewCategory.vue');
|
||||
const EditCategory = () => import('./pages/categories/EditCategory.vue');
|
||||
const ListCategoryArticles = () =>
|
||||
import('./pages/articles/ListCategoryArticles');
|
||||
const ListAllArticles = () => import('./pages/articles/ListAllArticles');
|
||||
import('./pages/articles/ListCategoryArticles.vue');
|
||||
const ListAllArticles = () => import('./pages/articles/ListAllArticles.vue');
|
||||
const DefaultPortalArticles = () =>
|
||||
import('./pages/articles/DefaultPortalArticles');
|
||||
const NewArticle = () => import('./pages/articles/NewArticle');
|
||||
const EditArticle = () => import('./pages/articles/EditArticle');
|
||||
import('./pages/articles/DefaultPortalArticles.vue');
|
||||
const NewArticle = () => import('./pages/articles/NewArticle.vue');
|
||||
const EditArticle = () => import('./pages/articles/EditArticle.vue');
|
||||
|
||||
const portalRoutes = [
|
||||
{
|
||||
path: getPortalRoute(''),
|
||||
name: 'default_portal_articles',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
component: DefaultPortalArticles,
|
||||
},
|
||||
{
|
||||
path: getPortalRoute('all'),
|
||||
name: 'list_all_portals',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ListAllPortals,
|
||||
},
|
||||
{
|
||||
@@ -47,55 +53,73 @@ const portalRoutes = [
|
||||
path: '',
|
||||
name: 'new_portal_information',
|
||||
component: PortalDetails,
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: ':portalSlug/customization',
|
||||
name: 'portal_customization',
|
||||
component: PortalCustomization,
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: ':portalSlug/finish',
|
||||
name: 'portal_finish',
|
||||
component: PortalSettingsFinish,
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: getPortalRoute(':portalSlug'),
|
||||
name: 'portalSlug',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ShowPortal,
|
||||
},
|
||||
{
|
||||
path: getPortalRoute(':portalSlug/edit'),
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: EditPortal,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'edit_portal_information',
|
||||
component: EditPortalBasic,
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'customizations',
|
||||
name: 'edit_portal_customization',
|
||||
component: EditPortalCustomization,
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'locales',
|
||||
name: 'edit_portal_locales',
|
||||
component: EditPortalLocales,
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'categories',
|
||||
name: 'list_all_locale_categories',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ListAllCategories,
|
||||
},
|
||||
],
|
||||
@@ -106,39 +130,51 @@ const articleRoutes = [
|
||||
{
|
||||
path: getPortalRoute(':portalSlug/:locale/articles'),
|
||||
name: 'list_all_locale_articles',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ListAllArticles,
|
||||
},
|
||||
{
|
||||
path: getPortalRoute(':portalSlug/:locale/articles/new'),
|
||||
name: 'new_article',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: NewArticle,
|
||||
},
|
||||
{
|
||||
path: getPortalRoute(':portalSlug/:locale/articles/mine'),
|
||||
name: 'list_mine_articles',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ListAllArticles,
|
||||
},
|
||||
{
|
||||
path: getPortalRoute(':portalSlug/:locale/articles/archived'),
|
||||
name: 'list_archived_articles',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ListAllArticles,
|
||||
},
|
||||
|
||||
{
|
||||
path: getPortalRoute(':portalSlug/:locale/articles/draft'),
|
||||
name: 'list_draft_articles',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ListAllArticles,
|
||||
},
|
||||
|
||||
{
|
||||
path: getPortalRoute(':portalSlug/:locale/articles/:articleSlug'),
|
||||
name: 'edit_article',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: EditArticle,
|
||||
},
|
||||
];
|
||||
@@ -147,19 +183,25 @@ const categoryRoutes = [
|
||||
{
|
||||
path: getPortalRoute(':portalSlug/:locale/categories'),
|
||||
name: 'all_locale_categories',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ListAllCategories,
|
||||
},
|
||||
{
|
||||
path: getPortalRoute(':portalSlug/:locale/categories/new'),
|
||||
name: 'new_category_in_locale',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: NewCategory,
|
||||
},
|
||||
{
|
||||
path: getPortalRoute(':portalSlug/:locale/categories/:categorySlug'),
|
||||
name: 'show_category',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ListAllArticles,
|
||||
},
|
||||
{
|
||||
@@ -167,13 +209,17 @@ const categoryRoutes = [
|
||||
':portalSlug/:locale/categories/:categorySlug/articles'
|
||||
),
|
||||
name: 'show_category_articles',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: ListCategoryArticles,
|
||||
},
|
||||
{
|
||||
path: getPortalRoute(':portalSlug/:locale/categories/:categorySlug'),
|
||||
name: 'edit_category',
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
component: EditCategory,
|
||||
},
|
||||
];
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const EmojiInput = () => import('shared/components/emoji/EmojiInput');
|
||||
const EmojiInput = () => import('shared/components/emoji/EmojiInput.vue');
|
||||
|
||||
export default {
|
||||
components: { EmojiInput },
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
@contextmenu="openContextMenu($event)"
|
||||
@click="openConversation(notificationItem)"
|
||||
>
|
||||
<div class="flex relative items-center justify-between w-full">
|
||||
<div class="relative flex items-center justify-between w-full">
|
||||
<div
|
||||
v-if="isUnread"
|
||||
class="absolute ltr:-left-3.5 rtl:-right-3.5 flex w-2 h-2 rounded bg-woot-500 dark:bg-woot-500"
|
||||
@@ -23,7 +23,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row justify-between items-center w-full gap-2">
|
||||
<div class="flex flex-row items-center justify-between w-full gap-2">
|
||||
<Thumbnail
|
||||
v-if="assigneeMeta"
|
||||
:src="assigneeMeta.thumbnail"
|
||||
@@ -31,19 +31,19 @@
|
||||
size="16px"
|
||||
/>
|
||||
<span
|
||||
class="flex-1 text-slate-800 dark:text-slate-50 text-sm overflow-hidden text-ellipsis whitespace-nowrap"
|
||||
class="flex-1 overflow-hidden text-sm text-slate-800 dark:text-slate-50 text-ellipsis whitespace-nowrap"
|
||||
:class="isUnread ? 'font-medium' : 'font-normal'"
|
||||
>
|
||||
{{ pushTitle }}
|
||||
</span>
|
||||
<span
|
||||
class="font-medium text-slate-600 dark:text-slate-300 text-xs whitespace-nowrap"
|
||||
class="text-xs font-medium text-slate-600 dark:text-slate-300 whitespace-nowrap"
|
||||
>
|
||||
{{ lastActivityAt }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="snoozedUntilTime" class="flex items-center">
|
||||
<span class="text-woot-500 dark:text-woot-500 text-xs font-medium">
|
||||
<span class="text-xs font-medium text-woot-500 dark:text-woot-500">
|
||||
{{ snoozedDisplayText }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -63,7 +63,7 @@ import StatusIcon from './StatusIcon.vue';
|
||||
import InboxNameAndId from './InboxNameAndId.vue';
|
||||
import InboxContextMenu from './InboxContextMenu.vue';
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
import timeMixin from 'dashboard/mixins/time';
|
||||
import { dynamicTime, shortTimestamp } from 'shared/helpers/timeHelper';
|
||||
import { snoozedReopenTime } from 'dashboard/helper/snoozeHelpers';
|
||||
import { INBOX_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||
export default {
|
||||
@@ -74,7 +74,6 @@ export default {
|
||||
InboxNameAndId,
|
||||
Thumbnail,
|
||||
},
|
||||
mixins: [timeMixin],
|
||||
props: {
|
||||
notificationItem: {
|
||||
type: Object,
|
||||
@@ -115,10 +114,8 @@ export default {
|
||||
);
|
||||
},
|
||||
lastActivityAt() {
|
||||
const dynamicTime = this.dynamicTime(
|
||||
this.notificationItem?.last_activity_at
|
||||
);
|
||||
return this.shortTimestamp(dynamicTime, true);
|
||||
const time = dynamicTime(this.notificationItem?.last_activity_at);
|
||||
return shortTimestamp(time, true);
|
||||
},
|
||||
menuItems() {
|
||||
const items = [
|
||||
|
||||
@@ -12,13 +12,17 @@ export const routes = [
|
||||
path: '',
|
||||
name: 'inbox_view',
|
||||
component: InboxEmptyStateView,
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: ':notification_id',
|
||||
name: 'inbox_view_conversation',
|
||||
component: InboxDetailView,
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
+7
-7
@@ -14,12 +14,12 @@
|
||||
v-if="!notificationItem.read_at"
|
||||
class="w-2 h-2 rounded-full bg-woot-500"
|
||||
/>
|
||||
<div v-else class="w-2 flex" />
|
||||
<div v-else class="flex w-2" />
|
||||
<div
|
||||
class="flex-col ml-2.5 overflow-hidden w-full flex justify-between"
|
||||
>
|
||||
<div class="flex justify-between">
|
||||
<div class="items-center flex">
|
||||
<div class="flex items-center">
|
||||
<span class="font-bold text-slate-800 dark:text-slate-100">
|
||||
{{
|
||||
`#${
|
||||
@@ -47,15 +47,15 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full flex">
|
||||
<div class="flex w-full">
|
||||
<span
|
||||
class="text-slate-700 dark:text-slate-200 font-normal overflow-hidden whitespace-nowrap text-ellipsis"
|
||||
class="overflow-hidden font-normal text-slate-700 dark:text-slate-200 whitespace-nowrap text-ellipsis"
|
||||
>
|
||||
{{ notificationItem.push_message_title }}
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
class="mt-1 text-slate-500 dark:text-slate-400 text-xxs font-semibold flex"
|
||||
class="flex mt-1 font-semibold text-slate-500 dark:text-slate-400 text-xxs"
|
||||
>
|
||||
{{ dynamicTime(notificationItem.last_activity_at) }}
|
||||
</span>
|
||||
@@ -67,13 +67,12 @@
|
||||
|
||||
<script>
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
import timeMixin from 'dashboard/mixins/time';
|
||||
import { dynamicTime } from 'shared/helpers/timeHelper';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Thumbnail,
|
||||
},
|
||||
mixins: [timeMixin],
|
||||
props: {
|
||||
notificationItem: {
|
||||
type: Object,
|
||||
@@ -96,6 +95,7 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
dynamicTime,
|
||||
onClickOpenNotification() {
|
||||
this.$emit('open-notification', this.notificationItem);
|
||||
},
|
||||
|
||||
+7
-5
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<section
|
||||
class="h-full flex-shrink flex-grow overflow-hidden py-8 px-4 bg-white dark:bg-slate-900"
|
||||
class="flex-grow flex-shrink h-full px-4 py-8 overflow-hidden bg-white dark:bg-slate-900"
|
||||
>
|
||||
<woot-submit-button
|
||||
v-if="notificationMetadata.unreadCount"
|
||||
@@ -22,7 +22,7 @@
|
||||
>
|
||||
<td>
|
||||
<div
|
||||
class="flex-view notification-contant--wrap overflow-hidden whitespace-nowrap text-ellipsis"
|
||||
class="overflow-hidden flex-view notification-contant--wrap whitespace-nowrap text-ellipsis"
|
||||
>
|
||||
<h5 class="notification--title">
|
||||
{{
|
||||
@@ -34,7 +34,7 @@
|
||||
}}
|
||||
</h5>
|
||||
<span
|
||||
class="notification--message-title overflow-hidden whitespace-nowrap text-ellipsis"
|
||||
class="overflow-hidden notification--message-title whitespace-nowrap text-ellipsis"
|
||||
>
|
||||
{{ notificationItem.push_message_title }}
|
||||
</span>
|
||||
@@ -88,7 +88,7 @@
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
import EmptyState from 'dashboard/components/widgets/EmptyState.vue';
|
||||
import timeMixin from '../../../../mixins/time';
|
||||
import { dynamicTime } from 'shared/helpers/timeHelper';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
@@ -97,7 +97,6 @@ export default {
|
||||
Spinner,
|
||||
EmptyState,
|
||||
},
|
||||
mixins: [timeMixin],
|
||||
props: {
|
||||
notifications: {
|
||||
type: Array,
|
||||
@@ -128,6 +127,9 @@ export default {
|
||||
return !this.isLoading && this.notifications.length === 0;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
dynamicTime,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -18,7 +18,9 @@ export const routes = [
|
||||
path: '',
|
||||
name: 'notifications_index',
|
||||
component: NotificationsView,
|
||||
roles: ['administrator', 'agent'],
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -6,7 +6,9 @@ export default {
|
||||
routes: [
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/settings/general'),
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
component: SettingsContent,
|
||||
props: {
|
||||
headerTitle: 'GENERAL_SETTINGS.TITLE',
|
||||
@@ -18,7 +20,9 @@ export default {
|
||||
path: '',
|
||||
name: 'general_settings_index',
|
||||
component: Index,
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -8,7 +8,9 @@ export default {
|
||||
routes: [
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/settings/agent-bots'),
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
component: SettingsContent,
|
||||
props: {
|
||||
headerTitle: 'AGENT_BOTS.HEADER',
|
||||
@@ -20,19 +22,25 @@ export default {
|
||||
path: '',
|
||||
name: 'agent_bots',
|
||||
component: Bot,
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'csml/new',
|
||||
name: 'agent_bots_csml_new',
|
||||
component: CsmlNewBot,
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'csml/:botId',
|
||||
name: 'agent_bots_csml_edit',
|
||||
component: CsmlEditBot,
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<woot-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('AGENT_MGMT.ADD.TITLE')"
|
||||
:header-content="$t('AGENT_MGMT.ADD.DESC')"
|
||||
/>
|
||||
|
||||
<form
|
||||
class="flex flex-col w-full items-start"
|
||||
class="flex flex-col items-start w-full"
|
||||
@submit.prevent="addAgent()"
|
||||
>
|
||||
<div class="w-full">
|
||||
@@ -45,7 +45,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-submit-button
|
||||
:disabled="
|
||||
@@ -140,7 +140,7 @@ export default {
|
||||
} = error;
|
||||
|
||||
let errorMessage = '';
|
||||
if (error.response.status === 422 && !attributes.includes('base')) {
|
||||
if (error?.response?.status === 422 && !attributes.includes('base')) {
|
||||
errorMessage = this.$t('AGENT_MGMT.ADD.API.EXIST_MESSAGE');
|
||||
} else {
|
||||
errorMessage = this.$t('AGENT_MGMT.ADD.API.ERROR_MESSAGE');
|
||||
|
||||
@@ -15,14 +15,15 @@ export default {
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'agents_wrapper',
|
||||
redirect: 'list',
|
||||
},
|
||||
{
|
||||
path: 'list',
|
||||
name: 'agent_list',
|
||||
component: AgentHome,
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -15,14 +15,15 @@ export default {
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'attributes_wrapper',
|
||||
redirect: 'list',
|
||||
},
|
||||
{
|
||||
path: 'list',
|
||||
name: 'attributes_list',
|
||||
component: AttributesHome,
|
||||
roles: ['administrator'],
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<div class="flex-1 overflow-auto p-4 flex justify-between flex-col">
|
||||
<div class="flex flex-col justify-between flex-1 p-4 overflow-auto">
|
||||
<!-- List Audit Logs -->
|
||||
<div>
|
||||
<div>
|
||||
<p
|
||||
v-if="!uiFlags.fetchingList && !records.length"
|
||||
class="flex h-full items-center flex-col justify-center"
|
||||
class="flex flex-col items-center justify-center h-full"
|
||||
>
|
||||
{{ $t('AUDIT_LOGS.LIST.404') }}
|
||||
</p>
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
<table
|
||||
v-if="!uiFlags.fetchingList && records.length"
|
||||
class="woot-table w-full"
|
||||
class="w-full woot-table"
|
||||
>
|
||||
<colgroup>
|
||||
<col class="w-3/5" />
|
||||
@@ -34,10 +34,10 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="auditLogItem in records" :key="auditLogItem.id">
|
||||
<td class="whitespace-nowrap break-all">
|
||||
<td class="break-all whitespace-nowrap">
|
||||
{{ generateLogText(auditLogItem) }}
|
||||
</td>
|
||||
<td class="whitespace-nowrap break-all">
|
||||
<td class="break-all whitespace-nowrap">
|
||||
{{
|
||||
messageTimestamp(
|
||||
auditLogItem.created_at,
|
||||
@@ -65,7 +65,7 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import TableFooter from 'dashboard/components/widgets/TableFooter.vue';
|
||||
import timeMixin from 'dashboard/mixins/time';
|
||||
import { messageTimestamp } from 'shared/helpers/timeHelper';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import {
|
||||
generateTranslationPayload,
|
||||
@@ -76,7 +76,7 @@ export default {
|
||||
components: {
|
||||
TableFooter,
|
||||
},
|
||||
mixins: [alertMixin, timeMixin],
|
||||
mixins: [alertMixin],
|
||||
beforeRouteEnter(to, from, next) {
|
||||
// Fetch Audit Logs on page load without manual refresh
|
||||
next(vm => {
|
||||
@@ -104,6 +104,7 @@ export default {
|
||||
this.$store.dispatch('agents/get');
|
||||
},
|
||||
methods: {
|
||||
messageTimestamp,
|
||||
fetchAuditLogs() {
|
||||
const page = this.$route.query.page ?? 1;
|
||||
this.$store.dispatch('auditlogs/fetch', { page }).catch(error => {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user