Merge branch 'develop' into chore/upgrade-page

This commit is contained in:
Sivin Varghese
2025-04-07 10:24:42 +05:30
committed by GitHub
108 changed files with 3060 additions and 556 deletions
@@ -11,11 +11,15 @@ import semver from 'semver';
import { getLanguageDirection } from 'dashboard/components/widgets/conversation/advancedFilterItems/languages';
import BaseSettingsHeader from '../components/BaseSettingsHeader.vue';
import V4Button from 'dashboard/components-next/button/Button.vue';
import WootConfirmDeleteModal from 'dashboard/components/widgets/modal/ConfirmDeleteModal.vue';
import NextButton from 'dashboard/components-next/button/Button.vue';
export default {
components: {
BaseSettingsHeader,
V4Button,
WootConfirmDeleteModal,
NextButton,
},
setup() {
const { updateUISettings } = useUISettings();
@@ -35,6 +39,7 @@ export default {
features: {},
autoResolveDuration: null,
latestChatwootVersion: null,
showDeletePopup: false,
};
},
validations: {
@@ -55,6 +60,7 @@ export default {
getAccount: 'accounts/getAccount',
uiFlags: 'accounts/getUIFlags',
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
isOnChatwootCloud: 'globalConfig/isOnChatwootCloud',
}),
showAutoResolutionConfig() {
return this.isFeatureEnabledonAccount(
@@ -101,6 +107,34 @@ export default {
getAccountId() {
return this.id.toString();
},
confirmPlaceHolderText() {
return `${this.$t(
'GENERAL_SETTINGS.ACCOUNT_DELETE_SECTION.CONFIRM.PLACE_HOLDER',
{
accountName: this.name,
}
)}`;
},
isMarkedForDeletion() {
const { custom_attributes = {} } = this.currentAccount;
return !!custom_attributes.marked_for_deletion_at;
},
markedForDeletionDate() {
const { custom_attributes = {} } = this.currentAccount;
if (!custom_attributes.marked_for_deletion_at) return null;
return new Date(custom_attributes.marked_for_deletion_at);
},
markedForDeletionReason() {
const { custom_attributes = {} } = this.currentAccount;
return custom_attributes.marked_for_deletion_reason || 'manual_deletion';
},
formattedDeletionDate() {
if (!this.markedForDeletionDate) return '';
return this.markedForDeletionDate.toLocaleString();
},
currentAccount() {
return this.getAccount(this.accountId) || {};
},
},
mounted() {
this.initializeAccount();
@@ -162,6 +196,56 @@ export default {
rtl_view: isRTLSupported,
});
},
// Delete Function
openDeletePopup() {
this.showDeletePopup = true;
},
closeDeletePopup() {
this.showDeletePopup = false;
},
async markAccountForDeletion() {
this.closeDeletePopup();
try {
// Use the enterprise API to toggle deletion with delete action
await this.$store.dispatch('accounts/toggleDeletion', {
action_type: 'delete',
});
// Refresh account data
await this.$store.dispatch('accounts/get');
useAlert(this.$t('GENERAL_SETTINGS.ACCOUNT_DELETE_SECTION.SUCCESS'));
} catch (error) {
// Handle error message
this.handleDeletionError(error);
}
},
handleDeletionError(error) {
const errorKey = error.response?.data?.error_key;
if (errorKey) {
useAlert(
this.$t(`GENERAL_SETTINGS.ACCOUNT_DELETE_SECTION.${errorKey}`)
);
return;
}
const message = error.response?.data?.message;
if (message) {
useAlert(message);
return;
}
useAlert(this.$t('GENERAL_SETTINGS.ACCOUNT_DELETE_SECTION.FAILURE'));
},
async clearDeletionMark() {
try {
// Use the enterprise API to toggle deletion with undelete action
await this.$store.dispatch('accounts/toggleDeletion', {
action_type: 'undelete',
});
// Refresh account data
await this.$store.dispatch('accounts/get');
useAlert(this.$t('GENERAL_SETTINGS.UPDATE.SUCCESS'));
} catch (error) {
useAlert(this.$t('GENERAL_SETTINGS.UPDATE.ERROR'));
}
},
},
};
</script>
@@ -175,7 +259,7 @@ export default {
</V4Button>
</template>
</BaseSettingsHeader>
<div class="flex-grow flex-shrink min-w-0 overflow-auto mt-3">
<div class="flex-grow flex-shrink min-w-0 mt-3 overflow-auto">
<form v-if="!uiFlags.isFetchingItem" @submit.prevent="updateAccount">
<div
class="flex flex-row border-b border-slate-25 dark:border-slate-800"
@@ -279,6 +363,73 @@ export default {
<woot-code :script="getAccountId" />
</div>
</div>
<div v-if="!uiFlags.isFetchingItem && isOnChatwootCloud">
<div
class="flex flex-row pt-4 mt-2 border-t border-slate-25 dark:border-slate-800 text-black-900 dark:text-slate-300"
>
<div
class="flex-grow-0 flex-shrink-0 flex-[25%] min-w-0 py-4 pr-6 pl-0"
>
<h4 class="text-lg font-medium text-black-900 dark:text-slate-200">
{{ $t('GENERAL_SETTINGS.ACCOUNT_DELETE_SECTION.TITLE') }}
</h4>
<p>
{{ $t('GENERAL_SETTINGS.ACCOUNT_DELETE_SECTION.NOTE') }}
</p>
</div>
<div class="p-4 flex-grow-0 flex-shrink-0 flex-[50%]">
<div v-if="isMarkedForDeletion">
<div
class="p-4 flex-grow-0 flex-shrink-0 flex-[50%] bg-red-50 dark:bg-red-900 rounded"
>
<p class="mb-4">
{{
$t(
`GENERAL_SETTINGS.ACCOUNT_DELETE_SECTION.SCHEDULED_DELETION.MESSAGE_${markedForDeletionReason === 'manual_deletion' ? 'MANUAL' : 'INACTIVITY'}`,
{
deletionDate: formattedDeletionDate,
}
)
}}
</p>
<NextButton
:label="
$t(
'GENERAL_SETTINGS.ACCOUNT_DELETE_SECTION.SCHEDULED_DELETION.CLEAR_BUTTON'
)
"
color="ruby"
:is-loading="uiFlags.isUpdating"
@click="clearDeletionMark"
/>
</div>
</div>
<div v-if="!isMarkedForDeletion">
<NextButton
:label="$t('GENERAL_SETTINGS.ACCOUNT_DELETE_SECTION.BUTTON_TEXT')"
color="ruby"
@click="openDeletePopup()"
/>
</div>
</div>
</div>
<WootConfirmDeleteModal
v-if="showDeletePopup"
v-model:show="showDeletePopup"
:title="$t('GENERAL_SETTINGS.ACCOUNT_DELETE_SECTION.CONFIRM.TITLE')"
:message="$t('GENERAL_SETTINGS.ACCOUNT_DELETE_SECTION.CONFIRM.MESSAGE')"
:confirm-text="
$t('GENERAL_SETTINGS.ACCOUNT_DELETE_SECTION.CONFIRM.BUTTON_TEXT')
"
:reject-text="
$t('GENERAL_SETTINGS.ACCOUNT_DELETE_SECTION.CONFIRM.DISMISS')
"
:confirm-value="name"
:confirm-place-holder-text="confirmPlaceHolderText"
@on-confirm="markAccountForDeletion"
@on-close="closeDeletePopup"
/>
</div>
<div class="p-4 text-sm text-center">
<div>{{ `v${globalConfig.appVersion}` }}</div>
<div v-if="hasAnUpdateAvailable && globalConfig.displayManifest">
@@ -9,6 +9,7 @@ import Sms from './channels/Sms.vue';
import Whatsapp from './channels/Whatsapp.vue';
import Line from './channels/Line.vue';
import Telegram from './channels/Telegram.vue';
import Instagram from './channels/Instagram.vue';
const channelViewList = {
facebook: Facebook,
@@ -20,6 +21,7 @@ const channelViewList = {
whatsapp: Whatsapp,
line: Line,
telegram: Telegram,
instagram: Instagram,
};
export default defineComponent({
@@ -7,6 +7,7 @@ import SettingIntroBanner from 'dashboard/components/widgets/SettingIntroBanner.
import SettingsSection from '../../../../components/SettingsSection.vue';
import inboxMixin from 'shared/mixins/inboxMixin';
import FacebookReauthorize from './facebook/Reauthorize.vue';
import InstagramReauthorize from './channels/instagram/Reauthorize.vue';
import MicrosoftReauthorize from './channels/microsoft/Reauthorize.vue';
import GoogleReauthorize from './channels/google/Reauthorize.vue';
import PreChatFormSettings from './PreChatForm/Settings.vue';
@@ -36,6 +37,7 @@ export default {
MicrosoftReauthorize,
GoogleReauthorize,
NextButton,
InstagramReauthorize,
},
mixins: [inboxMixin],
setup() {
@@ -202,6 +204,9 @@ export default {
return true;
return false;
},
instagramUnauthorized() {
return this.isAInstagramChannel && this.inbox.reauthorization_required;
},
microsoftUnauthorized() {
return this.isAMicrosoftInbox && this.inbox.reauthorization_required;
},
@@ -383,10 +388,11 @@ export default {
/>
</woot-tabs>
</SettingIntroBanner>
<section class="max-w-6xl mx-auto w-full">
<section class="w-full max-w-6xl mx-auto">
<MicrosoftReauthorize v-if="microsoftUnauthorized" :inbox="inbox" />
<FacebookReauthorize v-if="facebookUnauthorized" :inbox="inbox" />
<GoogleReauthorize v-if="googleUnauthorized" :inbox="inbox" />
<InstagramReauthorize v-if="instagramUnauthorized" :inbox="inbox" />
<div v-if="selectedTabKey === 'inbox_settings'" class="mx-8">
<SettingsSection
:title="$t('INBOX_MGMT.SETTINGS_POPUP.INBOX_UPDATE_TITLE')"
@@ -11,6 +11,7 @@ import ChannelApi from '../../../../../api/channels';
import PageHeader from '../../SettingsSubPageHeader.vue';
import router from '../../../../index';
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
import NextButton from 'dashboard/components-next/button/Button.vue';
import { loadScript } from 'dashboard/helper/DOMHelpers';
import * as Sentry from '@sentry/vue';
@@ -19,6 +20,7 @@ export default {
components: {
LoadingState,
PageHeader,
NextButton,
},
mixins: [globalConfigMixin],
setup() {
@@ -207,7 +209,7 @@ export default {
<template>
<div
class="border border-n-weak bg-n-solid-1 rounded-t-lg border-b-0 h-full w-full p-6 col-span-6 overflow-auto"
class="w-full h-full col-span-6 p-6 overflow-auto border border-b-0 rounded-t-lg border-n-weak bg-n-solid-1"
>
<div
v-if="!hasLoginStarted"
@@ -240,7 +242,7 @@ export default {
<LoadingState v-else-if="showLoader" :message="emptyStateMessage" />
<form
v-else
class="flex flex-wrap flex-col mx-0"
class="flex flex-col flex-wrap mx-0"
@submit.prevent="createChannel()"
>
<div class="w-full">
@@ -291,7 +293,7 @@ export default {
</label>
</div>
<div class="w-full text-right">
<input type="submit" value="Create Inbox" class="button" />
<NextButton :label="$t('INBOX_MGMT.ADD.FB.CREATE_INBOX')" />
</div>
</div>
</form>
@@ -0,0 +1,129 @@
<script>
import { useVuelidate } from '@vuelidate/core';
import { useAccount } from 'dashboard/composables/useAccount';
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
import instagramClient from 'dashboard/api/channel/instagramClient';
export default {
mixins: [globalConfigMixin],
setup() {
const { accountId } = useAccount();
return {
accountId,
v$: useVuelidate(),
};
},
data() {
return {
isCreating: false,
hasError: false,
errorStateMessage: '',
errorStateDescription: '',
isRequestingAuthorization: false,
};
},
mounted() {
const urlParams = new URLSearchParams(window.location.search);
// TODO: Handle error type
// const errorType = urlParams.get('error_type');
const errorCode = urlParams.get('code');
const errorMessage = urlParams.get('error_message');
if (errorMessage) {
this.hasError = true;
if (errorCode === '400') {
this.errorStateMessage = errorMessage;
this.errorStateDescription = this.$t(
'INBOX_MGMT.ADD.INSTAGRAM.ERROR_AUTH'
);
} else {
this.errorStateMessage = this.$t(
'INBOX_MGMT.ADD.INSTAGRAM.ERROR_MESSAGE'
);
this.errorStateDescription = errorMessage;
}
}
// User need to remove the error params from the url to avoid the error to be shown again after page reload, so that user can try again
const cleanURL = window.location.pathname;
window.history.replaceState({}, document.title, cleanURL);
},
methods: {
async requestAuthorization() {
this.isRequestingAuthorization = true;
const response = await instagramClient.generateAuthorization();
const {
data: { url },
} = response;
window.location.href = url;
},
},
};
</script>
<template>
<div
class="border border-slate-25 dark:border-slate-800/60 bg-white dark:bg-slate-900 h-full p-6 w-full max-w-full md:w-3/4 md:max-w-[75%] flex-shrink-0 flex-grow-0"
>
<div class="flex flex-col items-center justify-center h-full text-center">
<div v-if="hasError" class="max-w-lg mx-auto text-center">
<h5>{{ errorStateMessage }}</h5>
<p
v-if="errorStateDescription"
v-dompurify-html="errorStateDescription"
/>
</div>
<div
v-else
class="flex flex-col items-center justify-center h-full text-center"
>
<button
class="flex items-center justify-center px-8 py-3.5 text-white rounded-full bg-gradient-to-r from-[#833AB4] via-[#FD1D1D] to-[#FCAF45] hover:shadow-lg transition-all duration-300 min-w-[240px] overflow-hidden"
:disabled="isRequestingAuthorization"
@click="requestAuthorization()"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-5 h-5 mr-2"
viewBox="0 0 24 24"
fill="currentColor"
>
<path
d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zM12 0C8.741 0 8.333.014 7.053.072 2.695.272.273 2.69.073 7.052.014 8.333 0 8.741 0 12c0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98C8.333 23.986 8.741 24 12 24c3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98C15.668.014 15.259 0 12 0zm0 5.838a6.162 6.162 0 100 12.324 6.162 6.162 0 000-12.324zM12 16a4 4 0 110-8 4 4 0 010 8zm6.406-11.845a1.44 1.44 0 100 2.881 1.44 1.44 0 000-2.881z"
/>
</svg>
<span class="text-base font-medium">
{{ $t('INBOX_MGMT.ADD.INSTAGRAM.CONTINUE_WITH_INSTAGRAM') }}
</span>
<span v-if="isRequestingAuthorization" class="ml-2">
<svg
class="w-5 h-5 animate-spin"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
/>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
</span>
</button>
<p class="py-6">
{{ $t('INBOX_MGMT.ADD.INSTAGRAM.HELP') }}
</p>
</div>
</div>
</div>
</template>
@@ -0,0 +1,37 @@
<script setup>
import { ref } from 'vue';
import InboxReconnectionRequired from '../../components/InboxReconnectionRequired.vue';
import instagramClient from 'dashboard/api/channel/instagramClient';
import { useI18n } from 'vue-i18n';
import { useAlert } from 'dashboard/composables';
const { t } = useI18n();
const isRequestingAuthorization = ref(false);
async function requestAuthorization() {
try {
isRequestingAuthorization.value = true;
const response = await instagramClient.generateAuthorization();
const {
data: { url },
} = response;
window.location.href = url;
} catch (error) {
useAlert(t('INBOX_MGMT.ADD.INSTAGRAM.ERROR_AUTH'));
} finally {
isRequestingAuthorization.value = false;
}
}
</script>
<template>
<InboxReconnectionRequired
class="mx-8 mt-5"
@reauthorize="requestAuthorization"
/>
</template>
@@ -28,6 +28,7 @@ const i18nMap = {
'Channel::Telegram': 'TELEGRAM',
'Channel::Line': 'LINE',
'Channel::Api': 'API',
'Channel::Instagram': 'INSTAGRAM',
};
const twilioChannelName = () => {