Chore: Move conversationStats to a seperate module (#962)
* Chore: Move conversationStats to a seperate module * Move toggleTyping to conversationTypingStatus * Remove unused agentTyping flag * Fix review comments
This commit is contained in:
parent
5ec9af9325
commit
5cb88237f5
16 changed files with 139 additions and 98 deletions
|
@ -6,20 +6,6 @@ class FBChannel extends ApiClient {
|
||||||
super('facebook_indicators', { accountScoped: true });
|
super('facebook_indicators', { accountScoped: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
markSeen({ inboxId, contactId }) {
|
|
||||||
return axios.post(`${this.url}/mark_seen`, {
|
|
||||||
inbox_id: inboxId,
|
|
||||||
contact_id: contactId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleTyping({ status, inboxId, contactId }) {
|
|
||||||
return axios.post(`${this.url}/typing_${status}`, {
|
|
||||||
inbox_id: inboxId,
|
|
||||||
contact_id: contactId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
create(params) {
|
create(params) {
|
||||||
return axios.post(
|
return axios.post(
|
||||||
`${this.url.replace(this.resource, '')}callbacks/register_facebook_page`,
|
`${this.url.replace(this.resource, '')}callbacks/register_facebook_page`,
|
||||||
|
|
|
@ -9,7 +9,5 @@ describe('#FBChannel', () => {
|
||||||
expect(fbChannel).toHaveProperty('create');
|
expect(fbChannel).toHaveProperty('create');
|
||||||
expect(fbChannel).toHaveProperty('update');
|
expect(fbChannel).toHaveProperty('update');
|
||||||
expect(fbChannel).toHaveProperty('delete');
|
expect(fbChannel).toHaveProperty('delete');
|
||||||
expect(fbChannel).toHaveProperty('markSeen');
|
|
||||||
expect(fbChannel).toHaveProperty('toggleTyping');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
import agents from '../contacts';
|
import contacts from '../contacts';
|
||||||
import ApiClient from '../ApiClient';
|
import ApiClient from '../ApiClient';
|
||||||
|
|
||||||
describe('#ContactsAPI', () => {
|
describe('#ContactsAPI', () => {
|
||||||
it('creates correct instance', () => {
|
it('creates correct instance', () => {
|
||||||
expect(agents).toBeInstanceOf(ApiClient);
|
expect(contacts).toBeInstanceOf(ApiClient);
|
||||||
expect(agents).toHaveProperty('get');
|
expect(contacts).toHaveProperty('get');
|
||||||
expect(agents).toHaveProperty('show');
|
expect(contacts).toHaveProperty('show');
|
||||||
expect(agents).toHaveProperty('create');
|
expect(contacts).toHaveProperty('create');
|
||||||
expect(agents).toHaveProperty('update');
|
expect(contacts).toHaveProperty('update');
|
||||||
expect(agents).toHaveProperty('delete');
|
expect(contacts).toHaveProperty('delete');
|
||||||
expect(agents).toHaveProperty('getConversations');
|
expect(contacts).toHaveProperty('getConversations');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -88,14 +88,17 @@ export default {
|
||||||
chatListLoading: 'getChatListLoadingStatus',
|
chatListLoading: 'getChatListLoadingStatus',
|
||||||
currentUserID: 'getCurrentUserID',
|
currentUserID: 'getCurrentUserID',
|
||||||
activeInbox: 'getSelectedInbox',
|
activeInbox: 'getSelectedInbox',
|
||||||
convStats: 'getConvTabStats',
|
conversationStats: 'conversationStats/getStats',
|
||||||
}),
|
}),
|
||||||
assigneeTabItems() {
|
assigneeTabItems() {
|
||||||
return this.$t('CHAT_LIST.ASSIGNEE_TYPE_TABS').map(item => ({
|
return this.$t('CHAT_LIST.ASSIGNEE_TYPE_TABS').map(item => {
|
||||||
key: item.KEY,
|
const count = this.conversationStats[item.COUNT_KEY] || 0;
|
||||||
name: item.NAME,
|
return {
|
||||||
count: this.convStats[item.COUNT_KEY] || 0,
|
key: item.KEY,
|
||||||
}));
|
name: item.NAME,
|
||||||
|
count,
|
||||||
|
};
|
||||||
|
});
|
||||||
},
|
},
|
||||||
inbox() {
|
inbox() {
|
||||||
return this.$store.getters['inboxes/getInbox'](this.activeInbox);
|
return this.$store.getters['inboxes/getInbox'](this.activeInbox);
|
||||||
|
@ -130,7 +133,7 @@ export default {
|
||||||
this.$store.dispatch('agents/get');
|
this.$store.dispatch('agents/get');
|
||||||
|
|
||||||
bus.$on('fetch_conversation_stats', () => {
|
bus.$on('fetch_conversation_stats', () => {
|
||||||
this.$store.dispatch('getConversationStats', this.conversationFilters);
|
this.$store.dispatch('conversationStats/get', this.conversationFilters);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
|
@ -22,11 +22,6 @@ export default {
|
||||||
default: wootConstants.ASSIGNEE_TYPE.ME,
|
default: wootConstants.ASSIGNEE_TYPE.ME,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
tabsIndex: wootConstants.ASSIGNEE_TYPE.ME,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
activeTabIndex() {
|
activeTabIndex() {
|
||||||
return this.items.findIndex(item => item.key === this.activeTab);
|
return this.items.findIndex(item => item.key === this.activeTab);
|
||||||
|
|
|
@ -269,7 +269,7 @@ export default {
|
||||||
toggleTyping(status) {
|
toggleTyping(status) {
|
||||||
if (this.channelType === 'Channel::WebWidget' && !this.isPrivate) {
|
if (this.channelType === 'Channel::WebWidget' && !this.isPrivate) {
|
||||||
const conversationId = this.currentChat.id;
|
const conversationId = this.currentChat.id;
|
||||||
this.$store.dispatch('toggleTyping', {
|
this.$store.dispatch('conversationTypingStatus/toggleTyping', {
|
||||||
status,
|
status,
|
||||||
conversationId,
|
conversationId,
|
||||||
});
|
});
|
||||||
|
|
|
@ -12,6 +12,7 @@ import conversationLabels from './modules/conversationLabels';
|
||||||
import conversationMetadata from './modules/conversationMetadata';
|
import conversationMetadata from './modules/conversationMetadata';
|
||||||
import conversationPage from './modules/conversationPage';
|
import conversationPage from './modules/conversationPage';
|
||||||
import conversations from './modules/conversations';
|
import conversations from './modules/conversations';
|
||||||
|
import conversationStats from './modules/conversationStats';
|
||||||
import conversationTypingStatus from './modules/conversationTypingStatus';
|
import conversationTypingStatus from './modules/conversationTypingStatus';
|
||||||
import globalConfig from 'shared/store/globalConfig';
|
import globalConfig from 'shared/store/globalConfig';
|
||||||
import inboxes from './modules/inboxes';
|
import inboxes from './modules/inboxes';
|
||||||
|
@ -33,6 +34,7 @@ export default new Vuex.Store({
|
||||||
conversationLabels,
|
conversationLabels,
|
||||||
conversationMetadata,
|
conversationMetadata,
|
||||||
conversationPage,
|
conversationPage,
|
||||||
|
conversationStats,
|
||||||
conversations,
|
conversations,
|
||||||
conversationTypingStatus,
|
conversationTypingStatus,
|
||||||
globalConfig,
|
globalConfig,
|
||||||
|
|
53
app/javascript/dashboard/store/modules/conversationStats.js
Normal file
53
app/javascript/dashboard/store/modules/conversationStats.js
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
import Vue from 'vue';
|
||||||
|
import types from '../mutation-types';
|
||||||
|
import ConversationApi from '../../api/inbox/conversation';
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
mineCount: 0,
|
||||||
|
unAssignedCount: 0,
|
||||||
|
allCount: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getters = {
|
||||||
|
getStats: $state => $state,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const actions = {
|
||||||
|
get: async ({ commit }, params) => {
|
||||||
|
try {
|
||||||
|
const response = await ConversationApi.meta(params);
|
||||||
|
const {
|
||||||
|
data: { meta },
|
||||||
|
} = response;
|
||||||
|
commit(types.SET_CONV_TAB_META, meta);
|
||||||
|
} catch (error) {
|
||||||
|
// Ignore error
|
||||||
|
}
|
||||||
|
},
|
||||||
|
set({ commit }, meta) {
|
||||||
|
commit(types.SET_CONV_TAB_META, meta);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mutations = {
|
||||||
|
[types.SET_CONV_TAB_META](
|
||||||
|
$state,
|
||||||
|
{
|
||||||
|
mine_count: mineCount,
|
||||||
|
unassigned_count: unAssignedCount,
|
||||||
|
all_count: allCount,
|
||||||
|
} = {}
|
||||||
|
) {
|
||||||
|
Vue.set($state, 'mineCount', mineCount);
|
||||||
|
Vue.set($state, 'allCount', allCount);
|
||||||
|
Vue.set($state, 'unAssignedCount', unAssignedCount);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
namespaced: true,
|
||||||
|
state,
|
||||||
|
getters,
|
||||||
|
actions,
|
||||||
|
mutations,
|
||||||
|
};
|
|
@ -1,6 +1,6 @@
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import * as types from '../mutation-types';
|
import * as types from '../mutation-types';
|
||||||
|
import ConversationAPI from '../../api/inbox/conversation';
|
||||||
const state = {
|
const state = {
|
||||||
records: {},
|
records: {},
|
||||||
};
|
};
|
||||||
|
@ -12,6 +12,13 @@ export const getters = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
|
toggleTyping: async (_, { status, conversationId }) => {
|
||||||
|
try {
|
||||||
|
await ConversationAPI.toggleTyping({ status, conversationId });
|
||||||
|
} catch (error) {
|
||||||
|
// Handle error
|
||||||
|
}
|
||||||
|
},
|
||||||
create: ({ commit }, { conversationId, user }) => {
|
create: ({ commit }, { conversationId, user }) => {
|
||||||
commit(types.default.ADD_USER_TYPING_TO_CONVERSATION, {
|
commit(types.default.ADD_USER_TYPING_TO_CONVERSATION, {
|
||||||
conversationId,
|
conversationId,
|
||||||
|
|
|
@ -2,7 +2,6 @@ import Vue from 'vue';
|
||||||
import * as types from '../../mutation-types';
|
import * as types from '../../mutation-types';
|
||||||
import ConversationApi from '../../../api/inbox/conversation';
|
import ConversationApi from '../../../api/inbox/conversation';
|
||||||
import MessageApi from '../../../api/inbox/message';
|
import MessageApi from '../../../api/inbox/message';
|
||||||
import FBChannel from '../../../api/channel/fbChannel';
|
|
||||||
|
|
||||||
// actions
|
// actions
|
||||||
const actions = {
|
const actions = {
|
||||||
|
@ -26,7 +25,7 @@ const actions = {
|
||||||
const { data } = response.data;
|
const { data } = response.data;
|
||||||
const { payload: chatList, meta: metaData } = data;
|
const { payload: chatList, meta: metaData } = data;
|
||||||
commit(types.default.SET_ALL_CONVERSATION, chatList);
|
commit(types.default.SET_ALL_CONVERSATION, chatList);
|
||||||
commit(types.default.SET_CONV_TAB_META, metaData);
|
dispatch('conversationStats/set', metaData);
|
||||||
commit(types.default.CLEAR_LIST_LOADING_STATUS);
|
commit(types.default.CLEAR_LIST_LOADING_STATUS);
|
||||||
commit(
|
commit(
|
||||||
`contacts/${types.default.SET_CONTACTS}`,
|
`contacts/${types.default.SET_CONTACTS}`,
|
||||||
|
@ -171,24 +170,6 @@ const actions = {
|
||||||
commit(types.default.UPDATE_CONVERSATION, conversation);
|
commit(types.default.UPDATE_CONVERSATION, conversation);
|
||||||
},
|
},
|
||||||
|
|
||||||
toggleTyping: async ({ commit }, { status, conversationId }) => {
|
|
||||||
try {
|
|
||||||
commit(types.default.SET_AGENT_TYPING, { status });
|
|
||||||
await ConversationApi.toggleTyping({ status, conversationId });
|
|
||||||
} catch (error) {
|
|
||||||
// Handle error
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
markSeen: async ({ commit }, data) => {
|
|
||||||
try {
|
|
||||||
await FBChannel.markSeen(data);
|
|
||||||
commit(types.default.MARK_SEEN);
|
|
||||||
} catch (error) {
|
|
||||||
// Handle error
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
markMessagesRead: async ({ commit }, data) => {
|
markMessagesRead: async ({ commit }, data) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
commit(types.default.MARK_MESSAGE_READ, data);
|
commit(types.default.MARK_MESSAGE_READ, data);
|
||||||
|
@ -236,15 +217,6 @@ const actions = {
|
||||||
//
|
//
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
getConversationStats: async ({ commit }, params) => {
|
|
||||||
try {
|
|
||||||
const response = await ConversationApi.meta(params);
|
|
||||||
commit(types.default.SET_CONV_TAB_META, response.data.meta);
|
|
||||||
} catch (error) {
|
|
||||||
// Ignore error
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default actions;
|
export default actions;
|
||||||
|
|
|
@ -52,7 +52,6 @@ const getters = {
|
||||||
},
|
},
|
||||||
getChatStatusFilter: ({ chatStatusFilter }) => chatStatusFilter,
|
getChatStatusFilter: ({ chatStatusFilter }) => chatStatusFilter,
|
||||||
getSelectedInbox: ({ currentInbox }) => currentInbox,
|
getSelectedInbox: ({ currentInbox }) => currentInbox,
|
||||||
getConvTabStats: ({ convTabStats }) => convTabStats,
|
|
||||||
getNextChatConversation: _state => {
|
getNextChatConversation: _state => {
|
||||||
const { selectedChat } = _state;
|
const { selectedChat } = _state;
|
||||||
const conversations = getters.getAllStatusChats(_state);
|
const conversations = getters.getAllStatusChats(_state);
|
||||||
|
|
|
@ -12,16 +12,10 @@ const initialSelectedChat = {
|
||||||
status: null,
|
status: null,
|
||||||
muted: false,
|
muted: false,
|
||||||
seen: false,
|
seen: false,
|
||||||
agentTyping: 'off',
|
|
||||||
dataFetched: false,
|
dataFetched: false,
|
||||||
};
|
};
|
||||||
const state = {
|
const state = {
|
||||||
allConversations: [],
|
allConversations: [],
|
||||||
convTabStats: {
|
|
||||||
mineCount: 0,
|
|
||||||
unAssignedCount: 0,
|
|
||||||
allCount: 0,
|
|
||||||
},
|
|
||||||
selectedChat: { ...initialSelectedChat },
|
selectedChat: { ...initialSelectedChat },
|
||||||
listLoadingStatus: true,
|
listLoadingStatus: true,
|
||||||
chatStatusFilter: wootConstants.STATUS_TYPE.OPEN,
|
chatStatusFilter: wootConstants.STATUS_TYPE.OPEN,
|
||||||
|
@ -57,7 +51,6 @@ const mutations = {
|
||||||
},
|
},
|
||||||
[types.default.CLEAR_CURRENT_CHAT_WINDOW](_state) {
|
[types.default.CLEAR_CURRENT_CHAT_WINDOW](_state) {
|
||||||
_state.selectedChat.id = null;
|
_state.selectedChat.id = null;
|
||||||
_state.selectedChat.agentTyping = 'off';
|
|
||||||
},
|
},
|
||||||
|
|
||||||
[types.default.SET_PREVIOUS_CONVERSATIONS](_state, { id, data }) {
|
[types.default.SET_PREVIOUS_CONVERSATIONS](_state, { id, data }) {
|
||||||
|
@ -67,19 +60,6 @@ const mutations = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
[types.default.SET_CONV_TAB_META](
|
|
||||||
_state,
|
|
||||||
{
|
|
||||||
mine_count: mineCount,
|
|
||||||
unassigned_count: unAssignedCount,
|
|
||||||
all_count: allCount,
|
|
||||||
} = {}
|
|
||||||
) {
|
|
||||||
Vue.set(_state.convTabStats, 'mineCount', mineCount);
|
|
||||||
Vue.set(_state.convTabStats, 'allCount', allCount);
|
|
||||||
Vue.set(_state.convTabStats, 'unAssignedCount', unAssignedCount);
|
|
||||||
},
|
|
||||||
|
|
||||||
[types.default.CURRENT_CHAT_WINDOW](_state, activeChat) {
|
[types.default.CURRENT_CHAT_WINDOW](_state, activeChat) {
|
||||||
if (activeChat) {
|
if (activeChat) {
|
||||||
Object.assign(_state.selectedChat, activeChat);
|
Object.assign(_state.selectedChat, activeChat);
|
||||||
|
@ -176,10 +156,6 @@ const mutations = {
|
||||||
_state.selectedChat.seen = true;
|
_state.selectedChat.seen = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
[types.default.SET_AGENT_TYPING](_state, { status }) {
|
|
||||||
_state.selectedChat.agentTyping = status;
|
|
||||||
},
|
|
||||||
|
|
||||||
[types.default.SET_LIST_LOADING_STATUS](_state) {
|
[types.default.SET_LIST_LOADING_STATUS](_state) {
|
||||||
_state.listLoadingStatus = true;
|
_state.listLoadingStatus = true;
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import actions from '../actions';
|
import { actions } from '../../conversationStats';
|
||||||
import * as types from '../../../mutation-types';
|
import * as types from '../../../mutation-types';
|
||||||
|
|
||||||
const commit = jest.fn();
|
const commit = jest.fn();
|
||||||
|
@ -7,10 +7,10 @@ global.axios = axios;
|
||||||
jest.mock('axios');
|
jest.mock('axios');
|
||||||
|
|
||||||
describe('#actions', () => {
|
describe('#actions', () => {
|
||||||
describe('#getConversationStats', () => {
|
describe('#get', () => {
|
||||||
it('sends correct actions if API is success', async () => {
|
it('sends correct mutations if API is success', async () => {
|
||||||
axios.get.mockResolvedValue({ data: { meta: { mine_count: 1 } } });
|
axios.get.mockResolvedValue({ data: { meta: { mine_count: 1 } } });
|
||||||
await actions.getConversationStats(
|
await actions.get(
|
||||||
{ commit },
|
{ commit },
|
||||||
{ inboxId: 1, assigneeTpe: 'me', status: 'open' }
|
{ inboxId: 1, assigneeTpe: 'me', status: 'open' }
|
||||||
);
|
);
|
||||||
|
@ -20,11 +20,26 @@ describe('#actions', () => {
|
||||||
});
|
});
|
||||||
it('sends correct actions if API is error', async () => {
|
it('sends correct actions if API is error', async () => {
|
||||||
axios.get.mockRejectedValue({ message: 'Incorrect header' });
|
axios.get.mockRejectedValue({ message: 'Incorrect header' });
|
||||||
await actions.getConversationStats(
|
await actions.get(
|
||||||
{ commit },
|
{ commit },
|
||||||
{ inboxId: 1, assigneeTpe: 'me', status: 'open' }
|
{ inboxId: 1, assigneeTpe: 'me', status: 'open' }
|
||||||
);
|
);
|
||||||
expect(commit.mock.calls).toEqual([]);
|
expect(commit.mock.calls).toEqual([]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('#set', () => {
|
||||||
|
it('sends correct mutations', async () => {
|
||||||
|
actions.set(
|
||||||
|
{ commit },
|
||||||
|
{ mine_count: 1, unassigned_count: 1, all_count: 2 }
|
||||||
|
);
|
||||||
|
expect(commit.mock.calls).toEqual([
|
||||||
|
[
|
||||||
|
types.default.SET_CONV_TAB_META,
|
||||||
|
{ mine_count: 1, unassigned_count: 1, all_count: 2 },
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { getters } from '../../conversationStats';
|
||||||
|
|
||||||
|
describe('#getters', () => {
|
||||||
|
it('getCurrentPage', () => {
|
||||||
|
const state = {
|
||||||
|
mineCount: 1,
|
||||||
|
unAssignedCount: 1,
|
||||||
|
allCount: 2,
|
||||||
|
};
|
||||||
|
expect(getters.getStats(state)).toEqual({
|
||||||
|
mineCount: 1,
|
||||||
|
unAssignedCount: 1,
|
||||||
|
allCount: 2,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,20 @@
|
||||||
|
import types from '../../../mutation-types';
|
||||||
|
import { mutations } from '../../conversationStats';
|
||||||
|
|
||||||
|
describe('#mutations', () => {
|
||||||
|
describe('#SET_CONV_TAB_META', () => {
|
||||||
|
it('set conversation stats correctly', () => {
|
||||||
|
const state = {};
|
||||||
|
mutations[types.SET_CONV_TAB_META](state, {
|
||||||
|
mine_count: 1,
|
||||||
|
unassigned_count: 1,
|
||||||
|
all_count: 2,
|
||||||
|
});
|
||||||
|
expect(state).toEqual({
|
||||||
|
mineCount: 1,
|
||||||
|
unAssignedCount: 1,
|
||||||
|
allCount: 2,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -32,7 +32,6 @@ export default {
|
||||||
ADD_MESSAGE: 'ADD_MESSAGE',
|
ADD_MESSAGE: 'ADD_MESSAGE',
|
||||||
MARK_SEEN: 'MARK_SEEN',
|
MARK_SEEN: 'MARK_SEEN',
|
||||||
MARK_MESSAGE_READ: 'MARK_MESSAGE_READ',
|
MARK_MESSAGE_READ: 'MARK_MESSAGE_READ',
|
||||||
SET_AGENT_TYPING: 'SET_AGENT_TYPING',
|
|
||||||
SET_PREVIOUS_CONVERSATIONS: 'SET_PREVIOUS_CONVERSATIONS',
|
SET_PREVIOUS_CONVERSATIONS: 'SET_PREVIOUS_CONVERSATIONS',
|
||||||
SET_ACTIVE_INBOX: 'SET_ACTIVE_INBOX',
|
SET_ACTIVE_INBOX: 'SET_ACTIVE_INBOX',
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue