mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-04 00:14:12 +00:00
fix: load the assistant's calendar and settings calls on first use so plugin views don't load the calendar store
This commit is contained in:
@@ -112,8 +112,7 @@ vi.mock('@/stores/settings-store', () => {
|
||||
typeof sel === 'function' ? sel(state) : state;
|
||||
hook.getState = () => state;
|
||||
hook.setState = (p: Partial<typeof state>) => Object.assign(state, p);
|
||||
// The calendar store (reached through the assistant's host calls) registers its sync bridge on import.
|
||||
return { useSettingsStore: hook, registerCalendarSubscriptionSyncBridge: () => {} };
|
||||
return { useSettingsStore: hook };
|
||||
});
|
||||
|
||||
vi.mock('@/stores/contact-store', () => {
|
||||
@@ -406,27 +405,4 @@ describe('plugins editing the message being written', () => {
|
||||
cleanup();
|
||||
expect(composerHandle(id)).toBeNull();
|
||||
});
|
||||
|
||||
// api.files.attach: a file from the user's Files keeps the login that
|
||||
// holds it, so a draft written from another login copies it over before
|
||||
// it is sent (rehomeForeignBlobs) and reads go to its owner.
|
||||
it('attaches a Files file with the login that holds it', async () => {
|
||||
const fetchB = vi.fn(async () => ({ type: 'text/plain', size: 1, arrayBuffer: async () => new Uint8Array([7]).buffer }));
|
||||
const previous = useAuthStore.getState();
|
||||
const oldGetClient = previous.getClientForAccount;
|
||||
useAuthStore.setState({ getClientForAccount: (id: string) => (id === 'login-b' ? { fetchBlob: fetchB } : null) as never });
|
||||
try {
|
||||
const { handle } = await renderComposer();
|
||||
let ok = false;
|
||||
await act(async () => { ok = await handle.attachFile!({ blobId: 'F1', name: 'notes.md', type: 'text/markdown', size: 1, sourceAccountId: 'login-b' }); });
|
||||
expect(ok).toBe(true);
|
||||
expect(await screen.findByText('notes.md')).toBeTruthy();
|
||||
const [attached] = handle.readMetadata!().attachments;
|
||||
await handle.readAttachment!(attached.id);
|
||||
expect(fetchB).toHaveBeenCalledWith('F1', 'notes.md', 'text/markdown');
|
||||
} finally {
|
||||
cleanup();
|
||||
useAuthStore.setState({ getClientForAccount: oldGetClient });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -118,8 +118,7 @@ vi.mock('@/stores/settings-store', () => {
|
||||
typeof sel === 'function' ? sel(state) : state;
|
||||
hook.getState = () => state;
|
||||
hook.setState = (p: Partial<typeof state>) => Object.assign(state, p);
|
||||
// The calendar store (reached through the assistant's host calls) registers its sync bridge on import.
|
||||
return { useSettingsStore: hook, registerCalendarSubscriptionSyncBridge: () => {} };
|
||||
return { useSettingsStore: hook };
|
||||
});
|
||||
|
||||
vi.mock('@/stores/contact-store', () => {
|
||||
|
||||
@@ -110,8 +110,7 @@ vi.mock('@/stores/settings-store', () => {
|
||||
typeof sel === 'function' ? sel(state) : state;
|
||||
hook.getState = () => state;
|
||||
hook.setState = (p: Partial<typeof state>) => Object.assign(state, p);
|
||||
// The calendar store (reached through the assistant's host calls) registers its sync bridge on import.
|
||||
return { useSettingsStore: hook, registerCalendarSubscriptionSyncBridge: () => {} };
|
||||
return { useSettingsStore: hook };
|
||||
});
|
||||
|
||||
vi.mock('@/stores/contact-store', () => {
|
||||
|
||||
@@ -109,8 +109,7 @@ vi.mock('@/stores/settings-store', () => {
|
||||
typeof sel === 'function' ? sel(state) : state;
|
||||
hook.getState = () => state;
|
||||
hook.setState = (p: Partial<typeof state>) => Object.assign(state, p);
|
||||
// The calendar store (reached through the assistant's host calls) registers its sync bridge on import.
|
||||
return { useSettingsStore: hook, registerCalendarSubscriptionSyncBridge: () => {} };
|
||||
return { useSettingsStore: hook };
|
||||
});
|
||||
|
||||
vi.mock('@/stores/contact-store', () => {
|
||||
@@ -681,36 +680,6 @@ describe('composer-sidebar and composer-sidebar-right', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('show and hide the panels from the header, the left one at its start and the right one at its end', () => {
|
||||
stubWidth(1400);
|
||||
offerSlots('p1', ['composer-sidebar', 'composer-sidebar-right'], { startFolded: ['composer-sidebar-right'] });
|
||||
render(<EmailComposer initialData={DRAFT} onClose={vi.fn()} />);
|
||||
const header = document.querySelector('[data-composer-header]') as HTMLElement;
|
||||
// As high as the reader's toolbar and the panels' own headers.
|
||||
expect(header.className).toContain('md:h-14');
|
||||
const left = header.querySelector('[data-composer-panel-toggle="left"]') as HTMLElement;
|
||||
const right = header.querySelector('[data-composer-panel-toggle="right"]') as HTMLElement;
|
||||
expect(left).toBeTruthy();
|
||||
expect(right).toBeTruthy();
|
||||
// Left first; right just before Close, at the end.
|
||||
expect(left.compareDocumentPosition(right) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
||||
expect(right.nextElementSibling).toBe(screen.getByTestId('composer-close'));
|
||||
// The panels hold no buttons of their own: no strip, no row above the plugin.
|
||||
expect(panel('right').dataset.composerSidebarMode).toBe('folded');
|
||||
expect(panel('right').hidden).toBe(true);
|
||||
expect(panel('left').querySelector('button')).toBeNull();
|
||||
expect(right.getAttribute('aria-pressed')).toBe('false');
|
||||
|
||||
// Opening by hand asks the panel to take the focus, as a plugin's request does.
|
||||
fireEvent.click(right);
|
||||
expect(panel('right').dataset.composerSidebarMode).toBe('docked');
|
||||
expect(slotProps(screen.getByTestId('slot-composer-sidebar-right')).focusRequest).toBe(1);
|
||||
expect(right.getAttribute('aria-pressed')).toBe('true');
|
||||
expect(right.getAttribute('aria-label')).toBe('plugin_panel_hide');
|
||||
fireEvent.click(right);
|
||||
expect(panel('right').dataset.composerSidebarMode).toBe('folded');
|
||||
});
|
||||
|
||||
it('open a folded panel over the composer when there is no room beside it', () => {
|
||||
stubWidth(600);
|
||||
offerSlots('p1', ['composer-sidebar-right']);
|
||||
|
||||
@@ -15,10 +15,8 @@ import { doVacationGet, doVacationSet } from '../host-api-settings';
|
||||
import { isPlainRecord, sessionClient } from '../host-api-input';
|
||||
import { NOT_MINE } from './not-mine';
|
||||
import { pluginHasPermission } from '../permissions';
|
||||
import { doCalendarDeleteWithUndo, doCalendarRespond, doCalendarUndoDelete } from './c.calendar';
|
||||
import { doFiltersCreate, doFiltersList, doFiltersUndo } from './c.filters';
|
||||
import { doEmailBatch, doEmailUndoBatch } from './c.batch';
|
||||
import { doDescribeSettings, doSetSettings, doUndoSettings } from './c.settings';
|
||||
import type { AreaHostFeaturesC, VacationForwardField } from './c.types';
|
||||
|
||||
/** Method → permission, joined into PERM_PER_METHOD. */
|
||||
@@ -75,12 +73,18 @@ async function doVacationSetGuarded(input: unknown): Promise<VacationView | null
|
||||
return doVacationSet(input);
|
||||
}
|
||||
|
||||
// Loaded on first use: both reach the calendar store, which registers itself
|
||||
// with the settings store on import. host-features imports this file, and
|
||||
// every view that offers plugin slots imports host-features.
|
||||
const calendar = () => import('./c.calendar');
|
||||
const settings = () => import('./c.settings');
|
||||
|
||||
/** Runs a method of this area; NOT_MINE for any other. */
|
||||
export async function dispatch(plugin: InstalledPlugin, method: string, args: unknown[]): Promise<unknown> {
|
||||
switch (method) {
|
||||
case 'calendar.respond': return doCalendarRespond(args[0], args[1]);
|
||||
case 'calendar.deleteEvent': return doCalendarDeleteWithUndo(plugin.id, args[0]);
|
||||
case 'calendar.undoDelete': return doCalendarUndoDelete(plugin.id, args[0]);
|
||||
case 'calendar.respond': return (await calendar()).doCalendarRespond(args[0], args[1]);
|
||||
case 'calendar.deleteEvent': return (await calendar()).doCalendarDeleteWithUndo(plugin.id, args[0]);
|
||||
case 'calendar.undoDelete': return (await calendar()).doCalendarUndoDelete(plugin.id, args[0]);
|
||||
case 'filters.list': return doFiltersList();
|
||||
case 'filters.create': return doFiltersCreate(plugin.id, args[0]);
|
||||
case 'filters.undo': return doFiltersUndo(plugin.id, args[0]);
|
||||
@@ -88,9 +92,9 @@ export async function dispatch(plugin: InstalledPlugin, method: string, args: un
|
||||
case 'email.undoBatch': return doEmailUndoBatch(plugin.id, args[0]);
|
||||
case 'vacation.get': return doVacationGetWithForward();
|
||||
case 'vacation.set': return doVacationSetGuarded(args[0]);
|
||||
case 'app.describeSettings': return doDescribeSettings(args[0]);
|
||||
case 'app.setSettings': return doSetSettings(plugin.id, args[0]);
|
||||
case 'app.undoSettings': return doUndoSettings(plugin.id, args[0]);
|
||||
case 'app.describeSettings': return (await settings()).doDescribeSettings(args[0]);
|
||||
case 'app.setSettings': return (await settings()).doSetSettings(plugin.id, args[0]);
|
||||
case 'app.undoSettings': return (await settings()).doUndoSettings(plugin.id, args[0]);
|
||||
default: return NOT_MINE;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user