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