source-tree-item.js (9955B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ 4 5 import { showMenu } from "../../context-menu/menu"; 6 7 import { 8 isSourceMapIgnoreListEnabled, 9 isSourceOnSourceMapIgnoreList, 10 getProjectDirectoryRoot, 11 getSourcesTreeSources, 12 getBlackBoxRanges, 13 } from "../../selectors/index"; 14 15 import { 16 setNetworkOverride, 17 removeNetworkOverride, 18 } from "devtools/client/framework/actions/index"; 19 20 import { loadSourceText } from "../sources/loadSourceText"; 21 import { toggleBlackBox, blackBoxSources } from "../sources/blackbox"; 22 import { 23 setProjectDirectoryRoot, 24 clearProjectDirectoryRoot, 25 } from "../sources-tree"; 26 27 import { shouldBlackbox } from "../../utils/source"; 28 import { copyToTheClipboard } from "../../utils/clipboard"; 29 import { saveAsLocalFile } from "../../utils/utils"; 30 31 /** 32 * Show the context menu of SourceTreeItem. 33 * 34 * @param {object} event 35 * The context-menu DOM event. 36 * @param {object} item 37 * Source Tree Item object. 38 */ 39 export function showSourceTreeItemContextMenu( 40 event, 41 item, 42 depth, 43 setExpanded, 44 itemName, 45 isOverridden 46 ) { 47 return async ({ dispatch, getState, panel }) => { 48 const copySourceUri2Label = L10N.getStr("copySourceUri2"); 49 const copySourceUri2Key = L10N.getStr("copySourceUri2.accesskey"); 50 const setDirectoryRootLabel = L10N.getStr("setDirectoryRoot.label"); 51 const setDirectoryRootKey = L10N.getStr("setDirectoryRoot.accesskey"); 52 const removeDirectoryRootLabel = L10N.getStr("removeDirectoryRoot.label"); 53 54 const menuOptions = []; 55 56 const state = getState(); 57 const isSourceOnIgnoreList = 58 isSourceMapIgnoreListEnabled(state) && 59 isSourceOnSourceMapIgnoreList(state, item.source); 60 const projectRoot = getProjectDirectoryRoot(state); 61 62 if (item.type == "source") { 63 const { source } = item; 64 const copySourceUri2 = { 65 id: "node-menu-copy-source", 66 label: copySourceUri2Label, 67 accesskey: copySourceUri2Key, 68 disabled: false, 69 click: () => copyToTheClipboard(source.url), 70 }; 71 72 const ignoreStr = item.isBlackBoxed ? "unignore" : "ignore"; 73 const blackBoxMenuItem = { 74 id: "node-menu-blackbox", 75 label: L10N.getStr(`ignoreContextItem.${ignoreStr}`), 76 accesskey: L10N.getStr(`ignoreContextItem.${ignoreStr}.accesskey`), 77 disabled: isSourceOnIgnoreList || !shouldBlackbox(source), 78 click: () => dispatch(toggleBlackBox(source)), 79 }; 80 const downloadFileItem = { 81 id: "node-menu-download-file", 82 label: L10N.getStr("downloadFile.label"), 83 accesskey: L10N.getStr("downloadFile.accesskey"), 84 disabled: false, 85 click: () => saveLocalFile(dispatch, source), 86 }; 87 88 const overrideStr = !isOverridden ? "override" : "removeOverride"; 89 const overridesItem = { 90 id: "node-menu-overrides", 91 label: L10N.getStr(`overridesContextItem.${overrideStr}`), 92 accesskey: L10N.getStr(`overridesContextItem.${overrideStr}.accesskey`), 93 // Network overrides are disabled for original files. 94 disabled: source.isOriginal, 95 // Network overrides are disabled for remote debugging (bug 1881441). 96 visible: panel.toolbox.commands.descriptorFront.isLocalTab, 97 click: () => 98 handleLocalOverride(dispatch, panel.toolbox, source, isOverridden), 99 }; 100 101 menuOptions.push( 102 copySourceUri2, 103 blackBoxMenuItem, 104 downloadFileItem, 105 overridesItem 106 ); 107 } 108 109 // All other types other than source are folder-like 110 if (item.type != "source") { 111 addCollapseExpandAllOptions(menuOptions, item, setExpanded); 112 113 if (projectRoot == item.uniquePath) { 114 menuOptions.push({ 115 id: "node-remove-directory-root", 116 label: removeDirectoryRootLabel, 117 disabled: false, 118 click: () => dispatch(clearProjectDirectoryRoot()), 119 }); 120 } else { 121 menuOptions.push({ 122 id: "node-set-directory-root", 123 label: setDirectoryRootLabel, 124 accesskey: setDirectoryRootKey, 125 disabled: false, 126 click: () => 127 dispatch( 128 setProjectDirectoryRoot( 129 item.uniquePath, 130 itemName, 131 getItemProjectDirectoryRootName(item) 132 ) 133 ), 134 }); 135 } 136 137 addBlackboxAllOption(dispatch, state, menuOptions, item, depth); 138 } 139 140 showMenu(event, menuOptions); 141 }; 142 } 143 144 /** 145 * Compute the string which will be displayed as tooltip on the project directory root header 146 */ 147 function getItemProjectDirectoryRootName(item) { 148 if (item.thread) { 149 return item.thread.name; 150 } 151 152 // Go up the source tree to get to the group item 153 let groupItem = item; 154 while (!groupItem.groupName) { 155 groupItem = groupItem.parent; 156 } 157 158 // Group's origin is the base URL 159 const origin = groupItem.origin; 160 const path = item != groupItem ? item.path : ""; 161 // The group item's parent is always a thread item 162 const threadName = groupItem.parent.thread.name; 163 164 return `${origin}${path} on ${threadName}`; 165 } 166 167 async function saveLocalFile(dispatch, source) { 168 if (!source) { 169 return null; 170 } 171 172 const data = await dispatch(loadSourceText(source)); 173 if (!data) { 174 return null; 175 } 176 return saveAsLocalFile(data.value, source.displayURL.filename); 177 } 178 179 async function handleLocalOverride(dispatch, toolbox, source, isOverridden) { 180 if (!source || !source.url) { 181 return; 182 } 183 184 const toolboxStore = toolbox.store; 185 if (!isOverridden) { 186 const data = await dispatch(loadSourceText(source)); 187 if (data?.value && data.value.type == "text") { 188 toolboxStore.dispatch( 189 setNetworkOverride( 190 toolbox.commands, 191 source.url, 192 data.value.value, 193 window 194 ) 195 ); 196 } 197 } else { 198 toolboxStore.dispatch(removeNetworkOverride(toolbox.commands, source.url)); 199 } 200 } 201 202 function addBlackboxAllOption(dispatch, state, menuOptions, item, depth) { 203 const { 204 sourcesInside, 205 sourcesOutside, 206 allInsideBlackBoxed, 207 allOutsideBlackBoxed, 208 } = getBlackBoxSourcesGroups(state, item); 209 const projectRoot = getProjectDirectoryRoot(state); 210 211 let blackBoxInsideMenuItemLabel; 212 let blackBoxOutsideMenuItemLabel; 213 if (depth === 0 || (depth === 1 && projectRoot === "")) { 214 blackBoxInsideMenuItemLabel = allInsideBlackBoxed 215 ? L10N.getStr("unignoreAllInGroup.label") 216 : L10N.getStr("ignoreAllInGroup.label"); 217 if (sourcesOutside.length) { 218 blackBoxOutsideMenuItemLabel = allOutsideBlackBoxed 219 ? L10N.getStr("unignoreAllOutsideGroup.label") 220 : L10N.getStr("ignoreAllOutsideGroup.label"); 221 } 222 } else { 223 blackBoxInsideMenuItemLabel = allInsideBlackBoxed 224 ? L10N.getStr("unignoreAllInDir.label") 225 : L10N.getStr("ignoreAllInDir.label"); 226 if (sourcesOutside.length) { 227 blackBoxOutsideMenuItemLabel = allOutsideBlackBoxed 228 ? L10N.getStr("unignoreAllOutsideDir.label") 229 : L10N.getStr("ignoreAllOutsideDir.label"); 230 } 231 } 232 233 const blackBoxInsideMenuItem = { 234 id: allInsideBlackBoxed 235 ? "node-unblackbox-all-inside" 236 : "node-blackbox-all-inside", 237 label: blackBoxInsideMenuItemLabel, 238 disabled: false, 239 click: () => dispatch(blackBoxSources(sourcesInside, !allInsideBlackBoxed)), 240 }; 241 242 if (sourcesOutside.length) { 243 menuOptions.push({ 244 id: "node-blackbox-all", 245 label: L10N.getStr("ignoreAll.label"), 246 submenu: [ 247 blackBoxInsideMenuItem, 248 { 249 id: allOutsideBlackBoxed 250 ? "node-unblackbox-all-outside" 251 : "node-blackbox-all-outside", 252 label: blackBoxOutsideMenuItemLabel, 253 disabled: false, 254 click: () => 255 dispatch(blackBoxSources(sourcesOutside, !allOutsideBlackBoxed)), 256 }, 257 ], 258 }); 259 } else { 260 menuOptions.push(blackBoxInsideMenuItem); 261 } 262 } 263 264 function addCollapseExpandAllOptions(menuOptions, item, setExpanded) { 265 menuOptions.push({ 266 id: "node-menu-collapse-all", 267 label: L10N.getStr("collapseAll.label"), 268 disabled: false, 269 click: () => setExpanded(item, false, true), 270 }); 271 272 menuOptions.push({ 273 id: "node-menu-expand-all", 274 label: L10N.getStr("expandAll.label"), 275 disabled: false, 276 click: () => setExpanded(item, true, true), 277 }); 278 } 279 280 /** 281 * Computes 4 lists: 282 * - `sourcesInside`: the list of all Source Items that are 283 * children of the current item (can be thread/group/directory). 284 * This include any nested level of children. 285 * - `sourcesOutside`: all other Source Items. 286 * i.e. all sources that are in any other folder of any group/thread. 287 * - `allInsideBlackBoxed`, all sources of `sourcesInside` which are currently 288 * blackboxed. 289 * - `allOutsideBlackBoxed`, all sources of `sourcesOutside` which are currently 290 * blackboxed. 291 */ 292 function getBlackBoxSourcesGroups(state, item) { 293 const allSources = []; 294 function collectAllSources(list, _item) { 295 if (_item.children) { 296 _item.children.forEach(i => collectAllSources(list, i)); 297 } 298 if (_item.type == "source") { 299 list.push(_item.source); 300 } 301 } 302 303 const rootItems = getSourcesTreeSources(state); 304 const blackBoxRanges = getBlackBoxRanges(state); 305 306 for (const rootItem of rootItems) { 307 collectAllSources(allSources, rootItem); 308 } 309 310 const sourcesInside = []; 311 collectAllSources(sourcesInside, item); 312 313 const sourcesOutside = allSources.filter( 314 source => !sourcesInside.includes(source) 315 ); 316 const allInsideBlackBoxed = sourcesInside.every( 317 source => blackBoxRanges[source.url] 318 ); 319 const allOutsideBlackBoxed = sourcesOutside.every( 320 source => blackBoxRanges[source.url] 321 ); 322 323 return { 324 sourcesInside, 325 sourcesOutside, 326 allInsideBlackBoxed, 327 allOutsideBlackBoxed, 328 }; 329 }