From 726dfb8fea45352b2eb0729ad6d3b4a7b0824e1a Mon Sep 17 00:00:00 2001 From: bobzel Date: Sat, 6 Mar 2021 16:34:44 -0500 Subject: updated a bunch of _backgroundColors to backgroundColors --- src/client/documents/Documents.ts | 8 ++++---- src/client/util/CurrentUserUtils.ts | 8 ++++---- src/client/views/collections/CollectionSubView.tsx | 2 +- src/client/views/collections/CollectionView.tsx | 2 +- src/client/views/collections/TreeView.tsx | 2 +- src/client/views/nodes/EquationBox.tsx | 2 +- 6 files changed, 12 insertions(+), 12 deletions(-) (limited to 'src') diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index ca5ee9cbd..0da80d2c4 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -153,7 +153,7 @@ export class DocumentOptions { "_carousel-caption-yMargin"?: number; x?: number; y?: number; - z?: number; + z?: number; // whether document is in overlay (1) or not (0 or undefined) author?: string; layoutKey?: string; type?: string; @@ -260,9 +260,9 @@ export namespace Docs { const _docOptions = new DocumentOptions(); export async function setupFieldInfos() { - return await DocServer.GetRefField("FieldInfos7") as Doc ?? + return await DocServer.GetRefField("FieldInfos8") as Doc ?? runInAction(() => { - const infos = new Doc("FieldInfos7", true); + const infos = new Doc("FieldInfos8", true); const keys = Object.keys(new DocumentOptions()); for (const key of keys) { const options = (_docOptions as any)[key] as FInfo; @@ -728,7 +728,7 @@ export namespace Docs { } export function ComparisonDocument(options: DocumentOptions = { title: "Comparison Box" }) { - return InstanceFromProto(Prototypes.get(DocumentType.COMPARISON), "", { clipWidth: 50, _backgroundColor: "gray", targetDropAction: "alias", ...options }); + return InstanceFromProto(Prototypes.get(DocumentType.COMPARISON), "", { clipWidth: 50, backgroundColor: "gray", targetDropAction: "alias", ...options }); } export function AudioDocument(url: string, options: DocumentOptions = {}) { diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index ddb7ded1d..8b425f929 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -378,7 +378,7 @@ export class CurrentUserUtils { ((doc.emptyPane as Doc).proto as Doc)["dragFactory-count"] = 0; } if (doc.emptySlide === undefined) { - const textDoc = Docs.Create.TreeDocument([], { title: "Slide", _viewType: CollectionViewType.Tree, _fontSize: "20px", treeViewType: "outline", _xMargin: 0, _yMargin: 0, _width: 300, _height: 200, _singleLine: true, _backgroundColor: "transparent", system: true, cloneFieldFilter: new List(["system"]) }); + const textDoc = Docs.Create.TreeDocument([], { title: "Slide", _viewType: CollectionViewType.Tree, _fontSize: "20px", treeViewType: "outline", _xMargin: 0, _yMargin: 0, _width: 300, _height: 200, _singleLine: true, backgroundColor: "transparent", system: true, cloneFieldFilter: new List(["system"]) }); Doc.GetProto(textDoc).title = ComputedField.MakeFunction('self.text?.Text'); FormattedTextBox.SelectOnLoad = textDoc[Id]; doc.emptySlide = textDoc; @@ -549,7 +549,7 @@ export class CurrentUserUtils { dontUndo: true, title, target, - _backgroundColor: "black", + backgroundColor: "black", dropAction: "alias", removeDropProperties: new List(["dropAction", "_stayInCollection"]), _width: 60, @@ -564,7 +564,7 @@ export class CurrentUserUtils { title: "menuItemPanel", childDropAction: "alias", dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }), - _backgroundColor: "black", ignoreClick: true, + backgroundColor: "black", ignoreClick: true, _gridGap: 0, _yMargin: 0, _yPadding: 0, _xMargin: 0, _autoHeight: false, _width: 60, _columnWidth: 60, _lockedPosition: true, _chromeStatus: "disabled", system: true @@ -621,7 +621,7 @@ export class CurrentUserUtils { title: data.title, _lockedPosition: true, onClick: data.click ? ScriptField.MakeScript(data.click) : undefined, - _backgroundColor: data.backgroundColor, system: true + backgroundColor: data.backgroundColor, system: true }, [this.ficon({ ignoreClick: true, icon: data.icon, backgroundColor: "rgba(0,0,0,0)", system: true }), this.mobileTextContainer({}, [this.mobileButtonText({}, data.title), this.mobileButtonInfo({}, data.info)])]) ); diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index e5c835f39..59e1824c1 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -329,7 +329,7 @@ export function CollectionSubView(schemaCtor: (doc: Doc) => T, moreProps?: const y = NumCast(srcWeb._scrollTop) + ((rects && Array.from(rects).reduce((y: any, r: DOMRect) => y === undefined || r.y < y ? r.y : y, undefined as any)) || 0); const r = (rects && Array.from(rects).reduce((x: any, r: DOMRect) => x === undefined || r.x + r.width > x ? r.x + r.width : x, undefined as any)) || 0; const b = NumCast(srcWeb._scrollTop) + ((rects && Array.from(rects).reduce((y: any, r: DOMRect) => y === undefined || r.y + r.height > y ? r.y + r.height : y, undefined as any)) || 0); - const anchor = Docs.Create.FreeformDocument([], { _backgroundColor: "transparent", _width: r - x, _height: b - y, x, y, annotationOn: srcWeb }); + const anchor = Docs.Create.FreeformDocument([], { backgroundColor: "transparent", _width: r - x, _height: b - y, x, y, annotationOn: srcWeb }); anchor.context = srcWeb; const key = Doc.LayoutFieldKey(srcWeb); Doc.AddDocToList(srcWeb, key + "-annotations", anchor); diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index a217de193..18abd3a88 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -154,7 +154,7 @@ export class CollectionView extends Touchable { if (context && !hasContextAnchor && (context.type === DocumentType.VID || context.type === DocumentType.WEB || context.type === DocumentType.PDF || context.type === DocumentType.IMG)) { const pushpin = Docs.Create.FontIconDocument({ title: "pushpin", label: "", annotationOn: Cast(doc.annotationOn, Doc, null), isPushpin: true, - icon: "map-pin", x: Cast(doc.x, "number", null), y: Cast(doc.y, "number", null), _backgroundColor: "#0000003d", color: "#ACCEF7", + icon: "map-pin", x: Cast(doc.x, "number", null), y: Cast(doc.y, "number", null), backgroundColor: "#0000003d", color: "#ACCEF7", _width: 15, _height: 15, _xPadding: 0, isLinkButton: true, _timecodeToShow: Cast(doc._timecodeToShow, "number", null) }); Doc.SetInPlace(doc, "annotationOn", undefined, true); diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx index b8794d6bb..106129127 100644 --- a/src/client/views/collections/TreeView.tsx +++ b/src/client/views/collections/TreeView.tsx @@ -218,7 +218,7 @@ export class TreeView extends React.Component { layout: CollectionView.LayoutString("data"), title: "-title-", "sidebarColor": "transparent", "sidebarViewType": CollectionViewType.Freeform, _viewType: CollectionViewType.Tree, hideLinkButton: true, _showSidebar: true, treeViewType: "outline", - x: 0, y: 0, _xMargin: 0, _yMargin: 0, _autoHeight: true, _singleLine: true, _backgroundColor: "transparent", _width: 1000, _height: 10 + x: 0, y: 0, _xMargin: 0, _yMargin: 0, _autoHeight: true, _singleLine: true, backgroundColor: "transparent", _width: 1000, _height: 10 }); Doc.GetProto(bullet).title = ComputedField.MakeFunction('self.text?.Text'); Doc.GetProto(bullet).data = new List([]); diff --git a/src/client/views/nodes/EquationBox.tsx b/src/client/views/nodes/EquationBox.tsx index 9111cb418..dacafcdf4 100644 --- a/src/client/views/nodes/EquationBox.tsx +++ b/src/client/views/nodes/EquationBox.tsx @@ -65,7 +65,7 @@ export class EquationBox extends ViewBoxBaseComponent Date: Sun, 7 Mar 2021 21:20:53 -0500 Subject: added start of Git functionality within Dash. --- src/client/views/DocumentDecorations.tsx | 13 ++--- src/client/views/collections/CollectionView.tsx | 71 ++++++++++++++++++++++++- src/fields/Doc.ts | 25 ++++----- 3 files changed, 90 insertions(+), 19 deletions(-) (limited to 'src') diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 44d4460fa..aeb2d582b 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -10,7 +10,7 @@ import { InkField } from "../../fields/InkField"; import { ScriptField } from '../../fields/ScriptField'; import { Cast, NumCast } from "../../fields/Types"; import { GetEffectiveAcl } from '../../fields/util'; -import { setupMoveUpEvents, emptyFunction } from "../../Utils"; +import { setupMoveUpEvents, emptyFunction, returnFalse } from "../../Utils"; import { Docs, DocUtils } from "../documents/Documents"; import { DocumentType } from '../documents/DocumentTypes'; import { CurrentUserUtils } from '../util/CurrentUserUtils'; @@ -129,7 +129,7 @@ export class DocumentDecorations extends React.Component<{ boundsLeft: number, b } @undoBatch @action - onMaximizeClick = (e: React.MouseEvent): void => { + onMaximizeClick = (e: any): void => { const selectedDocs = SelectionManager.Views(); if (selectedDocs.length) { if (e.ctrlKey) { // open an alias in a new tab with Ctrl Key @@ -152,12 +152,12 @@ export class DocumentDecorations extends React.Component<{ boundsLeft: number, b } @undoBatch - onIconifyClick = (e: React.MouseEvent): void => { + onIconifyClick = (): void => { SelectionManager.Views().forEach(dv => dv?.iconify()); SelectionManager.DeselectAll(); } - onSelectorClick = (e: React.MouseEvent) => SelectionManager.Views()?.[0]?.props.ContainingCollectionView?.props.select(false); + onSelectorClick = () => SelectionManager.Views()?.[0]?.props.ContainingCollectionView?.props.select(false); onRadiusDown = (e: React.PointerEvent): void => { this._resizeUndo = UndoManager.StartBatch("DocDecs set radius"); @@ -413,9 +413,10 @@ export class DocumentDecorations extends React.Component<{ boundsLeft: number, b return (!docView.rootDoc._stayInCollection || docView.rootDoc.isInkMask) && (collectionAcl === AclAdmin || collectionAcl === AclEdit || GetEffectiveAcl(docView.rootDoc) === AclAdmin); }); - const topBtn = (key: string, icon: string, click: (e: React.MouseEvent) => void, title: string) => ( + const topBtn = (key: string, icon: string, click: (e: any) => void, title: string) => ( {title}} placement="top"> -
{ e.preventDefault(); e.stopPropagation(); }} onClick={click}> +
e.preventDefault()} + onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, click, emptyFunction)} >
); diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 18abd3a88..b8fc7786d 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -3,7 +3,7 @@ import { observer } from "mobx-react"; import * as React from 'react'; import 'react-image-lightbox-with-rotate/style.css'; // This only needs to be imported once in your app import { DateField } from '../../../fields/DateField'; -import { AclAddonly, AclAdmin, AclEdit, AclPrivate, AclReadonly, AclSym, DataSym, Doc, DocListCast } from '../../../fields/Doc'; +import { AclAddonly, AclAdmin, AclEdit, AclPrivate, AclReadonly, AclSym, DataSym, Doc, DocListCast, DocListCastAsync } from '../../../fields/Doc'; import { Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; import { ObjectField } from '../../../fields/ObjectField'; @@ -277,6 +277,65 @@ export class CollectionView extends Touchable { } } + // pulls documents onto a branch from the branch's master + // if a document exists on master but not on the branch, it is branched and added + // NOTE/TODO: if a document is deleted on master, pulling master should delete the document from the branch + // + static pullFromMaster = async (branch: Doc, suffix = "") => { + const masterMain = Cast(branch.branchOf, Doc, null); + // get the set of documents on both the branch and master + const masterMainDocs = await DocListCastAsync(masterMain[Doc.LayoutFieldKey(masterMain) + suffix]); + const branchMainDocs = await DocListCastAsync(branch[Doc.LayoutFieldKey(branch) + suffix]); + // get the master documents that correspond to the branch documents + const branchMasterMainDocs = branchMainDocs?.map(bd => Cast(bd.branchOf, Doc, null) || bd).map(doc => Doc.GetProto(doc)); + // get documents on master that don't have a corresponding master doc (form a branch doc), and ... + const newDocsFromMaster = masterMainDocs?.filter(md => !branchMasterMainDocs?.includes(Doc.GetProto(md))); + // make branch clones of them, then add them to the branch + const newlyBranchedDocs = await Promise.all(newDocsFromMaster?.map(async md => (await Doc.MakeClone(md, false, true)).clone) || []); + newlyBranchedDocs.forEach(nd => Doc.AddDocToList(branch, Doc.LayoutFieldKey(branch) + suffix, nd)); + } + + // merges all branches from the master branch by first merging the top-level collection of documents, + // and then merging all the annotations on those documents. + // NOTE: "merging" only means making sure that documents in the branches are on master -- it does not + // currently update the state of those documents to be identical. + // TODO: deleting a document on a branch should remove it from master (but doesn't yet). + static mergeWithMaster = async (master: Doc, suffix = "") => { + const branches = await DocListCastAsync(master.branches); + branches?.map(async branch => { + const branchChildren = await DocListCastAsync(branch[Doc.LayoutFieldKey(branch) + suffix]); + branchChildren?.forEach(async bd => { + // see if the branch's child exists on master. + const masterChild = Cast(bd.branchOf, Doc, null) || (await Doc.MakeClone(bd, false, true)).clone; + // if the branch's child didn't exist on master, we make a branch clone of the child to add to master. + // however, since master is supposed to have the "main" clone, and branches, the "branch" clones, we have to reverse the fields + // on the branch child and master clone. + if (masterChild.branchOf) { + const branchDocProto = Doc.GetProto(bd); + const masterChildProto = Doc.GetProto(masterChild); + masterChildProto.branchOf = undefined; // the master child should not be a branch of the branch child, so unset 'branchOf' + masterChildProto.branches = new List([bd]); // the master child's branches needs to include the branch child + Doc.RemoveDocFromList(branchDocProto, "branches", masterChildProto); // the branch child should not have the master child in its branch list. + branchDocProto.branchOf = masterChild; // the branch child is now a branch of the master child + } + Doc.AddDocToList(master, Doc.LayoutFieldKey(master) + suffix, masterChild); // add the masterChild to master (if it's already there, this is a no-op) + }); + }); + } + + // performs a "git"-like task: pull or merge + // if pull, then target is a specific branch document that will be updated from its associated master + // if merge, then target is the master doc that will merge in all branches associated with it. + // TODO: parameterize 'merge' to specify which branch(es) should be merged. + // extend 'merge' to allow a specific branch to be merge target (not just master); + // make pull/merge be recursive (ie, this func currently just operates on the main doc and its children) + static async GitTask(target: Doc, action: "pull" | "merge") { + const func = action === "pull" ? CollectionView.pullFromMaster : CollectionView.mergeWithMaster; + await func(target, ""); + const targetChildren = await DocListCast(target[Doc.LayoutFieldKey(target)]); + targetChildren.forEach(async targetChild => await func(targetChild, "-annotations")); + } + onContextMenu = (e: React.MouseEvent): void => { const cm = ContextMenu.Instance; if (cm && !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 @@ -298,6 +357,16 @@ export class CollectionView extends Touchable { } !Doc.UserDoc().noviceMode && optionItems.push({ description: `${this.props.Document.isInPlaceContainer ? "Unset" : "Set"} inPlace Container`, event: () => this.props.Document.isInPlaceContainer = !this.props.Document.isInPlaceContainer, icon: "project-diagram" }); + optionItems.push({ + description: "Create Branch", event: async () => this.props.addDocTab((await Doc.MakeClone(this.props.Document, false, true)).clone, "add:right"), icon: "project-diagram" + }); + optionItems.push({ + description: "Pull Master", event: () => CollectionView.GitTask(this.props.Document, "pull"), icon: "project-diagram" + }); + optionItems.push({ + description: "Merge Branches", event: () => CollectionView.GitTask(this.props.Document, "merge"), icon: "project-diagram" + }); + !options && cm.addItem({ description: "Options...", subitems: optionItems, icon: "hand-point-right" }); if (!Doc.UserDoc().noviceMode && !this.props.Document.annotationOn) { diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index e24821116..c82c05c28 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -503,13 +503,13 @@ export namespace Doc { return alias; } - export async function makeClone(doc: Doc, cloneMap: Map, rtfs: { copy: Doc, key: string, field: RichTextField }[], exclusions: string[], dontCreate: boolean): Promise { + export async function makeClone(doc: Doc, cloneMap: Map, rtfs: { copy: Doc, key: string, field: RichTextField }[], exclusions: string[], dontCreate: boolean, asBranch: boolean): Promise { if (Doc.IsBaseProto(doc)) return doc; if (cloneMap.get(doc[Id])) return cloneMap.get(doc[Id])!; - const copy = dontCreate ? doc : new Doc(undefined, true); + const copy = dontCreate ? asBranch ? (Cast(doc.branchMaster, Doc, null) || doc) : doc : new Doc(undefined, true); cloneMap.set(doc[Id], copy); if (LinkManager.Instance.getAllLinks().includes(doc) && LinkManager.Instance.getAllLinks().indexOf(copy) === -1) LinkManager.Instance.addLink(copy); - const filter = Cast(doc.cloneFieldFilter, listSpec("string"), exclusions); + const filter = Cast(doc.cloneFieldFilter, listSpec("string"), ["branches", ...exclusions]); await Promise.all(Object.keys(doc).map(async key => { if (filter.includes(key)) return; const assignKey = (val: any) => !dontCreate && (copy[key] = val); @@ -519,12 +519,12 @@ export namespace Doc { const list = Cast(doc[key], listSpec(Doc)); const docs = list && (await DocListCastAsync(list))?.filter(d => d instanceof Doc); if (docs !== undefined && docs.length) { - const clones = await Promise.all(docs.map(async d => Doc.makeClone(d, cloneMap, rtfs, exclusions, dontCreate))); + const clones = await Promise.all(docs.map(async d => Doc.makeClone(d, cloneMap, rtfs, exclusions, dontCreate, asBranch))); !dontCreate && assignKey(new List(clones)); } else if (doc[key] instanceof Doc) { - assignKey(key.includes("layout[") ? undefined : key.startsWith("layout") ? doc[key] as Doc : await Doc.makeClone(doc[key] as Doc, cloneMap, rtfs, exclusions, dontCreate)); // reference documents except copy documents that are expanded teplate fields + assignKey(key.includes("layout[") ? undefined : key.startsWith("layout") ? doc[key] as Doc : await Doc.makeClone(doc[key] as Doc, cloneMap, rtfs, exclusions, dontCreate, asBranch)); // reference documents except copy documents that are expanded teplate fields } else { - assignKey(ObjectField.MakeCopy(field)); + !dontCreate && assignKey(ObjectField.MakeCopy(field)); if (field instanceof RichTextField) { if (field.Data.includes('"docid":') || field.Data.includes('"targetId":') || field.Data.includes('"linkId":')) { rtfs.push({ copy, key, field }); @@ -534,7 +534,7 @@ export namespace Doc { }; if (key === "proto") { if (doc[key] instanceof Doc) { - assignKey(await Doc.makeClone(doc[key]!, cloneMap, rtfs, exclusions, dontCreate)); + assignKey(await Doc.makeClone(doc[key]!, cloneMap, rtfs, exclusions, dontCreate, asBranch)); } } else { if (field instanceof RefField) { @@ -552,16 +552,17 @@ export namespace Doc { } })); if (!dontCreate) { - Doc.SetInPlace(copy, "title", "CLONE: " + doc.title, true); - copy.cloneOf = doc; + Doc.SetInPlace(copy, "title", (asBranch ? "BRANCH: " : "CLONE: ") + doc.title, true); + asBranch ? (copy.branchOf = doc) : (copy.cloneOf = doc); + if (!Doc.IsPrototype(copy)) Doc.AddDocToList(doc, "branches", Doc.GetProto(copy)); cloneMap.set(doc[Id], copy); } return copy; } - export async function MakeClone(doc: Doc, dontCreate: boolean = false) { + export async function MakeClone(doc: Doc, dontCreate: boolean = false, asBranch = false) { const cloneMap = new Map(); const rtfMap: { copy: Doc, key: string, field: RichTextField }[] = []; - const copy = await Doc.makeClone(doc, cloneMap, rtfMap, ["context", "annotationOn", "cloneOf"], dontCreate); + const copy = await Doc.makeClone(doc, cloneMap, rtfMap, ["context", "annotationOn", "cloneOf", "branchOf"], dontCreate, asBranch); rtfMap.map(({ copy, key, field }) => { const replacer = (match: any, attr: string, id: string, offset: any, string: any) => { const mapped = cloneMap.get(id); @@ -586,7 +587,7 @@ export namespace Doc { // a.click(); const { clone, map } = await Doc.MakeClone(doc, true); function replacer(key: any, value: any) { - if (["cloneOf", "context", "cursors"].includes(key)) return undefined; + if (["branchOf", "cloneOf", "context", "cursors"].includes(key)) return undefined; else if (value instanceof Doc) { if (key !== "field" && Number.isNaN(Number(key))) { const __fields = value[FieldsSym](); -- cgit v1.2.3-70-g09d2 From ca6e8145c5883e32f4cc65edbfff245cdde326ed Mon Sep 17 00:00:00 2001 From: bobzel Date: Sun, 7 Mar 2021 23:22:39 -0500 Subject: added removal of docs to git-like actions. --- src/client/views/DocComponent.tsx | 1 + src/client/views/collections/CollectionView.tsx | 29 +++++++++++++++++++------ 2 files changed, 23 insertions(+), 7 deletions(-) (limited to 'src') diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx index 128ba858f..6480c6507 100644 --- a/src/client/views/DocComponent.tsx +++ b/src/client/views/DocComponent.tsx @@ -144,6 +144,7 @@ export function ViewBoxAnnotatableComponent

