(core) Add rules to eslint to better match our coding conventions.

Summary:
We used tslint earlier, and on switching to eslint, some rules were not
transfered. This moves more rules over, for consistent conventions or helpful
warnings.

- Name private members with a leading underscore.
- Prefer interface over a type alias.
- Use consistent spacing around ':' in type annotations.
- Use consistent spacing around braces of code blocks.
- Use semicolons consistently at the ends of statements.
- Use braces around even one-liner blocks, like conditionals and loops.
- Warn about shadowed variables.

Test Plan: Fixed all new warnings. Should be no behavior changes in code.

Reviewers: paulfitz

Reviewed By: paulfitz

Differential Revision: https://phab.getgrist.com/D2831
pull/9/head
Dmitry S 3 years ago
parent 0890749d15
commit d1c1416d78

@ -1067,7 +1067,7 @@ class ObsUserAttributeRule extends Disposable {
// TODO this weirdly only works on the first click // TODO this weirdly only works on the first click
(editor as any).completer?.showPopup(editor); (editor as any).completer?.showPopup(editor);
} }
}) });
}) })
}), }),
testId('rule-userattr-attr'), testId('rule-userattr-attr'),

@ -440,7 +440,7 @@ export class ActionLog extends dispose.Disposable implements IDomComponent {
const fieldIndex = viewSection.viewFields().peek().findIndex((f: any) => f.colId.peek() === colId); const fieldIndex = viewSection.viewFields().peek().findIndex((f: any) => f.colId.peek() === colId);
// Finally, move cursor position to the section, column (if we found it), and row. // Finally, move cursor position to the section, column (if we found it), and row.
this._gristDoc.moveToCursorPos({rowId, sectionId, fieldIndex}).catch(() => { /* do nothing */}); this._gristDoc.moveToCursorPos({rowId, sectionId, fieldIndex}).catch(() => { /* do nothing */ });
} }
} }

