mirror of
https://github.com/jupyter/notebook.git
synced 2025-04-12 14:00:27 +08:00
Merge pull request #1347 from Carreau/shortcut-editor-2
Create a shortcut editor for the notebook.
This commit is contained in:
commit
d8fc95173b
5
.eslintignore
Normal file
5
.eslintignore
Normal file
@ -0,0 +1,5 @@
|
||||
*.min.js
|
||||
*components*
|
||||
*node_modules*
|
||||
*built*
|
||||
*build*
|
13
.eslintrc.json
Normal file
13
.eslintrc.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 6,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"rules": {
|
||||
"semi": 1,
|
||||
"no-cond-assign": 2,
|
||||
"no-debugger": 2,
|
||||
"comma-dangle": 1,
|
||||
"no-unreachable" : 2
|
||||
}
|
||||
}
|
@ -36,6 +36,7 @@ install:
|
||||
|
||||
script:
|
||||
- 'if [[ $GROUP == js* ]]; then travis_retry python -m notebook.jstest ${GROUP:3}; fi'
|
||||
- 'if [[ $GROUP == "js/notebook" ]]; then npm run lint; fi'
|
||||
- 'if [[ $GROUP == python ]]; then nosetests -v --with-coverage --cover-package=notebook notebook; fi'
|
||||
|
||||
matrix:
|
||||
|
@ -231,10 +231,21 @@ define([
|
||||
}
|
||||
return dct;
|
||||
};
|
||||
|
||||
|
||||
ShortcutManager.prototype.get_action_shortcuts = function(name){
|
||||
var ftree = flatten_shorttree(this._shortcuts);
|
||||
var res = [];
|
||||
for (var sht in ftree ){
|
||||
if(ftree[sht] === name){
|
||||
res.push(sht);
|
||||
}
|
||||
}
|
||||
return res;
|
||||
};
|
||||
|
||||
ShortcutManager.prototype.get_action_shortcut = function(name){
|
||||
var ftree = flatten_shorttree(this._shortcuts);
|
||||
var res = {};
|
||||
for (var sht in ftree ){
|
||||
if(ftree[sht] === name){
|
||||
return sht;
|
||||
@ -405,25 +416,27 @@ define([
|
||||
|
||||
shortcut = shortcut.toLowerCase();
|
||||
this.remove_shortcut(shortcut);
|
||||
var patch = {keys:{}};
|
||||
var b = {bind:{}};
|
||||
const patch = {keys:{}};
|
||||
const b = {bind:{}};
|
||||
patch.keys[this._mode] = {bind:{}};
|
||||
patch.keys[this._mode].bind[shortcut] = null;
|
||||
this._config.update(patch);
|
||||
|
||||
// if the shortcut we unbind is a default one, we add it to the list of
|
||||
// things to unbind at startup
|
||||
if( this._defaults_bindings.indexOf(shortcut) !== -1 ){
|
||||
const cnf = (this._config.data.keys||{})[this._mode];
|
||||
const unbind_array = cnf.unbind||[];
|
||||
|
||||
if(this._defaults_bindings.indexOf(shortcut) !== -1){
|
||||
var cnf = (this._config.data.keys||{})[this._mode];
|
||||
var unbind_array = cnf.unbind||[];
|
||||
|
||||
// unless it's already there (like if we have remapped a default
|
||||
// shortcut to another command, and unbind it)
|
||||
if(unbind_array.indexOf(shortcut) !== -1){
|
||||
unbind_array.concat(shortcut);
|
||||
var unbind_patch = {keys:{unbind:unbind_array}};
|
||||
this._config._update(unbind_patch);
|
||||
// shortcut to another command): unbind it)
|
||||
if(unbind_array.indexOf(shortcut) === -1){
|
||||
const _parray = unbind_array.concat(shortcut);
|
||||
const unbind_patch = {keys:{}};
|
||||
unbind_patch.keys[this._mode] = {unbind:_parray}
|
||||
console.warn('up:', unbind_patch);
|
||||
this._config.update(unbind_patch);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -62,6 +62,12 @@ define(function(require){
|
||||
*
|
||||
**/
|
||||
var _actions = {
|
||||
'edit-command-mode-keyboard-shortcuts': {
|
||||
help: 'Open a dialog to edit the command mode keyboard shortcuts',
|
||||
handler: function (env) {
|
||||
env.notebook.show_shortcuts_editor();
|
||||
}
|
||||
},
|
||||
'restart-kernel': {
|
||||
help: 'restart the kernel (no confirmation dialog)',
|
||||
handler: function (env) {
|
||||
|
@ -2,6 +2,25 @@
|
||||
// Distributed under the terms of the Modified BSD License.
|
||||
__webpack_public_path__ = window['staticURL'] + 'notebook/js/built/';
|
||||
|
||||
// adapted from Mozilla Developer Network example at
|
||||
// https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Function/bind
|
||||
// shim `bind` for testing under casper.js
|
||||
var bind = function bind(obj) {
|
||||
var slice = [].slice;
|
||||
var args = slice.call(arguments, 1),
|
||||
self = this,
|
||||
nop = function() {
|
||||
},
|
||||
bound = function() {
|
||||
return self.apply(this instanceof nop ? this : (obj || {}), args.concat(slice.call(arguments)));
|
||||
};
|
||||
nop.prototype = this.prototype || {}; // Firefox cries sometimes if prototype is undefined
|
||||
bound.prototype = new nop();
|
||||
return bound;
|
||||
};
|
||||
Function.prototype.bind = Function.prototype.bind || bind ;
|
||||
|
||||
|
||||
requirejs(['contents'], function(contentsModule) {
|
||||
require([
|
||||
'base/js/namespace',
|
||||
|
@ -4,31 +4,31 @@
|
||||
/**
|
||||
* @module notebook
|
||||
*/
|
||||
define(function (require) {
|
||||
"use strict";
|
||||
var IPython = require('base/js/namespace');
|
||||
var _ = require('underscore');
|
||||
var utils = require('base/js/utils');
|
||||
var dialog = require('base/js/dialog');
|
||||
var cellmod = require('notebook/js/cell');
|
||||
var textcell = require('notebook/js/textcell');
|
||||
var codecell = require('notebook/js/codecell');
|
||||
var moment = require('moment');
|
||||
var configmod = require('services/config');
|
||||
var session = require('services/sessions/session');
|
||||
var celltoolbar = require('notebook/js/celltoolbar');
|
||||
var marked = require('components/marked/lib/marked');
|
||||
var CodeMirror = require('codemirror/lib/codemirror');
|
||||
var runMode = require('codemirror/addon/runmode/runmode');
|
||||
var mathjaxutils = require('notebook/js/mathjaxutils');
|
||||
var keyboard = require('base/js/keyboard');
|
||||
var tooltip = require('notebook/js/tooltip');
|
||||
var default_celltoolbar = require('notebook/js/celltoolbarpresets/default');
|
||||
var rawcell_celltoolbar = require('notebook/js/celltoolbarpresets/rawcell');
|
||||
var slideshow_celltoolbar = require('notebook/js/celltoolbarpresets/slideshow');
|
||||
var attachments_celltoolbar = require('notebook/js/celltoolbarpresets/attachments');
|
||||
var scrollmanager = require('notebook/js/scrollmanager');
|
||||
var commandpalette = require('notebook/js/commandpalette');
|
||||
"use strict";
|
||||
import IPython from 'base/js/namespace';
|
||||
import _ from 'underscore';
|
||||
import utils from 'base/js/utils';
|
||||
import dialog from 'base/js/dialog';
|
||||
import cellmod from 'notebook/js/cell';
|
||||
import textcell from 'notebook/js/textcell';
|
||||
import codecell from 'notebook/js/codecell';
|
||||
import moment from 'moment';
|
||||
import configmod from 'services/config';
|
||||
import session from 'services/sessions/session';
|
||||
import celltoolbar from 'notebook/js/celltoolbar';
|
||||
import marked from 'components/marked/lib/marked';
|
||||
import CodeMirror from 'codemirror/lib/codemirror';
|
||||
import runMode from 'codemirror/addon/runmode/runmode';
|
||||
import mathjaxutils from 'notebook/js/mathjaxutils';
|
||||
import keyboard from 'base/js/keyboard';
|
||||
import tooltip from 'notebook/js/tooltip';
|
||||
import default_celltoolbar from 'notebook/js/celltoolbarpresets/default';
|
||||
import rawcell_celltoolbar from 'notebook/js/celltoolbarpresets/rawcell';
|
||||
import slideshow_celltoolbar from 'notebook/js/celltoolbarpresets/slideshow';
|
||||
import attachments_celltoolbar from 'notebook/js/celltoolbarpresets/attachments';
|
||||
import scrollmanager from 'notebook/js/scrollmanager';
|
||||
import commandpalette from 'notebook/js/commandpalette';
|
||||
import {ShortcutEditor} from 'notebook/js/shortcuteditor';
|
||||
|
||||
var _SOFT_SELECTION_CLASS = 'jupyter-soft-selected';
|
||||
|
||||
@ -50,7 +50,7 @@ define(function (require) {
|
||||
* @param {string} options.notebook_path
|
||||
* @param {string} options.notebook_name
|
||||
*/
|
||||
var Notebook = function (selector, options) {
|
||||
export function Notebook(selector, options) {
|
||||
this.config = options.config;
|
||||
this.class_config = new configmod.ConfigWithDefaults(this.config,
|
||||
Notebook.options_default, 'Notebook');
|
||||
@ -362,6 +362,10 @@ define(function (require) {
|
||||
var x = new commandpalette.CommandPalette(this);
|
||||
};
|
||||
|
||||
Notebook.prototype.show_shortcuts_editor = function() {
|
||||
new ShortcutEditor(this);
|
||||
};
|
||||
|
||||
/**
|
||||
* Trigger a warning dialog about missing functionality from newer minor versions
|
||||
*/
|
||||
@ -3160,5 +3164,3 @@ define(function (require) {
|
||||
this.load_notebook(this.notebook_path);
|
||||
};
|
||||
|
||||
return {'Notebook': Notebook};
|
||||
});
|
||||
|
173
notebook/static/notebook/js/shortcuteditor.js
Normal file
173
notebook/static/notebook/js/shortcuteditor.js
Normal file
@ -0,0 +1,173 @@
|
||||
// Copyright (c) Jupyter Development Team.
|
||||
// Distributed under the terms of the Modified BSD License.
|
||||
|
||||
|
||||
import QH from "notebook/js/quickhelp";
|
||||
import dialog from "base/js/dialog";
|
||||
import {render} from "preact";
|
||||
import {createElement, createClass} from "preact-compat";
|
||||
import marked from 'components/marked/lib/marked';
|
||||
|
||||
/**
|
||||
* Humanize the action name to be consumed by user.
|
||||
* internally the actions name are of the form
|
||||
* <namespace>:<description-with-dashes>
|
||||
* we drop <namespace> and replace dashes for space.
|
||||
*/
|
||||
const humanize_action_id = function(str) {
|
||||
return str.split(':')[1].replace(/-/g, ' ').replace(/_/g, '-');
|
||||
};
|
||||
|
||||
/**
|
||||
* given an action id return 'command-shortcut', 'edit-shortcut' or 'no-shortcut'
|
||||
* for the action. This allows us to tag UI in order to visually distinguish
|
||||
* Wether an action have a keybinding or not.
|
||||
**/
|
||||
|
||||
const KeyBinding = createClass({
|
||||
displayName: 'KeyBindings',
|
||||
getInitialState: function() {
|
||||
return {shrt:''};
|
||||
},
|
||||
handleShrtChange: function (element){
|
||||
this.setState({shrt:element.target.value});
|
||||
},
|
||||
render: function(){
|
||||
const that = this;
|
||||
const available = this.props.available(this.state.shrt);
|
||||
const empty = (this.state.shrt === '');
|
||||
return createElement('div', {className:'jupyter-keybindings'},
|
||||
createElement('i', {className: "pull-right fa fa-plus", alt: 'add-keyboard-shortcut',
|
||||
onClick:()=>{
|
||||
available?that.props.onAddBindings(that.state.shrt, that.props.ckey):null;
|
||||
that.state.shrt='';
|
||||
}
|
||||
}),
|
||||
createElement('input', {
|
||||
type:'text',
|
||||
placeholder:'add shortcut',
|
||||
className:'pull-right'+((available||empty)?'':' alert alert-danger'),
|
||||
value:this.state.shrt,
|
||||
onChange:this.handleShrtChange
|
||||
}),
|
||||
this.props.shortcuts?this.props.shortcuts.map((item, index) => {
|
||||
return createElement('span', {className: 'pull-right'},
|
||||
createElement('kbd', {}, [
|
||||
item.h,
|
||||
createElement('i', {className: "fa fa-times", alt: 'remove '+item.h,
|
||||
onClick:()=>{
|
||||
that.props.unbind(item.raw);
|
||||
}
|
||||
})
|
||||
])
|
||||
);
|
||||
}):null,
|
||||
createElement('div', {title: '(' +this.props.ckey + ')' , className:'jupyter-keybindings-text'}, this.props.display )
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const KeyBindingList = createClass({
|
||||
displayName: 'KeyBindingList',
|
||||
getInitialState: function(){
|
||||
return {data:[]};
|
||||
},
|
||||
componentDidMount: function(){
|
||||
this.setState({data:this.props.callback()});
|
||||
},
|
||||
render: function() {
|
||||
const childrens = this.state.data.map((binding)=>{
|
||||
return createElement(KeyBinding, Object.assign({}, binding, {onAddBindings:(shortcut, action)=>{
|
||||
this.props.bind(shortcut, action);
|
||||
this.setState({data:this.props.callback()});
|
||||
},
|
||||
available:this.props.available,
|
||||
unbind: (shortcut) => {
|
||||
this.props.unbind(shortcut);
|
||||
this.setState({data:this.props.callback()});
|
||||
}
|
||||
}));
|
||||
});
|
||||
childrens.unshift(createElement('div', {className:'well', key:'disclamer', dangerouslySetInnerHTML:
|
||||
{__html:
|
||||
marked(
|
||||
|
||||
"This dialog allows you to modify the keymap of the command mode, and persist the changes. "+
|
||||
"You can define many type of shorctuts and sequences of keys. "+
|
||||
"\n\n"+
|
||||
" - Use dashes `-` to represent keys that should be pressed with modifiers, "+
|
||||
"for example `Shift-a`, or `Ctrl-;`. \n"+
|
||||
" - Separate by commas if the keys need to be pressed in sequence: `h,a,l,t`.\n"+
|
||||
"\n\nYou can combine the two: `Ctrl-x,Meta-c,Meta-b,u,t,t,e,r,f,l,y`.\n"+
|
||||
"Casing will have no effects: (e.g: `;` and `:` are the same on english keyboards)."+
|
||||
" You need to explicitelty write the `Shift` modifier.\n"+
|
||||
"Valid modifiers are `Cmd`, `Ctrl`, `Alt` ,`Meta`, `Cmdtrl`. Refer to developper docs "+
|
||||
"for their signification depending on the platform."
|
||||
)}
|
||||
}));
|
||||
return createElement('div',{}, childrens);
|
||||
}
|
||||
});
|
||||
|
||||
const get_shortcuts_data = function(notebook) {
|
||||
const actions = Object.keys(notebook.keyboard_manager.actions._actions);
|
||||
const src = [];
|
||||
|
||||
for (let i = 0; i < actions.length; i++) {
|
||||
const action_id = actions[i];
|
||||
const action = notebook.keyboard_manager.actions.get(action_id);
|
||||
|
||||
let shortcuts = notebook.keyboard_manager.command_shortcuts.get_action_shortcuts(action_id);
|
||||
let hshortcuts;
|
||||
if (shortcuts.length > 0) {
|
||||
hshortcuts = shortcuts.map((raw)=>{
|
||||
return {h:QH._humanize_sequence(raw),raw:raw};}
|
||||
);
|
||||
}
|
||||
src.push({
|
||||
display: humanize_action_id(action_id),
|
||||
shortcuts: hshortcuts,
|
||||
key:action_id, // react specific thing
|
||||
ckey: action_id
|
||||
});
|
||||
}
|
||||
return src;
|
||||
};
|
||||
|
||||
|
||||
export const ShortcutEditor = function(notebook) {
|
||||
|
||||
if(!notebook){
|
||||
throw new Error("CommandPalette takes a notebook non-null mandatory arguement");
|
||||
}
|
||||
|
||||
const body = $('<div>');
|
||||
const mod = dialog.modal({
|
||||
notebook: notebook,
|
||||
keyboard_manager: notebook.keyboard_manager,
|
||||
title : "Edit Command mode Shortcuts",
|
||||
body : body,
|
||||
buttons : {
|
||||
OK : {}
|
||||
}
|
||||
});
|
||||
|
||||
const src = get_shortcuts_data(notebook);
|
||||
|
||||
mod.addClass("modal_stretch");
|
||||
|
||||
mod.modal('show');
|
||||
render(
|
||||
createElement(KeyBindingList, {
|
||||
callback:()=>{ return get_shortcuts_data(notebook);},
|
||||
bind: (shortcut, command) => {
|
||||
return notebook.keyboard_manager.command_shortcuts._persist_shortcut(shortcut, command);
|
||||
},
|
||||
unbind: (shortcut) => {
|
||||
return notebook.keyboard_manager.command_shortcuts._persist_remove_shortcut(shortcut);
|
||||
},
|
||||
available: (shrt) => { return notebook.keyboard_manager.command_shortcuts.is_available_shortcut(shrt);}
|
||||
}),
|
||||
body.get(0)
|
||||
);
|
||||
};
|
@ -99,3 +99,19 @@ kbd {
|
||||
padding-top: 1px;
|
||||
padding-bottom: 1px;
|
||||
}
|
||||
|
||||
.jupyter-keybindings {
|
||||
padding: 0px;
|
||||
line-height: 24px;
|
||||
border-bottom: 1px solid gray;
|
||||
}
|
||||
|
||||
.jupyter-keybindings input {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.jupyter-keybindings i {
|
||||
padding: 6px;
|
||||
}
|
||||
|
@ -2,6 +2,25 @@
|
||||
// Distributed under the terms of the Modified BSD License.
|
||||
__webpack_public_path__ = window['staticURL'] + 'tree/js/built/';
|
||||
|
||||
// adapted from Mozilla Developer Network example at
|
||||
// https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Function/bind
|
||||
// shim `bind` for testing under casper.js
|
||||
var bind = function bind(obj) {
|
||||
var slice = [].slice;
|
||||
var args = slice.call(arguments, 1),
|
||||
self = this,
|
||||
nop = function() {
|
||||
},
|
||||
bound = function() {
|
||||
return self.apply(this instanceof nop ? this : (obj || {}), args.concat(slice.call(arguments)));
|
||||
};
|
||||
nop.prototype = this.prototype || {}; // Firefox cries sometimes if prototype is undefined
|
||||
bound.prototype = new nop();
|
||||
return bound;
|
||||
};
|
||||
Function.prototype.bind = Function.prototype.bind || bind ;
|
||||
|
||||
|
||||
requirejs(['contents'], function(contents_service) {
|
||||
require([
|
||||
'base/js/namespace',
|
||||
|
@ -1,14 +1,14 @@
|
||||
casper.notebook_test(function () {
|
||||
// Note, \033 is the octal notation of \u001b
|
||||
// Note, \u001b is the unicode notation of octal \033 which is not officially in js
|
||||
var input = [
|
||||
"\033[0m[\033[0minfo\033[0m] \033[0mtext\033[0m",
|
||||
"\033[0m[\033[33mwarn\033[0m] \033[0m\tmore text\033[0m",
|
||||
"\033[0m[\033[33mwarn\033[0m] \033[0m https://some/url/to/a/file.ext\033[0m",
|
||||
"\033[0m[\033[31merror\033[0m] \033[0m\033[0m",
|
||||
"\033[0m[\033[31merror\033[0m] \033[0m\teven more text\033[0m",
|
||||
"\u001b[0m[\u001b[0minfo\u001b[0m] \u001b[0mtext\u001b[0m",
|
||||
"\u001b[0m[\u001b[33mwarn\u001b[0m] \u001b[0m\tmore text\u001b[0m",
|
||||
"\u001b[0m[\u001b[33mwarn\u001b[0m] \u001b[0m https://some/url/to/a/file.ext\u001b[0m",
|
||||
"\u001b[0m[\u001b[31merror\u001b[0m] \u001b[0m\u001b[0m",
|
||||
"\u001b[0m[\u001b[31merror\u001b[0m] \u001b[0m\teven more text\u001b[0m",
|
||||
"\u001b[?25hBuilding wheels for collected packages: scipy",
|
||||
"\x1b[38;5;28;01mtry\x1b[39;00m",
|
||||
"\033[0m[\033[31merror\033[0m] \033[0m\t\tand more more text\033[0m",
|
||||
"\u001b[0m[\u001b[31merror\u001b[0m] \u001b[0m\t\tand more more text\u001b[0m",
|
||||
"normal\x1b[43myellowbg\x1b[35mmagentafg\x1b[1mbold\x1b[49mdefaultbg\x1b[39mdefaultfg\x1b[22mnormal",
|
||||
].join("\n");
|
||||
|
||||
|
@ -9,6 +9,7 @@
|
||||
"url": "https://github.com/jupyter/notebook.git"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint --quiet notebook/",
|
||||
"bower": "bower install --allow-root --config.interactive=false",
|
||||
"build:watch": "concurrent \"npm run build:css:watch\" \"npm run build:js:watch\"",
|
||||
"build": "npm run build:css && npm run build:js",
|
||||
@ -18,17 +19,21 @@
|
||||
"build:js:watch": "npm run build:js -- --watch"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-cli": "^6.7.5",
|
||||
"babel-core": "^6.7.4",
|
||||
"babel-loader": "^6.2.4",
|
||||
"babel-preset-es2015": "^6.6.0",
|
||||
"bower": "*",
|
||||
"concurrently": "^1.0.0",
|
||||
"eslint": "^2.8.0",
|
||||
"less": "~2",
|
||||
"requirejs": "^2.1.17",
|
||||
"underscore": "^1.8.3",
|
||||
"webpack": "^1.12.13"
|
||||
},
|
||||
"dependencies": {
|
||||
"moment": "^2.8.4"
|
||||
"moment": "^2.8.4",
|
||||
"preact": "^4.5.1",
|
||||
"preact-compat": "^1.7.0"
|
||||
}
|
||||
}
|
||||
|
@ -22,7 +22,6 @@ from distutils.cmd import Command
|
||||
from distutils.version import LooseVersion
|
||||
from fnmatch import fnmatch
|
||||
from glob import glob
|
||||
from multiprocessing.pool import ThreadPool
|
||||
from subprocess import check_call, check_output
|
||||
|
||||
if sys.platform == 'win32':
|
||||
|
@ -1,6 +1,12 @@
|
||||
var _ = require('underscore');
|
||||
var path = require('path');
|
||||
var sourcemaps = 'source-map'
|
||||
|
||||
if(process.argv.indexOf('-w') !== -1 || process.argv.indexOf('-w') !== -1 ){
|
||||
console.log('watch mode detected, will switch to cheep sourcemaps')
|
||||
sourcemaps = 'eval-source-map';
|
||||
|
||||
}
|
||||
var commonConfig = {
|
||||
resolve: {
|
||||
root: [
|
||||
@ -44,7 +50,7 @@ function buildConfig(appName) {
|
||||
filename: 'main.min.js',
|
||||
path: './notebook/static/' + appName + '/js/built'
|
||||
},
|
||||
devtool: 'source-map',
|
||||
devtool: sourcemaps,
|
||||
});
|
||||
}
|
||||
|
||||
@ -61,7 +67,7 @@ module.exports = [
|
||||
path: './notebook/static/services/built',
|
||||
libraryTarget: 'amd'
|
||||
},
|
||||
devtool: 'source-map',
|
||||
devtool: sourcemaps,
|
||||
}),
|
||||
_.extend({}, commonConfig, {
|
||||
entry: './notebook/static/index.js',
|
||||
@ -70,6 +76,6 @@ module.exports = [
|
||||
path: './notebook/static/built',
|
||||
libraryTarget: 'amd'
|
||||
},
|
||||
devtool: 'source-map',
|
||||
devtool: sourcemaps,
|
||||
}),
|
||||
].map(buildConfig);
|
||||
|
Loading…
x
Reference in New Issue
Block a user