From c7078b86bd966d1f5208f4ea08c405e47d060fb6 Mon Sep 17 00:00:00 2001 From: Tyler Schicke Date: Wed, 10 Jul 2019 16:17:41 -0400 Subject: Added stuff to check for empty object sent to server --- src/client/util/SerializationHelper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/client/util') diff --git a/src/client/util/SerializationHelper.ts b/src/client/util/SerializationHelper.ts index a7246d7c4..17ae407c4 100644 --- a/src/client/util/SerializationHelper.ts +++ b/src/client/util/SerializationHelper.ts @@ -9,7 +9,7 @@ export namespace SerializationHelper { export function Serialize(obj: Field): any { if (obj === undefined || obj === null) { - return undefined; + return null; } if (typeof obj !== 'object') { -- cgit v1.2.3-70-g09d2 From b3964f3be3ec6208b7a9ccf0a26fb5fea861c29b Mon Sep 17 00:00:00 2001 From: Tyler Schicke 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/util') 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 From a56eb6c5dde8a7015932faec094cbe83bf0a825b Mon Sep 17 00:00:00 2001 From: Bob Zeleznik Date: Wed, 10 Jul 2019 21:18:44 -0400 Subject: added lastmodified time to text data extension doc. now need to figure out how to get at it. --- src/client/util/DocumentManager.ts | 2 +- .../views/collections/CollectionTreeView.tsx | 23 ++++++++-------- src/client/views/nodes/DocumentView.tsx | 4 +-- src/client/views/nodes/FormattedTextBox.tsx | 32 ++++++++++++++-------- .../views/presentationview/PresentationElement.tsx | 4 +-- src/client/views/search/SearchItem.tsx | 6 ++-- src/new_fields/Types.ts | 4 +++ 7 files changed, 45 insertions(+), 30 deletions(-) (limited to 'src/client/util') diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index acd8dcef7..8fa460bca 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -106,7 +106,7 @@ export class DocumentManager { @computed public get LinkedDocumentViews() { - let pairs = DocumentManager.Instance.DocumentViews.filter(dv => dv.isSelected() || BoolCast(dv.props.Document.libraryBrush, false)).reduce((pairs, dv) => { + let pairs = DocumentManager.Instance.DocumentViews.filter(dv => dv.isSelected() || BoolCast(dv.props.Document.libraryBrush)).reduce((pairs, dv) => { let linksList = LinkManager.Instance.getAllRelatedLinks(dv.props.Document); pairs.push(...linksList.reduce((pairs, link) => { if (link) { diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index d7725f444..d90c78f85 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -1,7 +1,7 @@ import { library } from '@fortawesome/fontawesome-svg-core'; import { faAngleRight, faCamera, faExpand, faTrash, faBell, faCaretDown, faCaretRight, faCaretSquareDown, faCaretSquareRight, faTrashAlt } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, computed, observable, trace } from "mobx"; +import { action, computed, observable, trace, untracked } from "mobx"; import { observer } from "mobx-react"; import { Doc, DocListCast, HeightSym, WidthSym, Opt } from '../../../new_fields/Doc'; import { Id } from '../../../new_fields/FieldSymbols'; @@ -73,9 +73,11 @@ class TreeView extends React.Component { @observable _collapsed: boolean = true; @computed get fieldKey() { - let keys = Array.from(Object.keys(this.resolvedDataDoc)); + trace(); + let keys = Array.from(Object.keys(this.resolvedDataDoc)); // bcz: Argh -- make untracked to avoid this rerunning whenever 'libraryBrush' is set if (this.resolvedDataDoc.proto instanceof Doc) { - keys.push(...Array.from(Object.keys(this.resolvedDataDoc.proto))); + let arr = Array.from(Object.keys(this.resolvedDataDoc.proto!));// bcz: Argh -- make untracked to avoid this rerunning whenever 'libraryBrush' is set + keys.push(...arr); while (keys.indexOf("proto") !== -1) keys.splice(keys.indexOf("proto"), 1); } let keyList: string[] = []; @@ -113,12 +115,12 @@ class TreeView extends React.Component { } } onPointerLeave = (e: React.PointerEvent): void => { - this.props.document.libraryBrush = false; + this.props.document.libraryBrush = undefined; this._header!.current!.className = "treeViewItem-header"; document.removeEventListener("pointermove", this.onDragMove, true); } onDragMove = (e: PointerEvent): void => { - this.props.document.libraryBrush = false; + this.props.document.libraryBrush = undefined; 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); @@ -203,7 +205,7 @@ class TreeView extends React.Component { let onItemDown = SetupDrag(reference, () => this.resolvedDataDoc, this.move, this.props.dropAction, this.props.treeViewId, true); let headerElements = ( - { let ind = this.keyList.indexOf(this._chosenKey); ind = (ind + 1) % this.keyList.length; @@ -219,8 +221,8 @@ class TreeView extends React.Component { return <>
@@ -354,7 +356,7 @@ class TreeView extends React.Component { ; } else { let layoutDoc = Doc.expandTemplateLayout(this.props.document, this.props.dataDoc); - contentElement =
+ contentElement =
{ dataDoc={dataDoc} containingCollection={containingCollection} treeViewId={treeViewId} - key={child[Id]} + key={child[Id] + "child " + i} indentDocument={indent} renderDepth={renderDepth} deleteDoc={remove} @@ -526,7 +528,6 @@ export class CollectionTreeView extends CollectionSubView(Document) { 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); let moveDoc = (d: Doc, target: Doc, addDoc: (doc: Doc) => boolean) => this.props.moveDocument(d, target, addDoc); - return !this.childDocs ? (null) : (
(Docu fullScreenAlias.templates = new List(); this.props.addDocTab(fullScreenAlias, this.dataDoc, "inTab"); SelectionManager.DeselectAll(); - this.props.Document.libraryBrush = false; + this.props.Document.libraryBrush = undefined; } else if (CurrentUserUtils.MainDocId !== this.props.Document[Id] && (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && @@ -575,7 +575,7 @@ export class DocumentView extends DocComponent(Docu } onPointerEnter = (e: React.PointerEvent): void => { this.props.Document.libraryBrush = true; }; - onPointerLeave = (e: React.PointerEvent): void => { this.props.Document.libraryBrush = false; }; + onPointerLeave = (e: React.PointerEvent): void => { this.props.Document.libraryBrush = undefined; }; isSelected = () => SelectionManager.IsSelected(this); @action select = (ctrlPressed: boolean) => { SelectionManager.SelectDoc(this, ctrlPressed); }; diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index d84ad0918..5475aa2b2 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -9,11 +9,11 @@ import { NodeType } from 'prosemirror-model'; import { EditorState, Plugin, Transaction } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; import { Doc, Opt } from "../../../new_fields/Doc"; -import { Id } from '../../../new_fields/FieldSymbols'; +import { Id, Copy } from '../../../new_fields/FieldSymbols'; import { List } from '../../../new_fields/List'; import { RichTextField } from "../../../new_fields/RichTextField"; import { createSchema, listSpec, makeInterface } from "../../../new_fields/Schema"; -import { BoolCast, Cast, NumCast, StrCast } from "../../../new_fields/Types"; +import { BoolCast, Cast, NumCast, StrCast, DateCast } from "../../../new_fields/Types"; import { DocServer } from "../../DocServer"; import { Docs } from '../../documents/Documents'; import { DocumentManager } from '../../util/DocumentManager'; @@ -33,6 +33,8 @@ import { Templates } from '../Templates'; import { FieldView, FieldViewProps } from "./FieldView"; import "./FormattedTextBox.scss"; import React = require("react"); +import { DateField } from '../../../new_fields/DateField'; +import { thisExpression } from 'babel-types'; library.add(faEdit); library.add(faSmile); @@ -68,6 +70,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe private _applyingChange: boolean = false; private _linkClicked = ""; private _reactionDisposer: Opt; + private _textReactionDisposer: Opt; private _proxyReactionDisposer: Opt; private dropDisposer?: DragManager.DragDropDisposer; public get CurrentDiv(): HTMLDivElement { return this._ref.current!; } @@ -131,6 +134,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe this._editorView.updateState(state); this._applyingChange = true; if (this.extensionDoc) this.extensionDoc.text = state.doc.textBetween(0, state.doc.content.size, "\n\n"); + if (this.extensionDoc) this.extensionDoc.lastModified = new DateField(new Date(Date.now())); this.dataDoc[this.props.fieldKey] = new RichTextField(JSON.stringify(state.toJSON())); this._applyingChange = false; let title = StrCast(this.dataDoc.title); @@ -233,6 +237,17 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe this._editorView.updateState(EditorState.fromJSON(config, JSON.parse(field2))); } ); + + this._textReactionDisposer = reaction( + () => this.extensionDoc, + () => { + if (this.dataDoc.text || this.dataDoc.lastModified) { + this.extensionDoc.text = this.dataDoc.text; + this.extensionDoc.lastModified = DateCast(this.dataDoc.lastModified)[Copy](); + this.dataDoc.text = undefined; + this.dataDoc.lastModified = undefined; + } + }, { fireImmediately: true }); this.setupEditor(config, this.dataDoc, this.props.fieldKey); } @@ -271,15 +286,10 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } componentWillUnmount() { - if (this._editorView) { - this._editorView.destroy(); - } - if (this._reactionDisposer) { - this._reactionDisposer(); - } - if (this._proxyReactionDisposer) { - this._proxyReactionDisposer(); - } + this._editorView && this._editorView.destroy(); + this._reactionDisposer && this._reactionDisposer(); + this._proxyReactionDisposer && this._proxyReactionDisposer(); + this._textReactionDisposer && this._textReactionDisposer(); } onPointerDown = (e: React.PointerEvent): void => { diff --git a/src/client/views/presentationview/PresentationElement.tsx b/src/client/views/presentationview/PresentationElement.tsx index 6896ee452..a16d7bc76 100644 --- a/src/client/views/presentationview/PresentationElement.tsx +++ b/src/client/views/presentationview/PresentationElement.tsx @@ -370,14 +370,14 @@ export default class PresentationElement extends React.Component { p.document.libraryBrush = true; }; - let onLeave = (e: React.PointerEvent) => { p.document.libraryBrush = false; }; + let onLeave = (e: React.PointerEvent) => { p.document.libraryBrush = undefined; }; return (
{ p.gotoDocument(p.index, NumCast(this.props.mainDocument.selectedDoc)); e.stopPropagation(); }}> diff --git a/src/client/views/search/SearchItem.tsx b/src/client/views/search/SearchItem.tsx index 804ffa7f5..87cae5487 100644 --- a/src/client/views/search/SearchItem.tsx +++ b/src/client/views/search/SearchItem.tsx @@ -206,13 +206,13 @@ export class SearchItem extends React.Component { let doc1 = Cast(this.props.doc.anchor1, Doc, null); let doc2 = Cast(this.props.doc.anchor2, Doc, null); - doc1 && (doc1.libraryBrush = false); - doc2 && (doc2.libraryBrush = false); + doc1 && (doc1.libraryBrush = undefined); + doc2 && (doc2.libraryBrush = undefined); } } else { let docViews: DocumentView[] = DocumentManager.Instance.getAllDocumentViews(this.props.doc); docViews.forEach(element => { - element.props.Document.libraryBrush = false; + element.props.Document.libraryBrush = undefined; }); } } diff --git a/src/new_fields/Types.ts b/src/new_fields/Types.ts index 39d384d64..f8a4a30b4 100644 --- a/src/new_fields/Types.ts +++ b/src/new_fields/Types.ts @@ -1,6 +1,7 @@ import { Field, Opt, FieldResult, Doc } from "./Doc"; import { List } from "./List"; import { RefField } from "./RefField"; +import { DateField } from "./DateField"; export type ToType = T extends "string" ? string : @@ -80,6 +81,9 @@ export function StrCast(field: FieldResult, defaultVal: string | null = "") { export function BoolCast(field: FieldResult, defaultVal: boolean | null = null) { return Cast(field, "boolean", defaultVal); } +export function DateCast(field: FieldResult) { + return Cast(field, DateField, null); +} type WithoutList = T extends List ? (R extends RefField ? (R | Promise)[] : R[]) : T; -- cgit v1.2.3-70-g09d2