mirror of
				https://github.com/gristlabs/grist-core.git
				synced 2025-06-13 20:53:59 +00:00 
			
		
		
		
	(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
This commit is contained in:
		
							parent
							
								
									0890749d15
								
							
						
					
					
						commit
						d1c1416d78
					
				@ -1067,7 +1067,7 @@ class ObsUserAttributeRule extends Disposable {
 | 
			
		||||
                    // TODO this weirdly only works on the first click
 | 
			
		||||
                    (editor as any).completer?.showPopup(editor);
 | 
			
		||||
                  }
 | 
			
		||||
                })
 | 
			
		||||
                });
 | 
			
		||||
              })
 | 
			
		||||
            }),
 | 
			
		||||
            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);
 | 
			
		||||
 | 
			
		||||
    // 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
 | 
			
		||||
 */
 | 
			
		||||
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;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const section = docModel.viewSections.getRowModel(position.sectionId);
 | 
			
		||||
  const colRef = section.viewFields().peek()[position.fieldIndex]?.colRef.peek();
 | 
			
		||||
 | 
			
		||||
@ -31,6 +31,7 @@ CodeEditorPanel.prototype.buildDom = function() {
 | 
			
		||||
    kd.scope(this._schema, function(schema) {
 | 
			
		||||
      // 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.
 | 
			
		||||
      if (!schema) { return null; }
 | 
			
		||||
      return dom(
 | 
			
		||||
        'code.g-code-viewer.python',
 | 
			
		||||
        schema,
 | 
			
		||||
 | 
			
		||||
@ -204,15 +204,16 @@ export class Comm extends dispose.Disposable implements GristServerAPI, DocListA
 | 
			
		||||
  public pendingRequests: Map<number, CommRequestInFlight>;
 | 
			
		||||
  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
 | 
			
		||||
  // that docId.  In classic Grist, which doesn't have fixed docIds or multiple
 | 
			
		||||
  // servers, the key is always "null".
 | 
			
		||||
  private _connections: Map<string|null, GristWSConnection> = new Map();
 | 
			
		||||
  private _collectedUserActions: UserAction[] | null;
 | 
			
		||||
  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() {
 | 
			
		||||
    this.autoDisposeCallback(() => {
 | 
			
		||||
 | 
			
		||||
@ -79,7 +79,7 @@ export class Cursor extends Disposable {
 | 
			
		||||
    optCursorPos = optCursorPos || {};
 | 
			
		||||
    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.rowIndex = this.autoDispose(ko.computed({
 | 
			
		||||
      read: () => {
 | 
			
		||||
 | 
			
		||||
@ -48,33 +48,33 @@ export class CursorMonitor extends Disposable {
 | 
			
		||||
    // whenever current position changes, store it in the memory
 | 
			
		||||
    this.autoDispose(doc.cursorPosition.addListener(pos => {
 | 
			
		||||
      // if current position is not restored yet, don't change it
 | 
			
		||||
      if (!this._restored) return;
 | 
			
		||||
      if (pos) this.storePosition(pos);
 | 
			
		||||
    }))
 | 
			
		||||
      if (!this._restored) { return; }
 | 
			
		||||
      if (pos) { this._storePosition(pos); }
 | 
			
		||||
    }));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _whenDocumentLoadsRestorePosition(doc: GristDoc) {
 | 
			
		||||
    // on view shown
 | 
			
		||||
    this.autoDispose(doc.currentView.addListener(async view => {
 | 
			
		||||
      // 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
 | 
			
		||||
      this._restored = true;
 | 
			
		||||
      // 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 position = this.restoreLastPosition(viewId);
 | 
			
		||||
      const position = this._restoreLastPosition(viewId);
 | 
			
		||||
      if (position) {
 | 
			
		||||
        await doc.recursiveMoveToCursorPos(position, true);
 | 
			
		||||
      }
 | 
			
		||||
    }));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private storePosition(pos: ViewCursorPos) {
 | 
			
		||||
  private _storePosition(pos: ViewCursorPos) {
 | 
			
		||||
    this._store.update(this._docId, pos);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private restoreLastPosition(view: IDocPage) {
 | 
			
		||||
  private _restoreLastPosition(view: IDocPage) {
 | 
			
		||||
    const lastPosition = this._store.read(this._docId);
 | 
			
		||||
    this._store.clear(this._docId);
 | 
			
		||||
    if (lastPosition && lastPosition.position.viewId == view) {
 | 
			
		||||
@ -87,13 +87,13 @@ export class CursorMonitor extends Disposable {
 | 
			
		||||
// Internal implementations for working with local storage
 | 
			
		||||
class StorageWrapper {
 | 
			
		||||
 | 
			
		||||
  constructor(private storage = getStorage()) {
 | 
			
		||||
  constructor(private _storage = getStorage()) {
 | 
			
		||||
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public update(docId: string, position: ViewCursorPos): void {
 | 
			
		||||
    try {
 | 
			
		||||
      const storage = this.storage;
 | 
			
		||||
      const storage = this._storage;
 | 
			
		||||
      const data = { docId, position, timestamp: Date.now() };
 | 
			
		||||
      storage.setItem(this._key(docId), JSON.stringify(data));
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
@ -102,14 +102,14 @@ class StorageWrapper {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public clear(docId: string,): void {
 | 
			
		||||
    const storage = this.storage;
 | 
			
		||||
    const storage = this._storage;
 | 
			
		||||
    storage.removeItem(this._key(docId));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public read(docId: string): { docId: string; position: ViewCursorPos; } | undefined {
 | 
			
		||||
    const storage = this.storage;
 | 
			
		||||
    const storage = this._storage;
 | 
			
		||||
    const result = storage.getItem(this._key(docId));
 | 
			
		||||
    if (!result) return undefined;
 | 
			
		||||
    if (!result) { return undefined; }
 | 
			
		||||
    return JSON.parse(result);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -25,7 +25,7 @@ export class EditorMonitor extends Disposable {
 | 
			
		||||
    this._store = new EditMemoryStorage(doc.docId(), store);
 | 
			
		||||
 | 
			
		||||
    // 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;
 | 
			
		||||
      // if view wasn't rendered (page is displaying history or code view) do nothing
 | 
			
		||||
      if (!view) return;
 | 
			
		||||
      if (!view) { return; }
 | 
			
		||||
      const lastEdit = this._restorePosition();
 | 
			
		||||
      if (lastEdit) {
 | 
			
		||||
        // set the cursor at right cell
 | 
			
		||||
@ -98,11 +98,11 @@ function typedListener(owner: IDisposableOwner) {
 | 
			
		||||
type EditorState = any;
 | 
			
		||||
 | 
			
		||||
// Schema for value stored in the local storage
 | 
			
		||||
type LastEditData = {
 | 
			
		||||
interface LastEditData {
 | 
			
		||||
  // absolute position for a cell
 | 
			
		||||
  position: CellPosition,
 | 
			
		||||
  position: CellPosition;
 | 
			
		||||
  // editor's state
 | 
			
		||||
  value: EditorState
 | 
			
		||||
  value: EditorState;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Abstraction for working with local storage
 | 
			
		||||
@ -111,7 +111,7 @@ class EditMemoryStorage {
 | 
			
		||||
  private _entry: LastEditData | null = null;
 | 
			
		||||
  private _timestamp = 0;
 | 
			
		||||
 | 
			
		||||
  constructor(private _docId: string, private storage = getStorage()) {
 | 
			
		||||
  constructor(private _docId: string, private _storage = getStorage()) {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public updateValue(pos: CellPosition, value: EditorState): void {
 | 
			
		||||
@ -138,7 +138,7 @@ class EditMemoryStorage {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected load() {
 | 
			
		||||
    const storage = this.storage;
 | 
			
		||||
    const storage = this._storage;
 | 
			
		||||
    const data = storage.getItem(this._key());
 | 
			
		||||
    this._entry = null;
 | 
			
		||||
    this._timestamp = 0;
 | 
			
		||||
@ -150,7 +150,7 @@ class EditMemoryStorage {
 | 
			
		||||
          console.error("[EditMemory] Data in local storage has a different structure");
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
        this._entry = entry
 | 
			
		||||
        this._entry = entry;
 | 
			
		||||
        this._timestamp = timestamp;
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
        console.error("[EditMemory] Can't deserialize date from local storage");
 | 
			
		||||
@ -159,7 +159,7 @@ class EditMemoryStorage {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected save(): void {
 | 
			
		||||
    const storage = this.storage;
 | 
			
		||||
    const storage = this._storage;
 | 
			
		||||
 | 
			
		||||
    // if entry was removed - clear the storage
 | 
			
		||||
    if (!this._entry) {
 | 
			
		||||
 | 
			
		||||
@ -111,10 +111,10 @@ export class GristDoc extends DisposableWithEvents {
 | 
			
		||||
  public readonly fieldEditorHolder = Holder.create(this);
 | 
			
		||||
 | 
			
		||||
  // 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
 | 
			
		||||
  public cursorPosition : Computed<ViewCursorPos | undefined>;
 | 
			
		||||
  public cursorPosition: Computed<ViewCursorPos | undefined>;
 | 
			
		||||
 | 
			
		||||
  private _actionLog: ActionLog;
 | 
			
		||||
  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
 | 
			
		||||
    this.autoDispose(viewInstance.addListener(async (view) => {
 | 
			
		||||
      if (!view) return;
 | 
			
		||||
      if (!view) { return; }
 | 
			
		||||
      await view.getLoadingDonePromise();
 | 
			
		||||
      this.currentView.set(view);
 | 
			
		||||
    }))
 | 
			
		||||
    }));
 | 
			
		||||
 | 
			
		||||
    // create observable for current cursor position
 | 
			
		||||
    this.cursorPosition = Computed.create<ViewCursorPos | undefined>(this, use => {
 | 
			
		||||
      // get the BaseView
 | 
			
		||||
      const view = use(viewInstance);
 | 
			
		||||
      if (!view) return undefined;
 | 
			
		||||
      if (!view) { return undefined; }
 | 
			
		||||
      // get current viewId
 | 
			
		||||
      const viewId = use(this.activeViewId);
 | 
			
		||||
      if (typeof viewId != 'number') return undefined;
 | 
			
		||||
      if (typeof viewId != 'number') { return undefined; }
 | 
			
		||||
      // read latest position
 | 
			
		||||
      const currentPosition = use(view.cursor.currentPosition);
 | 
			
		||||
      if (currentPosition) return { ...currentPosition, viewId }
 | 
			
		||||
      if (currentPosition) { return { ...currentPosition, viewId }; }
 | 
			
		||||
      return undefined;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
@ -333,7 +333,7 @@ export class GristDoc extends DisposableWithEvents {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    try {
 | 
			
		||||
      const viewInstance = await this._switchToSectionId(cursorPos.sectionId)
 | 
			
		||||
      const viewInstance = await this._switchToSectionId(cursorPos.sectionId);
 | 
			
		||||
      if (viewInstance) {
 | 
			
		||||
        viewInstance.setCursorPos(cursorPos);
 | 
			
		||||
      }
 | 
			
		||||
@ -643,7 +643,7 @@ export class GristDoc extends DisposableWithEvents {
 | 
			
		||||
      }
 | 
			
		||||
      const view: ViewRec = section.view.peek();
 | 
			
		||||
      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); }
 | 
			
		||||
      const fieldIndex = cursorPos.fieldIndex;
 | 
			
		||||
      const viewInstance = await waitObs(section.viewInstance);
 | 
			
		||||
@ -669,14 +669,14 @@ export class GristDoc extends DisposableWithEvents {
 | 
			
		||||
   * @param input Optional. Cell's initial value
 | 
			
		||||
   */
 | 
			
		||||
  public async activateEditorAtCursor(options: { init?: string, state?: any}) {
 | 
			
		||||
    const view = await this.waitForView();
 | 
			
		||||
    const view = await this._waitForView();
 | 
			
		||||
    view?.activateEditorAtCursor(options);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Waits for a view to be ready
 | 
			
		||||
   */
 | 
			
		||||
  private async waitForView() {
 | 
			
		||||
  private async _waitForView() {
 | 
			
		||||
    const view = await waitObs(this.viewModel.activeSection.peek().viewInstance);
 | 
			
		||||
    await view?.getLoadingDonePromise();
 | 
			
		||||
    return view;
 | 
			
		||||
@ -788,13 +788,13 @@ export class GristDoc extends DisposableWithEvents {
 | 
			
		||||
   * Convert a url hash to a cursor position.
 | 
			
		||||
   */
 | 
			
		||||
  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){
 | 
			
		||||
      // translate colRef to a fieldIndex
 | 
			
		||||
      const section = this.docModel.viewSections.getRowModel(cursorPos.sectionId);
 | 
			
		||||
      const fieldIndex = section.viewFields.peek().all()
 | 
			
		||||
          .findIndex(x=> x.colRef.peek() == hash.colRef);
 | 
			
		||||
      if (fieldIndex >= 0) cursorPos.fieldIndex = fieldIndex;
 | 
			
		||||
      if (fieldIndex >= 0) { cursorPos.fieldIndex = fieldIndex; }
 | 
			
		||||
    }
 | 
			
		||||
    return cursorPos;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -106,6 +106,8 @@ export class GristWSConnection extends Disposable {
 | 
			
		||||
  public useCount: number = 0;
 | 
			
		||||
  public on: BackboneEvents['on'];    // set by Backbone
 | 
			
		||||
 | 
			
		||||
  protected trigger: BackboneEvents['trigger']; // set by Backbone
 | 
			
		||||
 | 
			
		||||
  private _clientId: string|null;
 | 
			
		||||
  private _clientCounter: string;     // Identifier of this GristWSConnection object in this browser tab session
 | 
			
		||||
  private _assignmentId: string|null;
 | 
			
		||||
@ -118,7 +120,6 @@ export class GristWSConnection extends Disposable {
 | 
			
		||||
  private _reconnectAttempts: number = 0;
 | 
			
		||||
  private _wantReconnect: boolean = true;
 | 
			
		||||
  private _ws: WebSocket|null = null;
 | 
			
		||||
  private trigger: BackboneEvents['trigger']; // set by Backbone
 | 
			
		||||
 | 
			
		||||
  constructor(private _settings: GristWSSettings = new GristWSSettingsBrowser()) {
 | 
			
		||||
    super();
 | 
			
		||||
 | 
			
		||||
@ -28,8 +28,8 @@ import pickBy = require('lodash/pickBy');
 | 
			
		||||
 * Creates an instance of TypeTransform for a single field. Extends ColumnTransform.
 | 
			
		||||
 */
 | 
			
		||||
export class TypeTransform extends ColumnTransform {
 | 
			
		||||
  private reviseTypeChange = Observable.create(this, false);
 | 
			
		||||
  private transformWidget: Computed<NewAbstractWidget|null>;
 | 
			
		||||
  private _reviseTypeChange = Observable.create(this, false);
 | 
			
		||||
  private _transformWidget: Computed<NewAbstractWidget|null>;
 | 
			
		||||
 | 
			
		||||
  constructor(gristDoc: GristDoc, fieldBuilder: 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.
 | 
			
		||||
    // 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;
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
@ -49,12 +49,12 @@ export class TypeTransform extends ColumnTransform {
 | 
			
		||||
    // An observable to disable all buttons before the dom get removed.
 | 
			
		||||
    const disableButtons = Observable.create(null, false);
 | 
			
		||||
 | 
			
		||||
    this.reviseTypeChange.set(false);
 | 
			
		||||
    this._reviseTypeChange.set(false);
 | 
			
		||||
    this.editor = this.autoDispose(AceEditor.create({ observable: this.transformColumn.formula }));
 | 
			
		||||
    return dom('div',
 | 
			
		||||
      testId('type-transform-top'),
 | 
			
		||||
      dom.maybe(this.transformWidget, transformWidget => transformWidget.buildTransformConfigDom()),
 | 
			
		||||
      dom.maybe(this.reviseTypeChange, () =>
 | 
			
		||||
      dom.maybe(this._transformWidget, transformWidget => transformWidget.buildTransformConfigDom()),
 | 
			
		||||
      dom.maybe(this._reviseTypeChange, () =>
 | 
			
		||||
        dom('div.transform_editor', this.buildEditorDom(),
 | 
			
		||||
          testId("type-transform-formula")
 | 
			
		||||
        )
 | 
			
		||||
@ -64,7 +64,7 @@ export class TypeTransform extends ColumnTransform {
 | 
			
		||||
          'Cancel', testId("type-transform-cancel"),
 | 
			
		||||
          dom.cls('disabled', disableButtons)
 | 
			
		||||
        ),
 | 
			
		||||
        dom.domComputed(this.reviseTypeChange, revising => {
 | 
			
		||||
        dom.domComputed(this._reviseTypeChange, revising => {
 | 
			
		||||
          if (revising) {
 | 
			
		||||
            return basicButton(dom.on('click', () => this.editor.writeObservable()),
 | 
			
		||||
              'Preview', testId("type-transform-update"),
 | 
			
		||||
@ -72,7 +72,7 @@ export class TypeTransform extends ColumnTransform {
 | 
			
		||||
              { title: 'Update formula (Shift+Enter)' }
 | 
			
		||||
            );
 | 
			
		||||
          } else {
 | 
			
		||||
            return basicButton(dom.on('click', () => { this.reviseTypeChange.set(true); }),
 | 
			
		||||
            return basicButton(dom.on('click', () => { this._reviseTypeChange.set(true); }),
 | 
			
		||||
              'Revise', testId("type-transform-revise"),
 | 
			
		||||
              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
 | 
			
		||||
      // 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.
 | 
			
		||||
      this._gristDoc.moveToCursorPos(ag.cursorPos, ag).catch(() => {/* do nothing */})
 | 
			
		||||
      this._gristDoc.moveToCursorPos(ag.cursorPos, ag).catch(() => { /* do nothing */ });
 | 
			
		||||
      await this._gristDoc.docComm.applyUserActionsById(
 | 
			
		||||
        actionGroups.map(a => a.actionNum),
 | 
			
		||||
        actionGroups.map(a => a.actionHash),
 | 
			
		||||
        isUndo,
 | 
			
		||||
        { otherId: ag.actionNum });
 | 
			
		||||
      this._gristDoc.moveToCursorPos(ag.cursorPos, ag).catch(() => {/* do nothing */})
 | 
			
		||||
      this._gristDoc.moveToCursorPos(ag.cursorPos, ag).catch(() => { /* do nothing */ });
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      err.message = `Failed to apply ${isUndo ? 'undo' : 'redo'} action: ${err.message}`;
 | 
			
		||||
      throw err;
 | 
			
		||||
 | 
			
		||||
@ -18,7 +18,7 @@ export async function duplicatePage(gristDoc: GristDoc, pageId: number) {
 | 
			
		||||
  const pagesTable = gristDoc.docModel.pages;
 | 
			
		||||
  const pageName = pagesTable.rowModels[pageId].view.peek().name.peek();
 | 
			
		||||
  let inputEl: HTMLInputElement;
 | 
			
		||||
  setTimeout(() => {inputEl.focus(); inputEl.select(); }, 100);
 | 
			
		||||
  setTimeout(() => { inputEl.focus(); inputEl.select(); }, 100);
 | 
			
		||||
 | 
			
		||||
  confirmModal('Duplicate page', 'Save', () => makeDuplicate(gristDoc, pageId, inputEl.value), (
 | 
			
		||||
    dom('div', [
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										8
									
								
								app/client/declarations.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								app/client/declarations.d.ts
									
									
									
									
										vendored
									
									
								
							@ -45,9 +45,9 @@ declare module "app/client/components/BaseView" {
 | 
			
		||||
  import {DomArg} from 'grainjs';
 | 
			
		||||
  import {IOpenController} from 'popweasel';
 | 
			
		||||
 | 
			
		||||
  type Options = {
 | 
			
		||||
    init? : string,
 | 
			
		||||
    state? : any
 | 
			
		||||
  interface Options {
 | 
			
		||||
    init?: string;
 | 
			
		||||
    state?: any;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  namespace BaseView {}
 | 
			
		||||
@ -68,7 +68,7 @@ declare module "app/client/components/BaseView" {
 | 
			
		||||
    public createFilterMenu(ctl: IOpenController, field: ViewFieldRec, onClose?: () => void): HTMLElement;
 | 
			
		||||
    public buildTitleControls(): DomArg;
 | 
			
		||||
    public getLoadingDonePromise(): Promise<void>;
 | 
			
		||||
    public activateEditorAtCursor(options?: Options) : void;
 | 
			
		||||
    public activateEditorAtCursor(options?: Options): void;
 | 
			
		||||
    public onResize(): void;
 | 
			
		||||
    public prepareToPrint(onOff: boolean): void;
 | 
			
		||||
    public moveEditRowToCursor(): DataRowModel;
 | 
			
		||||
 | 
			
		||||
@ -55,18 +55,18 @@ export class Autocomplete<Item extends ACItem> extends Disposable {
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private _triggerElem: HTMLInputElement | HTMLTextAreaElement,
 | 
			
		||||
    private readonly options: IAutocompleteOptions<Item>,
 | 
			
		||||
    private readonly _options: IAutocompleteOptions<Item>,
 | 
			
		||||
  ) {
 | 
			
		||||
    super();
 | 
			
		||||
 | 
			
		||||
    const content = cssMenuWrap(
 | 
			
		||||
      this._menuContent = cssMenu({class: options.menuCssClass || ''},
 | 
			
		||||
        dom.forEach(this._items, (item) => options.renderItem(item, this._highlightFunc)),
 | 
			
		||||
      this._menuContent = cssMenu({class: _options.menuCssClass || ''},
 | 
			
		||||
        dom.forEach(this._items, (item) => _options.renderItem(item, this._highlightFunc)),
 | 
			
		||||
        dom.style('min-width', _triggerElem.getBoundingClientRect().width + 'px'),
 | 
			
		||||
        dom.on('mouseleave', (ev) => this._setSelected(-1, true)),
 | 
			
		||||
        dom.on('click', (ev) => {
 | 
			
		||||
          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.
 | 
			
		||||
@ -91,7 +91,7 @@ export class Autocomplete<Item extends ACItem> extends Disposable {
 | 
			
		||||
    this.onDispose(() => { dom.domDispose(content); content.remove(); });
 | 
			
		||||
 | 
			
		||||
    // 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.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 prev = this._selected;
 | 
			
		||||
    if (elem !== prev) {
 | 
			
		||||
      const clsName = this.options.selectedCssClass || 'selected';
 | 
			
		||||
      const clsName = this._options.selectedCssClass || 'selected';
 | 
			
		||||
      if (prev) { prev.classList.remove(clsName); }
 | 
			
		||||
      if (elem) {
 | 
			
		||||
        elem.classList.add(clsName);
 | 
			
		||||
@ -123,7 +123,7 @@ export class Autocomplete<Item extends ACItem> extends Disposable {
 | 
			
		||||
    if (updateValue) {
 | 
			
		||||
      // Update trigger's value with the selected choice, or else with the last typed value.
 | 
			
		||||
      if (elem) {
 | 
			
		||||
        this._triggerElem.value = this.options.getItemText(this.getSelectedItem()!);
 | 
			
		||||
        this._triggerElem.value = this._options.getItemText(this.getSelectedItem()!);
 | 
			
		||||
      } else {
 | 
			
		||||
        this._triggerElem.value = this._lastAsTyped;
 | 
			
		||||
      }
 | 
			
		||||
@ -147,7 +147,7 @@ export class Autocomplete<Item extends ACItem> extends Disposable {
 | 
			
		||||
    this._lastAsTyped = inputVal;
 | 
			
		||||
    // 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.
 | 
			
		||||
    const acResults = await this.options.search(inputVal);
 | 
			
		||||
    const acResults = await this._options.search(inputVal);
 | 
			
		||||
    this._highlightFunc = acResults.highlightFunc;
 | 
			
		||||
    this._items.set(acResults.items);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -31,7 +31,7 @@ export function isEquivalentFilter(state: FilterState, spec: FilterSpec): boolea
 | 
			
		||||
  const other = makeFilterState(spec);
 | 
			
		||||
  if (state.include !== other.include) { 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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -92,7 +92,7 @@ export class SearchModelImpl extends Disposable implements SearchModel {
 | 
			
		||||
    this.autoDispose(this.value.addListener(v => { void findFirst(v); }));
 | 
			
		||||
 | 
			
		||||
    // 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
 | 
			
		||||
    // 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.
 | 
			
		||||
  public getRecords(): 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]));
 | 
			
		||||
    return records;
 | 
			
		||||
  }
 | 
			
		||||
@ -255,7 +255,7 @@ export function find(model: TreeNode, func: (item: TreeItem) => boolean): TreeIt
 | 
			
		||||
  if (children) {
 | 
			
		||||
    for (const child of children.get()) {
 | 
			
		||||
      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.
 | 
			
		||||
 */
 | 
			
		||||
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
 | 
			
		||||
@ -105,7 +105,7 @@ export class UrlStateImpl {
 | 
			
		||||
   *    localhost:8080/o/<org>
 | 
			
		||||
   */
 | 
			
		||||
  public encodeUrl(state: IGristUrlState, baseLocation: Location | URL): string {
 | 
			
		||||
    const gristConfig = this.window.gristConfig || {};
 | 
			
		||||
    const gristConfig = this._window.gristConfig || {};
 | 
			
		||||
    return encodeUrl(gristConfig, state, baseLocation);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -113,7 +113,7 @@ export class UrlStateImpl {
 | 
			
		||||
   * Parse a URL location into an IGristUrlState object. See encodeUrl() documentation.
 | 
			
		||||
   */
 | 
			
		||||
  public decodeUrl(location: Location | URL): IGristUrlState {
 | 
			
		||||
    const gristConfig = this.window.gristConfig || {};
 | 
			
		||||
    const gristConfig = this._window.gristConfig || {};
 | 
			
		||||
    return decodeUrl(gristConfig, location);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -136,7 +136,7 @@ export class UrlStateImpl {
 | 
			
		||||
   * a matter of DocWorker requiring a different version (e.g. /v/OTHER/doc/...).
 | 
			
		||||
   */
 | 
			
		||||
  public needPageLoad(prevState: IGristUrlState, newState: IGristUrlState): boolean {
 | 
			
		||||
    const gristConfig = this.window.gristConfig || {};
 | 
			
		||||
    const gristConfig = this._window.gristConfig || {};
 | 
			
		||||
    const orgReload = prevState.org !== newState.org;
 | 
			
		||||
    // Reload when moving to/from a document or between doc and non-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.
 | 
			
		||||
 */
 | 
			
		||||
class RowGroupHelper<Value> extends RowSource {
 | 
			
		||||
  private rows: Set<RowId> = new Set();
 | 
			
		||||
  private _rows: Set<RowId> = new Set();
 | 
			
		||||
  constructor(public readonly groupValue: Value) {
 | 
			
		||||
    super();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public getAllRows() {
 | 
			
		||||
    return this.rows.values();
 | 
			
		||||
    return this._rows.values();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public getNumRows(): number {
 | 
			
		||||
    return this.rows.size;
 | 
			
		||||
    return this._rows.size;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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) {
 | 
			
		||||
    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
 | 
			
		||||
          // using a transition, and scroll into view, when it turns back to false.
 | 
			
		||||
          transition(flash, {
 | 
			
		||||
            prepare(elem, val) { if (!val) { elem.style.backgroundColor = colors.slate.toString(); }},
 | 
			
		||||
            run(elem, val) { if (!val) { elem.style.backgroundColor = ''; scrollIntoViewIfNeeded(elem); }},
 | 
			
		||||
            prepare(elem, val) { if (!val) { elem.style.backgroundColor = colors.slate.toString(); } },
 | 
			
		||||
            run(elem, val) { if (!val) { elem.style.backgroundColor = ''; scrollIntoViewIfNeeded(elem); } },
 | 
			
		||||
          })
 | 
			
		||||
        ),
 | 
			
		||||
        css.docRowWrapper.cls('-renaming', isRenaming),
 | 
			
		||||
 | 
			
		||||
@ -45,7 +45,7 @@ export abstract class MultiItemSelector<Item extends BaseItem> extends Disposabl
 | 
			
		||||
    return cssMultiSelectorWrapper(
 | 
			
		||||
      cssItemList(testId('list'),
 | 
			
		||||
        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`
 | 
			
		||||
  protected async remove(item: Item): Promise<void> {
 | 
			
		||||
    const idx = this.findIncIndex(item);
 | 
			
		||||
    const idx = this._findIncIndex(item);
 | 
			
		||||
    if (idx === -1) { return; }
 | 
			
		||||
    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
 | 
			
		||||
  protected async changeItem(item: Item, newItem: Item): Promise<void> {
 | 
			
		||||
    const idx = this.findIncIndex(item);
 | 
			
		||||
    const idx = this._findIncIndex(item);
 | 
			
		||||
    if (idx === -1) { return; }
 | 
			
		||||
    this._incItems.splice(idx, 1, newItem);
 | 
			
		||||
  }
 | 
			
		||||
@ -82,7 +82,7 @@ export abstract class MultiItemSelector<Item extends BaseItem> extends Disposabl
 | 
			
		||||
                           selectCb: (newItem: Item) => void,
 | 
			
		||||
                           selectOptions?: {}): Element {
 | 
			
		||||
    const obs = computed(use => selectedValue).onWrite(async value => {
 | 
			
		||||
      const newItem = this.findItemByValue(value);
 | 
			
		||||
      const newItem = this._findItemByValue(value);
 | 
			
		||||
      if (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.
 | 
			
		||||
  private findIncIndex(item: Item): number {
 | 
			
		||||
  private _findIncIndex(item: Item): number {
 | 
			
		||||
    return this._incItems.get().findIndex(_item => _item === item);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 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);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 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);
 | 
			
		||||
    return dom('li', testId('add-item'),
 | 
			
		||||
      dom.domComputed(addNewItem, isAdding => isAdding
 | 
			
		||||
 | 
			
		||||
@ -196,7 +196,7 @@ export function buildPageWidgetPicker(
 | 
			
		||||
    dom.create(PageWidgetSelect, value, tables, columns, onSaveCB, options),
 | 
			
		||||
 | 
			
		||||
    // gives focus and binds keydown events
 | 
			
		||||
    (elem: any) => {setTimeout(() => elem.focus(), 0); },
 | 
			
		||||
    (elem: any) => { setTimeout(() => elem.focus(), 0); },
 | 
			
		||||
    onKeyDown({
 | 
			
		||||
      Escape: () => ctl.close(),
 | 
			
		||||
      Enter: () => isValid() && onSaveCB()
 | 
			
		||||
 | 
			
		||||
@ -82,7 +82,7 @@ function buildColorPicker(ctl: IOpenController, textColor: Observable<string>, f
 | 
			
		||||
    }),
 | 
			
		||||
 | 
			
		||||
    // gives focus and binds keydown events
 | 
			
		||||
    (elem: any) => {setTimeout(() => elem.focus(), 0); },
 | 
			
		||||
    (elem: any) => { setTimeout(() => elem.focus(), 0); },
 | 
			
		||||
    onKeyDown({
 | 
			
		||||
      Escape: () => { revert(); },
 | 
			
		||||
      Enter: () => { ctl.close(); },
 | 
			
		||||
@ -158,7 +158,7 @@ class PickerComponent extends Disposable {
 | 
			
		||||
          ),
 | 
			
		||||
          cssHexBox(
 | 
			
		||||
            this._color,
 | 
			
		||||
            async (val) => { if (isValidHex(val)) {this._model.setValue(val); }},
 | 
			
		||||
            async (val) => { if (isValidHex(val)) { this._model.setValue(val); } },
 | 
			
		||||
            testId(`${title}-hex`),
 | 
			
		||||
            // select the hex value on click. Doing it using settimeout allows to avoid some
 | 
			
		||||
            // sporadically losing the selection just after the click.
 | 
			
		||||
 | 
			
		||||
@ -105,7 +105,7 @@ export function rawTextInput(value: Observable<string>, save: SaveFunc, onChange
 | 
			
		||||
  let inputEl: HTMLInputElement;
 | 
			
		||||
 | 
			
		||||
  // 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) {
 | 
			
		||||
    inputEl.value = val;
 | 
			
		||||
 | 
			
		||||
@ -128,8 +128,8 @@ export function searchBar(model: SearchModel, testId: TestId = noTestId) {
 | 
			
		||||
    find: () => { inputElem.focus(); inputElem.select(); },
 | 
			
		||||
    // 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.
 | 
			
		||||
    findNext: () => {model.findNext().catch(reportError); return false; },
 | 
			
		||||
    findPrev: () => {model.findPrev().catch(reportError); return false; },
 | 
			
		||||
    findNext: () => { model.findNext().catch(reportError); return false; },
 | 
			
		||||
    findPrev: () => { model.findPrev().catch(reportError); return false; },
 | 
			
		||||
  }, null, true);
 | 
			
		||||
 | 
			
		||||
  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
 | 
			
		||||
        // 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)
 | 
			
		||||
      ];
 | 
			
		||||
    }, {noEscapeKey: true});
 | 
			
		||||
 | 
			
		||||
@ -66,30 +66,30 @@ export class FieldBuilder extends Disposable {
 | 
			
		||||
  public readonly widgetImpl: ko.Computed<NewAbstractWidget>;
 | 
			
		||||
  public readonly diffImpl: NewAbstractWidget;
 | 
			
		||||
 | 
			
		||||
  private readonly availableTypes: Computed<Array<IOptionFull<string>>>;
 | 
			
		||||
  private readonly readOnlyPureType: ko.PureComputed<string>;
 | 
			
		||||
  private readonly isRightType: ko.PureComputed<(value: CellValue, options?: any) => boolean>;
 | 
			
		||||
  private readonly refTableId: ko.Computed<string | null>;
 | 
			
		||||
  private readonly isRef: ko.Computed<boolean>;
 | 
			
		||||
  private readonly _availableTypes: Computed<Array<IOptionFull<string>>>;
 | 
			
		||||
  private readonly _readOnlyPureType: ko.PureComputed<string>;
 | 
			
		||||
  private readonly _isRightType: ko.PureComputed<(value: CellValue, options?: any) => boolean>;
 | 
			
		||||
  private readonly _refTableId: ko.Computed<string | null>;
 | 
			
		||||
  private readonly _isRef: ko.Computed<boolean>;
 | 
			
		||||
  private readonly _rowMap: Map<DataRowModel, Element>;
 | 
			
		||||
  private readonly isTransformingFormula: ko.Computed<boolean>;
 | 
			
		||||
  private readonly isTransformingType: ko.Computed<boolean>;
 | 
			
		||||
  private readonly _isTransformingFormula: ko.Computed<boolean>;
 | 
			
		||||
  private readonly _isTransformingType: ko.Computed<boolean>;
 | 
			
		||||
  private readonly _fieldEditorHolder: Holder<IDisposable>;
 | 
			
		||||
  private readonly widgetCons: ko.Computed<{create: (...args: any[]) => NewAbstractWidget}>;
 | 
			
		||||
  private readonly docModel: DocModel;
 | 
			
		||||
  private readonly _widgetCons: ko.Computed<{create: (...args: any[]) => NewAbstractWidget}>;
 | 
			
		||||
  private readonly _docModel: DocModel;
 | 
			
		||||
 | 
			
		||||
  public constructor(public readonly gristDoc: GristDoc, public readonly field: ViewFieldRec,
 | 
			
		||||
                     private _cursor: Cursor) {
 | 
			
		||||
    super();
 | 
			
		||||
 | 
			
		||||
    this.docModel = gristDoc.docModel;
 | 
			
		||||
    this._docModel = gristDoc.docModel;
 | 
			
		||||
    this.origColumn = field.column();
 | 
			
		||||
    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.
 | 
			
		||||
    this.availableTypes = Computed.create(this, (use) => {
 | 
			
		||||
    this._availableTypes = Computed.create(this, (use) => {
 | 
			
		||||
      const isFormula = use(this.origColumn.isFormula);
 | 
			
		||||
      const types: Array<IOptionFull<string>> = [];
 | 
			
		||||
      _.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.
 | 
			
		||||
    this.isRightType = ko.pureComputed(function() {
 | 
			
		||||
      return gristTypes.isRightType(this.readOnlyPureType()) || _.constant(false);
 | 
			
		||||
    this._isRightType = ko.pureComputed(function() {
 | 
			
		||||
      return gristTypes.isRightType(this._readOnlyPureType()) || _.constant(false);
 | 
			
		||||
    }, this);
 | 
			
		||||
 | 
			
		||||
    // 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:');
 | 
			
		||||
    }));
 | 
			
		||||
 | 
			
		||||
    // 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:"),
 | 
			
		||||
      write: val => this._setType(`Ref:${val}`)
 | 
			
		||||
    }));
 | 
			
		||||
@ -148,11 +148,11 @@ export class FieldBuilder extends Disposable {
 | 
			
		||||
    this.columnTransform = null;
 | 
			
		||||
 | 
			
		||||
    // 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;
 | 
			
		||||
    }));
 | 
			
		||||
    // 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()) &&
 | 
			
		||||
        (this.columnTransform instanceof TypeTransform);
 | 
			
		||||
    }));
 | 
			
		||||
@ -165,14 +165,14 @@ export class FieldBuilder extends Disposable {
 | 
			
		||||
    this._rowMap = new Map();
 | 
			
		||||
 | 
			
		||||
    // Returns the constructor for the widget, and only notifies subscribers on changes.
 | 
			
		||||
    this.widgetCons = this.autoDispose(koUtil.withKoUtils(ko.computed(function() {
 | 
			
		||||
    this._widgetCons = this.autoDispose(koUtil.withKoUtils(ko.computed(function() {
 | 
			
		||||
      return UserTypeImpl.getWidgetConstructor(this.options().widget,
 | 
			
		||||
                                               this.readOnlyPureType());
 | 
			
		||||
                                               this._readOnlyPureType());
 | 
			
		||||
    }, this)).onlyNotifyUnequal());
 | 
			
		||||
 | 
			
		||||
    // Computed builder for the widget.
 | 
			
		||||
    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.
 | 
			
		||||
      return cons.create.bind(cons, this.field, this.field.colId());
 | 
			
		||||
    }, this).extend({ deferred: true }));
 | 
			
		||||
@ -180,11 +180,8 @@ export class FieldBuilder extends Disposable {
 | 
			
		||||
    this.diffImpl = this.autoDispose(DiffBox.create(this.field));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
// dispose.makeDisposable(FieldBuilder);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  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 widgetOptions = Object.keys(typeWidgets).map(label => ({
 | 
			
		||||
        label,
 | 
			
		||||
@ -206,32 +203,32 @@ export class FieldBuilder extends Disposable {
 | 
			
		||||
   * Build the type change dom.
 | 
			
		||||
   */
 | 
			
		||||
  public buildSelectTypeDom() {
 | 
			
		||||
    const selectType = Computed.create(null, (use) => use(fromKo(this.readOnlyPureType)));
 | 
			
		||||
    selectType.onWrite(newType => newType === this.readOnlyPureType.peek() || this._setType(newType));
 | 
			
		||||
    const selectType = Computed.create(null, (use) => use(fromKo(this._readOnlyPureType)));
 | 
			
		||||
    selectType.onWrite(newType => newType === this._readOnlyPureType.peek() || this._setType(newType));
 | 
			
		||||
    const onDispose = () => (this.isDisposed() || selectType.set(this.field.column().pureType()));
 | 
			
		||||
 | 
			
		||||
    return [
 | 
			
		||||
      cssRow(
 | 
			
		||||
        grainjsDom.autoDispose(selectType),
 | 
			
		||||
        select(selectType, this.availableTypes, {
 | 
			
		||||
          disabled: (use) => use(this.isTransformingFormula) || use(this.origColumn.disableModifyBase) ||
 | 
			
		||||
        select(selectType, this._availableTypes, {
 | 
			
		||||
          disabled: (use) => use(this._isTransformingFormula) || use(this.origColumn.disableModifyBase) ||
 | 
			
		||||
            use(this.isCallPending)
 | 
			
		||||
        }),
 | 
			
		||||
        testId('type-select')
 | 
			
		||||
      ),
 | 
			
		||||
      grainjsDom.maybe((use) => use(this.isRef) && !use(this.isTransformingType), () => this._buildRefTableSelect()),
 | 
			
		||||
      grainjsDom.maybe(this.isTransformingType, () => {
 | 
			
		||||
      grainjsDom.maybe((use) => use(this._isRef) && !use(this._isTransformingType), () => this._buildRefTableSelect()),
 | 
			
		||||
      grainjsDom.maybe(this._isTransformingType, () => {
 | 
			
		||||
        // Editor dom must be built before preparing transform.
 | 
			
		||||
        return dom('div.type_transform_prompt',
 | 
			
		||||
                   kf.prompt(
 | 
			
		||||
                     dom('div',
 | 
			
		||||
                         grainjsDom.maybe(this.isRef, () => this._buildRefTableSelect()),
 | 
			
		||||
                         grainjsDom.maybe(this._isRef, () => this._buildRefTableSelect()),
 | 
			
		||||
                         grainjsDom.maybe((use) => use(this.field.column().isTransforming),
 | 
			
		||||
                                          () => this.columnTransform!.buildDom())
 | 
			
		||||
                        )
 | 
			
		||||
                     )
 | 
			
		||||
                   ),
 | 
			
		||||
                   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
 | 
			
		||||
      // the full type, and set it.
 | 
			
		||||
      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) {
 | 
			
		||||
      this.columnTransform = TypeTransform.create(null, this.gristDoc, this);
 | 
			
		||||
      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.
 | 
			
		||||
  public _buildRefTableSelect() {
 | 
			
		||||
    const allTables = Computed.create(null, (use) =>
 | 
			
		||||
                                      use(this.docModel.allTableIds.getObservable()).map(tableId => ({
 | 
			
		||||
                                      use(this._docModel.allTableIds.getObservable()).map(tableId => ({
 | 
			
		||||
                                        value: tableId,
 | 
			
		||||
                                        label: tableId,
 | 
			
		||||
                                        icon: 'FieldTable' as const
 | 
			
		||||
@ -266,7 +263,7 @@ export class FieldBuilder extends Disposable {
 | 
			
		||||
      cssLabel('DATA FROM TABLE'),
 | 
			
		||||
      cssRow(
 | 
			
		||||
        dom.autoDispose(allTables),
 | 
			
		||||
        select(fromKo(this.refTableId), allTables),
 | 
			
		||||
        select(fromKo(this._refTableId), allTables),
 | 
			
		||||
        testId('ref-table-select')
 | 
			
		||||
      )
 | 
			
		||||
    ];
 | 
			
		||||
@ -301,15 +298,15 @@ export class FieldBuilder extends Disposable {
 | 
			
		||||
                   kf.checkButton(transformButton,
 | 
			
		||||
                     dom('span.glyphicon.glyphicon-flash'),
 | 
			
		||||
                     dom.testId("FieldBuilder_editTransform"),
 | 
			
		||||
                     kd.toggleClass('disabled', () => this.isTransformingType() || this.origColumn.isFormula() ||
 | 
			
		||||
                     kd.toggleClass('disabled', () => this._isTransformingType() || this.origColumn.isFormula() ||
 | 
			
		||||
                       this.origColumn.disableModifyBase())
 | 
			
		||||
                   )
 | 
			
		||||
                 )
 | 
			
		||||
               ),
 | 
			
		||||
               kd.maybe(this.isTransformingFormula, () => {
 | 
			
		||||
               kd.maybe(this._isTransformingFormula, () => {
 | 
			
		||||
                 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
 | 
			
		||||
    // the dom created by the widgetImpl to get out of sync.
 | 
			
		||||
    return dom('div',
 | 
			
		||||
      kd.maybe(() => !this.isTransformingType() && this.widgetImpl(), (widget: NewAbstractWidget) =>
 | 
			
		||||
      kd.maybe(() => !this._isTransformingType() && this.widgetImpl(), (widget: NewAbstractWidget) =>
 | 
			
		||||
        dom('div',
 | 
			
		||||
            widget.buildConfigDom(),
 | 
			
		||||
            widget.buildColorConfigDom(),
 | 
			
		||||
@ -401,7 +398,7 @@ export class FieldBuilder extends Disposable {
 | 
			
		||||
      if (this.isDisposed()) { return null; }   // Work around JS errors during field removal.
 | 
			
		||||
      const value = row.cells[this.field.colId()];
 | 
			
		||||
      const cell = value && value();
 | 
			
		||||
      if (value && this.isRightType()(cell, this.options) || row._isAddRow.peek()) {
 | 
			
		||||
      if (value && this._isRightType()(cell, this.options) || row._isAddRow.peek()) {
 | 
			
		||||
        return this.widgetImpl();
 | 
			
		||||
      } else if (gristTypes.isVersions(cell)) {
 | 
			
		||||
        return this.diffImpl;
 | 
			
		||||
@ -462,7 +459,7 @@ export class FieldBuilder extends Disposable {
 | 
			
		||||
      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.
 | 
			
		||||
    if (!editorCtor) {
 | 
			
		||||
      // 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 = {
 | 
			
		||||
  position : CellPosition,
 | 
			
		||||
  currentState : any,
 | 
			
		||||
  type: string
 | 
			
		||||
export interface FieldEditorStateEvent {
 | 
			
		||||
  position: CellPosition;
 | 
			
		||||
  currentState: any;
 | 
			
		||||
  type: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class FieldEditor extends Disposable {
 | 
			
		||||
@ -125,7 +125,7 @@ export class FieldEditor extends Disposable {
 | 
			
		||||
      unmakeFormula: () => this._unmakeFormula(),
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const state : any = options.state;
 | 
			
		||||
    const state: any = options.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.
 | 
			
		||||
  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 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.editorState) {
 | 
			
		||||
      editor.autoDispose(editor.editorState.addListener((currentState) => {
 | 
			
		||||
        const event : FieldEditorStateEvent = {
 | 
			
		||||
          position : this.cellPosition(),
 | 
			
		||||
        const event: FieldEditorStateEvent = {
 | 
			
		||||
          position: this._cellPosition(),
 | 
			
		||||
          currentState,
 | 
			
		||||
          type : this._field.column.peek().pureType.peek()
 | 
			
		||||
        }
 | 
			
		||||
          type: this._field.column.peek().pureType.peek()
 | 
			
		||||
        };
 | 
			
		||||
        this.changeEmitter.emit(event);
 | 
			
		||||
      }));
 | 
			
		||||
    }
 | 
			
		||||
@ -181,7 +181,7 @@ export class FieldEditor extends Disposable {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // calculate current cell's absolute position
 | 
			
		||||
  private cellPosition() {
 | 
			
		||||
  private _cellPosition() {
 | 
			
		||||
    const rowId = this._editRow.getRowId();
 | 
			
		||||
    const colRef = this._field.colRef.peek();
 | 
			
		||||
    const sectionId = this._field.viewSection.peek().id.peek();
 | 
			
		||||
@ -189,7 +189,7 @@ export class FieldEditor extends Disposable {
 | 
			
		||||
      rowId,
 | 
			
		||||
      colRef,
 | 
			
		||||
      sectionId
 | 
			
		||||
    }
 | 
			
		||||
    };
 | 
			
		||||
    return position;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -240,11 +240,11 @@ export class FieldEditor extends Disposable {
 | 
			
		||||
 | 
			
		||||
  // Cancels the edit
 | 
			
		||||
  private _cancelEdit() {
 | 
			
		||||
    const event : FieldEditorStateEvent = {
 | 
			
		||||
      position : this.cellPosition(),
 | 
			
		||||
      currentState : this._editorHolder.get()?.editorState?.get(),
 | 
			
		||||
      type : this._field.column.peek().pureType.peek()
 | 
			
		||||
    }
 | 
			
		||||
    const event: FieldEditorStateEvent = {
 | 
			
		||||
      position: this._cellPosition(),
 | 
			
		||||
      currentState: this._editorHolder.get()?.editorState?.get(),
 | 
			
		||||
      type: this._field.column.peek().pureType.peek()
 | 
			
		||||
    };
 | 
			
		||||
    this.cancelEmitter.emit(event);
 | 
			
		||||
    this.dispose();
 | 
			
		||||
  }
 | 
			
		||||
@ -296,11 +296,11 @@ export class FieldEditor extends Disposable {
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const event : FieldEditorStateEvent = {
 | 
			
		||||
      position : this.cellPosition(),
 | 
			
		||||
      currentState : this._editorHolder.get()?.editorState?.get(),
 | 
			
		||||
      type : this._field.column.peek().pureType.peek()
 | 
			
		||||
    }
 | 
			
		||||
    const event: FieldEditorStateEvent = {
 | 
			
		||||
      position: this._cellPosition(),
 | 
			
		||||
      currentState: this._editorHolder.get()?.editorState?.get(),
 | 
			
		||||
      type: this._field.column.peek().pureType.peek()
 | 
			
		||||
    };
 | 
			
		||||
    this.saveEmitter.emit(event);
 | 
			
		||||
 | 
			
		||||
    const cursor = this._cursor;
 | 
			
		||||
 | 
			
		||||
@ -13,7 +13,7 @@ import {dom, Observable} from 'grainjs';
 | 
			
		||||
 | 
			
		||||
export class NTextEditor extends NewBaseEditor {
 | 
			
		||||
  // 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 textInput: HTMLTextAreaElement;
 | 
			
		||||
@ -29,7 +29,7 @@ export class NTextEditor extends NewBaseEditor {
 | 
			
		||||
  constructor(options: Options) {
 | 
			
		||||
    super(options);
 | 
			
		||||
 | 
			
		||||
    const initialValue : string = undef(
 | 
			
		||||
    const initialValue: string = undef(
 | 
			
		||||
        options.state as string | undefined,
 | 
			
		||||
        options.editValue, String(options.cellValue ?? ""));
 | 
			
		||||
    this.editorState = Observable.create<string>(this, initialValue);
 | 
			
		||||
@ -96,10 +96,10 @@ export class NTextEditor extends NewBaseEditor {
 | 
			
		||||
   */
 | 
			
		||||
  protected onInput() {
 | 
			
		||||
    // Resize the textbox whenever user types in it.
 | 
			
		||||
    this.resizeInput()
 | 
			
		||||
    this.resizeInput();
 | 
			
		||||
 | 
			
		||||
    // 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;
 | 
			
		||||
  cursorPos: number;
 | 
			
		||||
  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.
 | 
			
		||||
   */
 | 
			
		||||
  public editorState? : Observable<any>;
 | 
			
		||||
  public editorState?: Observable<any>;
 | 
			
		||||
 | 
			
		||||
  constructor(protected options: Options) {
 | 
			
		||||
    super();
 | 
			
		||||
 | 
			
		||||
@ -171,7 +171,7 @@ export function summarizePermissionSet(pset: PartialPermissionSet): MixedPermiss
 | 
			
		||||
  for (const key of Object.keys(pset) as Array<keyof PartialPermissionSet>) {
 | 
			
		||||
    const pWithSome = pset[key];
 | 
			
		||||
    // "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 (!sign) {
 | 
			
		||||
      sign = p;
 | 
			
		||||
 | 
			
		||||
@ -240,7 +240,7 @@ export function getAffectedTables(summary: ActionSummary): string[] {
 | 
			
		||||
 */
 | 
			
		||||
export function getTableIdBefore(renames: LabelDelta[], tableIdAfter: string|null): string|null {
 | 
			
		||||
  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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -250,7 +250,7 @@ export function getTableIdBefore(renames: LabelDelta[], tableIdAfter: string|nul
 | 
			
		||||
 */
 | 
			
		||||
export function getTableIdAfter(renames: LabelDelta[], tableIdBefore: string|null): string|null {
 | 
			
		||||
  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;
 | 
			
		||||
  if (tableIdAfter?.startsWith('-')) { return null; }
 | 
			
		||||
  return tableIdAfter;
 | 
			
		||||
 | 
			
		||||
@ -78,7 +78,7 @@ export interface UserAttributeRule {
 | 
			
		||||
 * Check some key facts about the formula.
 | 
			
		||||
 */
 | 
			
		||||
export function getFormulaProperties(formula: ParsedAclFormula) {
 | 
			
		||||
  const result: FormulaProperties = {}
 | 
			
		||||
  const result: FormulaProperties = {};
 | 
			
		||||
  if (usesRec(formula)) { result.hasRecOrNewRec = true; }
 | 
			
		||||
  const colIds = new Set<string>();
 | 
			
		||||
  collectRecColIds(formula, colIds);
 | 
			
		||||
 | 
			
		||||
@ -633,8 +633,8 @@ export class DocWorkerAPIImpl extends BaseAPI implements DocWorkerAPI {
 | 
			
		||||
    super(_options);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async importDocToWorkspace(uploadId: number, workspaceId: number, browserSettings?: BrowserSettings)
 | 
			
		||||
    : Promise<DocCreationInfo> {
 | 
			
		||||
  public async importDocToWorkspace(uploadId: number, workspaceId: number, browserSettings?: BrowserSettings):
 | 
			
		||||
      Promise<DocCreationInfo> {
 | 
			
		||||
    return this.requestJson(`${this.url}/api/workspaces/${workspaceId}/import`, {
 | 
			
		||||
      method: 'POST',
 | 
			
		||||
      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.
 | 
			
		||||
 * 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) {
 | 
			
		||||
    if (value !== undefined) return value;
 | 
			
		||||
    if (value !== undefined) { return value; }
 | 
			
		||||
  }
 | 
			
		||||
  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> {
 | 
			
		||||
  let isPending = true;
 | 
			
		||||
  const done = () => {isPending = false; };
 | 
			
		||||
  const done = () => { isPending = false; };
 | 
			
		||||
  await Promise.race([
 | 
			
		||||
    promise.then(done, done),
 | 
			
		||||
    delay(timeoutMsec)
 | 
			
		||||
 | 
			
		||||
@ -126,34 +126,34 @@ export class WrappedObj {
 | 
			
		||||
 * communication with PyPy-based sandbox.)
 | 
			
		||||
 */
 | 
			
		||||
export class Marshaller {
 | 
			
		||||
  private memBuf: MemBuffer;
 | 
			
		||||
  private readonly floatCode: number;
 | 
			
		||||
  private readonly stringCode: number;
 | 
			
		||||
  private _memBuf: MemBuffer;
 | 
			
		||||
  private readonly _floatCode: number;
 | 
			
		||||
  private readonly _stringCode: number;
 | 
			
		||||
 | 
			
		||||
  constructor(options?: MarshalOptions) {
 | 
			
		||||
    this.memBuf = new MemBuffer(undefined);
 | 
			
		||||
    this.floatCode = options && options.version && options.version >= 2 ? marshalCodes.BFLOAT : marshalCodes.FLOAT;
 | 
			
		||||
    this.stringCode = options && options.stringToBuffer ? marshalCodes.STRING : marshalCodes.UNICODE;
 | 
			
		||||
    this._memBuf = new MemBuffer(undefined);
 | 
			
		||||
    this._floatCode = options && options.version && options.version >= 2 ? marshalCodes.BFLOAT : marshalCodes.FLOAT;
 | 
			
		||||
    this._stringCode = options && options.stringToBuffer ? marshalCodes.STRING : marshalCodes.UNICODE;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public dump(): Uint8Array {
 | 
			
		||||
    // 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.
 | 
			
		||||
    const bytes = new Uint8Array(this.memBuf.asByteArray());
 | 
			
		||||
    this.memBuf.clear();
 | 
			
		||||
    const bytes = new Uint8Array(this._memBuf.asByteArray());
 | 
			
		||||
    this._memBuf.clear();
 | 
			
		||||
    return bytes;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public dumpAsBuffer(): Buffer {
 | 
			
		||||
    const bytes = Buffer.from(this.memBuf.asByteArray());
 | 
			
		||||
    this.memBuf.clear();
 | 
			
		||||
    const bytes = Buffer.from(this._memBuf.asByteArray());
 | 
			
		||||
    this._memBuf.clear();
 | 
			
		||||
    return bytes;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public getCode(value: any) {
 | 
			
		||||
    switch (typeof value) {
 | 
			
		||||
      case 'number': return isInteger(value) ? marshalCodes.INT : this.floatCode;
 | 
			
		||||
      case 'string': return this.stringCode;
 | 
			
		||||
      case 'number': return isInteger(value) ? marshalCodes.INT : this._floatCode;
 | 
			
		||||
      case 'string': return this._stringCode;
 | 
			
		||||
      case 'boolean': return value ? marshalCodes.TRUE : marshalCodes.FALSE;
 | 
			
		||||
      case 'undefined': return marshalCodes.NONE;
 | 
			
		||||
      case 'object': {
 | 
			
		||||
@ -181,16 +181,16 @@ export class Marshaller {
 | 
			
		||||
    if (value instanceof WrappedObj) {
 | 
			
		||||
      value = value.value;
 | 
			
		||||
    }
 | 
			
		||||
    this.memBuf.writeUint8(code);
 | 
			
		||||
    this._memBuf.writeUint8(code);
 | 
			
		||||
    switch (code) {
 | 
			
		||||
      case marshalCodes.NULL:       return;
 | 
			
		||||
      case marshalCodes.NONE:       return;
 | 
			
		||||
      case marshalCodes.FALSE:      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.FLOAT:      return this._writeStringFloat(value);
 | 
			
		||||
      case marshalCodes.BFLOAT:     return this.memBuf.writeFloat64LE(value);
 | 
			
		||||
      case marshalCodes.BFLOAT:     return this._memBuf.writeFloat64LE(value);
 | 
			
		||||
      case marshalCodes.STRING:
 | 
			
		||||
        return (value instanceof Uint8Array || Buffer.isBuffer(value) ?
 | 
			
		||||
          this._writeByteArray(value) :
 | 
			
		||||
@ -219,8 +219,8 @@ export class Marshaller {
 | 
			
		||||
      // TODO We could actually support 53 bits or so.
 | 
			
		||||
      throw new Error("Marshaller: int64 still only supports 32-bit ints for now: " + value);
 | 
			
		||||
    }
 | 
			
		||||
    this.memBuf.writeInt32LE(value);
 | 
			
		||||
    this.memBuf.writeInt32LE(value >= 0 ? 0 : -1);
 | 
			
		||||
    this._memBuf.writeInt32LE(value);
 | 
			
		||||
    this._memBuf.writeInt32LE(value >= 0 ? 0 : -1);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _writeStringFloat(value: number) {
 | 
			
		||||
@ -230,28 +230,28 @@ export class Marshaller {
 | 
			
		||||
    if (bytes.byteLength >= 127) {
 | 
			
		||||
      throw new Error("Marshaller: Trying to write a float that takes " + bytes.byteLength + " bytes");
 | 
			
		||||
    }
 | 
			
		||||
    this.memBuf.writeUint8(bytes.byteLength);
 | 
			
		||||
    this.memBuf.writeByteArray(bytes);
 | 
			
		||||
    this._memBuf.writeUint8(bytes.byteLength);
 | 
			
		||||
    this._memBuf.writeByteArray(bytes);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _writeByteArray(value: Uint8Array|Buffer) {
 | 
			
		||||
    // This works for both Uint8Arrays and Node Buffers.
 | 
			
		||||
    this.memBuf.writeInt32LE(value.length);
 | 
			
		||||
    this.memBuf.writeByteArray(value);
 | 
			
		||||
    this._memBuf.writeInt32LE(value.length);
 | 
			
		||||
    this._memBuf.writeByteArray(value);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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.
 | 
			
		||||
    this.memBuf.writeInt32LE(0);
 | 
			
		||||
    this.memBuf.writeString(value);
 | 
			
		||||
    const byteLength = this.memBuf.size() - offset - 4;
 | 
			
		||||
    this._memBuf.writeInt32LE(0);
 | 
			
		||||
    this._memBuf.writeString(value);
 | 
			
		||||
    const byteLength = this._memBuf.size() - offset - 4;
 | 
			
		||||
    // 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[]) {
 | 
			
		||||
    this.memBuf.writeInt32LE(array.length);
 | 
			
		||||
    this._memBuf.writeInt32LE(array.length);
 | 
			
		||||
    for (const item of array) {
 | 
			
		||||
      this.marshal(item);
 | 
			
		||||
    }
 | 
			
		||||
@ -264,7 +264,7 @@ export class Marshaller {
 | 
			
		||||
      this.marshal(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 {
 | 
			
		||||
  public memBuf: MemBuffer;
 | 
			
		||||
  private consumer: any = null;
 | 
			
		||||
  private _consumer: any = null;
 | 
			
		||||
  private _lastCode: number|null = null;
 | 
			
		||||
  private readonly bufferToString: boolean;
 | 
			
		||||
  private emitter: (v: any) => boolean;
 | 
			
		||||
  private stringTable: Array<string|Uint8Array> = [];
 | 
			
		||||
  private readonly _bufferToString: boolean;
 | 
			
		||||
  private _emitter: (v: any) => boolean;
 | 
			
		||||
  private _stringTable: Array<string|Uint8Array> = [];
 | 
			
		||||
 | 
			
		||||
  constructor(options?: UnmarshalOptions) {
 | 
			
		||||
    super();
 | 
			
		||||
    this.memBuf = new MemBuffer(undefined);
 | 
			
		||||
    this.bufferToString = Boolean(options && options.bufferToString);
 | 
			
		||||
    this.emitter = this.emit.bind(this, 'value');
 | 
			
		||||
    this._bufferToString = Boolean(options && options.bufferToString);
 | 
			
		||||
    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.
 | 
			
		||||
   */
 | 
			
		||||
  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);
 | 
			
		||||
    try {
 | 
			
		||||
      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.
 | 
			
		||||
        this.stringTable.length = 0;
 | 
			
		||||
        this._stringTable.length = 0;
 | 
			
		||||
 | 
			
		||||
        const value = this._parse();
 | 
			
		||||
        this.memBuf.consume(this.consumer);
 | 
			
		||||
        this.memBuf.consume(this._consumer);
 | 
			
		||||
        if (valueCB(value) === false) {
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
@ -341,7 +341,7 @@ export class Unmarshaller extends EventEmitter {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _parse(): unknown {
 | 
			
		||||
    const code = this.memBuf.readUint8(this.consumer);
 | 
			
		||||
    const code = this.memBuf.readUint8(this._consumer);
 | 
			
		||||
    this._lastCode = code;
 | 
			
		||||
    switch (code) {
 | 
			
		||||
      case marshalCodes.NULL:       return null;
 | 
			
		||||
@ -374,12 +374,12 @@ export class Unmarshaller extends EventEmitter {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _parseInt() {
 | 
			
		||||
    return this.memBuf.readInt32LE(this.consumer);
 | 
			
		||||
    return this.memBuf.readInt32LE(this._consumer);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _parseInt64() {
 | 
			
		||||
    const low = this.memBuf.readInt32LE(this.consumer);
 | 
			
		||||
    const hi = this.memBuf.readInt32LE(this.consumer);
 | 
			
		||||
    const low = this.memBuf.readInt32LE(this._consumer);
 | 
			
		||||
    const hi = this.memBuf.readInt32LE(this._consumer);
 | 
			
		||||
    if ((hi === 0 && low >= 0) || (hi === -1 && low < 0)) {
 | 
			
		||||
      return low;
 | 
			
		||||
    }
 | 
			
		||||
@ -395,46 +395,46 @@ export class Unmarshaller extends EventEmitter {
 | 
			
		||||
  private _parseLong() {
 | 
			
		||||
    // The format is a 32-bit size whose sign is the sign of the result, followed by 16-bit digits
 | 
			
		||||
    // 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 numDigits = size < 0 ? -size : size;
 | 
			
		||||
    const digits = [];
 | 
			
		||||
    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();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _parseStringFloat() {
 | 
			
		||||
    const len = this.memBuf.readUint8(this.consumer);
 | 
			
		||||
    const buf = this.memBuf.readString(this.consumer, len);
 | 
			
		||||
    const len = this.memBuf.readUint8(this._consumer);
 | 
			
		||||
    const buf = this.memBuf.readString(this._consumer, len);
 | 
			
		||||
    return parseFloat(buf);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _parseBinaryFloat() {
 | 
			
		||||
    return this.memBuf.readFloat64LE(this.consumer);
 | 
			
		||||
    return this.memBuf.readFloat64LE(this._consumer);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _parseByteString(): string|Uint8Array {
 | 
			
		||||
    const len = this.memBuf.readInt32LE(this.consumer);
 | 
			
		||||
    return (this.bufferToString ?
 | 
			
		||||
      this.memBuf.readString(this.consumer, len) :
 | 
			
		||||
      this.memBuf.readByteArray(this.consumer, len));
 | 
			
		||||
    const len = this.memBuf.readInt32LE(this._consumer);
 | 
			
		||||
    return (this._bufferToString ?
 | 
			
		||||
      this.memBuf.readString(this._consumer, len) :
 | 
			
		||||
      this.memBuf.readByteArray(this._consumer, len));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _parseInterned() {
 | 
			
		||||
    const s = this._parseByteString();
 | 
			
		||||
    this.stringTable.push(s);
 | 
			
		||||
    this._stringTable.push(s);
 | 
			
		||||
    return s;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _parseStringRef() {
 | 
			
		||||
    const index = this._parseInt();
 | 
			
		||||
    return this.stringTable[index];
 | 
			
		||||
    return this._stringTable[index];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _parseList() {
 | 
			
		||||
    const len = this.memBuf.readInt32LE(this.consumer);
 | 
			
		||||
    const len = this.memBuf.readInt32LE(this._consumer);
 | 
			
		||||
    const value = [];
 | 
			
		||||
    for (let i = 0; i < len; i++) {
 | 
			
		||||
      value[i] = this._parse();
 | 
			
		||||
@ -461,8 +461,8 @@ export class Unmarshaller extends EventEmitter {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _parseUnicode() {
 | 
			
		||||
    const len = this.memBuf.readInt32LE(this.consumer);
 | 
			
		||||
    return this.memBuf.readString(this.consumer, len);
 | 
			
		||||
    const len = this.memBuf.readInt32LE(this._consumer);
 | 
			
		||||
    return this.memBuf.readString(this._consumer, len);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -364,7 +364,7 @@ export class ApiServer {
 | 
			
		||||
      const userId = getAuthorizedUserId(req);
 | 
			
		||||
      await this._dbManager.connection.transaction(async manager => {
 | 
			
		||||
        const user = await manager.findOne(User, userId);
 | 
			
		||||
        if (!user) {return handleDeletedUser(); }
 | 
			
		||||
        if (!user) { return handleDeletedUser(); }
 | 
			
		||||
        user.apiKey = null;
 | 
			
		||||
        await manager.save(User, user);
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
@ -337,9 +337,9 @@ export class DocWorkerMap implements IDocWorkerMap {
 | 
			
		||||
    if (docId === 'import') {
 | 
			
		||||
      const lock = await this._redlock.lock(`workers-lock`, LOCK_TIMEOUT);
 | 
			
		||||
      try {
 | 
			
		||||
        const workerId = await this._client.srandmemberAsync(`workers-available-default`);
 | 
			
		||||
        if (!workerId) { throw new Error('no doc worker available'); }
 | 
			
		||||
        const docWorker = await this._client.hgetallAsync(`worker-${workerId}`) as DocWorkerInfo|null;
 | 
			
		||||
        const _workerId = await this._client.srandmemberAsync(`workers-available-default`);
 | 
			
		||||
        if (!_workerId) { throw new Error('no doc worker available'); }
 | 
			
		||||
        const docWorker = await this._client.hgetallAsync(`worker-${_workerId}`) as DocWorkerInfo|null;
 | 
			
		||||
        if (!docWorker) { throw new Error('no doc worker contact info available'); }
 | 
			
		||||
        return {
 | 
			
		||||
          docMD5: null,
 | 
			
		||||
 | 
			
		||||
@ -172,7 +172,7 @@ if (typeof window !== 'undefined') {
 | 
			
		||||
  // 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
 | 
			
		||||
  // imported, but the methods don't do anything useful.
 | 
			
		||||
  rpc.setSendMessage((data) => {return; });
 | 
			
		||||
  rpc.setSendMessage((data) => { return; });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function createRpcLogger(): IRpcLogger {
 | 
			
		||||
 | 
			
		||||
@ -188,11 +188,13 @@ export class ChecksummedExternalStorage implements ExternalStorage {
 | 
			
		||||
      if (!snapshotIds) {
 | 
			
		||||
        await this._options.latestVersion.save(key, DELETED_TOKEN);
 | 
			
		||||
        await this._options.sharedHash.save(key, DELETED_TOKEN);
 | 
			
		||||
      } else for (const snapshotId of snapshotIds) {
 | 
			
		||||
        // Removing snapshots breaks their partial immutability, so we mark them
 | 
			
		||||
        // as deleted in redis so that we don't get stale info from S3 if we check
 | 
			
		||||
        // for their existence.  Nothing currently depends on this in practice.
 | 
			
		||||
        await this._options.sharedHash.save(this._keyWithSnapshot(key, snapshotId), DELETED_TOKEN);
 | 
			
		||||
      } else {
 | 
			
		||||
        for (const snapshotId of snapshotIds) {
 | 
			
		||||
          // Removing snapshots breaks their partial immutability, so we mark them
 | 
			
		||||
          // as deleted in redis so that we don't get stale info from S3 if we check
 | 
			
		||||
          // for their existence.  Nothing currently depends on this in practice.
 | 
			
		||||
          await this._options.sharedHash.save(this._keyWithSnapshot(key, snapshotId), DELETED_TOKEN);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      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.addSupportPaths(docAccessMiddleware);
 | 
			
		||||
    this._addSupportPaths(docAccessMiddleware);
 | 
			
		||||
 | 
			
		||||
    if (!isSingleUserMode()) {
 | 
			
		||||
      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};
 | 
			
		||||
        await this._sendAppPage(req, resp, {path: 'error.html', status: err.status || 400, config});
 | 
			
		||||
      } catch (err) {
 | 
			
		||||
        return next(err);
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        return next(error);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
@ -1183,7 +1183,7 @@ export class FlexServer implements GristServer {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 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"); }
 | 
			
		||||
 | 
			
		||||
    this.app.get('/download', ...docAccessMiddleware, expressWrap(async (req, res) => {
 | 
			
		||||
 | 
			
		||||
@ -598,7 +598,7 @@ export class GranularAccess implements GranularAccessForBundle {
 | 
			
		||||
    const message = { actionGroup, docActions };
 | 
			
		||||
    await this._docClients.broadcastDocMessage(client, 'docUserAction',
 | 
			
		||||
                                               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 (access.getColumnAccess(tableId, colId).perms.read !== 'mixed') { continue; }
 | 
			
		||||
      // Check column accessibility before and after.
 | 
			
		||||
      const forbiddenBefores = new Set(await this._getForbiddenRows(cursor, rowsBefore, ids, colId));
 | 
			
		||||
      const forbiddenAfters = new Set(await this._getForbiddenRows(cursor, rowsAfter, ids, colId));
 | 
			
		||||
      const _forbiddenBefores = new Set(await this._getForbiddenRows(cursor, rowsBefore, 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,
 | 
			
		||||
      // pull it into the doc actions.  We don't censor cells yet, that happens later
 | 
			
		||||
      // (if that's what needs doing).
 | 
			
		||||
      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) {
 | 
			
		||||
        revisedDocActions.push(this._makeColumnUpdate(rowsAfter, colId, new Set(changedIds)));
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Return the results, also applying any cell-level access control.
 | 
			
		||||
    for (const action of revisedDocActions) {
 | 
			
		||||
      await this._filterRowsAndCells({...cursor, action}, rowsAfter, rowsAfter, readAccessCheck);
 | 
			
		||||
    for (const a of revisedDocActions) {
 | 
			
		||||
      await this._filterRowsAndCells({...cursor, action: a}, rowsAfter, rowsAfter, readAccessCheck);
 | 
			
		||||
    }
 | 
			
		||||
    return revisedDocActions;
 | 
			
		||||
  }
 | 
			
		||||
@ -1656,7 +1656,7 @@ export const accessChecks = {
 | 
			
		||||
const readAccessCheck = accessChecks.check.read;
 | 
			
		||||
 | 
			
		||||
// 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,
 | 
			
		||||
        snapshotId: newSnapshotId,
 | 
			
		||||
        metadata
 | 
			
		||||
      }
 | 
			
		||||
      };
 | 
			
		||||
      await this._inventory.add(docId, snapshot, prevSnapshotId);
 | 
			
		||||
      await this._onInventoryChange(docId);
 | 
			
		||||
    } 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.
 | 
			
		||||
 | 
			
		||||
@ -27,7 +27,7 @@ export function getRelatedRows(docActions: DocAction[]): ReadonlyArray<readonly
 | 
			
		||||
    if (docAction[0] === 'RenameTable') {
 | 
			
		||||
      if (addedTables.has(currentTableId)) {
 | 
			
		||||
        addedTables.delete(currentTableId);
 | 
			
		||||
        addedTables.add(docAction[2])
 | 
			
		||||
        addedTables.add(docAction[2]);
 | 
			
		||||
        continue;
 | 
			
		||||
      }
 | 
			
		||||
      tableIds.delete(currentTableId);
 | 
			
		||||
 | 
			
		||||
@ -118,7 +118,7 @@ export class Sharing {
 | 
			
		||||
    assert(this._hubQueue.isEmpty() && !this._pendingQueue.isEmpty());
 | 
			
		||||
    const userRequest: UserRequest = this._pendingQueue.shift()!;
 | 
			
		||||
    try {
 | 
			
		||||
      const ret = await this.doApplyUserActionBundle(userRequest.action, userRequest.docSession);
 | 
			
		||||
      const ret = await this._doApplyUserActionBundle(userRequest.action, userRequest.docSession);
 | 
			
		||||
      userRequest.resolve(ret);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      log.warn("Unable to apply action...", e);
 | 
			
		||||
@ -130,7 +130,7 @@ export class Sharing {
 | 
			
		||||
    assert(!this._hubQueue.isEmpty() && !this._actionHistory.haveLocalActions());
 | 
			
		||||
    const action: ActionBundle = this._hubQueue.shift()!;
 | 
			
		||||
    try {
 | 
			
		||||
      await this.doApplySharedActionBundle(action);
 | 
			
		||||
      await this._doApplySharedActionBundle(action);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      log.error("Unable to apply hub action... skipping");
 | 
			
		||||
    }
 | 
			
		||||
@ -155,15 +155,15 @@ export class Sharing {
 | 
			
		||||
  private async _rebaseLocalActions(): Promise<void> {
 | 
			
		||||
    const rebaseQueue: Deque<UserActionBundle> = new Deque<UserActionBundle>();
 | 
			
		||||
    try {
 | 
			
		||||
      this.createCheckpoint();
 | 
			
		||||
      this._createCheckpoint();
 | 
			
		||||
      const actions: LocalActionBundle[] = await this._actionHistory.fetchAllLocal();
 | 
			
		||||
      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)));
 | 
			
		||||
      await this._actionHistory.clearLocalActions();
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      log.error("Can't undo local actions; sharing is off");
 | 
			
		||||
      this.rollbackToCheckpoint();
 | 
			
		||||
      this._rollbackToCheckpoint();
 | 
			
		||||
      // TODO this.disconnect();
 | 
			
		||||
      // TODO errorState = true;
 | 
			
		||||
      return;
 | 
			
		||||
@ -178,34 +178,34 @@ export class Sharing {
 | 
			
		||||
      const action: UserActionBundle = rebaseQueue.shift()!;
 | 
			
		||||
      const adjusted: UserActionBundle = this._mergeAdjust(action);
 | 
			
		||||
      try {
 | 
			
		||||
        await this.doApplyUserActionBundle(adjusted, null);
 | 
			
		||||
        await this._doApplyUserActionBundle(adjusted, null);
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
        log.warn("Unable to apply rebased action...");
 | 
			
		||||
        rebaseFailures.push([action, adjusted]);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    if (rebaseFailures.length > 0) {
 | 
			
		||||
      this.createBackupAtCheckpoint();
 | 
			
		||||
      this._createBackupAtCheckpoint();
 | 
			
		||||
      // TODO we should notify the user too.
 | 
			
		||||
      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[] = [
 | 
			
		||||
      ['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> {
 | 
			
		||||
    return this.doApplyUserActions(action.info, action.userActions, Branch.Local, docSession);
 | 
			
		||||
  private _doApplyUserActionBundle(action: UserActionBundle, docSession: OptDocSession|null): Promise<UserResult> {
 | 
			
		||||
    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> {
 | 
			
		||||
    const client = docSession && docSession.client;
 | 
			
		||||
 | 
			
		||||
@ -245,7 +245,7 @@ export class Sharing {
 | 
			
		||||
        actionHash: 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
 | 
			
		||||
      // 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.
 | 
			
		||||
  private createCheckpoint() { /* TODO */ }
 | 
			
		||||
  private releaseCheckpoint() { /* TODO */ }
 | 
			
		||||
  private rollbackToCheckpoint() { /* TODO */ }
 | 
			
		||||
  private createBackupAtCheckpoint() { /* TODO */ }
 | 
			
		||||
  private _createCheckpoint() { /* TODO */ }
 | 
			
		||||
  private _releaseCheckpoint() { /* TODO */ }
 | 
			
		||||
  private _rollbackToCheckpoint() { /* TODO */ }
 | 
			
		||||
  private _createBackupAtCheckpoint() { /* TODO */ }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * 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.
 | 
			
		||||
          store.client.unref();
 | 
			
		||||
        }});
 | 
			
		||||
    }
 | 
			
		||||
    };
 | 
			
		||||
  } else {
 | 
			
		||||
    const SQLiteStore = require('@gristlabs/connect-sqlite3')(session);
 | 
			
		||||
    promisifyAll(SQLiteStore.prototype);
 | 
			
		||||
@ -72,7 +72,7 @@ function createSessionStoreFactory(sessionsDB: string): () => SessionStore {
 | 
			
		||||
        table: 'sessions'
 | 
			
		||||
      });
 | 
			
		||||
      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,
 | 
			
		||||
                                   family?: number, allowHalfOpen?: boolean; }): 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> {
 | 
			
		||||
  return new Promise((resolve, reject) => {
 | 
			
		||||
    const s = net.connect(arg, ...moreArgs, () => resolve(s));
 | 
			
		||||
 | 
			
		||||
@ -1333,8 +1333,9 @@ export class Session {
 | 
			
		||||
    const api = this.createHomeApi();
 | 
			
		||||
    if (!noCleanup) {
 | 
			
		||||
      cleanup.addAfterEach(async () => {
 | 
			
		||||
        if (doc.id)
 | 
			
		||||
        if (doc.id) {
 | 
			
		||||
          await api.deleteDoc(doc.id).catch(noop);
 | 
			
		||||
        }
 | 
			
		||||
        doc.id = '';
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user