Profile and settings tabs
Use a contributed tab when a module needs another panel in the local profile or settings page. Each panel is a Vue component with its own loading, validation and save workflow.
Add both components
Create src/components/ContactProfileTab.vue. This example uses the real Account profile proxy to edit the phone number. It preserves the other profile values, extra properties and concurrency stamp rather than replacing the profile with a partial body.
<template>
<form @submit.prevent="save">
<p v-if="refreshFailed" role="alert">
Saved, but application configuration could not be refreshed.
<AbpButton variant="secondary" size="sm" @click="refreshConfiguration"
>Retry refresh</AbpButton
>
</p>
<p v-if="loading" role="status">Loading profile…</p>
<p v-if="failed" role="alert">
Profile could not be loaded. <button type="button" @click="load">Retry</button>
</p>
<AbpFormField v-slot="{ id, describedBy, invalid }" label="Phone number" :errors="errors">
<AbpInput
:id="id"
v-model="form.controls.phoneNumber.value"
type="tel"
:maxlength="32"
:disabled="loading || failed || saving"
:invalid="invalid"
:aria-describedby="describedBy"
/>
</AbpFormField>
<AbpButton type="submit" :loading="saving" :disabled="loading || failed">Save</AbpButton>
</form>
</template>
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref, shallowRef } from 'vue';
import { ConfigStateService, inject } from '@lsw-abpvue/core';
import { ManageProfileStateService } from '@lsw-abpvue/account-core';
import { ProfileService, type ProfileDto } from '@lsw-abpvue/account-core/proxy';
import {
AbpButton,
AbpFormField,
AbpInput,
useAbpForm,
useServerValidation,
useToaster,
useValidationMessages,
Validators,
} from '@lsw-abpvue/theme-shared';
const service = inject(ProfileService);
const state = inject(ManageProfileStateService);
const config = inject(ConfigStateService);
const toaster = useToaster();
const original = shallowRef<ProfileDto>();
const form = useAbpForm({ phoneNumber: { value: '', validators: [Validators.maxLength(32)] } });
useServerValidation(form);
const messages = useValidationMessages();
const errors = computed(() =>
form.controls.phoneNumber.touched ? messages(form.controls.phoneNumber.errors) : [],
);
const loading = ref(true);
const failed = ref(false);
const saving = ref(false);
const refreshFailed = ref(false);
const controller = new AbortController();
onBeforeUnmount(() => controller.abort());
onMounted(load);
async function load(): Promise<void> {
loading.value = true;
failed.value = false;
try {
original.value = await service.get({ signal: controller.signal });
form.reset({ phoneNumber: original.value.phoneNumber ?? '' });
} catch {
if (!controller.signal.aborted) failed.value = true;
} finally {
loading.value = false;
}
}
async function save(): Promise<void> {
if (!original.value || saving.value || !form.validate()) return;
saving.value = true;
try {
const result = await service.update(
{
userName: original.value.userName,
email: original.value.email,
name: original.value.name,
surname: original.value.surname,
concurrencyStamp: original.value.concurrencyStamp,
extraProperties: original.value.extraProperties,
phoneNumber: form.value.phoneNumber,
},
{ signal: controller.signal },
);
original.value = result;
state.set(result);
form.reset({ phoneNumber: result.phoneNumber ?? '' });
await refreshConfiguration();
toaster.success('AbpUi::SavedSuccessfully');
} catch {
/* Request handlers report errors; keep the entered phone number. */
} finally {
saving.value = false;
}
}
async function refreshConfiguration(): Promise<void> {
refreshFailed.value = false;
try {
await config.refreshAppState();
} catch {
refreshFailed.value = true;
}
}
</script>Create src/components/PrintingTab.vue. Use the complete PrintingSettingsAppService from the backend examples. It implements GET/PUT /api/app/printing-settings, accepts/returns { copies: number }, persists a per-user setting and requires AbpIdentity.Users for this walkthrough. These are sample application endpoints, not built-in setting-management endpoints.
<template>
<form @submit.prevent="save">
<p v-if="refreshFailed" role="alert">
Saved, but application configuration could not be refreshed.
<AbpButton variant="secondary" size="sm" @click="refreshConfiguration"
>Retry refresh</AbpButton
>
</p>
<p v-if="loading" role="status">Loading printing settings…</p>
<p v-if="failed" role="alert">
Settings could not be loaded. <button type="button" @click="load">Retry</button>
</p>
<AbpFormField
v-slot="{ id, describedBy, invalid }"
label="Default copies"
:errors="errors"
required
>
<AbpInput
:id="id"
v-model="form.controls.copies.value"
type="number"
:min="1"
:max="20"
:disabled="loading || failed || saving"
:invalid="invalid"
:aria-describedby="describedBy"
/>
</AbpFormField>
<AbpButton type="submit" :loading="saving" :disabled="loading || failed">Save</AbpButton>
</form>
</template>
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
import { ConfigStateService, inject, RestService } from '@lsw-abpvue/core';
import {
AbpButton,
AbpFormField,
AbpInput,
useAbpForm,
useServerValidation,
useToaster,
useValidationMessages,
Validators,
} from '@lsw-abpvue/theme-shared';
interface PrintingSettings {
copies: number;
}
const rest = inject(RestService);
const config = inject(ConfigStateService);
const toaster = useToaster();
const form = useAbpForm<{ copies: number | null }>({
copies: { value: 1, validators: [Validators.required(), Validators.range(1, 20)] },
});
useServerValidation(form);
const messages = useValidationMessages();
const errors = computed(() =>
form.controls.copies.touched ? messages(form.controls.copies.errors) : [],
);
const loading = ref(true);
const failed = ref(false);
const saving = ref(false);
const refreshFailed = ref(false);
const controller = new AbortController();
onBeforeUnmount(() => controller.abort());
onMounted(load);
async function load(): Promise<void> {
loading.value = true;
failed.value = false;
try {
const result = await rest.request<never, PrintingSettings>(
{ method: 'GET', url: '/api/app/printing-settings' },
{ signal: controller.signal },
);
form.reset(result);
} catch {
if (!controller.signal.aborted) failed.value = true;
} finally {
loading.value = false;
}
}
async function save(): Promise<void> {
if (saving.value || loading.value || failed.value || !form.validate()) return;
saving.value = true;
try {
await rest.request(
{ method: 'PUT', url: '/api/app/printing-settings', body: form.value },
{ signal: controller.signal },
);
form.reset(form.value);
await refreshConfiguration();
toaster.success('AbpUi::SavedSuccessfully');
} catch {
/* Request handlers report errors; preserve the form. */
} finally {
saving.value = false;
}
}
async function refreshConfiguration(): Promise<void> {
refreshFailed.value = false;
try {
await config.refreshAppState();
} catch {
refreshFailed.value = true;
}
}
</script>A failed load disables Save and exposes Retry. A failed save keeps the entered value and field errors. An abort signal ends requests when the panel unmounts.
Register the panels
Create src/custom-tabs.ts, adjusting the two imports to the components directory:
import { inject, makeEnvironmentProviders, provideAppInitializer } from '@lsw-abpvue/core';
import { ManageProfileTabsService } from '@lsw-abpvue/account-core';
import { SettingTabsService } from '@lsw-abpvue/setting-management/config';
import ContactProfileTab from './ContactProfileTab.vue';
import PrintingTab from './PrintingTab.vue';
export const customTabs = makeEnvironmentProviders([
provideAppInitializer(() => {
inject(ManageProfileTabsService).add([
{
name: 'BookStore::Contact',
text: 'BookStore::Contact',
order: 3,
component: ContactProfileTab,
},
]);
inject(SettingTabsService).add([
{
name: 'BookStore::Printing',
order: 5,
requiredPolicy: 'AbpIdentity.Users',
component: PrintingTab,
},
]);
}),
]);Add customTabs to your existing startup providers after the selected modules' configuration and default-tab registrations. Keep provideAccountConfig() after OAuth for a local My account destination, provideManageProfileTabs() from account for the personal/password defaults, and provideSettingManagementConfig() for settings. Keep their existing account and setting-management lazy routes. Registering a tab does not create a route or install a backend endpoint.
Add BookStore::Contact and BookStore::Printing to the application's localization resources. Open /account/manage or /setting-management and select the new tab.
Stable identity, order and permission
| Property | Purpose |
|---|---|
name | Stable identity, default localization key |
text | Required on profile tabs; optional label override on settings tabs |
component | Panel to mount when selected |
order | Sort order, default zero |
requiredPolicy | Permission required to see the panel |
visible / invisible | Additional condition / explicit hide flag |
parentName | Parent group identity when grouping |
Patch with tabs.patch(name, { order: 10 }); remove with tabs.remove([name]). Removal also removes descendants. Keep the name stable when changing text. A hidden tab does not remove backend capabilities: its endpoint still enforces authorization.
Refresh and unsaved state
The profile example writes the returned profile into ManageProfileStateService, so other panels see the latest shared profile. Refresh application configuration after saving an effective setting or current-user field that other UI reads. Saving a setting override and reading its effective value are different requests; retain a clear error if refresh fails after the save succeeded.
Switching panels may unmount their components. Page/tab navigation is not automatically protected by a modal's dirty-close guard. For a panel requiring unsaved-change confirmation, implement a page navigation policy or edit inside a guarded modal rather than claiming the tab list saves it automatically.
Scope and verification
These contributions affect the local pages. They do not modify /Account/Manage rendered by a separate authorization server. Check the actual My account provider and destination.
Test first load, load failure/retry, invalid copies, save failure, denied policy, reopen after save, another tab observing updated profile, and tenant-specific effective settings. See authentication, forms and settings.