From 982dbab4306008e30cf74eea1d3155d39912a513 Mon Sep 17 00:00:00 2001
From: Tyler Schicke <tyler_schicke@brown.edu>
Date: Wed, 10 Jul 2019 11:54:49 -0400
Subject: Added readonly mode to documents

---
 src/client/views/MainView.tsx | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

(limited to 'src/client/views/MainView.tsx')

diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index da126fb74..204503d7f 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -13,7 +13,7 @@ import { Id } from '../../new_fields/FieldSymbols';
 import { InkTool } from '../../new_fields/InkField';
 import { List } from '../../new_fields/List';
 import { listSpec } from '../../new_fields/Schema';
-import { Cast, FieldValue, NumCast } from '../../new_fields/Types';
+import { Cast, FieldValue, NumCast, BoolCast } from '../../new_fields/Types';
 import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils';
 import { RouteStore } from '../../server/RouteStore';
 import { emptyFunction, returnOne, returnTrue } from '../../Utils';
@@ -194,6 +194,11 @@ export class MainView extends React.Component {
     openWorkspace = async (doc: Doc, fromHistory = false) => {
         CurrentUserUtils.MainDocId = doc[Id];
         this.mainContainer = doc;
+        if (BoolCast(doc.readOnly)) {
+            DocServer.makeReadOnly();
+        } else {
+            DocServer.makeEditable();
+        }
         fromHistory || HistoryUtil.pushState({ type: "doc", docId: doc[Id], initializers: {} });
         const col = await Cast(CurrentUserUtils.UserDocument.optionalRightCollection, Doc);
         // if there is a pending doc, and it has new data, show it (syip: we use a timeout to prevent collection docking view from being uninitialized)
-- 
cgit v1.2.3-70-g09d2


From b3964f3be3ec6208b7a9ccf0a26fb5fea861c29b Mon Sep 17 00:00:00 2001
From: Tyler Schicke <tyler_schicke@brown.edu>
Date: Wed, 10 Jul 2019 20:27:25 -0400
Subject: Refactored how History stuff works to make readonly work better

---
 package.json                  |   1 +
 src/client/util/History.ts    | 136 ++++++++++++++++++++++++++++++++----------
 src/client/views/MainView.tsx |  22 +++----
 3 files changed, 118 insertions(+), 41 deletions(-)

(limited to 'src/client/views/MainView.tsx')

diff --git a/package.json b/package.json
index 51d1bab5d..85fc7f1be 100644
--- a/package.json
+++ b/package.json
@@ -164,6 +164,7 @@
         "prosemirror-transform": "^1.1.3",
         "prosemirror-view": "^1.8.3",
         "pug": "^2.0.3",
+        "query-string": "^6.8.1",
         "raw-loader": "^1.0.0",
         "react": "^16.8.4",
         "react-anime": "^2.2.0",
diff --git a/src/client/util/History.ts b/src/client/util/History.ts
index 545ea8629..1a807b581 100644
--- a/src/client/util/History.ts
+++ b/src/client/util/History.ts
@@ -2,6 +2,8 @@ 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";
 
 export namespace HistoryUtil {
     export interface DocInitializerList {
@@ -11,9 +13,11 @@ export namespace HistoryUtil {
     export interface DocUrl {
         type: "doc";
         docId: string;
-        initializers: {
+        initializers?: {
             [docId: string]: DocInitializerList;
         };
+        readonly?: boolean;
+        nro?: boolean;
     }
 
     export type ParsedUrl = DocUrl;
@@ -21,7 +25,7 @@ export namespace HistoryUtil {
     // const handlers: ((state: ParsedUrl | null) => void)[] = [];
     function onHistory(e: PopStateEvent) {
         if (window.location.pathname !== RouteStore.home) {
-            const url = e.state as ParsedUrl || parseUrl(window.location.pathname);
+            const url = e.state as ParsedUrl || parseUrl(window.location);
             if (url) {
                 switch (url.type) {
                     case "doc":
@@ -62,42 +66,111 @@ export namespace HistoryUtil {
     //     }
     // }
 
-    export function parseUrl(pathname: string): ParsedUrl | undefined {
-        let pathnameSplit = pathname.split("/");
-        if (pathnameSplit.length !== 2) {
-            return undefined;
+    const parsers: { [type: string]: (pathname: string[], opts: qs.ParsedQuery) => ParsedUrl | undefined } = {};
+    const stringifiers: { [type: string]: (state: ParsedUrl) => string } = {};
+
+    type ParserValue = true | "none" | "json" | ((value: string) => any);
+
+    type Parser = {
+        [key: string]: ParserValue
+    };
+
+    function addParser(type: string, requiredFields: Parser, optionalFields: Parser, customParser?: (pathname: string[], opts: qs.ParsedQuery, current: ParsedUrl) => ParsedUrl | null | undefined) {
+        function parse(parser: ParserValue, value: string | string[] | null | undefined) {
+            if (value === undefined || value === null) {
+                return value;
+            }
+            if (Array.isArray(value)) {
+            } else if (parser === true || parser === "json") {
+                value = JSON.parse(value);
+            } else if (parser === "none") {
+            } else {
+                value = parser(value);
+            }
+            return value;
         }
-        const type = pathnameSplit[0];
-        const data = pathnameSplit[1];
+        parsers[type] = (pathname, opts) => {
+            const current: any = { type };
+            for (const required in requiredFields) {
+                if (!(required in opts)) {
+                    return undefined;
+                }
+                const parser = requiredFields[required];
+                let value = opts[required];
+                value = parse(parser, value);
+                if (value !== null && value !== undefined) {
+                    current[required] = value;
+                }
+            }
+            for (const opt in optionalFields) {
+                if (!(opt in opts)) {
+                    continue;
+                }
+                const parser = optionalFields[opt];
+                let value = opts[opt];
+                value = parse(parser, value);
+                if (value !== undefined) {
+                    current[opt] = value;
+                }
+            }
+            if (customParser) {
+                const val = customParser(pathname, opts, current);
+                if (val === null) {
+                    return undefined;
+                } else if (val === undefined) {
+                    return current;
+                } else {
+                    return val;
+                }
+            }
+            return current;
+        };
+    }
 
-        if (type === "doc") {
-            const s = data.split("?");
-            if (s.length < 1 || s.length > 2) {
-                return undefined;
+    function addStringifier(type: string, keys: string[], customStringifier?: (state: ParsedUrl, current: string) => string) {
+        stringifiers[type] = state => {
+            let path = DocServer.prepend(`/${type}`);
+            if (customStringifier) {
+                path = customStringifier(state, path);
             }
-            const docId = s[0];
-            const initializers = s.length === 2 ? JSON.parse(decodeURIComponent(s[1])) : {};
-            return {
-                type: "doc",
-                docId,
-                initializers
-            };
+            const queryObj = OmitKeys(state, keys).extract;
+            const query: any = {};
+            Object.keys(queryObj).forEach(key => query[key] = queryObj[key] === null ? null : queryObj[key]);
+            const queryString = qs.stringify(query);
+            return path + (queryString ? `?${queryString}` : "");
+        };
+    }
+
+    addParser("doc", {}, { readonly: true, initializers: true, nro: true }, (pathname, opts, current) => {
+        if (pathname.length !== 2) return undefined;
+
+        current.initializers = current.initializers || {};
+        const docId = pathname[1];
+        current.docId = docId;
+    });
+    addStringifier("doc", ["initializers", "readonly", "nro"], (state, current) => {
+        return `${current}/${state.docId}`;
+    });
+
+
+    export function parseUrl(location: Location | URL): ParsedUrl | undefined {
+        const pathname = location.pathname.substring(1);
+        const search = location.search;
+        const opts = qs.parse(search, { sort: false });
+        let pathnameSplit = pathname.split("/");
+
+        const type = pathnameSplit[0];
+
+        if (type in parsers) {
+            return parsers[type](pathnameSplit, opts);
         }
 
         return undefined;
     }
 
     export function createUrl(params: ParsedUrl): string {
-        let baseUrl = DocServer.prepend(`/${params.type}`);
-        switch (params.type) {
-            case "doc":
-                const initializers = encodeURIComponent(JSON.stringify(params.initializers));
-                const id = params.docId;
-                let url = baseUrl + `/${id}`;
-                if (Object.keys(params.initializers).length) {
-                    url += `?${initializers}`;
-                }
-                return url;
+        if (params.type in stringifiers) {
+            return stringifiers[params.type](params);
         }
         return "";
     }
@@ -112,7 +185,10 @@ export namespace HistoryUtil {
 
     async function onDocUrl(url: DocUrl) {
         const field = await DocServer.GetRefField(url.docId);
-        await Promise.all(Object.keys(url.initializers).map(id => initDoc(id, url.initializers[id])));
+        const init = url.initializers;
+        if (init) {
+            await Promise.all(Object.keys(init).map(id => initDoc(id, init[id])));
+        }
         if (field instanceof Doc) {
             MainView.Instance.openWorkspace(field, true);
         }
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index dd07fdc3a..bf4ee84d6 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -93,15 +93,6 @@ export class MainView extends React.Component {
         MainView.Instance = this;
         // causes errors to be generated when modifying an observable outside of an action
         configure({ enforceActions: "observed" });
-        if (window.location.search.includes("readonly")) {
-            DocServer.makeReadOnly();
-        }
-        if (window.location.search.includes("safe")) {
-            if (!window.location.search.includes("nro")) {
-                DocServer.makeReadOnly();
-            }
-            CollectionBaseView.SetSafeMode(true);
-        }
         if (window.location.pathname !== RouteStore.home) {
             let pathname = window.location.pathname.substr(1).split("/");
             if (pathname.length > 1) {
@@ -194,12 +185,21 @@ export class MainView extends React.Component {
     openWorkspace = async (doc: Doc, fromHistory = false) => {
         CurrentUserUtils.MainDocId = doc[Id];
         this.mainContainer = doc;
-        if (BoolCast(doc.readOnly)) {
+        const state = HistoryUtil.parseUrl(window.location) || {} as any;
+        fromHistory || HistoryUtil.pushState({ type: "doc", docId: doc[Id], readonly: state.readonly, nro: state.nro });
+        if (state.readonly === true || state.readonly === null) {
+            DocServer.makeReadOnly();
+        } else if (state.safe) {
+            if (!state.nro) {
+                DocServer.makeReadOnly();
+            }
+            CollectionBaseView.SetSafeMode(true);
+        } else if (state.nro || state.nro === null || state.readonly === false) {
+        } else if (BoolCast(doc.readOnly)) {
             DocServer.makeReadOnly();
         } else {
             DocServer.makeEditable();
         }
-        fromHistory || HistoryUtil.pushState({ type: "doc", docId: doc[Id], initializers: {} });
         const col = await Cast(CurrentUserUtils.UserDocument.optionalRightCollection, Doc);
         // if there is a pending doc, and it has new data, show it (syip: we use a timeout to prevent collection docking view from being uninitialized)
         setTimeout(async () => {
-- 
cgit v1.2.3-70-g09d2

