Merge branch 'develop' into feat/voice-channel

This commit is contained in:
Sojan Jose
2025-05-27 16:49:02 -07:00
committed by GitHub
702 changed files with 47238 additions and 7574 deletions
@@ -1,14 +1,16 @@
<script>
import { mapGetters } from 'vuex';
import Spinner from 'shared/components/Spinner.vue';
import { CSAT_RATINGS } from 'shared/constants/messages';
import { CSAT_RATINGS, CSAT_DISPLAY_TYPES } from 'shared/constants/messages';
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
import StarRating from 'shared/components/StarRating.vue';
import { getContrastingTextColor } from '@chatwoot/utils';
export default {
components: {
Spinner,
FluentIcon,
StarRating,
},
props: {
messageContentAttributes: {
@@ -19,6 +21,14 @@ export default {
type: Number,
required: true,
},
displayType: {
type: String,
default: CSAT_DISPLAY_TYPES.EMOJI,
},
message: {
type: String,
default: '',
},
},
data() {
return {
@@ -47,7 +57,13 @@ export default {
title() {
return this.isRatingSubmitted
? this.$t('CSAT.SUBMITTED_TITLE')
: this.$t('CSAT.TITLE');
: this.message || this.$t('CSAT.TITLE');
},
isEmojiType() {
return this.displayType === CSAT_DISPLAY_TYPES.EMOJI;
},
isStarType() {
return this.displayType === CSAT_DISPLAY_TYPES.STAR;
},
},
@@ -88,10 +104,15 @@ export default {
this.isUpdating = false;
}
},
selectRating(rating) {
this.selectedRating = rating.value;
this.onSubmit();
},
selectStarRating(value) {
this.selectedRating = value;
this.onSubmit();
},
},
};
</script>
@@ -104,7 +125,7 @@ export default {
<h6 class="text-n-slate-12 text-sm font-medium pt-5 px-2.5 text-center">
{{ title }}
</h6>
<div class="ratings flex justify-around py-5 px-4">
<div v-if="isEmojiType" class="ratings flex justify-around py-5 px-4">
<button
v-for="rating in ratings"
:key="rating.key"
@@ -114,6 +135,12 @@ export default {
{{ rating.emoji }}
</button>
</div>
<StarRating
v-else-if="isStarType"
:selected-rating="selectedRating"
:is-disabled="isRatingSubmitted"
@select-rating="selectStarRating"
/>
<form
v-if="!isFeedbackSubmitted"
class="feedback-form flex"
@@ -0,0 +1,65 @@
<script setup>
import { ref, defineProps, defineEmits } from 'vue';
const props = defineProps({
selectedRating: {
type: Number,
default: null,
},
isDisabled: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['selectRating']);
const starRatings = [1, 2, 3, 4, 5];
const hoveredRating = ref(0);
const onHoverRating = value => {
if (props.isDisabled) return;
hoveredRating.value = value;
};
const selectRating = value => {
if (props.isDisabled) return;
emit('selectRating', value);
};
const getStarClass = value => {
const isStarActive =
(hoveredRating.value > 0 &&
!props.isDisabled &&
hoveredRating.value >= value) ||
props.selectedRating >= value;
const starTypeClass = isStarActive
? 'i-ri-star-fill text-n-amber-9'
: 'i-ri-star-line text-n-slate-10';
return starTypeClass;
};
</script>
<template>
<div class="flex justify-center py-5 px-4 gap-3">
<button
v-for="value in starRatings"
:key="value"
type="button"
class="rounded-full p-1 transition-all duration-200 focus:enabled:scale-[1.2] focus-within:enabled:scale-[1.2] hover:enabled:scale-[1.2] focus:outline-none flex items-center flex-shrink-0"
:class="{ 'cursor-not-allowed opacity-50': isDisabled }"
:disabled="isDisabled"
:aria-label="'Star ' + value"
@click="selectRating(value)"
@mouseenter="onHoverRating(value)"
@mouseleave="onHoverRating(0)"
>
<span
:class="getStarClass(value)"
class="transition-all duration-500 text-2xl"
/>
</button>
</div>
</template>
@@ -100,6 +100,11 @@ export const CSAT_RATINGS = [
},
];
export const CSAT_DISPLAY_TYPES = {
EMOJI: 'emoji',
STAR: 'star',
};
export const AUDIO_FORMATS = {
WEBM: 'audio/webm',
OGG: 'audio/ogg',
@@ -0,0 +1,26 @@
export default {
clearAll() {
window.sessionStorage.clear();
},
get(key) {
try {
const value = window.sessionStorage.getItem(key);
return value ? JSON.parse(value) : null;
} catch (error) {
return window.sessionStorage.getItem(key);
}
},
set(key, value) {
if (typeof value === 'object') {
window.sessionStorage.setItem(key, JSON.stringify(value));
} else {
window.sessionStorage.setItem(key, value);
}
},
remove(key) {
window.sessionStorage.removeItem(key);
},
};
@@ -0,0 +1,137 @@
import SessionStorage from '../sessionStorage';
// Mocking sessionStorage
const sessionStorageMock = (() => {
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, 'sessionStorage', {
value: sessionStorageMock,
});
describe('SessionStorage utility', () => {
beforeEach(() => {
sessionStorage.clear();
});
describe('clearAll method', () => {
it('should clear all items from sessionStorage', () => {
sessionStorage.setItem('testKey1', 'testValue1');
sessionStorage.setItem('testKey2', 'testValue2');
SessionStorage.clearAll();
expect(sessionStorage.getItem('testKey1')).toBeNull();
expect(sessionStorage.getItem('testKey2')).toBeNull();
});
});
describe('get method', () => {
it('should retrieve and parse JSON values correctly', () => {
const testObject = { a: 1, b: 'test' };
sessionStorage.setItem('testKey', JSON.stringify(testObject));
expect(SessionStorage.get('testKey')).toEqual(testObject);
});
it('should return null for non-existent keys', () => {
expect(SessionStorage.get('nonExistentKey')).toBeNull();
});
it('should handle non-JSON values by returning the raw value', () => {
sessionStorage.setItem('testKey', 'plain string value');
expect(SessionStorage.get('testKey')).toBe('plain string value');
});
it('should handle malformed JSON gracefully', () => {
sessionStorage.setItem('testKey', '{malformed:json}');
expect(SessionStorage.get('testKey')).toBe('{malformed:json}');
});
});
describe('set method', () => {
it('should store object values as JSON strings', () => {
const testObject = { a: 1, b: 'test' };
SessionStorage.set('testKey', testObject);
expect(sessionStorage.getItem('testKey')).toBe(
JSON.stringify(testObject)
);
});
it('should store primitive values directly', () => {
SessionStorage.set('stringKey', 'test string');
expect(sessionStorage.getItem('stringKey')).toBe('test string');
SessionStorage.set('numberKey', 42);
expect(sessionStorage.getItem('numberKey')).toBe('42');
SessionStorage.set('booleanKey', true);
expect(sessionStorage.getItem('booleanKey')).toBe('true');
});
it('should handle null values', () => {
SessionStorage.set('nullKey', null);
expect(sessionStorage.getItem('nullKey')).toBe('null');
expect(SessionStorage.get('nullKey')).toBeNull();
});
it('should handle undefined values', () => {
SessionStorage.set('undefinedKey', undefined);
expect(sessionStorage.getItem('undefinedKey')).toBe('undefined');
});
});
describe('remove method', () => {
it('should remove an item from sessionStorage', () => {
SessionStorage.set('testKey', 'testValue');
expect(SessionStorage.get('testKey')).toBe('testValue');
SessionStorage.remove('testKey');
expect(SessionStorage.get('testKey')).toBeNull();
});
it('should do nothing when removing a non-existent key', () => {
expect(() => {
SessionStorage.remove('nonExistentKey');
}).not.toThrow();
});
});
describe('Integration of methods', () => {
it('should set, get, and remove values correctly', () => {
SessionStorage.set('testKey', { value: 'test' });
expect(SessionStorage.get('testKey')).toEqual({ value: 'test' });
SessionStorage.remove('testKey');
expect(SessionStorage.get('testKey')).toBeNull();
});
it('should correctly handle impersonation flag (common use case)', () => {
SessionStorage.set('impersonationUser', true);
expect(SessionStorage.get('impersonationUser')).toBe(true);
expect(sessionStorage.getItem('impersonationUser')).toBe('true');
SessionStorage.remove('impersonationUser');
expect(SessionStorage.get('impersonationUser')).toBeNull();
});
});
});