diff options
Diffstat (limited to 'src/client/util')
| -rw-r--r-- | src/client/util/BranchingTrailManager.tsx | 41 | ||||
| -rw-r--r-- | src/client/util/CalendarManager.tsx | 39 | ||||
| -rw-r--r-- | src/client/util/CurrentUserUtils.ts | 2 | ||||
| -rw-r--r-- | src/client/util/DragManager.ts | 18 | ||||
| -rw-r--r-- | src/client/util/GroupManager.tsx | 85 | ||||
| -rw-r--r-- | src/client/util/HypothesisUtils.ts | 2 | ||||
| -rw-r--r-- | src/client/util/Import & Export/ImageUtils.ts | 2 | ||||
| -rw-r--r-- | src/client/util/LinkFollower.ts | 7 | ||||
| -rw-r--r-- | src/client/util/LinkManager.ts | 28 | ||||
| -rw-r--r-- | src/client/util/ScriptingGlobals.ts | 1 | ||||
| -rw-r--r-- | src/client/util/SearchUtil.ts | 20 | ||||
| -rw-r--r-- | src/client/util/SettingsManager.tsx | 323 | ||||
| -rw-r--r-- | src/client/util/SharingManager.tsx | 725 | ||||
| -rw-r--r-- | src/client/util/SnappingManager.ts | 3 | ||||
| -rw-r--r-- | src/client/util/UndoManager.ts | 90 | ||||
| -rw-r--r-- | src/client/util/reportManager/ReportManager.tsx | 38 |
16 files changed, 810 insertions, 614 deletions
diff --git a/src/client/util/BranchingTrailManager.tsx b/src/client/util/BranchingTrailManager.tsx index 02879e3c4..28c00644f 100644 --- a/src/client/util/BranchingTrailManager.tsx +++ b/src/client/util/BranchingTrailManager.tsx @@ -1,18 +1,31 @@ +/* eslint-disable react/no-unused-class-component-methods */ +/* eslint-disable react/no-array-index-key */ import { action, computed, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc } from '../../fields/Doc'; import { Id } from '../../fields/FieldSymbols'; -import { PresBox } from '../views/nodes/trails'; import { OverlayView } from '../views/OverlayView'; +import { PresBox } from '../views/nodes/trails'; import { DocumentManager } from './DocumentManager'; -import { Docs } from '../documents/Documents'; -import { nullAudio } from '../../fields/URLField'; @observer export class BranchingTrailManager extends React.Component { + // eslint-disable-next-line no-use-before-define public static Instance: BranchingTrailManager; + // stack of the history + @observable private slideHistoryStack: String[] = []; + @observable private containsSet: Set<String> = new Set<String>(); + // docId to Doc map + @observable private docIdToDocMap: Map<String, Doc> = new Map<String, Doc>(); + + // prev pres to copmare with + @observable private prevPresId: String | null = null; + @action setPrevPres = action((newId: String | null) => { + this.prevPresId = newId; + }); + constructor(props: any) { super(props); makeObservable(this); @@ -22,7 +35,7 @@ export class BranchingTrailManager extends React.Component { } setupUi = () => { - OverlayView.Instance.addWindow(<BranchingTrailManager></BranchingTrailManager>, { x: 100, y: 150, width: 1000, title: 'Branching Trail' }); + OverlayView.Instance.addWindow(<BranchingTrailManager />, { x: 100, y: 150, width: 1000, title: 'Branching Trail' }); // OverlayView.Instance.forceUpdate(); console.log(OverlayView.Instance); // let hi = Docs.Create.TextDocument("beee", { @@ -36,23 +49,11 @@ export class BranchingTrailManager extends React.Component { console.log(DocumentManager._overlayViews); }; - // stack of the history - @observable private slideHistoryStack: String[] = []; @action setSlideHistoryStack = action((newArr: String[]) => { this.slideHistoryStack = newArr; }); - @observable private containsSet: Set<String> = new Set<String>(); - - // prev pres to copmare with - @observable private prevPresId: String | null = null; - @action setPrevPres = action((newId: String | null) => { - this.prevPresId = newId; - }); - - // docId to Doc map - @observable private docIdToDocMap: Map<String, Doc> = new Map<String, Doc>(); - + // eslint-disable-next-line react/sort-comp observeDocumentChange = (targetDoc: Doc, pres: PresBox) => { const presId = pres.Document[Id]; if (this.prevPresId === presId) { @@ -106,7 +107,7 @@ export class BranchingTrailManager extends React.Component { if (this.slideHistoryStack.length === 0) { Doc.UserDoc().isBranchingMode = false; } - //PresBox.NavigateToTarget(targetDoc, targetDoc); + // PresBox.NavigateToTarget(targetDoc, targetDoc); }; @computed get trailBreadcrumbs() { @@ -116,11 +117,11 @@ export class BranchingTrailManager extends React.Component { const [presId, targetDocId] = info.split(','); const doc = this.docIdToDocMap.get(targetDocId); if (!doc) { - return <></>; + return null; } return ( <span key={targetDocId}> - <button key={index} onPointerDown={e => this.clickHandler(e, targetDocId, index)}> + <button type="button" key={index} onPointerDown={e => this.clickHandler(e, targetDocId, index)}> {presId.slice(0, 3) + ':' + doc.title} </button> -{'>'} diff --git a/src/client/util/CalendarManager.tsx b/src/client/util/CalendarManager.tsx index 6e9094b3a..46aa4d238 100644 --- a/src/client/util/CalendarManager.tsx +++ b/src/client/util/CalendarManager.tsx @@ -1,4 +1,10 @@ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ +import { DateRangePicker, Provider, defaultTheme } from '@adobe/react-spectrum'; +import { IconLookup, faPlus } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { TextField } from '@mui/material'; +import { Button } from 'browndash-components'; import { action, computed, makeObservable, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; @@ -6,21 +12,16 @@ import Select from 'react-select'; import { Doc, DocListCast } from '../../fields/Doc'; import { DocData } from '../../fields/DocSymbols'; import { StrCast } from '../../fields/Types'; +import { Docs } from '../documents/Documents'; import { DictationOverlay } from '../views/DictationOverlay'; import { MainViewModal } from '../views/MainViewModal'; +import { ObservableReactComponent } from '../views/ObservableReactComponent'; import { DocumentView } from '../views/nodes/DocumentView'; import { TaskCompletionBox } from '../views/nodes/TaskCompletedBox'; import './CalendarManager.scss'; import { DocumentManager } from './DocumentManager'; import { SelectionManager } from './SelectionManager'; import { SettingsManager } from './SettingsManager'; -// import { DateRange, Range, RangeKeyDict } from 'react-date-range'; -import { DateRangePicker, Provider, defaultTheme } from '@adobe/react-spectrum'; -import { IconLookup, faPlus } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Button } from 'browndash-components'; -import { Docs } from '../documents/Documents'; -import { ObservableReactComponent } from '../views/ObservableReactComponent'; // import 'react-date-range/dist/styles.css'; // import 'react-date-range/dist/theme/default.css'; @@ -47,6 +48,7 @@ const formatCalendarDateToString = (calendarDate: any) => { @observer export class CalendarManager extends ObservableReactComponent<{}> { + // eslint-disable-next-line no-use-before-define public static Instance: CalendarManager; @observable private isOpen = false; @observable private targetDoc: Doc | undefined = undefined; // the target document @@ -83,10 +85,10 @@ export class CalendarManager extends ObservableReactComponent<{}> { this.creationType = type; }; - public open = (target?: DocumentView, target_doc?: Doc) => { + public open = (target?: DocumentView, targetDoc?: Doc) => { console.log('hi'); runInAction(() => { - this.targetDoc = target_doc || target?.Document; + this.targetDoc = targetDoc || target?.Document; this.targetDocView = target; DictationOverlay.Instance.hasActiveModal = true; this.isOpen = this.targetDoc !== undefined; @@ -117,7 +119,7 @@ export class CalendarManager extends ObservableReactComponent<{}> { @action handleSelectChange = (option: any) => { if (option) { - let selectOpt = option as CalendarSelectOptions; + const selectOpt = option as CalendarSelectOptions; this.selectedExistingCalendarOption = selectOpt; this.calendarName = selectOpt.value; // or label } @@ -136,7 +138,7 @@ export class CalendarManager extends ObservableReactComponent<{}> { // TODO: Make undoable private addToCalendar = () => { - let docs = SelectionManager.Views.length < 2 ? [this.targetDoc] : SelectionManager.Views.map(docView => docView.Document); + const docs = SelectionManager.Views.length < 2 ? [this.targetDoc] : SelectionManager.Views.map(docView => docView.Document); const targetDoc = this.layoutDocAcls ? docs[0] : docs[0]?.[DocData]; // doc to add to calendar console.log(targetDoc); @@ -159,7 +161,7 @@ export class CalendarManager extends ObservableReactComponent<{}> { } } else { // find existing calendar based on selected name (should technically always find one) - const existingCalendar = this.existingCalendars.find(calendar => StrCast(calendar.title) === this.calendarName); + const existingCalendar = this.existingCalendars.find(findCal => StrCast(findCal.title) === this.calendarName); if (existingCalendar) calendar = existingCalendar; else { this.errorMessage = 'Must select an existing calendar'; @@ -252,11 +254,9 @@ export class CalendarManager extends ObservableReactComponent<{}> { @computed get calendarInterface() { - let docs = SelectionManager.Views.length < 2 ? [this.targetDoc] : SelectionManager.Views.map(docView => docView.Document); + const docs = SelectionManager.Views.length < 2 ? [this.targetDoc] : SelectionManager.Views.map(docView => docView.Document); const targetDoc = this.layoutDocAcls ? docs[0] : docs[0]?.[DocData]; - const currentDate = new Date(); - return ( <div className="calendar-interface" @@ -268,10 +268,10 @@ export class CalendarManager extends ObservableReactComponent<{}> { <b>{this.focusOn(docs.length < 2 ? StrCast(targetDoc?.title, 'this document') : '-multiple-')}</b> </p> <div className="creation-type-container"> - <div className={`calendar-creation ${this.creationType === 'new-calendar' ? 'calendar-creation-selected' : ''}`} onClick={e => this.setInterationType('new-calendar')}> + <div className={`calendar-creation ${this.creationType === 'new-calendar' ? 'calendar-creation-selected' : ''}`} onClick={() => this.setInterationType('new-calendar')}> Add to New Calendar </div> - <div className={`calendar-creation ${this.creationType === 'existing-calendar' ? 'calendar-creation-selected' : ''}`} onClick={e => this.setInterationType('existing-calendar')}> + <div className={`calendar-creation ${this.creationType === 'existing-calendar' ? 'calendar-creation-selected' : ''}`} onClick={() => this.setInterationType('existing-calendar')}> Add to Existing calendar </div> </div> @@ -317,7 +317,8 @@ export class CalendarManager extends ObservableReactComponent<{}> { color: StrCast(Doc.UserDoc().userColor), width: '100%', }), - }}></Select> + }} + /> )} </div> <div className="description-container"> @@ -351,6 +352,6 @@ export class CalendarManager extends ObservableReactComponent<{}> { } render() { - return <MainViewModal contents={this.calendarInterface} isDisplayed={this.isOpen} interactive={true} dialogueBoxDisplayedOpacity={this.dialogueBoxOpacity} overlayDisplayedOpacity={this.overlayOpacity} closeOnExternalClick={this.close} />; + return <MainViewModal contents={this.calendarInterface} isDisplayed={this.isOpen} interactive dialogueBoxDisplayedOpacity={this.dialogueBoxOpacity} overlayDisplayedOpacity={this.overlayOpacity} closeOnExternalClick={this.close} />; } } diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 27ae5c9a0..6dba8027d 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -709,7 +709,7 @@ pie title Minerals in my tap water return [ { title: "Back", toolTip: "Go back", btnType: ButtonType.ClickButton, icon: "arrow-left", scripts: { onClick: '{ return webBack(); }' }}, { title: "Forward", toolTip: "Go forward", btnType: ButtonType.ClickButton, icon: "arrow-right", scripts: { onClick: '{ return webForward(); }'}}, - { title: "URL", toolTip: "URL", width: 250, btnType: ButtonType.EditableText, icon: "lock", ignoreClick: true, scripts: { script: '{ return webSetURL(value, _readOnly_); }'} }, + { title: "URL", toolTip: "URL", width: 250, btnType: ButtonType.EditText, icon: "lock", ignoreClick: true, scripts: { script: '{ return webSetURL(value, _readOnly_); }'} }, ]; } static videoTools() { diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index 62f055f1a..3890b7845 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -495,18 +495,20 @@ export namespace DragManager { .filter(pb => pb.width && pb.height) .map((pb, i) => pb.getContext('2d')!.drawImage(pdfBoxSrc[i], 0, 0)); } - [dragElement, ...Array.from(dragElement.getElementsByTagName('*'))].forEach(ele => { - (ele as any).style && ((ele as any).style.pointerEvents = 'none'); - }); + [dragElement, ...Array.from(dragElement.getElementsByTagName('*'))] + .map(dele => (dele as any).style) + .forEach(style => { + style && (style.pointerEvents = 'none'); + }); dragDiv.appendChild(dragElement); if (dragElement !== ele) { - const children = [Array.from(ele.children), Array.from(dragElement.children)]; - while (children[0].length) { - const childs = [children[0].pop(), children[1].pop()]; + const dragChildren = [Array.from(ele.children), Array.from(dragElement.children)]; + while (dragChildren[0].length) { + const childs = [dragChildren[0].pop(), dragChildren[1].pop()]; if (childs[0]?.children) { - children[0].push(...Array.from(childs[0].children)); - children[1].push(...Array.from(childs[1]!.children)); + dragChildren[0].push(...Array.from(childs[0].children)); + dragChildren[1].push(...Array.from(childs[1]!.children)); } if (childs[0]?.scrollTop) childs[1]!.scrollTop = childs[0].scrollTop; } diff --git a/src/client/util/GroupManager.tsx b/src/client/util/GroupManager.tsx index c261c0f1e..8d84dbad8 100644 --- a/src/client/util/GroupManager.tsx +++ b/src/client/util/GroupManager.tsx @@ -1,3 +1,5 @@ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Button, IconButton, Size, Type } from 'browndash-components'; import { action, computed, makeObservable, observable } from 'mobx'; @@ -30,6 +32,7 @@ export interface UserOptions { @observer export class GroupManager extends ObservableReactComponent<{}> { + // eslint-disable-next-line no-use-before-define static Instance: GroupManager; @observable isOpen: boolean = false; // whether the GroupManager is to be displayed or not. @observable private users: string[] = []; // list of users populated from the database. @@ -160,7 +163,7 @@ export class GroupManager extends ObservableReactComponent<{}> { addGroup(groupDoc: Doc): boolean { if (this.GroupManagerDoc) { Doc.AddDocToList(this.GroupManagerDoc, 'data', groupDoc); - this.GroupManagerDoc['data_modificationDate'] = new DateField(); + this.GroupManagerDoc.data_modificationDate = new DateField(); return true; } return false; @@ -202,7 +205,7 @@ export class GroupManager extends ObservableReactComponent<{}> { !memberList.includes(email) && memberList.push(email); groupDoc.members = JSON.stringify(memberList); SharingManager.Instance.shareWithAddedMember(groupDoc, email); - this.GroupManagerDoc && (this.GroupManagerDoc['data_modificationDate'] = new DateField()); + this.GroupManagerDoc && (this.GroupManagerDoc.data_modificationDate = new DateField()); } } @@ -216,10 +219,9 @@ export class GroupManager extends ObservableReactComponent<{}> { const memberList = JSON.parse(StrCast(groupDoc.members)); const index = memberList.indexOf(email); if (index !== -1) { - const user = memberList.splice(index, 1)[0]; groupDoc.members = JSON.stringify(memberList); SharingManager.Instance.removeMember(groupDoc, email); - this.GroupManagerDoc && (this.GroupManagerDoc['data_modificationDate'] = new DateField()); + this.GroupManagerDoc && (this.GroupManagerDoc.data_modificationDate = new DateField()); } } } @@ -275,7 +277,9 @@ export class GroupManager extends ObservableReactComponent<{}> { TaskCompletionBox.textDisplayed = 'Group created!'; TaskCompletionBox.taskCompleted = true; setTimeout( - action(() => (TaskCompletionBox.taskCompleted = false)), + action(() => { + TaskCompletionBox.taskCompleted = false; + }), 2000 ); }; @@ -292,7 +296,7 @@ export class GroupManager extends ObservableReactComponent<{}> { </p> <div className="close-button"> <Button - icon={<FontAwesomeIcon icon={'times'} size={'lg'} />} + icon={<FontAwesomeIcon icon="times" size="lg" />} onClick={action(() => { this.createGroupModalOpen = false; TaskCompletionBox.taskCompleted = false; @@ -302,7 +306,17 @@ export class GroupManager extends ObservableReactComponent<{}> { </div> </div> <div className="group-input" style={{ border: StrCast(Doc.UserDoc().userColor) }}> - <input ref={this.inputRef} onKeyDown={this.handleKeyDown} autoFocus type="text" placeholder="Group name" onChange={action(() => (this.buttonColour = this.inputRef.current?.value ? 'black' : '#979797'))} /> + <input + ref={this.inputRef} + onKeyDown={this.handleKeyDown} + // eslint-disable-next-line jsx-a11y/no-autofocus + autoFocus + type="text" + placeholder="Group name" + onChange={action(() => { + this.buttonColour = this.inputRef.current?.value ? 'black' : '#979797'; + })} + /> </div> <div style={{ border: StrCast(Doc.UserDoc().userColor) }}> <Select @@ -310,7 +324,7 @@ export class GroupManager extends ObservableReactComponent<{}> { isMulti options={this.options} onChange={this.handleChange} - placeholder={'Select users'} + placeholder="Select users" value={this.selectedUsers} closeMenuOnSelect={false} styles={{ @@ -335,8 +349,8 @@ export class GroupManager extends ObservableReactComponent<{}> { }} /> </div> - <div className={'create-button'}> - <Button text={'Create'} type={Type.TERT} color={StrCast(Doc.UserDoc().userColor)} onClick={this.createGroup} /> + <div className="create-button"> + <Button text="Create" type={Type.TERT} color={StrCast(Doc.UserDoc().userColor)} onClick={this.createGroup} /> </div> </div> ); @@ -344,7 +358,7 @@ export class GroupManager extends ObservableReactComponent<{}> { return ( <MainViewModal isDisplayed={this.createGroupModalOpen} - interactive={true} + interactive contents={contents} dialogueBoxStyle={{ width: '90%', height: '70%' }} closeOnExternalClick={action(() => { @@ -372,28 +386,59 @@ export class GroupManager extends ObservableReactComponent<{}> { return ( <div className="group-interface" style={{ background: SettingsManager.userBackgroundColor, color: SettingsManager.userColor }}> {this.groupCreationModal} - {this.currentGroup ? <GroupMemberView group={this.currentGroup} onCloseButtonClick={action(() => (this.currentGroup = undefined))} /> : null} + {this.currentGroup ? ( + <GroupMemberView + group={this.currentGroup} + onCloseButtonClick={action(() => { + this.currentGroup = undefined; + })} + /> + ) : null} <div className="group-heading"> <p> <b>Manage Groups</b> </p> - <Button icon={<FontAwesomeIcon icon={'plus'} />} iconPlacement={'left'} text={'Create Group'} type={Type.TERT} color={StrCast(Doc.UserDoc().userColor)} onClick={action(() => (this.createGroupModalOpen = true))} /> - <div className={'close-button'}> - <Button icon={<FontAwesomeIcon icon={'times'} size={'lg'} />} onClick={this.close} color={StrCast(Doc.UserDoc().userColor)} /> + <Button + icon={<FontAwesomeIcon icon="plus" />} + iconPlacement="left" + text="Create Group" + type={Type.TERT} + color={StrCast(Doc.UserDoc().userColor)} + onClick={action(() => { + this.createGroupModalOpen = true; + })} + /> + <div className="close-button"> + <Button icon={<FontAwesomeIcon icon="times" size="lg" />} onClick={this.close} color={StrCast(Doc.UserDoc().userColor)} /> </div> </div> <div className="main-container"> - <div className="sort-groups" onClick={action(() => (this.groupSort = this.groupSort === 'ascending' ? 'descending' : this.groupSort === 'descending' ? 'none' : 'ascending'))}> + <div + className="sort-groups" + onClick={action(() => { + this.groupSort = this.groupSort === 'ascending' ? 'descending' : this.groupSort === 'descending' ? 'none' : 'ascending'; + })}> Name <IconButton icon={<FontAwesomeIcon icon={this.groupSort === 'ascending' ? 'caret-up' : this.groupSort === 'descending' ? 'caret-down' : 'caret-right'} />} size={Size.XSMALL} color={StrCast(Doc.UserDoc().userColor)} /> </div> - <div className={'style-divider'} style={{ background: StrCast(Doc.UserDoc().userColor) }} /> + <div className="style-divider" style={{ background: StrCast(Doc.UserDoc().userColor) }} /> <div className="group-body" style={{ background: StrCast(Doc.UserDoc().userBackgroundColor), color: StrCast(Doc.UserDoc().userColor) }}> {groups.map(group => ( <div className="group-row" key={StrCast(group.title || group.groupName)}> <div className="group-name">{StrCast(group.title || group.groupName)}</div> - <div className="group-info" onClick={action(() => (this.currentGroup = group))}> - <IconButton icon={<FontAwesomeIcon icon={'info-circle'} />} size={Size.XSMALL} color={StrCast(Doc.UserDoc().userColor)} onClick={action(() => (this.currentGroup = group))} /> + <div + className="group-info" + onClick={action(() => { + this.currentGroup = group; + })}> + <IconButton + icon={<FontAwesomeIcon icon="info-circle" />} + size={Size.XSMALL} + color={StrCast(Doc.UserDoc().userColor)} + onClick={action(() => { + this.currentGroup = group; + })} + /> </div> </div> ))} @@ -404,6 +449,6 @@ export class GroupManager extends ObservableReactComponent<{}> { } render() { - return <MainViewModal contents={this.groupInterface} isDisplayed={this.isOpen} interactive={true} dialogueBoxStyle={{ zIndex: 1002 }} overlayStyle={{ zIndex: 1001 }} closeOnExternalClick={this.close} />; + return <MainViewModal contents={this.groupInterface} isDisplayed={this.isOpen} interactive dialogueBoxStyle={{ zIndex: 1002 }} overlayStyle={{ zIndex: 1001 }} closeOnExternalClick={this.close} />; } } diff --git a/src/client/util/HypothesisUtils.ts b/src/client/util/HypothesisUtils.ts index 48d3ac046..6dc96f1b5 100644 --- a/src/client/util/HypothesisUtils.ts +++ b/src/client/util/HypothesisUtils.ts @@ -3,12 +3,10 @@ import { simulateMouseClick } from '../../ClientUtils'; import { Doc, Opt } from '../../fields/Doc'; import { Cast, StrCast } from '../../fields/Types'; import { WebField } from '../../fields/URLField'; -import { DocumentType } from '../documents/DocumentTypes'; import { Docs } from '../documents/Documents'; import { DocumentLinksButton } from '../views/nodes/DocumentLinksButton'; import { DocumentView } from '../views/nodes/DocumentView'; import { DocumentManager } from './DocumentManager'; -import { SearchUtil } from './SearchUtil'; import { SelectionManager } from './SelectionManager'; export namespace Hypothesis { diff --git a/src/client/util/Import & Export/ImageUtils.ts b/src/client/util/Import & Export/ImageUtils.ts index dfad9755c..8d4eefa7e 100644 --- a/src/client/util/Import & Export/ImageUtils.ts +++ b/src/client/util/Import & Export/ImageUtils.ts @@ -16,7 +16,7 @@ export namespace ImageUtils { }; export const ExtractImgInfo = async (document: Doc): Promise<imgInfo | undefined> => { const field = Cast(document.data, ImageField); - return field ? await Networking.PostToServer('/inspectImage', { source: field.url.href }) : undefined; + return field ? Networking.PostToServer('/inspectImage', { source: field.url.href }) : undefined; }; export const AssignImgInfo = (document: Doc, data?: imgInfo) => { diff --git a/src/client/util/LinkFollower.ts b/src/client/util/LinkFollower.ts index ba715aa1f..85bada8c9 100644 --- a/src/client/util/LinkFollower.ts +++ b/src/client/util/LinkFollower.ts @@ -53,7 +53,7 @@ export class LinkFollower { const linkWithoutTargetDoc = traverseBacklink === undefined ? fwdLinkWithoutTargetView ?? backLinkWithoutTargetView : traverseBacklink ? backLinkWithoutTargetView : fwdLinkWithoutTargetView; const linkDocList = linkWithoutTargetDoc && !sourceDoc.followAllLinks ? [linkWithoutTargetDoc] : traverseBacklink === undefined ? fwdLinks.concat(backLinks) : traverseBacklink ? backLinks : fwdLinks; const followLinks = sourceDoc.followLinkToggle || sourceDoc.followAllLinks ? linkDocList : linkDocList.slice(0, 1); - var count = 0; + let count = 0; const allFinished = () => ++count === followLinks.length && finished?.(); if (!followLinks.length) { finished?.(); @@ -69,7 +69,7 @@ export class LinkFollower { ? linkDoc.link_anchor_2 : linkDoc.link_anchor_1 ) as Doc; - const srcAnchor = LinkManager.getOppositeAnchor(linkDoc, target) ?? sourceDoc; + const srcAnchor: Doc = LinkManager.getOppositeAnchor(linkDoc, target) ?? sourceDoc; if (target) { const doFollow = (canToggle?: boolean) => { const toggleTarget = canToggle && BoolCast(sourceDoc.followLinkToggle); @@ -113,10 +113,12 @@ export class LinkFollower { } const moveTo = [NumCast(sourceDoc.x) + NumCast(sourceDoc.followLinkXoffset), NumCast(sourceDoc.y) + NumCast(sourceDoc.followLinkYoffset)]; if (srcAnchor.followLinkXoffset !== undefined && moveTo[0] !== target.x) { + // eslint-disable-next-line prefer-destructuring target.x = moveTo[0]; movedTarget = true; } if (srcAnchor.followLinkYoffset !== undefined && moveTo[1] !== target.y) { + // eslint-disable-next-line prefer-destructuring target.y = moveTo[1]; movedTarget = true; } @@ -130,6 +132,7 @@ export class LinkFollower { } } +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function followLink(doc: Doc, altKey: boolean) { SelectionManager.DeselectAll(); return LinkFollower.FollowLink(undefined, doc, altKey) ? undefined : { select: true }; diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts index 82cd791cc..c986bd674 100644 --- a/src/client/util/LinkManager.ts +++ b/src/client/util/LinkManager.ts @@ -25,6 +25,7 @@ import { ScriptingGlobals } from './ScriptingGlobals'; * - user defined kvps */ export class LinkManager { + // eslint-disable-next-line no-use-before-define @observable static _instance: LinkManager; @observable.shallow userLinkDBs: Doc[] = []; @observable public currentLink: Opt<Doc> = undefined; @@ -61,7 +62,7 @@ export class LinkManager { Promise.all(lAnchs.map(lAnch => PromiseValue(lAnch?.proto as Doc))).then((lAnchProtos: Opt<Doc>[]) => Promise.all(lAnchProtos.map(lAnchProto => PromiseValue(lAnchProto?.proto as Doc))).then( link && - action(lAnchProtoProtos => { + action(() => { Doc.AddDocToList(Doc.UserDoc(), 'links', link); lAnchs[0]?.[DocData][DirectLinks].add(link); lAnchs[1]?.[DocData][DirectLinks].add(link); @@ -78,7 +79,7 @@ export class LinkManager { Promise.all([lproto?.link_anchor_1 as Doc, lproto?.link_anchor_2 as Doc].map(PromiseValue)).then((lAnchs: Opt<Doc>[]) => Promise.all(lAnchs.map(lAnch => PromiseValue(lAnch?.proto as Doc))).then((lAnchProtos: Opt<Doc>[]) => Promise.all(lAnchProtos.map(lAnchProto => PromiseValue(lAnchProto?.proto as Doc))).then( - action(lAnchProtoProtos => { + action(() => { link && lAnchs[0] && lAnchs[0][DocData][DirectLinks].delete(link); link && lAnchs[1] && lAnchs[1][DocData][DirectLinks].delete(link); }) @@ -100,7 +101,8 @@ export class LinkManager { (change as any).added.forEach((link: any) => addLinkToDoc(toRealField(link))); (change as any).removed.forEach((link: any) => remLinkFromDoc(toRealField(link))); break; - case 'update': //let oldValue = change.oldValue; + case 'update': // let oldValue = change.oldValue; + default: } }, true @@ -120,6 +122,8 @@ export class LinkManager { added?.forEach((link: any) => addLinkToDoc(toRealField(link))); removed?.forEach((link: any) => remLinkFromDoc(toRealField(link))); }); + break; + default: } }, true @@ -133,7 +137,8 @@ export class LinkManager { case 'splice': (change as any).added.forEach(watchUserLinkDB); break; - case 'update': //let oldValue = change.oldValue; + case 'update': // let oldValue = change.oldValue; + default: } }, true @@ -143,7 +148,7 @@ export class LinkManager { } public createlink_relationshipLists = () => { - //create new lists for link relations and their associated colors if the lists don't already exist + // create new lists for link relations and their associated colors if the lists don't already exist !Doc.UserDoc().link_relationshipList && (Doc.UserDoc().link_relationshipList = new List<string>()); !Doc.UserDoc().link_ColorList && (Doc.UserDoc().link_ColorList = new List<string>()); !Doc.UserDoc().link_relationshipSizes && (Doc.UserDoc().link_relationshipSizes = new List<number>()); @@ -153,6 +158,7 @@ export class LinkManager { Doc.AddDocToList(Doc.UserDoc(), 'links', linkDoc); if (!checkExists || !DocListCast(Doc.LinkDBDoc().data).includes(linkDoc)) { Doc.AddDocToList(Doc.LinkDBDoc(), 'data', linkDoc); + // eslint-disable-next-line no-use-before-define setTimeout(UPDATE_SERVER_CACHE, 100); } } @@ -203,7 +209,7 @@ export class LinkManager { this.relatedLinker(anchor).forEach(link => { if (link.link_relationship && link.link_relationship !== '-ungrouped-') { const relation = StrCast(link.link_relationship); - const anchorRelation = relation.indexOf(':') !== -1 ? relation.split(':')[Doc.AreProtosEqual(Cast(link.link_anchor_1, Doc, null), anchor) ? 0 : 1] : relation; + const anchorRelation: string = relation.indexOf(':') !== -1 ? relation.split(':')[Doc.AreProtosEqual(Cast(link.link_anchor_1, Doc, null), anchor) ? 0 : 1] : relation; const group = anchorGroups.get(anchorRelation); anchorGroups.set(anchorRelation, group ? [...group, link] : [link]); } else { @@ -234,14 +240,7 @@ export class LinkManager { if (Doc.AreProtosEqual(DocCast(anchor?.annotationOn, anchor), DocCast(a1?.annotationOn, a1))) return '1'; if (Doc.AreProtosEqual(DocCast(anchor?.annotationOn, anchor), DocCast(a2?.annotationOn, a2))) return '2'; if (Doc.AreProtosEqual(anchor, linkDoc)) return '0'; - - // const a1 = DocCast(linkDoc.link_anchor_1); - // const a2 = DocCast(linkDoc.link_anchor_2); - // if (linkDoc.link_matchEmbeddings) { - // return [a2, a2.annotationOn].includes(anchor) ? '2' : '1'; - // } - // if (Doc.AreProtosEqual(a2, anchor) || Doc.AreProtosEqual(a2.annotationOn as Doc, anchor)) return '2'; - // return Doc.AreProtosEqual(a1, anchor) || Doc.AreProtosEqual(a1.annotationOn as Doc, anchor) ? '1' : '2'; + return undefined; } } @@ -282,6 +281,7 @@ export function UPDATE_SERVER_CACHE() { } ScriptingGlobals.add( + // eslint-disable-next-line prefer-arrow-callback function links(doc: any) { return new List(LinkManager.Links(doc)); }, diff --git a/src/client/util/ScriptingGlobals.ts b/src/client/util/ScriptingGlobals.ts index bc159ed65..ac524394a 100644 --- a/src/client/util/ScriptingGlobals.ts +++ b/src/client/util/ScriptingGlobals.ts @@ -41,6 +41,7 @@ export namespace ScriptingGlobals { if (n === undefined || n === 'undefined') { return false; } + // eslint-disable-next-line no-prototype-builtins if (_scriptingGlobals.hasOwnProperty(n)) { throw new Error(`Global with name ${n} is already registered, choose another name`); } diff --git a/src/client/util/SearchUtil.ts b/src/client/util/SearchUtil.ts index 65b9a977d..609fedfa9 100644 --- a/src/client/util/SearchUtil.ts +++ b/src/client/util/SearchUtil.ts @@ -8,16 +8,16 @@ import { DocOptions, FInfo } from '../documents/Documents'; export namespace SearchUtil { export type HighlightingResult = { [id: string]: { [key: string]: string[] } }; - export function SearchCollection(collectionDoc: Opt<Doc>, query: string, matchKeyNames: boolean, onlyKeys?: string[]) { + export function SearchCollection(collectionDoc: Opt<Doc>, queryIn: string, matchKeyNames: boolean, onlyKeys?: string[]) { const blockedTypes = [DocumentType.PRESELEMENT, DocumentType.CONFIG, DocumentType.KVP, DocumentType.FONTICON, DocumentType.BUTTON, DocumentType.SCRIPTING]; const blockedKeys = matchKeyNames ? [] : Object.entries(DocOptions) - .filter(([key, info]: [string, FInfo]) => !info?.searchable()) + .filter(([, info]: [string, FInfo]) => !info?.searchable()) .map(([key]) => key); - const exact = query.startsWith('='); - query = query.toLowerCase().split('=').lastElement(); + const exact = queryIn.startsWith('='); + const query = queryIn.toLowerCase().split('=').lastElement(); const results = new ObservableMap<Doc, string[]>(); if (collectionDoc) { @@ -51,7 +51,11 @@ export namespace SearchUtil { */ export function documentKeys(doc: Doc) { const keys: { [key: string]: boolean } = {}; - Doc.GetAllPrototypes(doc).map(proto => Object.keys(proto).forEach(key => (keys[key] = false))); + Doc.GetAllPrototypes(doc).map(proto => + Object.keys(proto).forEach(key => { + keys[key] = false; + }) + ); return Array.from(Object.keys(keys)); } @@ -62,12 +66,14 @@ export namespace SearchUtil { * This method iterates through an array of docs and all docs within those docs, calling * the function func on each doc. */ - export function foreachRecursiveDoc(docs: Doc[], func: (depth: number, doc: Doc) => void) { + export function foreachRecursiveDoc(docsIn: Doc[], func: (depth: number, doc: Doc) => void) { + let docs = docsIn; let newarray: Doc[] = []; - var depth = 0; + let depth = 0; const visited: Doc[] = []; while (docs.length > 0) { newarray = []; + // eslint-disable-next-line no-loop-func docs.filter(d => d && !visited.includes(d)).forEach(d => { visited.push(d); const fieldKey = Doc.LayoutFieldKey(d); diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx index f983c29b7..8347844f7 100644 --- a/src/client/util/SettingsManager.tsx +++ b/src/client/util/SettingsManager.tsx @@ -1,3 +1,5 @@ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Button, ColorPicker, Dropdown, DropdownType, EditableText, Group, NumberDropdown, Size, Toggle, ToggleType, Type } from 'browndash-components'; import { action, computed, makeObservable, observable, reaction, runInAction } from 'mobx'; @@ -17,8 +19,8 @@ import { MainViewModal } from '../views/MainViewModal'; import { FontIconBox } from '../views/nodes/FontIconBox/FontIconBox'; import { GroupManager } from './GroupManager'; import './SettingsManager.scss'; -import { undoBatch } from './UndoManager'; import { SnappingManager } from './SnappingManager'; +import { undoable } from './UndoManager'; export enum ColorScheme { Dark = 'Dark', @@ -38,24 +40,104 @@ export class SettingsManager extends React.Component<{}> { // eslint-disable-next-line no-use-before-define public static Instance: SettingsManager; static _settingsStyle = addStyleSheet(); - @observable public isOpen = false; - @observable private passwordResultText = ''; - @observable private playgroundMode = false; + @observable private _passwordResultText = ''; + @observable private _playgroundMode = false; - @observable private curr_password = ''; - @observable private new_password = ''; - @observable private new_confirm = ''; + @observable private _curr_password = ''; + @observable private _new_password = ''; + @observable private _new_confirm = ''; @observable private _lastPressedSidebarBtn: Opt<Doc> = undefined; // bcz: this is a hack to handle highlighting buttons in the leftpanel menu .. need to find a cleaner approach - @observable activeTab = 'Accounts'; + @observable private _activeTab = 'Accounts'; + @observable private _isOpen = false; @observable public propertiesWidth: number = 0; + private googleAuthorize = action(() => GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken(true)); + + public closeMgr = action(() => { + this._isOpen = false; + }); + public openMgr = action(() => { + this._isOpen = true; + }); + + private matchSystem = undoable(() => { + if (Doc.UserDoc().userThemeSystem) { + if (window.matchMedia('(prefers-color-scheme: dark)').matches) this.changeColorScheme(ColorScheme.Dark); + if (window.matchMedia('(prefers-color-scheme: light)').matches) this.changeColorScheme(ColorScheme.Light); + } + }, 'match system theme'); + private setFreeformScrollMode = undoable((mode: string) => { + Doc.UserDoc().freeformScrollMode = mode; + }, 'set scroll mode'); + private selectUserMode = undoable((mode: string) => { + Doc.noviceMode = mode === 'Novice'; + }, 'change user mode'); + private changeFontFamily = undoable((font: string) => { + Doc.UserDoc().fontFamily = font; + }, 'change font family'); + private switchUserBackgroundColor = undoable((color: string) => { + Doc.UserDoc().userBackgroundColor = color; + addStyleSheetRule(SettingsManager._settingsStyle, 'lm_header', { background: `${color} !important` }); + }, 'change background color'); + private switchUserColor = undoable((color: string) => { + Doc.UserDoc().userColor = color; + }, 'change user color'); + switchUserVariantColor = undoable((color: string) => { + Doc.UserDoc().userVariantColor = color; + }, 'change variant color'); + userThemeSystemToggle = undoable(() => { + Doc.UserDoc().userThemeSystem = !Doc.UserDoc().userThemeSystem; + this.matchSystem(); + }, 'change theme color'); + playgroundModeToggle = undoable( + action(() => { + this._playgroundMode = !this._playgroundMode; + if (this._playgroundMode) { + DocServer.Control.makeReadOnly(); + addStyleSheetRule(SettingsManager._settingsStyle, 'topbar-inner-container', { background: 'red !important' }); + } else ClientUtils.CurrentUserEmail() !== 'guest' && DocServer.Control.makeEditable(); + }), + 'set playgorund mode' + ); + changeColorScheme = undoable( + action((scheme: string) => { + Doc.UserDoc().userTheme = scheme; + switch (scheme) { + case ColorScheme.Light: + this.switchUserColor('#323232'); + this.switchUserBackgroundColor('#DFDFDF'); + this.switchUserVariantColor('#BDDDF5'); + break; + case ColorScheme.Dark: + this.switchUserColor('#DFDFDF'); + this.switchUserBackgroundColor('#323232'); + this.switchUserVariantColor('#4476F7'); + break; + case ColorScheme.CoolBlue: + this.switchUserColor('#ADEAFF'); + this.switchUserBackgroundColor('#060A15'); + this.switchUserVariantColor('#3C51FF'); + break; + case ColorScheme.Cupcake: + this.switchUserColor('#3BC7FF'); + this.switchUserBackgroundColor('#fffdf7'); + this.switchUserVariantColor('#FFD7F3'); + break; + case ColorScheme.Custom: + break; + default: + } + }), + 'change color scheme' + ); + constructor(props: {}) { super(props); makeObservable(this); SettingsManager.Instance = this; this.matchSystem(); - window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => { + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { if (Doc.UserDoc().userThemeSystem) { if (window.matchMedia('(prefers-color-scheme: dark)').matches) this.changeColorScheme(ColorScheme.Dark); if (window.matchMedia('(prefers-color-scheme: light)').matches) this.changeColorScheme(ColorScheme.Light); @@ -73,26 +155,9 @@ export class SettingsManager extends React.Component<{}> { ); SnappingManager.SettingsStyle = SettingsManager._settingsStyle; } - matchSystem = () => { - if (Doc.UserDoc().userThemeSystem) { - if (window.matchMedia('(prefers-color-scheme: dark)').matches) this.changeColorScheme(ColorScheme.Dark); - if (window.matchMedia('(prefers-color-scheme: light)').matches) this.changeColorScheme(ColorScheme.Light); - } - }; - - 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!')); - } - }; + public get LastPressedBtn() { return this._lastPressedSidebarBtn; } // prettier-ignore + public set LastPressedBtn(state:Doc|undefined) { this._lastPressedSidebarBtn = state; } // prettier-ignore @computed public static get userColor() { return StrCast(Doc.UserDoc().userColor); @@ -106,60 +171,6 @@ export class SettingsManager extends React.Component<{}> { return StrCast(Doc.UserDoc().userBackgroundColor); } - public get LastPressedBtn() { return this._lastPressedSidebarBtn; } // prettier-ignore - public SetLastPressedBtn = (state?:Doc) => runInAction(() => (this._lastPressedSidebarBtn = state)); // prettier-ignore - - @undoBatch selectUserMode = action((mode: string) => (Doc.noviceMode = mode === 'Novice')); - @undoBatch changelayout_showTitle = action((e: React.ChangeEvent) => (Doc.UserDoc().layout_showTitle = (e.currentTarget as any).value ? 'title' : undefined)); - @undoBatch changeFontFamily = action((font: string) => (Doc.UserDoc().fontFamily = font)); - @undoBatch changeFontSize = action((val: number) => (Doc.UserDoc().fontSize = val)); - @undoBatch switchUserBackgroundColor = action((color: string) => { - Doc.UserDoc().userBackgroundColor = color; - addStyleSheetRule(SettingsManager._settingsStyle, 'lm_header', { background: `${color} !important` }); - }); - @undoBatch switchUserColor = action((color: string) => (Doc.UserDoc().userColor = color)); - @undoBatch switchUserVariantColor = action((color: string) => (Doc.UserDoc().userVariantColor = color)); - @undoBatch userThemeSystemToggle = action(() => { - Doc.UserDoc().userThemeSystem = !Doc.UserDoc().userThemeSystem; - this.matchSystem(); - }); - @undoBatch playgroundModeToggle = action(() => { - this.playgroundMode = !this.playgroundMode; - if (this.playgroundMode) { - DocServer.Control.makeReadOnly(); - addStyleSheetRule(SettingsManager._settingsStyle, 'topbar-inner-container', { background: 'red !important' }); - } else ClientUtils.CurrentUserEmail() !== 'guest' && DocServer.Control.makeEditable(); - }); - - @undoBatch - changeColorScheme = action((scheme: string) => { - Doc.UserDoc().userTheme = scheme; - switch (scheme) { - case ColorScheme.Light: - this.switchUserColor('#323232'); - this.switchUserBackgroundColor('#DFDFDF'); - this.switchUserVariantColor('#BDDDF5'); - break; - case ColorScheme.Dark: - this.switchUserColor('#DFDFDF'); - this.switchUserBackgroundColor('#323232'); - this.switchUserVariantColor('#4476F7'); - break; - case ColorScheme.CoolBlue: - this.switchUserColor('#ADEAFF'); - this.switchUserBackgroundColor('#060A15'); - this.switchUserVariantColor('#3C51FF'); - break; - case ColorScheme.Cupcake: - this.switchUserColor('#3BC7FF'); - this.switchUserBackgroundColor('#fffdf7'); - this.switchUserVariantColor('#FFD7F3'); - break; - case ColorScheme.Custom: - break; - } - }); - @computed get colorsContent() { const schemeMap = Array.from(Object.keys(ColorScheme)); const userTheme = StrCast(Doc.UserDoc().userTheme); @@ -227,7 +238,9 @@ export class SettingsManager extends React.Component<{}> { formLabel="Show document header" formLabelPlacement="right" toggleType={ToggleType.SWITCH} - onClick={e => (Doc.UserDoc().layout_showTitle = Doc.UserDoc().layout_showTitle ? undefined : 'author_date')} + onClick={() => { + Doc.UserDoc().layout_showTitle = Doc.UserDoc().layout_showTitle ? undefined : 'author_date'; + }} toggleStatus={Doc.UserDoc().layout_showTitle !== undefined} size={Size.XSMALL} color={SettingsManager.userColor} @@ -236,8 +249,10 @@ export class SettingsManager extends React.Component<{}> { formLabel="Show Full Toolbar" formLabelPlacement="right" toggleType={ToggleType.SWITCH} - onClick={e => (Doc.UserDoc()['documentLinksButton-fullMenu'] = !Doc.UserDoc()['documentLinksButton-fullMenu'])} - toggleStatus={BoolCast(Doc.UserDoc()['documentLinksButton-fullMenu'])} + onClick={() => { + Doc.UserDoc().documentLinksButton_fullMenu = !Doc.UserDoc().documentLinksButton_fullMenu; + }} + toggleStatus={BoolCast(Doc.UserDoc().documentLinksButton_fullMenu)} size={Size.XSMALL} color={SettingsManager.userColor} /> @@ -245,7 +260,9 @@ export class SettingsManager extends React.Component<{}> { formLabel="Show Button Labels" formLabelPlacement="right" toggleType={ToggleType.SWITCH} - onClick={e => (FontIconBox.ShowIconLabels = !FontIconBox.ShowIconLabels)} + onClick={() => { + FontIconBox.ShowIconLabels = !FontIconBox.ShowIconLabels; + }} toggleStatus={FontIconBox.ShowIconLabels} size={Size.XSMALL} color={SettingsManager.userColor} @@ -254,7 +271,9 @@ export class SettingsManager extends React.Component<{}> { formLabel="Recognize Ink Gestures" formLabelPlacement="right" toggleType={ToggleType.SWITCH} - onClick={e => (GestureOverlay.RecognizeGestures = !GestureOverlay.RecognizeGestures)} + onClick={() => { + GestureOverlay.RecognizeGestures = !GestureOverlay.RecognizeGestures; + }} toggleStatus={GestureOverlay.RecognizeGestures} size={Size.XSMALL} color={SettingsManager.userColor} @@ -263,7 +282,9 @@ export class SettingsManager extends React.Component<{}> { formLabel="Hide Labels In Ink Shapes" formLabelPlacement="right" toggleType={ToggleType.SWITCH} - onClick={e => (Doc.UserDoc().activeInkHideTextLabels = !Doc.UserDoc().activeInkHideTextLabels)} + onClick={() => { + Doc.UserDoc().activeInkHideTextLabels = !Doc.UserDoc().activeInkHideTextLabels; + }} toggleStatus={BoolCast(Doc.UserDoc().activeInkHideTextLabels)} size={Size.XSMALL} color={SettingsManager.userColor} @@ -272,7 +293,9 @@ export class SettingsManager extends React.Component<{}> { formLabel="Open Ink Docs in Lightbox" formLabelPlacement="right" toggleType={ToggleType.SWITCH} - onClick={e => (Doc.UserDoc().openInkInLightbox = !Doc.UserDoc().openInkInLightbox)} + onClick={() => { + Doc.UserDoc().openInkInLightbox = !Doc.UserDoc().openInkInLightbox; + }} toggleStatus={BoolCast(Doc.UserDoc().openInkInLightbox)} size={Size.XSMALL} color={SettingsManager.userColor} @@ -281,7 +304,9 @@ export class SettingsManager extends React.Component<{}> { formLabel="Show Link Lines" formLabelPlacement="right" toggleType={ToggleType.SWITCH} - onClick={e => (Doc.UserDoc().showLinkLines = !Doc.UserDoc().showLinkLines)} + onClick={() => { + Doc.UserDoc().showLinkLines = !Doc.UserDoc().showLinkLines; + }} toggleStatus={BoolCast(Doc.UserDoc().showLinkLines)} size={Size.XSMALL} color={SettingsManager.userColor} @@ -296,7 +321,9 @@ export class SettingsManager extends React.Component<{}> { step={2} type={Type.TERT} unit="px" - setNumber={val => console.log('GOT: ' + (Doc.UserDoc().headerHeight = val))} + setNumber={val => { + Doc.UserDoc().headerHeight = val; + }} /> </Group> </div> @@ -320,7 +347,6 @@ export class SettingsManager extends React.Component<{}> { @computed get textContent() { const fontFamilies = ['Times New Roman', 'Arial', 'Georgia', 'Comic Sans MS', 'Tahoma', 'Impact', 'Crimson Text', 'Roboto']; - const fontSizes = ['7px', '8px', '9px', '10px', '12px', '14px', '16px', '18px', '20px', '24px', '32px', '48px', '72px']; return ( <div className="tab-content appearances-content"> @@ -338,19 +364,19 @@ export class SettingsManager extends React.Component<{}> { type={Type.PRIM} number={NumCast(Doc.UserDoc().fontSize, Number(StrCast(Doc.UserDoc().fontSize).replace('px', '')))} unit="px" - setNumber={val => (Doc.UserDoc().fontSize = val + 'px')} + setNumber={val => { + Doc.UserDoc().fontSize = val + 'px'; + }} /> <Dropdown - items={fontFamilies.map(val => { - return { - text: val, - val: val, - style: { - fontFamily: val, - }, - }; - })} - closeOnSelect={true} + items={fontFamilies.map(val => ({ + text: val, + val: val, + style: { + fontFamily: val, + }, + }))} + closeOnSelect dropdownType={DropdownType.SELECT} type={Type.TERT} selectedVal={StrCast(Doc.UserDoc().fontFamily)} @@ -367,28 +393,13 @@ export class SettingsManager extends React.Component<{}> { ); } - @action - changeVal = (value: string, pass: string) => { - switch (pass) { - case 'curr': - this.curr_password = value; - break; - case 'new': - this.new_password = value; - break; - case 'conf': - this.new_confirm = value; - break; - } - }; - @computed get passwordContent() { return ( <div className="password-content"> <EditableText placeholder="Current password" type={Type.SEC} color={SettingsManager.userColor} val="" setVal={val => this.changeVal(val as string, 'curr')} fillWidth password /> <EditableText placeholder="New password" type={Type.SEC} color={SettingsManager.userColor} val="" setVal={val => this.changeVal(val as string, 'new')} fillWidth password /> <EditableText placeholder="Confirm new password" type={Type.SEC} color={SettingsManager.userColor} val="" setVal={val => this.changeVal(val as string, 'conf')} fillWidth password /> - {!this.passwordResultText ? null : <div className={`${this.passwordResultText.startsWith('Error') ? 'error' : 'success'}-text`}>{this.passwordResultText}</div>} + {!this._passwordResultText ? null : <div className={`${this._passwordResultText.startsWith('Error') ? 'error' : 'success'}-text`}>{this._passwordResultText}</div>} <Button type={Type.SEC} text="Forgot Password" color={SettingsManager.userColor} /> <Button type={Type.TERT} text="Submit" onClick={this.changePassword} color={SettingsManager.userColor} /> </div> @@ -418,10 +429,6 @@ export class SettingsManager extends React.Component<{}> { ); } - setFreeformScrollMode = (mode: string) => { - Doc.UserDoc().freeformScrollMode = mode; - }; - @computed get modesContent() { return ( <div className="tab-content modes-content"> @@ -430,7 +437,7 @@ export class SettingsManager extends React.Component<{}> { <div className="tab-column-content"> <Dropdown formLabel="Mode" - closeOnSelect={true} + closeOnSelect items={[ { text: 'Novice', @@ -454,7 +461,7 @@ export class SettingsManager extends React.Component<{}> { color={SettingsManager.userColor} fillWidth /> - <Toggle formLabel="Playground Mode" toggleType={ToggleType.SWITCH} toggleStatus={this.playgroundMode} onClick={this.playgroundModeToggle} color={SettingsManager.userColor} /> + <Toggle formLabel="Playground Mode" toggleType={ToggleType.SWITCH} toggleStatus={this._playgroundMode} onClick={this.playgroundModeToggle} color={SettingsManager.userColor} /> </div> <div className="tab-column-title" style={{ marginTop: 20, marginBottom: 10 }}> Freeform Navigation @@ -462,7 +469,7 @@ export class SettingsManager extends React.Component<{}> { <div className="tab-column-content"> <Dropdown formLabel="Scroll Mode" - closeOnSelect={true} + closeOnSelect items={[ { text: 'Scroll to Pan', @@ -493,10 +500,28 @@ export class SettingsManager extends React.Component<{}> { formLabel="Default access private" color={SettingsManager.userColor} toggleStatus={BoolCast(Doc.defaultAclPrivate)} - onClick={action(() => (Doc.defaultAclPrivate = !Doc.defaultAclPrivate))} + onClick={action(() => { + Doc.defaultAclPrivate = !Doc.defaultAclPrivate; + })} + /> + <Toggle + toggleType={ToggleType.SWITCH} + formLabel="Enable Sharing UI" + color={SettingsManager.userColor} + toggleStatus={BoolCast(Doc.IsSharingEnabled)} + onClick={action(() => { + Doc.IsSharingEnabled = !Doc.IsSharingEnabled; + })} + /> + <Toggle + toggleType={ToggleType.SWITCH} + formLabel="Disable Info UI" + color={SettingsManager.userColor} + toggleStatus={BoolCast(Doc.IsInfoUIDisabled)} + onClick={action(() => { + Doc.IsInfoUIDisabled = !Doc.IsInfoUIDisabled; + })} /> - <Toggle toggleType={ToggleType.SWITCH} formLabel="Enable Sharing UI" color={SettingsManager.userColor} toggleStatus={BoolCast(Doc.IsSharingEnabled)} onClick={action(() => (Doc.IsSharingEnabled = !Doc.IsSharingEnabled))} /> - <Toggle toggleType={ToggleType.SWITCH} formLabel="Disable Info UI" color={SettingsManager.userColor} toggleStatus={BoolCast(Doc.IsInfoUIDisabled)} onClick={action(() => (Doc.IsInfoUIDisabled = !Doc.IsInfoUIDisabled))} /> </div> </div> </div> @@ -518,7 +543,7 @@ export class SettingsManager extends React.Component<{}> { <div className="settings-panel" style={{ background: SettingsManager.userColor }}> <div className="settings-tabs"> {tabs.map(tab => { - const isActive = this.activeTab === tab.title; + const isActive = this._activeTab === tab.title; return ( <div key={tab.title} @@ -527,7 +552,9 @@ export class SettingsManager extends React.Component<{}> { color: isActive ? SettingsManager.userColor : SettingsManager.userBackgroundColor, }} className={'tab-control ' + (isActive ? 'active' : 'inactive')} - onClick={action(() => (this.activeTab = tab.title))}> + onClick={action(() => { + this._activeTab = tab.title; + })}> {tab.title} </div> ); @@ -544,12 +571,12 @@ export class SettingsManager extends React.Component<{}> { </div> <div className="close-button"> - <Button icon={<FontAwesomeIcon icon="times" size="lg" />} onClick={this.close} color={SettingsManager.userColor} /> + <Button icon={<FontAwesomeIcon icon="times" size="lg" />} onClick={this.closeMgr} color={SettingsManager.userColor} /> </div> <div className="settings-content" style={{ color: SettingsManager.userColor, background: SettingsManager.userBackgroundColor }}> {tabs.map(tab => ( - <div key={tab.title} className={'tab-section ' + (this.activeTab === tab.title ? 'active' : 'inactive')}> + <div key={tab.title} className={'tab-section ' + (this._activeTab === tab.title ? 'active' : 'inactive')}> {tab.ele} </div> ))} @@ -558,13 +585,37 @@ export class SettingsManager extends React.Component<{}> { ); } + 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!'; + }); + } + }; + + @action + changeVal = (value: string, pass: string) => { + switch (pass) { + case 'curr': this._curr_password = value; break; + case 'new': this._new_password = value; break; + case 'conf': this._new_confirm = value; break; + default: + } // prettier-ignore + }; + render() { return ( <MainViewModal contents={this.settingsInterface} - isDisplayed={this.isOpen} - interactive={true} - closeOnExternalClick={this.close} + isDisplayed={this._isOpen} + interactive + closeOnExternalClick={this.closeMgr} dialogueBoxStyle={{ width: 'fit-content', height: '300px', background: Cast(Doc.UserDoc().userColor, 'string', null) }} /> ); diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx index ade4cc218..6676e4e03 100644 --- a/src/client/util/SharingManager.tsx +++ b/src/client/util/SharingManager.tsx @@ -1,3 +1,6 @@ +/* eslint-disable jsx-a11y/label-has-associated-control */ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Button, IconButton, Size, Type } from 'browndash-components'; import { concat, intersection } from 'lodash'; @@ -65,7 +68,10 @@ interface ValidatedUser { @observer export class SharingManager extends React.Component<{}> { + // eslint-disable-next-line no-use-before-define public static Instance: SharingManager; + private shareDocumentButtonRef: React.RefObject<HTMLButtonElement> = React.createRef(); // ref for the share button, used for the position of the popup + private populating: boolean = false; // whether the list of users is populating or not @observable private isOpen = false; // whether the SharingManager modal is open or not @observable public users: ValidatedUser[] = []; // the list of users with sharing docs @observable private targetDoc: Doc | undefined = undefined; // the document being shared @@ -77,11 +83,9 @@ export class SharingManager extends React.Component<{}> { @observable private permissions: SharingPermissions = SharingPermissions.Edit; // the permission with which to share with other users @observable private individualSort: 'ascending' | 'descending' | 'none' = 'none'; // sorting options for the list of individuals @observable private groupSort: 'ascending' | 'descending' | 'none' = 'none'; // sorting options for the list of groups - private shareDocumentButtonRef: React.RefObject<HTMLButtonElement> = React.createRef(); // ref for the share button, used for the position of the popup // if both showUserOptions and showGroupOptions are false then both are displayed @observable private showUserOptions: boolean = false; // whether to show individuals as options when sharing (in the react-select component) @observable private showGroupOptions: boolean = false; // // whether to show groups as options when sharing (in the react-select component) - private populating: boolean = false; // whether the list of users is populating or not @observable private upgradeNested: boolean = false; // whether child docs in a collection/dashboard should be changed to be less private - initially selected so default is upgrade all @observable private layoutDocAcls: boolean = false; // whether the layout doc or data doc's acls are to be used @observable private myDocAcls: boolean = false; // whether the My Docs checkbox is selected or not @@ -90,33 +94,6 @@ export class SharingManager extends React.Component<{}> { // return this.targetDoc ? this.targetDoc["acl-" + PublicKey] !== SharingPermissions.None : false; // } - public open = (target?: DocumentView, target_doc?: Doc) => { - this.populateUsers(); - runInAction(() => { - this.targetDocView = target; - this.targetDoc = target_doc || target?.Document; - DictationOverlay.Instance.hasActiveModal = true; - this.isOpen = this.targetDoc !== undefined; - this.permissions = SharingPermissions.Augment; - this.upgradeNested = true; - }); - }; - - public close = action(() => { - this.isOpen = false; - this.selectedUsers = null; // resets the list of users and selected users (in the react-select component) - TaskCompletionBox.taskCompleted = false; - setTimeout( - action(() => { - // this.copied = false; - DictationOverlay.Instance.hasActiveModal = false; - this.targetDoc = undefined; - }), - 500 - ); - this.layoutDocAcls = false; - }); - constructor(props: {}) { super(props); makeObservable(this); @@ -131,226 +108,6 @@ export class SharingManager extends React.Component<{}> { } /** - * Populates the list of validated users (this.users) by adding registered users which have a sharingDocument. - */ - populateUsers = async () => { - if (!this.populating && Doc.UserDoc()[Id] !== Utils.GuestID()) { - this.populating = true; - const userList = await RequestPromise.get(ClientUtils.prepend('/getUsers')); - const raw = (JSON.parse(userList) as User[]).filter(user => user.email !== 'guest' && user.email !== ClientUtils.CurrentUserEmail()); - runInAction(() => { - FieldLoader.ServerLoadStatus.message = 'users'; - }); - const docs = await DocServer.GetRefFields(raw.reduce((list, user) => [...list, user.sharingDocumentId, user.linkDatabaseId], [] as string[])); - raw.map( - action((newUser: User) => { - const sharingDoc = docs[newUser.sharingDocumentId]; - const linkDatabase = docs[newUser.linkDatabaseId]; - if (sharingDoc instanceof Doc && linkDatabase instanceof Doc) { - if (!this.users.find(user => user.user.email === newUser.email)) { - this.users.push({ user: newUser, sharingDoc, linkDatabase, userColor: StrCast(sharingDoc.userColor) }); - //LinkManager.addLinkDB(linkDatabase); - } - } - }) - ); - this.populating = false; - } - }; - - /** - * Shares the document with a user. - */ - setInternalSharing = undoable((recipient: ValidatedUser, permission: string, targetDoc: Doc | undefined) => { - const { user, sharingDoc } = recipient; - const target = targetDoc || this.targetDoc!; - const acl = `acl-${normalizeEmail(user.email)}`; - const docs = SelectionManager.Views.length < 2 ? [target] : SelectionManager.Views.map(docView => docView.Document); - docs.map(doc => (this.layoutDocAcls || doc.dockingConfig ? doc : Doc.GetProto(doc))).forEach(doc => { - distributeAcls(acl, permission as SharingPermissions, doc, undefined, this.upgradeNested ? true : undefined); - if (permission !== SharingPermissions.None) { - Doc.AddDocToList(sharingDoc, doc.dockingConfig ? dashStorage : storage, doc); - } else GetEffectiveAcl(doc, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, ((doc.createdFrom as Doc) || doc).dockingConfig ? dashStorage : storage, (doc.createdFrom as Doc) || doc); - }); - }, 'set Doc permissions'); - - /** - * Sets the permission on the target for the group. - * @param group - * @param permission - */ - setInternalGroupSharing = undoable((group: Doc | { title: string }, permission: string, targetDoc?: Doc) => { - const target = targetDoc || this.targetDoc!; - const acl = `acl-${normalizeEmail(StrCast(group.title))}`; - - const docs = SelectionManager.Views.length < 2 ? [target] : SelectionManager.Views.map(docView => docView.Document); - docs.map(doc => (this.layoutDocAcls || doc.dockingConfig ? doc : Doc.GetProto(doc))).forEach(doc => { - distributeAcls(acl, permission as SharingPermissions, doc, undefined, this.upgradeNested ? true : undefined); - - if (group instanceof Doc) { - Doc.AddDocToList(group, 'docsShared', doc); - - this.users - .filter(({ user: { email } }) => JSON.parse(StrCast(group.members)).includes(email)) - .forEach(({ user, sharingDoc }) => { - if (permission !== SharingPermissions.None) - Doc.AddDocToList(sharingDoc, doc.dockingConfig ? dashStorage : storage, doc); // add the doc to the sharingDoc if it hasn't already been added - else GetEffectiveAcl(doc, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, ((doc.createdFrom as Doc) || doc).dockingConfig ? dashStorage : storage, (doc.createdFrom as Doc) || doc); // remove the doc from the list if it already exists - }); - } - }); - }, 'set group permissions'); - - /** - * Shares the documents shared with a group with a new user who has been added to that group. - * @param group - * @param emailId - */ - shareWithAddedMember = (group: Doc, emailId: string, retry: boolean = true) => { - const user = this.users.find(({ user: { email } }) => email === emailId)!; - const self = this; - if (group.docsShared) { - if (!user) retry && this.populateUsers().then(() => self.shareWithAddedMember(group, emailId, false)); - else { - DocListCastAsync(user.sharingDoc[storage]).then(userdocs => - DocListCastAsync(group.docsShared).then(dl => { - const filtered = dl?.filter(doc => !doc.dockingConfig && !userdocs?.includes(doc)); - filtered && userdocs?.push(...filtered); - }) - ); - DocListCastAsync(user.sharingDoc[dashStorage]).then(userdocs => - DocListCastAsync(group.docsShared).then(dl => { - const filtered = dl?.filter(doc => doc.dockingConfig && !userdocs?.includes(doc)); - filtered && userdocs?.push(...filtered); - }) - ); - } - } - }; - - /** - * Called from the properties sidebar to change permissions of a user. - */ - shareFromPropertiesSidebar = undoable((shareWith: string, permission: SharingPermissions, docs: Doc[], layout: boolean) => { - if (layout) this.layoutDocAcls = true; - if (shareWith !== 'Guest') { - const user = this.users.find(({ user: { email } }) => email === (shareWith === 'Me' ? ClientUtils.CurrentUserEmail() : shareWith)); - docs.forEach(doc => { - if (user) this.setInternalSharing(user, permission, doc); - else this.setInternalGroupSharing(GroupManager.Instance.getGroup(shareWith)!, permission, doc, undefined, true); - }); - } else { - docs.forEach(doc => { - if (GetEffectiveAcl(doc) === AclAdmin) { - distributeAcls(`acl-${shareWith}`, permission, doc, undefined); - } - }); - } - this.layoutDocAcls = false; - }, 'sidebar set permissions'); - - /** - * Removes the documents shared with a user through a group when the user is removed from the group. - * @param group - * @param emailId - */ - removeMember = (group: Doc, emailId: string) => { - const user: ValidatedUser = this.users.find(({ user: { email } }) => email === emailId)!; - - if (group.docsShared && user) { - DocListCastAsync(user.sharingDoc[storage]).then(userdocs => - DocListCastAsync(group.docsShared).then(dl => { - const remaining = userdocs?.filter(doc => !dl?.includes(doc)) || []; - userdocs?.splice(0, userdocs.length, ...remaining); - }) - ); - DocListCastAsync(user.sharingDoc[dashStorage]).then(userdocs => - DocListCastAsync(group.docsShared).then(dl => { - const remaining = userdocs?.filter(doc => !dl?.includes(doc)) || []; - userdocs?.splice(0, userdocs.length, ...remaining); - }) - ); - } - }; - - /** - * Removes a group's permissions from documents that have been shared with it. - * @param group - */ - removeGroup = (group: Doc) => { - if (group.docsShared) { - DocListCast(group.docsShared).forEach(doc => { - const acl = `acl-${StrCast(group.title)}`; - 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(({ sharingDoc }) => Doc.RemoveDocFromList(sharingDoc, storage, doc)); - }); - } - }; - - // private setExternalSharing = (permission: string) => { - // const targetDoc = this.targetDoc; - // if (!targetDoc) { - // return; - // } - // targetDoc["acl-" + PublicKey] = permission; - // }s - - /** - * Copies the Public sharing url to the user's clipboard. - */ - private copyURL = (e: any) => { - ClientUtils.CopyText(ClientUtils.shareUrl(this.targetDoc![Id])); - }; - - /** - * Returns the SharingPermissions (Admin, Can Edit etc) access that's used to share - */ - private sharingOptions(uniform: boolean, showGuestOptions?: boolean) { - const dropdownValues: string[] = showGuestOptions ? [SharingPermissions.None, SharingPermissions.View] : Object.values(SharingPermissions); - if (!uniform) dropdownValues.unshift('-multiple-'); - return dropdownValues.map(permission => ( - <option key={permission} value={permission}> - {concat(ReverseHierarchyMap.get(permission)?.image, ' ', permission)} - </option> - )); - } - - private focusOn = (contents: string) => { - const title = this.targetDoc ? StrCast(this.targetDoc.title) : ''; - const docs = SelectionManager.Views.length > 1 ? SelectionManager.Views.map(docView => docView.props.Document) : [this.targetDoc]; - return ( - <span - className="focus-span" - title={title} - onClick={() => { - if (this.targetDoc && this.targetDocView && docs.length === 1) { - DocumentManager.Instance.showDocument(this.targetDoc, { willZoomCentered: true }); - } - }} - onPointerEnter={action(() => { - if (docs.length) { - docs.forEach(doc => doc && Doc.BrushDoc(doc)); - this.dialogueBoxOpacity = 0.1; - this.overlayOpacity = 0.1; - } - })} - onPointerLeave={action(() => { - if (docs.length) { - docs.forEach(doc => doc && Doc.UnBrushDoc(doc)); - this.dialogueBoxOpacity = 1; - this.overlayOpacity = 0.4; - } - })}> - {contents} - </span> - ); - }; - - /** * Handles changes in the users selected in react-select */ @action @@ -369,57 +126,6 @@ export class SharingManager extends React.Component<{}> { ); /** - * Calls the relevant method for sharing, displays the popup, and resets the relevant variables. - */ - share = undoable( - action(() => { - if (this.selectedUsers) { - this.selectedUsers.forEach(user => { - if (user.value.includes(indType)) { - this.setInternalSharing(this.users.find(u => u.user.email === user.label)!, this.permissions, undefined); - } else { - this.setInternalGroupSharing(GroupManager.Instance.getGroup(user.label)!, this.permissions); - } - }); - - if (this.shareDocumentButtonRef.current) { - const { left, width, top, height } = this.shareDocumentButtonRef.current.getBoundingClientRect(); - TaskCompletionBox.popupX = left - 1.5 * width; - TaskCompletionBox.popupY = top - 1.5 * height; - TaskCompletionBox.textDisplayed = 'Document shared!'; - TaskCompletionBox.taskCompleted = true; - setTimeout( - action(() => (TaskCompletionBox.taskCompleted = false)), - 2000 - ); - } - - this.layoutDocAcls = false; - this.selectedUsers = null; - } - }), - 'share Doc' - ); - - /** - * 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.title); - const g2 = StrCast(group2.title); - return g1 < g2 ? -1 : g1 === g2 ? 0 : 1; - }; - - /** * @returns the main interface of the SharingManager. */ @computed get sharingInterface() { @@ -477,8 +183,8 @@ export class SharingManager extends React.Component<{}> { permissions = uniform ? StrCast(targetDoc?.[userKey]) : '-multiple-'; return !permissions ? null : ( - <div key={userKey} className={'container'}> - <span className={'padding'}>{user.email}</span> + <div key={userKey} className="container"> + <span className="padding">{user.email}</span> <div className="edit-actions"> {admin || this.myDocAcls ? ( <select className={`permissions-dropdown-${permissions}`} value={permissions} onChange={e => this.setInternalSharing({ user, linkDatabase, sharingDoc, userColor }, e.currentTarget.value, undefined)}> @@ -504,16 +210,16 @@ export class SharingManager extends React.Component<{}> { // const curUserPermission = HierarchyMapping.get(effectiveAcls[0])!.name userListContents.unshift( sameAuthor ? ( - <div key={'owner'} className={'container'}> + <div key="owner" className="container"> <span className="padding">{targetDoc?.author === ClientUtils.CurrentUserEmail() ? 'Me' : StrCast(targetDoc?.author)}</span> <div className="edit-actions"> - <div className={'permissions-dropdown'}>Owner</div> + <div className="permissions-dropdown">Owner</div> </div> </div> ) : null, sameAuthor && targetDoc?.author !== ClientUtils.CurrentUserEmail() ? ( - <div key={'me'} className={'container'}> - <span className={'padding'}>Me</span> + <div key="me" className="container"> + <span className="padding">Me</span> <div className="edit-actions"> <div className={`permissions-dropdown-${curUserPermission}`}> {effectiveAcls.every(acl => acl === effectiveAcls[0]) ? concat(ReverseHierarchyMap.get(curUserPermission!)?.image, ' ', curUserPermission) : '-multiple-'} @@ -526,18 +232,27 @@ export class SharingManager extends React.Component<{}> { // the list of groups shared with const groupListMap: (Doc | { title: string })[] = groups.filter(({ title }) => (docs.length > 1 ? commonKeys.includes(`acl-${normalizeEmail(StrCast(title))}`) : true)); - groupListMap.unshift({ title: 'Guest' }); //, { title: "ALL" }); + groupListMap.unshift({ title: 'Guest' }); // , { title: "ALL" }); const groupListContents = groupListMap.map(group => { - let groupKey = `acl-${StrCast(group.title)}`; + const groupKey = `acl-${StrCast(group.title)}`; const uniform = docs.every(doc => doc?.[DocAcl]?.[groupKey] === docs[0]?.[DocAcl]?.[groupKey]); const permissions = uniform ? StrCast(targetDoc?.[groupKey]) : '-multiple-'; return !permissions ? null : ( - <div key={groupKey} className={'container'} style={{ background: SettingsManager.userBackgroundColor, color: SettingsManager.userColor }}> - <div className={'padding'}>{StrCast(group.title)}</div> + <div key={groupKey} className="container" style={{ background: SettingsManager.userBackgroundColor, color: SettingsManager.userColor }}> + <div className="padding">{StrCast(group.title)}</div> - {group instanceof Doc ? <IconButton icon={<FontAwesomeIcon icon={'info-circle'} />} size={Size.XSMALL} color={SettingsManager.userColor} onClick={action(() => (GroupManager.Instance.currentGroup = group))} /> : null} - <div className={'edit-actions'}> + {group instanceof Doc ? ( + <IconButton + icon={<FontAwesomeIcon icon="info-circle" />} + size={Size.XSMALL} + color={SettingsManager.userColor} + onClick={action(() => { + GroupManager.Instance.currentGroup = group; + })} + /> + ) : null} + <div className="edit-actions"> {admin || this.myDocAcls ? ( <select className={`permissions-dropdown-${permissions}`} value={permissions} onChange={e => this.setInternalGroupSharing(group, e.currentTarget.value)}> {this.sharingOptions(uniform, group.title === 'Guest')} @@ -554,7 +269,14 @@ export class SharingManager extends React.Component<{}> { }); return ( <div className="sharing-interface"> - {GroupManager.Instance?.currentGroup ? <GroupMemberView group={GroupManager.Instance.currentGroup} onCloseButtonClick={action(() => (GroupManager.Instance.currentGroup = undefined))} /> : null} + {GroupManager.Instance?.currentGroup ? ( + <GroupMemberView + group={GroupManager.Instance.currentGroup} + onCloseButtonClick={action(() => { + GroupManager.Instance.currentGroup = undefined; + })} + /> + ) : null} <div className="sharing-contents" style={{ @@ -563,16 +285,16 @@ export class SharingManager extends React.Component<{}> { }}> <p className="share-title" style={{ color: SettingsManager.userColor }}> <div className="share-info" onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/features/collaboration/', '_blank')}> - <FontAwesomeIcon icon={'question-circle'} size={'sm'} onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/features/collaboration/', '_blank')} /> + <FontAwesomeIcon icon="question-circle" size="sm" onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/features/collaboration/', '_blank')} /> </div> <b>Share </b> {this.focusOn(docs.length < 2 ? StrCast(targetDoc?.title, 'this document') : '-multiple-')} </p> <div className="share-copy-link"> - <Button type={Type.TERT} color={SettingsManager.userColor} icon={<FontAwesomeIcon icon={'copy'} size="sm" />} iconPlacement={'left'} text={'Copy Guest URL'} onClick={this.copyURL} /> + <Button type={Type.TERT} color={SettingsManager.userColor} icon={<FontAwesomeIcon icon="copy" size="sm" />} iconPlacement="left" text="Copy Guest URL" onClick={this.copyURL} /> </div> <div className="close-button"> - <Button icon={<FontAwesomeIcon icon={'times'} size={'lg'} />} onClick={this.close} color={SettingsManager.userColor} /> + <Button icon={<FontAwesomeIcon icon="times" size="lg" />} onClick={this.close} color={SettingsManager.userColor} /> </div> {admin ? ( <div className="share-container"> @@ -614,19 +336,45 @@ export class SharingManager extends React.Component<{}> { </select> </div> <div className="share-button"> - <Button text={'SHARE'} type={Type.TERT} color={SettingsManager.userColor} onClick={this.share} /> + <Button text="SHARE" type={Type.TERT} color={SettingsManager.userColor} onClick={this.share} /> </div> </div> <div className="sort-checkboxes"> - <input type="checkbox" onChange={action(() => (this.showUserOptions = !this.showUserOptions))} /> <label style={{ marginRight: 10 }}>Individuals</label> - <input type="checkbox" onChange={action(() => (this.showGroupOptions = !this.showGroupOptions))} /> <label>Groups</label> + <input + type="checkbox" + onChange={action(() => { + this.showUserOptions = !this.showUserOptions; + })} + />{' '} + <label style={{ marginRight: 10 }}>Individuals</label> + <input + type="checkbox" + onChange={action(() => { + this.showGroupOptions = !this.showGroupOptions; + })} + />{' '} + <label>Groups</label> </div> <div className="acl-container"> {Doc.noviceMode ? null : ( <div className="layoutDoc-acls"> - <input type="checkbox" onChange={action(() => (this.upgradeNested = !this.upgradeNested))} checked={this.upgradeNested} /> <label>Upgrade Nested </label> - <input type="checkbox" onChange={action(() => (this.layoutDocAcls = !this.layoutDocAcls))} checked={this.layoutDocAcls} /> <label>Layout</label> + <input + type="checkbox" + onChange={action(() => { + this.upgradeNested = !this.upgradeNested; + })} + checked={this.upgradeNested} + />{' '} + <label>Upgrade Nested </label> + <input + type="checkbox" + onChange={action(() => { + this.layoutDocAcls = !this.layoutDocAcls; + })} + checked={this.layoutDocAcls} + />{' '} + <label>Layout</label> </div> )} </div> @@ -635,14 +383,25 @@ export class SharingManager extends React.Component<{}> { <div className="share-container"> <div className="acl-container"> <div className="layoutDoc-acls"> - <input type="checkbox" onChange={action(() => (this.layoutDocAcls = !this.layoutDocAcls))} checked={this.layoutDocAcls} /> <label>Layout</label> + <input + type="checkbox" + onChange={action(() => { + this.layoutDocAcls = !this.layoutDocAcls; + })} + checked={this.layoutDocAcls} + />{' '} + <label>Layout</label> </div> </div> </div> )} <div className="main-container" style={{ color: StrCast(Doc.UserDoc().userColor), border: StrCast(Doc.UserDoc().userColor) }}> - <div className={'individual-container'}> - <div className="user-sort" onClick={action(() => (this.individualSort = this.individualSort === 'ascending' ? 'descending' : this.individualSort === 'descending' ? 'none' : 'ascending'))}> + <div className="individual-container"> + <div + className="user-sort" + onClick={action(() => { + this.individualSort = this.individualSort === 'ascending' ? 'descending' : this.individualSort === 'descending' ? 'none' : 'ascending'; + })}> <div className="title-individual"> Individuals <IconButton @@ -654,11 +413,15 @@ export class SharingManager extends React.Component<{}> { </div> <div className="users-list">{userListContents}</div> </div> - <div className={'group-container'}> - <div className="user-sort" onClick={action(() => (this.groupSort = this.groupSort === 'ascending' ? 'descending' : this.groupSort === 'descending' ? 'none' : 'ascending'))}> + <div className="group-container"> + <div + className="user-sort" + onClick={action(() => { + this.groupSort = this.groupSort === 'ascending' ? 'descending' : this.groupSort === 'descending' ? 'none' : 'ascending'; + })}> <div className="title-group"> Groups - <IconButton icon={<FontAwesomeIcon icon={'info-circle'} />} size={Size.XSMALL} color={StrCast(Doc.UserDoc().userColor)} onClick={action(() => GroupManager.Instance.open())} /> + <IconButton icon={<FontAwesomeIcon icon="info-circle" />} size={Size.XSMALL} color={StrCast(Doc.UserDoc().userColor)} onClick={action(() => GroupManager.Instance.open())} /> <IconButton icon={<FontAwesomeIcon icon={this.groupSort === 'ascending' ? 'caret-up' : this.groupSort === 'descending' ? 'caret-down' : 'caret-right'} />} size={Size.XSMALL} @@ -666,7 +429,7 @@ export class SharingManager extends React.Component<{}> { /> </div> </div> - <div className={'groups-list'}>{groupListContents}</div> + <div className="groups-list">{groupListContents}</div> </div> </div> </div> @@ -674,7 +437,311 @@ export class SharingManager extends React.Component<{}> { ); } + /** + * Shares the document with a user. + */ + setInternalSharing = undoable((recipient: ValidatedUser, permission: string, targetDoc: Doc | undefined) => { + const { user, sharingDoc } = recipient; + const target = targetDoc || this.targetDoc!; + const acl = `acl-${normalizeEmail(user.email)}`; + const docs = SelectionManager.Views.length < 2 ? [target] : SelectionManager.Views.map(docView => docView.Document); + docs.map(doc => (this.layoutDocAcls || doc.dockingConfig ? doc : Doc.GetProto(doc))).forEach(doc => { + distributeAcls(acl, permission as SharingPermissions, doc, undefined, this.upgradeNested ? true : undefined); + if (permission !== SharingPermissions.None) { + Doc.AddDocToList(sharingDoc, doc.dockingConfig ? dashStorage : storage, doc); + } else GetEffectiveAcl(doc, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, ((doc.createdFrom as Doc) || doc).dockingConfig ? dashStorage : storage, (doc.createdFrom as Doc) || doc); + }); + }, 'set Doc permissions'); + + /** + * Sets the permission on the target for the group. + * @param group + * @param permission + */ + setInternalGroupSharing = undoable((group: Doc | { title: string }, permission: string, targetDoc?: Doc) => { + const target = targetDoc || this.targetDoc!; + const acl = `acl-${normalizeEmail(StrCast(group.title))}`; + + const docs = SelectionManager.Views.length < 2 ? [target] : SelectionManager.Views.map(docView => docView.Document); + docs.map(doc => (this.layoutDocAcls || doc.dockingConfig ? doc : Doc.GetProto(doc))).forEach(doc => { + distributeAcls(acl, permission as SharingPermissions, doc, undefined, this.upgradeNested ? true : undefined); + + if (group instanceof Doc) { + Doc.AddDocToList(group, 'docsShared', doc); + + this.users + .filter(({ user: { email } }) => JSON.parse(StrCast(group.members)).includes(email)) + .forEach(({ user, sharingDoc }) => { + if (permission !== SharingPermissions.None) + Doc.AddDocToList(sharingDoc, doc.dockingConfig ? dashStorage : storage, doc); // add the doc to the sharingDoc if it hasn't already been added + else GetEffectiveAcl(doc, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, ((doc.createdFrom as Doc) || doc).dockingConfig ? dashStorage : storage, (doc.createdFrom as Doc) || doc); // remove the doc from the list if it already exists + }); + } + }); + }, 'set group permissions'); + /** + * Populates the list of validated users (this.users) by adding registered users which have a sharingDocument. + */ + populateUsers = async () => { + if (!this.populating && Doc.UserDoc()[Id] !== Utils.GuestID()) { + this.populating = true; + const userList = await RequestPromise.get(ClientUtils.prepend('/getUsers')); + const raw = (JSON.parse(userList) as User[]).filter(user => user.email !== 'guest' && user.email !== ClientUtils.CurrentUserEmail()); + runInAction(() => { + FieldLoader.ServerLoadStatus.message = 'users'; + }); + const docs = await DocServer.GetRefFields(raw.reduce((list, user) => [...list, user.sharingDocumentId, user.linkDatabaseId], [] as string[])); + raw.map( + action((newUser: User) => { + const sharingDoc = docs[newUser.sharingDocumentId]; + const linkDatabase = docs[newUser.linkDatabaseId]; + if (sharingDoc instanceof Doc && linkDatabase instanceof Doc) { + if (!this.users.find(user => user.user.email === newUser.email)) { + this.users.push({ user: newUser, sharingDoc, linkDatabase, userColor: StrCast(sharingDoc.userColor) }); + // LinkManager.addLinkDB(linkDatabase); + } + } + }) + ); + this.populating = false; + } + }; + + // eslint-disable-next-line react/sort-comp + public close = action(() => { + this.isOpen = false; + this.selectedUsers = null; // resets the list of users and selected users (in the react-select component) + TaskCompletionBox.taskCompleted = false; + setTimeout( + action(() => { + // this.copied = false; + DictationOverlay.Instance.hasActiveModal = false; + this.targetDoc = undefined; + }), + 500 + ); + this.layoutDocAcls = false; + }); + + // eslint-disable-next-line react/no-unused-class-component-methods + public open = (target?: DocumentView, targetDoc?: Doc) => { + this.populateUsers(); + runInAction(() => { + this.targetDocView = target; + this.targetDoc = targetDoc || target?.Document; + DictationOverlay.Instance.hasActiveModal = true; + this.isOpen = this.targetDoc !== undefined; + this.permissions = SharingPermissions.Augment; + this.upgradeNested = true; + }); + }; + + /** + * Shares the documents shared with a group with a new user who has been added to that group. + * @param group + * @param emailId + */ + // eslint-disable-next-line react/no-unused-class-component-methods + shareWithAddedMember = (group: Doc, emailId: string, retry: boolean = true) => { + const user = this.users.find(({ user: { email } }) => email === emailId)!; + const self = this; + if (group.docsShared) { + if (!user) retry && this.populateUsers().then(() => self.shareWithAddedMember(group, emailId, false)); + else { + DocListCastAsync(user.sharingDoc[storage]).then(userdocs => + DocListCastAsync(group.docsShared).then(dl => { + const filtered = dl?.filter(doc => !doc.dockingConfig && !userdocs?.includes(doc)); + filtered && userdocs?.push(...filtered); + }) + ); + DocListCastAsync(user.sharingDoc[dashStorage]).then(userdocs => + DocListCastAsync(group.docsShared).then(dl => { + const filtered = dl?.filter(doc => doc.dockingConfig && !userdocs?.includes(doc)); + filtered && userdocs?.push(...filtered); + }) + ); + } + } + }; + + /** + * Called from the properties sidebar to change permissions of a user. + */ + // eslint-disable-next-line react/no-unused-class-component-methods + shareFromPropertiesSidebar = undoable((shareWith: string, permission: SharingPermissions, docs: Doc[], layout: boolean) => { + if (layout) this.layoutDocAcls = true; + if (shareWith !== 'Guest') { + const user = this.users.find(({ user: { email } }) => email === (shareWith === 'Me' ? ClientUtils.CurrentUserEmail() : shareWith)); + docs.forEach(doc => { + if (user) this.setInternalSharing(user, permission, doc); + else this.setInternalGroupSharing(GroupManager.Instance.getGroup(shareWith)!, permission, doc, undefined, true); + }); + } else { + docs.forEach(doc => { + if (GetEffectiveAcl(doc) === AclAdmin) { + distributeAcls(`acl-${shareWith}`, permission, doc, undefined); + } + }); + } + this.layoutDocAcls = false; + }, 'sidebar set permissions'); + + /** + * Removes the documents shared with a user through a group when the user is removed from the group. + * @param group + * @param emailId + */ + // eslint-disable-next-line react/no-unused-class-component-methods + removeMember = (group: Doc, emailId: string) => { + const user: ValidatedUser = this.users.find(({ user: { email } }) => email === emailId)!; + + if (group.docsShared && user) { + DocListCastAsync(user.sharingDoc[storage]).then(userdocs => + DocListCastAsync(group.docsShared).then(dl => { + const remaining = userdocs?.filter(doc => !dl?.includes(doc)) || []; + userdocs?.splice(0, userdocs.length, ...remaining); + }) + ); + DocListCastAsync(user.sharingDoc[dashStorage]).then(userdocs => + DocListCastAsync(group.docsShared).then(dl => { + const remaining = userdocs?.filter(doc => !dl?.includes(doc)) || []; + userdocs?.splice(0, userdocs.length, ...remaining); + }) + ); + } + }; + + /** + * Removes a group's permissions from documents that have been shared with it. + * @param group + */ + // eslint-disable-next-line react/no-unused-class-component-methods + removeGroup = (group: Doc) => { + if (group.docsShared) { + DocListCast(group.docsShared).forEach(doc => { + const acl = `acl-${StrCast(group.title)}`; + 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(({ sharingDoc }) => Doc.RemoveDocFromList(sharingDoc, storage, doc)); + }); + } + }; + + // private setExternalSharing = (permission: string) => { + // const targetDoc = this.targetDoc; + // if (!targetDoc) { + // return; + // } + // targetDoc["acl-" + PublicKey] = permission; + // }s + + /** + * Copies the Public sharing url to the user's clipboard. + */ + private copyURL = () => { + ClientUtils.CopyText(ClientUtils.shareUrl(this.targetDoc![Id])); + }; + + private focusOn = (contents: string) => { + const title = this.targetDoc ? StrCast(this.targetDoc.title) : ''; + const docs = SelectionManager.Views.length > 1 ? SelectionManager.Views.map(docView => docView.props.Document) : [this.targetDoc]; + return ( + <span + className="focus-span" + title={title} + onClick={() => { + if (this.targetDoc && this.targetDocView && docs.length === 1) { + DocumentManager.Instance.showDocument(this.targetDoc, { willZoomCentered: true }); + } + }} + onPointerEnter={action(() => { + if (docs.length) { + docs.forEach(doc => doc && Doc.BrushDoc(doc)); + this.dialogueBoxOpacity = 0.1; + this.overlayOpacity = 0.1; + } + })} + onPointerLeave={action(() => { + if (docs.length) { + docs.forEach(doc => doc && Doc.UnBrushDoc(doc)); + this.dialogueBoxOpacity = 1; + this.overlayOpacity = 0.4; + } + })}> + {contents} + </span> + ); + }; + + /** + * Calls the relevant method for sharing, displays the popup, and resets the relevant variables. + */ + share = undoable( + action(() => { + if (this.selectedUsers) { + this.selectedUsers.forEach(user => { + if (user.value.includes(indType)) { + this.setInternalSharing(this.users.find(u => u.user.email === user.label)!, this.permissions, undefined); + } else { + this.setInternalGroupSharing(GroupManager.Instance.getGroup(user.label)!, this.permissions); + } + }); + + if (this.shareDocumentButtonRef.current) { + const { left, width, top, height } = this.shareDocumentButtonRef.current.getBoundingClientRect(); + TaskCompletionBox.popupX = left - 1.5 * width; + TaskCompletionBox.popupY = top - 1.5 * height; + TaskCompletionBox.textDisplayed = 'Document shared!'; + TaskCompletionBox.taskCompleted = true; + setTimeout( + action(() => { + TaskCompletionBox.taskCompleted = false; + }), + 2000 + ); + } + + this.layoutDocAcls = false; + this.selectedUsers = null; + } + }), + 'share Doc' + ); + + /** + * 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.title); + const g2 = StrCast(group2.title); + return g1 < g2 ? -1 : g1 === g2 ? 0 : 1; + }; + /** + * Returns the SharingPermissions (Admin, Can Edit etc) access that's used to share + */ + private sharingOptions(uniform: boolean, showGuestOptions?: boolean) { + const dropdownValues: string[] = showGuestOptions ? [SharingPermissions.None, SharingPermissions.View] : Object.values(SharingPermissions); + if (!uniform) dropdownValues.unshift('-multiple-'); + return dropdownValues.map(permission => ( + <option key={permission} value={permission}> + {concat(ReverseHierarchyMap.get(permission)?.image, ' ', permission)} + </option> + )); + } + render() { - return <MainViewModal contents={this.sharingInterface} isDisplayed={this.isOpen} interactive={true} dialogueBoxDisplayedOpacity={this.dialogueBoxOpacity} overlayDisplayedOpacity={this.overlayOpacity} closeOnExternalClick={this.close} />; + return <MainViewModal contents={this.sharingInterface} isDisplayed={this.isOpen} interactive dialogueBoxDisplayedOpacity={this.dialogueBoxOpacity} overlayDisplayedOpacity={this.overlayOpacity} closeOnExternalClick={this.close} />; } } diff --git a/src/client/util/SnappingManager.ts b/src/client/util/SnappingManager.ts index eb47bbe88..3da85191f 100644 --- a/src/client/util/SnappingManager.ts +++ b/src/client/util/SnappingManager.ts @@ -10,6 +10,7 @@ export class SnappingManager { @observable _shiftKey = false; @observable _ctrlKey = false; @observable _metaKey = false; + @observable _showPresPaths = false; @observable _isLinkFollowing = false; @observable _isDragging: boolean = false; @observable _isResizing: string | undefined = undefined; // the string is the Id of the document being resized @@ -36,6 +37,7 @@ export class SnappingManager { public static get ShiftKey() { return this.Instance._shiftKey; } // prettier-ignore public static get CtrlKey() { return this.Instance._ctrlKey; } // prettier-ignore public static get MetaKey() { return this.Instance._metaKey; } // prettier-ignore + public static get ShowPresPaths() { return this.Instance._showPresPaths; } // prettier-ignore public static get IsLinkFollowing(){ return this.Instance._isLinkFollowing; } // prettier-ignore public static get IsDragging() { return this.Instance._isDragging; } // prettier-ignore public static get IsResizing() { return this.Instance._isResizing; } // prettier-ignore @@ -44,6 +46,7 @@ export class SnappingManager { public static SetShiftKey = (down: boolean) => runInAction(() => {this.Instance._shiftKey = down}); // prettier-ignore public static SetCtrlKey = (down: boolean) => runInAction(() => {this.Instance._ctrlKey = down}); // prettier-ignore public static SetMetaKey = (down: boolean) => runInAction(() => {this.Instance._metaKey = down}); // prettier-ignore + public static SetShowPresPaths = (paths:boolean) => runInAction(() => {this.Instance._showPresPaths = paths}); // prettier-ignore public static SetIsLinkFollowing= (follow:boolean)=> runInAction(() => {this.Instance._isLinkFollowing = follow}); // prettier-ignore public static SetIsDragging = (drag: boolean) => runInAction(() => {this.Instance._isDragging = drag}); // prettier-ignore public static SetIsResizing = (docid?:string) => runInAction(() => {this.Instance._isResizing = docid}); // prettier-ignore diff --git a/src/client/util/UndoManager.ts b/src/client/util/UndoManager.ts index 4e941508d..956c0e674 100644 --- a/src/client/util/UndoManager.ts +++ b/src/client/util/UndoManager.ts @@ -1,8 +1,11 @@ +/* eslint-disable prefer-spread */ +/* eslint-disable no-use-before-define */ import { action, observable, runInAction } from 'mobx'; import { Without } from '../../Utils'; import { RichTextField } from '../../fields/RichTextField'; -export let printToConsole = false; // Doc.MyDockedBtns.linearView_IsOpen +// eslint-disable-next-line prefer-const +let printToConsole = false; // Doc.MyDockedBtns.linearView_IsOpen function getBatchName(target: any, key: string | symbol): string { const keyName = key.toString(); @@ -38,10 +41,11 @@ function propertyDecorator(target: any, key: string | symbol) { } export function undoable(fn: (...args: any[]) => any, batchName: string): (...args: any[]) => any { - return function () { + return function (...fargs) { const batch = UndoManager.StartBatch(batchName); try { - return fn.apply(undefined, arguments as any); + // eslint-disable-next-line prefer-rest-params + return fn.apply(undefined, fargs); } finally { batch.end(); } @@ -49,13 +53,15 @@ export function undoable(fn: (...args: any[]) => any, batchName: string): (...ar } export function undoBatch(target: any, key: string | symbol, descriptor?: TypedPropertyDescriptor<any>): any; +// eslint-disable-next-line no-redeclare export function undoBatch(fn: (...args: any[]) => any): (...args: any[]) => any; +// eslint-disable-next-line no-redeclare export function undoBatch(target: any, key?: string | symbol, descriptor?: TypedPropertyDescriptor<any>): any { if (!key) { - return function () { + return function (...fargs: any[]) { const batch = UndoManager.StartBatch(''); try { - return target.apply(undefined, arguments); + return target.apply(undefined, fargs); } finally { batch.end(); } @@ -63,7 +69,7 @@ export function undoBatch(target: any, key?: string | symbol, descriptor?: Typed } if (!descriptor) { propertyDecorator(target, key); - return; + return undefined; } const oldFunction = descriptor.value; @@ -87,14 +93,18 @@ export namespace UndoManager { } type UndoBatch = UndoEvent[]; - export let undoStackNames: string[] = observable([]); - export let redoStackNames: string[] = observable([]); - export let undoStack: UndoBatch[] = observable([]); - export let redoStack: UndoBatch[] = observable([]); let currentBatch: UndoBatch | undefined; - export let batchCounter = observable.box(0); let undoing = false; - export let tempEvents: UndoEvent[] | undefined = undefined; + let tempEvents: UndoEvent[] | undefined; + export const undoStackNames: string[] = observable([]); + export const redoStackNames: string[] = observable([]); + export const undoStack: UndoBatch[] = observable([]); + export const redoStack: UndoBatch[] = observable([]); + export const batchCounter = observable.box(0); + let _fieldPrinter: (val: any) => string = val => val?.toString(); + export function SetFieldPrinter(printer: (val: any) => string) { + _fieldPrinter = printer; + } export function AddEvent(event: UndoEvent, value?: any): void { if (currentBatch && batchCounter.get() && !undoing) { @@ -103,8 +113,8 @@ export namespace UndoManager { ' '.slice(0, batchCounter.get()) + 'UndoEvent : ' + event.prop + - ' = ' + - (value instanceof RichTextField ? value.Text : value instanceof Array ? value.map(val => Field.toJavascriptString(val)).join(',') : Field.toJavascriptString(value)) + ' = ' + // prettier-ignore + (value instanceof RichTextField ? value.Text : value instanceof Array ? value.map(_fieldPrinter).join(',') : _fieldPrinter(value)) ); currentBatch.push(event); tempEvents?.push(event); @@ -130,21 +140,22 @@ export namespace UndoManager { } export function FilterBatches(fieldTypes: string[]) { const fieldCounts: { [key: string]: number } = {}; - const lastStack = UndoManager.undoStack.slice(-1)[0]; //.lastElement(); + const lastStack = UndoManager.undoStack.slice(-1)[0]; // .lastElement(); if (lastStack) { - lastStack.forEach(ev => fieldTypes.includes(ev.prop) && (fieldCounts[ev.prop] = (fieldCounts[ev.prop] || 0) + 1)); + lastStack.forEach(ev => { + fieldTypes.includes(ev.prop) && (fieldCounts[ev.prop] = (fieldCounts[ev.prop] || 0) + 1); + }); const fieldCount2: { [key: string]: number } = {}; - runInAction( - () => - (UndoManager.undoStack[UndoManager.undoStack.length - 1] = lastStack.filter(ev => { - if (fieldTypes.includes(ev.prop)) { - fieldCount2[ev.prop] = (fieldCount2[ev.prop] || 0) + 1; - if (fieldCount2[ev.prop] === 1 || fieldCount2[ev.prop] === fieldCounts[ev.prop]) return true; - return false; - } - return true; - })) - ); + runInAction(() => { + UndoManager.undoStack[UndoManager.undoStack.length - 1] = lastStack.filter(ev => { + if (fieldTypes.includes(ev.prop)) { + fieldCount2[ev.prop] = (fieldCount2[ev.prop] || 0) + 1; + if (fieldCount2[ev.prop] === 1 || fieldCount2[ev.prop] === fieldCounts[ev.prop]) return true; + return false; + } + return true; + }); + }); } } export function TraceOpenBatches() { @@ -161,11 +172,10 @@ export namespace UndoManager { if (this.disposed) { console.log('WARNING: undo batch already disposed'); return false; - } else { - this.disposed = true; - openBatches.splice(openBatches.indexOf(this)); - return EndBatch(this.batchName, cancel); } + this.disposed = true; + openBatches.splice(openBatches.indexOf(this)); + return EndBatch(this.batchName, cancel); }; end = () => this.dispose(false); @@ -183,7 +193,7 @@ export namespace UndoManager { const EndBatch = action((batchName: string, cancel: boolean = false) => { runInAction(() => batchCounter.set(batchCounter.get() - 1)); - printToConsole && console.log(' '.slice(0, batchCounter.get()) + 'End ' + batchName + ' (' + currentBatch?.length + ')'); + printToConsole && console.log(' '.slice(0, batchCounter.get()) + 'End ' + batchName + ' (' + (currentBatch?.length ?? 0) + ')'); if (batchCounter.get() === 0 && currentBatch?.length) { if (!cancel) { undoStack.push(currentBatch); @@ -200,10 +210,10 @@ export namespace UndoManager { export function StartTempBatch() { tempEvents = []; } - export function EndTempBatch<T>(success: boolean) { + export function EndTempBatch(success: boolean) { UndoManager.UndoTempBatch(success); } - //TODO Make this return the return value + // TODO Make this return the return value export function RunInBatch<T>(fn: () => T, batchName: string) { const batch = StartBatch(batchName); try { @@ -235,9 +245,11 @@ export namespace UndoManager { } undoing = true; - for (let i = commands.length - 1; i >= 0; i--) { - commands[i].undo(); - } + // eslint-disable-next-line prettier/prettier + commands + .slice() + .reverse() + .forEach(command => command.undo()); undoing = false; redoStackNames.push(names ?? '???'); @@ -256,9 +268,7 @@ export namespace UndoManager { } undoing = true; - for (const command of commands) { - command.redo(); - } + commands.forEach(command => command.redo()); undoing = false; undoStackNames.push(names ?? '???'); diff --git a/src/client/util/reportManager/ReportManager.tsx b/src/client/util/reportManager/ReportManager.tsx index 02b3ee32c..2224e642d 100644 --- a/src/client/util/reportManager/ReportManager.tsx +++ b/src/client/util/reportManager/ReportManager.tsx @@ -1,3 +1,6 @@ +/* eslint-disable jsx-a11y/label-has-associated-control */ +/* eslint-disable jsx-a11y/media-has-caption */ +/* eslint-disable react/no-unused-class-component-methods */ import { Octokit } from '@octokit/core'; import { Button, Dropdown, DropdownType, IconButton, Type } from 'browndash-components'; import { action, makeObservable, observable } from 'mobx'; @@ -13,7 +16,7 @@ import { ClientUtils } from '../../../ClientUtils'; import { Doc } from '../../../fields/Doc'; import { StrCast } from '../../../fields/Types'; import { MainViewModal } from '../../views/MainViewModal'; -import '.././SettingsManager.scss'; +import '../SettingsManager.scss'; import { SettingsManager } from '../SettingsManager'; import './ReportManager.scss'; import { Filter, FormInput, FormTextArea, IssueCard, IssueView } from './ReportManagerComponents'; @@ -25,10 +28,12 @@ import { BugType, FileData, Priority, ReportForm, ViewState, bugDropdownItems, d */ @observer export class ReportManager extends React.Component<{}> { + // eslint-disable-next-line no-use-before-define public static Instance: ReportManager; @observable private isOpen = false; @observable private query = ''; + // eslint-disable-next-line react/sort-comp @action private setQuery = (q: string) => { this.query = q; }; @@ -83,7 +88,9 @@ export class ReportManager extends React.Component<{}> { this.formData = newData; }); - public close = action(() => (this.isOpen = false)); + public close = action(() => { + this.isOpen = false; + }); public open = action(async () => { this.isOpen = true; if (this.shownIssues.length === 0) { @@ -165,7 +172,7 @@ export class ReportManager extends React.Component<{}> { * @returns JSX element of a piece of media (image, video, audio) */ private getMediaPreview = (fileData: FileData): JSX.Element => { - const file = fileData.file; + const { file } = fileData; const mimeType = file.type; const preview = URL.createObjectURL(file); @@ -180,7 +187,8 @@ export class ReportManager extends React.Component<{}> { </div> </div> ); - } else if (mimeType.startsWith('video/')) { + } + if (mimeType.startsWith('video/')) { return ( <div key={fileData._id} className="report-media-wrapper"> <div className="report-media-content"> @@ -194,7 +202,8 @@ export class ReportManager extends React.Component<{}> { </div> </div> ); - } else if (mimeType.startsWith('audio/')) { + } + if (mimeType.startsWith('audio/')) { return ( <div key={fileData._id} className="report-audio-wrapper"> <audio src={preview} controls /> @@ -204,7 +213,7 @@ export class ReportManager extends React.Component<{}> { </div> ); } - return <></>; + return <div />; }; /** @@ -307,8 +316,8 @@ export class ReportManager extends React.Component<{}> { <div className="report-selects"> <Dropdown color={StrCast(Doc.UserDoc().userColor)} - formLabel={'Type'} - closeOnSelect={true} + formLabel="Type" + closeOnSelect items={bugDropdownItems} selectedVal={this.formData.type} setSelectedVal={val => { @@ -320,8 +329,8 @@ export class ReportManager extends React.Component<{}> { /> <Dropdown color={StrCast(Doc.UserDoc().userColor)} - formLabel={'Priority'} - closeOnSelect={true} + formLabel="Priority" + closeOnSelect items={priorityDropdownItems} selectedVal={this.formData.priority} setSelectedVal={val => { @@ -347,7 +356,7 @@ export class ReportManager extends React.Component<{}> { text="Submit" type={Type.TERT} color={StrCast(Doc.UserDoc().userVariantColor)} - icon={<ReactLoading type="spin" color={'#ffffff'} width={20} height={20} />} + icon={<ReactLoading type="spin" color="#ffffff" width={20} height={20} />} iconPlacement="right" onClick={() => { this.reportIssue(); @@ -364,7 +373,7 @@ export class ReportManager extends React.Component<{}> { /> )} <div style={{ position: 'absolute', top: '4px', right: '4px' }}> - <IconButton color={StrCast(Doc.UserDoc().userColor)} tooltip="close" icon={<CgClose size={'16px'} />} onClick={this.close} /> + <IconButton color={StrCast(Doc.UserDoc().userColor)} tooltip="close" icon={<CgClose size="16px" />} onClick={this.close} /> </div> </div> ); @@ -376,9 +385,8 @@ export class ReportManager extends React.Component<{}> { private reportComponent = () => { if (this.viewState === ViewState.VIEW) { return this.viewIssuesComponent(); - } else { - return this.reportIssueComponent(); } + return this.reportIssueComponent(); }; render() { @@ -386,7 +394,7 @@ export class ReportManager extends React.Component<{}> { <MainViewModal contents={this.reportComponent()} isDisplayed={this.isOpen} - interactive={true} + interactive closeOnExternalClick={this.close} dialogueBoxStyle={{ width: 'auto', minWidth: '300px', height: '85vh', maxHeight: '90vh', background: StrCast(Doc.UserDoc().userBackgroundColor), borderRadius: '8px' }} /> |
