feat: Ability to delete account for administrators (#1874)
## Description Add account delete option in the user account settings. Fixes #1555 ## Type of change - [ ] New feature (non-breaking change which adds functionality)   ## Checklist: - [ ] My code follows the style guidelines of this project - [ ] I have performed a self-review of my own code - [ ] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules --------- Co-authored-by: Sojan Jose <sojan@pepalo.com> Co-authored-by: Sojan Jose <sojan.official@gmail.com> Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
This commit is contained in:
@@ -17,6 +17,12 @@ class EnterpriseAccountAPI extends ApiClient {
|
||||
getLimits() {
|
||||
return axios.get(`${this.url}limits`);
|
||||
}
|
||||
|
||||
toggleDeletion(action) {
|
||||
return axios.post(`${this.url}toggle_deletion`, {
|
||||
action_type: action,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new EnterpriseAccountAPI();
|
||||
|
||||
@@ -10,6 +10,7 @@ describe('#enterpriseAccountAPI', () => {
|
||||
expect(accountAPI).toHaveProperty('update');
|
||||
expect(accountAPI).toHaveProperty('delete');
|
||||
expect(accountAPI).toHaveProperty('checkout');
|
||||
expect(accountAPI).toHaveProperty('toggleDeletion');
|
||||
});
|
||||
|
||||
describe('API calls', () => {
|
||||
@@ -42,5 +43,21 @@ describe('#enterpriseAccountAPI', () => {
|
||||
'/enterprise/api/v1/subscription'
|
||||
);
|
||||
});
|
||||
|
||||
it('#toggleDeletion with delete action', () => {
|
||||
accountAPI.toggleDeletion('delete');
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
'/enterprise/api/v1/toggle_deletion',
|
||||
{ action_type: 'delete' }
|
||||
);
|
||||
});
|
||||
|
||||
it('#toggleDeletion with undelete action', () => {
|
||||
accountAPI.toggleDeletion('undelete');
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
'/enterprise/api/v1/toggle_deletion',
|
||||
{ action_type: 'undelete' }
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,6 +14,26 @@
|
||||
"ERROR": "Could not update settings, try again!",
|
||||
"SUCCESS": "Successfully updated account settings"
|
||||
},
|
||||
"ACCOUNT_DELETE_SECTION": {
|
||||
"TITLE": "Delete your Account",
|
||||
"NOTE": "Once you delete your account, all your data will be deleted.",
|
||||
"BUTTON_TEXT": "Delete Your Account",
|
||||
"CONFIRM": {
|
||||
"TITLE": "Delete Account",
|
||||
"MESSAGE": "Deleting your Account is irreversible. Enter your account name below to confirm you want to permanently delete it.",
|
||||
"BUTTON_TEXT": "Delete",
|
||||
"DISMISS": "Cancel",
|
||||
"PLACE_HOLDER": "Please type {accountName} to confirm"
|
||||
},
|
||||
"SUCCESS": "Account marked for deletion",
|
||||
"FAILURE": "Could not delete account, try again!",
|
||||
"SCHEDULED_DELETION": {
|
||||
"TITLE": "Account Scheduled for Deletion",
|
||||
"MESSAGE_MANUAL": "This account is scheduled for deletion on {deletionDate}. This was requested by an administrator. You can cancel the deletion before this date.",
|
||||
"MESSAGE_INACTIVITY": "This account is scheduled for deletion on {deletionDate} due to account inactivity. You can cancel the deletion before this date.",
|
||||
"CLEAR_BUTTON": "Cancel Scheduled Deletion"
|
||||
}
|
||||
},
|
||||
"FORM": {
|
||||
"ERROR": "Please fix form errors",
|
||||
"GENERAL_SECTION": {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -73,6 +73,29 @@ export const actions = {
|
||||
throw new Error(error);
|
||||
}
|
||||
},
|
||||
delete: async ({ commit }, { id }) => {
|
||||
commit(types.default.SET_ACCOUNT_UI_FLAG, { isUpdating: true });
|
||||
try {
|
||||
await AccountAPI.delete(id);
|
||||
commit(types.default.SET_ACCOUNT_UI_FLAG, { isUpdating: false });
|
||||
} catch (error) {
|
||||
commit(types.default.SET_ACCOUNT_UI_FLAG, { isUpdating: false });
|
||||
throw new Error(error);
|
||||
}
|
||||
},
|
||||
toggleDeletion: async (
|
||||
{ commit },
|
||||
{ action_type } = { action_type: 'delete' }
|
||||
) => {
|
||||
commit(types.default.SET_ACCOUNT_UI_FLAG, { isUpdating: true });
|
||||
try {
|
||||
await EnterpriseAccountAPI.toggleDeletion(action_type);
|
||||
commit(types.default.SET_ACCOUNT_UI_FLAG, { isUpdating: false });
|
||||
} catch (error) {
|
||||
commit(types.default.SET_ACCOUNT_UI_FLAG, { isUpdating: false });
|
||||
throw new Error(error);
|
||||
}
|
||||
},
|
||||
create: async ({ commit }, accountInfo) => {
|
||||
commit(types.default.SET_ACCOUNT_UI_FLAG, { isCreating: true });
|
||||
try {
|
||||
|
||||
@@ -80,4 +80,41 @@ describe('#actions', () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#toggleDeletion', () => {
|
||||
it('sends correct actions with delete action if API is success', async () => {
|
||||
axios.post.mockResolvedValue({});
|
||||
await actions.toggleDeletion({ commit }, { action_type: 'delete' });
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.default.SET_ACCOUNT_UI_FLAG, { isUpdating: true }],
|
||||
[types.default.SET_ACCOUNT_UI_FLAG, { isUpdating: false }],
|
||||
]);
|
||||
expect(axios.post.mock.calls[0][1]).toEqual({
|
||||
action_type: 'delete',
|
||||
});
|
||||
});
|
||||
|
||||
it('sends correct actions with undelete action if API is success', async () => {
|
||||
axios.post.mockResolvedValue({});
|
||||
await actions.toggleDeletion({ commit }, { action_type: 'undelete' });
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.default.SET_ACCOUNT_UI_FLAG, { isUpdating: true }],
|
||||
[types.default.SET_ACCOUNT_UI_FLAG, { isUpdating: false }],
|
||||
]);
|
||||
expect(axios.post.mock.calls[0][1]).toEqual({
|
||||
action_type: 'undelete',
|
||||
});
|
||||
});
|
||||
|
||||
it('sends correct actions if API is error', async () => {
|
||||
axios.post.mockRejectedValue({ message: 'Incorrect header' });
|
||||
await expect(
|
||||
actions.toggleDeletion({ commit }, { action_type: 'delete' })
|
||||
).rejects.toThrow(Error);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.default.SET_ACCOUNT_UI_FLAG, { isUpdating: true }],
|
||||
[types.default.SET_ACCOUNT_UI_FLAG, { isUpdating: false }],
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user