diff options
Diffstat (limited to 'src/client/util')
| -rw-r--r-- | src/client/util/GroupManager.scss | 1 | ||||
| -rw-r--r-- | src/client/util/GroupManager.tsx | 54 | ||||
| -rw-r--r-- | src/client/util/SettingsManager.scss | 249 | ||||
| -rw-r--r-- | src/client/util/SettingsManager.tsx | 191 | ||||
| -rw-r--r-- | src/client/util/SharingManager.scss | 1 | ||||
| -rw-r--r-- | src/client/util/SharingManager.tsx | 144 | ||||
| -rw-r--r-- | src/client/util/type_decls.d | 5 |
7 files changed, 469 insertions, 176 deletions
diff --git a/src/client/util/GroupManager.scss b/src/client/util/GroupManager.scss index 51e4fa9e2..9438bdd72 100644 --- a/src/client/util/GroupManager.scss +++ b/src/client/util/GroupManager.scss @@ -92,6 +92,7 @@ .sort-groups { text-align: left; margin-left: 5; + width: 50px; cursor: pointer; } diff --git a/src/client/util/GroupManager.tsx b/src/client/util/GroupManager.tsx index 229a846a9..277e96a89 100644 --- a/src/client/util/GroupManager.tsx +++ b/src/client/util/GroupManager.tsx @@ -18,7 +18,7 @@ import { setGroups } from "../../fields/util"; import { DocServer } from "../DocServer"; import { TaskCompletionBox } from "../views/nodes/TaskCompletedBox"; -library.add(fa.faPlus, fa.faTimes, fa.faInfoCircle); +library.add(fa.faPlus, fa.faTimes, fa.faInfoCircle, fa.faCaretUp, fa.faCaretRight, fa.faCaretDown); /** * Interface for options for the react-select component @@ -42,6 +42,7 @@ export default class GroupManager extends React.Component<{}> { private currentUserGroups: string[] = []; // the list of groups the current user is a member of @observable private buttonColour: "#979797" | "black" = "#979797"; @observable private groupSort: "ascending" | "descending" | "none" = "none"; + private populating: boolean = false; @@ -62,21 +63,24 @@ export default class GroupManager extends React.Component<{}> { * Fetches the list of users stored on the database. */ 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 => { - const userDocument = await DocServer.GetRefField(user.userDocumentId); - if (userDocument instanceof Doc) { - const notificationDoc = await Cast(userDocument["sidebar-sharing"], Doc); - runInAction(() => { - if (notificationDoc instanceof Doc) { - this.users.push(user.email); - } - }); - } - }); - return Promise.all(evaluating); + if (!this.populating) { + this.populating = true; + runInAction(() => this.users = []); + const userList = await RequestPromise.get(Utils.prepend("/getUsers")); + const raw = JSON.parse(userList) as User[]; + const evaluating = raw.map(async user => { + const userDocument = await DocServer.GetRefField(user.userDocumentId); + if (userDocument instanceof Doc) { + const notificationDoc = await Cast(userDocument["sidebar-sharing"], Doc); + runInAction(() => { + if (notificationDoc instanceof Doc) { + this.users.push(user.email); + } + }); + } + }); + return Promise.all(evaluating).then(() => this.populating = false); + } } /** @@ -88,7 +92,6 @@ export default class GroupManager extends React.Component<{}> { const members: string[] = JSON.parse(StrCast(group.members)); if (members.includes(Doc.CurrentUserEmail)) this.currentUserGroups.push(StrCast(group.groupName)); }); - setGroups(this.currentUserGroups); }); } @@ -120,6 +123,7 @@ export default class GroupManager extends React.Component<{}> { this.currentGroup = undefined; // this.users = []; this.createGroupModalOpen = false; + TaskCompletionBox.taskCompleted = false; } /** @@ -132,7 +136,6 @@ export default class GroupManager extends React.Component<{}> { /** * @returns a list of all group documents. */ - // private ? getAllGroups(): Doc[] { const groupDoc = this.GroupManagerDoc; return groupDoc ? DocListCast(groupDoc.data) : []; @@ -142,7 +145,6 @@ export default class GroupManager extends React.Component<{}> { * @returns a group document based on the group name. * @param groupName */ - // private? getGroup(groupName: string): Doc | undefined { const groupDoc = this.getAllGroups().find(group => group.groupName === groupName); return groupDoc; @@ -209,8 +211,6 @@ export default class GroupManager extends React.Component<{}> { deleteGroup(group: Doc): boolean { if (group) { if (this.GroupManagerDoc && this.hasEditAccess(group)) { - // TODO look at this later - // SharingManager.Instance.setInternalGroupSharing(group, "Not Shared"); Doc.RemoveDocFromList(this.GroupManagerDoc, "data", group); SharingManager.Instance.removeGroup(group); const members: string[] = JSON.parse(StrCast(group.members)); @@ -311,7 +311,9 @@ export default class GroupManager extends React.Component<{}> { <div className="group-create"> <div className="group-heading" style={{ marginBottom: 0 }}> <p><b>New Group</b></p> - <div className={"close-button"} onClick={action(() => this.createGroupModalOpen = false)}> + <div className={"close-button"} onClick={action(() => { + this.createGroupModalOpen = false; TaskCompletionBox.taskCompleted = false; + })}> <FontAwesomeIcon icon={fa.faTimes} color={"black"} size={"lg"} /> </div> </div> @@ -319,6 +321,7 @@ export default class GroupManager extends React.Component<{}> { className="group-input" ref={this.inputRef} onKeyDown={this.handleKeyDown} + autoFocus type="text" placeholder="Group name" onChange={action(() => this.buttonColour = this.inputRef.current?.value ? "black" : "#979797")} /> @@ -363,7 +366,7 @@ export default class GroupManager extends React.Component<{}> { interactive={true} contents={contents} dialogueBoxStyle={{ width: "90%", height: "70%" }} - closeOnExternalClick={action(() => this.createGroupModalOpen = false)} + closeOnExternalClick={action(() => { this.createGroupModalOpen = false; TaskCompletionBox.taskCompleted = false; })} /> ); } @@ -405,7 +408,10 @@ 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" ? <FontAwesomeIcon icon={fa.faCaretUp} size={"xs"} /> + : this.groupSort === "descending" ? <FontAwesomeIcon icon={fa.faCaretDown} size={"xs"} /> + : <FontAwesomeIcon icon={fa.faCaretRight} size={"xs"} /> + } </div> <div className="group-body"> {groups.map(group => diff --git a/src/client/util/SettingsManager.scss b/src/client/util/SettingsManager.scss index 6923fe879..41bce8a1b 100644 --- a/src/client/util/SettingsManager.scss +++ b/src/client/util/SettingsManager.scss @@ -1,7 +1,7 @@ @import "../views/globalCssVariables"; .settings-interface { - background-color: whitesmoke !important; + //background-color: whitesmoke !important; color: grey; width: 450px; height: 300px; @@ -22,85 +22,232 @@ } } -.settings-interface { +.settings-title { + font-size: 25px; + font-weight: bold; + padding-right: 10px; + color: black; +} + +.settings-username { + font-size: 14px; + padding-right: 15px; + color: black; + margin-top: 10px; +} + +.settings-section { display: flex; - flex-direction: column; + border-bottom: 1px solid grey; + padding-bottom: 8px; + padding-top: 6px; - button { - width: 100%; - align-self: center; - background: #252b33; - margin-top: 4px; + .settings-section-title { + font-size: 16; + font-weight: bold; + text-align: left; + color: black; + width: 80; + margin-right: 50px; + } + + &:last-child { + border-bottom: none; + } +} + + +.password-content { + display: flex; + + .password-content-inputs { + width: 100; + + .password-inputs { + border: none; + margin-bottom: 8px; + width: 180; + color: black; + border-radius: 5px; + } + } + + .password-content-buttons { + margin-left: 84px; + width: 100; + + .password-submit { + margin-left: 85px; + } + + .password-forgot { + margin-left: 65px; + margin-top: -20px; + white-space: nowrap; + } + } +} + +.accounts-content { + display: flex; +} + +.modes-content { + display: flex; + + .modes-select { + width: 170px; + margin-right: 65px; + color: black; + border-radius: 5px; &:hover { - background: $main-accent; + cursor: pointer; } } - .delete-button { - background: rgb(227, 86, 86); + .modes-playground { + display: flex; + + .playground-check { + margin-right: 5px; + + &:hover { + cursor: pointer; + } + } + + .playground-text { + color: black; + } } +} - .close-button { - position: absolute; - right: 1em; - top: 1em; +.colorFlyout { + margin-top: 2px; + margin-right: 25px; + + &:hover { cursor: pointer; } - .settings-heading { - letter-spacing: .5em; + .colorFlyout-button { + width: 20px; + height: 20px; + border: 0.5px solid black; + border-radius: 5px; } +} +.preferences-content { + display: flex; + margin-top: 4px; - .settings-body { + .preferences-color { display: flex; - justify-content: space-between; - margin-top: -10; - .settings-type { - display: flex; - flex-direction: column; - flex-basis: 45%; + .preferences-color-text { + color: black; + font-size: 11; + margin-top: 4; + margin-right: 4; + } + } + .preferences-font { + display: flex; + + .preferences-font-text { + color: black; + font-size: 11; + margin-top: 4; + margin-right: 4; } - .settings-content { - padding-left: 1em; - padding-right: 1em; - display: flex; - flex-direction: column; - flex-basis: 70%; - justify-content: space-around; - text-align: left; - - ::placeholder { - color: $intermediate-color; - } + .font-select { + width: 100px; + color: black; + font-size: 9; + margin-right: 6; + border-radius: 5px; - input { - border-radius: 5px; - border: none; - padding: 4px; - min-width: 100%; - margin: 2px 0; + &:hover { + cursor: pointer; } + } - .error-text { - color: #C40233; - } + .size-select { + width: 60px; + color: black; + font-size: 9; + border-radius: 5px; - .success-text { - color: #009F6B; + &:hover { + cursor: pointer; } + } + } +} - p { - padding: 0 0 .1em .2em; - } +.settings-interface { + display: flex; + flex-direction: column; + + button { + width: auto; + align-self: center; + background: #252b33; + margin-right: 15px; + //margin-top: 4px; + + &:hover { + background: $main-accent; } } + // .delete-button { + // background: rgb(227, 86, 86); + // } + + .close-button { + position: absolute; + right: 1em; + top: 1em; + cursor: pointer; + } + + .settings-content { + background: #e4e4e4; + border-radius: 6px; + padding: 10px; + width: 560px; + } + + .settings-top { + display: flex; + margin-bottom: 10px; + } + + + .error-text { + color: #C40233; + width: 300; + margin-left: -20; + font-size: 10; + margin-bottom: 4; + margin-top: -3; + } + + .success-text { + width: 300; + margin-left: -20; + font-size: 10; + margin-bottom: 4; + margin-top: -3; + color: #009F6B; + } + .focus-span { text-decoration: underline; } @@ -138,8 +285,6 @@ color: black; } - - } } diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx index 6276fae96..68ed32c0f 100644 --- a/src/client/util/SettingsManager.tsx +++ b/src/client/util/SettingsManager.tsx @@ -1,4 +1,4 @@ -import { observable, runInAction, action } from "mobx"; +import { observable, runInAction, action, computed } from "mobx"; import * as React from "react"; import MainViewModal from "../views/MainViewModal"; import { observer } from "mobx-react"; @@ -15,6 +15,12 @@ import GroupManager from "./GroupManager"; import HypothesisAuthenticationManager from "../apis/HypothesisAuthenticationManager"; import GoogleAuthenticationManager from "../apis/GoogleAuthenticationManager"; import { DocServer } from "../DocServer"; +import { BoolCast, StrCast, NumCast } from "../../fields/Types"; +import { undoBatch } from "./UndoManager"; +import { ColorState, SketchPicker } from "react-color"; +const higflyout = require("@hig/flyout"); +export const { anchorPoints } = higflyout; +export const Flyout = higflyout.default; library.add(fa.faTimes); @@ -33,6 +39,9 @@ export default class SettingsManager extends React.Component<{}> { private new_password_ref = React.createRef<HTMLInputElement>(); private new_confirm_ref = React.createRef<HTMLInputElement>(); + + @computed get backgroundColor() { return Doc.UserDoc().defaultColor; } + public open = action(() => { SelectionManager.DeselectAll(); this.isOpen = true; @@ -107,50 +116,151 @@ export default class SettingsManager extends React.Component<{}> { addStyleSheetRule(SettingsManager._settingsStyle, "lm_header", { background: "pink !important" }); } + @action + changeMode = (e: any) => { + if (e.currentTarget.value === "Novice") { + Doc.UserDoc().noviceMode = true; + } else { + Doc.UserDoc().noviceMode = false; + } + } + + @action + changeFontFamily = (e: any) => { + Doc.UserDoc().fontFamily = e.currentTarget.value; + } + + @action + changeFontSize = (e: any) => { + Doc.UserDoc().fontSize = e.currentTarget.value; + } + + @action @undoBatch + switchColor = (color: ColorState) => { + const val = String(color.hex); + Doc.UserDoc().defaultColor = val; + return true; + } + private get settingsInterface() { - return ( - <div className={"settings-interface"}> - <div className="settings-heading"> - <h1>settings</h1> - <div className={"close-button"} onClick={this.close}> - <FontAwesomeIcon icon={fa.faTimes} color="black" size={"lg"} /> - </div> - </div> - <div className="settings-body"> - <div className="settings-type"> - <button onClick={this.onClick} value="password">reset password</button> - <button onClick={this.noviceToggle} value="data">{`Set ${Doc.UserDoc().noviceMode ? "developer" : "novice"} mode`}</button> - <button onClick={this.togglePlaygroundMode}>{`${this.playgroundMode ? "Disable" : "Enable"} playground mode`}</button> - <button onClick={this.googleAuthorize} value="data">{`Link to Google`}</button> - <button onClick={this.hypothesisAuthorize} value="data">{`Link to Hypothes.is`}</button> - <button onClick={() => GroupManager.Instance.open()}>Manage groups</button> - <button onClick={() => window.location.assign(Utils.prepend("/logout"))}> - {CurrentUserUtils.GuestWorkspace ? "Exit" : "Log Out"} - </button> + + + const passwordContent = <div className="password-content"> + <div className="password-content-inputs"> + <input className="password-inputs" type="password" placeholder="current password" ref={this.curr_password_ref} /> + <input className="password-inputs" type="password" placeholder="new password" ref={this.new_password_ref} /> + <input className="password-inputs" type="password" placeholder="confirm new password" ref={this.new_confirm_ref} /> + </div> + <div className="password-content-buttons"> + {this.errorText ? <div className="error-text">{this.errorText}</div> : undefined} + {this.successText ? <div className="success-text">{this.successText}</div> : undefined} + <button className="password-submit" onClick={this.dispatchRequest}>submit</button> + <a className="password-forgot" style={{ marginLeft: 65, marginTop: -20 }} + href="/forgotPassword">forgot password?</a> + </div> + </div>; + + const modesContent = <div className="modes-content"> + <select className="modes-select" + onChange={e => this.changeMode(e)}> + <option key={"Novice"} value={"Novice"} selected={BoolCast(Doc.UserDoc().noviceMode)}> + Novice + </option> + <option key={"Developer"} value={"Developer"} selected={!BoolCast(Doc.UserDoc().noviceMode)}> + Developer + </option> + </select> + <div className="modes-playground"> + <input className="playground-check" type="checkbox" + checked={this.playgroundMode} + onChange={undoBatch(action(() => this.togglePlaygroundMode()))} + /><div className="playground-text">Playground Mode</div> + </div> + </div>; + + const accountsContent = <div className="accounts-content"> + <button onClick={this.googleAuthorize} value="data">{`Link to Google`}</button> + <button onClick={this.hypothesisAuthorize} value="data">{`Link to Hypothes.is`}</button> + <button onClick={() => GroupManager.Instance.open()}>Manage groups</button> + </div>; + + const colorBox = <SketchPicker onChange={this.switchColor} + presetColors={['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', + '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', + '#FFFFFF', '#f1efeb', 'transparent']} + color={StrCast(this.backgroundColor)} />; + + const colorFlyout = <div className="colorFlyout"> + <Flyout anchorPoint={anchorPoints.LEFT_TOP} + content={colorBox}> + <div> + <div className="colorFlyout-button" style={{ backgroundColor: StrCast(this.backgroundColor) }} + onPointerDown={e => e.stopPropagation()} > + <FontAwesomeIcon icon="palette" size="sm" color={StrCast(this.backgroundColor)} /> </div> - {this.settingsContent === "password" ? - <div className="settings-content"> - <input placeholder="current password" ref={this.curr_password_ref} /> - <input placeholder="new password" ref={this.new_password_ref} /> - <input placeholder="confirm new password" ref={this.new_confirm_ref} /> - {this.errorText ? <div className="error-text">{this.errorText}</div> : undefined} - {this.successText ? <div className="success-text">{this.successText}</div> : undefined} - <button onClick={this.dispatchRequest}>submit</button> - <a style={{ marginLeft: 65, marginTop: -20 }} href="/forgotPassword">forgot password?</a> - - </div> - : undefined} - {this.settingsContent === "data" ? - <div className="settings-content"> - <p>WARNING: <br /> - THIS WILL ERASE ALL YOUR CURRENT DOCUMENTS STORED ON DASH. IF YOU WISH TO PROCEED, CLICK THE BUTTON BELOW.</p> - <button className="delete-button">DELETE</button> - </div> - : undefined} </div> + </Flyout> + </div>; + const fontFamilies: string[] = ["Times New Roman", "Arial", "Georgia", "Comic Sans MS", "Tahoma", "Impact", "Crimson Text"]; + const fontSizes: string[] = ["7pt", "8pt", "9pt", "10pt", "12pt", "14pt", "16pt", "18pt", "20pt", "24pt", "32pt", "48pt", "72pt"]; + + const preferencesContent = <div className="preferences-content"> + <div className="preferences-color"> + <div className="preferences-color-text">Background Color</div> {colorFlyout} </div> - ); + <div className="preferences-font"> + <div className="preferences-font-text">Default Font</div> + <select className="font-select" + onChange={e => this.changeFontFamily(e)}> + {fontFamilies.map((font) => { + return <option key={font} value={font} selected={StrCast(Doc.UserDoc().fontFamily) === font}> + {font} + </option>; + })} + </select> + <select className="size-select" + onChange={e => this.changeFontSize(e)}> + {fontSizes.map((size) => { + return <option key={size} value={size} selected={StrCast(Doc.UserDoc().fontSize) === size}> + {size} + </option>; + })} + </select> + </div> + </div>; + + return (<div className="settings-interface"> + <div className="settings-top"> + <div className="settings-title">Settings</div> + <div className="settings-username">{Doc.CurrentUserEmail}</div> + <button onClick={() => window.location.assign(Utils.prepend("/logout"))} + style={{ right: 35, position: "absolute" }} > + {CurrentUserUtils.GuestWorkspace ? "Exit" : "Log Out"} + </button> + <div className="close-button" onClick={this.close}> + <FontAwesomeIcon icon={fa.faTimes} color="black" size={"lg"} /> + </div> + </div> + <div className="settings-content"> + <div className="settings-section"> + <div className="settings-section-title">Password</div> + <div className="settings-section-context">{passwordContent}</div> + </div> + <div className="settings-section"> + <div className="settings-section-title">Modes</div> + <div className="settings-section-context">{modesContent}</div> + </div> + <div className="settings-section"> + <div className="settings-section-title">Accounts</div> + <div className="settings-section-context">{accountsContent}</div> + </div> + <div className="settings-section" style={{ paddingBottom: 4 }}> + <div className="settings-section-title">Preferences</div> + <div className="settings-section-context">{preferencesContent}</div> + </div> + </div> + </div>); } render() { @@ -160,6 +270,7 @@ export default class SettingsManager extends React.Component<{}> { isDisplayed={this.isOpen} interactive={true} closeOnExternalClick={this.close} + dialogueBoxStyle={{ width: "600px", height: "340px" }} /> ); } diff --git a/src/client/util/SharingManager.scss b/src/client/util/SharingManager.scss index 8da80ef52..7912db74d 100644 --- a/src/client/util/SharingManager.scss +++ b/src/client/util/SharingManager.scss @@ -84,6 +84,7 @@ .user-sort { text-align: left; margin-left: 10; + width: 100px; cursor: pointer; } diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx index 9d91ce1ba..d50a132f8 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, AclAdmin, DataSym, AclPrivate } from "../../fields/Doc"; +import { Doc, Opt, AclAdmin, AclPrivate, DocListCast } from "../../fields/Doc"; import { DocServer } from "../DocServer"; import { Cast, StrCast } from "../../fields/Types"; import * as RequestPromise from "request-promise"; @@ -21,6 +21,10 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { List } from "../../fields/List"; import { distributeAcls, SharingPermissions, GetEffectiveAcl } from "../../fields/util"; import { TaskCompletionBox } from "../views/nodes/TaskCompletedBox"; +import { library } from "@fortawesome/fontawesome-svg-core"; + +library.add(fa.faInfoCircle, fa.faCaretUp, fa.faCaretRight, fa.faCaretDown); + export interface User { email: string; @@ -71,6 +75,7 @@ export default class SharingManager extends React.Component<{}> { // if both showUserOptions and showGroupOptions are false then both are displayed @observable private showUserOptions: boolean = false; // whether to show individuals as options when sharing (in the react-select component) @observable private showGroupOptions: boolean = false; // // whether to show groups as options when sharing (in the react-select component) + private populating: boolean = false; // private get linkVisible() { // return this.sharingDoc ? this.sharingDoc[PublicKey] !== SharingPermissions.None : false; @@ -87,13 +92,13 @@ export default class SharingManager extends React.Component<{}> { this.isOpen = true; this.permissions = SharingPermissions.Edit; }); - + this.targetDoc!.author === Doc.CurrentUserEmail && !this.targetDoc![`ACL-${Doc.CurrentUserEmail.replace(".", "_")}`] && distributeAcls(`ACL-${Doc.CurrentUserEmail.replace(".", "_")}`, SharingPermissions.Admin, this.targetDoc!); } public close = action(() => { this.isOpen = false; this.selectedUsers = null; // resets the list of users and seleected users (in the react-select component) - + TaskCompletionBox.taskCompleted = false; setTimeout(action(() => { // this.copied = false; DictationOverlay.Instance.hasActiveModal = false; @@ -117,24 +122,27 @@ export default class SharingManager extends React.Component<{}> { * Populates the list of validated users (this.users) by adding registered users which have a sidebar-sharing. */ 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 => { - const isCandidate = user.email !== Doc.CurrentUserEmail; - if (isCandidate) { - const userDocument = await DocServer.GetRefField(user.userDocumentId); - if (userDocument instanceof Doc) { - const notificationDoc = await Cast(userDocument["sidebar-sharing"], Doc); - runInAction(() => { - if (notificationDoc instanceof Doc) { - this.users.push({ user, notificationDoc }); - } - }); + 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 isCandidate = user.email !== Doc.CurrentUserEmail; + if (isCandidate) { + const userDocument = await DocServer.GetRefField(user.userDocumentId); + if (userDocument instanceof Doc) { + const notificationDoc = await Cast(userDocument["sidebar-sharing"], Doc); + runInAction(() => { + if (notificationDoc instanceof Doc) { + this.users.push({ user, notificationDoc }); + } + }); + } } - } - }); - return Promise.all(evaluating); + }); + return Promise.all(evaluating).then(() => this.populating = false); + } } /** @@ -152,13 +160,11 @@ export default class SharingManager extends React.Component<{}> { 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]); + group.docsShared ? Doc.IndexOf(target, DocListCast(group.docsShared)) === -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); // 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 - }); + if (permission !== SharingPermissions.None) Doc.IndexOf(target, DocListCast(notificationDoc[storage])) === -1 && Doc.AddDocToList(notificationDoc, storage, target); // add the target to the notificationDoc if it hasn't already been added + else Doc.IndexOf(target, DocListCast(notificationDoc[storage])) !== -1 && Doc.RemoveDocFromList(notificationDoc, storage, target); // remove the target from the list if it already exists }); } @@ -170,13 +176,7 @@ export default class SharingManager extends React.Component<{}> { 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)); // add the doc if it isn't already in the list - }); - }); - } + if (group.docsShared) DocListCast(group.docsShared).forEach(doc => Doc.IndexOf(doc, DocListCast(user.notificationDoc[storage])) === -1 && Doc.AddDocToList(user.notificationDoc, storage, doc)); } shareFromPropertiesSidebar = (shareWith: string, permission: SharingPermissions, target: Doc) => { @@ -194,10 +194,8 @@ export default class SharingManager extends React.Component<{}> { 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)); // remove the doc only if it is in the list - }); + DocListCast(group.docsShared).forEach(doc => { + Doc.IndexOf(doc, DocListCast(user.notificationDoc[storage])) !== -1 && Doc.RemoveDocFromList(user.notificationDoc, storage, doc); // remove the doc only if it is in the list }); } } @@ -208,39 +206,36 @@ export default class SharingManager extends React.Component<{}> { */ removeGroup = (group: Doc) => { if (group.docsShared) { - DocListCastAsync(group.docsShared).then(resolved => { - resolved?.forEach(doc => { - const ACL = `ACL-${StrCast(group.groupName)}`; + DocListCast(group.docsShared).forEach(doc => { + const ACL = `ACL-${StrCast(group.groupName)}`; - distributeAcls(ACL, SharingPermissions.None, doc); + distributeAcls(ACL, SharingPermissions.None, doc); - const members: string[] = JSON.parse(StrCast(group.members)); - const users: ValidatedUser[] = this.users.filter(({ user: { email } }) => members.includes(email)); - - users.forEach(({ notificationDoc }) => Doc.RemoveDocFromList(notificationDoc, storage, doc)); - }); + const members: string[] = JSON.parse(StrCast(group.members)); + const users: ValidatedUser[] = this.users.filter(({ user: { email } }) => members.includes(email)); + users.forEach(({ notificationDoc }) => Doc.RemoveDocFromList(notificationDoc, storage, doc)); }); } } + /** + * Shares the document with a user. + */ setInternalSharing = (recipient: ValidatedUser, permission: string, targetDoc?: Doc) => { const { user, notificationDoc } = recipient; const target = targetDoc || this.targetDoc!; const key = user.email.replace('.', '_'); const ACL = `ACL-${key}`; + target.author === Doc.CurrentUserEmail && distributeAcls(ACL, permission as SharingPermissions, target); if (permission !== SharingPermissions.None) { - DocListCastAsync(notificationDoc[storage]).then(resolved => { - Doc.IndexOf(target, resolved!) === -1 && Doc.AddDocToList(notificationDoc, storage, target); - }); + Doc.IndexOf(target, DocListCast(notificationDoc[storage])) === -1 && Doc.AddDocToList(notificationDoc, storage, target); } else { - DocListCastAsync(notificationDoc[storage]).then(resolved => { - Doc.IndexOf(target, resolved!) !== -1 && Doc.RemoveDocFromList(notificationDoc, storage, target); - }); + Doc.IndexOf(target, DocListCast(notificationDoc[storage])) !== -1 && Doc.RemoveDocFromList(notificationDoc, storage, target); } } @@ -268,14 +263,17 @@ export default class SharingManager extends React.Component<{}> { // } // }); + /** + * Returns the SharingPermissions (Admin, Can Edit etc) access that's used to share + */ private get sharingOptions() { - return Object.values(SharingPermissions).map(permission => { - return ( - <option key={permission} value={permission} selected={permission === SharingPermissions.Edit}> + return Object.values(SharingPermissions).map(permission => + ( + <option key={permission} value={permission}> {permission} </option> - ); - }); + ) + ); } private focusOn = (contents: string) => { @@ -308,16 +306,25 @@ export default class SharingManager extends React.Component<{}> { ); } + /** + * Handles changes in the users selected in react-select + */ @action handleUsersChange = (selectedOptions: any) => { this.selectedUsers = selectedOptions as UserOptions[]; } + /** + * Handles changes in the permission chosen to share with someone with + */ @action handlePermissionsChange = (event: React.ChangeEvent<HTMLSelectElement>) => { this.permissions = event.currentTarget.value as SharingPermissions; } + /** + * Calls the relevant method for sharing, displays the popup, and resets the relevant variables. + */ @action share = () => { if (this.selectedUsers) { @@ -341,18 +348,27 @@ export default class SharingManager extends React.Component<{}> { } } + /** + * Sorting algorithm to sort users. + */ sortUsers = (u1: ValidatedUser, u2: ValidatedUser) => { const { email: e1 } = u1.user; const { email: e2 } = u2.user; return e1 < e2 ? -1 : e1 === e2 ? 0 : 1; } + /** + * Sorting algorithm to sort groups. + */ sortGroups = (group1: Doc, group2: Doc) => { const g1 = StrCast(group1.groupName); const g2 = StrCast(group2.groupName); return g1 < g2 ? -1 : g1 === g2 ? 0 : 1; } + /** + * @returns the main interface of the SharingManager. + */ private get sharingInterface() { const groupList = GroupManager.Instance?.getAllGroups() || []; @@ -361,8 +377,8 @@ export default class SharingManager extends React.Component<{}> { const sortedGroups = groupList.slice().sort(this.sortGroups) .map(({ groupName }) => ({ label: StrCast(groupName), value: groupType + StrCast(groupName) })); + // the next block handles the users shown (individuals/groups/both) const options: GroupedOptions[] = []; - if (GroupManager.Instance) { if ((this.showUserOptions && this.showGroupOptions) || (!this.showUserOptions && !this.showGroupOptions)) { options.push({ @@ -393,6 +409,7 @@ export default class SharingManager extends React.Component<{}> { const effectiveAcl = this.targetDoc ? GetEffectiveAcl(this.targetDoc) : AclPrivate; + // the list of users shared with const userListContents: (JSX.Element | null)[] = users.map(({ user, notificationDoc }) => { const userKey = user.email.replace('.', '_'); const permissions = StrCast(this.targetDoc?.[`ACL-${userKey}`]); @@ -422,6 +439,7 @@ export default class SharingManager extends React.Component<{}> { ); }); + // the owner of the doc and the current user are placed at the top of the user list. userListContents.unshift( ( <div @@ -452,6 +470,7 @@ export default class SharingManager extends React.Component<{}> { ) : null ); + // the list of groups shared with const groupListContents = groups.map(group => { const permissions = StrCast(this.targetDoc?.[`ACL-${StrCast(group.groupName)}`]); @@ -477,6 +496,7 @@ export default class SharingManager extends React.Component<{}> { ); }); + // don't display the group list if all groups are null const displayGroupList = !groupListContents?.every(group => group === null); return ( @@ -538,7 +558,7 @@ export default class SharingManager extends React.Component<{}> { }) }} /> - <select className="permissions-select" onChange={this.handlePermissionsChange}> + <select className="permissions-select" onChange={this.handlePermissionsChange} value={this.permissions}> {this.sharingOptions} </select> <button ref={this.shareDocumentButtonRef} className="share-button" onClick={this.share}> @@ -556,7 +576,9 @@ export default class SharingManager extends React.Component<{}> { <div className="user-sort" onClick={action(() => this.individualSort = this.individualSort === "ascending" ? "descending" : this.individualSort === "descending" ? "none" : "ascending")}> - Individuals {this.individualSort === "ascending" ? "↑" : this.individualSort === "descending" ? "↓" : ""} {/* → */} + Individuals {this.individualSort === "ascending" ? <FontAwesomeIcon icon={fa.faCaretUp} size={"xs"} /> + : this.individualSort === "descending" ? <FontAwesomeIcon icon={fa.faCaretDown} size={"xs"} /> + : <FontAwesomeIcon icon={fa.faCaretRight} size={"xs"} />} </div> <div className={"users-list"} style={{ display: "block" }}>{/*200*/} {userListContents} @@ -566,7 +588,9 @@ export default class SharingManager extends React.Component<{}> { <div className="user-sort" onClick={action(() => this.groupSort = this.groupSort === "ascending" ? "descending" : this.groupSort === "descending" ? "none" : "ascending")}> - Groups {this.groupSort === "ascending" ? "↑" : this.groupSort === "descending" ? "↓" : ""} {/* → */} + Groups {this.groupSort === "ascending" ? <FontAwesomeIcon icon={fa.faCaretUp} size={"xs"} /> + : this.groupSort === "descending" ? <FontAwesomeIcon icon={fa.faCaretDown} size={"xs"} /> + : <FontAwesomeIcon icon={fa.faCaretRight} size={"xs"} />} </div> <div className={"groups-list"} style={{ display: !displayGroupList ? "flex" : "block" }}>{/*200*/} diff --git a/src/client/util/type_decls.d b/src/client/util/type_decls.d index 08aec3724..ab6c94f83 100644 --- a/src/client/util/type_decls.d +++ b/src/client/util/type_decls.d @@ -187,6 +187,11 @@ declare class List<T extends Field> extends ObjectField { [Copy](): ObjectField; } +declare class InkField extends ObjectField { + constructor(data:Array<{X:number, Y:number}>); + [Copy](): ObjectField; +} + // @ts-ignore declare const console: any; |
