Skip to content

Commit e846192

Browse files
committed
feat: enhance docks-tabs to support single-tabs and hide-tabs
1 parent 52ae72e commit e846192

4 files changed

Lines changed: 96 additions & 13 deletions

File tree

packages/core/src/commands/open-view-as-editor.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,21 @@ registerAll({
1111
description: "Opens a dashboard view in the editor area",
1212
parameters: [
1313
{ name: "name", description: "View contribution name", required: true },
14-
{ name: "sourceContributionSlot", description: "source contribution slot (default: SYSTEM_VIEWS)", required: false }
14+
{ name: "sourceContributionSlot", description: "source contribution slot (default: SYSTEM_VIEWS)", required: false },
15+
{ name: "singleTab", description: "If true, close all other editor tabs first so only this view remains open", required: false }
1516
]
1617
},
1718
handler: {
18-
execute: async ({ params }: { params?: { name?: string, sourceContributionSlot?: string } }) => {
19+
execute: async ({ params }: { params?: { name?: string, sourceContributionSlot?: string, singleTab?: boolean } }) => {
1920
const name = params?.name;
2021
if (!name) return;
2122
const slot = params?.sourceContributionSlot ?? SYSTEM_VIEWS
2223
const contributions = contributionRegistry.getContributions(slot) as TabContribution[];
2324
const contribution = contributions.find(c => c.name === name);
2425
if (!contribution?.component) return;
25-
await editorRegistry.openTab(contribution as TabContribution);
26+
await editorRegistry.openTab(contribution as TabContribution, {
27+
singleTab: params?.singleTab === true,
28+
});
2629
}
2730
}
2831
});

packages/core/src/core/editorregistry.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,14 +226,21 @@ class EditorRegistry {
226226
} as TabContribution)
227227
}
228228

