diff --git a/src/commandsAndMenu.tsx b/src/commandsAndMenu.tsx index e2920d18d..a7ef3e778 100644 --- a/src/commandsAndMenu.tsx +++ b/src/commandsAndMenu.tsx @@ -24,6 +24,7 @@ import { ContextMenu, DockPanel, Menu, Panel, Widget } from '@lumino/widgets'; import * as React from 'react'; import { CancelledError } from './cancelledError'; import { BranchPicker } from './components/BranchPicker'; +import { NewTagDialogBox } from './components/NewTagDialog'; import { DiffModel } from './components/diff/model'; import { createPlainTextDiff } from './components/diff/PlainTextDiff'; import { PreviewMainAreaWidget } from './components/diff/PreviewMainAreaWidget'; @@ -39,7 +40,8 @@ import { gitIcon, historyIcon, openIcon, - removeIcon + removeIcon, + tagIcon } from './style/icons'; import { CommandIDs, @@ -102,6 +104,9 @@ export namespace CommandArguments { export interface IGitContextAction { files: Git.IStatusFile[]; } + export interface IGitCommitInfo { + commit: Git.ISingleCommitInfo; + } } function pluralizedContextLabel(singular: string, plural: string) { @@ -1537,6 +1542,79 @@ export function addCommands( isEnabled: () => false, execute: () => void 0 }); + + commands.addCommand(ContextCommandIDs.gitTagAdd, { + label: trans.__('Add Tag'), + caption: trans.__('Add tag pointing to selected commit'), + execute: async args => { + const commit = args as any as CommandArguments.IGitCommitInfo; + + const widgetId = 'git-dialog-AddTag'; + let anchor = document.querySelector(`#${widgetId}`); + if (!anchor) { + anchor = document.createElement('div'); + anchor.id = widgetId; + document.body.appendChild(anchor); + } + + const tagDialog = true; + const isSingleCommit = true; + + const waitForDialog = new PromiseDelegate(); + const dialog = ReactWidget.create( + { + dialog.dispose(); + waitForDialog.resolve(tagName ?? null); + }} + isSingleCommit={isSingleCommit} + /> + ); + + Widget.attach(dialog, anchor); + + const tagName = await waitForDialog.promise; + + if (tagName) { + logger.log({ + level: Level.RUNNING, + message: trans.__( + "Create tag pointing to '%1'...", + commit.commit.commit_msg + ) + }); + try { + await gitModel.setTag(tagName, commit.commit.commit); + } catch (err) { + logger.log({ + level: Level.ERROR, + message: trans.__( + "Failed to create tag '%1' poining to '%2'.", + tagName, + commit + ), + error: err as Error + }); + return; + } + + logger.log({ + level: Level.SUCCESS, + message: trans.__( + "Created tag '%1' pointing to '%2'.", + tagName, + commit + ) + }); + } + }, + icon: tagIcon.bindprops({ stylesheet: 'menuItem' }) + }); } /** @@ -1639,6 +1717,21 @@ export function addMenuItems( }); } +export function addHistoryMenuItems( + commands: ContextCommandIDs[], + contextMenu: Menu, + selectedCommit: Git.ISingleCommitInfo +): void { + commands.forEach(command => { + contextMenu.addItem({ + command, + args: { + commit: selectedCommit + } as CommandArguments.IGitCommitInfo as any + }); + }); +} + /** * Populate Git context submenu depending on the selected files. */ diff --git a/src/components/HistorySideBar.tsx b/src/components/HistorySideBar.tsx index c633f7f2e..5199b3fda 100644 --- a/src/components/HistorySideBar.tsx +++ b/src/components/HistorySideBar.tsx @@ -1,6 +1,8 @@ import { TranslationBundle } from '@jupyterlab/translation'; import { closeIcon } from '@jupyterlab/ui-components'; import { CommandRegistry } from '@lumino/commands'; +import { Menu } from '@lumino/widgets'; +import { addHistoryMenuItems } from '../commandsAndMenu'; import * as React from 'react'; import { GitExtension } from '../model'; import { hiddenButtonStyle } from '../style/ActionButtonStyle'; @@ -10,7 +12,7 @@ import { selectedHistoryFileStyle, historySideBarWrapperStyle } from '../style/HistorySideBarStyle'; -import { Git } from '../tokens'; +import { ContextCommandIDs, Git } from '../tokens'; import { openFileDiff } from '../utils'; import { ActionButton } from './ActionButton'; import { FileItem } from './FileItem'; @@ -79,6 +81,8 @@ export interface IHistorySideBarProps { ) => (event: React.MouseEvent) => Promise; } +export const CONTEXT_COMMANDS = [ContextCommandIDs.gitTagAdd]; + /** * Returns a React component for displaying commit history. * @@ -145,6 +149,25 @@ export const HistorySideBar: React.FunctionComponent = ( return () => resizeObserver.disconnect(); }, [props.commits]); + /** + * Open the context menu on the advanced view + * + * @param selectedCommit The commit on which the context menu is opened + * @param event The click event + */ + const openContextMenu = ( + selectedCommit: Git.ISingleCommitInfo, + event: React.MouseEvent + ): void => { + event.preventDefault(); + + const contextMenu = new Menu({ commands: props.commands }); + const commands = [ContextCommandIDs.gitTagAdd]; + addHistoryMenuItems(commands, contextMenu, selectedCommit); + + contextMenu.open(event.clientX, event.clientY); + }; + return (
{!props.model.selectedHistoryFile && ( @@ -232,6 +255,7 @@ export const HistorySideBar: React.FunctionComponent = ( } }) } + contextMenu={openContextMenu} > {!props.model.selectedHistoryFile && ( >; + + /** + * Dialog box open from the context menu. + */ + isSingleCommit: boolean; } /** @@ -185,6 +195,10 @@ export const DialogBoxCommitGraph: React.FunctionComponent< isFilter = true; } + if (props.isSingleCommit === true) { + isFilter = false; + } + return (
{isFilter && ( @@ -427,27 +441,29 @@ export const NewTagDialogBox: React.FunctionComponent = ( title={props.trans.__('Enter a tag name')} />

{props.trans.__('Create tag pointing to…')}

-
-
- - {filterState ? ( - - ) : null} + {props.isSingleCommit ? null : ( +
+
+ + {filterState ? ( + + ) : null} +
-
+ )} { = ( filter={filterState} baseCommitId={baseCommitIdState} updateBaseCommitId={setBaseCommitIdState} + isSingleCommit={props.isSingleCommit} /> }
diff --git a/src/components/PastCommitNode.tsx b/src/components/PastCommitNode.tsx index 5183dbf0f..60c3977ac 100644 --- a/src/components/PastCommitNode.tsx +++ b/src/components/PastCommitNode.tsx @@ -118,6 +118,14 @@ export interface IPastCommitNodeProps { * @param el the
  • element representing a past commit */ setRef: (el: HTMLLIElement) => void; + + /** + * Callback to open a context menu on the commit + */ + contextMenu?: ( + commit: Git.ISingleCommitInfo, + event: React.MouseEvent + ) => void; } /** @@ -162,6 +170,12 @@ export class PastCommitNode extends React.Component< : this.props.trans.__('View file changes') } onClick={event => this._onCommitClick(event, this.props.commit.commit)} + onContextMenu={ + this.props.contextMenu && + (event => { + this.props.contextMenu(this.props.commit, event); + }) + } >
    diff --git a/src/components/TagMenu.tsx b/src/components/TagMenu.tsx index 3972c354a..c804dcb20 100644 --- a/src/components/TagMenu.tsx +++ b/src/components/TagMenu.tsx @@ -265,6 +265,7 @@ export class TagMenu extends React.Component { * @returns React element */ private _renderNewTagDialog(): React.ReactElement { + const isSingleCommit = false; return ( { trans={this.props.trans} open={this.state.tagDialog} onClose={this._onNewTagDialogClose} + isSingleCommit={isSingleCommit} /> ); } diff --git a/src/tokens.ts b/src/tokens.ts index 467d4fd96..f662c0a2a 100644 --- a/src/tokens.ts +++ b/src/tokens.ts @@ -1373,7 +1373,8 @@ export enum ContextCommandIDs { gitIgnoreExtension = 'git:context-ignoreExtension', gitNoAction = 'git:no-action', openFileFromDiff = 'git:open-file-from-diff', - gitFileStashPop = 'git:context-stash-pop' + gitFileStashPop = 'git:context-stash-pop', + gitTagAdd = 'git:context-tag-add' } /** diff --git a/ui-tests/tests/add-tag.spec.ts b/ui-tests/tests/add-tag.spec.ts new file mode 100644 index 000000000..dce44754f --- /dev/null +++ b/ui-tests/tests/add-tag.spec.ts @@ -0,0 +1,73 @@ +import { expect, galata, test } from '@jupyterlab/galata'; +import path from 'path'; +import { extractFile } from './utils'; + +const baseRepositoryPath = 'test-repository-dirty.tar.gz'; +test.use({ autoGoto: false, mockSettings: galata.DEFAULT_SETTINGS }); + +test.describe('Add tag', () => { + test.beforeEach(async ({ baseURL, page, tmpPath }) => { + await extractFile( + baseURL, + path.resolve(__dirname, 'data', baseRepositoryPath), + path.join(tmpPath, 'repository.tar.gz') + ); + + // URL for merge conflict example repository + await page.goto(`tree/${tmpPath}/test-repository`); + + await page.sidebar.openTab('jp-git-sessions'); + }); + + test('should show Add Tag command on commit from history sidebar', async ({ + page + }) => { + await page.click('button:has-text("History")'); + + const commits = page.locator('li[title="View commit details"]'); + + expect(await commits.count()).toBeGreaterThanOrEqual(2); + + // Right click the first commit to open the context menu, with the add tag command + await page.getByText('master changes').click({ button: 'right' }); + + expect(await page.getByRole('menuitem', { name: 'Add Tag' })).toBeTruthy(); + }); + + test('should open new tag dialog box', async ({ page }) => { + await page.click('button:has-text("History")'); + + const commits = page.locator('li[title="View commit details"]'); + + expect(await commits.count()).toBeGreaterThanOrEqual(2); + + // Right click the first commit to open the context menu, with the add tag command + await page.getByText('master changes').click({ button: 'right' }); + + // Click on the add tag command + await page.getByRole('menuitem', { name: 'Add Tag' }).click(); + + expect(page.getByText('Create a Tag')).toBeTruthy(); + }); + + test('should create new tag pointing to selected commit', async ({ + page + }) => { + await page.click('button:has-text("History")'); + + const commits = page.locator('li[title="View commit details"]'); + expect(await commits.count()).toBeGreaterThanOrEqual(2); + + // Right click the first commit to open the context menu, with the add tag command + await page.getByText('master changes').click({ button: 'right' }); + + // Click on the add tag command + await page.getByRole('menuitem', { name: 'Add Tag' }).click(); + + // Create a test tag + await page.getByRole('textbox').fill('testTag'); + await page.getByRole('button', { name: 'Create Tag' }).click(); + + expect(await page.getByText('testTag')).toBeTruthy(); + }); +});