aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/Utils.ts1
-rw-r--r--src/client/DocServer.ts9
-rw-r--r--src/client/apis/google_docs/GooglePhotosClientUtils.ts2
-rw-r--r--src/client/documents/Documents.ts130
-rw-r--r--src/client/documents/Gitlike.ts36
-rw-r--r--src/client/util/CurrentUserUtils.ts116
-rw-r--r--src/client/util/DocumentManager.ts11
-rw-r--r--src/client/util/DragManager.ts9
-rw-r--r--src/client/util/GroupMemberView.tsx1
-rw-r--r--src/client/util/HypothesisUtils.ts2
-rw-r--r--src/client/util/LinkManager.ts70
-rw-r--r--src/client/util/SettingsManager.tsx3
-rw-r--r--src/client/util/SharingManager.tsx93
-rw-r--r--src/client/views/.DS_Storebin10244 -> 10244 bytes
-rw-r--r--src/client/views/DocComponent.tsx24
-rw-r--r--src/client/views/DocumentButtonBar.scss22
-rw-r--r--src/client/views/DocumentButtonBar.tsx28
-rw-r--r--src/client/views/DocumentDecorations.scss2
-rw-r--r--src/client/views/DocumentDecorations.tsx2
-rw-r--r--src/client/views/EditableView.scss8
-rw-r--r--src/client/views/InkingStroke.tsx5
-rw-r--r--src/client/views/Main.tsx1
-rw-r--r--src/client/views/MainView.tsx8
-rw-r--r--src/client/views/MarqueeAnnotator.tsx4
-rw-r--r--src/client/views/PropertiesView.scss37
-rw-r--r--src/client/views/PropertiesView.tsx14
-rw-r--r--src/client/views/SidebarAnnos.tsx35
-rw-r--r--src/client/views/collections/CollectionDockingView.tsx54
-rw-r--r--src/client/views/collections/CollectionLinearView.scss11
-rw-r--r--src/client/views/collections/CollectionMenu.tsx7
-rw-r--r--src/client/views/collections/CollectionSchemaView.tsx4
-rw-r--r--src/client/views/collections/CollectionStackingView.tsx4
-rw-r--r--src/client/views/collections/CollectionSubView.tsx35
-rw-r--r--src/client/views/collections/CollectionTimeView.tsx14
-rw-r--r--src/client/views/collections/CollectionView.tsx11
-rw-r--r--src/client/views/collections/TabDocView.tsx14
-rw-r--r--src/client/views/collections/TreeView.tsx6
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx23
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx4
-rw-r--r--src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx4
-rw-r--r--src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx4
-rw-r--r--src/client/views/collections/collectionSchema/CollectionSchemaView.tsx4
-rw-r--r--src/client/views/global/globalCssVariables.scss24
-rw-r--r--src/client/views/linking/LinkPopup.tsx1
-rw-r--r--src/client/views/nodes/AudioBox.tsx2
-rw-r--r--src/client/views/nodes/DocumentLinksButton.scss38
-rw-r--r--src/client/views/nodes/DocumentLinksButton.tsx104
-rw-r--r--src/client/views/nodes/DocumentView.tsx25
-rw-r--r--src/client/views/nodes/FieldView.tsx6
-rw-r--r--src/client/views/nodes/ImageBox.tsx2
-rw-r--r--src/client/views/nodes/LinkDescriptionPopup.scss65
-rw-r--r--src/client/views/nodes/LinkDocPreview.tsx4
-rw-r--r--src/client/views/nodes/PDFBox.tsx112
-rw-r--r--src/client/views/nodes/ScreenshotBox.tsx27
-rw-r--r--src/client/views/nodes/VideoBox.tsx8
-rw-r--r--src/client/views/nodes/WebBox.tsx103
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx66
-rw-r--r--src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts70
-rw-r--r--src/client/views/nodes/formattedText/RichTextMenu.tsx8
-rw-r--r--src/client/views/pdf/AnchorMenu.tsx1
-rw-r--r--src/client/views/pdf/PDFViewer.tsx2
-rw-r--r--src/client/views/search/SearchBox.tsx2
-rw-r--r--src/client/views/topbar/TopBar.scss14
-rw-r--r--src/client/views/topbar/TopBar.tsx1
-rw-r--r--src/fields/Doc.ts100
-rw-r--r--src/fields/List.ts19
-rw-r--r--src/fields/URLField.ts15
-rw-r--r--src/fields/util.ts53
-rw-r--r--src/mobile/ImageUpload.tsx2
-rw-r--r--src/server/server_Initialization.ts3
-rw-r--r--src/server/websocket.ts2
71 files changed, 1010 insertions, 741 deletions
diff --git a/src/Utils.ts b/src/Utils.ts
index d87c3cc6b..194c38a6f 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -67,7 +67,6 @@ export namespace Utils {
export function prepend(extension: string): string {
return window.location.origin + extension;
}
-
export function fileUrl(filename: string): string {
return prepend(`/files/${filename}`);
}
diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts
index 1d7497cf8..e498a7cca 100644
--- a/src/client/DocServer.ts
+++ b/src/client/DocServer.ts
@@ -1,6 +1,6 @@
import * as io from 'socket.io-client';
import { MessageStore, YoutubeQueryTypes, GestureContent, MobileInkOverlayContent, UpdateMobileInkOverlayPositionContent, MobileDocumentUploadContent } from "./../server/Message";
-import { Opt, Doc, UpdatingFromServer } from '../fields/Doc';
+import { Opt, Doc, UpdatingFromServer, updateCachedAcls } from '../fields/Doc';
import { Utils, emptyFunction } from '../Utils';
import { SerializationHelper } from './util/SerializationHelper';
import { RefField } from '../fields/RefField';
@@ -61,6 +61,9 @@ export namespace DocServer {
DocServer.PlaygroundFields = livePlaygroundFields;
livePlaygroundFields.forEach(f => DocServer.setFieldWriteMode(f, DocServer.WriteMode.Playground));
}
+ export function IsPlaygroundField(field: string) {
+ return DocServer.PlaygroundFields?.includes(field.replace(/^_/, ""));
+ }
export function setFieldWriteMode(field: string, writeMode: WriteMode) {
fieldWriteModes[field] = writeMode;
@@ -225,7 +228,7 @@ export namespace DocServer {
* the server if the document has not been cached.
* @param id the id of the requested document
*/
- const _GetRefFieldImpl = (id: string, force: boolean = false): Promise<Opt<RefField>> => {
+ const _GetRefFieldImpl = async (id: string, force: boolean = false): Promise<Opt<RefField>> => {
// an initial pass through the cache to determine whether the document needs to be fetched,
// is already in the process of being fetched or already exists in the
// cache
@@ -395,7 +398,7 @@ export namespace DocServer {
(_cache[field.id] as any).then((f: any) => fieldMap[field.id] = f);
} else if (field) {
proms.push(_cache[field.id] as any);
- fieldMap[field.id] = field;
+ fieldMap[field.id] = DocServer.GetCachedRefField(field.id) || field;
}
}
});
diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
index 899e65a16..ff9460b62 100644
--- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts
+++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
@@ -285,7 +285,7 @@ export namespace GooglePhotos {
const photos = await endpoint();
const albumId = StrCast(collection.albumId);
if (albumId && albumId.length) {
- const enrichment = new photos.TextEnrichment(content || Utils.prepend("/doc/" + collection[Id]));
+ const enrichment = new photos.TextEnrichment(content || Doc.globalServerPath(collection));
const position = new photos.AlbumPosition(photos.AlbumPosition.POSITIONS.FIRST_IN_ALBUM);
const enrichmentItem = await photos.albums.addEnrichment(albumId, enrichment, position);
if (enrichmentItem) {
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 72b52616a..48886aa3b 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -1,7 +1,7 @@
import { action, runInAction } from "mobx";
import { basename, extname } from "path";
import { DateField } from "../../fields/DateField";
-import { Doc, DocListCast, DocListCastAsync, Field, HeightSym, Opt, WidthSym, Initializing } from "../../fields/Doc";
+import { Doc, DocListCast, DocListCastAsync, Field, HeightSym, Opt, WidthSym, Initializing, updateCachedAcls } from "../../fields/Doc";
import { Id } from "../../fields/FieldSymbols";
import { HtmlField } from "../../fields/HtmlField";
import { InkField } from "../../fields/InkField";
@@ -550,84 +550,6 @@ export namespace Docs {
export namespace Create {
/**
- * Synchronously returns a collection into which
- * the device documents will be put. This is initially empty,
- * but gets populated by updates from the web socket. When everything is over,
- * this function cleans up after itself.
- * s
- * Look at Websocket.ts for the server-side counterpart to this
- * function.
- */
- export function Buxton() {
- let responded = false;
- const loading = new Doc;
- loading.title = "Please wait for the import script...";
- const parent = TreeDocument([loading], {
- title: "The Buxton Collection",
- _width: 400,
- _height: 400
- });
- const parentProto = Doc.GetProto(parent);
- const { _socket } = DocServer;
-
- // just in case, clean up
- _socket.off(MessageStore.BuxtonDocumentResult.Message);
- _socket.off(MessageStore.BuxtonImportComplete.Message);
-
- // this is where the client handles the receipt of a new valid parsed document
- Utils.AddServerHandler(_socket, MessageStore.BuxtonDocumentResult, ({ device, invalid: errors }) => {
- if (!responded) {
- responded = true;
- parentProto.data = new List<Doc>();
- }
- if (device) {
- const { title, __images, additionalMedia } = device;
- delete device.__images;
- delete device.additionalMedia;
- const { ImageDocument, StackingDocument } = Docs.Create;
- const constructed = __images.map(({ url, nativeWidth, nativeHeight }) => ({ url: Utils.prepend(url), nativeWidth, nativeHeight }));
- const deviceImages = constructed.map(({ url, nativeWidth, nativeHeight }, i) => {
- const imageDoc = ImageDocument(url, {
- title: `image${i}.${extname(url)}`,
- _nativeWidth: nativeWidth,
- _nativeHeight: nativeHeight
- });
- const media = additionalMedia[i];
- if (media) {
- for (const key of Object.keys(media)) {
- imageDoc[`additionalMedia_${key}`] = Utils.prepend(`/files/${key}/buxton/${media[key]}`);
- }
- }
- return imageDoc;
- });
- // the main document we create
- const doc = StackingDocument(deviceImages, { title, hero: new ImageField(constructed[0].url) });
- doc.nameAliases = new List<string>([title.toLowerCase()]);
- // add the parsed attributes to this main document
- Doc.Get.FromJson({ data: device, appendToExisting: { targetDoc: Doc.GetProto(doc) } });
- Doc.AddDocToList(parentProto, "data", doc);
- } else if (errors) {
- console.log("Documents:" + errors);
- } else {
- alert("A Buxton document import was completely empty (??)");
- }
- });
-
- // when the import is complete, we stop listening for these creation
- // and termination events and alert the user
- Utils.AddServerHandler(_socket, MessageStore.BuxtonImportComplete, ({ deviceCount, errorCount }) => {
- _socket.off(MessageStore.BuxtonDocumentResult.Message);
- _socket.off(MessageStore.BuxtonImportComplete.Message);
- alert(`Successfully imported ${deviceCount} device${deviceCount === 1 ? "" : "s"}, with ${errorCount} error${errorCount === 1 ? "" : "s"}, in ${(Date.now() - startTime) / 1000} seconds.`);
- });
- const startTime = Date.now();
- Utils.Emit(_socket, MessageStore.BeginBuxtonImport, ""); // signal the server to start importing
- return parent; // synchronously return the collection, to be populateds
- }
-
- Scripting.addGlobal(Buxton);
-
- /**
* This function receives the relevant document prototype and uses
* it to create a new of that base-level prototype, or the
* underlying data document, which it then delegates again
@@ -655,8 +577,10 @@ export namespace Docs {
dataProps.creationDate = new DateField;
dataProps[`${fieldKey}-lastModified`] = new DateField;
dataProps["acl-Override"] = "None";
- dataProps["acl-Public"] = Doc.UserDoc()?.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Add;
+ dataProps["acl-Public"] = options["acl-Public"] ? options["acl-Public"] : Doc.UserDoc()?.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Augment;
+
dataProps[fieldKey] = data;
+
// so that the list of annotations is already initialised, prevents issues in addonly.
// without this, if a doc has no annotations but the user has AddOnly privileges, they won't be able to add an annotation because they would have needed to create the field's list which they don't have permissions to do.
dataProps[fieldKey + "-annotations"] = new List<Doc>();
@@ -664,18 +588,20 @@ export namespace Docs {
viewProps.author = Doc.CurrentUserEmail;
viewProps["acl-Override"] = "None";
- viewProps["acl-Public"] = Doc.UserDoc()?.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Add;
+ viewProps["acl-Public"] = options["_acl-Public"] ? options["_acl-Public"] : Doc.UserDoc()?.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Augment;
const viewDoc = Doc.assign(Doc.MakeDelegate(dataDoc, delegId), viewProps, true, true);
![DocumentType.LINK, DocumentType.MARKER, DocumentType.LABEL].includes(viewDoc.type as any) && DocUtils.MakeLinkToActiveAudio(() => viewDoc);
!Doc.IsSystem(dataDoc) && ![DocumentType.MARKER, DocumentType.KVP, DocumentType.LINK, DocumentType.LINKANCHOR].includes(proto.type as any) &&
!dataDoc.isFolder && !dataProps.annotationOn && Doc.AddDocToList(Cast(Doc.UserDoc().myFileOrphans, Doc, null), "data", dataDoc);
+ updateCachedAcls(dataDoc);
+ updateCachedAcls(viewDoc);
return viewDoc;
}
export function ImageDocument(url: string, options: DocumentOptions = {}) {
- const imgField = new ImageField(new URL(url));
+ const imgField = new ImageField(url);
return InstanceFromProto(Prototypes.get(DocumentType.IMG), imgField, { title: path.basename(url), ...options });
}
@@ -689,11 +615,11 @@ export namespace Docs {
}
export function VideoDocument(url: string, options: DocumentOptions = {}) {
- return InstanceFromProto(Prototypes.get(DocumentType.VID), new VideoField(new URL(url)), options);
+ return InstanceFromProto(Prototypes.get(DocumentType.VID), new VideoField(url), options);
}
export function YoutubeDocument(url: string, options: DocumentOptions = {}) {
- return InstanceFromProto(Prototypes.get(DocumentType.YOUTUBE), new YoutubeField(new URL(url)), options);
+ return InstanceFromProto(Prototypes.get(DocumentType.YOUTUBE), new YoutubeField(url), options);
}
export function WebCamDocument(url: string, options: DocumentOptions = {}) {
@@ -709,7 +635,7 @@ export namespace Docs {
}
export function AudioDocument(url: string, options: DocumentOptions = {}) {
- return InstanceFromProto(Prototypes.get(DocumentType.AUDIO), new AudioField(new URL(url)),
+ return InstanceFromProto(Prototypes.get(DocumentType.AUDIO), new AudioField(url),
{ ...options, backgroundColor: ComputedField.MakeFunction("this._mediaState === 'playing' ? 'green':'gray'") as any });
}
@@ -775,18 +701,18 @@ export namespace Docs {
I.author = Doc.CurrentUserEmail;
I.rotation = 0;
I.data = new InkField(points);
- I["acl-Public"] = Doc.UserDoc()?.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Add;
+ I["acl-Public"] = Doc.UserDoc()?.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Augment;
I["acl-Override"] = "None";
I[Initializing] = false;
return I;
}
export function PdfDocument(url: string, options: DocumentOptions = {}) {
- return InstanceFromProto(Prototypes.get(DocumentType.PDF), new PdfField(new URL(url)), options);
+ return InstanceFromProto(Prototypes.get(DocumentType.PDF), new PdfField(url), options);
}
export function WebDocument(url: string, options: DocumentOptions = {}) {
- return InstanceFromProto(Prototypes.get(DocumentType.WEB), url ? new WebField(new URL(url)) : undefined, options);
+ return InstanceFromProto(Prototypes.get(DocumentType.WEB), url ? new WebField(url) : undefined, options);
}
export function HtmlDocument(html: string, options: DocumentOptions = {}) {
@@ -797,15 +723,20 @@ export namespace Docs {
return InstanceFromProto(Prototypes.get(DocumentType.KVP), document, { title: document.title + ".kvp", ...options });
}
- export function TextanchorDocument(options: DocumentOptions = {}, id?: string) {
- return InstanceFromProto(Prototypes.get(DocumentType.MARKER), undefined, options, id);
- }
-
export function FreeformDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) {
const inst = InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Freeform }, id);
documents.map(d => d.context = inst);
return inst;
}
+
+ export function WebanchorDocument(url?: string, options: DocumentOptions = {}, id?: string) {
+ return InstanceFromProto(Prototypes.get(DocumentType.MARKER), url, options, id);
+ }
+
+ export function TextanchorDocument(options: DocumentOptions = {}, id?: string) {
+ return InstanceFromProto(Prototypes.get(DocumentType.MARKER), options?.data, options, id);
+ }
+
export function HTMLAnchorDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) {
return InstanceFromProto(Prototypes.get(DocumentType.MARKER), new List(documents), options, id);
}
@@ -834,8 +765,8 @@ export namespace Docs {
return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { schemaHeaders: new List(schemaHeaders), ...options, _viewType: CollectionViewType.Schema });
}
- export function TreeDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) {
- return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Tree }, id);
+ export function TreeDocument(documents: Array<Doc>, options: DocumentOptions, id?: string, protoId?: string) {
+ return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Tree }, id, undefined, protoId);
}
export function StackingDocument(documents: Array<Doc>, options: DocumentOptions, id?: string, protoId?: string) {
@@ -885,8 +816,8 @@ export namespace Docs {
}
export function DockDocument(documents: Array<Doc>, config: string, options: DocumentOptions, id?: string) {
- const tabs = TreeDocument(documents, { title: "On-Screen Tabs", childDontRegisterViews: true, freezeChildren: "remove|add", treeViewExpandedViewLock: true, treeViewExpandedView: "data", _fitWidth: true, system: true });
- const all = TreeDocument([], { title: "Off-Screen Tabs", childDontRegisterViews: true, freezeChildren: "add", treeViewExpandedViewLock: true, treeViewExpandedView: "data", system: true });
+ const tabs = TreeDocument(documents, { title: "On-Screen Tabs", childDontRegisterViews: true, freezeChildren: "remove|add", treeViewExpandedViewLock: true, treeViewExpandedView: "data", _fitWidth: true, system: true, isFolder: true });
+ const all = TreeDocument([], { title: "Off-Screen Tabs", childDontRegisterViews: true, freezeChildren: "add", treeViewExpandedViewLock: true, treeViewExpandedView: "data", system: true, isFolder: true });
return InstanceFromProto(Prototypes.get(DocumentType.COL), new List([tabs, all]), { freezeChildren: "remove|add", treeViewExpandedViewLock: true, treeViewExpandedView: "data", ...options, _viewType: CollectionViewType.Docking, dockingConfig: config }, id);
}
@@ -1109,8 +1040,8 @@ export namespace DocUtils {
title: ComputedField.MakeFunction("generateLinkTitle(self)") as any,
"anchor1-useLinkSmallAnchor": source.doc.useLinkSmallAnchor ? true : undefined,
"anchor2-useLinkSmallAnchor": target.doc.useLinkSmallAnchor ? true : undefined,
- "acl-Public": SharingPermissions.Add,
- "_acl-Public": SharingPermissions.Add,
+ "acl-Public": SharingPermissions.Augment,
+ "_acl-Public": SharingPermissions.Augment,
layout_linkView: Cast(Cast(Doc.UserDoc()["template-button-link"], Doc, null).dragFactory, Doc, null),
linkDisplay: true, hidden: true,
linkRelationship,
@@ -1428,4 +1359,7 @@ Scripting.addGlobal(function generateLinkTitle(self: Doc) {
const anchor2title = self.anchor2 && self.anchor2 !== self ? Cast(self.anchor2, Doc, null).title : "<?>";
const relation = self.linkRelationship || "to";
return `${anchor1title} (${relation}) ${anchor2title}`;
+});
+Scripting.addGlobal(function openTabAlias(tab: Doc) {
+ CollectionDockingView.AddSplit(Doc.MakeAlias(tab), "right");
}); \ No newline at end of file
diff --git a/src/client/documents/Gitlike.ts b/src/client/documents/Gitlike.ts
index fddf317bc..575c984f5 100644
--- a/src/client/documents/Gitlike.ts
+++ b/src/client/documents/Gitlike.ts
@@ -1,7 +1,8 @@
-import { Doc, DocListCast, DocListCastAsync } from "../../fields/Doc";
+import { Doc, DocListCast, DocListCastAsync, Field } from "../../fields/Doc";
import { List } from "../../fields/List";
-import { ObjectField } from "../../fields/ObjectField";
import { Cast, DateCast } from "../../fields/Types";
+import { DateField } from "../../fields/DateField";
+import { Id } from "../../fields/FieldSymbols";
// synchs matching documents on the two branches that are being merged/pulled
// currently this just synchs the main 'fieldKey' component of the data since
@@ -10,11 +11,22 @@ function GitlikeSynchDocs(bd: Doc, md: Doc) {
const fieldKey = Doc.LayoutFieldKey(md);
const bdate = DateCast(bd[`${fieldKey}-lastModified`])?.date;
const mdate = DateCast(md[`${fieldKey}-lastModified`])?.date;
- if (bdate === mdate || bdate > mdate) return;
const bdproto = bd && Doc.GetProto(bd);
+ if (bdate !== mdate && bdate <= mdate) {
+ if (bdproto && md) {
+ bdproto[fieldKey] = Field.Copy(md[fieldKey]);
+ bdproto[`${fieldKey}-lastModified`] = new DateField();
+ }
+ }
+ const bldate = DateCast(bd._lastModified)?.date;
+ const mldate = DateCast(md._lastModified)?.date;
+ if (bldate === mldate || bldate > mldate) return;
if (bdproto && md) {
- bdproto[fieldKey] = ObjectField.MakeCopy(md[fieldKey] as ObjectField);
- bdproto[`${fieldKey}-lastModified`] = ObjectField.MakeCopy(md[`${fieldKey}-lastModified`] as ObjectField);
+ bd.x = Field.Copy(md.x);
+ bd.y = Field.Copy(md.y);
+ bd.width = Field.Copy(md.width);
+ bd.height = Field.Copy(md.height);
+ bdproto._lastModified = new DateField();
}
}
@@ -36,8 +48,9 @@ async function GitlikePullFromMaster(branch: Doc, suffix = "") {
const bd = branchMainDocs?.find(bd => (Cast(bd.branchOf, Doc, null) || bd) === md);
bd && GitlikeSynchDocs(bd, md);
});
+ const cloneMap = new Map<string, Doc>(); cloneMap.set(masterMain[Id], branch);
// make branch clones of them, then add them to the branch
- const newlyBranchedDocs = await Promise.all(newDocsFromMaster?.map(async md => (await Doc.MakeClone(md, false, true)).clone) || []);
+ const newlyBranchedDocs = await Promise.all(newDocsFromMaster?.map(async md => (await Doc.MakeClone(md, false, true, cloneMap)).clone) || []);
newlyBranchedDocs.forEach(nd => {
Doc.AddDocToList(branch, Doc.LayoutFieldKey(branch) + suffix, nd);
nd.context = branch;
@@ -56,22 +69,26 @@ async function GitlikeMergeWithMaster(master: Doc, suffix = "") {
branches?.map(async branch => {
const branchChildren = await DocListCastAsync(branch[Doc.LayoutFieldKey(branch) + suffix]);
branchChildren && await Promise.all(branchChildren.map(async bd => {
+ const cloneMap = new Map<string, Doc>(); cloneMap.set(master[Id], branch);
// see if the branch's child exists on master.
- const masterChild = Cast(bd.branchOf, Doc, null) || (await Doc.MakeClone(bd, false, true)).clone;
+ const masterChild = Cast(bd.branchOf, Doc, null) || (await Doc.MakeClone(bd, false, true, cloneMap)).clone;
// if the branch's child didn't exist on master, we make a branch clone of the child to add to master.
// however, since master is supposed to have the "main" clone, and branches, the "branch" clones, we have to reverse the fields
// on the branch child and master clone.
if (masterChild.branchOf) {
const branchDocProto = Doc.GetProto(bd);
const masterChildProto = Doc.GetProto(masterChild);
- masterChildProto.branchOf = undefined; // the master child should not be a branch of the branch child, so unset 'branchOf'
+ const branchTitle = bd.title;
+ branchDocProto.title = masterChildProto.title;
+ masterChildProto.title = branchTitle;
+ masterChildProto.branchOf = masterChild.branchOf = undefined; // the master child should not be a branch of the branch child, so unset 'branchOf'
masterChildProto.branches = new List<Doc>([bd]); // the master child's branches needs to include the branch child
Doc.RemoveDocFromList(branchDocProto, "branches", masterChildProto); // the branch child should not have the master child in its branch list.
branchDocProto.branchOf = masterChild; // the branch child is now a branch of the master child
}
Doc.AddDocToList(master, Doc.LayoutFieldKey(master) + suffix, masterChild); // add the masterChild to master (if it's already there, this is a no-op)
masterChild.context = master;
- GitlikeSynchDocs(Doc.GetProto(masterChild), bd);
+ GitlikeSynchDocs(masterChild, bd);//Doc.GetProto(masterChild), bd);
}));
const masterChildren = await DocListCastAsync(master[Doc.LayoutFieldKey(master) + suffix]);
masterChildren?.forEach(mc => { // see if any master children
@@ -93,6 +110,7 @@ export async function BranchTask(target: Doc, action: "pull" | "merge") {
const func = action === "pull" ? GitlikePullFromMaster : GitlikeMergeWithMaster;
await func(target, "");
await DocListCast(target[Doc.LayoutFieldKey(target)]).forEach(async targetChild => func(targetChild, "-annotations"));
+ await DocListCast(target[Doc.LayoutFieldKey(target)]).forEach(async targetChild => func(targetChild, "-sidebar"));
}
export async function BranchCreate(target: Doc) {
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 62fab1b23..34990e121 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -1,14 +1,15 @@
-import { computed, observable, reaction, action } from "mobx";
+import { computed, observable, reaction } from "mobx";
import * as rp from 'request-promise';
-import { DataSym, Doc, DocListCast, DocListCastAsync, AclReadonly } from "../../fields/Doc";
+import { DataSym, Doc, DocListCast, DocListCastAsync } from "../../fields/Doc";
import { Id } from "../../fields/FieldSymbols";
+import { InkTool } from "../../fields/InkField";
import { List } from "../../fields/List";
import { PrefetchProxy } from "../../fields/Proxy";
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, DateCast } from "../../fields/Types";
+import { BoolCast, Cast, DateCast, NumCast, PromiseValue, StrCast } from "../../fields/Types";
import { nullAudio } from "../../fields/URLField";
import { SharingPermissions } from "../../fields/util";
import { Utils } from "../../Utils";
@@ -19,6 +20,7 @@ import { Networking } from "../Network";
import { CollectionDockingView } from "../views/collections/CollectionDockingView";
import { DimUnit } from "../views/collections/collectionMulticolumn/CollectionMulticolumnView";
import { CollectionView, CollectionViewType } from "../views/collections/CollectionView";
+import { Colors } from "../views/global/globalEnums";
import { MainView } from "../views/MainView";
import { FormattedTextBox } from "../views/nodes/formattedText/FormattedTextBox";
import { LabelBox } from "../views/nodes/LabelBox";
@@ -31,12 +33,10 @@ import { LinkManager } from "./LinkManager";
import { Scripting } from "./Scripting";
import { SearchUtil } from "./SearchUtil";
import { SelectionManager } from "./SelectionManager";
-import { UndoManager } from "./UndoManager";
-import { SnappingManager } from "./SnappingManager";
-import { InkTool } from "../../fields/InkField";
-import { computedFn } from "mobx-utils";
import { ColorScheme } from "./SettingsManager";
-import { Colors } from "../views/global/globalEnums";
+import { SharingManager } from "./SharingManager";
+import { SnappingManager } from "./SnappingManager";
+import { UndoManager } from "./UndoManager";
export let resolvedPorts: { server: number, socket: number };
@@ -458,7 +458,7 @@ export class CurrentUserUtils {
((doc.emptyButton as Doc).proto as Doc)["dragFactory-count"] = 0;
}
if (doc.emptyWebpage === undefined) {
- doc.emptyWebpage = Docs.Create.WebDocument("", { title: "webpage", _nativeWidth: 850, isTemplateDoc: true, _height: 512, _width: 400, useCors: true, system: true, cloneFieldFilter: new List<string>(["system"]) });
+ doc.emptyWebpage = Docs.Create.WebDocument("", { title: "webpage", _nativeWidth: 850, _height: 512, _width: 400, useCors: true, system: true, cloneFieldFilter: new List<string>(["system"]) });
}
if (doc.activeMobileMenu === undefined) {
this.setupActiveMobileMenu(doc);
@@ -534,7 +534,6 @@ export class CurrentUserUtils {
{ title: "Import", target: Cast(doc.myImportPanel, Doc, null), icon: "upload", click: 'selectMainMenu(self)' },
{ title: "Recently Closed", target: Cast(doc.myRecentlyClosedDocs, Doc, null), icon: "archive", click: 'selectMainMenu(self)' },
{ title: "Sharing", target: Cast(doc.mySharedDocs, Doc, null), icon: "users", click: 'selectMainMenu(self)', watchedDocuments: doc.mySharedDocs as Doc },
- // { title: "Filter", target: Cast(doc.currentFilter, Doc, null), icon: "filter", click: 'selectMainMenu(self)' },
{ title: "Pres. Trails", target: Cast(doc.myPresentations, Doc, null), icon: "pres-trail", click: 'selectMainMenu(self)' },
// { title: "Help", target: undefined as any, icon: "question-circle", click: 'selectMainMenu(self)' },
// { title: "Settings", target: undefined as any, icon: "cog", click: 'selectMainMenu(self)' },
@@ -748,7 +747,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, ignoreClick: true, _lockedPosition: true, _forceActive: true,
- system: true, _stayInCollection: true, _hideContextMenu: true, _chromeHidden: true,
+ system: true, _stayInCollection: true, _hideContextMenu: true, _chromeHidden: true, boxShadow: "0 0",
})) as any as Doc;
doc.myTools = toolsStack;
@@ -763,7 +762,7 @@ export class CurrentUserUtils {
title: "My Dashboards", _showTitle: "title", _height: 400, childHideLinkButton: true,
treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _gridGap: 5, _forceActive: true, childDropAction: "alias",
treeViewTruncateTitleWidth: 150, ignoreClick: true,
- _lockedPosition: true, boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same", system: true
+ _lockedPosition: true, boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same", treeViewType: "fileSystem", isFolder: true, system: true
}));
const newDashboard = ScriptField.MakeScript(`createNewDashboard(Doc.UserDoc())`);
(doc.myDashboards as any as Doc).contextMenuScripts = new List<ScriptField>([newDashboard!]);
@@ -865,7 +864,6 @@ export class CurrentUserUtils {
CurrentUserUtils.setupPresentations(doc);
CurrentUserUtils.setupFilesystem(doc);
CurrentUserUtils.setupRecentlyClosedDocs(doc);
- // CurrentUserUtils.setupFilterDocs(doc);
CurrentUserUtils.setupUserDoc(doc);
}
@@ -916,21 +914,22 @@ export class CurrentUserUtils {
let linkDocs = Docs.newAccount ? undefined : await DocServer.GetRefField(linkDatabaseId);
if (!linkDocs) {
linkDocs = new Doc(linkDatabaseId, true);
+ (linkDocs as Doc).title = "LINK DATABASE: " + Doc.CurrentUserEmail;
(linkDocs as Doc).author = Doc.CurrentUserEmail;
(linkDocs as Doc).data = new List<Doc>([]);
- (linkDocs as Doc)["acl-Public"] = SharingPermissions.Add;
+ (linkDocs as Doc)["acl-Public"] = SharingPermissions.Augment;
}
doc.myLinkDatabase = new PrefetchProxy(linkDocs);
}
if (doc.mySharedDocs === undefined) {
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, "acl-Public": SharingPermissions.Add, "_acl-Public": SharingPermissions.Add,
+ sharedDocs = Docs.Create.TreeDocument([], {
+ title: "My SharedDocs", childDropAction: "alias", system: true, contentPointerEvents: "all", childLimitHeight: 0, _yMargin: 50, _gridGap: 15,
+ _showTitle: "title", ignoreClick: true, _lockedPosition: true, "acl-Public": SharingPermissions.Augment, "_acl-Public": SharingPermissions.Augment,
_chromeHidden: true, boxShadow: "0 0",
}, sharingDocumentId + "outer", sharingDocumentId);
- (sharedDocs as Doc)["acl-Public"] = (sharedDocs as Doc)[DataSym]["acl-Public"] = SharingPermissions.Add;
+ (sharedDocs as Doc)["acl-Public"] = (sharedDocs as Doc)[DataSym]["acl-Public"] = SharingPermissions.Augment;
}
if (sharedDocs instanceof Doc) {
Doc.GetProto(sharedDocs).userColor = sharedDocs.userColor || "rgb(202, 202, 202)";
@@ -1030,7 +1029,7 @@ export class CurrentUserUtils {
doc.fontFamily = StrCast(doc.fontFamily, "Arial");
doc.fontColor = StrCast(doc.fontColor, "black");
doc.fontHighlight = StrCast(doc.fontHighlight, "");
- doc.defaultAclPrivate = BoolCast(doc.defaultAclPrivate, true);
+ doc.defaultAclPrivate = BoolCast(doc.defaultAclPrivate, false);
doc.activeCollectionBackground = StrCast(doc.activeCollectionBackground, "white");
doc.activeCollectionNestedBackground = Cast(doc.activeCollectionNestedBackground, "string", null);
doc.noviceMode = BoolCast(doc.noviceMode, true);
@@ -1207,14 +1206,37 @@ export class CurrentUserUtils {
};
const freeformDoc = CurrentUserUtils.GuestTarget || Docs.Create.FreeformDocument([], freeformOptions);
const dashboardDoc = Docs.Create.StandardCollectionDockingDocument([{ doc: freeformDoc, initialWidth: 600 }], { title: `Dashboard ${dashboardCount}` }, id, "row");
+ freeformDoc.context = dashboardDoc;
+
+ // switching the tabs from the datadoc to the regular doc
+ const dashboardTabs = dashboardDoc[DataSym].data;
+ dashboardDoc[DataSym].data = new List<Doc>();
+ dashboardDoc.data = dashboardTabs;
+
+ // collating all docs on the dashboard to make a data-all field
+ const allDocs = new List<Doc>();
+ const allDocs2 = new List<Doc>(); // Array.from, spread, splice all cause so stack or acl issues for some reason
+ DocListCast(dashboardTabs).forEach(doc => {
+ const tabDocs = DocListCast(doc.data);
+ allDocs.push(...tabDocs);
+ allDocs2.push(...tabDocs);
+ });
+ dashboardDoc[DataSym]["data-all"] = allDocs;
+ dashboardDoc["data-all"] = allDocs2;
+ DocListCast(dashboardDoc.data).forEach(doc => doc.dashboard = dashboardDoc);
+ DocListCast(dashboardDoc.data)[1].data = ComputedField.MakeFunction(`dynamicOffScreenDocs(self.dashboard)`) as any;
+
Doc.AddDocToList(myPresentations, "data", presentation);
userDoc.activePresentation = presentation;
const toggleTheme = ScriptField.MakeScript(`Doc.UserDoc().darkScheme = !Doc.UserDoc().darkScheme`);
const toggleComic = ScriptField.MakeScript(`toggleComicMode()`);
const snapshotDashboard = ScriptField.MakeScript(`snapshotDashboard()`);
const createDashboard = ScriptField.MakeScript(`createNewDashboard()`);
- dashboardDoc.contextMenuScripts = new List<ScriptField>([toggleTheme!, toggleComic!, snapshotDashboard!, createDashboard!]);
- dashboardDoc.contextMenuLabels = new List<string>(["Toggle Theme Colors", "Toggle Comic Mode", "Snapshot Dashboard", "Create Dashboard"]);
+ const shareDashboard = ScriptField.MakeScript(`shareDashboard(self)`);
+ const addToDashboards = ScriptField.MakeScript(`addToDashboards(self)`);
+ const removeDashboard = ScriptField.MakeScript('removeDashboard(self)');
+ dashboardDoc.contextMenuScripts = new List<ScriptField>([toggleTheme!, toggleComic!, snapshotDashboard!, createDashboard!, shareDashboard!, addToDashboards!, removeDashboard!]);
+ dashboardDoc.contextMenuLabels = new List<string>(["Toggle Theme Colors", "Toggle Comic Mode", "Snapshot Dashboard", "Create Dashboard", "Share Dashboard", "Add to Dashboards", "Remove Dashboard"]);
Doc.AddDocToList(dashboards, "data", dashboardDoc);
CurrentUserUtils.openDashboard(userDoc, dashboardDoc);
@@ -1265,4 +1287,54 @@ Scripting.addGlobal(function createNewPresentation() { return MainView.Instance.
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 importDocument() { return CurrentUserUtils.importDocument(); },
- "imports files from device directly into the import sidebar"); \ No newline at end of file
+ "imports files from device directly into the import sidebar");
+Scripting.addGlobal(function shareDashboard(dashboard: Doc) {
+ SharingManager.Instance.open(undefined, dashboard);
+},
+ "opens sharing dialog for Dashboard");
+Scripting.addGlobal(async function removeDashboard(dashboard: Doc) {
+ const dashboards = await DocListCastAsync(CurrentUserUtils.MyDashboards.data);
+ if (dashboards && dashboards.length > 1) {
+ if (dashboard === CurrentUserUtils.ActiveDashboard) CurrentUserUtils.openDashboard(Doc.UserDoc(), dashboards.find(doc => doc !== dashboard)!);
+ Doc.RemoveDocFromList(CurrentUserUtils.MyDashboards, "data", dashboard);
+ }
+},
+ "Remove Dashboard from Dashboards");
+Scripting.addGlobal(async function addToDashboards(dashboard: Doc) {
+ const dashboardAlias = Doc.MakeAlias(dashboard);
+
+ const allDocs = await DocListCastAsync(dashboard[DataSym]["data-all"]);
+
+ // moves the data-all field from the datadoc to the layoutdoc, necessary for off screen docs tab to function properly
+ dashboard["data-all"] = new List<Doc>(allDocs);
+ dashboardAlias["data-all"] = new List<Doc>((allDocs || []).map(doc => Doc.MakeAlias(doc)));
+
+ const dockingConfig = JSON.parse(StrCast(dashboardAlias.dockingConfig));
+ dockingConfig.content = [];
+ dashboardAlias.dockingConfig = JSON.stringify(dockingConfig);
+
+
+ dashboardAlias.data = new List<Doc>(DocListCast(dashboard.data).map(tabFolder => Doc.MakeAlias(tabFolder)));
+ DocListCast(dashboardAlias.data).forEach(doc => doc.dashboard = dashboardAlias);
+ DocListCast(dashboardAlias.data)[0].data = new List<Doc>();
+ DocListCast(dashboardAlias.data)[1].data = ComputedField.MakeFunction(`dynamicOffScreenDocs(self.dashboard)`) as any;
+ Doc.AddDocToList(CurrentUserUtils.MyDashboards, "data", dashboardAlias);
+ CurrentUserUtils.openDashboard(Doc.UserDoc(), dashboardAlias);
+},
+ "adds Dashboard to set of Dashboards");
+
+/**
+ * Dynamically computes which docs should be rendered in the off-screen tabs tree of a dashboard.
+ */
+Scripting.addGlobal(function dynamicOffScreenDocs(dashboard: Doc) {
+ if (dashboard[DataSym] instanceof Doc) {
+ const allDocs = DocListCast(dashboard["data-all"]);
+ const onScreenTab = DocListCast(dashboard.data)[0];
+ const onScreenDocs = DocListCast(onScreenTab.data);
+ return new List<Doc>(allDocs.reduce((result: Doc[], doc) => {
+ !onScreenDocs.includes(doc) && !onScreenDocs.includes(doc.aliasOf as Doc) && (result.push(doc));
+ return result;
+ }, []));
+ }
+ return [];
+});
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 5b092258a..cb0ee411c 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -144,9 +144,11 @@ export class DocumentManager {
originalTarget = originalTarget ?? targetDoc;
const getFirstDocView = LightboxView.LightboxDoc ? DocumentManager.Instance.getLightboxDocumentView : DocumentManager.Instance.getFirstDocumentView;
const docView = getFirstDocView(targetDoc, originatingDoc);
+ const wasHidden = targetDoc.hidden; //
+ if (wasHidden) runInAction(() => targetDoc.hidden = false); // if the target is hidden, un-hide it here.
const focusAndFinish = (didFocus: boolean) => {
if (originatingDoc?.isPushpin) {
- if (!didFocus || targetDoc.hidden) {
+ if (!didFocus && !wasHidden) { // don't toggle the hidden state if the doc was already un-hidden as part of this document traversal
targetDoc.hidden = !targetDoc.hidden;
}
} else {
@@ -161,13 +163,14 @@ export class DocumentManager {
const contextDocs = docContext ? await DocListCastAsync(docContext.data) : undefined;
const contextDoc = contextDocs?.find(doc => Doc.AreProtosEqual(doc, targetDoc) || Doc.AreProtosEqual(doc, annotatedDoc)) ? docContext : undefined;
const targetDocContext = contextDoc || annotatedDoc;
- const targetDocContextView = targetDocContext && getFirstDocView(targetDocContext);
+ const targetDocContextView = (targetDocContext && getFirstDocView(targetDocContext)) ||
+ (wasHidden && annoContainerView);// if we have an annotation container and the target was hidden, then try again because we just un-hid the document above
const focusView = !docView && targetDoc.type === DocumentType.MARKER && annoContainerView ? annoContainerView : docView;
- if (!docView && annoContainerView && !focusView) {
+ if (!docView && annoContainerView) {
annoContainerView.focus(targetDoc); // this allows something like a PDF view to remove its doc filters to expose the target so that it can be found in the retry code below
}
if (focusView) {
- focusView && Doc.linkFollowHighlight(focusView.rootDoc);
+ Doc.linkFollowHighlight(focusView.rootDoc);
focusView.focus(targetDoc, {
originalTarget, willZoom, afterFocus: (didFocus: boolean) =>
new Promise<ViewAdjustment>(res => {
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index c4842e88a..5e16de617 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -15,6 +15,15 @@ import { SnappingManager } from "./SnappingManager";
import { UndoManager } from "./UndoManager";
export type dropActionType = "alias" | "copy" | "move" | "same" | "proto" | "none" | undefined; // undefined = move, "same" = move but don't call removeDropProperties
+
+/**
+ * Initialize drag
+ * @param _reference: The HTMLElement that is being dragged
+ * @param docFunc: The Dash document being moved
+ * @param moveFunc: The function called when the document is moved
+ * @param dropAction: What to do with the document when it is dropped
+ * @param dragStarted: Method to call when the drag is started
+ */
export function SetupDrag(
_reference: React.RefObject<HTMLElement>,
docFunc: () => Doc | Promise<Doc> | undefined,
diff --git a/src/client/util/GroupMemberView.tsx b/src/client/util/GroupMemberView.tsx
index 927200ed3..b7f89794d 100644
--- a/src/client/util/GroupMemberView.tsx
+++ b/src/client/util/GroupMemberView.tsx
@@ -50,7 +50,6 @@ export class GroupMemberView extends React.Component<GroupMemberViewProps> {
onChange={selectedOption => GroupManager.Instance.addMemberToGroup(this.props.group, (selectedOption as UserOptions).value)}
placeholder={"Add members"}
value={null}
- closeMenuOnSelect={true}
styles={{
dropdownIndicator: (base, state) => ({
...base,
diff --git a/src/client/util/HypothesisUtils.ts b/src/client/util/HypothesisUtils.ts
index 8ddfce772..635673025 100644
--- a/src/client/util/HypothesisUtils.ts
+++ b/src/client/util/HypothesisUtils.ts
@@ -126,7 +126,7 @@ export namespace Hypothesis {
});
const annotationId = StrCast(linkDoc.annotationId);
- const linkUrl = Utils.prepend("/doc/" + sourceDoc[Id]);
+ const linkUrl = Doc.globalServerPath(sourceDoc);
const interval = setInterval(() => {// keep trying to edit until annotations have loaded and editing is successful
!success && document.dispatchEvent(new CustomEvent<{ targetUrl: string, id: string }>("deleteLink", {
detail: { targetUrl: linkUrl, id: annotationId },
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index 08f4ac9b7..933b98a8c 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -1,4 +1,4 @@
-import { observable, observe, action } from "mobx";
+import { observable, observe, action, reaction, computed } from "mobx";
import { computedFn } from "mobx-utils";
import { DirectLinksSym, Doc, DocListCast, Field, Opt } from "../../fields/Doc";
import { List } from "../../fields/List";
@@ -26,36 +26,41 @@ type CreateViewFunc = (doc: Doc, followLinkLocation: string, finished?: () => vo
export class LinkManager {
@observable static _instance: LinkManager;
- @observable static userDocs: Doc[] = [];
+ @observable static userLinkDBs: Doc[] = [];
public static currentLink: Opt<Doc>;
public static get Instance() { return LinkManager._instance; }
+ public static addLinkDB = (linkDb: any) => LinkManager.userLinkDBs.push(linkDb);
+ static links: Doc[] = [];
constructor() {
LinkManager._instance = this;
setTimeout(() => {
- LinkManager.userDocs = [Doc.LinkDBDoc().data as Doc, ...SharingManager.Instance.users.map(user => user.linkDatabase)];
- const addLinkToDoc = action((link: Doc): any => {
+ LinkManager.userLinkDBs = [];
+ const addLinkToDoc = (link: Doc) => {
const a1 = link?.anchor1;
const a2 = link?.anchor2;
- if (a1 instanceof Promise || a2 instanceof Promise) return PromiseValue(a1).then(a1 => PromiseValue(a2).then(a2 => addLinkToDoc(link)));
- if (a1 instanceof Doc && a2 instanceof Doc && ((a1.author !== undefined && a2.author !== undefined) || link.author === Doc.CurrentUserEmail)) {
- Doc.GetProto(a1)[DirectLinksSym].add(link);
- Doc.GetProto(a2)[DirectLinksSym].add(link);
- Doc.GetProto(link)[DirectLinksSym].add(link);
- }
- });
- const remLinkFromDoc = action((link: Doc): any => {
+ Promise.all([a1, a2]).then(action(() => {
+ if (a1 instanceof Doc && a2 instanceof Doc && ((a1.author !== undefined && a2.author !== undefined) || link.author === Doc.CurrentUserEmail)) {
+ Doc.GetProto(a1)[DirectLinksSym].add(link);
+ Doc.GetProto(a2)[DirectLinksSym].add(link);
+ Doc.GetProto(link)[DirectLinksSym].add(link);
+ }
+ }));
+ }
+ const remLinkFromDoc = (link: Doc) => {
const a1 = link?.anchor1;
const a2 = link?.anchor2;
- if (a1 instanceof Promise || a2 instanceof Promise) return PromiseValue(a1).then(a1 => PromiseValue(a2).then(a2 => remLinkFromDoc(link)));
- if (a1 instanceof Doc && a2 instanceof Doc && ((a1.author !== undefined && a2.author !== undefined) || link.author === Doc.CurrentUserEmail)) {
- Doc.GetProto(a1)[DirectLinksSym].delete(link);
- Doc.GetProto(a2)[DirectLinksSym].delete(link);
- Doc.GetProto(link)[DirectLinksSym].delete(link);
- }
- });
- const watchUserLinks = (userLinks: List<Doc>) => {
+ Promise.all([a1, a2]).then(action(() => {
+ if (a1 instanceof Doc && a2 instanceof Doc && ((a1.author !== undefined && a2.author !== undefined) || link.author === Doc.CurrentUserEmail)) {
+ Doc.GetProto(a1)[DirectLinksSym].delete(link);
+ Doc.GetProto(a2)[DirectLinksSym].delete(link);
+ Doc.GetProto(link)[DirectLinksSym].delete(link);
+ }
+ }));
+ }
+ const watchUserLinkDB = (userLinkDBDoc: Doc) => {
+ LinkManager.links.push(...DocListCast(userLinkDBDoc.data));
const toRealField = (field: Field) => field instanceof ProxyField ? field.value() : field; // see List.ts. data structure is not a simple list of Docs, but a list of ProxyField/Fields
- observe(userLinks, change => {
+ observe(userLinkDBDoc.data as Doc, change => { // observe pushes/splices on a user link DB 'data' field (should only happen for local changes)
switch (change.type as any) {
case "splice":
(change as any).added.forEach((link: any) => addLinkToDoc(toRealField(link)));
@@ -64,13 +69,29 @@ export class LinkManager {
case "update": //let oldValue = change.oldValue;
}
}, true);
+ observe(userLinkDBDoc, "data", // obsever when a new array of links is assigned as the link DB 'data' field (should happen whenever a remote user adds/removes a link)
+ change => {
+ switch (change.type as any) {
+ case "update":
+ Promise.all([...(change.oldValue as any as Doc[] || []), ...(change.newValue as any as Doc[] || [])]).then(doclist => {
+ const oldDocs = doclist.slice(0, (change.oldValue as any as Doc[] || []).length);
+ const newDocs = doclist.slice((change.oldValue as any as Doc[] || []).length, doclist.length);
+
+ const added = newDocs?.filter(link => !(oldDocs || []).includes(link));
+ const removed = oldDocs?.filter(link => !(newDocs || []).includes(link));
+ added?.forEach((link: any) => addLinkToDoc(toRealField(link)));
+ removed?.forEach((link: any) => remLinkFromDoc(toRealField(link)));
+ });
+ }
+ }, true);
};
- observe(LinkManager.userDocs, change => {
+ observe(LinkManager.userLinkDBs, change => {
switch (change.type as any) {
- case "splice": (change as any).added.forEach(watchUserLinks); break;
+ case "splice": (change as any).added.forEach(watchUserLinkDB); break;
case "update": //let oldValue = change.oldValue;
}
}, true);
+ LinkManager.addLinkDB(Doc.LinkDBDoc());
});
}
@@ -135,7 +156,8 @@ export class LinkManager {
const where = LightboxView.LightboxDoc ? "lightbox" : StrCast(sourceDoc.followLinkLocation, followLoc);
docViewProps.addDocTab(doc, where);
setTimeout(() => {
- const targDocView = DocumentManager.Instance.getFirstDocumentView(doc);
+ const getFirstDocView = LightboxView.LightboxDoc ? DocumentManager.Instance.getLightboxDocumentView : DocumentManager.Instance.getFirstDocumentView;
+ const targDocView = getFirstDocView(doc); // get first document view available within the lightbox if that's open, or anywhere otherwise.
if (targDocView) {
targDocView.props.focus(doc, {
willZoom: BoolCast(sourceDoc.followLinkZoom, false),
diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx
index 3987497b8..bd91db779 100644
--- a/src/client/util/SettingsManager.tsx
+++ b/src/client/util/SettingsManager.tsx
@@ -268,7 +268,8 @@ export class SettingsManager extends React.Component<{}> {
<div className="tab-column-content">
<button onClick={() => GroupManager.Instance?.open()}>Manage groups</button>
<div className="default-acl">
- <input className="acl-check" type="checkbox" checked={BoolCast(Doc.UserDoc()?.defaultAclPrivate)} onChange={action(() => Doc.UserDoc().defaultAclPrivate = !Doc.UserDoc().defaultAclPrivate)} />
+ <input className="acl-check" type="checkbox" checked={BoolCast(Doc.UserDoc()?.defaultAclPrivate)}
+ onChange={action(() => Doc.UserDoc().defaultAclPrivate = !Doc.UserDoc().defaultAclPrivate)} />
<div className="acl-text">Default access private</div>
</div>
</div>
diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx
index dc5f488b2..6c4556250 100644
--- a/src/client/util/SharingManager.tsx
+++ b/src/client/util/SharingManager.tsx
@@ -5,7 +5,7 @@ import { observer } from "mobx-react";
import * as React from "react";
import Select from "react-select";
import * as RequestPromise from "request-promise";
-import { AclAddonly, AclAdmin, AclEdit, AclPrivate, AclReadonly, AclSym, DataSym, Doc, DocListCast, DocListCastAsync, Opt } from "../../fields/Doc";
+import { AclAugment, AclAdmin, AclEdit, AclPrivate, AclReadonly, AclSym, AclUnset, DataSym, Doc, DocListCast, DocListCastAsync, Opt, AclSelfEdit } from "../../fields/Doc";
import { List } from "../../fields/List";
import { Cast, NumCast, StrCast } from "../../fields/Types";
import { distributeAcls, GetEffectiveAcl, normalizeEmail, SharingPermissions, TraceMobx } from "../../fields/util";
@@ -17,11 +17,13 @@ import { MainViewModal } from "../views/MainViewModal";
import { DocumentView } from "../views/nodes/DocumentView";
import { TaskCompletionBox } from "../views/nodes/TaskCompletedBox";
import { SearchBox } from "../views/search/SearchBox";
+import { CurrentUserUtils } from "./CurrentUserUtils";
import { DocumentManager } from "./DocumentManager";
import { GroupManager, UserOptions } from "./GroupManager";
import { GroupMemberView } from "./GroupMemberView";
import { SelectionManager } from "./SelectionManager";
import "./SharingManager.scss";
+import { LinkManager } from "./LinkManager";
export interface User {
email: string;
@@ -38,7 +40,7 @@ interface GroupedOptions {
}
// const SharingKey = "sharingPermissions";
-// const PublicKey = "publicLinkPermissions";
+// const PublicKey = "all";
// const DefaultColor = "black";
// used to differentiate between individuals and groups when sharing
@@ -84,13 +86,14 @@ export class SharingManager extends React.Component<{}> {
private AclMap = new Map<symbol, string>([
[AclPrivate, SharingPermissions.None],
[AclReadonly, SharingPermissions.View],
- [AclAddonly, SharingPermissions.Add],
+ [AclAugment, SharingPermissions.Augment],
+ [AclSelfEdit, SharingPermissions.SelfEdit],
[AclEdit, SharingPermissions.Edit],
[AclAdmin, SharingPermissions.Admin]
]);
// private get linkVisible() {
- // return this.sharingDoc ? this.sharingDoc[PublicKey] !== SharingPermissions.None : false;
+ // return this.targetDoc ? this.targetDoc["acl-" + PublicKey] !== SharingPermissions.None : false;
// }
public open = (target?: DocumentView, target_doc?: Doc) => {
@@ -100,7 +103,7 @@ export class SharingManager extends React.Component<{}> {
this.targetDoc = target_doc || target?.props.Document;
DictationOverlay.Instance.hasActiveModal = true;
this.isOpen = this.targetDoc !== undefined;
- this.permissions = SharingPermissions.Add;
+ this.permissions = SharingPermissions.Augment;
});
}
@@ -152,10 +155,11 @@ export class SharingManager extends React.Component<{}> {
}
});
return Promise.all(evaluating).then(() => {
- runInAction(() => {
+ runInAction(async () => {
for (const sharer of sharingDocs) {
if (!this.users.find(user => user.user.email === sharer.user.email)) {
this.users.push(sharer);
+ LinkManager.addLinkDB(sharer.linkDatabase);
}
}
});
@@ -172,10 +176,11 @@ export class SharingManager extends React.Component<{}> {
const target = targetDoc || this.targetDoc!;
const acl = `acl-${normalizeEmail(user.email)}`;
const myAcl = `acl-${Doc.CurrentUserEmailNormalized}`;
+ const isDashboard = DocListCast(CurrentUserUtils.MyDashboards.data).indexOf(target) !== -1;
const docs = SelectionManager.Views().length < 2 ? [target] : SelectionManager.Views().map(docView => docView.props.Document);
return !docs.map(doc => {
- doc.author === Doc.CurrentUserEmail && !doc[myAcl] && distributeAcls(myAcl, SharingPermissions.Admin, doc);
+ doc.author === Doc.CurrentUserEmail && !doc[myAcl] && distributeAcls(myAcl, SharingPermissions.Admin, doc, undefined, undefined, isDashboard);
if (permission === SharingPermissions.None) {
if (doc[acl] && doc[acl] !== SharingPermissions.None) doc.numUsersShared = NumCast(doc.numUsersShared, 1) - 1;
@@ -184,8 +189,9 @@ export class SharingManager extends React.Component<{}> {
if (!doc[acl] || doc[acl] === SharingPermissions.None) doc.numUsersShared = NumCast(doc.numUsersShared, 0) + 1;
}
- distributeAcls(acl, permission as SharingPermissions, doc);
+ distributeAcls(acl, permission as SharingPermissions, doc, undefined, undefined, isDashboard);
+ this.setDashboardBackground(doc, permission as SharingPermissions);
if (permission !== SharingPermissions.None) return Doc.AddDocToList(sharingDoc, storage, doc);
else return GetEffectiveAcl(doc, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, storage, (doc.aliasOf as Doc || doc));
}).some(success => !success);
@@ -201,12 +207,13 @@ export class SharingManager extends React.Component<{}> {
const target = targetDoc || this.targetDoc!;
const key = normalizeEmail(StrCast(group.title));
const acl = `acl-${key}`;
+ const isDashboard = DocListCast(CurrentUserUtils.MyDashboards.data).indexOf(target) !== -1;
const docs = SelectionManager.Views().length < 2 ? [target] : SelectionManager.Views().map(docView => docView.props.Document);
// ! ensures it returns true if document has been shared successfully, false otherwise
return !docs.map(doc => {
- doc.author === Doc.CurrentUserEmail && !doc[`acl-${Doc.CurrentUserEmailNormalized}`] && distributeAcls(`acl-${Doc.CurrentUserEmailNormalized}`, SharingPermissions.Admin, doc);
+ doc.author === Doc.CurrentUserEmail && !doc[`acl-${Doc.CurrentUserEmailNormalized}`] && distributeAcls(`acl-${Doc.CurrentUserEmailNormalized}`, SharingPermissions.Admin, doc, undefined, undefined, isDashboard);
if (permission === SharingPermissions.None) {
if (doc[acl] && doc[acl] !== SharingPermissions.None) doc.numGroupsShared = NumCast(doc.numGroupsShared, 1) - 1;
@@ -215,7 +222,8 @@ export class SharingManager extends React.Component<{}> {
if (!doc[acl] || doc[acl] === SharingPermissions.None) doc.numGroupsShared = NumCast(doc.numGroupsShared, 0) + 1;
}
- distributeAcls(acl, permission as SharingPermissions, doc);
+ distributeAcls(acl, permission as SharingPermissions, doc, undefined, undefined, isDashboard);
+ this.setDashboardBackground(doc, permission as SharingPermissions);
if (group instanceof Doc) {
const members: string[] = JSON.parse(StrCast(group.members));
@@ -264,13 +272,34 @@ export class SharingManager extends React.Component<{}> {
});
}
else {
+ const dashboards = DocListCast(CurrentUserUtils.MyDashboards.data);
docs.forEach(doc => {
- if (GetEffectiveAcl(doc) === AclAdmin) distributeAcls(`acl-${shareWith}`, permission, doc);
+ const isDashboard = dashboards.indexOf(doc) !== -1;
+ if (GetEffectiveAcl(doc) === AclAdmin) distributeAcls(`acl-${shareWith}`, permission, doc, undefined, undefined, isDashboard);
});
}
}
/**
+ * Sets the background of the Dashboard if it has been shared as a visual indicator
+ */
+ setDashboardBackground = async (doc: Doc, permission: SharingPermissions) => {
+ if (Doc.IndexOf(doc, DocListCast(CurrentUserUtils.MyDashboards.data)) !== -1) {
+ if (permission !== SharingPermissions.None) {
+ doc.isShared = true;
+ doc.backgroundColor = "green";
+ }
+ else {
+ const acls = doc[DataSym][AclSym];
+ if (Object.keys(acls).every(key => key === `acl-${Doc.CurrentUserEmailNormalized}` ? true : [AclUnset, AclPrivate].includes(acls[key]))) {
+ doc.isShared = undefined;
+ doc.backgroundColor = undefined;
+ }
+ }
+ }
+ }
+
+ /**
* Removes the documents shared with a user through a group when the user is removed from the group.
* @param group
* @param emailId
@@ -294,10 +323,11 @@ export class SharingManager extends React.Component<{}> {
*/
removeGroup = (group: Doc) => {
if (group.docsShared) {
+ const dashboards = DocListCast(CurrentUserUtils.MyDashboards.data);
DocListCast(group.docsShared).forEach(doc => {
const acl = `acl-${StrCast(group.title)}`;
-
- distributeAcls(acl, SharingPermissions.None, doc);
+ const isDashboard = dashboards.indexOf(doc) !== -1;
+ distributeAcls(acl, SharingPermissions.None, doc, undefined, undefined, isDashboard);
const members: string[] = JSON.parse(StrCast(group.members));
const users: ValidatedUser[] = this.users.filter(({ user: { email } }) => members.includes(email));
@@ -310,11 +340,11 @@ export class SharingManager extends React.Component<{}> {
// private setExternalSharing = (permission: string) => {
- // const sharingDoc = this.sharingDoc;
- // if (!sharingDoc) {
+ // const targetDoc = this.targetDoc;
+ // if (!targetDoc) {
// return;
// }
- // sharingDoc[PublicKey] = permission;
+ // targetDoc["acl-" + PublicKey] = permission;
// }
// private get sharingUrl() {
@@ -339,10 +369,10 @@ export class SharingManager extends React.Component<{}> {
const dropdownValues: string[] = Object.values(SharingPermissions);
if (!uniform) dropdownValues.unshift("-multiple-");
if (override) dropdownValues.unshift("None");
- return dropdownValues.filter(permission => permission !== SharingPermissions.View).map(permission =>
+ return dropdownValues.filter(permission => !Doc.UserDoc().noviceMode || ![SharingPermissions.View, SharingPermissions.SelfEdit].includes(permission as any)).map(permission =>
(
<option key={permission} value={permission}>
- {permission === SharingPermissions.Add ? "Can Augment" : permission}
+ {permission}
</option>
)
);
@@ -423,16 +453,16 @@ export class SharingManager extends React.Component<{}> {
}
}
- distributeOverCollection = (targetDoc?: Doc) => {
- const target = targetDoc || this.targetDoc!;
+ // distributeOverCollection = (targetDoc?: Doc) => {
+ // const target = targetDoc || this.targetDoc!;
- const docs = SelectionManager.Views().length < 2 ? [target] : SelectionManager.Views().map(docView => docView.props.Document);
- docs.forEach(doc => {
- for (const [key, value] of Object.entries(doc[AclSym])) {
- distributeAcls(key, this.AclMap.get(value)! as SharingPermissions, target);
- }
- });
- }
+ // const docs = SelectionManager.Views().length < 2 ? [target] : SelectionManager.Views().map(docView => docView.props.Document);
+ // docs.forEach(doc => {
+ // for (const [key, value] of Object.entries(doc[AclSym])) {
+ // distributeAcls(key, this.AclMap.get(value)! as SharingPermissions, target);
+ // }
+ // });
+ // }
/**
* Sorting algorithm to sort users.
@@ -519,7 +549,7 @@ export class SharingManager extends React.Component<{}> {
</select>
) : (
<div className={"permissions-dropdown"}>
- {permissions === SharingPermissions.Add ? "Can Augment" : permissions}
+ {permissions}
</div>
)}
</div>
@@ -565,7 +595,7 @@ export class SharingManager extends React.Component<{}> {
// the list of groups shared with
const groupListMap: (Doc | { title: string })[] = groups.filter(({ title }) => docs.length > 1 ? commonKeys.includes(`acl-${normalizeEmail(StrCast(title))}`) : true);
- groupListMap.unshift({ title: "Public" });//, { title: "Override" });
+ groupListMap.unshift({ title: "Public" });//, { title: "ALL" });
const groupListContents = groupListMap.map(group => {
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]);
@@ -614,6 +644,11 @@ export class SharingManager extends React.Component<{}> {
<div className={"close-button"} onClick={this.close}>
<FontAwesomeIcon icon={"times"} color={"black"} size={"lg"} />
</div>
+ {/* {this.linkVisible ?
+ <div>
+ {this.sharingUrl}
+ </div> :
+ (null)} */}
{<div className="share-container">
<div className="share-setup">
<Select
diff --git a/src/client/views/.DS_Store b/src/client/views/.DS_Store
index 33e624ef4..e4ac87aad 100644
--- a/src/client/views/.DS_Store
+++ b/src/client/views/.DS_Store
Binary files differ
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index 0b70ce68d..14d32ef12 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -1,4 +1,4 @@
-import { Doc, Opt, DataSym, AclReadonly, AclAddonly, AclPrivate, AclEdit, AclSym, DocListCastAsync, DocListCast, AclAdmin } from '../../fields/Doc';
+import { Doc, Opt, DataSym, AclReadonly, AclAugment, AclPrivate, AclEdit, AclSym, DocListCast, AclAdmin, AclSelfEdit } from '../../fields/Doc';
import { Touchable } from './Touchable';
import { computed, action, observable } from 'mobx';
import { Cast, BoolCast, ScriptCast } from '../../fields/Types';
@@ -7,7 +7,7 @@ import { InteractionUtils } from '../util/InteractionUtils';
import { List } from '../../fields/List';
import { DateField } from '../../fields/DateField';
import { ScriptField } from '../../fields/ScriptField';
-import { GetEffectiveAcl, SharingPermissions, distributeAcls, denormalizeEmail } from '../../fields/util';
+import { GetEffectiveAcl, SharingPermissions, distributeAcls, denormalizeEmail, inheritParentAcls } from '../../fields/util';
import { CurrentUserUtils } from '../util/CurrentUserUtils';
import { DocUtils } from '../documents/Documents';
import { returnFalse } from '../../Utils';
@@ -107,13 +107,6 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T
// key where data is stored
@computed get fieldKey() { return this.props.fieldKey; }
- private AclMap = new Map<symbol, string>([
- [AclPrivate, SharingPermissions.None],
- [AclReadonly, SharingPermissions.View],
- [AclAddonly, SharingPermissions.Add],
- [AclEdit, SharingPermissions.Edit],
- [AclAdmin, SharingPermissions.Admin]
- ]);
lookupField = (field: string) => ScriptCast((this.layoutDoc as any).lookupField)?.script.run({ self: this.layoutDoc, data: this.rootDoc, field: field }).result;
@@ -138,7 +131,7 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T
removeDocument(doc: Doc | Doc[], annotationKey?: string, leavePushpin?: boolean): boolean {
const effectiveAcl = GetEffectiveAcl(this.dataDoc);
const indocs = doc instanceof Doc ? [doc] : doc;
- const docs = indocs.filter(doc => effectiveAcl === AclEdit || effectiveAcl === AclAdmin || GetEffectiveAcl(doc) === AclAdmin);
+ const docs = indocs.filter(doc => [AclEdit, AclAdmin].includes(effectiveAcl) || GetEffectiveAcl(doc) === AclAdmin);
if (docs.length) {
setTimeout(() => docs.map(doc => { // this allows 'addDocument' to see the annotationOn field in order to create a pushin
Doc.SetInPlace(doc, "isPushpin", undefined, true);
@@ -201,15 +194,14 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T
if (this.props.Document[AclSym] && Object.keys(this.props.Document[AclSym]).length) {
added.forEach(d => {
for (const [key, value] of Object.entries(this.props.Document[AclSym])) {
- if (d.author === denormalizeEmail(key.substring(4)) && !d.aliasOf) distributeAcls(key, SharingPermissions.Admin, d, true);
- //else if (this.props.Document[key] === SharingPermissions.Admin) distributeAcls(key, SharingPermissions.Add, d, true);
- // else distributeAcls(key, this.AclMap.get(value) as SharingPermissions, d, true);
+ if (d.author === denormalizeEmail(key.substring(4)) && !d.aliasOf) distributeAcls(key, SharingPermissions.Admin, d);
}
});
}
- if (effectiveAcl === AclAddonly) {
+ if (effectiveAcl === AclAugment) {
added.map(doc => {
+ if ([AclAdmin, AclEdit].includes(GetEffectiveAcl(doc))) inheritParentAcls(CurrentUserUtils.ActiveDashboard, doc);
doc.context = this.props.Document;
if (annotationKey ?? this._annotationKey) Doc.GetProto(doc).annotationOn = this.props.Document;
this.props.layerProvider?.(doc, true);
@@ -223,9 +215,11 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T
doc._stayInCollection = undefined;
doc.context = this.props.Document;
if (annotationKey ?? this._annotationKey) Doc.GetProto(doc).annotationOn = this.props.Document;
+
+ inheritParentAcls(CurrentUserUtils.ActiveDashboard, doc);
});
const annoDocs = targetDataDoc[annotationKey ?? this.annotationKey] as List<Doc>;
- if (annoDocs) annoDocs.push(...added);
+ if (annoDocs instanceof List) annoDocs.push(...added);
else targetDataDoc[annotationKey ?? this.annotationKey] = new List<Doc>(added);
targetDataDoc[(annotationKey ?? this.annotationKey) + "-lastModified"] = new DateField(new Date(Date.now()));
}
diff --git a/src/client/views/DocumentButtonBar.scss b/src/client/views/DocumentButtonBar.scss
index 157f3a4f2..a112f4745 100644
--- a/src/client/views/DocumentButtonBar.scss
+++ b/src/client/views/DocumentButtonBar.scss
@@ -46,7 +46,6 @@ $linkGap : 3px;
.documentButtonBar {
display: flex;
flex-direction: row;
- gap: 3px;
}
.documentButtonBar-button {
@@ -59,6 +58,7 @@ $linkGap : 3px;
align-items: center;
}
+// depracated (now use .documentButtonBar-icon) for standard buttons
.documentButtonBar-linker {
height: 20px;
width: 20px;
@@ -74,6 +74,26 @@ $linkGap : 3px;
}
}
+.documentButtonBar-icon {
+ height: 80%;
+ width: 80%;
+ font-size: 100%;
+ text-align: center;
+ border-radius: 50%;
+ pointer-events: auto;
+ background-color: $dark-gray;
+ border: none;
+ transition: 0.2s ease all;
+ display: flex;
+ align-content: center;
+ justify-content: center;
+ align-items: center;
+
+ &:hover {
+ background-color: $black;
+ }
+}
+
.documentButtonBar-linker:hover {
cursor: pointer;
transform: scale(1.05);
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index 1e5380971..5f09a322c 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -110,7 +110,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
const animation = this.isAnimatingPulse ? "shadow-pulse 1s linear infinite" : "none";
return !targetDoc ? (null) : <Tooltip title={<><div className="dash-tooltip">{`${published ? "Push" : "Publish"} to Google Docs`}</div></>}>
<div
- className="documentButtonBar-linker"
+ className="documentButtonBar-button"
style={{ animation }}
onClick={async () => {
await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken();
@@ -139,7 +139,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
return !targetDoc || !dataDoc || !dataDoc[GoogleRef] ? (null) : <Tooltip
title={<><div className="dash-tooltip">{title}</div></>}>
- <div className="documentButtonBar-linker"
+ <div className="documentButtonBar-button"
style={{ backgroundColor: this.pullColor }}
onPointerEnter={action(e => {
if (e.altKey) {
@@ -188,8 +188,8 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
const targetDoc = this.view0?.props.Document;
return !targetDoc ? (null) : <Tooltip title={
<div className="dash-tooltip">{"follow primary link on click"}</div>}>
- <div className="documentButtonBar-linker"
- style={{ color: targetDoc.isLinkButton ? "black" : "white", backgroundColor: targetDoc.isLinkButton ? "white" : "black" }}
+ <div className="documentButtonBar-icon"
+ style={{ color: targetDoc.isLinkButton ? "black" : "white" }}
onClick={undoBatch(e => this.props.views().map(view => view?.docView?.toggleFollowLink(undefined, false, false)))}>
<FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="hand-point-right" />
</div>
@@ -200,7 +200,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
const targetDoc = this.view0?.props.Document;
return !targetDoc ? (null) : <Tooltip title={
<div className="dash-tooltip">{SelectionManager.Views().length > 1 ? "Pin multiple documents to presentation" : "Pin to presentation"}</div>}>
- <div className="documentButtonBar-linker"
+ <div className="documentButtonBar-icon"
style={{ color: "white" }}
onClick={undoBatch(e => this.props.views().map(view => view && TabDocView.PinDoc(view.props.Document, { setPosition: e.shiftKey ? true : undefined })))}>
<FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="map-pin" />
@@ -243,7 +243,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
const presPinWithViewIcon = <img src="/assets/pinWithView.png" style={{ margin: "auto", width: 17, transform: 'translate(0, 1px)' }} />;
const targetDoc = this.view0?.props.Document;
return !targetDoc ? (null) : <Tooltip title={<><div className="dash-tooltip">{"Pin with current view"}</div></>}>
- <div className="documentButtonBar-linker" onClick={() => this.pinWithView(targetDoc)}>
+ <div className="documentButtonBar-icon" onClick={() => this.pinWithView(targetDoc)}>
{presPinWithViewIcon}
</div>
</Tooltip>;
@@ -253,8 +253,8 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
get shareButton() {
const targetDoc = this.view0?.props.Document;
return !targetDoc ? (null) : <Tooltip title={<><div className="dash-tooltip">{"Open Sharing Manager"}</div></>}>
- <div className="documentButtonBar-linker" style={{ color: "white" }} onClick={e => SharingManager.Instance.open(this.view0, targetDoc)}>
- <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="users" />
+ <div className="documentButtonBar-icon" style={{ color: "white" }} onClick={e => SharingManager.Instance.open(this.view0, targetDoc)}>
+ <FontAwesomeIcon className="documentdecorations-icon" icon="users" />
</div></Tooltip >;
}
@@ -262,8 +262,8 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
get menuButton() {
const targetDoc = this.view0?.props.Document;
return !targetDoc ? (null) : <Tooltip title={<><div className="dash-tooltip">{`Open Context Menu`}</div></>}>
- <div className="documentButtonBar-linker" style={{ color: "white", cursor: "pointer" }} onClick={e => this.openContextMenu(e)}>
- <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="bars" />
+ <div className="documentButtonBar-icon" style={{ color: "white", cursor: "pointer" }} onClick={e => this.openContextMenu(e)}>
+ <FontAwesomeIcon className="documentdecorations-icon" icon="bars" />
</div></Tooltip >;
}
@@ -271,9 +271,9 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
get moreButton() {
const targetDoc = this.view0?.props.Document;
return !targetDoc ? (null) : <Tooltip title={<><div className="dash-tooltip">{`${CurrentUserUtils.propertiesWidth > 0 ? "Close" : "Open"} Properties Panel`}</div></>}>
- <div className="documentButtonBar-linker" style={{ color: "white", cursor: "e-resize" }} onClick={action(e =>
+ <div className="documentButtonBar-icon" style={{ color: "white", cursor: "e-resize" }} onClick={action(e =>
CurrentUserUtils.propertiesWidth = CurrentUserUtils.propertiesWidth > 0 ? 0 : 250)}>
- <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="ellipsis-h"
+ <FontAwesomeIcon className="documentdecorations-icon" icon="ellipsis-h"
/>
</div></Tooltip >;
}
@@ -286,7 +286,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
<Flyout anchorPoint={anchorPoints.LEFT_TOP}
content={<MetadataEntryMenu docs={this.props.views().filter(dv => dv).map(dv => dv!.props.Document)} suggestWithFunction /> /* tfs: @bcz This might need to be the data document? */}>
<div className={"documentButtonBar-linkButton-" + "empty"} onPointerDown={e => e.stopPropagation()} >
- {<FontAwesomeIcon className="documentdecorations-icon" icon="tag" size="sm" />}
+ {<FontAwesomeIcon className="documentdecorations-icon" icon="tag" />}
</div>
</Flyout>
</div></Tooltip>;
@@ -355,7 +355,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
<div className="documentButtonBar-button">
<DocumentLinksButton View={this.view0} AlwaysOn={true} InMenu={true} StartLink={true} />
</div>
- {(DocumentLinksButton.StartLink || Doc.UserDoc()["documentLinksButton-fullMenu"]) && DocumentLinksButton.StartLink != doc ? <div className="documentButtonBar-button">
+ {(DocumentLinksButton.StartLink || Doc.UserDoc()["documentLinksButton-fullMenu"]) && DocumentLinksButton.StartLink !== doc ? <div className="documentButtonBar-button">
<DocumentLinksButton View={this.view0} AlwaysOn={true} InMenu={true} StartLink={false} />
</div> : (null)}
{/*!Doc.UserDoc()["documentLinksButton-fullMenu"] ? (null) : <div className="documentButtonBar-button">
diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss
index 952d8d150..316f63240 100644
--- a/src/client/views/DocumentDecorations.scss
+++ b/src/client/views/DocumentDecorations.scss
@@ -336,7 +336,7 @@ $linkGap : 3px;
}
.documentdecorations-icon {
- margin-top: 3px;
+ margin: 0px;
}
.templating-button,
.docDecs-tagButton {
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index d24ab974c..118d2e7c7 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -27,6 +27,7 @@ import { LightboxView } from './LightboxView';
import { DocumentView } from "./nodes/DocumentView";
import React = require("react");
import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
+import { DateField } from '../../fields/DateField';
@observer
export class DocumentDecorations extends React.Component<{ boundsLeft: number, boundsTop: number }, { value: string }> {
@@ -367,6 +368,7 @@ export class DocumentDecorations extends React.Component<{ boundsLeft: number, b
dW && (doc._width = actualdW);
dH && (doc._autoHeight = false);
}
+ doc._lastModified = new DateField();
}
const val = this._dragHeights.get(docView.layoutDoc);
if (val) this._dragHeights.set(docView.layoutDoc, { start: val.start, lowest: Math.min(val.lowest, NumCast(docView.layoutDoc._height)) });
diff --git a/src/client/views/EditableView.scss b/src/client/views/EditableView.scss
index 5dc0c1962..1aebedf2e 100644
--- a/src/client/views/EditableView.scss
+++ b/src/client/views/EditableView.scss
@@ -26,4 +26,10 @@
width: 100%;
background: inherit;
pointer-events: all;
-} \ No newline at end of file
+}
+
+.editableView-input:focus {
+ border: none;
+ outline: none;
+}
+ \ No newline at end of file
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 834c3b745..5fc159f14 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -45,14 +45,13 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
CognitiveServices.Inking.Appliers.ConcatenateHandwriting(this.dataDoc, ["inkAnalysis", "handwriting"], [data]);
}
- @action
- public static toggleMask = (inkDoc: Doc) => {
+ public static toggleMask = action((inkDoc: Doc) => {
inkDoc.isInkMask = !inkDoc.isInkMask;
inkDoc._backgroundColor = inkDoc.isInkMask ? "rgba(0,0,0,0.7)" : undefined;
inkDoc.mixBlendMode = inkDoc.isInkMask ? "hard-light" : undefined;
inkDoc.color = "#9b9b9bff";
inkDoc._stayInCollection = inkDoc.isInkMask ? true : undefined;
- }
+ });
/**
* Handles the movement of the entire ink object when the user clicks and drags.
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index 60327f1bf..7553c8118 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -12,6 +12,7 @@ import { LinkManager } from "../util/LinkManager";
AssignAllExtensions();
(async () => {
+ MainView.Live = window.location.search.includes("live");
window.location.search.includes("safe") && CollectionView.SetSafeMode(true);
const info = await CurrentUserUtils.loadCurrentUser();
if (info.id !== "__guest__") {
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 49f4f7a6e..b0b8d7f41 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -70,6 +70,7 @@ const _global = (window /* browser */ || global /* node */) as any;
@observer
export class MainView extends React.Component {
public static Instance: MainView;
+ public static Live: boolean = false;
private _docBtnRef = React.createRef<HTMLDivElement>();
@observable public LastButton: Opt<Doc>;
@observable private _windowWidth: number = 0;
@@ -105,8 +106,11 @@ export class MainView extends React.Component {
}
new InkStrokeProperties();
this._sidebarContent.proto = undefined;
- DocServer.setPlaygroundFields(["x", "y", "dataTransition", "_autoHeight", "_showSidebar", "_sidebarWidthPercent", "_width", "_height", "_viewTransition", "_panX", "_panY", "_viewScale", "_scrollTop", "hidden", "_curPage", "_viewType", "_chromeHidden"]); // can play with these fields on someone else's
-
+ if (!MainView.Live) {
+ DocServer.setPlaygroundFields(["dataTransition", "autoHeight", "showSidebar", "sidebarWidthPercent", "viewTransition",
+ "panX", "panY", "width", "height", "nativeWidth", "nativeHeight", "text-scrollHeight", "text-height", "hideMinimap",
+ "viewScale", "scrollTop", "hidden", "curPage", "viewType", "chromeHidden", "nativeWidth"]); // can play with these fields on someone else's
+ }
DocServer.GetRefField("rtfProto").then(proto => (proto instanceof Doc) && reaction(() => StrCast(proto.BROADCAST_MESSAGE), msg => msg && alert(msg)));
const tag = document.createElement('script');
diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx
index 805cda95c..a3a3bce56 100644
--- a/src/client/views/MarqueeAnnotator.tsx
+++ b/src/client/views/MarqueeAnnotator.tsx
@@ -1,6 +1,6 @@
import { action, observable, ObservableMap, runInAction } from "mobx";
import { observer } from "mobx-react";
-import { AclAddonly, AclAdmin, AclEdit, DataSym, Doc, Opt } from "../../fields/Doc";
+import { AclAugment, AclAdmin, AclEdit, DataSym, Doc, Opt, AclSelfEdit } from "../../fields/Doc";
import { Id } from "../../fields/FieldSymbols";
import { List } from "../../fields/List";
import { NumCast } from "../../fields/Types";
@@ -156,7 +156,7 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
highlight = (color: string, isLinkButton: boolean, savedAnnotations?: ObservableMap<number, HTMLDivElement[]>) => {
// creates annotation documents for current highlights
const effectiveAcl = GetEffectiveAcl(this.props.rootDoc[DataSym]);
- const annotationDoc = [AclAddonly, AclEdit, AclAdmin].includes(effectiveAcl) && this.makeAnnotationDocument(color, isLinkButton, savedAnnotations);
+ const annotationDoc = [AclAugment, AclSelfEdit, AclEdit, AclAdmin].includes(effectiveAcl) && this.makeAnnotationDocument(color, isLinkButton, savedAnnotations);
!savedAnnotations && annotationDoc && this.props.addDocument(annotationDoc);
return annotationDoc as Doc ?? undefined;
}
diff --git a/src/client/views/PropertiesView.scss b/src/client/views/PropertiesView.scss
index fa45a065d..321b83f52 100644
--- a/src/client/views/PropertiesView.scss
+++ b/src/client/views/PropertiesView.scss
@@ -1,6 +1,8 @@
+@import "./global/globalCssVariables.scss";
+
.propertiesView {
height: 100%;
- font-family: "Noto Sans";
+ font-family: "Roboto";
cursor: auto;
overflow-x: hidden;
@@ -28,9 +30,7 @@
color: grey;
cursor: pointer;
}
-
}
-
}
.propertiesView-name {
@@ -80,7 +80,6 @@
padding-bottom: 10px;
padding-top: 8px;
}
-
}
.propertiesView-sharing {
@@ -140,8 +139,6 @@
}
}
-
-
.change-buttons {
display: flex;
@@ -216,7 +213,6 @@
}
}
-
.propertiesView-appearance {
//border-bottom: 1px solid black;
//padding: 8.5px;
@@ -305,7 +301,7 @@
.notify-button-icon {
width: 6px;
height: 6.5px;
- margin-left: .5px;
+ margin-left: 0.5px;
}
&:hover {
@@ -331,7 +327,6 @@
}
.propertiesView-sharingTable {
-
// whatever's commented out - add it back in when adding the buttons
// border: 1.5px solid black;
@@ -347,7 +342,6 @@
width: 92%;
.propertiesView-sharingTable-item {
-
display: flex;
// padding: 5px;
padding: 3px;
@@ -421,7 +415,6 @@
cursor: pointer;
}
}
-
}
.propertiesView-fields-checkbox {
@@ -468,7 +461,6 @@
}
.propertiesView-contexts {
-
.propertiesView-contexts-title {
font-weight: bold;
font-size: 12.5px;
@@ -499,11 +491,9 @@
overflow: hidden;
padding: 10px;
}
-
}
.propertiesView-layout {
-
.propertiesView-layout-title {
font-weight: bold;
font-size: 12.5px;
@@ -534,7 +524,6 @@
overflow: hidden;
padding: 10px;
}
-
}
.propertiesView-presTrails {
@@ -576,7 +565,6 @@
}
.inking-button {
-
display: flex;
.inking-button-points {
@@ -635,7 +623,6 @@
}
.inputBox {
-
margin-top: 10px;
display: flex;
height: 19px;
@@ -658,7 +645,6 @@
}
.inputBox-button {
-
.inputBox-button-up {
background-color: #333333;
height: 9px;
@@ -690,7 +676,6 @@
cursor: pointer;
}
}
-
}
}
@@ -767,7 +752,6 @@
}
.widthAndDash {
-
.width {
.width-top {
display: flex;
@@ -792,13 +776,11 @@
}
.arrows {
-
display: flex;
margin-bottom: 3px;
margin-left: 4px;
.arrows-head {
-
display: flex;
margin-right: 35px;
@@ -827,7 +809,6 @@
}
.dashed {
-
display: flex;
margin-left: 64px;
margin-bottom: 6px;
@@ -844,19 +825,15 @@
}
.editable-title {
- border: none;
padding: 6px;
padding-bottom: 2px;
- background: #eeeeee;
- border-top: 1px solid;
- border-left: 1px solid;
+ border: solid 1px $dark-gray;
&:hover {
- border: 0.75px solid rgb(122, 28, 28);
+ border: 0.75px solid $medium-blue;
}
}
-
.properties-flyout {
grid-column: 2/4;
-} \ No newline at end of file
+}
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index 8136edf04..de437e1df 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -5,7 +5,7 @@ import { intersection } from "lodash";
import { action, autorun, computed, Lambda, observable, reaction, runInAction } from "mobx";
import { observer } from "mobx-react";
import { ColorState, SketchPicker } from "react-color";
-import { AclAddonly, AclAdmin, AclEdit, AclPrivate, AclReadonly, AclSym, AclUnset, DataSym, Doc, Field, HeightSym, Opt, WidthSym } from "../../fields/Doc";
+import { AclAugment, AclAdmin, AclEdit, AclPrivate, AclReadonly, AclSym, AclUnset, DataSym, Doc, Field, HeightSym, Opt, WidthSym, AclSelfEdit } from "../../fields/Doc";
import { Id } from "../../fields/FieldSymbols";
import { InkField } from "../../fields/InkField";
import { ComputedField } from "../../fields/ScriptField";
@@ -342,12 +342,9 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
return <select className="permissions-select"
value={permission}
onChange={e => this.changePermissions(e, user)}>
- {dropdownValues.filter(permission => permission !== SharingPermissions.View).map(permission => {
- return (
- <option key={permission} value={permission}>
- {permission === SharingPermissions.Add ? "Can Augment" : permission}
- </option>);
- })}
+ {dropdownValues.filter(permission =>
+ !Doc.UserDoc().noviceMode || ![SharingPermissions.View, SharingPermissions.SelfEdit].includes(permission as any)).map(permission =>
+ <option key={permission} value={permission}> {permission} </option>)}
</select>;
}
@@ -402,7 +399,8 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
[AclUnset, "None"],
[AclPrivate, SharingPermissions.None],
[AclReadonly, SharingPermissions.View],
- [AclAddonly, SharingPermissions.Add],
+ [AclAugment, SharingPermissions.Augment],
+ [AclSelfEdit, SharingPermissions.SelfEdit],
[AclEdit, SharingPermissions.Edit],
[AclAdmin, SharingPermissions.Admin]
]);
diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx
index c5ae82c61..1f9763d18 100644
--- a/src/client/views/SidebarAnnos.tsx
+++ b/src/client/views/SidebarAnnos.tsx
@@ -23,6 +23,8 @@ interface ExtraProps {
layoutDoc: Doc;
rootDoc: Doc;
dataDoc: Doc;
+ showSidebar: boolean;
+ nativeWidth: number;
whenChildContentsActiveChanged: (isActive: boolean) => void;
ScreenToLocalTransform: () => Transform;
sidebarAddDocument: (doc: (Doc | Doc[]), suffix: string) => boolean;
@@ -31,18 +33,22 @@ interface ExtraProps {
}
@observer
export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
+ constructor(props: Readonly<FieldViewProps & ExtraProps>) {
+ super(props);
+ // this.props.dataDoc[this.sidebarKey] = new List<Doc>(); // bcz: can't do this here. it blows away existing things and isn't a robust solution for making sure the field exists -- instead this should happen when the document is created and/or shared
+ }
_stackRef = React.createRef<CollectionStackingView>();
@computed get allHashtags() {
const keys = new Set<string>();
- DocListCast(this.props.rootDoc[this.sidebarKey()]).forEach(doc => SearchBox.documentKeys(doc).forEach(key => keys.add(key)));
+ DocListCast(this.props.rootDoc[this.sidebarKey]).forEach(doc => SearchBox.documentKeys(doc).forEach(key => keys.add(key)));
return Array.from(keys.keys()).filter(key => key[0]).filter(key => !key.startsWith("_") && (key[0] === "#" || key[0] === key[0].toUpperCase())).sort();
}
@computed get allUsers() {
const keys = new Set<string>();
- DocListCast(this.props.rootDoc[this.sidebarKey()]).forEach(doc => keys.add(StrCast(doc.author)));
+ DocListCast(this.props.rootDoc[this.sidebarKey]).forEach(doc => keys.add(StrCast(doc.author)));
return Array.from(keys.keys()).sort();
}
- get filtersKey() { return "_" + this.sidebarKey() + "-docFilters"; }
+ get filtersKey() { return "_" + this.sidebarKey + "-docFilters"; }
anchorMenuClick = (anchor: Doc) => {
const startup = StrListCast(this.props.rootDoc.docFilters).map(filter => filter.split(":")[0]).join(" ");
@@ -59,7 +65,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
this._stackRef.current?.focusDocument(target);
}
makeDocUnfiltered = (doc: Doc) => {
- if (DocListCast(this.props.rootDoc[this.sidebarKey()]).includes(doc)) {
+ if (DocListCast(this.props.rootDoc[this.sidebarKey]).includes(doc)) {
if (this.props.layoutDoc[this.filtersKey]) {
this.props.layoutDoc[this.filtersKey] = new List<string>();
return true;
@@ -67,14 +73,15 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
}
return false;
}
- sidebarKey = () => this.props.fieldKey + "-sidebar";
+
+ get sidebarKey() { return this.props.fieldKey + "-sidebar"; }
filtersHeight = () => 38;
screenToLocalTransform = () => this.props.ScreenToLocalTransform().translate(Doc.NativeWidth(this.props.dataDoc), 0).scale(this.props.scaling?.() || 1);
- panelWidth = () => !this.props.layoutDoc._showSidebar ? 0 : this.props.layoutDoc.type === DocumentType.RTF ? this.props.PanelWidth() : (NumCast(this.props.layoutDoc.nativeWidth) - Doc.NativeWidth(this.props.dataDoc)) * this.props.PanelWidth() / NumCast(this.props.layoutDoc.nativeWidth);
+ panelWidth = () => !this.props.showSidebar ? 0 : this.props.layoutDoc.type === DocumentType.RTF ? this.props.PanelWidth() : (NumCast(this.props.nativeWidth) - Doc.NativeWidth(this.props.dataDoc)) * this.props.PanelWidth() / NumCast(this.props.nativeWidth);
panelHeight = () => this.props.PanelHeight() - this.filtersHeight();
- addDocument = (doc: Doc | Doc[]) => this.props.sidebarAddDocument(doc, this.sidebarKey());
- moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean) => this.props.moveDocument(doc, targetCollection, addDocument, this.sidebarKey());
- removeDocument = (doc: Doc | Doc[]) => this.props.removeDocument(doc, this.sidebarKey());
+ addDocument = (doc: Doc | Doc[]) => this.props.sidebarAddDocument(doc, this.sidebarKey);
+ moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean) => this.props.moveDocument(doc, targetCollection, addDocument, this.sidebarKey);
+ removeDocument = (doc: Doc | Doc[]) => this.props.removeDocument(doc, this.sidebarKey);
docFilters = () => [...StrListCast(this.props.layoutDoc._docFilters), ...StrListCast(this.props.layoutDoc[this.filtersKey])];
sidebarStyleProvider = (doc: Opt<Doc>, props: Opt<FieldViewProps | DocumentViewProps>, property: string) => {
@@ -87,18 +94,18 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
const renderTag = (tag: string) => {
const active = StrListCast(this.props.rootDoc[this.filtersKey]).includes(`${tag}:${tag}:check`);
return <div key={tag} className={`sidebarAnnos-filterTag${active ? "-active" : ""}`}
- onClick={e => Doc.setDocFilter(this.props.rootDoc, tag, tag, "check", true, this.sidebarKey(), e.shiftKey)}>
+ onClick={e => Doc.setDocFilter(this.props.rootDoc, tag, tag, "check", true, this.sidebarKey, e.shiftKey)}>
{tag}
</div>;
};
const renderUsers = (user: string) => {
const active = StrListCast(this.props.rootDoc[this.filtersKey]).includes(`author:${user}:check`);
return <div key={user} className={`sidebarAnnos-filterUser${active ? "-active" : ""}`}
- onClick={e => Doc.setDocFilter(this.props.rootDoc, "author", user, "check", true, this.sidebarKey(), e.shiftKey)}>
+ onClick={e => Doc.setDocFilter(this.props.rootDoc, "author", user, "check", true, this.sidebarKey, e.shiftKey)}>
{user}
</div>;
};
- return !this.props.layoutDoc._showSidebar ? (null) :
+ return !this.props.showSidebar ? (null) :
<div style={{
position: "absolute", pointerEvents: this.props.isContentActive() ? "all" : undefined, top: 0, right: 0,
background: this.props.styleProvider?.(this.props.rootDoc, this.props, StyleProp.WidgetColor),
@@ -118,7 +125,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
PanelWidth={this.panelWidth}
styleProvider={this.sidebarStyleProvider}
docFilters={this.docFilters}
- scaleField={this.sidebarKey() + "-scale"}
+ scaleField={this.sidebarKey + "-scale"}
setHeight={(height) => this.props.setHeight(height + this.filtersHeight())}
isAnnotationOverlay={false}
select={emptyFunction}
@@ -132,7 +139,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
ScreenToLocalTransform={this.screenToLocalTransform}
renderDepth={this.props.renderDepth + 1}
viewType={CollectionViewType.Stacking}
- fieldKey={this.sidebarKey()}
+ fieldKey={this.sidebarKey}
pointerEvents={"all"}
/>
</div>
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index a8471f8e2..c0d39b2a2 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -4,7 +4,7 @@ import { action, IReactionDisposer, observable, reaction, runInAction } from "mo
import { observer } from "mobx-react";
import * as ReactDOM from 'react-dom';
import * as GoldenLayout from "../../../client/goldenLayout";
-import { Doc, DocListCast, Opt, DocListCastAsync } from "../../../fields/Doc";
+import { Doc, DocListCast, Opt, DocListCastAsync, DataSym } from "../../../fields/Doc";
import { Id } from '../../../fields/FieldSymbols';
import { InkTool } from '../../../fields/InkField';
import { List } from '../../../fields/List';
@@ -24,6 +24,7 @@ import React = require("react");
import { DocumentType } from '../../documents/DocumentTypes';
import { listSpec } from '../../../fields/Schema';
import { LightboxView } from '../LightboxView';
+import { inheritParentAcls } from '../../../fields/util';
const _global = (window /* browser */ || global /* node */) as any;
@observer
@@ -160,6 +161,18 @@ export class CollectionDockingView extends CollectionSubView(doc => doc) {
}
const instance = CollectionDockingView.Instance;
if (!instance) return false;
+ else {
+ const docList = DocListCast(instance.props.Document[DataSym]["data-all"]);
+ // adds the doc of the newly created tab to the data-all field if it doesn't already include that doc or one of its aliases
+ !docList.includes(document) && !docList.includes(document.aliasOf as Doc) && Doc.AddDocToList(instance.props.Document[DataSym], "data-all", document);
+ // adds an alias of the doc to the data-all field of the layoutdocs of the aliases
+ DocListCast(instance.props.Document[DataSym].aliases).forEach(alias => {
+ const aliasDocList = DocListCast(alias["data-all"]);
+ // if aliasDocList contains the alias, don't do anything
+ // otherwise add the original or an alias depending on whether the doc you're looking at is the current doc or a different alias
+ !DocListCast(document.aliases).some(a => aliasDocList.includes(a)) && Doc.AddDocToList(alias, "data-all", alias !== instance.props.Document ? Doc.MakeAlias(document) : document);
+ });
+ }
const docContentConfig = CollectionDockingView.makeDocumentConfig(document, panelName);
if (!pullSide && stack) {
@@ -381,15 +394,22 @@ export class CollectionDockingView extends CollectionSubView(doc => doc) {
setTimeout(async () => {
const sublists = await DocListCastAsync(this.props.Document[this.props.fieldKey]);
const tabs = sublists && Cast(sublists[0], Doc, null);
- const other = sublists && Cast(sublists[1], Doc, null);
+ // const other = sublists && Cast(sublists[1], Doc, null);
const tabdocs = await DocListCastAsync(tabs?.data);
- const otherdocs = await DocListCastAsync(other?.data);
- tabs && (Doc.GetProto(tabs).data = new List<Doc>(docs));
- const otherSet = new Set<Doc>();
- otherdocs?.filter(doc => !docs.includes(doc)).forEach(doc => otherSet.add(doc));
- tabdocs?.filter(doc => !docs.includes(doc) && doc.type !== DocumentType.KVP).forEach(doc => otherSet.add(doc));
- const vals = Array.from(otherSet.values()).filter(val => val instanceof Doc).map(d => d).filter(d => d.type !== DocumentType.KVP);
- other && (Doc.GetProto(other).data = new List<Doc>(vals));
+ // const otherdocs = await DocListCastAsync(other?.data);
+ if (tabs) {
+ tabs.data = new List<Doc>(docs);
+ // DocListCast(tabs.aliases).forEach(tab => tab !== tabs && (tab.data = new List<Doc>(docs)));
+ }
+ // const otherSet = new Set<Doc>();
+ // otherdocs?.filter(doc => !docs.includes(doc)).forEach(doc => otherSet.add(doc));
+ // tabdocs?.filter(doc => !docs.includes(doc) && doc.type !== DocumentType.KVP).forEach(doc => otherSet.add(doc));
+ // const vals = Array.from(otherSet.values()).filter(val => val instanceof Doc).map(d => d).filter(d => d.type !== DocumentType.KVP);
+ // this.props.Document[DataSym][this.props.fieldKey + "-all"] = new List<Doc>([...docs, ...vals]);
+ // if (other) {
+ // other.data = new List<Doc>(vals);
+ // // DocListCast(other.aliases).forEach(tab => tab !== other && (tab.data = new List<Doc>(vals)));
+ // }
}, 0);
}
@@ -399,7 +419,7 @@ export class CollectionDockingView extends CollectionSubView(doc => doc) {
tab.reactComponents?.forEach((ele: any) => ReactDOM.unmountComponentAtNode(ele));
}
tabCreated = (tab: any) => {
- tab.contentItem.element[0]?.firstChild?.firstChild?.InitTab?.(tab); // have to explicitly initialize tabs that reuse contents from previous abs (ie, when dragging a tab around a new tab is created for the old content)
+ tab.contentItem.element[0]?.firstChild?.firstChild?.InitTab?.(tab); // have to explicitly initialize tabs that reuse contents from previous tabs (ie, when dragging a tab around a new tab is created for the old content)
}
stackCreated = (stack: any) => {
@@ -407,9 +427,11 @@ export class CollectionDockingView extends CollectionSubView(doc => doc) {
if (e.target === stack.header?.element[0] && e.button === 2) {
const emptyPane = CurrentUserUtils.EmptyPane;
emptyPane["dragFactory-count"] = NumCast(emptyPane["dragFactory-count"]) + 1;
- CollectionDockingView.AddSplit(Docs.Create.FreeformDocument([], {
- _width: this.props.PanelWidth(), _height: this.props.PanelHeight(), _fitWidth: true, title: `Untitled Tab ${NumCast(emptyPane["dragFactory-count"])}`
- }), "", stack);
+ const docToAdd = Docs.Create.FreeformDocument([], {
+ _width: this.props.PanelWidth(), _height: this.props.PanelHeight(), _fitWidth: true, title: `Untitled Tab ${NumCast(emptyPane["dragFactory-count"])}`,
+ });
+ this.props.Document.isShared && inheritParentAcls(this.props.Document, docToAdd);
+ CollectionDockingView.AddSplit(docToAdd, "", stack);
}
});
@@ -430,9 +452,11 @@ export class CollectionDockingView extends CollectionSubView(doc => doc) {
// stack.config.fixed = !stack.config.fixed; // force the stack to have a fixed size
const emptyPane = CurrentUserUtils.EmptyPane;
emptyPane["dragFactory-count"] = NumCast(emptyPane["dragFactory-count"]) + 1;
- CollectionDockingView.AddSplit(Docs.Create.FreeformDocument([], {
+ const docToAdd = Docs.Create.FreeformDocument([], {
_width: this.props.PanelWidth(), _height: this.props.PanelHeight(), _fitWidth: true, title: `Untitled Tab ${NumCast(emptyPane["dragFactory-count"])}`
- }), "", stack);
+ });
+ this.props.Document.isShared && inheritParentAcls(this.props.Document, docToAdd);
+ CollectionDockingView.AddSplit(docToAdd, "", stack);
}));
}
diff --git a/src/client/views/collections/CollectionLinearView.scss b/src/client/views/collections/CollectionLinearView.scss
index 86610ac20..46e40489b 100644
--- a/src/client/views/collections/CollectionLinearView.scss
+++ b/src/client/views/collections/CollectionLinearView.scss
@@ -20,8 +20,9 @@
}
.bottomPopup-background {
- background: $light-blue;
+ background: $medium-blue;
display: flex;
+ border-radius: 10px;
height: 35;
transform: translate3d(6px, 0px, 0px);
align-content: center;
@@ -30,7 +31,7 @@
}
.bottomPopup-text {
- color: black;
+ color: $white;
display: inline;
white-space: nowrap;
padding-left: 8px;
@@ -40,25 +41,27 @@
}
.bottomPopup-descriptions {
+ cursor:pointer;
display: inline;
white-space: nowrap;
padding-left: 8px;
padding-right: 8px;
vertical-align: middle;
- background-color: #efefef;
+ background-color: $light-gray;
border-radius: 3px;
color: black;
margin-right: 5px;
}
.bottomPopup-exit {
+ cursor:pointer;
display: inline;
white-space: nowrap;
margin-right: 10px;
padding-left: 8px;
padding-right: 8px;
vertical-align: middle;
- background-color: #f3b6b6;
+ background-color: $close-red;
border-radius: 3px;
color: black;
}
diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx
index a9b978c4e..8f4df4a92 100644
--- a/src/client/views/collections/CollectionMenu.tsx
+++ b/src/client/views/collections/CollectionMenu.tsx
@@ -665,7 +665,7 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionMenu
}
@computed get drawButtons() {
- const func = action((i: number, keep: boolean) => {
+ const func = action((e: React.MouseEvent | React.PointerEvent, i: number, keep: boolean) => {
this._keepPrimitiveMode = keep;
if (this._selectedPrimitive !== i) {
this._selectedPrimitive = i;
@@ -683,13 +683,14 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionMenu
GestureOverlay.Instance.InkShape = "";
SetActiveBezierApprox("0");
}
+ e.stopPropagation();
});
return <div className="btn-draw" key="draw">
{this._draw.map((icon, i) =>
<Tooltip key={icon} title={<div className="dash-tooltip">{this._title[i]}</div>} placement="bottom">
<button className="antimodeMenu-button"
- onPointerDown={() => func(i, false)}
- onDoubleClick={() => func(i, true)}
+ onPointerDown={e => func(e, i, false)}
+ onDoubleClick={e => func(e, i, true)}
style={{ backgroundColor: i === this._selectedPrimitive ? "525252" : "", fontSize: "20" }}>
<FontAwesomeIcon icon={this._faName[i] as IconProp} size="sm" />
</button>
diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx
index e1e04915a..380f82813 100644
--- a/src/client/views/collections/CollectionSchemaView.tsx
+++ b/src/client/views/collections/CollectionSchemaView.tsx
@@ -413,8 +413,8 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
isContentActive={returnTrue}
isDocumentActive={returnFalse}
ScreenToLocalTransform={this.getPreviewTransform}
- docFilters={this.docFilters}
- docRangeFilters={this.docRangeFilters}
+ docFilters={this.childDocFilters}
+ docRangeFilters={this.childDocRangeFilters}
searchFilterDocs={this.searchFilterDocs}
styleProvider={DefaultStyleProvider}
layerProvider={undefined}
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 7aa8dfd56..b9bc83d49 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -239,10 +239,10 @@ export class CollectionStackingView extends CollectionSubView<StackingDocument,
onDoubleClick={this.onChildDoubleClickHandler}
ScreenToLocalTransform={stackedDocTransform}
focus={this.focusDocument}
- docFilters={this.docFilters}
+ docFilters={this.childDocFilters}
hideDecorationTitle={this.props.childHideDecorationTitle?.()}
hideTitle={this.props.childHideTitle?.()}
- docRangeFilters={this.docRangeFilters}
+ docRangeFilters={this.childDocRangeFilters}
searchFilterDocs={this.searchFilterDocs}
ContainingCollectionDoc={this.props.CollectionView?.props.Document}
ContainingCollectionView={this.props.CollectionView}
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index a5d27f038..2f07c0241 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -8,7 +8,7 @@ import { ScriptField } from "../../../fields/ScriptField";
import { WebField } from "../../../fields/URLField";
import { Cast, ScriptCast, NumCast, StrCast } from "../../../fields/Types";
import { GestureUtils } from "../../../pen-gestures/GestureUtils";
-import { Utils, returnFalse } from "../../../Utils";
+import { Utils, returnFalse, returnEmptyFilter } from "../../../Utils";
import { DocServer } from "../../DocServer";
import { Networking } from "../../Network";
import { ImageUtils } from "../../util/Import & Export/ImageUtils";
@@ -22,6 +22,7 @@ import ReactLoading from 'react-loading';
export interface SubCollectionViewProps extends CollectionViewProps {
CollectionView: Opt<CollectionView>;
+ SetSubView?: (subView: any) => void;
}
export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: X) {
@@ -30,6 +31,8 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
private gestureDisposer?: GestureUtils.GestureEventDisposer;
protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
protected _mainCont?: HTMLDivElement;
+ @observable _focusFilters: Opt<string[]>; // docFilters that are overridden when previewing a link to an anchor which has docFilters set on it
+ @observable _focusRangeFilters: Opt<string[]>; // docRangeFilters that are overridden when previewing a link to an anchor which has docRangeFilters set on it
protected createDashEventsTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view
this.dropDisposer?.();
this.gestureDisposer?.();
@@ -45,6 +48,10 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
this.createDashEventsTarget(ele);
}
+ componentDidMount() {
+ this.props.SetSubView?.(this);
+ }
+
componentWillUnmount() {
this.gestureDisposer?.();
this._multiTouchDisposer?.();
@@ -73,23 +80,21 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
get childLayoutPairs(): { layout: Doc; data: Doc; }[] {
const { Document, DataDoc } = this.props;
const validPairs = this.childDocs.map(doc => Doc.GetLayoutDataDocPair(Document, !this.props.isAnnotationOverlay ? DataDoc : undefined, doc)).
- filter(pair => { // filter out any documents that have a proto that we don't have permissions to (which we determine by not having any keys
- return pair.layout && /*!pair.layout.hidden &&*/ (!pair.layout.proto || (pair.layout.proto instanceof Doc && GetEffectiveAcl(pair.layout.proto) !== AclPrivate));// Object.keys(pair.layout.proto).length));
+ filter(pair => { // filter out any documents that have a proto that we don't have permissions to
+ return pair.layout && (!pair.layout.proto || (pair.layout.proto instanceof Doc && GetEffectiveAcl(pair.layout.proto) !== AclPrivate));
});
return validPairs.map(({ data, layout }) => ({ data: data as Doc, layout: layout! })); // this mapping is a bit of a hack to coerce types
}
get childDocList() {
return Cast(this.dataField, listSpec(Doc));
}
- docFilters = () => {
- return [...this.props.docFilters(), ...Cast(this.props.Document._docFilters, listSpec("string"), [])];
- }
- docRangeFilters = () => {
- return [...this.props.docRangeFilters(), ...Cast(this.props.Document._docRangeFilters, listSpec("string"), [])];
- }
- searchFilterDocs = () => {
- return [...this.props.searchFilterDocs(), ...DocListCast(this.props.Document._searchFilterDocs)];
- }
+ collectionFilters = () => this._focusFilters ?? Cast(this.props.Document._docFilters, listSpec("string"), []);
+ collectionRangeDocFilters = () => this._focusRangeFilters ?? Cast(this.props.Document._docRangeFilters, listSpec("string"), []);
+ childDocFilters = () => [...this.props.docFilters(), ...this.collectionFilters()];
+ childDocRangeFilters = () => [...(this.props.docRangeFilters?.() || []), ...this.collectionRangeDocFilters()];
+ IsFiltered = () => this.collectionFilters().length || this.collectionRangeDocFilters().length ? "hasFilter" :
+ this.props.docFilters().length || this.props.docRangeFilters().length ? "inheritsFilter" : undefined
+ searchFilterDocs = () => this.props.searchFilterDocs?.() ?? DocListCast(this.props.Document._searchFilterDocs);
@computed.struct get childDocs() {
TraceMobx();
let rawdocs: (Doc | Promise<Doc>)[] = [];
@@ -108,8 +113,8 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
const viewSpecScript = Cast(this.props.Document.viewSpecScript, ScriptField);
const childDocs = viewSpecScript ? docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result) : docs;
- const docFilters = this.docFilters();
- const docRangeFilters = this.docRangeFilters();
+ const docFilters = this.childDocFilters();
+ const docRangeFilters = this.childDocRangeFilters();
const searchDocs = this.searchFilterDocs();
if (this.props.Document.dontRegisterView || (!docFilters.length && !docRangeFilters.length && !searchDocs.length)) {
return childDocs.filter(cd => !cd.cookies); // remove any documents that require a cookie if there are no filters to provide one
@@ -303,7 +308,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
} else {
const path = window.location.origin + "/doc/";
if (text.startsWith(path)) {
- const docid = text.replace(Utils.prepend("/doc/"), "").split("?")[0];
+ const docid = text.replace(Doc.globalServerPath(), "").split("?")[0];
DocServer.GetRefField(docid).then(f => {
if (f instanceof Doc) {
if (options.x || options.y) { f.x = options.x; f.y = options.y; } // should be in CollectionFreeFormView
diff --git a/src/client/views/collections/CollectionTimeView.tsx b/src/client/views/collections/CollectionTimeView.tsx
index 339163510..292dfd77c 100644
--- a/src/client/views/collections/CollectionTimeView.tsx
+++ b/src/client/views/collections/CollectionTimeView.tsx
@@ -32,12 +32,10 @@ export class CollectionTimeView extends CollectionSubView(doc => doc) {
@observable _collapsed: boolean = false;
@observable _childClickedScript: Opt<ScriptField>;
@observable _viewDefDivClick: Opt<ScriptField>;
- @observable _focusDocFilters: Opt<string[]>; // fields that get overridden by a focus anchor
@observable _focusPivotField: Opt<string>;
- @observable _focusRangeFilters: Opt<string[]>;
getAnchor = () => {
- const anchor = Docs.Create.HTMLAnchorDocument({
+ const anchor = Docs.Create.HTMLAnchorDocument([], {
title: ComputedField.MakeFunction(`"${this.pivotField}"])`) as any,
annotationOn: this.rootDoc
});
@@ -72,9 +70,9 @@ export class CollectionTimeView extends CollectionSubView(doc => doc) {
@action
setViewSpec = (anchor: Doc, preview: boolean) => {
if (preview) { // if in preview, then override document's fields with view spec
+ this._focusFilters = StrListCast(Doc.GetProto(anchor).docFilters);
+ this._focusRangeFilters = StrListCast(Doc.GetProto(anchor).docRangeFilters);
this._focusPivotField = StrCast(anchor.pivotField);
- this._focusDocFilters = StrListCast(anchor.docFilters);
- this._focusRangeFilters = StrListCast(anchor.docRangeFilters);
} else if (anchor.pivotField !== undefined) { // otherwise set document's fields based on anchor view spec
this.layoutDoc._prevFilterIndex = 1;
this.layoutDoc._pivotField = StrCast(anchor.pivotField);
@@ -84,8 +82,6 @@ export class CollectionTimeView extends CollectionSubView(doc => doc) {
return 0;
}
- pivotDocFilters = () => this._focusDocFilters || this.props.docFilters();
- pivotDocRangeFilters = () => this._focusRangeFilters || this.props.docRangeFilters();
layoutEngine = () => this._layoutEngine;
toggleVisibility = action(() => this._collapsed = !this._collapsed);
@@ -139,10 +135,8 @@ export class CollectionTimeView extends CollectionSubView(doc => doc) {
return <div className="collectionTimeView-innards" key="timeline" style={{ pointerEvents: this.props.isContentActive() ? undefined : "none" }}
onClick={this.contentsDown}>
<CollectionFreeFormView {...this.props}
- engineProps={{ pivotField: this.pivotField, docFilters: this.docFilters, docRangeFilters: this.docRangeFilters }}
+ engineProps={{ pivotField: this.pivotField, docFilters: this.childDocFilters, docRangeFilters: this.childDocRangeFilters }}
fitContentsToDoc={returnTrue}
- docFilters={this.pivotDocFilters}
- docRangeFilters={this.pivotDocRangeFilters}
childClickScript={this._childClickedScript}
viewDefDivClick={this._viewDefDivClick}
childFreezeDimensions={true}
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index e225c4a11..e65ebf075 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -1,4 +1,4 @@
-import { computed, observable, runInAction } from 'mobx';
+import { computed, observable, runInAction, action } from 'mobx';
import { observer } from "mobx-react";
import * as React from 'react';
import 'react-image-lightbox-with-rotate/style.css'; // This only needs to be imported once in your app
@@ -236,13 +236,16 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab
* Shows the filter icon if it's a user-created collection which isn't a dashboard and has some docFilters applied on it or on the current dashboard.
*/
@computed get showFilterIcon() {
- return this.props.Document.viewType !== CollectionViewType.Docking && !Doc.IsSystem(this.props.Document) && ((StrListCast(this.props.Document._docFilters).length || StrListCast(this.props.Document._docRangeFilters).length || StrListCast(CurrentUserUtils.ActiveDashboard._docFilters).length || StrListCast(CurrentUserUtils.ActiveDashboard._docRangeFilters).length));
+ return this.props.Document.viewType !== CollectionViewType.Docking && !Doc.IsSystem(this.props.Document) && this._subView?.IsFiltered();
}
+ @observable _subView: any = undefined;
+
render() {
TraceMobx();
const props: SubCollectionViewProps = {
...this.props,
+ SetSubView: action((subView: any) => this._subView = subView),
addDocument: this.addDocument,
moveDocument: this.moveDocument,
removeDocument: this.removeDocument,
@@ -260,8 +263,8 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab
{this.collectionViewType !== undefined ? this.SubView(this.collectionViewType, props) : (null)}
{this.showFilterIcon ?
<FontAwesomeIcon icon={"filter"} size="lg"
- style={{ position: 'absolute', top: '1%', right: '1%', cursor: "pointer", padding: 1, color: '#18c718bd', zIndex: 1 }}
- onPointerDown={e => { runInAction(() => CurrentUserUtils.propertiesWidth = 250); e.stopPropagation(); }}
+ style={{ position: 'absolute', top: '1%', right: '1%', cursor: "pointer", padding: 1, color: this.showFilterIcon === "hasFilter" ? '#18c718bd' : "orange", zIndex: 1 }}
+ onPointerDown={action(e => { this.props.select(false); CurrentUserUtils.propertiesWidth = 250; e.stopPropagation(); })}
/>
: (null)}
</div>);
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index a24f1eb7a..1969d728c 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -171,10 +171,9 @@ export class TabDocView extends React.Component<TabDocViewProps> {
//attach the selection doc buttons menu to the drag handle
const stack: HTMLDivElement = tab.contentItem.parent;
const header: HTMLDivElement = tab;
- console.log("Stack: " + stack.id, stack.className)
stack.onscroll = action((e: any) => {
- console.log('scrolling...')
- })
+ console.log('scrolling...');
+ });
const moreInfoDrag = document.createElement("div");
moreInfoDrag.className = "lm_iconWrap";
tab._disposers.buttonDisposer = reaction(() => this.view, view =>
@@ -363,8 +362,8 @@ export class TabDocView extends React.Component<TabDocViewProps> {
PanelHeight={this.PanelHeight}
layerProvider={this.layerProvider}
styleProvider={DefaultStyleProvider}
- docFilters={CollectionDockingView.Instance.docFilters}
- docRangeFilters={CollectionDockingView.Instance.docRangeFilters}
+ docFilters={CollectionDockingView.Instance.childDocFilters}
+ docRangeFilters={CollectionDockingView.Instance.childDocRangeFilters}
searchFilterDocs={CollectionDockingView.Instance.searchFilterDocs}
addDocument={undefined}
removeDocument={undefined}
@@ -468,6 +467,7 @@ export class TabMinimapView extends React.Component<TabMinimapViewProps> {
<div className="miniMap" style={{ width: miniSize, height: miniSize, background: this.props.background() }}>
<CollectionFreeFormView
Document={this.props.document}
+ SetSubView={() => this}
CollectionView={undefined}
ContainingCollectionView={undefined}
ContainingCollectionDoc={undefined}
@@ -496,8 +496,8 @@ export class TabMinimapView extends React.Component<TabMinimapViewProps> {
layerProvider={undefined}
addDocTab={this.props.addDocTab}
pinToPres={TabDocView.PinDoc}
- docFilters={CollectionDockingView.Instance.docFilters}
- docRangeFilters={CollectionDockingView.Instance.docRangeFilters}
+ docFilters={CollectionDockingView.Instance.childDocFilters}
+ docRangeFilters={CollectionDockingView.Instance.childDocRangeFilters}
searchFilterDocs={CollectionDockingView.Instance.searchFilterDocs}
fitContentsToDoc={returnTrue}
/>
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index 401bdcb02..206e48aac 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -151,7 +151,10 @@ export class TreeView extends React.Component<TreeViewProps> {
this.treeViewOpen = !this.treeViewOpen;
} else {
// choose an appropriate alias or make one. --- choose the first alias that (1) user owns, (2) has no context field ... otherwise make a new alias
+ // this.props.addDocTab(CurrentUserUtils.ActiveDashboard.isShared ? Doc.MakeAlias(this.props.document) : this.props.document, "add:right");
+ // choose an appropriate alias or make one -- -- choose the first alias that (1) the user owns, (2) has no context field - if I own it and someone else does not have it open,, otherwise create an alias
this.props.addDocTab(this.props.document, "add:right");
+
}
}
constructor(props: any) {
@@ -508,9 +511,10 @@ export class TreeView extends React.Component<TreeViewProps> {
Doc.IsSystem(this.doc) ? [] :
this.props.treeView.fileSysMode && this.doc === Doc.GetProto(this.doc) ?
[{ script: ScriptField.MakeFunction(`openOnRight(getAlias(self))`)!, label: "Open Alias" }, makeFolder] :
- [{ script: ScriptField.MakeFunction(`DocFocusOrOpen(self)`)!, label: "Focus or Open" }];
+ [{ script: ScriptField.MakeFunction(`openOnRight(getAlias(self))`)!, label: "Open Alias" }, { script: ScriptField.MakeFunction(`DocFocusOrOpen(self)`)!, label: "Focus or Open" }];
}
onChildClick = () => this.props.onChildClick?.() ?? (this._editTitleScript?.() || ScriptCast(this.doc.treeChildClick));
+
onChildDoubleClick = () => (!this.props.treeView.outlineMode && this._openScript?.()) || ScriptCast(this.doc.treeChildDoubleClick);
refocus = () => this.props.treeView.props.focus(this.props.treeView.props.Document);
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 143d8e070..fa7e75202 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -49,6 +49,7 @@ import "./CollectionFreeFormView.scss";
import { MarqueeView } from "./MarqueeView";
import React = require("react");
import { DocumentType } from "../../../documents/DocumentTypes";
+import { DateField } from "../../../../fields/DateField";
export const panZoomSchema = createSchema({
_panX: "number",
@@ -110,8 +111,6 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
@observable _timelineRef = React.createRef<Timeline>();
@observable _marqueeRef = React.createRef<HTMLDivElement>();
@observable _keyframeEditing = false;
- @observable _focusFilters: Opt<string[]>; // docFilters that are overridden when previewing a link to an anchor which has docFilters set on it
- @observable _focusRangeFilters: Opt<string[]>; // docRangeFilters that are overridden when previewing a link to an anchor which has docRangeFilters set on it
@observable ChildDrag: DocumentView | undefined; // child document view being dragged. needed to update drop areas of groups when a group item is dragged.
@computed get views() { return this._layoutElements.filter(ele => ele.bounds && !ele.bounds.z).map(ele => ele.ele); }
@@ -159,8 +158,6 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
this.layoutDoc._viewScale = vals.scale;
}
freeformData = (force?: boolean) => this.fitToContent || force ? this.fitToContentVals : undefined;
- freeformDocFilters = () => this._focusFilters || this.docFilters();
- freeformRangeDocFilters = () => this._focusRangeFilters || this.docRangeFilters();
reverseNativeScaling = () => this.fitToContent ? true : false;
panX = () => this.freeformData()?.bounds.cx ?? NumCast(this.Document._panX);
panY = () => this.freeformData()?.bounds.cy ?? NumCast(this.Document._panY);
@@ -261,6 +258,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
d.x = x + NumCast(d.x) - dropPos[0];
d.y = y + NumCast(d.y) - dropPos[1];
}
+ d._lastModified = new DateField();
const nd = [Doc.NativeWidth(layoutDoc), Doc.NativeHeight(layoutDoc)];
layoutDoc._width = NumCast(layoutDoc._width, 300);
layoutDoc._height = NumCast(layoutDoc._height, nd[0] && nd[1] ? nd[1] / nd[0] * NumCast(layoutDoc._width) : 300);
@@ -1030,8 +1028,8 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
ScreenToLocalTransform={childLayout.z ? this.getTransformOverlay : this.getTransform}
PanelWidth={childLayout[WidthSym]}
PanelHeight={childLayout[HeightSym]}
- docFilters={this.freeformDocFilters}
- docRangeFilters={this.freeformRangeDocFilters}
+ docFilters={this.childDocFilters}
+ docRangeFilters={this.childDocRangeFilters}
searchFilterDocs={this.searchFilterDocs}
isContentActive={this.isAnnotationOverlay ? this.props.isContentActive : returnFalse}
isDocumentActive={this.props.childDocumentsActive ? this.props.isDocumentActive : this.isContentActive}
@@ -1197,14 +1195,21 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
if (preview) {
this._focusFilters = StrListCast(Doc.GetProto(anchor).docFilters);
this._focusRangeFilters = StrListCast(Doc.GetProto(anchor).docRangeFilters);
- } else if (anchor.pivotField !== undefined) {
- this.layoutDoc._docFilters = new List<string>(StrListCast(anchor.docFilters));
- this.layoutDoc._docRangeFilters = new List<string>(StrListCast(anchor.docRangeFilters));
+ } else {
+ if (anchor.docFilters) {
+ this.layoutDoc._docFilters = new List<string>(StrListCast(anchor.docFilters));
+ }
+ if (anchor.docRangeFilters) {
+ this.layoutDoc._docRangeFilters = new List<string>(StrListCast(anchor.docRangeFilters));
+ }
}
return 0;
}
getAnchor = () => {
+ if (this.props.Document.annotationOn) {
+ return this.rootDoc;
+ }
const anchor = Docs.Create.TextanchorDocument({ title: StrCast(this.layoutDoc._viewType), annotationOn: this.rootDoc });
const proto = Doc.GetProto(anchor);
proto[ViewSpecPrefix + "_viewType"] = this.layoutDoc._viewType;
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 846d28214..19da7ea00 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -1,6 +1,6 @@
import { action, computed, observable } from "mobx";
import { observer } from "mobx-react";
-import { AclAddonly, AclAdmin, AclEdit, DataSym, Doc, Opt } from "../../../../fields/Doc";
+import { AclAugment, AclAdmin, AclEdit, DataSym, Doc, Opt } from "../../../../fields/Doc";
import { Id } from "../../../../fields/FieldSymbols";
import { InkData, InkField, InkTool } from "../../../../fields/InkField";
import { List } from "../../../../fields/List";
@@ -298,7 +298,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
this._downX = x;
this._downY = y;
const effectiveAcl = GetEffectiveAcl(this.props.Document[DataSym]);
- if ([AclAdmin, AclEdit, AclAddonly].includes(effectiveAcl)) {
+ if ([AclAdmin, AclEdit, AclAugment].includes(effectiveAcl)) {
PreviewCursor.Show(x, y, this.onKeyPress, this.props.addLiveTextDocument, this.props.getTransform, this.props.addDocument, this.props.nudge);
}
this.clearSelection();
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
index f3a39a262..65c345547 100644
--- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
@@ -237,8 +237,8 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu
onDoubleClick={this.onChildDoubleClickHandler}
ScreenToLocalTransform={dxf}
focus={this.props.focus}
- docFilters={this.docFilters}
- docRangeFilters={this.docRangeFilters}
+ docFilters={this.childDocFilters}
+ docRangeFilters={this.childDocRangeFilters}
searchFilterDocs={this.searchFilterDocs}
ContainingCollectionDoc={this.props.CollectionView?.props.Document}
ContainingCollectionView={this.props.CollectionView}
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
index 29cb3511a..30836854a 100644
--- a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
@@ -236,9 +236,9 @@ export class CollectionMultirowView extends CollectionSubView(MultirowDocument)
onDoubleClick={this.onChildDoubleClickHandler}
ScreenToLocalTransform={dxf}
focus={this.props.focus}
- docFilters={this.docFilters}
+ docFilters={this.childDocFilters}
isContentActive={returnFalse}
- docRangeFilters={this.docRangeFilters}
+ docRangeFilters={this.childDocRangeFilters}
searchFilterDocs={this.searchFilterDocs}
ContainingCollectionDoc={this.props.CollectionView?.props.Document}
ContainingCollectionView={this.props.CollectionView}
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
index 585cda729..fed64b620 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
@@ -413,8 +413,8 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
isContentActive={returnTrue}
isDocumentActive={returnFalse}
ScreenToLocalTransform={this.getPreviewTransform}
- docFilters={this.docFilters}
- docRangeFilters={this.docRangeFilters}
+ docFilters={this.childDocFilters}
+ docRangeFilters={this.childDocRangeFilters}
searchFilterDocs={this.searchFilterDocs}
styleProvider={DefaultStyleProvider}
layerProvider={undefined}
diff --git a/src/client/views/global/globalCssVariables.scss b/src/client/views/global/globalCssVariables.scss
index a9f33c4da..7556f8b8a 100644
--- a/src/client/views/global/globalCssVariables.scss
+++ b/src/client/views/global/globalCssVariables.scss
@@ -1,17 +1,17 @@
-@import url("https://fonts.googleapis.com/css?family=Noto+Sans:400,700|Crimson+Text:400,400i,700");
+@import url("https://fonts.googleapis.com/css2?family=Roboto&display=swap");
// colors
$white: #ffffff;
-$light-gray:#dfdfdf;
-$medium-gray: #9F9F9F;
+$light-gray: #dfdfdf;
+$medium-gray: #9f9f9f;
$dark-gray: #323232;
$black: #000000;
-$light-blue: #BDDDF5;
-$medium-blue: #4476F7;
-$pink: #E0217D;
-$yellow: #F5D747;
+$light-blue: #bdddf5;
+$medium-blue: #4476f7;
+$pink: #e0217d;
+$yellow: #f5d747;
-$logout-red: #ca4444;
+$close-red: #e48282;
$drop-shadow: "#32323215";
@@ -24,7 +24,7 @@ $large-padding: 32px;
$icon-size: 28px;
// fonts
-$sans-serif: "Noto Sans", sans-serif;
+$sans-serif: "Roboto", sans-serif;
$large-header: 16px;
$body-text: 12px;
$small-text: 9px;
@@ -41,7 +41,7 @@ $contextMenu-zindex: 100000; // context menu shows up over everything
$radialMenu-zindex: 100000; // context menu shows up over everything
// borders
-$standard-border: solid 1px #9F9F9F;
+$standard-border: solid 1px #9f9f9f;
$searchpanel-height: 32px;
$mainTextInput-zindex: 999; // then text input overlay so that it's context menu will appear over decorations, etc
@@ -49,7 +49,7 @@ $docDecorations-zindex: 998; // then doc decorations appear over everything else
$remoteCursors-zindex: 997; // ... not sure what level the remote cursors should go -- is this right?
$COLLECTION_BORDER_WIDTH: 0;
$SCHEMA_DIVIDER_WIDTH: 4;
-$MINIMIZED_ICON_SIZE:24;
+$MINIMIZED_ICON_SIZE: 24;
$MAX_ROW_HEIGHT: 44px;
$DFLT_IMAGE_NATIVE_DIM: 900px;
$MENU_PANEL_WIDTH: 60px;
@@ -67,4 +67,4 @@ $TREE_BULLET_WIDTH: 20px;
DFLT_IMAGE_NATIVE_DIM: $DFLT_IMAGE_NATIVE_DIM;
MENU_PANEL_WIDTH: $MENU_PANEL_WIDTH;
TREE_BULLET_WIDTH: $TREE_BULLET_WIDTH;
-} \ No newline at end of file
+}
diff --git a/src/client/views/linking/LinkPopup.tsx b/src/client/views/linking/LinkPopup.tsx
index 2c4b718f4..df469c53b 100644
--- a/src/client/views/linking/LinkPopup.tsx
+++ b/src/client/views/linking/LinkPopup.tsx
@@ -54,7 +54,6 @@ export class LinkPopup extends React.Component<LinkPopupProps> {
@action
onLinkChange = (e: React.ChangeEvent<HTMLInputElement>) => {
this.linkURL = e.target.value;
- console.log(this.linkURL)
}
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index a2e36f12e..82bad971d 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -196,7 +196,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
this._recorder.ondataavailable = async (e: any) => {
const [{ result }] = await Networking.UploadFilesToServer(e.data);
if (!(result instanceof Error)) {
- this.props.Document[this.props.fieldKey] = new AudioField(Utils.prepend(result.accessPaths.agnostic.client));
+ this.props.Document[this.props.fieldKey] = new AudioField(result.accessPaths.agnostic.client);
}
};
this._recordStart = new Date().getTime();
diff --git a/src/client/views/nodes/DocumentLinksButton.scss b/src/client/views/nodes/DocumentLinksButton.scss
index 9bab72d55..b37b68249 100644
--- a/src/client/views/nodes/DocumentLinksButton.scss
+++ b/src/client/views/nodes/DocumentLinksButton.scss
@@ -44,25 +44,43 @@
}
.documentLinksButton {
- background-color: black;
+ background-color: $dark-gray;
+ color: $white;
font-weight: bold;
+ width: 80%;
+ height: 80%;
+ font-size: 100%;
+ transition: 0.2s ease all;
&:hover {
- transform: scale(1.05);
- cursor: pointer;
+ background-color: $black;
+ }
+}
+
+.documentLinksButton.startLink {
+ background-color: $medium-blue;
+ color: $white;
+ font-weight: bold;
+ width: 80%;
+ height: 80%;
+ font-size: 100%;
+ transition: 0.2s ease all;
+
+ &:hover {
+ background-color: $black;
}
}
.documentLinksButton-endLink {
border: $medium-blue 2px dashed;
color: $medium-blue;
+ background-color: none !important;
+ width: 80%;
+ height: 80%;
+ font-size: 100%;
+ transition: 0.2s ease all;
+
&:hover {
- background: $light-gray;
- transform: scale(1.05);
- cursor: pointer;
+ background-color: $light-blue;
}
-}
-
-.documentLinksButton-startLink {
- background-color: $medium-blue;
} \ No newline at end of file
diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx
index cec06d2d4..7648e866e 100644
--- a/src/client/views/nodes/DocumentLinksButton.tsx
+++ b/src/client/views/nodes/DocumentLinksButton.tsx
@@ -68,6 +68,40 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
return false;
}
+ onLinkMenuOpen = (e: React.PointerEvent): void => {
+ setupMoveUpEvents(this, e, this.onLinkButtonMoved, emptyFunction, action((e, doubleTap) => {
+ if (doubleTap) {
+ const rootDoc = this.props.View.rootDoc;
+ const docid = Doc.CurrentUserEmail + Doc.GetProto(rootDoc)[Id] + "-pivotish";
+ DocServer.GetRefField(docid).then(async docx => {
+ const rootAlias = () => {
+ const rootAlias = Doc.MakeAlias(rootDoc);
+ rootAlias.x = rootAlias.y = 0;
+ return rootAlias;
+ };
+ let wid = rootDoc[WidthSym]();
+ const target = ((docx instanceof Doc) && docx) || Docs.Create.FreeformDocument([rootAlias()], { title: this.props.View.Document.title + "-pivot", _width: 500, _height: 500, }, docid);
+ const docs = await DocListCastAsync(Doc.GetProto(target).data);
+ if (!target.pivotFocusish) (Doc.GetProto(target).pivotFocusish = target);
+ DocListCast(rootDoc.links).forEach(link => {
+ const other = LinkManager.getOppositeAnchor(link, rootDoc);
+ const otherdoc = !other ? undefined : other.annotationOn ? Cast(other.annotationOn, Doc, null) : other;
+ if (otherdoc && !docs?.some(d => Doc.AreProtosEqual(d, otherdoc))) {
+ const alias = Doc.MakeAlias(otherdoc);
+ alias.x = wid;
+ alias.y = 0;
+ alias._lockedPosition = false;
+ wid += otherdoc[WidthSym]();
+ Doc.AddDocToList(Doc.GetProto(target), "data", alias);
+ }
+ });
+ LightboxView.SetLightboxDoc(target);
+ });
+ }
+ else DocumentLinksButton.LinkEditorDocView = this.props.View;
+ }));
+ }
+
@undoBatch
onLinkButtonDown = (e: React.PointerEvent): void => {
setupMoveUpEvents(this, e, this.onLinkButtonMoved, emptyFunction, action((e, doubleTap) => {
@@ -80,36 +114,6 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
DocumentLinksButton.StartLink = this.props.View.props.Document;
DocumentLinksButton.StartLinkView = this.props.View;
}
- } else if (!this.props.InMenu) {
- if (doubleTap) {
- const rootDoc = this.props.View.rootDoc;
- const docid = Doc.CurrentUserEmail + Doc.GetProto(rootDoc)[Id] + "-pivotish";
- DocServer.GetRefField(docid).then(async docx => {
- const rootAlias = () => {
- const rootAlias = Doc.MakeAlias(rootDoc);
- rootAlias.x = rootAlias.y = 0;
- return rootAlias;
- };
- let wid = rootDoc[WidthSym]();
- const target = ((docx instanceof Doc) && docx) || Docs.Create.FreeformDocument([rootAlias()], { title: this.props.View.Document.title + "-pivot", _width: 500, _height: 500, }, docid);
- const docs = await DocListCastAsync(Doc.GetProto(target).data);
- if (!target.pivotFocusish) (Doc.GetProto(target).pivotFocusish = target);
- DocListCast(rootDoc.links).forEach(link => {
- const other = LinkManager.getOppositeAnchor(link, rootDoc);
- const otherdoc = !other ? undefined : other.annotationOn ? Cast(other.annotationOn, Doc, null) : other;
- if (otherdoc && !docs?.some(d => Doc.AreProtosEqual(d, otherdoc))) {
- const alias = Doc.MakeAlias(otherdoc);
- alias.x = wid;
- alias.y = 0;
- alias._lockedPosition = false;
- wid += otherdoc[WidthSym]();
- Doc.AddDocToList(Doc.GetProto(target), "data", alias);
- }
- });
- LightboxView.SetLightboxDoc(target);
- });
- }
- else DocumentLinksButton.LinkEditorDocView = this.props.View;
}
}));
}
@@ -126,7 +130,6 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
DocumentLinksButton.StartLink = this.props.View.props.Document;
DocumentLinksButton.StartLinkView = this.props.View;
}
-
//action(() => Doc.BrushDoc(this.props.View.Document));
} else if (!this.props.InMenu) {
DocumentLinksButton.LinkEditorDocView = this.props.View;
@@ -195,7 +198,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
Doc.GetProto(linkDoc as Doc).linksToAnnotation = true;
Doc.GetProto(linkDoc as Doc).annotationId = DocumentLinksButton.AnnotationId;
Doc.GetProto(linkDoc as Doc).annotationUri = DocumentLinksButton.AnnotationUri;
- const dashHyperlink = Utils.prepend("/doc/" + (startIsAnnotation ? endLink[Id] : startLink[Id]));
+ const dashHyperlink = Doc.globalServerPath(startIsAnnotation ? endLink : startLink);
Hypothesis.makeLink(StrCast(startIsAnnotation ? endLink.title : startLink.title), dashHyperlink, DocumentLinksButton.AnnotationId,
(startIsAnnotation ? startLink : endLink)); // edit annotation to add a Dash hyperlink to the linked doc
}
@@ -247,17 +250,19 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
/**
* gets the JSX of the link button (btn used to start/complete links) OR the link-view button (btn on bottom left of each linked node)
+ *
+ * todo:glr / anh seperate functionality such as onClick onPointerDown of link menu button
*/
@computed get linkButtonInner() {
- const btnDim = this.props.InMenu ? "20px" : "30px";
+ const btnDim = "30px";
const link = <img style={{ width: "22px", height: "16px" }} src={`/assets/${"link.png"}`} />;
-
+ const isActive = (DocumentLinksButton.StartLink === this.props.View.props.Document) && this.props.StartLink;
return (!this.props.InMenu ?
- <div className="documentLinksButton-cont" ref={this._linkButton}
+ <div className="documentLinksButton-cont"
style={{ left: this.props.Offset?.[0], top: this.props.Offset?.[1], right: this.props.Offset?.[2], bottom: this.props.Offset?.[3] }}
>
<div className={"documentLinksButton"}
- onPointerDown={this.onLinkButtonDown} onClick={this.onLinkClick}
+ onPointerDown={this.onLinkMenuOpen} onClick={this.onLinkClick}
style={{
backgroundColor: Colors.LIGHT_BLUE,
color: Colors.BLACK,
@@ -268,34 +273,25 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
</div>
</div>
:
- <div className="documentLinksButton-menu">
+ <div className="documentLinksButton-menu" ref={this._linkButton}>
{this.props.InMenu && !this.props.StartLink && DocumentLinksButton.StartLink !== this.props.View.props.Document ? //if the origin node is not this node
<div className={"documentLinksButton-endLink"}
- style={{
- width: btnDim, height: btnDim,
- backgroundColor: DocumentLinksButton.StartLink ? "" : Colors.LIGHT_GRAY,
- opacity: DocumentLinksButton.StartLink ? "" : "50%",
- border: DocumentLinksButton.StartLink ? "" : "none",
- cursor: DocumentLinksButton.StartLink ? "pointer" : "default"
- }}
onPointerDown={DocumentLinksButton.StartLink && this.completeLink}
onClick={e => DocumentLinksButton.StartLink && DocumentLinksButton.finishLinkClick(e.clientX, e.clientY, DocumentLinksButton.StartLink, this.props.View.props.Document, true, this.props.View)}>
- {this.props.StartLink ?
- <FontAwesomeIcon className="documentdecorations-icon" icon="link" size="sm" />
- : link}
+ <FontAwesomeIcon className="documentdecorations-icon" icon="link" />
</div>
: (null)
}
{
- this.props.InMenu ? //if link has been started from current node, then set behavior of link button to deactivate linking when clicked again
- <div className={'documentLinksButton' + (DocumentLinksButton.StartLink === this.props.View.props.Document && this.props.StartLink) ? '-startLink' : ''} onPointerDown={this.clearLinks} onClick={this.clearLinks} style={{ width: btnDim, height: btnDim }}>
- {this.props.StartLink ?
- <FontAwesomeIcon className="documentdecorations-icon" icon="link" size="sm" />
- : link}
+ this.props.InMenu && this.props.StartLink ? //if link has been started from current node, then set behavior of link button to deactivate linking when clicked again
+ <div className={`documentLinksButton ${isActive ? `startLink` : ``}`} onPointerDown={isActive ? undefined : this.onLinkButtonDown} onClick={isActive ? this.clearLinks : this.onLinkClick}>
+ <FontAwesomeIcon className="documentdecorations-icon" icon="link" />
</div>
- : (null)}
+ :
+ (null)
+ }
</div>
- )
+ );
}
render() {
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 80a014926..745d58656 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -64,7 +64,7 @@ export enum ViewAdjustment {
doNothing = 0
}
-export const ViewSpecPrefix = "_VIEW"; // field prefix for anchor fields that are immediately copied over to the target document when link is followed. Other anchor properties will be copied over in the specific setViewSpec() method on their view (which allows for seting preview values instead of writing to the document)
+export const ViewSpecPrefix = "viewSpec"; // field prefix for anchor fields that are immediately copied over to the target document when link is followed. Other anchor properties will be copied over in the specific setViewSpec() method on their view (which allows for seting preview values instead of writing to the document)
export interface DocFocusOptions {
originalTarget?: Doc; // set in JumpToDocument, used by TabDocView to determine whether to fit contents to tab
@@ -420,8 +420,10 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
focus = (anchor: Doc, options?: DocFocusOptions) => {
LightboxView.SetCookie(StrCast(anchor["cookies-set"]));
- // copying over _VIEW fields immediately allows the view type to switch to create the right _componentView
- Array.from(Object.keys(Doc.GetProto(anchor))).filter(key => key.startsWith(ViewSpecPrefix)).forEach(spec => this.layoutDoc[spec.replace(ViewSpecPrefix, "")] = ((field) => field instanceof ObjectField ? ObjectField.MakeCopy(field) : field)(anchor[spec]));
+ // copying over VIEW fields immediately allows the view type to switch to create the right _componentView
+ Array.from(Object.keys(Doc.GetProto(anchor))).filter(key => key.startsWith(ViewSpecPrefix)).forEach(spec => {
+ this.layoutDoc[spec.replace(ViewSpecPrefix, "")] = ((field) => field instanceof ObjectField ? ObjectField.MakeCopy(field) : field)(anchor[spec]);
+ });
// after a timeout, the right _componentView should have been created, so call it to update its view spec values
setTimeout(() => this._componentView?.setViewSpec?.(anchor, LinkDocPreview.LinkInfo ? true : false));
const focusSpeed = this._componentView?.scrollFocus?.(anchor, !LinkDocPreview.LinkInfo); // bcz: smooth parameter should really be passed into focus() instead of inferred here
@@ -746,7 +748,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
moreItems.push({ description: "Tag Child Images via Google Photos", event: () => GooglePhotos.Query.TagChildImages(this.props.Document), icon: "caret-square-right" });
moreItems.push({ description: "Write Back Link to Album", event: () => GooglePhotos.Transactions.AddTextEnrichment(this.props.Document), icon: "caret-square-right" });
}
- moreItems.push({ description: "Copy ID", event: () => Utils.CopyText(Utils.prepend("/doc/" + this.props.Document[Id])), icon: "fingerprint" });
+ moreItems.push({ description: "Copy ID", event: () => Utils.CopyText(Doc.globalServerPath(this.props.Document)), icon: "fingerprint" });
}
}
@@ -754,16 +756,15 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
moreItems.push({ description: "Close", event: this.deleteClicked, icon: "times" });
}
- !more && cm.addItem({ description: "More...", subitems: moreItems, icon: "hand-point-right" });
- cm.moveAfter(cm.findByDescription("More...")!, cm.findByDescription("OnClick...")!);
-
const help = cm.findByDescription("Help...");
const helpItems: ContextMenuProps[] = help && "subitems" in help ? help.subitems : [];
!Doc.UserDoc().novice && helpItems.push({ description: "Show Fields ", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), "add:right"), icon: "layer-group" });
- helpItems.push({ description: "Text Shortcuts Ctrl+/", event: () => this.props.addDocTab(Docs.Create.PdfDocument(Utils.prepend("/assets/cheat-sheet.pdf"), { _width: 300, _height: 300 }), "add:right"), icon: "keyboard" });
+ helpItems.push({ description: "Text Shortcuts Ctrl+/", event: () => this.props.addDocTab(Docs.Create.PdfDocument("/assets/cheat-sheet.pdf", { _width: 300, _height: 300 }), "add:right"), icon: "keyboard" });
!Doc.UserDoc().novice && helpItems.push({ description: "Print Document in Console", event: () => console.log(this.props.Document), icon: "hand-point-right" });
+ !Doc.UserDoc().novice && helpItems.push({ description: "Print DataDoc in Console", event: () => console.log(this.props.Document[DataSym]), icon: "hand-point-right" });
cm.addItem({ description: "Help...", noexpand: true, subitems: helpItems, icon: "question" });
}
+
if (!this.topMost) e?.stopPropagation(); // DocumentViews should stop propagation of this event
cm.displayMenu((e?.pageX || pageX || 0) - 15, (e?.pageY || pageY || 0) - 15);
DocumentViewInternal.SelectAfterContextMenu && !this.props.isSelected(true) && setTimeout(() => SelectionManager.SelectView(this.props.DocumentView(), false), 300); // on a mac, the context menu is triggered on mouse down, but a YouTube video becaomes interactive when selected which means that the context menu won't show up. by delaying the selection until hopefully after the pointer up, the context menu will appear.
@@ -838,7 +839,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
if (this.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) return null;
if (this.layoutDoc.presBox || this.rootDoc.type === DocumentType.LINK || this.props.dontRegisterView) return (null);
// need to use allLinks for RTF since embedded linked text anchors are not rendered with DocumentViews. All other documents render their anchors with nested DocumentViews so we just need to render the directLinks here
- const filtered = DocUtils.FilterDocs(this.rootDoc.type === DocumentType.RTF ? this.allLinks : this.directLinks, this.props.docFilters(), []).filter(d => !d.hidden);
+ const filtered = DocUtils.FilterDocs(this.rootDoc.type === DocumentType.RTF ? this.allLinks : this.directLinks, this.props.docFilters?.() ?? [], []).filter(d => !d.hidden);
return filtered.map((link, i) =>
<div className="documentView-anchorCont" key={i + 1}>
<DocumentView {...this.props}
@@ -885,7 +886,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
recorder.ondataavailable = async (e: any) => {
const [{ result }] = await Networking.UploadFilesToServer(e.data);
if (!(result instanceof Error)) {
- const audioDoc = Docs.Create.AudioDocument(Utils.prepend(result.accessPaths.agnostic.client), { title: "audio test", _width: 200, _height: 32 });
+ const audioDoc = Docs.Create.AudioDocument(result.accessPaths.agnostic.client, { title: "audio test", _width: 200, _height: 32 });
audioDoc.treeViewExpandedView = "layout";
const audioAnnos = Cast(self.dataDoc[self.LayoutFieldKey + "-audioAnnotations"], listSpec(Doc));
if (audioAnnos === undefined) {
@@ -971,7 +972,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
const highlightIndex = this.props.LayoutTemplateString ? (Doc.IsHighlighted(this.props.Document) ? 6 : 0) : Doc.isBrushedHighlightedDegree(this.props.Document); // bcz: Argh!! need to identify a tree view doc better than a LayoutTemlatString
const highlightColor = (CurrentUserUtils.ActiveDashboard?.darkScheme ?
["transparent", "#65350c", "#65350c", "yellow", "magenta", "cyan", "orange"] :
- ["transparent", "maroon", "maroon", "yellow", "magenta", "cyan", "orange"])[highlightIndex];
+ ["transparent", "#4476F7", "#4476F7", "yellow", "magenta", "cyan", "orange"])[highlightIndex];
const highlightStyle = ["solid", "dashed", "solid", "solid", "solid", "solid", "solid"][highlightIndex];
const excludeTypes = !this.props.treeViewDoc ? [DocumentType.FONTICON, DocumentType.INK] : [DocumentType.FONTICON];
let highlighting = !this.props.disableDocBrushing && highlightIndex && !excludeTypes.includes(this.layoutDoc.type as any) && this.layoutDoc._viewType !== CollectionViewType.Linear;
@@ -979,7 +980,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
const borderPath = this.props.styleProvider?.(this.props.Document, this.props, StyleProp.BorderPath) || { path: undefined };
const internal = PresBox.EffectsProvider(this.layoutDoc, this.renderDoc) || this.renderDoc;
- const boxShadow = highlighting && this.borderRounding && highlightStyle !== "dashed" ? `0 0 0 ${highlightIndex}px ${highlightColor}` :
+ const boxShadow = this.props.treeViewDoc ? null : highlighting && this.borderRounding && highlightStyle !== "dashed" ? `0 0 0 ${highlightIndex}px ${highlightColor}` :
this.boxShadow || (this.props.Document.isTemplateForField ? "black 0.2vw 0.2vw 0.8vw" : undefined);
return <div className={DocumentView.ROOT_DIV} ref={this._mainCont}
onContextMenu={this.onContextMenu}
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index 86250c9d1..ebbc1138a 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -64,9 +64,9 @@ export class FieldView extends React.Component<FieldViewProps> {
// else if (field instaceof PresBox) {
// return <PresBox {...this.props} />;
// }
- else if (field instanceof VideoField) {
- return <VideoBox {...this.props} />;
- }
+ // else if (field instanceof VideoField) {
+ // return <VideoBox {...this.props} />;
+ // }
// else if (field instanceof AudioField) {
// return <AudioBox {...this.props} />;
//}
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index cfd43bb62..2c0106960 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -238,7 +238,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
let succeeded = true;
let data: ImageField | undefined;
try {
- data = new ImageField(Utils.prepend(accessPaths.agnostic.client));
+ data = new ImageField(accessPaths.agnostic.client);
} catch {
succeeded = false;
}
diff --git a/src/client/views/nodes/LinkDescriptionPopup.scss b/src/client/views/nodes/LinkDescriptionPopup.scss
index d92823ccc..a8db5d360 100644
--- a/src/client/views/nodes/LinkDescriptionPopup.scss
+++ b/src/client/views/nodes/LinkDescriptionPopup.scss
@@ -1,9 +1,13 @@
+@import "../global/globalCssVariables.scss";
+
.linkDescriptionPopup {
display: flex;
-
- border: 1px solid rgb(170, 26, 26);
-
+ flex-direction: row;
+ justify-content: center;
+ align-items: center;
+ border: 2px solid $medium-blue;
+ background-color: $white;
width: auto;
position: absolute;
@@ -11,17 +15,11 @@
z-index: 10000;
border-radius: 10px;
font-size: 12px;
- //white-space: nowrap;
-
- background-color: rgba(250, 250, 250, 0.95);
- padding-top: 9px;
- padding-bottom: 9px;
- padding-left: 9px;
- padding-right: 9px;
+ gap: 5px;
+ padding: 9px;
.linkDescriptionPopup-input {
float: left;
- background-color: rgba(250, 250, 250, 0.95);
color: rgb(100, 100, 100);
border: none;
min-width: 160px;
@@ -30,46 +28,29 @@
.linkDescriptionPopup-btn {
float: right;
-
justify-content: center;
vertical-align: middle;
-
.linkDescriptionPopup-btn-dismiss {
- background-color: white;
- color: black;
+ cursor: pointer;
display: inline;
- right: 0;
- border-radius: 10px;
- border: 1px solid black;
- padding: 3px;
- font-size: 9px;
- text-align: center;
- position: relative;
- margin-right: 4px;
- justify-content: center;
-
- &:hover{
- cursor: pointer;
- }
+ white-space: nowrap;
+ padding: 5px;
+ vertical-align: middle;
+ background-color: $close-red;
+ border-radius: 3px;
+ color: black;
}
.linkDescriptionPopup-btn-add {
- background-color: black;
- color: white;
+ cursor: pointer;
display: inline;
- right: 0;
- border-radius: 10px;
- border: 1px solid black;
- padding: 3px;
- font-size: 9px;
- text-align: center;
- position: relative;
- justify-content: center;
-
- &:hover{
- cursor: pointer;
- }
+ white-space: nowrap;
+ padding: 5px;
+ vertical-align: middle;
+ background-color: $light-blue;
+ border-radius: 3px;
+ color: black;
}
}
diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx
index b73fb10df..126a37eb8 100644
--- a/src/client/views/nodes/LinkDocPreview.tsx
+++ b/src/client/views/nodes/LinkDocPreview.tsx
@@ -72,14 +72,14 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
@computed get href() {
if (this.props.hrefs?.length) {
const href = this.props.hrefs[this._hrefInd];
- if (href.indexOf(Utils.prepend("/doc/")) !== 0) { // link to a web page URL -- try to show a preview
+ if (href.indexOf(Doc.localServerPath()) !== 0) { // link to a web page URL -- try to show a preview
if (href.startsWith("https://en.wikipedia.org/wiki/")) {
wiki().page(href.replace("https://en.wikipedia.org/wiki/", "")).then(page => page.summary().then(action(summary => this._toolTipText = summary.substring(0, 500))));
} else {
setTimeout(action(() => this._toolTipText = "url => " + href));
}
} else { // hyperlink to a document .. decode doc id and retrieve from the server. this will trigger vals() being invalidated
- const anchorDoc = href.replace(Utils.prepend("/doc/"), "").split("?")[0];
+ const anchorDoc = href.replace(Doc.localServerPath(), "").split("?")[0];
anchorDoc && DocServer.GetRefField(anchorDoc).then(action(anchor => {
if (anchor instanceof Doc && DocListCast(anchor.links).length) {
this._linkDoc = DocListCast(anchor.links)[0];
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 8f61e252b..23236cf20 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -3,13 +3,13 @@ import { action, computed, IReactionDisposer, observable, reaction, runInAction
import { observer } from "mobx-react";
import * as Pdfjs from "pdfjs-dist";
import "pdfjs-dist/web/pdf_viewer.css";
-import { Doc, Opt, WidthSym } from "../../../fields/Doc";
+import { Doc, Opt, WidthSym, DocListCast } from "../../../fields/Doc";
import { documentSchema } from '../../../fields/documentSchemas';
import { makeInterface } from "../../../fields/Schema";
-import { Cast, NumCast, StrCast } from '../../../fields/Types';
+import { Cast, NumCast, StrCast, BoolCast } from '../../../fields/Types';
import { PdfField } from "../../../fields/URLField";
import { TraceMobx } from '../../../fields/util';
-import { Utils, setupMoveUpEvents, emptyFunction } from '../../../Utils';
+import { Utils, setupMoveUpEvents, emptyFunction, returnOne } from '../../../Utils';
import { Docs } from '../../documents/Documents';
import { KeyCodes } from '../../util/KeyCodes';
import { undoBatch } from '../../util/UndoManager';
@@ -31,6 +31,7 @@ const PdfDocument = makeInterface(documentSchema, panZoomSchema, pageSchema);
@observer
export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps, PdfDocument>(PdfDocument) {
public static LayoutString(fieldKey: string) { return FieldView.LayoutString(PDFBox, fieldKey); }
+ public static openSidebarWidth = 250;
private _searchString: string = "";
private _initialScrollTarget: Opt<Doc>;
private _pdfViewer: PDFViewer | undefined;
@@ -53,30 +54,6 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
if (PDFBox.pdfcache.get(this.pdfUrl.url.href)) runInAction(() => this._pdf = PDFBox.pdfcache.get(this.pdfUrl!.url.href));
else if (PDFBox.pdfpromise.get(this.pdfUrl.url.href)) PDFBox.pdfpromise.get(this.pdfUrl.url.href)?.then(action(pdf => this._pdf = pdf));
}
-
- const backup = "oldPath";
- const href = this.pdfUrl?.url.href;
- if (href) {
- const pathCorrectionTest = /upload\_[a-z0-9]{32}.(.*)/g;
- const matches = pathCorrectionTest.exec(href);
- // console.log("\nHere's the { url } being fed into the outer regex:");
- // console.log(href);
- // console.log("And here's the 'properPath' build from the captured filename:\n");
- if (matches !== null && href.startsWith(window.location.origin)) {
- const properPath = Utils.prepend(`/files/pdfs/${matches[0]}`);
- //console.log(properPath);
- if (!properPath.includes(href)) {
- console.log(`The two (url and proper path) were not equal`);
- const proto = Doc.GetProto(this.props.Document);
- proto[this.props.fieldKey] = new PdfField(properPath);
- proto[backup] = href;
- } else {
- //console.log(`The two (url and proper path) were equal`);
- }
- } else {
- console.log("Outer matches was null!");
- }
- }
}
componentWillUnmount() { this._selectReactionDisposer?.(); }
@@ -90,6 +67,9 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
}
scrollFocus = (doc: Doc, smooth: boolean) => {
+ if (DocListCast(this.props.Document[this.fieldKey + "-sidebar"]).includes(doc) && !this.SidebarShown) {
+ this.toggleSidebar(!smooth);
+ }
if (this._sidebarRef?.current?.makeDocUnfiltered(doc)) return 1;
this._initialScrollTarget = doc;
return this._pdfViewer?.scrollFocus(doc, smooth);
@@ -165,15 +145,24 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
this.layoutDoc._showSidebar = nativeWidth !== this.layoutDoc._nativeWidth;
}
return false;
- }, emptyFunction, this.toggleSidebar);
+ }, emptyFunction, () => this.toggleSidebar());
}
- toggleSidebar = action(() => {
+ @observable _previewNativeWidth: Opt<number> = undefined;
+ @observable _previewWidth: Opt<number> = undefined;
+ toggleSidebar = action((preview: boolean = false) => {
const nativeWidth = NumCast(this.layoutDoc[this.fieldKey + "-nativeWidth"]);
- const ratio = ((!this.layoutDoc.nativeWidth || this.layoutDoc.nativeWidth === nativeWidth ? 250 : 0) + nativeWidth) / nativeWidth;
+ const ratio = ((!this.layoutDoc.nativeWidth || this.layoutDoc.nativeWidth === nativeWidth ? PDFBox.openSidebarWidth : 0) + nativeWidth) / nativeWidth;
const curNativeWidth = NumCast(this.layoutDoc.nativeWidth, nativeWidth);
- this.layoutDoc.nativeWidth = nativeWidth * ratio;
- this.layoutDoc._width = this.layoutDoc[WidthSym]() * nativeWidth * ratio / curNativeWidth;
- this.layoutDoc._showSidebar = nativeWidth !== this.layoutDoc._nativeWidth;
+ if (preview) {
+ this._previewNativeWidth = nativeWidth * ratio;
+ this._previewWidth = this.layoutDoc[WidthSym]() * nativeWidth * ratio / curNativeWidth;
+ this._showSidebar = true;
+ }
+ else {
+ this.layoutDoc.nativeWidth = nativeWidth * ratio;
+ this.layoutDoc._width = this.layoutDoc[WidthSym]() * nativeWidth * ratio / curNativeWidth;
+ this.layoutDoc._showSidebar = nativeWidth !== this.layoutDoc._nativeWidth;
+ }
});
settingsPanel() {
const pageBtns = <>
@@ -226,7 +215,9 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
</button>
</div>;
}
- sidebarWidth = () => !this.layoutDoc._showSidebar ? 0 : (NumCast(this.layoutDoc.nativeWidth) - Doc.NativeWidth(this.dataDoc)) * this.props.PanelWidth() / NumCast(this.layoutDoc.nativeWidth);
+ sidebarWidth = () => !this.SidebarShown ? 0 :
+ this._previewWidth ? PDFBox.openSidebarWidth :
+ (NumCast(this.layoutDoc.nativeWidth) - Doc.NativeWidth(this.dataDoc)) * this.props.PanelWidth() / NumCast(this.layoutDoc.nativeWidth)
specificContextMenu = (e: React.MouseEvent): void => {
const funcs: ContextMenuProps[] = [];
@@ -247,38 +238,55 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
}
anchorMenuClick = () => this._sidebarRef.current?.anchorMenuClick;
+ @observable _showSidebar = false;
+ @computed get SidebarShown() { return this._showSidebar || this.layoutDoc._showSidebar ? true : false; }
+ contentScaling = () => {
+ return 1;
+ }
@computed get renderPdfView() {
TraceMobx();
+ const previewScale = this._previewNativeWidth ? 1 - this.sidebarWidth() / this._previewNativeWidth : 1;
+ const scale = previewScale * (this.props.scaling?.() || 1);
return <div className={"pdfBox"} onContextMenu={this.specificContextMenu}
style={{
height: this.props.Document._scrollTop && !this.Document._fitWidth && (window.screen.width > 600) ?
NumCast(this.Document._height) * this.props.PanelWidth() / NumCast(this.Document._width) : undefined
}}>
<div className="pdfBox-background" />
- <PDFViewer {...this.props}
- rootDoc={this.rootDoc}
- layoutDoc={this.layoutDoc}
- dataDoc={this.dataDoc}
- pdf={this._pdf!}
- url={this.pdfUrl!.url.pathname}
- isContentActive={this.isContentActive}
- anchorMenuClick={this.anchorMenuClick}
- loaded={!Doc.NativeAspect(this.dataDoc) ? this.loaded : undefined}
- setPdfViewer={this.setPdfViewer}
- addDocument={this.addDocument}
- moveDocument={this.moveDocument}
- removeDocument={this.removeDocument}
- whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
- startupLive={true}
- ContentScaling={this.props.scaling}
- sidebarWidth={this.sidebarWidth}
- />
+ <div style={{
+ width: `calc(${100 / scale}% - ${this.sidebarWidth() / scale * (this._previewWidth ? scale : 1)}px)`,
+ height: `${100 / scale}%`,
+ transform: `scale(${scale})`,
+ position: "absolute",
+ transformOrigin: "top left",
+ top: 0
+ }}>
+ <PDFViewer {...this.props}
+ rootDoc={this.rootDoc}
+ layoutDoc={this.layoutDoc}
+ dataDoc={this.dataDoc}
+ pdf={this._pdf!}
+ url={this.pdfUrl!.url.pathname}
+ isContentActive={this.isContentActive}
+ anchorMenuClick={this.anchorMenuClick}
+ loaded={!Doc.NativeAspect(this.dataDoc) ? this.loaded : undefined}
+ setPdfViewer={this.setPdfViewer}
+ addDocument={this.addDocument}
+ moveDocument={this.moveDocument}
+ removeDocument={this.removeDocument}
+ whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
+ startupLive={true}
+ ContentScaling={returnOne}
+ />
+ </div>
<SidebarAnnos ref={this._sidebarRef}
{...this.props}
rootDoc={this.rootDoc}
layoutDoc={this.layoutDoc}
dataDoc={this.dataDoc}
+ nativeWidth={this._previewNativeWidth ?? NumCast(this.layoutDoc._nativeWidth)}
+ showSidebar={this.SidebarShown}
whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
sidebarAddDocument={this.sidebarAddDocument}
moveDocument={this.moveDocument}
diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx
index 700f8a7d3..68ab3193b 100644
--- a/src/client/views/nodes/ScreenshotBox.tsx
+++ b/src/client/views/nodes/ScreenshotBox.tsx
@@ -1,11 +1,11 @@
import React = require("react");
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
// import { Canvas } from '@react-three/fiber';
-import { action, computed, observable, reaction } from "mobx";
+import { action, computed, observable, reaction, trace, runInAction } from "mobx";
import { observer } from "mobx-react";
// import { BufferAttribute, Camera, Vector2, Vector3 } from 'three';
import { DateField } from "../../../fields/DateField";
-import { Doc, WidthSym } from "../../../fields/Doc";
+import { Doc, WidthSym, HeightSym } from "../../../fields/Doc";
import { documentSchema } from "../../../fields/documentSchemas";
import { Id } from "../../../fields/FieldSymbols";
import { InkTool } from "../../../fields/InkField";
@@ -218,16 +218,15 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
// }
return (null);
}
- toggleRecording = action(async () => {
- this._screenCapture = !this._screenCapture;
- if (this._screenCapture) {
+ toggleRecording = async () => {
+ if (!this._screenCapture) {
this._audioRec = new MediaRecorder(await navigator.mediaDevices.getUserMedia({ audio: true }));
const aud_chunks: any = [];
this._audioRec.ondataavailable = (e: any) => aud_chunks.push(e.data);
this._audioRec.onstop = async (e: any) => {
const [{ result }] = await Networking.UploadFilesToServer(aud_chunks);
if (!(result instanceof Error)) {
- this.dataDoc[this.props.fieldKey + "-audio"] = new AudioField(Utils.prepend(result.accessPaths.agnostic.client));
+ this.dataDoc[this.props.fieldKey + "-audio"] = new AudioField(result.accessPaths.agnostic.client);
}
};
this._videoRef!.srcObject = await (navigator.mediaDevices as any).getDisplayMedia({ video: true });
@@ -244,23 +243,29 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
this.layoutDoc.layout = VideoBox.LayoutString(this.fieldKey);
this.dataDoc.nativeWidth = this.dataDoc.nativeHeight = undefined;
this.layoutDoc._fitWidth = undefined;
- this.dataDoc[this.props.fieldKey] = new VideoField(Utils.prepend(result.accessPaths.agnostic.client));
+ this.dataDoc[this.props.fieldKey] = new VideoField(result.accessPaths.agnostic.client);
} else alert("video conversion failed");
};
this._audioRec.start();
this._videoRec.start();
- this.dataDoc.mediaState = "recording";
+ runInAction(() => {
+ this._screenCapture = true;
+ this.dataDoc.mediaState = "recording";
+ });
DocUtils.ActiveRecordings.push(this);
} else {
this._audioRec?.stop();
this._videoRec?.stop();
- this.dataDoc.mediaState = "paused";
+ runInAction(() => {
+ this._screenCapture = false;
+ this.dataDoc.mediaState = "paused";
+ });
const ind = DocUtils.ActiveRecordings.indexOf(this);
ind !== -1 && (DocUtils.ActiveRecordings.splice(ind, 1));
CaptureManager.Instance.open(this.rootDoc);
}
- });
+ };
setupDictation = () => {
if (this.dataDoc[this.fieldKey + "-dictation"]) return;
@@ -275,7 +280,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
this.dataDoc[this.fieldKey + "-dictation"] = dictationText;
}
contentFunc = () => [this.threed, this.content];
- videoPanelHeight = () => NumCast(this.dataDoc[this.fieldKey + "-nativeHeight"], 1) / NumCast(this.dataDoc[this.fieldKey + "-nativeWidth"], 1) * this.props.PanelWidth();
+ videoPanelHeight = () => NumCast(this.dataDoc[this.fieldKey + "-nativeHeight"], this.layoutDoc[HeightSym]()) / NumCast(this.dataDoc[this.fieldKey + "-nativeWidth"], this.layoutDoc[WidthSym]()) * this.props.PanelWidth();
formattedPanelHeight = () => Math.max(0, this.props.PanelHeight() - this.videoPanelHeight());
render() {
TraceMobx();
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index fc08a2302..ce45c01e6 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -75,10 +75,6 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
return CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, "_timecodeToShow"/* videoStart */, "_timecodeToHide" /* videoEnd */, timecode ? timecode : undefined) || this.rootDoc;
}
- choosePath(url: string) {
- return url.indexOf(window.location.origin) === -1 ? Utils.CorsProxy(url) : url;
- }
-
videoLoad = () => {
const aspect = this.player!.videoWidth / this.player!.videoHeight;
Doc.SetNativeWidth(this.dataDoc, this.player!.videoWidth);
@@ -182,8 +178,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
}
}
- private createRealSummaryLink = (relative: string, downX?: number, downY?: number) => {
- const url = this.choosePath(Utils.prepend(relative));
+ private createRealSummaryLink = (imagePath: string, downX?: number, downY?: number) => {
+ const url = !imagePath.startsWith("/") ? Utils.CorsProxy(imagePath) : imagePath;
const width = this.layoutDoc._width || 1;
const height = this.layoutDoc._height || 0;
const imageSummary = Docs.Create.ImageDocument(url, {
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index f5b1f96f2..f3a4a46de 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -43,6 +43,7 @@ const WebDocument = makeInterface(documentSchema);
@observer
export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps, WebDocument>(WebDocument) {
public static LayoutString(fieldKey: string) { return FieldView.LayoutString(WebBox, fieldKey); }
+ public static openSidebarWidth = 250;
private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean) => void);
private _mainCont: React.RefObject<HTMLDivElement> = React.createRef();
private _outerRef: React.RefObject<HTMLDivElement> = React.createRef();
@@ -51,6 +52,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
private _keyInput = React.createRef<HTMLInputElement>();
private _initialScroll: Opt<number>;
private _sidebarRef = React.createRef<SidebarAnnos>();
+ @observable private _urlHash: string = "";
@observable private _scrollTimer: any;
@observable private _overlayAnnoInfo: Opt<Doc>;
@observable private _marqueeing: number[] | undefined;
@@ -67,7 +69,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
constructor(props: any) {
super(props);
- if (this.webField) {
+ if (true) {// his.webField) {
Doc.SetNativeWidth(this.dataDoc, Doc.NativeWidth(this.dataDoc) || 850);
Doc.SetNativeHeight(this.dataDoc, Doc.NativeHeight(this.dataDoc) || this.Document[HeightSym]() / this.Document[WidthSym]() * 850);
}
@@ -81,9 +83,11 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
runInAction(() => {
this._url = this.webField?.toString() || "";
- this._annotationKey = "annotations-" + WebBox.urlHash(this._url);
+ this._urlHash = WebBox.urlHash(this._url) + "";
+ this._annotationKey = this._urlHash + "-annotations";
// bcz: need to make sure that doc.data-annotations points to the currently active web page's annotations (this could/should be when the doc is created)
- this.dataDoc[this.fieldKey + "-annotations"] = ComputedField.MakeFunction(`copyField(this["${this.fieldKey}-annotations-"+urlHash(this["${this.fieldKey}"]?.url?.toString()))`);
+ this.dataDoc[this.fieldKey + "-annotations"] = ComputedField.MakeFunction(`copyField(this["${this.fieldKey}-"+urlHash(this["${this.fieldKey}"]?.url?.toString())+"-annotations"`);
+ this.dataDoc[this.fieldKey + "-sidebar"] = ComputedField.MakeFunction(`copyField(this["${this.fieldKey}-"+urlHash(this["${this.fieldKey}"]?.url?.toString())+"-sidebar"`);
});
this._disposers.selection = reaction(() => this.props.isSelected(),
@@ -160,6 +164,10 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
menuControls = () => this.urlEditor; // controls to be added to the top bar when a document of this type is selected
scrollFocus = (doc: Doc, smooth: boolean) => {
+ if (StrCast(doc.webUrl) !== this._url) this.submitURL(StrCast(doc.webUrl), !smooth);
+ if (DocListCast(this.props.Document[this.fieldKey + "-sidebar"]).includes(doc) && !this.SidebarShown) {
+ this.toggleSidebar(!smooth);
+ }
if (this._sidebarRef?.current?.makeDocUnfiltered(doc)) return 1;
if (doc !== this.rootDoc && this._outerRef.current) {
const windowHeight = this.props.PanelHeight() / (this.props.scaling?.() || 1);
@@ -178,12 +186,12 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
getAnchor = () => {
const anchor =
AnchorMenu.Instance?.GetAnchor(this._savedAnnotations) ??
- Docs.Create.TextanchorDocument({
+ Docs.Create.WebanchorDocument(this._url, {
title: StrCast(this.rootDoc.title + " " + this.layoutDoc._scrollTop),
annotationOn: this.rootDoc,
- y: NumCast(this.layoutDoc._scrollTop),
+ y: NumCast(this.layoutDoc._scrollTop)
});
- this.addDocument(anchor);
+ this.addDocumentWrapper(anchor);
return anchor;
}
@@ -295,9 +303,10 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
const future = Cast(this.dataDoc[this.fieldKey + "-future"], listSpec("string"), []);
const history = Cast(this.dataDoc[this.fieldKey + "-history"], listSpec("string"), []);
if (future.length) {
- history.push(this._url);
+ this.dataDoc[this.fieldKey + "-history"] = new List<string>([...history, this._url]);
this.dataDoc[this.fieldKey] = new WebField(new URL(this._url = future.pop()!));
- this._annotationKey = "annotations-" + WebBox.urlHash(this._url);
+ this._urlHash = WebBox.urlHash(this._url) + "";
+ this._annotationKey = this._urlHash + "-annotations";
return true;
}
return false;
@@ -309,9 +318,10 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
const history = Cast(this.dataDoc[this.fieldKey + "-history"], listSpec("string"), []);
if (history.length) {
if (future === undefined) this.dataDoc[this.fieldKey + "-future"] = new List<string>([this._url]);
- else future.push(this._url);
+ else this.dataDoc[this.fieldKey + "-future"] = new List<string>([...future, this._url]);
this.dataDoc[this.fieldKey] = new WebField(new URL(this._url = history.pop()!));
- this._annotationKey = "annotations-" + WebBox.urlHash(this._url);
+ this._urlHash = WebBox.urlHash(this._url) + "";
+ this._annotationKey = this._urlHash + "-annotations";
return true;
}
return false;
@@ -322,25 +332,26 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
}
@action
- submitURL = (newUrl?: string) => {
+ submitURL = (newUrl?: string, preview?: boolean) => {
if (!newUrl) return;
if (!newUrl.startsWith("http")) newUrl = "http://" + newUrl;
try {
const future = Cast(this.dataDoc[this.fieldKey + "-future"], listSpec("string"));
const history = Cast(this.dataDoc[this.fieldKey + "-history"], listSpec("string"));
const url = this.webField?.toString();
- if (url) {
+ if (url && !preview) {
if (history === undefined) {
this.dataDoc[this.fieldKey + "-history"] = new List<string>([url]);
} else {
- history.push(url);
+ this.dataDoc[this.fieldKey + "-history"] = new List<string>([...history, url]);
}
this.layoutDoc._scrollTop = 0;
future && (future.length = 0);
}
this._url = newUrl;
- this._annotationKey = "annotations-" + WebBox.urlHash(this._url);
- this.dataDoc[this.fieldKey] = new WebField(new URL(newUrl));
+ this._urlHash = WebBox.urlHash(this._url) + "";
+ this._annotationKey = this._urlHash + "-annotations";
+ if (!preview) this.dataDoc[this.fieldKey] = new WebField(new URL(newUrl));
} catch (e) {
console.log("WebBox URL error:" + this._url);
}
@@ -418,7 +429,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
if (field instanceof HtmlField) {
view = <span className="webBox-htmlSpan" dangerouslySetInnerHTML={{ __html: field.html }} />;
} else if (field instanceof WebField) {
- const url = this.layoutDoc.useCors ? Utils.CorsProxy(field.url.href) : field.url.href;
+ const url = this.layoutDoc.useCors ? Utils.CorsProxy(this._url) : this._url;
view = <iframe className="webBox-iframe" enable-annotation={"true"}
style={{ pointerEvents: this._scrollTimer ? "none" : undefined }}
ref={action((r: HTMLIFrameElement | null) => this._iframe = r)} src={url} onLoad={this.iframeLoaded}
@@ -433,9 +444,14 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
return view;
}
+ addDocumentWrapper = (doc: Doc | Doc[], annotationKey?: string) => {
+ (doc instanceof Doc ? [doc] : doc).forEach(doc => doc.webUrl = this._url);
+ return this.addDocument(doc, annotationKey);
+ }
+
sidebarAddDocument = (doc: Doc | Doc[], sidebarKey?: string) => {
if (!this.layoutDoc._showSidebar) this.toggleSidebar();
- return this.addDocument(doc, sidebarKey);
+ return this.addDocumentWrapper(doc, sidebarKey);
}
sidebarBtnDown = (e: React.PointerEvent) => {
setupMoveUpEvents(this, e, (e, down, delta) => {
@@ -449,17 +465,29 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
this.layoutDoc._showSidebar = nativeWidth !== this.layoutDoc._nativeWidth;
}
return false;
- }, emptyFunction, this.toggleSidebar);
+ }, emptyFunction, () => this.toggleSidebar());
}
- toggleSidebar = action(() => {
+ @observable _previewNativeWidth: Opt<number> = undefined;
+ @observable _previewWidth: Opt<number> = undefined;
+ toggleSidebar = action((preview: boolean = false) => {
const nativeWidth = NumCast(this.layoutDoc[this.fieldKey + "-nativeWidth"]);
- const ratio = ((!this.layoutDoc.nativeWidth || this.layoutDoc.nativeWidth === nativeWidth ? 250 : 0) + nativeWidth) / nativeWidth;
+ const ratio = ((!this.layoutDoc.nativeWidth || this.layoutDoc.nativeWidth === nativeWidth ? WebBox.openSidebarWidth : 0) + nativeWidth) / nativeWidth;
const curNativeWidth = NumCast(this.layoutDoc.nativeWidth, nativeWidth);
- this.layoutDoc.nativeWidth = nativeWidth * ratio;
- this.layoutDoc._width = this.layoutDoc[WidthSym]() * nativeWidth * ratio / curNativeWidth;
- this.layoutDoc._showSidebar = nativeWidth !== this.layoutDoc._nativeWidth;
+ if (preview) {
+ this._previewNativeWidth = nativeWidth * ratio;
+ this._previewWidth = this.layoutDoc[WidthSym]() * nativeWidth * ratio / curNativeWidth;
+ this._showSidebar = true;
+ }
+ else {
+ this.layoutDoc.nativeWidth = nativeWidth * ratio;
+ this.layoutDoc._width = this.layoutDoc[WidthSym]() * nativeWidth * ratio / curNativeWidth;
+ this.layoutDoc._showSidebar = nativeWidth !== this.layoutDoc._nativeWidth;
+ }
});
- sidebarWidth = () => !this.layoutDoc._showSidebar ? 0 : (NumCast(this.layoutDoc.nativeWidth) - Doc.NativeWidth(this.dataDoc)) * this.props.PanelWidth() / NumCast(this.layoutDoc.nativeWidth);
+ sidebarWidth = () => !this.SidebarShown ? 0 :
+ this._previewWidth ? WebBox.openSidebarWidth :
+ (NumCast(this.layoutDoc.nativeWidth) - Doc.NativeWidth(this.dataDoc)) * this.props.PanelWidth() /
+ NumCast(this.layoutDoc.nativeWidth)
@computed get content() {
return <div className={"webBox-cont" + (!this.props.docViewPath().lastElement()?.docView?._pendingDoubleClick && this.isContentActive() && CurrentUserUtils.SelectedTool === InkTool.None && !DocumentDecorations.Instance?.Interacting ? "-interactive" : "")}
@@ -475,7 +503,10 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
<Annotation {...this.props} fieldKey={this.annotationKey} showInfo={this.showInfo} dataDoc={this.dataDoc} anno={anno} key={`${anno[Id]}-annotation`} />)
}
</div>;
+
}
+ @observable _showSidebar = false;
+ @computed get SidebarShown() { return this._showSidebar || this.layoutDoc._showSidebar ? true : false; }
showInfo = action((anno: Opt<Doc>) => this._overlayAnnoInfo = anno);
setPreviewCursor = (func?: (x: number, y: number, drag: boolean) => void) => this._setPreviewCursor = func;
@@ -484,29 +515,25 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
scrollXf = () => this.props.ScreenToLocalTransform().translate(0, NumCast(this.layoutDoc._scrollTop));
anchorMenuClick = () => this._sidebarRef.current?.anchorMenuClick;
render() {
- const inactiveLayer = this.props.layerProvider?.(this.layoutDoc) === false;
- const scale = this.props.scaling?.() || 1;
+ const pointerEvents = this.props.layerProvider?.(this.layoutDoc) === false ? "none" : undefined;
+ const previewScale = this._previewNativeWidth ? 1 - this.sidebarWidth() / this._previewNativeWidth : 1;
+ const scale = previewScale * (this.props.scaling?.() || 1);
return (
<div className="webBox" ref={this._mainCont} style={{ pointerEvents: this.isContentActive() ? "all" : this.isContentActive() || SnappingManager.GetIsDragging() ? undefined : "none" }} >
- <div className={`webBox-container`}
- style={{ pointerEvents: inactiveLayer ? "none" : undefined }}
- onContextMenu={this.specificContextMenu}>
+ <div className={`webBox-container`} style={{ pointerEvents }} onContextMenu={this.specificContextMenu}>
<base target="_blank" />
<div className={"webBox-outerContent"} ref={this._outerRef}
style={{
- width: `calc(${100 / scale}% - ${this.sidebarWidth() / scale}px)`,
+ width: `calc(${100 / scale}% - ${this.sidebarWidth() / scale * (this._previewWidth ? scale : 1)}px)`,
height: `${100 / scale}%`,
transform: `scale(${scale})`,
- pointerEvents: inactiveLayer ? "none" : undefined
+ pointerEvents
}}
onWheel={e => { e.stopPropagation(); e.preventDefault(); }} // block wheel events from propagating since they're handled by the iframe
onScroll={e => this.setDashScrollTop(this._outerRef.current?.scrollTop || 0)}
onPointerDown={this.onMarqueeDown}
>
- <div className={"webBox-innerContent"} style={{
- height: NumCast(this.scrollHeight, 50),
- pointerEvents: inactiveLayer ? "none" : undefined
- }}>
+ <div className={"webBox-innerContent"} style={{ height: NumCast(this.scrollHeight, 50), pointerEvents }}>
{this.content}
<CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit}
renderDepth={this.props.renderDepth + 1}
@@ -539,7 +566,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
anchorMenuClick={this.anchorMenuClick}
scrollTop={0}
down={this._marqueeing} scaling={returnOne}
- addDocument={this.addDocument}
+ addDocument={this.addDocumentWrapper}
docView={this.props.docViewPath().lastElement()}
finishMarquee={this.finishMarquee}
savedAnnotations={this._savedAnnotations}
@@ -548,10 +575,12 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
</div >
<SidebarAnnos ref={this._sidebarRef}
{...this.props}
- fieldKey={this.annotationKey}
+ fieldKey={this.fieldKey + "-" + this._urlHash}
rootDoc={this.rootDoc}
layoutDoc={this.layoutDoc}
dataDoc={this.dataDoc}
+ nativeWidth={this._previewNativeWidth ?? NumCast(this.layoutDoc._nativeWidth)}
+ showSidebar={this.SidebarShown}
sidebarAddDocument={this.sidebarAddDocument}
moveDocument={this.moveDocument}
removeDocument={this.removeDocument}
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 140d39929..1058070f8 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -11,7 +11,7 @@ import { ReplaceStep } from 'prosemirror-transform';
import { EditorState, NodeSelection, Plugin, TextSelection, Transaction } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
import { DateField } from '../../../../fields/DateField';
-import { AclAdmin, AclEdit, DataSym, Doc, DocListCast, DocListCastAsync, Field, ForceServerWrite, HeightSym, Opt, UpdatingFromServer, WidthSym } from "../../../../fields/Doc";
+import { AclAdmin, AclEdit, AclSelfEdit, DataSym, Doc, DocListCast, DocListCastAsync, Field, ForceServerWrite, HeightSym, Opt, UpdatingFromServer, WidthSym, AclAugment } from "../../../../fields/Doc";
import { documentSchema } from '../../../../fields/documentSchemas';
import { Id } from '../../../../fields/FieldSymbols';
import { InkTool } from '../../../../fields/InkField';
@@ -120,7 +120,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
public get EditorView() { return this._editorView; }
public get SidebarKey() { return this.fieldKey + "-sidebar"; }
- @computed get sidebarWidthPercent() { return StrCast(this.layoutDoc._sidebarWidthPercent, "0%"); }
+ @computed get sidebarWidthPercent() { return this._showSidebar ? "20%" : StrCast(this.layoutDoc._sidebarWidthPercent, "0%"); }
@computed get sidebarColor() { return StrCast(this.layoutDoc.sidebarColor, StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], "#e4e4e4")); }
@computed get autoHeight() { return this.layoutDoc._autoHeight && !this.props.ignoreAutoHeight; }
@computed get textHeight() { return NumCast(this.rootDoc[this.fieldKey + "-height"]); }
@@ -215,7 +215,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
AnchorMenu.Instance.Status = "marquee";
AnchorMenu.Instance.Highlight = action((color: string, isLinkButton: boolean) => {
this._editorView?.state && RichTextMenu.Instance.insertHighlight(color, this._editorView.state, this._editorView?.dispatch);
- console.log("highlight")
return undefined;
});
/**
@@ -254,7 +253,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
const removeSelection = (json: string | undefined) => json?.indexOf("\"storedMarks\"") === -1 ?
json?.replace(/"selection":.*/, "") : json?.replace(/"selection":"\"storedMarks\""/, "\"storedMarks\"");
- if (effectiveAcl === AclEdit || effectiveAcl === AclAdmin) {
+ if (effectiveAcl === AclEdit || effectiveAcl === AclAdmin || effectiveAcl === AclSelfEdit) {
const accumTags = [] as string[];
state.tr.doc.nodesBetween(0, state.doc.content.size, (node: any, pos: number, parent: any) => {
if (node.type === schema.nodes.dashField && node.attrs.fieldKey.startsWith("#")) {
@@ -371,7 +370,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
this._searchIndex = ++this._searchIndex > flattened.length - 1 ? 0 : this._searchIndex;
const anchor = Docs.Create.TextanchorDocument();
const alink = DocUtils.MakeLink({ doc: anchor }, { doc: target }, "automatic")!;
- const allAnchors = [{ href: Utils.prepend("/doc/" + anchor[Id]), title: "a link", anchorId: anchor[Id] }];
+ const allAnchors = [{ href: Doc.localServerPath(anchor), title: "a link", anchorId: anchor[Id] }];
const link = this._editorView!.state.schema.marks.linkAnchor.create({ allAnchors, title: "auto link", location });
tr = tr.addMark(flattened[i].from, flattened[i].to, link);
});
@@ -432,6 +431,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
this.ProseRef = ele;
this._dropDisposer?.();
ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.layoutDoc));
+ // if (this.autoHeight) this.tryUpdateScrollHeight();
}
@undoBatch
@@ -544,11 +544,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
}
}
+ @observable _showSidebar = false;
+ @computed get SidebarShown() { return this._showSidebar || this.layoutDoc._showSidebar ? true : false; }
+
@action
- toggleSidebar = () => {
+ toggleSidebar = (preview: boolean = false) => {
const prevWidth = this.sidebarWidth();
- this.layoutDoc._showSidebar = ((this.layoutDoc._sidebarWidthPercent = StrCast(this.layoutDoc._sidebarWidthPercent, "0%") === "0%" ? "50%" : "0%")) !== "0%";
- this.layoutDoc._width = this.layoutDoc._showSidebar ? NumCast(this.layoutDoc._width) * 2 : Math.max(20, NumCast(this.layoutDoc._width) - prevWidth);
+ if (preview) this._showSidebar = true;
+ else this.layoutDoc._showSidebar = ((this.layoutDoc._sidebarWidthPercent = StrCast(this.layoutDoc._sidebarWidthPercent, "0%") === "0%" ? "50%" : "0%")) !== "0%";
+
+ this.layoutDoc._width = !preview && this.SidebarShown ? NumCast(this.layoutDoc._width) * 2 : Math.max(20, NumCast(this.layoutDoc._width) - prevWidth);
}
sidebarDown = (e: React.PointerEvent) => {
setupMoveUpEvents(this, e, this.sidebarMove, emptyFunction, () => setTimeout(this.toggleSidebar), false);
@@ -705,7 +710,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
let tr = state.tr.addMark(sel.from, sel.to, splitter);
if (sel.from !== sel.to) {
const anchor = anchorDoc ?? Docs.Create.TextanchorDocument({ title: this._editorView?.state.doc.textBetween(sel.from, sel.to) });
- const href = targetHref ?? Utils.prepend("/doc/" + anchor[Id]);
+ const href = targetHref ?? Doc.localServerPath(anchor);
if (anchor !== anchorDoc) this.addDocument(anchor);
tr.doc.nodesBetween(sel.from, sel.to, (node: any, pos: number, parent: any) => {
if (node.firstChild === null && node.marks.find((m: Mark) => m.type.name === schema.marks.splitter.name)) {
@@ -726,6 +731,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
}
scrollFocus = (textAnchor: Doc, smooth: boolean) => {
+ if (DocListCast(this.Document[this.fieldKey + "-sidebar"]).includes(textAnchor) && !this.SidebarShown) {
+ this.toggleSidebar(!smooth);
+ }
const textAnchorId = textAnchor[Id];
const findAnchorFrag = (frag: Fragment, editor: EditorView) => {
const nodes: Node[] = [];
@@ -1042,7 +1050,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
}
const marks = [...node.marks];
const linkIndex = marks.findIndex(mark => mark.type.name === "link");
- const allLinks = [{ href: Utils.prepend(`/doc/${linkId}`), title, linkId }];
+ const allLinks = [{ href: Doc.globalServerPath(linkId), title, linkId }];
const link = view.state.schema.mark(view.state.schema.marks.linkAnchor, { allLinks, location: "add:right", title, docref: true });
marks.splice(linkIndex === -1 ? 0 : linkIndex, 1, link);
return node.mark(marks);
@@ -1394,6 +1402,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
this._rules!.EnteringStyle = false;
}
e.stopPropagation();
+ for (var i = state.selection.from; i < state.selection.to; i++) {
+ const node = state.doc.resolve(i);
+ if (node?.marks?.().some(mark => mark.type === schema.marks.user_mark &&
+ mark.attrs.userid !== Doc.CurrentUserEmail) &&
+ [AclAugment, AclSelfEdit].includes(GetEffectiveAcl(this.rootDoc))) {
+ e.preventDefault();
+ }
+ }
switch (e.key) {
case "Escape":
this._editorView!.dispatch(state.tr.setSelection(TextSelection.create(state.doc, state.selection.from, state.selection.from)));
@@ -1423,16 +1439,24 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
}
}
}
- tryUpdateScrollHeight() {
+ tryUpdateScrollHeight = () => {
if (!LightboxView.LightboxDoc || LightboxView.IsLightboxDocView(this.props.docViewPath())) {
const margins = 2 * NumCast(this.layoutDoc._yMargin, this.props.yPadding || 0);
- const proseHeight = !this.ProseRef ? 0 : Array.from(this.ProseRef.children[0].children).reduce((p, child) => p + Number(getComputedStyle(child).height.replace("px", "")), margins);
- const scrollHeight = this.ProseRef && Math.min(NumCast(this.layoutDoc.docMaxAutoHeight, proseHeight), proseHeight);
- if (scrollHeight && this.props.renderDepth && !this.props.dontRegisterView) { // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation
- const setScrollHeight = () => this.rootDoc[this.fieldKey + "-scrollHeight"] = scrollHeight;
- if (this.rootDoc === this.layoutDoc.doc || this.layoutDoc.resolvedDataDoc) {
- setScrollHeight();
- } else setTimeout(setScrollHeight, 10); // if we have a template that hasn't been resolved yet, we can't set the height or we'd be setting it on the unresolved template. So set a timeout and hope its arrived...
+ const children = this.ProseRef?.children.length ? Array.from(this.ProseRef.children[0].children) : undefined;
+ if (children) {
+ var proseHeight = !this.ProseRef ? 0 : children.reduce((p, child) => p + Number(getComputedStyle(child).height.replace("px", "")), margins);
+ var scrollHeight = this.ProseRef && Math.min(NumCast(this.layoutDoc.docMaxAutoHeight, proseHeight), proseHeight);
+ if (scrollHeight && this.props.renderDepth && !this.props.dontRegisterView) { // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation
+ const setScrollHeight = () => this.rootDoc[this.fieldKey + "-scrollHeight"] = scrollHeight;
+ if (this.rootDoc === this.layoutDoc.doc || this.layoutDoc.resolvedDataDoc) {
+ setScrollHeight();
+ setTimeout(() => {
+ proseHeight = !this.ProseRef ? 0 : children.reduce((p, child) => p + Number(getComputedStyle(child).height.replace("px", "")), margins);
+ scrollHeight = this.ProseRef && Math.min(NumCast(this.layoutDoc.docMaxAutoHeight, proseHeight), proseHeight);
+ setScrollHeight();
+ }, 10);
+ } else setTimeout(setScrollHeight, 10); // if we have a template that hasn't been resolved yet, we can't set the height or we'd be setting it on the unresolved template. So set a timeout and hope its arrived...
+ }
}
}
}
@@ -1468,10 +1492,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
return ComponentTag === CollectionStackingView ?
<SidebarAnnos ref={this._sidebarRef}
{...this.props}
- fieldKey={this.annotationKey}
+ fieldKey={this.fieldKey}
rootDoc={this.rootDoc}
layoutDoc={this.layoutDoc}
dataDoc={this.dataDoc}
+ nativeWidth={NumCast(this.layoutDoc._nativeWidth)}
+ showSidebar={this.SidebarShown}
PanelWidth={this.sidebarWidth}
setHeight={this.setSidebarHeight}
sidebarAddDocument={this.sidebarAddDocument}
@@ -1569,7 +1595,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
}}
/>
</div>
- {(this.props.noSidebar || this.Document._noSidebar) || this.props.dontSelectOnLoad || !this.layoutDoc._showSidebar || this.sidebarWidthPercent === "0%" ? (null) : this.sidebarCollection}
+ {(this.props.noSidebar || this.Document._noSidebar) || this.props.dontSelectOnLoad || !this.SidebarShown || this.sidebarWidthPercent === "0%" ? (null) : this.sidebarCollection}
{(this.props.noSidebar || this.Document._noSidebar) || this.props.dontSelectOnLoad || this.Document._singleLine ? (null) : this.sidebarHandle}
{!this.layoutDoc._showAudio ? (null) : this.audioHandle}
</div>
diff --git a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
index d5c77786c..1f78b2204 100644
--- a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
+++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
@@ -7,13 +7,14 @@ import { splitListItem, wrapInList, } from "prosemirror-schema-list";
import { EditorState, Transaction, TextSelection } from "prosemirror-state";
import { SelectionManager } from "../../../util/SelectionManager";
import { NumCast, BoolCast, Cast, StrCast } from "../../../../fields/Types";
-import { Doc, DataSym, DocListCast } from "../../../../fields/Doc";
+import { Doc, DataSym, DocListCast, AclAugment } from "../../../../fields/Doc";
import { FormattedTextBox } from "./FormattedTextBox";
import { Id } from "../../../../fields/FieldSymbols";
import { Docs } from "../../../documents/Documents";
import { Utils } from "../../../../Utils";
import { listSpec } from "../../../../fields/Schema";
import { List } from "../../../../fields/List";
+import { GetEffectiveAcl } from "../../../../fields/util";
const mac = typeof navigator !== "undefined" ? /Mac/.test(navigator.platform) : false;
@@ -70,25 +71,39 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
return false;
};
+ const canEdit = (state: any) => {
+ for (var i = state.selection.from; i < state.selection.to; i++) {
+ const node = state.doc.resolve(i);
+ if (node?.marks?.().some((mark: any) => mark.type === schema.marks.user_mark &&
+ mark.attrs.userid !== Doc.CurrentUserEmail) &&
+ GetEffectiveAcl(props.Document) === AclAugment) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ const toggleEditableMark = (mark: any) => (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => canEdit(state) && toggleMark(mark)(state, dispatch);
+
//History commands
bind("Mod-z", undo);
bind("Shift-Mod-z", redo);
!mac && bind("Mod-y", redo);
//Commands to modify Mark
- bind("Mod-b", toggleMark(schema.marks.strong));
- bind("Mod-B", toggleMark(schema.marks.strong));
+ bind("Mod-b", toggleEditableMark(schema.marks.strong));
+ bind("Mod-B", toggleEditableMark(schema.marks.strong));
- bind("Mod-e", toggleMark(schema.marks.em));
- bind("Mod-E", toggleMark(schema.marks.em));
+ bind("Mod-e", toggleEditableMark(schema.marks.em));
+ bind("Mod-E", toggleEditableMark(schema.marks.em));
- bind("Mod-*", toggleMark(schema.marks.code));
+ bind("Mod-*", toggleEditableMark(schema.marks.code));
- bind("Mod-u", toggleMark(schema.marks.underline));
- bind("Mod-U", toggleMark(schema.marks.underline));
+ bind("Mod-u", toggleEditableMark(schema.marks.underline));
+ bind("Mod-U", toggleEditableMark(schema.marks.underline));
//Commands for lists
- bind("Ctrl-i", wrapInList(schema.nodes.ordered_list));
+ bind("Ctrl-i", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => canEdit(state) && wrapInList(schema.nodes.ordered_list)(state, dispatch as any));
bind("Tab", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
/// bcz; Argh!! replace layotuTEmpalteString with a onTab prop conditionally handles Tab);
@@ -96,6 +111,7 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
if (!props.LayoutTemplateString) return addTextBox(false, true);
return true;
}
+ if (!canEdit(state)) return true;
const ref = state.selection;
const range = ref.$from.blockRange(ref.$to);
const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
@@ -121,6 +137,7 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
bind("Shift-Tab", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
/// bcz; Argh!! replace with a onShiftTab prop conditionally handles Tab);
if (props.Document._singleLine) return true;
+ if (!canEdit(state)) return true;
const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
if (!liftListItem(schema.nodes.list_item)(state.tr, (tx2: Transaction) => {
@@ -140,24 +157,19 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
});
//Commands to modify BlockType
- bind("Ctrl->", wrapIn(schema.nodes.blockquote));
- bind("Alt-\\", setBlockType(schema.nodes.paragraph));
- bind("Shift-Ctrl-\\", setBlockType(schema.nodes.code_block));
+ bind("Ctrl->", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => canEdit((state) && wrapIn(schema.nodes.blockquote)(state, dispatch as any)));
+ bind("Alt-\\", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => canEdit(state) && setBlockType(schema.nodes.paragraph)(state, dispatch as any));
+ bind("Shift-Ctrl-\\", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => canEdit(state) && setBlockType(schema.nodes.code_block)(state, dispatch as any));
- bind("Ctrl-m", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
- dispatch(state.tr.replaceSelectionWith(schema.nodes.equation.create({ fieldKey: "math" + Utils.GenerateGuid() })));
- });
+ bind("Ctrl-m", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => canEdit(state) && dispatch(state.tr.replaceSelectionWith(schema.nodes.equation.create({ fieldKey: "math" + Utils.GenerateGuid() }))));
for (let i = 1; i <= 6; i++) {
- bind("Shift-Ctrl-" + i, setBlockType(schema.nodes.heading, { level: i }));
+ bind("Shift-Ctrl-" + i, (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => canEdit(state) && setBlockType(schema.nodes.heading, { level: i })(state, dispatch as any));
}
//Command to create a horizontal break line
const hr = schema.nodes.horizontal_rule;
- bind("Mod-_", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
- dispatch(state.tr.replaceSelectionWith(hr.create()).scrollIntoView());
- return true;
- });
+ bind("Mod-_", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => canEdit(state) && dispatch(state.tr.replaceSelectionWith(hr.create()).scrollIntoView()));
//Command to unselect all
bind("Escape", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
@@ -173,13 +185,15 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
};
//Command to create a text document to the right of the selected textbox
- bind("Alt-Enter", (state: EditorState<S>, dispatch: (tx: Transaction<Schema<any, any>>) => void) => addTextBox(false, true));
+ bind("Alt-Enter", () => addTextBox(false, true));
//Command to create a text document to the bottom of the selected textbox
- bind("Ctrl-Enter", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => addTextBox(true, true));
+ bind("Ctrl-Enter", () => addTextBox(true, true));
// backspace = chainCommands(deleteSelection, joinBackward, selectNodeBackward);
bind("Backspace", (state: EditorState<S>, dispatch: (tx: Transaction<Schema<any, any>>) => void) => {
+ if (!canEdit(state)) return true;
+
if (!deleteSelection(state, (tx: Transaction<Schema<any, any>>) => {
dispatch(updateBullets(tx, schema));
})) {
@@ -200,6 +214,9 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
//command to break line
bind("Enter", (state: EditorState<S>, dispatch: (tx: Transaction<Schema<any, any>>) => void) => {
if (addTextBox(true, false)) return true;
+
+ if (!canEdit(state)) return true;
+
const trange = state.selection.$from.blockRange(state.selection.$to);
const path = (state.selection.$from as any).path;
const depth = trange ? liftTarget(trange) : undefined;
@@ -238,18 +255,19 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
//Command to create a blank space
bind("Space", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
+ if (!canEdit(state)) return true;
const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
dispatch(splitMetadata(marks, state.tr));
return false;
});
- bind("Alt-ArrowUp", joinUp);
- bind("Alt-ArrowDown", joinDown);
- bind("Mod-BracketLeft", lift);
+ bind("Alt-ArrowUp", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => canEdit(state) && joinUp(state, dispatch as any));
+ bind("Alt-ArrowDown", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => canEdit(state) && joinDown(state, dispatch as any));
+ bind("Mod-BracketLeft", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => canEdit(state) && lift(state, dispatch as any));
const cmd = chainCommands(exitCode, (state, dispatch) => {
if (dispatch) {
- dispatch(state.tr.replaceSelectionWith(schema.nodes.hard_break.create()).scrollIntoView());
+ canEdit(state) && dispatch(state.tr.replaceSelectionWith(schema.nodes.hard_break.create()).scrollIntoView());
return true;
}
return false;
diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx
index 2523dda38..82ad2b7db 100644
--- a/src/client/views/nodes/formattedText/RichTextMenu.tsx
+++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx
@@ -821,8 +821,8 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
if (link) {
const href = link.attrs.allAnchors.length > 0 ? link.attrs.allAnchors[0].href : undefined;
if (href) {
- if (href.indexOf(Utils.prepend("/doc/")) === 0) {
- const linkclicked = href.replace(Utils.prepend("/doc/"), "").split("?")[0];
+ if (href.indexOf(Doc.localServerPath()) === 0) {
+ const linkclicked = href.replace(Doc.localServerPath(), "").split("?")[0];
if (linkclicked) {
const linkDoc = await DocServer.GetRefField(linkclicked);
if (linkDoc instanceof Doc) {
@@ -864,8 +864,8 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
const allAnchors = linkAnchor.attrs.allAnchors.slice();
this.TextView.RemoveAnchorFromSelection(allAnchors);
// bcz: Argh ... this will remove the link from the document even it's anchored somewhere else in the text which happens if only part of the anchor text was selected.
- allAnchors.filter((aref: any) => aref?.href.indexOf(Utils.prepend("/doc/")) === 0).forEach((aref: any) => {
- const anchorId = aref.href.replace(Utils.prepend("/doc/"), "").split("?")[0];
+ allAnchors.filter((aref: any) => aref?.href.indexOf(Doc.localServerPath()) === 0).forEach((aref: any) => {
+ const anchorId = aref.href.replace(Doc.localServerPath(), "").split("?")[0];
anchorId && DocServer.GetRefField(anchorId).then(linkDoc => LinkManager.Instance.deleteLink(linkDoc as Doc));
});
}
diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx
index 70ca19842..55816ed52 100644
--- a/src/client/views/pdf/AnchorMenu.tsx
+++ b/src/client/views/pdf/AnchorMenu.tsx
@@ -85,7 +85,6 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
@action
toggleLinkPopup = (e: React.MouseEvent) => {
//ignore the potential null type error because this method cannot be called unless the user selects text and clicks the link button
- console.log(window.getSelection().toString())
//change popup visibility field to visible
this._showLinkPopup = !this._showLinkPopup;
}
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index e8c7a4ab0..e7911e8f8 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -46,7 +46,6 @@ interface IViewerProps extends FieldViewProps {
loaded?: (nw: number, nh: number, np: number) => void;
setPdfViewer: (view: PDFViewer) => void;
ContentScaling?: () => number;
- sidebarWidth: () => number;
anchorMenuClick?: () => undefined | ((anchor: Doc) => void);
}
@@ -550,7 +549,6 @@ export class PDFViewer extends React.Component<IViewerProps> {
onScroll={this.onScroll} onWheel={this.onZoomWheel} onPointerDown={this.onPointerDown} onClick={this.onClick}
style={{
overflowX: this._zoomed !== 1 ? "scroll" : undefined,
- width: !this.props.Document._fitWidth && (window.screen.width > 600) ? Doc.NativeWidth(this.props.Document) - this.props.sidebarWidth() / this.contentScaling : `calc(${100 / this.contentScaling}% - ${this.props.sidebarWidth() / this.contentScaling}px)`,
height: !this.props.Document._fitWidth && (window.screen.width > 600) ? Doc.NativeHeight(this.props.Document) : `${100 / this.contentScaling}%`,
transform: `scale(${this.contentScaling})`
}} >
diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx
index 6a2325342..c72b25040 100644
--- a/src/client/views/search/SearchBox.tsx
+++ b/src/client/views/search/SearchBox.tsx
@@ -530,7 +530,7 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc
<div className="searchBox-lozenge-dashboard" >
<select className="searchBox-dashSelect" onChange={e => CurrentUserUtils.openDashboard(Doc.UserDoc(), myDashboards[Number(e.target.value)])}
value={myDashboards.indexOf(CurrentUserUtils.ActiveDashboard)}>
- {myDashboards.map((dash, i) => <option key={dash[Id]} value={i}> {StrCast(dash.title)} </option>)}
+ {myDashboards.map((dash, i) => <option key={dash[Id]} value={i} style={{ backgroundColor: "black" }}> {StrCast(dash.title)} </option>)}
</select>
<div className="searchBox-dashboards" onClick={undoBatch(() => CurrentUserUtils.createNewDashboard(Doc.UserDoc()))}>
New
diff --git a/src/client/views/topbar/TopBar.scss b/src/client/views/topbar/TopBar.scss
index ebdf030e7..2ecbb536b 100644
--- a/src/client/views/topbar/TopBar.scss
+++ b/src/client/views/topbar/TopBar.scss
@@ -23,6 +23,7 @@
.topBar-icon {
cursor: pointer;
font-size: 12px;
+ font-family: 'Roboto';
width: fit-content;
display: flex;
justify-content: center;
@@ -49,12 +50,15 @@
align-items: center;
gap: 5px;
+ .topbar-dashboards {
+ display: flex;
+ flex-direction: row;
+ }
+
.topbar-lozenge-dashboard {
display: flex;
- .topbar-dashboards {
- display: inline-flex;
- }
+
.topbar-dashSelect {
border: none;
@@ -87,11 +91,11 @@
font-family: 'Roboto';
position: relative;
display: flex;
- width: 450;
+ width: fit-content;
gap: 5px;
.topBar-icon:hover {
- background-color: $logout-red;
+ background-color: $close-red;
}
.topbar-lozenge-user,
diff --git a/src/client/views/topbar/TopBar.tsx b/src/client/views/topbar/TopBar.tsx
index bd9935333..05edb975c 100644
--- a/src/client/views/topbar/TopBar.tsx
+++ b/src/client/views/topbar/TopBar.tsx
@@ -12,7 +12,6 @@ import { Borders, Colors } from "../global/globalEnums";
import "./TopBar.scss";
/**
- * REACT TYPE: FUNCTIONAL
* ABOUT: This is the topbar in Dash, which included the current Dashboard as well as access to information on the user
* and settings and help buttons. Future scope for this bar is to include the collaborators that are on the same Dashboard.
*/
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 976bd5ee1..1eeadeedc 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -21,8 +21,9 @@ import { listSpec } from "./Schema";
import { ComputedField, ScriptField } from "./ScriptField";
import { Cast, FieldValue, NumCast, StrCast, ToConstructor } from "./Types";
import { AudioField, ImageField, PdfField, VideoField, WebField } from "./URLField";
-import { deleteProperty, GetEffectiveAcl, getField, getter, makeEditable, makeReadOnly, normalizeEmail, setter, SharingPermissions, updateFunction } from "./util";
+import { deleteProperty, GetEffectiveAcl, getField, getter, inheritParentAcls, makeEditable, makeReadOnly, normalizeEmail, setter, SharingPermissions, updateFunction } from "./util";
import JSZip = require("jszip");
+import { CurrentUserUtils } from "../client/util/CurrentUserUtils";
import { IconProp } from "@fortawesome/fontawesome-svg-core";
export namespace Field {
@@ -54,6 +55,9 @@ export namespace Field {
|| (field instanceof RefField)
|| (includeUndefined && field === undefined);
}
+ export function Copy(field: any) {
+ return field instanceof ObjectField ? ObjectField.MakeCopy(field) : field;
+ }
}
export type Field = number | string | boolean | ObjectField | RefField;
export type Opt<T> = T | undefined;
@@ -61,10 +65,10 @@ export type FieldWaiting<T extends RefField = RefField> = T extends undefined ?
export type FieldResult<T extends Field = Field> = Opt<T> | FieldWaiting<Extract<T, RefField>>;
/**
- * Cast any field to either a List of Docs or undefined if the given field isn't a List of Docs.
- * If a default value is given, that will be returned instead of undefined.
- * If a default value is given, the returned value should not be modified as it might be a temporary value.
- * If no default value is given, and the returned value is not undefined, it can be safely modified.
+ * Cast any field to either a List of Docs or undefined if the given field isn't a List of Docs.
+ * If a default value is given, that will be returned instead of undefined.
+ * If a default value is given, the returned value should not be modified as it might be a temporary value.
+ * If no default value is given, and the returned value is not undefined, it can be safely modified.
*/
export function DocListCastAsync(field: FieldResult): Promise<Doc[] | undefined>;
export function DocListCastAsync(field: FieldResult, defaultValue: Doc[]): Promise<Doc[]>;
@@ -89,7 +93,8 @@ export const DirectLinksSym = Symbol("DirectLinks");
export const AclUnset = Symbol("AclUnset");
export const AclPrivate = Symbol("AclOwnerOnly");
export const AclReadonly = Symbol("AclReadOnly");
-export const AclAddonly = Symbol("AclAddonly");
+export const AclAugment = Symbol("AclAugment");
+export const AclSelfEdit = Symbol("AclSelfEdit");
export const AclEdit = Symbol("AclEdit");
export const AclAdmin = Symbol("AclAdmin");
export const UpdatingFromServer = Symbol("UpdatingFromServer");
@@ -101,7 +106,8 @@ const AclMap = new Map<string, symbol>([
["None", AclUnset],
[SharingPermissions.None, AclPrivate],
[SharingPermissions.View, AclReadonly],
- [SharingPermissions.Add, AclAddonly],
+ [SharingPermissions.Augment, AclAugment],
+ [SharingPermissions.SelfEdit, AclSelfEdit],
[SharingPermissions.Edit, AclEdit],
[SharingPermissions.Admin, AclAdmin]
]);
@@ -251,7 +257,8 @@ export class Doc extends RefField {
DocServer.GetRefField(this[Id], true);
}
};
- if (sameAuthor || fKey.startsWith("acl") || DocServer.getFieldWriteMode(fKey) !== DocServer.WriteMode.Playground) {
+ const writeMode = DocServer.getFieldWriteMode(fKey as string);
+ if (fKey.startsWith("acl") || writeMode !== DocServer.WriteMode.Playground) {
delete this[CachedUpdates][fKey];
await fn();
} else {
@@ -365,13 +372,13 @@ export namespace Doc {
/**
* This function is intended to model Object.assign({}, {}) [https://mzl.la/1Mo3l21], which copies
* the values of the properties of a source object into the target.
- *
+ *
* This is just a specific, Dash-authored version that serves the same role for our
* Doc class.
- *
- * @param doc the target document into which you'd like to insert the new fields
+ *
+ * @param doc the target document into which you'd like to insert the new fields
* @param fields the fields to project onto the target. Its type signature defines a mapping from some string key
- * to a potentially undefined field, where each entry in this mapping is optional.
+ * to a potentially undefined field, where each entry in this mapping is optional.
*/
export function assign<K extends string>(doc: Doc, fields: Partial<Record<K, Opt<Field>>>, skipUndefineds: boolean = false, isInitializing = false) {
isInitializing && (doc[Initializing] = true);
@@ -398,7 +405,7 @@ export namespace Doc {
}
// Gets the data document for the document. Note: this is mis-named -- it does not specifically
- // return the doc's proto, but rather recursively searches through the proto inheritance chain
+ // return the doc's proto, but rather recursively searches through the proto inheritance chain
// and returns the document who's proto is undefined or whose proto is marked as a base prototype ('isPrototype').
export function GetProto(doc: Doc): Doc {
if (doc instanceof Promise) {
@@ -424,6 +431,9 @@ export namespace Doc {
return Array.from(results);
}
+ /**
+ * @returns the index of doc toFind in list of docs, -1 otherwise
+ */
export function IndexOf(toFind: Doc, list: Doc[], allowProtos: boolean = true) {
let index = list.reduce((p, v, i) => (v instanceof Doc && v === toFind) ? i : p, -1);
index = allowProtos && index !== -1 ? index : list.reduce((p, v, i) => (v instanceof Doc && Doc.AreProtosEqual(v, toFind)) ? i : p, -1);
@@ -530,13 +540,13 @@ export namespace Doc {
const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key]));
const field = ProxyField.WithoutProxy(() => doc[key]);
const copyObjectField = async (field: ObjectField) => {
- const list = Cast(doc[key], listSpec(Doc));
+ const list = await Cast(doc[key], listSpec(Doc));
const docs = list && (await DocListCastAsync(list))?.filter(d => d instanceof Doc);
if (docs !== undefined && docs.length) {
const clones = await Promise.all(docs.map(async d => Doc.makeClone(d, cloneMap, linkMap, rtfs, exclusions, dontCreate, asBranch)));
!dontCreate && assignKey(new List<Doc>(clones));
} else if (doc[key] instanceof Doc) {
- assignKey(key.includes("layout[") ? undefined : key.startsWith("layout") ? doc[key] as Doc : await Doc.makeClone(doc[key] as Doc, cloneMap, linkMap, rtfs, exclusions, dontCreate, asBranch)); // reference documents except copy documents that are expanded teplate fields
+ assignKey(key.includes("layout[") ? undefined : key.startsWith("layout") ? doc[key] as Doc : await Doc.makeClone(doc[key] as Doc, cloneMap, linkMap, rtfs, exclusions, dontCreate, asBranch)); // reference documents except copy documents that are expanded template fields
} else {
!dontCreate && assignKey(ObjectField.MakeCopy(field));
if (field instanceof RichTextField) {
@@ -562,7 +572,7 @@ export namespace Doc {
} else if (field instanceof ObjectField) {
await copyObjectField(field);
} else if (field instanceof Promise) {
- debugger; //This shouldn't happend...
+ debugger; //This shouldn't happen...
} else {
assignKey(field);
}
@@ -582,11 +592,10 @@ export namespace Doc {
}
return copy;
}
- export async function MakeClone(doc: Doc, dontCreate: boolean = false, asBranch = false) {
- const cloneMap = new Map<string, Doc>();
+ export async function MakeClone(doc: Doc, dontCreate: boolean = false, asBranch = false, cloneMap: Map<string, Doc> = new Map()) {
const linkMap = new Map<Doc, Doc>();
const rtfMap: { copy: Doc, key: string, field: RichTextField }[] = [];
- const copy = await Doc.makeClone(doc, cloneMap, linkMap, rtfMap, ["context", "annotationOn", "cloneOf", "branches", "branchOf"], dontCreate, asBranch);
+ const copy = await Doc.makeClone(doc, cloneMap, linkMap, rtfMap, ["cloneOf", "branches", "branchOf"], dontCreate, asBranch);
Array.from(linkMap.entries()).map((links: Doc[]) => LinkManager.Instance.addLink(links[1], true));
rtfMap.map(({ copy, key, field }) => {
const replacer = (match: any, attr: string, id: string, offset: any, string: any) => {
@@ -597,7 +606,7 @@ export namespace Doc {
const mapped = cloneMap.get(id);
return href + (mapped ? mapped[Id] : id);
};
- const regex = `(${Utils.prepend("/doc/")})([^"]*)`;
+ const regex = `(${Doc.localServerPath()})([^"]*)`;
const re = new RegExp(regex, "g");
copy[key] = new RichTextField(field.Data.replace(/("textId":|"audioId":|"anchorId":)"([^"]+)"/g, replacer).replace(re, replacer2), field.Text);
});
@@ -667,14 +676,14 @@ export namespace Doc {
const _pendingMap: Map<string, boolean> = new Map();
//
// Returns an expanded template layout for a target data document if there is a template relationship
- // between the two. If so, the layoutDoc is expanded into a new document that inherits the properties
+ // between the two. If so, the layoutDoc is expanded into a new document that inherits the properties
// of the original layout while allowing for individual layout properties to be overridden in the expanded layout.
// templateArgs should be equivalent to the layout key that generates the template since that's where the template parameters are stored in ()'s at the end of the key.
// NOTE: the template will have references to "@params" -- the template arguments will be assigned to the '@params' field
// so that when the @params key is accessed, it will be rewritten as the key that is stored in the 'params' field and
// the derefence will then occur on the rootDocument (the original document).
// in the future, field references could be written as @<someparam> and then arguments would be passed in the layout key as:
- // layout_mytemplate(somparam=somearg).
+ // layout_mytemplate(somparam=somearg).
// then any references to @someparam would be rewritten as accesses to 'somearg' on the rootDocument
export function expandTemplateLayout(templateLayoutDoc: Doc, targetDoc?: Doc, templateArgs?: string) {
const args = templateArgs?.match(/\(([a-zA-Z0-9._\-]*)\)/)?.[1].replace("()", "") || StrCast(templateLayoutDoc.PARAMS);
@@ -775,7 +784,7 @@ export namespace Doc {
copy[key] = cfield[Copy]();// ComputedField.MakeFunction(cfield.script.originalScript);
} else if (field instanceof ObjectField) {
copy[key] = doc[key] instanceof Doc ?
- key.includes("layout[") ? undefined : doc[key] : // reference documents except remove documents that are expanded teplate fields
+ key.includes("layout[") ? undefined : doc[key] : // reference documents except remove documents that are expanded teplate fields
ObjectField.MakeCopy(field);
} else if (field instanceof Promise) {
debugger; //This shouldn't happend...
@@ -897,6 +906,16 @@ export namespace Doc {
return true;
}
+
+ // converts a document id to a url path on the server
+ export function globalServerPath(doc: Doc | string = ""): string {
+ return Utils.prepend("/doc/" + (doc instanceof Doc ? doc[Id] : doc));
+ }
+ // converts a document id to a url path on the server
+ export function localServerPath(doc?: Doc): string {
+ return "/doc/" + (doc ? doc[Id] : "");
+ }
+
export function overlapping(doc1: Doc, doc2: Doc, clusterDistance: number) {
const doc2Layout = Doc.Layout(doc2);
const doc1Layout = Doc.Layout(doc1);
@@ -928,7 +947,7 @@ export namespace Doc {
}
// the document containing the view layout information - will be the Document itself unless the Document has
- // a layout field or 'layout' is given.
+ // a layout field or 'layout' is given.
export function Layout(doc: Doc, layout?: Doc): Doc {
const overrideLayout = layout && Cast(doc[`${StrCast(layout.isTemplateForField, "data")}-layout[` + layout[Id] + "]"], Doc, null);
return overrideLayout || doc[LayoutSym] || doc;
@@ -1107,7 +1126,7 @@ export namespace Doc {
}
// filters document in a container collection:
- // all documents with the specified value for the specified key are included/excluded
+ // all documents with the specified value for the specified key are included/excluded
// based on the modifiers :"check", "x", undefined
export function setDocFilter(container: Opt<Doc>, key: string, value: any, modifiers: "remove" | "match" | "check" | "x", toggle?: boolean, fieldSuffix?: string, append: boolean = true) {
if (!container) return;
@@ -1178,6 +1197,9 @@ export namespace Doc {
dragFactory["dragFactory-count"] = NumCast(dragFactory["dragFactory-count"]) + 1;
Doc.SetInPlace(ndoc, "title", ndoc.title + " " + NumCast(dragFactory["dragFactory-count"]).toString(), true);
}
+
+ if (ndoc) inheritParentAcls(CurrentUserUtils.ActiveDashboard, ndoc);
+
return ndoc;
}
export function delegateDragFactory(dragFactory: Doc) {
@@ -1196,7 +1218,7 @@ export namespace Doc {
case DocumentType.RTF: return "sticky-note";
case DocumentType.COL:
const folder: IconProp = isOpen ? "folder-open" : "folder";
- const chevron: IconProp = isOpen ? "chevron-down" : "chevron-right"
+ const chevron: IconProp = isOpen ? "chevron-down" : "chevron-right";
return !doc?.isFolder ? folder : chevron;
case DocumentType.WEB: return "globe-asia";
case DocumentType.SCREENSHOT: return "photo-video";
@@ -1231,39 +1253,39 @@ export namespace Doc {
/**
* This function takes any valid JSON(-like) data, i.e. parsed or unparsed, and at arbitrarily
* deep levels of nesting, converts the data and structure into nested documents with the appropriate fields.
- *
+ *
* After building a hierarchy within / below a top-level document, it then returns that top-level parent.
- *
+ *
* If we've received a string, treat it like valid JSON and try to parse it into an object. If this fails, the
* string is invalid JSON, so we should assume that the input is the result of a JSON.parse()
* call that returned a regular string value to be stored as a Field.
- *
+ *
* If we've received something other than a string, since the caller might also pass in the results of a
* JSON.parse() call, valid input might be an object, an array (still typeof object), a boolean or a number.
* Anything else (like a function, etc. passed in naively as any) is meaningless for this operation.
- *
+ *
* All TS/JS objects get converted directly to documents, directly preserving the key value structure. Everything else,
* lacking the key value structure, gets stored as a field in a wrapper document.
- *
+ *
* @param data for convenience and flexibility, either a valid JSON string to be parsed,
* or the result of any JSON.parse() call.
* @param title an optional title to give to the highest parent document in the hierarchy.
* If whether this function creates a new document or appendToExisting is specified and that document already has a title,
* because this title field can be left undefined for the opposite behavior, including a title will overwrite the existing title.
* @param appendToExisting **if specified**, there are two cases, both of which return the target document:
- *
+ *
* 1) the json to be converted can be represented as a document, in which case the target document will act as the root
* of the tree and receive all the conversion results as new fields on itself
* 2) the json can't be represented as a document, in which case the function will assign the field-level conversion
* results to either the specified key on the target document, or to its "json" key by default.
- *
+ *
* If not specified, the function creates and returns a new entirely generic document (different from the Doc.Create calls)
* to act as the root of the tree.
- *
+ *
* One might choose to specify this field if you want to write to a document returned from a Document.Create function call,
* say a TreeView document that will be rendered, not just an untyped, identityless doc that would otherwise be created
* from a default call to new Doc.
- *
+ *
* @param excludeEmptyObjects whether non-primitive objects (TypeScript objects and arrays) should be converted even
* if they contain no data. By default, empty objects and arrays are ignored.
*/
@@ -1299,7 +1321,7 @@ export namespace Doc {
* For each value of the object, recursively convert it to its appropriate field value
* and store the field at the appropriate key in the document if it is not undefined
* @param object the object to convert
- * @returns the object mapped from JSON to field values, where each mapping
+ * @returns the object mapped from JSON to field values, where each mapping
* might involve arbitrary recursion (since toField might itself call convertObject)
*/
const convertObject = (object: any, excludeEmptyObjects: boolean, title?: string, target?: Doc): Opt<Doc> => {
@@ -1323,10 +1345,10 @@ export namespace Doc {
};
/**
- * For each element in the list, recursively convert it to a document or other field
+ * For each element in the list, recursively convert it to a document or other field
* and push the field to the list if it is not undefined
* @param list the list to convert
- * @returns the list mapped from JSON to field values, where each mapping
+ * @returns the list mapped from JSON to field values, where each mapping
* might involve arbitrary recursion (since toField might itself call convertList)
*/
const convertList = (list: Array<any>, excludeEmptyObjects: boolean): Opt<List<Field>> => {
@@ -1365,7 +1387,7 @@ Scripting.addGlobal(function getAlias(doc: any) { return Doc.MakeAlias(doc); });
Scripting.addGlobal(function getCopy(doc: any, copyProto: any) { return doc.isTemplateDoc ? Doc.ApplyTemplate(doc) : Doc.MakeCopy(doc, copyProto); });
Scripting.addGlobal(function copyDragFactory(dragFactory: Doc) { return Doc.copyDragFactory(dragFactory); });
Scripting.addGlobal(function delegateDragFactory(dragFactory: Doc) { return Doc.delegateDragFactory(dragFactory); });
-Scripting.addGlobal(function copyField(field: any) { return field instanceof ObjectField ? ObjectField.MakeCopy(field) : field; });
+Scripting.addGlobal(function copyField(field: any) { return Field.Copy(field); });
Scripting.addGlobal(function docList(field: any) { return DocListCast(field); });
Scripting.addGlobal(function setInPlace(doc: any, field: any, value: any) { return Doc.SetInPlace(doc, field, value, false); });
Scripting.addGlobal(function sameDocs(doc1: any, doc2: any) { return Doc.AreProtosEqual(doc1, doc2); });
diff --git a/src/fields/List.ts b/src/fields/List.ts
index 215dff34b..93a8d1d60 100644
--- a/src/fields/List.ts
+++ b/src/fields/List.ts
@@ -1,4 +1,4 @@
-import { action, observable, runInAction } from "mobx";
+import { action, observable } from "mobx";
import { alias, list, serializable } from "serializr";
import { DocServer } from "../client/DocServer";
import { Scripting } from "../client/util/Scripting";
@@ -264,24 +264,19 @@ class ListImpl<T extends Field> extends ObjectField {
// this requests all ProxyFields at the same time to avoid the overhead
// of separate network requests and separate updates to the React dom.
private __realFields() {
- const waiting = this.__fields.filter(f => f instanceof ProxyField && f.promisedValue());
- const promised = waiting.map(f => f instanceof ProxyField ? f.promisedValue() : "");
+ const promised = this.__fields.filter(f => f instanceof ProxyField && f.promisedValue()).map(f => ({ field: f as any, promisedFieldId: (f instanceof ProxyField) ? f.promisedValue() : "" }));
// if we find any ProxyFields that don't have a current value, then
// start the server request for all of them
if (promised.length) {
- const promise = DocServer.GetRefFields(promised);
+ const batchPromise = DocServer.GetRefFields(promised.map(p => p.promisedFieldId));
// as soon as we get the fields from the server, set all the list values in one
// action to generate one React dom update.
- promise.then(fields => runInAction(() => {
- waiting.map((w, i) => w instanceof ProxyField && w.setValue(fields[promised[i]]));
- }));
+ batchPromise.then(pfields => promised.forEach(p => p.field.setValue(pfields[p.promisedFieldId])));
// we also have to mark all lists items with this promise so that any calls to them
- // will await the batch request.
- // This counts on the handler for 'promise' in the call above being invoked before the
+ // will await the batch request and return the requested field value.
+ // This assumes the handler for 'promise' in the call above being invoked before the
// handler for 'promise' in the lines below.
- waiting.map((w, i) => {
- w instanceof ProxyField && w.setPromise(promise.then(fields => fields[promised[i]]));
- });
+ promised.forEach(p => p.field.setPromise(batchPromise.then(pfields => pfields[p.promisedFieldId])));
}
return this.__fields.map(toRealField);
}
diff --git a/src/fields/URLField.ts b/src/fields/URLField.ts
index fb71160ca..d96e8a70a 100644
--- a/src/fields/URLField.ts
+++ b/src/fields/URLField.ts
@@ -3,14 +3,17 @@ import { serializable, custom } from "serializr";
import { ObjectField } from "./ObjectField";
import { ToScriptString, ToString, Copy } from "./FieldSymbols";
import { Scripting, scriptingGlobal } from "../client/util/Scripting";
+import { Utils } from "../Utils";
function url() {
return custom(
function (value: URL) {
- return value.href;
+ return value.origin === window.location.origin ?
+ value.pathname :
+ value.href;
},
function (jsonValue: string) {
- return new URL(jsonValue);
+ return new URL(jsonValue, window.location.origin);
}
);
}
@@ -24,15 +27,21 @@ export abstract class URLField extends ObjectField {
constructor(url: URL | string) {
super();
if (typeof url === "string") {
- url = new URL(url);
+ url = url.startsWith("http") ? new URL(url) : new URL(url, window.location.origin);
}
this.url = url;
}
[ToScriptString]() {
+ if (Utils.prepend(this.url.pathname) === this.url.href) {
+ return `new ${this.constructor.name}("${this.url.pathname}")`;
+ }
return `new ${this.constructor.name}("${this.url.href}")`;
}
[ToString]() {
+ if (Utils.prepend(this.url.pathname) === this.url.href) {
+ return this.url.pathname;
+ }
return this.url.href;
}
diff --git a/src/fields/util.ts b/src/fields/util.ts
index ea91cc057..439c4d333 100644
--- a/src/fields/util.ts
+++ b/src/fields/util.ts
@@ -1,5 +1,5 @@
import { UndoManager } from "../client/util/UndoManager";
-import { Doc, FieldResult, UpdatingFromServer, LayoutSym, AclPrivate, AclEdit, AclReadonly, AclAddonly, AclSym, DataSym, DocListCast, AclAdmin, HeightSym, WidthSym, updateCachedAcls, AclUnset, DocListCastAsync, ForceServerWrite, Initializing } from "./Doc";
+import { Doc, FieldResult, UpdatingFromServer, LayoutSym, AclPrivate, AclEdit, AclReadonly, AclAugment, AclSym, DataSym, DocListCast, AclAdmin, HeightSym, WidthSym, updateCachedAcls, AclUnset, DocListCastAsync, ForceServerWrite, Initializing, AclSelfEdit } from "./Doc";
import { SerializationHelper } from "../client/util/SerializationHelper";
import { ProxyField, PrefetchProxy } from "./Proxy";
import { RefField } from "./RefField";
@@ -14,6 +14,7 @@ import CursorField from "./CursorField";
import { List } from "./List";
import { SnappingManager } from "../client/util/SnappingManager";
import { computedFn } from "mobx-utils";
+import { RichTextField } from "./RichTextField";
function _readOnlySetter(): never {
throw new Error("Documents can't be modified in read-only mode");
@@ -77,7 +78,9 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number
const fromServer = target[UpdatingFromServer];
const sameAuthor = fromServer || (receiver.author === Doc.CurrentUserEmail);
const writeToDoc = sameAuthor || effectiveAcl === AclEdit || effectiveAcl === AclAdmin || (writeMode !== DocServer.WriteMode.LiveReadonly);
- const writeToServer = (sameAuthor || effectiveAcl === AclEdit || effectiveAcl === AclAdmin || writeMode === DocServer.WriteMode.Default) && !DocServer.Control.isReadOnly();// && !playgroundMode;
+ const writeToServer =
+ (sameAuthor || effectiveAcl === AclEdit || effectiveAcl === AclAdmin || (effectiveAcl === AclSelfEdit && (value instanceof RichTextField))) &&
+ !DocServer.Control.isReadOnly();
if (writeToDoc) {
if (value === undefined) {
@@ -131,6 +134,19 @@ export function denormalizeEmail(email: string) {
// playgroundMode = !playgroundMode;
// }
+
+/**
+ * Copies parent's acl fields to the child
+ */
+export function inheritParentAcls(parent: Doc, child: Doc) {
+ const dataDoc = parent[DataSym];
+ for (const key of Object.keys(dataDoc)) {
+ // if the default acl mode is private, then don't inherit the acl-Public permission, but set it to private.
+ const permission = (key === "acl-Public" && Doc.UserDoc().defaultAclPrivate) ? AclPrivate : dataDoc[key];
+ key.startsWith("acl") && distributeAcls(key, permission, child);
+ }
+}
+
/**
* These are the various levels of access a user can have to a document.
*
@@ -146,9 +162,10 @@ export function denormalizeEmail(email: string) {
*/
export enum SharingPermissions {
Admin = "Admin",
- Edit = "Can Edit",
- Add = "Can Augment",
- View = "Can View",
+ Edit = "Edit",
+ SelfEdit = "Self Edit",
+ Augment = "Augment",
+ View = "View",
None = "Not Shared"
}
@@ -165,7 +182,7 @@ export function GetEffectiveAcl(target: any, user?: string): symbol {
function getPropAcl(target: any, prop: string | symbol | number) {
if (prop === UpdatingFromServer || prop === Initializing || target[UpdatingFromServer] || prop === AclSym) return AclAdmin; // requesting the UpdatingFromServer prop or AclSym must always go through to keep the local DB consistent
- if (prop && DocServer.PlaygroundFields?.includes(prop.toString())) return AclEdit; // playground props are always editable
+ if (prop && DocServer.IsPlaygroundField(prop.toString())) return AclEdit; // playground props are always editable
return GetEffectiveAcl(target);
}
@@ -181,7 +198,8 @@ function getEffectiveAcl(target: any, user?: string): symbol {
HierarchyMapping = HierarchyMapping || new Map<symbol, number>([
[AclPrivate, 0],
[AclReadonly, 1],
- [AclAddonly, 2],
+ [AclAugment, 2],
+ [AclSelfEdit, 2.5],
[AclEdit, 3],
[AclAdmin, 4]
]);
@@ -215,7 +233,7 @@ function getEffectiveAcl(target: any, user?: string): symbol {
* @param inheritingFromCollection whether the target is being assigned rights after being dragged into a collection (and so is inheriting the acls from the collection)
* inheritingFromCollection is not currently being used but could be used if acl assignment defaults change
*/
-export function distributeAcls(key: string, acl: SharingPermissions, target: Doc, inheritingFromCollection?: boolean, visited?: Doc[]) {
+export function distributeAcls(key: string, acl: SharingPermissions, target: Doc, inheritingFromCollection?: boolean, visited?: Doc[], isDashboard?: boolean) {
if (!visited) visited = [] as Doc[];
if (visited.includes(target)) return;
visited.push(target);
@@ -224,6 +242,7 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc
["Not Shared", 0],
["Can View", 1],
["Can Augment", 2],
+ ["Self Edit", 2.5],
["Can Edit", 3],
["Admin", 4]
]);
@@ -236,6 +255,12 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc
if (GetEffectiveAcl(target) === AclAdmin && (!inheritingFromCollection || !target[key] || HierarchyMapping.get(StrCast(target[key]))! > HierarchyMapping.get(acl)!)) {
target[key] = acl;
layoutDocChanged = true;
+
+ if (isDashboard) {
+ DocListCastAsync(target[Doc.LayoutFieldKey(target)]).then(docs => {
+ docs?.forEach(d => distributeAcls(key, acl, d, inheritingFromCollection, visited));
+ });
+ }
}
if (dataDoc && (!inheritingFromCollection || !dataDoc[key] || HierarchyMapping.get(StrCast(dataDoc[key]))! > HierarchyMapping.get(acl)!)) {
@@ -245,28 +270,26 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc
dataDocChanged = true;
}
- // maps over the aliases of the document
+ // maps over the links of the document
const links = DocListCast(dataDoc.links);
links.forEach(link => distributeAcls(key, acl, link, inheritingFromCollection, visited));
// maps over the children of the document
- DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc)]).map(d => {
- // if (GetEffectiveAcl(d) === AclAdmin && (!inheritingFromCollection || !d[key] || HierarchyMapping.get(StrCast(d[key]))! > HierarchyMapping.get(acl)!)) {
+ DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc) + (isDashboard ? "-all" : "")]).map(d => {
distributeAcls(key, acl, d, inheritingFromCollection, visited);
// }
const data = d[DataSym];
- if (data) {// && GetEffectiveAcl(data) === AclAdmin && (!inheritingFromCollection || !data[key] || HierarchyMapping.get(StrCast(data[key]))! > HierarchyMapping.get(acl)!)) {
+ if (data) {
distributeAcls(key, acl, data, inheritingFromCollection, visited);
}
});
// maps over the annotations of the document
DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc) + "-annotations"]).map(d => {
- // if (GetEffectiveAcl(d) === AclAdmin && (!inheritingFromCollection || !d[key] || HierarchyMapping.get(StrCast(d[key]))! > HierarchyMapping.get(acl)!)) {
distributeAcls(key, acl, d, inheritingFromCollection, visited);
// }
const data = d[DataSym];
- if (data) {// && GetEffectiveAcl(data) === AclAdmin && (!inheritingFromCollection || !data[key] || HierarchyMapping.get(StrCast(data[key]))! > HierarchyMapping.get(acl)!)) {
+ if (data) {
distributeAcls(key, acl, data, inheritingFromCollection, visited);
}
});
@@ -279,7 +302,7 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc
export function setter(target: any, in_prop: string | symbol | number, value: any, receiver: any): boolean {
let prop = in_prop;
const effectiveAcl = getPropAcl(target, prop);
- if (effectiveAcl !== AclEdit && effectiveAcl !== AclAdmin) return true;
+ if (effectiveAcl !== AclEdit && effectiveAcl !== AclAdmin && !(effectiveAcl === AclSelfEdit && value instanceof RichTextField)) return true;
// if you're trying to change an acl but don't have Admin access / you're trying to change it to something that isn't an acceptable acl, you can't
if (typeof prop === "string" && prop.startsWith("acl") && (effectiveAcl !== AclAdmin || ![...Object.values(SharingPermissions), undefined, "None"].includes(value))) return true;
// if (typeof prop === "string" && prop.startsWith("acl") && !["Can Edit", "Can Augment", "Can View", "Not Shared", undefined].includes(value)) return true;
diff --git a/src/mobile/ImageUpload.tsx b/src/mobile/ImageUpload.tsx
index 98696496f..f910d765e 100644
--- a/src/mobile/ImageUpload.tsx
+++ b/src/mobile/ImageUpload.tsx
@@ -50,7 +50,7 @@ export class Uploader extends React.Component<ImageUploadProps> {
if (result instanceof Error) {
return;
}
- const path = Utils.prepend(result.accessPaths.agnostic.client);
+ const path = result.accessPaths.agnostic.client;
let doc = null;
// Case 1: File is a video
if (file.type === "video/mp4") {
diff --git a/src/server/server_Initialization.ts b/src/server/server_Initialization.ts
index e40f2b8e5..0f4a067fc 100644
--- a/src/server/server_Initialization.ts
+++ b/src/server/server_Initialization.ts
@@ -142,8 +142,9 @@ function registerCorsProxy(server: express.Express) {
const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/;
server.use("/corsProxy", async (req, res) => {
- const requrl = decodeURIComponent(req.url.substring(1));
const referer = req.headers.referer ? decodeURIComponent(req.headers.referer) : "";
+ const requrlraw = decodeURIComponent(req.url.substring(1));
+ const requrl = requrlraw.startsWith("/") ? referer + requrlraw : requrlraw;
// cors weirdness here...
// if the referer is a cors page and the cors() route (I think) redirected to /corsProxy/<path> and the requested url path was relative,
// then we redirect again to the cors referer and just add the relative path.
diff --git a/src/server/websocket.ts b/src/server/websocket.ts
index 4ae97913f..224a9eefb 100644
--- a/src/server/websocket.ts
+++ b/src/server/websocket.ts
@@ -283,7 +283,7 @@ export namespace WebSocket {
return;
}
const curList = (curListItems as any)?.fields?.[updatefield.replace("fields.", "")]?.fields.filter((item: any) => item !== undefined) || [];
- diff.diff.$set[updatefield].fields = [...curList, ...newListItems.filter((newItem: any) => newItem === null || !curList.some((curItem: any) => curItem.fieldId ? curItem.fieldId === newItem.fieldId : curItem.heading ? curItem.heading === newItem.heading : curItem === newItem))];
+ diff.diff.$set[updatefield].fields = [...curList, ...newListItems];//, ...newListItems.filter((newItem: any) => newItem === null || !curList.some((curItem: any) => curItem.fieldId ? curItem.fieldId === newItem.fieldId : curItem.heading ? curItem.heading === newItem.heading : curItem === newItem))];
const sendBack = diff.diff.length !== diff.diff.$set[updatefield].fields.length;
delete diff.diff.length;
Database.Instance.update(diff.id, diff.diff,