diff options
author | Geireann Lindfield Roberts <60007097+geireann@users.noreply.github.com> | 2020-08-03 00:05:38 +0800 |
---|---|---|
committer | Geireann Lindfield Roberts <60007097+geireann@users.noreply.github.com> | 2020-08-03 00:05:38 +0800 |
commit | 85c9f1301f989b3a774ee8f72123dac7603a3ada (patch) | |
tree | 761b16b394a51df06e3180e3c818ea98ebbf93c5 | |
parent | 1987cf9c4585fce9fe897c5146ebed1bf45ecd64 (diff) | |
parent | 63fd10d0940331d68a9ce58b4b77734b11e393f5 (diff) |
Merge branch 'master' into presentation_updates
35 files changed, 442 insertions, 554 deletions
diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts index dec8724c6..6fa8cf909 100644 --- a/src/client/DocServer.ts +++ b/src/client/DocServer.ts @@ -156,23 +156,23 @@ export namespace DocServer { let _isReadOnly = false; export function makeReadOnly() { - if (_isReadOnly) return; - _isReadOnly = true; - _CreateField = field => { - _cache[field[Id]] = field; - }; - _UpdateField = emptyFunction; - _RespondToUpdate = emptyFunction; + if (!_isReadOnly) { + _isReadOnly = true; + _CreateField = field => _cache[field[Id]] = field; + _UpdateField = emptyFunction; + _RespondToUpdate = emptyFunction; + } } export function makeEditable() { - if (!_isReadOnly) return; - location.reload(); - // _isReadOnly = false; - // _CreateField = _CreateFieldImpl; - // _UpdateField = _UpdateFieldImpl; - // _respondToUpdate = _respondToUpdateImpl; - // _cache = {}; + if (_isReadOnly) { + location.reload(); + // _isReadOnly = false; + // _CreateField = _CreateFieldImpl; + // _UpdateField = _UpdateFieldImpl; + // _respondToUpdate = _respondToUpdateImpl; + // _cache = {}; + } } export function isReadOnly() { return _isReadOnly; } @@ -451,7 +451,7 @@ export namespace DocServer { } function _UpdateFieldImpl(id: string, diff: any) { - Utils.Emit(_socket, MessageStore.UpdateField, { id, diff }); + (!DocServer.Control.isReadOnly()) && Utils.Emit(_socket, MessageStore.UpdateField, { id, diff }); } let _UpdateField: (id: string, diff: any) => void = errorFunc; diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts index 7578b7df0..985fcce11 100644 --- a/src/client/documents/DocumentTypes.ts +++ b/src/client/documents/DocumentTypes.ts @@ -36,6 +36,5 @@ export enum DocumentType { LINKDB = "linkdb", // database of links ??? why do we have this SCRIPTDB = "scriptdb", // database of scripts - RECOMMENDATION = "recommendation", // view of a recommendation GROUPDB = "groupdb" // database of groups }
\ No newline at end of file diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 98d80e3b0..0da93aa7a 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -45,7 +45,6 @@ import { SliderBox } from "../views/nodes/SliderBox"; import { VideoBox } from "../views/nodes/VideoBox"; import { WebBox } from "../views/nodes/WebBox"; import { PresElementBox } from "../views/presentationview/PresElementBox"; -import { RecommendationsBox } from "../views/RecommendationsBox"; import { DashWebRTCVideo } from "../views/webcam/DashWebRTCVideo"; import { DocumentType } from "./DocumentTypes"; import { Networking } from "../Network"; @@ -306,10 +305,6 @@ export namespace Docs { layout: { view: FontIconBox, dataField: defaultDataKey }, options: { _width: 40, _height: 40, borderRounding: "100%" }, }], - [DocumentType.RECOMMENDATION, { - layout: { view: RecommendationsBox, dataField: defaultDataKey }, - options: { _width: 200, _height: 200 }, - }], [DocumentType.WEBCAM, { layout: { view: DashWebRTCVideo, dataField: defaultDataKey } }], @@ -810,10 +805,6 @@ export namespace Docs { return InstanceFromProto(Prototypes.get(DocumentType.IMPORT), new List<Doc>(), options); } - export function RecommendationsDocument(data: Doc[], options: DocumentOptions = {}) { - return InstanceFromProto(Prototypes.get(DocumentType.RECOMMENDATION), new List<Doc>(data), options); - } - export type DocConfig = { doc: Doc, initialWidth?: number, @@ -1039,6 +1030,7 @@ export namespace DocUtils { event: (args: { x: number, y: number }) => { const newDoc = Doc.ApplyTemplate(dragDoc); if (newDoc) { + newDoc.author = Doc.CurrentUserEmail; newDoc.x = x; newDoc.y = y; docAdder(newDoc); diff --git a/src/client/util/GroupManager.tsx b/src/client/util/GroupManager.tsx index 72fba5c1b..5215ea35f 100644 --- a/src/client/util/GroupManager.tsx +++ b/src/client/util/GroupManager.tsx @@ -20,6 +20,9 @@ import { TaskCompletionBox } from "../views/nodes/TaskCompletedBox"; library.add(fa.faPlus, fa.faTimes, fa.faInfoCircle); +/** + * Interface for options for the react-select component + */ export interface UserOptions { label: string; value: string; @@ -30,15 +33,13 @@ export default class GroupManager extends React.Component<{}> { static Instance: GroupManager; @observable isOpen: boolean = false; // whether the GroupManager is to be displayed or not. - @observable private dialogueBoxOpacity: number = 1; // opacity of the dialogue box div of the MainViewModal. - @observable private overlayOpacity: number = 0.4; // opacity of the overlay div of the MainViewModal. @observable private users: string[] = []; // list of users populated from the database. @observable private selectedUsers: UserOptions[] | null = null; // list of users selected in the "Select users" dropdown. @observable currentGroup: Opt<Doc>; // the currently selected group. @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(); - private currentUserGroups: string[] = []; + private createGroupButtonRef: React.RefObject<HTMLButtonElement> = React.createRef(); // the ref for the group creation button + private currentUserGroups: string[] = []; // the list of groups the current user is a member of @observable private buttonColour: "#979797" | "black" = "#979797"; @observable private groupSort: "ascending" | "descending" | "none" = "none"; @@ -49,6 +50,9 @@ export default class GroupManager extends React.Component<{}> { GroupManager.Instance = this; } + /** + * Populates the list of users and groups. + */ componentDidMount() { this.populateUsers(); this.populateGroups(); @@ -62,8 +66,6 @@ export default class GroupManager extends React.Component<{}> { const userList = await RequestPromise.get(Utils.prepend("/getUsers")); const raw = JSON.parse(userList) as User[]; const evaluating = raw.map(async user => { - // const isCandidate = user.email !== Doc.CurrentUserEmail; - // if (isCandidate) { const userDocument = await DocServer.GetRefField(user.userDocumentId); if (userDocument instanceof Doc) { const notificationDoc = await Cast(userDocument.rightSidebarCollection, Doc); @@ -73,11 +75,13 @@ export default class GroupManager extends React.Component<{}> { } }); } - // } }); return Promise.all(evaluating); } + /** + * Populates the list of groups the current user is a member of and sets this list to be used in the GetEffectiveAcl in util.ts + */ populateGroups = () => { DocListCastAsync(this.GroupManagerDoc?.data).then(groups => { groups?.forEach(group => { @@ -101,7 +105,7 @@ export default class GroupManager extends React.Component<{}> { */ @action open = () => { - SelectionManager.DeselectAll(); + // SelectionManager.DeselectAll(); this.isOpen = true; this.populateUsers(); this.populateGroups(); @@ -145,25 +149,8 @@ export default class GroupManager extends React.Component<{}> { } /** - * @returns a readonly copy of a single group document + * Returns an array of the list of members of a given group. */ - getGroupCopy(groupName: string): Doc | undefined { - const groupDoc = this.getGroup(groupName); - if (groupDoc) { - const { members, owners } = groupDoc; - return Doc.assign(new Doc, { groupName, members: StrCast(members), owners: StrCast(owners) }); - } - return undefined; - } - /** - * @returns a readonly copy of the list of group documents - */ - getAllGroupsCopy(): Doc[] { - return this.getAllGroups().map(({ groupName, owners, members }) => - Doc.assign(new Doc, { groupName: (StrCast(groupName)), owners: (StrCast(owners)), members: (StrCast(members)) }) - ); - } - 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[]; @@ -316,6 +303,9 @@ export default class GroupManager extends React.Component<{}> { } + /** + * @returns the MainViewModal which allows the user to create groups. + */ private get groupCreationModal() { const contents = ( <div className="group-create"> @@ -415,7 +405,7 @@ export default class GroupManager extends React.Component<{}> { <div className="sort-groups" onClick={action(() => this.groupSort = this.groupSort === "ascending" ? "descending" : this.groupSort === "descending" ? "none" : "ascending")}> - Name {this.groupSort === "ascending" ? "↑" : this.groupSort === "descending" ? "↓" : ""} {/* → */} + Name {this.groupSort === "ascending" ? "↑" : this.groupSort === "descending" ? "↓" : ""} </div> <div className="group-body"> {groups.map(group => diff --git a/src/client/util/GroupMemberView.scss b/src/client/util/GroupMemberView.scss index c609c5c7b..2fc27ed03 100644 --- a/src/client/util/GroupMemberView.scss +++ b/src/client/util/GroupMemberView.scss @@ -41,6 +41,7 @@ margin-top: -5; height: 20; text-overflow: ellipsis; + background: none; &:hover { text-overflow: visible; @@ -72,7 +73,7 @@ .editing-contents { overflow-y: auto; - height: 65%; + height: 62%; width: 100%; color: black; margin-top: -15px; diff --git a/src/client/util/GroupMemberView.tsx b/src/client/util/GroupMemberView.tsx index f20670c4e..531ef988a 100644 --- a/src/client/util/GroupMemberView.tsx +++ b/src/client/util/GroupMemberView.tsx @@ -29,13 +29,17 @@ export default class GroupMemberView extends React.Component<GroupMemberViewProp const options: UserOptions[] = this.props.group ? GroupManager.Instance.options.filter(option => !(JSON.parse(StrCast(this.props.group.members)) as string[]).includes(option.value)) : []; + const hasEditAccess = GroupManager.Instance.hasEditAccess(this.props.group); + return (!this.props.group ? null : <div className="editing-interface"> <div className="editing-header"> <input className="group-title" + style={{ marginLeft: !hasEditAccess ? "-14%" : 0 }} value={StrCast(this.props.group.groupName)} onChange={e => this.props.group.groupName = e.currentTarget.value} + disabled={!hasEditAccess} > </input> <div className={"memberView-closeButton"} onClick={action(this.props.onCloseButtonClick)}> @@ -65,12 +69,15 @@ export default class GroupMemberView extends React.Component<GroupMemberViewProp null} <div className="sort-emails" + style={{ paddingTop: hasEditAccess ? 0 : 35 }} onClick={action(() => this.memberSort = this.memberSort === "ascending" ? "descending" : this.memberSort === "descending" ? "none" : "ascending")}> Emails {this.memberSort === "ascending" ? "↑" : this.memberSort === "descending" ? "↓" : ""} {/* → */} </div> </div> <hr /> - <div className="editing-contents"> + <div className="editing-contents" + style={{ height: hasEditAccess ? "62%" : "85%" }} + > {members.map(member => ( <div className="editing-row" @@ -79,7 +86,7 @@ export default class GroupMemberView extends React.Component<GroupMemberViewProp <div className="user-email"> {member} </div> - {GroupManager.Instance.hasEditAccess(this.props.group) ? + {hasEditAccess ? <div className={"remove-button"} onClick={() => GroupManager.Instance.removeMemberFromGroup(this.props.group, member)}> <FontAwesomeIcon icon={fa.faTrashAlt} size={"sm"} /> </div> diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx index 90d59aa51..207c78964 100644 --- a/src/client/util/SettingsManager.tsx +++ b/src/client/util/SettingsManager.tsx @@ -9,18 +9,19 @@ import "./SettingsManager.scss"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Networking } from "../Network"; import { CurrentUserUtils } from "./CurrentUserUtils"; -import { Utils } from "../../Utils"; +import { Utils, addStyleSheet, addStyleSheetRule, removeStyleSheetRule } from "../../Utils"; import { Doc } from "../../fields/Doc"; import GroupManager from "./GroupManager"; import HypothesisAuthenticationManager from "../apis/HypothesisAuthenticationManager"; import GoogleAuthenticationManager from "../apis/GoogleAuthenticationManager"; -import { togglePlaygroundMode } from "../../fields/util"; +import { DocServer } from "../DocServer"; library.add(fa.faTimes); @observer export default class SettingsManager extends React.Component<{}> { public static Instance: SettingsManager; + static _settingsStyle = addStyleSheet(); @observable private isOpen = false; @observable private dialogueBoxOpacity = 1; @observable private overlayOpacity = 0.4; @@ -99,8 +100,11 @@ export default class SettingsManager extends React.Component<{}> { @action togglePlaygroundMode = () => { - togglePlaygroundMode(); this.playgroundMode = !this.playgroundMode; + if (this.playgroundMode) DocServer.Control.makeReadOnly(); + else DocServer.Control.makeEditable(); + + addStyleSheetRule(SettingsManager._settingsStyle, "lm_header", { background: "pink !important" }); } private get settingsInterface() { diff --git a/src/client/util/SharingManager.scss b/src/client/util/SharingManager.scss index 130785672..8da80ef52 100644 --- a/src/client/util/SharingManager.scss +++ b/src/client/util/SharingManager.scss @@ -1,6 +1,6 @@ .sharing-interface { width: 600px; - height: 360px; + // height: 360px; .overlay { transform: translate(-20px, -20px); @@ -23,33 +23,51 @@ z-index: 999; } - .share-setup { - display: flex; - margin-bottom: 20px; - align-items: center; - height: 36; + .share-container { + .share-setup { + display: flex; + margin-bottom: 20px; + align-items: center; + height: 36; - .user-search { - width: 90%; + .user-search { + width: 90%; - input { - height: 30; + input { + height: 30; + } + } + + .permissions-select { + z-index: 1; + margin-left: -100; + border: none; + outline: none; + text-align: justify; // for Edge + text-align-last: end; } - } - .permissions-select { - z-index: 1; - margin-left: -100; - border: none; - outline: none; - text-align: justify; // for Edge - text-align-last: end; + .share-button { + height: 105%; + margin-left: 2%; + background-color: black; + } } - .share-button { - height: 105%; - margin-left: 2%; - background-color: #979797; + .sort-checkboxes { + float: left; + margin-top: -17px; + margin-bottom: 10px; + font-size: 10px; + + input { + height: 10px; + } + + label { + font-weight: normal; + font-style: italic; + } } } @@ -92,10 +110,8 @@ height: 250px; margin: 0 2; - .none { font-style: italic; - } } } diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx index 0d8b33fbe..892fb6d6d 100644 --- a/src/client/util/SharingManager.tsx +++ b/src/client/util/SharingManager.tsx @@ -1,7 +1,7 @@ import { observable, runInAction, action } from "mobx"; import * as React from "react"; import MainViewModal from "../views/MainViewModal"; -import { Doc, Opt, DocListCastAsync } from "../../fields/Doc"; +import { Doc, Opt, DocListCastAsync, AclAdmin, DataSym, AclPrivate } from "../../fields/Doc"; import { DocServer } from "../DocServer"; import { Cast, StrCast } from "../../fields/Types"; import * as RequestPromise from "request-promise"; @@ -19,7 +19,7 @@ import GroupMemberView from "./GroupMemberView"; import Select from "react-select"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { List } from "../../fields/List"; -import { distributeAcls, SharingPermissions } from "../../fields/util"; +import { distributeAcls, SharingPermissions, GetEffectiveAcl } from "../../fields/util"; import { TaskCompletionBox } from "../views/nodes/TaskCompletedBox"; export interface User { @@ -27,7 +27,10 @@ export interface User { userDocumentId: string; } -interface GroupOptions { +/** + * Interface for grouped options for the react-select component. + */ +interface GroupedOptions { label: string; options: UserOptions[]; } @@ -36,9 +39,13 @@ interface GroupOptions { // const PublicKey = "publicLinkPermissions"; // const DefaultColor = "black"; -const groupType = "!groupType/"; +// used to differentiate between individuals and groups when sharing const indType = "!indType/"; +const groupType = "!groupType/"; +/** + * A user who also has a notificationDoc. + */ interface ValidatedUser { user: User; notificationDoc: Doc; @@ -49,18 +56,21 @@ const storage = "data"; @observer export default class SharingManager extends React.Component<{}> { public static Instance: SharingManager; - @observable private isOpen = false; - @observable private users: ValidatedUser[] = []; - @observable private targetDoc: Doc | undefined; - @observable private targetDocView: DocumentView | undefined; + @observable private isOpen = false; // whether the SharingManager modal is open or not + @observable private users: ValidatedUser[] = []; // the list of users with notificationDocs + @observable private targetDoc: Doc | undefined; // the document being shared + @observable private targetDocView: DocumentView | undefined; // the DocumentView of the document being shared // @observable private copied = false; - @observable private dialogueBoxOpacity = 1; - @observable private overlayOpacity = 0.4; - @observable private selectedUsers: UserOptions[] | null = null; - @observable private permissions: SharingPermissions = SharingPermissions.Edit; - @observable private individualSort: "ascending" | "descending" | "none" = "none"; - @observable private groupSort: "ascending" | "descending" | "none" = "none"; - private shareDocumentButtonRef: React.RefObject<HTMLButtonElement> = React.createRef(); + @observable private dialogueBoxOpacity = 1; // for the modal + @observable private overlayOpacity = 0.4; // for the modal + @observable private selectedUsers: UserOptions[] | null = null; // users (individuals/groups) selected to share with + @observable private permissions: SharingPermissions = SharingPermissions.Edit; // the permission with which to share with other users + @observable private individualSort: "ascending" | "descending" | "none" = "none"; // sorting options for the list of individuals + @observable private groupSort: "ascending" | "descending" | "none" = "none"; // sorting options for the list of groups + private shareDocumentButtonRef: React.RefObject<HTMLButtonElement> = React.createRef(); // ref for the share button, used for the position of the popup + // if both showUserOptions and showGroupOptions are false then both are displayed + @observable private showUserOptions: boolean = false; // whether to show individuals as options when sharing (in the react-select component) + @observable private showGroupOptions: boolean = false; // // whether to show groups as options when sharing (in the react-select component) @@ -69,21 +79,22 @@ export default class SharingManager extends React.Component<{}> { // } public open = (target: DocumentView) => { - SelectionManager.DeselectAll(); - this.populateUsers().then(action(() => { + runInAction(() => this.users = []); + // SelectionManager.DeselectAll(); + this.populateUsers(); + runInAction(() => { this.targetDocView = target; this.targetDoc = target.props.Document; DictationOverlay.Instance.hasActiveModal = true; this.isOpen = true; this.permissions = SharingPermissions.Edit; - })); + }); } public close = action(() => { this.isOpen = false; - this.users = []; - this.selectedUsers = null; + this.selectedUsers = null; // resets the list of users and seleected users (in the react-select component) setTimeout(action(() => { // this.copied = false; @@ -97,7 +108,18 @@ export default class SharingManager extends React.Component<{}> { SharingManager.Instance = this; } + /** + * Populates the list of users. + */ + componentDidMount() { + this.populateUsers(); + } + + /** + * Populates the list of validated users (this.users) by adding registered users which have a rightSidebarCollection. + */ populateUsers = async () => { + runInAction(() => this.users = []); const userList = await RequestPromise.get(Utils.prepend("/getUsers")); const raw = JSON.parse(userList) as User[]; const evaluating = raw.map(async user => { @@ -117,58 +139,74 @@ export default class SharingManager extends React.Component<{}> { return Promise.all(evaluating); } + /** + * Sets the permission on the target for the group. + * @param group + * @param permission + */ setInternalGroupSharing = (group: Doc, permission: string) => { const members: string[] = JSON.parse(StrCast(group.members)); const users: ValidatedUser[] = this.users.filter(({ user: { email } }) => members.includes(email)); const target = this.targetDoc!; const ACL = `ACL-${StrCast(group.groupName)}`; - // fix this - not needed (here and setinternalsharing and removegroup) - // target[ACL] = permission; - // Doc.GetProto(target)[ACL] = permission; - distributeAcls(ACL, permission as SharingPermissions, this.targetDoc!); + target.author === Doc.CurrentUserEmail && distributeAcls(ACL, permission as SharingPermissions, target); + // if documents have been shared, add the target to that list if it doesn't already exist, otherwise create a new list with the target group.docsShared ? DocListCastAsync(group.docsShared).then(resolved => Doc.IndexOf(target, resolved!) === -1 && (group.docsShared as List<Doc>).push(target)) : group.docsShared = new List<Doc>([target]); users.forEach(({ notificationDoc }) => { DocListCastAsync(notificationDoc[storage]).then(resolved => { - if (permission !== SharingPermissions.None) Doc.IndexOf(target, resolved!) === -1 && Doc.AddDocToList(notificationDoc, storage, target); - else Doc.IndexOf(target, resolved!) !== -1 && Doc.RemoveDocFromList(notificationDoc, storage, target); + if (permission !== SharingPermissions.None) Doc.IndexOf(target, resolved!) === -1 && Doc.AddDocToList(notificationDoc, storage, target); // add the target to the notificationDoc if it hasn't already been added + else Doc.IndexOf(target, resolved!) !== -1 && Doc.RemoveDocFromList(notificationDoc, storage, target); // remove the target from the list if it already exists }); }); } + /** + * Shares the documents shared with a group with a new user who has been added to that group. + * @param group + * @param emailId + */ shareWithAddedMember = (group: Doc, emailId: string) => { const user: ValidatedUser = this.users.find(({ user: { email } }) => email === emailId)!; if (group.docsShared) { DocListCastAsync(group.docsShared).then(docsShared => { docsShared?.forEach(doc => { - DocListCastAsync(user.notificationDoc[storage]).then(resolved => Doc.IndexOf(doc, resolved!) === -1 && Doc.AddDocToList(user.notificationDoc, storage, doc)); + DocListCastAsync(user.notificationDoc[storage]).then(resolved => Doc.IndexOf(doc, resolved!) === -1 && Doc.AddDocToList(user.notificationDoc, storage, doc)); // add the doc if it isn't already in the list }); }); } } + /** + * Removes the documents shared with a user through a group when the user is removed from the group. + * @param group + * @param emailId + */ removeMember = (group: Doc, emailId: string) => { const user: ValidatedUser = this.users.find(({ user: { email } }) => email === emailId)!; if (group.docsShared) { DocListCastAsync(group.docsShared).then(docsShared => { docsShared?.forEach(doc => { - DocListCastAsync(user.notificationDoc[storage]).then(resolved => Doc.IndexOf(doc, resolved!) !== -1 && Doc.RemoveDocFromList(user.notificationDoc, storage, doc)); + DocListCastAsync(user.notificationDoc[storage]).then(resolved => Doc.IndexOf(doc, resolved!) !== -1 && Doc.RemoveDocFromList(user.notificationDoc, storage, doc)); // remove the doc only if it is in the list }); }); } } + /** + * Removes a group's permissions from documents that have been shared with it. + * @param group + */ removeGroup = (group: Doc) => { if (group.docsShared) { DocListCastAsync(group.docsShared).then(resolved => { resolved?.forEach(doc => { const ACL = `ACL-${StrCast(group.groupName)}`; - // doc[ACL] = doc[DataSym][ACL] = "Not Shared"; distributeAcls(ACL, SharingPermissions.None, doc); @@ -182,14 +220,13 @@ export default class SharingManager extends React.Component<{}> { } } - // @action setInternalSharing = (recipient: ValidatedUser, permission: string) => { const { user, notificationDoc } = recipient; const target = this.targetDoc!; const key = user.email.replace('.', '_'); const ACL = `ACL-${key}`; - distributeAcls(ACL, permission as SharingPermissions, this.targetDoc!); + target.author === Doc.CurrentUserEmail && distributeAcls(ACL, permission as SharingPermissions, target); if (permission !== SharingPermissions.None) { DocListCastAsync(notificationDoc[storage]).then(resolved => { @@ -291,7 +328,7 @@ export default class SharingManager extends React.Component<{}> { const { left, width, top, height } = this.shareDocumentButtonRef.current!.getBoundingClientRect(); TaskCompletionBox.popupX = left - 1.5 * width; - TaskCompletionBox.popupY = top - height; + TaskCompletionBox.popupY = top - 1.5 * height; TaskCompletionBox.textDisplayed = "Document shared!"; TaskCompletionBox.taskCompleted = true; setTimeout(action(() => TaskCompletionBox.taskCompleted = false), 2000); @@ -320,40 +357,62 @@ export default class SharingManager extends React.Component<{}> { const sortedGroups = groupList.sort(this.sortGroups) .map(({ groupName }) => ({ label: StrCast(groupName), value: groupType + StrCast(groupName) })); - const options: GroupOptions[] = GroupManager.Instance ? - [ - { + const options: GroupedOptions[] = []; + + if (GroupManager.Instance) { + if ((this.showUserOptions && this.showGroupOptions) || (!this.showUserOptions && !this.showGroupOptions)) { + options.push({ label: 'Individuals', options: sortedUsers }, - { + { + label: 'Groups', + options: sortedGroups + }); + } + else if (this.showUserOptions) { + options.push({ + label: 'Individuals', + options: sortedUsers + }); + } + else { + options.push({ label: 'Groups', options: sortedGroups - } - ] - : []; + }); + } + } const users = this.individualSort === "ascending" ? this.users.sort(this.sortUsers) : this.individualSort === "descending" ? this.users.sort(this.sortUsers).reverse() : this.users; const groups = this.groupSort === "ascending" ? groupList.sort(this.sortGroups) : this.groupSort === "descending" ? groupList.sort(this.sortGroups).reverse() : groupList; + const effectiveAcl = this.targetDoc ? GetEffectiveAcl(this.targetDoc) : AclPrivate; + const userListContents: (JSX.Element | null)[] = users.map(({ user, notificationDoc }) => { const userKey = user.email.replace('.', '_'); - const permissions = StrCast(this.targetDoc?.[`ACL-${userKey}`], SharingPermissions.None); + const permissions = StrCast(this.targetDoc?.[`ACL-${userKey}`]); - return permissions === SharingPermissions.None || user.email === this.targetDoc?.author ? null : ( + return !permissions || user.email === this.targetDoc?.author ? null : ( <div key={userKey} className={"container"} > <span className={"padding"}>{user.email}</span> <div className="edit-actions"> - <select - className={"permissions-dropdown"} - value={permissions} - onChange={e => this.setInternalSharing({ user, notificationDoc }, e.currentTarget.value)} - > - {this.sharingOptions} - </select> + {effectiveAcl === AclAdmin ? ( + <select + className={"permissions-dropdown"} + value={permissions} + onChange={e => this.setInternalSharing({ user, notificationDoc }, e.currentTarget.value)} + > + {this.sharingOptions} + </select> + ) : ( + <div className={"permissions-dropdown"}> + {permissions} + </div> + )} </div> </div> ); @@ -365,20 +424,34 @@ export default class SharingManager extends React.Component<{}> { key={"owner"} className={"container"} > - <span className={"padding"}>{this.targetDoc?.author}</span> + <span className={"padding"}>{this.targetDoc?.author === Doc.CurrentUserEmail ? "Me" : this.targetDoc?.author}</span> <div className="edit-actions"> <div className={"permissions-dropdown"}> Owner </div> </div> </div> - ) + ), + this.targetDoc?.author !== Doc.CurrentUserEmail ? + ( + <div + key={"me"} + className={"container"} + > + <span className={"padding"}>Me</span> + <div className="edit-actions"> + <div className={"permissions-dropdown"}> + {this.targetDoc?.[`ACL-${Doc.CurrentUserEmail.replace(".", "_")}`]} + </div> + </div> + </div> + ) : null ); const groupListContents = groups.map(group => { - const permissions = StrCast(this.targetDoc?.[`ACL-${StrCast(group.groupName)}`], SharingPermissions.None); + const permissions = StrCast(this.targetDoc?.[`ACL-${StrCast(group.groupName)}`]); - return permissions === SharingPermissions.None ? null : ( + return !permissions ? null : ( <div key={StrCast(group.groupName)} className={"container"} @@ -400,7 +473,6 @@ export default class SharingManager extends React.Component<{}> { ); }); - const displayUserList = !userListContents?.every(user => user === null); const displayGroupList = !groupListContents?.every(group => group === null); return ( @@ -446,8 +518,7 @@ export default class SharingManager extends React.Component<{}> { <div className={"close-button"} onClick={this.close}> <FontAwesomeIcon icon={"times"} color={"black"} size={"lg"} /> </div> - {this.targetDoc?.author !== Doc.CurrentUserEmail ? null - : + {<div className="share-container"> <div className="share-setup"> <Select className={"user-search"} @@ -457,6 +528,11 @@ export default class SharingManager extends React.Component<{}> { options={options} onChange={this.handleUsersChange} value={this.selectedUsers} + styles={{ + indicatorSeparator: () => ({ + visibility: "hidden" + }) + }} /> <select className="permissions-select" onChange={this.handlePermissionsChange}> {this.sharingOptions} @@ -465,6 +541,11 @@ export default class SharingManager extends React.Component<{}> { Share </button> </div> + <div className="sort-checkboxes"> + <input type="checkbox" onChange={action(() => this.showUserOptions = !this.showUserOptions)} /> <label style={{ marginRight: 10 }}>Individuals</label> + <input type="checkbox" onChange={action(() => this.showGroupOptions = !this.showGroupOptions)} /> <label>Groups</label> + </div> + </div> } <div className="main-container"> <div className={"individual-container"}> @@ -473,17 +554,8 @@ export default class SharingManager extends React.Component<{}> { onClick={action(() => this.individualSort = this.individualSort === "ascending" ? "descending" : this.individualSort === "descending" ? "none" : "ascending")}> Individuals {this.individualSort === "ascending" ? "↑" : this.individualSort === "descending" ? "↓" : ""} {/* → */} </div> - <div className={"users-list"} style={{ display: !displayUserList ? "flex" : "block" }}>{/*200*/} - { - !displayUserList ? - <div - className={"none"} - > - There are no users this document has been shared with. - </div> - : - userListContents - } + <div className={"users-list"} style={{ display: "block" }}>{/*200*/} + {userListContents} </div> </div> <div className={"group-container"}> diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx index 4c82149e2..804c7a8d4 100644 --- a/src/client/views/DocComponent.tsx +++ b/src/client/views/DocComponent.tsx @@ -7,7 +7,7 @@ import { InteractionUtils } from '../util/InteractionUtils'; import { List } from '../../fields/List'; import { DateField } from '../../fields/DateField'; import { ScriptField } from '../../fields/ScriptField'; -import { GetEffectiveAcl, getPlaygroundMode, SharingPermissions } from '../../fields/util'; +import { GetEffectiveAcl, SharingPermissions } from '../../fields/util'; /// DocComponent returns a generic React base class used by views that don't have 'fieldKey' props (e.g.,CollectionFreeFormDocumentView, DocumentView) @@ -150,25 +150,25 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T const effectiveAcl = GetEffectiveAcl(this.dataDoc); if (added.length) { - if (effectiveAcl === AclPrivate || (effectiveAcl === AclReadonly && !getPlaygroundMode())) { + if (effectiveAcl === AclPrivate || effectiveAcl === AclReadonly) { return false; } else { - if (this.props.Document[AclSym]) { - added.forEach(d => { - const dataDoc = d[DataSym]; - dataDoc[AclSym] = d[AclSym] = this.props.Document[AclSym]; - for (const [key, value] of Object.entries(this.props.Document[AclSym])) { - dataDoc[key] = d[key] = this.AclMap.get(value); - } - }); - } + // if (this.props.Document[AclSym]) { + // added.forEach(d => { + // const dataDoc = d[DataSym]; + // dataDoc[AclSym] = d[AclSym] = this.props.Document[AclSym]; + // for (const [key, value] of Object.entries(this.props.Document[AclSym])) { + // dataDoc[key] = d[key] = this.AclMap.get(value); + // } + // }); + // } if (effectiveAcl === AclAddonly) { added.map(doc => Doc.AddDocToList(targetDataDoc, this.annotationKey, doc)); } else { added.map(doc => doc.context = this.props.Document); - targetDataDoc[this.annotationKey] = new List<Doc>([...docList, ...added]); + (targetDataDoc[this.annotationKey] as List<Doc>).push(...added); targetDataDoc[this.annotationKey + "-lastModified"] = new DateField(new Date(Date.now())); } } diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 190dbc8c3..7fc4a5c99 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -1,9 +1,9 @@ import { IconProp, library } from '@fortawesome/fontawesome-svg-core'; import { faCaretUp, faFilePdf, faFilm, faImage, faObjectGroup, faStickyNote, faTextHeight, faArrowAltCircleDown, faArrowAltCircleUp, faCheckCircle, faCloudUploadAlt, faLink, faShare, faStopCircle, faSyncAlt, faTag, faTimes, faAngleLeft, faAngleRight, faAngleDoubleLeft, faAngleDoubleRight, faPause } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, computed, observable, reaction, runInAction } from "mobx"; +import { action, computed, observable, reaction, runInAction, get } from "mobx"; import { observer } from "mobx-react"; -import { Doc, DataSym, Field, WidthSym, HeightSym } from "../../fields/Doc"; +import { Doc, DataSym, Field, WidthSym, HeightSym, AclEdit, AclAdmin } from "../../fields/Doc"; import { Document } from '../../fields/documentSchemas'; import { ScriptField } from '../../fields/ScriptField'; import { Cast, StrCast, NumCast } from "../../fields/Types"; @@ -23,6 +23,9 @@ import { SnappingManager } from '../util/SnappingManager'; import { HtmlField } from '../../fields/HtmlField'; import { InkField } from "../../fields/InkField"; import { Tooltip } from '@material-ui/core'; +import { GetEffectiveAcl } from '../../fields/util'; +import { DocumentIcon } from './nodes/DocumentIcon'; +import { render } from 'react-dom'; library.add(faCaretUp); library.add(faObjectGroup); @@ -89,7 +92,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> const transform = (documentView.props.ScreenToLocalTransform().scale(documentView.props.ContentScaling())).inverse(); var [sptX, sptY] = transform.transformPoint(0, 0); let [bptX, bptY] = transform.transformPoint(documentView.props.PanelWidth(), documentView.props.PanelHeight()); - if (StrCast(Doc.Layout(documentView.props.Document).layout).includes("LinkAnchorBox")) { + if (documentView.props.LayoutTemplateString?.includes("LinkAnchorBox")) { const docuBox = documentView.ContentDiv.getElementsByClassName("linkAnchorBox-cont"); if (docuBox.length) { const rect = docuBox[0].getBoundingClientRect(); @@ -194,8 +197,11 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> SelectionManager.DeselectAll(); selected.map(dv => { - recent && Doc.AddDocToList(recent, "data", dv.props.Document, undefined, true, true); - dv.props.removeDocument?.(dv.props.Document); + const effectiveAcl = GetEffectiveAcl(dv.props.Document); + if (effectiveAcl === AclEdit || effectiveAcl === AclAdmin) { // deletes whatever you have the right to delete + recent && Doc.AddDocToList(recent, "data", dv.props.Document, undefined, true, true); + dv.props.removeDocument?.(dv.props.Document); + } }); } } @@ -580,17 +586,18 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> if (SnappingManager.GetIsDragging() || bounds.r - bounds.x < 2 || bounds.x === Number.MAX_VALUE || !seldoc || this._hidden || isNaN(bounds.r) || isNaN(bounds.b) || isNaN(bounds.x) || isNaN(bounds.y)) { return (null); } + const canDelete = SelectionManager.SelectedDocuments().map(docView => GetEffectiveAcl(docView.props.ContainingCollectionDoc)).some(permission => permission === AclAdmin || permission === AclEdit); const minimal = bounds.r - bounds.x < 100 ? true : false; const maximizeIcon = minimal ? ( <Tooltip title={<><div className="dash-tooltip">Show context menu</div></>} placement="top"> <div className="documentDecorations-contextMenu" onPointerDown={this.onSettingsDown}> <FontAwesomeIcon size="lg" icon="cog" /> - </div></Tooltip>) : ( - <Tooltip title={<><div className="dash-tooltip">Delete</div></>} placement="top"> - <div className="documentDecorations-closeButton" onClick={this.onCloseClick}> - {/* Currently, this is set to be enabled if there is no ink selected. It might be interesting to think about minimizing ink if it's useful? -syip2*/} - <FontAwesomeIcon className="documentdecorations-times" icon={faTimes} size="lg" /> - </div></Tooltip>); + </div></Tooltip>) : canDelete ? ( + <Tooltip title={<><div className="dash-tooltip">Delete</div></>} placement="top"> + <div className="documentDecorations-closeButton" onClick={this.onCloseClick}> + {/* Currently, this is set to be enabled if there is no ink selected. It might be interesting to think about minimizing ink if it's useful? -syip2*/} + <FontAwesomeIcon className="documentdecorations-times" icon={faTimes} size="lg" /> + </div></Tooltip>) : (null); const titleArea = this._edtingTitle ? <> diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index 086085db5..c9f95a538 100644 --- a/src/client/views/GlobalKeyHandler.ts +++ b/src/client/views/GlobalKeyHandler.ts @@ -105,7 +105,6 @@ export default class KeyManager { } doDeselect && SelectionManager.DeselectAll(); DictationManager.Controls.stop(); - // RecommendationsBox.Instance.closeMenu(); GoogleAuthenticationManager.Instance.cancel(); HypothesisAuthenticationManager.Instance.cancel(); SharingManager.Instance.close(); diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 14d46c5e9..58478ce92 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -1,5 +1,5 @@ import { library } from '@fortawesome/fontawesome-svg-core'; -import { faHireAHelper } from '@fortawesome/free-brands-svg-icons'; +import { faHireAHelper, faBuffer } from '@fortawesome/free-brands-svg-icons'; import * as fa from '@fortawesome/free-solid-svg-icons'; import { ANTIMODEMENU_HEIGHT } from './globalCssVariables.scss'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; @@ -149,7 +149,7 @@ export class MainView extends React.Component { fa.faFillDrip, fa.faLink, fa.faUnlink, fa.faBold, fa.faItalic, fa.faChevronLeft, fa.faUnderline, fa.faStrikethrough, fa.faSuperscript, fa.faSubscript, fa.faIndent, fa.faEyeDropper, fa.faPaintRoller, fa.faBars, fa.faBrush, fa.faShapes, fa.faEllipsisH, fa.faHandPaper, fa.faMap, fa.faUser, faHireAHelper, fa.faBezierCurve, fa.faCircle, fa.faLongArrowAltRight, fa.faPenFancy, fa.faAngleDoubleRight, fa.faAngleUp, fa.faAngleDown, fa.faPlayCircle, fa.faClock, - fa.faRocket, fa.faExchangeAlt); + fa.faRocket, fa.faExchangeAlt, faBuffer); this.initEventListeners(); this.initAuthenticationRouters(); } diff --git a/src/client/views/MainViewModal.tsx b/src/client/views/MainViewModal.tsx index 249715511..66ea2dbf8 100644 --- a/src/client/views/MainViewModal.tsx +++ b/src/client/views/MainViewModal.tsx @@ -10,7 +10,7 @@ export interface MainViewOverlayProps { overlayStyle?: React.CSSProperties; dialogueBoxDisplayedOpacity?: number; overlayDisplayedOpacity?: number; - closeOnExternalClick?: () => void; + closeOnExternalClick?: () => void; // the close method of a MainViewModal, triggered if there is a click on the overlay (closing the modal) } @observer diff --git a/src/client/views/RecommendationsBox.scss b/src/client/views/RecommendationsBox.scss deleted file mode 100644 index 7d89042a4..000000000 --- a/src/client/views/RecommendationsBox.scss +++ /dev/null @@ -1,69 +0,0 @@ -@import "globalCssVariables"; - -.rec-content *{ - display: inline-block; - margin: auto; - width: 50; - height: 150px; - border: 1px dashed grey; - padding: 10px 10px; -} - -.rec-content { - float: left; - width: inherit; - align-content: center; -} - -.rec-scroll { - overflow-y: scroll; - overflow-x: hidden; - position: absolute; - pointer-events: all; - // display: flex; - z-index: 10000; - box-shadow: gray 0.2vw 0.2vw 0.4vw; - // flex-direction: column; - background: whitesmoke; - padding-bottom: 10px; - padding-top: 20px; - // border-radius: 15px; - border: solid #BBBBBBBB 1px; - width: 100%; - text-align: center; - // max-height: 250px; - height: 100%; - text-transform: uppercase; - color: grey; - letter-spacing: 2px; -} - -.content { - padding: 10px; - display: flex; - flex-direction: row; - align-items: center; - justify-content: center; -} - -.image-background { - pointer-events: none; - background-color: transparent; - width: 50%; - text-align: center; - margin-left: 5px; -} - -// bcz: UGH!! Can't have global settings like this!!! -// img{ -// width: 100%; -// height: 100%; -// } - -.score { - // margin-left: 15px; - width: 50%; - height: 100%; - text-align: center; - margin-left: 10px; -} diff --git a/src/client/views/RecommendationsBox.tsx b/src/client/views/RecommendationsBox.tsx deleted file mode 100644 index 196151e32..000000000 --- a/src/client/views/RecommendationsBox.tsx +++ /dev/null @@ -1,201 +0,0 @@ -import { observer } from "mobx-react"; -import React = require("react"); -import { observable, action, computed, runInAction } from "mobx"; -import Measure from "react-measure"; -import "./RecommendationsBox.scss"; -import { Doc, DocListCast, WidthSym, HeightSym } from "../../fields/Doc"; -import { DocumentIcon } from "./nodes/DocumentIcon"; -import { StrCast, NumCast } from "../../fields/Types"; -import { returnFalse, emptyFunction, returnEmptyString, returnOne, emptyPath, returnZero, returnEmptyFilter } from "../../Utils"; -import { Transform } from "../util/Transform"; -import { ObjectField } from "../../fields/ObjectField"; -import { DocumentView } from "./nodes/DocumentView"; -import { DocumentType } from '../documents/DocumentTypes'; -import { ClientRecommender } from "../ClientRecommender"; -import { DocServer } from "../DocServer"; -import { Id } from "../../fields/FieldSymbols"; -import { FieldView, FieldViewProps } from "./nodes/FieldView"; -import { DocumentManager } from "../util/DocumentManager"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { library } from "@fortawesome/fontawesome-svg-core"; -import { faBullseye, faLink } from "@fortawesome/free-solid-svg-icons"; -import { DocUtils } from "../documents/Documents"; - -export interface RecProps { - documents: { preview: Doc, similarity: number }[]; - node: Doc; -} - -library.add(faBullseye, faLink); - -@observer -export class RecommendationsBox extends React.Component<FieldViewProps> { - - public static LayoutString(fieldKey: string) { return FieldView.LayoutString(RecommendationsBox, fieldKey); } - - // @observable private _display: boolean = false; - @observable private _pageX: number = 0; - @observable private _pageY: number = 0; - @observable private _width: number = 0; - @observable private _height: number = 0; - @observable.shallow private _docViews: JSX.Element[] = []; - // @observable private _documents: { preview: Doc, score: number }[] = []; - private previewDocs: Doc[] = []; - - constructor(props: FieldViewProps) { - super(props); - } - - @action - private DocumentIcon(doc: Doc) { - const layoutresult = StrCast(doc.type); - let renderDoc = doc; - //let box: number[] = []; - if (layoutresult.indexOf(DocumentType.COL) !== -1) { - renderDoc = Doc.MakeDelegate(renderDoc); - } - const returnXDimension = () => 150; - const returnYDimension = () => 150; - const scale = () => returnXDimension() / NumCast(renderDoc._nativeWidth, returnXDimension()); - //let scale = () => 1; - const newRenderDoc = Doc.MakeAlias(renderDoc); /// newRenderDoc -> renderDoc -> render"data"Doc -> TextProt - newRenderDoc.height = NumCast(this.props.Document.documentIconHeight); - newRenderDoc.autoHeight = false; - const docview = <div> - <DocumentView - fitToBox={StrCast(doc.type).indexOf(DocumentType.COL) !== -1} - Document={newRenderDoc} - addDocument={returnFalse} - LibraryPath={emptyPath} - removeDocument={returnFalse} - rootSelected={returnFalse} - ScreenToLocalTransform={Transform.Identity} - addDocTab={returnFalse} - pinToPres={returnFalse} - renderDepth={1} - NativeHeight={returnZero} - NativeWidth={returnZero} - PanelWidth={returnXDimension} - PanelHeight={returnYDimension} - focus={emptyFunction} - backgroundColor={returnEmptyString} - parentActive={returnFalse} - whenActiveChanged={returnFalse} - bringToFront={emptyFunction} - docFilters={returnEmptyFilter} - ContainingCollectionView={undefined} - ContainingCollectionDoc={undefined} - ContentScaling={scale} - /> - </div>; - return docview; - - } - - // @action - // closeMenu = () => { - // this._display = false; - // this.previewDocs.forEach(doc => DocServer.DeleteDocument(doc[Id])); - // this.previewDocs = []; - // } - - // @action - // resetDocuments = () => { - // this._documents = []; - // } - - // @action - // displayRecommendations(x: number, y: number) { - // this._pageX = x; - // this._pageY = y; - // this._display = true; - // } - - static readonly buffer = 20; - - // get pageX() { - // const x = this._pageX; - // if (x < 0) { - // return 0; - // } - // const width = this._width; - // if (x + width > window.innerWidth - RecommendationsBox.buffer) { - // return window.innerWidth - RecommendationsBox.buffer - width; - // } - // return x; - // } - - // get pageY() { - // const y = this._pageY; - // if (y < 0) { - // return 0; - // } - // const height = this._height; - // if (y + height > window.innerHeight - RecommendationsBox.buffer) { - // return window.innerHeight - RecommendationsBox.buffer - height; - // } - // return y; - // } - - // get createDocViews() { - // return DocListCast(this.props.Document.data).map(doc => { - // return ( - // <div className="content"> - // <span style={{ height: NumCast(this.props.Document.documentIconHeight) }} className="image-background"> - // {this.DocumentIcon(doc)} - // </span> - // <span className="score">{NumCast(doc.score).toFixed(4)}</span> - // <div style={{ marginRight: 50 }} onClick={() => DocumentManager.Instance.jumpToDocument(doc, false)}> - // <FontAwesomeIcon className="documentdecorations-icon" icon={"bullseye"} size="sm" /> - // </div> - // <div style={{ marginRight: 50 }} onClick={() => DocUtils.MakeLink({ doc: this.props.Document.sourceDoc as Doc }, { doc: doc }, "User Selected Link", "Generated from Recommender", undefined)}> - // <FontAwesomeIcon className="documentdecorations-icon" icon={"link"} size="sm" /> - // </div> - // </div> - // ); - // }); - // } - - componentDidMount() { //TODO: invoking a computedFn from outside an reactive context won't be memoized, unless keepAlive is set - runInAction(() => { - if (this._docViews.length === 0) { - this._docViews = DocListCast(this.props.Document.data).map(doc => { - return ( - <div className="content"> - <span style={{ height: NumCast(this.props.Document.documentIconHeight) }} className="image-background"> - {this.DocumentIcon(doc)} - </span> - <span className="score">{NumCast(doc.score).toFixed(4)}</span> - <div style={{ marginRight: 50 }} onClick={() => DocumentManager.Instance.jumpToDocument(doc, false)}> - <FontAwesomeIcon className="documentdecorations-icon" icon={"bullseye"} size="sm" /> - </div> - <div style={{ marginRight: 50 }} onClick={() => DocUtils.MakeLink({ doc: this.props.Document.sourceDoc as Doc }, { doc: doc }, "Recommender", "", undefined)}> - <FontAwesomeIcon className="documentdecorations-icon" icon={"link"} size="sm" /> - </div> - </div> - ); - }); - } - }); - } - - render() { //TODO: Invariant violation: max depth exceeded error. Occurs when images are rendered. - // if (!this._display) { - // return null; - // } - // let style = { left: this.pageX, top: this.pageY }; - //const transform = "translate(" + (NumCast(this.props.node.x) + 350) + "px, " + NumCast(this.props.node.y) + "px" - let title = StrCast((this.props.Document.sourceDoc as Doc).title); - if (title.length > 15) { - title = title.substring(0, 15) + "..."; - } - return ( - <div className="rec-scroll"> - <p>Recommendations for "{title}"</p> - {this._docViews} - </div> - ); - } - // - // -}
\ No newline at end of file diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx index 9fb8a227e..eb20fc257 100644 --- a/src/client/views/TemplateMenu.tsx +++ b/src/client/views/TemplateMenu.tsx @@ -63,14 +63,6 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> { this.props.docViews.map(dv => dv.switchViews(false, "layout")); } - toggleFloat = (e: React.ChangeEvent<HTMLInputElement>): void => { - SelectionManager.DeselectAll(); - const topDocView = this.props.docViews[0]; - const ex = e.target.getBoundingClientRect().left; - const ey = e.target.getBoundingClientRect().top; - DocumentView.FloatDoc(topDocView, ex, ey); - } - toggleAudio = (e: React.ChangeEvent<HTMLInputElement>): void => { this.props.docViews.map(dv => dv.props.Document._showAudio = e.target.checked); } @@ -127,7 +119,6 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> { this.props.templates.forEach((checked, template) => templateMenu.push(<TemplateToggle key={template.Name} template={template} checked={checked} toggle={this.toggleTemplate} />)); templateMenu.push(<OtherToggle key={"audio"} name={"Audio"} checked={firstDoc._showAudio ? true : false} toggle={this.toggleAudio} />); - templateMenu.push(<OtherToggle key={"float"} name={"Float"} checked={firstDoc.z ? true : false} toggle={this.toggleFloat} />); templateMenu.push(<OtherToggle key={"chrome"} name={"Chrome"} checked={layout._chromeStatus !== "disabled"} toggle={this.toggleChrome} />); templateMenu.push(<OtherToggle key={"default"} name={"Default"} checked={templateName === "layout"} toggle={this.toggleDefault} />); addedTypes.concat(noteTypes).map(template => template.treeViewChecked = this.templateIsUsed(firstDoc, template)); diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index b82a33bd8..f658e9816 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -597,7 +597,6 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp stackCreated = (stack: any) => { //stack.header.controlsContainer.find('.lm_popout').hide(); - stack.header.element[0].style.backgroundColor = DocServer.Control.isReadOnly() ? "#228540" : undefined; stack.header.element.on('mousedown', (e: any) => { if (e.target === stack.header.element[0] && e.button === 1) { this.AddTab(stack, Docs.Create.FreeformDocument([], { _width: this.props.PanelWidth(), _height: this.props.PanelHeight(), title: "Untitled Collection" })); diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx index d6cb79e9c..0ca86172f 100644 --- a/src/client/views/collections/CollectionMenu.tsx +++ b/src/client/views/collections/CollectionMenu.tsx @@ -27,6 +27,7 @@ import { ColorState } from "react-color"; import { ObjectField } from "../../../fields/ObjectField"; import { ScriptField } from "../../../fields/ScriptField"; import { IconProp } from '@fortawesome/fontawesome-svg-core'; +import { DocUtils } from "../../documents/Documents"; @observer export default class CollectionMenu extends AntimodeMenu { @@ -313,12 +314,19 @@ export class CollectionViewBaseChrome extends React.Component<CollectionMenuProp <div className="collectionMenu"> <div className="collectionViewBaseChrome"> {this.props.type === CollectionViewType.Invalid || this.props.type === CollectionViewType.Docking ? (null) : this.viewModes} - {this.props.type === CollectionViewType.Invalid || this.props.type === CollectionViewType.Docking ? (null) : this.templateChrome} + {this.props.type === CollectionViewType.Docking ? (null) : this.templateChrome} <div className="collectionViewBaseChrome-viewSpecs" title="filter documents to show" style={{ display: "grid" }}> <button className={"antimodeMenu-button"} onClick={this.toggleViewSpecs} > <FontAwesomeIcon icon="filter" size="lg" /> </button> </div> + + {this.props.docView.props.ContainingCollectionDoc?._viewType !== CollectionViewType.Freeform ? (null) : <button className={"antimodeMenu-button"} key="float" + style={{ backgroundColor: !this.props.docView.layoutDoc.isAnnotating ? "121212" : undefined, borderRight: "1px solid gray" }} + title="Toggle Overlay Layer" + onClick={() => DocumentView.FloatDoc(this.props.docView)}> + <FontAwesomeIcon icon={["fab", "buffer"]} size={"lg"} /> + </button>} </div> {this.subChrome} </div> @@ -540,7 +548,7 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionMenu <FontAwesomeIcon icon={"caret-right"} size={"lg"} /> </div> - {!this.props.isOverlay ? (null) : + {!this.props.isOverlay || this.document.type !== DocumentType.WEB ? (null) : <button className={"antimodeMenu-button"} key="hypothesis" style={{ backgroundColor: !this.props.docView.layoutDoc.isAnnotating ? "121212" : undefined, borderRight: "1px solid gray" }} title="Use Hypothesis" diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index a89fcc703..9f78c15eb 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -127,7 +127,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: const docs = rawdocs.filter(d => !(d instanceof Promise)).map(d => d as Doc); const docFilters = this.docFilters(); - const viewSpecScript = Cast(this.props.Document.viewSpecScript, ScriptField); + const viewSpecScript = ScriptCast(this.props.Document.viewSpecScript); const docRangeFilters = this.props.ignoreFields?.includes("_docRangeFilters") ? [] : Cast(this.props.Document._docRangeFilters, listSpec("string"), []); return this.props.Document.dontRegisterView ? docs : DocUtils.FilterDocs(docs, docFilters, docRangeFilters, viewSpecScript); diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index dd823f5d5..b8996c178 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -100,8 +100,8 @@ class TreeView extends React.Component<TreeViewProps> { childDocList(field: string) { const layout = Doc.LayoutField(this.doc) instanceof Doc ? Doc.LayoutField(this.doc) as Doc : undefined; return ((this.props.dataDoc ? DocListCast(this.props.dataDoc[field]) : undefined) || // if there's a data doc for an expanded template, use it's data field - (layout ? Cast(layout[field], listSpec(Doc)) : undefined) || // else if there's a layout doc, display it's fields - Cast(this.doc[field], listSpec(Doc))) as Doc[]; // otherwise use the document's data field + (layout ? DocListCast(layout[field]) : undefined) || // else if there's a layout doc, display it's fields + DocListCast(this.doc[field])) as Doc[]; // otherwise use the document's data field } @computed get childDocs() { return this.childDocList(this.fieldKey); } @computed get childLinks() { return this.childDocList("links"); } diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 42d320308..7e7ea6786 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -17,7 +17,7 @@ import { listSpec } from '../../../fields/Schema'; import { ComputedField, ScriptField } from '../../../fields/ScriptField'; import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; import { ImageField } from '../../../fields/URLField'; -import { TraceMobx, GetEffectiveAcl, getPlaygroundMode, distributeAcls, SharingPermissions } from '../../../fields/util'; +import { TraceMobx, GetEffectiveAcl, SharingPermissions } from '../../../fields/util'; import { emptyFunction, emptyPath, returnEmptyFilter, returnFalse, returnOne, returnZero, setupMoveUpEvents, Utils } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; import { DocumentType } from '../../documents/DocumentTypes'; @@ -142,20 +142,20 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus const effectiveAcl = GetEffectiveAcl(this.props.Document); if (added.length) { - if (effectiveAcl === AclPrivate || (effectiveAcl === AclReadonly && !getPlaygroundMode())) { + if (effectiveAcl === AclPrivate || effectiveAcl === AclReadonly) { return false; } else { - if (this.props.Document[AclSym]) { - // change so it only adds if more restrictive - added.forEach(d => { - // const dataDoc = d[DataSym]; - for (const [key, value] of Object.entries(this.props.Document[AclSym])) { - distributeAcls(key, this.AclMap.get(value) as SharingPermissions, d, true); - } - // dataDoc[AclSym] = d[AclSym] = this.props.Document[AclSym]; - }); - } + // if (this.props.Document[AclSym]) { + // // change so it only adds if more restrictive + // added.forEach(d => { + // // const dataDoc = d[DataSym]; + // for (const [key, value] of Object.entries(this.props.Document[AclSym])) { + // // key.substring(4).replace("_", ".") !== Doc.CurrentUserEmail && distributeAcls(key, this.AclMap.get(value) as SharingPermissions, d, true); + // distributeAcls(key, this.AclMap.get(value) as SharingPermissions, d, true); + // } + // }); + // } if (effectiveAcl === AclAddonly) { added.map(doc => Doc.AddDocToList(targetDataDoc, this.props.fieldKey, doc)); @@ -179,7 +179,8 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus doc.context = this.props.Document; }); added.map(add => Doc.AddDocToList(Cast(Doc.UserDoc().myCatalog, Doc, null), "data", add)); - targetDataDoc[this.props.fieldKey] = new List<Doc>([...docList, ...added]); + // targetDataDoc[this.props.fieldKey] = new List<Doc>([...docList, ...added]); + (targetDataDoc[this.props.fieldKey] as List<Doc>).push(...added); targetDataDoc[this.props.fieldKey + "-lastModified"] = new DateField(new Date(Date.now())); } } @@ -189,14 +190,16 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus @action.bound removeDocument = (doc: any): boolean => { - const effectiveAcl = GetEffectiveAcl(this.props.Document); - if (effectiveAcl === AclEdit || effectiveAcl === AclAdmin || getPlaygroundMode()) { + const collectionEffectiveAcl = GetEffectiveAcl(this.props.Document); + const docEffectiveAcl = GetEffectiveAcl(doc); + // you can remove the document if you either have Admin/Edit access to the collection or to the specific document + if (collectionEffectiveAcl === AclEdit || collectionEffectiveAcl === AclAdmin || docEffectiveAcl === AclAdmin || docEffectiveAcl === AclEdit) { const docs = doc instanceof Doc ? [doc] : doc as Doc[]; const targetDataDoc = this.props.Document[DataSym]; const value = DocListCast(targetDataDoc[this.props.fieldKey]); - const result = value.filter(v => !docs.includes(v)); - if (result.length !== value.length) { - targetDataDoc[this.props.fieldKey] = new List<Doc>(result); + const toRemove = value.filter(v => docs.includes(v)); + if (toRemove.length !== 0) { + toRemove.forEach(doc => Doc.RemoveDocFromList(targetDataDoc, this.props.fieldKey, doc)); return true; } } diff --git a/src/client/views/collections/collectionFreeForm/FormatShapePane.scss b/src/client/views/collections/collectionFreeForm/FormatShapePane.scss index 010beb836..d49ab27fb 100644 --- a/src/client/views/collections/collectionFreeForm/FormatShapePane.scss +++ b/src/client/views/collections/collectionFreeForm/FormatShapePane.scss @@ -27,13 +27,15 @@ position: absolute; } -.sketch-picker { - background: #323232; - width: 160px !important; - height: 80% !important; - - .flexbox-fit { +.btn-group-palette { + .sketch-picker { background: #323232; + width: 160px !important; + height: 80% !important; + + .flexbox-fit { + background: #323232; + } } } diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 764758eee..a32c8b363 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -1,7 +1,7 @@ import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; import { Doc, Opt, DocListCast, DataSym, AclEdit, AclAddonly, AclAdmin } from "../../../../fields/Doc"; -import { GetEffectiveAcl, getPlaygroundMode } from "../../../../fields/util"; +import { GetEffectiveAcl } from "../../../../fields/util"; import { InkData, InkField, InkTool } from "../../../../fields/InkField"; import { List } from "../../../../fields/List"; import { RichTextField } from "../../../../fields/RichTextField"; @@ -281,7 +281,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque this._downX = x; this._downY = y; const effectiveAcl = GetEffectiveAcl(this.props.Document); - if ([AclAdmin, AclEdit, AclAddonly].includes(effectiveAcl) || getPlaygroundMode()) PreviewCursor.Show(x, y, this.onKeyPress, this.props.addLiveTextDocument, this.props.getTransform, this.props.addDocument, this.props.nudge); + if ([AclAdmin, AclEdit, AclAddonly].includes(effectiveAcl)) PreviewCursor.Show(x, y, this.onKeyPress, this.props.addLiveTextDocument, this.props.getTransform, this.props.addDocument, this.props.nudge); this.clearSelection(); } }); diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index 47dc0a773..e8173d103 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -35,7 +35,6 @@ import { VideoBox } from "./VideoBox"; import { WebBox } from "./WebBox"; import { InkingStroke } from "../InkingStroke"; import React = require("react"); -import { RecommendationsBox } from "../RecommendationsBox"; import { TraceMobx, GetEffectiveAcl } from "../../../fields/util"; import { ScriptField } from "../../../fields/ScriptField"; import XRegExp = require("xregexp"); @@ -194,7 +193,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & { CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, PresBox, YoutubeBox, PresElementBox, QueryBox, ColorBox, DashWebRTCVideo, LinkAnchorBox, InkingStroke, DocHolderBox, LinkBox, ScriptingBox, - RecommendationsBox, ScreenshotBox, HTMLtag, ComparisonBox + ScreenshotBox, HTMLtag, ComparisonBox }} bindings={bindings} jsx={layoutFrame} diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 37d561954..b3bfadf4f 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -238,20 +238,28 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } } - public static FloatDoc(topDocView: DocumentView, x: number, y: number) { + @undoBatch @action + public static FloatDoc(topDocView: DocumentView, x?: number, y?: number) { const topDoc = topDocView.props.Document; - const de = new DragManager.DocumentDragData([topDoc]); - de.dragDivName = topDocView.props.dragDivName; - de.moveDocument = topDocView.props.moveDocument; - setTimeout(() => { - const newDocView = DocumentManager.Instance.getDocumentView(topDoc); - if (newDocView) { - const contentDiv = newDocView.ContentDiv!; - const xf = contentDiv.getBoundingClientRect(); - DragManager.StartDocumentDrag([contentDiv], de, x, y, { offsetX: x - xf.left, offsetY: y - xf.top, hideSource: true }); + const container = topDocView.props.ContainingCollectionView; + if (container) { + SelectionManager.DeselectAll(); + if (topDoc.z && (x === undefined && y === undefined)) { + const spt = container.screenToLocalTransform().inverse().transformPoint(NumCast(topDoc.x), NumCast(topDoc.y)); + topDoc.z = 0; + topDoc.x = spt[0]; + topDoc.y = spt[1]; + topDocView.props.removeDocument?.(topDoc); + topDocView.props.addDocTab(topDoc, "inParent"); + } else { + const spt = topDocView.props.ScreenToLocalTransform().inverse().transformPoint(0, 0); + const fpt = container.screenToLocalTransform().transformPoint(x !== undefined ? x : spt[0], y !== undefined ? y : spt[1]); + topDoc.z = 1; + topDoc.x = fpt[0]; + topDoc.y = fpt[1]; } - }, 0); - UndoManager.RunInBatch(action(() => topDoc.z = topDoc.z ? 0 : 1), "float"); + setTimeout(() => SelectionManager.SelectDoc(DocumentManager.Instance.getDocumentView(topDoc, container)!, false), 0); + } } onKeyDown = (e: React.KeyboardEvent) => { @@ -849,6 +857,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu if (this.props.LayoutTemplateString?.includes("LinkAnchorBox")) return null; return (this.props.treeViewDoc && this.props.LayoutTemplateString) || // render nothing for: tree view anchor dots this.layoutDoc.presBox || // presentationbox nodes + this.rootDoc.type === DocumentType.LINK || this.props.dontRegisterView ? (null) : // view that are not registered DocUtils.FilterDocs(this.directLinks, this.props.docFilters(), []).filter(d => !d.hidden && this.isNonTemporalLink).map((d, i) => <DocumentView {...this.props} key={i + 1} diff --git a/src/client/views/nodes/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx index d4ab70200..be6292bb6 100644 --- a/src/client/views/nodes/LinkAnchorBox.tsx +++ b/src/client/views/nodes/LinkAnchorBox.tsx @@ -49,14 +49,13 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps, LinkAnch const bounds = cdiv.getBoundingClientRect(); const pt = Utils.getNearestPointInPerimeter(bounds.left, bounds.top, bounds.width, bounds.height, e.clientX, e.clientY); const separation = Math.sqrt((pt[0] - e.clientX) * (pt[0] - e.clientX) + (pt[1] - e.clientY) * (pt[1] - e.clientY)); - const dragdist = Math.sqrt((pt[0] - down[0]) * (pt[0] - down[0]) + (pt[1] - down[1]) * (pt[1] - down[1])); if (separation > 100) { const dragData = new DragManager.DocumentDragData([this.rootDoc]); dragData.dropAction = "alias"; dragData.removeDropProperties = ["anchor1_x", "anchor1_y", "anchor2_x", "anchor2_y", "isLinkButton"]; - DragManager.StartDocumentDrag([this._ref.current!], dragData, down[0], down[1]); + DragManager.StartDocumentDrag([this._ref.current!], dragData, pt[0], pt[1]); return true; - } else if (dragdist > separation) { + } else { this.rootDoc[this.fieldKey + "_x"] = (pt[0] - bounds.left) / bounds.width * 100; this.rootDoc[this.fieldKey + "_y"] = (pt[1] - bounds.top) / bounds.height * 100; } diff --git a/src/client/views/nodes/TaskCompletedBox.tsx b/src/client/views/nodes/TaskCompletedBox.tsx index 89602f219..2a3dd8d2d 100644 --- a/src/client/views/nodes/TaskCompletedBox.tsx +++ b/src/client/views/nodes/TaskCompletedBox.tsx @@ -1,7 +1,5 @@ import React = require("react"); import { observer } from "mobx-react"; -import { documentSchema } from "../../../fields/documentSchemas"; -import { makeInterface } from "../../../fields/Schema"; import "./TaskCompletedBox.scss"; import { observable, action } from "mobx"; import { Fade } from "@material-ui/core"; diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index d30f1499e..646a94aa7 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -4,7 +4,7 @@ import { action, computed, IReactionDisposer, observable, reaction, runInAction import { observer } from "mobx-react"; import { Dictionary } from "typescript-collections"; import * as WebRequest from 'web-request'; -import { Doc, DocListCast, Opt } from "../../../fields/Doc"; +import { Doc, DocListCast, Opt, AclAddonly, AclEdit, AclAdmin } from "../../../fields/Doc"; import { documentSchema } from "../../../fields/documentSchemas"; import { Id } from "../../../fields/FieldSymbols"; import { HtmlField } from "../../../fields/HtmlField"; @@ -13,7 +13,7 @@ import { List } from "../../../fields/List"; import { listSpec, makeInterface } from "../../../fields/Schema"; import { Cast, NumCast, StrCast } from "../../../fields/Types"; import { WebField } from "../../../fields/URLField"; -import { TraceMobx } from "../../../fields/util"; +import { TraceMobx, GetEffectiveAcl } from "../../../fields/util"; import { addStyleSheet, clearStyleSheetRules, emptyFunction, returnOne, returnZero, Utils, returnTrue } from "../../../Utils"; import { Docs, DocUtils } from "../../documents/Documents"; import { DragManager } from "../../util/DragManager"; @@ -535,9 +535,10 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum @action highlight = (color: string) => { // creates annotation documents for current highlights - const annotationDoc = this.makeAnnotationDocument(color); - annotationDoc && Doc.AddDocToList(this.props.Document, this.annotationKey, annotationDoc); - return annotationDoc; + const effectiveAcl = GetEffectiveAcl(this.props.Document); + const annotationDoc = [AclAddonly, AclEdit, AclAdmin].includes(effectiveAcl) && this.makeAnnotationDocument(color); + annotationDoc && this.addDocument?.(annotationDoc); + return annotationDoc ?? undefined; } /** * This is temporary for creating annotations from highlights. It will diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx index 47a4911b8..7ccbfa051 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.tsx +++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx @@ -223,7 +223,7 @@ export default class RichTextMenu extends AntimodeMenu { if (this.view && this.TextView.props.isSelected(true)) { const path = (this.view.state.selection.$from as any).path; for (let i = path.length - 3; i < path.length && i >= 0; i -= 3) { - if (path[i]?.type === this.view.state.schema.nodes.paragraph) { + if (path[i]?.type === this.view.state.schema.nodes.paragraph || path[i]?.type === this.view.state.schema.nodes.heading) { return path[i].attrs.align || "left"; } } @@ -490,7 +490,7 @@ export default class RichTextMenu extends AntimodeMenu { alignParagraphs(state: EditorState<any>, align: "left" | "right" | "center", dispatch: any) { var tr = state.tr; state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos, parent, index) => { - if (node.type === schema.nodes.paragraph) { + if (node.type === schema.nodes.paragraph || node.type === schema.nodes.heading) { tr = tr.setNodeMarkup(pos, node.type, { ...node.attrs, align }, node.marks); return false; } @@ -503,7 +503,7 @@ export default class RichTextMenu extends AntimodeMenu { insetParagraph(state: EditorState<any>, dispatch: any) { var tr = state.tr; state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos, parent, index) => { - if (node.type === schema.nodes.paragraph) { + if (node.type === schema.nodes.paragraph || node.type === schema.nodes.heading) { const inset = (node.attrs.inset ? Number(node.attrs.inset) : 0) + 10; tr = tr.setNodeMarkup(pos, node.type, { ...node.attrs, inset }, node.marks); return false; @@ -516,7 +516,7 @@ export default class RichTextMenu extends AntimodeMenu { outsetParagraph(state: EditorState<any>, dispatch: any) { var tr = state.tr; state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos, parent, index) => { - if (node.type === schema.nodes.paragraph) { + if (node.type === schema.nodes.paragraph || node.type === schema.nodes.heading) { const inset = Math.max(0, (node.attrs.inset ? Number(node.attrs.inset) : 0) - 10); tr = tr.setNodeMarkup(pos, node.type, { ...node.attrs, inset }, node.marks); return false; @@ -529,8 +529,9 @@ export default class RichTextMenu extends AntimodeMenu { indentParagraph(state: EditorState<any>, dispatch: any) { var tr = state.tr; + let headin = false; state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos, parent, index) => { - if (node.type === schema.nodes.paragraph) { + if (node.type === schema.nodes.paragraph || node.type === schema.nodes.heading) { const nodeval = node.attrs.indent ? Number(node.attrs.indent) : undefined; const indent = !nodeval ? 25 : nodeval < 0 ? 0 : nodeval + 25; tr = tr.setNodeMarkup(pos, node.type, { ...node.attrs, indent }, node.marks); @@ -538,14 +539,14 @@ export default class RichTextMenu extends AntimodeMenu { } return true; }); - dispatch?.(tr); + !headin && dispatch?.(tr); return true; } hangingIndentParagraph(state: EditorState<any>, dispatch: any) { var tr = state.tr; state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos, parent, index) => { - if (node.type === schema.nodes.paragraph) { + if (node.type === schema.nodes.paragraph || node.type === schema.nodes.heading) { const nodeval = node.attrs.indent ? Number(node.attrs.indent) : undefined; const indent = !nodeval ? -25 : nodeval > 0 ? 0 : nodeval - 10; tr = tr.setNodeMarkup(pos, node.type, { ...node.attrs, indent }, node.marks); @@ -827,6 +828,7 @@ export default class RichTextMenu extends AntimodeMenu { } // TODO: should check for valid URL + @undoBatch makeLinkToURL = (target: string, lcoation: string) => { ((this.view as any)?.TextView as FormattedTextBox).makeLinkToSelection("", target, "onRight", "", target); } diff --git a/src/client/views/nodes/formattedText/nodes_rts.ts b/src/client/views/nodes/formattedText/nodes_rts.ts index 1af821738..0eca6d753 100644 --- a/src/client/views/nodes/formattedText/nodes_rts.ts +++ b/src/client/views/nodes/formattedText/nodes_rts.ts @@ -66,9 +66,11 @@ export const nodes: { [index: string]: NodeSpec } = { // should hold the number 1 to 6. Parsed and serialized as `<h1>` to // `<h6>` elements. heading: { - attrs: { level: { default: 1 } }, - content: "inline*", - group: "block", + ...ParagraphNodeSpec, + attrs: { + ...ParagraphNodeSpec.attrs, + level: { default: 1 }, + }, defining: true, parseDOM: [{ tag: "h1", attrs: { level: 1 } }, { tag: "h2", attrs: { level: 2 } }, @@ -76,7 +78,18 @@ export const nodes: { [index: string]: NodeSpec } = { { tag: "h4", attrs: { level: 4 } }, { tag: "h5", attrs: { level: 5 } }, { tag: "h6", attrs: { level: 6 } }], - toDOM(node: any) { return ["h" + node.attrs.level, 0]; } + toDOM(node) { + var dom = toParagraphDOM(node) as any; + var level = node.attrs.level || 1; + dom[0] = 'h' + level; + return dom; + }, + getAttrs(dom: any) { + var attrs = getParagraphNodeAttrs(dom) as any; + var level = Number(dom.nodeName.substring(1)) || 1; + attrs.level = level; + return attrs; + } }, // :: NodeSpec A code listing. Disallows marks or non-text inline diff --git a/src/client/views/pdf/PDFMenu.tsx b/src/client/views/pdf/PDFMenu.tsx index c3e1ae22f..7bea8d01b 100644 --- a/src/client/views/pdf/PDFMenu.tsx +++ b/src/client/views/pdf/PDFMenu.tsx @@ -93,7 +93,7 @@ export default class PDFMenu extends AntimodeMenu { @computed get highlighter() { const button = - <button className="antimodeMenu-button color-preview-button" title="" key="highilghter-button" onPointerDown={this.highlightClicked}> + <button className="antimodeMenu-button color-preview-button" title="" key="highlighter-button" onPointerDown={this.highlightClicked}> <FontAwesomeIcon icon="highlighter" size="lg" style={{ transition: "transform 0.1s", transform: this.Highlighting ? "" : "rotate(-45deg)" }} /> <div className="color-preview" style={{ backgroundColor: this.highlightColor }}></div> </button>; diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index d1010de48..192a6300a 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -4,7 +4,7 @@ const pdfjs = require('pdfjs-dist/es5/build/pdf.js'); import * as Pdfjs from "pdfjs-dist"; import "pdfjs-dist/web/pdf_viewer.css"; import { Dictionary } from "typescript-collections"; -import { Doc, DocListCast, FieldResult, HeightSym, Opt, WidthSym } from "../../../fields/Doc"; +import { Doc, DocListCast, FieldResult, HeightSym, Opt, WidthSym, AclAddonly, AclEdit, AclAdmin } from "../../../fields/Doc"; import { documentSchema } from "../../../fields/documentSchemas"; import { Id } from "../../../fields/FieldSymbols"; import { InkTool } from "../../../fields/InkField"; @@ -13,7 +13,7 @@ import { createSchema, makeInterface, listSpec } from "../../../fields/Schema"; import { ScriptField, ComputedField } from "../../../fields/ScriptField"; import { Cast, NumCast } from "../../../fields/Types"; import { PdfField } from "../../../fields/URLField"; -import { TraceMobx } from "../../../fields/util"; +import { TraceMobx, GetEffectiveAcl } from "../../../fields/util"; import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, emptyPath, intersectRect, returnZero, smoothScroll, Utils } from "../../../Utils"; import { Docs, DocUtils } from "../../documents/Documents"; import { DocumentType } from "../../documents/DocumentTypes"; @@ -570,9 +570,10 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu @action highlight = (color: string) => { // creates annotation documents for current highlights - const annotationDoc = this.makeAnnotationDocument(color); - annotationDoc && this.props.addDocument?.(annotationDoc); - return annotationDoc; + const effectiveAcl = GetEffectiveAcl(this.props.Document); + const annotationDoc = [AclAddonly, AclEdit, AclAdmin].includes(effectiveAcl) && this.makeAnnotationDocument(color); + annotationDoc && this.addDocument?.(annotationDoc); + return annotationDoc as Doc ?? undefined; } /** diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index d2237380e..43e74ff61 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -508,6 +508,10 @@ export namespace Doc { alias.aliasOf = doc; alias.title = ComputedField.MakeFunction(`renameAlias(this, ${Doc.GetProto(doc).aliasNumber = NumCast(Doc.GetProto(doc).aliasNumber) + 1})`); alias.author = Doc.CurrentUserEmail; + + if (!doc.aliases) doc.aliases = new List<Doc>([alias]); + else Doc.AddDocToList(doc, "aliases", alias); + return alias; } diff --git a/src/fields/util.ts b/src/fields/util.ts index a62795e64..957b2c8cd 100644 --- a/src/fields/util.ts +++ b/src/fields/util.ts @@ -1,5 +1,5 @@ import { UndoManager } from "../client/util/UndoManager"; -import { Doc, FieldResult, UpdatingFromServer, LayoutSym, AclPrivate, AclEdit, AclReadonly, AclAddonly, AclSym, CachedUpdates, DataSym, DocListCast, AclAdmin, FieldsSym, HeightSym, WidthSym } from "./Doc"; +import { Doc, FieldResult, UpdatingFromServer, LayoutSym, AclPrivate, AclEdit, AclReadonly, AclAddonly, AclSym, CachedUpdates, DataSym, DocListCast, AclAdmin, FieldsSym, HeightSym, WidthSym, fetchProto } from "./Doc"; import { SerializationHelper } from "../client/util/SerializationHelper"; import { ProxyField, PrefetchProxy } from "./Proxy"; import { RefField } from "./RefField"; @@ -10,6 +10,7 @@ import { DocServer } from "../client/DocServer"; import { ComputedField } from "./ScriptField"; import { ScriptCast, StrCast } from "./Types"; import { returnZero } from "../Utils"; +import { addSyntheticLeadingComment } from "typescript"; function _readOnlySetter(): never { @@ -74,7 +75,7 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number const fromServer = target[UpdatingFromServer]; const sameAuthor = fromServer || (receiver.author === Doc.CurrentUserEmail); const writeToDoc = sameAuthor || effectiveAcl === AclEdit || effectiveAcl === AclAdmin || (writeMode !== DocServer.WriteMode.LiveReadonly); - const writeToServer = (sameAuthor || effectiveAcl === AclEdit || effectiveAcl === AclAdmin || writeMode === DocServer.WriteMode.Default) && !playgroundMode; + const writeToServer = (sameAuthor || effectiveAcl === AclEdit || effectiveAcl === AclAdmin || writeMode === DocServer.WriteMode.Default) && !DocServer.Control.isReadOnly();// && !playgroundMode; if (writeToDoc) { if (value === undefined) { @@ -115,22 +116,34 @@ export function OVERRIDE_ACL(val: boolean) { _overrideAcl = val; } -let playgroundMode = false; +// playground mode allows the user to add/delete documents or make layout changes without them saving to the server +// let playgroundMode = false; -export function togglePlaygroundMode() { - playgroundMode = !playgroundMode; -} - -export function getPlaygroundMode() { - return playgroundMode; -} +// export function togglePlaygroundMode() { +// 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. + * + * Admin: a user with admin access to a document can remove/edit that document, add/remove/edit annotations (depending on permissions), as well as change others' access rights to that document. + * + * 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. + * + * View: a user with view access to a document can only view it - they cannot add/remove/edit anything. + * + * None: the document is not shared with that user. + */ export enum SharingPermissions { Admin = "Admin", Edit = "Can Edit", @@ -139,18 +152,23 @@ export enum SharingPermissions { None = "Not Shared" } +/** + * Calculates the effective access right to a document for the current user. + */ export function GetEffectiveAcl(target: any, in_prop?: string | symbol | number): symbol { + if (!target) return AclPrivate; if (in_prop === UpdatingFromServer || target[UpdatingFromServer]) return AclAdmin; if (target[AclSym] && Object.keys(target[AclSym]).length) { - if (target.__fields?.author === Doc.CurrentUserEmail || target.author === Doc.CurrentUserEmail || currentUserGroups.includes("admin")) return AclAdmin; + // if the current user is the author of the document / the current user is a member of the admin group + // but not if the doc in question is an alias - the current user will be the author of their alias rather than the original author + if ((Doc.CurrentUserEmail === (target.__fields?.author || target.author) && !(target.aliasOf || target.__fields?.aliasOf)) || currentUserGroups.includes("admin")) return AclAdmin; + // 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; let effectiveAcl = AclPrivate; - let aclPresent = false; - const HierarchyMapping = new Map<symbol, number>([ [AclPrivate, 0], [AclReadonly, 1], @@ -160,19 +178,28 @@ export function GetEffectiveAcl(target: any, in_prop?: string | symbol | number) ]); for (const [key, value] of Object.entries(target[AclSym])) { + // 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. if (currentUserGroups.includes(key.substring(4)) || Doc.CurrentUserEmail === key.substring(4).replace("_", ".")) { - if (HierarchyMapping.get(value as symbol)! >= HierarchyMapping.get(effectiveAcl)!) { - aclPresent = true; + if (HierarchyMapping.get(value as symbol)! > HierarchyMapping.get(effectiveAcl)!) { effectiveAcl = value as symbol; - if (effectiveAcl === AclEdit) break; + if (effectiveAcl === AclAdmin) break; } } } - return aclPresent ? effectiveAcl : AclEdit; + // 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; } return AclAdmin; } - +/** + * Recursively distributes the access right for a user across the children of a document and its annotations. + * @param key the key storing the access right (e.g. ACL-groupname) + * @param acl the access right being stored (e.g. "Can Edit") + * @param target the document on which this access right is being set + * @param inheritingFromCollection whether the target is being assigned rights after being dragged into a collection (and so is inheriting the ACLs from the collection) + * inheritingFromCollection is not currently being used but could be used if ACL assignment defaults change + */ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc, inheritingFromCollection?: boolean) { const HierarchyMapping = new Map<string, number>([ @@ -183,37 +210,51 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc ["Admin", 4] ]); + let changed = false; // determines whether fetchProto should be called or not (i.e. is there a change that should be reflected in target[AclSym]) const dataDoc = target[DataSym]; - if (!inheritingFromCollection || !target[key] || HierarchyMapping.get(StrCast(target[key]))! > HierarchyMapping.get(acl)!) target[key] = acl; + // if it is inheriting from a collection, it only inherits if A) the key doesn't already exist or B) the right being inherited is more restrictive + if (!inheritingFromCollection || !target[key] || HierarchyMapping.get(StrCast(target[key]))! > HierarchyMapping.get(acl)!) { + target[key] = acl; + changed = true; + + // maps over the aliases of the document + if (target.aliases) { + DocListCast(target.aliases).map(alias => { + distributeAcls(key, acl, alias); + }); + } + + } if (dataDoc && (!inheritingFromCollection || !dataDoc[key] || HierarchyMapping.get(StrCast(dataDoc[key]))! > HierarchyMapping.get(acl)!)) { dataDoc[key] = acl; + changed = true; + // maps over the children of the document DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc)]).map(d => { if (d.author === Doc.CurrentUserEmail && (!inheritingFromCollection || !d[key] || HierarchyMapping.get(StrCast(d[key]))! > HierarchyMapping.get(acl)!)) { - distributeAcls(key, acl, d); - d[key] = acl; + distributeAcls(key, acl, d, inheritingFromCollection); } const data = d[DataSym]; if (data && data.author === Doc.CurrentUserEmail && (!inheritingFromCollection || !data[key] || HierarchyMapping.get(StrCast(data[key]))! > HierarchyMapping.get(acl)!)) { - distributeAcls(key, acl, data); - data[key] = acl; + distributeAcls(key, acl, data, inheritingFromCollection); } }); + // maps over the annotations of the document DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc) + "-annotations"]).map(d => { if (d.author === Doc.CurrentUserEmail && (!inheritingFromCollection || !d[key] || HierarchyMapping.get(StrCast(d[key]))! > HierarchyMapping.get(acl)!)) { - distributeAcls(key, acl, d); - d[key] = acl; + distributeAcls(key, acl, d, inheritingFromCollection); } const data = d[DataSym]; if (data && data.author === Doc.CurrentUserEmail && (!inheritingFromCollection || !data[key] || HierarchyMapping.get(StrCast(data[key]))! > HierarchyMapping.get(acl)!)) { - distributeAcls(key, acl, data); - data[key] = acl; + distributeAcls(key, acl, data, inheritingFromCollection); } }); } + + changed && fetchProto(target); // updates target[AclSym] when changes to acls have been made } const layoutProps = ["panX", "panY", "width", "height", "nativeWidth", "nativeHeight", "fitWidth", "fitToBox", @@ -223,6 +264,7 @@ export function setter(target: any, in_prop: string | symbol | number, value: an const effectiveAcl = GetEffectiveAcl(target, in_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].includes(value))) return true; // if (typeof prop === "string" && prop.startsWith("ACL") && !["Can Edit", "Can Add", "Can View", "Not Shared", undefined].includes(value)) return true; |