Merge branch 'develop' into feat/chat-list-header
This commit is contained in:
+1
-4
@@ -16,10 +16,7 @@ const { t } = useI18n();
|
||||
|
||||
const searchQuery = ref('');
|
||||
|
||||
const contactAttributes = computed(() => {
|
||||
const attributes = useMapGetter('attributes/getAttributesByModelType');
|
||||
return attributes.value('contact_attribute') || [];
|
||||
});
|
||||
const contactAttributes = useMapGetter('attributes/getContactAttributes') || [];
|
||||
|
||||
const hasContactAttributes = computed(
|
||||
() => contactAttributes.value?.length > 0
|
||||
|
||||
@@ -132,7 +132,7 @@ const iconStyles = computed(() => ({
|
||||
}));
|
||||
|
||||
const initialsStyles = computed(() => ({
|
||||
fontSize: `${props.size > 32 ? 16 : props.size / 2}px`,
|
||||
fontSize: `${Math.min(props.size / 2.5, 24)}px`,
|
||||
}));
|
||||
|
||||
const invalidateCurrentImage = () => {
|
||||
|
||||
@@ -21,9 +21,9 @@ provideDropdownContext({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative space-y-2">
|
||||
<div v-on-click-outside="closeMenu" class="relative space-y-2">
|
||||
<slot name="trigger" :is-open :toggle="() => toggle()" />
|
||||
<div v-if="isOpen" v-on-click-outside="closeMenu" class="absolute">
|
||||
<div v-if="isOpen" class="absolute">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -45,7 +45,7 @@ useSidebarKeyboardShortcuts(toggleShortcutModalFn);
|
||||
const expandedItem = useStorage(
|
||||
'next-sidebar-expanded-item',
|
||||
null,
|
||||
localStorage
|
||||
sessionStorage
|
||||
);
|
||||
|
||||
const setExpandedItem = name => {
|
||||
|
||||
@@ -67,7 +67,7 @@ function updateAutoOffline(autoOffline) {
|
||||
<template>
|
||||
<DropdownSection>
|
||||
<div class="grid gap-0">
|
||||
<DropdownItem>
|
||||
<DropdownItem preserve-open>
|
||||
<div class="flex-grow flex items-center gap-1">
|
||||
{{ $t('SIDEBAR.SET_YOUR_AVAILABILITY') }}
|
||||
</div>
|
||||
@@ -90,7 +90,7 @@ function updateAutoOffline(autoOffline) {
|
||||
</div>
|
||||
</Button>
|
||||
</template>
|
||||
<DropdownBody class="min-w-32">
|
||||
<DropdownBody class="min-w-32 z-20">
|
||||
<DropdownItem
|
||||
v-for="status in availabilityStatuses"
|
||||
:key="status.value"
|
||||
|
||||
@@ -23,16 +23,3 @@ export function useSnakeCase(payload) {
|
||||
const unrefPayload = unref(payload);
|
||||
return snakecaseKeys(unrefPayload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a string from snake_case to camelCase
|
||||
* @param {string} str - String to convert (can contain letters, numbers, or both)
|
||||
* Examples: 'hello_world', 'user_123', 'checkbox_2', 'test_string_99'
|
||||
* @returns {string} Converted string in camelCase
|
||||
* Examples: 'helloWorld', 'user123', 'checkbox2', 'testString99'
|
||||
*/
|
||||
export function toCamelCase(str) {
|
||||
return str
|
||||
.toLowerCase()
|
||||
.replace(/_([a-z0-9])/g, (_, char) => char.toUpperCase());
|
||||
}
|
||||
|
||||
@@ -190,7 +190,6 @@ export default {
|
||||
<UserProfilePicture
|
||||
:src="avatarUrl"
|
||||
:name="name"
|
||||
size="72px"
|
||||
@change="updateProfilePicture"
|
||||
@delete="deleteProfilePicture"
|
||||
/>
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import ProfileAvatar from 'v3/components/Form/ProfileAvatar.vue';
|
||||
import { removeEmoji } from 'shared/helpers/emoji';
|
||||
const props = defineProps({
|
||||
import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
|
||||
defineProps({
|
||||
src: {
|
||||
type: String,
|
||||
default: '',
|
||||
@@ -15,8 +13,6 @@ const props = defineProps({
|
||||
|
||||
const emit = defineEmits(['change', 'delete']);
|
||||
|
||||
const userNameWithoutEmoji = computed(() => removeEmoji(props.name));
|
||||
|
||||
const updateProfilePicture = e => {
|
||||
emit('change', e);
|
||||
};
|
||||
@@ -31,10 +27,12 @@ const deleteProfilePicture = () => {
|
||||
<span class="text-sm font-medium text-ash-900">
|
||||
{{ $t('PROFILE_SETTINGS.FORM.PICTURE') }}
|
||||
</span>
|
||||
<ProfileAvatar
|
||||
:src="src"
|
||||
:name="userNameWithoutEmoji"
|
||||
@change="updateProfilePicture"
|
||||
<Avatar
|
||||
:src="src || ''"
|
||||
:name="name || ''"
|
||||
:size="72"
|
||||
allow-upload
|
||||
@upload="updateProfilePicture"
|
||||
@delete="deleteProfilePicture"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -35,12 +35,6 @@ export const getters = {
|
||||
record => record.attribute_model === attributeModel
|
||||
);
|
||||
},
|
||||
getAttributesByModelType: _state => attributeModel => {
|
||||
const records = _state.records.filter(
|
||||
record => record.attribute_model === attributeModel
|
||||
);
|
||||
return camelcaseKeys(records, { deep: true });
|
||||
},
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
|
||||
@@ -133,9 +133,10 @@ export const actions = {
|
||||
}
|
||||
},
|
||||
|
||||
deleteAvatar: async () => {
|
||||
deleteAvatar: async ({ commit }) => {
|
||||
try {
|
||||
await authAPI.deleteAvatar();
|
||||
const response = await authAPI.deleteAvatar();
|
||||
commit(types.SET_CURRENT_USER, response.data);
|
||||
} catch (error) {
|
||||
// Ignore error
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
} from 'shared/helpers/CustomErrors';
|
||||
import types from '../../mutation-types';
|
||||
import ContactAPI from '../../../api/contacts';
|
||||
import decamelizeKeys from 'decamelize-keys';
|
||||
import snakecaseKeys from 'snakecase-keys';
|
||||
import AccountActionsAPI from '../../../api/accountActions';
|
||||
import AnalyticsHelper from '../../../helper/AnalyticsHelper';
|
||||
import { CONTACTS_EVENTS } from '../../../helper/AnalyticsHelper/events';
|
||||
@@ -93,7 +93,7 @@ export const actions = {
|
||||
update: async ({ commit }, { id, isFormData = false, ...contactParams }) => {
|
||||
const { avatar, customAttributes, ...paramsToDecamelize } = contactParams;
|
||||
const decamelizedContactParams = {
|
||||
...decamelizeKeys(paramsToDecamelize),
|
||||
...snakecaseKeys(paramsToDecamelize, { deep: true }),
|
||||
...(customAttributes && { custom_attributes: customAttributes }),
|
||||
...(avatar && { avatar }),
|
||||
};
|
||||
@@ -114,7 +114,7 @@ export const actions = {
|
||||
},
|
||||
|
||||
create: async ({ commit }, { isFormData = false, ...contactParams }) => {
|
||||
const decamelizedContactParams = decamelizeKeys(contactParams, {
|
||||
const decamelizedContactParams = snakecaseKeys(contactParams, {
|
||||
deep: true,
|
||||
});
|
||||
commit(types.SET_CONTACT_UI_FLAG, { isCreating: true });
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import InitialsAvatar from './InitialsAvatar.vue';
|
||||
const props = defineProps({
|
||||
src: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['change', 'delete']);
|
||||
|
||||
const hasImageLoaded = ref(false);
|
||||
const imageLoadedError = ref(false);
|
||||
const fileInputRef = ref(null);
|
||||
|
||||
const shouldShowImage = computed(() => props.src && !imageLoadedError.value);
|
||||
|
||||
const onImageLoadError = () => {
|
||||
imageLoadedError.value = true;
|
||||
};
|
||||
|
||||
const onImageLoad = () => {
|
||||
hasImageLoaded.value = true;
|
||||
imageLoadedError.value = false;
|
||||
};
|
||||
|
||||
const openFileInput = () => {
|
||||
fileInputRef.value.click();
|
||||
};
|
||||
|
||||
const onImageUpload = event => {
|
||||
const [file] = event.target.files;
|
||||
emit('change', {
|
||||
file,
|
||||
url: file ? URL.createObjectURL(file) : null,
|
||||
});
|
||||
};
|
||||
|
||||
const onAvatarDelete = () => {
|
||||
emit('delete');
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative rounded-xl h-[72px] w-[72px] cursor-pointe group">
|
||||
<img
|
||||
v-if="shouldShowImage"
|
||||
class="rounded-xl h-[72px] w-[72px]"
|
||||
:alt="name"
|
||||
:src="src"
|
||||
draggable="false"
|
||||
@load="onImageLoad"
|
||||
@error="onImageLoadError"
|
||||
/>
|
||||
<InitialsAvatar v-else-if="!shouldShowImage" :name="name" :size="72" />
|
||||
|
||||
<input
|
||||
ref="fileInputRef"
|
||||
type="file"
|
||||
accept="image/png, image/jpeg, image/jpg, image/gif, image/webp"
|
||||
hidden
|
||||
@change="onImageUpload"
|
||||
/>
|
||||
<div class="hidden group-hover:block">
|
||||
<button
|
||||
v-if="src"
|
||||
class="absolute z-10 flex items-center justify-center w-6 h-6 p-1 border border-white rounded-full select-none dark:border-ash-75 reset-base -top-2 -right-2 bg-ash-300"
|
||||
@click="onAvatarDelete"
|
||||
>
|
||||
<fluent-icon icon="dismiss" size="16" class="text-ash-900" />
|
||||
</button>
|
||||
<button
|
||||
class="reset-base absolute h-[72px] w-[72px] top-0 left-0 rounded-xl select-none flex items-center justify-center bg-modal-backdrop-dark dark:bg-modal-backdrop-dark"
|
||||
@click="openFileInput"
|
||||
>
|
||||
<fluent-icon icon="avatar-upload" size="32" class="text-white" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user