Merge branch 'develop' into feat/captain-limits-frontend

This commit is contained in:
Sojan Jose
2025-01-23 01:25:06 +05:30
committed by GitHub
5 changed files with 156 additions and 81 deletions
@@ -35,7 +35,7 @@ const FORM_CONFIG = {
EMAIL_ADDRESS: { field: 'email' },
PHONE_NUMBER: { field: 'phoneNumber' },
CITY: { field: 'additionalAttributes.city' },
COUNTRY: { field: 'additionalAttributes.country' },
COUNTRY: { field: 'additionalAttributes.countryCode' },
BIO: { field: 'additionalAttributes.description' },
COMPANY_NAME: { field: 'additionalAttributes.companyName' },
};
@@ -123,7 +123,7 @@ const prepareStateBasedOnProps = () => {
};
const countryOptions = computed(() =>
countries.map(({ name }) => ({ label: name, value: name }))
countries.map(({ name, id }) => ({ label: name, value: id }))
);
const editDetailsForm = computed(() =>
@@ -205,8 +205,8 @@ const getMessageType = key => {
};
const handleCountrySelection = value => {
const selectedCountry = countries.find(option => option.name === value);
state.additionalAttributes.countryCode = selectedCountry?.id || '';
const selectedCountry = countries.find(option => option.id === value);
state.additionalAttributes.country = selectedCountry?.name || '';
emit('update', state);
};
@@ -242,7 +242,7 @@ defineExpose({
<template v-for="item in editDetailsForm" :key="item.key">
<ComboBox
v-if="item.key === 'COUNTRY'"
v-model="state.additionalAttributes.country"
v-model="state.additionalAttributes.countryCode"
:options="countryOptions"
:placeholder="item.placeholder"
class="[&>div>button]:h-8"
@@ -18,6 +18,7 @@ const attachment = computed(() => {
const hasError = ref(false);
const showGallery = ref(false);
const isDownloading = ref(false);
const handleError = () => {
hasError.value = true;
@@ -26,7 +27,14 @@ const handleError = () => {
const downloadAttachment = async () => {
const { fileType, dataUrl, extension } = attachment.value;
downloadFile({ url: dataUrl, type: fileType, extension });
try {
isDownloading.value = true;
await downloadFile({ url: dataUrl, type: fileType, extension });
} catch (error) {
// error
} finally {
isDownloading.value = false;
}
};
</script>
@@ -60,7 +68,9 @@ const downloadAttachment = async () => {
slate
icon="i-lucide-download"
class="opacity-60"
@click="downloadAttachment"
:is-loading="isDownloading"
:disabled="isDownloading"
@click.stop="downloadAttachment"
/>
</div>
</div>
@@ -1,34 +1,67 @@
<script setup>
import { ref, computed, onMounted, nextTick, defineEmits } from 'vue';
import { computed, onMounted, nextTick, useTemplateRef } from 'vue';
import { useWindowSize, useElementBounding } from '@vueuse/core';
const { x, y } = defineProps({
const props = defineProps({
x: { type: Number, default: 0 },
y: { type: Number, default: 0 },
});
const emit = defineEmits(['close']);
const left = ref(x);
const top = ref(y);
const menuRef = useTemplateRef('menuRef');
const style = computed(() => ({
top: top.value + 'px',
left: left.value + 'px',
}));
const { width: windowWidth, height: windowHeight } = useWindowSize();
const { width: menuWidth, height: menuHeight } = useElementBounding(menuRef);
const calculatePosition = (x, y, menuW, menuH, windowW, windowH) => {
// Initial position
let left = x;
let top = y;
// Boundary checks
const isOverflowingRight = left + menuW > windowW;
const isOverflowingBottom = top + menuH > windowH;
// Adjust position if overflowing
if (isOverflowingRight) left = windowW - menuW;
if (isOverflowingBottom) top = windowH - menuH;
return {
left: Math.max(0, left),
top: Math.max(0, top),
};
};
const position = computed(() => {
if (!menuRef.value) return { top: `${props.y}px`, left: `${props.x}px` };
const { left, top } = calculatePosition(
props.x,
props.y,
menuWidth.value,
menuHeight.value,
windowWidth.value,
windowHeight.value
);
return {
top: `${top}px`,
left: `${left}px`,
};
});
const target = ref();
onMounted(() => {
nextTick(() => {
target.value.focus();
});
nextTick(() => menuRef.value?.focus());
});
</script>
<template>
<Teleport to="body">
<div
ref="target"
ref="menuRef"
class="fixed outline-none z-[9999] cursor-pointer"
:style="style"
:style="position"
tabindex="0"
@blur="emit('close')"
>
@@ -5,6 +5,7 @@ import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
import { messageTimestamp } from 'shared/helpers/timeHelper';
import { downloadFile } from '@chatwoot/utils';
import NextButton from 'dashboard/components-next/button/Button.vue';
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
const props = defineProps({
@@ -33,6 +34,7 @@ const ALLOWED_FILE_TYPES = {
const MAX_ZOOM_LEVEL = 2;
const MIN_ZOOM_LEVEL = 1;
const isDownloading = ref(false);
const zoomScale = ref(1);
const activeAttachment = ref({});
const activeFileType = ref('');
@@ -117,12 +119,20 @@ const onClickChangeAttachment = (attachment, index) => {
zoomScale.value = 1;
};
const onClickDownload = () => {
const onClickDownload = async () => {
const { file_type: type, data_url: url, extension } = activeAttachment.value;
if (!Object.values(ALLOWED_FILE_TYPES).includes(type)) {
return;
}
downloadFile({ url, type, extension });
try {
isDownloading.value = true;
await downloadFile({ url, type, extension });
} catch (error) {
// error
} finally {
isDownloading.value = false;
}
};
const onRotate = type => {
@@ -162,6 +172,12 @@ const onZoom = scale => {
};
const onClickZoomImage = () => {
// If already at max zoom, clicking should zoom out to minimum
if (zoomScale.value >= MAX_ZOOM_LEVEL) {
zoomScale.value = MIN_ZOOM_LEVEL;
return;
}
// Otherwise zoom in
onZoom(0.1);
};
@@ -211,7 +227,6 @@ onMounted(() => {
:on-close="onClose"
>
<div
v-on-clickaway="onClose"
class="bg-white dark:bg-slate-900 flex flex-col h-[inherit] w-[inherit] overflow-hidden"
@click="onClose"
>
@@ -256,63 +271,54 @@ onMounted(() => {
<div
class="items-center flex gap-2 justify-end min-w-[8rem] sm:min-w-[15rem]"
>
<woot-button
<NextButton
v-if="isImage"
size="large"
color-scheme="secondary"
variant="clear"
icon="zoom-in"
icon="i-lucide-zoom-in"
slate
ghost
@click="onZoom(0.1)"
/>
<woot-button
<NextButton
v-if="isImage"
size="large"
color-scheme="secondary"
variant="clear"
icon="zoom-out"
icon="i-lucide-zoom-out"
slate
ghost
@click="onZoom(-0.1)"
/>
<woot-button
<NextButton
v-if="isImage"
size="large"
color-scheme="secondary"
variant="clear"
icon="arrow-rotate-counter-clockwise"
icon="i-lucide-rotate-ccw"
slate
ghost
@click="onRotate('counter-clockwise')"
/>
<woot-button
<NextButton
v-if="isImage"
size="large"
color-scheme="secondary"
variant="clear"
icon="arrow-rotate-clockwise"
icon="i-lucide-rotate-cw"
slate
ghost
@click="onRotate('clockwise')"
/>
<woot-button
size="large"
color-scheme="secondary"
variant="clear"
icon="arrow-download"
<NextButton
icon="i-lucide-download"
slate
ghost
:is-loading="isDownloading"
:disabled="isDownloading"
@click="onClickDownload"
/>
<woot-button
size="large"
color-scheme="secondary"
variant="clear"
icon="dismiss"
@click="onClose"
/>
<NextButton icon="i-lucide-x" slate ghost @click="onClose" />
</div>
</div>
<div class="flex items-center justify-center w-full h-full">
<div class="flex justify-center min-w-[6.25rem] w-[6.25rem]">
<woot-button
<NextButton
v-if="hasMoreThanOneAttachment"
class="z-10"
size="large"
variant="smooth"
color-scheme="primary"
icon="chevron-left"
icon="i-lucide-chevron-left"
class="z-10 disabled:pointer-events-auto"
blue
faded
lg
:disabled="activeImageIndex === 0"
@click.stop="
onClickChangeAttachment(
@@ -354,14 +360,14 @@ onMounted(() => {
</div>
</div>
<div class="flex justify-center min-w-[6.25rem] w-[6.25rem]">
<woot-button
<NextButton
v-if="hasMoreThanOneAttachment"
class="z-10"
size="large"
variant="smooth"
color-scheme="primary"
icon="i-lucide-chevron-right"
class="z-10 disabled:pointer-events-auto"
blue
faded
lg
:disabled="activeImageIndex === allAttachments.length - 1"
icon="chevron-right"
@click.stop="
onClickChangeAttachment(
allAttachments[activeImageIndex + 1],
@@ -1,20 +1,45 @@
<script>
export default {
props: {
option: {
type: Object,
default: () => {},
},
subMenuAvailable: {
type: Boolean,
default: true,
},
<script setup>
import { computed, useTemplateRef } from 'vue';
import { useWindowSize, useElementBounding } from '@vueuse/core';
defineProps({
option: {
type: Object,
default: () => {},
},
};
subMenuAvailable: {
type: Boolean,
default: true,
},
});
const menuRef = useTemplateRef('menuRef');
const { width: windowWidth, height: windowHeight } = useWindowSize();
const { bottom, right } = useElementBounding(menuRef);
// Vertical position
const verticalPosition = computed(() => {
const SUBMENU_HEIGHT = 240; // 15rem in pixels
const spaceBelow = windowHeight.value - bottom.value;
return spaceBelow < SUBMENU_HEIGHT ? 'bottom-0' : 'top-0';
});
// Horizontal position
const horizontalPosition = computed(() => {
const SUBMENU_WIDTH = 240;
const spaceRight = windowWidth.value - right.value;
return spaceRight < SUBMENU_WIDTH ? 'right-full' : 'left-full';
});
const submenuPosition = computed(() => [
verticalPosition.value,
horizontalPosition.value,
]);
</script>
<template>
<div
ref="menuRef"
class="text-slate-800 dark:text-slate-100 menu-with-submenu min-width-calc w-full p-1 flex items-center h-7 rounded-md relative bg-n-alpha-3/50 backdrop-blur-[100px] justify-between hover:bg-n-brand/10 cursor-pointer dark:hover:bg-n-solid-3"
:class="!subMenuAvailable ? 'opacity-50 cursor-not-allowed' : ''"
>
@@ -25,7 +50,8 @@ export default {
<fluent-icon icon="chevron-right" size="12" />
<div
v-if="subMenuAvailable"
class="submenu bg-n-alpha-3 backdrop-blur-[100px] p-1 shadow-lg rounded-md absolute left-full top-0 hidden min-h-min max-h-[15rem] overflow-y-auto overflow-x-hidden cursor-pointer"
class="submenu bg-n-alpha-3 backdrop-blur-[100px] p-1 shadow-lg rounded-md absolute hidden max-h-[15rem] overflow-y-auto overflow-x-hidden cursor-pointer"
:class="submenuPosition"
>
<slot />
</div>