fix: stabilize development config and account settings

This commit is contained in:
2026-07-11 17:15:38 +08:00
parent d2cb6bbbc6
commit 7f2d5579ff
19 changed files with 607 additions and 392 deletions
@@ -18,230 +18,254 @@ import AudioTranscription from './components/AudioTranscription.vue';
import SectionLayout from './components/SectionLayout.vue';
export default {
components: {
BaseSettingsHeader,
NextButton,
AccountId,
BuildInfo,
AccountDelete,
AudioTranscription,
SectionLayout,
WithLabel,
NextInput,
},
setup() {
const { updateUISettings, uiSettings } = useUISettings();
const { enabledLanguages } = useConfig();
const { accountId } = useAccount();
const v$ = useVuelidate();
components: {
BaseSettingsHeader,
NextButton,
AccountId,
BuildInfo,
AccountDelete,
AudioTranscription,
SectionLayout,
WithLabel,
NextInput,
},
setup() {
const { updateUISettings, uiSettings } = useUISettings();
const { enabledLanguages } = useConfig();
const { accountId } = useAccount();
const v$ = useVuelidate();
return { updateUISettings, uiSettings, v$, enabledLanguages, accountId };
},
data() {
return {
id: '',
name: '',
locale: 'en',
domain: '',
supportEmail: '',
features: {},
};
},
validations: {
name: {
required,
},
locale: {
required,
},
},
computed: {
...mapGetters({
getAccount: 'accounts/getAccount',
uiFlags: 'accounts/getUIFlags',
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
isOnChatwootCloud: 'globalConfig/isOnChatwootCloud',
}),
showAudioTranscriptionConfig() {
return this.isFeatureEnabledonAccount(
this.accountId,
FEATURE_FLAGS.CAPTAIN
);
},
languagesSortedByCode() {
const enabledLanguages = [...this.enabledLanguages];
return enabledLanguages.sort((l1, l2) =>
l1.iso_639_1_code.localeCompare(l2.iso_639_1_code)
);
},
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
);
},
currentAccount() {
return this.getAccount(this.accountId) || {};
},
},
mounted() {
this.initializeAccount();
},
methods: {
async initializeAccount() {
try {
const { name, locale, id, domain, support_email, features } =
this.getAccount(this.accountId);
return {
updateUISettings,
uiSettings,
v$,
enabledLanguages,
accountId,
};
},
data() {
return {
id: '',
name: '',
locale: 'en',
domain: '',
supportEmail: '',
features: {},
};
},
validations: {
name: {
required,
},
locale: {
required,
},
},
computed: {
...mapGetters({
getAccount: 'accounts/getAccount',
uiFlags: 'accounts/getUIFlags',
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
isOnChatwootCloud: 'globalConfig/isOnChatwootCloud',
}),
showAudioTranscriptionConfig() {
return this.isFeatureEnabledonAccount(
this.accountId,
FEATURE_FLAGS.CAPTAIN
);
},
languagesSortedByCode() {
const enabledLanguages = [...this.enabledLanguages];
return enabledLanguages.sort((l1, l2) =>
l1.iso_639_1_code.localeCompare(l2.iso_639_1_code)
);
},
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
);
},
currentAccount() {
return this.getAccount(this.accountId) || {};
},
},
watch: {
currentAccount: {
handler(account) {
this.initializeAccount(account);
},
immediate: true,
},
},
methods: {
initializeAccount(account = {}) {
if (!account.id) {
return;
}
const effectiveLocale = this.uiSettings?.locale || locale;
if (effectiveLocale) {
this.$root.$i18n.locale = effectiveLocale;
}
this.name = name;
this.locale = locale;
this.id = id;
this.domain = domain;
this.supportEmail = support_email;
this.features = features;
} catch (error) {
// Ignore error
}
},
const { name, locale, id, domain, support_email, features } =
account;
const effectiveLocale = this.uiSettings?.locale || locale;
if (effectiveLocale) {
this.$root.$i18n.locale = effectiveLocale;
}
this.name = name || '';
this.locale = locale || 'en';
this.id = id;
this.domain = domain || '';
this.supportEmail = support_email || '';
this.features = features || {};
},
async updateAccount() {
this.v$.$touch();
if (this.v$.$invalid) {
useAlert(this.$t('GENERAL_SETTINGS.FORM.ERROR'));
return;
}
try {
await this.$store.dispatch('accounts/update', {
locale: this.locale,
name: this.name,
domain: this.domain,
support_email: this.supportEmail,
});
// If user locale is set, update the locale with user locale
const updatedLocale = this.uiSettings?.locale || this.locale;
if (updatedLocale) {
this.$root.$i18n.locale = updatedLocale;
}
this.getAccount(this.id).locale = this.locale;
useAlert(this.$t('GENERAL_SETTINGS.UPDATE.SUCCESS'));
} catch (error) {
useAlert(this.$t('GENERAL_SETTINGS.UPDATE.ERROR'));
}
},
},
async updateAccount() {
this.v$.$touch();
if (this.v$.$invalid) {
useAlert(this.$t('GENERAL_SETTINGS.FORM.ERROR'));
return;
}
try {
await this.$store.dispatch('accounts/update', {
locale: this.locale,
name: this.name,
domain: this.domain,
support_email: this.supportEmail,
});
// If user locale is set, update the locale with user locale
const updatedLocale = this.uiSettings?.locale || this.locale;
if (updatedLocale) {
this.$root.$i18n.locale = updatedLocale;
}
this.getAccount(this.id).locale = this.locale;
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 max-w-2xl ltr:mr-auto rtl:ml-auto">
<BaseSettingsHeader :title="$t('GENERAL_SETTINGS.TITLE')" />
<div class="flex-grow flex-shrink min-w-0 mt-3">
<SectionLayout
:title="$t('GENERAL_SETTINGS.FORM.GENERAL_SECTION.TITLE')"
:description="$t('GENERAL_SETTINGS.FORM.GENERAL_SECTION.NOTE')"
class="!pt-0"
>
<form
v-if="!uiFlags.isFetchingItem"
class="grid gap-4"
@submit.prevent="updateAccount"
>
<WithLabel
name="account-name"
:has-error="v$.name.$error"
:label="$t('GENERAL_SETTINGS.FORM.NAME.LABEL')"
:error-message="$t('GENERAL_SETTINGS.FORM.NAME.ERROR')"
>
<NextInput
v-model="name"
type="text"
class="w-full"
:placeholder="$t('GENERAL_SETTINGS.FORM.NAME.PLACEHOLDER')"
@blur="v$.name.$touch"
/>
</WithLabel>
<WithLabel
name="site-language"
: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"
name="custom-domain"
: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')
}}
<div class="flex flex-col w-full max-w-2xl ltr:mr-auto rtl:ml-auto">
<BaseSettingsHeader :title="$t('GENERAL_SETTINGS.TITLE')" />
<div class="flex-grow flex-shrink min-w-0 mt-3">
<SectionLayout
:title="$t('GENERAL_SETTINGS.FORM.GENERAL_SECTION.TITLE')"
:description="$t('GENERAL_SETTINGS.FORM.GENERAL_SECTION.NOTE')"
class="!pt-0"
>
<form
v-if="!uiFlags.isFetchingItem"
class="grid gap-4"
@submit.prevent="updateAccount"
>
<WithLabel
name="account-name"
:has-error="v$.name.$error"
:label="$t('GENERAL_SETTINGS.FORM.NAME.LABEL')"
:error-message="$t('GENERAL_SETTINGS.FORM.NAME.ERROR')"
>
<NextInput
v-model="name"
type="text"
class="w-full"
:placeholder="
$t('GENERAL_SETTINGS.FORM.NAME.PLACEHOLDER')
"
@blur="v$.name.$touch"
/>
</WithLabel>
<WithLabel
name="site-language"
: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"
name="custom-domain"
: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')
}}
</template>
</WithLabel>
<WithLabel
v-if="featureCustomReplyEmailEnabled"
name="support-email"
: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>
</form>
</SectionLayout>
{{
featureCustomReplyDomainEnabled &&
$t(
'GENERAL_SETTINGS.FORM.FEATURES.CUSTOM_EMAIL_DOMAIN_ENABLED'
)
}}
</template>
</WithLabel>
<WithLabel
v-if="featureCustomReplyEmailEnabled"
name="support-email"
: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>
</form>
</SectionLayout>
<woot-loading-state v-if="uiFlags.isFetchingItem" />
</div>
<AudioTranscription v-if="showAudioTranscriptionConfig" />
<AccountId />
<div v-if="!uiFlags.isFetchingItem && isOnChatwootCloud">
<AccountDelete />
</div>
<BuildInfo />
</div>
<woot-loading-state v-if="uiFlags.isFetchingItem" />
</div>
<AudioTranscription v-if="showAudioTranscriptionConfig" />
<AccountId />
<div v-if="!uiFlags.isFetchingItem && isOnChatwootCloud">
<AccountDelete />
</div>
<BuildInfo />
</div>
</template>