aboutsummaryrefslogtreecommitdiff
path: root/src/client/util
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/util')
-rw-r--r--src/client/util/CurrentUserUtils.ts76
-rw-r--r--src/client/util/DocumentManager.ts2
-rw-r--r--src/client/util/GroupManager.scss1
-rw-r--r--src/client/util/GroupManager.tsx54
-rw-r--r--src/client/util/HypothesisUtils.ts170
-rw-r--r--src/client/util/Scripting.ts2
-rw-r--r--src/client/util/SearchUtil.ts2
-rw-r--r--src/client/util/SettingsManager.scss265
-rw-r--r--src/client/util/SettingsManager.tsx248
-rw-r--r--src/client/util/SharingManager.scss1
-rw-r--r--src/client/util/SharingManager.tsx144
11 files changed, 683 insertions, 282 deletions
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 8023df8b4..b2fb1e33a 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -8,7 +8,7 @@ import { Doc, DocListCast, DocListCastAsync, DataSym } from "../../fields/Doc";
import { List } from "../../fields/List";
import { listSpec } from "../../fields/Schema";
import { ScriptField, ComputedField } from "../../fields/ScriptField";
-import { Cast, PromiseValue, StrCast, NumCast } from "../../fields/Types";
+import { Cast, PromiseValue, StrCast, NumCast, BoolCast } from "../../fields/Types";
import { nullAudio } from "../../fields/URLField";
import { DragManager } from "./DragManager";
import { Scripting } from "./Scripting";
@@ -45,7 +45,7 @@ export class CurrentUserUtils {
if (doc["template-button-query"] === undefined) {
const queryTemplate = Docs.Create.MulticolumnDocument(
[
- Docs.Create.QueryDocument({ title: "query", _height: 200 }),
+ Docs.Create.SearchDocument({ _viewType: CollectionViewType.Schema, ignoreClick: true, forceActive: true, lockedPosition: true, title: "query", _height: 200 }),
Docs.Create.FreeformDocument([], { title: "data", _height: 100 })
],
{ _width: 400, _height: 300, title: "queryView", _chromeStatus: "disabled", _xMargin: 3, _yMargin: 3, hideFilterView: true }
@@ -248,8 +248,8 @@ export class CurrentUserUtils {
if (doc["template-buttons"] === undefined) {
doc["template-buttons"] = new PrefetchProxy(Docs.Create.MasonryDocument(requiredTypes, {
title: "Advanced Item Prototypes", _xMargin: 0, _showTitle: "title",
- hidden: ComputedField.MakeFunction("self.target.noviceMode") as any,
- target: doc,
+ hidden: ComputedField.MakeFunction("self.userDoc.noviceMode") as any,
+ userDoc: doc,
_autoHeight: true, _width: 500, _columnWidth: 35, ignoreClick: true, lockedPosition: true, _chromeStatus: "disabled",
dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }),
}));
@@ -387,7 +387,7 @@ export class CurrentUserUtils {
static creatorBtnDescriptors(doc: Doc): {
title: string, toolTip: string, icon: string, drag?: string, ignoreClick?: boolean,
- click?: string, ischecked?: string, activeInkPen?: Doc, backgroundColor?: string, dragFactory?: Doc
+ click?: string, ischecked?: string, activeInkPen?: Doc, backgroundColor?: string, dragFactory?: Doc, noviceMode?: boolean
}[] {
if (doc.emptyPresentation === undefined) {
doc.emptyPresentation = Docs.Create.PresDocument(new List<Doc>(),
@@ -415,31 +415,28 @@ export class CurrentUserUtils {
if (doc.emptyButton === undefined) {
doc.emptyButton = Docs.Create.ButtonDocument({ _width: 150, _height: 50, _xPadding: 10, _yPadding: 10, title: "Button" });
}
- if (doc.emptySearch === undefined) {
- doc.emptySearch = Docs.Create.QueryDocument({ _width: 200, title: "empty search" });
- }
if (doc.emptyDocHolder === undefined) {
doc.emptyDocHolder = Docs.Create.DocumentDocument(
ComputedField.MakeFunction("selectedDocs(this,this.excludeCollections,[_last_])?.[0]") as any,
{ _width: 250, _height: 250, title: "container" });
}
if (doc.emptyWebpage === undefined) {
- doc.emptyWebpage = Docs.Create.WebDocument("", { title: "webpage", _nativeWidth: 850, _nativeHeight: 962, _width: 600, UseCors: true });
+ doc.emptyWebpage = Docs.Create.WebDocument("", { title: "webpage", _nativeWidth: 850, _nativeHeight: 962, _width: 400, UseCors: true });
}
if (doc.activeMobileMenu === undefined) {
this.setupActiveMobileMenu(doc);
}
return [
- { toolTip: "Drag a collection", title: "Col", icon: "folder", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyCollection as Doc },
- { toolTip: "Drag a web page", title: "Web", icon: "globe-asia", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyWebpage as Doc },
+ { toolTip: "Drag a collection", title: "Col", icon: "folder", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyCollection as Doc, noviceMode: true },
+ { toolTip: "Drag a web page", title: "Web", icon: "globe-asia", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyWebpage as Doc, noviceMode: true },
{ toolTip: "Drag a cat image", title: "Image", icon: "cat", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyImage as Doc },
- { toolTip: "Drag a comparison box", title: "Compare", icon: "columns", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyComparison as Doc },
+ { toolTip: "Drag a comparison box", title: "Compare", icon: "columns", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyComparison as Doc, noviceMode: true },
{ toolTip: "Drag a screengrabber", title: "Grab", icon: "photo-video", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyScreenshot as Doc },
// { title: "Drag a webcam", title: "Cam", icon: "video", ignoreClick: true, drag: 'Docs.Create.WebCamDocument("", { _width: 400, _height: 400, title: "a test cam" })' },
- { toolTip: "Drag a audio recorder", title: "Audio", icon: "microphone", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyAudio as Doc },
- { toolTip: "Drag a button", title: "Button", icon: "bolt", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyButton as Doc },
+ { toolTip: "Drag a audio recorder", title: "Audio", icon: "microphone", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyAudio as Doc, noviceMode: true },
+ { toolTip: "Drag a button", title: "Button", icon: "bolt", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyButton as Doc, noviceMode: true },
- { toolTip: "Drag a presentation view", title: "Prezi", icon: "tv", click: 'openOnRight(Doc.UserDoc().activePresentation = getCopy(this.dragFactory, true))', drag: `Doc.UserDoc().activePresentation = getCopy(this.dragFactory, true)`, dragFactory: doc.emptyPresentation as Doc },
+ { toolTip: "Drag a presentation view", title: "Present", icon: "tv", click: 'openOnRight(Doc.UserDoc().activePresentation = getCopy(this.dragFactory, true))', drag: `Doc.UserDoc().activePresentation = getCopy(this.dragFactory, true)`, dragFactory: doc.emptyPresentation as Doc, noviceMode: true },
{ toolTip: "Drag a search box", title: "Query", icon: "search", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptySearch as Doc },
{ toolTip: "Drag a scripting box", title: "Script", icon: "terminal", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyScript as Doc },
// { title: "Drag an import folder", title: "Load", icon: "cloud-upload-alt", ignoreClick: true, drag: 'Docs.Create.DirectoryImportDocument({ title: "Directory Import", _width: 400, _height: 400 })' },
@@ -450,8 +447,9 @@ export class CurrentUserUtils {
// { title: "use stamp", icon: "stamp", click: 'activateStamp(this.activeInkPen = sameDocs(this.activeInkPen, this) ? undefined : this)', backgroundColor: "orange", ischecked: `sameDocs(this.activeInkPen, this)`, activeInkPen: doc },
// { title: "use eraser", icon: "eraser", click: 'activateEraser(this.activeInkPen = sameDocs(this.activeInkPen, this) ? undefined : this);', ischecked: `sameDocs(this.activeInkPen, this)`, backgroundColor: "pink", activeInkPen: doc },
// { title: "use drag", icon: "mouse-pointer", click: 'deactivateInk();this.activeInkPen = this;', ischecked: `sameDocs(this.activeInkPen, this)`, backgroundColor: "white", activeInkPen: doc },
- { toolTip: "Drag a document previewer", title: "Prev", icon: "expand", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyDocHolder as Doc },
+ { toolTip: "Drag a document previewer", title: "Prev", icon: "expand", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory,true)', dragFactory: doc.emptyDocHolder as Doc },
{ toolTip: "Toggle a Calculator REPL", title: "repl", icon: "calculator", click: 'addOverlayWindow("ScriptingRepl", { x: 300, y: 100, width: 200, height: 200, title: "Scripting REPL" })' },
+ { toolTip: "Connect a Google Account", title: "Google Account", icon: "external-link-alt", click: 'GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken(true)' },
];
}
@@ -468,7 +466,7 @@ export class CurrentUserUtils {
}
}
const buttons = CurrentUserUtils.creatorBtnDescriptors(doc).filter(d => !alreadyCreatedButtons?.includes(d.title));
- const creatorBtns = buttons.map(({ title, toolTip, icon, ignoreClick, drag, click, ischecked, activeInkPen, backgroundColor, dragFactory }) => Docs.Create.FontIconDocument({
+ const creatorBtns = buttons.map(({ title, toolTip, icon, ignoreClick, drag, click, ischecked, activeInkPen, backgroundColor, dragFactory, noviceMode }) => Docs.Create.FontIconDocument({
_nativeWidth: 50, _nativeHeight: 50, _width: 50, _height: 50,
icon,
title,
@@ -482,6 +480,8 @@ export class CurrentUserUtils {
backgroundColor,
removeDropProperties: new List<string>(["dropAction"]),
dragFactory,
+ userDoc: noviceMode ? undefined as any : doc,
+ hidden: noviceMode ? undefined as any : ComputedField.MakeFunction("self.userDoc.noviceMode")
}));
if (dragCreatorSet === undefined) {
@@ -512,6 +512,14 @@ export class CurrentUserUtils {
];
}
+ static setupSearchPanel(doc: Doc) {
+ if (doc["search-panel"] === undefined) {
+ doc["search-panel"] = new PrefetchProxy(Docs.Create.SearchDocument({
+ _width: 500, _height: 400, backgroundColor: "dimGray", ignoreClick: true,
+ childDropAction: "alias", lockedPosition: true, _viewType: CollectionViewType.Schema, _chromeStatus: "disabled", title: "sidebar search stack",
+ })) as any as Doc;
+ }
+ }
static setupMenuPanel(doc: Doc) {
if (doc.menuStack === undefined) {
const menuBtns = CurrentUserUtils.menuBtnDescriptions().map(({ title, icon, click }) =>
@@ -527,8 +535,8 @@ export class CurrentUserUtils {
onClick: ScriptField.MakeScript(click, { scriptContext: "any" }),
}));
const userDoc = menuBtns[menuBtns.length - 1];
- userDoc.target = doc;
- userDoc.hidden = ComputedField.MakeFunction("self.target.noviceMode");
+ userDoc.userDoc = doc;
+ userDoc.hidden = ComputedField.MakeFunction("self.userDoc.noviceMode");
doc.menuStack = new PrefetchProxy(Docs.Create.StackingDocument(menuBtns, {
title: "menuItemPanel",
@@ -546,7 +554,7 @@ export class CurrentUserUtils {
btn.color = "white";
btn._backgroundColor = "";
}));
- })
+ });
});
return doc.menuStack as Doc;
}
@@ -652,7 +660,7 @@ export class CurrentUserUtils {
// setup the Creator button which will display the creator panel. This panel will include the drag creators and the color picker.
// when clicked, this panel will be displayed in the target container (ie, sidebarContainer)
- static async setupToolsBtnPanel(doc: Doc, sidebarContainer: Doc) {
+ static async setupToolsBtnPanel(doc: Doc) {
// setup a masonry view of all he creators
const creatorBtns = await CurrentUserUtils.setupCreatorButtons(doc);
const templateBtns = CurrentUserUtils.setupUserTemplateButtons(doc);
@@ -750,6 +758,8 @@ export class CurrentUserUtils {
})) as any as Doc;
}
}
+
+
static setupUserDoc(doc: Doc) {
if (doc["sidebar-userDoc"] === undefined) {
doc.treeViewOpen = true;
@@ -763,19 +773,19 @@ export class CurrentUserUtils {
}
static setupSidebarContainer(doc: Doc) {
- if (doc["sidebar"] === undefined) {
+ if (doc.sidebar === undefined) {
const sidebarContainer = new Doc();
sidebarContainer._chromeStatus = "disabled";
sidebarContainer.onClick = ScriptField.MakeScript("freezeSidebar()");
- doc["sidebar"] = new PrefetchProxy(sidebarContainer);
+ doc.sidebar = new PrefetchProxy(sidebarContainer);
}
- return doc["sidebar"] as Doc;
+ return doc.sidebar as Doc;
}
// setup the list of sidebar mode buttons which determine what is displayed in the sidebar
static async setupSidebarButtons(doc: Doc) {
- const sidebarContainer = CurrentUserUtils.setupSidebarContainer(doc);
- await CurrentUserUtils.setupToolsBtnPanel(doc, sidebarContainer);
+ CurrentUserUtils.setupSidebarContainer(doc);
+ await CurrentUserUtils.setupToolsBtnPanel(doc);
CurrentUserUtils.setupWorkspaces(doc);
CurrentUserUtils.setupCatalog(doc);
CurrentUserUtils.setupRecentlyClosed(doc);
@@ -813,6 +823,11 @@ export class CurrentUserUtils {
// the initial presentation Doc to use
static setupDefaultPresentation(doc: Doc) {
+ if (doc["template-presentation"] === undefined) {
+ doc["template-presentation"] = new PrefetchProxy(Docs.Create.PresElementBoxDocument({
+ title: "pres element template", backgroundColor: "transparent", _xMargin: 5, _height: 46, isTemplateDoc: true, isTemplateForField: "data"
+ }));
+ }
if (doc.activePresentation === undefined) {
doc.activePresentation = Docs.Create.PresDocument(new List<Doc>(), {
title: "Presentation", _viewType: CollectionViewType.Stacking, targetDropAction: "alias",
@@ -822,9 +837,9 @@ export class CurrentUserUtils {
}
// Right sidebar is where mobile uploads are contained
- static setupRightSidebar(doc: Doc) {
+ static setupSharingSidebar(doc: Doc) {
if (doc["sidebar-sharing"] === undefined) {
- doc["sidebar-sharing"] = new PrefetchProxy(Docs.Create.StackingDocument([], { title: "Mobile Uploads" }));
+ doc["sidebar-sharing"] = new PrefetchProxy(Docs.Create.StackingDocument([], { title: "Shared Documents", childDropAction: "alias" }));
}
}
@@ -890,14 +905,17 @@ export class CurrentUserUtils {
doc.fontFamily = StrCast(doc.fontFamily, "Arial");
doc.fontColor = StrCast(doc.fontColor, "black");
doc.fontHighlight = StrCast(doc.fontHighlight, "");
+ doc.defaultColor = StrCast(doc.defaultColor, "white");
+ doc.noviceMode = BoolCast(doc.noviceMode, true);
doc["constants-snapThreshold"] = NumCast(doc["constants-snapThreshold"], 10); //
doc["constants-dragThreshold"] = NumCast(doc["constants-dragThreshold"], 4); //
Utils.DRAG_THRESHOLD = NumCast(doc["constants-dragThreshold"]);
this.setupDefaultIconTemplates(doc); // creates a set of icon templates triggered by the document deoration icon
this.setupDocTemplates(doc); // sets up the template menu of templates
- this.setupRightSidebar(doc); // sets up the right sidebar collection for mobile upload documents and sharing
+ this.setupSharingSidebar(doc); // sets up the right sidebar collection for mobile upload documents and sharing
this.setupActiveMobileMenu(doc); // sets up the current mobile menu for Dash Mobile
this.setupMenuPanel(doc);
+ this.setupSearchPanel(doc);
this.setupOverlays(doc); // documents in overlay layer
this.setupDockedButtons(doc); // the bottom bar of font icons
this.setupDefaultPresentation(doc); // presentation that's initially triggered
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 523dbfca0..bd57e7f48 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -152,7 +152,7 @@ export class DocumentManager {
const first = getFirstDocView(annotatedDoc);
if (first) {
annotatedDoc = first.props.Document;
- docView?.props.focus(annotatedDoc, false);
+ first.props.focus(annotatedDoc, false);
}
}
if (docView) { // we have a docView already and aren't forced to create a new one ... just focus on the document. TODO move into view if necessary otherwise just highlight?
diff --git a/src/client/util/GroupManager.scss b/src/client/util/GroupManager.scss
index 51e4fa9e2..9438bdd72 100644
--- a/src/client/util/GroupManager.scss
+++ b/src/client/util/GroupManager.scss
@@ -92,6 +92,7 @@
.sort-groups {
text-align: left;
margin-left: 5;
+ width: 50px;
cursor: pointer;
}
diff --git a/src/client/util/GroupManager.tsx b/src/client/util/GroupManager.tsx
index 229a846a9..d03989675 100644
--- a/src/client/util/GroupManager.tsx
+++ b/src/client/util/GroupManager.tsx
@@ -18,7 +18,7 @@ import { setGroups } from "../../fields/util";
import { DocServer } from "../DocServer";
import { TaskCompletionBox } from "../views/nodes/TaskCompletedBox";
-library.add(fa.faPlus, fa.faTimes, fa.faInfoCircle);
+library.add(fa.faPlus, fa.faTimes, fa.faInfoCircle, fa.faCaretUp, fa.faCaretRight, fa.faCaretDown);
/**
* Interface for options for the react-select component
@@ -42,6 +42,7 @@ export default class GroupManager extends React.Component<{}> {
private currentUserGroups: string[] = []; // the list of groups the current user is a member of
@observable private buttonColour: "#979797" | "black" = "#979797";
@observable private groupSort: "ascending" | "descending" | "none" = "none";
+ private populating: boolean = false;
@@ -62,21 +63,24 @@ export default class GroupManager extends React.Component<{}> {
* Fetches the list of users stored on the database.
*/
populateUsers = async () => {
- runInAction(() => this.users = []);
- const userList = await RequestPromise.get(Utils.prepend("/getUsers"));
- const raw = JSON.parse(userList) as User[];
- const evaluating = raw.map(async user => {
- const userDocument = await DocServer.GetRefField(user.userDocumentId);
- if (userDocument instanceof Doc) {
- const notificationDoc = await Cast(userDocument["sidebar-sharing"], Doc);
- runInAction(() => {
- if (notificationDoc instanceof Doc) {
- this.users.push(user.email);
- }
- });
- }
- });
- return Promise.all(evaluating);
+ if (!this.populating) {
+ this.populating = true;
+ runInAction(() => this.users = []);
+ const userList = await RequestPromise.get(Utils.prepend("/getUsers"));
+ const raw = JSON.parse(userList) as User[];
+ const evaluating = raw.map(async user => {
+ const userDocument = await DocServer.GetRefField(user.userDocumentId);
+ if (userDocument instanceof Doc) {
+ const notificationDoc = await Cast(userDocument["sidebar-sharing"], Doc);
+ runInAction(() => {
+ if (notificationDoc instanceof Doc) {
+ this.users.push(user.email);
+ }
+ });
+ }
+ });
+ return Promise.all(evaluating).then(() => this.populating = false);
+ }
}
/**
@@ -88,7 +92,6 @@ export default class GroupManager extends React.Component<{}> {
const members: string[] = JSON.parse(StrCast(group.members));
if (members.includes(Doc.CurrentUserEmail)) this.currentUserGroups.push(StrCast(group.groupName));
});
-
setGroups(this.currentUserGroups);
});
}
@@ -120,6 +123,7 @@ export default class GroupManager extends React.Component<{}> {
this.currentGroup = undefined;
// this.users = [];
this.createGroupModalOpen = false;
+ TaskCompletionBox.taskCompleted = false;
}
/**
@@ -132,7 +136,6 @@ export default class GroupManager extends React.Component<{}> {
/**
* @returns a list of all group documents.
*/
- // private ?
getAllGroups(): Doc[] {
const groupDoc = this.GroupManagerDoc;
return groupDoc ? DocListCast(groupDoc.data) : [];
@@ -142,7 +145,6 @@ export default class GroupManager extends React.Component<{}> {
* @returns a group document based on the group name.
* @param groupName
*/
- // private?
getGroup(groupName: string): Doc | undefined {
const groupDoc = this.getAllGroups().find(group => group.groupName === groupName);
return groupDoc;
@@ -209,8 +211,6 @@ export default class GroupManager extends React.Component<{}> {
deleteGroup(group: Doc): boolean {
if (group) {
if (this.GroupManagerDoc && this.hasEditAccess(group)) {
- // TODO look at this later
- // SharingManager.Instance.setInternalGroupSharing(group, "Not Shared");
Doc.RemoveDocFromList(this.GroupManagerDoc, "data", group);
SharingManager.Instance.removeGroup(group);
const members: string[] = JSON.parse(StrCast(group.members));
@@ -311,7 +311,9 @@ export default class GroupManager extends React.Component<{}> {
<div className="group-create">
<div className="group-heading" style={{ marginBottom: 0 }}>
<p><b>New Group</b></p>
- <div className={"close-button"} onClick={action(() => this.createGroupModalOpen = false)}>
+ <div className={"close-button"} onClick={action(() => {
+ this.createGroupModalOpen = false; TaskCompletionBox.taskCompleted = false;
+ })}>
<FontAwesomeIcon icon={fa.faTimes} color={"black"} size={"lg"} />
</div>
</div>
@@ -319,6 +321,7 @@ export default class GroupManager extends React.Component<{}> {
className="group-input"
ref={this.inputRef}
onKeyDown={this.handleKeyDown}
+ autoFocus
type="text"
placeholder="Group name"
onChange={action(() => this.buttonColour = this.inputRef.current?.value ? "black" : "#979797")} />
@@ -363,7 +366,7 @@ export default class GroupManager extends React.Component<{}> {
interactive={true}
contents={contents}
dialogueBoxStyle={{ width: "90%", height: "70%" }}
- closeOnExternalClick={action(() => this.createGroupModalOpen = false)}
+ closeOnExternalClick={action(() => { this.createGroupModalOpen = false; this.selectedUsers = null; TaskCompletionBox.taskCompleted = false; })}
/>
);
}
@@ -405,7 +408,10 @@ export default class GroupManager extends React.Component<{}> {
<div
className="sort-groups"
onClick={action(() => this.groupSort = this.groupSort === "ascending" ? "descending" : this.groupSort === "descending" ? "none" : "ascending")}>
- Name {this.groupSort === "ascending" ? "↑" : this.groupSort === "descending" ? "↓" : ""}
+ Name {this.groupSort === "ascending" ? <FontAwesomeIcon icon={fa.faCaretUp} size={"xs"} />
+ : this.groupSort === "descending" ? <FontAwesomeIcon icon={fa.faCaretDown} size={"xs"} />
+ : <FontAwesomeIcon icon={fa.faCaretRight} size={"xs"} />
+ }
</div>
<div className="group-body">
{groups.map(group =>
diff --git a/src/client/util/HypothesisUtils.ts b/src/client/util/HypothesisUtils.ts
new file mode 100644
index 000000000..9ede94e4b
--- /dev/null
+++ b/src/client/util/HypothesisUtils.ts
@@ -0,0 +1,170 @@
+import { StrCast, Cast } from "../../fields/Types";
+import { SearchUtil } from "./SearchUtil";
+import { action, runInAction } from "mobx";
+import { Doc, Opt } from "../../fields/Doc";
+import { DocumentType } from "../documents/DocumentTypes";
+import { Docs } from "../documents/Documents";
+import { SelectionManager } from "./SelectionManager";
+import { WebField } from "../../fields/URLField";
+import { DocumentManager } from "./DocumentManager";
+import { DocumentLinksButton } from "../views/nodes/DocumentLinksButton";
+import { simulateMouseClick, Utils } from "../../Utils";
+import { DocumentView } from "../views/nodes/DocumentView";
+import { Id } from "../../fields/FieldSymbols";
+
+export namespace Hypothesis {
+
+ /**
+ * Retrieve a WebDocument with the given url, prioritizing results that are on screen.
+ * If none exist, create and return a new WebDocument.
+ */
+ export const getSourceWebDoc = async (uri: string) => {
+ const result = await findWebDoc(uri);
+ console.log(result ? "existing doc found" : "existing doc NOT found");
+ return result || Docs.Create.WebDocument(uri, { title: uri, _nativeWidth: 850, _nativeHeight: 962, _width: 400, UseCors: true }); // create and return a new Web doc with given uri if no matching docs are found
+ };
+
+
+ /**
+ * Search for a WebDocument whose url field matches the given uri, return undefined if not found
+ */
+ export const findWebDoc = async (uri: string) => {
+ const currentDoc = SelectionManager.SelectedDocuments().length && SelectionManager.SelectedDocuments()[0].props.Document;
+ if (currentDoc && Cast(currentDoc.data, WebField)?.url.href === uri) return currentDoc; // always check first whether the currently selected doc is the annotation's source, only use Search otherwise
+
+ const results: Doc[] = [];
+ await SearchUtil.Search("web", true).then(action(async (res: SearchUtil.DocSearchResult) => {
+ const docs = await Promise.all(res.docs.map(async doc => (await Cast(doc.extendsDoc, Doc)) || doc));
+ const filteredDocs = docs.filter(doc =>
+ doc.author === Doc.CurrentUserEmail && doc.type === DocumentType.WEB && doc.data
+ );
+ filteredDocs.forEach(doc => {
+ uri === Cast(doc.data, WebField)?.url.href && results.push(doc); // TODO check visited sites history?
+ });
+ }));
+
+ const onScreenResults = results.filter(doc => DocumentManager.Instance.getFirstDocumentView(doc));
+ return onScreenResults.length ? onScreenResults[0] : (results.length ? results[0] : undefined); // prioritize results that are currently on the screen
+ };
+
+ /**
+ * listen for event from Hypothes.is plugin to link an annotation to Dash
+ */
+ export const linkListener = async (e: any) => {
+ const annotationId: string = e.detail.id;
+ const annotationUri: string = StrCast(e.detail.uri).split("#annotations:")[0]; // clean hypothes.is URLs that reference a specific annotation
+ const sourceDoc: Doc = await getSourceWebDoc(annotationUri);
+
+ if (!DocumentLinksButton.StartLink || sourceDoc === DocumentLinksButton.StartLink) { // start new link if there were none already started, or if the old startLink came from the same web document (prevent links to itself)
+ runInAction(() => {
+ DocumentLinksButton.AnnotationId = annotationId;
+ DocumentLinksButton.AnnotationUri = annotationUri;
+ DocumentLinksButton.StartLink = sourceDoc;
+ });
+ } else { // if a link has already been started, complete the link to sourceDoc
+ runInAction(() => {
+ DocumentLinksButton.AnnotationId = annotationId;
+ DocumentLinksButton.AnnotationUri = annotationUri;
+ });
+ const endLinkView = DocumentManager.Instance.getFirstDocumentView(sourceDoc);
+ const rect = document.body.getBoundingClientRect();
+ const x = rect.x + rect.width / 2;
+ const y = 250;
+ DocumentLinksButton.finishLinkClick(x, y, DocumentLinksButton.StartLink, sourceDoc, false, endLinkView);
+ }
+ };
+
+ /**
+ * Send message to Hypothes.is client to edit an annotation to add a Dash hyperlink
+ */
+ export const makeLink = async (title: string, url: string, annotationId: string, annotationSourceDoc: Doc) => {
+ // if the annotation's source webpage isn't currently loaded in Dash, we're not able to access and edit the annotation from the client
+ // so we're loading the webpage and its annotations invisibly in a WebBox in MainView.tsx, until the editing is done
+ !DocumentManager.Instance.getFirstDocumentView(annotationSourceDoc) && runInAction(() => DocumentLinksButton.invisibleWebDoc = annotationSourceDoc);
+
+ var success = false;
+ const onSuccess = action(() => {
+ console.log("Edit success!!");
+ success = true;
+ clearTimeout(interval);
+ DocumentLinksButton.invisibleWebDoc = undefined;
+ document.removeEventListener("editSuccess", onSuccess);
+ });
+
+ const newHyperlink = `[${title}\n](${url})`;
+ const interval = setInterval(() => // keep trying to edit until annotations have loaded and editing is successful
+ !success && document.dispatchEvent(new CustomEvent<{ newHyperlink: string, id: string }>("addLink", {
+ detail: { newHyperlink: newHyperlink, id: annotationId },
+ bubbles: true
+ })), 300);
+
+ setTimeout(action(() => {
+ if (!success) {
+ clearInterval(interval);
+ DocumentLinksButton.invisibleWebDoc = undefined;
+ }
+ }), 10000); // give up if no success after 10s
+ document.addEventListener("editSuccess", onSuccess);
+ };
+
+ /**
+ * Send message Hypothes.is client request to edit an annotation to find and delete the target Dash hyperlink
+ */
+ export const deleteLink = async (linkDoc: Doc, sourceDoc: Doc, destinationDoc: Doc) => {
+ if (Cast(destinationDoc.data, WebField)?.url.href !== StrCast(linkDoc.annotationUri)) return; // check that the destinationDoc is a WebDocument containing the target annotation
+
+ !DocumentManager.Instance.getFirstDocumentView(destinationDoc) && runInAction(() => DocumentLinksButton.invisibleWebDoc = destinationDoc); // see note in makeLink
+
+ var success = false;
+ const onSuccess = action(() => {
+ console.log("Edit success!");
+ success = true;
+ clearTimeout(interval);
+ DocumentLinksButton.invisibleWebDoc = undefined;
+ document.removeEventListener("editSuccess", onSuccess);
+ });
+
+ const annotationId = StrCast(linkDoc.annotationId);
+ const linkUrl = Utils.prepend("/doc/" + sourceDoc[Id]);
+ const interval = setInterval(() => {// keep trying to edit until annotations have loaded and editing is successful
+ !success && document.dispatchEvent(new CustomEvent<{ targetUrl: string, id: string }>("deleteLink", {
+ detail: { targetUrl: linkUrl, id: annotationId },
+ bubbles: true
+ }));
+ }, 300);
+
+ setTimeout(action(() => {
+ if (!success) {
+ clearInterval(interval);
+ DocumentLinksButton.invisibleWebDoc = undefined;
+ }
+ }), 10000); // give up if no success after 10s
+ document.addEventListener("editSuccess", onSuccess);
+ };
+
+ /**
+ * Send message to Hypothes.is client to scroll to an annotation when it loads
+ */
+ export const scrollToAnnotation = (annotationId: string, target: Doc) => {
+ var success = false;
+ const onSuccess = () => {
+ console.log("Scroll success!!");
+ document.removeEventListener('scrollSuccess', onSuccess);
+ clearInterval(interval);
+ success = true;
+ };
+
+ const interval = setInterval(() => { // keep trying to scroll every 250ms until annotations have loaded and scrolling is successful
+ document.dispatchEvent(new CustomEvent('scrollToAnnotation', {
+ detail: annotationId,
+ bubbles: true
+ }));
+ const targetView: Opt<DocumentView> = DocumentManager.Instance.getFirstDocumentView(target);
+ const position = targetView?.props.ScreenToLocalTransform().inverse().transformPoint(0, 0);
+ targetView && position && simulateMouseClick(targetView.ContentDiv!, position[0], position[1], position[0], position[1], false);
+ }, 300);
+
+ document.addEventListener('scrollSuccess', onSuccess); // listen for success message from client
+ setTimeout(() => !success && clearInterval(interval), 10000); // give up if no success after 10s
+ };
+} \ No newline at end of file
diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts
index f1e6155d2..cb0a4bea0 100644
--- a/src/client/util/Scripting.ts
+++ b/src/client/util/Scripting.ts
@@ -134,7 +134,7 @@ export function scriptingGlobal(constructor: { new(...args: any[]): any }) {
Scripting.addGlobal(constructor);
}
-const _scriptingGlobals: { [name: string]: any } = {};
+export const _scriptingGlobals: { [name: string]: any } = {};
let scriptingGlobals: { [name: string]: any } = _scriptingGlobals;
const _scriptingDescriptions: { [name: string]: any } = {};
const _scriptingParams: { [name: string]: any } = {};
diff --git a/src/client/util/SearchUtil.ts b/src/client/util/SearchUtil.ts
index 0a01d8ac7..7b2c601fe 100644
--- a/src/client/util/SearchUtil.ts
+++ b/src/client/util/SearchUtil.ts
@@ -29,6 +29,8 @@ export namespace SearchUtil {
rows?: number;
fq?: string;
allowAliases?: boolean;
+ "facet"?: string;
+ "facet.field"?: string;
}
export function Search(query: string, returnDocs: true, options?: SearchParams): Promise<DocSearchResult>;
export function Search(query: string, returnDocs: false, options?: SearchParams): Promise<IdSearchResult>;
diff --git a/src/client/util/SettingsManager.scss b/src/client/util/SettingsManager.scss
index 6923fe879..01dda0aca 100644
--- a/src/client/util/SettingsManager.scss
+++ b/src/client/util/SettingsManager.scss
@@ -1,7 +1,7 @@
@import "../views/globalCssVariables";
.settings-interface {
- background-color: whitesmoke !important;
+ //background-color: whitesmoke !important;
color: grey;
width: 450px;
height: 300px;
@@ -22,85 +22,248 @@
}
}
-.settings-interface {
+.settings-title {
+ font-size: 25px;
+ font-weight: bold;
+ padding-right: 10px;
+ color: black;
+}
+
+.settings-username {
+ font-size: 12px;
+ padding-right: 15px;
+ color: black;
+ margin-top: 4px;
+ /* right: 135; */
+ position: absolute;
+ left: 235;
+}
+
+.settings-section {
display: flex;
- flex-direction: column;
+ border-bottom: 1px solid grey;
+ padding-bottom: 8px;
+ padding-top: 6px;
- button {
- width: 100%;
- align-self: center;
- background: #252b33;
- margin-top: 4px;
+ .settings-section-title {
+ font-size: 16;
+ font-weight: bold;
+ text-align: left;
+ color: black;
+ width: 80;
+ margin-right: 50px;
+ }
+
+ &:last-child {
+ border-bottom: none;
+ }
+}
+
+
+.password-content {
+ display: flex;
+
+ .password-content-inputs {
+ width: 100;
+
+ .password-inputs {
+ border: none;
+ margin-bottom: 8px;
+ width: 180;
+ color: black;
+ border-radius: 5px;
+ }
+ }
+
+ .password-content-buttons {
+ margin-left: 84px;
+ width: 100;
+
+ .password-submit {
+ margin-left: 85px;
+ }
+
+ .password-forgot {
+ margin-left: 65px;
+ margin-top: -20px;
+ white-space: nowrap;
+ }
+ }
+}
+
+.accounts-content {
+ display: flex;
+}
+
+.modes-content {
+ display: flex;
+
+ .modes-select {
+ // width: 170px;
+ margin-right: 10px;
+ color: black;
+ border-radius: 5px;
&:hover {
- background: $main-accent;
+ cursor: pointer;
}
}
- .delete-button {
- background: rgb(227, 86, 86);
+ .modes-playground,
+ .default-acl {
+ display: flex;
+
+ .playground-check,
+ .acl-check {
+ margin-right: 5px;
+
+ &:hover {
+ cursor: pointer;
+ }
+ }
+
+ .playground-text {
+ color: black;
+ margin-right: 10px;
+ }
+
+ .acl-text {
+ color: black;
+ }
+
}
+}
- .close-button {
- position: absolute;
- right: 1em;
- top: 1em;
+.colorFlyout {
+ margin-top: 2px;
+ margin-right: 25px;
+
+ &:hover {
cursor: pointer;
}
- .settings-heading {
- letter-spacing: .5em;
+ .colorFlyout-button {
+ width: 20px;
+ height: 20px;
+ border: 0.5px solid black;
+ border-radius: 5px;
}
+}
+.preferences-content {
+ display: flex;
+ margin-top: 4px;
- .settings-body {
+ .preferences-color {
display: flex;
- justify-content: space-between;
- margin-top: -10;
- .settings-type {
- display: flex;
- flex-direction: column;
- flex-basis: 45%;
+ .preferences-color-text {
+ color: black;
+ font-size: 11;
+ margin-top: 4;
+ margin-right: 4;
+ }
+ }
+
+ .preferences-font {
+ display: flex;
+ .preferences-font-text {
+ color: black;
+ font-size: 11;
+ margin-top: 4;
+ margin-right: 4;
}
- .settings-content {
- padding-left: 1em;
- padding-right: 1em;
- display: flex;
- flex-direction: column;
- flex-basis: 70%;
- justify-content: space-around;
- text-align: left;
-
- ::placeholder {
- color: $intermediate-color;
- }
+ .font-select {
+ width: 100px;
+ color: black;
+ font-size: 9;
+ margin-right: 6;
+ border-radius: 5px;
- input {
- border-radius: 5px;
- border: none;
- padding: 4px;
- min-width: 100%;
- margin: 2px 0;
+ &:hover {
+ cursor: pointer;
}
+ }
- .error-text {
- color: #C40233;
- }
+ .size-select {
+ width: 60px;
+ color: black;
+ font-size: 9;
+ border-radius: 5px;
- .success-text {
- color: #009F6B;
+ &:hover {
+ cursor: pointer;
}
+ }
+ }
+}
- p {
- padding: 0 0 .1em .2em;
- }
+.settings-interface {
+ display: flex;
+ flex-direction: column;
+
+ button {
+ width: auto;
+ align-self: center;
+ background: #252b33;
+ margin-right: 15px;
+
+ //margin-top: 4px;
+ &:hover {
+ background: $main-accent;
}
}
+ // .delete-button {
+ // background: rgb(227, 86, 86);
+ // }
+
+ .close-button {
+ position: absolute;
+ right: 1em;
+ top: 1em;
+ cursor: pointer;
+ }
+
+ .logout-button {
+ right: 35;
+ position: absolute;
+ }
+
+ .settings-content {
+ background: #e4e4e4;
+ border-radius: 6px;
+ padding: 10px;
+ width: 560px;
+ }
+
+ .settings-top {
+ display: flex;
+ margin-bottom: 10px;
+ }
+
+
+ .error-text {
+ color: #C40233;
+ width: 300;
+ margin-left: -20;
+ font-size: 10;
+ margin-bottom: 4;
+ margin-top: -3;
+ }
+
+ .success-text {
+ width: 300;
+ margin-left: -20;
+ font-size: 10;
+ margin-bottom: 4;
+ margin-top: -3;
+ color: #009F6B;
+ }
+
.focus-span {
text-decoration: underline;
}
@@ -138,8 +301,6 @@
color: black;
}
-
-
}
}
diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx
index 6276fae96..e3b91925a 100644
--- a/src/client/util/SettingsManager.tsx
+++ b/src/client/util/SettingsManager.tsx
@@ -1,8 +1,7 @@
-import { observable, runInAction, action } from "mobx";
+import { observable, runInAction, action, computed } from "mobx";
import * as React from "react";
import MainViewModal from "../views/MainViewModal";
import { observer } from "mobx-react";
-import { library } from '@fortawesome/fontawesome-svg-core';
import * as fa from '@fortawesome/free-solid-svg-icons';
import { SelectionManager } from "./SelectionManager";
import "./SettingsManager.scss";
@@ -12,156 +11,175 @@ import { CurrentUserUtils } from "./CurrentUserUtils";
import { Utils, addStyleSheet, addStyleSheetRule, removeStyleSheetRule } from "../../Utils";
import { Doc } from "../../fields/Doc";
import GroupManager from "./GroupManager";
-import HypothesisAuthenticationManager from "../apis/HypothesisAuthenticationManager";
import GoogleAuthenticationManager from "../apis/GoogleAuthenticationManager";
import { DocServer } from "../DocServer";
-
-library.add(fa.faTimes);
+import { BoolCast, StrCast, NumCast } from "../../fields/Types";
+import { undoBatch } from "./UndoManager";
+import { ColorState, SketchPicker } from "react-color";
+const higflyout = require("@hig/flyout");
+export const { anchorPoints } = higflyout;
+export const Flyout = higflyout.default;
@observer
export default class SettingsManager extends React.Component<{}> {
public static Instance: SettingsManager;
static _settingsStyle = addStyleSheet();
@observable private isOpen = false;
- @observable private dialogueBoxOpacity = 1;
- @observable private overlayOpacity = 0.4;
- @observable private settingsContent = "password";
- @observable private errorText = "";
- @observable private successText = "";
+ @observable private passwordResultText = "";
@observable private playgroundMode = false;
- private curr_password_ref = React.createRef<HTMLInputElement>();
- private new_password_ref = React.createRef<HTMLInputElement>();
- private new_confirm_ref = React.createRef<HTMLInputElement>();
- public open = action(() => {
- SelectionManager.DeselectAll();
- this.isOpen = true;
- });
+ @observable private curr_password = "";
+ @observable private new_password = "";
+ @observable private new_confirm = "";
- public close = action(() => {
- this.isOpen = false;
- });
+ @computed get backgroundColor() { return Doc.UserDoc().defaultColor; }
constructor(props: {}) {
super(props);
SettingsManager.Instance = this;
}
- @action
- private dispatchRequest = async () => {
- const curr_pass = this.curr_password_ref.current?.value;
- const new_pass = this.new_password_ref.current?.value;
- const new_confirm = this.new_confirm_ref.current?.value;
-
- if (!(curr_pass && new_pass && new_confirm)) {
- this.changeAlertText("Hey, we're missing some fields!", "");
- return;
+ public close = action(() => this.isOpen = false);
+ public open = action(() => this.isOpen = true);
+
+ private googleAuthorize = action(() => GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken(true));
+ private changePassword = async () => {
+ if (!(this.curr_password && this.new_password && this.new_confirm)) {
+ runInAction(() => this.passwordResultText = "Error: Hey, we're missing some fields!");
+ } else {
+ const passwordBundle = { curr_pass: this.curr_password, new_pass: this.new_password, new_confirm: this.new_confirm };
+ const { error } = await Networking.PostToServer('/internalResetPassword', passwordBundle);
+ runInAction(() => this.passwordResultText = error ? "Error: " + error[0].msg + "..." : "Password successfully updated!");
}
+ }
- const passwordBundle = {
- curr_pass,
- new_pass,
- new_confirm
- };
-
- const { error } = await Networking.PostToServer('/internalResetPassword', passwordBundle);
- if (error) {
- this.changeAlertText("Uh oh! " + error[0].msg + "...", "");
- return;
+ @undoBatch selectUserMode = action((e: React.ChangeEvent) => Doc.UserDoc().noviceMode = (e.currentTarget as any)?.value === "Novice");
+ @undoBatch changeFontFamily = action((e: React.ChangeEvent) => Doc.UserDoc().fontFamily = (e.currentTarget as any).value);
+ @undoBatch changeFontSize = action((e: React.ChangeEvent) => Doc.UserDoc().fontSize = (e.currentTarget as any).value);
+ @undoBatch switchColor = action((color: ColorState) => Doc.UserDoc().defaultColor = String(color.hex));
+ @undoBatch
+ playgroundModeToggle = action(() => {
+ this.playgroundMode = !this.playgroundMode;
+ if (this.playgroundMode) {
+ DocServer.Control.makeReadOnly();
+ addStyleSheetRule(SettingsManager._settingsStyle, "lm_header", { background: "pink !important" });
}
+ else DocServer.Control.makeEditable();
+ });
- this.changeAlertText("", "Password successfully updated!");
- }
+ @computed get preferencesContent() {
+ const colorBox = <SketchPicker onChange={this.switchColor} color={StrCast(this.backgroundColor)}
+ presetColors={['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505',
+ '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B',
+ '#FFFFFF', '#f1efeb', 'transparent']} />;
- @action
- private changeAlertText = (errortxt: string, successtxt: string) => {
- this.errorText = errortxt;
- this.successText = successtxt;
- }
+ const colorFlyout = <div className="colorFlyout">
+ <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={colorBox}>
+ <div className="colorFlyout-button" style={{ backgroundColor: StrCast(this.backgroundColor) }} onPointerDown={e => e.stopPropagation()} >
+ <FontAwesomeIcon icon="palette" size="sm" color={StrCast(this.backgroundColor)} />
+ </div>
+ </Flyout>
+ </div>;
- @action
- onClick = (event: any) => {
- this.settingsContent = event.currentTarget.value;
- this.errorText = "";
- this.successText = "";
- }
- @action
- noviceToggle = (event: any) => {
- Doc.UserDoc().noviceMode = !Doc.UserDoc().noviceMode;
+ const fontFamilies = ["Times New Roman", "Arial", "Georgia", "Comic Sans MS", "Tahoma", "Impact", "Crimson Text"];
+ const fontSizes = ["7pt", "8pt", "9pt", "10pt", "12pt", "14pt", "16pt", "18pt", "20pt", "24pt", "32pt", "48pt", "72pt"];
+
+ return <div className="preferences-content">
+ <div className="preferences-color">
+ <div className="preferences-color-text">Background Color</div>
+ {colorFlyout}
+ </div>
+ <div className="preferences-font">
+ <div className="preferences-font-text">Default Font</div>
+ <select className="font-select" onChange={this.changeFontFamily}>
+ {fontFamilies.map(font => <option key={font} value={font} defaultValue={StrCast(Doc.UserDoc().fontFamily)}> {font} </option>)}
+ </select>
+ <select className="size-select" onChange={this.changeFontSize}>
+ {fontSizes.map(size => <option key={size} value={size} defaultValue={StrCast(Doc.UserDoc().fontSize)}> {size} </option>)}
+ </select>
+ </div>
+ </div>;
}
+
@action
- googleAuthorize = (event: any) => {
- GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken(true);
+ changeVal = (e: React.ChangeEvent, pass: string) => {
+ const value = (e.target as any).value;
+ switch (pass) {
+ case "curr": this.curr_password = value; break;
+ case "new": this.new_password = value; break;
+ case "conf": this.new_confirm = value; break;
+ }
}
- @action
- hypothesisAuthorize = (event: any) => {
- HypothesisAuthenticationManager.Instance.fetchAccessToken(true);
+
+ @computed get passwordContent() {
+ return <div className="password-content">
+ <div className="password-content-inputs">
+ <input className="password-inputs" type="password" placeholder="current password" onChange={e => this.changeVal(e, "curr")} />
+ <input className="password-inputs" type="password" placeholder="new password" onChange={e => this.changeVal(e, "new")} />
+ <input className="password-inputs" type="password" placeholder="confirm new password" onChange={e => this.changeVal(e, "conf")} />
+ </div>
+ <div className="password-content-buttons">
+ {!this.passwordResultText ? (null) : <div className={`${this.passwordResultText.startsWith("Error") ? "error" : "success"}-text`}>{this.passwordResultText}</div>}
+ <button className="password-submit" onClick={this.changePassword}>submit</button>
+ <a className="password-forgot" href="/forgotPassword">forgot password?</a>
+ </div>
+ </div>;
}
- @action
- togglePlaygroundMode = () => {
- this.playgroundMode = !this.playgroundMode;
- if (this.playgroundMode) DocServer.Control.makeReadOnly();
- else DocServer.Control.makeEditable();
+ @computed get modesContent() {
+ return <div className="modes-content">
+ <select className="modes-select" onChange={this.selectUserMode} defaultValue={Doc.UserDoc().noviceMode ? "Novice" : "Developer"}>
+ <option key={"Novice"} value={"Novice"}> Novice </option>
+ <option key={"Developer"} value={"Developer"}> Developer</option>
+ </select>
+ <div className="modes-playground">
+ <input className="playground-check" type="checkbox" checked={this.playgroundMode} onChange={this.playgroundModeToggle} />
+ <div className="playground-text">Playground Mode</div>
+ </div>
+ <div className="default-acl">
+ <input className="acl-check" type="checkbox" checked={BoolCast(Doc.UserDoc()?.defaultAclPrivate)} onChange={action(() => Doc.UserDoc().defaultAclPrivate = !Doc.UserDoc().defaultAclPrivate)} />
+ <div className="acl-text">Default access private</div>
+ </div>
+ </div>;
+ }
- addStyleSheetRule(SettingsManager._settingsStyle, "lm_header", { background: "pink !important" });
+ @computed get accountsContent() {
+ return <div className="accounts-content">
+ <button onClick={this.googleAuthorize} value="data">Link to Google</button>
+ <button onClick={() => GroupManager.Instance?.open()}>Manage groups</button>
+ </div>;
}
private get settingsInterface() {
- return (
- <div className={"settings-interface"}>
- <div className="settings-heading">
- <h1>settings</h1>
- <div className={"close-button"} onClick={this.close}>
- <FontAwesomeIcon icon={fa.faTimes} color="black" size={"lg"} />
- </div>
+ const pairs = [{ title: "Password", ele: this.passwordContent }, { title: "Modes", ele: this.modesContent },
+ { title: "Accounts", ele: this.accountsContent }, { title: "Preferences", ele: this.preferencesContent }];
+ return <div className="settings-interface">
+ <div className="settings-top">
+ <div className="settings-title">Settings</div>
+ <div className="settings-username">{Doc.CurrentUserEmail}</div>
+ <button className="logout-button" onClick={() => window.location.assign(Utils.prepend("/logout"))} >
+ {CurrentUserUtils.GuestWorkspace ? "Exit" : "Log Out"}
+ </button>
+ <div className="close-button" onClick={this.close}>
+ <FontAwesomeIcon icon={fa.faTimes} color="black" size={"lg"} />
</div>
- <div className="settings-body">
- <div className="settings-type">
- <button onClick={this.onClick} value="password">reset password</button>
- <button onClick={this.noviceToggle} value="data">{`Set ${Doc.UserDoc().noviceMode ? "developer" : "novice"} mode`}</button>
- <button onClick={this.togglePlaygroundMode}>{`${this.playgroundMode ? "Disable" : "Enable"} playground mode`}</button>
- <button onClick={this.googleAuthorize} value="data">{`Link to Google`}</button>
- <button onClick={this.hypothesisAuthorize} value="data">{`Link to Hypothes.is`}</button>
- <button onClick={() => GroupManager.Instance.open()}>Manage groups</button>
- <button onClick={() => window.location.assign(Utils.prepend("/logout"))}>
- {CurrentUserUtils.GuestWorkspace ? "Exit" : "Log Out"}
- </button>
- </div>
- {this.settingsContent === "password" ?
- <div className="settings-content">
- <input placeholder="current password" ref={this.curr_password_ref} />
- <input placeholder="new password" ref={this.new_password_ref} />
- <input placeholder="confirm new password" ref={this.new_confirm_ref} />
- {this.errorText ? <div className="error-text">{this.errorText}</div> : undefined}
- {this.successText ? <div className="success-text">{this.successText}</div> : undefined}
- <button onClick={this.dispatchRequest}>submit</button>
- <a style={{ marginLeft: 65, marginTop: -20 }} href="/forgotPassword">forgot password?</a>
-
- </div>
- : undefined}
- {this.settingsContent === "data" ?
- <div className="settings-content">
- <p>WARNING: <br />
- THIS WILL ERASE ALL YOUR CURRENT DOCUMENTS STORED ON DASH. IF YOU WISH TO PROCEED, CLICK THE BUTTON BELOW.</p>
- <button className="delete-button">DELETE</button>
- </div>
- : undefined}
+ </div>
+ <div className="settings-content">
+ {pairs.map(pair => <div className="settings-section" key={pair.title}>
+ <div className="settings-section-title">{pair.title}</div>
+ <div className="settings-section-context">{pair.ele}</div>
</div>
-
+ )}
</div>
- );
+ </div>;
}
render() {
- return (
- <MainViewModal
- contents={this.settingsInterface}
- isDisplayed={this.isOpen}
- interactive={true}
- closeOnExternalClick={this.close}
- />
- );
+ return <MainViewModal
+ contents={this.settingsInterface}
+ isDisplayed={this.isOpen}
+ interactive={true}
+ closeOnExternalClick={this.close}
+ dialogueBoxStyle={{ width: "600px", height: "340px" }} />;
}
-
} \ No newline at end of file
diff --git a/src/client/util/SharingManager.scss b/src/client/util/SharingManager.scss
index 8da80ef52..7912db74d 100644
--- a/src/client/util/SharingManager.scss
+++ b/src/client/util/SharingManager.scss
@@ -84,6 +84,7 @@
.user-sort {
text-align: left;
margin-left: 10;
+ width: 100px;
cursor: pointer;
}
diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx
index 9d91ce1ba..d50a132f8 100644
--- a/src/client/util/SharingManager.tsx
+++ b/src/client/util/SharingManager.tsx
@@ -1,7 +1,7 @@
import { observable, runInAction, action } from "mobx";
import * as React from "react";
import MainViewModal from "../views/MainViewModal";
-import { Doc, Opt, DocListCastAsync, AclAdmin, DataSym, AclPrivate } from "../../fields/Doc";
+import { Doc, Opt, AclAdmin, AclPrivate, DocListCast } from "../../fields/Doc";
import { DocServer } from "../DocServer";
import { Cast, StrCast } from "../../fields/Types";
import * as RequestPromise from "request-promise";
@@ -21,6 +21,10 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { List } from "../../fields/List";
import { distributeAcls, SharingPermissions, GetEffectiveAcl } from "../../fields/util";
import { TaskCompletionBox } from "../views/nodes/TaskCompletedBox";
+import { library } from "@fortawesome/fontawesome-svg-core";
+
+library.add(fa.faInfoCircle, fa.faCaretUp, fa.faCaretRight, fa.faCaretDown);
+
export interface User {
email: string;
@@ -71,6 +75,7 @@ export default class SharingManager extends React.Component<{}> {
// if both showUserOptions and showGroupOptions are false then both are displayed
@observable private showUserOptions: boolean = false; // whether to show individuals as options when sharing (in the react-select component)
@observable private showGroupOptions: boolean = false; // // whether to show groups as options when sharing (in the react-select component)
+ private populating: boolean = false;
// private get linkVisible() {
// return this.sharingDoc ? this.sharingDoc[PublicKey] !== SharingPermissions.None : false;
@@ -87,13 +92,13 @@ export default class SharingManager extends React.Component<{}> {
this.isOpen = true;
this.permissions = SharingPermissions.Edit;
});
-
+ this.targetDoc!.author === Doc.CurrentUserEmail && !this.targetDoc![`ACL-${Doc.CurrentUserEmail.replace(".", "_")}`] && distributeAcls(`ACL-${Doc.CurrentUserEmail.replace(".", "_")}`, SharingPermissions.Admin, this.targetDoc!);
}
public close = action(() => {
this.isOpen = false;
this.selectedUsers = null; // resets the list of users and seleected users (in the react-select component)
-
+ TaskCompletionBox.taskCompleted = false;
setTimeout(action(() => {
// this.copied = false;
DictationOverlay.Instance.hasActiveModal = false;
@@ -117,24 +122,27 @@ export default class SharingManager extends React.Component<{}> {
* Populates the list of validated users (this.users) by adding registered users which have a sidebar-sharing.
*/
populateUsers = async () => {
- runInAction(() => this.users = []);
- const userList = await RequestPromise.get(Utils.prepend("/getUsers"));
- const raw = JSON.parse(userList) as User[];
- const evaluating = raw.map(async user => {
- const isCandidate = user.email !== Doc.CurrentUserEmail;
- if (isCandidate) {
- const userDocument = await DocServer.GetRefField(user.userDocumentId);
- if (userDocument instanceof Doc) {
- const notificationDoc = await Cast(userDocument["sidebar-sharing"], Doc);
- runInAction(() => {
- if (notificationDoc instanceof Doc) {
- this.users.push({ user, notificationDoc });
- }
- });
+ if (!this.populating) {
+ this.populating = true;
+ runInAction(() => this.users = []);
+ const userList = await RequestPromise.get(Utils.prepend("/getUsers"));
+ const raw = JSON.parse(userList) as User[];
+ const evaluating = raw.map(async user => {
+ const isCandidate = user.email !== Doc.CurrentUserEmail;
+ if (isCandidate) {
+ const userDocument = await DocServer.GetRefField(user.userDocumentId);
+ if (userDocument instanceof Doc) {
+ const notificationDoc = await Cast(userDocument["sidebar-sharing"], Doc);
+ runInAction(() => {
+ if (notificationDoc instanceof Doc) {
+ this.users.push({ user, notificationDoc });
+ }
+ });
+ }
}
- }
- });
- return Promise.all(evaluating);
+ });
+ return Promise.all(evaluating).then(() => this.populating = false);
+ }
}
/**
@@ -152,13 +160,11 @@ export default class SharingManager extends React.Component<{}> {
target.author === Doc.CurrentUserEmail && distributeAcls(ACL, permission as SharingPermissions, target);
// if documents have been shared, add the target to that list if it doesn't already exist, otherwise create a new list with the target
- group.docsShared ? DocListCastAsync(group.docsShared).then(resolved => Doc.IndexOf(target, resolved!) === -1 && (group.docsShared as List<Doc>).push(target)) : group.docsShared = new List<Doc>([target]);
+ group.docsShared ? Doc.IndexOf(target, DocListCast(group.docsShared)) === -1 && (group.docsShared as List<Doc>).push(target) : group.docsShared = new List<Doc>([target]);
users.forEach(({ notificationDoc }) => {
- DocListCastAsync(notificationDoc[storage]).then(resolved => {
- if (permission !== SharingPermissions.None) Doc.IndexOf(target, resolved!) === -1 && Doc.AddDocToList(notificationDoc, storage, target); // add the target to the notificationDoc if it hasn't already been added
- else Doc.IndexOf(target, resolved!) !== -1 && Doc.RemoveDocFromList(notificationDoc, storage, target); // remove the target from the list if it already exists
- });
+ if (permission !== SharingPermissions.None) Doc.IndexOf(target, DocListCast(notificationDoc[storage])) === -1 && Doc.AddDocToList(notificationDoc, storage, target); // add the target to the notificationDoc if it hasn't already been added
+ else Doc.IndexOf(target, DocListCast(notificationDoc[storage])) !== -1 && Doc.RemoveDocFromList(notificationDoc, storage, target); // remove the target from the list if it already exists
});
}
@@ -170,13 +176,7 @@ export default class SharingManager extends React.Component<{}> {
shareWithAddedMember = (group: Doc, emailId: string) => {
const user: ValidatedUser = this.users.find(({ user: { email } }) => email === emailId)!;
- if (group.docsShared) {
- DocListCastAsync(group.docsShared).then(docsShared => {
- docsShared?.forEach(doc => {
- DocListCastAsync(user.notificationDoc[storage]).then(resolved => Doc.IndexOf(doc, resolved!) === -1 && Doc.AddDocToList(user.notificationDoc, storage, doc)); // add the doc if it isn't already in the list
- });
- });
- }
+ if (group.docsShared) DocListCast(group.docsShared).forEach(doc => Doc.IndexOf(doc, DocListCast(user.notificationDoc[storage])) === -1 && Doc.AddDocToList(user.notificationDoc, storage, doc));
}
shareFromPropertiesSidebar = (shareWith: string, permission: SharingPermissions, target: Doc) => {
@@ -194,10 +194,8 @@ export default class SharingManager extends React.Component<{}> {
const user: ValidatedUser = this.users.find(({ user: { email } }) => email === emailId)!;
if (group.docsShared) {
- DocListCastAsync(group.docsShared).then(docsShared => {
- docsShared?.forEach(doc => {
- DocListCastAsync(user.notificationDoc[storage]).then(resolved => Doc.IndexOf(doc, resolved!) !== -1 && Doc.RemoveDocFromList(user.notificationDoc, storage, doc)); // remove the doc only if it is in the list
- });
+ DocListCast(group.docsShared).forEach(doc => {
+ Doc.IndexOf(doc, DocListCast(user.notificationDoc[storage])) !== -1 && Doc.RemoveDocFromList(user.notificationDoc, storage, doc); // remove the doc only if it is in the list
});
}
}
@@ -208,39 +206,36 @@ export default class SharingManager extends React.Component<{}> {
*/
removeGroup = (group: Doc) => {
if (group.docsShared) {
- DocListCastAsync(group.docsShared).then(resolved => {
- resolved?.forEach(doc => {
- const ACL = `ACL-${StrCast(group.groupName)}`;
+ DocListCast(group.docsShared).forEach(doc => {
+ const ACL = `ACL-${StrCast(group.groupName)}`;
- distributeAcls(ACL, SharingPermissions.None, doc);
+ distributeAcls(ACL, SharingPermissions.None, doc);
- const members: string[] = JSON.parse(StrCast(group.members));
- const users: ValidatedUser[] = this.users.filter(({ user: { email } }) => members.includes(email));
-
- users.forEach(({ notificationDoc }) => Doc.RemoveDocFromList(notificationDoc, storage, doc));
- });
+ const members: string[] = JSON.parse(StrCast(group.members));
+ const users: ValidatedUser[] = this.users.filter(({ user: { email } }) => members.includes(email));
+ users.forEach(({ notificationDoc }) => Doc.RemoveDocFromList(notificationDoc, storage, doc));
});
}
}
+ /**
+ * Shares the document with a user.
+ */
setInternalSharing = (recipient: ValidatedUser, permission: string, targetDoc?: Doc) => {
const { user, notificationDoc } = recipient;
const target = targetDoc || this.targetDoc!;
const key = user.email.replace('.', '_');
const ACL = `ACL-${key}`;
+
target.author === Doc.CurrentUserEmail && distributeAcls(ACL, permission as SharingPermissions, target);
if (permission !== SharingPermissions.None) {
- DocListCastAsync(notificationDoc[storage]).then(resolved => {
- Doc.IndexOf(target, resolved!) === -1 && Doc.AddDocToList(notificationDoc, storage, target);
- });
+ Doc.IndexOf(target, DocListCast(notificationDoc[storage])) === -1 && Doc.AddDocToList(notificationDoc, storage, target);
}
else {
- DocListCastAsync(notificationDoc[storage]).then(resolved => {
- Doc.IndexOf(target, resolved!) !== -1 && Doc.RemoveDocFromList(notificationDoc, storage, target);
- });
+ Doc.IndexOf(target, DocListCast(notificationDoc[storage])) !== -1 && Doc.RemoveDocFromList(notificationDoc, storage, target);
}
}
@@ -268,14 +263,17 @@ export default class SharingManager extends React.Component<{}> {
// }
// });
+ /**
+ * Returns the SharingPermissions (Admin, Can Edit etc) access that's used to share
+ */
private get sharingOptions() {
- return Object.values(SharingPermissions).map(permission => {
- return (
- <option key={permission} value={permission} selected={permission === SharingPermissions.Edit}>
+ return Object.values(SharingPermissions).map(permission =>
+ (
+ <option key={permission} value={permission}>
{permission}
</option>
- );
- });
+ )
+ );
}
private focusOn = (contents: string) => {
@@ -308,16 +306,25 @@ export default class SharingManager extends React.Component<{}> {
);
}
+ /**
+ * Handles changes in the users selected in react-select
+ */
@action
handleUsersChange = (selectedOptions: any) => {
this.selectedUsers = selectedOptions as UserOptions[];
}
+ /**
+ * Handles changes in the permission chosen to share with someone with
+ */
@action
handlePermissionsChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
this.permissions = event.currentTarget.value as SharingPermissions;
}
+ /**
+ * Calls the relevant method for sharing, displays the popup, and resets the relevant variables.
+ */
@action
share = () => {
if (this.selectedUsers) {
@@ -341,18 +348,27 @@ export default class SharingManager extends React.Component<{}> {
}
}
+ /**
+ * Sorting algorithm to sort users.
+ */
sortUsers = (u1: ValidatedUser, u2: ValidatedUser) => {
const { email: e1 } = u1.user;
const { email: e2 } = u2.user;
return e1 < e2 ? -1 : e1 === e2 ? 0 : 1;
}
+ /**
+ * Sorting algorithm to sort groups.
+ */
sortGroups = (group1: Doc, group2: Doc) => {
const g1 = StrCast(group1.groupName);
const g2 = StrCast(group2.groupName);
return g1 < g2 ? -1 : g1 === g2 ? 0 : 1;
}
+ /**
+ * @returns the main interface of the SharingManager.
+ */
private get sharingInterface() {
const groupList = GroupManager.Instance?.getAllGroups() || [];
@@ -361,8 +377,8 @@ export default class SharingManager extends React.Component<{}> {
const sortedGroups = groupList.slice().sort(this.sortGroups)
.map(({ groupName }) => ({ label: StrCast(groupName), value: groupType + StrCast(groupName) }));
+ // the next block handles the users shown (individuals/groups/both)
const options: GroupedOptions[] = [];
-
if (GroupManager.Instance) {
if ((this.showUserOptions && this.showGroupOptions) || (!this.showUserOptions && !this.showGroupOptions)) {
options.push({
@@ -393,6 +409,7 @@ export default class SharingManager extends React.Component<{}> {
const effectiveAcl = this.targetDoc ? GetEffectiveAcl(this.targetDoc) : AclPrivate;
+ // the list of users shared with
const userListContents: (JSX.Element | null)[] = users.map(({ user, notificationDoc }) => {
const userKey = user.email.replace('.', '_');
const permissions = StrCast(this.targetDoc?.[`ACL-${userKey}`]);
@@ -422,6 +439,7 @@ export default class SharingManager extends React.Component<{}> {
);
});
+ // the owner of the doc and the current user are placed at the top of the user list.
userListContents.unshift(
(
<div
@@ -452,6 +470,7 @@ export default class SharingManager extends React.Component<{}> {
) : null
);
+ // the list of groups shared with
const groupListContents = groups.map(group => {
const permissions = StrCast(this.targetDoc?.[`ACL-${StrCast(group.groupName)}`]);
@@ -477,6 +496,7 @@ export default class SharingManager extends React.Component<{}> {
);
});
+ // don't display the group list if all groups are null
const displayGroupList = !groupListContents?.every(group => group === null);
return (
@@ -538,7 +558,7 @@ export default class SharingManager extends React.Component<{}> {
})
}}
/>
- <select className="permissions-select" onChange={this.handlePermissionsChange}>
+ <select className="permissions-select" onChange={this.handlePermissionsChange} value={this.permissions}>
{this.sharingOptions}
</select>
<button ref={this.shareDocumentButtonRef} className="share-button" onClick={this.share}>
@@ -556,7 +576,9 @@ export default class SharingManager extends React.Component<{}> {
<div
className="user-sort"
onClick={action(() => this.individualSort = this.individualSort === "ascending" ? "descending" : this.individualSort === "descending" ? "none" : "ascending")}>
- Individuals {this.individualSort === "ascending" ? "↑" : this.individualSort === "descending" ? "↓" : ""} {/* → */}
+ Individuals {this.individualSort === "ascending" ? <FontAwesomeIcon icon={fa.faCaretUp} size={"xs"} />
+ : this.individualSort === "descending" ? <FontAwesomeIcon icon={fa.faCaretDown} size={"xs"} />
+ : <FontAwesomeIcon icon={fa.faCaretRight} size={"xs"} />}
</div>
<div className={"users-list"} style={{ display: "block" }}>{/*200*/}
{userListContents}
@@ -566,7 +588,9 @@ export default class SharingManager extends React.Component<{}> {
<div
className="user-sort"
onClick={action(() => this.groupSort = this.groupSort === "ascending" ? "descending" : this.groupSort === "descending" ? "none" : "ascending")}>
- Groups {this.groupSort === "ascending" ? "↑" : this.groupSort === "descending" ? "↓" : ""} {/* → */}
+ Groups {this.groupSort === "ascending" ? <FontAwesomeIcon icon={fa.faCaretUp} size={"xs"} />
+ : this.groupSort === "descending" ? <FontAwesomeIcon icon={fa.faCaretDown} size={"xs"} />
+ : <FontAwesomeIcon icon={fa.faCaretRight} size={"xs"} />}
</div>
<div className={"groups-list"} style={{ display: !displayGroupList ? "flex" : "block" }}>{/*200*/}