Split Web App Settings into tabs; allow unset plugin Control mode

Web App: Settings is now two tabs — Personal settings (account,
appearance, profile, privacy, advanced/danger) and Integrations
(Toyota, Anker Solix). Panels use v-show so loaded state and in-flight
edits survive a tab switch. Adds settings.tabs.* labels (en/pl/da).

API Server panel: non-required select config fields now render a
leading "Not set" option, so a superadmin can leave the Anker Solix
Control mode (and Toyota Brand) unset at the global layer. Previously
every option was a real value, forcing a pick that won the cascade and
locked organizations and users out of choosing their own mode. The
server side already treated an empty value as "abstain"; this closes
the UI gap. Adds plugins.notSet label (en/pl/da) and rebuilds the
embedded panel dist.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
tajniak81
2026-07-19 00:02:37 +02:00
co-authored by Claude Opus 4.8
parent 318e9c1670
commit 8656aaaee3
10 changed files with 50 additions and 3 deletions
+4
View File
@@ -134,6 +134,10 @@
"eyebrow": "Konto",
"title": "Indstillinger",
"subtitle": "Administrer din konto, udseende og dine data.",
"tabs": {
"personal": "Personlige indstillinger",
"integrations": "Integrationer"
},
"account": {
"title": "Konto",
+4
View File
@@ -152,6 +152,10 @@
"eyebrow": "Account",
"title": "Settings",
"subtitle": "Manage your account, appearance, and data.",
"tabs": {
"personal": "Personal settings",
"integrations": "Integrations"
},
"account": {
"title": "Account",
+4
View File
@@ -138,6 +138,10 @@
"eyebrow": "Konto",
"title": "Ustawienia",
"subtitle": "Zarządzaj kontem, wyglądem i danymi.",
"tabs": {
"personal": "Ustawienia osobiste",
"integrations": "Integracje"
},
"account": {
"title": "Konto",
+30 -1
View File
@@ -13,6 +13,11 @@ const loading = ref(true);
const loadError = ref("");
const profile = ref(null);
// Settings is split into two tabs: personal account settings and external
// integrations. The panels stay mounted (v-show) so their loaded state and
// in-flight edits survive a tab switch.
const activeTab = ref("personal"); // "personal" | "integrations"
async function load() {
loading.value = true;
loadError.value = "";
@@ -722,7 +727,24 @@ onBeforeUnmount(() => {
<p v-if="loadError" class="mb-4 rounded-control bg-danger-soft px-4 py-3 text-sm font-medium text-danger">{{ loadError }}</p>
<p v-if="loading" class="text-muted">{{ t("common.loading") }}</p>
<div v-else-if="profile" class="space-y-6">
<div v-else-if="profile">
<!-- Tabs: personal settings vs. integrations -->
<div class="mb-6 flex gap-2 border-b border-subtle">
<button
v-for="tab in ['personal', 'integrations']"
:key="tab"
class="-mb-px border-b-2 px-1 pb-3 text-sm font-semibold transition-colors"
:class="activeTab === tab
? 'border-accent text-strong'
: 'border-transparent text-muted hover:text-body'"
@click="activeTab = tab"
>
{{ t(`settings.tabs.${tab}`) }}
</button>
</div>
<!-- Personal settings -->
<div v-show="activeTab === 'personal'" class="space-y-6">
<!-- Account -->
<section class="dh-card p-6">
<h2 class="mb-4 text-lg font-bold tracking-[-0.02em] text-strong">{{ t("settings.account.title") }}</h2>
@@ -891,7 +913,10 @@ onBeforeUnmount(() => {
</div>
</section>
</div>
<!-- Integrations -->
<div v-show="activeTab === 'integrations'" class="space-y-6">
<section v-if="toyota || anker" class="dh-card p-6">
<h2 class="text-lg font-bold tracking-[-0.02em] text-strong">{{ t("settings.integrations.title") }}</h2>
<p class="mb-4 mt-1 text-sm text-muted">{{ t("settings.integrations.subtitle") }}</p>
@@ -1197,7 +1222,10 @@ onBeforeUnmount(() => {
<p v-if="ankerError" class="mt-2 text-sm text-danger">{{ ankerError }}</p>
</div>
</section>
</div>
<!-- Personal settings (continued) -->
<div v-show="activeTab === 'personal'" class="space-y-6">
<!-- Privacy & Security -->
<section class="dh-card p-6">
<div class="mb-4 flex items-center justify-between">
@@ -1279,6 +1307,7 @@ onBeforeUnmount(() => {
<p v-if="deleteError" class="mt-3 text-sm font-medium text-danger">{{ deleteError }}</p>
</section>
</div>
</div>
</div>
</template>