Merge branch 'develop' into fix/cw-4085

This commit is contained in:
Shivam Mishra
2025-03-20 10:22:49 +05:30
committed by GitHub
595 changed files with 9887 additions and 2291 deletions
@@ -1253,7 +1253,7 @@ export default {
@apply relative py-0 px-4 -mt-px;
textarea {
@apply shadow-none border-transparent bg-transparent m-0 max-h-60 min-h-[3rem] pt-4 pb-0 px-0 resize-none;
@apply shadow-none outline-none border-transparent bg-transparent m-0 max-h-60 min-h-[3rem] pt-4 pb-0 px-0 resize-none;
}
}
@@ -97,7 +97,7 @@ export default {
<woot-input
v-model="v$.toEmailsVal.$model"
type="text"
class="[&>input]:!mb-0 [&>input]:border-transparent [&>input]:h-8 [&>input]:text-sm [&>input]:!border-0 [&>input]:border-none [&>input]:!bg-transparent dark:[&>input]:!bg-transparent"
class="[&>input]:!mb-0 [&>input]:border-transparent [&>input]:!outline-none [&>input]:h-8 [&>input]:!text-sm [&>input]:!border-0 [&>input]:border-none [&>input]:!bg-transparent dark:[&>input]:!bg-transparent"
:class="{ error: v$.toEmailsVal.$error }"
:placeholder="$t('CONVERSATION.REPLYBOX.EMAIL_HEAD.CC.PLACEHOLDER')"
@blur="onBlur"
@@ -113,7 +113,7 @@ export default {
<div class="flex-1 min-w-0 m-0 rounded-none whitespace-nowrap">
<woot-input
v-model="v$.ccEmailsVal.$model"
class="[&>input]:!mb-0 [&>input]:border-transparent [&>input]:h-8 [&>input]:text-sm [&>input]:!border-0 [&>input]:border-none [&>input]:!bg-transparent dark:[&>input]:!bg-transparent"
class="[&>input]:!mb-0 [&>input]:border-transparent [&>input]:!outline-none [&>input]:h-8 [&>input]:!text-sm [&>input]:!border-0 [&>input]:border-none [&>input]:!bg-transparent dark:[&>input]:!bg-transparent"
type="text"
:class="{ error: v$.ccEmailsVal.$error }"
:placeholder="$t('CONVERSATION.REPLYBOX.EMAIL_HEAD.CC.PLACEHOLDER')"
@@ -142,7 +142,7 @@ export default {
<woot-input
v-model="v$.bccEmailsVal.$model"
type="text"
class="[&>input]:!mb-0 [&>input]:border-transparent [&>input]:h-8 [&>input]:text-sm [&>input]:!border-0 [&>input]:border-none [&>input]:!bg-transparent dark:[&>input]:!bg-transparent"
class="[&>input]:!mb-0 [&>input]:border-transparent [&>input]:!outline-none [&>input]:h-8 [&>input]:!text-sm [&>input]:!border-0 [&>input]:border-none [&>input]:!bg-transparent dark:[&>input]:!bg-transparent"
:class="{ error: v$.bccEmailsVal.$error }"
:placeholder="
$t('CONVERSATION.REPLYBOX.EMAIL_HEAD.BCC.PLACEHOLDER')
@@ -0,0 +1,105 @@
<script setup>
import { computed } from 'vue';
import { format } from 'date-fns';
import { useI18n } from 'vue-i18n';
const props = defineProps({
order: {
type: Object,
required: true,
},
});
const { t } = useI18n();
const formatDate = dateString => {
return format(new Date(dateString), 'MMM d, yyyy');
};
const formatCurrency = (amount, currency) => {
return new Intl.NumberFormat('en', {
style: 'currency',
currency: currency || 'USD',
}).format(amount);
};
const getStatusClass = status => {
const classes = {
paid: 'bg-n-teal-5 text-n-teal-12',
};
return classes[status] || 'bg-slate-50 text-slate-700';
};
const getStatusI18nKey = (type, status = '') => {
return `CONVERSATION_SIDEBAR.SHOPIFY.${type.toUpperCase()}_STATUS.${status.toUpperCase()}`;
};
const fulfillmentStatus = computed(() => {
const { fulfillment_status: status } = props.order;
if (!status) {
return '';
}
return t(getStatusI18nKey('FULFILLMENT', status));
});
const financialStatus = computed(() => {
const { financial_status: status } = props.order;
if (!status) {
return '';
}
return t(getStatusI18nKey('FINANCIAL', status));
});
const getFulfillmentClass = status => {
const classes = {
fulfilled: 'text-green-600',
partial: 'text-yellow-600',
unfulfilled: 'text-red-600',
};
return classes[status] || 'text-slate-600';
};
</script>
<template>
<div
class="py-3 border-b border-n-weak last:border-b-0 flex flex-col gap-1.5"
>
<div class="flex justify-between items-center">
<div class="font-medium flex">
<a
:href="order.admin_url"
target="_blank"
rel="noopener noreferrer"
class="hover:underline text-n-slate-12 cursor-pointer truncate"
>
{{ $t('CONVERSATION_SIDEBAR.SHOPIFY.ORDER_ID', { id: order.id }) }}
<i class="i-lucide-external-link pl-5" />
</a>
</div>
<div
:class="getStatusClass(order.financial_status)"
class="text-xs px-2 py-1 rounded capitalize truncate"
:title="financialStatus"
>
{{ financialStatus }}
</div>
</div>
<div class="text-sm text-n-slate-12">
<span class="text-n-slate-11 border-r border-n-weak pr-2">
{{ formatDate(order.created_at) }}
</span>
<span class="text-n-slate-11 pl-2">
{{ formatCurrency(order.total_price, order.currency) }}
</span>
</div>
<div v-if="fulfillmentStatus">
<span
:class="getFulfillmentClass(order.fulfillment_status)"
class="capitalize font-medium"
:title="fulfillmentStatus"
>
{{ fulfillmentStatus }}
</span>
</div>
</div>
</template>
@@ -0,0 +1,71 @@
<script setup>
import { ref, watch, computed } from 'vue';
import { useFunctionGetter } from 'dashboard/composables/store';
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
import ShopifyAPI from '../../../api/integrations/shopify';
import ShopifyOrderItem from './ShopifyOrderItem.vue';
const props = defineProps({
contactId: {
type: [Number, String],
required: true,
},
});
const contact = useFunctionGetter('contacts/getContact', props.contactId);
const hasSearchableInfo = computed(
() => !!contact.value?.email || !!contact.value?.phone_number
);
const orders = ref([]);
const loading = ref(true);
const error = ref('');
const fetchOrders = async () => {
try {
loading.value = true;
const response = await ShopifyAPI.getOrders(props.contactId);
orders.value = response.data.orders;
} catch (e) {
error.value =
e.response?.data?.error || 'CONVERSATION_SIDEBAR.SHOPIFY.ERROR';
} finally {
loading.value = false;
}
};
watch(
() => props.contactId,
() => {
if (hasSearchableInfo.value) {
fetchOrders();
}
},
{ immediate: true }
);
</script>
<template>
<div class="px-4 py-2 text-n-slate-12">
<div v-if="!hasSearchableInfo" class="text-center text-n-slate-12">
{{ $t('CONVERSATION_SIDEBAR.SHOPIFY.NO_SHOPIFY_ORDERS') }}
</div>
<div v-else-if="loading" class="flex justify-center items-center p-4">
<Spinner size="32" class="text-n-brand" />
</div>
<div v-else-if="error" class="text-center text-n-ruby-12">
{{ error }}
</div>
<div v-else-if="!orders.length" class="text-center text-n-slate-12">
{{ $t('CONVERSATION_SIDEBAR.SHOPIFY.NO_SHOPIFY_ORDERS') }}
</div>
<div v-else>
<ShopifyOrderItem
v-for="order in orders"
:key="order.id"
:order="order"
/>
</div>
</div>
</template>
@@ -166,188 +166,190 @@ onMounted(() => {
</script>
<template>
<woot-modal
v-model:show="show"
full-width
:show-close-button="false"
:on-close="onClose"
>
<div
class="bg-n-background flex flex-col h-[inherit] w-[inherit] overflow-hidden select-none"
@click="onClose"
<Teleport to="body">
<woot-modal
v-model:show="show"
full-width
:show-close-button="false"
:on-close="onClose"
>
<header
class="z-10 flex items-center justify-between w-full h-16 px-6 py-2 bg-n-background border-b border-n-weak"
@click.stop
<div
class="bg-n-background flex flex-col h-[inherit] w-[inherit] overflow-hidden select-none"
@click="onClose"
>
<div
v-if="senderDetails"
class="flex items-center min-w-[15rem] shrink-0"
<header
class="z-10 flex items-center justify-between w-full h-16 px-6 py-2 bg-n-background border-b border-n-weak"
@click.stop
>
<Thumbnail
v-if="senderDetails.avatar"
:username="senderDetails.name"
:src="senderDetails.avatar"
class="flex-shrink-0"
/>
<div class="flex flex-col ml-2 rtl:ml-0 rtl:mr-2 overflow-hidden">
<h3 class="text-base leading-5 m-0 font-medium">
<span
class="overflow-hidden text-n-slate-12 whitespace-nowrap text-ellipsis"
>
{{ senderDetails.name }}
</span>
</h3>
<span
class="text-xs text-n-slate-11 whitespace-nowrap text-ellipsis"
>
{{ readableTime }}
</span>
</div>
</div>
<div
class="flex-1 mx-2 px-2 truncate text-sm font-medium text-center text-n-slate-12"
>
<span v-dompurify-html="fileNameFromDataUrl" class="truncate" />
</div>
<div class="flex items-center gap-2 ml-2 shrink-0">
<NextButton
v-if="isImage"
icon="i-lucide-zoom-in"
slate
ghost
@click="onZoom(0.1)"
/>
<NextButton
v-if="isImage"
icon="i-lucide-zoom-out"
slate
ghost
@click="onZoom(-0.1)"
/>
<NextButton
v-if="isImage"
icon="i-lucide-rotate-ccw"
slate
ghost
@click="onRotate('counter-clockwise')"
/>
<NextButton
v-if="isImage"
icon="i-lucide-rotate-cw"
slate
ghost
@click="onRotate('clockwise')"
/>
<NextButton
icon="i-lucide-download"
slate
ghost
:is-loading="isDownloading"
:disabled="isDownloading"
@click="onClickDownload"
/>
<NextButton icon="i-lucide-x" slate ghost @click="onClose" />
</div>
</header>
<main class="flex items-stretch flex-1 h-full overflow-hidden">
<div class="flex items-center justify-center w-16 shrink-0">
<NextButton
v-if="hasMoreThanOneAttachment"
icon="i-lucide-chevron-left"
class="z-10"
blue
faded
lg
:disabled="activeImageIndex === 0"
@click.stop="
onClickChangeAttachment(
allAttachments[activeImageIndex - 1],
activeImageIndex - 1
)
"
/>
</div>
<div class="flex-1 flex items-center justify-center overflow-hidden">
<div
v-if="isImage"
:style="imageWrapperStyle"
class="flex items-center justify-center origin-center"
:class="{
// Adjust dimensions when rotated 90/270 degrees to maintain visibility
// and prevent image from overflowing container in different aspect ratios
'w-[calc(100dvh-8rem)] h-[calc(100dvw-7rem)]':
activeImageRotation % 180 !== 0,
'size-full': activeImageRotation % 180 === 0,
}"
v-if="senderDetails"
class="flex items-center min-w-[15rem] shrink-0"
>
<img
ref="imageRef"
:key="activeAttachment.message_id"
:src="activeAttachment.data_url"
:style="imageStyle"
class="max-h-full max-w-full object-contain duration-100 ease-in-out transform select-none"
@click.stop
@dblclick.stop="onDoubleClickZoomImage"
@wheel.prevent.stop="onWheelImageZoom"
@mousemove="onMouseMove"
@mouseleave="onMouseLeave"
<Thumbnail
v-if="senderDetails.avatar"
:username="senderDetails.name"
:src="senderDetails.avatar"
class="flex-shrink-0"
/>
<div class="flex flex-col ml-2 rtl:ml-0 rtl:mr-2 overflow-hidden">
<h3 class="text-base leading-5 m-0 font-medium">
<span
class="overflow-hidden text-n-slate-12 whitespace-nowrap text-ellipsis"
>
{{ senderDetails.name }}
</span>
</h3>
<span
class="text-xs text-n-slate-11 whitespace-nowrap text-ellipsis"
>
{{ readableTime }}
</span>
</div>
</div>
<div
class="flex-1 mx-2 px-2 truncate text-sm font-medium text-center text-n-slate-12"
>
<span v-dompurify-html="fileNameFromDataUrl" class="truncate" />
</div>
<div class="flex items-center gap-2 ml-2 shrink-0">
<NextButton
v-if="isImage"
icon="i-lucide-zoom-in"
slate
ghost
@click="onZoom(0.1)"
/>
<NextButton
v-if="isImage"
icon="i-lucide-zoom-out"
slate
ghost
@click="onZoom(-0.1)"
/>
<NextButton
v-if="isImage"
icon="i-lucide-rotate-ccw"
slate
ghost
@click="onRotate('counter-clockwise')"
/>
<NextButton
v-if="isImage"
icon="i-lucide-rotate-cw"
slate
ghost
@click="onRotate('clockwise')"
/>
<NextButton
icon="i-lucide-download"
slate
ghost
:is-loading="isDownloading"
:disabled="isDownloading"
@click="onClickDownload"
/>
<NextButton icon="i-lucide-x" slate ghost @click="onClose" />
</div>
</header>
<main class="flex items-stretch flex-1 h-full overflow-hidden">
<div class="flex items-center justify-center w-16 shrink-0">
<NextButton
v-if="hasMoreThanOneAttachment"
icon="i-lucide-chevron-left"
class="z-10"
blue
faded
lg
:disabled="activeImageIndex === 0"
@click.stop="
onClickChangeAttachment(
allAttachments[activeImageIndex - 1],
activeImageIndex - 1
)
"
/>
</div>
<video
v-if="isVideo"
:key="activeAttachment.message_id"
:src="activeAttachment.data_url"
controls
playsInline
class="max-h-full max-w-full object-contain"
@click.stop
/>
<div class="flex-1 flex items-center justify-center overflow-hidden">
<div
v-if="isImage"
:style="imageWrapperStyle"
class="flex items-center justify-center origin-center"
:class="{
// Adjust dimensions when rotated 90/270 degrees to maintain visibility
// and prevent image from overflowing container in different aspect ratios
'w-[calc(100dvh-8rem)] h-[calc(100dvw-7rem)]':
activeImageRotation % 180 !== 0,
'size-full': activeImageRotation % 180 === 0,
}"
>
<img
ref="imageRef"
:key="activeAttachment.message_id"
:src="activeAttachment.data_url"
:style="imageStyle"
class="max-h-full max-w-full object-contain duration-100 ease-in-out transform select-none"
@click.stop
@dblclick.stop="onDoubleClickZoomImage"
@wheel.prevent.stop="onWheelImageZoom"
@mousemove="onMouseMove"
@mouseleave="onMouseLeave"
/>
</div>
<audio
v-if="isAudio"
:key="activeAttachment.message_id"
controls
class="w-full max-w-md"
@click.stop
>
<source :src="`${activeAttachment.data_url}?t=${Date.now()}`" />
</audio>
</div>
<video
v-if="isVideo"
:key="activeAttachment.message_id"
:src="activeAttachment.data_url"
controls
playsInline
class="max-h-full max-w-full object-contain"
@click.stop
/>
<div class="flex items-center justify-center w-16 shrink-0">
<NextButton
v-if="hasMoreThanOneAttachment"
icon="i-lucide-chevron-right"
class="z-10"
blue
faded
lg
:disabled="activeImageIndex === allAttachments.length - 1"
@click.stop="
onClickChangeAttachment(
allAttachments[activeImageIndex + 1],
activeImageIndex + 1
)
"
/>
</div>
</main>
<audio
v-if="isAudio"
:key="activeAttachment.message_id"
controls
class="w-full max-w-md"
@click.stop
>
<source :src="`${activeAttachment.data_url}?t=${Date.now()}`" />
</audio>
</div>
<footer
class="z-10 flex items-center justify-center h-12 border-t border-n-weak"
>
<div
class="rounded-md flex items-center justify-center px-3 py-1 bg-n-slate-3 text-n-slate-12 text-sm font-medium"
<div class="flex items-center justify-center w-16 shrink-0">
<NextButton
v-if="hasMoreThanOneAttachment"
icon="i-lucide-chevron-right"
class="z-10"
blue
faded
lg
:disabled="activeImageIndex === allAttachments.length - 1"
@click.stop="
onClickChangeAttachment(
allAttachments[activeImageIndex + 1],
activeImageIndex + 1
)
"
/>
</div>
</main>
<footer
class="z-10 flex items-center justify-center h-12 border-t border-n-weak"
>
{{ `${activeImageIndex + 1} / ${allAttachments.length}` }}
</div>
</footer>
</div>
</woot-modal>
<div
class="rounded-md flex items-center justify-center px-3 py-1 bg-n-slate-3 text-n-slate-12 text-sm font-medium"
>
{{ `${activeImageIndex + 1} / ${allAttachments.length}` }}
</div>
</footer>
</div>
</woot-modal>
</Teleport>
</template>