mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-03 16:16:59 +00:00
feat: read the Pro tabs before the first paint
This commit is contained in:
@@ -360,6 +360,102 @@ describe('boot script: Pro interface', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('boot script: Pro tabs', () => {
|
||||
const tab = (id: string, kind: string, paneId = 'main') => ({ id, kind, labelKey: kind, closeable: id !== 'home-mail', paneId });
|
||||
const proTabs = (state: Record<string, unknown>) =>
|
||||
localStorage.setItem('pro-tabs', JSON.stringify({ state, version: 4 }));
|
||||
const pro = () => ({
|
||||
split: attr('pro-split'),
|
||||
focus: attr('pro-focus'),
|
||||
kind: attr('pro-kind'),
|
||||
main: [attr('pro-main-tabs'), attr('pro-main-active'), attr('pro-main-kind')],
|
||||
side: [attr('pro-split-tabs'), attr('pro-split-active'), attr('pro-split-kind')],
|
||||
});
|
||||
|
||||
beforeEach(() => settings({ proInterface: true }));
|
||||
|
||||
it('draws the single home mail tab when nothing is stored', () => {
|
||||
boot('/en/pro');
|
||||
expect(pro()).toEqual({ split: null, focus: 'main', kind: 'mail', main: ['1', '0', 'mail'], side: [null, null, null] });
|
||||
});
|
||||
|
||||
it('falls back to the home mail tab for corrupt or empty tabs', () => {
|
||||
localStorage.setItem('pro-tabs', '{not json');
|
||||
boot('/en/pro');
|
||||
expect(pro().main).toEqual(['1', '0', 'mail']);
|
||||
proTabs({ tabs: 'nope', activeTabId: 3 });
|
||||
boot('/en/pro');
|
||||
expect(pro().main).toEqual(['1', '0', 'mail']);
|
||||
proTabs({ tabs: [null, { id: 'x' }, 7], activeTabId: 'x' });
|
||||
boot('/en/pro');
|
||||
expect(pro().main).toEqual(['1', '0', 'mail']);
|
||||
});
|
||||
|
||||
it('reads the tab count, the active tab and the split', () => {
|
||||
proTabs({
|
||||
tabs: [tab('home-mail', 'mail'), tab('c', 'calendar'), tab('f', 'folder'), tab('s', 'settings', 'split'), tab('e', 'email', 'split')],
|
||||
activeTabId: 'c',
|
||||
activeSplitTabId: 'e',
|
||||
focusedPaneId: 'split',
|
||||
splitSide: 'left',
|
||||
});
|
||||
boot('/en/pro');
|
||||
expect(pro()).toEqual({
|
||||
split: 'left', focus: 'split', kind: 'email',
|
||||
main: ['3', '1', 'calendar'],
|
||||
side: ['2', '1', 'other'],
|
||||
});
|
||||
});
|
||||
|
||||
it('gives the calendar and settings variants to a pane that shows them', () => {
|
||||
proTabs({ tabs: [tab('home-mail', 'mail'), tab('c', 'calendar'), tab('s', 'settings', 'split')], activeTabId: 'c', activeSplitTabId: 's' });
|
||||
localStorage.setItem('calendar-storage', JSON.stringify({ state: { viewMode: 'week' }, version: 0 }));
|
||||
localStorage.setItem('settings-active-tab', 'filters');
|
||||
boot('/en/pro');
|
||||
expect(attr('calendar-view')).toBe('week');
|
||||
expect(cssVar('--boot-cal-cols')).toBe('7');
|
||||
expect(attr('settings-tab')).toBe('filters');
|
||||
expect(attr('pane')).toBeNull();
|
||||
});
|
||||
|
||||
it('merges a split pane into an empty main pane', () => {
|
||||
proTabs({ tabs: [tab('c', 'calendar', 'split'), tab('f', 'files', 'split')], activeTabId: 'gone', activeSplitTabId: 'f', focusedPaneId: 'split' });
|
||||
boot('/en/pro');
|
||||
expect(pro()).toEqual({ split: null, focus: 'main', kind: 'files', main: ['2', '1', 'files'], side: [null, null, null] });
|
||||
});
|
||||
|
||||
it('focuses the tab the redirect opens for a standard page', () => {
|
||||
proTabs({
|
||||
tabs: [tab('home-mail', 'mail'), tab('c', 'calendar', 'split')],
|
||||
activeTabId: 'home-mail',
|
||||
activeSplitTabId: 'c',
|
||||
focusedPaneId: 'main',
|
||||
});
|
||||
boot('/en/calendar/week/2026-11-04');
|
||||
expect(pro()).toMatchObject({ split: 'right', focus: 'split', kind: 'calendar', main: ['1', '0', 'mail'], side: ['1', '0', 'calendar'] });
|
||||
expect(attr('calendar-view')).toBe('week');
|
||||
|
||||
// No contacts tab yet: a new one at the end of the focused pane.
|
||||
boot('/en/contacts');
|
||||
expect(pro()).toMatchObject({ focus: 'main', kind: 'contacts', main: ['2', '1', 'contacts'], side: ['1', '0', 'calendar'] });
|
||||
});
|
||||
|
||||
it('caps the drawn tabs at eight', () => {
|
||||
const many = Array.from({ length: 12 }, (_, i) => tab(`t${i}`, i === 11 ? 'files' : 'email'));
|
||||
proTabs({ tabs: many, activeTabId: 't11' });
|
||||
boot('/en/pro');
|
||||
expect(pro().main).toEqual(['8', '7', 'files']);
|
||||
});
|
||||
|
||||
it('sets nothing when Pro does not take the page over', () => {
|
||||
proTabs({ tabs: [tab('home-mail', 'mail'), tab('c', 'calendar', 'split')], activeSplitTabId: 'c' });
|
||||
desktop(false);
|
||||
boot('/en/mail');
|
||||
expect(pro()).toEqual({ split: null, focus: null, kind: null, main: [null, null, null], side: [null, null, null] });
|
||||
expect(attr('calendar-view')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('boot script: signed-out visitors', () => {
|
||||
it('marks a visitor with nothing stored as signed out on app pages', () => {
|
||||
boot('/en/mail');
|
||||
|
||||
@@ -174,7 +174,70 @@ export function applyBootLayout(root: HTMLElement, c: BootLayoutConfig): void {
|
||||
|
||||
var desktop = false;
|
||||
try { desktop = w.matchMedia('(min-width: 64rem)').matches; } catch { /* assume not */ }
|
||||
mark('pro', s.proInterface === true && desktop && (surface === 'pro' || has(c.pro, surface)) ? 'on' : null);
|
||||
var proOn = s.proInterface === true && desktop && (surface === 'pro' || has(c.pro, surface));
|
||||
mark('pro', proOn ? 'on' : null);
|
||||
|
||||
// --- Pro tabs ----------------------------------------------------------
|
||||
// What the Pro shell will show (stores/pro-tab-store.ts, normalizeProTabState):
|
||||
// each pane's tabs and its active tab, after the redirect from a standard
|
||||
// page has opened or focused that page's tab (ProInterfaceRedirect, openTab).
|
||||
var proShows: string[] = [];
|
||||
var proTabs: { id: unknown; kind: string; pane: string }[] = [];
|
||||
var proState = proOn ? stateOf('pro-tabs') : {};
|
||||
var rawTabs = Array.isArray(proState.tabs) ? (proState.tabs as unknown[]) : [];
|
||||
for (var t = 0; t < rawTabs.length; t++) {
|
||||
var rt = rawTabs[t] as Record<string, unknown> | null;
|
||||
if (rt && typeof rt === 'object' && typeof rt.kind === 'string') {
|
||||
proTabs.push({ id: rt.id, kind: rt.kind, pane: rt.paneId === 'split' ? 'split' : 'main' });
|
||||
}
|
||||
}
|
||||
if (!proTabs.length) proTabs.push({ id: 'home-mail', kind: 'mail', pane: 'main' });
|
||||
var activeMain = proState.activeTabId;
|
||||
var activeSplit = proState.activeSplitTabId;
|
||||
var inPane = function (which: string) {
|
||||
return proTabs.filter(function (tab) { return tab.pane === which; });
|
||||
};
|
||||
if (!inPane('main').length) {
|
||||
// An empty main pane takes the split pane's tabs over.
|
||||
for (var m = 0; m < proTabs.length; m++) proTabs[m].pane = 'main';
|
||||
activeMain = activeSplit;
|
||||
activeSplit = null;
|
||||
}
|
||||
var proFocus = proState.focusedPaneId === 'split' && inPane('split').length ? 'split' : 'main';
|
||||
if (proOn && surface !== 'pro') {
|
||||
var opened = null as { id: unknown; kind: string; pane: string } | null;
|
||||
for (var o = 0; o < proTabs.length && !opened; o++) if (proTabs[o].kind === surface) opened = proTabs[o];
|
||||
if (!opened) {
|
||||
opened = { id: '#boot-opened', kind: surface, pane: proFocus };
|
||||
proTabs.push(opened);
|
||||
}
|
||||
if (opened.pane === 'split') activeSplit = opened.id;
|
||||
else activeMain = opened.id;
|
||||
proFocus = opened.pane;
|
||||
}
|
||||
var proPane = function (which: string, activeId: unknown): string {
|
||||
var list = inPane(which);
|
||||
if (!proOn || !list.length) {
|
||||
mark('pro-' + which + '-tabs', null);
|
||||
mark('pro-' + which + '-active', null);
|
||||
mark('pro-' + which + '-kind', null);
|
||||
return '';
|
||||
}
|
||||
var at = 0;
|
||||
for (var p = 0; p < list.length; p++) if (list[p].id === activeId) at = p;
|
||||
var kind = list[at].kind;
|
||||
// The skeleton draws up to eight tabs; a longer strip scrolls anyway.
|
||||
mark('pro-' + which + '-tabs', String(Math.min(list.length, 8)));
|
||||
mark('pro-' + which + '-active', String(Math.min(at, 7)));
|
||||
mark('pro-' + which + '-kind', has(c.pro, kind) ? kind : 'other');
|
||||
proShows.push(kind);
|
||||
return kind;
|
||||
};
|
||||
var mainKind = proPane('main', activeMain);
|
||||
var splitKind = proPane('split', activeSplit);
|
||||
mark('pro-split', proOn && splitKind ? (proState.splitSide === 'left' ? 'left' : 'right') : null);
|
||||
mark('pro-focus', proOn ? proFocus : null);
|
||||
mark('pro-kind', proOn ? (proFocus === 'split' ? splitKind : mainKind) : null);
|
||||
|
||||
// --- Which pane is open ----------------------------------------------
|
||||
var pane = '';
|
||||
@@ -187,21 +250,22 @@ export function applyBootLayout(root: HTMLElement, c: BootLayoutConfig): void {
|
||||
if (seg[1]) pane = 'detail';
|
||||
}
|
||||
|
||||
if (surface === 'settings') {
|
||||
// A Pro pane showing settings or the calendar needs their variants too.
|
||||
if (surface === 'settings' || proShows.indexOf('settings') !== -1) {
|
||||
var migrate = function (raw: string | null): string | null {
|
||||
if (!raw) return null;
|
||||
var id = c.legacyTabs[raw] || raw;
|
||||
return has(c.tabs, id) ? id : null;
|
||||
};
|
||||
var linked = migrate(seg[1] || null) || migrate(readSession('settings-deep-link-tab'));
|
||||
if (linked) pane = 'detail';
|
||||
var linked = migrate(surface === 'settings' ? seg[1] || null : null) || migrate(readSession('settings-deep-link-tab'));
|
||||
if (linked && surface === 'settings') pane = 'detail';
|
||||
mark('settings-tab', linked || migrate(readLocal('settings-active-tab')) || 'appearance');
|
||||
} else mark('settings-tab', null);
|
||||
mark('pane', pane);
|
||||
|
||||
// --- Calendar ----------------------------------------------------------
|
||||
if (surface === 'calendar') {
|
||||
var view = seg[1] || '';
|
||||
if (surface === 'calendar' || proShows.indexOf('calendar') !== -1) {
|
||||
var view = surface === 'calendar' ? seg[1] || '' : '';
|
||||
var calView = has(c.calendarViews, view) ? view : '';
|
||||
if (!calView && /^\d{4}-\d{2}-\d{2}$/.test(view)) calView = 'month';
|
||||
if (!calView) {
|
||||
|
||||
Reference in New Issue
Block a user