diff --git a/devtools/client/debugger/src/components/PrimaryPanes/SourcesTreeItem.js b/devtools/client/debugger/src/components/PrimaryPanes/SourcesTreeItem.js index 573d78973cec..8a920e00ff7f 100644 --- a/devtools/client/debugger/src/components/PrimaryPanes/SourcesTreeItem.js +++ b/devtools/client/debugger/src/components/PrimaryPanes/SourcesTreeItem.js @@ -19,6 +19,7 @@ import { hasPrettySource as checkHasPrettySource, getContext, getMainThread, + getSourceContent, } from "../../selectors"; import actions from "../../actions"; @@ -31,9 +32,16 @@ import { import { isDirectory } from "../../utils/sources-tree"; import { copyToTheClipboard } from "../../utils/clipboard"; import { features } from "../../utils/prefs"; +import { downloadFile } from "../../utils/utils"; import type { TreeNode } from "../../utils/sources-tree/types"; -import type { Source, Context, MainThread, Thread } from "../../types"; +import type { + Source, + Context, + MainThread, + Thread, + SourceContent, +} from "../../types"; type Props = { autoExpand: ?boolean, @@ -42,6 +50,7 @@ type Props = { projectRoot: string, source: ?Source, item: TreeNode, + sourceContent: SourceContent, depth: number, focused: boolean, expanded: boolean, @@ -56,6 +65,7 @@ type Props = { clearProjectDirectoryRoot: typeof actions.clearProjectDirectoryRoot, setProjectDirectoryRoot: typeof actions.setProjectDirectoryRoot, toggleBlackBox: typeof actions.toggleBlackBox, + loadSourceText: typeof actions.loadSourceText, }; type State = {}; @@ -122,7 +132,14 @@ class SourceTreeItem extends Component { disabled: !shouldBlackbox(source), click: () => this.props.toggleBlackBox(cx, source), }; - menuOptions.push(copySourceUri2, blackBoxMenuItem); + const downloadFileItem = { + id: "node-menu-download-file", + label: L10N.getStr("downloadFile.label"), + accesskey: L10N.getStr("downloadFile.accesskey"), + disabled: false, + click: () => this.handleDownloadFile(cx, source, item), + }; + menuOptions.push(copySourceUri2, blackBoxMenuItem, downloadFileItem); } } } @@ -156,6 +173,15 @@ class SourceTreeItem extends Component { showMenu(event, menuOptions); }; + handleDownloadFile = async (cx: Context, source: ?Source, item: TreeNode) => { + const name = item.name; + if (!this.props.sourceContent) { + await this.props.loadSourceText({ cx, source }); + } + const data = this.props.sourceContent; + downloadFile(data, name); + }; + addCollapseExpandAllOptions = (menuOptions: ContextMenu, item: TreeNode) => { const { setExpanded } = this.props; @@ -310,6 +336,11 @@ function getHasMatchingGeneratedSource(state, source: ?Source) { return !!getGeneratedSourceByURL(state, source.url); } +function getSourceContentValue(state, source: Source) { + const content = getSourceContent(state, source.id); + return content !== null ? content.value : false; +} + const mapStateToProps = (state, props) => { const { source } = props; return { @@ -318,6 +349,7 @@ const mapStateToProps = (state, props) => { hasMatchingGeneratedSource: getHasMatchingGeneratedSource(state, source), hasSiblingOfSameName: getHasSiblingOfSameName(state, source), hasPrettySource: source ? checkHasPrettySource(state, source.id) : false, + sourceContent: source ? getSourceContentValue(state, source) : false, }; }; @@ -327,5 +359,6 @@ export default connect( setProjectDirectoryRoot: actions.setProjectDirectoryRoot, clearProjectDirectoryRoot: actions.clearProjectDirectoryRoot, toggleBlackBox: actions.toggleBlackBox, + loadSourceText: actions.loadSourceText, } )(SourceTreeItem); diff --git a/devtools/client/debugger/src/components/PrimaryPanes/tests/SourcesTreeItem.spec.js b/devtools/client/debugger/src/components/PrimaryPanes/tests/SourcesTreeItem.spec.js index 643943b66163..9e33b7ae7906 100644 --- a/devtools/client/debugger/src/components/PrimaryPanes/tests/SourcesTreeItem.spec.js +++ b/devtools/client/debugger/src/components/PrimaryPanes/tests/SourcesTreeItem.spec.js @@ -91,6 +91,13 @@ describe("SourceTreeItem", () => { id: "node-menu-blackbox", label: "Blackbox source", }, + { + accesskey: "d", + click: expect.any(Function), + disabled: false, + id: "node-menu-download-file", + label: "Download file", + }, ]; const mockEvent = { preventDefault: jest.fn(), @@ -104,7 +111,6 @@ describe("SourceTreeItem", () => { await instance.onContextMenu(mockEvent, item, source); expect(showMenu).toHaveBeenCalledWith(mockEvent, menuOptions); - expect(mockEvent.preventDefault).toHaveBeenCalled(); expect(mockEvent.stopPropagation).toHaveBeenCalled(); @@ -130,6 +136,13 @@ describe("SourceTreeItem", () => { id: "node-menu-blackbox", label: "Blackbox source", }, + { + accesskey: "d", + click: expect.any(Function), + disabled: false, + id: "node-menu-download-file", + label: "Download file", + }, ]; const mockEvent = { preventDefault: jest.fn(), @@ -153,6 +166,57 @@ describe("SourceTreeItem", () => { expect(props.toggleBlackBox).toHaveBeenCalled(); }); + it("shows context menu on file to download source file", async () => { + const menuOptions = [ + { + accesskey: "u", + click: expect.any(Function), + disabled: false, + id: "node-menu-copy-source", + label: "Copy source URI", + }, + { + accesskey: "B", + click: expect.any(Function), + disabled: false, + id: "node-menu-blackbox", + label: "Blackbox source", + }, + { + accesskey: "d", + click: expect.any(Function), + disabled: false, + id: "node-menu-download-file", + label: "Download file", + }, + ]; + const mockEvent = { + preventDefault: jest.fn(), + stopPropagation: jest.fn(), + }; + + const { props, instance } = render({ + projectRoot: "root/", + }); + const { item, source } = instance.props; + + instance.handleDownloadFile = jest.fn(() => {}); + + await instance.onContextMenu(mockEvent, item, source); + + expect(showMenu).toHaveBeenCalledWith(mockEvent, menuOptions); + + expect(mockEvent.preventDefault).toHaveBeenCalled(); + expect(mockEvent.stopPropagation).toHaveBeenCalled(); + + showMenu.mock.calls[0][1][2].click(); + expect(props.setProjectDirectoryRoot).not.toHaveBeenCalled(); + expect(props.clearProjectDirectoryRoot).not.toHaveBeenCalled(); + expect(props.toggleBlackBox).not.toHaveBeenCalled(); + expect(copyToTheClipboard).not.toHaveBeenCalled(); + expect(instance.handleDownloadFile).toHaveBeenCalled(); + }); + it("shows context menu on root to remove directory root", async () => { const menuOptions = [ { diff --git a/devtools/client/debugger/src/components/PrimaryPanes/tests/__snapshots__/SourcesTreeItem.spec.js.snap b/devtools/client/debugger/src/components/PrimaryPanes/tests/__snapshots__/SourcesTreeItem.spec.js.snap index 0e8ca3ab3389..cddbb115169a 100644 --- a/devtools/client/debugger/src/components/PrimaryPanes/tests/__snapshots__/SourcesTreeItem.spec.js.snap +++ b/devtools/client/debugger/src/components/PrimaryPanes/tests/__snapshots__/SourcesTreeItem.spec.js.snap @@ -38,6 +38,7 @@ Object { "instance": SourceTreeItem { "addCollapseExpandAllOptions": [Function], "context": Object {}, + "handleDownloadFile": [Function], "onClick": [Function], "onContextMenu": [Function], "props": Object { @@ -263,6 +264,7 @@ Object { "instance": SourceTreeItem { "addCollapseExpandAllOptions": [Function], "context": Object {}, + "handleDownloadFile": [Function], "onClick": [Function], "onContextMenu": [Function], "props": Object { @@ -494,6 +496,7 @@ Object { "instance": SourceTreeItem { "addCollapseExpandAllOptions": [Function], "context": Object {}, + "handleDownloadFile": [Function], "onClick": [Function], "onContextMenu": [Function], "props": Object { @@ -708,6 +711,7 @@ Object { "instance": SourceTreeItem { "addCollapseExpandAllOptions": [Function], "context": Object {}, + "handleDownloadFile": [Function], "onClick": [Function], "onContextMenu": [Function], "props": Object { @@ -880,6 +884,7 @@ Object { "instance": SourceTreeItem { "addCollapseExpandAllOptions": [Function], "context": Object {}, + "handleDownloadFile": [Function], "onClick": [Function], "onContextMenu": [Function], "props": Object { @@ -1055,6 +1060,7 @@ Object { "instance": SourceTreeItem { "addCollapseExpandAllOptions": [Function], "context": Object {}, + "handleDownloadFile": [Function], "onClick": [Function], "onContextMenu": [Function], "props": Object { @@ -1198,6 +1204,7 @@ Object { "instance": SourceTreeItem { "addCollapseExpandAllOptions": [Function], "context": Object {}, + "handleDownloadFile": [Function], "onClick": [Function], "onContextMenu": [Function], "props": Object { @@ -1367,6 +1374,7 @@ Object { "instance": SourceTreeItem { "addCollapseExpandAllOptions": [Function], "context": Object {}, + "handleDownloadFile": [Function], "onClick": [Function], "onContextMenu": [Function], "props": Object { @@ -1504,6 +1512,7 @@ Object { "instance": SourceTreeItem { "addCollapseExpandAllOptions": [Function], "context": Object {}, + "handleDownloadFile": [Function], "onClick": [Function], "onContextMenu": [Function], "props": Object { @@ -1647,6 +1656,7 @@ Object { "instance": SourceTreeItem { "addCollapseExpandAllOptions": [Function], "context": Object {}, + "handleDownloadFile": [Function], "onClick": [Function], "onContextMenu": [Function], "props": Object { @@ -1819,6 +1829,7 @@ Object { "instance": SourceTreeItem { "addCollapseExpandAllOptions": [Function], "context": Object {}, + "handleDownloadFile": [Function], "onClick": [Function], "onContextMenu": [Function], "props": Object { @@ -2000,6 +2011,7 @@ Object { "instance": SourceTreeItem { "addCollapseExpandAllOptions": [Function], "context": Object {}, + "handleDownloadFile": [Function], "onClick": [Function], "onContextMenu": [Function], "props": Object { @@ -2223,6 +2235,7 @@ Object { "instance": SourceTreeItem { "addCollapseExpandAllOptions": [Function], "context": Object {}, + "handleDownloadFile": [Function], "onClick": [Function], "onContextMenu": [Function], "props": Object { @@ -2449,6 +2462,7 @@ Object { "instance": SourceTreeItem { "addCollapseExpandAllOptions": [Function], "context": Object {}, + "handleDownloadFile": [Function], "onClick": [Function], "onContextMenu": [Function], "props": Object {