{ Doc.RemoveDocFromList(targetDataDoc, annotationKey ?? this.annotationKey, doc); + doc.context = undefined; recent && Doc.AddDocToList(recent, "data", doc, undefined, true, true); }); this.props.select(false); diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index b8fc7786d..c1db8fd0b 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -279,27 +279,34 @@ export class CollectionView extends Touchable { // pulls documents onto a branch from the branch's master // if a document exists on master but not on the branch, it is branched and added - // NOTE/TODO: if a document is deleted on master, pulling master should delete the document from the branch - // + // NOTE: need to set a timestamp on the branch that is equal to the master's last merge timestamp. static pullFromMaster = async (branch: Doc, suffix = "") => { const masterMain = Cast(branch.branchOf, Doc, null); // get the set of documents on both the branch and master - const masterMainDocs = await DocListCastAsync(masterMain[Doc.LayoutFieldKey(masterMain) + suffix]); + const masterMainDocs = masterMain && await DocListCastAsync(masterMain[Doc.LayoutFieldKey(masterMain) + suffix]); const branchMainDocs = await DocListCastAsync(branch[Doc.LayoutFieldKey(branch) + suffix]); // get the master documents that correspond to the branch documents - const branchMasterMainDocs = branchMainDocs?.map(bd => Cast(bd.branchOf, Doc, null) || bd).map(doc => Doc.GetProto(doc)); + const branchMasterMainDocs = branchMainDocs?.map(bd => Cast(bd.branchOf, Doc, null) || bd); + const branchMasterMainDocProtos = branchMasterMainDocs?.map(doc => Doc.GetProto(doc)); // get documents on master that don't have a corresponding master doc (form a branch doc), and ... - const newDocsFromMaster = masterMainDocs?.filter(md => !branchMasterMainDocs?.includes(Doc.GetProto(md))); + const newDocsFromMaster = masterMainDocs?.filter(md => !branchMasterMainDocProtos?.includes(Doc.GetProto(md))); // make branch clones of them, then add them to the branch const newlyBranchedDocs = await Promise.all(newDocsFromMaster?.map(async md => (await Doc.MakeClone(md, false, true)).clone) || []); - newlyBranchedDocs.forEach(nd => Doc.AddDocToList(branch, Doc.LayoutFieldKey(branch) + suffix, nd)); + newlyBranchedDocs.forEach(nd => { + Doc.AddDocToList(branch, Doc.LayoutFieldKey(branch) + suffix, nd); + nd.context = branch; + }); + // if a branch doc's corresponding main branch doc doesn't have a context, then it was deleted. + const remDocsFromMaster = branchMainDocs?.filter(bd => Cast(bd.branchOf, Doc, null) && !Cast(bd.branchOf, Doc, null)?.context); + // so then remove all the deleted main docs from this branch. + remDocsFromMaster?.forEach(rd => Doc.RemoveDocFromList(branch, Doc.LayoutFieldKey(branch) + suffix, rd)); } // merges all branches from the master branch by first merging the top-level collection of documents, // and then merging all the annotations on those documents. // NOTE: "merging" only means making sure that documents in the branches are on master -- it does not // currently update the state of those documents to be identical. - // TODO: deleting a document on a branch should remove it from master (but doesn't yet). + // TODO: need to add an incrementing timestamp whenever anything merges. don't allow a branch to merge if it's last pull timestamp isn't equal to the last merge timestamp. static mergeWithMaster = async (master: Doc, suffix = "") => { const branches = await DocListCastAsync(master.branches); branches?.map(async branch => { @@ -319,6 +326,14 @@ export class CollectionView extends Touchable { branchDocProto.branchOf = masterChild; // the branch child is now a branch of the master child } Doc.AddDocToList(master, Doc.LayoutFieldKey(master) + suffix, masterChild); // add the masterChild to master (if it's already there, this is a no-op) + masterChild.context = master; + }); + const masterChildren = await DocListCastAsync(master[Doc.LayoutFieldKey(master) + suffix]); + masterChildren?.forEach(async mc => { // see if any master children + if (!branchChildren?.find(bc => bc.branchOf === mc)) { // are not in the list of children for this branch. + Doc.RemoveDocFromList(master, Doc.LayoutFieldKey(master) + suffix, mc); // if so, delete the master child since the branch has deleted it. + mc.context = undefined; // NOTE if we merge a branch that didn't do a pull, it will look like the branch deleted documents -- need edit timestamps that prevent merging if branch isn't up-to-date with last edit timestamp + } }); }); } -- cgit v1.2.3-70-g09d2 From 94f2e575b95c6edd5410d440f7410d712c90b5db Mon Sep 17 00:00:00 2001 From: bobzel Date: Mon, 8 Mar 2021 10:00:36 -0500 Subject: added doc data content synching to Git-like operations. --- src/client/util/CurrentUserUtils.ts | 2 +- src/client/util/GroupManager.tsx | 8 ++++---- src/client/views/collections/CollectionView.tsx | 27 +++++++++++++++++++++---- 3 files changed, 28 insertions(+), 9 deletions(-) (limited to 'src') diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 8b425f929..ccdfeed5f 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -988,7 +988,7 @@ export class CurrentUserUtils { static async updateUserDocument(doc: Doc, sharingDocumentId: string, linkDatabaseId: string) { if (!doc.globalGroupDatabase) doc.globalGroupDatabase = Docs.Prototypes.MainGroupDocument(); const groups = await DocListCastAsync((doc.globalGroupDatabase as Doc).data); - reaction(() => DateCast((doc.globalGroupDatabase as Doc).lastModified), + reaction(() => DateCast((doc.globalGroupDatabase as Doc)["data-lastModified"]), async () => { const groups = await DocListCastAsync((doc.globalGroupDatabase as Doc).data); const mygroups = groups?.filter(group => JSON.parse(StrCast(group.members)).includes(Doc.CurrentUserEmail)) || []; diff --git a/src/client/util/GroupManager.tsx b/src/client/util/GroupManager.tsx index 6458de0ed..68af67a3b 100644 --- a/src/client/util/GroupManager.tsx +++ b/src/client/util/GroupManager.tsx @@ -146,7 +146,7 @@ export class GroupManager extends React.Component<{}> { addGroup(groupDoc: Doc): boolean { if (this.GroupManagerDoc) { Doc.AddDocToList(this.GroupManagerDoc, "data", groupDoc); - this.GroupManagerDoc.lastModified = new DateField; + this.GroupManagerDoc["data-lastModified"] = new DateField; return true; } return false; @@ -167,7 +167,7 @@ export class GroupManager extends React.Component<{}> { const index = DocListCast(this.GroupManagerDoc.data).findIndex(grp => grp === group); index !== -1 && Cast(this.GroupManagerDoc.data, listSpec(Doc), [])?.splice(index, 1); } - this.GroupManagerDoc.lastModified = new DateField; + this.GroupManagerDoc["data-lastModified"] = new DateField; if (group === this.currentGroup) { this.currentGroup = undefined; } @@ -188,7 +188,7 @@ export class GroupManager extends React.Component<{}> { !memberList.includes(email) && memberList.push(email); groupDoc.members = JSON.stringify(memberList); SharingManager.Instance.shareWithAddedMember(groupDoc, email); - this.GroupManagerDoc && (this.GroupManagerDoc.lastModified = new DateField); + this.GroupManagerDoc && (this.GroupManagerDoc["data-lastModified"] = new DateField); } } @@ -205,7 +205,7 @@ export class GroupManager extends React.Component<{}> { const user = memberList.splice(index, 1)[0]; groupDoc.members = JSON.stringify(memberList); SharingManager.Instance.removeMember(groupDoc, email); - this.GroupManagerDoc && (this.GroupManagerDoc.lastModified = new DateField); + this.GroupManagerDoc && (this.GroupManagerDoc["data-lastModified"] = new DateField); } } } diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index c1db8fd0b..5d1e8b41c 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -3,12 +3,12 @@ import { observer } from "mobx-react"; import * as React from 'react'; import 'react-image-lightbox-with-rotate/style.css'; // This only needs to be imported once in your app import { DateField } from '../../../fields/DateField'; -import { AclAddonly, AclAdmin, AclEdit, AclPrivate, AclReadonly, AclSym, DataSym, Doc, DocListCast, DocListCastAsync } from '../../../fields/Doc'; +import { AclAddonly, AclAdmin, AclEdit, AclPrivate, AclReadonly, AclSym, DataSym, Doc, DocListCast, DocListCastAsync, Field } from '../../../fields/Doc'; import { Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; import { ObjectField } from '../../../fields/ObjectField'; import { ScriptField } from '../../../fields/ScriptField'; -import { Cast, ScriptCast, StrCast } from '../../../fields/Types'; +import { Cast, ScriptCast, StrCast, DateCast } from '../../../fields/Types'; import { denormalizeEmail, distributeAcls, GetEffectiveAcl, SharingPermissions, TraceMobx } from '../../../fields/util'; import { returnFalse } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; @@ -277,6 +277,21 @@ export class CollectionView extends Touchable { } } + // synchs matching documents on the two branches that are being merged/pulled + // currently this just synchs the main 'fieldKey' component of the data since + // we don't have individual timestamps for all fields -- this is a problematic design issue. + static synchDocs(bd: Doc, md: Doc) { + const fieldKey = Doc.LayoutFieldKey(md); + let bdate = DateCast(bd[`${fieldKey}-lastModified`])?.date; + let mdate = DateCast(md[`${fieldKey}-lastModified`])?.date; + if (bdate === mdate || bdate > mdate) return; + const bdproto = bd && Doc.GetProto(bd); + if (bdproto && md) { + bdproto[fieldKey] = ObjectField.MakeCopy(md[fieldKey] as ObjectField); + bdproto[`${fieldKey}-lastModified`] = ObjectField.MakeCopy(md[`${fieldKey}-lastModified`] as ObjectField); + } + } + // pulls documents onto a branch from the branch's master // if a document exists on master but not on the branch, it is branched and added // NOTE: need to set a timestamp on the branch that is equal to the master's last merge timestamp. @@ -290,6 +305,11 @@ export class CollectionView extends Touchable { const branchMasterMainDocProtos = branchMasterMainDocs?.map(doc => Doc.GetProto(doc)); // get documents on master that don't have a corresponding master doc (form a branch doc), and ... const newDocsFromMaster = masterMainDocs?.filter(md => !branchMasterMainDocProtos?.includes(Doc.GetProto(md))); + const oldDocsFromMaster = masterMainDocs?.filter(md => branchMasterMainDocProtos?.includes(Doc.GetProto(md))); + oldDocsFromMaster?.forEach(md => { + const bd = branchMainDocs?.find(bd => (Cast(bd.branchOf, Doc, null) || bd) === md); + bd && CollectionView.synchDocs(bd, md); + }) // make branch clones of them, then add them to the branch const newlyBranchedDocs = await Promise.all(newDocsFromMaster?.map(async md => (await Doc.MakeClone(md, false, true)).clone) || []); newlyBranchedDocs.forEach(nd => { @@ -304,8 +324,6 @@ export class CollectionView extends Touchable { // merges all branches from the master branch by first merging the top-level collection of documents, // and then merging all the annotations on those documents. - // NOTE: "merging" only means making sure that documents in the branches are on master -- it does not - // currently update the state of those documents to be identical. // TODO: need to add an incrementing timestamp whenever anything merges. don't allow a branch to merge if it's last pull timestamp isn't equal to the last merge timestamp. static mergeWithMaster = async (master: Doc, suffix = "") => { const branches = await DocListCastAsync(master.branches); @@ -327,6 +345,7 @@ export class CollectionView extends Touchable { } Doc.AddDocToList(master, Doc.LayoutFieldKey(master) + suffix, masterChild); // add the masterChild to master (if it's already there, this is a no-op) masterChild.context = master; + CollectionView.synchDocs(Doc.GetProto(masterChild), bd); }); const masterChildren = await DocListCastAsync(master[Doc.LayoutFieldKey(master) + suffix]); masterChildren?.forEach(async mc => { // see if any master children -- cgit v1.2.3-70-g09d2 From 17da55a9b6225ee656aa2eb4dc167dc2d60cba52 Mon Sep 17 00:00:00 2001 From: bobzel Date: Mon, 8 Mar 2021 10:30:24 -0500 Subject: split gitlike stuff into its own file --- src/client/documents/Gitlike.ts | 101 ++++++++++++++++++++++++ src/client/views/collections/CollectionView.tsx | 100 +---------------------- 2 files changed, 105 insertions(+), 96 deletions(-) create mode 100644 src/client/documents/Gitlike.ts (limited to 'src') diff --git a/src/client/documents/Gitlike.ts b/src/client/documents/Gitlike.ts new file mode 100644 index 000000000..0ab2df8cd --- /dev/null +++ b/src/client/documents/Gitlike.ts @@ -0,0 +1,101 @@ +import { Doc, DocListCast, DocListCastAsync } from "../../fields/Doc"; +import { List } from "../../fields/List"; +import { ObjectField } from "../../fields/ObjectField"; +import { Cast, DateCast } from "../../fields/Types"; + +// synchs matching documents on the two branches that are being merged/pulled +// currently this just synchs the main 'fieldKey' component of the data since +// we don't have individual timestamps for all fields -- this is a problematic design issue. +function GitlikeSynchDocs(bd: Doc, md: Doc) { + const fieldKey = Doc.LayoutFieldKey(md); + let bdate = DateCast(bd[`${fieldKey}-lastModified`])?.date; + let mdate = DateCast(md[`${fieldKey}-lastModified`])?.date; + if (bdate === mdate || bdate > mdate) return; + const bdproto = bd && Doc.GetProto(bd); + if (bdproto && md) { + bdproto[fieldKey] = ObjectField.MakeCopy(md[fieldKey] as ObjectField); + bdproto[`${fieldKey}-lastModified`] = ObjectField.MakeCopy(md[`${fieldKey}-lastModified`] as ObjectField); + } +} + +// pulls documents onto a branch from the branch's master +// if a document exists on master but not on the branch, it is branched and added +// NOTE: need to set a timestamp on the branch that is equal to the master's last merge timestamp. +async function GitlikePullFromMaster(branch: Doc, suffix = "") { + const masterMain = Cast(branch.branchOf, Doc, null); + // get the set of documents on both the branch and master + const masterMainDocs = masterMain && await DocListCastAsync(masterMain[Doc.LayoutFieldKey(masterMain) + suffix]); + const branchMainDocs = await DocListCastAsync(branch[Doc.LayoutFieldKey(branch) + suffix]); + // get the master documents that correspond to the branch documents + const branchMasterMainDocs = branchMainDocs?.map(bd => Cast(bd.branchOf, Doc, null) || bd); + const branchMasterMainDocProtos = branchMasterMainDocs?.map(doc => Doc.GetProto(doc)); + // get documents on master that don't have a corresponding master doc (form a branch doc), and ... + const newDocsFromMaster = masterMainDocs?.filter(md => !branchMasterMainDocProtos?.includes(Doc.GetProto(md))); + const oldDocsFromMaster = masterMainDocs?.filter(md => branchMasterMainDocProtos?.includes(Doc.GetProto(md))); + oldDocsFromMaster?.forEach(md => { + const bd = branchMainDocs?.find(bd => (Cast(bd.branchOf, Doc, null) || bd) === md); + bd && GitlikeSynchDocs(bd, md); + }) + // make branch clones of them, then add them to the branch + const newlyBranchedDocs = await Promise.all(newDocsFromMaster?.map(async md => (await Doc.MakeClone(md, false, true)).clone) || []); + newlyBranchedDocs.forEach(nd => { + Doc.AddDocToList(branch, Doc.LayoutFieldKey(branch) + suffix, nd); + nd.context = branch; + }); + // if a branch doc's corresponding main branch doc doesn't have a context, then it was deleted. + const remDocsFromMaster = branchMainDocs?.filter(bd => Cast(bd.branchOf, Doc, null) && !Cast(bd.branchOf, Doc, null)?.context); + // so then remove all the deleted main docs from this branch. + remDocsFromMaster?.forEach(rd => Doc.RemoveDocFromList(branch, Doc.LayoutFieldKey(branch) + suffix, rd)); +} + +// merges all branches from the master branch by first merging the top-level collection of documents, +// and then merging all the annotations on those documents. +// TODO: need to add an incrementing timestamp whenever anything merges. don't allow a branch to merge if it's last pull timestamp isn't equal to the last merge timestamp. +async function GitlikeMergeWithMaster(master: Doc, suffix = "") { + const branches = await DocListCastAsync(master.branches); + branches?.map(async branch => { + const branchChildren = await DocListCastAsync(branch[Doc.LayoutFieldKey(branch) + suffix]); + branchChildren?.forEach(async bd => { + // see if the branch's child exists on master. + const masterChild = Cast(bd.branchOf, Doc, null) || (await Doc.MakeClone(bd, false, true)).clone; + // if the branch's child didn't exist on master, we make a branch clone of the child to add to master. + // however, since master is supposed to have the "main" clone, and branches, the "branch" clones, we have to reverse the fields + // on the branch child and master clone. + if (masterChild.branchOf) { + const branchDocProto = Doc.GetProto(bd); + const masterChildProto = Doc.GetProto(masterChild); + masterChildProto.branchOf = undefined; // the master child should not be a branch of the branch child, so unset 'branchOf' + masterChildProto.branches = new List([bd]); // the master child's branches needs to include the branch child + Doc.RemoveDocFromList(branchDocProto, "branches", masterChildProto); // the branch child should not have the master child in its branch list. + branchDocProto.branchOf = masterChild; // the branch child is now a branch of the master child + } + Doc.AddDocToList(master, Doc.LayoutFieldKey(master) + suffix, masterChild); // add the masterChild to master (if it's already there, this is a no-op) + masterChild.context = master; + GitlikeSynchDocs(Doc.GetProto(masterChild), bd); + }); + const masterChildren = await DocListCastAsync(master[Doc.LayoutFieldKey(master) + suffix]); + masterChildren?.forEach(async mc => { // see if any master children + if (!branchChildren?.find(bc => bc.branchOf === mc)) { // are not in the list of children for this branch. + Doc.RemoveDocFromList(master, Doc.LayoutFieldKey(master) + suffix, mc); // if so, delete the master child since the branch has deleted it. + mc.context = undefined; // NOTE if we merge a branch that didn't do a pull, it will look like the branch deleted documents -- need edit timestamps that prevent merging if branch isn't up-to-date with last edit timestamp + } + }); + }); +} + +// performs a "git"-like task: pull or merge +// if pull, then target is a specific branch document that will be updated from its associated master +// if merge, then target is the master doc that will merge in all branches associated with it. +// TODO: parameterize 'merge' to specify which branch(es) should be merged. +// extend 'merge' to allow a specific branch to be merge target (not just master); +// make pull/merge be recursive (ie, this func currently just operates on the main doc and its children) +export async function BranchTask(target: Doc, action: "pull" | "merge") { + const func = action === "pull" ? GitlikePullFromMaster : GitlikeMergeWithMaster; + await func(target, ""); + const targetChildren = await DocListCast(target[Doc.LayoutFieldKey(target)]); + targetChildren.forEach(async targetChild => await func(targetChild, "-annotations")); +} + +export async function BranchCreate(target: Doc) { + return (await Doc.MakeClone(target, false, true)).clone; +} \ No newline at end of file diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 5d1e8b41c..d652bd2a2 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -12,6 +12,7 @@ import { Cast, ScriptCast, StrCast, DateCast } from '../../../fields/Types'; import { denormalizeEmail, distributeAcls, GetEffectiveAcl, SharingPermissions, TraceMobx } from '../../../fields/util'; import { returnFalse } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; +import { BranchTask, BranchCreate } from '../../documents/Gitlike'; import { DocumentType } from '../../documents/DocumentTypes'; import { CurrentUserUtils } from '../../util/CurrentUserUtils'; import { ImageUtils } from '../../util/Import & Export/ImageUtils'; @@ -277,99 +278,6 @@ export class CollectionView extends Touchable { } } - // synchs matching documents on the two branches that are being merged/pulled - // currently this just synchs the main 'fieldKey' component of the data since - // we don't have individual timestamps for all fields -- this is a problematic design issue. - static synchDocs(bd: Doc, md: Doc) { - const fieldKey = Doc.LayoutFieldKey(md); - let bdate = DateCast(bd[`${fieldKey}-lastModified`])?.date; - let mdate = DateCast(md[`${fieldKey}-lastModified`])?.date; - if (bdate === mdate || bdate > mdate) return; - const bdproto = bd && Doc.GetProto(bd); - if (bdproto && md) { - bdproto[fieldKey] = ObjectField.MakeCopy(md[fieldKey] as ObjectField); - bdproto[`${fieldKey}-lastModified`] = ObjectField.MakeCopy(md[`${fieldKey}-lastModified`] as ObjectField); - } - } - - // pulls documents onto a branch from the branch's master - // if a document exists on master but not on the branch, it is branched and added - // NOTE: need to set a timestamp on the branch that is equal to the master's last merge timestamp. - static pullFromMaster = async (branch: Doc, suffix = "") => { - const masterMain = Cast(branch.branchOf, Doc, null); - // get the set of documents on both the branch and master - const masterMainDocs = masterMain && await DocListCastAsync(masterMain[Doc.LayoutFieldKey(masterMain) + suffix]); - const branchMainDocs = await DocListCastAsync(branch[Doc.LayoutFieldKey(branch) + suffix]); - // get the master documents that correspond to the branch documents - const branchMasterMainDocs = branchMainDocs?.map(bd => Cast(bd.branchOf, Doc, null) || bd); - const branchMasterMainDocProtos = branchMasterMainDocs?.map(doc => Doc.GetProto(doc)); - // get documents on master that don't have a corresponding master doc (form a branch doc), and ... - const newDocsFromMaster = masterMainDocs?.filter(md => !branchMasterMainDocProtos?.includes(Doc.GetProto(md))); - const oldDocsFromMaster = masterMainDocs?.filter(md => branchMasterMainDocProtos?.includes(Doc.GetProto(md))); - oldDocsFromMaster?.forEach(md => { - const bd = branchMainDocs?.find(bd => (Cast(bd.branchOf, Doc, null) || bd) === md); - bd && CollectionView.synchDocs(bd, md); - }) - // make branch clones of them, then add them to the branch - const newlyBranchedDocs = await Promise.all(newDocsFromMaster?.map(async md => (await Doc.MakeClone(md, false, true)).clone) || []); - newlyBranchedDocs.forEach(nd => { - Doc.AddDocToList(branch, Doc.LayoutFieldKey(branch) + suffix, nd); - nd.context = branch; - }); - // if a branch doc's corresponding main branch doc doesn't have a context, then it was deleted. - const remDocsFromMaster = branchMainDocs?.filter(bd => Cast(bd.branchOf, Doc, null) && !Cast(bd.branchOf, Doc, null)?.context); - // so then remove all the deleted main docs from this branch. - remDocsFromMaster?.forEach(rd => Doc.RemoveDocFromList(branch, Doc.LayoutFieldKey(branch) + suffix, rd)); - } - - // merges all branches from the master branch by first merging the top-level collection of documents, - // and then merging all the annotations on those documents. - // TODO: need to add an incrementing timestamp whenever anything merges. don't allow a branch to merge if it's last pull timestamp isn't equal to the last merge timestamp. - static mergeWithMaster = async (master: Doc, suffix = "") => { - const branches = await DocListCastAsync(master.branches); - branches?.map(async branch => { - const branchChildren = await DocListCastAsync(branch[Doc.LayoutFieldKey(branch) + suffix]); - branchChildren?.forEach(async bd => { - // see if the branch's child exists on master. - const masterChild = Cast(bd.branchOf, Doc, null) || (await Doc.MakeClone(bd, false, true)).clone; - // if the branch's child didn't exist on master, we make a branch clone of the child to add to master. - // however, since master is supposed to have the "main" clone, and branches, the "branch" clones, we have to reverse the fields - // on the branch child and master clone. - if (masterChild.branchOf) { - const branchDocProto = Doc.GetProto(bd); - const masterChildProto = Doc.GetProto(masterChild); - masterChildProto.branchOf = undefined; // the master child should not be a branch of the branch child, so unset 'branchOf' - masterChildProto.branches = new List([bd]); // the master child's branches needs to include the branch child - Doc.RemoveDocFromList(branchDocProto, "branches", masterChildProto); // the branch child should not have the master child in its branch list. - branchDocProto.branchOf = masterChild; // the branch child is now a branch of the master child - } - Doc.AddDocToList(master, Doc.LayoutFieldKey(master) + suffix, masterChild); // add the masterChild to master (if it's already there, this is a no-op) - masterChild.context = master; - CollectionView.synchDocs(Doc.GetProto(masterChild), bd); - }); - const masterChildren = await DocListCastAsync(master[Doc.LayoutFieldKey(master) + suffix]); - masterChildren?.forEach(async mc => { // see if any master children - if (!branchChildren?.find(bc => bc.branchOf === mc)) { // are not in the list of children for this branch. - Doc.RemoveDocFromList(master, Doc.LayoutFieldKey(master) + suffix, mc); // if so, delete the master child since the branch has deleted it. - mc.context = undefined; // NOTE if we merge a branch that didn't do a pull, it will look like the branch deleted documents -- need edit timestamps that prevent merging if branch isn't up-to-date with last edit timestamp - } - }); - }); - } - - // performs a "git"-like task: pull or merge - // if pull, then target is a specific branch document that will be updated from its associated master - // if merge, then target is the master doc that will merge in all branches associated with it. - // TODO: parameterize 'merge' to specify which branch(es) should be merged. - // extend 'merge' to allow a specific branch to be merge target (not just master); - // make pull/merge be recursive (ie, this func currently just operates on the main doc and its children) - static async GitTask(target: Doc, action: "pull" | "merge") { - const func = action === "pull" ? CollectionView.pullFromMaster : CollectionView.mergeWithMaster; - await func(target, ""); - const targetChildren = await DocListCast(target[Doc.LayoutFieldKey(target)]); - targetChildren.forEach(async targetChild => await func(targetChild, "-annotations")); - } - onContextMenu = (e: React.MouseEvent): void => { const cm = ContextMenu.Instance; if (cm && !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 @@ -392,13 +300,13 @@ export class CollectionView extends Touchable { !Doc.UserDoc().noviceMode && optionItems.push({ description: `${this.props.Document.isInPlaceContainer ? "Unset" : "Set"} inPlace Container`, event: () => this.props.Document.isInPlaceContainer = !this.props.Document.isInPlaceContainer, icon: "project-diagram" }); optionItems.push({ - description: "Create Branch", event: async () => this.props.addDocTab((await Doc.MakeClone(this.props.Document, false, true)).clone, "add:right"), icon: "project-diagram" + description: "Create Branch", event: async () => this.props.addDocTab(BranchCreate(this.props.Document), "add:right"), icon: "project-diagram" }); optionItems.push({ - description: "Pull Master", event: () => CollectionView.GitTask(this.props.Document, "pull"), icon: "project-diagram" + description: "Pull Master", event: () => BranchTask(this.props.Document, "pull"), icon: "project-diagram" }); optionItems.push({ - description: "Merge Branches", event: () => CollectionView.GitTask(this.props.Document, "merge"), icon: "project-diagram" + description: "Merge Branches", event: () => BranchTask(this.props.Document, "merge"), icon: "project-diagram" }); !options && cm.addItem({ description: "Options...", subitems: optionItems, icon: "hand-point-right" }); -- cgit v1.2.3-70-g09d2 From 7b3937698e5b22a84c623680e8c84164268bda8f Mon Sep 17 00:00:00 2001 From: bobzel Date: Mon, 8 Mar 2021 11:06:45 -0500 Subject: from last --- src/client/views/collections/CollectionView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index d652bd2a2..857982782 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -300,7 +300,7 @@ export class CollectionView extends Touchable { !Doc.UserDoc().noviceMode && optionItems.push({ description: `${this.props.Document.isInPlaceContainer ? "Unset" : "Set"} inPlace Container`, event: () => this.props.Document.isInPlaceContainer = !this.props.Document.isInPlaceContainer, icon: "project-diagram" }); optionItems.push({ - description: "Create Branch", event: async () => this.props.addDocTab(BranchCreate(this.props.Document), "add:right"), icon: "project-diagram" + description: "Create Branch", event: async () => this.props.addDocTab(await BranchCreate(this.props.Document), "add:right"), icon: "project-diagram" }); optionItems.push({ description: "Pull Master", event: () => BranchTask(this.props.Document, "pull"), icon: "project-diagram" -- cgit v1.2.3-70-g09d2 From 81cfcf5ab5caed1ab5cd052b3e8f00829631018f Mon Sep 17 00:00:00 2001 From: bobzel Date: Mon, 8 Mar 2021 12:03:32 -0500 Subject: fixed bug with branch/clone --- src/client/documents/Gitlike.ts | 15 +++++++-------- src/fields/Doc.ts | 9 ++++++--- 2 files changed, 13 insertions(+), 11 deletions(-) (limited to 'src') diff --git a/src/client/documents/Gitlike.ts b/src/client/documents/Gitlike.ts index 0ab2df8cd..fddf317bc 100644 --- a/src/client/documents/Gitlike.ts +++ b/src/client/documents/Gitlike.ts @@ -8,8 +8,8 @@ import { Cast, DateCast } from "../../fields/Types"; // we don't have individual timestamps for all fields -- this is a problematic design issue. function GitlikeSynchDocs(bd: Doc, md: Doc) { const fieldKey = Doc.LayoutFieldKey(md); - let bdate = DateCast(bd[`${fieldKey}-lastModified`])?.date; - let mdate = DateCast(md[`${fieldKey}-lastModified`])?.date; + const bdate = DateCast(bd[`${fieldKey}-lastModified`])?.date; + const mdate = DateCast(md[`${fieldKey}-lastModified`])?.date; if (bdate === mdate || bdate > mdate) return; const bdproto = bd && Doc.GetProto(bd); if (bdproto && md) { @@ -35,7 +35,7 @@ async function GitlikePullFromMaster(branch: Doc, suffix = "") { oldDocsFromMaster?.forEach(md => { const bd = branchMainDocs?.find(bd => (Cast(bd.branchOf, Doc, null) || bd) === md); bd && GitlikeSynchDocs(bd, md); - }) + }); // make branch clones of them, then add them to the branch const newlyBranchedDocs = await Promise.all(newDocsFromMaster?.map(async md => (await Doc.MakeClone(md, false, true)).clone) || []); newlyBranchedDocs.forEach(nd => { @@ -55,7 +55,7 @@ async function GitlikeMergeWithMaster(master: Doc, suffix = "") { const branches = await DocListCastAsync(master.branches); branches?.map(async branch => { const branchChildren = await DocListCastAsync(branch[Doc.LayoutFieldKey(branch) + suffix]); - branchChildren?.forEach(async bd => { + branchChildren && await Promise.all(branchChildren.map(async bd => { // see if the branch's child exists on master. const masterChild = Cast(bd.branchOf, Doc, null) || (await Doc.MakeClone(bd, false, true)).clone; // if the branch's child didn't exist on master, we make a branch clone of the child to add to master. @@ -72,9 +72,9 @@ async function GitlikeMergeWithMaster(master: Doc, suffix = "") { Doc.AddDocToList(master, Doc.LayoutFieldKey(master) + suffix, masterChild); // add the masterChild to master (if it's already there, this is a no-op) masterChild.context = master; GitlikeSynchDocs(Doc.GetProto(masterChild), bd); - }); + })); const masterChildren = await DocListCastAsync(master[Doc.LayoutFieldKey(master) + suffix]); - masterChildren?.forEach(async mc => { // see if any master children + masterChildren?.forEach(mc => { // see if any master children if (!branchChildren?.find(bc => bc.branchOf === mc)) { // are not in the list of children for this branch. Doc.RemoveDocFromList(master, Doc.LayoutFieldKey(master) + suffix, mc); // if so, delete the master child since the branch has deleted it. mc.context = undefined; // NOTE if we merge a branch that didn't do a pull, it will look like the branch deleted documents -- need edit timestamps that prevent merging if branch isn't up-to-date with last edit timestamp @@ -92,8 +92,7 @@ async function GitlikeMergeWithMaster(master: Doc, suffix = "") { export async function BranchTask(target: Doc, action: "pull" | "merge") { const func = action === "pull" ? GitlikePullFromMaster : GitlikeMergeWithMaster; await func(target, ""); - const targetChildren = await DocListCast(target[Doc.LayoutFieldKey(target)]); - targetChildren.forEach(async targetChild => await func(targetChild, "-annotations")); + await DocListCast(target[Doc.LayoutFieldKey(target)]).forEach(async targetChild => func(targetChild, "-annotations")); } export async function BranchCreate(target: Doc) { diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index c82c05c28..05acaf39d 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -509,7 +509,7 @@ export namespace Doc { const copy = dontCreate ? asBranch ? (Cast(doc.branchMaster, Doc, null) || doc) : doc : new Doc(undefined, true); cloneMap.set(doc[Id], copy); if (LinkManager.Instance.getAllLinks().includes(doc) && LinkManager.Instance.getAllLinks().indexOf(copy) === -1) LinkManager.Instance.addLink(copy); - const filter = Cast(doc.cloneFieldFilter, listSpec("string"), ["branches", ...exclusions]); + const filter = [...exclusions, ...Cast(doc.cloneFieldFilter, listSpec("string"), [])]; await Promise.all(Object.keys(doc).map(async key => { if (filter.includes(key)) return; const assignKey = (val: any) => !dontCreate && (copy[key] = val); @@ -554,7 +554,10 @@ export namespace Doc { if (!dontCreate) { Doc.SetInPlace(copy, "title", (asBranch ? "BRANCH: " : "CLONE: ") + doc.title, true); asBranch ? (copy.branchOf = doc) : (copy.cloneOf = doc); - if (!Doc.IsPrototype(copy)) Doc.AddDocToList(doc, "branches", Doc.GetProto(copy)); + if (!Doc.IsPrototype(copy)) { + console.log("ADDING: " + copy.title + " to " + doc.title + "'s branches"); + Doc.AddDocToList(doc, "branches", Doc.GetProto(copy)); + } cloneMap.set(doc[Id], copy); } return copy; @@ -562,7 +565,7 @@ export namespace Doc { export async function MakeClone(doc: Doc, dontCreate: boolean = false, asBranch = false) { const cloneMap = new Map(); const rtfMap: { copy: Doc, key: string, field: RichTextField }[] = []; - const copy = await Doc.makeClone(doc, cloneMap, rtfMap, ["context", "annotationOn", "cloneOf", "branchOf"], dontCreate, asBranch); + const copy = await Doc.makeClone(doc, cloneMap, rtfMap, ["context", "annotationOn", "cloneOf", "branches", "branchOf"], dontCreate, asBranch); rtfMap.map(({ copy, key, field }) => { const replacer = (match: any, attr: string, id: string, offset: any, string: any) => { const mapped = cloneMap.get(id); -- cgit v1.2.3-70-g09d2