mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-30 16:16:19 +00:00
Merge pull request #1358 from illuwa-soft/fix/full-height-app-page
fix: honor full-height app-page slots
This commit is contained in:
@@ -28,8 +28,9 @@ const registry = await import('@/lib/plugin-sandbox/registry');
|
||||
const pages = await import('@/lib/plugin-sandbox/pages');
|
||||
const { useAuthStore } = await import('@/stores/auth-store');
|
||||
const { usePluginStore } = await import('@/stores/plugin-store');
|
||||
const { isFillableSlot } = await import('@/lib/plugin-sandbox/slot-layout');
|
||||
|
||||
function activate(id: string, slots: string[]) {
|
||||
function activate(id: string, slots: string[], fill = false) {
|
||||
const plugin: InstalledPlugin = {
|
||||
id, name: 'Kanban', version: '1.0.0', author: '', description: '', type: 'ui-extension',
|
||||
permissions: ['ui:app-page'], grantedPermissions: ['ui:app-page'], entrypoint: 'index.js',
|
||||
@@ -38,7 +39,7 @@ function activate(id: string, slots: string[]) {
|
||||
registry.register({
|
||||
plugin, code: '', tier: 'untrusted',
|
||||
background: { evaluateShouldShow: vi.fn(async () => true) } as never,
|
||||
slotOffers: slots.map((name) => ({ name: name as never, order: 100, hasShouldShow: false })),
|
||||
slotOffers: slots.map((name) => ({ name: name as never, order: 100, hasShouldShow: false, fill })),
|
||||
hookDisposables: [],
|
||||
});
|
||||
}
|
||||
@@ -67,6 +68,21 @@ describe('plugin pages', () => {
|
||||
expect(opts.slot).toBe('app-page');
|
||||
expect(opts.plugin.id).toBe('kanban');
|
||||
expect(opts.extraProps).toEqual({ pageId: 'board', path: 'cards/42' });
|
||||
expect(createSlotInstance.mock.calls[0][0]).toMatchObject({ fill: false });
|
||||
});
|
||||
|
||||
it('gives an opted-in page the available height through the host and sandbox fill contract', async () => {
|
||||
activate('chat', ['app-page'], true);
|
||||
act(() => pages.setPluginPages('chat', 'Chat', [{ id: 'agent', title: 'Agent' }]));
|
||||
const { container } = render(<PluginPageApp linkSegments={['chat', 'agent']} />);
|
||||
|
||||
await waitFor(() => expect(createSlotInstance).toHaveBeenCalled());
|
||||
expect(createSlotInstance.mock.calls[0][0]).toMatchObject({ slot: 'app-page', fill: true });
|
||||
expect(isFillableSlot('app-page')).toBe(true);
|
||||
expect(isFillableSlot('email-toolbar')).toBe(false);
|
||||
const page = container.querySelector('[data-plugin-page="chat/agent"]');
|
||||
expect(page).toHaveClass('flex', 'flex-col', 'min-h-0', 'overflow-hidden');
|
||||
expect(page?.querySelector('[data-plugin-iframe-fill]')).toHaveStyle({ flex: '1 1 0%', minHeight: 0 });
|
||||
});
|
||||
|
||||
it('passes an empty path for the page itself', async () => {
|
||||
|
||||
@@ -60,6 +60,11 @@ export function PluginPageApp({ linkSegments = [] }: PluginPageAppProps) {
|
||||
() => offersForSlot("app-page").some((o) => o.pluginId === pluginId),
|
||||
() => false,
|
||||
);
|
||||
const fillsPage = useSyncExternalStore(
|
||||
subscribeSlots,
|
||||
() => offersForSlot("app-page").some((o) => o.pluginId === pluginId && o.fill),
|
||||
() => false,
|
||||
);
|
||||
useDocumentTitle(page?.title ?? null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -92,8 +97,8 @@ export function PluginPageApp({ linkSegments = [] }: PluginPageAppProps) {
|
||||
let content: React.ReactNode;
|
||||
if (available) {
|
||||
content = (
|
||||
<div className="flex-1 min-h-0 overflow-y-auto" data-plugin-page={`${pluginId}/${pageId}`}>
|
||||
<PluginIframeSlot key={`${pluginId}/${pageId}`} pluginId={pluginId} slot="app-page" extraProps={extraProps} />
|
||||
<div className={cn("flex-1 min-h-0", fillsPage ? "flex flex-col overflow-hidden" : "overflow-y-auto")} data-plugin-page={`${pluginId}/${pageId}`}>
|
||||
<PluginIframeSlot key={`${pluginId}/${pageId}`} pluginId={pluginId} slot="app-page" extraProps={extraProps} fill={fillsPage} />
|
||||
</div>
|
||||
);
|
||||
} else if (!pluginsReady) {
|
||||
|
||||
@@ -44,7 +44,7 @@ export function clampSlotWidth(width: unknown): number | null {
|
||||
* can pin controls at the bottom, like a chat input. Elsewhere a frame grows
|
||||
* with its content.
|
||||
*/
|
||||
export const FILLABLE_SLOTS: readonly string[] = ['composer-sidebar', 'composer-sidebar-right', 'email-detail-sidebar'];
|
||||
export const FILLABLE_SLOTS: readonly string[] = ['composer-sidebar', 'composer-sidebar-right', 'email-detail-sidebar', 'app-page'];
|
||||
|
||||
export function isFillableSlot(slot: string): boolean {
|
||||
return FILLABLE_SLOTS.includes(slot);
|
||||
|
||||
Reference in New Issue
Block a user