A page opens where you put its tabs
Every tab bar in the app drags into the order you want, and then all three of them opened on a tab picked in the source anyway: "public" on Charging, "info" on a car, "personal" in Settings. Dragging Home chargers to the front of the charging bar rearranged the bar and changed nothing about where the page landed, which is the opposite of what dragging it there says. So the front of the bar is now the landing tab, everywhere. An arrangement is already the statement of what you want to see first; it just wasn't being read as one. Settings > Appearance overrides it per page for the case where reading order and landing tab are two different wishes, with "First in the bar" as the default and the meaning of no override at all. The rule lives in one place, lib/tabs.js, because it is one rule and three pages: the saved choice if that tab is actually on the bar, otherwise whatever leads it. The bar it is given is the one that will really render, hidden tabs and inapplicable ones already dropped, so a default that no longer has a button - a tab switched off for that car, Users on a non-admin - falls back to the front instead of opening nothing. The tab key lists moved there too, since the picker needs all three and would otherwise have copied them. Each page starts on no tab and keeps following the profile until the user says otherwise, rather than guessing and then correcting itself: the arrangement and the default both arrive with /api/me, which on a hard refresh lands after the view has mounted. A click ends the following, and so does the start of a drag - rearranging a bar must not pull the content out from under the pointer. In Settings ?tab= still wins over both, since that is what /admin redirects to. Stored as defaultTabs on the profile, one page->tab map validated per page: a tab that exists but on another page is an error, and an empty value is stored as an absent key so "no default" has a single representation. Also adds charger_tab_order and charger_card_order to the PocketBase setup script. They were never there - the arrangements of the last two commits had no column to persist into on a freshly set-up server - and default_tabs would have gone the same way beside them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
641f427db4
commit
f3235c403c
13 files changed
+347
-33
No files matched your search
@@ -6,6 +6,7 @@ import { state, isAdmin, logout, refreshProfile } from "../auth";
|
||||
import { prefs, applyProfilePrefs } from "../prefs";
|
||||
import { formatDate, formatMoney } from "../lib/format.js";
|
||||
import { t, tSplit, TRANSLATED_LANGUAGES } from "../i18n";
|
||||
import { TAB_SURFACES, SETTINGS_TABS, defaultTabFor } from "../lib/tabs.js";
|
||||
import { askConfirm } from "../lib/confirm.js";
|
||||
import OrgManager from "../components/OrgManager.vue";
|
||||
import AdminUsers from "../components/AdminUsers.vue";
|
||||
@@ -22,16 +23,31 @@ const profile = ref(null);
|
||||
// mounted (v-show) so their loaded state and in-flight edits survive a tab
|
||||
// switch.
|
||||
//
|
||||
// `?tab=` picks the starting tab, which is what /admin redirects to.
|
||||
const ALL_TABS = ["personal", "users", "organization", "integrations"];
|
||||
// `?tab=` picks the starting tab, which is what /admin redirects to. Without
|
||||
// one the page opens on the account's default (below, in Appearance), and
|
||||
// otherwise on the first tab.
|
||||
const ALL_TABS = SETTINGS_TABS;
|
||||
const tabs = computed(() => ALL_TABS.filter((tab) => tab !== "users" || isAdmin.value));
|
||||
const activeTab = ref(ALL_TABS.includes(route.query.tab) ? route.query.tab : "personal");
|
||||
const tabPicked = ref(ALL_TABS.includes(route.query.tab)); // `?tab=` is an explicit ask
|
||||
const activeTab = ref(tabPicked.value ? route.query.tab : "personal");
|
||||
|
||||
// The admin gate only settles once the profile is loaded, so a non-admin who
|
||||
// asked for ?tab=users lands back on the personal tab rather than on nothing.
|
||||
watch(tabs, (list) => {
|
||||
if (!list.includes(activeTab.value)) activeTab.value = "personal";
|
||||
});
|
||||
// The admin gate only settles once the profile is loaded — which is also when
|
||||
// the default arrives — so until then the bar is still being decided: a
|
||||
// non-admin who asked for ?tab=users lands back on the personal tab rather than
|
||||
// on nothing, and a default of Users does the same.
|
||||
watch(
|
||||
[tabs, () => prefs.defaultTabs],
|
||||
([list]) => {
|
||||
if (!tabPicked.value) activeTab.value = defaultTabFor("settings", list);
|
||||
else if (!list.includes(activeTab.value)) activeTab.value = "personal";
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
function selectTab(tab) {
|
||||
tabPicked.value = true;
|
||||
activeTab.value = tab;
|
||||
}
|
||||
|
||||
// Each integration card folds open/closed, like the plugin rows in the API
|
||||
// Server panel. Collapsed by default so the Integrations tab reads as a compact
|
||||
@@ -160,6 +176,18 @@ async function saveAppearance(patch) {
|
||||
}
|
||||
}
|
||||
|
||||
// Which tab each tabbed page opens on. Empty means "whichever tab leads the
|
||||
// bar", so somebody who has already dragged their tabs into the order they want
|
||||
// never has to come here at all. Saved through saveAppearance like everything
|
||||
// else on this card, as a whole map — the API stores it as one field.
|
||||
function defaultTabValue(surface) {
|
||||
return prefs.defaultTabs?.[surface] || "";
|
||||
}
|
||||
|
||||
function saveDefaultTab(surface, key) {
|
||||
saveAppearance({ defaultTabs: { ...prefs.defaultTabs, [surface]: key } });
|
||||
}
|
||||
|
||||
const dateFormatExample = computed(() => formatDate(new Date().toISOString()));
|
||||
const currencyExample = computed(() => formatMoney(1234.5));
|
||||
|
||||
@@ -986,7 +1014,7 @@ onBeforeUnmount(() => {
|
||||
:class="activeTab === tab
|
||||
? 'border-accent text-strong'
|
||||
: 'border-transparent text-muted hover:text-body'"
|
||||
@click="activeTab = tab"
|
||||
@click="selectTab(tab)"
|
||||
>
|
||||
{{ t(`settings.tabs.${tab}`) }}
|
||||
</button>
|
||||
@@ -1123,6 +1151,24 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-5">
|
||||
<label class="dh-label">{{ t("settings.appearance.defaultTab") }}</label>
|
||||
<div class="grid gap-4 sm:grid-cols-3">
|
||||
<div v-for="page in TAB_SURFACES" :key="page.surface">
|
||||
<p class="mb-1.5 text-xs text-muted">{{ t(`settings.appearance.defaultTabPage.${page.surface}`) }}</p>
|
||||
<select
|
||||
:value="defaultTabValue(page.surface)"
|
||||
class="dh-input"
|
||||
@change="saveDefaultTab(page.surface, $event.target.value)"
|
||||
>
|
||||
<option value="">{{ t("settings.appearance.defaultTabFirst") }}</option>
|
||||
<option v-for="key in page.keys" :key="key" :value="key">{{ t(page.labelKey(key)) }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-1 text-xs text-muted">{{ t("settings.appearance.defaultTabHint") }}</p>
|
||||
</div>
|
||||
|
||||
<p v-if="appearanceError" class="mt-3 text-sm text-danger">{{ appearanceError }}</p>
|
||||
</section>
|
||||
|
||||
|
||||
Reference in new issue
Block a user