Skip to content

feat(workspaces): context menu on tabs to duplicate and close all other tabs COMPASS-9397 #7053

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Jul 2, 2025

Conversation

kraenhansen
Copy link
Contributor

@kraenhansen kraenhansen commented Jun 25, 2025

Description

Stacked on #6956

Merging this PR will:

  • Add two new context menu actions to workspace tabs:
    • duplicate, which will create and activate a tab to the right of the tab
    • close all other tabs, which will ... you've guessed it

Duplicate tab and close all others

duplicate-and-close-other-tabs.mov

Partial closing of tabs

Any tab with unsaved work will confirm the close by getting selected and displaying a confirmation dialog.

partial-closing-tabs.mov

Checklist

  • New tests and/or benchmarks are included
  • Documentation is changed or added
  • If this change updates the UI, screenshots/videos are added and a design review is requested
  • I have signed the MongoDB Contributor License Agreement (https://www.mongodb.com/legal/contributor-agreement)

Motivation and Context

  • Bugfix
  • New feature
  • Dependency update
  • Misc

Open Questions

I feel the tooltip is getting in the way of this interaction and while we could solve this in a bespoke way (ex disabling the tooltip component if the context menu isOpen), we could disable the underlying UI (using ex the pointer-events CSS property) when the menu is in its opened state.

Screenshot 2025-06-25 at 15 51 59

For what it's worth, this match the behavior in Google Sheets:

Screenshot 2025-06-30 at 15 43 17

Dependents

Types of changes

  • Backport Needed
  • Patch (non-breaking change which fixes an issue)
  • Minor (non-breaking change which adds functionality)
  • Major (fix or feature that would cause existing functionality to change)

@github-actions github-actions bot added the feat label Jun 25, 2025
@kraenhansen kraenhansen changed the base branch from main to gagik/context-menu-compass-ui June 25, 2025 13:52
@kraenhansen kraenhansen self-assigned this Jun 27, 2025
dispatch({ type: WorkspacesActions.CloseTab, atIndex });
cleanupLocalAppRegistryForTab(tab?.id);
dispatch({ type: WorkspacesActions.CloseAllOtherTabs, atIndex });
cleanupLocalAppRegistryForTab(tabs[atIndex].id);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You want to cleanup for tabs that you closed, not for the only one that stays open. We usually do this through cleanupRemovedTabs method used in similar cases in this slice

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great catch.

Copy link
Collaborator

@gribnoysup gribnoysup Jun 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I think you caught one too! 😄 The closeTab method is not doing this full cleanup and it probably should, I don't see necessarily anything breaking because of that, but seems like we might be slowly leaking some uncleaned data, maybe you can drive-by address this? Good if not, we can follow-up quickly

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pushed a change to call cleanupRemovedTabs in closeTab as well 👍

const { tabs } = getState();
const otherTabs = tabs.filter((_, index) => index !== atIndex);
for (const tab of otherTabs) {
if (!canCloseTab(tab) && !(await confirmClosingTabs())) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't feel right that canceling close for one tab stops all the others from closing too (or the other way around, you say "yes" for one, then "no" for other, and all stay open), in vscode you'd be asked for evey one separately and they are closed based on your answers. Wouldn't that make more sense for our flow too?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also now that we have this case of confirmation being shown for multiple tabs, it would probably make sense to add something to this confirmation model to make identification easier. "Are you sure you want to close the tab?" reads fine if you actively clicked a close button on a certain tab, but not when you are getting this message as a side effect of another action with no context what exactly will be closed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can make sure only the tabs which are confirmed will close 👍 Getting to the title of the tab turns out very difficulty for me, as this is controlled by the plugin ultimately rendering the tab. I suggest selecting the tab just before confirming, to make the dialog display in context of the tab being closed.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, good point, makes sense! This seems to match what VSCode is doing also

@kraenhansen kraenhansen force-pushed the kh/tab-context-menu branch from c0ad255 to 69a787d Compare June 30, 2025 11:10
@kraenhansen kraenhansen changed the title feat(workspaces): context menu on tabs to duplicate and close all other tabs feat(workspaces): context menu on tabs to duplicate and close all other tabs COMPASS-9397 Jun 30, 2025
@kraenhansen kraenhansen force-pushed the kh/tab-context-menu branch from 4460015 to 6c4c97a Compare June 30, 2025 13:33
@kraenhansen
Copy link
Contributor Author

@gribnoysup I'm curious if you would add "release notes" or "no release notes" to this? It seems a bit niece but then again it does introduce new functionality that might be hard to discover.

@gribnoysup
Copy link
Collaborator

Ah, a good question! Honestly I feel like it might be worth manually editing release notes at the point we're publishing this to highlight this change in conjunction with other related ones and just have a big mention of all newly available context menus as this is a long requested Compass feature. Having release notes label is already pretty good not to lose track of this in the release notes 👍

@gribnoysup
Copy link
Collaborator

For just some random context I think generally speaking for "bigger" features we usually have them behind a flag and so a PR that would enable a feature by default will be the one we'd include in the notes (hence the special feature flagged label for everything else), but it's just not the case with this one

@kraenhansen kraenhansen marked this pull request as ready for review June 30, 2025 14:16
@kraenhansen kraenhansen requested a review from a team as a code owner June 30, 2025 14:16
@kraenhansen kraenhansen requested review from gribnoysup and gagik June 30, 2025 15:54
Copy link
Collaborator

@gribnoysup gribnoysup left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two notes, otherwise lgtm

Comment on lines 904 to 920
const tabsToClose = await tabs.reduce(
async (prev: Promise<WorkspaceTab[]>, tab, tabIndex) => {
const tabsToClose = await prev;
if (tabIndex === atIndex) {
return tabsToClose; // Skip the tab which is not being closed
}
if (!canCloseTab(tab)) {
// Select the closing tab - to show the confirmation dialog in context
dispatch({ type: WorkspacesActions.SelectTab, atIndex: tabIndex });
if (!(await confirmClosingTab())) {
return tabsToClose; // Skip this tab
}
}
return [...tabsToClose, tab];
},
Promise.resolve([])
);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I found it really hard to read through this reducer with the promise being an accumulator that always needs to be awaited, I think a for of loop reads easier, but feel free to ignore this suggestion

Suggested change
const tabsToClose = await tabs.reduce(
async (prev: Promise<WorkspaceTab[]>, tab, tabIndex) => {
const tabsToClose = await prev;
if (tabIndex === atIndex) {
return tabsToClose; // Skip the tab which is not being closed
}
if (!canCloseTab(tab)) {
// Select the closing tab - to show the confirmation dialog in context
dispatch({ type: WorkspacesActions.SelectTab, atIndex: tabIndex });
if (!(await confirmClosingTab())) {
return tabsToClose; // Skip this tab
}
}
return [...tabsToClose, tab];
},
Promise.resolve([])
);
const tabsToClose = [];
for (const [tabIndex, tab] of tabs.entries()) {
if (tabIndex !== atIndex && !canCloseTab(tab)) {
// Select the closing tab - to show the confirmation dialog in context
dispatch({ type: WorkspacesActions.SelectTab, atIndex: tabIndex });
if (await confirmClosingTab()) {
tabsToClose.push(tab);
}
}
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right - a simple loop reads much better. Thanks!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've pushed an update using a for loop. I needed to change it slightly to accommodate for tabs to which canCloseTab returned true.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, sorry, I sketched the code, but forgot to double-check it when making a suggestion, thanks for refactoring, looks good to me!

}
if (!canCloseTab(tab)) {
// Select the closing tab - to show the confirmation dialog in context
dispatch({ type: WorkspacesActions.SelectTab, atIndex: tabIndex });
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UI nit: as discussed, it makes sense to switch to the tab that's being closed during the process, but I found it a bit confusing that if you skip closing some of the tabs, you're not being returned back to the tab that you started with

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added an optional activeTabId to the CloseTabs action.

Base automatically changed from gagik/context-menu-compass-ui to main July 1, 2025 14:23
@kraenhansen kraenhansen force-pushed the kh/tab-context-menu branch from 0822ce8 to 6a77ddf Compare July 2, 2025 07:05
@kraenhansen kraenhansen requested a review from gagik July 2, 2025 07:07
@kraenhansen kraenhansen merged commit 7ee186c into main Jul 2, 2025
53 of 56 checks passed
@kraenhansen kraenhansen deleted the kh/tab-context-menu branch July 2, 2025 09:12
@kraenhansen
Copy link
Contributor Author

The failures are unrelated to these changes.

@lerouxb lerouxb added feature flagged PRs labeled with this label will not be included in the release notes of the next release and removed release notes labels Jul 17, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feat feature flagged PRs labeled with this label will not be included in the release notes of the next release
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants