diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/client/DocServer.ts | 2 | ||||
-rw-r--r-- | src/client/apis/GoogleAuthenticationManager.tsx | 1 | ||||
-rw-r--r-- | src/client/util/GroupManager.tsx | 140 | ||||
-rw-r--r-- | src/client/util/GroupMemberView.tsx | 12 | ||||
-rw-r--r-- | src/client/util/SettingsManager.tsx | 1 | ||||
-rw-r--r-- | src/client/util/SharingManager.scss | 174 | ||||
-rw-r--r-- | src/client/util/SharingManager.tsx | 472 | ||||
-rw-r--r-- | src/client/views/DictationOverlay.tsx | 1 | ||||
-rw-r--r-- | src/client/views/MainViewModal.scss | 5 | ||||
-rw-r--r-- | src/client/views/MainViewModal.tsx | 5 | ||||
-rw-r--r-- | src/client/views/nodes/DocumentView.tsx | 3 | ||||
-rw-r--r-- | src/fields/Doc.ts | 8 | ||||
-rw-r--r-- | src/fields/util.ts | 2 | ||||
-rw-r--r-- | src/mobile/AudioUpload.tsx | 1 | ||||
-rw-r--r-- | src/mobile/ImageUpload.tsx | 1 |
15 files changed, 548 insertions, 280 deletions
diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts index 2a7a7c59a..eac53bb02 100644 --- a/src/client/DocServer.ts +++ b/src/client/DocServer.ts @@ -31,7 +31,7 @@ export namespace DocServer { export enum WriteMode { Default = 0, //Anything goes - Playground = 1, //Playground (write own/no read) + Playground = 1, //Playground (write own/no read other updates) LiveReadonly = 2,//Live Readonly (no write/read others) LivePlayground = 3,//Live Playground (write own/read others) } diff --git a/src/client/apis/GoogleAuthenticationManager.tsx b/src/client/apis/GoogleAuthenticationManager.tsx index bf4469aeb..5a2bdb13b 100644 --- a/src/client/apis/GoogleAuthenticationManager.tsx +++ b/src/client/apis/GoogleAuthenticationManager.tsx @@ -157,6 +157,7 @@ export default class GoogleAuthenticationManager extends React.Component<{}> { contents={this.renderPrompt} overlayDisplayedOpacity={0.9} dialogueBoxStyle={this.dialogueBoxStyle} + closeOnExternalClick={() => this.isOpen = false} /> ); } diff --git a/src/client/util/GroupManager.tsx b/src/client/util/GroupManager.tsx index 7c68fc2a0..b14dcf55b 100644 --- a/src/client/util/GroupManager.tsx +++ b/src/client/util/GroupManager.tsx @@ -3,7 +3,7 @@ import { observable, action, runInAction, computed } from "mobx"; import { SelectionManager } from "./SelectionManager"; import MainViewModal from "../views/MainViewModal"; import { observer } from "mobx-react"; -import { Doc, DocListCast, Opt } from "../../fields/Doc"; +import { Doc, DocListCast, Opt, DocListCastAsync } from "../../fields/Doc"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import * as fa from '@fortawesome/free-solid-svg-icons'; import { library } from "@fortawesome/fontawesome-svg-core"; @@ -33,6 +33,7 @@ export default class GroupManager extends React.Component<{}> { @observable private selectedUsers: UserOptions[] | null = null; // list of users selected in the "Select users" dropdown. @observable currentGroup: Opt<Doc>; // the currently selected group. private inputRef: React.RefObject<HTMLInputElement> = React.createRef(); // the ref for the input box. + private currentUserGroups: Doc[] = []; constructor(props: Readonly<{}>) { super(props); @@ -42,6 +43,26 @@ export default class GroupManager extends React.Component<{}> { // sets up the list of users componentDidMount() { this.populateUsers().then(resolved => runInAction(() => this.users = resolved)); + + // this.getAllGroups().forEach(group => { + // const members: string[] = JSON.parse(StrCast(group.members)); + // if (members.includes(Doc.CurrentUserEmail)) this.currentUserGroups.push(group); + // }); + DocListCastAsync(this.GroupManagerDoc?.data).then(groups => { + groups?.forEach(group => { + const members: string[] = JSON.parse(StrCast(group.members)); + if (members.includes(Doc.CurrentUserEmail)) this.currentUserGroups.push(group); + }); + }) + .finally(() => console.log(this.currentUserGroups)); + + // (this.GroupManagerDoc?.data as List<Doc>).forEach(group => { + // Promise.resolve(group).then(resolvedGroup => { + // const members: string[] = JSON.parse(StrCast(resolvedGroup.members)); + // if (members.includes(Doc.CurrentUserEmail)) this.currentUserGroups.push(resolvedGroup); + // }); + // }); + } /** @@ -49,8 +70,8 @@ export default class GroupManager extends React.Component<{}> { */ populateUsers = async () => { const userList: User[] = JSON.parse(await RequestPromise.get(Utils.prepend("/getUsers"))); - const currentUserIndex = userList.findIndex(user => user.email === Doc.CurrentUserEmail); - currentUserIndex !== -1 && userList.splice(currentUserIndex, 1); + // const currentUserIndex = userList.findIndex(user => user.email === Doc.CurrentUserEmail); + // currentUserIndex !== -1 && userList.splice(currentUserIndex, 1); return userList.map(user => user.email); } @@ -61,6 +82,11 @@ export default class GroupManager extends React.Component<{}> { return this.users.map(user => ({ label: user, value: user })); } + + get groupMemberships() { + return this.currentUserGroups; + } + /** * Makes the GroupManager visible. */ @@ -89,7 +115,8 @@ export default class GroupManager extends React.Component<{}> { /** * @returns a list of all group documents. */ - private getAllGroups(): Doc[] { + // private ? + getAllGroups(): Doc[] { const groupDoc = this.GroupManagerDoc; return groupDoc ? DocListCast(groupDoc.data) : []; } @@ -98,7 +125,8 @@ export default class GroupManager extends React.Component<{}> { * @returns a group document based on the group name. * @param groupName */ - private getGroup(groupName: string): Doc | undefined { + // private? + getGroup(groupName: string): Doc | undefined { const groupDoc = this.getAllGroups().find(group => group.groupName === groupName); return groupDoc; } @@ -172,8 +200,10 @@ 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.setInternalGroupSharing(group, "Not Shared"); + SharingManager.Instance.removeGroup(group); if (group === this.currentGroup) { runInAction(() => this.currentGroup = undefined); } @@ -193,6 +223,7 @@ export default class GroupManager extends React.Component<{}> { const memberList: string[] = JSON.parse(StrCast(groupDoc.members)); !memberList.includes(email) && memberList.push(email); groupDoc.members = JSON.stringify(memberList); + SharingManager.Instance.shareWithAddedMember(groupDoc, email); } } @@ -205,8 +236,11 @@ export default class GroupManager extends React.Component<{}> { if (this.hasEditAccess(groupDoc)) { const memberList: string[] = JSON.parse(StrCast(groupDoc.members)); const index = memberList.indexOf(email); - index !== -1 && memberList.splice(index, 1); - groupDoc.members = JSON.stringify(memberList); + if (index !== -1) { + const user = memberList.splice(index, 1)[0]; + groupDoc.members = JSON.stringify(memberList); + SharingManager.Instance.removeMember(groupDoc, email); + } } } @@ -248,48 +282,48 @@ export default class GroupManager extends React.Component<{}> { /** * A getter that @returns the interface rendered to view an individual group. */ - private get editingInterface() { - const members: string[] = this.currentGroup ? JSON.parse(StrCast(this.currentGroup.members)) : []; - const options: UserOptions[] = this.currentGroup ? this.options.filter(option => !(JSON.parse(StrCast(this.currentGroup!.members)) as string[]).includes(option.value)) : []; - return (!this.currentGroup ? null : - <div className="editing-interface"> - <div className="editing-header"> - <b>{this.currentGroup.groupName}</b> - <div className={"close-button"} onClick={action(() => this.currentGroup = undefined)}> - <FontAwesomeIcon icon={fa.faWindowClose} size={"lg"} /> - </div> + // private get editingInterface() { + // const members: string[] = this.currentGroup ? JSON.parse(StrCast(this.currentGroup.members)) : []; + // const options: UserOptions[] = this.currentGroup ? this.options.filter(option => !(JSON.parse(StrCast(this.currentGroup!.members)) as string[]).includes(option.value)) : []; + // return (!this.currentGroup ? null : + // <div className="editing-interface"> + // <div className="editing-header"> + // <b>{this.currentGroup.groupName}</b> + // <div className={"close-button"} onClick={action(() => this.currentGroup = undefined)}> + // <FontAwesomeIcon icon={fa.faWindowClose} size={"lg"} /> + // </div> - {this.hasEditAccess(this.currentGroup) ? - <div className="group-buttons"> - <div className="add-member-dropdown"> - <Select - // isMulti={true} - isSearchable={true} - options={options} - onChange={selectedOption => this.addMemberToGroup(this.currentGroup!, (selectedOption as UserOptions).value)} - placeholder={"Add members"} - value={null} - closeMenuOnSelect={true} - /> - </div> - <button onClick={() => this.deleteGroup(this.currentGroup!)}>Delete group</button> - </div> : - null} - </div> - <div className="editing-contents"> - {members.map(member => ( - <div className="editing-row"> - <div className="user-email"> - {member} - </div> - {this.hasEditAccess(this.currentGroup!) ? <button onClick={() => this.removeMemberFromGroup(this.currentGroup!, member)}> Remove </button> : null} - </div> - ))} - </div> - </div> - ); + // {this.hasEditAccess(this.currentGroup) ? + // <div className="group-buttons"> + // <div className="add-member-dropdown"> + // <Select + // // isMulti={true} + // isSearchable={true} + // options={options} + // onChange={selectedOption => this.addMemberToGroup(this.currentGroup!, (selectedOption as UserOptions).value)} + // placeholder={"Add members"} + // value={null} + // closeMenuOnSelect={true} + // /> + // </div> + // <button onClick={() => this.deleteGroup(this.currentGroup!)}>Delete group</button> + // </div> : + // null} + // </div> + // <div className="editing-contents"> + // {members.map(member => ( + // <div className="editing-row"> + // <div className="user-email"> + // {member} + // </div> + // {this.hasEditAccess(this.currentGroup!) ? <button onClick={() => this.removeMemberFromGroup(this.currentGroup!, member)}> Remove </button> : null} + // </div> + // ))} + // </div> + // </div> + // ); - } + // } /** * A getter that @returns the main interface for the GroupManager. @@ -307,7 +341,7 @@ export default class GroupManager extends React.Component<{}> { {this.currentGroup ? <GroupMemberView group={this.currentGroup} - onCloseButtonClick={() => this.currentGroup = undefined} + onCloseButtonClick={action(() => this.currentGroup = undefined)} /> : null} <div className="group-heading"> @@ -328,6 +362,13 @@ export default class GroupManager extends React.Component<{}> { placeholder={"Select users"} value={this.selectedUsers} closeMenuOnSelect={false} + styles={{ + dropdownIndicator: (base, state) => ({ + ...base, + transition: '0.5s all ease', + transform: state.selectProps.menuIsOpen ? 'rotate(180deg)' : undefined + }) + }} /> </div> <div className="group-content"> @@ -353,6 +394,7 @@ export default class GroupManager extends React.Component<{}> { interactive={true} dialogueBoxDisplayedOpacity={this.dialogueBoxOpacity} overlayDisplayedOpacity={this.overlayOpacity} + closeOnExternalClick={this.close} /> ); } diff --git a/src/client/util/GroupMemberView.tsx b/src/client/util/GroupMemberView.tsx index b2d75158e..cc279b6b2 100644 --- a/src/client/util/GroupMemberView.tsx +++ b/src/client/util/GroupMemberView.tsx @@ -8,7 +8,7 @@ import { action } from "mobx"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import * as fa from '@fortawesome/free-solid-svg-icons'; import Select from "react-select"; -import { Doc, Opt } from "../../fields/Doc"; +import { Doc } from "../../fields/Doc"; import "./GroupMemberView.scss"; library.add(fa.faWindowClose); @@ -42,9 +42,16 @@ export default class GroupMemberView extends React.Component<GroupMemberViewProp placeholder={"Add members"} value={null} closeMenuOnSelect={true} + styles={{ + dropdownIndicator: (base, state) => ({ + ...base, + transition: '0.5s all ease', + transform: state.selectProps.menuIsOpen ? 'rotate(180deg)' : undefined + }) + }} /> </div> - <button onClick={() => GroupManager.Instance.deleteGroup(this.props.group)}>Delete group</button> + <button onClick={() => console.log(GroupManager.Instance.deleteGroup(this.props.group))}>Delete group</button> </div> : null} </div> @@ -68,6 +75,7 @@ export default class GroupMemberView extends React.Component<GroupMemberViewProp isDisplayed={true} interactive={true} contents={this.editingInterface} + closeOnExternalClick={this.props.onCloseButtonClick} />; } diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx index 9888cce48..fa2b20095 100644 --- a/src/client/util/SettingsManager.tsx +++ b/src/client/util/SettingsManager.tsx @@ -134,6 +134,7 @@ export default class SettingsManager extends React.Component<{}> { interactive={true} dialogueBoxDisplayedOpacity={this.dialogueBoxOpacity} overlayDisplayedOpacity={this.overlayOpacity} + closeOnExternalClick={this.close} /> ); } diff --git a/src/client/util/SharingManager.scss b/src/client/util/SharingManager.scss index fcbc05f8a..2708876a3 100644 --- a/src/client/util/SharingManager.scss +++ b/src/client/util/SharingManager.scss @@ -1,9 +1,10 @@ @import "../views/globalCssVariables"; .sharing-interface { - display: flex; - flex-direction: column; - width: 730px; + // display: flex; + // flex-direction: column; + width: 600px; + height: 360px; .dialogue-box { width: 450; @@ -14,8 +15,90 @@ transform: translate(-20px, -20px); } + select { + text-align: justify; + text-align-last: end + } + .sharing-contents { display: flex; + flex-direction: column; + + .close-button { + position: absolute; + right: 1em; + top: 1em; + cursor: pointer; + z-index: 999; + } + + .share-setup { + display: flex; + margin-bottom: 20px; + align-items: center; + height: 36; + + .user-search { + width: 90%; + } + + .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: #979797; + } + } + + .main-container { + display: flex; + + + .individual-container, + .group-container { + width: 50%; + + .share-title { + margin-top: 20px; + margin-bottom: 20px; + } + + .groups-list, + .users-list { + font-style: italic; + background: gainsboro; + // border: 1px solid black; + padding-left: 10px; + padding-right: 10px; + overflow-y: scroll; + overflow-x: hidden; + text-align: left; + display: flex; + align-content: center; + align-items: center; + text-align: center; + justify-content: center; + // color: red; + color: black; + height: 255px; + margin: 0 2; + + + .none { + font-style: italic; + + } + } + } + } button { background: $darker-alt-accent; @@ -31,37 +114,6 @@ transition: transform 0.2s; height: 25; } - - .individual-container, - .group-container { - width: 50%; - - .share-groups, - .share-individual { - margin-top: 20px; - margin-bottom: 20px; - } - - .groups-list, - .users-list { - font-style: italic; - background: white; - border: 1px solid black; - padding-left: 10px; - padding-right: 10px; - overflow-y: scroll; - overflow-x: hidden; - text-align: left; - display: flex; - align-content: center; - align-items: center; - text-align: center; - justify-content: center; - color: red; - height: 150px; - margin: 0 2; - } - } } .focus-span { @@ -69,11 +121,12 @@ } p { - font-size: 15px; + font-size: 20px; text-align: left; - font-style: italic; - padding: 0; + // font-style: italic; + // padding: 0; margin: 0 0 20px 0; + color: black; } .hr-substitute { @@ -108,30 +161,39 @@ -moz-user-select: none; -ms-user-select: none; user-select: none; - width: 700px; - min-width: 700px; - max-width: 700px; + width: 100%; + // min-width: 700px; + // max-width: 700px; text-align: left; font-style: normal; font-size: 14; font-weight: normal; padding: 0; - align-items: baseline; + align-items: center; + + &:hover .padding { + white-space: unset; + } .padding { padding: 0 10px 0 0; color: black; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + max-width: 48%; } .permissions-dropdown { - outline: none; + border: none; height: 25; + background: gainsboro; } .edit-actions { display: flex; position: absolute; - right: 51.5%; + right: -10; } } @@ -172,17 +234,17 @@ } } - .close-button { - border-radius: 5px; - margin-top: 20px; - padding: 10px 0; - background: aliceblue; - transition: 0.5s ease all; - border: 1px solid; - border-color: aliceblue; - } - - .close-button:hover { - border-color: black; - } + // .close-button { + // border-radius: 5px; + // margin-top: 20px; + // padding: 10px 0; + // background: aliceblue; + // transition: 0.5s ease all; + // border: 1px solid; + // border-color: aliceblue; + // } + + // .close-button:hover { + // border-color: black; + // } }
\ No newline at end of file diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx index 127ee33ce..e74824581 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, DocCastAsync } from "../../fields/Doc"; +import { Doc, Opt, DocCastAsync, DocListCast } from "../../fields/Doc"; import { DocServer } from "../DocServer"; import { Cast, StrCast } from "../../fields/Types"; import * as RequestPromise from "request-promise"; @@ -9,7 +9,6 @@ import { Utils } from "../../Utils"; import "./SharingManager.scss"; import { Id } from "../../fields/FieldSymbols"; import { observer } from "mobx-react"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { library } from '@fortawesome/fontawesome-svg-core'; import * as fa from '@fortawesome/free-solid-svg-icons'; import { DocumentView } from "../views/nodes/DocumentView"; @@ -17,8 +16,11 @@ import { SelectionManager } from "./SelectionManager"; import { DocumentManager } from "./DocumentManager"; import { CollectionView } from "../views/collections/CollectionView"; import { DictationOverlay } from "../views/DictationOverlay"; -import GroupManager from "./GroupManager"; +import GroupManager, { UserOptions } from "./GroupManager"; import GroupMemberView from "./GroupMemberView"; +import Select from "react-select"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { List } from "../../fields/List"; library.add(fa.faCopy); @@ -28,18 +30,18 @@ export interface User { } export enum SharingPermissions { - None = "Not Shared", - View = "Can View", + Edit = "Can Edit", Add = "Can Add", - Edit = "Can Edit" + View = "Can View", + None = "Not Shared" } -const ColorMapping = new Map<string, string>([ - [SharingPermissions.None, "red"], - [SharingPermissions.View, "maroon"], - [SharingPermissions.Add, "blue"], - [SharingPermissions.Edit, "green"] -]); +// const ColorMapping = new Map<string, string>([ +// [SharingPermissions.None, "red"], +// [SharingPermissions.View, "maroon"], +// [SharingPermissions.Add, "blue"], +// [SharingPermissions.Edit, "green"] +// ]); const HierarchyMapping = new Map<string, string>([ [SharingPermissions.None, "0"], @@ -54,10 +56,18 @@ const HierarchyMapping = new Map<string, string>([ ]); +interface GroupOptions { + label: string; + options: UserOptions[]; +} + const SharingKey = "sharingPermissions"; const PublicKey = "publicLinkPermissions"; const DefaultColor = "black"; +const groupType = "!groupType/"; +const indType = "!indType/"; + interface ValidatedUser { user: User; notificationDoc: Doc; @@ -70,17 +80,20 @@ export default class SharingManager extends React.Component<{}> { public static Instance: SharingManager; @observable private isOpen = false; @observable private users: ValidatedUser[] = []; - @observable private groups: Doc[] = []; + // @observable private groups: Doc[] = []; @observable private targetDoc: Doc | undefined; @observable private targetDocView: DocumentView | undefined; @observable private copied = false; @observable private dialogueBoxOpacity = 1; @observable private overlayOpacity = 0.4; - @observable private groupToView: Opt<Doc>; + @observable private selectedUsers: UserOptions[] | null = null; + @observable private permissions: SharingPermissions = SharingPermissions.Edit; + @observable private sharedUsers: ValidatedUser[] = []; + @observable private sharedGroups: Doc[] = []; - private get linkVisible() { - return this.sharingDoc ? this.sharingDoc[PublicKey] !== SharingPermissions.None : false; - } + // private get linkVisible() { + // return this.sharingDoc ? this.sharingDoc[PublicKey] !== SharingPermissions.None : false; + // } public open = (target: DocumentView) => { SelectionManager.DeselectAll(); @@ -89,12 +102,12 @@ export default class SharingManager extends React.Component<{}> { this.targetDoc = target.props.Document; DictationOverlay.Instance.hasActiveModal = true; this.isOpen = true; - if (!this.sharingDoc) { - this.sharingDoc = new Doc; - } + // if (!this.sharingDoc) { + // this.sharingDoc = new Doc; + // } })); - runInAction(() => this.groups = GroupManager.Instance.getAllGroupsCopy()); + // runInAction(() => this.groups = GroupManager.Instance.getAllGroups()); } public close = action(() => { @@ -107,13 +120,13 @@ export default class SharingManager extends React.Component<{}> { }), 500); }); - private get sharingDoc() { - return this.targetDoc ? Cast(this.targetDoc[SharingKey], Doc) as Doc : undefined; - } + // private get sharingDoc() { + // return this.targetDoc ? Cast(this.targetDoc[SharingKey], Doc) as Doc : undefined; + // } - private set sharingDoc(value: Doc | undefined) { - this.targetDoc && (this.targetDoc[SharingKey] = value); - } + // private set sharingDoc(value: Doc | undefined) { + // this.targetDoc && (this.targetDoc[SharingKey] = value); + // } constructor(props: {}) { super(props); @@ -144,77 +157,121 @@ export default class SharingManager extends React.Component<{}> { const members: string[] = JSON.parse(StrCast(group.members)); const users: ValidatedUser[] = this.users.filter(user => members.includes(user.user.email)); - const sharingDoc = this.sharingDoc!; - if (permission === SharingPermissions.None) { - const metadata = sharingDoc[StrCast(group.groupName)]; - if (metadata) sharingDoc[StrCast(group.groupName)] = undefined; - } - else { - sharingDoc[StrCast(group.groupName)] = permission; - } + const target = this.targetDoc!; + const ACL = `ACL-${StrCast(group.groupName)}`; + + target[ACL] = permission; + // const sharingDoc = this.sharingDoc!; + // if (permission === SharingPermissions.None) { + // const metadata = sharingDoc[StrCast(group.groupName)]; + // if (metadata) sharingDoc[StrCast(group.groupName)] = undefined; + // } + // else { + // sharingDoc[StrCast(group.groupName)] = permission; + // } + + 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 }) => { + + if (permission !== SharingPermissions.None) Doc.IndexOf(target, DocListCast(notificationDoc[storage])) === -1 && Doc.AddDocToList(notificationDoc, storage, target); + else Doc.IndexOf(target, DocListCast(notificationDoc[storage])) !== -1 && Doc.RemoveDocFromList(notificationDoc, storage, target); - users.forEach(user => { - this.setInternalSharing(user, permission, group); }); } - setInternalSharing = async (recipient: ValidatedUser, state: string, group: Opt<Doc>) => { - const { user, notificationDoc } = recipient; - const target = this.targetDoc!; - const manager = this.sharingDoc!; - const key = user.userDocumentId; - - let metadata = await DocCastAsync(manager[key]); - const permissions: { [key: string]: number } = metadata?.permissions ? JSON.parse(StrCast(metadata.permissions)) : {}; - permissions[StrCast(group ? group.groupName : Doc.CurrentUserEmail)] = parseInt(HierarchyMapping.get(state)!); - const max = Math.max(...Object.values(permissions)); - - // let max = 0; - // const keys: string[] = []; - // for (const [key, value] of Object.entries(permissions)) { - // if (value === max && max !== 0) { - // keys.push(key); - // } - // else if (value > max) { - // keys.splice(0, keys.length); - // keys.push(key); - // max = value; - // } - // } + shareWithAddedMember = (group: Doc, email: string) => { + const user: ValidatedUser = this.users.find(user => user.user.email === email)!; - switch (max) { - case 0: - if (metadata) { - const sharedAlias = (await DocCastAsync(metadata.sharedAlias))!; - Doc.RemoveDocFromList(notificationDoc, storage, sharedAlias); - manager[key] = undefined; - } - break; - - case 1: case 2: case 3: - if (!metadata) { - metadata = new Doc; - const sharedAlias = Doc.MakeAlias(target); - Doc.AddDocToList(notificationDoc, storage, sharedAlias); - metadata.sharedAlias = sharedAlias; - manager[key] = metadata; - } - metadata.permissions = JSON.stringify(permissions); - // metadata.usersShared = JSON.stringify(keys); - break; + if (group.docsShared) { + DocListCast(group.docsShared).forEach(doc => Doc.IndexOf(doc, DocListCast(user.notificationDoc[storage])) === -1 && Doc.AddDocToList(user.notificationDoc, storage, doc)); } + } - if (metadata) metadata.maxPermission = HierarchyMapping.get(`${max}`); + removeMember = (group: Doc, email: string) => { + const user: ValidatedUser = this.users.find(user => user.user.email === email)!; + + if (group.docsShared) { + DocListCast(group.docsShared).forEach(doc => Doc.IndexOf(doc, DocListCast(user.notificationDoc[storage])) !== -1 && Doc.RemoveDocFromList(user.notificationDoc, storage, doc)); + } } - private setExternalSharing = (state: string) => { - const sharingDoc = this.sharingDoc; - if (!sharingDoc) { - return; + removeGroup = (group: Doc) => { + if (group.docsShared) { + DocListCast(group.docsShared).forEach(doc => { + const ACL = `ACL-${StrCast(group.groupName)}`; + doc[ACL] = "Not Shared"; + + const members: string[] = JSON.parse(StrCast(group.members)); + const users: ValidatedUser[] = this.users.filter(user => members.includes(user.user.email)); + + users.forEach(user => Doc.RemoveDocFromList(user.notificationDoc, storage, doc)); + }) } - sharingDoc[PublicKey] = state; } + setInternalSharing = (recipient: ValidatedUser, permission: string) => { + const { user, notificationDoc } = recipient; + const target = this.targetDoc!; + // const manager = this.sharingDoc!; + const key = user.email.replace('.', '_'); + // const key = user.userDocumentId; + + const ACL = `ACL-${key}`; + + // const permissions: { [key: string]: number } = target[ACL] ? JSON.parse(StrCast(target[ACL])) : {}; + + target[ACL] = permission; + + + if (permission !== SharingPermissions.None) Doc.IndexOf(target, DocListCast(notificationDoc[storage])) === -1 && Doc.AddDocToList(notificationDoc, storage, target); + else Doc.IndexOf(target, DocListCast(notificationDoc[storage])) !== -1 && Doc.RemoveDocFromList(notificationDoc, storage, target); + + } + + + // let metadata = await DocCastAsync(manager[key]); + // const permissions: { [key: string]: number } = metadata?.permissions ? JSON.parse(StrCast(metadata.permissions)) : {}; + // permissions[StrCast(group ? group.groupName : Doc.CurrentUserEmail)] = parseInt(HierarchyMapping.get(permission)!); + // const max = Math.max(...Object.values(permissions)); + + // switch (max) { + // case 0: + // // if (metadata) { + // // const sharedAlias = (await DocCastAsync(metadata.sharedAlias))!; + // // Doc.RemoveDocFromList(notificationDoc, storage, sharedAlias); + // // manager[key] = undefined; + // // } + // Doc.RemoveDocFromList(notificationDoc, storage, target); + // break; + + // case 1: case 2: case 3: + + // Doc.AddDocToList(notificationDoc, storage, target); + + // if (!metadata) { + // metadata = new Doc; + // const sharedAlias = Doc.MakeAlias(target); + // Doc.AddDocToList(notificationDoc, storage, target); + // metadata.sharedAlias = sharedAlias; + // manager[key] = metadata; + // } + // metadata.permissions = JSON.stringify(permissions); + // // metadata.usersShared = JSON.stringify(keys); + // break; + // } + + // if (metadata) metadata.maxPermission = HierarchyMapping.get(`${max}`); + + + // private setExternalSharing = (permission: string) => { + // const sharingDoc = this.sharingDoc; + // if (!sharingDoc) { + // return; + // } + // sharingDoc[PublicKey] = permission; + // } + private get sharingUrl() { if (!this.targetDoc) { return undefined; @@ -271,32 +328,137 @@ export default class SharingManager extends React.Component<{}> { } private computePermissions = (userKey: string) => { - const sharingDoc = this.sharingDoc; - if (!sharingDoc) { - return SharingPermissions.None; - } - const metadata = sharingDoc[userKey] as Doc | string; - if (!metadata) { - return SharingPermissions.None; - } - return StrCast(metadata instanceof Doc ? metadata.maxPermission : metadata, SharingPermissions.None); + // const sharingDoc = this.sharingDoc; + // if (!sharingDoc) { + // return SharingPermissions.None; + // } + // const metadata = sharingDoc[userKey] as Doc | string; + + if (!this.targetDoc) return SharingPermissions.None; + + const ACL = `ACL-${userKey}`; + const permission = StrCast(this.targetDoc[ACL]); + + // if (!metadata) { + // return SharingPermissions.None; + // } + return StrCast(this.targetDoc[ACL], SharingPermissions.None); + } + + @action + handleUsersChange = (selectedOptions: any) => { + this.selectedUsers = selectedOptions as UserOptions[]; + } + + @action + handlePermissionsChange = (event: React.ChangeEvent<HTMLSelectElement>) => { + this.permissions = event.currentTarget.value as SharingPermissions; + } + + @action + share = () => { + this.selectedUsers?.forEach(user => { + if (user.value.includes(indType)) { + console.log(user); + console.log(this.users.find(u => u.user.email === user.label)); + this.setInternalSharing(this.users.find(u => u.user.email === user.label)!, this.permissions); + } + else { + this.setInternalGroupSharing(GroupManager.Instance.getGroup(user.label)!, this.permissions); + } + }); + this.selectedUsers = null; } private get sharingInterface() { const existOtherUsers = this.users.length > 0; - const existGroups = this.groups.length > 0; + const existGroups = GroupManager.Instance?.getAllGroups().length > 0; // const manager = this.sharingDoc!; + const options: GroupOptions[] = GroupManager.Instance ? + [ + { + label: 'Individuals', + options: GroupManager.Instance.options.map(({ label, value }) => ({ label, value: "!indType/" + value })) + }, + { + label: 'Groups', + options: GroupManager.Instance.getAllGroups().map(({ groupName }) => ({ label: StrCast(groupName), value: "!groupType/" + StrCast(groupName) })) + } + ] + : []; + + const userListContents: (JSX.Element | null)[] = this.users.map(({ user, notificationDoc }) => { // can't use async here + const userKey = user.email.replace('.', '_'); + // const userKey = user.userDocumentId; + const permissions = this.computePermissions(userKey); + // const color = ColorMapping.get(permissions); + + // console.log(manager); + // const metadata = manager[userKey] as Doc; + // const usersShared = StrCast(metadata?.usersShared, ""); + // console.log(usersShared) + + return permissions === SharingPermissions.None ? null : ( + <div + key={userKey} + className={"container"} + > + <span className={"padding"}>{user.email}</span> + {/* <div className={"shared-by"}>{usersShared}</div> */} + <div className="edit-actions"> + <select + className={"permissions-dropdown"} + value={permissions} + // style={{ color, borderColor: color }} + onChange={e => this.setInternalSharing({ user, notificationDoc }, e.currentTarget.value)} + > + {this.sharingOptions} + </select> + </div> + </div> + ); + }); + + + const groupListContents = GroupManager.Instance?.getAllGroups().map(group => { + const permissions = this.computePermissions(StrCast(group.groupName)); + // const color = ColorMapping.get(permissions); + + return permissions === SharingPermissions.None ? null : ( + <div + key={StrCast(group.groupName)} + className={"container"} + > + <span className={"padding"}>{group.groupName}</span> + <div className="edit-actions"> + <select + className={"permissions-dropdown"} + value={permissions} + // style={{ color, borderColor: color }} + onChange={e => this.setInternalGroupSharing(group, e.currentTarget.value)} + > + {this.sharingOptions} + </select> + <button onClick={action(() => GroupManager.Instance.currentGroup = group)}>Edit</button> + </div> + </div> + ); + }); + + const displayUserList = userListContents?.every(user => user === null); + const displayGroupList = groupListContents?.every(group => group === null); + return ( <div className={"sharing-interface"}> - {this.groupToView ? + {GroupManager.Instance?.currentGroup ? <GroupMemberView - group={this.groupToView} - onCloseButtonClick={action(() => this.groupToView = undefined)} + group={GroupManager.Instance.currentGroup} + onCloseButtonClick={action(() => GroupManager.Instance.currentGroup = undefined)} /> : null} - <p className={"share-link"}>Manage the public link to {this.focusOn("this document...")}</p> + {/* <p className={"share-link"}>Manage the public link to {this.focusOn("this document...")}</p> {!this.linkVisible ? (null) : <div className={"link-container"}> <div className={"link-box"} onClick={this.copy}>{this.sharingUrl}</div> @@ -325,80 +487,65 @@ export default class SharingManager extends React.Component<{}> { {this.sharingOptions} </select> </div> - <div className={"hr-substitute"} /> + <div className={"hr-substitute"} /> */} <div className="sharing-contents"> - <div className={"individual-container"}> - <p className={"share-individual"}>Privately share {this.focusOn("this document")} with an individual...</p> - <div className={"users-list"} style={{ display: existOtherUsers ? "block" : "flex", minHeight: existOtherUsers ? undefined : 150 }}>{/*200*/} - {!existOtherUsers ? "There are no other users in your database." : - this.users.map(({ user, notificationDoc }) => { // can't use async here - const userKey = user.userDocumentId; - const permissions = this.computePermissions(userKey); - const color = ColorMapping.get(permissions); - - // console.log(manager); - // const metadata = manager[userKey] as Doc; - // const usersShared = StrCast(metadata?.usersShared, ""); - // console.log(usersShared) - - - return ( + <p className={"share-title"}><b>Share </b>{this.focusOn(StrCast(this.targetDoc?.title, "this document"))}</p> + <div className={"close-button"} onClick={this.close}> + <FontAwesomeIcon icon={fa.faWindowClose} size={"lg"} /> + </div> + {this.targetDoc?.author !== Doc.CurrentUserEmail ? null + : + <div className="share-setup"> + <Select + className={"user-search"} + placeholder={"Enter user or group name..."} + isMulti + closeMenuOnSelect={false} + options={options} + onChange={this.handleUsersChange} + value={this.selectedUsers} + /> + <select className="permissions-select" onChange={this.handlePermissionsChange}> + {this.sharingOptions} + </select> + <button className="share-button" onClick={this.share}> + Share + </button> + </div> + } + <div className="main-container"> + <div className={"individual-container"}> + <div className={"users-list"} style={{ display: displayUserList ? "flex" : "block" }}>{/*200*/} + { + displayUserList ? <div - key={userKey} - className={"container"} + className={"none"} > - <span className={"padding"}>{user.email}</span> - {/* <div className={"shared-by"}>{usersShared}</div> */} - <div className="edit-actions"> - <select - className={"permissions-dropdown"} - value={permissions} - style={{ color, borderColor: color }} - onChange={e => this.setInternalSharing({ user, notificationDoc }, e.currentTarget.value, undefined)} - > - {this.sharingOptions} - </select> - </div> + There are no users this document has been shared with. </div> - ); - }) - } + : + userListContents + } + </div> </div> - </div> - <div className={"group-container"}> - <p className={"share-groups"}>Privately share {this.focusOn("this document")} with a group...</p> - <div className={"groups-list"} style={{ display: existGroups ? "block" : "flex", minHeight: existOtherUsers ? undefined : 150 }}>{/*200*/} - {!existGroups ? "There are no groups in your database." : - this.groups.map(group => { - const permissions = this.computePermissions(StrCast(group.groupName)); - const color = ColorMapping.get(permissions); - return ( + <div className={"group-container"}> + <div className={"groups-list"} style={{ display: displayGroupList ? "flex" : "block" }}>{/*200*/} + { + displayGroupList ? <div - key={StrCast(group.groupName)} - className={"container"} + className={"none"} > - <span className={"padding"}>{group.groupName}</span> - <div className="edit-actions"> - <select - className={"permissions-dropdown"} - value={permissions} - style={{ color, borderColor: color }} - onChange={e => this.setInternalGroupSharing(group, e.currentTarget.value)} - > - {this.sharingOptions} - </select> - <button onClick={action(() => this.groupToView = group)}>Edit</button> + There are no groups this document has been shared with. </div> - </div> - ); - }) - - } + : + groupListContents + } + </div> </div> </div> + </div> - <div className={"close-button"} onClick={this.close}>Done</div> </div> ); } @@ -412,6 +559,7 @@ export default class SharingManager extends React.Component<{}> { interactive={true} dialogueBoxDisplayedOpacity={this.dialogueBoxOpacity} overlayDisplayedOpacity={this.overlayOpacity} + closeOnExternalClick={this.close} /> ); } diff --git a/src/client/views/DictationOverlay.tsx b/src/client/views/DictationOverlay.tsx index 65770c0bb..9ed14509f 100644 --- a/src/client/views/DictationOverlay.tsx +++ b/src/client/views/DictationOverlay.tsx @@ -66,6 +66,7 @@ export class DictationOverlay extends React.Component { interactive={false} dialogueBoxStyle={dialogueBoxStyle} overlayStyle={overlayStyle} + closeOnExternalClick={this.initiateDictationFade} />); } }
\ No newline at end of file diff --git a/src/client/views/MainViewModal.scss b/src/client/views/MainViewModal.scss index f5a9ee76c..812fe540b 100644 --- a/src/client/views/MainViewModal.scss +++ b/src/client/views/MainViewModal.scss @@ -6,9 +6,10 @@ align-self: center; align-content: center; padding: 20px; - background: gainsboro; + // background: gainsboro; + background: white; border-radius: 10px; - border: 3px solid black; + border: 0.5px solid black; box-shadow: #00000044 5px 5px 10px; transform: translate(-50%, -50%); top: 50%; diff --git a/src/client/views/MainViewModal.tsx b/src/client/views/MainViewModal.tsx index a7bd5882d..0b73a6ad7 100644 --- a/src/client/views/MainViewModal.tsx +++ b/src/client/views/MainViewModal.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; import "./MainViewModal.scss"; +import { observer } from 'mobx-react'; export interface MainViewOverlayProps { isDisplayed: boolean; @@ -9,8 +10,10 @@ export interface MainViewOverlayProps { overlayStyle?: React.CSSProperties; dialogueBoxDisplayedOpacity?: number; overlayDisplayedOpacity?: number; + closeOnExternalClick?: () => void; } +@observer export default class MainViewModal extends React.Component<MainViewOverlayProps> { render() { @@ -22,7 +25,6 @@ export default class MainViewModal extends React.Component<MainViewOverlayProps> <div className={"dialogue-box"} style={{ - backgroundColor: "gainsboro", borderColor: "black", ...(p.dialogueBoxStyle || {}), opacity: p.isDisplayed ? dialogueOpacity : 0 @@ -30,6 +32,7 @@ export default class MainViewModal extends React.Component<MainViewOverlayProps> >{p.contents}</div> <div className={"overlay"} + onClick={this.props?.closeOnExternalClick} style={{ backgroundColor: "gainsboro", ...(p.overlayStyle || {}), diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index b38db9a1e..297a9f2a5 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -1188,9 +1188,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } render() { - if (!(this.props.Document instanceof Doc)) return (null); if (this.props.Document[AclSym] && this.props.Document[AclSym] === AclPrivate) return (null); - if (this.props.Document.hidden) return (null); + if (!(this.props.Document instanceof Doc)) return (null); const backgroundColor = Doc.UserDoc().renderStyle === "comic" ? undefined : this.props.forcedBackgroundColor?.(this.Document) || StrCast(this.layoutDoc._backgroundColor) || StrCast(this.layoutDoc.backgroundColor) || StrCast(this.Document.backgroundColor) || this.props.backgroundColor?.(this.Document); const opacity = Cast(this.layoutDoc._opacity, "number", Cast(this.layoutDoc.opacity, "number", Cast(this.Document.opacity, "number", null))); const finalOpacity = this.props.opacity ? this.props.opacity() : opacity; diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index 7aa1d528d..bbc9d388a 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -96,13 +96,13 @@ export const AclSym = Symbol("Acl"); export const AclPrivate = Symbol("AclOwnerOnly"); export const AclReadonly = Symbol("AclReadOnly"); export const AclAddonly = Symbol("AclAddonly"); -export const AclReadWrite = Symbol("AclReadWrite"); +export const AclEdit = Symbol("AclEdit"); export const UpdatingFromServer = Symbol("UpdatingFromServer"); const CachedUpdates = Symbol("Cached updates"); export function fetchProto(doc: Doc) { - if (doc.author !== Doc.CurrentUserEmail) { + if (doc.author !== Doc.CurrentUserEmail) { // storing acls for groups needs to be extended here - AclSym should store a datastructure that stores information about permissions const acl = Doc.Get(doc, "ACL", true); switch (acl) { case "ownerOnly": @@ -114,8 +114,8 @@ export function fetchProto(doc: Doc) { case "addOnly": doc[AclSym] = AclAddonly; break; - case "write": - doc[AclSym] = AclReadWrite; + case "edit": + doc[AclSym] = AclEdit; } } diff --git a/src/fields/util.ts b/src/fields/util.ts index 2dc21c987..7bb090a93 100644 --- a/src/fields/util.ts +++ b/src/fields/util.ts @@ -111,7 +111,7 @@ const layoutProps = ["panX", "panY", "width", "height", "nativeWidth", "nativeHe "chromeStatus", "viewType", "gridGap", "xMargin", "yMargin", "autoHeight"]; export function setter(target: any, in_prop: string | symbol | number, value: any, receiver: any): boolean { let prop = in_prop; - if (target[AclSym] && !_overrideAcl && !DocServer.PlaygroundFields.includes(in_prop.toString())) return true; + if (target[AclSym] && !_overrideAcl && !DocServer.PlaygroundFields.includes(in_prop.toString())) return true; // generalise to a testpermission function if (typeof prop === "string" && prop !== "__id" && prop !== "__fields" && (prop.startsWith("_") || layoutProps.includes(prop))) { if (!prop.startsWith("_")) { console.log(prop + " is deprecated - switch to _" + prop); diff --git a/src/mobile/AudioUpload.tsx b/src/mobile/AudioUpload.tsx index 590bf8f9d..b75102e43 100644 --- a/src/mobile/AudioUpload.tsx +++ b/src/mobile/AudioUpload.tsx @@ -143,6 +143,7 @@ export class AudioUpload extends React.Component { interactive={true} dialogueBoxDisplayedOpacity={this.dialogueBoxOpacity} overlayDisplayedOpacity={this.overlayOpacity} + closeOnExternalClick={this.closeUpload} /> ); } diff --git a/src/mobile/ImageUpload.tsx b/src/mobile/ImageUpload.tsx index 5ea626d52..6a5834f52 100644 --- a/src/mobile/ImageUpload.tsx +++ b/src/mobile/ImageUpload.tsx @@ -172,6 +172,7 @@ export class Uploader extends React.Component<ImageUploadProps> { interactive={true} dialogueBoxDisplayedOpacity={this.dialogueBoxOpacity} overlayDisplayedOpacity={this.overlayOpacity} + closeOnExternalClick={this.closeUpload} /> ); } |