summaryrefslogtreecommitdiffstats
path: root/devtools/client/webconsole/actions
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
commit6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /devtools/client/webconsole/actions
parentInitial commit. (diff)
downloadthunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz
thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client/webconsole/actions')
-rw-r--r--devtools/client/webconsole/actions/autocomplete.js376
-rw-r--r--devtools/client/webconsole/actions/filters.js59
-rw-r--r--devtools/client/webconsole/actions/history.js90
-rw-r--r--devtools/client/webconsole/actions/index.js21
-rw-r--r--devtools/client/webconsole/actions/input.js466
-rw-r--r--devtools/client/webconsole/actions/messages.js173
-rw-r--r--devtools/client/webconsole/actions/moz.build17
-rw-r--r--devtools/client/webconsole/actions/notifications.js48
-rw-r--r--devtools/client/webconsole/actions/object.js63
-rw-r--r--devtools/client/webconsole/actions/toolbox.js49
-rw-r--r--devtools/client/webconsole/actions/ui.js247
11 files changed, 1609 insertions, 0 deletions
diff --git a/devtools/client/webconsole/actions/autocomplete.js b/devtools/client/webconsole/actions/autocomplete.js
new file mode 100644
index 0000000000..e2d08351ec
--- /dev/null
+++ b/devtools/client/webconsole/actions/autocomplete.js
@@ -0,0 +1,376 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ AUTOCOMPLETE_CLEAR,
+ AUTOCOMPLETE_DATA_RECEIVE,
+ AUTOCOMPLETE_PENDING_REQUEST,
+ AUTOCOMPLETE_RETRIEVE_FROM_CACHE,
+} = require("resource://devtools/client/webconsole/constants.js");
+
+const {
+ analyzeInputString,
+ shouldInputBeAutocompleted,
+} = require("resource://devtools/shared/webconsole/analyze-input-string.js");
+
+loader.lazyRequireGetter(
+ this,
+ "getSelectedTarget",
+ "resource://devtools/shared/commands/target/selectors/targets.js",
+ true
+);
+
+/**
+ * Update the data used for the autocomplete popup in the console input (JsTerm).
+ *
+ * @param {Boolean} force: True to force a call to the server (as opposed to retrieve
+ * from the cache).
+ * @param {Array<String>} getterPath: Array representing the getter access (i.e.
+ * `a.b.c.d.` is described as ['a', 'b', 'c', 'd'] ).
+ * @param {Array<String>} expressionVars: Array of the variables defined in the expression.
+ */
+function autocompleteUpdate(force, getterPath, expressionVars) {
+ return async ({ dispatch, getState, webConsoleUI, hud }) => {
+ if (hud.inputHasSelection()) {
+ return dispatch(autocompleteClear());
+ }
+
+ const inputValue = hud.getInputValue();
+ const mappedVars = hud.getMappedVariables() ?? {};
+ const allVars = (expressionVars ?? []).concat(Object.keys(mappedVars));
+ const frameActorId = await hud.getSelectedFrameActorID();
+
+ const cursor = webConsoleUI.getInputCursor();
+
+ const state = getState().autocomplete;
+ const { cache } = state;
+ if (
+ !force &&
+ (!inputValue || /^[a-zA-Z0-9_$]/.test(inputValue.substring(cursor)))
+ ) {
+ return dispatch(autocompleteClear());
+ }
+
+ const rawInput = inputValue.substring(0, cursor);
+ const retrieveFromCache =
+ !force &&
+ cache &&
+ cache.input &&
+ rawInput.startsWith(cache.input) &&
+ /[a-zA-Z0-9]$/.test(rawInput) &&
+ frameActorId === cache.frameActorId;
+
+ if (retrieveFromCache) {
+ return dispatch(autoCompleteDataRetrieveFromCache(rawInput));
+ }
+
+ const authorizedEvaluations = updateAuthorizedEvaluations(
+ state.authorizedEvaluations,
+ getterPath,
+ mappedVars
+ );
+
+ const { input, originalExpression } = await getMappedInput(
+ rawInput,
+ mappedVars,
+ hud
+ );
+
+ return dispatch(
+ autocompleteDataFetch({
+ input,
+ frameActorId,
+ authorizedEvaluations,
+ force,
+ allVars,
+ mappedVars,
+ originalExpression,
+ })
+ );
+ };
+}
+
+/**
+ * Combine or replace authorizedEvaluations with the newly authorized getter path, if any.
+ * @param {Array<Array<String>>} authorizedEvaluations Existing authorized evaluations (may
+ * be updated in place)
+ * @param {Array<String>} getterPath The new getter path
+ * @param {{[String]: String}} mappedVars Map of original to generated variable names.
+ * @returns {Array<Array<String>>} The updated authorized evaluations (the original array,
+ * if it was updated in place) */
+function updateAuthorizedEvaluations(
+ authorizedEvaluations,
+ getterPath,
+ mappedVars
+) {
+ if (!Array.isArray(authorizedEvaluations) || !authorizedEvaluations.length) {
+ authorizedEvaluations = [];
+ }
+
+ if (Array.isArray(getterPath) && getterPath.length) {
+ // We need to check for any previous authorizations. For example, here if getterPath
+ // is ["a", "b", "c", "d"], we want to see if there was any other path that was
+ // authorized in a previous request. For that, we only add the previous
+ // authorizations if the last auth is contained in getterPath. (for the example, we
+ // would keep if it is [["a", "b"]], not if [["b"]] nor [["f", "g"]])
+ const last = authorizedEvaluations[authorizedEvaluations.length - 1];
+
+ const generatedPath = mappedVars[getterPath[0]]?.split(".");
+ if (generatedPath) {
+ getterPath = generatedPath.concat(getterPath.slice(1));
+ }
+
+ const isMappedVariable =
+ generatedPath && getterPath.length === generatedPath.length;
+ const concat = !last || last.every((x, index) => x === getterPath[index]);
+ if (isMappedVariable) {
+ // If the path consists only of an original variable, add all the prefixes of its
+ // mapping. For example, for myVar => a.b.c, authorize a, a.b, and a.b.c. This
+ // ensures we'll only show a prompt for myVar once even if a.b and a.b.c are both
+ // unsafe getters.
+ authorizedEvaluations = generatedPath.map((_, i) =>
+ generatedPath.slice(0, i + 1)
+ );
+ } else if (concat) {
+ authorizedEvaluations.push(getterPath);
+ } else {
+ authorizedEvaluations = [getterPath];
+ }
+ }
+ return authorizedEvaluations;
+}
+
+/**
+ * Apply source mapping to the autocomplete input.
+ * @param {String} rawInput The input to map.
+ * @param {{[String]: String}} mappedVars Map of original to generated variable names.
+ * @param {WebConsole} hud A reference to the webconsole hud.
+ * @returns {String} The source-mapped expression to autocomplete.
+ */
+async function getMappedInput(rawInput, mappedVars, hud) {
+ if (!mappedVars || !Object.keys(mappedVars).length) {
+ return { input: rawInput, originalExpression: undefined };
+ }
+
+ const inputAnalysis = analyzeInputString(rawInput, 500);
+ if (!shouldInputBeAutocompleted(inputAnalysis)) {
+ return { input: rawInput, originalExpression: undefined };
+ }
+
+ const {
+ mainExpression: originalExpression,
+ isPropertyAccess,
+ isElementAccess,
+ lastStatement,
+ } = inputAnalysis;
+
+ // If we're autocompleting a variable name, pass it through unchanged so that we
+ // show original variable names rather than generated ones.
+ // For example, if we have the mapping `myVariable` => `x`, show variables starting
+ // with myVariable rather than x.
+ if (!isPropertyAccess && !isElementAccess) {
+ return { input: lastStatement, originalExpression };
+ }
+
+ let generated =
+ (await hud.getMappedExpression(originalExpression))?.expression ??
+ originalExpression;
+ // Strip off the semicolon if the expression was converted to a statement
+ const trailingSemicolon = /;\s*$/;
+ if (
+ trailingSemicolon.test(generated) &&
+ !trailingSemicolon.test(originalExpression)
+ ) {
+ generated = generated.slice(0, generated.lastIndexOf(";"));
+ }
+
+ const suffix = lastStatement.slice(originalExpression.length);
+ return { input: generated + suffix, originalExpression };
+}
+
+/**
+ * Called when the autocompletion data should be cleared.
+ */
+function autocompleteClear() {
+ return {
+ type: AUTOCOMPLETE_CLEAR,
+ };
+}
+
+/**
+ * Called when the autocompletion data should be retrieved from the cache (i.e.
+ * client-side).
+ *
+ * @param {String} input: The input used to filter the cached data.
+ */
+function autoCompleteDataRetrieveFromCache(input) {
+ return {
+ type: AUTOCOMPLETE_RETRIEVE_FROM_CACHE,
+ input,
+ };
+}
+
+let currentRequestId = 0;
+function generateRequestId() {
+ return currentRequestId++;
+}
+
+/**
+ * Action that fetch autocompletion data from the server.
+ *
+ * @param {Object} Object of the following shape:
+ * - {String} input: the expression that we want to complete.
+ * - {String} frameActorId: The id of the frame we want to autocomplete in.
+ * - {Boolean} force: true if the user forced an autocompletion (with Ctrl+Space).
+ * - {Array} authorizedEvaluations: Array of the properties access which can be
+ * executed by the engine.
+ * Example: [["x", "myGetter"], ["x", "myGetter", "y", "glitter"]]
+ * to retrieve properties of `x.myGetter.` and `x.myGetter.y.glitter`.
+ */
+function autocompleteDataFetch({
+ input,
+ frameActorId,
+ force,
+ authorizedEvaluations,
+ allVars,
+ mappedVars,
+ originalExpression,
+}) {
+ return async ({ dispatch, commands, webConsoleUI, hud }) => {
+ // Retrieve the right WebConsole front that relates either to (by order of priority):
+ // - the currently selected target in the context selector
+ // (contextSelectedTargetFront),
+ // - the currently selected Node in the inspector (selectedNodeActor),
+ // - the currently selected frame in the debugger (when paused) (frameActor),
+ // - the currently selected target in the iframe dropdown
+ // (selectedTargetFront from the TargetCommand)
+ const selectedNodeActorId = webConsoleUI.getSelectedNodeActorID();
+
+ let targetFront = commands.targetCommand.selectedTargetFront;
+ // Note that getSelectedTargetFront will return null if we default to the top level target.
+ const contextSelectorTargetFront = getSelectedTarget(
+ hud.commands.targetCommand.store.getState()
+ );
+ const selectedActorId = selectedNodeActorId || frameActorId;
+ if (contextSelectorTargetFront) {
+ targetFront = contextSelectorTargetFront;
+ } else if (selectedActorId) {
+ const selectedFront = commands.client.getFrontByID(selectedActorId);
+ if (selectedFront) {
+ targetFront = selectedFront.targetFront;
+ }
+ }
+
+ const webconsoleFront = await targetFront.getFront("console");
+
+ const id = generateRequestId();
+ dispatch({ type: AUTOCOMPLETE_PENDING_REQUEST, id });
+
+ webconsoleFront
+ .autocomplete(
+ input,
+ undefined,
+ frameActorId,
+ selectedNodeActorId,
+ authorizedEvaluations,
+ allVars
+ )
+ .then(data => {
+ if (data.isUnsafeGetter && originalExpression !== undefined) {
+ data.getterPath = unmapGetterPath(
+ data.getterPath,
+ originalExpression,
+ mappedVars
+ );
+ }
+ return dispatch(
+ autocompleteDataReceive({
+ id,
+ input,
+ force,
+ frameActorId,
+ data,
+ authorizedEvaluations,
+ })
+ );
+ })
+ .catch(e => {
+ console.error("failed autocomplete", e);
+ dispatch(autocompleteClear());
+ });
+ };
+}
+
+/**
+ * Replace generated variable names in an unsafe getter path with their original
+ * counterparts.
+ * @param {Array<String>} getterPath Array of properties leading up to and including the
+ * unsafe getter.
+ * @param {String} originalExpression The expression that was evaluated, before mapping.
+ * @param {{[String]: String}} mappedVars Map of original to generated variable names.
+ * @returns {Array<String>} An updated getter path containing original variables.
+ */
+function unmapGetterPath(getterPath, originalExpression, mappedVars) {
+ // We know that the original expression is a sequence of property accesses, that only
+ // the first part can be a mapped variable, and that the getter path must start with
+ // its generated path or be a prefix of it.
+
+ // Suppose we have the expression `foo.bar`, which maps to `a.b.c.bar`.
+ // Get the first part of the expression ("foo")
+ const originalVariable = /^[^.[?]*/s.exec(originalExpression)[0].trim();
+ const generatedVariable = mappedVars[originalVariable];
+ if (generatedVariable) {
+ // Get number of properties in "a.b.c"
+ const generatedVariableParts = generatedVariable.split(".");
+ // Replace ["a", "b", "c"] with "foo" in the getter path.
+ // Note that this will also work if the getter path ends inside of the mapped
+ // variable, like ["a", "b"].
+ return [
+ originalVariable,
+ ...getterPath.slice(generatedVariableParts.length),
+ ];
+ }
+ return getterPath;
+}
+
+/**
+ * Called when we receive the autocompletion data from the server.
+ *
+ * @param {Object} Object of the following shape:
+ * - {Integer} id: The autocompletion request id. This will be used in the reducer
+ * to check that we update the state with the last request results.
+ * - {String} input: the expression that we want to complete.
+ * - {String} frameActorId: The id of the frame we want to autocomplete in.
+ * - {Boolean} force: true if the user forced an autocompletion (with Ctrl+Space).
+ * - {Object} data: The actual data returned from the server.
+ * - {Array} authorizedEvaluations: Array of the properties access which can be
+ * executed by the engine.
+ * Example: [["x", "myGetter"], ["x", "myGetter", "y", "glitter"]]
+ * to retrieve properties of `x.myGetter.` and `x.myGetter.y.glitter`.
+ */
+function autocompleteDataReceive({
+ id,
+ input,
+ frameActorId,
+ force,
+ data,
+ authorizedEvaluations,
+}) {
+ return {
+ type: AUTOCOMPLETE_DATA_RECEIVE,
+ id,
+ input,
+ force,
+ frameActorId,
+ data,
+ authorizedEvaluations,
+ };
+}
+
+module.exports = {
+ autocompleteClear,
+ autocompleteUpdate,
+};
diff --git a/devtools/client/webconsole/actions/filters.js b/devtools/client/webconsole/actions/filters.js
new file mode 100644
index 0000000000..108614ee9f
--- /dev/null
+++ b/devtools/client/webconsole/actions/filters.js
@@ -0,0 +1,59 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ getAllFilters,
+} = require("resource://devtools/client/webconsole/selectors/filters.js");
+
+const {
+ FILTER_TEXT_SET,
+ FILTER_TOGGLE,
+ FILTERS_CLEAR,
+ PREFS,
+ FILTERS,
+} = require("resource://devtools/client/webconsole/constants.js");
+
+function filterTextSet(text) {
+ return {
+ type: FILTER_TEXT_SET,
+ text,
+ };
+}
+
+function filterToggle(filter) {
+ return ({ dispatch, getState, prefsService }) => {
+ dispatch({
+ type: FILTER_TOGGLE,
+ filter,
+ });
+ const filterState = getAllFilters(getState());
+ prefsService.setBoolPref(
+ PREFS.FILTER[filter.toUpperCase()],
+ filterState[filter]
+ );
+ };
+}
+
+function filtersClear() {
+ return ({ dispatch, getState, prefsService }) => {
+ dispatch({
+ type: FILTERS_CLEAR,
+ });
+
+ const filterState = getAllFilters(getState());
+ for (const filter in filterState) {
+ if (filter !== FILTERS.TEXT) {
+ prefsService.clearUserPref(PREFS.FILTER[filter.toUpperCase()]);
+ }
+ }
+ };
+}
+
+module.exports = {
+ filterTextSet,
+ filterToggle,
+ filtersClear,
+};
diff --git a/devtools/client/webconsole/actions/history.js b/devtools/client/webconsole/actions/history.js
new file mode 100644
index 0000000000..3c55389020
--- /dev/null
+++ b/devtools/client/webconsole/actions/history.js
@@ -0,0 +1,90 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ APPEND_TO_HISTORY,
+ CLEAR_HISTORY,
+ HISTORY_LOADED,
+ UPDATE_HISTORY_POSITION,
+ REVERSE_SEARCH_INPUT_CHANGE,
+ REVERSE_SEARCH_BACK,
+ REVERSE_SEARCH_NEXT,
+} = require("resource://devtools/client/webconsole/constants.js");
+
+/**
+ * Append a new value in the history of executed expressions,
+ * or overwrite the most recent entry. The most recent entry may
+ * contain the last edited input value that was not evaluated yet.
+ */
+function appendToHistory(expression) {
+ return {
+ type: APPEND_TO_HISTORY,
+ expression,
+ };
+}
+
+/**
+ * Clear the console history altogether. Note that this will not affect
+ * other consoles that are already opened (since they have their own copy),
+ * but it will reset the array for all newly-opened consoles.
+ */
+function clearHistory() {
+ return {
+ type: CLEAR_HISTORY,
+ };
+}
+
+/**
+ * Fired when the console history from previous Firefox sessions is loaded.
+ */
+function historyLoaded(entries) {
+ return {
+ type: HISTORY_LOADED,
+ entries,
+ };
+}
+
+/**
+ * Update place-holder position in the history list.
+ */
+function updateHistoryPosition(direction, expression) {
+ return {
+ type: UPDATE_HISTORY_POSITION,
+ direction,
+ expression,
+ };
+}
+
+function reverseSearchInputChange(value) {
+ return {
+ type: REVERSE_SEARCH_INPUT_CHANGE,
+ value,
+ };
+}
+
+function showReverseSearchNext({ access } = {}) {
+ return {
+ type: REVERSE_SEARCH_NEXT,
+ access,
+ };
+}
+
+function showReverseSearchBack({ access } = {}) {
+ return {
+ type: REVERSE_SEARCH_BACK,
+ access,
+ };
+}
+
+module.exports = {
+ appendToHistory,
+ clearHistory,
+ historyLoaded,
+ updateHistoryPosition,
+ reverseSearchInputChange,
+ showReverseSearchNext,
+ showReverseSearchBack,
+};
diff --git a/devtools/client/webconsole/actions/index.js b/devtools/client/webconsole/actions/index.js
new file mode 100644
index 0000000000..29aeb937a2
--- /dev/null
+++ b/devtools/client/webconsole/actions/index.js
@@ -0,0 +1,21 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const actionModules = [
+ require("resource://devtools/client/webconsole/actions/autocomplete.js"),
+ require("resource://devtools/client/webconsole/actions/filters.js"),
+ require("resource://devtools/client/webconsole/actions/input.js"),
+ require("resource://devtools/client/webconsole/actions/messages.js"),
+ require("resource://devtools/client/webconsole/actions/ui.js"),
+ require("resource://devtools/client/webconsole/actions/notifications.js"),
+ require("resource://devtools/client/webconsole/actions/object.js"),
+ require("resource://devtools/client/webconsole/actions/toolbox.js"),
+ require("resource://devtools/client/webconsole/actions/history.js"),
+];
+
+const actions = Object.assign({}, ...actionModules);
+
+module.exports = actions;
diff --git a/devtools/client/webconsole/actions/input.js b/devtools/client/webconsole/actions/input.js
new file mode 100644
index 0000000000..85b1867fc8
--- /dev/null
+++ b/devtools/client/webconsole/actions/input.js
@@ -0,0 +1,466 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ Utils: WebConsoleUtils,
+} = require("resource://devtools/client/webconsole/utils.js");
+const {
+ EVALUATE_EXPRESSION,
+ SET_TERMINAL_INPUT,
+ SET_TERMINAL_EAGER_RESULT,
+ EDITOR_PRETTY_PRINT,
+} = require("resource://devtools/client/webconsole/constants.js");
+const {
+ getAllPrefs,
+} = require("resource://devtools/client/webconsole/selectors/prefs.js");
+const ResourceCommand = require("resource://devtools/shared/commands/resource/resource-command.js");
+const l10n = require("resource://devtools/client/webconsole/utils/l10n.js");
+
+loader.lazyServiceGetter(
+ this,
+ "clipboardHelper",
+ "@mozilla.org/widget/clipboardhelper;1",
+ "nsIClipboardHelper"
+);
+loader.lazyRequireGetter(
+ this,
+ "messagesActions",
+ "resource://devtools/client/webconsole/actions/messages.js"
+);
+loader.lazyRequireGetter(
+ this,
+ "historyActions",
+ "resource://devtools/client/webconsole/actions/history.js"
+);
+loader.lazyRequireGetter(
+ this,
+ "ConsoleCommand",
+ "resource://devtools/client/webconsole/types.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "netmonitorBlockingActions",
+ "resource://devtools/client/netmonitor/src/actions/request-blocking.js"
+);
+
+loader.lazyRequireGetter(
+ this,
+ ["saveScreenshot", "captureAndSaveScreenshot"],
+ "resource://devtools/client/shared/screenshot.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "createSimpleTableMessage",
+ "resource://devtools/client/webconsole/utils/messages.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "getSelectedTarget",
+ "resource://devtools/shared/commands/target/selectors/targets.js",
+ true
+);
+
+const HELP_URL =
+ "https://firefox-source-docs.mozilla.org/devtools-user/web_console/helpers/";
+
+async function getMappedExpression(hud, expression) {
+ let mapResult;
+ try {
+ mapResult = await hud.getMappedExpression(expression);
+ } catch (e) {
+ console.warn("Error when calling getMappedExpression", e);
+ }
+
+ let mapped = null;
+ if (mapResult) {
+ ({ expression, mapped } = mapResult);
+ }
+ return { expression, mapped };
+}
+
+function evaluateExpression(expression, from = "input") {
+ return async ({ dispatch, toolbox, webConsoleUI, hud, commands }) => {
+ if (!expression) {
+ expression = hud.getInputSelection() || hud.getInputValue();
+ }
+ if (!expression) {
+ return null;
+ }
+
+ // We use the messages action as it's doing additional transformation on the message.
+ const { messages } = dispatch(
+ messagesActions.messagesAdd([
+ new ConsoleCommand({
+ messageText: expression,
+ timeStamp: Date.now(),
+ }),
+ ])
+ );
+ const [consoleCommandMessage] = messages;
+
+ dispatch({
+ type: EVALUATE_EXPRESSION,
+ expression,
+ from,
+ });
+
+ WebConsoleUtils.usageCount++;
+
+ let mapped;
+ ({ expression, mapped } = await getMappedExpression(hud, expression));
+
+ // Even if the evaluation fails,
+ // we still need to pass the error response to onExpressionEvaluated.
+ const onSettled = res => res;
+
+ const response = await commands.scriptCommand
+ .execute(expression, {
+ frameActor: hud.getSelectedFrameActorID(),
+ selectedNodeActor: webConsoleUI.getSelectedNodeActorID(),
+ selectedTargetFront: getSelectedTarget(
+ webConsoleUI.hud.commands.targetCommand.store.getState()
+ ),
+ mapped,
+ })
+ .then(onSettled, onSettled);
+
+ const serverConsoleCommandTimestamp = response.startTime;
+
+ // In case of remote debugging, it might happen that the debuggee page does not have
+ // the exact same clock time as the client. This could cause some ordering issues
+ // where the result message is displayed *before* the expression that lead to it.
+ if (
+ serverConsoleCommandTimestamp &&
+ consoleCommandMessage.timeStamp > serverConsoleCommandTimestamp
+ ) {
+ // If we're in such case, we remove the original command message, and add it again,
+ // with the timestamp coming from the server.
+ dispatch(messagesActions.messageRemove(consoleCommandMessage.id));
+ dispatch(
+ messagesActions.messagesAdd([
+ new ConsoleCommand({
+ messageText: expression,
+ timeStamp: serverConsoleCommandTimestamp,
+ }),
+ ])
+ );
+ }
+
+ return dispatch(onExpressionEvaluated(response));
+ };
+}
+
+/**
+ * The JavaScript evaluation response handler.
+ *
+ * @private
+ * @param {Object} response
+ * The message received from the server.
+ */
+function onExpressionEvaluated(response) {
+ return async ({ dispatch, webConsoleUI }) => {
+ if (response.error) {
+ console.error(`Evaluation error`, response.error, ": ", response.message);
+ return;
+ }
+
+ // If the evaluation was a top-level await expression that was rejected, there will
+ // be an uncaught exception reported, so we don't need to do anything.
+ if (response.topLevelAwaitRejected === true) {
+ return;
+ }
+
+ if (!response.helperResult) {
+ webConsoleUI.wrapper.dispatchMessageAdd(response);
+ return;
+ }
+
+ await dispatch(handleHelperResult(response));
+ };
+}
+
+function handleHelperResult(response) {
+ // eslint-disable-next-line complexity
+ return async ({ dispatch, hud, toolbox, webConsoleUI, getState }) => {
+ const { result, helperResult } = response;
+ const helperHasRawOutput = !!helperResult?.rawOutput;
+
+ if (helperResult?.type) {
+ switch (helperResult.type) {
+ case "clearOutput":
+ dispatch(messagesActions.messagesClear());
+ break;
+ case "clearHistory":
+ dispatch(historyActions.clearHistory());
+ break;
+ case "historyOutput":
+ const history = getState().history.entries || [];
+ const columns = new Map([
+ ["_index", "(index)"],
+ ["expression", "Expressions"],
+ ]);
+ dispatch(
+ messagesActions.messagesAdd([
+ {
+ ...createSimpleTableMessage(
+ columns,
+ history.map((expression, index) => {
+ return { _index: index, expression };
+ })
+ ),
+ },
+ ])
+ );
+ break;
+ case "inspectObject": {
+ const objectActor = helperResult.object;
+ if (hud.toolbox && !helperResult.forceExpandInConsole) {
+ hud.toolbox.inspectObjectActor(objectActor);
+ } else {
+ webConsoleUI.inspectObjectActor(objectActor);
+ }
+ break;
+ }
+ case "help":
+ hud.openLink(HELP_URL);
+ break;
+ case "copyValueToClipboard":
+ clipboardHelper.copyString(helperResult.value);
+ dispatch(
+ messagesActions.messagesAdd([
+ {
+ resourceType: ResourceCommand.TYPES.PLATFORM_MESSAGE,
+ message: l10n.getStr(
+ "webconsole.message.commands.copyValueToClipboard"
+ ),
+ },
+ ])
+ );
+ break;
+ case "screenshotOutput":
+ const { args, value } = helperResult;
+ const targetFront =
+ getSelectedTarget(hud.commands.targetCommand.store.getState()) ||
+ hud.currentTarget;
+ let screenshotMessages;
+
+ // @backward-compat { version 87 } The screenshot-content actor isn't available
+ // in older server.
+ // With an old server, the console actor captures the screenshot when handling
+ // the command, and send it to the client which only needs to save it to a file.
+ // With a new server, the server simply acknowledges the command,
+ // and the client will drive the whole screenshot process (capture and save).
+ if (targetFront.hasActor("screenshotContent")) {
+ screenshotMessages = await captureAndSaveScreenshot(
+ targetFront,
+ webConsoleUI.getPanelWindow(),
+ args
+ );
+ } else {
+ screenshotMessages = await saveScreenshot(
+ webConsoleUI.getPanelWindow(),
+ args,
+ value
+ );
+ }
+
+ if (screenshotMessages && screenshotMessages.length) {
+ dispatch(
+ messagesActions.messagesAdd(
+ screenshotMessages.map(message => ({
+ message: {
+ level: message.level || "log",
+ arguments: [message.text],
+ chromeContext: true,
+ },
+ resourceType: ResourceCommand.TYPES.CONSOLE_MESSAGE,
+ }))
+ )
+ );
+ }
+ break;
+ case "blockURL":
+ const blockURL = helperResult.args.url;
+ // The console actor isn't able to block the request as the console actor runs in the content
+ // process, while the request has to be blocked from the parent process.
+ // Then, calling the Netmonitor action will only update the visual state of the Netmonitor,
+ // but we also have to block the request via the NetworkParentActor.
+ await hud.commands.networkCommand.blockRequestForUrl(blockURL);
+ toolbox
+ .getPanel("netmonitor")
+ ?.panelWin.store.dispatch(
+ netmonitorBlockingActions.addBlockedUrl(blockURL)
+ );
+
+ dispatch(
+ messagesActions.messagesAdd([
+ {
+ resourceType: ResourceCommand.TYPES.PLATFORM_MESSAGE,
+ message: l10n.getFormatStr(
+ "webconsole.message.commands.blockedURL",
+ [blockURL]
+ ),
+ },
+ ])
+ );
+ break;
+ case "unblockURL":
+ const unblockURL = helperResult.args.url;
+ await hud.commands.networkCommand.unblockRequestForUrl(unblockURL);
+ toolbox
+ .getPanel("netmonitor")
+ ?.panelWin.store.dispatch(
+ netmonitorBlockingActions.removeBlockedUrl(unblockURL)
+ );
+
+ dispatch(
+ messagesActions.messagesAdd([
+ {
+ resourceType: ResourceCommand.TYPES.PLATFORM_MESSAGE,
+ message: l10n.getFormatStr(
+ "webconsole.message.commands.unblockedURL",
+ [unblockURL]
+ ),
+ },
+ ])
+ );
+ // early return as we already dispatched necessary messages.
+ return;
+ }
+ }
+
+ const hasErrorMessage =
+ response.exceptionMessage ||
+ (helperResult && helperResult.type === "error");
+
+ // Hide undefined results coming from helper functions.
+ const hasUndefinedResult =
+ result && typeof result == "object" && result.type == "undefined";
+
+ if (hasErrorMessage || helperHasRawOutput || !hasUndefinedResult) {
+ dispatch(messagesActions.messagesAdd([response]));
+ }
+ };
+}
+
+function focusInput() {
+ return ({ hud }) => {
+ return hud.focusInput();
+ };
+}
+
+function setInputValue(value) {
+ return ({ hud }) => {
+ return hud.setInputValue(value);
+ };
+}
+
+/**
+ * Request an eager evaluation from the server.
+ *
+ * @param {String} expression: The expression to evaluate.
+ * @param {Boolean} force: When true, will request an eager evaluation again, even if
+ * the expression is the same one than the one that was used in
+ * the previous evaluation.
+ */
+function terminalInputChanged(expression, force = false) {
+ return async ({ dispatch, webConsoleUI, hud, commands, getState }) => {
+ const prefs = getAllPrefs(getState());
+ if (!prefs.eagerEvaluation) {
+ return null;
+ }
+
+ const { terminalInput = "" } = getState().history;
+
+ // Only re-evaluate if the expression did change.
+ if (
+ (!terminalInput && !expression) ||
+ (typeof terminalInput === "string" &&
+ typeof expression === "string" &&
+ expression.trim() === terminalInput.trim() &&
+ !force)
+ ) {
+ return null;
+ }
+
+ dispatch({
+ type: SET_TERMINAL_INPUT,
+ expression: expression.trim(),
+ });
+
+ // There's no need to evaluate an empty string.
+ if (!expression || !expression.trim()) {
+ return dispatch({
+ type: SET_TERMINAL_EAGER_RESULT,
+ expression,
+ result: null,
+ });
+ }
+
+ let mapped;
+ ({ expression, mapped } = await getMappedExpression(hud, expression));
+
+ // We don't want to evaluate top-level await expressions (see Bug 1786805)
+ if (mapped?.await) {
+ return dispatch({
+ type: SET_TERMINAL_EAGER_RESULT,
+ expression,
+ result: null,
+ });
+ }
+
+ const response = await commands.scriptCommand.execute(expression, {
+ frameActor: hud.getSelectedFrameActorID(),
+ selectedNodeActor: webConsoleUI.getSelectedNodeActorID(),
+ selectedTargetFront: getSelectedTarget(
+ hud.commands.targetCommand.store.getState()
+ ),
+ mapped,
+ eager: true,
+ });
+
+ return dispatch({
+ type: SET_TERMINAL_EAGER_RESULT,
+ result: getEagerEvaluationResult(response),
+ });
+ };
+}
+
+/**
+ * Refresh the current eager evaluation by requesting a new eager evaluation.
+ */
+function updateInstantEvaluationResultForCurrentExpression() {
+ return ({ getState, dispatch }) =>
+ dispatch(terminalInputChanged(getState().history.terminalInput, true));
+}
+
+function getEagerEvaluationResult(response) {
+ const result = response.exception || response.result;
+ // Don't show syntax errors results to the user.
+ if (result?.isSyntaxError || (result && result.type == "undefined")) {
+ return null;
+ }
+
+ return result;
+}
+
+function prettyPrintEditor() {
+ return {
+ type: EDITOR_PRETTY_PRINT,
+ };
+}
+
+module.exports = {
+ evaluateExpression,
+ focusInput,
+ setInputValue,
+ terminalInputChanged,
+ updateInstantEvaluationResultForCurrentExpression,
+ prettyPrintEditor,
+};
diff --git a/devtools/client/webconsole/actions/messages.js b/devtools/client/webconsole/actions/messages.js
new file mode 100644
index 0000000000..1da76b4475
--- /dev/null
+++ b/devtools/client/webconsole/actions/messages.js
@@ -0,0 +1,173 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ prepareMessage,
+ getNaturalOrder,
+} = require("resource://devtools/client/webconsole/utils/messages.js");
+const {
+ IdGenerator,
+} = require("resource://devtools/client/webconsole/utils/id-generator.js");
+const {
+ batchActions,
+} = require("resource://devtools/client/shared/redux/middleware/debounce.js");
+
+const {
+ CSS_MESSAGE_ADD_MATCHING_ELEMENTS,
+ MESSAGE_CLOSE,
+ MESSAGE_OPEN,
+ MESSAGE_REMOVE,
+ MESSAGE_TYPE,
+ MESSAGES_ADD,
+ MESSAGES_CLEAR,
+ MESSAGES_DISABLE,
+ NETWORK_MESSAGES_UPDATE,
+ NETWORK_UPDATES_REQUEST,
+ PRIVATE_MESSAGES_CLEAR,
+ TARGET_MESSAGES_REMOVE,
+} = require("resource://devtools/client/webconsole/constants.js");
+
+const defaultIdGenerator = new IdGenerator();
+
+function messagesAdd(packets, idGenerator = null) {
+ if (idGenerator == null) {
+ idGenerator = defaultIdGenerator;
+ }
+ const messages = packets.map(packet => prepareMessage(packet, idGenerator));
+ // Sort the messages by their timestamps.
+ messages.sort(getNaturalOrder);
+ for (let i = messages.length - 1; i >= 0; i--) {
+ if (messages[i].type === MESSAGE_TYPE.CLEAR) {
+ return batchActions([
+ messagesClear(),
+ {
+ type: MESSAGES_ADD,
+ messages: messages.slice(i),
+ },
+ ]);
+ }
+ }
+
+ // When this is used for non-cached messages then handle clear message and
+ // split up into batches
+ return {
+ type: MESSAGES_ADD,
+ messages,
+ };
+}
+
+function messagesClear() {
+ return {
+ type: MESSAGES_CLEAR,
+ };
+}
+
+function messagesDisable(ids) {
+ return {
+ type: MESSAGES_DISABLE,
+ ids,
+ };
+}
+
+function privateMessagesClear() {
+ return {
+ type: PRIVATE_MESSAGES_CLEAR,
+ };
+}
+
+function targetMessagesRemove(targetFront) {
+ return {
+ type: TARGET_MESSAGES_REMOVE,
+ targetFront,
+ };
+}
+
+function messageOpen(id) {
+ return {
+ type: MESSAGE_OPEN,
+ id,
+ };
+}
+
+function messageClose(id) {
+ return {
+ type: MESSAGE_CLOSE,
+ id,
+ };
+}
+
+/**
+ * Make a query on the server to get a list of DOM elements matching the given
+ * CSS selectors and store the information in the state.
+ *
+ * @param {Message} message
+ * The CSSWarning message
+ */
+function messageGetMatchingElements(message) {
+ return async ({ dispatch, commands }) => {
+ try {
+ // We need to do the querySelectorAll using the target the message is coming from,
+ // as well as with the window the warning message was emitted from.
+ const selectedTargetFront = message?.targetFront;
+
+ const response = await commands.scriptCommand.execute(
+ `document.querySelectorAll('${message.cssSelectors}')`,
+ {
+ selectedTargetFront,
+ innerWindowID: message.innerWindowID,
+ }
+ );
+ dispatch({
+ type: CSS_MESSAGE_ADD_MATCHING_ELEMENTS,
+ id: message.id,
+ elements: response.result,
+ });
+ } catch (err) {
+ console.error(err);
+ }
+ };
+}
+
+function messageRemove(id) {
+ return {
+ type: MESSAGE_REMOVE,
+ id,
+ };
+}
+
+function networkMessageUpdates(packets, idGenerator = null) {
+ if (idGenerator == null) {
+ idGenerator = defaultIdGenerator;
+ }
+
+ const messages = packets.map(packet => prepareMessage(packet, idGenerator));
+
+ return {
+ type: NETWORK_MESSAGES_UPDATE,
+ messages,
+ };
+}
+
+function networkUpdateRequests(updates) {
+ return {
+ type: NETWORK_UPDATES_REQUEST,
+ updates,
+ };
+}
+
+module.exports = {
+ messagesAdd,
+ messagesClear,
+ messagesDisable,
+ messageOpen,
+ messageClose,
+ messageRemove,
+ messageGetMatchingElements,
+ networkMessageUpdates,
+ networkUpdateRequests,
+ privateMessagesClear,
+ targetMessagesRemove,
+};
diff --git a/devtools/client/webconsole/actions/moz.build b/devtools/client/webconsole/actions/moz.build
new file mode 100644
index 0000000000..5b06311064
--- /dev/null
+++ b/devtools/client/webconsole/actions/moz.build
@@ -0,0 +1,17 @@
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+ "autocomplete.js",
+ "filters.js",
+ "history.js",
+ "index.js",
+ "input.js",
+ "messages.js",
+ "notifications.js",
+ "object.js",
+ "toolbox.js",
+ "ui.js",
+)
diff --git a/devtools/client/webconsole/actions/notifications.js b/devtools/client/webconsole/actions/notifications.js
new file mode 100644
index 0000000000..2ffe96985b
--- /dev/null
+++ b/devtools/client/webconsole/actions/notifications.js
@@ -0,0 +1,48 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ APPEND_NOTIFICATION,
+ REMOVE_NOTIFICATION,
+} = require("resource://devtools/client/webconsole/constants.js");
+
+/**
+ * Append a notification into JSTerm notification list.
+ */
+function appendNotification(
+ label,
+ value,
+ image,
+ priority,
+ buttons = [],
+ eventCallback
+) {
+ return {
+ type: APPEND_NOTIFICATION,
+ label,
+ value,
+ image,
+ priority,
+ buttons,
+ eventCallback,
+ };
+}
+
+/**
+ * Remove notification with specified value from JSTerm
+ * notification list.
+ */
+function removeNotification(value) {
+ return {
+ type: REMOVE_NOTIFICATION,
+ value,
+ };
+}
+
+module.exports = {
+ appendNotification,
+ removeNotification,
+};
diff --git a/devtools/client/webconsole/actions/object.js b/devtools/client/webconsole/actions/object.js
new file mode 100644
index 0000000000..c2b255bd4d
--- /dev/null
+++ b/devtools/client/webconsole/actions/object.js
@@ -0,0 +1,63 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+loader.lazyServiceGetter(
+ this,
+ "clipboardHelper",
+ "@mozilla.org/widget/clipboardhelper;1",
+ "nsIClipboardHelper"
+);
+
+function storeAsGlobal(actor) {
+ return async ({ commands, hud }) => {
+ const evalString = `{ let i = 0;
+ while (this.hasOwnProperty("temp" + i) && i < 1000) {
+ i++;
+ }
+ this["temp" + i] = _self;
+ "temp" + i;
+ }`;
+
+ const res = await commands.scriptCommand.execute(evalString, {
+ selectedObjectActor: actor,
+ });
+
+ // Select the adhoc target in the console.
+ if (hud.toolbox) {
+ const objectFront = commands.client.getFrontByID(actor);
+ if (objectFront) {
+ const targetActorID = objectFront.targetFront?.actorID;
+ if (targetActorID) {
+ hud.toolbox.selectTarget(targetActorID);
+ }
+ }
+ }
+
+ hud.focusInput();
+ hud.setInputValue(res.result);
+ };
+}
+
+function copyMessageObject(actor, variableText) {
+ return async ({ commands }) => {
+ if (actor) {
+ // The Debugger.Object of the OA will be bound to |_self| during evaluation.
+ // See server/actors/webconsole/eval-with-debugger.js `evalWithDebugger`.
+ const res = await commands.scriptCommand.execute("copy(_self)", {
+ selectedObjectActor: actor,
+ });
+
+ clipboardHelper.copyString(res.helperResult.value);
+ } else {
+ clipboardHelper.copyString(variableText);
+ }
+ };
+}
+
+module.exports = {
+ storeAsGlobal,
+ copyMessageObject,
+};
diff --git a/devtools/client/webconsole/actions/toolbox.js b/devtools/client/webconsole/actions/toolbox.js
new file mode 100644
index 0000000000..65739f2ca5
--- /dev/null
+++ b/devtools/client/webconsole/actions/toolbox.js
@@ -0,0 +1,49 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+function openNetworkPanel(messageId) {
+ return ({ hud }) => {
+ hud.openNetworkPanel(messageId);
+ };
+}
+
+function resendNetworkRequest(messageId) {
+ return ({ hud }) => {
+ hud.resendNetworkRequest(messageId);
+ };
+}
+
+function highlightDomElement(grip) {
+ return ({ hud }) => {
+ const highlighter = hud.getHighlighter();
+ if (highlighter) {
+ highlighter.highlight(grip);
+ }
+ };
+}
+
+function unHighlightDomElement(grip) {
+ return ({ hud }) => {
+ const highlighter = hud.getHighlighter();
+ if (highlighter) {
+ highlighter.unhighlight(grip);
+ }
+ };
+}
+
+function openNodeInInspector(contentDomReference) {
+ return ({ hud }) => {
+ hud.openNodeInInspector({ contentDomReference });
+ };
+}
+
+module.exports = {
+ highlightDomElement,
+ unHighlightDomElement,
+ openNetworkPanel,
+ resendNetworkRequest,
+ openNodeInInspector,
+};
diff --git a/devtools/client/webconsole/actions/ui.js b/devtools/client/webconsole/actions/ui.js
new file mode 100644
index 0000000000..a8ffaee0f3
--- /dev/null
+++ b/devtools/client/webconsole/actions/ui.js
@@ -0,0 +1,247 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ getAllPrefs,
+} = require("resource://devtools/client/webconsole/selectors/prefs.js");
+const {
+ getAllUi,
+} = require("resource://devtools/client/webconsole/selectors/ui.js");
+const {
+ getMessage,
+} = require("resource://devtools/client/webconsole/selectors/messages.js");
+
+const {
+ INITIALIZE,
+ PERSIST_TOGGLE,
+ PREFS,
+ REVERSE_SEARCH_INPUT_TOGGLE,
+ SELECT_NETWORK_MESSAGE_TAB,
+ SHOW_OBJECT_IN_SIDEBAR,
+ SIDEBAR_CLOSE,
+ SPLIT_CONSOLE_CLOSE_BUTTON_TOGGLE,
+ TIMESTAMPS_TOGGLE,
+ WARNING_GROUPS_TOGGLE,
+ FILTERBAR_DISPLAY_MODE_SET,
+ EDITOR_TOGGLE,
+ EDITOR_SET_WIDTH,
+ EDITOR_ONBOARDING_DISMISS,
+ EAGER_EVALUATION_TOGGLE,
+ AUTOCOMPLETE_TOGGLE,
+ ENABLE_NETWORK_MONITORING,
+} = require("resource://devtools/client/webconsole/constants.js");
+
+function openLink(url, e) {
+ return ({ hud }) => {
+ return hud.openLink(url, e);
+ };
+}
+
+function persistToggle() {
+ return ({ dispatch, getState, prefsService }) => {
+ dispatch({
+ type: PERSIST_TOGGLE,
+ });
+ const uiState = getAllUi(getState());
+ prefsService.setBoolPref(PREFS.UI.PERSIST, uiState.persistLogs);
+ };
+}
+
+function networkMonitoringToggle() {
+ return ({ dispatch, getState, prefsService, webConsoleUI }) => {
+ dispatch({ type: ENABLE_NETWORK_MONITORING });
+ const uiState = getAllUi(getState());
+
+ prefsService.setBoolPref(
+ PREFS.UI.ENABLE_NETWORK_MONITORING,
+ uiState.enableNetworkMonitoring
+ );
+
+ if (uiState.enableNetworkMonitoring) {
+ webConsoleUI.startWatchingNetworkResources();
+ } else {
+ webConsoleUI.stopWatchingNetworkResources();
+ }
+ };
+}
+
+function timestampsToggle() {
+ return ({ dispatch, getState, prefsService }) => {
+ dispatch({
+ type: TIMESTAMPS_TOGGLE,
+ });
+ const uiState = getAllUi(getState());
+ prefsService.setBoolPref(
+ PREFS.UI.MESSAGE_TIMESTAMP,
+ uiState.timestampsVisible
+ );
+ };
+}
+
+function autocompleteToggle() {
+ return ({ dispatch, getState, prefsService }) => {
+ dispatch({
+ type: AUTOCOMPLETE_TOGGLE,
+ });
+ const prefsState = getAllPrefs(getState());
+ prefsService.setBoolPref(
+ PREFS.FEATURES.AUTOCOMPLETE,
+ prefsState.autocomplete
+ );
+ };
+}
+
+function warningGroupsToggle() {
+ return ({ dispatch, getState, prefsService }) => {
+ dispatch({
+ type: WARNING_GROUPS_TOGGLE,
+ });
+ const prefsState = getAllPrefs(getState());
+ prefsService.setBoolPref(
+ PREFS.FEATURES.GROUP_WARNINGS,
+ prefsState.groupWarnings
+ );
+ };
+}
+
+function eagerEvaluationToggle() {
+ return ({ dispatch, getState, prefsService }) => {
+ dispatch({
+ type: EAGER_EVALUATION_TOGGLE,
+ });
+ const prefsState = getAllPrefs(getState());
+ prefsService.setBoolPref(
+ PREFS.FEATURES.EAGER_EVALUATION,
+ prefsState.eagerEvaluation
+ );
+ };
+}
+
+function selectNetworkMessageTab(id) {
+ return {
+ type: SELECT_NETWORK_MESSAGE_TAB,
+ id,
+ };
+}
+
+function initialize() {
+ return {
+ type: INITIALIZE,
+ };
+}
+
+function sidebarClose() {
+ return {
+ type: SIDEBAR_CLOSE,
+ };
+}
+
+function splitConsoleCloseButtonToggle(shouldDisplayButton) {
+ return {
+ type: SPLIT_CONSOLE_CLOSE_BUTTON_TOGGLE,
+ shouldDisplayButton,
+ };
+}
+
+function editorToggle() {
+ return ({ dispatch, getState, prefsService }) => {
+ dispatch({
+ type: EDITOR_TOGGLE,
+ });
+ const uiState = getAllUi(getState());
+ prefsService.setBoolPref(PREFS.UI.EDITOR, uiState.editor);
+ };
+}
+
+function editorOnboardingDismiss() {
+ return ({ dispatch, prefsService }) => {
+ dispatch({
+ type: EDITOR_ONBOARDING_DISMISS,
+ });
+ prefsService.setBoolPref(PREFS.UI.EDITOR_ONBOARDING, false);
+ };
+}
+
+function setEditorWidth(width) {
+ return ({ dispatch, prefsService }) => {
+ dispatch({
+ type: EDITOR_SET_WIDTH,
+ width,
+ });
+ prefsService.setIntPref(PREFS.UI.EDITOR_WIDTH, width);
+ };
+}
+
+/**
+ * Dispatches a SHOW_OBJECT_IN_SIDEBAR action, with a grip property corresponding to the
+ * {actor} parameter in the {messageId} message.
+ *
+ * @param {String} actorID: Actor id of the object we want to place in the sidebar.
+ * @param {String} messageId: id of the message containing the {actor} parameter.
+ */
+function showMessageObjectInSidebar(actorID, messageId) {
+ return ({ dispatch, getState }) => {
+ const { parameters } = getMessage(getState(), messageId);
+ if (Array.isArray(parameters)) {
+ for (const parameter of parameters) {
+ if (parameter && parameter.actorID === actorID) {
+ dispatch(showObjectInSidebar(parameter));
+ return;
+ }
+ }
+ }
+ };
+}
+
+function showObjectInSidebar(front) {
+ return {
+ type: SHOW_OBJECT_IN_SIDEBAR,
+ front,
+ };
+}
+
+function reverseSearchInputToggle({ initialValue, access } = {}) {
+ return {
+ type: REVERSE_SEARCH_INPUT_TOGGLE,
+ initialValue,
+ access,
+ };
+}
+
+function filterBarDisplayModeSet(displayMode) {
+ return {
+ type: FILTERBAR_DISPLAY_MODE_SET,
+ displayMode,
+ };
+}
+
+function openSidebar(messageId, rootActorId) {
+ return ({ dispatch }) => {
+ dispatch(showMessageObjectInSidebar(rootActorId, messageId));
+ };
+}
+
+module.exports = {
+ eagerEvaluationToggle,
+ editorOnboardingDismiss,
+ editorToggle,
+ filterBarDisplayModeSet,
+ initialize,
+ persistToggle,
+ reverseSearchInputToggle,
+ selectNetworkMessageTab,
+ setEditorWidth,
+ showMessageObjectInSidebar,
+ showObjectInSidebar,
+ sidebarClose,
+ splitConsoleCloseButtonToggle,
+ timestampsToggle,
+ networkMonitoringToggle,
+ warningGroupsToggle,
+ openLink,
+ openSidebar,
+ autocompleteToggle,
+};