aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorbobzel <zzzman@gmail.com>2021-03-07 21:20:53 -0500
committerbobzel <zzzman@gmail.com>2021-03-07 21:20:53 -0500
commitf23e497d5ad05b9af0f90b69f9a383deed0c1e39 (patch)
treec4cd5a110e68b7b36ff1eec6ef5ab21eb6e1ab5e
parent726dfb8fea45352b2eb0729ad6d3b4a7b0824e1a (diff)
added start of Git functionality within Dash.
-rw-r--r--src/client/views/DocumentDecorations.tsx13
-rw-r--r--src/client/views/collections/CollectionView.tsx71
-rw-r--r--src/fields/Doc.ts25
3 files changed, 90 insertions, 19 deletions
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) => (
<Tooltip key={key} title={<div className="dash-tooltip">{title}</div>} placement="top">
- <div className={`documentDecorations-${key}Button`} onContextMenu={e => { e.preventDefault(); e.stopPropagation(); }} onClick={click}>
+ <div className={`documentDecorations-${key}Button`} onContextMenu={e => e.preventDefault()}
+ onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, click, emptyFunction)} >
<FontAwesomeIcon icon={icon as any} />
</div>
</Tooltip>);
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<CollectionViewProps> {
}
}
+ // 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<Doc>([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<CollectionViewProps> {
}
!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<string, Doc>, rtfs: { copy: Doc, key: string, field: RichTextField }[], exclusions: string[], dontCreate: boolean): Promise<Doc> {
+ export async function makeClone(doc: Doc, cloneMap: Map<string, Doc>, rtfs: { copy: Doc, key: string, field: RichTextField }[], exclusions: string[], dontCreate: boolean, asBranch: boolean): Promise<Doc> {
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<Doc>(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<string, Doc>();
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]();