Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

82 changes: 81 additions & 1 deletion packages/compass-collection/src/components/collection-tab.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect } from 'react';
import React, { useCallback, useEffect } from 'react';
import { connect } from 'react-redux';
import { type CollectionState, selectTab } from '../modules/collection-tab';
import { css, ErrorBoundary, TabNavBar } from '@mongodb-js/compass-components';
Expand All @@ -19,6 +19,11 @@ import {
useConnectionSupports,
} from '@mongodb-js/compass-connections/provider';
import { usePreference } from 'compass-preferences-model/provider';
import { useApplicationMenu } from '@mongodb-js/compass-workspaces/application-menu';
import {
useGlobalAppRegistry,
useLocalAppRegistry,
} from '@mongodb-js/compass-app-registry';

type CollectionSubtabTrackingId = Lowercase<CollectionSubtab> extends infer U
? U extends string
Expand Down Expand Up @@ -228,13 +233,88 @@ const CollectionTabWithMetadata: React.FunctionComponent<
);
};

// Setup the Electron application menu for the collection tab
function useCollectionTabApplicationMenu(
collectionMetadata: CollectionMetadata | null
) {
const localAppRegistry = useLocalAppRegistry();
const globalAppRegistry = useGlobalAppRegistry();
const connectionInfoRef = useConnectionInfoRef();
const preferencesReadOnly = usePreference('readOnly');

const shareSchemaClick = useCallback(() => {
localAppRegistry.emit('menu-share-schema-json');
}, [localAppRegistry]);

const importClick = useCallback(() => {
if (!collectionMetadata) return;
globalAppRegistry.emit(
'open-import',
{
namespace: collectionMetadata.namespace,
origin: 'menu',
},
{
connectionId: connectionInfoRef.current.id,
},
{}
);
}, [collectionMetadata, globalAppRegistry, connectionInfoRef]);

const exportClick = useCallback(() => {
if (!collectionMetadata) return;
globalAppRegistry.emit(
'open-export',
{
exportFullCollection: true,
namespace: collectionMetadata.namespace,
origin: 'menu',
},
{
connectionId: connectionInfoRef.current.id,
}
);
}, [collectionMetadata, globalAppRegistry, connectionInfoRef]);

useApplicationMenu({
menu: collectionMetadata
? {
label: '&Collection',
submenu: [
{
label: '&Share Schema as JSON (Legacy)',
accelerator: 'Alt+CmdOrCtrl+S',
click: shareSchemaClick,
},
{
type: 'separator',
},
...(preferencesReadOnly || collectionMetadata?.isReadonly
? []
: [
{
label: '&Import Data',
click: importClick,
},
]),
{
label: '&Export Collection',
click: exportClick,
},
],
}
: undefined,
});
}

