Compare commits

..
Author SHA1 Message Date
Shivam Mishra 164b4df679 feat: implement handleReplyTo
* save to localstorage
* trigger a event on the bus
2023-10-05 14:38:55 +05:30
Shivam Mishra 2014a82ff8 test: localstorage utils 2023-10-05 14:35:11 +05:30
Shivam Mishra f60ab0eb1d feat: add json store utils 2023-10-05 14:28:21 +05:30
Shivam Mishra dc6757df3d refactor: use feature flag to toggle reply to 2023-10-05 11:30:32 +05:30
Shivam Mishra cfeddcbec2 feat: setup message_reply_to feature flag 2023-10-05 11:30:32 +05:30
13 changed files with 166 additions and 125 deletions
@@ -150,6 +150,8 @@ import { MESSAGE_TYPE, MESSAGE_STATUS } from 'shared/constants/messages';
import { generateBotMessageContent } from './helpers/botMessageContentHelper';
import { BUS_EVENTS } from 'shared/constants/busEvents';
import { ACCOUNT_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
import { LocalStorage } from 'shared/helpers/localStorage';
export default {
components: {
@@ -274,8 +276,7 @@ export default {
copy: this.hasText,
delete: this.hasText || this.hasAttachments,
cannedResponse: this.isOutgoing && this.hasText,
replyTo: false,
// replyTo: !this.data.private && this.inboxSupportsReplyTo,
replyTo: !this.data.private && this.inboxSupportsReplyTo,
};
},
contentAttributes() {
@@ -502,10 +503,11 @@ export default {
this.contextMenuPosition = { x: null, y: null };
},
handleReplyTo() {
this.$store.dispatch('draftMessages/setInReplyTo', {
conversationId: this.data.conversation_id,
inReplyToMessage: this.data.id,
});
const replyStorageKey = LOCAL_STORAGE_KEYS.MESSAGE_REPLY_TO;
const { conversation_id: conversationId, id: replyTo } = this.data;
LocalStorage.updateJsonStore(replyStorageKey, conversationId, replyTo);
bus.$emit(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE, this.data);
},
setupHighlightTimer() {
if (Number(this.$route.query.messageId) !== Number(this.data.id)) {
@@ -124,6 +124,7 @@ import { LocalStorage } from 'shared/helpers/localStorage';
// constants
import { BUS_EVENTS } from 'shared/constants/busEvents';
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
import { REPLY_POLICY } from 'shared/constants/links';
import wootConstants from 'dashboard/constants/globals';
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
@@ -165,12 +166,14 @@ export default {
computed: {
...mapGetters({
accountId: 'getCurrentAccountId',
currentChat: 'getSelectedChat',
allConversations: 'getAllConversations',
inboxesList: 'inboxes/getInboxes',
listLoadingStatus: 'getAllMessagesLoaded',
loadingChatList: 'getChatListLoadingStatus',
appIntegrations: 'integrations/getAppIntegrations',
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
currentAccountId: 'getCurrentAccountId',
}),
isOpen() {
@@ -318,7 +321,13 @@ export default {
return this.currentChat.unread_count || 0;
},
inboxSupportsReplyTo() {
return this.inboxHasFeature(INBOX_FEATURES.REPLY_TO);
return (
this.inboxHasFeature(INBOX_FEATURES.REPLY_TO) &&
this.isFeatureEnabledonAccount(
this.accountId,
FEATURE_FLAGS.MESSAGE_REPLY_TO
)
);
},
},
@@ -2,7 +2,7 @@ export const LOCAL_STORAGE_KEYS = {
DISMISSED_UPDATES: 'dismissedUpdates',
WIDGET_BUILDER: 'widgetBubble_',
DRAFT_MESSAGES: 'draftMessages',
MESSAGE_IN_REPLY_TO: 'messageInReplyTo',
COLOR_SCHEME: 'color_scheme',
DISMISSED_LABEL_SUGGESTIONS: 'labelSuggestionsDismissed',
MESSAGE_REPLY_TO: 'messageReplyTo',
};
+1
View File
@@ -16,4 +16,5 @@ export const FEATURE_FLAGS = {
TEAM_MANAGEMENT: 'team_management',
VOICE_RECORDER: 'voice_recorder',
AUDIT_LOGS: 'audit_logs',
MESSAGE_REPLY_TO: 'message_reply_to',
};
@@ -7,7 +7,6 @@ import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
const state = {
records: LocalStorage.get(LOCAL_STORAGE_KEYS.DRAFT_MESSAGES) || {},
inReplyTo: LocalStorage.get(LOCAL_STORAGE_KEYS.MESSAGE_IN_REPLY_TO) || {},
replyEditorMode: REPLY_EDITOR_MODES.REPLY,
};
@@ -15,9 +14,6 @@ export const getters = {
get: _state => key => {
return _state.records[key] || '';
},
getInReplyTo: _state => conversationId => {
return _state.inReplyTo[conversationId];
},
getReplyEditorMode: _state => _state.replyEditorMode,
};
@@ -28,12 +24,6 @@ export const actions = {
delete: ({ commit }, { key }) => {
commit(types.SET_DRAFT_MESSAGES, { key });
},
setInReplyTo({ commit }, { conversationId, inReplyToMessage }) {
commit(types.SET_IN_REPLY_TO, { conversationId, inReplyToMessage });
},
deleteInReplyTo({ commit }, { conversationId }) {
commit(types.REMOVE_IN_REPLY_TO, { conversationId });
},
setReplyEditorMode: ({ commit }, { mode }) => {
commit(types.SET_REPLY_EDITOR_MODE, { mode });
},
@@ -49,16 +39,6 @@ export const mutations = {
Vue.set($state, 'records', updatedRecords);
LocalStorage.set(LOCAL_STORAGE_KEYS.DRAFT_MESSAGES, $state.records);
},
[types.SET_IN_REPLY_TO]($state, { conversationId, inReplyToMessage }) {
Vue.set($state.inReplyTo, conversationId, inReplyToMessage);
LocalStorage.set(LOCAL_STORAGE_KEYS.MESSAGE_IN_REPLY_TO, $state.inReplyTo);
},
[types.REMOVE_IN_REPLY_TO]($state, { conversationId }) {
const { [conversationId]: messageIdToBeRemoved, ...newRecords } =
$state.inReplyTo;
Vue.set($state, 'inReplyTo', newRecords);
LocalStorage.set(LOCAL_STORAGE_KEYS.DRAFT_MESSAGES, $state.inReplyTo);
},
[types.SET_REPLY_EDITOR_MODE]($state, { mode }) {
Vue.set($state, 'replyEditorMode', mode);
},
@@ -73,49 +73,4 @@ describe('#actions', () => {
]);
});
});
describe('#setInReplyTo', () => {
it('sends correct actions', async () => {
await actions.setInReplyTo(
{
commit,
state: {
inReplyTo: {},
},
},
{ conversationId: 45534, inReplyToMessage: 24332 }
);
expect(commit.mock.calls).toEqual([
[
types.SET_IN_REPLY_TO,
{
conversationId: 45534,
inReplyToMessage: 24332,
},
],
]);
});
});
describe('#deleteInReplyTo', () => {
it('sends correct actions', async () => {
await actions.deleteInReplyTo(
{
commit,
state: {
inReplyTo: {},
},
},
{ conversationId: 45534 }
);
expect(commit.mock.calls).toEqual([
[
types.REMOVE_IN_REPLY_TO,
{
conversationId: 45534,
},
],
]);
});
});
});
@@ -22,24 +22,4 @@ describe('#getters', () => {
};
expect(getters.getReplyEditorMode(state)).toEqual('reply');
});
describe('#getInReplyTo', () => {
it('returns the inReplyTo value if key is present', () => {
const state = {
inReplyTo: {
1234: 9876,
},
};
expect(getters.getInReplyTo(state)(1234)).toEqual(9876);
});
it('returns undefined if key is not present', () => {
const state = {
inReplyTo: {
1234: 9876,
},
};
expect(getters.getInReplyTo(state)('message-22')).toBeUndefined();
});
});
});
@@ -31,35 +31,6 @@ describe('#mutations', () => {
});
});
describe('#SET_IN_REPLY_TO', () => {
it('sets the inReplyTo value', () => {
const state = {
inReplyTo: {},
};
mutations[types.SET_IN_REPLY_TO](state, {
conversationId: 1234,
inReplyToMessage: 9876,
});
expect(state.inReplyTo).toEqual({
1234: 9876,
});
});
});
describe('#REMOVE_IN_REPLY_TO', () => {
it('removes the inReplyTo value', () => {
const state = {
inReplyTo: {
1234: 9876,
},
};
mutations[types.REMOVE_IN_REPLY_TO](state, {
conversationId: 1234,
});
expect(state.inReplyTo).toEqual({});
});
});
describe('#SET_REPLY_EDITOR_MODE', () => {
it('sets the reply editor mode', () => {
const state = {
@@ -28,8 +28,6 @@ export default {
SET_DRAFT_MESSAGES: 'SET_DRAFT_MESSAGES',
REMOVE_DRAFT_MESSAGES: 'REMOVE_DRAFT_MESSAGES',
SET_REPLY_EDITOR_MODE: 'SET_REPLY_EDITOR_MODE',
SET_IN_REPLY_TO: 'SET_IN_REPLY_TO',
REMOVE_IN_REPLY_TO: 'REMOVE_IN_REPLY_TO',
SET_CURRENT_CHAT_WINDOW: 'SET_CURRENT_CHAT_WINDOW',
CLEAR_CURRENT_CHAT_WINDOW: 'CLEAR_CURRENT_CHAT_WINDOW',
@@ -9,5 +9,6 @@ export const BUS_EVENTS = {
TOGGLE_SIDEMENU: 'TOGGLE_SIDEMENU',
ON_MESSAGE_LIST_SCROLL: 'ON_MESSAGE_LIST_SCROLL',
WEBSOCKET_DISCONNECT: 'WEBSOCKET_DISCONNECT',
TOGGLE_REPLY_TO_MESSAGE: 'TOGGLE_REPLY_TO_MESSAGE',
SHOW_TOAST: 'newToastMessage',
};
+36 -1
View File
@@ -19,7 +19,7 @@ export const LocalStorage = {
} else {
window.localStorage.setItem(key, value);
}
window.localStorage.setItem(key + ':ts', Date.now());
window.localStorage.setItem(key + ':ts', Date.now().toString());
},
setFlag(store, accountId, key, expiry = 24 * 60 * 60 * 1000) {
@@ -46,4 +46,39 @@ export const LocalStorage = {
window.localStorage.removeItem(key);
window.localStorage.removeItem(key + ':ts');
},
updateJsonStore(storeName, key, value) {
try {
const storedValue = this.get(storeName) || {};
storedValue[key] = value;
this.set(storeName, storedValue);
} catch (e) {
// eslint-disable-next-line no-console
console.error('Error updating JSON store in localStorage', e);
}
},
getFromJsonStore(storeName, key) {
try {
const storedValue = this.get(storeName) || {};
return storedValue[key] || null;
} catch (e) {
// eslint-disable-next-line no-console
console.error('Error getting value from JSON store in localStorage', e);
return null;
}
},
deleteFromJsonStore(storeName, key) {
try {
const storedValue = this.get(storeName);
if (storedValue && key in storedValue) {
delete storedValue[key];
this.set(storeName, storedValue);
}
} catch (e) {
// eslint-disable-next-line no-console
console.error('Error deleting entry from JSON store in localStorage', e);
}
},
};
@@ -0,0 +1,107 @@
import { LocalStorage } from '../localStorage';
// Mocking localStorage
const localStorageMock = (() => {
let store = {};
return {
getItem: key => store[key] || null,
setItem: (key, value) => {
store[key] = String(value);
},
removeItem: key => delete store[key],
clear: () => {
store = {};
},
};
})();
Object.defineProperty(window, 'localStorage', {
value: localStorageMock,
});
describe('LocalStorage utility', () => {
beforeEach(() => {
localStorage.clear();
});
it('set and get methods', () => {
LocalStorage.set('testKey', { a: 1 });
expect(LocalStorage.get('testKey')).toEqual({ a: 1 });
});
it('remove method', () => {
LocalStorage.set('testKey', 'testValue');
LocalStorage.remove('testKey');
expect(LocalStorage.get('testKey')).toBeNull();
});
it('updateJsonStore method', () => {
LocalStorage.updateJsonStore('testStore', 'testKey', 'testValue');
expect(LocalStorage.get('testStore')).toEqual({ testKey: 'testValue' });
});
it('getFromJsonStore method', () => {
LocalStorage.set('testStore', { testKey: 'testValue' });
expect(LocalStorage.getFromJsonStore('testStore', 'testKey')).toBe(
'testValue'
);
});
it('deleteFromJsonStore method', () => {
LocalStorage.set('testStore', { testKey: 'testValue' });
LocalStorage.deleteFromJsonStore('testStore', 'testKey');
expect(LocalStorage.getFromJsonStore('testStore', 'testKey')).toBeNull();
});
it('setFlag and getFlag methods', () => {
const store = 'testStore';
const accountId = '123';
const key = 'flagKey';
const expiry = 1000; // 1 second
// Set flag and verify it's set
LocalStorage.setFlag(store, accountId, key, expiry);
expect(LocalStorage.getFlag(store, accountId, key)).toBe(true);
// Wait for expiry and verify flag is not set
return new Promise(resolve => {
setTimeout(() => {
expect(LocalStorage.getFlag(store, accountId, key)).toBe(false);
resolve();
}, expiry + 100); // wait a bit more than expiry time to ensure the flag has expired
});
});
it('clearAll method', () => {
LocalStorage.set('testKey1', 'testValue1');
LocalStorage.set('testKey2', 'testValue2');
LocalStorage.clearAll();
expect(LocalStorage.get('testKey1')).toBeNull();
expect(LocalStorage.get('testKey2')).toBeNull();
});
it('set method with non-object value', () => {
LocalStorage.set('testKey', 'testValue');
expect(LocalStorage.get('testKey')).toBe('testValue');
});
it('set and get methods with null value', () => {
LocalStorage.set('testKey', null);
expect(LocalStorage.get('testKey')).toBeNull();
});
it('set and get methods with undefined value', () => {
LocalStorage.set('testKey', undefined);
expect(LocalStorage.get('testKey')).toBe('undefined');
});
it('set and get methods with boolean value', () => {
LocalStorage.set('testKey', true);
expect(LocalStorage.get('testKey')).toBe(true);
});
it('set and get methods with number value', () => {
LocalStorage.set('testKey', 42);
expect(LocalStorage.get('testKey')).toBe(42);
});
});
+2
View File
@@ -57,3 +57,5 @@
enabled: false
- name: response_bot
enabled: false
- name: message_reply_to
enabled: false