aboutsummaryrefslogtreecommitdiff
path: root/src/client/util
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/util')
-rw-r--r--src/client/util/CurrentUserUtils.ts127
-rw-r--r--src/client/util/DocumentManager.ts102
-rw-r--r--src/client/util/DragManager.ts33
-rw-r--r--src/client/util/GroupManager.tsx155
-rw-r--r--src/client/util/GroupMemberView.tsx4
-rw-r--r--src/client/util/Import & Export/ImageUtils.ts4
-rw-r--r--src/client/util/LinkManager.ts192
-rw-r--r--src/client/util/SearchUtil.ts19
-rw-r--r--src/client/util/SelectionManager.ts8
-rw-r--r--src/client/util/SerializationHelper.ts2
-rw-r--r--src/client/util/SettingsManager.tsx6
-rw-r--r--src/client/util/SharingManager.scss29
-rw-r--r--src/client/util/SharingManager.tsx199
-rw-r--r--src/client/util/SnappingManager.ts10
-rw-r--r--src/client/util/UndoManager.ts2
15 files changed, 431 insertions, 461 deletions
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 342954a4e..4f054269f 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -1,6 +1,6 @@
import { computed, observable, reaction } from "mobx";
import * as rp from 'request-promise';
-import { DataSym, Doc, DocListCast, DocListCastAsync } from "../../fields/Doc";
+import { DataSym, Doc, DocListCast, DocListCastAsync, AclReadonly } from "../../fields/Doc";
import { Id } from "../../fields/FieldSymbols";
import { List } from "../../fields/List";
import { PrefetchProxy } from "../../fields/Proxy";
@@ -8,12 +8,14 @@ import { RichTextField } from "../../fields/RichTextField";
import { listSpec } from "../../fields/Schema";
import { SchemaHeaderField } from "../../fields/SchemaHeaderField";
import { ComputedField, ScriptField } from "../../fields/ScriptField";
-import { BoolCast, Cast, NumCast, PromiseValue, StrCast } from "../../fields/Types";
+import { BoolCast, Cast, NumCast, PromiseValue, StrCast, DateCast } from "../../fields/Types";
import { nullAudio } from "../../fields/URLField";
+import { SharingPermissions } from "../../fields/util";
import { Utils } from "../../Utils";
import { DocServer } from "../DocServer";
import { Docs, DocumentOptions, DocUtils } from "../documents/Documents";
import { DocumentType } from "../documents/DocumentTypes";
+import { Networking } from "../Network";
import { CollectionDockingView } from "../views/collections/CollectionDockingView";
import { DimUnit } from "../views/collections/collectionMulticolumn/CollectionMulticolumnView";
import { CollectionView, CollectionViewType } from "../views/collections/CollectionView";
@@ -30,8 +32,10 @@ import { Scripting } from "./Scripting";
import { SearchUtil } from "./SearchUtil";
import { SelectionManager } from "./SelectionManager";
import { UndoManager } from "./UndoManager";
+import { SnappingManager } from "./SnappingManager";
+export let resolvedPorts: { server: number, socket: number };
const headerViewVersion = "0.1";
export class CurrentUserUtils {
private static curr_id: string;
@@ -228,7 +232,7 @@ export class CurrentUserUtils {
} else {
const curButnTypes = Cast(doc["template-buttons"], Doc, null);
DocListCastAsync(curButnTypes.data).then(async curBtns => {
- await Promise.all(curBtns!);
+ curBtns && await Promise.all(curBtns);
requiredTypes.map(btype => Doc.AddDocToList(curButnTypes, "data", btype));
});
}
@@ -277,7 +281,7 @@ export class CurrentUserUtils {
const curNoteTypes = Cast(doc["template-notes"], Doc, null);
const requiredTypes = [doc["template-note-Note"] as any as Doc, doc["template-note-Idea"] as any as Doc, doc["template-note-Topic"] as any as Doc];//, doc["template-note-Todo"] as any as Doc];
DocListCastAsync(curNoteTypes.data).then(async curNotes => {
- await Promise.all(curNotes!);
+ curNotes && await Promise.all(curNotes);
requiredTypes.map(ntype => Doc.AddDocToList(curNoteTypes, "data", ntype));
});
}
@@ -348,7 +352,7 @@ export class CurrentUserUtils {
const requiredTypes = [doc["template-icon-view"] as Doc, doc["template-icon-view-img"] as Doc, doc["template-icon-view-button"] as Doc,
doc["template-icon-view-col"] as Doc, doc["template-icon-view-rtf"] as Doc];
DocListCastAsync(templateIconsDoc.data).then(async curIcons => {
- await Promise.all(curIcons!);
+ curIcons && await Promise.all(curIcons);
requiredTypes.map(ntype => Doc.AddDocToList(templateIconsDoc, "data", ntype));
});
}
@@ -401,10 +405,10 @@ export class CurrentUserUtils {
selection: { type: "text", anchor: 1, head: 1 },
storedMarks: []
};
- const headerTemplate = Docs.Create.RTFDocument(new RichTextField(JSON.stringify(json), ""), { title: "header", version: headerViewVersion, target: doc, _height: 70, _headerHeight: 12, _headerFontSize: 9, _autoHeight: true, system: true, cloneFieldFilter: new List<string>(["system"]) }, "header"); // text needs to be a space to allow templateText to be created
+ const headerTemplate = Docs.Create.RTFDocument(new RichTextField(JSON.stringify(json), ""), { title: "header", version: headerViewVersion, target: doc, _height: 70, _headerPointerEvents: "all", _headerHeight: 12, _headerFontSize: 9, _autoHeight: true, system: true, cloneFieldFilter: new List<string>(["system"]) }, "header"); // text needs to be a space to allow templateText to be created
headerTemplate[DataSym].layout =
"<div style={'height:100%'}>" +
- " <FormattedTextBox {...props} fieldKey={'header'} dontSelectOnLoad={'true'} ignoreAutoHeight={'true'} pointerEvents='{this._headerPointerEvents||`none`}' fontSize='{this._headerFontSize}px' height='{this._headerHeight}px' background='{this._headerColor||this.target.userColor}' />" +
+ " <FormattedTextBox {...props} fieldKey={'header'} dontSelectOnLoad={'true'} ignoreAutoHeight={'true'} pointerEvents='{this._headerPointerEvents||`none`}' fontSize='{this._headerFontSize}px' height='{this._headerHeight}px' background='{this._headerColor||this.target.mySharedDocs.userColor}' />" +
" <FormattedTextBox {...props} fieldKey={'text'} position='absolute' top='{(this._headerHeight)*scale}px' height='calc({100/scale}% - {this._headerHeight}px)'/>" +
"</div>";
(headerTemplate.proto as Doc).isTemplateDoc = makeTemplate(headerTemplate.proto as Doc, true, "headerView");
@@ -422,9 +426,13 @@ export class CurrentUserUtils {
doc.emptyScreenshot = Docs.Create.ScreenshotDocument("", { _width: 400, _height: 200, title: "screen snapshot", system: true, cloneFieldFilter: new List<string>(["system"]) });
}
if (doc.emptyAudio === undefined) {
- doc.emptyAudio = Docs.Create.AudioDocument(nullAudio, { _width: 200, title: "ready to record audio", system: true, cloneFieldFilter: new List<string>(["system"]) });
+ doc.emptyAudio = Docs.Create.AudioDocument(nullAudio, { _width: 200, title: "audio recording", system: true, cloneFieldFilter: new List<string>(["system"]) });
((doc.emptyAudio as Doc).proto as Doc)["dragFactory-count"] = 0;
}
+ if (doc.emptyNote === undefined) {
+ doc.emptyNote = Docs.Create.TextDocument("", { _width: 200, title: "text note", system: true, cloneFieldFilter: new List<string>(["system"]) });
+ ((doc.emptyNote as Doc).proto as Doc)["dragFactory-count"] = 0;
+ }
if (doc.emptyImage === undefined) {
doc.emptyImage = Docs.Create.ImageDocument("https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg", { _width: 250, _nativeWidth: 250, title: "an image of a cat", system: true });
}
@@ -444,6 +452,7 @@ export class CurrentUserUtils {
this.setupActiveMobileMenu(doc);
}
return [
+ { toolTip: "Tap to create a note in a new pane, drag for a note", title: "Note", icon: "sticky-note", click: 'openOnRight(copyDragFactory(this.clickFactory))', drag: 'copyDragFactory(this.dragFactory)', dragFactory: doc.emptyNote as Doc, noviceMode: true, clickFactory: doc.emptyNote as Doc, },
{ toolTip: "Tap to create a collection in a new pane, drag for a collection", title: "Col", icon: "folder", click: 'openOnRight(copyDragFactory(this.clickFactory))', drag: 'copyDragFactory(this.dragFactory)', dragFactory: doc.emptyCollection as Doc, noviceMode: true, clickFactory: doc.emptyPane as Doc, },
{ toolTip: "Tap to create a webpage in a new pane, drag for a webpage", title: "Web", icon: "globe-asia", click: 'openOnRight(copyDragFactory(this.dragFactory))', drag: 'copyDragFactory(this.dragFactory)', dragFactory: doc.emptyWebpage as Doc, noviceMode: true },
{ toolTip: "Tap to create a progressive slide", title: "Slide", icon: "file", click: 'openOnRight(copyDragFactory(this.dragFactory))', drag: 'copyDragFactory(this.dragFactory)', dragFactory: doc.emptySlide as Doc, noviceMode: true },
@@ -508,10 +517,7 @@ export class CurrentUserUtils {
return doc.myItemCreators as Doc;
}
- static menuBtnDescriptions(doc: Doc): {
- title: string, target: Doc, icon: string, click: string, watchedDocuments?: Doc
- }[] {
- this.setupSharingSidebar(doc); // sets up the right sidebar collection for mobile upload documents and sharing
+ static async menuBtnDescriptions(doc: Doc) {
return [
{ title: "Dashboards", target: Cast(doc.myDashboards, Doc, null), icon: "desktop", click: 'selectMainMenu(self)' },
{ title: "Recently Closed", target: Cast(doc.myRecentlyClosedDocs, Doc, null), icon: "archive", click: 'selectMainMenu(self)' },
@@ -535,9 +541,10 @@ export class CurrentUserUtils {
})) as any as Doc;
}
}
- static setupMenuPanel(doc: Doc) {
+ static async setupMenuPanel(doc: Doc, sharingDocumentId: string, linkDatabaseId: string) {
if (doc.menuStack === undefined) {
- const menuBtns = CurrentUserUtils.menuBtnDescriptions(doc).map(({ title, target, icon, click, watchedDocuments }) =>
+ await this.setupSharingSidebar(doc, sharingDocumentId, linkDatabaseId); // sets up the right sidebar collection for mobile upload documents and sharing
+ const menuBtns = (await CurrentUserUtils.menuBtnDescriptions(doc)).map(({ title, target, icon, click, watchedDocuments }) =>
Docs.Create.FontIconDocument({
icon,
iconShape: "square",
@@ -561,7 +568,7 @@ export class CurrentUserUtils {
title: "menuItemPanel",
childDropAction: "alias",
dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }),
- _backgroundColor: "black",
+ _backgroundColor: "black", ignoreClick: true,
_gridGap: 0,
_yMargin: 0,
_yPadding: 0, _xMargin: 0, _autoHeight: false, _width: 60, _columnWidth: 60, lockedPosition: true, _chromeStatus: "disabled", system: true
@@ -724,7 +731,7 @@ export class CurrentUserUtils {
if (doc.myTools === undefined) {
const toolsStack = new PrefetchProxy(Docs.Create.StackingDocument([doc.myCreators as Doc, doc.myColorPicker as Doc], {
- title: "My Tools", _width: 500, _yMargin: 20, lockedPosition: true, _chromeStatus: "disabled", forceActive: true, system: true, _stayInCollection: true, _hideContextMenu: true,
+ title: "My Tools", _width: 500, _yMargin: 20, ignoreClick: true, lockedPosition: true, _chromeStatus: "disabled", forceActive: true, system: true, _stayInCollection: true, _hideContextMenu: true,
})) as any as Doc;
doc.myTools = toolsStack;
@@ -738,7 +745,7 @@ export class CurrentUserUtils {
doc.myDashboards = new PrefetchProxy(Docs.Create.TreeDocument([], {
title: "My Dashboards", _height: 400,
treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _gridGap: 5, forceActive: true, childDropAction: "alias",
- treeViewTruncateTitleWidth: 150, treeViewPreventOpen: false,
+ treeViewTruncateTitleWidth: 150, treeViewPreventOpen: false, ignoreClick: true,
lockedPosition: true, boxShadow: "0 0", dontRegisterChildViews: true, targetDropAction: "same", system: true
}));
const newDashboard = ScriptField.MakeScript(`createNewDashboard(Doc.UserDoc())`);
@@ -754,7 +761,7 @@ export class CurrentUserUtils {
doc.myPresentations = new PrefetchProxy(Docs.Create.TreeDocument([], {
title: "My Presentations", _height: 100,
treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _gridGap: 5, forceActive: true, childDropAction: "alias",
- treeViewTruncateTitleWidth: 150, treeViewPreventOpen: false,
+ treeViewTruncateTitleWidth: 150, treeViewPreventOpen: false, ignoreClick: true,
lockedPosition: true, boxShadow: "0 0", dontRegisterChildViews: true, targetDropAction: "same", system: true
}));
const newPresentations = ScriptField.MakeScript(`createNewPresentation()`);
@@ -772,7 +779,7 @@ export class CurrentUserUtils {
doc.myRecentlyClosedDocs = new PrefetchProxy(Docs.Create.TreeDocument([], {
title: "Recently Closed", _height: 500,
treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _gridGap: 5, forceActive: true, childDropAction: "alias",
- treeViewTruncateTitleWidth: 150, treeViewPreventOpen: false,
+ treeViewTruncateTitleWidth: 150, treeViewPreventOpen: false, ignoreClick: true,
lockedPosition: true, boxShadow: "0 0", dontRegisterChildViews: true, targetDropAction: "same", system: true
}));
const clearAll = ScriptField.MakeScript(`getProto(self).data = new List([])`);
@@ -787,7 +794,7 @@ export class CurrentUserUtils {
doc.myFilter = new PrefetchProxy(Docs.Create.FilterDocument({
title: "FilterDoc", _height: 500,
treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _gridGap: 5, forceActive: true, childDropAction: "none",
- treeViewTruncateTitleWidth: 150, treeViewPreventOpen: false,
+ treeViewTruncateTitleWidth: 150, treeViewPreventOpen: false, ignoreClick: true,
lockedPosition: true, boxShadow: "0 0", dontRegisterChildViews: true, targetDropAction: "same", system: true
}));
const clearAll = ScriptField.MakeScript(`getProto(self).data = new List([])`);
@@ -803,7 +810,7 @@ export class CurrentUserUtils {
doc.treeViewExpandedView = "fields";
doc.myUserDoc = new PrefetchProxy(Docs.Create.TreeDocument([doc], {
treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _gridGap: 5, forceActive: true, title: "My UserDoc",
- treeViewTruncateTitleWidth: 150, treeViewPreventOpen: false,
+ treeViewTruncateTitleWidth: 150, treeViewPreventOpen: false, ignoreClick: true,
lockedPosition: true, boxShadow: "0 0", dontRegisterChildViews: true, targetDropAction: "same", system: true
})) as any as Doc;
}
@@ -869,9 +876,30 @@ export class CurrentUserUtils {
}
// Sharing sidebar is where shared documents are contained
- static setupSharingSidebar(doc: Doc) {
+ static async setupSharingSidebar(doc: Doc, sharingDocumentId: string, linkDatabaseId: string) {
+ if (doc.myLinkDatabase === undefined) {
+ let linkDocs = Docs.newAccount ? undefined : await DocServer.GetRefField(linkDatabaseId);
+ if (!linkDocs) {
+ linkDocs = new Doc(linkDatabaseId, true);
+ (linkDocs as Doc).author = Doc.CurrentUserEmail;
+ (linkDocs as Doc).data = new List<Doc>([]);
+ (linkDocs as Doc)["acl-Public"] = SharingPermissions.Add;
+ }
+ doc.myLinkDatabase = new PrefetchProxy(linkDocs);
+ }
if (doc.mySharedDocs === undefined) {
- doc.mySharedDocs = new PrefetchProxy(Docs.Create.StackingDocument([], { title: "My SharedDocs", childDropAction: "alias", system: true, contentPointerEvents: "none", childLimitHeight: 0, _yMargin: 50, _gridGap: 15, _showTitle: "title", ignoreClick: true, lockedPosition: true }));
+ let sharedDocs = Docs.newAccount ? undefined : await DocServer.GetRefField(sharingDocumentId + "outer");
+ if (!sharedDocs) {
+ sharedDocs = Docs.Create.StackingDocument([], {
+ title: "My SharedDocs", childDropAction: "alias", system: true, contentPointerEvents: "none", childLimitHeight: 0, _yMargin: 50, _gridGap: 15,
+ _showTitle: "title", ignoreClick: true, lockedPosition: true,
+ }, sharingDocumentId + "outer", sharingDocumentId);
+ (sharedDocs as Doc)["acl-Public"] = Doc.GetProto(sharedDocs as Doc)["acl-Public"] = SharingPermissions.Add;
+ }
+ if (sharedDocs instanceof Doc) {
+ sharedDocs.userColor = sharedDocs.userColor || "rgb(202, 202, 202)";
+ }
+ doc.mySharedDocs = new PrefetchProxy(sharedDocs);
}
}
@@ -938,11 +966,18 @@ export class CurrentUserUtils {
return doc.clickFuncs as Doc;
}
- static async updateUserDocument(doc: Doc) {
+ 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),
+ async () => {
+ const groups = await DocListCastAsync((doc.globalGroupDatabase as Doc).data);
+ const mygroups = groups?.filter(group => JSON.parse(StrCast(group.members)).includes(Doc.CurrentUserEmail)) || [];
+ SnappingManager.SetCachedGroups(["Public", ...mygroups?.map(g => StrCast(g.title))]);
+ }, { fireImmediately: true });
doc.system = true;
doc.noviceMode = doc.noviceMode === undefined ? "true" : doc.noviceMode;
doc.title = Doc.CurrentUserEmail;
- doc.userColor = doc.userColor || "#12121233";
doc._raiseWhenDragged = true;
doc.activeInkPen = doc;
doc.activeInkColor = StrCast(doc.activeInkColor, "rgb(0, 0, 0)");
@@ -971,10 +1006,8 @@ export class CurrentUserUtils {
this.setupOverlays(doc); // documents in overlay layer
this.setupDockedButtons(doc); // the bottom bar of font icons
await this.setupSidebarButtons(doc); // the pop-out left sidebar of tools/panels
- this.setupMenuPanel(doc);
- doc.globalLinkDatabase = Docs.Prototypes.MainLinkDocument();
- doc.globalScriptDatabase = Docs.Prototypes.MainScriptDocument();
- doc.globalGroupDatabase = Docs.Prototypes.MainGroupDocument();
+ await this.setupMenuPanel(doc, sharingDocumentId, linkDatabaseId);
+ if (!doc.globalScriptDatabase) doc.globalScriptDatabase = Docs.Prototypes.MainScriptDocument();
setTimeout(() => this.setupDefaultPresentation(doc), 0); // presentation that's initially triggered
@@ -991,13 +1024,18 @@ export class CurrentUserUtils {
// Doc.AddDocToList(Cast(doc["template-notes"], Doc, null), "data", deleg);
// }
// });
+ setTimeout(() => DocServer.UPDATE_SERVER_CACHE(), 2500);
return doc;
}
public static async loadCurrentUser() {
- return rp.get(Utils.prepend("/getCurrentUser")).then(response => {
+ return rp.get(Utils.prepend("/getCurrentUser")).then(async response => {
if (response) {
- const result: { id: string, email: string } = JSON.parse(response);
+ const result: { id: string, email: string, cacheDocumentIds: string } = JSON.parse(response);
+ Doc.CurrentUserEmail = result.email;
+ resolvedPorts = JSON.parse(await Networking.FetchFromServer("/resolvedPorts"));
+ DocServer.init(window.location.protocol, window.location.hostname, resolvedPorts.socket, result.email);
+ result.cacheDocumentIds && (await DocServer.GetRefFields(result.cacheDocumentIds.split(";")));
return result;
} else {
throw new Error("There should be a user! Why does Dash think there isn't one?");
@@ -1005,13 +1043,17 @@ export class CurrentUserUtils {
});
}
- public static async loadUserDocument({ id, email }: { id: string, email: string }) {
+ public static async loadUserDocument(id: string) {
this.curr_id = id;
- Doc.CurrentUserEmail = email;
- await rp.get(Utils.prepend("/getUserDocumentId")).then(id => {
- if (id && id !== "guest") {
- return DocServer.GetRefField(id).then(async field =>
- Doc.SetUserDoc(await this.updateUserDocument(field instanceof Doc ? field : new Doc(id, true))));
+ await rp.get(Utils.prepend("/getUserDocumentIds")).then(ids => {
+ const { userDocumentId, sharingDocumentId, linkDatabaseId } = JSON.parse(ids);
+ if (userDocumentId !== "guest") {
+ return DocServer.GetRefField(userDocumentId).then(async field => {
+ Docs.newAccount = !(field instanceof Doc);
+ await Docs.Prototypes.initialize();
+ const userDoc = Docs.newAccount ? new Doc(userDocumentId, true) : field as Doc;
+ return this.updateUserDocument(Doc.SetUserDoc(userDoc), sharingDocumentId, linkDatabaseId);
+ });
} else {
throw new Error("There should be a user id! Why does Dash think there isn't one?");
}
@@ -1071,7 +1113,7 @@ export class CurrentUserUtils {
const response = await fetch(upload, { method: "POST", body: formData });
const json = await response.json();
if (json !== "error") {
- const doc = await DocServer.GetRefField(json);
+ const doc = Docs.newAccount ? undefined : await DocServer.GetRefField(json);
if (doc instanceof Doc) {
setTimeout(() => SearchUtil.Search(`{!join from=id to=proto_i}id:link*`, true, {}).then(docs =>
docs.docs.forEach(d => LinkManager.Instance.addLink(d))), 2000); // need to give solr some time to update so that this query will find any link docs we've added.
@@ -1082,6 +1124,9 @@ export class CurrentUserUtils {
const disposer = OverlayView.ShowSpinner();
DocListCastAsync(importDocs.data).then(async list => {
const results = await DocUtils.uploadFilesToDocs(Array.from(input.files || []), {});
+ if (results.length !== input.files?.length) {
+ alert("Error uploading files - possibly due to unsupported file types");
+ }
list?.splice(0, 0, ...results);
disposer();
});
@@ -1127,8 +1172,9 @@ export class CurrentUserUtils {
CurrentUserUtils.openDashboard(userDoc, dashboardDoc);
}
- public static GetNewTextDoc(title: string, x: number, y: number, width?: number, height?: number) {
+ public static GetNewTextDoc(title: string, x: number, y: number, width?: number, height?: number, noMargins?: boolean) {
const tbox = Docs.Create.TextDocument("", {
+ _xMargin: noMargins ? 0 : undefined, _yMargin: noMargins ? 0 : undefined,
_width: width || 200, _height: height || 100, x: x, y: y, _autoHeight: true, _fontSize: StrCast(Doc.UserDoc().fontSize),
_fontFamily: StrCast(Doc.UserDoc().fontFamily), title
});
@@ -1147,6 +1193,7 @@ export class CurrentUserUtils {
public static get MyRecentlyClosed() { return Cast(Doc.UserDoc().myRecentlyClosedDocs, Doc, null); }
public static get MyDashboards() { return Cast(Doc.UserDoc().myDashboards, Doc, null); }
public static get EmptyPane() { return Cast(Doc.UserDoc().emptyPane, Doc, null); }
+ public static get OverlayDocs() { return DocListCast((Doc.UserDoc().myOverlayDocs as Doc)?.data); }
}
Scripting.addGlobal(function openDragFactory(dragFactory: Doc) {
@@ -1165,7 +1212,5 @@ Scripting.addGlobal(function createNewPresentation() { return MainView.Instance.
"creates a new presentation when called");
Scripting.addGlobal(function links(doc: any) { return new List(LinkManager.Instance.getAllRelatedLinks(doc)); },
"returns all the links to the document or its annotations", "(doc: any)");
-Scripting.addGlobal(function directLinks(doc: any) { return new List(LinkManager.Instance.getAllDirectLinks(doc)); },
- "returns all the links directly to the document", "(doc: any)");
Scripting.addGlobal(function importDocument() { return CurrentUserUtils.importDocument(); },
"imports files from device directly into the import sidebar");
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index c3d78a028..a6816c7f9 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -1,16 +1,17 @@
-import { action, computed, observable } from 'mobx';
-import { Doc, DocListCastAsync, DocListCast, Opt } from '../../fields/Doc';
+import { action, observable } from 'mobx';
+import { Doc, DocListCast, DocListCastAsync, Opt } from '../../fields/Doc';
import { Id } from '../../fields/FieldSymbols';
import { Cast, NumCast, StrCast } from '../../fields/Types';
+import { returnFalse } from '../../Utils';
+import { DocumentType } from '../documents/DocumentTypes';
import { CollectionDockingView } from '../views/collections/CollectionDockingView';
import { CollectionView } from '../views/collections/CollectionView';
-import { DocumentView, DocFocusFunc } from '../views/nodes/DocumentView';
+import { DocumentView } from '../views/nodes/DocumentView';
import { LinkManager } from './LinkManager';
import { Scripting } from './Scripting';
import { SelectionManager } from './SelectionManager';
-import { DocumentType } from '../documents/DocumentTypes';
-import { TraceMobx } from '../../fields/util';
-import { returnFalse } from '../../Utils';
+import { LinkDocPreview } from '../views/nodes/LinkDocPreview';
+import { FormattedTextBoxComment } from '../views/nodes/formattedText/FormattedTextBoxComment';
export type CreateViewFunc = (doc: Doc, followLinkLocation: string, finished?: () => void) => void;
@@ -19,6 +20,7 @@ export class DocumentManager {
//global holds all of the nodes (regardless of which collection they're in)
@observable
public DocumentViews: DocumentView[] = [];
+ @observable LinkedDocumentViews: { a: DocumentView, b: DocumentView, l: Doc }[] = [];
// singleton instance
private static _instance: DocumentManager;
@@ -32,6 +34,26 @@ export class DocumentManager {
private constructor() {
}
+ @action
+ public AddView = (view: DocumentView) => {
+ const linksList = DocListCast(view.props.Document.links);
+ linksList.forEach(link => {
+ const linkToDoc = link && LinkManager.getOppositeAnchor(link, view.props.Document);
+ linkToDoc && DocumentManager.Instance.DocumentViews.filter(dv => Doc.AreProtosEqual(dv.props.Document, linkToDoc)).forEach(dv => {
+ if (dv.props.Document.type !== DocumentType.LINK || dv.props.LayoutTemplateString !== view.props.LayoutTemplateString) {
+ this.LinkedDocumentViews.push({ a: dv, b: view, l: link });
+ }
+ });
+ });
+ this.DocumentViews.push(view);
+ }
+ public RemoveView = (view: DocumentView) => {
+ const index = this.DocumentViews.indexOf(view);
+ index !== -1 && this.DocumentViews.splice(index, 1);
+
+ this.LinkedDocumentViews.slice().forEach(action((pair, i) => pair.a === view || pair.b === view ? this.LinkedDocumentViews.splice(i, 1) : null));
+ }
+
//gets all views
public getDocumentViewsById(id: string) {
const toReturn: DocumentView[] = [];
@@ -88,7 +110,7 @@ export class DocumentManager {
public getFirstDocumentView = (toFind: Doc, originatingDoc: Opt<Doc> = undefined): DocumentView | undefined => {
const views = this.getDocumentViews(toFind).filter(view => view.props.Document !== originatingDoc);
- return views?.find(view => view.props.focus !== returnFalse) || (views.length ? views[0] : undefined);
+ return views?.find(view => view.ContentDiv?.getBoundingClientRect().width && view.props.focus !== returnFalse) || views?.find(view => view.props.focus !== returnFalse) || (views.length ? views[0] : undefined);
}
public getDocumentViews(toFind: Doc): DocumentView[] {
const toReturn: DocumentView[] = [];
@@ -105,39 +127,20 @@ export class DocumentManager {
return toReturn;
}
- @computed
- public get LinkedDocumentViews() {
- TraceMobx();
- const pairs = DocumentManager.Instance.DocumentViews.reduce((pairs, dv) => {
- const linksList = DocListCast(dv.props.Document.links);
- pairs.push(...linksList.reduce((pairs, link) => {
- const linkToDoc = link && LinkManager.getOppositeAnchor(link, dv.props.Document);
- linkToDoc && DocumentManager.Instance.getDocumentViews(linkToDoc).map(docView1 => {
- if (dv.props.Document.type !== DocumentType.LINK || dv.props.LayoutTemplateString !== docView1.props.LayoutTemplateString) {
- pairs.push({ a: dv, b: docView1, l: link });
- }
- });
- return pairs;
- }, [] as { a: DocumentView, b: DocumentView, l: Doc }[]));
- return pairs;
- }, [] as { a: DocumentView, b: DocumentView, l: Doc }[]);
- return pairs;
- }
-
- static addRightSplit = (doc: Doc, finished?: () => void) => {
+ static addView = (doc: Doc, finished?: () => void) => {
CollectionDockingView.AddSplit(doc, "right");
finished?.();
}
public jumpToDocument = async (
targetDoc: Doc, // document to display
willZoom: boolean, // whether to zoom doc to take up most of screen
- createViewFunc = DocumentManager.addRightSplit, // how to create a view of the doc if it doesn't exist
+ createViewFunc = DocumentManager.addView, // how to create a view of the doc if it doesn't exist
docContext?: Doc, // context to load that should contain the target
linkDoc?: Doc, // link that's being followed
closeContextIfNotFound: boolean = false, // after opening a context where the document should be, this determines whether the context should be closed if the Doc isn't actually there
originatingDoc: Opt<Doc> = undefined, // doc that initiated the display of the target odoc
- finished?: () => void
+ finished?: () => void,
): Promise<void> => {
const getFirstDocView = DocumentManager.Instance.getFirstDocumentView;
const focusAndFinish = () => { finished?.(); return false; };
@@ -150,7 +153,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;
@@ -158,15 +161,23 @@ 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?
+ const sameContext = annotatedDoc && annotatedDoc === originatingDoc?.context;
if (originatingDoc?.isPushpin) {
- docView.props.Document.hidden = !docView.props.Document.hidden;
+ docView.props.focus(docView.props.Document, willZoom, undefined, (didFocus: boolean) => {
+ if (!didFocus || docView.props.Document.hidden) {
+ docView.props.Document.hidden = !docView.props.Document.hidden;
+ }
+ return focusAndFinish();
+ }, sameContext, false);// don't want to focus the container if the source and target are in the same container, so pass 'sameContext' for dontCenter parameter
+ //finished?.();
}
else {
docView.select(false);
docView.props.Document.hidden && (docView.props.Document.hidden = undefined);
- docView.props.focus(docView.props.Document, willZoom, undefined, focusAndFinish);
- highlight();
+ // @ts-ignore
+ docView.props.focus(docView.props.Document, willZoom, undefined, focusAndFinish, sameContext, false);
}
+ highlight();
} else {
const contextDocs = docContext ? await DocListCastAsync(docContext.data) : undefined;
const contextDoc = contextDocs?.find(doc => Doc.AreProtosEqual(doc, targetDoc)) ? docContext : undefined;
@@ -177,7 +188,7 @@ export class DocumentManager {
highlight();
} else { // otherwise try to get a view of the context of the target
const targetDocContextView = getFirstDocView(targetDocContext);
- targetDocContext._scrollY = NumCast(targetDocContext._scrollTop, 0); // this will force PDFs to activate and load their annotations / allow scrolling
+ targetDocContext._scrollY = targetDocContext._scrollPreviewY = NumCast(targetDocContext._scrollTop, 0); // this will force PDFs to activate and load their annotations / allow scrolling
if (targetDocContextView) { // we found a context view and aren't forced to create a new one ... focus on the context first..
targetDocContext._viewTransition = "transform 500ms";
targetDocContextView.props.focus(targetDocContextView.props.Document, willZoom);
@@ -192,34 +203,31 @@ 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);
}
};
findView(0);
}
- } else { // there's no context view so we need to create one first and try again
- createViewFunc(targetDocContext); // so first we create the target, but don't pass finished because we still need to create the target
- setTimeout(() => {
- const finalDocView = getFirstDocView(targetDoc);
- const finalDocContextView = getFirstDocView(targetDocContext);
- setTimeout(() => // if not, wait a bit to see if the context can be loaded (e.g., a PDF). wait interval heurisitic tries to guess how we're animating based on what's just become visible
- this.jumpToDocument(targetDoc, willZoom, createViewFunc, undefined, linkDoc, true, undefined, finished), // pass true this time for closeContextIfNotFound
- finalDocView ? 0 : finalDocContextView ? 250 : 2000); // so call jump to doc again and if the doc isn't found, it will be created.
- }, 0);
+ } else { // there's no context view so we need to create one first and try again when that finishes
+ createViewFunc(targetDocContext, // after creating the context, this calls the finish function that will retry looking for the target
+ () => this.jumpToDocument(targetDoc, willZoom, createViewFunc, docContext, linkDoc, true /* if we don't find the target, we want to get rid of the context just created */, undefined, finished));
}
}
}
}
public async FollowLink(link: Opt<Doc>, doc: Doc, createViewFunc: CreateViewFunc, zoom = false, currentContext?: Doc, finished?: () => void, traverseBacklink?: boolean) {
+ LinkDocPreview.TargetDoc = undefined;
+ FormattedTextBoxComment.linkDoc = undefined;
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/DragManager.ts b/src/client/util/DragManager.ts
index 91ffab41d..86e2d339e 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -129,6 +129,7 @@ export namespace DragManager {
treeViewDoc?: Doc;
dontHideOnDrop?: boolean;
offset: number[];
+ canEmbed?: boolean;
userDropAction: dropActionType; // the user requested drop action -- this will be honored as specified by modifier keys
defaultDropAction?: dropActionType; // an optionally specified default drop action when there is no user drop actionl - this will be honored if there is no user drop action
dropAction: dropActionType; // a drop action request by the initiating code. the actual drop action may be different -- eg, if the request is 'alias', but the document is dropped within the same collection, the drop action will be switched to 'move'
@@ -200,7 +201,14 @@ export namespace DragManager {
}
// drag a document and drop it (or make an alias/copy on drop)
- export function StartDocumentDrag(eles: HTMLElement[], dragData: DocumentDragData, downX: number, downY: number, options?: DragOptions) {
+ export function StartDocumentDrag(
+ eles: HTMLElement[],
+ dragData: DocumentDragData,
+ downX: number,
+ downY: number,
+ options?: DragOptions,
+ dropEvent?: () => any
+ ) {
const addAudioTag = (dropDoc: any) => {
dropDoc && !dropDoc.creationDate && (dropDoc.creationDate = new DateField);
dropDoc instanceof Doc && DocUtils.MakeLinkToActiveAudio(dropDoc);
@@ -208,6 +216,7 @@ export namespace DragManager {
};
const finishDrag = (e: DragCompleteEvent) => {
const docDragData = e.docDragData;
+ if (dropEvent) dropEvent(); // glr: optional additional function to be called - in this case with presentation trails
if (docDragData && !docDragData.droppedDocuments.length) {
docDragData.dropAction = dragData.userDropAction || dragData.dropAction;
docDragData.droppedDocuments =
@@ -324,7 +333,7 @@ export namespace DragManager {
if (dragData.dropAction === "none") return;
const batch = UndoManager.StartBatch("dragging");
eles = eles.filter(e => e);
- CanEmbed = false;
+ CanEmbed = dragData.canEmbed || false;
if (!dragDiv) {
dragDiv = document.createElement("div");
dragDiv.className = "dragManager-dragDiv";
@@ -339,7 +348,6 @@ export namespace DragManager {
DragManager.Root().appendChild(dragDiv);
}
dragLabel.style.display = "";
- SnappingManager.SetIsDragging(true);
const scaleXs: number[] = [];
const scaleYs: number[] = [];
const xs: number[] = [];
@@ -408,8 +416,15 @@ export namespace DragManager {
});
const hideSource = options?.hideSource ? true : false;
- eles.map(ele => ele.parentElement && ele.parentElement?.className === dragData.dragDivName ? (ele.parentElement.hidden = hideSource) : (ele.hidden = hideSource));
+ eles.forEach(ele => {
+ if (ele.parentElement && ele.parentElement?.className === dragData.dragDivName) {
+ ele.parentElement.hidden = hideSource;
+ } else {
+ ele.hidden = hideSource;
+ }
+ });
+ SnappingManager.SetIsDragging(true);
let lastX = downX;
let lastY = downY;
const xFromLeft = downX - elesCont.left;
@@ -505,27 +520,25 @@ export namespace DragManager {
const hideDragShowOriginalElements = () => {
dragLabel.style.display = "none";
dragElements.map(dragElement => dragElement.parentNode === dragDiv && dragDiv.removeChild(dragElement));
- eles.map(ele => ele.parentElement && ele.parentElement?.className === dragData.dragDivName ? (ele.parentElement.hidden = false) : (ele.hidden = false));
+ eles.map(ele => ele.parentElement && ele.parentElement?.className === dragData.dragDivName ? (ele.hidden = ele.parentElement.hidden = false) : (ele.hidden = false));
};
const endDrag = action(() => {
+ hideDragShowOriginalElements();
document.removeEventListener("pointermove", moveHandler, true);
document.removeEventListener("pointerup", upHandler);
+ SnappingManager.SetIsDragging(false);
SnappingManager.clearSnapLines();
batch.end();
});
AbortDrag = () => {
- hideDragShowOriginalElements();
- SnappingManager.SetIsDragging(false);
options?.dragComplete?.(new DragCompleteEvent(true, dragData));
endDrag();
};
const upHandler = (e: PointerEvent) => {
- hideDragShowOriginalElements();
dispatchDrag(eles, e, dragData, xFromLeft, yFromTop, xFromRight, yFromBottom, options, finishDrag);
- SnappingManager.SetIsDragging(false);
- endDrag();
options?.dragComplete?.(new DragCompleteEvent(false, dragData));
+ endDrag();
};
document.addEventListener("pointermove", moveHandler, true);
document.addEventListener("pointerup", upHandler);
diff --git a/src/client/util/GroupManager.tsx b/src/client/util/GroupManager.tsx
index cb15b5081..6458de0ed 100644
--- a/src/client/util/GroupManager.tsx
+++ b/src/client/util/GroupManager.tsx
@@ -5,15 +5,15 @@ import * as React from "react";
import Select from 'react-select';
import * as RequestPromise from "request-promise";
import { Doc, DocListCast, DocListCastAsync, Opt } from "../../fields/Doc";
-import { Cast, StrCast } from "../../fields/Types";
-import { setGroups } from "../../fields/util";
+import { StrCast, Cast } from "../../fields/Types";
import { Utils } from "../../Utils";
-import { DocServer } from "../DocServer";
import { MainViewModal } from "../views/MainViewModal";
import { TaskCompletionBox } from "../views/nodes/TaskCompletedBox";
import "./GroupManager.scss";
import { GroupMemberView } from "./GroupMemberView";
import { SharingManager, User } from "./SharingManager";
+import { listSpec } from "../../fields/Schema";
+import { DateField } from "../../fields/DateField";
/**
* Interface for options for the react-select component
@@ -34,62 +34,23 @@ export class GroupManager extends React.Component<{}> {
@observable private createGroupModalOpen: boolean = false;
private inputRef: React.RefObject<HTMLInputElement> = React.createRef(); // the ref for the input box.
private createGroupButtonRef: React.RefObject<HTMLButtonElement> = React.createRef(); // the ref for the group creation button
- private currentUserGroups: string[] = []; // the list of groups the current user is a member of
@observable private buttonColour: "#979797" | "black" = "#979797";
@observable private groupSort: "ascending" | "descending" | "none" = "none";
- private populating: boolean = false;
-
-
constructor(props: Readonly<{}>) {
super(props);
GroupManager.Instance = this;
}
- /**
- * Populates the list of users and groups.
- */
- componentDidMount() {
- this.populateUsers();
- this.populateGroups();
- }
+ componentDidMount() { this.populateUsers(); }
/**
* Fetches the list of users stored on the database.
*/
populateUsers = async () => {
- if (!this.populating) {
- this.populating = true;
- runInAction(() => this.users = []);
- const userList = await RequestPromise.get(Utils.prepend("/getUsers"));
- const raw = JSON.parse(userList) as User[];
- const evaluating = raw.map(async user => {
- const userDocument = await DocServer.GetRefField(user.userDocumentId);
- if (userDocument instanceof Doc) {
- const notificationDoc = await Cast(userDocument.mySharedDocs, Doc);
- runInAction(() => {
- if (notificationDoc instanceof Doc) {
- this.users.push(user.email);
- }
- });
- }
- });
- return Promise.all(evaluating).then(() => this.populating = false);
- }
- }
-
- /**
- * Populates the list of groups the current user is a member of and sets this list to be used in the GetEffectiveAcl in util.ts
- */
- populateGroups = () => {
- DocListCastAsync(this.GroupManagerDoc?.data).then(groups => {
- groups?.forEach(group => {
- const members: string[] = JSON.parse(StrCast(group.members));
- if (members.includes(Doc.CurrentUserEmail)) this.currentUserGroups.push(StrCast(group.groupName));
- });
- this.currentUserGroups.push("Public");
- setGroups(this.currentUserGroups);
- });
+ const userList = await RequestPromise.get(Utils.prepend("/getUsers"));
+ const raw = JSON.parse(userList) as User[];
+ raw.map(action(user => !this.users.some(umail => umail === user.email) && this.users.push(user.email)));
}
/**
@@ -107,7 +68,6 @@ export class GroupManager extends React.Component<{}> {
// SelectionManager.DeselectAll();
this.isOpen = true;
this.populateUsers();
- this.populateGroups();
}
/**
@@ -126,25 +86,24 @@ export class GroupManager extends React.Component<{}> {
/**
* @returns the database of groups.
*/
- get GroupManagerDoc(): Doc | undefined {
- return Doc.UserDoc().globalGroupDatabase as Doc;
- }
+ @computed get GroupManagerDoc(): Doc | undefined { return Doc.UserDoc().globalGroupDatabase as Doc; }
/**
* @returns a list of all group documents.
*/
- getAllGroups(): Doc[] {
- const groupDoc = this.GroupManagerDoc;
- return groupDoc ? DocListCast(groupDoc.data) : [];
- }
+ @computed get allGroups(): Doc[] { return DocListCast(this.GroupManagerDoc?.data); }
+
+ /**
+ * @returns the members of the admin group.
+ */
+ @computed get adminGroupMembers(): string[] { return this.getGroup("Admin") ? JSON.parse(StrCast(this.getGroup("Admin")!.members)) : ""; }
/**
* @returns a group document based on the group name.
* @param groupName
*/
getGroup(groupName: string): Doc | undefined {
- const groupDoc = this.getAllGroups().find(group => group.groupName === groupName);
- return groupDoc;
+ return this.allGroups.find(group => group.title === groupName);
}
/**
@@ -152,15 +111,9 @@ export class GroupManager extends React.Component<{}> {
*/
getGroupMembers(group: string | Doc): string[] {
if (group instanceof Doc) return JSON.parse(StrCast(group.members)) as string[];
- else return JSON.parse(StrCast(this.getGroup(group)!.members)) as string[];
+ return JSON.parse(StrCast(this.getGroup(group)!.members)) as string[];
}
- /**
- * @returns the members of the admin group.
- */
- get adminGroupMembers(): string[] {
- return this.getGroup("Admin") ? JSON.parse(StrCast(this.getGroup("Admin")!.members)) : "";
- }
/**
* @returns a boolean indicating whether the current user has access to edit group documents.
@@ -178,14 +131,11 @@ export class GroupManager extends React.Component<{}> {
* @param memberEmails
*/
createGroupDoc(groupName: string, memberEmails: string[] = []) {
- const groupDoc = new Doc;
- groupDoc.groupName = groupName.toLowerCase() === "admin" ? "Admin" : groupName;
+ const name = groupName.toLowerCase() === "admin" ? "Admin" : groupName;
+ const groupDoc = new Doc("GROUP:" + name, true);
+ groupDoc.title = name;
groupDoc.owners = JSON.stringify([Doc.CurrentUserEmail]);
groupDoc.members = JSON.stringify(memberEmails);
- if (memberEmails.includes(Doc.CurrentUserEmail)) {
- this.currentUserGroups.push(groupName);
- setGroups(this.currentUserGroups);
- }
this.addGroup(groupDoc);
}
@@ -196,6 +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;
return true;
}
return false;
@@ -205,19 +156,20 @@ export class GroupManager extends React.Component<{}> {
* Deletes a group from the database of group documents and @returns whether the group was deleted or not.
* @param group
*/
+ @action
deleteGroup(group: Doc): boolean {
if (group) {
if (this.GroupManagerDoc && this.hasEditAccess(group)) {
Doc.RemoveDocFromList(this.GroupManagerDoc, "data", group);
SharingManager.Instance.removeGroup(group);
- const members: string[] = JSON.parse(StrCast(group.members));
+ const members = JSON.parse(StrCast(group.members));
if (members.includes(Doc.CurrentUserEmail)) {
- const index = this.currentUserGroups.findIndex(groupName => groupName === group.groupName);
- index !== -1 && this.currentUserGroups.splice(index, 1);
- setGroups(this.currentUserGroups);
+ 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;
if (group === this.currentGroup) {
- runInAction(() => this.currentGroup = undefined);
+ this.currentGroup = undefined;
}
return true;
}
@@ -232,10 +184,11 @@ export class GroupManager extends React.Component<{}> {
*/
addMemberToGroup(groupDoc: Doc, email: string) {
if (this.hasEditAccess(groupDoc)) {
- const memberList: string[] = JSON.parse(StrCast(groupDoc.members));
+ const memberList = JSON.parse(StrCast(groupDoc.members));
!memberList.includes(email) && memberList.push(email);
groupDoc.members = JSON.stringify(memberList);
SharingManager.Instance.shareWithAddedMember(groupDoc, email);
+ this.GroupManagerDoc && (this.GroupManagerDoc.lastModified = new DateField);
}
}
@@ -246,12 +199,13 @@ export class GroupManager extends React.Component<{}> {
*/
removeMemberFromGroup(groupDoc: Doc, email: string) {
if (this.hasEditAccess(groupDoc)) {
- const memberList: string[] = JSON.parse(StrCast(groupDoc.members));
+ const memberList = JSON.parse(StrCast(groupDoc.members));
const index = memberList.indexOf(email);
if (index !== -1) {
const user = memberList.splice(index, 1)[0];
groupDoc.members = JSON.stringify(memberList);
SharingManager.Instance.removeMember(groupDoc, email);
+ this.GroupManagerDoc && (this.GroupManagerDoc.lastModified = new DateField);
}
}
}
@@ -278,21 +232,24 @@ export class GroupManager extends React.Component<{}> {
*/
@action
createGroup = () => {
- if (!this.inputRef.current?.value) {
+ const { value } = this.inputRef.current!;
+ if (!value) {
alert("Please enter a group name");
return;
}
- if (this.inputRef.current.value.toLowerCase() === "admin" && this.getGroup("Admin")) {
- alert("You cannot override the Admin group");
- return;
+ if (["admin", "public", "override"].includes(value.toLowerCase())) {
+ if (value.toLowerCase() !== "admin" || (value.toLowerCase() === "admin" && this.getGroup("Admin"))) {
+ alert(`You cannot override the ${value.charAt(0).toUpperCase() + value.slice(1)} group`);
+ return;
+ }
}
- if (this.getGroup(this.inputRef.current.value)) {
+ if (this.getGroup(value)) {
alert("Please select a unique group name");
return;
}
- this.createGroupDoc(this.inputRef.current.value, this.selectedUsers?.map(user => user.value));
+ this.createGroupDoc(value, this.selectedUsers?.map(user => user.value));
this.selectedUsers = null;
- this.inputRef.current.value = "";
+ this.inputRef.current!.value = "";
this.buttonColour = "#979797";
const { left, width, top } = this.createGroupButtonRef.current!.getBoundingClientRect();
@@ -378,14 +335,13 @@ export class GroupManager extends React.Component<{}> {
private get groupInterface() {
const sortGroups = (d1: Doc, d2: Doc) => {
- const g1 = StrCast(d1.groupName);
- const g2 = StrCast(d2.groupName);
+ const g1 = StrCast(d1.title);
+ const g2 = StrCast(d2.title);
return g1 < g2 ? -1 : g1 === g2 ? 0 : 1;
};
- let groups = this.getAllGroups();
- groups = this.groupSort === "ascending" ? groups.sort(sortGroups) : this.groupSort === "descending" ? groups.sort(sortGroups).reverse() : groups;
+ const groups = this.groupSort === "ascending" ? this.allGroups.sort(sortGroups) : this.groupSort === "descending" ? this.allGroups.sort(sortGroups).reverse() : this.allGroups;
return (
<div className="group-interface">
@@ -399,7 +355,7 @@ export class GroupManager extends React.Component<{}> {
<div className="group-heading">
<p><b>Manage Groups</b></p>
<button onClick={action(() => this.createGroupModalOpen = true)}>
- <FontAwesomeIcon icon={"plus-hexagon"} size={"sm"} /> Create Group
+ <FontAwesomeIcon icon={"plus"} size={"sm"} /> Create Group
</button>
<div className={"close-button"} onClick={this.close}>
<FontAwesomeIcon icon={"times"} color={"black"} size={"lg"} />
@@ -418,9 +374,9 @@ export class GroupManager extends React.Component<{}> {
{groups.map(group =>
<div
className="group-row"
- key={StrCast(group.groupName)}
+ key={StrCast(group.title || group.groupName)}
>
- <div className="group-name" >{group.groupName}</div>
+ <div className="group-name" >{group.title || group.groupName}</div>
<div className="group-info" onClick={action(() => this.currentGroup = group)}>
<FontAwesomeIcon icon={"info-circle"} color={"#e8e8e8"} size={"sm"} style={{ backgroundColor: "#1e89d7", borderRadius: "100%", border: "1px solid #1e89d7" }} />
</div>
@@ -434,16 +390,13 @@ export class GroupManager extends React.Component<{}> {
}
render() {
- return (
- <MainViewModal
- contents={this.groupInterface}
- isDisplayed={this.isOpen}
- interactive={true}
- dialogueBoxStyle={{ zIndex: 1002 }}
- overlayStyle={{ zIndex: 1001 }}
- closeOnExternalClick={this.close}
- />
- );
+ return <MainViewModal
+ contents={this.groupInterface}
+ isDisplayed={this.isOpen}
+ interactive={true}
+ dialogueBoxStyle={{ zIndex: 1002 }}
+ overlayStyle={{ zIndex: 1001 }}
+ closeOnExternalClick={this.close}
+ />;
}
-
} \ No newline at end of file
diff --git a/src/client/util/GroupMemberView.tsx b/src/client/util/GroupMemberView.tsx
index 4ead01e9f..927200ed3 100644
--- a/src/client/util/GroupMemberView.tsx
+++ b/src/client/util/GroupMemberView.tsx
@@ -33,8 +33,8 @@ export class GroupMemberView extends React.Component<GroupMemberViewProps> {
<input
className="group-title"
style={{ marginLeft: !hasEditAccess ? "-14%" : 0 }}
- value={StrCast(this.props.group.groupName)}
- onChange={e => this.props.group.groupName = e.currentTarget.value}
+ value={StrCast(this.props.group.title || this.props.group.groupName)}
+ onChange={e => this.props.group.title = e.currentTarget.value}
disabled={!hasEditAccess}
>
</input>
diff --git a/src/client/util/Import & Export/ImageUtils.ts b/src/client/util/Import & Export/ImageUtils.ts
index 0d12b39b8..9bd92a316 100644
--- a/src/client/util/Import & Export/ImageUtils.ts
+++ b/src/client/util/Import & Export/ImageUtils.ts
@@ -1,7 +1,6 @@
import { Doc } from "../../../fields/Doc";
import { ImageField } from "../../../fields/URLField";
-import { Cast, StrCast } from "../../../fields/Types";
-import { Docs } from "../../documents/Documents";
+import { Cast, StrCast, NumCast } from "../../../fields/Types";
import { Networking } from "../../Network";
import { Id } from "../../../fields/FieldSymbols";
import { Utils } from "../../../Utils";
@@ -22,6 +21,7 @@ export namespace ImageUtils {
} = await Networking.PostToServer("/inspectImage", { source });
document.exif = error || Doc.Get.FromJson({ data });
const proto = Doc.GetProto(document);
+ nativeWidth && (document._height = NumCast(document._width) * nativeHeight / nativeWidth);
proto["data-nativeWidth"] = nativeWidth;
proto["data-nativeHeight"] = nativeHeight;
proto["data-path"] = source;
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index 4f3cfcd03..802b8ae7b 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -1,8 +1,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";
+import { computedFn } from "mobx-utils";
/*
* link doc:
@@ -23,181 +22,58 @@ import { CurrentUserUtils } from "./CurrentUserUtils";
export class LinkManager {
private static _instance: LinkManager;
-
public static currentLink: Opt<Doc>;
-
- public static get Instance(): LinkManager {
- return this._instance || (this._instance = new this());
- }
-
- 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) : [];
- }
-
- public addLink(linkDoc: Doc): boolean {
- if (LinkManager.Instance.LinkManagerDoc) {
- Doc.AddDocToList(LinkManager.Instance.LinkManagerDoc, "data", linkDoc);
- return true;
- }
- return false;
- }
-
- public deleteLink(linkDoc: Doc): boolean {
- if (LinkManager.Instance.LinkManagerDoc && linkDoc instanceof Doc) {
- Doc.RemoveDocFromList(LinkManager.Instance.LinkManagerDoc, "data", linkDoc);
- return true;
- }
- return false;
- }
-
- // finds all links that contain the given anchor
- public getAllDirectLinks(anchor: Doc): Doc[] {
- const related = LinkManager.Instance.getAllLinks().filter(link => link).filter(link => {
- const a1 = Cast(link.anchor1, Doc, null)
- const a2 = Cast(link.anchor2, Doc, null);
- const protomatch1 = Doc.AreProtosEqual(anchor, a1);
- const protomatch2 = Doc.AreProtosEqual(anchor, a2);
- return ((a1?.title !== undefined && a2?.title !== undefined) || link.author === Doc.CurrentUserEmail) && (protomatch1 || protomatch2 || Doc.AreProtosEqual(link, anchor));
- });
- return related;
- }
- // finds all links that contain the given anchor
- public getAllRelatedLinks(anchor: Doc): Doc[] {
- const related = LinkManager.Instance.getAllDirectLinks(anchor);
- DocListCast(anchor[Doc.LayoutFieldKey(anchor) + "-annotations"]).map(anno => {
- related.push(...LinkManager.Instance.getAllRelatedLinks(anno));
+ public static get Instance(): LinkManager { return this._instance || (this._instance = new this()); }
+
+ public addLink(linkDoc: Doc) { return Doc.AddDocToList(Doc.LinkDBDoc(), "data", linkDoc); }
+ public deleteLink(linkDoc: Doc) { return Doc.RemoveDocFromList(Doc.LinkDBDoc(), "data", linkDoc); }
+ public deleteAllLinksOnAnchor(anchor: Doc) { LinkManager.Instance.relatedLinker(anchor).forEach(linkDoc => LinkManager.Instance.deleteLink(linkDoc)); }
+
+ public getAllRelatedLinks(anchor: Doc) { return this.relatedLinker(anchor); } // finds all links that contain the given anchor
+ public getAllDirectLinks(anchor: Doc): Doc[] { return this.directLinker(anchor); } // finds all links that contain the given anchor
+ public getAllLinks(): Doc[] { return this.allLinks(); }
+
+ allLinks = computedFn(function allLinks(this: any): Doc[] {
+ const lset = new Set<Doc>(DocListCast(Doc.LinkDBDoc().data));
+ SharingManager.Instance.users.forEach(user => DocListCast(user.linkDatabase?.data).forEach(doc => lset.add(doc)));
+ return Array.from(lset);
+ }, true);
+
+ directLinker = computedFn(function directLinker(this: any, anchor: Doc): Doc[] {
+ return LinkManager.Instance.allLinks().filter(link => {
+ const a1 = Cast(link?.anchor1, Doc, null);
+ const a2 = Cast(link?.anchor2, Doc, null);
+ return link && ((a1?.author !== undefined && a2?.author !== undefined) || link.author === Doc.CurrentUserEmail) && (Doc.AreProtosEqual(anchor, a1) || Doc.AreProtosEqual(anchor, a2) || Doc.AreProtosEqual(link, anchor));
});
- return related;
- }
-
- public deleteAllLinksOnAnchor(anchor: Doc) {
- const related = LinkManager.Instance.getAllRelatedLinks(anchor);
- 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;
- }
+ }, true);
- // 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))) {
- return DocListCast(linkDoc.anchor1Groups);
- } else {
- return DocListCast(linkDoc.anchor2Groups);
- }
- }
- public addGroupToAnchor(linkDoc: Doc, anchor: Doc, groupDoc: Doc, replace: boolean = false) {
- Doc.GetProto(linkDoc).linkRelationship = groupDoc.linkRelationship;
- }
-
- // removes group doc of given group type only from given anchor on given link
- public removeGroupFromAnchor(linkDoc: Doc, anchor: Doc, groupType: string) {
- Doc.GetProto(linkDoc).linkRelationship = "-ungrouped-";
- }
+ relatedLinker = computedFn(function relatedLinker(this: any, anchor: Doc): Doc[] {
+ return DocListCast(anchor[Doc.LayoutFieldKey(anchor) + "-annotations"]).reduce((list, anno) =>
+ [...list, ...LinkManager.Instance.relatedLinker(anno)],
+ LinkManager.Instance.directLinker(anchor).slice());
+ }, true);
// returns map of group type to anchor's links in that group type
public getRelatedGroupedLinks(anchor: Doc): Map<string, Array<Doc>> {
- const related = this.getAllRelatedLinks(anchor);
const anchorGroups = new Map<string, Array<Doc>>();
- related.forEach(link => {
+ this.relatedLinker(anchor).forEach(link => {
if (!link.linkRelationship || link?.linkRelationship !== "-ungrouped-") {
const group = anchorGroups.get(StrCast(link.linkRelationship));
anchorGroups.set(StrCast(link.linkRelationship), group ? [...group, link] : [link]);
-
} else {
// if link is in no groups then put it in default group
const group = anchorGroups.get("*");
anchorGroups.set("*", group ? [...group, link] : [link]);
}
-
});
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[] = [];
- const allLinks = LinkManager.Instance.getAllLinks();
- allLinks.forEach(linkDoc => {
- if (StrCast(linkDoc.linkRelationship).toUpperCase() === groupType.toUpperCase()) { md.push(linkDoc); }
- });
- return md;
- }
-
// checks if a link with the given anchors exists
public doesLinkExist(anchor1: Doc, anchor2: Doc): boolean {
- const allLinks = LinkManager.Instance.getAllLinks();
- const index = allLinks.findIndex(linkDoc => {
- return (Doc.AreProtosEqual(Cast(linkDoc.anchor1, Doc, null), anchor1) && Doc.AreProtosEqual(Cast(linkDoc.anchor2, Doc, null), anchor2)) ||
- (Doc.AreProtosEqual(Cast(linkDoc.anchor1, Doc, null), anchor2) && Doc.AreProtosEqual(Cast(linkDoc.anchor2, Doc, null), anchor1));
- });
- return index !== -1;
+ return -1 !== LinkManager.Instance.allLinks().findIndex(linkDoc =>
+ (Doc.AreProtosEqual(Cast(linkDoc.anchor1, Doc, null), anchor1) && Doc.AreProtosEqual(Cast(linkDoc.anchor2, Doc, null), anchor2)) ||
+ (Doc.AreProtosEqual(Cast(linkDoc.anchor1, Doc, null), anchor2) && Doc.AreProtosEqual(Cast(linkDoc.anchor2, Doc, null), anchor1)));
}
// finds the opposite anchor of a given anchor in a link
@@ -208,6 +84,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/util/SearchUtil.ts b/src/client/util/SearchUtil.ts
index b09eff849..79759a71d 100644
--- a/src/client/util/SearchUtil.ts
+++ b/src/client/util/SearchUtil.ts
@@ -42,8 +42,9 @@ export namespace SearchUtil {
let replacedQuery = query.replace(/type_t:([^ )])/g, (substring, arg) => `{!join from=id to=proto_i}*:* AND ${arg}`);
if (options.onlyAliases) {
const header = query.match(/_[atnb]?:/) ? replacedQuery : "DEFAULT:" + replacedQuery;
- replacedQuery = `{!join from=id to=proto_i}${header}`;
+ replacedQuery = `{!join from=id to=proto_i}* AND ${header}`;
}
+ //console.log("Q: " + replacedQuery + " fq: " + options.fq);
const gotten = await rp.get(rpquery, { qs: { ...options, q: replacedQuery } });
const result: IdSearchResult = gotten.startsWith("<") ? { ids: [], docs: [], numFound: 0, lines: [] } : JSON.parse(gotten);
if (!returnDocs) {
@@ -59,12 +60,14 @@ export namespace SearchUtil {
const fileids = txtresult ? txtresult.ids : [];
const newIds: string[] = [];
const newLines: string[][] = [];
- await Promise.all(fileids.map(async (tr: string, i: number) => {
- const docQuery = "fileUpload_t:" + tr.substr(0, 7); //If we just have a filter query, search for * as the query
- const docResult = JSON.parse(await rp.get(Utils.prepend("/dashsearch"), { qs: { ...options, q: docQuery } }));
- newIds.push(...docResult.ids);
- newLines.push(...docResult.ids.map((dr: any) => txtresult.lines[i]));
- }));
+ if (fileids) {
+ await Promise.all(fileids.map(async (tr: string, i: number) => {
+ const docQuery = "fileUpload_t:" + tr.substr(0, 7); //If we just have a filter query, search for * as the query
+ const docResult = JSON.parse(await rp.get(Utils.prepend("/dashsearch"), { qs: { ...options, q: docQuery } }));
+ newIds.push(...docResult.ids);
+ newLines.push(...docResult.ids.map((dr: any) => txtresult.lines[i]));
+ }));
+ }
const theDocs: Doc[] = [];
@@ -91,7 +94,7 @@ export namespace SearchUtil {
}
}
- return { docs: theDocs, numFound: result.numFound, highlighting, lines: theLines };
+ return { docs: theDocs, numFound: Math.max(0, result.numFound), highlighting, lines: theLines };
}
export async function GetAliasesOfDocument(doc: Doc): Promise<Doc[]>;
diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index 008ce281c..34e88c7b0 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -2,7 +2,6 @@ import { observable, action, runInAction, ObservableMap } from "mobx";
import { Doc, Opt } from "../../fields/Doc";
import { DocumentView } from "../views/nodes/DocumentView";
import { computedFn } from "mobx-utils";
-import { List } from "../../fields/List";
import { CollectionSchemaView } from "../views/collections/CollectionSchemaView";
import { CollectionViewType } from "../views/collections/CollectionView";
@@ -67,15 +66,16 @@ export namespace SelectionManager {
manager.SelectSchemaDoc(colSchema, document);
}
+ const IsSelectedCache = computedFn(function isSelected(doc: DocumentView) { // wraapping get() in a computedFn only generates mobx() invalidations when the return value of the function for the specific get parameters has changed
+ return manager.SelectedDocuments.get(doc) ? true : false;
+ });
// computed functions, such as used in IsSelected generate errors if they're called outside of a
// reaction context. Specifying the context with 'outsideReaction' allows an efficiency feature
// to avoid unnecessary mobx invalidations when running inside a reaction.
export function IsSelected(doc: DocumentView | undefined, outsideReaction?: boolean): boolean {
return !doc ? false : outsideReaction ?
manager.SelectedDocuments.get(doc) ? true : false : // get() accesses a hashtable -- setting anything in the hashtable generates a mobx invalidation for every get()
- computedFn(function isSelected(doc: DocumentView) { // wraapping get() in a computedFn only generates mobx() invalidations when the return value of the function for the specific get parameters has changed
- return manager.SelectedDocuments.get(doc) ? true : false;
- })(doc);
+ IsSelectedCache(doc);
}
export function DeselectAll(except?: Doc): void {
diff --git a/src/client/util/SerializationHelper.ts b/src/client/util/SerializationHelper.ts
index 19b217726..00ac6e521 100644
--- a/src/client/util/SerializationHelper.ts
+++ b/src/client/util/SerializationHelper.ts
@@ -43,7 +43,7 @@ export namespace SerializationHelper {
}
if (!obj.__type) {
- if (ClientUtils.RELEASE) {
+ if (true || ClientUtils.RELEASE) {
console.warn("No property 'type' found in JSON.");
return undefined;
} else {
diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx
index 17e93ad17..9934f26d3 100644
--- a/src/client/util/SettingsManager.tsx
+++ b/src/client/util/SettingsManager.tsx
@@ -4,7 +4,7 @@ import { observer } from "mobx-react";
import * as React from "react";
import { ColorState, SketchPicker } from "react-color";
import { Doc } from "../../fields/Doc";
-import { BoolCast, StrCast } from "../../fields/Types";
+import { BoolCast, StrCast, Cast } from "../../fields/Types";
import { addStyleSheet, addStyleSheetRule, Utils } from "../../Utils";
import { GoogleAuthenticationManager } from "../apis/GoogleAuthenticationManager";
import { DocServer } from "../DocServer";
@@ -56,7 +56,7 @@ export class SettingsManager extends React.Component<{}> {
@undoBatch changeFontFamily = action((e: React.ChangeEvent) => Doc.UserDoc().fontFamily = (e.currentTarget as any).value);
@undoBatch changeFontSize = action((e: React.ChangeEvent) => Doc.UserDoc().fontSize = (e.currentTarget as any).value);
@undoBatch switchActiveBackgroundColor = action((color: ColorState) => Doc.UserDoc().activeCollectionBackground = String(color.hex));
- @undoBatch switchUserColor = action((color: ColorState) => Doc.UserDoc().userColor = String(color.hex));
+ @undoBatch switchUserColor = action((color: ColorState) => Doc.SharingDoc().userColor = String(color.hex));
@undoBatch
playgroundModeToggle = action(() => {
this.playgroundMode = !this.playgroundMode;
@@ -205,6 +205,6 @@ export class SettingsManager extends React.Component<{}> {
isDisplayed={this.isOpen}
interactive={true}
closeOnExternalClick={this.close}
- dialogueBoxStyle={{ width: "600px" }} />;
+ dialogueBoxStyle={{ width: "600px", background: Cast(Doc.SharingDoc().userColor, "string", null) }} />;
}
} \ No newline at end of file
diff --git a/src/client/util/SharingManager.scss b/src/client/util/SharingManager.scss
index b756716bf..9dc57dd1e 100644
--- a/src/client/util/SharingManager.scss
+++ b/src/client/util/SharingManager.scss
@@ -40,7 +40,7 @@
.permissions-select {
z-index: 1;
- margin-left: -100;
+ margin-left: -115;
border: none;
outline: none;
text-align: justify; // for Edge
@@ -59,6 +59,7 @@
margin-top: -17px;
margin-bottom: 10px;
font-size: 10px;
+ min-height: 40px;
input {
height: 10px;
@@ -71,21 +72,25 @@
}
}
- .layoutDoc-acls {
+ .acl-container {
display: flex;
- flex-direction: column;
float: right;
- margin-right: 12;
- margin-top: -15;
- align-items: center;
+ align-items: baseline;
+ margin-top: -12;
- label {
- font-weight: normal;
- font-style: italic;
- }
+ .layoutDoc-acls,
+ .myDocs-acls {
+ flex-direction: column;
+ margin-right: 12;
- input {
- cursor: pointer;
+ label {
+ font-weight: normal;
+ font-style: italic;
+ }
+
+ input {
+ cursor: pointer;
+ }
}
}
}
diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx
index a53ef3cde..2b13d2a44 100644
--- a/src/client/util/SharingManager.tsx
+++ b/src/client/util/SharingManager.tsx
@@ -4,10 +4,10 @@ import { observer } from "mobx-react";
import * as React from "react";
import Select from "react-select";
import * as RequestPromise from "request-promise";
-import { AclAdmin, AclPrivate, DataSym, Doc, DocListCast, Opt, AclSym } from "../../fields/Doc";
+import { AclAdmin, AclPrivate, DataSym, Doc, DocListCast, Opt, AclSym, AclAddonly, AclEdit, AclReadonly, DocListCastAsync } from "../../fields/Doc";
import { List } from "../../fields/List";
import { Cast, StrCast } from "../../fields/Types";
-import { distributeAcls, GetEffectiveAcl, SharingPermissions, TraceMobx } from "../../fields/util";
+import { distributeAcls, GetEffectiveAcl, SharingPermissions, TraceMobx, normalizeEmail } from "../../fields/util";
import { Utils } from "../../Utils";
import { DocServer } from "../DocServer";
import { CollectionView } from "../views/collections/CollectionView";
@@ -21,10 +21,12 @@ import { GroupMemberView } from "./GroupMemberView";
import "./SharingManager.scss";
import { SelectionManager } from "./SelectionManager";
import { intersection } from "lodash";
+import { SearchBox } from "../views/search/SearchBox";
export interface User {
email: string;
- userDocumentId: string;
+ sharingDocumentId: string;
+ linkDatabaseId: string;
}
/**
@@ -46,20 +48,20 @@ const groupType = "!groupType/";
const storage = "data";
/**
- * A user who also has a notificationDoc.
+ * A user who also has a sharing doc.
*/
interface ValidatedUser {
- user: User;
- notificationDoc: Doc;
- userColor: string;
+ user: User; // database minimal info to identify / communicate with a user (email, sharing doc id)
+ sharingDoc: Doc; // document to share/message another user
+ linkDatabase: Doc;
+ userColor: string; // stored on the sharinDoc, extracted for convenience?
}
-
@observer
export class SharingManager extends React.Component<{}> {
public static Instance: SharingManager;
@observable private isOpen = false; // whether the SharingManager modal is open or not
- @observable public users: ValidatedUser[] = []; // the list of users with notificationDocs
+ @observable public users: ValidatedUser[] = []; // the list of users with sharing docs
@observable private targetDoc: Doc | undefined; // the document being shared
@observable private targetDocView: DocumentView | undefined; // the DocumentView of the document being shared
// @observable private copied = false;
@@ -70,18 +72,19 @@ export class SharingManager extends React.Component<{}> {
@observable private individualSort: "ascending" | "descending" | "none" = "none"; // sorting options for the list of individuals
@observable private groupSort: "ascending" | "descending" | "none" = "none"; // sorting options for the list of groups
private shareDocumentButtonRef: React.RefObject<HTMLButtonElement> = React.createRef(); // ref for the share button, used for the position of the popup
+ private distributeAclsButtonRef: React.RefObject<HTMLButtonElement> = React.createRef(); // ref for the distribute button, used for the position of the popup
// if both showUserOptions and showGroupOptions are false then both are displayed
@observable private showUserOptions: boolean = false; // whether to show individuals as options when sharing (in the react-select component)
@observable private showGroupOptions: boolean = false; // // whether to show groups as options when sharing (in the react-select component)
private populating: boolean = false; // whether the list of users is populating or not
@observable private layoutDocAcls: boolean = false; // whether the layout doc or data doc's acls are to be used
+ @observable private myDocAcls: boolean = false;
// private get linkVisible() {
// return this.sharingDoc ? this.sharingDoc[PublicKey] !== SharingPermissions.None : false;
// }
public open = (target?: DocumentView, target_doc?: Doc) => {
- runInAction(() => this.users = []);
this.populateUsers();
runInAction(() => {
this.targetDocView = target;
@@ -116,30 +119,35 @@ export class SharingManager extends React.Component<{}> {
}
/**
- * Populates the list of validated users (this.users) by adding registered users which have a mySharedDocs.
+ * Populates the list of validated users (this.users) by adding registered users which have a sharingDocument.
*/
populateUsers = async () => {
if (!this.populating) {
this.populating = true;
- runInAction(() => this.users = []);
const userList = await RequestPromise.get(Utils.prepend("/getUsers"));
const raw = JSON.parse(userList) as User[];
+ const sharingDocs: ValidatedUser[] = [];
const evaluating = raw.map(async user => {
const isCandidate = user.email !== Doc.CurrentUserEmail;
if (isCandidate) {
- const userDocument = await DocServer.GetRefField(user.userDocumentId);
- if (userDocument instanceof Doc) {
- const notificationDoc = await Cast(userDocument.mySharedDocs, Doc);
- const userColor = StrCast(userDocument.userColor);
- runInAction(() => {
- if (notificationDoc instanceof Doc) {
- this.users.push({ user, notificationDoc, userColor });
- }
- });
+ const sharingDoc = await DocServer.GetRefField(user.sharingDocumentId);
+ const linkDatabase = await DocServer.GetRefField(user.linkDatabaseId);
+ if (sharingDoc instanceof Doc && linkDatabase instanceof Doc) {
+ await DocListCastAsync(linkDatabase.data);
+ sharingDocs.push({ user, sharingDoc, linkDatabase, userColor: StrCast(sharingDoc.color) });
}
}
});
- return Promise.all(evaluating).then(() => this.populating = false);
+ return Promise.all(evaluating).then(() => {
+ runInAction(() => {
+ for (const sharer of sharingDocs) {
+ if (!this.users.find(user => user.user.email === sharer.user.email)) {
+ this.users.push(sharer);
+ }
+ }
+ });
+ this.populating = false;
+ });
}
}
@@ -148,17 +156,17 @@ export class SharingManager extends React.Component<{}> {
* @param group
* @param permission
*/
- setInternalGroupSharing = (group: Doc | { groupName: string }, permission: string, targetDoc?: Doc) => {
+ setInternalGroupSharing = (group: Doc | { title: string }, permission: string, targetDoc?: Doc) => {
const target = targetDoc || this.targetDoc!;
- const key = StrCast(group.groupName).replace(".", "_");
+ const key = normalizeEmail(StrCast(group.title));
const acl = `acl-${key}`;
const docs = SelectionManager.SelectedDocuments().length < 2 ? [target] : SelectionManager.SelectedDocuments().map(docView => docView.props.Document);
docs.forEach(doc => {
- doc.author === Doc.CurrentUserEmail && !doc[`acl-${Doc.CurrentUserEmail.replace(".", "_")}`] && distributeAcls(`acl-${Doc.CurrentUserEmail.replace(".", "_")}`, SharingPermissions.Admin, doc);
- GetEffectiveAcl(doc) === AclAdmin && distributeAcls(acl, permission as SharingPermissions, doc);
+ doc.author === Doc.CurrentUserEmail && !doc[`acl-${Doc.CurrentUserEmailNormalized}`] && distributeAcls(`acl-${Doc.CurrentUserEmailNormalized}`, SharingPermissions.Admin, doc);
+ distributeAcls(acl, permission as SharingPermissions, doc);
if (group instanceof Doc) {
const members: string[] = JSON.parse(StrCast(group.members));
@@ -167,9 +175,9 @@ export class SharingManager extends React.Component<{}> {
// if documents have been shared, add the doc to that list if it doesn't already exist, otherwise create a new list with the doc
group.docsShared ? Doc.IndexOf(doc, DocListCast(group.docsShared)) === -1 && (group.docsShared as List<Doc>).push(doc) : group.docsShared = new List<Doc>([doc]);
- users.forEach(({ user, notificationDoc }) => {
- if (permission !== SharingPermissions.None) Doc.IndexOf(doc, DocListCast(notificationDoc[storage])) === -1 && Doc.AddDocToList(notificationDoc, storage, doc); // add the doc to the notificationDoc if it hasn't already been added
- else GetEffectiveAcl(doc, undefined, user.email) === AclPrivate && Doc.IndexOf((doc.aliasOf as Doc || doc), DocListCast(notificationDoc[storage])) !== -1 && Doc.RemoveDocFromList(notificationDoc, storage, (doc.aliasOf as Doc || doc)); // remove the doc from the list if it already exists
+ users.forEach(({ user, sharingDoc }) => {
+ if (permission !== SharingPermissions.None) Doc.AddDocToList(sharingDoc, storage, doc); // add the doc to the sharingDoc if it hasn't already been added
+ else GetEffectiveAcl(doc, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, storage, (doc.aliasOf as Doc || doc)); // remove the doc from the list if it already exists
});
}
});
@@ -180,16 +188,26 @@ export class SharingManager extends React.Component<{}> {
* @param group
* @param emailId
*/
- shareWithAddedMember = (group: Doc, emailId: string) => {
- const user: ValidatedUser = this.users.find(({ user: { email } }) => email === emailId)!;
- if (group.docsShared) DocListCast(group.docsShared).forEach(doc => Doc.IndexOf(doc, DocListCast(user.notificationDoc[storage])) === -1 && Doc.AddDocToList(user.notificationDoc, storage, doc));
+ shareWithAddedMember = (group: Doc, emailId: string, retry: boolean = true) => {
+ const user = this.users.find(({ user: { email } }) => email === emailId)!;
+ const self = this;
+ if (group.docsShared) {
+ if (!user) retry && this.populateUsers().then(() => self.shareWithAddedMember(group, emailId, false));
+ else {
+ DocListCastAsync(user.sharingDoc[storage]).then(userdocs =>
+ DocListCastAsync(group.docsShared).then(dl => {
+ const filtered = dl?.filter(doc => !userdocs?.includes(doc));
+ filtered && userdocs?.push(...filtered);
+ }));
+ }
+ }
}
/**
* Called from the properties sidebar to change permissions of a user.
*/
shareFromPropertiesSidebar = (shareWith: string, permission: SharingPermissions, docs: Doc[]) => {
- if (shareWith !== "Public") {
+ if (shareWith !== "Public" && shareWith !== "Override") {
const user = this.users.find(({ user: { email } }) => email === (shareWith === "Me" ? Doc.CurrentUserEmail : shareWith));
docs.forEach(doc => {
if (user) this.setInternalSharing(user, permission, doc);
@@ -198,7 +216,7 @@ export class SharingManager extends React.Component<{}> {
}
else {
docs.forEach(doc => {
- if (GetEffectiveAcl(doc) === AclAdmin) distributeAcls("acl-Public", permission, doc);
+ if (GetEffectiveAcl(doc) === AclAdmin) distributeAcls(`acl-${shareWith}`, permission, doc);
});
}
}
@@ -212,9 +230,12 @@ export class SharingManager extends React.Component<{}> {
const user: ValidatedUser = this.users.find(({ user: { email } }) => email === emailId)!;
if (group.docsShared) {
- DocListCast(group.docsShared).forEach(doc => {
- Doc.IndexOf(doc, DocListCast(user.notificationDoc[storage])) !== -1 && Doc.RemoveDocFromList(user.notificationDoc, storage, doc); // remove the doc only if it is in the list
- });
+ DocListCastAsync(user.sharingDoc[storage]).then(userdocs =>
+ DocListCastAsync(group.docsShared).then(dl => {
+ const remaining = userdocs?.filter(doc => !dl?.includes(doc)) || [];
+ userdocs?.splice(0, userdocs.length, ...remaining);
+ })
+ );
}
}
@@ -225,14 +246,14 @@ export class SharingManager extends React.Component<{}> {
removeGroup = (group: Doc) => {
if (group.docsShared) {
DocListCast(group.docsShared).forEach(doc => {
- const acl = `acl-${StrCast(group.groupName)}`;
+ const acl = `acl-${StrCast(group.title)}`;
distributeAcls(acl, SharingPermissions.None, doc);
const members: string[] = JSON.parse(StrCast(group.members));
const users: ValidatedUser[] = this.users.filter(({ user: { email } }) => members.includes(email));
- users.forEach(({ notificationDoc }) => Doc.RemoveDocFromList(notificationDoc, storage, doc));
+ users.forEach(({ sharingDoc }) => Doc.RemoveDocFromList(sharingDoc, storage, doc));
});
}
}
@@ -241,20 +262,18 @@ export class SharingManager extends React.Component<{}> {
* Shares the document with a user.
*/
setInternalSharing = (recipient: ValidatedUser, permission: string, targetDoc?: Doc) => {
- const { user, notificationDoc } = recipient;
+ const { user, sharingDoc } = recipient;
const target = targetDoc || this.targetDoc!;
- const key = user.email.replace('.', '_');
- const acl = `acl-${key}`;
-
+ const acl = `acl-${normalizeEmail(user.email)}`;
+ const myAcl = `acl-${Doc.CurrentUserEmailNormalized}`;
const docs = SelectionManager.SelectedDocuments().length < 2 ? [target] : SelectionManager.SelectedDocuments().map(docView => docView.props.Document);
-
docs.forEach(doc => {
- doc.author === Doc.CurrentUserEmail && !doc[`acl-${Doc.CurrentUserEmail.replace(".", "_")}`] && distributeAcls(`acl-${Doc.CurrentUserEmail.replace(".", "_")}`, SharingPermissions.Admin, doc);
- GetEffectiveAcl(doc) === AclAdmin && distributeAcls(acl, permission as SharingPermissions, doc);
+ doc.author === Doc.CurrentUserEmail && !doc[myAcl] && distributeAcls(myAcl, SharingPermissions.Admin, doc);
+ distributeAcls(acl, permission as SharingPermissions, doc);
- if (permission !== SharingPermissions.None) Doc.IndexOf(doc, DocListCast(notificationDoc[storage])) === -1 && Doc.AddDocToList(notificationDoc, storage, doc);
- else GetEffectiveAcl(doc, undefined, user.email) === AclPrivate && Doc.IndexOf((doc.aliasOf as Doc || doc), DocListCast(notificationDoc[storage])) !== -1 && Doc.RemoveDocFromList(notificationDoc, storage, (doc.aliasOf as Doc || doc));
+ if (permission !== SharingPermissions.None) Doc.AddDocToList(sharingDoc, storage, doc);
+ else GetEffectiveAcl(doc, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, storage, (doc.aliasOf as Doc || doc));
});
}
@@ -285,13 +304,14 @@ export class SharingManager extends React.Component<{}> {
/**
* Returns the SharingPermissions (Admin, Can Edit etc) access that's used to share
*/
- private sharingOptions(uniform: boolean) {
+ private sharingOptions(uniform: boolean, override?: boolean) {
const dropdownValues: string[] = Object.values(SharingPermissions);
if (!uniform) dropdownValues.unshift("-multiple-");
- return dropdownValues.map(permission =>
+ if (override) dropdownValues.unshift("None");
+ return dropdownValues.filter(permission => permission !== SharingPermissions.View).map(permission =>
(
<option key={permission} value={permission}>
- {permission}
+ {permission === SharingPermissions.Add ? "Can Augment" : permission}
</option>
)
);
@@ -372,6 +392,25 @@ export class SharingManager extends React.Component<{}> {
}
}
+ distributeOverCollection = (targetDoc?: Doc) => {
+ const AclMap = new Map<symbol, string>([
+ [AclPrivate, SharingPermissions.None],
+ [AclReadonly, SharingPermissions.View],
+ [AclAddonly, SharingPermissions.Add],
+ [AclEdit, SharingPermissions.Edit],
+ [AclAdmin, SharingPermissions.Admin]
+ ]);
+
+ const target = targetDoc || this.targetDoc!;
+
+ const docs = SelectionManager.SelectedDocuments().length < 2 ? [target] : SelectionManager.SelectedDocuments().map(docView => docView.props.Document);
+ docs.forEach(doc => {
+ for (const [key, value] of Object.entries(doc[AclSym])) {
+ distributeAcls(key, AclMap.get(value)! as SharingPermissions, target);
+ }
+ });
+ }
+
/**
* Sorting algorithm to sort users.
*/
@@ -385,8 +424,8 @@ export class SharingManager extends React.Component<{}> {
* Sorting algorithm to sort groups.
*/
sortGroups = (group1: Doc, group2: Doc) => {
- const g1 = StrCast(group1.groupName);
- const g2 = StrCast(group2.groupName);
+ const g1 = StrCast(group1.title);
+ const g2 = StrCast(group2.title);
return g1 < g2 ? -1 : g1 === g2 ? 0 : 1;
}
@@ -395,9 +434,9 @@ export class SharingManager extends React.Component<{}> {
*/
@computed get sharingInterface() {
TraceMobx();
- const groupList = GroupManager.Instance?.getAllGroups() || [];
+ const groupList = GroupManager.Instance?.allGroups || [];
const sortedUsers = this.users.slice().sort(this.sortUsers).map(({ user: { email } }) => ({ label: email, value: indType + email }));
- const sortedGroups = groupList.slice().sort(this.sortGroups).map(({ groupName }) => ({ label: StrCast(groupName), value: groupType + StrCast(groupName) }));
+ const sortedGroups = groupList.slice().sort(this.sortGroups).map(({ title }) => ({ label: StrCast(title), value: groupType + StrCast(title) }));
// the next block handles the users shown (individuals/groups/both)
const options: GroupedOptions[] = [];
@@ -415,21 +454,27 @@ export class SharingManager extends React.Component<{}> {
const groups = this.groupSort === "ascending" ? groupList.slice().sort(this.sortGroups) : this.groupSort === "descending" ? groupList.slice().sort(this.sortGroups).reverse() : groupList;
// handles the case where multiple documents are selected
- const docs = SelectionManager.SelectedDocuments().length < 2 ?
+ let docs = SelectionManager.SelectedDocuments().length < 2 ?
[this.layoutDocAcls ? this.targetDoc : this.targetDoc?.[DataSym]]
: SelectionManager.SelectedDocuments().map(docView => this.layoutDocAcls ? docView.props.Document : docView.props.Document?.[DataSym]);
+ if (this.myDocAcls) {
+ const newDocs: Doc[] = [];
+ SearchBox.foreachRecursiveDoc(docs, doc => newDocs.push(doc));
+ docs = newDocs.filter(doc => GetEffectiveAcl(doc) === AclAdmin);
+ }
+
const targetDoc = docs[0];
// tslint:disable-next-line: no-unnecessary-callback-wrapper
- const admin = docs.map(doc => GetEffectiveAcl(doc)).every(acl => acl === AclAdmin); // if the user has admin access to all selected docs
+ const admin = this.myDocAcls ? Boolean(docs.length) : docs.map(doc => GetEffectiveAcl(doc)).every(acl => acl === AclAdmin); // if the user has admin access to all selected docs
// users in common between all docs
const commonKeys = intersection(...docs.map(doc => this.layoutDocAcls ? doc?.[AclSym] && Object.keys(doc[AclSym]) : doc?.[DataSym]?.[AclSym] && Object.keys(doc[DataSym][AclSym])));
// the list of users shared with
- const userListContents: (JSX.Element | null)[] = users.filter(({ user }) => docs.length > 1 ? commonKeys.includes(`acl-${user.email.replace('.', '_')}`) : true).map(({ user, notificationDoc, userColor }) => {
- const userKey = `acl-${user.email.replace('.', '_')}`;
+ const userListContents: (JSX.Element | null)[] = users.filter(({ user }) => docs.length > 1 ? commonKeys.includes(`acl-${normalizeEmail(user.email)}`) : docs[0]?.author !== user.email).map(({ user, linkDatabase, sharingDoc, userColor }) => {
+ const userKey = `acl-${normalizeEmail(user.email)}`;
const uniform = docs.every(doc => this.layoutDocAcls ? doc?.[AclSym]?.[userKey] === docs[0]?.[AclSym]?.[userKey] : doc?.[DataSym]?.[AclSym]?.[userKey] === docs[0]?.[DataSym]?.[AclSym]?.[userKey]);
const permissions = uniform ? StrCast(targetDoc?.[userKey]) : "-multiple-";
@@ -440,17 +485,17 @@ export class SharingManager extends React.Component<{}> {
>
<span className={"padding"}>{user.email}</span>
<div className="edit-actions">
- {admin ? (
+ {admin || this.myDocAcls ? (
<select
className={"permissions-dropdown"}
value={permissions}
- onChange={e => this.setInternalSharing({ user, notificationDoc, userColor }, e.currentTarget.value)}
+ onChange={e => this.setInternalSharing({ user, linkDatabase, sharingDoc, userColor }, e.currentTarget.value)}
>
{this.sharingOptions(uniform)}
</select>
) : (
<div className={"permissions-dropdown"}>
- {permissions}
+ {permissions === SharingPermissions.Add ? "Can Augment" : permissions}
</div>
)}
</div>
@@ -486,7 +531,7 @@ export class SharingManager extends React.Component<{}> {
<span className={"padding"}>Me</span>
<div className="edit-actions">
<div className={"permissions-dropdown"}>
- {targetDoc?.[`acl-${Doc.CurrentUserEmail.replace(".", "_")}`]}
+ {targetDoc?.[`acl-${Doc.CurrentUserEmailNormalized}`]}
</div>
</div>
</div>
@@ -495,32 +540,32 @@ export class SharingManager extends React.Component<{}> {
// the list of groups shared with
- const groupListMap: (Doc | { groupName: string })[] = groups.filter(({ groupName }) => docs.length > 1 ? commonKeys.includes(`acl-${StrCast(groupName).replace('.', '_')}`) : true);
- groupListMap.unshift({ groupName: "Public" });
+ const groupListMap: (Doc | { title: string })[] = groups.filter(({ title }) => docs.length > 1 ? commonKeys.includes(`acl-${normalizeEmail(StrCast(title))}`) : true);
+ groupListMap.unshift({ title: "Public" }, { title: "Override" });
const groupListContents = groupListMap.map(group => {
- const groupKey = `acl-${StrCast(group.groupName)}`;
+ const groupKey = `acl-${StrCast(group.title)}`;
const uniform = docs.every(doc => this.layoutDocAcls ? doc?.[AclSym]?.[groupKey] === docs[0]?.[AclSym]?.[groupKey] : doc?.[DataSym]?.[AclSym]?.[groupKey] === docs[0]?.[DataSym]?.[AclSym]?.[groupKey]);
- const permissions = uniform ? StrCast(targetDoc?.[`acl-${StrCast(group.groupName)}`]) : "-multiple-";
+ const permissions = uniform ? StrCast(targetDoc?.[`acl-${StrCast(group.title)}`]) : "-multiple-";
return !permissions ? (null) : (
<div
key={groupKey}
className={"container"}
>
- <div className={"padding"}>{group.groupName}</div>
+ <div className={"padding"}>{group.title}</div>
{group instanceof Doc ?
(<div className="group-info" onClick={action(() => GroupManager.Instance.currentGroup = group)}>
<FontAwesomeIcon icon={"info-circle"} color={"#e8e8e8"} size={"sm"} style={{ backgroundColor: "#1e89d7", borderRadius: "100%", border: "1px solid #1e89d7" }} />
</div>)
: (null)}
<div className="edit-actions">
- {admin ? (
+ {admin || this.myDocAcls ? (
<select
className={"permissions-dropdown"}
value={permissions}
onChange={e => this.setInternalGroupSharing(group, e.currentTarget.value)}
>
- {this.sharingOptions(uniform)}
+ {this.sharingOptions(uniform, group.title === "Override")}
</select>
) : (
<div className={"permissions-dropdown"}>
@@ -572,8 +617,18 @@ export class SharingManager extends React.Component<{}> {
<input type="checkbox" onChange={action(() => this.showUserOptions = !this.showUserOptions)} /> <label style={{ marginRight: 10 }}>Individuals</label>
<input type="checkbox" onChange={action(() => this.showGroupOptions = !this.showGroupOptions)} /> <label>Groups</label>
</div>
- <div className="layoutDoc-acls">
- <input type="checkbox" onChange={action(() => this.layoutDocAcls = !this.layoutDocAcls)} checked={this.layoutDocAcls} /> <label>Layout</label>
+
+ <div className="acl-container">
+ <div className="myDocs-acls">
+ <input type="checkbox" onChange={action(() => this.myDocAcls = !this.myDocAcls)} checked={this.myDocAcls} /> <label>My Docs</label>
+ </div>
+ {Doc.UserDoc().noviceMode ? (null) :
+ <div className="layoutDoc-acls">
+ <input type="checkbox" onChange={action(() => this.layoutDocAcls = !this.layoutDocAcls)} checked={this.layoutDocAcls} /> <label>Layout</label>
+ </div>}
+ <button className="distribute-button" onClick={() => this.distributeOverCollection()}>
+ Distribute
+ </button>
</div>
</div>
}
diff --git a/src/client/util/SnappingManager.ts b/src/client/util/SnappingManager.ts
index fc07e8ab4..069f81d38 100644
--- a/src/client/util/SnappingManager.ts
+++ b/src/client/util/SnappingManager.ts
@@ -1,4 +1,5 @@
import { observable, action, runInAction } from "mobx";
+import { computedFn } from "mobx-utils";
export namespace SnappingManager {
@@ -14,6 +15,9 @@ export namespace SnappingManager {
this.horizSnapLines = horizLines;
this.vertSnapLines = vertLines;
}
+
+ @observable cachedGroups: string[] = [];
+ @action setCachedGroups(groups: string[]) { this.cachedGroups = groups; }
}
const manager = new Manager();
@@ -25,5 +29,11 @@ export namespace SnappingManager {
export function SetIsDragging(dragging: boolean) { runInAction(() => manager.IsDragging = dragging); }
export function GetIsDragging() { return manager.IsDragging; }
+
+ /// bcz; argh!! TODO; These do not belong here, but there were include order problems with leaving them in util.ts
+ // need to investigate further what caused the mobx update problems and move to a better location.
+ const getCachedGroupByNameCache = computedFn(function (name: string) { return manager.cachedGroups.includes(name); }, true);
+ export function GetCachedGroupByName(name: string) { return getCachedGroupByNameCache(name); }
+ export function SetCachedGroups(groups: string[]) { manager.setCachedGroups(groups); }
}
diff --git a/src/client/util/UndoManager.ts b/src/client/util/UndoManager.ts
index 0f7ad6d0a..569ad8ab4 100644
--- a/src/client/util/UndoManager.ts
+++ b/src/client/util/UndoManager.ts
@@ -76,7 +76,7 @@ export namespace UndoManager {
export let undoStack: UndoBatch[] = observable([]);
export let redoStack: UndoBatch[] = observable([]);
let currentBatch: UndoBatch | undefined;
- let batchCounter = 0;
+ export let batchCounter = 0;
let undoing = false;
let tempEvents: UndoEvent[] | undefined = undefined;