Integrations, sorted into the categories the panel already sorts them into

The API Server panel has grouped plugins by category since the plugin list was
drawn — car manufacturers, EV chargers, notifications, each tab carrying its
count. The Settings page had the same integrations in one flat stack, so the two
screens described the same set of things in two different shapes. Now they agree:
the same categories, the same counted tabs, the same accent border on the active
one. The category ids come from the Category* constants both sides already read,
so nothing here invents a third vocabulary.

Only a category holding something gets a tab, which is the panel's rule too: two
tabs today rather than six empty ones. Below one category the bar hides itself
entirely — with nothing to switch between, a single tab is a label pretending to
be a control.

The active group falls back to the first rather than to a fixed id, because the
three integration views load independently and a tab can be momentarily empty on
first paint; falling back keeps the section populated instead of blanking it.
The cards moved into a space-y-4 wrapper and lost their per-card mt-4, so the
first card sits the same distance under the bar whichever tab is showing —
previously Toyota was the only one that could ever be first.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
tajniak81
2026-08-31 21:20:08 +02:00
co-authored by Claude Opus 5
parent a3f69fa5ef
commit 06f91578da
4 changed files with 56 additions and 3 deletions
+1
View File
@@ -251,6 +251,7 @@
}, },
"integrations": { "integrations": {
"categories": { "vehicles": "Bilproducenter", "chargers": "EV-ladere" },
"title": "Integrationer", "title": "Integrationer",
"subtitle": "Forbind eksterne tjenester til din konto.", "subtitle": "Forbind eksterne tjenester til din konto.",
"toyota": "Toyota Connected (MyToyota)", "toyota": "Toyota Connected (MyToyota)",
+1
View File
@@ -250,6 +250,7 @@
}, },
"integrations": { "integrations": {
"categories": { "vehicles": "Car manufacturers", "chargers": "EV chargers" },
"title": "Integrations", "title": "Integrations",
"subtitle": "Connect external services to your account.", "subtitle": "Connect external services to your account.",
"toyota": "Toyota Connected (MyToyota)", "toyota": "Toyota Connected (MyToyota)",
+1
View File
@@ -255,6 +255,7 @@
}, },
"integrations": { "integrations": {
"categories": { "vehicles": "Producenci samochodów", "chargers": "Ładowarki EV" },
"title": "Integracje", "title": "Integracje",
"subtitle": "Połącz zewnętrzne usługi ze swoim kontem.", "subtitle": "Połącz zewnętrzne usługi ze swoim kontem.",
"toyota": "Toyota Connected (MyToyota)", "toyota": "Toyota Connected (MyToyota)",
+53 -3
View File
@@ -424,6 +424,39 @@ async function testToyotaConnection() {
} }
} }
// --- Integrations: category tabs ---
//
// The API Server panel groups plugins into the same categories (see
// panel/src/components/PluginsCard.vue, and the Category* constants in
// internal/plugins/plugin.go that both read from), so grouping them the same way
// here means someone who has seen one list can read the other. Only a category
// that actually has an integration gets a tab — two tabs today, not six empty
// ones — and a category whose integrations are all still loading has none, which
// is why the active group falls back to the first rather than to a fixed id.
const INTEGRATION_CATEGORIES = ["vehicles", "chargers"];
const integrationsByCategory = computed(() => ({
vehicles: [toyota.value && "toyota"].filter(Boolean),
chargers: [anker.value && "anker", greencell.value && "greencell"].filter(Boolean),
}));
const integrationGroups = computed(() =>
INTEGRATION_CATEGORIES.filter((c) => integrationsByCategory.value[c].length).map((c) => ({
id: c,
label: t(`settings.integrations.categories.${c}`),
ids: integrationsByCategory.value[c],
}))
);
const integrationTab = ref("");
const activeIntegrationGroup = computed(
() => integrationGroups.value.find((g) => g.id === integrationTab.value) || integrationGroups.value[0]
);
function showsIntegration(id) {
return !!activeIntegrationGroup.value?.ids.includes(id);
}
// --- Integrations: Anker Solix (V1 Smart EV Charger) --- // --- Integrations: Anker Solix (V1 Smart EV Charger) ---
// //
// Same superadmin → org admin → user cascade as Toyota above; the fields are the // Same superadmin → org admin → user cascade as Toyota above; the fields are the
@@ -1211,7 +1244,23 @@ onBeforeUnmount(() => {
<h2 class="text-lg font-bold tracking-[-0.02em] text-strong">{{ t("settings.integrations.title") }}</h2> <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> <p class="mb-4 mt-1 text-sm text-muted">{{ t("settings.integrations.subtitle") }}</p>
<div v-if="toyota" class="rounded-control border border-subtle p-4"> <!-- Category tabs, mirroring the API Server panel's plugin list. -->
<nav v-if="integrationGroups.length > 1" class="mb-4 flex flex-wrap gap-1.5 border-b border-subtle pb-3">
<button
v-for="g in integrationGroups"
:key="g.id"
type="button"
class="dh-btn dh-btn-ghost !px-3 !py-1.5 text-sm"
:class="activeIntegrationGroup?.id === g.id ? 'border-accent text-brandtext' : ''"
@click="integrationTab = g.id"
>
{{ g.label }}
<span class="dh-badge dh-badge-neutral ml-2">{{ g.ids.length }}</span>
</button>
</nav>
<div class="space-y-4">
<div v-if="toyota && showsIntegration('toyota')" class="rounded-control border border-subtle p-4">
<button <button
type="button" type="button"
class="flex w-full items-start justify-between gap-3 text-left" class="flex w-full items-start justify-between gap-3 text-left"
@@ -1341,7 +1390,7 @@ onBeforeUnmount(() => {
</div> </div>
<!-- Anker Solix (V1 Smart EV Charger) --> <!-- Anker Solix (V1 Smart EV Charger) -->
<div v-if="anker" class="mt-4 rounded-control border border-subtle p-4"> <div v-if="anker && showsIntegration('anker')" class="rounded-control border border-subtle p-4">
<button <button
type="button" type="button"
class="flex w-full items-start justify-between gap-3 text-left" class="flex w-full items-start justify-between gap-3 text-left"
@@ -1588,7 +1637,7 @@ onBeforeUnmount(() => {
</div> </div>
<!-- Greencell (HabuDen EV charger, read over your own MQTT broker) --> <!-- Greencell (HabuDen EV charger, read over your own MQTT broker) -->
<div v-if="greencell" class="mt-4 rounded-control border border-subtle p-4"> <div v-if="greencell && showsIntegration('greencell')" class="rounded-control border border-subtle p-4">
<button <button
type="button" type="button"
class="flex w-full items-start justify-between gap-3 text-left" class="flex w-full items-start justify-between gap-3 text-left"
@@ -1792,6 +1841,7 @@ onBeforeUnmount(() => {
<p v-if="greencellError" class="mt-2 text-sm text-danger">{{ greencellError }}</p> <p v-if="greencellError" class="mt-2 text-sm text-danger">{{ greencellError }}</p>
</div> </div>
</div> </div>
</div>
</section> </section>
</div> </div>