Merge pull request #1358 from illuwa-soft/fix/full-height-app-page

fix: honor full-height app-page slots
This commit is contained in:
Linus Rath
2026-10-29 20:27:00 +01:00
committed by GitHub
3 changed files with 26 additions and 5 deletions
@@ -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 () => {
+7 -2
View File
@@ -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) {
+1 -1
View File
@@ -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);