diff options
Diffstat (limited to 'src')
24 files changed, 154 insertions, 427 deletions
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index ec550c15a..1096b8e5f 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -884,7 +884,7 @@ export class CurrentUserUtils { (sharedDocs as Doc)["acl-Public"] = Doc.GetProto(sharedDocs as Doc)["acl-Public"] = SharingPermissions.Add; } if (sharedDocs instanceof Doc) { - sharedDocs.userColor = sharedDocs.userColor || "#12121233"; + sharedDocs.userColor = sharedDocs.userColor || "rgb(202, 202, 202)"; } doc.mySharedDocs = new PrefetchProxy(sharedDocs); } @@ -986,9 +986,9 @@ export class CurrentUserUtils { this.setupDockedButtons(doc); // the bottom bar of font icons await this.setupSidebarButtons(doc); // the pop-out left sidebar of tools/panels await this.setupMenuPanel(doc, sharingDocumentId); - doc.globalLinkDatabase = Docs.Prototypes.MainLinkDocument(); doc.globalScriptDatabase = Docs.Prototypes.MainScriptDocument(); doc.globalGroupDatabase = Docs.Prototypes.MainGroupDocument(); + if (!doc.myLinkDatabase) doc.myLinkDatabase = new List([]); setTimeout(() => this.setupDefaultPresentation(doc), 0); // presentation that's initially triggered diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index 2ca29cb7e..4becdf4a3 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -151,7 +151,7 @@ export class DocumentManager { }; const docView = getFirstDocView(targetDoc, originatingDoc); let annotatedDoc = await Cast(targetDoc.annotationOn, Doc); - if (annotatedDoc && !targetDoc?.isPushpin) { + if (annotatedDoc && annotatedDoc !== originatingDoc?.context && !targetDoc?.isPushpin) { const first = getFirstDocView(annotatedDoc); if (first) { annotatedDoc = first.props.Document; @@ -161,11 +161,13 @@ export class DocumentManager { if (docView) { // we have a docView already and aren't forced to create a new one ... just focus on the document. TODO move into view if necessary otherwise just highlight? if (originatingDoc?.isPushpin) { docView.props.Document.hidden = !docView.props.Document.hidden; + finished?.(); } else { docView.select(false); docView.props.Document.hidden && (docView.props.Document.hidden = undefined); - docView.props.focus(docView.props.Document, willZoom, undefined, focusAndFinish); + // @ts-ignore + docView.props.focus(docView.props.Document, willZoom, undefined, focusAndFinish, annotatedDoc && annotatedDoc === originatingDoc?.context); highlight(); } } else { @@ -193,11 +195,13 @@ export class DocumentManager { if (retryDocView) { // we found the target in the context retryDocView.props.focus(targetDoc, willZoom, undefined, focusAndFinish); // focus on the target in the context highlight(); - } - if (delay > 2500) { + } else if (delay > 1500) { // we didn't find the target, so it must have moved out of the context. Go back to just creating it. if (closeContextIfNotFound) targetDocContextView.props.removeDocument?.(targetDocContextView.props.Document); - // targetDoc.layout && createViewFunc(Doc.BrushDoc(targetDoc), finished); // create a new view of the target + if (targetDoc.layout) { + Doc.SetInPlace(targetDoc, "annotationOn", undefined, false); + createViewFunc(Doc.BrushDoc(targetDoc), finished); // create a new view of the target + } } else { setTimeout(() => findView(delay + 250), 250); } @@ -214,7 +218,6 @@ export class DocumentManager { public async FollowLink(link: Opt<Doc>, doc: Doc, createViewFunc: CreateViewFunc, zoom = false, currentContext?: Doc, finished?: () => void, traverseBacklink?: boolean) { const linkDocs = link ? [link] : DocListCast(doc.links); - SelectionManager.DeselectAll(); const firstDocs = linkDocs.filter(linkDoc => Doc.AreProtosEqual(linkDoc.anchor1 as Doc, doc) || Doc.AreProtosEqual((linkDoc.anchor1 as Doc).annotationOn as Doc, doc)); // link docs where 'doc' is anchor1 const secondDocs = linkDocs.filter(linkDoc => Doc.AreProtosEqual(linkDoc.anchor2 as Doc, doc) || Doc.AreProtosEqual((linkDoc.anchor2 as Doc).annotationOn as Doc, doc)); // link docs where 'doc' is anchor2 const fwdLinkWithoutTargetView = firstDocs.find(d => DocumentManager.Instance.getDocumentViews(d.anchor2 as Doc).length === 0); diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts index 269de08a1..a2bb16dfc 100644 --- a/src/client/util/LinkManager.ts +++ b/src/client/util/LinkManager.ts @@ -2,7 +2,7 @@ import { Doc, DocListCast, Opt } from "../../fields/Doc"; import { List } from "../../fields/List"; import { listSpec } from "../../fields/Schema"; import { Cast, StrCast } from "../../fields/Types"; -import { CurrentUserUtils } from "./CurrentUserUtils"; +import { SharingManager } from "./SharingManager"; /* * link doc: @@ -33,31 +33,19 @@ export class LinkManager { private constructor() { } - // the linkmanagerdoc stores a list of docs representing all linkdocs in 'allLinks' and a list of strings representing all group types in 'allGroupTypes' - // lists of strings representing the metadata keys for each group type is stored under a key that is the same as the group type - public get LinkManagerDoc(): Doc | undefined { - return Doc.UserDoc().globalLinkDatabase as Doc; - } public getAllLinks(): Doc[] { - const ldoc = LinkManager.Instance.LinkManagerDoc; - return ldoc ? DocListCast(ldoc.data) : []; + const lset = new Set<Doc>(DocListCast(Doc.UserDoc().myLinkDatabase)); + SharingManager.Instance.users.forEach(user => DocListCast(user.sharingDoc.myLinkDatabase).map(lset.add)); + return Array.from(lset); } public addLink(linkDoc: Doc): boolean { - if (LinkManager.Instance.LinkManagerDoc) { - Doc.AddDocToList(LinkManager.Instance.LinkManagerDoc, "data", linkDoc); - return true; - } - return false; + return Doc.AddDocToList(Doc.UserDoc(), "myLinkDatabase", linkDoc); } public deleteLink(linkDoc: Doc): boolean { - if (LinkManager.Instance.LinkManagerDoc && linkDoc instanceof Doc) { - Doc.RemoveDocFromList(LinkManager.Instance.LinkManagerDoc, "data", linkDoc); - return true; - } - return false; + return Doc.RemoveDocFromList(Doc.UserDoc(), "myLinkDatabase", linkDoc); } // finds all links that contain the given anchor @@ -85,49 +73,6 @@ export class LinkManager { related.forEach(linkDoc => LinkManager.Instance.deleteLink(linkDoc)); } - public addGroupType(groupType: string): boolean { - if (LinkManager.Instance.LinkManagerDoc) { - LinkManager.Instance.LinkManagerDoc[groupType] = new List<string>([]); - const groupTypes = LinkManager.Instance.getAllGroupTypes(); - groupTypes.push(groupType); - LinkManager.Instance.LinkManagerDoc.allGroupTypes = new List<string>(groupTypes); - return true; - } - return false; - } - - // removes all group docs from all links with the given group type - public deleteGroupType(groupType: string): boolean { - if (LinkManager.Instance.LinkManagerDoc) { - if (LinkManager.Instance.LinkManagerDoc[groupType]) { - 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; - LinkManager.Instance.getAllLinks().forEach(async linkDoc => { - const anchor1 = await Cast(linkDoc.anchor1, Doc); - const anchor2 = await Cast(linkDoc.anchor2, Doc); - anchor1 && LinkManager.Instance.removeGroupFromAnchor(linkDoc, anchor1, groupType); - anchor2 && LinkManager.Instance.removeGroupFromAnchor(linkDoc, anchor2, groupType); - }); - } - return true; - } else return false; - } - - public getAllGroupTypes(): string[] { - if (LinkManager.Instance.LinkManagerDoc) { - if (LinkManager.Instance.LinkManagerDoc.allGroupTypes) { - return Cast(LinkManager.Instance.LinkManagerDoc.allGroupTypes, listSpec("string"), []); - } else { - LinkManager.Instance.LinkManagerDoc.allGroupTypes = new List<string>([]); - return []; - } - } - return []; - } - // gets the groups associates with an anchor in a link public getAnchorGroups(linkDoc: Doc, anchor?: Doc): Array<Doc> { if (Doc.AreProtosEqual(anchor, Cast(linkDoc.anchor1, Doc, null))) { @@ -164,22 +109,6 @@ export class LinkManager { return anchorGroups; } - // gets a list of strings representing the keys of the metadata associated with the given group type - public getMetadataKeysInGroup(groupType: string): string[] { - if (LinkManager.Instance.LinkManagerDoc) { - return LinkManager.Instance.LinkManagerDoc[groupType] ? Cast(LinkManager.Instance.LinkManagerDoc[groupType], listSpec("string"), []) : []; - } - return []; - } - - public setMetadataKeysForGroup(groupType: string, keys: string[]): boolean { - if (LinkManager.Instance.LinkManagerDoc) { - LinkManager.Instance.LinkManagerDoc[groupType] = new List<string>(keys); - return true; - } - return false; - } - // returns a list of all metadata docs associated with the given group type public getAllMetadataDocsInGroup(groupType: string): Array<Doc> { const md: Doc[] = []; @@ -208,6 +137,8 @@ export class LinkManager { 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, a1.annotationOn as Doc)) return a2; + if (Doc.AreProtosEqual(anchor, a2.annotationOn as Doc)) return a1; if (Doc.AreProtosEqual(anchor, linkDoc)) return linkDoc; } }
\ No newline at end of file diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx index b3fbe418b..98e888538 100644 --- a/src/client/views/DocComponent.tsx +++ b/src/client/views/DocComponent.tsx @@ -123,8 +123,9 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T @action.bound removeDocument(doc: Doc | Doc[]): boolean { const effectiveAcl = GetEffectiveAcl(this.dataDoc); - const docAcl = GetEffectiveAcl(doc); - if (effectiveAcl === AclEdit || effectiveAcl === AclAdmin || docAcl === AclAdmin) { + const indocs = doc instanceof Doc ? [doc] : doc; + const docs = indocs.filter(doc => effectiveAcl === AclEdit || effectiveAcl === AclAdmin || GetEffectiveAcl(doc) === AclAdmin); + if (docs.length) { const docs = doc instanceof Doc ? [doc] : doc; docs.map(doc => doc.isPushpin = doc.annotationOn = undefined); const targetDataDoc = this.dataDoc; diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index c48ba109a..89292a445 100644 --- a/src/client/views/GlobalKeyHandler.ts +++ b/src/client/views/GlobalKeyHandler.ts @@ -223,6 +223,11 @@ export class KeyManager { stopPropagation = false; break; case "a": + if (e.target !== document.body) { + stopPropagation = false; + preventDefault = false; + } + break; case "v": stopPropagation = false; preventDefault = false; diff --git a/src/client/views/MainView.scss b/src/client/views/MainView.scss index 5efe9adad..b608eceb1 100644 --- a/src/client/views/MainView.scss +++ b/src/client/views/MainView.scss @@ -293,7 +293,6 @@ .mainView-libraryFlyout-out, .mainView-libraryFlyout { height: 100%; - width: 100%; position: relative; display: flex; flex-direction: column; diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index cb84409cb..69354020b 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -86,7 +86,7 @@ export class MainView extends React.Component { document.getElementById("root")?.addEventListener("scroll", e => ((ele) => ele.scrollLeft = ele.scrollTop = 0)(document.getElementById("root")!)); new InkStrokeProperties(); this._sidebarContent.proto = undefined; - DocServer.setPlaygroundFields(["dataTransition", "_delayAutoHeight", "_autoHeight", "_showSidebar", "_sidebarWidthPercent", "_width", "_height", "_viewTransition", "_panX", "_panY", "_viewScale", "_scrollY", "_scrollTop", "hidden", "_curPage", "_viewType", "_chromeStatus"]); // can play with these fields on someone else's + DocServer.setPlaygroundFields(["x", "y", "dataTransition", "_delayAutoHeight", "_autoHeight", "_showSidebar", "_sidebarWidthPercent", "_width", "_height", "_viewTransition", "_panX", "_panY", "_viewScale", "_scrollY", "_scrollTop", "hidden", "_curPage", "_viewType", "_chromeStatus"]); // can play with these fields on someone else's DocServer.GetRefField("rtfProto").then(proto => (proto instanceof Doc) && reaction(() => StrCast(proto.BROADCAST_MESSAGE), msg => msg && alert(msg))); diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx index f3241e8d9..a64004c5c 100644 --- a/src/client/views/PropertiesView.tsx +++ b/src/client/views/PropertiesView.tsx @@ -972,7 +972,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { </div> {this.openContexts ? <div className="propertiesView-contexts-content" >{this.contexts}</div> : null} </div> - <div className="propertiesView-layout"> + {/* <div className="propertiesView-layout"> <div className="propertiesView-layout-title" onPointerDown={action(() => this.openLayout = !this.openLayout)} style={{ backgroundColor: this.openLayout ? "black" : "" }}> @@ -982,7 +982,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { </div> </div> {this.openLayout ? <div className="propertiesView-layout-content" >{this.layoutPreview}</div> : null} - </div> + </div> */} </div>; } if (this.isPres) { diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 2bdc8e2f3..80e9b41ad 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -157,7 +157,7 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus }); } else { - added.map(doc => { + added.filter(doc => [AclAdmin, AclEdit].includes(GetEffectiveAcl(doc))).map(doc => { // only make a pushpin if we have acl's to edit the document const context = Cast(doc.context, Doc, null); if (context && (context.type === DocumentType.VID || context.type === DocumentType.WEB || context.type === DocumentType.PDF || context.type === DocumentType.IMG)) { const pushpin = Docs.Create.FontIconDocument({ @@ -186,9 +186,9 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus @action.bound removeDocument = (doc: any): boolean => { const effectiveAcl = GetEffectiveAcl(this.props.Document[DataSym]); - const docAcl = GetEffectiveAcl(doc); - if (effectiveAcl === AclEdit || effectiveAcl === AclAdmin || docAcl === AclAdmin) { - const docs = doc instanceof Doc ? [doc] : doc as Doc[]; + const indocs = doc instanceof Doc ? [doc] : doc as Doc[]; + const docs = indocs.filter(doc => effectiveAcl === AclEdit || effectiveAcl === AclAdmin || GetEffectiveAcl(doc) === AclAdmin); + if (docs.length) { const targetDataDoc = this.props.Document[DataSym]; const value = DocListCast(targetDataDoc[this.props.fieldKey]); const toRemove = value.filter(v => docs.includes(v)); @@ -196,7 +196,6 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus const recent = Cast(Doc.UserDoc().myRecentlyClosedDocs, Doc) as Doc; toRemove.forEach(doc => { const ind = (targetDataDoc[this.props.fieldKey] as List<Doc>).indexOf(doc); - (targetDataDoc[this.props.fieldKey] as List<Doc>).splice(ind, 0); if (ind !== -1) { Doc.RemoveDocFromList(targetDataDoc, this.props.fieldKey, doc); recent && Doc.AddDocToList(recent, "data", doc, undefined, true, true); diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 2783011cf..35b4c8e98 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -867,7 +867,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P this.layoutDoc._panY = NumCast(this.layoutDoc._panY) - newpan[1]; } - focusDocument = (doc: Doc, willZoom: boolean, scale?: number, afterFocus?: () => boolean) => { + focusDocument = (doc: Doc, willZoom: boolean, scale?: number, afterFocus?: () => boolean, dontCenter?: boolean) => { const state = HistoryUtil.getState(); // TODO This technically isn't correct if type !== "doc", as @@ -886,15 +886,19 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P SelectionManager.DeselectAll(); if (this.props.Document.scrollHeight) { const annotOn = Cast(doc.annotationOn, Doc) as Doc; + let delay = 1000; if (!annotOn) { this.props.focus(doc); } else { const contextHgt = Doc.AreProtosEqual(annotOn, this.props.Document) && this.props.VisibleHeight ? this.props.VisibleHeight() : NumCast(annotOn._height); const offset = annotOn && (contextHgt / 2); - this.props.Document._scrollY = NumCast(doc.y) - offset; + const scrollTo = NumCast(doc.y) - ((Number.isNaN(offset) ? 150 : offset)); + this.props.Document._scrollY = scrollTo; + delay = Math.abs(scrollTo - NumCast(this.props.Document._scrollTop)) > 5 ? 1000 : 0; } - afterFocus && setTimeout(afterFocus, 1000); + !dontCenter && this.props.focus(this.props.Document); + afterFocus && setTimeout(afterFocus, delay); } else { const layoutdoc = Doc.Layout(doc); const newPanX = NumCast(doc.x) + NumCast(layoutdoc._width) / 2; @@ -1159,7 +1163,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P @action onCursorMove = (e: React.PointerEvent) => { - super.setCursorPosition(this.getTransform().transformPoint(e.clientX, e.clientY)); + // super.setCursorPosition(this.getTransform().transformPoint(e.clientX, e.clientY)); } diff --git a/src/client/views/linking/LinkEditor.tsx b/src/client/views/linking/LinkEditor.tsx index 3713a1026..435b9d904 100644 --- a/src/client/views/linking/LinkEditor.tsx +++ b/src/client/views/linking/LinkEditor.tsx @@ -10,264 +10,6 @@ import { undoBatch } from "../../util/UndoManager"; import './LinkEditor.scss'; import React = require("react"); -interface GroupTypesDropdownProps { - groupType: string; - setGroupType: (group: string) => void; -} -// this dropdown could be generalized -@observer -class GroupTypesDropdown extends React.Component<GroupTypesDropdownProps> { - @observable private _searchTerm: string = this.props.groupType; - @observable private _groupType: string = this.props.groupType; - @observable private _isEditing: boolean = false; - - @action - createGroup = (groupType: string): void => { - this.props.setGroupType(groupType); - LinkManager.Instance.addGroupType(groupType); - } - - @action - onChange = (val: string): void => { - this._searchTerm = val; - this._groupType = val; - this._isEditing = true; - } - - @action - onKeyDown = (e: React.KeyboardEvent): void => { - if (e.key === "Enter") { - 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) { - const groupType = groupOptions[exactFound]; - this.props.setGroupType(groupType); - this._groupType = groupType; - } else { - this.createGroup(this._searchTerm); - this._groupType = this._searchTerm; - } - - this._searchTerm = this._groupType; - this._isEditing = false; - } - } - - @action - onOptionClick = (value: string, createNew: boolean): void => { - if (createNew) { - this.createGroup(this._searchTerm); - this._groupType = this._searchTerm; - - } else { - this.props.setGroupType(value); - this._groupType = value; - - } - this._searchTerm = this._groupType; - this._isEditing = false; - } - - @action - onButtonPointerDown = (): void => { - this._isEditing = true; - } - - renderOptions = (): JSX.Element[] | JSX.Element => { - if (this._searchTerm === "") return <></>; - - 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; - - 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 !== "") { - 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>); - } - - return options; - } - - render() { - if (this._isEditing || this._groupType === "") { - return ( - <div className="linkEditor-dropdown"> - <input type="text" value={this._groupType === "-ungrouped-" ? "" : this._groupType} placeholder="Search for or create a new group" - onChange={e => this.onChange(e.target.value)} onKeyDown={this.onKeyDown} autoFocus></input> - <div className="linkEditor-options-wrapper"> - {this.renderOptions()} - </div> - </div > - ); - } else { - return <button className="linkEditor-typeButton" onClick={() => this.onButtonPointerDown()}>{this._groupType}</button>; - } - } -} - - -interface LinkMetadataEditorProps { - id: string; - groupType: string; - mdDoc: Doc; - mdKey: string; - mdValue: string; - changeMdIdKey: (id: string, newKey: string) => void; -} -@observer -class LinkMetadataEditor extends React.Component<LinkMetadataEditorProps> { - @observable private _key: string = this.props.mdKey; - @observable private _value: string = this.props.mdValue; - @observable private _keyError: boolean = false; - - @action - setMetadataKey = (value: string): void => { - const groupMdKeys = LinkManager.Instance.getMetadataKeysInGroup(this.props.groupType); - - // don't allow user to create existing key - const newIndex = groupMdKeys.findIndex(key => key.toUpperCase() === value.toUpperCase()); - if (newIndex > -1) { - this._keyError = true; - this._key = value; - return; - } else { - this._keyError = false; - } - - // set new value for key - const currIndex = groupMdKeys.findIndex(key => { - return StrCast(key).toUpperCase() === this._key.toUpperCase(); - }); - if (currIndex === -1) console.error("LinkMetadataEditor: key was not found"); - groupMdKeys[currIndex] = value; - - this.props.changeMdIdKey(this.props.id, value); - this._key = value; - LinkManager.Instance.setMetadataKeysForGroup(this.props.groupType, [...groupMdKeys]); - } - - @action - setMetadataValue = (value: string): void => { - if (!this._keyError) { - this._value = value; - Doc.GetProto(this.props.mdDoc)[this._key] = value; - } - } - - @action - removeMetadata = (): void => { - const groupMdKeys = LinkManager.Instance.getMetadataKeysInGroup(this.props.groupType); - - const index = groupMdKeys.findIndex(key => key.toUpperCase() === this._key.toUpperCase()); - if (index === -1) console.error("LinkMetadataEditor: key was not found"); - groupMdKeys.splice(index, 1); - - LinkManager.Instance.setMetadataKeysForGroup(this.props.groupType, groupMdKeys); - this._key = ""; - } - - render() { - return ( - <div className="linkEditor-metadata-row"> - <input className={this._keyError ? "linkEditor-error" : ""} type="text" value={this._key === "new key" ? "" : this._key} placeholder="key" onChange={e => this.setMetadataKey(e.target.value)}></input>: - <input type="text" value={this._value} placeholder="value" onChange={e => this.setMetadataValue(e.target.value)}></input> - <button title="remove metadata from relationship" onClick={() => this.removeMetadata()}><FontAwesomeIcon icon="times" size="sm" /></button> - </div> - ); - } -} - -interface LinkGroupEditorProps { - sourceDoc: Doc; - linkDoc: Doc; - groupDoc: Doc; -} -@observer -export class LinkGroupEditor extends React.Component<LinkGroupEditorProps> { - - private _metadataIds: Map<string, string> = new Map(); - - constructor(props: LinkGroupEditorProps) { - super(props); - - const groupMdKeys = LinkManager.Instance.getMetadataKeysInGroup(StrCast(props.groupDoc.linkRelationship)); - groupMdKeys.forEach(key => this._metadataIds.set(key, Utils.GenerateGuid())); - } - - @action - setGroupType = (groupType: string): void => { - Doc.GetProto(this.props.groupDoc).linkRelationship = groupType; - } - - removeGroupFromLink = (groupType: string): void => { - LinkManager.Instance.removeGroupFromAnchor(this.props.linkDoc, this.props.sourceDoc, groupType); - } - - deleteGroup = (groupType: string): void => { - LinkManager.Instance.deleteGroupType(groupType); - } - - - @action - addMetadata = (groupType: string): void => { - this._metadataIds.set("new key", Utils.GenerateGuid()); - 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); - } - - // for key rendering purposes - changeMdIdKey = (id: string, newKey: string) => { - this._metadataIds.set(newKey, id); - } - - renderMetadata = (): JSX.Element[] => { - const metadata: Array<JSX.Element> = []; - const groupDoc = this.props.groupDoc; - const groupType = StrCast(groupDoc.linkRelationship); - const groupMdKeys = LinkManager.Instance.getMetadataKeysInGroup(groupType); - - groupMdKeys.forEach((key) => { - const val = StrCast(groupDoc[key]); - metadata.push( - <LinkMetadataEditor key={"mded-" + this._metadataIds.get(key)} id={this._metadataIds.get(key)!} groupType={groupType} mdDoc={groupDoc} mdKey={key} mdValue={val} changeMdIdKey={this.changeMdIdKey} /> - ); - }); - return metadata; - } - - render() { - const groupType = StrCast(this.props.groupDoc.linkRelationship); - // if ((groupType && LinkManager.Instance.getMetadataKeysInGroup(groupType).length > 0) || groupType === "") { - const buttons = <button className="linkEditor-button" disabled={groupType === ""} onClick={() => this.deleteGroup(groupType)} title="Delete Relationship from all links"><FontAwesomeIcon icon="trash" size="sm" /></button>; - const addButton = <button className="linkEditor-addbutton" onClick={() => this.addMetadata(groupType)} disabled={groupType === ""} title="Add metadata to relationship"><FontAwesomeIcon icon="plus" size="sm" /></button>; - - return ( - <div className="linkEditor-group"> - <div className="linkEditor-group-row "> - {buttons} - <GroupTypesDropdown groupType={groupType} setGroupType={this.setGroupType} /> - <button className="linkEditor-button" onClick={() => this.removeGroupFromLink(groupType)} title="Remove relationship from link"><FontAwesomeIcon icon="times" size="sm" /></button> - </div> - {this.renderMetadata().length > 0 ? <p className="linkEditor-group-row-label">metadata:</p> : <></>} - {addButton} - {this.renderMetadata()} - </div> - ); - } -} - interface LinkEditorProps { sourceDoc: Doc; @@ -422,10 +164,6 @@ export class LinkEditor extends React.Component<LinkEditorProps> { render() { const destination = LinkManager.getOppositeAnchor(this.props.linkDoc, this.props.sourceDoc); - const groups = [this.props.linkDoc].map(groupDoc => { - return <LinkGroupEditor key={"gred-" + StrCast(groupDoc.linkRelationship)} linkDoc={this.props.linkDoc} - sourceDoc={this.props.sourceDoc} groupDoc={groupDoc} />; - }); return !destination ? (null) : ( <div className="linkEditor"> @@ -450,8 +188,6 @@ export class LinkEditor extends React.Component<LinkEditorProps> { <div>{this.editDescription}</div> <div>{this.followingDropdown}</div> - - {/* {groups.length > 0 ? groups : <div className="linkEditor-group">There are currently no relationships associated with this link.</div>} */} </div> ); diff --git a/src/client/views/linking/LinkMenuGroup.tsx b/src/client/views/linking/LinkMenuGroup.tsx index 29e1d921c..e76227ccf 100644 --- a/src/client/views/linking/LinkMenuGroup.tsx +++ b/src/client/views/linking/LinkMenuGroup.tsx @@ -54,17 +54,6 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> { e.stopPropagation(); } - viewGroupAsTable = (groupType: string): JSX.Element => { - const keys = LinkManager.Instance.getMetadataKeysInGroup(groupType); - const index = keys.indexOf(""); - 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", childDropAction: "alias" })); - 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() { const set = new Set<Doc>(this.props.group); const groupItems = Array.from(set.keys()).map(linkDoc => { diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx index 3a8d41fef..1ba724889 100644 --- a/src/client/views/linking/LinkMenuItem.tsx +++ b/src/client/views/linking/LinkMenuItem.tsx @@ -90,19 +90,6 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { return true; } - renderMetadata = (): JSX.Element => { - const index = StrCast(this.props.linkDoc.title).toUpperCase() === this.props.groupType.toUpperCase() ? 0 : -1; - const mdDoc = index > -1 ? this.props.linkDoc : undefined; - - let mdRows: Array<JSX.Element> = []; - if (mdDoc) { - const keys = LinkManager.Instance.getMetadataKeysInGroup(this.props.groupType);//groupMetadataKeys.get(this.props.groupType); - mdRows = keys.map(key => <div key={key} className="link-metadata-row"><b>{key}</b>: {StrCast(mdDoc[key])}</div>); - } - - return (<div className="link-metadata">{mdRows}</div>); - } - @action onLinkButtonDown = (e: React.PointerEvent): void => { this._downX = e.clientX; @@ -191,8 +178,6 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { } render() { - const keys = LinkManager.Instance.getMetadataKeysInGroup(this.props.groupType);//groupMetadataKeys.get(this.props.groupType); - const canExpand = keys ? keys.length > 0 : false; const eyeIcon = this.props.linkDoc.hidden ? "eye-slash" : "eye"; @@ -230,7 +215,7 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { return ( <div className="linkMenu-item"> - <div className={canExpand ? "linkMenu-item-content expand-three" : "linkMenu-item-content expand-two"}> + <div className={"linkMenu-item-content expand-two"}> <div ref={this._drag} className="linkMenu-name" //title="drag to view target. click to customize." onPointerLeave={action(() => LinkDocPreview.LinkInfo = undefined)} @@ -257,8 +242,6 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { {StrCast(this.props.linkDoc.description)}</p> : null} </div> <div className="linkMenu-item-buttons" ref={this._buttonRef} > - {canExpand ? <div title="Show more" className="button" onPointerDown={e => this.toggleShowMore(e)}> - <FontAwesomeIcon className="fa-icon" icon={this._showMore ? "chevron-up" : "chevron-down"} size="sm" /></div> : <></>} <Tooltip title={<><div className="dash-tooltip">{this.props.linkDoc.hidden ? "Show link" : "Hide link"}</div></>}> <div className="button" ref={this._editRef} onPointerDown={this.showLink}> @@ -277,7 +260,6 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { <FontAwesomeIcon className="fa-icon" icon="arrow-right" size="sm" /></div> */} </div> </div> - {this._showMore ? this.renderMetadata() : <></>} </div> </div > diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index ddcf7f6f4..369b53aa0 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -740,6 +740,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu const portal = Docs.Create.FreeformDocument([], { _width: NumCast(this.layoutDoc._width) + 10, _height: NumCast(this.layoutDoc._height), title: StrCast(this.props.Document.title) + ".portal" }); DocUtils.MakeLink({ doc: this.props.Document }, { doc: portal }, "portal to"); } + this.Document.followLinkLocation = "inPlace"; this.Document.followLinkZoom = true; this.Document.isLinkButton = true; } diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index d65b43704..3a5b27b21 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -134,7 +134,7 @@ export class FieldView extends React.Component<FieldViewProps> { // ); } else if (field instanceof List) { - return <div> {field.length ? field.map(f => Field.toString(f)).join(", ") : "[]"} </div>; + return <div> {field.length ? field.map(f => Field.toString(f)).join(", ") : ""} </div>; } // bcz: this belongs here, but it doesn't render well so taking it out for now else if (field instanceof WebField) { diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index a998386d8..ef39e6a90 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -1123,6 +1123,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } } + private isActiveTab(el: Element | null | undefined) { + while (el && el !== document.body) { + if (getComputedStyle(el).display === "none") return false; + el = el.parentNode as any; + } + return true; + } + private setupEditor(config: any, fieldKey: string) { const curText = Cast(this.dataDoc[this.props.fieldKey], RichTextField, null); const rtfField = Cast((!curText?.Text && this.layoutDoc[this.props.fieldKey]) || this.dataDoc[fieldKey], RichTextField); @@ -1161,7 +1169,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } const selectOnLoad = this.rootDoc[Id] === FormattedTextBox.SelectOnLoad; - if (selectOnLoad && !this.props.dontRegisterView && !this.props.dontSelectOnLoad) { + if (selectOnLoad && !this.props.dontRegisterView && !this.props.dontSelectOnLoad && this.isActiveTab(this.ProseRef)) { FormattedTextBox.SelectOnLoad = ""; this.props.select(false); if (FormattedTextBox.SelectOnLoadChar && this._editorView) { diff --git a/src/client/views/nodes/formattedText/FormattedTextBoxComment.scss b/src/client/views/nodes/formattedText/FormattedTextBoxComment.scss index 582ada6ce..81afba4d7 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBoxComment.scss +++ b/src/client/views/nodes/formattedText/FormattedTextBoxComment.scss @@ -29,9 +29,6 @@ font-style: italic; color: rgb(95, 97, 102); font-size: 10px; - padding-bottom: 4px; - margin-bottom: 5px; - } } diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx index 84b14cd61..e7f901091 100644 --- a/src/client/views/pdf/Annotation.tsx +++ b/src/client/views/pdf/Annotation.tsx @@ -15,6 +15,7 @@ interface IAnnotationProps { addDocTab: (document: Doc, where: string) => boolean; pinToPres: (document: Doc, unpin?: boolean) => void; focus: (doc: Doc) => void; + select: (isCtrlPressed: boolean) => void; dataDoc: Doc; fieldKey: string; showInfo: (anno: Opt<Doc>) => void; @@ -25,7 +26,7 @@ export class Annotation extends React.Component<IAnnotationProps> { render() { return DocListCast(this.props.anno.annotations).map(a => - <RegionAnnotation {...this.props} showInfo={this.props.showInfo} pinToPres={this.props.pinToPres} document={a} x={NumCast(a.x)} y={NumCast(a.y)} width={a[WidthSym]()} height={a[HeightSym]()} key={a[Id]} />); + <RegionAnnotation {...this.props} showInfo={this.props.showInfo} select={this.props.select} pinToPres={this.props.pinToPres} document={a} x={NumCast(a.x)} y={NumCast(a.y)} width={a[WidthSym]()} height={a[HeightSym]()} key={a[Id]} />); } } @@ -37,6 +38,7 @@ interface IRegionAnnotationProps { height: number; addDocTab: (document: Doc, where: string) => boolean; pinToPres: (document: Doc, unpin: boolean) => void; + select: (isCtrlPressed: boolean) => void; document: Doc; dataDoc: Doc; fieldKey: string; @@ -115,7 +117,8 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> { e.persist(); e.stopPropagation(); PromiseValue(this.props.document.group).then(annoGroup => annoGroup instanceof Doc && - DocumentManager.Instance.FollowLink(undefined, annoGroup, (doc, followLinkLocation) => this.props.addDocTab(doc, e.ctrlKey ? "add" : followLinkLocation), false, undefined) + DocumentManager.Instance.FollowLink(undefined, annoGroup, (doc, followLinkLocation) => this.props.addDocTab(doc, e.ctrlKey ? "add" : followLinkLocation), false, undefined, + () => this.props.select(false)) ); } } diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index 77dd40f2a..3570c565a 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -155,6 +155,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu this._mainCont.current.scrollTop = this.layoutDoc._scrollTop || 0; const observer = new _global.ResizeObserver(action((entries: any) => this._mainCont.current && (this._mainCont.current.scrollTop = this.layoutDoc._scrollTop || 0))); observer.observe(this._mainCont.current); + this._mainCont.current.addEventListener("scroll", (e) => (e.target as any).scrollLeft = 0); } this._disposers.searchMatch = reaction(() => Doc.IsSearchMatch(this.rootDoc), @@ -179,9 +180,9 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu if (scrollY !== undefined) { (this._showCover || this._showWaiting) && this.setupPdfJsViewer(); if ((this.props.renderDepth === -1 || (!LinkDocPreview.TargetDoc && !FormattedTextBoxComment.linkDoc)) && this._mainCont.current) { - smoothScroll(1000, this._mainCont.current, (this.Document._scrollY || 0)); - } else { - console.log("Waiting for preview"); + smoothScroll(1000, this._mainCont.current, scrollY || 0); + } else if (!LinkDocPreview.TargetDoc && !FormattedTextBoxComment.linkDoc) { // wait for mainCont and try again to scroll + setTimeout(() => this._mainCont.current && smoothScroll(1000, this._mainCont.current, scrollY || 0), 250); } setTimeout(() => this.Document._scrollY = undefined, 1000); } @@ -459,7 +460,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu @action onPointerDown = (e: React.PointerEvent): void => { const hit = document.elementFromPoint(e.clientX, e.clientY); - if (hit && hit.localName === "span" && this.props.isSelected(true)) { // drag selecting text stops propagation + if (hit && hit.localName === "span" && this.annotationsActive(true)) { // drag selecting text stops propagation e.button === 0 && e.stopPropagation(); } // if alt+left click, drag and annotate @@ -556,6 +557,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu @action onSelectEnd = (e: PointerEvent): void => { clearStyleSheetRules(PDFViewer._annotationStyle); + this.props.select(false); this._savedAnnotations.clear(); if (this._marqueeing) { if (this._marqueeWidth > 10 || this._marqueeHeight > 10) { @@ -693,7 +695,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu TraceMobx(); return <div className="pdfViewerDash-annotationLayer" style={{ height: NumCast(this.Document._nativeHeight), transform: `scale(${this._zoomed})` }} ref={this._annotationLayer}> {this.nonDocAnnotations.sort((a, b) => NumCast(a.y) - NumCast(b.y)).map(anno => - <Annotation {...this.props} showInfo={this.showInfo} focus={this.props.focus} dataDoc={this.dataDoc} fieldKey={this.props.fieldKey} anno={anno} key={`${anno[Id]}-annotation`} />) + <Annotation {...this.props} showInfo={this.showInfo} select={this.props.select} focus={this.props.focus} dataDoc={this.dataDoc} fieldKey={this.props.fieldKey} anno={anno} key={`${anno[Id]}-annotation`} />) } </div>; } @@ -762,7 +764,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu contentZoom = () => this._zoomed; render() { TraceMobx(); - return <div className={"pdfViewerDash" + (this.active() ? "-interactive" : "")} ref={this._mainCont} + return <div className={"pdfViewerDash" + (this.annotationsActive() ? "-interactive" : "")} ref={this._mainCont} onScroll={this.onScroll} onWheel={this.onZoomWheel} onPointerDown={this.onPointerDown} onClick={this.onClick} style={{ overflowX: this._zoomed !== 1 ? "scroll" : undefined, diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx index b381bbfa9..1104d8d2a 100644 --- a/src/client/views/search/SearchBox.tsx +++ b/src/client/views/search/SearchBox.tsx @@ -146,7 +146,7 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc const mod = "_t:"; const newWords: string[] = []; const oldWords = values[0].split(" "); - oldWords.forEach((word, i) => i === 0 ? newWords.push(key + mod + "\"" + word + "\"") : newWords.push("AND " + key + mod + "\"" + word + "\"")); + oldWords.forEach((word, i) => i === 0 ? newWords.push(key + mod + word) : newWords.push("AND " + key + mod + word)); query = `(${query}) AND (${newWords.join(" ")})`; } else { @@ -154,7 +154,7 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc const mod = "_t:"; const newWords: string[] = []; const oldWords = values[i].split(" "); - oldWords.forEach((word, i) => i === 0 ? newWords.push(key + mod + "\"" + word + "\"") : newWords.push("AND " + key + mod + "\"" + word + "\"")); + oldWords.forEach((word, i) => i === 0 ? newWords.push(key + mod + word) : newWords.push("AND " + key + mod + word)); const v = "(" + newWords.join(" ") + ")"; if (i === 0) { query = `(${query}) AND (${v}` + (values.length === 1 ? ")" : ""); diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index a8a5ba9bd..54d85ba86 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -104,6 +104,7 @@ const AclMap = new Map<string, symbol>([ ]); export function fetchProto(doc: Doc) { + if (!doc) return; const permissions: { [key: string]: symbol } = {}; Object.keys(doc).filter(key => key.startsWith("acl") && (permissions[key] = AclMap.get(StrCast(doc[key]))!)); diff --git a/src/fields/List.ts b/src/fields/List.ts index c9e4bd3c1..ceb538b2d 100644 --- a/src/fields/List.ts +++ b/src/fields/List.ts @@ -43,7 +43,7 @@ const listHandlers: any = { } } const res = list.__fields.push(...items); - this[Update](); + this[Update]({ op: "$addToSet", items }); return res; }), reverse() { @@ -66,6 +66,7 @@ const listHandlers: any = { this[Self].__realFields(); // coerce retrieving entire array items = items.map(toObjectField); const list = this[Self]; + const removed = list.__fields.filter((item: any, i: number) => i >= start && i < start + deleteCount); for (let i = 0; i < items.length; i++) { const item = items[i]; //TODO Error checking to make sure parent doesn't already exist @@ -76,7 +77,7 @@ const listHandlers: any = { } } const res = list.__fields.splice(start, deleteCount, ...items); - this[Update](); + this[Update](items.length === 0 && deleteCount ? { op: "$remFromSet", items: removed } : undefined); return res.map(toRealField); }), unshift(...items: any[]) { @@ -314,7 +315,7 @@ class ListImpl<T extends Field> extends ObjectField { // console.log(diff); const update = this[OnUpdate]; // update && update(diff); - update?.(); + update?.(diff); } private [Self] = this; diff --git a/src/fields/util.ts b/src/fields/util.ts index 4da9fce74..8a694de83 100644 --- a/src/fields/util.ts +++ b/src/fields/util.ts @@ -11,7 +11,7 @@ import { ComputedField } from "./ScriptField"; import { ScriptCast, StrCast } from "./Types"; import { returnZero } from "../Utils"; import CursorField from "./CursorField"; - +import { List } from "./List"; function _readOnlySetter(): never { throw new Error("Documents can't be modified in read-only mode"); @@ -93,7 +93,7 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number } else { DocServer.registerDocWithCachedUpdate(receiver, prop as string, curValue); } - UndoManager.AddEvent({ + !receiver[UpdatingFromServer] && UndoManager.AddEvent({ redo: () => receiver[prop] = value, undo: () => receiver[prop] = curValue }); @@ -371,18 +371,44 @@ export function deleteProperty(target: any, prop: string | number | symbol) { export function updateFunction(target: any, prop: any, value: any, receiver: any) { let current = ObjectField.MakeCopy(value); return (diff?: any) => { - if (true || !diff) { - diff = { '$set': { ["fields." + prop]: SerializationHelper.Serialize(value) } }; - const oldValue = current; - const newValue = ObjectField.MakeCopy(value); - current = newValue; - if (!(value instanceof CursorField) && !(value?.some?.((v: any) => v instanceof CursorField))) { - UndoManager.AddEvent({ - redo() { receiver[prop] = newValue; }, - undo() { receiver[prop] = oldValue; } - }); - } + const op = diff?.op === "$addToSet" ? { '$addToSet': { ["fields." + prop]: SerializationHelper.Serialize(new List<Doc>(diff.items)) } } : + diff?.op === "$remFromSet" ? { '$remFromSet': { ["fields." + prop]: SerializationHelper.Serialize(new List<Doc>(diff.items)) } } + : { '$set': { ["fields." + prop]: SerializationHelper.Serialize(value) } }; + const oldValue = current; + const newValue = ObjectField.MakeCopy(value); + current = newValue; + if (!(value instanceof CursorField) && !(value?.some?.((v: any) => v instanceof CursorField))) { + !receiver[UpdatingFromServer] && UndoManager.AddEvent( + diff?.op === "$addToSet" ? + { + redo: () => { + receiver[prop].push(...diff.items); + }, + undo: action(() => { + let curList = receiver[prop]; + //while (curList[ForwardUpates]) curList = curList[ForwardUpates]; + diff.items.forEach((doc: any) => { + const ind = curList.indexOf(doc.value()); + ind !== -1 && curList.splice(ind, 1); + }); + }) + } : + diff?.op === "$remFromSet" ? + { + redo: action(() => { + let curList = receiver[prop]; + diff.items.forEach((doc: any) => { + const ind = curList.indexOf(doc.value()); + ind !== -1 && curList.splice(ind, 1); + }); + }), + undo: () => receiver[prop].push(...diff.items) + } + : { + redo: () => receiver[prop] = newValue, + undo: () => receiver[prop] = oldValue + }); } - target[Update](diff); + target[Update](op); }; }
\ No newline at end of file diff --git a/src/server/websocket.ts b/src/server/websocket.ts index b33e76c0b..459fa520b 100644 --- a/src/server/websocket.ts +++ b/src/server/websocket.ts @@ -201,7 +201,7 @@ export namespace WebSocket { function setField(socket: Socket, newValue: Transferable) { Database.Instance.update(newValue.id, newValue, () => - socket.broadcast.emit(MessageStore.SetField.Message, newValue)); + socket.broadcast.emit(MessageStore.SetField.Message, newValue)); // broadcast set value to all other clients if (newValue.type === Types.Text) { // if the newValue has sring type, then it's suitable for searching -- pass it to SOLR Search.updateDocument({ id: newValue.id, data: { set: (newValue as any).data } }); } @@ -271,7 +271,46 @@ export namespace WebSocket { return typeof value === "string" ? value : value[0]; } + function addToListField(socket: Socket, diff: Diff, curListItems?: Transferable): void { + diff.diff.$set = diff.diff.$addToSet; delete diff.diff.$addToSet;// convert add to set to a query of the current fields, and then a set of the composition of the new fields with the old ones + const updatefield = Array.from(Object.keys(diff.diff.$set))[0]; + const newListItems = diff.diff.$set[updatefield].fields; + const curList = (curListItems as any)?.fields?.[updatefield.replace("fields.", "")]?.fields || []; + diff.diff.$set[updatefield].fields = [...curList, ...newListItems.filter((newItem: any) => !curList.some((curItem: any) => curItem.fieldId === newItem.fieldId))]; + const sendBack = true;//curList.length !== prelen; + Database.Instance.update(diff.id, diff.diff, + () => { + if (sendBack) { + const id = socket.id; + socket.id = ""; + socket.broadcast.emit(MessageStore.UpdateField.Message, diff); + socket.id = id; + } else socket.broadcast.emit(MessageStore.UpdateField.Message, diff); + }, false); + } + + function remFromListField(socket: Socket, diff: Diff, curListItems?: Transferable): void { + diff.diff.$set = diff.diff.$remFromSet; delete diff.diff.$remFromSet; + const updatefield = Array.from(Object.keys(diff.diff.$set))[0]; + const remListItems = diff.diff.$set[updatefield].fields; + const curList = (curListItems as any)?.fields?.[updatefield.replace("fields.", "")]?.fields || []; + diff.diff.$set[updatefield].fields = curList?.filter((curItem: any) => !remListItems.some((remItem: any) => remItem.fieldId === curItem.fieldId)); + const sendBack = true;//curList.length + remListItems.length !== prelen; + Database.Instance.update(diff.id, diff.diff, + () => { + if (sendBack) { + const id = socket.id; + socket.id = ""; + socket.broadcast.emit(MessageStore.UpdateField.Message, diff); + socket.id = id; + } else socket.broadcast.emit(MessageStore.UpdateField.Message, diff); + }, false); + } + + function UpdateField(socket: Socket, diff: Diff) { + if (diff.diff.$addToSet) return GetRefField([diff.id, (result?: Transferable) => addToListField(socket, diff, result)]); // would prefer to have Mongo handle list additions direclty, but for now handle it on our own + if (diff.diff.$remFromSet) return GetRefField([diff.id, (result?: Transferable) => remFromListField(socket, diff, result)]); // would prefer to have Mongo handle list additions direclty, but for now handle it on our own Database.Instance.update(diff.id, diff.diff, () => socket.broadcast.emit(MessageStore.UpdateField.Message, diff), false); const docfield = diff.diff.$set || diff.diff.$unset; |