const CollectionTab = ({
collectionMetadata,
...props
}: Omit<CollectionTabProps, 'collectionMetadata'> & {
collectionMetadata: CollectionMetadata | null;
}) => {
const QueryBarPlugin = useCollectionQueryBar();
useCollectionTabApplicationMenu(collectionMetadata);

if (!collectionMetadata) {
return null;
Expand Down
7 changes: 5 additions & 2 deletions packages/compass-workspaces/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,13 @@
"compass:main": "src/index.ts",
"exports": {
".": "./dist/index.js",
"./provider": "./dist/provider.js"
"./provider": "./dist/provider.js",
"./application-menu": "./dist/application-menu.js"
},
"compass:exports": {
".": "./src/index.ts",
"./provider": "./src/provider.tsx"
"./provider": "./src/provider.tsx",
"./application-menu": "./src/application-menu.tsx"
},
"types": "./dist/index.d.ts",
"scripts": {
Expand Down Expand Up @@ -81,6 +83,7 @@
"@types/sinon-chai": "^3.2.5",
"chai": "^4.3.6",
"depcheck": "^1.4.1",
"electron": "^37.6.1",
"electron-mocha": "^12.2.0",
"mocha": "^10.2.0",
"nyc": "^15.1.0",
Expand Down
142 changes: 142 additions & 0 deletions packages/compass-workspaces/src/application-menu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import React, { useContext, useEffect } from 'react';
import { createServiceLocator } from '@mongodb-js/compass-app-registry';

// Type-only import in a separate entry point, so this is fine
// compass-peer-deps-ignore
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
import type { MenuItemConstructorOptions } from 'electron';

export type CompassAppMenu<ClickHandlerType = () => void> = Omit<
MenuItemConstructorOptions,
'click' | 'submenu'
> & { click?: ClickHandlerType; submenu?: CompassAppMenu<ClickHandlerType>[] };

export interface ApplicationMenuProvider {
// These functions return 'unsubscribe'-style listeners to remove
// the handlers again
showApplicationMenu(this: void, menu: CompassAppMenu): () => void;
handleMenuRole(
this: void,
role: MenuItemConstructorOptions['role'],
handler: () => void
): () => void;
}

const ApplicationMenuContext = React.createContext<ApplicationMenuProvider>({
showApplicationMenu: () => () => {},
handleMenuRole: () => () => {},
});

export function ApplicationMenuContextProvider({
provider,
children,
}: {
provider: ApplicationMenuProvider;
children: React.ReactNode;
}) {
return (
<ApplicationMenuContext.Provider value={provider}>
{children}
</ApplicationMenuContext.Provider>
);
}

function useApplicationMenuService(): ApplicationMenuProvider {
return useContext(ApplicationMenuContext);
}

export const applicationMenuServiceLocator = createServiceLocator(
useApplicationMenuService,
'applicationMenuServiceLocator'
);

// Shared helper that is useful in a few places since we need to
// translate between 'real function' click handlers and
// string identifiers for those click handlers in a few places.
export function transformAppMenu<T, U>(
menu: CompassAppMenu<T>,
transform: (
cb: Omit<CompassAppMenu<T>, 'submenu'>
) => Omit<CompassAppMenu<U>, 'submenu'>
): CompassAppMenu<U> {
return {
...transform({ ...menu }),
submenu: menu.submenu
? menu.submenu.map((sub) => transformAppMenu(sub, transform))
: undefined,
};
}

const objectIds = new WeakMap<object, number>();
let objectIdCounter = 0;

function getObjectId(obj: object): number {
let id = objectIds.get(obj);
if (id === undefined) {
id = ++objectIdCounter;
objectIds.set(obj, id);
}
return id;
}

// Hook to set up an additional application menu, as well as
// override handlers for pre-defined Electron menu roles.
//
// Example usage:
//
// useApplicationMenu({
// menu: {
// label: '&MyMenu',
// submenu: [
// {
// label: 'Do Something',
// click: () => { ... }
// }
// ]
// },
// roles: {
// undo: () => { ... },
// redo: () => { ... }
// }
// });
//
// You will typically want to memoize the callbacks used in these objects
// since they end up as part of the dependency array for this hook.
export function useApplicationMenu({
menu,
roles,
}: {
menu?: CompassAppMenu;
roles?: Partial<
Record<NonNullable<MenuItemConstructorOptions['role']>, () => void>
>;
}): void {
const { showApplicationMenu, handleMenuRole } = useApplicationMenuService();

useEffect(() => {
const hideMenu = menu && showApplicationMenu(menu);
const subscriptions = Object.entries(roles ?? {}).map(([role, handler]) =>
handleMenuRole(role as MenuItemConstructorOptions['role'], handler)
);

return () => {
hideMenu?.();
for (const unsubscribe of subscriptions) unsubscribe();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
showApplicationMenu,
handleMenuRole,
// eslint-disable-next-line react-hooks/exhaustive-deps
menu
? JSON.stringify(
transformAppMenu(menu, (item) => ({
...item,
click: item.click && getObjectId(item.click),
}))
)
: undefined,
// eslint-disable-next-line react-hooks/exhaustive-deps
roles ? JSON.stringify(Object.values(roles).map(getObjectId)) : undefined,
]);
}
42 changes: 0 additions & 42 deletions packages/compass-workspaces/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,7 @@ import workspacesReducer, {
collectionRemoved,
collectionRenamed,
databaseRemoved,
getActiveTab,
getInitialTabState,
getLocalAppRegistryForTab,
cleanupLocalAppRegistries,
connectionDisconnected,
updateDatabaseInfo,
Expand Down Expand Up @@ -168,46 +166,6 @@ export function activateWorkspacePlugin(
}
);

on(globalAppRegistry, 'menu-share-schema-json', () => {
const activeTab = getActiveTab(store.getState());
if (activeTab?.type === 'Collection') {
getLocalAppRegistryForTab(activeTab.id).emit('menu-share-schema-json');
}
});

on(globalAppRegistry, 'open-active-namespace-export', function () {
const activeTab = getActiveTab(store.getState());
if (activeTab?.type === 'Collection') {
globalAppRegistry.emit(
'open-export',
{
exportFullCollection: true,
namespace: activeTab.namespace,
origin: 'menu',
},
{
connectionId: activeTab.connectionId,
}
);
}
});

on(globalAppRegistry, 'open-active-namespace-import', function () {
const activeTab = getActiveTab(store.getState());
if (activeTab?.type === 'Collection') {
globalAppRegistry.emit(
'open-import',
{
namespace: activeTab.namespace,
origin: 'menu',
},
{
connectionId: activeTab.connectionId,
}
);
}
});

onBeforeUnloadCallbackRequest?.(() => {
return store.dispatch(beforeUnloading());
});
Expand Down
Loading
Loading