Summary: - Upgrades to build-related packages: - Upgrade typescript, related libraries and typings. - Upgrade webpack, eslint; add tsc-watch, node-dev, eslint_d. - Build organization changes: - Build webpack from original typescript, transpiling only; with errors still reported by a background tsc watching process. - Typescript-related changes: - Reduce imports of AWS dependencies (very noticeable speedup) - Avoid auto-loading global @types - Client code is now built with isolatedModules flag (for safe transpilation) - Use allowJs to avoid copying JS files manually. - Linting changes - Enhance Arcanist ESLintLinter to run before/after commands, and set up to use eslint_d - Update eslint config, and include .eslintignore to avoid linting generated files. - Include a bunch of eslint-prompted and eslint-generated fixes - Add no-unused-expression rule to eslint, and fix a few warnings about it - Other items: - Refactor cssInput to avoid circular dependency - Remove a bit of unused code, libraries, dependencies Test Plan: No behavior changes, all existing tests pass. There are 30 tests fewer reported because `test_gpath.py` was removed (it's been unused for years) Reviewers: paulfitz Reviewed By: paulfitz Subscribers: paulfitz Differential Revision: https://phab.getgrist.com/D3498pull/214/head
parent
64ff9ccd0a
commit
dd2eadc86e
@ -1,90 +0,0 @@
|
|||||||
/**
|
|
||||||
* koSession offers observables whose values are tied to the browser session or history:
|
|
||||||
*
|
|
||||||
* sessionValue(key) - an observable preserved across history entries and reloads.
|
|
||||||
*
|
|
||||||
* Note: we could also support "browserValue", shared across all tabs and across browser restarts
|
|
||||||
* (same as sessionValue but using window.localStorage), but it seems more appropriate to store
|
|
||||||
* such values on the server.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/* global window, $ */
|
|
||||||
|
|
||||||
var _ = require('underscore');
|
|
||||||
var ko = require('knockout');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Maps a string key to an observable. The space of keys is shared for all kinds of observables,
|
|
||||||
* and they differ only in where they store their state. Each observable gets several extra
|
|
||||||
* properties:
|
|
||||||
* @property {String} ksKey The key used for storage. It should be unique across koSession values.
|
|
||||||
* @property {Object} ksDefault The default value if the storage doesn't have one.
|
|
||||||
* @property {Function} ksFetch The method to fetch the value from storage.
|
|
||||||
* @property {Function} ksSave The method to save the value to storage.
|
|
||||||
*/
|
|
||||||
var _sessionValues = {};
|
|
||||||
|
|
||||||
function createObservable(key, defaultValue, methods) {
|
|
||||||
var obs = _sessionValues[key];
|
|
||||||
if (!obs) {
|
|
||||||
_sessionValues[key] = obs = ko.observable();
|
|
||||||
obs.ksKey = key;
|
|
||||||
obs.ksDefaultValue = defaultValue;
|
|
||||||
obs.ksFetch = methods.fetch;
|
|
||||||
obs.ksSave = methods.save;
|
|
||||||
obs.dispose = methods.dispose;
|
|
||||||
|
|
||||||
// We initialize the observable before setting rateLimit, to ensure that the initialization
|
|
||||||
// doesn't end up triggering subscribers that are about to be added (which seems to be a bit
|
|
||||||
// of a problem with rateLimit extender, and possibly deferred). This workaround relies on the
|
|
||||||
// fact that the extender modifies its target without creating a new one.
|
|
||||||
obs(obs.ksFetch());
|
|
||||||
obs.extend({deferred: true});
|
|
||||||
|
|
||||||
obs.subscribe(function(newValue) {
|
|
||||||
if (newValue !== this.ksFetch()) {
|
|
||||||
console.log("koSession: %s changed %s -> %s", this.ksKey, this.ksFetch(), newValue);
|
|
||||||
this.ksSave(newValue);
|
|
||||||
}
|
|
||||||
}, obs);
|
|
||||||
}
|
|
||||||
return obs;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an observable whose value sticks across reloads and navigation, but is different for
|
|
||||||
* different browser tabs. E.g. it may be used to reflect whether a side pane is open.
|
|
||||||
* The `key` isn't visible to the user, so pick any unique string name.
|
|
||||||
*/
|
|
||||||
function sessionValue(key, optDefault) {
|
|
||||||
return createObservable(key, optDefault, sessionValueMethods);
|
|
||||||
}
|
|
||||||
exports.sessionValue = sessionValue;
|
|
||||||
|
|
||||||
var sessionValueMethods = {
|
|
||||||
'fetch': function() {
|
|
||||||
var value = window.sessionStorage.getItem(this.ksKey);
|
|
||||||
if (!value) {
|
|
||||||
return this.ksDefaultValue;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return JSON.parse(value);
|
|
||||||
} catch (e) {
|
|
||||||
return this.ksDefaultValue;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'save': function(value) {
|
|
||||||
window.sessionStorage.setItem(this.ksKey, JSON.stringify(value));
|
|
||||||
},
|
|
||||||
'dispose': function(value) {
|
|
||||||
window.sessionStorage.removeItem(this.ksKey);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function onApplyState() {
|
|
||||||
_.each(_sessionValues, function(obs, key) {
|
|
||||||
obs(obs.ksFetch());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
$(window).on('applyState', onApplyState);
|
|
@ -0,0 +1,12 @@
|
|||||||
|
import {colors, vars} from 'app/client/ui2018/cssVars';
|
||||||
|
import {styled} from 'grainjs';
|
||||||
|
|
||||||
|
export const cssInput = styled('input', `
|
||||||
|
height: 30px;
|
||||||
|
width: 100%;
|
||||||
|
font-size: ${vars.mediumFontSize};
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 5px;
|
||||||
|
border: 1px solid ${colors.darkGrey};
|
||||||
|
outline: none;
|
||||||
|
`);
|
@ -1,5 +1,6 @@
|
|||||||
export interface INotifier {
|
export interface INotifier {
|
||||||
deleteUser(userId: number): Promise<void>;
|
|
||||||
// for test purposes, check if any notifications are in progress
|
// for test purposes, check if any notifications are in progress
|
||||||
readonly testPending: boolean;
|
readonly testPending: boolean;
|
||||||
|
|
||||||
|
deleteUser(userId: number): Promise<void>;
|
||||||
}
|
}
|
||||||
|
@ -1,144 +0,0 @@
|
|||||||
from six.moves import xrange
|
|
||||||
|
|
||||||
|
|
||||||
def _is_array(obj):
|
|
||||||
return isinstance(obj, list)
|
|
||||||
|
|
||||||
def get(obj, path):
|
|
||||||
"""
|
|
||||||
Looks up and returns a path in the object. Returns None if the path isn't there.
|
|
||||||
"""
|
|
||||||
for part in path:
|
|
||||||
try:
|
|
||||||
obj = obj[part]
|
|
||||||
except(KeyError, IndexError):
|
|
||||||
return None
|
|
||||||
return obj
|
|
||||||
|
|
||||||
def glob(obj, path, func, extra_arg):
|
|
||||||
"""
|
|
||||||
Resolves wildcards in `path`, calling func for all matching paths. Returns the number of
|
|
||||||
times that func was called.
|
|
||||||
obj - An object to scan.
|
|
||||||
path - Path to an item in an object or an array in obj. May contain the special key '*', which
|
|
||||||
-- for arrays only -- means "for all indices".
|
|
||||||
func - Will be called as func(subobj, key, fullPath, extraArg).
|
|
||||||
extra_arg - An arbitrary value to pass along to func, for convenience.
|
|
||||||
Returns count of matching paths, for which func got called.
|
|
||||||
"""
|
|
||||||
return _globHelper(obj, path, path, func, extra_arg)
|
|
||||||
|
|
||||||
def _globHelper(obj, path, full_path, func, extra_arg):
|
|
||||||
for i, part in enumerate(path[:-1]):
|
|
||||||
if part == "*" and _is_array(obj):
|
|
||||||
# We got an array wildcard
|
|
||||||
subpath = path[i + 1:]
|
|
||||||
count = 0
|
|
||||||
for subobj in obj:
|
|
||||||
count += _globHelper(subobj, subpath, full_path, func, extra_arg)
|
|
||||||
return count
|
|
||||||
|
|
||||||
try:
|
|
||||||
obj = obj[part]
|
|
||||||
except:
|
|
||||||
raise Exception("gpath.glob: non-existent object at " +
|
|
||||||
describe(full_path[:len(full_path) - len(path) + i + 1]))
|
|
||||||
|
|
||||||
return func(obj, path[-1], full_path, extra_arg) or 1
|
|
||||||
|
|
||||||
def place(obj, path, value):
|
|
||||||
"""
|
|
||||||
Sets or deletes an object property in DocObj.
|
|
||||||
gpath - Path to an Object in obj.
|
|
||||||
value - Any value. Setting None will remove the selected object key.
|
|
||||||
"""
|
|
||||||
return glob(obj, path, _placeHelper, value)
|
|
||||||
|
|
||||||
def _placeHelper(subobj, key, full_path, value):
|
|
||||||
if not isinstance(subobj, dict):
|
|
||||||
raise Exception("gpath.place: not a plain object at " + describe(dirname(full_path)))
|
|
||||||
|
|
||||||
if value is not None:
|
|
||||||
subobj[key] = value
|
|
||||||
elif key in subobj:
|
|
||||||
del subobj[key]
|
|
||||||
|
|
||||||
def _checkIsArray(subobj, errPrefix, index, itemPath, isInsert):
|
|
||||||
"""
|
|
||||||
This is a helper for checking operations on arrays, and throwing descriptive errors.
|
|
||||||
"""
|
|
||||||
if subobj is None:
|
|
||||||
raise Exception(errPrefix + ": non-existent object at " + describe(dirname(itemPath)))
|
|
||||||
elif not _is_array(subobj):
|
|
||||||
raise Exception(errPrefix + ": not an array at " + describe(dirname(itemPath)))
|
|
||||||
else:
|
|
||||||
length = len(subobj)
|
|
||||||
validIndex = (isinstance(index, int) and index >= 0 and index < length)
|
|
||||||
validInsertIndex = (index is None or index == length)
|
|
||||||
if not (validIndex or (isInsert and validInsertIndex)):
|
|
||||||
raise Exception(errPrefix + ": invalid array index: " + describe(itemPath))
|
|
||||||
|
|
||||||
def insert(obj, path, value):
|
|
||||||
"""
|
|
||||||
Inserts an element into an array in DocObj.
|
|
||||||
gpath - Path to an item in an array in obj.
|
|
||||||
The new value will be inserted before the item pointed to by gpath.
|
|
||||||
The last component of gpath may be null, in which case the value is appended at the end.
|
|
||||||
value - Any value.
|
|
||||||
"""
|
|
||||||
return glob(obj, path, _insertHelper, value)
|
|
||||||
|
|
||||||
def _insertHelper(subobj, index, fullPath, value):
|
|
||||||
_checkIsArray(subobj, "gpath.insert", index, fullPath, True)
|
|
||||||
if index is None:
|
|
||||||
subobj.append(value)
|
|
||||||
else:
|
|
||||||
subobj.insert(index, value)
|
|
||||||
|
|
||||||
def update(obj, path, value):
|
|
||||||
"""
|
|
||||||
Updates an element in an array in DocObj.
|
|
||||||
gpath - Path to an item in an array in obj.
|
|
||||||
value - Any value.
|
|
||||||
"""
|
|
||||||
return glob(obj, path, _updateHelper, value)
|
|
||||||
|
|
||||||
def _updateHelper(subobj, index, fullPath, value):
|
|
||||||
if index == '*':
|
|
||||||
_checkIsArray(subobj, "gpath.update", None, fullPath, True)
|
|
||||||
for i in xrange(len(subobj)):
|
|
||||||
subobj[i] = value
|
|
||||||
return len(subobj)
|
|
||||||
else:
|
|
||||||
_checkIsArray(subobj, "gpath.update", index, fullPath, False)
|
|
||||||
subobj[index] = value
|
|
||||||
|
|
||||||
def remove(obj, path):
|
|
||||||
"""
|
|
||||||
Removes an element from an array in DocObj.
|
|
||||||
gpath - Path to an item in an array in obj.
|
|
||||||
"""
|
|
||||||
return glob(obj, path, _removeHelper, None)
|
|
||||||
|
|
||||||
def _removeHelper(subobj, index, fullPath, _):
|
|
||||||
_checkIsArray(subobj, "gpath.remove", index, fullPath, False)
|
|
||||||
del subobj[index]
|
|
||||||
|
|
||||||
|
|
||||||
def dirname(path):
|
|
||||||
"""
|
|
||||||
Returns path without the last component, like a directory name in a filesystem path.
|
|
||||||
"""
|
|
||||||
return path[:-1]
|
|
||||||
|
|
||||||
def basename(path):
|
|
||||||
"""
|
|
||||||
Returns the last component of path, like base name of a filesystem path.
|
|
||||||
"""
|
|
||||||
return path[-1] if path else None
|
|
||||||
|
|
||||||
def describe(path):
|
|
||||||
"""
|
|
||||||
Returns a human-readable representation of path.
|
|
||||||
"""
|
|
||||||
return "/" + "/".join(str(p) for p in path)
|
|
@ -1,159 +0,0 @@
|
|||||||
import unittest
|
|
||||||
import gpath
|
|
||||||
|
|
||||||
class TestGpath(unittest.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.obj = {
|
|
||||||
"foo": [{"bar": 1}, {"bar": 2}, {"baz": 3}],
|
|
||||||
"hello": "world"
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_get(self):
|
|
||||||
self.assertEqual(gpath.get(self.obj, ["foo", 0, "bar"]), 1)
|
|
||||||
self.assertEqual(gpath.get(self.obj, ["foo", 2]), {"baz": 3})
|
|
||||||
self.assertEqual(gpath.get(self.obj, ["hello"]), "world")
|
|
||||||
self.assertEqual(gpath.get(self.obj, []), self.obj)
|
|
||||||
|
|
||||||
self.assertEqual(gpath.get(self.obj, ["foo", 0, "baz"]), None)
|
|
||||||
self.assertEqual(gpath.get(self.obj, ["foo", 4]), None)
|
|
||||||
self.assertEqual(gpath.get(self.obj, ["foo", 4, "baz"]), None)
|
|
||||||
self.assertEqual(gpath.get(self.obj, [0]), None)
|
|
||||||
|
|
||||||
def test_set(self):
|
|
||||||
gpath.place(self.obj, ["foo"], {"bar": 1, "baz": 2})
|
|
||||||
self.assertEqual(self.obj["foo"], {"bar": 1, "baz": 2})
|
|
||||||
gpath.place(self.obj, ["foo", "bar"], 17)
|
|
||||||
self.assertEqual(self.obj["foo"], {"bar": 17, "baz": 2})
|
|
||||||
gpath.place(self.obj, ["foo", "baz"], None)
|
|
||||||
self.assertEqual(self.obj["foo"], {"bar": 17})
|
|
||||||
|
|
||||||
self.assertEqual(self.obj["hello"], "world")
|
|
||||||
gpath.place(self.obj, ["hello"], None)
|
|
||||||
self.assertFalse("hello" in self.obj)
|
|
||||||
gpath.place(self.obj, ["hello"], None) # OK to remove a non-existent property.
|
|
||||||
self.assertFalse("hello" in self.obj)
|
|
||||||
gpath.place(self.obj, ["hello"], "blah")
|
|
||||||
self.assertEqual(self.obj["hello"], "blah")
|
|
||||||
|
|
||||||
def test_set_strict(self):
|
|
||||||
with self.assertRaisesRegex(Exception, r"non-existent"):
|
|
||||||
gpath.place(self.obj, ["bar", 4], 17)
|
|
||||||
|
|
||||||
with self.assertRaisesRegex(Exception, r"not a plain object"):
|
|
||||||
gpath.place(self.obj, ["foo", 0], 17)
|
|
||||||
|
|
||||||
|
|
||||||
def test_insert(self):
|
|
||||||
self.assertEqual(self.obj["foo"], [{"bar": 1}, {"bar": 2}, {"baz": 3}])
|
|
||||||
gpath.insert(self.obj, ["foo", 0], "asdf")
|
|
||||||
self.assertEqual(self.obj["foo"], ["asdf", {"bar": 1}, {"bar": 2}, {"baz": 3}])
|
|
||||||
gpath.insert(self.obj, ["foo", 3], "hello")
|
|
||||||
self.assertEqual(self.obj["foo"], ["asdf", {"bar": 1}, {"bar": 2}, "hello", {"baz": 3}])
|
|
||||||
gpath.insert(self.obj, ["foo", None], "world")
|
|
||||||
self.assertEqual(self.obj["foo"],
|
|
||||||
["asdf", {"bar": 1}, {"bar": 2}, "hello", {"baz": 3}, "world"])
|
|
||||||
|
|
||||||
def test_insert_strict(self):
|
|
||||||
with self.assertRaisesRegex(Exception, r'not an array'):
|
|
||||||
gpath.insert(self.obj, ["foo"], "asdf")
|
|
||||||
|
|
||||||
with self.assertRaisesRegex(Exception, r'invalid.*index'):
|
|
||||||
gpath.insert(self.obj, ["foo", -1], 17)
|
|
||||||
|
|
||||||
with self.assertRaisesRegex(Exception, r'invalid.*index'):
|
|
||||||
gpath.insert(self.obj, ["foo", "foo"], 17)
|
|
||||||
|
|
||||||
def test_update(self):
|
|
||||||
"""update should update array items"""
|
|
||||||
self.assertEqual(self.obj["foo"], [{"bar": 1}, {"bar": 2}, {"baz": 3}])
|
|
||||||
gpath.update(self.obj, ["foo", 0], "asdf")
|
|
||||||
self.assertEqual(self.obj["foo"], ["asdf", {"bar": 2}, {"baz": 3}])
|
|
||||||
gpath.update(self.obj, ["foo", 2], "hello")
|
|
||||||
self.assertEqual(self.obj["foo"], ["asdf", {"bar": 2}, "hello"])
|
|
||||||
gpath.update(self.obj, ["foo", 1], None)
|
|
||||||
self.assertEqual(self.obj["foo"], ["asdf", None, "hello"])
|
|
||||||
|
|
||||||
def test_update_strict(self):
|
|
||||||
"""update should be strict"""
|
|
||||||
with self.assertRaisesRegex(Exception, r'non-existent'):
|
|
||||||
gpath.update(self.obj, ["bar", 4], 17)
|
|
||||||
with self.assertRaisesRegex(Exception, r'not an array'):
|
|
||||||
gpath.update(self.obj, ["foo"], 17)
|
|
||||||
with self.assertRaisesRegex(Exception, r'invalid.*index'):
|
|
||||||
gpath.update(self.obj, ["foo", -1], 17)
|
|
||||||
with self.assertRaisesRegex(Exception, r'invalid.*index'):
|
|
||||||
gpath.update(self.obj, ["foo", None], 17)
|
|
||||||
|
|
||||||
def test_remove(self):
|
|
||||||
"""remove should remove indices"""
|
|
||||||
self.assertEqual(self.obj["foo"], [{"bar": 1}, {"bar": 2}, {"baz": 3}])
|
|
||||||
gpath.remove(self.obj, ["foo", 0])
|
|
||||||
self.assertEqual(self.obj["foo"], [{"bar": 2}, {"baz": 3}])
|
|
||||||
gpath.remove(self.obj, ["foo", 1])
|
|
||||||
self.assertEqual(self.obj["foo"], [{"bar": 2}])
|
|
||||||
gpath.remove(self.obj, ["foo", 0])
|
|
||||||
self.assertEqual(self.obj["foo"], [])
|
|
||||||
|
|
||||||
def test_remove_strict(self):
|
|
||||||
"""remove should be strict"""
|
|
||||||
with self.assertRaisesRegex(Exception, r'non-existent'):
|
|
||||||
gpath.remove(self.obj, ["bar", 4])
|
|
||||||
with self.assertRaisesRegex(Exception, r'not an array'):
|
|
||||||
gpath.remove(self.obj, ["foo"])
|
|
||||||
with self.assertRaisesRegex(Exception, r'invalid.*index'):
|
|
||||||
gpath.remove(self.obj, ["foo", -1])
|
|
||||||
with self.assertRaisesRegex(Exception, r'invalid.*index'):
|
|
||||||
gpath.remove(self.obj, ["foo", None])
|
|
||||||
|
|
||||||
def test_glob(self):
|
|
||||||
"""glob should scan arrays"""
|
|
||||||
self.assertEqual(self.obj["foo"], [{"bar": 1}, {"bar": 2}, {"baz": 3}])
|
|
||||||
|
|
||||||
self.assertEqual(gpath.place(self.obj, ["foo", "*", "bar"], 17), 3)
|
|
||||||
self.assertEqual(self.obj["foo"], [{"bar": 17}, {"bar": 17}, {"baz": 3, "bar": 17}])
|
|
||||||
|
|
||||||
with self.assertRaisesRegex(Exception, r'non-existent object at \/foo\/\*\/bad'):
|
|
||||||
gpath.place(self.obj, ["foo", "*", "bad", "test"], 10)
|
|
||||||
|
|
||||||
self.assertEqual(gpath.update(self.obj, ["foo", "*"], "hello"), 3)
|
|
||||||
self.assertEqual(self.obj["foo"], ["hello", "hello", "hello"])
|
|
||||||
|
|
||||||
def test_glob_strict_wildcard(self):
|
|
||||||
"""should only support tail wildcard for updates"""
|
|
||||||
with self.assertRaisesRegex(Exception, r'invalid array index'):
|
|
||||||
gpath.remove(self.obj, ["foo", "*"])
|
|
||||||
with self.assertRaisesRegex(Exception, r'invalid array index'):
|
|
||||||
gpath.insert(self.obj, ["foo", "*"], 1)
|
|
||||||
|
|
||||||
def test_glob_wildcard_keys(self):
|
|
||||||
"""should not scan object keys"""
|
|
||||||
self.assertEqual(self.obj["foo"], [{"bar": 1}, {"bar": 2}, {"baz": 3}])
|
|
||||||
|
|
||||||
self.assertEqual(gpath.place(self.obj, ["foo", 0, "*"], 17), 1)
|
|
||||||
self.assertEqual(self.obj["foo"], [{"bar": 1, '*': 17}, {"bar": 2}, {"baz": 3}])
|
|
||||||
|
|
||||||
with self.assertRaisesRegex(Exception, r'non-existent'):
|
|
||||||
gpath.place(self.obj, ["*", 0, "bar"], 17)
|
|
||||||
|
|
||||||
def test_glob_nested(self):
|
|
||||||
"""should scan nested arrays"""
|
|
||||||
self.obj = [{"a": [1,2,3]}, {"a": [4,5,6]}, {"a": [7,8,9]}]
|
|
||||||
self.assertEqual(gpath.update(self.obj, ["*", "a", "*"], 5), 9)
|
|
||||||
self.assertEqual(self.obj, [{"a": [5,5,5]}, {"a": [5,5,5]}, {"a": [5,5,5]}])
|
|
||||||
|
|
||||||
def test_dirname(self):
|
|
||||||
"""dirname should return path without last component"""
|
|
||||||
self.assertEqual(gpath.dirname(["foo", "bar", "baz"]), ["foo", "bar"])
|
|
||||||
self.assertEqual(gpath.dirname([1, 2]), [1])
|
|
||||||
self.assertEqual(gpath.dirname(["foo"]), [])
|
|
||||||
self.assertEqual(gpath.dirname([]), [])
|
|
||||||
|
|
||||||
def test_basename(self):
|
|
||||||
"""basename should return the last component of path"""
|
|
||||||
self.assertEqual(gpath.basename(["foo", "bar", "baz"]), "baz")
|
|
||||||
self.assertEqual(gpath.basename([1, 2]), 2)
|
|
||||||
self.assertEqual(gpath.basename(["foo"]), "foo")
|
|
||||||
self.assertEqual(gpath.basename([]), None)
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main()
|
|
Loading…
Reference in new issue