From af25eaf2a848278a58f0993cba2e68c05da0760c Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Tue, 29 Oct 2019 20:00:54 -0400
Subject: comments and fixes for google photos server sid

---
 src/client/util/Import & Export/DirectoryImportBox.tsx | 5 ++---
 1 file changed, 2 insertions(+), 3 deletions(-)

(limited to 'src/client/util/Import & Export/DirectoryImportBox.tsx')

diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx
index d74b51993..2d1b6fe20 100644
--- a/src/client/util/Import & Export/DirectoryImportBox.tsx	
+++ b/src/client/util/Import & Export/DirectoryImportBox.tsx	
@@ -20,9 +20,8 @@ import { listSpec } from "../../../new_fields/Schema";
 import { GooglePhotos } from "../../apis/google_docs/GooglePhotosClientUtils";
 import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField";
 import "./DirectoryImportBox.scss";
-import { Identified } from "../../Network";
+import { Networking } from "../../Network";
 import { BatchedArray } from "array-batcher";
-import { ExifData } from "exif";
 
 const unsupported = ["text/html", "text/plain"];
 
@@ -117,7 +116,7 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
                 formData.append(Utils.GenerateGuid(), file);
             });
 
-            const responses = await Identified.PostFormDataToServer(RouteStore.upload, formData);
+            const responses = await Networking.PostFormDataToServer(RouteStore.upload, formData);
             runInAction(() => this.completed += batch.length);
             return responses as ImageUploadResponse[];
         });
-- 
cgit v1.2.3-70-g09d2


From 9c7e619fb9d3116649ec3779bd528b947235d5a4 Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Wed, 30 Oct 2019 15:51:29 -0400
Subject: updated array batcher

---
 package.json                                       |  2 +-
 .../util/Import & Export/DirectoryImportBox.tsx    |  5 +--
 src/server/RouteManager.ts                         |  1 +
 src/server/apis/google/GooglePhotosUploadUtils.ts  | 11 ++---
 src/server/index.ts                                | 48 ++++++++++++----------
 5 files changed, 36 insertions(+), 31 deletions(-)

(limited to 'src/client/util/Import & Export/DirectoryImportBox.tsx')

diff --git a/package.json b/package.json
index 8cbbb84af..4572a3f73 100644
--- a/package.json
+++ b/package.json
@@ -115,7 +115,7 @@
         "@types/youtube": "0.0.38",
         "adm-zip": "^0.4.13",
         "archiver": "^3.0.3",
-        "array-batcher": "^1.1.3",
+        "array-batcher": "^1.2.3",
         "async": "^2.6.2",
         "babel-runtime": "^6.26.0",
         "bcrypt-nodejs": "0.0.3",
diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx
index 2d1b6fe20..bdd59cb16 100644
--- a/src/client/util/Import & Export/DirectoryImportBox.tsx	
+++ b/src/client/util/Import & Export/DirectoryImportBox.tsx	
@@ -107,7 +107,7 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
 
         runInAction(() => this.phase = `Internal: uploading ${this.quota - this.completed} files to Dash...`);
 
-        const uploads = await BatchedArray.from(validated, { batchSize: 15 }).batchedMapAsync(async batch => {
+        const uploads = await BatchedArray.from(validated, { batchSize: 15 }).batchedMapAsync<ImageUploadResponse>(async (batch, collector) => {
             const formData = new FormData();
 
             batch.forEach(file => {
@@ -116,9 +116,8 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
                 formData.append(Utils.GenerateGuid(), file);
             });
 
-            const responses = await Networking.PostFormDataToServer(RouteStore.upload, formData);
+            collector.push(...(await Networking.PostFormDataToServer(RouteStore.upload, formData)));
             runInAction(() => this.completed += batch.length);
-            return responses as ImageUploadResponse[];
         });
 
         await Promise.all(uploads.map(async upload => {
diff --git a/src/server/RouteManager.ts b/src/server/RouteManager.ts
index eda2a49d2..c1d38327f 100644
--- a/src/server/RouteManager.ts
+++ b/src/server/RouteManager.ts
@@ -114,6 +114,7 @@ export const STATUS = {
 };
 
 export function _error(res: express.Response, message: string, error?: any) {
+    console.error(message);
     res.statusMessage = message;
     res.status(STATUS.EXECUTION_ERROR).send(error);
 }
diff --git a/src/server/apis/google/GooglePhotosUploadUtils.ts b/src/server/apis/google/GooglePhotosUploadUtils.ts
index 172fa8d46..d3442338b 100644
--- a/src/server/apis/google/GooglePhotosUploadUtils.ts
+++ b/src/server/apis/google/GooglePhotosUploadUtils.ts
@@ -1,7 +1,7 @@
 import request = require('request-promise');
 import { GoogleApiServerUtils } from './GoogleApiServerUtils';
 import * as path from 'path';
-import { MediaItemCreationResult } from './SharedTypes';
+import { MediaItemCreationResult, NewMediaItemResult } from './SharedTypes';
 import { NewMediaItem } from "../../index";
 import { BatchedArray, TimeUnit } from 'array-batcher';
 import { DashUploadUtils } from '../../DashUploadUtils';
@@ -50,9 +50,9 @@ export namespace GooglePhotosUploadUtils {
     };
 
     export const CreateMediaItems = async (bearerToken: string, newMediaItems: NewMediaItem[], album?: { id: string }): Promise<MediaItemCreationResult> => {
-        const newMediaItemResults = await BatchedArray.from(newMediaItems, { batchSize: 50 }).batchedMapPatientInterval(
+        const newMediaItemResults = await BatchedArray.from(newMediaItems, { batchSize: 50 }).batchedMapPatientInterval<NewMediaItemResult>(
             { magnitude: 100, unit: TimeUnit.Milliseconds },
-            async (batch: NewMediaItem[]) => {
+            async (batch: NewMediaItem[], collector) => {
                 const parameters = {
                     method: 'POST',
                     headers: headers('json', bearerToken),
@@ -61,7 +61,7 @@ export namespace GooglePhotosUploadUtils {
                     json: true
                 };
                 album && (parameters.body.albumId = album.id);
-                return (await new Promise<MediaItemCreationResult>((resolve, reject) => {
+                const { newMediaItemResults } = await new Promise<MediaItemCreationResult>((resolve, reject) => {
                     request(parameters, (error, _response, body) => {
                         if (error) {
                             reject(error);
@@ -69,7 +69,8 @@ export namespace GooglePhotosUploadUtils {
                             resolve(body);
                         }
                     });
-                })).newMediaItemResults;
+                });
+                collector.push(...newMediaItemResults);
             }
         );
         return { newMediaItemResults };
diff --git a/src/server/index.ts b/src/server/index.ts
index 860cde3b5..05c866eae 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -573,18 +573,15 @@ function routeSetter(router: RouteManager) {
         onValidation: async ({ req, res, user }) => {
             let sector: GoogleApiServerUtils.Service = req.params.sector as GoogleApiServerUtils.Service;
             let action: GoogleApiServerUtils.Action = req.params.action as GoogleApiServerUtils.Action;
-            return GoogleApiServerUtils.GetEndpoint(GoogleApiServerUtils.Service[sector], user.id).then(endpoint => {
-                let handler = EndpointHandlerMap.get(action);
-                if (endpoint && handler) {
-                    let execute = handler(endpoint, req.body).then(
-                        response => res.send(response.data),
-                        rejection => res.send(rejection)
-                    );
-                    execute.catch(exception => res.send(exception));
-                    return;
-                }
-                res.send(undefined);
-            });
+            const endpoint = await GoogleApiServerUtils.GetEndpoint(GoogleApiServerUtils.Service[sector], user.id);
+            let handler = EndpointHandlerMap.get(action);
+            if (endpoint && handler) {
+                handler(endpoint, req.body)
+                    .then(response => res.send(response.data))
+                    .catch(exception => res.send(exception));
+                return;
+            }
+            res.send(undefined);
         }
     });
 
@@ -611,6 +608,12 @@ function routeSetter(router: RouteManager) {
 
     const authenticationError = "Unable to authenticate Google credentials before uploading to Google Photos!";
     const mediaError = "Unable to convert all uploaded bytes to media items!";
+    interface GooglePhotosUploadFailure {
+        batch: number;
+        index: number;
+        url: string;
+        reason: string;
+    }
 
     router.addSupervisedRoute({
         method: Method.POST,
@@ -623,30 +626,31 @@ function routeSetter(router: RouteManager) {
                 return _error(res, authenticationError);
             }
 
-            let failed: number[] = [];
-            const newMediaItems = await BatchedArray.from<GooglePhotosUploadUtils.MediaInput>(media, { batchSize: 25 }).batchedMapPatientInterval(
+            let failed: GooglePhotosUploadFailure[] = [];
+            const batched = BatchedArray.from<GooglePhotosUploadUtils.MediaInput>(media, { batchSize: 25 });
+            const newMediaItems = await batched.batchedMapPatientInterval<NewMediaItem>(
                 { magnitude: 100, unit: TimeUnit.Milliseconds },
-                async (batch: GooglePhotosUploadUtils.MediaInput[]) => {
-                    const newMediaItems: NewMediaItem[] = [];
+                async (batch, collector, { completedBatches }) => {
                     for (let index = 0; index < batch.length; index++) {
-                        const element = batch[index];
-                        const uploadToken = await GooglePhotosUploadUtils.DispatchGooglePhotosUpload(token, element.url);
+                        const { url, description } = batch[index];
+                        const fail = (reason: string) => failed.push({ reason, batch: completedBatches + 1, index, url });
+                        const uploadToken = await GooglePhotosUploadUtils.DispatchGooglePhotosUpload(token, url).catch(fail);
                         if (!uploadToken) {
-                            failed.push(index);
+                            fail(`${path.extname(url)} is not an accepted extension`);
                         } else {
-                            newMediaItems.push({
-                                description: element.description,
+                            collector.push({
+                                description,
                                 simpleMediaItem: { uploadToken }
                             });
                         }
                     }
-                    return newMediaItems;
                 }
             );
 
             const failedCount = failed.length;
             if (failedCount) {
                 console.error(`Unable to upload ${failedCount} image${failedCount === 1 ? "" : "s"} to Google's servers`);
+                console.log(failed.map(({ reason, batch, index, url }) => `@${batch}.${index}: ${url} failed: ${reason}`).join('\n'));
             }
 
             return GooglePhotosUploadUtils.CreateMediaItems(token, newMediaItems, req.body.album).then(
-- 
cgit v1.2.3-70-g09d2


From 0b72a27ead9d1e933ae349b8a3e9e9b8702664d1 Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Sat, 9 Nov 2019 16:18:23 -0500
Subject: factored out all but google resources into managers

---
 client_secret.json                                 |   1 -
 .../util/Import & Export/DirectoryImportBox.tsx    |  10 +-
 src/server/ApiManagers/DeleteManager.ts            |  65 +++
 src/server/ApiManagers/ExportManager.ts            | 157 +++++-
 src/server/ApiManagers/PDFManager.ts               | 107 ++++
 src/server/ApiManagers/SearchManager.ts            |   4 +-
 src/server/ApiManagers/UploadManager.ts            | 227 ++++++++
 src/server/ApiManagers/UserManager.ts              |  47 +-
 src/server/DashUploadUtils.ts                      |  17 +-
 src/server/SharedMediaTypes.ts                     |   9 +
 src/server/Websocket/Websocket.ts                  |  19 +-
 src/server/apis/google/GoogleApiServerUtils.ts     |  46 +-
 src/server/apis/google/GooglePhotosUploadUtils.ts  |  19 +-
 src/server/credentials/CredentialsLoader.ts        |  29 ++
 .../credentials/google_docs_credentials.json       |  11 -
 .../credentials/google_project_credentials.json    |  14 +
 src/server/credentials/test.json                   |  14 +
 src/server/index.ts                                | 570 ++-------------------
 18 files changed, 770 insertions(+), 596 deletions(-)
 delete mode 100644 client_secret.json
 create mode 100644 src/server/ApiManagers/DeleteManager.ts
 create mode 100644 src/server/ApiManagers/PDFManager.ts
 create mode 100644 src/server/ApiManagers/UploadManager.ts
 create mode 100644 src/server/SharedMediaTypes.ts
 create mode 100644 src/server/credentials/CredentialsLoader.ts
 delete mode 100644 src/server/credentials/google_docs_credentials.json
 create mode 100644 src/server/credentials/google_project_credentials.json
 create mode 100644 src/server/credentials/test.json

(limited to 'src/client/util/Import & Export/DirectoryImportBox.tsx')

diff --git a/client_secret.json b/client_secret.json
deleted file mode 100644
index a9c698421..000000000
--- a/client_secret.json
+++ /dev/null
@@ -1 +0,0 @@
-{"installed":{"client_id":"1005546247619-kqpnvh42mpa803tem8556b87umi4j9r0.apps.googleusercontent.com","project_id":"brown-dash","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","client_secret":"WshLb5TH9SdFVGGbQcnYj7IU","redirect_uris":["urn:ietf:wg:oauth:2.0:oob","http://localhost"]}}
\ No newline at end of file
diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx
index bdd59cb16..2e0ba25eb 100644
--- a/src/client/util/Import & Export/DirectoryImportBox.tsx	
+++ b/src/client/util/Import & Export/DirectoryImportBox.tsx	
@@ -22,6 +22,9 @@ import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField";
 import "./DirectoryImportBox.scss";
 import { Networking } from "../../Network";
 import { BatchedArray } from "array-batcher";
+import * as path from 'path';
+import { DashUploadUtils } from "../../../server/DashUploadUtils";
+import { SharedMediaTypes } from "../../../server/SharedMediaTypes";
 
 const unsupported = ["text/html", "text/plain"];
 
@@ -94,7 +97,12 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
         let validated: File[] = [];
         for (let i = 0; i < files.length; i++) {
             let file = files.item(i);
-            file && !unsupported.includes(file.type) && validated.push(file);
+            if (file && !unsupported.includes(file.type)) {
+                const ext = path.extname(file.name).toLowerCase();
+                if (SharedMediaTypes.imageFormats.includes(ext)) {
+                    validated.push(file);
+                }
+            }
         }
 
         runInAction(() => {
diff --git a/src/server/ApiManagers/DeleteManager.ts b/src/server/ApiManagers/DeleteManager.ts
new file mode 100644
index 000000000..bbf1d0425
--- /dev/null
+++ b/src/server/ApiManagers/DeleteManager.ts
@@ -0,0 +1,65 @@
+import ApiManager, { Registration } from "./ApiManager";
+import { Method, _permission_denied } from "../RouteManager";
+import { RouteStore } from "../RouteStore";
+import { WebSocket } from "../Websocket/Websocket";
+import { Database } from "../database";
+
+export default class DeleteManager extends ApiManager {
+
+    protected initialize(register: Registration): void {
+
+        register({
+            method: Method.GET,
+            subscription: RouteStore.delete,
+            onValidation: async ({ res, isRelease }) => {
+                if (isRelease) {
+                    return _permission_denied(res, deletionPermissionError);
+                }
+                await WebSocket.deleteFields();
+                res.redirect(RouteStore.home);
+            }
+        });
+
+        register({
+            method: Method.GET,
+            subscription: RouteStore.deleteAll,
+            onValidation: async ({ res, isRelease }) => {
+                if (isRelease) {
+                    return _permission_denied(res, deletionPermissionError);
+                }
+                await WebSocket.deleteAll();
+                res.redirect(RouteStore.home);
+            }
+        });
+
+
+        register({
+            method: Method.GET,
+            subscription: "/deleteWithAux",
+            onValidation: async ({ res, isRelease }) => {
+                if (isRelease) {
+                    return _permission_denied(res, deletionPermissionError);
+                }
+                await Database.Auxiliary.DeleteAll();
+                res.redirect(RouteStore.delete);
+            }
+        });
+
+        register({
+            method: Method.GET,
+            subscription: "/deleteWithGoogleCredentials",
+            onValidation: async ({ res, isRelease }) => {
+                if (isRelease) {
+                    return _permission_denied(res, deletionPermissionError);
+                }
+                await Database.Auxiliary.GoogleAuthenticationToken.DeleteAll();
+                res.redirect(RouteStore.delete);
+            }
+        });
+
+
+    }
+
+}
+
+const deletionPermissionError = "Cannot perform a delete operation outside of the development environment!";
diff --git a/src/server/ApiManagers/ExportManager.ts b/src/server/ApiManagers/ExportManager.ts
index 14ac7dd5b..d42db1056 100644
--- a/src/server/ApiManagers/ExportManager.ts
+++ b/src/server/ApiManagers/ExportManager.ts
@@ -1,5 +1,5 @@
 import ApiManager, { Registration } from "./ApiManager";
-import RouteManager, { Method } from "../RouteManager";
+import { Method } from "../RouteManager";
 import RouteSubscriber from "../RouteSubscriber";
 import { RouteStore } from "../RouteStore";
 import * as Archiver from 'archiver';
@@ -7,6 +7,7 @@ import * as express from 'express';
 import { Database } from "../database";
 import * as path from "path";
 import { DashUploadUtils } from "../DashUploadUtils";
+import { publicDirectory } from "..";
 
 export type Hierarchy = { [id: string]: string | Hierarchy };
 export type ZipMutator = (file: Archiver.Archiver) => void | Promise<void>;
@@ -15,10 +16,20 @@ export interface DocumentElements {
     title: string;
 }
 
-export default class ExportManager extends ApiManager {
+export default class DownloadManager extends ApiManager {
 
     protected initialize(register: Registration): void {
 
+        /**
+         * Let's say someone's using Dash to organize images in collections.
+         * This lets them export the hierarchy they've built to their
+         * own file system in a useful format.
+         * 
+         * This handler starts with a single document id (interesting only
+         * if it's that of a collection). It traverses the database, captures
+         * the nesting of only nested images or collections, writes
+         * that to a zip file and returns it to the client for download.
+         */
         register({
             method: Method.GET,
             subscription: new RouteSubscriber(RouteStore.imageHierarchyExport).add('docId'),
@@ -29,10 +40,101 @@ export default class ExportManager extends ApiManager {
                 return BuildAndDispatchZip(res, zip => writeHierarchyRecursive(zip, hierarchy));
             }
         });
+
+        register({
+            method: Method.GET,
+            subscription: new RouteSubscriber("/downloadId").add("docId"),
+            onValidation: async ({ req, res }) => {
+                return BuildAndDispatchZip(res, async zip => {
+                    const { id, docs, files } = await getDocs(req.params.docId);
+                    const docString = JSON.stringify({ id, docs });
+                    zip.append(docString, { name: "doc.json" });
+                    files.forEach(val => {
+                        zip.file(publicDirectory + val, { name: val.substring(1) });
+                    });
+                });
+            }
+        });
+
+        register({
+            method: Method.GET,
+            subscription: new RouteSubscriber("/serializeDoc").add("docId"),
+            onValidation: async ({ req, res }) => {
+                const { docs, files } = await getDocs(req.params.docId);
+                res.send({ docs, files: Array.from(files) });
+            }
+        });
+
+
     }
 
 }
 
+async function getDocs(id: string) {
+    const files = new Set<string>();
+    const docs: { [id: string]: any } = {};
+    const fn = (doc: any): string[] => {
+        const id = doc.id;
+        if (typeof id === "string" && id.endsWith("Proto")) {
+            //Skip protos
+            return [];
+        }
+        const ids: string[] = [];
+        for (const key in doc.fields) {
+            if (!doc.fields.hasOwnProperty(key)) {
+                continue;
+            }
+            const field = doc.fields[key];
+            if (field === undefined || field === null) {
+                continue;
+            }
+
+            if (field.__type === "proxy" || field.__type === "prefetch_proxy") {
+                ids.push(field.fieldId);
+            } else if (field.__type === "script" || field.__type === "computed") {
+                if (field.captures) {
+                    ids.push(field.captures.fieldId);
+                }
+            } else if (field.__type === "list") {
+                ids.push(...fn(field));
+            } else if (typeof field === "string") {
+                const re = /"(?:dataD|d)ocumentId"\s*:\s*"([\w\-]*)"/g;
+                let match: string[] | null;
+                while ((match = re.exec(field)) !== null) {
+                    ids.push(match[1]);
+                }
+            } else if (field.__type === "RichTextField") {
+                const re = /"href"\s*:\s*"(.*?)"/g;
+                let match: string[] | null;
+                while ((match = re.exec(field.Data)) !== null) {
+                    const urlString = match[1];
+                    const split = new URL(urlString).pathname.split("doc/");
+                    if (split.length > 1) {
+                        ids.push(split[split.length - 1]);
+                    }
+                }
+                const re2 = /"src"\s*:\s*"(.*?)"/g;
+                while ((match = re2.exec(field.Data)) !== null) {
+                    const urlString = match[1];
+                    const pathname = new URL(urlString).pathname;
+                    files.add(pathname);
+                }
+            } else if (["audio", "image", "video", "pdf", "web"].includes(field.__type)) {
+                const url = new URL(field.url);
+                const pathname = url.pathname;
+                files.add(pathname);
+            }
+        }
+
+        if (doc.id) {
+            docs[doc.id] = doc;
+        }
+        return ids;
+    };
+    await Database.Instance.visit([id], fn);
+    return { id, docs, files };
+}
+
 /**
  * This utility function factors out the process
  * of creating a zip file and sending it back to the client
@@ -45,6 +147,8 @@ export default class ExportManager extends ApiManager {
  * @param mutator the callback function used to actually modify and insert information into the zip instance
  */
 export async function BuildAndDispatchZip(res: express.Response, mutator: ZipMutator): Promise<void> {
+    res.set('Content-disposition', `attachment;`);
+    res.set('Content-Type', "application/zip");
     const zip = Archiver('zip');
     zip.pipe(res);
     await mutator(zip);
@@ -76,7 +180,6 @@ following the general recursive structure shown immediately below
     }
 }
 */
-
 async function buildHierarchyRecursive(seedId: string, hierarchy: Hierarchy): Promise<void> {
     const { title, data } = await getData(seedId);
     const label = `${title} (${seedId})`;
@@ -93,9 +196,20 @@ async function buildHierarchyRecursive(seedId: string, hierarchy: Hierarchy): Pr
     }
 }
 
-async function getData(seedId: string): Promise<DocumentElements> {
+/**
+ * This is a very specific utility method to help traverse the database
+ * to parse data and titles out of images and collections alone.
+ * 
+ * We don't know if the document id given to is corresponds to a view document or a data
+ * document. If it's a data document, the response from the database will have
+ * a data field. If not, call recursively on the proto, and resolve with *its* data
+ * 
+ * @param targetId the id of the Dash document whose data is being requests
+ * @returns the data of the document, as well as its title
+ */
+async function getData(targetId: string): Promise<DocumentElements> {
     return new Promise<DocumentElements>((resolve, reject) => {
-        Database.Instance.getDocument(seedId, async (result: any) => {
+        Database.Instance.getDocument(targetId, async (result: any) => {
             const { data, proto, title } = result.fields;
             if (data) {
                 if (data.url) {
@@ -105,29 +219,50 @@ async function getData(seedId: string): Promise<DocumentElements> {
                 } else {
                     reject();
                 }
-            }
-            if (proto) {
+            } else if (proto) {
                 getData(proto.fieldId).then(resolve, reject);
+            } else {
+                reject();
             }
         });
     });
 }
 
+/**
+ * 
+ * @param file the zip file to which we write the files
+ * @param hierarchy the data structure from which we read, defining the nesting of the documents in the zip
+ * @param prefix lets us create nested folders in the zip file by continually appending to the end
+ * of the prefix with each layer of recursion.
+ * 
+ * Function Call #1 => "Dash Export"
+ * Function Call #2 => "Dash Export/a nested collection"
+ * Function Call #3 => "Dash Export/a nested collection/lowest level collection"
+ * ...
+ */
 async function writeHierarchyRecursive(file: Archiver.Archiver, hierarchy: Hierarchy, prefix = "Dash Export"): Promise<void> {
-    for (const key of Object.keys(hierarchy)) {
-        const result = hierarchy[key];
+    for (const documentTitle of Object.keys(hierarchy)) {
+        const result = hierarchy[documentTitle];
+        // base case or leaf node, we've hit a url (image)
         if (typeof result === "string") {
             let path: string;
             let matches: RegExpExecArray | null;
             if ((matches = /\:1050\/files\/(upload\_[\da-z]{32}.*)/g.exec(result)) !== null) {
+                // image already exists on our server
                 path = `${__dirname}/public/files/${matches[1]}`;
             } else {
+                // the image doesn't already exist on our server (may have been dragged
+                // and dropped in the browser and thus hosted remotely) so we upload it
+                // to our server and point the zip file to it, so it can bundle up the bytes
                 const information = await DashUploadUtils.UploadImage(result);
                 path = information.mediaPaths[0];
             }
-            file.file(path, { name: key, prefix });
+            // write the file specified by the path to the directory in the
+            // zip file given by the prefix.
+            file.file(path, { name: documentTitle, prefix });
         } else {
-            await writeHierarchyRecursive(file, result, `${prefix}/${key}`);
+            // we've hit a collection, so we have to recurse
+            await writeHierarchyRecursive(file, result, `${prefix}/${documentTitle}`);
         }
     }
 }
\ No newline at end of file
diff --git a/src/server/ApiManagers/PDFManager.ts b/src/server/ApiManagers/PDFManager.ts
new file mode 100644
index 000000000..f328557b4
--- /dev/null
+++ b/src/server/ApiManagers/PDFManager.ts
@@ -0,0 +1,107 @@
+import ApiManager, { Registration } from "./ApiManager";
+import { Method } from "../RouteManager";
+import RouteSubscriber from "../RouteSubscriber";
+import { exists, createReadStream, createWriteStream } from "fs";
+import { filesDirectory } from "..";
+import * as Pdfjs from 'pdfjs-dist';
+import { createCanvas } from "canvas";
+const probe = require("probe-image-size");
+import * as express from "express";
+import * as path from "path";
+
+export default class PDFManager extends ApiManager {
+
+    protected initialize(register: Registration): void {
+
+        register({
+            method: Method.GET,
+            subscription: new RouteSubscriber("/thumbnail").add("filename"),
+            onValidation: ({ req, res }) => {
+                let filename = req.params.filename;
+                let noExt = filename.substring(0, filename.length - ".png".length);
+                let pagenumber = parseInt(noExt.split('-')[1]);
+                return new Promise<void>(resolve => {
+                    exists(filesDirectory + filename, (exists: boolean) => {
+                        console.log(`${filesDirectory + filename} ${exists ? "exists" : "does not exist"}`);
+                        if (exists) {
+                            let input = createReadStream(filesDirectory + filename);
+                            probe(input, (err: any, result: any) => {
+                                if (err) {
+                                    console.log(err);
+                                    console.log(`error on ${filename}`);
+                                    return;
+                                }
+                                res.send({ path: "/files/" + filename, width: result.width, height: result.height });
+                            });
+                        }
+                        else {
+                            LoadPage(filesDirectory + filename.substring(0, filename.length - noExt.split('-')[1].length - ".PNG".length - 1) + ".pdf", pagenumber, res);
+                        }
+                        resolve();
+                    });
+                });
+            }
+        });
+
+        function LoadPage(file: string, pageNumber: number, res: express.Response) {
+            console.log(file);
+            Pdfjs.getDocument(file).promise
+                .then((pdf: Pdfjs.PDFDocumentProxy) => {
+                    let factory = new NodeCanvasFactory();
+                    console.log(pageNumber);
+                    pdf.getPage(pageNumber).then((page: Pdfjs.PDFPageProxy) => {
+                        console.log("reading " + page);
+                        let viewport = page.getViewport(1 as any);
+                        let canvasAndContext = factory.create(viewport.width, viewport.height);
+                        let renderContext = {
+                            canvasContext: canvasAndContext.context,
+                            viewport: viewport,
+                            canvasFactory: factory
+                        };
+                        console.log("read " + pageNumber);
+
+                        page.render(renderContext).promise
+                            .then(() => {
+                                console.log("saving " + pageNumber);
+                                let stream = canvasAndContext.canvas.createPNGStream();
+                                let pngFile = `${file.substring(0, file.length - ".pdf".length)}-${pageNumber}.PNG`;
+                                let out = createWriteStream(pngFile);
+                                stream.pipe(out);
+                                out.on("finish", () => {
+                                    console.log(`Success! Saved to ${pngFile}`);
+                                    let name = path.basename(pngFile);
+                                    res.send({ path: "/files/" + name, width: viewport.width, height: viewport.height });
+                                });
+                            }, (reason: string) => {
+                                console.error(reason + ` ${pageNumber}`);
+                            });
+                    });
+                });
+        }
+
+    }
+
+}
+
+class NodeCanvasFactory {
+    create = (width: number, height: number) => {
+        var canvas = createCanvas(width, height);
+        var context = canvas.getContext('2d');
+        return {
+            canvas,
+            context
+        };
+    }
+
+    reset = (canvasAndContext: any, width: number, height: number) => {
+        canvasAndContext.canvas.width = width;
+        canvasAndContext.canvas.height = height;
+    }
+
+    destroy = (canvasAndContext: any) => {
+        canvasAndContext.canvas.width = 0;
+        canvasAndContext.canvas.height = 0;
+        canvasAndContext.canvas = null;
+        canvasAndContext.context = null;
+    }
+}
\ No newline at end of file
diff --git a/src/server/ApiManagers/SearchManager.ts b/src/server/ApiManagers/SearchManager.ts
index 1c4b805e5..1c801715a 100644
--- a/src/server/ApiManagers/SearchManager.ts
+++ b/src/server/ApiManagers/SearchManager.ts
@@ -3,7 +3,7 @@ import { Method } from "../RouteManager";
 import { Search } from "../Search";
 var findInFiles = require('find-in-files');
 import * as path from 'path';
-import { uploadDirectory } from "..";
+import { filesDirectory } from "..";
 
 export default class SearchManager extends ApiManager {
 
@@ -18,7 +18,7 @@ export default class SearchManager extends ApiManager {
                     res.send([]);
                     return;
                 }
-                let results = await findInFiles.find({ 'term': q, 'flags': 'ig' }, uploadDirectory + "text", ".txt$");
+                let results = await findInFiles.find({ 'term': q, 'flags': 'ig' }, filesDirectory + "text", ".txt$");
                 let resObj: { ids: string[], numFound: number, lines: string[] } = { ids: [], numFound: 0, lines: [] };
                 for (var result in results) {
                     resObj.ids.push(path.basename(result, ".txt").replace(/upload_/, ""));
diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts
new file mode 100644
index 000000000..38635eda5
--- /dev/null
+++ b/src/server/ApiManagers/UploadManager.ts
@@ -0,0 +1,227 @@
+import ApiManager, { Registration } from "./ApiManager";
+import { Method, _success } from "../RouteManager";
+import * as formidable from 'formidable';
+import v4 = require('uuid/v4');
+var AdmZip = require('adm-zip');
+import * as path from 'path';
+import { createReadStream, createWriteStream, unlink, readFileSync } from "fs";
+import { publicDirectory, filesDirectory, Partitions } from "..";
+import { RouteStore } from "../RouteStore";
+import { Database } from "../database";
+import { DashUploadUtils } from "../DashUploadUtils";
+import { Opt } from "../../new_fields/Doc";
+import { ParsedPDF } from "../PdfTypes";
+const pdf = require('pdf-parse');
+import * as sharp from 'sharp';
+import { SharedMediaTypes } from "../SharedMediaTypes";
+const imageDataUri = require('image-data-uri');
+
+export default class UploadManager extends ApiManager {
+
+    protected initialize(register: Registration): void {
+
+        register({
+            method: Method.POST,
+            subscription: "/uploadDoc",
+            onValidation: ({ req, res }) => {
+                let form = new formidable.IncomingForm();
+                form.keepExtensions = true;
+                // let path = req.body.path;
+                const ids: { [id: string]: string } = {};
+                let remap = true;
+                const getId = (id: string): string => {
+                    if (!remap) return id;
+                    if (id.endsWith("Proto")) return id;
+                    if (id in ids) {
+                        return ids[id];
+                    } else {
+                        return ids[id] = v4();
+                    }
+                };
+                const mapFn = (doc: any) => {
+                    if (doc.id) {
+                        doc.id = getId(doc.id);
+                    }
+                    for (const key in doc.fields) {
+                        if (!doc.fields.hasOwnProperty(key)) {
+                            continue;
+                        }
+                        const field = doc.fields[key];
+                        if (field === undefined || field === null) {
+                            continue;
+                        }
+
+                        if (field.__type === "proxy" || field.__type === "prefetch_proxy") {
+                            field.fieldId = getId(field.fieldId);
+                        } else if (field.__type === "script" || field.__type === "computed") {
+                            if (field.captures) {
+                                field.captures.fieldId = getId(field.captures.fieldId);
+                            }
+                        } else if (field.__type === "list") {
+                            mapFn(field);
+                        } else if (typeof field === "string") {
+                            const re = /("(?:dataD|d)ocumentId"\s*:\s*")([\w\-]*)"/g;
+                            doc.fields[key] = (field as any).replace(re, (match: any, p1: string, p2: string) => {
+                                return `${p1}${getId(p2)}"`;
+                            });
+                        } else if (field.__type === "RichTextField") {
+                            const re = /("href"\s*:\s*")(.*?)"/g;
+                            field.Data = field.Data.replace(re, (match: any, p1: string, p2: string) => {
+                                return `${p1}${getId(p2)}"`;
+                            });
+                        }
+                    }
+                };
+                return new Promise<void>(resolve => {
+                    form.parse(req, async (_err, fields, files) => {
+                        remap = fields.remap !== "false";
+                        let id: string = "";
+                        try {
+                            for (const name in files) {
+                                const path_2 = files[name].path;
+                                const zip = new AdmZip(path_2);
+                                zip.getEntries().forEach((entry: any) => {
+                                    if (!entry.entryName.startsWith("files/")) return;
+                                    let dirname = path.dirname(entry.entryName) + "/";
+                                    let extname = path.extname(entry.entryName);
+                                    let basename = path.basename(entry.entryName).split(".")[0];
+                                    // zip.extractEntryTo(dirname + basename + "_o" + extname, __dirname + RouteStore.public, true, false);
+                                    // zip.extractEntryTo(dirname + basename + "_s" + extname, __dirname + RouteStore.public, true, false);
+                                    // zip.extractEntryTo(dirname + basename + "_m" + extname, __dirname + RouteStore.public, true, false);
+                                    // zip.extractEntryTo(dirname + basename + "_l" + extname, __dirname + RouteStore.public, true, false);
+                                    try {
+                                        zip.extractEntryTo(entry.entryName, __dirname + RouteStore.public, true, false);
+                                        dirname = "/" + dirname;
+
+                                        createReadStream(publicDirectory + dirname + basename + extname).pipe(createWriteStream(publicDirectory + dirname + basename + "_o" + extname));
+                                        createReadStream(publicDirectory + dirname + basename + extname).pipe(createWriteStream(publicDirectory + dirname + basename + "_s" + extname));
+                                        createReadStream(publicDirectory + dirname + basename + extname).pipe(createWriteStream(publicDirectory + dirname + basename + "_m" + extname));
+                                        createReadStream(publicDirectory + dirname + basename + extname).pipe(createWriteStream(publicDirectory + dirname + basename + "_l" + extname));
+                                    } catch (e) {
+                                        console.log(e);
+                                    }
+                                });
+                                const json = zip.getEntry("doc.json");
+                                let docs: any;
+                                try {
+                                    let data = JSON.parse(json.getData().toString("utf8"));
+                                    docs = data.docs;
+                                    id = data.id;
+                                    docs = Object.keys(docs).map(key => docs[key]);
+                                    docs.forEach(mapFn);
+                                    await Promise.all(docs.map((doc: any) => new Promise(res => Database.Instance.replace(doc.id, doc, (err, r) => {
+                                        err && console.log(err);
+                                        res();
+                                    }, true, "newDocuments"))));
+                                } catch (e) { console.log(e); }
+                                unlink(path_2, () => { });
+                            }
+                            if (id) {
+                                res.send(JSON.stringify(getId(id)));
+                            } else {
+                                res.send(JSON.stringify("error"));
+                            }
+                        } catch (e) { console.log(e); }
+                        resolve();
+                    });
+                });
+            }
+        });
+
+
+        register({
+            method: Method.POST,
+            subscription: RouteStore.upload,
+            onValidation: async ({ req, res }) => {
+                let form = new formidable.IncomingForm();
+                form.uploadDir = filesDirectory;
+                form.keepExtensions = true;
+                return new Promise<void>(resolve => {
+                    form.parse(req, async (_err, _fields, files) => {
+                        let results: DashUploadUtils.ImageFileResponse[] = [];
+                        for (const key in files) {
+                            const { type, path: location, name } = files[key];
+                            const filename = path.basename(location);
+                            let uploadInformation: Opt<DashUploadUtils.UploadInformation>;
+                            if (filename.endsWith(".pdf")) {
+                                let dataBuffer = readFileSync(filesDirectory + filename);
+                                const result: ParsedPDF = await pdf(dataBuffer);
+                                await new Promise<void>((resolve, reject) => {
+                                    const path = filesDirectory + Partitions.PdfText + "/" + filename.substring(0, filename.length - ".pdf".length) + ".txt";
+                                    createWriteStream(path).write(result.text, error => {
+                                        if (!error) {
+                                            resolve();
+                                        } else {
+                                            reject(error);
+                                        }
+                                    });
+                                });
+                            } else {
+                                uploadInformation = await DashUploadUtils.UploadImage(filesDirectory + filename, filename);
+                            }
+                            const exif = uploadInformation ? uploadInformation.exifData : undefined;
+                            results.push({ name, type, path: `/files/${filename}`, exif });
+                        }
+                        _success(res, results);
+                        resolve();
+                    });
+                });
+            }
+        });
+
+        register({
+            method: Method.POST,
+            subscription: RouteStore.inspectImage,
+            onValidation: async ({ req, res }) => {
+                const { source } = req.body;
+                if (typeof source === "string") {
+                    const uploadInformation = await DashUploadUtils.UploadImage(source);
+                    return res.send(await DashUploadUtils.InspectImage(uploadInformation.mediaPaths[0]));
+                }
+                res.send({});
+            }
+        });
+
+        register({
+            method: Method.POST,
+            subscription: RouteStore.dataUriToImage,
+            onValidation: ({ req, res }) => {
+                const uri = req.body.uri;
+                const filename = req.body.name;
+                if (!uri || !filename) {
+                    res.status(401).send("incorrect parameters specified");
+                    return;
+                }
+                return imageDataUri.outputFile(uri, filesDirectory + filename).then((savedName: string) => {
+                    const ext = path.extname(savedName).toLowerCase();
+                    const { pngs, jpgs } = SharedMediaTypes;
+                    let resizers = [
+                        { resizer: sharp().resize(100, undefined, { withoutEnlargement: true }), suffix: "_s" },
+                        { resizer: sharp().resize(400, undefined, { withoutEnlargement: true }), suffix: "_m" },
+                        { resizer: sharp().resize(900, undefined, { withoutEnlargement: true }), suffix: "_l" },
+                    ];
+                    let isImage = false;
+                    if (pngs.includes(ext)) {
+                        resizers.forEach(element => {
+                            element.resizer = element.resizer.png();
+                        });
+                        isImage = true;
+                    } else if (jpgs.includes(ext)) {
+                        resizers.forEach(element => {
+                            element.resizer = element.resizer.jpeg();
+                        });
+                        isImage = true;
+                    }
+                    if (isImage) {
+                        resizers.forEach(resizer => {
+                            createReadStream(savedName).pipe(resizer.resizer).pipe(createWriteStream(filesDirectory + filename + resizer.suffix + ext));
+                        });
+                    }
+                    res.send("/files/" + filename + ext);
+                });
+            }
+        });
+
+    }
+
+}
\ No newline at end of file
diff --git a/src/server/ApiManagers/UserManager.ts b/src/server/ApiManagers/UserManager.ts
index dd1e50133..fe1ce7f2b 100644
--- a/src/server/ApiManagers/UserManager.ts
+++ b/src/server/ApiManagers/UserManager.ts
@@ -1,11 +1,36 @@
 import ApiManager, { Registration } from "./ApiManager";
 import { Method } from "../RouteManager";
 import { WebSocket } from "../Websocket/Websocket";
+import { RouteStore } from "../RouteStore";
+import { Database } from "../database";
 
 export default class UserManager extends ApiManager {
 
     protected initialize(register: Registration): void {
 
+        register({
+            method: Method.GET,
+            subscription: RouteStore.getUsers,
+            onValidation: async ({ res }) => {
+                const cursor = await Database.Instance.query({}, { email: 1, userDocumentId: 1 }, "users");
+                const results = await cursor.toArray();
+                res.send(results.map(user => ({ email: user.email, userDocumentId: user.userDocumentId })));
+            }
+        });
+
+        register({
+            method: Method.GET,
+            subscription: RouteStore.getUserDocumentId,
+            onValidation: ({ res, user }) => res.send(user.userDocumentId)
+        });
+
+        register({
+            method: Method.GET,
+            subscription: RouteStore.getCurrUser,
+            onValidation: ({ res, user }) => res.send(JSON.stringify(user)),
+            onUnauthenticated: ({ res }) => res.send(JSON.stringify({ id: "__guest__", email: "" }))
+        });
+
         register({
             method: Method.GET,
             subscription: "/whosOnline",
@@ -17,7 +42,7 @@ export default class UserManager extends ApiManager {
                 for (const user in timeMap) {
                     const time = timeMap[user];
                     const key = ((now - time) / 1000) < (60 * 5) ? "active" : "inactive";
-                    users[key][user] = `Last active ${this.msToTime(now - time)} ago`;
+                    users[key][user] = `Last active ${msToTime(now - time)} ago`;
                 }
 
                 res.send(users);
@@ -26,17 +51,17 @@ export default class UserManager extends ApiManager {
 
     }
 
-    private msToTime(duration: number) {
-        let milliseconds = Math.floor((duration % 1000) / 100),
-            seconds = Math.floor((duration / 1000) % 60),
-            minutes = Math.floor((duration / (1000 * 60)) % 60),
-            hours = Math.floor((duration / (1000 * 60 * 60)) % 24);
+}
 
-        let hoursS = (hours < 10) ? "0" + hours : hours;
-        let minutesS = (minutes < 10) ? "0" + minutes : minutes;
-        let secondsS = (seconds < 10) ? "0" + seconds : seconds;
+function msToTime(duration: number) {
+    let milliseconds = Math.floor((duration % 1000) / 100),
+        seconds = Math.floor((duration / 1000) % 60),
+        minutes = Math.floor((duration / (1000 * 60)) % 60),
+        hours = Math.floor((duration / (1000 * 60 * 60)) % 24);
 
-        return hoursS + ":" + minutesS + ":" + secondsS + "." + milliseconds;
-    }
+    let hoursS = (hours < 10) ? "0" + hours : hours;
+    let minutesS = (minutes < 10) ? "0" + minutes : minutes;
+    let secondsS = (seconds < 10) ? "0" + seconds : seconds;
 
+    return hoursS + ":" + minutesS + ":" + secondsS + "." + milliseconds;
 }
\ No newline at end of file
diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts
index 9fddb466c..8f5b0e1a8 100644
--- a/src/server/DashUploadUtils.ts
+++ b/src/server/DashUploadUtils.ts
@@ -5,6 +5,7 @@ import * as sharp from 'sharp';
 import request = require('request-promise');
 import { ExifData, ExifImage } from 'exif';
 import { Opt } from '../new_fields/Doc';
+import { SharedMediaTypes } from './SharedMediaTypes';
 
 const uploadDirectory = path.join(__dirname, './public/files/');
 
@@ -15,20 +16,21 @@ export namespace DashUploadUtils {
         suffix: string;
     }
 
+    export interface ImageFileResponse {
+        name: string;
+        path: string;
+        type: string;
+        exif: Opt<DashUploadUtils.EnrichedExifData>;
+    }
+
     export const Sizes: { [size: string]: Size } = {
         SMALL: { width: 100, suffix: "_s" },
         MEDIUM: { width: 400, suffix: "_m" },
         LARGE: { width: 900, suffix: "_l" },
     };
 
-    const gifs = [".gif"];
-    const pngs = [".png"];
-    const jpgs = [".jpg", ".jpeg"];
-    const imageFormats = [...pngs, ...jpgs, ...gifs];
-    const videoFormats = [".mov", ".mp4"];
-
     export function validateExtension(url: string) {
-        return imageFormats.includes(path.extname(url).toLowerCase());
+        return SharedMediaTypes.imageFormats.includes(path.extname(url).toLowerCase());
     }
 
     const size = "content-length";
@@ -132,6 +134,7 @@ export namespace DashUploadUtils {
             contentSize,
             contentType,
         };
+        const { pngs, imageFormats, jpgs, videoFormats } = SharedMediaTypes;
         return new Promise<UploadInformation>(async (resolve, reject) => {
             const resizers = [
                 { resizer: sharp().rotate(), suffix: "_o" },
diff --git a/src/server/SharedMediaTypes.ts b/src/server/SharedMediaTypes.ts
new file mode 100644
index 000000000..3d3234125
--- /dev/null
+++ b/src/server/SharedMediaTypes.ts
@@ -0,0 +1,9 @@
+export namespace SharedMediaTypes {
+
+    export const gifs = [".gif"];
+    export const pngs = [".png"];
+    export const jpgs = [".jpg", ".jpeg"];
+    export const imageFormats = [...pngs, ...jpgs, ...gifs];
+    export const videoFormats = [".mov", ".mp4"];
+
+}
\ No newline at end of file
diff --git a/src/server/Websocket/Websocket.ts b/src/server/Websocket/Websocket.ts
index cd2813d99..f6a6c8718 100644
--- a/src/server/Websocket/Websocket.ts
+++ b/src/server/Websocket/Websocket.ts
@@ -6,7 +6,9 @@ import { Database } from "../database";
 import { Search } from "../Search";
 import * as io from 'socket.io';
 import YoutubeApi from "../apis/youtube/youtubeApiSample";
-import { youtubeApiKey } from "..";
+import { readFile } from "fs";
+import { Credentials } from "google-auth-library";
+import { GoogleCredentialsLoader } from "../credentials/CredentialsLoader";
 
 export namespace WebSocket {
 
@@ -18,6 +20,14 @@ export namespace WebSocket {
     export const socketMap = new Map<SocketIO.Socket, string>();
     export const timeMap: { [id: string]: number } = {};
 
+    export async function start(serverPort: number, isRelease: boolean) {
+        await preliminaryFunctions();
+        initialize(serverPort, isRelease);
+    }
+
+    async function preliminaryFunctions() {
+    }
+
     export function initialize(serverPort: number, isRelease: boolean) {
         const endpoint = io();
         endpoint.listen(serverPort);
@@ -54,14 +64,15 @@ export namespace WebSocket {
     }
 
     function HandleYoutubeQuery([query, callback]: [YoutubeQueryInput, (result?: any[]) => void]) {
+        const { ProjectCredentials } = GoogleCredentialsLoader;
         switch (query.type) {
             case YoutubeQueryTypes.Channels:
-                YoutubeApi.authorizedGetChannel(youtubeApiKey);
+                YoutubeApi.authorizedGetChannel(ProjectCredentials);
                 break;
             case YoutubeQueryTypes.SearchVideo:
-                YoutubeApi.authorizedGetVideos(youtubeApiKey, query.userInput, callback);
+                YoutubeApi.authorizedGetVideos(ProjectCredentials, query.userInput, callback);
             case YoutubeQueryTypes.VideoDetails:
-                YoutubeApi.authorizedGetVideoDetails(youtubeApiKey, query.videoIds, callback);
+                YoutubeApi.authorizedGetVideoDetails(ProjectCredentials, query.videoIds, callback);
         }
     }
 
diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts
index 35a2541a9..b3657ee43 100644
--- a/src/server/apis/google/GoogleApiServerUtils.ts
+++ b/src/server/apis/google/GoogleApiServerUtils.ts
@@ -1,12 +1,11 @@
 import { google } from "googleapis";
-import { readFile } from "fs";
 import { OAuth2Client, Credentials, OAuth2ClientOptions } from "google-auth-library";
 import { Opt } from "../../../new_fields/Doc";
 import { GaxiosResponse } from "gaxios";
 import request = require('request-promise');
 import * as qs from 'query-string';
 import { Database } from "../../database";
-import * as path from "path";
+import { GoogleCredentialsLoader } from "../../credentials/CredentialsLoader";
 
 /**
  * Scopes give Google users fine granularity of control
@@ -61,6 +60,23 @@ export namespace GoogleApiServerUtils {
      */
     let worker: OAuth2Client;
 
+    /**
+     * This function is called once before the server is started,
+     * reading in Dash's project-specific credentials (client secret
+     * and client id) for later repeated access. It also sets up the
+     * global, intentionally unauthenticated worker OAuth2 client instance. 
+     */
+    export function processProjectCredentials(): void {
+        const { client_secret, client_id, redirect_uris } = GoogleCredentialsLoader.ProjectCredentials;
+        // initialize the global authorization client
+        installed = {
+            clientId: client_id,
+            clientSecret: client_secret,
+            redirectUri: redirect_uris[0]
+        };
+        worker = generateClient();
+    }
+
     /**
      * A briefer format for the response from a 'googleapis' API request
      */
@@ -96,32 +112,6 @@ export namespace GoogleApiServerUtils {
         batchUpdate: ApiHandler;
     }
 
-    /**
-     * This function is called once before the server is started,
-     * reading in Dash's project-specific credentials (client secret
-     * and client id) for later repeated access. It also sets up the
-     * global, intentionally unauthenticated worker OAuth2 client instance. 
-     */
-    export async function loadClientSecret(): Promise<void> {
-        return new Promise((resolve, reject) => {
-            readFile(path.join(__dirname, "../../credentials/google_docs_credentials.json"), async (err, projectCredentials) => {
-                if (err) {
-                    reject(err);
-                    return console.log('Error loading client secret file:', err);
-                }
-                const { client_secret, client_id, redirect_uris } = JSON.parse(projectCredentials.toString()).installed;
-                // initialize the global authorization client
-                installed = {
-                    clientId: client_id,
-                    clientSecret: client_secret,
-                    redirectUri: redirect_uris[0]
-                };
-                worker = generateClient();
-                resolve();
-            });
-        });
-    }
-
     /**
      * Maps the Dash user id of a given user to their single
      * associated OAuth2 client, mitigating the creation
diff --git a/src/server/apis/google/GooglePhotosUploadUtils.ts b/src/server/apis/google/GooglePhotosUploadUtils.ts
index d8cf795b5..0abed3f1d 100644
--- a/src/server/apis/google/GooglePhotosUploadUtils.ts
+++ b/src/server/apis/google/GooglePhotosUploadUtils.ts
@@ -1,7 +1,6 @@
 import request = require('request-promise');
 import * as path from 'path';
-import { MediaItemCreationResult, NewMediaItemResult } from './SharedTypes';
-import { NewMediaItem } from "../../index";
+import { NewMediaItemResult } from './SharedTypes';
 import { BatchedArray, TimeUnit } from 'array-batcher';
 import { DashUploadUtils } from '../../DashUploadUtils';
 
@@ -28,6 +27,22 @@ export namespace GooglePhotosUploadUtils {
         description: string;
     }
 
+    /**
+     * This is the format needed to pass
+     * into the BatchCreate API request
+     * to take a reference to raw uploaded bytes
+     * and actually create an image in Google Photos.
+     * 
+     * So, to instantiate this interface you must have already dispatched an upload
+     * and received an upload token.
+     */
+    export interface NewMediaItem {
+        description: string;
+        simpleMediaItem: {
+            uploadToken: string;
+        };
+    }
+
     /**
      * A utility function to streamline making
      * calls to the API's url - accentuates
diff --git a/src/server/credentials/CredentialsLoader.ts b/src/server/credentials/CredentialsLoader.ts
new file mode 100644
index 000000000..e3f4d167b
--- /dev/null
+++ b/src/server/credentials/CredentialsLoader.ts
@@ -0,0 +1,29 @@
+import { readFile } from "fs";
+
+export namespace GoogleCredentialsLoader {
+
+    export interface InstalledCredentials {
+        client_id: string;
+        project_id: string;
+        auth_uri: string;
+        token_uri: string;
+        auth_provider_x509_cert_url: string;
+        client_secret: string;
+        redirect_uris: string[];
+    }
+
+    export let ProjectCredentials: InstalledCredentials;
+
+    export async function loadCredentials() {
+        ProjectCredentials = await new Promise<InstalledCredentials>(resolve => {
+            readFile(__dirname + '/google_project_credentials.json', function processClientSecrets(err, content) {
+                if (err) {
+                    console.log('Error loading client secret file: ' + err);
+                    return;
+                }
+                resolve(JSON.parse(content.toString()).installed);
+            });
+        });
+    }
+
+}
diff --git a/src/server/credentials/google_docs_credentials.json b/src/server/credentials/google_docs_credentials.json
deleted file mode 100644
index 955c5a3c1..000000000
--- a/src/server/credentials/google_docs_credentials.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
-    "installed": {
-        "client_id": "343179513178-ud6tvmh275r2fq93u9eesrnc66t6akh9.apps.googleusercontent.com",
-        "project_id": "quickstart-1565056383187",
-        "auth_uri": "https://accounts.google.com/o/oauth2/auth",
-        "token_uri": "https://oauth2.googleapis.com/token",
-        "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
-        "client_secret": "w8KIFSc0MQpmUYHed4qEzn8b",
-        "redirect_uris": ["urn:ietf:wg:oauth:2.0:oob", "http://localhost"]
-    }
-}
\ No newline at end of file
diff --git a/src/server/credentials/google_project_credentials.json b/src/server/credentials/google_project_credentials.json
new file mode 100644
index 000000000..5d9c62eb1
--- /dev/null
+++ b/src/server/credentials/google_project_credentials.json
@@ -0,0 +1,14 @@
+{
+    "installed": {
+        "client_id": "1005546247619-kqpnvh42mpa803tem8556b87umi4j9r0.apps.googleusercontent.com",
+        "project_id": "brown-dash",
+        "auth_uri": "https://accounts.google.com/o/oauth2/auth",
+        "token_uri": "https://oauth2.googleapis.com/token",
+        "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
+        "client_secret": "WshLb5TH9SdFVGGbQcnYj7IU",
+        "redirect_uris": [
+            "urn:ietf:wg:oauth:2.0:oob",
+            "http://localhost"
+        ]
+    }
+}
\ No newline at end of file
diff --git a/src/server/credentials/test.json b/src/server/credentials/test.json
new file mode 100644
index 000000000..0a032cc2d
--- /dev/null
+++ b/src/server/credentials/test.json
@@ -0,0 +1,14 @@
+{
+    "installed": {
+        "client_id": "343179513178-ud6tvmh275r2fq93u9eesrnc66t6akh9.apps.googleusercontent.com",
+        "project_id": "quickstart-1565056383187",
+        "auth_uri": "https://accounts.google.com/o/oauth2/auth",
+        "token_uri": "https://oauth2.googleapis.com/token",
+        "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
+        "client_secret": "w8KIFSc0MQpmUYHed4qEzn8b",
+        "redirect_uris": [
+            "urn:ietf:wg:oauth:2.0:oob",
+            "http://localhost"
+        ]
+    }
+}
\ No newline at end of file
diff --git a/src/server/index.ts b/src/server/index.ts
index 25697e71f..8eb88cf8b 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -1,352 +1,75 @@
 require('dotenv').config();
-import * as formidable from 'formidable';
-import * as fs from 'fs';
-import * as sharp from 'sharp';
-import * as Pdfjs from 'pdfjs-dist';
-const imageDataUri = require('image-data-uri');
+import { GoogleApiServerUtils } from "./apis/google/GoogleApiServerUtils";
 import * as mobileDetect from 'mobile-detect';
 import * as path from 'path';
 import { Database } from './database';
 import { RouteStore } from './RouteStore';
-import v4 = require('uuid/v4');
-import { createCanvas } from "canvas";
 const serverPort = 4321;
-import { Search } from './Search';
-import * as Archiver from 'archiver';
-var AdmZip = require('adm-zip');
-import * as YoutubeApi from "./apis/youtube/youtubeApiSample";
-import { Response } from 'express-serve-static-core';
-import { GoogleApiServerUtils } from "./apis/google/GoogleApiServerUtils";
-const probe = require("probe-image-size");
-const pdf = require('pdf-parse');
 import { GooglePhotosUploadUtils } from './apis/google/GooglePhotosUploadUtils';
 import { Opt } from '../new_fields/Doc';
 import { DashUploadUtils } from './DashUploadUtils';
 import { BatchedArray, TimeUnit } from 'array-batcher';
-import { ParsedPDF } from "./PdfTypes";
-import { reject } from 'bluebird';
 import RouteSubscriber from './RouteSubscriber';
-import InitializeServer from './Initialization';
+import initializeServer from './Initialization';
 import RouteManager, { Method, _success, _permission_denied, _error, _invalid, OnUnauthenticated } from './RouteManager';
 import * as qs from 'query-string';
 import UtilManager from './ApiManagers/UtilManager';
 import SearchManager from './ApiManagers/SearchManager';
 import UserManager from './ApiManagers/UserManager';
 import { WebSocket } from './Websocket/Websocket';
-import ExportManager from './ApiManagers/ExportManager';
-import ApiManager from './ApiManagers/ApiManager';
-
-export let youtubeApiKey: string;
-
-export interface NewMediaItem {
-    description: string;
-    simpleMediaItem: {
-        uploadToken: string;
-    };
+import DownloadManager from './ApiManagers/ExportManager';
+import { GoogleCredentialsLoader } from './credentials/CredentialsLoader';
+import DeleteManager from "./ApiManagers/DeleteManager";
+import PDFManager from "./ApiManagers/PDFManager";
+import UploadManager from "./ApiManagers/UploadManager";
+
+export const publicDirectory = __dirname + RouteStore.public;
+export const filesDirectory = publicDirectory + "/files/";
+export enum Partitions {
+    PdfText = "pdf_text"
 }
 
-const pngTypes = [".png", ".PNG"];
-const jpgTypes = [".jpg", ".JPG", ".jpeg", ".JPEG"];
-export const uploadDirectory = __dirname + "/public/files/";
-const pdfDirectory = uploadDirectory + "text";
-const solrURL = "http://localhost:8983/solr/#/dash";
-
-start();
-
-async function start() {
-    await PreliminaryFunctions();
-    await InitializeServer({ listenAtPort: 1050, routeSetter });
-}
-
-async function PreliminaryFunctions() {
-    await new Promise<void>(resolve => {
-        YoutubeApi.readApiKey((apiKey: string) => {
-            youtubeApiKey = apiKey;
-            resolve();
-        });
-    });
-    await GoogleApiServerUtils.loadClientSecret();
-    await DashUploadUtils.createIfNotExists(pdfDirectory);
+/**
+ * These are the functions run before the server starts
+ * listening. Anything that must be complete
+ * before clients can access the server should be run or awaited here.
+ */
+async function preliminaryFunctions() {
+    // make project credentials globally accessible
+    await GoogleCredentialsLoader.loadCredentials();
+    // read the resulting credentials into a different namespace
+    GoogleApiServerUtils.processProjectCredentials();
+    // divide the public directory based on type
+    await Promise.all(Object.keys(Partitions).map(partition => DashUploadUtils.createIfNotExists(filesDirectory + partition)));
+    // connect to the database
     await Database.tryInitializeConnection();
 }
 
+/**
+ * Either clustered together as an API manager
+ * or individually referenced below, by the completion
+ * of this function's execution, all routes will
+ * be registered on the server 
+ * @param router the instance of the route manager
+ * that will manage the registration of new routes
+ * with the server
+ */
 function routeSetter(router: RouteManager) {
-    const managers: ApiManager[] = [
-        new UtilManager(),
-        new SearchManager(),
+    // initialize API Managers
+    [
         new UserManager(),
-        new ExportManager()
-    ];
-    managers.forEach(manager => manager.register(router));
+        new UploadManager(),
+        new DownloadManager(),
+        new SearchManager(),
+        new PDFManager(),
+        new DeleteManager(),
+        new UtilManager()
+    ].forEach(manager => manager.register(router));
 
+    // initialize the web socket (bidirectional communication: if a user changes
+    // a field on one client, that change must be broadcast to all other clients)
     WebSocket.initialize(serverPort, router.isRelease);
 
-    async function getDocs(id: string) {
-        const files = new Set<string>();
-        const docs: { [id: string]: any } = {};
-        const fn = (doc: any): string[] => {
-            const id = doc.id;
-            if (typeof id === "string" && id.endsWith("Proto")) {
-                //Skip protos
-                return [];
-            }
-            const ids: string[] = [];
-            for (const key in doc.fields) {
-                if (!doc.fields.hasOwnProperty(key)) {
-                    continue;
-                }
-                const field = doc.fields[key];
-                if (field === undefined || field === null) {
-                    continue;
-                }
-
-                if (field.__type === "proxy" || field.__type === "prefetch_proxy") {
-                    ids.push(field.fieldId);
-                } else if (field.__type === "script" || field.__type === "computed") {
-                    if (field.captures) {
-                        ids.push(field.captures.fieldId);
-                    }
-                } else if (field.__type === "list") {
-                    ids.push(...fn(field));
-                } else if (typeof field === "string") {
-                    const re = /"(?:dataD|d)ocumentId"\s*:\s*"([\w\-]*)"/g;
-                    let match: string[] | null;
-                    while ((match = re.exec(field)) !== null) {
-                        ids.push(match[1]);
-                    }
-                } else if (field.__type === "RichTextField") {
-                    const re = /"href"\s*:\s*"(.*?)"/g;
-                    let match: string[] | null;
-                    while ((match = re.exec(field.Data)) !== null) {
-                        const urlString = match[1];
-                        const split = new URL(urlString).pathname.split("doc/");
-                        if (split.length > 1) {
-                            ids.push(split[split.length - 1]);
-                        }
-                    }
-                    const re2 = /"src"\s*:\s*"(.*?)"/g;
-                    while ((match = re2.exec(field.Data)) !== null) {
-                        const urlString = match[1];
-                        const pathname = new URL(urlString).pathname;
-                        files.add(pathname);
-                    }
-                } else if (["audio", "image", "video", "pdf", "web"].includes(field.__type)) {
-                    const url = new URL(field.url);
-                    const pathname = url.pathname;
-                    files.add(pathname);
-                }
-            }
-
-            if (doc.id) {
-                docs[doc.id] = doc;
-            }
-            return ids;
-        };
-        await Database.Instance.visit([id], fn);
-        return { id, docs, files };
-    }
-
-    router.addSupervisedRoute({
-        method: Method.GET,
-        subscription: new RouteSubscriber("/serializeDoc").add("docId"),
-        onValidation: async ({ req, res }) => {
-            const { docs, files } = await getDocs(req.params.docId);
-            res.send({ docs, files: Array.from(files) });
-        }
-    });
-
-    router.addSupervisedRoute({
-        method: Method.GET,
-        subscription: new RouteSubscriber("/downloadId").add("docId"),
-        onValidation: async ({ req, res }) => {
-            res.set('Content-disposition', `attachment;`);
-            res.set('Content-Type', "application/zip");
-            const { id, docs, files } = await getDocs(req.params.docId);
-            const docString = JSON.stringify({ id, docs });
-            const zip = Archiver('zip');
-            zip.pipe(res);
-            zip.append(docString, { name: "doc.json" });
-            files.forEach(val => {
-                zip.file(__dirname + RouteStore.public + val, { name: val.substring(1) });
-            });
-            zip.finalize();
-        }
-    });
-
-    router.addSupervisedRoute({
-        method: Method.POST,
-        subscription: "/uploadDoc",
-        onValidation: ({ req, res }) => {
-            let form = new formidable.IncomingForm();
-            form.keepExtensions = true;
-            // let path = req.body.path;
-            const ids: { [id: string]: string } = {};
-            let remap = true;
-            const getId = (id: string): string => {
-                if (!remap) return id;
-                if (id.endsWith("Proto")) return id;
-                if (id in ids) {
-                    return ids[id];
-                } else {
-                    return ids[id] = v4();
-                }
-            };
-            const mapFn = (doc: any) => {
-                if (doc.id) {
-                    doc.id = getId(doc.id);
-                }
-                for (const key in doc.fields) {
-                    if (!doc.fields.hasOwnProperty(key)) {
-                        continue;
-                    }
-                    const field = doc.fields[key];
-                    if (field === undefined || field === null) {
-                        continue;
-                    }
-
-                    if (field.__type === "proxy" || field.__type === "prefetch_proxy") {
-                        field.fieldId = getId(field.fieldId);
-                    } else if (field.__type === "script" || field.__type === "computed") {
-                        if (field.captures) {
-                            field.captures.fieldId = getId(field.captures.fieldId);
-                        }
-                    } else if (field.__type === "list") {
-                        mapFn(field);
-                    } else if (typeof field === "string") {
-                        const re = /("(?:dataD|d)ocumentId"\s*:\s*")([\w\-]*)"/g;
-                        doc.fields[key] = (field as any).replace(re, (match: any, p1: string, p2: string) => {
-                            return `${p1}${getId(p2)}"`;
-                        });
-                    } else if (field.__type === "RichTextField") {
-                        const re = /("href"\s*:\s*")(.*?)"/g;
-                        field.Data = field.Data.replace(re, (match: any, p1: string, p2: string) => {
-                            return `${p1}${getId(p2)}"`;
-                        });
-                    }
-                }
-            };
-            return new Promise<void>(resolve => {
-                form.parse(req, async (_err, fields, files) => {
-                    remap = fields.remap !== "false";
-                    let id: string = "";
-                    try {
-                        for (const name in files) {
-                            const path_2 = files[name].path;
-                            const zip = new AdmZip(path_2);
-                            zip.getEntries().forEach((entry: any) => {
-                                if (!entry.entryName.startsWith("files/")) return;
-                                let dirname = path.dirname(entry.entryName) + "/";
-                                let extname = path.extname(entry.entryName);
-                                let basename = path.basename(entry.entryName).split(".")[0];
-                                // zip.extractEntryTo(dirname + basename + "_o" + extname, __dirname + RouteStore.public, true, false);
-                                // zip.extractEntryTo(dirname + basename + "_s" + extname, __dirname + RouteStore.public, true, false);
-                                // zip.extractEntryTo(dirname + basename + "_m" + extname, __dirname + RouteStore.public, true, false);
-                                // zip.extractEntryTo(dirname + basename + "_l" + extname, __dirname + RouteStore.public, true, false);
-                                try {
-                                    zip.extractEntryTo(entry.entryName, __dirname + RouteStore.public, true, false);
-                                    dirname = "/" + dirname;
-
-                                    fs.createReadStream(__dirname + RouteStore.public + dirname + basename + extname).pipe(fs.createWriteStream(__dirname + RouteStore.public + dirname + basename + "_o" + extname));
-                                    fs.createReadStream(__dirname + RouteStore.public + dirname + basename + extname).pipe(fs.createWriteStream(__dirname + RouteStore.public + dirname + basename + "_s" + extname));
-                                    fs.createReadStream(__dirname + RouteStore.public + dirname + basename + extname).pipe(fs.createWriteStream(__dirname + RouteStore.public + dirname + basename + "_m" + extname));
-                                    fs.createReadStream(__dirname + RouteStore.public + dirname + basename + extname).pipe(fs.createWriteStream(__dirname + RouteStore.public + dirname + basename + "_l" + extname));
-                                } catch (e) {
-                                    console.log(e);
-                                }
-                            });
-                            const json = zip.getEntry("doc.json");
-                            let docs: any;
-                            try {
-                                let data = JSON.parse(json.getData().toString("utf8"));
-                                docs = data.docs;
-                                id = data.id;
-                                docs = Object.keys(docs).map(key => docs[key]);
-                                docs.forEach(mapFn);
-                                await Promise.all(docs.map((doc: any) => new Promise(res => Database.Instance.replace(doc.id, doc, (err, r) => {
-                                    err && console.log(err);
-                                    res();
-                                }, true, "newDocuments"))));
-                            } catch (e) { console.log(e); }
-                            fs.unlink(path_2, () => { });
-                        }
-                        if (id) {
-                            res.send(JSON.stringify(getId(id)));
-                        } else {
-                            res.send(JSON.stringify("error"));
-                        }
-                    } catch (e) { console.log(e); }
-                    resolve();
-                });
-            });
-        }
-    });
-
-    router.addSupervisedRoute({
-        method: Method.GET,
-        subscription: new RouteSubscriber("/thumbnail").add("filename"),
-        onValidation: ({ req, res }) => {
-            let filename = req.params.filename;
-            let noExt = filename.substring(0, filename.length - ".png".length);
-            let pagenumber = parseInt(noExt.split('-')[1]);
-            return new Promise<void>(resolve => {
-                fs.exists(uploadDirectory + filename, (exists: boolean) => {
-                    console.log(`${uploadDirectory + filename} ${exists ? "exists" : "does not exist"}`);
-                    if (exists) {
-                        let input = fs.createReadStream(uploadDirectory + filename);
-                        probe(input, (err: any, result: any) => {
-                            if (err) {
-                                console.log(err);
-                                console.log(`error on ${filename}`);
-                                return;
-                            }
-                            res.send({ path: "/files/" + filename, width: result.width, height: result.height });
-                        });
-                    }
-                    else {
-                        LoadPage(uploadDirectory + filename.substring(0, filename.length - noExt.split('-')[1].length - ".PNG".length - 1) + ".pdf", pagenumber, res);
-                    }
-                    resolve();
-                });
-            });
-        }
-    });
-
-    function LoadPage(file: string, pageNumber: number, res: Response) {
-        console.log(file);
-        Pdfjs.getDocument(file).promise
-            .then((pdf: Pdfjs.PDFDocumentProxy) => {
-                let factory = new NodeCanvasFactory();
-                console.log(pageNumber);
-                pdf.getPage(pageNumber).then((page: Pdfjs.PDFPageProxy) => {
-                    console.log("reading " + page);
-                    let viewport = page.getViewport(1 as any);
-                    let canvasAndContext = factory.create(viewport.width, viewport.height);
-                    let renderContext = {
-                        canvasContext: canvasAndContext.context,
-                        viewport: viewport,
-                        canvasFactory: factory
-                    };
-                    console.log("read " + pageNumber);
-
-                    page.render(renderContext).promise
-                        .then(() => {
-                            console.log("saving " + pageNumber);
-                            let stream = canvasAndContext.canvas.createPNGStream();
-                            let pngFile = `${file.substring(0, file.length - ".pdf".length)}-${pageNumber}.PNG`;
-                            let out = fs.createWriteStream(pngFile);
-                            stream.pipe(out);
-                            out.on("finish", () => {
-                                console.log(`Success! Saved to ${pngFile}`);
-                                let name = path.basename(pngFile);
-                                res.send({ path: "/files/" + name, width: viewport.width, height: viewport.height });
-                            });
-                        }, (reason: string) => {
-                            console.error(reason + ` ${pageNumber}`);
-                        });
-                });
-            });
-    }
-
     /**
      * Anyone attempting to navigate to localhost at this port will
      * first have to log in.
@@ -357,16 +80,6 @@ function routeSetter(router: RouteManager) {
         onValidation: ({ res }) => res.redirect(RouteStore.home)
     });
 
-    router.addSupervisedRoute({
-        method: Method.GET,
-        subscription: RouteStore.getUsers,
-        onValidation: async ({ res }) => {
-            const cursor = await Database.Instance.query({}, { email: 1, userDocumentId: 1 }, "users");
-            const results = await cursor.toArray();
-            res.send(results.map(user => ({ email: user.email, userDocumentId: user.userDocumentId })));
-        }
-    });
-
     const serve: OnUnauthenticated = ({ req, res }) => {
         let detector = new mobileDetect(req.headers['user-agent'] || "");
         let filename = detector.mobile() !== null ? 'mobile/image.html' : 'index.html';
@@ -387,19 +100,6 @@ function routeSetter(router: RouteManager) {
         }
     });
 
-    router.addSupervisedRoute({
-        method: Method.GET,
-        subscription: RouteStore.getUserDocumentId,
-        onValidation: ({ res, user }) => res.send(user.userDocumentId)
-    });
-
-    router.addSupervisedRoute({
-        method: Method.GET,
-        subscription: RouteStore.getCurrUser,
-        onValidation: ({ res, user }) => res.send(JSON.stringify(user)),
-        onUnauthenticated: ({ res }) => res.send(JSON.stringify({ id: "__guest__", email: "" }))
-    });
-
     const ServicesApiKeyMap = new Map<string, string | undefined>([
         ["face", process.env.FACE],
         ["vision", process.env.VISION],
@@ -415,152 +115,6 @@ function routeSetter(router: RouteManager) {
         }
     });
 
-    class NodeCanvasFactory {
-        create = (width: number, height: number) => {
-            var canvas = createCanvas(width, height);
-            var context = canvas.getContext('2d');
-            return {
-                canvas,
-                context
-            };
-        }
-
-        reset = (canvasAndContext: any, width: number, height: number) => {
-            canvasAndContext.canvas.width = width;
-            canvasAndContext.canvas.height = height;
-        }
-
-        destroy = (canvasAndContext: any) => {
-            canvasAndContext.canvas.width = 0;
-            canvasAndContext.canvas.height = 0;
-            canvasAndContext.canvas = null;
-            canvasAndContext.context = null;
-        }
-    }
-
-    interface ImageFileResponse {
-        name: string;
-        path: string;
-        type: string;
-        exif: Opt<DashUploadUtils.EnrichedExifData>;
-    }
-
-    router.addSupervisedRoute({
-        method: Method.POST,
-        subscription: RouteStore.upload,
-        onValidation: async ({ req, res }) => {
-            let form = new formidable.IncomingForm();
-            form.uploadDir = uploadDirectory;
-            form.keepExtensions = true;
-            return new Promise<void>(resolve => {
-                form.parse(req, async (_err, _fields, files) => {
-                    let results: ImageFileResponse[] = [];
-                    for (const key in files) {
-                        const { type, path: location, name } = files[key];
-                        const filename = path.basename(location);
-                        let uploadInformation: Opt<DashUploadUtils.UploadInformation>;
-                        if (filename.endsWith(".pdf")) {
-                            let dataBuffer = fs.readFileSync(uploadDirectory + filename);
-                            const result: ParsedPDF = await pdf(dataBuffer);
-                            await new Promise<void>(resolve => {
-                                const path = pdfDirectory + "/" + filename.substring(0, filename.length - ".pdf".length) + ".txt";
-                                fs.createWriteStream(path).write(result.text, error => {
-                                    if (!error) {
-                                        resolve();
-                                    } else {
-                                        reject(error);
-                                    }
-                                });
-                            });
-                        } else {
-                            uploadInformation = await DashUploadUtils.UploadImage(uploadDirectory + filename, filename);
-                        }
-                        const exif = uploadInformation ? uploadInformation.exifData : undefined;
-                        results.push({ name, type, path: `/files/${filename}`, exif });
-                    }
-                    _success(res, results);
-                    resolve();
-                });
-            });
-        }
-    });
-
-    router.addSupervisedRoute({
-        method: Method.POST,
-        subscription: RouteStore.inspectImage,
-        onValidation: async ({ req, res }) => {
-            const { source } = req.body;
-            if (typeof source === "string") {
-                const uploadInformation = await DashUploadUtils.UploadImage(source);
-                return res.send(await DashUploadUtils.InspectImage(uploadInformation.mediaPaths[0]));
-            }
-            res.send({});
-        }
-    });
-
-    router.addSupervisedRoute({
-        method: Method.POST,
-        subscription: RouteStore.dataUriToImage,
-        onValidation: ({ req, res }) => {
-            const uri = req.body.uri;
-            const filename = req.body.name;
-            if (!uri || !filename) {
-                res.status(401).send("incorrect parameters specified");
-                return;
-            }
-            return imageDataUri.outputFile(uri, uploadDirectory + filename).then((savedName: string) => {
-                const ext = path.extname(savedName);
-                let resizers = [
-                    { resizer: sharp().resize(100, undefined, { withoutEnlargement: true }), suffix: "_s" },
-                    { resizer: sharp().resize(400, undefined, { withoutEnlargement: true }), suffix: "_m" },
-                    { resizer: sharp().resize(900, undefined, { withoutEnlargement: true }), suffix: "_l" },
-                ];
-                let isImage = false;
-                if (pngTypes.includes(ext)) {
-                    resizers.forEach(element => {
-                        element.resizer = element.resizer.png();
-                    });
-                    isImage = true;
-                } else if (jpgTypes.includes(ext)) {
-                    resizers.forEach(element => {
-                        element.resizer = element.resizer.jpeg();
-                    });
-                    isImage = true;
-                }
-                if (isImage) {
-                    resizers.forEach(resizer => {
-                        fs.createReadStream(savedName).pipe(resizer.resizer).pipe(fs.createWriteStream(uploadDirectory + filename + resizer.suffix + ext));
-                    });
-                }
-                res.send("/files/" + filename + ext);
-            });
-        }
-    });
-
-    router.addSupervisedRoute({
-        method: Method.GET,
-        subscription: RouteStore.delete,
-        onValidation: async ({ res, isRelease }) => {
-            if (isRelease) {
-                return _permission_denied(res, deletionPermissionError);
-            }
-            await WebSocket.deleteFields();
-            res.redirect(RouteStore.home);
-        }
-    });
-
-    router.addSupervisedRoute({
-        method: Method.GET,
-        subscription: RouteStore.deleteAll,
-        onValidation: async ({ res, isRelease }) => {
-            if (isRelease) {
-                return _permission_denied(res, deletionPermissionError);
-            }
-            await WebSocket.deleteAll();
-            res.redirect(RouteStore.home);
-        }
-    });
-
     const EndpointHandlerMap = new Map<GoogleApiServerUtils.Action, GoogleApiServerUtils.ApiRouter>([
         ["create", (api, params) => api.create(params)],
         ["retrieve", (api, params) => api.get(params)],
@@ -628,7 +182,7 @@ function routeSetter(router: RouteManager) {
 
             let failed: GooglePhotosUploadFailure[] = [];
             const batched = BatchedArray.from<GooglePhotosUploadUtils.UploadSource>(media, { batchSize: 25 });
-            const newMediaItems = await batched.batchedMapPatientInterval<NewMediaItem>(
+            const newMediaItems = await batched.batchedMapPatientInterval<GooglePhotosUploadUtils.NewMediaItem>(
                 { magnitude: 100, unit: TimeUnit.Milliseconds },
                 async (batch, collector, { completedBatches }) => {
                     for (let index = 0; index < batch.length; index++) {
@@ -668,31 +222,6 @@ function routeSetter(router: RouteManager) {
 
     const downloadError = "Encountered an error while executing downloads.";
     const requestError = "Unable to execute download: the body's media items were malformed.";
-    const deletionPermissionError = "Cannot perform specialized delete outside of the development environment!";
-
-    router.addSupervisedRoute({
-        method: Method.GET,
-        subscription: "/deleteWithAux",
-        onValidation: async ({ res, isRelease }) => {
-            if (isRelease) {
-                return _permission_denied(res, deletionPermissionError);
-            }
-            await Database.Auxiliary.DeleteAll();
-            res.redirect(RouteStore.delete);
-        }
-    });
-
-    router.addSupervisedRoute({
-        method: Method.GET,
-        subscription: "/deleteWithGoogleCredentials",
-        onValidation: async ({ res, isRelease }) => {
-            if (isRelease) {
-                return _permission_denied(res, deletionPermissionError);
-            }
-            await Database.Auxiliary.GoogleAuthenticationToken.DeleteAll();
-            res.redirect(RouteStore.delete);
-        }
-    });
 
     const UploadError = (count: number) => `Unable to upload ${count} images to Dash's server`;
     router.addSupervisedRoute({
@@ -726,4 +255,9 @@ function routeSetter(router: RouteManager) {
             _invalid(res, requestError);
         }
     });
-}
\ No newline at end of file
+}
+
+(async function start() {
+    await preliminaryFunctions();
+    await initializeServer({ listenAtPort: 1050, routeSetter });
+})();
-- 
cgit v1.2.3-70-g09d2


From 36ad83493d2bd58dc6fe62df6002789ccc1b06a1 Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Sun, 10 Nov 2019 14:56:58 -0500
Subject: no more RouteStore

---
 src/Utils.ts                                       |  3 +-
 src/client/apis/GoogleAuthenticationManager.tsx    |  5 +--
 .../apis/google_docs/GoogleApiClientUtils.ts       |  7 ++--
 .../apis/google_docs/GooglePhotosClientUtils.ts    |  5 +--
 src/client/cognitive_services/CognitiveServices.ts |  3 +-
 src/client/util/History.ts                         |  3 +-
 .../util/Import & Export/DirectoryImportBox.tsx    |  3 +-
 src/client/util/Import & Export/ImageUtils.ts      |  7 ++--
 src/client/util/SharingManager.tsx                 |  3 +-
 src/client/views/MainView.tsx                      |  5 +--
 src/client/views/collections/CollectionSubView.tsx |  4 +-
 src/client/views/nodes/ImageBox.tsx                |  3 +-
 src/client/views/nodes/VideoBox.tsx                |  3 +-
 src/client/views/search/SearchBox.tsx              |  3 +-
 src/mobile/ImageUpload.tsx                         |  3 +-
 src/new_fields/RichTextUtils.ts                    |  3 +-
 src/server/ApiManagers/DeleteManager.ts            | 13 +++----
 src/server/ApiManagers/ExportManager.ts            |  5 +--
 src/server/ApiManagers/PDFManager.ts               |  2 +-
 src/server/ApiManagers/UploadManager.ts            | 15 +++-----
 src/server/ApiManagers/UserManager.ts              |  7 ++--
 src/server/ApiManagers/UtilManager.ts              |  7 ++++
 src/server/Initialization.ts                       | 28 +++++++-------
 src/server/RouteManager.ts                         |  5 +--
 src/server/RouteStore.ts                           | 45 ----------------------
 src/server/RouteSubscriber.ts                      |  2 +-
 src/server/authentication/config/passport.ts       |  5 +--
 .../authentication/controllers/user_controller.ts  | 32 +++++++--------
 .../authentication/models/current_user_utils.ts    |  7 ++--
 src/server/index.ts                                | 26 ++++++-------
 30 files changed, 96 insertions(+), 166 deletions(-)
 delete mode 100644 src/server/RouteStore.ts

(limited to 'src/client/util/Import & Export/DirectoryImportBox.tsx')

diff --git a/src/Utils.ts b/src/Utils.ts
index 9a2f01f80..abff2eaba 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -2,7 +2,6 @@ import v4 = require('uuid/v4');
 import v5 = require("uuid/v5");
 import { Socket } from 'socket.io';
 import { Message } from './server/Message';
-import { RouteStore } from './server/RouteStore';
 
 export namespace Utils {
 
@@ -46,7 +45,7 @@ export namespace Utils {
     }
 
     export function CorsProxy(url: string): string {
-        return prepend(RouteStore.corsProxy + "/") + encodeURIComponent(url);
+        return prepend("/corsProxy/") + encodeURIComponent(url);
     }
 
     export function CopyText(text: string) {
diff --git a/src/client/apis/GoogleAuthenticationManager.tsx b/src/client/apis/GoogleAuthenticationManager.tsx
index 1ec9d8412..ae77c4b7b 100644
--- a/src/client/apis/GoogleAuthenticationManager.tsx
+++ b/src/client/apis/GoogleAuthenticationManager.tsx
@@ -4,7 +4,6 @@ import * as React from "react";
 import MainViewModal from "../views/MainViewModal";
 import { Opt } from "../../new_fields/Doc";
 import { Networking } from "../Network";
-import { RouteStore } from "../../server/RouteStore";
 import "./GoogleAuthenticationManager.scss";
 
 const AuthenticationUrl = "https://accounts.google.com/o/oauth2/v2/auth";
@@ -31,7 +30,7 @@ export default class GoogleAuthenticationManager extends React.Component<{}> {
     }
 
     public fetchOrGenerateAccessToken = async () => {
-        let response = await Networking.FetchFromServer(RouteStore.readGoogleAccessToken);
+        let response = await Networking.FetchFromServer("/readGoogleAccessToken");
         // if this is an authentication url, activate the UI to register the new access token
         if (new RegExp(AuthenticationUrl).test(response)) {
             this.isOpen = true;
@@ -44,7 +43,7 @@ export default class GoogleAuthenticationManager extends React.Component<{}> {
                             return;
                         }
                         const { access_token, avatar, name } = await Networking.PostToServer(
-                            RouteStore.writeGoogleAccessToken,
+                            "/writeGoogleAccessToken",
                             { authenticationCode }
                         );
                         runInAction(() => {
diff --git a/src/client/apis/google_docs/GoogleApiClientUtils.ts b/src/client/apis/google_docs/GoogleApiClientUtils.ts
index 183679317..26c7f8d2e 100644
--- a/src/client/apis/google_docs/GoogleApiClientUtils.ts
+++ b/src/client/apis/google_docs/GoogleApiClientUtils.ts
@@ -1,5 +1,4 @@
 import { docs_v1, slides_v1 } from "googleapis";
-import { RouteStore } from "../../../server/RouteStore";
 import { Opt } from "../../../new_fields/Doc";
 import { isArray } from "util";
 import { EditorState } from "prosemirror-state";
@@ -77,7 +76,7 @@ export namespace GoogleApiClientUtils {
         * @returns the documentId of the newly generated document, or undefined if the creation process fails.
         */
         export const create = async (options: CreateOptions): Promise<CreationResult> => {
-            const path = `${RouteStore.googleDocs}/Documents/${Actions.Create}`;
+            const path = `/googleDocs/Documents/${Actions.Create}`;
             const parameters = {
                 requestBody: {
                     title: options.title || `Dash Export (${new Date().toDateString()})`
@@ -154,7 +153,7 @@ export namespace GoogleApiClientUtils {
         }
 
         export const retrieve = async (options: RetrieveOptions): Promise<RetrievalResult> => {
-            const path = `${RouteStore.googleDocs}/Documents/${Actions.Retrieve}`;
+            const path = `/googleDocs/Documents/${Actions.Retrieve}`;
             try {
                 const parameters = { documentId: options.documentId };
                 const schema: RetrievalResult = await Networking.PostToServer(path, parameters);
@@ -165,7 +164,7 @@ export namespace GoogleApiClientUtils {
         };
 
         export const update = async (options: UpdateOptions): Promise<UpdateResult> => {
-            const path = `${RouteStore.googleDocs}/Documents/${Actions.Update}`;
+            const path = `/googleDocs/Documents/${Actions.Update}`;
             const parameters = {
                 documentId: options.documentId,
                 requestBody: {
diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
index 402fc64b5..bf8897061 100644
--- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts
+++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
@@ -1,5 +1,4 @@
 import { Utils } from "../../../Utils";
-import { RouteStore } from "../../../server/RouteStore";
 import { ImageField } from "../../../new_fields/URLField";
 import { Cast, StrCast } from "../../../new_fields/Types";
 import { Doc, Opt, DocListCastAsync } from "../../../new_fields/Doc";
@@ -307,7 +306,7 @@ export namespace GooglePhotos {
         };
 
         export const WriteMediaItemsToServer = async (body: { mediaItems: any[] }): Promise<UploadInformation[]> => {
-            const uploads = await Networking.PostToServer(RouteStore.googlePhotosMediaDownload, body);
+            const uploads = await Networking.PostToServer("/googlePhotosMediaDownload", body);
             return uploads;
         };
 
@@ -345,7 +344,7 @@ export namespace GooglePhotos {
                 media.push({ url, description });
             }
             if (media.length) {
-                const results = await Networking.PostToServer(RouteStore.googlePhotosMediaUpload, { media, album });
+                const results = await Networking.PostToServer("/googlePhotosMediaUpload", { media, album });
                 return results;
             }
         };
diff --git a/src/client/cognitive_services/CognitiveServices.ts b/src/client/cognitive_services/CognitiveServices.ts
index 08fcb4883..af5fb39fc 100644
--- a/src/client/cognitive_services/CognitiveServices.ts
+++ b/src/client/cognitive_services/CognitiveServices.ts
@@ -2,7 +2,6 @@ import * as request from "request-promise";
 import { Doc, Field, Opt } from "../../new_fields/Doc";
 import { Cast } from "../../new_fields/Types";
 import { Docs } from "../documents/Documents";
-import { RouteStore } from "../../server/RouteStore";
 import { Utils } from "../../Utils";
 import { InkData } from "../../new_fields/InkField";
 import { UndoManager } from "../util/UndoManager";
@@ -39,7 +38,7 @@ export enum Confidence {
 export namespace CognitiveServices {
 
     const ExecuteQuery = async <D>(service: Service, manager: APIManager<D>, data: D): Promise<any> => {
-        return fetch(Utils.prepend(`${RouteStore.cognitiveServices}/${service}`)).then(async response => {
+        return fetch(Utils.prepend(`cognitiveServices/${service}`)).then(async response => {
             let apiKey = await response.text();
             if (!apiKey) {
                 console.log(`No API key found for ${service}: ensure index.ts has access to a .env file in your root directory`);
diff --git a/src/client/util/History.ts b/src/client/util/History.ts
index 899abbe40..1c51236cb 100644
--- a/src/client/util/History.ts
+++ b/src/client/util/History.ts
@@ -1,6 +1,5 @@
 import { Doc, Opt, Field } from "../../new_fields/Doc";
 import { DocServer } from "../DocServer";
-import { RouteStore } from "../../server/RouteStore";
 import { MainView } from "../views/MainView";
 import * as qs from 'query-string';
 import { Utils, OmitKeys } from "../../Utils";
@@ -26,7 +25,7 @@ export namespace HistoryUtil {
 
     // const handlers: ((state: ParsedUrl | null) => void)[] = [];
     function onHistory(e: PopStateEvent) {
-        if (window.location.pathname !== RouteStore.home) {
+        if (window.location.pathname !== "/home") {
             const url = e.state as ParsedUrl || parseUrl(window.location);
             if (url) {
                 switch (url.type) {
diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx
index 2e0ba25eb..437e7766b 100644
--- a/src/client/util/Import & Export/DirectoryImportBox.tsx	
+++ b/src/client/util/Import & Export/DirectoryImportBox.tsx	
@@ -1,7 +1,6 @@
 import "fs";
 import React = require("react");
 import { Doc, DocListCast, DocListCastAsync, Opt } from "../../../new_fields/Doc";
-import { RouteStore } from "../../../server/RouteStore";
 import { action, observable, autorun, runInAction, computed, reaction, IReactionDisposer } from "mobx";
 import { FieldViewProps, FieldView } from "../../views/nodes/FieldView";
 import Measure, { ContentRect } from "react-measure";
@@ -124,7 +123,7 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
                 formData.append(Utils.GenerateGuid(), file);
             });
 
-            collector.push(...(await Networking.PostFormDataToServer(RouteStore.upload, formData)));
+            collector.push(...(await Networking.PostFormDataToServer("/upload", formData)));
             runInAction(() => this.completed += batch.length);
         });
 
diff --git a/src/client/util/Import & Export/ImageUtils.ts b/src/client/util/Import & Export/ImageUtils.ts
index 914f4870a..ca80f3bca 100644
--- a/src/client/util/Import & Export/ImageUtils.ts	
+++ b/src/client/util/Import & Export/ImageUtils.ts	
@@ -1,7 +1,6 @@
-import { Doc, DocListCast, DocListCastAsync, Opt } from "../../../new_fields/Doc";
+import { Doc } from "../../../new_fields/Doc";
 import { ImageField } from "../../../new_fields/URLField";
 import { Cast, StrCast } from "../../../new_fields/Types";
-import { RouteStore } from "../../../server/RouteStore";
 import { Docs } from "../../documents/Documents";
 import { Networking } from "../../Network";
 import { Id } from "../../../new_fields/FieldSymbols";
@@ -15,7 +14,7 @@ export namespace ImageUtils {
             return false;
         }
         const source = field.url.href;
-        const response = await Networking.PostToServer(RouteStore.inspectImage, { source });
+        const response = await Networking.PostToServer("/inspectImage", { source });
         const { error, data } = response.exifData;
         document.exif = error || Docs.Get.DocumentHierarchyFromJson(data);
         return data !== undefined;
@@ -23,7 +22,7 @@ export namespace ImageUtils {
 
     export const ExportHierarchyToFileSystem = async (collection: Doc): Promise<void> => {
         const a = document.createElement("a");
-        a.href = Utils.prepend(`${RouteStore.imageHierarchyExport}/${collection[Id]}`);
+        a.href = Utils.prepend(`imageHierarchyExport/${collection[Id]}`);
         a.download = `Dash Export [${StrCast(collection.title)}].zip`;
         a.click();
     };
diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx
index 2082d6324..cc1d628b1 100644
--- a/src/client/util/SharingManager.tsx
+++ b/src/client/util/SharingManager.tsx
@@ -4,7 +4,6 @@ import MainViewModal from "../views/MainViewModal";
 import { Doc, Opt, DocCastAsync } from "../../new_fields/Doc";
 import { DocServer } from "../DocServer";
 import { Cast, StrCast } from "../../new_fields/Types";
-import { RouteStore } from "../../server/RouteStore";
 import * as RequestPromise from "request-promise";
 import { Utils } from "../../Utils";
 import "./SharingManager.scss";
@@ -104,7 +103,7 @@ export default class SharingManager extends React.Component<{}> {
     }
 
     populateUsers = async () => {
-        let userList = await RequestPromise.get(Utils.prepend(RouteStore.getUsers));
+        let userList = await RequestPromise.get(Utils.prepend("/getUsers"));
         const raw = JSON.parse(userList) as User[];
         const evaluating = raw.map(async user => {
             let isCandidate = user.email !== Doc.CurrentUserEmail;
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 39585113b..0c5a1003b 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -12,7 +12,6 @@ import { List } from '../../new_fields/List';
 import { listSpec } from '../../new_fields/Schema';
 import { Cast, FieldValue, StrCast } from '../../new_fields/Types';
 import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils';
-import { RouteStore } from '../../server/RouteStore';
 import { emptyFunction, returnEmptyString, returnFalse, returnOne, returnTrue, Utils } from '../../Utils';
 import GoogleAuthenticationManager from '../apis/GoogleAuthenticationManager';
 import { DocServer } from '../DocServer';
@@ -74,7 +73,7 @@ export class MainView extends React.Component {
         this._urlState = HistoryUtil.parseUrl(window.location) || {} as any;
         // causes errors to be generated when modifying an observable outside of an action
         configure({ enforceActions: "observed" });
-        if (window.location.pathname !== RouteStore.home) {
+        if (window.location.pathname !== "/home") {
             let pathname = window.location.pathname.substr(1).split("/");
             if (pathname.length > 1) {
                 let type = pathname[0];
@@ -395,7 +394,7 @@ export class MainView extends React.Component {
                     zoomToScale={emptyFunction}
                     getScale={returnOne}>
                 </DocumentView>
-                <button className="mainView-logout" key="logout" onClick={() => window.location.assign(Utils.prepend(RouteStore.logout))}>
+                <button className="mainView-logout" key="logout" onClick={() => window.location.assign(Utils.prepend("/logout"))}>
                     {CurrentUserUtils.GuestWorkspace ? "Exit" : "Log Out"}
                 </button>
             </div></div>;
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 6e8e4fa12..306f8e052 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -8,7 +8,6 @@ import { listSpec } from "../../../new_fields/Schema";
 import { ScriptField } from "../../../new_fields/ScriptField";
 import { Cast } from "../../../new_fields/Types";
 import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
-import { RouteStore } from "../../../server/RouteStore";
 import { Utils } from "../../../Utils";
 import { DocServer } from "../../DocServer";
 import { DocumentType } from "../../documents/DocumentTypes";
@@ -243,7 +242,6 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
             let promises: Promise<void>[] = [];
             // tslint:disable-next-line:prefer-for-of
             for (let i = 0; i < e.dataTransfer.items.length; i++) {
-                const upload = window.location.origin + RouteStore.upload;
                 let item = e.dataTransfer.items[i];
                 if (item.kind === "string" && item.type.indexOf("uri") !== -1) {
                     let str: string;
@@ -268,7 +266,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
                     }
                     let dropFileName = file ? file.name : "-empty-";
 
-                    let prom = fetch(upload, {
+                    let prom = fetch(Utils.prepend("/upload"), {
                         method: 'POST',
                         body: formData
                     }).then(async (res: Response) => {
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 9f39eccea..07fd832be 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -12,7 +12,6 @@ import { createSchema, listSpec, makeInterface } from '../../../new_fields/Schem
 import { ComputedField } from '../../../new_fields/ScriptField';
 import { BoolCast, Cast, FieldValue, NumCast, StrCast } from '../../../new_fields/Types';
 import { AudioField, ImageField } from '../../../new_fields/URLField';
-import { RouteStore } from '../../../server/RouteStore';
 import { Utils, returnOne, emptyFunction } from '../../../Utils';
 import { CognitiveServices, Confidence, Service, Tag } from '../../cognitive_services/CognitiveServices';
 import { Docs } from '../../documents/Documents';
@@ -152,7 +151,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
             recorder.ondataavailable = async function (e: any) {
                 const formData = new FormData();
                 formData.append("file", e.data);
-                const res = await fetch(Utils.prepend(RouteStore.upload), {
+                const res = await fetch(Utils.prepend("/upload"), {
                     method: 'POST',
                     body: formData
                 });
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index aa9b28118..4884fa2bf 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -6,7 +6,6 @@ import { InkTool } from "../../../new_fields/InkField";
 import { makeInterface, createSchema, listSpec } from "../../../new_fields/Schema";
 import { Cast, FieldValue, NumCast, BoolCast, StrCast } from "../../../new_fields/Types";
 import { VideoField } from "../../../new_fields/URLField";
-import { RouteStore } from "../../../server/RouteStore";
 import { Utils, emptyFunction, returnOne } from "../../../Utils";
 import { Docs, DocUtils } from "../../documents/Documents";
 import { ContextMenu } from "../ContextMenu";
@@ -183,7 +182,7 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
 
     public static async convertDataUri(imageUri: string, returnedFilename: string) {
         try {
-            let posting = Utils.prepend(RouteStore.dataUriToImage);
+            let posting = Utils.prepend("/uploadURI");
             const returnedUri = await rp.post(posting, {
                 body: {
                     uri: imageUri,
diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx
index b728ffaa9..6100c74df 100644
--- a/src/client/views/search/SearchBox.tsx
+++ b/src/client/views/search/SearchBox.tsx
@@ -8,7 +8,6 @@ import * as rp from 'request-promise';
 import { Doc } from '../../../new_fields/Doc';
 import { Id } from '../../../new_fields/FieldSymbols';
 import { Cast, NumCast } from '../../../new_fields/Types';
-import { RouteStore } from '../../../server/RouteStore';
 import { Utils } from '../../../Utils';
 import { Docs } from '../../documents/Documents';
 import { SetupDrag } from '../../util/DragManager';
@@ -90,7 +89,7 @@ export class SearchBox extends React.Component {
 
     public static async convertDataUri(imageUri: string, returnedFilename: string) {
         try {
-            let posting = Utils.prepend(RouteStore.dataUriToImage);
+            let posting = Utils.prepend("uploadURI");
             const returnedUri = await rp.post(posting, {
                 body: {
                     uri: imageUri,
diff --git a/src/mobile/ImageUpload.tsx b/src/mobile/ImageUpload.tsx
index 33a615cbf..9fdaac66e 100644
--- a/src/mobile/ImageUpload.tsx
+++ b/src/mobile/ImageUpload.tsx
@@ -1,7 +1,6 @@
 import * as ReactDOM from 'react-dom';
 import * as rp from 'request-promise';
 import { Docs } from '../client/documents/Documents';
-import { RouteStore } from '../server/RouteStore';
 import "./ImageUpload.scss";
 import React = require('react');
 import { DocServer } from '../client/DocServer';
@@ -58,7 +57,7 @@ class Uploader extends React.Component {
 
                         this.status = "getting user document";
 
-                        const res = await rp.get(Utils.prepend(RouteStore.getUserDocumentId));
+                        const res = await rp.get(Utils.prepend("/getUserDocumentId"));
                         if (!res) {
                             throw new Error("No user id returned");
                         }
diff --git a/src/new_fields/RichTextUtils.ts b/src/new_fields/RichTextUtils.ts
index 63d718ce8..238b27439 100644
--- a/src/new_fields/RichTextUtils.ts
+++ b/src/new_fields/RichTextUtils.ts
@@ -8,7 +8,6 @@ import { Opt, Doc } from "./Doc";
 import Color = require('color');
 import { sinkListItem } from "prosemirror-schema-list";
 import { Utils } from "../Utils";
-import { RouteStore } from "../server/RouteStore";
 import { Docs } from "../client/documents/Documents";
 import { schema } from "../client/util/RichTextSchema";
 import { GooglePhotos } from "../client/apis/google_docs/GooglePhotosClientUtils";
@@ -129,7 +128,7 @@ export namespace RichTextUtils {
                     return { baseUrl, filename };
                 });
 
-                const uploads = await Networking.PostToServer(RouteStore.googlePhotosMediaDownload, { mediaItems });
+                const uploads = await Networking.PostToServer("/googlePhotosMediaDownload", { mediaItems });
 
                 if (uploads.length !== mediaItems.length) {
                     throw new AssertionError({ expected: mediaItems.length, actual: uploads.length, message: "Error with internally uploading inlineObjects!" });
diff --git a/src/server/ApiManagers/DeleteManager.ts b/src/server/ApiManagers/DeleteManager.ts
index bbf1d0425..f58a28ce5 100644
--- a/src/server/ApiManagers/DeleteManager.ts
+++ b/src/server/ApiManagers/DeleteManager.ts
@@ -1,6 +1,5 @@
 import ApiManager, { Registration } from "./ApiManager";
 import { Method, _permission_denied } from "../RouteManager";
-import { RouteStore } from "../RouteStore";
 import { WebSocket } from "../Websocket/Websocket";
 import { Database } from "../database";
 
@@ -10,25 +9,25 @@ export default class DeleteManager extends ApiManager {
 
         register({
             method: Method.GET,
-            subscription: RouteStore.delete,
+            subscription: "/delete",
             onValidation: async ({ res, isRelease }) => {
                 if (isRelease) {
                     return _permission_denied(res, deletionPermissionError);
                 }
                 await WebSocket.deleteFields();
-                res.redirect(RouteStore.home);
+                res.redirect("/home");
             }
         });
 
         register({
             method: Method.GET,
-            subscription: RouteStore.deleteAll,
+            subscription: "/deleteAll",
             onValidation: async ({ res, isRelease }) => {
                 if (isRelease) {
                     return _permission_denied(res, deletionPermissionError);
                 }
                 await WebSocket.deleteAll();
-                res.redirect(RouteStore.home);
+                res.redirect("/home");
             }
         });
 
@@ -41,7 +40,7 @@ export default class DeleteManager extends ApiManager {
                     return _permission_denied(res, deletionPermissionError);
                 }
                 await Database.Auxiliary.DeleteAll();
-                res.redirect(RouteStore.delete);
+                res.redirect("/delete");
             }
         });
 
@@ -53,7 +52,7 @@ export default class DeleteManager extends ApiManager {
                     return _permission_denied(res, deletionPermissionError);
                 }
                 await Database.Auxiliary.GoogleAuthenticationToken.DeleteAll();
-                res.redirect(RouteStore.delete);
+                res.redirect("/delete");
             }
         });
 
diff --git a/src/server/ApiManagers/ExportManager.ts b/src/server/ApiManagers/ExportManager.ts
index d42db1056..fc6ba0d22 100644
--- a/src/server/ApiManagers/ExportManager.ts
+++ b/src/server/ApiManagers/ExportManager.ts
@@ -1,7 +1,6 @@
 import ApiManager, { Registration } from "./ApiManager";
 import { Method } from "../RouteManager";
 import RouteSubscriber from "../RouteSubscriber";
-import { RouteStore } from "../RouteStore";
 import * as Archiver from 'archiver';
 import * as express from 'express';
 import { Database } from "../database";
@@ -32,7 +31,7 @@ export default class DownloadManager extends ApiManager {
          */
         register({
             method: Method.GET,
-            subscription: new RouteSubscriber(RouteStore.imageHierarchyExport).add('docId'),
+            subscription: new RouteSubscriber("imageHierarchyExport").add('docId'),
             onValidation: async ({ req, res }) => {
                 const id = req.params.docId;
                 const hierarchy: Hierarchy = {};
@@ -43,7 +42,7 @@ export default class DownloadManager extends ApiManager {
 
         register({
             method: Method.GET,
-            subscription: new RouteSubscriber("/downloadId").add("docId"),
+            subscription: new RouteSubscriber("downloadId").add("docId"),
             onValidation: async ({ req, res }) => {
                 return BuildAndDispatchZip(res, async zip => {
                     const { id, docs, files } = await getDocs(req.params.docId);
diff --git a/src/server/ApiManagers/PDFManager.ts b/src/server/ApiManagers/PDFManager.ts
index f328557b4..632b4965a 100644
--- a/src/server/ApiManagers/PDFManager.ts
+++ b/src/server/ApiManagers/PDFManager.ts
@@ -15,7 +15,7 @@ export default class PDFManager extends ApiManager {
 
         register({
             method: Method.GET,
-            subscription: new RouteSubscriber("/thumbnail").add("filename"),
+            subscription: new RouteSubscriber("thumbnail").add("filename"),
             onValidation: ({ req, res }) => {
                 let filename = req.params.filename;
                 let noExt = filename.substring(0, filename.length - ".png".length);
diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts
index 38635eda5..01abdab54 100644
--- a/src/server/ApiManagers/UploadManager.ts
+++ b/src/server/ApiManagers/UploadManager.ts
@@ -6,7 +6,6 @@ var AdmZip = require('adm-zip');
 import * as path from 'path';
 import { createReadStream, createWriteStream, unlink, readFileSync } from "fs";
 import { publicDirectory, filesDirectory, Partitions } from "..";
-import { RouteStore } from "../RouteStore";
 import { Database } from "../database";
 import { DashUploadUtils } from "../DashUploadUtils";
 import { Opt } from "../../new_fields/Doc";
@@ -85,12 +84,8 @@ export default class UploadManager extends ApiManager {
                                     let dirname = path.dirname(entry.entryName) + "/";
                                     let extname = path.extname(entry.entryName);
                                     let basename = path.basename(entry.entryName).split(".")[0];
-                                    // zip.extractEntryTo(dirname + basename + "_o" + extname, __dirname + RouteStore.public, true, false);
-                                    // zip.extractEntryTo(dirname + basename + "_s" + extname, __dirname + RouteStore.public, true, false);
-                                    // zip.extractEntryTo(dirname + basename + "_m" + extname, __dirname + RouteStore.public, true, false);
-                                    // zip.extractEntryTo(dirname + basename + "_l" + extname, __dirname + RouteStore.public, true, false);
                                     try {
-                                        zip.extractEntryTo(entry.entryName, __dirname + RouteStore.public, true, false);
+                                        zip.extractEntryTo(entry.entryName, publicDirectory, true, false);
                                         dirname = "/" + dirname;
 
                                         createReadStream(publicDirectory + dirname + basename + extname).pipe(createWriteStream(publicDirectory + dirname + basename + "_o" + extname));
@@ -131,7 +126,7 @@ export default class UploadManager extends ApiManager {
 
         register({
             method: Method.POST,
-            subscription: RouteStore.upload,
+            subscription: "/upload",
             onValidation: async ({ req, res }) => {
                 let form = new formidable.IncomingForm();
                 form.uploadDir = filesDirectory;
@@ -147,7 +142,7 @@ export default class UploadManager extends ApiManager {
                                 let dataBuffer = readFileSync(filesDirectory + filename);
                                 const result: ParsedPDF = await pdf(dataBuffer);
                                 await new Promise<void>((resolve, reject) => {
-                                    const path = filesDirectory + Partitions.PdfText + "/" + filename.substring(0, filename.length - ".pdf".length) + ".txt";
+                                    const path = filesDirectory + Partitions.pdf_text + "/" + filename.substring(0, filename.length - ".pdf".length) + ".txt";
                                     createWriteStream(path).write(result.text, error => {
                                         if (!error) {
                                             resolve();
@@ -171,7 +166,7 @@ export default class UploadManager extends ApiManager {
 
         register({
             method: Method.POST,
-            subscription: RouteStore.inspectImage,
+            subscription: "/inspectImage",
             onValidation: async ({ req, res }) => {
                 const { source } = req.body;
                 if (typeof source === "string") {
@@ -184,7 +179,7 @@ export default class UploadManager extends ApiManager {
 
         register({
             method: Method.POST,
-            subscription: RouteStore.dataUriToImage,
+            subscription: "/uploadURI",
             onValidation: ({ req, res }) => {
                 const uri = req.body.uri;
                 const filename = req.body.name;
diff --git a/src/server/ApiManagers/UserManager.ts b/src/server/ApiManagers/UserManager.ts
index fe1ce7f2b..51a434fcf 100644
--- a/src/server/ApiManagers/UserManager.ts
+++ b/src/server/ApiManagers/UserManager.ts
@@ -1,7 +1,6 @@
 import ApiManager, { Registration } from "./ApiManager";
 import { Method } from "../RouteManager";
 import { WebSocket } from "../Websocket/Websocket";
-import { RouteStore } from "../RouteStore";
 import { Database } from "../database";
 
 export default class UserManager extends ApiManager {
@@ -10,7 +9,7 @@ export default class UserManager extends ApiManager {
 
         register({
             method: Method.GET,
-            subscription: RouteStore.getUsers,
+            subscription: "/getUsers",
             onValidation: async ({ res }) => {
                 const cursor = await Database.Instance.query({}, { email: 1, userDocumentId: 1 }, "users");
                 const results = await cursor.toArray();
@@ -20,13 +19,13 @@ export default class UserManager extends ApiManager {
 
         register({
             method: Method.GET,
-            subscription: RouteStore.getUserDocumentId,
+            subscription: "/getUserDocumentId",
             onValidation: ({ res, user }) => res.send(user.userDocumentId)
         });
 
         register({
             method: Method.GET,
-            subscription: RouteStore.getCurrUser,
+            subscription: "/getCurrentUser",
             onValidation: ({ res, user }) => res.send(JSON.stringify(user)),
             onUnauthenticated: ({ res }) => res.send(JSON.stringify({ id: "__guest__", email: "" }))
         });
diff --git a/src/server/ApiManagers/UtilManager.ts b/src/server/ApiManagers/UtilManager.ts
index 61cda2e9b..c1234be6c 100644
--- a/src/server/ApiManagers/UtilManager.ts
+++ b/src/server/ApiManagers/UtilManager.ts
@@ -2,11 +2,18 @@ import ApiManager, { Registration } from "./ApiManager";
 import { Method } from "../RouteManager";
 import { exec } from 'child_process';
 import { command_line } from "../ActionUtilities";
+import RouteSubscriber from "../RouteSubscriber";
 
 export default class UtilManager extends ApiManager {
 
     protected initialize(register: Registration): void {
 
+        register({
+            method: Method.GET,
+            subscription: new RouteSubscriber("environment").add("key"),
+            onValidation: ({ req, res }) => res.send(process.env[req.params.key])
+        });
+
         register({
             method: Method.GET,
             subscription: "/pull",
diff --git a/src/server/Initialization.ts b/src/server/Initialization.ts
index fbb5ae7a6..306058d81 100644
--- a/src/server/Initialization.ts
+++ b/src/server/Initialization.ts
@@ -9,7 +9,6 @@ import flash = require('connect-flash');
 import { Database } from './database';
 import { getForgot, getLogin, getLogout, getReset, getSignup, postForgot, postLogin, postReset, postSignup } from './authentication/controllers/user_controller';
 const MongoStore = require('connect-mongo')(session);
-import { RouteStore } from './RouteStore';
 import RouteManager from './RouteManager';
 import * as webpack from 'webpack';
 const config = require('../../webpack.config');
@@ -18,6 +17,8 @@ import * as wdm from 'webpack-dev-middleware';
 import * as whm from 'webpack-hot-middleware';
 import * as fs from 'fs';
 import * as request from 'request';
+import RouteSubscriber from './RouteSubscriber';
+import { publicDirectory } from '.';
 
 export type RouteSetter = (server: RouteManager) => void;
 export interface InitializationOptions {
@@ -29,8 +30,8 @@ export default async function InitializeServer(options: InitializationOptions) {
     const { listenAtPort, routeSetter } = options;
     const server = buildWithMiddleware(express());
 
-    server.use(express.static(__dirname + RouteStore.public));
-    server.use(RouteStore.images, express.static(__dirname + RouteStore.public));
+    server.use(express.static(publicDirectory));
+    server.use("/images", express.static(publicDirectory));
 
     server.use(wdm(compiler, { publicPath: config.output.publicPath }));
     server.use(whm(compiler));
@@ -87,24 +88,25 @@ function determineEnvironment() {
 }
 
 function registerAuthenticationRoutes(server: express.Express) {
-    server.get(RouteStore.signup, getSignup);
-    server.post(RouteStore.signup, postSignup);
+    server.get("/signup", getSignup);
+    server.post("/signup", postSignup);
 
-    server.get(RouteStore.login, getLogin);
-    server.post(RouteStore.login, postLogin);
+    server.get("/login", getLogin);
+    server.post("/login", postLogin);
 
-    server.get(RouteStore.logout, getLogout);
+    server.get("/logout", getLogout);
 
-    server.get(RouteStore.forgot, getForgot);
-    server.post(RouteStore.forgot, postForgot);
+    server.get("/forgotPassword", getForgot);
+    server.post("/forgotPassword", postForgot);
 
-    server.get(RouteStore.reset, getReset);
-    server.post(RouteStore.reset, postReset);
+    const reset = new RouteSubscriber("resetPassword").add("token").build;
+    server.get(reset, getReset);
+    server.post(reset, postReset);
 }
 
 function registerCorsProxy(server: express.Express) {
     const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/;
-    server.use(RouteStore.corsProxy, (req, res) => {
+    server.use("/corsProxy", (req, res) => {
         req.pipe(request(decodeURIComponent(req.url.substring(1)))).on("response", res => {
             const headers = Object.keys(res.headers);
             headers.forEach(headerName => {
diff --git a/src/server/RouteManager.ts b/src/server/RouteManager.ts
index c1d38327f..3aae5734a 100644
--- a/src/server/RouteManager.ts
+++ b/src/server/RouteManager.ts
@@ -1,5 +1,4 @@
 import RouteSubscriber from "./RouteSubscriber";
-import { RouteStore } from "./RouteStore";
 import { DashUserModel } from "./authentication/models/user_model";
 import * as express from 'express';
 
@@ -67,10 +66,10 @@ export default class RouteManager {
                 if (onUnauthenticated) {
                     await tryExecute(onUnauthenticated, core);
                     if (!res.headersSent) {
-                        res.redirect(RouteStore.login);
+                        res.redirect("/login");
                     }
                 } else {
-                    res.redirect(RouteStore.login);
+                    res.redirect("/login");
                 }
             }
             setTimeout(() => {
diff --git a/src/server/RouteStore.ts b/src/server/RouteStore.ts
deleted file mode 100644
index a310d0c95..000000000
--- a/src/server/RouteStore.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-// PREPEND ALL ROUTES WITH FORWARD SLASHES!
-
-export enum RouteStore {
-    // GENERAL
-    root = "/",
-    home = "/home",
-    corsProxy = "/corsProxy",
-    delete = "/delete",
-    deleteAll = "/deleteAll",
-    pull = "/pull",
-
-    // UPLOAD AND STATIC FILE SERVING
-    public = "/public",
-    upload = "/upload",
-    dataUriToImage = "/uploadURI",
-    images = "/images",
-    inspectImage = "/inspectImage",
-    imageHierarchyExport = "/imageHierarchyExport",
-
-    // USER AND WORKSPACES
-    getCurrUser = "/getCurrentUser",
-    getUsers = "/getUsers",
-    getUserDocumentId = "/getUserDocumentId",
-    updateCursor = "/updateCursor",
-
-    openDocumentWithId = "/doc/:docId",
-
-    // AUTHENTICATION
-    signup = "/signup",
-    login = "/login",
-    logout = "/logout",
-    forgot = "/forgotpassword",
-    reset = "/reset/:token",
-
-    // APIS
-    cognitiveServices = "/cognitiveservices",
-    googleDocs = "/googleDocs",
-    readGoogleAccessToken = "/readGoogleAccessToken",
-    writeGoogleAccessToken = "/writeGoogleAccessToken",
-    googlePhotosMediaUpload = "/googlePhotosMediaUpload",
-    googlePhotosMediaDownload = "/googlePhotosMediaDownload",
-    googleDocsGet = "/googleDocsGet",
-    checkGoogle = "/checkGoogleAuthentication"
-
-}
\ No newline at end of file
diff --git a/src/server/RouteSubscriber.ts b/src/server/RouteSubscriber.ts
index e49be8af5..a1cf7c1c4 100644
--- a/src/server/RouteSubscriber.ts
+++ b/src/server/RouteSubscriber.ts
@@ -3,7 +3,7 @@ export default class RouteSubscriber {
     private requestParameters: string[] = [];
 
     constructor(root: string) {
-        this._root = root;
+        this._root = `/${root}`;
     }
 
     add(...parameters: string[]) {
diff --git a/src/server/authentication/config/passport.ts b/src/server/authentication/config/passport.ts
index 8915a4abf..0b15c3a36 100644
--- a/src/server/authentication/config/passport.ts
+++ b/src/server/authentication/config/passport.ts
@@ -3,7 +3,6 @@ import * as passportLocal from 'passport-local';
 import _ from "lodash";
 import { default as User } from '../models/user_model';
 import { Request, Response, NextFunction } from "express";
-import { RouteStore } from '../../RouteStore';
 
 const LocalStrategy = passportLocal.Strategy;
 
@@ -35,13 +34,13 @@ export let isAuthenticated = (req: Request, res: Response, next: NextFunction) =
     if (req.isAuthenticated()) {
         return next();
     }
-    return res.redirect(RouteStore.login);
+    return res.redirect("/login");
 };
 
 export let isAuthorized = (req: Request, res: Response, next: NextFunction) => {
     const provider = req.path.split("/").slice(-1)[0];
 
-    if (_.find((req.user as any).tokens, { kind: provider })) {
+    if (_.find((req.user).tokens, { kind: provider })) {
         next();
     } else {
         res.redirect(`/auth/${provider}`);
diff --git a/src/server/authentication/controllers/user_controller.ts b/src/server/authentication/controllers/user_controller.ts
index f5c6e1610..b2b9d33f6 100644
--- a/src/server/authentication/controllers/user_controller.ts
+++ b/src/server/authentication/controllers/user_controller.ts
@@ -10,10 +10,7 @@ import * as pug from 'pug';
 import * as async from 'async';
 import * as nodemailer from 'nodemailer';
 import c = require("crypto");
-import { RouteStore } from "../../RouteStore";
 import { Utils } from "../../../Utils";
-import { Schema } from "mongoose";
-import { Opt } from "../../../new_fields/Doc";
 import { MailOptions } from "nodemailer/lib/stream-transport";
 
 /**
@@ -23,8 +20,7 @@ import { MailOptions } from "nodemailer/lib/stream-transport";
  */
 export let getSignup = (req: Request, res: Response) => {
     if (req.user) {
-        let user = req.user;
-        return res.redirect(RouteStore.home);
+        return res.redirect("/home");
     }
     res.render("signup.pug", {
         title: "Sign Up",
@@ -45,7 +41,7 @@ export let postSignup = (req: Request, res: Response, next: NextFunction) => {
     const errors = req.validationErrors();
 
     if (errors) {
-        return res.redirect(RouteStore.signup);
+        return res.redirect("/signup");
     }
 
     const email = req.body.email as String;
@@ -62,7 +58,7 @@ export let postSignup = (req: Request, res: Response, next: NextFunction) => {
     User.findOne({ email }, (err, existingUser) => {
         if (err) { return next(err); }
         if (existingUser) {
-            return res.redirect(RouteStore.login);
+            return res.redirect("/login");
         }
         user.save((err: any) => {
             if (err) { return next(err); }
@@ -81,7 +77,7 @@ let tryRedirectToTarget = (req: Request, res: Response) => {
         req.session.target = undefined;
         res.redirect(target);
     } else {
-        res.redirect(RouteStore.home);
+        res.redirect("/home");
     }
 };
 
@@ -93,7 +89,7 @@ let tryRedirectToTarget = (req: Request, res: Response) => {
 export let getLogin = (req: Request, res: Response) => {
     if (req.user) {
         req.session!.target = undefined;
-        return res.redirect(RouteStore.home);
+        return res.redirect("/home");
     }
     res.render("login.pug", {
         title: "Log In",
@@ -115,13 +111,13 @@ export let postLogin = (req: Request, res: Response, next: NextFunction) => {
 
     if (errors) {
         req.flash("errors", "Unable to login at this time. Please try again.");
-        return res.redirect(RouteStore.signup);
+        return res.redirect("/signup");
     }
 
     passport.authenticate("local", (err: Error, user: DashUserModel, info: IVerifyOptions) => {
         if (err) { next(err); return; }
         if (!user) {
-            return res.redirect(RouteStore.signup);
+            return res.redirect("/signup");
         }
         req.logIn(user, (err) => {
             if (err) { next(err); return; }
@@ -141,7 +137,7 @@ export let getLogout = (req: Request, res: Response) => {
     if (sess) {
         sess.destroy((err) => { if (err) { console.log(err); } });
     }
-    res.redirect(RouteStore.login);
+    res.redirect("/login");
 };
 
 export let getForgot = function (req: Request, res: Response) {
@@ -168,7 +164,7 @@ export let postForgot = function (req: Request, res: Response, next: NextFunctio
             User.findOne({ email }, function (err, user: DashUserModel) {
                 if (!user) {
                     // NO ACCOUNT WITH SUBMITTED EMAIL
-                    res.redirect(RouteStore.forgot);
+                    res.redirect("/forgotPassword");
                     return;
                 }
                 user.passwordResetToken = token;
@@ -192,7 +188,7 @@ export let postForgot = function (req: Request, res: Response, next: NextFunctio
                 subject: 'Dash Password Reset',
                 text: 'You are receiving this because you (or someone else) have requested the reset of the password for your account.\n\n' +
                     'Please click on the following link, or paste this into your browser to complete the process:\n\n' +
-                    'http://' + req.headers.host + '/reset/' + token + '\n\n' +
+                    'http://' + req.headers.host + '/resetPassword/' + token + '\n\n' +
                     'If you did not request this, please ignore this email and your password will remain unchanged.\n'
             } as MailOptions;
             smtpTransport.sendMail(mailOptions, function (err: Error | null) {
@@ -202,14 +198,14 @@ export let postForgot = function (req: Request, res: Response, next: NextFunctio
         }
     ], function (err) {
         if (err) return next(err);
-        res.redirect(RouteStore.forgot);
+        res.redirect("/forgotPassword");
     });
 };
 
 export let getReset = function (req: Request, res: Response) {
     User.findOne({ passwordResetToken: req.params.token, passwordResetExpires: { $gt: Date.now() } }, function (err, user: DashUserModel) {
         if (!user || err) {
-            return res.redirect(RouteStore.forgot);
+            return res.redirect("/forgotPassword");
         }
         res.render("reset.pug", {
             title: "Reset Password",
@@ -239,7 +235,7 @@ export let postReset = function (req: Request, res: Response) {
 
                 user.save(function (err) {
                     if (err) {
-                        res.redirect(RouteStore.login);
+                        res.redirect("/login");
                         return;
                     }
                     req.logIn(user, function (err) {
@@ -271,6 +267,6 @@ export let postReset = function (req: Request, res: Response) {
             });
         }
     ], function (err) {
-        res.redirect(RouteStore.login);
+        res.redirect("/login");
     });
 };
\ No newline at end of file
diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts
index 73cac879e..5a8815983 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -11,7 +11,6 @@ import { List } from "../../../new_fields/List";
 import { listSpec } from "../../../new_fields/Schema";
 import { Cast, StrCast, PromiseValue } from "../../../new_fields/Types";
 import { Utils } from "../../../Utils";
-import { RouteStore } from "../../RouteStore";
 import { ScriptField } from "../../../new_fields/ScriptField";
 import { ButtonBox } from "../../../client/views/nodes/ButtonBox";
 import { UndoManager } from "../../../client/util/UndoManager";
@@ -198,8 +197,8 @@ export class CurrentUserUtils {
         return doc;
     }
 
-    public static loadCurrentUser() {
-        return rp.get(Utils.prepend(RouteStore.getCurrUser)).then(response => {
+    public static async loadCurrentUser() {
+        return rp.get(Utils.prepend("/getCurrentUser")).then(response => {
             if (response) {
                 const result: { id: string, email: string } = JSON.parse(response);
                 return result;
@@ -212,7 +211,7 @@ export class CurrentUserUtils {
     public static async loadUserDocument({ id, email }: { id: string, email: string }) {
         this.curr_id = id;
         Doc.CurrentUserEmail = email;
-        await rp.get(Utils.prepend(RouteStore.getUserDocumentId)).then(id => {
+        await rp.get(Utils.prepend("/getUserDocumentId")).then(id => {
             if (id && id !== "guest") {
                 return DocServer.GetRefField(id).then(async field => {
                     if (field instanceof Doc) {
diff --git a/src/server/index.ts b/src/server/index.ts
index aec301a74..8fc402cc9 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -3,7 +3,6 @@ import { GoogleApiServerUtils } from "./apis/google/GoogleApiServerUtils";
 import * as mobileDetect from 'mobile-detect';
 import * as path from 'path';
 import { Database } from './database';
-import { RouteStore } from './RouteStore';
 const serverPort = 4321;
 import { GooglePhotosUploadUtils } from './apis/google/GooglePhotosUploadUtils';
 import { Opt } from '../new_fields/Doc';
@@ -23,7 +22,7 @@ import DeleteManager from "./ApiManagers/DeleteManager";
 import PDFManager from "./ApiManagers/PDFManager";
 import UploadManager from "./ApiManagers/UploadManager";
 
-export const publicDirectory = __dirname + RouteStore.public;
+export const publicDirectory = __dirname + "/public";
 export const filesDirectory = publicDirectory + "/files/";
 export enum Partitions {
     pdf_text,
@@ -73,13 +72,12 @@ function routeSetter(router: RouteManager) {
     WebSocket.initialize(serverPort, router.isRelease);
 
     /**
-     * Anyone attempting to navigate to localhost at this port will
-     * first have to log in.
+     * Accessing root index redirects to home
      */
     router.addSupervisedRoute({
         method: Method.GET,
-        subscription: RouteStore.root,
-        onValidation: ({ res }) => res.redirect(RouteStore.home)
+        subscription: "/",
+        onValidation: ({ res }) => res.redirect("/home")
     });
 
     const serve: OnUnauthenticated = ({ req, res }) => {
@@ -90,7 +88,7 @@ function routeSetter(router: RouteManager) {
 
     router.addSupervisedRoute({
         method: Method.GET,
-        subscription: [RouteStore.home, new RouteSubscriber("/doc").add("docId")],
+        subscription: ["/home", new RouteSubscriber("doc").add("docId")],
         onValidation: serve,
         onUnauthenticated: ({ req, ...remaining }) => {
             const { originalUrl: target } = req;
@@ -110,9 +108,9 @@ function routeSetter(router: RouteManager) {
 
     router.addSupervisedRoute({
         method: Method.GET,
-        subscription: new RouteSubscriber(RouteStore.cognitiveServices).add('requestedservice'),
+        subscription: new RouteSubscriber("cognitiveServices").add('requestedService'),
         onValidation: ({ req, res }) => {
-            let service = req.params.requestedservice;
+            let service = req.params.requestedService;
             res.send(ServicesApiKeyMap.get(service));
         }
     });
@@ -125,7 +123,7 @@ function routeSetter(router: RouteManager) {
 
     router.addSupervisedRoute({
         method: Method.POST,
-        subscription: new RouteSubscriber(RouteStore.googleDocs).add("sector", "action"),
+        subscription: new RouteSubscriber("googleDocs").add("sector", "action"),
         onValidation: async ({ req, res, user }) => {
             let sector: GoogleApiServerUtils.Service = req.params.sector as GoogleApiServerUtils.Service;
             let action: GoogleApiServerUtils.Action = req.params.action as GoogleApiServerUtils.Action;
@@ -143,7 +141,7 @@ function routeSetter(router: RouteManager) {
 
     router.addSupervisedRoute({
         method: Method.GET,
-        subscription: RouteStore.readGoogleAccessToken,
+        subscription: "/readGoogleAccessToken",
         onValidation: async ({ user, res }) => {
             const userId = user.id;
             const token = await GoogleApiServerUtils.retrieveAccessToken(userId);
@@ -156,7 +154,7 @@ function routeSetter(router: RouteManager) {
 
     router.addSupervisedRoute({
         method: Method.POST,
-        subscription: RouteStore.writeGoogleAccessToken,
+        subscription: "/writeGoogleAccessToken",
         onValidation: async ({ user, req, res }) => {
             res.send(await GoogleApiServerUtils.processNewUser(user.id, req.body.authenticationCode));
         }
@@ -173,7 +171,7 @@ function routeSetter(router: RouteManager) {
 
     router.addSupervisedRoute({
         method: Method.POST,
-        subscription: RouteStore.googlePhotosMediaUpload,
+        subscription: "/googlePhotosMediaUpload",
         onValidation: async ({ user, req, res }) => {
             const { media } = req.body;
 
@@ -228,7 +226,7 @@ function routeSetter(router: RouteManager) {
     const UploadError = (count: number) => `Unable to upload ${count} images to Dash's server`;
     router.addSupervisedRoute({
         method: Method.POST,
-        subscription: RouteStore.googlePhotosMediaDownload,
+        subscription: "/googlePhotosMediaDownload",
         onValidation: async ({ req, res }) => {
             const contents: { mediaItems: MediaItem[] } = req.body;
             let failed = 0;
-- 
cgit v1.2.3-70-g09d2


From df5584ccd40bd83f1362b32db67969e7ffbf2e3f Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Wed, 27 Nov 2019 04:03:30 -0500
Subject: improved file partitioning in server and generified upload method

---
 package.json                                       |   2 +
 src/client/documents/Documents.ts                  |   1 -
 src/client/util/ClientDiagnostics.ts               |  11 +-
 .../util/Import & Export/DirectoryImportBox.tsx    |  19 +-
 src/client/views/Main.tsx                          |   2 +-
 src/client/views/collections/CollectionSubView.tsx |  22 +-
 src/client/views/pdf/PDFViewer.tsx                 |   3 +-
 src/server/ActionUtilities.ts                      |  17 +-
 src/server/ApiManagers/DownloadManager.ts          |   4 +-
 src/server/ApiManagers/GooglePhotosManager.ts      |   4 +-
 src/server/ApiManagers/PDFManager.ts               |  34 +-
 src/server/ApiManagers/SearchManager.ts            |   4 +-
 src/server/ApiManagers/UploadManager.ts            |  47 ++-
 src/server/DashUploadUtils.ts                      | 388 +++++++++++----------
 src/server/SharedMediaTypes.ts                     |   5 +-
 src/server/database.ts                             |   4 +-
 src/server/index.ts                                |   4 +-
 17 files changed, 314 insertions(+), 257 deletions(-)

(limited to 'src/client/util/Import & Export/DirectoryImportBox.tsx')

diff --git a/package.json b/package.json
index 393df8574..3725d76eb 100644
--- a/package.json
+++ b/package.json
@@ -105,6 +105,7 @@
         "@types/react-table": "^6.7.22",
         "@types/request": "^2.48.1",
         "@types/request-promise": "^4.1.42",
+        "@types/rimraf": "^2.0.3",
         "@types/sharp": "^0.22.2",
         "@types/shelljs": "^0.8.5",
         "@types/socket.io": "^2.1.2",
@@ -211,6 +212,7 @@
         "readline": "^1.3.0",
         "request": "^2.88.0",
         "request-promise": "^4.2.4",
+        "rimraf": "^3.0.0",
         "serializr": "^1.5.1",
         "sharp": "^0.22.1",
         "shelljs": "^0.8.3",
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index dea057b93..a2f4d23c7 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -377,7 +377,6 @@ export namespace Docs {
                 let extension = path.extname(target);
                 target = `${target.substring(0, target.length - extension.length)}_o${extension}`;
             }
-            // if (target !== "http://www.cs.brown.edu/") {
             requestImageSize(target)
                 .then((size: any) => {
                     let aspect = size.height / size.width;
diff --git a/src/client/util/ClientDiagnostics.ts b/src/client/util/ClientDiagnostics.ts
index 24f196252..7eef935fd 100644
--- a/src/client/util/ClientDiagnostics.ts
+++ b/src/client/util/ClientDiagnostics.ts
@@ -1,9 +1,6 @@
-import { observable, runInAction } from "mobx";
-import { MainView } from "../views/MainView";
-
 export namespace ClientDiagnostics {
 
-    export function start() {
+    export async function start() {
 
         let serverPolls = 0;
         const serverHandle = setInterval(async () => {
@@ -17,14 +14,16 @@ export namespace ClientDiagnostics {
 
 
         let executed = false;
-        const solrHandle = setInterval(async () => {
+        const handle = async () => {
             const response = await fetch("/solrHeartbeat");
             if (!(await response.json()).running) {
                 !executed && alert("Looks like SOLR is not running on your machine.");
                 executed = true;
                 clearInterval(solrHandle);
             }
-        }, 1000 * 15);
+        };
+        await handle();
+        const solrHandle = setInterval(handle, 1000 * 15);
 
     }
 
diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx
index f0880f193..16ae50685 100644
--- a/src/client/util/Import & Export/DirectoryImportBox.tsx	
+++ b/src/client/util/Import & Export/DirectoryImportBox.tsx	
@@ -22,18 +22,10 @@ import "./DirectoryImportBox.scss";
 import { Networking } from "../../Network";
 import { BatchedArray } from "array-batcher";
 import * as path from 'path';
-import { DashUploadUtils } from "../../../server/DashUploadUtils";
-import { SharedMediaTypes } from "../../../server/SharedMediaTypes";
+import { AcceptibleMedia } from "../../../server/SharedMediaTypes";
 
 const unsupported = ["text/html", "text/plain"];
 
-interface ImageUploadResponse {
-    name: string;
-    path: string;
-    type: string;
-    exif: any;
-}
-
 @observer
 export default class DirectoryImportBox extends React.Component<FieldViewProps> {
     private selector = React.createRef<HTMLInputElement>();
@@ -98,7 +90,7 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
             let file = files.item(i);
             if (file && !unsupported.includes(file.type)) {
                 const ext = path.extname(file.name).toLowerCase();
-                if (SharedMediaTypes.imageFormats.includes(ext)) {
+                if (AcceptibleMedia.imageFormats.includes(ext)) {
                     validated.push(file);
                 }
             }
@@ -114,7 +106,7 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
 
         runInAction(() => this.phase = `Internal: uploading ${this.quota - this.completed} files to Dash...`);
 
-        const uploads = await BatchedArray.from(validated, { batchSize: 15 }).batchedMapAsync<ImageUploadResponse>(async (batch, collector) => {
+        const uploads = await BatchedArray.from(validated, { batchSize: 15 }).batchedMapAsync<any>(async (batch, collector) => {
             const formData = new FormData();
 
             batch.forEach(file => {
@@ -127,16 +119,17 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
             runInAction(() => this.completed += batch.length);
         });
 
+        const size = "_o";
         await Promise.all(uploads.map(async upload => {
             const type = upload.type;
-            const path = Utils.prepend(upload.path);
+            const path = Utils.prepend(upload.clientAccessPath);
             const options = {
                 nativeWidth: 300,
                 width: 300,
                 title: upload.name
             };
             const document = await Docs.Get.DocumentFromType(type, path, options);
-            const { data, error } = upload.exif;
+            const { data, error } = upload.exifData;
             if (document) {
                 Doc.GetProto(document).exif = error || Docs.Get.DocumentHierarchyFromJson(data);
                 docs.push(document);
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index dec4a24e4..9e699978f 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -10,7 +10,7 @@ import { ClientDiagnostics } from "../util/ClientDiagnostics";
 AssignAllExtensions();
 
 (async () => {
-    ClientDiagnostics.start();
+    await ClientDiagnostics.start();
     const info = await CurrentUserUtils.loadCurrentUser();
     DocServer.init(window.location.protocol, window.location.hostname, 4321, info.email);
     await Docs.Prototypes.initialize();
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 1c3ff37ee..a1bd1527e 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -22,6 +22,7 @@ import React = require("react");
 var path = require('path');
 import { GooglePhotos } from "../../apis/google_docs/GooglePhotosClientUtils";
 import { ImageUtils } from "../../util/Import & Export/ImageUtils";
+import { Networking } from "../../Network";
 
 export interface CollectionViewProps extends FieldViewProps {
     addDocument: (document: Doc) => boolean;
@@ -271,28 +272,25 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
                     let file = item.getAsFile();
                     let formData = new FormData();
 
-                    if (file) {
-                        formData.append('file', file);
+                    if (!file || !file.type) {
+                        continue;
                     }
-                    let dropFileName = file ? file.name : "-empty-";
 
-                    let prom = fetch(Utils.prepend("/upload"), {
-                        method: 'POST',
-                        body: formData
-                    }).then(async (res: Response) => {
-                        (await res.json()).map(action((file: any) => {
+                    formData.append('file', file);
+                    let dropFileName = file ? file.name : "-empty-";
+                    promises.push(Networking.PostFormDataToServer("/upload", formData).then(results => {
+                        results.map(action((file: any) => {
                             let full = { ...options, nativeWidth: type.indexOf("video") !== -1 ? 600 : 300, width: 300, title: dropFileName };
-                            let pathname = Utils.prepend(file.path);
+                            let pathname = Utils.prepend(file.clientAccessPath);
                             Docs.Get.DocumentFromType(type, pathname, full).then(doc => {
                                 doc && (Doc.GetProto(doc).fileUpload = path.basename(pathname).replace("upload_", "").replace(/\.[a-z0-9]*$/, ""));
                                 doc && this.props.addDocument(doc);
                             });
                         }));
-                    });
-                    promises.push(prom);
+                    }));
                 }
             }
-            if (text) {
+            if (text && !text.includes("https://")) {
                 this.props.addDocument(Docs.Create.TextDocument({ ...options, documentText: "@@@" + text, width: 400, height: 315 }));
                 return;
             }
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index b737ce221..c075a4f99 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -125,7 +125,8 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
         !this.props.Document.lockedTransform && (this.props.Document.lockedTransform = true);
         // change the address to be the file address of the PNG version of each page
         // file address of the pdf
-        this._coverPath = JSON.parse(await rp.get(Utils.prepend(`/thumbnail${this.props.url.substring("files/".length, this.props.url.length - ".pdf".length)}-${(this.Document.curPage || 1)}.PNG`)));
+        const path = Utils.prepend(`/thumbnail${this.props.url.substring("files/pdfs/".length, this.props.url.length - ".pdf".length)}-${(this.Document.curPage || 1)}.png`);
+        this._coverPath = JSON.parse(await rp.get(path));
         runInAction(() => this._showWaiting = this._showCover = true);
         this.props.startupLive && this.setupPdfJsViewer();
         this._searchReactionDisposer = reaction(() => this.Document.search_string, searchString => {
diff --git a/src/server/ActionUtilities.ts b/src/server/ActionUtilities.ts
index 7f493dd70..c9fc86fea 100644
--- a/src/server/ActionUtilities.ts
+++ b/src/server/ActionUtilities.ts
@@ -2,6 +2,7 @@ import * as fs from 'fs';
 import { ExecOptions } from 'shelljs';
 import { exec } from 'child_process';
 import * as path from 'path';
+import * as rimraf from "rimraf";
 
 export const command_line = (command: string, fromDirectory?: string) => {
     return new Promise<string>((resolve, reject) => {
@@ -68,4 +69,18 @@ export function msToTime(duration: number) {
     let secondsS = (seconds < 10) ? "0" + seconds : seconds;
 
     return hoursS + ":" + minutesS + ":" + secondsS + "." + milliseconds;
-}
\ No newline at end of file
+}
+
+export const createIfNotExists = async (path: string) => {
+    if (await new Promise<boolean>(resolve => fs.exists(path, resolve))) {
+        return true;
+    }
+    return new Promise<boolean>(resolve => fs.mkdir(path, error => resolve(error === null)));
+};
+
+export async function Prune(rootDirectory: string): Promise<boolean> {
+    const error = await new Promise<Error>(resolve => rimraf(rootDirectory, resolve));
+    return error === null;
+}
+
+export const Destroy = (mediaPath: string) => new Promise<boolean>(resolve => fs.unlink(mediaPath, error => resolve(error === null)));
\ No newline at end of file
diff --git a/src/server/ApiManagers/DownloadManager.ts b/src/server/ApiManagers/DownloadManager.ts
index fc6ba0d22..5bad46eda 100644
--- a/src/server/ApiManagers/DownloadManager.ts
+++ b/src/server/ApiManagers/DownloadManager.ts
@@ -5,7 +5,7 @@ import * as Archiver from 'archiver';
 import * as express from 'express';
 import { Database } from "../database";
 import * as path from "path";
-import { DashUploadUtils } from "../DashUploadUtils";
+import { DashUploadUtils, SizeSuffix } from "../DashUploadUtils";
 import { publicDirectory } from "..";
 
 export type Hierarchy = { [id: string]: string | Hierarchy };
@@ -254,7 +254,7 @@ async function writeHierarchyRecursive(file: Archiver.Archiver, hierarchy: Hiera
                 // and dropped in the browser and thus hosted remotely) so we upload it
                 // to our server and point the zip file to it, so it can bundle up the bytes
                 const information = await DashUploadUtils.UploadImage(result);
-                path = information.mediaPaths[0];
+                path = information.serverAccessPaths[SizeSuffix.Original];
             }
             // write the file specified by the path to the directory in the
             // zip file given by the prefix.
diff --git a/src/server/ApiManagers/GooglePhotosManager.ts b/src/server/ApiManagers/GooglePhotosManager.ts
index c7af69375..5a709688b 100644
--- a/src/server/ApiManagers/GooglePhotosManager.ts
+++ b/src/server/ApiManagers/GooglePhotosManager.ts
@@ -86,10 +86,10 @@ export default class GooglePhotosManager extends ApiManager {
                 const contents: { mediaItems: MediaItem[] } = req.body;
                 let failed = 0;
                 if (contents) {
-                    const completed: Opt<DashUploadUtils.UploadInformation>[] = [];
+                    const completed: Opt<DashUploadUtils.ImageUploadInformation>[] = [];
                     for (let item of contents.mediaItems) {
                         const { contentSize, ...attributes } = await DashUploadUtils.InspectImage(item.baseUrl);
-                        const found: Opt<DashUploadUtils.UploadInformation> = await Database.Auxiliary.QueryUploadHistory(contentSize!);
+                        const found: Opt<DashUploadUtils.ImageUploadInformation> = await Database.Auxiliary.QueryUploadHistory(contentSize!);
                         if (!found) {
                             const upload = await DashUploadUtils.UploadInspectedImage({ contentSize, ...attributes }, item.filename, prefix).catch(error => _error(res, downloadError, error));
                             if (upload) {
diff --git a/src/server/ApiManagers/PDFManager.ts b/src/server/ApiManagers/PDFManager.ts
index 632b4965a..4bd750aaf 100644
--- a/src/server/ApiManagers/PDFManager.ts
+++ b/src/server/ApiManagers/PDFManager.ts
@@ -2,12 +2,12 @@ import ApiManager, { Registration } from "./ApiManager";
 import { Method } from "../RouteManager";
 import RouteSubscriber from "../RouteSubscriber";
 import { exists, createReadStream, createWriteStream } from "fs";
-import { filesDirectory } from "..";
 import * as Pdfjs from 'pdfjs-dist';
 import { createCanvas } from "canvas";
 const probe = require("probe-image-size");
 import * as express from "express";
 import * as path from "path";
+import { Directory, serverPathToFile, clientPathToFile } from "./UploadManager";
 
 export default class PDFManager extends ApiManager {
 
@@ -21,21 +21,27 @@ export default class PDFManager extends ApiManager {
                 let noExt = filename.substring(0, filename.length - ".png".length);
                 let pagenumber = parseInt(noExt.split('-')[1]);
                 return new Promise<void>(resolve => {
-                    exists(filesDirectory + filename, (exists: boolean) => {
-                        console.log(`${filesDirectory + filename} ${exists ? "exists" : "does not exist"}`);
+                    const path = serverPathToFile(Directory.pdf_thumbnails, filename);
+                    exists(path, (exists: boolean) => {
+                        console.log(`${path} ${exists ? "exists" : "does not exist"}`);
                         if (exists) {
-                            let input = createReadStream(filesDirectory + filename);
-                            probe(input, (err: any, result: any) => {
+                            let input = createReadStream(path);
+                            probe(input, (err: any, { width, height }: any) => {
                                 if (err) {
                                     console.log(err);
                                     console.log(`error on ${filename}`);
                                     return;
                                 }
-                                res.send({ path: "/files/" + filename, width: result.width, height: result.height });
+                                res.send({
+                                    path: clientPathToFile(Directory.pdf_thumbnails, filename),
+                                    width,
+                                    height
+                                });
                             });
                         }
                         else {
-                            LoadPage(filesDirectory + filename.substring(0, filename.length - noExt.split('-')[1].length - ".PNG".length - 1) + ".pdf", pagenumber, res);
+                            const name = filename.substring(0, filename.length - noExt.split('-')[1].length - ".PNG".length - 1) + ".pdf";
+                            LoadPage(serverPathToFile(Directory.pdfs, name), pagenumber, res);
                         }
                         resolve();
                     });
@@ -55,8 +61,8 @@ export default class PDFManager extends ApiManager {
                         let canvasAndContext = factory.create(viewport.width, viewport.height);
                         let renderContext = {
                             canvasContext: canvasAndContext.context,
-                            viewport: viewport,
-                            canvasFactory: factory
+                            canvasFactory: factory,
+                            viewport
                         };
                         console.log("read " + pageNumber);
 
@@ -64,13 +70,17 @@ export default class PDFManager extends ApiManager {
                             .then(() => {
                                 console.log("saving " + pageNumber);
                                 let stream = canvasAndContext.canvas.createPNGStream();
-                                let pngFile = `${file.substring(0, file.length - ".pdf".length)}-${pageNumber}.PNG`;
+                                let filenames = path.basename(file).split(".");
+                                const pngFile = serverPathToFile(Directory.pdf_thumbnails, `${filenames[0]}-${pageNumber}.png`);
                                 let out = createWriteStream(pngFile);
                                 stream.pipe(out);
                                 out.on("finish", () => {
                                     console.log(`Success! Saved to ${pngFile}`);
-                                    let name = path.basename(pngFile);
-                                    res.send({ path: "/files/" + name, width: viewport.width, height: viewport.height });
+                                    res.send({
+                                        path: pngFile,
+                                        width: viewport.width,
+                                        height: viewport.height
+                                    });
                                 });
                             }, (reason: string) => {
                                 console.error(reason + ` ${pageNumber}`);
diff --git a/src/server/ApiManagers/SearchManager.ts b/src/server/ApiManagers/SearchManager.ts
index 1c801715a..d3f8995b0 100644
--- a/src/server/ApiManagers/SearchManager.ts
+++ b/src/server/ApiManagers/SearchManager.ts
@@ -3,7 +3,7 @@ import { Method } from "../RouteManager";
 import { Search } from "../Search";
 var findInFiles = require('find-in-files');
 import * as path from 'path';
-import { filesDirectory } from "..";
+import { pathToDirectory, Directory } from "./UploadManager";
 
 export default class SearchManager extends ApiManager {
 
@@ -18,7 +18,7 @@ export default class SearchManager extends ApiManager {
                     res.send([]);
                     return;
                 }
-                let results = await findInFiles.find({ 'term': q, 'flags': 'ig' }, filesDirectory + "text", ".txt$");
+                let results = await findInFiles.find({ 'term': q, 'flags': 'ig' }, pathToDirectory(Directory.text), ".txt$");
                 let resObj: { ids: string[], numFound: number, lines: string[] } = { ids: [], numFound: 0, lines: [] };
                 for (var result in results) {
                     resObj.ids.push(path.basename(result, ".txt").replace(/upload_/, ""));
diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts
index 2a9faacd8..2f76871a6 100644
--- a/src/server/ApiManagers/UploadManager.ts
+++ b/src/server/ApiManagers/UploadManager.ts
@@ -7,14 +7,33 @@ import { extname, basename, dirname } from 'path';
 import { createReadStream, createWriteStream, unlink, readFileSync } from "fs";
 import { publicDirectory, filesDirectory } from "..";
 import { Database } from "../database";
-import { DashUploadUtils } from "../DashUploadUtils";
-import { Opt } from "../../new_fields/Doc";
-import { ParsedPDF } from "../PdfTypes";
-const pdf = require('pdf-parse');
+import { DashUploadUtils, SizeSuffix } from "../DashUploadUtils";
 import * as sharp from 'sharp';
-import { SharedMediaTypes } from "../SharedMediaTypes";
+import { AcceptibleMedia } from "../SharedMediaTypes";
+import { normalize } from "path";
 const imageDataUri = require('image-data-uri');
 
+export enum Directory {
+    parsed_files = "parsed_files",
+    images = "images",
+    videos = "videos",
+    pdfs = "pdfs",
+    text = "text",
+    pdf_thumbnails = "pdf_thumbnails"
+}
+
+export function serverPathToFile(directory: Directory, filename: string) {
+    return normalize(`${filesDirectory}/${directory}/${filename}`);
+}
+
+export function pathToDirectory(directory: Directory) {
+    return normalize(`${filesDirectory}/${directory}`);
+}
+
+export function clientPathToFile(directory: Directory, filename: string) {
+    return `/files/${directory}/${filename}`;
+}
+
 export default class UploadManager extends ApiManager {
 
     protected initialize(register: Registration): void {
@@ -129,13 +148,14 @@ export default class UploadManager extends ApiManager {
             subscription: "/upload",
             onValidation: async ({ req, res }) => {
                 let form = new formidable.IncomingForm();
-                form.uploadDir = filesDirectory;
+                form.uploadDir = pathToDirectory(Directory.parsed_files);
                 form.keepExtensions = true;
                 return new Promise<void>(resolve => {
                     form.parse(req, async (_err, _fields, files) => {
                         let results: any[] = [];
                         for (const key in files) {
-                            results.push(DashUploadUtils.upload(files[key]));
+                            const result = await DashUploadUtils.upload(files[key]);
+                            result && results.push(result);
                         }
                         _success(res, results);
                         resolve();
@@ -150,8 +170,8 @@ export default class UploadManager extends ApiManager {
             onValidation: async ({ req, res }) => {
                 const { source } = req.body;
                 if (typeof source === "string") {
-                    const uploadInformation = await DashUploadUtils.UploadImage(source);
-                    return res.send(await DashUploadUtils.InspectImage(uploadInformation.mediaPaths[0]));
+                    const { serverAccessPaths } = await DashUploadUtils.UploadImage(source);
+                    return res.send(await DashUploadUtils.InspectImage(serverAccessPaths[SizeSuffix.Original]));
                 }
                 res.send({});
             }
@@ -167,9 +187,9 @@ export default class UploadManager extends ApiManager {
                     res.status(401).send("incorrect parameters specified");
                     return;
                 }
-                return imageDataUri.outputFile(uri, filesDirectory + filename).then((savedName: string) => {
+                return imageDataUri.outputFile(uri, serverPathToFile(Directory.images, filename)).then((savedName: string) => {
                     const ext = extname(savedName).toLowerCase();
-                    const { pngs, jpgs } = SharedMediaTypes;
+                    const { pngs, jpgs } = AcceptibleMedia;
                     let resizers = [
                         { resizer: sharp().resize(100, undefined, { withoutEnlargement: true }), suffix: "_s" },
                         { resizer: sharp().resize(400, undefined, { withoutEnlargement: true }), suffix: "_m" },
@@ -189,10 +209,11 @@ export default class UploadManager extends ApiManager {
                     }
                     if (isImage) {
                         resizers.forEach(resizer => {
-                            createReadStream(savedName).pipe(resizer.resizer).pipe(createWriteStream(filesDirectory + filename + resizer.suffix + ext));
+                            const path = serverPathToFile(Directory.images, filename + resizer.suffix + ext);
+                            createReadStream(savedName).pipe(resizer.resizer).pipe(createWriteStream(path));
                         });
                     }
-                    res.send("/files/" + filename + ext);
+                    res.send(clientPathToFile(Directory.images, filename + ext));
                 });
             }
         });
diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts
index 839aada4b..0a670ec01 100644
--- a/src/server/DashUploadUtils.ts
+++ b/src/server/DashUploadUtils.ts
@@ -5,18 +5,32 @@ import * as sharp from 'sharp';
 import request = require('request-promise');
 import { ExifData, ExifImage } from 'exif';
 import { Opt } from '../new_fields/Doc';
-import { SharedMediaTypes } from './SharedMediaTypes';
+import { AcceptibleMedia } from './SharedMediaTypes';
 import { filesDirectory } from '.';
 import { File } from 'formidable';
-import { extname, basename } from "path";
+import { basename } from "path";
+import { ConsoleColors, createIfNotExists } from './ActionUtilities';
+import { ParsedPDF } from "../server/PdfTypes";
+const parse = require('pdf-parse');
+import { Directory, serverPathToFile, clientPathToFile } from './ApiManagers/UploadManager';
 
-const uploadDirectory = path.join(__dirname, './public/files/');
+export enum SizeSuffix {
+    Small = "_s",
+    Medium = "_m",
+    Large = "_l",
+    Original = "_o"
+}
 
 export namespace DashUploadUtils {
 
+    function InjectSize(filename: string, size: SizeSuffix) {
+        const extension = path.extname(filename).toLowerCase();
+        return filename.substring(0, filename.length - extension.length) + size + extension;
+    }
+
     export interface Size {
         width: number;
-        suffix: string;
+        suffix: SizeSuffix;
     }
 
     export interface ImageFileResponse {
@@ -27,215 +41,221 @@ export namespace DashUploadUtils {
     }
 
     export const Sizes: { [size: string]: Size } = {
-        SMALL: { width: 100, suffix: "_s" },
-        MEDIUM: { width: 400, suffix: "_m" },
-        LARGE: { width: 900, suffix: "_l" },
+        SMALL: { width: 100, suffix: SizeSuffix.Small },
+        MEDIUM: { width: 400, suffix: SizeSuffix.Medium },
+        LARGE: { width: 900, suffix: SizeSuffix.Large },
     };
 
     export function validateExtension(url: string) {
-        return SharedMediaTypes.imageFormats.includes(path.extname(url).toLowerCase());
+        return AcceptibleMedia.imageFormats.includes(path.extname(url).toLowerCase());
     }
 
     const size = "content-length";
     const type = "content-type";
 
-    export interface UploadInformation {
-        mediaPaths: string[];
-        fileNames: { [key: string]: string };
+    export interface ImageUploadInformation {
+        clientAccessPath: string;
+        serverAccessPaths: { [key: string]: string };
         exifData: EnrichedExifData;
         contentSize?: number;
         contentType?: string;
     }
 
-    export function upload(file: File): any {
+    export async function upload(file: File): Promise<any> {
         const { type, path, name } = file;
-        const filename = basename(path);
-        const extension = extname(path).toLowerCase();
-        if (extension === ".pdf") {
-
-        } else if  {
-            let partition: Opt<string>;
-            if(imageFormats.includes(extension)) {
-            partition = DashUploadUtils.Partitions.images;
-        } else if (videoFormats.includes(extension)) {
-            partition = DashUploadUtils.Partitions.videos;
-        }
-        let uploadInformation: Opt<DashUploadUtils.UploadInformation>;
-        if (partition) {
-            uploadInformation = await DashUploadUtils.UploadImage(`${filesDirectory}/${partition}/${filename}`, filename);
-        } else {
-            console.log(`Unable to accommodate, and ignored, the following file upload: ${filename}`);
+        const { imageFormats, videoFormats, applicationFormats } = AcceptibleMedia;
+        const types = type.split("/");
+
+        const category = types[0];
+        const format = `.${types[1]}`;
+
+        switch (category) {
+            case "image":
+                if (imageFormats.includes(format)) {
+                    const { clientAccessPath } = await UploadImage(path, basename(path), format);
+                    return { clientAccessPath, name, type };
+                }
+            case "video":
+                if (videoFormats.includes(format)) {
+                    return MoveParsedFile(path, Directory.videos);
+                }
+            case "application":
+                if (applicationFormats.includes(format)) {
+                    return UploadPdf(path);
+                }
         }
+        console.log(ConsoleColors.Red, `Ignoring unsupported file ${name} with upload type (${type}).`);
+        return { clientAccessPath: undefined };
     }
-    const exif = uploadInformation ? uploadInformation.exifData : undefined;
-    results.push({ name, type, path: `/files/${filename}`, exif });
 
-}
+    async function UploadPdf(absolutePath: string) {
+        let dataBuffer = fs.readFileSync(absolutePath);
+        const result: ParsedPDF = await parse(dataBuffer);
+        const parsedName = basename(absolutePath);
+        await new Promise<void>((resolve, reject) => {
+            const textFilename = `${parsedName.substring(0, parsedName.length - 4)}.txt`;
+            const writeStream = fs.createWriteStream(serverPathToFile(Directory.text, textFilename));
+            writeStream.write(result.text, error => error ? reject(error) : resolve());
+        });
+        return MoveParsedFile(absolutePath, Directory.pdfs);
+    }
 
-const generate = (prefix: string, url: string) => `${prefix}upload_${Utils.GenerateGuid()}${sanitizeExtension(url)}`;
-const sanitize = (filename: string) => filename.replace(/\s+/g, "_");
-const sanitizeExtension = (source: string) => {
-    let extension = path.extname(source);
-    extension = extension.toLowerCase();
-    extension = extension.split("?")[0];
-    return extension;
-};
-
-/**
- * Uploads an image specified by the @param source to Dash's /public/files/
- * directory, and returns information generated during that upload 
- * 
- * @param {string} source is either the absolute path of an already uploaded image or
- * the url of a remote image
- * @param {string} filename dictates what to call the image. If not specified,
- * the name {@param prefix}_upload_{GUID}
- * @param {string} prefix is a string prepended to the generated image name in the
- * event that @param filename is not specified
- * 
- * @returns {UploadInformation} This method returns
- * 1) the paths to the uploaded images (plural due to resizing)
- * 2) the file name of each of the resized images
- * 3) the size of the image, in bytes (4432130)
- * 4) the content type of the image, i.e. image/(jpeg | png | ...)
- */
-export const UploadImage = async (source: string, filename?: string, prefix: string = ""): Promise<UploadInformation> => {
-    const metadata = await InspectImage(source);
-    return UploadInspectedImage(metadata, filename, prefix);
-};
-
-export interface InspectionResults {
-    isLocal: boolean;
-    stream: any;
-    normalizedUrl: string;
-    exifData: EnrichedExifData;
-    contentSize?: number;
-    contentType?: string;
-}
+    const generate = (prefix: string, url: string) => `${prefix}upload_${Utils.GenerateGuid()}${sanitizeExtension(url)}`;
+    const sanitizeExtension = (source: string) => {
+        let extension = path.extname(source);
+        extension = extension.toLowerCase();
+        extension = extension.split("?")[0];
+        return extension;
+    };
 
-export interface EnrichedExifData {
-    data: ExifData;
-    error?: string;
-}
+    /**
+     * Uploads an image specified by the @param source to Dash's /public/files/
+     * directory, and returns information generated during that upload 
+     * 
+     * @param {string} source is either the absolute path of an already uploaded image or
+     * the url of a remote image
+     * @param {string} filename dictates what to call the image. If not specified,
+     * the name {@param prefix}_upload_{GUID}
+     * @param {string} prefix is a string prepended to the generated image name in the
+     * event that @param filename is not specified
+     * 
+     * @returns {ImageUploadInformation} This method returns
+     * 1) the paths to the uploaded images (plural due to resizing)
+     * 2) the file name of each of the resized images
+     * 3) the size of the image, in bytes (4432130)
+     * 4) the content type of the image, i.e. image/(jpeg | png | ...)
+     */
+    export const UploadImage = async (source: string, filename?: string, format?: string, prefix: string = ""): Promise<ImageUploadInformation> => {
+        const metadata = await InspectImage(source);
+        return UploadInspectedImage(metadata, filename, format, prefix);
+    };
 
-export enum Partitions {
-    pdf_text = "pdf_text",
-    images = "images",
-    videos = "videos"
-}
+    export interface InspectionResults {
+        isLocal: boolean;
+        stream: any;
+        normalizedUrl: string;
+        exifData: EnrichedExifData;
+        contentSize?: number;
+        contentType?: string;
+    }
 
-export async function buildFilePartitions() {
-    const pending = Object.keys(Partitions).map(sub => createIfNotExists(filesDirectory + sub));
-    return Promise.all(pending);
-}
+    export interface EnrichedExifData {
+        data: ExifData;
+        error?: string;
+    }
 
-/**
- * Based on the url's classification as local or remote, gleans
- * as much information as possible about the specified image
- * 
- * @param source is the path or url to the image in question
- */
-export const InspectImage = async (source: string): Promise<InspectionResults> => {
-    const { isLocal, stream, normalized: normalizedUrl } = classify(source);
-    const exifData = await parseExifData(source);
-    const results = {
-        exifData,
-        isLocal,
-        stream,
-        normalizedUrl
-    };
-    // stop here if local, since request.head() can't handle local paths, only urls on the web
-    if (isLocal) {
-        return results;
+    export async function buildFileDirectories() {
+        const pending = Object.keys(Directory).map(sub => createIfNotExists(`${filesDirectory}/${sub}`));
+        return Promise.all(pending);
     }
-    const metadata = (await new Promise<any>((resolve, reject) => {
-        request.head(source, async (error, res) => {
-            if (error) {
-                return reject(error);
-            }
-            resolve(res);
-        });
-    })).headers;
-    return {
-        contentSize: parseInt(metadata[size]),
-        contentType: metadata[type],
-        ...results
-    };
-};
-
-export const UploadInspectedImage = async (metadata: InspectionResults, filename?: string, prefix = ""): Promise<UploadInformation> => {
-    const { isLocal, stream, normalizedUrl, contentSize, contentType, exifData } = metadata;
-    const resolved = filename ? sanitize(filename) : generate(prefix, normalizedUrl);
-    const extension = sanitizeExtension(normalizedUrl || resolved);
-    let information: UploadInformation = {
-        mediaPaths: [],
-        fileNames: { clean: resolved },
-        exifData,
-        contentSize,
-        contentType,
-    };
-    const { pngs, jpgs } = SharedMediaTypes;
-    return new Promise<UploadInformation>(async (resolve, reject) => {
-        const resizers = [
-            { resizer: sharp().rotate(), suffix: "_o" },
-            ...Object.values(Sizes).map(size => ({
-                resizer: sharp().resize(size.width, undefined, { withoutEnlargement: true }).rotate(),
-                suffix: size.suffix
-            }))
-        ];
-        if (pngs.includes(extension)) {
-            resizers.forEach(element => element.resizer = element.resizer.png());
-        } else if (jpgs.includes(extension)) {
-            resizers.forEach(element => element.resizer = element.resizer.jpeg());
-        }
-        for (let resizer of resizers) {
-            const suffix = resizer.suffix;
-            let mediaPath: string;
-            await new Promise<void>(resolve => {
-                const filename = resolved.substring(0, resolved.length - extension.length) + suffix + extension;
-                information.mediaPaths.push(mediaPath = uploadDirectory + filename);
-                information.fileNames[suffix] = filename;
-                stream(normalizedUrl).pipe(resizer.resizer).pipe(fs.createWriteStream(mediaPath))
-                    .on('close', resolve)
-                    .on('error', reject);
-            });
+
+    /**
+     * Based on the url's classification as local or remote, gleans
+     * as much information as possible about the specified image
+     * 
+     * @param source is the path or url to the image in question
+     */
+    export const InspectImage = async (source: string): Promise<InspectionResults> => {
+        const { isLocal, stream, normalized: normalizedUrl } = classify(source);
+        const exifData = await parseExifData(source);
+        const results = {
+            exifData,
+            isLocal,
+            stream,
+            normalizedUrl
+        };
+        // stop here if local, since request.head() can't handle local paths, only urls on the web
+        if (isLocal) {
+            return results;
         }
-        if (!isLocal) {
-            await new Promise<void>(resolve => {
-                stream(normalizedUrl).pipe(fs.createWriteStream(uploadDirectory + resolved)).on('close', resolve);
+        const metadata = (await new Promise<any>((resolve, reject) => {
+            request.head(source, async (error, res) => {
+                if (error) {
+                    return reject(error);
+                }
+                resolve(res);
             });
-        }
-        resolve(information);
-    });
-};
-
-const classify = (url: string) => {
-    const isLocal = /Dash-Web(\\|\/)src(\\|\/)server(\\|\/)public(\\|\/)files/g.test(url);
-    return {
-        isLocal,
-        stream: isLocal ? fs.createReadStream : request,
-        normalized: isLocal ? path.normalize(url) : url
+        })).headers;
+        return {
+            contentSize: parseInt(metadata[size]),
+            contentType: metadata[type],
+            ...results
+        };
     };
-};
-
-const parseExifData = async (source: string): Promise<EnrichedExifData> => {
-    return new Promise<EnrichedExifData>(resolve => {
-        new ExifImage(source, (error, data) => {
-            let reason: Opt<string> = undefined;
-            if (error) {
-                reason = (error as any).code;
+
+    export async function MoveParsedFile(absolutePath: string, destination: Directory): Promise<{ clientAccessPath: Opt<string> }> {
+        return new Promise<{ clientAccessPath: Opt<string> }>(resolve => {
+            const filename = basename(absolutePath);
+            const destinationPath = serverPathToFile(destination, filename);
+            fs.rename(absolutePath, destinationPath, error => {
+                resolve({ clientAccessPath: error ? undefined : clientPathToFile(destination, filename) });
+            });
+        });
+    }
+
+    export const UploadInspectedImage = async (metadata: InspectionResults, filename?: string, format?: string, prefix = ""): Promise<ImageUploadInformation> => {
+        const { isLocal, stream, normalizedUrl, contentSize, contentType, exifData } = metadata;
+        const resolved = filename || generate(prefix, normalizedUrl);
+        const extension = format || sanitizeExtension(normalizedUrl || resolved);
+        let information: ImageUploadInformation = {
+            clientAccessPath: clientPathToFile(Directory.images, resolved),
+            serverAccessPaths: {},
+            exifData,
+            contentSize,
+            contentType,
+        };
+        const { pngs, jpgs } = AcceptibleMedia;
+        return new Promise<ImageUploadInformation>(async (resolve, reject) => {
+            const resizers = [
+                { resizer: sharp().rotate(), suffix: SizeSuffix.Original },
+                ...Object.values(Sizes).map(size => ({
+                    resizer: sharp().resize(size.width, undefined, { withoutEnlargement: true }).rotate(),
+                    suffix: size.suffix
+                }))
+            ];
+            if (pngs.includes(extension)) {
+                resizers.forEach(element => element.resizer = element.resizer.png());
+            } else if (jpgs.includes(extension)) {
+                resizers.forEach(element => element.resizer = element.resizer.jpeg());
+            }
+            for (let { resizer, suffix } of resizers) {
+                let mediaPath: string;
+                await new Promise<void>(resolve => {
+                    const filename = InjectSize(resolved, suffix);
+                    information.serverAccessPaths[suffix] = serverPathToFile(Directory.images, filename);
+                    stream(normalizedUrl).pipe(resizer).pipe(fs.createWriteStream(serverPathToFile(Directory.images, filename)))
+                        .on('close', resolve)
+                        .on('error', reject);
+                });
             }
-            resolve({ data, error: reason });
+            if (isLocal) {
+                await new Promise<boolean>(resolve => {
+                    fs.unlink(normalizedUrl, error => resolve(error === null));
+                });
+            }
+            resolve(information);
         });
-    });
-};
+    };
 
-export const createIfNotExists = async (path: string) => {
-    if (await new Promise<boolean>(resolve => fs.exists(path, resolve))) {
-        return true;
-    }
-    return new Promise<boolean>(resolve => fs.mkdir(path, error => resolve(error === null)));
-};
+    const classify = (url: string) => {
+        const isLocal = /Dash-Web(\\|\/)src(\\|\/)server(\\|\/)public(\\|\/)files/g.test(url);
+        return {
+            isLocal,
+            stream: isLocal ? fs.createReadStream : request,
+            normalized: isLocal ? path.normalize(url) : url
+        };
+    };
 
-export const Destroy = (mediaPath: string) => new Promise<boolean>(resolve => fs.unlink(mediaPath, error => resolve(error === null)));
+    const parseExifData = async (source: string): Promise<EnrichedExifData> => {
+        return new Promise<EnrichedExifData>(resolve => {
+            new ExifImage(source, (error, data) => {
+                let reason: Opt<string> = undefined;
+                if (error) {
+                    reason = (error as any).code;
+                }
+                resolve({ data, error: reason });
+            });
+        });
+    };
 
 }
\ No newline at end of file
diff --git a/src/server/SharedMediaTypes.ts b/src/server/SharedMediaTypes.ts
index 3d3234125..8d0f441f0 100644
--- a/src/server/SharedMediaTypes.ts
+++ b/src/server/SharedMediaTypes.ts
@@ -1,9 +1,8 @@
-export namespace SharedMediaTypes {
-
+export namespace AcceptibleMedia {
     export const gifs = [".gif"];
     export const pngs = [".png"];
     export const jpgs = [".jpg", ".jpeg"];
     export const imageFormats = [...pngs, ...jpgs, ...gifs];
     export const videoFormats = [".mov", ".mp4"];
-
+    export const applicationFormats = [".pdf"];
 }
\ No newline at end of file
diff --git a/src/server/database.ts b/src/server/database.ts
index b81fc03a4..db81245c1 100644
--- a/src/server/database.ts
+++ b/src/server/database.ts
@@ -293,7 +293,7 @@ export namespace Database {
         };
 
         export const QueryUploadHistory = async (contentSize: number) => {
-            return SanitizedSingletonQuery<DashUploadUtils.UploadInformation>({ contentSize }, AuxiliaryCollections.GooglePhotosUploadHistory);
+            return SanitizedSingletonQuery<DashUploadUtils.ImageUploadInformation>({ contentSize }, AuxiliaryCollections.GooglePhotosUploadHistory);
         };
 
         export namespace GoogleAuthenticationToken {
@@ -322,7 +322,7 @@ export namespace Database {
 
         }
 
-        export const LogUpload = async (information: DashUploadUtils.UploadInformation) => {
+        export const LogUpload = async (information: DashUploadUtils.ImageUploadInformation) => {
             const bundle = {
                 _id: Utils.GenerateDeterministicGuid(String(information.contentSize!)),
                 ...information
diff --git a/src/server/index.ts b/src/server/index.ts
index d02a6005e..d77923710 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -24,7 +24,7 @@ import GooglePhotosManager from "./ApiManagers/GooglePhotosManager";
 import DiagnosticManager from "./ApiManagers/DiagnosticManager";
 
 export const publicDirectory = path.resolve(__dirname, "public");
-export const filesDirectory = path.resolve(publicDirectory, "files") + "/";
+export const filesDirectory = path.resolve(publicDirectory, "files");
 
 /**
  * These are the functions run before the server starts
@@ -34,7 +34,7 @@ export const filesDirectory = path.resolve(publicDirectory, "files") + "/";
 async function preliminaryFunctions() {
     await GoogleCredentialsLoader.loadCredentials();
     GoogleApiServerUtils.processProjectCredentials();
-    await DashUploadUtils.buildFilePartitions();
+    await DashUploadUtils.buildFileDirectories();
     await log_execution({
         startMessage: "attempting to initialize mongodb connection",
         endMessage: "connection outcome determined",
-- 
cgit v1.2.3-70-g09d2


From db6d3f77b1b429c1942019b79c44e378eb8b1ee4 Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Wed, 27 Nov 2019 04:25:55 -0500
Subject: fixed google photos upload by appending size suffix

---
 src/client/util/Import & Export/DirectoryImportBox.tsx | 10 ++++------
 src/server/ApiManagers/GooglePhotosManager.ts          |  4 ++--
 src/server/DashUploadUtils.ts                          | 14 +++++++-------
 3 files changed, 13 insertions(+), 15 deletions(-)

(limited to 'src/client/util/Import & Export/DirectoryImportBox.tsx')

diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx
index 16ae50685..b5e806a97 100644
--- a/src/client/util/Import & Export/DirectoryImportBox.tsx	
+++ b/src/client/util/Import & Export/DirectoryImportBox.tsx	
@@ -119,17 +119,15 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
             runInAction(() => this.completed += batch.length);
         });
 
-        const size = "_o";
-        await Promise.all(uploads.map(async upload => {
-            const type = upload.type;
-            const path = Utils.prepend(upload.clientAccessPath);
+        await Promise.all(uploads.map(async ({ name, type, clientAccessPath, exifData }) => {
+            const path = Utils.prepend(clientAccessPath);
             const options = {
                 nativeWidth: 300,
                 width: 300,
-                title: upload.name
+                title: name
             };
             const document = await Docs.Get.DocumentFromType(type, path, options);
-            const { data, error } = upload.exifData;
+            const { data, error } = exifData;
             if (document) {
                 Doc.GetProto(document).exif = error || Docs.Get.DocumentHierarchyFromJson(data);
                 docs.push(document);
diff --git a/src/server/ApiManagers/GooglePhotosManager.ts b/src/server/ApiManagers/GooglePhotosManager.ts
index 5a709688b..4a0c0b936 100644
--- a/src/server/ApiManagers/GooglePhotosManager.ts
+++ b/src/server/ApiManagers/GooglePhotosManager.ts
@@ -5,7 +5,7 @@ import { GoogleApiServerUtils } from "../apis/google/GoogleApiServerUtils";
 import { BatchedArray, TimeUnit } from "array-batcher";
 import { GooglePhotosUploadUtils } from "../apis/google/GooglePhotosUploadUtils";
 import { Opt } from "../../new_fields/Doc";
-import { DashUploadUtils } from "../DashUploadUtils";
+import { DashUploadUtils, InjectSize, SizeSuffix } from "../DashUploadUtils";
 import { Database } from "../database";
 
 const authenticationError = "Unable to authenticate Google credentials before uploading to Google Photos!";
@@ -55,7 +55,7 @@ export default class GooglePhotosManager extends ApiManager {
                         for (let index = 0; index < batch.length; index++) {
                             const { url, description } = batch[index];
                             const fail = (reason: string) => failed.push({ reason, batch: completedBatches + 1, index, url });
-                            const uploadToken = await GooglePhotosUploadUtils.DispatchGooglePhotosUpload(token, url).catch(fail);
+                            const uploadToken = await GooglePhotosUploadUtils.DispatchGooglePhotosUpload(token, InjectSize(url, SizeSuffix.Original)).catch(fail);
                             if (!uploadToken) {
                                 fail(`${path.extname(url)} is not an accepted extension`);
                             } else {
diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts
index 0a670ec01..81cd2d602 100644
--- a/src/server/DashUploadUtils.ts
+++ b/src/server/DashUploadUtils.ts
@@ -21,12 +21,12 @@ export enum SizeSuffix {
     Original = "_o"
 }
 
-export namespace DashUploadUtils {
+export function InjectSize(filename: string, size: SizeSuffix) {
+    const extension = path.extname(filename).toLowerCase();
+    return filename.substring(0, filename.length - extension.length) + size + extension;
+}
 
-    function InjectSize(filename: string, size: SizeSuffix) {
-        const extension = path.extname(filename).toLowerCase();
-        return filename.substring(0, filename.length - extension.length) + size + extension;
-    }
+export namespace DashUploadUtils {
 
     export interface Size {
         width: number;
@@ -72,8 +72,8 @@ export namespace DashUploadUtils {
         switch (category) {
             case "image":
                 if (imageFormats.includes(format)) {
-                    const { clientAccessPath } = await UploadImage(path, basename(path), format);
-                    return { clientAccessPath, name, type };
+                    const results = await UploadImage(path, basename(path), format);
+                    return { ...results, name, type };
                 }
             case "video":
                 if (videoFormats.includes(format)) {
-- 
cgit v1.2.3-70-g09d2


From 77ee66de66a411f79bbbc036d379d09be38d172f Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Mon, 2 Dec 2019 12:12:58 -0500
Subject: further cleanup

---
 src/Utils.ts                                       |   2 -
 src/client/util/ClientDiagnostics.ts               |  18 +--
 .../util/Import & Export/DirectoryImportBox.tsx    |   3 +-
 src/client/views/MainView.tsx                      |   2 +-
 src/client/views/collections/CollectionSubView.tsx |   6 +-
 src/server/ApiManagers/DeleteManager.ts            |   1 -
 src/server/ApiManagers/GeneralGoogleManager.ts     |  15 +--
 src/server/ApiManagers/PDFManager.ts               | 134 ++++++++++-----------
 src/server/ApiManagers/UploadManager.ts            |  43 ++++---
 src/server/DashUploadUtils.ts                      |  18 ++-
 src/server/RouteManager.ts                         |  14 +++
 .../authentication/models/current_user_utils.ts    |   3 +-
 src/server/credentials/test.json                   |  14 ---
 13 files changed, 125 insertions(+), 148 deletions(-)
 delete mode 100644 src/server/credentials/test.json

(limited to 'src/client/util/Import & Export/DirectoryImportBox.tsx')

diff --git a/src/Utils.ts b/src/Utils.ts
index b60e9e023..2543743a4 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -2,8 +2,6 @@ import v4 = require('uuid/v4');
 import v5 = require("uuid/v5");
 import { Socket } from 'socket.io';
 import { Message } from './server/Message';
-import { EventEmitter } from 'events';
-import { ConsoleColors } from './server/ActionUtilities';
 
 export namespace Utils {
 
diff --git a/src/client/util/ClientDiagnostics.ts b/src/client/util/ClientDiagnostics.ts
index 7eef935fd..0a213aa1c 100644
--- a/src/client/util/ClientDiagnostics.ts
+++ b/src/client/util/ClientDiagnostics.ts
@@ -12,18 +12,22 @@ export namespace ClientDiagnostics {
             serverPolls--;
         }, 1000 * 15);
 
-
         let executed = false;
-        const handle = async () => {
+        let solrHandle: NodeJS.Timeout | undefined;
+        const handler = async () => {
             const response = await fetch("/solrHeartbeat");
             if (!(await response.json()).running) {
-                !executed && alert("Looks like SOLR is not running on your machine.");
-                executed = true;
-                clearInterval(solrHandle);
+                if (!executed) {
+                    alert("Looks like SOLR is not running on your machine.");
+                    executed = true;
+                    solrHandle && clearInterval(solrHandle);
+                }
             }
         };
-        await handle();
-        const solrHandle = setInterval(handle, 1000 * 15);
+        await handler();
+        if (!executed) {
+            solrHandle = setInterval(handler, 1000 * 15);
+        }
 
     }
 
diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx
index b5e806a97..104d9e099 100644
--- a/src/client/util/Import & Export/DirectoryImportBox.tsx	
+++ b/src/client/util/Import & Export/DirectoryImportBox.tsx	
@@ -106,7 +106,8 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
 
         runInAction(() => this.phase = `Internal: uploading ${this.quota - this.completed} files to Dash...`);
 
-        const uploads = await BatchedArray.from(validated, { batchSize: 15 }).batchedMapAsync<any>(async (batch, collector) => {
+        const batched = BatchedArray.from(validated, { batchSize: 15 });
+        const uploads = await batched.batchedMapAsync<any>(async (batch, collector) => {
             const formData = new FormData();
 
             batch.forEach(file => {
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 5231075a1..85dfd8be2 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -4,7 +4,7 @@ import {
     faMusic, faObjectGroup, faPause, faMousePointer, faPenNib, faFileAudio, faPen, faEraser, faPlay, faPortrait, faRedoAlt, faThumbtack, faTree, faTv, faUndoAlt, faHighlighter, faMicrophone, faCompressArrowsAlt
 } from '@fortawesome/free-solid-svg-icons';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, configure, observable, reaction, runInAction, autorun } from 'mobx';
+import { action, computed, configure, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import "normalize.css";
 import * as React from 'react';
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index a1bd1527e..a1ae77fef 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -6,7 +6,7 @@ import { Id } from "../../../new_fields/FieldSymbols";
 import { List } from "../../../new_fields/List";
 import { listSpec } from "../../../new_fields/Schema";
 import { ScriptField } from "../../../new_fields/ScriptField";
-import { Cast, StrCast } from "../../../new_fields/Types";
+import { Cast } from "../../../new_fields/Types";
 import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
 import { Utils } from "../../../Utils";
 import { DocServer } from "../../DocServer";
@@ -279,9 +279,9 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
                     formData.append('file', file);
                     let dropFileName = file ? file.name : "-empty-";
                     promises.push(Networking.PostFormDataToServer("/upload", formData).then(results => {
-                        results.map(action((file: any) => {
+                        results.map(action(({ clientAccessPath }: any) => {
                             let full = { ...options, nativeWidth: type.indexOf("video") !== -1 ? 600 : 300, width: 300, title: dropFileName };
-                            let pathname = Utils.prepend(file.clientAccessPath);
+                            let pathname = Utils.prepend(clientAccessPath);
                             Docs.Get.DocumentFromType(type, pathname, full).then(doc => {
                                 doc && (Doc.GetProto(doc).fileUpload = path.basename(pathname).replace("upload_", "").replace(/\.[a-z0-9]*$/, ""));
                                 doc && this.props.addDocument(doc);
diff --git a/src/server/ApiManagers/DeleteManager.ts b/src/server/ApiManagers/DeleteManager.ts
index f58a28ce5..71818c673 100644
--- a/src/server/ApiManagers/DeleteManager.ts
+++ b/src/server/ApiManagers/DeleteManager.ts
@@ -56,7 +56,6 @@ export default class DeleteManager extends ApiManager {
             }
         });
 
-
     }
 
 }
diff --git a/src/server/ApiManagers/GeneralGoogleManager.ts b/src/server/ApiManagers/GeneralGoogleManager.ts
index 171912185..629684e0c 100644
--- a/src/server/ApiManagers/GeneralGoogleManager.ts
+++ b/src/server/ApiManagers/GeneralGoogleManager.ts
@@ -20,8 +20,7 @@ export default class GeneralGoogleManager extends ApiManager {
             method: Method.GET,
             subscription: "/readGoogleAccessToken",
             onValidation: async ({ user, res }) => {
-                const userId = user.id;
-                const token = await GoogleApiServerUtils.retrieveAccessToken(userId);
+                const token = await GoogleApiServerUtils.retrieveAccessToken(user.id);
                 if (!token) {
                     return res.send(GoogleApiServerUtils.generateAuthenticationUrl());
                 }
@@ -37,18 +36,6 @@ export default class GeneralGoogleManager extends ApiManager {
             }
         });
 
-        register({
-            method: Method.GET,
-            subscription: "/deleteWithGoogleCredentials",
-            onValidation: async ({ res, isRelease }) => {
-                if (isRelease) {
-                    return _permission_denied(res, deletionPermissionError);
-                }
-                await Database.Auxiliary.GoogleAuthenticationToken.DeleteAll();
-                res.redirect("/delete");
-            }
-        });
-
         register({
             method: Method.POST,
             subscription: new RouteSubscriber("/googleDocs").add("sector", "action"),
diff --git a/src/server/ApiManagers/PDFManager.ts b/src/server/ApiManagers/PDFManager.ts
index 4bd750aaf..151b48dd9 100644
--- a/src/server/ApiManagers/PDFManager.ts
+++ b/src/server/ApiManagers/PDFManager.ts
@@ -4,10 +4,11 @@ import RouteSubscriber from "../RouteSubscriber";
 import { exists, createReadStream, createWriteStream } from "fs";
 import * as Pdfjs from 'pdfjs-dist';
 import { createCanvas } from "canvas";
-const probe = require("probe-image-size");
+const imageSize = require("probe-image-size");
 import * as express from "express";
 import * as path from "path";
 import { Directory, serverPathToFile, clientPathToFile } from "./UploadManager";
+import { ConsoleColors } from "../ActionUtilities";
 
 export default class PDFManager extends ApiManager {
 
@@ -16,84 +17,77 @@ export default class PDFManager extends ApiManager {
         register({
             method: Method.GET,
             subscription: new RouteSubscriber("thumbnail").add("filename"),
-            onValidation: ({ req, res }) => {
-                let filename = req.params.filename;
-                let noExt = filename.substring(0, filename.length - ".png".length);
-                let pagenumber = parseInt(noExt.split('-')[1]);
-                return new Promise<void>(resolve => {
-                    const path = serverPathToFile(Directory.pdf_thumbnails, filename);
-                    exists(path, (exists: boolean) => {
-                        console.log(`${path} ${exists ? "exists" : "does not exist"}`);
-                        if (exists) {
-                            let input = createReadStream(path);
-                            probe(input, (err: any, { width, height }: any) => {
-                                if (err) {
-                                    console.log(err);
-                                    console.log(`error on ${filename}`);
-                                    return;
-                                }
-                                res.send({
-                                    path: clientPathToFile(Directory.pdf_thumbnails, filename),
-                                    width,
-                                    height
-                                });
-                            });
-                        }
-                        else {
-                            const name = filename.substring(0, filename.length - noExt.split('-')[1].length - ".PNG".length - 1) + ".pdf";
-                            LoadPage(serverPathToFile(Directory.pdfs, name), pagenumber, res);
-                        }
-                        resolve();
-                    });
-                });
-            }
+            onValidation: ({ req, res }) => getOrCreateThumbnail(req.params.filename, res)
         });
 
-        function LoadPage(file: string, pageNumber: number, res: express.Response) {
-            console.log(file);
-            Pdfjs.getDocument(file).promise
-                .then((pdf: Pdfjs.PDFDocumentProxy) => {
-                    let factory = new NodeCanvasFactory();
-                    console.log(pageNumber);
-                    pdf.getPage(pageNumber).then((page: Pdfjs.PDFPageProxy) => {
-                        console.log("reading " + page);
-                        let viewport = page.getViewport(1 as any);
-                        let canvasAndContext = factory.create(viewport.width, viewport.height);
-                        let renderContext = {
-                            canvasContext: canvasAndContext.context,
-                            canvasFactory: factory,
-                            viewport
-                        };
-                        console.log("read " + pageNumber);
+    }
 
-                        page.render(renderContext).promise
-                            .then(() => {
-                                console.log("saving " + pageNumber);
-                                let stream = canvasAndContext.canvas.createPNGStream();
-                                let filenames = path.basename(file).split(".");
-                                const pngFile = serverPathToFile(Directory.pdf_thumbnails, `${filenames[0]}-${pageNumber}.png`);
-                                let out = createWriteStream(pngFile);
-                                stream.pipe(out);
-                                out.on("finish", () => {
-                                    console.log(`Success! Saved to ${pngFile}`);
-                                    res.send({
-                                        path: pngFile,
-                                        width: viewport.width,
-                                        height: viewport.height
-                                    });
-                                });
-                            }, (reason: string) => {
-                                console.error(reason + ` ${pageNumber}`);
-                            });
+}
+
+function getOrCreateThumbnail(thumbnailName: string, res: express.Response) {
+    const noExtension = thumbnailName.substring(0, thumbnailName.length - ".png".length);
+    const pageString = noExtension.split('-')[1];
+    const pageNumber = parseInt(pageString);
+    return new Promise<void>(resolve => {
+        const path = serverPathToFile(Directory.pdf_thumbnails, thumbnailName);
+        exists(path, (exists: boolean) => {
+            if (exists) {
+                let existingThumbnail = createReadStream(path);
+                imageSize(existingThumbnail, (err: any, { width, height }: any) => {
+                    if (err) {
+                        console.log(ConsoleColors.Red, `In PDF thumbnail response, unable to determine dimensions of ${thumbnailName}:`);
+                        console.log(err);
+                        return;
+                    }
+                    res.send({
+                        path: clientPathToFile(Directory.pdf_thumbnails, thumbnailName),
+                        width,
+                        height
                     });
                 });
-        }
-
-    }
+            } else {
+                const offset = thumbnailName.length - pageString.length - 5;
+                const name = thumbnailName.substring(0, offset) + ".pdf";
+                const path = serverPathToFile(Directory.pdfs, name);
+                CreateThumbnail(path, pageNumber, res);
+            }
+            resolve();
+        });
+    });
+}
 
+async function CreateThumbnail(file: string, pageNumber: number, res: express.Response) {
+    const documentProxy = await Pdfjs.getDocument(file).promise;
+    const factory = new NodeCanvasFactory();
+    const page = await documentProxy.getPage(pageNumber);
+    const viewport = page.getViewport(1 as any);
+    const { canvas, context } = factory.create(viewport.width, viewport.height);
+    const renderContext = {
+        canvasContext: context,
+        canvasFactory: factory,
+        viewport
+    };
+    await page.render(renderContext).promise;
+    const pngStream = canvas.createPNGStream();
+    const filenames = path.basename(file).split(".");
+    const pngFile = serverPathToFile(Directory.pdf_thumbnails, `${filenames[0]}-${pageNumber}.png`);
+    const out = createWriteStream(pngFile);
+    pngStream.pipe(out);
+    out.on("finish", () => {
+        res.send({
+            path: pngFile,
+            width: viewport.width,
+            height: viewport.height
+        });
+    });
+    out.on("error", error => {
+        console.log(ConsoleColors.Red, `In PDF thumbnail creation, encountered the following error when piping ${pngFile}:`);
+        console.log(error);
+    });
 }
 
 class NodeCanvasFactory {
+
     create = (width: number, height: number) => {
         var canvas = createCanvas(width, height);
         var context = canvas.getContext('2d');
diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts
index 2f76871a6..80ae0ad61 100644
--- a/src/server/ApiManagers/UploadManager.ts
+++ b/src/server/ApiManagers/UploadManager.ts
@@ -38,6 +38,27 @@ export default class UploadManager extends ApiManager {
 
     protected initialize(register: Registration): void {
 
+        register({
+            method: Method.POST,
+            subscription: "/upload",
+            onValidation: async ({ req, res }) => {
+                let form = new formidable.IncomingForm();
+                form.uploadDir = pathToDirectory(Directory.parsed_files);
+                form.keepExtensions = true;
+                return new Promise<void>(resolve => {
+                    form.parse(req, async (_err, _fields, files) => {
+                        let results: any[] = [];
+                        for (const key in files) {
+                            const result = await DashUploadUtils.upload(files[key]);
+                            result && results.push(result);
+                        }
+                        _success(res, results);
+                        resolve();
+                    });
+                });
+            }
+        });
+
         register({
             method: Method.POST,
             subscription: "/uploadDoc",
@@ -142,28 +163,6 @@ export default class UploadManager extends ApiManager {
             }
         });
 
-
-        register({
-            method: Method.POST,
-            subscription: "/upload",
-            onValidation: async ({ req, res }) => {
-                let form = new formidable.IncomingForm();
-                form.uploadDir = pathToDirectory(Directory.parsed_files);
-                form.keepExtensions = true;
-                return new Promise<void>(resolve => {
-                    form.parse(req, async (_err, _fields, files) => {
-                        let results: any[] = [];
-                        for (const key in files) {
-                            const result = await DashUploadUtils.upload(files[key]);
-                            result && results.push(result);
-                        }
-                        _success(res, results);
-                        resolve();
-                    });
-                });
-            }
-        });
-
         register({
             method: Method.POST,
             subscription: "/inspectImage",
diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts
index c831eb072..9ccc72e35 100644
--- a/src/server/DashUploadUtils.ts
+++ b/src/server/DashUploadUtils.ts
@@ -85,7 +85,8 @@ export namespace DashUploadUtils {
                     return UploadPdf(path);
                 }
         }
-        console.log(ConsoleColors.Red, `Ignoring unsupported file ${name} with upload type (${type}).`);
+
+        console.log(ConsoleColors.Red, `Ignoring unsupported file (${name}) with upload type (${type}).`);
         return { clientAccessPath: undefined };
     }
 
@@ -169,17 +170,12 @@ export namespace DashUploadUtils {
         if (isLocal) {
             return results;
         }
-        const metadata = (await new Promise<any>((resolve, reject) => {
-            request.head(source, async (error, res) => {
-                if (error) {
-                    return reject(error);
-                }
-                resolve(res);
-            });
-        })).headers;
+        const { headers } = (await new Promise<any>((resolve, reject) => {
+            request.head(source, (error, res) => error ? reject(error) : resolve(res));
+        }));
         return {
-            contentSize: parseInt(metadata[size]),
-            contentType: metadata[type],
+            contentSize: parseInt(headers[size]),
+            contentType: headers[type],
             ...results
         };
     };
diff --git a/src/server/RouteManager.ts b/src/server/RouteManager.ts
index 3a20d5af5..7c49485f1 100644
--- a/src/server/RouteManager.ts
+++ b/src/server/RouteManager.ts
@@ -26,6 +26,8 @@ export interface RouteInitializer {
     onError?: OnError;
 }
 
+const registered = new Map<string, Set<Method>>();
+
 export default class RouteManager {
     private server: express.Express;
     private _isRelease: boolean;
@@ -89,6 +91,18 @@ export default class RouteManager {
             } else {
                 route = subscriber.build;
             }
+            const existing = registered.get(route);
+            if (existing) {
+                if (existing.has(method)) {
+                    console.log(ConsoleColors.Red, `\nDuplicate registration error: already registered ${route} with Method[${method}]`);
+                    console.log('Please remove duplicate registrations before continuing...\n');
+                    process.exit(0);
+                }
+            } else {
+                const specific = new Set<Method>();
+                specific.add(method);
+                registered.set(route, specific);
+            }
             switch (method) {
                 case Method.GET:
                     this.server.get(route, supervised);
diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts
index 052aa54a6..ac4462f78 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -1,4 +1,4 @@
-import { action, computed, observable, reaction, runInAction } from "mobx";
+import { action, computed, observable, reaction } from "mobx";
 import * as rp from 'request-promise';
 import { DocServer } from "../../../client/DocServer";
 import { Docs } from "../../../client/documents/Documents";
@@ -11,7 +11,6 @@ import { listSpec } from "../../../new_fields/Schema";
 import { ScriptField, ComputedField } from "../../../new_fields/ScriptField";
 import { Cast, PromiseValue } from "../../../new_fields/Types";
 import { Utils } from "../../../Utils";
-import { ButtonBox } from "../../../client/views/nodes/ButtonBox";
 import { nullAudio } from "../../../new_fields/URLField";
 import { DragManager } from "../../../client/util/DragManager";
 import { InkingControl } from "../../../client/views/InkingControl";
diff --git a/src/server/credentials/test.json b/src/server/credentials/test.json
deleted file mode 100644
index 0a032cc2d..000000000
--- a/src/server/credentials/test.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
-    "installed": {
-        "client_id": "343179513178-ud6tvmh275r2fq93u9eesrnc66t6akh9.apps.googleusercontent.com",
-        "project_id": "quickstart-1565056383187",
-        "auth_uri": "https://accounts.google.com/o/oauth2/auth",
-        "token_uri": "https://oauth2.googleapis.com/token",
-        "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
-        "client_secret": "w8KIFSc0MQpmUYHed4qEzn8b",
-        "redirect_uris": [
-            "urn:ietf:wg:oauth:2.0:oob",
-            "http://localhost"
-        ]
-    }
-}
\ No newline at end of file
-- 
cgit v1.2.3-70-g09d2


From ef94ad7df2a087141ddb8d347d3e3c484ff7609b Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Tue, 3 Dec 2019 01:46:36 -0500
Subject: const linter rule and restored google docs push, fixed routing

---
 package.json                                       |   3 +-
 src/Utils.ts                                       |  91 +++---
 src/client/DocServer.ts                            |  10 +-
 src/client/Network.ts                              |   2 +-
 src/client/apis/GoogleAuthenticationManager.tsx    |   2 +-
 .../apis/google_docs/GoogleApiClientUtils.ts       |  20 +-
 .../apis/google_docs/GooglePhotosClientUtils.ts    |  16 +-
 src/client/apis/youtube/YoutubeBox.tsx             | 102 +++----
 src/client/cognitive_services/CognitiveServices.ts |  50 ++--
 src/client/documents/Documents.ts                  |  80 ++---
 src/client/northstar/dash-fields/HistogramField.ts |   8 +-
 .../model/binRanges/QuantitativeVisualBinRange.ts  |  16 +-
 src/client/northstar/operations/BaseOperation.ts   |  14 +-
 src/client/northstar/utils/MathUtil.ts             |  44 +--
 src/client/util/DictationManager.ts                |  75 +++--
 src/client/util/DocumentManager.ts                 |  28 +-
 src/client/util/DragManager.ts                     |  91 +++---
 src/client/util/DropConverter.ts                   |  10 +-
 src/client/util/History.ts                         |   6 +-
 .../util/Import & Export/DirectoryImportBox.tsx    |  58 ++--
 .../util/Import & Export/ImportMetadataEntry.tsx   |   6 +-
 src/client/util/InteractionUtils.ts                |  18 +-
 src/client/util/LinkManager.ts                     |  62 ++--
 src/client/util/ProsemirrorExampleTransfer.ts      |  50 ++--
 src/client/util/RichTextRules.ts                   |  78 ++---
 src/client/util/RichTextSchema.tsx                 |  86 +++---
 src/client/util/Scripting.ts                       |  38 +--
 src/client/util/SearchUtil.ts                      |  24 +-
 src/client/util/SerializationHelper.ts             |   9 +-
 src/client/util/SharingManager.tsx                 |  13 +-
 src/client/util/TooltipLinkingMenu.tsx             |  22 +-
 src/client/util/TooltipTextMenu.tsx                | 332 ++++++++++-----------
 src/client/util/TypedEvent.ts                      |  62 ++--
 src/client/util/UndoManager.ts                     |  16 +-
 src/client/views/CollectionLinearView.tsx          |  12 +-
 src/client/views/ContextMenu.tsx                   |   6 +-
 src/client/views/ContextMenuItem.tsx               |   2 +-
 src/client/views/DictationOverlay.tsx              |  10 +-
 src/client/views/DocComponent.tsx                  |   5 +-
 src/client/views/DocumentButtonBar.scss            |  32 +-
 src/client/views/DocumentButtonBar.tsx             |  57 ++--
 src/client/views/DocumentDecorations.tsx           |  97 +++---
 src/client/views/GlobalKeyHandler.ts               |  30 +-
 src/client/views/InkSelectDecorations.tsx          |  10 +-
 src/client/views/InkingControl.tsx                 |  26 +-
 src/client/views/InkingStroke.tsx                  |  26 +-
 src/client/views/MainView.tsx                      |  36 +--
 src/client/views/MainViewModal.tsx                 |   6 +-
 src/client/views/MetadataEntryMenu.tsx             |   4 +-
 src/client/views/OverlayView.tsx                   |   6 +-
 src/client/views/PreviewCursor.tsx                 |  12 +-
 src/client/views/ScriptBox.tsx                     |   8 +-
 src/client/views/TemplateMenu.tsx                  |  32 +-
 src/client/views/Touchable.tsx                     |   6 +-
 .../views/collections/CollectionDockingView.tsx    |  92 +++---
 .../collections/CollectionMasonryViewFieldRow.tsx  |  82 ++---
 .../views/collections/CollectionSchemaCells.tsx    |  42 +--
 .../views/collections/CollectionSchemaHeaders.tsx  |  36 ++-
 .../CollectionSchemaMovableTableHOC.tsx            |  62 ++--
 .../views/collections/CollectionSchemaView.tsx     | 118 ++++----
 .../views/collections/CollectionStackingView.tsx   | 120 ++++----
 .../CollectionStackingViewFieldColumn.tsx          |  83 +++---
 .../views/collections/CollectionStaffView.tsx      |   6 +-
 src/client/views/collections/CollectionSubView.tsx |  66 ++--
 .../views/collections/CollectionTreeView.tsx       | 144 ++++-----
 src/client/views/collections/CollectionView.tsx    |  28 +-
 .../views/collections/CollectionViewChromes.tsx    |  67 ++---
 src/client/views/collections/KeyRestrictionRow.tsx |   6 +-
 .../views/collections/ParentDocumentSelector.tsx   |   4 +-
 .../CollectionFreeFormLayoutEngines.tsx            |  10 +-
 .../CollectionFreeFormLinkView.tsx                 |  45 ++-
 .../CollectionFreeFormLinksView.tsx                |   8 +-
 .../CollectionFreeFormRemoteCursors.tsx            |  12 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 231 +++++++-------
 .../collectionFreeForm/MarqueeOptionsMenu.tsx      |   2 +-
 .../collections/collectionFreeForm/MarqueeView.tsx | 137 +++++----
 .../caption_toggle/DetailedCaptionToggle.tsx       |   6 +-
 src/client/views/linking/LinkEditor.tsx            |  82 ++---
 src/client/views/linking/LinkFollowBox.tsx         |  48 +--
 src/client/views/linking/LinkMenu.tsx              |   6 +-
 src/client/views/linking/LinkMenuGroup.tsx         |  22 +-
 src/client/views/linking/LinkMenuItem.tsx          |  14 +-
 src/client/views/nodes/AudioBox.tsx                |  24 +-
 src/client/views/nodes/ButtonBox.tsx               |   8 +-
 .../views/nodes/CollectionFreeFormDocumentView.tsx |  20 +-
 .../views/nodes/ContentFittingDocumentView.tsx     |   4 +-
 src/client/views/nodes/DocuLinkBox.tsx             |  26 +-
 src/client/views/nodes/DocumentContentsView.tsx    |   2 +-
 src/client/views/nodes/DocumentView.tsx            |  72 ++---
 src/client/views/nodes/FaceRectangle.tsx           |   2 +-
 src/client/views/nodes/FaceRectangles.tsx          |   8 +-
 src/client/views/nodes/FontIconBox.tsx             |   8 +-
 src/client/views/nodes/FormattedTextBox.tsx        | 197 ++++++------
 src/client/views/nodes/FormattedTextBoxComment.tsx |  22 +-
 src/client/views/nodes/IconBox.tsx                 |   6 +-
 src/client/views/nodes/ImageBox.tsx                |  86 +++---
 src/client/views/nodes/KeyValueBox.tsx             |  40 +--
 src/client/views/nodes/KeyValuePair.tsx            |   8 +-
 src/client/views/nodes/PDFBox.tsx                  |   8 +-
 src/client/views/nodes/PresBox.tsx                 |  38 +--
 src/client/views/nodes/VideoBox.tsx                |  66 ++--
 src/client/views/nodes/WebBox.tsx                  |  22 +-
 src/client/views/pdf/Annotation.tsx                |  14 +-
 src/client/views/pdf/PDFMenu.tsx                   |   2 +-
 src/client/views/pdf/PDFViewer.tsx                 |  66 ++--
 .../views/presentationview/PresElementBox.tsx      |  12 +-
 src/client/views/search/FilterBox.tsx              |  44 +--
 src/client/views/search/IconButton.tsx             |   2 +-
 src/client/views/search/NaviconButton.tsx          |  22 +-
 src/client/views/search/SearchBox.tsx              |  20 +-
 src/client/views/search/SearchItem.tsx             |  26 +-
 src/client/views/search/ToggleBar.tsx              |   3 +-
 src/debug/Viewer.tsx                               |   4 +-
 src/mobile/ImageUpload.tsx                         |  10 +-
 src/new_fields/Doc.ts                              |  94 +++---
 src/new_fields/List.ts                             |   4 +-
 src/new_fields/RichTextUtils.ts                    |  76 ++---
 src/new_fields/Schema.ts                           |   2 +-
 src/new_fields/ScriptField.ts                      |  10 +-
 src/server/ActionUtilities.ts                      |  29 +-
 src/server/ApiManagers/DownloadManager.ts          |   3 +-
 src/server/ApiManagers/GeneralGoogleManager.ts     |  17 +-
 src/server/ApiManagers/GooglePhotosManager.ts      |   4 +-
 src/server/ApiManagers/PDFManager.ts               |  12 +-
 src/server/ApiManagers/SearchManager.ts            |  12 +-
 src/server/ApiManagers/UploadManager.ts            |  16 +-
 src/server/ApiManagers/UtilManager.ts              |   8 +-
 src/server/DashUploadUtils.ts                      |  12 +-
 src/server/Initialization.ts                       |  15 +-
 src/server/RouteManager.ts                         |  16 +-
 src/server/Websocket/Websocket.ts                  |  15 +-
 src/server/apis/google/GoogleApiServerUtils.ts     |   8 +-
 src/server/authentication/config/passport.ts       |   2 +-
 .../authentication/controllers/user_controller.ts  |   5 +-
 .../authentication/models/current_user_utils.ts    |   6 +-
 src/server/database.ts                             |   4 +-
 src/server/downsize.ts                             |   2 +-
 src/server/index.ts                                |  11 +-
 src/typings/index.d.ts                             |   1 +
 test/test.ts                                       |  18 +-
 tslint.json                                        |  58 ++--
 141 files changed, 2467 insertions(+), 2429 deletions(-)

(limited to 'src/client/util/Import & Export/DirectoryImportBox.tsx')

diff --git a/package.json b/package.json
index 3725d76eb..499aefdb5 100644
--- a/package.json
+++ b/package.json
@@ -127,6 +127,7 @@
         "child_process": "^1.0.2",
         "class-transformer": "^0.2.0",
         "color": "^3.1.2",
+        "colors": "^1.4.0",
         "connect-flash": "^0.1.1",
         "connect-mongo": "^2.0.3",
         "cookie-parser": "^1.4.4",
@@ -228,4 +229,4 @@
         "xoauth2": "^1.2.0",
         "youtube": "^0.1.0"
     }
-}
+}
\ No newline at end of file
diff --git a/src/Utils.ts b/src/Utils.ts
index 2543743a4..7401ef981 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -54,7 +54,7 @@ export namespace Utils {
     }
 
     export function CopyText(text: string) {
-        var textArea = document.createElement("textarea");
+        const textArea = document.createElement("textarea");
         textArea.value = text;
         document.body.appendChild(textArea);
         textArea.focus();
@@ -66,14 +66,14 @@ export namespace Utils {
     }
 
     export function fromRGBAstr(rgba: string) {
-        let rm = rgba.match(/rgb[a]?\(([ 0-9]+)/);
-        let r = rm ? Number(rm[1]) : 0;
-        let gm = rgba.match(/rgb[a]?\([ 0-9]+,([ 0-9]+)/);
-        let g = gm ? Number(gm[1]) : 0;
-        let bm = rgba.match(/rgb[a]?\([ 0-9]+,[ 0-9]+,([ 0-9]+)/);
-        let b = bm ? Number(bm[1]) : 0;
-        let am = rgba.match(/rgba?\([ 0-9]+,[ 0-9]+,[ 0-9]+,([ .0-9]+)/);
-        let a = am ? Number(am[1]) : 1;
+        const rm = rgba.match(/rgb[a]?\(([ 0-9]+)/);
+        const r = rm ? Number(rm[1]) : 0;
+        const gm = rgba.match(/rgb[a]?\([ 0-9]+,([ 0-9]+)/);
+        const g = gm ? Number(gm[1]) : 0;
+        const bm = rgba.match(/rgb[a]?\([ 0-9]+,[ 0-9]+,([ 0-9]+)/);
+        const b = bm ? Number(bm[1]) : 0;
+        const am = rgba.match(/rgba?\([ 0-9]+,[ 0-9]+,[ 0-9]+,([ .0-9]+)/);
+        const a = am ? Number(am[1]) : 1;
         return { r: r, g: g, b: b, a: a };
     }
 
@@ -86,10 +86,10 @@ export namespace Utils {
         // s /= 100;
         // l /= 100;
 
-        let c = (1 - Math.abs(2 * l - 1)) * s,
+        const c = (1 - Math.abs(2 * l - 1)) * s,
             x = c * (1 - Math.abs((h / 60) % 2 - 1)),
-            m = l - c / 2,
-            r = 0,
+            m = l - c / 2;
+        let r = 0,
             g = 0,
             b = 0;
         if (0 <= h && h < 60) {
@@ -118,10 +118,10 @@ export namespace Utils {
         b /= 255;
 
         // Find greatest and smallest channel values
-        let cmin = Math.min(r, g, b),
+        const cmin = Math.min(r, g, b),
             cmax = Math.max(r, g, b),
-            delta = cmax - cmin,
-            h = 0,
+            delta = cmax - cmin;
+        let h = 0,
             s = 0,
             l = 0;
         // Calculate hue
@@ -173,11 +173,11 @@ export namespace Utils {
     function project(px: number, py: number, ax: number, ay: number, bx: number, by: number) {
 
         if (ax === bx && ay === by) return { point: { x: ax, y: ay }, left: false, dot: 0, t: 0 };
-        var atob = { x: bx - ax, y: by - ay };
-        var atop = { x: px - ax, y: py - ay };
-        var len = atob.x * atob.x + atob.y * atob.y;
+        const atob = { x: bx - ax, y: by - ay };
+        const atop = { x: px - ax, y: py - ay };
+        const len = atob.x * atob.x + atob.y * atob.y;
         var dot = atop.x * atob.x + atop.y * atob.y;
-        var t = Math.min(1, Math.max(0, dot / len));
+        const t = Math.min(1, Math.max(0, dot / len));
 
         dot = (bx - ax) * (py - ay) - (by - ay) * (px - ax);
 
@@ -195,38 +195,38 @@ export namespace Utils {
     export function closestPtBetweenRectangles(l: number, t: number, w: number, h: number,
         l1: number, t1: number, w1: number, h1: number,
         x: number, y: number) {
-        var r = l + w,
+        const r = l + w,
             b = t + h;
-        var r1 = l1 + w1,
+        const r1 = l1 + w1,
             b1 = t1 + h1;
-        let hsegs = [[l, r, t, l1, r1, t1], [l, r, b, l1, r1, t1], [l, r, t, l1, r1, b1], [l, r, b, l1, r1, b1]];
-        let vsegs = [[l, t, b, l1, t1, b1], [r, t, b, l1, t1, b1], [l, t, b, r1, t1, b1], [r, t, b, r1, t1, b1]];
-        let res = hsegs.reduce((closest, seg) => {
-            let res = distanceBetweenHorizontalLines(seg[0], seg[1], seg[2], seg[3], seg[4], seg[5]);
+        const hsegs = [[l, r, t, l1, r1, t1], [l, r, b, l1, r1, t1], [l, r, t, l1, r1, b1], [l, r, b, l1, r1, b1]];
+        const vsegs = [[l, t, b, l1, t1, b1], [r, t, b, l1, t1, b1], [l, t, b, r1, t1, b1], [r, t, b, r1, t1, b1]];
+        const res = hsegs.reduce((closest, seg) => {
+            const res = distanceBetweenHorizontalLines(seg[0], seg[1], seg[2], seg[3], seg[4], seg[5]);
             return (res[0] < closest[0]) ? res : closest;
         }, [Number.MAX_VALUE, []] as [number, number[]]);
-        let fres = vsegs.reduce((closest, seg) => {
-            let res = distanceBetweenVerticalLines(seg[0], seg[1], seg[2], seg[3], seg[4], seg[5]);
+        const fres = vsegs.reduce((closest, seg) => {
+            const res = distanceBetweenVerticalLines(seg[0], seg[1], seg[2], seg[3], seg[4], seg[5]);
             return (res[0] < closest[0]) ? res : closest;
         }, res);
 
-        let near = project(x, y, fres[1][0], fres[1][1], fres[1][2], fres[1][3]);
+        const near = project(x, y, fres[1][0], fres[1][1], fres[1][2], fres[1][3]);
         return project(near.point.x, near.point.y, fres[1][0], fres[1][1], fres[1][2], fres[1][3]);
     }
 
     export function getNearestPointInPerimeter(l: number, t: number, w: number, h: number, x: number, y: number) {
-        var r = l + w,
+        const r = l + w,
             b = t + h;
 
-        var x = clamp(x, l, r),
+        x = clamp(x, l, r),
             y = clamp(y, t, b);
 
-        var dl = Math.abs(x - l),
+        const dl = Math.abs(x - l),
             dr = Math.abs(x - r),
             dt = Math.abs(y - t),
             db = Math.abs(y - b);
 
-        var m = Math.min(dl, dr, dt, db);
+        const m = Math.min(dl, dr, dt, db);
 
         return (m === dt) ? [x, t] :
             (m === db) ? [x, b] :
@@ -234,7 +234,7 @@ export namespace Utils {
     }
 
     export function GetClipboardText(): string {
-        var textArea = document.createElement("textarea");
+        const textArea = document.createElement("textarea");
         document.body.appendChild(textArea);
         textArea.focus();
         textArea.select();
@@ -257,7 +257,7 @@ export namespace Utils {
         if (logFilter !== undefined && logFilter !== message.type) {
             return;
         }
-        let idString = (message.id || "").padStart(36, ' ');
+        const idString = (message.id || "").padStart(36, ' ');
         prefix = prefix.padEnd(16, ' ');
         console.log(`${prefix}: ${idString}, ${receiving ? 'receiving' : 'sending'} ${messageName} with data ${JSON.stringify(message)} `);
     }
@@ -309,18 +309,18 @@ export function OmitKeys(obj: any, keys: string[], addKeyFunc?: (dup: any) => vo
 }
 
 export function WithKeys(obj: any, keys: string[], addKeyFunc?: (dup: any) => void) {
-    var dup: any = {};
+    const dup: any = {};
     keys.forEach(key => dup[key] = obj[key]);
     addKeyFunc && addKeyFunc(dup);
     return dup;
 }
 
 export function timenow() {
-    var now = new Date();
+    const now = new Date();
     let ampm = 'am';
     let h = now.getHours();
     let m: any = now.getMinutes();
-    let s: any = now.getSeconds();
+    const s: any = now.getSeconds();
     if (h >= 12) {
         if (h > 12) h -= 12;
         ampm = 'pm';
@@ -331,8 +331,8 @@ export function timenow() {
 
 export function aggregateBounds(boundsList: { x: number, y: number, width: number, height: number }[]) {
     return boundsList.reduce((bounds, b) => {
-        var [sptX, sptY] = [b.x, b.y];
-        let [bptX, bptY] = [sptX + b.width, sptY + b.height];
+        const [sptX, sptY] = [b.x, b.y];
+        const [bptX, bptY] = [sptX + b.width, sptY + b.height];
         return {
             x: Math.min(sptX, bounds.x), y: Math.min(sptY, bounds.y),
             r: Math.max(bptX, bounds.r), b: Math.max(bptY, bounds.b)
@@ -371,10 +371,11 @@ export type Without<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
 export type Predicate<K, V> = (entry: [K, V]) => boolean;
 
 export function DeepCopy<K, V>(source: Map<K, V>, predicate?: Predicate<K, V>) {
-    let deepCopy = new Map<K, V>();
-    let entries = source.entries(), next = entries.next();
+    const deepCopy = new Map<K, V>();
+    const entries = source.entries();
+    let next = entries.next();
     while (!next.done) {
-        let entry = next.value;
+        const entry = next.value;
         if (!predicate || predicate(entry)) {
             deepCopy.set(entry[0], entry[1]);
         }
@@ -427,13 +428,13 @@ export function smoothScroll(duration: number, element: HTMLElement, to: number)
     animateScroll();
 }
 export function addStyleSheet(styleType: string = "text/css") {
-    let style = document.createElement("style");
+    const style = document.createElement("style");
     style.type = styleType;
-    var sheets = document.head.appendChild(style);
+    const sheets = document.head.appendChild(style);
     return (sheets as any).sheet;
 }
 export function addStyleSheetRule(sheet: any, selector: any, css: any) {
-    var propText = typeof css === "string" ? css : Object.keys(css).map(p => p + ":" + (p === "content" ? "'" + css[p] + "'" : css[p])).join(";");
+    const propText = typeof css === "string" ? css : Object.keys(css).map(p => p + ":" + (p === "content" ? "'" + css[p] + "'" : css[p])).join(";");
     return sheet.insertRule("." + selector + "{" + propText + "}", sheet.cssRules.length);
 }
 export function removeStyleSheetRule(sheet: any, rule: number) {
diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts
index 2cec1046b..e4b183715 100644
--- a/src/client/DocServer.ts
+++ b/src/client/DocServer.ts
@@ -1,5 +1,5 @@
 import * as OpenSocket from 'socket.io-client';
-import { MessageStore, Diff, YoutubeQueryTypes } from "./../server/Message";
+import { MessageStore, YoutubeQueryTypes } from "./../server/Message";
 import { Opt, Doc } from '../new_fields/Doc';
 import { Utils, emptyFunction } from '../Utils';
 import { SerializationHelper } from './util/SerializationHelper';
@@ -148,7 +148,7 @@ export namespace DocServer {
         // an initial pass through the cache to determine whether the document needs to be fetched,
         // is already in the process of being fetched or already exists in the
         // cache
-        let cached = _cache[id];
+        const cached = _cache[id];
         if (cached === undefined) {
             // NOT CACHED => we'll have to send a request to the server
 
@@ -195,7 +195,7 @@ export namespace DocServer {
     }
 
     export async function getYoutubeChannels() {
-        let apiKey = await Utils.EmitCallback(_socket, MessageStore.YoutubeApiQuery, { type: YoutubeQueryTypes.Channels });
+        const apiKey = await Utils.EmitCallback(_socket, MessageStore.YoutubeApiQuery, { type: YoutubeQueryTypes.Channels });
         return apiKey;
     }
 
@@ -255,7 +255,7 @@ export namespace DocServer {
             for (const field of fields) {
                 if (field !== undefined) {
                     // deserialize
-                    let prom = SerializationHelper.Deserialize(field).then(deserialized => {
+                    const prom = SerializationHelper.Deserialize(field).then(deserialized => {
                         fieldMap[field.id] = deserialized;
 
                         //overwrite or delete any promises (that we inserted as flags
@@ -411,7 +411,7 @@ export namespace DocServer {
     }
 
     let _RespondToUpdate = _respondToUpdateImpl;
-    let _respondToDelete = _respondToDeleteImpl;
+    const _respondToDelete = _respondToDeleteImpl;
 
     function respondToUpdate(diff: any) {
         _RespondToUpdate(diff);
diff --git a/src/client/Network.ts b/src/client/Network.ts
index f9ef27267..ccf60f199 100644
--- a/src/client/Network.ts
+++ b/src/client/Network.ts
@@ -8,7 +8,7 @@ export namespace Networking {
     }
 
     export async function PostToServer(relativeRoute: string, body?: any) {
-        let options = {
+        const options = {
             uri: Utils.prepend(relativeRoute),
             method: "POST",
             body,
diff --git a/src/client/apis/GoogleAuthenticationManager.tsx b/src/client/apis/GoogleAuthenticationManager.tsx
index ae77c4b7b..ce1277667 100644
--- a/src/client/apis/GoogleAuthenticationManager.tsx
+++ b/src/client/apis/GoogleAuthenticationManager.tsx
@@ -30,7 +30,7 @@ export default class GoogleAuthenticationManager extends React.Component<{}> {
     }
 
     public fetchOrGenerateAccessToken = async () => {
-        let response = await Networking.FetchFromServer("/readGoogleAccessToken");
+        const response = await Networking.FetchFromServer("/readGoogleAccessToken");
         // if this is an authentication url, activate the UI to register the new access token
         if (new RegExp(AuthenticationUrl).test(response)) {
             this.isOpen = true;
diff --git a/src/client/apis/google_docs/GoogleApiClientUtils.ts b/src/client/apis/google_docs/GoogleApiClientUtils.ts
index 26c7f8d2e..d2a79f189 100644
--- a/src/client/apis/google_docs/GoogleApiClientUtils.ts
+++ b/src/client/apis/google_docs/GoogleApiClientUtils.ts
@@ -1,4 +1,4 @@
-import { docs_v1, slides_v1 } from "googleapis";
+import { docs_v1 } from "googleapis";
 import { Opt } from "../../../new_fields/Doc";
 import { isArray } from "util";
 import { EditorState } from "prosemirror-state";
@@ -94,7 +94,7 @@ export namespace GoogleApiClientUtils {
 
             export type ExtractResult = { text: string, paragraphs: DeconstructedParagraph[] };
             export const extractText = (document: docs_v1.Schema$Document, removeNewlines = false): ExtractResult => {
-                let paragraphs = extractParagraphs(document);
+                const paragraphs = extractParagraphs(document);
                 let text = paragraphs.map(paragraph => paragraph.contents.filter(content => !("inlineObjectId" in content)).map(run => run as docs_v1.Schema$TextRun).join("")).join("");
                 text = text.substring(0, text.length - 1);
                 removeNewlines && text.ReplaceAll("\n", "");
@@ -107,14 +107,14 @@ export namespace GoogleApiClientUtils {
                 const fragments: DeconstructedParagraph[] = [];
                 if (document.body && document.body.content) {
                     for (const element of document.body.content) {
-                        let runs: ContentArray = [];
+                        const runs: ContentArray = [];
                         let bullet: Opt<number>;
                         if (element.paragraph) {
                             if (element.paragraph.elements) {
                                 for (const inner of element.paragraph.elements) {
                                     if (inner) {
                                         if (inner.textRun) {
-                                            let run = inner.textRun;
+                                            const run = inner.textRun;
                                             (run.content || !filterEmpty) && runs.push(inner.textRun);
                                         } else if (inner.inlineObjectElement) {
                                             runs.push(inner.inlineObjectElement);
@@ -182,8 +182,8 @@ export namespace GoogleApiClientUtils {
         export const read = async (options: ReadOptions): Promise<Opt<ReadResult>> => {
             return retrieve({ documentId: options.documentId }).then(document => {
                 if (document) {
-                    let title = document.title!;
-                    let body = Utils.extractText(document, options.removeNewlines).text;
+                    const title = document.title!;
+                    const body = Utils.extractText(document, options.removeNewlines).text;
                     return { title, body };
                 }
             });
@@ -192,7 +192,7 @@ export namespace GoogleApiClientUtils {
         export const readLines = async (options: ReadOptions): Promise<Opt<ReadLinesResult>> => {
             return retrieve({ documentId: options.documentId }).then(document => {
                 if (document) {
-                    let title = document.title;
+                    const title = document.title;
                     let bodyLines = Utils.extractText(document).text.split("\n");
                     options.removeNewlines && (bodyLines = bodyLines.filter(line => line.length));
                     return { title, bodyLines };
@@ -201,7 +201,7 @@ export namespace GoogleApiClientUtils {
         };
 
         export const setStyle = async (options: UpdateOptions) => {
-            let replies: any = await update({
+            const replies: any = await update({
                 documentId: options.documentId,
                 requests: options.requests
             });
@@ -221,7 +221,7 @@ export namespace GoogleApiClientUtils {
             let index = options.index;
             const mode = options.mode;
             if (!(index && mode === WriteMode.Insert)) {
-                let schema = await retrieve({ documentId });
+                const schema = await retrieve({ documentId });
                 if (!schema || !(index = Utils.endOf(schema))) {
                     return undefined;
                 }
@@ -248,7 +248,7 @@ export namespace GoogleApiClientUtils {
                 return undefined;
             }
             requests.push(...options.content.requests);
-            let replies: any = await update({ documentId: documentId, requests });
+            const replies: any = await update({ documentId: documentId, requests });
             if ("errors" in replies) {
                 console.log("Write operation failed:");
                 console.log(replies.errors.map((error: any) => error.message));
diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
index bf8897061..966d8053a 100644
--- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts
+++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
@@ -128,10 +128,10 @@ export namespace GooglePhotos {
 
         export const CollectionFromSearch = async (constructor: CollectionConstructor, requested: Opt<Partial<Query.SearchOptions>>): Promise<Doc> => {
             await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken();
-            let response = await Query.ContentSearch(requested);
-            let uploads = await Transactions.WriteMediaItemsToServer(response);
+            const response = await Query.ContentSearch(requested);
+            const uploads = await Transactions.WriteMediaItemsToServer(response);
             const children = uploads.map((upload: Transactions.UploadInformation) => {
-                let document = Docs.Create.ImageDocument(Utils.fileUrl(upload.fileNames.clean));
+                const document = Docs.Create.ImageDocument(Utils.fileUrl(upload.fileNames.clean));
                 document.fillColumn = true;
                 document.contentSize = upload.contentSize;
                 return document;
@@ -157,12 +157,12 @@ export namespace GooglePhotos {
             const images = (await DocListCastAsync(collection.data))!.map(Doc.GetProto);
             images && images.forEach(image => tagMapping.set(image[Id], ContentCategories.NONE));
             const values = Object.values(ContentCategories);
-            for (let value of values) {
+            for (const value of values) {
                 if (value !== ContentCategories.NONE) {
                     const results = await ContentSearch({ included: [value] });
                     if (results.mediaItems) {
                         const ids = results.mediaItems.map(item => item.id);
-                        for (let id of ids) {
+                        for (const id of ids) {
                             const image = await Cast(idMapping[id], Doc);
                             if (image) {
                                 const key = image[Id];
@@ -220,9 +220,9 @@ export namespace GooglePhotos {
 
         export const AlbumSearch = async (albumId: string, pageSize = 100): Promise<MediaItem[]> => {
             const photos = await endpoint();
-            let mediaItems: MediaItem[] = [];
+            const mediaItems: MediaItem[] = [];
             let nextPageTokenStored: Opt<string> = undefined;
-            let found = 0;
+            const found = 0;
             do {
                 const response: any = await photos.mediaItems.search(albumId, pageSize, nextPageTokenStored);
                 mediaItems.push(...response.mediaItems);
@@ -332,7 +332,7 @@ export namespace GooglePhotos {
                 album = await Create.Album(album.title);
             }
             const media: MediaInput[] = [];
-            for (let source of sources) {
+            for (const source of sources) {
                 const data = Cast(Doc.GetProto(source).data, ImageField);
                 if (!data) {
                     return;
diff --git a/src/client/apis/youtube/YoutubeBox.tsx b/src/client/apis/youtube/YoutubeBox.tsx
index bed812852..fd3d9e2f1 100644
--- a/src/client/apis/youtube/YoutubeBox.tsx
+++ b/src/client/apis/youtube/YoutubeBox.tsx
@@ -48,44 +48,44 @@ export class YoutubeBox extends React.Component<FieldViewProps> {
      */
     async componentWillMount() {
         //DocServer.getYoutubeChannels();
-        let castedSearchBackUp = Cast(this.props.Document.cachedSearchResults, Doc);
-        let awaitedBackUp = await castedSearchBackUp;
-        let castedDetailBackUp = Cast(this.props.Document.cachedDetails, Doc);
-        let awaitedDetails = await castedDetailBackUp;
+        const castedSearchBackUp = Cast(this.props.Document.cachedSearchResults, Doc);
+        const awaitedBackUp = await castedSearchBackUp;
+        const castedDetailBackUp = Cast(this.props.Document.cachedDetails, Doc);
+        const awaitedDetails = await castedDetailBackUp;
 
 
         if (awaitedBackUp) {
 
 
-            let jsonList = await DocListCastAsync(awaitedBackUp.json);
-            let jsonDetailList = await DocListCastAsync(awaitedDetails!.json);
+            const jsonList = await DocListCastAsync(awaitedBackUp.json);
+            const jsonDetailList = await DocListCastAsync(awaitedDetails!.json);
 
             if (jsonList!.length !== 0) {
                 runInAction(() => this.searchResultsFound = true);
                 let index = 0;
                 //getting the necessary information from backUps and building templates that will be used to map in render
-                for (let video of jsonList!) {
-
-                    let videoId = await Cast(video.id, Doc);
-                    let id = StrCast(videoId!.videoId);
-                    let snippet = await Cast(video.snippet, Doc);
-                    let videoTitle = this.filterYoutubeTitleResult(StrCast(snippet!.title));
-                    let thumbnail = await Cast(snippet!.thumbnails, Doc);
-                    let thumbnailMedium = await Cast(thumbnail!.medium, Doc);
-                    let thumbnailUrl = StrCast(thumbnailMedium!.url);
-                    let videoDescription = StrCast(snippet!.description);
-                    let pusblishDate = (this.roundPublishTime(StrCast(snippet!.publishedAt)))!;
-                    let channelTitle = StrCast(snippet!.channelTitle);
+                for (const video of jsonList!) {
+
+                    const videoId = await Cast(video.id, Doc);
+                    const id = StrCast(videoId!.videoId);
+                    const snippet = await Cast(video.snippet, Doc);
+                    const videoTitle = this.filterYoutubeTitleResult(StrCast(snippet!.title));
+                    const thumbnail = await Cast(snippet!.thumbnails, Doc);
+                    const thumbnailMedium = await Cast(thumbnail!.medium, Doc);
+                    const thumbnailUrl = StrCast(thumbnailMedium!.url);
+                    const videoDescription = StrCast(snippet!.description);
+                    const pusblishDate = (this.roundPublishTime(StrCast(snippet!.publishedAt)))!;
+                    const channelTitle = StrCast(snippet!.channelTitle);
                     let duration: string = "";
                     let viewCount: string = "";
                     if (jsonDetailList!.length !== 0) {
-                        let contentDetails = await Cast(jsonDetailList![index].contentDetails, Doc);
-                        let statistics = await Cast(jsonDetailList![index].statistics, Doc);
+                        const contentDetails = await Cast(jsonDetailList![index].contentDetails, Doc);
+                        const statistics = await Cast(jsonDetailList![index].statistics, Doc);
                         duration = this.convertIsoTimeToDuration(StrCast(contentDetails!.duration));
                         viewCount = this.abbreviateViewCount(parseInt(StrCast(statistics!.viewCount)))!;
                     }
                     index = index + 1;
-                    let newTemplate: VideoTemplate = { videoId: id, videoTitle: videoTitle, thumbnailUrl: thumbnailUrl, publishDate: pusblishDate, channelTitle: channelTitle, videoDescription: videoDescription, duration: duration, viewCount: viewCount };
+                    const newTemplate: VideoTemplate = { videoId: id, videoTitle: videoTitle, thumbnailUrl: thumbnailUrl, publishDate: pusblishDate, channelTitle: channelTitle, videoDescription: videoDescription, duration: duration, viewCount: viewCount };
                     runInAction(() => this.curVideoTemplates.push(newTemplate));
                 }
             }
@@ -115,7 +115,7 @@ export class YoutubeBox extends React.Component<FieldViewProps> {
      */
     onEnterKeyDown = (e: React.KeyboardEvent) => {
         if (e.keyCode === 13) {
-            let submittedTitle = this.YoutubeSearchElement!.value;
+            const submittedTitle = this.YoutubeSearchElement!.value;
             this.YoutubeSearchElement!.value = "";
             this.YoutubeSearchElement!.blur();
             DocServer.getYoutubeVideos(submittedTitle, this.processesVideoResults);
@@ -184,23 +184,23 @@ export class YoutubeBox extends React.Component<FieldViewProps> {
      * difference between today's date and that date, in terms of "ago" to imitate youtube.
      */
     roundPublishTime = (publishTime: string) => {
-        let date = new Date(publishTime).getTime();
-        let curDate = new Date().getTime();
-        let timeDif = curDate - date;
-        let totalSeconds = timeDif / 1000;
-        let totalMin = totalSeconds / 60;
-        let totalHours = totalMin / 60;
-        let totalDays = totalHours / 24;
-        let totalMonths = totalDays / 30.417;
-        let totalYears = totalMonths / 12;
-
-
-        let truncYears = Math.trunc(totalYears);
-        let truncMonths = Math.trunc(totalMonths);
-        let truncDays = Math.trunc(totalDays);
-        let truncHours = Math.trunc(totalHours);
-        let truncMin = Math.trunc(totalMin);
-        let truncSec = Math.trunc(totalSeconds);
+        const date = new Date(publishTime).getTime();
+        const curDate = new Date().getTime();
+        const timeDif = curDate - date;
+        const totalSeconds = timeDif / 1000;
+        const totalMin = totalSeconds / 60;
+        const totalHours = totalMin / 60;
+        const totalDays = totalHours / 24;
+        const totalMonths = totalDays / 30.417;
+        const totalYears = totalMonths / 12;
+
+
+        const truncYears = Math.trunc(totalYears);
+        const truncMonths = Math.trunc(totalMonths);
+        const truncDays = Math.trunc(totalDays);
+        const truncHours = Math.trunc(totalHours);
+        const truncMin = Math.trunc(totalMin);
+        const truncSec = Math.trunc(totalSeconds);
 
         let pluralCase = "";
 
@@ -230,7 +230,7 @@ export class YoutubeBox extends React.Component<FieldViewProps> {
      */
     convertIsoTimeToDuration = (isoDur: string) => {
 
-        let convertedTime = isoDur.replace(/D|H|M/g, ":").replace(/P|T|S/g, "").split(":");
+        const convertedTime = isoDur.replace(/D|H|M/g, ":").replace(/P|T|S/g, "").split(":");
 
         if (1 === convertedTime.length) {
             2 !== convertedTime[0].length && (convertedTime[0] = "0" + convertedTime[0]), convertedTime[0] = "0:" + convertedTime[0];
@@ -269,10 +269,10 @@ export class YoutubeBox extends React.Component<FieldViewProps> {
             if (this.searchResults.length !== 0) {
                 return <ul>
                     {this.searchResults.map((video, index) => {
-                        let filteredTitle = this.filterYoutubeTitleResult(video.snippet.title);
-                        let channelTitle = video.snippet.channelTitle;
-                        let videoDescription = video.snippet.description;
-                        let pusblishDate = this.roundPublishTime(video.snippet.publishedAt);
+                        const filteredTitle = this.filterYoutubeTitleResult(video.snippet.title);
+                        const channelTitle = video.snippet.channelTitle;
+                        const videoDescription = video.snippet.description;
+                        const pusblishDate = this.roundPublishTime(video.snippet.publishedAt);
                         let duration;
                         let viewCount;
                         if (this.videoDetails.length !== 0) {
@@ -331,26 +331,26 @@ export class YoutubeBox extends React.Component<FieldViewProps> {
      */
     @action
     embedVideoOnClick = (videoId: string, filteredTitle: string) => {
-        let embeddedUrl = "https://www.youtube.com/embed/" + videoId;
+        const embeddedUrl = "https://www.youtube.com/embed/" + videoId;
         this.selectedVideoUrl = embeddedUrl;
-        let addFunction = this.props.addDocument!;
-        let newVideoX = NumCast(this.props.Document.x);
-        let newVideoY = NumCast(this.props.Document.y) + NumCast(this.props.Document.height);
+        const addFunction = this.props.addDocument!;
+        const newVideoX = NumCast(this.props.Document.x);
+        const newVideoY = NumCast(this.props.Document.y) + NumCast(this.props.Document.height);
 
         addFunction(Docs.Create.VideoDocument(embeddedUrl, { title: filteredTitle, width: 400, height: 315, x: newVideoX, y: newVideoY }));
         this.videoClicked = true;
     }
 
     render() {
-        let content =
+        const content =
             <div className="youtubeBox-cont" style={{ width: "100%", height: "100%", position: "absolute" }} onWheel={this.onPostWheel} onPointerDown={this.onPostPointer} onPointerMove={this.onPostPointer} onPointerUp={this.onPostPointer}>
                 <input type="text" placeholder="Search for a video" onKeyDown={this.onEnterKeyDown} style={{ height: 40, width: "100%", border: "1px solid black", padding: 5, textAlign: "center" }} ref={(e) => this.YoutubeSearchElement = e!} />
                 {this.renderSearchResultsOrVideo()}
             </div>;
 
-        let frozen = !this.props.isSelected() || DocumentDecorations.Instance.Interacting;
+        const frozen = !this.props.isSelected() || DocumentDecorations.Instance.Interacting;
 
-        let classname = "webBox-cont" + (this.props.isSelected() && !InkingControl.Instance.selectedTool && !DocumentDecorations.Instance.Interacting ? "-interactive" : "");
+        const classname = "webBox-cont" + (this.props.isSelected() && !InkingControl.Instance.selectedTool && !DocumentDecorations.Instance.Interacting ? "-interactive" : "");
         return (
             <>
                 <div className={classname}  >
diff --git a/src/client/cognitive_services/CognitiveServices.ts b/src/client/cognitive_services/CognitiveServices.ts
index 5a7f5e991..02eff3b25 100644
--- a/src/client/cognitive_services/CognitiveServices.ts
+++ b/src/client/cognitive_services/CognitiveServices.ts
@@ -1,5 +1,5 @@
 import * as request from "request-promise";
-import { Doc, Field, Opt } from "../../new_fields/Doc";
+import { Doc, Field } from "../../new_fields/Doc";
 import { Cast } from "../../new_fields/Types";
 import { Docs } from "../documents/Documents";
 import { Utils } from "../../Utils";
@@ -101,14 +101,14 @@ export namespace CognitiveServices {
         export namespace Appliers {
 
             export const ProcessImage: AnalysisApplier<string> = async (target: Doc, keys: string[], url: string, service: Service, converter: Converter) => {
-                let batch = UndoManager.StartBatch("Image Analysis");
+                const batch = UndoManager.StartBatch("Image Analysis");
 
-                let storageKey = keys[0];
+                const storageKey = keys[0];
                 if (!url || await Cast(target[storageKey], Doc)) {
                     return;
                 }
                 let toStore: any;
-                let results = await ExecuteQuery(service, Manager, url);
+                const results = await ExecuteQuery(service, Manager, url);
                 if (!results) {
                     toStore = "Cognitive Services could not process the given image URL.";
                 } else {
@@ -131,36 +131,32 @@ export namespace CognitiveServices {
 
     export namespace Inking {
 
-        export const Manager: APIManager<InkData> = {
-
-            converter: (inkData: InkData): string => {
-                let entries = inkData.entries(), next = entries.next();
-                let strokes: AzureStrokeData[] = [], id = 0;
-                while (!next.done) {
-                    strokes.push({
-                        id: id++,
-                        points: next.value[1].pathData.map(point => `${point.x},${point.y}`).join(","),
-                        language: "en-US"
-                    });
-                    next = entries.next();
-                }
+        export const Manager: APIManager<InkData[]> = {
+
+            converter: (inkData: InkData[]): string => {
+                let id = 0;
+                const strokes: AzureStrokeData[] = inkData.map(points => ({
+                    id: id++,
+                    points: points.map(({ x, y }) => `${x},${y}`).join(","),
+                    language: "en-US"
+                }));
                 return JSON.stringify({
                     version: 1,
                     language: "en-US",
                     unit: "mm",
-                    strokes: strokes
+                    strokes
                 });
             },
 
             requester: async (apiKey: string, body: string) => {
-                let xhttp = new XMLHttpRequest();
-                let serverAddress = "https://api.cognitive.microsoft.com";
-                let endpoint = serverAddress + "/inkrecognizer/v1.0-preview/recognize";
+                const xhttp = new XMLHttpRequest();
+                const serverAddress = "https://api.cognitive.microsoft.com";
+                const endpoint = serverAddress + "/inkrecognizer/v1.0-preview/recognize";
 
-                let promisified = (resolve: any, reject: any) => {
+                const promisified = (resolve: any, reject: any) => {
                     xhttp.onreadystatechange = function () {
                         if (this.readyState === 4) {
-                            let result = xhttp.responseText;
+                            const result = xhttp.responseText;
                             switch (this.status) {
                                 case 200:
                                     return resolve(result);
@@ -184,15 +180,15 @@ export namespace CognitiveServices {
 
         export namespace Appliers {
 
-            export const ConcatenateHandwriting: AnalysisApplier<InkData> = async (target: Doc, keys: string[], inkData: InkData) => {
-                let batch = UndoManager.StartBatch("Ink Analysis");
+            export const ConcatenateHandwriting: AnalysisApplier<InkData[]> = async (target: Doc, keys: string[], inkData: InkData[]) => {
+                const batch = UndoManager.StartBatch("Ink Analysis");
 
                 let results = await ExecuteQuery(Service.Handwriting, Manager, inkData);
                 if (results) {
                     results.recognitionUnits && (results = results.recognitionUnits);
                     target[keys[0]] = Docs.Get.DocumentHierarchyFromJson(results, "Ink Analysis");
-                    let recognizedText = results.map((item: any) => item.recognizedText);
-                    let individualWords = recognizedText.filter((text: string) => text && text.split(" ").length === 1);
+                    const recognizedText = results.map((item: any) => item.recognizedText);
+                    const individualWords = recognizedText.filter((text: string) => text && text.split(" ").length === 1);
                     target[keys[1]] = individualWords.join(" ");
                 }
 
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index d1e3ea708..e0f2858ba 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -35,10 +35,10 @@ import { CollectionDockingView } from "../views/collections/CollectionDockingVie
 import { LinkManager } from "../util/LinkManager";
 import { DocumentManager } from "../util/DocumentManager";
 import DirectoryImportBox from "../util/Import & Export/DirectoryImportBox";
-import { Scripting, CompileScript } from "../util/Scripting";
+import { Scripting } from "../util/Scripting";
 import { ButtonBox } from "../views/nodes/ButtonBox";
 import { FontIconBox } from "../views/nodes/FontIconBox";
-import { SchemaHeaderField, RandomPastel } from "../../new_fields/SchemaHeaderField";
+import { SchemaHeaderField } from "../../new_fields/SchemaHeaderField";
 import { PresBox } from "../views/nodes/PresBox";
 import { ComputedField, ScriptField } from "../../new_fields/ScriptField";
 import { ProxyField } from "../../new_fields/Proxy";
@@ -50,8 +50,8 @@ import { ColorBox } from "../views/nodes/ColorBox";
 import { DocuLinkBox } from "../views/nodes/DocuLinkBox";
 import { InkingStroke } from "../views/InkingStroke";
 import { InkField } from "../../new_fields/InkField";
-var requestImageSize = require('../util/request-image-size');
-var path = require('path');
+const requestImageSize = require('../util/request-image-size');
+const path = require('path');
 
 export interface DocumentOptions {
     x?: number;
@@ -239,16 +239,16 @@ export namespace Docs {
             ProxyField.initPlugin();
             ComputedField.initPlugin();
             // non-guid string ids for each document prototype
-            let prototypeIds = Object.values(DocumentType).filter(type => type !== DocumentType.NONE).map(type => type + suffix);
+            const prototypeIds = Object.values(DocumentType).filter(type => type !== DocumentType.NONE).map(type => type + suffix);
             // fetch the actual prototype documents from the server
-            let actualProtos = await DocServer.GetRefFields(prototypeIds);
+            const actualProtos = await DocServer.GetRefFields(prototypeIds);
 
             // update this object to include any default values: DocumentOptions for all prototypes
             prototypeIds.map(id => {
-                let existing = actualProtos[id] as Doc;
-                let type = id.replace(suffix, "") as DocumentType;
+                const existing = actualProtos[id] as Doc;
+                const type = id.replace(suffix, "") as DocumentType;
                 // get or create prototype of the specified type...
-                let target = existing || buildPrototype(type, id);
+                const target = existing || buildPrototype(type, id);
                 // ...and set it if not undefined (can be undefined only if TemplateMap does not contain
                 // an entry dedicated to the given DocumentType)
                 target && PrototypeMap.set(type, target);
@@ -287,17 +287,17 @@ export namespace Docs {
          */
         function buildPrototype(type: DocumentType, prototypeId: string): Opt<Doc> {
             // load template from type
-            let template = TemplateMap.get(type);
+            const template = TemplateMap.get(type);
             if (!template) {
                 return undefined;
             }
-            let layout = template.layout;
+            const layout = template.layout;
             // create title
-            let upper = suffix.toUpperCase();
-            let title = prototypeId.toUpperCase().replace(upper, `_${upper}`);
+            const upper = suffix.toUpperCase();
+            const title = prototypeId.toUpperCase().replace(upper, `_${upper}`);
             // synthesize the default options, the type and title from computed values and
             // whatever options pertain to this specific prototype
-            let options = { title, type, baseProto: true, ...defaultOptions, ...(template.options || {}) };
+            const options = { title, type, baseProto: true, ...defaultOptions, ...(template.options || {}) };
             options.layout = layout.view.LayoutString(layout.dataField);
             return Doc.assign(new Doc(prototypeId, true), { ...options });
         }
@@ -343,8 +343,8 @@ export namespace Docs {
 
             protoProps.isPrototype = true;
 
-            let dataDoc = MakeDataDelegate(proto, protoProps, data);
-            let viewDoc = Doc.MakeDelegate(dataDoc, delegId);
+            const dataDoc = MakeDataDelegate(proto, protoProps, data);
+            const viewDoc = Doc.MakeDelegate(dataDoc, delegId);
 
             AudioBox.ActiveRecordings.map(d => DocUtils.MakeLink({ doc: viewDoc }, { doc: d }, "audio link", "link to audio: " + d.title));
 
@@ -370,16 +370,16 @@ export namespace Docs {
         }
 
         export function ImageDocument(url: string, options: DocumentOptions = {}) {
-            let imgField = new ImageField(new URL(url));
-            let inst = InstanceFromProto(Prototypes.get(DocumentType.IMG), imgField, { title: path.basename(url), ...options });
+            const imgField = new ImageField(new URL(url));
+            const inst = InstanceFromProto(Prototypes.get(DocumentType.IMG), imgField, { title: path.basename(url), ...options });
             let target = imgField.url.href;
             if (new RegExp(window.location.origin).test(target)) {
-                let extension = path.extname(target);
+                const extension = path.extname(target);
                 target = `${target.substring(0, target.length - extension.length)}_o${extension}`;
             }
             requestImageSize(target)
                 .then((size: any) => {
-                    let aspect = size.height / size.width;
+                    const aspect = size.height / size.width;
                     if (!inst.nativeWidth) {
                         inst.nativeWidth = size.width;
                     }
@@ -423,7 +423,7 @@ export namespace Docs {
         }
 
         export function InkDocument(color: string, tool: number, strokeWidth: number, points: { x: number, y: number }[], options: DocumentOptions = {}) {
-            let doc = InstanceFromProto(Prototypes.get(DocumentType.INK), new InkField(points), options);
+            const doc = InstanceFromProto(Prototypes.get(DocumentType.INK), new InkField(points), options);
             doc.color = color;
             doc.strokeWidth = strokeWidth;
             doc.tool = tool;
@@ -439,12 +439,12 @@ export namespace Docs {
         }
 
         export async function DBDocument(url: string, options: DocumentOptions = {}, columnOptions: DocumentOptions = {}) {
-            let schemaName = options.title ? options.title : "-no schema-";
-            let ctlog = await Gateway.Instance.GetSchema(url, schemaName);
+            const schemaName = options.title ? options.title : "-no schema-";
+            const ctlog = await Gateway.Instance.GetSchema(url, schemaName);
             if (ctlog && ctlog.schemas) {
-                let schema = ctlog.schemas[0];
-                let schemaDoc = Docs.Create.TreeDocument([], { ...options, nativeWidth: undefined, nativeHeight: undefined, width: 150, height: 100, title: schema.displayName! });
-                let schemaDocuments = Cast(schemaDoc.data, listSpec(Doc), []);
+                const schema = ctlog.schemas[0];
+                const schemaDoc = Docs.Create.TreeDocument([], { ...options, nativeWidth: undefined, nativeHeight: undefined, width: 150, height: 100, title: schema.displayName! });
+                const schemaDocuments = Cast(schemaDoc.data, listSpec(Doc), []);
                 if (!schemaDocuments) {
                     return;
                 }
@@ -455,8 +455,8 @@ export namespace Docs {
                         if (field instanceof Doc) {
                             docs.push(field);
                         } else {
-                            var atmod = new ColumnAttributeModel(attr);
-                            let histoOp = new HistogramOperation(schema.displayName!,
+                            const atmod = new ColumnAttributeModel(attr);
+                            const histoOp = new HistogramOperation(schema.displayName!,
                                 new AttributeTransformationModel(atmod, AggregateFunction.None),
                                 new AttributeTransformationModel(atmod, AggregateFunction.Count),
                                 new AttributeTransformationModel(atmod, AggregateFunction.Count));
@@ -523,7 +523,7 @@ export namespace Docs {
         }
 
         export function DockDocument(documents: Array<Doc>, config: string, options: DocumentOptions, id?: string) {
-            let inst = InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, viewType: CollectionViewType.Docking, dockingConfig: config }, id);
+            const inst = InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, viewType: CollectionViewType.Docking, dockingConfig: config }, id);
             Doc.GetProto(inst).data = new List<Doc>(documents);
             return inst;
         }
@@ -538,7 +538,7 @@ export namespace Docs {
         };
 
         export function StandardCollectionDockingDocument(configs: Array<DocConfig>, options: DocumentOptions, id?: string, type: string = "row") {
-            let layoutConfig = {
+            const layoutConfig = {
                 content: [
                     {
                         type: type,
@@ -603,7 +603,8 @@ export namespace Docs {
          * might involve arbitrary recursion (since toField might itself call convertObject)
          */
         const convertObject = (object: any, title?: string): Doc => {
-            let target = new Doc(), result: Opt<Field>;
+            const target = new Doc();
+            let result: Opt<Field>;
             Object.keys(object).map(key => (result = toField(object[key], key)) && (target[key] = result));
             title && !target.title && (target.title = title);
             return target;
@@ -617,7 +618,8 @@ export namespace Docs {
          * might involve arbitrary recursion (since toField might itself call convertList)
          */
         const convertList = (list: Array<any>): List<Field> => {
-            let target = new List(), result: Opt<Field>;
+            const target = new List();
+            let result: Opt<Field>;
             list.map(item => (result = toField(item)) && target.push(result));
             return target;
         };
@@ -658,11 +660,11 @@ export namespace Docs {
             }
             if (type.indexOf("html") !== -1) {
                 if (path.includes(window.location.hostname)) {
-                    let s = path.split('/');
-                    let id = s[s.length - 1];
+                    const s = path.split('/');
+                    const id = s[s.length - 1];
                     return DocServer.GetRefField(id).then(field => {
                         if (field instanceof Doc) {
-                            let alias = Doc.MakeAlias(field);
+                            const alias = Doc.MakeAlias(field);
                             alias.x = options.x || 0;
                             alias.y = options.y || 0;
                             alias.width = options.width || 300;
@@ -699,9 +701,9 @@ export namespace DocUtils {
                     DocListCastAsync(promoteDoc.links).then(links => {
                         links && links.map(async link => {
                             if (link) {
-                                let a1 = await Cast(link.anchor1, Doc);
+                                const a1 = await Cast(link.anchor1, Doc);
                                 if (a1 && Doc.AreProtosEqual(a1, promoteDoc)) link.anchor1 = copy;
-                                let a2 = await Cast(link.anchor2, Doc);
+                                const a2 = await Cast(link.anchor2, Doc);
                                 if (a2 && Doc.AreProtosEqual(a2, promoteDoc)) link.anchor2 = copy;
                                 LinkManager.Instance.deleteLink(link);
                                 LinkManager.Instance.addLink(link);
@@ -714,11 +716,11 @@ export namespace DocUtils {
     }
 
     export function MakeLink(source: { doc: Doc, ctx?: Doc }, target: { doc: Doc, ctx?: Doc }, title: string = "", description: string = "", id?: string) {
-        let sv = DocumentManager.Instance.getDocumentView(source.doc);
+        const sv = DocumentManager.Instance.getDocumentView(source.doc);
         if (sv && sv.props.ContainingCollectionDoc === target.doc) return;
         if (target.doc === CurrentUserUtils.UserDocument) return undefined;
 
-        let linkDocProto = new Doc(id, true);
+        const linkDocProto = new Doc(id, true);
         UndoManager.RunInBatch(() => {
             linkDocProto.type = DocumentType.LINK;
 
diff --git a/src/client/northstar/dash-fields/HistogramField.ts b/src/client/northstar/dash-fields/HistogramField.ts
index e6f32272e..f3365e73d 100644
--- a/src/client/northstar/dash-fields/HistogramField.ts
+++ b/src/client/northstar/dash-fields/HistogramField.ts
@@ -10,7 +10,7 @@ import { Deserializable } from "../../util/SerializationHelper";
 import { Copy, ToScriptString } from "../../../new_fields/FieldSymbols";
 
 function serialize(field: HistogramField) {
-    let obj = OmitKeys(field, ['Links', 'BrushLinks', 'Result', 'BrushColors', 'FilterModels', 'FilterOperand']).omit;
+    const obj = OmitKeys(field, ['Links', 'BrushLinks', 'Result', 'BrushColors', 'FilterModels', 'FilterOperand']).omit;
     return obj;
 }
 
@@ -19,7 +19,7 @@ function deserialize(jp: any) {
     let Y: AttributeTransformationModel | undefined;
     let V: AttributeTransformationModel | undefined;
 
-    let schema = CurrentUserUtils.GetNorthstarSchema(jp.SchemaName);
+    const schema = CurrentUserUtils.GetNorthstarSchema(jp.SchemaName);
     if (schema) {
         CurrentUserUtils.GetAllNorthstarColumnAttributes(schema).map(attr => {
             if (attr.displayName === jp.X.AttributeModel.Attribute.DisplayName) {
@@ -52,8 +52,8 @@ export class HistogramField extends ObjectField {
     }
 
     [Copy]() {
-        let y = this.HistoOp;
-        let z = this.HistoOp.Copy;
+        // const y = this.HistoOp;
+        // const z = this.HistoOp.Copy;
         return new HistogramField(HistogramOperation.Duplicate(this.HistoOp));
     }
 
diff --git a/src/client/northstar/model/binRanges/QuantitativeVisualBinRange.ts b/src/client/northstar/model/binRanges/QuantitativeVisualBinRange.ts
index c579c8e5f..7bc097e1d 100644
--- a/src/client/northstar/model/binRanges/QuantitativeVisualBinRange.ts
+++ b/src/client/northstar/model/binRanges/QuantitativeVisualBinRange.ts
@@ -37,7 +37,7 @@ export class QuantitativeVisualBinRange extends VisualBinRange {
     }
 
     public GetBins(): number[] {
-        let bins = new Array<number>();
+        const bins = new Array<number>();
 
         for (let v: number = this.DataBinRange.minValue!; v < this.DataBinRange.maxValue!; v += this.DataBinRange.step!) {
             bins.push(v);
@@ -46,8 +46,8 @@ export class QuantitativeVisualBinRange extends VisualBinRange {
     }
 
     public static Initialize(dataMinValue: number, dataMaxValue: number, targetBinNumber: number, isIntegerRange: boolean): QuantitativeVisualBinRange {
-        let extent = QuantitativeVisualBinRange.getExtent(dataMinValue, dataMaxValue, targetBinNumber, isIntegerRange);
-        let dataBinRange = new QuantitativeBinRange();
+        const extent = QuantitativeVisualBinRange.getExtent(dataMinValue, dataMaxValue, targetBinNumber, isIntegerRange);
+        const dataBinRange = new QuantitativeBinRange();
         dataBinRange.minValue = extent[0];
         dataBinRange.maxValue = extent[1];
         dataBinRange.step = extent[2];
@@ -60,10 +60,10 @@ export class QuantitativeVisualBinRange extends VisualBinRange {
             // dataMin -= 0.1;
             dataMax += 0.1;
         }
-        let span = dataMax - dataMin;
+        const span = dataMax - dataMin;
 
         let step = Math.pow(10, Math.floor(Math.log10(span / m)));
-        let err = m / span * step;
+        const err = m / span * step;
 
         if (err <= .15) {
             step *= 10;
@@ -78,9 +78,9 @@ export class QuantitativeVisualBinRange extends VisualBinRange {
         if (isIntegerRange) {
             step = Math.ceil(step);
         }
-        let ret: number[] = new Array<number>(3);
-        let minDivStep = Math.floor(dataMin / step);
-        let maxDivStep = Math.floor(dataMax / step);
+        const ret: number[] = new Array<number>(3);
+        const minDivStep = Math.floor(dataMin / step);
+        const maxDivStep = Math.floor(dataMax / step);
         ret[0] = minDivStep * step; // Math.floor(Math.Round(dataMin, 8)/step)*step;
         ret[1] = maxDivStep * step + step; // Math.floor(Math.Round(dataMax, 8)/step)*step + step;
         ret[2] = step;
diff --git a/src/client/northstar/operations/BaseOperation.ts b/src/client/northstar/operations/BaseOperation.ts
index 0d1361ebf..013f2244e 100644
--- a/src/client/northstar/operations/BaseOperation.ts
+++ b/src/client/northstar/operations/BaseOperation.ts
@@ -44,12 +44,12 @@ export abstract class BaseOperation {
                 }
             }
 
-            let operationParameters = this.CreateOperationParameters();
+            const operationParameters = this.CreateOperationParameters();
             if (this.Result) {
                 this.Result.progress = 0;
             } // bcz: used to set Result to undefined, but that causes the display to blink
             this.Error = "";
-            let salt = Math.random().toString();
+            const salt = Math.random().toString();
             this.RequestSalt = salt;
 
             if (!operationParameters) {
@@ -59,27 +59,27 @@ export abstract class BaseOperation {
 
             this.ComputationStarted = true;
             //let start = performance.now();
-            let promise = Gateway.Instance.StartOperation(operationParameters.toJSON());
+            const promise = Gateway.Instance.StartOperation(operationParameters.toJSON());
             promise.catch(err => {
                 action(() => {
                     this.Error = err;
                     console.error(err);
                 });
             });
-            let operationReference = await promise;
+            const operationReference = await promise;
 
 
             if (operationReference) {
                 this.OperationReference = operationReference;
 
-                let resultParameters = new ResultParameters();
+                const resultParameters = new ResultParameters();
                 resultParameters.operationReference = operationReference;
 
-                let pollPromise = new PollPromise(salt, operationReference);
+                const pollPromise = new PollPromise(salt, operationReference);
                 BaseOperation._currentOperations.set(this.Id, pollPromise);
 
                 pollPromise.Start(async () => {
-                    let result = await Gateway.Instance.GetResult(resultParameters.toJSON());
+                    const result = await Gateway.Instance.GetResult(resultParameters.toJSON());
                     if (result instanceof ErrorResult) {
                         throw new Error((result).message);
                     }
diff --git a/src/client/northstar/utils/MathUtil.ts b/src/client/northstar/utils/MathUtil.ts
index 4b44f40c3..5def5e704 100644
--- a/src/client/northstar/utils/MathUtil.ts
+++ b/src/client/northstar/utils/MathUtil.ts
@@ -92,37 +92,37 @@ export class MathUtil {
 
     public static DistToLineSegment(v: PIXIPoint, w: PIXIPoint, p: PIXIPoint) {
         // Return minimum distance between line segment vw and point p
-        var l2 = MathUtil.DistSquared(v, w);  // i.e. |w-v|^2 -  avoid a sqrt
+        const l2 = MathUtil.DistSquared(v, w);  // i.e. |w-v|^2 -  avoid a sqrt
         if (l2 === 0.0) return MathUtil.Dist(p, v);   // v === w case
         // Consider the line extending the segment, parameterized as v + t (w - v).
         // We find projection of point p onto the line. 
         // It falls where t = [(p-v) . (w-v)] / |w-v|^2
         // We clamp t from [0,1] to handle points outside the segment vw.
-        var dot = MathUtil.Dot(
+        const dot = MathUtil.Dot(
             MathUtil.SubtractPoint(p, v),
             MathUtil.SubtractPoint(w, v)) / l2;
-        var t = Math.max(0, Math.min(1, dot));
+        const t = Math.max(0, Math.min(1, dot));
         // Projection falls on the segment
-        var projection = MathUtil.AddPoint(v,
+        const projection = MathUtil.AddPoint(v,
             MathUtil.MultiplyConstant(
                 MathUtil.SubtractPoint(w, v), t));
         return MathUtil.Dist(p, projection);
     }
 
     public static LineSegmentIntersection(ps1: PIXIPoint, pe1: PIXIPoint, ps2: PIXIPoint, pe2: PIXIPoint): PIXIPoint | undefined {
-        var a1 = pe1.y - ps1.y;
-        var b1 = ps1.x - pe1.x;
+        const a1 = pe1.y - ps1.y;
+        const b1 = ps1.x - pe1.x;
 
-        var a2 = pe2.y - ps2.y;
-        var b2 = ps2.x - pe2.x;
+        const a2 = pe2.y - ps2.y;
+        const b2 = ps2.x - pe2.x;
 
-        var delta = a1 * b2 - a2 * b1;
+        const delta = a1 * b2 - a2 * b1;
         if (delta === 0) {
             return undefined;
         }
-        var c2 = a2 * ps2.x + b2 * ps2.y;
-        var c1 = a1 * ps1.x + b1 * ps1.y;
-        var invdelta = 1 / delta;
+        const c2 = a2 * ps2.x + b2 * ps2.y;
+        const c1 = a1 * ps1.x + b1 * ps1.y;
+        const invdelta = 1 / delta;
         return new PIXIPoint((b2 * c1 - b1 * c2) * invdelta, (a1 * c2 - a2 * c1) * invdelta);
     }
 
@@ -144,13 +144,13 @@ export class MathUtil {
     }
 
     public static LinePIXIRectangleIntersection(lineFrom: PIXIPoint, lineTo: PIXIPoint, rect: PIXIRectangle): Array<PIXIPoint> {
-        var r1 = new PIXIPoint(rect.left, rect.top);
-        var r2 = new PIXIPoint(rect.right, rect.top);
-        var r3 = new PIXIPoint(rect.right, rect.bottom);
-        var r4 = new PIXIPoint(rect.left, rect.bottom);
-        var ret = new Array<PIXIPoint>();
-        var dist = this.Dist(lineFrom, lineTo);
-        var inter = this.LineSegmentIntersection(lineFrom, lineTo, r1, r2);
+        const r1 = new PIXIPoint(rect.left, rect.top);
+        const r2 = new PIXIPoint(rect.right, rect.top);
+        const r3 = new PIXIPoint(rect.right, rect.bottom);
+        const r4 = new PIXIPoint(rect.left, rect.bottom);
+        const ret = new Array<PIXIPoint>();
+        const dist = this.Dist(lineFrom, lineTo);
+        let inter = this.LineSegmentIntersection(lineFrom, lineTo, r1, r2);
         if (inter && this.PointInPIXIRectangle(inter, rect) &&
             this.Dist(inter, lineFrom) < dist && this.Dist(inter, lineTo) < dist) {
             ret.push(inter);
@@ -190,7 +190,7 @@ export class MathUtil {
     }
 
     public static Normalize(p1: PIXIPoint) {
-        var d = this.Length(p1);
+        const d = this.Length(p1);
         return new PIXIPoint(p1.x / d, p1.y / d);
     }
 
@@ -236,8 +236,8 @@ export class MathUtil {
     }
 
     public static Combinations<T>(chars: T[]) {
-        let result = new Array<T>();
-        let f = (prefix: any, chars: any) => {
+        const result = new Array<T>();
+        const f = (prefix: any, chars: any) => {
             for (let i = 0; i < chars.length; i++) {
                 result.push(prefix.concat(chars[i]));
                 f(prefix.concat(chars[i]), chars.slice(i + 1));
diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts
index 6bbd3d0ed..3d8f2d234 100644
--- a/src/client/util/DictationManager.ts
+++ b/src/client/util/DictationManager.ts
@@ -11,7 +11,6 @@ import { Cast, CastCtor } from "../../new_fields/Types";
 import { listSpec } from "../../new_fields/Schema";
 import { AudioField, ImageField } from "../../new_fields/URLField";
 import { HistogramField } from "../northstar/dash-fields/HistogramField";
-import { MainView } from "../views/MainView";
 import { Utils } from "../../Utils";
 import { RichTextField } from "../../new_fields/RichTextField";
 import { DictationOverlay } from "../views/DictationOverlay";
@@ -48,7 +47,7 @@ export namespace DictationManager {
 
         export const Infringed = "unable to process: dictation manager still involved in previous session";
         const browser = (() => {
-            let identifier = navigator.userAgent.toLowerCase();
+            const identifier = navigator.userAgent.toLowerCase();
             if (identifier.indexOf("safari") >= 0) {
                 return "Safari";
             }
@@ -90,7 +89,7 @@ export namespace DictationManager {
         export const listen = async (options?: Partial<ListeningOptions>) => {
             let results: string | undefined;
 
-            let overlay = options !== undefined && options.useOverlay;
+            const overlay = options !== undefined && options.useOverlay;
             if (overlay) {
                 DictationOverlay.Instance.dictationOverlayVisible = true;
                 DictationOverlay.Instance.isListening = { interim: false };
@@ -102,7 +101,7 @@ export namespace DictationManager {
                     Utils.CopyText(results);
                     if (overlay) {
                         DictationOverlay.Instance.isListening = false;
-                        let execute = options && options.tryExecute;
+                        const execute = options && options.tryExecute;
                         DictationOverlay.Instance.dictatedPhrase = execute ? results.toLowerCase() : results;
                         DictationOverlay.Instance.dictationSuccess = execute ? await DictationManager.Commands.execute(results) : true;
                     }
@@ -131,12 +130,12 @@ export namespace DictationManager {
             }
             isListening = true;
 
-            let handler = options ? options.interimHandler : undefined;
-            let continuous = options ? options.continuous : undefined;
-            let indefinite = continuous && continuous.indefinite;
-            let language = options ? options.language : undefined;
-            let intra = options && options.delimiters ? options.delimiters.intra : undefined;
-            let inter = options && options.delimiters ? options.delimiters.inter : undefined;
+            const handler = options ? options.interimHandler : undefined;
+            const continuous = options ? options.continuous : undefined;
+            const indefinite = continuous && continuous.indefinite;
+            const language = options ? options.language : undefined;
+            const intra = options && options.delimiters ? options.delimiters.intra : undefined;
+            const inter = options && options.delimiters ? options.delimiters.inter : undefined;
 
             recognizer.onstart = () => console.log("initiating speech recognition session...");
             recognizer.interimResults = handler !== undefined;
@@ -177,7 +176,7 @@ export namespace DictationManager {
                     recognizer.start();
                 };
 
-                let complete = () => {
+                const complete = () => {
                     if (indefinite) {
                         current && sessionResults.push(current);
                         sessionResults.length && resolve(sessionResults.join(inter || interSession));
@@ -213,8 +212,8 @@ export namespace DictationManager {
         };
 
         const synthesize = (e: SpeechRecognitionEvent, delimiter?: string) => {
-            let results = e.results;
-            let transcripts: string[] = [];
+            const results = e.results;
+            const transcripts: string[] = [];
             for (let i = 0; i < results.length; i++) {
                 transcripts.push(results.item(i).item(0).transcript.trim());
             }
@@ -238,18 +237,18 @@ export namespace DictationManager {
 
         export const execute = async (phrase: string) => {
             return UndoManager.RunInBatch(async () => {
-                let targets = SelectionManager.SelectedDocuments();
+                const targets = SelectionManager.SelectedDocuments();
                 if (!targets || !targets.length) {
                     return;
                 }
 
                 phrase = phrase.toLowerCase();
-                let entry = Independent.get(phrase);
+                const entry = Independent.get(phrase);
 
                 if (entry) {
                     let success = false;
-                    let restrictTo = entry.restrictTo;
-                    for (let target of targets) {
+                    const restrictTo = entry.restrictTo;
+                    for (const target of targets) {
                         if (!restrictTo || validate(target, restrictTo)) {
                             await entry.action(target);
                             success = true;
@@ -258,14 +257,14 @@ export namespace DictationManager {
                     return success;
                 }
 
-                for (let entry of Dependent) {
-                    let regex = entry.expression;
-                    let matches = regex.exec(phrase);
+                for (const entry of Dependent) {
+                    const regex = entry.expression;
+                    const matches = regex.exec(phrase);
                     regex.lastIndex = 0;
                     if (matches !== null) {
                         let success = false;
-                        let restrictTo = entry.restrictTo;
-                        for (let target of targets) {
+                        const restrictTo = entry.restrictTo;
+                        for (const target of targets) {
                             if (!restrictTo || validate(target, restrictTo)) {
                                 await entry.action(target, matches);
                                 success = true;
@@ -289,7 +288,7 @@ export namespace DictationManager {
         ]);
 
         const tryCast = (view: DocumentView, type: DocumentType) => {
-            let ctor = ConstructorMap.get(type);
+            const ctor = ConstructorMap.get(type);
             if (!ctor) {
                 return false;
             }
@@ -297,7 +296,7 @@ export namespace DictationManager {
         };
 
         const validate = (target: DocumentView, types: DocumentType[]) => {
-            for (let type of types) {
+            for (const type of types) {
                 if (tryCast(target, type)) {
                     return true;
                 }
@@ -306,11 +305,11 @@ export namespace DictationManager {
         };
 
         const interpretNumber = (number: string) => {
-            let initial = parseInt(number);
+            const initial = parseInt(number);
             if (!isNaN(initial)) {
                 return initial;
             }
-            let converted = interpreter.wordsToNumbers(number, { fuzzy: true });
+            const converted = interpreter.wordsToNumbers(number, { fuzzy: true });
             if (converted === null) {
                 return NaN;
             }
@@ -326,20 +325,20 @@ export namespace DictationManager {
 
             ["open fields", {
                 action: (target: DocumentView) => {
-                    let kvp = Docs.Create.KVPDocument(target.props.Document, { width: 300, height: 300 });
+                    const kvp = Docs.Create.KVPDocument(target.props.Document, { width: 300, height: 300 });
                     target.props.addDocTab(kvp, target.props.DataDoc, "onRight");
                 }
             }],
 
             ["new outline", {
                 action: (target: DocumentView) => {
-                    let newBox = Docs.Create.TextDocument({ width: 400, height: 200, title: "My Outline" });
+                    const newBox = Docs.Create.TextDocument({ width: 400, height: 200, title: "My Outline" });
                     newBox.autoHeight = true;
-                    let proto = newBox.proto!;
-                    let prompt = "Press alt + r to start dictating here...";
-                    let head = 3;
-                    let anchor = head + prompt.length;
-                    let proseMirrorState = `{"doc":{"type":"doc","content":[{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"type":"text","text":"${prompt}"}]}]}]}]},"selection":{"type":"text","anchor":${anchor},"head":${head}}}`;
+                    const proto = newBox.proto!;
+                    const prompt = "Press alt + r to start dictating here...";
+                    const head = 3;
+                    const anchor = head + prompt.length;
+                    const proseMirrorState = `{"doc":{"type":"doc","content":[{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"type":"text","text":"${prompt}"}]}]}]}]},"selection":{"type":"text","anchor":${anchor},"head":${head}}}`;
                     proto.data = new RichTextField(proseMirrorState);
                     proto.backgroundColor = "#eeffff";
                     target.props.addDocTab(newBox, proto, "onRight");
@@ -353,10 +352,10 @@ export namespace DictationManager {
             {
                 expression: /create (\w+) documents of type (image|nested collection)/g,
                 action: (target: DocumentView, matches: RegExpExecArray) => {
-                    let count = interpretNumber(matches[1]);
-                    let what = matches[2];
-                    let dataDoc = Doc.GetProto(target.props.Document);
-                    let fieldKey = "data";
+                    const count = interpretNumber(matches[1]);
+                    const what = matches[2];
+                    const dataDoc = Doc.GetProto(target.props.Document);
+                    const fieldKey = "data";
                     if (isNaN(count)) {
                         return;
                     }
@@ -379,7 +378,7 @@ export namespace DictationManager {
             {
                 expression: /view as (freeform|stacking|masonry|schema|tree)/g,
                 action: (target: DocumentView, matches: RegExpExecArray) => {
-                    let mode = CollectionViewType.valueOf(matches[1]);
+                    const mode = CollectionViewType.valueOf(matches[1]);
                     mode && (target.props.Document.viewType = mode);
                 },
                 restrictTo: [DocumentType.COL]
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 346e88f40..d491cd1b1 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -33,7 +33,7 @@ export class DocumentManager {
 
     //gets all views
     public getDocumentViewsById(id: string) {
-        let toReturn: DocumentView[] = [];
+        const toReturn: DocumentView[] = [];
         DocumentManager.Instance.DocumentViews.map(view => {
             if (view.props.Document[Id] === id) {
                 toReturn.push(view);
@@ -41,7 +41,7 @@ export class DocumentManager {
         });
         if (toReturn.length === 0) {
             DocumentManager.Instance.DocumentViews.map(view => {
-                let doc = view.props.Document.proto;
+                const doc = view.props.Document.proto;
                 if (doc && doc[Id] && doc[Id] === id) {
                     toReturn.push(view);
                 }
@@ -57,9 +57,9 @@ export class DocumentManager {
     public getDocumentViewById(id: string, preferredCollection?: CollectionView): DocumentView | undefined {
 
         let toReturn: DocumentView | undefined;
-        let passes = preferredCollection ? [preferredCollection, undefined] : [undefined];
+        const passes = preferredCollection ? [preferredCollection, undefined] : [undefined];
 
-        for (let pass of passes) {
+        for (const pass of passes) {
             DocumentManager.Instance.DocumentViews.map(view => {
                 if (view.props.Document[Id] === id && (!pass || view.props.ContainingCollectionView === preferredCollection)) {
                     toReturn = view;
@@ -68,7 +68,7 @@ export class DocumentManager {
             });
             if (!toReturn) {
                 DocumentManager.Instance.DocumentViews.map(view => {
-                    let doc = view.props.Document.proto;
+                    const doc = view.props.Document.proto;
                     if (doc && doc[Id] === id && (!pass || view.props.ContainingCollectionView === preferredCollection)) {
                         toReturn = view;
                     }
@@ -90,7 +90,7 @@ export class DocumentManager {
         return views.length ? views[0] : undefined;
     }
     public getDocumentViews(toFind: Doc): DocumentView[] {
-        let toReturn: DocumentView[] = [];
+        const toReturn: DocumentView[] = [];
 
         DocumentManager.Instance.DocumentViews.map(view =>
             Doc.AreProtosEqual(view.props.Document, toFind) && toReturn.push(view));
@@ -100,17 +100,17 @@ export class DocumentManager {
 
     @computed
     public get LinkedDocumentViews() {
-        let pairs = DocumentManager.Instance.DocumentViews.filter(dv =>
+        const pairs = DocumentManager.Instance.DocumentViews.filter(dv =>
             (dv.isSelected() || Doc.IsBrushed(dv.props.Document)) // draw links from DocumentViews that are selected or brushed OR
             || DocumentManager.Instance.DocumentViews.some(dv2 => {                                                  // Documentviews which
-                let rest = DocListCast(dv2.props.Document.links).some(l => Doc.AreProtosEqual(l, dv.props.Document));// are link doc anchors 
-                let init = (dv2.isSelected() || Doc.IsBrushed(dv2.props.Document)) && dv2.Document.type !== DocumentType.AUDIO;  // on a view that is selected or brushed
+                const rest = DocListCast(dv2.props.Document.links).some(l => Doc.AreProtosEqual(l, dv.props.Document));// are link doc anchors 
+                const init = (dv2.isSelected() || Doc.IsBrushed(dv2.props.Document)) && dv2.Document.type !== DocumentType.AUDIO;  // on a view that is selected or brushed
                 return init && rest;
             })
         ).reduce((pairs, dv) => {
-            let linksList = LinkManager.Instance.getAllRelatedLinks(dv.props.Document);
+            const linksList = LinkManager.Instance.getAllRelatedLinks(dv.props.Document);
             pairs.push(...linksList.reduce((pairs, link) => {
-                let linkToDoc = link && LinkManager.Instance.getOppositeAnchor(link, dv.props.Document);
+                const linkToDoc = link && LinkManager.Instance.getOppositeAnchor(link, dv.props.Document);
                 linkToDoc && DocumentManager.Instance.getDocumentViews(linkToDoc).map(docView1 => {
                     if (dv.props.Document.type !== DocumentType.LINK || dv.props.layoutKey !== docView1.props.layoutKey) {
                         pairs.push({ a: dv, b: docView1, l: link });
@@ -125,7 +125,7 @@ export class DocumentManager {
     }
 
     public jumpToDocument = async (targetDoc: Doc, willZoom: boolean, dockFunc?: (doc: Doc) => void, docContext?: Doc, linkId?: string, closeContextIfNotFound: boolean = false): Promise<void> => {
-        let highlight = () => {
+        const highlight = () => {
             const finalDocView = DocumentManager.Instance.getFirstDocumentView(targetDoc);
             finalDocView && (finalDocView.Document.scrollToLinkID = linkId);
             finalDocView && Doc.linkFollowHighlight(finalDocView.props.Document);
@@ -199,12 +199,12 @@ export class DocumentManager {
 
     @action
     zoomIntoScale = (docDelegate: Doc, scale: number) => {
-        let docView = DocumentManager.Instance.getDocumentView(Doc.GetProto(docDelegate));
+        const docView = DocumentManager.Instance.getDocumentView(Doc.GetProto(docDelegate));
         docView && docView.props.zoomToScale(scale);
     }
 
     getScaleOfDocView = (docDelegate: Doc) => {
-        let doc = Doc.GetProto(docDelegate);
+        const doc = Doc.GetProto(docDelegate);
 
         const docView = DocumentManager.Instance.getDocumentView(doc);
         if (docView) {
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index bbc29585c..b681387d1 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -1,7 +1,6 @@
 import { action, runInAction } from "mobx";
 import { Doc, Field } from "../../new_fields/Doc";
-import { Cast, StrCast, ScriptCast } from "../../new_fields/Types";
-import { URLField } from "../../new_fields/URLField";
+import { Cast, ScriptCast } from "../../new_fields/Types";
 import { emptyFunction } from "../../Utils";
 import { CollectionDockingView } from "../views/collections/CollectionDockingView";
 import * as globalCssVariables from "../views/globalCssVariables.scss";
@@ -27,14 +26,14 @@ export function SetupDrag(
     dontHideOnDrop?: boolean,
     dragStarted?: () => void
 ) {
-    let onRowMove = async (e: PointerEvent) => {
+    const onRowMove = async (e: PointerEvent) => {
         e.stopPropagation();
         e.preventDefault();
 
         document.removeEventListener("pointermove", onRowMove);
         document.removeEventListener('pointerup', onRowUp);
-        let doc = await docFunc();
-        var dragData = new DragManager.DocumentDragData([doc]);
+        const doc = await docFunc();
+        const dragData = new DragManager.DocumentDragData([doc]);
         dragData.dropAction = dropAction;
         dragData.moveDocument = moveFunc;
         dragData.options = options;
@@ -42,11 +41,11 @@ export function SetupDrag(
         DragManager.StartDocumentDrag([_reference.current!], dragData, e.x, e.y);
         dragStarted && dragStarted();
     };
-    let onRowUp = (): void => {
+    const onRowUp = (): void => {
         document.removeEventListener("pointermove", onRowMove);
         document.removeEventListener('pointerup', onRowUp);
     };
-    let onItemDown = async (e: React.PointerEvent) => {
+    const onItemDown = async (e: React.PointerEvent) => {
         if (e.button === 0) {
             e.stopPropagation();
             if (e.shiftKey && CollectionDockingView.Instance) {
@@ -74,11 +73,11 @@ function moveLinkedDocument(doc: Doc, targetCollection: Doc, addDocument: (doc:
 }
 
 export async function DragLinkAsDocument(dragEle: HTMLElement, x: number, y: number, linkDoc: Doc, sourceDoc: Doc) {
-    let draggeddoc = LinkManager.Instance.getOppositeAnchor(linkDoc, sourceDoc);
+    const draggeddoc = LinkManager.Instance.getOppositeAnchor(linkDoc, sourceDoc);
     if (draggeddoc) {
-        let moddrag = await Cast(draggeddoc.annotationOn, Doc);
-        let dragdocs = moddrag ? [moddrag] : [draggeddoc];
-        let dragData = new DragManager.DocumentDragData(dragdocs);
+        const moddrag = await Cast(draggeddoc.annotationOn, Doc);
+        const dragdocs = moddrag ? [moddrag] : [draggeddoc];
+        const dragData = new DragManager.DocumentDragData(dragdocs);
         dragData.moveDocument = moveLinkedDocument;
         DragManager.StartLinkedDocumentDrag([dragEle], dragData, x, y, {
             handlers: {
@@ -90,26 +89,26 @@ export async function DragLinkAsDocument(dragEle: HTMLElement, x: number, y: num
 }
 
 export async function DragLinksAsDocuments(dragEle: HTMLElement, x: number, y: number, sourceDoc: Doc, singleLink?: Doc) {
-    let srcTarg = sourceDoc.proto;
+    const srcTarg = sourceDoc.proto;
     let draggedDocs: Doc[] = [];
 
     if (srcTarg) {
-        let linkDocs = singleLink ? [singleLink] : LinkManager.Instance.getAllRelatedLinks(srcTarg);
+        const linkDocs = singleLink ? [singleLink] : LinkManager.Instance.getAllRelatedLinks(srcTarg);
         if (linkDocs) {
             draggedDocs = linkDocs.map(link => {
-                let opp = LinkManager.Instance.getOppositeAnchor(link, sourceDoc);
+                const opp = LinkManager.Instance.getOppositeAnchor(link, sourceDoc);
                 if (opp) return opp;
             }) as Doc[];
         }
     }
     if (draggedDocs.length) {
-        let moddrag: Doc[] = [];
+        const moddrag: Doc[] = [];
         for (const draggedDoc of draggedDocs) {
-            let doc = await Cast(draggedDoc.annotationOn, Doc);
+            const doc = await Cast(draggedDoc.annotationOn, Doc);
             if (doc) moddrag.push(doc);
         }
-        let dragdocs = moddrag.length ? moddrag : draggedDocs;
-        let dragData = new DragManager.DocumentDragData(dragdocs);
+        const dragdocs = moddrag.length ? moddrag : draggedDocs;
+        const dragData = new DragManager.DocumentDragData(dragdocs);
         dragData.moveDocument = moveLinkedDocument;
         DragManager.StartLinkedDocumentDrag([dragEle], dragData, x, y, {
             handlers: {
@@ -254,11 +253,11 @@ export namespace DragManager {
     }
 
     export function StartButtonDrag(eles: HTMLElement[], script: string, title: string, vars: { [name: string]: Field }, params: string[], initialize: (button: Doc) => void, downX: number, downY: number, options?: DragOptions) {
-        let dragData = new DragManager.DocumentDragData([]);
+        const dragData = new DragManager.DocumentDragData([]);
         runInAction(() => StartDragFunctions.map(func => func()));
         StartDrag(eles, dragData, downX, downY, options, options && options.finishDrag ? options.finishDrag :
             (dropData: { [id: string]: any }) => {
-                let bd = Docs.Create.ButtonDocument({ width: 150, height: 50, title: title });
+                const bd = Docs.Create.ButtonDocument({ width: 150, height: 50, title: title });
                 bd.onClick = ScriptField.MakeScript(script);
                 params.map(p => Object.keys(vars).indexOf(p) !== -1 && (Doc.GetProto(bd)[p] = new PrefetchProxy(vars[p] as Doc)));
                 initialize && initialize(bd);
@@ -273,11 +272,11 @@ export namespace DragManager {
         runInAction(() => StartDragFunctions.map(func => func()));
         StartDrag(eles, dragData, downX, downY, options,
             (dropData: { [id: string]: any }) => {
-                let droppedDocuments: Doc[] = dragData.draggedDocuments.reduce((droppedDocs: Doc[], d) => {
-                    let dvs = DocumentManager.Instance.getDocumentViews(d);
+                const droppedDocuments: Doc[] = dragData.draggedDocuments.reduce((droppedDocs: Doc[], d) => {
+                    const dvs = DocumentManager.Instance.getDocumentViews(d);
                     if (dvs.length) {
-                        let containingView = SelectionManager.SelectedDocuments()[0] ? SelectionManager.SelectedDocuments()[0].props.ContainingCollectionView : undefined;
-                        let inContext = dvs.filter(dv => dv.props.ContainingCollectionView === containingView);
+                        const containingView = SelectionManager.SelectedDocuments()[0] ? SelectionManager.SelectedDocuments()[0].props.ContainingCollectionView : undefined;
+                        const inContext = dvs.filter(dv => dv.props.ContainingCollectionView === containingView);
                         if (inContext.length) {
                             inContext.forEach(dv => droppedDocs.push(dv.props.Document));
                         } else {
@@ -336,26 +335,26 @@ export namespace DragManager {
             DragManager.Root().appendChild(dragDiv);
         }
         SelectionManager.SetIsDragging(true);
-        let scaleXs: number[] = [];
-        let scaleYs: number[] = [];
-        let xs: number[] = [];
-        let ys: number[] = [];
+        const scaleXs: number[] = [];
+        const scaleYs: number[] = [];
+        const xs: number[] = [];
+        const ys: number[] = [];
 
         const docs = dragData instanceof DocumentDragData ? dragData.draggedDocuments :
             dragData instanceof AnnotationDragData ? [dragData.dragDocument] : [];
-        let dragElements = eles.map(ele => {
+        const dragElements = eles.map(ele => {
             const w = ele.offsetWidth,
                 h = ele.offsetHeight;
             const rect = ele.getBoundingClientRect();
             const scaleX = rect.width / w,
                 scaleY = rect.height / h;
-            let x = rect.left,
+            const x = rect.left,
                 y = rect.top;
             xs.push(x);
             ys.push(y);
             scaleXs.push(scaleX);
             scaleYs.push(scaleY);
-            let dragElement = ele.cloneNode(true) as HTMLElement;
+            const dragElement = ele.cloneNode(true) as HTMLElement;
             dragElement.style.opacity = "0.7";
             dragElement.style.borderRadius = getComputedStyle(ele).borderRadius;
             dragElement.style.position = "absolute";
@@ -372,25 +371,25 @@ export namespace DragManager {
             dragElement.style.height = `${rect.height / scaleY}px`;
 
             if (docs.length) {
-                var pdfBox = dragElement.getElementsByTagName("canvas");
-                var pdfBoxSrc = ele.getElementsByTagName("canvas");
+                const pdfBox = dragElement.getElementsByTagName("canvas");
+                const pdfBoxSrc = ele.getElementsByTagName("canvas");
                 Array.from(pdfBox).map((pb, i) => pb.getContext('2d')!.drawImage(pdfBoxSrc[i], 0, 0));
-                var pdfView = dragElement.getElementsByClassName("pdfViewer-viewer");
-                var pdfViewSrc = ele.getElementsByClassName("pdfViewer-viewer");
-                let tops = Array.from(pdfViewSrc).map(p => p.scrollTop);
-                let oldopacity = dragElement.style.opacity;
+                const pdfView = dragElement.getElementsByClassName("pdfViewer-viewer");
+                const pdfViewSrc = ele.getElementsByClassName("pdfViewer-viewer");
+                const tops = Array.from(pdfViewSrc).map(p => p.scrollTop);
+                const oldopacity = dragElement.style.opacity;
                 dragElement.style.opacity = "0";
                 setTimeout(() => {
                     dragElement.style.opacity = oldopacity;
                     Array.from(pdfView).map((v, i) => v.scrollTo({ top: tops[i] }));
                 }, 0);
             }
-            let set = dragElement.getElementsByTagName('*');
+            const set = dragElement.getElementsByTagName('*');
             if (dragElement.hasAttribute("style")) (dragElement as any).style.pointerEvents = "none";
             // tslint:disable-next-line: prefer-for-of
             for (let i = 0; i < set.length; i++) {
                 if (set[i].hasAttribute("style")) {
-                    let s = set[i];
+                    const s = set[i];
                     (s as any).style.pointerEvents = "none";
                 }
             }
@@ -429,8 +428,8 @@ export namespace DragManager {
                 }, dragData.droppedDocuments);
             }
             //TODO: Why can't we use e.movementX and e.movementY?
-            let moveX = e.pageX - lastX;
-            let moveY = e.pageY - lastY;
+            const moveX = e.pageX - lastX;
+            const moveY = e.pageY - lastY;
             lastX = e.pageX;
             lastY = e.pageY;
             dragElements.map((dragElement, i) => (dragElement.style.transform =
@@ -438,11 +437,11 @@ export namespace DragManager {
             );
         };
 
-        let hideDragShowOriginalElements = () => {
+        const hideDragShowOriginalElements = () => {
             dragElements.map(dragElement => dragElement.parentNode === dragDiv && dragDiv.removeChild(dragElement));
             eles.map(ele => ele.hidden = false);
         };
-        let endDrag = () => {
+        const endDrag = () => {
             document.removeEventListener("pointermove", moveHandler, true);
             document.removeEventListener("pointerup", upHandler);
             if (options) {
@@ -466,17 +465,17 @@ export namespace DragManager {
     }
 
     function dispatchDrag(dragEles: HTMLElement[], e: PointerEvent, dragData: { [index: string]: any }, options?: DragOptions, finishDrag?: (dragData: { [index: string]: any }) => void) {
-        let removed = dragData.dontHideOnDrop ? [] : dragEles.map(dragEle => {
+        const removed = dragData.dontHideOnDrop ? [] : dragEles.map(dragEle => {
             // let parent = dragEle.parentElement;
             // if (parent) parent.removeChild(dragEle);
-            let ret = [dragEle, dragEle.style.width, dragEle.style.height];
+            const ret = [dragEle, dragEle.style.width, dragEle.style.height];
             dragEle.style.width = "0";
             dragEle.style.height = "0";
             return ret;
         });
         const target = document.elementFromPoint(e.x, e.y);
         removed.map(r => {
-            let dragEle = r[0] as HTMLElement;
+            const dragEle = r[0] as HTMLElement;
             dragEle.style.width = r[1] as string;
             dragEle.style.height = r[2] as string;
             // let parent = r[1];
diff --git a/src/client/util/DropConverter.ts b/src/client/util/DropConverter.ts
index 6b53333d7..b2c720d5d 100644
--- a/src/client/util/DropConverter.ts
+++ b/src/client/util/DropConverter.ts
@@ -9,10 +9,10 @@ import { ScriptField } from "../../new_fields/ScriptField";
 
 
 function makeTemplate(doc: Doc): boolean {
-    let layoutDoc = doc.layout instanceof Doc && doc.layout.isTemplateField ? doc.layout : doc;
-    let layout = StrCast(layoutDoc.layout).match(/fieldKey={"[^"]*"}/)![0];
-    let fieldKey = layout.replace('fieldKey={"', "").replace(/"}$/, "");
-    let docs = DocListCast(layoutDoc[fieldKey]);
+    const layoutDoc = doc.layout instanceof Doc && doc.layout.isTemplateField ? doc.layout : doc;
+    const layout = StrCast(layoutDoc.layout).match(/fieldKey={"[^"]*"}/)![0];
+    const fieldKey = layout.replace('fieldKey={"', "").replace(/"}$/, "");
+    const docs = DocListCast(layoutDoc[fieldKey]);
     let any = false;
     docs.map(d => {
         if (!StrCast(d.title).startsWith("-")) {
@@ -28,7 +28,7 @@ export function convertDropDataToButtons(data: DragManager.DocumentDragData) {
     data && data.draggedDocuments.map((doc, i) => {
         let dbox = doc;
         if (!doc.onDragStart && !doc.onClick && doc.viewType !== CollectionViewType.Linear) {
-            let layoutDoc = doc.layout instanceof Doc && doc.layout.isTemplateField ? doc.layout : doc;
+            const layoutDoc = doc.layout instanceof Doc && doc.layout.isTemplateField ? doc.layout : doc;
             if (layoutDoc.type === DocumentType.COL) {
                 layoutDoc.isTemplateDoc = makeTemplate(layoutDoc);
             } else {
diff --git a/src/client/util/History.ts b/src/client/util/History.ts
index 1c51236cb..545e8acb4 100644
--- a/src/client/util/History.ts
+++ b/src/client/util/History.ts
@@ -1,4 +1,4 @@
-import { Doc, Opt, Field } from "../../new_fields/Doc";
+import { Doc } from "../../new_fields/Doc";
 import { DocServer } from "../DocServer";
 import { MainView } from "../views/MainView";
 import * as qs from 'query-string';
@@ -53,7 +53,7 @@ export namespace HistoryUtil {
     }
 
     export function getState(): ParsedUrl {
-        let state = copyState(history.state);
+        const state = copyState(history.state);
         state.initializers = state.initializers || {};
         return state;
     }
@@ -160,7 +160,7 @@ export namespace HistoryUtil {
         const pathname = location.pathname.substring(1);
         const search = location.search;
         const opts = search.length ? qs.parse(search, { sort: false }) : {};
-        let pathnameSplit = pathname.split("/");
+        const pathnameSplit = pathname.split("/");
 
         const type = pathnameSplit[0];
 
diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx
index 104d9e099..e6a215b2c 100644
--- a/src/client/util/Import & Export/DirectoryImportBox.tsx	
+++ b/src/client/util/Import & Export/DirectoryImportBox.tsx	
@@ -1,7 +1,7 @@
 import "fs";
 import React = require("react");
 import { Doc, DocListCast, DocListCastAsync, Opt } from "../../../new_fields/Doc";
-import { action, observable, autorun, runInAction, computed, reaction, IReactionDisposer } from "mobx";
+import { action, observable, runInAction, computed, reaction, IReactionDisposer } from "mobx";
 import { FieldViewProps, FieldView } from "../../views/nodes/FieldView";
 import Measure, { ContentRect } from "react-measure";
 import { library } from '@fortawesome/fontawesome-svg-core';
@@ -48,7 +48,7 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
     constructor(props: FieldViewProps) {
         super(props);
         library.add(faTag, faPlus);
-        let doc = this.props.Document;
+        const doc = this.props.Document;
         this.editingMetadata = this.editingMetadata || false;
         this.persistent = this.persistent || false;
         !Cast(doc.data, listSpec(Doc)) && (doc.data = new List<Doc>());
@@ -78,16 +78,16 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
             this.phase = "Initializing download...";
         });
 
-        let docs: Doc[] = [];
+        const docs: Doc[] = [];
 
-        let files = e.target.files;
+        const files = e.target.files;
         if (!files || files.length === 0) return;
 
-        let directory = (files.item(0) as any).webkitRelativePath.split("/", 1)[0];
+        const directory = (files.item(0) as any).webkitRelativePath.split("/", 1)[0];
 
-        let validated: File[] = [];
+        const validated: File[] = [];
         for (let i = 0; i < files.length; i++) {
-            let file = files.item(i);
+            const file = files.item(i);
             if (file && !unsupported.includes(file.type)) {
                 const ext = path.extname(file.name).toLowerCase();
                 if (AcceptibleMedia.imageFormats.includes(ext)) {
@@ -101,8 +101,8 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
             this.completed = 0;
         });
 
-        let sizes: number[] = [];
-        let modifiedDates: number[] = [];
+        const sizes: number[] = [];
+        const modifiedDates: number[] = [];
 
         runInAction(() => this.phase = `Internal: uploading ${this.quota - this.completed} files to Dash...`);
 
@@ -136,26 +136,26 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
         }));
 
         for (let i = 0; i < docs.length; i++) {
-            let doc = docs[i];
+            const doc = docs[i];
             doc.size = sizes[i];
             doc.modified = modifiedDates[i];
             this.entries.forEach(entry => {
-                let target = entry.onDataDoc ? Doc.GetProto(doc) : doc;
+                const target = entry.onDataDoc ? Doc.GetProto(doc) : doc;
                 target[entry.key] = entry.value;
             });
         }
 
-        let doc = this.props.Document;
-        let height: number = NumCast(doc.height) || 0;
-        let offset: number = this.persistent ? (height === 0 ? 0 : height + 30) : 0;
-        let options: DocumentOptions = {
+        const doc = this.props.Document;
+        const height: number = NumCast(doc.height) || 0;
+        const offset: number = this.persistent ? (height === 0 ? 0 : height + 30) : 0;
+        const options: DocumentOptions = {
             title: `Import of ${directory}`,
             width: 1105,
             height: 500,
             x: NumCast(doc.x),
             y: NumCast(doc.y) + offset
         };
-        let parent = this.props.ContainingCollectionView;
+        const parent = this.props.ContainingCollectionView;
         if (parent) {
             let importContainer: Doc;
             if (docs.length < 50) {
@@ -194,18 +194,18 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
 
     @action
     preserveCentering = (rect: ContentRect) => {
-        let bounds = rect.offset!;
+        const bounds = rect.offset!;
         if (bounds.width === 0 || bounds.height === 0) {
             return;
         }
-        let offset = this.dimensions / 2;
+        const offset = this.dimensions / 2;
         this.left = bounds.width / 2 - offset;
         this.top = bounds.height / 2 - offset;
     }
 
     @action
     addMetadataEntry = async () => {
-        let entryDoc = new Doc();
+        const entryDoc = new Doc();
         entryDoc.checked = false;
         entryDoc.key = keyPlaceholder;
         entryDoc.value = valuePlaceholder;
@@ -214,7 +214,7 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
 
     @action
     remove = async (entry: ImportMetadataEntry) => {
-        let metadata = await DocListCastAsync(this.props.Document.data);
+        const metadata = await DocListCastAsync(this.props.Document.data);
         if (metadata) {
             let index = this.entries.indexOf(entry);
             if (index !== -1) {
@@ -228,18 +228,18 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
     }
 
     render() {
-        let dimensions = 50;
-        let entries = DocListCast(this.props.Document.data);
-        let isEditing = this.editingMetadata;
-        let completed = this.completed;
-        let quota = this.quota;
-        let uploading = this.uploading;
-        let showRemoveLabel = this.removeHover;
-        let persistent = this.persistent;
+        const dimensions = 50;
+        const entries = DocListCast(this.props.Document.data);
+        const isEditing = this.editingMetadata;
+        const completed = this.completed;
+        const quota = this.quota;
+        const uploading = this.uploading;
+        const showRemoveLabel = this.removeHover;
+        const persistent = this.persistent;
         let percent = `${completed / quota * 100}`;
         percent = percent.split(".")[0];
         percent = percent.startsWith("100") ? "99" : percent;
-        let marginOffset = (percent.length === 1 ? 5 : 0) - 1.6;
+        const marginOffset = (percent.length === 1 ? 5 : 0) - 1.6;
         const message = <span className={"phase"}>{this.phase}</span>;
         const centerPiece = this.phase.includes("Google Photos") ?
             <img src={"/assets/google_photos.png"} style={{
diff --git a/src/client/util/Import & Export/ImportMetadataEntry.tsx b/src/client/util/Import & Export/ImportMetadataEntry.tsx
index f5198c39b..8e1c50bea 100644
--- a/src/client/util/Import & Export/ImportMetadataEntry.tsx	
+++ b/src/client/util/Import & Export/ImportMetadataEntry.tsx	
@@ -1,11 +1,11 @@
 import React = require("react");
 import { observer } from "mobx-react";
 import { EditableView } from "../../views/EditableView";
-import { observable, action, computed } from "mobx";
+import { action, computed } from "mobx";
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 import { faPlus } from "@fortawesome/free-solid-svg-icons";
 import { library } from '@fortawesome/fontawesome-svg-core';
-import { Opt, Doc } from "../../../new_fields/Doc";
+import { Doc } from "../../../new_fields/Doc";
 import { StrCast, BoolCast } from "../../../new_fields/Types";
 
 interface KeyValueProps {
@@ -85,7 +85,7 @@ export default class ImportMetadataEntry extends React.Component<KeyValueProps>
     }
 
     render() {
-        let keyValueStyle: React.CSSProperties = {
+        const keyValueStyle: React.CSSProperties = {
             paddingLeft: 10,
             width: "50%",
             opacity: this.valid ? 1 : 0.5,
diff --git a/src/client/util/InteractionUtils.ts b/src/client/util/InteractionUtils.ts
index b7738e862..0c3de66ed 100644
--- a/src/client/util/InteractionUtils.ts
+++ b/src/client/util/InteractionUtils.ts
@@ -29,8 +29,8 @@ export namespace InteractionUtils {
      * @param pts - n-arbitrary long list of points
      */
     export function CenterPoint(pts: React.Touch[]): { X: number, Y: number } {
-        let centerX = pts.map(pt => pt.clientX).reduce((a, b) => a + b, 0) / pts.length;
-        let centerY = pts.map(pt => pt.clientY).reduce((a, b) => a + b, 0) / pts.length;
+        const centerX = pts.map(pt => pt.clientX).reduce((a, b) => a + b, 0) / pts.length;
+        const centerY = pts.map(pt => pt.clientY).reduce((a, b) => a + b, 0) / pts.length;
         return { X: centerX, Y: centerY };
     }
 
@@ -42,9 +42,9 @@ export namespace InteractionUtils {
      * @param oldPoint2 - previous point 2
      */
     export function Pinching(pt1: React.Touch, pt2: React.Touch, oldPoint1: React.Touch, oldPoint2: React.Touch): number {
-        let threshold = 4;
-        let oldDist = TwoPointEuclidist(oldPoint1, oldPoint2);
-        let newDist = TwoPointEuclidist(pt1, pt2);
+        const threshold = 4;
+        const oldDist = TwoPointEuclidist(oldPoint1, oldPoint2);
+        const newDist = TwoPointEuclidist(pt1, pt2);
 
         /** if they have the same sign, then we are either pinching in or out.
           * threshold it by 10 (it has to be pinching by at least threshold to be a valid pinch)
@@ -64,12 +64,12 @@ export namespace InteractionUtils {
      * @param oldPoint2 - previous point 2
      */
     export function Pinning(pt1: React.Touch, pt2: React.Touch, oldPoint1: React.Touch, oldPoint2: React.Touch): number {
-        let threshold = 4;
+        const threshold = 4;
 
-        let pt1Dist = TwoPointEuclidist(oldPoint1, pt1);
-        let pt2Dist = TwoPointEuclidist(oldPoint2, pt2);
+        const pt1Dist = TwoPointEuclidist(oldPoint1, pt1);
+        const pt2Dist = TwoPointEuclidist(oldPoint2, pt2);
 
-        let pinching = Pinching(pt1, pt2, oldPoint1, oldPoint2);
+        const pinching = Pinching(pt1, pt2, oldPoint1, oldPoint2);
 
         if (pinching !== 0) {
             if ((pt1Dist < threshold && pt2Dist > threshold) || (pt1Dist > threshold && pt2Dist < threshold)) {
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index eedc4967d..fb6f27478 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -38,16 +38,16 @@ export class LinkManager {
     }
 
     public getAllLinks(): Doc[] {
-        let ldoc = LinkManager.Instance.LinkManagerDoc;
+        const ldoc = LinkManager.Instance.LinkManagerDoc;
         if (ldoc) {
-            let docs = DocListCast(ldoc.allLinks);
+            const docs = DocListCast(ldoc.allLinks);
             return docs;
         }
         return [];
     }
 
     public addLink(linkDoc: Doc): boolean {
-        let linkList = LinkManager.Instance.getAllLinks();
+        const linkList = LinkManager.Instance.getAllLinks();
         linkList.push(linkDoc);
         if (LinkManager.Instance.LinkManagerDoc) {
             LinkManager.Instance.LinkManagerDoc.allLinks = new List<Doc>(linkList);
@@ -57,8 +57,8 @@ export class LinkManager {
     }
 
     public deleteLink(linkDoc: Doc): boolean {
-        let linkList = LinkManager.Instance.getAllLinks();
-        let index = LinkManager.Instance.getAllLinks().indexOf(linkDoc);
+        const linkList = LinkManager.Instance.getAllLinks();
+        const index = LinkManager.Instance.getAllLinks().indexOf(linkDoc);
         if (index > -1) {
             linkList.splice(index, 1);
             if (LinkManager.Instance.LinkManagerDoc) {
@@ -71,23 +71,23 @@ export class LinkManager {
 
     // finds all links that contain the given anchor
     public getAllRelatedLinks(anchor: Doc): Doc[] {//List<Doc> {
-        let related = LinkManager.Instance.getAllLinks().filter(link => {
-            let protomatch1 = Doc.AreProtosEqual(anchor, Cast(link.anchor1, Doc, null));
-            let protomatch2 = Doc.AreProtosEqual(anchor, Cast(link.anchor2, Doc, null));
+        const related = LinkManager.Instance.getAllLinks().filter(link => {
+            const protomatch1 = Doc.AreProtosEqual(anchor, Cast(link.anchor1, Doc, null));
+            const protomatch2 = Doc.AreProtosEqual(anchor, Cast(link.anchor2, Doc, null));
             return protomatch1 || protomatch2 || Doc.AreProtosEqual(link, anchor);
         });
         return related;
     }
 
     public deleteAllLinksOnAnchor(anchor: Doc) {
-        let related = LinkManager.Instance.getAllRelatedLinks(anchor);
+        const related = LinkManager.Instance.getAllRelatedLinks(anchor);
         related.forEach(linkDoc => LinkManager.Instance.deleteLink(linkDoc));
     }
 
     public addGroupType(groupType: string): boolean {
         if (LinkManager.Instance.LinkManagerDoc) {
             LinkManager.Instance.LinkManagerDoc[groupType] = new List<string>([]);
-            let groupTypes = LinkManager.Instance.getAllGroupTypes();
+            const groupTypes = LinkManager.Instance.getAllGroupTypes();
             groupTypes.push(groupType);
             LinkManager.Instance.LinkManagerDoc.allGroupTypes = new List<string>(groupTypes);
             return true;
@@ -99,8 +99,8 @@ export class LinkManager {
     public deleteGroupType(groupType: string): boolean {
         if (LinkManager.Instance.LinkManagerDoc) {
             if (LinkManager.Instance.LinkManagerDoc[groupType]) {
-                let groupTypes = LinkManager.Instance.getAllGroupTypes();
-                let index = groupTypes.findIndex(type => type.toUpperCase() === groupType.toUpperCase());
+                const groupTypes = LinkManager.Instance.getAllGroupTypes();
+                const index = groupTypes.findIndex(type => type.toUpperCase() === groupType.toUpperCase());
                 if (index > -1) groupTypes.splice(index, 1);
                 LinkManager.Instance.LinkManagerDoc.allGroupTypes = new List<string>(groupTypes);
                 LinkManager.Instance.LinkManagerDoc[groupType] = undefined;
@@ -146,8 +146,8 @@ export class LinkManager {
     }
 
     public addGroupToAnchor(linkDoc: Doc, anchor: Doc, groupDoc: Doc, replace: boolean = false) {
-        let groups = LinkManager.Instance.getAnchorGroups(linkDoc, anchor);
-        let index = groups.findIndex(gDoc => {
+        const groups = LinkManager.Instance.getAnchorGroups(linkDoc, anchor);
+        const index = groups.findIndex(gDoc => {
             return StrCast(groupDoc.type).toUpperCase() === StrCast(gDoc.type).toUpperCase();
         });
         if (index > -1 && replace) {
@@ -161,32 +161,32 @@ export class LinkManager {
 
     // removes group doc of given group type only from given anchor on given link
     public removeGroupFromAnchor(linkDoc: Doc, anchor: Doc, groupType: string) {
-        let groups = LinkManager.Instance.getAnchorGroups(linkDoc, anchor);
-        let newGroups = groups.filter(groupDoc => StrCast(groupDoc.type).toUpperCase() !== groupType.toUpperCase());
+        const groups = LinkManager.Instance.getAnchorGroups(linkDoc, anchor);
+        const newGroups = groups.filter(groupDoc => StrCast(groupDoc.type).toUpperCase() !== groupType.toUpperCase());
         LinkManager.Instance.setAnchorGroups(linkDoc, anchor, newGroups);
     }
 
     // returns map of group type to anchor's links in that group type
     public getRelatedGroupedLinks(anchor: Doc): Map<string, Array<Doc>> {
-        let related = this.getAllRelatedLinks(anchor);
-        let anchorGroups = new Map<string, Array<Doc>>();
+        const related = this.getAllRelatedLinks(anchor);
+        const anchorGroups = new Map<string, Array<Doc>>();
         related.forEach(link => {
-            let groups = LinkManager.Instance.getAnchorGroups(link, anchor);
+            const groups = LinkManager.Instance.getAnchorGroups(link, anchor);
 
             if (groups.length > 0) {
                 groups.forEach(groupDoc => {
-                    let groupType = StrCast(groupDoc.type);
+                    const groupType = StrCast(groupDoc.type);
                     if (groupType === "") {
-                        let group = anchorGroups.get("*");
+                        const group = anchorGroups.get("*");
                         anchorGroups.set("*", group ? [...group, link] : [link]);
                     } else {
-                        let group = anchorGroups.get(groupType);
+                        const group = anchorGroups.get(groupType);
                         anchorGroups.set(groupType, group ? [...group, link] : [link]);
                     }
                 });
             } else {
                 // if link is in no groups then put it in default group
-                let group = anchorGroups.get("*");
+                const group = anchorGroups.get("*");
                 anchorGroups.set("*", group ? [...group, link] : [link]);
             }
 
@@ -212,11 +212,11 @@ export class LinkManager {
 
     // returns a list of all metadata docs associated with the given group type
     public getAllMetadataDocsInGroup(groupType: string): Array<Doc> {
-        let md: Doc[] = [];
-        let allLinks = LinkManager.Instance.getAllLinks();
+        const md: Doc[] = [];
+        const allLinks = LinkManager.Instance.getAllLinks();
         allLinks.forEach(linkDoc => {
-            let anchor1Groups = LinkManager.Instance.getAnchorGroups(linkDoc, Cast(linkDoc.anchor1, Doc, null));
-            let anchor2Groups = LinkManager.Instance.getAnchorGroups(linkDoc, Cast(linkDoc.anchor2, Doc, null));
+            const anchor1Groups = LinkManager.Instance.getAnchorGroups(linkDoc, Cast(linkDoc.anchor1, Doc, null));
+            const anchor2Groups = LinkManager.Instance.getAnchorGroups(linkDoc, Cast(linkDoc.anchor2, Doc, null));
             anchor1Groups.forEach(groupDoc => { if (StrCast(groupDoc.type).toUpperCase() === groupType.toUpperCase()) { const meta = Cast(groupDoc.metadata, Doc, null); meta && md.push(meta); } });
             anchor2Groups.forEach(groupDoc => { if (StrCast(groupDoc.type).toUpperCase() === groupType.toUpperCase()) { const meta = Cast(groupDoc.metadata, Doc, null); meta && md.push(meta); } });
         });
@@ -225,8 +225,8 @@ export class LinkManager {
 
     // checks if a link with the given anchors exists
     public doesLinkExist(anchor1: Doc, anchor2: Doc): boolean {
-        let allLinks = LinkManager.Instance.getAllLinks();
-        let index = allLinks.findIndex(linkDoc => {
+        const allLinks = LinkManager.Instance.getAllLinks();
+        const index = allLinks.findIndex(linkDoc => {
             return (Doc.AreProtosEqual(Cast(linkDoc.anchor1, Doc, null), anchor1) && Doc.AreProtosEqual(Cast(linkDoc.anchor2, Doc, null), anchor2)) ||
                 (Doc.AreProtosEqual(Cast(linkDoc.anchor1, Doc, null), anchor2) && Doc.AreProtosEqual(Cast(linkDoc.anchor2, Doc, null), anchor1));
         });
@@ -237,8 +237,8 @@ export class LinkManager {
     //TODO This should probably return undefined if there isn't an opposite anchor
     //TODO This should also await the return value of the anchor so we don't filter out promises
     public getOppositeAnchor(linkDoc: Doc, anchor: Doc): Doc | undefined {
-        let a1 = Cast(linkDoc.anchor1, Doc, null);
-        let a2 = Cast(linkDoc.anchor2, Doc, null);
+        const a1 = Cast(linkDoc.anchor1, Doc, null);
+        const a2 = Cast(linkDoc.anchor2, Doc, null);
         if (Doc.AreProtosEqual(anchor, a1)) return a2;
         if (Doc.AreProtosEqual(anchor, a2)) return a1;
         if (Doc.AreProtosEqual(anchor, linkDoc)) return linkDoc;
diff --git a/src/client/util/ProsemirrorExampleTransfer.ts b/src/client/util/ProsemirrorExampleTransfer.ts
index 003ff6272..f1fa6f11d 100644
--- a/src/client/util/ProsemirrorExampleTransfer.ts
+++ b/src/client/util/ProsemirrorExampleTransfer.ts
@@ -4,7 +4,7 @@ import { undoInputRule } from "prosemirror-inputrules";
 import { Schema } from "prosemirror-model";
 import { liftListItem, sinkListItem } from "./prosemirrorPatches.js";
 import { splitListItem, wrapInList, } from "prosemirror-schema-list";
-import { EditorState, Transaction, TextSelection, NodeSelection } from "prosemirror-state";
+import { EditorState, Transaction, TextSelection } from "prosemirror-state";
 import { TooltipTextMenu } from "./TooltipTextMenu";
 
 const mac = typeof navigator !== "undefined" ? /Mac/.test(navigator.platform) : false;
@@ -15,22 +15,22 @@ export let updateBullets = (tx2: Transaction, schema: Schema, mapStyle?: string)
     let fontSize: number | undefined = undefined;
     tx2.doc.descendants((node: any, offset: any, index: any) => {
         if (node.type === schema.nodes.ordered_list || node.type === schema.nodes.list_item) {
-            let path = (tx2.doc.resolve(offset) as any).path;
+            const path = (tx2.doc.resolve(offset) as any).path;
             let depth = Array.from(path).reduce((p: number, c: any) => p + (c.hasOwnProperty("type") && c.type === schema.nodes.ordered_list ? 1 : 0), 0);
             if (node.type === schema.nodes.ordered_list) depth++;
             fontSize = depth === 1 && node.attrs.setFontSize ? Number(node.attrs.setFontSize) : fontSize;
-            let fsize = fontSize && node.type === schema.nodes.ordered_list ? Math.max(6, fontSize - (depth - 1) * 4) : undefined;
+            const fsize = fontSize && node.type === schema.nodes.ordered_list ? Math.max(6, fontSize - (depth - 1) * 4) : undefined;
             tx2.setNodeMarkup(offset, node.type, { ...node.attrs, mapStyle: mapStyle ? mapStyle : node.attrs.mapStyle, bulletStyle: depth, inheritedFontSize: fsize }, node.marks);
         }
     });
     return tx2;
 };
 export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?: KeyMap): KeyMap {
-    let keys: { [key: string]: any } = {}, type;
+    const keys: { [key: string]: any } = {};
 
     function bind(key: string, cmd: any) {
         if (mapKeys) {
-            let mapped = mapKeys[key];
+            const mapped = mapKeys[key];
             if (mapped === false) return;
             if (mapped) key = mapped;
         }
@@ -79,7 +79,7 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?:
     // });
 
 
-    let cmd = chainCommands(exitCode, (state, dispatch) => {
+    const cmd = chainCommands(exitCode, (state, dispatch) => {
         if (dispatch) {
             dispatch(state.tr.replaceSelectionWith(schema.nodes.hard_break.create()).scrollIntoView());
             return true;
@@ -99,7 +99,7 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?:
         bind("Shift-Ctrl-" + i, setBlockType(schema.nodes.heading, { level: i }));
     }
 
-    let hr = schema.nodes.horizontal_rule;
+    const hr = schema.nodes.horizontal_rule;
     bind("Mod-_", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
         dispatch(state.tr.replaceSelectionWith(hr.create()).scrollIntoView());
         return true;
@@ -108,18 +108,18 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?:
     bind("Mod-s", TooltipTextMenu.insertStar);
 
     bind("Tab", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
-        var ref = state.selection;
-        var range = ref.$from.blockRange(ref.$to);
-        var marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
+        const ref = state.selection;
+        const range = ref.$from.blockRange(ref.$to);
+        const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
         if (!sinkListItem(schema.nodes.list_item)(state, (tx2: Transaction) => {
-            let tx3 = updateBullets(tx2, schema);
+            const tx3 = updateBullets(tx2, schema);
             marks && tx3.ensureMarks([...marks]);
             marks && tx3.setStoredMarks([...marks]);
             dispatch(tx3);
         })) { // couldn't sink into an existing list, so wrap in a new one
-            let newstate = state.applyTransaction(state.tr.setSelection(TextSelection.create(state.doc, range!.start, range!.end)));
+            const newstate = state.applyTransaction(state.tr.setSelection(TextSelection.create(state.doc, range!.start, range!.end)));
             if (!wrapInList(schema.nodes.ordered_list)(newstate.state, (tx2: Transaction) => {
-                let tx3 = updateBullets(tx2, schema);
+                const tx3 = updateBullets(tx2, schema);
                 // when promoting to a list, assume list will format things so don't copy the stored marks.
                 marks && tx3.ensureMarks([...marks]);
                 marks && tx3.setStoredMarks([...marks]);
@@ -131,10 +131,10 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?:
     });
 
     bind("Shift-Tab", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
-        var marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
+        const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
 
         if (!liftListItem(schema.nodes.list_item)(state.tr, (tx2: Transaction) => {
-            let tx3 = updateBullets(tx2, schema);
+            const tx3 = updateBullets(tx2, schema);
             marks && tx3.ensureMarks([...marks]);
             marks && tx3.setStoredMarks([...marks]);
             dispatch(tx3);
@@ -143,14 +143,14 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?:
         }
     });
 
-    let splitMetadata = (marks: any, tx: Transaction) => {
+    const splitMetadata = (marks: any, tx: Transaction) => {
         marks && tx.ensureMarks(marks.filter((val: any) => val.type !== schema.marks.metadata && val.type !== schema.marks.metadataKey && val.type !== schema.marks.metadataVal));
         marks && tx.setStoredMarks(marks.filter((val: any) => val.type !== schema.marks.metadata && val.type !== schema.marks.metadataKey && val.type !== schema.marks.metadataVal));
         return tx;
     };
-    bind("Enter", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
-        var marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
-        if (!splitListItem(schema.nodes.list_item)(state, (tx3: Transaction) => dispatch(tx3))) {
+    bind("Enter", (state: EditorState<S>, dispatch: (tx: Transaction<Schema<any, any>>) => void) => {
+        const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
+        if (!splitListItem(schema.nodes.list_item)(state, dispatch)) {
             if (!splitBlockKeepMarks(state, (tx3: Transaction) => {
                 splitMetadata(marks, tx3);
                 if (!liftListItem(schema.nodes.list_item)(tx3, dispatch as ((tx: Transaction<Schema<any, any>>) => void))) {
@@ -163,18 +163,18 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?:
         return true;
     });
     bind("Space", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
-        var marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
+        const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
         dispatch(splitMetadata(marks, state.tr));
         return false;
     });
     bind(":", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
-        let range = state.selection.$from.blockRange(state.selection.$to, (node: any) => {
+        const range = state.selection.$from.blockRange(state.selection.$to, (node: any) => {
             return !node.marks || !node.marks.find((m: any) => m.type === schema.marks.metadata);
         });
-        let path = (state.doc.resolve(state.selection.from - 1) as any).path;
-        let spaceSeparator = path[path.length - 3].childCount > 1 ? 0 : -1;
-        let textsel = TextSelection.create(state.doc, range!.end - path[path.length - 3].lastChild.nodeSize + spaceSeparator, range!.end);
-        let text = range ? state.doc.textBetween(textsel.from, textsel.to) : "";
+        const path = (state.doc.resolve(state.selection.from - 1) as any).path;
+        const spaceSeparator = path[path.length - 3].childCount > 1 ? 0 : -1;
+        const textsel = TextSelection.create(state.doc, range!.end - path[path.length - 3].lastChild.nodeSize + spaceSeparator, range!.end);
+        const text = range ? state.doc.textBetween(textsel.from, textsel.to) : "";
         let whitespace = text.length - 1;
         for (; whitespace >= 0 && text[whitespace] !== " "; whitespace--) { }
         if (text.endsWith(":")) {
diff --git a/src/client/util/RichTextRules.ts b/src/client/util/RichTextRules.ts
index f4c44e5ce..1a637df32 100644
--- a/src/client/util/RichTextRules.ts
+++ b/src/client/util/RichTextRules.ts
@@ -62,109 +62,109 @@ export const inpRules = {
         new InputRule(
             new RegExp(/^#([0-9]+)\s$/),
             (state, match, start, end) => {
-                let size = Number(match[1]);
-                let ruleProvider = FormattedTextBox.InputBoxOverlay!.props.ruleProvider;
-                let heading = NumCast(FormattedTextBox.InputBoxOverlay!.props.Document.heading);
+                const size = Number(match[1]);
+                const ruleProvider = FormattedTextBox.InputBoxOverlay!.props.ruleProvider;
+                const heading = NumCast(FormattedTextBox.InputBoxOverlay!.props.Document.heading);
                 if (ruleProvider && heading) {
-                    (Cast(FormattedTextBox.InputBoxOverlay!.props.Document, Doc) as Doc).heading = Number(match[1]);
+                    (Cast(FormattedTextBox.InputBoxOverlay!.props.Document, Doc) as Doc).heading = size;
                     return state.tr.deleteRange(start, end);
                 }
-                return state.tr.deleteRange(start, end).addStoredMark(schema.marks.pFontSize.create({ fontSize: Number(match[1]) }));
+                return state.tr.deleteRange(start, end).addStoredMark(schema.marks.pFontSize.create({ fontSize: size }));
             }),
         new InputRule(
             new RegExp(/t/),
             (state, match, start, end) => {
                 if (state.selection.to === state.selection.from) return null;
-                let node = (state.doc.resolve(start) as any).nodeAfter;
+                const node = (state.doc.resolve(start) as any).nodeAfter;
                 return node ? state.tr.addMark(start, end, schema.marks.user_tag.create({ userid: Doc.CurrentUserEmail, tag: "todo", modified: Math.round(Date.now() / 1000 / 60) })) : state.tr;
             }),
         new InputRule(
             new RegExp(/i/),
             (state, match, start, end) => {
                 if (state.selection.to === state.selection.from) return null;
-                let node = (state.doc.resolve(start) as any).nodeAfter;
+                const node = (state.doc.resolve(start) as any).nodeAfter;
                 return node ? state.tr.addMark(start, end, schema.marks.user_tag.create({ userid: Doc.CurrentUserEmail, tag: "ignore", modified: Math.round(Date.now() / 1000 / 60) })) : state.tr;
             }),
         new InputRule(
             new RegExp(/\!/),
             (state, match, start, end) => {
                 if (state.selection.to === state.selection.from) return null;
-                let node = (state.doc.resolve(start) as any).nodeAfter;
+                const node = (state.doc.resolve(start) as any).nodeAfter;
                 return node ? state.tr.addMark(start, end, schema.marks.user_tag.create({ userid: Doc.CurrentUserEmail, tag: "important", modified: Math.round(Date.now() / 1000 / 60) })) : state.tr;
             }),
         new InputRule(
             new RegExp(/\x/),
             (state, match, start, end) => {
                 if (state.selection.to === state.selection.from) return null;
-                let node = (state.doc.resolve(start) as any).nodeAfter;
+                const node = (state.doc.resolve(start) as any).nodeAfter;
                 return node ? state.tr.addMark(start, end, schema.marks.user_tag.create({ userid: Doc.CurrentUserEmail, tag: "disagree", modified: Math.round(Date.now() / 1000 / 60) })) : state.tr;
             }),
         new InputRule(
             new RegExp(/^\^\^\s$/),
             (state, match, start, end) => {
-                let node = (state.doc.resolve(start) as any).nodeAfter;
-                let sm = state.storedMarks || undefined;
-                let ruleProvider = FormattedTextBox.InputBoxOverlay!.props.ruleProvider;
-                let heading = NumCast(FormattedTextBox.InputBoxOverlay!.props.Document.heading);
+                const node = (state.doc.resolve(start) as any).nodeAfter;
+                const sm = state.storedMarks || undefined;
+                const ruleProvider = FormattedTextBox.InputBoxOverlay!.props.ruleProvider;
+                const heading = NumCast(FormattedTextBox.InputBoxOverlay!.props.Document.heading);
                 if (ruleProvider && heading) {
                     ruleProvider["ruleAlign_" + heading] = "center";
                     return node ? state.tr.deleteRange(start, end).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : state.tr;
                 }
-                let replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "center" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
+                const replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "center" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
                     state.tr;
                 return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 2)));
             }),
         new InputRule(
             new RegExp(/^\[\[\s$/),
             (state, match, start, end) => {
-                let node = (state.doc.resolve(start) as any).nodeAfter;
-                let sm = state.storedMarks || undefined;
-                let ruleProvider = FormattedTextBox.InputBoxOverlay!.props.ruleProvider;
-                let heading = NumCast(FormattedTextBox.InputBoxOverlay!.props.Document.heading);
+                const node = (state.doc.resolve(start) as any).nodeAfter;
+                const sm = state.storedMarks || undefined;
+                const ruleProvider = FormattedTextBox.InputBoxOverlay!.props.ruleProvider;
+                const heading = NumCast(FormattedTextBox.InputBoxOverlay!.props.Document.heading);
                 if (ruleProvider && heading) {
                     ruleProvider["ruleAlign_" + heading] = "left";
                     return node ? state.tr.deleteRange(start, end).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : state.tr;
                 }
-                let replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "left" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
+                const replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "left" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
                     state.tr;
                 return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 2)));
             }),
         new InputRule(
             new RegExp(/^\]\]\s$/),
             (state, match, start, end) => {
-                let node = (state.doc.resolve(start) as any).nodeAfter;
-                let sm = state.storedMarks || undefined;
-                let ruleProvider = FormattedTextBox.InputBoxOverlay!.props.ruleProvider;
-                let heading = NumCast(FormattedTextBox.InputBoxOverlay!.props.Document.heading);
+                const node = (state.doc.resolve(start) as any).nodeAfter;
+                const sm = state.storedMarks || undefined;
+                const ruleProvider = FormattedTextBox.InputBoxOverlay!.props.ruleProvider;
+                const heading = NumCast(FormattedTextBox.InputBoxOverlay!.props.Document.heading);
                 if (ruleProvider && heading) {
                     ruleProvider["ruleAlign_" + heading] = "right";
                     return node ? state.tr.deleteRange(start, end).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : state.tr;
                 }
-                let replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "right" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
+                const replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "right" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
                     state.tr;
                 return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 2)));
             }),
         new InputRule(
             new RegExp(/##\s$/),
             (state, match, start, end) => {
-                let target = Docs.Create.TextDocument({ width: 75, height: 35, autoHeight: true, fontSize: 9, title: "inline comment" });
-                let node = (state.doc.resolve(start) as any).nodeAfter;
-                let newNode = schema.nodes.dashComment.create({ docid: target[Id] });
-                let dashDoc = schema.nodes.dashDoc.create({ width: 75, height: 35, title: "dashDoc", docid: target[Id], float: "right" });
-                let sm = state.storedMarks || undefined;
-                let replaced = node ? state.tr.insert(start, newNode).replaceRangeWith(start + 1, end + 1, dashDoc).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
+                const target = Docs.Create.TextDocument({ width: 75, height: 35, autoHeight: true, fontSize: 9, title: "inline comment" });
+                const node = (state.doc.resolve(start) as any).nodeAfter;
+                const newNode = schema.nodes.dashComment.create({ docid: target[Id] });
+                const dashDoc = schema.nodes.dashDoc.create({ width: 75, height: 35, title: "dashDoc", docid: target[Id], float: "right" });
+                const sm = state.storedMarks || undefined;
+                const replaced = node ? state.tr.insert(start, newNode).replaceRangeWith(start + 1, end + 1, dashDoc).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
                     state.tr;
                 return replaced;//.setSelection(new NodeSelection(replaced.doc.resolve(end)));
             }),
         new InputRule(
             new RegExp(/\(\(/),
             (state, match, start, end) => {
-                let node = (state.doc.resolve(start) as any).nodeAfter;
-                let sm = state.storedMarks || undefined;
-                let mark = state.schema.marks.highlight.create();
-                let selected = state.tr.setSelection(new TextSelection(state.doc.resolve(start), state.doc.resolve(end))).addMark(start, end, mark);
-                let content = selected.selection.content();
-                let replaced = node ? selected.replaceRangeWith(start, start,
+                const node = (state.doc.resolve(start) as any).nodeAfter;
+                const sm = state.storedMarks || undefined;
+                const mark = state.schema.marks.highlight.create();
+                const selected = state.tr.setSelection(new TextSelection(state.doc.resolve(start), state.doc.resolve(end))).addMark(start, end, mark);
+                const content = selected.selection.content();
+                const replaced = node ? selected.replaceRangeWith(start, start,
                     schema.nodes.star.create({ visibility: true, text: content, textslice: content.toJSON() })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
                     state.tr;
                 return replaced.setSelection(new TextSelection(replaced.doc.resolve(end + 1)));
@@ -172,14 +172,14 @@ export const inpRules = {
         new InputRule(
             new RegExp(/\)\)/),
             (state, match, start, end) => {
-                let mark = state.schema.marks.highlight.create();
+                const mark = state.schema.marks.highlight.create();
                 return state.tr.removeStoredMark(mark);
             }),
         new InputRule(
             new RegExp(/\^f\s$/),
             (state, match, start, end) => {
-                let newNode = schema.nodes.footnote.create({});
-                let tr = state.tr;
+                const newNode = schema.nodes.footnote.create({});
+                const tr = state.tr;
                 tr.deleteRange(start, end).replaceSelectionWith(newNode); // replace insertion with a footnote.
                 return tr.setSelection(new NodeSelection( // select the footnote node to open its display
                     tr.doc.resolve(  // get the location of the footnote node by subtracting the nodesize of the footnote from the current insertion point anchor (which will be immediately after the footnote node)
diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index 522232e9f..cb03892f3 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -1,4 +1,4 @@
-import { action, observable, runInAction, reaction, IReactionDisposer } from "mobx";
+import { reaction, IReactionDisposer } from "mobx";
 import { baseKeymap, toggleMark } from "prosemirror-commands";
 import { redo, undo } from "prosemirror-history";
 import { keymap } from "prosemirror-keymap";
@@ -257,9 +257,9 @@ export const nodes: { [index: string]: NodeSpec } = {
             if (node.attrs.mapStyle === "bullet") return ['ul', 0];
             const decMap = bs ? "decimal" + bs : "";
             const multiMap = bs === 1 ? "decimal1" : bs === 2 ? "upper-alpha" : bs === 3 ? "lower-roman" : bs === 4 ? "lower-alpha" : "";
-            let map = node.attrs.mapStyle === "decimal" ? decMap : multiMap;
-            let fsize = node.attrs.setFontSize ? node.attrs.setFontSize : node.attrs.inheritedFontSize;
-            let ffam = node.attrs.setFontFamily;
+            const map = node.attrs.mapStyle === "decimal" ? decMap : multiMap;
+            const fsize = node.attrs.setFontSize ? node.attrs.setFontSize : node.attrs.inheritedFontSize;
+            const ffam = node.attrs.setFontFamily;
             return node.attrs.visibility ? ['ol', { class: `${map}-ol`, style: `list-style: none; font-size: ${fsize}; font-family: ${ffam}` }, 0] :
                 ['ol', { class: `${map}-ol`, style: `list-style: none; font-size: ${fsize}; font-family: ${ffam}` }];
         }
@@ -287,7 +287,7 @@ export const nodes: { [index: string]: NodeSpec } = {
             const bs = node.attrs.bulletStyle;
             const decMap = bs ? "decimal" + bs : "";
             const multiMap = bs === 1 ? "decimal1" : bs === 2 ? "upper-alpha" : bs === 3 ? "lower-roman" : bs === 4 ? "lower-alpha" : "";
-            let map = node.attrs.mapStyle === "decimal" ? decMap : node.attrs.mapStyle === "multi" ? multiMap : "";
+            const map = node.attrs.mapStyle === "decimal" ? decMap : node.attrs.mapStyle === "multi" ? multiMap : "";
             return node.attrs.visibility ? ["li", { class: `${map}` }, 0] : ["li", { class: `${map}` }, "..."];
             //return ["li", { class: `${map}` }, 0];
         }
@@ -432,7 +432,7 @@ export const marks: { [index: string]: MarkSpec } = {
                 tag: "span",
                 getAttrs: (p: any) => {
                     if (typeof (p) !== "string") {
-                        let style = getComputedStyle(p);
+                        const style = getComputedStyle(p);
                         if (style.textDecoration === "underline") return null;
                         if (p.parentElement.outerHTML.indexOf("text-decoration: underline") !== -1 &&
                             p.parentElement.outerHTML.indexOf("text-decoration-style: dotted") !== -1) {
@@ -457,7 +457,7 @@ export const marks: { [index: string]: MarkSpec } = {
                 tag: "span",
                 getAttrs: (p: any) => {
                     if (typeof (p) !== "string") {
-                        let style = getComputedStyle(p);
+                        const style = getComputedStyle(p);
                         if (style.textDecoration === "underline" || p.parentElement.outerHTML.indexOf("text-decoration-style:line") !== -1) {
                             return null;
                         }
@@ -493,11 +493,11 @@ export const marks: { [index: string]: MarkSpec } = {
         },
         group: "inline",
         toDOM(node: any) {
-            let uid = node.attrs.userid.replace(".", "").replace("@", "");
-            let min = Math.round(node.attrs.modified / 12);
-            let hr = Math.round(min / 60);
-            let day = Math.round(hr / 60 / 24);
-            let remote = node.attrs.userid !== Doc.CurrentUserEmail ? " userMark-remote" : "";
+            const uid = node.attrs.userid.replace(".", "").replace("@", "");
+            const min = Math.round(node.attrs.modified / 12);
+            const hr = Math.round(min / 60);
+            const day = Math.round(hr / 60 / 24);
+            const remote = node.attrs.userid !== Doc.CurrentUserEmail ? " userMark-remote" : "";
             return node.attrs.opened ?
                 ['span', { class: "userMark-" + uid + remote + " userMark-min-" + min + " userMark-hr-" + hr + " userMark-day-" + day }, 0] :
                 ['span', { class: "userMark-" + uid + remote + " userMark-min-" + min + " userMark-hr-" + hr + " userMark-day-" + day }, ['span', 0]];
@@ -513,7 +513,7 @@ export const marks: { [index: string]: MarkSpec } = {
         },
         group: "inline",
         toDOM(node: any) {
-            let uid = node.attrs.userid.replace(".", "").replace("@", "");
+            const uid = node.attrs.userid.replace(".", "").replace("@", "");
             return node.attrs.opened ?
                 ['span', { class: "userTag-" + uid + " userTag-" + node.attrs.tag }, 0] :
                 ['span', { class: "userTag-" + uid + " userTag-" + node.attrs.tag }, ['span', 0]];
@@ -534,7 +534,7 @@ export const marks: { [index: string]: MarkSpec } = {
         },
         parseDOM: [{
             tag: "span", getAttrs(dom: any) {
-                let cstyle = getComputedStyle(dom);
+                const cstyle = getComputedStyle(dom);
                 if (cstyle.font) {
                     if (cstyle.font.indexOf("Times New Roman") !== -1) return { family: "Times New Roman" };
                     if (cstyle.font.indexOf("Arial") !== -1) return { family: "Arial" };
@@ -599,7 +599,7 @@ export class ImageResizeView {
         this._handle.style.display = "none";
         this._handle.style.bottom = "-10px";
         this._handle.style.right = "-10px";
-        let self = this;
+        const self = this;
         this._img.onclick = function (e: any) {
             e.stopPropagation();
             e.preventDefault();
@@ -620,8 +620,8 @@ export class ImageResizeView {
         this._handle.onpointerdown = function (e: any) {
             e.preventDefault();
             e.stopPropagation();
-            let wid = Number(getComputedStyle(self._img).width.replace(/px/, ""));
-            let hgt = Number(getComputedStyle(self._img).height.replace(/px/, ""));
+            const wid = Number(getComputedStyle(self._img).width.replace(/px/, ""));
+            const hgt = Number(getComputedStyle(self._img).height.replace(/px/, ""));
             const startX = e.pageX;
             const startWidth = parseFloat(node.attrs.width);
             const onpointermove = (e: any) => {
@@ -634,7 +634,7 @@ export class ImageResizeView {
             const onpointerup = () => {
                 document.removeEventListener("pointermove", onpointermove);
                 document.removeEventListener("pointerup", onpointerup);
-                let pos = view.state.selection.from;
+                const pos = view.state.selection.from;
                 view.dispatch(view.state.tr.setNodeMarkup(getPos(), null, { ...node.attrs, width: self._outer.style.width, height: self._outer.style.height }));
                 view.dispatch(view.state.tr.setSelection(new NodeSelection(view.state.doc.resolve(pos))));
             };
@@ -671,19 +671,19 @@ export class DashDocCommentView {
         this._collapsed.id = "DashDocCommentView-" + node.attrs.docid;
         this._view = view;
         this._collapsed.onpointerdown = (e: any) => {
-            let node = view.state.doc.nodeAt(getPos() + 1);
+            const node = view.state.doc.nodeAt(getPos() + 1);
             view.dispatch(view.state.tr.
                 setNodeMarkup(getPos() + 1, undefined, { ...node.attrs, hidden: node.attrs.hidden ? false : true })); // update the attrs 
             setTimeout(() => node.attrs.hidden && DocServer.GetRefField(node.attrs.docid).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc)), 100);
-        }
+        };
         this._collapsed.onpointerenter = (e: any) => {
-            let node = view.state.doc.nodeAt(getPos() + 1);
+            const node = view.state.doc.nodeAt(getPos() + 1);
             DocServer.GetRefField(node.attrs.docid).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc));
             e.preventDefault();
             e.stopPropagation();
         };
         this._collapsed.onpointerleave = (e: any) => {
-            let node = view.state.doc.nodeAt(getPos() + 1);
+            const node = view.state.doc.nodeAt(getPos() + 1);
             DocServer.GetRefField(node.attrs.docid).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowUnhighlight());
             e.preventDefault();
             e.stopPropagation();
@@ -701,7 +701,7 @@ export class DashDocView {
     _textBox: FormattedTextBox;
 
     getDocTransform = () => {
-        let { scale, translateX, translateY } = Utils.GetScreenTransform(this._outer);
+        const { scale, translateX, translateY } = Utils.GetScreenTransform(this._outer);
         return new Transform(-translateX, -translateY, 1).scale(1 / this.contentScaling() / scale);
     }
     contentScaling = () => NumCast(this._dashDoc!.nativeWidth) > 0 && !this._dashDoc!.ignoreAspect ? this._dashDoc![WidthSym]() / NumCast(this._dashDoc!.nativeWidth) : 1;
@@ -721,24 +721,24 @@ export class DashDocView {
         this._dashSpan.style.position = "absolute";
         this._dashSpan.style.display = "inline-block";
         this._dashSpan.style.borderWidth = "4";
-        let removeDoc = () => {
-            let pos = getPos();
-            let ns = new NodeSelection(view.state.doc.resolve(pos));
+        const removeDoc = () => {
+            const pos = getPos();
+            const ns = new NodeSelection(view.state.doc.resolve(pos));
             view.dispatch(view.state.tr.setSelection(ns).deleteSelection());
             return true;
         };
         this._dashSpan.onpointerleave = () => {
-            let ele = document.getElementById("DashDocCommentView-" + node.attrs.docid);
+            const ele = document.getElementById("DashDocCommentView-" + node.attrs.docid);
             if (ele) {
                 (ele as HTMLDivElement).style.backgroundColor = "";
             }
-        }
+        };
         this._dashSpan.onpointerenter = () => {
-            let ele = document.getElementById("DashDocCommentView-" + node.attrs.docid);
+            const ele = document.getElementById("DashDocCommentView-" + node.attrs.docid);
             if (ele) {
                 (ele as HTMLDivElement).style.backgroundColor = "orange";
             }
-        }
+        };
         DocServer.GetRefField(node.attrs.docid).then(async dashDoc => {
             if (dashDoc instanceof Doc) {
                 self._dashDoc = dashDoc;
@@ -777,7 +777,7 @@ export class DashDocView {
                 />, this._dashSpan);
             }
         });
-        let self = this;
+        const self = this;
         this._dashSpan.onkeydown = function (e: any) { e.stopPropagation(); };
         this._dashSpan.onkeypress = function (e: any) { e.stopPropagation(); };
         this._dashSpan.onwheel = function (e: any) { e.preventDefault(); };
@@ -830,7 +830,7 @@ export class FootnoteView {
     }
     open() {
         // Append a tooltip to the outer node
-        let tooltip = this.dom.appendChild(document.createElement("div"));
+        const tooltip = this.dom.appendChild(document.createElement("div"));
         tooltip.className = "footnote-tooltip";
         // And put a sub-ProseMirror into that
         this.innerView = new EditorView(tooltip, {
@@ -885,14 +885,14 @@ export class FootnoteView {
         this.dom.textContent = "";
     }
     dispatchInner(tr: any) {
-        let { state, transactions } = this.innerView.state.applyTransaction(tr);
+        const { state, transactions } = this.innerView.state.applyTransaction(tr);
         this.innerView.updateState(state);
 
         if (!tr.getMeta("fromOutside")) {
-            let outerTr = this.outerView.state.tr, offsetMap = StepMap.offset(this.getPos() + 1);
-            for (let transaction of transactions) {
-                let steps = transaction.steps;
-                for (let step of steps) {
+            const outerTr = this.outerView.state.tr, offsetMap = StepMap.offset(this.getPos() + 1);
+            for (const transaction of transactions) {
+                const steps = transaction.steps;
+                for (const step of steps) {
                     outerTr.step(step.map(offsetMap));
                 }
             }
@@ -903,11 +903,11 @@ export class FootnoteView {
         if (!node.sameMarkup(this.node)) return false;
         this.node = node;
         if (this.innerView) {
-            let state = this.innerView.state;
-            let start = node.content.findDiffStart(state.doc.content);
+            const state = this.innerView.state;
+            const start = node.content.findDiffStart(state.doc.content);
             if (start !== null) {
                 let { a: endA, b: endB } = node.content.findDiffEnd(state.doc.content);
-                let overlap = start - Math.min(endA, endB);
+                const overlap = start - Math.min(endA, endB);
                 if (overlap > 0) { endA += overlap; endB += overlap; }
                 this.innerView.dispatch(
                     state.tr
@@ -967,10 +967,10 @@ export class SummarizedView {
     className = (visible: boolean) => "formattedTextBox-summarizer" + (visible ? "" : "-collapsed");
 
     updateSummarizedText(start?: any) {
-        let mark = this._view.state.schema.marks.highlight.create();
+        const mark = this._view.state.schema.marks.highlight.create();
         let endPos = start;
 
-        let visited = new Set();
+        const visited = new Set();
         for (let i: number = start + 1; i < this._view.state.doc.nodeSize - 1; i++) {
             let skip = false;
             this._view.state.doc.nodesBetween(start, i, (node: Node, pos: number, parent: Node, index: number) => {
@@ -999,7 +999,7 @@ export const schema = new Schema({ nodes, marks });
 const fromJson = schema.nodeFromJSON;
 
 schema.nodeFromJSON = (json: any) => {
-    let node = fromJson(json);
+    const node = fromJson(json);
     if (json.type === "star") {
         node.attrs.text = Slice.fromJSON(schema, node.attrs.textslice);
     }
diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts
index ff4451824..0fa96963e 100644
--- a/src/client/util/Scripting.ts
+++ b/src/client/util/Scripting.ts
@@ -94,16 +94,16 @@ function Run(script: string | undefined, customParams: string[], diagnostics: an
         return { compiled: false, errors: diagnostics };
     }
 
-    let paramNames = Object.keys(scriptingGlobals);
-    let params = paramNames.map(key => scriptingGlobals[key]);
+    const paramNames = Object.keys(scriptingGlobals);
+    const params = paramNames.map(key => scriptingGlobals[key]);
     // let fieldTypes = [Doc, ImageField, PdfField, VideoField, AudioField, List, RichTextField, ScriptField, ComputedField, CompileScript];
     // let paramNames = ["Docs", ...fieldTypes.map(fn => fn.name)];
     // let params: any[] = [Docs, ...fieldTypes];
-    let compiledFunction = new Function(...paramNames, `return ${script}`);
-    let { capturedVariables = {} } = options;
-    let run = (args: { [name: string]: any } = {}, onError?: (e: any) => void, errorVal?: any): ScriptResult => {
-        let argsArray: any[] = [];
-        for (let name of customParams) {
+    const compiledFunction = new Function(...paramNames, `return ${script}`);
+    const { capturedVariables = {} } = options;
+    const run = (args: { [name: string]: any } = {}, onError?: (e: any) => void, errorVal?: any): ScriptResult => {
+        const argsArray: any[] = [];
+        for (const name of customParams) {
             if (name === "this") {
                 continue;
             }
@@ -113,7 +113,7 @@ function Run(script: string | undefined, customParams: string[], diagnostics: an
                 argsArray.push(capturedVariables[name]);
             }
         }
-        let thisParam = args.this || capturedVariables.this;
+        const thisParam = args.this || capturedVariables.this;
         let batch: { end(): void } | undefined = undefined;
         try {
             if (!options.editable) {
@@ -146,7 +146,7 @@ class ScriptingCompilerHost {
 
     // getSourceFile(fileName: string, languageVersion: ts.ScriptTarget, onError?: ((message: string) => void) | undefined, shouldCreateNewSourceFile?: boolean | undefined): ts.SourceFile | undefined {
     getSourceFile(fileName: string, languageVersion: any, onError?: ((message: string) => void) | undefined, shouldCreateNewSourceFile?: boolean | undefined): any | undefined {
-        let contents = this.readFile(fileName);
+        const contents = this.readFile(fileName);
         if (contents !== undefined) {
             return ts.createSourceFile(fileName, contents, languageVersion, true);
         }
@@ -180,7 +180,7 @@ class ScriptingCompilerHost {
         return this.files.some(file => file.fileName === fileName);
     }
     readFile(fileName: string): string | undefined {
-        let file = this.files.find(file => file.fileName === fileName);
+        const file = this.files.find(file => file.fileName === fileName);
         if (file) {
             return file.content;
         }
@@ -218,7 +218,7 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp
     if (options.globals) {
         Scripting.setScriptingGlobals(options.globals);
     }
-    let host = new ScriptingCompilerHost;
+    const host = new ScriptingCompilerHost;
     if (options.traverser) {
         const sourceFile = ts.createSourceFile('script.ts', script, ts.ScriptTarget.ES2015, true);
         const onEnter = typeof options.traverser === "object" ? options.traverser.onEnter : options.traverser;
@@ -240,7 +240,7 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp
         script = printer.printFile(transformed[0]);
         result.dispose();
     }
-    let paramNames: string[] = [];
+    const paramNames: string[] = [];
     if ("this" in params || "this" in capturedVariables) {
         paramNames.push("this");
     }
@@ -248,7 +248,7 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp
         if (key === "this") continue;
         paramNames.push(key);
     }
-    let paramList = paramNames.map(key => {
+    const paramList = paramNames.map(key => {
         const val = params[key];
         return `${key}: ${val}`;
     });
@@ -258,18 +258,18 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp
         paramNames.push(key);
         paramList.push(`${key}: ${typeof val === "object" ? Object.getPrototypeOf(val).constructor.name : typeof val}`);
     }
-    let paramString = paramList.join(", ");
-    let funcScript = `(function(${paramString})${requiredType ? `: ${requiredType}` : ''} {
+    const paramString = paramList.join(", ");
+    const funcScript = `(function(${paramString})${requiredType ? `: ${requiredType}` : ''} {
         ${addReturn ? `return ${script};` : script}
     })`;
     host.writeFile("file.ts", funcScript);
 
     if (typecheck) host.writeFile('node_modules/typescript/lib/lib.d.ts', typescriptlib);
-    let program = ts.createProgram(["file.ts"], {}, host);
-    let testResult = program.emit();
-    let outputText = host.readFile("file.js");
+    const program = ts.createProgram(["file.ts"], {}, host);
+    const testResult = program.emit();
+    const outputText = host.readFile("file.js");
 
-    let diagnostics = ts.getPreEmitDiagnostics(program).concat(testResult.diagnostics);
+    const diagnostics = ts.getPreEmitDiagnostics(program).concat(testResult.diagnostics);
 
     const result = Run(outputText, paramNames, diagnostics, script, options);
 
diff --git a/src/client/util/SearchUtil.ts b/src/client/util/SearchUtil.ts
index 2cf13680a..7a9176bec 100644
--- a/src/client/util/SearchUtil.ts
+++ b/src/client/util/SearchUtil.ts
@@ -34,36 +34,36 @@ export namespace SearchUtil {
     export function Search(query: string, returnDocs: false, options?: SearchParams): Promise<IdSearchResult>;
     export async function Search(query: string, returnDocs: boolean, options: SearchParams = {}) {
         query = query || "*"; //If we just have a filter query, search for * as the query
-        let result: IdSearchResult = JSON.parse(await rp.get(Utils.prepend("/search"), {
+        const result: IdSearchResult = JSON.parse(await rp.get(Utils.prepend("/search"), {
             qs: { ...options, q: query },
         }));
         if (!returnDocs) {
             return result;
         }
 
-        let { ids, numFound, highlighting } = result;
+        const { ids, highlighting } = result;
 
-        let txtresult = query !== "*" && JSON.parse(await rp.get(Utils.prepend("/textsearch"), {
+        const txtresult = query !== "*" && JSON.parse(await rp.get(Utils.prepend("/textsearch"), {
             qs: { ...options, q: query },
         }));
 
-        let fileids = txtresult ? txtresult.ids : [];
-        let newIds: string[] = [];
-        let newLines: string[][] = [];
+        const fileids = txtresult ? txtresult.ids : [];
+        const newIds: string[] = [];
+        const newLines: string[][] = [];
         await Promise.all(fileids.map(async (tr: string, i: number) => {
-            let docQuery = "fileUpload_t:" + tr.substr(0, 7); //If we just have a filter query, search for * as the query
-            let docResult = JSON.parse(await rp.get(Utils.prepend("/search"), { qs: { ...options, q: docQuery } }));
+            const docQuery = "fileUpload_t:" + tr.substr(0, 7); //If we just have a filter query, search for * as the query
+            const docResult = JSON.parse(await rp.get(Utils.prepend("/search"), { qs: { ...options, q: docQuery } }));
             newIds.push(...docResult.ids);
             newLines.push(...docResult.ids.map((dr: any) => txtresult.lines[i]));
         }));
 
 
-        let theDocs: Doc[] = [];
-        let theLines: string[][] = [];
+        const theDocs: Doc[] = [];
+        const theLines: string[][] = [];
         const textDocMap = await DocServer.GetRefFields(newIds);
         const textDocs = newIds.map((id: string) => textDocMap[id]).map(doc => doc as Doc);
         for (let i = 0; i < textDocs.length; i++) {
-            let testDoc = textDocs[i];
+            const testDoc = textDocs[i];
             if (testDoc instanceof Doc && testDoc.type !== DocumentType.KVP && theDocs.findIndex(d => Doc.AreProtosEqual(d, testDoc)) === -1) {
                 theDocs.push(Doc.GetProto(testDoc));
                 theLines.push(newLines[i].map(line => line.replace(query, query.toUpperCase())));
@@ -73,7 +73,7 @@ export namespace SearchUtil {
         const docMap = await DocServer.GetRefFields(ids);
         const docs = ids.map((id: string) => docMap[id]).map(doc => doc as Doc);
         for (let i = 0; i < ids.length; i++) {
-            let testDoc = docs[i];
+            const testDoc = docs[i];
             if (testDoc instanceof Doc && testDoc.type !== DocumentType.KVP && (options.allowAliases || theDocs.findIndex(d => Doc.AreProtosEqual(d, testDoc)) === -1)) {
                 theDocs.push(testDoc);
                 theLines.push([]);
diff --git a/src/client/util/SerializationHelper.ts b/src/client/util/SerializationHelper.ts
index ff048f647..1f6b939d3 100644
--- a/src/client/util/SerializationHelper.ts
+++ b/src/client/util/SerializationHelper.ts
@@ -1,7 +1,6 @@
-import { PropSchema, serialize, deserialize, custom, setDefaultModelSchema, getDefaultModelSchema, primitive, SKIP } from "serializr";
-import { Field, Doc } from "../../new_fields/Doc";
+import { PropSchema, serialize, deserialize, custom, setDefaultModelSchema, getDefaultModelSchema } from "serializr";
+import { Field } from "../../new_fields/Doc";
 import { ClientUtils } from "./ClientUtils";
-import { emptyFunction } from "../../Utils";
 
 let serializing = 0;
 export function afterDocDeserialize(cb: (err: any, val: any) => void, err: any, newValue: any) {
@@ -65,8 +64,8 @@ export namespace SerializationHelper {
     }
 }
 
-let serializationTypes: { [name: string]: { ctor: { new(): any }, afterDeserialize?: (obj: any) => void | Promise<any> } } = {};
-let reverseMap: { [ctor: string]: string } = {};
+const serializationTypes: { [name: string]: { ctor: { new(): any }, afterDeserialize?: (obj: any) => void | Promise<any> } } = {};
+const reverseMap: { [ctor: string]: string } = {};
 
 export interface DeserializableOpts {
     (constructor: { new(...args: any[]): any }): void;
diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx
index cc1d628b1..7496ac73c 100644
--- a/src/client/util/SharingManager.tsx
+++ b/src/client/util/SharingManager.tsx
@@ -9,7 +9,6 @@ import { Utils } from "../../Utils";
 import "./SharingManager.scss";
 import { Id } from "../../new_fields/FieldSymbols";
 import { observer } from "mobx-react";
-import { MainView } from "../views/MainView";
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 import { library } from '@fortawesome/fontawesome-svg-core';
 import * as fa from '@fortawesome/free-solid-svg-icons';
@@ -103,10 +102,10 @@ export default class SharingManager extends React.Component<{}> {
     }
 
     populateUsers = async () => {
-        let userList = await RequestPromise.get(Utils.prepend("/getUsers"));
+        const userList = await RequestPromise.get(Utils.prepend("/getUsers"));
         const raw = JSON.parse(userList) as User[];
         const evaluating = raw.map(async user => {
-            let isCandidate = user.email !== Doc.CurrentUserEmail;
+            const isCandidate = user.email !== Doc.CurrentUserEmail;
             if (isCandidate) {
                 const userDocument = await DocServer.GetRefField(user.userDocumentId);
                 if (userDocument instanceof Doc) {
@@ -130,7 +129,7 @@ export default class SharingManager extends React.Component<{}> {
         if (state === SharingPermissions.None) {
             const metadata = (await DocCastAsync(manager[key]));
             if (metadata) {
-                let sharedAlias = (await DocCastAsync(metadata.sharedAlias))!;
+                const sharedAlias = (await DocCastAsync(metadata.sharedAlias))!;
                 Doc.RemoveDocFromList(notificationDoc, storage, sharedAlias);
                 manager[key] = undefined;
             }
@@ -145,7 +144,7 @@ export default class SharingManager extends React.Component<{}> {
     }
 
     private setExternalSharing = (state: string) => {
-        let sharingDoc = this.sharingDoc;
+        const sharingDoc = this.sharingDoc;
         if (!sharingDoc) {
             return;
         }
@@ -156,7 +155,7 @@ export default class SharingManager extends React.Component<{}> {
         if (!this.targetDoc) {
             return undefined;
         }
-        let baseUrl = Utils.prepend("/doc/" + this.targetDoc[Id]);
+        const baseUrl = Utils.prepend("/doc/" + this.targetDoc[Id]);
         return `${baseUrl}?sharing=true`;
     }
 
@@ -178,7 +177,7 @@ export default class SharingManager extends React.Component<{}> {
     }
 
     private focusOn = (contents: string) => {
-        let title = this.targetDoc ? StrCast(this.targetDoc.title) : "";
+        const title = this.targetDoc ? StrCast(this.targetDoc.title) : "";
         return (
             <span
                 className={"focus-span"}
diff --git a/src/client/util/TooltipLinkingMenu.tsx b/src/client/util/TooltipLinkingMenu.tsx
index e6d6c471f..b46675a04 100644
--- a/src/client/util/TooltipLinkingMenu.tsx
+++ b/src/client/util/TooltipLinkingMenu.tsx
@@ -2,10 +2,6 @@ import { EditorState } from "prosemirror-state";
 import { EditorView } from "prosemirror-view";
 import { FieldViewProps } from "../views/nodes/FieldView";
 import "./TooltipTextMenu.scss";
-import React = require("react");
-const { toggleMark, setBlockType, wrapIn } = require("prosemirror-commands");
-
-const SVG = "http://www.w3.org/2000/svg";
 
 //appears above a selection of text in a RichTextBox to give user options such as Bold, Italics, etc.
 export class TooltipLinkingMenu {
@@ -23,9 +19,9 @@ export class TooltipLinkingMenu {
         //add the div which is the tooltip
         view.dom.parentNode!.parentNode!.appendChild(this.tooltip);
 
-        let target = "https://www.google.com";
+        const target = "https://www.google.com";
 
-        let link = document.createElement("a");
+        const link = document.createElement("a");
         link.href = target;
         link.textContent = target;
         link.target = "_blank";
@@ -37,7 +33,7 @@ export class TooltipLinkingMenu {
 
     //updates the tooltip menu when the selection changes
     update(view: EditorView, lastState: EditorState | undefined) {
-        let state = view.state;
+        const state = view.state;
         // Don't do anything if the document/selection didn't change
         if (lastState && lastState.doc.eq(state.doc) &&
             lastState.selection.eq(state.selection)) return;
@@ -53,16 +49,16 @@ export class TooltipLinkingMenu {
 
         // Otherwise, reposition it and update its content
         this.tooltip.style.display = "";
-        let { from, to } = state.selection;
-        let start = view.coordsAtPos(from), end = view.coordsAtPos(to);
+        const { from, to } = state.selection;
+        const start = view.coordsAtPos(from), end = view.coordsAtPos(to);
         // The box in which the tooltip is positioned, to use as base
-        let box = this.tooltip.offsetParent!.getBoundingClientRect();
+        const box = this.tooltip.offsetParent!.getBoundingClientRect();
         // Find a center-ish x position from the selection endpoints (when
         // crossing lines, end may be more to the left)
-        let left = Math.max((start.left + end.left) / 2, start.left + 3);
+        const left = Math.max((start.left + end.left) / 2, start.left + 3);
         this.tooltip.style.left = (left - box.left) * this.editorProps.ScreenToLocalTransform().Scale + "px";
-        let width = Math.abs(start.left - end.left) / 2 * this.editorProps.ScreenToLocalTransform().Scale;
-        let mid = Math.min(start.left, end.left) + width;
+        const width = Math.abs(start.left - end.left) / 2 * this.editorProps.ScreenToLocalTransform().Scale;
+        const mid = Math.min(start.left, end.left) + width;
 
         this.tooltip.style.width = "auto";
         this.tooltip.style.bottom = (box.bottom - start.top) * this.editorProps.ScreenToLocalTransform().Scale + "px";
diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx
index 5136089b3..5e777cef9 100644
--- a/src/client/util/TooltipTextMenu.tsx
+++ b/src/client/util/TooltipTextMenu.tsx
@@ -2,7 +2,7 @@ import { action } from "mobx";
 import { Dropdown, icons, MenuItem } from "prosemirror-menu"; //no import css
 import { Mark, MarkType, Node as ProsNode, NodeType, ResolvedPos, Schema } from "prosemirror-model";
 import { wrapInList } from 'prosemirror-schema-list';
-import { EditorState, NodeSelection, TextSelection, Transaction } from "prosemirror-state";
+import { EditorState, NodeSelection, TextSelection } from "prosemirror-state";
 import { EditorView } from "prosemirror-view";
 import { Doc, Field, Opt } from "../../new_fields/Doc";
 import { Id } from "../../new_fields/FieldSymbols";
@@ -86,7 +86,7 @@ export class TooltipTextMenu {
         this.basicTools.className = "basic-tools";
 
         // init buttons to the tooltip -- paths to svgs are obtained from fontawesome
-        let items = [
+        const items = [
             { command: toggleMark(schema.marks.strong), dom: this.svgIcon("strong", "Bold", "M333.49 238a122 122 0 0 0 27-65.21C367.87 96.49 308 32 233.42 32H34a16 16 0 0 0-16 16v48a16 16 0 0 0 16 16h31.87v288H34a16 16 0 0 0-16 16v48a16 16 0 0 0 16 16h209.32c70.8 0 134.14-51.75 141-122.4 4.74-48.45-16.39-92.06-50.83-119.6zM145.66 112h87.76a48 48 0 0 1 0 96h-87.76zm87.76 288h-87.76V288h87.76a56 56 0 0 1 0 112z") },
             { command: toggleMark(schema.marks.em), dom: this.svgIcon("em", "Italic", "M320 48v32a16 16 0 0 1-16 16h-62.76l-80 320H208a16 16 0 0 1 16 16v32a16 16 0 0 1-16 16H16a16 16 0 0 1-16-16v-32a16 16 0 0 1 16-16h62.76l80-320H112a16 16 0 0 1-16-16V48a16 16 0 0 1 16-16h192a16 16 0 0 1 16 16z") },
             { command: toggleMark(schema.marks.underline), dom: this.svgIcon("underline", "Underline", "M32 64h32v160c0 88.22 71.78 160 160 160s160-71.78 160-160V64h32a16 16 0 0 0 16-16V16a16 16 0 0 0-16-16H272a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h32v160a80 80 0 0 1-160 0V64h32a16 16 0 0 0 16-16V16a16 16 0 0 0-16-16H32a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16zm400 384H16a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h416a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16z") },
@@ -145,7 +145,7 @@ export class TooltipTextMenu {
 
         // link menu
         this.updateLinkMenu();
-        let dropdown = await this.createLinkDropdown();
+        const dropdown = await this.createLinkDropdown();
         this._linkDropdownDom = dropdown.render(this.view).dom;
         this.tooltip.appendChild(this._linkDropdownDom);
 
@@ -209,17 +209,17 @@ export class TooltipTextMenu {
     // creates dragger element that allows dragging and collapsing (on double click) 
     // of editor and appends it to the wrapper
     createDragger() {
-        let draggerWrapper = document.createElement("div");
+        const draggerWrapper = document.createElement("div");
         draggerWrapper.className = "dragger-wrapper";
 
-        let dragger = document.createElement("div");
+        const dragger = document.createElement("div");
         dragger.className = "dragger";
 
-        let line1 = document.createElement("span");
+        const line1 = document.createElement("span");
         line1.className = "dragger-line";
-        let line2 = document.createElement("span");
+        const line2 = document.createElement("span");
         line2.className = "dragger-line";
-        let line3 = document.createElement("span");
+        const line3 = document.createElement("span");
         line3.className = "dragger-line";
 
         dragger.appendChild(line1);
@@ -293,12 +293,12 @@ export class TooltipTextMenu {
     //label of dropdown will change to given label
     updateFontSizeDropdown(label: string) {
         //font SIZES
-        let fontSizeBtns: MenuItem[] = [];
+        const fontSizeBtns: MenuItem[] = [];
         this.fontSizes.forEach(mark => {
             fontSizeBtns.push(this.dropdownFontSizeBtn(String(mark.attrs.fontSize), "color: black; width: 50px;", mark, this.view, this.changeToFontSize));
         });
 
-        let newfontSizeDom = (new Dropdown(fontSizeBtns, {
+        const newfontSizeDom = (new Dropdown(fontSizeBtns, {
             label: label,
             css: "color:black; min-width: 60px;"
         }) as MenuItem).render(this.view).dom;
@@ -312,12 +312,12 @@ export class TooltipTextMenu {
     //label of dropdown will change to given label
     updateFontStyleDropdown(label: string) {
         //font STYLES
-        let fontBtns: MenuItem[] = [];
+        const fontBtns: MenuItem[] = [];
         this.fontStyles.forEach((mark) => {
             fontBtns.push(this.dropdownFontFamilyBtn(mark.attrs.family, "color: black; font-family: " + mark.attrs.family + ", sans-serif; width: 125px;", mark, this.view, this.changeToFontFamily));
         });
 
-        let newfontStyleDom = (new Dropdown(fontBtns, {
+        const newfontStyleDom = (new Dropdown(fontBtns, {
             label: label,
             css: "color:black; width: 125px;"
         }) as MenuItem).render(this.view).dom;
@@ -339,19 +339,19 @@ export class TooltipTextMenu {
             this.linkText.style.overflow = "hidden";
             this.linkText.style.color = "white";
             this.linkText.onpointerdown = (e: PointerEvent) => { e.stopPropagation(); };
-            let linkBtn = document.createElement("div");
+            const linkBtn = document.createElement("div");
             linkBtn.textContent = ">>";
             linkBtn.style.width = "10px";
             linkBtn.style.height = "10px";
             linkBtn.style.color = "white";
             linkBtn.style.cssFloat = "left";
             linkBtn.onpointerdown = (e: PointerEvent) => {
-                let node = this.view.state.selection.$from.nodeAfter;
-                let link = node && node.marks.find(m => m.type.name === "link");
+                const node = this.view.state.selection.$from.nodeAfter;
+                const link = node && node.marks.find(m => m.type.name === "link");
                 if (link) {
-                    let href: string = link.attrs.href;
+                    const href: string = link.attrs.href;
                     if (href.indexOf(Utils.prepend("/doc/")) === 0) {
-                        let docid = href.replace(Utils.prepend("/doc/"), "");
+                        const docid = href.replace(Utils.prepend("/doc/"), "");
                         DocServer.GetRefField(docid).then(action((f: Opt<Field>) => {
                             if (f instanceof Doc) {
                                 if (DocumentManager.Instance.getDocumentView(f)) {
@@ -374,23 +374,23 @@ export class TooltipTextMenu {
             this.linkDrag.id = "link-drag";
             this.linkDrag.onpointerdown = (e: PointerEvent) => {
                 if (!this.editorProps) return;
-                let dragData = new DragManager.LinkDragData(this.editorProps.Document);
+                const dragData = new DragManager.LinkDragData(this.editorProps.Document);
                 dragData.dontClearTextBox = true;
                 // hack to get source context -sy
-                let docView = DocumentManager.Instance.getDocumentView(this.editorProps.Document);
+                const docView = DocumentManager.Instance.getDocumentView(this.editorProps.Document);
                 e.stopPropagation();
-                let ctrlKey = e.ctrlKey;
+                const ctrlKey = e.ctrlKey;
                 DragManager.StartLinkDrag(this.linkDrag!, dragData, e.clientX, e.clientY,
                     {
                         handlers: {
                             dragComplete: action(() => {
                                 if (dragData.linkDocument) {
-                                    let linkDoc = dragData.linkDocument;
-                                    let proto = Doc.GetProto(linkDoc);
+                                    const linkDoc = dragData.linkDocument;
+                                    const proto = Doc.GetProto(linkDoc);
                                     if (proto && docView) {
                                         proto.sourceContext = docView.props.ContainingCollectionDoc;
                                     }
-                                    let text = this.makeLink(linkDoc, StrCast(linkDoc.anchor2.title), ctrlKey ? "onRight" : "inTab");
+                                    const text = this.makeLink(linkDoc, StrCast(linkDoc.anchor2.title), ctrlKey ? "onRight" : "inTab");
                                     if (linkDoc instanceof Doc && linkDoc.anchor2 instanceof Doc) {
                                         proto.title = text === "" ? proto.title : text + " to " + linkDoc.anchor2.title; // TODODO open to more descriptive descriptions of following in text link
                                     }
@@ -406,8 +406,8 @@ export class TooltipTextMenu {
             this.tooltip.appendChild(this.linkEditor);
         }
 
-        let node = this.view.state.selection.$from.nodeAfter;
-        let link = node && node.marks.find(m => m.type.name === "link");
+        const node = this.view.state.selection.$from.nodeAfter;
+        const link = node && node.marks.find(m => m.type.name === "link");
         this.linkText.textContent = link ? link.attrs.href : "-empty-";
 
         this.linkText.onkeydown = (e: KeyboardEvent) => {
@@ -420,19 +420,19 @@ export class TooltipTextMenu {
     }
 
     async getTextLinkTargetTitle() {
-        let node = this.view.state.selection.$from.nodeAfter;
-        let link = node && node.marks.find(m => m.type.name === "link");
+        const node = this.view.state.selection.$from.nodeAfter;
+        const link = node && node.marks.find(m => m.type.name === "link");
         if (link) {
-            let href = link.attrs.href;
+            const href = link.attrs.href;
             if (href) {
                 if (href.indexOf(Utils.prepend("/doc/")) === 0) {
                     const linkclicked = href.replace(Utils.prepend("/doc/"), "").split("?")[0];
                     if (linkclicked) {
-                        let linkDoc = await DocServer.GetRefField(linkclicked);
+                        const linkDoc = await DocServer.GetRefField(linkclicked);
                         if (linkDoc instanceof Doc) {
-                            let anchor1 = await Cast(linkDoc.anchor1, Doc);
-                            let anchor2 = await Cast(linkDoc.anchor2, Doc);
-                            let currentDoc = SelectionManager.SelectedDocuments().length && SelectionManager.SelectedDocuments()[0].props.Document;
+                            const anchor1 = await Cast(linkDoc.anchor1, Doc);
+                            const anchor2 = await Cast(linkDoc.anchor2, Doc);
+                            const currentDoc = SelectionManager.SelectedDocuments().length && SelectionManager.SelectedDocuments()[0].props.Document;
                             if (currentDoc && anchor1 && anchor2) {
                                 if (Doc.AreProtosEqual(currentDoc, anchor1)) {
                                     return StrCast(anchor2.title);
@@ -453,18 +453,18 @@ export class TooltipTextMenu {
     }
 
     async createLinkDropdown() {
-        let targetTitle = await this.getTextLinkTargetTitle();
-        let input = document.createElement("input");
+        const targetTitle = await this.getTextLinkTargetTitle();
+        const input = document.createElement("input");
 
         // menu item for input for hyperlink url 
         // TODO: integrate search to allow users to search for a doc to link to
-        let linkInfo = new MenuItem({
+        const linkInfo = new MenuItem({
             title: "",
             execEvent: "",
             class: "button-setting-disabled",
             css: "",
             render() {
-                let p = document.createElement("p");
+                const p = document.createElement("p");
                 p.textContent = "Linked to:";
 
                 input.type = "text";
@@ -475,7 +475,7 @@ export class TooltipTextMenu {
                     input.focus();
                 };
 
-                let div = document.createElement("div");
+                const div = document.createElement("div");
                 div.appendChild(p);
                 div.appendChild(input);
                 return div;
@@ -487,13 +487,13 @@ export class TooltipTextMenu {
         });
 
         // menu item to update/apply the hyperlink to the selected text
-        let linkApply = new MenuItem({
+        const linkApply = new MenuItem({
             title: "",
             execEvent: "",
             class: "",
             css: "",
             render() {
-                let button = document.createElement("button");
+                const button = document.createElement("button");
                 button.className = "link-url-button";
                 button.textContent = "Apply hyperlink";
                 return button;
@@ -507,17 +507,17 @@ export class TooltipTextMenu {
 
         // menu item to remove the link
         // TODO: allow this to be undoable
-        let self = this;
-        let deleteLink = new MenuItem({
+        const self = this;
+        const deleteLink = new MenuItem({
             title: "Delete link",
             execEvent: "",
             class: "separated-button",
             css: "",
             render() {
-                let button = document.createElement("button");
+                const button = document.createElement("button");
                 button.textContent = "Remove link";
 
-                let wrapper = document.createElement("div");
+                const wrapper = document.createElement("div");
                 wrapper.appendChild(button);
                 return wrapper;
             },
@@ -525,15 +525,15 @@ export class TooltipTextMenu {
             async run() {
                 self.deleteLink();
                 // update link dropdown
-                let dropdown = await self.createLinkDropdown();
-                let newLinkDropdowndom = dropdown.render(self.view).dom;
+                const dropdown = await self.createLinkDropdown();
+                const newLinkDropdowndom = dropdown.render(self.view).dom;
                 self._linkDropdownDom && self.tooltip.replaceChild(newLinkDropdowndom, self._linkDropdownDom);
                 self._linkDropdownDom = newLinkDropdowndom;
             }
         });
 
 
-        let linkDropdown = new Dropdown(targetTitle ? [linkInfo, linkApply, deleteLink] : [linkInfo, linkApply], { class: "buttonSettings-dropdown" }) as MenuItem;
+        const linkDropdown = new Dropdown(targetTitle ? [linkInfo, linkApply, deleteLink] : [linkInfo, linkApply], { class: "buttonSettings-dropdown" }) as MenuItem;
         return linkDropdown;
     }
 
@@ -542,10 +542,10 @@ export class TooltipTextMenu {
     // }
 
     makeLink = (targetDoc: Doc, title: string, location: string): string => {
-        let link = this.view.state.schema.marks.link.create({ href: Utils.prepend("/doc/" + targetDoc[Id]), title: title, location: location });
+        const link = this.view.state.schema.marks.link.create({ href: Utils.prepend("/doc/" + targetDoc[Id]), title: title, location: location });
         this.view.dispatch(this.view.state.tr.removeMark(this.view.state.selection.from, this.view.state.selection.to, this.view.state.schema.marks.link).
             addMark(this.view.state.selection.from, this.view.state.selection.to, link));
-        let node = this.view.state.selection.$from.nodeAfter;
+        const node = this.view.state.selection.$from.nodeAfter;
         if (node && node.text) {
             return node.text;
         }
@@ -562,9 +562,9 @@ export class TooltipTextMenu {
     }
 
     deleteLink = () => {
-        let node = this.view.state.selection.$from.nodeAfter;
-        let link = node && node.marks.find(m => m.type === this.view.state.schema.marks.link);
-        let href = link!.attrs.href;
+        const node = this.view.state.selection.$from.nodeAfter;
+        const link = node && node.marks.find(m => m.type === this.view.state.schema.marks.link);
+        const href = link!.attrs.href;
         if (href) {
             if (href.indexOf(Utils.prepend("/doc/")) === 0) {
                 const linkclicked = href.replace(Utils.prepend("/doc/"), "").split("?")[0];
@@ -599,7 +599,7 @@ export class TooltipTextMenu {
     }
 
     createLink() {
-        let markType = schema.marks.link;
+        const markType = schema.marks.link;
         return new MenuItem({
             title: "Add or remove link",
             label: "Add or remove link",
@@ -613,8 +613,8 @@ export class TooltipTextMenu {
                 let curLink = "";
                 if (this.markActive(state, markType)) {
 
-                    let { from, $from, to, empty } = state.selection;
-                    let node = state.doc.nodeAt(from);
+                    const { from, $from, to, empty } = state.selection;
+                    const node = state.doc.nodeAt(from);
                     node && node.marks.map(m => {
                         m.type === markType && (curLink = m.attrs.href);
                     });
@@ -649,7 +649,7 @@ export class TooltipTextMenu {
         if (listTypeBtn) { this.tooltip.removeChild(listTypeBtn); }
 
         //Make a dropdown of all list types
-        let toAdd: MenuItem[] = [];
+        const toAdd: MenuItem[] = [];
         this.listTypeToIcon.forEach((icon, type) => {
             toAdd.push(this.dropdownNodeBtn(icon, "color: black; width: 40px;", type, this.view, this.listTypes, this.changeToNodeType));
         });
@@ -683,22 +683,22 @@ export class TooltipTextMenu {
 
     public static insertStar(state: EditorState<any>, dispatch: any) {
         if (state.selection.empty) return false;
-        let mark = state.schema.marks.highlight.create();
-        let tr = state.tr;
+        const mark = state.schema.marks.highlight.create();
+        const tr = state.tr;
         tr.addMark(state.selection.from, state.selection.to, mark);
-        let content = tr.selection.content();
-        let newNode = state.schema.nodes.star.create({ visibility: false, text: content, textslice: content.toJSON() });
+        const content = tr.selection.content();
+        const newNode = state.schema.nodes.star.create({ visibility: false, text: content, textslice: content.toJSON() });
         dispatch && dispatch(tr.replaceSelectionWith(newNode).removeMark(tr.selection.from - 1, tr.selection.from, mark));
         return true;
     }
 
     public static insertComment(state: EditorState<any>, dispatch: any) {
         if (state.selection.empty) return false;
-        let mark = state.schema.marks.highlight.create();
-        let tr = state.tr;
+        const mark = state.schema.marks.highlight.create();
+        const tr = state.tr;
         tr.addMark(state.selection.from, state.selection.to, mark);
-        let content = tr.selection.content();
-        let newNode = state.schema.nodes.star.create({ visibility: false, text: content, textslice: content.toJSON() });
+        const content = tr.selection.content();
+        const newNode = state.schema.nodes.star.create({ visibility: false, text: content, textslice: content.toJSON() });
         dispatch && dispatch(tr.replaceSelectionWith(newNode).removeMark(tr.selection.from - 1, tr.selection.from, mark));
         return true;
     }
@@ -710,17 +710,17 @@ export class TooltipTextMenu {
             class: "menuicon",
             execEvent: "",
             render() {
-                let svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+                const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
                 svg.setAttribute("viewBox", "-100 -100 650 650");
-                let path = document.createElementNS('http://www.w3.org/2000/svg', "path");
+                const path = document.createElementNS('http://www.w3.org/2000/svg', "path");
                 path.setAttributeNS(null, "d", "M0 479.98L99.92 512l35.45-35.45-67.04-67.04L0 479.98zm124.61-240.01a36.592 36.592 0 0 0-10.79 38.1l13.05 42.83-50.93 50.94 96.23 96.23 50.86-50.86 42.74 13.08c13.73 4.2 28.65-.01 38.15-10.78l35.55-41.64-173.34-173.34-41.52 35.44zm403.31-160.7l-63.2-63.2c-20.49-20.49-53.38-21.52-75.12-2.35L190.55 183.68l169.77 169.78L530.27 154.4c19.18-21.74 18.15-54.63-2.35-75.13z");
                 svg.appendChild(path);
 
-                let color = document.createElement("div");
+                const color = document.createElement("div");
                 color.className = "buttonColor";
                 color.style.backgroundColor = TooltipTextMenuManager.Instance.highlight.toString();
 
-                let wrapper = document.createElement("div");
+                const wrapper = document.createElement("div");
                 wrapper.id = "colorPicker";
                 wrapper.appendChild(svg);
                 wrapper.appendChild(color);
@@ -735,26 +735,26 @@ export class TooltipTextMenu {
     public static insertHighlight(color: String, state: EditorState<any>, dispatch: any) {
         if (state.selection.empty) return false;
 
-        let highlightMark = state.schema.mark(state.schema.marks.marker, { highlight: color });
+        const highlightMark = state.schema.mark(state.schema.marks.marker, { highlight: color });
         dispatch(state.tr.addMark(state.selection.from, state.selection.to, highlightMark));
     }
 
     createHighlightDropdown() {
         // menu item for color picker
-        let self = this;
-        let colors = new MenuItem({
+        const self = this;
+        const colors = new MenuItem({
             title: "",
             execEvent: "",
             class: "button-setting-disabled",
             css: "",
             render() {
-                let p = document.createElement("p");
+                const p = document.createElement("p");
                 p.textContent = "Change highlight:";
 
-                let colorsWrapper = document.createElement("div");
+                const colorsWrapper = document.createElement("div");
                 colorsWrapper.className = "colorPicker-wrapper";
 
-                let colors = [
+                const colors = [
                     PastelSchemaPalette.get("pink2"),
                     PastelSchemaPalette.get("purple4"),
                     PastelSchemaPalette.get("bluegreen1"),
@@ -768,7 +768,7 @@ export class TooltipTextMenu {
                 ];
 
                 colors.forEach(color => {
-                    let button = document.createElement("button");
+                    const button = document.createElement("button");
                     button.className = color === TooltipTextMenuManager.Instance.highlight ? "colorPicker active" : "colorPicker";
                     if (color) {
                         button.style.backgroundColor = color;
@@ -779,8 +779,8 @@ export class TooltipTextMenu {
                             TooltipTextMenu.insertHighlight(TooltipTextMenuManager.Instance.highlight, self.view.state, self.view.dispatch);
 
                             // update color menu
-                            let highlightDom = self.createHighlightTool().render(self.view).dom;
-                            let highlightDropdownDom = self.createHighlightDropdown().render(self.view).dom;
+                            const highlightDom = self.createHighlightTool().render(self.view).dom;
+                            const highlightDropdownDom = self.createHighlightDropdown().render(self.view).dom;
                             self.highlightDom && self.tooltip.replaceChild(highlightDom, self.highlightDom);
                             self.highlightDropdownDom && self.tooltip.replaceChild(highlightDropdownDom, self.highlightDropdownDom);
                             self.highlightDom = highlightDom;
@@ -790,7 +790,7 @@ export class TooltipTextMenu {
                     colorsWrapper.appendChild(button);
                 });
 
-                let div = document.createElement("div");
+                const div = document.createElement("div");
                 div.appendChild(p);
                 div.appendChild(colorsWrapper);
                 return div;
@@ -801,7 +801,7 @@ export class TooltipTextMenu {
             }
         });
 
-        let colorDropdown = new Dropdown([colors], { class: "buttonSettings-dropdown" }) as MenuItem;
+        const colorDropdown = new Dropdown([colors], { class: "buttonSettings-dropdown" }) as MenuItem;
         return colorDropdown;
     }
 
@@ -812,17 +812,17 @@ export class TooltipTextMenu {
             class: "menuicon",
             execEvent: "",
             render() {
-                let svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+                const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
                 svg.setAttribute("viewBox", "-100 -100 650 650");
-                let path = document.createElementNS('http://www.w3.org/2000/svg', "path");
+                const path = document.createElementNS('http://www.w3.org/2000/svg', "path");
                 path.setAttributeNS(null, "d", "M204.3 5C104.9 24.4 24.8 104.3 5.2 203.4c-37 187 131.7 326.4 258.8 306.7 41.2-6.4 61.4-54.6 42.5-91.7-23.1-45.4 9.9-98.4 60.9-98.4h79.7c35.8 0 64.8-29.6 64.9-65.3C511.5 97.1 368.1-26.9 204.3 5zM96 320c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32zm32-128c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32zm128-64c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32zm128 64c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32z");
                 svg.appendChild(path);
 
-                let color = document.createElement("div");
+                const color = document.createElement("div");
                 color.className = "buttonColor";
                 color.style.backgroundColor = TooltipTextMenuManager.Instance.color.toString();
 
-                let wrapper = document.createElement("div");
+                const wrapper = document.createElement("div");
                 wrapper.id = "colorPicker";
                 wrapper.appendChild(svg);
                 wrapper.appendChild(color);
@@ -837,26 +837,26 @@ export class TooltipTextMenu {
     public static insertColor(color: String, state: EditorState<any>, dispatch: any) {
         if (state.selection.empty) return false;
 
-        let colorMark = state.schema.mark(state.schema.marks.color, { color: color });
+        const colorMark = state.schema.mark(state.schema.marks.color, { color: color });
         dispatch(state.tr.addMark(state.selection.from, state.selection.to, colorMark));
     }
 
     createColorDropdown() {
         // menu item for color picker
-        let self = this;
-        let colors = new MenuItem({
+        const self = this;
+        const colors = new MenuItem({
             title: "",
             execEvent: "",
             class: "button-setting-disabled",
             css: "",
             render() {
-                let p = document.createElement("p");
+                const p = document.createElement("p");
                 p.textContent = "Change color:";
 
-                let colorsWrapper = document.createElement("div");
+                const colorsWrapper = document.createElement("div");
                 colorsWrapper.className = "colorPicker-wrapper";
 
-                let colors = [
+                const colors = [
                     DarkPastelSchemaPalette.get("pink2"),
                     DarkPastelSchemaPalette.get("purple4"),
                     DarkPastelSchemaPalette.get("bluegreen1"),
@@ -870,7 +870,7 @@ export class TooltipTextMenu {
                 ];
 
                 colors.forEach(color => {
-                    let button = document.createElement("button");
+                    const button = document.createElement("button");
                     button.className = color === TooltipTextMenuManager.Instance.color ? "colorPicker active" : "colorPicker";
                     if (color) {
                         button.style.backgroundColor = color;
@@ -880,8 +880,8 @@ export class TooltipTextMenu {
                             TooltipTextMenu.insertColor(TooltipTextMenuManager.Instance.color, self.view.state, self.view.dispatch);
 
                             // update color menu
-                            let colorDom = self.createColorTool().render(self.view).dom;
-                            let colorDropdownDom = self.createColorDropdown().render(self.view).dom;
+                            const colorDom = self.createColorTool().render(self.view).dom;
+                            const colorDropdownDom = self.createColorDropdown().render(self.view).dom;
                             self.colorDom && self.tooltip.replaceChild(colorDom, self.colorDom);
                             self.colorDropdownDom && self.tooltip.replaceChild(colorDropdownDom, self.colorDropdownDom);
                             self.colorDom = colorDom;
@@ -891,7 +891,7 @@ export class TooltipTextMenu {
                     colorsWrapper.appendChild(button);
                 });
 
-                let div = document.createElement("div");
+                const div = document.createElement("div");
                 div.appendChild(p);
                 div.appendChild(colorsWrapper);
                 return div;
@@ -902,7 +902,7 @@ export class TooltipTextMenu {
             }
         });
 
-        let colorDropdown = new Dropdown([colors], { class: "buttonSettings-dropdown" }) as MenuItem;
+        const colorDropdown = new Dropdown([colors], { class: "buttonSettings-dropdown" }) as MenuItem;
         return colorDropdown;
     }
 
@@ -911,7 +911,7 @@ export class TooltipTextMenu {
             height: 32, width: 32,
             path: "M30.828 1.172c-1.562-1.562-4.095-1.562-5.657 0l-5.379 5.379-3.793-3.793-4.243 4.243 3.326 3.326-14.754 14.754c-0.252 0.252-0.358 0.592-0.322 0.921h-0.008v5c0 0.552 0.448 1 1 1h5c0 0 0.083 0 0.125 0 0.288 0 0.576-0.11 0.795-0.329l14.754-14.754 3.326 3.326 4.243-4.243-3.793-3.793 5.379-5.379c1.562-1.562 1.562-4.095 0-5.657zM5.409 30h-3.409v-3.409l14.674-14.674 3.409 3.409-14.674 14.674z"
         };
-        let self = this;
+        const self = this;
         return new MenuItem({
             title: "Brush tool",
             label: "Brush tool",
@@ -923,7 +923,7 @@ export class TooltipTextMenu {
                 this.brush_function(state, dispatch);
 
                 // update dropdown with marks
-                let newBrushDropdowndom = self.createBrushDropdown().render(self.view).dom;
+                const newBrushDropdowndom = self.createBrushDropdown().render(self.view).dom;
                 self._brushDropdownDom && self.tooltip.replaceChild(newBrushDropdowndom, self._brushDropdownDom);
                 self._brushDropdownDom = newBrushDropdowndom;
             },
@@ -947,7 +947,7 @@ export class TooltipTextMenu {
             }
         }
         else {
-            let { from, to, $from } = this.view.state.selection;
+            const { from, to, $from } = this.view.state.selection;
             if (this._brushdom) {
                 if (!this.view.state.selection.empty && $from && $from.nodeAfter) {
                     if (TooltipTextMenuManager.Instance._brushMarks && to - from > 0) {
@@ -982,7 +982,7 @@ export class TooltipTextMenu {
         }
 
 
-        let brushInfo = new MenuItem({
+        const brushInfo = new MenuItem({
             title: "",
             label: label,
             execEvent: "",
@@ -994,17 +994,17 @@ export class TooltipTextMenu {
             }
         });
 
-        let self = this;
-        let clearBrush = new MenuItem({
+        const self = this;
+        const clearBrush = new MenuItem({
             title: "Clear brush",
             execEvent: "",
             class: "separated-button",
             css: "",
             render() {
-                let button = document.createElement("button");
+                const button = document.createElement("button");
                 button.textContent = "Clear brush";
 
-                let wrapper = document.createElement("div");
+                const wrapper = document.createElement("div");
                 wrapper.appendChild(button);
                 return wrapper;
             },
@@ -1015,24 +1015,24 @@ export class TooltipTextMenu {
 
                 // update brush tool
                 // TODO: this probably isn't very clean
-                let newBrushdom = self.createBrush().render(self.view).dom;
+                const newBrushdom = self.createBrush().render(self.view).dom;
                 self._brushdom && self.tooltip.replaceChild(newBrushdom, self._brushdom);
                 self._brushdom = newBrushdom;
-                let newBrushDropdowndom = self.createBrushDropdown().render(self.view).dom;
+                const newBrushDropdowndom = self.createBrushDropdown().render(self.view).dom;
                 self._brushDropdownDom && self.tooltip.replaceChild(newBrushDropdowndom, self._brushDropdownDom);
                 self._brushDropdownDom = newBrushDropdowndom;
             }
         });
 
-        let hasMarks = TooltipTextMenuManager.Instance._brushMarks && TooltipTextMenuManager.Instance._brushMarks.size > 0;
-        let brushDom = new Dropdown(hasMarks ? [brushInfo, clearBrush] : [brushInfo], { class: "buttonSettings-dropdown" }) as MenuItem;
+        const hasMarks = TooltipTextMenuManager.Instance._brushMarks && TooltipTextMenuManager.Instance._brushMarks.size > 0;
+        const brushDom = new Dropdown(hasMarks ? [brushInfo, clearBrush] : [brushInfo], { class: "buttonSettings-dropdown" }) as MenuItem;
         return brushDom;
     }
     //for a specific grouping of marks (passed in), remove all and apply the passed-in one to the selected textchangeToMarkInGroup = (markType: MarkType | undefined, view: EditorView, fontMarks: MarkType[]) => {
     changeToMarkInGroup = (markType: MarkType | undefined, view: EditorView, fontMarks: MarkType[]) => {
-        let { $cursor, ranges } = view.state.selection as TextSelection;
-        let state = view.state;
-        let dispatch = view.dispatch;
+        const { $cursor, ranges } = view.state.selection as TextSelection;
+        const state = view.state;
+        const dispatch = view.dispatch;
 
         //remove all other active font marks
         fontMarks.forEach((type) => {
@@ -1044,10 +1044,10 @@ export class TooltipTextMenu {
                 } else {
                     let has = false;
                     for (let i = 0; !has && i < ranges.length; i++) {
-                        let { $from, $to } = ranges[i];
+                        const { $from, $to } = ranges[i];
                         has = state.doc.rangeHasMark($from.pos, $to.pos, type);
                     }
-                    for (let i of ranges) {
+                    for (const i of ranges) {
                         if (has) {
                             toggleMark(type)(view.state, view.dispatch, view);
                         }
@@ -1059,7 +1059,7 @@ export class TooltipTextMenu {
         if (markType) {
             //actually apply font
             if ((view.state.selection as any).node && (view.state.selection as any).node.type === view.state.schema.nodes.ordered_list) {
-                let status = updateBullets(view.state.tr.setNodeMarkup(view.state.selection.from, (view.state.selection as any).node.type,
+                const status = updateBullets(view.state.tr.setNodeMarkup(view.state.selection.from, (view.state.selection as any).node.type,
                     { ...(view.state.selection as NodeSelection).node.attrs, setFontFamily: markType.name, setFontSize: Number(markType.name.replace(/p/, "")) }), view.state.schema);
                 view.dispatch(status.setSelection(new NodeSelection(status.doc.resolve(view.state.selection.from))));
             }
@@ -1068,9 +1068,9 @@ export class TooltipTextMenu {
     }
 
     changeToFontFamily = (mark: Mark, view: EditorView) => {
-        let { $cursor, ranges } = view.state.selection as TextSelection;
-        let state = view.state;
-        let dispatch = view.dispatch;
+        const { $cursor, ranges } = view.state.selection as TextSelection;
+        const state = view.state;
+        const dispatch = view.dispatch;
 
         //remove all other active font marks
         if ($cursor) {
@@ -1080,28 +1080,28 @@ export class TooltipTextMenu {
         } else {
             let has = false;
             for (let i = 0; !has && i < ranges.length; i++) {
-                let { $from, $to } = ranges[i];
+                const { $from, $to } = ranges[i];
                 has = state.doc.rangeHasMark($from.pos, $to.pos, view.state.schema.marks.pFontFamily);
             }
-            for (let i of ranges) {
+            for (const i of ranges) {
                 if (has) {
                     toggleMark(view.state.schema.marks.pFontFamily)(view.state, view.dispatch, view);
                 }
             }
         }
 
-        let fontName = mark.attrs.family;
+        const fontName = mark.attrs.family;
         if (fontName) { this.updateFontStyleDropdown(fontName); }
         if (this.editorProps) {
-            let ruleProvider = this.editorProps.ruleProvider;
-            let heading = NumCast(this.editorProps.Document.heading);
+            const ruleProvider = this.editorProps.ruleProvider;
+            const heading = NumCast(this.editorProps.Document.heading);
             if (ruleProvider && heading) {
                 ruleProvider["ruleFont_" + heading] = fontName;
             }
         }
         //actually apply font
         if ((view.state.selection as any).node && (view.state.selection as any).node.type === view.state.schema.nodes.ordered_list) {
-            let status = updateBullets(view.state.tr.setNodeMarkup(view.state.selection.from, (view.state.selection as any).node.type,
+            const status = updateBullets(view.state.tr.setNodeMarkup(view.state.selection.from, (view.state.selection as any).node.type,
                 { ...(view.state.selection as NodeSelection).node.attrs, setFontFamily: fontName }), view.state.schema);
             view.dispatch(status.setSelection(new NodeSelection(status.doc.resolve(view.state.selection.from))));
         }
@@ -1110,9 +1110,9 @@ export class TooltipTextMenu {
     }
 
     changeToFontSize = (mark: Mark, view: EditorView) => {
-        let { $cursor, ranges } = view.state.selection as TextSelection;
-        let state = view.state;
-        let dispatch = view.dispatch;
+        const { $cursor, ranges } = view.state.selection as TextSelection;
+        const state = view.state;
+        const dispatch = view.dispatch;
 
         //remove all other active font marks
         if ($cursor) {
@@ -1122,28 +1122,28 @@ export class TooltipTextMenu {
         } else {
             let has = false;
             for (let i = 0; !has && i < ranges.length; i++) {
-                let { $from, $to } = ranges[i];
+                const { $from, $to } = ranges[i];
                 has = state.doc.rangeHasMark($from.pos, $to.pos, view.state.schema.marks.pFontSize);
             }
-            for (let i of ranges) {
+            for (const i of ranges) {
                 if (has) {
                     toggleMark(view.state.schema.marks.pFontSize)(view.state, view.dispatch, view);
                 }
             }
         }
 
-        let size = mark.attrs.fontSize;
+        const size = mark.attrs.fontSize;
         if (size) { this.updateFontSizeDropdown(String(size) + " pt"); }
         if (this.editorProps) {
-            let ruleProvider = this.editorProps.ruleProvider;
-            let heading = NumCast(this.editorProps.Document.heading);
+            const ruleProvider = this.editorProps.ruleProvider;
+            const heading = NumCast(this.editorProps.Document.heading);
             if (ruleProvider && heading) {
                 ruleProvider["ruleSize_" + heading] = size;
             }
         }
         //actually apply font
         if ((view.state.selection as any).node && (view.state.selection as any).node.type === view.state.schema.nodes.ordered_list) {
-            let status = updateBullets(view.state.tr.setNodeMarkup(view.state.selection.from, (view.state.selection as any).node.type,
+            const status = updateBullets(view.state.tr.setNodeMarkup(view.state.selection.from, (view.state.selection as any).node.type,
                 { ...(view.state.selection as NodeSelection).node.attrs, setFontSize: size }), view.state.schema);
             view.dispatch(status.setSelection(new NodeSelection(status.doc.resolve(view.state.selection.from))));
         }
@@ -1154,20 +1154,20 @@ export class TooltipTextMenu {
     //remove all node typeand apply the passed-in one to the selected text
     changeToNodeType = (nodeType: NodeType | undefined) => {
         //remove oldif (nodeType) { //add new
-        let view = this.view;
+        const view = this.view;
         if (nodeType === schema.nodes.bullet_list) {
             wrapInList(nodeType)(view.state, view.dispatch);
         } else {
-            var marks = view.state.storedMarks || (view.state.selection.$to.parentOffset && view.state.selection.$from.marks());
+            const marks = view.state.storedMarks || (view.state.selection.$to.parentOffset && view.state.selection.$from.marks());
             if (!wrapInList(schema.nodes.ordered_list)(view.state, (tx2: any) => {
-                let tx3 = updateBullets(tx2, schema, nodeType && (nodeType as any).attrs.mapStyle);
+                const tx3 = updateBullets(tx2, schema, nodeType && (nodeType as any).attrs.mapStyle);
                 marks && tx3.ensureMarks([...marks]);
                 marks && tx3.setStoredMarks([...marks]);
 
                 view.dispatch(tx2);
             })) {
-                let tx2 = view.state.tr;
-                let tx3 = updateBullets(tx2, schema, nodeType && (nodeType as any).attrs.mapStyle);
+                const tx2 = view.state.tr;
+                const tx3 = updateBullets(tx2, schema, nodeType && (nodeType as any).attrs.mapStyle);
                 marks && tx3.ensureMarks([...marks]);
                 marks && tx3.setStoredMarks([...marks]);
 
@@ -1223,15 +1223,15 @@ export class TooltipTextMenu {
         });
     }
 
-    markActive = function(state: EditorState<any>, type: MarkType<Schema<string, string>>) {
-        let { from, $from, to, empty } = state.selection;
+    markActive = function (state: EditorState<any>, type: MarkType<Schema<string, string>>) {
+        const { from, $from, to, empty } = state.selection;
         if (empty) return type.isInSet(state.storedMarks || $from.marks());
         else return state.doc.rangeHasMark(from, to, type);
     };
 
     // Helper function to create menu icons
     icon(text: string, name: string, title: string = name) {
-        let span = document.createElement("span");
+        const span = document.createElement("span");
         span.className = name + " menuicon";
         span.title = title;
         span.textContent = text;
@@ -1240,13 +1240,13 @@ export class TooltipTextMenu {
     }
 
     svgIcon(name: string, title: string = name, dpath: string) {
-        let svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+        const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
         svg.setAttribute("viewBox", "-100 -100 650 650");
-        let path = document.createElementNS('http://www.w3.org/2000/svg', "path");
+        const path = document.createElementNS('http://www.w3.org/2000/svg', "path");
         path.setAttributeNS(null, "d", dpath);
         svg.appendChild(path);
 
-        let span = document.createElement("span");
+        const span = document.createElement("span");
         span.className = name + " menuicon";
         span.title = title;
         span.appendChild(svg);
@@ -1256,9 +1256,9 @@ export class TooltipTextMenu {
 
     //method for checking whether node can be inserted
     canInsert(state: EditorState, nodeType: NodeType<Schema<string, string>>) {
-        let $from = state.selection.$from;
+        const $from = state.selection.$from;
         for (let d = $from.depth; d >= 0; d--) {
-            let index = $from.index(d);
+            const index = $from.index(d);
             if ($from.node(d).canReplaceWith(index, index, nodeType)) return true;
         }
         return false;
@@ -1267,13 +1267,13 @@ export class TooltipTextMenu {
 
     //adapted this method - use it to check if block has a tag (ie bulleting)
     blockActive(type: NodeType<Schema<string, string>>, state: EditorState) {
-        let attrs = {};
+        const attrs = {};
 
         if (state.selection instanceof NodeSelection) {
             const sel: NodeSelection = state.selection;
-            let $from = sel.$from;
-            let to = sel.to;
-            let node = sel.node;
+            const $from = sel.$from;
+            const to = sel.to;
+            const node = sel.node;
 
             if (node) {
                 return node.hasMarkup(type, attrs);
@@ -1292,10 +1292,10 @@ export class TooltipTextMenu {
     }
 
     getMarksInSelection(state: EditorState<any>) {
-        let found = new Set<Mark>();
-        let { from, to } = state.selection as TextSelection;
+        const found = new Set<Mark>();
+        const { from, to } = state.selection as TextSelection;
         state.doc.nodesBetween(from, to, (node) => {
-            let marks = node.marks;
+            const marks = node.marks;
             if (marks) {
                 marks.forEach(m => {
                     found.add(m);
@@ -1306,7 +1306,7 @@ export class TooltipTextMenu {
     }
 
     reset_mark_doms() {
-        let iterator = this._marksToDoms.values();
+        const iterator = this._marksToDoms.values();
         let next = iterator.next();
         while (!next.done) {
             next.value.style.color = "white";
@@ -1322,7 +1322,7 @@ export class TooltipTextMenu {
             return;
         }
         this.view = view;
-        let state = view.state;
+        const state = view.state;
         DocumentDecorations.Instance.showTextBar();
         props && (this.editorProps = props);
         // Don't do anything if the document/selection didn't change
@@ -1338,13 +1338,13 @@ export class TooltipTextMenu {
         }
 
         // update link dropdown
-        let linkDropdown = await this.createLinkDropdown();
-        let newLinkDropdowndom = linkDropdown.render(this.view).dom;
+        const linkDropdown = await this.createLinkDropdown();
+        const newLinkDropdowndom = linkDropdown.render(this.view).dom;
         this._linkDropdownDom && this.tooltip.replaceChild(newLinkDropdowndom, this._linkDropdownDom);
         this._linkDropdownDom = newLinkDropdowndom;
 
         //UPDATE FONT STYLE DROPDOWN
-        let activeStyles = this.activeFontFamilyOnSelection();
+        const activeStyles = this.activeFontFamilyOnSelection();
         if (activeStyles !== undefined) {
             if (activeStyles.length === 1) {
                 console.log("updating font style dropdown", activeStyles[0]);
@@ -1353,7 +1353,7 @@ export class TooltipTextMenu {
         }
 
         //UPDATE FONT SIZE DROPDOWN
-        let activeSizes = this.activeFontSizeOnSelection();
+        const activeSizes = this.activeFontSizeOnSelection();
         if (activeSizes !== undefined) {
             if (activeSizes.length === 1) { //if there's only one active font size
                 activeSizes[0] && this.updateFontSizeDropdown(String(activeSizes[0]) + " pt");
@@ -1366,7 +1366,7 @@ export class TooltipTextMenu {
         this.reset_mark_doms();
         this._activeMarks.forEach((mark) => {
             if (this._marksToDoms.has(mark)) {
-                let dom = this._marksToDoms.get(mark);
+                const dom = this._marksToDoms.get(mark);
                 if (dom) dom.style.color = "greenyellow";
             }
         });
@@ -1385,8 +1385,8 @@ export class TooltipTextMenu {
     //finds fontSize at start of selection
     activeFontSizeOnSelection() {
         //current selection
-        let state = this.view.state;
-        let activeSizes: number[] = [];
+        const state = this.view.state;
+        const activeSizes: number[] = [];
         const pos = this.view.state.selection.$from;
         const ref_node: ProsNode = this.reference_node(pos);
         if (ref_node && ref_node !== this.view.state.doc && ref_node.isText) {
@@ -1397,8 +1397,8 @@ export class TooltipTextMenu {
     //finds fontSize at start of selection
     activeFontFamilyOnSelection() {
         //current selection
-        let state = this.view.state;
-        let activeFamilies: string[] = [];
+        const state = this.view.state;
+        const activeFamilies: string[] = [];
         const pos = this.view.state.selection.$from;
         const ref_node: ProsNode = this.reference_node(pos);
         if (ref_node && ref_node !== this.view.state.doc && ref_node.isText) {
@@ -1409,15 +1409,15 @@ export class TooltipTextMenu {
     //finds all active marks on selection in given group
     activeMarksOnSelection(markGroup: MarkType[]) {
         //current selection
-        let { empty, ranges, $to } = this.view.state.selection as TextSelection;
-        let state = this.view.state;
-        let dispatch = this.view.dispatch;
+        const { empty, ranges, $to } = this.view.state.selection as TextSelection;
+        const state = this.view.state;
+        const dispatch = this.view.dispatch;
         let activeMarks: MarkType[];
         if (!empty) {
             activeMarks = markGroup.filter(mark => {
-                let has = false;
+                const has = false;
                 for (let i = 0; !has && i < ranges.length; i++) {
-                    let { $from, $to } = ranges[i];
+                    const { $from, $to } = ranges[i];
                     return state.doc.rangeHasMark($from.pos, $to.pos, mark);
                 }
                 return false;
@@ -1440,7 +1440,7 @@ export class TooltipTextMenu {
                     if (mark_type === state.schema.marks.pFontSize) {
                         return ref_node.marks.some(m => m.type.name === state.schema.marks.pFontSize.name);
                     }
-                    let mark = state.schema.mark(mark_type);
+                    const mark = state.schema.mark(mark_type);
                     return ref_node.marks.includes(mark);
                     return false;
                 });
diff --git a/src/client/util/TypedEvent.ts b/src/client/util/TypedEvent.ts
index 532ba78eb..90fd299c1 100644
--- a/src/client/util/TypedEvent.ts
+++ b/src/client/util/TypedEvent.ts
@@ -1,40 +1,40 @@
 export interface Listener<T> {
-  (event: T): any;
+    (event: T): any;
 }
 
 export interface Disposable {
-  dispose(): void;
+    dispose(): void;
 }
 
 /** passes through events as they happen. You will not get events from before you start listening */
 export class TypedEvent<T> {
-  private listeners: Listener<T>[] = [];
-  private listenersOncer: Listener<T>[] = [];
-
-  on = (listener: Listener<T>): Disposable => {
-    this.listeners.push(listener);
-    return {
-      dispose: () => this.off(listener)
-    };
-  }
-
-  once = (listener: Listener<T>): void => {
-    this.listenersOncer.push(listener);
-  }
-
-  off = (listener: Listener<T>) => {
-    var callbackIndex = this.listeners.indexOf(listener);
-    if (callbackIndex > -1) this.listeners.splice(callbackIndex, 1);
-  }
-
-  emit = (event: T) => {
-    /** Update any general listeners */
-    this.listeners.forEach((listener) => listener(event));
-
-    /** Clear the `once` queue */
-    this.listenersOncer.forEach((listener) => listener(event));
-    this.listenersOncer = [];
-  }
-
-  pipe = (te: TypedEvent<T>): Disposable => this.on((e) => te.emit(e));
+    private listeners: Listener<T>[] = [];
+    private listenersOncer: Listener<T>[] = [];
+
+    on = (listener: Listener<T>): Disposable => {
+        this.listeners.push(listener);
+        return {
+            dispose: () => this.off(listener)
+        };
+    }
+
+    once = (listener: Listener<T>): void => {
+        this.listenersOncer.push(listener);
+    }
+
+    off = (listener: Listener<T>) => {
+        const callbackIndex = this.listeners.indexOf(listener);
+        if (callbackIndex > -1) this.listeners.splice(callbackIndex, 1);
+    }
+
+    emit = (event: T) => {
+        /** Update any general listeners */
+        this.listeners.forEach((listener) => listener(event));
+
+        /** Clear the `once` queue */
+        this.listenersOncer.forEach((listener) => listener(event));
+        this.listenersOncer = [];
+    }
+
+    pipe = (te: TypedEvent<T>): Disposable => this.on((e) => te.emit(e));
 }
\ No newline at end of file
diff --git a/src/client/util/UndoManager.ts b/src/client/util/UndoManager.ts
index 472afac1d..314b52bf3 100644
--- a/src/client/util/UndoManager.ts
+++ b/src/client/util/UndoManager.ts
@@ -3,7 +3,7 @@ import 'source-map-support/register';
 import { Without } from "../../Utils";
 
 function getBatchName(target: any, key: string | symbol): string {
-    let keyName = key.toString();
+    const keyName = key.toString();
     if (target && target.constructor && target.constructor.name) {
         return `${target.constructor.name}.${keyName}`;
     }
@@ -23,7 +23,7 @@ function propertyDecorator(target: any, key: string | symbol) {
                 writable: true,
                 configurable: true,
                 value: function (...args: any[]) {
-                    let batch = UndoManager.StartBatch(getBatchName(target, key));
+                    const batch = UndoManager.StartBatch(getBatchName(target, key));
                     try {
                         return value.apply(this, args);
                     } finally {
@@ -40,7 +40,7 @@ export function undoBatch(fn: (...args: any[]) => any): (...args: any[]) => any;
 export function undoBatch(target: any, key?: string | symbol, descriptor?: TypedPropertyDescriptor<any>): any {
     if (!key) {
         return function () {
-            let batch = UndoManager.StartBatch("");
+            const batch = UndoManager.StartBatch("");
             try {
                 return target.apply(undefined, arguments);
             } finally {
@@ -55,7 +55,7 @@ export function undoBatch(target: any, key?: string | symbol, descriptor?: Typed
     const oldFunction = descriptor.value;
 
     descriptor.value = function (...args: any[]) {
-        let batch = UndoManager.StartBatch(getBatchName(target, key));
+        const batch = UndoManager.StartBatch(getBatchName(target, key));
         try {
             return oldFunction.apply(this, args);
         } finally {
@@ -98,7 +98,7 @@ export namespace UndoManager {
         GetOpenBatches().forEach(batch => console.log(batch.batchName));
     }
 
-    let openBatches: Batch[] = [];
+    const openBatches: Batch[] = [];
     export function GetOpenBatches(): Without<Batch, 'end'>[] {
         return openBatches;
     }
@@ -146,7 +146,7 @@ export namespace UndoManager {
 
     //TODO Make this return the return value
     export function RunInBatch<T>(fn: () => T, batchName: string) {
-        let batch = StartBatch(batchName);
+        const batch = StartBatch(batchName);
         try {
             return runInAction(fn);
         } finally {
@@ -159,7 +159,7 @@ export namespace UndoManager {
             return;
         }
 
-        let commands = undoStack.pop();
+        const commands = undoStack.pop();
         if (!commands) {
             return;
         }
@@ -178,7 +178,7 @@ export namespace UndoManager {
             return;
         }
 
-        let commands = redoStack.pop();
+        const commands = redoStack.pop();
         if (!commands) {
             return;
         }
diff --git a/src/client/views/CollectionLinearView.tsx b/src/client/views/CollectionLinearView.tsx
index f718735a8..09e4ef99c 100644
--- a/src/client/views/CollectionLinearView.tsx
+++ b/src/client/views/CollectionLinearView.tsx
@@ -48,12 +48,12 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) {
     dimension = () => NumCast(this.props.Document.height); // 2 * the padding
     getTransform = (ele: React.RefObject<HTMLDivElement>) => () => {
         if (!ele.current) return Transform.Identity();
-        let { scale, translateX, translateY } = Utils.GetScreenTransform(ele.current);
+        const { scale, translateX, translateY } = Utils.GetScreenTransform(ele.current);
         return new Transform(-translateX, -translateY, 1 / scale);
     }
 
     render() {
-        let guid = Utils.GenerateGuid();
+        const guid = Utils.GenerateGuid();
         return <div className="collectionLinearView-outer">
             <div className="collectionLinearView" ref={this.createDropTarget} >
                 <input id={`${guid}`} type="checkbox" checked={BoolCast(this.props.Document.isExpanded)} ref={this.addMenuToggle}
@@ -62,10 +62,10 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) {
 
                 <div className="collectionLinearView-content" style={{ height: this.dimension(), width: NumCast(this.props.Document.width, 25) }}>
                     {this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map(pair => {
-                        let nested = pair.layout.viewType === CollectionViewType.Linear;
-                        let dref = React.createRef<HTMLDivElement>();
-                        let nativeWidth = NumCast(pair.layout.nativeWidth, this.dimension());
-                        let deltaSize = nativeWidth * .15 / 2;
+                        const nested = pair.layout.viewType === CollectionViewType.Linear;
+                        const dref = React.createRef<HTMLDivElement>();
+                        const nativeWidth = NumCast(pair.layout.nativeWidth, this.dimension());
+                        const deltaSize = nativeWidth * .15 / 2;
                         return <div className={`collectionLinearView-docBtn` + (pair.layout.onClick || pair.layout.onDragStart ? "-scalable" : "")} key={pair.layout[Id]} ref={dref}
                             style={{
                                 width: nested ? pair.layout[WidthSym]() : this.dimension() - deltaSize,
diff --git a/src/client/views/ContextMenu.tsx b/src/client/views/ContextMenu.tsx
index 5d452e72e..937aff0d6 100644
--- a/src/client/views/ContextMenu.tsx
+++ b/src/client/views/ContextMenu.tsx
@@ -49,8 +49,8 @@ export class ContextMenu extends React.Component {
     @action
     onPointerUp = (e: PointerEvent) => {
         this._mouseDown = false;
-        let curX = e.clientX;
-        let curY = e.clientY;
+        const curX = e.clientX;
+        const curY = e.clientY;
         if (this._mouseX !== curX || this._mouseY !== curY) {
             this._shouldDisplay = false;
         }
@@ -208,7 +208,7 @@ export class ContextMenu extends React.Component {
         if (!this._display) {
             return null;
         }
-        let style = this._yRelativeToTop ? { left: this.pageX, top: this.pageY } :
+        const style = this._yRelativeToTop ? { left: this.pageX, top: this.pageY } :
             { left: this.pageX, bottom: this.pageY };
 
         const contents = (
diff --git a/src/client/views/ContextMenuItem.tsx b/src/client/views/ContextMenuItem.tsx
index 330b94afa..fef9e5f60 100644
--- a/src/client/views/ContextMenuItem.tsx
+++ b/src/client/views/ContextMenuItem.tsx
@@ -88,7 +88,7 @@ export class ContextMenuItem extends React.Component<ContextMenuProps & { select
                 </div>
             );
         } else if ("subitems" in this.props) {
-            let submenu = !this.overItem ? (null) :
+            const submenu = !this.overItem ? (null) :
                 <div className="contextMenu-subMenu-cont" style={{ marginLeft: "25%", left: "0px" }}>
                     {this._items.map(prop => <ContextMenuItem {...prop} key={prop.description} closeMenu={this.props.closeMenu} />)}
                 </div>;
diff --git a/src/client/views/DictationOverlay.tsx b/src/client/views/DictationOverlay.tsx
index 2accf9bfd..65770c0bb 100644
--- a/src/client/views/DictationOverlay.tsx
+++ b/src/client/views/DictationOverlay.tsx
@@ -24,7 +24,7 @@ export class DictationOverlay extends React.Component {
     }
 
     public initiateDictationFade = () => {
-        let duration = DictationManager.Commands.dictationFadeDuration;
+        const duration = DictationManager.Commands.dictationFadeDuration;
         this.overlayTimeout = setTimeout(() => {
             this.dictationOverlayVisible = false;
             this.dictationSuccess = undefined;
@@ -50,14 +50,14 @@ export class DictationOverlay extends React.Component {
     public set isListening(value: DictationManager.Controls.ListeningUIStatus) { runInAction(() => this._dictationListeningState = value); }
 
     render() {
-        let success = this.dictationSuccess;
-        let result = this.isListening && !this.isListening.interim ? DictationManager.placeholder : `"${this.dictatedPhrase}"`;
-        let dialogueBoxStyle = {
+        const success = this.dictationSuccess;
+        const result = this.isListening && !this.isListening.interim ? DictationManager.placeholder : `"${this.dictatedPhrase}"`;
+        const dialogueBoxStyle = {
             background: success === undefined ? "gainsboro" : success ? "lawngreen" : "red",
             borderColor: this.isListening ? "red" : "black",
             fontStyle: "italic"
         };
-        let overlayStyle = {
+        const overlayStyle = {
             backgroundColor: this.isListening ? "red" : "darkslategrey"
         };
         return (<MainViewModal
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index b3a130b33..27ee9f122 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -1,4 +1,3 @@
-import * as React from 'react';
 import { Doc } from '../../new_fields/Doc';
 import { Touchable } from './Touchable';
 import { computed, action, observable } from 'mobx';
@@ -65,8 +64,8 @@ export function DocAnnotatableComponent<P extends DocAnnotatableProps, T>(schema
         @action.bound
         removeDocument(doc: Doc): boolean {
             Doc.GetProto(doc).annotationOn = undefined;
-            let value = this.extensionDoc && Cast(this.extensionDoc[this.annotationsKey], listSpec(Doc), []);
-            let index = value ? Doc.IndexOf(doc, value.map(d => d as Doc), true) : -1;
+            const value = this.extensionDoc && Cast(this.extensionDoc[this.annotationsKey], listSpec(Doc), []);
+            const index = value ? Doc.IndexOf(doc, value.map(d => d as Doc), true) : -1;
             return index !== -1 && value && value.splice(index, 1) ? true : false;
         }
         // if the moved document is already in this overlay collection nothing needs to be done.
diff --git a/src/client/views/DocumentButtonBar.scss b/src/client/views/DocumentButtonBar.scss
index db6bf2ba0..c2ca93900 100644
--- a/src/client/views/DocumentButtonBar.scss
+++ b/src/client/views/DocumentButtonBar.scss
@@ -17,6 +17,7 @@ $linkGap : 3px;
     transform: scale(1.05);
     cursor: pointer;
 }
+
 .documentButtonBar-linkButton-empty,
 .documentButtonBar-linkButton-nonempty {
     height: 20px;
@@ -74,6 +75,31 @@ $linkGap : 3px;
 }
 
 
-@-moz-keyframes spin { 100% { -moz-transform: rotate(360deg); } }
-@-webkit-keyframes spin { 100% { -webkit-transform: rotate(360deg); } }
-@keyframes spin { 100% { -webkit-transform: rotate(360deg); transform: rotate(360deg); } }
\ No newline at end of file
+@-moz-keyframes spin {
+    100% {
+        -moz-transform: rotate(360deg);
+    }
+}
+
+@-webkit-keyframes spin {
+    100% {
+        -webkit-transform: rotate(360deg);
+    }
+}
+
+@keyframes spin {
+    100% {
+        -webkit-transform: rotate(360deg);
+        transform: rotate(360deg);
+    }
+}
+
+@keyframes shadow-pulse {
+    0% {
+        box-shadow: 0 0 0 0px rgba(0, 0, 0, 0.8);
+    }
+
+    100% {
+        box-shadow: 0 0 0 10px rgba(0, 255, 0, 0);
+    }
+}
\ No newline at end of file
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index 1fefc70f1..15cbd2788 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -21,6 +21,7 @@ import React = require("react");
 import { DocumentView } from './nodes/DocumentView';
 import { ParentDocSelector } from './collections/ParentDocumentSelector';
 import { CollectionDockingView } from './collections/CollectionDockingView';
+import { DocumentDecorations } from './DocumentDecorations';
 const higflyout = require("@hig/flyout");
 export const { anchorPoints } = higflyout;
 export const Flyout = higflyout.default;
@@ -51,7 +52,9 @@ export class DocumentButtonBar extends React.Component<{ views: DocumentView[],
     @observable private pushIcon: IconProp = "arrow-alt-circle-up";
     @observable private pullIcon: IconProp = "arrow-alt-circle-down";
     @observable private pullColor: string = "white";
-    @observable private isAnimatingFetch = false;
+    @observable public isAnimatingFetch = false;
+    @observable public isAnimatingPulse = false;
+
     @observable private openHover = false;
 
     public static Instance: DocumentButtonBar;
@@ -75,6 +78,7 @@ export class DocumentButtonBar extends React.Component<{ views: DocumentView[],
     });
 
     public startPushOutcome = action((success: boolean) => {
+        this.isAnimatingPulse = false;
         if (!this._pushAnimating) {
             this._pushAnimating = true;
             this.pushIcon = success ? "check-circle" : "stop-circle";
@@ -99,27 +103,26 @@ export class DocumentButtonBar extends React.Component<{ views: DocumentView[],
         this._pullColorAnimating = false;
     });
 
-
     @action
     onLinkButtonMoved = (e: PointerEvent): void => {
         if (this._linkButton.current !== null && (Math.abs(e.clientX - this._downX) > 3 || Math.abs(e.clientY - this._downY) > 3)) {
             document.removeEventListener("pointermove", this.onLinkButtonMoved);
             document.removeEventListener("pointerup", this.onLinkButtonUp);
-            let docView = this.props.views[0];
-            let container = docView.props.ContainingCollectionDoc?.proto;
-            let dragData = new DragManager.LinkDragData(docView.props.Document, container ? [container] : []);
-            let linkDrag = UndoManager.StartBatch("Drag Link");
+            const docView = this.props.views[0];
+            const container = docView.props.ContainingCollectionDoc?.proto;
+            const dragData = new DragManager.LinkDragData(docView.props.Document, container ? [container] : []);
+            const linkDrag = UndoManager.StartBatch("Drag Link");
             DragManager.StartLinkDrag(this._linkButton.current, dragData, e.pageX, e.pageY, {
                 handlers: {
                     dragComplete: () => {
-                        let tooltipmenu = FormattedTextBox.ToolTipTextMenu;
-                        let linkDoc = dragData.linkDocument;
+                        const tooltipmenu = FormattedTextBox.ToolTipTextMenu;
+                        const linkDoc = dragData.linkDocument;
                         if (linkDoc && tooltipmenu) {
-                            let proto = Doc.GetProto(linkDoc);
+                            const proto = Doc.GetProto(linkDoc);
                             if (proto && docView) {
                                 proto.sourceContext = docView.props.ContainingCollectionDoc;
                             }
-                            let text = tooltipmenu.makeLink(linkDoc, StrCast(linkDoc.anchor2.title), e.ctrlKey ? "onRight" : "inTab");
+                            const text = tooltipmenu.makeLink(linkDoc, StrCast(linkDoc.anchor2.title), e.ctrlKey ? "onRight" : "inTab");
                             if (linkDoc instanceof Doc && linkDoc.anchor2 instanceof Doc) {
                                 proto.title = text === "" ? proto.title : text + " to " + linkDoc.anchor2.title; // TODODO open to more descriptive descriptions of following in text link
                             }
@@ -152,21 +155,27 @@ export class DocumentButtonBar extends React.Component<{ views: DocumentView[],
 
     @computed
     get considerGoogleDocsPush() {
-        let targetDoc = this.props.views[0].props.Document;
-        let published = Doc.GetProto(targetDoc)[GoogleRef] !== undefined;
-        return <div title={`${published ? "Push" : "Publish"} to Google Docs`} className="documentButtonBar-linker" onClick={() => {
-            DocumentButtonBar.hasPushedHack = false;
-            targetDoc[Pushes] = NumCast(targetDoc[Pushes]) + 1;
-        }}>
+        const targetDoc = this.props.views[0].props.Document;
+        const published = Doc.GetProto(targetDoc)[GoogleRef] !== undefined;
+        const animation = this.isAnimatingPulse ? "shadow-pulse 1s linear infinite" : "none";
+        return <div
+            title={`${published ? "Push" : "Publish"} to Google Docs`}
+            className="documentButtonBar-linker"
+            style={{ animation }}
+            onClick={() => {
+                !published && runInAction(() => this.isAnimatingPulse = true);
+                DocumentButtonBar.hasPushedHack = false;
+                targetDoc[Pushes] = NumCast(targetDoc[Pushes]) + 1;
+            }}>
             <FontAwesomeIcon className="documentdecorations-icon" icon={published ? (this.pushIcon as any) : cloud} size={published ? "sm" : "xs"} />
         </div>;
     }
 
     @computed
     get considerGoogleDocsPull() {
-        let targetDoc = this.props.views[0].props.Document;
-        let dataDoc = Doc.GetProto(targetDoc);
-        let animation = this.isAnimatingFetch ? "spin 0.5s linear infinite" : "none";
+        const targetDoc = this.props.views[0].props.Document;
+        const dataDoc = Doc.GetProto(targetDoc);
+        const animation = this.isAnimatingFetch ? "spin 0.5s linear infinite" : "none";
         return !dataDoc[GoogleRef] ? (null) : <div className="documentButtonBar-linker"
             title={`${!dataDoc.unchanged ? "Pull from" : "Fetch"} Google Docs`}
             style={{ backgroundColor: this.pullColor }}
@@ -192,7 +201,7 @@ export class DocumentButtonBar extends React.Component<{ views: DocumentView[],
 
     @computed
     get linkButton() {
-        let linkCount = LinkManager.Instance.getAllRelatedLinks(this.props.views[0].props.Document).length;
+        const linkCount = LinkManager.Instance.getAllRelatedLinks(this.props.views[0].props.Document).length;
         return <div title="Drag(create link) Tap(view links)" className="documentButtonBar-linkFlyout" ref={this._linkButton}>
             <Flyout anchorPoint={anchorPoints.RIGHT_TOP}
                 content={<LinkMenu docView={this.props.views[0]} addDocTab={this.props.views[0].props.addDocTab} changeFlyout={emptyFunction} />}>
@@ -214,13 +223,13 @@ export class DocumentButtonBar extends React.Component<{ views: DocumentView[],
     }
 
     render() {
-        let templates: Map<Template, boolean> = new Map();
+        const templates: Map<Template, boolean> = new Map();
         Array.from(Object.values(Templates.TemplateList)).map(template =>
             templates.set(template, this.props.views.reduce((checked, doc) => checked || doc.getLayoutPropStr("show" + template.Name) ? true : false, false as boolean)));
 
-        let isText = this.props.views[0].props.Document.data instanceof RichTextField; // bcz: Todo - can't assume layout is using the 'data' field.  need to add fieldKey to DocumentView
-        let considerPull = isText && this.considerGoogleDocsPull;
-        let considerPush = isText && this.considerGoogleDocsPush;
+        const isText = this.props.views[0].props.Document.data instanceof RichTextField; // bcz: Todo - can't assume layout is using the 'data' field.  need to add fieldKey to DocumentView
+        const considerPull = isText && this.considerGoogleDocsPull;
+        const considerPush = isText && this.considerGoogleDocsPush;
         return <div className="documentButtonBar">
             <div className="documentButtonBar-button">
                 {this.linkButton}
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 66f47147f..7b0c31e37 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -24,7 +24,6 @@ import { DocumentView } from "./nodes/DocumentView";
 import { FieldView } from "./nodes/FieldView";
 import { IconBox } from "./nodes/IconBox";
 import React = require("react");
-import { PointData } from '../../new_fields/InkField';
 import { DocumentType } from '../documents/DocumentTypes';
 const higflyout = require("@hig/flyout");
 export const { anchorPoints } = higflyout;
@@ -68,8 +67,6 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
     @observable public pushIcon: IconProp = "arrow-alt-circle-up";
     @observable public pullIcon: IconProp = "arrow-alt-circle-down";
     @observable public pullColor: string = "white";
-    @observable public isAnimatingFetch = false;
-    @observable public isAnimatingPulse = false;
     @observable public openHover = false;
 
     constructor(props: Readonly<{}>) {
@@ -82,25 +79,25 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
     @action titleChanged = (event: any) => { this._title = event.target.value; };
     @action titleBlur = () => { this._edtingTitle = false; };
     @action titleEntered = (e: any) => {
-        var key = e.keyCode || e.which;
+        const key = e.keyCode || e.which;
         // enter pressed
         if (key === 13) {
-            var text = e.target.value;
+            const text = e.target.value;
             if (text[0] === '#') {
                 this._fieldKey = text.slice(1, text.length);
                 this._title = this.selectionTitle;
             } else if (text.startsWith("::")) {
-                let targetID = text.slice(2, text.length);
-                let promoteDoc = SelectionManager.SelectedDocuments()[0];
+                const targetID = text.slice(2, text.length);
+                const promoteDoc = SelectionManager.SelectedDocuments()[0];
                 DocUtils.Publish(promoteDoc.props.Document, targetID, promoteDoc.props.addDocument, promoteDoc.props.removeDocument);
             } else if (text.startsWith(">")) {
-                let fieldTemplateView = SelectionManager.SelectedDocuments()[0];
+                const fieldTemplateView = SelectionManager.SelectedDocuments()[0];
                 SelectionManager.DeselectAll();
-                let fieldTemplate = fieldTemplateView.props.Document;
-                let containerView = fieldTemplateView.props.ContainingCollectionView;
-                let docTemplate = fieldTemplateView.props.ContainingCollectionDoc;
+                const fieldTemplate = fieldTemplateView.props.Document;
+                const containerView = fieldTemplateView.props.ContainingCollectionView;
+                const docTemplate = fieldTemplateView.props.ContainingCollectionDoc;
                 if (containerView && docTemplate) {
-                    let metaKey = text.startsWith(">>") ? text.slice(2, text.length) : text.slice(1, text.length);
+                    const metaKey = text.startsWith(">>") ? text.slice(2, text.length) : text.slice(1, text.length);
                     if (metaKey !== containerView.props.fieldKey && containerView.props.DataDoc) {
                         const fd = fieldTemplate.data;
                         fd instanceof ObjectField && (Doc.GetProto(containerView.props.DataDoc)[metaKey] = ObjectField.MakeCopy(fd));
@@ -115,15 +112,15 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
             else {
                 if (SelectionManager.SelectedDocuments().length > 0) {
                     SelectionManager.SelectedDocuments()[0].props.Document.customTitle = !this._title.startsWith("-");
-                    let field = SelectionManager.SelectedDocuments()[0].props.Document[this._fieldKey];
+                    const field = SelectionManager.SelectedDocuments()[0].props.Document[this._fieldKey];
                     if (typeof field === "number") {
                         SelectionManager.SelectedDocuments().forEach(d => {
-                            let doc = d.props.Document.proto ? d.props.Document.proto : d.props.Document;
+                            const doc = d.props.Document.proto ? d.props.Document.proto : d.props.Document;
                             doc[this._fieldKey] = +this._title;
                         });
                     } else {
                         SelectionManager.SelectedDocuments().forEach(d => {
-                            let doc = d.props.Document.proto ? d.props.Document.proto : d.props.Document;
+                            const doc = d.props.Document.proto ? d.props.Document.proto : d.props.Document;
                             doc[this._fieldKey] = this._title;
                         });
                     }
@@ -165,11 +162,11 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
                 Doc.AreProtosEqual(documentView.props.Document, CurrentUserUtils.UserDocument)) {
                 return bounds;
             }
-            let transform = (documentView.props.ScreenToLocalTransform().scale(documentView.props.ContentScaling())).inverse();
+            const transform = (documentView.props.ScreenToLocalTransform().scale(documentView.props.ContentScaling())).inverse();
             var [sptX, sptY] = transform.transformPoint(0, 0);
             let [bptX, bptY] = transform.transformPoint(documentView.props.PanelWidth(), documentView.props.PanelHeight());
             if (documentView.props.Document.type === DocumentType.LINK) {
-                let rect = documentView.ContentDiv!.getElementsByClassName("docuLinkBox-cont")[0].getBoundingClientRect();
+                const rect = documentView.ContentDiv!.getElementsByClassName("docuLinkBox-cont")[0].getBoundingClientRect();
                 sptX = rect.left;
                 sptY = rect.top;
                 bptX = rect.right;
@@ -192,8 +189,8 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
 
     @action
     onBackgroundMove = (e: PointerEvent): void => {
-        let dragDocView = SelectionManager.SelectedDocuments()[0];
-        let dragData = new DragManager.DocumentDragData(SelectionManager.SelectedDocuments().map(dv => dv.props.Document));
+        const dragDocView = SelectionManager.SelectedDocuments()[0];
+        const dragData = new DragManager.DocumentDragData(SelectionManager.SelectedDocuments().map(dv => dv.props.Document));
         const [left, top] = dragDocView.props.ScreenToLocalTransform().scale(dragDocView.props.ContentScaling()).inverse().transformPoint(0, 0);
         dragData.offset = dragDocView.props.ScreenToLocalTransform().scale(dragDocView.props.ContentScaling()).transformDirection(e.x - left, e.y - top);
         dragData.moveDocument = SelectionManager.SelectedDocuments()[0].props.moveDocument;
@@ -256,8 +253,8 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
             this._downX = e.pageX;
             this._downY = e.pageY;
             this._removeIcon = false;
-            let selDoc = SelectionManager.SelectedDocuments()[0];
-            let selDocPos = selDoc.props.ScreenToLocalTransform().scale(selDoc.props.ContentScaling()).inverse().transformPoint(0, 0);
+            const selDoc = SelectionManager.SelectedDocuments()[0];
+            const selDocPos = selDoc.props.ScreenToLocalTransform().scale(selDoc.props.ContentScaling()).inverse().transformPoint(0, 0);
             this._minimizedX = selDocPos[0] + 12;
             this._minimizedY = selDocPos[1] + 12;
             document.removeEventListener("pointermove", this.onMinimizeMove);
@@ -272,12 +269,12 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
         e.stopPropagation();
         if (Math.abs(e.pageX - this._downX) > Utils.DRAG_THRESHOLD ||
             Math.abs(e.pageY - this._downY) > Utils.DRAG_THRESHOLD) {
-            let selDoc = SelectionManager.SelectedDocuments()[0];
-            let selDocPos = selDoc.props.ScreenToLocalTransform().scale(selDoc.props.ContentScaling()).inverse().transformPoint(0, 0);
-            let snapped = Math.abs(e.pageX - selDocPos[0]) < 20 && Math.abs(e.pageY - selDocPos[1]) < 20;
+            const selDoc = SelectionManager.SelectedDocuments()[0];
+            const selDocPos = selDoc.props.ScreenToLocalTransform().scale(selDoc.props.ContentScaling()).inverse().transformPoint(0, 0);
+            const snapped = Math.abs(e.pageX - selDocPos[0]) < 20 && Math.abs(e.pageY - selDocPos[1]) < 20;
             this._minimizedX = snapped ? selDocPos[0] + 4 : e.clientX;
             this._minimizedY = snapped ? selDocPos[1] - 18 : e.clientY;
-            let selectedDocs = SelectionManager.SelectedDocuments().map(sd => sd);
+            const selectedDocs = SelectionManager.SelectedDocuments().map(sd => sd);
 
             if (selectedDocs.length > 1) {
                 this._iconDoc = this._iconDoc ? this._iconDoc : this.createIcon(SelectionManager.SelectedDocuments(), CollectionView.LayoutString(""));
@@ -295,15 +292,15 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
         if (e.button === 0) {
             document.removeEventListener("pointermove", this.onMinimizeMove);
             document.removeEventListener("pointerup", this.onMinimizeUp);
-            let selectedDocs = SelectionManager.SelectedDocuments().map(sd => sd);
+            const selectedDocs = SelectionManager.SelectedDocuments().map(sd => sd);
             if (this._iconDoc && selectedDocs.length === 1 && this._removeIcon) {
                 selectedDocs[0].props.removeDocument && selectedDocs[0].props.removeDocument(this._iconDoc);
             }
             if (!this._removeIcon && selectedDocs.length === 1) { // if you click on the top-left button when just 1 doc is selected, then collapse it.  not sure why we don't do it for multiple selections
                 this.getIconDoc(selectedDocs[0]).then(async icon => {
-                    let minimizedDoc = await Cast(selectedDocs[0].props.Document.minimizedDoc, Doc);
+                    const minimizedDoc = await Cast(selectedDocs[0].props.Document.minimizedDoc, Doc);
                     if (minimizedDoc) {
-                        let scrpt = selectedDocs[0].props.ScreenToLocalTransform().scale(selectedDocs[0].props.ContentScaling()).inverse().transformPoint(
+                        const scrpt = selectedDocs[0].props.ScreenToLocalTransform().scale(selectedDocs[0].props.ContentScaling()).inverse().transformPoint(
                             NumCast(minimizedDoc.x) - NumCast(selectedDocs[0].Document.x), NumCast(minimizedDoc.y) - NumCast(selectedDocs[0].Document.y));
                         SelectionManager.DeselectAll();
                         DocumentManager.Instance.animateBetweenPoint(scrpt, await DocListCastAsync(minimizedDoc.maximizedDocs));
@@ -317,8 +314,8 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
 
     @undoBatch
     @action createIcon = (selected: DocumentView[], layoutString: string): Doc => {
-        let doc = selected[0].props.Document;
-        let iconDoc = Docs.Create.IconDocument(layoutString);
+        const doc = selected[0].props.Document;
+        const iconDoc = Docs.Create.IconDocument(layoutString);
         iconDoc.isButton = true;
 
         IconBox.AutomaticTitle(iconDoc);
@@ -334,7 +331,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
     }
     @action
     public getIconDoc = async (docView: DocumentView): Promise<Doc | undefined> => {
-        let doc = docView.props.Document;
+        const doc = docView.props.Document;
         let iconDoc: Doc | undefined = await Cast(doc.minimizedDoc, Doc);
 
         if (!iconDoc || !DocumentManager.Instance.getDocumentView(iconDoc)) {
@@ -344,8 +341,8 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
         return iconDoc;
     }
     moveIconDoc(iconDoc: Doc) {
-        let selView = SelectionManager.SelectedDocuments()[0];
-        let where = (selView.props.ScreenToLocalTransform()).scale(selView.props.ContentScaling()).
+        const selView = SelectionManager.SelectedDocuments()[0];
+        const where = (selView.props.ScreenToLocalTransform()).scale(selView.props.ContentScaling()).
             transformPoint(this._minimizedX - 12, this._minimizedY - 12);
         iconDoc.x = where[0] + NumCast(selView.props.Document.x);
         iconDoc.y = where[1] + NumCast(selView.props.Document.y);
@@ -370,8 +367,8 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
         dist = dist < 3 ? 0 : dist;
         let usingRule = false;
         SelectionManager.SelectedDocuments().map(dv => {
-            let ruleProvider = dv.props.ruleProvider;
-            let heading = NumCast(dv.props.Document.heading);
+            const ruleProvider = dv.props.ruleProvider;
+            const heading = NumCast(dv.props.Document.heading);
             ruleProvider && heading && (Doc.GetProto(ruleProvider)["ruleRounding_" + heading] = `${Math.min(100, dist)}%`);
             usingRule = usingRule || (ruleProvider && heading ? true : false);
         });
@@ -419,8 +416,8 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
 
         let dX = 0, dY = 0, dW = 0, dH = 0;
 
-        let moveX = e.clientX - this._lastX; // e.movementX;
-        let moveY = e.clientY - this._lastY; // e.movementY;
+        const moveX = e.clientX - this._lastX; // e.movementX;
+        const moveY = e.clientY - this._lastY; // e.movementY;
         this._lastX = e.clientX;
         this._lastY = e.clientY;
 
@@ -465,18 +462,18 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
 
         SelectionManager.SelectedDocuments().forEach(action((element: DocumentView) => {
             if (dX !== 0 || dY !== 0 || dW !== 0 || dH !== 0) {
-                let doc = PositionDocument(element.props.Document);
-                let layoutDoc = PositionDocument(Doc.Layout(element.props.Document));
+                const doc = PositionDocument(element.props.Document);
+                const layoutDoc = PositionDocument(Doc.Layout(element.props.Document));
                 let nwidth = layoutDoc.nativeWidth || 0;
                 let nheight = layoutDoc.nativeHeight || 0;
-                let width = (layoutDoc.width || 0);
-                let height = (layoutDoc.height || (nheight / nwidth * width));
-                let scale = element.props.ScreenToLocalTransform().Scale * element.props.ContentScaling();
-                let actualdW = Math.max(width + (dW * scale), 20);
-                let actualdH = Math.max(height + (dH * scale), 20);
+                const width = (layoutDoc.width || 0);
+                const height = (layoutDoc.height || (nheight / nwidth * width));
+                const scale = element.props.ScreenToLocalTransform().Scale * element.props.ContentScaling();
+                const actualdW = Math.max(width + (dW * scale), 20);
+                const actualdH = Math.max(height + (dH * scale), 20);
                 doc.x = (doc.x || 0) + dX * (actualdW - width);
                 doc.y = (doc.y || 0) + dY * (actualdH - height);
-                let fixedAspect = e.ctrlKey || (!layoutDoc.ignoreAspect && nwidth && nheight);
+                const fixedAspect = e.ctrlKey || (!layoutDoc.ignoreAspect && nwidth && nheight);
                 if (fixedAspect && e.ctrlKey && layoutDoc.ignoreAspect) {
                     layoutDoc.ignoreAspect = false;
                     layoutDoc.nativeWidth = nwidth = layoutDoc.width || 0;
@@ -529,8 +526,8 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
     @computed
     get selectionTitle(): string {
         if (SelectionManager.SelectedDocuments().length === 1) {
-            let selected = SelectionManager.SelectedDocuments()[0];
-            let field = selected.props.Document[this._fieldKey];
+            const selected = SelectionManager.SelectedDocuments()[0];
+            const field = selected.props.Document[this._fieldKey];
             if (typeof field === "string") {
                 return field;
             }
@@ -555,12 +552,12 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
         }
     }
     render() {
-        var bounds = this.Bounds;
-        let seldoc = SelectionManager.SelectedDocuments().length ? SelectionManager.SelectedDocuments()[0] : undefined;
+        const bounds = this.Bounds;
+        const seldoc = SelectionManager.SelectedDocuments().length ? SelectionManager.SelectedDocuments()[0] : undefined;
         if (SelectionManager.GetIsDragging() || bounds.x === Number.MAX_VALUE || !seldoc || this._hidden || isNaN(bounds.r) || isNaN(bounds.b) || isNaN(bounds.x) || isNaN(bounds.y)) {
             return (null);
         }
-        let minimizeIcon = (
+        const minimizeIcon = (
             <div className="documentDecorations-minimizeButton" onPointerDown={this.onMinimizeDown}>
                 {/* Currently, this is set to be enabled if there is no ink selected. It might be interesting to think about minimizing ink if it's useful? -syip2*/}
                 {SelectionManager.SelectedDocuments().length === 1 ? IconBox.DocumentIcon(StrCast(SelectionManager.SelectedDocuments()[0].props.Document.layout, "...")) : "..."}
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index 8f397e331..edc7df12a 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -25,7 +25,7 @@ export default class KeyManager {
     private router = new Map<string, KeyHandler>();
 
     constructor() {
-        let isMac = navigator.platform.toLowerCase().indexOf("mac") >= 0;
+        const isMac = navigator.platform.toLowerCase().indexOf("mac") >= 0;
 
         // SHIFT CONTROL ALT META
         this.router.set("0000", this.unmodified);
@@ -36,22 +36,22 @@ export default class KeyManager {
     }
 
     public handle = async (e: KeyboardEvent) => {
-        let keyname = e.key && e.key.toLowerCase();
+        const keyname = e.key && e.key.toLowerCase();
         this.handleGreedy(keyname);
 
         if (modifiers.includes(keyname)) {
             return;
         }
 
-        let bit = (value: boolean) => value ? "1" : "0";
-        let modifierIndex = bit(e.shiftKey) + bit(e.ctrlKey) + bit(e.altKey) + bit(e.metaKey);
+        const bit = (value: boolean) => value ? "1" : "0";
+        const modifierIndex = bit(e.shiftKey) + bit(e.ctrlKey) + bit(e.altKey) + bit(e.metaKey);
 
-        let handleConstrained = this.router.get(modifierIndex);
+        const handleConstrained = this.router.get(modifierIndex);
         if (!handleConstrained) {
             return;
         }
 
-        let control = await handleConstrained(keyname, e);
+        const control = await handleConstrained(keyname, e);
 
         control.stopPropagation && e.stopPropagation();
         control.preventDefault && e.preventDefault();
@@ -65,7 +65,7 @@ export default class KeyManager {
     private unmodified = action((keyname: string, e: KeyboardEvent) => {
         switch (keyname) {
             case "escape":
-                let main = MainView.Instance;
+                const main = MainView.Instance;
                 InkingControl.Instance.switchTool(InkTool.None);
                 if (main.isPointerDown) {
                     DragManager.AbortDrag();
@@ -89,8 +89,8 @@ export default class KeyManager {
                 }
                 UndoManager.RunInBatch(() => {
                     SelectionManager.SelectedDocuments().map(docView => {
-                        let doc = docView.props.Document;
-                        let remove = docView.props.removeDocument;
+                        const doc = docView.props.Document;
+                        const remove = docView.props.removeDocument;
                         remove && remove(doc);
                     });
                 }, "delete");
@@ -121,8 +121,8 @@ export default class KeyManager {
     }
 
     private alt = action((keyname: string) => {
-        let stopPropagation = true;
-        let preventDefault = true;
+        const stopPropagation = true;
+        const preventDefault = true;
 
         switch (keyname) {
             // case "n":
@@ -190,7 +190,7 @@ export default class KeyManager {
                 }
                 break;
             case "o":
-                let target = SelectionManager.SelectedDocuments()[0];
+                const target = SelectionManager.SelectedDocuments()[0];
                 target && CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(target);
                 break;
             case "r":
@@ -220,12 +220,12 @@ export default class KeyManager {
     });
 
     async printClipboard() {
-        let text: string = await navigator.clipboard.readText();
+        const text: string = await navigator.clipboard.readText();
     }
 
     private ctrl_shift = action((keyname: string) => {
-        let stopPropagation = true;
-        let preventDefault = true;
+        const stopPropagation = true;
+        const preventDefault = true;
 
         switch (keyname) {
             case "z":
diff --git a/src/client/views/InkSelectDecorations.tsx b/src/client/views/InkSelectDecorations.tsx
index d40df9b75..3ad50762d 100644
--- a/src/client/views/InkSelectDecorations.tsx
+++ b/src/client/views/InkSelectDecorations.tsx
@@ -29,10 +29,10 @@ export default class InkSelectDecorations extends Touchable {
 
     @computed
     get Bounds(): { x: number, y: number, b: number, r: number } {
-        let left = Number.MAX_VALUE;
-        let top = Number.MAX_VALUE;
-        let right = -Number.MAX_VALUE;
-        let bottom = -Number.MAX_VALUE;
+        const left = Number.MAX_VALUE;
+        const top = Number.MAX_VALUE;
+        const right = -Number.MAX_VALUE;
+        const bottom = -Number.MAX_VALUE;
         this._selectedInkNodes.forEach((value: PointData, key: string) => {
             // value.pathData.map(val => {
             //     left = Math.min(val.x, left);
@@ -45,7 +45,7 @@ export default class InkSelectDecorations extends Touchable {
     }
 
     render() {
-        let bounds = this.Bounds;
+        const bounds = this.Bounds;
         return <div style={{
             top: bounds.y, left: bounds.x,
             height: bounds.b - bounds.y,
diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx
index 75faa9641..e33f193b8 100644
--- a/src/client/views/InkingControl.tsx
+++ b/src/client/views/InkingControl.tsx
@@ -1,5 +1,5 @@
 import { action, computed, observable } from "mobx";
-import { ColorResult } from 'react-color';
+import { ColorState } from 'react-color';
 import { Doc } from "../../new_fields/Doc";
 import { InkTool } from "../../new_fields/InkField";
 import { List } from "../../new_fields/List";
@@ -35,16 +35,16 @@ export class InkingControl {
     }
 
     @undoBatch
-    switchColor = action((color: ColorResult): void => {
+    switchColor = action((color: ColorState): void => {
         this._selectedColor = color.hex + (color.rgb.a !== undefined ? this.decimalToHexString(Math.round(color.rgb.a * 255)) : "ff");
 
         if (InkingControl.Instance.selectedTool === InkTool.None) {
-            let selected = SelectionManager.SelectedDocuments();
-            let oldColors = selected.map(view => {
-                let targetDoc = view.props.Document.dragFactory instanceof Doc ? view.props.Document.dragFactory :
+            const selected = SelectionManager.SelectedDocuments();
+            const oldColors = selected.map(view => {
+                const targetDoc = view.props.Document.dragFactory instanceof Doc ? view.props.Document.dragFactory :
                     view.props.Document.layout instanceof Doc ? view.props.Document.layout :
                         view.props.Document.isTemplateField ? view.props.Document : Doc.GetProto(view.props.Document);
-                let sel = window.getSelection();
+                const sel = window.getSelection();
                 if (StrCast(targetDoc.layout).indexOf("FormattedTextBox") !== -1 && (!sel || sel.toString() !== "")) {
                     targetDoc.color = this._selectedColor;
                     return {
@@ -52,24 +52,24 @@ export class InkingControl {
                         previous: StrCast(targetDoc.color)
                     };
                 }
-                let oldColor = StrCast(targetDoc.backgroundColor);
+                const oldColor = StrCast(targetDoc.backgroundColor);
                 let matchedColor = this._selectedColor;
                 const cvd = view.props.ContainingCollectionDoc;
                 let ruleProvider = view.props.ruleProvider;
                 if (cvd) {
                     if (!cvd.colorPalette) {
-                        let defaultPalette = ["rg(114,229,239)", "rgb(255,246,209)", "rgb(255,188,156)", "rgb(247,220,96)", "rgb(122,176,238)",
+                        const defaultPalette = ["rg(114,229,239)", "rgb(255,246,209)", "rgb(255,188,156)", "rgb(247,220,96)", "rgb(122,176,238)",
                             "rgb(209,150,226)", "rgb(127,235,144)", "rgb(252,188,189)", "rgb(247,175,81)",];
-                        let colorPalette = Cast(cvd.colorPalette, listSpec("string"));
+                        const colorPalette = Cast(cvd.colorPalette, listSpec("string"));
                         if (!colorPalette) cvd.colorPalette = new List<string>(defaultPalette);
                     }
-                    let cp = Cast(cvd.colorPalette, listSpec("string")) as string[];
+                    const cp = Cast(cvd.colorPalette, listSpec("string")) as string[];
                     let closest = 0;
                     let dist = 10000000;
-                    let ccol = Utils.fromRGBAstr(StrCast(targetDoc.backgroundColor));
+                    const ccol = Utils.fromRGBAstr(StrCast(targetDoc.backgroundColor));
                     for (let i = 0; i < cp.length; i++) {
-                        let cpcol = Utils.fromRGBAstr(cp[i]);
-                        let d = Math.sqrt((ccol.r - cpcol.r) * (ccol.r - cpcol.r) + (ccol.b - cpcol.b) * (ccol.b - cpcol.b) + (ccol.g - cpcol.g) * (ccol.g - cpcol.g));
+                        const cpcol = Utils.fromRGBAstr(cp[i]);
+                        const d = Math.sqrt((ccol.r - cpcol.r) * (ccol.r - cpcol.r) + (ccol.b - cpcol.b) * (ccol.b - cpcol.b) + (ccol.g - cpcol.g) * (ccol.g - cpcol.g));
                         if (d < dist) {
                             dist = d;
                             closest = i;
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index a27f106e3..7cee84fc5 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -14,7 +14,7 @@ type InkDocument = makeInterface<[typeof documentSchema]>;
 const InkDocument = makeInterface(documentSchema);
 
 export function CreatePolyline(points: { x: number, y: number }[], left: number, top: number, color?: string, width?: number) {
-    let pts = points.reduce((acc: string, pt: { x: number, y: number }) => acc + `${pt.x - left},${pt.y - top} `, "");
+    const pts = points.reduce((acc: string, pt: { x: number, y: number }) => acc + `${pt.x - left},${pt.y - top} `, "");
     return (
         <polyline
             points={pts}
@@ -35,18 +35,18 @@ export class InkingStroke extends DocExtendableComponent<FieldViewProps, InkDocu
     @computed get PanelHeight() { return this.props.PanelHeight(); }
 
     render() {
-        let data: InkData = Cast(this.Document.data, InkField)?.inkData ?? [];
-        let xs = data.map(p => p.x);
-        let ys = data.map(p => p.y);
-        let left = Math.min(...xs);
-        let top = Math.min(...ys);
-        let right = Math.max(...xs);
-        let bottom = Math.max(...ys);
-        let points = CreatePolyline(data, 0, 0, this.Document.color, this.Document.strokeWidth);
-        let width = right - left;
-        let height = bottom - top;
-        let scaleX = this.PanelWidth / width;
-        let scaleY = this.PanelHeight / height;
+        const data: InkData = Cast(this.Document.data, InkField)?.inkData ?? [];
+        const xs = data.map(p => p.x);
+        const ys = data.map(p => p.y);
+        const left = Math.min(...xs);
+        const top = Math.min(...ys);
+        const right = Math.max(...xs);
+        const bottom = Math.max(...ys);
+        const points = CreatePolyline(data, 0, 0, this.Document.color, this.Document.strokeWidth);
+        const width = right - left;
+        const height = bottom - top;
+        const scaleX = this.PanelWidth / width;
+        const scaleY = this.PanelHeight / height;
         return (
             <svg width={width} height={height} style={{
                 transformOrigin: "top left",
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 291781da1..bd20cbcf1 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -60,10 +60,10 @@ export class MainView extends React.Component {
     public isPointerDown = false;
 
     componentWillMount() {
-        var tag = document.createElement('script');
+        const tag = document.createElement('script');
 
         tag.src = "https://www.youtube.com/iframe_api";
-        var firstScriptTag = document.getElementsByTagName('script')[0];
+        const firstScriptTag = document.getElementsByTagName('script')[0];
         firstScriptTag.parentNode!.insertBefore(tag, firstScriptTag);
         window.removeEventListener("keydown", KeyManager.Instance.handle);
         window.addEventListener("keydown", KeyManager.Instance.handle);
@@ -82,9 +82,9 @@ export class MainView extends React.Component {
         // causes errors to be generated when modifying an observable outside of an action
         configure({ enforceActions: "observed" });
         if (window.location.pathname !== "/home") {
-            let pathname = window.location.pathname.substr(1).split("/");
+            const pathname = window.location.pathname.substr(1).split("/");
             if (pathname.length > 1) {
-                let type = pathname[0];
+                const type = pathname[0];
                 if (type === "doc") {
                     CurrentUserUtils.MainDocId = pathname[1];
                     if (!this.userDoc) {
@@ -158,7 +158,7 @@ export class MainView extends React.Component {
 
     initAuthenticationRouters = async () => {
         // Load the user's active workspace, or create a new one if initial session after signup
-        let received = CurrentUserUtils.MainDocId;
+        const received = CurrentUserUtils.MainDocId;
         if (received && !this.userDoc) {
             reaction(
                 () => CurrentUserUtils.GuestTarget,
@@ -175,7 +175,7 @@ export class MainView extends React.Component {
                     }),
                 );
             }
-            let doc = this.userDoc && await Cast(this.userDoc.activeWorkspace, Doc);
+            const doc = this.userDoc && await Cast(this.userDoc.activeWorkspace, Doc);
             if (doc) {
                 this.openWorkspace(doc);
             } else {
@@ -186,9 +186,9 @@ export class MainView extends React.Component {
 
     @action
     createNewWorkspace = async (id?: string) => {
-        let workspaces = Cast(this.userDoc.workspaces, Doc) as Doc;
-        let workspaceCount = DocListCast(workspaces.data).length + 1;
-        let freeformOptions: DocumentOptions = {
+        const workspaces = Cast(this.userDoc.workspaces, Doc) as Doc;
+        const workspaceCount = DocListCast(workspaces.data).length + 1;
+        const freeformOptions: DocumentOptions = {
             x: 0,
             y: 400,
             width: this._panelWidth * .7,
@@ -196,10 +196,10 @@ export class MainView extends React.Component {
             title: "Collection " + workspaceCount,
             backgroundColor: "white"
         };
-        let freeformDoc = CurrentUserUtils.GuestTarget || Docs.Create.FreeformDocument([], freeformOptions);
+        const freeformDoc = CurrentUserUtils.GuestTarget || Docs.Create.FreeformDocument([], freeformOptions);
         Doc.AddDocToList(Doc.GetProto(CurrentUserUtils.UserDocument.documents as Doc), "data", freeformDoc);
-        var dockingLayout = { content: [{ type: 'row', content: [CollectionDockingView.makeDocumentConfig(freeformDoc, freeformDoc, 600)] }] };
-        let mainDoc = Docs.Create.DockDocument([freeformDoc], JSON.stringify(dockingLayout), { title: `Workspace ${workspaceCount}` }, id);
+        const dockingLayout = { content: [{ type: 'row', content: [CollectionDockingView.makeDocumentConfig(freeformDoc, freeformDoc, 600)] }] };
+        const mainDoc = Docs.Create.DockDocument([freeformDoc], JSON.stringify(dockingLayout), { title: `Workspace ${workspaceCount}` }, id);
         Doc.AddDocToList(workspaces, "data", mainDoc);
         // bcz: strangely, we need a timeout to prevent exceptions/issues initializing GoldenLayout (the rendering engine for Main Container)
         setTimeout(() => this.openWorkspace(mainDoc), 0);
@@ -213,7 +213,7 @@ export class MainView extends React.Component {
             !("presentationView" in doc) && (doc.presentationView = new List<Doc>([Docs.Create.TreeDocument([], { title: "Presentation" })]));
             this.userDoc ? (this.userDoc.activeWorkspace = doc) : (CurrentUserUtils.GuestWorkspace = doc);
         }
-        let state = this._urlState;
+        const state = this._urlState;
         if (state.sharing === true && !this.userDoc) {
             DocServer.Control.makeReadOnly();
         } else {
@@ -263,8 +263,8 @@ export class MainView extends React.Component {
 
     @computed get dockingContent() {
         const mainContainer = this.mainContainer;
-        let flyoutWidth = this.flyoutWidth; // bcz: need to be here because Measure messes with observables.
-        let flyoutTranslate = this._flyoutTranslate;
+        const flyoutWidth = this.flyoutWidth; // bcz: need to be here because Measure messes with observables.
+        const flyoutTranslate = this._flyoutTranslate;
         return <Measure offset onResize={this.onResize}>
             {({ measureRef }) =>
                 <div ref={measureRef} id="mainContent-div" style={{ width: `calc(100% - ${flyoutTranslate ? flyoutWidth : 0}px`, transform: `translate(${flyoutTranslate ? flyoutWidth : 0}px, 0px)` }} onDrop={this.onDrop}>
@@ -352,11 +352,11 @@ export class MainView extends React.Component {
     mainContainerXf = () => new Transform(0, -this._buttonBarHeight, 1);
 
     @computed get flyout() {
-        let sidebarContent = this.userDoc && this.userDoc.sidebarContainer;
+        const sidebarContent = this.userDoc && this.userDoc.sidebarContainer;
         if (!(sidebarContent instanceof Doc)) {
             return (null);
         }
-        let sidebarButtonsDoc = Cast(CurrentUserUtils.UserDocument.sidebarButtons, Doc) as Doc;
+        const sidebarButtonsDoc = Cast(CurrentUserUtils.UserDocument.sidebarButtons, Doc) as Doc;
         sidebarButtonsDoc.columnWidth = this.flyoutWidth / 3 - 30;
         return <div className="mainView-flyoutContainer" >
             <div className="mainView-tabButtons" style={{ height: `${this._buttonBarHeight}px` }}>
@@ -463,7 +463,7 @@ export class MainView extends React.Component {
 
     buttonBarXf = () => {
         if (!this._docBtnRef.current) return Transform.Identity();
-        let { scale, translateX, translateY } = Utils.GetScreenTransform(this._docBtnRef.current);
+        const { scale, translateX, translateY } = Utils.GetScreenTransform(this._docBtnRef.current);
         return new Transform(-translateX, -translateY, 1 / scale);
     }
     @computed get docButtons() {
diff --git a/src/client/views/MainViewModal.tsx b/src/client/views/MainViewModal.tsx
index 221a0260a..9198fe3e3 100644
--- a/src/client/views/MainViewModal.tsx
+++ b/src/client/views/MainViewModal.tsx
@@ -14,9 +14,9 @@ export interface MainViewOverlayProps {
 export default class MainViewModal extends React.Component<MainViewOverlayProps> {
 
     render() {
-        let p = this.props;
-        let dialogueOpacity = p.dialogueBoxDisplayedOpacity || 1;
-        let overlayOpacity = p.overlayDisplayedOpacity || 0.4;
+        const p = this.props;
+        const dialogueOpacity = p.dialogueBoxDisplayedOpacity || 1;
+        const overlayOpacity = p.overlayDisplayedOpacity || 0.4;
         return !p.isDisplayed ? (null) : (
             <div style={{ pointerEvents: p.isDisplayed ? p.interactive ? "all" : "none" : "none" }}>
                 <div
diff --git a/src/client/views/MetadataEntryMenu.tsx b/src/client/views/MetadataEntryMenu.tsx
index 41453f8b2..8859c14cb 100644
--- a/src/client/views/MetadataEntryMenu.tsx
+++ b/src/client/views/MetadataEntryMenu.tsx
@@ -99,8 +99,8 @@ export class MetadataEntryMenu extends React.Component<MetadataEntryProps>{
             } else {
                 let childSuccess = true;
                 if (this._addChildren) {
-                    for (let document of doc) {
-                        let collectionChildren = await DocListCastAsync(document.data);
+                    for (const document of doc) {
+                        const collectionChildren = await DocListCastAsync(document.data);
                         if (collectionChildren) {
                             childSuccess = collectionChildren.every(c => KeyValueBox.ApplyKVPScript(c, this._currentKey, script));
                         }
diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx
index 9869e24d1..32ada293c 100644
--- a/src/client/views/OverlayView.tsx
+++ b/src/client/views/OverlayView.tsx
@@ -148,7 +148,7 @@ export class OverlayView extends React.Component {
         return CurrentUserUtils.UserDocument.overlays instanceof Doc && DocListCast(CurrentUserUtils.UserDocument.overlays.data).map(d => {
             d.inOverlay = true;
             let offsetx = 0, offsety = 0;
-            let onPointerMove = action((e: PointerEvent) => {
+            const onPointerMove = action((e: PointerEvent) => {
                 if (e.buttons === 1) {
                     d.x = e.clientX + offsetx;
                     d.y = e.clientY + offsety;
@@ -156,14 +156,14 @@ export class OverlayView extends React.Component {
                     e.preventDefault();
                 }
             });
-            let onPointerUp = action((e: PointerEvent) => {
+            const onPointerUp = action((e: PointerEvent) => {
                 document.removeEventListener("pointermove", onPointerMove);
                 document.removeEventListener("pointerup", onPointerUp);
                 e.stopPropagation();
                 e.preventDefault();
             });
 
-            let onPointerDown = (e: React.PointerEvent) => {
+            const onPointerDown = (e: React.PointerEvent) => {
                 offsetx = NumCast(d.x) - e.clientX;
                 offsety = NumCast(d.y) - e.clientY;
                 e.stopPropagation();
diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx
index 136a272ab..fd0287b6c 100644
--- a/src/client/views/PreviewCursor.tsx
+++ b/src/client/views/PreviewCursor.tsx
@@ -26,7 +26,7 @@ export class PreviewCursor extends React.Component<{}> {
     paste = (e: ClipboardEvent) => {
         if (PreviewCursor.Visible) {
             if (e.clipboardData) {
-                let newPoint = PreviewCursor._getTransform().transformPoint(PreviewCursor._clickPoint[0], PreviewCursor._clickPoint[1]);
+                const newPoint = PreviewCursor._getTransform().transformPoint(PreviewCursor._clickPoint[0], PreviewCursor._clickPoint[1]);
                 runInAction(() => { PreviewCursor.Visible = false; });
 
 
@@ -44,7 +44,7 @@ export class PreviewCursor extends React.Component<{}> {
                     }
 
                     // tests for URL and makes web document
-                    let re: any = /^https?:\/\//g;
+                    const re: any = /^https?:\/\//g;
                     if (re.test(e.clipboardData.getData("text/plain"))) {
                         const url = e.clipboardData.getData("text/plain");
                         PreviewCursor._addDocument(Docs.Create.WebDocument(url, {
@@ -56,7 +56,7 @@ export class PreviewCursor extends React.Component<{}> {
                     }
 
                     // creates text document
-                    let newBox = Docs.Create.TextDocument({
+                    const newBox = Docs.Create.TextDocument({
                         width: 200, height: 100,
                         x: newPoint[0],
                         y: newPoint[1],
@@ -69,10 +69,10 @@ export class PreviewCursor extends React.Component<{}> {
                 }
                 //pasting in images
                 if (e.clipboardData.getData("text/html") !== "" && e.clipboardData.getData("text/html").includes("<img src=")) {
-                    let re: any = /<img src="(.*?)"/g;
-                    let arr: any[] = re.exec(e.clipboardData.getData("text/html"));
+                    const re: any = /<img src="(.*?)"/g;
+                    const arr: any[] = re.exec(e.clipboardData.getData("text/html"));
 
-                    let img: Doc = Docs.Create.ImageDocument(
+                    const img: Doc = Docs.Create.ImageDocument(
                         arr[1], {
                         width: 300, title: arr[1],
                         x: newPoint[0],
diff --git a/src/client/views/ScriptBox.tsx b/src/client/views/ScriptBox.tsx
index 8ef9f3be6..65d96f364 100644
--- a/src/client/views/ScriptBox.tsx
+++ b/src/client/views/ScriptBox.tsx
@@ -59,7 +59,7 @@ export class ScriptBox extends React.Component<ScriptBoxProps> {
             onFocus = this.onFocus;
             onBlur = this.onBlur;
         }
-        let params = <EditableView
+        const params = <EditableView
             contents={""}
             display={"block"}
             maxHeight={72}
@@ -96,9 +96,9 @@ export class ScriptBox extends React.Component<ScriptBoxProps> {
             }
         }
         // tslint:disable-next-line: no-unnecessary-callback-wrapper
-        let params: string[] = [];
-        let setParams = (p: string[]) => params.splice(0, params.length, ...p);
-        let scriptingBox = <ScriptBox initialText={originalText} setParams={setParams} onCancel={overlayDisposer} onSave={(text, onError) => {
+        const params: string[] = [];
+        const setParams = (p: string[]) => params.splice(0, params.length, ...p);
+        const scriptingBox = <ScriptBox initialText={originalText} setParams={setParams} onCancel={overlayDisposer} onSave={(text, onError) => {
             if (prewrapper) {
                 text = prewrapper + text + (postwrapper ? postwrapper : "");
             }
diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx
index c65b338b4..e6116ca09 100644
--- a/src/client/views/TemplateMenu.tsx
+++ b/src/client/views/TemplateMenu.tsx
@@ -61,19 +61,19 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
 
     toggleFloat = (e: React.ChangeEvent<HTMLInputElement>): void => {
         SelectionManager.DeselectAll();
-        let topDocView = this.props.docs[0];
-        let topDoc = topDocView.props.Document;
-        let xf = topDocView.props.ScreenToLocalTransform();
-        let ex = e.target.clientLeft;
-        let ey = e.target.clientTop;
+        const topDocView = this.props.docs[0];
+        const topDoc = topDocView.props.Document;
+        const xf = topDocView.props.ScreenToLocalTransform();
+        const ex = e.target.clientLeft;
+        const ey = e.target.clientTop;
         undoBatch(action(() => topDoc.z = topDoc.z ? 0 : 1))();
         if (e.target.checked) {
             setTimeout(() => {
-                let newDocView = DocumentManager.Instance.getDocumentView(topDoc);
+                const newDocView = DocumentManager.Instance.getDocumentView(topDoc);
                 if (newDocView) {
-                    let de = new DragManager.DocumentDragData([topDoc]);
+                    const de = new DragManager.DocumentDragData([topDoc]);
                     de.moveDocument = topDocView.props.moveDocument;
-                    let xf = newDocView.ContentDiv!.getBoundingClientRect();
+                    const xf = newDocView.ContentDiv!.getBoundingClientRect();
                     DragManager.StartDocumentDrag([newDocView.ContentDiv!], de, ex, ey, {
                         offsetX: (ex - xf.left), offsetY: (ey - xf.top),
                         handlers: { dragComplete: () => { }, },
@@ -82,9 +82,9 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
                 }
             }, 10);
         } else if (topDocView.props.ContainingCollectionView) {
-            let collView = topDocView.props.ContainingCollectionView;
-            let [sx, sy] = xf.inverse().transformPoint(0, 0);
-            let [x, y] = collView.props.ScreenToLocalTransform().transformPoint(sx, sy);
+            const collView = topDocView.props.ContainingCollectionView;
+            const [sx, sy] = xf.inverse().transformPoint(0, 0);
+            const [x, y] = collView.props.ScreenToLocalTransform().transformPoint(sx, sy);
             topDoc.x = x;
             topDoc.y = y;
         }
@@ -122,7 +122,7 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
     @action
     toggleChrome = (): void => {
         this.props.docs.map(dv => {
-            let layout = Doc.Layout(dv.Document);
+            const layout = Doc.Layout(dv.Document);
             layout.chromeStatus = (layout.chromeStatus !== "disabled" ? "disabled" : "enabled");
         });
     }
@@ -147,8 +147,8 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
             document.removeEventListener("pointermove", this.onAliasButtonMoved);
             document.removeEventListener("pointerup", this.onAliasButtonUp);
 
-            let dragDocView = this.props.docs[0];
-            let dragData = new DragManager.DocumentDragData([dragDocView.props.Document]);
+            const dragDocView = this.props.docs[0];
+            const dragData = new DragManager.DocumentDragData([dragDocView.props.Document]);
             const [left, top] = dragDocView.props.ScreenToLocalTransform().inverse().transformPoint(0, 0);
             dragData.embedDoc = true;
             dragData.dropAction = "alias";
@@ -165,8 +165,8 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
     }
 
     render() {
-        let layout = Doc.Layout(this.props.docs[0].Document);
-        let templateMenu: Array<JSX.Element> = [];
+        const layout = Doc.Layout(this.props.docs[0].Document);
+        const templateMenu: Array<JSX.Element> = [];
         this.props.templates.forEach((checked, template) =>
             templateMenu.push(<TemplateToggle key={template.Name} template={template} checked={checked} toggle={this.toggleTemplate} />));
         templateMenu.push(<OtherToggle key={"float"} name={"Float"} checked={this.props.docs[0].Document.z ? true : false} toggle={this.toggleFloat} />);
diff --git a/src/client/views/Touchable.tsx b/src/client/views/Touchable.tsx
index 0056a1d96..183d3e4e8 100644
--- a/src/client/views/Touchable.tsx
+++ b/src/client/views/Touchable.tsx
@@ -17,7 +17,7 @@ export abstract class Touchable<T = {}> extends React.Component<T> {
     @action
     protected onTouchStart = (e: React.TouchEvent): void => {
         for (let i = 0; i < e.targetTouches.length; i++) {
-            let pt: any = e.targetTouches.item(i);
+            const pt: any = e.targetTouches.item(i);
             // pen is also a touch, but with a radius of 0.5 (at least with the surface pens). i doubt anyone's fingers are 2 pixels wide,
             // and this seems to be the only way of differentiating pen and touch on touch events
             if (pt.radiusX > 2 && pt.radiusY > 2) {
@@ -59,7 +59,7 @@ export abstract class Touchable<T = {}> extends React.Component<T> {
         }
 
         for (let i = 0; i < e.targetTouches.length; i++) {
-            let pt = e.targetTouches.item(i);
+            const pt = e.targetTouches.item(i);
             if (pt) {
                 if (this.prevPoints.has(pt.identifier)) {
                     this.prevPoints.set(pt.identifier, pt);
@@ -78,7 +78,7 @@ export abstract class Touchable<T = {}> extends React.Component<T> {
 
         // remove all the touches associated with the event
         for (let i = 0; i < e.targetTouches.length; i++) {
-            let pt = e.targetTouches.item(i);
+            const pt = e.targetTouches.item(i);
             if (pt) {
                 if (this.prevPoints.has(pt.identifier)) {
                     this.prevPoints.delete(pt.identifier);
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 3040e74b0..77e6e1c93 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -96,13 +96,13 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
     @undoBatch
     @action
     public OpenFullScreen(docView: DocumentView) {
-        let document = Doc.MakeAlias(docView.props.Document);
-        let dataDoc = docView.props.DataDoc;
-        let newItemStackConfig = {
+        const document = Doc.MakeAlias(docView.props.Document);
+        const dataDoc = docView.props.DataDoc;
+        const newItemStackConfig = {
             type: 'stack',
             content: [CollectionDockingView.makeDocumentConfig(document, dataDoc)]
         };
-        var docconfig = this._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, this._goldenLayout);
+        const docconfig = this._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, this._goldenLayout);
         this._goldenLayout.root.contentItems[0].addChild(docconfig);
         docconfig.callDownwards('_$init');
         this._goldenLayout._$maximiseItem(docconfig);
@@ -113,7 +113,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
     }
 
     public CloseFullScreen = () => {
-        let target = this._goldenLayout._maximisedItem;
+        const target = this._goldenLayout._maximisedItem;
         if (target !== null && this._maximizedSrc) {
             this._goldenLayout._maximisedItem.remove();
             SelectionManager.SelectDoc(this._maximizedSrc, false);
@@ -130,7 +130,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
     @action
     public static CloseRightSplit(document: Doc): boolean {
         if (!CollectionDockingView.Instance) return false;
-        let instance = CollectionDockingView.Instance;
+        const instance = CollectionDockingView.Instance;
         let retVal = false;
         if (instance._goldenLayout.root.contentItems[0].isRow) {
             retVal = Array.from(instance._goldenLayout.root.contentItems[0].contentItems).some((child: any) => {
@@ -176,21 +176,21 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
     @action
     public static AddRightSplit(document: Doc, dataDoc: Doc | undefined, minimize: boolean = false) {
         if (!CollectionDockingView.Instance) return false;
-        let instance = CollectionDockingView.Instance;
-        let newItemStackConfig = {
+        const instance = CollectionDockingView.Instance;
+        const newItemStackConfig = {
             type: 'stack',
             content: [CollectionDockingView.makeDocumentConfig(document, dataDoc)]
         };
 
-        var newContentItem = instance._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, instance._goldenLayout);
+        const newContentItem = instance._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, instance._goldenLayout);
 
         if (instance._goldenLayout.root.contentItems.length === 0) {
             instance._goldenLayout.root.addChild(newContentItem);
         } else if (instance._goldenLayout.root.contentItems[0].isRow) {
             instance._goldenLayout.root.contentItems[0].addChild(newContentItem);
         } else {
-            var collayout = instance._goldenLayout.root.contentItems[0];
-            var newRow = collayout.layoutManager.createContentItem({ type: "row" }, instance._goldenLayout);
+            const collayout = instance._goldenLayout.root.contentItems[0];
+            const newRow = collayout.layoutManager.createContentItem({ type: "row" }, instance._goldenLayout);
             collayout.parent.replaceChild(collayout, newRow);
 
             newRow.addChild(newContentItem, undefined, true);
@@ -213,7 +213,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
     @action
     public AddTab = (stack: any, document: Doc, dataDocument: Doc | undefined) => {
         Doc.GetProto(document).lastOpened = new DateField;
-        let docContentConfig = CollectionDockingView.makeDocumentConfig(document, dataDocument);
+        const docContentConfig = CollectionDockingView.makeDocumentConfig(document, dataDocument);
         if (stack === undefined) {
             let stack: any = this._goldenLayout.root;
             while (!stack.isStack) {
@@ -236,7 +236,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
     }
 
     setupGoldenLayout() {
-        var config = StrCast(this.props.Document.dockingConfig);
+        const config = StrCast(this.props.Document.dockingConfig);
         if (config) {
             if (!this._goldenLayout) {
                 runInAction(() => this._goldenLayout = new GoldenLayout(JSON.parse(config)));
@@ -280,7 +280,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
                         // Because this is in a set timeout, if this component unmounts right after mounting,
                         // we will leak a GoldenLayout, because we try to destroy it before we ever create it
                         setTimeout(() => this.setupGoldenLayout(), 1);
-                        let userDoc = CurrentUserUtils.UserDocument;
+                        const userDoc = CurrentUserUtils.UserDocument;
                         userDoc && DocListCast((userDoc.workspaces as Doc).data).map(d => d.workspaceBrush = false);
                         this.props.Document.workspaceBrush = true;
                     }
@@ -311,7 +311,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
     }
     @action
     onResize = (event: any) => {
-        var cur = this._containerRef.current;
+        const cur = this._containerRef.current;
 
         // bcz: since GoldenLayout isn't a React component itself, we need to notify it to resize when its document container's size has changed
         this._goldenLayout && this._goldenLayout.updateSize(cur!.getBoundingClientRect().width, cur!.getBoundingClientRect().height);
@@ -330,19 +330,19 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
     @action
     onPointerDown = (e: React.PointerEvent): void => {
         this._isPointerDown = true;
-        let onPointerUp = action(() => {
+        const onPointerUp = action(() => {
             window.removeEventListener("pointerup", onPointerUp);
             this._isPointerDown = false;
         });
         window.addEventListener("pointerup", onPointerUp);
-        var className = (e.target as any).className;
+        const className = (e.target as any).className;
         if (className === "messageCounter") {
             e.stopPropagation();
             e.preventDefault();
-            let x = e.clientX;
-            let y = e.clientY;
-            let docid = (e.target as any).DashDocId;
-            let tab = (e.target as any).parentElement as HTMLElement;
+            const x = e.clientX;
+            const y = e.clientY;
+            const docid = (e.target as any).DashDocId;
+            const tab = (e.target as any).parentElement as HTMLElement;
             DocServer.GetRefField(docid).then(action(async (sourceDoc: Opt<Field>) =>
                 (sourceDoc instanceof Doc) && DragLinksAsDocuments(tab, x, y, sourceDoc)));
         }
@@ -352,18 +352,18 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
     }
 
     updateDataField = async (json: string) => {
-        let matches = json.match(/\"documentId\":\"[a-z0-9-]+\"/g);
-        let docids = matches?.map(m => m.replace("\"documentId\":\"", "").replace("\"", ""));
+        const matches = json.match(/\"documentId\":\"[a-z0-9-]+\"/g);
+        const docids = matches?.map(m => m.replace("\"documentId\":\"", "").replace("\"", ""));
 
         if (docids) {
-            let docs = (await Promise.all(docids.map(id => DocServer.GetRefField(id)))).filter(f => f).map(f => f as Doc);
+            const docs = (await Promise.all(docids.map(id => DocServer.GetRefField(id)))).filter(f => f).map(f => f as Doc);
             Doc.GetProto(this.props.Document)[this.props.fieldKey] = new List<Doc>(docs);
         }
     }
 
     @undoBatch
     stateChanged = () => {
-        var json = JSON.stringify(this._goldenLayout.toConfig());
+        const json = JSON.stringify(this._goldenLayout.toConfig());
         this.props.Document.dockingConfig = json;
         this.updateDataField(json);
 
@@ -380,7 +380,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
     }
 
     htmlToElement(html: string) {
-        var template = document.createElement('template');
+        const template = document.createElement('template');
         html = html.trim(); // Never return a text node of whitespace as the result
         template.innerHTML = html;
         return template.content.firstChild;
@@ -392,24 +392,24 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
                 tab.contentItem.parent.config.fixed = true;
             }
 
-            let doc = await DocServer.GetRefField(tab.contentItem.config.props.documentId) as Doc;
-            let dataDoc = await DocServer.GetRefField(tab.contentItem.config.props.dataDocumentId) as Doc;
+            const doc = await DocServer.GetRefField(tab.contentItem.config.props.documentId) as Doc;
+            const dataDoc = await DocServer.GetRefField(tab.contentItem.config.props.dataDocumentId) as Doc;
             if (doc instanceof Doc) {
-                let dragSpan = document.createElement("span");
+                const dragSpan = document.createElement("span");
                 dragSpan.style.position = "relative";
                 dragSpan.style.bottom = "6px";
                 dragSpan.style.paddingLeft = "4px";
                 dragSpan.style.paddingRight = "2px";
-                let gearSpan = document.createElement("span");
+                const gearSpan = document.createElement("span");
                 gearSpan.style.position = "relative";
                 gearSpan.style.paddingLeft = "0px";
                 gearSpan.style.paddingRight = "12px";
-                let upDiv = document.createElement("span");
+                const upDiv = document.createElement("span");
                 const stack = tab.contentItem.parent;
                 // shifts the focus to this tab when another tab is dragged over it
                 tab.element[0].onmouseenter = (e: any) => {
                     if (!this._isPointerDown || !SelectionManager.GetIsDragging()) return;
-                    var activeContentItem = tab.header.parent.getActiveContentItem();
+                    const activeContentItem = tab.header.parent.getActiveContentItem();
                     if (tab.contentItem !== activeContentItem) {
                         tab.header.parent.setActiveContentItem(tab.contentItem);
                     }
@@ -447,12 +447,12 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
         tab.closeElement.off('click') //unbind the current click handler
             .click(async function () {
                 tab.reactionDisposer && tab.reactionDisposer();
-                let doc = await DocServer.GetRefField(tab.contentItem.config.props.documentId);
+                const doc = await DocServer.GetRefField(tab.contentItem.config.props.documentId);
                 if (doc instanceof Doc) {
-                    let theDoc = doc;
+                    const theDoc = doc;
                     CollectionDockingView.Instance._removedDocs.push(theDoc);
 
-                    let userDoc = CurrentUserUtils.UserDocument;
+                    const userDoc = CurrentUserUtils.UserDocument;
                     let recent: Doc | undefined;
                     if (userDoc && (recent = await Cast(CurrentUserUtils.UserDocument.recentlyClosed, Doc))) {
                         Doc.AddDocToList(recent, "data", doc, undefined, true, true);
@@ -489,13 +489,13 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
 
                 stack.remove();
                 stack.contentItems.forEach(async (contentItem: any) => {
-                    let doc = await DocServer.GetRefField(contentItem.config.props.documentId);
+                    const doc = await DocServer.GetRefField(contentItem.config.props.documentId);
                     if (doc instanceof Doc) {
                         let recent: Doc | undefined;
                         if (CurrentUserUtils.UserDocument && (recent = await Cast(CurrentUserUtils.UserDocument.recentlyClosed, Doc))) {
                             Doc.AddDocToList(recent, "data", doc, undefined, true, true);
                         }
-                        let theDoc = doc;
+                        const theDoc = doc;
                         CollectionDockingView.Instance._removedDocs.push(theDoc);
                     }
                 });
@@ -505,7 +505,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
             .off('click') //unbind the current click handler
             .click(action(function () {
                 stack.config.fixed = !stack.config.fixed;
-                // var url = Utils.prepend("/doc/" + stack.contentItems[0].tab.contentItem.config.props.documentId);
+                // const url = Utils.prepend("/doc/" + stack.contentItems[0].tab.contentItem.config.props.documentId);
                 // let win = window.open(url, stack.contentItems[0].tab.title, "width=300,height=400");
             }));
     }
@@ -563,9 +563,9 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
     @action
     public PinDoc(doc: Doc) {
         //add this new doc to props.Document
-        let curPres = Cast(CurrentUserUtils.UserDocument.curPresentation, Doc) as Doc;
+        const curPres = Cast(CurrentUserUtils.UserDocument.curPresentation, Doc) as Doc;
         if (curPres) {
-            let pinDoc = Docs.Create.PresElementBoxDocument({ backgroundColor: "transparent" });
+            const pinDoc = Docs.Create.PresElementBoxDocument({ backgroundColor: "transparent" });
             Doc.GetProto(pinDoc).presentationTargetDoc = doc;
             Doc.GetProto(pinDoc).title = ComputedField.MakeFunction('(this.presentationTargetDoc instanceof Doc) && this.presentationTargetDoc.title.toString()');
             const data = Cast(curPres.data, listSpec(Doc));
@@ -581,8 +581,8 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
     }
 
     componentDidMount() {
-        let observer = new _global.ResizeObserver(action((entries: any) => {
-            for (let entry of entries) {
+        const observer = new _global.ResizeObserver(action((entries: any) => {
+            for (const entry of entries) {
                 this._panelWidth = entry.contentRect.width;
                 this._panelHeight = entry.contentRect.height;
             }
@@ -625,14 +625,14 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
         const nativeH = this.nativeHeight();
         const nativeW = this.nativeWidth();
         if (!nativeW || !nativeH) return 1;
-        let wscale = this.panelWidth() / nativeW;
+        const wscale = this.panelWidth() / nativeW;
         return wscale * nativeH > this._panelHeight ? this._panelHeight / nativeH : wscale;
     }
 
     ScreenToLocalTransform = () => {
         if (this._mainCont && this._mainCont.children) {
-            let { scale, translateX, translateY } = Utils.GetScreenTransform(this._mainCont.children[0].firstChild as HTMLElement);
-            scale = Utils.GetScreenTransform(this._mainCont).scale;
+            const { translateX, translateY } = Utils.GetScreenTransform(this._mainCont.children[0].firstChild as HTMLElement);
+            const scale = Utils.GetScreenTransform(this._mainCont).scale;
             return CollectionDockingView.Instance.props.ScreenToLocalTransform().translate(-translateX, -translateY).scale(1 / this.contentScaling() / scale);
         }
         return Transform.Identity();
@@ -657,7 +657,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
     @computed get docView() {
         if (!this._document) return (null);
         const document = this._document;
-        let resolvedDataDoc = document.layout instanceof Doc ? document : this._dataDoc;
+        const resolvedDataDoc = document.layout instanceof Doc ? document : this._dataDoc;
         return <DocumentView key={document[Id]}
             Document={document}
             DataDoc={resolvedDataDoc}
diff --git a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
index df4b00b3a..d697e721b 100644
--- a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
+++ b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
@@ -64,9 +64,9 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
         if (this._collapsed) {
             this.props.setDocHeight(this._heading, 20);
         } else {
-            let rawHeight = this._contRef.current!.getBoundingClientRect().height + 15; //+ 15 accounts for the group header
-            let transformScale = this.props.screenToLocalTransform().Scale;
-            let trueHeight = rawHeight * transformScale;
+            const rawHeight = this._contRef.current!.getBoundingClientRect().height + 15; //+ 15 accounts for the group header
+            const transformScale = this.props.screenToLocalTransform().Scale;
+            const trueHeight = rawHeight * transformScale;
             this.props.setDocHeight(this._heading, trueHeight);
         }
     }
@@ -77,8 +77,8 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
         if (de.data instanceof DragManager.DocumentDragData) {
             (this.props.parent.Document.dropConverter instanceof ScriptField) &&
                 this.props.parent.Document.dropConverter.script.run({ dragData: de.data });
-            let key = StrCast(this.props.parent.props.Document.sectionFilter);
-            let castedValue = this.getValue(this._heading);
+            const key = StrCast(this.props.parent.props.Document.sectionFilter);
+            const castedValue = this.getValue(this._heading);
             de.data.droppedDocuments.forEach(d => d[key] = castedValue);
             this.props.parent.drop(e, de);
             e.stopPropagation();
@@ -86,7 +86,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
     });
 
     getValue = (value: string): any => {
-        let parsed = parseInt(value);
+        const parsed = parseInt(value);
         if (!isNaN(parsed)) return parsed;
         if (value.toLowerCase().indexOf("true") > -1) return true;
         if (value.toLowerCase().indexOf("false") > -1) return false;
@@ -96,8 +96,8 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
     @action
     headingChanged = (value: string, shiftDown?: boolean) => {
         this._createAliasSelected = false;
-        let key = StrCast(this.props.parent.props.Document.sectionFilter);
-        let castedValue = this.getValue(value);
+        const key = StrCast(this.props.parent.props.Document.sectionFilter);
+        const castedValue = this.getValue(value);
         if (castedValue) {
             if (this.props.parent.sectionHeaders) {
                 if (this.props.parent.sectionHeaders.map(i => i.heading).indexOf(castedValue.toString()) > -1) {
@@ -135,18 +135,18 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
     @action
     addDocument = (value: string, shiftDown?: boolean) => {
         this._createAliasSelected = false;
-        let key = StrCast(this.props.parent.props.Document.sectionFilter);
-        let newDoc = Docs.Create.TextDocument({ height: 18, width: 200, title: value });
+        const key = StrCast(this.props.parent.props.Document.sectionFilter);
+        const newDoc = Docs.Create.TextDocument({ height: 18, width: 200, title: value });
         newDoc[key] = this.getValue(this.props.heading);
         return this.props.parent.props.addDocument(newDoc);
     }
 
     deleteRow = undoBatch(action(() => {
         this._createAliasSelected = false;
-        let key = StrCast(this.props.parent.props.Document.sectionFilter);
+        const key = StrCast(this.props.parent.props.Document.sectionFilter);
         this.props.docList.forEach(d => d[key] = undefined);
         if (this.props.parent.sectionHeaders && this.props.headingObject) {
-            let index = this.props.parent.sectionHeaders.indexOf(this.props.headingObject);
+            const index = this.props.parent.sectionHeaders.indexOf(this.props.headingObject);
             this.props.parent.sectionHeaders.splice(index, 1);
         }
     }));
@@ -162,18 +162,18 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
     }
 
     startDrag = (e: PointerEvent) => {
-        let [dx, dy] = this.props.screenToLocalTransform().transformDirection(e.clientX - this._startDragPosition.x, e.clientY - this._startDragPosition.y);
+        const [dx, dy] = this.props.screenToLocalTransform().transformDirection(e.clientX - this._startDragPosition.x, e.clientY - this._startDragPosition.y);
         if (Math.abs(dx) + Math.abs(dy) > this._sensitivity) {
-            let alias = Doc.MakeAlias(this.props.parent.props.Document);
-            let key = StrCast(this.props.parent.props.Document.sectionFilter);
+            const alias = Doc.MakeAlias(this.props.parent.props.Document);
+            const key = StrCast(this.props.parent.props.Document.sectionFilter);
             let value = this.getValue(this._heading);
             value = typeof value === "string" ? `"${value}"` : value;
-            let script = `return doc.${key} === ${value}`;
-            let compiled = CompileScript(script, { params: { doc: Doc.name } });
+            const script = `return doc.${key} === ${value}`;
+            const compiled = CompileScript(script, { params: { doc: Doc.name } });
             if (compiled.compiled) {
-                let scriptField = new ScriptField(compiled);
+                const scriptField = new ScriptField(compiled);
                 alias.viewSpecScript = scriptField;
-                let dragData = new DragManager.DocumentDragData([alias]);
+                const dragData = new DragManager.DocumentDragData([alias]);
                 DragManager.StartDocumentDrag([this._headerRef.current!], dragData, e.clientX, e.clientY);
             }
 
@@ -196,7 +196,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
         e.stopPropagation();
         e.preventDefault();
 
-        let [dx, dy] = this.props.screenToLocalTransform().transformDirection(e.clientX, e.clientY);
+        const [dx, dy] = this.props.screenToLocalTransform().transformDirection(e.clientX, e.clientY);
         this._startDragPosition = { x: dx, y: dy };
 
         if (this._createAliasSelected) {
@@ -209,17 +209,17 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
     }
 
     renderColorPicker = () => {
-        let selected = this.props.headingObject ? this.props.headingObject.color : "#f1efeb";
+        const selected = this.props.headingObject ? this.props.headingObject.color : "#f1efeb";
 
-        let pink = PastelSchemaPalette.get("pink2");
-        let purple = PastelSchemaPalette.get("purple4");
-        let blue = PastelSchemaPalette.get("bluegreen1");
-        let yellow = PastelSchemaPalette.get("yellow4");
-        let red = PastelSchemaPalette.get("red2");
-        let green = PastelSchemaPalette.get("bluegreen7");
-        let cyan = PastelSchemaPalette.get("bluegreen5");
-        let orange = PastelSchemaPalette.get("orange1");
-        let gray = "#f1efeb";
+        const pink = PastelSchemaPalette.get("pink2");
+        const purple = PastelSchemaPalette.get("purple4");
+        const blue = PastelSchemaPalette.get("bluegreen1");
+        const yellow = PastelSchemaPalette.get("yellow4");
+        const red = PastelSchemaPalette.get("red2");
+        const green = PastelSchemaPalette.get("bluegreen7");
+        const cyan = PastelSchemaPalette.get("bluegreen5");
+        const orange = PastelSchemaPalette.get("orange1");
+        const gray = "#f1efeb";
 
         return (
             <div className="collectionStackingView-colorPicker">
@@ -242,7 +242,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
     toggleVisibility = action(() => this._collapsed = !this._collapsed);
 
     renderMenu = () => {
-        let selected = this._createAliasSelected;
+        const selected = this._createAliasSelected;
         return (<div className="collectionStackingView-optionPicker">
             <div className="optionOptions">
                 <div className={"optionPicker" + (selected === true ? " active" : "")} onClick={this.toggleAlias}>Create Alias</div>
@@ -258,10 +258,10 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
     }
 
     @computed get contentLayout() {
-        let rows = Math.max(1, Math.min(this.props.docList.length, Math.floor((this.props.parent.props.PanelWidth() - 2 * this.props.parent.xMargin) / (this.props.parent.columnWidth + this.props.parent.gridGap))));
-        let style = this.props.parent; const collapsed = this._collapsed;
-        let chromeStatus = this.props.parent.props.Document.chromeStatus;
-        let newEditableViewProps = {
+        const rows = Math.max(1, Math.min(this.props.docList.length, Math.floor((this.props.parent.props.PanelWidth() - 2 * this.props.parent.xMargin) / (this.props.parent.columnWidth + this.props.parent.gridGap))));
+        const style = this.props.parent; const collapsed = this._collapsed;
+        const chromeStatus = this.props.parent.props.Document.chromeStatus;
+        const newEditableViewProps = {
             GetValue: () => "",
             SetValue: this.addDocument,
             contents: "+ NEW",
@@ -293,10 +293,10 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
     }
 
     @computed get headingView() {
-        let heading = this._heading;
-        let key = StrCast(this.props.parent.props.Document.sectionFilter);
-        let evContents = heading ? heading : this.props.type && this.props.type === "number" ? "0" : `NO ${key.toUpperCase()} VALUE`;
-        let headerEditableViewProps = {
+        const heading = this._heading;
+        const key = StrCast(this.props.parent.props.Document.sectionFilter);
+        const evContents = heading ? heading : this.props.type && this.props.type === "number" ? "0" : `NO ${key.toUpperCase()} VALUE`;
+        const headerEditableViewProps = {
             GetValue: () => evContents,
             SetValue: this.headingChanged,
             contents: evContents,
@@ -343,8 +343,8 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
     }
     render() {
         const background = this._background; //to account for observables in Measure
-        let contentlayout = this.contentLayout;
-        let headingview = this.headingView;
+        const contentlayout = this.contentLayout;
+        const headingview = this.headingView;
         return <Measure offset onResize={this.handleResize}>
             {({ measureRef }) => {
                 return <div ref={measureRef}>
diff --git a/src/client/views/collections/CollectionSchemaCells.tsx b/src/client/views/collections/CollectionSchemaCells.tsx
index 54a36f691..1700c14cf 100644
--- a/src/client/views/collections/CollectionSchemaCells.tsx
+++ b/src/client/views/collections/CollectionSchemaCells.tsx
@@ -1,7 +1,7 @@
 import React = require("react");
-import { action, computed, observable, trace, untracked, toJS } from "mobx";
+import { action, observable } from "mobx";
 import { observer } from "mobx-react";
-import ReactTable, { CellInfo, ComponentPropsGetterR, ReactTableDefaults, Column } from "react-table";
+import { CellInfo } from "react-table";
 import "react-table/react-table.css";
 import { emptyFunction, returnFalse, returnZero, returnOne } from "../../../Utils";
 import { Doc, DocListCast, DocListCastAsync, Field, Opt } from "../../../new_fields/Doc";
@@ -9,7 +9,7 @@ import { Id } from "../../../new_fields/FieldSymbols";
 import { SetupDrag, DragManager } from "../../util/DragManager";
 import { CompileScript } from "../../util/Scripting";
 import { Transform } from "../../util/Transform";
-import { COLLECTION_BORDER_WIDTH, MAX_ROW_HEIGHT } from '../globalCssVariables.scss';
+import { MAX_ROW_HEIGHT } from '../globalCssVariables.scss';
 import '../DocumentDecorations.scss';
 import { EditableView } from "../EditableView";
 import { FieldView, FieldViewProps } from "../nodes/FieldView";
@@ -89,8 +89,8 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
         // this._isEditing = true;
         // this.props.setIsEditing(true);
 
-        let field = this.props.rowProps.original[this.props.rowProps.column.id!];
-        let doc = FieldValue(Cast(field, Doc));
+        const field = this.props.rowProps.original[this.props.rowProps.column.id!];
+        const doc = FieldValue(Cast(field, Doc));
         if (typeof field === "object" && doc) this.props.setPreviewDoc(doc);
     }
 
@@ -106,12 +106,12 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
 
     private drop = (e: Event, de: DragManager.DropEvent) => {
         if (de.data instanceof DragManager.DocumentDragData) {
-            let fieldKey = this.props.rowProps.column.id as string;
+            const fieldKey = this.props.rowProps.column.id as string;
             if (de.data.draggedDocuments.length === 1) {
                 this._document[fieldKey] = de.data.draggedDocuments[0];
             }
             else {
-                let coll = Docs.Create.SchemaDocument([new SchemaHeaderField("title", "#f1efeb")], de.data.draggedDocuments, {});
+                const coll = Docs.Create.SchemaDocument([new SchemaHeaderField("title", "#f1efeb")], de.data.draggedDocuments, {});
                 this._document[fieldKey] = coll;
             }
             e.stopPropagation();
@@ -138,9 +138,9 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
     // }
 
     renderCellWithType(type: string | undefined) {
-        let dragRef: React.RefObject<HTMLDivElement> = React.createRef();
+        const dragRef: React.RefObject<HTMLDivElement> = React.createRef();
 
-        let props: FieldViewProps = {
+        const props: FieldViewProps = {
             Document: this.props.rowProps.original,
             DataDoc: this.props.rowProps.original,
             fieldKey: this.props.rowProps.column.id as string,
@@ -161,23 +161,23 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
             ContentScaling: returnOne
         };
 
-        let field = props.Document[props.fieldKey];
-        let doc = FieldValue(Cast(field, Doc));
-        let fieldIsDoc = (type === "document" && typeof field === "object") || (typeof field === "object" && doc);
+        const field = props.Document[props.fieldKey];
+        const doc = FieldValue(Cast(field, Doc));
+        const fieldIsDoc = (type === "document" && typeof field === "object") || (typeof field === "object" && doc);
 
-        let onItemDown = (e: React.PointerEvent) => {
+        const onItemDown = (e: React.PointerEvent) => {
             if (fieldIsDoc) {
                 SetupDrag(this._focusRef, () => this._document[props.fieldKey] instanceof Doc ? this._document[props.fieldKey] : this._document,
                     this._document[props.fieldKey] instanceof Doc ? (doc: Doc, target: Doc, addDoc: (newDoc: Doc) => any) => addDoc(doc) : this.props.moveDocument,
                     this._document[props.fieldKey] instanceof Doc ? "alias" : this.props.Document.schemaDoc ? "copy" : undefined)(e);
             }
         };
-        let onPointerEnter = (e: React.PointerEvent): void => {
+        const onPointerEnter = (e: React.PointerEvent): void => {
             if (e.buttons === 1 && SelectionManager.GetIsDragging() && (type === "document" || type === undefined)) {
                 dragRef.current!.className = "collectionSchemaView-cellContainer doc-drag-over";
             }
         };
-        let onPointerLeave = (e: React.PointerEvent): void => {
+        const onPointerLeave = (e: React.PointerEvent): void => {
             dragRef.current!.className = "collectionSchemaView-cellContainer";
         };
 
@@ -187,7 +187,7 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
         if (type === "string") contents = typeof field === "string" ? (StrCast(field) === "" ? "--" : StrCast(field)) : "--" + typeof field + "--";
         if (type === "boolean") contents = typeof field === "boolean" ? (BoolCast(field) ? "true" : "false") : "--" + typeof field + "--";
         if (type === "document") {
-            let doc = FieldValue(Cast(field, Doc));
+            const doc = FieldValue(Cast(field, Doc));
             contents = typeof field === "object" ? doc ? StrCast(doc.title) === "" ? "--" : StrCast(doc.title) : `--${typeof field}--` : `--${typeof field}--`;
         }
 
@@ -215,7 +215,7 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
                             height={"auto"}
                             maxHeight={Number(MAX_ROW_HEIGHT)}
                             GetValue={() => {
-                                let field = props.Document[props.fieldKey];
+                                const field = props.Document[props.fieldKey];
                                 if (Field.IsField(field)) {
                                     return Field.toScriptString(field);
                                 }
@@ -226,7 +226,7 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
                                 if (value.startsWith(":=")) {
                                     return this.props.setComputed(value.substring(2), props.Document, this.props.rowProps.column.id!, this.props.row, this.props.col);
                                 }
-                                let script = CompileScript(value, { requiredType: type, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } });
+                                const script = CompileScript(value, { requiredType: type, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } });
                                 if (!script.compiled) {
                                     return false;
                                 }
@@ -287,15 +287,15 @@ export class CollectionSchemaCheckboxCell extends CollectionSchemaCell {
     @action
     toggleChecked = (e: React.ChangeEvent<HTMLInputElement>) => {
         this._isChecked = e.target.checked;
-        let script = CompileScript(e.target.checked.toString(), { requiredType: "boolean", addReturn: true, params: { this: Doc.name } });
+        const script = CompileScript(e.target.checked.toString(), { requiredType: "boolean", addReturn: true, params: { this: Doc.name } });
         if (script.compiled) {
             this.applyToDoc(this._document, this.props.row, this.props.col, script.run);
         }
     }
 
     render() {
-        let reference = React.createRef<HTMLDivElement>();
-        let onItemDown = (e: React.PointerEvent) => {
+        const reference = React.createRef<HTMLDivElement>();
+        const onItemDown = (e: React.PointerEvent) => {
             (!this.props.CollectionView || !this.props.CollectionView.props.isSelected() ? undefined :
                 SetupDrag(reference, () => this._document, this.props.moveDocument, this.props.Document.schemaDoc ? "copy" : undefined)(e));
         };
diff --git a/src/client/views/collections/CollectionSchemaHeaders.tsx b/src/client/views/collections/CollectionSchemaHeaders.tsx
index d24f63fbb..0114342b9 100644
--- a/src/client/views/collections/CollectionSchemaHeaders.tsx
+++ b/src/client/views/collections/CollectionSchemaHeaders.tsx
@@ -1,5 +1,5 @@
 import React = require("react");
-import { action, computed, observable, trace, untracked } from "mobx";
+import { action, observable } from "mobx";
 import { observer } from "mobx-react";
 import "./CollectionSchemaView.scss";
 import { faPlus, faFont, faHashtag, faAlignJustify, faCheckSquare, faToggleOn, faSortAmountDown, faSortAmountUp, faTimes } from '@fortawesome/free-solid-svg-icons';
@@ -7,10 +7,8 @@ import { library, IconProp } from "@fortawesome/fontawesome-svg-core";
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 import { Flyout, anchorPoints } from "../DocumentDecorations";
 import { ColumnType } from "./CollectionSchemaView";
-import { emptyFunction } from "../../../Utils";
-import { contains } from "typescript-collections/dist/lib/arrays";
 import { faFile } from "@fortawesome/free-regular-svg-icons";
-import { SchemaHeaderField, RandomPastel, PastelSchemaPalette } from "../../../new_fields/SchemaHeaderField";
+import { SchemaHeaderField, PastelSchemaPalette } from "../../../new_fields/SchemaHeaderField";
 import { undoBatch } from "../../util/UndoManager";
 
 library.add(faPlus, faFont, faHashtag, faAlignJustify, faCheckSquare, faToggleOn, faFile as any, faSortAmountDown, faSortAmountUp, faTimes);
@@ -32,7 +30,7 @@ export interface HeaderProps {
 
 export class CollectionSchemaHeader extends React.Component<HeaderProps> {
     render() {
-        let icon: IconProp = this.props.keyType === ColumnType.Number ? "hashtag" : this.props.keyType === ColumnType.String ? "font" :
+        const icon: IconProp = this.props.keyType === ColumnType.Number ? "hashtag" : this.props.keyType === ColumnType.String ? "font" :
             this.props.keyType === ColumnType.Boolean ? "check-square" : this.props.keyType === ColumnType.Doc ? "file" : "align-justify";
         return (
             <div className="collectionSchemaView-header" style={{ background: this.props.keyValue.color }}>
@@ -139,7 +137,7 @@ export class CollectionSchemaColumnMenu extends React.Component<ColumnMenuProps>
     renderTypes = () => {
         if (this.props.typeConst) return <></>;
 
-        let type = this.props.columnField.type;
+        const type = this.props.columnField.type;
         return (
             <div className="collectionSchema-headerMenu-group">
                 <label>Column type:</label>
@@ -170,7 +168,7 @@ export class CollectionSchemaColumnMenu extends React.Component<ColumnMenuProps>
     }
 
     renderSorting = () => {
-        let sort = this.props.columnField.desc;
+        const sort = this.props.columnField.desc;
         return (
             <div className="collectionSchema-headerMenu-group">
                 <label>Sort by:</label>
@@ -193,14 +191,14 @@ export class CollectionSchemaColumnMenu extends React.Component<ColumnMenuProps>
     }
 
     renderColors = () => {
-        let selected = this.props.columnField.color;
+        const selected = this.props.columnField.color;
 
-        let pink = PastelSchemaPalette.get("pink2");
-        let purple = PastelSchemaPalette.get("purple2");
-        let blue = PastelSchemaPalette.get("bluegreen1");
-        let yellow = PastelSchemaPalette.get("yellow4");
-        let red = PastelSchemaPalette.get("red2");
-        let gray = "#f1efeb";
+        const pink = PastelSchemaPalette.get("pink2");
+        const purple = PastelSchemaPalette.get("purple2");
+        const blue = PastelSchemaPalette.get("bluegreen1");
+        const yellow = PastelSchemaPalette.get("yellow4");
+        const red = PastelSchemaPalette.get("red2");
+        const gray = "#f1efeb";
 
         return (
             <div className="collectionSchema-headerMenu-group">
@@ -291,8 +289,8 @@ class KeysDropdown extends React.Component<KeysDropdownProps> {
     @action
     onKeyDown = (e: React.KeyboardEvent): void => {
         if (e.key === "Enter") {
-            let keyOptions = this._searchTerm === "" ? this.props.possibleKeys : this.props.possibleKeys.filter(key => key.toUpperCase().indexOf(this._searchTerm.toUpperCase()) > -1);
-            let exactFound = keyOptions.findIndex(key => key.toUpperCase() === this._searchTerm.toUpperCase()) > -1 ||
+            const keyOptions = this._searchTerm === "" ? this.props.possibleKeys : this.props.possibleKeys.filter(key => key.toUpperCase().indexOf(this._searchTerm.toUpperCase()) > -1);
+            const exactFound = keyOptions.findIndex(key => key.toUpperCase() === this._searchTerm.toUpperCase()) > -1 ||
                 this.props.existingKeys.findIndex(key => key.toUpperCase() === this._searchTerm.toUpperCase()) > -1;
 
             if (!exactFound && this._searchTerm !== "" && this.props.canAddNew) {
@@ -334,11 +332,11 @@ class KeysDropdown extends React.Component<KeysDropdownProps> {
     renderOptions = (): JSX.Element[] | JSX.Element => {
         if (!this._isOpen) return <></>;
 
-        let keyOptions = this._searchTerm === "" ? this.props.possibleKeys : this.props.possibleKeys.filter(key => key.toUpperCase().indexOf(this._searchTerm.toUpperCase()) > -1);
-        let exactFound = keyOptions.findIndex(key => key.toUpperCase() === this._searchTerm.toUpperCase()) > -1 ||
+        const keyOptions = this._searchTerm === "" ? this.props.possibleKeys : this.props.possibleKeys.filter(key => key.toUpperCase().indexOf(this._searchTerm.toUpperCase()) > -1);
+        const exactFound = keyOptions.findIndex(key => key.toUpperCase() === this._searchTerm.toUpperCase()) > -1 ||
             this.props.existingKeys.findIndex(key => key.toUpperCase() === this._searchTerm.toUpperCase()) > -1;
 
-        let options = keyOptions.map(key => {
+        const options = keyOptions.map(key => {
             return <div key={key} className="key-option" onClick={() => { this.onSelect(key); this.setSearchTerm(""); }}>{key}</div>;
         });
 
diff --git a/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx b/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx
index 274c8b6d1..90320df82 100644
--- a/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx
+++ b/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx
@@ -1,18 +1,18 @@
 import React = require("react");
-import { ReactTableDefaults, TableCellRenderer, ComponentPropsGetterR, ComponentPropsGetter0, RowInfo } from "react-table";
+import { ReactTableDefaults, TableCellRenderer, RowInfo } from "react-table";
 import "./CollectionSchemaView.scss";
 import { Transform } from "../../util/Transform";
 import { Doc } from "../../../new_fields/Doc";
 import { DragManager, SetupDrag } from "../../util/DragManager";
 import { SelectionManager } from "../../util/SelectionManager";
-import { Cast, FieldValue, StrCast } from "../../../new_fields/Types";
+import { Cast, FieldValue } from "../../../new_fields/Types";
 import { ContextMenu } from "../ContextMenu";
 import { action } from "mobx";
 import { library } from '@fortawesome/fontawesome-svg-core';
 import { faGripVertical, faTrash } from '@fortawesome/free-solid-svg-icons';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { DocumentManager } from "../../util/DocumentManager";
-import { PastelSchemaPalette, SchemaHeaderField } from "../../../new_fields/SchemaHeaderField";
+import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField";
 import { undoBatch } from "../../util/UndoManager";
 
 library.add(faGripVertical, faTrash);
@@ -43,10 +43,10 @@ export class MovableColumn extends React.Component<MovableColumnProps> {
         document.removeEventListener("pointermove", this.onPointerMove);
     }
     onDragMove = (e: PointerEvent): void => {
-        let x = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY);
-        let rect = this._header!.current!.getBoundingClientRect();
-        let bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left + ((rect.right - rect.left) / 2), rect.top);
-        let before = x[0] < bounds[0];
+        const x = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY);
+        const rect = this._header!.current!.getBoundingClientRect();
+        const bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left + ((rect.right - rect.left) / 2), rect.top);
+        const before = x[0] < bounds[0];
         this._header!.current!.className = "collectionSchema-col-wrapper";
         if (before) this._header!.current!.className += " col-before";
         if (!before) this._header!.current!.className += " col-after";
@@ -62,10 +62,10 @@ export class MovableColumn extends React.Component<MovableColumnProps> {
 
     colDrop = (e: Event, de: DragManager.DropEvent) => {
         document.removeEventListener("pointermove", this.onDragMove, true);
-        let x = this.props.ScreenToLocalTransform().transformPoint(de.x, de.y);
-        let rect = this._header!.current!.getBoundingClientRect();
-        let bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left + ((rect.right - rect.left) / 2), rect.top);
-        let before = x[0] < bounds[0];
+        const x = this.props.ScreenToLocalTransform().transformPoint(de.x, de.y);
+        const rect = this._header!.current!.getBoundingClientRect();
+        const bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left + ((rect.right - rect.left) / 2), rect.top);
+        const before = x[0] < bounds[0];
         if (de.data instanceof DragManager.ColumnDragData) {
             this.props.reorderColumns(de.data.colKey, this.props.columnValue, before, this.props.allColumns);
             return true;
@@ -74,21 +74,21 @@ export class MovableColumn extends React.Component<MovableColumnProps> {
     }
 
     onPointerMove = (e: PointerEvent) => {
-        let onRowMove = (e: PointerEvent) => {
+        const onRowMove = (e: PointerEvent) => {
             e.stopPropagation();
             e.preventDefault();
 
             document.removeEventListener("pointermove", onRowMove);
             document.removeEventListener('pointerup', onRowUp);
-            let dragData = new DragManager.ColumnDragData(this.props.columnValue);
+            const dragData = new DragManager.ColumnDragData(this.props.columnValue);
             DragManager.StartColumnDrag(this._dragRef.current!, dragData, e.x, e.y);
         };
-        let onRowUp = (): void => {
+        const onRowUp = (): void => {
             document.removeEventListener("pointermove", onRowMove);
             document.removeEventListener('pointerup', onRowUp);
         };
         if (e.buttons === 1) {
-            let [dx, dy] = this.props.ScreenToLocalTransform().transformDirection(e.clientX - this._startDragPosition.x, e.clientY - this._startDragPosition.y);
+            const [dx, dy] = this.props.ScreenToLocalTransform().transformDirection(e.clientX - this._startDragPosition.x, e.clientY - this._startDragPosition.y);
             if (Math.abs(dx) + Math.abs(dy) > this._sensitivity) {
                 document.removeEventListener("pointermove", this.onPointerMove);
                 e.stopPropagation();
@@ -106,14 +106,14 @@ export class MovableColumn extends React.Component<MovableColumnProps> {
     @action
     onPointerDown = (e: React.PointerEvent, ref: React.RefObject<HTMLDivElement>) => {
         this._dragRef = ref;
-        let [dx, dy] = this.props.ScreenToLocalTransform().transformDirection(e.clientX, e.clientY);
+        const [dx, dy] = this.props.ScreenToLocalTransform().transformDirection(e.clientX, e.clientY);
         this._startDragPosition = { x: dx, y: dy };
         document.addEventListener("pointermove", this.onPointerMove);
     }
 
 
     render() {
-        let reference = React.createRef<HTMLDivElement>();
+        const reference = React.createRef<HTMLDivElement>();
 
         return (
             <div className="collectionSchema-col" ref={this.createColDropTarget}>
@@ -152,10 +152,10 @@ export class MovableRow extends React.Component<MovableRowProps> {
         document.removeEventListener("pointermove", this.onDragMove, true);
     }
     onDragMove = (e: PointerEvent): void => {
-        let x = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY);
-        let rect = this._header!.current!.getBoundingClientRect();
-        let bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left, rect.top + rect.height / 2);
-        let before = x[1] < bounds[1];
+        const x = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY);
+        const rect = this._header!.current!.getBoundingClientRect();
+        const bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left, rect.top + rect.height / 2);
+        const before = x[1] < bounds[1];
         this._header!.current!.className = "collectionSchema-row-wrapper";
         if (before) this._header!.current!.className += " row-above";
         if (!before) this._header!.current!.className += " row-below";
@@ -173,16 +173,16 @@ export class MovableRow extends React.Component<MovableRowProps> {
         const rowDoc = FieldValue(Cast(this.props.rowInfo.original, Doc));
         if (!rowDoc) return false;
 
-        let x = this.props.ScreenToLocalTransform().transformPoint(de.x, de.y);
-        let rect = this._header!.current!.getBoundingClientRect();
-        let bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left, rect.top + rect.height / 2);
-        let before = x[1] < bounds[1];
+        const x = this.props.ScreenToLocalTransform().transformPoint(de.x, de.y);
+        const rect = this._header!.current!.getBoundingClientRect();
+        const bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left, rect.top + rect.height / 2);
+        const before = x[1] < bounds[1];
 
         if (de.data instanceof DragManager.DocumentDragData) {
             e.stopPropagation();
             if (de.data.draggedDocuments[0] === rowDoc) return true;
-            let addDocument = (doc: Doc) => this.props.addDoc(doc, rowDoc, before);
-            let movedDocs = de.data.draggedDocuments;
+            const addDocument = (doc: Doc) => this.props.addDoc(doc, rowDoc, before);
+            const movedDocs = de.data.draggedDocuments;
             return (de.data.dropAction || de.data.userDropAction) ?
                 de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDoc(d, rowDoc, before) || added, false)
                 : (de.data.moveDocument) ?
@@ -193,14 +193,14 @@ export class MovableRow extends React.Component<MovableRowProps> {
     }
 
     onRowContextMenu = (e: React.MouseEvent): void => {
-        let description = this.props.rowWrapped ? "Unwrap text on row" : "Text wrap row";
+        const description = this.props.rowWrapped ? "Unwrap text on row" : "Text wrap row";
         ContextMenu.Instance.addItem({ description: description, event: () => this.props.textWrapRow(this.props.rowInfo.original), icon: "file-pdf" });
     }
 
     @undoBatch
     @action
     move: DragManager.MoveFunction = (doc: Doc, target: Doc, addDoc) => {
-        let targetView = DocumentManager.Instance.getDocumentView(target);
+        const targetView = DocumentManager.Instance.getDocumentView(target);
         if (targetView && targetView.props.ContainingCollectionDoc) {
             return doc !== target && doc !== targetView.props.ContainingCollectionDoc && this.props.removeDoc(doc) && addDoc(doc);
         }
@@ -217,8 +217,8 @@ export class MovableRow extends React.Component<MovableRowProps> {
         const doc = FieldValue(Cast(original, Doc));
         if (!doc) return <></>;
 
-        let reference = React.createRef<HTMLDivElement>();
-        let onItemDown = SetupDrag(reference, () => doc, this.move);
+        const reference = React.createRef<HTMLDivElement>();
+        const onItemDown = SetupDrag(reference, () => doc, this.move);
 
         let className = "collectionSchema-row";
         if (this.props.rowFocused) className += " row-focused";
diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx
index 65856cad3..f336eaf75 100644
--- a/src/client/views/collections/CollectionSchemaView.tsx
+++ b/src/client/views/collections/CollectionSchemaView.tsx
@@ -94,11 +94,11 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
     }
     @action
     onDividerMove = (e: PointerEvent): void => {
-        let nativeWidth = this._mainCont!.getBoundingClientRect();
-        let minWidth = 40;
-        let maxWidth = 1000;
-        let movedWidth = this.props.ScreenToLocalTransform().transformDirection(nativeWidth.right - e.clientX, 0)[0];
-        let width = movedWidth < minWidth ? minWidth : movedWidth > maxWidth ? maxWidth : movedWidth;
+        const nativeWidth = this._mainCont!.getBoundingClientRect();
+        const minWidth = 40;
+        const maxWidth = 1000;
+        const movedWidth = this.props.ScreenToLocalTransform().transformDirection(nativeWidth.right - e.clientX, 0)[0];
+        const width = movedWidth < minWidth ? minWidth : movedWidth > maxWidth ? maxWidth : movedWidth;
         this.props.Document.schemaPreviewWidth = width;
     }
     @action
@@ -136,7 +136,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
 
     @computed
     get previewPanel() {
-        let layoutDoc = this.previewDocument ? Doc.expandTemplateLayout(this.previewDocument, this.props.DataDoc) : undefined;
+        const layoutDoc = this.previewDocument ? Doc.expandTemplateLayout(this.previewDocument, this.props.DataDoc) : undefined;
         return <div ref={this.createTarget}>
             <ContentFittingDocumentView
                 Document={layoutDoc}
@@ -258,11 +258,11 @@ export class SchemaTable extends React.Component<SchemaTableProps> {
     @computed get childDocs() {
         if (this.props.childDocs) return this.props.childDocs;
 
-        let doc = this.props.dataDoc ? this.props.dataDoc : this.props.Document;
+        const doc = this.props.dataDoc ? this.props.dataDoc : this.props.Document;
         return DocListCast(doc[this.props.fieldKey]);
     }
     set childDocs(docs: Doc[]) {
-        let doc = this.props.dataDoc ? this.props.dataDoc : this.props.Document;
+        const doc = this.props.dataDoc ? this.props.dataDoc : this.props.Document;
         doc[this.props.fieldKey] = new List<Doc>(docs);
     }
 
@@ -288,12 +288,12 @@ export class SchemaTable extends React.Component<SchemaTableProps> {
 
     @computed get borderWidth() { return Number(COLLECTION_BORDER_WIDTH); }
     @computed get tableColumns(): Column<Doc>[] {
-        let possibleKeys = this.documentKeys.filter(key => this.columns.findIndex(existingKey => existingKey.heading.toUpperCase() === key.toUpperCase()) === -1);
-        let columns: Column<Doc>[] = [];
-        let tableIsFocused = this.props.isFocused(this.props.Document);
-        let focusedRow = this._focusedCell.row;
-        let focusedCol = this._focusedCell.col;
-        let isEditable = !this._headerIsEditing;
+        const possibleKeys = this.documentKeys.filter(key => this.columns.findIndex(existingKey => existingKey.heading.toUpperCase() === key.toUpperCase()) === -1);
+        const columns: Column<Doc>[] = [];
+        const tableIsFocused = this.props.isFocused(this.props.Document);
+        const focusedRow = this._focusedCell.row;
+        const focusedCol = this._focusedCell.col;
+        const isEditable = !this._headerIsEditing;
 
         if (this.childDocs.reduce((found, doc) => found || doc.type === "collection", false)) {
             columns.push(
@@ -313,8 +313,8 @@ export class SchemaTable extends React.Component<SchemaTableProps> {
             );
         }
 
-        let cols = this.columns.map(col => {
-            let header = <CollectionSchemaHeader
+        const cols = this.columns.map(col => {
+            const header = <CollectionSchemaHeader
                 keyValue={col}
                 possibleKeys={possibleKeys}
                 existingKeys={this.columns.map(c => c.heading)}
@@ -333,11 +333,11 @@ export class SchemaTable extends React.Component<SchemaTableProps> {
                 accessor: (doc: Doc) => doc ? doc[col.heading] : 0,
                 id: col.heading,
                 Cell: (rowProps: CellInfo) => {
-                    let rowIndex = rowProps.index;
-                    let columnIndex = this.columns.map(c => c.heading).indexOf(rowProps.column.id!);
-                    let isFocused = focusedRow === rowIndex && focusedCol === columnIndex && tableIsFocused;
+                    const rowIndex = rowProps.index;
+                    const columnIndex = this.columns.map(c => c.heading).indexOf(rowProps.column.id!);
+                    const isFocused = focusedRow === rowIndex && focusedCol === columnIndex && tableIsFocused;
 
-                    let props: CellProps = {
+                    const props: CellProps = {
                         row: rowIndex,
                         col: columnIndex,
                         rowProps: rowProps,
@@ -358,7 +358,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> {
                         getField: this.getField,
                     };
 
-                    let colType = this.getColumnType(col);
+                    const colType = this.getColumnType(col);
                     if (colType === ColumnType.Number) return <CollectionSchemaNumberCell {...props} />;
                     if (colType === ColumnType.String) return <CollectionSchemaStringCell {...props} />;
                     if (colType === ColumnType.Boolean) return <CollectionSchemaCheckboxCell {...props} />;
@@ -384,9 +384,9 @@ export class SchemaTable extends React.Component<SchemaTableProps> {
     constructor(props: SchemaTableProps) {
         super(props);
         // convert old schema columns (list of strings) into new schema columns (list of schema header fields)
-        let oldSchemaColumns = Cast(this.props.Document.schemaColumns, listSpec("string"), []);
+        const oldSchemaColumns = Cast(this.props.Document.schemaColumns, listSpec("string"), []);
         if (oldSchemaColumns && oldSchemaColumns.length && typeof oldSchemaColumns[0] !== "object") {
-            let newSchemaColumns = oldSchemaColumns.map(i => typeof i === "string" ? new SchemaHeaderField(i, "#f1efeb") : i);
+            const newSchemaColumns = oldSchemaColumns.map(i => typeof i === "string" ? new SchemaHeaderField(i, "#f1efeb") : i);
             this.props.Document.schemaColumns = new List<SchemaHeaderField>(newSchemaColumns);
         }
     }
@@ -418,10 +418,10 @@ export class SchemaTable extends React.Component<SchemaTableProps> {
     private getTdProps: ComponentPropsGetterR = (state, rowInfo, column, instance) => {
         if (!rowInfo || column) return {};
 
-        let row = rowInfo.index;
+        const row = rowInfo.index;
         //@ts-ignore
-        let col = this.columns.map(c => c.heading).indexOf(column!.id);
-        let isFocused = this._focusedCell.row === row && this._focusedCell.col === col && this.props.isFocused(this.props.Document);
+        const col = this.columns.map(c => c.heading).indexOf(column!.id);
+        const isFocused = this._focusedCell.row === row && this._focusedCell.col === col && this.props.isFocused(this.props.Document);
         // TODO: editing border doesn't work :(
         return {
             style: {
@@ -432,7 +432,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> {
 
     @action
     onCloseCollection = (collection: Doc): void => {
-        let index = this._openCollections.findIndex(col => col === collection[Id]);
+        const index = this._openCollections.findIndex(col => col === collection[Id]);
         if (index > -1) this._openCollections.splice(index, 1);
     }
 
@@ -450,7 +450,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> {
     @action
     onKeyDown = (e: KeyboardEvent): void => {
         if (!this._cellIsEditing && !this._headerIsEditing && this.props.isFocused(this.props.Document)) {// && this.props.isSelected(true)) {
-            let direction = e.key === "Tab" ? "tab" : e.which === 39 ? "right" : e.which === 37 ? "left" : e.which === 38 ? "up" : e.which === 40 ? "down" : "";
+            const direction = e.key === "Tab" ? "tab" : e.which === 39 ? "right" : e.which === 37 ? "left" : e.which === 38 ? "up" : e.which === 40 ? "down" : "";
             this._focusedCell = this.changeFocusedCellByDirection(direction, this._focusedCell.row, this._focusedCell.col);
 
             const pdoc = FieldValue(this.childDocs[this._focusedCell.row]);
@@ -479,7 +479,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> {
 
     @undoBatch
     createRow = () => {
-        let newDoc = Docs.Create.TextDocument({ title: "", width: 100, height: 30 });
+        const newDoc = Docs.Create.TextDocument({ title: "", width: 100, height: 30 });
         this.props.addDocument(newDoc);
     }
 
@@ -498,7 +498,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> {
     @undoBatch
     @action
     deleteColumn = (key: string) => {
-        let columns = this.columns;
+        const columns = this.columns;
         if (columns === undefined) {
             this.columns = new List<SchemaHeaderField>([]);
         } else {
@@ -513,7 +513,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> {
     @undoBatch
     @action
     changeColumns = (oldKey: string, newKey: string, addNew: boolean) => {
-        let columns = this.columns;
+        const columns = this.columns;
         if (columns === undefined) {
             this.columns = new List<SchemaHeaderField>([new SchemaHeaderField(newKey, "f1efeb")]);
         } else {
@@ -523,7 +523,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> {
             } else {
                 const index = columns.map(c => c.heading).indexOf(oldKey);
                 if (index > -1) {
-                    let column = columns[index];
+                    const column = columns[index];
                     column.setHeading(newKey);
                     columns[index] = column;
                     this.columns = columns;
@@ -554,8 +554,8 @@ export class SchemaTable extends React.Component<SchemaTableProps> {
     setColumnType = (columnField: SchemaHeaderField, type: ColumnType): void => {
         if (columnTypes.get(columnField.heading)) return;
 
-        let columns = this.columns;
-        let index = columns.indexOf(columnField);
+        const columns = this.columns;
+        const index = columns.indexOf(columnField);
         if (index > -1) {
             columnField.setType(NumCast(type));
             columns[index] = columnField;
@@ -575,8 +575,8 @@ export class SchemaTable extends React.Component<SchemaTableProps> {
 
     @undoBatch
     setColumnColor = (columnField: SchemaHeaderField, color: string): void => {
-        let columns = this.columns;
-        let index = columns.indexOf(columnField);
+        const columns = this.columns;
+        const index = columns.indexOf(columnField);
         if (index > -1) {
             columnField.setColor(color);
             columns[index] = columnField;
@@ -589,10 +589,10 @@ export class SchemaTable extends React.Component<SchemaTableProps> {
 
     @undoBatch
     reorderColumns = (toMove: SchemaHeaderField, relativeTo: SchemaHeaderField, before: boolean, columnsValues: SchemaHeaderField[]) => {
-        let columns = [...columnsValues];
-        let oldIndex = columns.indexOf(toMove);
-        let relIndex = columns.indexOf(relativeTo);
-        let newIndex = (oldIndex > relIndex && !before) ? relIndex + 1 : (oldIndex < relIndex && before) ? relIndex - 1 : relIndex;
+        const columns = [...columnsValues];
+        const oldIndex = columns.indexOf(toMove);
+        const relIndex = columns.indexOf(relativeTo);
+        const newIndex = (oldIndex > relIndex && !before) ? relIndex + 1 : (oldIndex < relIndex && before) ? relIndex - 1 : relIndex;
 
         if (oldIndex === newIndex) return;
 
@@ -603,17 +603,17 @@ export class SchemaTable extends React.Component<SchemaTableProps> {
     @undoBatch
     @action
     setColumnSort = (columnField: SchemaHeaderField, descending: boolean | undefined) => {
-        let columns = this.columns;
-        let index = columns.findIndex(c => c.heading === columnField.heading);
-        let column = columns[index];
+        const columns = this.columns;
+        const index = columns.findIndex(c => c.heading === columnField.heading);
+        const column = columns[index];
         column.setDesc(descending);
         columns[index] = column;
         this.columns = columns;
     }
 
     get documentKeys() {
-        let docs = this.childDocs;
-        let keys: { [key: string]: boolean } = {};
+        const docs = this.childDocs;
+        const keys: { [key: string]: boolean } = {};
         // bcz: ugh.  this is untracked since otherwise a large collection of documents will blast the server for all their fields.
         //  then as each document's fields come back, we update the documents _proxies.  Each time we do this, the whole schema will be
         //  invalidated and re-rendered.   This workaround will inquire all of the document fields before the options button is clicked.
@@ -628,8 +628,8 @@ export class SchemaTable extends React.Component<SchemaTableProps> {
 
     @action
     toggleTextWrapRow = (doc: Doc): void => {
-        let textWrapped = this.textWrappedRows;
-        let index = textWrapped.findIndex(id => doc[Id] === id);
+        const textWrapped = this.textWrappedRows;
+        const index = textWrapped.findIndex(id => doc[Id] === id);
 
         index > -1 ? textWrapped.splice(index, 1) : textWrapped.push(doc[Id]);
 
@@ -638,10 +638,10 @@ export class SchemaTable extends React.Component<SchemaTableProps> {
 
     @computed
     get reactTable() {
-        let children = this.childDocs;
-        let hasCollectionChild = children.reduce((found, doc) => found || doc.type === "collection", false);
-        let expandedRowsList = this._openCollections.map(col => children.findIndex(doc => doc[Id] === col).toString());
-        let expanded = {};
+        const children = this.childDocs;
+        const hasCollectionChild = children.reduce((found, doc) => found || doc.type === "collection", false);
+        const expandedRowsList = this._openCollections.map(col => children.findIndex(doc => doc[Id] === col).toString());
+        const expanded = {};
         //@ts-ignore
         expandedRowsList.forEach(row => expanded[row] = true);
         console.log("text wrapped rows", ...[...this.textWrappedRows]); // TODO: get component to rerender on text wrap change without needign to console.log :((((
@@ -668,10 +668,10 @@ export class SchemaTable extends React.Component<SchemaTableProps> {
     }
 
     onResizedChange = (newResized: Resize[], event: any) => {
-        let columns = this.columns;
+        const columns = this.columns;
         newResized.forEach(resized => {
-            let index = columns.findIndex(c => c.heading === resized.id);
-            let column = columns[index];
+            const index = columns.findIndex(c => c.heading === resized.id);
+            const column = columns[index];
             column.setWidth(resized.value);
             columns[index] = column;
         });
@@ -688,16 +688,16 @@ export class SchemaTable extends React.Component<SchemaTableProps> {
     makeDB = async () => {
         let csv: string = this.columns.reduce((val, col) => val + col + ",", "");
         csv = csv.substr(0, csv.length - 1) + "\n";
-        let self = this;
+        const self = this;
         this.childDocs.map(doc => {
             csv += self.columns.reduce((val, col) => val + (doc[col.heading] ? doc[col.heading]!.toString() : "0") + ",", "");
             csv = csv.substr(0, csv.length - 1) + "\n";
         });
         csv.substring(0, csv.length - 1);
-        let dbName = StrCast(this.props.Document.title);
-        let res = await Gateway.Instance.PostSchema(csv, dbName);
+        const dbName = StrCast(this.props.Document.title);
+        const res = await Gateway.Instance.PostSchema(csv, dbName);
         if (self.props.CollectionView && self.props.CollectionView.props.addDocument) {
-            let schemaDoc = await Docs.Create.DBDocument("https://www.cs.brown.edu/" + dbName, { title: dbName }, { dbDoc: self.props.Document });
+            const schemaDoc = await Docs.Create.DBDocument("https://www.cs.brown.edu/" + dbName, { title: dbName }, { dbDoc: self.props.Document });
             if (schemaDoc) {
                 //self.props.CollectionView.props.addDocument(schemaDoc, false);
                 self.props.Document.schemaDoc = schemaDoc;
@@ -706,7 +706,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> {
     }
 
     getField = (row: number, col?: number) => {
-        let docs = this.childDocs;
+        const docs = this.childDocs;
 
         row = row % docs.length;
         while (row < 0) row += docs.length;
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index e564f1193..955fcda80 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -1,7 +1,7 @@
 import React = require("react");
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 import { CursorProperty } from "csstype";
-import { action, computed, IReactionDisposer, observable, reaction, runInAction, trace } from "mobx";
+import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx";
 import { observer } from "mobx-react";
 import Switch from 'rc-switch';
 import { Doc, HeightSym, WidthSym } from "../../../new_fields/Doc";
@@ -10,7 +10,7 @@ import { List } from "../../../new_fields/List";
 import { listSpec } from "../../../new_fields/Schema";
 import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField";
 import { BoolCast, Cast, NumCast, StrCast, ScriptCast } from "../../../new_fields/Types";
-import { emptyFunction, Utils, numberRange } from "../../../Utils";
+import { emptyFunction, Utils } from "../../../Utils";
 import { DocumentType } from "../../documents/DocumentTypes";
 import { DragManager } from "../../util/DragManager";
 import { Transform } from "../../util/Transform";
@@ -56,15 +56,15 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
     children(docs: Doc[]) {
         this._docXfs.length = 0;
         return docs.map((d, i) => {
-            let pair = Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, d);
-            let layoutDoc = pair.layout ? Doc.Layout(pair.layout) : d;
-            let width = () => Math.min(layoutDoc.nativeWidth && !layoutDoc.ignoreAspect && !this.props.Document.fillColumn ? layoutDoc[WidthSym]() : Number.MAX_VALUE, this.columnWidth / this.numGroupColumns);
-            let height = () => this.getDocHeight(layoutDoc);
-            let dref = React.createRef<HTMLDivElement>();
-            let dxf = () => this.getDocTransform(layoutDoc, dref.current!);
+            const pair = Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, d);
+            const layoutDoc = pair.layout ? Doc.Layout(pair.layout) : d;
+            const width = () => Math.min(layoutDoc.nativeWidth && !layoutDoc.ignoreAspect && !this.props.Document.fillColumn ? layoutDoc[WidthSym]() : Number.MAX_VALUE, this.columnWidth / this.numGroupColumns);
+            const height = () => this.getDocHeight(layoutDoc);
+            const dref = React.createRef<HTMLDivElement>();
+            const dxf = () => this.getDocTransform(layoutDoc, dref.current!);
             this._docXfs.push({ dxf: dxf, width: width, height: height });
-            let rowSpan = Math.ceil((height() + this.gridGap) / this.gridGap);
-            let style = this.isStackingView ? { width: width(), marginTop: i === 0 ? 0 : this.gridGap, height: height() } : { gridRowEnd: `span ${rowSpan}` };
+            const rowSpan = Math.ceil((height() + this.gridGap) / this.gridGap);
+            const style = this.isStackingView ? { width: width(), marginTop: i === 0 ? 0 : this.gridGap, height: height() } : { gridRowEnd: `span ${rowSpan}` };
             return <div className={`collectionStackingView-${this.isStackingView ? "columnDoc" : "masonryDoc"}`} key={d[Id]} ref={dref} style={style} >
                 {this.getDisplayDoc(pair.layout || d, pair.data, dxf, width)}
             </div>;
@@ -83,20 +83,20 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
             return new Map<SchemaHeaderField, Doc[]>();
         }
         const sectionHeaders = this.sectionHeaders;
-        let fields = new Map<SchemaHeaderField, Doc[]>(sectionHeaders.map(sh => [sh, []] as [SchemaHeaderField, []]));
+        const fields = new Map<SchemaHeaderField, Doc[]>(sectionHeaders.map(sh => [sh, []] as [SchemaHeaderField, []]));
         this.filteredChildren.map(d => {
-            let sectionValue = (d[this.sectionFilter] ? d[this.sectionFilter] : `NO ${this.sectionFilter.toUpperCase()} VALUE`) as object;
+            const sectionValue = (d[this.sectionFilter] ? d[this.sectionFilter] : `NO ${this.sectionFilter.toUpperCase()} VALUE`) as object;
             // the next five lines ensures that floating point rounding errors don't create more than one section -syip
-            let parsed = parseInt(sectionValue.toString());
-            let castedSectionValue = !isNaN(parsed) ? parsed : sectionValue;
+            const parsed = parseInt(sectionValue.toString());
+            const castedSectionValue = !isNaN(parsed) ? parsed : sectionValue;
 
             // look for if header exists already
-            let existingHeader = sectionHeaders.find(sh => sh.heading === (castedSectionValue ? castedSectionValue.toString() : `NO ${this.sectionFilter.toUpperCase()} VALUE`));
+            const existingHeader = sectionHeaders.find(sh => sh.heading === (castedSectionValue ? castedSectionValue.toString() : `NO ${this.sectionFilter.toUpperCase()} VALUE`));
             if (existingHeader) {
                 fields.get(existingHeader)!.push(d);
             }
             else {
-                let newSchemaHeader = new SchemaHeaderField(castedSectionValue ? castedSectionValue.toString() : `NO ${this.sectionFilter.toUpperCase()} VALUE`);
+                const newSchemaHeader = new SchemaHeaderField(castedSectionValue ? castedSectionValue.toString() : `NO ${this.sectionFilter.toUpperCase()} VALUE`);
                 fields.set(newSchemaHeader, [d]);
                 sectionHeaders.push(newSchemaHeader);
             }
@@ -108,26 +108,26 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
         super.componentDidMount();
         this._heightDisposer = reaction(() => {
             if (this.props.Document.autoHeight) {
-                let sectionsList = Array.from(this.Sections.size ? this.Sections.values() : [this.filteredChildren]);
+                const sectionsList = Array.from(this.Sections.size ? this.Sections.values() : [this.filteredChildren]);
                 if (this.isStackingView) {
-                    let res = this.props.ContentScaling() * sectionsList.reduce((maxHght, s) => {
-                        let r1 = Math.max(maxHght,
+                    const res = this.props.ContentScaling() * sectionsList.reduce((maxHght, s) => {
+                        const r1 = Math.max(maxHght,
                             (this.Sections.size ? 50 : 0) + s.reduce((height, d, i) => {
-                                let val = height + this.childDocHeight(d) + (i === s.length - 1 ? this.yMargin : this.gridGap);
+                                const val = height + this.childDocHeight(d) + (i === s.length - 1 ? this.yMargin : this.gridGap);
                                 return val;
                             }, this.yMargin));
                         return r1;
                     }, 0);
                     return res;
                 } else {
-                    let sum = Array.from(this._heightMap.values()).reduce((acc: number, curr: number) => acc += curr, 0);
+                    const sum = Array.from(this._heightMap.values()).reduce((acc: number, curr: number) => acc += curr, 0);
                     return this.props.ContentScaling() * (sum + (this.Sections.size ? (this.props.Document.miniHeaders ? 20 : 85) : -15));
                 }
             }
             return -1;
         },
             (hgt: number) => {
-                let doc = hgt === -1 ? undefined : this.props.DataDoc && this.props.DataDoc.layout === this.layoutDoc ? this.props.DataDoc : this.layoutDoc;
+                const doc = hgt === -1 ? undefined : this.props.DataDoc && this.props.DataDoc.layout === this.layoutDoc ? this.props.DataDoc : this.layoutDoc;
                 doc && hgt > 0 && (Doc.Layout(doc).height = hgt);
             },
             { fireImmediately: true }
@@ -162,9 +162,9 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
     @computed get onClickHandler() { return ScriptCast(this.Document.onChildClick); }
 
     getDisplayDoc(doc: Doc, dataDoc: Doc | undefined, dxf: () => Transform, width: () => number) {
-        let layoutDoc = Doc.Layout(doc);
-        let height = () => this.getDocHeight(doc);
-        let finalDxf = () => dxf().scale(this.columnWidth / layoutDoc[WidthSym]());
+        const layoutDoc = Doc.Layout(doc);
+        const height = () => this.getDocHeight(doc);
+        const finalDxf = () => dxf().scale(this.columnWidth / layoutDoc[WidthSym]());
         return <ContentFittingDocumentView
             Document={doc}
             DataDocument={dataDoc}
@@ -192,12 +192,12 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
     }
     getDocHeight(d?: Doc) {
         if (!d) return 0;
-        let layoutDoc = Doc.Layout(d);
-        let nw = NumCast(layoutDoc.nativeWidth);
-        let nh = NumCast(layoutDoc.nativeHeight);
+        const layoutDoc = Doc.Layout(d);
+        const nw = NumCast(layoutDoc.nativeWidth);
+        const nh = NumCast(layoutDoc.nativeHeight);
         let wid = this.columnWidth / (this.isStackingView ? this.numGroupColumns : 1);
         if (!layoutDoc.ignoreAspect && !layoutDoc.fitWidth && nw && nh) {
-            let aspect = nw && nh ? nh / nw : 1;
+            const aspect = nw && nh ? nh / nw : 1;
             if (!(d.nativeWidth && !layoutDoc.ignoreAspect && this.props.Document.fillColumn)) wid = Math.min(layoutDoc[WidthSym](), wid);
             return wid * aspect;
         }
@@ -215,8 +215,8 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
     }
     @action
     onDividerMove = (e: PointerEvent): void => {
-        let dragPos = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY)[0];
-        let delta = dragPos - this._columnStart;
+        const dragPos = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY)[0];
+        const delta = dragPos - this._columnStart;
         this._columnStart = dragPos;
         this.layoutDoc.columnWidth = Math.max(10, this.columnWidth + delta);
     }
@@ -238,13 +238,13 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
     @undoBatch
     @action
     drop = (e: Event, de: DragManager.DropEvent) => {
-        let where = [de.x, de.y];
+        const where = [de.x, de.y];
         let targInd = -1;
         let plusOne = false;
         if (de.data instanceof DragManager.DocumentDragData) {
             this._docXfs.map((cd, i) => {
-                let pos = cd.dxf().inverse().transformPoint(-2 * this.gridGap, -2 * this.gridGap);
-                let pos1 = cd.dxf().inverse().transformPoint(cd.width(), cd.height());
+                const pos = cd.dxf().inverse().transformPoint(-2 * this.gridGap, -2 * this.gridGap);
+                const pos1 = cd.dxf().inverse().transformPoint(cd.width(), cd.height());
                 if (where[0] > pos[0] && where[0] < pos1[0] && where[1] > pos[1] && where[1] < pos1[1]) {
                     targInd = i;
                     plusOne = (where[1] > (pos[1] + pos1[1]) / 2 ? 1 : 0) ? true : false;
@@ -252,12 +252,12 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
             });
         }
         if (super.drop(e, de)) {
-            let newDoc = de.data.droppedDocuments[0];
-            let docs = this.childDocList;
+            const newDoc = de.data.droppedDocuments[0];
+            const docs = this.childDocList;
             if (docs) {
                 if (targInd === -1) targInd = docs.length;
                 else targInd = docs.indexOf(this.filteredChildren[targInd]);
-                let srcInd = docs.indexOf(newDoc);
+                const srcInd = docs.indexOf(newDoc);
                 docs.splice(srcInd, 1);
                 docs.splice((targInd > srcInd ? targInd - 1 : targInd) + (plusOne ? 1 : 0), 0, newDoc);
             }
@@ -267,19 +267,19 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
     @undoBatch
     @action
     onDrop = async (e: React.DragEvent): Promise<void> => {
-        let where = [e.clientX, e.clientY];
+        const where = [e.clientX, e.clientY];
         let targInd = -1;
         this._docXfs.map((cd, i) => {
-            let pos = cd.dxf().inverse().transformPoint(-2 * this.gridGap, -2 * this.gridGap);
-            let pos1 = cd.dxf().inverse().transformPoint(cd.width(), cd.height());
+            const pos = cd.dxf().inverse().transformPoint(-2 * this.gridGap, -2 * this.gridGap);
+            const pos1 = cd.dxf().inverse().transformPoint(cd.width(), cd.height());
             if (where[0] > pos[0] && where[0] < pos1[0] && where[1] > pos[1] && where[1] < pos1[1]) {
                 targInd = i;
             }
         });
         super.onDrop(e, {}, () => {
             if (targInd !== -1) {
-                let newDoc = this.childDocs[this.childDocs.length - 1];
-                let docs = this.childDocList;
+                const newDoc = this.childDocs[this.childDocs.length - 1];
+                const docs = this.childDocList;
                 if (docs) {
                     docs.splice(docs.length - 1, 1);
                     docs.splice(targInd, 0, newDoc);
@@ -289,13 +289,13 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
     }
     headings = () => Array.from(this.Sections.keys());
     sectionStacking = (heading: SchemaHeaderField | undefined, docList: Doc[]) => {
-        let key = this.sectionFilter;
+        const key = this.sectionFilter;
         let type: "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" | undefined = undefined;
-        let types = docList.length ? docList.map(d => typeof d[key]) : this.childDocs.map(d => typeof d[key]);
+        const types = docList.length ? docList.map(d => typeof d[key]) : this.childDocs.map(d => typeof d[key]);
         if (types.map((i, idx) => types.indexOf(i) === idx).length === 1) {
             type = types[0];
         }
-        let cols = () => this.isStackingView ? 1 : Math.max(1, Math.min(this.filteredChildren.length,
+        const cols = () => this.isStackingView ? 1 : Math.max(1, Math.min(this.filteredChildren.length,
             Math.floor((this.props.PanelWidth() - 2 * this.xMargin) / (this.columnWidth + this.gridGap))));
         return <CollectionStackingViewFieldColumn
             key={heading ? heading.heading : ""}
@@ -313,23 +313,23 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
 
     getDocTransform(doc: Doc, dref: HTMLDivElement) {
         if (!dref) return Transform.Identity();
-        let y = this._scroll; // required for document decorations to update when the text box container is scrolled
-        let { scale, translateX, translateY } = Utils.GetScreenTransform(dref);
-        let outerXf = Utils.GetScreenTransform(this._masonryGridRef!);
-        let offset = this.props.ScreenToLocalTransform().transformDirection(outerXf.translateX - translateX, outerXf.translateY - translateY);
+        const y = this._scroll; // required for document decorations to update when the text box container is scrolled
+        const { scale, translateX, translateY } = Utils.GetScreenTransform(dref);
+        const outerXf = Utils.GetScreenTransform(this._masonryGridRef!);
+        const offset = this.props.ScreenToLocalTransform().transformDirection(outerXf.translateX - translateX, outerXf.translateY - translateY);
         return this.props.ScreenToLocalTransform().
             translate(offset[0], offset[1] + (this.props.ChromeHeight && this.props.ChromeHeight() < 0 ? this.props.ChromeHeight() : 0)).
             scale(NumCast(doc.width, 1) / this.columnWidth);
     }
 
     sectionMasonry = (heading: SchemaHeaderField | undefined, docList: Doc[]) => {
-        let key = this.sectionFilter;
+        const key = this.sectionFilter;
         let type: "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" | undefined = undefined;
-        let types = docList.length ? docList.map(d => typeof d[key]) : this.childDocs.map(d => typeof d[key]);
+        const types = docList.length ? docList.map(d => typeof d[key]) : this.childDocs.map(d => typeof d[key]);
         if (types.map((i, idx) => types.indexOf(i) === idx).length === 1) {
             type = types[0];
         }
-        let rows = () => !this.isStackingView ? 1 : Math.max(1, Math.min(docList.length,
+        const rows = () => !this.isStackingView ? 1 : Math.max(1, Math.min(docList.length,
             Math.floor((this.props.PanelWidth() - 2 * this.xMargin) / (this.columnWidth + this.gridGap))));
         return <CollectionMasonryViewFieldRow
             key={heading ? heading.heading : ""}
@@ -356,9 +356,9 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
     }
 
     sortFunc = (a: [SchemaHeaderField, Doc[]], b: [SchemaHeaderField, Doc[]]): 1 | -1 => {
-        let descending = BoolCast(this.props.Document.stackingHeadersSortDescending);
-        let firstEntry = descending ? b : a;
-        let secondEntry = descending ? a : b;
+        const descending = BoolCast(this.props.Document.stackingHeadersSortDescending);
+        const firstEntry = descending ? b : a;
+        const secondEntry = descending ? a : b;
         return firstEntry[0].heading > secondEntry[0].heading ? 1 : -1;
     }
 
@@ -369,28 +369,28 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
     onContextMenu = (e: React.MouseEvent): void => {
         // need to test if propagation has stopped because GoldenLayout forces a parallel react hierarchy to be created for its top-level layout
         if (!e.isPropagationStopped()) {
-            let subItems: ContextMenuProps[] = [];
+            const subItems: ContextMenuProps[] = [];
             subItems.push({ description: `${this.props.Document.fillColumn ? "Variable Size" : "Autosize"} Column`, event: () => this.props.Document.fillColumn = !this.props.Document.fillColumn, icon: "plus" });
             subItems.push({ description: `${this.props.Document.showTitles ? "Hide Titles" : "Show Titles"}`, event: () => this.props.Document.showTitles = !this.props.Document.showTitles ? "title" : "", icon: "plus" });
             subItems.push({ description: `${this.props.Document.showCaptions ? "Hide Captions" : "Show Captions"}`, event: () => this.props.Document.showCaptions = !this.props.Document.showCaptions ? "caption" : "", icon: "plus" });
             ContextMenu.Instance.addItem({ description: "Stacking Options ...", subitems: subItems, icon: "eye" });
 
-            let existingOnClick = ContextMenu.Instance.findByDescription("OnClick...");
-            let onClicks: ContextMenuProps[] = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : [];
+            const existingOnClick = ContextMenu.Instance.findByDescription("OnClick...");
+            const onClicks: ContextMenuProps[] = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : [];
             onClicks.push({ description: "Edit onChildClick script", icon: "edit", event: (obj: any) => ScriptBox.EditButtonScript("On Child Clicked...", this.props.Document, "onChildClick", obj.x, obj.y) });
             !existingOnClick && ContextMenu.Instance.addItem({ description: "OnClick...", subitems: onClicks, icon: "hand-point-right" });
         }
     }
 
     render() {
-        let editableViewProps = {
+        const editableViewProps = {
             GetValue: () => "",
             SetValue: this.addGroup,
             contents: "+ ADD A GROUP"
         };
         let sections = [[undefined, this.filteredChildren] as [SchemaHeaderField | undefined, Doc[]]];
         if (this.sectionFilter) {
-            let entries = Array.from(this.Sections.entries());
+            const entries = Array.from(this.Sections.entries());
             sections = entries.sort(this.sortFunc);
         }
         return (
diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
index b9d334b10..80dc482af 100644
--- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
+++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
@@ -2,17 +2,14 @@ import React = require("react");
 import { library } from '@fortawesome/fontawesome-svg-core';
 import { faPalette } from '@fortawesome/free-solid-svg-icons';
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { action, observable, trace, runInAction } from "mobx";
+import { action, observable, runInAction } from "mobx";
 import { observer } from "mobx-react";
-import { Doc, WidthSym } from "../../../new_fields/Doc";
-import { Id } from "../../../new_fields/FieldSymbols";
+import { Doc } from "../../../new_fields/Doc";
 import { PastelSchemaPalette, SchemaHeaderField } from "../../../new_fields/SchemaHeaderField";
 import { ScriptField } from "../../../new_fields/ScriptField";
 import { NumCast, StrCast } from "../../../new_fields/Types";
-import { Utils } from "../../../Utils";
 import { Docs } from "../../documents/Documents";
 import { DragManager } from "../../util/DragManager";
-import { CompileScript } from "../../util/Scripting";
 import { SelectionManager } from "../../util/SelectionManager";
 import { Transform } from "../../util/Transform";
 import { undoBatch } from "../../util/UndoManager";
@@ -61,8 +58,8 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
     columnDrop = action((e: Event, de: DragManager.DropEvent) => {
         this._createAliasSelected = false;
         if (de.data instanceof DragManager.DocumentDragData) {
-            let key = StrCast(this.props.parent.props.Document.sectionFilter);
-            let castedValue = this.getValue(this._heading);
+            const key = StrCast(this.props.parent.props.Document.sectionFilter);
+            const castedValue = this.getValue(this._heading);
             if (castedValue) {
                 de.data.droppedDocuments.forEach(d => d[key] = castedValue);
             }
@@ -74,7 +71,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
         }
     });
     getValue = (value: string): any => {
-        let parsed = parseInt(value);
+        const parsed = parseInt(value);
         if (!isNaN(parsed)) {
             return parsed;
         }
@@ -90,8 +87,8 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
     @action
     headingChanged = (value: string, shiftDown?: boolean) => {
         this._createAliasSelected = false;
-        let key = StrCast(this.props.parent.props.Document.sectionFilter);
-        let castedValue = this.getValue(value);
+        const key = StrCast(this.props.parent.props.Document.sectionFilter);
+        const castedValue = this.getValue(value);
         if (castedValue) {
             if (this.props.parent.sectionHeaders) {
                 if (this.props.parent.sectionHeaders.map(i => i.heading).indexOf(castedValue.toString()) > -1) {
@@ -135,11 +132,11 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
     @action
     addDocument = (value: string, shiftDown?: boolean) => {
         this._createAliasSelected = false;
-        let key = StrCast(this.props.parent.props.Document.sectionFilter);
-        let newDoc = Docs.Create.TextDocument({ height: 18, width: 200, documentText: "@@@" + value, title: value, autoHeight: true });
+        const key = StrCast(this.props.parent.props.Document.sectionFilter);
+        const newDoc = Docs.Create.TextDocument({ height: 18, width: 200, documentText: "@@@" + value, title: value, autoHeight: true });
         newDoc[key] = this.getValue(this.props.heading);
-        let maxHeading = this.props.docList.reduce((maxHeading, doc) => NumCast(doc.heading) > maxHeading ? NumCast(doc.heading) : maxHeading, 0);
-        let heading = maxHeading === 0 || this.props.docList.length === 0 ? 1 : maxHeading === 1 ? 2 : 3;
+        const maxHeading = this.props.docList.reduce((maxHeading, doc) => NumCast(doc.heading) > maxHeading ? NumCast(doc.heading) : maxHeading, 0);
+        const heading = maxHeading === 0 || this.props.docList.length === 0 ? 1 : maxHeading === 1 ? 2 : 3;
         newDoc.heading = heading;
         return this.props.parent.props.addDocument(newDoc);
     }
@@ -147,10 +144,10 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
     @action
     deleteColumn = () => {
         this._createAliasSelected = false;
-        let key = StrCast(this.props.parent.props.Document.sectionFilter);
+        const key = StrCast(this.props.parent.props.Document.sectionFilter);
         this.props.docList.forEach(d => d[key] = undefined);
         if (this.props.parent.sectionHeaders && this.props.headingObject) {
-            let index = this.props.parent.sectionHeaders.indexOf(this.props.headingObject);
+            const index = this.props.parent.sectionHeaders.indexOf(this.props.headingObject);
             this.props.parent.sectionHeaders.splice(index, 1);
         }
     }
@@ -166,10 +163,10 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
     }
 
     startDrag = (e: PointerEvent) => {
-        let [dx, dy] = this.props.screenToLocalTransform().transformDirection(e.clientX - this._startDragPosition.x, e.clientY - this._startDragPosition.y);
+        const [dx, dy] = this.props.screenToLocalTransform().transformDirection(e.clientX - this._startDragPosition.x, e.clientY - this._startDragPosition.y);
         if (Math.abs(dx) + Math.abs(dy) > this._sensitivity) {
-            let alias = Doc.MakeAlias(this.props.parent.props.Document);
-            let key = StrCast(this.props.parent.props.Document.sectionFilter);
+            const alias = Doc.MakeAlias(this.props.parent.props.Document);
+            const key = StrCast(this.props.parent.props.Document.sectionFilter);
             let value = this.getValue(this._heading);
             value = typeof value === "string" ? `"${value}"` : value;
             alias.viewSpecScript = ScriptField.MakeFunction(`doc.${key} === ${value}`, { doc: Doc.name });
@@ -195,7 +192,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
         e.stopPropagation();
         e.preventDefault();
 
-        let [dx, dy] = this.props.screenToLocalTransform().transformDirection(e.clientX, e.clientY);
+        const [dx, dy] = this.props.screenToLocalTransform().transformDirection(e.clientX, e.clientY);
         this._startDragPosition = { x: dx, y: dy };
 
         if (this._createAliasSelected) {
@@ -208,17 +205,17 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
     }
 
     renderColorPicker = () => {
-        let selected = this.props.headingObject ? this.props.headingObject.color : "#f1efeb";
+        const selected = this.props.headingObject ? this.props.headingObject.color : "#f1efeb";
 
-        let pink = PastelSchemaPalette.get("pink2");
-        let purple = PastelSchemaPalette.get("purple4");
-        let blue = PastelSchemaPalette.get("bluegreen1");
-        let yellow = PastelSchemaPalette.get("yellow4");
-        let red = PastelSchemaPalette.get("red2");
-        let green = PastelSchemaPalette.get("bluegreen7");
-        let cyan = PastelSchemaPalette.get("bluegreen5");
-        let orange = PastelSchemaPalette.get("orange1");
-        let gray = "#f1efeb";
+        const pink = PastelSchemaPalette.get("pink2");
+        const purple = PastelSchemaPalette.get("purple4");
+        const blue = PastelSchemaPalette.get("bluegreen1");
+        const yellow = PastelSchemaPalette.get("yellow4");
+        const red = PastelSchemaPalette.get("red2");
+        const green = PastelSchemaPalette.get("bluegreen7");
+        const cyan = PastelSchemaPalette.get("bluegreen5");
+        const orange = PastelSchemaPalette.get("orange1");
+        const gray = "#f1efeb";
 
         return (
             <div className="collectionStackingView-colorPicker">
@@ -243,7 +240,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
     }
 
     renderMenu = () => {
-        let selected = this._createAliasSelected;
+        const selected = this._createAliasSelected;
         return (
             <div className="collectionStackingView-optionPicker">
                 <div className="optionOptions">
@@ -262,16 +259,16 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
     @observable _headingsHack: number = 1;
 
     render() {
-        let cols = this.props.cols();
-        let key = StrCast(this.props.parent.props.Document.sectionFilter);
+        const cols = this.props.cols();
+        const key = StrCast(this.props.parent.props.Document.sectionFilter);
         let templatecols = "";
-        let headings = this.props.headings();
-        let heading = this._heading;
-        let style = this.props.parent;
-        let singleColumn = style.isStackingView;
-        let uniqueHeadings = headings.map((i, idx) => headings.indexOf(i) === idx);
-        let evContents = heading ? heading : this.props.type && this.props.type === "number" ? "0" : `NO ${key.toUpperCase()} VALUE`;
-        let headerEditableViewProps = {
+        const headings = this.props.headings();
+        const heading = this._heading;
+        const style = this.props.parent;
+        const singleColumn = style.isStackingView;
+        const uniqueHeadings = headings.map((i, idx) => headings.indexOf(i) === idx);
+        const evContents = heading ? heading : this.props.type && this.props.type === "number" ? "0" : `NO ${key.toUpperCase()} VALUE`;
+        const headerEditableViewProps = {
             GetValue: () => evContents,
             SetValue: this.headingChanged,
             contents: evContents,
@@ -281,7 +278,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
             toggle: this.toggleVisibility,
             color: this._color
         };
-        let newEditableViewProps = {
+        const newEditableViewProps = {
             GetValue: () => "",
             SetValue: this.addDocument,
             contents: "+ NEW",
@@ -290,7 +287,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
             toggle: this.toggleVisibility,
             color: this._color
         };
-        let headingView = this.props.headingObject ?
+        const headingView = this.props.headingObject ?
             <div key={heading} className="collectionStackingView-sectionHeader" ref={this._headerRef}
                 style={{
                     width: (style.columnWidth) /
@@ -335,7 +332,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
                 </div>
             </div> : (null);
         for (let i = 0; i < cols; i++) templatecols += `${style.columnWidth / style.numGroupColumns}px `;
-        let chromeStatus = this.props.parent.props.Document.chromeStatus;
+        const chromeStatus = this.props.parent.props.Document.chromeStatus;
         return (
             <div className="collectionStackingViewFieldColumn" key={heading} style={{ width: `${100 / ((uniqueHeadings.length + ((chromeStatus !== 'view-mode' && chromeStatus !== 'disabled') ? 1 : 0)) || 1)}%`, background: this._background }}
                 ref={this.createColumnDropRef} onPointerEnter={this.pointerEntered} onPointerLeave={this.pointerLeave}>
diff --git a/src/client/views/collections/CollectionStaffView.tsx b/src/client/views/collections/CollectionStaffView.tsx
index 40e860b12..105061f46 100644
--- a/src/client/views/collections/CollectionStaffView.tsx
+++ b/src/client/views/collections/CollectionStaffView.tsx
@@ -2,7 +2,7 @@ import { CollectionSubView } from "./CollectionSubView";
 import { Transform } from "../../util/Transform";
 import React = require("react");
 import { computed, action, IReactionDisposer, reaction, runInAction, observable } from "mobx";
-import { Doc, HeightSym } from "../../../new_fields/Doc";
+import { Doc } from "../../../new_fields/Doc";
 import { NumCast } from "../../../new_fields/Types";
 import "./CollectionStaffView.scss";
 import { observer } from "mobx-react";
@@ -32,9 +32,9 @@ export class CollectionStaffView extends CollectionSubView(doc => doc) {
     }
 
     @computed get staves() {
-        let staves = [];
+        const staves = [];
         for (let i = 0; i < this._staves; i++) {
-            let rows = [];
+            const rows = [];
             for (let j = 0; j < 5; j++) {
                 rows.push(<div key={`staff-${i}-${j}`} className="collectionStaffView-line"></div>);
             }
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index e80825825..0a2e27165 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -19,7 +19,7 @@ import { FieldViewProps } from "../nodes/FieldView";
 import { FormattedTextBox, GoogleRef } from "../nodes/FormattedTextBox";
 import { CollectionView } from "./CollectionView";
 import React = require("react");
-var path = require('path');
+import { basename } from 'path';
 import { GooglePhotos } from "../../apis/google_docs/GooglePhotosClientUtils";
 import { ImageUtils } from "../../util/Import & Export/ImageUtils";
 import { Networking } from "../../Network";
@@ -92,7 +92,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
             return Cast(this.dataField, listSpec(Doc));
         }
         get childDocs() {
-            let docs = DocListCast(this.dataField);
+            const docs = DocListCast(this.dataField);
             const viewSpecScript = Cast(this.props.Document.viewSpecScript, ScriptField);
             return viewSpecScript ? docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result) : docs;
         }
@@ -100,10 +100,10 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
         @action
         protected async setCursorPosition(position: [number, number]) {
             let ind;
-            let doc = this.props.Document;
-            let id = CurrentUserUtils.id;
-            let email = Doc.CurrentUserEmail;
-            let pos = { x: position[0], y: position[1] };
+            const doc = this.props.Document;
+            const id = CurrentUserUtils.id;
+            const email = Doc.CurrentUserEmail;
+            const pos = { x: position[0], y: position[1] };
             if (id && email) {
                 const proto = Doc.GetProto(doc);
                 if (!proto) {
@@ -123,7 +123,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
                 if (cursors.length > 0 && (ind = cursors.findIndex(entry => entry.data.metadata.id === id)) > -1) {
                     cursors[ind].setPosition(pos);
                 } else {
-                    let entry = new CursorField({ metadata: { id: id, identifier: email, timestamp: Date.now() }, position: pos });
+                    const entry = new CursorField({ metadata: { id: id, identifier: email, timestamp: Date.now() }, position: pos });
                     cursors.push(entry);
                 }
             }
@@ -145,7 +145,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
                 if (de.data.dropAction || de.data.userDropAction) {
                     added = de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d) || added, false);
                 } else if (de.data.moveDocument) {
-                    let movedDocs = de.data.draggedDocuments;
+                    const movedDocs = de.data.draggedDocuments;
                     added = movedDocs.reduce((added: boolean, d, i) =>
                         de.data.droppedDocuments[i] !== d ? this.props.addDocument(de.data.droppedDocuments[i]) :
                             de.data.moveDocument(d, this.props.Document, this.props.addDocument) || added, false);
@@ -169,8 +169,8 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
                 e.stopPropagation(); // bcz: this is a hack to stop propagation when dropping an image on a text document with shift+ctrl
                 return;
             }
-            let html = e.dataTransfer.getData("text/html");
-            let text = e.dataTransfer.getData("text/plain");
+            const html = e.dataTransfer.getData("text/html");
+            const text = e.dataTransfer.getData("text/plain");
 
             if (text && text.startsWith("<div")) {
                 return;
@@ -179,9 +179,9 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
             e.preventDefault();
 
             if (html && FormattedTextBox.IsFragment(html)) {
-                let href = FormattedTextBox.GetHref(html);
+                const href = FormattedTextBox.GetHref(html);
                 if (href) {
-                    let docid = FormattedTextBox.GetDocFromUrl(href);
+                    const docid = FormattedTextBox.GetDocFromUrl(href);
                     if (docid) { // prosemirror text containing link to dash document
                         DocServer.GetRefField(docid).then(f => {
                             if (f instanceof Doc) {
@@ -198,19 +198,19 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
                 return;
             }
             if (html && !html.startsWith("<a")) {
-                let tags = html.split("<");
+                const tags = html.split("<");
                 if (tags[0] === "") tags.splice(0, 1);
-                let img = tags[0].startsWith("img") ? tags[0] : tags.length > 1 && tags[1].startsWith("img") ? tags[1] : "";
+                const img = tags[0].startsWith("img") ? tags[0] : tags.length > 1 && tags[1].startsWith("img") ? tags[1] : "";
                 if (img) {
-                    let split = img.split("src=\"")[1].split("\"")[0];
-                    let doc = Docs.Create.ImageDocument(split, { ...options, width: 300 });
+                    const split = img.split("src=\"")[1].split("\"")[0];
+                    const doc = Docs.Create.ImageDocument(split, { ...options, width: 300 });
                     ImageUtils.ExtractExif(doc);
                     this.props.addDocument(doc);
                     return;
                 } else {
-                    let path = window.location.origin + "/doc/";
+                    const path = window.location.origin + "/doc/";
                     if (text.startsWith(path)) {
-                        let docid = text.replace(Utils.prepend("/doc/"), "").split("?")[0];
+                        const docid = text.replace(Utils.prepend("/doc/"), "").split("?")[0];
                         DocServer.GetRefField(docid).then(f => {
                             if (f instanceof Doc) {
                                 if (options.x || options.y) { f.x = options.x; f.y = options.y; } // should be in CollectionFreeFormView
@@ -218,7 +218,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
                             }
                         });
                     } else {
-                        let htmlDoc = Docs.Create.HtmlDocument(html, { ...options, title: "-web page-", width: 300, height: 300, documentText: text });
+                        const htmlDoc = Docs.Create.HtmlDocument(html, { ...options, title: "-web page-", width: 300, height: 300, documentText: text });
                         this.props.addDocument(htmlDoc);
                     }
                     return;
@@ -231,8 +231,8 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
             }
             let matches: RegExpExecArray | null;
             if ((matches = /(https:\/\/)?docs\.google\.com\/document\/d\/([^\\]+)\/edit/g.exec(text)) !== null) {
-                let newBox = Docs.Create.TextDocument({ ...options, width: 400, height: 200, title: "Awaiting title from Google Docs..." });
-                let proto = newBox.proto!;
+                const newBox = Docs.Create.TextDocument({ ...options, width: 400, height: 200, title: "Awaiting title from Google Docs..." });
+                const proto = newBox.proto!;
                 const documentId = matches[2];
                 proto[GoogleRef] = documentId;
                 proto.data = "Please select this document and then click on its pull button to load its contents from from Google Docs...";
@@ -249,17 +249,17 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
                 const mediaItems = await GooglePhotos.Query.AlbumSearch(albumId);
                 console.log(mediaItems);
             }
-            let batch = UndoManager.StartBatch("collection view drop");
-            let promises: Promise<void>[] = [];
+            const batch = UndoManager.StartBatch("collection view drop");
+            const promises: Promise<void>[] = [];
             // tslint:disable-next-line:prefer-for-of
             for (let i = 0; i < e.dataTransfer.items.length; i++) {
-                let item = e.dataTransfer.items[i];
+                const item = e.dataTransfer.items[i];
                 if (item.kind === "string" && item.type.indexOf("uri") !== -1) {
                     let str: string;
-                    let prom = new Promise<string>(resolve => e.dataTransfer.items[i].getAsString(resolve))
+                    const prom = new Promise<string>(resolve => e.dataTransfer.items[i].getAsString(resolve))
                         .then(action((s: string) => rp.head(Utils.CorsProxy(str = s))))
                         .then(result => {
-                            let type = result["content-type"];
+                            const type = result["content-type"];
                             if (type) {
                                 Docs.Get.DocumentFromType(type, str, { ...options, width: 300, nativeWidth: type.indexOf("video") !== -1 ? 600 : 300 })
                                     .then(doc => doc && this.props.addDocument(doc));
@@ -267,23 +267,23 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
                         });
                     promises.push(prom);
                 }
-                let type = item.type;
+                const type = item.type;
                 if (item.kind === "file") {
-                    let file = item.getAsFile();
-                    let formData = new FormData();
+                    const file = item.getAsFile();
+                    const formData = new FormData();
 
                     if (!file || !file.type) {
                         continue;
                     }
 
                     formData.append('file', file);
-                    let dropFileName = file ? file.name : "-empty-";
+                    const dropFileName = file ? file.name : "-empty-";
                     promises.push(Networking.PostFormDataToServer("/upload", formData).then(results => {
                         results.map(action(({ clientAccessPath }: any) => {
-                            let full = { ...options, nativeWidth: type.indexOf("video") !== -1 ? 600 : 300, width: 300, title: dropFileName };
-                            let pathname = Utils.prepend(clientAccessPath);
+                            const full = { ...options, nativeWidth: type.indexOf("video") !== -1 ? 600 : 300, width: 300, title: dropFileName };
+                            const pathname = Utils.prepend(clientAccessPath);
                             Docs.Get.DocumentFromType(type, pathname, full).then(doc => {
-                                doc && (Doc.GetProto(doc).fileUpload = path.basename(pathname).replace("upload_", "").replace(/\.[a-z0-9]*$/, ""));
+                                doc && (Doc.GetProto(doc).fileUpload = basename(pathname).replace("upload_", "").replace(/\.[a-z0-9]*$/, ""));
                                 doc && this.props.addDocument(doc);
                             });
                         }));
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index c4b7e2d31..48ea35c6b 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -3,7 +3,7 @@ import { faAngleRight, faArrowsAltH, faBell, faCamera, faCaretDown, faCaretRight
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { action, computed, observable } from "mobx";
 import { observer } from "mobx-react";
-import { Doc, DocListCast, Field, HeightSym, Opt, WidthSym } from '../../../new_fields/Doc';
+import { Doc, DocListCast, Field, HeightSym, WidthSym } from '../../../new_fields/Doc';
 import { Id } from '../../../new_fields/FieldSymbols';
 import { List } from '../../../new_fields/List';
 import { Document, listSpec } from '../../../new_fields/Schema';
@@ -95,11 +95,11 @@ class TreeView extends React.Component<TreeViewProps> {
     @computed get MAX_EMBED_HEIGHT() { return NumCast(this.props.document.maxEmbedHeight, 300); }
     @computed get dataDoc() { return this.templateDataDoc ? this.templateDataDoc : this.props.document; }
     @computed get fieldKey() {
-        let splits = StrCast(Doc.LayoutField(this.props.document)).split("fieldKey={\"");
+        const splits = StrCast(Doc.LayoutField(this.props.document)).split("fieldKey={\"");
         return splits.length > 1 ? splits[1].split("\"")[0] : "data";
     }
     childDocList(field: string) {
-        let layout = Doc.LayoutField(this.props.document) instanceof Doc ? Doc.LayoutField(this.props.document) as Doc : undefined;
+        const layout = Doc.LayoutField(this.props.document) instanceof Doc ? Doc.LayoutField(this.props.document) as Doc : undefined;
         return ((this.props.dataDoc ? Cast(this.props.dataDoc[field], listSpec(Doc)) : undefined) ||
             (layout ? Cast(layout[field], listSpec(Doc)) : undefined) ||
             Cast(this.props.document[field], listSpec(Doc))) as Doc[];
@@ -149,10 +149,10 @@ class TreeView extends React.Component<TreeViewProps> {
     }
     onDragMove = (e: PointerEvent): void => {
         Doc.UnBrushDoc(this.dataDoc);
-        let pt = [e.clientX, e.clientY]
-        let rect = this._header!.current!.getBoundingClientRect();
-        let before = pt[1] < rect.top + rect.height / 2;
-        let inside = pt[0] > Math.min(rect.left + 75, rect.left + rect.width * .75) || (!before && this.treeViewOpen && DocListCast(this.dataDoc[this.fieldKey]).length);
+        const pt = [e.clientX, e.clientY];
+        const rect = this._header!.current!.getBoundingClientRect();
+        const before = pt[1] < rect.top + rect.height / 2;
+        const inside = pt[0] > Math.min(rect.left + 75, rect.left + rect.width * .75) || (!before && this.treeViewOpen && DocListCast(this.dataDoc[this.fieldKey]).length);
         this._header!.current!.className = "treeViewItem-header";
         if (inside) this._header!.current!.className += " treeViewItem-header-inside";
         else if (before) this._header!.current!.className += " treeViewItem-header-above";
@@ -172,8 +172,8 @@ class TreeView extends React.Component<TreeViewProps> {
         SetValue={undoBatch((value: string) => Doc.SetInPlace(this.props.document, key, value, false) || true)}
         OnFillDown={undoBatch((value: string) => {
             Doc.SetInPlace(this.props.document, key, value, false);
-            let layoutDoc = this.props.document.layoutCustom instanceof Doc ? Doc.ApplyTemplate(Doc.GetProto(this.props.document.layoutCustom)) : undefined;
-            let doc = layoutDoc || Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, width: 100, height: 25, templates: new List<string>([Templates.Title.Layout]) });
+            const layoutDoc = this.props.document.layoutCustom instanceof Doc ? Doc.ApplyTemplate(Doc.GetProto(this.props.document.layoutCustom)) : undefined;
+            const doc = layoutDoc || Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, width: 100, height: 25, templates: new List<string>([Templates.Title.Layout]) });
             TreeView.loadId = doc[Id];
             return this.props.addDocument(doc);
         })}
@@ -205,7 +205,7 @@ class TreeView extends React.Component<TreeViewProps> {
                 ContextMenu.Instance.addItem({ description: "Delete Workspace", event: () => this.props.deleteDoc(this.props.document), icon: "trash-alt" });
                 ContextMenu.Instance.addItem({ description: "Create New Workspace", event: () => MainView.Instance.createNewWorkspace(), icon: "plus" });
             }
-            ContextMenu.Instance.addItem({ description: "Open Fields", event: () => { let kvp = Docs.Create.KVPDocument(this.props.document, { width: 300, height: 300 }); this.props.addDocTab(kvp, this.props.dataDoc ? this.props.dataDoc : kvp, "onRight"); }, icon: "layer-group" });
+            ContextMenu.Instance.addItem({ description: "Open Fields", event: () => { const kvp = Docs.Create.KVPDocument(this.props.document, { width: 300, height: 300 }); this.props.addDocTab(kvp, this.props.dataDoc ? this.props.dataDoc : kvp, "onRight"); }, icon: "layer-group" });
             ContextMenu.Instance.addItem({ description: "Publish", event: () => DocUtils.Publish(this.props.document, StrCast(this.props.document.title), () => { }, () => { }), icon: "file" });
             ContextMenu.Instance.displayMenu(e.pageX > 156 ? e.pageX - 156 : 0, e.pageY - 15);
             e.stopPropagation();
@@ -215,13 +215,13 @@ class TreeView extends React.Component<TreeViewProps> {
 
     @undoBatch
     treeDrop = (e: Event, de: DragManager.DropEvent) => {
-        let pt = [de.x, de.y];
-        let rect = this._header!.current!.getBoundingClientRect();
-        let before = pt[1] < rect.top + rect.height / 2;
-        let inside = pt[0] > Math.min(rect.left + 75, rect.left + rect.width * .75) || (!before && this.treeViewOpen && DocListCast(this.dataDoc[this.fieldKey]).length);
+        const pt = [de.x, de.y];
+        const rect = this._header!.current!.getBoundingClientRect();
+        const before = pt[1] < rect.top + rect.height / 2;
+        const inside = pt[0] > Math.min(rect.left + 75, rect.left + rect.width * .75) || (!before && this.treeViewOpen && DocListCast(this.dataDoc[this.fieldKey]).length);
         if (de.data instanceof DragManager.LinkDragData) {
-            let sourceDoc = de.data.linkSourceDocument;
-            let destDoc = this.props.document;
+            const sourceDoc = de.data.linkSourceDocument;
+            const destDoc = this.props.document;
             DocUtils.MakeLink({ doc: sourceDoc }, { doc: destDoc });
             e.stopPropagation();
         }
@@ -232,7 +232,7 @@ class TreeView extends React.Component<TreeViewProps> {
             if (inside) {
                 addDoc = (doc: Doc) => Doc.AddDocToList(this.dataDoc, this.fieldKey, doc) || addDoc(doc);
             }
-            let movedDocs = (de.data.options === this.props.treeViewId ? de.data.draggedDocuments : de.data.droppedDocuments);
+            const movedDocs = (de.data.options === this.props.treeViewId ? de.data.draggedDocuments : de.data.droppedDocuments);
             return ((de.data.dropAction && (de.data.options !== this.props.treeViewId)) || de.data.userDropAction) ?
                 de.data.droppedDocuments.reduce((added, d) => addDoc(d) || added, false)
                 : de.data.moveDocument ?
@@ -243,23 +243,23 @@ class TreeView extends React.Component<TreeViewProps> {
     }
 
     docTransform = () => {
-        let { scale, translateX, translateY } = Utils.GetScreenTransform(this._dref.current!);
-        let outerXf = this.props.outerXf();
-        let offset = this.props.ScreenToLocalTransform().transformDirection(outerXf.translateX - translateX, outerXf.translateY - translateY);
-        let finalXf = this.props.ScreenToLocalTransform().translate(offset[0], offset[1] + (this.props.ChromeHeight && this.props.ChromeHeight() < 0 ? this.props.ChromeHeight() : 0));
+        const { scale, translateX, translateY } = Utils.GetScreenTransform(this._dref.current!);
+        const outerXf = this.props.outerXf();
+        const offset = this.props.ScreenToLocalTransform().transformDirection(outerXf.translateX - translateX, outerXf.translateY - translateY);
+        const finalXf = this.props.ScreenToLocalTransform().translate(offset[0], offset[1] + (this.props.ChromeHeight && this.props.ChromeHeight() < 0 ? this.props.ChromeHeight() : 0));
         return finalXf;
     }
     docWidth = () => {
-        let layoutDoc = Doc.Layout(this.props.document);
-        let aspect = NumCast(layoutDoc.nativeHeight) / NumCast(layoutDoc.nativeWidth);
+        const layoutDoc = Doc.Layout(this.props.document);
+        const aspect = NumCast(layoutDoc.nativeHeight) / NumCast(layoutDoc.nativeWidth);
         if (aspect) return Math.min(layoutDoc[WidthSym](), Math.min(this.MAX_EMBED_HEIGHT / aspect, this.props.panelWidth() - 20));
         return NumCast(layoutDoc.nativeWidth) ? Math.min(layoutDoc[WidthSym](), this.props.panelWidth() - 20) : this.props.panelWidth() - 20;
     }
     docHeight = () => {
-        let layoutDoc = Doc.Layout(this.props.document);
-        let bounds = this.boundsOfCollectionDocument;
+        const layoutDoc = Doc.Layout(this.props.document);
+        const bounds = this.boundsOfCollectionDocument;
         return Math.min(this.MAX_EMBED_HEIGHT, (() => {
-            let aspect = NumCast(layoutDoc.nativeHeight) / NumCast(layoutDoc.nativeWidth, 1);
+            const aspect = NumCast(layoutDoc.nativeHeight) / NumCast(layoutDoc.nativeWidth, 1);
             if (aspect) return this.docWidth() * aspect;
             if (bounds) return this.docWidth() * (bounds.b - bounds.y) / (bounds.r - bounds.x);
             return layoutDoc.fitWidth ? (!this.props.document.nativeHeight ? NumCast(this.props.containingCollection.height) :
@@ -270,18 +270,18 @@ class TreeView extends React.Component<TreeViewProps> {
     }
 
     @computed get expandedField() {
-        let ids: { [key: string]: string } = {};
-        let doc = this.props.document;
+        const ids: { [key: string]: string } = {};
+        const doc = this.props.document;
         doc && Object.keys(doc).forEach(key => !(key in ids) && doc[key] !== ComputedField.undefined && (ids[key] = key));
 
-        let rows: JSX.Element[] = [];
-        for (let key of Object.keys(ids).slice().sort()) {
-            let contents = doc[key];
+        const rows: JSX.Element[] = [];
+        for (const key of Object.keys(ids).slice().sort()) {
+            const contents = doc[key];
             let contentElement: (JSX.Element | null)[] | JSX.Element = [];
 
             if (contents instanceof Doc || Cast(contents, listSpec(Doc))) {
-                let remDoc = (doc: Doc) => this.remove(doc, key);
-                let addDoc = (doc: Doc, addBefore?: Doc, before?: boolean) => Doc.AddDocToList(this.dataDoc, key, doc, addBefore, before, false, true);
+                const remDoc = (doc: Doc) => this.remove(doc, key);
+                const addDoc = (doc: Doc, addBefore?: Doc, before?: boolean) => Doc.AddDocToList(this.dataDoc, key, doc, addBefore, before, false, true);
                 contentElement = TreeView.GetChildElements(contents instanceof Doc ? [contents] :
                     DocListCast(contents), this.props.treeViewId, doc, undefined, key, this.props.containingCollection, this.props.prevSibling, addDoc, remDoc, this.move,
                     this.props.dropAction, this.props.addDocTab, this.props.pinToPres, this.props.ScreenToLocalTransform, this.props.outerXf, this.props.active,
@@ -310,9 +310,9 @@ class TreeView extends React.Component<TreeViewProps> {
     @computed get renderContent() {
         const expandKey = this.treeViewExpandedView === this.fieldKey ? this.fieldKey : this.treeViewExpandedView === "links" ? "links" : undefined;
         if (expandKey !== undefined) {
-            let remDoc = (doc: Doc) => this.remove(doc, expandKey);
-            let addDoc = (doc: Doc, addBefore?: Doc, before?: boolean) => Doc.AddDocToList(this.dataDoc, expandKey, doc, addBefore, before, false, true);
-            let docs = expandKey === "links" ? this.childLinks : this.childDocs;
+            const remDoc = (doc: Doc) => this.remove(doc, expandKey);
+            const addDoc = (doc: Doc, addBefore?: Doc, before?: boolean) => Doc.AddDocToList(this.dataDoc, expandKey, doc, addBefore, before, false, true);
+            const docs = expandKey === "links" ? this.childLinks : this.childDocs;
             return <ul key={expandKey + "more"}>
                 {!docs ? (null) :
                     TreeView.GetChildElements(docs, this.props.treeViewId, Doc.Layout(this.props.document),
@@ -326,7 +326,7 @@ class TreeView extends React.Component<TreeViewProps> {
                 {this.expandedField}
             </div></ul>;
         } else {
-            let layoutDoc = Doc.Layout(this.props.document);
+            const layoutDoc = Doc.Layout(this.props.document);
             return <div ref={this._dref} style={{ display: "inline-block", height: this.docHeight() }} key={this.props.document[Id] + this.props.document.title}>
                 <ContentFittingDocumentView
                     Document={layoutDoc}
@@ -363,10 +363,10 @@ class TreeView extends React.Component<TreeViewProps> {
      */
     @computed
     get renderTitle() {
-        let reference = React.createRef<HTMLDivElement>();
-        let onItemDown = SetupDrag(reference, () => this.dataDoc, this.move, this.props.dropAction, this.props.treeViewId, true);
+        const reference = React.createRef<HTMLDivElement>();
+        const onItemDown = SetupDrag(reference, () => this.dataDoc, this.move, this.props.dropAction, this.props.treeViewId, true);
 
-        let headerElements = (
+        const headerElements = (
             <span className="collectionTreeView-keyHeader" key={this.treeViewExpandedView}
                 onPointerDown={action(() => {
                     if (this.treeViewOpen) {
@@ -379,7 +379,7 @@ class TreeView extends React.Component<TreeViewProps> {
                 })}>
                 {this.treeViewExpandedView}
             </span>);
-        let openRight = (<div className="treeViewItem-openRight" onPointerDown={this.onPointerDown} onClick={this.openRight}>
+        const openRight = (<div className="treeViewItem-openRight" onPointerDown={this.onPointerDown} onClick={this.openRight}>
             <FontAwesomeIcon title="open in pane on right" icon="angle-right" size="lg" />
         </div>);
         return <>
@@ -440,28 +440,28 @@ class TreeView extends React.Component<TreeViewProps> {
             childDocs = childDocs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result);
         }
 
-        let docs = childDocs.slice();
-        let dataExtension = containingCollection[key + "_ext"] as Doc;
-        let ascending = dataExtension && BoolCast(dataExtension.sortAscending, null);
+        const docs = childDocs.slice();
+        const dataExtension = containingCollection[key + "_ext"] as Doc;
+        const ascending = dataExtension && BoolCast(dataExtension.sortAscending, null);
         if (ascending !== undefined) {
 
-            let sortAlphaNum = (a: string, b: string): 0 | 1 | -1 => {
-                var reN = /[0-9]*$/;
-                var aA = a.replace(reN, ""); // get rid of trailing numbers
-                var bA = b.replace(reN, "");
+            const sortAlphaNum = (a: string, b: string): 0 | 1 | -1 => {
+                const reN = /[0-9]*$/;
+                const aA = a.replace(reN, ""); // get rid of trailing numbers
+                const bA = b.replace(reN, "");
                 if (aA === bA) {  // if header string matches, then compare numbers numerically
-                    var aN = parseInt(a.match(reN)![0], 10);
-                    var bN = parseInt(b.match(reN)![0], 10);
+                    const aN = parseInt(a.match(reN)![0], 10);
+                    const bN = parseInt(b.match(reN)![0], 10);
                     return aN === bN ? 0 : aN > bN ? 1 : -1;
                 } else {
                     return aA > bA ? 1 : -1;
                 }
-            }
+            };
             docs.sort(function (a, b): 0 | 1 | -1 {
-                let descA = ascending ? b : a;
-                let descB = ascending ? a : b;
-                let first = descA.title;
-                let second = descB.title;
+                const descA = ascending ? b : a;
+                const descB = ascending ? a : b;
+                const first = descA.title;
+                const second = descB.title;
                 // TODO find better way to sort how to sort..................
                 if (typeof first === 'number' && typeof second === 'number') {
                     return (first - second) > 0 ? 1 : -1;
@@ -479,17 +479,17 @@ class TreeView extends React.Component<TreeViewProps> {
             });
         }
 
-        let rowWidth = () => panelWidth() - 20;
+        const rowWidth = () => panelWidth() - 20;
         return docs.map((child, i) => {
             const pair = Doc.GetLayoutDataDocPair(containingCollection, dataDoc, key, child);
             if (!pair.layout || pair.data instanceof Promise) {
                 return (null);
             }
 
-            let indent = i === 0 ? undefined : () => {
+            const indent = i === 0 ? undefined : () => {
                 if (StrCast(docs[i - 1].layout).indexOf("fieldKey") !== -1) {
-                    let fieldKeysub = StrCast(docs[i - 1].layout).split("fieldKey")[1];
-                    let fieldKey = fieldKeysub.split("\"")[1];
+                    const fieldKeysub = StrCast(docs[i - 1].layout).split("fieldKey")[1];
+                    const fieldKey = fieldKeysub.split("\"")[1];
                     if (fieldKey && Cast(docs[i - 1][fieldKey], listSpec(Doc)) !== undefined) {
                         Doc.AddDocToList(docs[i - 1], fieldKey, child);
                         docs[i - 1].treeViewOpen = true;
@@ -497,21 +497,21 @@ class TreeView extends React.Component<TreeViewProps> {
                     }
                 }
             };
-            let outdent = !parentCollectionDoc ? undefined : () => {
+            const outdent = !parentCollectionDoc ? undefined : () => {
                 if (StrCast(parentCollectionDoc.layout).indexOf("fieldKey") !== -1) {
-                    let fieldKeysub = StrCast(parentCollectionDoc.layout).split("fieldKey")[1];
-                    let fieldKey = fieldKeysub.split("\"")[1];
+                    const fieldKeysub = StrCast(parentCollectionDoc.layout).split("fieldKey")[1];
+                    const fieldKey = fieldKeysub.split("\"")[1];
                     Doc.AddDocToList(parentCollectionDoc, fieldKey, child, parentPrevSibling, false);
                     parentCollectionDoc.treeViewOpen = true;
                     remove(child);
                 }
             };
-            let addDocument = (doc: Doc, relativeTo?: Doc, before?: boolean) => {
+            const addDocument = (doc: Doc, relativeTo?: Doc, before?: boolean) => {
                 return add(doc, relativeTo ? relativeTo : docs[i], before !== undefined ? before : false);
             };
             const childLayout = Doc.Layout(pair.layout);
-            let rowHeight = () => {
-                let aspect = NumCast(childLayout.nativeWidth, 0) / NumCast(childLayout.nativeHeight, 0);
+            const rowHeight = () => {
+                const aspect = NumCast(childLayout.nativeWidth, 0) / NumCast(childLayout.nativeHeight, 0);
                 return aspect ? Math.min(childLayout[WidthSym](), rowWidth()) / aspect : childLayout[HeightSym]();
             };
             return !(child instanceof Doc) ? (null) : <TreeView
@@ -566,7 +566,7 @@ export class CollectionTreeView extends CollectionSubView(Document) {
 
     @action
     remove = (document: Document): boolean => {
-        let children = Cast(this.props.Document[this.props.fieldKey], listSpec(Doc), []);
+        const children = Cast(this.props.Document[this.props.fieldKey], listSpec(Doc), []);
         if (children.indexOf(document) !== -1) {
             children.splice(children.indexOf(document), 1);
             return true;
@@ -587,7 +587,7 @@ export class CollectionTreeView extends CollectionSubView(Document) {
             e.preventDefault();
             ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15);
         } else {
-            let layoutItems: ContextMenuProps[] = [];
+            const layoutItems: ContextMenuProps[] = [];
             layoutItems.push({ description: (this.props.Document.preventTreeViewOpen ? "Persist" : "Abandon") + "Treeview State", event: () => this.props.Document.preventTreeViewOpen = !this.props.Document.preventTreeViewOpen, icon: "paint-brush" });
             layoutItems.push({ description: (this.props.Document.hideHeaderFields ? "Show" : "Hide") + " Header Fields", event: () => this.props.Document.hideHeaderFields = !this.props.Document.hideHeaderFields, icon: "paint-brush" });
             ContextMenu.Instance.addItem({ description: "Treeview Options ...", subitems: layoutItems, icon: "eye" });
@@ -606,9 +606,9 @@ export class CollectionTreeView extends CollectionSubView(Document) {
     }
 
     render() {
-        let dropAction = StrCast(this.props.Document.dropAction) as dropActionType;
-        let addDoc = (doc: Doc, relativeTo?: Doc, before?: boolean) => Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, relativeTo, before, false, false, false);
-        let moveDoc = (d: Doc, target: Doc, addDoc: (doc: Doc) => boolean) => this.props.moveDocument(d, target, addDoc);
+        const dropAction = StrCast(this.props.Document.dropAction) as dropActionType;
+        const addDoc = (doc: Doc, relativeTo?: Doc, before?: boolean) => Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, relativeTo, before, false, false, false);
+        const moveDoc = (d: Doc, target: Doc, addDoc: (doc: Doc) => boolean) => this.props.moveDocument(d, target, addDoc);
         return !this.childDocs ? (null) : (
             <div id="body" className="collectionTreeView-dropTarget"
                 style={{ overflow: "auto", background: StrCast(this.props.Document.backgroundColor, "lightgray"), paddingTop: `${NumCast(this.props.Document.yMargin, 20)}px` }}
@@ -625,8 +625,8 @@ export class CollectionTreeView extends CollectionSubView(Document) {
                     SetValue={undoBatch((value: string) => Doc.SetInPlace(this.dataDoc, "title", value, false) || true)}
                     OnFillDown={undoBatch((value: string) => {
                         Doc.SetInPlace(this.dataDoc, "title", value, false);
-                        let layoutDoc = this.props.Document.layoutCustom instanceof Doc ? Doc.ApplyTemplate(Doc.GetProto(this.props.Document.layoutCustom)) : undefined;
-                        let doc = layoutDoc || Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, width: 100, height: 25, templates: new List<string>([Templates.Title.Layout]) });
+                        const layoutDoc = this.props.Document.layoutCustom instanceof Doc ? Doc.ApplyTemplate(Doc.GetProto(this.props.Document.layoutCustom)) : undefined;
+                        const doc = layoutDoc || Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, width: 100, height: 25, templates: new List<string>([Templates.Title.Layout]) });
                         TreeView.loadId = doc[Id];
                         Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, this.childDocs.length ? this.childDocs[0] : undefined, true, false, false, false);
                     })} />
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 4c49054d2..54f5a2c42 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -84,7 +84,7 @@ export class CollectionView extends Touchable<FieldViewProps> {
     public static SetSafeMode(safeMode: boolean) { this._safeMode = safeMode; }
 
     get collectionViewType(): CollectionViewType | undefined {
-        let viewField = Cast(this.props.Document.viewType, "number");
+        const viewField = Cast(this.props.Document.viewType, "number");
         if (CollectionView._safeMode) {
             if (viewField === CollectionViewType.Freeform) {
                 return CollectionViewType.Tree;
@@ -101,7 +101,7 @@ export class CollectionView extends Touchable<FieldViewProps> {
             () => {
                 // chrome status is one of disabled, collapsed, or visible. this determines initial state from document
                 // chrome status may also be view-mode, in reference to stacking view's toggle mode. it is essentially disabled mode, but prevents the toggle button from showing up on the left sidebar.
-                let chromeStatus = this.props.Document.chromeStatus;
+                const chromeStatus = this.props.Document.chromeStatus;
                 if (chromeStatus && (chromeStatus === "disabled" || chromeStatus === "collapsed")) {
                     runInAction(() => this._collapsed = true);
                 }
@@ -119,9 +119,9 @@ export class CollectionView extends Touchable<FieldViewProps> {
 
     @action.bound
     addDocument(doc: Doc): boolean {
-        let targetDataDoc = Doc.GetProto(this.props.Document);
+        const targetDataDoc = Doc.GetProto(this.props.Document);
         Doc.AddDocToList(targetDataDoc, this.props.fieldKey, doc);
-        let extension = Doc.fieldExtensionDoc(targetDataDoc, this.props.fieldKey);  // set metadata about the field being rendered (ie, the set of documents) on an extension field for that field
+        const extension = Doc.fieldExtensionDoc(targetDataDoc, this.props.fieldKey);  // set metadata about the field being rendered (ie, the set of documents) on an extension field for that field
         extension && (extension.lastModified = new DateField(new Date(Date.now())));
         Doc.GetProto(doc).lastOpened = new DateField;
         return true;
@@ -129,9 +129,9 @@ export class CollectionView extends Touchable<FieldViewProps> {
 
     @action.bound
     removeDocument(doc: Doc): boolean {
-        let docView = DocumentManager.Instance.getDocumentView(doc, this.props.ContainingCollectionView);
+        const docView = DocumentManager.Instance.getDocumentView(doc, this.props.ContainingCollectionView);
         docView && SelectionManager.DeselectDoc(docView);
-        let value = Cast(this.props.Document[this.props.fieldKey], listSpec(Doc), []);
+        const value = Cast(this.props.Document[this.props.fieldKey], listSpec(Doc), []);
         let index = value.reduce((p, v, i) => (v instanceof Doc && v === doc) ? i : p, -1);
         index = index !== -1 ? index : value.reduce((p, v, i) => (v instanceof Doc && Doc.AreProtosEqual(v, doc)) ? i : p, -1);
 
@@ -163,7 +163,7 @@ export class CollectionView extends Touchable<FieldViewProps> {
     }
 
     private SubViewHelper = (type: CollectionViewType, renderProps: CollectionRenderProps) => {
-        let props = { ...this.props, ...renderProps, chromeCollapsed: this._collapsed, ChromeHeight: this.chromeHeight, CollectionView: this, annotationsKey: "" };
+        const props = { ...this.props, ...renderProps, chromeCollapsed: this._collapsed, ChromeHeight: this.chromeHeight, CollectionView: this, annotationsKey: "" };
         switch (type) {
             case CollectionViewType.Schema: return (<CollectionSchemaView key="collview" {...props} />);
             case CollectionViewType.Docking: return (<CollectionDockingView key="collview" {...props} />);
@@ -186,7 +186,7 @@ export class CollectionView extends Touchable<FieldViewProps> {
 
     private SubView = (type: CollectionViewType, renderProps: CollectionRenderProps) => {
         // currently cant think of a reason for collection docking view to have a chrome. mind may change if we ever have nested docking views -syip
-        let chrome = this.props.Document.chromeStatus === "disabled" || type === CollectionViewType.Docking ? (null) :
+        const chrome = this.props.Document.chromeStatus === "disabled" || type === CollectionViewType.Docking ? (null) :
             <CollectionViewBaseChrome CollectionView={this} key="chrome" type={type} collapse={this.collapse} />;
         return [chrome, this.SubViewHelper(type, renderProps)];
     }
@@ -194,8 +194,8 @@ export class CollectionView extends Touchable<FieldViewProps> {
 
     onContextMenu = (e: React.MouseEvent): void => {
         if (!e.isPropagationStopped() && this.props.Document[Id] !== CurrentUserUtils.MainDocId) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7
-            let existingVm = ContextMenu.Instance.findByDescription("View Modes...");
-            let subItems = existingVm && "subitems" in existingVm ? existingVm.subitems : [];
+            const existingVm = ContextMenu.Instance.findByDescription("View Modes...");
+            const subItems = existingVm && "subitems" in existingVm ? existingVm.subitems : [];
             subItems.push({ description: "Freeform", event: () => { this.props.Document.viewType = CollectionViewType.Freeform; }, icon: "signature" });
             if (CollectionView._safeMode) {
                 ContextMenu.Instance.addItem({ description: "Test Freeform", event: () => this.props.Document.viewType = CollectionViewType.Invalid, icon: "project-diagram" });
@@ -221,13 +221,13 @@ export class CollectionView extends Touchable<FieldViewProps> {
             subItems.push({ description: "lightbox", event: action(() => this._isLightboxOpen = true), icon: "eye" });
             !existingVm && ContextMenu.Instance.addItem({ description: "View Modes...", subitems: subItems, icon: "eye" });
 
-            let existing = ContextMenu.Instance.findByDescription("Layout...");
-            let layoutItems = existing && "subitems" in existing ? existing.subitems : [];
+            const existing = ContextMenu.Instance.findByDescription("Layout...");
+            const layoutItems = existing && "subitems" in existing ? existing.subitems : [];
             layoutItems.push({ description: `${this.props.Document.forceActive ? "Select" : "Force"} Contents Active`, event: () => this.props.Document.forceActive = !this.props.Document.forceActive, icon: "project-diagram" });
             !existing && ContextMenu.Instance.addItem({ description: "Layout...", subitems: layoutItems, icon: "hand-point-right" });
 
-            let more = ContextMenu.Instance.findByDescription("More...");
-            let moreItems = more && "subitems" in more ? more.subitems : [];
+            const more = ContextMenu.Instance.findByDescription("More...");
+            const moreItems = more && "subitems" in more ? more.subitems : [];
             moreItems.push({ description: "Export Image Hierarchy", icon: "columns", event: () => ImageUtils.ExportHierarchyToFileSystem(this.props.Document) });
             !more && ContextMenu.Instance.addItem({ description: "More...", subitems: moreItems, icon: "hand-point-right" });
         }
diff --git a/src/client/views/collections/CollectionViewChromes.tsx b/src/client/views/collections/CollectionViewChromes.tsx
index 06fca7c38..4161e5d6e 100644
--- a/src/client/views/collections/CollectionViewChromes.tsx
+++ b/src/client/views/collections/CollectionViewChromes.tsx
@@ -13,7 +13,6 @@ import { DragManager } from "../../util/DragManager";
 import { undoBatch } from "../../util/UndoManager";
 import { EditableView } from "../EditableView";
 import { COLLECTION_BORDER_WIDTH } from "../globalCssVariables.scss";
-import { DocLike } from "../MetadataEntryMenu";
 import { CollectionViewType } from "./CollectionView";
 import { CollectionView } from "./CollectionView";
 import "./CollectionViewChromes.scss";
@@ -33,7 +32,7 @@ interface Filter {
     contains: boolean;
 }
 
-let stopPropagation = (e: React.SyntheticEvent) => e.stopPropagation();
+const stopPropagation = (e: React.SyntheticEvent) => e.stopPropagation();
 
 @observer
 export class CollectionViewBaseChrome extends React.Component<CollectionViewChromeProps> {
@@ -80,11 +79,11 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
     @computed private get filterValue() { return Cast(this.props.CollectionView.props.Document.viewSpecScript, ScriptField); }
 
     getFilters = (script: string) => {
-        let re: any = /(!)?\(\(\(doc\.(\w+)\s+&&\s+\(doc\.\w+\s+as\s+\w+\)\.includes\(\"(\w+)\"\)/g;
-        let arr: any[] = re.exec(script);
-        let toReturn: Filter[] = [];
+        const re: any = /(!)?\(\(\(doc\.(\w+)\s+&&\s+\(doc\.\w+\s+as\s+\w+\)\.includes\(\"(\w+)\"\)/g;
+        const arr: any[] = re.exec(script);
+        const toReturn: Filter[] = [];
         if (arr !== null) {
-            let filter: Filter = {
+            const filter: Filter = {
                 key: arr[2],
                 value: arr[3],
                 contains: (arr[1] === "!") ? false : true,
@@ -120,14 +119,14 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
 
         let fields: Filter[] = [];
         if (this.filterValue) {
-            let string = this.filterValue.script.originalScript;
+            const string = this.filterValue.script.originalScript;
             fields = this.getFilters(string);
         }
 
         runInAction(() => {
             this.addKeyRestrictions(fields);
             // chrome status is one of disabled, collapsed, or visible. this determines initial state from document
-            let chromeStatus = this.props.CollectionView.props.Document.chromeStatus;
+            const chromeStatus = this.props.CollectionView.props.Document.chromeStatus;
             if (chromeStatus) {
                 if (chromeStatus === "disabled") {
                     throw new Error("how did you get here, if chrome status is 'disabled' on a collection, a chrome shouldn't even be instantiated!");
@@ -183,7 +182,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
 
     @action
     addKeyRestriction = (e: React.MouseEvent) => {
-        let index = this._keyRestrictions.length;
+        const index = this._keyRestrictions.length;
         this._keyRestrictions.push([<KeyRestrictionRow field="" value="" key={Utils.GenerateGuid()} contains={true} script={(value: string) => runInAction(() => this._keyRestrictions[index][1] = value)} />, ""]);
 
         this.openViewSpecs(e);
@@ -194,26 +193,26 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
 
         this.openViewSpecs(e);
 
-        let keyRestrictionScript = "(" + this._keyRestrictions.map(i => i[1]).filter(i => i.length > 0).join(" && ") + ")";
-        let yearOffset = this._dateWithinValue[1] === 'y' ? 1 : 0;
-        let monthOffset = this._dateWithinValue[1] === 'm' ? parseInt(this._dateWithinValue[0]) : 0;
-        let weekOffset = this._dateWithinValue[1] === 'w' ? parseInt(this._dateWithinValue[0]) : 0;
-        let dayOffset = (this._dateWithinValue[1] === 'd' ? parseInt(this._dateWithinValue[0]) : 0) + weekOffset * 7;
+        const keyRestrictionScript = "(" + this._keyRestrictions.map(i => i[1]).filter(i => i.length > 0).join(" && ") + ")";
+        const yearOffset = this._dateWithinValue[1] === 'y' ? 1 : 0;
+        const monthOffset = this._dateWithinValue[1] === 'm' ? parseInt(this._dateWithinValue[0]) : 0;
+        const weekOffset = this._dateWithinValue[1] === 'w' ? parseInt(this._dateWithinValue[0]) : 0;
+        const dayOffset = (this._dateWithinValue[1] === 'd' ? parseInt(this._dateWithinValue[0]) : 0) + weekOffset * 7;
         let dateRestrictionScript = "";
         if (this._dateValue instanceof Date) {
-            let lowerBound = new Date(this._dateValue.getFullYear() - yearOffset, this._dateValue.getMonth() - monthOffset, this._dateValue.getDate() - dayOffset);
-            let upperBound = new Date(this._dateValue.getFullYear() + yearOffset, this._dateValue.getMonth() + monthOffset, this._dateValue.getDate() + dayOffset + 1);
+            const lowerBound = new Date(this._dateValue.getFullYear() - yearOffset, this._dateValue.getMonth() - monthOffset, this._dateValue.getDate() - dayOffset);
+            const upperBound = new Date(this._dateValue.getFullYear() + yearOffset, this._dateValue.getMonth() + monthOffset, this._dateValue.getDate() + dayOffset + 1);
             dateRestrictionScript = `((doc.creationDate as any).date >= ${lowerBound.valueOf()} && (doc.creationDate as any).date <= ${upperBound.valueOf()})`;
         }
         else {
-            let createdDate = new Date(this._dateValue);
+            const createdDate = new Date(this._dateValue);
             if (!isNaN(createdDate.getTime())) {
-                let lowerBound = new Date(createdDate.getFullYear() - yearOffset, createdDate.getMonth() - monthOffset, createdDate.getDate() - dayOffset);
-                let upperBound = new Date(createdDate.getFullYear() + yearOffset, createdDate.getMonth() + monthOffset, createdDate.getDate() + dayOffset + 1);
+                const lowerBound = new Date(createdDate.getFullYear() - yearOffset, createdDate.getMonth() - monthOffset, createdDate.getDate() - dayOffset);
+                const upperBound = new Date(createdDate.getFullYear() + yearOffset, createdDate.getMonth() + monthOffset, createdDate.getDate() + dayOffset + 1);
                 dateRestrictionScript = `((doc.creationDate as any).date >= ${lowerBound.valueOf()} && (doc.creationDate as any).date <= ${upperBound.valueOf()})`;
             }
         }
-        let fullScript = dateRestrictionScript.length || keyRestrictionScript.length ? dateRestrictionScript.length ?
+        const fullScript = dateRestrictionScript.length || keyRestrictionScript.length ? dateRestrictionScript.length ?
             `${dateRestrictionScript} ${keyRestrictionScript.length ? "&&" : ""} (${keyRestrictionScript})` :
             `(${keyRestrictionScript}) ${dateRestrictionScript.length ? "&&" : ""} ${dateRestrictionScript}` :
             "true";
@@ -270,7 +269,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
             value={this.pivotKeyDisplay}
             onChange={action((e: React.ChangeEvent<HTMLInputElement>) => this.pivotKeyDisplay = e.currentTarget.value)}
             onKeyPress={action((e: React.KeyboardEvent<HTMLInputElement>) => {
-                let value = e.currentTarget.value;
+                const value = e.currentTarget.value;
                 if (e.which === 13) {
                     this.pivotKey = value;
                     this.pivotKeyDisplay = "";
@@ -357,7 +356,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
     dragPointerMove = (e: PointerEvent) => {
         e.stopPropagation();
         e.preventDefault();
-        let [dx, dy] = [e.clientX - this._startDragPosition.x, e.clientY - this._startDragPosition.y];
+        const [dx, dy] = [e.clientX - this._startDragPosition.x, e.clientY - this._startDragPosition.y];
         if (Math.abs(dx) + Math.abs(dy) > this._sensitivity) {
             this._buttonizableCommands.filter(c => c.title === this._currentKey).map(c =>
                 DragManager.StartButtonDrag([this._commandRef.current!], c.script, c.title,
@@ -373,7 +372,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
     }
 
     render() {
-        let collapsed = this.props.CollectionView.props.Document.chromeStatus !== "enabled";
+        const collapsed = this.props.CollectionView.props.Document.chromeStatus !== "enabled";
         return (
             <div className="collectionViewChrome-cont" style={{ top: collapsed ? -70 : 0, height: collapsed ? 0 : undefined }}>
                 <div className="collectionViewChrome">
@@ -480,7 +479,7 @@ export class CollectionStackingViewChrome extends React.Component<CollectionView
 
     getKeySuggestions = async (value: string): Promise<string[]> => {
         value = value.toLowerCase();
-        let docs = DocListCast(this.props.CollectionView.props.Document[this.props.CollectionView.props.fieldKey]);
+        const docs = DocListCast(this.props.CollectionView.props.Document[this.props.CollectionView.props.fieldKey]);
         if (docs instanceof Doc) {
             return Object.keys(docs).filter(key => key.toLowerCase().startsWith(value));
         } else {
@@ -571,31 +570,31 @@ export class CollectionSchemaViewChrome extends React.Component<CollectionViewCh
 
     @undoBatch
     togglePreview = () => {
-        let dividerWidth = 4;
-        let borderWidth = Number(COLLECTION_BORDER_WIDTH);
-        let panelWidth = this.props.CollectionView.props.PanelWidth();
-        let previewWidth = NumCast(this.props.CollectionView.props.Document.schemaPreviewWidth);
-        let tableWidth = panelWidth - 2 * borderWidth - dividerWidth - previewWidth;
+        const dividerWidth = 4;
+        const borderWidth = Number(COLLECTION_BORDER_WIDTH);
+        const panelWidth = this.props.CollectionView.props.PanelWidth();
+        const previewWidth = NumCast(this.props.CollectionView.props.Document.schemaPreviewWidth);
+        const tableWidth = panelWidth - 2 * borderWidth - dividerWidth - previewWidth;
         this.props.CollectionView.props.Document.schemaPreviewWidth = previewWidth === 0 ? Math.min(tableWidth / 3, 200) : 0;
     }
 
     @undoBatch
     @action
     toggleTextwrap = async () => {
-        let textwrappedRows = Cast(this.props.CollectionView.props.Document.textwrappedSchemaRows, listSpec("string"), []);
+        const textwrappedRows = Cast(this.props.CollectionView.props.Document.textwrappedSchemaRows, listSpec("string"), []);
         if (textwrappedRows.length) {
             this.props.CollectionView.props.Document.textwrappedSchemaRows = new List<string>([]);
         } else {
-            let docs = DocListCast(this.props.CollectionView.props.Document[this.props.CollectionView.props.fieldKey]);
-            let allRows = docs instanceof Doc ? [docs[Id]] : docs.map(doc => doc[Id]);
+            const docs = DocListCast(this.props.CollectionView.props.Document[this.props.CollectionView.props.fieldKey]);
+            const allRows = docs instanceof Doc ? [docs[Id]] : docs.map(doc => doc[Id]);
             this.props.CollectionView.props.Document.textwrappedSchemaRows = new List<string>(allRows);
         }
     }
 
 
     render() {
-        let previewWidth = NumCast(this.props.CollectionView.props.Document.schemaPreviewWidth);
-        let textWrapped = Cast(this.props.CollectionView.props.Document.textwrappedSchemaRows, listSpec("string"), []).length > 0;
+        const previewWidth = NumCast(this.props.CollectionView.props.Document.schemaPreviewWidth);
+        const textWrapped = Cast(this.props.CollectionView.props.Document.textwrappedSchemaRows, listSpec("string"), []).length > 0;
 
         return (
             <div className="collectionSchemaViewChrome-cont">
diff --git a/src/client/views/collections/KeyRestrictionRow.tsx b/src/client/views/collections/KeyRestrictionRow.tsx
index e35b7d7d3..f3071b316 100644
--- a/src/client/views/collections/KeyRestrictionRow.tsx
+++ b/src/client/views/collections/KeyRestrictionRow.tsx
@@ -1,8 +1,6 @@
 import * as React from "react";
 import { observable, runInAction } from "mobx";
 import { observer } from "mobx-react";
-import { PastelSchemaPalette } from "../../../new_fields/SchemaHeaderField";
-import { Doc } from "../../../new_fields/Doc";
 
 interface IKeyRestrictionProps {
     contains: boolean;
@@ -20,13 +18,13 @@ export default class KeyRestrictionRow extends React.Component<IKeyRestrictionPr
     render() {
         if (this._key && this._value) {
             let parsedValue: string | number = `"${this._value}"`;
-            let parsed = parseInt(this._value);
+            const parsed = parseInt(this._value);
             let type = "string";
             if (!isNaN(parsed)) {
                 parsedValue = parsed;
                 type = "number";
             }
-            let scriptText = `${this._contains ? "" : "!"}(((doc.${this._key} && (doc.${this._key} as ${type})${type === "string" ? ".includes" : "<="}(${parsedValue}))) ||
+            const scriptText = `${this._contains ? "" : "!"}(((doc.${this._key} && (doc.${this._key} as ${type})${type === "string" ? ".includes" : "<="}(${parsedValue}))) ||
                 ((doc.data_ext && doc.data_ext.${this._key}) && (doc.data_ext.${this._key} as ${type})${type === "string" ? ".includes" : "<="}(${parsedValue}))))`;
             // let doc = new Doc();
             // ((doc.data_ext && doc.data_ext!.text) && (doc.data_ext!.text as string).includes("hello"));
diff --git a/src/client/views/collections/ParentDocumentSelector.tsx b/src/client/views/collections/ParentDocumentSelector.tsx
index 4eb9e9d1e..75ee8de32 100644
--- a/src/client/views/collections/ParentDocumentSelector.tsx
+++ b/src/client/views/collections/ParentDocumentSelector.tsx
@@ -34,7 +34,7 @@ export class SelectorContextMenu extends React.Component<SelectorProps> {
     }
 
     async fetchDocuments() {
-        let aliases = (await SearchUtil.GetAliasesOfDocument(this.props.Document)).filter(doc => doc !== this.props.Document);
+        const aliases = (await SearchUtil.GetAliasesOfDocument(this.props.Document)).filter(doc => doc !== this.props.Document);
         const { docs } = await SearchUtil.Search("", true, { fq: `data_l:"${this.props.Document[Id]}"` });
         const map: Map<Doc, Doc> = new Map;
         const allDocs = await Promise.all(aliases.map(doc => SearchUtil.Search("", true, { fq: `data_l:"${doc[Id]}"` }).then(result => result.docs)));
@@ -129,7 +129,7 @@ export class ButtonSelector extends React.Component<{ Document: Doc, Stack: any
     render() {
         let flyout;
         if (this.hover) {
-            let view = DocumentManager.Instance.getDocumentView(this.props.Document);
+            const view = DocumentManager.Instance.getDocumentView(this.props.Document);
             flyout = !view ? (null) : (
                 <div className="PDS-flyout" title=" " onMouseLeave={this.onMouseLeave}>
                     <DocumentButtonBar views={[view]} stack={this.props.Stack} />
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
index 4a32c1647..012115b1f 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
@@ -77,7 +77,7 @@ export function computePivotLayout(poolData: ObservableMap<string, any>, pivotDo
             fontSize: NumCast(pivotDoc.pivotFontSize, 10)
         });
         for (const doc of val) {
-            let layoutDoc = Doc.Layout(doc);
+            const layoutDoc = Doc.Layout(doc);
             let wid = pivotAxisWidth;
             let hgt = layoutDoc.nativeWidth ? (NumCast(layoutDoc.nativeHeight) / NumCast(layoutDoc.nativeWidth)) * pivotAxisWidth : pivotAxisWidth;
             if (hgt > pivotAxisWidth) {
@@ -100,7 +100,7 @@ export function computePivotLayout(poolData: ObservableMap<string, any>, pivotDo
     });
 
     childPairs.map(pair => {
-        let defaultPosition = {
+        const defaultPosition = {
             x: NumCast(pair.layout.x),
             y: NumCast(pair.layout.y),
             z: NumCast(pair.layout.z),
@@ -108,7 +108,7 @@ export function computePivotLayout(poolData: ObservableMap<string, any>, pivotDo
             height: NumCast(pair.layout.height)
         };
         const pos = docMap.get(pair.layout) || defaultPosition;
-        let data = poolData.get(pair.layout[Id]);
+        const data = poolData.get(pair.layout[Id]);
         if (!data || pos.x !== data.x || pos.y !== data.y || pos.z !== data.z || pos.width !== data.width || pos.height !== data.height) {
             runInAction(() => poolData.set(pair.layout[Id], { transition: "transform 1s", ...pos }));
         }
@@ -118,10 +118,10 @@ export function computePivotLayout(poolData: ObservableMap<string, any>, pivotDo
 
 export function AddCustomFreeFormLayout(doc: Doc, dataKey: string): () => void {
     return () => {
-        let addOverlay = (key: "arrangeScript" | "arrangeInit", options: OverlayElementOptions, params?: Record<string, string>, requiredType?: string) => {
+        const addOverlay = (key: "arrangeScript" | "arrangeInit", options: OverlayElementOptions, params?: Record<string, string>, requiredType?: string) => {
             let overlayDisposer: () => void = emptyFunction; // filled in below after we have a reference to the scriptingBox
             const scriptField = Cast(doc[key], ScriptField);
-            let scriptingBox = <ScriptBox initialText={scriptField && scriptField.script.originalScript}
+            const scriptingBox = <ScriptBox initialText={scriptField && scriptField.script.originalScript}
                 // tslint:disable-next-line: no-unnecessary-callback-wrapper
                 onCancel={() => overlayDisposer()}  // don't get rid of the function wrapper-- we don't want to use the current value of overlayDiposer, but the one set below
                 onSave={(text, onError) => {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index b00728079..5e4b4fd27 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -6,9 +6,8 @@ import "./CollectionFreeFormLinkView.scss";
 import React = require("react");
 import v5 = require("uuid/v5");
 import { DocumentType } from "../../../documents/DocumentTypes";
-import { observable, action, reaction, IReactionDisposer, trace } from "mobx";
-import { StrCast, Cast } from "../../../../new_fields/Types";
-import { TraceMobx } from "../../../../new_fields/util";
+import { observable, action, reaction, IReactionDisposer } from "mobx";
+import { StrCast } from "../../../../new_fields/Types";
 
 export interface CollectionFreeFormLinkViewProps {
     A: DocumentView;
@@ -26,22 +25,22 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
             action(() => {
                 setTimeout(action(() => this._opacity = 1), 0); // since the render code depends on querying the Dom through getBoudndingClientRect, we need to delay triggering render()
                 setTimeout(action(() => this._opacity = 0.05), 750); // this will unhighlight the link line.
-                let acont = this.props.A.props.Document.type === DocumentType.LINK ? this.props.A.ContentDiv!.getElementsByClassName("docuLinkBox-cont") : [];
-                let bcont = this.props.B.props.Document.type === DocumentType.LINK ? this.props.B.ContentDiv!.getElementsByClassName("docuLinkBox-cont") : [];
-                let adiv = (acont.length ? acont[0] : this.props.A.ContentDiv!);
-                let bdiv = (bcont.length ? bcont[0] : this.props.B.ContentDiv!);
-                let a = adiv.getBoundingClientRect();
-                let b = bdiv.getBoundingClientRect();
-                let abounds = adiv.parentElement!.getBoundingClientRect();
-                let bbounds = bdiv.parentElement!.getBoundingClientRect();
-                let apt = Utils.closestPtBetweenRectangles(abounds.left, abounds.top, abounds.width, abounds.height,
+                const acont = this.props.A.props.Document.type === DocumentType.LINK ? this.props.A.ContentDiv!.getElementsByClassName("docuLinkBox-cont") : [];
+                const bcont = this.props.B.props.Document.type === DocumentType.LINK ? this.props.B.ContentDiv!.getElementsByClassName("docuLinkBox-cont") : [];
+                const adiv = (acont.length ? acont[0] : this.props.A.ContentDiv!);
+                const bdiv = (bcont.length ? bcont[0] : this.props.B.ContentDiv!);
+                const a = adiv.getBoundingClientRect();
+                const b = bdiv.getBoundingClientRect();
+                const abounds = adiv.parentElement!.getBoundingClientRect();
+                const bbounds = bdiv.parentElement!.getBoundingClientRect();
+                const apt = Utils.closestPtBetweenRectangles(abounds.left, abounds.top, abounds.width, abounds.height,
                     bbounds.left, bbounds.top, bbounds.width, bbounds.height,
                     a.left + a.width / 2, a.top + a.height / 2);
-                let bpt = Utils.closestPtBetweenRectangles(bbounds.left, bbounds.top, bbounds.width, bbounds.height,
+                const bpt = Utils.closestPtBetweenRectangles(bbounds.left, bbounds.top, bbounds.width, bbounds.height,
                     abounds.left, abounds.top, abounds.width, abounds.height,
                     apt.point.x, apt.point.y);
-                let afield = StrCast(this.props.A.props.Document[StrCast(this.props.A.props.layoutKey, "layout")]).indexOf("anchor1") === -1 ? "anchor2" : "anchor1";
-                let bfield = afield === "anchor1" ? "anchor2" : "anchor1";
+                const afield = StrCast(this.props.A.props.Document[StrCast(this.props.A.props.layoutKey, "layout")]).indexOf("anchor1") === -1 ? "anchor2" : "anchor1";
+                const bfield = afield === "anchor1" ? "anchor2" : "anchor1";
                 this.props.A.props.Document[afield + "_x"] = (apt.point.x - abounds.left) / abounds.width * 100;
                 this.props.A.props.Document[afield + "_y"] = (apt.point.y - abounds.top) / abounds.height * 100;
                 this.props.A.props.Document[bfield + "_x"] = (bpt.point.x - bbounds.left) / bbounds.width * 100;
@@ -55,18 +54,18 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
     }
 
     render() {
-        let acont = this.props.A.props.Document.type === DocumentType.LINK ? this.props.A.ContentDiv!.getElementsByClassName("docuLinkBox-cont") : [];
-        let bcont = this.props.B.props.Document.type === DocumentType.LINK ? this.props.B.ContentDiv!.getElementsByClassName("docuLinkBox-cont") : [];
-        let a = (acont.length ? acont[0] : this.props.A.ContentDiv!).getBoundingClientRect();
-        let b = (bcont.length ? bcont[0] : this.props.B.ContentDiv!).getBoundingClientRect();
-        let apt = Utils.closestPtBetweenRectangles(a.left, a.top, a.width, a.height,
+        const acont = this.props.A.props.Document.type === DocumentType.LINK ? this.props.A.ContentDiv!.getElementsByClassName("docuLinkBox-cont") : [];
+        const bcont = this.props.B.props.Document.type === DocumentType.LINK ? this.props.B.ContentDiv!.getElementsByClassName("docuLinkBox-cont") : [];
+        const a = (acont.length ? acont[0] : this.props.A.ContentDiv!).getBoundingClientRect();
+        const b = (bcont.length ? bcont[0] : this.props.B.ContentDiv!).getBoundingClientRect();
+        const apt = Utils.closestPtBetweenRectangles(a.left, a.top, a.width, a.height,
             b.left, b.top, b.width, b.height,
             a.left + a.width / 2, a.top + a.height / 2);
-        let bpt = Utils.closestPtBetweenRectangles(b.left, b.top, b.width, b.height,
+        const bpt = Utils.closestPtBetweenRectangles(b.left, b.top, b.width, b.height,
             a.left, a.top, a.width, a.height,
             apt.point.x, apt.point.y);
-        let pt1 = [apt.point.x, apt.point.y];
-        let pt2 = [bpt.point.x, bpt.point.y];
+        const pt1 = [apt.point.x, apt.point.y];
+        const pt2 = [bpt.point.x, bpt.point.y];
         return (<line key="linkLine" className="collectionfreeformlinkview-linkLine"
             style={{ opacity: this._opacity }}
             x1={`${pt1[0]}`} y1={`${pt1[1]}`}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
index e9191c176..218012fe1 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
@@ -72,11 +72,11 @@ export class CollectionFreeFormLinksView extends React.Component {
     }
     @computed
     get uniqueConnections() {
-        let connections = DocumentManager.Instance.LinkedDocumentViews.reduce((drawnPairs, connection) => {
+        const connections = DocumentManager.Instance.LinkedDocumentViews.reduce((drawnPairs, connection) => {
             if (!drawnPairs.reduce((found, drawnPair) => {
-                let match1 = (connection.a === drawnPair.a && connection.b === drawnPair.b);
-                let match2 = (connection.a === drawnPair.b && connection.b === drawnPair.a);
-                let match = match1 || match2;
+                const match1 = (connection.a === drawnPair.a && connection.b === drawnPair.b);
+                const match2 = (connection.a === drawnPair.b && connection.b === drawnPair.a);
+                const match = match1 || match2;
                 if (match && !drawnPair.l.reduce((found, link) => found || link[Id] === connection.l[Id], false)) {
                     drawnPair.l.push(connection.l);
                 }
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx
index b8148852d..bb9ae4326 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx
@@ -13,14 +13,14 @@ import v5 = require("uuid/v5");
 export class CollectionFreeFormRemoteCursors extends React.Component<CollectionViewProps> {
 
     protected getCursors(): CursorField[] {
-        let doc = this.props.Document;
+        const doc = this.props.Document;
 
-        let id = CurrentUserUtils.id;
+        const id = CurrentUserUtils.id;
         if (!id) {
             return [];
         }
 
-        let cursors = Cast(doc.cursors, listSpec(CursorField));
+        const cursors = Cast(doc.cursors, listSpec(CursorField));
 
         const now = mobxUtils.now();
         // const now = Date.now();
@@ -30,7 +30,7 @@ export class CollectionFreeFormRemoteCursors extends React.Component<CollectionV
     private crosshairs?: HTMLCanvasElement;
     drawCrosshairs = (backgroundColor: string) => {
         if (this.crosshairs) {
-            let ctx = this.crosshairs.getContext('2d');
+            const ctx = this.crosshairs.getContext('2d');
             if (ctx) {
                 ctx.fillStyle = backgroundColor;
                 ctx.fillRect(0, 0, 20, 20);
@@ -62,8 +62,8 @@ export class CollectionFreeFormRemoteCursors extends React.Component<CollectionV
 
     get sharedCursors() {
         return this.getCursors().map(c => {
-            let m = c.data.metadata;
-            let l = c.data.position;
+            const m = c.data.metadata;
+            const l = c.data.position;
             this.drawCrosshairs("#" + v5(m.id, v5.URL).substring(0, 6).toUpperCase() + "22");
             return (
                 <div key={m.id} className="collectionFreeFormRemoteCursors-cont"
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 9506ce084..c69b264c4 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1,12 +1,12 @@
 import { library } from "@fortawesome/fontawesome-svg-core";
 import { faEye } from "@fortawesome/free-regular-svg-icons";
 import { faBraille, faChalkboard, faCompass, faCompressArrowsAlt, faExpandArrowsAlt, faFileUpload, faPaintBrush, faTable, faUpload } from "@fortawesome/free-solid-svg-icons";
-import { action, computed, observable, trace, ObservableMap, untracked, reaction, runInAction, IReactionDisposer } from "mobx";
+import { action, computed, observable, ObservableMap, reaction, runInAction, IReactionDisposer } from "mobx";
 import { observer } from "mobx-react";
-import { Doc, DocListCast, HeightSym, Opt, WidthSym } from "../../../../new_fields/Doc";
+import { Doc, DocListCast, HeightSym, Opt, WidthSym, DocListCastAsync } from "../../../../new_fields/Doc";
 import { documentSchema, positionSchema } from "../../../../new_fields/documentSchemas";
 import { Id } from "../../../../new_fields/FieldSymbols";
-import { InkTool } from "../../../../new_fields/InkField";
+import { InkTool, InkField, InkData } from "../../../../new_fields/InkField";
 import { createSchema, makeInterface } from "../../../../new_fields/Schema";
 import { ScriptField } from "../../../../new_fields/ScriptField";
 import { BoolCast, Cast, DateCast, NumCast, StrCast } from "../../../../new_fields/Types";
@@ -26,7 +26,7 @@ import { COLLECTION_BORDER_WIDTH } from "../../../views/globalCssVariables.scss"
 import { ContextMenu } from "../../ContextMenu";
 import { ContextMenuProps } from "../../ContextMenuItem";
 import { InkingControl } from "../../InkingControl";
-import { CreatePolyline, InkingStroke } from "../../InkingStroke";
+import { CreatePolyline } from "../../InkingStroke";
 import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView";
 import { DocumentViewProps } from "../../nodes/DocumentView";
 import { FormattedTextBox } from "../../nodes/FormattedTextBox";
@@ -39,8 +39,9 @@ import "./CollectionFreeFormView.scss";
 import MarqueeOptionsMenu from "./MarqueeOptionsMenu";
 import { MarqueeView } from "./MarqueeView";
 import React = require("react");
-import { computedFn, keepAlive } from "mobx-utils";
+import { computedFn } from "mobx-utils";
 import { TraceMobx } from "../../../../new_fields/util";
+import { CognitiveServices } from "../../../cognitive_services/CognitiveServices";
 
 library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard, faFileUpload);
 
@@ -98,10 +99,10 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     private getLocalTransform = (): Transform => Transform.Identity().scale(1 / this.zoomScaling()).translate(this.panX(), this.panY());
     private addLiveTextBox = (newBox: Doc) => {
         FormattedTextBox.SelectOnLoad = newBox[Id];// track the new text box so we can give it a prop that tells it to focus itself when it's displayed
-        let maxHeading = this.childDocs.reduce((maxHeading, doc) => NumCast(doc.heading) > maxHeading ? NumCast(doc.heading) : maxHeading, 0);
+        const maxHeading = this.childDocs.reduce((maxHeading, doc) => NumCast(doc.heading) > maxHeading ? NumCast(doc.heading) : maxHeading, 0);
         let heading = maxHeading === 0 || this.childDocs.length === 0 ? 1 : maxHeading === 1 ? 2 : 0;
         if (heading === 0) {
-            let sorted = this.childDocs.filter(d => d.type === DocumentType.TEXT && d.data_ext instanceof Doc && d.data_ext.lastModified).sort((a, b) => DateCast((Cast(a.data_ext, Doc) as Doc).lastModified).date > DateCast((Cast(b.data_ext, Doc) as Doc).lastModified).date ? 1 :
+            const sorted = this.childDocs.filter(d => d.type === DocumentType.TEXT && d.data_ext instanceof Doc && d.data_ext.lastModified).sort((a, b) => DateCast((Cast(a.data_ext, Doc) as Doc).lastModified).date > DateCast((Cast(b.data_ext, Doc) as Doc).lastModified).date ? 1 :
                 DateCast((Cast(a.data_ext, Doc) as Doc).lastModified).date < DateCast((Cast(b.data_ext, Doc) as Doc).lastModified).date ? -1 : 0);
             heading = !sorted.length ? Math.max(1, maxHeading) : NumCast(sorted[sorted.length - 1].heading) === 1 ? 2 : NumCast(sorted[sorted.length - 1].heading);
         }
@@ -109,7 +110,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         this.addDocument(newBox);
     }
     private addDocument = (newBox: Doc) => {
-        let added = this.props.addDocument(newBox);
+        const added = this.props.addDocument(newBox);
         added && this.bringToFront(newBox);
         added && this.updateCluster(newBox);
         return added;
@@ -126,36 +127,36 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
 
     @action
     onDrop = (e: React.DragEvent): Promise<void> => {
-        var pt = this.getTransform().transformPoint(e.pageX, e.pageY);
+        const pt = this.getTransform().transformPoint(e.pageX, e.pageY);
         return super.onDrop(e, { x: pt[0], y: pt[1] });
     }
 
     @undoBatch
     @action
     drop = (e: Event, de: DragManager.DropEvent) => {
-        let xf = this.getTransform();
-        let xfo = this.getTransformOverlay();
-        let [xp, yp] = xf.transformPoint(de.x, de.y);
-        let [xpo, ypo] = xfo.transformPoint(de.x, de.y);
+        const xf = this.getTransform();
+        const xfo = this.getTransformOverlay();
+        const [xp, yp] = xf.transformPoint(de.x, de.y);
+        const [xpo, ypo] = xfo.transformPoint(de.x, de.y);
         if (super.drop(e, de)) {
             if (de.data instanceof DragManager.DocumentDragData) {
                 if (de.data.droppedDocuments.length) {
-                    let firstDoc = de.data.droppedDocuments[0];
-                    let z = NumCast(firstDoc.z);
-                    let x = (z ? xpo : xp) - de.data.offset[0];
-                    let y = (z ? ypo : yp) - de.data.offset[1];
-                    let dropX = NumCast(firstDoc.x);
-                    let dropY = NumCast(firstDoc.y);
+                    const firstDoc = de.data.droppedDocuments[0];
+                    const z = NumCast(firstDoc.z);
+                    const x = (z ? xpo : xp) - de.data.offset[0];
+                    const y = (z ? ypo : yp) - de.data.offset[1];
+                    const dropX = NumCast(firstDoc.x);
+                    const dropY = NumCast(firstDoc.y);
                     de.data.droppedDocuments.forEach(action((d: Doc) => {
-                        let layoutDoc = Doc.Layout(d);
+                        const layoutDoc = Doc.Layout(d);
                         d.x = x + NumCast(d.x) - dropX;
                         d.y = y + NumCast(d.y) - dropY;
                         if (!NumCast(layoutDoc.width)) {
                             layoutDoc.width = 300;
                         }
                         if (!NumCast(layoutDoc.height)) {
-                            let nw = NumCast(layoutDoc.nativeWidth);
-                            let nh = NumCast(layoutDoc.nativeHeight);
+                            const nw = NumCast(layoutDoc.nativeWidth);
+                            const nh = NumCast(layoutDoc.nativeHeight);
                             layoutDoc.height = nw && nh ? nh / nw * NumCast(layoutDoc.width) : 300;
                         }
                         this.bringToFront(d);
@@ -166,11 +167,11 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
             }
             else if (de.data instanceof DragManager.AnnotationDragData) {
                 if (de.data.dropDocument) {
-                    let dragDoc = de.data.dropDocument;
-                    let x = xp - de.data.offset[0];
-                    let y = yp - de.data.offset[1];
-                    let dropX = NumCast(dragDoc.x);
-                    let dropY = NumCast(dragDoc.y);
+                    const dragDoc = de.data.dropDocument;
+                    const x = xp - de.data.offset[0];
+                    const y = yp - de.data.offset[1];
+                    const dropX = NumCast(dragDoc.x);
+                    const dropY = NumCast(dragDoc.y);
                     dragDoc.x = x + NumCast(dragDoc.x) - dropX;
                     dragDoc.y = y + NumCast(dragDoc.y) - dropY;
                     de.data.targetContext = this.props.Document; // dropped a PDF annotation, so we need to set the targetContext on the dragData which the PDF view uses at the end of the drop operation
@@ -183,23 +184,23 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
 
     pickCluster(probe: number[]) {
         return this.childLayoutPairs.map(pair => pair.layout).reduce((cluster, cd) => {
-            let layoutDoc = Doc.Layout(cd);
-            let cx = NumCast(cd.x) - this._clusterDistance;
-            let cy = NumCast(cd.y) - this._clusterDistance;
-            let cw = NumCast(layoutDoc.width) + 2 * this._clusterDistance;
-            let ch = NumCast(layoutDoc.height) + 2 * this._clusterDistance;
+            const layoutDoc = Doc.Layout(cd);
+            const cx = NumCast(cd.x) - this._clusterDistance;
+            const cy = NumCast(cd.y) - this._clusterDistance;
+            const cw = NumCast(layoutDoc.width) + 2 * this._clusterDistance;
+            const ch = NumCast(layoutDoc.height) + 2 * this._clusterDistance;
             return !layoutDoc.z && intersectRect({ left: cx, top: cy, width: cw, height: ch }, { left: probe[0], top: probe[1], width: 1, height: 1 }) ?
                 NumCast(cd.cluster) : cluster;
         }, -1);
     }
     tryDragCluster(e: PointerEvent | TouchEvent) {
-        let ptsParent = e instanceof PointerEvent ? e : e.targetTouches.item(0);
+        const ptsParent = e instanceof PointerEvent ? e : e.targetTouches.item(0);
         if (ptsParent) {
-            let cluster = this.pickCluster(this.getTransform().transformPoint(ptsParent.clientX, ptsParent.clientY));
+            const cluster = this.pickCluster(this.getTransform().transformPoint(ptsParent.clientX, ptsParent.clientY));
             if (cluster !== -1) {
-                let eles = this.childLayoutPairs.map(pair => pair.layout).filter(cd => NumCast(cd.cluster) === cluster);
-                let clusterDocs = eles.map(ele => DocumentManager.Instance.getDocumentView(ele, this.props.CollectionView)!);
-                let de = new DragManager.DocumentDragData(eles);
+                const eles = this.childLayoutPairs.map(pair => pair.layout).filter(cd => NumCast(cd.cluster) === cluster);
+                const clusterDocs = eles.map(ele => DocumentManager.Instance.getDocumentView(ele, this.props.CollectionView)!);
+                const de = new DragManager.DocumentDragData(eles);
                 de.moveDocument = this.props.moveDocument;
                 const [left, top] = clusterDocs[0].props.ScreenToLocalTransform().scale(clusterDocs[0].props.ContentScaling()).inverse().transformPoint(0, 0);
                 de.offset = this.getTransform().transformDirection(ptsParent.clientX - left, ptsParent.clientY - top);
@@ -225,10 +226,10 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     @undoBatch
     @action
     updateCluster(doc: Doc) {
-        let childLayouts = this.childLayoutPairs.map(pair => pair.layout);
+        const childLayouts = this.childLayoutPairs.map(pair => pair.layout);
         if (this.props.Document.useClusters) {
             this._clusterSets.map(set => Doc.IndexOf(doc, set) !== -1 && set.splice(Doc.IndexOf(doc, set), 1));
-            let preferredInd = NumCast(doc.cluster);
+            const preferredInd = NumCast(doc.cluster);
             doc.cluster = -1;
             this._clusterSets.map((set, i) => set.map(member => {
                 if (doc.cluster === -1 && Doc.IndexOf(member, childLayouts) !== -1 && Doc.overlapping(doc, member, this._clusterDistance)) {
@@ -255,15 +256,15 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
 
     getClusterColor = (doc: Doc) => {
         let clusterColor = "";
-        let cluster = NumCast(doc.cluster);
+        const cluster = NumCast(doc.cluster);
         if (this.Document.useClusters) {
             if (this._clusterSets.length <= cluster) {
                 setTimeout(() => this.updateCluster(doc), 0);
             } else {
                 // choose a cluster color from a palette
-                let colors = ["#da42429e", "#31ea318c", "#8c4000", "#4a7ae2c4", "#d809ff", "#ff7601", "#1dffff", "yellow", "#1b8231f2", "#000000ad"];
+                const colors = ["#da42429e", "#31ea318c", "#8c4000", "#4a7ae2c4", "#d809ff", "#ff7601", "#1dffff", "yellow", "#1b8231f2", "#000000ad"];
                 clusterColor = colors[cluster % colors.length];
-                let set = this._clusterSets[cluster] && this._clusterSets[cluster].filter(s => s.backgroundColor && (s.backgroundColor !== s.defaultBackgroundColor));
+                const set = this._clusterSets[cluster] && this._clusterSets[cluster].filter(s => s.backgroundColor && (s.backgroundColor !== s.defaultBackgroundColor));
                 // override the cluster color with an explicitly set color on a non-background document.  then override that with an explicitly set color on a background document
                 set && set.filter(s => !s.isBackground).map(s => clusterColor = StrCast(s.backgroundColor));
                 set && set.filter(s => s.isBackground).map(s => clusterColor = StrCast(s.backgroundColor));
@@ -287,7 +288,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
             if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen)) {
                 e.stopPropagation();
                 e.preventDefault();
-                let point = this.getTransform().transformPoint(e.pageX, e.pageY);
+                const point = this.getTransform().transformPoint(e.pageX, e.pageY);
                 this._points.push({ x: point[0], y: point[1] });
             }
             // if not using a pen and in no ink mode
@@ -324,7 +325,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
 
     @action
     handle1PointerDown = (e: React.TouchEvent) => {
-        let pt = e.targetTouches.item(0);
+        const pt = e.targetTouches.item(0);
         if (pt) {
             this._hitCluster = this.props.Document.useCluster ? this.pickCluster(this.getTransform().transformPoint(pt.clientX, pt.clientY)) !== -1 : false;
         }
@@ -335,9 +336,9 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         if (InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) && this._points.length <= 1) return;
 
         if (this._points.length > 1) {
-            let B = this.svgBounds;
-            let points = this._points.map(p => ({ x: p.x - B.left, y: p.y - B.top }));
-            let inkDoc = Docs.Create.InkDocument(InkingControl.Instance.selectedColor, InkingControl.Instance.selectedTool, parseInt(InkingControl.Instance.selectedWidth), points, { width: B.width, height: B.height, x: B.left, y: B.top });
+            const B = this.svgBounds;
+            const points = this._points.map(p => ({ x: p.x - B.left, y: p.y - B.top }));
+            const inkDoc = Docs.Create.InkDocument(InkingControl.Instance.selectedColor, InkingControl.Instance.selectedTool, parseInt(InkingControl.Instance.selectedWidth), points, { width: B.width, height: B.height, x: B.left, y: B.top });
             this.addDocument(inkDoc);
             this._points = [];
         }
@@ -355,26 +356,26 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
 
         let x = this.Document.panX || 0;
         let y = this.Document.panY || 0;
-        let docs = this.childLayoutPairs.map(pair => pair.layout);
-        let [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY);
+        const docs = this.childLayoutPairs.map(pair => pair.layout);
+        const [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY);
         if (!this.isAnnotationOverlay) {
             PDFMenu.Instance.fadeOut(true);
-            let minx = docs.length ? NumCast(docs[0].x) : 0;
-            let maxx = docs.length ? NumCast(docs[0].width) + minx : minx;
-            let miny = docs.length ? NumCast(docs[0].y) : 0;
-            let maxy = docs.length ? NumCast(docs[0].height) + miny : miny;
-            let ranges = docs.filter(doc => doc).reduce((range, doc) => {
-                let layoutDoc = Doc.Layout(doc);
-                let x = NumCast(doc.x);
-                let xe = x + NumCast(layoutDoc.width);
-                let y = NumCast(doc.y);
-                let ye = y + NumCast(layoutDoc.height);
+            const minx = docs.length ? NumCast(docs[0].x) : 0;
+            const maxx = docs.length ? NumCast(docs[0].width) + minx : minx;
+            const miny = docs.length ? NumCast(docs[0].y) : 0;
+            const maxy = docs.length ? NumCast(docs[0].height) + miny : miny;
+            const ranges = docs.filter(doc => doc).reduce((range, doc) => {
+                const layoutDoc = Doc.Layout(doc);
+                const x = NumCast(doc.x);
+                const xe = x + NumCast(layoutDoc.width);
+                const y = NumCast(doc.y);
+                const ye = y + NumCast(layoutDoc.height);
                 return [[range[0][0] > x ? x : range[0][0], range[0][1] < xe ? xe : range[0][1]],
                 [range[1][0] > y ? y : range[1][0], range[1][1] < ye ? ye : range[1][1]]];
             }, [[minx, maxx], [miny, maxy]]);
 
-            let cscale = this.props.ContainingCollectionDoc ? NumCast(this.props.ContainingCollectionDoc.scale) : 1;
-            let panelDim = this.props.ScreenToLocalTransform().transformDirection(this.props.PanelWidth() / this.zoomScaling() * cscale,
+            const cscale = this.props.ContainingCollectionDoc ? NumCast(this.props.ContainingCollectionDoc.scale) : 1;
+            const panelDim = this.props.ScreenToLocalTransform().transformDirection(this.props.PanelWidth() / this.zoomScaling() * cscale,
                 this.props.PanelHeight() / this.zoomScaling() * cscale);
             if (ranges[0][0] - dx > (this.panX() + panelDim[0] / 2)) x = ranges[0][1] + panelDim[0] / 2;
             if (ranges[0][1] - dx < (this.panX() - panelDim[0] / 2)) x = ranges[0][0] - panelDim[0] / 2;
@@ -397,7 +398,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         if (!e.cancelBubble) {
             const selectedTool = InkingControl.Instance.selectedTool;
             if (selectedTool === InkTool.Highlighter || selectedTool === InkTool.Pen || InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) {
-                let point = this.getTransform().transformPoint(e.clientX, e.clientY);
+                const point = this.getTransform().transformPoint(e.clientX, e.clientY);
                 this._points.push({ x: point[0], y: point[1] });
             }
             else if (selectedTool === InkTool.None) {
@@ -418,7 +419,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     handle1PointerMove = (e: TouchEvent) => {
         // panning a workspace
         if (!e.cancelBubble) {
-            let pt = e.targetTouches.item(0);
+            const pt = e.targetTouches.item(0);
             if (pt) {
                 if (InkingControl.Instance.selectedTool === InkTool.None) {
                     if (this._hitCluster && this.tryDragCluster(e)) {
@@ -431,7 +432,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
                     this.pan(pt);
                 }
                 else if (InkingControl.Instance.selectedTool !== InkTool.Eraser && InkingControl.Instance.selectedTool !== InkTool.Scrubber) {
-                    let point = this.getTransform().transformPoint(pt.clientX, pt.clientY);
+                    const point = this.getTransform().transformPoint(pt.clientX, pt.clientY);
                     this._points.push({ x: point[0], y: point[1] });
                 }
             }
@@ -443,28 +444,28 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     handle2PointersMove = (e: TouchEvent) => {
         // pinch zooming
         if (!e.cancelBubble) {
-            let pt1: Touch | null = e.targetTouches.item(0);
-            let pt2: Touch | null = e.targetTouches.item(1);
+            const pt1: Touch | null = e.targetTouches.item(0);
+            const pt2: Touch | null = e.targetTouches.item(1);
             if (!pt1 || !pt2) return;
 
             if (this.prevPoints.size === 2) {
-                let oldPoint1 = this.prevPoints.get(pt1.identifier);
-                let oldPoint2 = this.prevPoints.get(pt2.identifier);
+                const oldPoint1 = this.prevPoints.get(pt1.identifier);
+                const oldPoint2 = this.prevPoints.get(pt2.identifier);
                 if (oldPoint1 && oldPoint2) {
-                    let dir = InteractionUtils.Pinching(pt1, pt2, oldPoint1, oldPoint2);
+                    const dir = InteractionUtils.Pinching(pt1, pt2, oldPoint1, oldPoint2);
 
                     // if zooming, zoom
                     if (dir !== 0) {
-                        let d1 = Math.sqrt(Math.pow(pt1.clientX - oldPoint1.clientX, 2) + Math.pow(pt1.clientY - oldPoint1.clientY, 2));
-                        let d2 = Math.sqrt(Math.pow(pt2.clientX - oldPoint2.clientX, 2) + Math.pow(pt2.clientY - oldPoint2.clientY, 2));
-                        let centerX = Math.min(pt1.clientX, pt2.clientX) + Math.abs(pt2.clientX - pt1.clientX) / 2;
-                        let centerY = Math.min(pt1.clientY, pt2.clientY) + Math.abs(pt2.clientY - pt1.clientY) / 2;
+                        const d1 = Math.sqrt(Math.pow(pt1.clientX - oldPoint1.clientX, 2) + Math.pow(pt1.clientY - oldPoint1.clientY, 2));
+                        const d2 = Math.sqrt(Math.pow(pt2.clientX - oldPoint2.clientX, 2) + Math.pow(pt2.clientY - oldPoint2.clientY, 2));
+                        const centerX = Math.min(pt1.clientX, pt2.clientX) + Math.abs(pt2.clientX - pt1.clientX) / 2;
+                        const centerY = Math.min(pt1.clientY, pt2.clientY) + Math.abs(pt2.clientY - pt1.clientY) / 2;
 
                         // calculate the raw delta value
-                        let rawDelta = (dir * (d1 + d2));
+                        const rawDelta = (dir * (d1 + d2));
 
                         // this floors and ceils the delta value to prevent jitteriness
-                        let delta = Math.sign(rawDelta) * Math.min(Math.abs(rawDelta), 16);
+                        const delta = Math.sign(rawDelta) * Math.min(Math.abs(rawDelta), 16);
                         this.zoom(centerX, centerY, delta);
                         this.prevPoints.set(pt1.identifier, pt1);
                         this.prevPoints.set(pt2.identifier, pt2);
@@ -472,8 +473,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
                     // this is not zooming. derive some form of panning from it.
                     else {
                         // use the centerx and centery as the "new mouse position"
-                        let centerX = Math.min(pt1.clientX, pt2.clientX) + Math.abs(pt2.clientX - pt1.clientX) / 2;
-                        let centerY = Math.min(pt1.clientY, pt2.clientY) + Math.abs(pt2.clientY - pt1.clientY) / 2;
+                        const centerX = Math.min(pt1.clientX, pt2.clientX) + Math.abs(pt2.clientX - pt1.clientX) / 2;
+                        const centerY = Math.min(pt1.clientY, pt2.clientY) + Math.abs(pt2.clientY - pt1.clientY) / 2;
                         this.pan({ clientX: centerX, clientY: centerY });
                         this._lastX = centerX;
                         this._lastY = centerY;
@@ -486,12 +487,12 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     }
 
     handle2PointersDown = (e: React.TouchEvent) => {
-        let pt1: React.Touch | null = e.targetTouches.item(0);
-        let pt2: React.Touch | null = e.targetTouches.item(1);
+        const pt1: React.Touch | null = e.targetTouches.item(0);
+        const pt2: React.Touch | null = e.targetTouches.item(1);
         if (!pt1 || !pt2) return;
 
-        let centerX = Math.min(pt1.clientX, pt2.clientX) + Math.abs(pt2.clientX - pt1.clientX) / 2;
-        let centerY = Math.min(pt1.clientY, pt2.clientY) + Math.abs(pt2.clientY - pt1.clientY) / 2;
+        const centerX = Math.min(pt1.clientX, pt2.clientX) + Math.abs(pt2.clientX - pt1.clientX) / 2;
+        const centerY = Math.min(pt1.clientY, pt2.clientY) + Math.abs(pt2.clientY - pt1.clientY) / 2;
         this._lastX = centerX;
         this._lastY = centerY;
     }
@@ -510,11 +511,11 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
             deltaScale = 1 / this.zoomScaling();
         }
         if (deltaScale < 0) deltaScale = -deltaScale;
-        let [x, y] = this.getTransform().transformPoint(pointX, pointY);
-        let localTransform = this.getLocalTransform().inverse().scaleAbout(deltaScale, x, y);
+        const [x, y] = this.getTransform().transformPoint(pointX, pointY);
+        const localTransform = this.getLocalTransform().inverse().scaleAbout(deltaScale, x, y);
 
         if (localTransform.Scale >= 0.15) {
-            let safeScale = Math.min(Math.max(0.15, localTransform.Scale), 40);
+            const safeScale = Math.min(Math.max(0.15, localTransform.Scale), 40);
             this.props.Document.scale = Math.abs(safeScale);
             this.setPan(-localTransform.TranslateX / safeScale, -localTransform.TranslateY / safeScale);
         }
@@ -536,7 +537,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     setPan(panX: number, panY: number, panType: string = "None") {
         if (!this.Document.lockedTransform || this.Document.inOverlay) {
             this.Document.panTransformType = panType;
-            var scale = this.getLocalTransform().inverse().Scale;
+            const scale = this.getLocalTransform().inverse().Scale;
             const newPanX = Math.min((1 - 1 / scale) * this.nativeWidth, Math.max(0, panX));
             const newPanY = Math.min((this.props.Document.scrollHeight !== undefined ? NumCast(this.Document.scrollHeight) : (1 - 1 / scale) * this.nativeHeight), Math.max(0, panY));
             this.Document.panX = this.isAnnotationOverlay ? newPanX : panX;
@@ -576,23 +577,23 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         }
         SelectionManager.DeselectAll();
         if (this.props.Document.scrollHeight) {
-            let annotOn = Cast(doc.annotationOn, Doc) as Doc;
+            const annotOn = Cast(doc.annotationOn, Doc) as Doc;
             if (!annotOn) {
                 this.props.focus(doc);
             } else {
-                let contextHgt = Doc.AreProtosEqual(annotOn, this.props.Document) && this.props.VisibleHeight ? this.props.VisibleHeight() : NumCast(annotOn.height);
-                let offset = annotOn && (contextHgt / 2 * 96 / 72);
+                const contextHgt = Doc.AreProtosEqual(annotOn, this.props.Document) && this.props.VisibleHeight ? this.props.VisibleHeight() : NumCast(annotOn.height);
+                const offset = annotOn && (contextHgt / 2 * 96 / 72);
                 this.props.Document.scrollY = NumCast(doc.y) - offset;
             }
         } else {
-            let layoutdoc = Doc.Layout(doc);
+            const layoutdoc = Doc.Layout(doc);
             const newPanX = NumCast(doc.x) + NumCast(layoutdoc.width) / 2;
             const newPanY = NumCast(doc.y) + NumCast(layoutdoc.height) / 2;
             const newState = HistoryUtil.getState();
             newState.initializers![this.Document[Id]] = { panX: newPanX, panY: newPanY };
             HistoryUtil.pushState(newState);
 
-            let savedState = { px: this.Document.panX, py: this.Document.panY, s: this.Document.scale, pt: this.Document.panTransformType };
+            const savedState = { px: this.Document.panX, py: this.Document.panY, s: this.Document.scale, pt: this.Document.panTransformType };
 
             this.setPan(newPanX, newPanY, "Ease");
             Doc.BrushDoc(this.props.Document);
@@ -677,7 +678,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         }
     }
 
-    childDataProvider = computedFn(function childDataProvider(doc: Doc) { return (this as any)._layoutPoolData.get(doc[Id]); }.bind(this));
+    childDataProvider = computedFn(function childDataProvider(this: any, doc: Doc) { return this._layoutPoolData.get(doc[Id]); }.bind(this));
 
     doPivotLayout(poolData: ObservableMap<string, any>) {
         return computePivotLayout(poolData, this.props.Document, this.childDocs,
@@ -685,10 +686,10 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     }
 
     doFreeformLayout(poolData: ObservableMap<string, any>) {
-        let layoutDocs = this.childLayoutPairs.map(pair => pair.layout);
+        const layoutDocs = this.childLayoutPairs.map(pair => pair.layout);
         const initResult = this.Document.arrangeInit && this.Document.arrangeInit.script.run({ docs: layoutDocs, collection: this.Document }, console.log);
         let state = initResult && initResult.success ? initResult.result.scriptState : undefined;
-        let elements = initResult && initResult.success ? this.viewDefsToJSX(initResult.result.views) : [];
+        const elements = initResult && initResult.success ? this.viewDefsToJSX(initResult.result.views) : [];
 
         this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map((pair, i) => {
             const data = poolData.get(pair.layout[Id]);
@@ -737,7 +738,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     layoutDocsInGrid = () => {
         UndoManager.RunInBatch(() => {
             const docs = DocListCast(this.Document[this.props.fieldKey]);
-            let startX = this.Document.panX || 0;
+            const startX = this.Document.panX || 0;
             let x = startX;
             let y = this.Document.panY || 0;
             let i = 0;
@@ -762,8 +763,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         this.Document.isRuleProvider && this.childLayoutPairs.map(pair =>
             // iterate over the children of a displayed document (or if the displayed document is a template, iterate over the children of that template)
             DocListCast(Doc.Layout(pair.layout).data).map(heading => {
-                let headingPair = Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, heading);
-                let headingLayout = headingPair.layout && (pair.layout.data_ext instanceof Doc) && (pair.layout.data_ext[`Layout[${headingPair.layout[Id]}]`] as Doc) || headingPair.layout;
+                const headingPair = Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, heading);
+                const headingLayout = headingPair.layout && (pair.layout.data_ext instanceof Doc) && (pair.layout.data_ext[`Layout[${headingPair.layout[Id]}]`] as Doc) || headingPair.layout;
                 if (headingLayout && NumCast(headingLayout.heading) > 0 && headingLayout.backgroundColor !== headingLayout.defaultBackgroundColor) {
                     Doc.GetProto(this.props.Document)["ruleColor_" + NumCast(headingLayout.heading)] = headingLayout.backgroundColor;
                 }
@@ -772,11 +773,23 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     }
 
     analyzeStrokes = async () => {
-        //  CognitiveServices.Inking.Appliers.ConcatenateHandwriting(this.dataDoc, ["inkAnalysis", "handwriting"], data.inkData);
+        const children = await DocListCastAsync(this.dataDoc.data);
+        if (!children) {
+            return;
+        }
+        const inkData: InkData[] = [];
+        for (const doc of children) {
+            const data = Cast(doc.data, InkField)?.inkData;
+            data && inkData.push(data);
+        }
+        if (!inkData.length) {
+            return;
+        }
+        CognitiveServices.Inking.Appliers.ConcatenateHandwriting(this.dataDoc, ["inkAnalysis", "handwriting"], inkData);
     }
 
     onContextMenu = (e: React.MouseEvent) => {
-        let layoutItems: ContextMenuProps[] = [];
+        const layoutItems: ContextMenuProps[] = [];
 
         if (this.childDocs.some(d => BoolCast(d.isTemplateDoc))) {
             layoutItems.push({ description: "Template Layout Instance", event: () => this.props.addDocTab(Doc.ApplyTemplate(this.props.Document)!, undefined, "onRight"), icon: "project-diagram" });
@@ -795,7 +808,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
                 input.accept = ".zip";
                 input.onchange = async _e => {
                     const upload = Utils.prepend("/uploadDoc");
-                    let formData = new FormData();
+                    const formData = new FormData();
                     const file = input.files && input.files[0];
                     if (file) {
                         formData.append('file', file);
@@ -830,7 +843,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
 
 
     private childViews = () => {
-        let children = typeof this.props.children === "function" ? (this.props.children as any)() as JSX.Element[] : [];
+        const children = typeof this.props.children === "function" ? (this.props.children as any)() as JSX.Element[] : [];
         return [
             ...children,
             ...this.views,
@@ -838,12 +851,12 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     }
 
     @computed get svgBounds() {
-        let xs = this._points.map(p => p.x);
-        let ys = this._points.map(p => p.y);
-        let right = Math.max(...xs);
-        let left = Math.min(...xs);
-        let bottom = Math.max(...ys);
-        let top = Math.min(...ys);
+        const xs = this._points.map(p => p.x);
+        const ys = this._points.map(p => p.y);
+        const right = Math.max(...xs);
+        const left = Math.min(...xs);
+        const bottom = Math.max(...ys);
+        const top = Math.min(...ys);
         return { right: right, left: left, bottom: bottom, top: top, width: right - left, height: bottom - top };
     }
 
@@ -852,7 +865,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
             return (null);
         }
 
-        let B = this.svgBounds;
+        const B = this.svgBounds;
 
         return (
             <svg width={B.width} height={B.height} style={{ transform: `translate(${B.left}px, ${B.top}px)` }}>
@@ -862,7 +875,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     }
 
     children = () => {
-        let eles: JSX.Element[] = [];
+        const eles: JSX.Element[] = [];
         this.extensionDoc && (eles.push(...this.childViews()));
         this.currentStroke && (eles.push(this.currentStroke));
         eles.push(<CollectionFreeFormRemoteCursors {...this.props} key="remoteCursors" />);
@@ -917,7 +930,7 @@ interface CollectionFreeFormViewPannableContentsProps {
 @observer
 class CollectionFreeFormViewPannableContents extends React.Component<CollectionFreeFormViewPannableContentsProps>{
     render() {
-        let freeformclass = "collectionfreeformview" + (this.props.easing() ? "-ease" : "-none");
+        const freeformclass = "collectionfreeformview" + (this.props.easing() ? "-ease" : "-none");
         const cenx = this.props.centeringShiftX();
         const ceny = this.props.centeringShiftY();
         const panx = -this.props.panX();
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx
index 28ddc19d7..32e39d25e 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx
@@ -21,7 +21,7 @@ export default class MarqueeOptionsMenu extends AntimodeMenu {
     }
 
     render() {
-        let buttons = [
+        const buttons = [
             <button
                 className="antimodeMenu-button"
                 title="Create a Collection"
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 5ed3fecb5..dc68ae81c 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -1,7 +1,7 @@
 import { action, computed, observable } from "mobx";
 import { observer } from "mobx-react";
 import { Doc, DocListCast } from "../../../../new_fields/Doc";
-import { InkField, PointData } from "../../../../new_fields/InkField";
+import { InkField } from "../../../../new_fields/InkField";
 import { List } from "../../../../new_fields/List";
 import { listSpec } from "../../../../new_fields/Schema";
 import { SchemaHeaderField } from "../../../../new_fields/SchemaHeaderField";
@@ -15,11 +15,9 @@ import { Transform } from "../../../util/Transform";
 import { undoBatch } from "../../../util/UndoManager";
 import { PreviewCursor } from "../../PreviewCursor";
 import { CollectionViewType } from "../CollectionView";
-import { CollectionFreeFormView } from "./CollectionFreeFormView";
 import "./MarqueeView.scss";
 import React = require("react");
 import MarqueeOptionsMenu from "./MarqueeOptionsMenu";
-import InkSelectDecorations from "../../InkSelectDecorations";
 import { SubCollectionViewProps } from "../CollectionSubView";
 
 interface MarqueeViewProps {
@@ -67,26 +65,27 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
     @action
     onKeyPress = (e: KeyboardEvent) => {
         //make textbox and add it to this collection
+        // tslint:disable-next-line:prefer-const
         let [x, y] = this.props.getTransform().transformPoint(this._downX, this._downY);
         if (e.key === "q" && e.ctrlKey) {
             e.preventDefault();
             (async () => {
-                let text: string = await navigator.clipboard.readText();
-                let ns = text.split("\n").filter(t => t.trim() !== "\r" && t.trim() !== "");
+                const text: string = await navigator.clipboard.readText();
+                const ns = text.split("\n").filter(t => t.trim() !== "\r" && t.trim() !== "");
                 for (let i = 0; i < ns.length - 1; i++) {
                     while (!(ns[i].trim() === "" || ns[i].endsWith("-\r") || ns[i].endsWith("-") ||
                         ns[i].endsWith(";\r") || ns[i].endsWith(";") ||
                         ns[i].endsWith(".\r") || ns[i].endsWith(".") ||
                         ns[i].endsWith(":\r") || ns[i].endsWith(":")) && i < ns.length - 1) {
-                        let sub = ns[i].endsWith("\r") ? 1 : 0;
-                        let br = ns[i + 1].trim() === "";
+                        const sub = ns[i].endsWith("\r") ? 1 : 0;
+                        const br = ns[i + 1].trim() === "";
                         ns.splice(i, 2, ns[i].substr(0, ns[i].length - sub) + ns[i + 1].trimLeft());
                         if (br) break;
                     }
                 }
                 ns.map(line => {
-                    let indent = line.search(/\S|$/);
-                    let newBox = Docs.Create.TextDocument({ width: 200, height: 35, x: x + indent / 3 * 10, y: y, documentText: "@@@" + line, title: line });
+                    const indent = line.search(/\S|$/);
+                    const newBox = Docs.Create.TextDocument({ width: 200, height: 35, x: x + indent / 3 * 10, y: y, documentText: "@@@" + line, title: line });
                     this.props.addDocument(newBox);
                     y += 40 * this.props.getTransform().Scale;
                 });
@@ -94,7 +93,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
         } else if (e.key === "b" && e.ctrlKey) {
             e.preventDefault();
             navigator.clipboard.readText().then(text => {
-                let ns = text.split("\n").filter(t => t.trim() !== "\r" && t.trim() !== "");
+                const ns = text.split("\n").filter(t => t.trim() !== "\r" && t.trim() !== "");
                 if (ns.length === 1 && text.startsWith("http")) {
                     this.props.addDocument(Docs.Create.ImageDocument(text, { nativeWidth: 300, width: 300, x: x, y: y }));// paste an image from its URL in the paste buffer
                 } else {
@@ -105,8 +104,8 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
             this.props.addLiveTextDocument(
                 Docs.Create.TextDocument({ width: 200, height: 100, x: x, y: y, autoHeight: true, title: "-typed text-" }));
         } else if (e.keyCode > 48 && e.keyCode <= 57) {
-            let notes = DocListCast((CurrentUserUtils.UserDocument.noteTypes as Doc).data);
-            let text = Docs.Create.TextDocument({ width: 200, height: 100, x: x, y: y, autoHeight: true, title: "-typed text-" });
+            const notes = DocListCast((CurrentUserUtils.UserDocument.noteTypes as Doc).data);
+            const text = Docs.Create.TextDocument({ width: 200, height: 100, x: x, y: y, autoHeight: true, title: "-typed text-" });
             text.layout = notes[(e.keyCode - 49) % notes.length];
             this.props.addLiveTextDocument(text);
         }
@@ -124,31 +123,31 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
             ns.splice(0, 1);
         }
         if (ns.length > 0) {
-            let columns = ns[0].split("\t");
-            let docList: Doc[] = [];
+            const columns = ns[0].split("\t");
+            const docList: Doc[] = [];
             let groupAttr: string | number = "";
-            let rowProto = new Doc();
+            const rowProto = new Doc();
             rowProto.title = rowProto.Id;
             rowProto.width = 200;
             rowProto.isPrototype = true;
             for (let i = 1; i < ns.length - 1; i++) {
-                let values = ns[i].split("\t");
+                const values = ns[i].split("\t");
                 if (values.length === 1 && columns.length > 1) {
                     groupAttr = values[0];
                     continue;
                 }
-                let docDataProto = Doc.MakeDelegate(rowProto);
+                const docDataProto = Doc.MakeDelegate(rowProto);
                 docDataProto.isPrototype = true;
                 columns.forEach((col, i) => docDataProto[columns[i]] = (values.length > i ? ((values[i].indexOf(Number(values[i]).toString()) !== -1) ? Number(values[i]) : values[i]) : undefined));
                 if (groupAttr) {
                     docDataProto._group = groupAttr;
                 }
                 docDataProto.title = i.toString();
-                let doc = Doc.MakeDelegate(docDataProto);
+                const doc = Doc.MakeDelegate(docDataProto);
                 doc.width = 200;
                 docList.push(doc);
             }
-            let newCol = Docs.Create.SchemaDocument([...(groupAttr ? [new SchemaHeaderField("_group", "#f1efeb")] : []), ...columns.filter(c => c).map(c => new SchemaHeaderField(c, "#f1efeb"))], docList, { x: x, y: y, title: "droppedTable", width: 300, height: 100 });
+            const newCol = Docs.Create.SchemaDocument([...(groupAttr ? [new SchemaHeaderField("_group", "#f1efeb")] : []), ...columns.filter(c => c).map(c => new SchemaHeaderField(c, "#f1efeb"))], docList, { x: x, y: y, title: "droppedTable", width: 300, height: 100 });
 
             this.props.addDocument(newCol);
         }
@@ -193,13 +192,13 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
     onPointerUp = (e: PointerEvent): void => {
         if (!this.props.active(true)) this.props.selectDocuments([this.props.Document], []);
         if (this._visible) {
-            let mselect = this.marqueeSelect();
+            const mselect = this.marqueeSelect();
             if (!e.shiftKey) {
                 SelectionManager.DeselectAll(mselect.length ? undefined : this.props.Document);
             }
             // let inkselect = this.ink ? this.marqueeInkSelect(this.ink.inkData) : new Map();
             // let inks = inkselect.size ? [{ Document: this.inkDoc, Ink: inkselect }] : [];
-            let docs = mselect.length ? mselect : [this.props.Document];
+            const docs = mselect.length ? mselect : [this.props.Document];
             this.props.selectDocuments(docs, []);
         }
         if (!this._commandExecuted && (Math.abs(this.Bounds.height * this.Bounds.width) > 100)) {
@@ -212,7 +211,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
         }
         this.cleanupInteractions(true, this._commandExecuted);
 
-        let hideMarquee = () => {
+        const hideMarquee = () => {
             this.hideMarquee();
             MarqueeOptionsMenu.Instance.fadeOut(true);
             document.removeEventListener("pointerdown", hideMarquee);
@@ -260,10 +259,10 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
 
     @computed
     get Bounds() {
-        let left = this._downX < this._lastX ? this._downX : this._lastX;
-        let top = this._downY < this._lastY ? this._downY : this._lastY;
-        let topLeft = this.props.getTransform().transformPoint(left, top);
-        let size = this.props.getTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY);
+        const left = this._downX < this._lastX ? this._downX : this._lastX;
+        const top = this._downY < this._lastY ? this._downY : this._lastY;
+        const topLeft = this.props.getTransform().transformPoint(left, top);
+        const size = this.props.getTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY);
         return { left: topLeft[0], top: topLeft[1], width: Math.abs(size[0]), height: Math.abs(size[1]) };
     }
 
@@ -302,15 +301,15 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
     }
 
     getCollection = (selected: Doc[]) => {
-        let bounds = this.Bounds;
-        let defaultPalette = ["rgb(114,229,239)", "rgb(255,246,209)", "rgb(255,188,156)", "rgb(247,220,96)", "rgb(122,176,238)",
+        const bounds = this.Bounds;
+        const defaultPalette = ["rgb(114,229,239)", "rgb(255,246,209)", "rgb(255,188,156)", "rgb(247,220,96)", "rgb(122,176,238)",
             "rgb(209,150,226)", "rgb(127,235,144)", "rgb(252,188,189)", "rgb(247,175,81)",];
-        let colorPalette = Cast(this.props.Document.colorPalette, listSpec("string"));
+        const colorPalette = Cast(this.props.Document.colorPalette, listSpec("string"));
         if (!colorPalette) this.props.Document.colorPalette = new List<string>(defaultPalette);
-        let palette = Array.from(Cast(this.props.Document.colorPalette, listSpec("string")) as string[]);
-        let usedPaletted = new Map<string, number>();
+        const palette = Array.from(Cast(this.props.Document.colorPalette, listSpec("string")) as string[]);
+        const usedPaletted = new Map<string, number>();
         [...this.props.activeDocuments(), this.props.Document].map(child => {
-            let bg = StrCast(Doc.Layout(child).backgroundColor);
+            const bg = StrCast(Doc.Layout(child).backgroundColor);
             if (palette.indexOf(bg) !== -1) {
                 palette.splice(palette.indexOf(bg), 1);
                 if (usedPaletted.get(bg)) usedPaletted.set(bg, usedPaletted.get(bg)! + 1);
@@ -320,10 +319,10 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
         usedPaletted.delete("#f1efeb");
         usedPaletted.delete("white");
         usedPaletted.delete("rgba(255,255,255,1)");
-        let usedSequnce = Array.from(usedPaletted.keys()).sort((a, b) => usedPaletted.get(a)! < usedPaletted.get(b)! ? -1 : usedPaletted.get(a)! > usedPaletted.get(b)! ? 1 : 0);
-        let chosenColor = (usedPaletted.size === 0) ? "white" : palette.length ? palette[0] : usedSequnce[0];
-        let inkData = this.ink ? this.ink.inkData : undefined;
-        let newCollection = Docs.Create.FreeformDocument(selected, {
+        const usedSequnce = Array.from(usedPaletted.keys()).sort((a, b) => usedPaletted.get(a)! < usedPaletted.get(b)! ? -1 : usedPaletted.get(a)! > usedPaletted.get(b)! ? 1 : 0);
+        const chosenColor = (usedPaletted.size === 0) ? "white" : palette.length ? palette[0] : usedSequnce[0];
+        // const inkData = this.ink ? this.ink.inkData : undefined;
+        const newCollection = Docs.Create.FreeformDocument(selected, {
             x: bounds.left,
             y: bounds.top,
             panX: 0,
@@ -334,7 +333,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
             height: bounds.height,
             title: "a nested collection",
         });
-        let dataExtensionField = Doc.CreateDocumentExtensionForField(newCollection, "data");
+        // const dataExtensionField = Doc.CreateDocumentExtensionForField(newCollection, "data");
         // dataExtensionField.ink = inkData ? new InkField(this.marqueeInkSelect(inkData)) : undefined;
         // this.marqueeInkDelete(inkData);
         this.hideMarquee();
@@ -343,8 +342,8 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
 
     @action
     collection = (e: KeyboardEvent | React.PointerEvent | undefined) => {
-        let bounds = this.Bounds;
-        let selected = this.marqueeSelect(false);
+        const bounds = this.Bounds;
+        const selected = this.marqueeSelect(false);
         if (e instanceof KeyboardEvent ? e.key === "c" : true) {
             selected.map(d => {
                 this.props.removeDocument(d);
@@ -354,7 +353,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
                 return d;
             });
         }
-        let newCollection = this.getCollection(selected);
+        const newCollection = this.getCollection(selected);
         this.props.addDocument(newCollection);
         this.props.selectDocuments([newCollection], []);
         MarqueeOptionsMenu.Instance.fadeOut(true);
@@ -363,9 +362,9 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
 
     @action
     summary = (e: KeyboardEvent | React.PointerEvent | undefined) => {
-        let bounds = this.Bounds;
-        let selected = this.marqueeSelect(false);
-        let newCollection = this.getCollection(selected);
+        const bounds = this.Bounds;
+        const selected = this.marqueeSelect(false);
+        const newCollection = this.getCollection(selected);
 
         selected.map(d => {
             this.props.removeDocument(d);
@@ -375,13 +374,13 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
             return d;
         });
         newCollection.chromeStatus = "disabled";
-        let summary = Docs.Create.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, autoHeight: true, backgroundColor: "#e2ad32" /* yellow */, title: "-summary-" });
+        const summary = Docs.Create.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, autoHeight: true, backgroundColor: "#e2ad32" /* yellow */, title: "-summary-" });
         Doc.GetProto(summary).summarizedDocs = new List<Doc>([newCollection]);
         newCollection.x = bounds.left + bounds.width;
         Doc.GetProto(newCollection).summaryDoc = summary;
         Doc.GetProto(newCollection).title = ComputedField.MakeFunction(`summaryTitle(this);`);
         if (e instanceof KeyboardEvent ? e.key === "s" : true) { // summary is wrapped in an expand/collapse container that also contains the summarized documents in a free form view.
-            let container = Docs.Create.FreeformDocument([summary, newCollection], { x: bounds.left, y: bounds.top, width: 300, height: 200, chromeStatus: "disabled", title: "-summary-" });
+            const container = Docs.Create.FreeformDocument([summary, newCollection], { x: bounds.left, y: bounds.top, width: 300, height: 200, chromeStatus: "disabled", title: "-summary-" });
             container.viewType = CollectionViewType.Stacking;
             container.autoHeight = true;
             Doc.GetProto(summary).maximizeLocation = "inPlace";  // or "onRight"
@@ -462,42 +461,42 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
     // }
 
     marqueeSelect(selectBackgrounds: boolean = true) {
-        let selRect = this.Bounds;
-        let selection: Doc[] = [];
+        const selRect = this.Bounds;
+        const selection: Doc[] = [];
         this.props.activeDocuments().filter(doc => !doc.isBackground && doc.z === undefined).map(doc => {
-            let layoutDoc = Doc.Layout(doc);
-            var x = NumCast(doc.x);
-            var y = NumCast(doc.y);
-            var w = NumCast(layoutDoc.width);
-            var h = NumCast(layoutDoc.height);
+            const layoutDoc = Doc.Layout(doc);
+            const x = NumCast(doc.x);
+            const y = NumCast(doc.y);
+            const w = NumCast(layoutDoc.width);
+            const h = NumCast(layoutDoc.height);
             if (this.intersectRect({ left: x, top: y, width: w, height: h }, selRect)) {
                 selection.push(doc);
             }
         });
         if (!selection.length && selectBackgrounds) {
             this.props.activeDocuments().filter(doc => doc.z === undefined).map(doc => {
-                let layoutDoc = Doc.Layout(doc);
-                var x = NumCast(doc.x);
-                var y = NumCast(doc.y);
-                var w = NumCast(layoutDoc.width);
-                var h = NumCast(layoutDoc.height);
+                const layoutDoc = Doc.Layout(doc);
+                const x = NumCast(doc.x);
+                const y = NumCast(doc.y);
+                const w = NumCast(layoutDoc.width);
+                const h = NumCast(layoutDoc.height);
                 if (this.intersectRect({ left: x, top: y, width: w, height: h }, selRect)) {
                     selection.push(doc);
                 }
             });
         }
         if (!selection.length) {
-            let left = this._downX < this._lastX ? this._downX : this._lastX;
-            let top = this._downY < this._lastY ? this._downY : this._lastY;
-            let topLeft = this.props.getContainerTransform().transformPoint(left, top);
-            let size = this.props.getContainerTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY);
-            let otherBounds = { left: topLeft[0], top: topLeft[1], width: Math.abs(size[0]), height: Math.abs(size[1]) };
+            const left = this._downX < this._lastX ? this._downX : this._lastX;
+            const top = this._downY < this._lastY ? this._downY : this._lastY;
+            const topLeft = this.props.getContainerTransform().transformPoint(left, top);
+            const size = this.props.getContainerTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY);
+            const otherBounds = { left: topLeft[0], top: topLeft[1], width: Math.abs(size[0]), height: Math.abs(size[1]) };
             this.props.activeDocuments().filter(doc => doc.z !== undefined).map(doc => {
-                let layoutDoc = Doc.Layout(doc);
-                var x = NumCast(doc.x);
-                var y = NumCast(doc.y);
-                var w = NumCast(layoutDoc.width);
-                var h = NumCast(layoutDoc.height);
+                const layoutDoc = Doc.Layout(doc);
+                const x = NumCast(doc.x);
+                const y = NumCast(doc.y);
+                const w = NumCast(layoutDoc.width);
+                const h = NumCast(layoutDoc.height);
                 if (this.intersectRect({ left: x, top: y, width: w, height: h }, otherBounds)) {
                     selection.push(doc);
                 }
@@ -508,8 +507,8 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
 
     @computed
     get marqueeDiv() {
-        let p: [number, number] = this._visible ? this.props.getContainerTransform().transformPoint(this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY) : [0, 0];
-        let v = this.props.getContainerTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY);
+        const p: [number, number] = this._visible ? this.props.getContainerTransform().transformPoint(this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY) : [0, 0];
+        const v = this.props.getContainerTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY);
         /**
          * @RE - The commented out span below
          * This contains the "C for collection, ..." text on marquees.
diff --git a/src/client/views/document_templates/caption_toggle/DetailedCaptionToggle.tsx b/src/client/views/document_templates/caption_toggle/DetailedCaptionToggle.tsx
index f8104cef3..3aaf4120c 100644
--- a/src/client/views/document_templates/caption_toggle/DetailedCaptionToggle.tsx
+++ b/src/client/views/document_templates/caption_toggle/DetailedCaptionToggle.tsx
@@ -1,8 +1,8 @@
 import * as React from 'react';
-import { FontWeightProperty, FontStyleProperty, FontSizeProperty, ColorProperty } from 'csstype';
+import { FontStyleProperty, ColorProperty } from 'csstype';
 import { observer } from 'mobx-react';
 import { observable, action, runInAction } from 'mobx';
-import { FormattedTextBox, FormattedTextBoxProps } from '../../nodes/FormattedTextBox';
+import { FormattedTextBox } from '../../nodes/FormattedTextBox';
 import { FieldViewProps } from '../../nodes/FieldView';
 
 interface DetailedCaptionDataProps {
@@ -33,7 +33,7 @@ export default class DetailedCaptionToggle extends React.Component<DetailedCapti
     }
 
     render() {
-        let size = this.props.toggleSize || 20;
+        const size = this.props.toggleSize || 20;
         return (
             <div style={{
                 transition: "0.5s opacity ease",
diff --git a/src/client/views/linking/LinkEditor.tsx b/src/client/views/linking/LinkEditor.tsx
index ecb3e9db4..bb8a8b47b 100644
--- a/src/client/views/linking/LinkEditor.tsx
+++ b/src/client/views/linking/LinkEditor.tsx
@@ -43,12 +43,12 @@ class GroupTypesDropdown extends React.Component<GroupTypesDropdownProps> {
     @action
     onKeyDown = (e: React.KeyboardEvent): void => {
         if (e.key === "Enter") {
-            let allGroupTypes = Array.from(LinkManager.Instance.getAllGroupTypes());
-            let groupOptions = allGroupTypes.filter(groupType => groupType.toUpperCase().indexOf(this._searchTerm.toUpperCase()) > -1);
-            let exactFound = groupOptions.findIndex(groupType => groupType.toUpperCase() === this._searchTerm.toUpperCase());
+            const allGroupTypes = Array.from(LinkManager.Instance.getAllGroupTypes());
+            const groupOptions = allGroupTypes.filter(groupType => groupType.toUpperCase().indexOf(this._searchTerm.toUpperCase()) > -1);
+            const exactFound = groupOptions.findIndex(groupType => groupType.toUpperCase() === this._searchTerm.toUpperCase());
 
             if (exactFound > -1) {
-                let groupType = groupOptions[exactFound];
+                const groupType = groupOptions[exactFound];
                 this.props.setGroupType(groupType);
                 this._groupType = groupType;
             } else {
@@ -84,19 +84,19 @@ class GroupTypesDropdown extends React.Component<GroupTypesDropdownProps> {
     renderOptions = (): JSX.Element[] | JSX.Element => {
         if (this._searchTerm === "") return <></>;
 
-        let allGroupTypes = Array.from(LinkManager.Instance.getAllGroupTypes());
-        let groupOptions = allGroupTypes.filter(groupType => groupType.toUpperCase().indexOf(this._searchTerm.toUpperCase()) > -1);
-        let exactFound = groupOptions.findIndex(groupType => groupType.toUpperCase() === this._searchTerm.toUpperCase()) > -1;
+        const allGroupTypes = Array.from(LinkManager.Instance.getAllGroupTypes());
+        const groupOptions = allGroupTypes.filter(groupType => groupType.toUpperCase().indexOf(this._searchTerm.toUpperCase()) > -1);
+        const exactFound = groupOptions.findIndex(groupType => groupType.toUpperCase() === this._searchTerm.toUpperCase()) > -1;
 
-        let options = groupOptions.map(groupType => {
-            let ref = React.createRef<HTMLDivElement>();
+        const options = groupOptions.map(groupType => {
+            const ref = React.createRef<HTMLDivElement>();
             return <div key={groupType} ref={ref} className="linkEditor-option"
                 onClick={() => this.onOptionClick(groupType, false)}>{groupType}</div>;
         });
 
         // if search term does not already exist as a group type, give option to create new group type
         if (!exactFound && this._searchTerm !== "") {
-            let ref = React.createRef<HTMLDivElement>();
+            const ref = React.createRef<HTMLDivElement>();
             options.push(<div key={""} ref={ref} className="linkEditor-option"
                 onClick={() => this.onOptionClick(this._searchTerm, true)}>Define new "{this._searchTerm}" relationship</div>);
         }
@@ -138,10 +138,10 @@ class LinkMetadataEditor extends React.Component<LinkMetadataEditorProps> {
 
     @action
     setMetadataKey = (value: string): void => {
-        let groupMdKeys = LinkManager.Instance.getMetadataKeysInGroup(this.props.groupType);
+        const groupMdKeys = LinkManager.Instance.getMetadataKeysInGroup(this.props.groupType);
 
         // don't allow user to create existing key
-        let newIndex = groupMdKeys.findIndex(key => key.toUpperCase() === value.toUpperCase());
+        const newIndex = groupMdKeys.findIndex(key => key.toUpperCase() === value.toUpperCase());
         if (newIndex > -1) {
             this._keyError = true;
             this._key = value;
@@ -151,7 +151,7 @@ class LinkMetadataEditor extends React.Component<LinkMetadataEditorProps> {
         }
 
         // set new value for key
-        let currIndex = groupMdKeys.findIndex(key => {
+        const currIndex = groupMdKeys.findIndex(key => {
             return StrCast(key).toUpperCase() === this._key.toUpperCase();
         });
         if (currIndex === -1) console.error("LinkMetadataEditor: key was not found");
@@ -172,9 +172,9 @@ class LinkMetadataEditor extends React.Component<LinkMetadataEditorProps> {
 
     @action
     removeMetadata = (): void => {
-        let groupMdKeys = LinkManager.Instance.getMetadataKeysInGroup(this.props.groupType);
+        const groupMdKeys = LinkManager.Instance.getMetadataKeysInGroup(this.props.groupType);
 
-        let index = groupMdKeys.findIndex(key => key.toUpperCase() === this._key.toUpperCase());
+        const index = groupMdKeys.findIndex(key => key.toUpperCase() === this._key.toUpperCase());
         if (index === -1) console.error("LinkMetadataEditor: key was not found");
         groupMdKeys.splice(index, 1);
 
@@ -206,7 +206,7 @@ export class LinkGroupEditor extends React.Component<LinkGroupEditorProps> {
     constructor(props: LinkGroupEditorProps) {
         super(props);
 
-        let groupMdKeys = LinkManager.Instance.getMetadataKeysInGroup(StrCast(props.groupDoc.type));
+        const groupMdKeys = LinkManager.Instance.getMetadataKeysInGroup(StrCast(props.groupDoc.type));
         groupMdKeys.forEach(key => {
             this._metadataIds.set(key, Utils.GenerateGuid());
         });
@@ -226,25 +226,25 @@ export class LinkGroupEditor extends React.Component<LinkGroupEditorProps> {
     }
 
     copyGroup = async (groupType: string): Promise<void> => {
-        let sourceGroupDoc = this.props.groupDoc;
+        const sourceGroupDoc = this.props.groupDoc;
         const sourceMdDoc = await Cast(sourceGroupDoc.metadata, Doc);
         if (!sourceMdDoc) return;
 
-        let destDoc = LinkManager.Instance.getOppositeAnchor(this.props.linkDoc, this.props.sourceDoc);
+        const destDoc = LinkManager.Instance.getOppositeAnchor(this.props.linkDoc, this.props.sourceDoc);
         // let destGroupList = LinkManager.Instance.getAnchorGroups(this.props.linkDoc, destDoc);
-        let keys = LinkManager.Instance.getMetadataKeysInGroup(groupType);
+        const keys = LinkManager.Instance.getMetadataKeysInGroup(groupType);
 
         // create new metadata doc with copied kvp
-        let destMdDoc = new Doc();
+        const destMdDoc = new Doc();
         destMdDoc.anchor1 = StrCast(sourceMdDoc.anchor2);
         destMdDoc.anchor2 = StrCast(sourceMdDoc.anchor1);
         keys.forEach(key => {
-            let val = sourceMdDoc[key] === undefined ? "" : StrCast(sourceMdDoc[key]);
+            const val = sourceMdDoc[key] === undefined ? "" : StrCast(sourceMdDoc[key]);
             destMdDoc[key] = val;
         });
 
         // create new group doc with new metadata doc
-        let destGroupDoc = new Doc();
+        const destGroupDoc = new Doc();
         destGroupDoc.type = groupType;
         destGroupDoc.metadata = destMdDoc;
 
@@ -256,7 +256,7 @@ export class LinkGroupEditor extends React.Component<LinkGroupEditorProps> {
     @action
     addMetadata = (groupType: string): void => {
         this._metadataIds.set("new key", Utils.GenerateGuid());
-        let mdKeys = LinkManager.Instance.getMetadataKeysInGroup(groupType);
+        const mdKeys = LinkManager.Instance.getMetadataKeysInGroup(groupType);
         // only add "new key" if there is no other key with value "new key"; prevents spamming
         if (mdKeys.indexOf("new key") === -1) mdKeys.push("new key");
         LinkManager.Instance.setMetadataKeysForGroup(groupType, mdKeys);
@@ -268,17 +268,17 @@ export class LinkGroupEditor extends React.Component<LinkGroupEditorProps> {
     }
 
     renderMetadata = (): JSX.Element[] => {
-        let metadata: Array<JSX.Element> = [];
-        let groupDoc = this.props.groupDoc;
+        const metadata: Array<JSX.Element> = [];
+        const groupDoc = this.props.groupDoc;
         const mdDoc = FieldValue(Cast(groupDoc.metadata, Doc));
         if (!mdDoc) {
             return [];
         }
-        let groupType = StrCast(groupDoc.type);
-        let groupMdKeys = LinkManager.Instance.getMetadataKeysInGroup(groupType);
+        const groupType = StrCast(groupDoc.type);
+        const groupMdKeys = LinkManager.Instance.getMetadataKeysInGroup(groupType);
 
         groupMdKeys.forEach((key) => {
-            let val = StrCast(mdDoc[key]);
+            const val = StrCast(mdDoc[key]);
             metadata.push(
                 <LinkMetadataEditor key={"mded-" + this._metadataIds.get(key)} id={this._metadataIds.get(key)!} groupType={groupType} mdDoc={mdDoc} mdKey={key} mdValue={val} changeMdIdKey={this.changeMdIdKey} />
             );
@@ -287,18 +287,18 @@ export class LinkGroupEditor extends React.Component<LinkGroupEditorProps> {
     }
 
     viewGroupAsTable = (groupType: string): JSX.Element => {
-        let keys = LinkManager.Instance.getMetadataKeysInGroup(groupType);
-        let index = keys.indexOf("");
+        const keys = LinkManager.Instance.getMetadataKeysInGroup(groupType);
+        const index = keys.indexOf("");
         if (index > -1) keys.splice(index, 1);
-        let cols = ["anchor1", "anchor2", ...[...keys]].map(c => new SchemaHeaderField(c, "#f1efeb"));
-        let docs: Doc[] = LinkManager.Instance.getAllMetadataDocsInGroup(groupType);
-        let createTable = action(() => Docs.Create.SchemaDocument(cols, docs, { width: 500, height: 300, title: groupType + " table" }));
-        let ref = React.createRef<HTMLDivElement>();
+        const cols = ["anchor1", "anchor2", ...[...keys]].map(c => new SchemaHeaderField(c, "#f1efeb"));
+        const docs: Doc[] = LinkManager.Instance.getAllMetadataDocsInGroup(groupType);
+        const createTable = action(() => Docs.Create.SchemaDocument(cols, docs, { width: 500, height: 300, title: groupType + " table" }));
+        const ref = React.createRef<HTMLDivElement>();
         return <div ref={ref}><button className="linkEditor-button" onPointerDown={SetupDrag(ref, createTable)} title="Drag to view relationship table"><FontAwesomeIcon icon="table" size="sm" /></button></div>;
     }
 
     render() {
-        let groupType = StrCast(this.props.groupDoc.type);
+        const groupType = StrCast(this.props.groupDoc.type);
         // if ((groupType && LinkManager.Instance.getMetadataKeysInGroup(groupType).length > 0) || groupType === "") {
         let buttons;
         if (groupType === "") {
@@ -356,15 +356,15 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
     @action
     addGroup = (): void => {
         // create new metadata document for group
-        let mdDoc = new Doc();
+        const mdDoc = new Doc();
         mdDoc.anchor1 = this.props.sourceDoc.title;
-        let opp = LinkManager.Instance.getOppositeAnchor(this.props.linkDoc, this.props.sourceDoc);
+        const opp = LinkManager.Instance.getOppositeAnchor(this.props.linkDoc, this.props.sourceDoc);
         if (opp) {
             mdDoc.anchor2 = opp.title;
         }
 
         // create new group document
-        let groupDoc = new Doc();
+        const groupDoc = new Doc();
         groupDoc.type = "";
         groupDoc.metadata = mdDoc;
 
@@ -372,10 +372,10 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
     }
 
     render() {
-        let destination = LinkManager.Instance.getOppositeAnchor(this.props.linkDoc, this.props.sourceDoc);
+        const destination = LinkManager.Instance.getOppositeAnchor(this.props.linkDoc, this.props.sourceDoc);
 
-        let groupList = LinkManager.Instance.getAnchorGroups(this.props.linkDoc, this.props.sourceDoc);
-        let groups = groupList.map(groupDoc => {
+        const groupList = LinkManager.Instance.getAnchorGroups(this.props.linkDoc, this.props.sourceDoc);
+        const groups = groupList.map(groupDoc => {
             return <LinkGroupEditor key={"gred-" + StrCast(groupDoc.type)} linkDoc={this.props.linkDoc} sourceDoc={this.props.sourceDoc} groupDoc={groupDoc} />;
         });
 
diff --git a/src/client/views/linking/LinkFollowBox.tsx b/src/client/views/linking/LinkFollowBox.tsx
index efe2c7f2a..29e167ff7 100644
--- a/src/client/views/linking/LinkFollowBox.tsx
+++ b/src/client/views/linking/LinkFollowBox.tsx
@@ -68,14 +68,14 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
         this._contextDisposer = reaction(
             () => this.selectedContextString,
             async () => {
-                let ref = await DocServer.GetRefField(this.selectedContextString);
+                const ref = await DocServer.GetRefField(this.selectedContextString);
                 runInAction(() => {
                     if (ref instanceof Doc) {
                         this.selectedContext = ref;
                     }
                 });
                 if (this.selectedContext instanceof Doc) {
-                    let aliases = await SearchUtil.GetViewsOfDocument(this.selectedContext);
+                    const aliases = await SearchUtil.GetViewsOfDocument(this.selectedContext);
                     runInAction(() => { this.selectedContextAliases = aliases; });
                 }
             }
@@ -90,8 +90,8 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
         if (LinkFollowBox.destinationDoc && this.sourceView && this.sourceView.props.ContainingCollectionDoc) {
             runInAction(() => this.canPan = false);
             if (this.sourceView.props.ContainingCollectionDoc.viewType === CollectionViewType.Freeform) {
-                let docs = Cast(this.sourceView.props.ContainingCollectionDoc.data, listSpec(Doc), []);
-                let aliases = await SearchUtil.GetViewsOfDocument(Doc.GetProto(LinkFollowBox.destinationDoc));
+                const docs = Cast(this.sourceView.props.ContainingCollectionDoc.data, listSpec(Doc), []);
+                const aliases = await SearchUtil.GetViewsOfDocument(Doc.GetProto(LinkFollowBox.destinationDoc));
 
                 aliases.forEach(alias => {
                     if (docs.filter(doc => doc === alias).length > 0) {
@@ -118,8 +118,8 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
 
     async fetchDocuments() {
         if (LinkFollowBox.destinationDoc) {
-            let dest: Doc = LinkFollowBox.destinationDoc;
-            let aliases = await SearchUtil.GetViewsOfDocument(Doc.GetProto(dest));
+            const dest: Doc = LinkFollowBox.destinationDoc;
+            const aliases = await SearchUtil.GetViewsOfDocument(Doc.GetProto(dest));
             const { docs } = await SearchUtil.Search("", true, { fq: `data_l:"${dest[Id]}"` });
             const map: Map<Doc, Doc> = new Map;
             const allDocs = await Promise.all(aliases.map(doc => SearchUtil.Search("", true, { fq: `data_l:"${doc[Id]}"` }).then(result => result.docs)));
@@ -128,7 +128,7 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
             runInAction(async () => {
                 this._docs = docs.filter(doc => !Doc.AreProtosEqual(doc, CollectionDockingView.Instance.props.Document)).map(doc => ({ col: doc, target: dest }));
                 this._otherDocs = Array.from(map.entries()).filter(entry => !Doc.AreProtosEqual(entry[0], CollectionDockingView.Instance.props.Document)).map(([col, target]) => ({ col, target }));
-                let tcontext = LinkFollowBox.linkDoc && (await Cast(LinkFollowBox.linkDoc.anchor2Context, Doc)) as Doc;
+                const tcontext = LinkFollowBox.linkDoc && (await Cast(LinkFollowBox.linkDoc.anchor2Context, Doc)) as Doc;
                 runInAction(() => tcontext && this._docs.splice(0, 0, { col: tcontext, target: dest }));
             });
         }
@@ -157,7 +157,7 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
     @undoBatch
     openFullScreen = () => {
         if (LinkFollowBox.destinationDoc) {
-            let view = DocumentManager.Instance.getDocumentView(LinkFollowBox.destinationDoc);
+            const view = DocumentManager.Instance.getDocumentView(LinkFollowBox.destinationDoc);
             view && CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(view);
         }
     }
@@ -171,7 +171,7 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
                 options.context.panX = newPanX;
                 options.context.panY = newPanY;
             }
-            let view = DocumentManager.Instance.getDocumentView(options.context);
+            const view = DocumentManager.Instance.getDocumentView(options.context);
             view && CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(view);
             this.highlightDoc();
         }
@@ -211,7 +211,7 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
     @undoBatch
     openLinkRight = () => {
         if (LinkFollowBox.destinationDoc) {
-            let alias = Doc.MakeAlias(LinkFollowBox.destinationDoc);
+            const alias = Doc.MakeAlias(LinkFollowBox.destinationDoc);
             (LinkFollowBox._addDocTab || this.props.addDocTab)(alias, undefined, "onRight");
             this.highlightDoc();
             SelectionManager.DeselectAll();
@@ -222,7 +222,7 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
     @undoBatch
     jumpToLink = async (options: { shouldZoom: boolean }) => {
         if (LinkFollowBox.sourceDoc && LinkFollowBox.linkDoc) {
-            let focus = (document: Doc) => { (LinkFollowBox._addDocTab || this.props.addDocTab)(document, undefined, "inTab"); SelectionManager.DeselectAll(); };
+            const focus = (document: Doc) => { (LinkFollowBox._addDocTab || this.props.addDocTab)(document, undefined, "inTab"); SelectionManager.DeselectAll(); };
             //let focus = (doc: Doc, maxLocation: string) => this.props.focus(docthis.props.focus(LinkFollowBox.destinationDoc, true, 1, () => this.props.addDocTab(doc, undefined, maxLocation));
 
             DocumentManager.Instance.FollowLink(LinkFollowBox.linkDoc, LinkFollowBox.sourceDoc, focus, options && options.shouldZoom, false, undefined);
@@ -232,7 +232,7 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
     @undoBatch
     openLinkTab = () => {
         if (LinkFollowBox.destinationDoc) {
-            let fullScreenAlias = Doc.MakeAlias(LinkFollowBox.destinationDoc);
+            const fullScreenAlias = Doc.MakeAlias(LinkFollowBox.destinationDoc);
             // this.prosp.addDocTab is empty -- use the link source's addDocTab 
             (LinkFollowBox._addDocTab || this.props.addDocTab)(fullScreenAlias, undefined, "inTab");
 
@@ -264,14 +264,14 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
 
         if (LinkFollowBox.destinationDoc && LinkFollowBox.sourceDoc) {
             if (this.sourceView && this.sourceView.props.addDocument) {
-                let destViews = DocumentManager.Instance.getDocumentViews(LinkFollowBox.destinationDoc);
+                const destViews = DocumentManager.Instance.getDocumentViews(LinkFollowBox.destinationDoc);
                 if (!destViews.find(dv => dv.props.ContainingCollectionView === this.sourceView!.props.ContainingCollectionView)) {
-                    let alias = Doc.MakeAlias(LinkFollowBox.destinationDoc);
-                    let y = NumCast(LinkFollowBox.sourceDoc.y);
-                    let x = NumCast(LinkFollowBox.sourceDoc.x);
+                    const alias = Doc.MakeAlias(LinkFollowBox.destinationDoc);
+                    const y = NumCast(LinkFollowBox.sourceDoc.y);
+                    const x = NumCast(LinkFollowBox.sourceDoc.x);
 
-                    let width = NumCast(LinkFollowBox.sourceDoc.width);
-                    let height = NumCast(LinkFollowBox.sourceDoc.height);
+                    const width = NumCast(LinkFollowBox.sourceDoc.width);
+                    const height = NumCast(LinkFollowBox.sourceDoc.height);
 
                     alias.x = x + width + 30;
                     alias.y = y;
@@ -301,8 +301,8 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
                 this.selectedContext = LinkFollowBox.destinationDoc;
             }
             if (this.selectedOption === "") this.selectedOption = FollowOptions.NOZOOM;
-            let shouldZoom: boolean = this.selectedOption === FollowOptions.NOZOOM ? false : true;
-            let notOpenInContext: boolean = this.selectedContextString === "self" || this.selectedContextString === LinkFollowBox.destinationDoc[Id];
+            const shouldZoom: boolean = this.selectedOption === FollowOptions.NOZOOM ? false : true;
+            const notOpenInContext: boolean = this.selectedContextString === "self" || this.selectedContextString === LinkFollowBox.destinationDoc[Id];
 
             if (this.selectedMode === FollowModes.INPLACE) {
                 if (shouldZoom !== undefined) this.openLinkInPlace({ shouldZoom: shouldZoom });
@@ -328,7 +328,7 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
 
     @action
     handleModeChange = (e: React.ChangeEvent) => {
-        let target = e.target as HTMLInputElement;
+        const target = e.target as HTMLInputElement;
         this.selectedMode = target.value;
         this.selectedContext = undefined;
         this.selectedContextString = "";
@@ -345,13 +345,13 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
 
     @action
     handleOptionChange = (e: React.ChangeEvent) => {
-        let target = e.target as HTMLInputElement;
+        const target = e.target as HTMLInputElement;
         this.selectedOption = target.value;
     }
 
     @action
     handleContextChange = (e: React.ChangeEvent) => {
-        let target = e.target as HTMLInputElement;
+        const target = e.target as HTMLInputElement;
         this.selectedContextString = target.value;
         // selectedContext is updated in reaction
         this.selectedOption = "";
@@ -360,7 +360,7 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
     @computed
     get canOpenInPlace() {
         if (this.sourceView && this.sourceView.props.ContainingCollectionDoc) {
-            let colDoc = this.sourceView.props.ContainingCollectionDoc;
+            const colDoc = this.sourceView.props.ContainingCollectionDoc;
             if (colDoc.viewType && colDoc.viewType === CollectionViewType.Freeform) return true;
         }
         return false;
diff --git a/src/client/views/linking/LinkMenu.tsx b/src/client/views/linking/LinkMenu.tsx
index 27af873b5..52628ba4c 100644
--- a/src/client/views/linking/LinkMenu.tsx
+++ b/src/client/views/linking/LinkMenu.tsx
@@ -34,7 +34,7 @@ export class LinkMenu extends React.Component<Props> {
     }
 
     renderAllGroups = (groups: Map<string, Array<Doc>>): Array<JSX.Element> => {
-        let linkItems: Array<JSX.Element> = [];
+        const linkItems: Array<JSX.Element> = [];
         groups.forEach((group, groupType) => {
             linkItems.push(
                 <LinkMenuGroup
@@ -55,8 +55,8 @@ export class LinkMenu extends React.Component<Props> {
     }
 
     render() {
-        let sourceDoc = this.props.docView.props.Document;
-        let groups: Map<string, Doc[]> = LinkManager.Instance.getRelatedGroupedLinks(sourceDoc);
+        const sourceDoc = this.props.docView.props.Document;
+        const groups: Map<string, Doc[]> = LinkManager.Instance.getRelatedGroupedLinks(sourceDoc);
         if (this._editingLink === undefined) {
             return (
                 <div className="linkMenu">
diff --git a/src/client/views/linking/LinkMenuGroup.tsx b/src/client/views/linking/LinkMenuGroup.tsx
index 1891919ce..15aacbbc9 100644
--- a/src/client/views/linking/LinkMenuGroup.tsx
+++ b/src/client/views/linking/LinkMenuGroup.tsx
@@ -51,11 +51,11 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> {
                 document.removeEventListener("pointermove", this.onLinkButtonMoved);
                 document.removeEventListener("pointerup", this.onLinkButtonUp);
 
-                let draggedDocs = this.props.group.map(linkDoc => {
-                    let opp = LinkManager.Instance.getOppositeAnchor(linkDoc, this.props.sourceDoc);
+                const draggedDocs = this.props.group.map(linkDoc => {
+                    const opp = LinkManager.Instance.getOppositeAnchor(linkDoc, this.props.sourceDoc);
                     if (opp) return opp;
                 }) as Doc[];
-                let dragData = new DragManager.DocumentDragData(draggedDocs);
+                const dragData = new DragManager.DocumentDragData(draggedDocs);
 
                 DragManager.StartLinkedDocumentDrag([this._drag.current], dragData, e.x, e.y, {
                     handlers: {
@@ -69,19 +69,19 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> {
     }
 
     viewGroupAsTable = (groupType: string): JSX.Element => {
-        let keys = LinkManager.Instance.getMetadataKeysInGroup(groupType);
-        let index = keys.indexOf("");
+        const keys = LinkManager.Instance.getMetadataKeysInGroup(groupType);
+        const index = keys.indexOf("");
         if (index > -1) keys.splice(index, 1);
-        let cols = ["anchor1", "anchor2", ...[...keys]].map(c => new SchemaHeaderField(c, "#f1efeb"));
-        let docs: Doc[] = LinkManager.Instance.getAllMetadataDocsInGroup(groupType);
-        let createTable = action(() => Docs.Create.SchemaDocument(cols, docs, { width: 500, height: 300, title: groupType + " table" }));
-        let ref = React.createRef<HTMLDivElement>();
+        const cols = ["anchor1", "anchor2", ...[...keys]].map(c => new SchemaHeaderField(c, "#f1efeb"));
+        const docs: Doc[] = LinkManager.Instance.getAllMetadataDocsInGroup(groupType);
+        const createTable = action(() => Docs.Create.SchemaDocument(cols, docs, { width: 500, height: 300, title: groupType + " table" }));
+        const ref = React.createRef<HTMLDivElement>();
         return <div ref={ref}><button className="linkEditor-button linkEditor-tableButton" onPointerDown={SetupDrag(ref, createTable)} title="Drag to view relationship table"><FontAwesomeIcon icon="table" size="sm" /></button></div>;
     }
 
     render() {
-        let groupItems = this.props.group.map(linkDoc => {
-            let destination = LinkManager.Instance.getOppositeAnchor(linkDoc, this.props.sourceDoc);
+        const groupItems = this.props.group.map(linkDoc => {
+            const destination = LinkManager.Instance.getOppositeAnchor(linkDoc, this.props.sourceDoc);
             if (destination && this.props.sourceDoc) {
                 return <LinkMenuItem key={destination[Id] + this.props.sourceDoc[Id]}
                     groupType={this.props.groupType}
diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx
index 238660de3..edf5e9c26 100644
--- a/src/client/views/linking/LinkMenuItem.tsx
+++ b/src/client/views/linking/LinkMenuItem.tsx
@@ -36,15 +36,15 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
     }
 
     renderMetadata = (): JSX.Element => {
-        let groups = LinkManager.Instance.getAnchorGroups(this.props.linkDoc, this.props.sourceDoc);
-        let index = groups.findIndex(groupDoc => StrCast(groupDoc.type).toUpperCase() === this.props.groupType.toUpperCase());
-        let groupDoc = index > -1 ? groups[index] : undefined;
+        const groups = LinkManager.Instance.getAnchorGroups(this.props.linkDoc, this.props.sourceDoc);
+        const index = groups.findIndex(groupDoc => StrCast(groupDoc.type).toUpperCase() === this.props.groupType.toUpperCase());
+        const groupDoc = index > -1 ? groups[index] : undefined;
 
         let mdRows: Array<JSX.Element> = [];
         if (groupDoc) {
-            let mdDoc = Cast(groupDoc.metadata, Doc, null);
+            const mdDoc = Cast(groupDoc.metadata, Doc, null);
             if (mdDoc) {
-                let keys = LinkManager.Instance.getMetadataKeysInGroup(this.props.groupType);//groupMetadataKeys.get(this.props.groupType);
+                const keys = LinkManager.Instance.getMetadataKeysInGroup(this.props.groupType);//groupMetadataKeys.get(this.props.groupType);
                 mdRows = keys.map(key => {
                     return (<div key={key} className="link-metadata-row"><b>{key}</b>: {StrCast(mdDoc[key])}</div>);
                 });
@@ -110,8 +110,8 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
 
     render() {
 
-        let keys = LinkManager.Instance.getMetadataKeysInGroup(this.props.groupType);//groupMetadataKeys.get(this.props.groupType);
-        let canExpand = keys ? keys.length > 0 : false;
+        const keys = LinkManager.Instance.getMetadataKeysInGroup(this.props.groupType);//groupMetadataKeys.get(this.props.groupType);
+        const canExpand = keys ? keys.length > 0 : false;
 
         return (
             <div className="linkMenu-item">
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 77b10e395..95c765e8a 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -56,19 +56,19 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
         this._linkPlayDisposer = reaction(() => this.layoutDoc.scrollToLinkID,
             scrollLinkId => {
                 scrollLinkId && DocListCast(this.dataDoc.links).filter(l => l[Id] === scrollLinkId).map(l => {
-                    let la1 = l.anchor1 as Doc;
-                    let linkTime = Doc.AreProtosEqual(la1, this.dataDoc) ? NumCast(l.anchor1Timecode) : NumCast(l.anchor2Timecode);
+                    const la1 = l.anchor1 as Doc;
+                    const linkTime = Doc.AreProtosEqual(la1, this.dataDoc) ? NumCast(l.anchor1Timecode) : NumCast(l.anchor2Timecode);
                     setTimeout(() => { this.playFrom(linkTime); Doc.linkFollowHighlight(l); }, 250);
                 });
                 scrollLinkId && Doc.SetInPlace(this.layoutDoc, "scrollToLinkID", undefined, false);
             }, { fireImmediately: true });
         this._reactionDisposer = reaction(() => SelectionManager.SelectedDocuments(),
             selected => {
-                let sel = selected.length ? selected[0].props.Document : undefined;
+                const sel = selected.length ? selected[0].props.Document : undefined;
                 this.Document.playOnSelect && sel && !Doc.AreProtosEqual(sel, this.props.Document) && this.playFrom(DateCast(sel.creationTime).date.getTime());
             });
         this._scrubbingDisposer = reaction(() => AudioBox._scrubTime, timeInMillisecondsFrom1970 => {
-            let start = this.extensionDoc && DateCast(this.extensionDoc.recordingStart);
+            const start = this.extensionDoc && DateCast(this.extensionDoc.recordingStart);
             start && this.playFrom((timeInMillisecondsFrom1970 - start.date.getTime()) / 1000);
         });
     }
@@ -127,7 +127,7 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
 
     recordAudioAnnotation = () => {
         let gumStream: any;
-        let self = this;
+        const self = this;
         const extensionDoc = this.extensionDoc;
         extensionDoc && navigator.mediaDevices.getUserMedia({
             audio: true
@@ -160,7 +160,7 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
     }
 
     specificContextMenu = (e: React.MouseEvent): void => {
-        let funcs: ContextMenuProps[] = [];
+        const funcs: ContextMenuProps[] = [];
         funcs.push({ description: (this.Document.playOnSelect ? "Don't play" : "Play") + " when document selected", event: () => this.Document.playOnSelect = !this.Document.playOnSelect, icon: "expand-arrows-alt" });
 
         ContextMenu.Instance.addItem({ description: "Audio Funcs...", subitems: funcs, icon: "asterisk" });
@@ -170,7 +170,7 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
         this._recorder.stop();
         this.dataDoc.duration = (new Date().getTime() - this._recordStart) / 1000;
         this._audioState = "recorded";
-        let ind = AudioBox.ActiveRecordings.indexOf(this.props.Document);
+        const ind = AudioBox.ActiveRecordings.indexOf(this.props.Document);
         ind !== -1 && (AudioBox.ActiveRecordings.splice(ind, 1));
     });
 
@@ -198,13 +198,13 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
     }
 
     @computed get path() {
-        let field = Cast(this.props.Document[this.props.fieldKey], AudioField);
-        let path = (field instanceof AudioField) ? field.url.href : "";
+        const field = Cast(this.props.Document[this.props.fieldKey], AudioField);
+        const path = (field instanceof AudioField) ? field.url.href : "";
         return path === nullAudio ? "" : path;
     }
 
     @computed get audio() {
-        let interactive = this.active() ? "-interactive" : "";
+        const interactive = this.active() ? "-interactive" : "";
         return <audio ref={this.setRef} className={`audiobox-control${interactive}`}>
             <source src={this.path} type="audio/mpeg" />
             Not supported.
@@ -212,7 +212,7 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
     }
 
     render() {
-        let interactive = this.active() ? "-interactive" : "";
+        const interactive = this.active() ? "-interactive" : "";
         return (!this.extensionDoc ? (null) :
             <div className={`audiobox-container`} onContextMenu={this.specificContextMenu}
                 onClick={!this.path ? this.recordClick : undefined}>
@@ -228,7 +228,7 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
                             <div className="audiobox-timeline" onClick={e => e.stopPropagation()}
                                 onPointerDown={e => {
                                     if (e.button === 0 && !e.ctrlKey) {
-                                        let rect = (e.target as any).getBoundingClientRect();
+                                        const rect = (e.target as any).getBoundingClientRect();
                                         this._ele!.currentTime = this.Document.currentTimecode = (e.clientX - rect.x) / rect.width * NumCast(this.dataDoc.duration);
                                         this.pause();
                                         e.stopPropagation();
diff --git a/src/client/views/nodes/ButtonBox.tsx b/src/client/views/nodes/ButtonBox.tsx
index 659ba154a..34151a311 100644
--- a/src/client/views/nodes/ButtonBox.tsx
+++ b/src/client/views/nodes/ButtonBox.tsx
@@ -51,10 +51,10 @@ export class ButtonBox extends DocComponent<FieldViewProps, ButtonDocument>(Butt
     }
 
     specificContextMenu = (e: React.MouseEvent): void => {
-        let funcs: ContextMenuProps[] = [];
+        const funcs: ContextMenuProps[] = [];
         funcs.push({
             description: "Clear Script Params", event: () => {
-                let params = FieldValue(this.Document.buttonParams);
+                const params = FieldValue(this.Document.buttonParams);
                 params && params.map(p => this.props.Document[p] = undefined);
             }, icon: "trash"
         });
@@ -73,8 +73,8 @@ export class ButtonBox extends DocComponent<FieldViewProps, ButtonDocument>(Butt
     }
     // (!missingParams || !missingParams.length ? "" : "(" + missingParams.map(m => m + ":").join(" ") + ")")
     render() {
-        let params = this.Document.buttonParams;
-        let missingParams = params && params.filter(p => this.props.Document[p] === undefined);
+        const params = this.Document.buttonParams;
+        const missingParams = params && params.filter(p => this.props.Document[p] === undefined);
         params && params.map(p => DocListCast(this.props.Document[p])); // bcz: really hacky form of prefetching ... 
         return (
             <div className="buttonBox-outerDiv" ref={this.createDropTarget} onContextMenu={this.specificContextMenu}
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 2b079fe50..d599de56e 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -31,7 +31,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
     get Y() { return this._animPos !== undefined ? this._animPos[1] : this.renderScriptDim ? this.renderScriptDim.y : this.props.y !== undefined ? this.props.y : this.dataProvider ? this.dataProvider.y : (this.Document.y || 0); }
     get width() { return this.renderScriptDim ? this.renderScriptDim.width : this.props.width !== undefined ? this.props.width : this.props.dataProvider && this.dataProvider ? this.dataProvider.width : this.layoutDoc[WidthSym](); }
     get height() {
-        let hgt = this.renderScriptDim ? this.renderScriptDim.height : this.props.height !== undefined ? this.props.height : this.props.dataProvider && this.dataProvider ? this.dataProvider.height : this.layoutDoc[HeightSym]();
+        const hgt = this.renderScriptDim ? this.renderScriptDim.height : this.props.height !== undefined ? this.props.height : this.props.dataProvider && this.dataProvider ? this.dataProvider.height : this.layoutDoc[HeightSym]();
         return (hgt === undefined && this.nativeWidth && this.nativeHeight) ? this.width * this.nativeHeight / this.nativeWidth : hgt;
     }
     @computed get dataProvider() { return this.props.dataProvider && this.props.dataProvider(this.props.Document) ? this.props.dataProvider(this.props.Document) : undefined; }
@@ -40,13 +40,13 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
 
     @computed get renderScriptDim() {
         if (this.Document.renderScript) {
-            let someView = Cast(this.props.Document.someView, Doc);
-            let minimap = Cast(this.props.Document.minimap, Doc);
+            const someView = Cast(this.props.Document.someView, Doc);
+            const minimap = Cast(this.props.Document.minimap, Doc);
             if (someView instanceof Doc && minimap instanceof Doc) {
-                let x = (NumCast(someView.panX) - NumCast(someView.width) / 2 / NumCast(someView.scale) - (NumCast(minimap.fitX) - NumCast(minimap.fitW) / 2)) / NumCast(minimap.fitW) * NumCast(minimap.width) - NumCast(minimap.width) / 2;
-                let y = (NumCast(someView.panY) - NumCast(someView.height) / 2 / NumCast(someView.scale) - (NumCast(minimap.fitY) - NumCast(minimap.fitH) / 2)) / NumCast(minimap.fitH) * NumCast(minimap.height) - NumCast(minimap.height) / 2;
-                let w = NumCast(someView.width) / NumCast(someView.scale) / NumCast(minimap.fitW) * NumCast(minimap.width);
-                let h = NumCast(someView.height) / NumCast(someView.scale) / NumCast(minimap.fitH) * NumCast(minimap.height);
+                const x = (NumCast(someView.panX) - NumCast(someView.width) / 2 / NumCast(someView.scale) - (NumCast(minimap.fitX) - NumCast(minimap.fitW) / 2)) / NumCast(minimap.fitW) * NumCast(minimap.width) - NumCast(minimap.width) / 2;
+                const y = (NumCast(someView.panY) - NumCast(someView.height) / 2 / NumCast(someView.scale) - (NumCast(minimap.fitY) - NumCast(minimap.fitH) / 2)) / NumCast(minimap.fitH) * NumCast(minimap.height) - NumCast(minimap.height) / 2;
+                const w = NumCast(someView.width) / NumCast(someView.scale) / NumCast(minimap.fitW) * NumCast(minimap.width);
+                const h = NumCast(someView.height) / NumCast(someView.scale) / NumCast(minimap.fitH) * NumCast(minimap.height);
                 return { x: x, y: y, width: w, height: h };
             }
         }
@@ -70,9 +70,9 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
         .scale(1 / this.contentScaling())
 
     borderRounding = () => {
-        let ruleRounding = this.props.ruleProvider ? StrCast(this.props.ruleProvider["ruleRounding_" + this.Document.heading]) : undefined;
-        let ld = this.layoutDoc[StrCast(this.layoutDoc.layoutKey, "layout")] instanceof Doc ? this.layoutDoc[StrCast(this.layoutDoc.layoutKey, "layout")] as Doc : undefined;
-        let br = StrCast((ld || this.props.Document).borderRounding);
+        const ruleRounding = this.props.ruleProvider ? StrCast(this.props.ruleProvider["ruleRounding_" + this.Document.heading]) : undefined;
+        const ld = this.layoutDoc[StrCast(this.layoutDoc.layoutKey, "layout")] instanceof Doc ? this.layoutDoc[StrCast(this.layoutDoc.layoutKey, "layout")] as Doc : undefined;
+        const br = StrCast((ld || this.props.Document).borderRounding);
         return !br && ruleRounding ? ruleRounding : br;
     }
 
diff --git a/src/client/views/nodes/ContentFittingDocumentView.tsx b/src/client/views/nodes/ContentFittingDocumentView.tsx
index a5f96d2de..efc907f9b 100644
--- a/src/client/views/nodes/ContentFittingDocumentView.tsx
+++ b/src/client/views/nodes/ContentFittingDocumentView.tsx
@@ -47,7 +47,7 @@ export class ContentFittingDocumentView extends React.Component<ContentFittingDo
     private get nativeWidth() { return NumCast(this.layoutDoc!.nativeWidth, this.props.PanelWidth()); }
     private get nativeHeight() { return NumCast(this.layoutDoc!.nativeHeight, this.props.PanelHeight()); }
     private contentScaling = () => {
-        let wscale = this.props.PanelWidth() / (this.nativeWidth ? this.nativeWidth : this.props.PanelWidth());
+        const wscale = this.props.PanelWidth() / (this.nativeWidth ? this.nativeWidth : this.props.PanelWidth());
         if (wscale * this.nativeHeight > this.props.PanelHeight()) {
             return this.props.PanelHeight() / (this.nativeHeight ? this.nativeHeight : this.props.PanelHeight());
         }
@@ -59,7 +59,7 @@ export class ContentFittingDocumentView extends React.Component<ContentFittingDo
     drop = (e: Event, de: DragManager.DropEvent) => {
         if (de.data instanceof DragManager.DocumentDragData) {
             this.props.childDocs && this.props.childDocs.map(otherdoc => {
-                let target = Doc.GetProto(otherdoc);
+                const target = Doc.GetProto(otherdoc);
                 target.layout = ComputedField.MakeFunction("this.image_data[0]");
                 target.layoutCustom = Doc.MakeDelegate(de.data.draggedDocuments[0]);
             });
diff --git a/src/client/views/nodes/DocuLinkBox.tsx b/src/client/views/nodes/DocuLinkBox.tsx
index d73407903..a22472e9e 100644
--- a/src/client/views/nodes/DocuLinkBox.tsx
+++ b/src/client/views/nodes/DocuLinkBox.tsx
@@ -36,12 +36,12 @@ export class DocuLinkBox extends DocComponent<FieldViewProps, DocLinkSchema>(Doc
         (e.button === 0 && !e.ctrlKey) && e.stopPropagation();
     }
     onPointerMove = action((e: PointerEvent) => {
-        let cdiv = this._ref && this._ref.current && this._ref.current.parentElement;
+        const cdiv = this._ref && this._ref.current && this._ref.current.parentElement;
         if (cdiv && (Math.abs(e.clientX - this._downx) > 5 || Math.abs(e.clientY - this._downy) > 5)) {
-            let bounds = cdiv.getBoundingClientRect();
-            let pt = Utils.getNearestPointInPerimeter(bounds.left, bounds.top, bounds.width, bounds.height, e.clientX, e.clientY);
-            let separation = Math.sqrt((pt[0] - e.clientX) * (pt[0] - e.clientX) + (pt[1] - e.clientY) * (pt[1] - e.clientY));
-            let dragdist = Math.sqrt((pt[0] - this._downx) * (pt[0] - this._downx) + (pt[1] - this._downy) * (pt[1] - this._downy));
+            const bounds = cdiv.getBoundingClientRect();
+            const pt = Utils.getNearestPointInPerimeter(bounds.left, bounds.top, bounds.width, bounds.height, e.clientX, e.clientY);
+            const separation = Math.sqrt((pt[0] - e.clientX) * (pt[0] - e.clientX) + (pt[1] - e.clientY) * (pt[1] - e.clientY));
+            const dragdist = Math.sqrt((pt[0] - this._downx) * (pt[0] - this._downx) + (pt[1] - this._downy) * (pt[1] - this._downy));
             if (separation > 100) {
                 DragLinksAsDocuments(this._ref.current!, pt[0], pt[1], Cast(this.props.Document[this.props.fieldKey], Doc) as Doc, this.props.Document); // Containging collection is the document, not a collection... hack.
                 document.removeEventListener("pointermove", this.onPointerMove);
@@ -67,14 +67,14 @@ export class DocuLinkBox extends DocComponent<FieldViewProps, DocLinkSchema>(Doc
     }
 
     render() {
-        let anchorDoc = Cast(this.props.Document[this.props.fieldKey], Doc);
-        let hasAnchor = anchorDoc instanceof Doc && anchorDoc.type === DocumentType.PDFANNO;
-        let y = NumCast(this.props.Document[this.props.fieldKey + "_y"], 100);
-        let x = NumCast(this.props.Document[this.props.fieldKey + "_x"], 100);
-        let c = StrCast(this.props.Document.backgroundColor, "lightblue");
-        let anchor = this.props.fieldKey === "anchor1" ? "anchor2" : "anchor1";
-        let timecode = this.props.Document[anchor + "Timecode"];
-        let targetTitle = StrCast((this.props.Document[anchor]! as Doc).title) + (timecode !== undefined ? ":" + timecode : "");
+        const anchorDoc = Cast(this.props.Document[this.props.fieldKey], Doc);
+        const hasAnchor = anchorDoc instanceof Doc && anchorDoc.type === DocumentType.PDFANNO;
+        const y = NumCast(this.props.Document[this.props.fieldKey + "_y"], 100);
+        const x = NumCast(this.props.Document[this.props.fieldKey + "_x"], 100);
+        const c = StrCast(this.props.Document.backgroundColor, "lightblue");
+        const anchor = this.props.fieldKey === "anchor1" ? "anchor2" : "anchor1";
+        const timecode = this.props.Document[anchor + "Timecode"];
+        const targetTitle = StrCast((this.props.Document[anchor]! as Doc).title) + (timecode !== undefined ? ":" + timecode : "");
         return <div className="docuLinkBox-cont" onPointerDown={this.onPointerDown} onClick={this.onClick} title={targetTitle}
             ref={this._ref} style={{
                 background: c, left: `calc(${x}% - 12.5px)`, top: `calc(${y}% - 12.5px)`,
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index b9b84d5ce..13fd3cde1 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -83,7 +83,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
     }
 
     CreateBindings(): JsxBindings {
-        let list = {
+        const list = {
             ...OmitKeys(this.props, ['parentActive'], (obj: any) => obj.active = this.props.parentActive).omit,
             Document: this.layoutDoc,
             DataDoc: this.dataDoc,
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 057b4eecd..63ce6233c 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -122,7 +122,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
 
     startDragging(x: number, y: number, dropAction: dropActionType, applyAsTemplate?: boolean) {
         if (this._mainCont.current) {
-            let dragData = new DragManager.DocumentDragData([this.props.Document]);
+            const dragData = new DragManager.DocumentDragData([this.props.Document]);
             const [left, top] = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint(0, 0);
             dragData.offset = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).transformDirection(x - left, y - top);
             dragData.dropAction = dropAction;
@@ -143,7 +143,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
             e.stopPropagation();
             let preventDefault = true;
             if (this._doubleTap && this.props.renderDepth && !this.onClickHandler?.script) { // disable double-click to show full screen for things that have an on click behavior since clicking them twice can be misinterpreted as a double click
-                let fullScreenAlias = Doc.MakeAlias(this.props.Document);
+                const fullScreenAlias = Doc.MakeAlias(this.props.Document);
                 if (StrCast(fullScreenAlias.layoutKey) !== "layoutCustom" && fullScreenAlias.layoutCustom !== undefined) {
                     fullScreenAlias.layoutKey = "layoutCustom";
                 }
@@ -166,9 +166,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     }
 
     buttonClick = async (altKey: boolean, ctrlKey: boolean) => {
-        let maximizedDocs = await DocListCastAsync(this.Document.maximizedDocs);
-        let summarizedDocs = await DocListCastAsync(this.Document.summarizedDocs);
-        let linkDocs = LinkManager.Instance.getAllRelatedLinks(this.props.Document);
+        const maximizedDocs = await DocListCastAsync(this.Document.maximizedDocs);
+        const summarizedDocs = await DocListCastAsync(this.Document.summarizedDocs);
+        const linkDocs = LinkManager.Instance.getAllRelatedLinks(this.props.Document);
         let expandedDocs: Doc[] = [];
         expandedDocs = maximizedDocs ? [...maximizedDocs, ...expandedDocs] : expandedDocs;
         expandedDocs = summarizedDocs ? [...summarizedDocs, ...expandedDocs] : expandedDocs;
@@ -179,7 +179,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
             maxLocation = this.Document.maximizeLocation = (!ctrlKey ? !altKey ? maxLocation : (maxLocation !== "inPlace" ? "inPlace" : "onRight") : (maxLocation !== "inPlace" ? "inPlace" : "inTab"));
             if (maxLocation === "inPlace") {
                 expandedDocs.forEach(maxDoc => this.props.addDocument && this.props.addDocument(maxDoc));
-                let scrpt = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint(NumCast(this.layoutDoc.width) / 2, NumCast(this.layoutDoc.height) / 2);
+                const scrpt = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint(NumCast(this.layoutDoc.width) / 2, NumCast(this.layoutDoc.height) / 2);
                 DocumentManager.Instance.animateBetweenPoint(scrpt, expandedDocs);
             } else {
                 expandedDocs.forEach(maxDoc => (!this.props.addDocTab(maxDoc, undefined, "close") && this.props.addDocTab(maxDoc, undefined, maxLocation)));
@@ -278,7 +278,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
             fieldTemplate.heading = 1;
             fieldTemplate.autoHeight = true;
 
-            let docTemplate = Docs.Create.FreeformDocument([fieldTemplate], { title: doc.title + "_layout", width: width + 20, height: Math.max(100, height + 45) });
+            const docTemplate = Docs.Create.FreeformDocument([fieldTemplate], { title: doc.title + "_layout", width: width + 20, height: Math.max(100, height + 45) });
 
             Doc.MakeMetadataFieldTemplate(fieldTemplate, Doc.GetProto(docTemplate), true);
             Doc.ApplyTemplateTo(docTemplate, dataDoc || doc, "layoutCustom", undefined);
@@ -324,10 +324,10 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
 
     @action
     onDrop = (e: React.DragEvent) => {
-        let text = e.dataTransfer.getData("text/plain");
+        const text = e.dataTransfer.getData("text/plain");
         if (!e.isDefaultPrevented() && text && text.startsWith("<div")) {
-            let oldLayout = this.Document.layout || "";
-            let layout = text.replace("{layout}", oldLayout);
+            const oldLayout = this.Document.layout || "";
+            const layout = text.replace("{layout}", oldLayout);
             this.Document.layout = layout;
             e.stopPropagation();
             e.preventDefault();
@@ -348,11 +348,11 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     @undoBatch
     @action
     makeIntoPortal = async () => {
-        let anchors = await Promise.all(DocListCast(this.Document.links).map(async (d: Doc) => Cast(d.anchor2, Doc)));
+        const anchors = await Promise.all(DocListCast(this.Document.links).map(async (d: Doc) => Cast(d.anchor2, Doc)));
         if (!anchors.find(anchor2 => anchor2 && anchor2.title === this.Document.title + ".portal" ? true : false)) {
-            let portalID = (this.Document.title + ".portal").replace(/^-/, "").replace(/\([0-9]*\)$/, "");
+            const portalID = (this.Document.title + ".portal").replace(/^-/, "").replace(/\([0-9]*\)$/, "");
             DocServer.GetRefField(portalID).then(existingPortal => {
-                let portal = existingPortal instanceof Doc ? existingPortal : Docs.Create.FreeformDocument([], { width: (this.layoutDoc.width || 0) + 10, height: this.layoutDoc.height || 0, title: portalID });
+                const portal = existingPortal instanceof Doc ? existingPortal : Docs.Create.FreeformDocument([], { width: (this.layoutDoc.width || 0) + 10, height: this.layoutDoc.height || 0, title: portalID });
                 DocUtils.MakeLink({ doc: this.props.Document, ctx: this.props.ContainingCollectionDoc }, { doc: portal }, portalID, "portal link");
                 this.Document.isButton = true;
             });
@@ -400,7 +400,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         e.preventDefault();
 
         const cm = ContextMenu.Instance;
-        let subitems: ContextMenuProps[] = [];
+        const subitems: ContextMenuProps[] = [];
         subitems.push({ description: "Open Full Screen", event: () => CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(this), icon: "desktop" });
         subitems.push({ description: "Open Tab        ", event: () => this.props.addDocTab(this.props.Document, this.props.DataDoc, "inTab"), icon: "folder" });
         subitems.push({ description: "Open Right      ", event: () => this.props.addDocTab(this.props.Document, this.props.DataDoc, "onRight"), icon: "caret-square-right" });
@@ -410,8 +410,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         cm.addItem({ description: "Open...", subitems: subitems, icon: "external-link-alt" });
 
 
-        let existingOnClick = ContextMenu.Instance.findByDescription("OnClick...");
-        let onClicks: ContextMenuProps[] = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : [];
+        const existingOnClick = ContextMenu.Instance.findByDescription("OnClick...");
+        const onClicks: ContextMenuProps[] = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : [];
         onClicks.push({ description: "Enter Portal", event: this.makeIntoPortal, icon: "window-restore" });
         onClicks.push({ description: "Toggle Detail", event: () => this.Document.onClick = ScriptField.MakeScript("toggleDetail(this)"), icon: "window-restore" });
         onClicks.push({ description: this.Document.ignoreClick ? "Select" : "Do Nothing", event: () => this.Document.ignoreClick = !this.Document.ignoreClick, icon: this.Document.ignoreClick ? "unlock" : "lock" });
@@ -425,7 +425,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         });
         !existingOnClick && cm.addItem({ description: "OnClick...", subitems: onClicks, icon: "hand-point-right" });
 
-        let funcs: ContextMenuProps[] = [];
+        const funcs: ContextMenuProps[] = [];
         if (this.Document.onDragStart) {
             funcs.push({ description: "Drag an Alias", icon: "edit", event: () => this.Document.dragFactory && (this.Document.onDragStart = ScriptField.MakeFunction('getAlias(this.dragFactory)')) });
             funcs.push({ description: "Drag a Copy", icon: "edit", event: () => this.Document.dragFactory && (this.Document.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)')) });
@@ -433,8 +433,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
             ContextMenu.Instance.addItem({ description: "OnDrag...", subitems: funcs, icon: "asterisk" });
         }
 
-        let existing = ContextMenu.Instance.findByDescription("Layout...");
-        let layoutItems: ContextMenuProps[] = existing && "subitems" in existing ? existing.subitems : [];
+        const existing = ContextMenu.Instance.findByDescription("Layout...");
+        const layoutItems: ContextMenuProps[] = existing && "subitems" in existing ? existing.subitems : [];
         layoutItems.push({ description: this.Document.isBackground ? "As Foreground" : "As Background", event: this.makeBackground, icon: this.Document.lockedPosition ? "unlock" : "lock" });
         if (this.props.DataDoc) {
             layoutItems.push({ description: "Make View of Metadata Field", event: () => Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.DataDoc!), icon: "concierge-bell" });
@@ -453,8 +453,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         }
         !existing && cm.addItem({ description: "Layout...", subitems: layoutItems, icon: "compass" });
 
-        let more = ContextMenu.Instance.findByDescription("More...");
-        let moreItems: ContextMenuProps[] = more && "subitems" in more ? more.subitems : [];
+        const more = ContextMenu.Instance.findByDescription("More...");
+        const moreItems: ContextMenuProps[] = more && "subitems" in more ? more.subitems : [];
 
         if (!ClientUtils.RELEASE) {
             // let copies: ContextMenuProps[] = [];
@@ -489,7 +489,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         !more && cm.addItem({ description: "More...", subitems: moreItems, icon: "hand-point-right" });
         runInAction(() => {
             if (!ClientUtils.RELEASE) {
-                let setWriteMode = (mode: DocServer.WriteMode) => {
+                const setWriteMode = (mode: DocServer.WriteMode) => {
                     DocServer.AclsMode = mode;
                     const mode1 = mode;
                     const mode2 = mode === DocServer.WriteMode.Default ? mode : DocServer.WriteMode.Playground;
@@ -503,7 +503,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                     DocServer.setFieldWriteMode("scale", mode2);
                     DocServer.setFieldWriteMode("viewType", mode2);
                 };
-                let aclsMenu: ContextMenuProps[] = [];
+                const aclsMenu: ContextMenuProps[] = [];
                 aclsMenu.push({ description: "Default (write/read all)", event: () => setWriteMode(DocServer.WriteMode.Default), icon: DocServer.AclsMode === DocServer.WriteMode.Default ? "check" : "exclamation" });
                 aclsMenu.push({ description: "Playground (write own/no read)", event: () => setWriteMode(DocServer.WriteMode.Playground), icon: DocServer.AclsMode === DocServer.WriteMode.Playground ? "check" : "exclamation" });
                 aclsMenu.push({ description: "Live Playground (write own/read others)", event: () => setWriteMode(DocServer.WriteMode.LivePlayground), icon: DocServer.AclsMode === DocServer.WriteMode.LivePlayground ? "check" : "exclamation" });
@@ -539,8 +539,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     select = (ctrlPressed: boolean) => { SelectionManager.SelectDoc(this, ctrlPressed); };
 
     chromeHeight = () => {
-        let showOverlays = this.props.showOverlays ? this.props.showOverlays(this.Document) : undefined;
-        let showTitle = showOverlays && "title" in showOverlays ? showOverlays.title : StrCast(this.Document.showTitle);
+        const showOverlays = this.props.showOverlays ? this.props.showOverlays(this.Document) : undefined;
+        const showTitle = showOverlays && "title" in showOverlays ? showOverlays.title : StrCast(this.Document.showTitle);
         return (showTitle ? 25 : 0) + 1;
     }
 
@@ -585,8 +585,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     // if it's a tempoarl link (currently just for Audio), then the audioBox will display the anchor and we don't want to display it here.
     // would be good to generalize this some way.
     isNonTemporalLink = (linkDoc: Doc) => {
-        let anchor = Cast(Doc.AreProtosEqual(this.props.Document, Cast(linkDoc.anchor1, Doc) as Doc) ? linkDoc.anchor1 : linkDoc.anchor2, Doc) as Doc;
-        let ept = Doc.AreProtosEqual(this.props.Document, Cast(linkDoc.anchor1, Doc) as Doc) ? linkDoc.anchor1Timecode : linkDoc.anchor2Timecode;
+        const anchor = Cast(Doc.AreProtosEqual(this.props.Document, Cast(linkDoc.anchor1, Doc) as Doc) ? linkDoc.anchor1 : linkDoc.anchor2, Doc) as Doc;
+        const ept = Doc.AreProtosEqual(this.props.Document, Cast(linkDoc.anchor1, Doc) as Doc) ? linkDoc.anchor1Timecode : linkDoc.anchor2Timecode;
         return anchor.type === DocumentType.AUDIO && NumCast(ept) ? false : true;
     }
 
@@ -651,14 +651,14 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
 
     @action
     handle2PointersMove = (e: TouchEvent) => {
-        let pt1 = e.targetTouches.item(0);
-        let pt2 = e.targetTouches.item(1);
+        const pt1 = e.targetTouches.item(0);
+        const pt2 = e.targetTouches.item(1);
         if (pt1 && pt2 && this.prevPoints.has(pt1.identifier) && this.prevPoints.has(pt2.identifier)) {
-            let oldPoint1 = this.prevPoints.get(pt1.identifier);
-            let oldPoint2 = this.prevPoints.get(pt2.identifier);
-            let pinching = InteractionUtils.Pinning(pt1, pt2, oldPoint1!, oldPoint2!);
+            const oldPoint1 = this.prevPoints.get(pt1.identifier);
+            const oldPoint2 = this.prevPoints.get(pt2.identifier);
+            const pinching = InteractionUtils.Pinning(pt1, pt2, oldPoint1!, oldPoint2!);
             if (pinching !== 0) {
-                let newWidth = Math.max(Math.abs(oldPoint1!.clientX - oldPoint2!.clientX), Math.abs(pt1.clientX - pt2.clientX));
+                const newWidth = Math.max(Math.abs(oldPoint1!.clientX - oldPoint2!.clientX), Math.abs(pt1.clientX - pt2.clientX));
                 this.props.Document.width = newWidth;
             }
         }
@@ -679,12 +679,12 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         const localScale = fullDegree;
 
         const animDims = this.Document.animateToDimensions ? Array.from(this.Document.animateToDimensions) : undefined;
-        let animheight = animDims ? animDims[1] : "100%";
-        let animwidth = animDims ? animDims[0] : "100%";
+        const animheight = animDims ? animDims[1] : "100%";
+        const animwidth = animDims ? animDims[0] : "100%";
 
         const highlightColors = ["transparent", "maroon", "maroon", "yellow", "magenta", "cyan", "orange"];
         const highlightStyles = ["solid", "dashed", "solid", "solid", "solid", "solid", "solid"];
-        let highlighting = fullDegree && this.layoutDoc.type !== DocumentType.FONTICON && this.layoutDoc.viewType !== CollectionViewType.Linear;
+        const highlighting = fullDegree && this.layoutDoc.type !== DocumentType.FONTICON && this.layoutDoc.viewType !== CollectionViewType.Linear;
         return <div className={`documentView-node${this.topMost ? "-topmost" : ""}`} ref={this._mainCont}
             onDrop={this.onDrop} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick}
             onPointerEnter={e => Doc.BrushDoc(this.props.Document)} onPointerLeave={e => Doc.UnBrushDoc(this.props.Document)}
diff --git a/src/client/views/nodes/FaceRectangle.tsx b/src/client/views/nodes/FaceRectangle.tsx
index 887efc0d5..20afa4565 100644
--- a/src/client/views/nodes/FaceRectangle.tsx
+++ b/src/client/views/nodes/FaceRectangle.tsx
@@ -12,7 +12,7 @@ export default class FaceRectangle extends React.Component<{ rectangle: Rectangl
     }
 
     render() {
-        let rectangle = this.props.rectangle;
+        const rectangle = this.props.rectangle;
         return (
             <div
                 style={{
diff --git a/src/client/views/nodes/FaceRectangles.tsx b/src/client/views/nodes/FaceRectangles.tsx
index acf1aced3..3c7f1f206 100644
--- a/src/client/views/nodes/FaceRectangles.tsx
+++ b/src/client/views/nodes/FaceRectangles.tsx
@@ -20,10 +20,10 @@ export interface RectangleTemplate {
 export default class FaceRectangles extends React.Component<FaceRectanglesProps> {
 
     render() {
-        let faces = DocListCast(this.props.document.faces);
-        let templates: RectangleTemplate[] = faces.map(faceDoc => {
-            let rectangle = Cast(faceDoc.faceRectangle, Doc) as Doc;
-            let style = {
+        const faces = DocListCast(this.props.document.faces);
+        const templates: RectangleTemplate[] = faces.map(faceDoc => {
+            const rectangle = Cast(faceDoc.faceRectangle, Doc) as Doc;
+            const style = {
                 top: NumCast(rectangle.top),
                 left: NumCast(rectangle.left),
                 width: NumCast(rectangle.width),
diff --git a/src/client/views/nodes/FontIconBox.tsx b/src/client/views/nodes/FontIconBox.tsx
index 960b55e3e..2433251b3 100644
--- a/src/client/views/nodes/FontIconBox.tsx
+++ b/src/client/views/nodes/FontIconBox.tsx
@@ -25,8 +25,8 @@ export class FontIconBox extends DocComponent<FieldViewProps, FontIconDocument>(
         this._backgroundReaction = reaction(() => this.props.Document.backgroundColor,
             () => {
                 if (this._ref && this._ref.current) {
-                    let col = Utils.fromRGBAstr(getComputedStyle(this._ref.current).backgroundColor);
-                    let colsum = (col.r + col.g + col.b);
+                    const col = Utils.fromRGBAstr(getComputedStyle(this._ref.current).backgroundColor);
+                    const colsum = (col.r + col.g + col.b);
                     if (colsum / col.a > 600 || col.a < 0.25) runInAction(() => this._foregroundColor = "black");
                     else if (colsum / col.a <= 600 || col.a >= .25) runInAction(() => this._foregroundColor = "white");
                 }
@@ -36,8 +36,8 @@ export class FontIconBox extends DocComponent<FieldViewProps, FontIconDocument>(
         this._backgroundReaction && this._backgroundReaction();
     }
     render() {
-        let referenceDoc = (this.props.Document.dragFactory instanceof Doc ? this.props.Document.dragFactory : this.props.Document);
-        let referenceLayout = Doc.Layout(referenceDoc);
+        const referenceDoc = (this.props.Document.dragFactory instanceof Doc ? this.props.Document.dragFactory : this.props.Document);
+        const referenceLayout = Doc.Layout(referenceDoc);
         return <button className="fontIconBox-outerDiv" title={StrCast(this.props.Document.title)} ref={this._ref}
             style={{
                 background: StrCast(referenceLayout.backgroundColor),
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index a5530f32d..57321de49 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -1,6 +1,6 @@
 import { library } from '@fortawesome/fontawesome-svg-core';
 import { faEdit, faSmile, faTextHeight, faUpload } from '@fortawesome/free-solid-svg-icons';
-import _ from "lodash";
+import { isEqual } from "lodash";
 import { action, computed, IReactionDisposer, Lambda, observable, reaction, runInAction, trace } from "mobx";
 import { observer } from "mobx-react";
 import { baseKeymap } from "prosemirror-commands";
@@ -103,8 +103,8 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
         return html.indexOf("data-pm-slice") !== -1;
     }
     public static GetHref(html: string): string {
-        let parser = new DOMParser();
-        let parsedHtml = parser.parseFromString(html, 'text/html');
+        const parser = new DOMParser();
+        const parsedHtml = parser.parseFromString(html, 'text/html');
         if (parsedHtml.body.childNodes.length === 1 && parsedHtml.body.childNodes[0].childNodes.length === 1 &&
             (parsedHtml.body.childNodes[0].childNodes[0] as any).href) {
             return (parsedHtml.body.childNodes[0].childNodes[0] as any).href;
@@ -126,12 +126,12 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
 
     @undoBatch
     public setFontColor(color: string) {
-        let view = this._editorView!;
+        const view = this._editorView!;
         if (view.state.selection.from === view.state.selection.to) return false;
         if (view.state.selection.to - view.state.selection.from > view.state.doc.nodeSize - 3) {
             this.layoutDoc.color = color;
         }
-        let colorMark = view.state.schema.mark(view.state.schema.marks.pFontColor, { color: color });
+        const colorMark = view.state.schema.mark(view.state.schema.marks.pFontColor, { color: color });
         view.dispatch(view.state.tr.addMark(view.state.selection.from, view.state.selection.to, colorMark));
         return true;
     }
@@ -147,9 +147,9 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
 
     doLinkOnDeselect() {
         Array.from(this.linkOnDeselect.entries()).map(entry => {
-            let key = entry[0];
-            let value = entry[1];
-            let id = Utils.GenerateDeterministicGuid(this.dataDoc[Id] + key);
+            const key = entry[0];
+            const value = entry[1];
+            const id = Utils.GenerateDeterministicGuid(this.dataDoc[Id] + key);
             DocServer.GetRefField(value).then(doc => {
                 DocServer.GetRefField(id).then(linkDoc => {
                     this.dataDoc[key] = doc || Docs.Create.FreeformDocument([], { title: value, width: 500, height: 500 }, value);
@@ -164,24 +164,24 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
 
     dispatchTransaction = (tx: Transaction) => {
         if (this._editorView) {
-            let metadata = tx.selection.$from.marks().find((m: Mark) => m.type === schema.marks.metadata);
+            const metadata = tx.selection.$from.marks().find((m: Mark) => m.type === schema.marks.metadata);
             if (metadata) {
-                let range = tx.selection.$from.blockRange(tx.selection.$to);
+                const range = tx.selection.$from.blockRange(tx.selection.$to);
                 let text = range ? tx.doc.textBetween(range.start, range.end) : "";
                 let textEndSelection = tx.selection.to;
                 for (; textEndSelection < range!.end && text[textEndSelection - range!.start] !== " "; textEndSelection++) { }
                 text = text.substr(0, textEndSelection - range!.start);
                 text = text.split(" ")[text.split(" ").length - 1];
-                let split = text.split("::");
+                const split = text.split("::");
                 if (split.length > 1 && split[1]) {
-                    let key = split[0];
-                    let value = split[split.length - 1];
+                    const key = split[0];
+                    const value = split[split.length - 1];
                     this.linkOnDeselect.set(key, value);
 
-                    let id = Utils.GenerateDeterministicGuid(this.dataDoc[Id] + key);
+                    const id = Utils.GenerateDeterministicGuid(this.dataDoc[Id] + key);
                     const link = this._editorView.state.schema.marks.link.create({ href: `http://localhost:1050/doc/${id}`, location: "onRight", title: value });
                     const mval = this._editorView.state.schema.marks.metadataVal.create();
-                    let offset = (tx.selection.to === range!.end - 1 ? -1 : 0);
+                    const offset = (tx.selection.to === range!.end - 1 ? -1 : 0);
                     tx = tx.addMark(textEndSelection - value.length + offset, textEndSelection, link).addMark(textEndSelection - value.length + offset, textEndSelection, mval);
                     this.dataDoc[key] = value;
                 }
@@ -189,7 +189,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
             const state = this._editorView.state.apply(tx);
             this._editorView.updateState(state);
 
-            let tsel = this._editorView.state.selection.$from;
+            const tsel = this._editorView.state.selection.$from;
             tsel.marks().filter(m => m.type === this._editorView!.state.schema.marks.user_mark).map(m => AudioBox.SetScrubTime(Math.max(0, m.attrs.modified * 5000 - 1000)));
             this._applyingChange = true;
             this.extensionDoc && (this.extensionDoc.lastModified = new DateField(new Date(Date.now())));
@@ -202,8 +202,8 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
 
     updateTitle = () => {
         if (StrCast(this.dataDoc.title).startsWith("-") && this._editorView && !this.Document.customTitle) {
-            let str = this._editorView.state.doc.textContent;
-            let titlestr = str.substr(0, Math.min(40, str.length));
+            const str = this._editorView.state.doc.textContent;
+            const titlestr = str.substr(0, Math.min(40, str.length));
             this.dataDoc.title = "-" + titlestr + (str.length > 40 ? "..." : "");
         }
     }
@@ -212,11 +212,11 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
         if (this._editorView && (this._editorView as any).docView) {
             const mark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight);
             const activeMark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight, { selected: true });
-            let res = terms.map(term => this.findInNode(this._editorView!, this._editorView!.state.doc, term));
+            const res = terms.map(term => this.findInNode(this._editorView!, this._editorView!.state.doc, term));
             let tr = this._editorView.state.tr;
-            let flattened: TextSelection[] = [];
+            const flattened: TextSelection[] = [];
             res.map(r => r.map(h => flattened.push(h)));
-            let lastSel = Math.min(flattened.length - 1, this._searchIndex);
+            const lastSel = Math.min(flattened.length - 1, this._searchIndex);
             flattened.forEach((h: TextSelection, ind: number) => tr = tr.addMark(h.from, h.to, ind === lastSel ? activeMark : mark));
             this._searchIndex = ++this._searchIndex > flattened.length - 1 ? 0 : this._searchIndex;
             this._editorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(flattened[lastSel].from), tr.doc.resolve(flattened[lastSel].to))).scrollIntoView());
@@ -227,13 +227,13 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
         if (this._editorView && (this._editorView as any).docView) {
             const mark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight);
             const activeMark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight, { selected: true });
-            let end = this._editorView.state.doc.nodeSize - 2;
+            const end = this._editorView.state.doc.nodeSize - 2;
             this._editorView.dispatch(this._editorView.state.tr.removeMark(0, end, mark).removeMark(0, end, activeMark));
         }
     }
     setAnnotation = (start: number, end: number, mark: Mark, opened: boolean, keep: boolean = false) => {
-        let view = this._editorView!;
-        let nmark = view.state.schema.marks.user_mark.create({ ...mark.attrs, userid: keep ? Doc.CurrentUserEmail : mark.attrs.userid, opened: opened });
+        const view = this._editorView!;
+        const nmark = view.state.schema.marks.user_mark.create({ ...mark.attrs, userid: keep ? Doc.CurrentUserEmail : mark.attrs.userid, opened: opened });
         view.dispatch(view.state.tr.removeMark(start, end, nmark).addMark(start, end, nmark));
     }
     protected createDropTarget = (ele: HTMLDivElement) => {
@@ -266,16 +266,16 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
                 e.stopPropagation();
                 // embed document when dragging with a userDropAction or an embedDoc flag set
             } else if (de.data.userDropAction || de.data.embedDoc) {
-                let target = de.data.droppedDocuments[0];
+                const target = de.data.droppedDocuments[0];
                 // const link = DocUtils.MakeLink({ doc: this.dataDoc, ctx: this.props.ContainingCollectionDoc }, { doc: target }, "Embedded Doc:" + target.title);
                 // if (link) {
                 target.fitToBox = true;
-                let node = schema.nodes.dashDoc.create({
+                const node = schema.nodes.dashDoc.create({
                     width: target[WidthSym](), height: target[HeightSym](),
                     title: "dashDoc", docid: target[Id],
                     float: "right"
                 });
-                let view = this._editorView!;
+                const view = this._editorView!;
                 view.dispatch(view.state.tr.insert(view.posAtCoords({ left: de.x, top: de.y })!.pos, node));
                 this.tryUpdateHeight();
                 e.stopPropagation();
@@ -292,7 +292,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
         if (node.isBlock) {
             // tslint:disable-next-line: prefer-for-of
             for (let i = 0; i < (context.content as any).content.length; i++) {
-                let result = this.getNodeEndpoints((context.content as any).content[i], node);
+                const result = this.getNodeEndpoints((context.content as any).content[i], node);
                 if (result) {
                     return {
                         from: result.from + offset + (context.type.name === "doc" ? 0 : 1),
@@ -313,9 +313,10 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
         let ret: TextSelection[] = [];
 
         if (node.isTextblock) {
-            let index = 0, foundAt, ep = this.getNodeEndpoints(pm.state.doc, node);
+            let index = 0, foundAt;
+            const ep = this.getNodeEndpoints(pm.state.doc, node);
             while (ep && (foundAt = node.textContent.slice(index).search(RegExp(find, "i"))) > -1) {
-                let sel = new TextSelection(pm.state.doc.resolve(ep.from + index + foundAt + 1), pm.state.doc.resolve(ep.from + index + foundAt + find.length + 1));
+                const sel = new TextSelection(pm.state.doc.resolve(ep.from + index + foundAt + 1), pm.state.doc.resolve(ep.from + index + foundAt + find.length + 1));
                 ret.push(sel);
                 index = index + foundAt + find.length;
             }
@@ -348,13 +349,13 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
         }
         if (FormattedTextBox._highlights.indexOf("By Recent Minute") !== -1) {
             addStyleSheetRule(FormattedTextBox._userStyleSheet, "userMark-" + Doc.CurrentUserEmail.replace(".", "").replace("@", ""), { opacity: "0.1" });
-            let min = Math.round(Date.now() / 1000 / 60);
+            const min = Math.round(Date.now() / 1000 / 60);
             numberRange(10).map(i => addStyleSheetRule(FormattedTextBox._userStyleSheet, "userMark-min-" + (min - i), { opacity: ((10 - i - 1) / 10).toString() }));
             setTimeout(() => this.updateHighlights());
         }
         if (FormattedTextBox._highlights.indexOf("By Recent Hour") !== -1) {
             addStyleSheetRule(FormattedTextBox._userStyleSheet, "userMark-" + Doc.CurrentUserEmail.replace(".", "").replace("@", ""), { opacity: "0.1" });
-            let hr = Math.round(Date.now() / 1000 / 60 / 60);
+            const hr = Math.round(Date.now() / 1000 / 60 / 60);
             numberRange(10).map(i => addStyleSheetRule(FormattedTextBox._userStyleSheet, "userMark-hr-" + (hr - i), { opacity: ((10 - i - 1) / 10).toString() }));
         }
     }
@@ -362,7 +363,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
     toggleSidebar = () => this.props.Document.sidebarWidthPercent = StrCast(this.props.Document.sidebarWidthPercent, "0%") === "0%" ? "25%" : "0%";
 
     specificContextMenu = (e: React.MouseEvent): void => {
-        let funcs: ContextMenuProps[] = [];
+        const funcs: ContextMenuProps[] = [];
         funcs.push({ description: "Toggle Sidebar", event: () => { e.stopPropagation(); this.toggleSidebar(); }, icon: "expand-arrows-alt" });
         funcs.push({ description: "Record Bullet", event: () => { e.stopPropagation(); this.recordBullet(); }, icon: "expand-arrows-alt" });
         ["My Text", "Text from Others", "Todo Items", "Important Items", "Ignore Items", "Disagree Items", "By Recent Minute", "By Recent Hour"].forEach(option =>
@@ -403,8 +404,8 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
     }
 
     recordBullet = async () => {
-        let completedCue = "end session";
-        let results = await DictationManager.Controls.listen({
+        const completedCue = "end session";
+        const results = await DictationManager.Controls.listen({
             interimHandler: this.setCurrentBulletContent,
             continuous: { indefinite: false },
             terminators: [completedCue, "bullet", "next"]
@@ -420,20 +421,20 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
     setCurrentBulletContent = (value: string) => {
         if (this._editorView) {
             let state = this._editorView.state;
-            let from = state.selection.from;
-            let to = state.selection.to;
+            const from = state.selection.from;
+            const to = state.selection.to;
             this._editorView.dispatch(state.tr.insertText(value, from, to));
             state = this._editorView.state;
-            let updated = TextSelection.create(state.doc, from, from + value.length);
+            const updated = TextSelection.create(state.doc, from, from + value.length);
             this._editorView.dispatch(state.tr.setSelection(updated));
         }
     }
 
     nextBullet = (pos: number) => {
         if (this._editorView) {
-            let frag = Fragment.fromArray(this.newListItems(2));
+            const frag = Fragment.fromArray(this.newListItems(2));
             if (this._editorView.state.doc.resolve(pos).depth >= 2) {
-                let slice = new Slice(frag, 2, 2);
+                const slice = new Slice(frag, 2, 2);
                 let state = this._editorView.state;
                 this._editorView.dispatch(state.tr.step(new ReplaceStep(pos, pos, slice)));
                 pos += 4;
@@ -471,8 +472,10 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
     }
 
     componentDidMount() {
-        this.pullFromGoogleDoc(this.checkState);
-        this.dataDoc[GoogleRef] && this.dataDoc.unchanged && runInAction(() => DocumentDecorations.Instance.isAnimatingFetch = true);
+        if (DocumentButtonBar.Instance) {
+            this.pullFromGoogleDoc(this.checkState);
+            this.dataDoc[GoogleRef] && this.dataDoc.unchanged && runInAction(() => DocumentButtonBar.Instance.isAnimatingFetch = true);
+        }
 
         this._reactionDisposer = reaction(
             () => {
@@ -481,7 +484,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
             },
             incomingValue => {
                 if (this._editorView && !this._applyingChange) {
-                    let updatedState = JSON.parse(incomingValue);
+                    const updatedState = JSON.parse(incomingValue);
                     this._editorView.updateState(EditorState.fromJSON(this.config, updatedState));
                     this.tryUpdateHeight();
                 }
@@ -493,7 +496,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
             () => {
                 if (!DocumentButtonBar.hasPulledHack) {
                     DocumentButtonBar.hasPulledHack = true;
-                    let unchanged = this.dataDoc.unchanged;
+                    const unchanged = this.dataDoc.unchanged;
                     this.pullFromGoogleDoc(unchanged ? this.checkState : this.updateState);
                 }
             }
@@ -514,7 +517,6 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
             () => this.tryUpdateHeight()
         );
 
-
         this.setupEditor(this.config, this.dataDoc, this.props.fieldKey);
 
         this._searchReactionDisposer = reaction(() => {
@@ -528,10 +530,9 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
             }
         }, { fireImmediately: true });
 
-
         this._rulesReactionDisposer = reaction(() => {
-            let ruleProvider = this.props.ruleProvider;
-            let heading = NumCast(this.layoutDoc.heading);
+            const ruleProvider = this.props.ruleProvider;
+            const heading = NumCast(this.layoutDoc.heading);
             if (ruleProvider instanceof Doc) {
                 return {
                     align: StrCast(ruleProvider["ruleAlign_" + heading], ""),
@@ -547,7 +548,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
                 rules && setTimeout(() => {
                     const view = this._editorView!;
                     if (this._proseRef) {
-                        let n = new NodeSelection(view.state.doc.resolve(0));
+                        const n = new NodeSelection(view.state.doc.resolve(0));
                         if (this._editorView!.state.doc.textContent === "") {
                             view.dispatch(view.state.tr.setSelection(new TextSelection(view.state.doc.resolve(0), view.state.doc.resolve(2))).
                                 replaceSelectionWith(this._editorView!.state.schema.nodes.paragraph.create({ align: rules.align }), true));
@@ -562,10 +563,10 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
         this._scrollToRegionReactionDisposer = reaction(
             () => StrCast(this.layoutDoc.scrollToLinkID),
             async (scrollToLinkID) => {
-                let findLinkFrag = (frag: Fragment, editor: EditorView) => {
+                const findLinkFrag = (frag: Fragment, editor: EditorView) => {
                     const nodes: Node[] = [];
                     frag.forEach((node, index) => {
-                        let examinedNode = findLinkNode(node, editor);
+                        const examinedNode = findLinkNode(node, editor);
                         if (examinedNode && examinedNode.textContent) {
                             nodes.push(examinedNode);
                             start += index;
@@ -573,7 +574,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
                     });
                     return { frag: Fragment.fromArray(nodes), start: start };
                 };
-                let findLinkNode = (node: Node, editor: EditorView) => {
+                const findLinkNode = (node: Node, editor: EditorView) => {
                     if (!node.isText) {
                         const content = findLinkFrag(node.content, editor);
                         return node.copy(content.frag);
@@ -585,8 +586,8 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
 
                 let start = -1;
                 if (this._editorView && scrollToLinkID) {
-                    let editor = this._editorView;
-                    let ret = findLinkFrag(editor.state.doc.content, editor);
+                    const editor = this._editorView;
+                    const ret = findLinkFrag(editor.state.doc.content, editor);
 
                     if (ret.frag.size > 2 && ret.start >= 0) {
                         let selection = TextSelection.near(editor.state.doc.resolve(ret.start)); // default to near the start
@@ -610,28 +611,28 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
 
     pushToGoogleDoc = async () => {
         this.pullFromGoogleDoc(async (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => {
-            let modes = GoogleApiClientUtils.Docs.WriteMode;
+            const modes = GoogleApiClientUtils.Docs.WriteMode;
             let mode = modes.Replace;
             let reference: Opt<GoogleApiClientUtils.Docs.Reference> = Cast(this.dataDoc[GoogleRef], "string");
             if (!reference) {
                 mode = modes.Insert;
                 reference = { title: StrCast(this.dataDoc.title) };
             }
-            let redo = async () => {
+            const redo = async () => {
                 if (this._editorView && reference) {
-                    let content = await RichTextUtils.GoogleDocs.Export(this._editorView.state);
-                    let response = await GoogleApiClientUtils.Docs.write({ reference, content, mode });
+                    const content = await RichTextUtils.GoogleDocs.Export(this._editorView.state);
+                    const response = await GoogleApiClientUtils.Docs.write({ reference, content, mode });
                     response && (this.dataDoc[GoogleRef] = response.documentId);
-                    let pushSuccess = response !== undefined && !("errors" in response);
+                    const pushSuccess = response !== undefined && !("errors" in response);
                     dataDoc.unchanged = pushSuccess;
                     DocumentButtonBar.Instance.startPushOutcome(pushSuccess);
                 }
             };
-            let undo = () => {
+            const undo = () => {
                 if (!exportState) {
                     return;
                 }
-                let content: GoogleApiClientUtils.Docs.Content = {
+                const content: GoogleApiClientUtils.Docs.Content = {
                     text: exportState.text,
                     requests: []
                 };
@@ -645,8 +646,8 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
     }
 
     pullFromGoogleDoc = async (handler: PullHandler) => {
-        let dataDoc = this.dataDoc;
-        let documentId = StrCast(dataDoc[GoogleRef]);
+        const dataDoc = this.dataDoc;
+        const documentId = StrCast(dataDoc[GoogleRef]);
         let exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>;
         if (documentId) {
             exportState = await RichTextUtils.GoogleDocs.Import(documentId, dataDoc);
@@ -661,8 +662,8 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
             dataDoc.data = new RichTextField(JSON.stringify(exportState.state.toJSON()));
             setTimeout(() => {
                 if (this._editorView) {
-                    let state = this._editorView.state;
-                    let end = state.doc.content.size - 1;
+                    const state = this._editorView.state;
+                    const end = state.doc.content.size - 1;
                     this._editorView.dispatch(state.tr.setSelection(TextSelection.create(state.doc, end, end)));
                 }
             }, 0);
@@ -677,9 +678,9 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
 
     checkState = (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => {
         if (exportState && this._editorView) {
-            let equalContent = _.isEqual(this._editorView.state.doc, exportState.state.doc);
-            let equalTitles = dataDoc.title === exportState.title;
-            let unchanged = equalContent && equalTitles;
+            const equalContent = isEqual(this._editorView.state.doc, exportState.state.doc);
+            const equalTitles = dataDoc.title === exportState.title;
+            const unchanged = equalContent && equalTitles;
             dataDoc.unchanged = unchanged;
             DocumentButtonBar.Instance.setPullState(unchanged);
         }
@@ -707,7 +708,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
     }
 
     handlePaste = (view: EditorView, event: Event, slice: Slice): boolean => {
-        let cbe = event as ClipboardEvent;
+        const cbe = event as ClipboardEvent;
         const pdfDocId = cbe.clipboardData && cbe.clipboardData.getData("dash/pdfOrigin");
         const pdfRegionId = cbe.clipboardData && cbe.clipboardData.getData("dash/pdfRegion");
         if (pdfDocId && pdfRegionId) {
@@ -717,18 +718,18 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
                         setTimeout(async () => {
                             const extension = Doc.fieldExtensionDoc(pdfDoc, "data");
                             if (extension) {
-                                let targetAnnotations = await DocListCastAsync(extension.annotations);// bcz: NO... this assumes the pdf is using its 'data' field.  need to have the PDF's view handle updating its own annotations
+                                const targetAnnotations = await DocListCastAsync(extension.annotations);// bcz: NO... this assumes the pdf is using its 'data' field.  need to have the PDF's view handle updating its own annotations
                                 targetAnnotations && targetAnnotations.push(pdfRegion);
                             }
                         });
 
-                        let link = DocUtils.MakeLink({ doc: this.props.Document, ctx: this.props.ContainingCollectionDoc }, { doc: pdfRegion, ctx: pdfDoc }, "note on " + pdfDoc.title, "pasted PDF link");
+                        const link = DocUtils.MakeLink({ doc: this.props.Document, ctx: this.props.ContainingCollectionDoc }, { doc: pdfRegion, ctx: pdfDoc }, "note on " + pdfDoc.title, "pasted PDF link");
                         if (link) {
                             cbe.clipboardData!.setData("dash/linkDoc", link[Id]);
-                            let linkId = link[Id];
-                            let frag = addMarkToFrag(slice.content, (node: Node) => addLinkMark(node, StrCast(pdfDoc.title), linkId));
+                            const linkId = link[Id];
+                            const frag = addMarkToFrag(slice.content, (node: Node) => addLinkMark(node, StrCast(pdfDoc.title), linkId));
                             slice = new Slice(frag, slice.openStart, slice.openEnd);
-                            var tr = view.state.tr.replaceSelection(slice);
+                            const tr = view.state.tr.replaceSelection(slice);
                             view.dispatch(tr.scrollIntoView().setMeta("paste", true).setMeta("uiEvent", "paste"));
                         }
                     }
@@ -758,11 +759,11 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
     }
 
     private setupEditor(config: any, doc: Doc, fieldKey: string) {
-        let field = doc ? Cast(doc[fieldKey], RichTextField) : undefined;
+        const field = doc ? Cast(doc[fieldKey], RichTextField) : undefined;
         let startup = StrCast(doc.documentText);
         startup = startup.startsWith("@@@") ? startup.replace("@@@", "") : "";
         if (!field && doc) {
-            let text = StrCast(doc[fieldKey]);
+            const text = StrCast(doc[fieldKey]);
             if (text) {
                 startup = text;
             } else if (Cast(doc[fieldKey], "number")) {
@@ -770,16 +771,16 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
             }
         }
         if (this._proseRef) {
-            let self = this;
+            const self = this;
             this._editorView && this._editorView.destroy();
             this._editorView = new EditorView(this._proseRef, {
                 state: field && field.Data ? EditorState.fromJSON(config, JSON.parse(field.Data)) : EditorState.create(config),
                 handleScrollToSelection: (editorView) => {
-                    let ref = editorView.domAtPos(editorView.state.selection.from);
+                    const ref = editorView.domAtPos(editorView.state.selection.from);
                     let refNode = ref.node as any;
                     while (refNode && !("getBoundingClientRect" in refNode)) refNode = refNode.parentElement;
-                    let r1 = refNode && refNode.getBoundingClientRect();
-                    let r3 = self._ref.current!.getBoundingClientRect();
+                    const r1 = refNode && refNode.getBoundingClientRect();
+                    const r3 = self._ref.current!.getBoundingClientRect();
                     if (r1.top < r3.top || r1.top > r3.bottom) {
                         r1 && (self._ref.current!.scrollTop += (r1.top - r3.top) * self.props.ScreenToLocalTransform().Scale);
                     }
@@ -803,7 +804,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
             }
         }
 
-        let selectOnLoad = this.props.Document[Id] === FormattedTextBox.SelectOnLoad;
+        const selectOnLoad = this.props.Document[Id] === FormattedTextBox.SelectOnLoad;
         if (selectOnLoad) {
             FormattedTextBox.SelectOnLoad = "";
             this.props.select(false);
@@ -838,7 +839,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
     }
     onPointerDown = (e: React.PointerEvent): void => {
         FormattedTextBoxComment.textBox = this;
-        let pos = this._editorView!.posAtCoords({ left: e.clientX, top: e.clientY });
+        const pos = this._editorView!.posAtCoords({ left: e.clientX, top: e.clientY });
         pos && (this._nodeClicked = this._editorView!.state.doc.nodeAt(pos.pos));
         if (this.props.onClick && e.button === 0) {
             e.preventDefault();
@@ -923,16 +924,16 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
     hitBulletTargets(x: number, y: number, offsetX: number, select: boolean, highlightOnly = false) {
         clearStyleSheetRules(FormattedTextBox._bulletStyleSheet);
         if (this.props.isSelected(true) && offsetX < 40) {
-            let pos = this._editorView!.posAtCoords({ left: x, top: y });
+            const pos = this._editorView!.posAtCoords({ left: x, top: y });
             if (pos && pos.pos > 0) {
-                let node = this._editorView!.state.doc.nodeAt(pos.pos);
-                let node2 = node?.type === schema.nodes.paragraph ? this._editorView!.state.doc.nodeAt(pos.pos - 1) : undefined;
+                const node = this._editorView!.state.doc.nodeAt(pos.pos);
+                const node2 = node?.type === schema.nodes.paragraph ? this._editorView!.state.doc.nodeAt(pos.pos - 1) : undefined;
                 if ((node === this._nodeClicked || highlightOnly) && (node2?.type === schema.nodes.ordered_list || node2?.type === schema.nodes.list_item)) {
-                    let hit = this._editorView!.domAtPos(pos.pos).node as any;   // let beforeEle = document.querySelector("." + hit.className) as Element;
-                    let before = hit ? window.getComputedStyle(hit, ':before') : undefined;
-                    let beforeWidth = before ? Number(before.getPropertyValue('width').replace("px", "")) : undefined;
+                    const hit = this._editorView!.domAtPos(pos.pos).node as any;   // let beforeEle = document.querySelector("." + hit.className) as Element;
+                    const before = hit ? window.getComputedStyle(hit, ':before') : undefined;
+                    const beforeWidth = before ? Number(before.getPropertyValue('width').replace("px", "")) : undefined;
                     if (beforeWidth && offsetX < beforeWidth * .9) {
-                        let ol = this._editorView!.state.doc.nodeAt(pos.pos - 2) ? this._editorView!.state.doc.nodeAt(pos.pos - 2) : undefined;
+                        const ol = this._editorView!.state.doc.nodeAt(pos.pos - 2) ? this._editorView!.state.doc.nodeAt(pos.pos - 2) : undefined;
                         if (ol?.type === schema.nodes.ordered_list && select) {
                             if (!highlightOnly) {
                                 this._editorView!.dispatch(this._editorView!.state.tr.setSelection(new NodeSelection(this._editorView!.state.doc.resolve(pos.pos - 2))));
@@ -953,11 +954,11 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
     onMouseUp = (e: React.MouseEvent): void => {
         e.stopPropagation();
 
-        let view = this._editorView as any;
+        const view = this._editorView as any;
         // this interposes on prosemirror's upHandler to prevent prosemirror's up from invoked multiple times when there 
         // are nested prosemirrors.  We only want the lowest level prosemirror to be invoked.
         if (view.mouseDown) {
-            let originalUpHandler = view.mouseDown.up;
+            const originalUpHandler = view.mouseDown.up;
             view.root.removeEventListener("mouseup", originalUpHandler);
             view.mouseDown.up = (e: MouseEvent) => {
                 !(e as any).formattedHandled && originalUpHandler(e);
@@ -969,7 +970,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
     }
 
     tooltipTextMenuPlugin() {
-        let self = FormattedTextBox;
+        const self = FormattedTextBox;
         return new Plugin({
             view(newView) {
                 return self.ToolTipTextMenu = FormattedTextBox.getToolTip(newView);
@@ -978,7 +979,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
     }
 
     tooltipLinkingMenuPlugin() {
-        let myprops = this.props;
+        const myprops = this.props;
         return new Plugin({
             view(_editorView) {
                 return new TooltipLinkingMenu(_editorView, myprops);
@@ -1001,7 +1002,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
         if (e.key === "Tab" || e.key === "Enter") {
             e.preventDefault();
         }
-        let mark = schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.round(Date.now() / 1000 / 5) });
+        const mark = schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.round(Date.now() / 1000 / 5) });
         this._editorView!.dispatch(this._editorView!.state.tr.removeStoredMark(schema.marks.user_mark.create({})).addStoredMark(mark));
 
         if (!this._undoTyping) {
@@ -1018,8 +1019,8 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
         const scrollHeight = this._ref.current?.scrollHeight;
         if (!this.layoutDoc.animateToPos && this.layoutDoc.autoHeight && scrollHeight &&
             getComputedStyle(this._ref.current!.parentElement!).top === "0px") {  // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation
-            let nh = this.Document.isTemplateField ? 0 : NumCast(this.dataDoc.nativeHeight, 0);
-            let dh = NumCast(this.layoutDoc.height, 0);
+            const nh = this.Document.isTemplateField ? 0 : NumCast(this.dataDoc.nativeHeight, 0);
+            const dh = NumCast(this.layoutDoc.height, 0);
             this.layoutDoc.height = Math.max(10, (nh ? dh / nh * scrollHeight : scrollHeight) + (this.props.ChromeHeight ? this.props.ChromeHeight() : 0));
             this.dataDoc.nativeHeight = nh ? scrollHeight : undefined;
         }
@@ -1030,8 +1031,8 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
     @computed get annotationsKey() { return "annotations"; }
     render() {
         TraceMobx();
-        let rounded = StrCast(this.layoutDoc.borderRounding) === "100%" ? "-rounded" : "";
-        let interactive = InkingControl.Instance.selectedTool || this.layoutDoc.isBackground;
+        const rounded = StrCast(this.layoutDoc.borderRounding) === "100%" ? "-rounded" : "";
+        const interactive = InkingControl.Instance.selectedTool || this.layoutDoc.isBackground;
         if (this.props.isSelected()) {
             FormattedTextBox.ToolTipTextMenu!.updateFromDash(this._editorView!, undefined, this.props);
         } else if (FormattedTextBoxComment.textBox === this) {
diff --git a/src/client/views/nodes/FormattedTextBoxComment.tsx b/src/client/views/nodes/FormattedTextBoxComment.tsx
index c076fd34a..2ec30e3b3 100644
--- a/src/client/views/nodes/FormattedTextBoxComment.tsx
+++ b/src/client/views/nodes/FormattedTextBoxComment.tsx
@@ -81,7 +81,7 @@ export class FormattedTextBoxComment {
             FormattedTextBoxComment.tooltip.style.display = "none";
             FormattedTextBoxComment.tooltip.appendChild(FormattedTextBoxComment.tooltipInput);
             FormattedTextBoxComment.tooltip.onpointerdown = (e: PointerEvent) => {
-                let keep = e.target && (e.target as any).type === "checkbox" ? true : false;
+                const keep = e.target && (e.target as any).type === "checkbox" ? true : false;
                 const textBox = FormattedTextBoxComment.textBox;
                 if (FormattedTextBoxComment.linkDoc && !keep && textBox) {
                     DocumentManager.Instance.FollowLink(FormattedTextBoxComment.linkDoc, textBox.props.Document,
@@ -113,7 +113,7 @@ export class FormattedTextBoxComment {
     }
 
     static update(view: EditorView, lastState?: EditorState) {
-        let state = view.state;
+        const state = view.state;
         // Don't do anything if the document/selection didn't change
         if (lastState && lastState.doc.eq(state.doc) &&
             lastState.selection.eq(state.selection)) {
@@ -136,11 +136,11 @@ export class FormattedTextBoxComment {
         // this section checks to see if the insertion point is over text entered by a different user.  If so, it sets ths comment text to indicate the user and the modification date
         if (state.selection.$from) {
             nbef = findStartOfMark(state.selection.$from, view, findOtherUserMark);
-            let naft = findEndOfMark(state.selection.$from, view, findOtherUserMark);
-            let noselection = view.state.selection.$from === view.state.selection.$to;
+            const naft = findEndOfMark(state.selection.$from, view, findOtherUserMark);
+            const noselection = view.state.selection.$from === view.state.selection.$to;
             let child: any = null;
             state.doc.nodesBetween(state.selection.from, state.selection.to, (node: any, pos: number, parent: any) => !child && node.marks.length && (child = node));
-            let mark = child && findOtherUserMark(child.marks);
+            const mark = child && findOtherUserMark(child.marks);
             if (mark && child && (nbef || naft) && (!mark.attrs.opened || noselection)) {
                 FormattedTextBoxComment.SetState(FormattedTextBoxComment.textBox, mark.attrs.opened, state.selection.$from.pos - nbef, state.selection.$from.pos + naft, mark);
             }
@@ -153,10 +153,10 @@ export class FormattedTextBoxComment {
         // this checks if the selection is a hyperlink.  If so, it displays the target doc's text for internal links, and the url of the target for external links. 
         if (set === "none" && state.selection.$from) {
             nbef = findStartOfMark(state.selection.$from, view, findLinkMark);
-            let naft = findEndOfMark(state.selection.$from, view, findLinkMark);
+            const naft = findEndOfMark(state.selection.$from, view, findLinkMark);
             let child: any = null;
             state.doc.nodesBetween(state.selection.from, state.selection.to, (node: any, pos: number, parent: any) => !child && node.marks.length && (child = node));
-            let mark = child && findLinkMark(child.marks);
+            const mark = child && findLinkMark(child.marks);
             if (mark && child && nbef && naft) {
                 FormattedTextBoxComment.tooltipText.textContent = "external => " + mark.attrs.href;
                 if (mark.attrs.href.startsWith("https://en.wikipedia.org/wiki/")) {
@@ -167,7 +167,7 @@ export class FormattedTextBoxComment {
                 }
                 (FormattedTextBoxComment.tooltipText as any).href = mark.attrs.href;
                 if (mark.attrs.href.indexOf(Utils.prepend("/doc/")) === 0) {
-                    let docTarget = mark.attrs.href.replace(Utils.prepend("/doc/"), "").split("?")[0];
+                    const docTarget = mark.attrs.href.replace(Utils.prepend("/doc/"), "").split("?")[0];
                     docTarget && DocServer.GetRefField(docTarget).then(linkDoc => {
                         if (linkDoc instanceof Doc) {
                             FormattedTextBoxComment.linkDoc = linkDoc;
@@ -210,12 +210,12 @@ export class FormattedTextBoxComment {
         if (set !== "none") {
             // These are in screen coordinates
             // let start = view.coordsAtPos(state.selection.from), end = view.coordsAtPos(state.selection.to);
-            let start = view.coordsAtPos(state.selection.from - nbef), end = view.coordsAtPos(state.selection.from - nbef);
+            const start = view.coordsAtPos(state.selection.from - nbef), end = view.coordsAtPos(state.selection.from - nbef);
             // The box in which the tooltip is positioned, to use as base
-            let box = (document.getElementById("mainView-container") as any).getBoundingClientRect();
+            const box = (document.getElementById("mainView-container") as any).getBoundingClientRect();
             // Find a center-ish x position from the selection endpoints (when
             // crossing lines, end may be more to the left)
-            let left = Math.max((start.left + end.left) / 2, start.left + 3);
+            const left = Math.max((start.left + end.left) / 2, start.left + 3);
             FormattedTextBoxComment.tooltip.style.left = (left - box.left) + "px";
             FormattedTextBoxComment.tooltip.style.bottom = (box.bottom - start.top) + "px";
         }
diff --git a/src/client/views/nodes/IconBox.tsx b/src/client/views/nodes/IconBox.tsx
index 60f547b1e..9462ff024 100644
--- a/src/client/views/nodes/IconBox.tsx
+++ b/src/client/views/nodes/IconBox.tsx
@@ -51,7 +51,7 @@ export class IconBox extends React.Component<FieldViewProps> {
     }
 
     public static DocumentIcon(layout: string) {
-        let button = layout.indexOf("PDFBox") !== -1 ? faFilePdf :
+        const button = layout.indexOf("PDFBox") !== -1 ? faFilePdf :
             layout.indexOf("ImageBox") !== -1 ? faImage :
                 layout.indexOf("Formatted") !== -1 ? faStickyNote :
                     layout.indexOf("Video") !== -1 ? faFilm :
@@ -65,14 +65,14 @@ export class IconBox extends React.Component<FieldViewProps> {
     }
 
     specificContextMenu = (): void => {
-        let cm = ContextMenu.Instance;
+        const cm = ContextMenu.Instance;
         cm.addItem({ description: this.props.Document.hideLabel ? "Show label with icon" : "Remove label from icon", event: this.setLabelField, icon: "tag" });
         if (!this.props.Document.hideLabel) {
             cm.addItem({ description: "Use Target Title", event: () => IconBox.AutomaticTitle(this.props.Document), icon: "text-height" });
         }
     }
     render() {
-        let label = this.props.Document.hideLabel ? "" : this.props.Document.title;
+        const label = this.props.Document.hideLabel ? "" : this.props.Document.title;
         return (
             <div className="iconBox-container" onContextMenu={this.specificContextMenu}>
                 {this.minimizedIcon}
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index c283e4f21..b4a51657f 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -28,8 +28,8 @@ import { documentSchema } from '../../../new_fields/documentSchemas';
 import { Id } from '../../../new_fields/FieldSymbols';
 import { TraceMobx } from '../../../new_fields/util';
 import { SelectionManager } from '../../util/SelectionManager';
-var requestImageSize = require('../../util/request-image-size');
-var path = require('path');
+const requestImageSize = require('../../util/request-image-size');
+const path = require('path');
 const { Howl } = require('howler');
 
 
@@ -88,7 +88,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
     recordAudioAnnotation = () => {
         let gumStream: any;
         let recorder: any;
-        let self = this;
+        const self = this;
         const extensionDoc = this.extensionDoc;
         extensionDoc && navigator.mediaDevices.getUserMedia({
             audio: true
@@ -105,9 +105,9 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
                 const files = await res.json();
                 const url = Utils.prepend(files[0].path);
                 // upload to server with known URL 
-                let audioDoc = Docs.Create.AudioDocument(url, { title: "audio test", width: 200, height: 32 });
+                const audioDoc = Docs.Create.AudioDocument(url, { title: "audio test", width: 200, height: 32 });
                 audioDoc.treeViewExpandedView = "layout";
-                let audioAnnos = Cast(extensionDoc.audioAnnotations, listSpec(Doc));
+                const audioAnnos = Cast(extensionDoc.audioAnnotations, listSpec(Doc));
                 if (audioAnnos === undefined) {
                     extensionDoc.audioAnnotations = new List([audioDoc]);
                 } else {
@@ -126,10 +126,10 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
 
     @undoBatch
     rotate = action(() => {
-        let nw = this.Document.nativeWidth;
-        let nh = this.Document.nativeHeight;
-        let w = this.Document.width;
-        let h = this.Document.height;
+        const nw = this.Document.nativeWidth;
+        const nh = this.Document.nativeHeight;
+        const w = this.Document.width;
+        const h = this.Document.height;
         this.Document.rotation = ((this.Document.rotation || 0) + 90) % 360;
         this.Document.nativeWidth = nh;
         this.Document.nativeHeight = nw;
@@ -140,12 +140,12 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
     specificContextMenu = (e: React.MouseEvent): void => {
         const field = Cast(this.Document[this.props.fieldKey], ImageField);
         if (field) {
-            let funcs: ContextMenuProps[] = [];
+            const funcs: ContextMenuProps[] = [];
             funcs.push({ description: "Copy path", event: () => Utils.CopyText(field.url.href), icon: "expand-arrows-alt" });
             funcs.push({ description: "Rotate", event: this.rotate, icon: "expand-arrows-alt" });
 
-            let existingAnalyze = ContextMenu.Instance.findByDescription("Analyzers...");
-            let modes: ContextMenuProps[] = existingAnalyze && "subitems" in existingAnalyze ? existingAnalyze.subitems : [];
+            const existingAnalyze = ContextMenu.Instance.findByDescription("Analyzers...");
+            const modes: ContextMenuProps[] = existingAnalyze && "subitems" in existingAnalyze ? existingAnalyze.subitems : [];
             modes.push({ description: "Generate Tags", event: this.generateMetadata, icon: "tag" });
             modes.push({ description: "Find Faces", event: this.extractFaces, icon: "camera" });
             !existingAnalyze && ContextMenu.Instance.addItem({ description: "Analyzers...", subitems: modes, icon: "hand-point-right" });
@@ -155,8 +155,8 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
     }
 
     extractFaces = () => {
-        let converter = (results: any) => {
-            let faceDocs = new List<Doc>();
+        const converter = (results: any) => {
+            const faceDocs = new List<Doc>();
             results.reduce((face: CognitiveServices.Image.Face, faceDocs: List<Doc>) => faceDocs.push(Docs.Get.DocumentHierarchyFromJson(face, `Face: ${face.faceId}`)!), new List<Doc>());
             return faceDocs;
         };
@@ -164,12 +164,12 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
     }
 
     generateMetadata = (threshold: Confidence = Confidence.Excellent) => {
-        let converter = (results: any) => {
-            let tagDoc = new Doc;
-            let tagsList = new List();
+        const converter = (results: any) => {
+            const tagDoc = new Doc;
+            const tagsList = new List();
             results.tags.map((tag: Tag) => {
                 tagsList.push(tag.name);
-                let sanitized = tag.name.replace(" ", "_");
+                const sanitized = tag.name.replace(" ", "_");
                 tagDoc[sanitized] = ComputedField.MakeFunction(`(${tag.confidence} >= this.confidence) ? ${tag.confidence} : "${ComputedField.undefined}"`);
             });
             this.extensionDoc && (this.extensionDoc.generatedTags = tagsList);
@@ -181,7 +181,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
     }
 
     @computed private get url() {
-        let data = Cast(this.dataDoc[this.props.fieldKey], ImageField);
+        const data = Cast(this.dataDoc[this.props.fieldKey], ImageField);
         return data ? data.url.href : undefined;
     }
 
@@ -194,7 +194,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
         } else if (!(lower.endsWith(".png") || lower.endsWith(".jpg") || lower.endsWith(".jpeg"))) {
             return url.href;//Why is this here
         }
-        let ext = path.extname(url.href);
+        const ext = path.extname(url.href);
         const suffix = this.props.renderDepth < 1 ? "_o" : this._curSuffix;
         return url.href.replace(ext, suffix + ext);
     }
@@ -208,7 +208,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
         if (this._curSuffix === "_l") this._largeRetryCount++;
     }
     @action onError = () => {
-        let timeout = this._curSuffix === "_s" ? this._smallRetryCount : this._curSuffix === "_m" ? this._mediumRetryCount : this._largeRetryCount;
+        const timeout = this._curSuffix === "_s" ? this._smallRetryCount : this._curSuffix === "_m" ? this._mediumRetryCount : this._largeRetryCount;
         if (timeout < 10) {
             setTimeout(this.retryPath, Math.min(10000, timeout * 5));
         }
@@ -218,9 +218,9 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
     resize = (srcpath: string) => {
         requestImageSize(srcpath)
             .then((size: any) => {
-                let rotation = NumCast(this.dataDoc.rotation) % 180;
-                let realsize = rotation === 90 || rotation === 270 ? { height: size.width, width: size.height } : size;
-                let aspect = realsize.height / realsize.width;
+                const rotation = NumCast(this.dataDoc.rotation) % 180;
+                const realsize = rotation === 90 || rotation === 270 ? { height: size.width, width: size.height } : size;
+                const aspect = realsize.height / realsize.width;
                 if (this.Document.width && (Math.abs(1 - NumCast(this.Document.height) / NumCast(this.Document.width) / (realsize.height / realsize.width)) > 0.1)) {
                     setTimeout(action(() => {
                         this.Document.height = this.Document[WidthSym]() * aspect;
@@ -234,9 +234,9 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
     fadesize = (srcpath: string) => {
         requestImageSize(srcpath)
             .then((size: any) => {
-                let rotation = NumCast(this.dataDoc.rotation) % 180;
-                let realsize = rotation === 90 || rotation === 270 ? { height: size.width, width: size.height } : size;
-                let aspect = realsize.height / realsize.width;
+                const rotation = NumCast(this.dataDoc.rotation) % 180;
+                const realsize = rotation === 90 || rotation === 270 ? { height: size.width, width: size.height } : size;
+                const aspect = realsize.height / realsize.width;
                 if (this.Document.width && (Math.abs(1 - NumCast(this.Document.height) / NumCast(this.Document.width) / (realsize.height / realsize.width)) > 0.1)) {
                     setTimeout(action(() => {
                         this.Document.height = this.Document[WidthSym]() * aspect;
@@ -250,10 +250,10 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
 
     @action
     onPointerEnter = () => {
-        let self = this;
-        let audioAnnos = this.extensionDoc && DocListCast(this.extensionDoc.audioAnnotations);
+        const self = this;
+        const audioAnnos = this.extensionDoc && DocListCast(this.extensionDoc.audioAnnotations);
         if (audioAnnos && audioAnnos.length && this._audioState === 0) {
-            let anno = audioAnnos[Math.floor(Math.random() * audioAnnos.length)];
+            const anno = audioAnnos[Math.floor(Math.random() * audioAnnos.length)];
             anno.data instanceof AudioField && new Howl({
                 src: [anno.data.url.href],
                 format: ["mp3"],
@@ -289,32 +289,32 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
         const extensionDoc = this.extensionDoc;
         if (!extensionDoc) return (null);
         // let transform = this.props.ScreenToLocalTransform().inverse();
-        let pw = typeof this.props.PanelWidth === "function" ? this.props.PanelWidth() : typeof this.props.PanelWidth === "number" ? (this.props.PanelWidth as any) as number : 50;
+        const pw = typeof this.props.PanelWidth === "function" ? this.props.PanelWidth() : typeof this.props.PanelWidth === "number" ? (this.props.PanelWidth as any) as number : 50;
         // var [sptX, sptY] = transform.transformPoint(0, 0);
         // let [bptX, bptY] = transform.transformPoint(pw, this.props.PanelHeight());
         // let w = bptX - sptX;
 
-        let nativeWidth = (this.Document.nativeWidth || pw);
-        let nativeHeight = (this.Document.nativeHeight || 1);
+        const nativeWidth = (this.Document.nativeWidth || pw);
+        const nativeHeight = (this.Document.nativeHeight || 1);
         let paths = [[Utils.CorsProxy("http://www.cs.brown.edu/~bcz/noImage.png"), nativeWidth / nativeHeight]];
         // this._curSuffix = "";
         // if (w > 20) {
-        let alts = DocListCast(extensionDoc.Alternates);
-        let altpaths = alts.filter(doc => doc.data instanceof ImageField).map(doc => [this.choosePath((doc.data as ImageField).url), doc[WidthSym]() / doc[HeightSym]()]);
-        let field = this.dataDoc[this.props.fieldKey];
+        const alts = DocListCast(extensionDoc.Alternates);
+        const altpaths = alts.filter(doc => doc.data instanceof ImageField).map(doc => [this.choosePath((doc.data as ImageField).url), doc[WidthSym]() / doc[HeightSym]()]);
+        const field = this.dataDoc[this.props.fieldKey];
         // if (w < 100 && this._smallRetryCount < 10) this._curSuffix = "_s";
         // else if (w < 600 && this._mediumRetryCount < 10) this._curSuffix = "_m";
         // else if (this._largeRetryCount < 10) this._curSuffix = "_l";
         if (field instanceof ImageField) paths = [[this.choosePath(field.url), nativeWidth / nativeHeight]];
         paths.push(...altpaths);
         // }
-        let dragging = !SelectionManager.GetIsDragging() ? "" : "-dragging";
-        let rotation = NumCast(this.Document.rotation, 0);
-        let aspect = (rotation % 180) ? this.Document[HeightSym]() / this.Document[WidthSym]() : 1;
-        let shift = (rotation % 180) ? (nativeHeight - nativeWidth / aspect) / 2 : 0;
-        let srcpath = paths[Math.min(paths.length - 1, (this.Document.curPage || 0))][0] as string;
-        let srcaspect = paths[Math.min(paths.length - 1, (this.Document.curPage || 0))][1] as number;
-        let fadepath = paths[Math.min(paths.length - 1, 1)][0] as string;
+        const dragging = !SelectionManager.GetIsDragging() ? "" : "-dragging";
+        const rotation = NumCast(this.Document.rotation, 0);
+        const aspect = (rotation % 180) ? this.Document[HeightSym]() / this.Document[WidthSym]() : 1;
+        const shift = (rotation % 180) ? (nativeHeight - nativeWidth / aspect) / 2 : 0;
+        const srcpath = paths[Math.min(paths.length - 1, (this.Document.curPage || 0))][0] as string;
+        const srcaspect = paths[Math.min(paths.length - 1, (this.Document.curPage || 0))][1] as number;
+        const fadepath = paths[Math.min(paths.length - 1, 1)][0] as string;
 
         !this.Document.ignoreAspect && this.resize(srcpath);
 
diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index aa6e135fe..fba29e4cd 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -53,13 +53,13 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
         }
     }
     public static CompileKVPScript(value: string): KVPScript | undefined {
-        let eq = value.startsWith("=");
+        const eq = value.startsWith("=");
         value = eq ? value.substr(1) : value;
         const dubEq = value.startsWith(":=") ? "computed" : value.startsWith(";=") ? "script" : false;
         value = dubEq ? value.substr(2) : value;
-        let options: ScriptOptions = { addReturn: true, params: { this: "Doc" } };
+        const options: ScriptOptions = { addReturn: true, params: { this: "Doc" } };
         if (dubEq) options.typecheck = false;
-        let script = CompileScript(value, options);
+        const script = CompileScript(value, options);
         if (!script.compiled) {
             return undefined;
         }
@@ -76,7 +76,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
         } else if (type === "script") {
             field = new ScriptField(script);
         } else {
-            let res = script.run({ this: target }, console.log);
+            const res = script.run({ this: target }, console.log);
             if (!res.success) return false;
             field = res.result;
         }
@@ -106,14 +106,14 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
     rowHeight = () => 30;
 
     createTable = () => {
-        let doc = this.fieldDocToLayout;
+        const doc = this.fieldDocToLayout;
         if (!doc) {
             return <tr><td>Loading...</td></tr>;
         }
-        let realDoc = doc;
+        const realDoc = doc;
 
-        let ids: { [key: string]: string } = {};
-        let protos = Doc.GetAllPrototypes(doc);
+        const ids: { [key: string]: string } = {};
+        const protos = Doc.GetAllPrototypes(doc);
         for (const proto of protos) {
             Object.keys(proto).forEach(key => {
                 if (!(key in ids) && realDoc[key] !== ComputedField.undefined) {
@@ -122,10 +122,10 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
             });
         }
 
-        let rows: JSX.Element[] = [];
+        const rows: JSX.Element[] = [];
         let i = 0;
         const self = this;
-        for (let key of Object.keys(ids).slice().sort()) {
+        for (const key of Object.keys(ids).slice().sort()) {
             rows.push(<KeyValuePair doc={realDoc} addDocTab={this.props.addDocTab} PanelWidth={this.props.PanelWidth} PanelHeight={this.rowHeight}
                 ref={(function () {
                     let oldEl: KeyValuePair | undefined;
@@ -163,7 +163,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
 
     @action
     onDividerMove = (e: PointerEvent): void => {
-        let nativeWidth = this._mainCont.current!.getBoundingClientRect();
+        const nativeWidth = this._mainCont.current!.getBoundingClientRect();
         this.props.Document.schemaSplitPercentage = Math.max(0, 100 - Math.round((e.clientX - nativeWidth.left) / nativeWidth.width * 100));
     }
     @action
@@ -179,10 +179,10 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
     }
 
     getTemplate = async () => {
-        let parent = Docs.Create.StackingDocument([], { width: 800, height: 800, title: "Template" });
+        const parent = Docs.Create.StackingDocument([], { width: 800, height: 800, title: "Template" });
         parent.singleColumn = false;
         parent.columnWidth = 100;
-        for (let row of this.rows.filter(row => row.isChecked)) {
+        for (const row of this.rows.filter(row => row.isChecked)) {
             await this.createTemplateField(parent, row);
             row.uncheck();
         }
@@ -190,17 +190,17 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
     }
 
     createTemplateField = async (parentStackingDoc: Doc, row: KeyValuePair) => {
-        let metaKey = row.props.keyName;
-        let sourceDoc = await Cast(this.props.Document.data, Doc);
+        const metaKey = row.props.keyName;
+        const sourceDoc = await Cast(this.props.Document.data, Doc);
         if (!sourceDoc) {
             return;
         }
 
-        let fieldTemplate = await this.inferType(sourceDoc[metaKey], metaKey);
+        const fieldTemplate = await this.inferType(sourceDoc[metaKey], metaKey);
         if (!fieldTemplate) {
             return;
         }
-        let previousViewType = fieldTemplate.viewType;
+        const previousViewType = fieldTemplate.viewType;
         Doc.MakeMetadataFieldTemplate(fieldTemplate, Doc.GetProto(parentStackingDoc));
         previousViewType && (fieldTemplate.viewType = previousViewType);
 
@@ -208,14 +208,14 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
     }
 
     inferType = async (data: FieldResult, metaKey: string) => {
-        let options = { width: 300, height: 300, title: metaKey };
+        const options = { width: 300, height: 300, title: metaKey };
         if (data instanceof RichTextField || typeof data === "string" || typeof data === "number") {
             return Docs.Create.TextDocument(options);
         } else if (data instanceof List) {
             if (data.length === 0) {
                 return Docs.Create.StackingDocument([], options);
             }
-            let first = await Cast(data[0], Doc);
+            const first = await Cast(data[0], Doc);
             if (!first || !first.data) {
                 return Docs.Create.StackingDocument([], options);
             }
@@ -235,7 +235,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
     }
 
     render() {
-        let dividerDragger = this.splitPercentage === 0 ? (null) :
+        const dividerDragger = this.splitPercentage === 0 ? (null) :
             <div className="keyValueBox-dividerDragger" style={{ transform: `translate(calc(${100 - this.splitPercentage}% - 5px), 0px)` }}>
                 <div className="keyValueBox-dividerDraggerThumb" onPointerDown={this.onDividerDown} />
             </div>;
diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx
index 225565964..bca68cdd9 100644
--- a/src/client/views/nodes/KeyValuePair.tsx
+++ b/src/client/views/nodes/KeyValuePair.tsx
@@ -53,7 +53,7 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
     }
 
     render() {
-        let props: FieldViewProps = {
+        const props: FieldViewProps = {
             Document: this.props.doc,
             DataDoc: this.props.doc,
             ContainingCollectionView: undefined,
@@ -73,7 +73,7 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
             pinToPres: returnZero,
             ContentScaling: returnOne
         };
-        let contents = <FieldView {...props} />;
+        const contents = <FieldView {...props} />;
         // let fieldKey = Object.keys(props.Document).indexOf(props.fieldKey) !== -1 ? props.fieldKey : "(" + props.fieldKey + ")";
         let protoCount = 0;
         let doc: Doc | undefined = props.Document;
@@ -85,9 +85,9 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
             doc = doc.proto;
         }
         const parenCount = Math.max(0, protoCount - 1);
-        let keyStyle = protoCount === 0 ? "black" : "blue";
+        const keyStyle = protoCount === 0 ? "black" : "blue";
 
-        let hover = { transition: "0.3s ease opacity", opacity: this.isPointerOver || this.isChecked ? 1 : 0 };
+        const hover = { transition: "0.3s ease opacity", opacity: this.isPointerOver || this.isChecked ? 1 : 0 };
 
         return (
             <tr className={this.props.rowStyle} onPointerEnter={action(() => this.isPointerOver = true)} onPointerLeave={action(() => this.isPointerOver = false)}>
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 87be1f4e9..0f5cc0fa9 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -95,7 +95,7 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument>
     @undoBatch
     @action
     private applyFilter = () => {
-        let scriptText = this._scriptValue ? this._scriptValue :
+        const scriptText = this._scriptValue ? this._scriptValue :
             this._keyValue && this._valueValue ? `this.${this._keyValue} === ${this._valueValue}` : "true";
         this.props.Document.filterScript = ScriptField.MakeFunction(scriptText);
     }
@@ -116,7 +116,7 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument>
     searchStringChanged = (e: React.ChangeEvent<HTMLInputElement>) => this._searchString = e.currentTarget.value;
 
     settingsPanel() {
-        let pageBtns = <>
+        const pageBtns = <>
             <button className="pdfBox-overlayButton-iconCont" key="back" title="Page Back"
                 onPointerDown={e => e.stopPropagation()} onClick={e => this.backPage()} style={{ left: 45, top: 5 }}>
                 <FontAwesomeIcon style={{ color: "white" }} icon={"arrow-left"} size="sm" />
@@ -186,7 +186,7 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument>
 
     specificContextMenu = (e: React.MouseEvent): void => {
         const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField);
-        let funcs: ContextMenuProps[] = [];
+        const funcs: ContextMenuProps[] = [];
         pdfUrl && funcs.push({ description: "Copy path", event: () => Utils.CopyText(pdfUrl.url.pathname), icon: "expand-arrows-alt" });
         funcs.push({ description: "Toggle Fit Width " + (this.Document.fitWidth ? "Off" : "On"), event: () => this.Document.fitWidth = !this.Document.fitWidth, icon: "expand-arrows-alt" });
 
@@ -195,7 +195,7 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument>
 
     @computed get contentScaling() { return this.props.ContentScaling(); }
     @computed get renderTitleBox() {
-        let classname = "pdfBox" + (this.active() ? "-interactive" : "");
+        const classname = "pdfBox" + (this.active() ? "-interactive" : "");
         return <div className={classname} style={{
             width: !this.props.Document.fitWidth ? NumCast(this.props.Document.nativeWidth) : `${100 / this.contentScaling}%`,
             height: !this.props.Document.fitWidth ? NumCast(this.props.Document.nativeHeight) : `${100 / this.contentScaling}%`,
diff --git a/src/client/views/nodes/PresBox.tsx b/src/client/views/nodes/PresBox.tsx
index cbb83b511..1e6894f37 100644
--- a/src/client/views/nodes/PresBox.tsx
+++ b/src/client/views/nodes/PresBox.tsx
@@ -42,7 +42,7 @@ export class PresBox extends React.Component<FieldViewProps> {
             if (value) {
                 value.forEach((item, i) => {
                     if (item instanceof Doc && item.type !== DocumentType.PRESELEMENT) {
-                        let pinDoc = Docs.Create.PresElementBoxDocument({ backgroundColor: "transparent" });
+                        const pinDoc = Docs.Create.PresElementBoxDocument({ backgroundColor: "transparent" });
                         Doc.GetProto(pinDoc).presentationTargetDoc = item;
                         Doc.GetProto(pinDoc).title = ComputedField.MakeFunction('(this.presentationTargetDoc instanceof Doc) && this.presentationTargetDoc.title.toString()');
                         value.splice(i, 1, pinDoc);
@@ -61,9 +61,9 @@ export class PresBox extends React.Component<FieldViewProps> {
     next = async () => {
         const current = NumCast(this.props.Document.selectedDoc);
         //asking to get document at current index
-        let docAtCurrentNext = await this.getDocAtIndex(current + 1);
+        const docAtCurrentNext = await this.getDocAtIndex(current + 1);
         if (docAtCurrentNext !== undefined) {
-            let presDocs = DocListCast(this.props.Document[this.props.fieldKey]);
+            const presDocs = DocListCast(this.props.Document[this.props.fieldKey]);
             let nextSelected = current + 1;
 
             for (; nextSelected < presDocs.length - 1; nextSelected++) {
@@ -78,15 +78,15 @@ export class PresBox extends React.Component<FieldViewProps> {
     back = async () => {
         const current = NumCast(this.props.Document.selectedDoc);
         //requesting for the doc at current index
-        let docAtCurrent = await this.getDocAtIndex(current);
+        const docAtCurrent = await this.getDocAtIndex(current);
         if (docAtCurrent !== undefined) {
 
             //asking for its presentation id.
             let prevSelected = current;
             let zoomOut: boolean = false;
 
-            let presDocs = await DocListCastAsync(this.props.Document[this.props.fieldKey]);
-            let currentsArray: Doc[] = [];
+            const presDocs = await DocListCastAsync(this.props.Document[this.props.fieldKey]);
+            const currentsArray: Doc[] = [];
             for (; presDocs && prevSelected > 0 && presDocs[prevSelected].groupButton; prevSelected--) {
                 currentsArray.push(presDocs[prevSelected]);
             }
@@ -104,8 +104,8 @@ export class PresBox extends React.Component<FieldViewProps> {
             //If so making sure to zoom out, which goes back to state before zooming action
             if (current > 0) {
                 if (zoomOut || docAtCurrent.showButton) {
-                    let prevScale = NumCast(this.childDocs[prevSelected].viewScale, null);
-                    let curScale = DocumentManager.Instance.getScaleOfDocView(this.childDocs[current]);
+                    const prevScale = NumCast(this.childDocs[prevSelected].viewScale, null);
+                    const curScale = DocumentManager.Instance.getScaleOfDocView(this.childDocs[current]);
                     if (prevScale !== undefined && prevScale !== curScale) {
                         DocumentManager.Instance.zoomIntoScale(docAtCurrent, prevScale);
                     }
@@ -162,13 +162,13 @@ export class PresBox extends React.Component<FieldViewProps> {
      * te option open, navigates to that element.
      */
     navigateToElement = async (curDoc: Doc, fromDocIndex: number) => {
-        let fromDoc = this.childDocs[fromDocIndex].presentationTargetDoc as Doc;
+        const fromDoc = this.childDocs[fromDocIndex].presentationTargetDoc as Doc;
         let docToJump = curDoc;
         let willZoom = false;
 
-        let presDocs = DocListCast(this.props.Document[this.props.fieldKey]);
+        const presDocs = DocListCast(this.props.Document[this.props.fieldKey]);
         let nextSelected = presDocs.indexOf(curDoc);
-        let currentDocGroups: Doc[] = [];
+        const currentDocGroups: Doc[] = [];
         for (; nextSelected < presDocs.length - 1; nextSelected++) {
             if (!presDocs[nextSelected + 1].groupButton) {
                 break;
@@ -190,11 +190,11 @@ export class PresBox extends React.Component<FieldViewProps> {
         //docToJump stayed same meaning, it was not in the group or was the last element in the group
         if (docToJump === curDoc) {
             //checking if curDoc has navigation open
-            let target = await curDoc.presentationTargetDoc as Doc;
+            const target = await curDoc.presentationTargetDoc as Doc;
             if (curDoc.navButton) {
                 DocumentManager.Instance.jumpToDocument(target, false);
             } else if (curDoc.showButton) {
-                let curScale = DocumentManager.Instance.getScaleOfDocView(fromDoc);
+                const curScale = DocumentManager.Instance.getScaleOfDocView(fromDoc);
                 //awaiting jump so that new scale can be found, since jumping is async
                 await DocumentManager.Instance.jumpToDocument(target, true);
                 curDoc.viewScale = DocumentManager.Instance.getScaleOfDocView(target);
@@ -207,11 +207,11 @@ export class PresBox extends React.Component<FieldViewProps> {
             }
             return;
         }
-        let curScale = DocumentManager.Instance.getScaleOfDocView(fromDoc);
+        const curScale = DocumentManager.Instance.getScaleOfDocView(fromDoc);
 
         //awaiting jump so that new scale can be found, since jumping is async
         await DocumentManager.Instance.jumpToDocument(await docToJump.presentationTargetDoc as Doc, willZoom);
-        let newScale = DocumentManager.Instance.getScaleOfDocView(await curDoc.presentationTargetDoc as Doc);
+        const newScale = DocumentManager.Instance.getScaleOfDocView(await curDoc.presentationTargetDoc as Doc);
         curDoc.viewScale = newScale;
         //saving the scale that user was on
         if (curScale !== 1) {
@@ -238,7 +238,7 @@ export class PresBox extends React.Component<FieldViewProps> {
     public removeDocument = (doc: Doc) => {
         const value = FieldValue(Cast(this.props.Document[this.props.fieldKey], listSpec(Doc)));
         if (value) {
-            let indexOfDoc = value.indexOf(doc);
+            const indexOfDoc = value.indexOf(doc);
             if (indexOfDoc !== - 1) {
                 value.splice(indexOfDoc, 1)[0];
                 return true;
@@ -337,13 +337,13 @@ export class PresBox extends React.Component<FieldViewProps> {
     @action
     initializeScaleViews = (docList: Doc[], viewtype: number) => {
         this.props.Document.chromeStatus = "disabled";
-        let hgt = (viewtype === CollectionViewType.Tree) ? 50 : 72;
+        const hgt = (viewtype === CollectionViewType.Tree) ? 50 : 72;
         docList.forEach((doc: Doc) => {
             doc.presBox = this.props.Document;
             doc.presBoxKey = this.props.fieldKey;
             doc.collapsedHeight = hgt;
             doc.height = ComputedField.MakeFunction("this.collapsedHeight + Number(this.embedOpen ? 100:0)");
-            let curScale = NumCast(doc.viewScale, null);
+            const curScale = NumCast(doc.viewScale, null);
             if (curScale === undefined) {
                 doc.viewScale = 1;
             }
@@ -352,7 +352,7 @@ export class PresBox extends React.Component<FieldViewProps> {
 
 
     selectElement = (doc: Doc) => {
-        let index = DocListCast(this.props.Document[this.props.fieldKey]).indexOf(doc);
+        const index = DocListCast(this.props.Document[this.props.fieldKey]).indexOf(doc);
         index !== -1 && this.gotoDocument(index, NumCast(this.props.Document.selectedDoc));
     }
 
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 741dcada0..376d27380 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -22,7 +22,7 @@ import { InkingControl } from "../InkingControl";
 import { FieldView, FieldViewProps } from './FieldView';
 import "./VideoBox.scss";
 import { documentSchema, positionSchema } from "../../../new_fields/documentSchemas";
-var path = require('path');
+const path = require('path');
 
 export const timeSchema = createSchema({
     currentTimecode: "number",  // the current time of a video or other linear, time-based document.  Note, should really get set on an extension field, but that's more complicated when it needs to be set since the extension doc needs to be found first
@@ -54,9 +54,9 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
     }
 
     videoLoad = () => {
-        let aspect = this.player!.videoWidth / this.player!.videoHeight;
-        var nativeWidth = (this.Document.nativeWidth || 0);
-        var nativeHeight = (this.Document.nativeHeight || 0);
+        const aspect = this.player!.videoWidth / this.player!.videoHeight;
+        const nativeWidth = (this.Document.nativeWidth || 0);
+        const nativeHeight = (this.Document.nativeHeight || 0);
         if (!nativeWidth || !nativeHeight) {
             if (!this.Document.nativeWidth) this.Document.nativeWidth = this.player!.videoWidth;
             this.Document.nativeHeight = (this.Document.nativeWidth || 0) / aspect;
@@ -101,12 +101,12 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
     }
 
     @action public Snapshot() {
-        let width = this.Document.width || 0;
-        let height = this.Document.height || 0;
-        var canvas = document.createElement('canvas');
+        const width = this.Document.width || 0;
+        const height = this.Document.height || 0;
+        const canvas = document.createElement('canvas');
         canvas.width = 640;
         canvas.height = 640 * (this.Document.nativeHeight || 0) / (this.Document.nativeWidth || 1);
-        var ctx = canvas.getContext('2d');//draw image to canvas. scale to target dimensions
+        const ctx = canvas.getContext('2d');//draw image to canvas. scale to target dimensions
         if (ctx) {
             ctx.rect(0, 0, canvas.width, canvas.height);
             ctx.fillStyle = "blue";
@@ -115,20 +115,20 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
         }
 
         if (!this._videoRef) { // can't find a way to take snapshots of videos
-            let b = Docs.Create.ButtonDocument({
+            const b = Docs.Create.ButtonDocument({
                 x: (this.Document.x || 0) + width, y: (this.Document.y || 0),
                 width: 150, height: 50, title: (this.Document.currentTimecode || 0).toString()
             });
             b.onClick = ScriptField.MakeScript(`this.currentTimecode = ${(this.Document.currentTimecode || 0)}`);
         } else {
             //convert to desired file format
-            var dataUrl = canvas.toDataURL('image/png'); // can also use 'image/png'
+            const dataUrl = canvas.toDataURL('image/png'); // can also use 'image/png'
             // if you want to preview the captured image,
-            let filename = path.basename(encodeURIComponent("snapshot" + StrCast(this.Document.title).replace(/\..*$/, "") + "_" + (this.Document.currentTimecode || 0).toString().replace(/\./, "_")));
+            const filename = path.basename(encodeURIComponent("snapshot" + StrCast(this.Document.title).replace(/\..*$/, "") + "_" + (this.Document.currentTimecode || 0).toString().replace(/\./, "_")));
             VideoBox.convertDataUri(dataUrl, filename).then(returnedFilename => {
                 if (returnedFilename) {
-                    let url = this.choosePath(Utils.prepend(returnedFilename));
-                    let imageSummary = Docs.Create.ImageDocument(url, {
+                    const url = this.choosePath(Utils.prepend(returnedFilename));
+                    const imageSummary = Docs.Create.ImageDocument(url, {
                         x: (this.Document.x || 0) + width, y: (this.Document.y || 0),
                         width: 150, height: height / width * 150, title: "--snapshot" + (this.Document.currentTimecode || 0) + " image-"
                     });
@@ -150,9 +150,9 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
         if (this.props.setVideoBox) this.props.setVideoBox(this);
 
         if (this.youtubeVideoId) {
-            let youtubeaspect = 400 / 315;
-            var nativeWidth = (this.Document.nativeWidth || 0);
-            var nativeHeight = (this.Document.nativeHeight || 0);
+            const youtubeaspect = 400 / 315;
+            const nativeWidth = (this.Document.nativeWidth || 0);
+            const nativeHeight = (this.Document.nativeHeight || 0);
             if (!nativeWidth || !nativeHeight) {
                 if (!this.Document.nativeWidth) this.Document.nativeWidth = 600;
                 this.Document.nativeHeight = (this.Document.nativeWidth || 0) / youtubeaspect;
@@ -181,7 +181,7 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
 
     public static async convertDataUri(imageUri: string, returnedFilename: string) {
         try {
-            let posting = Utils.prepend("/uploadURI");
+            const posting = Utils.prepend("/uploadURI");
             const returnedUri = await rp.post(posting, {
                 body: {
                     uri: imageUri,
@@ -196,10 +196,10 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
         }
     }
     specificContextMenu = (e: React.MouseEvent): void => {
-        let field = Cast(this.dataDoc[this.props.fieldKey], VideoField);
+        const field = Cast(this.dataDoc[this.props.fieldKey], VideoField);
         if (field) {
-            let url = field.url.href;
-            let subitems: ContextMenuProps[] = [];
+            const url = field.url.href;
+            const subitems: ContextMenuProps[] = [];
             subitems.push({ description: "Copy path", event: () => { Utils.CopyText(url); }, icon: "expand-arrows-alt" });
             subitems.push({ description: "Toggle Show Controls", event: action(() => VideoBox._showControls = !VideoBox._showControls), icon: "expand-arrows-alt" });
             subitems.push({ description: "Take Snapshot", event: () => this.Snapshot(), icon: "expand-arrows-alt" });
@@ -208,9 +208,9 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
     }
 
     @computed get content() {
-        let field = Cast(this.dataDoc[this.props.fieldKey], VideoField);
-        let interactive = InkingControl.Instance.selectedTool || !this.props.isSelected() ? "" : "-interactive";
-        let style = "videoBox-content" + (this._fullScreen ? "-fullScreen" : "") + interactive;
+        const field = Cast(this.dataDoc[this.props.fieldKey], VideoField);
+        const interactive = InkingControl.Instance.selectedTool || !this.props.isSelected() ? "" : "-interactive";
+        const style = "videoBox-content" + (this._fullScreen ? "-fullScreen" : "") + interactive;
         return !field ? <div>Loading</div> :
             <video className={`${style}`} key="video" ref={this.setVideoRef} onCanPlay={this.videoLoad} controls={VideoBox._showControls}
                 onPlay={() => this.Play()} onSeeked={this.updateTimecode} onPause={() => this.Pause()} onClick={e => e.preventDefault()}>
@@ -220,7 +220,7 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
     }
 
     @computed get youtubeVideoId() {
-        let field = Cast(this.dataDoc[this.props.fieldKey], VideoField);
+        const field = Cast(this.dataDoc[this.props.fieldKey], VideoField);
         return field && field.url.href.indexOf("youtube") !== -1 ? ((arr: string[]) => arr[arr.length - 1])(field.url.href.split("/")) : "";
     }
 
@@ -231,9 +231,9 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
         }
         else this._youtubeContentCreated = false;
 
-        let iframe = e.target;
+        const iframe = e.target;
         let started = true;
-        let onYoutubePlayerStateChange = (event: any) => runInAction(() => {
+        const onYoutubePlayerStateChange = (event: any) => runInAction(() => {
             if (started && event.data === YT.PlayerState.PLAYING) {
                 started = false;
                 this._youtubePlayer && this._youtubePlayer.unMute();
@@ -243,12 +243,12 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
             if (event.data === YT.PlayerState.PLAYING && !this._playing) this.Play(false);
             if (event.data === YT.PlayerState.PAUSED && this._playing) this.Pause(false);
         });
-        let onYoutubePlayerReady = (event: any) => {
+        const onYoutubePlayerReady = (event: any) => {
             this._reactionDisposer && this._reactionDisposer();
             this._youtubeReactionDisposer && this._youtubeReactionDisposer();
             this._reactionDisposer = reaction(() => this.Document.currentTimecode, () => !this._playing && this.Seek(this.Document.currentTimecode || 0));
             this._youtubeReactionDisposer = reaction(() => [this.props.isSelected(), DocumentDecorations.Instance.Interacting, InkingControl.Instance.selectedTool], () => {
-                let interactive = InkingControl.Instance.selectedTool === InkTool.None && this.props.isSelected(true) && !DocumentDecorations.Instance.Interacting;
+                const interactive = InkingControl.Instance.selectedTool === InkTool.None && this.props.isSelected(true) && !DocumentDecorations.Instance.Interacting;
                 iframe.style.pointerEvents = interactive ? "all" : "none";
             }, { fireImmediately: true });
         };
@@ -261,8 +261,8 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
 
     }
     private get uIButtons() {
-        let scaling = Math.min(1.8, this.props.ScreenToLocalTransform().Scale);
-        let curTime = (this.Document.currentTimecode || 0);
+        const scaling = Math.min(1.8, this.props.ScreenToLocalTransform().Scale);
+        const curTime = (this.Document.currentTimecode || 0);
         return ([<div className="videoBox-time" key="time" onPointerDown={this.onResetDown} >
             <span>{"" + Math.round(curTime)}</span>
             <span style={{ fontSize: 8 }}>{" " + Math.round((curTime - Math.trunc(curTime)) * 100)}</span>
@@ -318,8 +318,8 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
     @computed get youtubeContent() {
         this._youtubeIframeId = VideoBox._youtubeIframeCounter++;
         this._youtubeContentCreated = this._forceCreateYouTubeIFrame ? true : true;
-        let style = "videoBox-content-YouTube" + (this._fullScreen ? "-fullScreen" : "");
-        let start = untracked(() => Math.round(this.Document.currentTimecode || 0));
+        const style = "videoBox-content-YouTube" + (this._fullScreen ? "-fullScreen" : "");
+        const start = untracked(() => Math.round(this.Document.currentTimecode || 0));
         return <iframe key={this._youtubeIframeId} id={`${this.youtubeVideoId + this._youtubeIframeId}-player`}
             onLoad={this.youtubeIframeLoaded} className={`${style}`} width={(this.Document.nativeWidth || 640)} height={(this.Document.nativeHeight || 390)}
             src={`https://www.youtube.com/embed/${this.youtubeVideoId}?enablejsapi=1&rel=0&showinfo=1&autoplay=1&mute=1&start=${start}&modestbranding=1&controls=${VideoBox._showControls ? 1 : 0}`} />;
@@ -327,7 +327,7 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
 
     @action.bound
     addDocumentWithTimestamp(doc: Doc): boolean {
-        var curTime = (this.Document.currentTimecode || -1);
+        const curTime = (this.Document.currentTimecode || -1);
         curTime !== -1 && (doc.displayTimecode = curTime);
         return this.addDocument(doc);
     }
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index 5af743859..b0a93224d 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -36,11 +36,11 @@ export class WebBox extends DocAnnotatableComponent<FieldViewProps, WebDocument>
 
     componentWillMount() {
 
-        let field = Cast(this.props.Document[this.props.fieldKey], WebField);
+        const field = Cast(this.props.Document[this.props.fieldKey], WebField);
         if (field && field.url.href.indexOf("youtube") !== -1) {
-            let youtubeaspect = 400 / 315;
-            var nativeWidth = NumCast(this.layoutDoc.nativeWidth);
-            var nativeHeight = NumCast(this.layoutDoc.nativeHeight);
+            const youtubeaspect = 400 / 315;
+            const nativeWidth = NumCast(this.layoutDoc.nativeWidth);
+            const nativeHeight = NumCast(this.layoutDoc.nativeHeight);
             if (!nativeWidth || !nativeHeight || Math.abs(nativeWidth / nativeHeight - youtubeaspect) > 0.05) {
                 if (!nativeWidth) this.layoutDoc.nativeWidth = 600;
                 this.layoutDoc.nativeHeight = NumCast(this.layoutDoc.nativeWidth) / youtubeaspect;
@@ -65,7 +65,7 @@ export class WebBox extends DocAnnotatableComponent<FieldViewProps, WebDocument>
 
     @action
     setURL() {
-        let urlField: FieldResult<WebField> = Cast(this.props.Document.data, WebField);
+        const urlField: FieldResult<WebField> = Cast(this.props.Document.data, WebField);
         if (urlField) this.url = urlField.url.toString();
         else this.url = "";
     }
@@ -80,10 +80,10 @@ export class WebBox extends DocAnnotatableComponent<FieldViewProps, WebDocument>
 
     switchToText = () => {
         let url: string = "";
-        let field = Cast(this.props.Document[this.props.fieldKey], WebField);
+        const field = Cast(this.props.Document[this.props.fieldKey], WebField);
         if (field) url = field.url.href;
 
-        let newBox = Docs.Create.TextDocument({
+        const newBox = Docs.Create.TextDocument({
             x: NumCast(this.props.Document.x),
             y: NumCast(this.props.Document.y),
             title: url,
@@ -167,7 +167,7 @@ export class WebBox extends DocAnnotatableComponent<FieldViewProps, WebDocument>
 
     @computed
     get content() {
-        let field = this.dataDoc[this.props.fieldKey];
+        const field = this.dataDoc[this.props.fieldKey];
         let view;
         if (field instanceof HtmlField) {
             view = <span id="webBox-htmlSpan" dangerouslySetInnerHTML={{ __html: field.html }} />;
@@ -176,15 +176,15 @@ export class WebBox extends DocAnnotatableComponent<FieldViewProps, WebDocument>
         } else {
             view = <iframe src={"https://crossorigin.me/https://cs.brown.edu"} style={{ position: "absolute", width: "100%", height: "100%", top: 0 }} />;
         }
-        let content =
+        const content =
             <div style={{ width: "100%", height: "100%", position: "absolute" }} onWheel={this.onPostWheel} onPointerDown={this.onPostPointer} onPointerMove={this.onPostPointer} onPointerUp={this.onPostPointer}>
                 {this.urlEditor()}
                 {view}
             </div>;
 
-        let frozen = !this.props.isSelected() || DocumentDecorations.Instance.Interacting;
+        const frozen = !this.props.isSelected() || DocumentDecorations.Instance.Interacting;
 
-        let classname = "webBox-cont" + (this.props.isSelected() && InkingControl.Instance.selectedTool === InkTool.None && !DocumentDecorations.Instance.Interacting ? "-interactive" : "");
+        const classname = "webBox-cont" + (this.props.isSelected() && InkingControl.Instance.selectedTool === InkTool.None && !DocumentDecorations.Instance.Interacting ? "-interactive" : "");
         return (
             <>
                 <div className={classname}  >
diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx
index 936af9ab8..6599c0e3c 100644
--- a/src/client/views/pdf/Annotation.tsx
+++ b/src/client/views/pdf/Annotation.tsx
@@ -62,11 +62,11 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> {
     }
 
     deleteAnnotation = () => {
-        let annotation = DocListCast(this.props.extensionDoc.annotations);
-        let group = FieldValue(Cast(this.props.document.group, Doc));
+        const annotation = DocListCast(this.props.extensionDoc.annotations);
+        const group = FieldValue(Cast(this.props.document.group, Doc));
         if (group) {
             if (annotation.indexOf(group) !== -1) {
-                let newAnnotations = annotation.filter(a => a !== FieldValue(Cast(this.props.document.group, Doc)));
+                const newAnnotations = annotation.filter(a => a !== FieldValue(Cast(this.props.document.group, Doc)));
                 this.props.extensionDoc.annotations = new List<Doc>(newAnnotations);
             }
 
@@ -77,7 +77,7 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> {
     }
 
     pinToPres = () => {
-        let group = FieldValue(Cast(this.props.document.group, Doc));
+        const group = FieldValue(Cast(this.props.document.group, Doc));
         group && this.props.pinToPres(group);
     }
 
@@ -93,7 +93,7 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> {
             e.stopPropagation();
         }
         else if (e.button === 0) {
-            let annoGroup = await Cast(this.props.document.group, Doc);
+            const annoGroup = await Cast(this.props.document.group, Doc);
             if (annoGroup) {
                 DocumentManager.Instance.FollowLink(undefined, annoGroup,
                     (doc: Doc, maxLocation: string) => this.props.addDocTab(doc, undefined, e.ctrlKey ? "inTab" : "onRight"),
@@ -105,9 +105,9 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> {
 
 
     addTag = (key: string, value: string): boolean => {
-        let group = FieldValue(Cast(this.props.document.group, Doc));
+        const group = FieldValue(Cast(this.props.document.group, Doc));
         if (group) {
-            let valNum = parseInt(value);
+            const valNum = parseInt(value);
             group[key] = isNaN(valNum) ? value : valNum;
             return true;
         }
diff --git a/src/client/views/pdf/PDFMenu.tsx b/src/client/views/pdf/PDFMenu.tsx
index c64741769..38e10e99a 100644
--- a/src/client/views/pdf/PDFMenu.tsx
+++ b/src/client/views/pdf/PDFMenu.tsx
@@ -128,7 +128,7 @@ export default class PDFMenu extends AntimodeMenu {
     }
 
     render() {
-        let buttons = this.Status === "pdf" || this.Status === "snippet" ?
+        const buttons = this.Status === "pdf" || this.Status === "snippet" ?
             [
                 <button key="1" className="antimodeMenu-button" title="Click to Highlight" onClick={this.highlightClicked} style={this.Highlighting ? { backgroundColor: "#121212" } : {}}>
                     <FontAwesomeIcon icon="highlighter" size="lg" style={{ transition: "transform 0.1s", transform: this.Highlighting ? "" : "rotate(-45deg)" }} /></button>,
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index c075a4f99..69aacc902 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -169,7 +169,7 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
 
     copy = (e: ClipboardEvent) => {
         if (this.props.active(true) && e.clipboardData) {
-            let annoDoc = this.makeAnnotationDocument("rgba(3,144,152,0.3)");  // copied text markup color (blueish)
+            const annoDoc = this.makeAnnotationDocument("rgba(3,144,152,0.3)");  // copied text markup color (blueish)
             if (annoDoc) {
                 e.clipboardData.setData("text/plain", this._selectionText);
                 e.clipboardData.setData("dash/pdfOrigin", this.props.Document[Id]);
@@ -212,7 +212,7 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
         this._filterReactionDisposer = reaction(
             () => ({ scriptField: Cast(this.Document.filterScript, ScriptField), annos: this._annotations.slice() }),
             action(({ scriptField, annos }: { scriptField: FieldResult<ScriptField>, annos: Doc[] }) => {
-                let oldScript = this._script.originalScript;
+                const oldScript = this._script.originalScript;
                 this._script = scriptField && scriptField.script.compiled ? scriptField.script : CompileScript("return true") as CompiledScript;
                 if (this._script.originalScript !== oldScript) {
                     this.Index = -1;
@@ -240,8 +240,8 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
             this.gotoPage(this.Document.curPage || 1);
         }));
         document.addEventListener("pagerendered", action(() => this._showCover = this._showWaiting = false));
-        var pdfLinkService = new PDFJSViewer.PDFLinkService();
-        let pdfFindController = new PDFJSViewer.PDFFindController({ linkService: pdfLinkService });
+        const pdfLinkService = new PDFJSViewer.PDFLinkService();
+        const pdfFindController = new PDFJSViewer.PDFFindController({ linkService: pdfLinkService });
         this._pdfViewer = new PDFJSViewer.PDFViewer({
             container: this._mainCont.current,
             viewer: this._viewer.current,
@@ -260,12 +260,12 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
         if (this._savedAnnotations.size() === 0) return undefined;
         let mainAnnoDoc = Docs.Create.InstanceFromProto(new Doc(), "", {});
         let mainAnnoDocProto = Doc.GetProto(mainAnnoDoc);
-        let annoDocs: Doc[] = [];
+        const annoDocs: Doc[] = [];
         let maxX = -Number.MAX_VALUE;
         let minY = Number.MAX_VALUE;
         if ((this._savedAnnotations.values()[0][0] as any).marqueeing) {
-            let anno = this._savedAnnotations.values()[0][0];
-            let annoDoc = Docs.Create.FreeformDocument([], { backgroundColor: color, title: "Annotation on " + this.Document.title });
+            const anno = this._savedAnnotations.values()[0][0];
+            const annoDoc = Docs.Create.FreeformDocument([], { backgroundColor: color, title: "Annotation on " + this.Document.title });
             if (anno.style.left) annoDoc.x = parseInt(anno.style.left);
             if (anno.style.top) annoDoc.y = parseInt(anno.style.top);
             if (anno.style.height) annoDoc.height = parseInt(anno.style.height);
@@ -280,7 +280,7 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
             mainAnnoDocProto.y = annoDoc.y;
         } else {
             this._savedAnnotations.forEach((key: number, value: HTMLDivElement[]) => value.map(anno => {
-                let annoDoc = new Doc();
+                const annoDoc = new Doc();
                 if (anno.style.left) annoDoc.x = parseInt(anno.style.left);
                 if (anno.style.top) annoDoc.y = parseInt(anno.style.top);
                 if (anno.style.height) annoDoc.height = parseInt(anno.style.height);
@@ -324,7 +324,7 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
     @action
     scrollToAnnotation = (scrollToAnnotation: Doc) => {
         if (scrollToAnnotation) {
-            let offset = this.visibleHeight() / 2 * 96 / 72;
+            const offset = this.visibleHeight() / 2 * 96 / 72;
             this._mainCont.current && smoothScroll(500, this._mainCont.current, NumCast(scrollToAnnotation.y) - offset);
             Doc.linkFollowHighlight(scrollToAnnotation);
         }
@@ -356,7 +356,7 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
             this._annotationLayer.current.append(div);
             div.style.backgroundColor = "yellow";
             div.style.opacity = "0.5";
-            let savedPage = this._savedAnnotations.getValue(page);
+            const savedPage = this._savedAnnotations.getValue(page);
             if (savedPage) {
                 savedPage.push(div);
                 this._savedAnnotations.setValue(page, savedPage);
@@ -382,7 +382,7 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
             });
         }
         else if (this._mainCont.current) {
-            let executeFind = () => {
+            const executeFind = () => {
                 this._pdfViewer.findController.executeCommand('find', {
                     caseSensitive: false,
                     findPrevious: !fwd,
@@ -398,7 +398,7 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
 
     @action
     onPointerDown = (e: React.PointerEvent): void => {
-        let hit = document.elementFromPoint(e.clientX, e.clientY);
+        const hit = document.elementFromPoint(e.clientX, e.clientY);
         if (hit && hit.localName === "span" && this.props.isSelected(true)) {  // drag selecting text stops propagation
             e.button === 0 && e.stopPropagation();
         }
@@ -425,7 +425,7 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
             }
             else if (this._mainCont.current) {
                 // set marquee x and y positions to the spatially transformed position
-                let boundingRect = this._mainCont.current.getBoundingClientRect();
+                const boundingRect = this._mainCont.current.getBoundingClientRect();
                 this._startX = this._marqueeX = (e.clientX - boundingRect.left) * (this._mainCont.current.offsetWidth / boundingRect.width);
                 this._startY = this._marqueeY = (e.clientY - boundingRect.top) * (this._mainCont.current.offsetHeight / boundingRect.height) + this._mainCont.current.scrollTop;
                 this._marqueeHeight = this._marqueeWidth = 0;
@@ -442,7 +442,7 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
     onSelectMove = (e: PointerEvent): void => {
         if (this._marqueeing && this._mainCont.current) {
             // transform positions and find the width and height to set the marquee to
-            let boundingRect = this._mainCont.current.getBoundingClientRect();
+            const boundingRect = this._mainCont.current.getBoundingClientRect();
             this._marqueeWidth = ((e.clientX - boundingRect.left) * (this._mainCont.current.offsetWidth / boundingRect.width)) - this._startX;
             this._marqueeHeight = ((e.clientY - boundingRect.top) * (this._mainCont.current.offsetHeight / boundingRect.height)) - this._startY + this._mainCont.current.scrollTop;
             this._marqueeX = Math.min(this._startX, this._startX + this._marqueeWidth);
@@ -460,16 +460,16 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
     @action
     createTextAnnotation = (sel: Selection, selRange: Range) => {
         if (this._mainCont.current) {
-            let boundingRect = this._mainCont.current.getBoundingClientRect();
-            let clientRects = selRange.getClientRects();
+            const boundingRect = this._mainCont.current.getBoundingClientRect();
+            const clientRects = selRange.getClientRects();
             for (let i = 0; i < clientRects.length; i++) {
-                let rect = clientRects.item(i);
+                const rect = clientRects.item(i);
                 if (rect) {
-                    let scaleY = this._mainCont.current.offsetHeight / boundingRect.height;
-                    let scaleX = this._mainCont.current.offsetWidth / boundingRect.width;
+                    const scaleY = this._mainCont.current.offsetHeight / boundingRect.height;
+                    const scaleX = this._mainCont.current.offsetWidth / boundingRect.width;
                     if (rect.width !== this._mainCont.current.clientWidth &&
                         (i === 0 || !intersectRect(clientRects[i], clientRects[i - 1]))) {
-                        let annoBox = document.createElement("div");
+                        const annoBox = document.createElement("div");
                         annoBox.className = "pdfViewer-annotationBox";
                         // transforms the positions from screen onto the pdf div
                         annoBox.style.top = ((rect.top - boundingRect.top) * scaleY / this._zoomed + this._mainCont.current.scrollTop).toString();
@@ -497,10 +497,10 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
         this._savedAnnotations.clear();
         if (this._marqueeing) {
             if (this._marqueeWidth > 10 || this._marqueeHeight > 10) {
-                let marquees = this._mainCont.current!.getElementsByClassName("pdfViewer-dragAnnotationBox");
+                const marquees = this._mainCont.current!.getElementsByClassName("pdfViewer-dragAnnotationBox");
                 if (marquees && marquees.length) { // copy the marquee and convert it to a permanent annotation. 
-                    let style = (marquees[0] as HTMLDivElement).style;
-                    let copy = document.createElement("div");
+                    const style = (marquees[0] as HTMLDivElement).style;
+                    const copy = document.createElement("div");
                     copy.style.left = style.left;
                     copy.style.top = style.top;
                     copy.style.width = style.width;
@@ -521,9 +521,9 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
             this._marqueeing = false;
         }
         else {
-            let sel = window.getSelection();
+            const sel = window.getSelection();
             if (sel && sel.type === "Range") {
-                let selRange = sel.getRangeAt(0);
+                const selRange = sel.getRangeAt(0);
                 this.createTextAnnotation(sel, selRange);
                 PDFMenu.Instance.jumpTo(e.clientX, e.clientY);
             }
@@ -543,7 +543,7 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
     @action
     highlight = (color: string) => {
         // creates annotation documents for current highlights
-        let annotationDoc = this.makeAnnotationDocument(color);
+        const annotationDoc = this.makeAnnotationDocument(color);
         annotationDoc && this.props.addDocument && this.props.addDocument(annotationDoc);
         return annotationDoc;
     }
@@ -556,10 +556,10 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
     startDrag = (e: PointerEvent, ele: HTMLElement): void => {
         e.preventDefault();
         e.stopPropagation();
-        let targetDoc = Docs.Create.TextDocument({ width: 200, height: 200, title: "Note linked to " + this.props.Document.title });
+        const targetDoc = Docs.Create.TextDocument({ width: 200, height: 200, title: "Note linked to " + this.props.Document.title });
         const annotationDoc = this.highlight("rgba(146, 245, 95, 0.467)"); // yellowish highlight color when dragging out a text selection
         if (annotationDoc) {
-            let dragData = new DragManager.AnnotationDragData(this.props.Document, annotationDoc, targetDoc);
+            const dragData = new DragManager.AnnotationDragData(this.props.Document, annotationDoc, targetDoc);
             DragManager.StartAnnotationDrag([ele], dragData, e.pageX, e.pageY, {
                 handlers: {
                     dragComplete: () => !(dragData as any).linkedToDoc &&
@@ -572,8 +572,8 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
     }
 
     createSnippet = (marquee: { left: number, top: number, width: number, height: number }): void => {
-        let view = Doc.MakeAlias(this.props.Document);
-        let data = Doc.MakeDelegate(Doc.GetProto(this.props.Document));
+        const view = Doc.MakeAlias(this.props.Document);
+        const data = Doc.MakeDelegate(Doc.GetProto(this.props.Document));
         data.title = StrCast(data.title) + "_snippet";
         view.proto = data;
         view.nativeHeight = marquee.height;
@@ -605,8 +605,8 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
                 this.Document.nativeHeight = nativeWidth * this._coverPath.height / this._coverPath.width;
             }).bind(this), 0);
         }
-        let nativeWidth = (this.Document.nativeWidth || 0);
-        let nativeHeight = (this.Document.nativeHeight || 0);
+        const nativeWidth = (this.Document.nativeWidth || 0);
+        const nativeHeight = (this.Document.nativeHeight || 0);
         return <img key={this._coverPath.path} src={this._coverPath.path} onError={action(() => this._coverPath.path = "http://www.cs.brown.edu/~bcz/face.gif")} onLoad={action(() => this._showWaiting = false)}
             style={{ position: "absolute", display: "inline-block", top: 0, left: 0, width: `${nativeWidth}px`, height: `${nativeHeight}px` }} />;
     }
@@ -615,7 +615,7 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
     onZoomWheel = (e: React.WheelEvent) => {
         e.stopPropagation();
         if (e.ctrlKey) {
-            let curScale = Number(this._pdfViewer.currentScaleValue);
+            const curScale = Number(this._pdfViewer.currentScaleValue);
             this._pdfViewer.currentScaleValue = Math.max(1, Math.min(10, curScale - curScale * e.deltaY / 1000));
             this._zoomed = Number(this._pdfViewer.currentScaleValue);
         }
diff --git a/src/client/views/presentationview/PresElementBox.tsx b/src/client/views/presentationview/PresElementBox.tsx
index f50a3a0ef..47f876ac2 100644
--- a/src/client/views/presentationview/PresElementBox.tsx
+++ b/src/client/views/presentationview/PresElementBox.tsx
@@ -161,9 +161,9 @@ export class PresElementBox extends DocComponent<FieldViewProps, PresDocument>(P
             return (null);
         }
 
-        let propDocWidth = NumCast(this.layoutDoc.nativeWidth);
-        let propDocHeight = NumCast(this.layoutDoc.nativeHeight);
-        let scale = () => 175 / NumCast(this.layoutDoc.nativeWidth, 175);
+        const propDocWidth = NumCast(this.layoutDoc.nativeWidth);
+        const propDocHeight = NumCast(this.layoutDoc.nativeHeight);
+        const scale = () => 175 / NumCast(this.layoutDoc.nativeWidth, 175);
         return (
             <div className="presElementBox-embedded" style={{
                 height: propDocHeight === 0 ? NumCast(this.layoutDoc.height) - NumCast(this.layoutDoc.collapsedHeight) : propDocHeight * scale(),
@@ -193,9 +193,9 @@ export class PresElementBox extends DocComponent<FieldViewProps, PresDocument>(P
     }
 
     render() {
-        let treecontainer = this.props.ContainingCollectionDoc && this.props.ContainingCollectionDoc.viewType === CollectionViewType.Tree;
-        let className = "presElementBox-item" + (this.currentIndex === this.indexInPres ? " presElementBox-selected" : "");
-        let pbi = "presElementBox-interaction";
+        const treecontainer = this.props.ContainingCollectionDoc && this.props.ContainingCollectionDoc.viewType === CollectionViewType.Tree;
+        const className = "presElementBox-item" + (this.currentIndex === this.indexInPres ? " presElementBox-selected" : "");
+        const pbi = "presElementBox-interaction";
         return (
             <div className={className} key={this.props.Document[Id] + this.indexInPres}
                 style={{ outlineWidth: Doc.IsBrushed(this.targetDoc) ? `1px` : "0px", }}
diff --git a/src/client/views/search/FilterBox.tsx b/src/client/views/search/FilterBox.tsx
index 7812c9d97..12e1bc265 100644
--- a/src/client/views/search/FilterBox.tsx
+++ b/src/client/views/search/FilterBox.tsx
@@ -79,7 +79,7 @@ export class FilterBox extends React.Component {
                 acc[i].addEventListener("click", function (this: HTMLElement) {
                     this.classList.toggle("active");
 
-                    var panel = this.nextElementSibling as HTMLElement;
+                    const panel = this.nextElementSibling as HTMLElement;
                     if (panel.style.maxHeight) {
                         panel.style.overflow = "hidden";
                         panel.style.maxHeight = "";
@@ -96,7 +96,7 @@ export class FilterBox extends React.Component {
                     }
                 });
 
-                let el = acc[i] as HTMLElement;
+                const el = acc[i] as HTMLElement;
                 el.click();
             }
         });
@@ -105,14 +105,14 @@ export class FilterBox extends React.Component {
     @action.bound
     minimizeAll() {
         $('document').ready(function () {
-            var acc = document.getElementsByClassName('filter-header');
+            const acc = document.getElementsByClassName('filter-header');
 
             // tslint:disable-next-line: prefer-for-of
             for (var i = 0; i < acc.length; i++) {
-                let classList = acc[i].classList;
+                const classList = acc[i].classList;
                 if (classList.contains("active")) {
                     acc[i].classList.toggle("active");
-                    var panel = acc[i].nextElementSibling as HTMLElement;
+                    const panel = acc[i].nextElementSibling as HTMLElement;
                     panel.style.overflow = "hidden";
                     panel.style.maxHeight = "";
                 }
@@ -128,10 +128,10 @@ export class FilterBox extends React.Component {
     }
 
     basicRequireWords(query: string): string {
-        let oldWords = query.split(" ");
-        let newWords: string[] = [];
+        const oldWords = query.split(" ");
+        const newWords: string[] = [];
         oldWords.forEach(word => {
-            let newWrd = "+" + word;
+            const newWrd = "+" + word;
             newWords.push(newWrd);
         });
         query = newWords.join(" ");
@@ -140,7 +140,7 @@ export class FilterBox extends React.Component {
     }
 
     basicFieldFilters(query: string, type: string): string {
-        let oldWords = query.split(" ");
+        const oldWords = query.split(" ");
         let mod = "";
 
         if (type === Keys.AUTHOR) {
@@ -151,9 +151,9 @@ export class FilterBox extends React.Component {
             mod = " title_t:";
         }
 
-        let newWords: string[] = [];
+        const newWords: string[] = [];
         oldWords.forEach(word => {
-            let newWrd = mod + word;
+            const newWrd = mod + word;
             newWords.push(newWrd);
         });
 
@@ -183,11 +183,11 @@ export class FilterBox extends React.Component {
     //gets all of the collections of all the docviews that are selected
     //if a collection is the only thing selected, search only in that collection (not its container)
     getCurCollections(): Doc[] {
-        let selectedDocs: DocumentView[] = SelectionManager.SelectedDocuments();
-        let collections: Doc[] = [];
+        const selectedDocs: DocumentView[] = SelectionManager.SelectedDocuments();
+        const collections: Doc[] = [];
 
         selectedDocs.forEach(async element => {
-            let layout: string = StrCast(element.props.Document.layout);
+            const layout: string = StrCast(element.props.Document.layout);
             //checks if selected view (element) is a collection. if it is, adds to list to search through
             if (layout.indexOf("Collection") > -1) {
                 //makes sure collections aren't added more than once
@@ -229,14 +229,14 @@ export class FilterBox extends React.Component {
     }
 
     addCollectionFilter(query: string): string {
-        let collections: Doc[] = this.getCurCollections();
-        let oldWords = query.split(" ");
+        const collections: Doc[] = this.getCurCollections();
+        const oldWords = query.split(" ");
 
-        let collectionString: string[] = [];
+        const collectionString: string[] = [];
         collections.forEach(doc => {
-            let proto = doc.proto;
-            let protoId = (proto || doc)[Id];
-            let colString: string = "{!join from=data_l to=id}id:" + protoId + " ";
+            const proto = doc.proto;
+            const protoId = (proto || doc)[Id];
+            const colString: string = "{!join from=data_l to=id}id:" + protoId + " ";
             collectionString.push(colString);
         });
 
@@ -254,9 +254,9 @@ export class FilterBox extends React.Component {
         if (this._icons.length === 9) {
             return docs;
         }
-        let finalDocs: Doc[] = [];
+        const finalDocs: Doc[] = [];
         docs.forEach(doc => {
-            let layoutresult = Cast(doc.type, "string");
+            const layoutresult = Cast(doc.type, "string");
             if (layoutresult && this._icons.includes(layoutresult)) {
                 finalDocs.push(doc);
             }
diff --git a/src/client/views/search/IconButton.tsx b/src/client/views/search/IconButton.tsx
index d2cfe7fad..f01508141 100644
--- a/src/client/views/search/IconButton.tsx
+++ b/src/client/views/search/IconButton.tsx
@@ -108,7 +108,7 @@ export class IconButton extends React.Component<IconButtonProps>{
 
     @action.bound
     onClick = () => {
-        let newList: string[] = FilterBox.Instance.getIcons();
+        const newList: string[] = FilterBox.Instance.getIcons();
 
         if (!this._isSelected) {
             this._isSelected = true;
diff --git a/src/client/views/search/NaviconButton.tsx b/src/client/views/search/NaviconButton.tsx
index 3fa36b163..0fa4a0fca 100644
--- a/src/client/views/search/NaviconButton.tsx
+++ b/src/client/views/search/NaviconButton.tsx
@@ -4,7 +4,7 @@ import "./NaviconButton.scss";
 import * as $ from 'jquery';
 import { observable } from 'mobx';
 
-export interface NaviconProps{
+export interface NaviconProps {
     onClick(): void;
 }
 
@@ -13,19 +13,21 @@ export class NaviconButton extends React.Component<NaviconProps> {
     @observable private _ref: React.RefObject<HTMLAnchorElement> = React.createRef();
 
     componentDidMount = () => {
-        let that = this;
-        if(this._ref.current){this._ref.current.addEventListener("click", function(e) {
-            e.preventDefault();
-            if(that._ref.current){
-                that._ref.current.classList.toggle('active');
-                return false;
-            }
-        });}
+        const that = this;
+        if (this._ref.current) {
+            this._ref.current.addEventListener("click", function (e) {
+                e.preventDefault();
+                if (that._ref.current) {
+                    that._ref.current.classList.toggle('active');
+                    return false;
+                }
+            });
+        }
     }
 
     render() {
         return (
-            <a id="hamburger-icon" href="#" ref = {this._ref} title="Menu">
+            <a id="hamburger-icon" href="#" ref={this._ref} title="Menu">
                 <span className="line line-1"></span>
                 <span className="line line-2"></span>
                 <span className="line line-3"></span>
diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx
index ff35542ed..c45fbe210 100644
--- a/src/client/views/search/SearchBox.tsx
+++ b/src/client/views/search/SearchBox.tsx
@@ -89,7 +89,7 @@ export class SearchBox extends React.Component {
 
     public static async convertDataUri(imageUri: string, returnedFilename: string) {
         try {
-            let posting = Utils.prepend("/uploadURI");
+            const posting = Utils.prepend("/uploadURI");
             const returnedUri = await rp.post(posting, {
                 body: {
                     uri: imageUri,
@@ -166,7 +166,7 @@ export class SearchBox extends React.Component {
                     const docs = await Promise.all(res.docs.map(async doc => (await Cast(doc.extendsDoc, Doc)) || doc));
                     const highlights: typeof res.highlighting = {};
                     docs.forEach((doc, index) => highlights[doc[Id]] = highlightList[index]);
-                    let filteredDocs = FilterBox.Instance.filterDocsByType(docs);
+                    const filteredDocs = FilterBox.Instance.filterDocsByType(docs);
                     runInAction(() => {
                         // this._results.push(...filteredDocs);
                         filteredDocs.forEach(doc => {
@@ -198,8 +198,8 @@ export class SearchBox extends React.Component {
 
     collectionRef = React.createRef<HTMLSpanElement>();
     startDragCollection = async () => {
-        let res = await this.getAllResults(FilterBox.Instance.getFinalQuery(this._searchString));
-        let filtered = FilterBox.Instance.filterDocsByType(res.docs);
+        const res = await this.getAllResults(FilterBox.Instance.getFinalQuery(this._searchString));
+        const filtered = FilterBox.Instance.filterDocsByType(res.docs);
         // console.log(this._results)
         const docs = filtered.map(doc => {
             const isProto = Doc.GetT(doc, "isPrototype", "boolean", true);
@@ -266,10 +266,10 @@ export class SearchBox extends React.Component {
 
     @action
     resultsScrolled = (e?: React.UIEvent<HTMLDivElement>) => {
-        let scrollY = e ? e.currentTarget.scrollTop : this.resultsRef.current ? this.resultsRef.current.scrollTop : 0;
-        let itemHght = 53;
-        let startIndex = Math.floor(Math.max(0, scrollY / itemHght));
-        let endIndex = Math.ceil(Math.min(this._numTotalResults - 1, startIndex + (this.resultsRef.current!.getBoundingClientRect().height / itemHght)));
+        const scrollY = e ? e.currentTarget.scrollTop : this.resultsRef.current ? this.resultsRef.current.scrollTop : 0;
+        const itemHght = 53;
+        const startIndex = Math.floor(Math.max(0, scrollY / itemHght));
+        const endIndex = Math.ceil(Math.min(this._numTotalResults - 1, startIndex + (this.resultsRef.current!.getBoundingClientRect().height / itemHght)));
 
         this._endIndex = endIndex === -1 ? 12 : endIndex;
 
@@ -307,7 +307,7 @@ export class SearchBox extends React.Component {
                         this.getResults(this._searchString);
                         if (i < this._results.length) result = this._results[i];
                         if (result) {
-                            let highlights = Array.from([...Array.from(new Set(result[1]).values())]).filter(v => v !== "search_string");
+                            const highlights = Array.from([...Array.from(new Set(result[1]).values())]).filter(v => v !== "search_string");
                             this._visibleElements[i] = <SearchItem doc={result[0]} query={this._searchString} key={result[0][Id]} lines={result[2]} highlighting={highlights} />;
                             this._isSearch[i] = "search";
                         }
@@ -315,7 +315,7 @@ export class SearchBox extends React.Component {
                     else {
                         result = this._results[i];
                         if (result) {
-                            let highlights = Array.from([...Array.from(new Set(result[1]).values())]).filter(v => v !== "search_string");
+                            const highlights = Array.from([...Array.from(new Set(result[1]).values())]).filter(v => v !== "search_string");
                             this._visibleElements[i] = <SearchItem doc={result[0]} query={this._searchString} key={result[0][Id]} lines={result[2]} highlighting={highlights} />;
                             this._isSearch[i] = "search";
                         }
diff --git a/src/client/views/search/SearchItem.tsx b/src/client/views/search/SearchItem.tsx
index f1d825aa0..6ffff0562 100644
--- a/src/client/views/search/SearchItem.tsx
+++ b/src/client/views/search/SearchItem.tsx
@@ -52,7 +52,7 @@ export class SelectorContextMenu extends React.Component<SearchItemProps> {
     }
 
     async fetchDocuments() {
-        let aliases = (await SearchUtil.GetViewsOfDocument(this.props.doc)).filter(doc => doc !== this.props.doc);
+        const aliases = (await SearchUtil.GetViewsOfDocument(this.props.doc)).filter(doc => doc !== this.props.doc);
         const { docs } = await SearchUtil.Search("", true, { fq: `data_l:"${this.props.doc[Id]}"` });
         const map: Map<Doc, Doc> = new Map;
         const allDocs = await Promise.all(aliases.map(doc => SearchUtil.Search("", true, { fq: `data_l:"${doc[Id]}"` }).then(result => result.docs)));
@@ -82,7 +82,7 @@ export class SelectorContextMenu extends React.Component<SearchItemProps> {
             <div className="parents">
                 <p className="contexts">Contexts:</p>
                 {[...this._docs, ...this._otherDocs].map(doc => {
-                    let item = React.createRef<HTMLDivElement>();
+                    const item = React.createRef<HTMLDivElement>();
                     return <div className="collection" key={doc.col[Id] + doc.target[Id]} ref={item}>
                         <div className="collection-item" onPointerDown={
                             SetupDrag(item, () => doc.col, undefined, undefined, undefined, undefined, () => SearchBox.Instance.closeSearch())}>
@@ -146,11 +146,11 @@ export class SearchItem extends React.Component<SearchItemProps> {
     //@computed
     @action
     public DocumentIcon() {
-        let layoutresult = StrCast(this.props.doc.type);
+        const layoutresult = StrCast(this.props.doc.type);
         if (!this._useIcons) {
-            let returnXDimension = () => this._useIcons ? 50 : Number(SEARCH_THUMBNAIL_SIZE);
-            let returnYDimension = () => this._displayDim;
-            let scale = () => returnXDimension() / NumCast(Doc.Layout(this.props.doc).nativeWidth, returnXDimension());
+            const returnXDimension = () => this._useIcons ? 50 : Number(SEARCH_THUMBNAIL_SIZE);
+            const returnYDimension = () => this._displayDim;
+            const scale = () => returnXDimension() / NumCast(Doc.Layout(this.props.doc).nativeWidth, returnXDimension());
             const docview = <div
                 onPointerDown={action(() => {
                     this._useIcons = !this._useIcons;
@@ -184,7 +184,7 @@ export class SearchItem extends React.Component<SearchItemProps> {
             </div>;
             return docview;
         }
-        let button = layoutresult.indexOf(DocumentType.PDF) !== -1 ? faFilePdf :
+        const button = layoutresult.indexOf(DocumentType.PDF) !== -1 ? faFilePdf :
             layoutresult.indexOf(DocumentType.IMG) !== -1 ? faImage :
                 layoutresult.indexOf(DocumentType.TEXT) !== -1 ? faStickyNote :
                     layoutresult.indexOf(DocumentType.VID) !== -1 ? faFilm :
@@ -201,7 +201,7 @@ export class SearchItem extends React.Component<SearchItemProps> {
 
     collectionRef = React.createRef<HTMLDivElement>();
     startDocDrag = () => {
-        let doc = this.props.doc;
+        const doc = this.props.doc;
         const isProto = Doc.GetT(doc, "isPrototype", "boolean", true);
         if (isProto) {
             return Doc.MakeDelegate(doc);
@@ -218,7 +218,7 @@ export class SearchItem extends React.Component<SearchItemProps> {
 
     nextHighlight = (e: React.PointerEvent) => {
         e.preventDefault(); e.button === 0 && SearchBox.Instance.openSearch(e);
-        let sstring = StrCast(this.props.doc.search_string);
+        const sstring = StrCast(this.props.doc.search_string);
         this.props.doc.search_string = "";
         setTimeout(() => this.props.doc.search_string = sstring, 0);
     }
@@ -226,8 +226,8 @@ export class SearchItem extends React.Component<SearchItemProps> {
         if (this.props.doc.type === DocumentType.LINK) {
             if (this.props.doc.anchor1 && this.props.doc.anchor2) {
 
-                let doc1 = Cast(this.props.doc.anchor1, Doc, null);
-                let doc2 = Cast(this.props.doc.anchor2, Doc, null);
+                const doc1 = Cast(this.props.doc.anchor1, Doc, null);
+                const doc2 = Cast(this.props.doc.anchor2, Doc, null);
                 Doc.BrushDoc(doc1);
                 Doc.BrushDoc(doc2);
             }
@@ -241,8 +241,8 @@ export class SearchItem extends React.Component<SearchItemProps> {
         if (this.props.doc.type === DocumentType.LINK) {
             if (this.props.doc.anchor1 && this.props.doc.anchor2) {
 
-                let doc1 = Cast(this.props.doc.anchor1, Doc, null);
-                let doc2 = Cast(this.props.doc.anchor2, Doc, null);
+                const doc1 = Cast(this.props.doc.anchor1, Doc, null);
+                const doc2 = Cast(this.props.doc.anchor2, Doc, null);
                 Doc.UnBrushDoc(doc1);
                 Doc.UnBrushDoc(doc2);
             }
diff --git a/src/client/views/search/ToggleBar.tsx b/src/client/views/search/ToggleBar.tsx
index ed5ecd3ba..e4d7f2fd5 100644
--- a/src/client/views/search/ToggleBar.tsx
+++ b/src/client/views/search/ToggleBar.tsx
@@ -33,8 +33,7 @@ export class ToggleBar extends React.Component<ToggleBarProps>{
     }
 
     componentDidMount = () => {
-
-        let totalWidth = 265;
+        const totalWidth = 265;
 
         if (this._originalStatus) {
             this._forwardTimeline.add({
diff --git a/src/debug/Viewer.tsx b/src/debug/Viewer.tsx
index 24db3f934..a26d2e06a 100644
--- a/src/debug/Viewer.tsx
+++ b/src/debug/Viewer.tsx
@@ -24,7 +24,7 @@ CursorField;
 function applyToDoc(doc: { [index: string]: FieldResult }, key: string, scriptString: string): boolean;
 function applyToDoc(doc: { [index: number]: FieldResult }, key: number, scriptString: string): boolean;
 function applyToDoc(doc: any, key: string | number, scriptString: string): boolean {
-    let script = CompileScript(scriptString, { addReturn: true, params: { this: doc instanceof Doc ? Doc.name : List.name } });
+    const script = CompileScript(scriptString, { addReturn: true, params: { this: doc instanceof Doc ? Doc.name : List.name } });
     if (!script.compiled) {
         return false;
     }
@@ -85,7 +85,7 @@ class DocumentViewer extends React.Component<{ field: Doc }> {
         let content;
         if (this.expanded) {
             const keys = Object.keys(this.props.field);
-            let fields = keys.map(key => {
+            const fields = keys.map(key => {
                 return (
                     <div key={key}>
                         <b>({key}): </b>
diff --git a/src/mobile/ImageUpload.tsx b/src/mobile/ImageUpload.tsx
index 9fdaac66e..ec698b151 100644
--- a/src/mobile/ImageUpload.tsx
+++ b/src/mobile/ImageUpload.tsx
@@ -34,13 +34,13 @@ class Uploader extends React.Component {
         try {
             this.status = "initializing protos";
             await Docs.Prototypes.initialize();
-            let imgPrev = document.getElementById("img_preview");
+            const imgPrev = document.getElementById("img_preview");
             if (imgPrev) {
-                let files: FileList | null = inputRef.current!.files;
+                const files: FileList | null = inputRef.current!.files;
                 if (files && files.length !== 0) {
                     console.log(files[0]);
                     const name = files[0].name;
-                    let formData = new FormData();
+                    const formData = new FormData();
                     formData.append("file", files[0]);
 
                     const upload = window.location.origin + "/upload";
@@ -52,8 +52,8 @@ class Uploader extends React.Component {
                     this.status = "upload image, getting json";
                     const json = await res.json();
                     json.map(async (file: any) => {
-                        let path = window.location.origin + file;
-                        var doc = Docs.Create.ImageDocument(path, { nativeWidth: 200, width: 200, title: name });
+                        const path = window.location.origin + file;
+                        const doc = Docs.Create.ImageDocument(path, { nativeWidth: 200, width: 200, title: name });
 
                         this.status = "getting user document";
 
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index a811322d7..cf32fa74a 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -21,7 +21,7 @@ export namespace Field {
     export function toKeyValueString(doc: Doc, key: string): string {
         const onDelegate = Object.keys(doc).includes(key);
 
-        let field = ComputedField.WithoutComputed(() => FieldValue(doc[key]));
+        const field = ComputedField.WithoutComputed(() => FieldValue(doc[key]));
         if (Field.IsField(field)) {
             return (onDelegate ? "=" : "") + (field instanceof ComputedField ? `:=${field.script.originalScript}` : Field.toScriptString(field));
         }
@@ -255,9 +255,9 @@ export namespace Doc {
         return GetT(doc, "isPrototype", "boolean", true);
     }
     export async function SetInPlace(doc: Doc, key: string, value: Field | undefined, defaultProto: boolean) {
-        let hasProto = doc.proto instanceof Doc;
-        let onDeleg = Object.getOwnPropertyNames(doc).indexOf(key) !== -1;
-        let onProto = hasProto && Object.getOwnPropertyNames(doc.proto).indexOf(key) !== -1;
+        const hasProto = doc.proto instanceof Doc;
+        const onDeleg = Object.getOwnPropertyNames(doc).indexOf(key) !== -1;
+        const onProto = hasProto && Object.getOwnPropertyNames(doc.proto).indexOf(key) !== -1;
         if (onDeleg || !hasProto || (!onProto && !defaultProto)) {
             doc[key] = value;
         } else doc.proto![key] = value;
@@ -306,10 +306,10 @@ export namespace Doc {
     // compare whether documents or their protos match
     export function AreProtosEqual(doc?: Doc, other?: Doc) {
         if (!doc || !other) return false;
-        let r = (doc === other);
-        let r2 = (Doc.GetProto(doc) === other);
-        let r3 = (Doc.GetProto(other) === doc);
-        let r4 = (Doc.GetProto(doc) === Doc.GetProto(other) && Doc.GetProto(other) !== undefined);
+        const r = (doc === other);
+        const r2 = (Doc.GetProto(doc) === other);
+        const r3 = (Doc.GetProto(other) === doc);
+        const r4 = (Doc.GetProto(doc) === Doc.GetProto(other) && Doc.GetProto(other) !== undefined);
         return r || r2 || r3 || r4;
     }
 
@@ -318,7 +318,7 @@ export namespace Doc {
         return doc && (Doc.GetT(doc, "isPrototype", "boolean", true) ? doc : (doc.proto || doc));
     }
     export function GetDataDoc(doc: Doc): Doc {
-        let proto = Doc.GetProto(doc);
+        const proto = Doc.GetProto(doc);
         return proto === doc ? proto : Doc.GetDataDoc(proto);
     }
 
@@ -343,9 +343,9 @@ export namespace Doc {
         if (listDoc[key] === undefined) {
             Doc.GetProto(listDoc)[key] = new List<Doc>();
         }
-        let list = Cast(listDoc[key], listSpec(Doc));
+        const list = Cast(listDoc[key], listSpec(Doc));
         if (list) {
-            let ind = list.indexOf(doc);
+            const ind = list.indexOf(doc);
             if (ind !== -1) {
                 list.splice(ind, 1);
                 return true;
@@ -357,10 +357,10 @@ export namespace Doc {
         if (listDoc[key] === undefined) {
             Doc.GetProto(listDoc)[key] = new List<Doc>();
         }
-        let list = Cast(listDoc[key], listSpec(Doc));
+        const list = Cast(listDoc[key], listSpec(Doc));
         if (list) {
             if (allowDuplicates !== true) {
-                let pind = list.reduce((l, d, i) => d instanceof Doc && d[Id] === doc[Id] ? i : l, -1);
+                const pind = list.reduce((l, d, i) => d instanceof Doc && d[Id] === doc[Id] ? i : l, -1);
                 if (pind !== -1) {
                     list.splice(pind, 1);
                 }
@@ -369,7 +369,7 @@ export namespace Doc {
                 list.splice(0, 0, doc);
             }
             else {
-                let ind = relativeTo ? list.indexOf(relativeTo) : -1;
+                const ind = relativeTo ? list.indexOf(relativeTo) : -1;
                 if (ind === -1) {
                     if (reversed) list.splice(0, 0, doc);
                     else list.push(doc);
@@ -388,9 +388,9 @@ export namespace Doc {
     // Computes the bounds of the contents of a set of documents.
     //
     export function ComputeContentBounds(docList: Doc[]) {
-        let bounds = docList.reduce((bounds, doc) => {
-            var [sptX, sptY] = [NumCast(doc.x), NumCast(doc.y)];
-            let [bptX, bptY] = [sptX + doc[WidthSym](), sptY + doc[HeightSym]()];
+        const bounds = docList.reduce((bounds, doc) => {
+            const [sptX, sptY] = [NumCast(doc.x), NumCast(doc.y)];
+            const [bptX, bptY] = [sptX + doc[WidthSym](), sptY + doc[HeightSym]()];
             return {
                 x: Math.min(sptX, bounds.x), y: Math.min(sptY, bounds.y),
                 r: Math.max(bptX, bounds.r), b: Math.max(bptY, bounds.b)
@@ -400,16 +400,16 @@ export namespace Doc {
     }
 
     export function MakeTitled(title: string) {
-        let doc = new Doc();
+        const doc = new Doc();
         doc.title = title;
         return doc;
     }
     export function MakeAlias(doc: Doc) {
-        let alias = !GetT(doc, "isPrototype", "boolean", true) ? Doc.MakeCopy(doc) : Doc.MakeDelegate(doc);
+        const alias = !GetT(doc, "isPrototype", "boolean", true) ? Doc.MakeCopy(doc) : Doc.MakeDelegate(doc);
         if (alias.layout instanceof Doc) {
             alias.layout = Doc.MakeAlias(alias.layout);
         }
-        let aliasNumber = Doc.GetProto(doc).aliasNumber = NumCast(Doc.GetProto(doc).aliasNumber) + 1;
+        const aliasNumber = Doc.GetProto(doc).aliasNumber = NumCast(Doc.GetProto(doc).aliasNumber) + 1;
         alias.title = ComputedField.MakeFunction(`renameAlias(this, ${aliasNumber})`);
         return alias;
     }
@@ -437,8 +437,8 @@ export namespace Doc {
         // ... which means we change the layout to be an expanded view of the template layout.  
         // This allows the view override the template's properties and be referenceable as its own document.
 
-        let expandedLayoutFieldKey = "Layout[" + templateLayoutDoc[Id] + "]";
-        let expandedTemplateLayout = dataDoc[expandedLayoutFieldKey];
+        const expandedLayoutFieldKey = "Layout[" + templateLayoutDoc[Id] + "]";
+        const expandedTemplateLayout = dataDoc[expandedLayoutFieldKey];
         if (expandedTemplateLayout instanceof Doc) {
             return expandedTemplateLayout;
         }
@@ -451,9 +451,9 @@ export namespace Doc {
 
     export function GetLayoutDataDocPair(doc: Doc, dataDoc: Doc | undefined, fieldKey: string, childDocLayout: Doc) {
         let layoutDoc: Doc | undefined = childDocLayout;
-        let resolvedDataDoc = !doc.isTemplateField && dataDoc !== doc && dataDoc ? Doc.GetDataDoc(dataDoc) : undefined;
+        const resolvedDataDoc = !doc.isTemplateField && dataDoc !== doc && dataDoc ? Doc.GetDataDoc(dataDoc) : undefined;
         if (resolvedDataDoc && Doc.WillExpandTemplateLayout(childDocLayout, resolvedDataDoc)) {
-            let extensionDoc = fieldExtensionDoc(resolvedDataDoc, StrCast(childDocLayout.templateField, StrCast(childDocLayout.title)));
+            const extensionDoc = fieldExtensionDoc(resolvedDataDoc, StrCast(childDocLayout.templateField, StrCast(childDocLayout.title)));
             layoutDoc = Doc.expandTemplateLayout(childDocLayout, extensionDoc !== resolvedDataDoc ? extensionDoc : undefined);
         } else layoutDoc = childDocLayout;
         return { layout: layoutDoc, data: resolvedDataDoc };
@@ -467,13 +467,13 @@ export namespace Doc {
     // to store annotations, ink, and other data.
     //
     export function fieldExtensionDoc(doc: Doc, fieldKey: string) {
-        let extension = doc[fieldKey + "_ext"] as Doc;
+        const extension = doc[fieldKey + "_ext"] as Doc;
         (extension === undefined) && setTimeout(() => CreateDocumentExtensionForField(doc, fieldKey), 0);
         return extension ? extension : undefined;
     }
 
     export function CreateDocumentExtensionForField(doc: Doc, fieldKey: string) {
-        let docExtensionForField = new Doc(doc[Id] + fieldKey, true);
+        const docExtensionForField = new Doc(doc[Id] + fieldKey, true);
         docExtensionForField.title = fieldKey + ".ext"; // courtesy field--- shouldn't be needed except maybe for debugging
         docExtensionForField.extendsDoc = doc; // this is used by search to map field matches on the extension doc back to the document it extends.
         docExtensionForField.extendsField = fieldKey; // this can be used by search to map matches on the extension doc back to the field that was extended.
@@ -512,7 +512,7 @@ export namespace Doc {
     export function MakeCopy(doc: Doc, copyProto: boolean = false, copyProtoId?: string): Doc {
         const copy = new Doc(copyProtoId, true);
         Object.keys(doc).forEach(key => {
-            let cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key]));
+            const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key]));
             const field = ProxyField.WithoutProxy(() => doc[key]);
             if (key === "proto" && copyProto) {
                 if (doc[key] instanceof Doc) {
@@ -551,7 +551,7 @@ export namespace Doc {
     let _applyCount: number = 0;
     export function ApplyTemplate(templateDoc: Doc) {
         if (templateDoc) {
-            let applied = ApplyTemplateTo(templateDoc, Doc.MakeDelegate(new Doc()), "layoutCustom", templateDoc.title + "(..." + _applyCount++ + ")");
+            const applied = ApplyTemplateTo(templateDoc, Doc.MakeDelegate(new Doc()), "layoutCustom", templateDoc.title + "(..." + _applyCount++ + ")");
             applied && (Doc.GetProto(applied).layout = applied.layout);
             return applied;
         }
@@ -567,7 +567,7 @@ export namespace Doc {
             return;
         }
 
-        let layoutCustomLayout = Doc.MakeDelegate(templateDoc);
+        const layoutCustomLayout = Doc.MakeDelegate(templateDoc);
 
         titleTarget && (Doc.GetProto(target).title = titleTarget);
         Doc.GetProto(target).type = DocumentType.TEMPLATE;
@@ -580,7 +580,7 @@ export namespace Doc {
 
     export function MakeMetadataFieldTemplate(fieldTemplate: Doc, templateDataDoc: Doc, suppressTitle: boolean = false): boolean {
         // move data doc fields to layout doc as needed (nativeWidth/nativeHeight, data, ??)
-        let metadataFieldName = StrCast(fieldTemplate.title).replace(/^-/, "");
+        const metadataFieldName = StrCast(fieldTemplate.title).replace(/^-/, "");
         let fieldLayoutDoc = fieldTemplate;
         if (fieldTemplate.layout instanceof Doc) {
             fieldLayoutDoc = Doc.MakeDelegate(fieldTemplate.layout);
@@ -601,11 +601,11 @@ export namespace Doc {
         fieldTemplate.panY = 0;
         fieldTemplate.scale = 1;
         fieldTemplate.showTitle = suppressTitle ? undefined : "title";
-        let data = fieldTemplate.data;
+        const data = fieldTemplate.data;
         // setTimeout(action(() => {
         !templateDataDoc[metadataFieldName] && data instanceof ObjectField && (Doc.GetProto(templateDataDoc)[metadataFieldName] = ObjectField.MakeCopy(data));
-        let layout = StrCast(fieldLayoutDoc.layout).replace(/fieldKey={"[^"]*"}/, `fieldKey={"${metadataFieldName}"}`);
-        let layoutDelegate = Doc.Layout(fieldTemplate);
+        const layout = StrCast(fieldLayoutDoc.layout).replace(/fieldKey={"[^"]*"}/, `fieldKey={"${metadataFieldName}"}`);
+        const layoutDelegate = Doc.Layout(fieldTemplate);
         layoutDelegate.layout = layout;
         fieldTemplate.layout = layoutDelegate !== fieldTemplate ? layoutDelegate : layout;
         if (fieldTemplate.backgroundColor !== templateDataDoc.defaultBackgroundColor) fieldTemplate.defaultBackgroundColor = fieldTemplate.backgroundColor;
@@ -615,16 +615,16 @@ export namespace Doc {
     }
 
     export function overlapping(doc1: Doc, doc2: Doc, clusterDistance: number) {
-        let doc2Layout = Doc.Layout(doc2);
-        let doc1Layout = Doc.Layout(doc1);
-        var x2 = NumCast(doc2.x) - clusterDistance;
-        var y2 = NumCast(doc2.y) - clusterDistance;
-        var w2 = NumCast(doc2Layout.width) + clusterDistance;
-        var h2 = NumCast(doc2Layout.height) + clusterDistance;
-        var x = NumCast(doc1.x) - clusterDistance;
-        var y = NumCast(doc1.y) - clusterDistance;
-        var w = NumCast(doc1Layout.width) + clusterDistance;
-        var h = NumCast(doc1Layout.height) + clusterDistance;
+        const doc2Layout = Doc.Layout(doc2);
+        const doc1Layout = Doc.Layout(doc1);
+        const x2 = NumCast(doc2.x) - clusterDistance;
+        const y2 = NumCast(doc2.y) - clusterDistance;
+        const w2 = NumCast(doc2Layout.width) + clusterDistance;
+        const h2 = NumCast(doc2Layout.height) + clusterDistance;
+        const x = NumCast(doc1.x) - clusterDistance;
+        const y = NumCast(doc1.y) - clusterDistance;
+        const w = NumCast(doc1Layout.width) + clusterDistance;
+        const h = NumCast(doc1Layout.height) + clusterDistance;
         return doc1.z === doc2.z && intersectRect({ left: x, top: y, width: w, height: h }, { left: x2, top: y2, width: w2, height: h2 });
     }
 
@@ -691,7 +691,7 @@ export namespace Doc {
         Doc.HighlightDoc(destDoc);
         document.removeEventListener("pointerdown", linkFollowUnhighlight);
         document.addEventListener("pointerdown", linkFollowUnhighlight);
-        let x = dt = Date.now();
+        const x = dt = Date.now();
         window.setTimeout(() => dt === x && linkFollowUnhighlight(), 5000);
     }
 
@@ -700,7 +700,7 @@ export namespace Doc {
     }
     const highlightManager = new HighlightBrush();
     export function IsHighlighted(doc: Doc) {
-        let IsHighlighted = highlightManager.HighlightedDoc.get(doc) || highlightManager.HighlightedDoc.get(Doc.GetDataDoc(doc));
+        const IsHighlighted = highlightManager.HighlightedDoc.get(doc) || highlightManager.HighlightedDoc.get(Doc.GetDataDoc(doc));
         return IsHighlighted;
     }
     export function HighlightDoc(doc: Doc) {
@@ -716,10 +716,10 @@ export namespace Doc {
         });
     }
     export function UnhighlightAll() {
-        let mapEntries = highlightManager.HighlightedDoc.keys();
+        const mapEntries = highlightManager.HighlightedDoc.keys();
         let docEntry: IteratorResult<Doc>;
         while (!(docEntry = mapEntries.next()).done) {
-            let targetDoc = docEntry.value;
+            const targetDoc = docEntry.value;
             targetDoc && Doc.UnHighlightDoc(targetDoc);
         }
 
diff --git a/src/new_fields/List.ts b/src/new_fields/List.ts
index 0c7b77fa5..e9101158b 100644
--- a/src/new_fields/List.ts
+++ b/src/new_fields/List.ts
@@ -270,8 +270,8 @@ class ListImpl<T extends Field> extends ObjectField {
     }
 
     [Copy]() {
-        let copiedData = this[Self].__fields.map(f => f instanceof ObjectField ? f[Copy]() : f);
-        let deepCopy = new ListImpl<T>(copiedData as any);
+        const copiedData = this[Self].__fields.map(f => f instanceof ObjectField ? f[Copy]() : f);
+        const deepCopy = new ListImpl<T>(copiedData as any);
         return deepCopy;
     }
 
diff --git a/src/new_fields/RichTextUtils.ts b/src/new_fields/RichTextUtils.ts
index dc5574782..682206aa2 100644
--- a/src/new_fields/RichTextUtils.ts
+++ b/src/new_fields/RichTextUtils.ts
@@ -25,8 +25,8 @@ export namespace RichTextUtils {
 
 
     export const Initialize = (initial?: string) => {
-        let content: any[] = [];
-        let state = {
+        const content: any[] = [];
+        const state = {
             doc: {
                 type: "doc",
                 content,
@@ -56,32 +56,32 @@ export namespace RichTextUtils {
 
     export const ToPlainText = (state: EditorState) => {
         // Because we're working with plain text, just concatenate all paragraphs
-        let content = state.doc.content;
-        let paragraphs: Node<any>[] = [];
+        const content = state.doc.content;
+        const paragraphs: Node<any>[] = [];
         content.forEach(node => node.type.name === "paragraph" && paragraphs.push(node));
 
         // Functions to flatten ProseMirror paragraph objects (and their components) to plain text
         // Concatentate paragraphs and string the result together
-        let textParagraphs: string[] = paragraphs.map(paragraph => {
-            let text: string[] = [];
+        const textParagraphs: string[] = paragraphs.map(paragraph => {
+            const text: string[] = [];
             paragraph.content.forEach(node => node.text && text.push(node.text));
             return text.join(joiner) + delimiter;
         });
-        let plainText = textParagraphs.join(joiner);
+        const plainText = textParagraphs.join(joiner);
         return plainText.substring(0, plainText.length - 1);
     };
 
     export const ToProsemirrorState = (plainText: string, oldState?: RichTextField) => {
         // Remap the text, creating blocks split on newlines
-        let elements = plainText.split(delimiter);
+        const elements = plainText.split(delimiter);
 
         // Google Docs adds in an extra carriage return automatically, so this counteracts it
         !elements[elements.length - 1].length && elements.pop();
 
         // Preserve the current state, but re-write the content to be the blocks
-        let parsed = JSON.parse(oldState ? oldState.Data : Initialize());
+        const parsed = JSON.parse(oldState ? oldState.Data : Initialize());
         parsed.doc.content = elements.map(text => {
-            let paragraph: any = { type: "paragraph" };
+            const paragraph: any = { type: "paragraph" };
             text.length && (paragraph.content = [{ type: "text", marks: [], text }]); // An empty paragraph gets treated as a line break
             return paragraph;
         });
@@ -97,7 +97,7 @@ export namespace RichTextUtils {
 
         export const Export = async (state: EditorState): Promise<GoogleApiClientUtils.Docs.Content> => {
             const nodes: (Node<any> | null)[] = [];
-            let text = ToPlainText(state);
+            const text = ToPlainText(state);
             state.doc.content.forEach(node => {
                 if (!node.childCount) {
                     nodes.push(null);
@@ -168,20 +168,20 @@ export namespace RichTextUtils {
             const title = document.title!;
             const { text, paragraphs } = GoogleApiClientUtils.Docs.Utils.extractText(document);
             let state = FormattedTextBox.blankState();
-            let structured = parseLists(paragraphs);
+            const structured = parseLists(paragraphs);
 
             let position = 3;
-            let lists: ListGroup[] = [];
+            const lists: ListGroup[] = [];
             const indentMap = new Map<ListGroup, BulletPosition[]>();
             let globalOffset = 0;
             const nodes: Node<any>[] = [];
-            for (let element of structured) {
+            for (const element of structured) {
                 if (Array.isArray(element)) {
                     lists.push(element);
-                    let positions: BulletPosition[] = [];
-                    let items = element.map(paragraph => {
-                        let item = listItem(state.schema, paragraph.contents);
-                        let sinks = paragraph.bullet!;
+                    const positions: BulletPosition[] = [];
+                    const items = element.map(paragraph => {
+                        const item = listItem(state.schema, paragraph.contents);
+                        const sinks = paragraph.bullet!;
                         positions.push({
                             value: position + globalOffset,
                             sinks
@@ -208,7 +208,7 @@ export namespace RichTextUtils {
                             }
                         });
                     } else {
-                        let paragraph = paragraphNode(state.schema, element.contents);
+                        const paragraph = paragraphNode(state.schema, element.contents);
                         nodes.push(paragraph);
                         position += paragraph.nodeSize;
                     }
@@ -216,11 +216,11 @@ export namespace RichTextUtils {
             }
             state = state.apply(state.tr.replaceWith(0, 2, nodes));
 
-            let sink = sinkListItem(state.schema.nodes.list_item);
-            let dispatcher = (tr: Transaction) => state = state.apply(tr);
-            for (let list of lists) {
-                for (let pos of indentMap.get(list)!) {
-                    let resolved = state.doc.resolve(pos.value);
+            const sink = sinkListItem(state.schema.nodes.list_item);
+            const dispatcher = (tr: Transaction) => state = state.apply(tr);
+            for (const list of lists) {
+                for (const pos of indentMap.get(list)!) {
+                    const resolved = state.doc.resolve(pos.value);
                     state = state.apply(state.tr.setSelection(new TextSelection(resolved)));
                     for (let i = 0; i < pos.sinks; i++) {
                         sink(state, dispatcher);
@@ -236,9 +236,9 @@ export namespace RichTextUtils {
         type PreparedParagraphs = (ListGroup | Paragraph)[];
 
         const parseLists = (paragraphs: ListGroup) => {
-            let groups: PreparedParagraphs = [];
+            const groups: PreparedParagraphs = [];
             let group: ListGroup = [];
-            for (let paragraph of paragraphs) {
+            for (const paragraph of paragraphs) {
                 if (paragraph.bullet !== undefined) {
                     group.push(paragraph);
                 } else {
@@ -262,8 +262,8 @@ export namespace RichTextUtils {
         };
 
         const paragraphNode = (schema: any, runs: docs_v1.Schema$TextRun[]): Node => {
-            let children = runs.map(run => textNode(schema, run)).filter(child => child !== undefined);
-            let fragment = children.length ? Fragment.from(children) : undefined;
+            const children = runs.map(run => textNode(schema, run)).filter(child => child !== undefined);
+            const fragment = children.length ? Fragment.from(children) : undefined;
             return schema.node("paragraph", null, fragment);
         };
 
@@ -284,7 +284,7 @@ export namespace RichTextUtils {
         };
 
         const textNode = (schema: any, run: docs_v1.Schema$TextRun) => {
-            let text = run.content!.removeTrailingNewlines();
+            const text = run.content!.removeTrailingNewlines();
             return text.length ? schema.text(text, styleToMarks(schema, run.textStyle)) : undefined;
         };
 
@@ -299,17 +299,17 @@ export namespace RichTextUtils {
             if (!textStyle) {
                 return undefined;
             }
-            let marks: Mark[] = [];
+            const marks: Mark[] = [];
             Object.keys(textStyle).forEach(key => {
                 let value: any;
-                let targeted = key as keyof docs_v1.Schema$TextStyle;
+                const targeted = key as keyof docs_v1.Schema$TextStyle;
                 if (value = textStyle[targeted]) {
-                    let attributes: any = {};
+                    const attributes: any = {};
                     let converted = StyleToMark.get(targeted) || targeted;
 
                     value.url && (attributes.href = value.url);
                     if (value.color) {
-                        let object = value.color.rgbColor;
+                        const object = value.color.rgbColor;
                         attributes.color = Color.rgb(["red", "green", "blue"].map(color => object[color] * 255 || 0)).hex();
                     }
                     if (value.magnitude) {
@@ -320,13 +320,13 @@ export namespace RichTextUtils {
                         converted = ImportFontFamilyMapping.get(value.fontFamily) || "timesNewRoman";
                     }
 
-                    let mapped = schema.marks[converted];
+                    const mapped = schema.marks[converted];
                     if (!mapped) {
                         alert(`No mapping found for ${converted}!`);
                         return;
                     }
 
-                    let mark = schema.mark(mapped, attributes);
+                    const mark = schema.mark(mapped, attributes);
                     mark && marks.push(mark);
                 }
             });
@@ -366,9 +366,9 @@ export namespace RichTextUtils {
         const ignored = ["user_mark"];
 
         const marksToStyle = async (nodes: (Node<any> | null)[]): Promise<docs_v1.Schema$Request[]> => {
-            let requests: docs_v1.Schema$Request[] = [];
+            const requests: docs_v1.Schema$Request[] = [];
             let position = 1;
-            for (let node of nodes) {
+            for (const node of nodes) {
                 if (node === null) {
                     position += 2;
                     continue;
@@ -382,7 +382,7 @@ export namespace RichTextUtils {
                 };
                 let mark: Mark<any>;
                 const markMap = BuildMarkMap(marks);
-                for (let markName of Object.keys(schema.marks)) {
+                for (const markName of Object.keys(schema.marks)) {
                     if (ignored.includes(markName) || !(mark = markMap[markName])) {
                         continue;
                     }
diff --git a/src/new_fields/Schema.ts b/src/new_fields/Schema.ts
index b1a449e08..3f0ff4284 100644
--- a/src/new_fields/Schema.ts
+++ b/src/new_fields/Schema.ts
@@ -23,7 +23,7 @@ export type makeInterface<T extends Interface[]> = AllToInterface<T> & Doc & { p
 // export function makeInterface<T extends Interface[], U extends Doc>(schemas: T): (doc: U) => All<T, U>;
 // export function makeInterface<T extends Interface, U extends Doc>(schema: T): (doc: U) => makeInterface<T, U>; 
 export function makeInterface<T extends Interface[]>(...schemas: T): InterfaceFunc<T> {
-    let schema: Interface = {};
+    const schema: Interface = {};
     for (const s of schemas) {
         for (const key in s) {
             schema[key] = s[key];
diff --git a/src/new_fields/ScriptField.ts b/src/new_fields/ScriptField.ts
index cdc9871a8..6b2723663 100644
--- a/src/new_fields/ScriptField.ts
+++ b/src/new_fields/ScriptField.ts
@@ -102,7 +102,7 @@ export class ScriptField extends ObjectField {
         return "script field";
     }
     public static CompileScript(script: string, params: object = {}, addReturn = false) {
-        let compiled = CompileScript(script, {
+        const compiled = CompileScript(script, {
             params: { this: Doc.name, ...params },
             typecheck: false,
             editable: true,
@@ -111,12 +111,12 @@ export class ScriptField extends ObjectField {
         return compiled;
     }
     public static MakeFunction(script: string, params: object = {}) {
-        let compiled = ScriptField.CompileScript(script, params, true);
+        const compiled = ScriptField.CompileScript(script, params, true);
         return compiled.compiled ? new ScriptField(compiled) : undefined;
     }
 
     public static MakeScript(script: string, params: object = {}) {
-        let compiled = ScriptField.CompileScript(script, params, false);
+        const compiled = ScriptField.CompileScript(script, params, false);
         return compiled.compiled ? new ScriptField(compiled) : undefined;
     }
 }
@@ -127,11 +127,11 @@ export class ComputedField extends ScriptField {
     //TODO maybe add an observable cache based on what is passed in for doc, considering there shouldn't really be that many possible values for doc
     value = computedFn((doc: Doc) => this.script.run({ this: doc }, console.log).result);
     public static MakeScript(script: string, params: object = {}, ) {
-        let compiled = ScriptField.CompileScript(script, params, false);
+        const compiled = ScriptField.CompileScript(script, params, false);
         return compiled.compiled ? new ComputedField(compiled) : undefined;
     }
     public static MakeFunction(script: string, params: object = {}) {
-        let compiled = ScriptField.CompileScript(script, params, true);
+        const compiled = ScriptField.CompileScript(script, params, true);
         return compiled.compiled ? new ComputedField(compiled) : undefined;
     }
 }
diff --git a/src/server/ActionUtilities.ts b/src/server/ActionUtilities.ts
index c9fc86fea..c155796a8 100644
--- a/src/server/ActionUtilities.ts
+++ b/src/server/ActionUtilities.ts
@@ -3,10 +3,11 @@ import { ExecOptions } from 'shelljs';
 import { exec } from 'child_process';
 import * as path from 'path';
 import * as rimraf from "rimraf";
+import { yellow } from 'colors';
 
 export const command_line = (command: string, fromDirectory?: string) => {
     return new Promise<string>((resolve, reject) => {
-        let options: ExecOptions = {};
+        const options: ExecOptions = {};
         if (fromDirectory) {
             options.cwd = path.join(__dirname, fromDirectory);
         }
@@ -15,14 +16,14 @@ export const command_line = (command: string, fromDirectory?: string) => {
 };
 
 export const read_text_file = (relativePath: string) => {
-    let target = path.join(__dirname, relativePath);
+    const target = path.join(__dirname, relativePath);
     return new Promise<string>((resolve, reject) => {
         fs.readFile(target, (err, data) => err ? reject(err) : resolve(data.toString()));
     });
 };
 
 export const write_text_file = (relativePath: string, contents: any) => {
-    let target = path.join(__dirname, relativePath);
+    const target = path.join(__dirname, relativePath);
     return new Promise<void>((resolve, reject) => {
         fs.writeFile(target, contents, (err) => err ? reject(err) : resolve());
     });
@@ -42,31 +43,19 @@ export async function log_execution({ startMessage, endMessage, action }: LogDat
     console.log(color, endMessage);
 }
 
-export enum ConsoleColors {
-    Black = `\x1b[30m%s\x1b[0m`,
-    Red = `\x1b[31m%s\x1b[0m`,
-    Green = `\x1b[32m%s\x1b[0m`,
-    Yellow = `\x1b[33m%s\x1b[0m`,
-    Blue = `\x1b[34m%s\x1b[0m`,
-    Magenta = `\x1b[35m%s\x1b[0m`,
-    Cyan = `\x1b[36m%s\x1b[0m`,
-    White = `\x1b[37m%s\x1b[0m`
-}
-
 export function logPort(listener: string, port: number) {
-    process.stdout.write(`${listener} listening on port `);
-    console.log(ConsoleColors.Yellow, port);
+    console.log(`${listener} listening on port ${yellow(String(port))}`);
 }
 
 export function msToTime(duration: number) {
-    let milliseconds = Math.floor((duration % 1000) / 100),
+    const milliseconds = Math.floor((duration % 1000) / 100),
         seconds = Math.floor((duration / 1000) % 60),
         minutes = Math.floor((duration / (1000 * 60)) % 60),
         hours = Math.floor((duration / (1000 * 60 * 60)) % 24);
 
-    let hoursS = (hours < 10) ? "0" + hours : hours;
-    let minutesS = (minutes < 10) ? "0" + minutes : minutes;
-    let secondsS = (seconds < 10) ? "0" + seconds : seconds;
+    const hoursS = (hours < 10) ? "0" + hours : hours;
+    const minutesS = (minutes < 10) ? "0" + minutes : minutes;
+    const secondsS = (seconds < 10) ? "0" + seconds : seconds;
 
     return hoursS + ":" + minutesS + ":" + secondsS + "." + milliseconds;
 }
diff --git a/src/server/ApiManagers/DownloadManager.ts b/src/server/ApiManagers/DownloadManager.ts
index 5bad46eda..2280739fc 100644
--- a/src/server/ApiManagers/DownloadManager.ts
+++ b/src/server/ApiManagers/DownloadManager.ts
@@ -57,14 +57,13 @@ export default class DownloadManager extends ApiManager {
 
         register({
             method: Method.GET,
-            subscription: new RouteSubscriber("/serializeDoc").add("docId"),
+            subscription: new RouteSubscriber("serializeDoc").add("docId"),
             onValidation: async ({ req, res }) => {
                 const { docs, files } = await getDocs(req.params.docId);
                 res.send({ docs, files: Array.from(files) });
             }
         });
 
-
     }
 
 }
diff --git a/src/server/ApiManagers/GeneralGoogleManager.ts b/src/server/ApiManagers/GeneralGoogleManager.ts
index 629684e0c..3617779d5 100644
--- a/src/server/ApiManagers/GeneralGoogleManager.ts
+++ b/src/server/ApiManagers/GeneralGoogleManager.ts
@@ -38,16 +38,19 @@ export default class GeneralGoogleManager extends ApiManager {
 
         register({
             method: Method.POST,
-            subscription: new RouteSubscriber("/googleDocs").add("sector", "action"),
+            subscription: new RouteSubscriber("googleDocs").add("sector", "action"),
             onValidation: async ({ req, res, user }) => {
-                let sector: GoogleApiServerUtils.Service = req.params.sector as GoogleApiServerUtils.Service;
-                let action: GoogleApiServerUtils.Action = req.params.action as GoogleApiServerUtils.Action;
+                const sector: GoogleApiServerUtils.Service = req.params.sector as GoogleApiServerUtils.Service;
+                const action: GoogleApiServerUtils.Action = req.params.action as GoogleApiServerUtils.Action;
                 const endpoint = await GoogleApiServerUtils.GetEndpoint(GoogleApiServerUtils.Service[sector], user.id);
-                let handler = EndpointHandlerMap.get(action);
+                const handler = EndpointHandlerMap.get(action);
                 if (endpoint && handler) {
-                    handler(endpoint, req.body)
-                        .then(response => res.send(response.data))
-                        .catch(exception => res.send(exception));
+                    try {
+                        const response = await handler(endpoint, req.body);
+                        res.send(response.data);
+                    } catch (e) {
+                        res.send(e);
+                    }
                     return;
                 }
                 res.send(undefined);
diff --git a/src/server/ApiManagers/GooglePhotosManager.ts b/src/server/ApiManagers/GooglePhotosManager.ts
index 4a0c0b936..e2539f120 100644
--- a/src/server/ApiManagers/GooglePhotosManager.ts
+++ b/src/server/ApiManagers/GooglePhotosManager.ts
@@ -47,7 +47,7 @@ export default class GooglePhotosManager extends ApiManager {
                 if (!token) {
                     return _error(res, authenticationError);
                 }
-                let failed: GooglePhotosUploadFailure[] = [];
+                const failed: GooglePhotosUploadFailure[] = [];
                 const batched = BatchedArray.from<GooglePhotosUploadUtils.UploadSource>(media, { batchSize: 25 });
                 const newMediaItems = await batched.batchedMapPatientInterval<NewMediaItem>(
                     { magnitude: 100, unit: TimeUnit.Milliseconds },
@@ -87,7 +87,7 @@ export default class GooglePhotosManager extends ApiManager {
                 let failed = 0;
                 if (contents) {
                     const completed: Opt<DashUploadUtils.ImageUploadInformation>[] = [];
-                    for (let item of contents.mediaItems) {
+                    for (const item of contents.mediaItems) {
                         const { contentSize, ...attributes } = await DashUploadUtils.InspectImage(item.baseUrl);
                         const found: Opt<DashUploadUtils.ImageUploadInformation> = await Database.Auxiliary.QueryUploadHistory(contentSize!);
                         if (!found) {
diff --git a/src/server/ApiManagers/PDFManager.ts b/src/server/ApiManagers/PDFManager.ts
index 151b48dd9..a190ab0cb 100644
--- a/src/server/ApiManagers/PDFManager.ts
+++ b/src/server/ApiManagers/PDFManager.ts
@@ -8,7 +8,7 @@ const imageSize = require("probe-image-size");
 import * as express from "express";
 import * as path from "path";
 import { Directory, serverPathToFile, clientPathToFile } from "./UploadManager";
-import { ConsoleColors } from "../ActionUtilities";
+import { red } from "colors";
 
 export default class PDFManager extends ApiManager {
 
@@ -32,10 +32,10 @@ function getOrCreateThumbnail(thumbnailName: string, res: express.Response) {
         const path = serverPathToFile(Directory.pdf_thumbnails, thumbnailName);
         exists(path, (exists: boolean) => {
             if (exists) {
-                let existingThumbnail = createReadStream(path);
+                const existingThumbnail = createReadStream(path);
                 imageSize(existingThumbnail, (err: any, { width, height }: any) => {
                     if (err) {
-                        console.log(ConsoleColors.Red, `In PDF thumbnail response, unable to determine dimensions of ${thumbnailName}:`);
+                        console.log(red(`In PDF thumbnail response, unable to determine dimensions of ${thumbnailName}:`));
                         console.log(err);
                         return;
                     }
@@ -81,7 +81,7 @@ async function CreateThumbnail(file: string, pageNumber: number, res: express.Re
         });
     });
     out.on("error", error => {
-        console.log(ConsoleColors.Red, `In PDF thumbnail creation, encountered the following error when piping ${pngFile}:`);
+        console.log(red(`In PDF thumbnail creation, encountered the following error when piping ${pngFile}:`));
         console.log(error);
     });
 }
@@ -89,8 +89,8 @@ async function CreateThumbnail(file: string, pageNumber: number, res: express.Re
 class NodeCanvasFactory {
 
     create = (width: number, height: number) => {
-        var canvas = createCanvas(width, height);
-        var context = canvas.getContext('2d');
+        const canvas = createCanvas(width, height);
+        const context = canvas.getContext('2d');
         return {
             canvas,
             context
diff --git a/src/server/ApiManagers/SearchManager.ts b/src/server/ApiManagers/SearchManager.ts
index d3f8995b0..7afecbb18 100644
--- a/src/server/ApiManagers/SearchManager.ts
+++ b/src/server/ApiManagers/SearchManager.ts
@@ -1,7 +1,7 @@
 import ApiManager, { Registration } from "./ApiManager";
 import { Method } from "../RouteManager";
 import { Search } from "../Search";
-var findInFiles = require('find-in-files');
+const findInFiles = require('find-in-files');
 import * as path from 'path';
 import { pathToDirectory, Directory } from "./UploadManager";
 
@@ -13,14 +13,14 @@ export default class SearchManager extends ApiManager {
             method: Method.GET,
             subscription: "/textsearch",
             onValidation: async ({ req, res }) => {
-                let q = req.query.q;
+                const q = req.query.q;
                 if (q === undefined) {
                     res.send([]);
                     return;
                 }
-                let results = await findInFiles.find({ 'term': q, 'flags': 'ig' }, pathToDirectory(Directory.text), ".txt$");
-                let resObj: { ids: string[], numFound: number, lines: string[] } = { ids: [], numFound: 0, lines: [] };
-                for (var result in results) {
+                const results = await findInFiles.find({ 'term': q, 'flags': 'ig' }, pathToDirectory(Directory.text), ".txt$");
+                const resObj: { ids: string[], numFound: number, lines: string[] } = { ids: [], numFound: 0, lines: [] };
+                for (const result in results) {
                     resObj.ids.push(path.basename(result, ".txt").replace(/upload_/, ""));
                     resObj.lines.push(results[result].line);
                     resObj.numFound++;
@@ -39,7 +39,7 @@ export default class SearchManager extends ApiManager {
                     res.send([]);
                     return;
                 }
-                let results = await Search.Instance.search(solrQuery);
+                const results = await Search.Instance.search(solrQuery);
                 res.send(results);
             }
         });
diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts
index 80ae0ad61..da1f83b75 100644
--- a/src/server/ApiManagers/UploadManager.ts
+++ b/src/server/ApiManagers/UploadManager.ts
@@ -2,7 +2,7 @@ import ApiManager, { Registration } from "./ApiManager";
 import { Method, _success } from "../RouteManager";
 import * as formidable from 'formidable';
 import v4 = require('uuid/v4');
-var AdmZip = require('adm-zip');
+const AdmZip = require('adm-zip');
 import { extname, basename, dirname } from 'path';
 import { createReadStream, createWriteStream, unlink, readFileSync } from "fs";
 import { publicDirectory, filesDirectory } from "..";
@@ -42,12 +42,12 @@ export default class UploadManager extends ApiManager {
             method: Method.POST,
             subscription: "/upload",
             onValidation: async ({ req, res }) => {
-                let form = new formidable.IncomingForm();
+                const form = new formidable.IncomingForm();
                 form.uploadDir = pathToDirectory(Directory.parsed_files);
                 form.keepExtensions = true;
                 return new Promise<void>(resolve => {
                     form.parse(req, async (_err, _fields, files) => {
-                        let results: any[] = [];
+                        const results: any[] = [];
                         for (const key in files) {
                             const result = await DashUploadUtils.upload(files[key]);
                             result && results.push(result);
@@ -63,7 +63,7 @@ export default class UploadManager extends ApiManager {
             method: Method.POST,
             subscription: "/uploadDoc",
             onValidation: ({ req, res }) => {
-                let form = new formidable.IncomingForm();
+                const form = new formidable.IncomingForm();
                 form.keepExtensions = true;
                 // let path = req.body.path;
                 const ids: { [id: string]: string } = {};
@@ -122,8 +122,8 @@ export default class UploadManager extends ApiManager {
                                 zip.getEntries().forEach((entry: any) => {
                                     if (!entry.entryName.startsWith("files/")) return;
                                     let directory = dirname(entry.entryName) + "/";
-                                    let extension = extname(entry.entryName);
-                                    let base = basename(entry.entryName).split(".")[0];
+                                    const extension = extname(entry.entryName);
+                                    const base = basename(entry.entryName).split(".")[0];
                                     try {
                                         zip.extractEntryTo(entry.entryName, publicDirectory, true, false);
                                         directory = "/" + directory;
@@ -139,7 +139,7 @@ export default class UploadManager extends ApiManager {
                                 const json = zip.getEntry("doc.json");
                                 let docs: any;
                                 try {
-                                    let data = JSON.parse(json.getData().toString("utf8"));
+                                    const data = JSON.parse(json.getData().toString("utf8"));
                                     docs = data.docs;
                                     id = data.id;
                                     docs = Object.keys(docs).map(key => docs[key]);
@@ -189,7 +189,7 @@ export default class UploadManager extends ApiManager {
                 return imageDataUri.outputFile(uri, serverPathToFile(Directory.images, filename)).then((savedName: string) => {
                     const ext = extname(savedName).toLowerCase();
                     const { pngs, jpgs } = AcceptibleMedia;
-                    let resizers = [
+                    const resizers = [
                         { resizer: sharp().resize(100, undefined, { withoutEnlargement: true }), suffix: "_s" },
                         { resizer: sharp().resize(400, undefined, { withoutEnlargement: true }), suffix: "_m" },
                         { resizer: sharp().resize(900, undefined, { withoutEnlargement: true }), suffix: "_l" },
diff --git a/src/server/ApiManagers/UtilManager.ts b/src/server/ApiManagers/UtilManager.ts
index c1234be6c..601a7d0d0 100644
--- a/src/server/ApiManagers/UtilManager.ts
+++ b/src/server/ApiManagers/UtilManager.ts
@@ -35,11 +35,11 @@ export default class UtilManager extends ApiManager {
             method: Method.GET,
             subscription: "/buxton",
             onValidation: async ({ res }) => {
-                let cwd = '../scraping/buxton';
+                const cwd = '../scraping/buxton';
 
-                let onResolved = (stdout: string) => { console.log(stdout); res.redirect("/"); };
-                let onRejected = (err: any) => { console.error(err.message); res.send(err); };
-                let tryPython3 = () => command_line('python3 scraper.py', cwd).then(onResolved, onRejected);
+                const onResolved = (stdout: string) => { console.log(stdout); res.redirect("/"); };
+                const onRejected = (err: any) => { console.error(err.message); res.send(err); };
+                const tryPython3 = () => command_line('python3 scraper.py', cwd).then(onResolved, onRejected);
 
                 return command_line('python scraper.py', cwd).then(onResolved, tryPython3);
             },
diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts
index 9ccc72e35..d9d985ca5 100644
--- a/src/server/DashUploadUtils.ts
+++ b/src/server/DashUploadUtils.ts
@@ -9,10 +9,11 @@ import { AcceptibleMedia } from './SharedMediaTypes';
 import { filesDirectory } from '.';
 import { File } from 'formidable';
 import { basename } from "path";
-import { ConsoleColors, createIfNotExists } from './ActionUtilities';
+import { createIfNotExists } from './ActionUtilities';
 import { ParsedPDF } from "../server/PdfTypes";
 const parse = require('pdf-parse');
 import { Directory, serverPathToFile, clientPathToFile } from './ApiManagers/UploadManager';
+import { red } from 'colors';
 
 export enum SizeSuffix {
     Small = "_s",
@@ -86,12 +87,12 @@ export namespace DashUploadUtils {
                 }
         }
 
-        console.log(ConsoleColors.Red, `Ignoring unsupported file (${name}) with upload type (${type}).`);
+        console.log(red(`Ignoring unsupported file (${name}) with upload type (${type}).`));
         return { clientAccessPath: undefined };
     }
 
     async function UploadPdf(absolutePath: string) {
-        let dataBuffer = fs.readFileSync(absolutePath);
+        const dataBuffer = fs.readFileSync(absolutePath);
         const result: ParsedPDF = await parse(dataBuffer);
         const parsedName = basename(absolutePath);
         await new Promise<void>((resolve, reject) => {
@@ -194,7 +195,7 @@ export namespace DashUploadUtils {
         const { isLocal, stream, normalizedUrl, contentSize, contentType, exifData } = metadata;
         const resolved = filename || generate(prefix, normalizedUrl);
         const extension = format || sanitizeExtension(normalizedUrl || resolved);
-        let information: ImageUploadInformation = {
+        const information: ImageUploadInformation = {
             clientAccessPath: clientPathToFile(Directory.images, resolved),
             serverAccessPaths: {},
             exifData,
@@ -215,8 +216,7 @@ export namespace DashUploadUtils {
             } else if (jpgs.includes(extension)) {
                 resizers.forEach(element => element.resizer = element.resizer.jpeg());
             }
-            for (let { resizer, suffix } of resizers) {
-                let mediaPath: string;
+            for (const { resizer, suffix } of resizers) {
                 await new Promise<void>(resolve => {
                     const filename = InjectSize(resolved, suffix);
                     information.serverAccessPaths[suffix] = serverPathToFile(Directory.images, filename);
diff --git a/src/server/Initialization.ts b/src/server/Initialization.ts
index 8b633a7cd..74770153d 100644
--- a/src/server/Initialization.ts
+++ b/src/server/Initialization.ts
@@ -19,8 +19,9 @@ import * as fs from 'fs';
 import * as request from 'request';
 import RouteSubscriber from './RouteSubscriber';
 import { publicDirectory } from '.';
-import { ConsoleColors, logPort } from './ActionUtilities';
+import { logPort } from './ActionUtilities';
 import { timeMap } from './ApiManagers/UserManager';
+import { blue, yellow } from 'colors';
 
 /* RouteSetter is a wrapper around the server that prevents the server
    from being exposed. */
@@ -53,10 +54,13 @@ export default async function InitializeServer(options: InitializationOptions) {
     registerAuthenticationRoutes(server);
     registerCorsProxy(server);
 
-    const isRelease = determineEnvironment(); //vs. dev mode
+    const isRelease = determineEnvironment();
     routeSetter(new RouteManager(server, isRelease));
 
-    server.listen(listenAtPort, () => logPort("server", listenAtPort));
+    server.listen(listenAtPort, () => {
+        logPort("server", listenAtPort);
+        console.log();
+    });
     return isRelease;
 }
 
@@ -92,8 +96,9 @@ function buildWithMiddleware(server: express.Express) {
 function determineEnvironment() {
     const isRelease = process.env.RELEASE === "true";
 
-    console.log(`running server in ${isRelease ? 'release' : 'debug'} mode`);
-    console.log(process.env.PWD);
+    const color = isRelease ? blue : yellow;
+    const label = isRelease ? "release" : "development";
+    console.log(`\nrunning server in ${color(label)} mode`);
 
     let clientUtils = fs.readFileSync("./src/client/util/ClientUtils.ts.temp", "utf8");
     clientUtils = `//AUTO-GENERATED FILE: DO NOT EDIT\n${clientUtils.replace('"mode"', String(isRelease))}`;
diff --git a/src/server/RouteManager.ts b/src/server/RouteManager.ts
index 7c49485f1..3af36d879 100644
--- a/src/server/RouteManager.ts
+++ b/src/server/RouteManager.ts
@@ -1,7 +1,7 @@
 import RouteSubscriber from "./RouteSubscriber";
 import { DashUserModel } from "./authentication/models/user_model";
 import * as express from 'express';
-import { ConsoleColors } from "./ActionUtilities";
+import { yellow, cyan, red } from 'colors';
 
 export enum Method {
     GET,
@@ -41,6 +41,12 @@ export default class RouteManager {
         this._isRelease = isRelease;
     }
 
+    log = () => {
+        console.log(yellow("\nthe following server routes have been registered:"));
+        Array.from(registered.keys()).sort().forEach(route => console.log(cyan(route)));
+        console.log();
+    }
+
     /**
      * 
      * @param initializer 
@@ -48,14 +54,14 @@ export default class RouteManager {
     addSupervisedRoute = (initializer: RouteInitializer): void => {
         const { method, subscription, onValidation, onUnauthenticated, onError } = initializer;
         const isRelease = this._isRelease;
-        let supervised = async (req: express.Request, res: express.Response) => {
+        const supervised = async (req: express.Request, res: express.Response) => {
             const { user, originalUrl: target } = req;
             const core = { req, res, isRelease };
             const tryExecute = async (toExecute: (args: any) => any | Promise<any>, args: any) => {
                 try {
                     await toExecute(args);
                 } catch (e) {
-                    console.log(ConsoleColors.Red, target, user?.email ?? "<user logged out>");
+                    console.log(red(target), user?.email ?? "<user logged out>");
                     if (onError) {
                         onError({ ...core, error: e });
                     } else {
@@ -78,7 +84,7 @@ export default class RouteManager {
             }
             setTimeout(() => {
                 if (!res.headersSent) {
-                    console.log("Initiating fallback for ", target);
+                    console.log(`Initiating fallback for ${target}`);
                     const warning = `request to ${target} fell through - this is a fallback response`;
                     res.send({ warning });
                 }
@@ -94,7 +100,7 @@ export default class RouteManager {
             const existing = registered.get(route);
             if (existing) {
                 if (existing.has(method)) {
-                    console.log(ConsoleColors.Red, `\nDuplicate registration error: already registered ${route} with Method[${method}]`);
+                    console.log(red(`\nDuplicate registration error: already registered ${route} with Method[${method}]`));
                     console.log('Please remove duplicate registrations before continuing...\n');
                     process.exit(0);
                 }
diff --git a/src/server/Websocket/Websocket.ts b/src/server/Websocket/Websocket.ts
index fbf71f707..2eb764945 100644
--- a/src/server/Websocket/Websocket.ts
+++ b/src/server/Websocket/Websocket.ts
@@ -7,12 +7,13 @@ import { Search } from "../Search";
 import * as io from 'socket.io';
 import YoutubeApi from "../apis/youtube/youtubeApiSample";
 import { GoogleCredentialsLoader } from "../credentials/CredentialsLoader";
-import { ConsoleColors, logPort } from "../ActionUtilities";
+import { logPort } from "../ActionUtilities";
 import { timeMap } from "../ApiManagers/UserManager";
+import { green } from "colors";
 
 export namespace WebSocket {
 
-    let clients: { [key: string]: Client } = {};
+    const clients: { [key: string]: Client } = {};
     export const socketMap = new Map<SocketIO.Socket, string>();
 
     export async function start(serverPort: number, isRelease: boolean) {
@@ -27,7 +28,7 @@ export namespace WebSocket {
         const endpoint = io();
         endpoint.on("connection", function (socket: Socket) {
             socket.use((_packet, next) => {
-                let userEmail = socketMap.get(socket);
+                const userEmail = socketMap.get(socket);
                 if (userEmail) {
                     timeMap[userEmail] = Date.now();
                 }
@@ -85,7 +86,7 @@ export namespace WebSocket {
 
     function barReceived(socket: SocketIO.Socket, userEmail: string) {
         clients[userEmail] = new Client(userEmail.toString());
-        console.log(ConsoleColors.Green, `user ${userEmail} has connected to the web socket`);
+        console.log(green(`user ${userEmail} has connected to the web socket`));
         socketMap.set(socket, userEmail);
     }
 
@@ -178,12 +179,12 @@ export namespace WebSocket {
         for (let key in docfield) {
             if (!key.startsWith("fields.")) continue;
             dynfield = true;
-            let val = docfield[key];
+            const val = docfield[key];
             key = key.substring(7);
             Object.values(suffixMap).forEach(suf => update[key + getSuffix(suf)] = { set: null });
-            let term = ToSearchTerm(val);
+            const term = ToSearchTerm(val);
             if (term !== undefined) {
-                let { suffix, value } = term;
+                const { suffix, value } = term;
                 update[key + suffix] = { set: value };
             }
         }
diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts
index b0f3ba993..329107a71 100644
--- a/src/server/apis/google/GoogleApiServerUtils.ts
+++ b/src/server/apis/google/GoogleApiServerUtils.ts
@@ -135,7 +135,7 @@ export namespace GoogleApiServerUtils {
                 return resolve();
             }
             let routed: Opt<Endpoint>;
-            let parameters: any = { auth, version: "v1" };
+            const parameters: any = { auth, version: "v1" };
             switch (sector) {
                 case Service.Documents:
                     routed = google.docs(parameters).documents;
@@ -318,7 +318,7 @@ export namespace GoogleApiServerUtils {
      */
     async function retrieveCredentials(userId: string): Promise<{ credentials: Opt<Credentials>, refreshed: boolean }> {
         let credentials: Opt<Credentials> = await Database.Auxiliary.GoogleAuthenticationToken.Fetch(userId);
-        let refreshed = false;
+        const refreshed = false;
         if (!credentials) {
             return { credentials: undefined, refreshed };
         }
@@ -340,9 +340,9 @@ export namespace GoogleApiServerUtils {
      * @returns the updated credentials
      */
     async function refreshAccessToken(credentials: Credentials, userId: string): Promise<Credentials> {
-        let headerParameters = { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } };
+        const headerParameters = { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } };
         const { client_id, client_secret } = GoogleCredentialsLoader.ProjectCredentials;
-        let url = `https://oauth2.googleapis.com/token?${qs.stringify({
+        const url = `https://oauth2.googleapis.com/token?${qs.stringify({
             refreshToken: credentials.refresh_token,
             client_id,
             client_secret,
diff --git a/src/server/authentication/config/passport.ts b/src/server/authentication/config/passport.ts
index 726df7fd7..2271109a5 100644
--- a/src/server/authentication/config/passport.ts
+++ b/src/server/authentication/config/passport.ts
@@ -40,7 +40,7 @@ export let isAuthenticated = (req: Request, res: Response, next: NextFunction) =
 export let isAuthorized = (req: Request, res: Response, next: NextFunction) => {
     const provider = req.path.split("/").slice(-1)[0];
 
-    if (_.find((req.user as any).tokens!, { kind: provider })) {
+    if (_.find(req.user?.tokens, { kind: provider })) {
         next();
     } else {
         res.redirect(`/auth/${provider}`);
diff --git a/src/server/authentication/controllers/user_controller.ts b/src/server/authentication/controllers/user_controller.ts
index 517353479..f0086d4ea 100644
--- a/src/server/authentication/controllers/user_controller.ts
+++ b/src/server/authentication/controllers/user_controller.ts
@@ -68,9 +68,9 @@ export let postSignup = (req: Request, res: Response, next: NextFunction) => {
 
 };
 
-let tryRedirectToTarget = (req: Request, res: Response) => {
+const tryRedirectToTarget = (req: Request, res: Response) => {
     if (req.session && req.session.target) {
-        let target = req.session.target;
+        const target = req.session.target;
         req.session.target = undefined;
         res.redirect(target);
     } else {
@@ -148,7 +148,6 @@ export let postForgot = function (req: Request, res: Response, next: NextFunctio
     const email = req.body.email;
     async.waterfall([
         function (done: any) {
-            let token: string;
             c.randomBytes(20, function (err: any, buffer: Buffer) {
                 if (err) {
                     done(null);
diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts
index ac4462f78..50bfb4832 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -42,10 +42,10 @@ export class CurrentUserUtils {
 
     // setup the "creator" buttons for the sidebar-- eg. the default set of draggable document creation tools
     static setupCreatorButtons(doc: Doc) {
-        let notes = CurrentUserUtils.setupNoteTypes(doc);
+        const notes = CurrentUserUtils.setupNoteTypes(doc);
         doc.noteTypes = Docs.Create.TreeDocument(notes, { title: "Note Types", height: 75 });
         doc.activePen = doc;
-        let docProtoData: { title: string, icon: string, drag?: string, ignoreClick?: boolean, click?: string, ischecked?: string, activePen?: Doc, backgroundColor?: string, dragFactory?: Doc }[] = [
+        const docProtoData: { title: string, icon: string, drag?: string, ignoreClick?: boolean, click?: string, ischecked?: string, activePen?: Doc, backgroundColor?: string, dragFactory?: Doc }[] = [
             { title: "collection", icon: "folder", ignoreClick: true, drag: 'Docs.Create.FreeformDocument([], { nativeWidth: undefined, nativeHeight: undefined, width: 150, height: 100, title: "freeform" })' },
             { title: "todo item", icon: "check", ignoreClick: true, drag: 'getCopy(this.dragFactory, true)', dragFactory: notes[notes.length - 1] },
             { title: "web page", icon: "globe-asia", ignoreClick: true, drag: 'Docs.Create.WebDocument("https://en.wikipedia.org/wiki/Hedgehog", { width: 300, height: 300, title: "New Webpage" })' },
@@ -278,7 +278,7 @@ export class CurrentUserUtils {
         if (this._northstarCatalog && CurrentUserUtils._northstarSchemas) {
             this._northstarCatalog.schemas!.push(schema);
             CurrentUserUtils._northstarSchemas.push(schemaDoc);
-            let schemas = Cast(CurrentUserUtils.UserDocument.DBSchemas, listSpec("string"), []);
+            const schemas = Cast(CurrentUserUtils.UserDocument.DBSchemas, listSpec("string"), []);
             schemas.push(schema.displayName!);
             CurrentUserUtils.UserDocument.DBSchemas = new List<string>(schemas);
         }
diff --git a/src/server/database.ts b/src/server/database.ts
index db81245c1..4039e6203 100644
--- a/src/server/database.ts
+++ b/src/server/database.ts
@@ -71,7 +71,7 @@ export namespace Database {
 
         public async update(id: string, value: any, callback: (err: mongodb.MongoError, res: mongodb.UpdateWriteOpResult) => void, upsert = true, collectionName = Database.DocumentsCollection) {
             if (this.db) {
-                let collection = this.db.collection(collectionName);
+                const collection = this.db.collection(collectionName);
                 const prom = this.currentWrites[id];
                 let newProm: Promise<void>;
                 const run = (): Promise<void> => {
@@ -96,7 +96,7 @@ export namespace Database {
 
         public replace(id: string, value: any, callback: (err: mongodb.MongoError, res: mongodb.UpdateWriteOpResult) => void, upsert = true, collectionName = Database.DocumentsCollection) {
             if (this.db) {
-                let collection = this.db.collection(collectionName);
+                const collection = this.db.collection(collectionName);
                 const prom = this.currentWrites[id];
                 let newProm: Promise<void>;
                 const run = (): Promise<void> => {
diff --git a/src/server/downsize.ts b/src/server/downsize.ts
index ed68fbecc..cd0d83812 100644
--- a/src/server/downsize.ts
+++ b/src/server/downsize.ts
@@ -17,7 +17,7 @@ fs.readdir(folder, async (err, files) => {
     // });
     for (const file of files) {
         const filesplit = file.split(".");
-        let resizers = [
+        const resizers = [
             { resizer: sharp().resize(100, undefined, { withoutEnlargement: true }), suffix: "_s" },
             { resizer: sharp().resize(400, undefined, { withoutEnlargement: true }), suffix: "_m" },
             { resizer: sharp().resize(900, undefined, { withoutEnlargement: true }), suffix: "_l" },
diff --git a/src/server/index.ts b/src/server/index.ts
index d77923710..0e0bd0bd6 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -51,7 +51,7 @@ async function preliminaryFunctions() {
  * that will manage the registration of new routes
  * with the server
  */
-function routeSetter({ isRelease, addSupervisedRoute }: RouteManager) {
+function routeSetter({ isRelease, addSupervisedRoute, log }: RouteManager) {
     const managers = [
         new UserManager(),
         new UploadManager(),
@@ -67,6 +67,7 @@ function routeSetter({ isRelease, addSupervisedRoute }: RouteManager) {
 
     // initialize API Managers
     managers.forEach(manager => manager.register(addSupervisedRoute));
+    log();
 
     // initialize the web socket (bidirectional communication: if a user changes
     // a field on one client, that change must be broadcast to all other clients)
@@ -82,8 +83,8 @@ function routeSetter({ isRelease, addSupervisedRoute }: RouteManager) {
     });
 
     const serve: OnUnauthenticated = ({ req, res }) => {
-        let detector = new mobileDetect(req.headers['user-agent'] || "");
-        let filename = detector.mobile() !== null ? 'mobile/image.html' : 'index.html';
+        const detector = new mobileDetect(req.headers['user-agent'] || "");
+        const filename = detector.mobile() !== null ? 'mobile/image.html' : 'index.html';
         res.sendFile(path.join(__dirname, '../../deploy/' + filename));
     };
 
@@ -104,8 +105,8 @@ function routeSetter({ isRelease, addSupervisedRoute }: RouteManager) {
 
 (async function start() {
     await log_execution({
-        startMessage: "starting execution of preliminary functions",
-        endMessage: "completed preliminary functions",
+        startMessage: "\nstarting execution of preliminary functions",
+        endMessage: "completed preliminary functions\n",
         action: preliminaryFunctions
     });
     await initializeServer({ listenAtPort: 1050, routeSetter });
diff --git a/src/typings/index.d.ts b/src/typings/index.d.ts
index 36d828fdb..9e8ace962 100644
--- a/src/typings/index.d.ts
+++ b/src/typings/index.d.ts
@@ -1,6 +1,7 @@
 /// <reference types="node" />
 
 declare module 'googlephotos';
+declare module 'react-image-lightbox-with-rotate';
 
 declare module '@react-pdf/renderer' {
     import * as React from 'react';
diff --git a/test/test.ts b/test/test.ts
index f1cf75bd4..245733e9b 100644
--- a/test/test.ts
+++ b/test/test.ts
@@ -14,21 +14,21 @@ import { createSchema, makeInterface, defaultSpec } from '../src/new_fields/Sche
 import { ImageField } from '../src/new_fields/URLField';
 describe("Document", () => {
     it('should hold fields', () => {
-        let key = "Test";
-        let key2 = "Test2";
-        let field = 15;
-        let doc = new Doc();
+        const key = "Test";
+        const key2 = "Test2";
+        const field = 15;
+        const doc = new Doc();
         doc[key] = field;
-        let getField = Cast(doc[key], "number");
-        let getField2 = Cast(doc[key2], "number");
+        const getField = Cast(doc[key], "number");
+        const getField2 = Cast(doc[key2], "number");
         expect(getField).to.equal(field);
         expect(getField2).to.equal(undefined);
     });
 
     it('should update', () => {
-        let doc = new Doc();
-        let key = "Test";
-        let key2 = "Test2";
+        const doc = new Doc();
+        const key = "Test";
+        const key2 = "Test2";
         let ran = false;
         reaction(() => doc[key], (field) => { ran = true; });
         expect(ran).to.equal(false);
diff --git a/tslint.json b/tslint.json
index 76d28b375..0056f4266 100644
--- a/tslint.json
+++ b/tslint.json
@@ -1,32 +1,18 @@
 {
+    "linterOptions": {
+        "exclude": ["./src/client/northstar/**", "**/*.js"]
+    },
     "rules": {
-        // "no-non-null-assertion": true,
         "no-return-await": true,
         "no-string-literal": true,
-        // "no-var-keyword": true,
-        // "no-var-requires": true,
         "prefer-object-spread": true,
         "prefer-for-of": true,
         "no-unnecessary-type-assertion": true,
-        // "no-void-expression": [
-        //     true,
-        //     "ignore-arrow-function-shorthand"
-        // ],
         "triple-equals": true,
-        // "prefer-const": true,
+        "prefer-const": true,
         "no-unnecessary-callback-wrapper": true,
-        // "align": [
-        //     true,
-        //     "parameters",
-        //     "arguments",
-        //     "statements",
-        //     "members",
-        //     "elements"
-        // ],
         "class-name": true,
         "arrow-return-shorthand": true,
-        // "object-literal-shorthand": true,
-        // "object-literal-sort-keys": true,
         "semicolon": [
             true,
             "always"
@@ -35,13 +21,6 @@
             true,
             "ignore-same-line"
         ],
-        // "quotemark": [
-        //     true,
-        //     "double",
-        //     "jsx-double",
-        //     "avoid-template",
-        //     "avoid-escape"
-        // ],
         "no-tautology-expression": true,
         "unnecessary-constructor": true
         // "trailing-comma": [
@@ -52,6 +31,33 @@
         //     }
         // ],
         // "ordered-imports": true
+        // "quotemark": [
+        //     true,
+        //     "double",
+        //     "jsx-double",
+        //     "avoid-template",
+        //     "avoid-escape"
+        // ],
+        // "object-literal-shorthand": true,
+        // "object-literal-sort-keys": true,
+        // "align": [
+        //     true,
+        //     "parameters",
+        //     "arguments",
+        //     "statements",
+        //     "members",
+        //     "elements"
+        // ],
+        // "no-void-expression": [
+        //     true,
+        //     "ignore-arrow-function-shorthand"
+        // ],
+        // "no-non-null-assertion": true,
+        // "no-var-keyword": true,
+        // "no-var-requires": true,
     },
-    "defaultSeverity": "warning"
+    "defaultSeverity": "warning",
+    "jsRules": {
+
+    }
 }
\ No newline at end of file
-- 
cgit v1.2.3-70-g09d2


From 56e384f2d97d2c399adf21fcf58143773fe3fd53 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Sat, 14 Dec 2019 01:51:13 -0500
Subject: fixes to make nativewidth/height more robust for PDFs.  fixes for
 search - filter out extension docs.

---
 src/client/documents/Documents.ts                  | 11 ++++---
 src/client/util/DragManager.ts                     | 23 ++++++++------
 .../util/Import & Export/DirectoryImportBox.tsx    |  7 +----
 src/client/util/SearchUtil.ts                      |  4 +--
 src/client/views/DocComponent.tsx                  |  1 +
 src/client/views/collections/CollectionSubView.tsx |  4 +--
 .../views/collections/ParentDocumentSelector.tsx   |  2 +-
 src/client/views/nodes/PDFBox.tsx                  | 34 ++++++++++++--------
 src/client/views/pdf/PDFMenu.tsx                   | 36 ++--------------------
 src/client/views/pdf/PDFViewer.tsx                 |  4 +--
 src/client/views/search/SearchBox.tsx              |  5 ++-
 src/new_fields/Doc.ts                              | 11 +++++--
 12 files changed, 61 insertions(+), 81 deletions(-)

(limited to 'src/client/util/Import & Export/DirectoryImportBox.tsx')

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 7df08c7e6..4ba45ac30 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -112,7 +112,7 @@ export interface DocumentOptions {
     dropConverter?: ScriptField; // script to run when documents are dropped on this Document.
     strokeWidth?: number;
     color?: string;
-    limitHeight?:number; // maximum height for newly created (eg, from pasting) text documents
+    limitHeight?: number; // maximum height for newly created (eg, from pasting) text documents
     // [key: string]: Opt<Field>;
 }
 
@@ -181,7 +181,7 @@ export namespace Docs {
             }],
             [DocumentType.PDF, {
                 layout: { view: PDFBox, dataField: data },
-                options: { nativeWidth: 1200, curPage: 1 }
+                options: { curPage: 1 }
             }],
             [DocumentType.ICON, {
                 layout: { view: IconBox, dataField: data },
@@ -644,17 +644,20 @@ export namespace Docs {
             let ctor: ((path: string, options: DocumentOptions) => (Doc | Promise<Doc | undefined>)) | undefined = undefined;
             if (type.indexOf("image") !== -1) {
                 ctor = Docs.Create.ImageDocument;
+                if (!options.width) options.width = 300;
             }
             if (type.indexOf("video") !== -1) {
                 ctor = Docs.Create.VideoDocument;
+                if (!options.width) options.width = 600;
+                if (!options.height) options.height = options.width * 2 / 3;
             }
             if (type.indexOf("audio") !== -1) {
                 ctor = Docs.Create.AudioDocument;
             }
             if (type.indexOf("pdf") !== -1) {
                 ctor = Docs.Create.PdfDocument;
-                options.nativeWidth = 927;
-                options.nativeHeight = 1200;
+                if (!options.width) options.width = 400;
+                if (!options.height) options.height = options.width * 1200 / 927;
             }
             if (type.indexOf("excel") !== -1) {
                 ctor = Docs.Create.DBDocument;
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 326262895..2631a1e3c 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -18,7 +18,7 @@ import { convertDropDataToButtons } from "./DropConverter";
 export type dropActionType = "alias" | "copy" | undefined;
 export function SetupDrag(
     _reference: React.RefObject<HTMLElement>,
-    docFunc: () => Doc | Promise<Doc>,
+    docFunc: () => Doc | Promise<Doc> | undefined,
     moveFunc?: DragManager.MoveFunction,
     dropAction?: dropActionType,
     treeViewId?: string,
@@ -32,13 +32,15 @@ export function SetupDrag(
         document.removeEventListener("pointermove", onRowMove);
         document.removeEventListener('pointerup', onRowUp);
         const doc = await docFunc();
-        const dragData = new DragManager.DocumentDragData([doc]);
-        dragData.dropAction = dropAction;
-        dragData.moveDocument = moveFunc;
-        dragData.treeViewId = treeViewId;
-        dragData.dontHideOnDrop = dontHideOnDrop;
-        DragManager.StartDocumentDrag([_reference.current!], dragData, e.x, e.y);
-        dragStarted && dragStarted();
+        if (doc) {
+            const dragData = new DragManager.DocumentDragData([doc]);
+            dragData.dropAction = dropAction;
+            dragData.moveDocument = moveFunc;
+            dragData.treeViewId = treeViewId;
+            dragData.dontHideOnDrop = dontHideOnDrop;
+            DragManager.StartDocumentDrag([_reference.current!], dragData, e.x, e.y);
+            dragStarted && dragStarted();
+        }
     };
     const onRowUp = (): void => {
         document.removeEventListener("pointermove", onRowMove);
@@ -49,12 +51,13 @@ export function SetupDrag(
             e.stopPropagation();
             if (e.shiftKey && CollectionDockingView.Instance) {
                 e.persist();
-                CollectionDockingView.Instance.StartOtherDrag({
+                let dragDoc = await docFunc();
+                dragDoc && CollectionDockingView.Instance.StartOtherDrag({
                     pageX: e.pageX,
                     pageY: e.pageY,
                     preventDefault: emptyFunction,
                     button: 0
-                }, [await docFunc()]);
+                }, [dragDoc]);
             } else {
                 document.addEventListener("pointermove", onRowMove);
                 document.addEventListener("pointerup", onRowUp);
diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx
index e6a215b2c..5b5bffd8c 100644
--- a/src/client/util/Import & Export/DirectoryImportBox.tsx	
+++ b/src/client/util/Import & Export/DirectoryImportBox.tsx	
@@ -122,12 +122,7 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
 
         await Promise.all(uploads.map(async ({ name, type, clientAccessPath, exifData }) => {
             const path = Utils.prepend(clientAccessPath);
-            const options = {
-                nativeWidth: 300,
-                width: 300,
-                title: name
-            };
-            const document = await Docs.Get.DocumentFromType(type, path, options);
+            const document = await Docs.Get.DocumentFromType(type, path, { width: 300, title: name });
             const { data, error } = exifData;
             if (document) {
                 Doc.GetProto(document).exif = error || Docs.Get.DocumentHierarchyFromJson(data);
diff --git a/src/client/util/SearchUtil.ts b/src/client/util/SearchUtil.ts
index 7a9176bec..1fda82880 100644
--- a/src/client/util/SearchUtil.ts
+++ b/src/client/util/SearchUtil.ts
@@ -64,7 +64,7 @@ export namespace SearchUtil {
         const textDocs = newIds.map((id: string) => textDocMap[id]).map(doc => doc as Doc);
         for (let i = 0; i < textDocs.length; i++) {
             const testDoc = textDocs[i];
-            if (testDoc instanceof Doc && testDoc.type !== DocumentType.KVP && theDocs.findIndex(d => Doc.AreProtosEqual(d, testDoc)) === -1) {
+            if (testDoc instanceof Doc && testDoc.type !== DocumentType.KVP && testDoc.type !== DocumentType.EXTENSION && theDocs.findIndex(d => Doc.AreProtosEqual(d, testDoc)) === -1) {
                 theDocs.push(Doc.GetProto(testDoc));
                 theLines.push(newLines[i].map(line => line.replace(query, query.toUpperCase())));
             }
@@ -74,7 +74,7 @@ export namespace SearchUtil {
         const docs = ids.map((id: string) => docMap[id]).map(doc => doc as Doc);
         for (let i = 0; i < ids.length; i++) {
             const testDoc = docs[i];
-            if (testDoc instanceof Doc && testDoc.type !== DocumentType.KVP && (options.allowAliases || theDocs.findIndex(d => Doc.AreProtosEqual(d, testDoc)) === -1)) {
+            if (testDoc instanceof Doc && testDoc.type !== DocumentType.KVP && testDoc.type !== DocumentType.EXTENSION && (options.allowAliases || theDocs.findIndex(d => Doc.AreProtosEqual(d, testDoc)) === -1)) {
                 theDocs.push(testDoc);
                 theLines.push([]);
             }
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index c7ddee7ea..7fbad4638 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -59,6 +59,7 @@ export function DocAnnotatableComponent<P extends DocAnnotatableProps, T>(schema
         @computed get layoutDoc() { return Doc.Layout(this.props.Document); }
         @computed get dataDoc() { return (this.props.DataDoc && this.props.Document.isTemplateField ? this.props.DataDoc : Doc.GetProto(this.props.Document)) as Doc; }
         @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); }
+        @computed get extensionDocSync() { return Doc.fieldExtensionDocSync(this.dataDoc, this.props.fieldKey); }
         @computed get annotationsKey() { return "annotations"; }
 
         @action.bound
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 5c7794cc0..062521690 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -262,7 +262,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
                         .then(result => {
                             const type = result["content-type"];
                             if (type) {
-                                Docs.Get.DocumentFromType(type, str, { ...options, width: 300, nativeWidth: type.indexOf("video") !== -1 ? 600 : 300 })
+                                Docs.Get.DocumentFromType(type, str, options)
                                     .then(doc => doc && this.props.addDocument(doc));
                             }
                         });
@@ -281,7 +281,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
                     const dropFileName = file ? file.name : "-empty-";
                     promises.push(Networking.PostFormDataToServer("/upload", formData).then(results => {
                         results.map(action(({ clientAccessPath }: any) => {
-                            const full = { ...options, nativeWidth: type.indexOf("video") !== -1 ? 600 : 300, width: 300, title: dropFileName };
+                            const full = { ...options, width: 300, title: dropFileName };
                             const pathname = Utils.prepend(clientAccessPath);
                             Docs.Get.DocumentFromType(type, pathname, full).then(doc => {
                                 doc && (Doc.GetProto(doc).fileUpload = basename(pathname).replace("upload_", "").replace(/\.[a-z0-9]*$/, ""));
diff --git a/src/client/views/collections/ParentDocumentSelector.tsx b/src/client/views/collections/ParentDocumentSelector.tsx
index 5208f65c4..31f19d42d 100644
--- a/src/client/views/collections/ParentDocumentSelector.tsx
+++ b/src/client/views/collections/ParentDocumentSelector.tsx
@@ -81,7 +81,7 @@ export class SelectorContextMenu extends React.Component<SelectorProps> {
 @observer
 export class ParentDocSelector extends React.Component<SelectorProps> {
     render() {
-        let flyout = (
+        const flyout = (
             <div className="parentDocumentSelector-flyout" style={{}} title=" ">
                 <SelectorContextMenu {...this.props} />
             </div>
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 9f67f3eab..2f1e1832e 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -3,7 +3,7 @@ import { action, observable, runInAction, reaction, IReactionDisposer, trace, un
 import { observer } from "mobx-react";
 import * as Pdfjs from "pdfjs-dist";
 import "pdfjs-dist/web/pdf_viewer.css";
-import { Opt, WidthSym, Doc } from "../../../new_fields/Doc";
+import { Opt, WidthSym, Doc, HeightSym } from "../../../new_fields/Doc";
 import { makeInterface } from "../../../new_fields/Schema";
 import { ScriptField } from '../../../new_fields/ScriptField';
 import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
@@ -49,6 +49,9 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument>
     constructor(props: any) {
         super(props);
         this._initialScale = this.props.ScreenToLocalTransform().Scale;
+        const nw = this.Document.nativeWidth = NumCast(this.extensionDocSync.nativeWidth, NumCast(this.Document.nativeWidth, 927));
+        const nh = this.Document.nativeHeight = NumCast(this.extensionDocSync.nativeHeight, NumCast(this.Document.nativeHeight, 1200));
+        !this.Document.fitWidth && !this.Document.ignoreAspect && (this.Document.height = this.Document[WidthSym]() * (nh / nw));
 
         const backup = "oldPath";
         const { Document } = this.props;
@@ -78,10 +81,6 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument>
         this._selectReactionDisposer && this._selectReactionDisposer();
     }
     componentDidMount() {
-        const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField);
-        if (pdfUrl instanceof PdfField) {
-            Pdfjs.getDocument(pdfUrl.url.href).promise.then(pdf => runInAction(() => this._pdf = pdf));
-        }
         this._selectReactionDisposer = reaction(() => this.props.isSelected(),
             () => {
                 document.removeEventListener("keydown", this.onKeyDown);
@@ -90,9 +89,9 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument>
     }
 
     loaded = (nw: number, nh: number, np: number) => {
-        this.dataDoc.numPages = np;
-        this.Document.nativeWidth = nw * 96 / 72;
-        this.Document.nativeHeight = nh * 96 / 72;
+        this.extensionDocSync.numPages = np;
+        this.extensionDocSync.nativeWidth = this.Document.nativeWidth = nw * 96 / 72;
+        this.extensionDocSync.nativeHeight = this.Document.nativeHeight = nh * 96 / 72;
         !this.Document.fitWidth && !this.Document.ignoreAspect && (this.Document.height = this.Document[WidthSym]() * (nh / nw));
     }
 
@@ -220,9 +219,9 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument>
     @computed get renderTitleBox() {
         const classname = "pdfBox" + (this.active() ? "-interactive" : "");
         return <div className={classname} style={{
-            width: !this.props.Document.fitWidth ? NumCast(this.props.Document.nativeWidth) : `${100 / this.contentScaling}%`,
-            height: !this.props.Document.fitWidth ? NumCast(this.props.Document.nativeHeight) : `${100 / this.contentScaling}%`,
-            transform: `scale(${this.props.ContentScaling()})`
+            width: !this.props.Document.fitWidth ? this.Document.nativeWidth || 0 : `${100 / this.contentScaling}%`,
+            height: !this.props.Document.fitWidth ? this.Document.nativeHeight || 0 : `${100 / this.contentScaling}%`,
+            transform: `scale(${this.contentScaling})`
         }}  >
             <div className="pdfBox-title-outer">
                 <strong className="pdfBox-title" >{this.props.Document.title}</strong>
@@ -248,10 +247,19 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument>
         </div>;
     }
 
+    _pdfjsRequested = false;
     render() {
         const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField, null);
         if (this.props.isSelected() || this.props.Document.scrollY !== undefined) this._everActive = true;
-        return !pdfUrl || !this._pdf || !this.extensionDoc || (!this._everActive && this.props.ScreenToLocalTransform().Scale > 2.5) ?
-            this.renderTitleBox : this.renderPdfView;
+        if (pdfUrl && this.extensionDoc && (this._everActive || (this.extensionDoc.nativeWidth && this.props.ScreenToLocalTransform().Scale < 2.5))) {
+            if (pdfUrl instanceof PdfField && this._pdf) {
+                return this.renderPdfView;
+            }
+            if (!this._pdfjsRequested) {
+                this._pdfjsRequested = true;
+                Pdfjs.getDocument(pdfUrl.url.href).promise.then(pdf => runInAction(() => this._pdf = pdf));
+            }
+        }
+        return this.renderTitleBox;
     }
 }
\ No newline at end of file
diff --git a/src/client/views/pdf/PDFMenu.tsx b/src/client/views/pdf/PDFMenu.tsx
index 38e10e99a..503696ae9 100644
--- a/src/client/views/pdf/PDFMenu.tsx
+++ b/src/client/views/pdf/PDFMenu.tsx
@@ -12,19 +12,17 @@ export default class PDFMenu extends AntimodeMenu {
     static Instance: PDFMenu;
 
     private _commentCont = React.createRef<HTMLButtonElement>();
-    private _snippetButton: React.RefObject<HTMLButtonElement> = React.createRef();
 
     @observable private _keyValue: string = "";
     @observable private _valueValue: string = "";
     @observable private _added: boolean = false;
 
     @observable public Highlighting: boolean = false;
-    @observable public Status: "pdf" | "annotation" | "snippet" | "" = "";
+    @observable public Status: "pdf" | "annotation" | "" = "";
 
     public StartDrag: (e: PointerEvent, ele: HTMLElement) => void = unimplementedFunction;
     public Highlight: (color: string) => Opt<Doc> = (color: string) => undefined;
     public Delete: () => void = unimplementedFunction;
-    public Snippet: (marquee: { left: number, top: number, width: number, height: number }) => void = unimplementedFunction;
     public AddTag: (key: string, value: string) => boolean = returnFalse;
     public PinToPres: () => void = unimplementedFunction;
     public Marquee: { left: number; top: number; width: number; height: number; } | undefined;
@@ -80,34 +78,6 @@ export default class PDFMenu extends AntimodeMenu {
         this.Delete();
     }
 
-    snippetStart = (e: React.PointerEvent) => {
-        document.removeEventListener("pointermove", this.snippetDrag);
-        document.addEventListener("pointermove", this.snippetDrag);
-        document.removeEventListener("pointerup", this.snippetEnd);
-        document.addEventListener("pointerup", this.snippetEnd);
-
-        e.stopPropagation();
-        e.preventDefault();
-    }
-
-    snippetDrag = (e: PointerEvent) => {
-        e.stopPropagation();
-        e.preventDefault();
-        if (!this._dragging) {
-            this._dragging = true;
-
-            this.Marquee && this.Snippet(this.Marquee);
-        }
-    }
-
-    snippetEnd = (e: PointerEvent) => {
-        this._dragging = false;
-        document.removeEventListener("pointermove", this.snippetDrag);
-        document.removeEventListener("pointerup", this.snippetEnd);
-        e.stopPropagation();
-        e.preventDefault();
-    }
-
     @action
     keyChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
         this._keyValue = e.currentTarget.value;
@@ -128,14 +98,12 @@ export default class PDFMenu extends AntimodeMenu {
     }
 
     render() {
-        const buttons = this.Status === "pdf" || this.Status === "snippet" ?
+        const buttons = this.Status === "pdf"  ?
             [
                 <button key="1" className="antimodeMenu-button" title="Click to Highlight" onClick={this.highlightClicked} style={this.Highlighting ? { backgroundColor: "#121212" } : {}}>
                     <FontAwesomeIcon icon="highlighter" size="lg" style={{ transition: "transform 0.1s", transform: this.Highlighting ? "" : "rotate(-45deg)" }} /></button>,
                 <button key="2" className="antimodeMenu-button" title="Drag to Annotate" ref={this._commentCont} onPointerDown={this.pointerDown}>
                     <FontAwesomeIcon icon="comment-alt" size="lg" /></button>,
-                <button key="3" className="antimodeMenu-button" title="Drag to Snippetize Selection" style={{ display: this.Status === "snippet" ? "" : "none" }} onPointerDown={this.snippetStart} ref={this._snippetButton}>
-                    <FontAwesomeIcon icon="cut" size="lg" /></button>,
                 <button key="4" className="antimodeMenu-button" title="Pin Menu" onClick={this.togglePin} style={this.Pinned ? { backgroundColor: "#121212" } : {}}>
                     <FontAwesomeIcon icon="thumbtack" size="lg" style={{ transition: "transform 0.1s", transform: this.Pinned ? "rotate(45deg)" : "" }} /> </button>
             ] : [
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index e34d353f4..1a09991d2 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -417,7 +417,6 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
             // clear out old marquees and initialize menu for new selection
             PDFMenu.Instance.StartDrag = this.startDrag;
             PDFMenu.Instance.Highlight = this.highlight;
-            PDFMenu.Instance.Snippet = this.createSnippet;
             PDFMenu.Instance.Status = "pdf";
             PDFMenu.Instance.fadeOut(true);
             this._savedAnnotations.values().forEach(v => v.forEach(a => a.remove()));
@@ -515,7 +514,6 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
                 }
 
                 if (!e.ctrlKey) {
-                    PDFMenu.Instance.Status = "snippet";
                     PDFMenu.Instance.Marquee = { left: this._marqueeX, top: this._marqueeY, width: this._marqueeWidth, height: this._marqueeHeight };
                 }
                 PDFMenu.Instance.jumpTo(e.clientX, e.clientY);
@@ -599,7 +597,7 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
         if (!this.props.Document[HeightSym]() || !this.props.Document.nativeHeight) {
             setTimeout((() => {
                 this.Document.height = this.Document[WidthSym]() * this._coverPath.height / this._coverPath.width;
-                this.Document.nativeHeight = nativeWidth * this._coverPath.height / this._coverPath.width;
+                this.Document.nativeHeight = (this.Document.nativeWidth || 0) * this._coverPath.height / this._coverPath.width;
             }).bind(this), 0);
         }
         const nativeWidth = (this.Document.nativeWidth || 0);
diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx
index b80c3bb54..2f28ebf76 100644
--- a/src/client/views/search/SearchBox.tsx
+++ b/src/client/views/search/SearchBox.tsx
@@ -234,8 +234,7 @@ export class SearchBox extends React.Component {
                 y += 300;
             }
         }
-        return Docs.Create.FreeformDocument(docs, { width: 400, height: 400, panX: 175, panY: 175, backgroundColor: "grey", title: `Search Docs: "${this._searchString}"` });
-
+        return Docs.Create.TreeDocument(docs, { width: 200, height: 400, backgroundColor: "grey", title: `Search Docs: "${this._searchString}"` });
     }
 
     @action.bound
@@ -341,7 +340,7 @@ export class SearchBox extends React.Component {
         return (
             <div className="searchBox-container" onPointerDown={e => { e.stopPropagation(); e.preventDefault(); }}>
                 <div className="searchBox-bar">
-                    <span className="searchBox-barChild searchBox-collection" onPointerDown={SetupDrag(this.collectionRef, this.startDragCollection)} ref={this.collectionRef} title="Drag Results as Collection">
+                    <span className="searchBox-barChild searchBox-collection" onPointerDown={SetupDrag(this.collectionRef, () => this._searchString ? this.startDragCollection() : undefined)} ref={this.collectionRef} title="Drag Results as Collection">
                         <FontAwesomeIcon icon="object-group" size="lg" />
                     </span>
                     <input value={this._searchString} onChange={this.onChange} type="text" placeholder="Search..." id="search-input" ref={this.inputRef}
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index 17c1b7e16..aa33d4c8b 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -467,9 +467,14 @@ export namespace Doc {
     // to store annotations, ink, and other data.
     //
     export function fieldExtensionDoc(doc: Doc, fieldKey: string) {
-        const extension = doc[fieldKey + "_ext"] as Doc;
-        (extension === undefined) && setTimeout(() => CreateDocumentExtensionForField(doc, fieldKey), 0);
-        return extension ? extension : undefined;
+        const extension = doc[fieldKey + "_ext"];
+        if (extension === undefined) {
+            setTimeout(() => CreateDocumentExtensionForField(doc, fieldKey), 0);
+        }
+        return extension ? extension as Doc : undefined;
+    }
+    export function fieldExtensionDocSync(doc: Doc, fieldKey: string) {
+        return (doc[fieldKey + "_ext"] as Doc) ||  CreateDocumentExtensionForField(doc, fieldKey);
     }
 
     export function CreateDocumentExtensionForField(doc: Doc, fieldKey: string) {
-- 
cgit v1.2.3-70-g09d2


From c23d9ba83c87511a626bfe8d1da5dd981e311262 Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Thu, 23 Jan 2020 18:24:38 -0500
Subject: got rid of extension docs.  changed layout-specific keys to start
 with "_" which flags them to be written to the current layout document

---
 .../apis/google_docs/GooglePhotosClientUtils.ts    |   2 +-
 src/client/apis/youtube/YoutubeBox.tsx             |   2 +-
 src/client/documents/Documents.ts                  | 109 +++++++++++----------
 src/client/util/DictationManager.ts                |   6 +-
 src/client/util/DragManager.ts                     |   2 +-
 src/client/util/DropConverter.ts                   |   3 +-
 .../util/Import & Export/DirectoryImportBox.tsx    |   4 +-
 src/client/util/RichTextRules.ts                   |   4 +-
 src/client/util/RichTextSchema.tsx                 |   4 +-
 src/client/views/DocComponent.tsx                  |   9 +-
 src/client/views/DocumentDecorations.tsx           |  40 ++++----
 src/client/views/MainView.tsx                      |  10 +-
 src/client/views/MetadataEntryMenu.tsx             |   4 +-
 src/client/views/PreviewCursor.tsx                 |   4 +-
 src/client/views/TemplateMenu.tsx                  |   4 +-
 .../views/collections/CollectionDockingView.tsx    |  20 ++--
 .../views/collections/CollectionLinearView.tsx     |  12 +--
 .../collections/CollectionMasonryViewFieldRow.tsx  |   4 +-
 .../views/collections/CollectionPivotView.tsx      |  23 ++---
 .../views/collections/CollectionSchemaView.tsx     |   2 +-
 .../views/collections/CollectionStackingView.tsx   |  34 +++----
 .../CollectionStackingViewFieldColumn.tsx          |   8 +-
 .../views/collections/CollectionStaffView.tsx      |   4 -
 src/client/views/collections/CollectionSubView.tsx |  23 ++---
 .../views/collections/CollectionTreeView.tsx       |  36 +++----
 src/client/views/collections/CollectionView.tsx    |  50 ++++------
 .../views/collections/CollectionViewChromes.tsx    |  22 ++---
 .../views/collections/ParentDocumentSelector.tsx   |  18 ++--
 .../CollectionFreeFormLayoutEngines.tsx            |   8 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 100 ++++++++++---------
 .../collections/collectionFreeForm/MarqueeView.tsx |  54 +++++-----
 src/client/views/linking/LinkEditor.tsx            |   2 +-
 src/client/views/linking/LinkFollowBox.tsx         |  48 ++++-----
 src/client/views/linking/LinkMenuGroup.tsx         |   2 +-
 src/client/views/nodes/AudioBox.tsx                | 101 +++++++++----------
 .../views/nodes/CollectionFreeFormDocumentView.tsx |  12 +--
 .../views/nodes/ContentFittingDocumentView.tsx     |  10 +-
 src/client/views/nodes/DocumentView.tsx            | 106 ++++++++++----------
 src/client/views/nodes/FormattedTextBox.tsx        |  26 +++--
 src/client/views/nodes/FormattedTextBoxComment.tsx |   2 +-
 src/client/views/nodes/IconBox.tsx                 |   2 +-
 src/client/views/nodes/ImageBox.tsx                |  89 ++++++++---------
 src/client/views/nodes/KeyValueBox.tsx             |   8 +-
 src/client/views/nodes/KeyValuePair.tsx            |   2 +-
 src/client/views/nodes/PDFBox.tsx                  |  22 ++---
 src/client/views/nodes/PresBox.tsx                 |   4 +-
 src/client/views/nodes/VideoBox.tsx                |  32 +++---
 src/client/views/nodes/WebBox.tsx                  |  14 +--
 src/client/views/pdf/Annotation.tsx                |  10 +-
 src/client/views/pdf/PDFViewer.tsx                 |  67 +++++++------
 .../views/presentationview/PresElementBox.tsx      |  10 +-
 src/client/views/search/SearchBox.tsx              |  16 +--
 src/client/views/search/SearchItem.tsx             |  10 +-
 src/mobile/ImageUpload.tsx                         |   2 +-
 src/new_fields/Doc.ts                              |  51 +++-------
 src/new_fields/RichTextUtils.ts                    |   2 +-
 src/new_fields/documentSchemas.ts                  |  13 +--
 src/new_fields/util.ts                             |  34 ++++++-
 src/scraping/buxton/scraper.py                     |  14 +--
 .../authentication/models/current_user_utils.ts    |  70 ++++++-------
 60 files changed, 696 insertions(+), 710 deletions(-)

(limited to 'src/client/util/Import & Export/DirectoryImportBox.tsx')

diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
index 966d8053a..21cd20e14 100644
--- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts
+++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
@@ -136,7 +136,7 @@ export namespace GooglePhotos {
                 document.contentSize = upload.contentSize;
                 return document;
             });
-            const options = { width: 500, height: 500 };
+            const options = { _width: 500, _height: 500 };
             return constructor(children, options);
         };
 
diff --git a/src/client/apis/youtube/YoutubeBox.tsx b/src/client/apis/youtube/YoutubeBox.tsx
index d654e2530..c940bba43 100644
--- a/src/client/apis/youtube/YoutubeBox.tsx
+++ b/src/client/apis/youtube/YoutubeBox.tsx
@@ -337,7 +337,7 @@ export class YoutubeBox extends React.Component<FieldViewProps> {
         const newVideoX = NumCast(this.props.Document.x);
         const newVideoY = NumCast(this.props.Document.y) + NumCast(this.props.Document.height);
 
-        addFunction(Docs.Create.VideoDocument(embeddedUrl, { title: filteredTitle, width: 400, height: 315, x: newVideoX, y: newVideoY }));
+        addFunction(Docs.Create.VideoDocument(embeddedUrl, { title: filteredTitle, _width: 400, _height: 315, x: newVideoX, y: newVideoY }));
         this.videoClicked = true;
     }
 
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 6132f787b..c665a7fd3 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -56,21 +56,28 @@ const requestImageSize = require('../util/request-image-size');
 const path = require('path');
 
 export interface DocumentOptions {
+    _panX?: number;
+    _panY?: number;
+    _width?: number;
+    _height?: number;
+    _nativeWidth?: number;
+    _nativeHeight?: number;
+    _fitWidth?: boolean;
+    _fitToBox?: boolean; // whether a freeformview should zoom/scale to create a shrinkwrapped view of its contents
+    _LODdisable?: boolean;
+    _dropAction?: dropActionType;
+    _chromeStatus?: string;
+    _viewType?: number;
+    _gridGap?: number; // gap between items in masonry view
+    _xMargin?: number; // gap between left edge of document and start of masonry/stacking layouts
+    _yMargin?: number; // gap between top edge of dcoument and start of masonry/stacking layouts
     x?: number;
     y?: number;
     z?: number;
     type?: string;
-    width?: number;
-    height?: number;
-    nativeWidth?: number;
-    nativeHeight?: number;
     title?: string;
-    panX?: number;
-    panY?: number;
     page?: number;
     scale?: number;
-    fitWidth?: boolean;
-    fitToBox?: boolean; // whether a freeformview should zoom/scale to create a shrinkwrapped view of its contents
     isDisplayPanel?: boolean; // whether the panel functions as GoldenLayout "stack" used to display documents
     forceActive?: boolean;
     preventTreeViewOpen?: boolean; // ignores the treeViewOpen Doc flag which allows a treeViewItem's expande/collapse state to be independent of other views of the same document in the tree view
@@ -79,16 +86,12 @@ export interface DocumentOptions {
     isTemplateForField?: string; // the field key for which the containing document is a rendering template
     isTemplateDoc?: boolean;
     templates?: List<string>;
-    viewType?: number;
     backgroundColor?: string | ScriptField;
     ignoreClick?: boolean;
     lockedPosition?: boolean; // lock the x,y coordinates of the document so that it can't be dragged
     lockedTransform?: boolean; // lock the panx,pany and scale parameters of the document so that it be panned/zoomed
     opacity?: number;
     defaultBackgroundColor?: string;
-    dropAction?: dropActionType;
-    chromeStatus?: string;
-    LODdisable?: boolean;
     columnWidth?: number;
     fontSize?: number;
     curPage?: number;
@@ -114,9 +117,6 @@ export interface DocumentOptions {
     onDragStart?: ScriptField; //script to execute at start of drag operation --  e.g., when a "creator" button is dragged this script generates a different document to drop
     clipboard?: Doc; //script to execute at start of drag operation --  e.g., when a "creator" button is dragged this script generates a different document to drop
     icon?: string;
-    gridGap?: number; // gap between items in masonry view
-    xMargin?: number; // gap between left edge of document and start of masonry/stacking layouts
-    yMargin?: number; // gap between top edge of dcoument and start of masonry/stacking layouts
     sourcePanel?: Doc; // panel to display in 'targetContainer' as the result of a button onClick script
     targetContainer?: Doc; // document whose proto will be set to 'panel' as the result of a onClick click script
     dropConverter?: ScriptField; // script to run when documents are dropped on this Document.
@@ -158,19 +158,19 @@ export namespace Docs {
         const TemplateMap: TemplateMap = new Map([
             [DocumentType.TEXT, {
                 layout: { view: FormattedTextBox, dataField: data },
-                options: { height: 150, backgroundColor: "#f1efeb", defaultBackgroundColor: "#f1efeb" }
+                options: { _height: 150, backgroundColor: "#f1efeb", defaultBackgroundColor: "#f1efeb" }
             }],
             [DocumentType.HIST, {
                 layout: { view: HistogramBox, dataField: data },
-                options: { height: 300, backgroundColor: "black" }
+                options: { _height: 300, backgroundColor: "black" }
             }],
             [DocumentType.QUERY, {
                 layout: { view: QueryBox, dataField: data },
-                options: { width: 400 }
+                options: { _width: 400 }
             }],
             [DocumentType.COLOR, {
                 layout: { view: ColorBox, dataField: data },
-                options: { nativeWidth: 220, nativeHeight: 300 }
+                options: { _nativeWidth: 220, _nativeHeight: 300 }
             }],
             [DocumentType.IMG, {
                 layout: { view: ImageBox, dataField: data },
@@ -178,19 +178,19 @@ export namespace Docs {
             }],
             [DocumentType.WEB, {
                 layout: { view: WebBox, dataField: data },
-                options: { height: 300 }
+                options: { _height: 300 }
             }],
             [DocumentType.COL, {
                 layout: { view: CollectionView, dataField: data },
-                options: { panX: 0, panY: 0, scale: 1, width: 500, height: 500 }
+                options: { _panX: 0, _panY: 0, scale: 1, _width: 500, _height: 500 }
             }],
             [DocumentType.KVP, {
                 layout: { view: KeyValueBox, dataField: data },
-                options: { height: 150 }
+                options: { _height: 150 }
             }],
             [DocumentType.DOCUMENT, {
                 layout: { view: DocumentBox, dataField: data },
-                options: { height: 250 }
+                options: { _height: 250 }
             }],
             [DocumentType.VID, {
                 layout: { view: VideoBox, dataField: data },
@@ -198,7 +198,7 @@ export namespace Docs {
             }],
             [DocumentType.AUDIO, {
                 layout: { view: AudioBox, dataField: data },
-                options: { height: 35, backgroundColor: "lightGray" }
+                options: { _height: 35, backgroundColor: "lightGray" }
             }],
             [DocumentType.PDF, {
                 layout: { view: PDFBox, dataField: data },
@@ -206,11 +206,11 @@ export namespace Docs {
             }],
             [DocumentType.ICON, {
                 layout: { view: IconBox, dataField: data },
-                options: { width: Number(MINIMIZED_ICON_SIZE), height: Number(MINIMIZED_ICON_SIZE) },
+                options: { _width: Number(MINIMIZED_ICON_SIZE), _height: Number(MINIMIZED_ICON_SIZE) },
             }],
             [DocumentType.IMPORT, {
                 layout: { view: DirectoryImportBox, dataField: data },
-                options: { height: 150 }
+                options: { _height: 150 }
             }],
             [DocumentType.LINKDOC, {
                 data: new List<Doc>(),
@@ -228,7 +228,7 @@ export namespace Docs {
             }],
             [DocumentType.FONTICON, {
                 layout: { view: FontIconBox, dataField: data },
-                options: { width: 40, height: 40, borderRounding: "100%" },
+                options: { _width: 40, _height: 40, borderRounding: "100%" },
             }],
             [DocumentType.LINKFOLLOW, {
                 layout: { view: LinkFollowBox, dataField: data }
@@ -243,7 +243,7 @@ export namespace Docs {
         ]);
 
         // All document prototypes are initialized with at least these values
-        const defaultOptions: DocumentOptions = { x: 0, y: 0, width: 300 };
+        const defaultOptions: DocumentOptions = { x: 0, y: 0, _width: 300 };
         const suffix = "Proto";
 
         /**
@@ -321,7 +321,8 @@ export namespace Docs {
             // whatever options pertain to this specific prototype
             const options = { title, type, baseProto: true, ...defaultOptions, ...(template.options || {}) };
             options.layout = layout.view.LayoutString(layout.dataField);
-            return Doc.assign(new Doc(prototypeId, true), { ...options });
+            let doc = Doc.assign(new Doc(prototypeId, true), { layoutKey: "layout", ...options });
+            return doc;
         }
 
     }
@@ -332,7 +333,7 @@ export namespace Docs {
      */
     export namespace Create {
 
-        const delegateKeys = ["x", "y", "width", "height", "panX", "panY", "nativeWidth", "nativeHeight", "dropAction", "annotationOn", "forceActive", "fitWidth", "diableLOD"];
+        const delegateKeys = ["x", "y", "_width", "_height", "_panX", "_panY", "_viewType", "_nativeWidth", "_nativeHeight", "_dropAction", "_annotationOn", "_chromeStatus", "_forceActive", "_fitWidth", "_LODdisable"];
 
         /**
          * This function receives the relevant document prototype and uses
@@ -402,11 +403,11 @@ export namespace Docs {
             requestImageSize(target)
                 .then((size: any) => {
                     const aspect = size.height / size.width;
-                    if (!inst.nativeWidth) {
-                        inst.nativeWidth = size.width;
+                    if (!inst._nativeWidth) {
+                        inst._nativeWidth = size.width;
                     }
-                    inst.nativeHeight = NumCast(inst.nativeWidth) * aspect;
-                    inst.height = NumCast(inst.width) * aspect;
+                    inst._nativeHeight = NumCast(inst._nativeWidth) * aspect;
+                    inst._height = NumCast(inst._width) * aspect;
                 })
                 .catch((err: any) => console.log(err));
             // }
@@ -465,7 +466,7 @@ export namespace Docs {
             const ctlog = await Gateway.Instance.GetSchema(url, schemaName);
             if (ctlog && ctlog.schemas) {
                 const schema = ctlog.schemas[0];
-                const schemaDoc = Docs.Create.TreeDocument([], { ...options, nativeWidth: undefined, nativeHeight: undefined, width: 150, height: 100, title: schema.displayName! });
+                const schemaDoc = Docs.Create.TreeDocument([], { ...options, _nativeWidth: undefined, _nativeHeight: undefined, _width: 150, _height: 100, title: schema.displayName! });
                 const schemaDocuments = Cast(schemaDoc.data, listSpec(Doc), []);
                 if (!schemaDocuments) {
                     return;
@@ -482,13 +483,13 @@ export namespace Docs {
                                 new AttributeTransformationModel(atmod, AggregateFunction.None),
                                 new AttributeTransformationModel(atmod, AggregateFunction.Count),
                                 new AttributeTransformationModel(atmod, AggregateFunction.Count));
-                            docs.push(Docs.Create.HistogramDocument(histoOp, { ...columnOptions, width: 200, height: 200, title: attr.displayName! }));
+                            docs.push(Docs.Create.HistogramDocument(histoOp, { ...columnOptions, _width: 200, _height: 200, title: attr.displayName! }));
                         }
                     }));
                 });
                 return schemaDoc;
             }
-            return Docs.Create.TreeDocument([], { width: 50, height: 100, title: schemaName });
+            return Docs.Create.TreeDocument([], { _width: 50, _height: 100, title: schemaName });
         }
 
         export function WebDocument(url: string, options: DocumentOptions = {}) {
@@ -508,31 +509,31 @@ export namespace Docs {
         }
 
         export function FreeformDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) {
-            return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { chromeStatus: "collapsed", schemaColumns: new List([new SchemaHeaderField("title", "#f1efeb")]), ...options, viewType: CollectionViewType.Freeform }, id);
+            return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", schemaColumns: new List([new SchemaHeaderField("title", "#f1efeb")]), ...options, _viewType: CollectionViewType.Freeform }, id);
         }
 
         export function LinearDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) {
-            return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { chromeStatus: "collapsed", backgroundColor: "black", schemaColumns: new List([new SchemaHeaderField("title", "#f1efeb")]), ...options, viewType: CollectionViewType.Linear }, id);
+            return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", backgroundColor: "black", schemaColumns: new List([new SchemaHeaderField("title", "#f1efeb")]), ...options, _viewType: CollectionViewType.Linear }, id);
         }
 
         export function SchemaDocument(schemaColumns: SchemaHeaderField[], documents: Array<Doc>, options: DocumentOptions) {
-            return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { chromeStatus: "collapsed", schemaColumns: new List(schemaColumns), ...options, viewType: CollectionViewType.Schema });
+            return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", schemaColumns: new List(schemaColumns), ...options, _viewType: CollectionViewType.Schema });
         }
 
         export function TreeDocument(documents: Array<Doc>, options: DocumentOptions) {
-            return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { chromeStatus: "collapsed", schemaColumns: new List([new SchemaHeaderField("title", "#f1efeb")]), ...options, viewType: CollectionViewType.Tree });
+            return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", schemaColumns: new List([new SchemaHeaderField("title", "#f1efeb")]), ...options, _viewType: CollectionViewType.Tree });
         }
 
         export function StackingDocument(documents: Array<Doc>, options: DocumentOptions) {
-            return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { chromeStatus: "collapsed", schemaColumns: new List([new SchemaHeaderField("title", "#f1efeb")]), ...options, viewType: CollectionViewType.Stacking });
+            return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", schemaColumns: new List([new SchemaHeaderField("title", "#f1efeb")]), ...options, _viewType: CollectionViewType.Stacking });
         }
 
         export function MulticolumnDocument(documents: Array<Doc>, options: DocumentOptions) {
-            return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { chromeStatus: "collapsed", schemaColumns: new List([new SchemaHeaderField("title", "#f1efeb")]), ...options, viewType: CollectionViewType.Multicolumn });
+            return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", schemaColumns: new List([new SchemaHeaderField("title", "#f1efeb")]), ...options, _viewType: CollectionViewType.Multicolumn });
         }
 
         export function MasonryDocument(documents: Array<Doc>, options: DocumentOptions) {
-            return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { chromeStatus: "collapsed", schemaColumns: new List([new SchemaHeaderField("title", "#f1efeb")]), ...options, viewType: CollectionViewType.Masonry });
+            return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", schemaColumns: new List([new SchemaHeaderField("title", "#f1efeb")]), ...options, _viewType: CollectionViewType.Masonry });
         }
 
         export function ButtonDocument(options?: DocumentOptions) {
@@ -553,7 +554,7 @@ export namespace Docs {
         }
 
         export function DockDocument(documents: Array<Doc>, config: string, options: DocumentOptions, id?: string) {
-            const inst = InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, viewType: CollectionViewType.Docking, dockingConfig: config }, id);
+            const inst = InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Docking, dockingConfig: config }, id);
             Doc.GetProto(inst).data = new List<Doc>(documents);
             return inst;
         }
@@ -714,24 +715,24 @@ export namespace Docs {
             let ctor: ((path: string, options: DocumentOptions) => (Doc | Promise<Doc | undefined>)) | undefined = undefined;
             if (type.indexOf("image") !== -1) {
                 ctor = Docs.Create.ImageDocument;
-                if (!options.width) options.width = 300;
+                if (!options._width) options._width = 300;
             }
             if (type.indexOf("video") !== -1) {
                 ctor = Docs.Create.VideoDocument;
-                if (!options.width) options.width = 600;
-                if (!options.height) options.height = options.width * 2 / 3;
+                if (!options._width) options._width = 600;
+                if (!options._height) options._height = options._width * 2 / 3;
             }
             if (type.indexOf("audio") !== -1) {
                 ctor = Docs.Create.AudioDocument;
             }
             if (type.indexOf("pdf") !== -1) {
                 ctor = Docs.Create.PdfDocument;
-                if (!options.width) options.width = 400;
-                if (!options.height) options.height = options.width * 1200 / 927;
+                if (!options._width) options._width = 400;
+                if (!options._height) options._height = options._width * 1200 / 927;
             }
             if (type.indexOf("excel") !== -1) {
                 ctor = Docs.Create.DBDocument;
-                options.dropAction = "copy";
+                options._dropAction = "copy";
             }
             if (type.indexOf("html") !== -1) {
                 if (path.includes(window.location.hostname)) {
@@ -742,15 +743,15 @@ export namespace Docs {
                             const alias = Doc.MakeAlias(field);
                             alias.x = options.x || 0;
                             alias.y = options.y || 0;
-                            alias.width = options.width || 300;
-                            alias.height = options.height || options.width || 300;
+                            alias._width = options._width || 300;
+                            alias._height = options._height || options._width || 300;
                             return alias;
                         }
                         return undefined;
                     });
                 }
                 ctor = Docs.Create.WebDocument;
-                options = { height: options.width, ...options, title: path, nativeWidth: undefined };
+                options = { _height: options._width, ...options, title: path, _nativeWidth: undefined };
             }
             return ctor ? ctor(path, options) : undefined;
         }
diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts
index 3d8f2d234..a99e3bc4f 100644
--- a/src/client/util/DictationManager.ts
+++ b/src/client/util/DictationManager.ts
@@ -325,14 +325,14 @@ export namespace DictationManager {
 
             ["open fields", {
                 action: (target: DocumentView) => {
-                    const kvp = Docs.Create.KVPDocument(target.props.Document, { width: 300, height: 300 });
+                    const kvp = Docs.Create.KVPDocument(target.props.Document, { _width: 300, _height: 300 });
                     target.props.addDocTab(kvp, target.props.DataDoc, "onRight");
                 }
             }],
 
             ["new outline", {
                 action: (target: DocumentView) => {
-                    const newBox = Docs.Create.TextDocument({ width: 400, height: 200, title: "My Outline" });
+                    const newBox = Docs.Create.TextDocument({ _width: 400, _height: 200, title: "My Outline" });
                     newBox.autoHeight = true;
                     const proto = newBox.proto!;
                     const prompt = "Press alt + r to start dictating here...";
@@ -379,7 +379,7 @@ export namespace DictationManager {
                 expression: /view as (freeform|stacking|masonry|schema|tree)/g,
                 action: (target: DocumentView, matches: RegExpExecArray) => {
                     const mode = CollectionViewType.valueOf(matches[1]);
-                    mode && (target.props.Document.viewType = mode);
+                    mode && (target.props.Document._viewType = mode);
                 },
                 restrictTo: [DocumentType.COL]
             }
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index df2f5fe3c..601a7e856 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -205,7 +205,7 @@ export namespace DragManager {
     // drag a button template and drop a new button 
     export function StartButtonDrag(eles: HTMLElement[], script: string, title: string, vars: { [name: string]: Field }, params: string[], initialize: (button: Doc) => void, downX: number, downY: number, options?: DragOptions) {
         const finishDrag = (e: DragCompleteEvent) => {
-            const bd = Docs.Create.ButtonDocument({ width: 150, height: 50, title: title });
+            const bd = Docs.Create.ButtonDocument({ _width: 150, _height: 50, title: title });
             bd.onClick = ScriptField.MakeScript(script);
             params.map(p => Object.keys(vars).indexOf(p) !== -1 && (Doc.GetProto(bd)[p] = new PrefetchProxy(vars[p] as Doc)));
             initialize && initialize(bd);
diff --git a/src/client/util/DropConverter.ts b/src/client/util/DropConverter.ts
index 66caf52cb..9c068d2d7 100644
--- a/src/client/util/DropConverter.ts
+++ b/src/client/util/DropConverter.ts
@@ -1,5 +1,4 @@
 import { DragManager } from "./DragManager";
-import { CollectionViewType } from "../views/collections/CollectionView";
 import { Doc, DocListCast } from "../../new_fields/Doc";
 import { DocumentType } from "../documents/DocumentTypes";
 import { ObjectField } from "../../new_fields/ObjectField";
@@ -34,7 +33,7 @@ export function convertDropDataToButtons(data: DragManager.DocumentDragData) {
             } else {
                 layoutDoc.isTemplateDoc = (layoutDoc.type === DocumentType.TEXT || layoutDoc.layout instanceof Doc) && !data.userDropAction;
             }
-            dbox = Docs.Create.FontIconDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, backgroundColor: StrCast(doc.backgroundColor), title: "Custom", icon: layoutDoc.isTemplateDoc ? "font" : "bolt" });
+            dbox = Docs.Create.FontIconDocument({ _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100, backgroundColor: StrCast(doc.backgroundColor), title: "Custom", icon: layoutDoc.isTemplateDoc ? "font" : "bolt" });
             dbox.dragFactory = layoutDoc;
             dbox.removeDropProperties = doc.removeDropProperties instanceof ObjectField ? ObjectField.MakeCopy(doc.removeDropProperties) : undefined;
             dbox.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)');
diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx
index 5b5bffd8c..c7c94abed 100644
--- a/src/client/util/Import & Export/DirectoryImportBox.tsx	
+++ b/src/client/util/Import & Export/DirectoryImportBox.tsx	
@@ -145,8 +145,8 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
         const offset: number = this.persistent ? (height === 0 ? 0 : height + 30) : 0;
         const options: DocumentOptions = {
             title: `Import of ${directory}`,
-            width: 1105,
-            height: 500,
+            _width: 1105,
+            _height: 500,
             x: NumCast(doc.x),
             y: NumCast(doc.y) + offset
         };
diff --git a/src/client/util/RichTextRules.ts b/src/client/util/RichTextRules.ts
index 0ee9481f0..fb22e0d18 100644
--- a/src/client/util/RichTextRules.ts
+++ b/src/client/util/RichTextRules.ts
@@ -79,7 +79,7 @@ export const inpRules = {
                 const value = state.doc.textBetween(start, end);
                 if (value) {
                     DocServer.GetRefField(value).then(docx => {
-                        const target = ((docx instanceof Doc) && docx) || Docs.Create.FreeformDocument([], { title: value, width: 500, height: 500, }, value);
+                        const target = ((docx instanceof Doc) && docx) || Docs.Create.FreeformDocument([], { title: value, _width: 500, _height: 500, }, value);
                         DocUtils.Publish(target, value, returnFalse, returnFalse);
                         DocUtils.MakeLink({ doc: (schema as any).Document }, { doc: target }, "portal link", "");
                     });
@@ -196,7 +196,7 @@ export const inpRules = {
         new InputRule(
             new RegExp(/%#$/),
             (state, match, start, end) => {
-                const target = Docs.Create.TextDocument({ width: 75, height: 35, backgroundColor: "yellow", annotationOn: FormattedTextBox.FocusedBox!.dataDoc, autoHeight: true, fontSize: 9, title: "inline comment" });
+                const target = Docs.Create.TextDocument({ _width: 75, _height: 35, backgroundColor: "yellow", annotationOn: FormattedTextBox.FocusedBox!.dataDoc, autoHeight: true, fontSize: 9, title: "inline comment" });
                 const node = (state.doc.resolve(start) as any).nodeAfter;
                 const newNode = schema.nodes.dashComment.create({ docid: target[Id] });
                 const dashDoc = schema.nodes.dashDoc.create({ width: 75, height: 35, title: "dashDoc", docid: target[Id], float: "right" });
diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index fe7ab31a8..d64098e5f 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -703,7 +703,7 @@ export class DashDocView {
         const { scale, translateX, translateY } = Utils.GetScreenTransform(this._outer);
         return new Transform(-translateX, -translateY, 1).scale(1 / this.contentScaling() / scale);
     }
-    contentScaling = () => NumCast(this._dashDoc!.nativeWidth) > 0 && !this._dashDoc!.ignoreAspect ? this._dashDoc![WidthSym]() / NumCast(this._dashDoc!.nativeWidth) : 1;
+    contentScaling = () => NumCast(this._dashDoc!._nativeWidth) > 0 && !this._dashDoc!.ignoreAspect ? this._dashDoc![WidthSym]() / NumCast(this._dashDoc!._nativeWidth) : 1;
     outerFocus = (target: Doc) => this._textBox.props.focus(this._textBox.props.Document);  // ideally, this would scroll to show the focus target
     constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) {
         this._textBox = tbox;
@@ -758,7 +758,7 @@ export class DashDocView {
                 ReactDOM.render(<DocumentView
                     Document={dashDoc}
                     LibraryPath={tbox.props.LibraryPath}
-                    fitToBox={BoolCast(dashDoc.fitToBox)}
+                    fitToBox={BoolCast(dashDoc._fitToBox)}
                     addDocument={returnFalse}
                     removeDocument={removeDoc}
                     ScreenToLocalTransform={this.getDocTransform}
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index 1ffbd80ca..6d185ff87 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -35,7 +35,6 @@ export function DocExtendableComponent<P extends DocExtendableProps, T>(schemaCt
         @computed get Document(): T { return schemaCtor(this.props.Document); }
         @computed get layoutDoc() { return Doc.Layout(this.props.Document); }
         @computed get dataDoc() { return (this.props.DataDoc && (this.props.Document.isTemplateForField || this.props.Document.isTemplateDoc) ? this.props.DataDoc : Doc.GetProto(this.props.Document)) as Doc; }
-        @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); }
         active = (outsideReaction?: boolean) => !this.props.Document.isBackground && (this.props.Document.forceActive || this.props.isSelected(outsideReaction) || this.props.renderDepth === 0);//  && !InkingControl.Instance.selectedTool;  // bcz: inking state shouldn't affect static tools 
     }
     return Component;
@@ -59,14 +58,12 @@ export function DocAnnotatableComponent<P extends DocAnnotatableProps, T>(schema
         @computed get Document(): T { return schemaCtor(this.props.Document); }
         @computed get layoutDoc() { return Doc.Layout(this.props.Document); }
         @computed get dataDoc() { return (this.props.DataDoc && (this.props.Document.isTemplateForField || this.props.Document.isTemplateDoc) ? this.props.DataDoc : Doc.GetProto(this.props.Document)) as Doc; }
-        @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); }
-        @computed get extensionDocSync() { return Doc.fieldExtensionDocSync(this.dataDoc, this.props.fieldKey); }
-        @computed get annotationsKey() { return "annotations"; }
+        @computed get annotationsKey() { return this.props.fieldKey+"-annotations"; }
 
         @action.bound
         removeDocument(doc: Doc): boolean {
             Doc.GetProto(doc).annotationOn = undefined;
-            const value = this.extensionDoc && Cast(this.extensionDoc[this.annotationsKey], listSpec(Doc), []);
+            const value = Cast(this.dataDoc[this.props.fieldKey+"-"+this.annotationsKey], listSpec(Doc), []);
             const index = value ? Doc.IndexOf(doc, value.map(d => d as Doc), true) : -1;
             return index !== -1 && value && value.splice(index, 1) ? true : false;
         }
@@ -79,7 +76,7 @@ export function DocAnnotatableComponent<P extends DocAnnotatableProps, T>(schema
         @action.bound
         addDocument(doc: Doc): boolean {
             Doc.GetProto(doc).annotationOn = this.props.Document;
-            return this.extensionDoc && Doc.AddDocToList(this.extensionDoc, this.annotationsKey, doc) ? true : false;
+            return Doc.AddDocToList(this.dataDoc, this.annotationsKey, doc) ? true : false;
         }
 
         whenActiveChanged = action((isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive));
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index e6a119d1f..095b9b17c 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -317,8 +317,8 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
 
         IconBox.AutomaticTitle(iconDoc);
         //iconDoc.proto![this._fieldKey] = selected.length > 1 ? "collection" : undefined;
-        iconDoc.width = Number(MINIMIZED_ICON_SIZE);
-        iconDoc.height = Number(MINIMIZED_ICON_SIZE);
+        iconDoc._width = Number(MINIMIZED_ICON_SIZE);
+        iconDoc._height = Number(MINIMIZED_ICON_SIZE);
         iconDoc.x = NumCast(doc.x);
         iconDoc.y = NumCast(doc.y) - 24;
         iconDoc.maximizedDocs = new List<Doc>(selected.map(s => s.props.Document));
@@ -454,10 +454,10 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
             if (dX !== 0 || dY !== 0 || dW !== 0 || dH !== 0) {
                 const doc = PositionDocument(element.props.Document);
                 const layoutDoc = PositionDocument(Doc.Layout(element.props.Document));
-                let nwidth = layoutDoc.nativeWidth || 0;
-                let nheight = layoutDoc.nativeHeight || 0;
-                const width = (layoutDoc.width || 0);
-                const height = (layoutDoc.height || (nheight / nwidth * width));
+                let nwidth = layoutDoc._nativeWidth || 0;
+                let nheight = layoutDoc._nativeHeight || 0;
+                const width = (layoutDoc._width || 0);
+                const height = (layoutDoc._height || (nheight / nwidth * width));
                 const scale = element.props.ScreenToLocalTransform().Scale * element.props.ContentScaling();
                 const actualdW = Math.max(width + (dW * scale), 20);
                 const actualdH = Math.max(height + (dH * scale), 20);
@@ -466,33 +466,33 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
                 const fixedAspect = e.ctrlKey || (!layoutDoc.ignoreAspect && nwidth && nheight);
                 if (fixedAspect && e.ctrlKey && layoutDoc.ignoreAspect) {
                     layoutDoc.ignoreAspect = false;
-                    layoutDoc.nativeWidth = nwidth = layoutDoc.width || 0;
-                    layoutDoc.nativeHeight = nheight = layoutDoc.height || 0;
+                    layoutDoc._nativeWidth = nwidth = layoutDoc._width || 0;
+                    layoutDoc._nativeHeight = nheight = layoutDoc._height || 0;
                 }
                 if (fixedAspect && (!nwidth || !nheight)) {
-                    layoutDoc.nativeWidth = nwidth = layoutDoc.width || 0;
-                    layoutDoc.nativeHeight = nheight = layoutDoc.height || 0;
+                    layoutDoc._nativeWidth = nwidth = layoutDoc._width || 0;
+                    layoutDoc._nativeHeight = nheight = layoutDoc._height || 0;
                 }
                 if (nwidth > 0 && nheight > 0 && !layoutDoc.ignoreAspect) {
                     if (Math.abs(dW) > Math.abs(dH)) {
                         if (!fixedAspect) {
-                            layoutDoc.nativeWidth = actualdW / (layoutDoc.width || 1) * (layoutDoc.nativeWidth || 0);
+                            layoutDoc._nativeWidth = actualdW / (layoutDoc._width || 1) * (layoutDoc._nativeWidth || 0);
                         }
-                        layoutDoc.width = actualdW;
-                        if (fixedAspect && !layoutDoc.fitWidth) layoutDoc.height = nheight / nwidth * layoutDoc.width;
-                        else layoutDoc.height = actualdH;
+                        layoutDoc._width = actualdW;
+                        if (fixedAspect && !layoutDoc._fitWidth) layoutDoc._height = nheight / nwidth * layoutDoc._width;
+                        else layoutDoc._height = actualdH;
                     }
                     else {
                         if (!fixedAspect) {
-                            layoutDoc.nativeHeight = actualdH / (layoutDoc.height || 1) * (doc.nativeHeight || 0);
+                            layoutDoc._nativeHeight = actualdH / (layoutDoc._height || 1) * (doc._nativeHeight || 0);
                         }
-                        layoutDoc.height = actualdH;
-                        if (fixedAspect && !layoutDoc.fitWidth) layoutDoc.width = nwidth / nheight * layoutDoc.height;
-                        else layoutDoc.width = actualdW;
+                        layoutDoc._height = actualdH;
+                        if (fixedAspect && !layoutDoc._fitWidth) layoutDoc._width = nwidth / nheight * layoutDoc._height;
+                        else layoutDoc._width = actualdW;
                     }
                 } else {
-                    dW && (layoutDoc.width = actualdW);
-                    dH && (layoutDoc.height = actualdH);
+                    dW && (layoutDoc._width = actualdW);
+                    dH && (layoutDoc._height = actualdH);
                     dH && layoutDoc.autoHeight && (layoutDoc.autoHeight = false);
                 }
             }
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index cd4e9a1a6..2297c2bb3 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -176,9 +176,9 @@ export class MainView extends React.Component {
         } else {
             if (received && this._urlState.sharing) {
                 reaction(() => CollectionDockingView.Instance && CollectionDockingView.Instance.initialized,
-                    initialized => initialized && received && DocServer.GetRefField(received).then(field => {
-                        if (field instanceof Doc && field.viewType !== CollectionViewType.Docking) {
-                            CollectionDockingView.AddRightSplit(field, undefined);
+                    initialized => initialized && received && DocServer.GetRefField(received).then(docField => {
+                        if (docField instanceof Doc && docField._viewType !== CollectionViewType.Docking) {
+                            CollectionDockingView.AddRightSplit(docField, undefined);
                         }
                     }),
                 );
@@ -199,8 +199,8 @@ export class MainView extends React.Component {
         const freeformOptions: DocumentOptions = {
             x: 0,
             y: 400,
-            width: this._panelWidth * .7,
-            height: this._panelHeight,
+            _width: this._panelWidth * .7,
+            _height: this._panelHeight,
             title: "Collection " + workspaceCount,
             backgroundColor: "white"
         };
diff --git a/src/client/views/MetadataEntryMenu.tsx b/src/client/views/MetadataEntryMenu.tsx
index 243cdb8f6..a48e8f508 100644
--- a/src/client/views/MetadataEntryMenu.tsx
+++ b/src/client/views/MetadataEntryMenu.tsx
@@ -178,11 +178,11 @@ export class MetadataEntryMenu extends React.Component<MetadataEntryProps>{
         }
         docSource = docSource as Doc[] | Doc;
         if (docSource instanceof Doc) {
-            if (docSource.viewType === undefined) {
+            if (docSource._viewType === undefined) {
                 return (null);
             }
         } else if (Array.isArray(docSource)) {
-            if (!docSource.every(doc => doc.viewType !== undefined)) {
+            if (!docSource.every(doc => doc._viewType !== undefined)) {
                 return null;
             }
         }
diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx
index 9706d0f99..831d9ca0b 100644
--- a/src/client/views/PreviewCursor.tsx
+++ b/src/client/views/PreviewCursor.tsx
@@ -31,8 +31,8 @@ export class PreviewCursor extends React.Component<{}> {
                 if (e.clipboardData.getData("text/plain").indexOf("www.youtube.com/watch") !== -1) {
                     const url = e.clipboardData.getData("text/plain").replace("youtube.com/watch?v=", "youtube.com/embed/");
                     return PreviewCursor._addDocument(Docs.Create.VideoDocument(url, {
-                        title: url, width: 400, height: 315,
-                        nativeWidth: 600, nativeHeight: 472.5,
+                        title: url, _width: 400, _height: 315,
+                        _nativeWidth: 600, _nativeHeight: 472.5,
                         x: newPoint[0], y: newPoint[1]
                     }));
                 }
diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx
index d953d3bab..d6cc394d8 100644
--- a/src/client/views/TemplateMenu.tsx
+++ b/src/client/views/TemplateMenu.tsx
@@ -104,7 +104,7 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
     toggleChrome = (): void => {
         this.props.docs.map(dv => {
             const layout = Doc.Layout(dv.Document);
-            layout.chromeStatus = (layout.chromeStatus !== "disabled" ? "disabled" : "enabled");
+            layout._chromeStatus = (layout._chromeStatus !== "disabled" ? "disabled" : "enabled");
         });
     }
     onAliasButtonUp = (e: PointerEvent): void => {
@@ -150,7 +150,7 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
         templateMenu.push(<OtherToggle key={"float"} name={"Float"} checked={this.props.docs[0].Document.z ? true : false} toggle={this.toggleFloat} />);
         templateMenu.push(<OtherToggle key={"custom"} name={"Custom"} checked={StrCast(this.props.docs[0].Document.layoutKey, "layout") !== "layout"} toggle={this.toggleCustom} />);
         templateMenu.push(<OtherToggle key={"narrative"} name={"Narrative"} checked={StrCast(this.props.docs[0].Document.layoutKey, "layout") === "layout_narrative"} toggle={this.toggleNarrative} />);
-        templateMenu.push(<OtherToggle key={"chrome"} name={"Chrome"} checked={layout.chromeStatus !== "disabled"} toggle={this.toggleChrome} />);
+        templateMenu.push(<OtherToggle key={"chrome"} name={"Chrome"} checked={layout._chromeStatus !== "disabled"} toggle={this.toggleChrome} />);
         return (
             <Flyout anchorPoint={anchorPoints.LEFT_TOP}
                 content={<ul className="template-list" ref={this._dragRef} style={{ display: "block" }}>
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 125cd828f..75e43a875 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -235,7 +235,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
             if (found) {
                 Doc.GetProto(found.props.Document).data = new List<Doc>([document]);
             } else {
-                const stackView = Docs.Create.FreeformDocument([document], { fitToBox: true, isDisplayPanel: true, title: "document viewer" });
+                const stackView = Docs.Create.FreeformDocument([document], { _fitToBox: true, isDisplayPanel: true, title: "document viewer" });
                 CollectionDockingView.AddRightSplit(stackView, undefined, []);
             }
         }
@@ -505,7 +505,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
         stack.header.element[0].style.backgroundColor = DocServer.Control.isReadOnly() ? "#228540" : undefined;
         stack.header.element.on('mousedown', (e: any) => {
             if (e.target === stack.header.element[0] && e.button === 1) {
-                this.AddTab(stack, Docs.Create.FreeformDocument([], { width: this.props.PanelWidth(), height: this.props.PanelHeight(), title: "Untitled Collection" }), undefined);
+                this.AddTab(stack, Docs.Create.FreeformDocument([], { _width: this.props.PanelWidth(), _height: this.props.PanelHeight(), title: "Untitled Collection" }), undefined);
             }
         });
 
@@ -665,19 +665,19 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
     }
 
     get layoutDoc() { return this._document && Doc.Layout(this._document); }
-    panelWidth = () => this.layoutDoc && this.layoutDoc.maxWidth ? Math.min(Math.max(NumCast(this.layoutDoc.width), NumCast(this.layoutDoc.nativeWidth)), this._panelWidth) : this._panelWidth;
+    panelWidth = () => this.layoutDoc && this.layoutDoc.maxWidth ? Math.min(Math.max(NumCast(this.layoutDoc._width), NumCast(this.layoutDoc._nativeWidth)), this._panelWidth) : this._panelWidth;
     panelHeight = () => this._panelHeight;
 
-    nativeWidth = () => !this.layoutDoc!.ignoreAspect && !this.layoutDoc!.fitWidth ? NumCast(this.layoutDoc!.nativeWidth) || this._panelWidth : 0;
-    nativeHeight = () => !this.layoutDoc!.ignoreAspect && !this.layoutDoc!.fitWidth ? NumCast(this.layoutDoc!.nativeHeight) || this._panelHeight : 0;
+    nativeWidth = () => !this.layoutDoc!.ignoreAspect && !this.layoutDoc!._fitWidth ? NumCast(this.layoutDoc!._nativeWidth) || this._panelWidth : 0;
+    nativeHeight = () => !this.layoutDoc!.ignoreAspect && !this.layoutDoc!._fitWidth ? NumCast(this.layoutDoc!._nativeHeight) || this._panelHeight : 0;
 
     contentScaling = () => {
         if (this.layoutDoc!.type === DocumentType.PDF) {
-            if ((this.layoutDoc && this.layoutDoc.fitWidth) ||
-                this._panelHeight / NumCast(this.layoutDoc!.nativeHeight) > this._panelWidth / NumCast(this.layoutDoc!.nativeWidth)) {
-                return this._panelWidth / NumCast(this.layoutDoc!.nativeWidth);
+            if ((this.layoutDoc && this.layoutDoc._fitWidth) ||
+                this._panelHeight / NumCast(this.layoutDoc!._nativeHeight) > this._panelWidth / NumCast(this.layoutDoc!._nativeWidth)) {
+                return this._panelWidth / NumCast(this.layoutDoc!._nativeWidth);
             } else {
-                return this._panelHeight / NumCast(this.layoutDoc!.nativeHeight);
+                return this._panelHeight / NumCast(this.layoutDoc!._nativeHeight);
             }
         }
         const nativeH = this.nativeHeight();
@@ -745,7 +745,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
             (<div className="collectionDockingView-content" ref={ref => this._mainCont = ref}
                 style={{
                     transform: `translate(${this.previewPanelCenteringOffset}px, 0px)`,
-                    height: this.layoutDoc && this.layoutDoc.fitWidth ? undefined : "100%",
+                    height: this.layoutDoc && this.layoutDoc._fitWidth ? undefined : "100%",
                     width: this.widthpercent
                 }}>
                 {this.docView}
diff --git a/src/client/views/collections/CollectionLinearView.tsx b/src/client/views/collections/CollectionLinearView.tsx
index 8bf6b4202..67062ae41 100644
--- a/src/client/views/collections/CollectionLinearView.tsx
+++ b/src/client/views/collections/CollectionLinearView.tsx
@@ -38,8 +38,8 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) {
 
     componentDidMount() {
         // is there any reason this needs to exist? -syip.  yes, it handles autoHeight for stacking views (masonry isn't yet supported).
-        this._widthDisposer = reaction(() => NumCast(this.props.Document.height, 0) + this.childDocs.length + (this.props.Document.isExpanded ? 1 : 0),
-            () => this.props.Document.width = 5 + (this.props.Document.isExpanded ? this.childDocs.length * (this.props.Document[HeightSym]()) : 10),
+        this._widthDisposer = reaction(() => this.props.Document[HeightSym]() + this.childDocs.length + (this.props.Document.isExpanded ? 1 : 0),
+            () => this.props.Document._width = 5 + (this.props.Document.isExpanded ? this.childDocs.length * (this.props.Document[HeightSym]()) : 10),
             { fireImmediately: true }
         );
 
@@ -73,7 +73,7 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) {
 
     public isCurrent(doc: Doc) { return !doc.isMinimized && (Math.abs(NumCast(doc.displayTimecode, -1) - NumCast(this.Document.currentTimecode, -1)) < 1.5 || NumCast(doc.displayTimecode, -1) === -1); }
 
-    dimension = () => NumCast(this.props.Document.height); // 2 * the padding
+    dimension = () => NumCast(this.props.Document._height); // 2 * the padding
     getTransform = (ele: React.RefObject<HTMLDivElement>) => () => {
         if (!ele.current) return Transform.Identity();
         const { scale, translateX, translateY } = Utils.GetScreenTransform(ele.current);
@@ -88,11 +88,11 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) {
                     onChange={action((e: any) => this.props.Document.isExpanded = this.addMenuToggle.current!.checked)} />
                 <label htmlFor={`${guid}`} style={{ marginTop: "auto", marginBottom: "auto", background: StrCast(this.props.Document.backgroundColor, "black") === StrCast(this.props.Document.color, "white") ? "black" : StrCast(this.props.Document.backgroundColor, "black") }} title="Close Menu"><p>+</p></label>
 
-                <div className="collectionLinearView-content" style={{ height: this.dimension(), width: NumCast(this.props.Document.width, 25) }}>
+                <div className="collectionLinearView-content" style={{ height: this.dimension(), width: NumCast(this.props.Document._width, 25) }}>
                     {this.childLayoutPairs.filter((pair) => this.isCurrent(pair.layout)).map((pair, ind) => {
-                        const nested = pair.layout.viewType === CollectionViewType.Linear;
+                        const nested = pair.layout._viewType === CollectionViewType.Linear;
                         const dref = React.createRef<HTMLDivElement>();
-                        const nativeWidth = NumCast(pair.layout.nativeWidth, this.dimension());
+                        const nativeWidth = NumCast(pair.layout._nativeWidth, this.dimension());
                         const deltaSize = nativeWidth * .15 / 2;
                         return <div className={`collectionLinearView-docBtn` + (pair.layout.onClick || pair.layout.onDragStart ? "-scalable" : "")} key={pair.layout[Id]} ref={dref}
                             style={{
diff --git a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
index 80752303c..57b091ce4 100644
--- a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
+++ b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
@@ -136,7 +136,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
     addDocument = (value: string, shiftDown?: boolean) => {
         this._createAliasSelected = false;
         const key = StrCast(this.props.parent.props.Document.sectionFilter);
-        const newDoc = Docs.Create.TextDocument({ height: 18, width: 200, title: value });
+        const newDoc = Docs.Create.TextDocument({ _height: 18, _width: 200, title: value });
         newDoc[key] = this.getValue(this.props.heading);
         return this.props.parent.props.addDocument(newDoc);
     }
@@ -258,7 +258,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
     @computed get contentLayout() {
         const rows = Math.max(1, Math.min(this.props.docList.length, Math.floor((this.props.parent.props.PanelWidth() - 2 * this.props.parent.xMargin) / (this.props.parent.columnWidth + this.props.parent.gridGap))));
         const style = this.props.parent; const collapsed = this._collapsed;
-        const chromeStatus = this.props.parent.props.Document.chromeStatus;
+        const chromeStatus = this.props.parent.props.Document._chromeStatus;
         const newEditableViewProps = {
             GetValue: () => "",
             SetValue: this.addDocument,
diff --git a/src/client/views/collections/CollectionPivotView.tsx b/src/client/views/collections/CollectionPivotView.tsx
index 43a302af7..a44f990e1 100644
--- a/src/client/views/collections/CollectionPivotView.tsx
+++ b/src/client/views/collections/CollectionPivotView.tsx
@@ -24,11 +24,11 @@ export class CollectionPivotView extends CollectionSubView(doc => doc) {
         this._narrativeDisposer?.();
     }
     componentDidMount() {
-        this.props.Document.freeformLayoutEngine = "pivot";
-        if (!this.props.Document.facetCollection) {
-            const facetCollection = Docs.Create.TreeDocument([], { title: "facetFilters", yMargin: 0, treeViewHideTitle: true });
+        this.props.Document._freeformLayoutEngine = "pivot";
+        if (!this.props.Document._facetCollection) {
+            const facetCollection = Docs.Create.TreeDocument([], { title: "facetFilters", _yMargin: 0, treeViewHideTitle: true });
             facetCollection.target = this.props.Document;
-            this.props.Document.excludeFields = new List<string>(["facetCollection", "docFilter", "viewSpecScript"]);
+            this.props.Document.excludeFields = new List<string>(["_facetCollection", "_docFilter", "viewSpecScript"]);
 
             const scriptText = "setDocFilter(containingTreeView.target, heading, this.title, checked)";
             const script = CompileScript(scriptText, {
@@ -54,15 +54,10 @@ export class CollectionPivotView extends CollectionSubView(doc => doc) {
                         }
                     }
                 }), { fireImmediately: true });
-            this.props.Document.facetCollection = facetCollection;
-            this.props.Document.fitToBox = true;
+            this.props.Document._facetCollection = facetCollection;
+            this.props.Document._fitToBox = true;
         }
     }
-
-    @computed get fieldExtensionDoc() {
-        return Doc.fieldExtensionDoc(this.props.DataDoc || this.props.Document, this.props.fieldKey);
-    }
-
     bodyPanelWidth = () => this.props.PanelWidth() - this._facetWidth;
     getTransform = () => this.props.ScreenToLocalTransform().translate(-200, 0);
 
@@ -76,7 +71,7 @@ export class CollectionPivotView extends CollectionSubView(doc => doc) {
      * Responds to clicking the check box in the flyout menu
      */
     facetClick = (facetHeader: string) => {
-        const facetCollection = this.props.Document.facetCollection;
+        const facetCollection = this.props.Document._facetCollection;
         if (facetCollection instanceof Doc) {
             const found = DocListCast(facetCollection.data).findIndex(doc => doc.title === facetHeader);
             if (found !== -1) {
@@ -120,11 +115,11 @@ export class CollectionPivotView extends CollectionSubView(doc => doc) {
     }
 
     render() {
-        const facetCollection = Cast(this.props.Document?.facetCollection, Doc, null);
+        const facetCollection = Cast(this.props.Document?._facetCollection, Doc, null);
         const flyout = (
             <div className="collectionPivotView-flyout" style={{ width: `${this._facetWidth}` }}>
                 {this._allFacets.map(facet => <label className="collectionPivotView-flyout-item" key={`${facet}`} onClick={e => this.facetClick(facet)}>
-                    <input type="checkbox" onChange={e => { }} checked={this.props.Document.facetCollection instanceof Doc && DocListCast(this.props.Document.facetCollection.data).some(d => {
+                    <input type="checkbox" onChange={e => { }} checked={this.props.Document._facetCollection instanceof Doc && DocListCast(this.props.Document._facetCollection.data).some(d => {
                         return d.title === facet;
                     })} />
                     <span className="checkmark" />
diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx
index 994c7b351..9ecb8dc43 100644
--- a/src/client/views/collections/CollectionSchemaView.tsx
+++ b/src/client/views/collections/CollectionSchemaView.tsx
@@ -477,7 +477,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> {
 
     @undoBatch
     createRow = () => {
-        const newDoc = Docs.Create.TextDocument({ title: "", width: 100, height: 30 });
+        const newDoc = Docs.Create.TextDocument({ title: "", _width: 100, _height: 30 });
         this.props.addDocument(newDoc);
     }
 
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index ec22bb813..372db5fee 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -39,13 +39,13 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
     @observable _scroll = 0; // used to force the document decoration to update when scrolling
     @computed get sectionHeaders() { return Cast(this.props.Document.sectionHeaders, listSpec(SchemaHeaderField)); }
     @computed get sectionFilter() { return StrCast(this.props.Document.sectionFilter); }
-    @computed get filteredChildren() { return this.childLayoutPairs.filter(pair => !pair.layout.isMinimized).map(pair => pair.layout); }
-    @computed get xMargin() { return NumCast(this.props.Document.xMargin, 2 * this.gridGap); }
-    @computed get yMargin() { return Math.max(this.props.Document.showTitle && !this.props.Document.showTitleHover ? 30 : 0, NumCast(this.props.Document.yMargin, 2 * this.gridGap)); }
-    @computed get gridGap() { return NumCast(this.props.Document.gridGap, 10); }
+    @computed get filteredChildren() { return this.childLayoutPairs.filter(pair => pair.layout instanceof Doc && !pair.layout.isMinimized).map(pair => pair.layout); }
+    @computed get xMargin() { return NumCast(this.props.Document._xMargin, 2 * this.gridGap); }
+    @computed get yMargin() { return Math.max(this.props.Document.showTitle && !this.props.Document.showTitleHover ? 30 : 0, NumCast(this.props.Document._yMargin, 2 * this.gridGap)); }
+    @computed get gridGap() { return NumCast(this.props.Document._gridGap, 10); }
     @computed get isStackingView() { return BoolCast(this.props.Document.singleColumn, true); }
     @computed get numGroupColumns() { return this.isStackingView ? Math.max(1, this.Sections.size + (this.showAddAGroup ? 1 : 0)) : 1; }
-    @computed get showAddAGroup() { return (this.sectionFilter && (this.props.Document.chromeStatus !== 'view-mode' && this.props.Document.chromeStatus !== 'disabled')); }
+    @computed get showAddAGroup() { return (this.sectionFilter && (this.props.Document._chromeStatus !== 'view-mode' && this.props.Document._chromeStatus !== 'disabled')); }
     @computed get columnWidth() {
         return Math.min(this.props.PanelWidth() / (this.props as any).ContentScaling() - 2 * this.xMargin,
             this.isStackingView ? Number.MAX_VALUE : NumCast(this.props.Document.columnWidth, 250));
@@ -55,7 +55,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
     children(docs: Doc[]) {
         this._docXfs.length = 0;
         return docs.map((d, i) => {
-            const width = () => Math.min(d.nativeWidth && !d.ignoreAspect && !this.props.Document.fillColumn ? d[WidthSym]() : Number.MAX_VALUE, this.columnWidth / this.numGroupColumns);
+            const width = () => Math.min(d._nativeWidth && !d.ignoreAspect && !this.props.Document.fillColumn ? d[WidthSym]() : Number.MAX_VALUE, this.columnWidth / this.numGroupColumns);
             const height = () => this.getDocHeight(d);
             const dref = React.createRef<HTMLDivElement>();
             const dxf = () => this.getDocTransform(d, dref.current!);
@@ -125,7 +125,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
         },
             (hgt: number) => {
                 const doc = hgt === -1 ? undefined : this.props.DataDoc && this.props.DataDoc.layout === this.layoutDoc ? this.props.DataDoc : this.layoutDoc;
-                doc && hgt > 0 && (Doc.Layout(doc).height = hgt);
+                doc && hgt > 0 && (Doc.Layout(doc)._height = hgt);
             },
             { fireImmediately: true }
         );
@@ -187,16 +187,16 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
     getDocHeight(d?: Doc) {
         if (!d) return 0;
         const layoutDoc = Doc.Layout(d);
-        const nw = NumCast(layoutDoc.nativeWidth);
-        const nh = NumCast(layoutDoc.nativeHeight);
+        const nw = NumCast(layoutDoc._nativeWidth);
+        const nh = NumCast(layoutDoc._nativeHeight);
         let wid = this.columnWidth / (this.isStackingView ? this.numGroupColumns : 1);
-        if (!layoutDoc.ignoreAspect && !layoutDoc.fitWidth && nw && nh) {
+        if (!layoutDoc.ignoreAspect && !layoutDoc._fitWidth && nw && nh) {
             const aspect = nw && nh ? nh / nw : 1;
-            if (!(d.nativeWidth && !layoutDoc.ignoreAspect && this.props.Document.fillColumn)) wid = Math.min(layoutDoc[WidthSym](), wid);
+            if (!(d._nativeWidth && !layoutDoc.ignoreAspect && this.props.Document.fillColumn)) wid = Math.min(layoutDoc[WidthSym](), wid);
             return wid * aspect;
         }
-        return layoutDoc.fitWidth ? !layoutDoc.nativeHeight ? this.props.PanelHeight() - 2 * this.yMargin :
-            Math.min(wid * NumCast(layoutDoc.scrollHeight, NumCast(layoutDoc.nativeHeight)) / NumCast(layoutDoc.nativeWidth, 1), this.props.PanelHeight() - 2 * this.yMargin) : layoutDoc[HeightSym]();
+        return layoutDoc._fitWidth ? !layoutDoc._nativeHeight ? this.props.PanelHeight() - 2 * this.yMargin :
+            Math.min(wid * NumCast(layoutDoc.scrollHeight, NumCast(layoutDoc._nativeHeight)) / NumCast(layoutDoc._nativeWidth, 1), this.props.PanelHeight() - 2 * this.yMargin) : layoutDoc[HeightSym]();
     }
 
     columnDividerDown = (e: React.PointerEvent) => {
@@ -241,7 +241,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
                 const pos1 = cd.dxf().inverse().transformPoint(cd.width(), cd.height());
                 if (where[0] > pos[0] && where[0] < pos1[0] && where[1] > pos[1] && where[1] < pos1[1]) {
                     targInd = i;
-                    const axis = this.Document.viewType === CollectionViewType.Masonry ? 0 : 1;
+                    const axis = this.Document._viewType === CollectionViewType.Masonry ? 0 : 1;
                     plusOne = where[axis] > (pos[axis] + pos1[axis]) / 2 ? 1 : 0;
                 }
             });
@@ -359,7 +359,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
     }
 
     onToggle = (checked: Boolean) => {
-        this.props.Document.chromeStatus = checked ? "collapsed" : "view-mode";
+        this.props.Document._chromeStatus = checked ? "collapsed" : "view-mode";
     }
 
     onContextMenu = (e: React.MouseEvent): void => {
@@ -408,10 +408,10 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
                             style={{ width: !this.isStackingView ? "100%" : this.columnWidth / this.numGroupColumns - 10, marginTop: 10 }}>
                             <EditableView {...editableViewProps} />
                         </div>}
-                    {this.props.Document.chromeStatus !== 'disabled' ? <Switch
+                    {this.props.Document._chromeStatus !== 'disabled' ? <Switch
                         onChange={this.onToggle}
                         onClick={this.onToggle}
-                        defaultChecked={this.props.Document.chromeStatus !== 'view-mode'}
+                        defaultChecked={this.props.Document._chromeStatus !== 'view-mode'}
                         checkedChildren="edit"
                         unCheckedChildren="view"
                     /> : null}
diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
index f0dcadb5b..29dfb60c5 100644
--- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
+++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
@@ -136,7 +136,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
     @action
     addDocument = (value: string, shiftDown?: boolean) => {
         if (value === ":freeForm") {
-            return this.props.parent.props.addDocument(Docs.Create.FreeformDocument([], { width: 200, height: 200 }));
+            return this.props.parent.props.addDocument(Docs.Create.FreeformDocument([], { _width: 200, _height: 200 }));
         } else if (value.startsWith(":")) {
             const { Document, addDocument } = this.props.parent.props;
             const fieldKey = value.substring(1);
@@ -152,7 +152,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
         }
         this._createAliasSelected = false;
         const key = StrCast(this.props.parent.props.Document.sectionFilter);
-        const newDoc = Docs.Create.TextDocument({ height: 18, width: 200, documentText: "@@@" + value, title: value, autoHeight: true });
+        const newDoc = Docs.Create.TextDocument({ _height: 18, _width: 200, documentText: "@@@" + value, title: value, autoHeight: true });
         newDoc[key] = this.getValue(this.props.heading);
         const maxHeading = this.props.docList.reduce((maxHeading, doc) => NumCast(doc.heading) > maxHeading ? NumCast(doc.heading) : maxHeading, 0);
         const heading = maxHeading === 0 || this.props.docList.length === 0 ? 1 : maxHeading === 1 ? 2 : 3;
@@ -310,7 +310,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
                 style={{
                     width: (style.columnWidth) /
                         ((uniqueHeadings.length +
-                            ((this.props.parent.props.Document.chromeStatus !== 'view-mode' && this.props.parent.props.Document.chromeStatus !== 'disabled') ? 1 : 0)) || 1)
+                            ((this.props.parent.props.Document._chromeStatus !== 'view-mode' && this.props.parent.props.Document._chromeStatus !== 'disabled') ? 1 : 0)) || 1)
                 }}>
                 <div className={"collectionStackingView-collapseBar" + (this.props.headingObject.collapsed === true ? " active" : "")} onClick={this.collapseSection}></div>
                 {/* the default bucket (no key value) has a tooltip that describes what it is.
@@ -350,7 +350,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
                 </div>
             </div> : (null);
         for (let i = 0; i < cols; i++) templatecols += `${style.columnWidth / style.numGroupColumns}px `;
-        const chromeStatus = this.props.parent.props.Document.chromeStatus;
+        const chromeStatus = this.props.parent.props.Document._chromeStatus;
         return (
             <div className="collectionStackingViewFieldColumn" key={heading} style={{ width: `${100 / ((uniqueHeadings.length + ((chromeStatus !== 'view-mode' && chromeStatus !== 'disabled') ? 1 : 0)) || 1)}%`, background: this._background }}
                 ref={this.createColumnDropRef} onPointerEnter={this.pointerEntered} onPointerLeave={this.pointerLeave}>
diff --git a/src/client/views/collections/CollectionStaffView.tsx b/src/client/views/collections/CollectionStaffView.tsx
index 105061f46..8c7e113b2 100644
--- a/src/client/views/collections/CollectionStaffView.tsx
+++ b/src/client/views/collections/CollectionStaffView.tsx
@@ -23,10 +23,6 @@ export class CollectionStaffView extends CollectionSubView(doc => doc) {
         this.props.Document.staves = 5;
     }
 
-    @computed get fieldExtensionDoc() {
-        return Doc.fieldExtensionDoc(this.props.DataDoc || this.props.Document, this.props.fieldKey);
-    }
-
     @computed get addStaffButton() {
         return <div onPointerDown={this.addStaff}>+</div>;
     }
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 063b5c5df..db393918d 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -82,7 +82,6 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
         }
 
         @computed get dataDoc() { return this.props.DataDoc && this.props.Document.isTemplateForField ? Doc.GetProto(this.props.DataDoc) : Doc.GetProto(this.props.Document); }
-        @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); }
 
         // The data field for rendering this collection will be on the this.props.Document unless we're rendering a template in which case we try to use props.DataDoc.
         // When a document has a DataDoc but it's not a template, then it contains its own rendering data, but needs to pass the DataDoc through
@@ -90,14 +89,10 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
         // If 'annotationField' is specified, then all children exist on that field of the extension document, otherwise, they exist directly on the data document under 'fieldKey'
         @computed get dataField() {
             const { annotationsKey, fieldKey } = this.props;
-            const { extensionDoc, dataDoc } = this;
             if (annotationsKey) {
-                if (extensionDoc) {
-                    return extensionDoc[annotationsKey];
-                }
-                return undefined;
+                return this.dataDoc[fieldKey + "-" + annotationsKey];
             }
-            return dataDoc[fieldKey];
+            return this.dataDoc[fieldKey];
         }
 
         get childLayoutPairs(): { layout: Doc; data: Doc; }[] {
@@ -216,7 +211,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
                         this.props.addDocument && this.props.addDocument(Docs.Create.WebDocument(href, { ...options, title: href }));
                     }
                 } else if (text) {
-                    this.props.addDocument && this.props.addDocument(Docs.Create.TextDocument({ ...options, width: 100, height: 25, documentText: "@@@" + text }));
+                    this.props.addDocument && this.props.addDocument(Docs.Create.TextDocument({ ...options, _width: 100, _height: 25, documentText: "@@@" + text }));
                 }
                 return;
             }
@@ -226,7 +221,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
                 const img = tags[0].startsWith("img") ? tags[0] : tags.length > 1 && tags[1].startsWith("img") ? tags[1] : "";
                 if (img) {
                     const split = img.split("src=\"")[1].split("\"")[0];
-                    const doc = Docs.Create.ImageDocument(split, { ...options, width: 300 });
+                    const doc = Docs.Create.ImageDocument(split, { ...options, _width: 300 });
                     ImageUtils.ExtractExif(doc);
                     this.props.addDocument(doc);
                     return;
@@ -241,7 +236,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
                             }
                         });
                     } else {
-                        const htmlDoc = Docs.Create.HtmlDocument(html, { ...options, title: "-web page-", width: 300, height: 300, documentText: text });
+                        const htmlDoc = Docs.Create.HtmlDocument(html, { ...options, title: "-web page-", _width: 300, _height: 300, documentText: text });
                         this.props.addDocument(htmlDoc);
                     }
                     return;
@@ -249,12 +244,12 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
             }
             if (text && text.indexOf("www.youtube.com/watch") !== -1) {
                 const url = text.replace("youtube.com/watch?v=", "youtube.com/embed/");
-                this.props.addDocument(Docs.Create.VideoDocument(url, { ...options, title: url, width: 400, height: 315, nativeWidth: 600, nativeHeight: 472.5 }));
+                this.props.addDocument(Docs.Create.VideoDocument(url, { ...options, title: url, _width: 400, _height: 315, _nativeWidth: 600, _nativeHeight: 472.5 }));
                 return;
             }
             let matches: RegExpExecArray | null;
             if ((matches = /(https:\/\/)?docs\.google\.com\/document\/d\/([^\\]+)\/edit/g.exec(text)) !== null) {
-                const newBox = Docs.Create.TextDocument({ ...options, width: 400, height: 200, title: "Awaiting title from Google Docs..." });
+                const newBox = Docs.Create.TextDocument({ ...options, _width: 400, _height: 200, title: "Awaiting title from Google Docs..." });
                 const proto = newBox.proto!;
                 const documentId = matches[2];
                 proto[GoogleRef] = documentId;
@@ -303,7 +298,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
                     const dropFileName = file ? file.name : "-empty-";
                     promises.push(Networking.PostFormDataToServer("/upload", formData).then(results => {
                         results.map(action(({ clientAccessPath }: any) => {
-                            const full = { ...options, width: 300, title: dropFileName };
+                            const full = { ...options, _width: 300, title: dropFileName };
                             const pathname = Utils.prepend(clientAccessPath);
                             Docs.Get.DocumentFromType(type, pathname, full).then(doc => {
                                 doc && (Doc.GetProto(doc).fileUpload = basename(pathname).replace("upload_", "").replace(/\.[a-z0-9]*$/, ""));
@@ -318,7 +313,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
                 Promise.all(promises).finally(() => { completed && completed(); batch.end(); });
             } else {
                 if (text && !text.includes("https://")) {
-                    this.props.addDocument(Docs.Create.TextDocument({ ...options, documentText: "@@@" + text, width: 400, height: 315 }));
+                    this.props.addDocument(Docs.Create.TextDocument({ ...options, documentText: "@@@" + text, _width: 400, _height: 315 }));
                 }
                 batch.end();
             }
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index ec348e74b..21f2bc326 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -179,7 +179,7 @@ class TreeView extends React.Component<TreeViewProps> {
         OnFillDown={undoBatch((value: string) => {
             Doc.SetInPlace(this.props.document, key, value, false);
             const layoutDoc = this.props.document.layoutCustom instanceof Doc ? Doc.ApplyTemplate(Doc.GetProto(this.props.document.layoutCustom)) : undefined;
-            const doc = layoutDoc || Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, width: 100, height: 25, templates: new List<string>([Templates.Title.Layout]) });
+            const doc = layoutDoc || Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, _width: 100, _height: 25, templates: new List<string>([Templates.Title.Layout]) });
             TreeView.loadId = doc[Id];
             return this.props.addDocument(doc);
         })}
@@ -210,7 +210,7 @@ class TreeView extends React.Component<TreeViewProps> {
                 ContextMenu.Instance.addItem({ description: "Delete Workspace", event: () => this.props.deleteDoc(this.props.document), icon: "trash-alt" });
                 ContextMenu.Instance.addItem({ description: "Create New Workspace", event: () => MainView.Instance.createNewWorkspace(), icon: "plus" });
             }
-            ContextMenu.Instance.addItem({ description: "Open Fields", event: () => { const kvp = Docs.Create.KVPDocument(this.props.document, { width: 300, height: 300 }); this.props.addDocTab(kvp, this.props.dataDoc ? this.props.dataDoc : kvp, "onRight"); }, icon: "layer-group" });
+            ContextMenu.Instance.addItem({ description: "Open Fields", event: () => { const kvp = Docs.Create.KVPDocument(this.props.document, { _width: 300, _height: 300 }); this.props.addDocTab(kvp, this.props.dataDoc ? this.props.dataDoc : kvp, "onRight"); }, icon: "layer-group" });
             ContextMenu.Instance.addItem({ description: "Publish", event: () => DocUtils.Publish(this.props.document, StrCast(this.props.document.title), () => { }, () => { }), icon: "file" });
             ContextMenu.Instance.displayMenu(e.pageX > 156 ? e.pageX - 156 : 0, e.pageY - 15);
             e.stopPropagation();
@@ -256,21 +256,21 @@ class TreeView extends React.Component<TreeViewProps> {
     }
     docWidth = () => {
         const layoutDoc = Doc.Layout(this.props.document);
-        const aspect = NumCast(layoutDoc.nativeHeight) / NumCast(layoutDoc.nativeWidth);
+        const aspect = NumCast(layoutDoc._nativeHeight) / NumCast(layoutDoc._nativeWidth);
         if (aspect) return Math.min(layoutDoc[WidthSym](), Math.min(this.MAX_EMBED_HEIGHT / aspect, this.props.panelWidth() - 20));
-        return NumCast(layoutDoc.nativeWidth) ? Math.min(layoutDoc[WidthSym](), this.props.panelWidth() - 20) : this.props.panelWidth() - 20;
+        return NumCast(layoutDoc._nativeWidth) ? Math.min(layoutDoc[WidthSym](), this.props.panelWidth() - 20) : this.props.panelWidth() - 20;
     }
     docHeight = () => {
         const layoutDoc = Doc.Layout(this.props.document);
         const bounds = this.boundsOfCollectionDocument;
         return Math.min(this.MAX_EMBED_HEIGHT, (() => {
-            const aspect = NumCast(layoutDoc.nativeHeight) / NumCast(layoutDoc.nativeWidth, 1);
+            const aspect = NumCast(layoutDoc._nativeHeight) / NumCast(layoutDoc._nativeWidth, 1);
             if (aspect) return this.docWidth() * aspect;
             if (bounds) return this.docWidth() * (bounds.b - bounds.y) / (bounds.r - bounds.x);
-            return layoutDoc.fitWidth ? (!this.props.document.nativeHeight ? NumCast(this.props.containingCollection.height) :
-                Math.min(this.docWidth() * NumCast(layoutDoc.scrollHeight, NumCast(layoutDoc.nativeHeight)) / NumCast(layoutDoc.nativeWidth,
-                    NumCast(this.props.containingCollection.height)))) :
-                NumCast(layoutDoc.height) ? NumCast(layoutDoc.height) : 50;
+            return layoutDoc._fitWidth ? (!this.props.document.nativeHeight ? NumCast(this.props.containingCollection._height) :
+                Math.min(this.docWidth() * NumCast(layoutDoc.scrollHeight, NumCast(layoutDoc._nativeHeight)) / NumCast(layoutDoc._nativeWidth,
+                    NumCast(this.props.containingCollection._height)))) :
+                NumCast(layoutDoc._height) ? NumCast(layoutDoc._height) : 50;
         })());
     }
 
@@ -547,7 +547,7 @@ class TreeView extends React.Component<TreeViewProps> {
             };
             const childLayout = Doc.Layout(pair.layout);
             const rowHeight = () => {
-                const aspect = NumCast(childLayout.nativeWidth, 0) / NumCast(childLayout.nativeHeight, 0);
+                const aspect = NumCast(childLayout._nativeWidth, 0) / NumCast(childLayout._nativeHeight, 0);
                 return aspect ? Math.min(childLayout[WidthSym](), rowWidth()) / aspect : childLayout[HeightSym]();
             };
             return !(child instanceof Doc) ? (null) : <TreeView
@@ -637,10 +637,10 @@ export class CollectionTreeView extends CollectionSubView(Document) {
 
                 const detailedLayout = Docs.Create.StackingDocument([
                     ImageDocument(fallbackImg, { title: "activeHero" }),
-                    TreeDocument([], { title: "data", height: 100, onChildClick: ScriptField.MakeFunction(`containingCollection.resolvedDataDoc.activeHero = copyField(this.data)`, { containingCollection: Doc.name }) }),
+                    MulticolumnDocument([], { title: "data", _height: 100, onChildClick: ScriptField.MakeFunction(`containingCollection.resolvedDataDoc.activeHero = copyField(this.data)`, { containingCollection: Doc.name }) }),
                     TextDocument({ title: "short_description", autoHeight: true }),
-                    ...["year", "company", "degrees_of_freedom"].map(key => TextDocument({ title: key, height: 30 }))
-                ], { autoHeight: true, chromeStatus: "disabled", title: "detailed layout stack" });
+                    ...["year", "company", "degrees_of_freedom"].map(key => TextDocument({ title: key, _height: 30 }))
+                ], { autoHeight: true, _chromeStatus: "disabled", title: "detailed layout stack" });
                 detailedLayout.isTemplateDoc = makeTemplate(detailedLayout);
 
                 const cardLayout = ImageDocument(fallbackImg, { isTemplateDoc: true, isTemplateForField: "hero", }); // this acts like a template doc and a template field ... a little weird, but seems to work?
@@ -650,7 +650,7 @@ export class CollectionTreeView extends CollectionSubView(Document) {
 
                 Document.childLayout = cardLayout;
                 Document.childDetailed = detailedLayout;
-                Document.viewType = CollectionViewType.Pivot;
+                Document._viewType = CollectionViewType.Pivot;
                 Document.pivotField = "company";
             }
         });
@@ -675,12 +675,12 @@ export class CollectionTreeView extends CollectionSubView(Document) {
     }
 
     render() {
-        const dropAction = StrCast(this.props.Document.dropAction) as dropActionType;
+        const dropAction = StrCast(this.props.Document._dropAction) as dropActionType;
         const addDoc = (doc: Doc, relativeTo?: Doc, before?: boolean) => Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, relativeTo, before, false, false, false);
         const moveDoc = (d: Doc, target: Doc | undefined, addDoc: (doc: Doc) => boolean) => this.props.moveDocument(d, target, addDoc);
         return !this.childDocs ? (null) : (
             <div className="collectionTreeView-dropTarget" id="body"
-                style={{ background: StrCast(this.props.Document.backgroundColor, "lightgray"), paddingTop: `${NumCast(this.props.Document.yMargin, 20)}px` }}
+                style={{ background: StrCast(this.props.Document.backgroundColor, "lightgray"), paddingTop: `${NumCast(this.props.Document._yMargin, 20)}px` }}
                 onContextMenu={this.onContextMenu}
                 onWheel={(e: React.WheelEvent) => this._mainEle && this._mainEle.scrollHeight > this._mainEle.clientHeight && e.stopPropagation()}
                 onDrop={this.onTreeDrop}
@@ -695,7 +695,7 @@ export class CollectionTreeView extends CollectionSubView(Document) {
                     OnFillDown={undoBatch((value: string) => {
                         Doc.SetInPlace(this.dataDoc, "title", value, false);
                         const layoutDoc = this.props.Document.layoutCustom instanceof Doc ? Doc.ApplyTemplate(Doc.GetProto(this.props.Document.layoutCustom)) : undefined;
-                        const doc = layoutDoc || Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, width: 100, height: 25, templates: new List<string>([Templates.Title.Layout]) });
+                        const doc = layoutDoc || Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, _width: 100, _height: 25, templates: new List<string>([Templates.Title.Layout]) });
                         TreeView.loadId = doc[Id];
                         Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, this.childDocs.length ? this.childDocs[0] : undefined, true, false, false, false);
                     })} />)}
@@ -734,7 +734,7 @@ Scripting.addGlobal(function readFacetData(layoutDoc: Doc, dataDoc: Doc, dataKey
 });
 
 Scripting.addGlobal(function determineCheckedState(layoutDoc: Doc, facetHeader: string, facetValue: string) {
-    const docFilters = Cast(layoutDoc.docFilter, listSpec("string"), []);
+    const docFilters = Cast(layoutDoc._docFilter, listSpec("string"), []);
     for (let i = 0; i < docFilters.length; i += 3) {
         const [header, value, state] = docFilters.slice(i, i + 3);
         if (header === facetHeader && value === facetValue) {
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 430aecef4..715094bf5 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -91,17 +91,8 @@ export class CollectionView extends Touchable<FieldViewProps> {
     @observable private static _safeMode = false;
     public static SetSafeMode(safeMode: boolean) { this._safeMode = safeMode; }
 
-    @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.props.Document, this.props.fieldKey); }
-
     get collectionViewType(): CollectionViewType | undefined {
-        if (!this.extensionDoc) return CollectionViewType.Invalid;
-        NumCast(this.props.Document.viewType) && setTimeout(() => {
-            if (this.props.Document.viewType) {
-                this.extensionDoc!.viewType = NumCast(this.props.Document.viewType);
-            }
-            Doc.GetProto(this.props.Document).viewType = this.props.Document.viewType = undefined;
-        });
-        const viewField = NumCast(this.extensionDoc.viewType, Cast(this.props.Document.viewType, "number"));
+        const viewField = NumCast(this.props.Document._viewType);
         if (CollectionView._safeMode) {
             if (viewField === CollectionViewType.Freeform) {
                 return CollectionViewType.Tree;
@@ -110,15 +101,15 @@ export class CollectionView extends Touchable<FieldViewProps> {
                 return CollectionViewType.Freeform;
             }
         }
-        return viewField === undefined ? CollectionViewType.Invalid : viewField;
+        return viewField;
     }
 
     componentDidMount = () => {
-        this._reactionDisposer = reaction(() => StrCast(this.props.Document.chromeStatus),
+        this._reactionDisposer = reaction(() => StrCast(this.props.Document._chromeStatus),
             () => {
                 // chrome status is one of disabled, collapsed, or visible. this determines initial state from document
                 // chrome status may also be view-mode, in reference to stacking view's toggle mode. it is essentially disabled mode, but prevents the toggle button from showing up on the left sidebar.
-                const chromeStatus = this.props.Document.chromeStatus;
+                const chromeStatus = this.props.Document._chromeStatus;
                 if (chromeStatus && (chromeStatus === "disabled" || chromeStatus === "collapsed")) {
                     runInAction(() => this._collapsed = true);
                 }
@@ -128,7 +119,7 @@ export class CollectionView extends Touchable<FieldViewProps> {
     componentWillUnmount = () => this._reactionDisposer && this._reactionDisposer();
 
     // bcz: Argh?  What's the height of the collection chromes??  
-    chromeHeight = () => (this.props.Document.chromeStatus === "enabled" ? -60 : 0);
+    chromeHeight = () => (this.props.Document._chromeStatus === "enabled" ? -60 : 0);
 
     active = (outsideReaction?: boolean) => this.props.isSelected(outsideReaction) || BoolCast(this.props.Document.forceActive) || this._isChildActive || this.props.renderDepth === 0;
 
@@ -138,8 +129,7 @@ export class CollectionView extends Touchable<FieldViewProps> {
     addDocument(doc: Doc): boolean {
         const targetDataDoc = Doc.GetProto(this.props.Document);
         Doc.AddDocToList(targetDataDoc, this.props.fieldKey, doc);
-        const extension = Doc.fieldExtensionDoc(targetDataDoc, this.props.fieldKey);  // set metadata about the field being rendered (ie, the set of documents) on an extension field for that field
-        extension && (extension.lastModified = new DateField(new Date(Date.now())));
+        targetDataDoc[this.props.fieldKey + "-lastModified"] = new DateField(new Date(Date.now()));
         Doc.GetProto(doc).lastOpened = new DateField;
         return true;
     }
@@ -192,19 +182,19 @@ export class CollectionView extends Touchable<FieldViewProps> {
             case CollectionViewType.Masonry: { this.props.Document.singleColumn = false; return (<CollectionStackingView key="collview" {...props} />); }
             case CollectionViewType.Pivot: { return (<CollectionPivotView key="collview" {...props} />); }
             case CollectionViewType.Freeform:
-            default: { this.props.Document.freeformLayoutEngine = undefined; return (<CollectionFreeFormView key="collview" {...props} />); }
+            default: { this.props.Document._freeformLayoutEngine = undefined; return (<CollectionFreeFormView key="collview" {...props} />); }
         }
     }
 
     @action
     private collapse = (value: boolean) => {
         this._collapsed = value;
-        this.props.Document.chromeStatus = value ? "collapsed" : "enabled";
+        this.props.Document._chromeStatus = value ? "collapsed" : "enabled";
     }
 
     private SubView = (type: CollectionViewType, renderProps: CollectionRenderProps) => {
         // currently cant think of a reason for collection docking view to have a chrome. mind may change if we ever have nested docking views -syip
-        const chrome = this.props.Document.chromeStatus === "disabled" || type === CollectionViewType.Docking ? (null) :
+        const chrome = this.props.Document._chromeStatus === "disabled" || type === CollectionViewType.Docking ? (null) :
             <CollectionViewBaseChrome CollectionView={this} key="chrome" type={type} collapse={this.collapse} />;
         return [chrome, this.SubViewHelper(type, renderProps)];
     }
@@ -214,24 +204,24 @@ export class CollectionView extends Touchable<FieldViewProps> {
         if (!e.isPropagationStopped() && this.props.Document[Id] !== CurrentUserUtils.MainDocId) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7
             const existingVm = ContextMenu.Instance.findByDescription("View Modes...");
             const subItems = existingVm && "subitems" in existingVm ? existingVm.subitems : [];
-            subItems.push({ description: "Freeform", event: () => { this.props.Document.viewType = CollectionViewType.Freeform; }, icon: "signature" });
+            subItems.push({ description: "Freeform", event: () => { this.props.Document._viewType = CollectionViewType.Freeform; }, icon: "signature" });
             if (CollectionView._safeMode) {
-                ContextMenu.Instance.addItem({ description: "Test Freeform", event: () => this.props.Document.viewType = CollectionViewType.Invalid, icon: "project-diagram" });
+                ContextMenu.Instance.addItem({ description: "Test Freeform", event: () => this.props.Document._viewType = CollectionViewType.Invalid, icon: "project-diagram" });
             }
-            subItems.push({ description: "Schema", event: () => this.props.Document.viewType = CollectionViewType.Schema, icon: "th-list" });
-            subItems.push({ description: "Treeview", event: () => this.props.Document.viewType = CollectionViewType.Tree, icon: "tree" });
-            subItems.push({ description: "Stacking", event: () => this.props.Document.viewType = CollectionViewType.Stacking, icon: "ellipsis-v" });
+            subItems.push({ description: "Schema", event: () => this.props.Document._viewType = CollectionViewType.Schema, icon: "th-list" });
+            subItems.push({ description: "Treeview", event: () => this.props.Document._viewType = CollectionViewType.Tree, icon: "tree" });
+            subItems.push({ description: "Stacking", event: () => this.props.Document._viewType = CollectionViewType.Stacking, icon: "ellipsis-v" });
             subItems.push({
                 description: "Stacking (AutoHeight)", event: () => {
-                    this.props.Document.viewType = CollectionViewType.Stacking;
+                    this.props.Document._viewType = CollectionViewType.Stacking;
                     this.props.Document.autoHeight = true;
                 }, icon: "ellipsis-v"
             });
-            subItems.push({ description: "Staff", event: () => this.props.Document.viewType = CollectionViewType.Staff, icon: "music" });
-            subItems.push({ description: "Multicolumn", event: () => this.props.Document.viewType = CollectionViewType.Multicolumn, icon: "columns" });
-            subItems.push({ description: "Masonry", event: () => this.props.Document.viewType = CollectionViewType.Masonry, icon: "columns" });
-            subItems.push({ description: "Pivot", event: () => this.props.Document.viewType = CollectionViewType.Pivot, icon: "columns" });
-            switch (this.props.Document.viewType) {
+            subItems.push({ description: "Staff", event: () => this.props.Document._viewType = CollectionViewType.Staff, icon: "music" });
+            subItems.push({ description: "Multicolumn", event: () => this.props.Document._viewType = CollectionViewType.Multicolumn, icon: "columns" });
+            subItems.push({ description: "Masonry", event: () => this.props.Document._viewType = CollectionViewType.Masonry, icon: "columns" });
+            subItems.push({ description: "Pivot", event: () => this.props.Document._viewType = CollectionViewType.Pivot, icon: "columns" });
+            switch (this.props.Document._viewType) {
                 case CollectionViewType.Freeform: {
                     subItems.push({ description: "Custom", icon: "fingerprint", event: AddCustomFreeFormLayout(this.props.Document, this.props.fieldKey) });
                     break;
diff --git a/src/client/views/collections/CollectionViewChromes.tsx b/src/client/views/collections/CollectionViewChromes.tsx
index 42c563fe2..59d7a2671 100644
--- a/src/client/views/collections/CollectionViewChromes.tsx
+++ b/src/client/views/collections/CollectionViewChromes.tsx
@@ -55,9 +55,9 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
         immediate: (draggedDocs: Doc[]) => Doc.GetProto(this.props.CollectionView.props.Document).data = new List<Doc>(draggedDocs.map((d: any) => Doc.MakeAlias(d)))
     };
     _viewCommand = {
-        title: "restore view", script: "this.target.panX = this.restoredPanX; this.target.panY = this.restoredPanY; this.target.scale = this.restoredScale;", params: ["target"],
-        immediate: (draggedDocs: Doc[]) => { this.props.CollectionView.props.Document.panX = 0; this.props.CollectionView.props.Document.panY = 0; this.props.CollectionView.props.Document.scale = 1; },
-        initialize: (button: Doc) => { button.restoredPanX = this.props.CollectionView.props.Document.panX; button.restoredPanY = this.props.CollectionView.props.Document.panY; button.restoredScale = this.props.CollectionView.props.Document.scale; }
+        title: "restore view", script: "this.target._panX = this.restoredPanX; this.target._panY = this.restoredPanY; this.target.scale = this.restoredScale;", params: ["target"],
+        immediate: (draggedDocs: Doc[]) => { this.props.CollectionView.props.Document._panX = 0; this.props.CollectionView.props.Document._panY = 0; this.props.CollectionView.props.Document.scale = 1; },
+        initialize: (button: Doc) => { button.restoredPanX = this.props.CollectionView.props.Document._panX; button.restoredPanY = this.props.CollectionView.props.Document._panY; button.restoredScale = this.props.CollectionView.props.Document.scale; }
     };
     _freeform_commands = [this._contentCommand, this._templateCommand, this._narrativeCommand, this._viewCommand];
     _stacking_commands = [this._contentCommand, this._templateCommand];
@@ -132,7 +132,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
         runInAction(() => {
             this.addKeyRestrictions(fields);
             // chrome status is one of disabled, collapsed, or visible. this determines initial state from document
-            const chromeStatus = this.props.CollectionView.props.Document.chromeStatus;
+            const chromeStatus = this.props.CollectionView.props.Document._chromeStatus;
             if (chromeStatus) {
                 if (chromeStatus === "disabled") {
                     throw new Error("how did you get here, if chrome status is 'disabled' on a collection, a chrome shouldn't even be instantiated!");
@@ -149,7 +149,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
     @undoBatch
     viewChanged = (e: React.ChangeEvent) => {
         //@ts-ignore
-        this.props.CollectionView.props.Document.viewType = parseInt(e.target.selectedOptions[0].value);
+        this.props.CollectionView.props.Document._viewType = parseInt(e.target.selectedOptions[0].value);
     }
 
     @action
@@ -223,7 +223,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
             `(${keyRestrictionScript}) ${dateRestrictionScript.length ? "&&" : ""} ${dateRestrictionScript}` :
             "true";
 
-        const docFilter = Cast(this.props.CollectionView.props.Document.docFilter, listSpec("string"), []);
+        const docFilter = Cast(this.props.CollectionView.props.Document._docFilter, listSpec("string"), []);
         const docFilterText = Doc.MakeDocFilter(docFilter);
         const finalScript = docFilterText && !fullScript.startsWith("(())") ? `${fullScript} ${docFilterText ? "&&" : ""} (${docFilterText})` :
             docFilterText ? docFilterText : fullScript;
@@ -242,9 +242,9 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
 
     @action
     toggleCollapse = () => {
-        this.props.CollectionView.props.Document.chromeStatus = this.props.CollectionView.props.Document.chromeStatus === "enabled" ? "collapsed" : "enabled";
+        this.props.CollectionView.props.Document._chromeStatus = this.props.CollectionView.props.Document._chromeStatus === "enabled" ? "collapsed" : "enabled";
         if (this.props.collapse) {
-            this.props.collapse(this.props.CollectionView.props.Document.chromeStatus !== "enabled");
+            this.props.collapse(this.props.CollectionView.props.Document._chromeStatus !== "enabled");
         }
     }
 
@@ -272,7 +272,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
 
     @observable private pivotKeyDisplay = this.pivotKey;
     getPivotInput = () => {
-        if (StrCast(this.document.freeformLayoutEngine) !== "pivot") {
+        if (StrCast(this.document._freeformLayoutEngine) !== "pivot") {
             return (null);
         }
         return (<input className="collectionViewBaseChrome-viewSpecsInput"
@@ -383,7 +383,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
     }
 
     render() {
-        const collapsed = this.props.CollectionView.props.Document.chromeStatus !== "enabled";
+        const collapsed = this.props.CollectionView.props.Document._chromeStatus !== "enabled";
         return (
             <div className="collectionViewChrome-cont" style={{ top: collapsed ? -70 : 0, height: collapsed ? 0 : undefined }}>
                 <div className="collectionViewChrome">
@@ -402,7 +402,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
                             className="collectionViewBaseChrome-viewPicker"
                             onPointerDown={stopPropagation}
                             onChange={this.viewChanged}
-                            value={NumCast(this.props.CollectionView.props.Document.viewType)}>
+                            value={NumCast(this.props.CollectionView.props.Document._viewType)}>
                             <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="1">Freeform View</option>
                             <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="2">Schema View</option>
                             <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="4">Tree View</option>
diff --git a/src/client/views/collections/ParentDocumentSelector.tsx b/src/client/views/collections/ParentDocumentSelector.tsx
index 1cc05f92c..18f1baf44 100644
--- a/src/client/views/collections/ParentDocumentSelector.tsx
+++ b/src/client/views/collections/ParentDocumentSelector.tsx
@@ -21,7 +21,13 @@ export const Flyout = higflyout.default;
 
 library.add(faEdit);
 
-type SelectorProps = { Document: Doc, Views: DocumentView[], Stack?: any, addDocTab(doc: Doc, dataDoc: Doc | undefined, location: string): void };
+type SelectorProps = {
+    Document: Doc,
+    Views: DocumentView[],
+    Stack?: any,
+    addDocTab(doc: Doc, dataDoc: Doc | undefined, location: string): void
+};
+
 @observer
 export class SelectorContextMenu extends React.Component<SelectorProps> {
     @observable private _docs: { col: Doc, target: Doc }[] = [];
@@ -49,11 +55,11 @@ export class SelectorContextMenu extends React.Component<SelectorProps> {
     getOnClick({ col, target }: { col: Doc, target: Doc }) {
         return () => {
             col = Doc.IsPrototype(col) ? Doc.MakeDelegate(col) : col;
-            if (NumCast(col.viewType, CollectionViewType.Invalid) === CollectionViewType.Freeform) {
-                const newPanX = NumCast(target.x) + NumCast(target.width) / 2;
-                const newPanY = NumCast(target.y) + NumCast(target.height) / 2;
-                col.panX = newPanX;
-                col.panY = newPanY;
+            if (NumCast(col._viewType, CollectionViewType.Invalid) === CollectionViewType.Freeform) {
+                const newPanX = NumCast(target.x) + NumCast(target._width) / 2;
+                const newPanY = NumCast(target.y) + NumCast(target._height) / 2;
+                col._panX = newPanX;
+                col._panY = newPanY;
             }
             this.props.addDocTab(col, undefined, "inTab"); // bcz: dataDoc?
         };
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
index 32097816d..8bf56338b 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
@@ -79,10 +79,10 @@ export function computePivotLayout(poolData: ObservableMap<string, any>, pivotDo
         for (const doc of val) {
             const layoutDoc = Doc.Layout(doc);
             let wid = pivotAxisWidth;
-            let hgt = layoutDoc.nativeWidth ? (NumCast(layoutDoc.nativeHeight) / NumCast(layoutDoc.nativeWidth)) * pivotAxisWidth : pivotAxisWidth;
+            let hgt = layoutDoc._nativeWidth ? (NumCast(layoutDoc._nativeHeight) / NumCast(layoutDoc._nativeWidth)) * pivotAxisWidth : pivotAxisWidth;
             if (hgt > pivotAxisWidth) {
                 hgt = pivotAxisWidth;
-                wid = layoutDoc.nativeHeight ? (NumCast(layoutDoc.nativeWidth) / NumCast(layoutDoc.nativeHeight)) * pivotAxisWidth : pivotAxisWidth;
+                wid = layoutDoc.nativeHeight ? (NumCast(layoutDoc._nativeWidth) / NumCast(layoutDoc._nativeHeight)) * pivotAxisWidth : pivotAxisWidth;
             }
             docMap.set(doc, {
                 x: x + xCount * pivotAxisWidth * expander + (pivotAxisWidth - wid) / 2 + (val.length < numCols ? (numCols - val.length) * pivotAxisWidth / 2 : 0),
@@ -104,8 +104,8 @@ export function computePivotLayout(poolData: ObservableMap<string, any>, pivotDo
             x: NumCast(pair.layout.x),
             y: NumCast(pair.layout.y),
             z: NumCast(pair.layout.z),
-            width: NumCast(pair.layout.width),
-            height: NumCast(pair.layout.height)
+            width: NumCast(pair.layout._width),
+            height: NumCast(pair.layout._height)
         };
         const pos = docMap.get(pair.layout) || defaultPosition;
         const data = poolData.get(pair.layout[Id]);
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 65953dbdc..c89fb92a5 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -41,15 +41,13 @@ import React = require("react");
 import { computedFn } from "mobx-utils";
 import { TraceMobx } from "../../../../new_fields/util";
 import { GestureUtils } from "../../../../pen-gestures/GestureUtils";
-import { LinkManager } from "../../../util/LinkManager";
 import { CognitiveServices } from "../../../cognitive_services/CognitiveServices";
-import CollectionPaletteVIew from "../../Palette";
 
 library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard, faFileUpload);
 
 export const panZoomSchema = createSchema({
-    panX: "number",
-    panY: "number",
+    _panX: "number",
+    _panY: "number",
     scale: "number",
     arrangeScript: ScriptField,
     arrangeInit: ScriptField,
@@ -81,16 +79,16 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     @observable.shallow _layoutElements: ViewDefResult[] = []; // shallow because some layout items (eg pivot labels) are just generated 'divs' and can't be frozen as observables
     @observable _clusterSets: (Doc[])[] = [];
 
-    @computed get fitToContent() { return (this.props.fitToBox || this.Document.fitToBox) && !this.isAnnotationOverlay; }
+    @computed get fitToContent() { return (this.props.fitToBox || this.Document._fitToBox) && !this.isAnnotationOverlay; }
     @computed get parentScaling() { return this.props.ContentScaling && this.fitToContent && !this.isAnnotationOverlay ? this.props.ContentScaling() : 1; }
     @computed get contentBounds() { return aggregateBounds(this._layoutElements.filter(e => e.bounds && !e.bounds.z).map(e => e.bounds!), NumCast(this.layoutDoc.xPadding, 10), NumCast(this.layoutDoc.yPadding, 10)); }
-    @computed get nativeWidth() { return this.Document.fitToContent ? 0 : this.Document.nativeWidth || 0; }
-    @computed get nativeHeight() { return this.fitToContent ? 0 : this.Document.nativeHeight || 0; }
+    @computed get nativeWidth() { return this.Document._fitToContent ? 0 : this.Document._nativeWidth || 0; }
+    @computed get nativeHeight() { return this.fitToContent ? 0 : this.Document._nativeHeight || 0; }
     private get isAnnotationOverlay() { return this.props.isAnnotationOverlay; }
     private get borderWidth() { return this.isAnnotationOverlay ? 0 : COLLECTION_BORDER_WIDTH; }
     private easing = () => this.props.Document.panTransformType === "Ease";
-    private panX = () => this.fitToContent ? (this.contentBounds.x + this.contentBounds.r) / 2 : this.Document.panX || 0;
-    private panY = () => this.fitToContent ? (this.contentBounds.y + this.contentBounds.b) / 2 : this.Document.panY || 0;
+    private panX = () => this.fitToContent ? (this.contentBounds.x + this.contentBounds.r) / 2 : this.Document._panX || 0;
+    private panY = () => this.fitToContent ? (this.contentBounds.y + this.contentBounds.b) / 2 : this.Document._panY || 0;
     private zoomScaling = () => (1 / this.parentScaling) * (this.fitToContent ?
         Math.min(this.props.PanelHeight() / (this.contentBounds.b - this.contentBounds.y), this.props.PanelWidth() / (this.contentBounds.r - this.contentBounds.x)) :
         this.Document.scale || 1)
@@ -146,13 +144,13 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
                         const layoutDoc = Doc.Layout(d);
                         d.x = x + NumCast(d.x) - dropX;
                         d.y = y + NumCast(d.y) - dropY;
-                        if (!NumCast(layoutDoc.width)) {
-                            layoutDoc.width = 300;
+                        if (!NumCast(layoutDoc._width)) {
+                            layoutDoc._width = 300;
                         }
-                        if (!NumCast(layoutDoc.height)) {
-                            const nw = NumCast(layoutDoc.nativeWidth);
-                            const nh = NumCast(layoutDoc.nativeHeight);
-                            layoutDoc.height = nw && nh ? nh / nw * NumCast(layoutDoc.width) : 300;
+                        if (!NumCast(layoutDoc._height)) {
+                            const nw = NumCast(layoutDoc._nativeWidth);
+                            const nh = NumCast(layoutDoc._nativeHeight);
+                            layoutDoc._height = nw && nh ? nh / nw * NumCast(layoutDoc._width) : 300;
                         }
                         this.bringToFront(d);
                     }));
@@ -182,8 +180,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
             const layoutDoc = Doc.Layout(cd);
             const cx = NumCast(cd.x) - this._clusterDistance;
             const cy = NumCast(cd.y) - this._clusterDistance;
-            const cw = NumCast(layoutDoc.width) + 2 * this._clusterDistance;
-            const ch = NumCast(layoutDoc.height) + 2 * this._clusterDistance;
+            const cw = NumCast(layoutDoc._width) + 2 * this._clusterDistance;
+            const ch = NumCast(layoutDoc._height) + 2 * this._clusterDistance;
             return !layoutDoc.z && intersectRect({ left: cx, top: cy, width: cw, height: ch }, { left: probe[0], top: probe[1], width: 1, height: 1 }) ?
                 NumCast(cd.cluster) : cluster;
         }, -1);
@@ -353,7 +351,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
             case GestureUtils.Gestures.Stroke:
                 const points = ge.points;
                 const B = this.getTransform().transformBounds(ge.bounds.left, ge.bounds.top, ge.bounds.width, ge.bounds.height);
-                const inkDoc = Docs.Create.InkDocument(InkingControl.Instance.selectedColor, InkingControl.Instance.selectedTool, parseInt(InkingControl.Instance.selectedWidth), points, { x: B.x, y: B.y, width: B.width, height: B.height });
+                const inkDoc = Docs.Create.InkDocument(InkingControl.Instance.selectedColor, InkingControl.Instance.selectedTool, parseInt(InkingControl.Instance.selectedWidth), points, { x: B.x, y: B.y, _width: B.width, _height: B.height });
                 this.addDocument(inkDoc);
                 e.stopPropagation();
                 break;
@@ -375,7 +373,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
                     }
                     return pass;
                 });
-                this.addDocument(Docs.Create.FreeformDocument(sel, { x: bounds.x, y: bounds.y, width: bWidth, height: bHeight, panX: 0, panY: 0 }));
+                this.addDocument(Docs.Create.FreeformDocument(sel, { x: bounds.x, y: bounds.y, _width: bWidth, _height: bHeight, _panX: 0, _panY: 0 }));
                 sel.forEach(d => this.props.removeDocument(d));
                 break;
 
@@ -397,11 +395,11 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         // I think it makes sense for the marquee menu to go away when panned. -syip2
         MarqueeOptionsMenu.Instance.fadeOut(true);
 
-        let x = this.Document.panX || 0;
-        let y = this.Document.panY || 0;
+        let x = this.Document._panX || 0;
+        let y = this.Document._panY || 0;
         const docs = this.childLayoutPairs.map(pair => pair.layout);
         const [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY);
-        if (!this.isAnnotationOverlay) {
+        if (!this.isAnnotationOverlay && docs.length) {
             PDFMenu.Instance.fadeOut(true);
             const minx = this.childDataProvider(docs[0]).x;//docs.length ? NumCast(docs[0].x) : 0;
             const miny = this.childDataProvider(docs[0]).y;//docs.length ? NumCast(docs[0].y) : 0;
@@ -591,8 +589,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
             const scale = this.getLocalTransform().inverse().Scale;
             const newPanX = Math.min((1 - 1 / scale) * this.nativeWidth, Math.max(0, panX));
             const newPanY = Math.min((this.props.Document.scrollHeight !== undefined ? NumCast(this.Document.scrollHeight) : (1 - 1 / scale) * this.nativeHeight), Math.max(0, panY));
-            this.Document.panX = this.isAnnotationOverlay ? newPanX : panX;
-            this.Document.panY = this.isAnnotationOverlay ? newPanY : panY;
+            this.Document._panX = this.isAnnotationOverlay ? newPanX : panX;
+            this.Document._panY = this.isAnnotationOverlay ? newPanY : panY;
         }
     }
 
@@ -616,14 +614,14 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
 
         // TODO This technically isn't correct if type !== "doc", as 
         // currently nothing is done, but we should probably push a new state
-        if (state.type === "doc" && this.Document.panX !== undefined && this.Document.panY !== undefined) {
+        if (state.type === "doc" && this.Document._panX !== undefined && this.Document._panY !== undefined) {
             const init = state.initializers![this.Document[Id]];
             if (!init) {
-                state.initializers![this.Document[Id]] = { panX: this.Document.panX, panY: this.Document.panY };
+                state.initializers![this.Document[Id]] = { panX: this.Document._panX, panY: this.Document._panY };
                 HistoryUtil.pushState(state);
             } else if (init.panX !== this.Document.panX || init.panY !== this.Document.panY) {
-                init.panX = this.Document.panX;
-                init.panY = this.Document.panY;
+                init.panX = this.Document._panX;
+                init.panY = this.Document._panY;
                 HistoryUtil.pushState(state);
             }
         }
@@ -639,13 +637,13 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
             }
         } else {
             const layoutdoc = Doc.Layout(doc);
-            const newPanX = NumCast(doc.x) + NumCast(layoutdoc.width) / 2;
-            const newPanY = NumCast(doc.y) + NumCast(layoutdoc.height) / 2;
+            const newPanX = NumCast(doc.x) + NumCast(layoutdoc._width) / 2;
+            const newPanY = NumCast(doc.y) + NumCast(layoutdoc._height) / 2;
             const newState = HistoryUtil.getState();
             newState.initializers![this.Document[Id]] = { panX: newPanX, panY: newPanY };
             HistoryUtil.pushState(newState);
 
-            const savedState = { px: this.Document.panX, py: this.Document.panY, s: this.Document.scale, pt: this.Document.panTransformType };
+            const savedState = { px: this.Document._panX, py: this.Document._panY, s: this.Document.scale, pt: this.Document.panTransformType };
 
             if (!doc.z) this.setPan(newPanX, newPanY, "Ease"); // docs that are floating in their collection can't be panned to from their collection -- need to propagate the pan to a parent freeform somehow
             Doc.BrushDoc(this.props.Document);
@@ -655,8 +653,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
 
             afterFocus && setTimeout(() => {
                 if (afterFocus && afterFocus()) {
-                    this.Document.panX = savedState.px;
-                    this.Document.panY = savedState.py;
+                    this.Document._panX = savedState.px;
+                    this.Document._panY = savedState.py;
                     this.Document.scale = savedState.s;
                     this.Document.panTransformType = savedState.pt;
                 }
@@ -666,7 +664,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     }
 
     setScaleToZoom = (doc: Doc, scale: number = 0.5) => {
-        this.Document.scale = scale * Math.min(this.props.PanelWidth() / NumCast(doc.width), this.props.PanelHeight() / NumCast(doc.height));
+        this.Document.scale = scale * Math.min(this.props.PanelWidth() / NumCast(doc._width), this.props.PanelHeight() / NumCast(doc._height));
     }
 
     zoomToScale = (scale: number) => {
@@ -709,7 +707,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
             return { ...result, transition: "transform 1s" };
         }
         const layoutDoc = Doc.Layout(params.doc);
-        return { x: Cast(params.doc.x, "number"), y: Cast(params.doc.y, "number"), z: Cast(params.doc.z, "number"), width: Cast(layoutDoc.width, "number"), height: Cast(layoutDoc.height, "number") };
+        return { x: Cast(params.doc.x, "number"), y: Cast(params.doc.y, "number"), z: Cast(params.doc.z, "number"), width: Cast(layoutDoc._width, "number"), height: Cast(layoutDoc._height, "number") };
     }
 
     viewDefsToJSX = (views: any[]) => {
@@ -735,7 +733,12 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         }
     }
 
-    childDataProvider = computedFn(function childDataProvider(this: any, doc: Doc) { return this._layoutPoolData.get(doc[Id]); }.bind(this));
+    childDataProvider = computedFn(function childDataProvider(this: any, doc: Doc) {
+        if (!doc) {
+            console.log(doc);
+        }
+        return this._layoutPoolData.get(doc[Id]);
+    }.bind(this));
 
     doPivotLayout(poolData: ObservableMap<string, any>) {
         return computePivotLayout(poolData, this.props.Document, this.childDocs,
@@ -761,7 +764,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
 
     get doLayoutComputation() {
         let computedElementData: { elements: ViewDefResult[] };
-        switch (this.Document.freeformLayoutEngine) {
+        switch (this.Document._freeformLayoutEngine) {
             case "pivot": computedElementData = this.doPivotLayout(this._layoutPoolData); break;
             default: computedElementData = this.doFreeformLayout(this._layoutPoolData); break;
         }
@@ -770,7 +773,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
                 ele: <CollectionFreeFormDocumentView key={pair.layout[Id]}  {...this.getChildDocumentViewProps(pair.layout, pair.data)}
                     dataProvider={this.childDataProvider}
                     jitterRotation={NumCast(this.props.Document.jitterRotation)}
-                    fitToBox={this.props.fitToBox || this.Document.freeformLayoutEngine === "pivot"} />,
+                    fitToBox={this.props.fitToBox || this.Document._freeformLayoutEngine === "pivot"} />,
                 bounds: this.childDataProvider(pair.layout)
             }));
 
@@ -797,12 +800,12 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     layoutDocsInGrid = () => {
         UndoManager.RunInBatch(() => {
             const docs = DocListCast(this.Document[this.props.fieldKey]);
-            const startX = this.Document.panX || 0;
+            const startX = this.Document._panX || 0;
             let x = startX;
-            let y = this.Document.panY || 0;
+            let y = this.Document._panY || 0;
             let i = 0;
-            const width = Math.max(...docs.map(doc => NumCast(doc.width)));
-            const height = Math.max(...docs.map(doc => NumCast(doc.height)));
+            const width = Math.max(...docs.map(doc => NumCast(doc._width)));
+            const height = Math.max(...docs.map(doc => NumCast(doc._height)));
             for (const doc of docs) {
                 doc.x = x;
                 doc.y = y;
@@ -880,8 +883,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         const layoutItems: ContextMenuProps[] = [];
 
         layoutItems.push({ description: "reset view", event: () => { this.props.Document.panX = this.props.Document.panY = 0; this.props.Document.scale = 1; }, icon: "compress-arrows-alt" });
-        layoutItems.push({ description: `${this.Document.LODdisable ? "Enable LOD" : "Disable LOD"}`, event: () => this.Document.LODdisable = !this.Document.LODdisable, icon: "table" });
-        layoutItems.push({ description: `${this.fitToContent ? "Unset" : "Set"} Fit To Container`, event: () => this.Document.fitToBox = !this.fitToContent, icon: !this.fitToContent ? "expand-arrows-alt" : "compress-arrows-alt" });
+        layoutItems.push({ description: `${this.Document._LODdisable ? "Enable LOD" : "Disable LOD"}`, event: () => this.Document._LODdisable = !this.Document._LODdisable, icon: "table" });
+        layoutItems.push({ description: `${this.fitToContent ? "Unset" : "Set"} Fit To Container`, event: () => this.Document._fitToBox = !this.fitToContent, icon: !this.fitToContent ? "expand-arrows-alt" : "compress-arrows-alt" });
         layoutItems.push({ description: `${this.Document.useClusters ? "Uncluster" : "Use Clusters"}`, event: () => this.updateClusters(!this.Document.useClusters), icon: "braille" });
         layoutItems.push({ description: "Arrange contents in grid", event: this.layoutDocsInGrid, icon: "table" });
         layoutItems.push({ description: "Analyze Strokes", event: this.analyzeStrokes, icon: "paint-brush" });
@@ -918,7 +921,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
             description: "Add Note ...",
             subitems: DocListCast((CurrentUserUtils.UserDocument.noteTypes as Doc).data).map((note, i) => ({
                 description: (i + 1) + ": " + StrCast(note.title),
-                event: (args: { x: number, y: number }) => this.addLiveTextBox(Docs.Create.TextDocument({ width: 200, height: 100, x: this.getTransform().transformPoint(args.x, args.y)[0], y: this.getTransform().transformPoint(args.x, args.y)[1], autoHeight: true, layout: note, title: StrCast(note.title) })),
+                event: (args: { x: number, y: number }) => this.addLiveTextBox(Docs.Create.TextDocument({ _width: 200, _height: 100, x: this.getTransform().transformPoint(args.x, args.y)[0], y: this.getTransform().transformPoint(args.x, args.y)[1], autoHeight: true, layout: note, title: StrCast(note.title) })),
                 icon: "eye"
             })) as ContextMenuProps[],
             icon: "eye"
@@ -939,7 +942,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
 
     children = () => {
         const eles: JSX.Element[] = [];
-        this.extensionDoc && (eles.push(...this.childViews()));
+        eles.push(...this.childViews());
         // this._palette && (eles.push(this._palette));
         // this.currentStroke && (eles.push(this.currentStroke));
         eles.push(<CollectionFreeFormRemoteCursors {...this.props} key="remoteCursors" />);
@@ -951,7 +954,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         </div>;
     }
     @computed get marqueeView() {
-        return <MarqueeView {...this.props} extensionDoc={this.extensionDoc!} activeDocuments={this.getActiveDocuments} selectDocuments={this.selectDocuments} addDocument={this.addDocument}
+        return <MarqueeView {...this.props} activeDocuments={this.getActiveDocuments} selectDocuments={this.selectDocuments} addDocument={this.addDocument}
             addLiveTextDocument={this.addLiveTextBox} getContainerTransform={this.getContainerTransform} getTransform={this.getTransform} isAnnotationOverlay={this.isAnnotationOverlay}>
             <CollectionFreeFormViewPannableContents centeringShiftX={this.centeringShiftX} centeringShiftY={this.centeringShiftY}
                 easing={this.easing} zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}>
@@ -973,7 +976,6 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         // this.Document.fitH = this.contentBounds && (this.contentBounds.b - this.contentBounds.y);
         // if isAnnotationOverlay is set, then children will be stored in the extension document for the fieldKey.
         // otherwise, they are stored in fieldKey.  All annotations to this document are stored in the extension document
-        if (!this.extensionDoc) return (null);
         // let lodarea = this.Document[WidthSym]() * this.Document[HeightSym]() / this.props.ScreenToLocalTransform().Scale / this.props.ScreenToLocalTransform().Scale;
         return <div className={"collectionfreeformview-container"}
             ref={this.createDashEventsTarget}
@@ -986,7 +988,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
                 width: this.contentScaling ? `${100 / this.contentScaling}%` : "",
                 height: this.contentScaling ? `${100 / this.contentScaling}%` : this.isAnnotationOverlay ? (this.props.Document.scrollHeight ? this.Document.scrollHeight : "100%") : this.props.PanelHeight()
             }}>
-            {!this.Document.LODdisable && !this.props.active() && !this.props.isAnnotationOverlay && !this.props.annotationsKey && this.props.renderDepth > 0 ? // && this.props.CollectionView && lodarea < NumCast(this.Document.LODarea, 100000) ?
+            {!this.Document._LODdisable && !this.props.active() && !this.props.isAnnotationOverlay && !this.props.annotationsKey && this.props.renderDepth > 0 ? // && this.props.CollectionView && lodarea < NumCast(this.Document.LODarea, 100000) ?
                 this.placeholder : this.marqueeView}
             <CollectionFreeFormOverlayView elements={this.elementFunc} />
         </div>;
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 584d18372..0e02d1ef2 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -29,7 +29,6 @@ interface MarqueeViewProps {
     removeDocument: (doc: Doc) => boolean;
     addLiveTextDocument: (doc: Doc) => void;
     isSelected: () => boolean;
-    extensionDoc: Doc;
     isAnnotationOverlay?: boolean;
     setPreviewCursor?: (func: (x: number, y: number, drag: boolean) => void) => void;
 }
@@ -85,7 +84,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
                 }
                 ns.map(line => {
                     const indent = line.search(/\S|$/);
-                    const newBox = Docs.Create.TextDocument({ width: 200, height: 35, x: x + indent / 3 * 10, y: y, documentText: "@@@" + line, title: line });
+                    const newBox = Docs.Create.TextDocument({ _width: 200, _height: 35, x: x + indent / 3 * 10, y: y, documentText: "@@@" + line, title: line });
                     this.props.addDocument(newBox);
                     y += 40 * this.props.getTransform().Scale;
                 });
@@ -95,17 +94,17 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
             navigator.clipboard.readText().then(text => {
                 const ns = text.split("\n").filter(t => t.trim() !== "\r" && t.trim() !== "");
                 if (ns.length === 1 && text.startsWith("http")) {
-                    this.props.addDocument(Docs.Create.ImageDocument(text, { nativeWidth: 300, width: 300, x: x, y: y }));// paste an image from its URL in the paste buffer
+                    this.props.addDocument(Docs.Create.ImageDocument(text, { _nativeWidth: 300, _width: 300, x: x, y: y }));// paste an image from its URL in the paste buffer
                 } else {
                     this.pasteTable(ns, x, y);
                 }
             });
         } else if (!e.ctrlKey) {
             this.props.addLiveTextDocument(
-                Docs.Create.TextDocument({ width: 200, height: 100, x: x, y: y, autoHeight: true, title: "-typed text-" }));
+                Docs.Create.TextDocument({ _width: 200, _height: 100, x: x, y: y, autoHeight: true, title: "-typed text-" }));
         } else if (e.keyCode > 48 && e.keyCode <= 57) {
             const notes = DocListCast((CurrentUserUtils.UserDocument.noteTypes as Doc).data);
-            const text = Docs.Create.TextDocument({ width: 200, height: 100, x: x, y: y, autoHeight: true, title: "-typed text-" });
+            const text = Docs.Create.TextDocument({ _width: 200, _height: 100, x: x, y: y, autoHeight: true, title: "-typed text-" });
             text.layout = notes[(e.keyCode - 49) % notes.length];
             this.props.addLiveTextDocument(text);
         }
@@ -128,7 +127,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
             let groupAttr: string | number = "";
             const rowProto = new Doc();
             rowProto.title = rowProto.Id;
-            rowProto.width = 200;
+            rowProto._width = 200;
             rowProto.isPrototype = true;
             for (let i = 1; i < ns.length - 1; i++) {
                 const values = ns[i].split("\t");
@@ -144,10 +143,10 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
                 }
                 docDataProto.title = i.toString();
                 const doc = Doc.MakeDelegate(docDataProto);
-                doc.width = 200;
+                doc._width = 200;
                 docList.push(doc);
             }
-            const newCol = Docs.Create.SchemaDocument([...(groupAttr ? [new SchemaHeaderField("_group", "#f1efeb")] : []), ...columns.filter(c => c).map(c => new SchemaHeaderField(c, "#f1efeb"))], docList, { x: x, y: y, title: "droppedTable", width: 300, height: 100 });
+            const newCol = Docs.Create.SchemaDocument([...(groupAttr ? [new SchemaHeaderField("_group", "#f1efeb")] : []), ...columns.filter(c => c).map(c => new SchemaHeaderField(c, "#f1efeb"))], docList, { x: x, y: y, title: "droppedTable", _width: 300, _height: 100 });
 
             this.props.addDocument(newCol);
         }
@@ -267,15 +266,15 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
     }
 
     get inkDoc() {
-        return this.props.extensionDoc;
+        return this.props.Document;
     }
 
     get ink() { // ink will be stored on the extension doc for the field (fieldKey) where the container's data is stored.
-        return this.props.extensionDoc && Cast(this.props.extensionDoc.ink, InkField);
+        return Cast(this.props.Document.ink, InkField);
     }
 
     set ink(value: InkField | undefined) {
-        this.props.extensionDoc && (this.props.extensionDoc.ink = value);
+        this.props.Document.ink = value;
     }
 
     @action
@@ -325,13 +324,13 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
         const newCollection = Docs.Create.FreeformDocument(selected, {
             x: bounds.left,
             y: bounds.top,
-            panX: 0,
-            panY: 0,
+            _panX: 0,
+            _panY: 0,
             backgroundColor: this.props.isAnnotationOverlay ? undefined : chosenColor,
             defaultBackgroundColor: this.props.isAnnotationOverlay ? undefined : chosenColor,
-            width: bounds.width,
-            height: bounds.height,
-            LODdisable: true,
+            _width: bounds.width,
+            _height: bounds.height,
+            _LODdisable: true,
             title: "a nested collection",
         });
         // const dataExtensionField = Doc.CreateDocumentExtensionForField(newCollection, "data");
@@ -374,16 +373,17 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
             d.page = -1;
             return d;
         });
-        newCollection.chromeStatus = "disabled";
-        const summary = Docs.Create.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, autoHeight: true, backgroundColor: "#e2ad32" /* yellow */, title: "-summary-" });
+        newCollection._chromeStatus = "disabled";
+        const summary = Docs.Create.TextDocument({ x: bounds.left, y: bounds.top, _width: 300, _height: 100, autoHeight: true, backgroundColor: "#e2ad32" /* yellow */, title: "-summary-" });
         Doc.GetProto(summary).summarizedDocs = new List<Doc>([newCollection]);
         newCollection.x = bounds.left + bounds.width;
         Doc.GetProto(newCollection).summaryDoc = summary;
         Doc.GetProto(newCollection).title = ComputedField.MakeFunction(`summaryTitle(this);`);
         if (e instanceof KeyboardEvent ? e.key === "s" : true) { // summary is wrapped in an expand/collapse container that also contains the summarized documents in a free form view.
-            const container = Docs.Create.FreeformDocument([summary, newCollection], { x: bounds.left, y: bounds.top, width: 300, height: 200, chromeStatus: "disabled", title: "-summary-" });
-            container.viewType = CollectionViewType.Stacking;
-            container.autoHeight = true;
+            const container = Docs.Create.FreeformDocument([summary, newCollection], {
+                x: bounds.left, y: bounds.top, _width: 300, _height: 200, autoHeight: true,
+                _viewType: CollectionViewType.Stacking, _chromeStatus: "disabled", title: "-summary-"
+            });
             Doc.GetProto(summary).maximizeLocation = "inPlace";  // or "onRight"
             this.props.addLiveTextDocument(container);
         } else if (e instanceof KeyboardEvent ? e.key === "S" : false) { // the summary stands alone, but is linked to a collection of the summarized documents - set the OnCLick behavior to link follow to access them
@@ -468,8 +468,8 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
             const layoutDoc = Doc.Layout(doc);
             const x = NumCast(doc.x);
             const y = NumCast(doc.y);
-            const w = NumCast(layoutDoc.width);
-            const h = NumCast(layoutDoc.height);
+            const w = NumCast(layoutDoc._width);
+            const h = NumCast(layoutDoc._height);
             if (this.intersectRect({ left: x, top: y, width: w, height: h }, selRect)) {
                 selection.push(doc);
             }
@@ -479,8 +479,8 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
                 const layoutDoc = Doc.Layout(doc);
                 const x = NumCast(doc.x);
                 const y = NumCast(doc.y);
-                const w = NumCast(layoutDoc.width);
-                const h = NumCast(layoutDoc.height);
+                const w = NumCast(layoutDoc._width);
+                const h = NumCast(layoutDoc._height);
                 if (this.intersectRect({ left: x, top: y, width: w, height: h }, selRect)) {
                     selection.push(doc);
                 }
@@ -496,8 +496,8 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
                 const layoutDoc = Doc.Layout(doc);
                 const x = NumCast(doc.x);
                 const y = NumCast(doc.y);
-                const w = NumCast(layoutDoc.width);
-                const h = NumCast(layoutDoc.height);
+                const w = NumCast(layoutDoc._width);
+                const h = NumCast(layoutDoc._height);
                 if (this.intersectRect({ left: x, top: y, width: w, height: h }, otherBounds)) {
                     selection.push(doc);
                 }
diff --git a/src/client/views/linking/LinkEditor.tsx b/src/client/views/linking/LinkEditor.tsx
index bb8a8b47b..e3bf6b5f8 100644
--- a/src/client/views/linking/LinkEditor.tsx
+++ b/src/client/views/linking/LinkEditor.tsx
@@ -292,7 +292,7 @@ export class LinkGroupEditor extends React.Component<LinkGroupEditorProps> {
         if (index > -1) keys.splice(index, 1);
         const cols = ["anchor1", "anchor2", ...[...keys]].map(c => new SchemaHeaderField(c, "#f1efeb"));
         const docs: Doc[] = LinkManager.Instance.getAllMetadataDocsInGroup(groupType);
-        const createTable = action(() => Docs.Create.SchemaDocument(cols, docs, { width: 500, height: 300, title: groupType + " table" }));
+        const createTable = action(() => Docs.Create.SchemaDocument(cols, docs, { _width: 500, _height: 300, title: groupType + " table" }));
         const ref = React.createRef<HTMLDivElement>();
         return <div ref={ref}><button className="linkEditor-button" onPointerDown={SetupDrag(ref, createTable)} title="Drag to view relationship table"><FontAwesomeIcon icon="table" size="sm" /></button></div>;
     }
diff --git a/src/client/views/linking/LinkFollowBox.tsx b/src/client/views/linking/LinkFollowBox.tsx
index 29e167ff7..325c92413 100644
--- a/src/client/views/linking/LinkFollowBox.tsx
+++ b/src/client/views/linking/LinkFollowBox.tsx
@@ -89,7 +89,7 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
     async resetPan() {
         if (LinkFollowBox.destinationDoc && this.sourceView && this.sourceView.props.ContainingCollectionDoc) {
             runInAction(() => this.canPan = false);
-            if (this.sourceView.props.ContainingCollectionDoc.viewType === CollectionViewType.Freeform) {
+            if (this.sourceView.props.ContainingCollectionDoc._viewType === CollectionViewType.Freeform) {
                 const docs = Cast(this.sourceView.props.ContainingCollectionDoc.data, listSpec(Doc), []);
                 const aliases = await SearchUtil.GetViewsOfDocument(Doc.GetProto(LinkFollowBox.destinationDoc));
 
@@ -165,11 +165,11 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
     @undoBatch
     openColFullScreen = (options: { context: Doc }) => {
         if (LinkFollowBox.destinationDoc) {
-            if (NumCast(options.context.viewType, CollectionViewType.Invalid) === CollectionViewType.Freeform) {
-                const newPanX = NumCast(LinkFollowBox.destinationDoc.x) + NumCast(LinkFollowBox.destinationDoc.width) / 2;
-                const newPanY = NumCast(LinkFollowBox.destinationDoc.y) + NumCast(LinkFollowBox.destinationDoc.height) / 2;
-                options.context.panX = newPanX;
-                options.context.panY = newPanY;
+            if (NumCast(options.context._viewType, CollectionViewType.Invalid) === CollectionViewType.Freeform) {
+                const newPanX = NumCast(LinkFollowBox.destinationDoc.x) + NumCast(LinkFollowBox.destinationDoc._width) / 2;
+                const newPanY = NumCast(LinkFollowBox.destinationDoc.y) + NumCast(LinkFollowBox.destinationDoc._height) / 2;
+                options.context._panX = newPanX;
+                options.context._panY = newPanY;
             }
             const view = DocumentManager.Instance.getDocumentView(options.context);
             view && CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(view);
@@ -193,11 +193,11 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
     openLinkColRight = (options: { context: Doc, shouldZoom: boolean }) => {
         if (LinkFollowBox.destinationDoc) {
             options.context = Doc.IsPrototype(options.context) ? Doc.MakeDelegate(options.context) : options.context;
-            if (NumCast(options.context.viewType, CollectionViewType.Invalid) === CollectionViewType.Freeform) {
-                const newPanX = NumCast(LinkFollowBox.destinationDoc.x) + NumCast(LinkFollowBox.destinationDoc.width) / 2;
-                const newPanY = NumCast(LinkFollowBox.destinationDoc.y) + NumCast(LinkFollowBox.destinationDoc.height) / 2;
-                options.context.panX = newPanX;
-                options.context.panY = newPanY;
+            if (NumCast(options.context._viewType, CollectionViewType.Invalid) === CollectionViewType.Freeform) {
+                const newPanX = NumCast(LinkFollowBox.destinationDoc.x) + NumCast(LinkFollowBox.destinationDoc._width) / 2;
+                const newPanY = NumCast(LinkFollowBox.destinationDoc.y) + NumCast(LinkFollowBox.destinationDoc._height) / 2;
+                options.context._panX = newPanX;
+                options.context._panY = newPanY;
             }
             (LinkFollowBox._addDocTab || this.props.addDocTab)(options.context, undefined, "onRight");
 
@@ -245,11 +245,11 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
     openLinkColTab = (options: { context: Doc, shouldZoom: boolean }) => {
         if (LinkFollowBox.destinationDoc) {
             options.context = Doc.IsPrototype(options.context) ? Doc.MakeDelegate(options.context) : options.context;
-            if (NumCast(options.context.viewType, CollectionViewType.Invalid) === CollectionViewType.Freeform) {
-                const newPanX = NumCast(LinkFollowBox.destinationDoc.x) + NumCast(LinkFollowBox.destinationDoc.width) / 2;
-                const newPanY = NumCast(LinkFollowBox.destinationDoc.y) + NumCast(LinkFollowBox.destinationDoc.height) / 2;
-                options.context.panX = newPanX;
-                options.context.panY = newPanY;
+            if (NumCast(options.context._viewType, CollectionViewType.Invalid) === CollectionViewType.Freeform) {
+                const newPanX = NumCast(LinkFollowBox.destinationDoc.x) + NumCast(LinkFollowBox.destinationDoc._width) / 2;
+                const newPanY = NumCast(LinkFollowBox.destinationDoc.y) + NumCast(LinkFollowBox.destinationDoc._height) / 2;
+                options.context._panX = newPanX;
+                options.context._panY = newPanY;
             }
             (LinkFollowBox._addDocTab || this.props.addDocTab)(options.context, undefined, "inTab");
             if (options.shouldZoom) this.jumpToLink({ shouldZoom: options.shouldZoom });
@@ -270,13 +270,13 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
                     const y = NumCast(LinkFollowBox.sourceDoc.y);
                     const x = NumCast(LinkFollowBox.sourceDoc.x);
 
-                    const width = NumCast(LinkFollowBox.sourceDoc.width);
-                    const height = NumCast(LinkFollowBox.sourceDoc.height);
+                    const width = NumCast(LinkFollowBox.sourceDoc._width);
+                    const height = NumCast(LinkFollowBox.sourceDoc._height);
 
                     alias.x = x + width + 30;
                     alias.y = y;
-                    alias.width = width;
-                    alias.height = height;
+                    alias._width = width;
+                    alias._height = height;
 
                     this.sourceView.props.addDocument(alias);
                 }
@@ -361,7 +361,7 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
     get canOpenInPlace() {
         if (this.sourceView && this.sourceView.props.ContainingCollectionDoc) {
             const colDoc = this.sourceView.props.ContainingCollectionDoc;
-            if (colDoc.viewType && colDoc.viewType === CollectionViewType.Freeform) return true;
+            if (colDoc._viewType === CollectionViewType.Freeform) return true;
         }
         return false;
     }
@@ -481,7 +481,7 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
         let contextMatch: boolean = false;
         if (this.selectedContextAliases) {
             this.selectedContextAliases.forEach(alias => {
-                if (alias.viewType === CollectionViewType.Freeform) contextMatch = true;
+                if (alias._viewType === CollectionViewType.Freeform) contextMatch = true;
             });
         }
         if (contextMatch) return true;
@@ -523,7 +523,7 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
 
     render() {
         return (
-            <div className="linkFollowBox-main" style={{ height: NumCast(this.props.Document.height), width: NumCast(this.props.Document.width) }}>
+            <div className="linkFollowBox-main" style={{ height: NumCast(this.props.Document._height), width: NumCast(this.props.Document._width) }}>
                 <div className="linkFollowBox-header">
                     <div className="topHeader">
                         {LinkFollowBox.linkDoc ? "Link Title: " + StrCast(LinkFollowBox.linkDoc.title) : "No Link Selected"}
@@ -533,7 +533,7 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
                         LinkFollowBox.sourceDoc && LinkFollowBox.destinationDoc ? "Source: " + StrCast(LinkFollowBox.sourceDoc.title) + ", Destination: " + StrCast(LinkFollowBox.destinationDoc.title)
                             : "" : ""}</div>
                 </div>
-                <div className="linkFollowBox-content" style={{ height: NumCast(this.props.Document.height) - 110 }}>
+                <div className="linkFollowBox-content" style={{ height: NumCast(this.props.Document._height) - 110 }}>
                     <div className="linkFollowBox-item">
                         <div className="linkFollowBox-item title">Mode</div>
                         <div className="linkFollowBox-itemContent">
diff --git a/src/client/views/linking/LinkMenuGroup.tsx b/src/client/views/linking/LinkMenuGroup.tsx
index ace9a9e4c..0c38ff45c 100644
--- a/src/client/views/linking/LinkMenuGroup.tsx
+++ b/src/client/views/linking/LinkMenuGroup.tsx
@@ -58,7 +58,7 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> {
         if (index > -1) keys.splice(index, 1);
         const cols = ["anchor1", "anchor2", ...[...keys]].map(c => new SchemaHeaderField(c, "#f1efeb"));
         const docs: Doc[] = LinkManager.Instance.getAllMetadataDocsInGroup(groupType);
-        const createTable = action(() => Docs.Create.SchemaDocument(cols, docs, { width: 500, height: 300, title: groupType + " table" }));
+        const createTable = action(() => Docs.Create.SchemaDocument(cols, docs, { _width: 500, _height: 300, title: groupType + " table" }));
         const ref = React.createRef<HTMLDivElement>();
         return <div ref={ref}><button className="linkEditor-button linkEditor-tableButton" onPointerDown={SetupDrag(ref, createTable)} title="Drag to view relationship table"><FontAwesomeIcon icon="table" size="sm" /></button></div>;
     }
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 95c765e8a..8ede79edc 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -68,7 +68,7 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
                 this.Document.playOnSelect && sel && !Doc.AreProtosEqual(sel, this.props.Document) && this.playFrom(DateCast(sel.creationTime).date.getTime());
             });
         this._scrubbingDisposer = reaction(() => AudioBox._scrubTime, timeInMillisecondsFrom1970 => {
-            const start = this.extensionDoc && DateCast(this.extensionDoc.recordingStart);
+            const start = DateCast(this.dataDoc[this.props.fieldKey + "-recordingStart"]);
             start && this.playFrom((timeInMillisecondsFrom1970 - start.date.getTime()) / 1000);
         });
     }
@@ -128,13 +128,12 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
     recordAudioAnnotation = () => {
         let gumStream: any;
         const self = this;
-        const extensionDoc = this.extensionDoc;
-        extensionDoc && navigator.mediaDevices.getUserMedia({
+        navigator.mediaDevices.getUserMedia({
             audio: true
         }).then(function (stream) {
             gumStream = stream;
             self._recorder = new MediaRecorder(stream);
-            extensionDoc.recordingStart = new DateField(new Date());
+            self.dataDoc[self.props.fieldKey + "-recordingStart"] = new DateField(new Date());
             AudioBox.ActiveRecordings.push(self.props.Document);
             self._recorder.ondataavailable = async function (e: any) {
                 const formData = new FormData();
@@ -213,55 +212,53 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
 
     render() {
         const interactive = this.active() ? "-interactive" : "";
-        return (!this.extensionDoc ? (null) :
-            <div className={`audiobox-container`} onContextMenu={this.specificContextMenu}
-                onClick={!this.path ? this.recordClick : undefined}>
-                <div className="audiobox-handle"></div>
-                {!this.path ?
-                    <button className={`audiobox-record${interactive}`} style={{ backgroundColor: this._audioState === "recording" ? "red" : "black" }}>
-                        {this._audioState === "recording" ? "STOP" : "RECORD"}
-                    </button> :
-                    <div className="audiobox-controls">
-                        <div className="audiobox-player" onClick={this.onPlay}>
-                            <div className="audiobox-playhead"> <FontAwesomeIcon style={{ width: "100%" }} icon={this._playing ? "pause" : "play"} size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /></div>
-                            <div className="audiobox-playhead" onClick={this.onStop}><FontAwesomeIcon style={{ width: "100%" }} icon="stop" size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /></div>
-                            <div className="audiobox-timeline" onClick={e => e.stopPropagation()}
-                                onPointerDown={e => {
-                                    if (e.button === 0 && !e.ctrlKey) {
-                                        const rect = (e.target as any).getBoundingClientRect();
-                                        this._ele!.currentTime = this.Document.currentTimecode = (e.clientX - rect.x) / rect.width * NumCast(this.dataDoc.duration);
-                                        this.pause();
-                                        e.stopPropagation();
-                                    }
-                                }} >
-                                {DocListCast(this.dataDoc.links).map((l, i) => {
-                                    let la1 = l.anchor1 as Doc;
-                                    let la2 = l.anchor2 as Doc;
-                                    let linkTime = NumCast(l.anchor2Timecode);
-                                    if (Doc.AreProtosEqual(la1, this.dataDoc)) {
-                                        la1 = l.anchor2 as Doc;
-                                        la2 = l.anchor1 as Doc;
-                                        linkTime = NumCast(l.anchor1Timecode);
-                                    }
-                                    return !linkTime ? (null) :
-                                        <div className={this.props.PanelHeight() < 32 ? "audiobox-marker-minicontainer" : "audiobox-marker-container"} key={l[Id]} style={{ left: `${linkTime / NumCast(this.dataDoc.duration, 1) * 100}%` }}>
-                                            <div className={this.props.PanelHeight() < 32 ? "audioBox-linker-mini" : "audioBox-linker"} key={"linker" + i}>
-                                                <DocumentView {...this.props} Document={l} layoutKey={Doc.LinkEndpoint(l, la2)}
-                                                    parentActive={returnTrue} bringToFront={emptyFunction} zoomToScale={emptyFunction} getScale={returnOne}
-                                                    backgroundColor={returnTransparent} />
-                                            </div>
-                                            <div key={i} className="audiobox-marker" onPointerEnter={() => Doc.linkFollowHighlight(la1)}
-                                                onPointerDown={e => { if (e.button === 0 && !e.ctrlKey) { this.playFrom(linkTime); e.stopPropagation(); } }}
-                                                onClick={e => { if (e.button === 0 && !e.ctrlKey) { this.pause(); e.stopPropagation(); } }} />
-                                        </div>;
-                                })}
-                                <div className="audiobox-current" style={{ left: `${NumCast(this.Document.currentTimecode) / NumCast(this.dataDoc.duration, 1) * 100}%` }} />
-                                {this.audio}
-                            </div>
+        return <div className={`audiobox-container`} onContextMenu={this.specificContextMenu}
+            onClick={!this.path ? this.recordClick : undefined}>
+            <div className="audiobox-handle"></div>
+            {!this.path ?
+                <button className={`audiobox-record${interactive}`} style={{ backgroundColor: this._audioState === "recording" ? "red" : "black" }}>
+                    {this._audioState === "recording" ? "STOP" : "RECORD"}
+                </button> :
+                <div className="audiobox-controls">
+                    <div className="audiobox-player" onClick={this.onPlay}>
+                        <div className="audiobox-playhead"> <FontAwesomeIcon style={{ width: "100%" }} icon={this._playing ? "pause" : "play"} size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /></div>
+                        <div className="audiobox-playhead" onClick={this.onStop}><FontAwesomeIcon style={{ width: "100%" }} icon="stop" size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /></div>
+                        <div className="audiobox-timeline" onClick={e => e.stopPropagation()}
+                            onPointerDown={e => {
+                                if (e.button === 0 && !e.ctrlKey) {
+                                    const rect = (e.target as any).getBoundingClientRect();
+                                    this._ele!.currentTime = this.Document.currentTimecode = (e.clientX - rect.x) / rect.width * NumCast(this.dataDoc.duration);
+                                    this.pause();
+                                    e.stopPropagation();
+                                }
+                            }} >
+                            {DocListCast(this.dataDoc.links).map((l, i) => {
+                                let la1 = l.anchor1 as Doc;
+                                let la2 = l.anchor2 as Doc;
+                                let linkTime = NumCast(l.anchor2Timecode);
+                                if (Doc.AreProtosEqual(la1, this.dataDoc)) {
+                                    la1 = l.anchor2 as Doc;
+                                    la2 = l.anchor1 as Doc;
+                                    linkTime = NumCast(l.anchor1Timecode);
+                                }
+                                return !linkTime ? (null) :
+                                    <div className={this.props.PanelHeight() < 32 ? "audiobox-marker-minicontainer" : "audiobox-marker-container"} key={l[Id]} style={{ left: `${linkTime / NumCast(this.dataDoc.duration, 1) * 100}%` }}>
+                                        <div className={this.props.PanelHeight() < 32 ? "audioBox-linker-mini" : "audioBox-linker"} key={"linker" + i}>
+                                            <DocumentView {...this.props} Document={l} layoutKey={Doc.LinkEndpoint(l, la2)}
+                                                parentActive={returnTrue} bringToFront={emptyFunction} zoomToScale={emptyFunction} getScale={returnOne}
+                                                backgroundColor={returnTransparent} />
+                                        </div>
+                                        <div key={i} className="audiobox-marker" onPointerEnter={() => Doc.linkFollowHighlight(la1)}
+                                            onPointerDown={e => { if (e.button === 0 && !e.ctrlKey) { this.playFrom(linkTime); e.stopPropagation(); } }}
+                                            onClick={e => { if (e.button === 0 && !e.ctrlKey) { this.pause(); e.stopPropagation(); } }} />
+                                    </div>;
+                            })}
+                            <div className="audiobox-current" style={{ left: `${NumCast(this.Document.currentTimecode) / NumCast(this.dataDoc.duration, 1) * 100}%` }} />
+                            {this.audio}
                         </div>
                     </div>
-                }
-            </div>
-        );
+                </div>
+            }
+        </div>;
     }
 }
\ No newline at end of file
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index cd78f5d49..ea3c505d8 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -38,18 +38,18 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
         return (hgt === undefined && this.nativeWidth && this.nativeHeight) ? this.width * this.nativeHeight / this.nativeWidth : hgt;
     }
     @computed get dataProvider() { return this.props.dataProvider && this.props.dataProvider(this.props.Document) ? this.props.dataProvider(this.props.Document) : undefined; }
-    @computed get nativeWidth() { return NumCast(this.layoutDoc.nativeWidth); }
-    @computed get nativeHeight() { return NumCast(this.layoutDoc.nativeHeight); }
+    @computed get nativeWidth() { return NumCast(this.layoutDoc._nativeWidth); }
+    @computed get nativeHeight() { return NumCast(this.layoutDoc._nativeHeight); }
 
     @computed get renderScriptDim() {
         if (this.Document.renderScript) {
             const someView = Cast(this.props.Document.someView, Doc);
             const minimap = Cast(this.props.Document.minimap, Doc);
             if (someView instanceof Doc && minimap instanceof Doc) {
-                const x = (NumCast(someView.panX) - NumCast(someView.width) / 2 / NumCast(someView.scale) - (NumCast(minimap.fitX) - NumCast(minimap.fitW) / 2)) / NumCast(minimap.fitW) * NumCast(minimap.width) - NumCast(minimap.width) / 2;
-                const y = (NumCast(someView.panY) - NumCast(someView.height) / 2 / NumCast(someView.scale) - (NumCast(minimap.fitY) - NumCast(minimap.fitH) / 2)) / NumCast(minimap.fitH) * NumCast(minimap.height) - NumCast(minimap.height) / 2;
-                const w = NumCast(someView.width) / NumCast(someView.scale) / NumCast(minimap.fitW) * NumCast(minimap.width);
-                const h = NumCast(someView.height) / NumCast(someView.scale) / NumCast(minimap.fitH) * NumCast(minimap.height);
+                const x = (NumCast(someView._panX) - NumCast(someView._width) / 2 / NumCast(someView.scale) - (NumCast(minimap.fitX) - NumCast(minimap.fitW) / 2)) / NumCast(minimap.fitW) * NumCast(minimap._width) - NumCast(minimap._width) / 2;
+                const y = (NumCast(someView._panY) - NumCast(someView._height) / 2 / NumCast(someView.scale) - (NumCast(minimap.fitY) - NumCast(minimap.fitH) / 2)) / NumCast(minimap.fitH) * NumCast(minimap._height) - NumCast(minimap._height) / 2;
+                const w = NumCast(someView._width) / NumCast(someView.scale) / NumCast(minimap.fitW) * NumCast(minimap.width);
+                const h = NumCast(someView._height) / NumCast(someView.scale) / NumCast(minimap.fitH) * NumCast(minimap.height);
                 return { x: x, y: y, width: w, height: h };
             }
         }
diff --git a/src/client/views/nodes/ContentFittingDocumentView.tsx b/src/client/views/nodes/ContentFittingDocumentView.tsx
index 96102e0e8..4f1bbd33c 100644
--- a/src/client/views/nodes/ContentFittingDocumentView.tsx
+++ b/src/client/views/nodes/ContentFittingDocumentView.tsx
@@ -44,8 +44,8 @@ interface ContentFittingDocumentViewProps {
 export class ContentFittingDocumentView extends React.Component<ContentFittingDocumentViewProps>{
     public get displayName() { return "DocumentView(" + this.props.Document?.title + ")"; } // this makes mobx trace() statements more descriptive
     private get layoutDoc() { return this.props.Document && Doc.Layout(this.props.Document); }
-    private get nativeWidth() { return NumCast(this.layoutDoc?.nativeWidth, this.props.PanelWidth()); }
-    private get nativeHeight() { return NumCast(this.layoutDoc?.nativeHeight, this.props.PanelHeight()); }
+    private get nativeWidth() { return NumCast(this.layoutDoc?._nativeWidth, this.props.PanelWidth()); }
+    private get nativeHeight() { return NumCast(this.layoutDoc?._nativeHeight, this.props.PanelHeight()); }
     private contentScaling = () => {
         const wscale = this.props.PanelWidth() / (this.nativeWidth || this.props.PanelWidth() || 1);
         if (wscale * this.nativeHeight > this.props.PanelHeight()) {
@@ -68,10 +68,10 @@ export class ContentFittingDocumentView extends React.Component<ContentFittingDo
         }
         return true;
     }
-    private PanelWidth = () => this.nativeWidth && (!this.props.Document || !this.props.Document.fitWidth) ? this.nativeWidth * this.contentScaling() : this.props.PanelWidth();
-    private PanelHeight = () => this.nativeHeight && (!this.props.Document || !this.props.Document.fitWidth) ? this.nativeHeight * this.contentScaling() : this.props.PanelHeight();
+    private PanelWidth = () => this.nativeWidth && (!this.props.Document || !this.props.Document._fitWidth) ? this.nativeWidth * this.contentScaling() : this.props.PanelWidth();
+    private PanelHeight = () => this.nativeHeight && (!this.props.Document || !this.props.Document._fitWidth) ? this.nativeHeight * this.contentScaling() : this.props.PanelHeight();
     private getTransform = () => this.props.getTransform().translate(-this.centeringOffset, -this.centeringYOffset).scale(1 / this.contentScaling());
-    private get centeringOffset() { return this.nativeWidth && (!this.props.Document || !this.props.Document.fitWidth) ? (this.props.PanelWidth() - this.nativeWidth * this.contentScaling()) / 2 : 0; }
+    private get centeringOffset() { return this.nativeWidth && (!this.props.Document || !this.props.Document._fitWidth) ? (this.props.PanelWidth() - this.nativeWidth * this.contentScaling()) / 2 : 0; }
     private get centeringYOffset() { return Math.abs(this.centeringOffset) < 0.001 ? (this.props.PanelHeight() - this.nativeHeight * this.contentScaling()) / 2 : 0; }
 
     @computed get borderRounding() { return StrCast(this.props.Document?.borderRounding); }
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 2570d232a..5663e5e3c 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -108,8 +108,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     public get ContentDiv() { return this._mainCont.current; }
     @computed get active() { return SelectionManager.IsSelected(this, true) || this.props.parentActive(true); }
     @computed get topMost() { return this.props.renderDepth === 0; }
-    @computed get nativeWidth() { return this.layoutDoc.nativeWidth || 0; }
-    @computed get nativeHeight() { return this.layoutDoc.nativeHeight || 0; }
+    @computed get nativeWidth() { return this.layoutDoc._nativeWidth || 0; }
+    @computed get nativeHeight() { return this.layoutDoc._nativeHeight || 0; }
     @computed get onClickHandler() { return this.props.onClick ? this.props.onClick : this.Document.onClick; }
     @computed get onPointerDownHandler() { return this.props.onPointerDown ? this.props.onPointerDown : this.Document.onPointerDown; }
     @computed get onPointerUpHandler() { return this.props.onPointerUp ? this.props.onPointerUp : this.Document.onPointerUp; }
@@ -343,7 +343,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
             if (Math.abs(this._downX - touch.clientX) > 3 || Math.abs(this._downY - touch.clientY) > 3) {
                 if (!e.altKey && (!this.topMost || this.Document.onDragStart || this.Document.onClick)) {
                     this.cleanUpInteractions();
-                    this.startDragging(this._downX, this._downY, this.Document.dropAction ? this.Document.dropAction as any : e.ctrlKey || e.altKey ? "alias" : undefined, this._hitTemplateDrag);
+                    this.startDragging(this._downX, this._downY, this.Document._dropAction ? this.Document._dropAction as any : e.ctrlKey || e.altKey ? "alias" : undefined, this._hitTemplateDrag);
                 }
             }
             e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document'  however it does mark the event as cancelBubble=true which we test for in the move event handlers
@@ -387,10 +387,10 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
             if (dX !== 0 || dY !== 0 || dW !== 0 || dH !== 0) {
                 const doc = PositionDocument(this.props.Document);
                 const layoutDoc = PositionDocument(Doc.Layout(this.props.Document));
-                let nwidth = layoutDoc.nativeWidth || 0;
-                let nheight = layoutDoc.nativeHeight || 0;
-                const width = (layoutDoc.width || 0);
-                const height = (layoutDoc.height || (nheight / nwidth * width));
+                let nwidth = layoutDoc._nativeWidth || 0;
+                let nheight = layoutDoc._nativeHeight || 0;
+                const width = (layoutDoc._width || 0);
+                const height = (layoutDoc._height || (nheight / nwidth * width));
                 const scale = this.props.ScreenToLocalTransform().Scale * this.props.ContentScaling();
                 const actualdW = Math.max(width + (dW * scale), 20);
                 const actualdH = Math.max(height + (dH * scale), 20);
@@ -400,33 +400,33 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                 if (fixedAspect && e.ctrlKey && layoutDoc.ignoreAspect) {
                     layoutDoc.ignoreAspect = false;
 
-                    layoutDoc.nativeWidth = nwidth = layoutDoc.width || 0;
-                    layoutDoc.nativeHeight = nheight = layoutDoc.height || 0;
+                    layoutDoc._nativeWidth = nwidth = layoutDoc._width || 0;
+                    layoutDoc._nativeHeight = nheight = layoutDoc._height || 0;
                 }
                 if (fixedAspect && (!nwidth || !nheight)) {
-                    layoutDoc.nativeWidth = nwidth = layoutDoc.width || 0;
-                    layoutDoc.nativeHeight = nheight = layoutDoc.height || 0;
+                    layoutDoc._nativeWidth = nwidth = layoutDoc._width || 0;
+                    layoutDoc._nativeHeight = nheight = layoutDoc._height || 0;
                 }
                 if (nwidth > 0 && nheight > 0 && !layoutDoc.ignoreAspect) {
                     if (Math.abs(dW) > Math.abs(dH)) {
                         if (!fixedAspect) {
-                            layoutDoc.nativeWidth = actualdW / (layoutDoc.width || 1) * (layoutDoc.nativeWidth || 0);
+                            layoutDoc._nativeWidth = actualdW / (layoutDoc._width || 1) * (layoutDoc._nativeWidth || 0);
                         }
-                        layoutDoc.width = actualdW;
-                        if (fixedAspect && !layoutDoc.fitWidth) layoutDoc.height = nheight / nwidth * layoutDoc.width;
-                        else layoutDoc.height = actualdH;
+                        layoutDoc._width = actualdW;
+                        if (fixedAspect && !layoutDoc._fitWidth) layoutDoc._height = nheight / nwidth * layoutDoc._width;
+                        else layoutDoc._height = actualdH;
                     }
                     else {
                         if (!fixedAspect) {
-                            layoutDoc.nativeHeight = actualdH / (layoutDoc.height || 1) * (doc.nativeHeight || 0);
+                            layoutDoc._nativeHeight = actualdH / (layoutDoc._height || 1) * (doc._nativeHeight || 0);
                         }
-                        layoutDoc.height = actualdH;
-                        if (fixedAspect && !layoutDoc.fitWidth) layoutDoc.width = nwidth / nheight * layoutDoc.height;
-                        else layoutDoc.width = actualdW;
+                        layoutDoc._height = actualdH;
+                        if (fixedAspect && !layoutDoc._fitWidth) layoutDoc._width = nwidth / nheight * layoutDoc._height;
+                        else layoutDoc._width = actualdW;
                     }
                 } else {
-                    dW && (layoutDoc.width = actualdW);
-                    dH && (layoutDoc.height = actualdH);
+                    dW && (layoutDoc._width = actualdW);
+                    dH && (layoutDoc._height = actualdH);
                     dH && layoutDoc.autoHeight && (layoutDoc.autoHeight = false);
                 }
             }
@@ -484,7 +484,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                 if (!e.altKey && (!this.topMost || this.Document.onDragStart || this.Document.onClick) && (e.buttons === 1 || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE))) {
                     document.removeEventListener("pointermove", this.onPointerMove);
                     document.removeEventListener("pointerup", this.onPointerUp);
-                    this.startDragging(this._downX, this._downY, this.Document.dropAction ? this.Document.dropAction as any : e.ctrlKey || e.altKey ? "alias" : undefined, this._hitTemplateDrag);
+                    this.startDragging(this._downX, this._downY, this.Document._dropAction ? this.Document._dropAction as any : e.ctrlKey || e.altKey ? "alias" : undefined, this._hitTemplateDrag);
                 }
             }
             e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document'  however it does mark the event as cancelBubble=true which we test for in the move event handlers
@@ -520,12 +520,13 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         undoBatch(() => doc.layoutKey = "layout")();
     }
 
-    static makeCustomViewClicked = (doc: Doc, dataDoc: Opt<Doc>) => {
+    static makeCustomViewClicked = (doc: Doc, dataDoc: Opt<Doc>, name: string = "custom") => {
         const batch = UndoManager.StartBatch("CustomViewClicked");
-        if (doc.layoutCustom === undefined) {
-            const width = NumCast(doc.width);
-            const height = NumCast(doc.height);
-            const options = { title: "data", width, x: -width / 2, y: - height / 2, };
+        const customName = "layout-" + name;
+        if (doc[customName] === undefined) {
+            const _width = NumCast(doc.width);
+            const _height = NumCast(doc.height);
+            const options = { title: "data", _width, x: -_width / 2, y: - _height / 2, };
 
             let fieldTemplate: Doc;
             switch (doc.type) {
@@ -549,12 +550,12 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
             fieldTemplate.heading = 1;
             fieldTemplate.autoHeight = true;
 
-            const docTemplate = Docs.Create.FreeformDocument([fieldTemplate], { title: doc.title + "_layout", width: width + 20, height: Math.max(100, height + 45) });
+            const docTemplate = Docs.Create.FreeformDocument([fieldTemplate], { title: customName + "(" + doc.title + ")", isTemplateDoc: true, _width: _width + 20, _height: Math.max(100, _height + 45) });
 
             Doc.MakeMetadataFieldTemplate(fieldTemplate, Doc.GetProto(docTemplate));
-            Doc.ApplyTemplateTo(docTemplate, dataDoc || doc, "layoutCustom", undefined);
+            Doc.ApplyTemplateTo(docTemplate, dataDoc || doc, customName, undefined);
         } else {
-            doc.layoutKey = "layoutCustom";
+            doc.layoutKey = customName;
         }
         batch.end();
     }
@@ -622,9 +623,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     freezeNativeDimensions = (): void => {
         this.layoutDoc.autoHeight = this.layoutDoc.autoHeight = false;
         this.layoutDoc.ignoreAspect = !this.layoutDoc.ignoreAspect;
-        if (!this.layoutDoc.ignoreAspect && !this.layoutDoc.nativeWidth) {
-            this.layoutDoc.nativeWidth = this.props.PanelWidth();
-            this.layoutDoc.nativeHeight = this.props.PanelHeight();
+        if (!this.layoutDoc.ignoreAspect && !this.layoutDoc._nativeWidth) {
+            this.layoutDoc._nativeWidth = this.props.PanelWidth();
+            this.layoutDoc._nativeHeight = this.props.PanelHeight();
         }
     }
 
@@ -635,7 +636,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         if (!anchors.find(anchor2 => anchor2 && anchor2.title === this.Document.title + ".portal" ? true : false)) {
             const portalID = (this.Document.title + ".portal").replace(/^-/, "").replace(/\([0-9]*\)$/, "");
             DocServer.GetRefField(portalID).then(existingPortal => {
-                const portal = existingPortal instanceof Doc ? existingPortal : Docs.Create.FreeformDocument([], { width: (this.layoutDoc.width || 0) + 10, height: this.layoutDoc.height || 0, title: portalID });
+                const portal = existingPortal instanceof Doc ? existingPortal : Docs.Create.FreeformDocument([], { _width: (this.layoutDoc._width || 0) + 10, _height: this.layoutDoc._height || 0, title: portalID });
                 DocUtils.MakeLink({ doc: this.props.Document, ctx: this.props.ContainingCollectionDoc }, { doc: portal }, portalID, "portal link");
                 this.Document.isButton = true;
             });
@@ -646,13 +647,14 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     @action
     setNarrativeView = (custom: boolean): void => {
         if (custom) {
-            this.props.Document.layout_narrative = CollectionView.LayoutString("narrative");
-            this.props.Document.nativeWidth = this.props.Document.nativeHeight = undefined;
-            !this.props.Document.narrative && (Doc.GetProto(this.props.Document).narrative = new List<Doc>([]));
-            this.props.Document.viewType = CollectionViewType.Stacking;
-            this.props.Document.layoutKey = "layout_narrative";
-        } else {
-            DocumentView.makeNativeViewClicked(this.props.Document);
+            custom ? DocumentView.makeCustomViewClicked(this.props.Document, this.props.DataDoc, "narrative") : DocumentView.makeNativeViewClicked(this.props.Document);
+            //     this.props.Document.layout_narrative = CollectionView.LayoutString("narrative");
+            //     this.props.Document.layoutKey = "layout_narrative";
+            //     this.props.Document._nativeWidth = this.props.Document._nativeHeight = undefined;
+            //     !this.props.Document.narrative && (Doc.GetProto(this.props.Document).narrative = new List<Doc>([]));
+            //     this.props.Document._viewType = CollectionViewType.Stacking;
+            // } else {
+            //     DocumentView.makeNativeViewClicked(this.props.Document);
         }
     }
 
@@ -709,7 +711,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         subitems.push({ description: "Open Right      ", event: () => this.props.addDocTab(this.props.Document, this.props.DataDoc, "onRight", this.props.LibraryPath), icon: "caret-square-right" });
         subitems.push({ description: "Open Alias Tab  ", event: () => this.props.addDocTab(Doc.MakeAlias(this.props.Document), this.props.DataDoc, "inTab"), icon: "folder" });
         subitems.push({ description: "Open Alias Right", event: () => this.props.addDocTab(Doc.MakeAlias(this.props.Document), this.props.DataDoc, "onRight"), icon: "caret-square-right" });
-        subitems.push({ description: "Open Fields     ", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { width: 300, height: 300 }), undefined, "onRight"), icon: "layer-group" });
+        subitems.push({ description: "Open Fields     ", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), undefined, "onRight"), icon: "layer-group" });
         cm.addItem({ description: "Open...", subitems: subitems, icon: "external-link-alt" });
 
 
@@ -737,9 +739,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         if (this.props.DataDoc) {
             layoutItems.push({ description: "Make View of Metadata Field", event: () => Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.DataDoc!), icon: "concierge-bell" });
         }
-        layoutItems.push({ description: `${this.Document.chromeStatus !== "disabled" ? "Hide" : "Show"} Chrome`, event: () => this.Document.chromeStatus = (this.Document.chromeStatus !== "disabled" ? "disabled" : "enabled"), icon: "project-diagram" });
+        layoutItems.push({ description: `${this.Document._chromeStatus !== "disabled" ? "Hide" : "Show"} Chrome`, event: () => this.Document._chromeStatus = (this.Document._chromeStatus !== "disabled" ? "disabled" : "enabled"), icon: "project-diagram" });
         layoutItems.push({ description: `${this.Document.autoHeight ? "Variable Height" : "Auto Height"}`, event: () => this.layoutDoc.autoHeight = !this.layoutDoc.autoHeight, icon: "plus" });
-        layoutItems.push({ description: this.Document.ignoreAspect || !this.Document.nativeWidth || !this.Document.nativeHeight ? "Freeze" : "Unfreeze", event: this.freezeNativeDimensions, icon: "snowflake" });
+        layoutItems.push({ description: this.Document.ignoreAspect || !this.Document._nativeWidth || !this.Document._nativeHeight ? "Freeze" : "Unfreeze", event: this.freezeNativeDimensions, icon: "snowflake" });
         layoutItems.push({ description: this.Document.lockedPosition ? "Unlock Position" : "Lock Position", event: this.toggleLockPosition, icon: BoolCast(this.Document.lockedPosition) ? "unlock" : "lock" });
         layoutItems.push({ description: this.Document.lockedTransform ? "Unlock Transform" : "Lock Transform", event: this.toggleLockTransform, icon: BoolCast(this.Document.lockedTransform) ? "unlock" : "lock" });
         layoutItems.push({ description: "Center View", event: () => this.props.focus(this.props.Document, false), icon: "crosshairs" });
@@ -793,13 +795,13 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                     const mode2 = mode === DocServer.WriteMode.Default ? mode : DocServer.WriteMode.Playground;
                     DocServer.setFieldWriteMode("x", mode1);
                     DocServer.setFieldWriteMode("y", mode1);
-                    DocServer.setFieldWriteMode("width", mode1);
-                    DocServer.setFieldWriteMode("height", mode1);
+                    DocServer.setFieldWriteMode("_width", mode1);
+                    DocServer.setFieldWriteMode("_height", mode1);
 
-                    DocServer.setFieldWriteMode("panX", mode2);
-                    DocServer.setFieldWriteMode("panY", mode2);
+                    DocServer.setFieldWriteMode("_panX", mode2);
+                    DocServer.setFieldWriteMode("_panY", mode2);
                     DocServer.setFieldWriteMode("scale", mode2);
-                    DocServer.setFieldWriteMode("viewType", mode2);
+                    DocServer.setFieldWriteMode("_viewType", mode2);
                 };
                 const aclsMenu: ContextMenuProps[] = [];
                 aclsMenu.push({ description: "Default (write/read all)", event: () => setWriteMode(DocServer.WriteMode.Default), icon: DocServer.AclsMode === DocServer.WriteMode.Default ? "check" : "exclamation" });
@@ -858,7 +860,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         const fallback = Cast(this.props.Document.layoutKey, "string");
         return typeof fallback === "string" ? fallback : "layout";
     }
-    childScaling = () => (this.layoutDoc.fitWidth ? this.props.PanelWidth() / this.nativeWidth : this.props.ContentScaling());
+    childScaling = () => (this.layoutDoc._fitWidth ? this.props.PanelWidth() / this._nativeWidth : this.props.ContentScaling());
     @computed get contents() {
         TraceMobx();
         return (<DocumentContentsView ContainingCollectionView={this.props.ContainingCollectionView}
@@ -981,7 +983,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
 
         const highlightColors = ["transparent", "maroon", "maroon", "yellow", "magenta", "cyan", "orange"];
         const highlightStyles = ["solid", "dashed", "solid", "solid", "solid", "solid", "solid"];
-        let highlighting = fullDegree && this.layoutDoc.type !== DocumentType.FONTICON && this.layoutDoc.viewType !== CollectionViewType.Linear;
+        let highlighting = fullDegree && this.layoutDoc.type !== DocumentType.FONTICON && this.layoutDoc._viewType !== CollectionViewType.Linear;
         highlighting = highlighting && this.props.focus !== emptyFunction;  // bcz: hack to turn off highlighting onsidebar panel documents.  need to flag a document as not highlightable in a more direct way
         return <div className={`documentView-node${this.topMost ? "-topmost" : ""}`} ref={this._mainCont} onKeyDown={this.onKeyDown}
             onDrop={this.onDrop} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick}
@@ -993,7 +995,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                 outline: highlighting && !borderRounding ? `${highlightColors[fullDegree]} ${highlightStyles[fullDegree]} ${localScale}px` : "solid 0px",
                 border: highlighting && borderRounding ? `${highlightStyles[fullDegree]} ${highlightColors[fullDegree]} ${localScale}px` : undefined,
                 boxShadow: this.props.Document.isTemplateForField ? "black 0.2vw 0.2vw 0.8vw" : undefined,
-                background: this.layoutDoc.type === DocumentType.FONTICON || this.layoutDoc.viewType === CollectionViewType.Linear ? undefined : backgroundColor,
+                background: this.layoutDoc.type === DocumentType.FONTICON || this.layoutDoc._viewType === CollectionViewType.Linear ? undefined : backgroundColor,
                 width: animwidth,
                 height: animheight,
                 opacity: this.Document.opacity
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 0a1a79bc4..9303ca0fa 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -152,7 +152,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
             const id = Utils.GenerateDeterministicGuid(this.dataDoc[Id] + key);
             DocServer.GetRefField(value).then(doc => {
                 DocServer.GetRefField(id).then(linkDoc => {
-                    this.dataDoc[key] = doc || Docs.Create.FreeformDocument([], { title: value, width: 500, height: 500 }, value);
+                    this.dataDoc[key] = doc || Docs.Create.FreeformDocument([], { title: value, _width: 500, _height: 500 }, value);
                     DocUtils.Publish(this.dataDoc[key] as Doc, value, this.props.addDocument, this.props.removeDocument);
                     if (linkDoc) { (linkDoc as Doc).anchor2 = this.dataDoc[key] as Doc; }
                     else DocUtils.MakeLink({ doc: this.dataDoc, ctx: this.props.ContainingCollectionDoc }, { doc: this.dataDoc[key] as Doc }, "Ref:" + value, "link to named target", id);
@@ -193,8 +193,8 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
             const tsel = this._editorView.state.selection.$from;
             tsel.marks().filter(m => m.type === this._editorView!.state.schema.marks.user_mark).map(m => AudioBox.SetScrubTime(Math.max(0, m.attrs.modified * 5000 - 1000)));
             this._applyingChange = true;
-            this.extensionDoc && !this.extensionDoc.lastModified && (this.extensionDoc.backgroundColor = "lightGray");
-            this.extensionDoc && (this.extensionDoc.lastModified = new DateField(new Date(Date.now())));
+            this.dataDoc[this.props.fieldKey + "-lastModified"] && (this.dataDoc[this.props.fieldKey + "-backgroundColor"] = "lightGray");
+            this.dataDoc[this.props.fieldKey + "-lastModified"] = new DateField(new Date(Date.now()));
             this.dataDoc[this.props.fieldKey] = new RichTextField(JSON.stringify(state.toJSON()), state.doc.textBetween(0, state.doc.content.size, "\n\n"));
             this._applyingChange = false;
             this.updateTitle();
@@ -271,7 +271,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
                 const target = de.complete.docDragData.droppedDocuments[0];
                 // const link = DocUtils.MakeLink({ doc: this.dataDoc, ctx: this.props.ContainingCollectionDoc }, { doc: target }, "Embedded Doc:" + target.title);
                 // if (link) {
-                target.fitToBox = true;
+                target._fitToBox = true;
                 const node = schema.nodes.dashDoc.create({
                     width: target[WidthSym](), height: target[HeightSym](),
                     title: "dashDoc", docid: target[Id],
@@ -704,11 +704,8 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
                 DocServer.GetRefField(pdfRegionId).then(pdfRegion => {
                     if ((pdfDoc instanceof Doc) && (pdfRegion instanceof Doc)) {
                         setTimeout(async () => {
-                            const extension = Doc.fieldExtensionDoc(pdfDoc, "data");
-                            if (extension) {
-                                const targetAnnotations = await DocListCastAsync(extension.annotations);// bcz: NO... this assumes the pdf is using its 'data' field.  need to have the PDF's view handle updating its own annotations
-                                targetAnnotations && targetAnnotations.push(pdfRegion);
-                            }
+                            const targetAnnotations = await DocListCastAsync(pdfDoc["data-annotations"]);// bcz: NO... this assumes the pdf is using its 'data' field.  need to have the PDF's view handle updating its own annotations
+                            targetAnnotations && targetAnnotations.push(pdfRegion);
                         });
 
                         const link = DocUtils.MakeLink({ doc: this.props.Document, ctx: this.props.ContainingCollectionDoc }, { doc: pdfRegion, ctx: pdfDoc }, "note on " + pdfDoc.title, "pasted PDF link");
@@ -1069,12 +1066,12 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
                 this.layoutDoc.limitHeight = undefined;
                 this.layoutDoc.autoHeight = false;
             }
-            const nh = this.Document.isTemplateForField ? 0 : NumCast(this.dataDoc.nativeHeight, 0);
-            const dh = NumCast(this.layoutDoc.height, 0);
+            const nh = this.Document.isTemplateForField ? 0 : NumCast(this.dataDoc._nativeHeight, 0);
+            const dh = NumCast(this.layoutDoc._height, 0);
             const newHeight = Math.max(10, (nh ? dh / nh * scrollHeight : scrollHeight) + (this.props.ChromeHeight ? this.props.ChromeHeight() : 0));
             if (Math.abs(newHeight - dh) > 1) { // bcz: Argh!  without this, we get into a React crash if the same document is opened in a freeform view and in the treeview.  no idea why, but after dragging the freeform document, selecting it, and selecting text, it will compute to 1 pixel higher than the treeview which causes a cycle
-                this.layoutDoc.height = newHeight;
-                this.dataDoc.nativeHeight = nh ? scrollHeight : undefined;
+                this.layoutDoc._height = newHeight;
+                this.dataDoc._nativeHeight = nh ? scrollHeight : undefined;
             }
         }
     }
@@ -1082,6 +1079,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
     @computed get sidebarWidthPercent() { return StrCast(this.props.Document.sidebarWidthPercent, "0%"); }
     @computed get sidebarWidth() { return Number(this.sidebarWidthPercent.substring(0, this.sidebarWidthPercent.length - 1)) / 100 * this.props.PanelWidth(); }
     @computed get annotationsKey() { return "annotations"; }
+    @computed get sidebarColor() { return StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], "transparent")); }
     render() {
         TraceMobx();
         const rounded = StrCast(this.layoutDoc.borderRounding) === "100%" ? "-rounded" : "";
@@ -1122,7 +1120,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
                 {this.props.Document.hideSidebar ? (null) : this.sidebarWidthPercent === "0%" ?
                     <div className="formattedTextBox-sidebar-handle" onPointerDown={this.sidebarDown} onClick={e => this.toggleSidebar()} /> :
                     <div className={"formattedTextBox-sidebar" + (InkingControl.Instance.selectedTool !== InkTool.None ? "-inking" : "")}
-                        style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${StrCast(this.extensionDoc?.backgroundColor, "transparent")}` }}>
+                        style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}>
                         <CollectionFreeFormView {...this.props}
                             PanelHeight={this.props.PanelHeight}
                             PanelWidth={() => this.sidebarWidth}
diff --git a/src/client/views/nodes/FormattedTextBoxComment.tsx b/src/client/views/nodes/FormattedTextBoxComment.tsx
index 8dedfda9b..fda3e3285 100644
--- a/src/client/views/nodes/FormattedTextBoxComment.tsx
+++ b/src/client/views/nodes/FormattedTextBoxComment.tsx
@@ -86,7 +86,7 @@ export class FormattedTextBoxComment {
                     DocumentManager.Instance.FollowLink(FormattedTextBoxComment.linkDoc, textBox.props.Document,
                         (doc: Doc, maxLocation: string) => textBox.props.addDocTab(doc, undefined, e.ctrlKey ? "inTab" : "onRight"));
                 } else if (textBox && (FormattedTextBoxComment.tooltipText as any).href) {
-                    textBox.props.addDocTab(Docs.Create.WebDocument((FormattedTextBoxComment.tooltipText as any).href, { title: (FormattedTextBoxComment.tooltipText as any).href, width: 200, height: 400 }), undefined, "onRight");
+                    textBox.props.addDocTab(Docs.Create.WebDocument((FormattedTextBoxComment.tooltipText as any).href, { title: (FormattedTextBoxComment.tooltipText as any).href, _width: 200, _height: 400 }), undefined, "onRight");
                 }
                 keep && textBox && FormattedTextBoxComment.start !== undefined && textBox.adoptAnnotation(
                     FormattedTextBoxComment.start, FormattedTextBoxComment.end, FormattedTextBoxComment.mark);
diff --git a/src/client/views/nodes/IconBox.tsx b/src/client/views/nodes/IconBox.tsx
index 9462ff024..172338eb6 100644
--- a/src/client/views/nodes/IconBox.tsx
+++ b/src/client/views/nodes/IconBox.tsx
@@ -79,7 +79,7 @@ export class IconBox extends React.Component<FieldViewProps> {
                 <Measure offset onResize={(r) => runInAction(() => {
                     if (r.offset!.width || this.props.Document.hideLabel) {
                         this.props.Document.iconWidth = (r.offset!.width + Number(MINIMIZED_ICON_SIZE));
-                        if (this.props.Document.height === Number(MINIMIZED_ICON_SIZE)) this.props.Document.width = this.props.Document.iconWidth;
+                        if (this.props.Document._height === Number(MINIMIZED_ICON_SIZE)) this.props.Document._width = this.props.Document.iconWidth;
                     }
                 })}>
                     {({ measureRef }) =>
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index b7e904283..1a07c1598 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -39,7 +39,6 @@ library.add(faFileAudio, faAsterisk);
 export const pageSchema = createSchema({
     curPage: "number",
     fitWidth: "boolean",
-    rotation: "number",
     googlePhotosUrl: "string",
     googlePhotosTags: "string"
 });
@@ -78,7 +77,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
                 e.stopPropagation();
             }
             de.metaKey && de.complete.docDragData.droppedDocuments.forEach(action((drop: Doc) => {
-                this.extensionDoc && Doc.AddDocToList(Doc.GetProto(this.extensionDoc), "Alternates", drop);
+                Doc.AddDocToList(this.dataDoc, this.props.fieldKey + "-alternates", drop);
                 e.stopPropagation();
             }));
         }
@@ -88,8 +87,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
         let gumStream: any;
         let recorder: any;
         const self = this;
-        const extensionDoc = this.extensionDoc;
-        extensionDoc && navigator.mediaDevices.getUserMedia({
+        navigator.mediaDevices.getUserMedia({
             audio: true
         }).then(function (stream) {
             gumStream = stream;
@@ -104,11 +102,11 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
                 const files = await res.json();
                 const url = Utils.prepend(files[0].path);
                 // upload to server with known URL 
-                const audioDoc = Docs.Create.AudioDocument(url, { title: "audio test", width: 200, height: 32 });
+                const audioDoc = Docs.Create.AudioDocument(url, { title: "audio test", _width: 200, _height: 32 });
                 audioDoc.treeViewExpandedView = "layout";
-                const audioAnnos = Cast(extensionDoc.audioAnnotations, listSpec(Doc));
+                const audioAnnos = Cast(this.dataDoc[this.props.fieldKey + "-audioAnnotations"], listSpec(Doc));
                 if (audioAnnos === undefined) {
-                    extensionDoc.audioAnnotations = new List([audioDoc]);
+                    this.dataDoc[this.props.fieldKey + "-audioAnnotations"] = new List([audioDoc]);
                 } else {
                     audioAnnos.push(audioDoc);
                 }
@@ -125,15 +123,15 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
 
     @undoBatch
     rotate = action(() => {
-        const nw = this.Document.nativeWidth;
-        const nh = this.Document.nativeHeight;
-        const w = this.Document.width;
-        const h = this.Document.height;
-        this.Document.rotation = ((this.Document.rotation || 0) + 90) % 360;
-        this.Document.nativeWidth = nh;
-        this.Document.nativeHeight = nw;
-        this.Document.width = h;
-        this.Document.height = w;
+        const nw = NumCast(this.Document[this.props.fieldKey + "-nativeWidth"]);
+        const nh = NumCast(this.Document[this.props.fieldKey + "-nativeHeight"]);
+        const w = this.Document._width;
+        const h = this.Document._height;
+        this.dataDoc[this.props.fieldKey + "-rotation"] = (NumCast(this.dataDoc[this.props.fieldKey + "-rotation"]) + 90) % 360;
+        this.dataDoc[this.props.fieldKey + "-nativeWidth"] = nh;
+        this.dataDoc[this.props.fieldKey + "-nativeHeight"] = nw;
+        this.Document._width = h;
+        this.Document._height = w;
     });
 
     specificContextMenu = (e: React.MouseEvent): void => {
@@ -159,7 +157,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
             results.reduce((face: CognitiveServices.Image.Face, faceDocs: List<Doc>) => faceDocs.push(Docs.Get.DocumentHierarchyFromJson(face, `Face: ${face.faceId}`)!), new List<Doc>());
             return faceDocs;
         };
-        this.url && this.extensionDoc && CognitiveServices.Image.Appliers.ProcessImage(this.extensionDoc, ["faces"], this.url, Service.Face, converter);
+        this.url && CognitiveServices.Image.Appliers.ProcessImage(this.dataDoc, [this.props.fieldKey + "-faces"], this.url, Service.Face, converter);
     }
 
     generateMetadata = (threshold: Confidence = Confidence.Excellent) => {
@@ -171,12 +169,12 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
                 const sanitized = tag.name.replace(" ", "_");
                 tagDoc[sanitized] = ComputedField.MakeFunction(`(${tag.confidence} >= this.confidence) ? ${tag.confidence} : "${ComputedField.undefined}"`);
             });
-            this.extensionDoc && (this.extensionDoc.generatedTags = tagsList);
+            this.dataDoc[this.props.fieldKey + "-generatedTags"] = tagsList;
             tagDoc.title = "Generated Tags Doc";
             tagDoc.confidence = threshold;
             return tagDoc;
         };
-        this.url && this.extensionDoc && CognitiveServices.Image.Appliers.ProcessImage(this.extensionDoc, ["generatedTagsDoc"], this.url, Service.ComputerVision, converter);
+        this.url && CognitiveServices.Image.Appliers.ProcessImage(this.dataDoc, [this.props.fieldKey + "-generatedTagsDoc"], this.url, Service.ComputerVision, converter);
     }
 
     @computed private get url() {
@@ -216,29 +214,30 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
 
     _resized = "";
     resize = (imgPath: string) => {
-        requestImageSize(imgPath)
-            .then((size: any) => {
-                const rotation = NumCast(this.dataDoc.rotation) % 180;
-                const realsize = rotation === 90 || rotation === 270 ? { height: size.width, width: size.height } : size;
-                const aspect = realsize.height / realsize.width;
-                if (this.Document.width && (Math.abs(1 - NumCast(this.Document.height) / NumCast(this.Document.width) / (realsize.height / realsize.width)) > 0.1)) {
-                    setTimeout(action(() => {
-                        if (this.paths[NumCast(this.props.Document.curPage)] === imgPath && (!this.layoutDoc.isTemplateDoc || this.dataDoc !== this.layoutDoc)) {
-                            this._resized = imgPath;
-                            this.Document.height = this.Document[WidthSym]() * aspect;
-                            this.Document.nativeHeight = realsize.height;
-                            this.Document.nativeWidth = realsize.width;
-                        }
-                    }), 0);
-                } else this._resized = imgPath;
-            })
-            .catch((err: any) => console.log(err));
+        if (!this.dataDoc[this.props.fieldKey + "-nativeHeight"])
+            requestImageSize(imgPath)
+                .then((size: any) => {
+                    const rotation = NumCast(this.dataDoc[this.props.fieldKey + "-rotation"]) % 180;
+                    const realsize = rotation === 90 || rotation === 270 ? { height: size.width, width: size.height } : size;
+                    const aspect = realsize.height / realsize.width;
+                    if (this.Document._width && (Math.abs(1 - NumCast(this.Document._height) / NumCast(this.Document._width) / (realsize.height / realsize.width)) > 0.1)) {
+                        setTimeout(action(() => {
+                            if (this.paths[NumCast(this.props.Document.curPage)] === imgPath && (!this.layoutDoc.isTemplateDoc || this.dataDoc !== this.layoutDoc)) {
+                                this._resized = imgPath;
+                                this.Document._height = this.Document[WidthSym]() * aspect;
+                                this.dataDoc[this.props.fieldKey + "-nativeHeight"] = this.dataDoc._nativeHeight = realsize.height;
+                                this.dataDoc[this.props.fieldKey + "-nativeWidth"] = this.dataDoc._nativeWidth = realsize.width;
+                            }
+                        }), 0);
+                    } else this._resized = imgPath;
+                })
+                .catch((err: any) => console.log(err));
     }
 
     @action
     onPointerEnter = () => {
         const self = this;
-        const audioAnnos = this.extensionDoc && DocListCast(this.extensionDoc.audioAnnotations);
+        const audioAnnos = DocListCast(this.dataDoc[this.props.fieldKey + "-audioAnnotations"]);
         if (audioAnnos && audioAnnos.length && this._audioState === 0) {
             const anno = audioAnnos[Math.floor(Math.random() * audioAnnos.length)];
             anno.data instanceof AudioField && new Howl({
@@ -273,17 +272,16 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
 
     @computed get nativeSize() {
         const pw = typeof this.props.PanelWidth === "function" ? this.props.PanelWidth() : typeof this.props.PanelWidth === "number" ? (this.props.PanelWidth as any) as number : 50;
-        const nativeWidth = (this.Document.nativeWidth || pw);
-        const nativeHeight = (this.Document.nativeHeight || 1);
+        const nativeWidth = NumCast(this.dataDoc[this.props.fieldKey + "-nativeWidth"], pw);
+        const nativeHeight = NumCast(this.dataDoc[this.props.fieldKey + "-nativeHeight"], 1);
         return { nativeWidth, nativeHeight };
     }
 
     @computed get paths() {
-        const extensionDoc = this.extensionDoc!;
         let paths = [Utils.CorsProxy("http://www.cs.brown.edu/~bcz/noImage.png")];
         // this._curSuffix = "";
         // if (w > 20) {
-        const alts = DocListCast(extensionDoc.Alternates);
+        const alts = DocListCast(this.dataDoc[this.props.fieldKey + "-alternates"]);
         const altpaths = alts.filter(doc => doc.data instanceof ImageField).map(doc => this.choosePath((doc.data as ImageField).url));
         const field = this.dataDoc[this.props.fieldKey];
         // if (w < 100 && this._smallRetryCount < 10) this._curSuffix = "_s";
@@ -296,13 +294,11 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
 
     @computed get content() {
         TraceMobx();
-        const extensionDoc = this.extensionDoc;
-        if (!extensionDoc) return (null);
 
         const srcpath = this.paths[NumCast(this.props.Document.curPage, 0)];
         const fadepath = this.paths[Math.min(1, this.paths.length - 1)];
         const { nativeWidth, nativeHeight } = this.nativeSize;
-        const rotation = NumCast(this.Document.rotation, 0);
+        const rotation = NumCast(this.dataDoc[this.props.fieldKey + "-rotation"]);
         const aspect = (rotation % 180) ? this.Document[HeightSym]() / this.Document[WidthSym]() : 1;
         const shift = (rotation % 180) ? (nativeHeight - nativeWidth / aspect) / 2 : 0;
 
@@ -331,10 +327,11 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
                 style={{ height: `calc(${.1 * nativeHeight / nativeWidth * 100}%)` }}
             >
                 <FontAwesomeIcon className="imageBox-audioFont"
-                    style={{ color: [DocListCast(extensionDoc.audioAnnotations).length ? "blue" : "gray", "green", "red"][this._audioState] }} icon={!DocListCast(extensionDoc.audioAnnotations).length ? "microphone" : faFileAudio} size="sm" />
+                    style={{ color: [DocListCast(this.dataDoc[this.props.fieldKey + "-audioAnnotations"]).length ? "blue" : "gray", "green", "red"][this._audioState] }}
+                    icon={!DocListCast(this.dataDoc[this.props.fieldKey + "-audioAnnotations"]).length ? "microphone" : faFileAudio} size="sm" />
             </div>
             {this.considerGooglePhotosLink()}
-            <FaceRectangles document={extensionDoc} color={"#0000FF"} backgroundColor={"#0000FF"} />
+            <FaceRectangles document={this.dataDoc} color={"#0000FF"} backgroundColor={"#0000FF"} />
         </div>;
     }
 
diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index 234a6a9d3..499cd2877 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -179,7 +179,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
     }
 
     getTemplate = async () => {
-        const parent = Docs.Create.StackingDocument([], { width: 800, height: 800, title: "Template" });
+        const parent = Docs.Create.StackingDocument([], { _width: 800, _height: 800, title: "Template" });
         parent.singleColumn = false;
         parent.columnWidth = 100;
         for (const row of this.rows.filter(row => row.isChecked)) {
@@ -200,15 +200,15 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
         if (!fieldTemplate) {
             return;
         }
-        const previousViewType = fieldTemplate.viewType;
+        const previousViewType = fieldTemplate._viewType;
         Doc.MakeMetadataFieldTemplate(fieldTemplate, Doc.GetProto(parentStackingDoc));
-        previousViewType && (fieldTemplate.viewType = previousViewType);
+        previousViewType && (fieldTemplate._viewType = previousViewType);
 
         Cast(parentStackingDoc.data, listSpec(Doc))!.push(fieldTemplate);
     }
 
     inferType = async (data: FieldResult, metaKey: string) => {
-        const options = { width: 300, height: 300, title: metaKey };
+        const options = { _width: 300, _height: 300, title: metaKey };
         if (data instanceof RichTextField || typeof data === "string" || typeof data === "number") {
             return Docs.Create.TextDocument(options);
         } else if (data instanceof List) {
diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx
index 46466aa0f..e6b512adf 100644
--- a/src/client/views/nodes/KeyValuePair.tsx
+++ b/src/client/views/nodes/KeyValuePair.tsx
@@ -46,7 +46,7 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
         if (value instanceof Doc) {
             e.stopPropagation();
             e.preventDefault();
-            ContextMenu.Instance.addItem({ description: "Open Fields", event: () => this.props.addDocTab(Docs.Create.KVPDocument(value, { width: 300, height: 300 }), undefined, "onRight"), icon: "layer-group" });
+            ContextMenu.Instance.addItem({ description: "Open Fields", event: () => this.props.addDocTab(Docs.Create.KVPDocument(value, { _width: 300, _height: 300 }), undefined, "onRight"), icon: "layer-group" });
             ContextMenu.Instance.displayMenu(e.clientX, e.clientY);
         }
     }
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 8370df6ba..49fdf56e6 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -50,9 +50,9 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument>
     constructor(props: any) {
         super(props);
         this._initialScale = this.props.ScreenToLocalTransform().Scale;
-        const nw = this.Document.nativeWidth = NumCast(this.extensionDocSync.nativeWidth, NumCast(this.Document.nativeWidth, 927));
-        const nh = this.Document.nativeHeight = NumCast(this.extensionDocSync.nativeHeight, NumCast(this.Document.nativeHeight, 1200));
-        !this.Document.fitWidth && !this.Document.ignoreAspect && (this.Document.height = this.Document[WidthSym]() * (nh / nw));
+        const nw = this.Document._nativeWidth = NumCast(this.dataDoc[this.props.fieldKey + "-nativeWidth"], NumCast(this.Document._nativeWidth, 927));
+        const nh = this.Document._nativeHeight = NumCast(this.dataDoc[this.props.fieldKey + "-nativeHeight"], NumCast(this.Document._nativeHeight, 1200));
+        !this.Document._fitWidth && !this.Document.ignoreAspect && (this.Document.height = this.Document[WidthSym]() * (nh / nw));
 
         const backup = "oldPath";
         const { Document } = this.props;
@@ -90,10 +90,10 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument>
     }
 
     loaded = (nw: number, nh: number, np: number) => {
-        this.extensionDocSync.numPages = np;
-        this.extensionDocSync.nativeWidth = this.Document.nativeWidth = nw * 96 / 72;
-        this.extensionDocSync.nativeHeight = this.Document.nativeHeight = nh * 96 / 72;
-        !this.Document.fitWidth && !this.Document.ignoreAspect && (this.Document.height = this.Document[WidthSym]() * (nh / nw));
+        this.dataDoc[this.props.fieldKey + "-numPages"] = np;
+        this.dataDoc[this.props.fieldKey + "-nativeWidth"] = this.Document._nativeWidth = nw * 96 / 72;
+        this.dataDoc[this.props.fieldKey + "-nativeHeight"] = this.Document._nativeHeight = nh * 96 / 72;
+        !this.Document._fitWidth && !this.Document.ignoreAspect && (this.Document._height = this.Document[WidthSym]() * (nh / nw));
     }
 
     public search(string: string, fwd: boolean) { this._pdfViewer && this._pdfViewer.search(string, fwd); }
@@ -211,7 +211,7 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument>
         const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField);
         const funcs: ContextMenuProps[] = [];
         pdfUrl && funcs.push({ description: "Copy path", event: () => Utils.CopyText(pdfUrl.url.pathname), icon: "expand-arrows-alt" });
-        funcs.push({ description: "Toggle Fit Width " + (this.Document.fitWidth ? "Off" : "On"), event: () => this.Document.fitWidth = !this.Document.fitWidth, icon: "expand-arrows-alt" });
+        funcs.push({ description: "Toggle Fit Width " + (this.Document._fitWidth ? "Off" : "On"), event: () => this.Document._fitWidth = !this.Document._fitWidth, icon: "expand-arrows-alt" });
 
         ContextMenu.Instance.addItem({ description: "Pdf Funcs...", subitems: funcs, icon: "asterisk" });
     }
@@ -220,8 +220,8 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument>
     @computed get renderTitleBox() {
         const classname = "pdfBox" + (this.active() ? "-interactive" : "");
         return <div className={classname} style={{
-            width: !this.props.Document.fitWidth ? this.Document.nativeWidth || 0 : `${100 / this.contentScaling}%`,
-            height: !this.props.Document.fitWidth ? this.Document.nativeHeight || 0 : `${100 / this.contentScaling}%`,
+            width: !this.props.Document._fitWidth ? this.Document._nativeWidth || 0 : `${100 / this.contentScaling}%`,
+            height: !this.props.Document._fitWidth ? this.Document._nativeHeight || 0 : `${100 / this.contentScaling}%`,
             transform: `scale(${this.contentScaling})`
         }}  >
             <div className="pdfBox-title-outer">
@@ -252,7 +252,7 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument>
     render() {
         const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField, null);
         if (this.props.isSelected() || this.props.Document.scrollY !== undefined) this._everActive = true;
-        if (pdfUrl && this.extensionDoc && (this._everActive || (this.extensionDoc.nativeWidth && this.props.ScreenToLocalTransform().Scale < 2.5))) {
+        if (pdfUrl && (this._everActive || (this.dataDoc[this.props.fieldKey + "-nativeWidth"] && this.props.ScreenToLocalTransform().Scale < 2.5))) {
             if (pdfUrl instanceof PdfField && this._pdf) {
                 return this.renderPdfView;
             }
diff --git a/src/client/views/nodes/PresBox.tsx b/src/client/views/nodes/PresBox.tsx
index 1e6894f37..eda12d6fb 100644
--- a/src/client/views/nodes/PresBox.tsx
+++ b/src/client/views/nodes/PresBox.tsx
@@ -336,7 +336,7 @@ export class PresBox extends React.Component<FieldViewProps> {
      */
     @action
     initializeScaleViews = (docList: Doc[], viewtype: number) => {
-        this.props.Document.chromeStatus = "disabled";
+        this.props.Document._chromeStatus = "disabled";
         const hgt = (viewtype === CollectionViewType.Tree) ? 50 : 72;
         docList.forEach((doc: Doc) => {
             doc.presBox = this.props.Document;
@@ -360,7 +360,7 @@ export class PresBox extends React.Component<FieldViewProps> {
         return this.props.ScreenToLocalTransform().translate(-10, -50);// listBox padding-left and pres-box-cont minHeight
     }
     render() {
-        this.initializeScaleViews(this.childDocs, NumCast(this.props.Document.viewType));
+        this.initializeScaleViews(this.childDocs, NumCast(this.props.Document._viewType));
         return (
             <div className="presBox-cont" onContextMenu={this.specificContextMenu}>
                 <div className="presBox-buttons">
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 52b4f391a..066c51654 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -55,12 +55,12 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
 
     videoLoad = () => {
         const aspect = this.player!.videoWidth / this.player!.videoHeight;
-        const nativeWidth = (this.Document.nativeWidth || 0);
-        const nativeHeight = (this.Document.nativeHeight || 0);
+        const nativeWidth = (this.Document._nativeWidth || 0);
+        const nativeHeight = (this.Document._nativeHeight || 0);
         if (!nativeWidth || !nativeHeight) {
-            if (!this.Document.nativeWidth) this.Document.nativeWidth = this.player!.videoWidth;
-            this.Document.nativeHeight = (this.Document.nativeWidth || 0) / aspect;
-            this.Document.height = (this.Document.width || 0) / aspect;
+            if (!this.Document._nativeWidth) this.Document._nativeWidth = this.player!.videoWidth;
+            this.Document._nativeHeight = (this.Document._nativeWidth || 0) / aspect;
+            this.Document._height = (this.Document._width || 0) / aspect;
         }
         if (!this.Document.duration) this.Document.duration = this.player!.duration;
     }
@@ -101,11 +101,11 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
     }
 
     @action public Snapshot() {
-        const width = this.Document.width || 0;
-        const height = this.Document.height || 0;
+        const width = this.Document._width || 0;
+        const height = this.Document._height || 0;
         const canvas = document.createElement('canvas');
         canvas.width = 640;
-        canvas.height = 640 * (this.Document.nativeHeight || 0) / (this.Document.nativeWidth || 1);
+        canvas.height = 640 * (this.Document._nativeHeight || 0) / (this.Document._nativeWidth || 1);
         const ctx = canvas.getContext('2d');//draw image to canvas. scale to target dimensions
         if (ctx) {
             ctx.rect(0, 0, canvas.width, canvas.height);
@@ -117,7 +117,7 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
         if (!this._videoRef) { // can't find a way to take snapshots of videos
             const b = Docs.Create.ButtonDocument({
                 x: (this.Document.x || 0) + width, y: (this.Document.y || 0),
-                width: 150, height: 50, title: (this.Document.currentTimecode || 0).toString()
+                _width: 150, _height: 50, title: (this.Document.currentTimecode || 0).toString()
             });
             b.onClick = ScriptField.MakeScript(`this.currentTimecode = ${(this.Document.currentTimecode || 0)}`);
         } else {
@@ -130,7 +130,7 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
                     const url = this.choosePath(Utils.prepend(returnedFilename));
                     const imageSummary = Docs.Create.ImageDocument(url, {
                         x: (this.Document.x || 0) + width, y: (this.Document.y || 0),
-                        width: 150, height: height / width * 150, title: "--snapshot" + (this.Document.currentTimecode || 0) + " image-"
+                        _width: 150, _height: height / width * 150, title: "--snapshot" + (this.Document.currentTimecode || 0) + " image-"
                     });
                     imageSummary.isButton = true;
                     this.props.addDocument && this.props.addDocument(imageSummary);
@@ -151,12 +151,12 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
 
         if (this.youtubeVideoId) {
             const youtubeaspect = 400 / 315;
-            const nativeWidth = (this.Document.nativeWidth || 0);
-            const nativeHeight = (this.Document.nativeHeight || 0);
+            const nativeWidth = (this.Document._nativeWidth || 0);
+            const nativeHeight = (this.Document._nativeHeight || 0);
             if (!nativeWidth || !nativeHeight) {
-                if (!this.Document.nativeWidth) this.Document.nativeWidth = 600;
-                this.Document.nativeHeight = (this.Document.nativeWidth || 0) / youtubeaspect;
-                this.Document.height = (this.Document.width || 0) / youtubeaspect;
+                if (!this.Document._nativeWidth) this.Document._nativeWidth = 600;
+                this.Document._nativeHeight = (this.Document._nativeWidth || 0) / youtubeaspect;
+                this.Document._height = (this.Document._width || 0) / youtubeaspect;
             }
         }
     }
@@ -321,7 +321,7 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
         const style = "videoBox-content-YouTube" + (this._fullScreen ? "-fullScreen" : "");
         const start = untracked(() => Math.round(this.Document.currentTimecode || 0));
         return <iframe key={this._youtubeIframeId} id={`${this.youtubeVideoId + this._youtubeIframeId}-player`}
-            onLoad={this.youtubeIframeLoaded} className={`${style}`} width={(this.Document.nativeWidth || 640)} height={(this.Document.nativeHeight || 390)}
+            onLoad={this.youtubeIframeLoaded} className={`${style}`} width={(this.Document._nativeWidth || 640)} height={(this.Document._nativeHeight || 390)}
             src={`https://www.youtube.com/embed/${this.youtubeVideoId}?enablejsapi=1&rel=0&showinfo=1&autoplay=1&mute=1&start=${start}&modestbranding=1&controls=${VideoBox._showControls ? 1 : 0}`} />;
     }
 
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index f21f9ff46..102db8810 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -39,12 +39,12 @@ export class WebBox extends DocAnnotatableComponent<FieldViewProps, WebDocument>
         const field = Cast(this.props.Document[this.props.fieldKey], WebField);
         if (field && field.url.href.indexOf("youtube") !== -1) {
             const youtubeaspect = 400 / 315;
-            const nativeWidth = NumCast(this.layoutDoc.nativeWidth);
-            const nativeHeight = NumCast(this.layoutDoc.nativeHeight);
+            const nativeWidth = NumCast(this.layoutDoc._nativeWidth);
+            const nativeHeight = NumCast(this.layoutDoc._nativeHeight);
             if (!nativeWidth || !nativeHeight || Math.abs(nativeWidth / nativeHeight - youtubeaspect) > 0.05) {
-                if (!nativeWidth) this.layoutDoc.nativeWidth = 600;
-                this.layoutDoc.nativeHeight = NumCast(this.layoutDoc.nativeWidth) / youtubeaspect;
-                this.layoutDoc.height = NumCast(this.layoutDoc.width) / youtubeaspect;
+                if (!nativeWidth) this.layoutDoc._nativeWidth = 600;
+                this.layoutDoc._nativeHeight = NumCast(this.layoutDoc._nativeWidth) / youtubeaspect;
+                this.layoutDoc._height = NumCast(this.layoutDoc._width) / youtubeaspect;
             }
         }
 
@@ -87,8 +87,8 @@ export class WebBox extends DocAnnotatableComponent<FieldViewProps, WebDocument>
             x: NumCast(this.props.Document.x),
             y: NumCast(this.props.Document.y),
             title: url,
-            width: 200,
-            height: 70,
+            _width: 200,
+            _height: 70,
             documentText: "@@@" + url
         });
 
diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx
index 6599c0e3c..d8b340db6 100644
--- a/src/client/views/pdf/Annotation.tsx
+++ b/src/client/views/pdf/Annotation.tsx
@@ -11,10 +11,11 @@ import "./Annotation.scss";
 
 interface IAnnotationProps {
     anno: Doc;
-    extensionDoc: Doc;
     addDocTab: (document: Doc, dataDoc: Opt<Doc>, where: string) => boolean;
     pinToPres: (document: Doc) => void;
     focus: (doc: Doc) => void;
+    dataDoc: Doc;
+    fieldKey: string;
 }
 
 export default class Annotation extends React.Component<IAnnotationProps> {
@@ -29,10 +30,11 @@ interface IRegionAnnotationProps {
     y: number;
     width: number;
     height: number;
-    extensionDoc: Doc;
     addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean;
     pinToPres: (document: Doc) => void;
     document: Doc;
+    dataDoc: Doc;
+    fieldKey: string;
 }
 
 @observer
@@ -62,12 +64,12 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> {
     }
 
     deleteAnnotation = () => {
-        const annotation = DocListCast(this.props.extensionDoc.annotations);
+        const annotation = DocListCast(this.props.dataDoc[this.props.fieldKey + "-annotations"]);
         const group = FieldValue(Cast(this.props.document.group, Doc));
         if (group) {
             if (annotation.indexOf(group) !== -1) {
                 const newAnnotations = annotation.filter(a => a !== FieldValue(Cast(this.props.document.group, Doc)));
-                this.props.extensionDoc.annotations = new List<Doc>(newAnnotations);
+                this.props.dataDoc[this.props.fieldKey + "-annotations"] = new List<Doc>(newAnnotations);
             }
 
             DocListCast(group.annotations).forEach(anno => anno.delete = true);
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index d6829937e..a00d2a7f4 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -113,8 +113,8 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
     private _coverPath: any;
 
     @computed get allAnnotations() {
-        return this.extensionDoc ? DocListCast(this.extensionDoc.annotations).filter(
-            anno => this._script.run({ this: anno }, console.log, true).result) : [];
+        return DocListCast(this.dataDoc[this.props.fieldKey + "-annotations"]).filter(
+            anno => this._script.run({ this: anno }, console.log, true).result);
     }
 
     @computed get nonDocAnnotations() {
@@ -208,8 +208,8 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
         await this.initialLoad();
 
         this._annotationReactionDisposer = reaction(
-            () => this.extensionDoc && DocListCast(this.extensionDoc.annotations),
-            annotations => annotations && annotations.length && (this._annotations = annotations),
+            () => DocListCast(this.dataDoc[this.props.fieldKey + "-annotations"]),
+            annotations => annotations?.length && (this._annotations = annotations),
             { fireImmediately: true });
 
         this._filterReactionDisposer = reaction(
@@ -271,8 +271,8 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
             const annoDoc = Docs.Create.FreeformDocument([], { backgroundColor: color, title: "Annotation on " + this.Document.title });
             if (anno.style.left) annoDoc.x = parseInt(anno.style.left);
             if (anno.style.top) annoDoc.y = parseInt(anno.style.top);
-            if (anno.style.height) annoDoc.height = parseInt(anno.style.height);
-            if (anno.style.width) annoDoc.width = parseInt(anno.style.width);
+            if (anno.style.height) annoDoc._height = parseInt(anno.style.height);
+            if (anno.style.width) annoDoc._width = parseInt(anno.style.width);
             annoDoc.group = mainAnnoDoc;
             annoDoc.isButton = true;
             annoDocs.push(annoDoc);
@@ -286,8 +286,8 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
                 const annoDoc = new Doc();
                 if (anno.style.left) annoDoc.x = parseInt(anno.style.left);
                 if (anno.style.top) annoDoc.y = parseInt(anno.style.top);
-                if (anno.style.height) annoDoc.height = parseInt(anno.style.height);
-                if (anno.style.width) annoDoc.width = parseInt(anno.style.width);
+                if (anno.style.height) annoDoc._height = parseInt(anno.style.height);
+                if (anno.style.width) annoDoc._width = parseInt(anno.style.width);
                 annoDoc.group = mainAnnoDoc;
                 annoDoc.backgroundColor = color;
                 annoDocs.push(annoDoc);
@@ -558,7 +558,7 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
     startDrag = (e: PointerEvent, ele: HTMLElement): void => {
         e.preventDefault();
         e.stopPropagation();
-        const targetDoc = Docs.Create.TextDocument({ width: 200, height: 200, title: "Note linked to " + this.props.Document.title });
+        const targetDoc = Docs.Create.TextDocument({ _width: 200, _height: 200, title: "Note linked to " + this.props.Document.title });
         const annotationDoc = this.highlight("rgba(146, 245, 95, 0.467)"); // yellowish highlight color when dragging out a text selection
         if (annotationDoc) {
             DragManager.StartPdfAnnoDrag([ele], new DragManager.PdfAnnoDragData(this.props.Document, annotationDoc, targetDoc), e.pageX, e.pageY, {
@@ -573,11 +573,11 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
         const data = Doc.MakeDelegate(Doc.GetProto(this.props.Document));
         data.title = StrCast(data.title) + "_snippet";
         view.proto = data;
-        view.nativeHeight = marquee.height;
-        view.height = (this.Document[WidthSym]() / (this.Document.nativeWidth || 1)) * marquee.height;
-        view.nativeWidth = this.Document.nativeWidth;
+        view._nativeHeight = marquee.height;
+        view._height = (this.Document[WidthSym]() / (this.Document._nativeWidth || 1)) * marquee.height;
+        view._nativeWidth = this.Document._nativeWidth;
         view.startY = marquee.top;
-        view.width = this.Document[WidthSym]();
+        view._width = this.Document[WidthSym]();
         DragManager.StartDocumentDrag([], new DragManager.DocumentDragData([view]), 0, 0);
     }
 
@@ -598,12 +598,12 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
     getCoverImage = () => {
         if (!this.props.Document[HeightSym]() || !this.props.Document.nativeHeight) {
             setTimeout((() => {
-                this.Document.height = this.Document[WidthSym]() * this._coverPath.height / this._coverPath.width;
-                this.Document.nativeHeight = (this.Document.nativeWidth || 0) * this._coverPath.height / this._coverPath.width;
+                this.Document._height = this.Document[WidthSym]() * this._coverPath.height / this._coverPath.width;
+                this.Document._nativeHeight = (this.Document._nativeWidth || 0) * this._coverPath.height / this._coverPath.width;
             }).bind(this), 0);
         }
-        const nativeWidth = (this.Document.nativeWidth || 0);
-        const nativeHeight = (this.Document.nativeHeight || 0);
+        const nativeWidth = (this.Document._nativeWidth || 0);
+        const nativeHeight = (this.Document._nativeHeight || 0);
         const resolved = Utils.prepend(this._coverPath.path);
         return <img key={resolved} src={resolved} onError={action(() => this._coverPath.path = "http://www.cs.brown.edu/~bcz/face.gif")} onLoad={action(() => this._showWaiting = false)}
             style={{ position: "absolute", display: "inline-block", top: 0, left: 0, width: `${nativeWidth}px`, height: `${nativeHeight}px` }} />;
@@ -623,12 +623,12 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
         TraceMobx();
         return <div className="pdfViewer-annotationLayer" style={{ height: (this.Document.nativeHeight || 0), transform: `scale(${this._zoomed})` }} ref={this._annotationLayer}>
             {this.nonDocAnnotations.sort((a, b) => NumCast(a.y) - NumCast(b.y)).map((anno, index) =>
-                <Annotation {...this.props} focus={this.props.focus} extensionDoc={this.extensionDoc!} anno={anno} key={`${anno[Id]}-annotation`} />)}
+                <Annotation {...this.props} focus={this.props.focus} dataDoc={this.dataDoc!} fieldKey={this.props.fieldKey} anno={anno} key={`${anno[Id]}-annotation`} />)}
         </div>;
     }
     overlayTransform = () => this.scrollXf().scale(1 / this._zoomed);
-    panelWidth = () => (this.Document.scrollHeight || this.Document.nativeHeight || 0);
-    panelHeight = () => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : (this.Document.nativeWidth || 0);
+    panelWidth = () => (this.Document.scrollHeight || this.Document._nativeHeight || 0);
+    panelHeight = () => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : (this.Document._nativeWidth || 0);
     @computed get overlayLayer() {
         return <div className={`pdfViewer-overlay${InkingControl.Instance.selectedTool !== InkTool.None ? "-inking" : ""}`} id="overlay" style={{ transform: `scale(${this._zoomed})` }}>
             <CollectionFreeFormView {...this.props}
@@ -675,20 +675,19 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
     contentZoom = () => this._zoomed;
     render() {
         TraceMobx();
-        return !this.extensionDoc ? (null) :
-            <div className={"pdfViewer" + (this._zoomed !== 1 ? "-zoomed" : "")} ref={this._mainCont}
-                onScroll={this.onScroll} onWheel={this.onZoomWheel} onPointerDown={this.onPointerDown} onClick={this.onClick}
-                style={{
-                    width: !this.props.Document.fitWidth ? NumCast(this.props.Document.nativeWidth) : `${100 / this.contentScaling}%`,
-                    height: !this.props.Document.fitWidth ? NumCast(this.props.Document.nativeHeight) : `${100 / this.contentScaling}%`,
-                    transform: `scale(${this.props.ContentScaling()})`
-                }}  >
-                {this.pdfViewerDiv}
-                {this.overlayLayer}
-                {this.annotationLayer}
-                {this.standinViews}
-                <PdfViewerMarquee isMarqueeing={this.marqueeing} width={this.marqueeWidth} height={this.marqueeHeight} x={this.marqueeX} y={this.marqueeY} />
-            </div >;
+        return <div className={"pdfViewer" + (this._zoomed !== 1 ? "-zoomed" : "")} ref={this._mainCont}
+            onScroll={this.onScroll} onWheel={this.onZoomWheel} onPointerDown={this.onPointerDown} onClick={this.onClick}
+            style={{
+                width: !this.props.Document._fitWidth ? NumCast(this.props.Document._nativeWidth) : `${100 / this.contentScaling}%`,
+                height: !this.props.Document._fitWidth ? NumCast(this.props.Document._nativeHeight) : `${100 / this.contentScaling}%`,
+                transform: `scale(${this.props.ContentScaling()})`
+            }}  >
+            {this.pdfViewerDiv}
+            {this.overlayLayer}
+            {this.annotationLayer}
+            {this.standinViews}
+            <PdfViewerMarquee isMarqueeing={this.marqueeing} width={this.marqueeWidth} height={this.marqueeHeight} x={this.marqueeX} y={this.marqueeY} />
+        </div >;
     }
 }
 
diff --git a/src/client/views/presentationview/PresElementBox.tsx b/src/client/views/presentationview/PresElementBox.tsx
index 92dc0f3d2..dad55e1fd 100644
--- a/src/client/views/presentationview/PresElementBox.tsx
+++ b/src/client/views/presentationview/PresElementBox.tsx
@@ -161,12 +161,12 @@ export class PresElementBox extends DocComponent<FieldViewProps, PresDocument>(P
             return (null);
         }
 
-        const propDocWidth = NumCast(this.layoutDoc.nativeWidth);
-        const propDocHeight = NumCast(this.layoutDoc.nativeHeight);
-        const scale = () => 175 / NumCast(this.layoutDoc.nativeWidth, 175);
+        const propDocWidth = NumCast(this.layoutDoc._nativeWidth);
+        const propDocHeight = NumCast(this.layoutDoc._nativeHeight);
+        const scale = () => 175 / NumCast(this.layoutDoc._nativeWidth, 175);
         return (
             <div className="presElementBox-embedded" style={{
-                height: propDocHeight === 0 ? NumCast(this.layoutDoc.height) - NumCast(this.layoutDoc.collapsedHeight) : propDocHeight * scale(),
+                height: propDocHeight === 0 ? NumCast(this.layoutDoc._height) - NumCast(this.layoutDoc.collapsedHeight) : propDocHeight * scale(),
                 width: propDocWidth === 0 ? "auto" : propDocWidth * scale(),
             }}>
                 <ContentFittingDocumentView
@@ -192,7 +192,7 @@ export class PresElementBox extends DocComponent<FieldViewProps, PresDocument>(P
     }
 
     render() {
-        const treecontainer = this.props.ContainingCollectionDoc && this.props.ContainingCollectionDoc.viewType === CollectionViewType.Tree;
+        const treecontainer = this.props.ContainingCollectionDoc?._viewType === CollectionViewType.Tree;
         const className = "presElementBox-item" + (this.currentIndex === this.indexInPres ? " presElementBox-selected" : "");
         const pbi = "presElementBox-interaction";
         return (
diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx
index f7ae83b2f..be13dae03 100644
--- a/src/client/views/search/SearchBox.tsx
+++ b/src/client/views/search/SearchBox.tsx
@@ -217,16 +217,16 @@ export class SearchBox extends React.Component {
             doc.x = x;
             doc.y = y;
             const size = 200;
-            const aspect = NumCast(doc.nativeHeight) / NumCast(doc.nativeWidth, 1);
+            const aspect = NumCast(doc._nativeHeight) / NumCast(doc._nativeWidth, 1);
             if (aspect > 1) {
-                doc.height = size;
-                doc.width = size / aspect;
+                doc._height = size;
+                doc._width = size / aspect;
             } else if (aspect > 0) {
-                doc.width = size;
-                doc.height = size * aspect;
+                doc._width = size;
+                doc._height = size * aspect;
             } else {
-                doc.width = size;
-                doc.height = size;
+                doc._width = size;
+                doc._height = size;
             }
             x += 250;
             if (x > 1000) {
@@ -234,7 +234,7 @@ export class SearchBox extends React.Component {
                 y += 300;
             }
         }
-        return Docs.Create.TreeDocument(docs, { width: 200, height: 400, backgroundColor: "grey", title: `Search Docs: "${this._searchString}"` });
+        return Docs.Create.TreeDocument(docs, { _width: 200, _height: 400, backgroundColor: "grey", title: `Search Docs: "${this._searchString}"` });
     }
 
     @action.bound
diff --git a/src/client/views/search/SearchItem.tsx b/src/client/views/search/SearchItem.tsx
index 21a4560cf..8aea737f0 100644
--- a/src/client/views/search/SearchItem.tsx
+++ b/src/client/views/search/SearchItem.tsx
@@ -68,11 +68,11 @@ export class SelectorContextMenu extends React.Component<SearchItemProps> {
     getOnClick({ col, target }: { col: Doc, target: Doc }) {
         return () => {
             col = Doc.IsPrototype(col) ? Doc.MakeDelegate(col) : col;
-            if (NumCast(col.viewType, CollectionViewType.Invalid) === CollectionViewType.Freeform) {
-                const newPanX = NumCast(target.x) + NumCast(target.width) / 2;
-                const newPanY = NumCast(target.y) + NumCast(target.height) / 2;
-                col.panX = newPanX;
-                col.panY = newPanY;
+            if (NumCast(col._viewType, CollectionViewType.Invalid) === CollectionViewType.Freeform) {
+                const newPanX = NumCast(target.x) + NumCast(target._width) / 2;
+                const newPanY = NumCast(target.y) + NumCast(target._height) / 2;
+                col._panX = newPanX;
+                col._panY = newPanY;
             }
             CollectionDockingView.AddRightSplit(col, undefined);
         };
diff --git a/src/mobile/ImageUpload.tsx b/src/mobile/ImageUpload.tsx
index 0b0280519..dab4de110 100644
--- a/src/mobile/ImageUpload.tsx
+++ b/src/mobile/ImageUpload.tsx
@@ -55,7 +55,7 @@ class Uploader extends React.Component {
                     const json = await res.json();
                     json.map(async (file: any) => {
                         const path = window.location.origin + file;
-                        const doc = Docs.Create.ImageDocument(path, { nativeWidth: 200, width: 200, title: name });
+                        const doc = Docs.Create.ImageDocument(path, { _nativeWidth: 200, _width: 200, title: name });
 
                         this.status = "getting user document";
 
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index 4b7f10f91..16f67d419 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -162,8 +162,8 @@ export class Doc extends RefField {
 
     private [Self] = this;
     private [SelfProxy]: any;
-    public [WidthSym] = () => NumCast(this[SelfProxy].width);
-    public [HeightSym] = () => NumCast(this[SelfProxy].height);
+    public [WidthSym] = () => NumCast(this[SelfProxy]._width);
+    public [HeightSym] = () => NumCast(this[SelfProxy]._height);
 
     [ToScriptString]() {
         return "invalid";
@@ -447,19 +447,18 @@ export namespace Doc {
         if (!WillExpandTemplateLayout(templateLayoutDoc, dataDoc) || !dataDoc) return templateLayoutDoc;
 
         const templateField = StrCast(templateLayoutDoc.isTemplateForField);  // the field that the template renders
-        const extensionDoc = fieldExtensionDoc(dataDoc, templateField); // an extension doc for the field that the template renders
         // First it checks if an expanded layout already exists -- if so it will be stored on the dataDoc
         // using the template layout doc's id as the field key.
         // If it doesn't find the expanded layout, then it makes a delegate of the template layout and
         // saves it on the data doc indexed by the template layout's id.
         //
-        const expandedLayoutFieldKey = "Layout[" + templateLayoutDoc[Id] + "]";
-        const expandedTemplateLayout = extensionDoc?.[expandedLayoutFieldKey];
-        if (expandedTemplateLayout === undefined && extensionDoc) {
+        const expandedLayoutFieldKey = templateField + "-layout[" + templateLayoutDoc[Id] + "]";
+        const expandedTemplateLayout = dataDoc?.[expandedLayoutFieldKey];
+        if (expandedTemplateLayout === undefined) {
             setTimeout(() => {
-                if (!extensionDoc[expandedLayoutFieldKey]) {
+                if (!dataDoc[expandedLayoutFieldKey]) {
                     const newLayoutDoc = Doc.MakeDelegate(templateLayoutDoc, undefined, "[" + templateLayoutDoc.title + "]");
-                    extensionDoc[expandedLayoutFieldKey] = newLayoutDoc;
+                    dataDoc[expandedLayoutFieldKey] = newLayoutDoc;
                     newLayoutDoc.resolvedDataDoc = dataDoc;
                 }
             }, 0);
@@ -473,26 +472,6 @@ export namespace Doc {
         const resolvedDataDoc = containerDataDoc === containerDoc || !containerDataDoc ? undefined : Doc.GetDataDoc(containerDataDoc);
         return { layout: Doc.expandTemplateLayout(childDoc, resolvedDataDoc), data: resolvedDataDoc };
     }
-
-    //
-    // Resolves a reference to a field by returning 'doc' if no field extension is specified,
-    // otherwise, it returns the extension document stored in doc.<fieldKey>_ext.
-    // This mechanism allows any fields to be extended with an extension document that can
-    // be used to capture field-specific metadata.  For example, an image field can be extended
-    // to store annotations, ink, and other data.
-    //
-    export function fieldExtensionDoc(doc: Doc, fieldKey: string) {
-        if (!fieldKey) return undefined;
-        const extension = doc[fieldKey + "_ext"];
-        if (doc instanceof Doc && extension === undefined) {
-            setTimeout(() => CreateDocumentExtensionForField(doc, fieldKey), 0);
-        }
-        return extension ? extension as Doc : undefined;
-    }
-    export function fieldExtensionDocSync(doc: Doc, fieldKey: string) {
-        return (doc[fieldKey + "_ext"] as Doc) || CreateDocumentExtensionForField(doc, fieldKey);
-    }
-
     export function CreateDocumentExtensionForField(doc: Doc, fieldKey: string) {
         let proto: Doc | undefined = doc;
         while (proto && !Doc.IsPrototype(proto) && proto.proto) {
@@ -586,8 +565,8 @@ export namespace Doc {
     export function ApplyTemplateTo(templateDoc: Doc, target: Doc, targetKey: string, titleTarget: string | undefined = undefined) {
         if (!templateDoc) {
             target.layout = undefined;
-            target.nativeWidth = undefined;
-            target.nativeHeight = undefined;
+            target._nativeWidth = undefined;
+            target._nativeHeight = undefined;
             target.onClick = undefined;
             target.type = undefined;
             return;
@@ -648,12 +627,12 @@ export namespace Doc {
         const doc1Layout = Doc.Layout(doc1);
         const x2 = NumCast(doc2.x) - clusterDistance;
         const y2 = NumCast(doc2.y) - clusterDistance;
-        const w2 = NumCast(doc2Layout.width) + clusterDistance;
-        const h2 = NumCast(doc2Layout.height) + clusterDistance;
+        const w2 = NumCast(doc2Layout._width) + clusterDistance;
+        const h2 = NumCast(doc2Layout._height) + clusterDistance;
         const x = NumCast(doc1.x) - clusterDistance;
         const y = NumCast(doc1.y) - clusterDistance;
-        const w = NumCast(doc1Layout.width) + clusterDistance;
-        const h = NumCast(doc1Layout.height) + clusterDistance;
+        const w = NumCast(doc1Layout._width) + clusterDistance;
+        const h = NumCast(doc1Layout._height) + clusterDistance;
         return doc1.z === doc2.z && intersectRect({ left: x, top: y, width: w, height: h }, { left: x2, top: y2, width: w2, height: h2 });
     }
 
@@ -812,7 +791,7 @@ Scripting.addGlobal(function matchFieldValue(doc: Doc, key: string, value: any)
     return fieldStr == value;
 });
 Scripting.addGlobal(function setDocFilter(container: Doc, key: string, value: any, modifiers?: string) {
-    const docFilters = Cast(container.docFilter, listSpec("string"), []);
+    const docFilters = Cast(container._docFilter, listSpec("string"), []);
     for (let i = 0; i < docFilters.length; i += 3) {
         if (docFilters[i] === key && docFilters[i + 1] === value) {
             docFilters.splice(i, 3);
@@ -823,7 +802,7 @@ Scripting.addGlobal(function setDocFilter(container: Doc, key: string, value: an
         docFilters.push(key);
         docFilters.push(value)
         docFilters.push(modifiers);
-        container.docFilter = new List<string>(docFilters);
+        container._docFilter = new List<string>(docFilters);
     }
     const docFilterText = Doc.MakeDocFilter(docFilters);
     container.viewSpecScript = docFilterText ? ScriptField.MakeFunction(docFilterText, { doc: Doc.name }) : undefined;
diff --git a/src/new_fields/RichTextUtils.ts b/src/new_fields/RichTextUtils.ts
index 682206aa2..6f34c82db 100644
--- a/src/new_fields/RichTextUtils.ts
+++ b/src/new_fields/RichTextUtils.ts
@@ -273,7 +273,7 @@ export namespace RichTextUtils {
             const guid = Utils.GenerateDeterministicGuid(src);
             const backingDocId = StrCast(textNote[guid]);
             if (!backingDocId) {
-                const backingDoc = Docs.Create.ImageDocument(src, { width: 300, height: 300 });
+                const backingDoc = Docs.Create.ImageDocument(src, { _width: 300, _height: 300 });
                 DocumentView.makeCustomViewClicked(backingDoc, undefined);
                 docid = backingDoc[Id];
                 textNote[guid] = docid;
diff --git a/src/new_fields/documentSchemas.ts b/src/new_fields/documentSchemas.ts
index 180c52b3d..44ba6d65c 100644
--- a/src/new_fields/documentSchemas.ts
+++ b/src/new_fields/documentSchemas.ts
@@ -8,16 +8,18 @@ export const documentSchema = createSchema({
     layoutKey: "string",        // holds the field key for the field that actually holds the current lyoat
     layoutCustom: Doc,          // used to hold a custom layout (there's nothing special about this field .. any field could hold a custom layout that can be selected by setting 'layoutKey')
     title: "string",            // document title (can be on either data document or layout)
-    nativeWidth: "number",      // native width of document which determines how much document contents are scaled when the document's width is set
-    nativeHeight: "number",     // "
-    width: "number",            // width of document in its container's coordinate system
-    height: "number",           // "
+    _dropAction: "string",       // override specifying what should happen when this document is dropped (can be "alias" or "copy")
+    _nativeWidth: "number",      // native width of document which determines how much document contents are scaled when the document's width is set
+    _nativeHeight: "number",     // "
+    _width: "number",            // width of document in its container's coordinate system
+    _height: "number",           // "
+    _freeformLayoutEngine: "string",// the string ID for the layout engine to use to layout freeform view documents
+    _LODdisable: "boolean",      // whether to disbale LOD switching for CollectionFreeFormViews
     color: "string",            // foreground color of document
     backgroundColor: "string",  // background color of document
     opacity: "number",          // opacity of document
     creationDate: DateField,     // when the document was created
     links: listSpec(Doc),       // computed (readonly) list of links associated with this document
-    dropAction: "string",       // override specifying what should happen when this document is dropped (can be "alias" or "copy")
     removeDropProperties: listSpec("string"), // properties that should be removed from the alias/copy/etc of this document when it is dropped
     onClick: ScriptField,       // script to run when document is clicked (can be overriden by an onClick prop)
     onPointerDown: ScriptField,       // script to run when document is clicked (can be overriden by an onClick prop)
@@ -56,7 +58,6 @@ export const documentSchema = createSchema({
     xPadding: "number",         // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set
     yPadding: "number",         // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set
     LODarea: "number",          // area (width*height) where CollectionFreeFormViews switch from a label to rendering contents
-    LODdisable: "boolean",      // whether to disbale LOD switching for CollectionFreeFormViews
     letterSpacing: "string",
     textTransform: "string"
 });
diff --git a/src/new_fields/util.ts b/src/new_fields/util.ts
index 3255c6172..7d093b6ea 100644
--- a/src/new_fields/util.ts
+++ b/src/new_fields/util.ts
@@ -7,6 +7,7 @@ import { ObjectField } from "./ObjectField";
 import { action, trace } from "mobx";
 import { Parent, OnUpdate, Update, Id, SelfProxy, Self } from "./FieldSymbols";
 import { DocServer } from "../client/DocServer";
+import { props } from "bluebird";
 
 function _readOnlySetter(): never {
     throw new Error("Documents can't be modified in read-only mode");
@@ -35,6 +36,7 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number
         target[prop] = value;
         return true;
     }
+
     if (typeof prop === "symbol") {
         target[prop] = value;
         return true;
@@ -98,11 +100,39 @@ export function makeEditable() {
     _setter = _setterImpl;
 }
 
-export function setter(target: any, prop: string | symbol | number, value: any, receiver: any): boolean {
+let layoutProps = ["panX", "panY", "width", "height", "nativeWidth", "nativeHeight", "fitWidth", "fitToBox",
+    "LODdisable", "dropAction", "chromeStatus", "viewType", "gridGap", "xMargin", "yMargin"];
+export function setter(target: any, in_prop: string | symbol | number, value: any, receiver: any): boolean {
+    let prop = in_prop;
+    if (typeof prop === "string" && prop !== "__id" && prop !== "__fields" &&
+        ((prop as string).startsWith("_") || layoutProps.includes(prop))) {
+        if (!prop.startsWith("_")) {
+            console.log(prop + " is deprecated - switch to _" + prop);
+            prop = "_" + prop;
+        }
+        const resolvedLayout = getFieldImpl(target, getFieldImpl(target, "layoutKey", receiver), receiver); // + "-" + (prop as string).substring(1);
+        if (resolvedLayout instanceof Doc) {
+            resolvedLayout[prop] = value;
+            return true;
+        }
+    }
     return _setter(target, prop, value, receiver);
 }
 
-export function getter(target: any, prop: string | symbol | number, receiver: any): any {
+export function getter(target: any, in_prop: string | symbol | number, receiver: any): any {
+    let prop = in_prop;
+    if (typeof prop === "string" && prop !== "__id" && prop !== "__fields" &&
+        ((prop as string).startsWith("_") || layoutProps.includes(prop))) {
+        if (!prop.startsWith("_")) {
+            console.log(prop + " is deprecated - switch to _" + prop);
+            prop = "_" + prop;
+        }
+        const resolvedLayout = getFieldImpl(target, getFieldImpl(target, "layoutKey", receiver), receiver); // + "-" + (prop as string).substring(1);
+        if (resolvedLayout instanceof Doc) {
+            return resolvedLayout[prop];
+        }
+        // prop = getFieldImpl(target, "layoutKey", receiver) + "-" + (prop as string).substring(1);
+    }
     if (prop === "then") {//If we're being awaited
         return undefined;
     }
diff --git a/src/scraping/buxton/scraper.py b/src/scraping/buxton/scraper.py
index c7b795bbd..f16bbfd14 100644
--- a/src/scraping/buxton/scraper.py
+++ b/src/scraping/buxton/scraper.py
@@ -85,13 +85,14 @@ def write_collection(parse_results, display_fields, storage_key, viewType):
             "proto": protofy(data_doc["_id"]),
             "x": 10,
             "y": 10,
-            "width": 900,
-            "height": 600,
-            "panX": 0,
-            "panY": 0,
+            "_width": 900,
+            "_height": 600,
+            "_panX": 0,
+            "_panY": 0,
             "zIndex": 2,
             "libraryBrush": False,
-            "viewType": viewType
+            "_viewType": viewType,
+            "_LODdisable": True
         },
         "__type": "Doc"
     }
@@ -100,9 +101,8 @@ def write_collection(parse_results, display_fields, storage_key, viewType):
     fields[storage_key] = listify(proxify_guids(view_guids))
     fields["schemaColumns"] = listify(display_fields)
     fields["backgroundColor"] = "white"
-    fields["viewType"] = 2
+    fields["_viewType"] = 2
     fields["author"] = "Bill Buxton"
-    fields["LODdisable"] = True
     fields["creationDate"] = {
         "date": datetime.datetime.utcnow().microsecond,
         "__type": "date"
diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts
index 406d71085..361cf9216 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -44,18 +44,18 @@ export class CurrentUserUtils {
     // setup the "creator" buttons for the sidebar-- eg. the default set of draggable document creation tools
     static setupCreatorButtons(doc: Doc, buttons?: string[]) {
         const notes = CurrentUserUtils.setupNoteTypes(doc);
-        doc.noteTypes = Docs.Create.TreeDocument(notes, { title: "Note Types", height: 75 });
+        doc.noteTypes = Docs.Create.TreeDocument(notes, { title: "Note Types", _height: 75 });
         doc.activePen = doc;
         const docProtoData: { title: string, icon: string, drag?: string, ignoreClick?: boolean, click?: string, ischecked?: string, activePen?: Doc, backgroundColor?: string, dragFactory?: Doc }[] = [
-            { title: "collection", icon: "folder", ignoreClick: true, drag: 'Docs.Create.FreeformDocument([], { nativeWidth: undefined, nativeHeight: undefined, LODdisable: true, width: 150, height: 100, title: "freeform" })' },
-            { title: "preview", icon: "expand", ignoreClick: true, drag: 'Docs.Create.DocumentDocument(ComputedField.MakeFunction("selectedDocs(this,true,[_last_])?.[0]"), { width: 250, height: 250, title: "container" })' },
+            { title: "collection", icon: "folder", ignoreClick: true, drag: 'Docs.Create.FreeformDocument([], { _nativeWidth: undefined, _nativeHeight: undefined, _LODdisable: true, _width: 150, _height: 100, title: "freeform" })' },
+            { title: "preview", icon: "expand", ignoreClick: true, drag: 'Docs.Create.DocumentDocument(ComputedField.MakeFunction("selectedDocs(this,true,[_last_])?.[0]"), { _width: 250, _height: 250, title: "container" })' },
             { title: "todo item", icon: "check", ignoreClick: true, drag: 'getCopy(this.dragFactory, true)', dragFactory: notes[notes.length - 1] },
-            { title: "web page", icon: "globe-asia", ignoreClick: true, drag: 'Docs.Create.WebDocument("https://en.wikipedia.org/wiki/Hedgehog", { width: 300, height: 300, title: "New Webpage" })' },
-            { title: "cat image", icon: "cat", ignoreClick: true, drag: 'Docs.Create.ImageDocument("https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg", { width: 200, title: "an image of a cat" })' },
-            { title: "record", icon: "microphone", ignoreClick: true, drag: `Docs.Create.AudioDocument("${nullAudio}", { width: 200, title: "ready to record audio" })` },
-            { title: "clickable button", icon: "bolt", ignoreClick: true, drag: 'Docs.Create.ButtonDocument({ width: 150, height: 50, title: "Button" })' },
-            { title: "presentation", icon: "tv", ignoreClick: true, drag: 'Doc.UserDoc().curPresentation = Docs.Create.PresDocument(new List<Doc>(), { width: 200, height: 500, title: "a presentation trail" })' },
-            { title: "import folder", icon: "cloud-upload-alt", ignoreClick: true, drag: 'Docs.Create.DirectoryImportDocument({ title: "Directory Import", width: 400, height: 400 })' },
+            { title: "web page", icon: "globe-asia", ignoreClick: true, drag: 'Docs.Create.WebDocument("https://en.wikipedia.org/wiki/Hedgehog", {_width: 300, _height: 300, title: "New Webpage" })' },
+            { title: "cat image", icon: "cat", ignoreClick: true, drag: 'Docs.Create.ImageDocument("https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg", { _width: 200, title: "an image of a cat" })' },
+            { title: "record", icon: "microphone", ignoreClick: true, drag: `Docs.Create.AudioDocument("${nullAudio}", { _width: 200, title: "ready to record audio" })` },
+            { title: "clickable button", icon: "bolt", ignoreClick: true, drag: 'Docs.Create.ButtonDocument({ _width: 150, _height: 50, title: "Button" })' },
+            { title: "presentation", icon: "tv", ignoreClick: true, drag: 'Doc.UserDoc().curPresentation = Docs.Create.PresDocument(new List<Doc>(), { _width: 200, _height: 500, title: "a presentation trail" })' },
+            { title: "import folder", icon: "cloud-upload-alt", ignoreClick: true, drag: 'Docs.Create.DirectoryImportDocument({ title: "Directory Import", _width: 400, _height: 400 })' },
             { title: "mobile view", icon: "phone", ignoreClick: true, drag: 'Doc.UserDoc().activeMobile' },
             { title: "use pen", icon: "pen-nib", click: 'activatePen(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this,2, this.backgroundColor)', backgroundColor: "blue", ischecked: `sameDocs(this.activePen.pen,  this)`, activePen: doc },
             { title: "use highlighter", icon: "highlighter", click: 'activateBrush(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this,20,this.backgroundColor)', backgroundColor: "yellow", ischecked: `sameDocs(this.activePen.pen, this)`, activePen: doc },
@@ -65,7 +65,7 @@ export class CurrentUserUtils {
             { title: "use drag", icon: "mouse-pointer", click: 'deactivateInk();this.activePen.pen = this;', ischecked: `sameDocs(this.activePen.pen, this)`, backgroundColor: "white", activePen: doc },
         ];
         return docProtoData.filter(d => !buttons || !buttons.includes(d.title)).map(data => Docs.Create.FontIconDocument({
-            nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, dropAction: data.click ? "copy" : undefined, title: data.title, icon: data.icon, ignoreClick: data.ignoreClick,
+            _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100, _dropAction: data.click ? "copy" : undefined, title: data.title, icon: data.icon, ignoreClick: data.ignoreClick,
             onDragStart: data.drag ? ScriptField.MakeFunction(data.drag) : undefined, onClick: data.click ? ScriptField.MakeScript(data.click) : undefined,
             ischecked: data.ischecked ? ComputedField.MakeFunction(data.ischecked) : undefined, activePen: data.activePen,
             backgroundColor: data.backgroundColor, removeDropProperties: new List<string>(["dropAction"]), dragFactory: data.dragFactory,
@@ -103,7 +103,7 @@ export class CurrentUserUtils {
             { title: "use drag", icon: "mouse-pointer", click: 'deactivateInk();this.activePen.pen = this;', ischecked: `sameDocs(this.activePen.pen, this)`, backgroundColor: "white", activePen: doc },
         ];
         return docProtoData.filter(d => !buttons || !buttons.includes(d.title)).map(data => Docs.Create.FontIconDocument({
-            nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, dropAction: data.click ? "copy" : undefined, title: data.title, icon: data.icon, ignoreClick: data.ignoreClick,
+            _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100, _dropAction: data.click ? "copy" : undefined, title: data.title, icon: data.icon, ignoreClick: data.ignoreClick,
             onDragStart: data.drag ? ScriptField.MakeFunction(data.drag) : undefined, onClick: data.click ? ScriptField.MakeScript(data.click) : undefined,
             ischecked: data.ischecked ? ComputedField.MakeFunction(data.ischecked) : undefined, activePen: data.activePen,
             backgroundColor: data.backgroundColor, removeDropProperties: new List<string>(["dropAction"]), dragFactory: data.dragFactory,
@@ -114,11 +114,11 @@ export class CurrentUserUtils {
         const docProtoData: { title: string, icon: string, drag?: string, ignoreClick?: boolean, pointerDown?: string, pointerUp?: string, ischecked?: string, clipboard?: Doc, activePen?: Doc, backgroundColor?: string, dragFactory?: Doc }[] = [
             { title: "use pen", icon: "pen-nib", pointerUp: "resetPen()", pointerDown: 'setPen(2, this.backgroundColor)', backgroundColor: "blue", ischecked: `sameDocs(this.activePen.pen,  this)`, activePen: doc },
             { title: "use highlighter", icon: "highlighter", pointerUp: "resetPen()", pointerDown: 'setPen(20, this.backgroundColor)', backgroundColor: "yellow", ischecked: `sameDocs(this.activePen.pen, this)`, activePen: doc },
-            { title: "notepad", icon: "clipboard", pointerUp: "GestureOverlay.Instance.closeFloatingDoc()", pointerDown: 'GestureOverlay.Instance.openFloatingDoc(this.clipboard)', clipboard: Docs.Create.FreeformDocument([], { width: 300, height: 300 }), backgroundColor: "orange", ischecked: `sameDocs(this.activePen.pen, this)`, activePen: doc },
-            { title: "interpret text", icon: "font", pointerUp: "setToolglass('none')", pointerDown: "setToolglass('inktotext')", clipboard: Docs.Create.FreeformDocument([], { width: 300, height: 300 }), backgroundColor: "orange", ischecked: `sameDocs(this.activePen.pen, this)`, activePen: doc },
+            { title: "notepad", icon: "clipboard", pointerUp: "GestureOverlay.Instance.closeFloatingDoc()", pointerDown: 'GestureOverlay.Instance.openFloatingDoc(this.clipboard)', clipboard: Docs.Create.FreeformDocument([], { _width: 300, _height: 300 }), backgroundColor: "orange", ischecked: `sameDocs(this.activePen.pen, this)`, activePen: doc },
+            { title: "interpret text", icon: "font", pointerUp: "setToolglass('none')", pointerDown: "setToolglass('inktotext')", clipboard: Docs.Create.FreeformDocument([], { _width: 300, _height: 300 }), backgroundColor: "orange", ischecked: `sameDocs(this.activePen.pen, this)`, activePen: doc },
         ];
         return docProtoData.map(data => Docs.Create.FontIconDocument({
-            nativeWidth: 10, nativeHeight: 10, width: 10, height: 10, dropAction: data.pointerDown ? "copy" : undefined, title: data.title, icon: data.icon, ignoreClick: data.ignoreClick,
+            _nativeWidth: 10, _nativeHeight: 10, _width: 10, _height: 10, _dropAction: data.pointerDown ? "copy" : undefined, title: data.title, icon: data.icon, ignoreClick: data.ignoreClick,
             onDragStart: data.drag ? ScriptField.MakeFunction(data.drag) : undefined,
             clipboard: data.clipboard,
             onPointerUp: data.pointerUp ? ScriptField.MakeScript(data.pointerUp) : undefined, onPointerDown: data.pointerDown ? ScriptField.MakeScript(data.pointerDown) : undefined,
@@ -130,7 +130,7 @@ export class CurrentUserUtils {
     static setupThumbDoc(userDoc: Doc) {
         if (!userDoc.thumbDoc) {
             userDoc.thumbDoc = Docs.Create.LinearDocument(CurrentUserUtils.setupThumbButtons(userDoc), {
-                width: 100, height: 50, ignoreClick: true, lockedPosition: true, chromeStatus: "disabled", title: "buttons", autoHeight: true, yMargin: 5, isExpanded: true, backgroundColor: "white"
+                _width: 100, _height: 50, ignoreClick: true, lockedPosition: true, _chromeStatus: "disabled", title: "buttons", autoHeight: true, _yMargin: 5, isExpanded: true, backgroundColor: "white"
             });
         }
         return userDoc.thumbDoc;
@@ -138,7 +138,7 @@ export class CurrentUserUtils {
 
     static setupMobileDoc(userDoc: Doc) {
         return userDoc.activeMoble ?? Docs.Create.MasonryDocument(CurrentUserUtils.setupMobileButtons(userDoc), {
-            columnWidth: 100, ignoreClick: true, lockedPosition: true, chromeStatus: "disabled", title: "buttons", autoHeight: true, yMargin: 5
+            columnWidth: 100, ignoreClick: true, lockedPosition: true, _chromeStatus: "disabled", title: "buttons", autoHeight: true, _yMargin: 5
         });
     }
 
@@ -146,19 +146,19 @@ export class CurrentUserUtils {
     static setupToolsPanel(sidebarContainer: Doc, doc: Doc) {
         // setup a masonry view of all he creators
         const dragCreators = Docs.Create.MasonryDocument(CurrentUserUtils.setupCreatorButtons(doc), {
-            width: 500, autoHeight: true, columnWidth: 35, ignoreClick: true, lockedPosition: true, chromeStatus: "disabled", title: "buttons",
-            dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }), yMargin: 5
+            _width: 500, autoHeight: true, columnWidth: 35, ignoreClick: true, lockedPosition: true, _chromeStatus: "disabled", title: "buttons",
+            dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }), _yMargin: 5
         });
         // setup a color picker
         const color = Docs.Create.ColorDocument({
-            title: "color picker", width: 300, dropAction: "alias", forceActive: true, removeDropProperties: new List<string>(["dropAction", "forceActive"])
+            title: "color picker", _width: 300, _dropAction: "alias", forceActive: true, removeDropProperties: new List<string>(["dropAction", "forceActive"])
         });
 
         return Docs.Create.ButtonDocument({
-            width: 35, height: 25, backgroundColor: "lightgrey", color: "rgb(34, 34, 34)", title: "Tools", fontSize: 10, targetContainer: sidebarContainer,
+            _width: 35, _height: 25, backgroundColor: "lightgrey", color: "rgb(34, 34, 34)", title: "Tools", fontSize: 10, targetContainer: sidebarContainer,
             letterSpacing: "0px", textTransform: "unset", borderRounding: "5px 5px 0px 0px", boxShadow: "3px 3px 0px rgb(34, 34, 34)",
             sourcePanel: Docs.Create.StackingDocument([dragCreators, color], {
-                width: 500, lockedPosition: true, chromeStatus: "disabled", title: "tools stack"
+                _width: 500, lockedPosition: true, _chromeStatus: "disabled", title: "tools stack"
             }),
             onClick: ScriptField.MakeScript("this.targetContainer.proto = this.sourcePanel"),
         });
@@ -168,23 +168,23 @@ export class CurrentUserUtils {
     static setupLibraryPanel(sidebarContainer: Doc, doc: Doc) {
         // setup workspaces library item
         doc.workspaces = Docs.Create.TreeDocument([], {
-            title: "WORKSPACES", height: 100, forceActive: true, boxShadow: "0 0", lockedPosition: true, backgroundColor: "#eeeeee"
+            title: "WORKSPACES", _height: 100, forceActive: true, boxShadow: "0 0", lockedPosition: true, backgroundColor: "#eeeeee"
         });
 
         doc.documents = Docs.Create.TreeDocument([], {
-            title: "DOCUMENTS", height: 42, forceActive: true, boxShadow: "0 0", preventTreeViewOpen: true, lockedPosition: true, backgroundColor: "#eeeeee"
+            title: "DOCUMENTS", _height: 42, forceActive: true, boxShadow: "0 0", preventTreeViewOpen: true, lockedPosition: true, backgroundColor: "#eeeeee"
         });
 
         // setup Recently Closed library item
         doc.recentlyClosed = Docs.Create.TreeDocument([], {
-            title: "RECENTLY CLOSED", height: 75, forceActive: true, boxShadow: "0 0", preventTreeViewOpen: true, lockedPosition: true, backgroundColor: "#eeeeee"
+            title: "RECENTLY CLOSED", _height: 75, forceActive: true, boxShadow: "0 0", preventTreeViewOpen: true, lockedPosition: true, backgroundColor: "#eeeeee"
         });
 
         return Docs.Create.ButtonDocument({
-            width: 50, height: 25, backgroundColor: "lightgrey", color: "rgb(34, 34, 34)", title: "Library", fontSize: 10,
+            _width: 50, _height: 25, backgroundColor: "lightgrey", color: "rgb(34, 34, 34)", title: "Library", fontSize: 10,
             letterSpacing: "0px", textTransform: "unset", borderRounding: "5px 5px 0px 0px", boxShadow: "3px 3px 0px rgb(34, 34, 34)",
             sourcePanel: Docs.Create.TreeDocument([doc.workspaces as Doc, doc.documents as Doc, doc.recentlyClosed as Doc], {
-                title: "Library", xMargin: 5, yMargin: 5, gridGap: 5, forceActive: true, dropAction: "alias", lockedPosition: true, boxShadow: "0 0",
+                title: "Library", _xMargin: 5, _yMargin: 5, _gridGap: 5, forceActive: true, _dropAction: "alias", lockedPosition: true, boxShadow: "0 0",
             }),
             targetContainer: sidebarContainer,
             onClick: ScriptField.MakeScript("this.targetContainer.proto = this.sourcePanel;")
@@ -194,7 +194,7 @@ export class CurrentUserUtils {
     // setup the Search button which will display the search panel.  
     static setupSearchPanel(sidebarContainer: Doc) {
         return Docs.Create.ButtonDocument({
-            width: 50, height: 25, backgroundColor: "lightgrey", color: "rgb(34, 34, 34)", title: "Search", fontSize: 10,
+            _width: 50, _height: 25, backgroundColor: "lightgrey", color: "rgb(34, 34, 34)", title: "Search", fontSize: 10,
             letterSpacing: "0px", textTransform: "unset", borderRounding: "5px 5px 0px 0px", boxShadow: "3px 3px 0px rgb(34, 34, 34)",
             sourcePanel: Docs.Create.QueryDocument({
                 title: "search stack", ignoreClick: true
@@ -208,7 +208,7 @@ export class CurrentUserUtils {
     // setup the list of sidebar mode buttons which determine what is displayed in the sidebar
     static setupSidebarButtons(doc: Doc) {
         doc.sidebarContainer = new Doc();
-        (doc.sidebarContainer as Doc).chromeStatus = "disabled";
+        (doc.sidebarContainer as Doc)._chromeStatus = "disabled";
         (doc.sidebarContainer as Doc).onClick = ScriptField.MakeScript("freezeSidebar()");
 
         doc.ToolsBtn = this.setupToolsPanel(doc.sidebarContainer as Doc, doc);
@@ -217,21 +217,21 @@ export class CurrentUserUtils {
 
         // Finally, setup the list of buttons to display in the sidebar
         doc.sidebarButtons = Docs.Create.StackingDocument([doc.SearchBtn as Doc, doc.LibraryBtn as Doc, doc.ToolsBtn as Doc], {
-            width: 500, height: 80, boxShadow: "0 0", sectionFilter: "title", hideHeadings: true, ignoreClick: true,
-            backgroundColor: "rgb(100, 100, 100)", chromeStatus: "disabled", title: "library stack",
-            yMargin: 10,
+            _width: 500, _height: 80, boxShadow: "0 0", sectionFilter: "title", hideHeadings: true, ignoreClick: true,
+            backgroundColor: "rgb(100, 100, 100)", _chromeStatus: "disabled", title: "library stack",
+            _yMargin: 10,
         });
     }
 
     /// sets up the default list of buttons to be shown in the expanding button menu at the bottom of the Dash window
     static setupExpandingButtons(doc: Doc) {
         doc.undoBtn = Docs.Create.FontIconDocument(
-            { nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, dropAction: "alias", onClick: ScriptField.MakeScript("undo()"), removeDropProperties: new List<string>(["dropAction"]), title: "undo button", icon: "undo-alt" });
+            { _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100, _dropAction: "alias", onClick: ScriptField.MakeScript("undo()"), removeDropProperties: new List<string>(["dropAction"]), title: "undo button", icon: "undo-alt" });
         doc.redoBtn = Docs.Create.FontIconDocument(
-            { nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, dropAction: "alias", onClick: ScriptField.MakeScript("redo()"), removeDropProperties: new List<string>(["dropAction"]), title: "redo button", icon: "redo-alt" });
+            { _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100, _dropAction: "alias", onClick: ScriptField.MakeScript("redo()"), removeDropProperties: new List<string>(["dropAction"]), title: "redo button", icon: "redo-alt" });
 
         doc.expandingButtons = Docs.Create.LinearDocument([doc.undoBtn as Doc, doc.redoBtn as Doc], {
-            title: "expanding buttons", gridGap: 5, xMargin: 5, yMargin: 5, height: 42, width: 100, boxShadow: "0 0",
+            title: "expanding buttons", _gridGap: 5, _xMargin: 5, _yMargin: 5, _height: 42, _width: 100, boxShadow: "0 0",
             backgroundColor: "black", preventTreeViewOpen: true, forceActive: true, lockedPosition: true,
             dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name })
         });
@@ -240,7 +240,7 @@ export class CurrentUserUtils {
     // sets up the default set of documents to be shown in the Overlay layer
     static setupOverlays(doc: Doc) {
         doc.overlays = Docs.Create.FreeformDocument([], { title: "Overlays", backgroundColor: "#aca3a6" });
-        doc.linkFollowBox = Docs.Create.LinkFollowBoxDocument({ x: 250, y: 20, width: 500, height: 370, title: "Link Follower" });
+        doc.linkFollowBox = Docs.Create.LinkFollowBoxDocument({ x: 250, y: 20, _width: 500, _height: 370, title: "Link Follower" });
         Doc.AddDocToList(doc.overlays as Doc, "data", doc.linkFollowBox as Doc);
     }
 
-- 
cgit v1.2.3-70-g09d2


From c5d618538d4fd9669476dc7eb66ddd45783e5fa6 Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Mon, 27 Jan 2020 05:50:15 -0500
Subject: nativewidth and nativeheight extension key fix and implemented UI to
 manually download a remotely hosted image

---
 .../util/Import & Export/DirectoryImportBox.tsx    |  4 ++--
 src/client/util/Import & Export/ImageUtils.ts      |  4 ++--
 src/client/views/DocComponent.tsx                  |  2 +-
 src/client/views/collections/CollectionSubView.tsx |  2 +-
 src/client/views/nodes/AudioBox.tsx                |  2 +-
 src/client/views/nodes/ImageBox.scss               | 24 ++++++++++++++-----
 src/client/views/nodes/ImageBox.tsx                | 28 +++++++++++++++++++++-
 src/mobile/ImageUpload.tsx                         |  2 +-
 src/server/ApiManagers/UploadManager.ts            | 14 ++++++++++-
 src/server/DashUploadUtils.ts                      | 12 +++-------
 10 files changed, 69 insertions(+), 25 deletions(-)

(limited to 'src/client/util/Import & Export/DirectoryImportBox.tsx')

diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx
index c7c94abed..071015193 100644
--- a/src/client/util/Import & Export/DirectoryImportBox.tsx	
+++ b/src/client/util/Import & Export/DirectoryImportBox.tsx	
@@ -116,13 +116,13 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
                 formData.append(Utils.GenerateGuid(), file);
             });
 
-            collector.push(...(await Networking.PostFormDataToServer("/upload", formData)));
+            collector.push(...(await Networking.PostFormDataToServer("/uploadFormData", formData)));
             runInAction(() => this.completed += batch.length);
         });
 
         await Promise.all(uploads.map(async ({ name, type, clientAccessPath, exifData }) => {
             const path = Utils.prepend(clientAccessPath);
-            const document = await Docs.Get.DocumentFromType(type, path, { width: 300, title: name });
+            const document = await Docs.Get.DocumentFromType(type, path, { _width: 300, title: name });
             const { data, error } = exifData;
             if (document) {
                 Doc.GetProto(document).exif = error || Docs.Get.DocumentHierarchyFromJson(data);
diff --git a/src/client/util/Import & Export/ImageUtils.ts b/src/client/util/Import & Export/ImageUtils.ts
index 2f4db0e17..ff909cc6b 100644
--- a/src/client/util/Import & Export/ImageUtils.ts	
+++ b/src/client/util/Import & Export/ImageUtils.ts	
@@ -22,8 +22,8 @@ export namespace ImageUtils {
         } = await Networking.PostToServer("/inspectImage", { source });
         document.exif = error || Docs.Get.DocumentHierarchyFromJson(data);
         const proto = Doc.GetProto(document);
-        proto.nativeWidth = nativeWidth;
-        proto.nativeHeight = nativeHeight;
+        proto["data-nativeWidth"] = nativeWidth;
+        proto["data-nativeHeight"] = nativeHeight;
         proto.contentSize = contentSize;
         return data !== undefined;
     };
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index d98301cf6..6f3eb6808 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -61,7 +61,7 @@ export function DocAnnotatableComponent<P extends DocAnnotatableProps, T>(schema
 
         _annotationKey: string = "annotations";
         public set annotationKey(val: string) { this._annotationKey = val; }
-        public get annotationKey() { return this._annotationKey }
+        public get annotationKey() { return this._annotationKey; }
 
         @action.bound
         removeDocument(doc: Doc): boolean {
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index b96b86929..80fcc33aa 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -324,7 +324,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
 
                     formData.append('file', file);
                     const dropFileName = file ? file.name : "-empty-";
-                    promises.push(Networking.PostFormDataToServer("/upload", formData).then(results => {
+                    promises.push(Networking.PostFormDataToServer("/uploadFormData", formData).then(results => {
                         results.map(action((result: any) => {
                             const { clientAccessPath, nativeWidth, nativeHeight, contentSize } = result;
                             const full = { ...options, _width: 300, title: dropFileName };
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 8ede79edc..62a479b2a 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -138,7 +138,7 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
             self._recorder.ondataavailable = async function (e: any) {
                 const formData = new FormData();
                 formData.append("file", e.data);
-                const res = await fetch(Utils.prepend("/upload"), {
+                const res = await fetch(Utils.prepend("/uploadFormData"), {
                     method: 'POST',
                     body: formData
                 });
diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss
index cf5d999a7..43f4a0ba9 100644
--- a/src/client/views/nodes/ImageBox.scss
+++ b/src/client/views/nodes/ImageBox.scss
@@ -1,10 +1,12 @@
-.imageBox, .imageBox-dragging{
+.imageBox,
+.imageBox-dragging {
     pointer-events: all;
     border-radius: inherit;
-    width:100%;
-    height:100%;
+    width: 100%;
+    height: 100%;
     position: absolute;
     transform-origin: top left;
+
     .imageBox-fader {
         pointer-events: all;
     }
@@ -16,6 +18,14 @@
     }
 }
 
+#upload-icon {
+    position: absolute;
+    bottom: 0;
+    right: 0;
+    width: 20px;
+    height: 20px;
+}
+
 .imageBox-cont {
     padding: 0vw;
     position: absolute;
@@ -25,7 +35,8 @@
     max-width: 100%;
     max-height: 100%;
     pointer-events: none;
-    background:transparent;
+    background: transparent;
+
     img {
         height: auto;
         width: 100%;
@@ -55,9 +66,10 @@
     padding: 3px;
     background: white;
     cursor: pointer;
-    opacity:0.15;
+    opacity: 0.15;
 }
-#google-photos:hover{
+
+#google-photos:hover {
     opacity: 1;
 }
 
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 8db66d367..3d15bcb15 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -29,6 +29,7 @@ import { TraceMobx } from '../../../new_fields/util';
 import { SelectionManager } from '../../util/SelectionManager';
 import { cache } from 'sharp';
 import { ObjectField } from '../../../new_fields/ObjectField';
+import { Networking } from '../../Network';
 const requestImageSize = require('../../util/request-image-size');
 const path = require('path');
 const { Howl } = require('howler');
@@ -104,7 +105,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
             recorder.ondataavailable = async function (e: any) {
                 const formData = new FormData();
                 formData.append("file", e.data);
-                const res = await fetch(Utils.prepend("/upload"), {
+                const res = await fetch(Utils.prepend("/uploadFormData"), {
                     method: 'POST',
                     body: formData
                 });
@@ -287,6 +288,30 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
         return !tags ? (null) : (<img id={"google-tags"} src={"/assets/google_tags.png"} />);
     }
 
+    @computed
+    private get considerDownloadIcon() {
+        const data = this.dataDoc[this.props.fieldKey];
+        if (!(data instanceof ImageField)) {
+            return (null);
+        }
+        const primary = data.url.href;
+        if (primary.includes(window.location.origin)) {
+            return (null);
+        }
+        return (
+            <img
+                id={"upload-icon"}
+                src={"/assets/downarrow.png"}
+                onClick={async () => {
+                    const { dataDoc } = this;
+                    const [{ clientAccessPath }] = await Networking.PostToServer("/uploadRemoteImage", { sources: [primary] });
+                    dataDoc.originalUrl = primary;
+                    dataDoc[this.props.fieldKey] = new ImageField(Utils.prepend(clientAccessPath));
+                }}
+            />
+        );
+    }
+
     @computed get nativeSize() {
         const pw = typeof this.props.PanelWidth === "function" ? this.props.PanelWidth() : typeof this.props.PanelWidth === "number" ? (this.props.PanelWidth as any) as number : 50;
         const nativeWidth = NumCast(this.dataDoc[this.props.fieldKey + "-nativeWidth"], pw);
@@ -347,6 +372,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
                     style={{ color: [DocListCast(this.dataDoc[this.props.fieldKey + "-audioAnnotations"]).length ? "blue" : "gray", "green", "red"][this._audioState] }}
                     icon={!DocListCast(this.dataDoc[this.props.fieldKey + "-audioAnnotations"]).length ? "microphone" : faFileAudio} size="sm" />
             </div>
+            {this.considerDownloadIcon}
             {this.considerGooglePhotosLink()}
             <FaceRectangles document={this.dataDoc} color={"#0000FF"} backgroundColor={"#0000FF"} />
         </div>;
diff --git a/src/mobile/ImageUpload.tsx b/src/mobile/ImageUpload.tsx
index dab4de110..1583e3d5d 100644
--- a/src/mobile/ImageUpload.tsx
+++ b/src/mobile/ImageUpload.tsx
@@ -45,7 +45,7 @@ class Uploader extends React.Component {
                     const formData = new FormData();
                     formData.append("file", files[0]);
 
-                    const upload = window.location.origin + "/upload";
+                    const upload = window.location.origin + "/uploadFormData";
                     this.status = "uploading image";
                     const res = await fetch(upload, {
                         method: 'POST',
diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts
index 583eaa59b..a92b613b7 100644
--- a/src/server/ApiManagers/UploadManager.ts
+++ b/src/server/ApiManagers/UploadManager.ts
@@ -40,7 +40,7 @@ export default class UploadManager extends ApiManager {
 
         register({
             method: Method.POST,
-            subscription: "/upload",
+            subscription: "/uploadFormData",
             secureHandler: async ({ req, res }) => {
                 const form = new formidable.IncomingForm();
                 form.uploadDir = pathToDirectory(Directory.parsed_files);
@@ -59,6 +59,18 @@ export default class UploadManager extends ApiManager {
             }
         });
 
+        register({
+            method: Method.POST,
+            subscription: "/uploadRemoteImage",
+            secureHandler: async ({ req, res }) => {
+                const { sources } = req.body;
+                if (Array.isArray(sources)) {
+                    return res.send(await Promise.all(sources.map(url => DashUploadUtils.UploadImage(url))));
+                }
+                res.send();
+            }
+        });
+
         register({
             method: Method.POST,
             subscription: "/uploadDoc",
diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts
index b826b4882..cb7104757 100644
--- a/src/server/DashUploadUtils.ts
+++ b/src/server/DashUploadUtils.ts
@@ -108,13 +108,7 @@ export namespace DashUploadUtils {
         return MoveParsedFile(absolutePath, Directory.pdfs);
     }
 
-    const generate = (prefix: string, url: string) => `${prefix}upload_${Utils.GenerateGuid()}${sanitizeExtension(url)}`;
-    const sanitizeExtension = (source: string) => {
-        let extension = path.extname(source);
-        extension = extension.toLowerCase();
-        extension = extension.split("?")[0];
-        return extension;
-    };
+    const generate = (prefix: string, extension: string) => `${prefix}upload_${Utils.GenerateGuid()}.${extension}`;
 
     /**
      * Uploads an image specified by the @param source to Dash's /public/files/
@@ -209,8 +203,8 @@ export namespace DashUploadUtils {
 
     export const UploadInspectedImage = async (metadata: InspectionResults, filename?: string, format?: string, prefix = ""): Promise<ImageUploadInformation> => {
         const { requestable, source, ...remaining } = metadata;
-        const resolved = filename || generate(prefix, requestable);
-        const extension = format || sanitizeExtension(requestable || resolved);
+        const extension = remaining.contentType.toLowerCase().split("/")[1]; //format || sanitizeExtension(requestable || resolved);
+        const resolved = filename || generate(prefix, extension);
         const information: ImageUploadInformation = {
             clientAccessPath: clientPathToFile(Directory.images, resolved),
             serverAccessPaths: {},
-- 
cgit v1.2.3-70-g09d2

