mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(core) Speed up and upgrade build.
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/D3498
This commit is contained in:
parent
64ff9ccd0a
commit
dd2eadc86e
@ -81,7 +81,7 @@ export class Cursor extends Disposable {
|
||||
this.viewData = baseView.viewData;
|
||||
|
||||
this._sectionId = this.autoDispose(ko.computed(() => baseView.viewSection.id()));
|
||||
this._rowId = ko.observable(optCursorPos.rowId || 0);
|
||||
this._rowId = ko.observable<RowId|null>(optCursorPos.rowId || 0);
|
||||
this.rowIndex = this.autoDispose(ko.computed({
|
||||
read: () => {
|
||||
if (!this._isLive()) { return this.rowIndex.peek(); }
|
||||
|
@ -93,7 +93,7 @@ export class TypeTransform extends ColumnTransform {
|
||||
const colInfo = await TypeConversion.prepTransformColInfo(docModel, this.origColumn, this.origDisplayCol, toType);
|
||||
// NOTE: We could add rules with AddColumn action, but there are some optimizations that converts array values.
|
||||
const rules = colInfo.rules;
|
||||
delete colInfo.rules;
|
||||
delete (colInfo as any).rules;
|
||||
const newColInfos = await this._tableData.sendTableActions([
|
||||
['AddColumn', 'gristHelper_Converted', {...colInfo, isFormula: false, formula: ''}],
|
||||
['AddColumn', 'gristHelper_Transform', colInfo],
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { GristDoc } from 'app/client/components/GristDoc';
|
||||
import { ViewFieldRec, ViewSectionRec } from 'app/client/models/DocModel';
|
||||
import { cssField, cssInput, cssLabel} from 'app/client/ui/MakeCopyMenu';
|
||||
import { cssInput } from 'app/client/ui/cssInput';
|
||||
import { cssField, cssLabel } from 'app/client/ui/MakeCopyMenu';
|
||||
import { IPageWidget, toPageWidget } from 'app/client/ui/PageWidgetPicker';
|
||||
import { confirmModal } from 'app/client/ui2018/modals';
|
||||
import { BulkColValues, getColValues, RowRecord, UserAction } from 'app/common/DocActions';
|
||||
|
7
app/client/declarations.d.ts
vendored
7
app/client/declarations.d.ts
vendored
@ -12,7 +12,6 @@ declare module "app/client/lib/browserGlobals";
|
||||
declare module "app/client/lib/dom";
|
||||
declare module "app/client/lib/koDom";
|
||||
declare module "app/client/lib/koForm";
|
||||
declare module "app/client/lib/koSession";
|
||||
declare module "app/client/widgets/UserType";
|
||||
declare module "app/client/widgets/UserTypeImpl";
|
||||
|
||||
@ -319,3 +318,9 @@ declare module "app/client/lib/koUtil" {
|
||||
// with polyfills for old browsers.
|
||||
declare module "bowser/bundled";
|
||||
declare module "randomcolor";
|
||||
|
||||
interface Location {
|
||||
// We use reload(true) in places, which has an effect in Firefox, but may be more of a
|
||||
// historical accident than an intentional choice.
|
||||
reload(forceGet?: boolean): void;
|
||||
}
|
||||
|
@ -8,7 +8,9 @@
|
||||
|
||||
exports.loadBillingPage = () => import('app/client/ui/BillingPage' /* webpackChunkName: "BillingModule" */);
|
||||
exports.loadGristDoc = () => import('app/client/components/GristDoc' /* webpackChunkName: "GristDoc" */);
|
||||
exports.loadMomentTimezone = () => import('moment-timezone');
|
||||
// When importing this way, the module is under the "default" member, not sure why (maybe
|
||||
// esbuild-loader's doing).
|
||||
exports.loadMomentTimezone = () => import('moment-timezone').then(m => m.default);
|
||||
exports.loadPlotly = () => import('plotly.js-basic-dist' /* webpackChunkName: "plotly" */);
|
||||
exports.loadSearch = () => import('app/client/ui2018/search' /* webpackChunkName: "search" */);
|
||||
exports.loadUserManager = () => import('app/client/ui/UserManager' /* webpackChunkName: "usermanager" */);
|
||||
|
@ -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);
|
@ -21,13 +21,12 @@ export class DataRowModel extends BaseRowModel {
|
||||
public _validationFailures: ko.PureComputed<Array<IRowModel<'_grist_Validations'>>>;
|
||||
public _isAddRow: ko.Observable<boolean>;
|
||||
|
||||
private _allValidationsList: ko.Computed<KoArray<ValidationRec>>;
|
||||
private _isRealChange: ko.Observable<boolean>;
|
||||
|
||||
public constructor(dataTableModel: DataTableModel, colNames: string[]) {
|
||||
super(dataTableModel, colNames);
|
||||
|
||||
this._allValidationsList = dataTableModel.tableMetaRow.validations;
|
||||
const allValidationsList: ko.Computed<KoArray<ValidationRec>> = dataTableModel.tableMetaRow.validations;
|
||||
|
||||
this._isAddRow = ko.observable(false);
|
||||
|
||||
@ -36,10 +35,10 @@ export class DataRowModel extends BaseRowModel {
|
||||
// changes, those should only be enabled when _isRealChange is true.
|
||||
this._isRealChange = ko.observable(true);
|
||||
|
||||
this._validationFailures = this.autoDispose(ko.pureComputed(function() {
|
||||
return this._allValidationsList().all().filter(
|
||||
this._validationFailures = this.autoDispose(ko.pureComputed(() => {
|
||||
return allValidationsList().all().filter(
|
||||
validation => !this.cells[this.getValidationNameFromId(validation.id())]());
|
||||
}, this));
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -41,16 +41,8 @@ import {decodeObject} from 'app/plugin/objtypes';
|
||||
|
||||
// Re-export all the entity types available. The recommended usage is like this:
|
||||
// import {ColumnRec, ViewFieldRec} from 'app/client/models/DocModel';
|
||||
export {ColumnRec} from 'app/client/models/entities/ColumnRec';
|
||||
export {DocInfoRec} from 'app/client/models/entities/DocInfoRec';
|
||||
export {FilterRec} from 'app/client/models/entities/FilterRec';
|
||||
export {PageRec} from 'app/client/models/entities/PageRec';
|
||||
export {TabBarRec} from 'app/client/models/entities/TabBarRec';
|
||||
export {TableRec} from 'app/client/models/entities/TableRec';
|
||||
export {ValidationRec} from 'app/client/models/entities/ValidationRec';
|
||||
export {ViewFieldRec} from 'app/client/models/entities/ViewFieldRec';
|
||||
export {ViewRec} from 'app/client/models/entities/ViewRec';
|
||||
export {ViewSectionRec} from 'app/client/models/entities/ViewSectionRec';
|
||||
export type {ColumnRec, DocInfoRec, FilterRec, PageRec, TabBarRec, TableRec, ValidationRec,
|
||||
ViewFieldRec, ViewRec, ViewSectionRec};
|
||||
|
||||
|
||||
/**
|
||||
|
@ -6,7 +6,7 @@ import {buildColFilter, ColumnFilterFunc} from 'app/common/ColumnFilterFunc';
|
||||
import {buildRowFilter, RowFilterFunc, RowValueFunc } from 'app/common/RowFilterFunc';
|
||||
import {Computed, Disposable, MutableObsArray, obsArray, Observable, UseCB} from 'grainjs';
|
||||
|
||||
export {ColumnFilterFunc} from 'app/common/ColumnFilterFunc';
|
||||
export type {ColumnFilterFunc};
|
||||
|
||||
interface OpenColumnFilter {
|
||||
colRef: number;
|
||||
|
@ -306,7 +306,7 @@ export function createViewSectionRec(this: ViewSectionRec, docModel: DocModel):
|
||||
// in which case the UI prevents various things like hiding columns or changing the widget type.
|
||||
this.isRaw = this.autoDispose(ko.pureComputed(() => this.table().rawViewSectionRef() === this.getRowId()));
|
||||
|
||||
this.borderWidthPx = ko.pureComputed(function() { return this.borderWidth() + 'px'; }, this);
|
||||
this.borderWidthPx = ko.pureComputed(() => this.borderWidth() + 'px');
|
||||
|
||||
this.layoutSpecObj = modelUtil.jsonObservable(this.layoutSpec);
|
||||
|
||||
@ -487,7 +487,7 @@ export function createViewSectionRec(this: ViewSectionRec, docModel: DocModel):
|
||||
this.linkSrcCol = refRecord(docModel.columns, this.activeLinkSrcColRef);
|
||||
this.linkTargetCol = refRecord(docModel.columns, this.activeLinkTargetColRef);
|
||||
|
||||
this.activeRowId = ko.observable(null);
|
||||
this.activeRowId = ko.observable<RowId|null>(null);
|
||||
|
||||
this._linkingState = Holder.create(this);
|
||||
this.linkingState = this.autoDispose(ko.pureComputed(() => {
|
||||
@ -506,7 +506,7 @@ export function createViewSectionRec(this: ViewSectionRec, docModel: DocModel):
|
||||
}));
|
||||
|
||||
// If the view instance for this section is instantiated, it will be accessible here.
|
||||
this.viewInstance = ko.observable(null);
|
||||
this.viewInstance = ko.observable<BaseView|null>(null);
|
||||
|
||||
// Describes the most recent cursor position in the section.
|
||||
this.lastCursorPos = {
|
||||
@ -542,8 +542,8 @@ export function createViewSectionRec(this: ViewSectionRec, docModel: DocModel):
|
||||
);
|
||||
|
||||
this.hasCustomOptions = ko.observable(false);
|
||||
this.desiredAccessLevel = ko.observable(null);
|
||||
this.columnsToMap = ko.observable(null);
|
||||
this.desiredAccessLevel = ko.observable<AccessLevel|null>(null);
|
||||
this.columnsToMap = ko.observable<ColumnsToMap|null>(null);
|
||||
// Calculate mapped columns for Custom Widget.
|
||||
this.mappedColumns = ko.pureComputed(() => {
|
||||
// First check if widget has requested a custom column mapping and
|
||||
|
@ -236,7 +236,7 @@ export class BillingPage extends Disposable {
|
||||
moneyPlan?.amount ? [
|
||||
makeSummaryFeature([`Your team site has `, `${sub.userCount}`,
|
||||
` member${sub.userCount !== 1 ? 's' : ''}`]),
|
||||
tier ? this._makeAppSumoFeature(discountName!) : null,
|
||||
tier ? this._makeAppSumoFeature(discountName) : null,
|
||||
// Currently the subtotal is misleading and scary when tiers are in effect.
|
||||
// In this case, for now, just report what will be invoiced.
|
||||
!tier ? makeSummaryFeature([`Your ${moneyPlan.interval}ly subtotal is `,
|
||||
|
@ -6,6 +6,7 @@
|
||||
import {AppModel, reportError} from 'app/client/models/AppModel';
|
||||
import {getLoginOrSignupUrl, urlState} from 'app/client/models/gristUrlState';
|
||||
import {getWorkspaceInfo, ownerName, workspaceName} from 'app/client/models/WorkspaceInfo';
|
||||
import {cssInput} from 'app/client/ui/cssInput';
|
||||
import {bigBasicButton, bigPrimaryButtonLink} from 'app/client/ui2018/buttons';
|
||||
import {labeledSquareCheckbox} from 'app/client/ui2018/checkbox';
|
||||
import {colors, testId, vars} from 'app/client/ui2018/cssVars';
|
||||
@ -275,16 +276,6 @@ class SaveCopyModal extends Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
`);
|
||||
|
||||
export const cssField = styled('div', `
|
||||
margin: 16px 0;
|
||||
display: flex;
|
||||
|
12
app/client/ui/cssInput.ts
Normal file
12
app/client/ui/cssInput.ts
Normal file
@ -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;
|
||||
`);
|
@ -75,7 +75,7 @@ export function prepareForTransition(elem: HTMLElement, prepare: () => void) {
|
||||
// Recompute styles while transitions are off. See https://stackoverflow.com/a/16575811/328565
|
||||
// for explanation and https://stackoverflow.com/a/31862081/328565 for the recommendation used
|
||||
// here to trigger a style computation without a reflow.
|
||||
window.getComputedStyle(elem).opacity; // tslint:disable-line:no-unused-expression
|
||||
window.getComputedStyle(elem).opacity; // eslint-disable-line no-unused-expressions
|
||||
|
||||
// Restore transitions.
|
||||
elem.style.transitionProperty = prior;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import {FocusLayer} from 'app/client/lib/FocusLayer';
|
||||
import {reportError} from 'app/client/models/errors';
|
||||
import {cssInput} from 'app/client/ui/MakeCopyMenu';
|
||||
import {cssInput} from 'app/client/ui/cssInput';
|
||||
import {prepareForTransition, TransitionWatcher} from 'app/client/ui/transitions';
|
||||
import {bigBasicButton, bigPrimaryButton, cssButton} from 'app/client/ui2018/buttons';
|
||||
import {colors, mediaSmall, testId, vars} from 'app/client/ui2018/cssVars';
|
||||
|
@ -29,7 +29,7 @@ export class ChoiceTextBox extends NTextBox {
|
||||
private _choiceValues: Computed<string[]>;
|
||||
private _choiceValuesSet: Computed<Set<string>>;
|
||||
private _choiceOptions: KoSaveableObservable<ChoiceOptions | null | undefined>;
|
||||
private _choiceOptionsByName: Computed<ChoiceOptionsByName>
|
||||
private _choiceOptionsByName: Computed<ChoiceOptionsByName>;
|
||||
|
||||
constructor(field: ViewFieldRec) {
|
||||
super(field);
|
||||
|
@ -17,6 +17,9 @@ export interface IMargins {
|
||||
right: number;
|
||||
}
|
||||
|
||||
export type IRect = ISize & IMargins;
|
||||
|
||||
|
||||
// edgeMargin is how many pixels to leave before the edge of the browser window by default.
|
||||
// This is added to margins that may be passed into the constructor.
|
||||
const edgeMargin = 12;
|
||||
@ -37,8 +40,8 @@ export class EditorPlacement extends Disposable {
|
||||
public readonly onReposition = this.autoDispose(new Emitter());
|
||||
|
||||
private _editorRoot: HTMLElement;
|
||||
private _maxRect: ClientRect|DOMRect;
|
||||
private _cellRect: ClientRect|DOMRect;
|
||||
private _maxRect: IRect;
|
||||
private _cellRect: IRect;
|
||||
private _margins: IMargins;
|
||||
|
||||
// - editorDom is the DOM to attach. It gets destroyed when EditorPlacement is disposed.
|
||||
@ -141,7 +144,7 @@ export class EditorPlacement extends Disposable {
|
||||
|
||||
// Get the bounding rect of elem excluding borders. This allows the editor to match cellElem more
|
||||
// closely which is more visible in case of DetailView.
|
||||
function rectWithoutBorders(elem: Element): ClientRect {
|
||||
function rectWithoutBorders(elem: Element): IRect {
|
||||
const rect = elem.getBoundingClientRect();
|
||||
const style = getComputedStyle(elem, null);
|
||||
const bTop = parseFloat(style.getPropertyValue('border-top-width'));
|
||||
|
@ -55,6 +55,7 @@ function getTypeDefinition(type: string | false) {
|
||||
}
|
||||
|
||||
type ComputedStyle = {style?: Style; error?: true} | null | undefined;
|
||||
|
||||
/**
|
||||
* Builds a font option computed property.
|
||||
*/
|
||||
@ -62,12 +63,13 @@ function buildFontOptions(
|
||||
builder: FieldBuilder,
|
||||
computedRule: ko.Computed<ComputedStyle>,
|
||||
optionName: keyof Style) {
|
||||
return koUtil.withKoUtils(ko.computed(function() {
|
||||
|
||||
return koUtil.withKoUtils(ko.computed(() => {
|
||||
if (builder.isDisposed()) { return false; }
|
||||
const style = computedRule()?.style;
|
||||
const styleFlag = style?.[optionName] || this.field[optionName]();
|
||||
const styleFlag = style?.[optionName] || builder.field[optionName]();
|
||||
return styleFlag;
|
||||
}, builder)).onlyNotifyUnequal();
|
||||
})).onlyNotifyUnequal();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -131,9 +133,9 @@ export class FieldBuilder extends Disposable {
|
||||
});
|
||||
|
||||
// Observable which evaluates to a *function* that decides if a value is valid.
|
||||
this._isRightType = ko.pureComputed(function() {
|
||||
this._isRightType = ko.pureComputed<(value: CellValue, options?: any) => boolean>(() => {
|
||||
return gristTypes.isRightType(this._readOnlyPureType()) || _.constant(false);
|
||||
}, this);
|
||||
});
|
||||
|
||||
// Returns a boolean indicating whether the column is type Reference or ReferenceList.
|
||||
this._isRef = this.autoDispose(ko.computed(() => {
|
||||
@ -154,7 +156,7 @@ export class FieldBuilder extends Disposable {
|
||||
}
|
||||
}));
|
||||
|
||||
this.widget = ko.pureComputed({
|
||||
this.widget = ko.pureComputed<object>({
|
||||
owner: this,
|
||||
read() { return this.options().widget; },
|
||||
write(widget) {
|
||||
@ -196,10 +198,10 @@ export class FieldBuilder extends Disposable {
|
||||
this._rowMap = new Map();
|
||||
|
||||
// Returns the constructor for the widget, and only notifies subscribers on changes.
|
||||
this._widgetCons = this.autoDispose(koUtil.withKoUtils(ko.computed(function() {
|
||||
this._widgetCons = this.autoDispose(koUtil.withKoUtils(ko.computed(() => {
|
||||
return UserTypeImpl.getWidgetConstructor(this.options().widget,
|
||||
this._readOnlyPureType());
|
||||
}, this)).onlyNotifyUnequal());
|
||||
})).onlyNotifyUnequal());
|
||||
|
||||
// Computed builder for the widget.
|
||||
this.widgetImpl = this.autoDispose(koUtil.computedBuilder(() => {
|
||||
@ -467,28 +469,28 @@ export class FieldBuilder extends Disposable {
|
||||
return { style : new CombinedStyle(styles, flags) };
|
||||
}, this).extend({ deferred: true })).previousOnUndefined();
|
||||
|
||||
const widgetObs = koUtil.withKoUtils(ko.computed(function() {
|
||||
const widgetObs = koUtil.withKoUtils(ko.computed(() => {
|
||||
// TODO: Accessing row values like this doesn't always work (row and field might not be updated
|
||||
// simultaneously).
|
||||
if (this.isDisposed()) { return null; } // Work around JS errors during field removal.
|
||||
const value = row.cells[this.field.colId()];
|
||||
const cell = value && value();
|
||||
if (value && this._isRightType()(cell, this.options) || row._isAddRow.peek()) {
|
||||
if ((value) && this._isRightType()(cell, this.options) || row._isAddRow.peek()) {
|
||||
return this.widgetImpl();
|
||||
} else if (gristTypes.isVersions(cell)) {
|
||||
return this.diffImpl;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}, this).extend({ deferred: true })).onlyNotifyUnequal();
|
||||
}).extend({ deferred: true })).onlyNotifyUnequal();
|
||||
|
||||
const textColor = koUtil.withKoUtils(ko.computed(function() {
|
||||
const textColor = koUtil.withKoUtils(ko.computed(() => {
|
||||
if (this.isDisposed()) { return null; }
|
||||
const fromRules = computedRule()?.style?.textColor;
|
||||
return fromRules || this.field.textColor() || '';
|
||||
}, this)).onlyNotifyUnequal();
|
||||
})).onlyNotifyUnequal();
|
||||
|
||||
const fillColor = koUtil.withKoUtils(ko.computed(function() {
|
||||
const fillColor = koUtil.withKoUtils(ko.computed(() => {
|
||||
if (this.isDisposed()) { return null; }
|
||||
const fromRules = computedRule()?.style?.fillColor;
|
||||
let fill = fromRules || this.field.fillColor();
|
||||
@ -496,7 +498,7 @@ export class FieldBuilder extends Disposable {
|
||||
// If there is no color we are using fully transparent white color (for tests mainly).
|
||||
fill = fill ? fill.toUpperCase() : fill;
|
||||
return (fill === '#FFFFFF' ? '' : fill) || '#FFFFFF00';
|
||||
}, this)).onlyNotifyUnequal();
|
||||
})).onlyNotifyUnequal();
|
||||
|
||||
const fontBold = buildFontOptions(this, computedRule, 'fontBold');
|
||||
const fontItalic = buildFontOptions(this, computedRule, 'fontItalic');
|
||||
|
@ -135,6 +135,7 @@ export class NTextEditor extends NewBaseEditor {
|
||||
// but we got same enough spaces, we will force browser to check the available space once more time.
|
||||
if (enoughSpace(rect, size) && hasScroll(textInput)) {
|
||||
textInput.style.overflow = "hidden";
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
textInput.clientHeight; // just access metrics is enough to repaint
|
||||
textInput.style.overflow = "auto";
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ import {
|
||||
dom,
|
||||
DomContents,
|
||||
fromKo,
|
||||
IDisposableOwnerT,
|
||||
Observable,
|
||||
} from 'grainjs';
|
||||
|
||||
@ -30,8 +31,13 @@ export abstract class NewAbstractWidget extends Disposable {
|
||||
/**
|
||||
* Override the create() method to match the parameters of create() expected by FieldBuilder.
|
||||
*/
|
||||
public static create(field: ViewFieldRec) {
|
||||
return Disposable.create.call(this as any, null, field);
|
||||
// We copy Disposable.create() signature (the second one) to pacify typescript, but code must
|
||||
// use the first signature, which is compatible with old-style constructors.
|
||||
public static create<T extends new (...args: any[]) => any>(field: ViewFieldRec): InstanceType<T>;
|
||||
public static create<T extends new (...args: any[]) => any>(
|
||||
this: T, owner: IDisposableOwnerT<InstanceType<T>>|null, ...args: ConstructorParameters<T>): InstanceType<T>;
|
||||
public static create(...args: any[]) {
|
||||
return Disposable.create.call(this as any, null, ...args);
|
||||
}
|
||||
|
||||
protected options: SaveableObjObservable<any>;
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
// Some definitions have moved to be part of plugin API.
|
||||
import { BulkColValues, CellValue, RowRecord } from 'app/plugin/GristData';
|
||||
export { BulkColValues, CellValue, RowRecord } from 'app/plugin/GristData';
|
||||
export type { BulkColValues, CellValue, RowRecord };
|
||||
|
||||
// Part of a special CellValue used for comparisons, embedding several versions of a CellValue.
|
||||
export interface AllCellVersions {
|
||||
@ -173,7 +173,7 @@ export function getNumRows(action: DocAction): number {
|
||||
export function toTableDataAction(tableId: string, colValues: TableColValues): TableDataAction {
|
||||
const colData = {...colValues}; // Make a copy to avoid changing passed-in arguments.
|
||||
const rowIds: number[] = colData.id;
|
||||
delete colData.id;
|
||||
delete (colData as BulkColValues).id;
|
||||
return ['TableData', tableId, rowIds, colData];
|
||||
}
|
||||
|
||||
|
@ -67,7 +67,7 @@ export type AclMatchFunc = (input: AclMatchInput) => boolean;
|
||||
* Representation of a parsed ACL formula.
|
||||
*/
|
||||
type PrimitiveCellValue = number|string|boolean|null;
|
||||
export type ParsedAclFormula = [string, ...Array<ParsedAclFormula|PrimitiveCellValue>];
|
||||
export type ParsedAclFormula = [string, ...(ParsedAclFormula|PrimitiveCellValue)[]];
|
||||
|
||||
/**
|
||||
* Observations about a formula.
|
||||
|
@ -13,9 +13,6 @@
|
||||
*
|
||||
*/
|
||||
|
||||
// important to explicitly import this, or webpack --watch gets confused.
|
||||
import {clearTimeout, setTimeout} from "timers";
|
||||
|
||||
export class InactivityTimer {
|
||||
|
||||
private _timeout?: NodeJS.Timer | null;
|
||||
|
@ -19,7 +19,7 @@ try {
|
||||
const display = (code: string) => {
|
||||
try {
|
||||
const locale = new Intl.Locale(code);
|
||||
const regionName = regionDisplay.of(locale.region);
|
||||
const regionName = regionDisplay.of(locale.region!);
|
||||
const languageName = languageDisplay.of(locale.language);
|
||||
return `${regionName} (${languageName})`;
|
||||
} catch (ex) {
|
||||
@ -58,7 +58,7 @@ export function getCurrency(code: string) {
|
||||
try {
|
||||
const currencyDisplay = new Intl.DisplayNames('en', {type: 'currency'});
|
||||
currencies = [...new Set(currenciesCodes)].map(code => {
|
||||
return {name: currencyDisplay.of(code), code};
|
||||
return {name: currencyDisplay.of(code)!, code};
|
||||
});
|
||||
} catch {
|
||||
// Fall back to using the currency code as the display name.
|
||||
|
@ -255,10 +255,6 @@ export class HomeDBManager extends EventEmitter {
|
||||
// In restricted mode, documents should be read-only.
|
||||
private _restrictedMode: boolean = false;
|
||||
|
||||
public emit(event: NotifierEvent, ...args: any[]): boolean {
|
||||
return super.emit(event, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Five aclRules, each with one group (with the names 'owners', 'editors', 'viewers',
|
||||
* 'guests', and 'members') are created by default on every new entity (Organization,
|
||||
@ -298,6 +294,10 @@ export class HomeDBManager extends EventEmitter {
|
||||
orgOnly: true
|
||||
}];
|
||||
|
||||
public emit(event: NotifierEvent, ...args: any[]): boolean {
|
||||
return super.emit(event, ...args);
|
||||
}
|
||||
|
||||
// All groups.
|
||||
public get defaultGroups(): GroupDescriptor[] {
|
||||
return this._defaultGroups;
|
||||
|
@ -1,5 +1,5 @@
|
||||
// Letter codes for CellValue types encoded as [code, args...] tuples.
|
||||
export const enum GristObjCode {
|
||||
export enum GristObjCode {
|
||||
List = 'L',
|
||||
LookUp = 'l',
|
||||
Dict = 'O',
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {createCheckers, ICheckerSuite} from 'ts-interface-checker';
|
||||
import {BasicType, createCheckers, ICheckerSuite} from 'ts-interface-checker';
|
||||
import CustomSectionAPITI from './CustomSectionAPI-ti';
|
||||
import FileParserAPITI from './FileParserAPI-ti';
|
||||
import GristAPITI from './GristAPI-ti';
|
||||
@ -18,7 +18,14 @@ export {
|
||||
|
||||
const allTypes = [
|
||||
CustomSectionAPITI, FileParserAPITI, GristAPITI, GristTableTI, ImportSourceAPITI,
|
||||
InternalImportSourceAPITI, RenderOptionsTI, StorageAPITI, WidgetAPITI];
|
||||
InternalImportSourceAPITI, RenderOptionsTI, StorageAPITI, WidgetAPITI,
|
||||
];
|
||||
|
||||
// Ensure Buffer can be handled if mentioned in the interface descriptions, even if not supported
|
||||
// in the current environment (i.e. browser).
|
||||
if (typeof Buffer === 'undefined') {
|
||||
allTypes.push({Buffer: new BasicType((v) => false, "Buffer is not supported")});
|
||||
}
|
||||
|
||||
function checkDuplicates(types: Array<{[key: string]: object}>) {
|
||||
const seen = new Set<string>();
|
||||
|
@ -402,7 +402,7 @@ export interface ReadyPayload extends Omit<InteractionOptionsRequest, "hasCustom
|
||||
/**
|
||||
* Handler that will be called by Grist to open additional configuration panel inside the Custom Widget.
|
||||
*/
|
||||
onEditOptions: () => unknown;
|
||||
onEditOptions?: () => unknown;
|
||||
}
|
||||
/**
|
||||
* Declare that a component is prepared to receive messages from the outside world.
|
||||
|
27
app/server/declarations.d.ts
vendored
27
app/server/declarations.d.ts
vendored
@ -18,33 +18,6 @@ declare module "bluebird" {
|
||||
class Disposer<T> {}
|
||||
}
|
||||
|
||||
// TODO This is a module by Grist Labs; we should add index.d.ts to it.
|
||||
declare module "@gristlabs/basket-api" {
|
||||
interface Item { [colId: string]: any; }
|
||||
interface ColValues { [colId: string]: any[]; }
|
||||
interface AuthToken { [authProvider: string]: string; }
|
||||
|
||||
class Basket {
|
||||
public static addBasket(login: AuthToken): Promise<string>;
|
||||
public static getBaskets(login: AuthToken): Promise<string[]>;
|
||||
|
||||
public basketId: Readonly<string>;
|
||||
public apiKey: Readonly<string|undefined>;
|
||||
|
||||
constructor(basketId: string, apiKey?: string);
|
||||
public addTable(optTableId: string): Promise<string>;
|
||||
public getTable(tableId: string): Promise<Item[]>;
|
||||
public renameTable(oldTableId: string, newTableId: string): Promise<void>;
|
||||
public replaceTableData(tableId: string, columnValues: ColValues): Promise<void>;
|
||||
public deleteTable(tableId: string): Promise<void>;
|
||||
public getTables(): Promise<string[]>;
|
||||
public uploadAttachment(attachmentId: string, attachment: Buffer): Promise<void>;
|
||||
public delete(login: AuthToken): Promise<void>;
|
||||
}
|
||||
namespace Basket {}
|
||||
export = Basket;
|
||||
}
|
||||
|
||||
// Used in one place, and the typings are almost entirely unhelpful.
|
||||
declare module "multiparty";
|
||||
|
||||
|
@ -49,7 +49,7 @@ import {
|
||||
adaptServerUrl, addOrgToPath, addPermit, getOrgUrl, getOriginUrl, getScope, optStringParam,
|
||||
RequestWithGristInfo, stringParam, TEST_HTTPS_OFFSET, trustOrigin} from 'app/server/lib/requestUtils';
|
||||
import {ISendAppPageOptions, makeGristConfig, makeMessagePage, makeSendAppPage} from 'app/server/lib/sendAppPage';
|
||||
import {getDatabaseUrl} from 'app/server/lib/serverUtils';
|
||||
import {getDatabaseUrl, listenPromise} from 'app/server/lib/serverUtils';
|
||||
import {Sessions} from 'app/server/lib/Sessions';
|
||||
import * as shutdown from 'app/server/lib/shutdown';
|
||||
import {TagChecker} from 'app/server/lib/TagChecker';
|
||||
@ -1666,14 +1666,11 @@ export class FlexServer implements GristServer {
|
||||
|
||||
private async _startServers(server: http.Server, httpsServer: https.Server|undefined,
|
||||
name: string, port: number, verbose: boolean) {
|
||||
await new Promise((resolve, reject) => server.listen(port, this.host, resolve).on('error', reject));
|
||||
await listenPromise(server.listen(port, this.host));
|
||||
if (verbose) { log.info(`${name} available at ${this.host}:${port}`); }
|
||||
if (TEST_HTTPS_OFFSET && httpsServer) {
|
||||
const httpsPort = port + TEST_HTTPS_OFFSET;
|
||||
await new Promise((resolve, reject) => {
|
||||
httpsServer.listen(httpsPort, this.host, resolve)
|
||||
.on('error', reject);
|
||||
});
|
||||
await listenPromise(httpsServer.listen(httpsPort, this.host));
|
||||
if (verbose) { log.info(`${name} available at https://${this.host}:${httpsPort}`); }
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
export interface INotifier {
|
||||
deleteUser(userId: number): Promise<void>;
|
||||
// for test purposes, check if any notifications are in progress
|
||||
readonly testPending: boolean;
|
||||
|
||||
deleteUser(userId: number): Promise<void>;
|
||||
}
|
||||
|
@ -125,16 +125,16 @@ export class NSandbox implements ISandbox {
|
||||
|
||||
if (options.minimalPipeMode) {
|
||||
log.rawDebug("3-pipe Sandbox started", this._logMeta);
|
||||
this._streamToSandbox = this.childProc.stdin;
|
||||
this._streamFromSandbox = this.childProc.stdout;
|
||||
this._streamToSandbox = this.childProc.stdin!;
|
||||
this._streamFromSandbox = this.childProc.stdout!;
|
||||
} else {
|
||||
log.rawDebug("5-pipe Sandbox started", this._logMeta);
|
||||
this._streamToSandbox = (this.childProc.stdio as Stream[])[3] as Writable;
|
||||
this._streamFromSandbox = (this.childProc.stdio as Stream[])[4];
|
||||
this.childProc.stdout.on('data', sandboxUtil.makeLinePrefixer('Sandbox stdout: ', this._logMeta));
|
||||
this.childProc.stdout!.on('data', sandboxUtil.makeLinePrefixer('Sandbox stdout: ', this._logMeta));
|
||||
}
|
||||
const sandboxStderrLogger = sandboxUtil.makeLinePrefixer('Sandbox stderr: ', this._logMeta);
|
||||
this.childProc.stderr.on('data', data => {
|
||||
this.childProc.stderr!.on('data', data => {
|
||||
this._lastStderr = data;
|
||||
sandboxStderrLogger(data);
|
||||
});
|
||||
|
@ -16,7 +16,7 @@ import {IMsgCustom, IMsgRpcCall} from 'grain-rpc';
|
||||
*/
|
||||
export class SafePythonComponent extends BaseComponent {
|
||||
|
||||
private _sandbox: ISandbox;
|
||||
private _sandbox?: ISandbox;
|
||||
private _logMeta: log.ILogMeta;
|
||||
|
||||
// safe python component does not need pluginInstance.rpc because it is not possible to forward
|
||||
|
@ -118,7 +118,7 @@ export class DocTriggers {
|
||||
public shutdown() {
|
||||
this._shuttingDown = true;
|
||||
if (!this._sending) {
|
||||
this._redisClientField?.quitAsync();
|
||||
void(this._redisClientField?.quitAsync());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -121,8 +121,8 @@ export class UnsafeNodeComponent extends BaseComponent {
|
||||
.catch(err => log.warn("unsafeNode[%s] failed with %s", child.pid, err))
|
||||
.then(() => { this._child = undefined; });
|
||||
|
||||
child.stdout.on('data', makeLinePrefixer('PLUGIN stdout: '));
|
||||
child.stderr.on('data', makeLinePrefixer('PLUGIN stderr: '));
|
||||
child.stdout!.on('data', makeLinePrefixer('PLUGIN stdout: '));
|
||||
child.stderr!.on('data', makeLinePrefixer('PLUGIN stderr: '));
|
||||
|
||||
warnIfNotReady(this._rpc, 3000, "Plugin isn't ready; be sure to call grist.ready() from plugin");
|
||||
child.on('message', this._rpc.receiveMessage.bind(this._rpc));
|
||||
|
@ -63,6 +63,13 @@ export function connect(arg: any, ...moreArgs: any[]): Promise<net.Socket> {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Promisified version of net.Server.listen().
|
||||
*/
|
||||
export function listenPromise<T extends net.Server>(server: T): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => server.once('listening', resolve).once('error', reject));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the path `inner` is contained within the directory `outer`.
|
||||
*/
|
||||
|
@ -1,10 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"target": "es2016",
|
||||
"target": "es2017",
|
||||
"module": "commonjs",
|
||||
"strict": true,
|
||||
"strictPropertyInitialization": false,
|
||||
"useUnknownInCatchVariables": false,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"noImplicitAny": true,
|
||||
"noUnusedLocals": true,
|
||||
@ -22,6 +24,7 @@
|
||||
],
|
||||
},
|
||||
"composite": true,
|
||||
"types" : [],
|
||||
"plugins": [{
|
||||
"name": "typescript-eslint-language-service"
|
||||
}],
|
||||
|
@ -3,9 +3,26 @@ const path = require('path');
|
||||
module.exports = {
|
||||
target: 'web',
|
||||
mode: 'production',
|
||||
entry: "./_build/app/client/browserCheck.js",
|
||||
entry: "./app/client/browserCheck",
|
||||
output: {
|
||||
path: path.resolve("./static"),
|
||||
filename: "browser-check.js"
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.ts', '.js'],
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.(js|ts)?$/,
|
||||
loader: 'esbuild-loader',
|
||||
options: {
|
||||
loader: 'ts',
|
||||
target: 'es2017',
|
||||
sourcemap: true,
|
||||
},
|
||||
exclude: /node_modules/
|
||||
},
|
||||
]
|
||||
}
|
||||
};
|
||||
|
@ -1,13 +1,13 @@
|
||||
const StatsPlugin = require('stats-webpack-plugin');
|
||||
const MomentLocalesPlugin = require('moment-locales-webpack-plugin');
|
||||
const { ProvidePlugin } = require('webpack');
|
||||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
target: 'web',
|
||||
entry: {
|
||||
main: "app/client/app.js",
|
||||
errorPages: "app/client/errorMain.js",
|
||||
account: "app/client/accountMain.js",
|
||||
main: "app/client/app",
|
||||
errorPages: "app/client/errorMain",
|
||||
account: "app/client/accountMain",
|
||||
},
|
||||
output: {
|
||||
filename: "[name].bundle.js",
|
||||
@ -32,15 +32,29 @@ module.exports = {
|
||||
// typescript ("cheap-module-eval-source-map" is faster, but breakpoints are largely broken).
|
||||
devtool: "source-map",
|
||||
resolve: {
|
||||
extensions: ['.ts', '.js'],
|
||||
modules: [
|
||||
path.resolve('./_build'),
|
||||
path.resolve('./_build/ext'),
|
||||
path.resolve('./_build/stubs'),
|
||||
path.resolve('.'),
|
||||
path.resolve('./ext'),
|
||||
path.resolve('./stubs'),
|
||||
path.resolve('./node_modules')
|
||||
],
|
||||
fallback: {
|
||||
'path': require.resolve("path-browserify"),
|
||||
},
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.(js|ts)?$/,
|
||||
loader: 'esbuild-loader',
|
||||
options: {
|
||||
loader: 'ts',
|
||||
target: 'es2017',
|
||||
sourcemap: true,
|
||||
},
|
||||
exclude: /node_modules/
|
||||
},
|
||||
{ test: /\.js$/,
|
||||
use: ["source-map-loader"],
|
||||
enforce: "pre"
|
||||
@ -48,10 +62,11 @@ module.exports = {
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new StatsPlugin(
|
||||
'../.build_stats_js_bundle', // relative to output folder
|
||||
{source: false}, // Omit sources, which unnecessarily make the stats file huge.
|
||||
),
|
||||
// Some modules assume presence of Buffer and process.
|
||||
new ProvidePlugin({
|
||||
process: 'process/browser',
|
||||
Buffer: ['buffer', 'Buffer']
|
||||
}),
|
||||
// To strip all locales except “en”
|
||||
new MomentLocalesPlugin()
|
||||
],
|
||||
|
12
package.json
12
package.json
@ -45,9 +45,8 @@
|
||||
"@types/mime-types": "2.1.0",
|
||||
"@types/mocha": "5.2.5",
|
||||
"@types/moment-timezone": "0.5.9",
|
||||
"@types/node": "^10",
|
||||
"@types/node": "^14",
|
||||
"@types/node-fetch": "2.1.2",
|
||||
"@types/numeral": "0.0.25",
|
||||
"@types/pidusage": "2.0.1",
|
||||
"@types/plotly.js": "1.44.15",
|
||||
"@types/qrcode": "1.4.2",
|
||||
@ -64,6 +63,7 @@
|
||||
"catw": "1.0.1",
|
||||
"chai": "4.2.0",
|
||||
"chai-as-promised": "7.1.1",
|
||||
"esbuild-loader": "2.19.0",
|
||||
"mocha": "5.2.0",
|
||||
"mocha-webdriver": "0.2.9",
|
||||
"moment-locales-webpack-plugin": "^1.2.0",
|
||||
@ -72,11 +72,10 @@
|
||||
"selenium-webdriver": "3.6.0",
|
||||
"sinon": "7.1.1",
|
||||
"source-map-loader": "^0.2.4",
|
||||
"stats-webpack-plugin": "^0.7.0",
|
||||
"tmp-promise": "1.0.5",
|
||||
"typescript": "3.9.3",
|
||||
"webpack": "4.41.0",
|
||||
"webpack-cli": "3.3.2",
|
||||
"typescript": "4.7.4",
|
||||
"webpack": "5.73.0",
|
||||
"webpack-cli": "4.10.0",
|
||||
"why-is-node-running": "2.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
@ -127,7 +126,6 @@
|
||||
"mousetrap": "1.6.2",
|
||||
"multiparty": "4.2.2",
|
||||
"node-fetch": "2.2.0",
|
||||
"numeral": "2.0.6",
|
||||
"pg": "8.6.0",
|
||||
"plotly.js-basic-dist": "1.51.1",
|
||||
"popper-max-size-modifier": "0.2.0",
|
||||
|
@ -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()
|
@ -16,7 +16,7 @@ import {Client, ClientMethod} from 'app/server/lib/Client';
|
||||
import {CommClientConnect} from 'app/common/CommTypes';
|
||||
import {delay} from 'app/common/delay';
|
||||
import {isLongerThan} from 'app/common/gutil';
|
||||
import {fromCallback, getAvailablePort} from 'app/server/lib/serverUtils';
|
||||
import {connect as connectSock, fromCallback, getAvailablePort, listenPromise} from 'app/server/lib/serverUtils';
|
||||
import {Sessions} from 'app/server/lib/Sessions';
|
||||
import * as testUtils from 'test/server/testUtils';
|
||||
import * as session from '@gristlabs/express-session';
|
||||
@ -52,7 +52,7 @@ describe('Comm', function() {
|
||||
server = http.createServer();
|
||||
comm = new Comm(server, {sessions});
|
||||
comm.registerMethods(methods);
|
||||
return fromCallback(cb => server.listen(0, 'localhost', cb));
|
||||
return listenPromise(server.listen(0, 'localhost'));
|
||||
}
|
||||
|
||||
async function stopComm() {
|
||||
@ -500,8 +500,7 @@ export class TcpForwarder {
|
||||
public async connect() {
|
||||
await this.disconnect();
|
||||
this._server = new Server((sock) => this._onConnect(sock));
|
||||
await new Promise((resolve, reject) =>
|
||||
this._server!.on('error', reject).listen(this.port, resolve));
|
||||
await listenPromise(this._server.listen(this.port));
|
||||
}
|
||||
public async disconnectClientSide() {
|
||||
await Promise.all(Array.from(this._connections.keys(), destroySock));
|
||||
@ -528,9 +527,7 @@ export class TcpForwarder {
|
||||
}
|
||||
}
|
||||
private async _onConnect(clientSock: Socket) {
|
||||
const serverSock = new Socket();
|
||||
await new Promise((resolve, reject) =>
|
||||
serverSock.on('error', reject).connect(this._serverPort, resolve));
|
||||
const serverSock = await connectSock(this._serverPort);
|
||||
clientSock.pipe(serverSock);
|
||||
serverSock.pipe(clientSock);
|
||||
clientSock.on('error', (err) => serverSock.destroy(err));
|
||||
|
@ -1,5 +1,5 @@
|
||||
import {getAppRoot} from 'app/server/lib/places';
|
||||
import {fromCallback} from 'app/server/lib/serverUtils';
|
||||
import {fromCallback, listenPromise} from 'app/server/lib/serverUtils';
|
||||
import * as express from 'express';
|
||||
import * as http from 'http';
|
||||
import {AddressInfo, Socket} from 'net';
|
||||
@ -45,7 +45,7 @@ export function serveCustomViews(): Promise<Serving> {
|
||||
export async function serveSomething(setup: (app: express.Express) => void, port= 0): Promise<Serving> {
|
||||
const app = express();
|
||||
const server = http.createServer(app);
|
||||
await fromCallback(cb => server.listen(port, cb));
|
||||
await listenPromise(server.listen(port));
|
||||
|
||||
const connections = new Set<Socket>();
|
||||
server.on('connection', (conn) => {
|
||||
|
Loading…
Reference in New Issue
Block a user