@ -28,8 +28,9 @@ export function samePosition(a: CellPosition, b: CellPosition) {
* @param docModel Document model * @param docModel Document model
*/ */
export function fromCursor(position: CursorPos, docModel: DocModel): CellPosition | null { export function fromCursor(position: CursorPos, docModel: DocModel): CellPosition | null {
if (!position.sectionId || !position.rowId || position.fieldIndex == null) if (!position.sectionId || !position.rowId || position.fieldIndex == null) {
return null; return null;
}
const section = docModel.viewSections.getRowModel(position.sectionId); const section = docModel.viewSections.getRowModel(position.sectionId);
const colRef = section.viewFields().peek()[position.fieldIndex]?.colRef.peek(); const colRef = section.viewFields().peek()[position.fieldIndex]?.colRef.peek();

@ -31,6 +31,7 @@ CodeEditorPanel.prototype.buildDom = function() {
kd.scope(this._schema, function(schema) { kd.scope(this._schema, function(schema) {
// The reason to scope and rebuild instead of using `kd.text(schema)` is because // The reason to scope and rebuild instead of using `kd.text(schema)` is because
// hljs.highlightBlock(elem) replaces `elem` with a whole new dom tree. // hljs.highlightBlock(elem) replaces `elem` with a whole new dom tree.
if (!schema) { return null; }
return dom( return dom(
'code.g-code-viewer.python', 'code.g-code-viewer.python',
schema, schema,

@ -204,15 +204,16 @@ export class Comm extends dispose.Disposable implements GristServerAPI, DocListA
public pendingRequests: Map<number, CommRequestInFlight>; public pendingRequests: Map<number, CommRequestInFlight>;
public nextRequestNumber: number = 0; public nextRequestNumber: number = 0;
protected listenTo: BackboneEvents["listenTo"]; // set by Backbone
protected trigger: BackboneEvents["trigger"]; // set by Backbone
protected stopListening: BackboneEvents["stopListening"]; // set by Backbone
// This is a map from docId to the connection for the server that manages // This is a map from docId to the connection for the server that manages
// that docId. In classic Grist, which doesn't have fixed docIds or multiple // that docId. In classic Grist, which doesn't have fixed docIds or multiple
// servers, the key is always "null". // servers, the key is always "null".
private _connections: Map<string|null, GristWSConnection> = new Map(); private _connections: Map<string|null, GristWSConnection> = new Map();
private _collectedUserActions: UserAction[] | null; private _collectedUserActions: UserAction[] | null;
private _singleWorkerMode: boolean = getInitialDocAssignment() === null; // is this classic Grist? private _singleWorkerMode: boolean = getInitialDocAssignment() === null; // is this classic Grist?
private listenTo: BackboneEvents["listenTo"]; // set by Backbone
private trigger: BackboneEvents["trigger"]; // set by Backbone
private stopListening: BackboneEvents["stopListening"]; // set by Backbone
public create() { public create() {
this.autoDisposeCallback(() => { this.autoDisposeCallback(() => {

@ -79,7 +79,7 @@ export class Cursor extends Disposable {
optCursorPos = optCursorPos || {}; optCursorPos = optCursorPos || {};
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(optCursorPos.rowId || 0);
this.rowIndex = this.autoDispose(ko.computed({ this.rowIndex = this.autoDispose(ko.computed({
read: () => { read: () => {

@ -48,33 +48,33 @@ export class CursorMonitor extends Disposable {
// whenever current position changes, store it in the memory // whenever current position changes, store it in the memory
this.autoDispose(doc.cursorPosition.addListener(pos => { this.autoDispose(doc.cursorPosition.addListener(pos => {
// if current position is not restored yet, don't change it // if current position is not restored yet, don't change it
if (!this._restored) return; if (!this._restored) { return; }
if (pos) this.storePosition(pos); if (pos) { this._storePosition(pos); }
})) }));
} }
private _whenDocumentLoadsRestorePosition(doc: GristDoc) { private _whenDocumentLoadsRestorePosition(doc: GristDoc) {
// on view shown // on view shown
this.autoDispose(doc.currentView.addListener(async view => { this.autoDispose(doc.currentView.addListener(async view => {
// if the position was restored for this document do nothing // if the position was restored for this document do nothing
if (this._restored) return; if (this._restored) { return; }
// set that we already restored the position, as some view is shown to the user // set that we already restored the position, as some view is shown to the user
this._restored = true; this._restored = true;
// if view wasn't rendered (page is displaying history or code view) do nothing // if view wasn't rendered (page is displaying history or code view) do nothing
if (!view) return; if (!view) { return; }
const viewId = doc.activeViewId.get(); const viewId = doc.activeViewId.get();
const position = this.restoreLastPosition(viewId); const position = this._restoreLastPosition(viewId);
if (position) { if (position) {
await doc.recursiveMoveToCursorPos(position, true); await doc.recursiveMoveToCursorPos(position, true);
} }
})); }));
} }
private storePosition(pos: ViewCursorPos) { private _storePosition(pos: ViewCursorPos) {
this._store.update(this._docId, pos); this._store.update(this._docId, pos);
} }
private restoreLastPosition(view: IDocPage) { private _restoreLastPosition(view: IDocPage) {
const lastPosition = this._store.read(this._docId); const lastPosition = this._store.read(this._docId);
this._store.clear(this._docId); this._store.clear(this._docId);
if (lastPosition && lastPosition.position.viewId == view) { if (lastPosition && lastPosition.position.viewId == view) {
@ -87,13 +87,13 @@ export class CursorMonitor extends Disposable {
// Internal implementations for working with local storage // Internal implementations for working with local storage
class StorageWrapper { class StorageWrapper {
constructor(private storage = getStorage()) { constructor(private _storage = getStorage()) {
} }
public update(docId: string, position: ViewCursorPos): void { public update(docId: string, position: ViewCursorPos): void {
try { try {
const storage = this.storage; const storage = this._storage;
const data = { docId, position, timestamp: Date.now() }; const data = { docId, position, timestamp: Date.now() };
storage.setItem(this._key(docId), JSON.stringify(data)); storage.setItem(this._key(docId), JSON.stringify(data));
} catch (e) { } catch (e) {
@ -102,14 +102,14 @@ class StorageWrapper {
} }
public clear(docId: string,): void { public clear(docId: string,): void {
const storage = this.storage; const storage = this._storage;
storage.removeItem(this._key(docId)); storage.removeItem(this._key(docId));
} }
public read(docId: string): { docId: string; position: ViewCursorPos; } | undefined { public read(docId: string): { docId: string; position: ViewCursorPos; } | undefined {
const storage = this.storage; const storage = this._storage;
const result = storage.getItem(this._key(docId)); const result = storage.getItem(this._key(docId));
if (!result) return undefined; if (!result) { return undefined; }
return JSON.parse(result); return JSON.parse(result);
} }

@ -25,7 +25,7 @@ export class EditorMonitor extends Disposable {
this._store = new EditMemoryStorage(doc.docId(), store); this._store = new EditMemoryStorage(doc.docId(), store);
// listen to document events to handle view load event // listen to document events to handle view load event
this._listenToReload(doc) this._listenToReload(doc);
} }
/** /**
@ -67,7 +67,7 @@ export class EditorMonitor extends Disposable {
} }
executed = true; executed = true;
// if view wasn't rendered (page is displaying history or code view) do nothing // if view wasn't rendered (page is displaying history or code view) do nothing
if (!view) return; if (!view) { return; }
const lastEdit = this._restorePosition(); const lastEdit = this._restorePosition();
if (lastEdit) { if (lastEdit) {
// set the cursor at right cell // set the cursor at right cell
@ -98,11 +98,11 @@ function typedListener(owner: IDisposableOwner) {
type EditorState = any; type EditorState = any;
// Schema for value stored in the local storage // Schema for value stored in the local storage
type LastEditData = { interface LastEditData {
// absolute position for a cell // absolute position for a cell
position: CellPosition, position: CellPosition;
// editor's state // editor's state
value: EditorState value: EditorState;
} }
// Abstraction for working with local storage // Abstraction for working with local storage
@ -111,7 +111,7 @@ class EditMemoryStorage {
private _entry: LastEditData | null = null; private _entry: LastEditData | null = null;
private _timestamp = 0; private _timestamp = 0;
constructor(private _docId: string, private storage = getStorage()) { constructor(private _docId: string, private _storage = getStorage()) {
} }
public updateValue(pos: CellPosition, value: EditorState): void { public updateValue(pos: CellPosition, value: EditorState): void {
@ -138,7 +138,7 @@ class EditMemoryStorage {
} }
protected load() { protected load() {
const storage = this.storage; const storage = this._storage;
const data = storage.getItem(this._key()); const data = storage.getItem(this._key());
this._entry = null; this._entry = null;
this._timestamp = 0; this._timestamp = 0;
@ -150,7 +150,7 @@ class EditMemoryStorage {
console.error("[EditMemory] Data in local storage has a different structure"); console.error("[EditMemory] Data in local storage has a different structure");
return; return;
} }
this._entry = entry this._entry = entry;
this._timestamp = timestamp; this._timestamp = timestamp;
} catch (e) { } catch (e) {
console.error("[EditMemory] Can't deserialize date from local storage"); console.error("[EditMemory] Can't deserialize date from local storage");
@ -159,7 +159,7 @@ class EditMemoryStorage {
} }
protected save(): void { protected save(): void {
const storage = this.storage; const storage = this._storage;
// if entry was removed - clear the storage // if entry was removed - clear the storage
if (!this._entry) { if (!this._entry) {

@ -111,10 +111,10 @@ export class GristDoc extends DisposableWithEvents {
public readonly fieldEditorHolder = Holder.create(this); public readonly fieldEditorHolder = Holder.create(this);
// Holds current view that is currently rendered // Holds current view that is currently rendered
public currentView : Observable<BaseView | null>; public currentView: Observable<BaseView | null>;
// Holds current cursor position with a view id // Holds current cursor position with a view id
public cursorPosition : Computed<ViewCursorPos | undefined>; public cursorPosition: Computed<ViewCursorPos | undefined>;
private _actionLog: ActionLog; private _actionLog: ActionLog;
private _undoStack: UndoStack; private _undoStack: UndoStack;
@ -250,22 +250,22 @@ export class GristDoc extends DisposableWithEvents {
}); });
// then listen if the view is present, because we still need to wait for it load properly // then listen if the view is present, because we still need to wait for it load properly
this.autoDispose(viewInstance.addListener(async (view) => { this.autoDispose(viewInstance.addListener(async (view) => {
if (!view) return; if (!view) { return; }
await view.getLoadingDonePromise(); await view.getLoadingDonePromise();
this.currentView.set(view); this.currentView.set(view);
})) }));
// create observable for current cursor position // create observable for current cursor position
this.cursorPosition = Computed.create<ViewCursorPos | undefined>(this, use => { this.cursorPosition = Computed.create<ViewCursorPos | undefined>(this, use => {
// get the BaseView // get the BaseView
const view = use(viewInstance); const view = use(viewInstance);
if (!view) return undefined; if (!view) { return undefined; }
// get current viewId // get current viewId
const viewId = use(this.activeViewId); const viewId = use(this.activeViewId);
if (typeof viewId != 'number') return undefined; if (typeof viewId != 'number') { return undefined; }
// read latest position // read latest position
const currentPosition = use(view.cursor.currentPosition); const currentPosition = use(view.cursor.currentPosition);
if (currentPosition) return { ...currentPosition, viewId } if (currentPosition) { return { ...currentPosition, viewId }; }
return undefined; return undefined;
}); });
@ -333,7 +333,7 @@ export class GristDoc extends DisposableWithEvents {
return; return;
} }
try { try {
const viewInstance = await this._switchToSectionId(cursorPos.sectionId) const viewInstance = await this._switchToSectionId(cursorPos.sectionId);
if (viewInstance) { if (viewInstance) {
viewInstance.setCursorPos(cursorPos); viewInstance.setCursorPos(cursorPos);
} }
@ -643,7 +643,7 @@ export class GristDoc extends DisposableWithEvents {
} }
const view: ViewRec = section.view.peek(); const view: ViewRec = section.view.peek();
const viewId = view.getRowId(); const viewId = view.getRowId();
if (viewId != this.activeViewId.get()) await this.openDocPage(view.getRowId()); if (viewId != this.activeViewId.get()) { await this.openDocPage(view.getRowId()); }
if (setAsActiveSection) { view.activeSectionId(cursorPos.sectionId); } if (setAsActiveSection) { view.activeSectionId(cursorPos.sectionId); }
const fieldIndex = cursorPos.fieldIndex; const fieldIndex = cursorPos.fieldIndex;
const viewInstance = await waitObs(section.viewInstance); const viewInstance = await waitObs(section.viewInstance);
@ -669,14 +669,14 @@ export class GristDoc extends DisposableWithEvents {
* @param input Optional. Cell's initial value * @param input Optional. Cell's initial value
*/ */
public async activateEditorAtCursor(options: { init?: string, state?: any}) { public async activateEditorAtCursor(options: { init?: string, state?: any}) {
const view = await this.waitForView(); const view = await this._waitForView();
view?.activateEditorAtCursor(options); view?.activateEditorAtCursor(options);
} }
/** /**
* Waits for a view to be ready * Waits for a view to be ready
*/ */
private async waitForView() { private async _waitForView() {
const view = await waitObs(this.viewModel.activeSection.peek().viewInstance); const view = await waitObs(this.viewModel.activeSection.peek().viewInstance);
await view?.getLoadingDonePromise(); await view?.getLoadingDonePromise();
return view; return view;
@ -788,13 +788,13 @@ export class GristDoc extends DisposableWithEvents {
* Convert a url hash to a cursor position. * Convert a url hash to a cursor position.
*/ */
private _getCursorPosFromHash(hash: HashLink): CursorPos { private _getCursorPosFromHash(hash: HashLink): CursorPos {
const cursorPos : CursorPos = { rowId: hash.rowId, sectionId: hash.sectionId }; const cursorPos: CursorPos = { rowId: hash.rowId, sectionId: hash.sectionId };
if (cursorPos.sectionId != undefined && hash.colRef !== undefined){ if (cursorPos.sectionId != undefined && hash.colRef !== undefined){
// translate colRef to a fieldIndex // translate colRef to a fieldIndex
const section = this.docModel.viewSections.getRowModel(cursorPos.sectionId); const section = this.docModel.viewSections.getRowModel(cursorPos.sectionId);
const fieldIndex = section.viewFields.peek().all() const fieldIndex = section.viewFields.peek().all()
.findIndex(x=> x.colRef.peek() == hash.colRef); .findIndex(x=> x.colRef.peek() == hash.colRef);
if (fieldIndex >= 0) cursorPos.fieldIndex = fieldIndex; if (fieldIndex >= 0) { cursorPos.fieldIndex = fieldIndex; }
} }
return cursorPos; return cursorPos;
} }

@ -106,6 +106,8 @@ export class GristWSConnection extends Disposable {
public useCount: number = 0; public useCount: number = 0;
public on: BackboneEvents['on']; // set by Backbone public on: BackboneEvents['on']; // set by Backbone
protected trigger: BackboneEvents['trigger']; // set by Backbone
private _clientId: string|null; private _clientId: string|null;
private _clientCounter: string; // Identifier of this GristWSConnection object in this browser tab session private _clientCounter: string; // Identifier of this GristWSConnection object in this browser tab session
private _assignmentId: string|null; private _assignmentId: string|null;
@ -118,7 +120,6 @@ export class GristWSConnection extends Disposable {
private _reconnectAttempts: number = 0; private _reconnectAttempts: number = 0;
private _wantReconnect: boolean = true; private _wantReconnect: boolean = true;
private _ws: WebSocket|null = null; private _ws: WebSocket|null = null;
private trigger: BackboneEvents['trigger']; // set by Backbone
constructor(private _settings: GristWSSettings = new GristWSSettingsBrowser()) { constructor(private _settings: GristWSSettings = new GristWSSettingsBrowser()) {
super(); super();

@ -28,8 +28,8 @@ import pickBy = require('lodash/pickBy');
* Creates an instance of TypeTransform for a single field. Extends ColumnTransform. * Creates an instance of TypeTransform for a single field. Extends ColumnTransform.
*/ */
export class TypeTransform extends ColumnTransform { export class TypeTransform extends ColumnTransform {
private reviseTypeChange = Observable.create(this, false); private _reviseTypeChange = Observable.create(this, false);
private transformWidget: Computed<NewAbstractWidget|null>; private _transformWidget: Computed<NewAbstractWidget|null>;
constructor(gristDoc: GristDoc, fieldBuilder: FieldBuilder) { constructor(gristDoc: GristDoc, fieldBuilder: FieldBuilder) {
super(gristDoc, fieldBuilder); super(gristDoc, fieldBuilder);
@ -37,7 +37,7 @@ export class TypeTransform extends ColumnTransform {
// The display widget of the new transform column. Used to build the transform config menu. // The display widget of the new transform column. Used to build the transform config menu.
// Only set while transforming. // Only set while transforming.
this.transformWidget = Computed.create(this, fromKo(fieldBuilder.widgetImpl), (use, widget) => { this._transformWidget = Computed.create(this, fromKo(fieldBuilder.widgetImpl), (use, widget) => {
return use(this.origColumn.isTransforming) ? widget : null; return use(this.origColumn.isTransforming) ? widget : null;
}); });
} }
@ -49,12 +49,12 @@ export class TypeTransform extends ColumnTransform {
// An observable to disable all buttons before the dom get removed. // An observable to disable all buttons before the dom get removed.
const disableButtons = Observable.create(null, false); const disableButtons = Observable.create(null, false);
this.reviseTypeChange.set(false); this._reviseTypeChange.set(false);
this.editor = this.autoDispose(AceEditor.create({ observable: this.transformColumn.formula })); this.editor = this.autoDispose(AceEditor.create({ observable: this.transformColumn.formula }));
return dom('div', return dom('div',
testId('type-transform-top'), testId('type-transform-top'),
dom.maybe(this.transformWidget, transformWidget => transformWidget.buildTransformConfigDom()), dom.maybe(this._transformWidget, transformWidget => transformWidget.buildTransformConfigDom()),
dom.maybe(this.reviseTypeChange, () => dom.maybe(this._reviseTypeChange, () =>
dom('div.transform_editor', this.buildEditorDom(), dom('div.transform_editor', this.buildEditorDom(),
testId("type-transform-formula") testId("type-transform-formula")
) )
@ -64,7 +64,7 @@ export class TypeTransform extends ColumnTransform {
'Cancel', testId("type-transform-cancel"), 'Cancel', testId("type-transform-cancel"),
dom.cls('disabled', disableButtons) dom.cls('disabled', disableButtons)
), ),
dom.domComputed(this.reviseTypeChange, revising => { dom.domComputed(this._reviseTypeChange, revising => {
if (revising) { if (revising) {
return basicButton(dom.on('click', () => this.editor.writeObservable()), return basicButton(dom.on('click', () => this.editor.writeObservable()),
'Preview', testId("type-transform-update"), 'Preview', testId("type-transform-update"),
@ -72,7 +72,7 @@ export class TypeTransform extends ColumnTransform {
{ title: 'Update formula (Shift+Enter)' } { title: 'Update formula (Shift+Enter)' }
); );
} else { } else {
return basicButton(dom.on('click', () => { this.reviseTypeChange.set(true); }), return basicButton(dom.on('click', () => { this._reviseTypeChange.set(true); }),
'Revise', testId("type-transform-revise"), 'Revise', testId("type-transform-revise"),
dom.cls('disabled', disableButtons) dom.cls('disabled', disableButtons)
); );

@ -117,13 +117,13 @@ export class UndoStack extends dispose.Disposable {
// context where the change was originally made. We jump first immediately to feel more // context where the change was originally made. We jump first immediately to feel more
// responsive, then again when the action is done. The second jump matters more for most // responsive, then again when the action is done. The second jump matters more for most
// changes, but the first is the important one when Undoing an AddRecord. // changes, but the first is the important one when Undoing an AddRecord.
this._gristDoc.moveToCursorPos(ag.cursorPos, ag).catch(() => {/* do nothing */}) this._gristDoc.moveToCursorPos(ag.cursorPos, ag).catch(() => { /* do nothing */ });
await this._gristDoc.docComm.applyUserActionsById( await this._gristDoc.docComm.applyUserActionsById(
actionGroups.map(a => a.actionNum), actionGroups.map(a => a.actionNum),
actionGroups.map(a => a.actionHash), actionGroups.map(a => a.actionHash),
isUndo, isUndo,
{ otherId: ag.actionNum }); { otherId: ag.actionNum });
this._gristDoc.moveToCursorPos(ag.cursorPos, ag).catch(() => {/* do nothing */}) this._gristDoc.moveToCursorPos(ag.cursorPos, ag).catch(() => { /* do nothing */ });
} catch (err) { } catch (err) {
err.message = `Failed to apply ${isUndo ? 'undo' : 'redo'} action: ${err.message}`; err.message = `Failed to apply ${isUndo ? 'undo' : 'redo'} action: ${err.message}`;
throw err; throw err;

@ -18,7 +18,7 @@ export async function duplicatePage(gristDoc: GristDoc, pageId: number) {
const pagesTable = gristDoc.docModel.pages; const pagesTable = gristDoc.docModel.pages;
const pageName = pagesTable.rowModels[pageId].view.peek().name.peek(); const pageName = pagesTable.rowModels[pageId].view.peek().name.peek();
let inputEl: HTMLInputElement; let inputEl: HTMLInputElement;
setTimeout(() => {inputEl.focus(); inputEl.select(); }, 100); setTimeout(() => { inputEl.focus(); inputEl.select(); }, 100);
confirmModal('Duplicate page', 'Save', () => makeDuplicate(gristDoc, pageId, inputEl.value), ( confirmModal('Duplicate page', 'Save', () => makeDuplicate(gristDoc, pageId, inputEl.value), (
dom('div', [ dom('div', [

@ -45,9 +45,9 @@ declare module "app/client/components/BaseView" {
import {DomArg} from 'grainjs'; import {DomArg} from 'grainjs';
import {IOpenController} from 'popweasel'; import {IOpenController} from 'popweasel';
type Options = { interface Options {
init? : string, init?: string;
state? : any state?: any;
} }
namespace BaseView {} namespace BaseView {}
@ -68,7 +68,7 @@ declare module "app/client/components/BaseView" {
public createFilterMenu(ctl: IOpenController, field: ViewFieldRec, onClose?: () => void): HTMLElement; public createFilterMenu(ctl: IOpenController, field: ViewFieldRec, onClose?: () => void): HTMLElement;
public buildTitleControls(): DomArg; public buildTitleControls(): DomArg;
public getLoadingDonePromise(): Promise<void>; public getLoadingDonePromise(): Promise<void>;
public activateEditorAtCursor(options?: Options) : void; public activateEditorAtCursor(options?: Options): void;
public onResize(): void; public onResize(): void;
public prepareToPrint(onOff: boolean): void; public prepareToPrint(onOff: boolean): void;
public moveEditRowToCursor(): DataRowModel; public moveEditRowToCursor(): DataRowModel;

@ -55,18 +55,18 @@ export class Autocomplete<Item extends ACItem> extends Disposable {
constructor( constructor(
private _triggerElem: HTMLInputElement | HTMLTextAreaElement, private _triggerElem: HTMLInputElement | HTMLTextAreaElement,
private readonly options: IAutocompleteOptions<Item>, private readonly _options: IAutocompleteOptions<Item>,
) { ) {
super(); super();
const content = cssMenuWrap( const content = cssMenuWrap(
this._menuContent = cssMenu({class: options.menuCssClass || ''}, this._menuContent = cssMenu({class: _options.menuCssClass || ''},
dom.forEach(this._items, (item) => options.renderItem(item, this._highlightFunc)), dom.forEach(this._items, (item) => _options.renderItem(item, this._highlightFunc)),
dom.style('min-width', _triggerElem.getBoundingClientRect().width + 'px'), dom.style('min-width', _triggerElem.getBoundingClientRect().width + 'px'),
dom.on('mouseleave', (ev) => this._setSelected(-1, true)), dom.on('mouseleave', (ev) => this._setSelected(-1, true)),
dom.on('click', (ev) => { dom.on('click', (ev) => {
this._setSelected(this._findTargetItem(ev.target), true); this._setSelected(this._findTargetItem(ev.target), true);
if (options.onClick) { options.onClick(); } if (_options.onClick) { _options.onClick(); }
}) })
), ),
// Prevent trigger element from being blurred on click. // Prevent trigger element from being blurred on click.
@ -91,7 +91,7 @@ export class Autocomplete<Item extends ACItem> extends Disposable {
this.onDispose(() => { dom.domDispose(content); content.remove(); }); this.onDispose(() => { dom.domDispose(content); content.remove(); });
// Prepare and create the Popper instance, which places the content according to the options. // Prepare and create the Popper instance, which places the content according to the options.
const popperOptions = merge({}, defaultPopperOptions, options.popperOptions); const popperOptions = merge({}, defaultPopperOptions, _options.popperOptions);
this._popper = createPopper(_triggerElem, content, popperOptions); this._popper = createPopper(_triggerElem, content, popperOptions);
this.onDispose(() => this._popper.destroy()); this.onDispose(() => this._popper.destroy());
} }
@ -110,7 +110,7 @@ export class Autocomplete<Item extends ACItem> extends Disposable {
const elem = (this._menuContent.children[index] as HTMLElement) || null; const elem = (this._menuContent.children[index] as HTMLElement) || null;
const prev = this._selected; const prev = this._selected;
if (elem !== prev) { if (elem !== prev) {
const clsName = this.options.selectedCssClass || 'selected'; const clsName = this._options.selectedCssClass || 'selected';
if (prev) { prev.classList.remove(clsName); } if (prev) { prev.classList.remove(clsName); }
if (elem) { if (elem) {
elem.classList.add(clsName); elem.classList.add(clsName);
@ -123,7 +123,7 @@ export class Autocomplete<Item extends ACItem> extends Disposable {
if (updateValue) { if (updateValue) {
// Update trigger's value with the selected choice, or else with the last typed value. // Update trigger's value with the selected choice, or else with the last typed value.
if (elem) { if (elem) {
this._triggerElem.value = this.options.getItemText(this.getSelectedItem()!); this._triggerElem.value = this._options.getItemText(this.getSelectedItem()!);
} else { } else {
this._triggerElem.value = this._lastAsTyped; this._triggerElem.value = this._lastAsTyped;
} }
@ -147,7 +147,7 @@ export class Autocomplete<Item extends ACItem> extends Disposable {
this._lastAsTyped = inputVal; this._lastAsTyped = inputVal;
// TODO We should perhaps debounce the search() call in some clever way, to avoid unnecessary // TODO We should perhaps debounce the search() call in some clever way, to avoid unnecessary
// searches while typing. Today, search() is synchronous in practice, so it doesn't matter. // searches while typing. Today, search() is synchronous in practice, so it doesn't matter.
const acResults = await this.options.search(inputVal); const acResults = await this._options.search(inputVal);
this._highlightFunc = acResults.highlightFunc; this._highlightFunc = acResults.highlightFunc;
this._items.set(acResults.items); this._items.set(acResults.items);

@ -31,7 +31,7 @@ export function isEquivalentFilter(state: FilterState, spec: FilterSpec): boolea
const other = makeFilterState(spec); const other = makeFilterState(spec);
if (state.include !== other.include) { return false; } if (state.include !== other.include) { return false; }
if (state.values.size !== other.values.size) { return false; } if (state.values.size !== other.values.size) { return false; }
for (const val of other.values) { if (!state.values.has(val)) { return false; }} for (const val of other.values) { if (!state.values.has(val)) { return false; } }
return true; return true;
} }

@ -92,7 +92,7 @@ export class SearchModelImpl extends Disposable implements SearchModel {
this.autoDispose(this.value.addListener(v => { void findFirst(v); })); this.autoDispose(this.value.addListener(v => { void findFirst(v); }));
// Set this.noMatch to false when multiPage gets turned ON. // Set this.noMatch to false when multiPage gets turned ON.
this.autoDispose(this.multiPage.addListener(v => { if (v) { this.noMatch.set(false); }})); this.autoDispose(this.multiPage.addListener(v => { if (v) { this.noMatch.set(false); } }));
// Schedule a search restart when user changes pages (otherwise search would resume from the // Schedule a search restart when user changes pages (otherwise search would resume from the
// previous page that is not shown anymore). Also revert noMatch flag when in single page mode. // previous page that is not shown anymore). Also revert noMatch flag when in single page mode.

@ -207,7 +207,7 @@ export class TreeNodeRecord implements TreeNode {
// Get all the records included in this item. // Get all the records included in this item.
public getRecords(): TreeRecord[] { public getRecords(): TreeRecord[] {
const records = [] as TreeRecord[]; const records = [] as TreeRecord[];
if (this.index !== "root") {records.push(this._records[this.index]); } if (this.index !== "root") { records.push(this._records[this.index]); }
walkTree(this, (item: TreeItemRecord) => records.push(this._records[item.index])); walkTree(this, (item: TreeItemRecord) => records.push(this._records[item.index]));
return records; return records;
} }
@ -255,7 +255,7 @@ export function find(model: TreeNode, func: (item: TreeItem) => boolean): TreeIt
if (children) { if (children) {
for (const child of children.get()) { for (const child of children.get()) {
const found = func(child) && child || find(child, func); const found = func(child) && child || find(child, func);
if (found) {return found; } if (found) { return found; }
} }
} }
} }

@ -91,7 +91,7 @@ function _getLoginLogoutUrl(method: 'login'|'logout'|'signin', nextUrl: string):
* only public interface is the urlState() accessor. * only public interface is the urlState() accessor.
*/ */
export class UrlStateImpl { export class UrlStateImpl {
constructor(private window: {gristConfig?: Partial<GristLoadConfig>}) {} constructor(private _window: {gristConfig?: Partial<GristLoadConfig>}) {}
/** /**
* The actual serialization of a url state into a URL. The URL has the form * The actual serialization of a url state into a URL. The URL has the form
@ -105,7 +105,7 @@ export class UrlStateImpl {
* localhost:8080/o/<org> * localhost:8080/o/<org>
*/ */
public encodeUrl(state: IGristUrlState, baseLocation: Location | URL): string { public encodeUrl(state: IGristUrlState, baseLocation: Location | URL): string {
const gristConfig = this.window.gristConfig || {}; const gristConfig = this._window.gristConfig || {};
return encodeUrl(gristConfig, state, baseLocation); return encodeUrl(gristConfig, state, baseLocation);
} }
@ -113,7 +113,7 @@ export class UrlStateImpl {
* Parse a URL location into an IGristUrlState object. See encodeUrl() documentation. * Parse a URL location into an IGristUrlState object. See encodeUrl() documentation.
*/ */
public decodeUrl(location: Location | URL): IGristUrlState { public decodeUrl(location: Location | URL): IGristUrlState {
const gristConfig = this.window.gristConfig || {}; const gristConfig = this._window.gristConfig || {};
return decodeUrl(gristConfig, location); return decodeUrl(gristConfig, location);
} }
@ -136,7 +136,7 @@ export class UrlStateImpl {
* a matter of DocWorker requiring a different version (e.g. /v/OTHER/doc/...). * a matter of DocWorker requiring a different version (e.g. /v/OTHER/doc/...).
*/ */
public needPageLoad(prevState: IGristUrlState, newState: IGristUrlState): boolean { public needPageLoad(prevState: IGristUrlState, newState: IGristUrlState): boolean {
const gristConfig = this.window.gristConfig || {}; const gristConfig = this._window.gristConfig || {};
const orgReload = prevState.org !== newState.org; const orgReload = prevState.org !== newState.org;
// Reload when moving to/from a document or between doc and non-doc. // Reload when moving to/from a document or between doc and non-doc.
const docReload = prevState.doc !== newState.doc; const docReload = prevState.doc !== newState.doc;

@ -368,25 +368,25 @@ export class FilteredRowSource extends BaseFilteredRowSource {
* Private helper object that maintains a set of rows for a particular group. * Private helper object that maintains a set of rows for a particular group.
*/ */
class RowGroupHelper<Value> extends RowSource { class RowGroupHelper<Value> extends RowSource {
private rows: Set<RowId> = new Set(); private _rows: Set<RowId> = new Set();
constructor(public readonly groupValue: Value) { constructor(public readonly groupValue: Value) {
super(); super();
} }
public getAllRows() { public getAllRows() {
return this.rows.values(); return this._rows.values();
} }
public getNumRows(): number { public getNumRows(): number {
return this.rows.size; return this._rows.size;
} }
public _addAll(rows: RowList) { public _addAll(rows: RowList) {
for (const r of rows) { this.rows.add(r); } for (const r of rows) { this._rows.add(r); }
} }
public _removeAll(rows: RowList) { public _removeAll(rows: RowList) {
for (const r of rows) { this.rows.delete(r); } for (const r of rows) { this._rows.delete(r); }
} }
} }

@ -245,8 +245,8 @@ function buildWorkspaceDocBlock(home: HomeModel, workspace: Workspace, flashDocI
// The flash value may change to true, and then immediately to false. We highlight it // The flash value may change to true, and then immediately to false. We highlight it
// using a transition, and scroll into view, when it turns back to false. // using a transition, and scroll into view, when it turns back to false.
transition(flash, { transition(flash, {
prepare(elem, val) { if (!val) { elem.style.backgroundColor = colors.slate.toString(); }}, prepare(elem, val) { if (!val) { elem.style.backgroundColor = colors.slate.toString(); } },
run(elem, val) { if (!val) { elem.style.backgroundColor = ''; scrollIntoViewIfNeeded(elem); }}, run(elem, val) { if (!val) { elem.style.backgroundColor = ''; scrollIntoViewIfNeeded(elem); } },
}) })
), ),
css.docRowWrapper.cls('-renaming', isRenaming), css.docRowWrapper.cls('-renaming', isRenaming),

@ -45,7 +45,7 @@ export abstract class MultiItemSelector<Item extends BaseItem> extends Disposabl
return cssMultiSelectorWrapper( return cssMultiSelectorWrapper(
cssItemList(testId('list'), cssItemList(testId('list'),
dom.forEach(this._incItems, item => this.buildItemDom(item)), dom.forEach(this._incItems, item => this.buildItemDom(item)),
this.buildAddItemDom(this._options.addItemLabel, this._options.addItemText) this._buildAddItemDom(this._options.addItemLabel, this._options.addItemText)
), ),
); );
} }
@ -60,7 +60,7 @@ export abstract class MultiItemSelector<Item extends BaseItem> extends Disposabl
// Called with an item from `_allItems` // Called with an item from `_allItems`
protected async remove(item: Item): Promise<void> { protected async remove(item: Item): Promise<void> {
const idx = this.findIncIndex(item); const idx = this._findIncIndex(item);
if (idx === -1) { return; } if (idx === -1) { return; }
this._incItems.splice(idx, 1); this._incItems.splice(idx, 1);
} }
@ -70,7 +70,7 @@ export abstract class MultiItemSelector<Item extends BaseItem> extends Disposabl
// Replaces an existing item (if found) with a new one // Replaces an existing item (if found) with a new one
protected async changeItem(item: Item, newItem: Item): Promise<void> { protected async changeItem(item: Item, newItem: Item): Promise<void> {
const idx = this.findIncIndex(item); const idx = this._findIncIndex(item);
if (idx === -1) { return; } if (idx === -1) { return; }
this._incItems.splice(idx, 1, newItem); this._incItems.splice(idx, 1, newItem);
} }
@ -82,7 +82,7 @@ export abstract class MultiItemSelector<Item extends BaseItem> extends Disposabl
selectCb: (newItem: Item) => void, selectCb: (newItem: Item) => void,
selectOptions?: {}): Element { selectOptions?: {}): Element {
const obs = computed(use => selectedValue).onWrite(async value => { const obs = computed(use => selectedValue).onWrite(async value => {
const newItem = this.findItemByValue(value); const newItem = this._findItemByValue(value);
if (newItem) { if (newItem) {
selectCb(newItem); selectCb(newItem);
} }
@ -115,17 +115,17 @@ export abstract class MultiItemSelector<Item extends BaseItem> extends Disposabl
} }
// Returns the index (order) of the item if it's been included, or -1 otherwise. // Returns the index (order) of the item if it's been included, or -1 otherwise.
private findIncIndex(item: Item): number { private _findIncIndex(item: Item): number {
return this._incItems.get().findIndex(_item => _item === item); return this._incItems.get().findIndex(_item => _item === item);
} }
// Returns the item object given it's value, or undefined if not found. // Returns the item object given it's value, or undefined if not found.
private findItemByValue(value: string): Item | undefined { private _findItemByValue(value: string): Item | undefined {
return this._allItems.get().find(_item => _item.value === value); return this._allItems.get().find(_item => _item.value === value);
} }
// Builds the about-to-be-added item // Builds the about-to-be-added item
private buildAddItemDom(defLabel: string, defText: string): Element { private _buildAddItemDom(defLabel: string, defText: string): Element {
const addNewItem: Observable<boolean> = observable(false); const addNewItem: Observable<boolean> = observable(false);
return dom('li', testId('add-item'), return dom('li', testId('add-item'),
dom.domComputed(addNewItem, isAdding => isAdding dom.domComputed(addNewItem, isAdding => isAdding

@ -196,7 +196,7 @@ export function buildPageWidgetPicker(
dom.create(PageWidgetSelect, value, tables, columns, onSaveCB, options), dom.create(PageWidgetSelect, value, tables, columns, onSaveCB, options),
// gives focus and binds keydown events // gives focus and binds keydown events
(elem: any) => {setTimeout(() => elem.focus(), 0); }, (elem: any) => { setTimeout(() => elem.focus(), 0); },
onKeyDown({ onKeyDown({
Escape: () => ctl.close(), Escape: () => ctl.close(),
Enter: () => isValid() && onSaveCB() Enter: () => isValid() && onSaveCB()

@ -82,7 +82,7 @@ function buildColorPicker(ctl: IOpenController, textColor: Observable<string>, f
}), }),
// gives focus and binds keydown events // gives focus and binds keydown events
(elem: any) => {setTimeout(() => elem.focus(), 0); }, (elem: any) => { setTimeout(() => elem.focus(), 0); },
onKeyDown({ onKeyDown({
Escape: () => { revert(); }, Escape: () => { revert(); },
Enter: () => { ctl.close(); }, Enter: () => { ctl.close(); },
@ -158,7 +158,7 @@ class PickerComponent extends Disposable {
), ),
cssHexBox( cssHexBox(
this._color, this._color,
async (val) => { if (isValidHex(val)) {this._model.setValue(val); }}, async (val) => { if (isValidHex(val)) { this._model.setValue(val); } },
testId(`${title}-hex`), testId(`${title}-hex`),
// select the hex value on click. Doing it using settimeout allows to avoid some // select the hex value on click. Doing it using settimeout allows to avoid some
// sporadically losing the selection just after the click. // sporadically losing the selection just after the click.

@ -105,7 +105,7 @@ export function rawTextInput(value: Observable<string>, save: SaveFunc, onChange
let inputEl: HTMLInputElement; let inputEl: HTMLInputElement;
// When label changes updates the input, unless in the middle of editing. // When label changes updates the input, unless in the middle of editing.
const lis = value.addListener((val) => { if (status !== Status.EDITING) { setValue(val); }}); const lis = value.addListener((val) => { if (status !== Status.EDITING) { setValue(val); } });
function setValue(val: string) { function setValue(val: string) {
inputEl.value = val; inputEl.value = val;

@ -128,8 +128,8 @@ export function searchBar(model: SearchModel, testId: TestId = noTestId) {
find: () => { inputElem.focus(); inputElem.select(); }, find: () => { inputElem.focus(); inputElem.select(); },
// On Mac, Firefox has a default behaviour witch causes to close the search bar on Cmd+g and // On Mac, Firefox has a default behaviour witch causes to close the search bar on Cmd+g and
// Cmd+shirt+G. Returning false is a Mousetrap convenience which prevents that. // Cmd+shirt+G. Returning false is a Mousetrap convenience which prevents that.
findNext: () => {model.findNext().catch(reportError); return false; }, findNext: () => { model.findNext().catch(reportError); return false; },
findPrev: () => {model.findPrev().catch(reportError); return false; }, findPrev: () => { model.findPrev().catch(reportError); return false; },
}, null, true); }, null, true);
const toggleMenu = debounce((_value?: boolean) => { const toggleMenu = debounce((_value?: boolean) => {

@ -105,7 +105,7 @@ export class AttachmentsEditor extends NewBaseEditor {
}), }),
// Close if clicking into the background. (The default modal's behavior for this isn't // Close if clicking into the background. (The default modal's behavior for this isn't
// triggered because our content covers the whole screen.) // triggered because our content covers the whole screen.)
dom.on('click', (ev, elem) => { if (ev.target === elem) { ctl.close(); }}), dom.on('click', (ev, elem) => { if (ev.target === elem) { ctl.close(); } }),
...this._buildDom(ctl) ...this._buildDom(ctl)
]; ];
}, {noEscapeKey: true}); }, {noEscapeKey: true});

@ -66,30 +66,30 @@ export class FieldBuilder extends Disposable {
public readonly widgetImpl: ko.Computed<NewAbstractWidget>; public readonly widgetImpl: ko.Computed<NewAbstractWidget>;
public readonly diffImpl: NewAbstractWidget; public readonly diffImpl: NewAbstractWidget;
private readonly availableTypes: Computed<Array<IOptionFull<string>>>; private readonly _availableTypes: Computed<Array<IOptionFull<string>>>;
private readonly readOnlyPureType: ko.PureComputed<string>; private readonly _readOnlyPureType: ko.PureComputed<string>;
private readonly isRightType: ko.PureComputed<(value: CellValue, options?: any) => boolean>; private readonly _isRightType: ko.PureComputed<(value: CellValue, options?: any) => boolean>;
private readonly refTableId: ko.Computed<string | null>; private readonly _refTableId: ko.Computed<string | null>;
private readonly isRef: ko.Computed<boolean>; private readonly _isRef: ko.Computed<boolean>;
private readonly _rowMap: Map<DataRowModel, Element>; private readonly _rowMap: Map<DataRowModel, Element>;
private readonly isTransformingFormula: ko.Computed<boolean>; private readonly _isTransformingFormula: ko.Computed<boolean>;
private readonly isTransformingType: ko.Computed<boolean>; private readonly _isTransformingType: ko.Computed<boolean>;
private readonly _fieldEditorHolder: Holder<IDisposable>; private readonly _fieldEditorHolder: Holder<IDisposable>;
private readonly widgetCons: ko.Computed<{create: (...args: any[]) => NewAbstractWidget}>; private readonly _widgetCons: ko.Computed<{create: (...args: any[]) => NewAbstractWidget}>;
private readonly docModel: DocModel; private readonly _docModel: DocModel;
public constructor(public readonly gristDoc: GristDoc, public readonly field: ViewFieldRec, public constructor(public readonly gristDoc: GristDoc, public readonly field: ViewFieldRec,
private _cursor: Cursor) { private _cursor: Cursor) {
super(); super();
this.docModel = gristDoc.docModel; this._docModel = gristDoc.docModel;
this.origColumn = field.column(); this.origColumn = field.column();
this.options = field.widgetOptionsJson; this.options = field.widgetOptionsJson;
this.readOnlyPureType = ko.pureComputed(() => this.field.column().pureType()); this._readOnlyPureType = ko.pureComputed(() => this.field.column().pureType());
// Observable with a list of available types. // Observable with a list of available types.
this.availableTypes = Computed.create(this, (use) => { this._availableTypes = Computed.create(this, (use) => {
const isFormula = use(this.origColumn.isFormula); const isFormula = use(this.origColumn.isFormula);
const types: Array<IOptionFull<string>> = []; const types: Array<IOptionFull<string>> = [];
_.each(UserType.typeDefs, (def: any, key: string|number) => { _.each(UserType.typeDefs, (def: any, key: string|number) => {
@ -108,17 +108,17 @@ 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(function() {
return gristTypes.isRightType(this.readOnlyPureType()) || _.constant(false); return gristTypes.isRightType(this._readOnlyPureType()) || _.constant(false);
}, this); }, this);
// Returns a boolean indicating whether the column is type Reference. // Returns a boolean indicating whether the column is type Reference.
this.isRef = this.autoDispose(ko.computed(() => { this._isRef = this.autoDispose(ko.computed(() => {
return gutil.startsWith(this.field.column().type(), 'Ref:'); return gutil.startsWith(this.field.column().type(), 'Ref:');
})); }));
// Gives the table ID to which the reference points. // Gives the table ID to which the reference points.
this.refTableId = this.autoDispose(ko.computed({ this._refTableId = this.autoDispose(ko.computed({
read: () => gutil.removePrefix(this.field.column().type(), "Ref:"), read: () => gutil.removePrefix(this.field.column().type(), "Ref:"),
write: val => this._setType(`Ref:${val}`) write: val => this._setType(`Ref:${val}`)
})); }));
@ -148,11 +148,11 @@ export class FieldBuilder extends Disposable {
this.columnTransform = null; this.columnTransform = null;
// Returns a boolean indicating whether a formula transform is in progress. // Returns a boolean indicating whether a formula transform is in progress.
this.isTransformingFormula = this.autoDispose(ko.computed(() => { this._isTransformingFormula = this.autoDispose(ko.computed(() => {
return this.field.column().isTransforming() && this.columnTransform instanceof FormulaTransform; return this.field.column().isTransforming() && this.columnTransform instanceof FormulaTransform;
})); }));
// Returns a boolean indicating whether a type transform is in progress. // Returns a boolean indicating whether a type transform is in progress.
this.isTransformingType = this.autoDispose(ko.computed(() => { this._isTransformingType = this.autoDispose(ko.computed(() => {
return (this.field.column().isTransforming() || this.isCallPending()) && return (this.field.column().isTransforming() || this.isCallPending()) &&
(this.columnTransform instanceof TypeTransform); (this.columnTransform instanceof TypeTransform);
})); }));
@ -165,14 +165,14 @@ 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(function() {
return UserTypeImpl.getWidgetConstructor(this.options().widget, return UserTypeImpl.getWidgetConstructor(this.options().widget,
this.readOnlyPureType()); this._readOnlyPureType());
}, this)).onlyNotifyUnequal()); }, this)).onlyNotifyUnequal());
// Computed builder for the widget. // Computed builder for the widget.
this.widgetImpl = this.autoDispose(koUtil.computedBuilder(() => { this.widgetImpl = this.autoDispose(koUtil.computedBuilder(() => {
const cons = this.widgetCons(); const cons = this._widgetCons();
// Must subscribe to `colId` so that field.colId is rechecked on transform. // Must subscribe to `colId` so that field.colId is rechecked on transform.
return cons.create.bind(cons, this.field, this.field.colId()); return cons.create.bind(cons, this.field, this.field.colId());
}, this).extend({ deferred: true })); }, this).extend({ deferred: true }));
@ -180,11 +180,8 @@ export class FieldBuilder extends Disposable {
this.diffImpl = this.autoDispose(DiffBox.create(this.field)); this.diffImpl = this.autoDispose(DiffBox.create(this.field));
} }
// dispose.makeDisposable(FieldBuilder);
public buildSelectWidgetDom() { public buildSelectWidgetDom() {
return grainjsDom.maybe((use) => !use(this.isTransformingType) && use(this.readOnlyPureType), type => { return grainjsDom.maybe((use) => !use(this._isTransformingType) && use(this._readOnlyPureType), type => {
const typeWidgets = getTypeDefinition(type).widgets; const typeWidgets = getTypeDefinition(type).widgets;
const widgetOptions = Object.keys(typeWidgets).map(label => ({ const widgetOptions = Object.keys(typeWidgets).map(label => ({
label, label,
@ -206,32 +203,32 @@ export class FieldBuilder extends Disposable {
* Build the type change dom. * Build the type change dom.
*/ */
public buildSelectTypeDom() { public buildSelectTypeDom() {
const selectType = Computed.create(null, (use) => use(fromKo(this.readOnlyPureType))); const selectType = Computed.create(null, (use) => use(fromKo(this._readOnlyPureType)));
selectType.onWrite(newType => newType === this.readOnlyPureType.peek() || this._setType(newType)); selectType.onWrite(newType => newType === this._readOnlyPureType.peek() || this._setType(newType));
const onDispose = () => (this.isDisposed() || selectType.set(this.field.column().pureType())); const onDispose = () => (this.isDisposed() || selectType.set(this.field.column().pureType()));
return [ return [
cssRow( cssRow(
grainjsDom.autoDispose(selectType), grainjsDom.autoDispose(selectType),
select(selectType, this.availableTypes, { select(selectType, this._availableTypes, {
disabled: (use) => use(this.isTransformingFormula) || use(this.origColumn.disableModifyBase) || disabled: (use) => use(this._isTransformingFormula) || use(this.origColumn.disableModifyBase) ||
use(this.isCallPending) use(this.isCallPending)
}), }),
testId('type-select') testId('type-select')
), ),
grainjsDom.maybe((use) => use(this.isRef) && !use(this.isTransformingType), () => this._buildRefTableSelect()), grainjsDom.maybe((use) => use(this._isRef) && !use(this._isTransformingType), () => this._buildRefTableSelect()),
grainjsDom.maybe(this.isTransformingType, () => { grainjsDom.maybe(this._isTransformingType, () => {
// Editor dom must be built before preparing transform. // Editor dom must be built before preparing transform.
return dom('div.type_transform_prompt', return dom('div.type_transform_prompt',
kf.prompt( kf.prompt(
dom('div', dom('div',
grainjsDom.maybe(this.isRef, () => this._buildRefTableSelect()), grainjsDom.maybe(this._isRef, () => this._buildRefTableSelect()),
grainjsDom.maybe((use) => use(this.field.column().isTransforming), grainjsDom.maybe((use) => use(this.field.column().isTransforming),
() => this.columnTransform!.buildDom()) () => this.columnTransform!.buildDom())
) )
), ),
grainjsDom.onDispose(onDispose) grainjsDom.onDispose(onDispose)
); );
}) })
]; ];
} }
@ -242,7 +239,7 @@ export class FieldBuilder extends Disposable {
// Do not type transform a new/empty column or a formula column. Just make a best guess for // Do not type transform a new/empty column or a formula column. Just make a best guess for
// the full type, and set it. // the full type, and set it.
const column = this.field.column(); const column = this.field.column();
column.type.setAndSave(addColTypeSuffix(newType, column, this.docModel)).catch(reportError); column.type.setAndSave(addColTypeSuffix(newType, column, this._docModel)).catch(reportError);
} else if (!this.columnTransform) { } else if (!this.columnTransform) {
this.columnTransform = TypeTransform.create(null, this.gristDoc, this); this.columnTransform = TypeTransform.create(null, this.gristDoc, this);
return this.columnTransform.prepare(newType); return this.columnTransform.prepare(newType);
@ -256,7 +253,7 @@ export class FieldBuilder extends Disposable {
// Builds the reference type table selector. Built when the column is type reference. // Builds the reference type table selector. Built when the column is type reference.
public _buildRefTableSelect() { public _buildRefTableSelect() {
const allTables = Computed.create(null, (use) => const allTables = Computed.create(null, (use) =>
use(this.docModel.allTableIds.getObservable()).map(tableId => ({ use(this._docModel.allTableIds.getObservable()).map(tableId => ({
value: tableId, value: tableId,
label: tableId, label: tableId,
icon: 'FieldTable' as const icon: 'FieldTable' as const
@ -266,7 +263,7 @@ export class FieldBuilder extends Disposable {
cssLabel('DATA FROM TABLE'), cssLabel('DATA FROM TABLE'),
cssRow( cssRow(
dom.autoDispose(allTables), dom.autoDispose(allTables),
select(fromKo(this.refTableId), allTables), select(fromKo(this._refTableId), allTables),
testId('ref-table-select') testId('ref-table-select')
) )
]; ];
@ -301,15 +298,15 @@ export class FieldBuilder extends Disposable {
kf.checkButton(transformButton, kf.checkButton(transformButton,
dom('span.glyphicon.glyphicon-flash'), dom('span.glyphicon.glyphicon-flash'),
dom.testId("FieldBuilder_editTransform"), dom.testId("FieldBuilder_editTransform"),
kd.toggleClass('disabled', () => this.isTransformingType() || this.origColumn.isFormula() || kd.toggleClass('disabled', () => this._isTransformingType() || this.origColumn.isFormula() ||
this.origColumn.disableModifyBase()) this.origColumn.disableModifyBase())
) )
) )
), ),
kd.maybe(this.isTransformingFormula, () => { kd.maybe(this._isTransformingFormula, () => {
return this.columnTransform!.buildDom(); return this.columnTransform!.buildDom();
}) })
); );
} }
/** /**
@ -319,7 +316,7 @@ export class FieldBuilder extends Disposable {
// NOTE: adding a grainjsDom .maybe here causes the disposable order of the widgetImpl and // NOTE: adding a grainjsDom .maybe here causes the disposable order of the widgetImpl and
// the dom created by the widgetImpl to get out of sync. // the dom created by the widgetImpl to get out of sync.
return dom('div', return dom('div',
kd.maybe(() => !this.isTransformingType() && this.widgetImpl(), (widget: NewAbstractWidget) => kd.maybe(() => !this._isTransformingType() && this.widgetImpl(), (widget: NewAbstractWidget) =>
dom('div', dom('div',
widget.buildConfigDom(), widget.buildConfigDom(),
widget.buildColorConfigDom(), widget.buildColorConfigDom(),
@ -401,7 +398,7 @@ export class FieldBuilder extends Disposable {
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;
@ -462,7 +459,7 @@ export class FieldBuilder extends Disposable {
return; return;
} }
const editorCtor = UserTypeImpl.getEditorConstructor(this.options().widget, this.readOnlyPureType()); const editorCtor = UserTypeImpl.getEditorConstructor(this.options().widget, this._readOnlyPureType());
// constructor may be null for a read-only non-formula field, though not today. // constructor may be null for a read-only non-formula field, though not today.
if (!editorCtor) { if (!editorCtor) {
// Actually, we only expect buildEditorDom() to be called when isEditorActive() is false (i.e. // Actually, we only expect buildEditorDom() to be called when isEditorActive() is false (i.e.

@ -47,10 +47,10 @@ export async function setAndSave(editRow: DataRowModel, field: ViewFieldRec, val
} }
} }
export type FieldEditorStateEvent = { export interface FieldEditorStateEvent {
position : CellPosition, position: CellPosition;
currentState : any, currentState: any;
type: string type: string;
} }
export class FieldEditor extends Disposable { export class FieldEditor extends Disposable {
@ -125,7 +125,7 @@ export class FieldEditor extends Disposable {
unmakeFormula: () => this._unmakeFormula(), unmakeFormula: () => this._unmakeFormula(),
}; };
const state : any = options.state; const state: any = options.state;
this.rebuildEditor(isFormula, editValue, Number.POSITIVE_INFINITY, state); this.rebuildEditor(isFormula, editValue, Number.POSITIVE_INFINITY, state);
@ -141,7 +141,7 @@ export class FieldEditor extends Disposable {
} }
// cursorPos refers to the position of the caret within the editor. // cursorPos refers to the position of the caret within the editor.
public rebuildEditor(isFormula: boolean, editValue: string|undefined, cursorPos: number, state? : any) { public rebuildEditor(isFormula: boolean, editValue: string|undefined, cursorPos: number, state?: any) {
const editorCtor: IEditorConstructor = isFormula ? FormulaEditor : this._editorCtor; const editorCtor: IEditorConstructor = isFormula ? FormulaEditor : this._editorCtor;
const column = this._field.column(); const column = this._field.column();
@ -168,11 +168,11 @@ export class FieldEditor extends Disposable {
// if editor supports live changes, connect it to the change emitter // if editor supports live changes, connect it to the change emitter
if (editor.editorState) { if (editor.editorState) {
editor.autoDispose(editor.editorState.addListener((currentState) => { editor.autoDispose(editor.editorState.addListener((currentState) => {
const event : FieldEditorStateEvent = { const event: FieldEditorStateEvent = {
position : this.cellPosition(), position: this._cellPosition(),
currentState, currentState,
type : this._field.column.peek().pureType.peek() type: this._field.column.peek().pureType.peek()
} };
this.changeEmitter.emit(event); this.changeEmitter.emit(event);
})); }));
} }
@ -181,7 +181,7 @@ export class FieldEditor extends Disposable {
} }
// calculate current cell's absolute position // calculate current cell's absolute position
private cellPosition() { private _cellPosition() {
const rowId = this._editRow.getRowId(); const rowId = this._editRow.getRowId();
const colRef = this._field.colRef.peek(); const colRef = this._field.colRef.peek();
const sectionId = this._field.viewSection.peek().id.peek(); const sectionId = this._field.viewSection.peek().id.peek();
@ -189,7 +189,7 @@ export class FieldEditor extends Disposable {
rowId, rowId,
colRef, colRef,
sectionId sectionId
} };
return position; return position;
} }
@ -240,11 +240,11 @@ export class FieldEditor extends Disposable {
// Cancels the edit // Cancels the edit
private _cancelEdit() { private _cancelEdit() {
const event : FieldEditorStateEvent = { const event: FieldEditorStateEvent = {
position : this.cellPosition(), position: this._cellPosition(),
currentState : this._editorHolder.get()?.editorState?.get(), currentState: this._editorHolder.get()?.editorState?.get(),
type : this._field.column.peek().pureType.peek() type: this._field.column.peek().pureType.peek()
} };
this.cancelEmitter.emit(event); this.cancelEmitter.emit(event);
this.dispose(); this.dispose();
} }
@ -296,11 +296,11 @@ export class FieldEditor extends Disposable {
} }
} }
const event : FieldEditorStateEvent = { const event: FieldEditorStateEvent = {
position : this.cellPosition(), position: this._cellPosition(),
currentState : this._editorHolder.get()?.editorState?.get(), currentState: this._editorHolder.get()?.editorState?.get(),
type : this._field.column.peek().pureType.peek() type: this._field.column.peek().pureType.peek()
} };
this.saveEmitter.emit(event); this.saveEmitter.emit(event);
const cursor = this._cursor; const cursor = this._cursor;

@ -13,7 +13,7 @@ import {dom, Observable} from 'grainjs';
export class NTextEditor extends NewBaseEditor { export class NTextEditor extends NewBaseEditor {
// Observable with current editor state (used by drafts or latest edit/position component) // Observable with current editor state (used by drafts or latest edit/position component)
public readonly editorState : Observable<string>; public readonly editorState: Observable<string>;
protected cellEditorDiv: HTMLElement; protected cellEditorDiv: HTMLElement;
protected textInput: HTMLTextAreaElement; protected textInput: HTMLTextAreaElement;
@ -29,7 +29,7 @@ export class NTextEditor extends NewBaseEditor {
constructor(options: Options) { constructor(options: Options) {
super(options); super(options);
const initialValue : string = undef( const initialValue: string = undef(
options.state as string | undefined, options.state as string | undefined,
options.editValue, String(options.cellValue ?? "")); options.editValue, String(options.cellValue ?? ""));
this.editorState = Observable.create<string>(this, initialValue); this.editorState = Observable.create<string>(this, initialValue);
@ -96,10 +96,10 @@ export class NTextEditor extends NewBaseEditor {
*/ */
protected onInput() { protected onInput() {
// Resize the textbox whenever user types in it. // Resize the textbox whenever user types in it.
this.resizeInput() this.resizeInput();
// notify about current state // notify about current state
this.editorState.set(String(this.getTextValue())) this.editorState.set(String(this.getTextValue()));
} }
/** /**

@ -21,7 +21,7 @@ export interface Options {
editValue?: string; editValue?: string;
cursorPos: number; cursorPos: number;
commands: IEditorCommandGroup; commands: IEditorCommandGroup;
state? : any; state?: any;
} }
/** /**
@ -58,7 +58,7 @@ export abstract class NewBaseEditor extends Disposable {
/** /**
* Current state of the editor. Optional, not all editors will report theirs current state. * Current state of the editor. Optional, not all editors will report theirs current state.
*/ */
public editorState? : Observable<any>; public editorState?: Observable<any>;
constructor(protected options: Options) { constructor(protected options: Options) {
super(); super();

@ -171,7 +171,7 @@ export function summarizePermissionSet(pset: PartialPermissionSet): MixedPermiss
for (const key of Object.keys(pset) as Array<keyof PartialPermissionSet>) { for (const key of Object.keys(pset) as Array<keyof PartialPermissionSet>) {
const pWithSome = pset[key]; const pWithSome = pset[key];
// "Some" postfix is not significant for summarization. // "Some" postfix is not significant for summarization.
const p = pWithSome === 'allowSome' ? 'allow' : (pWithSome === 'denySome' ? 'deny' : pWithSome) const p = pWithSome === 'allowSome' ? 'allow' : (pWithSome === 'denySome' ? 'deny' : pWithSome);
if (!p || p === sign) { continue; } if (!p || p === sign) { continue; }
if (!sign) { if (!sign) {
sign = p; sign = p;

@ -240,7 +240,7 @@ export function getAffectedTables(summary: ActionSummary): string[] {
*/ */
export function getTableIdBefore(renames: LabelDelta[], tableIdAfter: string|null): string|null { export function getTableIdBefore(renames: LabelDelta[], tableIdAfter: string|null): string|null {
if (tableIdAfter === null) { return tableIdAfter; } if (tableIdAfter === null) { return tableIdAfter; }
const rename = renames.find(rename => rename[1] === tableIdAfter); const rename = renames.find(_rename => _rename[1] === tableIdAfter);
return rename ? rename[0] : tableIdAfter; return rename ? rename[0] : tableIdAfter;
} }
@ -250,7 +250,7 @@ export function getTableIdBefore(renames: LabelDelta[], tableIdAfter: string|nul
*/ */
export function getTableIdAfter(renames: LabelDelta[], tableIdBefore: string|null): string|null { export function getTableIdAfter(renames: LabelDelta[], tableIdBefore: string|null): string|null {
if (tableIdBefore === null) { return tableIdBefore; } if (tableIdBefore === null) { return tableIdBefore; }
const rename = renames.find(rename => rename[0] === tableIdBefore); const rename = renames.find(_rename => _rename[0] === tableIdBefore);
const tableIdAfter = rename ? rename[1] : tableIdBefore; const tableIdAfter = rename ? rename[1] : tableIdBefore;
if (tableIdAfter?.startsWith('-')) { return null; } if (tableIdAfter?.startsWith('-')) { return null; }
return tableIdAfter; return tableIdAfter;

@ -78,7 +78,7 @@ export interface UserAttributeRule {
* Check some key facts about the formula. * Check some key facts about the formula.
*/ */
export function getFormulaProperties(formula: ParsedAclFormula) { export function getFormulaProperties(formula: ParsedAclFormula) {
const result: FormulaProperties = {} const result: FormulaProperties = {};
if (usesRec(formula)) { result.hasRecOrNewRec = true; } if (usesRec(formula)) { result.hasRecOrNewRec = true; }
const colIds = new Set<string>(); const colIds = new Set<string>();
collectRecColIds(formula, colIds); collectRecColIds(formula, colIds);

@ -633,8 +633,8 @@ export class DocWorkerAPIImpl extends BaseAPI implements DocWorkerAPI {
super(_options); super(_options);
} }
public async importDocToWorkspace(uploadId: number, workspaceId: number, browserSettings?: BrowserSettings) public async importDocToWorkspace(uploadId: number, workspaceId: number, browserSettings?: BrowserSettings):
: Promise<DocCreationInfo> { Promise<DocCreationInfo> {
return this.requestJson(`${this.url}/api/workspaces/${workspaceId}/import`, { return this.requestJson(`${this.url}/api/workspaces/${workspaceId}/import`, {
method: 'POST', method: 'POST',
body: JSON.stringify({ uploadId, browserSettings }) body: JSON.stringify({ uploadId, browserSettings })

@ -121,9 +121,9 @@ type Undef<T> = T extends [infer A, infer B, infer C, infer D] ?
* Returns the first defined value from the list or unknown. * Returns the first defined value from the list or unknown.
* Use with typed result, so the typescript type checker can provide correct type. * Use with typed result, so the typescript type checker can provide correct type.
*/ */
export function undef<T extends Array<any>>(...list : T): Undef<T> { export function undef<T extends Array<any>>(...list: T): Undef<T> {
for(const value of list) { for(const value of list) {
if (value !== undefined) return value; if (value !== undefined) { return value; }
} }
return undefined as any; return undefined as any;
} }
@ -853,7 +853,7 @@ export function isValidHex(val: string): boolean {
*/ */
export async function isLongerThan(promise: Promise<any>, timeoutMsec: number): Promise<boolean> { export async function isLongerThan(promise: Promise<any>, timeoutMsec: number): Promise<boolean> {
let isPending = true; let isPending = true;
const done = () => {isPending = false; }; const done = () => { isPending = false; };
await Promise.race([ await Promise.race([
promise.then(done, done), promise.then(done, done),
delay(timeoutMsec) delay(timeoutMsec)

@ -126,34 +126,34 @@ export class WrappedObj {
* communication with PyPy-based sandbox.) * communication with PyPy-based sandbox.)
*/ */
export class Marshaller { export class Marshaller {
private memBuf: MemBuffer; private _memBuf: MemBuffer;
private readonly floatCode: number; private readonly _floatCode: number;
private readonly stringCode: number; private readonly _stringCode: number;
constructor(options?: MarshalOptions) { constructor(options?: MarshalOptions) {
this.memBuf = new MemBuffer(undefined); this._memBuf = new MemBuffer(undefined);
this.floatCode = options && options.version && options.version >= 2 ? marshalCodes.BFLOAT : marshalCodes.FLOAT; this._floatCode = options && options.version && options.version >= 2 ? marshalCodes.BFLOAT : marshalCodes.FLOAT;
this.stringCode = options && options.stringToBuffer ? marshalCodes.STRING : marshalCodes.UNICODE; this._stringCode = options && options.stringToBuffer ? marshalCodes.STRING : marshalCodes.UNICODE;
} }
public dump(): Uint8Array { public dump(): Uint8Array {
// asByteArray returns a view on the underlying data, and the constructor creates a new copy. // asByteArray returns a view on the underlying data, and the constructor creates a new copy.
// For some usages, we may want to avoid making the copy. // For some usages, we may want to avoid making the copy.
const bytes = new Uint8Array(this.memBuf.asByteArray()); const bytes = new Uint8Array(this._memBuf.asByteArray());
this.memBuf.clear(); this._memBuf.clear();
return bytes; return bytes;
} }
public dumpAsBuffer(): Buffer { public dumpAsBuffer(): Buffer {
const bytes = Buffer.from(this.memBuf.asByteArray()); const bytes = Buffer.from(this._memBuf.asByteArray());
this.memBuf.clear(); this._memBuf.clear();
return bytes; return bytes;
} }
public getCode(value: any) { public getCode(value: any) {
switch (typeof value) { switch (typeof value) {
case 'number': return isInteger(value) ? marshalCodes.INT : this.floatCode; case 'number': return isInteger(value) ? marshalCodes.INT : this._floatCode;
case 'string': return this.stringCode; case 'string': return this._stringCode;
case 'boolean': return value ? marshalCodes.TRUE : marshalCodes.FALSE; case 'boolean': return value ? marshalCodes.TRUE : marshalCodes.FALSE;
case 'undefined': return marshalCodes.NONE; case 'undefined': return marshalCodes.NONE;
case 'object': { case 'object': {
@ -181,16 +181,16 @@ export class Marshaller {
if (value instanceof WrappedObj) { if (value instanceof WrappedObj) {
value = value.value; value = value.value;
} }
this.memBuf.writeUint8(code); this._memBuf.writeUint8(code);
switch (code) { switch (code) {
case marshalCodes.NULL: return; case marshalCodes.NULL: return;
case marshalCodes.NONE: return; case marshalCodes.NONE: return;
case marshalCodes.FALSE: return; case marshalCodes.FALSE: return;
case marshalCodes.TRUE: return; case marshalCodes.TRUE: return;
case marshalCodes.INT: return this.memBuf.writeInt32LE(value); case marshalCodes.INT: return this._memBuf.writeInt32LE(value);
case marshalCodes.INT64: return this._writeInt64(value); case marshalCodes.INT64: return this._writeInt64(value);
case marshalCodes.FLOAT: return this._writeStringFloat(value); case marshalCodes.FLOAT: return this._writeStringFloat(value);
case marshalCodes.BFLOAT: return this.memBuf.writeFloat64LE(value); case marshalCodes.BFLOAT: return this._memBuf.writeFloat64LE(value);
case marshalCodes.STRING: case marshalCodes.STRING:
return (value instanceof Uint8Array || Buffer.isBuffer(value) ? return (value instanceof Uint8Array || Buffer.isBuffer(value) ?
this._writeByteArray(value) : this._writeByteArray(value) :
@ -219,8 +219,8 @@ export class Marshaller {
// TODO We could actually support 53 bits or so. // TODO We could actually support 53 bits or so.
throw new Error("Marshaller: int64 still only supports 32-bit ints for now: " + value); throw new Error("Marshaller: int64 still only supports 32-bit ints for now: " + value);
} }
this.memBuf.writeInt32LE(value); this._memBuf.writeInt32LE(value);
this.memBuf.writeInt32LE(value >= 0 ? 0 : -1); this._memBuf.writeInt32LE(value >= 0 ? 0 : -1);
} }
private _writeStringFloat(value: number) { private _writeStringFloat(value: number) {
@ -230,28 +230,28 @@ export class Marshaller {
if (bytes.byteLength >= 127) { if (bytes.byteLength >= 127) {
throw new Error("Marshaller: Trying to write a float that takes " + bytes.byteLength + " bytes"); throw new Error("Marshaller: Trying to write a float that takes " + bytes.byteLength + " bytes");
} }
this.memBuf.writeUint8(bytes.byteLength); this._memBuf.writeUint8(bytes.byteLength);
this.memBuf.writeByteArray(bytes); this._memBuf.writeByteArray(bytes);
} }
private _writeByteArray(value: Uint8Array|Buffer) { private _writeByteArray(value: Uint8Array|Buffer) {
// This works for both Uint8Arrays and Node Buffers. // This works for both Uint8Arrays and Node Buffers.
this.memBuf.writeInt32LE(value.length); this._memBuf.writeInt32LE(value.length);
this.memBuf.writeByteArray(value); this._memBuf.writeByteArray(value);
} }
private _writeUtf8String(value: string) { private _writeUtf8String(value: string) {
const offset = this.memBuf.size(); const offset = this._memBuf.size();
// We don't know the length until we write the value. // We don't know the length until we write the value.
this.memBuf.writeInt32LE(0); this._memBuf.writeInt32LE(0);
this.memBuf.writeString(value); this._memBuf.writeString(value);
const byteLength = this.memBuf.size() - offset - 4; const byteLength = this._memBuf.size() - offset - 4;
// Overwrite the 0 length we wrote earlier with the correct byte length. // Overwrite the 0 length we wrote earlier with the correct byte length.
this.memBuf.asDataView.setInt32(this.memBuf.startPos + offset, byteLength, true); this._memBuf.asDataView.setInt32(this._memBuf.startPos + offset, byteLength, true);
} }
private _writeList(array: unknown[]) { private _writeList(array: unknown[]) {
this.memBuf.writeInt32LE(array.length); this._memBuf.writeInt32LE(array.length);
for (const item of array) { for (const item of array) {
this.marshal(item); this.marshal(item);
} }
@ -264,7 +264,7 @@ export class Marshaller {
this.marshal(key); this.marshal(key);
this.marshal(obj[key]); this.marshal(obj[key]);
} }
this.memBuf.writeUint8(marshalCodes.NULL); this._memBuf.writeUint8(marshalCodes.NULL);
} }
} }
@ -283,17 +283,17 @@ const TwoTo15 = 0x8000; // 2**15
*/ */
export class Unmarshaller extends EventEmitter { export class Unmarshaller extends EventEmitter {
public memBuf: MemBuffer; public memBuf: MemBuffer;
private consumer: any = null; private _consumer: any = null;
private _lastCode: number|null = null; private _lastCode: number|null = null;
private readonly bufferToString: boolean; private readonly _bufferToString: boolean;
private emitter: (v: any) => boolean; private _emitter: (v: any) => boolean;
private stringTable: Array<string|Uint8Array> = []; private _stringTable: Array<string|Uint8Array> = [];
constructor(options?: UnmarshalOptions) { constructor(options?: UnmarshalOptions) {
super(); super();
this.memBuf = new MemBuffer(undefined); this.memBuf = new MemBuffer(undefined);
this.bufferToString = Boolean(options && options.bufferToString); this._bufferToString = Boolean(options && options.bufferToString);
this.emitter = this.emit.bind(this, 'value'); this._emitter = this.emit.bind(this, 'value');
} }
/** /**
@ -301,7 +301,7 @@ export class Unmarshaller extends EventEmitter {
* @param {Uint8Array|Buffer} byteArray: Uint8Array or Node Buffer with bytes to parse. * @param {Uint8Array|Buffer} byteArray: Uint8Array or Node Buffer with bytes to parse.
*/ */
public push(byteArray: Uint8Array|Buffer) { public push(byteArray: Uint8Array|Buffer) {
this.parse(byteArray, this.emitter); this.parse(byteArray, this._emitter);
} }
/** /**
@ -312,13 +312,13 @@ export class Unmarshaller extends EventEmitter {
this.memBuf.writeByteArray(byteArray); this.memBuf.writeByteArray(byteArray);
try { try {
while (this.memBuf.size() > 0) { while (this.memBuf.size() > 0) {
this.consumer = this.memBuf.makeConsumer(); this._consumer = this.memBuf.makeConsumer();
// Have to reset stringTable for interned strings before each top-level parse call. // Have to reset stringTable for interned strings before each top-level parse call.
this.stringTable.length = 0; this._stringTable.length = 0;
const value = this._parse(); const value = this._parse();
this.memBuf.consume(this.consumer); this.memBuf.consume(this._consumer);
if (valueCB(value) === false) { if (valueCB(value) === false) {
return; return;
} }
@ -341,7 +341,7 @@ export class Unmarshaller extends EventEmitter {
} }
private _parse(): unknown { private _parse(): unknown {
const code = this.memBuf.readUint8(this.consumer); const code = this.memBuf.readUint8(this._consumer);
this._lastCode = code; this._lastCode = code;
switch (code) { switch (code) {
case marshalCodes.NULL: return null; case marshalCodes.NULL: return null;
@ -374,12 +374,12 @@ export class Unmarshaller extends EventEmitter {
} }
private _parseInt() { private _parseInt() {
return this.memBuf.readInt32LE(this.consumer); return this.memBuf.readInt32LE(this._consumer);
} }
private _parseInt64() { private _parseInt64() {
const low = this.memBuf.readInt32LE(this.consumer); const low = this.memBuf.readInt32LE(this._consumer);
const hi = this.memBuf.readInt32LE(this.consumer); const hi = this.memBuf.readInt32LE(this._consumer);
if ((hi === 0 && low >= 0) || (hi === -1 && low < 0)) { if ((hi === 0 && low >= 0) || (hi === -1 && low < 0)) {
return low; return low;
} }
@ -395,46 +395,46 @@ export class Unmarshaller extends EventEmitter {
private _parseLong() { private _parseLong() {
// The format is a 32-bit size whose sign is the sign of the result, followed by 16-bit digits // The format is a 32-bit size whose sign is the sign of the result, followed by 16-bit digits
// in base 2**15. // in base 2**15.
const size = this.memBuf.readInt32LE(this.consumer); const size = this.memBuf.readInt32LE(this._consumer);
const sign = size < 0 ? -1 : 1; const sign = size < 0 ? -1 : 1;
const numDigits = size < 0 ? -size : size; const numDigits = size < 0 ? -size : size;
const digits = []; const digits = [];
for (let i = 0; i < numDigits; i++) { for (let i = 0; i < numDigits; i++) {
digits.push(this.memBuf.readInt16LE(this.consumer)); digits.push(this.memBuf.readInt16LE(this._consumer));
} }
return new BigInt(TwoTo15, digits, sign).toNative(); return new BigInt(TwoTo15, digits, sign).toNative();
} }
private _parseStringFloat() { private _parseStringFloat() {
const len = this.memBuf.readUint8(this.consumer); const len = this.memBuf.readUint8(this._consumer);
const buf = this.memBuf.readString(this.consumer, len); const buf = this.memBuf.readString(this._consumer, len);
return parseFloat(buf); return parseFloat(buf);
} }
private _parseBinaryFloat() { private _parseBinaryFloat() {
return this.memBuf.readFloat64LE(this.consumer); return this.memBuf.readFloat64LE(this._consumer);
} }
private _parseByteString(): string|Uint8Array { private _parseByteString(): string|Uint8Array {
const len = this.memBuf.readInt32LE(this.consumer); const len = this.memBuf.readInt32LE(this._consumer);
return (this.bufferToString ? return (this._bufferToString ?
this.memBuf.readString(this.consumer, len) : this.memBuf.readString(this._consumer, len) :
this.memBuf.readByteArray(this.consumer, len)); this.memBuf.readByteArray(this._consumer, len));
} }
private _parseInterned() { private _parseInterned() {
const s = this._parseByteString(); const s = this._parseByteString();
this.stringTable.push(s); this._stringTable.push(s);
return s; return s;
} }
private _parseStringRef() { private _parseStringRef() {
const index = this._parseInt(); const index = this._parseInt();
return this.stringTable[index]; return this._stringTable[index];
} }
private _parseList() { private _parseList() {
const len = this.memBuf.readInt32LE(this.consumer); const len = this.memBuf.readInt32LE(this._consumer);
const value = []; const value = [];
for (let i = 0; i < len; i++) { for (let i = 0; i < len; i++) {
value[i] = this._parse(); value[i] = this._parse();
@ -461,8 +461,8 @@ export class Unmarshaller extends EventEmitter {
} }
private _parseUnicode() { private _parseUnicode() {
const len = this.memBuf.readInt32LE(this.consumer); const len = this.memBuf.readInt32LE(this._consumer);
return this.memBuf.readString(this.consumer, len); return this.memBuf.readString(this._consumer, len);
} }
} }

@ -364,7 +364,7 @@ export class ApiServer {
const userId = getAuthorizedUserId(req); const userId = getAuthorizedUserId(req);
await this._dbManager.connection.transaction(async manager => { await this._dbManager.connection.transaction(async manager => {
const user = await manager.findOne(User, userId); const user = await manager.findOne(User, userId);
if (!user) {return handleDeletedUser(); } if (!user) { return handleDeletedUser(); }
user.apiKey = null; user.apiKey = null;
await manager.save(User, user); await manager.save(User, user);
}); });

@ -337,9 +337,9 @@ export class DocWorkerMap implements IDocWorkerMap {
if (docId === 'import') { if (docId === 'import') {
const lock = await this._redlock.lock(`workers-lock`, LOCK_TIMEOUT); const lock = await this._redlock.lock(`workers-lock`, LOCK_TIMEOUT);
try { try {
const workerId = await this._client.srandmemberAsync(`workers-available-default`); const _workerId = await this._client.srandmemberAsync(`workers-available-default`);
if (!workerId) { throw new Error('no doc worker available'); } if (!_workerId) { throw new Error('no doc worker available'); }
const docWorker = await this._client.hgetallAsync(`worker-${workerId}`) as DocWorkerInfo|null; const docWorker = await this._client.hgetallAsync(`worker-${_workerId}`) as DocWorkerInfo|null;
if (!docWorker) { throw new Error('no doc worker contact info available'); } if (!docWorker) { throw new Error('no doc worker contact info available'); }
return { return {
docMD5: null, docMD5: null,

@ -172,7 +172,7 @@ if (typeof window !== 'undefined') {
// running under mocha. For now, we only provide a disfunctional implementation. It allows // running under mocha. For now, we only provide a disfunctional implementation. It allows
// plugins to call methods like registerFunction() without failing, so that plugin code may be // plugins to call methods like registerFunction() without failing, so that plugin code may be
// imported, but the methods don't do anything useful. // imported, but the methods don't do anything useful.
rpc.setSendMessage((data) => {return; }); rpc.setSendMessage((data) => { return; });
} }
function createRpcLogger(): IRpcLogger { function createRpcLogger(): IRpcLogger {

@ -188,11 +188,13 @@ export class ChecksummedExternalStorage implements ExternalStorage {
if (!snapshotIds) { if (!snapshotIds) {
await this._options.latestVersion.save(key, DELETED_TOKEN); await this._options.latestVersion.save(key, DELETED_TOKEN);
await this._options.sharedHash.save(key, DELETED_TOKEN); await this._options.sharedHash.save(key, DELETED_TOKEN);
} else for (const snapshotId of snapshotIds) { } else {
// Removing snapshots breaks their partial immutability, so we mark them for (const snapshotId of snapshotIds) {
// as deleted in redis so that we don't get stale info from S3 if we check // Removing snapshots breaks their partial immutability, so we mark them
// for their existence. Nothing currently depends on this in practice. // as deleted in redis so that we don't get stale info from S3 if we check
await this._options.sharedHash.save(this._keyWithSnapshot(key, snapshotId), DELETED_TOKEN); // for their existence. Nothing currently depends on this in practice.
await this._options.sharedHash.save(this._keyWithSnapshot(key, snapshotId), DELETED_TOKEN);
}
} }
} catch (err) { } catch (err) {
log.error("ext %s delete: %s failure to remove, error %s", this.label, key, err.message); log.error("ext %s delete: %s failure to remove, error %s", this.label, key, err.message);

@ -907,7 +907,7 @@ export class FlexServer implements GristServer {
this.tagChecker.requireTag this.tagChecker.requireTag
]; ];
this.addSupportPaths(docAccessMiddleware); this._addSupportPaths(docAccessMiddleware);
if (!isSingleUserMode()) { if (!isSingleUserMode()) {
addDocApiRoutes(this.app, docWorker, this._docWorkerMap, docManager, this.dbManager, this); addDocApiRoutes(this.app, docWorker, this._docWorkerMap, docManager, this.dbManager, this);
@ -1081,8 +1081,8 @@ export class FlexServer implements GristServer {
); );
const config = {errPage, errMessage: err.message || err}; const config = {errPage, errMessage: err.message || err};
await this._sendAppPage(req, resp, {path: 'error.html', status: err.status || 400, config}); await this._sendAppPage(req, resp, {path: 'error.html', status: err.status || 400, config});
} catch (err) { } catch (error) {
return next(err); return next(error);
} }
}); });
} }
@ -1183,7 +1183,7 @@ export class FlexServer implements GristServer {
} }
// Adds endpoints that support imports and exports. // Adds endpoints that support imports and exports.
private addSupportPaths(docAccessMiddleware: express.RequestHandler[]) { private _addSupportPaths(docAccessMiddleware: express.RequestHandler[]) {
if (!this._docWorker) { throw new Error("need DocWorker"); } if (!this._docWorker) { throw new Error("need DocWorker"); }
this.app.get('/download', ...docAccessMiddleware, expressWrap(async (req, res) => { this.app.get('/download', ...docAccessMiddleware, expressWrap(async (req, res) => {

@ -598,7 +598,7 @@ export class GranularAccess implements GranularAccessForBundle {
const message = { actionGroup, docActions }; const message = { actionGroup, docActions };
await this._docClients.broadcastDocMessage(client, 'docUserAction', await this._docClients.broadcastDocMessage(client, 'docUserAction',
message, message,
(docSession) => this._filterDocUpdate(docSession, message)); (_docSession) => this._filterDocUpdate(_docSession, message));
} }
/** /**
@ -786,21 +786,21 @@ export class GranularAccess implements GranularAccessForBundle {
// If the column is not row dependent, we have nothing to do. // If the column is not row dependent, we have nothing to do.
if (access.getColumnAccess(tableId, colId).perms.read !== 'mixed') { continue; } if (access.getColumnAccess(tableId, colId).perms.read !== 'mixed') { continue; }
// Check column accessibility before and after. // Check column accessibility before and after.
const forbiddenBefores = new Set(await this._getForbiddenRows(cursor, rowsBefore, ids, colId)); const _forbiddenBefores = new Set(await this._getForbiddenRows(cursor, rowsBefore, ids, colId));
const forbiddenAfters = new Set(await this._getForbiddenRows(cursor, rowsAfter, ids, colId)); const _forbiddenAfters = new Set(await this._getForbiddenRows(cursor, rowsAfter, ids, colId));
// For any column that is in a visible row and for which accessibility has changed, // For any column that is in a visible row and for which accessibility has changed,
// pull it into the doc actions. We don't censor cells yet, that happens later // pull it into the doc actions. We don't censor cells yet, that happens later
// (if that's what needs doing). // (if that's what needs doing).
const changedIds = orderedIds.filter(id => !forceRemoves.has(id) && !removals.has(id) && const changedIds = orderedIds.filter(id => !forceRemoves.has(id) && !removals.has(id) &&
(forbiddenBefores.has(id) !== forbiddenAfters.has(id))); (_forbiddenBefores.has(id) !== _forbiddenAfters.has(id)));
if (changedIds.length > 0) { if (changedIds.length > 0) {
revisedDocActions.push(this._makeColumnUpdate(rowsAfter, colId, new Set(changedIds))); revisedDocActions.push(this._makeColumnUpdate(rowsAfter, colId, new Set(changedIds)));
} }
} }
// Return the results, also applying any cell-level access control. // Return the results, also applying any cell-level access control.
for (const action of revisedDocActions) { for (const a of revisedDocActions) {
await this._filterRowsAndCells({...cursor, action}, rowsAfter, rowsAfter, readAccessCheck); await this._filterRowsAndCells({...cursor, action: a}, rowsAfter, rowsAfter, readAccessCheck);
} }
return revisedDocActions; return revisedDocActions;
} }
@ -1656,7 +1656,7 @@ export const accessChecks = {
const readAccessCheck = accessChecks.check.read; const readAccessCheck = accessChecks.check.read;
// This AccessCheck allows everything. // This AccessCheck allows everything.
const dummyAccessCheck = { get() { return 'allow'; } } const dummyAccessCheck = { get() { return 'allow'; } };
/** /**

@ -681,7 +681,7 @@ export class HostedStorageManager implements IDocStorageManager {
lastModified: t, lastModified: t,
snapshotId: newSnapshotId, snapshotId: newSnapshotId,
metadata metadata
} };
await this._inventory.add(docId, snapshot, prevSnapshotId); await this._inventory.add(docId, snapshot, prevSnapshotId);
await this._onInventoryChange(docId); await this._onInventoryChange(docId);
} finally { } finally {

@ -51,7 +51,7 @@ export class PluginManager {
}; };
} }
public dirs(): PluginDirectories {return this._dirs; } public dirs(): PluginDirectories { return this._dirs; }
/** /**
* Create tmp dir and load plugins. * Create tmp dir and load plugins.

@ -27,7 +27,7 @@ export function getRelatedRows(docActions: DocAction[]): ReadonlyArray<readonly
if (docAction[0] === 'RenameTable') { if (docAction[0] === 'RenameTable') {
if (addedTables.has(currentTableId)) { if (addedTables.has(currentTableId)) {
addedTables.delete(currentTableId); addedTables.delete(currentTableId);
addedTables.add(docAction[2]) addedTables.add(docAction[2]);
continue; continue;
} }
tableIds.delete(currentTableId); tableIds.delete(currentTableId);

@ -118,7 +118,7 @@ export class Sharing {
assert(this._hubQueue.isEmpty() && !this._pendingQueue.isEmpty()); assert(this._hubQueue.isEmpty() && !this._pendingQueue.isEmpty());
const userRequest: UserRequest = this._pendingQueue.shift()!; const userRequest: UserRequest = this._pendingQueue.shift()!;
try { try {
const ret = await this.doApplyUserActionBundle(userRequest.action, userRequest.docSession); const ret = await this._doApplyUserActionBundle(userRequest.action, userRequest.docSession);
userRequest.resolve(ret); userRequest.resolve(ret);
} catch (e) { } catch (e) {
log.warn("Unable to apply action...", e); log.warn("Unable to apply action...", e);
@ -130,7 +130,7 @@ export class Sharing {
assert(!this._hubQueue.isEmpty() && !this._actionHistory.haveLocalActions()); assert(!this._hubQueue.isEmpty() && !this._actionHistory.haveLocalActions());
const action: ActionBundle = this._hubQueue.shift()!; const action: ActionBundle = this._hubQueue.shift()!;
try { try {
await this.doApplySharedActionBundle(action); await this._doApplySharedActionBundle(action);
} catch (e) { } catch (e) {
log.error("Unable to apply hub action... skipping"); log.error("Unable to apply hub action... skipping");
} }
@ -155,15 +155,15 @@ export class Sharing {
private async _rebaseLocalActions(): Promise<void> { private async _rebaseLocalActions(): Promise<void> {
const rebaseQueue: Deque<UserActionBundle> = new Deque<UserActionBundle>(); const rebaseQueue: Deque<UserActionBundle> = new Deque<UserActionBundle>();
try { try {
this.createCheckpoint(); this._createCheckpoint();
const actions: LocalActionBundle[] = await this._actionHistory.fetchAllLocal(); const actions: LocalActionBundle[] = await this._actionHistory.fetchAllLocal();
assert(actions.length > 0); assert(actions.length > 0);
await this.doApplyUserActionBundle(this._createUndo(actions), null); await this._doApplyUserActionBundle(this._createUndo(actions), null);
rebaseQueue.push(...actions.map((a) => getUserActionBundle(a))); rebaseQueue.push(...actions.map((a) => getUserActionBundle(a)));
await this._actionHistory.clearLocalActions(); await this._actionHistory.clearLocalActions();
} catch (e) { } catch (e) {
log.error("Can't undo local actions; sharing is off"); log.error("Can't undo local actions; sharing is off");
this.rollbackToCheckpoint(); this._rollbackToCheckpoint();
// TODO this.disconnect(); // TODO this.disconnect();
// TODO errorState = true; // TODO errorState = true;
return; return;
@ -178,34 +178,34 @@ export class Sharing {
const action: UserActionBundle = rebaseQueue.shift()!; const action: UserActionBundle = rebaseQueue.shift()!;
const adjusted: UserActionBundle = this._mergeAdjust(action); const adjusted: UserActionBundle = this._mergeAdjust(action);
try { try {
await this.doApplyUserActionBundle(adjusted, null); await this._doApplyUserActionBundle(adjusted, null);
} catch (e) { } catch (e) {
log.warn("Unable to apply rebased action..."); log.warn("Unable to apply rebased action...");
rebaseFailures.push([action, adjusted]); rebaseFailures.push([action, adjusted]);
} }
} }
if (rebaseFailures.length > 0) { if (rebaseFailures.length > 0) {
this.createBackupAtCheckpoint(); this._createBackupAtCheckpoint();
// TODO we should notify the user too. // TODO we should notify the user too.
log.error('Rebase failed to reapply some of your actions, backup of local at...'); log.error('Rebase failed to reapply some of your actions, backup of local at...');
} }
this.releaseCheckpoint(); this._releaseCheckpoint();
} }
// ====================================================================== // ======================================================================
private doApplySharedActionBundle(action: ActionBundle): Promise<UserResult> { private _doApplySharedActionBundle(action: ActionBundle): Promise<UserResult> {
const userActions: UserAction[] = [ const userActions: UserAction[] = [
['ApplyDocActions', action.stored.map(envContent => envContent[1])] ['ApplyDocActions', action.stored.map(envContent => envContent[1])]
]; ];
return this.doApplyUserActions(action.info[1], userActions, Branch.Shared, null); return this._doApplyUserActions(action.info[1], userActions, Branch.Shared, null);
} }
private doApplyUserActionBundle(action: UserActionBundle, docSession: OptDocSession|null): Promise<UserResult> { private _doApplyUserActionBundle(action: UserActionBundle, docSession: OptDocSession|null): Promise<UserResult> {
return this.doApplyUserActions(action.info, action.userActions, Branch.Local, docSession); return this._doApplyUserActions(action.info, action.userActions, Branch.Local, docSession);
} }
private async doApplyUserActions(info: ActionInfo, userActions: UserAction[], private async _doApplyUserActions(info: ActionInfo, userActions: UserAction[],
branch: Branch, docSession: OptDocSession|null): Promise<UserResult> { branch: Branch, docSession: OptDocSession|null): Promise<UserResult> {
const client = docSession && docSession.client; const client = docSession && docSession.client;
@ -245,7 +245,7 @@ export class Sharing {
actionHash: null, // Gets set below by _actionHistory.recordNext... actionHash: null, // Gets set below by _actionHistory.recordNext...
parentActionHash: null, // Gets set below by _actionHistory.recordNext... parentActionHash: null, // Gets set below by _actionHistory.recordNext...
}; };
this._logActionBundle(`doApplyUserActions (${Branch[branch]})`, localActionBundle); this._logActionBundle(`_doApplyUserActions (${Branch[branch]})`, localActionBundle);
// TODO Note that the sandbox may produce actions which are not addressed to us (e.g. when we // TODO Note that the sandbox may produce actions which are not addressed to us (e.g. when we
// have EDIT permission without VIEW). These are not sent to the browser or the database. But // have EDIT permission without VIEW). These are not sent to the browser or the database. But
@ -332,10 +332,10 @@ export class Sharing {
} }
// Our beautiful little checkpointing interface, used to handle errors during rebase. // Our beautiful little checkpointing interface, used to handle errors during rebase.
private createCheckpoint() { /* TODO */ } private _createCheckpoint() { /* TODO */ }
private releaseCheckpoint() { /* TODO */ } private _releaseCheckpoint() { /* TODO */ }
private rollbackToCheckpoint() { /* TODO */ } private _rollbackToCheckpoint() { /* TODO */ }
private createBackupAtCheckpoint() { /* TODO */ } private _createBackupAtCheckpoint() { /* TODO */ }
/** /**
* Reduces a LocalActionBundle down to only those actions addressed to ourselves. * Reduces a LocalActionBundle down to only those actions addressed to ourselves.

@ -61,7 +61,7 @@ function createSessionStoreFactory(sessionsDB: string): () => SessionStore {
// Doesn't actually close, just unrefs stream so node becomes close-able. // Doesn't actually close, just unrefs stream so node becomes close-able.
store.client.unref(); store.client.unref();
}}); }});
} };
} else { } else {
const SQLiteStore = require('@gristlabs/connect-sqlite3')(session); const SQLiteStore = require('@gristlabs/connect-sqlite3')(session);
promisifyAll(SQLiteStore.prototype); promisifyAll(SQLiteStore.prototype);
@ -72,7 +72,7 @@ function createSessionStoreFactory(sessionsDB: string): () => SessionStore {
table: 'sessions' table: 'sessions'
}); });
return assignIn(store, { async close() {}}); return assignIn(store, { async close() {}});
} };
} }
} }

@ -49,7 +49,7 @@ export function getAvailablePort(firstPort: number = 8000, optCount: number = 20
export function connect(options: { port: number, host?: string, localAddress?: string, localPort?: string, export function connect(options: { port: number, host?: string, localAddress?: string, localPort?: string,
family?: number, allowHalfOpen?: boolean; }): Promise<net.Socket>; family?: number, allowHalfOpen?: boolean; }): Promise<net.Socket>;
export function connect(port: number, host?: string): Promise<net.Socket>; export function connect(port: number, host?: string): Promise<net.Socket>;
export function connect(path: string): Promise<net.Socket>; // tslint:disable-line:unified-signatures export function connect(sockPath: string): Promise<net.Socket>;
export function connect(arg: any, ...moreArgs: any[]): Promise<net.Socket> { export function connect(arg: any, ...moreArgs: any[]): Promise<net.Socket> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const s = net.connect(arg, ...moreArgs, () => resolve(s)); const s = net.connect(arg, ...moreArgs, () => resolve(s));

@ -1333,8 +1333,9 @@ export class Session {
const api = this.createHomeApi(); const api = this.createHomeApi();
if (!noCleanup) { if (!noCleanup) {
cleanup.addAfterEach(async () => { cleanup.addAfterEach(async () => {
if (doc.id) if (doc.id) {
await api.deleteDoc(doc.id).catch(noop); await api.deleteDoc(doc.id).catch(noop);
}
doc.id = ''; doc.id = '';
}); });
} }

Loading…
Cancel
Save