mirror of
				https://github.com/gristlabs/grist-core.git
				synced 2025-06-13 20:53:59 +00:00 
			
		
		
		
	(core) updates from grist-core
This commit is contained in:
		
						commit
						be8c053922
					
				@ -60,7 +60,7 @@ or browse [our extensive documentation](https://support.getgrist.com).
 | 
			
		||||
 | 
			
		||||
There are docker images set up for individual use, or (with some
 | 
			
		||||
configuration) for self-hosting. Grist Labs offers a hosted service
 | 
			
		||||
at [https://docs.getgrist.com](https://docs.getgrist.com).
 | 
			
		||||
at [docs.getgrist.com](https://docs.getgrist.com).
 | 
			
		||||
 | 
			
		||||
To run Grist running on your computer with [Docker](https://www.docker.com/get-started), do:
 | 
			
		||||
 | 
			
		||||
@ -76,10 +76,10 @@ and export documents. To preserve your work across docker runs, share a director
 | 
			
		||||
docker run -p 8484:8484 -v $PWD/persist:/persist -it gristlabs/grist
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Get templates at https://templates.getgrist.com/ for payroll,
 | 
			
		||||
Get templates at [templates.getgrist.com](https://templates.getgrist.com) for payroll,
 | 
			
		||||
inventory management, invoicing, D&D encounter tracking, and a lot
 | 
			
		||||
more, or use any document you've created on
 | 
			
		||||
[https://docs.getgrist.com](docs.getgrist.com).
 | 
			
		||||
[docs.getgrist.com](https://docs.getgrist.com).
 | 
			
		||||
 | 
			
		||||
If you need to change the port Grist runs on, set a `PORT` variable, don't just change the
 | 
			
		||||
port mapping:
 | 
			
		||||
 | 
			
		||||
@ -99,7 +99,7 @@ function makePermissionSet(bits: PermissionKey[], makeValue: (bit: PermissionKey
 | 
			
		||||
  return pset;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Helper for a tick (checkmark) icon, replacing it with an equialent space when not shown.
 | 
			
		||||
// Helper for a tick (checkmark) icon, replacing it with an equivalent space when not shown.
 | 
			
		||||
function tick(show: boolean) {
 | 
			
		||||
  return show ? menuIcon('Tick') : cssMenuIconSpace();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -158,7 +158,7 @@ function BaseView(gristDoc, viewSectionModel, options) {
 | 
			
		||||
  this._isLoading = ko.observable(true);
 | 
			
		||||
  this._pendingCursorPos = this.viewSection.lastCursorPos;
 | 
			
		||||
 | 
			
		||||
  // Initialize the cursor with the previous cursor position indicies, if they exist.
 | 
			
		||||
  // Initialize the cursor with the previous cursor position indices, if they exist.
 | 
			
		||||
  console.log("%s BaseView viewSection %s (%s) lastCursorPos %s", this._debugName, this.viewSection.getRowId(),
 | 
			
		||||
    this.viewSection.table().tableId(), JSON.stringify(this.viewSection.lastCursorPos));
 | 
			
		||||
  this.cursor = this.autoDispose(Cursor.create(null, this, this.viewSection.lastCursorPos));
 | 
			
		||||
 | 
			
		||||
@ -97,7 +97,7 @@ export class ColumnTransform extends Disposable {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Helper called by contructor to prepare the column transform.
 | 
			
		||||
   * Helper called by constructor to prepare the column transform.
 | 
			
		||||
   * @param {String} colType: A pure or complete type for the transformed column.
 | 
			
		||||
   */
 | 
			
		||||
  public async prepare(optColType?: string) {
 | 
			
		||||
@ -156,7 +156,7 @@ export class ColumnTransform extends Disposable {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Adds the tranform column and returns its colRef. May be overridden by derived classes to create
 | 
			
		||||
   * Adds the transform column and returns its colRef. May be overridden by derived classes to create
 | 
			
		||||
   * differently-prepared transform columns.
 | 
			
		||||
   * @param {String} colType: A pure or complete type for the transformed column.
 | 
			
		||||
   */
 | 
			
		||||
 | 
			
		||||
@ -77,7 +77,7 @@ export class Drafts extends Disposable {
 | 
			
		||||
        editor.setState(draft.state);
 | 
			
		||||
      }
 | 
			
		||||
      // We don't need the draft any more.
 | 
			
		||||
      // If user presses escape one more time it will be crated
 | 
			
		||||
      // If user presses escape one more time it will be created
 | 
			
		||||
      // once again
 | 
			
		||||
      storage.clear();
 | 
			
		||||
      // Close the notification
 | 
			
		||||
 | 
			
		||||
@ -610,7 +610,7 @@ GridView.prototype.assignCursor = function(elem, elemType) {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Schedules cursor assignement to happen at end of tick. Calling `preventAssignCursor()` before
 | 
			
		||||
 * Schedules cursor assignment to happen at end of tick. Calling `preventAssignCursor()` before
 | 
			
		||||
 * prevents assignment to happen. This was added to prevent cursor assignment on a `context click`
 | 
			
		||||
 * on a cell that is already selected.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
@ -351,7 +351,7 @@ export class Importer extends DisposableWithEvents {
 | 
			
		||||
      destTableId,
 | 
			
		||||
      destCols: transformFields.map<TransformColumn>((field) => ({
 | 
			
		||||
        label: field.label(),
 | 
			
		||||
        colId: destTableId ? field.colId() : null, // if inserting into new table, colId isnt defined
 | 
			
		||||
        colId: destTableId ? field.colId() : null, // if inserting into new table, colId isn't defined
 | 
			
		||||
        type: field.column().type(),
 | 
			
		||||
        formula: field.column().formula()
 | 
			
		||||
      })),
 | 
			
		||||
 | 
			
		||||
@ -215,7 +215,7 @@ RecordLayout.prototype.saveLayoutSpec = async function(layoutSpec) {
 | 
			
		||||
  var addedPositions = [];
 | 
			
		||||
 | 
			
		||||
  // Recursively process all layoutBoxes in the spec. Sets up bookkeeping arrays for
 | 
			
		||||
  // exisiting fields and added fields for new/hidden cols from which the action bundle will
 | 
			
		||||
  // existing fields and added fields for new/hidden cols from which the action bundle will
 | 
			
		||||
  // be created.
 | 
			
		||||
  function processBox(spec) {
 | 
			
		||||
    // "empty" is a temporary placeholder used by LayoutEditor, and not a valid leaf.
 | 
			
		||||
 | 
			
		||||
@ -3,7 +3,7 @@
 | 
			
		||||
 * of a plugin.
 | 
			
		||||
 *
 | 
			
		||||
 * A plugin's safeBrowser component is made of one main entry point (the javascript files declares
 | 
			
		||||
 * in the manifest), html files and any ressources included by the html files (css, scripts, images
 | 
			
		||||
 * in the manifest), html files and any resources included by the html files (css, scripts, images
 | 
			
		||||
 * ...). The main script is the main entry point which uses the Grist API to render the views,
 | 
			
		||||
 * communicate with them en dispose them.
 | 
			
		||||
 *
 | 
			
		||||
@ -18,13 +18,13 @@
 | 
			
		||||
 *
 | 
			
		||||
 * The grist API available to safeBrowser components is implemented in `app/plugin/PluginImpl.ts`.
 | 
			
		||||
 *
 | 
			
		||||
 * All the safeBrowser's component ressources, including the main script, the html files and any
 | 
			
		||||
 * other ressources needed by the views, should be placed within one plugins' subfolder, and Grist
 | 
			
		||||
 * All the safeBrowser's component resources, including the main script, the html files and any
 | 
			
		||||
 * other resources needed by the views, should be placed within one plugins' subfolder, and Grist
 | 
			
		||||
 * should serve only this folder. However, this is not yet implemented and is left as a TODO, as of
 | 
			
		||||
 * now the whole plugin's folder is served.
 | 
			
		||||
 *
 | 
			
		||||
 */
 | 
			
		||||
 // Todo: plugin ressources should not be made available on the server by default, but only after
 | 
			
		||||
 // Todo: plugin resources should not be made available on the server by default, but only after
 | 
			
		||||
 // activation.
 | 
			
		||||
 | 
			
		||||
// tslint:disable:max-classes-per-file
 | 
			
		||||
@ -131,7 +131,7 @@ export class SafeBrowser extends BaseComponent {
 | 
			
		||||
    // it's ok to leave it for now: (1) fixing this would require (yet) another refactoring of
 | 
			
		||||
    // SafeBrowser and (2) at this point it is not sure wether we want to keep `render()` in the
 | 
			
		||||
    // future (we could as well directly register contribution using files directly in the
 | 
			
		||||
    // manifest), and (3) plugins are only developped by us, we only have to remember that using
 | 
			
		||||
    // manifest), and (3) plugins are only developed by us, we only have to remember that using
 | 
			
		||||
    // `render()` is only supported from within the main process (which cover all our use cases so
 | 
			
		||||
    // far).
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										2
									
								
								app/client/lib/dispose.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								app/client/lib/dispose.d.ts
									
									
									
									
										vendored
									
									
								
							@ -1,4 +1,4 @@
 | 
			
		||||
// TODO: add remaining Disposable methode
 | 
			
		||||
// TODO: add remaining Disposable method
 | 
			
		||||
export abstract class Disposable {
 | 
			
		||||
  public static create<T extends new (...args: any[]) => any>(
 | 
			
		||||
    this: T, ...args: ConstructorParameters<T>): InstanceType<T>;
 | 
			
		||||
 | 
			
		||||
@ -146,7 +146,7 @@ export async function uploadFiles(
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Fetches ressource from a url and returns an UploadResult. Tries to fetch from the client and
 | 
			
		||||
 * Fetches resource from a url and returns an UploadResult. Tries to fetch from the client and
 | 
			
		||||
 * upload the file to the server. If unsuccessful, tries to fetch directly from the server. In both
 | 
			
		||||
 * case, it guesses the name of the file based on the response's content-type and the url.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
@ -34,7 +34,7 @@ export class DocData extends BaseDocData {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Constructor for DocData.
 | 
			
		||||
   * @param {Object} docComm: A map of server methods availble on this document.
 | 
			
		||||
   * @param {Object} docComm: A map of server methods available on this document.
 | 
			
		||||
   * @param {Object} metaTableData: A map from tableId to table data, presented as an action,
 | 
			
		||||
   *      equivalent to BulkAddRecord, i.e. ["TableData", tableId, rowIds, columnValues].
 | 
			
		||||
   */
 | 
			
		||||
 | 
			
		||||
@ -74,7 +74,7 @@ function getRecords(table: TreeTableData) {
 | 
			
		||||
  return fixIndents(records);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// The fixIndents function returns a copy of records with the garantee the .indentation starts at 0
 | 
			
		||||
// The fixIndents function returns a copy of records with the guarantee the .indentation starts at 0
 | 
			
		||||
// and can only increase one step at a time (note that it is however permitted to decrease several
 | 
			
		||||
// level at a time). This is useful to build a model for the tree view.
 | 
			
		||||
export function fixIndents(records: TreeRecord[]) {
 | 
			
		||||
 | 
			
		||||
@ -20,7 +20,7 @@ export class UserError extends Error {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This error causes Notifer to show the message with an upgrade link.
 | 
			
		||||
 * This error causes Notifier to show the message with an upgrade link.
 | 
			
		||||
 */
 | 
			
		||||
export class NeedUpgradeError extends Error {
 | 
			
		||||
  public name: string = 'NeedUpgradeError';
 | 
			
		||||
 | 
			
		||||
@ -20,7 +20,7 @@ const testId = makeTestId('test-apikey-');
 | 
			
		||||
 * ApiKey component shows an api key with controls to change it. Expects `options.apiKey` the api
 | 
			
		||||
 * key and shows it if value is truthy along with a 'Delete' button that triggers the
 | 
			
		||||
 * `options.onDelete` callback. When `options.apiKey` is falsy, hides it and show a 'Create' button
 | 
			
		||||
 * that triggers the `options.onCreate` callback. It is the responsability of the caller to update
 | 
			
		||||
 * that triggers the `options.onCreate` callback. It is the responsibility of the caller to update
 | 
			
		||||
 * the `options.apiKey` to its new value.
 | 
			
		||||
 */
 | 
			
		||||
export class ApiKey extends Disposable {
 | 
			
		||||
 | 
			
		||||
@ -56,7 +56,7 @@ function createMainPage(appModel: AppModel, appObj: App) {
 | 
			
		||||
    } else if (err && (err.status === 401 || err.status === 403)) {
 | 
			
		||||
      // Generally give access denied error.
 | 
			
		||||
      // The exception is for document pages, where we want to allow access to documents
 | 
			
		||||
      // shared publically without being shared specifically with the current user.
 | 
			
		||||
      // shared publicly without being shared specifically with the current user.
 | 
			
		||||
      if (appModel.pageType.get() !== 'doc') {
 | 
			
		||||
        return createForbiddenPage(appModel);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
@ -207,7 +207,7 @@ export function columnFilterMenu(owner: IDisposableOwner, opts: IFilterMenuOptio
 | 
			
		||||
 * On user clicks, if checkbox is checked, it does uncheck all the values, and if the
 | 
			
		||||
 * `switchFilterType` is true it also converts the filter into an inclusion filter. But if the
 | 
			
		||||
 * checkbox is unchecked, or in the Indeterminate state, it does check all the values, and if the
 | 
			
		||||
 * `switchFilterType` is true it also converts the filter into an exlusion filter.
 | 
			
		||||
 * `switchFilterType` is true it also converts the filter into an exclusion filter.
 | 
			
		||||
 */
 | 
			
		||||
function buildSummary(label: string|Computed<string>, values: Array<[CellValue, IFilterCount]>,
 | 
			
		||||
                      switchFilterType: boolean, model: ColumnFilterMenuModel) {
 | 
			
		||||
 | 
			
		||||
@ -53,7 +53,7 @@ export function openFilePicker(options: FileDialogOptions): Promise<File[]> {
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Opens the file picker dialog. If files are selected, calls the provided callback.
 | 
			
		||||
 * If no files are seleced, will call the callback with an empty list if possible, or more
 | 
			
		||||
 * If no files are selected, will call the callback with an empty list if possible, or more
 | 
			
		||||
 * typically not call it at all.
 | 
			
		||||
 */
 | 
			
		||||
export function open(options: FileDialogOptions, callback: FilesCB): void {
 | 
			
		||||
 | 
			
		||||
@ -194,7 +194,7 @@ export class MFAConfig extends Disposable {
 | 
			
		||||
              return [
 | 
			
		||||
                this._buildSecurityVerificationForm(ctl, {onSuccess: async (hadSecondStep) => {
 | 
			
		||||
                  /**
 | 
			
		||||
                   * If method was unspecified, but second step verification occured, we know that
 | 
			
		||||
                   * If method was unspecified, but second step verification occurred, we know that
 | 
			
		||||
                   * the client doesn't have up-to-date 2FA preferences. Close the modal, and force
 | 
			
		||||
                   * a refresh of UserMFAPreferences, which should cause the correct buttons to be
 | 
			
		||||
                   * rendered once preferences are loaded.
 | 
			
		||||
 | 
			
		||||
@ -90,7 +90,7 @@ function allowOtherOrgs(doc: Document, app: AppModel): boolean {
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Ask user for the desination and new name, and make a copy of the doc using those.
 | 
			
		||||
 * Ask user for the destination and new name, and make a copy of the doc using those.
 | 
			
		||||
 */
 | 
			
		||||
export async function makeCopy(doc: Document, app: AppModel, modalTitle: string): Promise<void> {
 | 
			
		||||
  if (!app.currentValidUser) {
 | 
			
		||||
 | 
			
		||||
@ -75,7 +75,7 @@ function buildDomFromTable(pagesTable: MetaTableModel<PageRec>, activeDoc: Grist
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // if user removes a primary view, let's confirm first, because this will remove the
 | 
			
		||||
    // corresponsing table and also all pages that are using this table.
 | 
			
		||||
    // corresponding table and also all pages that are using this table.
 | 
			
		||||
    // TODO: once we have raw table view, removing page should remove just the view (not the
 | 
			
		||||
    // table), but for now this is the only way to remove a table in the newui.
 | 
			
		||||
    actions.onRemove = () => confirmModal(
 | 
			
		||||
 | 
			
		||||
@ -91,7 +91,7 @@ const testId = makeTestId('test-treeview-');
 | 
			
		||||
 * item and by highlighting its parent. In order to ensure data consistency, the component prevents
 | 
			
		||||
 * dropping an item within its own children. If the cursor leaves the component during a drag, all
 | 
			
		||||
 * such visual artifact (handle, target and target's parent) are hidden, but if the cursor re-enter
 | 
			
		||||
 * the componet without releasing the mouse, they will show again allowing user to resume dragging.
 | 
			
		||||
 * the component without releasing the mouse, they will show again allowing user to resume dragging.
 | 
			
		||||
 */
 | 
			
		||||
 // note to self: in the future the model will be updated by the server, which could cause conflicts
 | 
			
		||||
 // if the user is dragging at the same time. It could be simpler to freeze the model and to differ
 | 
			
		||||
@ -125,7 +125,7 @@ export class TreeViewComponent extends Disposable {
 | 
			
		||||
 | 
			
		||||
    // While building dom we add listeners to the children of all tree nodes to watch for changes
 | 
			
		||||
    // and call this._update. Hence, repeated calls to this._update is likely to add or remove
 | 
			
		||||
    // listeners to the observable that triggered the udpate which is not supported by grainjs and
 | 
			
		||||
    // listeners to the observable that triggered the update which is not supported by grainjs and
 | 
			
		||||
    // could fail (possibly infinite loop). Debounce allows for several change to resolve to a
 | 
			
		||||
    // single update.
 | 
			
		||||
    this._update = debounce(this._update.bind(this), 0, {leading: false});
 | 
			
		||||
@ -230,7 +230,7 @@ export class TreeViewComponent extends Disposable {
 | 
			
		||||
 | 
			
		||||
  // Update this._childrenDom with the content of the new tree. Its rebuilds entirely the tree of
 | 
			
		||||
  // items and reuses dom from the old content for each item that were already part of the old
 | 
			
		||||
  // tree. Then takes care of disposing dom for thoses items that were removed from the old tree.
 | 
			
		||||
  // tree. Then takes care of disposing dom for those items that were removed from the old tree.
 | 
			
		||||
  private _update() {
 | 
			
		||||
    this._childrenDom.set(this._buildChildren(this._model.get().children(), 0));
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -423,7 +423,7 @@ function buildCheckbox(...args: IDomArgs<HTMLInputElement>) {
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// helper to find checkboxes withing a draggable list. This assumes that checkboxes are the only
 | 
			
		||||
// helper to find checkboxes within a draggable list. This assumes that checkboxes are the only
 | 
			
		||||
// <input> element in draggableElement.
 | 
			
		||||
function findCheckboxes(draggableElement: Element): NodeListOf<HTMLInputElement> {
 | 
			
		||||
  return draggableElement.querySelectorAll<HTMLInputElement>('input');
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
/**
 | 
			
		||||
 * CSS Variables. To use in your web appication, add `cssRootVars` to the class list for your app's
 | 
			
		||||
 * CSS Variables. To use in your web application, add `cssRootVars` to the class list for your app's
 | 
			
		||||
 * root node, typically `<body>`.
 | 
			
		||||
 *
 | 
			
		||||
 * The fonts used attempt to default to system fonts as described here:
 | 
			
		||||
 | 
			
		||||
@ -20,7 +20,7 @@ const cssWrapper = styled('div', `
 | 
			
		||||
`);
 | 
			
		||||
 | 
			
		||||
export const cssLabelText = styled(rawTextInput, `
 | 
			
		||||
  /* Reset apperance */
 | 
			
		||||
  /* Reset appearance */
 | 
			
		||||
  -webkit-appearance: none;
 | 
			
		||||
  -moz-appearance: none;
 | 
			
		||||
  padding: 0;
 | 
			
		||||
 | 
			
		||||
@ -22,7 +22,7 @@ export interface IModalControl {
 | 
			
		||||
  // Returns true if closed, false if closing was prevented.
 | 
			
		||||
  closeAndWait(): Promise<boolean>;
 | 
			
		||||
 | 
			
		||||
  // Prevents closing, if close has been called ans is pending. No-op otherwise.
 | 
			
		||||
  // Prevents closing, if close has been called and is pending. No-op otherwise.
 | 
			
		||||
  preventClose(): void;
 | 
			
		||||
 | 
			
		||||
  // Wraps the passed-in function, so that closing is delayed while the function is running. If
 | 
			
		||||
 | 
			
		||||
@ -64,7 +64,7 @@ export function buildPageDom(name: Observable<string>, actions: PageActions, ...
 | 
			
		||||
              dom.on('click', (ev) => { ev.stopPropagation(); ev.preventDefault(); })
 | 
			
		||||
            ),
 | 
			
		||||
            // Note that we don't pass extra args when renaming is on, because they usually includes
 | 
			
		||||
            // mouse event handlers interferring with input editor and yields wrong behavior on
 | 
			
		||||
            // mouse event handlers interfering with input editor and yields wrong behavior on
 | 
			
		||||
            // firefox.
 | 
			
		||||
          ) :
 | 
			
		||||
          cssPageItem(
 | 
			
		||||
 | 
			
		||||
@ -35,7 +35,7 @@ BaseEditor.prototype.getCellValue = function() {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Used if an editor needs preform any actions before a save
 | 
			
		||||
 * Used if an editor needs perform any actions before a save
 | 
			
		||||
 */
 | 
			
		||||
BaseEditor.prototype.prepForSave = function() {
 | 
			
		||||
  // No-op by default.
 | 
			
		||||
 | 
			
		||||
@ -430,9 +430,9 @@ export function openFormulaEditor(options: {
 | 
			
		||||
  editor.attach(refElem);
 | 
			
		||||
 | 
			
		||||
  // When formula is empty enter formula-editing mode (highlight formula icons; click on a column inserts its ID).
 | 
			
		||||
  // This function is used for primarily for switching between diffrent column behaviors, so we want to enter full
 | 
			
		||||
  // This function is used for primarily for switching between different column behaviors, so we want to enter full
 | 
			
		||||
  // edit mode right away.
 | 
			
		||||
  // TODO: consider converting it to parameter, when this will be used in diffrent scenarios.
 | 
			
		||||
  // TODO: consider converting it to parameter, when this will be used in different scenarios.
 | 
			
		||||
  if (!column.formula()) {
 | 
			
		||||
    field.editingFormula(true);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -91,7 +91,7 @@ export abstract class NewBaseEditor extends Disposable {
 | 
			
		||||
  public abstract getCellValue(): CellValue;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Used if an editor needs preform any actions before a save
 | 
			
		||||
   * Used if an editor needs perform any actions before a save
 | 
			
		||||
   */
 | 
			
		||||
  public prepForSave(): void | Promise<void> {
 | 
			
		||||
    // No-op by default.
 | 
			
		||||
 | 
			
		||||
@ -28,7 +28,7 @@ import toPairs = require('lodash/toPairs');
 | 
			
		||||
 * removal of a table (or column) as the special name pair [finalName, null].
 | 
			
		||||
 *
 | 
			
		||||
 * An ActionSummary contains two fields:
 | 
			
		||||
 *   - tableRenames: a list of table name changes (incuding addition/removal).
 | 
			
		||||
 *   - tableRenames: a list of table name changes (including addition/removal).
 | 
			
		||||
 *   - tableDeltas: a dictionary of changes within a table.
 | 
			
		||||
 *
 | 
			
		||||
 * The key of the tableDeltas dictionary is the name of a table at the end of the
 | 
			
		||||
@ -45,7 +45,7 @@ import toPairs = require('lodash/toPairs');
 | 
			
		||||
 *
 | 
			
		||||
 * The changes within a table are represented as a TableDelta, which has the following
 | 
			
		||||
 * fields:
 | 
			
		||||
 *   - columnRenames: a list of column name changes (incuding addition/removal).
 | 
			
		||||
 *   - columnRenames: a list of column name changes (including addition/removal).
 | 
			
		||||
 *   - columnDeltas: a dictionary of changes within a column.
 | 
			
		||||
 *   - updateRows, removeRows, addRows: lists of affected rows.
 | 
			
		||||
 *
 | 
			
		||||
 | 
			
		||||
@ -58,7 +58,7 @@ export interface ImportResult {
 | 
			
		||||
 | 
			
		||||
export interface ImportTableResult {
 | 
			
		||||
  hiddenTableId: string;
 | 
			
		||||
  uploadFileIndex: number;      // Index into upload.files array, for the file reponsible for this table.
 | 
			
		||||
  uploadFileIndex: number;      // Index into upload.files array, for the file responsible for this table.
 | 
			
		||||
  origTableName: string;
 | 
			
		||||
  transformSectionRef: number;
 | 
			
		||||
  destTableId: string|null;
 | 
			
		||||
 | 
			
		||||
@ -132,7 +132,7 @@ BinaryIndexedTree.prototype.fillFromCumulative = function(cumulValues) {
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Creates a tree from an array of invididual values.
 | 
			
		||||
 * Creates a tree from an array of individual values.
 | 
			
		||||
 * Takes time linear in the size of the array.
 | 
			
		||||
 * @param {Array<number>} - array with each element containing the value to insert.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,7 @@
 | 
			
		||||
 * InactivityTimer allows to set a function that executes after a certain time of
 | 
			
		||||
 * inactivity. Activities can be of two kinds: synchronous or asynchronous. Asynchronous activities,
 | 
			
		||||
 * are handle with the `disableUntiFinish` method that takes in a Promise and makes sure that the
 | 
			
		||||
 * timer does not start before the promise resolves. Synchroneous activities are monitored with the
 | 
			
		||||
 * timer does not start before the promise resolves. Synchronous activities are monitored with the
 | 
			
		||||
 * `ping` method which resets the timer if called during inactivity.
 | 
			
		||||
 *
 | 
			
		||||
 * Timer won't start before any activity happens, but you may simply call ping() after construction
 | 
			
		||||
 | 
			
		||||
@ -12,7 +12,7 @@ import {RenderOptions, RenderTarget} from 'app/plugin/RenderOptions';
 | 
			
		||||
export type ComponentKind = "safeBrowser" | "safePython" | "unsafeNode";
 | 
			
		||||
 | 
			
		||||
// Describes a function that appends some html content to `containerElement` given some
 | 
			
		||||
// options. Usefull for provided by a plugin.
 | 
			
		||||
// options. Useful for provided by a plugin.
 | 
			
		||||
export type TargetRenderFunc = (containerElement: HTMLElement, options?: RenderOptions) => void;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@ -93,7 +93,7 @@ export abstract class BaseComponent implements IForwarderDest {
 | 
			
		||||
 * Node Implementation for the PluginElement interface. A PluginInstance take care of activation of
 | 
			
		||||
 * the the plugins's components (activating, timing and deactivating), and create the api's for each contributions.
 | 
			
		||||
 *
 | 
			
		||||
 * Do not try to instanciate yourself, PluginManager does it for you. Instead use the
 | 
			
		||||
 * Do not try to instantiate yourself, PluginManager does it for you. Instead use the
 | 
			
		||||
 * PluginManager.getPlugin(id) method that get instances for you.
 | 
			
		||||
 *
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
@ -64,7 +64,7 @@ export function typedCompare(val1: any, val2: any): number {
 | 
			
		||||
  if ((result = nativeCompare(type1 = typeof val1, typeof val2)) !== 0) {
 | 
			
		||||
    return result;
 | 
			
		||||
  }
 | 
			
		||||
  // We need to worry about Array comparisons because formulas returing Any may return null or
 | 
			
		||||
  // We need to worry about Array comparisons because formulas returning Any may return null or
 | 
			
		||||
  // object values represented as arrays (e.g. ['D', ...] for dates). Comparing those without
 | 
			
		||||
  // distinguishing types would break the sort. Also, arrays need a special comparator.
 | 
			
		||||
  if (type1 === 'object') {
 | 
			
		||||
 | 
			
		||||
@ -40,7 +40,7 @@ function hasNestedObjects(value: any[]) {
 | 
			
		||||
 * Formats a decoded Grist value for displaying it. For top-level values, formats them the way we
 | 
			
		||||
 * like to see them in a cell or in, say, CSV export.
 | 
			
		||||
 * For top-level lists containing only simple values like strings and dates, formats them as a CSV row.
 | 
			
		||||
 * Nested lists and objects are formatted slighly differently, with quoted strings and ISO format for dates.
 | 
			
		||||
 * Nested lists and objects are formatted slightly differently, with quoted strings and ISO format for dates.
 | 
			
		||||
 */
 | 
			
		||||
export function formatDecoded(value: unknown, isTopLevel: boolean = true): string {
 | 
			
		||||
  if (typeof value === 'object' && value) {
 | 
			
		||||
 | 
			
		||||
@ -45,7 +45,7 @@ export interface GristAPI {
 | 
			
		||||
  /**
 | 
			
		||||
   * Render the file at `path` into the `target` location in Grist. `path` must be relative to the
 | 
			
		||||
   * root of the plugin's directory and point to an html that is contained within the plugin's
 | 
			
		||||
   * directory. `target` is a predifined location of the Grist UI, it could be `fullscreen` or
 | 
			
		||||
   * directory. `target` is a predefined location of the Grist UI, it could be `fullscreen` or
 | 
			
		||||
   * identifier for an inline target. Grist provides inline target identifiers in certain call
 | 
			
		||||
   * plugins. E.g. ImportSourceAPI.getImportSource is given a target identifier to allow rende UI
 | 
			
		||||
   * inline in the import dialog. Returns the procId which can be used to dispose the view.
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
// Provide a way to acess grist for iframe, web worker (which runs the main safeBrowser script) and
 | 
			
		||||
// Provide a way to access grist for iframe, web worker (which runs the main safeBrowser script) and
 | 
			
		||||
// unsafeNode. WebView should work the same way as iframe, grist is exposed just the same way and
 | 
			
		||||
// necessary api is exposed using preload script. Here we bootstrap from channel capabilities to key
 | 
			
		||||
// parts of the grist API.
 | 
			
		||||
@ -230,7 +230,7 @@ export function onOptions(callback: (options: any, settings: InteractionOptions)
 | 
			
		||||
 *
 | 
			
		||||
 */
 | 
			
		||||
export async function addImporter(name: string, path: string, mode: 'fullscreen' | 'inline', options?: RenderOptions) {
 | 
			
		||||
  // checker is omitted for implementation because call was alredy checked by grist.
 | 
			
		||||
  // checker is omitted for implementation because call was already checked by grist.
 | 
			
		||||
  rpc.registerImpl<InternalImportSourceAPI>(name, {
 | 
			
		||||
    async getImportSource(target: RenderTarget): Promise<ImportSource|undefined> {
 | 
			
		||||
      const procId = await api.render(path, mode === 'inline' ? target : 'fullscreen', options);
 | 
			
		||||
@ -308,7 +308,7 @@ if (typeof window !== 'undefined') {
 | 
			
		||||
  process.on('disconnect', () => { process.exit(0); });
 | 
			
		||||
} else {
 | 
			
		||||
  // Not a recognized environment, perhaps plain nodejs run independently of Grist, or tests
 | 
			
		||||
  // running under mocha. For now, we only provide a disfunctional implementation. It allows
 | 
			
		||||
  // running under mocha. For now, we only provide a dysfunctional 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; });
 | 
			
		||||
 | 
			
		||||
@ -453,7 +453,7 @@ export class ActionHistoryImpl implements ActionHistory {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Fetches the most recent action row from the history, orderd with earlier actions first.
 | 
			
		||||
   * Fetches the most recent action row from the history, ordered with earlier actions first.
 | 
			
		||||
   * If `maxActions` is supplied, at most that number of actions are returned.
 | 
			
		||||
   */
 | 
			
		||||
  private async _getRecentActionRows(maxActions: number|undefined,
 | 
			
		||||
 | 
			
		||||
@ -19,7 +19,7 @@ import values = require('lodash/values');
 | 
			
		||||
const MAXIMUM_INLINE_ROWS = 10;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Options when producing an action sumary.
 | 
			
		||||
 * Options when producing an action summary.
 | 
			
		||||
 */
 | 
			
		||||
export interface ActionSummaryOptions {
 | 
			
		||||
  maximumInlineRows?: number;       // Overrides the maximum number of rows in a
 | 
			
		||||
@ -309,7 +309,7 @@ function renameAndDelete<T>(entries: {[name: string]: T}, dead: Set<string>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Apply planned name changes to a pair of entries, and return a merged entry encorporating
 | 
			
		||||
 * Apply planned name changes to a pair of entries, and return a merged entry incorporating
 | 
			
		||||
 * their composition.
 | 
			
		||||
 *
 | 
			
		||||
 * @param names: the planned name changes as calculated by planNameMerge()
 | 
			
		||||
 | 
			
		||||
@ -349,7 +349,7 @@ export class ActiveDocImport {
 | 
			
		||||
        hiddenTableId: createdTableId, // TODO: rename thing?
 | 
			
		||||
        uploadFileIndex,
 | 
			
		||||
        origTableName,
 | 
			
		||||
        transformSectionRef, // TODO: this shouldnt always be needed, and we only get it if genimporttransform
 | 
			
		||||
        transformSectionRef, // TODO: this shouldn't always be needed, and we only get it if genimporttransform
 | 
			
		||||
        destTableId
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -482,7 +482,7 @@ export function assertAccess(
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Pull out headers to pass along to a proxied service.  Focussed primarily on
 | 
			
		||||
 * Pull out headers to pass along to a proxied service.  Focused primarily on
 | 
			
		||||
 * authentication.
 | 
			
		||||
 */
 | 
			
		||||
export function getTransitiveHeaders(req: Request): {[key: string]: string} {
 | 
			
		||||
 | 
			
		||||
@ -129,7 +129,7 @@ export function linkOrgWithEmail(session: SessionObj, email: string, org: string
 | 
			
		||||
 *
 | 
			
		||||
 * This is a view of the session object, for a single organization (the "scope").
 | 
			
		||||
 *
 | 
			
		||||
 * Local caching is disabled in an enviroment where there is a home server (or we are
 | 
			
		||||
 * Local caching is disabled in an environment where there is a home server (or we are
 | 
			
		||||
 * the home server).  In hosted Grist, per-instance caching would be a problem.
 | 
			
		||||
 *
 | 
			
		||||
 * We retain local caching for situations with a single server - especially electron.
 | 
			
		||||
 | 
			
		||||
@ -51,7 +51,7 @@ class GristDocAPIImpl implements GristDocAPI {
 | 
			
		||||
/**
 | 
			
		||||
 * DocPluginManager manages plugins for a document.
 | 
			
		||||
 *
 | 
			
		||||
 * DocPluginManager instanciates asynchronously. Wait for the `ready` to resolve before using any
 | 
			
		||||
 * DocPluginManager instantiates asynchronously. Wait for the `ready` to resolve before using any
 | 
			
		||||
 * plugin.
 | 
			
		||||
 *
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
@ -85,7 +85,7 @@ export class DocSnapshotPruner {
 | 
			
		||||
 * steady state).
 | 
			
		||||
 *
 | 
			
		||||
 * The list of versions (with metadata) for a document is itself stored in S3.  This isn't
 | 
			
		||||
 * ideal since we cannnot simply append a new version to the list without rewriting it in full.
 | 
			
		||||
 * ideal since we cannot simply append a new version to the list without rewriting it in full.
 | 
			
		||||
 * But the alternatives have more serious problems, and this way folds quite well into the
 | 
			
		||||
 * existing pruning setup.
 | 
			
		||||
 *   - Storing in db would mean we'd need sharding sooner than otherwise
 | 
			
		||||
 | 
			
		||||
@ -678,7 +678,7 @@ export class DocStorage implements ISQLiteDB {
 | 
			
		||||
   */
 | 
			
		||||
  public _initDB(): Promise<void> {
 | 
			
		||||
    // Set options for speed across multiple OSes/Filesystems.
 | 
			
		||||
    // WAL is fast and safe (guarantees consistency accross crashes), but has disadvantages
 | 
			
		||||
    // WAL is fast and safe (guarantees consistency across crashes), but has disadvantages
 | 
			
		||||
    // including generating unwanted extra files that can be tricky to deal with in renaming, etc
 | 
			
		||||
    // the options for WAL are commented out
 | 
			
		||||
    // Setting synchronous to OFF is the fastest method, but is not as safe, and could lead to
 | 
			
		||||
 | 
			
		||||
@ -57,7 +57,7 @@ export interface IDocWorkerMap extends IPermitStores, IElectionStore, IChecksumS
 | 
			
		||||
  setWorkerAvailability(workerId: string, available: boolean): Promise<void>;
 | 
			
		||||
 | 
			
		||||
  // Releases doc from worker, freeing it to be assigned elsewhere.
 | 
			
		||||
  // Assigments should only be released for workers that are now unavailable.
 | 
			
		||||
  // Assignments should only be released for workers that are now unavailable.
 | 
			
		||||
  releaseAssignment(workerId: string, docId: string): Promise<void>;
 | 
			
		||||
 | 
			
		||||
  // Get all assignments for a worker.  Should only be queried for a worker that
 | 
			
		||||
 | 
			
		||||
@ -134,7 +134,7 @@ export function expandQuery(iquery: ServerQuery, docData: DocData, onDemandFormu
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Build a query that relates two homogenous tables sharing a common set of columns,
 | 
			
		||||
 * Build a query that relates two homogeneous tables sharing a common set of columns,
 | 
			
		||||
 * returning rows that exist in both tables (if they have differences), and rows from
 | 
			
		||||
 * `leftTableId` that don't exist in `rightTableId`.
 | 
			
		||||
 *
 | 
			
		||||
 | 
			
		||||
@ -208,7 +208,7 @@ export class ChecksummedExternalStorage implements ExternalStorage {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * We may want to download material from one key and henceforth treat it as another
 | 
			
		||||
   * key (specifically for forking a document).  Since this class crossreferences the
 | 
			
		||||
   * key (specifically for forking a document).  Since this class cross-references the
 | 
			
		||||
   * key in the external store with other consistent stores, it needs to know we are
 | 
			
		||||
   * doing that.  So we add a downloadTo variant that takes before and after keys.
 | 
			
		||||
   */
 | 
			
		||||
 | 
			
		||||
@ -1070,7 +1070,7 @@ export class FlexServer implements GristServer {
 | 
			
		||||
      const scope = addPermit(getScope(mreq), this._dbManager.getSupportUserId(), {org: orgDomain});
 | 
			
		||||
      const query = await this._dbManager.getOrg(scope, orgDomain);
 | 
			
		||||
      const org = this._dbManager.unwrapQueryResult(query);
 | 
			
		||||
      // This page isn't availabe for personal site.
 | 
			
		||||
      // This page isn't available for personal site.
 | 
			
		||||
      if (org.owner) {
 | 
			
		||||
        return this._sendAppPage(req, resp, {path: 'error.html', status: 404, config: {errPage: 'not-found'}});
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
@ -373,7 +373,7 @@ export class GranularAccess implements GranularAccessForBundle {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Check whether an ActionGroup can be sent to the client.  TODO: in future, we'll want
 | 
			
		||||
   * to filter acceptible parts of ActionGroup, rather than denying entirely.
 | 
			
		||||
   * to filter acceptable parts of ActionGroup, rather than denying entirely.
 | 
			
		||||
   */
 | 
			
		||||
  public async allowActionGroup(docSession: OptDocSession, actionGroup: ActionGroup): Promise<boolean> {
 | 
			
		||||
    return this.canReadEverything(docSession);
 | 
			
		||||
 | 
			
		||||
@ -19,7 +19,7 @@ export class HostedMetadataManager {
 | 
			
		||||
  // Callback for next opportunity to push changes.
 | 
			
		||||
  private _timeout: any = null;
 | 
			
		||||
 | 
			
		||||
  // Mantains the update Promise to wait on it if the class is closing.
 | 
			
		||||
  // Maintains the update Promise to wait on it if the class is closing.
 | 
			
		||||
  private _push: Promise<any>|null;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
@ -65,7 +65,7 @@ export class HostedMetadataManager {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Push all metadata updates to the databse.
 | 
			
		||||
   * Push all metadata updates to the database.
 | 
			
		||||
   */
 | 
			
		||||
  private _update(): void {
 | 
			
		||||
    if (this._push) { return; }
 | 
			
		||||
 | 
			
		||||
@ -350,7 +350,7 @@ const spawners = {
 | 
			
		||||
 *   - GRIST_SANDBOX_FLAVOR: should be one of the spawners (pynbox, unsandboxed, docker,
 | 
			
		||||
 *     gvisor, macSandboxExec)
 | 
			
		||||
 *   - GRIST_SANDBOX: a program or image name to run as the sandbox.  Not needed for
 | 
			
		||||
 *     pynbox (it is either built in or not avaiable).  For unsandboxed, should be an
 | 
			
		||||
 *     pynbox (it is either built in or not available).  For unsandboxed, should be an
 | 
			
		||||
 *     absolute path to python within a virtualenv with all requirements installed.
 | 
			
		||||
 *     For docker, it should be `grist-docker-sandbox` (an image built via makefile
 | 
			
		||||
 *     in `sandbox/docker`) or a derived image.  For gvisor, it should be the full path
 | 
			
		||||
@ -743,7 +743,7 @@ function getWrappingEnv(options: ISandboxOptions) {
 | 
			
		||||
 * structure on the host rather than remapping, we can simplify nesting
 | 
			
		||||
 * wrappers, or cases where remapping isn't possible.  It does leak the names
 | 
			
		||||
 * of the host directories though, and there could be silly complications if the
 | 
			
		||||
 * directories have spaces or other idiosyncracies.  When committing to a sandbox
 | 
			
		||||
 * directories have spaces or other idiosyncrasies.  When committing to a sandbox
 | 
			
		||||
 * technology, for stand-alone Grist, it would be worth rethinking this.
 | 
			
		||||
 */
 | 
			
		||||
function getAbsolutePaths(options: ISandboxOptions) {
 | 
			
		||||
 | 
			
		||||
@ -66,7 +66,7 @@ export class PluginManager {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Re-load plugins (litterally re-run `loadPlugins`).
 | 
			
		||||
   * Re-load plugins (literally re-run `loadPlugins`).
 | 
			
		||||
   */
 | 
			
		||||
  // TODO: it's not clear right now what we do on reload. Do we deactivate plugins that were removed
 | 
			
		||||
  // from the fs? Do we update plugins that have changed on the fs ?
 | 
			
		||||
 | 
			
		||||
@ -114,7 +114,7 @@ export type DBFunc = (db: SQLiteDB) => Promise<void>;
 | 
			
		||||
export enum OpenMode {
 | 
			
		||||
  OPEN_CREATE,      // Open DB or create if doesn't exist (the default mode for sqlite3 module)
 | 
			
		||||
  OPEN_EXISTING,    // Open DB or fail if doesn't exist
 | 
			
		||||
  OPEN_READONLY,    // Open DB in read-only mode or fail if doens't exist.
 | 
			
		||||
  OPEN_READONLY,    // Open DB in read-only mode or fail if doesn't exist.
 | 
			
		||||
  CREATE_EXCL,      // Create new DB or fail if it already exists.
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -174,7 +174,7 @@ export class SamlConfig {
 | 
			
		||||
      if (state.action === 'login') {
 | 
			
		||||
        const samlUser = samlResponse.user;
 | 
			
		||||
        if (!samlUser || !samlUser.name_id) {
 | 
			
		||||
          log.warn(`SamlConfig: bad SAML reponse: ${JSON.stringify(samlUser)}`);
 | 
			
		||||
          log.warn(`SamlConfig: bad SAML response: ${JSON.stringify(samlUser)}`);
 | 
			
		||||
          throw new Error("Invalid user info in SAML response");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -183,7 +183,7 @@ export class SamlConfig {
 | 
			
		||||
        // available. Otherwise we use user.attributes which has the form {Name: [Value]}.
 | 
			
		||||
        const fname = samlUser.given_name || samlUser.attributes.FirstName || '';
 | 
			
		||||
        const lname = samlUser.surname || samlUser.attributes.LastName || '';
 | 
			
		||||
        const email = samlUser.email || samlUser.nameId;
 | 
			
		||||
        const email = samlUser.email || samlUser.name_id;
 | 
			
		||||
        const profile = {
 | 
			
		||||
          email,
 | 
			
		||||
          name: `${fname} ${lname}`.trim(),
 | 
			
		||||
 | 
			
		||||
@ -320,7 +320,7 @@ export class Sharing {
 | 
			
		||||
        isModification: sandboxActionBundle.stored.length > 0
 | 
			
		||||
      };
 | 
			
		||||
    } finally {
 | 
			
		||||
      // Make sure the bundle is marked as complete, even if some miscellaneous error occured.
 | 
			
		||||
      // Make sure the bundle is marked as complete, even if some miscellaneous error occurred.
 | 
			
		||||
      await accessControl.finishedBundle();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -74,8 +74,8 @@ exports.createNumbered = createNumbered;
 | 
			
		||||
/**
 | 
			
		||||
 * An easier-to-use alternative to createNumbered. Pass in a template string containing the
 | 
			
		||||
 * special token "{NUM}". It will first call creator() with "{NUM}" removed, then with "{NUM}"
 | 
			
		||||
 * replcaced by "-2", "-3", etc, until creator() succeeds, and will return the value for which it
 | 
			
		||||
 * suceeded.
 | 
			
		||||
 * replaced by "-2", "-3", etc, until creator() succeeds, and will return the value for which it
 | 
			
		||||
 * succeeded.
 | 
			
		||||
 */
 | 
			
		||||
function createNumberedTemplate(template, creator) {
 | 
			
		||||
  const [prefix, suffix] = template.split("{NUM}");
 | 
			
		||||
 | 
			
		||||
@ -141,7 +141,7 @@ class Engine(object):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  def __init__(self):
 | 
			
		||||
    # The document data, incuding logic (formulas), and metadata (tables prefixed with "_grist_").
 | 
			
		||||
    # The document data, including logic (formulas), and metadata (tables prefixed with "_grist_").
 | 
			
		||||
    self.tables = {}                # Maps table IDs (or names) to Table objects.
 | 
			
		||||
 | 
			
		||||
    # Schema contains information about tables and columns, needed in particular to generate the
 | 
			
		||||
 | 
			
		||||
@ -152,7 +152,7 @@ class GenCode(object):
 | 
			
		||||
    return textbuilder.Combiner(parts)
 | 
			
		||||
 | 
			
		||||
  def make_module(self, schema):
 | 
			
		||||
    """Regenerates the code text and usercode module from upated document schema."""
 | 
			
		||||
    """Regenerates the code text and usercode module from updated document schema."""
 | 
			
		||||
    # Collect summary tables to group them by source table.
 | 
			
		||||
    summary_tables = {}
 | 
			
		||||
    for table_info in six.itervalues(schema):
 | 
			
		||||
 | 
			
		||||
@ -328,7 +328,7 @@ def _sliding_triplets(tokens):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _analyze_tokens(tokens):
 | 
			
		||||
  """Analize each token and find out compatible types for it."""
 | 
			
		||||
  """Analyze each token and find out compatible types for it."""
 | 
			
		||||
  for token, prev, nxt in _sliding_triplets(tokens):
 | 
			
		||||
    token.compatible_types = tuple([t for t in DATE_ELEMENTS if t[2](token.val, prev, nxt)])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -97,12 +97,12 @@ for typ in six.string_types:
 | 
			
		||||
 | 
			
		||||
SCHEMA = [{
 | 
			
		||||
  'name': 'includes',
 | 
			
		||||
  'label': 'Includes (list of tables seperated by semicolon)',
 | 
			
		||||
  'label': 'Includes (list of tables separated by semicolon)',
 | 
			
		||||
  'type': 'string',
 | 
			
		||||
  'visible': True
 | 
			
		||||
}, {
 | 
			
		||||
  'name': 'excludes',
 | 
			
		||||
  'label': 'Excludes (list of tables seperated by semicolon)',
 | 
			
		||||
  'label': 'Excludes (list of tables separated by semicolon)',
 | 
			
		||||
  'type': 'string',
 | 
			
		||||
  'visible': True
 | 
			
		||||
}]
 | 
			
		||||
 | 
			
		||||
@ -111,7 +111,7 @@ def migration(schema_version, need_all_tables=False):
 | 
			
		||||
  is marked with need_all_tables=True, then the migration will be retried with all tables.
 | 
			
		||||
 | 
			
		||||
  NOTE: new migrations should NOT set need_all_tables=True; it would require more work to process
 | 
			
		||||
  very large documents safely (incuding those containing on-demand tables).
 | 
			
		||||
  very large documents safely (including those containing on-demand tables).
 | 
			
		||||
  """
 | 
			
		||||
  def add_migration(migration_func):
 | 
			
		||||
    migration_func.need_all_tables = need_all_tables
 | 
			
		||||
 | 
			
		||||
@ -559,7 +559,7 @@ class Table(object):
 | 
			
		||||
    """
 | 
			
		||||
    # We don't set up any dependencies, so it would be incorrect to use this from formulas.
 | 
			
		||||
    # We no longer assert, however, since such calls may still happen e.g. while applying
 | 
			
		||||
    # user-actions caused by formula side-effects (e.g. as trigged by lookupOrAddDerived())
 | 
			
		||||
    # user-actions caused by formula side-effects (e.g. as triggered by lookupOrAddDerived())
 | 
			
		||||
    if row_id not in self.row_ids:
 | 
			
		||||
      raise KeyError("'get_record' found no matching record")
 | 
			
		||||
    return self.Record(row_id, None)
 | 
			
		||||
 | 
			
		||||
@ -3,7 +3,7 @@ import test_engine
 | 
			
		||||
 | 
			
		||||
class TestFindCol(test_engine.EngineTestCase):
 | 
			
		||||
  def test_find_col_from_values(self):
 | 
			
		||||
    # Test basic funtionality.
 | 
			
		||||
    # Test basic functionality.
 | 
			
		||||
    self.load_sample(testsamples.sample_students)
 | 
			
		||||
    self.assertEqual(self.engine.find_col_from_values(("Columbia", "Yale", "Eureka"), 0),
 | 
			
		||||
        [4, 10])
 | 
			
		||||
 | 
			
		||||
@ -68,7 +68,7 @@ class TestRenames2(test_engine.EngineTestCase):
 | 
			
		||||
      [ 4,      "CheckersA" , 5,          1         ],
 | 
			
		||||
    ])
 | 
			
		||||
 | 
			
		||||
    # This was just setpu. Now create some crazy formulas that overuse referenes in crazy ways.
 | 
			
		||||
    # This was just setpu. Now create some crazy formulas that overuse references in crazy ways.
 | 
			
		||||
    self.partner_names = textwrap.dedent(
 | 
			
		||||
      """
 | 
			
		||||
      games = Entries.lookupRecords(person=$id).game
 | 
			
		||||
 | 
			
		||||
@ -1047,7 +1047,7 @@ class UserActions(object):
 | 
			
		||||
  @override_action('BulkRemoveRecord', '_grist_Pages')
 | 
			
		||||
  def _removePageRecords(self, table_id, row_ids):
 | 
			
		||||
    """
 | 
			
		||||
    Remove page records and for the those that have children, udpate the first child's indentation
 | 
			
		||||
    Remove page records and for the those that have children, update the first child's indentation
 | 
			
		||||
    so that it becomes the new parent. Note that this run a O(n) routine for each page to remove but
 | 
			
		||||
    it's ok considering that the list of _grist_Pages is not meant to grow that big.
 | 
			
		||||
    """
 | 
			
		||||
@ -1686,7 +1686,7 @@ class UserActions(object):
 | 
			
		||||
  # User actions on viewSections.
 | 
			
		||||
  #----------------------------------------
 | 
			
		||||
 | 
			
		||||
  # TODO: Deprecated; This should no longer be an exposed action; it is superceded by
 | 
			
		||||
  # TODO: Deprecated; This should no longer be an exposed action; it is superseded by
 | 
			
		||||
  # CreateViewSection.
 | 
			
		||||
  @useraction
 | 
			
		||||
  def AddViewSection(self, title, view_section_type, view_row_id, table_id):
 | 
			
		||||
 | 
			
		||||
@ -7,7 +7,7 @@
 | 
			
		||||
 | 
			
		||||
// This determines when a failed assertion shows a diff with details or
 | 
			
		||||
// "expected [ Array(3) ] to deeply equal [ Array(3) ]".
 | 
			
		||||
// Increase the threshhold since the default (of 40 characters) is often too low.
 | 
			
		||||
// Increase the threshold since the default (of 40 characters) is often too low.
 | 
			
		||||
// You can override it using CHAI_TRUNCATE_THRESHOLD env var; 0 disables it.
 | 
			
		||||
require('chai').config.truncateThreshold = process.env.CHAI_TRUNCATE_THRESHOLD ?
 | 
			
		||||
  parseFloat(process.env.CHAI_TRUNCATE_THRESHOLD) : 200;
 | 
			
		||||
 | 
			
		||||
@ -665,7 +665,7 @@ export async function importUrlDialog(url: string): Promise<void> {
 | 
			
		||||
/**
 | 
			
		||||
 * Starts or resets the collections of UserActions. This should be followed some time later by
 | 
			
		||||
 * a call to userActionsVerify() to check which UserActions were sent to the server. If the
 | 
			
		||||
 * argumet is false, then stops the collection.
 | 
			
		||||
 * argument is false, then stops the collection.
 | 
			
		||||
 */
 | 
			
		||||
export function userActionsCollect(yesNo: boolean = true) {
 | 
			
		||||
  return driver.executeScript("window.gristApp.comm.userActionsCollect(arguments[0])", yesNo);
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user