aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/client/DocServer.ts9
-rw-r--r--src/client/documents/Documents.ts16
-rw-r--r--src/client/util/CurrentUserUtils.ts62
-rw-r--r--src/client/util/DocumentManager.ts11
-rw-r--r--src/client/util/GroupManager.tsx139
-rw-r--r--src/client/util/GroupMemberView.tsx4
-rw-r--r--src/client/util/LinkManager.ts16
-rw-r--r--src/client/util/SelectionManager.ts8
-rw-r--r--src/client/util/SerializationHelper.ts2
-rw-r--r--src/client/util/SharingManager.tsx88
-rw-r--r--src/client/util/SnappingManager.ts10
-rw-r--r--src/client/views/DocComponent.tsx2
-rw-r--r--src/client/views/DocumentButtonBar.tsx5
-rw-r--r--src/client/views/DocumentDecorations.tsx2
-rw-r--r--src/client/views/EditableView.tsx7
-rw-r--r--src/client/views/Main.tsx6
-rw-r--r--src/client/views/PropertiesView.tsx4
-rw-r--r--src/client/views/collections/CollectionLinearView.tsx4
-rw-r--r--src/client/views/collections/CollectionMenu.tsx45
-rw-r--r--src/client/views/collections/CollectionSchemaHeaders.tsx14
-rw-r--r--src/client/views/collections/CollectionStackingView.tsx5
-rw-r--r--src/client/views/collections/CollectionStackingViewFieldColumn.tsx12
-rw-r--r--src/client/views/collections/CollectionSubView.tsx5
-rw-r--r--src/client/views/collections/CollectionView.tsx5
-rw-r--r--src/client/views/collections/TabDocView.tsx14
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx28
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx59
-rw-r--r--src/client/views/nodes/CollectionFreeFormDocumentView.tsx30
-rw-r--r--src/client/views/nodes/FilterBox.tsx16
-rw-r--r--src/client/views/nodes/FontIconBox.tsx5
-rw-r--r--src/client/views/nodes/ImageBox.tsx16
-rw-r--r--src/client/views/nodes/PDFBox.tsx31
-rw-r--r--src/client/views/nodes/PresBox.scss52
-rw-r--r--src/client/views/nodes/PresBox.tsx179
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.scss9
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx29
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx4
-rw-r--r--src/client/views/pdf/Annotation.tsx2
-rw-r--r--src/client/views/pdf/PDFViewer.tsx2
-rw-r--r--src/client/views/presentationview/PresElementBox.scss286
-rw-r--r--src/client/views/presentationview/PresElementBox.tsx178
-rw-r--r--src/client/views/search/SearchBox.tsx23
-rw-r--r--src/debug/Repl.tsx2
-rw-r--r--src/debug/Viewer.tsx2
-rw-r--r--src/fields/Doc.ts75
-rw-r--r--src/fields/List.ts5
-rw-r--r--src/fields/util.ts101
-rw-r--r--src/mobile/MobileMain.tsx2
-rw-r--r--src/server/ApiManagers/UserManager.ts33
-rw-r--r--src/server/GarbageCollector.ts2
-rw-r--r--src/server/authentication/AuthenticationManager.ts4
-rw-r--r--src/server/authentication/DashUserModel.ts4
-rw-r--r--src/server/websocket.ts10
-rw-r--r--test/test.ts2
54 files changed, 936 insertions, 750 deletions
diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts
index 9683eab45..00f9877c3 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, fetchProto, FieldsSym, UpdatingFromServer } from '../fields/Doc';
+import { Opt, Doc, UpdatingFromServer } from '../fields/Doc';
import { Utils, emptyFunction } from '../Utils';
import { SerializationHelper } from './util/SerializationHelper';
import { RefField } from '../fields/RefField';
@@ -10,6 +10,7 @@ import MobileInkOverlay from '../mobile/MobileInkOverlay';
import { runInAction } from 'mobx';
import { ObjectField } from '../fields/ObjectField';
import { StrCast } from '../fields/Types';
+import * as rp from 'request-promise';
/**
* This class encapsulates the transfer and cross-client synchronization of
@@ -34,6 +35,12 @@ export namespace DocServer {
if (doc instanceof Doc) strings.push(StrCast(doc.author) + " " + StrCast(doc.title) + " " + StrCast(Doc.GetT(doc, "title", "string", true)));
});
strings.sort().forEach((str, i) => console.log(i.toString() + " " + str));
+ rp.post(Utils.prepend("/setCacheDocumentIds"), {
+ body: {
+ cacheDocumentIds: Array.from(Object.keys(_cache)).join(";"),
+ },
+ json: true,
+ });
}
export let _socket: SocketIOClient.Socket;
// this client's distinct GUID created at initialization
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 7ee8267f8..159771145 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -244,6 +244,7 @@ export namespace Docs {
view: LayoutSource,
dataField: string
},
+ data?: any,
options?: Partial<DocumentOptions>
};
type TemplateMap = Map<DocumentType, PrototypeTemplate>;
@@ -464,6 +465,7 @@ export namespace Docs {
const options = { title, type, baseProto: true, ...defaultOptions, ...(template.options || {}) };
options.layout = layout.view?.LayoutString(layout.dataField);
const doc = Doc.assign(new Doc(prototypeId, true), { system: true, layoutKey: "layout", ...options });
+ doc.data = template.data;
doc.layout_keyValue = KeyValueBox.LayoutString("");
return doc;
}
@@ -893,8 +895,11 @@ export namespace Docs {
export namespace DocUtils {
export function Excluded(d: Doc, docFilters: string[]) {
const filterFacets: { [key: string]: { [value: string]: string } } = {}; // maps each filter key to an object with value=>modifier fields
- for (let i = 0; i < docFilters.length; i += 3) {
- const [key, value, modifiers] = docFilters.slice(i, i + 3);
+ for (let i = 0; i < docFilters.length; i++) {
+ const fields = docFilters[i].split(":");
+ const key = fields[0];
+ const value = fields[1];
+ const modifiers = fields[2];
if (!filterFacets[key]) {
filterFacets[key] = {};
}
@@ -916,8 +921,11 @@ export namespace DocUtils {
const childDocs = viewSpecScript ? docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result) : docs;
const filterFacets: { [key: string]: { [value: string]: string } } = {}; // maps each filter key to an object with value=>modifier fields
- for (let i = 0; i < docFilters.length; i += 3) {
- const [key, value, modifiers] = docFilters.slice(i, i + 3);
+ for (let i = 0; i < docFilters.length; i++) {
+ const fields = docFilters[i].split(":");
+ const key = fields[0];
+ const value = fields[1];
+ const modifiers = fields[2];
if (!filterFacets[key]) {
filterFacets[key] = {};
}
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 1096b8e5f..dcbeba8cd 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -1,6 +1,6 @@
import { computed, observable, reaction } from "mobx";
import * as rp from 'request-promise';
-import { DataSym, Doc, DocListCast, DocListCastAsync } from "../../fields/Doc";
+import { DataSym, Doc, DocListCast, DocListCastAsync, AclReadonly } from "../../fields/Doc";
import { Id } from "../../fields/FieldSymbols";
import { List } from "../../fields/List";
import { PrefetchProxy } from "../../fields/Proxy";
@@ -8,12 +8,14 @@ import { RichTextField } from "../../fields/RichTextField";
import { listSpec } from "../../fields/Schema";
import { SchemaHeaderField } from "../../fields/SchemaHeaderField";
import { ComputedField, ScriptField } from "../../fields/ScriptField";
-import { BoolCast, Cast, NumCast, PromiseValue, StrCast } from "../../fields/Types";
+import { BoolCast, Cast, NumCast, PromiseValue, StrCast, DateCast } from "../../fields/Types";
import { nullAudio } from "../../fields/URLField";
+import { SharingPermissions } from "../../fields/util";
import { Utils } from "../../Utils";
import { DocServer } from "../DocServer";
import { Docs, DocumentOptions, DocUtils } from "../documents/Documents";
import { DocumentType } from "../documents/DocumentTypes";
+import { Networking } from "../Network";
import { CollectionDockingView } from "../views/collections/CollectionDockingView";
import { DimUnit } from "../views/collections/collectionMulticolumn/CollectionMulticolumnView";
import { CollectionView, CollectionViewType } from "../views/collections/CollectionView";
@@ -30,9 +32,10 @@ import { Scripting } from "./Scripting";
import { SearchUtil } from "./SearchUtil";
import { SelectionManager } from "./SelectionManager";
import { UndoManager } from "./UndoManager";
-import { SharingPermissions } from "../../fields/util";
+import { SnappingManager } from "./SnappingManager";
+export let resolvedPorts: { server: number, socket: number };
const headerViewVersion = "0.1";
export class CurrentUserUtils {
private static curr_id: string;
@@ -229,7 +232,7 @@ export class CurrentUserUtils {
} else {
const curButnTypes = Cast(doc["template-buttons"], Doc, null);
DocListCastAsync(curButnTypes.data).then(async curBtns => {
- await Promise.all(curBtns!);
+ curBtns && await Promise.all(curBtns);
requiredTypes.map(btype => Doc.AddDocToList(curButnTypes, "data", btype));
});
}
@@ -278,7 +281,7 @@ export class CurrentUserUtils {
const curNoteTypes = Cast(doc["template-notes"], Doc, null);
const requiredTypes = [doc["template-note-Note"] as any as Doc, doc["template-note-Idea"] as any as Doc, doc["template-note-Topic"] as any as Doc];//, doc["template-note-Todo"] as any as Doc];
DocListCastAsync(curNoteTypes.data).then(async curNotes => {
- await Promise.all(curNotes!);
+ curNotes && await Promise.all(curNotes);
requiredTypes.map(ntype => Doc.AddDocToList(curNoteTypes, "data", ntype));
});
}
@@ -349,7 +352,7 @@ export class CurrentUserUtils {
const requiredTypes = [doc["template-icon-view"] as Doc, doc["template-icon-view-img"] as Doc, doc["template-icon-view-button"] as Doc,
doc["template-icon-view-col"] as Doc, doc["template-icon-view-rtf"] as Doc];
DocListCastAsync(templateIconsDoc.data).then(async curIcons => {
- await Promise.all(curIcons!);
+ curIcons && await Promise.all(curIcons);
requiredTypes.map(ntype => Doc.AddDocToList(templateIconsDoc, "data", ntype));
});
}
@@ -538,9 +541,9 @@ export class CurrentUserUtils {
})) as any as Doc;
}
}
- static async setupMenuPanel(doc: Doc, sharingDocumentId: string) {
+ static async setupMenuPanel(doc: Doc, sharingDocumentId: string, linkDatabaseId: string) {
if (doc.menuStack === undefined) {
- await this.setupSharingSidebar(doc, sharingDocumentId); // sets up the right sidebar collection for mobile upload documents and sharing
+ await this.setupSharingSidebar(doc, sharingDocumentId, linkDatabaseId); // sets up the right sidebar collection for mobile upload documents and sharing
const menuBtns = (await CurrentUserUtils.menuBtnDescriptions(doc)).map(({ title, target, icon, click, watchedDocuments }) =>
Docs.Create.FontIconDocument({
icon,
@@ -873,7 +876,17 @@ export class CurrentUserUtils {
}
// Sharing sidebar is where shared documents are contained
- static async setupSharingSidebar(doc: Doc, sharingDocumentId: string) {
+ static async setupSharingSidebar(doc: Doc, sharingDocumentId: string, linkDatabaseId: string) {
+ if (doc.myLinkDatabase === undefined) {
+ let linkDocs = await DocServer.GetRefField(linkDatabaseId);
+ if (!linkDocs) {
+ linkDocs = new Doc(linkDatabaseId, true);
+ (linkDocs as Doc).author = Doc.CurrentUserEmail;
+ (linkDocs as Doc).data = new List<Doc>([]);
+ (linkDocs as Doc)["acl-Public"] = SharingPermissions.Add;
+ }
+ doc.myLinkDatabase = new PrefetchProxy(linkDocs);
+ }
if (doc.mySharedDocs === undefined) {
let sharedDocs = await DocServer.GetRefField(sharingDocumentId + "outer");
if (!sharedDocs) {
@@ -953,7 +966,15 @@ export class CurrentUserUtils {
return doc.clickFuncs as Doc;
}
- static async updateUserDocument(doc: Doc, sharingDocumentId: string) {
+ static async updateUserDocument(doc: Doc, sharingDocumentId: string, linkDatabaseId: string) {
+ if (!doc.globalGroupDatabase) doc.globalGroupDatabase = Docs.Prototypes.MainGroupDocument();
+ const groups = await DocListCastAsync((doc.globalGroupDatabase as Doc).data);
+ reaction(() => DateCast((doc.globalGroupDatabase as Doc).lastModified),
+ async () => {
+ const groups = await DocListCastAsync((doc.globalGroupDatabase as Doc).data);
+ const mygroups = groups?.filter(group => JSON.parse(StrCast(group.members)).includes(Doc.CurrentUserEmail)) || [];
+ SnappingManager.SetCachedGroups(["Public", ...mygroups?.map(g => StrCast(g.title))]);
+ }, { fireImmediately: true });
doc.system = true;
doc.noviceMode = doc.noviceMode === undefined ? "true" : doc.noviceMode;
doc.title = Doc.CurrentUserEmail;
@@ -985,10 +1006,8 @@ export class CurrentUserUtils {
this.setupOverlays(doc); // documents in overlay layer
this.setupDockedButtons(doc); // the bottom bar of font icons
await this.setupSidebarButtons(doc); // the pop-out left sidebar of tools/panels
- await this.setupMenuPanel(doc, sharingDocumentId);
- doc.globalScriptDatabase = Docs.Prototypes.MainScriptDocument();
- doc.globalGroupDatabase = Docs.Prototypes.MainGroupDocument();
- if (!doc.myLinkDatabase) doc.myLinkDatabase = new List([]);
+ await this.setupMenuPanel(doc, sharingDocumentId, linkDatabaseId);
+ if (!doc.globalScriptDatabase) doc.globalScriptDatabase = Docs.Prototypes.MainScriptDocument();
setTimeout(() => this.setupDefaultPresentation(doc), 0); // presentation that's initially triggered
@@ -1009,9 +1028,13 @@ export class CurrentUserUtils {
}
public static async loadCurrentUser() {
- return rp.get(Utils.prepend("/getCurrentUser")).then(response => {
+ return rp.get(Utils.prepend("/getCurrentUser")).then(async response => {
if (response) {
- const result: { id: string, email: string } = JSON.parse(response);
+ const result: { id: string, email: string, cacheDocumentIds: string } = JSON.parse(response);
+ Doc.CurrentUserEmail = result.email;
+ resolvedPorts = JSON.parse(await Networking.FetchFromServer("/resolvedPorts"));
+ DocServer.init(window.location.protocol, window.location.hostname, resolvedPorts.socket, result.email);
+ result.cacheDocumentIds && (await DocServer.GetRefFields(result.cacheDocumentIds.split(";")));
return result;
} else {
throw new Error("There should be a user! Why does Dash think there isn't one?");
@@ -1019,14 +1042,13 @@ export class CurrentUserUtils {
});
}
- public static async loadUserDocument({ id, email }: { id: string, email: string }) {
+ public static async loadUserDocument(id: string) {
this.curr_id = id;
- Doc.CurrentUserEmail = email;
await rp.get(Utils.prepend("/getUserDocumentIds")).then(ids => {
- const { userDocumentId, sharingDocumentId } = JSON.parse(ids);
+ const { userDocumentId, sharingDocumentId, linkDatabaseId } = JSON.parse(ids);
if (userDocumentId !== "guest") {
return DocServer.GetRefField(userDocumentId).then(async field =>
- this.updateUserDocument(Doc.SetUserDoc(field instanceof Doc ? field : new Doc(userDocumentId, true)), sharingDocumentId));
+ this.updateUserDocument(Doc.SetUserDoc(field instanceof Doc ? field : new Doc(userDocumentId, true)), sharingDocumentId, linkDatabaseId));
} else {
throw new Error("There should be a user id! Why does Dash think there isn't one?");
}
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 178daf5f0..a408e1df6 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -164,13 +164,14 @@ export class DocumentManager {
const sameContext = annotatedDoc && annotatedDoc === originatingDoc?.context;
if (originatingDoc?.isPushpin) {
const hide = !docView.props.Document.hidden;
- (!hide || !sameContext) && (docView.props.Document.hidden = !docView.props.Document.hidden);
- docView.props.focus(docView.props.Document, willZoom, undefined, (notfocused: boolean) => { // bcz: Argh! TODO: Need to add a notFocused argument to the after finish callback function that indicates whether the window had to scroll to show the target
- notfocused && hide && (docView.props.Document.hidden = true);
+ docView.props.focus(docView.props.Document, willZoom, undefined, (notfocused: boolean) => { // bcz: Argh! TODO: Need to add a notFocused argument to the after finish callback function that indicates whether the window had to scroll to show the target
+ if (notfocused || docView.props.Document.hidden) {
+ docView.props.Document.hidden = !docView.props.Document.hidden;
+ }
return focusAndFinish();
// @ts-ignore bcz: Argh TODO: Need to add a parameter to focus() everywhere for whether focus should center the target's container in the view or not. // here we don't want to focus the container if the source and target are in the same container
}, sameContext);
- finished?.();
+ //finished?.();
}
else {
docView.select(false);
@@ -219,7 +220,7 @@ export class DocumentManager {
}
} else { // there's no context view so we need to create one first and try again when that finishes
createViewFunc(targetDocContext, // after creating the context, this calls the finish function that will retry looking for the target
- () => this.jumpToDocument(targetDoc, willZoom, createViewFunc, undefined, linkDoc, true /* if we don't find the target, we want to get rid of the context just created */, undefined, finished));
+ () => this.jumpToDocument(targetDoc, willZoom, createViewFunc, docContext, linkDoc, true /* if we don't find the target, we want to get rid of the context just created */, undefined, finished));
}
}
}
diff --git a/src/client/util/GroupManager.tsx b/src/client/util/GroupManager.tsx
index 3264b75f7..a9059ff3d 100644
--- a/src/client/util/GroupManager.tsx
+++ b/src/client/util/GroupManager.tsx
@@ -5,15 +5,15 @@ import * as React from "react";
import Select from 'react-select';
import * as RequestPromise from "request-promise";
import { Doc, DocListCast, DocListCastAsync, Opt } from "../../fields/Doc";
-import { StrCast } from "../../fields/Types";
-import { setGroups } from "../../fields/util";
+import { StrCast, Cast } from "../../fields/Types";
import { Utils } from "../../Utils";
-import { DocServer } from "../DocServer";
import { MainViewModal } from "../views/MainViewModal";
import { TaskCompletionBox } from "../views/nodes/TaskCompletedBox";
import "./GroupManager.scss";
import { GroupMemberView } from "./GroupMemberView";
import { SharingManager, User } from "./SharingManager";
+import { listSpec } from "../../fields/Schema";
+import { DateField } from "../../fields/DateField";
/**
* Interface for options for the react-select component
@@ -34,65 +34,23 @@ export class GroupManager extends React.Component<{}> {
@observable private createGroupModalOpen: boolean = false;
private inputRef: React.RefObject<HTMLInputElement> = React.createRef(); // the ref for the input box.
private createGroupButtonRef: React.RefObject<HTMLButtonElement> = React.createRef(); // the ref for the group creation button
- private currentUserGroups: string[] = ["Public"]; // the list of groups the current user is a member of
@observable private buttonColour: "#979797" | "black" = "#979797";
@observable private groupSort: "ascending" | "descending" | "none" = "none";
- private populating: boolean = false;
- private groupListener: Opt<Lambda>;
- private observableAllGroups: Doc[] = observable([]);
-
-
constructor(props: Readonly<{}>) {
super(props);
GroupManager.Instance = this;
}
- /**
- * Populates the list of users and groups.
- */
- componentDidMount() {
- this.populateUsers();
- this.groupListener = autorun(this.populateGroups);
- }
-
- componentWillUnmount() {
- this.groupListener?.();
- }
+ componentDidMount() { this.populateUsers(); }
/**
* Fetches the list of users stored on the database.
*/
populateUsers = async () => {
- if (!this.populating) {
- this.populating = true;
- runInAction(() => this.users = []);
- const userList = await RequestPromise.get(Utils.prepend("/getUsers"));
- const raw = JSON.parse(userList) as User[];
- const evaluating = raw.map(async user => {
- const userSharingDocument = await DocServer.GetRefField(user.sharingDocumentId);
- if (userSharingDocument instanceof Doc) {
- runInAction(() => this.users.push(user.email));
- }
- });
- return Promise.all(evaluating).then(() => this.populating = false);
- }
- }
-
- /**
- * Populates the list of groups the current user is a member of and sets this list to be used in the GetEffectiveAcl in util.ts
- */
- populateGroups = () => {
- this.observableAllGroups.map(g => g.members);
- DocListCastAsync(this.GroupManagerDoc?.data).then(groups => {
- groups?.forEach(group => {
- const members: string[] = JSON.parse(StrCast(group.members));
- if (members.includes(Doc.CurrentUserEmail) && this.currentUserGroups.indexOf(StrCast(group.groupName)) === -1) this.currentUserGroups.push(StrCast(group.groupName));
- if (this.observableAllGroups.indexOf(group) === -1) runInAction(() => this.observableAllGroups.push(group));
- });
- setGroups(this.currentUserGroups);
- console.log("populating groups");
- });
+ const userList = await RequestPromise.get(Utils.prepend("/getUsers"));
+ const raw = JSON.parse(userList) as User[];
+ raw.map(action(user => !this.users.some(umail => umail === user.email) && this.users.push(user.email)));
}
/**
@@ -110,7 +68,6 @@ export class GroupManager extends React.Component<{}> {
// SelectionManager.DeselectAll();
this.isOpen = true;
this.populateUsers();
- this.populateGroups();
}
/**
@@ -129,25 +86,24 @@ export class GroupManager extends React.Component<{}> {
/**
* @returns the database of groups.
*/
- get GroupManagerDoc(): Doc | undefined {
- return Doc.UserDoc().globalGroupDatabase as Doc;
- }
+ @computed get GroupManagerDoc(): Doc | undefined { return Doc.UserDoc().globalGroupDatabase as Doc; }
/**
* @returns a list of all group documents.
*/
- getAllGroups(): Doc[] {
- const groupDoc = this.GroupManagerDoc;
- return groupDoc ? DocListCast(groupDoc.data) : [];
- }
+ @computed get allGroups(): Doc[] { return DocListCast(this.GroupManagerDoc?.data); }
+
+ /**
+ * @returns the members of the admin group.
+ */
+ @computed get adminGroupMembers(): string[] { return this.getGroup("Admin") ? JSON.parse(StrCast(this.getGroup("Admin")!.members)) : ""; }
/**
* @returns a group document based on the group name.
* @param groupName
*/
getGroup(groupName: string): Doc | undefined {
- const groupDoc = this.getAllGroups().find(group => group.groupName === groupName);
- return groupDoc;
+ return this.allGroups.find(group => group.title === groupName);
}
/**
@@ -155,15 +111,9 @@ export class GroupManager extends React.Component<{}> {
*/
getGroupMembers(group: string | Doc): string[] {
if (group instanceof Doc) return JSON.parse(StrCast(group.members)) as string[];
- else return JSON.parse(StrCast(this.getGroup(group)!.members)) as string[];
+ return JSON.parse(StrCast(this.getGroup(group)!.members)) as string[];
}
- /**
- * @returns the members of the admin group.
- */
- get adminGroupMembers(): string[] {
- return this.getGroup("Admin") ? JSON.parse(StrCast(this.getGroup("Admin")!.members)) : "";
- }
/**
* @returns a boolean indicating whether the current user has access to edit group documents.
@@ -181,14 +131,11 @@ export class GroupManager extends React.Component<{}> {
* @param memberEmails
*/
createGroupDoc(groupName: string, memberEmails: string[] = []) {
- const groupDoc = new Doc;
- groupDoc.groupName = groupName.toLowerCase() === "admin" ? "Admin" : groupName;
+ const name = groupName.toLowerCase() === "admin" ? "Admin" : groupName;
+ const groupDoc = new Doc("GROUP:" + name, true);
+ groupDoc.title = name;
groupDoc.owners = JSON.stringify([Doc.CurrentUserEmail]);
groupDoc.members = JSON.stringify(memberEmails);
- if (memberEmails.includes(Doc.CurrentUserEmail)) {
- this.currentUserGroups.push(groupName);
- setGroups(this.currentUserGroups);
- }
this.addGroup(groupDoc);
}
@@ -199,6 +146,7 @@ export class GroupManager extends React.Component<{}> {
addGroup(groupDoc: Doc): boolean {
if (this.GroupManagerDoc) {
Doc.AddDocToList(this.GroupManagerDoc, "data", groupDoc);
+ this.GroupManagerDoc.lastModified = new DateField;
return true;
}
return false;
@@ -208,19 +156,20 @@ export class GroupManager extends React.Component<{}> {
* Deletes a group from the database of group documents and @returns whether the group was deleted or not.
* @param group
*/
+ @action
deleteGroup(group: Doc): boolean {
if (group) {
if (this.GroupManagerDoc && this.hasEditAccess(group)) {
Doc.RemoveDocFromList(this.GroupManagerDoc, "data", group);
SharingManager.Instance.removeGroup(group);
- const members: string[] = JSON.parse(StrCast(group.members));
+ const members = JSON.parse(StrCast(group.members));
if (members.includes(Doc.CurrentUserEmail)) {
- const index = this.currentUserGroups.findIndex(groupName => groupName === group.groupName);
- index !== -1 && this.currentUserGroups.splice(index, 1);
- setGroups(this.currentUserGroups);
+ const index = DocListCast(this.GroupManagerDoc.data).findIndex(grp => grp === group);
+ index !== -1 && Cast(this.GroupManagerDoc.data, listSpec(Doc), [])?.splice(index, 1);
}
+ this.GroupManagerDoc.lastModified = new DateField;
if (group === this.currentGroup) {
- runInAction(() => this.currentGroup = undefined);
+ this.currentGroup = undefined;
}
return true;
}
@@ -235,10 +184,11 @@ export class GroupManager extends React.Component<{}> {
*/
addMemberToGroup(groupDoc: Doc, email: string) {
if (this.hasEditAccess(groupDoc)) {
- const memberList: string[] = JSON.parse(StrCast(groupDoc.members));
+ const memberList = JSON.parse(StrCast(groupDoc.members));
!memberList.includes(email) && memberList.push(email);
groupDoc.members = JSON.stringify(memberList);
SharingManager.Instance.shareWithAddedMember(groupDoc, email);
+ this.GroupManagerDoc && (this.GroupManagerDoc.lastModified = new DateField);
}
}
@@ -249,12 +199,13 @@ export class GroupManager extends React.Component<{}> {
*/
removeMemberFromGroup(groupDoc: Doc, email: string) {
if (this.hasEditAccess(groupDoc)) {
- const memberList: string[] = JSON.parse(StrCast(groupDoc.members));
+ const memberList = JSON.parse(StrCast(groupDoc.members));
const index = memberList.indexOf(email);
if (index !== -1) {
const user = memberList.splice(index, 1)[0];
groupDoc.members = JSON.stringify(memberList);
SharingManager.Instance.removeMember(groupDoc, email);
+ this.GroupManagerDoc && (this.GroupManagerDoc.lastModified = new DateField);
}
}
}
@@ -384,14 +335,13 @@ export class GroupManager extends React.Component<{}> {
private get groupInterface() {
const sortGroups = (d1: Doc, d2: Doc) => {
- const g1 = StrCast(d1.groupName);
- const g2 = StrCast(d2.groupName);
+ const g1 = StrCast(d1.title);
+ const g2 = StrCast(d2.title);
return g1 < g2 ? -1 : g1 === g2 ? 0 : 1;
};
- let groups = this.getAllGroups();
- groups = this.groupSort === "ascending" ? groups.sort(sortGroups) : this.groupSort === "descending" ? groups.sort(sortGroups).reverse() : groups;
+ const groups = this.groupSort === "ascending" ? this.allGroups.sort(sortGroups) : this.groupSort === "descending" ? this.allGroups.sort(sortGroups).reverse() : this.allGroups;
return (
<div className="group-interface">
@@ -424,9 +374,9 @@ export class GroupManager extends React.Component<{}> {
{groups.map(group =>
<div
className="group-row"
- key={StrCast(group.groupName)}
+ key={StrCast(group.title || group.groupName)}
>
- <div className="group-name" >{group.groupName}</div>
+ <div className="group-name" >{group.title || group.groupName}</div>
<div className="group-info" onClick={action(() => this.currentGroup = group)}>
<FontAwesomeIcon icon={"info-circle"} color={"#e8e8e8"} size={"sm"} style={{ backgroundColor: "#1e89d7", borderRadius: "100%", border: "1px solid #1e89d7" }} />
</div>
@@ -440,16 +390,13 @@ export class GroupManager extends React.Component<{}> {
}
render() {
- return (
- <MainViewModal
- contents={this.groupInterface}
- isDisplayed={this.isOpen}
- interactive={true}
- dialogueBoxStyle={{ zIndex: 1002 }}
- overlayStyle={{ zIndex: 1001 }}
- closeOnExternalClick={this.close}
- />
- );
+ return <MainViewModal
+ contents={this.groupInterface}
+ isDisplayed={this.isOpen}
+ interactive={true}
+ dialogueBoxStyle={{ zIndex: 1002 }}
+ overlayStyle={{ zIndex: 1001 }}
+ closeOnExternalClick={this.close}
+ />;
}
-
} \ No newline at end of file
diff --git a/src/client/util/GroupMemberView.tsx b/src/client/util/GroupMemberView.tsx
index 4ead01e9f..927200ed3 100644
--- a/src/client/util/GroupMemberView.tsx
+++ b/src/client/util/GroupMemberView.tsx
@@ -33,8 +33,8 @@ export class GroupMemberView extends React.Component<GroupMemberViewProps> {
<input
className="group-title"
style={{ marginLeft: !hasEditAccess ? "-14%" : 0 }}
- value={StrCast(this.props.group.groupName)}
- onChange={e => this.props.group.groupName = e.currentTarget.value}
+ value={StrCast(this.props.group.title || this.props.group.groupName)}
+ onChange={e => this.props.group.title = e.currentTarget.value}
disabled={!hasEditAccess}
>
</input>
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index 56b6cb8a9..1ba6cff6d 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -1,6 +1,4 @@
import { Doc, DocListCast, Opt } from "../../fields/Doc";
-import { List } from "../../fields/List";
-import { listSpec } from "../../fields/Schema";
import { Cast, StrCast } from "../../fields/Types";
import { SharingManager } from "./SharingManager";
import { computedFn } from "mobx-utils";
@@ -36,17 +34,21 @@ export class LinkManager {
public getAllLinks(): Doc[] {
- const lset = new Set<Doc>(DocListCast(Doc.UserDoc().myLinkDatabase));
- SharingManager.Instance.users.forEach(user => DocListCast(user.sharingDoc.myLinkDatabase).map(lset.add));
+ const lset = new Set<Doc>(DocListCast(Doc.LinkDBDoc().data));
+ SharingManager.Instance.users.forEach(user => {
+ DocListCast(user.linkDatabase?.data).map(doc => {
+ lset.add(doc);
+ });
+ });
return Array.from(lset);
}
public addLink(linkDoc: Doc): boolean {
- return Doc.AddDocToList(Doc.UserDoc(), "myLinkDatabase", linkDoc);
+ return Doc.AddDocToList(Doc.LinkDBDoc(), "data", linkDoc);
}
public deleteLink(linkDoc: Doc): boolean {
- return Doc.RemoveDocFromList(Doc.UserDoc(), "myLinkDatabase", linkDoc);
+ return Doc.RemoveDocFromList(Doc.LinkDBDoc(), "data", linkDoc);
}
// finds all links that contain the given anchor
@@ -67,7 +69,7 @@ export class LinkManager {
related.push(...LinkManager.Instance.getAllRelatedLinks(anno));
});
return related;
- }.bind(this));
+ }.bind(this), true);
// finds all links that contain the given anchor
public getAllRelatedLinks(anchor: Doc): Doc[] {
diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index 008ce281c..34e88c7b0 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -2,7 +2,6 @@ import { observable, action, runInAction, ObservableMap } from "mobx";
import { Doc, Opt } from "../../fields/Doc";
import { DocumentView } from "../views/nodes/DocumentView";
import { computedFn } from "mobx-utils";
-import { List } from "../../fields/List";
import { CollectionSchemaView } from "../views/collections/CollectionSchemaView";
import { CollectionViewType } from "../views/collections/CollectionView";
@@ -67,15 +66,16 @@ export namespace SelectionManager {
manager.SelectSchemaDoc(colSchema, document);
}
+ const IsSelectedCache = computedFn(function isSelected(doc: DocumentView) { // wraapping get() in a computedFn only generates mobx() invalidations when the return value of the function for the specific get parameters has changed
+ return manager.SelectedDocuments.get(doc) ? true : false;
+ });
// computed functions, such as used in IsSelected generate errors if they're called outside of a
// reaction context. Specifying the context with 'outsideReaction' allows an efficiency feature
// to avoid unnecessary mobx invalidations when running inside a reaction.
export function IsSelected(doc: DocumentView | undefined, outsideReaction?: boolean): boolean {
return !doc ? false : outsideReaction ?
manager.SelectedDocuments.get(doc) ? true : false : // get() accesses a hashtable -- setting anything in the hashtable generates a mobx invalidation for every get()
- computedFn(function isSelected(doc: DocumentView) { // wraapping get() in a computedFn only generates mobx() invalidations when the return value of the function for the specific get parameters has changed
- return manager.SelectedDocuments.get(doc) ? true : false;
- })(doc);
+ IsSelectedCache(doc);
}
export function DeselectAll(except?: Doc): void {
diff --git a/src/client/util/SerializationHelper.ts b/src/client/util/SerializationHelper.ts
index 19b217726..00ac6e521 100644
--- a/src/client/util/SerializationHelper.ts
+++ b/src/client/util/SerializationHelper.ts
@@ -43,7 +43,7 @@ export namespace SerializationHelper {
}
if (!obj.__type) {
- if (ClientUtils.RELEASE) {
+ if (true || ClientUtils.RELEASE) {
console.warn("No property 'type' found in JSON.");
return undefined;
} else {
diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx
index 742811c18..360fad998 100644
--- a/src/client/util/SharingManager.tsx
+++ b/src/client/util/SharingManager.tsx
@@ -26,6 +26,7 @@ import { SearchBox } from "../views/search/SearchBox";
export interface User {
email: string;
sharingDocumentId: string;
+ linkDatabaseId: string;
}
/**
@@ -52,6 +53,7 @@ const storage = "data";
interface ValidatedUser {
user: User; // database minimal info to identify / communicate with a user (email, sharing doc id)
sharingDoc: Doc; // document to share/message another user
+ linkDatabase: Doc;
userColor: string; // stored on the sharinDoc, extracted for convenience?
}
@@ -138,8 +140,10 @@ export class SharingManager extends React.Component<{}> {
const isCandidate = user.email !== Doc.CurrentUserEmail;
if (isCandidate) {
const sharingDoc = await DocServer.GetRefField(user.sharingDocumentId);
- if (sharingDoc instanceof Doc) {
- sharingDocs.push({ user, sharingDoc, userColor: StrCast(sharingDoc.color) });
+ const linkDatabase = await DocServer.GetRefField(user.linkDatabaseId);
+ if (sharingDoc instanceof Doc && linkDatabase instanceof Doc) {
+ await DocListCastAsync(linkDatabase.data);
+ sharingDocs.push({ user, sharingDoc, linkDatabase, userColor: StrCast(sharingDoc.color) });
}
}
});
@@ -161,10 +165,10 @@ export class SharingManager extends React.Component<{}> {
* @param group
* @param permission
*/
- setInternalGroupSharing = (group: Doc | { groupName: string }, permission: string, targetDoc?: Doc) => {
+ setInternalGroupSharing = (group: Doc | { title: string }, permission: string, targetDoc?: Doc) => {
const target = targetDoc || this.targetDoc!;
- const key = normalizeEmail(StrCast(group.groupName));
+ const key = normalizeEmail(StrCast(group.title));
const acl = `acl-${key}`;
const docs = SelectionManager.SelectedDocuments().length < 2 ? [target] : SelectionManager.SelectedDocuments().map(docView => docView.props.Document);
@@ -181,8 +185,8 @@ export class SharingManager extends React.Component<{}> {
group.docsShared ? Doc.IndexOf(doc, DocListCast(group.docsShared)) === -1 && (group.docsShared as List<Doc>).push(doc) : group.docsShared = new List<Doc>([doc]);
users.forEach(({ user, sharingDoc }) => {
- if (permission !== SharingPermissions.None) Doc.IndexOf(doc, DocListCast(sharingDoc[storage])) === -1 && Doc.AddDocToList(sharingDoc, storage, doc); // add the doc to the sharingDoc if it hasn't already been added
- else GetEffectiveAcl(doc, undefined, user.email) === AclPrivate && Doc.IndexOf((doc.aliasOf as Doc || doc), DocListCast(sharingDoc[storage])) !== -1 && Doc.RemoveDocFromList(sharingDoc, storage, (doc.aliasOf as Doc || doc)); // remove the doc from the list if it already exists
+ if (permission !== SharingPermissions.None) Doc.AddDocToList(sharingDoc, storage, doc); // add the doc to the sharingDoc if it hasn't already been added
+ else GetEffectiveAcl(doc, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, storage, (doc.aliasOf as Doc || doc)); // remove the doc from the list if it already exists
});
}
});
@@ -193,22 +197,18 @@ export class SharingManager extends React.Component<{}> {
* @param group
* @param emailId
*/
- shareWithAddedMember = (group: Doc, emailId: string) => {
- const user: ValidatedUser = this.users.find(({ user: { email } }) => email === emailId)!;
+ shareWithAddedMember = (group: Doc, emailId: string, retry: boolean = true) => {
+ const user = this.users.find(({ user: { email } }) => email === emailId)!;
+ const self = this;
if (group.docsShared) {
- DocListCastAsync(group.docsShared).then(async docs => {
- if (docs) {
- const memberDocs = await DocListCastAsync(user.sharingDoc[storage]);
- memberDocs && docs.forEach(doc => {
- const index = Doc.IndexOf(doc, memberDocs);
- console.log(index);
- console.log(doc);
- index === -1 && (console.log(Doc.AddDocToList(user.sharingDoc, storage, doc)));
- });
- }
-
- });
- // === -1 && Doc.AddDocToList(user.sharingDoc, storage, doc));
+ if (!user) retry && this.populateUsers().then(() => self.shareWithAddedMember(group, emailId, false));
+ else {
+ DocListCastAsync(user.sharingDoc[storage]).then(userdocs =>
+ DocListCastAsync(group.docsShared).then(dl => {
+ const filtered = dl?.filter(doc => !userdocs?.includes(doc));
+ filtered && userdocs?.push(...filtered);
+ }));
+ }
}
}
@@ -239,16 +239,12 @@ export class SharingManager extends React.Component<{}> {
const user: ValidatedUser = this.users.find(({ user: { email } }) => email === emailId)!;
if (group.docsShared) {
- DocListCastAsync(group.docsShared).then(docs => {
- docs?.forEach(doc => {
- DocListCastAsync(user.sharingDoc[storage]).then(sharedDocs => {
- sharedDocs && Doc.IndexOf(doc, sharedDocs) !== -1 && Doc.RemoveDocFromList(user.sharingDoc, storage, doc);
- });
- });
- });
- // DocListCast(group.docsShared).forEach(doc => {
- // Doc.IndexOf(doc, DocListCast(user.sharingDoc[storage])) !== -1 && Doc.RemoveDocFromList(user.sharingDoc, storage, doc); // remove the doc only if it is in the list
- // });
+ DocListCastAsync(user.sharingDoc[storage]).then(userdocs =>
+ DocListCastAsync(group.docsShared).then(dl => {
+ const remaining = userdocs?.filter(doc => !dl?.includes(doc)) || [];
+ userdocs?.splice(0, userdocs.length, ...remaining);
+ })
+ );
}
}
@@ -259,7 +255,7 @@ export class SharingManager extends React.Component<{}> {
removeGroup = (group: Doc) => {
if (group.docsShared) {
DocListCast(group.docsShared).forEach(doc => {
- const acl = `acl-${StrCast(group.groupName)}`;
+ const acl = `acl-${StrCast(group.title)}`;
distributeAcls(acl, SharingPermissions.None, doc);
@@ -285,8 +281,8 @@ export class SharingManager extends React.Component<{}> {
doc.author === Doc.CurrentUserEmail && !doc[myAcl] && distributeAcls(myAcl, SharingPermissions.Admin, doc);
distributeAcls(acl, permission as SharingPermissions, doc);
- if (permission !== SharingPermissions.None) Doc.IndexOf(doc, DocListCast(sharingDoc[storage])) === -1 && Doc.AddDocToList(sharingDoc, storage, doc);
- else GetEffectiveAcl(doc, undefined, user.email) === AclPrivate && Doc.IndexOf((doc.aliasOf as Doc || doc), DocListCast(sharingDoc[storage])) !== -1 && Doc.RemoveDocFromList(sharingDoc, storage, (doc.aliasOf as Doc || doc));
+ if (permission !== SharingPermissions.None) Doc.AddDocToList(sharingDoc, storage, doc);
+ else GetEffectiveAcl(doc, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, storage, (doc.aliasOf as Doc || doc));
});
}
@@ -429,8 +425,8 @@ export class SharingManager extends React.Component<{}> {
* Sorting algorithm to sort groups.
*/
sortGroups = (group1: Doc, group2: Doc) => {
- const g1 = StrCast(group1.groupName);
- const g2 = StrCast(group2.groupName);
+ const g1 = StrCast(group1.title);
+ const g2 = StrCast(group2.title);
return g1 < g2 ? -1 : g1 === g2 ? 0 : 1;
}
@@ -439,9 +435,9 @@ export class SharingManager extends React.Component<{}> {
*/
@computed get sharingInterface() {
TraceMobx();
- const groupList = GroupManager.Instance?.getAllGroups() || [];
+ const groupList = GroupManager.Instance?.allGroups || [];
const sortedUsers = this.users.slice().sort(this.sortUsers).map(({ user: { email } }) => ({ label: email, value: indType + email }));
- const sortedGroups = groupList.slice().sort(this.sortGroups).map(({ groupName }) => ({ label: StrCast(groupName), value: groupType + StrCast(groupName) }));
+ const sortedGroups = groupList.slice().sort(this.sortGroups).map(({ title }) => ({ label: StrCast(title), value: groupType + StrCast(title) }));
// the next block handles the users shown (individuals/groups/both)
const options: GroupedOptions[] = [];
@@ -479,7 +475,7 @@ export class SharingManager extends React.Component<{}> {
const commonKeys = intersection(...docs.map(doc => this.layoutDocAcls ? doc?.[AclSym] && Object.keys(doc[AclSym]) : doc?.[DataSym]?.[AclSym] && Object.keys(doc[DataSym][AclSym])));
// the list of users shared with
- const userListContents: (JSX.Element | null)[] = users.filter(({ user }) => docs.length > 1 ? commonKeys.includes(`acl-${normalizeEmail(user.email)}`) : docs[0]?.author !== user.email).map(({ user, sharingDoc, userColor }) => {
+ const userListContents: (JSX.Element | null)[] = users.filter(({ user }) => docs.length > 1 ? commonKeys.includes(`acl-${normalizeEmail(user.email)}`) : docs[0]?.author !== user.email).map(({ user, linkDatabase, sharingDoc, userColor }) => {
const userKey = `acl-${normalizeEmail(user.email)}`;
const uniform = docs.every(doc => this.layoutDocAcls ? doc?.[AclSym]?.[userKey] === docs[0]?.[AclSym]?.[userKey] : doc?.[DataSym]?.[AclSym]?.[userKey] === docs[0]?.[DataSym]?.[AclSym]?.[userKey]);
const permissions = uniform ? StrCast(targetDoc?.[userKey]) : "-multiple-";
@@ -495,7 +491,7 @@ export class SharingManager extends React.Component<{}> {
<select
className={"permissions-dropdown"}
value={permissions}
- onChange={e => this.setInternalSharing({ user, sharingDoc: sharingDoc, userColor }, e.currentTarget.value)}
+ onChange={e => this.setInternalSharing({ user, linkDatabase, sharingDoc, userColor }, e.currentTarget.value)}
>
{this.sharingOptions(uniform)}
</select>
@@ -546,19 +542,19 @@ export class SharingManager extends React.Component<{}> {
// the list of groups shared with
- const groupListMap: (Doc | { groupName: string })[] = groups.filter(({ groupName }) => docs.length > 1 ? commonKeys.includes(`acl-${normalizeEmail(StrCast(groupName))}`) : true);
- groupListMap.unshift({ groupName: "Public" }, { groupName: "Override" });
+ const groupListMap: (Doc | { title: string })[] = groups.filter(({ title }) => docs.length > 1 ? commonKeys.includes(`acl-${normalizeEmail(StrCast(title))}`) : true);
+ groupListMap.unshift({ title: "Public" }, { title: "Override" });
const groupListContents = groupListMap.map(group => {
- const groupKey = `acl-${StrCast(group.groupName)}`;
+ const groupKey = `acl-${StrCast(group.title)}`;
const uniform = docs.every(doc => this.layoutDocAcls ? doc?.[AclSym]?.[groupKey] === docs[0]?.[AclSym]?.[groupKey] : doc?.[DataSym]?.[AclSym]?.[groupKey] === docs[0]?.[DataSym]?.[AclSym]?.[groupKey]);
- const permissions = uniform ? StrCast(targetDoc?.[`acl-${StrCast(group.groupName)}`]) : "-multiple-";
+ const permissions = uniform ? StrCast(targetDoc?.[`acl-${StrCast(group.title)}`]) : "-multiple-";
return !permissions ? (null) : (
<div
key={groupKey}
className={"container"}
>
- <div className={"padding"}>{group.groupName}</div>
+ <div className={"padding"}>{group.title}</div>
{group instanceof Doc ?
(<div className="group-info" onClick={action(() => GroupManager.Instance.currentGroup = group)}>
<FontAwesomeIcon icon={"info-circle"} color={"#e8e8e8"} size={"sm"} style={{ backgroundColor: "#1e89d7", borderRadius: "100%", border: "1px solid #1e89d7" }} />
@@ -571,7 +567,7 @@ export class SharingManager extends React.Component<{}> {
value={permissions}
onChange={e => this.setInternalGroupSharing(group, e.currentTarget.value)}
>
- {this.sharingOptions(uniform, group.groupName === "Override")}
+ {this.sharingOptions(uniform, group.title === "Override")}
</select>
) : (
<div className={"permissions-dropdown"}>
diff --git a/src/client/util/SnappingManager.ts b/src/client/util/SnappingManager.ts
index fc07e8ab4..069f81d38 100644
--- a/src/client/util/SnappingManager.ts
+++ b/src/client/util/SnappingManager.ts
@@ -1,4 +1,5 @@
import { observable, action, runInAction } from "mobx";
+import { computedFn } from "mobx-utils";
export namespace SnappingManager {
@@ -14,6 +15,9 @@ export namespace SnappingManager {
this.horizSnapLines = horizLines;
this.vertSnapLines = vertLines;
}
+
+ @observable cachedGroups: string[] = [];
+ @action setCachedGroups(groups: string[]) { this.cachedGroups = groups; }
}
const manager = new Manager();
@@ -25,5 +29,11 @@ export namespace SnappingManager {
export function SetIsDragging(dragging: boolean) { runInAction(() => manager.IsDragging = dragging); }
export function GetIsDragging() { return manager.IsDragging; }
+
+ /// bcz; argh!! TODO; These do not belong here, but there were include order problems with leaving them in util.ts
+ // need to investigate further what caused the mobx update problems and move to a better location.
+ const getCachedGroupByNameCache = computedFn(function (name: string) { return manager.cachedGroups.includes(name); }, true);
+ export function GetCachedGroupByName(name: string) { return getCachedGroupByNameCache(name); }
+ export function SetCachedGroups(groups: string[]) { manager.setCachedGroups(groups); }
}
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index 98e888538..a55f4adaf 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -164,7 +164,7 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T
return false;
}
else {
- if (this.props.Document[AclSym]) {
+ if (this.props.Document[AclSym] && Object.keys(this.props.Document[AclSym]).length) {
added.forEach(d => {
for (const [key, value] of Object.entries(this.props.Document[AclSym])) {
if (d.author === denormalizeEmail(key.substring(4)) && !d.aliasOf) distributeAcls(key, SharingPermissions.Admin, d, true);
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index d8c32a919..9dddb4c44 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -25,6 +25,7 @@ import { GoogleRef } from "./nodes/formattedText/FormattedTextBox";
import { TemplateMenu } from "./TemplateMenu";
import React = require("react");
import { PresBox } from './nodes/PresBox';
+import { undoBatch } from '../util/UndoManager';
const higflyout = require("@hig/flyout");
export const { anchorPoints } = higflyout;
export const Flyout = higflyout.default;
@@ -199,7 +200,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
const targetDoc = this.view0?.props.Document;
return !targetDoc ? (null) : <Tooltip title={<><div className="dash-tooltip">{"Pin with current view"}</div></>}>
<div className="documentButtonBar-linker"
- onClick={e => {
+ onClick={undoBatch(e => {
if (targetDoc) {
TabDocView.PinDoc(targetDoc, false);
const activeDoc = PresBox.Instance.childDocs[PresBox.Instance.childDocs.length - 1];
@@ -211,7 +212,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
activeDoc.presPinViewY = y;
activeDoc.presPinViewScale = scale;
}
- }}>
+ })}>
{presPinWithViewIcon}
</div>
</Tooltip>;
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 066cfd95a..66f0dd99d 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -450,7 +450,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
height = !height || isNaN(height) ? 20 : height;
const scale = docView.props.ScreenToLocalTransform().Scale * docView.props.ContentScaling();
if (nwidth && nheight) {
- if (nwidth / nheight !== width / height) {
+ if (nwidth / nheight !== width / height && !dragBottom) {
height = nheight / nwidth * width;
}
if (e.ctrlKey || (!dragBottom || !docView.layoutDoc._fitWidth)) { // ctrl key enables modification of the nativeWidth or nativeHeight durin the interaction
diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx
index d35271ffd..8b1b12365 100644
--- a/src/client/views/EditableView.tsx
+++ b/src/client/views/EditableView.tsx
@@ -47,6 +47,7 @@ export interface EditableProps {
onClick?: (e: React.MouseEvent) => boolean;
isEditingCallback?: (isEditing: boolean) => void;
menuCallback?: (x: number, y: number) => void;
+ textCallback?: (char: string) => boolean;
showMenuOnLoad?: boolean;
HeadingObject?: SchemaHeaderField | undefined;
toggle?: () => void;
@@ -119,6 +120,12 @@ export class EditableView extends React.Component<EditableProps> {
case ":":
this.props.menuCallback?.(e.currentTarget.getBoundingClientRect().x, e.currentTarget.getBoundingClientRect().y);
break;
+ case "Shift": case "Alt": case "Meta": case "Control": break;
+ default:
+ if (this.props.textCallback?.(e.key)) {
+ this._editing = false;
+ this.props.isEditingCallback?.(false,);
+ }
}
}
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index 77e37834d..3889e2d28 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -10,17 +10,13 @@ import { CollectionView } from "./collections/CollectionView";
AssignAllExtensions();
-export let resolvedPorts: { server: number, socket: number };
-
(async () => {
window.location.search.includes("safe") && CollectionView.SetSafeMode(true);
const info = await CurrentUserUtils.loadCurrentUser();
- resolvedPorts = JSON.parse(await Networking.FetchFromServer("/resolvedPorts"));
- DocServer.init(window.location.protocol, window.location.hostname, resolvedPorts.socket, info.email);
await Docs.Prototypes.initialize();
if (info.id !== "__guest__") {
// a guest will not have an id registered
- await CurrentUserUtils.loadUserDocument(info);
+ await CurrentUserUtils.loadUserDocument(info.id);
}
document.getElementById('root')!.addEventListener('wheel', event => {
if (event.ctrlKey) {
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index a64004c5c..6bdb6e21f 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -1040,7 +1040,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
{PresBox.Instance.optionsDropdown}
</div> : null}
</div>}
- <div className="propertiesView-presTrails">
+ {/* <div className="propertiesView-presTrails">
<div className="propertiesView-presTrails-title"
onPointerDown={action(() => { this.openAddSlide = !this.openAddSlide; })}
style={{ backgroundColor: this.openAddSlide ? "black" : "" }}>
@@ -1052,7 +1052,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
{this.openAddSlide ? <div className="propertiesView-presTrails-content">
{PresBox.Instance.newDocumentDropdown}
</div> : null}
- </div>
+ </div> */}
</div>;
}
}
diff --git a/src/client/views/collections/CollectionLinearView.tsx b/src/client/views/collections/CollectionLinearView.tsx
index 859ee9362..0eac5136a 100644
--- a/src/client/views/collections/CollectionLinearView.tsx
+++ b/src/client/views/collections/CollectionLinearView.tsx
@@ -38,8 +38,8 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) {
componentDidMount() {
// is there any reason this needs to exist? -syip. yes, it handles autoHeight for stacking views (masonry isn't yet supported).
- this._widthDisposer = reaction(() => this.props.Document[HeightSym]() + this.childDocs.length + (this.props.Document.linearViewIsExpanded ? 1 : 0),
- () => this.props.Document._width = 5 + (this.props.Document.linearViewIsExpanded ? this.childDocs.length * (this.props.Document[HeightSym]()) : 10),
+ this._widthDisposer = reaction(() => 5 + (this.props.Document.linearViewIsExpanded ? this.childDocs.length * (this.props.Document[HeightSym]()) : 10),
+ width => this.childDocs.length && (this.props.Document._width = width),
{ fireImmediately: true }
);
diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx
index 429764154..d9924c299 100644
--- a/src/client/views/collections/CollectionMenu.tsx
+++ b/src/client/views/collections/CollectionMenu.tsx
@@ -394,28 +394,39 @@ export class CollectionViewBaseChrome extends React.Component<CollectionMenuProp
</Tooltip>;
}
+ @undoBatch
+ @action
+ pinWithView = (targetDoc: Opt<Doc>) => {
+ if (targetDoc) {
+ TabDocView.PinDoc(targetDoc, false);
+ const activeDoc = PresBox.Instance.childDocs[PresBox.Instance.childDocs.length - 1];
+ if (targetDoc.type === DocumentType.PDF) {
+ const scroll = targetDoc._scrollTop;
+ activeDoc.presPinView = true;
+ activeDoc.presPinViewScroll = scroll;
+ } else {
+ const x = targetDoc._panX;
+ const y = targetDoc._panY;
+ const scale = targetDoc._viewScale;
+ activeDoc.presPinView = true;
+ activeDoc.presPinViewX = x;
+ activeDoc.presPinViewY = y;
+ activeDoc.presPinViewScale = scale;
+ }
+ }
+ }
+
@computed
get pinWithViewButton() {
const presPinWithViewIcon = <img src={`/assets/pinWithView.png`} style={{ margin: "auto", width: 19 }} />;
const targetDoc = this.selectedDoc;
- return (!targetDoc || (targetDoc._viewType !== CollectionViewType.Freeform && targetDoc.type !== DocumentType.IMG)) ? (null) : <Tooltip title={<><div className="dash-tooltip">{"Pin to presentation trail with current view"}</div></>} placement="top">
+ {/* return (!targetDoc || (targetDoc._viewType !== CollectionViewType.Freeform && targetDoc.type !== DocumentType.IMG)) ? (null) : <Tooltip title={<><div className="dash-tooltip">{"Pin to presentation trail with current view"}</div></>} placement="top"> */ }
+ return (targetDoc && (targetDoc._viewType === CollectionViewType.Freeform || targetDoc.type === DocumentType.IMG || targetDoc.type === DocumentType.PDF || targetDoc.type === DocumentType.WEB || targetDoc.type === DocumentType.RTF)) ? <Tooltip title={<><div className="dash-tooltip">{"Pin to presentation trail with current view"}</div></>} placement="top">
<button className="antimodeMenu-button" style={{ borderRight: "1px solid gray", borderLeft: "1px solid gray", justifyContent: 'center' }}
- onClick={e => {
- if (targetDoc) {
- TabDocView.PinDoc(targetDoc, false);
- const activeDoc = PresBox.Instance.childDocs[PresBox.Instance.childDocs.length - 1];
- const x = targetDoc._panX;
- const y = targetDoc._panY;
- const scale = targetDoc._viewScale;
- activeDoc.presPinView = true;
- activeDoc.presPinViewX = x;
- activeDoc.presPinViewY = y;
- activeDoc.presPinViewScale = scale;
- }
- }}>
+ onClick={() => this.pinWithView(targetDoc)}>
{presPinWithViewIcon}
</button>
- </Tooltip>;
+ </Tooltip> : (null);
}
@@ -528,7 +539,9 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionMenu
return SelectionManager.SelectedDocuments().length ? SelectionManager.SelectedDocuments()[0] : undefined;
}
@computed get selectedDoc() { return this.selectedDocumentView?.rootDoc; }
- @computed get isText() { return this.selectedDoc?.type === DocumentType.RTF || RichTextMenu.Instance?.view ? true : false; }
+ @computed get isText() {
+ return this.selectedDoc?.type === DocumentType.RTF || (RichTextMenu.Instance?.view as any)?.focused ? true : false;
+ }
@undoBatch
@action
diff --git a/src/client/views/collections/CollectionSchemaHeaders.tsx b/src/client/views/collections/CollectionSchemaHeaders.tsx
index dbf7488ec..b408fd680 100644
--- a/src/client/views/collections/CollectionSchemaHeaders.tsx
+++ b/src/client/views/collections/CollectionSchemaHeaders.tsx
@@ -261,7 +261,7 @@ export class KeysDropdown extends React.Component<KeysDropdownProps> {
componentDidMount() {
document.addEventListener("pointerdown", this.detectClick);
const filters = Cast(this.props.Document._docFilters, listSpec("string"));
- if (filters?.includes(this._key)) {
+ if (filters?.some(filter => filter.split(":")[0] === this._key)) {
runInAction(() => this.closeResultsVisibility = "contents");
}
}
@@ -396,19 +396,21 @@ export class KeysDropdown extends React.Component<KeysDropdownProps> {
});
const filters = Cast(this.props.Document._docFilters, listSpec("string"));
- if (filters === undefined || filters.length === 0 || filters.includes(this._key) === false) {
+ if (filters === undefined || filters.length === 0 || filters.some(filter => filter.split(":")[0] === this._key) === false) {
this.props.col.setColor("rgb(241, 239, 235)");
this.closeResultsVisibility = "none";
}
- for (let i = 0; i < (filters?.length ?? 0) - 1; i += 3) {
- if (filters![i] === this.props.col.heading && keyOptions.includes(filters![i + 1]) === false) {
+ for (let i = 0; i < (filters?.length ?? 0) - 1; i++) {
+ if (filters![i] === this.props.col.heading && keyOptions.includes(filters![i].split(":")[1]) === false) {
keyOptions.push(filters![i + 1]);
}
}
const options = keyOptions.map(key => {
let bool = false;
if (filters !== undefined) {
- bool = filters.includes(key) && filters[filters.indexOf(key) + 1] === "check";
+ const ind = filters.findIndex(filter => filter.split(":")[0] === key);
+ const fields = ind === -1 ? undefined : filters[ind].split(":");
+ bool = fields ? fields[1] === "check" : false;
}
return <div key={key} className="key-option" style={{
border: "1px solid lightgray", paddingLeft: 5, textAlign: "left",
@@ -453,7 +455,7 @@ export class KeysDropdown extends React.Component<KeysDropdownProps> {
updateFilter() {
const filters = Cast(this.props.Document._docFilters, listSpec("string"));
- if (filters === undefined || filters.length === 0 || filters.includes(this._key) === false) {
+ if (filters === undefined || filters.length === 0 || filters.some(filter => filter.split(":")[0] === this._key) === false) {
this.props.col.setColor("rgb(241, 239, 235)");
this.closeResultsVisibility = "none";
}
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index daaee67dc..3607b97d0 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -294,14 +294,17 @@ export class CollectionStackingView extends CollectionSubView<StackingDocument,
const docs = this.childDocList;
if (docs) {
newDocs.map((doc, i) => {
- console.log(doc.title);
if (i === 0) {
+ if (doc.presentationTargetDoc) doc.dragging = false; //glr: so it only applies to items in presentation
+ DragManager.docsBeingDragged = [];
if (targInd === -1) targInd = docs.length;
else targInd = docs.indexOf(this.filteredChildren[targInd]);
const srcInd = docs.indexOf(doc);
docs.splice(srcInd, 1);
docs.splice((targInd > srcInd ? targInd - 1 : targInd) + plusOne, 0, doc);
} else if (i < (newDocs.length / 2)) { //glr: for some reason dragged documents are duplicated
+ if (doc.presentationTargetDoc) doc.dragging = false;
+ DragManager.docsBeingDragged = [];
if (targInd === -1) targInd = docs.length;
else targInd = docs.indexOf(newDocs[0]) + 1;
const srcInd = docs.indexOf(doc);
diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
index 1bc989e83..b7562c45e 100644
--- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
+++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
@@ -131,8 +131,13 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
}
@action
- addDocument = (value: string, shiftDown?: boolean) => {
- if (!value) return false;
+ textCallback = (char: string) => {
+ return this.addDocument("", false, true);
+ }
+
+ @action
+ addDocument = (value: string, shiftDown?: boolean, forceEmptyNote?: boolean) => {
+ if (!value && !forceEmptyNote) return false;
const key = StrCast(this.props.parent.props.Document._pivotField);
const newDoc = Docs.Create.TextDocument(value, { _height: 18, _width: 200, title: value, _autoHeight: true });
newDoc[key] = this.getValue(this.props.heading);
@@ -140,7 +145,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
const heading = maxHeading === 0 || this.props.docList.length === 0 ? 1 : maxHeading === 1 ? 2 : 3;
newDoc.heading = heading;
FormattedTextBox.SelectOnLoad = newDoc[Id];
- FormattedTextBox.SelectOnLoadChar = " ";
+ FormattedTextBox.SelectOnLoadChar = forceEmptyNote ? "" : " ";
return this.props.parent.props.addDocument(newDoc);
}
@@ -300,6 +305,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
const newEditableViewProps = {
GetValue: () => "",
SetValue: this.addDocument,
+ textCallback: this.textCallback,
contents: "+ NEW",
HeadingObject: this.props.headingObject,
toggle: this.toggleVisibility,
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index fa80c8062..f3e563422 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -103,7 +103,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
const { Document, DataDoc } = this.props;
const validPairs = this.childDocs.map(doc => Doc.GetLayoutDataDocPair(Document, !this.props.annotationsKey ? 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.proto || (pair.layout.proto instanceof Doc && Object.keys(pair.layout.proto).length));
+ return pair.layout && (!pair.layout.proto || (pair.layout.proto instanceof Doc && GetEffectiveAcl(pair.layout.proto) !== AclPrivate));// Object.keys(pair.layout.proto).length));
});
return validPairs.map(({ data, layout }) => ({ data: data as Doc, layout: layout! })); // this mapping is a bit of a hack to coerce types
}
@@ -134,7 +134,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
rawdocs = rootDoc && !this.props.annotationsKey ? [Doc.GetProto(rootDoc)] : [];
}
- const docs = rawdocs.filter(d => !(d instanceof Promise)).map(d => d as Doc);
+ const docs = rawdocs.filter(d => !(d instanceof Promise) && GetEffectiveAcl(Doc.GetProto(d)) !== AclPrivate).map(d => d as Doc);
const viewSpecScript = Cast(this.props.Document.viewSpecScript, ScriptField);
const childDocs = viewSpecScript ? docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result) : docs;
@@ -502,4 +502,5 @@ import { SelectionManager } from "../../util/SelectionManager";
import { OverlayView } from "../OverlayView";
import { setTimeout } from "timers";
import { Hypothesis } from "../../util/HypothesisUtils";
+import { GetEffectiveAcl } from "../../../fields/util";
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 80e9b41ad..a27fa5a66 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -140,7 +140,7 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus
return false;
}
else {
- if (this.props.Document[AclSym]) {
+ if (this.props.Document[AclSym] && Object.keys(this.props.Document[AclSym])) {
added.forEach(d => {
for (const [key, value] of Object.entries(this.props.Document[AclSym])) {
if (d.author === denormalizeEmail(key.substring(4)) && !d.aliasOf) distributeAcls(key, SharingPermissions.Admin, d, true);
@@ -159,7 +159,8 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus
else {
added.filter(doc => [AclAdmin, AclEdit].includes(GetEffectiveAcl(doc))).map(doc => { // only make a pushpin if we have acl's to edit the document
const context = Cast(doc.context, Doc, null);
- if (context && (context.type === DocumentType.VID || context.type === DocumentType.WEB || context.type === DocumentType.PDF || context.type === DocumentType.IMG)) {
+ const hasContextAnchor = DocListCast(doc.links).some(l => (l.anchor2 === doc && Cast(l.anchor1, Doc, null)?.annotationOn === context) || (l.anchor1 === doc && Cast(l.anchor2, Doc, null)?.annotationOn === context));
+ if (context && !hasContextAnchor && (context.type === DocumentType.VID || context.type === DocumentType.WEB || context.type === DocumentType.PDF || context.type === DocumentType.IMG)) {
const pushpin = Docs.Create.FontIconDocument({
title: "pushpin", label: "",
icon: "map-pin", x: Cast(doc.x, "number", null), y: Cast(doc.y, "number", null), _backgroundColor: "#0000003d", color: "#ACCEF7",
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index 8c1a003b9..0c7f39dc7 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -45,6 +45,8 @@ export class TabDocView extends React.Component<TabDocViewProps> {
@observable private _document: Doc | undefined;
@observable private _view: DocumentView | undefined;
+ @computed get contentScaling() { return this.ContentScaling(); }
+
get stack(): any { return (this.props as any).glContainer.parent.parent; }
get tab() { return (this.props as any).glContainer.tab; }
get view() { return this._view; }
@@ -123,7 +125,7 @@ export class TabDocView extends React.Component<TabDocViewProps> {
@undoBatch
@action
public static PinDoc(doc: Doc, unpin = false, audioRange?: boolean) {
- if (unpin) console.log('remove unpin');
+ if (unpin) console.log('TODO: Remove UNPIN from this location');
//add this new doc to props.Document
const curPres = CurrentUserUtils.ActivePresentation;
if (curPres) {
@@ -194,7 +196,7 @@ export class TabDocView extends React.Component<TabDocViewProps> {
panelHeight = () => this.nativeAspect() && this.nativeAspect() > this._panelWidth / this._panelHeight ? this._panelWidth / this.nativeAspect() : this._panelHeight;
nativeWidth = () => !this.layoutDoc?._fitWidth ? NumCast(this.layoutDoc?._nativeWidth) || this._panelWidth : 0;
nativeHeight = () => !this.layoutDoc?._fitWidth ? NumCast(this.layoutDoc?._nativeHeight) || this._panelHeight : 0;
- contentScaling = () => {
+ ContentScaling = () => {
const nativeH = NumCast(this.layoutDoc?._nativeHeight);
const nativeW = NumCast(this.layoutDoc?._nativeWidth);
let scaling = 1;
@@ -210,12 +212,12 @@ export class TabDocView extends React.Component<TabDocViewProps> {
if (this._mainCont?.children) {
const { translateX, translateY } = Utils.GetScreenTransform(this._mainCont.children[0]?.firstChild as HTMLElement);
const scale = Utils.GetScreenTransform(this._mainCont).scale;
- return CollectionDockingView.Instance?.props.ScreenToLocalTransform().translate(-translateX, -translateY).scale(1 / this.contentScaling() / scale);
+ return CollectionDockingView.Instance?.props.ScreenToLocalTransform().translate(-translateX, -translateY).scale(1 / this.ContentScaling() / scale);
}
return Transform.Identity();
}
- @computed get previewPanelCenteringOffset() { return this.nativeWidth() ? (this._panelWidth - this.nativeWidth() * this.contentScaling()) / 2 : 0; }
- @computed get widthpercent() { return this.nativeWidth() ? `${(this.nativeWidth() * this.contentScaling()) / this._panelWidth * 100}% ` : undefined; }
+ @computed get previewPanelCenteringOffset() { return this.nativeWidth() ? (this._panelWidth - this.nativeWidth() * this.ContentScaling()) / 2 : 0; }
+ @computed get widthpercent() { return this.nativeWidth() ? `${(this.nativeWidth() * this.ContentScaling()) / this._panelWidth * 100}% ` : undefined; }
@computed get layoutDoc() { return this._document && Doc.Layout(this._document); }
// adds a tab to the layout based on the locaiton parameter which can be:
@@ -337,7 +339,7 @@ export class TabDocView extends React.Component<TabDocViewProps> {
rootSelected={returnTrue}
addDocument={undefined}
removeDocument={undefined}
- ContentScaling={this.contentScaling}
+ ContentScaling={this.ContentScaling}
PanelWidth={this.panelWidth}
PanelHeight={this.panelHeight}
NativeHeight={this.nativeHeight() ? this.nativeHeight : undefined}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index f051d5f8d..4cf257640 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -100,13 +100,15 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
let rect = el.getBoundingClientRect();
const top = rect.top, height = rect.height;
var el = el.parentNode;
- do {
- rect = el.getBoundingClientRect();
- if (top <= rect.bottom === false && getComputedStyle(el).overflow === "hidden") return rect.bottom;
- // Check if the element is out of view due to a container scrolling
- if ((top + height) <= rect.top && getComputedStyle(el).overflow === "hidden") return rect.top;
+ while (el && el !== document.body) {
+ if (el.hasOwnProperty("getBoundingClientRect")) {
+ rect = el.getBoundingClientRect();
+ if (top <= rect.bottom === false && getComputedStyle(el).overflow === "hidden") return rect.bottom;
+ // Check if the element is out of view due to a container scrolling
+ if ((top + height) <= rect.top && getComputedStyle(el).overflow === "hidden") return rect.top;
+ }
el = el.parentNode;
- } while (el !== document.body);
+ }
// Check its within the document viewport
return top; //top <= document.documentElement.clientHeight && getComputedStyle(document.documentElement).overflow === "hidden";
}
@@ -114,13 +116,15 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
let rect = el.getBoundingClientRect();
const left = rect.left, width = rect.width;
var el = el.parentNode;
- do {
- rect = el.getBoundingClientRect();
- if (left <= rect.right === false && getComputedStyle(el).overflow === "hidden") return rect.right;
- // Check if the element is out of view due to a container scrolling
- if ((left + width) <= rect.left && getComputedStyle(el).overflow === "hidden") return rect.left;
+ while (el && el !== document.body) {
+ if (el.hasOwnProperty("getBoundingClientRect")) {
+ rect = el.getBoundingClientRect();
+ if (left <= rect.right === false && getComputedStyle(el).overflow === "hidden") return rect.right;
+ // Check if the element is out of view due to a container scrolling
+ if ((left + width) <= rect.left && getComputedStyle(el).overflow === "hidden") return rect.left;
+ }
el = el.parentNode;
- } while (el !== document.body);
+ }
// Check its within the document viewport
return left; //top <= document.documentElement.clientHeight && getComputedStyle(document.documentElement).overflow === "hidden";
}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 5236fab27..8b9e84bd6 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -7,7 +7,7 @@ import { Id } from "../../../../fields/FieldSymbols";
import { InkData, InkField, InkTool } from "../../../../fields/InkField";
import { List } from "../../../../fields/List";
import { RichTextField } from "../../../../fields/RichTextField";
-import { createSchema, makeInterface } from "../../../../fields/Schema";
+import { createSchema, makeInterface, listSpec } from "../../../../fields/Schema";
import { ScriptField } from "../../../../fields/ScriptField";
import { BoolCast, Cast, FieldValue, NumCast, ScriptCast, StrCast } from "../../../../fields/Types";
import { TraceMobx } from "../../../../fields/util";
@@ -88,6 +88,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
private _clusterDistance: number = 75;
private _hitCluster = false;
private _layoutComputeReaction: IReactionDisposer | undefined;
+ private _boundsReaction: IReactionDisposer | undefined;
private _layoutPoolData = new ObservableMap<string, PoolData>();
private _layoutSizeData = new ObservableMap<string, { width?: number, height?: number }>();
private _cachedPool: Map<string, PoolData> = new Map();
@@ -782,10 +783,17 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
let deltaScale = deltaY > 0 ? (1 / 1.05) : 1.05;
if (deltaScale < 0) deltaScale = -deltaScale;
const [x, y] = this.getTransform().transformPoint(pointX, pointY);
+ const invTransform = this.getLocalTransform().inverse();
+ if (deltaScale * invTransform.Scale > 20) {
+ deltaScale = 20 / invTransform.Scale;
+ }
+ if (deltaScale * invTransform.Scale < 1 && this.isAnnotationOverlay) {
+ deltaScale = 1 / invTransform.Scale;
+ }
const localTransform = this.getLocalTransform().inverse().scaleAbout(deltaScale, x, y);
if (localTransform.Scale >= 0.15 || localTransform.Scale > this.zoomScaling()) {
- const safeScale = Math.min(Math.max(0.15, localTransform.Scale), 40);
+ const safeScale = Math.min(Math.max(0.15, localTransform.Scale), 20);
this.props.Document[this.scaleFieldKey] = Math.abs(safeScale);
this.setPan(-localTransform.TranslateX / safeScale, -localTransform.TranslateY / safeScale);
}
@@ -892,57 +900,62 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
afterFocus && setTimeout(afterFocus, delay);
} else {
const contextHgt = Doc.AreProtosEqual(annotOn, this.props.Document) && this.props.VisibleHeight ? this.props.VisibleHeight() : NumCast(annotOn._height);
- const offset = annotOn && (contextHgt / 2);
const curScroll = NumCast(this.props.Document._scrollTop);
let scrollTo = curScroll;
if (curScroll + contextHgt < NumCast(doc.y)) {
- scrollTo = NumCast(doc.y) + NumCast(doc._height) - contextHgt;
+ scrollTo = NumCast(doc.y) + Math.max(NumCast(doc._height), 100) - contextHgt;
} else if (curScroll > NumCast(doc.y)) {
- scrollTo = NumCast(doc.y);
+ scrollTo = Math.max(0, NumCast(doc.y) - 50);
}
- if (curScroll !== scrollTo) {
+ if (curScroll !== scrollTo || this.props.Document._viewTransition) {
this.props.Document._scrollPY = this.props.Document._scrollY = scrollTo;
delay = Math.abs(scrollTo - curScroll) > 5 ? 1000 : 0;
- !dontCenter && delay && this.props.focus(this.props.Document);
+ !dontCenter && this.props.focus(this.props.Document);
afterFocus && setTimeout(afterFocus, delay);
+ } else {
+ !dontCenter && delay && this.props.focus(this.props.Document);
// @ts-ignore
- } else afterFocus(true); // bcz: TODO Aragh -- need to add a parameter to afterFocus() functions to indicate whether the focus function didn't need to scroll
+ afterFocus(true); // bcz: TODO Aragh -- need to add a parameter to afterFocus() functions to indicate whether the focus function didn't need to scroll
+
+ }
}
} else {
const layoutdoc = Doc.Layout(doc);
- const newPanX = NumCast(doc.x) + NumCast(layoutdoc._width) / 2;
- const newPanY = NumCast(doc.y) + NumCast(layoutdoc._height) / 2;
+
+ willZoom && this.setScaleToZoom(layoutdoc, scale);
+ const newPanX = (NumCast(doc.x) + doc[WidthSym]() / 2) - (this.isAnnotationOverlay ? (NumCast(this.props.Document._nativeWidth)) / 2 / this.zoomScaling() : 0);
+ const newPanY = (NumCast(doc.y) + doc[HeightSym]() / 2) - (this.isAnnotationOverlay ? (NumCast(this.props.Document._nativeHeight)) / 2 / this.zoomScaling() : 0);
const newState = HistoryUtil.getState();
newState.initializers![this.Document[Id]] = { panX: newPanX, panY: newPanY };
HistoryUtil.pushState(newState);
const savedState = { px: this.Document._panX, py: this.Document._panY, s: this.Document[this.scaleFieldKey], pt: this.Document._viewTransition };
-
- if (DocListCast(this.dataDoc[this.props.fieldKey]).includes(doc)) {
+ if (DocListCast(this.dataDoc[this.props.annotationsKey || this.props.fieldKey]).includes(doc)) {
// glr: freeform transform speed can be set by adjusting presTransition field - needs a way of knowing when presentation is not active...
if (!doc.z) this.setPan(newPanX, newPanY, doc.focusSpeed || doc.focusSpeed === 0 ? `transform ${doc.focusSpeed}ms` : "transform 500ms", true); // docs that are floating in their collection can't be panned to from their collection -- need to propagate the pan to a parent freeform somehow
}
Doc.BrushDoc(this.props.Document);
this.props.focus(this.props.Document);
- willZoom && this.setScaleToZoom(layoutdoc, scale);
Doc.linkFollowHighlight(doc);
+ const notFocused = newPanX === savedState.px && newPanY === savedState.py;
afterFocus && setTimeout(() => {
- if (afterFocus?.()) {
+ // @ts-ignore
+ if (afterFocus?.(notFocused)) { // bcz: TODO Aragh -- need to add a parameter to afterFocus() functions to indicate whether the focus function didn't need to scroll
this.Document._panX = savedState.px;
this.Document._panY = savedState.py;
this.Document[this.scaleFieldKey] = savedState.s;
this.Document._viewTransition = savedState.pt;
}
- }, 500);
+ }, notFocused ? 0 : 500);
}
}
setScaleToZoom = (doc: Doc, scale: number = 0.75) => {
- const pw = this.props.PanelWidth();
- const ph = this.props.PanelHeight();
+ const pw = this.isAnnotationOverlay ? NumCast(this.props.Document._nativeWidth) : this.props.PanelWidth();
+ const ph = this.isAnnotationOverlay ? NumCast(this.props.Document._nativeHeight) : this.props.PanelHeight();
pw && ph && (this.Document[this.scaleFieldKey] = scale * Math.min(pw / NumCast(doc._width), ph / NumCast(doc._height)));
}
@@ -1159,12 +1172,22 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
this._layoutComputeReaction = reaction(() => this.doLayoutComputation,
(elements) => this._layoutElements = elements || [],
{ fireImmediately: true, name: "doLayout" });
+ if (!this.props.annotationsKey) {
+ this._boundsReaction = reaction(() => this.contentBounds,
+ bounds => (!this.fitToContent && this._layoutElements?.length) && setTimeout(() => {
+ const rbounds = Cast(this.Document._renderContentBounds, listSpec("number"), [0, 0, 0, 0]);
+ if (rbounds[0] !== bounds.x || rbounds[1] !== bounds.y || rbounds[2] !== bounds.r || rbounds[3] !== bounds.b) {
+ this.Document._renderContentBounds = new List<number>([bounds.x, bounds.y, bounds.r, bounds.b]);
+ }
+ }));
+ }
this._marqueeRef.current?.addEventListener("dashDragAutoScroll", this.onDragAutoScroll as any);
}
componentWillUnmount() {
this._layoutComputeReaction?.();
+ this._boundsReaction?.();
this._marqueeRef.current?.removeEventListener("dashDragAutoScroll", this.onDragAutoScroll as any);
}
@@ -1438,10 +1461,10 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
return wscale < hscale ? wscale : hscale;
}
@computed get backgroundEvents() { return this.layoutDoc._isBackground && SnappingManager.GetIsDragging(); }
+
render() {
TraceMobx();
const clientRect = this._mainCont?.getBoundingClientRect();
- !this.fitToContent && this._layoutElements?.length && setTimeout(() => this.Document._renderContentBounds = new List<number>([this.contentBounds.x, this.contentBounds.y, this.contentBounds.r, this.contentBounds.b]), 0);
return <div className={"collectionfreeformview-container"} ref={this.createDashEventsTarget}
onPointerOver={this.onPointerOver}
onWheel={this.onPointerWheel}
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index d6f8ce19c..5a8ce4e14 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -65,7 +65,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
w: Cast(doc["w-indexed"], listSpec("number"), [NumCast(doc._width)]).reduce((p, w, i) => (i <= timecode && w !== undefined) || p === undefined ? w : p, undefined as any as number),
x: Cast(doc["x-indexed"], listSpec("number"), [NumCast(doc.x)]).reduce((p, x, i) => (i <= timecode && x !== undefined) || p === undefined ? x : p, undefined as any as number),
y: Cast(doc["y-indexed"], listSpec("number"), [NumCast(doc.y)]).reduce((p, y, i) => (i <= timecode && y !== undefined) || p === undefined ? y : p, undefined as any as number),
- scroll: Cast(doc["scroll-indexed"], listSpec("number"), [NumCast(doc._scrollY, 0)]).reduce((p, s, i) => (i <= timecode && s !== undefined) || p === undefined ? s : p, undefined as any as number),
+ scroll: Cast(doc["scroll-indexed"], listSpec("number"), [NumCast(doc._scrollTop, 0)]).reduce((p, s, i) => (i <= timecode && s !== undefined) || p === undefined ? s : p, undefined as any as number),
opacity: Cast(doc["opacity-indexed"], listSpec("number"), [NumCast(doc.opacity, 1)]).reduce((p, o, i) => i <= timecode || p === undefined ? o : p, undefined as any as number),
});
}
@@ -97,24 +97,24 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
d["text-color"] = "grey";
} else { d["text-color"] = "black"; }
} else if (d.appearFrame === 0) {
- d["text-color"] = "black";
}
}
- public static updateScrollframe(doc: Doc, time: number) {
- const timecode = Math.round(time);
- const scrollIndexed = Cast(doc['scroll-indexed'], listSpec("number"), null);
- scrollIndexed?.length <= timecode + 1 && scrollIndexed.push(undefined as any as number);
- setTimeout(() => doc.dataTransition = "inherit", 1010);
- }
+ // public static updateScrollframe(doc: Doc, time: number) {
+ // console.log('update scroll frame');
+ // const timecode = Math.round(time);
+ // const scrollIndexed = Cast(doc['scroll-indexed'], listSpec("number"), null);
+ // scrollIndexed?.length <= timecode + 1 && scrollIndexed.push(undefined as any as number);
+ // setTimeout(() => doc.dataTransition = "inherit", 1010);
+ // }
- public static setupScroll(doc: Doc, timecode: number) {
- const scrollList = new List<number>();
- scrollList[timecode] = NumCast(doc._scrollY);
- doc["scroll-indexed"] = scrollList;
- doc.activeFrame = ComputedField.MakeFunction("self._currentFrame");
- doc._scrollY = ComputedField.MakeInterpolated("scroll", "activeFrame");
- }
+ // public static setupScroll(doc: Doc, timecode: number) {
+ // const scrollList = new List<number>();
+ // scrollList[timecode] = NumCast(doc._scrollTop);
+ // doc["scroll-indexed"] = scrollList;
+ // doc.activeFrame = ComputedField.MakeFunction("self._currentFrame");
+ // doc._scrollTop = ComputedField.MakeInterpolated("scroll", "activeFrame");
+ // }
public static updateKeyframe(docs: Doc[], time: number, targetDoc?: Doc) {
diff --git a/src/client/views/nodes/FilterBox.tsx b/src/client/views/nodes/FilterBox.tsx
index d3c0bfba3..0d02a4388 100644
--- a/src/client/views/nodes/FilterBox.tsx
+++ b/src/client/views/nodes/FilterBox.tsx
@@ -94,15 +94,15 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps, FilterBoxDoc
const docFilter = Cast(targetDoc._docFilters, listSpec("string"));
if (docFilter) {
let index: number;
- while ((index = docFilter.findIndex(item => item === facetHeader)) !== -1) {
- docFilter.splice(index, 3);
+ while ((index = docFilter.findIndex(item => item.split(":")[0] === facetHeader)) !== -1) {
+ docFilter.splice(index, 1);
}
}
const docRangeFilters = Cast(targetDoc._docRangeFilters, listSpec("string"));
if (docRangeFilters) {
let index: number;
- while ((index = docRangeFilters.findIndex(item => item === facetHeader)) !== -1) {
- docRangeFilters.splice(index, 3);
+ while ((index = docRangeFilters.findIndex(item => item.split(":")[0] === facetHeader)) !== -1) {
+ docRangeFilters.splice(index, 1);
}
}
} else {
@@ -224,10 +224,10 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps, FilterBoxDoc
Scripting.addGlobal(function determineCheckedState(layoutDoc: Doc, facetHeader: string, facetValue: string) {
const docFilters = Cast(layoutDoc._docFilters, listSpec("string"), []);
- for (let i = 0; i < docFilters.length; i += 3) {
- const [header, value, state] = docFilters.slice(i, i + 3);
- if (header === facetHeader && value === facetValue) {
- return state;
+ for (let i = 0; i < docFilters.length; i++) {
+ const fields = docFilters[i].split(":"); // split into key:value:modifiers
+ if (fields[0] === facetHeader && fields[1] === facetValue) {
+ return fields[2];
}
}
return undefined;
diff --git a/src/client/views/nodes/FontIconBox.tsx b/src/client/views/nodes/FontIconBox.tsx
index 156256fe5..276c66bb1 100644
--- a/src/client/views/nodes/FontIconBox.tsx
+++ b/src/client/views/nodes/FontIconBox.tsx
@@ -8,11 +8,12 @@ import { FieldView, FieldViewProps } from './FieldView';
import { StrCast, Cast, ScriptCast } from '../../../fields/Types';
import { Utils, setupMoveUpEvents, returnFalse, emptyFunction } from "../../../Utils";
import { runInAction, observable, reaction, IReactionDisposer } from 'mobx';
-import { Doc, DocListCast } from '../../../fields/Doc';
+import { Doc, DocListCast, AclPrivate } from '../../../fields/Doc';
import { ContextMenu } from '../ContextMenu';
import { ScriptField } from '../../../fields/ScriptField';
import { Tooltip } from '@material-ui/core';
import { DragManager } from '../../util/DragManager';
+import { GetEffectiveAcl } from '../../../fields/util';
const FontIconSchema = createSchema({
icon: "string",
});
@@ -105,7 +106,7 @@ export class FontIconBadge extends React.Component<FontIconBadgeProps> {
render() {
if (!(this.props.collection instanceof Doc)) return (null);
- const length = DocListCast(this.props.collection.data).filter(d => Object.keys(d).length).length; // filter out any documents that we can't read
+ const length = DocListCast(this.props.collection.data).filter(d => GetEffectiveAcl(d) !== AclPrivate).length; // Object.keys(d).length).length; // filter out any documents that we can't read
return <div className="fontIconBadge-container" style={{ width: 15, height: 15, top: 12 }} ref={this._notifsRef}>
<div className="fontIconBadge" style={length > 0 ? { "display": "initial" } : { "display": "none" }}
onPointerDown={this.onPointerDown} >
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 164456cfb..bc38a17eb 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -67,6 +67,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD
@observable static _showControls: boolean;
@observable uploadIcon = uploadIcons.idle;
+ @computed get contentScaling() { return this.props.ContentScaling(); }
+
protected createDropTarget = (ele: HTMLDivElement) => {
this._dropDisposer?.();
ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.props.Document));
@@ -266,7 +268,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD
considerGooglePhotosLink = () => {
const remoteUrl = this.dataDoc.googlePhotosUrl;
return !remoteUrl ? (null) : (<img draggable={false}
- style={{ transform: `scale(${this.props.ContentScaling()})`, transformOrigin: "bottom right" }}
+ style={{ transform: `scale(${this.contentScaling})`, transformOrigin: "bottom right" }}
id={"google-photos"}
src={"/assets/google_photos.png"}
onClick={() => window.open(remoteUrl)}
@@ -291,7 +293,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD
return (
<img
id={"upload-icon"} draggable={false}
- style={{ transform: `scale(${1 / this.props.ContentScaling()})`, transformOrigin: "bottom right" }}
+ style={{ transform: `scale(${1 / this.contentScaling})`, transformOrigin: "bottom right" }}
src={`/assets/${this.uploadIcon}`}
onClick={async () => {
const { dataDoc } = this;
@@ -402,18 +404,18 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD
(this.props.PanelHeight() - this.props.PanelWidth() * aspect) / 2 : 0;
}
- screenToLocalTransform = () => this.props.ScreenToLocalTransform().translate(0, -this.ycenter / this.props.ContentScaling());
+ screenToLocalTransform = () => this.props.ScreenToLocalTransform().translate(0, -this.ycenter / this.contentScaling);
contentFunc = () => [this.content];
render() {
TraceMobx();
return (<div className={`imageBox`} onContextMenu={this.specificContextMenu}
style={{
- transform: this.props.PanelWidth() ? undefined : `scale(${this.props.ContentScaling()})`,
- width: this.props.PanelWidth() ? undefined : `${100 / this.props.ContentScaling()}%`,
- height: this.props.PanelWidth() ? undefined : `${100 / this.props.ContentScaling()}%`,
+ transform: this.props.PanelWidth() ? undefined : `scale(${this.contentScaling})`,
+ width: this.props.PanelWidth() ? undefined : `${100 / this.contentScaling}%`,
+ height: this.props.PanelWidth() ? undefined : `${100 / this.contentScaling}%`,
pointerEvents: this.layoutDoc._isBackground ? "none" : undefined,
- borderRadius: `${Number(StrCast(this.layoutDoc.borderRounding).replace("px", "")) / this.props.ContentScaling()}px`
+ borderRadius: `${Number(StrCast(this.layoutDoc.borderRounding).replace("px", "")) / this.contentScaling}px`
}} >
<CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit}
forceScaling={true}
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 74b431bea..79e00eed7 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -35,7 +35,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps, PdfDocum
private _scriptValue: string = "";
private _searchString: string = "";
private _initialScale: number = 0; // the initial scale of the PDF when first rendered which determines whether the document will be live on startup or not. Getting bigger after startup won't make it automatically be live.
- private _everActive = false; // has this box ever had its contents activated -- if so, stop drawing the overlay title
+ private _displayPdfLive = false; // has this box ever had its contents activated -- if so, stop drawing the overlay title
private _pdfViewer: PDFViewer | undefined;
private _searchRef = React.createRef<HTMLInputElement>();
private _keyRef = React.createRef<HTMLInputElement>();
@@ -44,7 +44,6 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps, PdfDocum
private _selectReactionDisposer: IReactionDisposer | undefined;
@observable private _searching: boolean = false;
- @observable private _flyout: boolean = false;
@observable private _pdf: Opt<Pdfjs.PDFDocumentProxy>;
@observable private _pageControls = false;
@@ -54,6 +53,11 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps, PdfDocum
const nw = this.Document._nativeWidth = NumCast(this.dataDoc[this.props.fieldKey + "-nativeWidth"], NumCast(this.Document._nativeWidth, 927));
const nh = this.Document._nativeHeight = NumCast(this.dataDoc[this.props.fieldKey + "-nativeHeight"], NumCast(this.Document._nativeHeight, 1200));
!this.Document._fitWidth && (this.Document._height = this.Document[WidthSym]() * (nh / nw));
+ const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField);
+ if (pdfUrl) {
+ if (PDFBox.pdfcache.get(pdfUrl.url.href)) runInAction(() => this._pdf = PDFBox.pdfcache.get(pdfUrl.url.href));
+ else if (PDFBox.pdfpromise.get(pdfUrl.url.href)) PDFBox.pdfpromise.get(pdfUrl.url.href)?.then(action(pdf => this._pdf = pdf));
+ }
const backup = "oldPath";
const { Document } = this.props;
@@ -265,22 +269,23 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps, PdfDocum
</div>;
}
- _pdfjsRequested = false;
+ static pdfcache = new Map<string, Pdfjs.PDFDocumentProxy>();
+ static pdfpromise = new Map<string, Pdfjs.PDFPromise<Pdfjs.PDFDocumentProxy>>();
render() {
TraceMobx();
- const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField, null);
if (true) {//this.props.isSelected() || (this.props.active() && this.props.renderDepth === 0) || this.props.Document._scrollY !== undefined) {
- this._everActive = true;
+ this._displayPdfLive = true;
}
- if (pdfUrl && this._everActive) {
- if (pdfUrl instanceof PdfField && this._pdf) {
- return this.renderPdfView;
- }
- if (!this._pdfjsRequested) {
- this._pdfjsRequested = true;
- const promise = Pdfjs.getDocument(pdfUrl.url.href).promise;
- promise.then(action(pdf => this._pdf = pdf));
+ if (this._displayPdfLive) {
+ if (this._pdf) return this.renderPdfView;
+ const href = Cast(this.dataDoc[this.props.fieldKey], PdfField, null)?.url.href;
+ if (href) {
+ if (PDFBox.pdfcache.get(href)) setTimeout(action(() => this._pdf = PDFBox.pdfcache.get(href)));
+ else {
+ if (!PDFBox.pdfpromise.get(href)) PDFBox.pdfpromise.set(href, Pdfjs.getDocument(href).promise);
+ PDFBox.pdfpromise.get(href)?.then(action(pdf => PDFBox.pdfcache.set(href, this._pdf = pdf)));
+ }
}
}
return this.renderTitleBox;
diff --git a/src/client/views/nodes/PresBox.scss b/src/client/views/nodes/PresBox.scss
index 7bc6c1dfd..9a8b861ba 100644
--- a/src/client/views/nodes/PresBox.scss
+++ b/src/client/views/nodes/PresBox.scss
@@ -22,7 +22,6 @@ $light-background: #ececec;
position: relative;
height: calc(100% - 67px);
width: 100%;
- margin-top: 3px;
}
.presBox-toolbar-dropdown {
@@ -412,6 +411,53 @@ $light-background: #ececec;
transform: scale(1.05);
transition: all 0.4s;
}
+
+ .ribbon-frameList {
+ width: calc(100% - 5px);
+ height: 50px;
+ background-color: #ececec;
+ border: 1px solid #9f9f9f;
+ grid-template-rows: max-content;
+
+ .frameList-header {
+ display: grid;
+ width: 100%;
+ height: 20px;
+ background-color: #9f9f9f;
+
+ .frameList-headerButtons {
+ display: flex;
+ grid-column: 7;
+ width: 60px;
+ justify-self: right;
+ justify-content: flex-end;
+
+ .headerButton {
+ cursor: pointer;
+ position: relative;
+ border-radius: 100%;
+ z-index: 300;
+ width: 15px;
+ height: 15px;
+ display: flex;
+ font-size: 10px;
+ justify-self: center;
+ align-self: center;
+ background-color: rgba(0, 0, 0, 0.5);
+ color: white;
+ justify-content: center;
+ align-items: center;
+ transition: 0.2s;
+ margin-right: 3px;
+ }
+
+ .headerButton:hover {
+ background-color: rgba(0, 0, 0, 1);
+ transform: scale(1.15);
+ }
+ }
+ }
+ }
}
.selectedList {
@@ -620,7 +666,7 @@ $light-background: #ececec;
position: relative;
align-self: flex-start;
justify-self: flex-start;
- width: 80%;
+ width: 100%;
height: 100%;
display: flex;
align-items: center;
@@ -1008,6 +1054,8 @@ $light-background: #ececec;
.presPanel-button-text:hover {
background-color: #5a5a5a;
}
+
+
}
// .miniPres {
diff --git a/src/client/views/nodes/PresBox.tsx b/src/client/views/nodes/PresBox.tsx
index 7eca09f8c..0792be1da 100644
--- a/src/client/views/nodes/PresBox.tsx
+++ b/src/client/views/nodes/PresBox.tsx
@@ -118,12 +118,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
this.rootDoc._forceRenderEngine = "timeline";
this.rootDoc._replacedChrome = "replaced";
this.layoutDoc.presStatus = "edit";
- this.layoutDoc._gridGap = 5;
+ this.layoutDoc._gridGap = 0;
+ this.layoutDoc._yMargin = 0;
this.turnOffEdit(true);
- DocListCastAsync((Doc.UserDoc().myPresentations as Doc).data).then(async pres => {
- await Promise.all(pres!);
- if (!DocListCast((Doc.UserDoc().myPresentations as Doc).data).includes(this.rootDoc)) Doc.AddDocToList(Doc.UserDoc().myPresentations as Doc, "data", this.rootDoc);
- });
+ DocListCastAsync((Doc.UserDoc().myPresentations as Doc).data).then(pres =>
+ !pres?.includes(this.rootDoc) && Doc.AddDocToList(Doc.UserDoc().myPresentations as Doc, "data", this.rootDoc));
}
updateCurrentPresentation = () => {
@@ -153,7 +152,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
setTimeout(() => targetDoc._viewTransition = undefined, 1010);
// targetDoc._currentFrame = curFrame + 1;
this.nextKeyframe(targetDoc, activeItem);
- // if (targetDoc.scrollProgressivize) CollectionFreeFormDocumentView.updateScrollframe(targetDoc, currentFrame);
if (activeItem.presProgressivize) CollectionFreeFormDocumentView.updateKeyframe(childDocs, currentFrame || 0, targetDoc);
else targetDoc.editing = true;
// if (activeItem.zoomProgressivize) this.zoomProgressivizeNext(targetDoc);
@@ -277,7 +275,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
// If openDocument is selected then it should open the document for the user
if (activeItem.openDocument) {
- collectionDocView ? collectionDocView.props.addDocTab(activeItem, "inPlace") : this.props.addDocTab(activeItem, "replace:right");
+ collectionDocView ? collectionDocView.props.addDocTab(activeItem, "replace") : this.props.addDocTab(activeItem, "replace:left");
} else
//docToJump stayed same meaning, it was not in the group or was the last element in the group
if (activeItem.zoomProgressivize && this.rootDoc.presStatus !== 'edit') {
@@ -301,10 +299,14 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
// if targetDoc is not displayed but one of its aliases is, then we need to modify that alias, not the original target
const bestTarget = DocumentManager.Instance.getFirstDocumentView(targetDoc)?.props.Document;
bestTarget && runInAction(() => {
- bestTarget._viewTransition = activeItem.presTransition ? `transform ${activeItem.presTransition}ms` : 'all 0.5s';
- bestTarget._panX = activeItem.presPinViewX;
- bestTarget._panY = activeItem.presPinViewY;
- bestTarget._viewScale = activeItem.presPinViewScale;
+ if (bestTarget.type === DocumentType.PDF || bestTarget.type === DocumentType.WEB || bestTarget.type === DocumentType.RTF || bestTarget._viewType === CollectionViewType.Stacking) {
+ bestTarget._scrollY = activeItem.presPinViewScroll;
+ } else {
+ bestTarget._viewTransition = activeItem.presTransition ? `transform ${activeItem.presTransition}ms` : 'all 0.5s';
+ bestTarget._panX = activeItem.presPinViewX;
+ bestTarget._panY = activeItem.presPinViewY;
+ bestTarget._viewScale = activeItem.presPinViewScale;
+ }
});
//setTimeout(() => targetDoc._viewTransition = undefined, 1010);
}
@@ -453,6 +455,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
srcContext.presPathView = this.pathBoolean;
}
}
+
+ @undoBatch
@action toggleExpandMode = () => {
this.rootDoc.expandBoolean = !this.rootDoc.expandBoolean;
this.childDocs.forEach((doc) => {
@@ -481,7 +485,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
/**
* The method called to open the presentation as a minimized view
- * TODO: Look at old updateMinimize and compare...
*/
@undoBatch
@action
@@ -526,7 +529,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
// pivot field may be set by the user in timeline view (or some other way) -- need to reset it here
viewType === CollectionViewType.Stacking && (this.rootDoc._pivotField = undefined);
this.rootDoc._viewType = viewType;
- if (viewType === CollectionViewType.Stacking) this.layoutDoc._gridGap = 5;
+ if (viewType === CollectionViewType.Stacking) this.layoutDoc._gridGap = 0;
});
/**
@@ -823,6 +826,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
}
// Converts seconds to ms and updates presTransition
+ @undoBatch
setTransitionTime = (number: String, change?: number) => {
let timeInMS = Number(number) * 1000;
if (change) timeInMS += change;
@@ -832,6 +836,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
}
// Converts seconds to ms and updates presDuration
+ @undoBatch
setDurationTime = (number: String, change?: number) => {
let timeInMS = Number(number) * 1000;
if (change) timeInMS += change;
@@ -890,9 +895,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
<div className="ribbon-box">
Visibility {"&"} Duration
<div className="ribbon-doubleButton">
- <Tooltip title={<><div className="dash-tooltip">{"Hide before presented"}</div></>}><div className={`ribbon-toggle ${activeItem.presHideTillShownButton ? "active" : ""}`} onClick={() => activeItem.presHideTillShownButton = !activeItem.presHideTillShownButton}>Hide before</div></Tooltip>
- <Tooltip title={<><div className="dash-tooltip">{"Hide after presented"}</div></>}><div className={`ribbon-toggle ${activeItem.presHideAfterButton ? "active" : ""}`} onClick={() => activeItem.presHideAfterButton = !activeItem.presHideAfterButton}>Hide after</div></Tooltip>
- <Tooltip title={<><div className="dash-tooltip">{"Open document in a new tab"}</div></>}><div className="ribbon-toggle" style={{ backgroundColor: activeItem.openDocument ? "#aedef8" : "" }} onClick={() => activeItem.openDocument = !activeItem.openDocument}>Open</div></Tooltip>
+ <Tooltip title={<><div className="dash-tooltip">{"Hide before presented"}</div></>}><div className={`ribbon-toggle ${activeItem.presHideTillShownButton ? "active" : ""}`} onClick={undoBatch(() => activeItem.presHideTillShownButton = !activeItem.presHideTillShownButton)}>Hide before</div></Tooltip>
+ <Tooltip title={<><div className="dash-tooltip">{"Hide after presented"}</div></>}><div className={`ribbon-toggle ${activeItem.presHideAfterButton ? "active" : ""}`} onClick={undoBatch(() => activeItem.presHideAfterButton = !activeItem.presHideAfterButton)}>Hide after</div></Tooltip>
+ <Tooltip title={<><div className="dash-tooltip">{"Open document in a new tab"}</div></>}><div className="ribbon-toggle" style={{ backgroundColor: activeItem.openDocument ? "#aedef8" : "" }} onClick={undoBatch(() => activeItem.openDocument = !activeItem.openDocument)}>Open</div></Tooltip>
</div>
<div className="ribbon-doubleButton" >
<div className="presBox-subheading">Slide Duration</div>
@@ -924,12 +929,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
{effect}
<FontAwesomeIcon className='presBox-dropdownIcon' style={{ gridColumn: 2, color: this.openEffectDropdown ? '#5B9FDD' : 'black' }} icon={"angle-down"} />
<div className={'presBox-dropdownOptions'} id={'presBoxMovementDropdown'} style={{ display: this.openEffectDropdown ? "grid" : "none" }} onPointerDown={e => e.stopPropagation()}>
- <div className={'presBox-dropdownOption'} onPointerDown={e => e.stopPropagation()} onClick={() => targetDoc.presEffect = 'None'}>None</div>
- <div className={'presBox-dropdownOption'} onPointerDown={e => e.stopPropagation()} onClick={() => targetDoc.presEffect = 'Fade'}>Fade In</div>
- <div className={'presBox-dropdownOption'} onPointerDown={e => e.stopPropagation()} onClick={() => targetDoc.presEffect = 'Flip'}>Flip</div>
- <div className={'presBox-dropdownOption'} onPointerDown={e => e.stopPropagation()} onClick={() => targetDoc.presEffect = 'Rotate'}>Rotate</div>
- <div className={'presBox-dropdownOption'} onPointerDown={e => e.stopPropagation()} onClick={() => targetDoc.presEffect = 'Bounce'}>Bounce</div>
- <div className={'presBox-dropdownOption'} onPointerDown={e => e.stopPropagation()} onClick={() => targetDoc.presEffect = 'Roll'}>Roll</div>
+ <div className={'presBox-dropdownOption'} onPointerDown={e => e.stopPropagation()} onClick={undoBatch(() => targetDoc.presEffect = 'None')}>None</div>
+ <div className={'presBox-dropdownOption'} onPointerDown={e => e.stopPropagation()} onClick={undoBatch(() => targetDoc.presEffect = 'Fade')}>Fade In</div>
+ <div className={'presBox-dropdownOption'} onPointerDown={e => e.stopPropagation()} onClick={undoBatch(() => targetDoc.presEffect = 'Flip')}>Flip</div>
+ <div className={'presBox-dropdownOption'} onPointerDown={e => e.stopPropagation()} onClick={undoBatch(() => targetDoc.presEffect = 'Rotate')}>Rotate</div>
+ <div className={'presBox-dropdownOption'} onPointerDown={e => e.stopPropagation()} onClick={undoBatch(() => targetDoc.presEffect = 'Bounce')}>Bounce</div>
+ <div className={'presBox-dropdownOption'} onPointerDown={e => e.stopPropagation()} onClick={undoBatch(() => targetDoc.presEffect = 'Roll')}>Roll</div>
</div>
</div>
<div className="ribbon-doubleButton" style={{ display: effect === 'None' ? "none" : "inline-flex" }}>
@@ -1252,22 +1257,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
const currentFrame = Cast(tagDoc._currentFrame, "number", null);
if (currentFrame === undefined) {
tagDoc._currentFrame = 0;
- CollectionFreeFormDocumentView.setupScroll(tagDoc, 0);
- CollectionFreeFormDocumentView.setupKeyframes(childDocs, 0);
+ // CollectionFreeFormDocumentView.setupScroll(tagDoc, 0);
+ // CollectionFreeFormDocumentView.setupKeyframes(childDocs, 0);
}
- CollectionFreeFormDocumentView.updateScrollframe(tagDoc, currentFrame);
+ // if (tagDoc.editScrollProgressivize) CollectionFreeFormDocumentView.updateScrollframe(tagDoc, currentFrame);
CollectionFreeFormDocumentView.updateKeyframe(childDocs, currentFrame || 0, tagDoc);
tagDoc._currentFrame = Math.max(0, (currentFrame || 0) + 1);
tagDoc.lastFrame = Math.max(NumCast(tagDoc._currentFrame), NumCast(tagDoc.lastFrame));
- // if (curDoc.zoomProgressivize) {
- // const resize = document.getElementById('resizable');
- // if (resize) {
- // resize.style.width = this.checkList(tagDoc, curDoc["viewfinder-width-indexed"]) + 'px';
- // resize.style.height = this.checkList(tagDoc, curDoc["viewfinder-height-indexed"]) + 'px';
- // resize.style.top = this.checkList(tagDoc, curDoc["viewfinder-top-indexed"]) + 'px';
- // resize.style.left = this.checkList(tagDoc, curDoc["viewfinder-left-indexed"]) + 'px';
- // }
- // }
}
@undoBatch
@@ -1277,19 +1273,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
const currentFrame = Cast(tagDoc._currentFrame, "number", null);
if (currentFrame === undefined) {
tagDoc._currentFrame = 0;
- CollectionFreeFormDocumentView.setupKeyframes(childDocs, 0);
+ // CollectionFreeFormDocumentView.setupKeyframes(childDocs, 0);
}
CollectionFreeFormDocumentView.gotoKeyframe(childDocs.slice());
tagDoc._currentFrame = Math.max(0, (currentFrame || 0) - 1);
- // if (actItem.zoomProgressivize) {
- // const resize = document.getElementById('resizable');
- // if (resize) {
- // resize.style.width = this.checkList(tagDoc, actItem["viewfinder-width-indexed"]) + 'px';
- // resize.style.height = this.checkList(tagDoc, actItem["viewfinder-height-indexed"]) + 'px';
- // resize.style.top = this.checkList(tagDoc, actItem["viewfinder-top-indexed"]) + 'px';
- // resize.style.left = this.checkList(tagDoc, actItem["viewfinder-left-indexed"]) + 'px';
- // }
- // }
}
/**
@@ -1328,9 +1315,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
return (
<div>
<div className={`presBox-ribbon ${this.progressivizeTools && this.layoutDoc.presStatus === "edit" ? "active" : ""}`} onClick={e => e.stopPropagation()} onPointerUp={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}>
- <div className="ribbon-box">
+ {/* <div className="ribbon-box">
{this.stringType} selected
- <div className="ribbon-doubleButton" style={{ borderTop: 'solid 1px darkgrey', display: (targetDoc.type === DocumentType.COL && targetDoc._viewType === 'freeform') || targetDoc.type === DocumentType.IMG ? "inline-flex" : "none" }}>
+ <div className="ribbon-doubleButton" style={{ borderTop: 'solid 1px darkgrey', display: (targetDoc.type === DocumentType.COL && targetDoc._viewType === 'freeform') || targetDoc.type === DocumentType.IMG || targetDoc.type === DocumentType.RTF ? "inline-flex" : "none" }}>
<div className="ribbon-toggle" style={{ backgroundColor: activeItem.presProgressivize ? "#aedef8" : "" }} onClick={this.progressivizeChild}>Contents</div>
<div className="ribbon-toggle" style={{ opacity: activeItem.presProgressivize ? 1 : 0.4, backgroundColor: targetDoc.editProgressivize ? "#aedef8" : "" }} onClick={this.editProgressivize}>Edit</div>
</div>
@@ -1346,16 +1333,16 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
</div>
</div>
{this.viewedColorPicker}
- {/* <div className="ribbon-doubleButton" style={{ borderTop: 'solid 1px darkgrey', display: (targetDoc.type === DocumentType.COL && targetDoc._viewType === 'freeform') || targetDoc.type === DocumentType.IMG ? "inline-flex" : "none" }}>
+ <div className="ribbon-doubleButton" style={{ borderTop: 'solid 1px darkgrey', display: (targetDoc.type === DocumentType.COL && targetDoc._viewType === 'freeform') || targetDoc.type === DocumentType.IMG ? "inline-flex" : "none" }}>
<div className="ribbon-toggle" style={{ backgroundColor: activeItem.zoomProgressivize ? "#aedef8" : "" }} onClick={this.progressivizeZoom}>Zoom</div>
<div className="ribbon-toggle" style={{ opacity: activeItem.zoomProgressivize ? 1 : 0.4, backgroundColor: activeItem.editZoomProgressivize ? "#aedef8" : "" }} onClick={this.editZoomProgressivize}>Edit</div>
- </div> */}
+ </div>
<div className="ribbon-doubleButton" style={{ borderTop: 'solid 1px darkgrey', display: targetDoc._viewType === "stacking" || targetDoc.type === DocumentType.PDF || targetDoc.type === DocumentType.WEB || targetDoc.type === DocumentType.RTF ? "inline-flex" : "none" }}>
<div className="ribbon-toggle" style={{ backgroundColor: activeItem.scrollProgressivize ? "#aedef8" : "" }} onClick={this.progressivizeScroll}>Scroll</div>
<div className="ribbon-toggle" style={{ opacity: activeItem.scrollProgressivize ? 1 : 0.4, backgroundColor: targetDoc.editScrollProgressivize ? "#aedef8" : "" }} onClick={this.editScrollProgressivize}>Edit</div>
</div>
- </div>
- <div className="ribbon-final-box" style={{ display: activeItem.zoomProgressivize || activeItem.scrollProgressivize || activeItem.presProgressivize || activeItem.textProgressivize ? "grid" : "none" }}>
+ </div> */}
+ <div className="ribbon-final-box">
Frames
<div className="ribbon-doubleButton">
<div className="ribbon-frameSelector">
@@ -1372,6 +1359,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
</div>
<Tooltip title={<><div className="dash-tooltip">{"Last frame"}</div></>}><div className="ribbon-property">{NumCast(targetDoc.lastFrame)}</div></Tooltip>
</div>
+ <div className="ribbon-frameList">
+ {this.frameListHeader}
+ {this.frameList}
+ </div>
<div className="ribbon-toggle" style={{ height: 20, backgroundColor: "#AEDDF8" }} onClick={() => console.log(" TODO: play frames")}>Play</div>
</div>
</div>
@@ -1474,7 +1465,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
activeItem.scrollProgressivize = !activeItem.scrollProgressivize;
const targetDoc: Doc = this.targetDoc;
targetDoc.scrollProgressivize = !targetDoc.scrollProgressivize;
- CollectionFreeFormDocumentView.setupScroll(targetDoc, NumCast(targetDoc._currentFrame));
+ // CollectionFreeFormDocumentView.setupScroll(targetDoc, NumCast(targetDoc._currentFrame));
if (targetDoc.editScrollProgressivize) {
targetDoc.editScrollProgressivize = false;
targetDoc._currentFrame = 0;
@@ -1526,7 +1517,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
docs.forEach((doc, i) => CollectionFreeFormDocumentView.setupKeyframes([doc], i, true));
targetDoc.lastFrame = targetDoc.lastFrame ? NumCast(targetDoc.lastFrame) : docs.length - 1;
} else {
- targetDoc.editProgressivize = false;
+ // targetDoc.editProgressivize = false;
activeItem.presProgressivize = false;
targetDoc.presProgressivize = false;
targetDoc._currentFrame = 0;
@@ -1670,20 +1661,22 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
@computed get toolbar() {
const propIcon = CurrentUserUtils.propertiesWidth > 0 ? "angle-double-right" : "angle-double-left";
const propTitle = CurrentUserUtils.propertiesWidth > 0 ? "Close Presentation Panel" : "Open Presentation Panel";
- return (
+ const mode = StrCast(this.rootDoc._viewType) as CollectionViewType;
+ return (mode === CollectionViewType.Carousel3D) ? (null) : (
<div id="toolbarContainer" className={'presBox-toolbar'} style={{ display: this.layoutDoc.presStatus === "edit" ? "inline-flex" : "none" }}>
- <Tooltip title={<><div className="dash-tooltip">{"Add new slide"}</div></>}><div className={`toolbar-button ${this.newDocumentTools ? "active" : ""}`} onClick={action(() => this.newDocumentTools = !this.newDocumentTools)}>
+ {/* <Tooltip title={<><div className="dash-tooltip">{"Add new slide"}</div></>}><div className={`toolbar-button ${this.newDocumentTools ? "active" : ""}`} onClick={action(() => this.newDocumentTools = !this.newDocumentTools)}>
<FontAwesomeIcon icon={"plus"} />
<FontAwesomeIcon className={`dropdown ${this.newDocumentTools ? "active" : ""}`} icon={"angle-down"} />
- </div></Tooltip>
- <div className="toolbar-divider" />
+ </div></Tooltip> */}
<Tooltip title={<><div className="dash-tooltip">{"View paths"}</div></>}>
<div style={{ opacity: this.childDocs.length > 1 ? 1 : 0.3 }} className={`toolbar-button ${this.pathBoolean ? "active" : ""}`} onClick={this.childDocs.length > 1 ? this.viewPaths : undefined}>
<FontAwesomeIcon icon={"exchange-alt"} />
</div>
</Tooltip>
+ <div className="toolbar-divider" />
<Tooltip title={<><div className="dash-tooltip">{this.rootDoc.expandBoolean ? "Minimize all" : "Expand all"}</div></>}>
<div className={`toolbar-button ${this.rootDoc.expandBoolean ? "active" : ""}`} onClick={this.toggleExpandMode}>
+ {/* <FontAwesomeIcon icon={this.rootDoc.expandBoolean ? "eye-slash" : "eye"} /> */}
<FontAwesomeIcon icon={"eye"} />
</div>
</Tooltip>
@@ -1712,7 +1705,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
onChange={this.viewChanged}
value={mode}>
<option onPointerDown={e => e.stopPropagation()} value={CollectionViewType.Stacking}>List</option>
- <option onPointerDown={e => e.stopPropagation()} value={CollectionViewType.Carousel}>Slides</option>
+ <option onPointerDown={e => e.stopPropagation()} value={CollectionViewType.Carousel3D}>3D Carousel</option>
</select>
<div className="presBox-presentPanel" style={{ opacity: this.childDocs.length > 0 ? 1 : 0.3 }}>
<span className={`presBox-button ${this.layoutDoc.presStatus === "edit" ? "present" : ""}`}>
@@ -1720,13 +1713,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
<FontAwesomeIcon icon={"play-circle"} />
<div style={{ display: this.props.PanelWidth() > 200 ? "inline-flex" : "none" }}>&nbsp; Present</div>
</div>
- <div className={`presBox-button-right ${this.presentTools ? "active" : ""}`}
+ {(mode === CollectionViewType.Carousel3D) ? (null) : <div className={`presBox-button-right ${this.presentTools ? "active" : ""}`}
onClick={(action(() => {
if (this.childDocs.length > 0) this.presentTools = !this.presentTools;
}))}>
<FontAwesomeIcon className="dropdown" style={{ margin: 0, transform: this.presentTools ? 'rotate(180deg)' : 'rotate(0deg)' }} icon={"angle-down"} />
{this.presentDropdown}
- </div>
+ </div>}
</span>
{this.playButtons}
</div>
@@ -1734,6 +1727,72 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
);
}
+ @action
+ getList = (list: any): List<number> => {
+ const x: List<number> = list;
+ return x;
+ }
+
+ @action
+ updateList = (list: any): List<number> => {
+ const targetDoc: Doc = this.targetDoc;
+ const x: List<number> = list;
+ x.length + 1;
+ x[x.length - 1] = NumCast(targetDoc._scrollY);
+ return x;
+ }
+
+ @action
+ newFrame = () => {
+ const activeItem: Doc = this.activeItem;
+ const targetDoc: Doc = this.targetDoc;
+ const type: string = StrCast(targetDoc.type);
+ if (!activeItem.frameList) activeItem.frameList = new List<number>();
+ switch (type) {
+ case (DocumentType.PDF || DocumentType.RTF || DocumentType.WEB):
+ this.updateList(activeItem.frameList);
+ break;
+ case DocumentType.COL:
+ break;
+ default:
+ break;
+ }
+ }
+
+ @computed get frameListHeader() {
+ return (<div className="frameList-header">
+ Frames
+ <div className={"frameList-headerButtons"}>
+ <Tooltip title={<><div className="dash-tooltip">{"Add frame by example"}</div></>}><div className={"headerButton"} onClick={e => { e.stopPropagation(); this.newFrame(); }}>
+ <FontAwesomeIcon icon={"plus"} onPointerDown={e => e.stopPropagation()} />
+ </div></Tooltip>
+ <Tooltip title={<><div className="dash-tooltip">{"Edit in collection"}</div></>}><div className={"headerButton"} onClick={e => { e.stopPropagation(); console.log('New frame'); }}>
+ <FontAwesomeIcon icon={"edit"} onPointerDown={e => e.stopPropagation()} />
+ </div></Tooltip>
+ </div>
+ </div>);
+ }
+
+ @computed get frameList() {
+ const activeItem: Doc = this.activeItem;
+ const targetDoc: Doc = this.targetDoc;
+ const frameList: List<number> = this.getList(activeItem.frameList);
+ if (frameList) {
+ const frameItems = frameList.map((value) =>
+ <div className="framList-item">
+
+ </div>
+ );
+ return (
+
+ <div className="frameList-container">
+ {frameItems}
+ </div>
+ );
+ } else return (null);
+
+ }
+
@computed get playButtonFrames() {
const targetDoc: Doc = this.targetDoc;
return (
@@ -1794,7 +1853,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
{this.playButtonFrames}
</div>
<div className="presPanel-divider"></div>
- <div className="presPanel-button-text" onClick={this.updateMinimize}>EXIT</div>
+ <div className="presPanel-button-text" onClick={() => { this.updateMinimize(); this.layoutDoc.presStatus = "edit"; clearTimeout(this._presTimer); }}>EXIT</div>
</div>
</div>
:
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss
index dbf98a5e9..0d92d7062 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.scss
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss
@@ -12,7 +12,6 @@
.formattedTextBox-cont {
touch-action: none;
- cursor: text;
background: inherit;
padding: 0;
border-width: 0px;
@@ -38,6 +37,7 @@
}
}
+.formattedTextBox-outer-selected,
.formattedTextBox-outer {
position: relative;
overflow: auto;
@@ -45,6 +45,9 @@
width: 100%;
height: unset;
}
+.formattedTextBox-outer-selected {
+ cursor: text;
+}
.formattedTextBox-sidebar-handle {
position: absolute;
@@ -357,7 +360,6 @@ footnote::after {
.formattedTextBox-cont {
touch-action: none;
- cursor: text;
background: inherit;
padding: 0;
border-width: 0px;
@@ -383,6 +385,7 @@ footnote::after {
}
}
+ .formattedTextBox-outer-selected,
.formattedTextBox-outer {
position: relative;
overflow: auto;
@@ -399,7 +402,7 @@ footnote::after {
height: 35px;
background: lightgray;
border-radius: 20px;
- cursor:grabbing;
+ cursor: grabbing;
}
.formattedTextBox-sidebar,
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 8078a29b7..c3946dd57 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -11,7 +11,7 @@ import { EditorState, NodeSelection, Plugin, TextSelection, Transaction } from "
import { ReplaceStep } from 'prosemirror-transform';
import { EditorView } from "prosemirror-view";
import { DateField } from '../../../../fields/DateField';
-import { DataSym, Doc, DocListCast, DocListCastAsync, Field, HeightSym, Opt, WidthSym, AclEdit, AclAdmin } from "../../../../fields/Doc";
+import { DataSym, Doc, DocListCast, DocListCastAsync, Field, HeightSym, Opt, WidthSym, AclEdit, AclAdmin, UpdatingFromServer } from "../../../../fields/Doc";
import { documentSchema } from '../../../../fields/documentSchemas';
import applyDevTools = require("prosemirror-dev-tools");
import { removeMarkWithAttrs } from "./prosemirrorPatches";
@@ -22,7 +22,7 @@ import { RichTextField } from "../../../../fields/RichTextField";
import { RichTextUtils } from '../../../../fields/RichTextUtils';
import { makeInterface } from "../../../../fields/Schema";
import { Cast, DateCast, NumCast, StrCast, ScriptCast, BoolCast } from "../../../../fields/Types";
-import { TraceMobx, OVERRIDE_acl, GetEffectiveAcl } from '../../../../fields/util';
+import { TraceMobx, GetEffectiveAcl } from '../../../../fields/util';
import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, numberRange, returnOne, returnZero, Utils, setupMoveUpEvents, OmitKeys } from '../../../../Utils';
import { GoogleApiClientUtils, Pulls, Pushes } from '../../../apis/google_docs/GoogleApiClientUtils';
import { DocServer } from "../../../DocServer";
@@ -289,10 +289,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
}
}
} else {
-
- const json = JSON.parse(Cast(this.dataDoc[this.fieldKey], RichTextField)?.Data!);
- json.selection = state.toJSON().selection;
- this._editorView.updateState(EditorState.fromJSON(this.config, json));
+ const jsonstring = Cast(this.dataDoc[this.fieldKey], RichTextField)?.Data!;
+ if (jsonstring) {
+ const json = JSON.parse(jsonstring);
+ json.selection = state.toJSON().selection;
+ this._editorView.updateState(EditorState.fromJSON(this.config, json));
+ }
}
}
}
@@ -803,9 +805,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
tr = tr.addMark(pos, pos + node.nodeSize, link);
}
});
- OVERRIDE_acl(true);
+ this.dataDoc[UpdatingFromServer] = true; // need to allow permissions for adding links to readonly/augment only documents
this._editorView!.dispatch(tr.removeMark(sel.from, sel.to, splitter));
- OVERRIDE_acl(false);
+ this.dataDoc[UpdatingFromServer] = false;
}
}
componentDidMount() {
@@ -883,7 +885,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
({ width, autoHeight }) => width !== undefined && setTimeout(() => this.tryUpdateHeight(), 0)
);
this._disposers.height = reaction(
- () => NumCast(this.layoutDoc._height),
+ () => Cast(this.layoutDoc._height, "number", null),
action(height => {
if (height !== undefined && height <= 20 && height < NumCast(this.layoutDoc._delayAutoHeight, 20)) {
this.layoutDoc._delayAutoHeight = height;
@@ -1512,7 +1514,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
@action
tryUpdateHeight(limitHeight?: number) {
let scrollHeight = this.ProseRef?.scrollHeight || 0;
- this.rootDoc[this.fieldKey + "-height"] = 0;
if (this.props.renderDepth && this.layoutDoc._autoHeight && !this.props.ignoreAutoHeight && scrollHeight && !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
scrollHeight = scrollHeight * NumCast(this.layoutDoc._viewScale, 1);
if (limitHeight && scrollHeight > limitHeight) {
@@ -1543,7 +1544,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
this.rootDoc[this.fieldKey + "-height"] = finalHeight;
} catch (e) { console.log("Error in tryUpdateHeight"); }
}
- }
+ } else this.rootDoc[this.fieldKey + "-height"] = 0;
}
@computed get audioHandle() {
@@ -1554,7 +1555,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
}
@computed get sidebarHandle() {
- const annotated = DocListCast(this.dataDoc[this.annotationKey]).filter(d => d?.title).length;
+ const annotated = DocListCast(this.dataDoc[this.annotationKey]).filter(d => d?.author).length;
return !this.props.isSelected() && !(annotated && !this.sidebarWidth()) ? (null) :
<div className="formattedTextBox-sidebar-handle"
style={{ left: `max(0px, calc(100% - ${this.sidebarWidthPercent} ${this.sidebarWidth() ? "- 5px" : "- 10px"}))`, background: annotated ? "lightBlue" : undefined }}
@@ -1607,7 +1608,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
const scale = this.props.hideOnLeave ? 1 : this.props.ContentScaling() * NumCast(this.layoutDoc._viewScale, 1);
const rounded = StrCast(this.layoutDoc.borderRounding) === "100%" ? "-rounded" : "";
const interactive = (Doc.GetSelectedTool() === InkTool.None || SnappingManager.GetIsDragging()) && !this.layoutDoc._isBackground;
- if (!selected && FormattedTextBoxComment.textBox === this) { FormattedTextBoxComment.Hide(); }
+ if (!selected && FormattedTextBoxComment.textBox === this) setTimeout(() => FormattedTextBoxComment.Hide());
const minimal = this.props.ignoreAutoHeight;
const margins = NumCast(this.layoutDoc._yMargin, this.props.yMargin || 0);
const selPad = Math.min(margins, 10);
@@ -1654,7 +1655,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
})}
onDoubleClick={this.onDoubleClick}
>
- <div className={`formattedTextBox-outer`} ref={this._scrollRef}
+ <div className={`formattedTextBox-outer${selclass}`} ref={this._scrollRef}
style={{ width: `calc(100% - ${this.sidebarWidthPercent})`, pointerEvents: !active && !SnappingManager.GetIsDragging() ? "none" : undefined }}
onScroll={this.onscrolled} onDrop={this.ondrop} >
<div className={minimal ? "formattedTextBox-minimal" : `formattedTextBox-inner${rounded}${selclass}`} ref={this.createDropTarget}
diff --git a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx
index f96fda861..0919b2b14 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx
@@ -6,7 +6,7 @@ import { EditorState, Plugin } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
import * as ReactDOM from 'react-dom';
import wiki from "wikijs";
-import { Doc, DocCastAsync, Opt } from "../../../../fields/Doc";
+import { Doc, DocCastAsync, Opt, DocListCast } from "../../../../fields/Doc";
import { Cast, FieldValue, NumCast, StrCast } from "../../../../fields/Types";
import { emptyFunction, emptyPath, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnOne, Utils } from "../../../../Utils";
import { DocServer } from "../../../DocServer";
@@ -256,7 +256,7 @@ export class FormattedTextBoxComment {
docTarget && DocServer.GetRefField(docTarget).then(async linkDoc => {
if (linkDoc instanceof Doc) {
(FormattedTextBoxComment.tooltipText as any).href = href;
- FormattedTextBoxComment.linkDoc = linkDoc;
+ FormattedTextBoxComment.linkDoc = DocListCast(textBox.props.Document.links).find(link => link.anchor1 === textBox.props.Document || link.anchor2 === textBox.props.Document ? link : undefined) || linkDoc;
const anchor = FieldValue(Doc.AreProtosEqual(FieldValue(Cast(linkDoc.anchor1, Doc)), textBox.dataDoc) ? Cast(linkDoc.anchor2, Doc) : (Cast(linkDoc.anchor1, Doc)) || linkDoc);
const target = anchor?.annotationOn ? await DocCastAsync(anchor.annotationOn) : anchor;
if (anchor !== target && anchor && target) {
diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx
index e7f901091..a071abd21 100644
--- a/src/client/views/pdf/Annotation.tsx
+++ b/src/client/views/pdf/Annotation.tsx
@@ -71,6 +71,7 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> {
this._reactionDisposer && this._reactionDisposer();
}
+ @undoBatch
deleteAnnotation = () => {
const annotation = DocListCast(this.props.dataDoc[this.props.fieldKey + "-annotations"]);
const group = FieldValue(Cast(this.props.document.group, Doc));
@@ -86,6 +87,7 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> {
PDFMenu.Instance.fadeOut(true);
}
+ @undoBatch
pinToPres = () => {
const group = FieldValue(Cast(this.props.document.group, Doc));
const isPinned = group && Doc.isDocPinned(group) ? true : false;
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 78b95b385..d8be3defd 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -194,7 +194,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu
if (scrollY !== undefined) {
(this._showCover || this._showWaiting) && this.setupPdfJsViewer();
if (this.props.renderDepth === -1 && scrollY >= 0) {
- if (!this._mainCont.current) setTimeout(() => smoothScroll(1000, this._mainCont.current!, scrollY || 0));
+ if (!this._mainCont.current) setTimeout(() => this._mainCont.current && smoothScroll(1000, this._mainCont.current, scrollY || 0));
else smoothScroll(1000, this._mainCont.current, scrollY || 0);
this.Document._scrollPY = undefined;
}
diff --git a/src/client/views/presentationview/PresElementBox.scss b/src/client/views/presentationview/PresElementBox.scss
index 4642caeb2..f1bdb7737 100644
--- a/src/client/views/presentationview/PresElementBox.scss
+++ b/src/client/views/presentationview/PresElementBox.scss
@@ -1,12 +1,14 @@
$light-blue: #AEDDF8;
$dark-blue: #5B9FDD;
$light-background: #ececec;
+$slide-background: #d5dce2;
+$slide-hover: #98b7da;
+$slide-active: #5B9FDD;
-.presElementBox-item {
+.presItem-container {
cursor: grab;
display: grid;
- grid-template-columns: max-content max-content max-content max-content;
- background-color: #d5dce2;
+ grid-template-columns: 20px auto;
font-family: Roboto;
letter-spacing: normal;
position: relative;
@@ -14,200 +16,132 @@ $light-background: #ececec;
width: 100%;
height: 100%;
font-weight: 400;
- border-radius: 6px;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
- transition: all .3s;
- padding: 0px;
- padding-bottom: 3px;
-
- .presElementBox-highlight {
- position: absolute;
- transform: translate(-100px, -4px);
- z-index: -1;
- width: calc(100% + 200px);
- height: calc(100% + 8px);
- background-color: $light-blue;
- }
-
- .presElementBox-highlightTop {
- position: absolute;
- transform: translate(-100px, -4px);
- z-index: -1;
- width: calc(100% + 200px);
- height: calc(50% + 4px);
- }
-
- .presElementBox-highlightBottom {
- position: absolute;
- transform: translate(-100px, 0px);
- z-index: -1;
- top: 50%;
- width: calc(100% + 200px);
- height: calc(50% + 4px);
- }
+ align-items: center;
- .documentView-node {
- position: absolute;
- z-index: 1;
+ .presItem-number {
+ margin-top: 7px;
+ font-size: 12px;
+ font-weight: 700;
+ text-align: center;
+ justify-self: center;
+ align-self: flex-start;
+ position: relative;
+ display: inline-block;
+ overflow: hidden;
}
-}
-
-.presElementBox-item-above {
- border-top: black 2px solid;
-}
-
-.presElementBox-item-below {
- border-bottom: black 2px solid;
-}
-.presElementBox-item:hover {
- transition: all .1s;
- background: #98b7da;
- border-radius: 6px;
-}
-.presElementBox-active {
- color: black;
- border-radius: 6px;
- border: solid 2px $dark-blue;
}
-.presElementBox-buttons {
+.presItem-slide {
+ position: relative;
+ background-color: #d5dce2;
+ border-radius: 5px;
+ height: calc(100% - 7px);
+ width: calc(100% - 5px);
display: grid;
- grid-template-rows: 15px;
- top: 15px;
- left: -20;
- position: absolute;
- width: 100%;
- height: auto;
-
- .presElementBox-interaction {
- display: none;
+ grid-template-rows: 23px auto;
+ grid-template-columns: max-content max-content max-content max-content auto;
+
+ .presItem-name {
+ z-index: 300;
+ align-self: center;
+ font-size: 13px;
+ font-family: Roboto;
+ font-weight: 500;
+ position: relative;
+ top: 1px;
+ padding-left: 10px;
+ padding-right: 10px;
+ letter-spacing: normal;
+ width: max-content;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: pre;
}
- .presElementBox-interaction-selected {
- color: grey;
- background-color: rgba(0, 0, 0, 0);
- float: left;
- padding: 0px;
- width: 20px;
- height: 20px;
+ .presItem-time {
+ align-self: center;
+ position: relative;
+ top: 2px;
+ padding-right: 10px;
+ font-size: 10;
+ font-weight: 300;
+ font-family: Roboto;
+ z-index: 300;
+ letter-spacing: normal;
+ }
+
+ .presItem-embedded {
+ overflow: hidden;
+ grid-column: 1/8;
+ position: relative;
+ display: flex;
+ width: auto;
+ justify-content: center;
+ margin: auto;
+ margin-bottom: 2px;
+ border-bottom-left-radius: 5px;
+ border-bottom-right-radius: 5px;
+ }
+
+ .presItem-embeddedMask {
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ border-radius: 3px;
+ top: 0;
+ left: 0;
+ z-index: 1;
+ overflow: hidden;
}
-}
-
-.presElementBox-number {
- font-size: 12px;
- width: 20;
- font-weight: 700;
- text-align: right;
- justify-content: center;
- align-content: center;
- left: -20;
- position: absolute;
- display: inline-block;
- overflow: hidden;
-}
-
-.presElementBox-name {
- z-index: 300;
- align-self: center;
- font-size: 13px;
- font-family: Roboto;
- font-weight: 500;
- position: relative;
- top: 1px;
- padding-left: 10px;
- padding-right: 10px;
- letter-spacing: normal;
- width: max-content;
- text-overflow: ellipsis;
- overflow: hidden;
- white-space: pre;
-}
-.presElementBox-time {
- align-self: center;
- position: relative;
- top: 2px;
- padding-right: 10px;
- font-size: 10;
- font-weight: 300;
- font-family: Roboto;
- z-index: 300;
- letter-spacing: normal;
-}
-.presElementBox-embedded {
- grid-column: 1/8;
- position: relative;
- display: flex;
- width: auto;
- justify-content: center;
- margin: auto;
+ .presItem-slideButtons {
+ display: flex;
+ grid-column: 7;
+ width: 60px;
+ justify-self: right;
+ justify-content: flex-end;
+
+ .slideButton {
+ cursor: pointer;
+ position: relative;
+ border-radius: 100%;
+ z-index: 300;
+ width: 15px;
+ height: 15px;
+ display: flex;
+ font-size: 10px;
+ justify-self: center;
+ align-self: center;
+ background-color: rgba(0, 0, 0, 0.5);
+ color: white;
+ justify-content: center;
+ align-items: center;
+ transition: 0.2s;
+ margin-right: 3px;
+ }
+
+ .slideButton:hover {
+ background-color: rgba(0, 0, 0, 1);
+ transform: scale(1.15);
+ }
+ }
}
-.presElementBox-embeddedMask {
- width: 100%;
- height: 100%;
- position: absolute;
- border-radius: 3px;
- top: 0;
- left: 0;
- z-index: 1;
- overflow: hidden;
+.presItem-slide.active {
+ box-shadow: 0 0 0px 1.5px $dark-blue;
}
-.presElementBox-closeIcon {
- cursor: pointer;
- position: absolute;
- border-radius: 100%;
- z-index: 300;
- right: 3px;
- top: 3px;
- width: 20px;
- height: 20px;
- display: flex;
- font-size: 75%;
- background-color: black;
- color: white;
- justify-content: center;
- align-items: center;
-}
+// .presItem-slide:hover {
+// background: $slide-hover;
+// }
-.presElementBox-expand {
- cursor: pointer;
- position: absolute;
- border-radius: 100%;
- z-index: 300;
- right: 26px;
- top: 3px;
- width: 20px;
- height: 20px;
- display: flex;
- font-size: 75%;
- background-color: black;
- color: white;
- justify-content: center;
- align-items: center;
-}
-.presElementBox-expand-selected {
- cursor: pointer;
- position: absolute;
- border-radius: 100%;
- right: 3px;
- bottom: 3px;
- width: 20px;
- height: 20px;
- z-index: 300;
- display: flex;
- background-color: black;
- color: white;
- justify-content: center;
- align-items: center;
-} \ No newline at end of file
diff --git a/src/client/views/presentationview/PresElementBox.tsx b/src/client/views/presentationview/PresElementBox.tsx
index aa44c13d1..9a6e4313d 100644
--- a/src/client/views/presentationview/PresElementBox.tsx
+++ b/src/client/views/presentationview/PresElementBox.tsx
@@ -71,17 +71,17 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc
this.rootDoc.presExpandInlineButton = !this.rootDoc.presExpandInlineButton;
}
- embedHeight = () => 100;
+ embedHeight = (): number => 97;
// embedWidth = () => this.props.PanelWidth();
// embedHeight = () => Math.min(this.props.PanelWidth() - 20, this.props.PanelHeight() - this.collapsedHeight);
- embedWidth = () => this.props.PanelWidth() - 20;
+ embedWidth = (): number => this.props.PanelWidth() - 30;
/**
* The function that is responsible for rendering a preview or not for this
* presentation element.
*/
@computed get renderEmbeddedInline() {
return !this.rootDoc.presExpandInlineButton || !this.targetDoc ? (null) :
- <div className="presElementBox-embedded" style={{ height: this.embedHeight(), width: this.embedWidth() }}>
+ <div className="presItem-embedded" style={{ height: this.embedHeight(), width: this.embedWidth() }}>
<ContentFittingDocumentView
Document={this.targetDoc}
DataDoc={this.targetDoc[DataSym] !== this.targetDoc && this.targetDoc[DataSym]}
@@ -110,7 +110,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc
ContainingCollectionDoc={undefined}
ContentScaling={returnOne}
/>
- <div className="presElementBox-embeddedMask" />
+ <div className="presItem-embeddedMask" />
</div>;
}
@@ -157,9 +157,13 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc
headerUp = (e: React.PointerEvent<HTMLDivElement>) => {
e.stopPropagation();
e.preventDefault();
- DragManager.docsBeingDragged = [];
- this._highlightTopRef.current!.style.borderBottom = "0px";
- this._highlightBottomRef.current!.style.borderBottom = "0px";
+ }
+
+ stopDrag = (e: PointerEvent) => {
+ const activeItem = this.rootDoc;
+ activeItem.dragging = false;
+ e.stopPropagation();
+ e.preventDefault();
}
startDrag = (e: PointerEvent, down: number[], delta: number[]) => {
@@ -167,10 +171,9 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc
const dragData = new DragManager.DocumentDragData(PresBox.Instance.sortArray().map(doc => doc));
const dragItem: HTMLElement[] = [];
PresBox.Instance._dragArray.map(ele => {
- const drag = ele;
- drag.style.backgroundColor = "#d5dce2";
- drag.style.borderRadius = '5px';
- dragItem.push(drag);
+ const doc = ele;
+ doc.className = "presItem-slide"
+ dragItem.push(doc);
});
if (activeItem) {
DragManager.StartDocumentDrag(dragItem.map(ele => ele), dragData, e.clientX, e.clientY);
@@ -180,27 +183,33 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc
return false;
}
- private _highlightTopRef: React.RefObject<HTMLDivElement> = React.createRef();
- private _highlightBottomRef: React.RefObject<HTMLDivElement> = React.createRef();
-
-
- onPointerTop = (e: React.PointerEvent<HTMLDivElement>) => {
- if (DragManager.docsBeingDragged.length > 1) {
- this._highlightTopRef.current!.style.borderTop = "solid 2px #5B9FDD";
- }
+ onPointerOver = (e: any) => {
+ document.removeEventListener("pointermove", this.onPointerMove);
+ document.addEventListener("pointermove", this.onPointerMove);
}
- onPointerBottom = (e: React.PointerEvent<HTMLDivElement>) => {
- if (DragManager.docsBeingDragged.length > 1) {
- this._highlightBottomRef.current!.style.borderBottom = "solid 2px #5B9FDD";
+ onPointerMove = (e: PointerEvent) => {
+ const slide = this._itemRef.current!;
+ if (slide && DragManager.docsBeingDragged.length > 1) {
+ const rect = slide.getBoundingClientRect();
+ let y = e.clientY - rect.top; //y position within the element.
+ let height = slide.clientHeight;
+ let halfLine = height / 2;
+ if (y <= halfLine) {
+ slide.style.borderTop = "solid 2px #5B9FDD";
+ slide.style.borderBottom = "0px";
+ } else if (y > halfLine) {
+ slide.style.borderTop = "0px";
+ slide.style.borderBottom = "solid 2px #5B9FDD";
+ }
}
+ document.removeEventListener("pointermove", this.onPointerMove);
}
- onPointerLeave = (e: React.PointerEvent<HTMLDivElement>) => {
- if (DragManager.docsBeingDragged.length > 1) {
- this._highlightBottomRef.current!.style.borderBottom = "0px";
- this._highlightTopRef.current!.style.borderTop = "0px";
- }
+ onPointerLeave = (e: any) => {
+ this._itemRef.current!.style.borderTop = "0px";
+ this._itemRef.current!.style.borderBottom = "0px";
+ document.removeEventListener("pointermove", this.onPointerMove);
}
@action
@@ -225,12 +234,42 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc
return true;
}
- render() {
- const className = "presElementBox-item" + (PresBox.Instance._selectedArray.includes(this.rootDoc) ? " presElementBox-active" : "");
- return !(this.rootDoc instanceof Doc) || this.targetDoc instanceof Promise ? (null) : (
- <div className={className} key={this.props.Document[Id] + this.indexInPres}
+ @action
+ clearArrays = () => {
+ PresBox.Instance._eleArray = [];
+ PresBox.Instance._eleArray.push(this._itemRef.current!);
+ PresBox.Instance._dragArray = [];
+ PresBox.Instance._dragArray.push(this._dragRef.current!);
+ }
+
+ @undoBatch
+ @action
+ pinWithView = (targetDoc: Doc, activeItem: Doc) => {
+ console.log(targetDoc.type);
+ if (targetDoc.type === DocumentType.PDF || targetDoc.type === DocumentType.WEB || targetDoc.type === DocumentType.RTF) {
+ const scroll = targetDoc._scrollTop;
+ activeItem.presPinViewScroll = scroll;
+ } else {
+ const x = targetDoc._panX;
+ const y = targetDoc._panY;
+ const scale = targetDoc._viewScale;
+ activeItem.presPinViewX = x;
+ activeItem.presPinViewY = y;
+ activeItem.presPinViewScale = scale;
+ }
+ }
+
+ @computed get mainItem() {
+ const isSelected: boolean = PresBox.Instance._selectedArray.includes(this.rootDoc);
+ const isDragging: boolean = BoolCast(this.rootDoc.dragging);
+ const toolbarWidth: number = PresBox.Instance.toolbarWidth;
+ const showMore: boolean = PresBox.Instance.toolbarWidth >= 300;
+ const targetDoc: Doc = Cast(this.rootDoc.presentationTargetDoc, Doc, null);
+ const activeItem: Doc = this.rootDoc;
+ return (
+ <div className={`presItem-container`} key={this.props.Document[Id] + this.indexInPres}
ref={this._itemRef}
- style={{ outlineWidth: Doc.IsBrushed(this.targetDoc) ? `1px` : "0px", }}
+ style={{ backgroundColor: isSelected ? "#AEDDF8" : "rgba(0,0,0,0)", opacity: isDragging ? 0.3 : 1 }}
onClick={e => {
e.stopPropagation();
e.preventDefault();
@@ -243,30 +282,25 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc
// Regular click
} else {
this.props.focus(this.rootDoc);
- PresBox.Instance._eleArray = [];
- PresBox.Instance._eleArray.push(this._itemRef.current!);
- PresBox.Instance._dragArray = [];
- PresBox.Instance._dragArray.push(this._dragRef.current!);
+ this.clearArrays();
}
}}
onDoubleClick={e => {
- console.log('double click to open');
this.toggleProperties();
this.props.focus(this.rootDoc);
- PresBox.Instance._eleArray = [];
- PresBox.Instance._eleArray.push(this._itemRef.current!);
- PresBox.Instance._dragArray = [];
- PresBox.Instance._dragArray.push(this._dragRef.current!);
+ this.clearArrays();
}}
+ onPointerOver={this.onPointerOver}
+ onPointerLeave={this.onPointerLeave}
onPointerDown={this.headerDown}
onPointerUp={this.headerUp}
>
- <>
- <div className="presElementBox-number">
- {`${this.indexInPres + 1}.`}
- </div>
- <div ref={this._dragRef} className="presElementBox-name" style={{ maxWidth: (PresBox.Instance.toolbarWidth - 70) }}>
- <EditableView
+ <div className="presItem-number">
+ {`${this.indexInPres + 1}.`}
+ </div>
+ <div ref={this._dragRef} className={`presItem-slide ${isSelected ? "active" : ""}`}>
+ <div className="presItem-name" style={{ maxWidth: showMore ? (toolbarWidth - 175) : toolbarWidth - 85 }}>
+ {isSelected ? <EditableView
ref={this._titleRef}
contents={this.rootDoc.title}
GetValue={() => StrCast(this.rootDoc.title)}
@@ -274,25 +308,37 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc
this.onSetValue(value);
return true;
})}
- />
+ /> :
+ this.rootDoc.title
+ }
+ </div>
+ <Tooltip title={<><div className="dash-tooltip">{"Movement speed"}</div></>}><div className="presItem-time" style={{ display: showMore ? "block" : "none" }}>{this.transition}</div></Tooltip>
+ <Tooltip title={<><div className="dash-tooltip">{"Duration"}</div></>}><div className="presItem-time" style={{ display: showMore ? "block" : "none" }}>{this.duration}</div></Tooltip>
+ <div className={"presItem-slideButtons"}>
+ <Tooltip title={<><div className="dash-tooltip">{"Update view"}</div></>}>
+ <div className="slideButton"
+ onClick={() => this.pinWithView(targetDoc, activeItem)}
+ style={{ fontWeight: 700, display: activeItem.presPinView ? "flex" : "none" }}>V</div>
+ </Tooltip>
+ <Tooltip title={<><div className="dash-tooltip">{this.rootDoc.presExpandInlineButton ? "Minimize" : "Expand"}</div></>}><div className={"slideButton"} onClick={e => { e.stopPropagation(); this.presExpandDocumentClick(); }}>
+ <FontAwesomeIcon icon={this.rootDoc.presExpandInlineButton ? "eye-slash" : "eye"} onPointerDown={e => e.stopPropagation()} />
+ </div></Tooltip>
+ <Tooltip title={<><div className="dash-tooltip">{"Remove from presentation"}</div></>}><div
+ className={"slideButton"}
+ onClick={this.removeItem}>
+ <FontAwesomeIcon icon={"trash"} onPointerDown={e => e.stopPropagation()} />
+ </div></Tooltip>
</div>
- <Tooltip title={<><div className="dash-tooltip">{"Movement speed"}</div></>}><div className="presElementBox-time" style={{ display: PresBox.Instance.toolbarWidth > 300 ? "block" : "none" }}>{this.transition}</div></Tooltip>
- <Tooltip title={<><div className="dash-tooltip">{"Duration"}</div></>}><div className="presElementBox-time" style={{ display: PresBox.Instance.toolbarWidth > 300 ? "block" : "none" }}>{this.duration}</div></Tooltip>
- <Tooltip title={<><div className="dash-tooltip">{"Presentation pin view"}</div></>}><div className="presElementBox-time" style={{ fontWeight: 700, display: this.rootDoc.presPinView && PresBox.Instance.toolbarWidth > 300 ? "block" : "none" }}>V</div></Tooltip>
- <Tooltip title={<><div className="dash-tooltip">{"Remove from presentation"}</div></>}><div
- className="presElementBox-closeIcon"
- onClick={this.removeItem}>
- <FontAwesomeIcon icon={"trash"} onPointerDown={e => e.stopPropagation()} />
- </div></Tooltip>
- <Tooltip title={<><div className="dash-tooltip">{this.rootDoc.presExpandInlineButton ? "Minimize" : "Expand"}</div></>}><div className={"presElementBox-expand" + (this.rootDoc.presExpandInlineButton ? "-selected" : "")} onClick={e => { e.stopPropagation(); this.presExpandDocumentClick(); }}>
- <FontAwesomeIcon icon={(this.rootDoc.presExpandInlineButton ? "angle-up" : "angle-down")} onPointerDown={e => e.stopPropagation()} />
- </div></Tooltip>
- </>
- <div ref={this._highlightTopRef} onPointerOver={this.onPointerTop} onPointerLeave={this.onPointerLeave} className="presElementBox-highlightTop" style={{ zIndex: 299, backgroundColor: "rgba(0,0,0,0)" }} />
- <div ref={this._highlightBottomRef} onPointerOver={this.onPointerBottom} onPointerLeave={this.onPointerLeave} className="presElementBox-highlightBottom" style={{ zIndex: 299, backgroundColor: "rgba(0,0,0,0)" }} />
- <div className="presElementBox-highlight" style={{ backgroundColor: PresBox.Instance._selectedArray.includes(this.rootDoc) ? "#AEDDF8" : "rgba(0,0,0,0)" }} />
- {this.renderEmbeddedInline}
- </div>
- );
+ {this.renderEmbeddedInline}
+ </div>
+ </div >);
+ }
+
+ render() {
+ let item = null;
+ if (!(this.rootDoc instanceof Doc) || this.targetDoc instanceof Promise) item = null;
+ else item = this.mainItem;
+
+ return item;
}
} \ No newline at end of file
diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx
index 1104d8d2a..c559d4eb7 100644
--- a/src/client/views/search/SearchBox.tsx
+++ b/src/client/views/search/SearchBox.tsx
@@ -24,6 +24,7 @@ import { ViewBoxBaseComponent } from "../DocComponent";
import { FieldView, FieldViewProps } from '../nodes/FieldView';
import "./SearchBox.scss";
import { undoBatch } from "../../util/UndoManager";
+import { DocServer } from "../../DocServer";
export const searchSchema = createSchema({ Document: Doc });
@@ -121,22 +122,24 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc
const filters: string[] = [];
- for (let i = 0; i < initialfilters.length; i = i + 3) {
- if (initialfilters[i + 2] !== undefined) {
- filters.push(initialfilters[i]);
- filters.push(initialfilters[i + 1]);
- filters.push(initialfilters[i + 2]);
+ for (let i = 0; i < initialfilters.length; i++) {
+ const fields = initialfilters[i].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 + 3) {
- if (finalfilters[filters[i]] !== undefined) {
- finalfilters[filters[i]].push(filters[i + 1]);
+ 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[filters[i]] = [filters[i + 1]];
+ finalfilters[fields[0]] = [fields[1]];
}
}
@@ -503,7 +506,7 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc
Logoff
</div>
</div>
- <div className="searchBox-lozenge">
+ <div className="searchBox-lozenge" onClick={() => DocServer.PRINT_CACHE()}>
{`UI project`}
</div>
<div className="searchBox-lozenge-dashboard" >
diff --git a/src/debug/Repl.tsx b/src/debug/Repl.tsx
index be53c0b9b..1b12e208c 100644
--- a/src/debug/Repl.tsx
+++ b/src/debug/Repl.tsx
@@ -7,7 +7,7 @@ import { makeInterface } from '../fields/Schema';
import { ObjectField } from '../fields/ObjectField';
import { RefField } from '../fields/RefField';
import { DocServer } from '../client/DocServer';
-import { resolvedPorts } from '../client/views/Main';
+import { resolvedPorts } from '../client/util/CurrentUserUtils';
@observer
class Repl extends React.Component {
diff --git a/src/debug/Viewer.tsx b/src/debug/Viewer.tsx
index 0ca067ed3..bebd71dcf 100644
--- a/src/debug/Viewer.tsx
+++ b/src/debug/Viewer.tsx
@@ -14,7 +14,7 @@ import { RichTextField } from '../fields/RichTextField';
import { DateField } from '../fields/DateField';
import { ScriptField } from '../fields/ScriptField';
import CursorField from '../fields/CursorField';
-import { resolvedPorts } from '../client/views/Main';
+import { resolvedPorts } from '../client/util/CurrentUserUtils';
DateField;
URLField;
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 54d85ba86..c8d28b4a2 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -25,7 +25,6 @@ import JSZip = require("jszip");
import { saveAs } from "file-saver";
import { CollectionDockingView } from "../client/views/collections/CollectionDockingView";
import { SelectionManager } from "../client/util/SelectionManager";
-import { CurrentUserUtils } from "../client/util/CurrentUserUtils";
export namespace Field {
export function toKeyValueString(doc: Doc, key: string): string {
@@ -103,22 +102,28 @@ const AclMap = new Map<string, symbol>([
[SharingPermissions.Admin, AclAdmin]
]);
-export function fetchProto(doc: Doc) {
+// caches the document access permissions for the current user.
+// this recursively updates all protos as well.
+export function updateCachedAcls(doc: Doc) {
if (!doc) return;
const permissions: { [key: string]: symbol } = {};
+ doc[UpdatingFromServer] = true;
Object.keys(doc).filter(key => key.startsWith("acl") && (permissions[key] = AclMap.get(StrCast(doc[key]))!));
+ doc[UpdatingFromServer] = false;
- if (Object.keys(permissions).length) doc[AclSym] = permissions;
+ if (Object.keys(permissions).length) {
+ doc[AclSym] = permissions;
+ }
if (doc.proto instanceof Promise) {
- doc.proto.then(fetchProto);
+ doc.proto.then(updateCachedAcls);
return doc.proto;
}
}
@scriptingGlobal
-@Deserializable("Doc", fetchProto).withFields(["id"])
+@Deserializable("Doc", updateCachedAcls).withFields(["id"])
export class Doc extends RefField {
constructor(id?: FieldId, forceSave?: boolean) {
super(id);
@@ -234,18 +239,16 @@ export class Doc extends RefField {
const prev = GetEffectiveAcl(this);
this[UpdatingFromServer] = true;
this[fKey] = value;
+ this[UpdatingFromServer] = false;
if (fKey.startsWith("acl")) {
- fetchProto(this);
+ updateCachedAcls(this);
}
- this[UpdatingFromServer] = false;
if (prev === AclPrivate && GetEffectiveAcl(this) !== AclPrivate) {
DocServer.GetRefField(this[Id], true);
}
- if (prev !== AclPrivate && GetEffectiveAcl(this) === AclPrivate) {
- this[UpdatingFromServer] = true;
- this[FieldsSym](true);
- this[UpdatingFromServer] = false;
- }
+ // if (prev !== AclPrivate && GetEffectiveAcl(this) === AclPrivate) {
+ // this[FieldsSym](true);
+ // }
};
if (sameAuthor || fKey.startsWith("acl") || DocServer.getFieldWriteMode(fKey) !== DocServer.WriteMode.Playground) {
delete this[CachedUpdates][fKey];
@@ -676,16 +679,15 @@ export namespace Doc {
} else {
templateLayoutDoc.resolvedDataDoc && (templateLayoutDoc = Cast(templateLayoutDoc.proto, Doc, null) || templateLayoutDoc); // if the template has already been applied (ie, a nested template), then use the template's prototype
if (!targetDoc[expandedLayoutFieldKey]) {
- const newLayoutDoc = Doc.MakeDelegate(templateLayoutDoc, undefined, "[" + templateLayoutDoc.title + "]");
- // the template's arguments are stored in params which is derefenced to find
- // the actual field key where the parameterized template data is stored.
- newLayoutDoc[params] = args !== "..." ? args : ""; // ... signifies the layout has sub template(s) -- so we have to expand the layout for them so that they can get the correct 'rootDocument' field, but we don't need to reassign their params. it would be better if the 'rootDocument' field could be passed dynamically to avoid have to create instances
- newLayoutDoc.rootDocument = targetDoc;
- const dataDoc = Doc.GetProto(targetDoc);
- newLayoutDoc.resolvedDataDoc = dataDoc;
-
_pendingMap.set(targetDoc[Id] + expandedLayoutFieldKey + args, true);
setTimeout(() => {
+ const newLayoutDoc = Doc.MakeDelegate(templateLayoutDoc, undefined, "[" + templateLayoutDoc.title + "]");
+ // the template's arguments are stored in params which is derefenced to find
+ // the actual field key where the parameterized template data is stored.
+ newLayoutDoc[params] = args !== "..." ? args : ""; // ... signifies the layout has sub template(s) -- so we have to expand the layout for them so that they can get the correct 'rootDocument' field, but we don't need to reassign their params. it would be better if the 'rootDocument' field could be passed dynamically to avoid have to create instances
+ newLayoutDoc.rootDocument = targetDoc;
+ const dataDoc = Doc.GetProto(targetDoc);
+ newLayoutDoc.resolvedDataDoc = dataDoc;
if (dataDoc[templateField] === undefined && templateLayoutDoc[templateField] instanceof List && (templateLayoutDoc[templateField] as any).length) {
dataDoc[templateField] = ComputedField.MakeFunction(`ObjectField.MakeCopy(templateLayoutDoc["${templateField}"] as List)`, { templateLayoutDoc: Doc.name }, { templateLayoutDoc });
}
@@ -886,17 +888,17 @@ export namespace Doc {
export function SetSearchQuery(query: string) { runInAction(() => manager._searchQuery = query); }
export function UserDoc(): Doc { return manager._user_doc; }
export function SharingDoc(): Doc { return Cast(Doc.UserDoc().mySharedDocs, Doc, null); }
+ export function LinkDBDoc(): Doc { return Cast(Doc.UserDoc().myLinkDatabase, Doc, null); }
export function SetSelectedTool(tool: InkTool) { Doc.UserDoc().activeInkTool = tool; }
export function GetSelectedTool(): InkTool { return StrCast(Doc.UserDoc().activeInkTool, InkTool.None) as InkTool; }
export function SetUserDoc(doc: Doc) { return (manager._user_doc = doc); }
- export function IsSearchMatch(doc: Doc) {
- return computedFn(function IsSearchMatch(doc: Doc) {
- return brushManager.SearchMatchDoc.has(doc) ? brushManager.SearchMatchDoc.get(doc) :
- brushManager.SearchMatchDoc.has(Doc.GetProto(doc)) ? brushManager.SearchMatchDoc.get(Doc.GetProto(doc)) : undefined;
- })(doc);
- }
+ const isSearchMatchCache = computedFn(function IsSearchMatch(doc: Doc) {
+ return brushManager.SearchMatchDoc.has(doc) ? brushManager.SearchMatchDoc.get(doc) :
+ brushManager.SearchMatchDoc.has(Doc.GetProto(doc)) ? brushManager.SearchMatchDoc.get(Doc.GetProto(doc)) : undefined;
+ });
+ export function IsSearchMatch(doc: Doc) { return isSearchMatchCache(doc); }
export function IsSearchMatchUnmemoized(doc: Doc) {
return brushManager.SearchMatchDoc.has(doc) ? brushManager.SearchMatchDoc.get(doc) :
brushManager.SearchMatchDoc.has(Doc.GetProto(doc)) ? brushManager.SearchMatchDoc.get(Doc.GetProto(doc)) : undefined;
@@ -918,11 +920,9 @@ export namespace Doc {
brushManager.SearchMatchDoc.clear();
}
- export function IsBrushed(doc: Doc) {
- return computedFn(function IsBrushed(doc: Doc) {
- return brushManager.BrushedDoc.has(doc) || brushManager.BrushedDoc.has(Doc.GetProto(doc));
- })(doc);
- }
+ const isBrushedCache = computedFn(function IsBrushed(doc: Doc) { return brushManager.BrushedDoc.has(doc) || brushManager.BrushedDoc.has(Doc.GetProto(doc)); });
+ export function IsBrushed(doc: Doc) { return isBrushedCache(doc); }
+
// don't bother memoizing (caching) the result if called from a non-reactive context. (plus this avoids a warning message)
export function IsBrushedDegreeUnmemoized(doc: Doc) {
if (!doc || GetEffectiveAcl(doc) === AclPrivate || GetEffectiveAcl(Doc.GetProto(doc)) === AclPrivate) return 0;
@@ -1053,10 +1053,11 @@ export namespace Doc {
const container = target ?? CollectionDockingView.Instance.props.Document;
const docFilters = Cast(container._docFilters, listSpec("string"), []);
runInAction(() => {
- for (let i = 0; i < docFilters.length; i += 3) {
- if (docFilters[i] === key && (docFilters[i + 1] === value || modifiers === "match" || modifiers === "remove")) {
- if (docFilters[i + 2] === modifiers && modifiers && docFilters[i + 1] === value) return;
- docFilters.splice(i, 3);
+ for (let i = 0; i < docFilters.length; i++) {
+ const fields = docFilters[i].split(":"); // split key:value:modifier
+ if (fields[0] === key && (fields[1] === value || modifiers === "match" || modifiers === "remove")) {
+ if (fields[2] === modifiers && modifiers && fields[1] === value) return;
+ docFilters.splice(i, 1);
container._docFilters = new List<string>(docFilters);
break;
}
@@ -1065,9 +1066,7 @@ export namespace Doc {
if (!docFilters.length && modifiers === "match" && value === undefined) {
container._docFilters = undefined;
} else if (modifiers !== "remove") {
- docFilters.push(key);
- docFilters.push(value);
- docFilters.push(modifiers);
+ docFilters.push(key + ":" + value + ":" + modifiers);
container._docFilters = new List<string>(docFilters);
}
}
diff --git a/src/fields/List.ts b/src/fields/List.ts
index ceb538b2d..a0cbebaf5 100644
--- a/src/fields/List.ts
+++ b/src/fields/List.ts
@@ -43,7 +43,7 @@ const listHandlers: any = {
}
}
const res = list.__fields.push(...items);
- this[Update]({ op: "$addToSet", items });
+ this[Update]({ op: "$addToSet", items, length: length + items.length });
return res;
}),
reverse() {
@@ -77,7 +77,8 @@ const listHandlers: any = {
}
}
const res = list.__fields.splice(start, deleteCount, ...items);
- this[Update](items.length === 0 && deleteCount ? { op: "$remFromSet", items: removed } : undefined);
+ this[Update](items.length === 0 && deleteCount ? { op: "$remFromSet", items: removed, length: list.__fields.length } :
+ items.length && !deleteCount ? { op: "$addToSet", items, length: list.__fields.length } : undefined);
return res.map(toRealField);
}),
unshift(...items: any[]) {
diff --git a/src/fields/util.ts b/src/fields/util.ts
index 7293db0c2..d48011194 100644
--- a/src/fields/util.ts
+++ b/src/fields/util.ts
@@ -1,17 +1,19 @@
import { UndoManager } from "../client/util/UndoManager";
-import { Doc, FieldResult, UpdatingFromServer, LayoutSym, AclPrivate, AclEdit, AclReadonly, AclAddonly, AclSym, CachedUpdates, DataSym, DocListCast, AclAdmin, FieldsSym, HeightSym, WidthSym, fetchProto, AclUnset } from "./Doc";
+import { Doc, FieldResult, UpdatingFromServer, LayoutSym, AclPrivate, AclEdit, AclReadonly, AclAddonly, AclSym, DataSym, DocListCast, AclAdmin, HeightSym, WidthSym, updateCachedAcls, AclUnset, DocListCastAsync } from "./Doc";
import { SerializationHelper } from "../client/util/SerializationHelper";
import { ProxyField, PrefetchProxy } from "./Proxy";
import { RefField } from "./RefField";
import { ObjectField } from "./ObjectField";
-import { action, trace } from "mobx";
-import { Parent, OnUpdate, Update, Id, SelfProxy, Self, HandleUpdate, ToString, ToScriptString } from "./FieldSymbols";
+import { action, trace, } from "mobx";
+import { Parent, OnUpdate, Update, Id, SelfProxy, Self } from "./FieldSymbols";
import { DocServer } from "../client/DocServer";
import { ComputedField } from "./ScriptField";
import { ScriptCast, StrCast } from "./Types";
import { returnZero } from "../Utils";
import CursorField from "./CursorField";
import { List } from "./List";
+import { SnappingManager } from "../client/util/SnappingManager";
+import { computedFn } from "mobx-utils";
function _readOnlySetter(): never {
throw new Error("Documents can't be modified in read-only mode");
@@ -22,6 +24,7 @@ export function TraceMobx() {
tracing && trace();
}
+
export interface GetterResult {
value: FieldResult;
shouldReturn?: boolean;
@@ -111,10 +114,6 @@ export function makeReadOnly() {
export function makeEditable() {
_setter = _setterImpl;
}
-var _overrideAcl = false;
-export function OVERRIDE_acl(val: boolean) {
- _overrideAcl = val;
-}
export function normalizeEmail(email: string) {
return email.replace(/\./g, '__');
@@ -130,14 +129,6 @@ export function denormalizeEmail(email: string) {
// playgroundMode = !playgroundMode;
// }
-// the list of groups that the current user is a member of
-let currentUserGroups: string[] = [];
-
-// called from GroupManager once the groups have been fetched from the server
-export function setGroups(groups: string[]) {
- currentUserGroups = groups;
-}
-
/**
* These are the various levels of access a user can have to a document.
*
@@ -145,7 +136,7 @@ export function setGroups(groups: string[]) {
*
* Edit: a user with edit access to a document can remove/edit that document, add/remove/edit annotations (depending on permissions), but not change any access rights to that document.
*
- * Add: a user with add access to a document can add documents/annotations to that document but cannot edit or delete anything.
+ * Add: a user with add access to a document can augment documents/annotations to that document but cannot edit or delete anything.
*
* View: a user with view access to a document can only view it - they cannot add/remove/edit anything.
*
@@ -154,33 +145,38 @@ export function setGroups(groups: string[]) {
export enum SharingPermissions {
Admin = "Admin",
Edit = "Can Edit",
- Add = "Can Add",
+ Add = "Can Augment",
View = "Can View",
None = "Not Shared"
}
+// return acl from cache or cache the acl and return.
+const getEffectiveAclCache = computedFn(function (target: any, user?: string) { return getEffectiveAcl(target, user); }, true);
+
/**
* Calculates the effective access right to a document for the current user.
*/
-export function GetEffectiveAcl(target: any, in_prop?: string | symbol | number, user?: string): symbol {
- if (!target) return AclPrivate;
-
- // all changes received fromt the server must be processed as Admin
- if (in_prop === UpdatingFromServer || target[UpdatingFromServer]) return AclAdmin;
-
- // if the current user is the author of the document / the current user is a member of the admin group
- const userChecked = user || Doc.CurrentUserEmail;
- if (userChecked === (target.__fields?.author || target.author)) return AclAdmin;
- if (currentUserGroups.includes("Admin")) return AclAdmin;
+export function GetEffectiveAcl(target: any, user?: string): symbol {
+ return !target ? AclPrivate :
+ target[UpdatingFromServer] ? AclAdmin : getEffectiveAclCache(target, user);// all changes received from the server must be processed as Admin. return this directly so that the acls aren't cached (UpdatingFromServer is not observable)
+}
+function getPropAcl(target: any, prop: string | symbol | number) {
+ if (prop === UpdatingFromServer || 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
+ return GetEffectiveAcl(target);
+}
- if (target[AclSym] && Object.keys(target[AclSym]).length) {
+let HierarchyMapping: Map<symbol, number> | undefined;
- // if the acl is being overriden or the property being modified is one of the playground fields (which can be freely modified)
- if (_overrideAcl || (in_prop && DocServer.PlaygroundFields?.includes(in_prop.toString()))) return AclEdit;
+function getEffectiveAcl(target: any, user?: string): symbol {
+ const targetAcls = target[AclSym];
+ const userChecked = user || Doc.CurrentUserEmail; // if the current user is the author of the document / the current user is a member of the admin group
+ if (userChecked === (target.__fields?.author || target.author)) return AclAdmin; // target may be a Doc of Proxy, so check __fields.author and .author
+ if (SnappingManager.GetCachedGroupByName("Admin")) return AclAdmin;
- let effectiveAcl = AclPrivate;
- const HierarchyMapping = new Map<symbol, number>([
+ if (targetAcls && Object.keys(targetAcls).length) {
+ HierarchyMapping = HierarchyMapping || new Map<symbol, number>([
[AclPrivate, 0],
[AclReadonly, 1],
[AclAddonly, 2],
@@ -188,21 +184,21 @@ export function GetEffectiveAcl(target: any, in_prop?: string | symbol | number,
[AclAdmin, 4]
]);
- for (const [key, value] of Object.entries(target[AclSym])) {
+ let effectiveAcl = AclPrivate;
+ for (const [key, value] of Object.entries(targetAcls)) {
// there are issues with storing fields with . in the name, so they are replaced with _ during creation
// as a result we need to restore them again during this comparison.
const entity = denormalizeEmail(key.substring(4)); // an individual or a group
- if (currentUserGroups.includes(entity) || userChecked === entity) {
- if (HierarchyMapping.get(value as symbol)! > HierarchyMapping.get(effectiveAcl)!) {
+ if (HierarchyMapping.get(value as symbol)! > HierarchyMapping.get(effectiveAcl)!) {
+ if (SnappingManager.GetCachedGroupByName(entity) || userChecked === entity) {
effectiveAcl = value as symbol;
- if (effectiveAcl === AclAdmin) return effectiveAcl;
}
}
}
// if there's an overriding acl set through the properties panel or sharing menu, that's what's returned if the user isn't an admin of the document
- const override = target[AclSym]["acl-Override"];
- if (override !== AclUnset && override !== undefined) effectiveAcl = target[AclSym]["acl-Override"];
+ const override = targetAcls["acl-Override"];
+ if (override !== AclUnset && override !== undefined) effectiveAcl = override;
// if we're in playground mode, return AclEdit (or AclAdmin if that's the user's effectiveAcl)
return DocServer?.Control?.isReadOnly?.() && HierarchyMapping.get(effectiveAcl)! < 3 ? AclEdit : effectiveAcl;
@@ -225,7 +221,7 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc
const HierarchyMapping = new Map<string, number>([
["Not Shared", 0],
["Can View", 1],
- ["Can Add", 2],
+ ["Can Augment", 2],
["Can Edit", 3],
["Admin", 4]
]);
@@ -274,20 +270,20 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc
});
}
- layoutDocChanged && fetchProto(target); // updates target[AclSym] when changes to acls have been made
- dataDocChanged && fetchProto(dataDoc);
+ layoutDocChanged && updateCachedAcls(target); // updates target[AclSym] when changes to acls have been made
+ dataDocChanged && updateCachedAcls(dataDoc);
}
const layoutProps = ["panX", "panY", "width", "height", "nativeWidth", "nativeHeight", "fitWidth", "fitToBox",
"chromeStatus", "viewType", "gridGap", "xMargin", "yMargin", "autoHeight"];
export function setter(target: any, in_prop: string | symbol | number, value: any, receiver: any): boolean {
let prop = in_prop;
- const effectiveAcl = GetEffectiveAcl(target, in_prop);
+ const effectiveAcl = getPropAcl(target, prop);
if (effectiveAcl !== AclEdit && effectiveAcl !== AclAdmin) 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 Add", "Can View", "Not Shared", undefined].includes(value)) return true;
+ // if (typeof prop === "string" && prop.startsWith("acl") && !["Can Edit", "Can Augment", "Can View", "Not Shared", undefined].includes(value)) return true;
if (typeof prop === "string" && prop !== "__id" && prop !== "__fields" && (prop.startsWith("_") || layoutProps.includes(prop))) {
if (!prop.startsWith("_")) {
@@ -308,12 +304,10 @@ export function setter(target: any, in_prop: string | symbol | number, value: an
export function getter(target: any, in_prop: string | symbol | number, receiver: any): any {
let prop = in_prop;
- if (in_prop === "toString" || in_prop === ToString || in_prop === ToScriptString || in_prop === FieldsSym || in_prop === Id || in_prop === HandleUpdate || in_prop === CachedUpdates) return target.__fields[prop] || target[prop];
- if (in_prop === AclSym) return _overrideAcl ? undefined : target[AclSym];
- if (GetEffectiveAcl(target) === AclPrivate && !_overrideAcl) return prop === HeightSym || prop === WidthSym ? returnZero : undefined;
- if (prop === LayoutSym) {
- return target.__LAYOUT__;
- }
+ if (in_prop === AclSym) return target[AclSym];
+ if (in_prop === "toString" || (in_prop !== HeightSym && in_prop !== WidthSym && in_prop !== LayoutSym && typeof prop === "symbol")) return target.__fields[prop] || target[prop];
+ if (GetEffectiveAcl(target) === AclPrivate) return prop === HeightSym || prop === WidthSym ? returnZero : undefined;
+ if (prop === LayoutSym) return target.__LAYOUT__;
if (typeof prop === "string" && prop !== "__id" && prop !== "__fields" && (prop.startsWith("_") || layoutProps.includes(prop))) {
if (!prop.startsWith("_")) {
console.log(prop + " is deprecated - switch to _" + prop);
@@ -371,9 +365,12 @@ export function deleteProperty(target: any, prop: string | number | symbol) {
export function updateFunction(target: any, prop: any, value: any, receiver: any) {
let current = ObjectField.MakeCopy(value);
return (diff?: any) => {
- const op = diff?.op === "$addToSet" ? { '$addToSet': { ["fields." + prop]: SerializationHelper.Serialize(new List<Doc>(diff.items)) } } :
- diff?.op === "$remFromSet" ? { '$remFromSet': { ["fields." + prop]: SerializationHelper.Serialize(new List<Doc>(diff.items)) } }
- : { '$set': { ["fields." + prop]: SerializationHelper.Serialize(value) } };
+ const op =
+ diff?.op === "$addToSet" ? { '$addToSet': { ["fields." + prop]: SerializationHelper.Serialize(new List<Doc>(diff.items)) } } :
+ diff?.op === "$remFromSet" ? { '$remFromSet': { ["fields." + prop]: SerializationHelper.Serialize(new List<Doc>(diff.items)) } }
+ : { '$set': { ["fields." + prop]: SerializationHelper.Serialize(value) } };
+ !op.$set && ((op as any).length = diff.length);
+
const oldValue = current;
const newValue = ObjectField.MakeCopy(value);
current = newValue;
diff --git a/src/mobile/MobileMain.tsx b/src/mobile/MobileMain.tsx
index 3d4680d58..4a1e26078 100644
--- a/src/mobile/MobileMain.tsx
+++ b/src/mobile/MobileMain.tsx
@@ -14,7 +14,7 @@ AssignAllExtensions();
await Docs.Prototypes.initialize();
if (info.id !== "__guest__") {
// a guest will not have an id registered
- await CurrentUserUtils.loadUserDocument(info);
+ await CurrentUserUtils.loadUserDocument(info.id);
}
document.getElementById('root')!.addEventListener('wheel', event => {
if (event.ctrlKey) {
diff --git a/src/server/ApiManagers/UserManager.ts b/src/server/ApiManagers/UserManager.ts
index c9ffaff4c..f36506b14 100644
--- a/src/server/ApiManagers/UserManager.ts
+++ b/src/server/ApiManagers/UserManager.ts
@@ -19,16 +19,35 @@ export default class UserManager extends ApiManager {
method: Method.GET,
subscription: "/getUsers",
secureHandler: async ({ res }) => {
- const cursor = await Database.Instance.query({}, { email: 1, sharingDocumentId: 1 }, "users");
+ const cursor = await Database.Instance.query({}, { email: 1, linkDatabaseId: 1, sharingDocumentId: 1 }, "users");
const results = await cursor.toArray();
- res.send(results.map(user => ({ email: user.email, sharingDocumentId: user.sharingDocumentId })));
+ res.send(results.map(user => ({ email: user.email, linkDatabaseId: user.linkDatabaseId, sharingDocumentId: user.sharingDocumentId })));
+ }
+ });
+
+ register({
+ method: Method.POST,
+ subscription: "/setCacheDocumentIds",
+ secureHandler: async ({ user, req, res }) => {
+ const result: any = {};
+ user.cacheDocumentIds = req.body.cacheDocumentIds;
+ user.save(err => {
+ if (err) {
+ result.error = [{ msg: "Error while caching documents" }];
+ }
+ });
+
+ // Database.Instance.update(id, { "$set": { "fields.cacheDocumentIds": cacheDocumentIds } }, e => {
+ // console.log(e);
+ // });
+ res.send(result);
}
});
register({
method: Method.GET,
subscription: "/getUserDocumentIds",
- secureHandler: ({ res, user }) => res.send({ userDocumentId: user.userDocumentId, sharingDocumentId: user.sharingDocumentId })
+ secureHandler: ({ res, user }) => res.send({ userDocumentId: user.userDocumentId, linkDatabaseId: user.linkDatabaseId, sharingDocumentId: user.sharingDocumentId })
});
register({
@@ -39,8 +58,14 @@ export default class UserManager extends ApiManager {
register({
method: Method.GET,
+ subscription: "/getLinkDatabaseId",
+ secureHandler: ({ res, user }) => res.send(user.linkDatabaseId)
+ });
+
+ register({
+ method: Method.GET,
subscription: "/getCurrentUser",
- secureHandler: ({ res, user: { _id, email } }) => res.send(JSON.stringify({ id: _id, email })),
+ secureHandler: ({ res, user: { _id, email, cacheDocumentIds } }) => res.send(JSON.stringify({ id: _id, email, cacheDocumentIds })),
publicHandler: ({ res }) => res.send(JSON.stringify({ id: "__guest__", email: "" }))
});
diff --git a/src/server/GarbageCollector.ts b/src/server/GarbageCollector.ts
index 6bd0e5163..7c441e3c0 100644
--- a/src/server/GarbageCollector.ts
+++ b/src/server/GarbageCollector.ts
@@ -65,7 +65,7 @@ async function GarbageCollect(full: boolean = true) {
// await new Promise(res => setTimeout(res, 3000));
const cursor = await Database.Instance.query({}, { userDocumentId: 1 }, 'users');
const users = await cursor.toArray();
- const ids: string[] = [...users.map(user => user.userDocumentId), ...users.map(user => user.sharingDocumentId)]];
+ const ids: string[] = [...users.map(user => user.userDocumentId), ...users.map(user => user.sharingDocumentId), ...users.map(user => user.linkDatabaseId)];
const visited = new Set<string>();
const files: { [name: string]: string[] } = {};
diff --git a/src/server/authentication/AuthenticationManager.ts b/src/server/authentication/AuthenticationManager.ts
index 36363e3cf..9eb4a328f 100644
--- a/src/server/authentication/AuthenticationManager.ts
+++ b/src/server/authentication/AuthenticationManager.ts
@@ -48,7 +48,9 @@ export let postSignup = (req: Request, res: Response, next: NextFunction) => {
email,
password,
userDocumentId: Utils.GenerateGuid(),
- sharingDocumentId: Utils.GenerateGuid()
+ sharingDocumentId: Utils.GenerateGuid(),
+ linkDatabaseId: Utils.GenerateGuid(),
+ cacheDocumentIds: ""
} as Partial<DashUserModel>;
const user = new User(model);
diff --git a/src/server/authentication/DashUserModel.ts b/src/server/authentication/DashUserModel.ts
index 0bdc25644..bee28b96d 100644
--- a/src/server/authentication/DashUserModel.ts
+++ b/src/server/authentication/DashUserModel.ts
@@ -11,6 +11,8 @@ export type DashUserModel = mongoose.Document & {
userDocumentId: string;
sharingDocumentId: string;
+ linkDatabaseId: string;
+ cacheDocumentIds: string;
profile: {
name: string,
@@ -38,6 +40,8 @@ const userSchema = new mongoose.Schema({
userDocumentId: String, // id that identifies a document which hosts all of a user's account data
sharingDocumentId: String, // id that identifies a document that stores documents shared to a user, their user color, and any additional info needed to communicate between users
+ linkDatabaseId: String,
+ cacheDocumentIds: String, // set of document ids to retreive on startup
facebook: String,
twitter: String,
diff --git a/src/server/websocket.ts b/src/server/websocket.ts
index c9b5d1cbf..1e02b9e58 100644
--- a/src/server/websocket.ts
+++ b/src/server/websocket.ts
@@ -276,8 +276,9 @@ export namespace WebSocket {
const updatefield = Array.from(Object.keys(diff.diff.$set))[0];
const newListItems = diff.diff.$set[updatefield].fields;
const curList = (curListItems as any)?.fields?.[updatefield.replace("fields.", "")]?.fields || [];
- diff.diff.$set[updatefield].fields = [...curList, ...newListItems.filter((newItem: any) => !curList.some((curItem: any) => curItem.fieldId ? curItem.fieldId === newItem.fieldId : curItem.heading ? curItem.heading === newItem.heading : false))];
- const sendBack = true;//curList.length !== prelen;
+ diff.diff.$set[updatefield].fields = [...curList, ...newListItems.filter((newItem: any) => !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,
() => {
if (sendBack) {
@@ -294,8 +295,9 @@ export namespace WebSocket {
const updatefield = Array.from(Object.keys(diff.diff.$set))[0];
const remListItems = diff.diff.$set[updatefield].fields;
const curList = (curListItems as any)?.fields?.[updatefield.replace("fields.", "")]?.fields || [];
- diff.diff.$set[updatefield].fields = curList?.filter((curItem: any) => !remListItems.some((remItem: any) => remItem.fieldId ? remItem.fieldId === curItem.fieldId : remItem.heading ? remItem.heading === curItem.heading : false));
- const sendBack = true;//curList.length + remListItems.length !== prelen;
+ diff.diff.$set[updatefield].fields = curList?.filter((curItem: any) => !remListItems.some((remItem: any) => remItem.fieldId ? remItem.fieldId === curItem.fieldId : remItem.heading ? remItem.heading === curItem.heading : remItem === curItem));
+ const sendBack = diff.diff.length !== diff.diff.$set[updatefield].fields.length;
+ delete diff.diff.length;
Database.Instance.update(diff.id, diff.diff,
() => {
if (sendBack) {
diff --git a/test/test.ts b/test/test.ts
index 9dcd273af..489aa3025 100644
--- a/test/test.ts
+++ b/test/test.ts
@@ -12,7 +12,7 @@ import { Doc } from '../src/fields/Doc';
import { Cast } from '../src/fields/Types';
import { createSchema, makeInterface, defaultSpec } from '../src/fields/Schema';
import { ImageField } from '../src/fields/URLField';
-import { resolvedPorts } from '../src/client/views/Main';
+import { resolvedPorts } from '../src/client/util/CurrentUserUtils';
describe("Document", () => {
it('should hold fields', () => {
const key = "Test";