aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorAubrey-Li <63608597+Aubrey-Li@users.noreply.github.com>2021-08-21 13:59:06 -0700
committerAubrey-Li <63608597+Aubrey-Li@users.noreply.github.com>2021-08-21 13:59:06 -0700
commitdf03b0bbf84b8314423542d90c29a0a127638a73 (patch)
treedec130023105a60e5c3a04139b28364e7c3b7c6b /src
parentbecf6eeac2bd370fef2eb67c107ccfaeef5805e4 (diff)
parent3051d9a16dff8efbf4d32465812093cae7508c74 (diff)
Merge branch 'master' into trails-aubrey
Diffstat (limited to 'src')
-rw-r--r--src/client/DocServer.ts7
-rw-r--r--src/client/documents/Documents.ts27
-rw-r--r--src/client/documents/Gitlike.ts36
-rw-r--r--src/client/util/CurrentUserUtils.ts92
-rw-r--r--src/client/util/DocumentManager.ts6
-rw-r--r--src/client/util/InteractionUtils.tsx7
-rw-r--r--src/client/util/LinkManager.ts78
-rw-r--r--src/client/util/SettingsManager.tsx3
-rw-r--r--src/client/util/SharingManager.tsx19
-rw-r--r--src/client/views/DocComponent.tsx8
-rw-r--r--src/client/views/DocumentButtonBar.tsx2
-rw-r--r--src/client/views/DocumentDecorations.tsx2
-rw-r--r--src/client/views/GestureOverlay.tsx53
-rw-r--r--src/client/views/GlobalKeyHandler.ts8
-rw-r--r--src/client/views/InkHandles.tsx3
-rw-r--r--src/client/views/InkStrokeProperties.ts11
-rw-r--r--src/client/views/InkingStroke.tsx8
-rw-r--r--src/client/views/MainView.tsx33
-rw-r--r--src/client/views/MarqueeAnnotator.tsx4
-rw-r--r--src/client/views/PropertiesView.tsx14
-rw-r--r--src/client/views/collections/CollectionDockingView.tsx2
-rw-r--r--src/client/views/collections/CollectionSchemaView.tsx2
-rw-r--r--src/client/views/collections/CollectionSubView.tsx6
-rw-r--r--src/client/views/collections/CollectionTreeView.tsx15
-rw-r--r--src/client/views/collections/CollectionView.tsx26
-rw-r--r--src/client/views/collections/TabDocView.tsx5
-rw-r--r--src/client/views/collections/TreeView.tsx50
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx4
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx6
-rw-r--r--src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx2
-rw-r--r--src/client/views/collections/collectionSchema/SchemaTable.tsx3
-rw-r--r--src/client/views/linking/LinkPopup.tsx1
-rw-r--r--src/client/views/nodes/DocumentLinksButton.tsx4
-rw-r--r--src/client/views/nodes/DocumentView.tsx4
-rw-r--r--src/client/views/nodes/FilterBox.tsx4
-rw-r--r--src/client/views/nodes/PDFBox.tsx2
-rw-r--r--src/client/views/nodes/ScreenshotBox.tsx23
-rw-r--r--src/client/views/nodes/WebBox.tsx58
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx42
-rw-r--r--src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts73
-rw-r--r--src/client/views/pdf/PDFViewer.tsx7
-rw-r--r--src/client/views/search/SearchBox.scss194
-rw-r--r--src/client/views/search/SearchBox.tsx714
-rw-r--r--src/client/views/topbar/TopBar.scss2
-rw-r--r--src/fields/Doc.ts22
-rw-r--r--src/fields/List.ts19
-rw-r--r--src/fields/util.ts26
-rw-r--r--src/server/websocket.ts2
48 files changed, 798 insertions, 941 deletions
diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts
index 59278d2af..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;
@@ -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/documents/Documents.ts b/src/client/documents/Documents.ts
index c2d1fec51..1e2478c94 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";
@@ -584,7 +584,7 @@ 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;
@@ -595,13 +595,15 @@ 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;
}
@@ -706,7 +708,7 @@ 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;
@@ -736,15 +738,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);
}
@@ -1048,8 +1055,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,
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 f9036ac9b..b4e6d9be9 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -2,6 +2,7 @@ import { computed, observable, reaction } from "mobx";
import * as rp from 'request-promise';
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";
@@ -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,9 @@ import { LinkManager } from "./LinkManager";
import { Scripting } from "./Scripting";
import { SearchUtil } from "./SearchUtil";
import { SelectionManager } from "./SelectionManager";
-import { SnappingManager } from "./SnappingManager";
-import { InkTool } from "../../fields/InkField";
-import { SharingManager } from "./SharingManager";
-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";
@@ -47,6 +46,7 @@ export class CurrentUserUtils {
//TODO tfs: these should be temporary...
private static mainDocId: string | undefined;
+ public static searchBtn: Doc;
public static get id() { return this.curr_id; }
public static get MainDocId() { return this.mainDocId; }
public static set MainDocId(id: string | undefined) { this.mainDocId = id; }
@@ -56,6 +56,7 @@ export class CurrentUserUtils {
@observable public static GuestDashboard: Doc | undefined;
@observable public static GuestMobile: Doc | undefined;
@observable public static propertiesWidth: number = 0;
+ @observable public static searchPanelWidth: number = 0;
// sets up the default User Templates - slideView, headerView
static setupUserTemplateButtons(doc: Doc) {
@@ -535,6 +536,7 @@ export class CurrentUserUtils {
static async menuBtnDescriptions(doc: Doc) {
return [
{ title: "Dashboards", target: Cast(doc.myDashboards, Doc, null), icon: "desktop", click: 'selectMainMenu(self)' },
+ { title: "Search", target: Cast(doc.mySearchPanel, Doc, null), icon: "search", click: 'selectMainMenu(self)' },
{ title: "My Files", target: Cast(doc.myFilesystem, Doc, null), icon: "file", click: 'selectMainMenu(self)' },
{ title: "Tools", target: Cast(doc.myTools, Doc, null), icon: "wrench", click: 'selectMainMenu(self)' },
{ title: "Import", target: Cast(doc.myImportPanel, Doc, null), icon: "upload", click: 'selectMainMenu(self)' },
@@ -547,14 +549,6 @@ export class CurrentUserUtils {
];
}
- static setupSearchPanel(doc: Doc) {
- if (doc.mySearchPanelDoc === undefined) {
- doc.mySearchPanelDoc = new PrefetchProxy(Docs.Create.SearchDocument({
- _width: 500, _height: 300, backgroundColor: "dimGray", ignoreClick: true, _searchDoc: true,
- childDropAction: "alias", _lockedPosition: true, _viewType: CollectionViewType.Schema, title: "sidebar search stack", system: true
- })) as any as Doc;
- }
- }
static async setupMenuPanel(doc: Doc, sharingDocumentId: string, linkDatabaseId: string) {
if (doc.menuStack === undefined) {
await this.setupSharingSidebar(doc, sharingDocumentId, linkDatabaseId); // sets up the right sidebar collection for mobile upload documents and sharing
@@ -574,10 +568,23 @@ export class CurrentUserUtils {
_height: 60,
watchedDocuments,
onClick: ScriptField.MakeScript(click, { scriptContext: "any" })
- }));
+ })
+ );
+
+ menuBtns.forEach(menuBtn => {
+ if (menuBtn.title === "Search") {
+ this.searchBtn = menuBtn;
+ }
+ });
// hack -- last button is assumed to be the userDoc
menuBtns[menuBtns.length - 1].hidden = ComputedField.MakeFunction("IsNoviceMode()");
+ menuBtns.forEach(menuBtn => {
+ if (menuBtn.title === "Search") {
+ doc.searchBtn = menuBtn;
+ }
+ });
+
doc.menuStack = new PrefetchProxy(Docs.Create.StackingDocument(menuBtns, {
title: "menuItemPanel",
childDropAction: "alias",
@@ -771,8 +778,13 @@ export class CurrentUserUtils {
_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!]);
- (doc.myDashboards as any as Doc).contextMenuLabels = new List<string>(["Create New Dashboard"]);
+ const toggleTheme = ScriptField.MakeScript(`Doc.UserDoc().darkScheme = !Doc.UserDoc().darkScheme`);
+ const toggleComic = ScriptField.MakeScript(`toggleComicMode()`);
+ const snapshotDashboard = ScriptField.MakeScript(`snapshotDashboard()`);
+ const shareDashboard = ScriptField.MakeScript(`shareDashboard(self)`);
+ const removeDashboard = ScriptField.MakeScript('removeDashboard(self)');
+ (doc.myDashboards as any as Doc).childContextMenuScripts = new List<ScriptField>([newDashboard!, toggleTheme!, toggleComic!, snapshotDashboard!, shareDashboard!, removeDashboard!]);
+ (doc.myDashboards as any as Doc).childContextMenuLabels = new List<string>(["Create New Dashboard", "Toggle Theme Colors", "Toggle Comic Mode", "Snapshot Dashboard", "Share Dashboard", "Remove Dashboard"]);
}
return doc.myDashboards as any as Doc;
}
@@ -824,6 +836,7 @@ export class CurrentUserUtils {
(doc.myRecentlyClosedDocs as any as Doc).contextMenuLabels = new List<string>(["Clear All"]);
}
}
+
static setupFilterDocs(doc: Doc) {
// setup Filter item
if (doc.currentFilter === undefined) {
@@ -920,9 +933,10 @@ 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);
}
@@ -931,13 +945,18 @@ export class CurrentUserUtils {
if (!sharedDocs) {
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.Add, "_acl-Public": SharingPermissions.Add,
+ _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)";
+ const addToDashboards = ScriptField.MakeScript(`addToDashboards(self)`);
+ const dashboardFilter = ScriptField.MakeFunction(`doc._viewType === '${CollectionViewType.Docking}'`, { doc: Doc.name });
+ sharedDocs.childContextMenuFilters = new List<ScriptField>([dashboardFilter!,]);
+ sharedDocs.childContextMenuScripts = new List<ScriptField>([addToDashboards!,]);
+ sharedDocs.childContextMenuLabels = new List<string>(["Add to Dashboards",]);
}
doc.mySharedDocs = new PrefetchProxy(sharedDocs);
}
@@ -958,6 +977,16 @@ export class CurrentUserUtils {
}
}
+ // Search sidebar is where searches within the document are performed
+ static setupSearchSidebar(doc: Doc) {
+ if (doc.mySearchPanel === undefined) {
+ doc.mySearchPanel = new PrefetchProxy(Docs.Create.SearchDocument({
+ backgroundColor: "dimGray", ignoreClick: true, _searchDoc: true,
+ childDropAction: "alias", _lockedPosition: true, _viewType: CollectionViewType.Schema, title: "Search Sidebar", system: true
+ })) as any as Doc;
+ }
+ }
+
static setupClickEditorTemplates(doc: Doc) {
if (doc["clickFuncs-child"] === undefined) {
// to use this function, select it from the context menu of a collection. then edit the onChildClick script. Add two Doc variables: 'target' and 'thisContainer', then assign 'target' to some target collection. After that, clicking on any document in the initial collection will open it in the target
@@ -1034,7 +1063,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);
@@ -1046,8 +1075,8 @@ export class CurrentUserUtils {
this.setupDefaultIconTemplates(doc); // creates a set of icon templates triggered by the document deoration icon
this.setupDocTemplates(doc); // sets up the template menu of templates
this.setupImportSidebar(doc);
+ this.setupSearchSidebar(doc); // sets up the search sidebar
this.setupActiveMobileMenu(doc); // sets up the current mobile menu for Dash Mobile
- this.setupSearchPanel(doc);
this.setupOverlays(doc); // documents in overlay layer
this.setupDockedButtons(doc); // the bottom bar of font icons
await this.setupSidebarButtons(doc); // the pop-out left sidebar of tools/panels
@@ -1233,15 +1262,6 @@ export class CurrentUserUtils {
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()`);
- 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);
@@ -1311,17 +1331,15 @@ Scripting.addGlobal(async function addToDashboards(dashboard: Doc) {
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);
+ // 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));
+ // 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>();
+ //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);
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 27ae0447a..cb0ee411c 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -163,7 +163,8 @@ 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;
- var 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) {
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
@@ -181,9 +182,6 @@ export class DocumentManager {
if (!targetDocContext) { // we don't have a view and there's no context specified ... create a new view of the target using the dockFunc or default
createViewFunc(Doc.BrushDoc(targetDoc), finished); // bcz: should we use this?: Doc.MakeAlias(targetDoc)));
} else { // otherwise try to get a view of the context of the target
- if (annoContainerView && wasHidden) { // if we have an annotation container and the target was hidden, then try again because we just un-hid the document above
- targetDocContextView = annoContainerView;
- }
if (targetDocContextView) { // we found a context view and aren't forced to create a new one ... focus on the context first..
targetDocContext._viewTransition = "transform 500ms";
targetDocContextView.props.focus(targetDocContextView.rootDoc, {
diff --git a/src/client/util/InteractionUtils.tsx b/src/client/util/InteractionUtils.tsx
index ba935e3bf..e009fb3a9 100644
--- a/src/client/util/InteractionUtils.tsx
+++ b/src/client/util/InteractionUtils.tsx
@@ -210,7 +210,8 @@ export namespace InteractionUtils {
style={{
// filter: drawHalo ? "url(#inkSelectionHalo)" : undefined,
fill: fill ? fill : "none",
- opacity: strokeWidth !== width ? 0.5 : undefined,
+ opacity: 1.0,
+ // opacity: strokeWidth !== width ? 0.5 : undefined,
pointerEvents: pevents as any,
stroke: color ?? "rgb(0, 0, 0)",
strokeWidth: strokeWidth,
@@ -299,8 +300,6 @@ export namespace InteractionUtils {
return points;
case "circle":
-
-
const centerX = (Math.max(left, right) + Math.min(left, right)) / 2;
const centerY = (Math.max(top, bottom) + Math.min(top, bottom)) / 2;
const radius = Math.max(centerX - Math.min(left, right), centerY - Math.min(top, bottom));
@@ -315,7 +314,6 @@ export namespace InteractionUtils {
points.push({ X: newX, Y: y });
}
points.push({ X: Math.sqrt(Math.pow(radius, 2) - (Math.pow((Math.min(top, bottom) - centerY), 2))) + centerX, Y: Math.min(top, bottom) });
-
} else {
for (var x = Math.min(left, right); x < Math.max(left, right); x++) {
const y = Math.sqrt(Math.pow(radius, 2) - (Math.pow((x - centerX), 2))) + centerY;
@@ -327,7 +325,6 @@ export namespace InteractionUtils {
points.push({ X: x, Y: newY });
}
points.push({ X: Math.min(left, right), Y: Math.sqrt(Math.pow(radius, 2) - (Math.pow((Math.min(left, right) - centerX), 2))) + centerY });
-
}
return points;
// case "arrow":
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index 8abfd740c..3579083e4 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,43 @@ 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 => {
- 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 => {
+ LinkManager.userLinkDBs = [];
+ const addLinkToDoc = (link: Doc) => {
+ const a1Prom = link?.anchor1;
+ const a2Prom = link?.anchor2;
+ Promise.all([a1Prom, a2Prom]).then(action((all) => {
+ const a1 = all[0];
+ const a2 = all[1];
+ 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 +71,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());
});
}
@@ -89,9 +112,10 @@ export class LinkManager {
relatedLinker = computedFn(function relatedLinker(this: any, anchor: Doc): Doc[] {
const lfield = Doc.LayoutFieldKey(anchor);
- return DocListCast(anchor[lfield + "-annotations"]).concat(DocListCast(anchor[lfield + "-annotations-timeline"])).reduce((list, anno) =>
+ const related = DocListCast(anchor[lfield + "-annotations"]).concat(DocListCast(anchor[lfield + "-annotations-timeline"])).reduce((list, anno) =>
[...list, ...LinkManager.Instance.relatedLinker(anno)],
- Array.from(Doc.GetProto(anchor)[DirectLinksSym]).slice());// LinkManager.Instance.directLinker(anchor).slice());
+ Array.from(Doc.GetProto(anchor)[DirectLinksSym]).slice());
+ return related;
}, true);
// returns map of group type to anchor's links in that group type
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 d283510b7..6d7f7e8df 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, AclUnset, 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";
@@ -23,6 +23,7 @@ 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;
@@ -85,7 +86,8 @@ 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]
]);
@@ -101,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;
});
}
@@ -153,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);
}
}
});
@@ -366,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>
)
);
@@ -510,7 +513,7 @@ export class SharingManager extends React.Component<{}> {
if (this.myDocAcls) {
const newDocs: Doc[] = [];
- SearchBox.foreachRecursiveDoc(docs, doc => newDocs.push(doc));
+ SearchBox.foreachRecursiveDoc(docs, (depth, doc) => newDocs.push(doc));
docs = newDocs.filter(doc => GetEffectiveAcl(doc) === AclAdmin);
}
@@ -546,7 +549,7 @@ export class SharingManager extends React.Component<{}> {
</select>
) : (
<div className={"permissions-dropdown"}>
- {permissions === SharingPermissions.Add ? "Can Augment" : permissions}
+ {permissions}
</div>
)}
</div>
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index fc36c7e43..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';
@@ -131,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);
@@ -199,7 +199,7 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T
});
}
- if (effectiveAcl === AclAddonly) {
+ if (effectiveAcl === AclAugment) {
added.map(doc => {
if ([AclAdmin, AclEdit].includes(GetEffectiveAcl(doc))) inheritParentAcls(CurrentUserUtils.ActiveDashboard, doc);
doc.context = this.props.Document;
@@ -219,7 +219,7 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T
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.tsx b/src/client/views/DocumentButtonBar.tsx
index df1e6899d..5f09a322c 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -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.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/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index 6a4f55bef..bbf21f22c 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -726,39 +726,36 @@ export class GestureOverlay extends Touchable {
break;
case "circle":
-
+ // Approximation of a circle using 4 Bézier curves in which the constant "c" reduces the maximum radial drift to 0.019608%,
+ // making the curves indistinguishable from a circle.
+ // Source: https://spencermortensen.com/articles/bezier-circle/
+ const c = 0.551915024494;
const centerX = (Math.max(left, right) + Math.min(left, right)) / 2;
const centerY = (Math.max(top, bottom) + Math.min(top, bottom)) / 2;
const radius = Math.max(centerX - Math.min(left, right), centerY - Math.min(top, bottom));
- if (centerX - Math.min(left, right) < centerY - Math.min(top, bottom)) {
- for (var y = Math.min(top, bottom); y < Math.max(top, bottom); y++) {
- const x = Math.sqrt(Math.pow(radius, 2) - (Math.pow((y - centerY), 2))) + centerX;
- this._points.push({ X: x, Y: y });
- }
- for (var y = Math.max(top, bottom); y > Math.min(top, bottom); y--) {
- const x = Math.sqrt(Math.pow(radius, 2) - (Math.pow((y - centerY), 2))) + centerX;
- const newX = centerX - (x - centerX);
- this._points.push({ X: newX, Y: y });
- }
- this._points.push({ X: Math.sqrt(Math.pow(radius, 2) - (Math.pow((Math.min(top, bottom) - centerY), 2))) + centerX, Y: Math.min(top, bottom) });
- this._points.push({ X: Math.sqrt(Math.pow(radius, 2) - (Math.pow((Math.min(top, bottom) - centerY), 2))) + centerX, Y: Math.min(top, bottom) - 1 });
+ // Dividing the circle into four equal sections, and fitting each section to a cubic Bézier curve.
+ this._points.push({ X: centerX - radius, Y: centerY });
+ this._points.push({ X: centerX - radius, Y: centerY + (c * radius) });
+ this._points.push({ X: centerX - (c * radius), Y: centerY + radius });
+ this._points.push({ X: centerX, Y: centerY + radius });
+
+ this._points.push({ X: centerX, Y: centerY + radius });
+ this._points.push({ X: centerX + (c * radius), Y: centerY + radius });
+ this._points.push({ X: centerX + radius, Y: centerY + (c * radius) });
+ this._points.push({ X: centerX + radius, Y: centerY });
+
+ this._points.push({ X: centerX + radius, Y: centerY });
+ this._points.push({ X: centerX + radius, Y: centerY - (c * radius) });
+ this._points.push({ X: centerX + (c * radius), Y: centerY - radius });
+ this._points.push({ X: centerX, Y: centerY - radius });
+
+ this._points.push({ X: centerX, Y: centerY - radius });
+ this._points.push({ X: centerX - (c * radius), Y: centerY - radius });
+ this._points.push({ X: centerX - radius, Y: centerY - (c * radius) });
+ this._points.push({ X: centerX - radius, Y: centerY });
-
- } else {
- for (var x = Math.min(left, right); x < Math.max(left, right); x++) {
- const y = Math.sqrt(Math.pow(radius, 2) - (Math.pow((x - centerX), 2))) + centerY;
- this._points.push({ X: x, Y: y });
- }
- for (var x = Math.max(left, right); x > Math.min(left, right); x--) {
- const y = Math.sqrt(Math.pow(radius, 2) - (Math.pow((x - centerX), 2))) + centerY;
- const newY = centerY - (y - centerY);
- this._points.push({ X: x, Y: newY });
- }
- this._points.push({ X: Math.min(left, right), Y: Math.sqrt(Math.pow(radius, 2) - (Math.pow((Math.min(left, right) - centerX), 2))) + centerY });
- this._points.push({ X: Math.min(left, right), Y: Math.sqrt(Math.pow(radius, 2) - (Math.pow((Math.min(left, right) - centerX), 2))) + centerY - 1 });
-
- }
break;
+
case "line":
if (Math.abs(firstx - lastx) < 20) {
lastx = firstx;
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index 76eb4c142..0127d3080 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -27,7 +27,6 @@ import { LightboxView } from "./LightboxView";
import { MainView } from "./MainView";
import { DocumentLinksButton } from "./nodes/DocumentLinksButton";
import { AnchorMenu } from "./pdf/AnchorMenu";
-import { SearchBox } from "./search/SearchBox";
import { CurrentUserUtils } from "../util/CurrentUserUtils";
import { SettingsManager } from "../util/SettingsManager";
@@ -223,8 +222,11 @@ export class KeyManager {
PromiseValue(Cast(Doc.UserDoc()["tabs-button-tools"], Doc)).then(pv => pv && (pv.onClick as ScriptField).script.run({ this: pv }));
break;
case "f":
- SearchBox.Instance._searchFullDB = "My Stuff";
- SearchBox.Instance.enter(undefined);
+ const searchBtn = Doc.UserDoc().searchBtn as Doc;
+
+ if (searchBtn) {
+ MainView.Instance.selectMenu(searchBtn);
+ }
break;
case "o":
const target = SelectionManager.Views()[0];
diff --git a/src/client/views/InkHandles.tsx b/src/client/views/InkHandles.tsx
index f1eb4b9db..0b24c3c32 100644
--- a/src/client/views/InkHandles.tsx
+++ b/src/client/views/InkHandles.tsx
@@ -11,10 +11,12 @@ import { listSpec } from "../../fields/Schema";
import { List } from "../../fields/List";
import { Cast } from "../../fields/Types";
import { Colors } from "./global/globalEnums";
+import { GestureOverlay } from "./GestureOverlay";
export interface InkHandlesProps {
inkDoc: Doc;
data: InkData;
+ shape?: string;
format: number[];
ScreenToLocalTransform: () => Transform;
}
@@ -68,6 +70,7 @@ export class InkHandles extends React.Component<InkHandlesProps> {
// Accessing the current ink's data and extracting all handle points and handle lines.
const data = this.props.data;
+ const shape = this.props.shape;
const handlePoints: HandlePoint[] = [];
const handleLines: HandleLine[] = [];
if (data.length >= 4) {
diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts
index 76ca5b5ec..d527b2a05 100644
--- a/src/client/views/InkStrokeProperties.ts
+++ b/src/client/views/InkStrokeProperties.ts
@@ -266,8 +266,13 @@ export class InkStrokeProperties {
this.applyFunction((doc: Doc, ink: InkData) => {
const brokenIndices = Cast(doc.brokenInkIndices, listSpec("number"));
if (brokenIndices) {
- brokenIndices.splice(brokenIndices.indexOf(controlIndex), 1);
- doc.brokenInkIndices = brokenIndices;
+ const newBrokenIndices = new List;
+ brokenIndices.forEach(brokenIndex => {
+ if (brokenIndex !== controlIndex) {
+ newBrokenIndices.push(brokenIndex);
+ }
+ });
+ doc.brokenInkIndices = newBrokenIndices;
const [controlPoint, handleA, handleB] = [ink[controlIndex], ink[handleIndexA], ink[handleIndexB]];
const oppositeHandleA = this.rotatePoint(handleA, controlPoint, Math.PI);
const angleDifference = this.angleChange(handleB, oppositeHandleA, controlPoint);
@@ -283,7 +288,7 @@ export class InkStrokeProperties {
*/
@action
rotatePoint = (target: PointData, origin: PointData, angle: number) => {
- let rotatedTarget = { X: target.X - origin.X, Y: target.Y - origin.Y };
+ const rotatedTarget = { X: target.X - origin.X, Y: target.Y - origin.Y };
const newX = Math.cos(angle) * rotatedTarget.X - Math.sin(angle) * rotatedTarget.Y;
const newY = Math.sin(angle) * rotatedTarget.X + Math.cos(angle) * rotatedTarget.Y;
rotatedTarget.X = newX + origin.X;
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 63cefbf67..5fc159f14 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -20,6 +20,7 @@ import { CurrentUserUtils } from "../util/CurrentUserUtils";
import { InkControls } from "./InkControls";
import { InkHandles } from "./InkHandles";
import { Colors } from "./global/globalEnums";
+import { GestureOverlay } from "./GestureOverlay";
type InkDocument = makeInterface<[typeof documentSchema]>;
const InkDocument = makeInterface(documentSchema);
@@ -78,6 +79,8 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
TraceMobx();
this.toggleControlButton();
// Extracting the ink data and formatting information of the current ink stroke.
+ // console.log(InkingStroke.InkShape);
+ const InkShape = GestureOverlay.Instance.InkShape;
const data: InkData = Cast(this.dataDoc[this.fieldKey], InkField)?.inkData ?? [];
const inkDoc: Doc = this.layoutDoc;
const strokeWidth = Number(this.layoutDoc.strokeWidth);
@@ -100,10 +103,10 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
const inkLine = InteractionUtils.CreatePolyline(data, left, top, strokeColor, strokeWidth, strokeWidth, StrCast(this.layoutDoc.strokeBezier), StrCast(this.layoutDoc.fillColor, "none"), StrCast(this.layoutDoc.strokeStartMarker),
StrCast(this.layoutDoc.strokeEndMarker), StrCast(this.layoutDoc.strokeDash), scaleX, scaleY, "", "none", this.props.isSelected() && strokeWidth <= 5 && lineBottom - lineTop > 1 && lineRight - lineLeft > 1, false);
// Thin blue line indicating that the current ink stroke is selected.
- const selectedLine = InteractionUtils.CreatePolyline(data, left - strokeWidth / 3, top - strokeWidth / 3, Colors.MEDIUM_BLUE, strokeWidth / 6, strokeWidth / 6, StrCast(this.layoutDoc.strokeBezier), StrCast(this.layoutDoc.fillColor, "none"),
+ const selectedLine = InteractionUtils.CreatePolyline(data, left, top, Colors.MEDIUM_BLUE, strokeWidth, strokeWidth / 6, StrCast(this.layoutDoc.strokeBezier), StrCast(this.layoutDoc.fillColor, "none"),
StrCast(this.layoutDoc.strokeStartMarker), StrCast(this.layoutDoc.strokeEndMarker), StrCast(this.layoutDoc.strokeDash), scaleX, scaleY, "", "none", this.props.isSelected() && strokeWidth <= 5 && lineBottom - lineTop > 1 && lineRight - lineLeft > 1, false);
// Invisible polygonal line that enables the ink to be selected by the user.
- const clickableLine = InteractionUtils.CreatePolyline(data, left, top, this.props.isSelected() && strokeWidth > 5 ? strokeColor : "transparent", strokeWidth, strokeWidth + 15, StrCast(this.layoutDoc.strokeBezier),
+ const clickableLine = InteractionUtils.CreatePolyline(data, left, top, "transparent", strokeWidth, strokeWidth + 15, StrCast(this.layoutDoc.strokeBezier),
StrCast(this.layoutDoc.fillColor, "none"), "none", "none", undefined, scaleX, scaleY, "", this.props.layerProvider?.(this.props.Document) === false ? "none" : "visiblepainted", false, true);
// Set of points rendered upon the ink that can be added if a user clicks on one.
const addedPoints = InteractionUtils.CreatePoints(data, left, top, strokeColor, strokeWidth, strokeWidth, StrCast(this.layoutDoc.strokeBezier), StrCast(this.layoutDoc.fillColor, "none"), StrCast(this.layoutDoc.strokeStartMarker),
@@ -142,6 +145,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
<InkHandles
inkDoc={inkDoc}
data={data}
+ shape={InkShape}
format={[left, top, scaleX, scaleY, strokeWidth]}
ScreenToLocalTransform={this.props.ScreenToLocalTransform} />
</> : ""}
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index c7c0e7303..f197f5f29 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -13,7 +13,7 @@ import { List } from '../../fields/List';
import { PrefetchProxy } from '../../fields/Proxy';
import { BoolCast, PromiseValue, StrCast } from '../../fields/Types';
import { TraceMobx } from '../../fields/util';
-import { emptyFunction, emptyPath, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, setupMoveUpEvents, simulateMouseClick, Utils } from '../../Utils';
+import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, setupMoveUpEvents, simulateMouseClick, Utils } from '../../Utils';
import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager';
import { DocServer } from '../DocServer';
import { Docs, DocUtils } from '../documents/Documents';
@@ -29,28 +29,27 @@ import { SettingsManager } from '../util/SettingsManager';
import { SharingManager } from '../util/SharingManager';
import { SnappingManager } from '../util/SnappingManager';
import { Transform } from '../util/Transform';
-import { undoBatch, UndoManager } from '../util/UndoManager';
import { TimelineMenu } from './animationtimeline/TimelineMenu';
import { CollectionDockingView } from './collections/CollectionDockingView';
import { MarqueeOptionsMenu } from './collections/collectionFreeForm/MarqueeOptionsMenu';
import { CollectionLinearView } from './collections/CollectionLinearView';
import { CollectionMenu } from './collections/CollectionMenu';
import { CollectionViewType } from './collections/CollectionView';
+import "./collections/TreeView.scss";
import { ContextMenu } from './ContextMenu';
import { DictationOverlay } from './DictationOverlay';
import { DocumentDecorations } from './DocumentDecorations';
import { GestureOverlay } from './GestureOverlay';
import { MENU_PANEL_WIDTH, SEARCH_PANEL_HEIGHT } from './global/globalCssVariables.scss';
+import { Colors } from './global/globalEnums';
import { KeyManager } from './GlobalKeyHandler';
import { InkStrokeProperties } from './InkStrokeProperties';
import { LightboxView } from './LightboxView';
import { LinkMenu } from './linking/LinkMenu';
import "./MainView.scss";
-import "./collections/TreeView.scss";
import { AudioBox } from './nodes/AudioBox';
import { DocumentLinksButton } from './nodes/DocumentLinksButton';
-import { DocumentView, DocumentViewProps, DocAfterFocusFunc } from './nodes/DocumentView';
-import { FieldViewProps } from './nodes/FieldView';
+import { DocumentView } from './nodes/DocumentView';
import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
import { LinkDescriptionPopup } from './nodes/LinkDescriptionPopup';
import { LinkDocPreview } from './nodes/LinkDocPreview';
@@ -61,10 +60,8 @@ import { OverlayView } from './OverlayView';
import { AnchorMenu } from './pdf/AnchorMenu';
import { PreviewCursor } from './PreviewCursor';
import { PropertiesView } from './PropertiesView';
-import { SearchBox } from './search/SearchBox';
-import { DefaultStyleProvider, DashboardStyleProvider, StyleProp } from './StyleProvider';
+import { DashboardStyleProvider, DefaultStyleProvider } from './StyleProvider';
import { TopBar } from './topbar/TopBar';
-import { Colors } from './global/globalEnums';
const _global = (window /* browser */ || global /* node */) as any;
@observer
@@ -107,7 +104,9 @@ export class MainView extends React.Component {
new InkStrokeProperties();
this._sidebarContent.proto = undefined;
if (!MainView.Live) {
- DocServer.setPlaygroundFields(["x", "y", "dataTransition", "_autoHeight", "_showSidebar", "showSidebar", "_sidebarWidthPercent", "_width", "_height", "width", "height", "_viewTransition", "_panX", "_panY", "_viewScale", "_scrollTop", "hidden", "_curPage", "_viewType", "_chromeHidden", "nativeWidth", "_nativeWidth"]); // can play with these fields on someone else's
+ DocServer.setPlaygroundFields(["dataTransition", "treeViewOpen", "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)));
@@ -182,12 +181,6 @@ export class MainView extends React.Component {
const targets = document.elementsFromPoint(e.x, e.y);
if (targets.length) {
const targClass = targets[0].className.toString();
- // if (SearchBox.Instance._searchbarOpen || SearchBox.Instance.open) {
- // const check = targets.some((thing) =>
- // (thing.className === "collectionSchemaView-searchContainer" || (thing as any)?.dataset.icon === "filter" ||
- // thing.className === "collectionSchema-header-menuOptions"));
- // !check && SearchBox.Instance.resetSearch(true);
- // }
!targClass.includes("contextMenu") && ContextMenu.Instance.closeMenu();
!["timeline-menu-desc", "timeline-menu-item", "timeline-menu-input"].includes(targClass) && TimelineMenu.Instance.closeMenu();
}
@@ -394,10 +387,6 @@ export class MainView extends React.Component {
case "Settings":
SettingsManager.Instance.open();
break;
- case "Catalog":
- SearchBox.Instance._searchFullDB = "My Stuff";
- SearchBox.Instance.enter(undefined);
- break;
case "Help":
break;
default:
@@ -537,7 +526,7 @@ export class MainView extends React.Component {
</svg>;
}
- @computed get search() {
+ @computed get topbar() {
TraceMobx();
return <div className="mainView-topbar">
<TopBar />
@@ -591,7 +580,7 @@ export class MainView extends React.Component {
<GroupManager />
<GoogleAuthenticationManager />
<DocumentDecorations boundsLeft={this.leftOffset} boundsTop={this.topOffset} />
- {this.search}
+ {this.topbar}
{LinkDescriptionPopup.descriptionPopup ? <LinkDescriptionPopup /> : null}
{DocumentLinksButton.LinkEditorDocView ? <LinkMenu docView={DocumentLinksButton.LinkEditorDocView} changeFlyout={emptyFunction} /> : (null)}
{LinkDocPreview.LinkInfo ? <LinkDocPreview {...LinkDocPreview.LinkInfo} /> : (null)}
@@ -609,7 +598,7 @@ export class MainView extends React.Component {
{this.snapLines}
<div className="mainView-webRef" ref={this.makeWebRef} />
<LightboxView PanelWidth={this._windowWidth} PanelHeight={this._windowHeight} maxBorder={[200, 50]} />
- </div>);
+ </div >);
}
makeWebRef = (ele: HTMLDivElement) => {
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.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/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index c0d39b2a2..cae08e1f4 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -170,7 +170,7 @@ export class CollectionDockingView extends CollectionSubView(doc => doc) {
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);
+ !DocListCast(document.aliases).some(a => aliasDocList.includes(a)) && Doc.AddDocToList(alias, "data-all", document);//alias !== instance.props.Document ? Doc.MakeAlias(document) : document);
});
}
const docContentConfig = CollectionDockingView.makeDocumentConfig(document, panelName);
diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx
index 380f82813..1efea96be 100644
--- a/src/client/views/collections/CollectionSchemaView.tsx
+++ b/src/client/views/collections/CollectionSchemaView.tsx
@@ -556,7 +556,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
style={{
overflow: this.props.scrollOverflow === true ? "scroll" : undefined, backgroundColor: "white",
pointerEvents: this.props.Document._searchDoc !== undefined && !this.props.isContentActive() && !SnappingManager.GetIsDragging() ? "none" : undefined,
- width: name === "collectionSchemaView-searchContainer" ? "auto" : this.props.PanelWidth() || "100%", height: this.props.PanelHeight() || "100%", position: "relative",
+ width: this.props.PanelWidth() || "100%", height: this.props.PanelHeight() || "100%", position: "relative",
}} >
<div className="collectionSchemaView-tableContainer"
style={{ width: `calc(100% - ${this.previewWidth()}px)` }}
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index a9b5ce465..2f07c0241 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -80,8 +80,8 @@ 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
}
@@ -93,7 +93,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
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;
+ this.props.docFilters().length || this.props.docRangeFilters().length ? "inheritsFilter" : undefined
searchFilterDocs = () => this.props.searchFilterDocs?.() ?? DocListCast(this.props.Document._searchFilterDocs);
@computed.struct get childDocs() {
TraceMobx();
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index 3eece0086..a78034dca 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -1,10 +1,10 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { action, computed, reaction, IReactionDisposer, observable } from "mobx";
import { observer } from "mobx-react";
-import { DataSym, Doc, DocListCast, HeightSym, Opt, WidthSym } from '../../../fields/Doc';
+import { DataSym, Doc, DocListCast, HeightSym, Opt, WidthSym, StrListCast } from '../../../fields/Doc';
import { Id } from '../../../fields/FieldSymbols';
import { List } from '../../../fields/List';
-import { Document } from '../../../fields/Schema';
+import { Document, listSpec } from '../../../fields/Schema';
import { ScriptField } from '../../../fields/ScriptField';
import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
import { TraceMobx } from '../../../fields/util';
@@ -27,6 +27,7 @@ import { TreeView } from "./TreeView";
import React = require("react");
import { InkTool } from '../../../fields/InkField';
import { CurrentUserUtils } from '../../util/CurrentUserUtils';
+import { CollectionView, CollectionViewType } from './CollectionView';
const _global = (window /* browser */ || global /* node */) as any;
export type collectionTreeViewProps = {
@@ -52,6 +53,7 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
@computed get treeChildren() { TraceMobx(); return this.props.childDocuments || this.childDocs; }
@computed get outlineMode() { return this.doc.treeViewType === "outline"; }
@computed get fileSysMode() { return this.doc.treeViewType === "fileSystem"; }
+ @computed get dashboardMode() { return this.doc === Doc.UserDoc().myDashboards; }
// these should stay in synch with counterparts in DocComponent.ts ViewBoxAnnotatableComponent
@observable _isAnyChildContentActive = false;
@@ -215,6 +217,11 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
/>
</div>;
}
+ childContextMenuItems = () => {
+ const customScripts = Cast(this.doc.childContextMenuScripts, listSpec(ScriptField), []);
+ const filterScripts = Cast(this.doc.childContextMenuFilters, listSpec(ScriptField), []);
+ return StrListCast(this.doc.childContextMenuLabels).map((label, i) => ({ script: customScripts[i], filter: filterScripts[i], label }));
+ }
@computed get treeViewElements() {
TraceMobx();
const dropAction = StrCast(this.doc.childDropAction) as dropActionType;
@@ -247,7 +254,9 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
this.whenChildContentsActiveChanged,
this.props.dontRegisterView || Cast(this.props.Document.childDontRegisterViews, "boolean", null),
this.observerHeight,
- this.unobserveHeight);
+ this.unobserveHeight,
+ this.childContextMenuItems()
+ );
}
@computed get titleBar() {
const hideTitle = this.props.treeViewHideTitle || this.doc.treeViewHideTitle;
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 25a37ed71..2485c0f6a 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -70,6 +70,7 @@ export interface CollectionViewProps extends FieldViewProps {
childDocumentsActive?: () => boolean;// whether child documents can be dragged if collection can be dragged (eg., in a when a Pile document is in startburst mode)
childFitWidth?: () => boolean;
childOpacity?: () => number;
+ childContextMenuItems?: () => { script: ScriptField, label: string }[];
childHideTitle?: () => boolean; // whether to hide the documentdecorations title for children
childHideDecorationTitle?: () => boolean;
childLayoutTemplate?: () => (Doc | undefined);// specify a layout Doc template to use for children of the collection
@@ -186,15 +187,20 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab
}
!Doc.UserDoc().noviceMode && optionItems.push({ description: `${this.rootDoc.isInPlaceContainer ? "Unset" : "Set"} inPlace Container`, event: () => this.rootDoc.isInPlaceContainer = !this.rootDoc.isInPlaceContainer, icon: "project-diagram" });
- optionItems.push({
- description: "Create Branch", event: async () => this.props.addDocTab(await BranchCreate(this.rootDoc), "add:right"), icon: "project-diagram"
- });
- optionItems.push({
- description: "Pull Master", event: () => BranchTask(this.rootDoc, "pull"), icon: "project-diagram"
- });
- optionItems.push({
- description: "Merge Branches", event: () => BranchTask(this.rootDoc, "merge"), icon: "project-diagram"
- });
+ if (!Doc.UserDoc().noviceMode) {
+ optionItems.push({
+ description: "Create Branch", event: async () => this.props.addDocTab(await BranchCreate(this.rootDoc), "add:right"), icon: "project-diagram"
+ });
+ optionItems.push({
+ description: "Pull Master", event: () => BranchTask(this.rootDoc, "pull"), icon: "project-diagram"
+ });
+ optionItems.push({
+ description: "Merge Branches", event: () => BranchTask(this.rootDoc, "merge"), icon: "project-diagram"
+ });
+ }
+ if (this.Document._viewType === CollectionViewType.Docking) {
+ optionItems.push({ description: "Create Dashboard", event: () => CurrentUserUtils.createNewDashboard(Doc.UserDoc()), icon: "project-diagram" });
+ }
!options && cm.addItem({ description: "Options...", subitems: optionItems, icon: "hand-point-right" });
@@ -236,7 +242,7 @@ 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) && this._subView?.IsFiltered()
+ return this.props.Document.viewType !== CollectionViewType.Docking && !Doc.IsSystem(this.props.Document) && this._subView?.IsFiltered();
}
@observable _subView: any = undefined;
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index 7b77e859d..e6e71a3c4 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 =>
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index e33c39d20..3ee9dbf59 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -1,7 +1,7 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { action, computed, IReactionDisposer, observable, reaction } from "mobx";
import { observer } from "mobx-react";
-import { DataSym, Doc, DocListCast, DocListCastOrNull, Field, HeightSym, Opt, WidthSym } from '../../../fields/Doc';
+import { DataSym, Doc, DocListCast, DocListCastOrNull, Field, HeightSym, Opt, WidthSym, StrListCast } from '../../../fields/Doc';
import { Id } from '../../../fields/FieldSymbols';
import { List } from '../../../fields/List';
import { RichTextField } from '../../../fields/RichTextField';
@@ -54,6 +54,7 @@ export interface TreeViewProps {
indentDocument?: (editTitle: boolean) => void;
outdentDocument?: (editTitle: boolean) => void;
ScreenToLocalTransform: () => Transform;
+ contextMenuItems: { script: ScriptField, filter: ScriptField, label: string }[];
dontRegisterView?: boolean;
styleProvider?: StyleProviderFunc | undefined;
treeViewHideHeaderFields: () => boolean;
@@ -99,13 +100,14 @@ export class TreeView extends React.Component<TreeViewProps> {
@observable _dref: DocumentView | undefined | null;
get displayName() { return "TreeView(" + this.props.document.title + ")"; } // this makes mobx trace() statements more descriptive
get defaultExpandedView() {
- return this.props.treeView.fileSysMode ? (this.doc.isFolder ? this.fieldKey : "aliases") :
- this.props.treeView.outlineMode || this.childDocs ? this.fieldKey : Doc.UserDoc().noviceMode ? "layout" : StrCast(this.props.treeView.doc.treeViewExpandedView, "fields");
+ return this.doc.viewType === CollectionViewType.Docking ? this.fieldKey :
+ this.props.treeView.fileSysMode ? (this.doc.isFolder ? this.fieldKey : "layout") :
+ this.props.treeView.outlineMode || this.childDocs ? this.fieldKey : Doc.UserDoc().noviceMode ? "layout" : StrCast(this.props.treeView.doc.treeViewExpandedView, "fields");
}
@computed get doc() { return this.props.document; }
@computed get treeViewOpen() { return (!this.treeViewOpenIsTransient && Doc.GetT(this.doc, "treeViewOpen", "boolean", true)) || this._transientOpenState; }
- @computed get treeViewExpandedView() { return StrCast(this.doc.treeViewExpandedView, this.defaultExpandedView); }
+ @computed get treeViewExpandedView() { return this.validExpandViewTypes.includes(StrCast(this.doc.treeViewExpandedView)) ? StrCast(this.doc.treeViewExpandedView) : this.defaultExpandedView; }
@computed get MAX_EMBED_HEIGHT() { return NumCast(this.props.containerCollection.maxEmbedHeight, 200); }
@computed get dataDoc() { return this.doc[DataSym]; }
@computed get layoutDoc() { return Doc.Layout(this.doc); }
@@ -336,7 +338,7 @@ export class TreeView extends React.Component<TreeViewProps> {
this.props.dropAction, this.props.addDocTab, this.titleStyleProvider, this.props.ScreenToLocalTransform, this.props.isContentActive,
this.props.panelWidth, this.props.renderDepth, this.props.treeViewHideHeaderFields,
[...this.props.renderedIds, doc[Id]], this.props.onCheckedClick, this.props.onChildClick, this.props.skipFields, false, this.props.whenChildContentsActiveChanged,
- this.props.dontRegisterView, emptyFunction, emptyFunction);
+ this.props.dontRegisterView, emptyFunction, emptyFunction, this.childContextMenuItems());
} else {
contentElement = <EditableView key="editableView"
contents={contents !== undefined ? Field.toString(contents as Field) : "null"}
@@ -418,7 +420,7 @@ export class TreeView extends React.Component<TreeViewProps> {
StrCast(this.doc.childDropAction, this.props.dropAction) as dropActionType, this.props.addDocTab, this.titleStyleProvider, this.props.ScreenToLocalTransform,
this.props.isContentActive, this.props.panelWidth, this.props.renderDepth, this.props.treeViewHideHeaderFields,
[...this.props.renderedIds, this.doc[Id]], this.props.onCheckedClick, this.props.onChildClick, this.props.skipFields, false, this.props.whenChildContentsActiveChanged,
- this.props.dontRegisterView, emptyFunction, emptyFunction)}
+ this.props.dontRegisterView, emptyFunction, emptyFunction, this.childContextMenuItems())}
</ul >;
} else if (this.treeViewExpandedView === "fields") {
return <ul key={this.doc[Id] + this.doc.title}>
@@ -475,16 +477,20 @@ export class TreeView extends React.Component<TreeViewProps> {
</div>;
}
+ @computed get validExpandViewTypes() {
+ if (this.doc.viewType === CollectionViewType.Docking) return [this.fieldKey];
+ const annos = () => DocListCast(this.doc[this.fieldKey + "-annotations"]).length ? "annotations" : "";
+ const links = () => DocListCast(this.doc.links).length ? "links" : "";
+ const data = () => this.childDocs && !this.props.treeView.dashboardMode ? this.fieldKey : "";
+ const aliases = () => this.props.treeView.dashboardMode ? "" : "aliases";
+ const fields = () => Doc.UserDoc().noviceMode ? "" : "fields";
+ return [data(), "layout", ...(this.props.treeView.fileSysMode ? [aliases(), links(), annos()] : []), fields()].filter(m => m);
+ }
@action
expandNextviewType = () => {
if (this.treeViewOpen && !this.doc.isFolder && !this.props.treeView.outlineMode && !this.doc.treeViewExpandedViewLock) {
- const next = (modes: any[]) => modes[(modes.indexOf(StrCast(this.doc.treeViewExpandedView)) + 1) % modes.length];
- const annos = () => DocListCast(this.doc[this.fieldKey + "-annotations"]).length ? "annotations" : "";
- const links = () => DocListCast(this.doc.links).length ? "links" : "";
- const children = () => this.childDocs ? this.fieldKey : "";
- this.doc.treeViewExpandedView = next(this.props.treeView.fileSysMode ?
- (Doc.UserDoc().noviceMode ? ["layout", "aliases"] : ["layout", "aliases", "fields"]) :
- (Doc.UserDoc().noviceMode ? [children(), "layout"] : [children(), "fields", "layout", links(), annos()]).filter(mode => mode));
+ const next = (modes: any[]) => modes[(modes.indexOf(StrCast(this.treeViewExpandedView)) + 1) % modes.length];
+ this.doc.treeViewExpandedView = next(this.validExpandViewTypes);
}
this.treeViewOpen = true;
}
@@ -507,11 +513,19 @@ export class TreeView extends React.Component<TreeViewProps> {
}
contextMenuItems = () => {
const makeFolder = { script: ScriptField.MakeFunction(`scriptContext.makeFolder()`, { scriptContext: "any" })!, label: "New Folder" };
- return this.doc.isFolder ? [makeFolder] :
+ const openAlias = { script: ScriptField.MakeFunction(`openOnRight(getAlias(self))`)!, label: "Open Alias" };
+ const focusDoc = { script: ScriptField.MakeFunction(`DocFocusOrOpen(self)`)!, label: "Focus or Open" };
+ return [...this.props.contextMenuItems.filter(mi => !mi.filter ? true : mi.filter.script.run({ doc: this.doc })?.result), ... (this.doc.isFolder ? [makeFolder] :
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" }];
+ [openAlias, makeFolder] :
+ this.doc.viewType === CollectionViewType.Docking ? [] :
+ [openAlias, focusDoc])];
+ }
+ childContextMenuItems = () => {
+ const customScripts = Cast(this.doc.childContextMenuScripts, listSpec(ScriptField), []);
+ const customFilters = Cast(this.doc.childContextMenuFilters, listSpec(ScriptField), []);
+ return StrListCast(this.doc.childContextMenuLabels).map((label, i) => ({ script: customScripts[i], filter: customFilters[i], label }));
}
onChildClick = () => this.props.onChildClick?.() ?? (this._editTitleScript?.() || ScriptCast(this.doc.treeChildClick));
@@ -817,7 +831,8 @@ export class TreeView extends React.Component<TreeViewProps> {
whenChildContentsActiveChanged: (isActive: boolean) => void,
dontRegisterView: boolean | undefined,
observerHeight: (ref: any) => void,
- unobserveHeight: (ref: any) => void
+ unobserveHeight: (ref: any) => void,
+ contextMenuItems: ({ script: ScriptField, filter: ScriptField, label: string }[])
) {
const viewSpecScript = Cast(conainerCollection.viewSpecScript, ScriptField);
if (viewSpecScript) {
@@ -882,6 +897,7 @@ export class TreeView extends React.Component<TreeViewProps> {
parentTreeView={parentTreeView}
observeHeight={observerHeight}
unobserveHeight={unobserveHeight}
+ contextMenuItems={contextMenuItems}
/>;
});
}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index ba6222605..d09d9b9d7 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",
@@ -257,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);
@@ -1208,7 +1210,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
if (this.props.Document.annotationOn) {
return this.rootDoc;
}
- const anchor = Docs.Create.TextanchorDocument({ title: StrCast(this.layoutDoc._viewType), annotationOn: this.rootDoc });
+ const anchor = Docs.Create.TextanchorDocument({ title: "ViewSpec - " + StrCast(this.layoutDoc._viewType), annotationOn: this.rootDoc });
const proto = Doc.GetProto(anchor);
proto[ViewSpecPrefix + "_viewType"] = this.layoutDoc._viewType;
proto.docFilters = ObjectField.MakeCopy(this.layoutDoc.docFilters as ObjectField) || new List<string>([]);
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 846d28214..d0243850f 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";
@@ -92,7 +92,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
cm.setDefaultItem("?", (str: string) => this.props.addDocTab(
Docs.Create.WebDocument(`https://bing.com/search?q=${str}`, { _width: 400, x, y, _height: 512, _nativeWidth: 850, title: "bing", useCors: true }), "add:right"));
- cm.displayMenu(this._downX, this._downY);
+ cm.displayMenu(this._downX, this._downY, undefined, true);
e.stopPropagation();
} else
if (e.key === "u" && this.props.ungroup) {
@@ -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/collectionSchema/CollectionSchemaHeaders.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx
index b2115b22e..aaa50ba67 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx
@@ -424,7 +424,7 @@ export class KeysDropdown extends React.Component<KeysDropdownProps> {
e.target.checked === true ? Doc.setDocFilter(this.props.Document, this._key, key, "check") : Doc.setDocFilter(this.props.Document, this._key, key, "remove");
e.target.checked === true ? this.closeResultsVisibility = "contents" : console.log("");
e.target.checked === true ? this.props.col.setColor("green") : this.updateFilter();
- e.target.checked === true && SearchBox.Instance.filter === true ? Doc.setDocFilter(docs[0], this._key, key, "check") : Doc.setDocFilter(docs[0], this._key, key, "remove");
+ e.target.checked === true ? Doc.setDocFilter(docs[0], this._key, key, "check") : Doc.setDocFilter(docs[0], this._key, key, "remove");
}}
checked={bool}
/>
diff --git a/src/client/views/collections/collectionSchema/SchemaTable.tsx b/src/client/views/collections/collectionSchema/SchemaTable.tsx
index de08c327a..abe549072 100644
--- a/src/client/views/collections/collectionSchema/SchemaTable.tsx
+++ b/src/client/views/collections/collectionSchema/SchemaTable.tsx
@@ -197,6 +197,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> {
<div onClick={e => this.changeSorting(col)} style={{ width: 21, padding: 1, display: "inline", zIndex: 1, background: "inherit", cursor: "hand" }}>
<FontAwesomeIcon icon={sortIcon} size="lg" />
</div>
+ {this.props.Document._chromeHidden || this.props.addDocument == returnFalse ? undefined : <div className="collectionSchemaView-addRow" onClick={this.createRow}>+ new</div>}
</div>;
return {
@@ -561,7 +562,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> {
onPointerDown={this.props.onPointerDown} onClick={this.props.onClick} onWheel={e => this.props.active(true) && e.stopPropagation()}
onDrop={e => this.props.onDrop(e, {})} onContextMenu={this.onContextMenu} >
{this.reactTable}
- {this.props.Document._chromeHidden ? undefined : <div className="collectionSchemaView-addRow" onClick={this.createRow}>+ new</div>}
+ {this.props.Document._chromeHidden || this.props.addDocument == returnFalse ? undefined : <div className="collectionSchemaView-addRow" onClick={this.createRow}>+ new</div>}
{!this._showDoc ? (null) :
<div className="collectionSchemaView-documentPreview" ref="overlay"
style={{
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/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx
index b63174e54..7648e866e 100644
--- a/src/client/views/nodes/DocumentLinksButton.tsx
+++ b/src/client/views/nodes/DocumentLinksButton.tsx
@@ -114,7 +114,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
DocumentLinksButton.StartLink = this.props.View.props.Document;
DocumentLinksButton.StartLinkView = this.props.View;
}
- };
+ }
}));
}
@@ -291,7 +291,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
(null)
}
</div>
- )
+ );
}
render() {
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index bea219831..a29f2f662 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -420,7 +420,7 @@ 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
+ // 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]);
});
@@ -664,7 +664,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
if (!cm || (e as any)?.nativeEvent?.SchemaHandled) return;
const customScripts = Cast(this.props.Document.contextMenuScripts, listSpec(ScriptField), []);
- Cast(this.props.Document.contextMenuLabels, listSpec("string"), []).forEach((label, i) =>
+ StrListCast(this.Document.contextMenuLabels).forEach((label, i) =>
cm.addItem({ description: label, event: () => customScripts[i]?.script.run({ documentView: this, this: this.layoutDoc, scriptContext: this.props.scriptContext, self: this.rootDoc }), icon: "sticky-note" }));
this.props.contextMenuItems?.().forEach(item =>
item.label && cm.addItem({ description: item.label, event: () => item.script.script.run({ this: this.layoutDoc, scriptContext: this.props.scriptContext, self: this.rootDoc }), icon: "sticky-note" }));
diff --git a/src/client/views/nodes/FilterBox.tsx b/src/client/views/nodes/FilterBox.tsx
index c892a9f6c..beefc4a82 100644
--- a/src/client/views/nodes/FilterBox.tsx
+++ b/src/client/views/nodes/FilterBox.tsx
@@ -101,7 +101,7 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps, FilterBoxDoc
if (this._loaded && targetDoc) {
const allDocs = new Set<Doc>();
const activeTabs = DocListCast(targetDoc.data);
- SearchBox.foreachRecursiveDoc(activeTabs, (doc: Doc) => allDocs.add(doc));
+ SearchBox.foreachRecursiveDoc(activeTabs, (depth, doc) => allDocs.add(doc));
setTimeout(action(() => this._allDocs = Array.from(allDocs)));
}
return this._allDocs;
@@ -482,7 +482,7 @@ Scripting.addGlobal(function determineCheckedState(layoutDoc: Doc, facetHeader:
Scripting.addGlobal(function readFacetData(layoutDoc: Doc, facetHeader: string) {
const allCollectionDocs = new Set<Doc>();
const activeTabs = DocListCast(layoutDoc.data);
- SearchBox.foreachRecursiveDoc(activeTabs, (doc: Doc) => allCollectionDocs.add(doc));
+ SearchBox.foreachRecursiveDoc(activeTabs, (depth: number, doc: Doc) => allCollectionDocs.add(doc));
const set = new Set<string>();
if (facetHeader === "tags") allCollectionDocs.forEach(child => Field.toString(child[facetHeader] as Field).split(":").forEach(key => set.add(key)));
else allCollectionDocs.forEach(child => set.add(Field.toString(child[facetHeader] as Field)));
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 41e335be2..e3c09646f 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -219,7 +219,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
}
sidebarWidth = () => !this.SidebarShown ? 0 :
this._previewWidth ? PDFBox.openSidebarWidth :
- (NumCast(this.layoutDoc.nativeWidth) - Doc.NativeWidth(this.dataDoc)) * this.props.PanelWidth() / NumCast(this.layoutDoc.nativeWidth);
+ (NumCast(this.layoutDoc.nativeWidth) - Doc.NativeWidth(this.dataDoc)) * this.props.PanelWidth() / NumCast(this.layoutDoc.nativeWidth)
specificContextMenu = (e: React.MouseEvent): void => {
const funcs: ContextMenuProps[] = [];
diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx
index 0e235a62d..f0db0b594 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,9 +218,8 @@ 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);
@@ -249,18 +248,24 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
};
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/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index 6d701e4d1..142193476 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -52,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;
@@ -82,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(),
@@ -161,6 +164,7 @@ 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);
}
@@ -175,19 +179,19 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
return focusSpeed;
}
}
- this._initialScroll = NumCast(doc.y);
+ this._initialScroll = NumCast(this.layoutDoc._scrollTop);
return 0;
}
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;
}
@@ -237,8 +241,10 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
iframe?.contentDocument.addEventListener("pointerdown", this.iframeDown);
this._scrollHeight = Math.max(this.scrollHeight, iframe?.contentDocument.body.scrollHeight);
setTimeout(action(() => this._scrollHeight = Math.max(this.scrollHeight, iframe?.contentDocument?.body.scrollHeight || 0)), 5000);
- if (this._initialScroll !== undefined && this._outerRef.current) {
- this._outerRef.current.scrollTop = this._initialScroll;
+ const initialScroll = this._initialScroll;
+ if (initialScroll !== undefined && this._outerRef.current) {
+ // bcz: not sure why this happens, but if the webpage isn't ready yet, it's scroll height seems to be limited. So we need to wait tp set scroll location to what we want.
+ setTimeout(() => this._outerRef.current!.scrollTop = initialScroll);
this._initialScroll = undefined;
}
iframe.setAttribute("enable-annotation", "true");
@@ -299,9 +305,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;
@@ -313,9 +320,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;
@@ -326,25 +334,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);
}
@@ -422,7 +431,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}
@@ -437,9 +446,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) => {
@@ -475,7 +489,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
sidebarWidth = () => !this.SidebarShown ? 0 :
this._previewWidth ? WebBox.openSidebarWidth :
(NumCast(this.layoutDoc.nativeWidth) - Doc.NativeWidth(this.dataDoc)) * this.props.PanelWidth() /
- NumCast(this.layoutDoc.nativeWidth);
+ 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" : "")}
@@ -554,7 +568,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}
@@ -563,7 +577,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
</div >
<SidebarAnnos ref={this._sidebarRef}
{...this.props}
- fieldKey={this.fieldKey}
+ fieldKey={this.fieldKey + "-" + this._urlHash}
rootDoc={this.rootDoc}
layoutDoc={this.layoutDoc}
dataDoc={this.dataDoc}
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 8f0539103..12064fccf 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';
@@ -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("#")) {
@@ -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
@@ -535,7 +535,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
addStyleSheetRule(FormattedTextBox._userStyleSheet, "UM-" + Doc.CurrentUserEmail.replace(".", "").replace("@", ""), { opacity: "0.1" });
const min = Math.round(Date.now() / 1000 / 60);
numberRange(10).map(i => addStyleSheetRule(FormattedTextBox._userStyleSheet, "UM-min-" + (min - i), { opacity: ((10 - i - 1) / 10).toString() }));
- setTimeout(() => this.updateHighlights());
+ setTimeout(this.updateHighlights);
}
if (FormattedTextBox._highlights.indexOf("By Recent Hour") !== -1) {
addStyleSheetRule(FormattedTextBox._userStyleSheet, "UM-" + Doc.CurrentUserEmail.replace(".", "").replace("@", ""), { opacity: "0.1" });
@@ -882,7 +882,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
}
},
);
- if (this._recording) setTimeout(() => this.recordDictation());
+ if (this._recording) setTimeout(this.recordDictation);
}
var quickScroll: string | undefined = "";
this._disposers.scroll = reaction(() => NumCast(this.layoutDoc._scrollTop),
@@ -1402,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)));
@@ -1431,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);
+ scrollHeight && 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...
+ }
}
}
}
diff --git a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
index d5c77786c..76a5675de 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, AclSelfEdit } 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,42 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
return false;
};
+ const canEdit = (state: any) => {
+ switch (GetEffectiveAcl(props.Document)) {
+ case AclAugment: return false;
+ case AclSelfEdit:
+ for (var i = state.selection.from; i < state.selection.to; i++) {
+ const marks = state.doc.resolve(i)?.marks?.();
+ if (marks?.some((mark: any) => mark.type === schema.marks.user_mark && mark.attrs.userid !== Doc.CurrentUserEmail)) {
+ return false;
+ }
+ }
+ break;
+ }
+ 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 +114,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 +140,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 +160,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 +188,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 +217,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 +258,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/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index e7911e8f8..ee553fd43 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -182,17 +182,18 @@ export class PDFViewer extends React.Component<IViewerProps> {
scrollFocus = (doc: Doc, smooth: boolean) => {
const mainCont = this._mainCont.current;
let focusSpeed: Opt<number>;
- if (doc !== this.props.rootDoc && mainCont && this._pdfViewer) {
+ if (doc !== this.props.rootDoc && mainCont) {
const windowHeight = this.props.PanelHeight() / (this.props.scaling?.() || 1);
const scrollTo = Utils.scrollIntoView(NumCast(doc.y), doc[HeightSym](), NumCast(this.props.layoutDoc._scrollTop), windowHeight, .1 * windowHeight);
if (scrollTo !== undefined) {
focusSpeed = 500;
- if (smooth) smoothScroll(focusSpeed, mainCont, scrollTo);
+ if (!this._pdfViewer) this._initialScroll = scrollTo;
+ else if (smooth) smoothScroll(focusSpeed, mainCont, scrollTo);
else this._mainCont.current?.scrollTo({ top: Math.abs(scrollTo || 0) });
}
} else {
- this._initialScroll = NumCast(doc.y);
+ this._initialScroll = NumCast(this.props.layoutDoc._scrollTop);
}
return focusSpeed;
}
diff --git a/src/client/views/search/SearchBox.scss b/src/client/views/search/SearchBox.scss
index 6a2fe6f19..2586ef2ee 100644
--- a/src/client/views/search/SearchBox.scss
+++ b/src/client/views/search/SearchBox.scss
@@ -2,141 +2,109 @@
@import "./NaviconButton.scss";
.searchBox-container {
- display: flex;
- flex-direction: column;
width: 100%;
height: 100%;
- position: relative;
font-size: 10px;
line-height: 1;
- overflow-y: auto;
- overflow-x: visible;
- background: lightgrey;
- overflow: visible;
+ background: none;
z-index: 1000;
+ padding: 0px;
+ cursor: default;
.searchBox-bar {
- height: $searchpanel-height;
+ width: 100%;
+ height: 35px;
display: flex;
justify-content: center;
align-items: center;
- background-color: $dark-gray;
+ background-color: none;
+ padding: 5px;
- .searchBox-lozenges {
- position: absolute;
- left: 15;
- display: flex;
-
- .searchBox-lozenge-user,
- .searchBox-lozenge-dashboard,
- .searchBox-lozenge {
- height: 18px;
- padding: 4px;
- margin-right: 5px;
- display: flex;
- align-items: center;
- border: grey 1px solid;
- .searchBox-logoff,
- .searchBox-dashboards {
- border-radius: 3px;
- background: olivedrab;
- color: white;
- display: none;
- margin-left: 5px;
- padding: 1px 2px 1px 2px;
- cursor: pointer;
- }
- .searchBox-logoff {
- background: red;
- }
-
- .searchBox-dashSelect{
- border: none;
- background-color: transparent;
+ .searchBox-type {
+ display: block;
+ width: 55px;
+ outline: none;
+ padding: 1px 5px 1px 5px;
+ color: black;
+ height: 25px;
+ border: 1px solid black;
+ border-right: 0px;
+ }
- &:hover {
- cursor: pointer;
- }
- }
- }
- .searchBox-lozenge-user:hover {
- .searchBox-logoff {
- display:inline-block;
- }
- }
- .searchBox-lozenge-dashboard:hover {
- .searchBox-dashboards {
- display:inline-block;
- }
- }
+ .searchBox-input {
+ display: block;
+ width: calc(100% - 55px);
+ outline: none;
+ padding: 1px 5px 1px 5px;
+ color: black;
+ height: 25px;
+ border: 1px solid black;
}
- .searchBox-query {
- position: relative;
+ }
+
+ .searchBox-results-container {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ height: 100%;
+ justify-content: "center";
+
+ .searchBox-results-count {
display: flex;
- width: 450;
+ color: gray;
+ margin-left: 5px;
}
- .searchBox-barChild {
+
+ .searchBox-results-scroll-view {
+ margin-top: 10px;
+ display: inline-block;
+ width: 100%;
+ height: calc(100% - 55px);
+ overflow-y: scroll;
- &.searchBox-collection {
- flex: 0 1 auto;
- margin-left: 2px;
- margin-right: 2px
- }
+ .searchBox-results-scroll-view-result {
+ display: inline-block;
+ vertical-align: middle;
+ width: 100%;
+ height: 50px;
+ cursor: pointer;
+ font-size: 15px;
+ padding: 11px;
- &.searchBox-input {
- margin:5px;
- border-radius:20px;
- border:$dark-gray;
- display: block;
- width: 130px;
- -webkit-transition: width 0.4s;
- transition: width 0.4s;
- align-self: stretch;
- outline:none;
- &:focus {
- width: 500px;
- outline:none;
+ &.searchBox-results-scroll-view-result-selected {
+ background: #999;
}
- }
- &.searchBox-filter {
- align-self: stretch;
- button{
- transform:none;
- &:hover {
- transform: none;
- }
+
+ .searchBox-result-title {
+ display: relative;
+ float: left;
+ width: calc(100% - 60px);
+ text-align: left;
}
- }
- &.searchBox-submit {
- margin-left: 2px;
- margin-right: 2px
- }
+ .searchBox-result-type {
+ font-size: 12px;
+ margin-top: 6px;
+ display: relative;
+ float: right;
+ width: 60px;
+ text-align: right;
+ color: #222;
+ }
- &.searchBox-close {
- color: $white;
- max-height: $searchpanel-height;
+ .searchBox-result-keys {
+ font-size: 10px;
+ margin-top: 1px;
+ display: relative;
+ float: left;
+ width: 100%;
+ text-align: left;
+ color: #555;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
}
}
- }
-}
-
-.searchBox-results {
- display: flex;
- flex-direction: column;
- top: 300px;
- display: flex;
- flex-direction: column;
- height: 100%;
- overflow: visible;
-
- .no-result {
- width: 500px;
- background: $light-gray;
- padding: 10px;
- height: 50px;
- text-transform: uppercase;
- text-align: left;
- font-weight: bold;
}
} \ No newline at end of file
diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx
index e7127a49c..b07879674 100644
--- a/src/client/views/search/SearchBox.tsx
+++ b/src/client/views/search/SearchBox.tsx
@@ -1,601 +1,325 @@
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { Tooltip } from '@material-ui/core';
-import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
+import { action, computed, observable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
-import { Doc, DocListCast, Field, Opt, DocListCastAsync } from '../../../fields/Doc';
+import { Doc, DocListCast, DocListCastAsync, Field } from '../../../fields/Doc';
import { documentSchema } from "../../../fields/documentSchemas";
-import { Copy, Id } from '../../../fields/FieldSymbols';
-import { List } from '../../../fields/List';
-import { createSchema, listSpec, makeInterface } from '../../../fields/Schema';
-import { SchemaHeaderField } from '../../../fields/SchemaHeaderField';
-import { Cast, NumCast, StrCast } from '../../../fields/Types';
-import { emptyFunction, returnFalse, returnZero, setupMoveUpEvents, Utils } from '../../../Utils';
-import { Docs } from '../../documents/Documents';
+import { Id } from '../../../fields/FieldSymbols';
+import { createSchema, makeInterface } from '../../../fields/Schema';
+import { StrCast } from '../../../fields/Types';
import { DocumentType } from "../../documents/DocumentTypes";
-import { CurrentUserUtils } from "../../util/CurrentUserUtils";
-import { SetupDrag } from '../../util/DragManager';
-import { SearchUtil } from '../../util/SearchUtil';
-import { Transform } from '../../util/Transform';
+import { DocumentManager } from "../../util/DocumentManager";
import { CollectionDockingView } from "../collections/CollectionDockingView";
-import { CollectionSchemaView, ColumnType } from "../collections/collectionSchema/CollectionSchemaView";
-import { CollectionViewType } from '../collections/CollectionView';
import { ViewBoxBaseComponent } from "../DocComponent";
import { FieldView, FieldViewProps } from '../nodes/FieldView';
import "./SearchBox.scss";
-import { undoBatch } from "../../util/UndoManager";
-import { DocServer } from "../../DocServer";
-import { MainView } from "../MainView";
export const searchSchema = createSchema({ Document: Doc });
type SearchBoxDocument = makeInterface<[typeof documentSchema, typeof searchSchema]>;
const SearchBoxDocument = makeInterface(documentSchema, searchSchema);
+/**
+ * This is the SearchBox component. It represents the search box input and results in
+ * the search panel on the left side of the screen.
+ */
@observer
export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDocument>(SearchBoxDocument) {
public static LayoutString(fieldKey: string) { return FieldView.LayoutString(SearchBox, fieldKey); }
public static Instance: SearchBox;
- private _allIcons: string[] = [DocumentType.INK, DocumentType.AUDIO, DocumentType.COL, DocumentType.IMG, DocumentType.LINK, DocumentType.PDF, DocumentType.RTF, DocumentType.VID, DocumentType.WEB, DocumentType.MAP];
- private _numResultsPerPage = 500;
- private _numTotalResults = -1;
- private _endIndex = -1;
- private _lockPromise?: Promise<void>;
- private _resultsSet = new Map<Doc, number>();
private _inputRef = React.createRef<HTMLInputElement>();
- private _maxSearchIndex: number = 0;
- private _curRequest?: Promise<any> = undefined;
- private _disposers: { [name: string]: IReactionDisposer } = {};
- private _blockedTypes = [DocumentType.PRESELEMENT, DocumentType.KVP, DocumentType.FILTER, DocumentType.SEARCH, DocumentType.SEARCHITEM, DocumentType.FONTICON, DocumentType.BUTTON, DocumentType.SCRIPTING];
-
- private docsforfilter: Doc[] | undefined = [];
- private realTotalResults: number = 0;
- private newsearchstring = "";
- private collectionRef = React.createRef<HTMLDivElement>();
-
-
- @observable _undoBackground: string | undefined = "";
- @observable _icons: string[] = this._allIcons;
- @observable _results: [Doc, string[], string[]][] = [];
- @observable _visibleElements: JSX.Element[] = [];
- @observable _visibleDocuments: Doc[] = [];
+
+ @observable _searchString = "";
+ @observable _docTypeString = "all";
+ @observable _results: [Doc, string[]][] = [];
+ @observable _selectedResult: Doc | undefined = undefined;
@observable _deletedDocsStatus: boolean = false;
@observable _onlyAliases: boolean = true;
- @observable _searchbarOpen = false;
- @observable _searchFullDB = "DB"; // "DB" means searh the entire database. "My Stuff" adds a flag that selects only documents that the current user has authored
- @observable _noResults = "";
- @observable _pageStart = 0;
- @observable open = false;
- @observable children = 0;
- @computed get filter() { return this._results?.length && (this.currentSelectedCollection?.props.Document._searchFilterDocs || this.currentSelectedCollection?.props.Document._docFilters); }
+ /**
+ * This is the constructor for the SearchBox class.
+ */
constructor(props: any) {
super(props);
SearchBox.Instance = this;
}
+ /**
+ * This method is called when the SearchBox component is first mounted. When the user opens
+ * the search panel, the search input box is automatically selected. This allows the user to
+ * type in the search input box immediately, without needing clicking on it first.
+ */
componentDidMount = action(() => {
if (this._inputRef.current) {
this._inputRef.current.focus();
}
- this._disposers.filters = reaction(() => this.props.Document._docFilters,
- (filters: any) => this.setSearchFilter(this.currentSelectedCollection, !this.filter ? undefined : this.docsforfilter));
});
+ /**
+ * This method is called when the SearchBox component is about to be unmounted. When the user
+ * closes the search panel, the search and its results are reset.
+ */
componentWillUnmount() {
- Object.values(this._disposers).forEach(disposer => disposer?.());
+ this.resetSearch();
}
- @computed get currentSelectedCollection() { return CollectionDockingView.Instance; }
-
- onChange = action((e: React.ChangeEvent<HTMLInputElement>) => {
- this.newsearchstring = e.target.value;
- if (e.target.value === "") {
- console.log("Reset start");
- this.docsforfilter = undefined;
- this.setSearchFilter(this.currentSelectedCollection, undefined);
- this.resetSearch(false);
-
- this.open = false;
- this._results = [];
- this._resultsSet.clear();
- this._visibleElements = [];
- this._numTotalResults = -1;
- this._endIndex = -1;
- this._curRequest = undefined;
- this._maxSearchIndex = 0;
- }
+ /**
+ * This method is called when the text in the search input box is modified by the user. The
+ * _searchString is updated to the new value of the text in the input box and submitSearch
+ * is called to update the search results accordingly.
+ *
+ * (Note: There is no longer a need to press enter to submit a search. Any update to the input
+ * causes a search to be submitted automatically.)
+ */
+ onInputChange = action((e: React.ChangeEvent<HTMLInputElement>) => {
+ this._searchString = e.target.value;
+ this.submitSearch();
});
- enter = action((e: React.KeyboardEvent | undefined) => {
- if (!e || e.key === "Enter") {
- this.layoutDoc._searchString = this.newsearchstring;
- this._pageStart = 0;
- this.open = StrCast(this.layoutDoc._searchString) !== "" || this._searchFullDB !== "DB";
- this.submitSearch();
- }
+ /**
+ * This method is called when the option in the select drop-down menu is changed. The
+ * _docTypeString is updated to the new value of the option in the drop-down menu. This
+ * is used to filter the results of the search to documents of a specific type.
+ *
+ * (Note: This doesn't affect the results array, so there is no need to submit a new
+ * search here. The results of the search on the _searchString query are simply filtered
+ * by type directly before rendering them.)
+ */
+ onSelectChange = action((e: React.ChangeEvent<HTMLSelectElement>) => {
+ this._docTypeString = e.target.value;
});
- getFinalQuery(query: string): string {
- //alters the query so it looks in the correct fields
- //if this is true, then not all of the field boxes are checked
- //TODO: data
- const initialfilters = Cast(this.props.Document._docFilters, listSpec("string"), []);
-
- const filters: string[] = [];
-
- for (const initFilter of initialfilters) {
- const fields = initFilter.split(":");
- if (fields[2] !== undefined) {
- filters.push(fields[0]);
- filters.push(fields[1]);
- filters.push(fields[2]);
- }
- }
-
- const finalfilters: { [key: string]: string[] } = {};
-
- for (let i = 0; i < filters.length; i = i++) {
- const fields = filters[i].split(":");
- if (finalfilters[fields[0]] !== undefined) {
- finalfilters[fields[0]].push(fields[1]);
- }
- else {
- finalfilters[fields[0]] = [fields[1]];
- }
- }
-
- for (const key in finalfilters) {
- const values = finalfilters[key];
- if (values.length === 1) {
- const mod = "_t:";
- const newWords: string[] = [];
- const oldWords = values[0].split(" ");
- oldWords.forEach((word, i) => i === 0 ? newWords.push(key + mod + word) : newWords.push("AND " + key + mod + word));
- query = `(${query}) AND (${newWords.join(" ")})`;
- }
- else {
- for (let i = 0; i < values.length; i++) {
- const mod = "_t:";
- const newWords: string[] = [];
- const oldWords = values[i].split(" ");
- oldWords.forEach((word, i) => i === 0 ? newWords.push(key + mod + word) : newWords.push("AND " + key + mod + word));
- const v = "(" + newWords.join(" ") + ")";
- if (i === 0) {
- query = `(${query}) AND (${v}` + (values.length === 1 ? ")" : "");
- }
- else query = query + " OR " + v + (i === values.length - 1 ? ")" : "");
- }
- }
- }
-
- return query.replace(/-\s+/g, '');
- }
-
- @action
- filterDocsByType(docs: Doc[]) {
- const finalDocs: Doc[] = [];
- docs.forEach(doc => {
- const layoutresult = StrCast(doc.type, "string") as DocumentType;
- if (layoutresult && !this._blockedTypes.includes(layoutresult) && this._icons.includes(layoutresult)) {
- finalDocs.push(doc);
- }
- });
- return finalDocs;
- }
+ /**
+ * @param {Doc} doc - doc of the search result that has been clicked on
+ *
+ * This method is called when the user clicks on a search result. The _selectedResult is
+ * updated accordingly and the doc is highlighted with the selectElement method.
+ */
+ onResultClick = action((doc: Doc) => {
+ this.selectElement(doc);
+ this._selectedResult = doc;
+ });
- static async foreachRecursiveDocAsync(docs: Doc[], func: (doc: Doc) => void) {
+ /**
+ * @param {Doc[]} docs - docs to be searched through recursively
+ * @param {number, Doc => void} func - function to be called on each doc
+ *
+ * This method iterates through an array of docs and all docs within those docs, calling
+ * the function func on each doc.
+ */
+ static foreachRecursiveDoc(docs: Doc[], func: (depth: number, doc: Doc) => void) {
let newarray: Doc[] = [];
+ var depth = 0;
while (docs.length > 0) {
newarray = [];
- await Promise.all(docs.filter(d => d).map(async d => {
+ docs.filter(d => d).forEach(d => {
const fieldKey = Doc.LayoutFieldKey(d);
const annos = !Field.toString(Doc.LayoutField(d) as Field).includes("CollectionView");
const data = d[annos ? fieldKey + "-annotations" : fieldKey];
- const docs = await DocListCastAsync(data);
- docs && newarray.push(...docs);
- func(d);
- }));
+ data && newarray.push(...DocListCast(data));
+ func(depth, d);
+ });
docs = newarray;
+ depth++;
}
}
- static foreachRecursiveDoc(docs: Doc[], func: (doc: Doc) => void) {
+
+ /**
+ * @param {Doc[]} docs - docs to be searched through recursively
+ * @param {number, Doc => void} func - function to be called on each doc
+ *
+ * This method iterates asynchronously through an array of docs and all docs within those
+ * docs, calling the function func on each doc.
+ */
+ static async foreachRecursiveDocAsync(docs: Doc[], func: (depth: number, doc: Doc) => void) {
let newarray: Doc[] = [];
+ var depth = 0;
while (docs.length > 0) {
newarray = [];
- docs.filter(d => d).forEach(d => {
+ await Promise.all(docs.filter(d => d).map(async d => {
const fieldKey = Doc.LayoutFieldKey(d);
const annos = !Field.toString(Doc.LayoutField(d) as Field).includes("CollectionView");
const data = d[annos ? fieldKey + "-annotations" : fieldKey];
- data && newarray.push(...DocListCast(data));
- func(d);
- });
+ const docs = await DocListCastAsync(data);
+ docs && newarray.push(...docs);
+ func(depth, d);
+ }));
docs = newarray;
+ depth++;
}
}
+ /**
+ * @param {String} type - string representing the type of a doc
+ *
+ * This method converts a doc type string of any length to a 3-letter doc type string in
+ * which the first letter is capitalized. This is used when displaying the type on the
+ * right side of each search result.
+ */
+ static formatType(type: String): String {
+ if (type == "pdf") {
+ return "PDF";
+ }
+ else if (type == "image") {
+ return "Img";
+ }
+
+ return type.charAt(0).toUpperCase() + type.substring(1, 3);
+ }
+
+ /**
+ * @param {String} query - search query string
+ *
+ * This method searches the CollectionDockingView instance for a certain query and puts
+ * the matching results in the results array. Docs are considered to be matching results
+ * when the query is a substring of many different pieces of its metadata (title, text,
+ * author, etc).
+ */
@action
searchCollection(query: string) {
- const selectedCollection = this.currentSelectedCollection;//SelectionManager.SelectedDocuments()[0];
+ const blockedTypes = [DocumentType.PRESELEMENT, DocumentType.KVP, DocumentType.FILTER, DocumentType.SEARCH, DocumentType.SEARCHITEM, DocumentType.FONTICON, DocumentType.BUTTON, DocumentType.SCRIPTING];
+ const blockedKeys = ["x", "y", "proto", "width", "autoHeight", "acl-Override", "acl-Public", "context", "zIndex", "height", "text-scrollHeight", "text-height", "cloneFieldFilter", "isPrototype", "text-annotations",
+ "dragFactory-count", "text-noTemplate", "aliases", "system", "layoutKey", "baseProto", "xMargin", "yMargin", "links", "layout", "layout_keyValue", "fitWidth", "viewType", "title-custom",
+ "panX", "panY", "viewScale"]
+ const collection = CollectionDockingView.Instance;
query = query.toLowerCase();
- if (selectedCollection !== undefined) {
- // this._currentSelectedCollection = selectedCollection;
- const docs = DocListCast(selectedCollection.dataDoc[Doc.LayoutFieldKey(selectedCollection.dataDoc)]);
- const found: [Doc, string[], string[]][] = [];
- SearchBox.foreachRecursiveDoc(docs, (doc: Doc) => {
- const hlights = new Set<string>();
- SearchBox.documentKeys(doc).forEach(key => Field.toString(doc[key] as Field).toLowerCase().includes(query) && hlights.add(key));
- Array.from(hlights.keys()).length > 0 && found.push([doc, Array.from(hlights.keys()), []]);
+ this._results = []
+ this._selectedResult = undefined
+
+ if (collection !== undefined) {
+ const docs = DocListCast(collection.rootDoc[Doc.LayoutFieldKey(collection.rootDoc)]);
+ const docIDs: String[] = []
+ console.log(docs.length)
+ SearchBox.foreachRecursiveDoc(docs, (depth: number, doc: Doc) => {
+ const dtype = StrCast(doc.type, "string") as DocumentType;
+ if (dtype && !blockedTypes.includes(dtype) && !docIDs.includes(doc[Id]) && depth > 0) {
+ const hlights = new Set<string>();
+ SearchBox.documentKeys(doc).forEach(key => Field.toString(doc[key] as Field).toLowerCase().includes(query) && hlights.add(key));
+ blockedKeys.forEach(key => {
+ hlights.delete(key);
+ })
+ Array.from(hlights.keys()).length > 0 && this._results.push([doc, Array.from(hlights.keys())]);
+ }
+ docIDs.push(doc[Id])
});
-
- this._results = found;
- this.docsforfilter = this._results.map(r => r[0]);
- this.setSearchFilter(selectedCollection, this.filter && found.length ? this.docsforfilter : undefined);
- this._numTotalResults = found.length;
- this.realTotalResults = found.length;
}
- else {
- this._noResults = "No collection selected :(";
- }
-
}
+ /**
+ * @param {Doc} doc - doc for which keys are returned
+ *
+ * This method returns a list of a document doc's keys.
+ */
static documentKeys(doc: Doc) {
const keys: { [key: string]: boolean } = {};
- // bcz: ugh. this is untracked since otherwise a large collection of documents will blast the server for all their fields.
- // then as each document's fields come back, we update the documents _proxies. Each time we do this, the whole schema will be
- // invalidated and re-rendered. This workaround will inquire all of the document fields before the options button is clicked.
- // then by the time the options button is clicked, all of the fields should be in place. If a new field is added while this menu
- // is displayed (unlikely) it won't show up until something else changes.
- //TODO Types
Doc.GetAllPrototypes(doc).map(proto => Object.keys(proto).forEach(key => keys[key] = false));
return Array.from(Object.keys(keys));
}
+ /**
+ * This method submits a search with the _searchString as its query and updates
+ * the results array accordingly.
+ */
@action
submitSearch = async () => {
- this.resetSearch(false);
-
- //this.props.Document._docFilters = new List();
- this._noResults = "";
+ this.resetSearch();
- this.dataDoc[this.fieldKey] = new List<Doc>([]);
- this.children = 0;
- let query = StrCast(this.layoutDoc._searchString);
+ let query = StrCast(this._searchString);
Doc.SetSearchQuery(query);
- this._searchFullDB && (query = this.getFinalQuery(query));
this._results = [];
- this._resultsSet.clear();
- this._visibleElements = [];
- this._visibleDocuments = [];
-
- if (query || this._searchFullDB === "My Stuff") {
- this._endIndex = 12;
- this._maxSearchIndex = 0;
- this._numTotalResults = -1;
- this._searchFullDB ? await this.searchDatabase(query) : this.searchCollection(query);
- runInAction(() => {
- this.open = this._searchbarOpen = true;
- this.resultsScrolled();
- });
- }
- }
- getAllResults = async (query: string) => {
- return SearchUtil.Search(query, true, { fq: this.filterQuery, start: 0, rows: 10000000 });
- }
-
- private get filterQuery() {
- const baseExpr = "NOT system_b:true";
- const authorExpr = this._searchFullDB === "My Stuff" ? ` author_t:${Doc.CurrentUserEmail}` : undefined;
- const includeDeleted = this._deletedDocsStatus ? "" : " NOT deleted_b:true";
- const typeExpr = this._onlyAliases ? "NOT {!join from=id to=proto_i}type_t:*" : `(type_t:* OR {!join from=id to=proto_i}type_t:*) ${this._blockedTypes.map(type => `NOT ({!join from=id to=proto_i}type_t:${type}) AND NOT type_t:${type}`).join(" AND ")}`;
- // fq: type_t:collection OR {!join from=id to=proto_i}type_t:collection q:text_t:hello
- return [baseExpr, authorExpr, includeDeleted, typeExpr].filter(q => q).join(" AND ").replace(/AND $/, "");
- }
-
- @computed get primarySort() {
- const suffixMap = (type: ColumnType) => {
- switch (type) {
- case ColumnType.Date: return "_d";
- case ColumnType.String: return "_t";
- case ColumnType.Boolean: return "_b";
- case ColumnType.Number: return "_n";
- }
- };
- const headers = Cast(this.props.Document._schemaHeaders, listSpec(SchemaHeaderField), []);
- return headers.reduce((p: Opt<string>, header: SchemaHeaderField) => p || (header.desc !== undefined && suffixMap(header.type) ? (header.heading + suffixMap(header.type) + (header.desc ? " desc" : " asc")) : undefined), undefined);
- }
-
- searchDatabase = async (query: string) => {
- this._lockPromise && (await this._lockPromise);
- this._lockPromise = new Promise(async res => {
- while (this._results.length <= this._endIndex && (this._numTotalResults === -1 || this._maxSearchIndex < this._numTotalResults)) {
- this._curRequest = SearchUtil.Search(query, true, { onlyAliases: true, allowAliases: true, /*sort: this.primarySort,*/ fq: this.filterQuery, start: 0, rows: this._numResultsPerPage, hl: "on", "hl.fl": "*", }).then(action(async (res: SearchUtil.DocSearchResult) => {
- // happens at the beginning
- this.realTotalResults = res.numFound <= 0 ? 0 : res.numFound;
- if (res.numFound !== this._numTotalResults && this._numTotalResults === -1) {
- this._numTotalResults = res.numFound;
- }
- const highlighting = res.highlighting || {};
- const highlightList = res.docs.map(doc => highlighting[doc[Id]]);
- const lines = new Map<string, string[]>();
- res.docs.map((doc, i) => lines.set(doc[Id], res.lines[i]));
- const docs = res.docs;
- const highlights: typeof res.highlighting = {};
- docs.forEach((doc, index) => highlights[doc[Id]] = highlightList[index]);
- const filteredDocs = this.filterDocsByType(docs);
-
- runInAction(() => filteredDocs.forEach((doc, i) => {
- const index = this._resultsSet.get(doc);
- const highlight = highlights[doc[Id]];
- const line = lines.get(doc[Id]) || [];
- const hlights = highlight ? Object.keys(highlight).map(key => key.substring(0, key.length - 2)).filter(k => k) : [];
- // if (this.findCommonElements(hlights)) {
- // }
- if (index === undefined) {
- this._resultsSet.set(doc, this._results.length);
- this._results.push([doc, hlights, line]);
- } else {
- this._results[index][1].push(...hlights);
- this._results[index][2].push(...line);
- }
-
- }));
-
- this._curRequest = undefined;
- }));
- this._maxSearchIndex += this._numResultsPerPage;
-
- await this._curRequest;
- }
-
- this.resultsScrolled();
-
- const selectedCollection = this.currentSelectedCollection;//SelectionManager.SelectedDocuments()[0];
- this.docsforfilter = this._results.map(r => r[0]);
- this.setSearchFilter(selectedCollection, this.filter ? this.docsforfilter : undefined);
- res();
- });
- return this._lockPromise;
- }
-
- startDragCollection = async () => {
- const res = await this.getAllResults(this.getFinalQuery(StrCast(this.layoutDoc._searchString)));
- const filtered = this.filterDocsByType(res.docs);
- const docs = filtered.map(doc => Doc.GetT(doc, "isPrototype", "boolean", true) ? Doc.MakeDelegate(doc) : Doc.MakeAlias(doc));
- let x = 0;
- let y = 0;
- for (const doc of docs.map(d => Doc.Layout(d))) {
- doc.x = x;
- doc.y = y;
- const size = 200;
- const aspect = Doc.NativeHeight(doc) / (Doc.NativeWidth(doc) || 1);
- if (aspect > 1) {
- doc._height = size;
- doc._width = size / aspect;
- } else if (aspect > 0) {
- doc._width = size;
- doc._height = size * aspect;
- } else {
- doc._width = size;
- doc._height = size;
- }
- x += 250;
- if (x > 1000) {
- x = 0;
- y += 300;
- }
+ if (query) {
+ this.searchCollection(query);
}
- const headers = Cast(this.props.Document._schemaHeaders, listSpec(SchemaHeaderField), []).map(h => { const v = h[Copy](); v.color = "#f1efeb"; return v; });
- return Docs.Create.SchemaDocument(headers, DocListCast(this.dataDoc[this.fieldKey]), { _autoHeight: true, _viewType: CollectionViewType.Schema, title: StrCast(this.layoutDoc._searchString) });
- }
-
- @action.bound
- openSearch(e: React.SyntheticEvent) {
- e.stopPropagation();
- this._results.forEach(result => Doc.BrushDoc(result[0]));
}
- resetSearch = action((close: boolean) => {
+ /**
+ * This method resets the search by iterating through each result and removing all
+ * brushes and highlights. All search matches are cleared as well.
+ */
+ resetSearch = action(() => {
this._results.forEach(result => {
Doc.UnBrushDoc(result[0]);
+ Doc.UnHighlightDoc(result[0]);
Doc.ClearSearchMatches();
});
- close && (this.open = this._searchbarOpen = false);
});
- @action.bound
- closeResults() {
- this._results = [];
- this._resultsSet.clear();
- this._visibleElements = [];
- this._visibleDocuments = [];
- this._numTotalResults = -1;
- this._endIndex = -1;
- this._curRequest = undefined;
+ /**
+ * @param {Doc} doc - doc to be selected
+ *
+ * This method selects a doc by either jumping to it (centering/zooming in on it)
+ * or opening it in a new tab.
+ */
+ selectElement = async (doc: Doc) => {
+ await DocumentManager.Instance.jumpToDocument(doc, true);
}
- @action
- resultsScrolled = (e?: React.UIEvent<HTMLDivElement>) => {
- this._endIndex = 30;
- const headers = new Set<string>(["title", "author", "text", "type", "data", "*lastModified", "context"]);
-
- if (this._numTotalResults <= this._maxSearchIndex) {
- this._numTotalResults = this._results.length;
- }
-
- // only hit right at the beginning
- // visibleElements is all of the elements (even the ones you can't see)
- if (this._visibleElements.length !== this._numTotalResults) {
- // undefined until a searchitem is put in there
- this._visibleElements = Array<JSX.Element>(this._numTotalResults === -1 ? 0 : this._numTotalResults);
- this._visibleDocuments = Array<Doc>(this._numTotalResults === -1 ? 0 : this._numTotalResults);
- }
- let max = this._numResultsPerPage;
- max > this._results.length ? max = this._results.length : console.log("");
- for (let i = this._pageStart; i < max; i++) {
- //if the index is out of the window then put a placeholder in
- //should ones that have already been found get set to placeholders?
-
- let result: [Doc, string[], string[]] | undefined = undefined;
-
- result = this._results[i];
- if (result) {
- const highlights = Array.from([...Array.from(new Set(result[1]).values())]);
- const lines = new List<string>(result[2]);
- highlights.forEach((item) => headers.add(item));
- Doc.SetSearchMatch(result[0], { searchMatch: 1 });
- if (i < this._visibleDocuments.length) {
- this._visibleDocuments[i] = result[0];
- Doc.BrushDoc(result[0]);
- Doc.AddDocToList(this.dataDoc, this.props.fieldKey, result[0]);
- this.children++;
- }
- }
- }
- if (this.props.Document._schemaHeaders === undefined) {
- this.props.Document._schemaHeaders = new List<SchemaHeaderField>([new SchemaHeaderField("title", "#f1efeb")]);
- }
- if (this._maxSearchIndex >= this._numTotalResults) {
- this._visibleElements.length = this._results.length;
- this._visibleDocuments.length = this._results.length;
- }
+ /**
+ * This method returns a JSX list of the options in the select drop-down menu, which
+ * is used to filter the types of documents that appear in the search results.
+ */
+ @computed
+ public get selectOptions() {
+ const selectValues = ["all", "rtf", "image", "pdf", "web", "video", "audio", "collection"]
+
+ return selectValues.map(value => {
+ return <option key={value} value={value}>{SearchBox.formatType(value)}</option>
+ })
}
- getTransform = () => this.props.ScreenToLocalTransform().translate(-5, -65);// listBox padding-left and pres-box-cont minHeight
- panelHeight = () => this.props.PanelHeight();
- selectElement = (doc: Doc) => { /* this.gotoDocument(this.childDocs.indexOf(doc), NumCasst(this.layoutDoc._itemIndex)); */ };
- returnHeight = () => NumCast(this.layoutDoc._height);
- returnLength = () => Math.min(window.innerWidth, 51 + 205 * Cast(this.props.Document._schemaHeaders, listSpec(SchemaHeaderField), []).length);
-
- @action
- changeSearchScope = (scope: string) => {
- this.docsforfilter = undefined;
- this.setSearchFilter(this.currentSelectedCollection, undefined);
- this._searchFullDB = scope;
- this.dataDoc[this.fieldKey] = new List<Doc>([]);
- this.submitSearch();
- }
+ /**
+ * This method renders the search input box, select drop-down menu, and search results.
+ */
+ render() {
+ var validResults = 0;
- @computed get scopeButtons() {
- return <div style={{ height: 25, paddingLeft: "4px", paddingRight: "4px" }}>
- <form className="beta" style={{ justifyContent: "space-evenly", display: "flex" }}>
- <div style={{ display: "contents" }}>
- <div className="radio" style={{ margin: 0 }}>
- <label style={{ fontSize: 12, marginTop: 6 }} >
- <input type="radio" style={{ marginLeft: -16, marginTop: -1 }} checked={!this._searchFullDB} onChange={() => this.changeSearchScope("")} />
- Dashboard
- </label>
- </div>
- <div className="radio" style={{ margin: 0 }}>
- <label style={{ fontSize: 12, marginTop: 6 }} >
- <input type="radio" style={{ marginLeft: -16, marginTop: -1 }} checked={this._searchFullDB?.length ? true : false} onChange={() => this.changeSearchScope("DB")} />
- DB
- <span onClick={action(() => this._searchFullDB = this._searchFullDB === "My Stuff" ? "DB" : "My Stuff")}>
- {this._searchFullDB === "My Stuff" ? "(me)" : "(full)"}
- </span>
- </label>
- </div>
- </div>
- </form>
- </div>;
- }
+ const results = this._results.map(result => {
+ var className = "searchBox-results-scroll-view-result";
- setSearchFilter = action((collectionView: { props: { Document: Doc } }, docsForFilter: Doc[] | undefined) => {
- if (collectionView) {
- const docFilters = Cast(this.props.Document._docFilters, listSpec("string"), null);
- collectionView.props.Document._searchFilterDocs = docsForFilter?.length ? new List<Doc>(docsForFilter) : undefined;
- collectionView.props.Document._docFilters = docsForFilter?.length && docFilters?.length ? new List<string>(docFilters) : undefined;
- }
- });
+ if (this._selectedResult == result[0]) {
+ className += " searchBox-results-scroll-view-result-selected"
+ }
- render() {
- const myDashboards = DocListCast(CurrentUserUtils.MyDashboards.data);
- return (
- <div style={{ pointerEvents: "all" }} className="searchBox-container">
- <div className="searchBox-bar" style={{ background: SearchBox.Instance._undoBackground }}>
- <div className="searchBox-lozenges" >
- <div className="searchBox-lozenge-user">
- {`${Doc.CurrentUserEmail}`}
- <div className="searchBox-logoff" onClick={() => window.location.assign(Utils.prepend("/logout"))}>
- Logoff
- </div>
+ if (this._docTypeString == "all" || this._docTypeString == result[0].type) {
+ validResults++;
+ return (
+ <div key={result[0][Id]} onClick={() => this.onResultClick(result[0])} className={className}>
+ <div className="searchBox-result-title">
+ {result[0].title}
</div>
- <div className="searchBox-lozenge" onClick={() => DocServer.UPDATE_SERVER_CACHE()}>
- {`UI project`}
+ <div className="searchBox-result-type">
+ {SearchBox.formatType(StrCast(result[0].type))}
</div>
- <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} style={{ backgroundColor: "black" }}> {StrCast(dash.title)} </option>)}
- </select>
- <div className="searchBox-dashboards" onClick={undoBatch(() => CurrentUserUtils.createNewDashboard(Doc.UserDoc()))}>
- New
- </div>
- <div className="searchBox-dashboards" onClick={undoBatch(() => CurrentUserUtils.snapshotDashboard(Doc.UserDoc()))}>
- Snapshot
- </div>
+ <div className="searchBox-result-keys">
+ {result[1].join(", ")}
</div>
</div>
- <div className="searchBox-query" >
- <input defaultValue={""} autoComplete="off" onChange={this.onChange} type="text" placeholder="Search..." id="search-input" ref={this._inputRef}
- className="searchBox-barChild searchBox-input" onKeyPress={this.enter}
- style={{ padding: 1, paddingLeft: 20, paddingRight: 60, color: "black", height: 20, width: 250 }} />
- <div style={{ display: "flex", alignItems: "center" }}>
- <div style={{ position: "absolute", left: 10 }}>
- <Tooltip title={<div className="dash-tooltip" >drag search results as collection</div>}>
- <div ref={this.collectionRef}><FontAwesomeIcon onPointerDown={SetupDrag(this.collectionRef, () => StrCast(this.layoutDoc._searchString) ? this.startDragCollection() : undefined)} icon={"search"} size="lg"
- style={{ cursor: "hand", color: "black", padding: 1, position: "relative" }} /></div>
- </Tooltip>
- </div>
- <div style={{ position: "absolute", left: Doc.UserDoc().noviceMode ? 220 : 200, width: 30, zIndex: 9000, color: "grey", background: "white", }}>
- {`${this._results.length}` + " of " + `${this.realTotalResults}`}
- </div>
- {Doc.UserDoc().noviceMode ? (null) : <div style={{ cursor: "default", left: 235, position: "absolute", }}>
- <Tooltip title={<div className="dash-tooltip" >only display documents matching search</div>} >
- <div>
- <FontAwesomeIcon icon={"filter"} size="lg"
- style={{ cursor: "hand", padding: 1, backgroundColor: this.filter ? "white" : "lightgray", color: this.filter ? "black" : "white" }}
- onPointerDown={e => { e.stopPropagation(); SetupDrag(this.collectionRef, () => this.layoutDoc._searchString ? this.startDragCollection() : undefined); }}
- onClick={action(() => this.setSearchFilter(this.currentSelectedCollection, this.filter ? undefined : this.docsforfilter))} />
- </div>
- </Tooltip>
- </div>}
- {this.scopeButtons}
- </div>
- </div >
+ )
+ }
+
+ return null;
+ })
+
+ results.filter(result => result);
+
+ return (
+ <div style={{ pointerEvents: "all" }} className="searchBox-container">
+ <div className="searchBox-bar">
+ <select name="type" id="searchBox-type" className="searchBox-type" onChange={this.onSelectChange}>
+ {this.selectOptions}
+ </select>
+ <input defaultValue={""} autoComplete="off" onChange={this.onInputChange} type="text" placeholder="Search..." id="search-input" className="searchBox-input" ref={this._inputRef} />
</div >
- {!this._searchbarOpen ? (null) :
- <div style={{ zIndex: 20000, color: "black" }} ref={(r) => r?.focus()}>
- <div style={{ display: "flex", justifyContent: "center", }}>
- <div style={{ display: this.open ? "flex" : "none", overflow: "auto", position: "absolute" }}>
- <CollectionSchemaView {...this.props}
- CollectionView={undefined}
- addDocument={returnFalse}
- Document={this.props.Document}
- moveDocument={returnFalse}
- removeDocument={returnFalse}
- PanelHeight={this.open ? this.returnHeight : returnZero}
- PanelWidth={this.open ? this.returnLength : returnZero}
- scrollOverflow={length > window.innerWidth || this.children > 6 ? true : false}
- focus={this.selectElement}
- ScreenToLocalTransform={Transform.Identity}
- />
- <div style={{ position: "absolute", right: 5, bottom: 7, width: 15, height: 15, }}
- onPointerDown={e => setupMoveUpEvents(this, e, (e: PointerEvent, down: number[], delta: number[]) => {
- this.props.Document._height = NumCast(this.props.Document._height) + delta[1];
- return false;
- }, returnFalse, emptyFunction)}
- >
- <FontAwesomeIcon icon="grip-lines" size="lg" />
- </div>
- </div>
- </div>
+ <div className="searchBox-results-container">
+ <div className="searchBox-results-count">
+ {`${validResults}` + " result" + (validResults == 1 ? "" : "s")}
</div>
- }
+ <div className="searchBox-results-scroll-view">
+ {results}
+ </div>
+ </div>
</div >
);
}
diff --git a/src/client/views/topbar/TopBar.scss b/src/client/views/topbar/TopBar.scss
index 164cc29cd..2ecbb536b 100644
--- a/src/client/views/topbar/TopBar.scss
+++ b/src/client/views/topbar/TopBar.scss
@@ -91,7 +91,7 @@
font-family: 'Roboto';
position: relative;
display: flex;
- width: 450;
+ width: fit-content;
gap: 5px;
.topBar-icon:hover {
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index c759bae5a..28d2e0441 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -55,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;
@@ -90,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");
@@ -102,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]
]);
@@ -252,7 +257,7 @@ export class Doc extends RefField {
DocServer.GetRefField(this[Id], true);
}
};
- const writeMode = DocServer.getFieldWriteMode(fKey as string);
+ const writeMode = DocServer.getFieldWriteMode(fKey);
if (fKey.startsWith("acl") || writeMode !== DocServer.WriteMode.Playground) {
delete this[CachedUpdates][fKey];
await fn();
@@ -535,7 +540,7 @@ 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)));
@@ -587,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) => {
@@ -1215,7 +1219,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";
@@ -1385,7 +1389,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/util.ts b/src/fields/util.ts
index 526e5af72..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) {
@@ -138,7 +141,9 @@ export function denormalizeEmail(email: string) {
export function inheritParentAcls(parent: Doc, child: Doc) {
const dataDoc = parent[DataSym];
for (const key of Object.keys(dataDoc)) {
- key.startsWith("acl") && distributeAcls(key, dataDoc[key], child);
+ // 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);
}
}
@@ -157,9 +162,10 @@ export function inheritParentAcls(parent: Doc, child: Doc) {
*/
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"
}
@@ -176,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);
}
@@ -192,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]
]);
@@ -235,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]
]);
@@ -294,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/server/websocket.ts b/src/server/websocket.ts
index 115baa0be..13d7237f6 100644
--- a/src/server/websocket.ts
+++ b/src/server/websocket.ts
@@ -284,7 +284,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,