chore: Display Agent Bot token after creation (#11488)

This PR includes:

- Displaying the Agent Bot token after creation
- Updating the avatar icon when an avatar image is not present

Fixes: https://linear.app/chatwoot/issue/CW-4337/agent-bot-token-not-visible
This commit is contained in:
Sivin Varghese
2025-05-21 06:05:18 +05:30
committed by GitHub
parent 2ee63656e2
commit af650af489
3 changed files with 138 additions and 46 deletions

View File

@@ -59,6 +59,11 @@
"ERROR_MESSAGE": "Could not update bot. Please try again."
}
},
"ACCESS_TOKEN": {
"TITLE": "Access Token",
"DESCRIPTION": "Copy the access token and save it securely",
"COPY_SUCCESSFUL": "Access token copied to clipboard"
},
"FORM": {
"AVATAR": {
"LABEL": "Bot avatar"

View File

@@ -5,12 +5,15 @@ import { useAlert } from 'dashboard/composables';
import { useI18n } from 'vue-i18n';
import { required, helpers, url } from '@vuelidate/validators';
import { useVuelidate } from '@vuelidate/core';
import { copyTextToClipboard } from 'shared/helpers/clipboard';
import { useToggle } from '@vueuse/core';
import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
import NextButton from 'dashboard/components-next/button/Button.vue';
import Input from 'dashboard/components-next/input/Input.vue';
import TextArea from 'dashboard/components-next/textarea/TextArea.vue';
import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
import AccessToken from 'dashboard/routes/dashboard/settings/profile/AccessToken.vue';
const props = defineProps({
type: {
@@ -42,6 +45,9 @@ const formState = reactive({
botAvatarUrl: '',
});
const [showAccessToken, toggleAccessToken] = useToggle();
const accessToken = ref('');
const v$ = useVuelidate(
{
botName: {
@@ -70,11 +76,22 @@ const isLoading = computed(() =>
: uiFlags.value.isUpdating
);
const dialogTitle = computed(() =>
props.type === MODAL_TYPES.CREATE
const dialogTitle = computed(() => {
if (showAccessToken.value) {
return t('AGENT_BOTS.ACCESS_TOKEN.TITLE');
}
return props.type === MODAL_TYPES.CREATE
? t('AGENT_BOTS.ADD.TITLE')
: t('AGENT_BOTS.EDIT.TITLE')
);
: t('AGENT_BOTS.EDIT.TITLE');
});
const dialogDescription = computed(() => {
if (showAccessToken.value) {
return t('AGENT_BOTS.ACCESS_TOKEN.DESCRIPTION');
}
return '';
});
const confirmButtonLabel = computed(() =>
props.type === MODAL_TYPES.CREATE
@@ -90,6 +107,13 @@ const botUrlError = computed(() =>
v$.value.botUrl.$error ? v$.value.botUrl.$errors[0]?.$message : ''
);
const showAccessTokenInput = computed(
() =>
showAccessToken.value ||
props.type === MODAL_TYPES.EDIT ||
accessToken.value
);
const resetForm = () => {
Object.assign(formState, {
botName: '',
@@ -128,6 +152,7 @@ const handleAvatarDelete = async () => {
const handleSubmit = async () => {
v$.value.$touch();
if (v$.value.$invalid) return;
if (showAccessToken.value) return;
const botData = {
name: formState.botName,
@@ -144,7 +169,7 @@ const handleSubmit = async () => {
? botData
: { id: props.selectedBot.id, data: botData };
await store.dispatch(
const response = await store.dispatch(
`agentBots/${isCreate ? 'create' : 'update'}`,
actionPayload
);
@@ -154,7 +179,21 @@ const handleSubmit = async () => {
: t('AGENT_BOTS.EDIT.API.SUCCESS_MESSAGE');
useAlert(alertKey);
dialogRef.value.close();
// Show access token after creation
if (isCreate) {
const { access_token: responseAccessToken, id } = response || {};
if (id && responseAccessToken) {
accessToken.value = responseAccessToken;
toggleAccessToken(true);
} else {
accessToken.value = '';
dialogRef.value.close();
}
} else {
dialogRef.value.close();
}
resetForm();
} catch (error) {
const errorKey = isCreate
@@ -166,17 +205,43 @@ const handleSubmit = async () => {
const initializeForm = () => {
if (props.selectedBot && Object.keys(props.selectedBot).length) {
const { name, description, outgoing_url, thumbnail, bot_config } =
props.selectedBot;
const {
name,
description,
outgoing_url: botUrl,
thumbnail,
bot_config: botConfig,
access_token: botAccessToken,
} = props.selectedBot;
formState.botName = name || '';
formState.botDescription = description || '';
formState.botUrl = outgoing_url || bot_config?.webhook_url || '';
formState.botUrl = botUrl || botConfig?.webhook_url || '';
formState.botAvatarUrl = thumbnail || '';
if (botAccessToken && props.type === MODAL_TYPES.EDIT) {
accessToken.value = botAccessToken;
}
} else {
resetForm();
}
};
const onCopyToken = async value => {
await copyTextToClipboard(value);
useAlert(t('COMPONENTS.CODE.COPY_SUCCESSFUL'));
};
const closeModal = () => {
if (!showAccessToken.value) v$.value?.$reset();
accessToken.value = '';
toggleAccessToken(false);
};
const onClickClose = () => {
closeModal();
dialogRef.value.close();
};
watch(() => props.selectedBot, initializeForm, { immediate: true, deep: true });
defineExpose({ dialogRef });
@@ -187,48 +252,68 @@ defineExpose({ dialogRef });
ref="dialogRef"
type="edit"
:title="dialogTitle"
:description="dialogDescription"
:show-cancel-button="false"
:show-confirm-button="false"
@close="v$.$reset()"
@close="closeModal"
>
<form class="flex flex-col gap-4" @submit.prevent="handleSubmit">
<div class="mb-2 flex flex-col items-start">
<span class="mb-2 text-sm font-medium text-n-slate-12">
{{ $t('AGENT_BOTS.FORM.AVATAR.LABEL') }}
</span>
<Avatar
:src="formState.botAvatarUrl"
:name="formState.botName"
:size="68"
allow-upload
@upload="handleImageUpload"
@delete="handleAvatarDelete"
<div
v-if="!showAccessToken || type === MODAL_TYPES.EDIT"
class="flex flex-col gap-4"
>
<div class="mb-2 flex flex-col items-start">
<span class="mb-2 text-sm font-medium text-n-slate-12">
{{ $t('AGENT_BOTS.FORM.AVATAR.LABEL') }}
</span>
<Avatar
:src="formState.botAvatarUrl"
:name="formState.botName"
:size="68"
allow-upload
icon-name="i-lucide-bot-message-square"
@upload="handleImageUpload"
@delete="handleAvatarDelete"
/>
</div>
<Input
id="bot-name"
v-model="formState.botName"
:label="$t('AGENT_BOTS.FORM.NAME.LABEL')"
:placeholder="$t('AGENT_BOTS.FORM.NAME.PLACEHOLDER')"
:message="botNameError"
:message-type="botNameError ? 'error' : 'info'"
@blur="v$.botName.$touch()"
/>
<TextArea
id="bot-description"
v-model="formState.botDescription"
:label="$t('AGENT_BOTS.FORM.DESCRIPTION.LABEL')"
:placeholder="$t('AGENT_BOTS.FORM.DESCRIPTION.PLACEHOLDER')"
/>
<Input
id="bot-url"
v-model="formState.botUrl"
:label="$t('AGENT_BOTS.FORM.WEBHOOK_URL.LABEL')"
:placeholder="$t('AGENT_BOTS.FORM.WEBHOOK_URL.PLACEHOLDER')"
:message="botUrlError"
:message-type="botUrlError ? 'error' : 'info'"
@blur="v$.botUrl.$touch()"
/>
</div>
<Input
v-model="formState.botName"
:label="$t('AGENT_BOTS.FORM.NAME.LABEL')"
:placeholder="$t('AGENT_BOTS.FORM.NAME.PLACEHOLDER')"
:message="botNameError"
:message-type="botNameError ? 'error' : 'info'"
@blur="v$.botName.$touch()"
/>
<TextArea
v-model="formState.botDescription"
:label="$t('AGENT_BOTS.FORM.DESCRIPTION.LABEL')"
:placeholder="$t('AGENT_BOTS.FORM.DESCRIPTION.PLACEHOLDER')"
/>
<Input
v-model="formState.botUrl"
:label="$t('AGENT_BOTS.FORM.WEBHOOK_URL.LABEL')"
:placeholder="$t('AGENT_BOTS.FORM.WEBHOOK_URL.PLACEHOLDER')"
:message="botUrlError"
:message-type="botUrlError ? 'error' : 'info'"
@blur="v$.botUrl.$touch()"
/>
<div v-if="showAccessTokenInput" class="flex flex-col gap-1">
<label
v-if="type === MODAL_TYPES.EDIT"
class="mb-0.5 text-sm font-medium text-n-slate-12"
>
{{ $t('AGENT_BOTS.ACCESS_TOKEN.TITLE') }}
</label>
<AccessToken :value="accessToken" @on-copy="onCopyToken" />
</div>
<div class="flex items-center justify-end w-full gap-2 px-0 py-2">
<NextButton
@@ -236,9 +321,10 @@ defineExpose({ dialogRef });
slate
type="reset"
:label="$t('AGENT_BOTS.FORM.CANCEL')"
@click="dialogRef.close()"
@click="onClickClose()"
/>
<NextButton
v-if="!showAccessToken"
type="submit"
data-testid="label-submit"
:label="confirmButtonLabel"

View File

@@ -39,6 +39,7 @@ const onClick = () => {
<template #masked>
<button
class="absolute top-1.5 ltr:right-0.5 rtl:left-0.5"
type="button"
@click="toggleMasked"
>
<fluent-icon :icon="maskIcon" :size="16" />
@@ -46,7 +47,7 @@ const onClick = () => {
</template>
</woot-input>
<FormButton
type="submit"
type="button"
size="large"
icon="text-copy"
variant="outline"