229-
async openTab(tabContribution: TabContribution) {
229+
async openTab(tabContribution: TabContribution, options?: { singleTab?: boolean }) {
230230
const editorArea = this.getEditorArea();
231231

232232
if (!editorArea) {
233233
console.error("Editor area not found. The split pane system may not be initialized yet.");
234234
return;
235235
}
236236

237+
if (options?.singleTab) {
238+
const closed = await editorArea.closeAllTabs();
239+
if (!closed) return;
240+
editorArea.open(tabContribution);
241+
return;
242+
}
243+
237244
if (editorArea.has(tabContribution.name)) {
238245
editorArea.activate(tabContribution.name)
239246
return

packages/core/src/parts/tabs.ts

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ export class DocksTabs extends DocksContainer {
4141
@property({type: Boolean, reflect: true, attribute: 'with-toolbar'})
4242
withToolbar: boolean = false;
4343

44+
@property({ type: Boolean, reflect: true, attribute: 'hide-tabs' })
45+
hideTabs: boolean = false;
46+
4447
/** Tab contributions for this container */
4548
@state()
4649
private contributions: TabContribution[] = [];
@@ -152,17 +155,37 @@ export class DocksTabs extends DocksContainer {
152155

153156
async closeTab(event: Event, tabName: string): Promise<void> {
154157
event.stopPropagation();
155-
156-
if (this.isDirty(tabName) && !await confirmDialog("Unsaved changes will be lost: Do you really want to close?")) {
157-
return;
158+
await this.tryCloseTab(tabName, true);
159+
}
160+
161+
/**
162+
* Closes every tab in this group. Returns false if the user cancels a dirty-tab prompt.
163+
*/
164+
async closeAllTabs(): Promise<boolean> {
165+
const names = [...this.contributions.map((c) => c.name)];
166+
for (const tabName of names) {
167+
const ok = await this.tryCloseTab(tabName, true);
168+
if (!ok) return false;
158169
}
159-
170+
return true;
171+
}
172+
173+
/**
174+
* @returns false if the user cancelled closing a dirty tab; true if the tab was removed or was absent.
175+
*/
176+
private async tryCloseTab(tabName: string, confirmIfDirty: boolean): Promise<boolean> {
177+
if (confirmIfDirty && this.isDirty(tabName)) {
178+
if (!await confirmDialog("Unsaved changes will be lost: Do you really want to close?")) {
179+
return false;
180+
}
181+
}
182+
160183
const tabPanel = this.getTabPanel(tabName);
161-
if (!tabPanel) return;
162-
184+
if (!tabPanel) return true;
185+
163186
const contribution = this.contributions.find(c => c.name === tabName);
164-
if (!contribution) return;
165-
187+
if (!contribution) return true;
188+
166189
this.cleanupTabInstance(tabPanel);
167190
this.clearActiveSignalsIfPartInPanel(tabPanel);
168191

@@ -172,10 +195,11 @@ export class DocksTabs extends DocksContainer {
172195
}
173196

174197
this.requestUpdate();
175-
198+
176199
this.updateComplete.then(() => {
177200
this.activateNextAvailableTab();
178201
});
202+
return true;
179203
}
180204

181205
markDirty(name: string, dirty: boolean): void {
@@ -421,6 +445,14 @@ export class DocksTabs extends DocksContainer {
421445
min-height: 0;
422446
}
423447
448+
:host([hide-tabs]) wa-tab {
449+
display: none !important;
450+
}
451+
452+
:host([hide-tabs]:not([with-toolbar])) wa-tab-group::part(nav) {
453+
display: none;
454+
}
455+
424456
:host(:is([placement="top"], [placement="bottom"])) wa-tab-group::part(base) {
425457
display: grid;
426458
grid-template-rows: auto minmax(0, 1fr);

packages/core/test/units/editorregistry.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,20 +102,61 @@ describe('editorregistry', () => {
102102
it('openTab activates existing tab or opens new one', async () => {
103103
const activate = vi.fn();
104104
const open = vi.fn();
105+
const closeAllTabs = vi.fn(async () => true);
105106
(globalThis as any).document = {
106107
querySelector: vi.fn(() => ({
107108
has: (name: string) => name === 'existing',
108109
activate,
109110
open,
111+
closeAllTabs,
110112
})),
111113
};
112114

113115
const { editorRegistry } = await import('../../src/core/editorregistry');
114116
await editorRegistry.openTab({ name: 'existing', label: 'Existing' } as any);
115117
expect(activate).toHaveBeenCalledWith('existing');
116118
expect(open).not.toHaveBeenCalled();
119+
expect(closeAllTabs).not.toHaveBeenCalled();
117120

118121
await editorRegistry.openTab({ name: 'new-one', label: 'New One' } as any);
119122
expect(open).toHaveBeenCalledWith(expect.objectContaining({ name: 'new-one' }));
120123
});
124+
125+
it('openTab with singleTab closes all then opens', async () => {
126+
const activate = vi.fn();
127+
const open = vi.fn();
128+
const closeAllTabs = vi.fn(async () => true);
129+
(globalThis as any).document = {
130+
querySelector: vi.fn(() => ({
131+
has: vi.fn(() => false),
132+
activate,
133+
open,
134+
closeAllTabs,
135+
})),
136+
};
137+
138+
const { editorRegistry } = await import('../../src/core/editorregistry');
139+
await editorRegistry.openTab({ name: 'only', label: 'Only' } as any, { singleTab: true });
140+
expect(closeAllTabs).toHaveBeenCalledOnce();
141+
expect(open).toHaveBeenCalledWith(expect.objectContaining({ name: 'only' }));
142+
expect(activate).not.toHaveBeenCalled();
143+
});
144+
145+
it('openTab with singleTab aborts when closeAllTabs returns false', async () => {
146+
const open = vi.fn();
147+
const closeAllTabs = vi.fn(async () => false);
148+
(globalThis as any).document = {
149+
querySelector: vi.fn(() => ({
150+
has: vi.fn(() => false),
151+
activate: vi.fn(),
152+
open,
153+
closeAllTabs,
154+
})),
155+
};
156+
157+
const { editorRegistry } = await import('../../src/core/editorregistry');
158+
await editorRegistry.openTab({ name: 'x', label: 'X' } as any, { singleTab: true });
159+
expect(closeAllTabs).toHaveBeenCalledOnce();
160+
expect(open).not.toHaveBeenCalled();
161+
});
121162
});

0 commit comments

Comments
 (0)