feat: Add support for minutes in auto resolve feature (#11269)
### Summary - Converts conversation auto-resolution duration from days to minutes for more granular control - Updates validation to allow values from 10 minutes (minimum) to 999 days (maximum) - Implements smart messaging to show appropriate time units in activity messages ### Changes - Created migration to convert existing durations from days to minutes (x1440) - Updated conversation resolver to use minutes instead of days - Added dynamic translation key selection based on duration value - Updated related specs and documentation - Added support for displaying durations in days, hours, or minutes based on value ### Test plan - Verify account validation accepts new minute-based ranges - Confirm existing account settings are correctly migrated - Test auto-resolution works properly with minute values - Ensure proper time unit display in activity messages --------- Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
This commit is contained in:
@@ -1,25 +1,34 @@
|
||||
<script>
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { required, minValue, maxValue } from '@vuelidate/validators';
|
||||
import { required } from '@vuelidate/validators';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import { useConfig } from 'dashboard/composables/useConfig';
|
||||
import { useAccount } from 'dashboard/composables/useAccount';
|
||||
import { FEATURE_FLAGS } from '../../../../featureFlags';
|
||||
import semver from 'semver';
|
||||
import { getLanguageDirection } from 'dashboard/components/widgets/conversation/advancedFilterItems/languages';
|
||||
import WithLabel from 'v3/components/Form/WithLabel.vue';
|
||||
import NextInput from 'next/input/Input.vue';
|
||||
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';
|
||||
import AccountId from './components/AccountId.vue';
|
||||
import BuildInfo from './components/BuildInfo.vue';
|
||||
import AccountDelete from './components/AccountDelete.vue';
|
||||
import AutoResolve from './components/AutoResolve.vue';
|
||||
import SectionLayout from './components/SectionLayout.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
BaseSettingsHeader,
|
||||
V4Button,
|
||||
WootConfirmDeleteModal,
|
||||
NextButton,
|
||||
AccountId,
|
||||
BuildInfo,
|
||||
AccountDelete,
|
||||
AutoResolve,
|
||||
SectionLayout,
|
||||
WithLabel,
|
||||
NextInput,
|
||||
},
|
||||
setup() {
|
||||
const { updateUISettings } = useUISettings();
|
||||
@@ -37,9 +46,6 @@ export default {
|
||||
domain: '',
|
||||
supportEmail: '',
|
||||
features: {},
|
||||
autoResolveDuration: null,
|
||||
latestChatwootVersion: null,
|
||||
showDeletePopup: false,
|
||||
};
|
||||
},
|
||||
validations: {
|
||||
@@ -49,14 +55,9 @@ export default {
|
||||
locale: {
|
||||
required,
|
||||
},
|
||||
autoResolveDuration: {
|
||||
minValue: minValue(1),
|
||||
maxValue: maxValue(999),
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
globalConfig: 'globalConfig/get',
|
||||
getAccount: 'accounts/getAccount',
|
||||
uiFlags: 'accounts/getUIFlags',
|
||||
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
|
||||
@@ -68,16 +69,6 @@ export default {
|
||||
FEATURE_FLAGS.AUTO_RESOLVE_CONVERSATIONS
|
||||
);
|
||||
},
|
||||
hasAnUpdateAvailable() {
|
||||
if (!semver.valid(this.latestChatwootVersion)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return semver.lt(
|
||||
this.globalConfig.appVersion,
|
||||
this.latestChatwootVersion
|
||||
);
|
||||
},
|
||||
languagesSortedByCode() {
|
||||
const enabledLanguages = [...this.enabledLanguages];
|
||||
return enabledLanguages.sort((l1, l2) =>
|
||||
@@ -87,51 +78,19 @@ export default {
|
||||
isUpdating() {
|
||||
return this.uiFlags.isUpdating;
|
||||
},
|
||||
|
||||
featureInboundEmailEnabled() {
|
||||
return !!this.features?.inbound_emails;
|
||||
},
|
||||
|
||||
featureCustomReplyDomainEnabled() {
|
||||
return (
|
||||
this.featureInboundEmailEnabled && !!this.features.custom_reply_domain
|
||||
);
|
||||
},
|
||||
|
||||
featureCustomReplyEmailEnabled() {
|
||||
return (
|
||||
this.featureInboundEmailEnabled && !!this.features.custom_reply_email
|
||||
);
|
||||
},
|
||||
|
||||
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) || {};
|
||||
},
|
||||
@@ -142,16 +101,8 @@ export default {
|
||||
methods: {
|
||||
async initializeAccount() {
|
||||
try {
|
||||
const {
|
||||
name,
|
||||
locale,
|
||||
id,
|
||||
domain,
|
||||
support_email,
|
||||
features,
|
||||
auto_resolve_duration,
|
||||
latest_chatwoot_version: latestChatwootVersion,
|
||||
} = this.getAccount(this.accountId);
|
||||
const { name, locale, id, domain, support_email, features } =
|
||||
this.getAccount(this.accountId);
|
||||
|
||||
this.$root.$i18n.locale = locale;
|
||||
this.name = name;
|
||||
@@ -160,8 +111,6 @@ export default {
|
||||
this.domain = domain;
|
||||
this.supportEmail = support_email;
|
||||
this.features = features;
|
||||
this.autoResolveDuration = auto_resolve_duration;
|
||||
this.latestChatwootVersion = latestChatwootVersion;
|
||||
} catch (error) {
|
||||
// Ignore error
|
||||
}
|
||||
@@ -179,7 +128,6 @@ export default {
|
||||
name: this.name,
|
||||
domain: this.domain,
|
||||
support_email: this.supportEmail,
|
||||
auto_resolve_duration: this.autoResolveDuration,
|
||||
});
|
||||
this.$root.$i18n.locale = this.locale;
|
||||
this.getAccount(this.id).locale = this.locale;
|
||||
@@ -196,252 +144,101 @@ 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>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col w-full">
|
||||
<BaseSettingsHeader :title="$t('GENERAL_SETTINGS.TITLE')">
|
||||
<template #actions>
|
||||
<V4Button blue :loading="isUpdating" @click="updateAccount">
|
||||
{{ $t('GENERAL_SETTINGS.SUBMIT') }}
|
||||
</V4Button>
|
||||
</template>
|
||||
</BaseSettingsHeader>
|
||||
<div class="flex flex-col max-w-2xl mx-auto w-full">
|
||||
<BaseSettingsHeader :title="$t('GENERAL_SETTINGS.TITLE')" />
|
||||
<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"
|
||||
<SectionLayout
|
||||
:title="$t('GENERAL_SETTINGS.FORM.GENERAL_SECTION.TITLE')"
|
||||
:description="$t('GENERAL_SETTINGS.FORM.GENERAL_SECTION.NOTE')"
|
||||
>
|
||||
<form
|
||||
v-if="!uiFlags.isFetchingItem"
|
||||
class="grid gap-4"
|
||||
@submit.prevent="updateAccount"
|
||||
>
|
||||
<div
|
||||
class="flex-grow-0 flex-shrink-0 flex-[25%] min-w-0 py-4 pr-6 pl-0"
|
||||
<WithLabel
|
||||
:has-error="v$.name.$error"
|
||||
:label="$t('GENERAL_SETTINGS.FORM.NAME.LABEL')"
|
||||
:error-message="$t('GENERAL_SETTINGS.FORM.NAME.ERROR')"
|
||||
>
|
||||
<h4 class="text-lg font-medium text-black-900 dark:text-slate-200">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.GENERAL_SECTION.TITLE') }}
|
||||
</h4>
|
||||
<p>{{ $t('GENERAL_SETTINGS.FORM.GENERAL_SECTION.NOTE') }}</p>
|
||||
</div>
|
||||
<div class="p-4 flex-grow-0 flex-shrink-0 flex-[50%]">
|
||||
<label :class="{ error: v$.name.$error }">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.NAME.LABEL') }}
|
||||
<input
|
||||
v-model="name"
|
||||
type="text"
|
||||
:placeholder="$t('GENERAL_SETTINGS.FORM.NAME.PLACEHOLDER')"
|
||||
@blur="v$.name.$touch"
|
||||
/>
|
||||
<span v-if="v$.name.$error" class="message">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.NAME.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
<label :class="{ error: v$.locale.$error }">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.LANGUAGE.LABEL') }}
|
||||
<select v-model="locale">
|
||||
<option
|
||||
v-for="lang in languagesSortedByCode"
|
||||
:key="lang.iso_639_1_code"
|
||||
:value="lang.iso_639_1_code"
|
||||
>
|
||||
{{ lang.name }}
|
||||
</option>
|
||||
</select>
|
||||
<span v-if="v$.locale.$error" class="message">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.LANGUAGE.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
<label v-if="featureInboundEmailEnabled">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.FEATURES.INBOUND_EMAIL_ENABLED') }}
|
||||
</label>
|
||||
<label v-if="featureCustomReplyDomainEnabled">
|
||||
<NextInput
|
||||
v-model="name"
|
||||
type="text"
|
||||
class="w-full"
|
||||
:placeholder="$t('GENERAL_SETTINGS.FORM.NAME.PLACEHOLDER')"
|
||||
@blur="v$.name.$touch"
|
||||
/>
|
||||
</WithLabel>
|
||||
<WithLabel
|
||||
:has-error="v$.locale.$error"
|
||||
:label="$t('GENERAL_SETTINGS.FORM.LANGUAGE.LABEL')"
|
||||
:error-message="$t('GENERAL_SETTINGS.FORM.LANGUAGE.ERROR')"
|
||||
>
|
||||
<select v-model="locale" class="!mb-0 text-sm">
|
||||
<option
|
||||
v-for="lang in languagesSortedByCode"
|
||||
:key="lang.iso_639_1_code"
|
||||
:value="lang.iso_639_1_code"
|
||||
>
|
||||
{{ lang.name }}
|
||||
</option>
|
||||
</select>
|
||||
</WithLabel>
|
||||
<WithLabel
|
||||
v-if="featureCustomReplyDomainEnabled"
|
||||
:label="$t('GENERAL_SETTINGS.FORM.DOMAIN.LABEL')"
|
||||
>
|
||||
<NextInput
|
||||
v-model="domain"
|
||||
type="text"
|
||||
class="w-full"
|
||||
:placeholder="$t('GENERAL_SETTINGS.FORM.DOMAIN.PLACEHOLDER')"
|
||||
/>
|
||||
<template #help>
|
||||
{{
|
||||
featureInboundEmailEnabled &&
|
||||
$t('GENERAL_SETTINGS.FORM.FEATURES.INBOUND_EMAIL_ENABLED')
|
||||
}}
|
||||
|
||||
{{
|
||||
featureCustomReplyDomainEnabled &&
|
||||
$t('GENERAL_SETTINGS.FORM.FEATURES.CUSTOM_EMAIL_DOMAIN_ENABLED')
|
||||
}}
|
||||
</label>
|
||||
<label v-if="featureCustomReplyDomainEnabled">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.DOMAIN.LABEL') }}
|
||||
<input
|
||||
v-model="domain"
|
||||
type="text"
|
||||
:placeholder="$t('GENERAL_SETTINGS.FORM.DOMAIN.PLACEHOLDER')"
|
||||
/>
|
||||
</label>
|
||||
<label v-if="featureCustomReplyEmailEnabled">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.SUPPORT_EMAIL.LABEL') }}
|
||||
<input
|
||||
v-model="supportEmail"
|
||||
type="text"
|
||||
:placeholder="
|
||||
$t('GENERAL_SETTINGS.FORM.SUPPORT_EMAIL.PLACEHOLDER')
|
||||
"
|
||||
/>
|
||||
</label>
|
||||
<label
|
||||
v-if="showAutoResolutionConfig"
|
||||
:class="{ error: v$.autoResolveDuration.$error }"
|
||||
>
|
||||
{{ $t('GENERAL_SETTINGS.FORM.AUTO_RESOLVE_DURATION.LABEL') }}
|
||||
<input
|
||||
v-model="autoResolveDuration"
|
||||
type="number"
|
||||
:placeholder="
|
||||
$t('GENERAL_SETTINGS.FORM.AUTO_RESOLVE_DURATION.PLACEHOLDER')
|
||||
"
|
||||
@blur="v$.autoResolveDuration.$touch"
|
||||
/>
|
||||
<span v-if="v$.autoResolveDuration.$error" class="message">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.AUTO_RESOLVE_DURATION.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
</template>
|
||||
</WithLabel>
|
||||
<WithLabel
|
||||
v-if="featureCustomReplyEmailEnabled"
|
||||
:label="$t('GENERAL_SETTINGS.FORM.SUPPORT_EMAIL.LABEL')"
|
||||
>
|
||||
<NextInput
|
||||
v-model="supportEmail"
|
||||
type="text"
|
||||
class="w-full"
|
||||
:placeholder="
|
||||
$t('GENERAL_SETTINGS.FORM.SUPPORT_EMAIL.PLACEHOLDER')
|
||||
"
|
||||
/>
|
||||
</WithLabel>
|
||||
<div>
|
||||
<NextButton blue :is-loading="isUpdating" type="submit">
|
||||
{{ $t('GENERAL_SETTINGS.SUBMIT') }}
|
||||
</NextButton>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</form>
|
||||
</SectionLayout>
|
||||
|
||||
<woot-loading-state v-if="uiFlags.isFetchingItem" />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row">
|
||||
<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.FORM.ACCOUNT_ID.TITLE') }}
|
||||
</h4>
|
||||
<p>
|
||||
{{ $t('GENERAL_SETTINGS.FORM.ACCOUNT_ID.NOTE') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="p-4 flex-grow-0 flex-shrink-0 flex-[50%]">
|
||||
<woot-code :script="getAccountId" />
|
||||
</div>
|
||||
</div>
|
||||
<AutoResolve v-if="showAutoResolutionConfig" />
|
||||
<AccountId />
|
||||
<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">
|
||||
{{
|
||||
$t('GENERAL_SETTINGS.UPDATE_CHATWOOT', {
|
||||
latestChatwootVersion: latestChatwootVersion,
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
<div class="build-id">
|
||||
<div>{{ `Build ${globalConfig.gitSha}` }}</div>
|
||||
</div>
|
||||
<AccountDelete />
|
||||
</div>
|
||||
<BuildInfo />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useStore, useMapGetter } from 'dashboard/composables/store';
|
||||
import { useAccount } from 'dashboard/composables/useAccount';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useToggle } from '@vueuse/core';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import WootConfirmDeleteModal from 'dashboard/components/widgets/modal/ConfirmDeleteModal.vue';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
import SectionLayout from './SectionLayout.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
const store = useStore();
|
||||
const uiFlags = useMapGetter('accounts/getUIFlags');
|
||||
const { currentAccount } = useAccount();
|
||||
const [showDeletePopup, toggleDeletePopup] = useToggle();
|
||||
|
||||
const confirmPlaceHolderText = computed(() => {
|
||||
return `${t('GENERAL_SETTINGS.ACCOUNT_DELETE_SECTION.CONFIRM.PLACE_HOLDER', {
|
||||
accountName: currentAccount.value.name,
|
||||
})}`;
|
||||
});
|
||||
|
||||
const isMarkedForDeletion = computed(() => {
|
||||
const { custom_attributes = {} } = currentAccount.value;
|
||||
return !!custom_attributes.marked_for_deletion_at;
|
||||
});
|
||||
|
||||
const markedForDeletionDate = computed(() => {
|
||||
const { custom_attributes = {} } = currentAccount.value;
|
||||
if (!custom_attributes.marked_for_deletion_at) return null;
|
||||
return new Date(custom_attributes.marked_for_deletion_at);
|
||||
});
|
||||
|
||||
const markedForDeletionReason = computed(() => {
|
||||
const { custom_attributes = {} } = currentAccount.value;
|
||||
return custom_attributes.marked_for_deletion_reason || 'manual_deletion';
|
||||
});
|
||||
|
||||
const formattedDeletionDate = computed(() => {
|
||||
if (!markedForDeletionDate.value) return '';
|
||||
return markedForDeletionDate.value.toLocaleString();
|
||||
});
|
||||
|
||||
const markedForDeletionMessage = computed(() => {
|
||||
const params = { deletionDate: formattedDeletionDate.value };
|
||||
|
||||
if (markedForDeletionReason.value === 'manual_deletion') {
|
||||
return t(
|
||||
`GENERAL_SETTINGS.ACCOUNT_DELETE_SECTION.SCHEDULED_DELETION.MESSAGE_MANUAL`,
|
||||
params
|
||||
);
|
||||
}
|
||||
|
||||
return t(
|
||||
`GENERAL_SETTINGS.ACCOUNT_DELETE_SECTION.SCHEDULED_DELETION.MESSAGE_INACTIVITY`,
|
||||
params
|
||||
);
|
||||
});
|
||||
|
||||
function handleDeletionError(error) {
|
||||
const message = error.response?.data?.message;
|
||||
if (message) {
|
||||
useAlert(message);
|
||||
return;
|
||||
}
|
||||
useAlert(t('GENERAL_SETTINGS.ACCOUNT_DELETE_SECTION.FAILURE'));
|
||||
}
|
||||
|
||||
async function markAccountForDeletion() {
|
||||
toggleDeletePopup(false);
|
||||
try {
|
||||
// Use the enterprise API to toggle deletion with delete action
|
||||
await store.dispatch('accounts/toggleDeletion', {
|
||||
action_type: 'delete',
|
||||
});
|
||||
// Refresh account data
|
||||
await store.dispatch('accounts/get');
|
||||
useAlert(t('GENERAL_SETTINGS.ACCOUNT_DELETE_SECTION.SUCCESS'));
|
||||
} catch (error) {
|
||||
// Handle error message
|
||||
handleDeletionError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function clearDeletionMark() {
|
||||
try {
|
||||
// Use the enterprise API to toggle deletion with undelete action
|
||||
await store.dispatch('accounts/toggleDeletion', {
|
||||
action_type: 'undelete',
|
||||
});
|
||||
|
||||
// Refresh account data
|
||||
await store.dispatch('accounts/get');
|
||||
useAlert(t('GENERAL_SETTINGS.UPDATE.SUCCESS'));
|
||||
} catch (error) {
|
||||
useAlert(t('GENERAL_SETTINGS.UPDATE.ERROR'));
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SectionLayout
|
||||
:title="t('GENERAL_SETTINGS.ACCOUNT_DELETE_SECTION.TITLE')"
|
||||
:description="t('GENERAL_SETTINGS.ACCOUNT_DELETE_SECTION.NOTE')"
|
||||
with-border
|
||||
>
|
||||
<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">
|
||||
{{ markedForDeletionMessage }}
|
||||
</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="toggleDeletePopup(true)"
|
||||
/>
|
||||
</div>
|
||||
</SectionLayout>
|
||||
<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="currentAccount.name"
|
||||
:confirm-place-holder-text="confirmPlaceHolderText"
|
||||
@on-confirm="markAccountForDeletion"
|
||||
@on-close="toggleDeletePopup(false)"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,22 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useAccount } from 'dashboard/composables/useAccount';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import SectionLayout from './SectionLayout.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
const { currentAccount } = useAccount();
|
||||
|
||||
const getAccountId = computed(() => currentAccount.value.id.toString());
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SectionLayout
|
||||
:title="t('GENERAL_SETTINGS.FORM.ACCOUNT_ID.TITLE')"
|
||||
:description="t('GENERAL_SETTINGS.FORM.ACCOUNT_ID.NOTE')"
|
||||
with-border
|
||||
>
|
||||
<woot-code :script="getAccountId" />
|
||||
</SectionLayout>
|
||||
</template>
|
||||
@@ -0,0 +1,121 @@
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useAccount } from 'dashboard/composables/useAccount';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import SectionLayout from './SectionLayout.vue';
|
||||
import WithLabel from 'v3/components/Form/WithLabel.vue';
|
||||
import DurationInput from 'next/input/DurationInput.vue';
|
||||
import TextArea from 'next/textarea/TextArea.vue';
|
||||
import Switch from 'next/switch/Switch.vue';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
const duration = ref(0);
|
||||
const message = ref('');
|
||||
const isEnabled = ref(false);
|
||||
|
||||
const { currentAccount, updateAccount } = useAccount();
|
||||
|
||||
watch(
|
||||
currentAccount,
|
||||
() => {
|
||||
const { auto_resolve_after, auto_resolve_message } =
|
||||
currentAccount.value?.settings || {};
|
||||
|
||||
duration.value = auto_resolve_after;
|
||||
message.value = auto_resolve_message;
|
||||
|
||||
if (duration.value) {
|
||||
isEnabled.value = true;
|
||||
}
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
);
|
||||
|
||||
const updateAccountSettings = async settings => {
|
||||
try {
|
||||
await updateAccount(settings);
|
||||
useAlert(t('GENERAL_SETTINGS.FORM.AUTO_RESOLVE_DURATION.API.SUCCESS'));
|
||||
} catch (error) {
|
||||
useAlert(t('GENERAL_SETTINGS.FORM.AUTO_RESOLVE_DURATION.API.ERROR'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
return updateAccountSettings({
|
||||
auto_resolve_after: duration.value,
|
||||
auto_resolve_message: message.value,
|
||||
});
|
||||
};
|
||||
|
||||
const handleDisable = async () => {
|
||||
duration.value = null;
|
||||
message.value = '';
|
||||
|
||||
return updateAccountSettings({
|
||||
auto_resolve_after: null,
|
||||
auto_resolve_message: '',
|
||||
});
|
||||
};
|
||||
|
||||
const toggleAutoResolve = async () => {
|
||||
if (!isEnabled.value) handleDisable();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SectionLayout
|
||||
:title="t('GENERAL_SETTINGS.FORM.AUTO_RESOLVE.TITLE')"
|
||||
:description="t('GENERAL_SETTINGS.FORM.AUTO_RESOLVE.NOTE')"
|
||||
with-border
|
||||
>
|
||||
<template #headerActions>
|
||||
<div class="flex justify-end">
|
||||
<Switch v-model="isEnabled" @change="toggleAutoResolve" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<form class="grid gap-4" @submit.prevent="handleSubmit">
|
||||
<WithLabel
|
||||
:label="t('GENERAL_SETTINGS.FORM.AUTO_RESOLVE_DURATION.LABEL')"
|
||||
:help-message="t('GENERAL_SETTINGS.FORM.AUTO_RESOLVE_DURATION.HELP')"
|
||||
>
|
||||
<div class="gap-2 w-full grid grid-cols-[3fr_1fr]">
|
||||
<!-- allow 10 mins to 999 days -->
|
||||
<DurationInput
|
||||
v-model="duration"
|
||||
:disabled="!isEnabled"
|
||||
min="10"
|
||||
max="1439856"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
</WithLabel>
|
||||
<WithLabel
|
||||
:label="t('GENERAL_SETTINGS.FORM.AUTO_RESOLVE_DURATION.MESSAGE_LABEL')"
|
||||
:help-message="
|
||||
t('GENERAL_SETTINGS.FORM.AUTO_RESOLVE_DURATION.MESSAGE_HELP')
|
||||
"
|
||||
>
|
||||
<TextArea
|
||||
v-model="message"
|
||||
class="w-full"
|
||||
:disabled="!isEnabled"
|
||||
:placeholder="
|
||||
t('GENERAL_SETTINGS.FORM.AUTO_RESOLVE_DURATION.MESSAGE_PLACEHOLDER')
|
||||
"
|
||||
/>
|
||||
</WithLabel>
|
||||
<div class="flex gap-2">
|
||||
<NextButton
|
||||
blue
|
||||
type="submit"
|
||||
:label="
|
||||
t('GENERAL_SETTINGS.FORM.AUTO_RESOLVE_DURATION.UPDATE_BUTTON')
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</SectionLayout>
|
||||
</template>
|
||||
@@ -0,0 +1,56 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useAccount } from 'dashboard/composables/useAccount';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
import { copyTextToClipboard } from 'shared/helpers/clipboard';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import semver from 'semver';
|
||||
|
||||
const { t } = useI18n();
|
||||
const { currentAccount } = useAccount();
|
||||
|
||||
const latestChatwootVersion = computed(() => {
|
||||
return currentAccount.value.latest_chatwoot_version;
|
||||
});
|
||||
|
||||
const globalConfig = useMapGetter('globalConfig/get');
|
||||
|
||||
const hasAnUpdateAvailable = computed(() => {
|
||||
if (!semver.valid(latestChatwootVersion.value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return semver.lt(globalConfig.value.appVersion, latestChatwootVersion.value);
|
||||
});
|
||||
|
||||
const gitSha = computed(() => {
|
||||
return globalConfig.value.gitSha.substring(0, 7);
|
||||
});
|
||||
|
||||
const copyGitSha = () => {
|
||||
copyTextToClipboard(globalConfig.value.gitSha);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-4 text-sm text-center">
|
||||
<div v-if="hasAnUpdateAvailable && globalConfig.displayManifest">
|
||||
{{
|
||||
t('GENERAL_SETTINGS.UPDATE_CHATWOOT', {
|
||||
latestChatwootVersion: latestChatwootVersion,
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
<div class="divide-x divide-n-slate-9">
|
||||
<span class="px-2">{{ `v${globalConfig.appVersion}` }}</span>
|
||||
<span
|
||||
v-tooltip="t('COMPONENTS.CODE.BUTTON_TEXT')"
|
||||
class="px-2 build-id cursor-pointer"
|
||||
@click="copyGitSha"
|
||||
>
|
||||
{{ `Build ${gitSha}` }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,40 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
withBorder: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section
|
||||
class="grid grid-cols-1 py-8 gap-10"
|
||||
:class="{ 'border-t border-n-weak': withBorder }"
|
||||
>
|
||||
<header class="grid grid-cols-4">
|
||||
<div class="col-span-3">
|
||||
<h4 class="text-lg font-medium text-n-slate-12">
|
||||
<slot name="title">{{ title }}</slot>
|
||||
</h4>
|
||||
<p class="text-n-slate-11">
|
||||
<slot name="description">{{ description }}</slot>
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-span-1">
|
||||
<slot name="headerActions" />
|
||||
</div>
|
||||
</header>
|
||||
<div class="text-n-slate-12">
|
||||
<slot />
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
Reference in New Issue
Block a user