aboutsummaryrefslogtreecommitdiff
path: root/src/client
diff options
context:
space:
mode:
Diffstat (limited to 'src/client')
-rw-r--r--src/client/DocServer.ts8
-rw-r--r--src/client/apis/GoogleAuthenticationManager.tsx1
-rw-r--r--src/client/util/GroupManager.tsx117
-rw-r--r--src/client/util/GroupMemberView.tsx10
-rw-r--r--src/client/util/SettingsManager.tsx1
-rw-r--r--src/client/util/SharingManager.scss174
-rw-r--r--src/client/util/SharingManager.tsx495
-rw-r--r--src/client/views/DictationOverlay.tsx1
-rw-r--r--src/client/views/DocComponent.tsx8
-rw-r--r--src/client/views/GlobalKeyHandler.ts2
-rw-r--r--src/client/views/MainViewModal.scss5
-rw-r--r--src/client/views/MainViewModal.tsx5
-rw-r--r--src/client/views/collections/CollectionView.tsx25
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx6
-rw-r--r--src/client/views/nodes/DocumentContentsView.tsx7
-rw-r--r--src/client/views/nodes/DocumentView.tsx26
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx6
17 files changed, 570 insertions, 327 deletions
diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts
index 2a7a7c59a..bac324c77 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)
}
@@ -39,9 +39,9 @@ export namespace DocServer {
const docsWithUpdates: { [field: string]: Set<Doc> } = {};
export var PlaygroundFields: string[];
- export function setPlaygroundFields(livePlayougroundFields: string[]) {
- DocServer.PlaygroundFields = livePlayougroundFields;
- livePlayougroundFields.forEach(f => DocServer.setFieldWriteMode(f, DocServer.WriteMode.LivePlayground));
+ export function setPlaygroundFields(livePlaygroundFields: string[]) {
+ DocServer.PlaygroundFields = livePlaygroundFields;
+ livePlaygroundFields.forEach(f => DocServer.setFieldWriteMode(f, DocServer.WriteMode.LivePlayground));
}
export function setFieldWriteMode(field: string, writeMode: WriteMode) {
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..23bdd248b 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";
@@ -14,6 +14,7 @@ import Select from 'react-select';
import "./GroupManager.scss";
import { StrCast } from "../../fields/Types";
import GroupMemberView from "./GroupMemberView";
+import { setGroups } from "../../fields/util";
library.add(fa.faWindowClose);
@@ -33,6 +34,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.
+ currentUserGroups: string[] = [];
constructor(props: Readonly<{}>) {
super(props);
@@ -42,6 +44,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(StrCast(group.groupName));
+ });
+ })
+ .finally(() => setGroups(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 +71,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);
}
@@ -89,7 +111,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 +121,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;
}
@@ -123,6 +147,11 @@ export default class GroupManager extends React.Component<{}> {
);
}
+ getGroupMembers(group: string | Doc): string[] {
+ if (group instanceof Doc) return JSON.parse(StrCast(group.members)) as string[];
+ else return JSON.parse(StrCast(this.getGroup(group)!.members)) as string[];
+ }
+
/**
* @returns the members of the admin group.
*/
@@ -150,6 +179,10 @@ export default class GroupManager extends React.Component<{}> {
groupDoc.groupName = groupName;
groupDoc.owners = JSON.stringify([Doc.CurrentUserEmail]);
groupDoc.members = JSON.stringify(memberEmails);
+ if (memberEmails.includes(Doc.CurrentUserEmail)) {
+ this.currentUserGroups.push(groupName);
+ setGroups(this.currentUserGroups);
+ }
this.addGroup(groupDoc);
}
@@ -172,8 +205,16 @@ 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);
+ const members: string[] = JSON.parse(StrCast(group.members));
+ if (members.includes(Doc.CurrentUserEmail)) {
+ const index = this.currentUserGroups.findIndex(groupName => groupName === group.groupName);
+ index !== -1 && this.currentUserGroups.splice(index, 1);
+ setGroups(this.currentUserGroups);
+ }
if (group === this.currentGroup) {
runInAction(() => this.currentGroup = undefined);
}
@@ -193,6 +234,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 +247,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);
+ }
}
}
@@ -246,52 +291,6 @@ 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>
-
- {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.
*/
private get groupInterface() {
@@ -307,7 +306,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 +327,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 +359,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..742caa676 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,6 +42,13 @@ 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>
@@ -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..af68edab6 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, DocListCastAsync } 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,36 +30,31 @@ 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 HierarchyMapping = new Map<string, string>([
- [SharingPermissions.None, "0"],
- [SharingPermissions.View, "1"],
- [SharingPermissions.Add, "2"],
- [SharingPermissions.Edit, "3"],
-
- ["0", SharingPermissions.None],
- ["1", SharingPermissions.View],
- ["2", SharingPermissions.Add],
- ["3", SharingPermissions.Edit]
+// const ColorMapping = new Map<string, string>([
+// [SharingPermissions.None, "red"],
+// [SharingPermissions.View, "maroon"],
+// [SharingPermissions.Add, "blue"],
+// [SharingPermissions.Edit, "green"]
+// ]);
-]);
+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 +67,18 @@ 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;
- 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 +87,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 +105,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 +142,139 @@ 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;
+ const target = this.targetDoc!;
+ const ACL = `ACL-${StrCast(group.groupName)}`;
+
+ target[ACL] = permission;
+
+ 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(res => console.log(res));
+ 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);
+ });
+ });
+ }
+
+ shareWithAddedMember = (group: Doc, email: string) => {
+ const user: ValidatedUser = this.users.find(user => user.user.email === email)!;
+
+ 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));
+ });
+ });
+ // DocListCast(group.docsShared).forEach(doc => Doc.IndexOf(doc, DocListCast(user.notificationDoc[storage])) === -1 && Doc.AddDocToList(user.notificationDoc, storage, doc));
}
- else {
- sharingDoc[StrCast(group.groupName)] = permission;
+ }
+
+ removeMember = (group: Doc, email: string) => {
+ const user: ValidatedUser = this.users.find(user => user.user.email === email)!;
+
+ 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));
+ });
+ });
+ // DocListCast(group.docsShared).forEach(doc => Doc.IndexOf(doc, DocListCast(user.notificationDoc[storage])) === -1 && Doc.AddDocToList(user.notificationDoc, storage, doc));
}
+ }
- users.forEach(user => {
- this.setInternalSharing(user, permission, group);
- });
+ removeGroup = (group: Doc) => {
+ if (group.docsShared) {
+ DocListCastAsync(group.docsShared).then(resolved => {
+ resolved?.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));
+ });
+
+ });
+ }
}
- setInternalSharing = async (recipient: ValidatedUser, state: string, group: Opt<Doc>) => {
+ setInternalSharing = (recipient: ValidatedUser, permission: string) => {
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;
- // }
- // }
+ // const manager = this.sharingDoc!;
+ const key = user.email.replace('.', '_');
+ // const key = user.userDocumentId;
- 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;
- }
+ const ACL = `ACL-${key}`;
- if (metadata) metadata.maxPermission = HierarchyMapping.get(`${max}`);
- }
+ // const permissions: { [key: string]: number } = target[ACL] ? JSON.parse(StrCast(target[ACL])) : {};
- private setExternalSharing = (state: string) => {
- const sharingDoc = this.sharingDoc;
- if (!sharingDoc) {
- return;
+ target[ACL] = permission;
+
+
+ if (permission !== SharingPermissions.None) {
+ console.log(target);
+ console.log(notificationDoc);
+ DocListCastAsync(notificationDoc[storage]).then(resolved => {
+ Doc.IndexOf(target, resolved!) === -1 && Doc.AddDocToList(notificationDoc, storage, target);
+ });
}
- sharingDoc[PublicKey] = state;
+ else {
+ DocListCastAsync(notificationDoc[storage]).then(resolved => {
+ Doc.IndexOf(target, resolved!) === -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 +331,135 @@ 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)) {
+ 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 +488,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 +560,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/DocComponent.tsx b/src/client/views/DocComponent.tsx
index 9b9a28f0f..eb58d8a3e 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -1,4 +1,4 @@
-import { Doc, Opt, DataSym, DocListCast, AclSym, AclReadonly, AclAddonly } from '../../fields/Doc';
+import { Doc, Opt, DataSym, DocListCast, AclReadonly, AclAddonly } from '../../fields/Doc';
import { Touchable } from './Touchable';
import { computed, action, observable } from 'mobx';
import { Cast, BoolCast, ScriptCast } from '../../fields/Types';
@@ -7,6 +7,7 @@ import { InteractionUtils } from '../util/InteractionUtils';
import { List } from '../../fields/List';
import { DateField } from '../../fields/DateField';
import { ScriptField } from '../../fields/ScriptField';
+import { GetEffectiveAcl } from '../../fields/util';
/// DocComponent returns a generic React base class used by views that don't have 'fieldKey' props (e.g.,CollectionFreeFormDocumentView, DocumentView)
@@ -137,10 +138,11 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T
const targetDataDoc = this.props.Document[DataSym];
const docList = DocListCast(targetDataDoc[this.annotationKey]);
const added = docs.filter(d => !docList.includes(d));
+ const effectiveAcl = GetEffectiveAcl(this.dataDoc);
if (added.length) {
- if (this.dataDoc[AclSym] === AclReadonly) {
+ if (effectiveAcl === AclReadonly) {
return false;
- } else if (this.dataDoc[AclSym] === AclAddonly) {
+ } else if (effectiveAcl === AclAddonly) {
added.map(doc => Doc.AddDocToList(targetDataDoc, this.annotationKey, doc));
} else {
added.map(doc => doc.context = this.props.Document);
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index a3a023164..45d53a5f5 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -22,6 +22,7 @@ import { DocumentView } from "./nodes/DocumentView";
import { DocumentLinksButton } from "./nodes/DocumentLinksButton";
import PDFMenu from "./pdf/PDFMenu";
import { ContextMenu } from "./ContextMenu";
+import GroupManager from "../util/GroupManager";
const modifiers = ["control", "meta", "shift", "alt"];
type KeyHandler = (keycode: string, e: KeyboardEvent) => KeyControlInfo | Promise<KeyControlInfo>;
@@ -107,6 +108,7 @@ export default class KeyManager {
GoogleAuthenticationManager.Instance.cancel();
HypothesisAuthenticationManager.Instance.cancel();
SharingManager.Instance.close();
+ GroupManager.Instance.close();
break;
case "delete":
case "backspace":
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/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index df21d6a28..34d67905e 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -8,7 +8,7 @@ import * as React from 'react';
import Lightbox from 'react-image-lightbox-with-rotate';
import 'react-image-lightbox-with-rotate/style.css'; // This only needs to be imported once in your app
import { DateField } from '../../../fields/DateField';
-import { AclAddonly, AclReadonly, AclSym, DataSym, Doc, DocListCast, Field, Opt } from '../../../fields/Doc';
+import { AclAddonly, AclReadonly, DataSym, Doc, DocListCast, Field, Opt, AclEdit } from '../../../fields/Doc';
import { Id } from '../../../fields/FieldSymbols';
import { List } from '../../../fields/List';
import { ObjectField } from '../../../fields/ObjectField';
@@ -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 } from '../../../fields/util';
+import { TraceMobx, GetEffectiveAcl, getPlaygroundMode } 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';
@@ -132,10 +132,11 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus
const targetDataDoc = this.props.Document[DataSym];
const docList = DocListCast(targetDataDoc[this.props.fieldKey]);
const added = docs.filter(d => !docList.includes(d));
+ const effectiveAcl = GetEffectiveAcl(this.props.Document);
if (added.length) {
- if (this.dataDoc[AclSym] === AclReadonly) {
+ if (effectiveAcl === AclReadonly && !getPlaygroundMode()) {
return false;
- } else if (this.dataDoc[AclSym] === AclAddonly) {
+ } else if (effectiveAcl === AclAddonly && !getPlaygroundMode()) {
added.map(doc => Doc.AddDocToList(targetDataDoc, this.props.fieldKey, doc));
} else {
added.map(doc => {
@@ -165,13 +166,15 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus
@action.bound
removeDocument = (doc: any): boolean => {
- 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);
- return true;
+ if (GetEffectiveAcl(this.props.Document) === AclEdit || getPlaygroundMode()) {
+ 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);
+ return true;
+ }
}
return false;
}
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index b47236bea..8ce2dc9a4 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -1,6 +1,7 @@
import { action, computed, observable } from "mobx";
import { observer } from "mobx-react";
-import { Doc, Opt, DocListCast, DataSym } from "../../../../fields/Doc";
+import { Doc, Opt, DocListCast, DataSym, AclEdit, AclAddonly } from "../../../../fields/Doc";
+import { GetEffectiveAcl, getPlaygroundMode } from "../../../../fields/util";
import { InkData, InkField, InkTool } from "../../../../fields/InkField";
import { List } from "../../../../fields/List";
import { RichTextField } from "../../../../fields/RichTextField";
@@ -276,7 +277,8 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
} else {
this._downX = x;
this._downY = y;
- PreviewCursor.Show(x, y, this.onKeyPress, this.props.addLiveTextDocument, this.props.getTransform, this.props.addDocument, this.props.nudge);
+ const effectiveAcl = GetEffectiveAcl(this.props.Document);
+ if (effectiveAcl === AclEdit || effectiveAcl === AclAddonly || getPlaygroundMode()) 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 f1438fd54..f2f8ada68 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -1,6 +1,6 @@
import { computed } from "mobx";
import { observer } from "mobx-react";
-import { Doc, Opt, Field, AclSym, AclPrivate } from "../../../fields/Doc";
+import { Doc, Opt, Field, AclPrivate } from "../../../fields/Doc";
import { Cast, StrCast, NumCast } from "../../../fields/Types";
import { OmitKeys, Without, emptyPath } from "../../../Utils";
import { DirectoryImportBox } from "../../util/Import & Export/DirectoryImportBox";
@@ -36,7 +36,7 @@ import { WebBox } from "./WebBox";
import { InkingStroke } from "../InkingStroke";
import React = require("react");
import { RecommendationsBox } from "../RecommendationsBox";
-import { TraceMobx } from "../../../fields/util";
+import { TraceMobx, GetEffectiveAcl } from "../../../fields/util";
import { ScriptField } from "../../../fields/ScriptField";
import XRegExp = require("xregexp");
@@ -184,8 +184,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
const bindings = this.CreateBindings(onClick, onInput);
// layoutFrame = splits.length > 1 ? splits[0] + splits[1].replace(/{([^{}]|(?R))*}/, replacer4) : ""; // might have been more elegant if javascript supported recursive patterns
-
- return (this.props.renderDepth > 12 || !layoutFrame || !this.layoutDoc || this.layoutDoc[AclSym] === AclPrivate) ? (null) :
+ return (this.props.renderDepth > 12 || !layoutFrame || !this.layoutDoc || GetEffectiveAcl(this.layoutDoc) === AclPrivate) ? (null) :
<ObserverJsxParser
key={42}
blacklistedAttrs={[]}
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 97e3bc01c..1a9ff1ba8 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -4,7 +4,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { action, computed, observable, runInAction } from "mobx";
import { observer } from "mobx-react";
import * as rp from "request-promise";
-import { Doc, DocListCast, HeightSym, Opt, WidthSym, DataSym, AclSym, AclReadonly, AclPrivate } from "../../../fields/Doc";
+import { Doc, DocListCast, HeightSym, Opt, WidthSym, DataSym, AclPrivate, AclReadonly } from "../../../fields/Doc";
import { Document } from '../../../fields/documentSchemas';
import { Id } from '../../../fields/FieldSymbols';
import { InkTool } from '../../../fields/InkField';
@@ -12,7 +12,7 @@ import { listSpec } from "../../../fields/Schema";
import { SchemaHeaderField } from '../../../fields/SchemaHeaderField';
import { ScriptField } from '../../../fields/ScriptField';
import { BoolCast, Cast, NumCast, StrCast } from "../../../fields/Types";
-import { TraceMobx } from '../../../fields/util';
+import { TraceMobx, GetEffectiveAcl, getPlaygroundMode, togglePlaygroundMode } from '../../../fields/util';
import { GestureUtils } from '../../../pen-gestures/GestureUtils';
import { emptyFunction, OmitKeys, returnOne, returnTransparent, Utils, emptyPath } from "../../../Utils";
import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils';
@@ -26,7 +26,7 @@ import { InteractionUtils } from '../../util/InteractionUtils';
import { Scripting } from '../../util/Scripting';
import { SearchUtil } from '../../util/SearchUtil';
import { SelectionManager } from "../../util/SelectionManager";
-import SharingManager from '../../util/SharingManager';
+import SharingManager, { SharingPermissions } from '../../util/SharingManager';
import { Transform } from "../../util/Transform";
import { undoBatch, UndoManager } from "../../util/UndoManager";
import { CollectionView, CollectionViewType } from '../collections/CollectionView';
@@ -725,7 +725,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
@undoBatch
@action
- setAcl = (acl: "readOnly" | "addOnly" | "ownerOnly" | "write") => {
+ setAcl = (acl: SharingPermissions) => {
this.dataDoc.ACL = this.props.Document.ACL = acl;
DocListCast(this.dataDoc[Doc.LayoutFieldKey(this.dataDoc)]).map(d => {
if (d.author === Doc.CurrentUserEmail) d.ACL = acl;
@@ -735,7 +735,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
@undoBatch
@action
- testAcl = (acl: "readOnly" | "addOnly" | "ownerOnly" | "write") => {
+ testAcl = (acl: SharingPermissions) => {
this.dataDoc.author = this.props.Document.author = "ADMIN";
this.dataDoc.ACL = this.props.Document.ACL = acl;
DocListCast(this.dataDoc[Doc.LayoutFieldKey(this.dataDoc)]).map(d => {
@@ -845,14 +845,16 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
const existingAcls = cm.findByDescription("Privacy...");
const aclItems: ContextMenuProps[] = existingAcls && "subitems" in existingAcls ? existingAcls.subitems : [];
- aclItems.push({ description: "Make Add Only", event: () => this.setAcl("addOnly"), icon: "concierge-bell" });
- aclItems.push({ description: "Make Read Only", event: () => this.setAcl("readOnly"), icon: "concierge-bell" });
- aclItems.push({ description: "Make Private", event: () => this.setAcl("ownerOnly"), icon: "concierge-bell" });
- aclItems.push({ description: "Make Editable", event: () => this.setAcl("write"), icon: "concierge-bell" });
- aclItems.push({ description: "Test Private", event: () => this.testAcl("ownerOnly"), icon: "concierge-bell" });
- aclItems.push({ description: "Test Readonly", event: () => this.testAcl("readOnly"), icon: "concierge-bell" });
+ aclItems.push({ description: "Make Add Only", event: () => this.setAcl(SharingPermissions.Add), icon: "concierge-bell" });
+ aclItems.push({ description: "Make Read Only", event: () => this.setAcl(SharingPermissions.View), icon: "concierge-bell" });
+ aclItems.push({ description: "Make Private", event: () => this.setAcl(SharingPermissions.None), icon: "concierge-bell" });
+ aclItems.push({ description: "Make Editable", event: () => this.setAcl(SharingPermissions.Edit), icon: "concierge-bell" });
+ aclItems.push({ description: "Test Private", event: () => this.testAcl(SharingPermissions.None), icon: "concierge-bell" });
+ aclItems.push({ description: "Test Readonly", event: () => this.testAcl(SharingPermissions.View), icon: "concierge-bell" });
!existingAcls && cm.addItem({ description: "Privacy...", subitems: aclItems, icon: "question" });
+ cm.addItem({ description: `${getPlaygroundMode() ? "Disable" : "Enable"} playground mode`, event: togglePlaygroundMode, icon: "concierge-bell" });
+
// const recommender_subitems: ContextMenuProps[] = [];
// recommender_subitems.push({
@@ -1207,7 +1209,7 @@ 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 (GetEffectiveAcl(this.props.Document) === AclPrivate) return (null);
if (this.props.Document.hidden) 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)));
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 962d0012f..f12215164 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -13,7 +13,7 @@ import { EditorState, NodeSelection, Plugin, TextSelection, Transaction } from "
import { ReplaceStep } from 'prosemirror-transform';
import { EditorView } from "prosemirror-view";
import { DateField } from '../../../../fields/DateField';
-import { DataSym, Doc, DocListCast, DocListCastAsync, Field, HeightSym, Opt, WidthSym, AclSym } from "../../../../fields/Doc";
+import { DataSym, Doc, DocListCast, DocListCastAsync, Field, HeightSym, Opt, WidthSym, AclEdit } from "../../../../fields/Doc";
import { documentSchema } from '../../../../fields/documentSchemas';
import applyDevTools = require("prosemirror-dev-tools");
import { removeMarkWithAttrs } from "./prosemirrorPatches";
@@ -24,7 +24,7 @@ import { RichTextField } from "../../../../fields/RichTextField";
import { RichTextUtils } from '../../../../fields/RichTextUtils';
import { createSchema, makeInterface } from "../../../../fields/Schema";
import { Cast, DateCast, NumCast, StrCast, ScriptCast } from "../../../../fields/Types";
-import { TraceMobx, OVERRIDE_ACL } from '../../../../fields/util';
+import { TraceMobx, OVERRIDE_ACL, GetEffectiveAcl } from '../../../../fields/util';
import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, numberRange, returnOne, returnZero, Utils, setupMoveUpEvents } from '../../../../Utils';
import { GoogleApiClientUtils, Pulls, Pushes } from '../../../apis/google_docs/GoogleApiClientUtils';
import { DocServer } from "../../../DocServer";
@@ -226,7 +226,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
const curProto = Cast(Cast(this.dataDoc.proto, Doc, null)?.[this.fieldKey], RichTextField, null); // the default text inherited from a prototype
const curLayout = this.rootDoc !== this.layoutDoc ? Cast(this.layoutDoc[this.fieldKey], RichTextField, null) : undefined; // the default text stored in a layout template
const json = JSON.stringify(state.toJSON());
- if (!this.dataDoc[AclSym]) {
+ if (GetEffectiveAcl(this.dataDoc) === AclEdit) {
if (!this._applyingChange && json.replace(/"selection":.*/, "") !== curProto?.Data.replace(/"selection":.*/, "")) {
this._applyingChange = true;
(curText !== Cast(this.dataDoc[this.fieldKey], RichTextField)?.Text) && (this.dataDoc[this.props.fieldKey + "-lastModified"] = new DateField(new Date(Date.now())));