diff options
Diffstat (limited to 'src')
53 files changed, 963 insertions, 1272 deletions
diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts index b833d3287..cf7a61d24 100644 --- a/src/client/DocServer.ts +++ b/src/client/DocServer.ts @@ -8,7 +8,7 @@ import { FieldLoader } from '../fields/FieldLoader'; import { HandleUpdate, Id, Parent } from '../fields/FieldSymbols'; import { ObjectField, SetObjGetRefField, SetObjGetRefFields } from '../fields/ObjectField'; import { RefField } from '../fields/RefField'; -import { GestureContent, Message, MessageStore, MobileDocumentUploadContent, MobileInkOverlayContent, UpdateMobileInkOverlayPositionContent, YoutubeQueryTypes } from './../server/Message'; +import { GestureContent, Message, MessageStore, MobileDocumentUploadContent, MobileInkOverlayContent, UpdateMobileInkOverlayPositionContent, YoutubeQueryTypes } from '../server/Message'; import { SerializationHelper } from './util/SerializationHelper'; /** @@ -63,7 +63,7 @@ export namespace DocServer { return foundDocId ? (_cache[foundDocId] as Doc) : undefined; } - export let _socket: Socket; + let _socket: Socket; // this client's distinct GUID created at initialization let USER_ID: string; // indicates whether or not a document is currently being udpated, and, if so, its id @@ -317,7 +317,7 @@ export namespace DocServer { // ii) which are already in the process of being fetched // iii) which already exist in the cache // eslint-disable-next-line no-restricted-syntax - for (const id of ids.filter(id => id)) { + for (const id of ids.filter(filterid => filterid)) { const cached = _cache[id]; if (cached === undefined) { defaultPromises.push({ @@ -362,6 +362,7 @@ export namespace DocServer { for (const field of serializedFields) { processed++; if (processed % 150 === 0) { + // eslint-disable-next-line no-loop-func runInAction(() => { FieldLoader.ServerLoadStatus.retrieved = processed; }); @@ -375,6 +376,7 @@ export namespace DocServer { // deserialize // adds to a list of promises that will be awaited asynchronously promises.push( + // eslint-disable-next-line no-loop-func (_cache[field.id] = SerializationHelper.Deserialize(field).then(deserialized => { // overwrite or delete any promises (that we inserted as flags // to indicate that the field was in the process of being fetched). Now everything @@ -447,7 +449,10 @@ export namespace DocServer { } // WRITE A NEW DOCUMENT TO THE SERVER - export let CacheNeedsUpdate = false; + let _cacheNeedsUpdate = false; + export function CacheNeedsUpdate() { + return _cacheNeedsUpdate; + } /** * A wrapper around the function local variable _createField. @@ -456,7 +461,7 @@ export namespace DocServer { * @param field the [RefField] to be serialized and sent to the server to be stored in the database */ export function CreateField(field: RefField) { - CacheNeedsUpdate = true; + _cacheNeedsUpdate = true; _CreateField(field); } diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 6dba8027d..acbd0c0b9 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -946,7 +946,7 @@ pie title Minerals in my tap water // eslint-disable-next-line no-new new LinkManager(); - DocServer.CacheNeedsUpdate && setTimeout(UPDATE_SERVER_CACHE, 2500); + DocServer.CacheNeedsUpdate() && setTimeout(UPDATE_SERVER_CACHE, 2500); setInterval(UPDATE_SERVER_CACHE, 120000); return doc; } diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index 9a7786125..cca92816f 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -178,11 +178,13 @@ export class DocumentManager { return containerDocContext; } + static _howl: Howl; static playAudioAnno(doc: Doc) { const anno = Cast(doc[Doc.LayoutFieldKey(doc) + '_audioAnnotations'], listSpec(AudioField), null)?.lastElement(); if (anno) { + this._howl?.stop(); if (anno instanceof AudioField) { - new Howl({ + this._howl = new Howl({ src: [anno.url.href], format: ['mp3'], autoplay: true, @@ -200,13 +202,13 @@ export class DocumentManager { static _overlayViews = new ObservableSet<DocumentView>(); public static LinkCommonAncestor(linkDoc: Doc) { - const anchor = (which: number) => { + const getAnchor = (which: number) => { const anch = DocCast(linkDoc['link_anchor_' + which]); const anchor = anch?.layout_unrendered ? DocCast(anch.annotationOn) : anch; return DocumentManager.Instance.getDocumentView(anchor); }; - const anchor1 = anchor(1); - const anchor2 = anchor(2); + const anchor1 = getAnchor(1); + const anchor2 = getAnchor(2); return anchor1 ?.docViewPath() .reverse() @@ -275,9 +277,13 @@ export class DocumentManager { if (rootContextView) { const target = await this.focusViewsInPath(rootContextView, options, childViewIterator); - this.restoreDocView(target.viewSpec, target.docView, options, target.contextView ?? target.docView, targetDoc); - finished?.(target.focused); - } else finished?.(false); + if (target) { + this.restoreDocView(target.viewSpec, target.docView, options, target.contextView ?? target.docView, targetDoc); + finished?.(target.focused); + return; + } + } + finished?.(false); }; focusViewsInPath = async ( @@ -289,12 +295,15 @@ export class DocumentManager { let focused = false; let docView = docViewIn; const options = optionsIn; - while (true) { + const maxFocusLength = 100; // want to keep focusing until we get to target, but avoid an infinite loop + for (let i = 0; i < maxFocusLength; i++) { if (docView.Document.layout_fieldKey === 'layout_icon') { - // eslint-disable-next-line no-await-in-loop - await new Promise<any>(res => { + // eslint-disable-next-line no-loop-func + const prom = new Promise<void>(res => { docView.iconify(res); }); + // eslint-disable-next-line no-await-in-loop + await prom; options.didMove = true; } const nextFocus = docView._props.focus(docView.Document, options); // focus the view within its container @@ -305,6 +314,7 @@ export class DocumentManager { contextView = options.anchorDoc?.layout_unrendered && !childDocView.Document.layout_unrendered ? childDocView : docView; docView = childDocView; } + return undefined; }; @action @@ -347,9 +357,9 @@ export function DocFocusOrOpen(docIn: Doc, optionsIn: FocusViewOptions = { willZ const showDoc = !Doc.IsSystem(container) && !cv ? container : doc; options.toggleTarget = undefined; DocumentManager.Instance.showDocument(showDoc, options, () => DocumentManager.Instance.showDocument(doc, { ...options, openLocation: undefined })).then(() => { - const cv = DocumentManager.Instance.getDocumentView(containingDoc); - const dv = DocumentManager.Instance.getDocumentView(doc, cv); - dv && Doc.linkFollowHighlight(dv.Document); + const cvFound = DocumentManager.Instance.getDocumentView(containingDoc); + const dvFound = DocumentManager.Instance.getDocumentView(doc, cvFound); + dvFound && Doc.linkFollowHighlight(dvFound.Document); }); } }; diff --git a/src/client/util/request-image-size.ts b/src/client/util/request-image-size.ts index 57e8516ac..0f98a2710 100644 --- a/src/client/util/request-image-size.ts +++ b/src/client/util/request-image-size.ts @@ -14,19 +14,17 @@ const imageSize = require('image-size'); const HttpError = require('standard-http-error'); module.exports = function requestImageSize(options: any) { - let opts = { + let opts: any = { encoding: null, }; if (options && typeof options === 'object') { opts = Object.assign(options, opts); } else if (options && typeof options === 'string') { - opts = Object.assign( - { - uri: options, - }, - opts - ); + opts = { + uri: options, + ...opts, + }; } else { return Promise.reject(new Error('You should provide an URI string or a "request" options object.')); } @@ -38,7 +36,8 @@ module.exports = function requestImageSize(options: any) { req.on('response', (res: any) => { if (res.statusCode >= 400) { - return reject(new HttpError(res.statusCode, res.statusMessage)); + reject(new HttpError(res.statusCode, res.statusMessage)); + return; } let buffer = Buffer.from([]); @@ -51,20 +50,23 @@ module.exports = function requestImageSize(options: any) { size = imageSize(buffer); if (size) { resolve(size); - return req.abort(); + req.abort(); } - } catch (err) {} + } catch (err) { + /* empty */ + } }); res.on('error', reject); res.on('end', () => { if (!size) { - return reject(new Error('Image has no size')); + reject(new Error('Image has no size')); + return; } size.downloaded = buffer.length; - return resolve(size); + resolve(size); }); }); diff --git a/src/client/views/ScriptingRepl.tsx b/src/client/views/ScriptingRepl.tsx index acf0ecff4..ba2e22b3b 100644 --- a/src/client/views/ScriptingRepl.tsx +++ b/src/client/views/ScriptingRepl.tsx @@ -1,3 +1,6 @@ +/* eslint-disable react/no-array-index-key */ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; @@ -12,6 +15,36 @@ import { OverlayView } from './OverlayView'; import './ScriptingRepl.scss'; import { DocumentIconContainer } from './nodes/DocumentIcon'; +interface replValueProps { + scrollToBottom: () => void; + value: any; + name?: string; +} +@observer +export class ScriptingValueDisplay extends ObservableReactComponent<replValueProps> { + constructor(props: any) { + super(props); + makeObservable(this); + } + + render() { + const val = this._props.name ? this._props.value[this._props.name] : this._props.value; + const title = (name: string) => ( + <> + {this._props.name ? <b>{this._props.name} : </b> : <> </>} + {name} + </> + ); + if (typeof val === 'object') { + // eslint-disable-next-line no-use-before-define + return <ScriptingObjectDisplay scrollToBottom={this._props.scrollToBottom} value={val} name={this._props.name} />; + } + if (typeof val === 'function') { + return <div className="scriptingObject-leaf">{title('[Function]')}</div>; + } + return <div className="scriptingObject-leaf">{title(String(val))}</div>; + } +} interface ReplProps { scrollToBottom: () => void; value: { [key: string]: any }; @@ -37,7 +70,7 @@ export class ScriptingObjectDisplay extends ObservableReactComponent<ReplProps> const name = (proto && proto.constructor && proto.constructor.name) || String(val); const title = ( <> - {this.props.name ? <b>{this._props.name} : </b> : <></>} + {this.props.name ? <b>{this._props.name} : </b> : null} {name} </> ); @@ -50,53 +83,23 @@ export class ScriptingObjectDisplay extends ObservableReactComponent<ReplProps> {title} (+{Object.keys(val).length}) </div> ); - } else { - return ( - <div className="scriptingObject-open"> - <div> - <span onClick={this.toggle} className="scriptingObject-icon"> - <FontAwesomeIcon icon="caret-down" size="sm" /> - </span> - {title} - </div> - <div className="scriptingObject-fields"> - {Object.keys(val).map(key => ( - <ScriptingValueDisplay {...this._props} name={key} /> - ))} - </div> - </div> - ); } - } -} - -interface replValueProps { - scrollToBottom: () => void; - value: any; - name?: string; -} -@observer -export class ScriptingValueDisplay extends ObservableReactComponent<replValueProps> { - constructor(props: any) { - super(props); - makeObservable(this); - } - - render() { - const val = this._props.name ? this._props.value[this._props.name] : this._props.value; - const title = (name: string) => ( - <> - {this._props.name ? <b>{this._props.name} : </b> : <> </>} - {name} - </> + return ( + <div className="scriptingObject-open"> + <div> + <span onClick={this.toggle} className="scriptingObject-icon"> + <FontAwesomeIcon icon="caret-down" size="sm" /> + </span> + {title} + </div> + <div className="scriptingObject-fields"> + {Object.keys(val).map(key => ( + // eslint-disable-next-line react/jsx-props-no-spreading + <ScriptingValueDisplay {...this._props} name={key} /> + ))} + </div> + </div> ); - if (typeof val === 'object') { - return <ScriptingObjectDisplay scrollToBottom={this._props.scrollToBottom} value={val} name={this._props.name} />; - } else if (typeof val === 'function') { - const name = '[Function]'; - return <div className="scriptingObject-leaf">{title('[Function]')}</div>; - } - return <div className="scriptingObject-leaf">{title(String(val))}</div>; } } @@ -119,47 +122,45 @@ export class ScriptingRepl extends ObservableReactComponent<{}> { private args: any = {}; - getTransformer = (): Transformer => { - return { - transformer: context => { - const knownVars: { [name: string]: number } = {}; - const usedDocuments: number[] = []; - ScriptingGlobals.getGlobals().forEach((global: any) => (knownVars[global] = 1)); - return root => { - function visit(node: ts.Node) { - let skip = false; - if (ts.isIdentifier(node)) { - if (ts.isParameter(node.parent)) { - skip = true; - knownVars[node.text] = 1; - } + getTransformer = (): Transformer => ({ + transformer: context => { + const knownVars: { [name: string]: number } = {}; + const usedDocuments: number[] = []; + ScriptingGlobals.getGlobals().forEach((global: any) => { + knownVars[global] = 1; + }); + return root => { + function visit(nodeIn: ts.Node) { + if (ts.isIdentifier(nodeIn)) { + if (ts.isParameter(nodeIn.parent)) { + knownVars[nodeIn.text] = 1; } - node = ts.visitEachChild(node, visit, context); + } + const node = ts.visitEachChild(nodeIn, visit, context); - if (ts.isIdentifier(node)) { - const isntPropAccess = !ts.isPropertyAccessExpression(node.parent) || node.parent.expression === node; - const isntPropAssign = !ts.isPropertyAssignment(node.parent) || node.parent.name !== node; - if (ts.isParameter(node.parent)) { - // delete knownVars[node.text]; - } else if (isntPropAccess && isntPropAssign && !(node.text in knownVars) && !(node.text in globalThis)) { - const match = node.text.match(/d([0-9]+)/); - if (match) { - const m = parseInt(match[1]); - usedDocuments.push(m); - } else { - return ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('args'), node); - // ts.createPropertyAccess(ts.createIdentifier('args'), node); - } + if (ts.isIdentifier(node)) { + const isntPropAccess = !ts.isPropertyAccessExpression(node.parent) || node.parent.expression === node; + const isntPropAssign = !ts.isPropertyAssignment(node.parent) || node.parent.name !== node; + if (ts.isParameter(node.parent)) { + // delete knownVars[node.text]; + } else if (isntPropAccess && isntPropAssign && !(node.text in knownVars) && !(node.text in globalThis)) { + const match = node.text.match(/d([0-9]+)/); + if (match) { + const m = parseInt(match[1]); + usedDocuments.push(m); + } else { + return ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('args'), node); + // ts.createPropertyAccess(ts.createIdentifier('args'), node); } } - - return node; } - return ts.visitNode(root, visit); - }; - }, - }; - }; + + return node; + } + return ts.visitNode(root, visit); + }; + }, + }); @action onKeyDown = (e: React.KeyboardEvent) => { @@ -168,14 +169,16 @@ export class ScriptingRepl extends ObservableReactComponent<{}> { case 'Enter': { e.stopPropagation(); const docGlobals: { [name: string]: any } = {}; - DocumentManager.Instance.DocumentViews.forEach((dv, i) => (docGlobals[`d${i}`] = dv.Document)); + DocumentManager.Instance.DocumentViews.forEach((dv, i) => { + docGlobals[`d${i}`] = dv.Document; + }); const globals = ScriptingGlobals.makeMutableGlobalsCopy(docGlobals); const script = CompileScript(this.commandString, { typecheck: false, addReturn: true, editable: true, params: { args: 'any' }, transformer: this.getTransformer(), globals }); if (!script.compiled) { this.commands.push({ command: this.commandString, result: script.errors }); return; } - const result = undoable(() => script.run({ args: this.args }, e => this.commands.push({ command: this.commandString, result: e.toString() })), 'run:' + this.commandString)(); + const result = undoable(() => script.run({ args: this.args }, () => this.commands.push({ command: this.commandString, result: e.toString() })), 'run:' + this.commandString)(); if (result.success) { this.commands.push({ command: this.commandString, result: result.result }); this.commandsHistory.push(this.commandString); @@ -260,18 +263,16 @@ export class ScriptingRepl extends ObservableReactComponent<{}> { return ( <div className="scriptingRepl-outerContainer"> <div className="scriptingRepl-commandsContainer" style={{ background: SettingsManager.userBackgroundColor }} ref={this.commandsRef}> - {this.commands.map(({ command, result }, i) => { - return ( - <div className="scriptingRepl-resultContainer" style={{ background: SettingsManager.userBackgroundColor }} key={i}> - <div className="scriptingRepl-commandString" style={{ background: SettingsManager.userBackgroundColor }}> - {command || <br />} - </div> - <div className="scriptingRepl-commandResult" style={{ background: SettingsManager.userBackgroundColor }}> - {<ScriptingValueDisplay scrollToBottom={this.maybeScrollToBottom} value={result} />} - </div> + {this.commands.map(({ command, result }, i) => ( + <div className="scriptingRepl-resultContainer" style={{ background: SettingsManager.userBackgroundColor }} key={i}> + <div className="scriptingRepl-commandString" style={{ background: SettingsManager.userBackgroundColor }}> + {command || <br />} + </div> + <div className="scriptingRepl-commandResult" style={{ background: SettingsManager.userBackgroundColor }}> + <ScriptingValueDisplay scrollToBottom={this.maybeScrollToBottom} value={result} /> </div> - ); - })} + </div> + ))} </div> <input className="scriptingRepl-commandInput" diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx index 75f1a7d80..3697aa010 100644 --- a/src/client/views/StyleProvider.tsx +++ b/src/client/views/StyleProvider.tsx @@ -49,6 +49,7 @@ export enum StyleProp { TitleHeight = 'titleHeight', // Height of Title area ShowTitle = 'layout_showTitle', // whether to display a title on a Document (optional :hover suffix) BorderPath = 'customBorder', // border path for document view + FontColor = 'fontColor', // color o tet FontSize = 'fontSize', // size of text font FontFamily = 'fontFamily', // font family of text FontWeight = 'fontWeight', // font weight of text @@ -109,13 +110,35 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps & const layoutDoc = doc ? Doc.Layout(doc) : doc; const isOpen = property.includes(':open'); const boxBackground = property.includes(':box'); - const fieldKey = props?.fieldKey ? props.fieldKey + '_' : isCaption ? 'caption_' : ''; - const isInk = () => layoutDoc?._layout_isSvg && !props?.LayoutTemplateString; + const { + fieldKey: fieldKeyProp, + styleProvider, + pointerEvents, + isGroupActive, + isDocumentActive, + containerViewPath, + childFilters, + hideCaptions, + // eslint-disable-next-line camelcase + layout_showTitle, + childFiltersByRanges, + renderDepth, + docViewPath, + DocumentView, + LayoutTemplateString, + disableBrushing, + NativeDimScaling, + PanelWidth, + PanelHeight, + } = props || {}; // extract props that are not shared between fieldView and documentView props. + const fieldKey = fieldKeyProp ? fieldKeyProp + '_' : isCaption ? 'caption_' : ''; + const isInk = () => layoutDoc?._layout_isSvg && !LayoutTemplateString; const lockedPosition = () => doc && BoolCast(doc._lockedPosition); - const titleHeight = () => props?.styleProvider?.(doc, props, StyleProp.TitleHeight); - const backgroundCol = () => props?.styleProvider?.(doc, props, StyleProp.BackgroundColor + ':nonTransparent' + (isNonTransparentLevel + 1)); - const opacity = () => props?.styleProvider?.(doc, props, StyleProp.Opacity); - const layoutShowTitle = () => props?.styleProvider?.(doc, props, StyleProp.ShowTitle); + const titleHeight = () => styleProvider?.(doc, props, StyleProp.TitleHeight); + const backgroundCol = () => styleProvider?.(doc, props, StyleProp.BackgroundColor + ':nonTransparent' + (isNonTransparentLevel + 1)); + const color = () => styleProvider?.(doc, props, StyleProp.Color); + const opacity = () => styleProvider?.(doc, props, StyleProp.Opacity); + const layoutShowTitle = () => styleProvider?.(doc, props, StyleProp.ShowTitle); // prettier-ignore switch (property.split(':')[0]) { case StyleProp.TreeViewIcon: { @@ -137,7 +160,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps & } case StyleProp.Highlighting: if (doc && (Doc.IsSystem(doc) || doc.type === DocumentType.FONTICON)) return undefined; - if (doc && !doc.layout_disableBrushing && !props?.disableBrushing) { + if (doc && !doc.layout_disableBrushing && !disableBrushing) { const selected = Array.from(doc?.[DocViews]??[]).filter(dv => dv.IsSelected).length; const highlightIndex = Doc.GetBrushHighlightStatus(doc) || (selected ? Doc.DocBrushStatus.selfBrushed : 0); const highlightColor = ['transparent', 'rgb(68, 118, 247)', selected ? "black" : 'rgb(68, 118, 247)', 'orange', 'lightBlue'][highlightIndex]; @@ -152,26 +175,27 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps & } } return undefined; - case StyleProp.DocContents:return undefined; - case StyleProp.WidgetColor:return isAnnotated ? Colors.LIGHT_BLUE : 'dimgrey'; - case StyleProp.Opacity: return props?.LayoutTemplateString?.includes(KeyValueBox.name) ? 1 : doc?.text_inlineAnnotations ? 0 : Cast(doc?._opacity, "number", Cast(doc?.opacity, 'number', null)); - case StyleProp.FontSize: return StrCast(doc?.[fieldKey + 'fontSize'], StrCast(Doc.UserDoc().fontSize)); - case StyleProp.FontFamily: return StrCast(doc?.[fieldKey + 'fontFamily'], StrCast(Doc.UserDoc().fontFamily)); - case StyleProp.FontWeight: return StrCast(doc?.[fieldKey + 'fontWeight'], StrCast(Doc.UserDoc().fontWeight)); - case StyleProp.FillColor: return StrCast(doc?._fillColor, StrCast(doc?.fillColor, StrCast(doc?.backgroundColor, 'transparent'))); - case StyleProp.ShowCaption:return props?.hideCaptions || doc?._type_collection === CollectionViewType.Carousel ? undefined: StrCast(doc?._layout_showCaption); - case StyleProp.TitleHeight:return Math.min(4,(props?.DocumentView?.().screenToViewTransform().Scale ?? 1)) * NumCast(Doc.UserDoc().headerHeight,30); + case StyleProp.DocContents: return undefined; + case StyleProp.WidgetColor: return isAnnotated ? Colors.LIGHT_BLUE : 'dimgrey'; + case StyleProp.Opacity: return LayoutTemplateString?.includes(KeyValueBox.name) ? 1 : doc?.text_inlineAnnotations ? 0 : Cast(doc?._opacity, "number", Cast(doc?.opacity, 'number', null)); + case StyleProp.FontColor: return StrCast(doc?.[fieldKey + 'fontColor'], StrCast(Doc.UserDoc().fontColor, color())); + case StyleProp.FontSize: return StrCast(doc?.[fieldKey + 'fontSize'], StrCast(Doc.UserDoc().fontSize)); + case StyleProp.FontFamily: return StrCast(doc?.[fieldKey + 'fontFamily'], StrCast(Doc.UserDoc().fontFamily)); + case StyleProp.FontWeight: return StrCast(doc?.[fieldKey + 'fontWeight'], StrCast(Doc.UserDoc().fontWeight)); + case StyleProp.FillColor: return StrCast(doc?._fillColor, StrCast(doc?.fillColor, StrCast(doc?.backgroundColor, 'transparent'))); + case StyleProp.ShowCaption: return hideCaptions || doc?._type_collection === CollectionViewType.Carousel ? undefined: StrCast(doc?._layout_showCaption); + case StyleProp.TitleHeight: return Math.min(4,(DocumentView?.().screenToViewTransform().Scale ?? 1)) * NumCast(Doc.UserDoc().headerHeight,30); case StyleProp.ShowTitle: return ( (doc && - !(props?.DocumentView?.().ComponentView instanceof CollectionSchemaView) && - !props?.LayoutTemplateString && + !(DocumentView?.().ComponentView instanceof CollectionSchemaView) && + !LayoutTemplateString && !doc.presentation_targetDoc && - !props?.LayoutTemplateString?.includes(KeyValueBox.name) && - props?.layout_showTitle?.() !== '' && + !LayoutTemplateString?.includes(KeyValueBox.name) && + layout_showTitle?.() !== '' && StrCast( doc._layout_showTitle, - props?.layout_showTitle?.() || + layout_showTitle?.() || (!Doc.IsSystem(doc) && [DocumentType.COL, DocumentType.FUNCPLOT, DocumentType.LABEL, DocumentType.RTF, DocumentType.IMG, DocumentType.VID].includes(doc.type as any) ? doc.author === ClientUtils.CurrentUserEmail() ? StrCast(Doc.UserDoc().layout_showTitle) @@ -195,15 +219,15 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps & } case StyleProp.BorderPath: { const borderPath = Doc.IsComicStyle(doc) && - props?.renderDepth && - !doc?.layout_isSvg && { path: wavyBorderPath(props?.PanelWidth?.() || 0, props?.PanelHeight?.() || 0), fill: wavyBorderPath(props?.PanelWidth?.() || 0, props?.PanelHeight?.() || 0, 0.08), width: 3 }; + renderDepth && + !doc?.layout_isSvg && { path: wavyBorderPath(PanelWidth?.() || 0, PanelHeight?.() || 0), fill: wavyBorderPath(PanelWidth?.() || 0, PanelHeight?.() || 0, 0.08), width: 3 }; return !borderPath ? null : { clipPath: `path('${borderPath.path}')`, jsx: ( <div key="border2" className="documentView-customBorder" style={{ pointerEvents: 'none' }}> - <svg style={{ overflow: 'visible', height: '100%' }} viewBox={`0 0 ${props.PanelWidth()} ${props.PanelHeight()}`}> + <svg style={{ overflow: 'visible', height: '100%' }} viewBox={`0 0 ${PanelWidth?.()} ${PanelHeight?.()}`}> <path d={borderPath.path} style={{ stroke: 'black', fill: 'transparent', strokeWidth: borderPath.width }} /> </svg> </div> @@ -251,13 +275,13 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps & ? undefined : doc?._type_collection === CollectionViewType.Stacking ? (Colors.DARK_GRAY) - : Cast((props?.renderDepth || 0) > 0 ? Doc.UserDoc().activeCollectionNestedBackground : Doc.UserDoc().activeCollectionBackground, 'string') ?? (Colors.MEDIUM_GRAY)); + : Cast((renderDepth || 0) > 0 ? Doc.UserDoc().activeCollectionNestedBackground : Doc.UserDoc().activeCollectionBackground, 'string') ?? (Colors.MEDIUM_GRAY)); break; // if (doc._type_collection !== CollectionViewType.Freeform && doc._type_collection !== CollectionViewType.Time) return "rgb(62,62,62)"; default: docColor = docColor || (Colors.WHITE); } - if (isNonTransparent && isNonTransparentLevel < 9 && (!docColor || docColor === 'transparent') && doc?.embedContainer && props?.styleProvider) { - return props.styleProvider(DocCast(doc.embedContainer), props, StyleProp.BackgroundColor+":nonTransparent"+(isNonTransparentLevel+1)); + if (isNonTransparent && isNonTransparentLevel < 9 && (!docColor || docColor === 'transparent') && doc?.embedContainer && styleProvider) { + return styleProvider(DocCast(doc.embedContainer), props, StyleProp.BackgroundColor+":nonTransparent"+(isNonTransparentLevel+1)); } return (docColor && !doc) ? DashColor(docColor).fade(0.5).toString() : docColor; } @@ -271,7 +295,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps & doc?.layout_boxShadow, doc?._type_collection === CollectionViewType.Pile ? '4px 4px 10px 2px' - : lockedPosition() || doc?.isGroup || props?.LayoutTemplateString + : lockedPosition() || doc?.isGroup || LayoutTemplateString ? undefined // groups have no drop shadow -- they're supposed to be "invisible". LayoutString's imply collection is being rendered as something else (e.g., title of a Slide) : `${Colors.DARK_GRAY} ${StrCast(doc.layout_boxShadow, '0.2vw 0.2vw 0.8vw')}` ); @@ -282,10 +306,10 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps & default: return doc.z ? `#9c9396 ${StrCast(doc?.layout_boxShadow, '10px 10px 0.9vw')}` // if it's a floating doc, give it a big shadow - : props?.containerViewPath?.().lastElement()?.Document._freeform_useClusters - ? `${backgroundCol()} ${StrCast(doc.layout_boxShadow, `0vw 0vw ${(lockedPosition() ? 100 : 50) / (props?.NativeDimScaling?.() || 1)}px`)}` // if it's just in a cluster, make the shadown roughly match the cluster border extent + : containerViewPath?.().lastElement()?.Document._freeform_useClusters + ? `${backgroundCol()} ${StrCast(doc.layout_boxShadow, `0vw 0vw ${(lockedPosition() ? 100 : 50) / (NativeDimScaling?.() || 1)}px`)}` // if it's just in a cluster, make the shadown roughly match the cluster border extent : NumCast(doc.group, -1) !== -1 - ? `gray ${StrCast(doc.layout_boxShadow, `0vw 0vw ${(lockedPosition() ? 100 : 50) / (props?.NativeDimScaling?.() || 1)}px`)}` // if it's just in a cluster, make the shadown roughly match the cluster border extent + ? `gray ${StrCast(doc.layout_boxShadow, `0vw 0vw ${(lockedPosition() ? 100 : 50) / (NativeDimScaling?.() || 1)}px`)}` // if it's just in a cluster, make the shadown roughly match the cluster border extent : lockedPosition() ? undefined // if it's a background & has a cluster color, make the shadow spread really big : fieldKey.includes('_inline') // if doc is an inline document in a text box @@ -296,14 +320,14 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps & } } case StyleProp.PointerEvents: - if (StrCast(doc?.pointerEvents) && !props?.LayoutTemplateString?.includes(KeyValueBox.name)) return StrCast(doc!.pointerEvents); // honor pointerEvents field (set by lock button usually) if it's not a keyValue view of the Doc - if (props?.LayoutTemplateString?.includes(KeyValueBox.name)) return 'all'; + if (StrCast(doc?.pointerEvents) && !LayoutTemplateString?.includes(KeyValueBox.name)) return StrCast(doc!.pointerEvents); // honor pointerEvents field (set by lock button usually) if it's not a keyValue view of the Doc + if (LayoutTemplateString?.includes(KeyValueBox.name)) return 'all'; if (SnappingManager.ExploreMode || doc?.layout_unrendered) return isInk() ? 'visiblePainted' : 'all'; - if (props?.pointerEvents?.() === 'none') return 'none'; + if (pointerEvents?.() === 'none') return 'none'; if (opacity() === 0) return 'none'; - if (props?.isGroupActive?.() ) return isInk() ? 'visiblePainted': (doc?. + if (isGroupActive?.() ) return isInk() ? 'visiblePainted': (doc?. isGroup )? undefined: 'all' - if (props?.isDocumentActive?.()) return isInk() ? 'visiblePainted' : 'all'; + if (isDocumentActive?.()) return isInk() ? 'visiblePainted' : 'all'; return undefined; // fixes problem with tree view elements getting pointer events when the tree view is not active case StyleProp.Decorations: { const lock = () => doc?.pointerEvents !== 'none' ? null : ( @@ -312,7 +336,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps & </div> ); const paint = () => !doc?.onPaint ? null : ( - <div className={`styleProvider-paint${props?.DocumentView?.().IsSelected ? "-selected":""}`} onClick={e => togglePaintView(e, doc, props)}> + <div className={`styleProvider-paint${DocumentView?.().IsSelected ? "-selected":""}`} onClick={e => togglePaintView(e, doc, props)}> <FontAwesomeIcon icon='pen' size="lg" /> </div> ); @@ -321,7 +345,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps & const showFilterIcon = StrListCast(doc?._childFilters).length || StrListCast(doc?._childFiltersByRanges).length ? 'green' // #18c718bd' //'hasFilter' - : props?.childFilters?.().filter(f => ClientUtils.IsRecursiveFilter(f) && f !== ClientUtils.noDragDocsFilter).length || props?.childFiltersByRanges().length + : childFilters?.().filter(f => ClientUtils.IsRecursiveFilter(f) && f !== ClientUtils.noDragDocsFilter).length || childFiltersByRanges?.().length ? 'orange' // 'inheritsFilter' : undefined; return !showFilterIcon ? null : ( @@ -353,7 +377,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps & "this view inherits filters from one of its parents"} color={SettingsManager.userColor} background={showFilterIcon} - items={[ ...(dashView ? [dashView]: []), ...(props?.docViewPath?.()??[])] + items={[ ...(dashView ? [dashView]: []), ...(docViewPath?.()??[])] .filter(dv => StrListCast(dv?.Document.childFilters).length || StrListCast(dv?.Document.childRangeFilters).length) .map(dv => ({ text: StrCast(dv?.Document.title), @@ -365,9 +389,9 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps & ); }; const audio = () => { - const audioAnnoState = (doc: Doc) => StrCast(doc.audioAnnoState, AudioAnnoState.stopped); - const audioAnnosCount = (doc: Doc) => StrListCast(doc[fieldKey + 'audioAnnotations']).length; - if (!doc || props?.renderDepth === -1 || !audioAnnosCount(doc)) return null; + const audioAnnoState = (audioDoc: Doc) => StrCast(audioDoc.audioAnnoState, AudioAnnoState.stopped); + const audioAnnosCount = (audioDoc: Doc) => StrListCast(audioDoc[fieldKey + 'audioAnnotations']).length; + if (!doc || renderDepth === -1 || !audioAnnosCount(doc)) return null; const audioIconColors: { [key: string]: string } = { playing: 'green', stopped: 'blue' }; return ( <Tooltip title={<div>{StrListCast(doc[fieldKey + 'audioAnnotations_text']).lastElement()}</div>}> diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx index 5a509128d..6dba9e155 100644 --- a/src/client/views/collections/CollectionMenu.tsx +++ b/src/client/views/collections/CollectionMenu.tsx @@ -1,3 +1,9 @@ +/* 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 */ +/* eslint-disable jsx-a11y/control-has-associated-label */ +/* eslint-disable react/no-unused-class-component-methods */ +/* eslint-disable react/sort-comp */ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; import { Toggle, ToggleType, Type } from 'browndash-components'; @@ -312,8 +318,6 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu } private get _buttonizableCommands() { switch (this.props.type) { - default: - return this._doc_commands; case CollectionViewType.Freeform: return this._freeform_commands; case CollectionViewType.Tree: @@ -332,6 +336,8 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu return this._freeform_commands; case CollectionViewType.Carousel3D: return this._freeform_commands; + default: + return this._doc_commands; } } private _commandRef = React.createRef<HTMLInputElement>(); @@ -345,11 +351,13 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu @undoBatch viewChanged = (e: React.ChangeEvent) => { const target = this.document !== Doc.MyLeftSidebarPanel ? this.document : DocCast(this.document.proto); - target._type_collection = e.target.selectedOptions[0].value; + target._type_collection = (e.target as any).selectedOptions[0].value; }; commandChanged = (e: React.ChangeEvent) => { - runInAction(() => (this._currentKey = e.target.selectedOptions[0].value)); + runInAction(() => { + this._currentKey = (e.target as any).selectedOptions[0].value; + }); }; @action closeViewSpecs = () => { @@ -367,7 +375,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu @undoBatch @action protected drop(e: Event, de: DragManager.DropEvent): boolean { - const docDragData = de.complete.docDragData; + const { docDragData } = de.complete; if (docDragData?.draggedDocuments.length) { this._buttonizableCommands?.filter(c => c.title === this._currentKey).map(c => c.immediate(docDragData.draggedDocuments || [])); e.stopPropagation(); @@ -420,11 +428,11 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu <div className="collectionViewBaseChrome-template" ref={this.createDropTarget}> <Tooltip title={<div className="dash-tooltip">drop document to apply or drag to create button</div>} placement="bottom"> <div className="commandEntry-outerDiv" ref={this._commandRef} onPointerDown={this.dragCommandDown}> - <button className={'antimodeMenu-button'}> + <button type="button" className="antimodeMenu-button"> <FontAwesomeIcon icon="bullseye" size="lg" /> </button> <select className="collectionViewBaseChrome-cmdPicker" onPointerDown={stopPropagation} onChange={this.commandChanged} value={this._currentKey}> - <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} key={'empty'} value={''} /> + <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} key="empty" value="" /> {this._buttonizableCommands?.map(cmd => ( <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} key={cmd.title} value={cmd.title}> {cmd.title} @@ -471,23 +479,19 @@ export class CollectionNoteTakingViewChrome extends React.Component<CollectionVi if (docs instanceof Doc) { const keys = Object.keys(docs).filter(key => key.indexOf('title') >= 0 || key.indexOf('author') >= 0 || key.indexOf('author_date') >= 0 || key.indexOf('modificationDate') >= 0 || (key[0].toUpperCase() === key[0] && key[0] !== '_')); return keys.filter(key => key.toLowerCase().indexOf(val) > -1); - } else { - const keys = new Set<string>(); - docs.forEach(doc => Doc.allKeys(doc).forEach(key => keys.add(key))); - const noviceKeys = Array.from(keys).filter( - key => key.indexOf('title') >= 0 || key.indexOf('author') >= 0 || key.indexOf('author_date') >= 0 || key.indexOf('modificationDate') >= 0 || (key[0]?.toUpperCase() === key[0] && key[0] !== '_') - ); - return noviceKeys.filter(key => key.toLowerCase().indexOf(val) > -1); } + const keys = new Set<string>(); + docs.forEach(doc => Doc.allKeys(doc).forEach(key => keys.add(key))); + const noviceKeys = Array.from(keys).filter(key => key.indexOf('title') >= 0 || key.indexOf('author') >= 0 || key.indexOf('author_date') >= 0 || key.indexOf('modificationDate') >= 0 || (key[0]?.toUpperCase() === key[0] && key[0] !== '_')); + return noviceKeys.filter(key => key.toLowerCase().indexOf(val) > -1); } if (docs instanceof Doc) { return Object.keys(docs).filter(key => key.toLowerCase().indexOf(val) > -1); - } else { - const keys = new Set<string>(); - docs.forEach(doc => Doc.allKeys(doc).forEach(key => keys.add(key))); - return Array.from(keys).filter(key => key.toLowerCase().indexOf(val) > -1); } + const keys = new Set<string>(); + docs.forEach(doc => Doc.allKeys(doc).forEach(key => keys.add(key))); + return Array.from(keys).filter(key => key.toLowerCase().indexOf(val) > -1); }; @action @@ -497,9 +501,7 @@ export class CollectionNoteTakingViewChrome extends React.Component<CollectionVi getSuggestionValue = (suggestion: string) => suggestion; - renderSuggestion = (suggestion: string) => { - return <p>{suggestion}</p>; - }; + renderSuggestion = (suggestion: string) => <p>{suggestion}</p>; onSuggestionFetch = async ({ value }: { value: string }) => { const sugg = await this.getKeySuggestions(value); @@ -587,12 +589,16 @@ export class CollectionGridViewChrome extends React.Component<CollectionViewMenu } componentDidMount() { - runInAction(() => (this.resize = this.props.docView.props.PanelWidth() < 700)); + runInAction(() => { + this.resize = this.props.docView.props.PanelWidth() < 700; + }); // listener to reduce text on chrome resize (panel resize) this.resizeListenerDisposer = reaction( () => this.panelWidth, - newValue => (this.resize = newValue < 700) + newValue => { + this.resize = newValue < 700; + } ); } @@ -608,7 +614,10 @@ export class CollectionGridViewChrome extends React.Component<CollectionViewMenu * Sets the value of `numCols` on the grid's Document to the value entered. */ onNumColsChange = (e: React.ChangeEvent<HTMLInputElement>) => { - if (e.currentTarget.valueAsNumber > 0) undoBatch(() => (this.document.gridNumCols = e.currentTarget.valueAsNumber))(); + if (e.currentTarget.valueAsNumber > 0) + undoBatch(() => { + this.document.gridNumCols = e.currentTarget.valueAsNumber; + })(); }; /** @@ -637,7 +646,9 @@ export class CollectionGridViewChrome extends React.Component<CollectionViewMenu onIncrementButtonClick = () => { this.clicked = true; this.entered && (this.document.gridNumCols as number)--; - undoBatch(() => (this.document.gridNumCols = this.numCols + 1))(); + undoBatch(() => { + this.document.gridNumCols = this.numCols + 1; + })(); this.entered = false; }; @@ -648,7 +659,9 @@ export class CollectionGridViewChrome extends React.Component<CollectionViewMenu this.clicked = true; if (this.numCols > 1 && !this.decrementLimitReached) { this.entered && (this.document.gridNumCols as number)++; - undoBatch(() => (this.document.gridNumCols = this.numCols - 1))(); + undoBatch(() => { + this.document.gridNumCols = this.numCols - 1; + })(); if (this.numCols === 1) this.decrementLimitReached = true; } this.entered = false; @@ -741,7 +754,13 @@ export class CollectionGridViewChrome extends React.Component<CollectionViewMenu <label className="flexLabel">{this.resize ? 'Flex' : 'Flexible'}</label> </span> - <button onClick={() => (this.document.gridResetLayout = true)}>{!this.resize ? 'Reset' : <FontAwesomeIcon icon="redo-alt" size="1x" />}</button> + <button + type="button" + onClick={() => { + this.document.gridResetLayout = true; + }}> + {!this.resize ? 'Reset' : <FontAwesomeIcon icon="redo-alt" size="1x" />} + </button> </div> ); } diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx index 46bf56dc8..ee79812a1 100644 --- a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx +++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx @@ -1,3 +1,4 @@ +/* eslint-disable no-restricted-syntax */ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Popup, PopupTrigger, Type } from 'browndash-components'; import { ObservableMap, action, computed, makeObservable, observable, observe } from 'mobx'; @@ -31,6 +32,7 @@ import { CollectionSubView } from '../CollectionSubView'; import './CollectionSchemaView.scss'; import { SchemaColumnHeader } from './SchemaColumnHeader'; import { SchemaRowBox } from './SchemaRowBox'; + const { SCHEMA_NEW_NODE_HEIGHT } = require('../../global/globalCssVariables.module.scss'); // prettier-ignore export const FInfotoColType: { [key: string]: ColumnType } = { @@ -90,12 +92,11 @@ export class CollectionSchemaView extends CollectionSubView() { @computed get _selectedDocs() { const selected = SelectionManager.Docs.filter(doc => Doc.AreProtosEqual(DocCast(doc.embedContainer), this.Document)); if (!selected.length) { - for (const sel of SelectionManager.Docs) { - const contextPath = DocumentManager.GetContextPath(sel, true); - if (contextPath.includes(this.Document)) { - const parentInd = contextPath.indexOf(this.Document); - return parentInd < contextPath.length - 1 ? [contextPath[parentInd + 1]] : []; - } + // if no schema doc is directly selected, test if a child of a schema doc is selected (such as in the preview window) + const childOfSchemaDoc = SelectionManager.Docs.find(sel => DocumentManager.GetContextPath(sel, true).includes(this.Document)); + if (childOfSchemaDoc) { + const contextPath = DocumentManager.GetContextPath(childOfSchemaDoc, true); + return [contextPath[contextPath.indexOf(childOfSchemaDoc) - 1]]; // the schema doc that is "selected" by virtue of one of its children being selected } } return selected; @@ -158,7 +159,9 @@ export class CollectionSchemaView extends CollectionSubView() { Object.keys(proto).forEach(action(key => // check if any of its keys are new, and add them !this.fieldInfos.get(key) && this.fieldInfos.set(key, new FInfo("-no description-", key === 'author')))))); break; - case 'update': //let oldValue = change.oldValue; // fill this in if the entire child list will ever be reassigned with a new list + case 'update': // let oldValue = change.oldValue; // fill this in if the entire child list will ever be reassigned with a new list + break; + default: } }, true @@ -187,7 +190,7 @@ export class CollectionSchemaView extends CollectionSubView() { if (this._selectedDocs.includes(newDoc)) { SelectionManager.DeselectView(DocumentManager.Instance.getFirstDocumentView(curDoc)); } else { - this.addDocToSelection(newDoc, e.shiftKey, lastIndex + 1); + this.addDocToSelection(newDoc, e.shiftKey); this._selectedCell && (this._selectedCell[0] = newDoc); this.scrollToDoc(newDoc, {}); } @@ -206,7 +209,7 @@ export class CollectionSchemaView extends CollectionSubView() { const newDoc = this.sortedDocs.docs[firstIndex - 1]; if (this._selectedDocs.includes(newDoc)) SelectionManager.DeselectView(DocumentManager.Instance.getFirstDocumentView(curDoc)); else { - this.addDocToSelection(newDoc, e.shiftKey, firstIndex - 1); + this.addDocToSelection(newDoc, e.shiftKey); this._selectedCell && (this._selectedCell[0] = newDoc); this.scrollToDoc(newDoc, {}); } @@ -235,7 +238,9 @@ export class CollectionSchemaView extends CollectionSubView() { } case 'Escape': { this.deselectCell(); + break; } + default: } } }; @@ -254,7 +259,7 @@ export class CollectionSchemaView extends CollectionSubView() { this.addNewKey(newKey, defaultVal); } - let currKeys = [...this.columnKeys]; + const currKeys = [...this.columnKeys]; currKeys[index] = newKey; this.layoutDoc.schema_columnKeys = new List<string>(currKeys); }; @@ -271,13 +276,16 @@ export class CollectionSchemaView extends CollectionSubView() { const newDesiredTableWidth = currWidths.reduce((w, cw) => w + cw, 0); this.layoutDoc.schema_columnWidths = new List<number>(currWidths.map(w => (w / newDesiredTableWidth) * (this.tableWidth - CollectionSchemaView._rowMenuWidth))); - let currKeys = this.columnKeys.slice(); + const currKeys = this.columnKeys.slice(); currKeys.splice(0, 0, key); this.layoutDoc.schema_columnKeys = new List<string>(currKeys); }; @action - addNewKey = (key: string, defaultVal: any) => this.childDocs.forEach(doc => (doc[DocData][key] = defaultVal)); + addNewKey = (key: string, defaultVal: any) => + this.childDocs.forEach(doc => { + doc[DocData][key] = defaultVal; + }); @undoBatch removeColumn = (index: number) => { @@ -287,7 +295,7 @@ export class CollectionSchemaView extends CollectionSubView() { const newDesiredTableWidth = currWidths.reduce((w, cw) => w + cw, 0); this.layoutDoc.schema_columnWidths = new List<number>(currWidths.map(w => (w / newDesiredTableWidth) * (this.tableWidth - CollectionSchemaView._rowMenuWidth))); - let currKeys = this.columnKeys.slice(); + const currKeys = this.columnKeys.slice(); currKeys.splice(index, 1); this.layoutDoc.schema_columnKeys = new List<string>(currKeys); }; @@ -295,7 +303,7 @@ export class CollectionSchemaView extends CollectionSubView() { @action startResize = (e: any, index: number) => { this._displayColumnWidths = this.storedColumnWidths; - setupMoveUpEvents(this, e, (e, delta) => this.resizeColumn(e, index), this.finishResize, emptyFunction); + setupMoveUpEvents(this, e, moveEv => this.resizeColumn(moveEv, index), this.finishResize, emptyFunction); }; @action @@ -334,11 +342,11 @@ export class CollectionSchemaView extends CollectionSubView() { @undoBatch moveColumn = (fromIndex: number, toIndex: number) => { - let currKeys = this.columnKeys.slice(); + const currKeys = this.columnKeys.slice(); currKeys.splice(toIndex, 0, currKeys.splice(fromIndex, 1)[0]); this.layoutDoc.schema_columnKeys = new List<string>(currKeys); - let currWidths = this.storedColumnWidths.slice(); + const currWidths = this.storedColumnWidths.slice(); currWidths.splice(toIndex, 0, currWidths.splice(fromIndex, 1)[0]); this.layoutDoc.schema_columnWidths = new List<number>(currWidths); }; @@ -352,7 +360,7 @@ export class CollectionSchemaView extends CollectionSubView() { document.removeEventListener('pointermove', this.highlightDropColumn); document.addEventListener('pointermove', this.highlightDropColumn); - let stopHighlight = (e: PointerEvent) => { + const stopHighlight = () => { document.removeEventListener('pointermove', this.highlightDropColumn); document.removeEventListener('pointerup', stopHighlight); }; @@ -405,7 +413,7 @@ export class CollectionSchemaView extends CollectionSubView() { }; @action - addDocToSelection = (doc: Doc, extendSelection: boolean, index: number) => { + addDocToSelection = (doc: Doc, extendSelection: boolean) => { const rowDocView = DocumentManager.Instance.getDocumentView(doc); if (rowDocView) SelectionManager.SelectView(rowDocView, extendSelection); }; @@ -420,19 +428,25 @@ export class CollectionSchemaView extends CollectionSubView() { const endRow = Math.max(lastSelectedRow, index); for (let i = startRow; i <= endRow; i++) { const currDoc = this.sortedDocs.docs[i]; - if (!this._selectedDocs.includes(currDoc)) this.addDocToSelection(currDoc, true, i); + if (!this._selectedDocs.includes(currDoc)) this.addDocToSelection(currDoc, true); } }; @action - selectCell = (doc: Doc, index: number) => (this._selectedCell = [doc, index]); + selectCell = (doc: Doc, index: number) => { + this._selectedCell = [doc, index]; + }; @action - deselectCell = () => (this._selectedCell = undefined); + deselectCell = () => { + this._selectedCell = undefined; + }; sortedSelectedDocs = () => this.sortedDocs.docs.filter(doc => this._selectedDocs.includes(doc)); - setDropIndex = (index: number) => (this._closestDropIndex = index); + setDropIndex = (index: number) => { + this._closestDropIndex = index; + }; onInternalDrop = (e: Event, de: DragManager.DropEvent) => { if (de.complete.columnDragData) { @@ -476,7 +490,7 @@ export class CollectionSchemaView extends CollectionSubView() { onDividerDown = (e: React.PointerEvent) => setupMoveUpEvents(this, e, this.onDividerMove, emptyFunction, emptyFunction); @action - onDividerMove = (e: PointerEvent, down: number[], delta: number[]) => { + onDividerMove = (e: PointerEvent) => { const nativeWidth = this._previewRef!.getBoundingClientRect(); const minWidth = 40; const maxWidth = 1000; @@ -506,37 +520,76 @@ export class CollectionSchemaView extends CollectionSubView() { const rect = found.getBoundingClientRect(); const localRect = this.ScreenToLocalBoxXf().transformBounds(rect.left, rect.top, rect.width, rect.height); if (localRect.y < this.rowHeightFunc() || localRect.y + localRect.height > this._props.PanelHeight()) { - let focusSpeed = options.zoomTime ?? 50; + const focusSpeed = options.zoomTime ?? 50; smoothScroll(focusSpeed, this._tableContentRef!, localRect.y + this._tableContentRef!.scrollTop - this.rowHeightFunc(), options.easeFunc); return focusSpeed; } } + return undefined; }; @computed get fieldDefaultInput() { switch (this._newFieldType) { case ColumnType.Number: - return <input type="number" name="" id="" value={this._newFieldDefault ?? 0} onPointerDown={e => e.stopPropagation()} onChange={action(e => (this._newFieldDefault = e.target.value))} />; + return ( + <input + type="number" + name="" + id="" + value={this._newFieldDefault ?? 0} + onPointerDown={e => e.stopPropagation()} + onChange={action(e => { + this._newFieldDefault = e.target.value; + })} + /> + ); case ColumnType.Boolean: return ( <> - <input type="checkbox" name="" id="" value={this._newFieldDefault} onPointerDown={e => e.stopPropagation()} onChange={action(e => (this._newFieldDefault = e.target.checked))} /> + <input + type="checkbox" + name="" + id="" + value={this._newFieldDefault} + onPointerDown={e => e.stopPropagation()} + onChange={action(e => { + this._newFieldDefault = e.target.checked; + })} + /> {this._newFieldDefault ? 'true' : 'false'} </> ); case ColumnType.String: - return <input type="text" name="" id="" value={this._newFieldDefault ?? ''} onPointerDown={e => e.stopPropagation()} onChange={action(e => (this._newFieldDefault = e.target.value))} />; + return ( + <input + type="text" + name="" + id="" + value={this._newFieldDefault ?? ''} + onPointerDown={e => e.stopPropagation()} + onChange={action(e => { + this._newFieldDefault = e.target.value; + })} + /> + ); + default: + return undefined; } } onSearchKeyDown = (e: React.KeyboardEvent) => { switch (e.key) { case 'Enter': - this._menuKeys.length > 0 && this._menuValue.length > 0 ? this.setKey(this._menuKeys[0]) : action(() => (this._makeNewField = true))(); + this._menuKeys.length > 0 && this._menuValue.length > 0 + ? this.setKey(this._menuKeys[0]) + : action(() => { + this._makeNewField = true; + })(); break; case 'Escape': this.closeColumnMenu(); break; + default: } }; @@ -568,7 +621,9 @@ export class CollectionSchemaView extends CollectionSubView() { }; @action - closeColumnMenu = () => (this._columnMenuIndex = undefined); + closeColumnMenu = () => { + this._columnMenuIndex = undefined; + }; @action openFilterMenu = (index: number) => { @@ -577,7 +632,9 @@ export class CollectionSchemaView extends CollectionSubView() { }; @action - closeFilterMenu = () => (this._filterColumnIndex = undefined); + closeFilterMenu = () => { + this._filterColumnIndex = undefined; + }; openContextMenu = (x: number, y: number, index: number) => { this.closeColumnMenu(); @@ -607,7 +664,7 @@ export class CollectionSchemaView extends CollectionSubView() { this._menuKeys = this.documentKeys.filter(value => value.toLowerCase().includes(this._menuValue.toLowerCase())); }; - getFieldFilters = (field: string) => StrListCast(this.Document._childFilters).filter(filter => filter.split(Doc.FilterSep)[0] == field); + getFieldFilters = (field: string) => StrListCast(this.Document._childFilters).filter(filter => filter.split(Doc.FilterSep)[0] === field); removeFieldFilters = (field: string) => { this.getFieldFilters(field).forEach(filter => Doc.setDocFilter(this.Document, field, filter.split(Doc.FilterSep)[1], 'remove')); @@ -619,11 +676,14 @@ export class CollectionSchemaView extends CollectionSubView() { case 'Escape': this.closeFilterMenu(); break; + default: } }; @action - updateFilterSearch = (e: React.ChangeEvent<HTMLInputElement>) => (this._filterSearchValue = e.target.value); + updateFilterSearch = (e: React.ChangeEvent<HTMLInputElement>) => { + this._filterSearchValue = e.target.value; + }; @computed get newFieldMenu() { return ( @@ -632,7 +692,7 @@ export class CollectionSchemaView extends CollectionSubView() { <input type="radio" name="newFieldType" - checked={this._newFieldType == ColumnType.Number} + checked={this._newFieldType === ColumnType.Number} onChange={action(() => { this._newFieldType = ColumnType.Number; this._newFieldDefault = 0; @@ -644,7 +704,7 @@ export class CollectionSchemaView extends CollectionSubView() { <input type="radio" name="newFieldType" - checked={this._newFieldType == ColumnType.Boolean} + checked={this._newFieldType === ColumnType.Boolean} onChange={action(() => { this._newFieldType = ColumnType.Boolean; this._newFieldDefault = false; @@ -656,7 +716,7 @@ export class CollectionSchemaView extends CollectionSubView() { <input type="radio" name="newFieldType" - checked={this._newFieldType == ColumnType.String} + checked={this._newFieldType === ColumnType.String} onChange={action(() => { this._newFieldType = ColumnType.String; this._newFieldDefault = ''; @@ -668,7 +728,7 @@ export class CollectionSchemaView extends CollectionSubView() { <div className="schema-key-warning">{this._newFieldWarning}</div> <div className="schema-column-menu-button" - onPointerDown={action(e => { + onPointerDown={action(() => { if (this.documentKeys.includes(this._menuValue)) { this._newFieldWarning = 'Field already exists'; } else if (this._menuValue.length === 0) { @@ -733,7 +793,7 @@ export class CollectionSchemaView extends CollectionSubView() { } @computed get renderColumnMenu() { - const x = this._columnMenuIndex! == -1 ? 0 : this.displayColumnWidths.reduce((total, curr, index) => total + (index < this._columnMenuIndex! ? curr : 0), CollectionSchemaView._rowMenuWidth); + const x = this._columnMenuIndex! === -1 ? 0 : this.displayColumnWidths.reduce((total, curr, index) => total + (index < this._columnMenuIndex! ? curr : 0), CollectionSchemaView._rowMenuWidth); return ( <div className="schema-column-menu" style={{ left: x, minWidth: CollectionSchemaView._minColWidth }}> <input className="schema-key-search-input" type="text" onKeyDown={this.onSearchKeyDown} onChange={this.updateKeySearch} onPointerDown={e => e.stopPropagation()} /> @@ -817,7 +877,7 @@ export class CollectionSchemaView extends CollectionSubView() { : [...this.childDocs].sort((docA, docB) => { const aStr = Field.toString(docA[field] as FieldType); const bStr = Field.toString(docB[field] as FieldType); - var out = 0; + let out = 0; if (aStr < bStr) out = -1; if (aStr > bStr) out = 1; if (desc) out *= -1; @@ -835,7 +895,7 @@ export class CollectionSchemaView extends CollectionSubView() { render() { return ( <div className="collectionSchemaView" ref={(ele: HTMLDivElement | null) => this.createDashEventsTarget(ele)} onDrop={this.onExternalDrop.bind(this)}> - <div ref={this._menuTarget} style={{ background: 'red', top: 0, left: 0, position: 'absolute', zIndex: 10000 }}></div> + <div ref={this._menuTarget} style={{ background: 'red', top: 0, left: 0, position: 'absolute', zIndex: 10000 }} /> <div className="schema-table" style={{ width: `calc(100% - ${this.previewWidth}px)` }} @@ -851,7 +911,7 @@ export class CollectionSchemaView extends CollectionSubView() { placement="right" background={SettingsManager.userBackgroundColor} color={SettingsManager.userColor} - toggle={<FontAwesomeIcon onPointerDown={e => this.openColumnMenu(-1, true)} icon="plus" />} + toggle={<FontAwesomeIcon onPointerDown={() => this.openColumnMenu(-1, true)} icon="plus" />} trigger={PopupTrigger.CLICK} type={Type.TERT} isOpen={this._columnMenuIndex !== -1 ? false : undefined} @@ -860,6 +920,7 @@ export class CollectionSchemaView extends CollectionSubView() { </div> {this.columnKeys.map((key, index) => ( <SchemaColumnHeader + // eslint-disable-next-line react/no-array-index-key key={index} columnIndex={index} columnKeys={this.columnKeys} @@ -879,28 +940,42 @@ export class CollectionSchemaView extends CollectionSubView() { </div> {this._columnMenuIndex !== undefined && this._columnMenuIndex !== -1 && this.renderColumnMenu} {this._filterColumnIndex !== undefined && this.renderFilterMenu} - <CollectionSchemaViewDocs schema={this} childDocs={this.sortedDocsFunc} rowHeight={this.rowHeightFunc} setRef={(ref: HTMLDivElement | null) => (this._tableContentRef = ref)} /> + { + // eslint-disable-next-line no-use-before-define + <CollectionSchemaViewDocs + schema={this} + childDocs={this.sortedDocsFunc} + rowHeight={this.rowHeightFunc} + setRef={(ref: HTMLDivElement | null) => { + this._tableContentRef = ref; + }} + /> + } {this.layoutDoc.chromeHidden ? null : ( <div className="schema-add"> <EditableView GetValue={returnEmptyString} SetValue={undoable(value => (value ? this.addRow(Docs.Create.TextDocument(value, { title: value, _layout_autoHeight: true })) : false), 'add text doc')} placeholder={"Type text to create note or ':' to create specific type"} - contents={'+ New Node'} + contents="+ New Node" menuCallback={this.menuCallback} height={CollectionSchemaView._newNodeInputHeight} /> </div> )} </div> - {this.previewWidth > 0 && <div className="schema-preview-divider" style={{ width: CollectionSchemaView._previewDividerWidth }} onPointerDown={this.onDividerDown}></div>} + {this.previewWidth > 0 && <div className="schema-preview-divider" style={{ width: CollectionSchemaView._previewDividerWidth }} onPointerDown={this.onDividerDown} />} {this.previewWidth > 0 && ( - <div style={{ width: `${this.previewWidth}px` }} ref={ref => (this._previewRef = ref)}> + <div + style={{ width: `${this.previewWidth}px` }} + ref={ref => { + this._previewRef = ref; + }}> {Array.from(this._selectedDocs).lastElement() && ( <DocumentView Document={Array.from(this._selectedDocs).lastElement()} fitContentsToBox={returnTrue} - dontCenter={'y'} + dontCenter="y" onClickScriptDisable="always" focus={emptyFunction} defaultDoubleClick={returnIgnore} @@ -945,7 +1020,10 @@ class CollectionSchemaViewDocs extends React.Component<CollectionSchemaViewDocsP <div className="schema-table-content" ref={this.props.setRef} style={{ height: `calc(100% - ${CollectionSchemaView._newNodeInputHeight + this.props.rowHeight()}px)` }}> {this.props.childDocs().docs.map((doc: Doc, index: number) => ( <div key={doc[Id]} className="schema-row-wrapper" style={{ height: this.props.rowHeight() }}> - <CollectionSchemaViewDoc doc={doc} schema={this.props.schema} index={index} rowHeight={this.props.rowHeight} /> + { + // eslint-disable-next-line no-use-before-define + <CollectionSchemaViewDoc doc={doc} schema={this.props.schema} index={index} rowHeight={this.props.rowHeight} /> + } </div> ))} </div> @@ -977,6 +1055,7 @@ class CollectionSchemaViewDoc extends ObservableReactComponent<CollectionSchemaV return ( <DocumentView key={this._props.doc[Id]} + // eslint-disable-next-line react/jsx-props-no-spreading {...this._props.schema._props} containerViewPath={this._props.schema.childContainerViewPath} LayoutTemplate={this._props.schema._props.childLayoutTemplate} @@ -996,14 +1075,14 @@ class CollectionSchemaViewDoc extends ObservableReactComponent<CollectionSchemaV searchFilterDocs={this._props.schema.searchFilterDocs} rootSelected={this._props.schema.rootSelected} ScreenToLocalTransform={this.screenToLocalXf} - dragWhenActive={true} + dragWhenActive isDocumentActive={this._props.schema._props.childDocumentsActive?.() ? this._props.schema._props.isDocumentActive : this._props.schema.isContentActive} isContentActive={emptyFunction} whenChildContentsActiveChanged={this._props.schema._props.whenChildContentsActiveChanged} - hideDecorations={true} - hideTitle={true} - hideDocumentButtonBar={true} - hideLinkAnchors={true} + hideDecorations + hideTitle + hideDocumentButtonBar + hideLinkAnchors layout_fitWidth={returnTrue} /> ); diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index 832e18b68..cc4b5b67f 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -21,7 +21,6 @@ import { CollectionSchemaView } from '../collections/collectionSchema/Collection import { SchemaRowBox } from '../collections/collectionSchema/SchemaRowBox'; import { PresElementBox } from './trails/PresElementBox'; import { SearchBox } from '../search/SearchBox'; -import { DashWebRTCVideo } from '../webcam/DashWebRTCVideo'; import { AudioBox } from './AudioBox'; import { ComparisonBox } from './ComparisonBox'; import { DataVizBox } from './DataVizBox/DataVizBox'; @@ -248,7 +247,6 @@ export class DocumentContentsView extends ObservableReactComponent<DocumentConte PresElementBox, SearchBox, FunctionPlotBox, - DashWebRTCVideo, LinkAnchorBox, InkingStroke, LinkBox, diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss index 3a1a72910..99b4a84fc 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.scss +++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss @@ -349,7 +349,7 @@ footnote::before { touch-action: none; span { font-family: inherit; - background-color: inherit; + // background-color: inherit; // intended to allow texts to inherit background from list container, but this prevents css highlights e.,g highlight text from others display: inline; // needs to be inline for search highlighting to appear // display: contents; // BUT needs to be 'contents' to avoid Chrome bug where extra space is added above and <ol> lists when inside a prosemirror span } diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 99a2f4ab9..ba37c3265 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -65,6 +65,7 @@ import { FootnoteView } from './FootnoteView'; import './FormattedTextBox.scss'; import { findLinkMark, FormattedTextBoxComment } from './FormattedTextBoxComment'; import { buildKeymap, updateBullets } from './ProsemirrorExampleTransfer'; +// eslint-disable-next-line import/extensions import { removeMarkWithAttrs } from './prosemirrorPatches'; import { RichTextMenu, RichTextMenuPlugin } from './RichTextMenu'; import { RichTextRules } from './RichTextRules'; @@ -291,7 +292,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB }); }; AnchorMenu.Instance.Highlight = undoable((color: string) => { - this._editorView?.state && RichTextMenu.Instance?.setHighlight(color); + this._editorView?.state && RichTextMenu.Instance?.setFontField(color, 'fontHighlight'); return undefined; }, 'highlght text'); AnchorMenu.Instance.onMakeAnchor = () => this.getAnchor(true); @@ -637,8 +638,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB const pos = view.posAtCoords({ left: de.x, top: de.y })?.pos; pos && view.dispatch(view.state.tr.insert(pos, node)); added = !!pos; // pos will be null if you don't drop onto an actual text location - } catch (e) { - console.log('Drop failed', e); + } catch (err) { + console.log('Drop failed', err); added = false; } finally { this._inDrop = false; @@ -778,7 +779,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB this, e, this.sidebarMove, - (e, movement, isClick) => !isClick && batch.end(), + (moveEv, movement, isClick) => !isClick && batch.end(), () => { this.toggleSidebar(); batch.end(); @@ -1301,7 +1302,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB this._disposers.selected = reaction( () => this._props.rootSelected?.(), action(selected => { - // selected && setTimeout(() => this.prepareForTyping()); + this.prepareForTyping(); if (FormattedTextBox._globalHighlights.has('Bold Text')) { // eslint-disable-next-line operator-assignment this.layoutDoc[DocCss] = this.layoutDoc[DocCss] + 1; // css change happens outside of mobx/react, so this will notify anyone interested in the layout that it has changed @@ -1498,8 +1499,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB this._editorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(tr.doc.content.size)))); this.tryUpdateDoc(true); // calling select() above will make isContentActive() true only after a render .. which means the selectAll() above won't write to the Document and the incomingValue will overwrite the selection with the non-updated data - } else if (curText && !FormattedTextBox.DontSelectInitialText) { - selectAll(this._editorView.state, this._editorView?.dispatch); + } else if (!FormattedTextBox.DontSelectInitialText) { + const mark = schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified: Math.floor(Date.now() / 1000) }); + selectAll(this._editorView.state, (tx: Transaction) => { + this._editorView?.dispatch(tx.deleteSelection().addStoredMark(mark)); + }); this.tryUpdateDoc(true); // calling select() above will make isContentActive() true only after a render .. which means the selectAll() above won't write to the Document and the incomingValue will overwrite the selection with the non-updated data } else { const $from = this._editorView.state.selection.anchor ? this._editorView.state.doc.resolve(this._editorView.state.selection.anchor - 1) : undefined; @@ -1526,17 +1530,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB // add user mark for any first character that was typed since the user mark that gets set in KeyPress won't have been called yet. prepareForTyping = () => { - if (!this._editorView) return; - const docDefaultMarks = [ - ...(Doc.UserDoc().fontColor !== 'transparent' && Doc.UserDoc().fontColor ? [schema.mark(schema.marks.pFontColor, { color: StrCast(Doc.UserDoc().fontColor) })] : []), - ...(Doc.UserDoc().fontStyle === 'italics' ? [schema.mark(schema.marks.em)] : []), - ...(Doc.UserDoc().textDecoration === 'underline' ? [schema.mark(schema.marks.underline)] : []), - ...(Doc.UserDoc().fontFamily ? [schema.mark(schema.marks.pFontFamily, { fontFamily: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontFamily) })] : []), - ...(Doc.UserDoc().fontSize ? [schema.mark(schema.marks.pFontSize, { fontSize: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontSize) })] : []), - ...(Doc.UserDoc().fontWeight === 'bold' ? [schema.mark(schema.marks.strong)] : []), - ...[schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified: Math.floor(Date.now() / 1000) })], - ]; - this._editorView?.dispatch(this._editorView?.state.tr.setStoredMarks(docDefaultMarks)); + if (this._editorView) { + const { text, paragraph } = schema.nodes; + const selNode = this._editorView.state.selection.$anchor.node(); + if (this._editorView.state.selection.from === 1 && this._editorView.state.selection.empty && [undefined, text, paragraph].includes(selNode?.type)) { + const docDefaultMarks = [schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified: Math.floor(Date.now() / 1000) })]; + this._editorView.state.selection.empty && this._editorView.state.selection.from === 1 && this._editorView?.dispatch(this._editorView?.state.tr.setStoredMarks(docDefaultMarks).removeStoredMark(schema.marks.pFontColor)); + } + } }; componentWillUnmount() { @@ -1601,10 +1602,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB const state = this.EditorView?.state; if (state && this.ProseRef?.children[0].className.includes('-focused') && this._props.isContentActive() && !e.button) { if (!state.selection.empty && !(state.selection instanceof NodeSelection)) this.setupAnchorMenu(); - let target = e.target as any; // hrefs are stored on the dataset of the <a> node that wraps the hyerlink <span> - for (let target = e.target as any; target && !target.dataset?.targethrefs; target = target.parentElement); - while (target && !target.dataset?.targethrefs) target = target.parentElement; - FormattedTextBoxComment.update(this, this.EditorView!, undefined, target?.dataset?.targethrefs, target?.dataset.linkdoc, target?.dataset.nopreview === 'true'); + let clickTarget = e.target as any; // hrefs are stored on the dataset of the <a> node that wraps the hyerlink <span> + for (let { target } = e as any; target && !target.dataset?.targethrefs; target = target.parentElement); + while (clickTarget && !clickTarget.dataset?.targethrefs) clickTarget = clickTarget.parentElement; + FormattedTextBoxComment.update(this, this.EditorView!, undefined, clickTarget?.dataset?.targethrefs, clickTarget?.dataset.linkdoc, clickTarget?.dataset.nopreview === 'true'); } }; @action @@ -1724,9 +1725,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB if (!(this.EditorView?.state.selection instanceof NodeSelection)) { this.autoLink(); if (this._editorView?.state.tr) { - const tr = stordMarks?.reduce((tr, m) => { - tr.addStoredMark(m); - return tr; + const tr = stordMarks?.reduce((tr2, m) => { + tr2.addStoredMark(m); + return tr2; }, this._editorView.state.tr); tr && this._editorView.dispatch(tr); } @@ -2038,7 +2039,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB }; _oldWheel: any; @computed get fontColor() { - return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color); + return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontColor); } @computed get fontSize() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontSize); diff --git a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts index 073ed91c3..5d448d40e 100644 --- a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts +++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts @@ -7,7 +7,7 @@ import { liftTarget } from 'prosemirror-transform'; import { EditorView } from 'prosemirror-view'; import { ClientUtils } from '../../../../ClientUtils'; import { Utils } from '../../../../Utils'; -import { AclAdmin, AclAugment, AclEdit } from '../../../../fields/DocSymbols'; +import { AclAdmin, AclAugment, AclEdit, DocData } from '../../../../fields/DocSymbols'; import { GetEffectiveAcl } from '../../../../fields/util'; import { Docs } from '../../../documents/Documents'; import { RTFMarkup } from '../../../util/RTFMarkup'; @@ -48,11 +48,12 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any): KeyMa } const canEdit = (state: any) => { - switch (GetEffectiveAcl(props.TemplateDataDocument)) { + const permissions = GetEffectiveAcl(props.TemplateDataDocument ?? props.Document[DocData]); + switch (permissions) { case AclAugment: { const prevNode = state.selection.$cursor.nodeBefore; - const prevUser = !prevNode ? ClientUtils.CurrentUserEmail() : prevNode.marks[prevNode.marks.length - 1].attrs.userid; + const prevUser = !prevNode ? ClientUtils.CurrentUserEmail() : prevNode.marks.lastElement()?.attrs.userid; if (prevUser !== ClientUtils.CurrentUserEmail()) { return false; } @@ -278,7 +279,7 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any): KeyMa dispatch(updateBullets(tx, schema)); if (view.state.selection.$anchor.node(-1)?.type === schema.nodes.list_item) { // gets rid of an extra paragraph when joining two list items together. - joinBackward(view.state, (tx: Transaction) => view.dispatch(tx)); + joinBackward(view.state, (tx2: Transaction) => view.dispatch(tx2)); } }) ) { @@ -344,7 +345,7 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any): KeyMa !splitBlockKeepMarks(state, (tx3: Transaction) => { const tonode = tx3.selection.$to.node(); if (tx3.selection.to && tx3.doc.nodeAt(tx3.selection.to - 1)) { - const tx4 = tx3.setNodeMarkup(tx3.selection.to - 1, tonode.type, fromattrs, tonode.marks); + const tx4 = tx3.setNodeMarkup(tx3.selection.to - 1, tonode.type, fromattrs, tonode.marks).setStoredMarks(marks || []); dispatch(tx4); } @@ -365,7 +366,8 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any): KeyMa // Command to create a blank space bind('Space', () => { - if (props.TemplateDataDocument && ![AclAdmin, AclAugment, AclEdit].includes(GetEffectiveAcl(props.TemplateDataDocument))) return true; + const editDoc = props.TemplateDataDocument ?? props.Document[DocData]; + if (editDoc && ![AclAdmin, AclAugment, AclEdit].includes(GetEffectiveAcl(editDoc))) return true; return false; }); diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx index 6108383c2..6c12b9991 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.tsx +++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx @@ -10,7 +10,6 @@ import { EditorView } from 'prosemirror-view'; import * as React from 'react'; import { Doc } from '../../../../fields/Doc'; import { BoolCast, Cast, StrCast } from '../../../../fields/Types'; -import { numberRange } from '../../../../Utils'; import { DocServer } from '../../../DocServer'; import { LinkManager } from '../../../util/LinkManager'; import { SelectionManager } from '../../../util/SelectionManager'; @@ -147,12 +146,13 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { const { activeHighlights } = active; const refDoc = SelectionManager.Views.lastElement()?.layoutDoc ?? Doc.UserDoc(); const refField = (pfx => (pfx ? pfx + '_' : ''))(SelectionManager.Views.lastElement()?.LayoutFieldKey); + const refVal = (field: string, dflt: string) => StrCast(refDoc[refField + field], StrCast(Doc.UserDoc()[field], dflt)); this._activeListType = this.getActiveListStyle(); this._activeAlignment = this.getActiveAlignment(); - this._activeFontFamily = !activeFamilies.length ? StrCast(this.TextView?.Document._text_fontFamily, StrCast(refDoc[refField + 'fontFamily'], 'Arial')) : activeFamilies.length === 1 ? String(activeFamilies[0]) : 'various'; - this._activeFontSize = !activeSizes.length ? StrCast(this.TextView?.Document.fontSize, StrCast(refDoc[refField + 'fontSize'], '10px')) : activeSizes[0]; - this._activeFontColor = !activeColors.length ? StrCast(this.TextView?.Document.fontColor, StrCast(refDoc[refField + 'fontColor'], 'black')) : activeColors.length > 0 ? String(activeColors[0]) : '...'; + this._activeFontFamily = !activeFamilies.length ? StrCast(this.TextView?.Document._text_fontFamily, refVal('fontFamily', 'Arial')) : activeFamilies.length === 1 ? String(activeFamilies[0]) : 'various'; + this._activeFontSize = !activeSizes.length ? StrCast(this.TextView?.Document.fontSize, refVal('fontSize', '10px')) : activeSizes[0]; + this._activeFontColor = !activeColors.length ? StrCast(this.TextView?.Document.fontColor, refVal('fontColor', 'black')) : activeColors.length > 0 ? String(activeColors[0]) : '...'; this._activeHighlightColor = !activeHighlights.length ? '' : activeHighlights.length > 0 ? String(activeHighlights[0]) : '...'; // update link in current selection @@ -161,12 +161,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { setMark = (mark: Mark, state: EditorState, dispatch: any, dontToggle: boolean = false) => { if (mark) { - const liFirst = numberRange(state.selection.$from.depth + 1).find(i => state.selection.$from.node(i)?.type === state.schema.nodes.list_item); - const liTo = numberRange(state.selection.$to.depth + 1).find(i => state.selection.$to.node(i)?.type === state.schema.nodes.list_item); - const olFirst = numberRange(state.selection.$from.depth + 1).find(i => state.selection.$from.node(i)?.type === state.schema.nodes.ordered_list); - const nodeOl = (liFirst && liTo && state.selection.$from.node(liFirst) !== state.selection.$to.node(liTo) && olFirst) || (!liFirst && !liTo && olFirst); - const fromRange = numberRange(state.selection.from).reverse(); - const newPos = nodeOl ? fromRange.find(i => state.doc.nodeAt(i)?.type === state.schema.nodes.ordered_list) ?? state.selection.from : state.selection.from; + const newPos = state.selection.$anchor.node()?.type === schema.nodes.ordered_list ? state.selection.from : state.selection.from; const node = (state.selection as NodeSelection).node ?? (newPos >= 0 ? state.doc.nodeAt(newPos) : undefined); if (node?.type === schema.nodes.ordered_list || node?.type === schema.nodes.list_item) { const hasMark = node.marks.some(m => m.type === mark.type); @@ -174,18 +169,16 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { const addAnyway = node.marks.filter(m => m.type === mark.type && Object.keys(m.attrs).some(akey => m.attrs[akey] !== mark.attrs[akey])); const markup = state.tr.setNodeMarkup(newPos, node.type, node.attrs, hasMark && !addAnyway ? otherMarks : [...otherMarks, mark]); dispatch(updateBullets(markup, state.schema)); - } else { - const state = this.view?.state; - if (state) { - const { tr } = state; - if (dontToggle) { - tr.addMark(state.selection.from, state.selection.to, mark); - dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(state.selection.from), tr.doc.resolve(state.selection.to)))); // bcz: need to redo the selection because ctrl-a selections disappear otherwise - } else { - toggleMark(mark.type, mark.attrs)(state, dispatch); - } + } else if (state) { + const { tr } = state; + if (dontToggle) { + tr.addMark(state.selection.from, state.selection.to, mark); + dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(state.selection.from), tr.doc.resolve(state.selection.to)))); // bcz: need to redo the selection because ctrl-a selections disappear otherwise + } else { + toggleMark(mark.type, mark.attrs)(state, dispatch); } } + this.updateMenu(this.view, undefined, undefined, this.layoutDoc); } }; @@ -242,7 +235,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { m.type === state.schema.marks.pFontFamily && activeFamilies.add(m.attrs.fontFamily); m.type === state.schema.marks.pFontColor && activeColors.add(m.attrs.fontColor); m.type === state.schema.marks.pFontSize && activeSizes.add(m.attrs.fontSize); - m.type === state.schema.marks.pFontHighlight && activeHighlights.add(String(m.attrs.fontHigh)); + m.type === state.schema.marks.pFontHighlight && activeHighlights.add(String(m.attrs.fontHighlight)); }); } else if (SelectionManager.Views.some(dv => dv.ComponentView instanceof EquationBox)) { SelectionManager.Views.forEach(dv => StrCast(dv.Document._text_fontSize) && activeSizes.add(StrCast(dv.Document._text_fontSize))); @@ -359,18 +352,21 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { setFontField = (value: string, fontField: 'fontSize' | 'fontFamily' | 'fontColor' | 'fontHighlight') => { if (this.view) { - if (this.view.state.selection.from === 1 && this.view.state.selection.empty && (!this.view.state.doc.nodeAt(1) || !this.view.state.doc.nodeAt(1)?.marks.some(m => m.type.name === value))) { + const { text, paragraph } = this.view.state.schema.nodes; + const selNode = this.view.state.selection.$anchor.node(); + if (this.view.state.selection.from === 1 && this.view.state.selection.empty && [undefined, text, paragraph].includes(selNode?.type)) { this.TextView.dataDoc[this.TextView.fieldKey + `_${fontField}`] = value; this.view.focus(); - } else { - const attrs: { [key: string]: string } = {}; - attrs[fontField] = value; - const fmark = this.view?.state.schema.marks['pF' + fontField.substring(1)].create(attrs); - this.setMark(fmark, this.view.state, (tx: any) => this.view!.dispatch(tx.addStoredMark(fmark)), true); - this.view.focus(); } - } else Doc.UserDoc()[fontField] = value; - this.updateMenu(this.view, undefined, this.props, this.layoutDoc); + const attrs: { [key: string]: string } = {}; + attrs[fontField] = value; + const fmark = this.view?.state.schema.marks['pF' + fontField.substring(1)].create(attrs); + this.setMark(fmark, this.view.state, (tx: any) => this.view!.dispatch(tx.addStoredMark(fmark)), true); + this.view.focus(); + } else { + Doc.UserDoc()[fontField] = value; + this.updateMenu(this.view, undefined, this.props, this.layoutDoc); + } }; // TODO: remove doesn't work diff --git a/src/client/views/nodes/formattedText/nodes_rts.ts b/src/client/views/nodes/formattedText/nodes_rts.ts index 184487b7d..5bf942218 100644 --- a/src/client/views/nodes/formattedText/nodes_rts.ts +++ b/src/client/views/nodes/formattedText/nodes_rts.ts @@ -3,6 +3,7 @@ import { listItem, orderedList } from 'prosemirror-schema-list'; import { ParagraphNodeSpec, toParagraphDOM, getParagraphNodeAttrs } from './ParagraphNodeSpec'; import { DocServer } from '../../../DocServer'; import { Doc, Field, FieldType } from '../../../../fields/Doc'; +import { schema } from './schema_rts'; const blockquoteDOM: DOMOutputSpec = ['blockquote', 0]; const hrDOM: DOMOutputSpec = ['hr']; @@ -353,7 +354,7 @@ export const nodes: { [index: string]: NodeSpec } = { }, { style: 'list-style-type=disc', - getAttrs(dom: any) { + getAttrs() { return { mapStyle: 'bullet' }; }, }, @@ -373,10 +374,10 @@ export const nodes: { [index: string]: NodeSpec } = { ], toDOM(node: Node) { const map = node.attrs.bulletStyle ? node.attrs.mapStyle + node.attrs.bulletStyle : ''; - const fhigh = (found => (found ? `background-color: ${found};` : ''))(node.marks.find(m => m.type.name === 'marker')?.attrs.highlight); - const fsize = (found => (found ? `font-size: ${found};` : ''))(node.marks.find(m => m.type.name === 'pFontSize')?.attrs.fontSize); - const ffam = (found => (found ? `font-family: ${found};` : ''))(node.marks.find(m => m.type.name === 'pFontFamily')?.attrs.family); - const fcol = (found => (found ? `color: ${found};` : ''))(node.marks.find(m => m.type.name === 'pFontColor')?.attrs.color); + const fhigh = (found => (found ? `background-color: ${found};` : ''))(node.marks.find(m => m.type === schema.marks.pFontHighlight)?.attrs.fontHighlight); + const fsize = (found => (found ? `font-size: ${found};` : ''))(node.marks.find(m => m.type === schema.marks.pFontSize)?.attrs.fontSize); + const ffam = (found => (found ? `font-family: ${found};` : ''))(node.marks.find(m => m.type === schema.marks.pFontFamily)?.attrs.fontFamily); + const fcol = (found => (found ? `color: ${found};` : ''))(node.marks.find(m => m.type === schema.marks.pFontColor)?.attrs.fontColor); const marg = node.attrs.indent ? `margin-left: ${node.attrs.indent};` : ''; if (node.attrs.mapStyle === 'bullet') { return [ @@ -421,10 +422,10 @@ export const nodes: { [index: string]: NodeSpec } = { }, ], toDOM(node: Node) { - const fhigh = (found => (found ? `background-color: ${found};` : ''))(node.marks.find(m => m.type.name === 'marker')?.attrs.highlight); - const fsize = (found => (found ? `font-size: ${found};` : ''))(node.marks.find(m => m.type.name === 'pFontSize')?.attrs.fontSize); - const ffam = (found => (found ? `font-family: ${found};` : ''))(node.marks.find(m => m.type.name === 'pFontFamily')?.attrs.family); - const fcol = (found => (found ? `color: ${found};` : ''))(node.marks.find(m => m.type.name === 'pFontColor')?.attrs.color); + const fhigh = (found => (found ? `background-color: ${found};` : ''))(node.marks.find(m => m.type === schema.marks.pFontHighlight)?.attrs.fontHighlight); + const fsize = (found => (found ? `font-size: ${found};` : ''))(node.marks.find(m => m.type === schema.marks.pFontSize)?.attrs.fontSize); + const ffam = (found => (found ? `font-family: ${found};` : ''))(node.marks.find(m => m.type === schema.marks.pFontFamily)?.attrs.fontFamily); + const fcol = (found => (found ? `color: ${found};` : ''))(node.marks.find(m => m.type === schema.marks.pFontColor)?.attrs.fontColor); const map = node.attrs.bulletStyle ? node.attrs.mapStyle + node.attrs.bulletStyle : ''; return [ 'li', diff --git a/src/client/views/webcam/DashWebRTCVideo.scss b/src/client/views/webcam/DashWebRTCVideo.scss deleted file mode 100644 index 5744ebbcd..000000000 --- a/src/client/views/webcam/DashWebRTCVideo.scss +++ /dev/null @@ -1,82 +0,0 @@ -@import '../global/globalCssVariables.module.scss'; - -.webcam-cont { - background: whitesmoke; - color: grey; - border-radius: 15px; - box-shadow: #9c9396 0.2vw 0.2vw 0.4vw; - border: solid #bbbbbbbb 5px; - pointer-events: all; - display: flex; - flex-direction: column; - overflow: hidden; - - .webcam-header { - height: 50px; - text-align: center; - text-transform: uppercase; - letter-spacing: 2px; - font-size: 16px; - width: 100%; - margin-top: 20px; - } - - .videoContainer { - position: relative; - width: calc(100% - 20px); - height: 100%; - /* border: 10px solid red; */ - margin-left: 10px; - } - - .buttonContainer { - display: flex; - width: calc(100% - 20px); - height: 50px; - justify-content: center; - text-align: center; - /* border: 1px solid black; */ - margin-left: 10px; - margin-top: 0; - margin-bottom: 15px; - } - - #roomName { - outline: none; - border-radius: inherit; - border: 1px solid #bbbbbbbb; - margin: 10px; - padding: 10px; - } - - .side { - width: 25%; - height: 20%; - position: absolute; - /* top: 65%; */ - z-index: 2; - right: 0px; - bottom: 18px; - } - - .main { - position: absolute; - width: 100%; - height: 100%; - /* top: 20%; */ - align-self: center; - } - - .videoButtons { - border-radius: 50%; - height: 30px; - width: 30px; - display: flex; - justify-content: center; - align-items: center; - justify-self: center; - align-self: center; - margin: 5px; - border: 1px solid black; - } -} diff --git a/src/client/views/webcam/DashWebRTCVideo.tsx b/src/client/views/webcam/DashWebRTCVideo.tsx deleted file mode 100644 index 4e984f3d6..000000000 --- a/src/client/views/webcam/DashWebRTCVideo.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { IconLookup } from '@fortawesome/fontawesome-svg-core'; -import { faPhoneSlash, faSync } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, observable } from 'mobx'; -import { observer } from 'mobx-react'; -import * as React from 'react'; -import { Doc } from '../../../fields/Doc'; -import { InkTool } from '../../../fields/InkField'; -import { SnappingManager } from '../../util/SnappingManager'; -import '../../views/nodes/WebBox.scss'; -import { FieldView, FieldViewProps } from '../nodes/FieldView'; -import './DashWebRTCVideo.scss'; -import { hangup, initialize, refreshVideos } from './WebCamLogic'; - -/** - * This models the component that will be rendered, that can be used as a doc that will reflect the video cams. - */ -@observer -export class DashWebRTCVideo extends React.Component<FieldViewProps> { - private roomText: HTMLInputElement | undefined; - @observable remoteVideoAdded: boolean = false; - - @action - changeUILook = () => (this.remoteVideoAdded = true); - - /** - * Function that submits the title entered by user on enter press. - */ - private onEnterKeyDown = (e: React.KeyboardEvent) => { - if (e.keyCode === 13) { - const submittedTitle = this.roomText!.value; - this.roomText!.value = ''; - this.roomText!.blur(); - initialize(submittedTitle, this.changeUILook); - } - }; - - public static LayoutString(fieldKey: string) { - return FieldView.LayoutString(DashWebRTCVideo, fieldKey); - } - - onClickRefresh = () => refreshVideos(); - - onClickHangUp = () => hangup(); - - render() { - const content = ( - <div className="webcam-cont" style={{ width: '100%', height: '100%' }}> - <div className="webcam-header">DashWebRTC</div> - <input id="roomName" type="text" placeholder="Enter room name" ref={e => (this.roomText = e!)} onKeyDown={this.onEnterKeyDown} /> - <div className="videoContainer"> - <video id="localVideo" className={'RTCVideo' + (this.remoteVideoAdded ? ' side' : ' main')} autoPlay playsInline muted ref={e => {}}></video> - <video id="remoteVideo" className="RTCVideo main" autoPlay playsInline ref={e => {}}></video> - </div> - <div className="buttonContainer"> - <div className="videoButtons" style={{ background: 'red' }} onClick={this.onClickHangUp}> - <FontAwesomeIcon icon={faPhoneSlash as IconLookup} color="white" /> - </div> - <div className="videoButtons" style={{ background: 'green' }} onClick={this.onClickRefresh}> - <FontAwesomeIcon icon={faSync as IconLookup} color="white" /> - </div> - </div> - </div> - ); - - const frozen = !this.props.isSelected() || SnappingManager.IsResizing; - const classname = 'webBox-cont' + (this.props.isSelected() && Doc.ActiveTool === InkTool.None && !SnappingManager.IsResizing ? '-interactive' : ''); - - return ( - <> - <div className={classname}>{content}</div> - {!frozen ? null : <div className="webBox-overlay" />} - </> - ); - } -} diff --git a/src/client/views/webcam/WebCamLogic.js b/src/client/views/webcam/WebCamLogic.js deleted file mode 100644 index 5f6202bc8..000000000 --- a/src/client/views/webcam/WebCamLogic.js +++ /dev/null @@ -1,292 +0,0 @@ -'use strict'; -import io from "socket.io-client"; - -var socket; -var isChannelReady = false; -var isInitiator = false; -var isStarted = false; -var localStream; -var pc; -var remoteStream; -var turnReady; -var room; - -export function initialize(roomName, handlerUI) { - - var pcConfig = { - 'iceServers': [{ - 'urls': 'stun:stun.l.google.com:19302' - }] - }; - - // Set up audio and video regardless of what devices are present. - var sdpConstraints = { - offerToReceiveAudio: true, - offerToReceiveVideo: true - }; - - ///////////////////////////////////////////// - - room = roomName; - - socket = io.connect(`${window.location.protocol}//${window.location.hostname}:4321`); - - if (room !== '') { - socket.emit('create or join', room); - console.log('Attempted to create or join room', room); - } - - socket.on('created', function (room) { - console.log('Created room ' + room); - isInitiator = true; - }); - - socket.on('full', function (room) { - console.log('Room ' + room + ' is full'); - }); - - socket.on('join', function (room) { - console.log('Another peer made a request to join room ' + room); - console.log('This peer is the initiator of room ' + room + '!'); - isChannelReady = true; - }); - - socket.on('joined', function (room) { - console.log('joined: ' + room); - isChannelReady = true; - }); - - socket.on('log', function (array) { - console.log.apply(console, array); - }); - - //////////////////////////////////////////////// - - - // This client receives a message - socket.on('message', function (message) { - console.log('Client received message:', message); - if (message === 'got user media') { - maybeStart(); - } else if (message.type === 'offer') { - if (!isInitiator && !isStarted) { - maybeStart(); - } - pc.setRemoteDescription(new RTCSessionDescription(message)); - doAnswer(); - } else if (message.type === 'answer' && isStarted) { - pc.setRemoteDescription(new RTCSessionDescription(message)); - } else if (message.type === 'candidate' && isStarted) { - var candidate = new RTCIceCandidate({ - sdpMLineIndex: message.label, - candidate: message.candidate - }); - pc.addIceCandidate(candidate); - } else if (message === 'bye' && isStarted) { - handleRemoteHangup(); - } - }); - - //////////////////////////////////////////////////// - - var localVideo = document.querySelector('#localVideo'); - var remoteVideo = document.querySelector('#remoteVideo'); - - const gotStream = (stream) => { - console.log('Adding local stream.'); - localStream = stream; - localVideo.srcObject = stream; - sendMessage('got user media'); - if (isInitiator) { - maybeStart(); - } - } - - - navigator.mediaDevices.getUserMedia({ - audio: true, - video: true - }) - .then(gotStream) - .catch(function (e) { - alert('getUserMedia() error: ' + e.name); - }); - - - - var constraints = { - video: true - }; - - console.log('Getting user media with constraints', constraints); - - const requestTurn = (turnURL) => { - var turnExists = false; - for (var i in pcConfig.iceServers) { - if (pcConfig.iceServers[i].urls.substr(0, 5) === 'turn:') { - turnExists = true; - turnReady = true; - break; - } - } - if (!turnExists) { - console.log('Getting TURN server from ', turnURL); - // No TURN server. Get one from computeengineondemand.appspot.com: - var xhr = new XMLHttpRequest(); - xhr.onreadystatechange = function () { - if (xhr.readyState === 4 && xhr.status === 200) { - var turnServer = JSON.parse(xhr.responseText); - console.log('Got TURN server: ', turnServer); - pcConfig.iceServers.push({ - 'urls': 'turn:' + turnServer.username + '@' + turnServer.turn, - 'credential': turnServer.password - }); - turnReady = true; - } - }; - xhr.open('GET', turnURL, true); - xhr.send(); - } - } - - - - - if (location.hostname !== 'localhost') { - requestTurn( - `${window.location.origin}/corsProxy/${encodeURIComponent("https://computeengineondemand.appspot.com/turn?username=41784574&key=4080218913")}` - ); - } - - const maybeStart = () => { - console.log('>>>>>>> maybeStart() ', isStarted, localStream, isChannelReady); - if (!isStarted && typeof localStream !== 'undefined' && isChannelReady) { - console.log('>>>>>> creating peer connection'); - createPeerConnection(); - pc.addStream(localStream); - isStarted = true; - console.log('isInitiator', isInitiator); - if (isInitiator) { - doCall(); - } - } - }; - - window.onbeforeunload = function () { - sendMessage('bye'); - }; - - ///////////////////////////////////////////////////////// - - const createPeerConnection = () => { - try { - pc = new RTCPeerConnection(null); - pc.onicecandidate = handleIceCandidate; - pc.onaddstream = handleRemoteStreamAdded; - pc.onremovestream = handleRemoteStreamRemoved; - console.log('Created RTCPeerConnnection'); - } catch (e) { - console.log('Failed to create PeerConnection, exception: ' + e.message); - alert('Cannot create RTCPeerConnection object.'); - return; - } - } - - const handleIceCandidate = (event) => { - console.log('icecandidate event: ', event); - if (event.candidate) { - sendMessage({ - type: 'candidate', - label: event.candidate.sdpMLineIndex, - id: event.candidate.sdpMid, - candidate: event.candidate.candidate - }); - } else { - console.log('End of candidates.'); - } - } - - const handleCreateOfferError = (event) => { - console.log('createOffer() error: ', event); - } - - const doCall = () => { - console.log('Sending offer to peer'); - pc.createOffer(setLocalAndSendMessage, handleCreateOfferError); - } - - const doAnswer = () => { - console.log('Sending answer to peer.'); - pc.createAnswer().then( - setLocalAndSendMessage, - onCreateSessionDescriptionError - ); - } - - const setLocalAndSendMessage = (sessionDescription) => { - pc.setLocalDescription(sessionDescription); - console.log('setLocalAndSendMessage sending message', sessionDescription); - sendMessage(sessionDescription); - } - - const onCreateSessionDescriptionError = (error) => { - trace('Failed to create session description: ' + error.toString()); - } - - - - const handleRemoteStreamAdded = (event) => { - console.log('Remote stream added.'); - remoteStream = event.stream; - remoteVideo.srcObject = remoteStream; - handlerUI(); - - }; - - const handleRemoteStreamRemoved = (event) => { - console.log('Remote stream removed. Event: ', event); - } -} - -export function hangup() { - console.log('Hanging up.'); - stop(); - sendMessage('bye'); - if (localStream) { - localStream.getTracks().forEach(track => track.stop()); - } -} - -function stop() { - isStarted = false; - if (pc) { - pc.close(); - } - pc = null; -} - -function handleRemoteHangup() { - console.log('Session terminated.'); - stop(); - isInitiator = false; - if (localStream) { - localStream.getTracks().forEach(track => track.stop()); - } -} - -function sendMessage(message) { - console.log('Client sending message: ', message); - socket.emit('message', message, room); -}; - -export function refreshVideos() { - var localVideo = document.querySelector('#localVideo'); - var remoteVideo = document.querySelector('#remoteVideo'); - if (localVideo) { - localVideo.srcObject = localStream; - } - if (remoteVideo) { - remoteVideo.srcObject = remoteStream; - } - -}
\ No newline at end of file diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index 4512d5c5b..5028e1f8f 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -116,7 +116,9 @@ export type FieldResult<T extends FieldType = FieldType> = Opt<T> | FieldWaiting * If no default value is given, and the returned value is not undefined, it can be safely modified. */ export function DocListCastAsync(field: FieldResult): Promise<Doc[] | undefined>; +// eslint-disable-next-line no-redeclare export function DocListCastAsync(field: FieldResult, defaultValue: Doc[]): Promise<Doc[]>; +// eslint-disable-next-line no-redeclare export function DocListCastAsync(field: FieldResult, defaultValue?: Doc[]) { const list = Cast(field, listSpec(Doc)); return list ? Promise.all(list).then(() => list) : Promise.resolve(defaultValue); @@ -416,6 +418,7 @@ export class Doc extends RefField { } } +// eslint-disable-next-line no-redeclare export namespace Doc { export function SetContainer(doc: Doc, container: Doc) { if (container !== Doc.MyRecentlyClosed) { diff --git a/src/fields/util.ts b/src/fields/util.ts index d7268f31a..72b0ef721 100644 --- a/src/fields/util.ts +++ b/src/fields/util.ts @@ -285,10 +285,10 @@ export function inheritParentAcls(parent: Doc, child: Doc, layoutOnly: boolean) * sets a callback function to be called whenever a value is assigned to the specified field key. * For example, this is used to "publish" documents with titles that start with '@' * @param prop - * @param setter + * @param propSetter */ -export function SetPropSetterCb(prop: string, setter: ((target: any, value: any) => void) | undefined) { - _propSetterCB.set(prop, setter); +export function SetPropSetterCb(prop: string, propSetter: ((target: any, value: any) => void) | undefined) { + _propSetterCB.set(prop, propSetter); } // diff --git a/src/server/ActionUtilities.ts b/src/server/ActionUtilities.ts index 6f5b9272a..520ebb42e 100644 --- a/src/server/ActionUtilities.ts +++ b/src/server/ActionUtilities.ts @@ -16,29 +16,30 @@ export function pathFromRoot(relative?: string) { return path.resolve(projectRoot, relative); } -export async function fileDescriptorFromStream(path: string) { - const logStream = createWriteStream(path); - return new Promise<number>(resolve => logStream.on('open', resolve)); +export async function fileDescriptorFromStream(filePath: string) { + const logStream = createWriteStream(filePath); + return new Promise<number>(resolve => { + logStream.on('open', resolve); + }); } -export const command_line = (command: string, fromDirectory?: string) => { - return new Promise<string>((resolve, reject) => { +export const commandLine = (command: string, fromDirectory?: string) => + new Promise<string>((resolve, reject) => { const options: ExecOptions = {}; if (fromDirectory) { options.cwd = fromDirectory ? path.resolve(projectRoot, fromDirectory) : projectRoot; } exec(command, options, (err, stdout) => (err ? reject(err) : resolve(stdout))); }); -}; -export const read_text_file = (relativePath: string) => { +export const readTextFile = (relativePath: string) => { const target = path.resolve(__dirname, relativePath); return new Promise<string>((resolve, reject) => { readFile(target, (err, data) => (err ? reject(err) : resolve(data.toString()))); }); }; -export const write_text_file = (relativePath: string, contents: any) => { +export const writeTextFile = (relativePath: string, contents: any) => { const target = path.resolve(__dirname, relativePath); return new Promise<void>((resolve, reject) => { writeFile(target, contents, err => (err ? reject(err) : resolve())); @@ -55,39 +56,38 @@ export interface LogData<T> { color?: Color; } +function logHelper(content: string, color: Color | string) { + if (typeof color === 'string') { + console.log(color, content); + } else { + console.log(color(content)); + } +} + let current = Math.ceil(Math.random() * 20); export async function logExecution<T>({ startMessage, endMessage, action, color }: LogData<T>): Promise<T | undefined> { - let result: T | undefined = undefined, - error: Error | null = null; + let result: T | undefined; + let error: Error | null = null; const resolvedColor = color || `\x1b[${31 + (++current % 6)}m%s\x1b[0m`; - log_helper(`${startMessage}...`, resolvedColor); + logHelper(`${startMessage}...`, resolvedColor); try { result = await action(); } catch (e: any) { error = e; } finally { - log_helper(typeof endMessage === 'string' ? endMessage : endMessage({ result, error }), resolvedColor); + logHelper(typeof endMessage === 'string' ? endMessage : endMessage({ result, error }), resolvedColor); } return result; } - -function log_helper(content: string, color: Color | string) { - if (typeof color === 'string') { - console.log(color, content); - } else { - console.log(color(content)); - } -} - export function logPort(listener: string, port: number) { console.log(`${listener} listening on port ${yellow(String(port))}`); } export function msToTime(duration: number) { - const milliseconds = Math.floor((duration % 1000) / 100), - seconds = Math.floor((duration / 1000) % 60), - minutes = Math.floor((duration / (1000 * 60)) % 60), - hours = Math.floor((duration / (1000 * 60 * 60)) % 24); + const milliseconds = Math.floor((duration % 1000) / 100); + const seconds = Math.floor((duration / 1000) % 60); + const minutes = Math.floor((duration / (1000 * 60)) % 60); + const hours = Math.floor((duration / (1000 * 60 * 60)) % 24); const hoursS = hours < 10 ? '0' + hours : hours; const minutesS = minutes < 10 ? '0' + minutes : minutes; @@ -96,21 +96,32 @@ export function msToTime(duration: number) { return hoursS + ':' + minutesS + ':' + secondsS + '.' + milliseconds; } -export const createIfNotExists = async (path: string) => { - if (await new Promise<boolean>(resolve => exists(path, resolve))) { +export const createIfNotExists = async (filePath: string) => { + if ( + await new Promise<boolean>(resolve => { + exists(filePath, resolve); + }) + ) { return true; } - return new Promise<boolean>(resolve => mkdir(path, error => resolve(error === null))); + return new Promise<boolean>(resolve => { + mkdir(filePath, error => resolve(error === null)); + }); }; export async function Prune(rootDirectory: string): Promise<boolean> { // const error = await new Promise<Error>(resolve => rimraf(rootDirectory).then(resolve)); - await new Promise<void>(resolve => rimraf(rootDirectory).then(() => resolve())); + await new Promise<void>(resolve => { + rimraf(rootDirectory).then(() => resolve()); + }); // return error === null; return true; } -export const Destroy = (mediaPath: string) => new Promise<boolean>(resolve => unlink(mediaPath, error => resolve(error === null))); +export const Destroy = (mediaPath: string) => + new Promise<boolean>(resolve => { + unlink(mediaPath, error => resolve(error === null)); + }); export namespace Email { const smtpTransport = nodemailer.createTransport({ @@ -137,9 +148,9 @@ export namespace Email { const failures: DispatchFailure[] = []; await Promise.all( to.map(async recipient => { - let error: Error | null; const resolved = attachments ? ('length' in attachments ? attachments : [attachments]) : undefined; - if ((error = await Email.dispatch({ to: recipient, subject, content, attachments: resolved })) !== null) { + const error = await Email.dispatch({ to: recipient, subject, content, attachments: resolved }); + if (error !== null) { failures.push({ recipient, error, @@ -158,6 +169,8 @@ export namespace Email { text: `Hello ${to.split('@')[0]},\n\n${content}`, attachments, } as MailOptions; - return new Promise<Error | null>(resolve => smtpTransport.sendMail(mailOptions, resolve)); + return new Promise<Error | null>(resolve => { + smtpTransport.sendMail(mailOptions, resolve); + }); } } diff --git a/src/server/ApiManagers/ApiManager.ts b/src/server/ApiManagers/ApiManager.ts index 27e9de065..f55495b2e 100644 --- a/src/server/ApiManagers/ApiManager.ts +++ b/src/server/ApiManagers/ApiManager.ts @@ -1,4 +1,4 @@ -import { RouteInitializer } from "../RouteManager"; +import { RouteInitializer } from '../RouteManager'; export type Registration = (initializer: RouteInitializer) => void; @@ -8,4 +8,4 @@ export default abstract class ApiManager { public register(register: Registration) { this.initialize(register); } -}
\ No newline at end of file +} diff --git a/src/server/ApiManagers/DeleteManager.ts b/src/server/ApiManagers/DeleteManager.ts index 9a9b807ae..9ad334c1b 100644 --- a/src/server/ApiManagers/DeleteManager.ts +++ b/src/server/ApiManagers/DeleteManager.ts @@ -1,12 +1,12 @@ -import ApiManager, { Registration } from './ApiManager'; -import { Method, _permissionDenied } from '../RouteManager'; -import { WebSocket } from '../websocket'; -import { Database } from '../database'; +import { mkdirSync } from 'fs'; import { rimraf } from 'rimraf'; -import { filesDirectory } from '..'; +import { filesDirectory } from '../SocketData'; import { DashUploadUtils } from '../DashUploadUtils'; -import { mkdirSync } from 'fs'; +import { Method } from '../RouteManager'; import RouteSubscriber from '../RouteSubscriber'; +import { Database } from '../database'; +import { WebSocket } from '../websocket'; +import ApiManager, { Registration } from './ApiManager'; export default class DeleteManager extends ApiManager { protected initialize(register: Registration): void { @@ -24,9 +24,11 @@ export default class DeleteManager extends ApiManager { switch (target) { case 'all': all = true; + // eslint-disable-next-line no-fallthrough case 'database': await WebSocket.doDelete(false); if (!all) break; + // eslint-disable-next-line no-fallthrough case 'files': rimraf.sync(filesDirectory); mkdirSync(filesDirectory); diff --git a/src/server/ApiManagers/DownloadManager.ts b/src/server/ApiManagers/DownloadManager.ts index b105c825c..5ee21fb44 100644 --- a/src/server/ApiManagers/DownloadManager.ts +++ b/src/server/ApiManagers/DownloadManager.ts @@ -153,7 +153,7 @@ async function writeHierarchyRecursive(file: Archiver.Archiver, hierarchy: Hiera } } -async function getDocs(id: string) { +async function getDocs(docId: string) { const files = new Set<string>(); const docs: { [id: string]: any } = {}; const fn = (doc: any): string[] => { @@ -209,8 +209,8 @@ async function getDocs(id: string) { } return ids; }; - await Database.Instance.visit([id], fn); - return { id, docs, files }; + await Database.Instance.visit([docId], fn); + return { id: docId, docs, files }; } export default class DownloadManager extends ApiManager { diff --git a/src/server/ApiManagers/MongoStore.js b/src/server/ApiManagers/MongoStore.js index 28515fee4..5d91c2805 100644 --- a/src/server/ApiManagers/MongoStore.js +++ b/src/server/ApiManagers/MongoStore.js @@ -1,10 +1,9 @@ -'use strict'; -var __createBinding = +const __createBinding = (this && this.__createBinding) || (Object.create ? function (o, m, k, k2) { if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); + let desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ('get' in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, @@ -19,25 +18,25 @@ var __createBinding = if (k2 === undefined) k2 = k; o[k2] = m[k]; }); -var __setModuleDefault = +const __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? function (o, v) { Object.defineProperty(o, 'default', { enumerable: true, value: v }); } : function (o, v) { - o['default'] = v; + o.default = v; }); -var __importStar = +const __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k in mod) if (k !== 'default' && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + const result = {}; + if (mod != null) for (const k in mod) if (k !== 'default' && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; -var __importDefault = +const __importDefault = (this && this.__importDefault) || function (mod) { return mod && mod.__esModule ? mod : { default: mod }; @@ -246,7 +245,7 @@ class MongoStore extends session.Store { */ set(sid, session, callback = noop) { (async () => { - var _a; + let _a; try { debug(`MongoStore#set=${sid}`); // Removing the lastModified prop from the session object before update @@ -306,7 +305,7 @@ class MongoStore extends session.Store { } touch(sid, session, callback = noop) { (async () => { - var _a; + let _a; try { debug(`MongoStore#touch=${sid}`); const updateFields = {}; diff --git a/src/server/ApiManagers/SearchManager.ts b/src/server/ApiManagers/SearchManager.ts index 1b1db5809..f43ed6ac9 100644 --- a/src/server/ApiManagers/SearchManager.ts +++ b/src/server/ApiManagers/SearchManager.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-use-before-define */ import { exec } from 'child_process'; import { cyan, green, red, yellow } from 'colors'; import { logExecution } from '../ActionUtilities'; @@ -17,8 +18,10 @@ export class SearchManager extends ApiManager { switch (action) { case 'start': case 'stop': - const status = req.params.action === 'start'; - SolrManager.SetRunning(status); + { + const status = req.params.action === 'start'; + SolrManager.SetRunning(status); + } break; case 'update': await SolrManager.update(); @@ -95,7 +98,7 @@ export namespace SolrManager { if (doc.__type !== 'Doc') { return; } - const fields = doc.fields; + const { fields } = doc; if (!fields) { return; } diff --git a/src/server/ApiManagers/SessionManager.ts b/src/server/ApiManagers/SessionManager.ts index c3139896f..bebe50a62 100644 --- a/src/server/ApiManagers/SessionManager.ts +++ b/src/server/ApiManagers/SessionManager.ts @@ -9,20 +9,18 @@ const permissionError = 'You are not authorized!'; export default class SessionManager extends ApiManager { private secureSubscriber = (root: string, ...params: string[]) => new RouteSubscriber(root).add('session_key', ...params); - private authorizedAction = (handler: SecureHandler) => { - return (core: AuthorizedCore) => { - const { - req: { params }, - res, - } = core; - if (!process.env.MONITORED) { - return res.send('This command only makes sense in the context of a monitored session.'); - } - if (params.session_key !== process.env.session_key) { - return _permissionDenied(res, permissionError); - } - return handler(core); - }; + private authorizedAction = (handler: SecureHandler) => (core: AuthorizedCore) => { + const { + req: { params }, + res, + } = core; + if (!process.env.MONITORED) { + return res.send('This command only makes sense in the context of a monitored session.'); + } + if (params.session_key !== process.env.session_key) { + return _permissionDenied(res, permissionError); + } + return handler(core); }; protected initialize(register: Registration): void { diff --git a/src/server/ApiManagers/UtilManager.ts b/src/server/ApiManagers/UtilManager.ts index e657866ce..8ad421a30 100644 --- a/src/server/ApiManagers/UtilManager.ts +++ b/src/server/ApiManagers/UtilManager.ts @@ -1,6 +1,7 @@ -import ApiManager, { Registration } from "./ApiManager"; -import { Method } from "../RouteManager"; import { exec } from 'child_process'; +import ApiManager, { Registration } from './ApiManager'; +import { Method } from '../RouteManager'; + // import { IBM_Recommender } from "../../client/apis/IBM_Recommender"; // import { Recommender } from "../Recommender"; @@ -8,9 +9,7 @@ import { exec } from 'child_process'; // recommender.testModel(); export default class UtilManager extends ApiManager { - protected initialize(register: Registration): void { - // register({ // method: Method.POST, // subscription: "/IBMAnalysis", @@ -33,26 +32,25 @@ export default class UtilManager extends ApiManager { register({ method: Method.GET, - subscription: "/pull", - secureHandler: async ({ res }) => { - return new Promise<void>(resolve => { + subscription: '/pull', + secureHandler: async ({ res }) => + new Promise<void>(resolve => { exec('"C:\\Program Files\\Git\\git-bash.exe" -c "git pull"', err => { if (err) { res.send(err.message); return; } - res.redirect("/"); + res.redirect('/'); resolve(); }); - }); - } + }), }); register({ method: Method.GET, - subscription: "/version", - secureHandler: ({ res }) => { - return new Promise<void>(resolve => { + subscription: '/version', + secureHandler: ({ res }) => + new Promise<void>(resolve => { exec('"C:\\Program Files\\Git\\bin\\git.exe" rev-parse HEAD', (err, stdout) => { if (err) { res.send(err.message); @@ -61,10 +59,7 @@ export default class UtilManager extends ApiManager { res.send(stdout); }); resolve(); - }); - } + }), }); - } - -}
\ No newline at end of file +} diff --git a/src/server/DashSession/Session/agents/applied_session_agent.ts b/src/server/DashSession/Session/agents/applied_session_agent.ts index 2037e93e5..c42ba95cc 100644 --- a/src/server/DashSession/Session/agents/applied_session_agent.ts +++ b/src/server/DashSession/Session/agents/applied_session_agent.ts @@ -1,13 +1,13 @@ -import * as _cluster from "cluster"; -import { Monitor } from "./monitor"; -import { ServerWorker } from "./server_worker"; +import * as _cluster from 'cluster'; +import { Monitor } from './monitor'; +import { ServerWorker } from './server_worker'; + const cluster = _cluster as any; const isMaster = cluster.isPrimary; export type ExitHandler = (reason: Error | boolean) => void | Promise<void>; export abstract class AppliedSessionAgent { - // the following two methods allow the developer to create a custom // session and use the built in customization options for each thread protected abstract initializeMonitor(monitor: Monitor): Promise<string>; @@ -18,15 +18,15 @@ export abstract class AppliedSessionAgent { public killSession = (reason: string, graceful = true, errorCode = 0) => { const target = cluster.default.isPrimary ? this.sessionMonitor : this.serverWorker; target.killSession(reason, graceful, errorCode); - } + }; private sessionMonitorRef: Monitor | undefined; public get sessionMonitor(): Monitor { if (!cluster.default.isPrimary) { - this.serverWorker.emit("kill", { + this.serverWorker.emit('kill', { graceful: false, - reason: "Cannot access the session monitor directly from the server worker thread.", - errorCode: 1 + reason: 'Cannot access the session monitor directly from the server worker thread.', + errorCode: 1, }); throw new Error(); } @@ -36,7 +36,7 @@ export abstract class AppliedSessionAgent { private serverWorkerRef: ServerWorker | undefined; public get serverWorker(): ServerWorker { if (isMaster) { - throw new Error("Cannot access the server worker directly from the session monitor thread"); + throw new Error('Cannot access the server worker directly from the session monitor thread'); } return this.serverWorkerRef!; } @@ -52,8 +52,7 @@ export abstract class AppliedSessionAgent { this.serverWorkerRef = await this.initializeServerWorker(); } } else { - throw new Error("Cannot launch a session thread more than once per process."); + throw new Error('Cannot launch a session thread more than once per process.'); } } - -}
\ No newline at end of file +} diff --git a/src/server/DashSession/Session/agents/monitor.ts b/src/server/DashSession/Session/agents/monitor.ts index a6fde4356..6cdad46c2 100644 --- a/src/server/DashSession/Session/agents/monitor.ts +++ b/src/server/DashSession/Session/agents/monitor.ts @@ -1,21 +1,19 @@ -import { ExitHandler } from './applied_session_agent'; -import { Configuration, configurationSchema, defaultConfig, Identifiers, colorMapping } from '../utilities/session_config'; -import Repl, { ReplAction } from '../utilities/repl'; +import { ExecOptions, exec } from 'child_process'; import * as _cluster from 'cluster'; import { Worker } from 'cluster'; -import { manage, MessageHandler, ErrorLike } from './promisified_ipc_manager'; -import { red, cyan, white, yellow, blue } from 'colors'; -import { exec, ExecOptions } from 'child_process'; -import { validate, ValidationError } from 'jsonschema'; -import { Utilities } from '../utilities/utilities'; +import { blue, cyan, red, white, yellow } from 'colors'; import { readFileSync } from 'fs'; +import { ValidationError, validate } from 'jsonschema'; +import Repl, { ReplAction } from '../utilities/repl'; +import { Configuration, Identifiers, colorMapping, configurationSchema, defaultConfig } from '../utilities/session_config'; +import { Utilities } from '../utilities/utilities'; +import { ExitHandler } from './applied_session_agent'; import IPCMessageReceiver from './process_message_router'; +import { ErrorLike, MessageHandler, manage } from './promisified_ipc_manager'; import { ServerWorker } from './server_worker'; + const cluster = _cluster as any; -const isWorker = cluster.isWorker; -const setupMaster = cluster.setupPrimary; -const on = cluster.on; -const fork = cluster.fork; +const { isWorker, setupMaster, on, fork } = cluster; /** * Validates and reads the configuration file, accordingly builds a child process factory @@ -41,9 +39,8 @@ export class Monitor extends IPCMessageReceiver { } else if (++Monitor.count > 1) { console.error(red('cannot create more than one monitor.')); process.exit(1); - } else { - return new Monitor(); } + return new Monitor(); } private constructor() { @@ -128,25 +125,25 @@ export class Monitor extends IPCMessageReceiver { this.repl.registerCommand(basename, argPatterns, action); }; - public exec = (command: string, options?: ExecOptions) => { - return new Promise<void>(resolve => { + public exec = (command: string, options?: ExecOptions) => + new Promise<void>(resolve => { exec(command, { ...options, encoding: 'utf8' }, (error, stdout, stderr) => { if (error) { this.execLog(red(`unable to execute ${white(command)}`)); error.message.split('\n').forEach(line => line.length && this.execLog(red(`(error) ${line}`))); } else { - let outLines: string[], errorLines: string[]; - if ((outLines = stdout.split('\n').filter(line => line.length)).length) { + const outLines = stdout.split('\n').filter(line => line.length); + if (outLines.length) { outLines.forEach(line => line.length && this.execLog(cyan(`(stdout) ${line}`))); } - if ((errorLines = stderr.split('\n').filter(line => line.length)).length) { + const errorLines = stderr.split('\n').filter(line => line.length); + if (errorLines.length) { errorLines.forEach(line => line.length && this.execLog(yellow(`(stderr) ${line}`))); } } resolve(); }); }); - }; /** * Generates a blue UTC string associated with the time @@ -226,12 +223,10 @@ export class Monitor extends IPCMessageReceiver { const newPollingIntervalSeconds = Math.floor(Number(args[1])); if (newPollingIntervalSeconds < 0) { this.mainLog(red('the polling interval must be a non-negative integer')); - } else { - if (newPollingIntervalSeconds !== this.config.polling.intervalSeconds) { - this.config.polling.intervalSeconds = newPollingIntervalSeconds; - if (args[2] === 'true') { - Monitor.IPCManager.emit('updatePollingInterval', { newPollingIntervalSeconds }); - } + } else if (newPollingIntervalSeconds !== this.config.polling.intervalSeconds) { + this.config.polling.intervalSeconds = newPollingIntervalSeconds; + if (args[2] === 'true') { + Monitor.IPCManager.emit('updatePollingInterval', { newPollingIntervalSeconds }); } } }); @@ -297,6 +292,7 @@ export class Monitor extends IPCMessageReceiver { }; } +// eslint-disable-next-line no-redeclare export namespace Monitor { export enum IntrinsicEvents { KeyGenerated = 'key_generated', diff --git a/src/server/DashSession/Session/agents/process_message_router.ts b/src/server/DashSession/Session/agents/process_message_router.ts index 0745ea455..3e2b7d8d0 100644 --- a/src/server/DashSession/Session/agents/process_message_router.ts +++ b/src/server/DashSession/Session/agents/process_message_router.ts @@ -1,7 +1,6 @@ -import { MessageHandler, PromisifiedIPCManager, HandlerMap } from "./promisified_ipc_manager"; +import { MessageHandler, PromisifiedIPCManager, HandlerMap } from './promisified_ipc_manager'; export default abstract class IPCMessageReceiver { - protected static IPCManager: PromisifiedIPCManager; protected handlers: HandlerMap = {}; @@ -18,7 +17,7 @@ export default abstract class IPCMessageReceiver { } else { handlers.push(handler); } - } + }; /** * Unregister a given listener at this message. @@ -31,11 +30,10 @@ export default abstract class IPCMessageReceiver { handlers.splice(index, 1); } } - } + }; - /** + /** * Unregister all listeners at this message. */ public clearMessageListeners = (...names: string[]) => names.map(name => delete this.handlers[name]); - -}
\ No newline at end of file +} diff --git a/src/server/DashSession/Session/agents/promisified_ipc_manager.ts b/src/server/DashSession/Session/agents/promisified_ipc_manager.ts index 76e218977..99b4d4de3 100644 --- a/src/server/DashSession/Session/agents/promisified_ipc_manager.ts +++ b/src/server/DashSession/Session/agents/promisified_ipc_manager.ts @@ -1,13 +1,14 @@ -import { Utilities } from '../utilities/utilities'; import { ChildProcess } from 'child_process'; +import { Utilities } from '../utilities/utilities'; /** - * Convenience constructor - * @param target the process / worker to which to attach the specialized listeners + * Specifies a general message format for this API */ -export function manage(target: IPCTarget, handlers?: HandlerMap) { - return new PromisifiedIPCManager(target, handlers); -} +export type Message<T = any> = { + name: string; + args?: T; +}; +export type MessageHandler<T = any> = (args: T) => any | Promise<any>; /** * Captures the logic to execute upon receiving a message @@ -22,15 +23,10 @@ export type HandlerMap = { [name: string]: MessageHandler[] }; */ export type IPCTarget = NodeJS.Process | ChildProcess; -/** - * Specifies a general message format for this API - */ -export type Message<T = any> = { - name: string; - args?: T; -}; -export type MessageHandler<T = any> = (args: T) => any | Promise<any>; - +interface Metadata { + isResponse: boolean; + id: string; +} /** * When a message is emitted, it is embedded with private metadata * to facilitate the resolution of promises, etc. @@ -38,10 +34,6 @@ export type MessageHandler<T = any> = (args: T) => any | Promise<any>; interface InternalMessage extends Message { metadata: Metadata; } -interface Metadata { - isResponse: boolean; - id: string; -} /** * Allows for the transmission of the error's key features over IPC. @@ -95,7 +87,7 @@ export class PromisifiedIPCManager { } return new Promise<Response<T>>(resolve => { const messageId = Utilities.guid(); - type InternalMessageHandler = (message: any /* MessageListener*/) => any | Promise<any>; + type InternalMessageHandler = (message: any /* MessageListener */) => any | Promise<any>; const responseHandler: InternalMessageHandler = ({ metadata: { id, isResponse }, args }) => { if (isResponse && id === messageId) { this.target.removeListener('message', responseHandler); @@ -118,8 +110,8 @@ export class PromisifiedIPCManager { * completion response for each of the pending messages, allowing their * promises in the caller to resolve. */ - public destroy = () => { - return new Promise<void>(async resolve => { + public destroy = () => + new Promise<void>(async resolve => { if (this.callerIsTarget) { this.destroyHelper(); } else { @@ -127,7 +119,6 @@ export class PromisifiedIPCManager { } resolve(); }); - }; /** * Dispatches the dummy responses and sets the isDestroyed flag to true. @@ -168,12 +159,20 @@ export class PromisifiedIPCManager { error = e; } if (!this.isDestroyed && this.target.send) { - const metadata = { id, isResponse: true }; + const metadataRes = { id, isResponse: true }; const response: Response = { results, error }; - const message = { name, args: response, metadata }; + const messageRes = { name, args: response, metadata: metadataRes }; delete this.pendingMessages[id]; - this.target.send(message); + this.target.send(messageRes); } } }; } + +/** + * Convenience constructor + * @param target the process / worker to which to attach the specialized listeners + */ +export function manage(target: IPCTarget, handlers?: HandlerMap) { + return new PromisifiedIPCManager(target, handlers); +} diff --git a/src/server/DashSession/Session/agents/server_worker.ts b/src/server/DashSession/Session/agents/server_worker.ts index d8b3ee80b..85e1b31d6 100644 --- a/src/server/DashSession/Session/agents/server_worker.ts +++ b/src/server/DashSession/Session/agents/server_worker.ts @@ -1,10 +1,10 @@ -import cluster from "cluster"; -import { green, red, white, yellow } from "colors"; -import { get } from "request-promise"; -import { ExitHandler } from "./applied_session_agent"; -import { Monitor } from "./monitor"; -import IPCMessageReceiver from "./process_message_router"; -import { ErrorLike, manage } from "./promisified_ipc_manager"; +import cluster from 'cluster'; +import { green, red, white, yellow } from 'colors'; +import { get } from 'request-promise'; +import { ExitHandler } from './applied_session_agent'; +import { Monitor } from './monitor'; +import IPCMessageReceiver from './process_message_router'; +import { ErrorLike, manage } from './promisified_ipc_manager'; /** * Effectively, each worker repairs the connection to the server by reintroducing a consistent state @@ -23,18 +23,17 @@ export class ServerWorker extends IPCMessageReceiver { private isInitialized = false; public static Create(work: Function) { if (cluster.isPrimary) { - console.error(red("cannot create a worker on the monitor process.")); + console.error(red('cannot create a worker on the monitor process.')); process.exit(1); } else if (++ServerWorker.count > 1) { - ServerWorker.IPCManager.emit("kill", { - reason: "cannot create more than one worker on a given worker process.", + ServerWorker.IPCManager.emit('kill', { + reason: 'cannot create more than one worker on a given worker process.', graceful: false, - errorCode: 1 + errorCode: 1, }); process.exit(1); - } else { - return new ServerWorker(work); } + return new ServerWorker(work); } /** @@ -48,7 +47,7 @@ export class ServerWorker extends IPCMessageReceiver { * server worker (child process). This will also kill * this process (child process). */ - public killSession = (reason: string, graceful = true, errorCode = 0) => this.emit<never>("kill", { reason, graceful, errorCode }); + public killSession = (reason: string, graceful = true, errorCode = 0) => this.emit<never>('kill', { reason, graceful, errorCode }); /** * A convenience wrapper to tell the session monitor (parent process) @@ -60,7 +59,7 @@ export class ServerWorker extends IPCMessageReceiver { super(); this.configureInternalHandlers(); ServerWorker.IPCManager = manage(process, this.handlers); - this.lifecycleNotification(green(`initializing process... ${white(`[${process.execPath} ${process.execArgv.join(" ")}]`)}`)); + this.lifecycleNotification(green(`initializing process... ${white(`[${process.execPath} ${process.execArgv.join(' ')}]`)}`)); const { pollingRoute, serverPort, pollingIntervalSeconds, pollingFailureTolerance } = process.env; this.serverPort = Number(serverPort); @@ -78,8 +77,10 @@ export class ServerWorker extends IPCMessageReceiver { */ protected configureInternalHandlers = () => { // updates the local values of variables to the those sent from master - this.on("updatePollingInterval", ({ newPollingIntervalSeconds }) => this.pollingIntervalSeconds = newPollingIntervalSeconds); - this.on("manualExit", async ({ isSessionEnd }) => { + this.on('updatePollingInterval', ({ newPollingIntervalSeconds }) => { + this.pollingIntervalSeconds = newPollingIntervalSeconds; + }); + this.on('manualExit', async ({ isSessionEnd }) => { await ServerWorker.IPCManager.destroy(); await this.executeExitHandlers(isSessionEnd); process.exit(0); @@ -91,7 +92,7 @@ export class ServerWorker extends IPCMessageReceiver { const appropriateError = reason instanceof Error ? reason : new Error(`unhandled rejection: ${reason}`); this.proactiveUnplannedExit(appropriateError); }); - } + }; /** * Execute the list of functions registered to be called @@ -102,7 +103,7 @@ export class ServerWorker extends IPCMessageReceiver { /** * Notify master thread (which will log update in the console) of initialization via IPC. */ - public lifecycleNotification = (event: string) => this.emit("lifecycle", { event }); + public lifecycleNotification = (event: string) => this.emit('lifecycle', { event }); /** * Called whenever the process has a reason to terminate, either through an uncaught exception @@ -120,11 +121,11 @@ export class ServerWorker extends IPCMessageReceiver { this.lifecycleNotification(red(error.message)); await ServerWorker.IPCManager.destroy(); process.exit(1); - } + }; /** * This monitors the health of the server by submitting a get request to whatever port / route specified - * by the configuration every n seconds, where n is also given by the configuration. + * by the configuration every n seconds, where n is also given by the configuration. */ private pollServer = async (): Promise<void> => { await new Promise<void>(resolve => { @@ -156,6 +157,5 @@ export class ServerWorker extends IPCMessageReceiver { }); // controlled, asynchronous infinite recursion achieves a persistent poll that does not submit a new request until the previous has completed this.pollServer(); - } - + }; } diff --git a/src/server/DashSession/Session/utilities/repl.ts b/src/server/DashSession/Session/utilities/repl.ts index 643141286..5d9f15e4c 100644 --- a/src/server/DashSession/Session/utilities/repl.ts +++ b/src/server/DashSession/Session/utilities/repl.ts @@ -1,5 +1,5 @@ -import { createInterface, Interface } from "readline"; -import { red, green, white } from "colors"; +import { createInterface, Interface } from 'readline'; +import { red, green, white } from 'colors'; export interface Configuration { identifier: () => string | string; @@ -32,76 +32,82 @@ export default class Repl { this.interface = createInterface(process.stdin, process.stdout).on('line', this.considerInput); } - private resolvedIdentifier = () => typeof this.identifier === "string" ? this.identifier : this.identifier(); + private resolvedIdentifier = () => (typeof this.identifier === 'string' ? this.identifier : this.identifier()); private usage = (command: string, validCommand: boolean) => { if (validCommand) { const formatted = white(command); - const patterns = green(this.commandMap.get(command)!.map(({ argPatterns }) => `${formatted} ${argPatterns.join(" ")}`).join('\n')); + const patterns = green( + this.commandMap + .get(command)! + .map(({ argPatterns }) => `${formatted} ${argPatterns.join(' ')}`) + .join('\n') + ); return `${this.resolvedIdentifier()}\nthe given arguments do not match any registered patterns for ${formatted}\nthe list of valid argument patterns is given by:\n${patterns}`; - } else { - const resolved = this.keys; - if (resolved) { - return resolved; - } - const members: string[] = []; - const keys = this.commandMap.keys(); - let next: IteratorResult<string>; - while (!(next = keys.next()).done) { - members.push(next.value); - } - return `${this.resolvedIdentifier()} commands: { ${members.sort().join(", ")} }`; } - } + const resolved = this.keys; + if (resolved) { + return resolved; + } + const members: string[] = []; + const keys = this.commandMap.keys(); + let next: IteratorResult<string>; + // eslint-disable-next-line no-cond-assign + while (!(next = keys.next()).done) { + members.push(next.value); + } + return `${this.resolvedIdentifier()} commands: { ${members.sort().join(', ')} }`; + }; private success = (command: string) => `${this.resolvedIdentifier()} completed local execution of ${white(command)}`; public registerCommand = (basename: string, argPatterns: (RegExp | string)[], action: ReplAction) => { const existing = this.commandMap.get(basename); - const converted = argPatterns.map(input => input instanceof RegExp ? input : new RegExp(input)); + const converted = argPatterns.map(input => (input instanceof RegExp ? input : new RegExp(input))); const registration = { argPatterns: converted, action }; if (existing) { existing.push(registration); } else { this.commandMap.set(basename, [registration]); } - } + }; private invalid = (command: string, validCommand: boolean) => { - console.log(red(typeof this.onInvalid === "string" ? this.onInvalid : this.onInvalid(command, validCommand))); + console.log(red(typeof this.onInvalid === 'string' ? this.onInvalid : this.onInvalid(command, validCommand))); this.busy = false; - } + }; private valid = (command: string) => { - console.log(green(typeof this.onValid === "string" ? this.onValid : this.onValid(command))); + console.log(green(typeof this.onValid === 'string' ? this.onValid : this.onValid(command))); this.busy = false; - } + }; - private considerInput = async (line: string) => { + private considerInput = async (lineIn: string) => { if (this.busy) { - console.log(red("Busy")); + console.log(red('Busy')); return; } this.busy = true; - line = line.trim(); + let line = lineIn.trim(); if (this.isCaseSensitive) { line = line.toLowerCase(); } const [command, ...args] = line.split(/\s+/g); if (!command) { - return this.invalid(command, false); + this.invalid(command, false); + return; } const registered = this.commandMap.get(command); if (registered) { const { length } = args; const candidates = registered.filter(({ argPatterns: { length: count } }) => count === length); - for (const { argPatterns, action } of candidates) { + candidates.forEach(({ argPatterns, action }: { argPatterns: any; action: any }) => { const parsed: string[] = []; let matched = true; if (length) { for (let i = 0; i < length; i++) { - let matches: RegExpExecArray | null; - if ((matches = argPatterns[i].exec(args[i])) === null) { + const matches = argPatterns[i].exec(args[i]); + if (matches === null) { matched = false; break; } @@ -110,19 +116,17 @@ export default class Repl { } if (!length || matched) { const result = action(parsed); - const resolve = () => this.valid(`${command} ${parsed.join(" ")}`); + const resolve = () => this.valid(`${command} ${parsed.join(' ')}`); if (result instanceof Promise) { result.then(resolve); } else { resolve(); } - return; } - } + }); this.invalid(command, true); } else { this.invalid(command, false); } - } - -}
\ No newline at end of file + }; +} diff --git a/src/server/DashSession/Session/utilities/session_config.ts b/src/server/DashSession/Session/utilities/session_config.ts index 266759929..b42c1a3c7 100644 --- a/src/server/DashSession/Session/utilities/session_config.ts +++ b/src/server/DashSession/Session/utilities/session_config.ts @@ -1,85 +1,85 @@ -import { Schema } from "jsonschema"; -import { yellow, red, cyan, green, blue, magenta, Color, grey, gray, white, black } from "colors"; +import { Schema } from 'jsonschema'; +import { yellow, red, cyan, green, blue, magenta, Color, grey, gray, white, black } from 'colors'; const colorPattern = /black|red|green|yellow|blue|magenta|cyan|white|gray|grey/; const identifierProperties: Schema = { - type: "object", + type: 'object', properties: { text: { - type: "string", - minLength: 1 + type: 'string', + minLength: 1, }, color: { - type: "string", - pattern: colorPattern - } - } + type: 'string', + pattern: colorPattern, + }, + }, }; const portProperties: Schema = { - type: "number", + type: 'number', minimum: 443, - maximum: 65535 + maximum: 65535, }; export const configurationSchema: Schema = { - id: "/configuration", - type: "object", + id: '/configuration', + type: 'object', properties: { - showServerOutput: { type: "boolean" }, + showServerOutput: { type: 'boolean' }, ports: { - type: "object", + type: 'object', properties: { server: portProperties, - socket: portProperties + socket: portProperties, }, - required: ["server"], - additionalProperties: true + required: ['server'], + additionalProperties: true, }, identifiers: { - type: "object", + type: 'object', properties: { master: identifierProperties, worker: identifierProperties, - exec: identifierProperties - } + exec: identifierProperties, + }, }, polling: { - type: "object", + type: 'object', additionalProperties: false, properties: { intervalSeconds: { - type: "number", + type: 'number', minimum: 1, - maximum: 86400 + maximum: 86400, }, route: { - type: "string", - pattern: /\/[a-zA-Z]*/g + type: 'string', + pattern: /\/[a-zA-Z]*/g, }, failureTolerance: { - type: "number", + type: 'number', minimum: 0, - } - } + }, + }, }, - } + }, }; -type ColorLabel = "yellow" | "red" | "cyan" | "green" | "blue" | "magenta" | "grey" | "gray" | "white" | "black"; +type ColorLabel = 'yellow' | 'red' | 'cyan' | 'green' | 'blue' | 'magenta' | 'grey' | 'gray' | 'white' | 'black'; export const colorMapping: Map<ColorLabel, Color> = new Map([ - ["yellow", yellow], - ["red", red], - ["cyan", cyan], - ["green", green], - ["blue", blue], - ["magenta", magenta], - ["grey", grey], - ["gray", gray], - ["white", white], - ["black", black] + ['yellow', yellow], + ['red', red], + ['cyan', cyan], + ['green', green], + ['blue', blue], + ['magenta', magenta], + ['grey', grey], + ['gray', gray], + ['white', white], + ['black', black], ]); interface Identifier { @@ -108,22 +108,22 @@ export const defaultConfig: Configuration = { showServerOutput: false, identifiers: { master: { - text: "__monitor__", - color: "yellow" + text: '__monitor__', + color: 'yellow', }, worker: { - text: "__server__", - color: "magenta" + text: '__server__', + color: 'magenta', }, exec: { - text: "__exec__", - color: "green" - } + text: '__exec__', + color: 'green', + }, }, ports: { server: 1050 }, polling: { - route: "/", + route: '/', intervalSeconds: 30, - failureTolerance: 0 - } -};
\ No newline at end of file + failureTolerance: 0, + }, +}; diff --git a/src/server/DashSession/Session/utilities/utilities.ts b/src/server/DashSession/Session/utilities/utilities.ts index eb8de9d7e..a2ba29c67 100644 --- a/src/server/DashSession/Session/utilities/utilities.ts +++ b/src/server/DashSession/Session/utilities/utilities.ts @@ -1,31 +1,16 @@ -import { v4 } from "uuid"; +import { v4 } from 'uuid'; export namespace Utilities { - export function guid() { return v4(); } - /** - * At any arbitrary layer of nesting within the configuration objects, any single value that - * is not specified by the configuration is given the default counterpart. If, within an object, - * one peer is given by configuration and two are not, the one is preserved while the two are given - * the default value. - * @returns the composition of all of the assigned objects, much like Object.assign(), but with more - * granularity in the overwriting of nested objects - */ - export function preciseAssign(target: any, ...sources: any[]): any { - for (const source of sources) { - preciseAssignHelper(target, source); - } - return target; - } - export function preciseAssignHelper(target: any, source: any) { - Array.from(new Set([...Object.keys(target), ...Object.keys(source)])).map(property => { - let targetValue: any, sourceValue: any; - if (sourceValue = source[property]) { - if (typeof sourceValue === "object" && typeof (targetValue = target[property]) === "object") { + Array.from(new Set([...Object.keys(target), ...Object.keys(source)])).forEach(property => { + const targetValue = target[property]; + const sourceValue = source[property]; + if (sourceValue) { + if (typeof sourceValue === 'object' && typeof targetValue === 'object') { preciseAssignHelper(targetValue, sourceValue); } else { target[property] = sourceValue; @@ -34,4 +19,18 @@ export namespace Utilities { }); } -}
\ No newline at end of file + /** + * At any arbitrary layer of nesting within the configuration objects, any single value that + * is not specified by the configuration is given the default counterpart. If, within an object, + * one peer is given by configuration and two are not, the one is preserved while the two are given + * the default value. + * @returns the composition of all of the assigned objects, much like Object.assign(), but with more + * granularity in the overwriting of nested objects + */ + export function preciseAssign(target: any, ...sources: any[]): any { + sources.forEach(source => { + preciseAssignHelper(target, source); + }); + return target; + } +} diff --git a/src/server/DashStats.ts b/src/server/DashStats.ts index 485ab9f99..808d2c6f2 100644 --- a/src/server/DashStats.ts +++ b/src/server/DashStats.ts @@ -1,7 +1,6 @@ import { cyan, magenta } from 'colors'; import { Response } from 'express'; import * as fs from 'fs'; -import SocketIO from 'socket.io'; import { socketMap, timeMap, userOperations } from './SocketData'; /** @@ -242,7 +241,7 @@ export namespace DashStats { * @param username the username in the format of "username@domain.com logged in" * @param socket the websocket associated with the current connection */ - export function logUserLogin(username: string | undefined, socket: SocketIO.Socket) { + export function logUserLogin(username: string | undefined) { if (!(username === undefined)) { const currentDate = new Date(); console.log(magenta(`User ${username.split(' ')[0]} logged in at: ${currentDate.toISOString()}`)); @@ -267,7 +266,7 @@ export namespace DashStats { * @param username the username in the format of "username@domain.com logged in" * @param socket the websocket associated with the current connection. */ - export function logUserLogout(username: string | undefined, socket: SocketIO.Socket) { + export function logUserLogout(username: string | undefined) { if (!(username === undefined)) { const currentDate = new Date(); diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts index 3d8325da9..08cea1de5 100644 --- a/src/server/DashUploadUtils.ts +++ b/src/server/DashUploadUtils.ts @@ -186,8 +186,8 @@ export namespace DashUploadUtils { const image = await request.get(source, { encoding: null }); const { /* data, */ error } = await new Promise<{ data: any; error: any }>(resolve => { // eslint-disable-next-line no-new - new ExifImage({ image }, (error, data) => { - const reason = (error as any)?.code; + new ExifImage({ image }, (exifError, data) => { + const reason = (exifError as any)?.code; resolve({ data, error: reason }); }); }); @@ -506,8 +506,8 @@ export namespace DashUploadUtils { if (fExists(fileKey, Directory.pdfs) && fExists(textFilename, Directory.text)) { fs.unlink(file.filepath, () => {}); return new Promise<Upload.FileResponse>(res => { - const textFilename = `${fileKey.substring(0, fileKey.length - 4)}.txt`; - const readStream = createReadStream(serverPathToFile(Directory.text, textFilename)); + const pdfTextFilename = `${fileKey.substring(0, fileKey.length - 4)}.txt`; + const readStream = createReadStream(serverPathToFile(Directory.text, pdfTextFilename)); let rawText = ''; readStream .on('data', chunk => { diff --git a/src/server/GarbageCollector.ts b/src/server/GarbageCollector.ts index 423c719c2..041f65592 100644 --- a/src/server/GarbageCollector.ts +++ b/src/server/GarbageCollector.ts @@ -1,11 +1,15 @@ +/* eslint-disable no-await-in-loop */ +/* eslint-disable no-continue */ +/* eslint-disable no-cond-assign */ +/* eslint-disable no-restricted-syntax */ import * as fs from 'fs'; import * as path from 'path'; import { Database } from './database'; import { Search } from './Search'; - function addDoc(doc: any, ids: string[], files: { [name: string]: string[] }) { for (const key in doc) { + // eslint-disable-next-line no-prototype-builtins if (!doc.hasOwnProperty(key)) { continue; } @@ -13,22 +17,22 @@ function addDoc(doc: any, ids: string[], files: { [name: string]: string[] }) { if (field === undefined || field === null) { continue; } - if (field.__type === "proxy" || field.__type === "prefetch_proxy") { + if (field.__type === 'proxy' || field.__type === 'prefetch_proxy') { ids.push(field.fieldId); - } else if (field.__type === "list") { + } else if (field.__type === 'list') { addDoc(field.fields, ids, files); - } else if (typeof field === "string") { - const re = /"(?:dataD|d)ocumentId"\s*:\s*"([\w\-]*)"/g; + } else if (typeof field === 'string') { + const re = /"(?:dataD|d)ocumentId"\s*:\s*"([\w-]*)"/g; let match: string[] | null; while ((match = re.exec(field)) !== null) { ids.push(match[1]); } - } else if (field.__type === "RichTextField") { + } else if (field.__type === 'RichTextField') { const re = /"href"\s*:\s*"(.*?)"/g; let match: string[] | null; while ((match = re.exec(field.Data)) !== null) { const urlString = match[1]; - const split = new URL(urlString).pathname.split("doc/"); + const split = new URL(urlString).pathname.split('doc/'); if (split.length > 1) { ids.push(split[split.length - 1]); } @@ -36,7 +40,7 @@ function addDoc(doc: any, ids: string[], files: { [name: string]: string[] }) { const re2 = /"src"\s*:\s*"(.*?)"/g; while ((match = re2.exec(field.Data)) !== null) { const urlString = match[1]; - const pathname = new URL(urlString).pathname; + const { pathname } = new URL(urlString); const ext = path.extname(pathname); const fileName = path.basename(pathname, ext); let exts = files[fileName]; @@ -45,9 +49,9 @@ function addDoc(doc: any, ids: string[], files: { [name: string]: string[] }) { } exts.push(ext); } - } else if (["audio", "image", "video", "pdf", "web", "map"].includes(field.__type)) { + } else if (['audio', 'image', 'video', 'pdf', 'web', 'map'].includes(field.__type)) { const url = new URL(field.url); - const pathname = url.pathname; + const { pathname } = url; const ext = path.extname(pathname); const fileName = path.basename(pathname, ext); let exts = files[fileName]; @@ -60,12 +64,12 @@ function addDoc(doc: any, ids: string[], files: { [name: string]: string[] }) { } async function GarbageCollect(full: boolean = true) { - console.log("start GC"); + console.log('start GC'); const start = Date.now(); // await new Promise(res => setTimeout(res, 3000)); const cursor = await Database.Instance.query({}, { userDocumentId: 1 }, 'users'); const users = await cursor.toArray(); - const ids: string[] = [...users.map((user:any) => user.userDocumentId), ...users.map((user:any) => user.sharingDocumentId), ...users.map((user:any) => user.linkDatabaseId)]; + const ids: string[] = [...users.map((user: any) => user.userDocumentId), ...users.map((user: any) => user.sharingDocumentId), ...users.map((user: any) => user.linkDatabaseId)]; const visited = new Set<string>(); const files: { [name: string]: string[] } = {}; @@ -76,9 +80,11 @@ async function GarbageCollect(full: boolean = true) { if (!fetchIds.length) { continue; } - const docs = await new Promise<{ [key: string]: any }[]>(res => Database.Instance.getDocuments(fetchIds, res)); + const docs = await new Promise<{ [key: string]: any }[]>(res => { + Database.Instance.getDocuments(fetchIds, res); + }); for (const doc of docs) { - const id = doc.id; + const { id } = doc; if (doc === undefined) { console.log(`Couldn't find field with Id ${id}`); continue; @@ -95,19 +101,27 @@ async function GarbageCollect(full: boolean = true) { const notToDelete = Array.from(visited); const toDeleteCursor = await Database.Instance.query({ _id: { $nin: notToDelete } }, { _id: 1 }); - const toDelete: string[] = (await toDeleteCursor.toArray()).map((doc:any) => doc._id); + const toDelete: string[] = (await toDeleteCursor.toArray()).map((doc: any) => doc._id); toDeleteCursor.close(); if (!full) { - await Database.Instance.updateMany({ _id: { $nin: notToDelete } }, { $set: { "deleted": true } }); - await Database.Instance.updateMany({ _id: { $in: notToDelete } }, { $unset: { "deleted": true } }); - console.log(await Search.updateDocuments( - notToDelete.map<any>(id => ({ - id, deleted: { set: null } - })) - .concat(toDelete.map(id => ({ - id, deleted: { set: true } - }))))); - console.log("Done with partial GC"); + await Database.Instance.updateMany({ _id: { $nin: notToDelete } }, { $set: { deleted: true } }); + await Database.Instance.updateMany({ _id: { $in: notToDelete } }, { $unset: { deleted: true } }); + console.log( + await Search.updateDocuments( + notToDelete + .map<any>(id => ({ + id, + deleted: { set: null }, + })) + .concat( + toDelete.map(id => ({ + id, + deleted: { set: true }, + })) + ) + ) + ); + console.log('Done with partial GC'); console.log(`Took ${(Date.now() - start) / 1000} seconds`); } else { let i = 0; @@ -123,15 +137,15 @@ async function GarbageCollect(full: boolean = true) { console.log(`${deleted} documents deleted`); await Search.deleteDocuments(toDelete); - console.log("Cleared search documents"); + console.log('Cleared search documents'); - const folder = "./src/server/public/files/"; + const folder = './src/server/public/files/'; fs.readdir(folder, (_, fileList) => { const filesToDelete = fileList.filter(file => { const ext = path.extname(file); let base = path.basename(file, ext); const existsInDb = (base in files || (base = base.substring(0, base.length - 2)) in files) && files[base].includes(ext); - return file !== ".gitignore" && !existsInDb; + return file !== '.gitignore' && !existsInDb; }); console.log(`Deleting ${filesToDelete.length} files`); filesToDelete.forEach(file => { diff --git a/src/server/MemoryDatabase.ts b/src/server/MemoryDatabase.ts index b74332bf5..1432d91c4 100644 --- a/src/server/MemoryDatabase.ts +++ b/src/server/MemoryDatabase.ts @@ -3,16 +3,15 @@ import { DocumentsCollection, IDatabase } from './IDatabase'; import { Transferable } from './Message'; export class MemoryDatabase implements IDatabase { - private db: { [collectionName: string]: { [id: string]: any } } = {}; private getCollection(collectionName: string) { const collection = this.db[collectionName]; if (collection) { return collection; - } else { - return this.db[collectionName] = {}; } + this.db[collectionName] = {}; + return {}; } public getCollectionNames() { @@ -21,15 +20,15 @@ export class MemoryDatabase implements IDatabase { public update(id: string, value: any, callback: (err: mongodb.MongoError, res: mongodb.UpdateResult) => void, _upsert?: boolean, collectionName = DocumentsCollection): Promise<void> { const collection = this.getCollection(collectionName); - const set = "$set"; + const set = '$set'; if (set in value) { let currentVal = collection[id] ?? (collection[id] = {}); const val = value[set]; for (const key in val) { - const keys = key.split("."); + const keys = key.split('.'); for (let i = 0; i < keys.length - 1; i++) { const k = keys[i]; - if (typeof currentVal[k] === "object") { + if (typeof currentVal[k] === 'object') { currentVal = currentVal[k]; } else { currentVal[k] = {}; @@ -45,7 +44,7 @@ export class MemoryDatabase implements IDatabase { return Promise.resolve(undefined); } - public updateMany(query: any, update: any, collectionName = DocumentsCollection): Promise<mongodb.UpdateResult> { + public updateMany(/* query: any, update: any, collectionName = DocumentsCollection */): Promise<mongodb.UpdateResult> { throw new Error("Can't updateMany a MemoryDatabase"); } @@ -54,7 +53,9 @@ export class MemoryDatabase implements IDatabase { } public delete(query: any, collectionName?: string): Promise<mongodb.DeleteResult>; + // eslint-disable-next-line no-dupe-class-members public delete(id: string, collectionName?: string): Promise<mongodb.DeleteResult>; + // eslint-disable-next-line no-dupe-class-members public delete(id: any, collectionName = DocumentsCollection): Promise<mongodb.DeleteResult> { const i = id.id ?? id; delete this.getCollection(collectionName)[i]; @@ -75,7 +76,7 @@ export class MemoryDatabase implements IDatabase { } public insert(value: any, collectionName = DocumentsCollection): Promise<void> { - const id = value.id; + const { id } = value; this.getCollection(collectionName)[id] = value; return Promise.resolve(); } @@ -93,14 +94,18 @@ export class MemoryDatabase implements IDatabase { const count = Math.min(ids.length, 1000); const index = ids.length - count; const fetchIds = ids.splice(index, count).filter(id => !visited.has(id)); - if (!fetchIds.length) { - continue; - } - const docs = await new Promise<{ [key: string]: any }[]>(res => this.getDocuments(fetchIds, res, collectionName)); - for (const doc of docs) { - const id = doc.id; - visited.add(id); - ids.push(...(await fn(doc))); + if (fetchIds.length) { + // eslint-disable-next-line no-await-in-loop + const docs = await new Promise<{ [key: string]: any }[]>(res => { + this.getDocuments(fetchIds, res, collectionName); + }); + // eslint-disable-next-line no-restricted-syntax + for (const doc of docs) { + const { id } = doc; + visited.add(id); + // eslint-disable-next-line no-await-in-loop + ids.push(...(await fn(doc))); + } } } } diff --git a/src/server/PdfTypes.ts b/src/server/PdfTypes.ts index e87f08e1d..fb435a677 100644 --- a/src/server/PdfTypes.ts +++ b/src/server/PdfTypes.ts @@ -1,21 +1,19 @@ -export interface ParsedPDF { - numpages: number; - numrender: number; - info: PDFInfo; - metadata: PDFMetadata; - version: string; //https://mozilla.github.io/pdf.js/getting_started/ - text: string; -} - export interface PDFInfo { PDFFormatVersion: string; IsAcroFormPresent: boolean; IsXFAPresent: boolean; [key: string]: any; } - export interface PDFMetadata { parse(): void; get(name: string): string; has(name: string): boolean; -}
\ No newline at end of file +} +export interface ParsedPDF { + numpages: number; + numrender: number; + info: PDFInfo; + metadata: PDFMetadata; + version: string; // https://mozilla.github.io/pdf.js/getting_started/ + text: string; +} diff --git a/src/server/ProcessFactory.ts b/src/server/ProcessFactory.ts index f69eda4c3..3791b0e1e 100644 --- a/src/server/ProcessFactory.ts +++ b/src/server/ProcessFactory.ts @@ -1,44 +1,42 @@ -import { ChildProcess, spawn, StdioOptions } from "child_process"; -import { existsSync, mkdirSync } from "fs"; -import { Stream } from "stream"; +import { ChildProcess, spawn, StdioOptions } from 'child_process'; +import { existsSync, mkdirSync } from 'fs'; +import { rimraf } from 'rimraf'; +import { Stream } from 'stream'; import { fileDescriptorFromStream, pathFromRoot } from './ActionUtilities'; -import { rimraf } from "rimraf"; - -export namespace ProcessFactory { - - export type Sink = "pipe" | "ipc" | "ignore" | "inherit" | Stream | number | null | undefined; - - export async function createWorker(command: string, args?: readonly string[], stdio?: StdioOptions | "logfile", detached = true): Promise<ChildProcess> { - if (stdio === "logfile") { - const log_fd = await Logger.create(command, args); - stdio = ["ignore", log_fd, log_fd]; - } - const child = spawn(command, args ?? [], { detached, stdio }); - child.unref(); - return child; - } - -} export namespace Logger { - - const logPath = pathFromRoot("./logs"); + const logPath = pathFromRoot('./logs'); export async function initialize() { if (existsSync(logPath)) { if (!process.env.SPAWNED) { - await new Promise<any>(resolve => rimraf(logPath).then(resolve)); + await new Promise<any>(resolve => { + rimraf(logPath).then(resolve); + }); } } mkdirSync(logPath); } - export async function create(command: string, args?: readonly string[]): Promise<number> { - return fileDescriptorFromStream(generate_log_path(command, args)); + function generateLogPath(command: string, args?: readonly string[]) { + return pathFromRoot(`./logs/${command}-${args?.length}-${new Date().toUTCString()}.log`); } - function generate_log_path(command: string, args?: readonly string[]) { - return pathFromRoot(`./logs/${command}-${args?.length}-${new Date().toUTCString()}.log`); + export async function create(command: string, args?: readonly string[]): Promise<number> { + return fileDescriptorFromStream(generateLogPath(command, args)); } +} +export namespace ProcessFactory { + export type Sink = 'pipe' | 'ipc' | 'ignore' | 'inherit' | Stream | number | null | undefined; -}
\ No newline at end of file + export async function createWorker(command: string, args?: readonly string[], stdio?: StdioOptions | 'logfile', detached = true): Promise<ChildProcess> { + if (stdio === 'logfile') { + const logFd = await Logger.create(command, args); + // eslint-disable-next-line no-param-reassign + stdio = ['ignore', logFd, logFd]; + } + const child = spawn(command, args ?? [], { detached, stdio }); + child.unref(); + return child; + } +} diff --git a/src/server/RouteSubscriber.ts b/src/server/RouteSubscriber.ts index a1cf7c1c4..b923805a8 100644 --- a/src/server/RouteSubscriber.ts +++ b/src/server/RouteSubscriber.ts @@ -18,9 +18,8 @@ export default class RouteSubscriber { public get build() { let output = this._root; if (this.requestParameters.length) { - output = `${output}/:${this.requestParameters.join("/:")}`; + output = `${output}/:${this.requestParameters.join('/:')}`; } return output; } - -}
\ No newline at end of file +} diff --git a/src/server/SharedMediaTypes.ts b/src/server/SharedMediaTypes.ts index 7db1c2dae..0a961f570 100644 --- a/src/server/SharedMediaTypes.ts +++ b/src/server/SharedMediaTypes.ts @@ -22,24 +22,17 @@ export namespace Upload { return 'duration' in uploadResponse; } + export interface AccessPathInfo { + [suffix: string]: { client: string; server: string }; + } export interface FileInformation { accessPaths: AccessPathInfo; rawText?: string; duration?: number; } - - export type FileResponse<T extends FileInformation = FileInformation> = { source: File; result: T | Error }; - - export type ImageInformation = FileInformation & InspectionResults; - - export type VideoInformation = FileInformation & VideoResults; - - export interface AccessPathInfo { - [suffix: string]: { client: string; server: string }; - } - - export interface VideoResults { - duration: number; + export interface EnrichedExifData { + data: ExifData & ExifData['gps']; + error?: string; } export interface InspectionResults { source: string; @@ -51,9 +44,13 @@ export namespace Upload { nativeHeight: number; filename?: string; } - - export interface EnrichedExifData { - data: ExifData & ExifData['gps']; - error?: string; + export interface VideoResults { + duration: number; } + + export type FileResponse<T extends FileInformation = FileInformation> = { source: File; result: T | Error }; + + export type ImageInformation = FileInformation & InspectionResults; + + export type VideoInformation = FileInformation & VideoResults; } diff --git a/src/server/apis/google/CredentialsLoader.ts b/src/server/apis/google/CredentialsLoader.ts index ef1f9a91e..46dc00b8a 100644 --- a/src/server/apis/google/CredentialsLoader.ts +++ b/src/server/apis/google/CredentialsLoader.ts @@ -1,10 +1,9 @@ -import { readFile, readFileSync } from "fs"; -import { pathFromRoot } from "../../ActionUtilities"; -import { SecureContextOptions } from "tls"; -import { blue, red } from "colors"; +import { readFile, readFileSync } from 'fs'; +import { SecureContextOptions } from 'tls'; +import { blue, red } from 'colors'; +import { pathFromRoot } from '../../ActionUtilities'; export namespace GoogleCredentialsLoader { - export interface InstalledCredentials { client_id: string; project_id: string; @@ -28,18 +27,16 @@ export namespace GoogleCredentialsLoader { }); }); } - } export namespace SSL { - export let Credentials: SecureContextOptions = {}; export let Loaded = false; const suffixes = { - privateKey: ".key", - certificate: ".crt", - caBundle: "-ca.crt" + privateKey: '.key', + certificate: '.crt', + caBundle: '-ca.crt', }; export async function loadCredentials() { @@ -57,11 +54,10 @@ export namespace SSL { } export function exit() { - console.log(red("Running this server in release mode requires the following SSL credentials in the project root:")); - const serverName = process.env.serverName ? process.env.serverName : "{process.env.serverName}"; + console.log(red('Running this server in release mode requires the following SSL credentials in the project root:')); + const serverName = process.env.serverName ? process.env.serverName : '{process.env.serverName}'; Object.values(suffixes).forEach(suffix => console.log(blue(`${serverName}${suffix}`))); - console.log(red("Please ensure these files exist and restart, or run this in development mode.")); + console.log(red('Please ensure these files exist and restart, or run this in development mode.')); process.exit(0); } - } diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts index 55e5fd7c0..d3acc968b 100644 --- a/src/server/apis/google/GoogleApiServerUtils.ts +++ b/src/server/apis/google/GoogleApiServerUtils.ts @@ -57,12 +57,12 @@ export namespace GoogleApiServerUtils { * global, intentionally unauthenticated worker OAuth2 client instance. */ export function processProjectCredentials(): void { - const { client_secret, client_id, redirect_uris } = GoogleCredentialsLoader.ProjectCredentials; + const { client_secret: clientSecret, client_id: clientId, redirect_uris: redirectUris } = GoogleCredentialsLoader.ProjectCredentials; // initialize the global authorization client oAuthOptions = { - clientId: client_id, - clientSecret: client_secret, - redirectUri: redirect_uris[0], + clientId, + clientSecret, + redirectUri: redirectUris[0], }; worker = generateClient(); } diff --git a/src/server/apis/google/SharedTypes.ts b/src/server/apis/google/SharedTypes.ts index 9ad6130b6..f5e0e2e2b 100644 --- a/src/server/apis/google/SharedTypes.ts +++ b/src/server/apis/google/SharedTypes.ts @@ -1,9 +1,3 @@ -export interface NewMediaItemResult { - uploadToken: string; - status: { code: number, message: string }; - mediaItem: MediaItem; -} - export interface MediaItem { id: string; description: string; @@ -17,5 +11,10 @@ export interface MediaItem { }; filename: string; } +export interface NewMediaItemResult { + uploadToken: string; + status: { code: number; message: string }; + mediaItem: MediaItem; +} -export type MediaItemCreationResult = { newMediaItemResults: NewMediaItemResult[] };
\ No newline at end of file +export type MediaItemCreationResult = { newMediaItemResults: NewMediaItemResult[] }; diff --git a/src/server/apis/youtube/youtubeApiSample.d.ts b/src/server/apis/youtube/youtubeApiSample.d.ts index 427f54608..97c3f0b54 100644 --- a/src/server/apis/youtube/youtubeApiSample.d.ts +++ b/src/server/apis/youtube/youtubeApiSample.d.ts @@ -1,2 +1,2 @@ declare const YoutubeApi: any; -export = YoutubeApi;
\ No newline at end of file +export = YoutubeApi; diff --git a/src/server/authentication/DashUserModel.ts b/src/server/authentication/DashUserModel.ts index 3bc21ecb6..a288bfeab 100644 --- a/src/server/authentication/DashUserModel.ts +++ b/src/server/authentication/DashUserModel.ts @@ -73,9 +73,9 @@ userSchema.pre('save', function save(next) { user.password, salt, () => {}, - (err: mongoose.Error, hash: string) => { - if (err) { - return next(err); + (cryptErr: mongoose.Error, hash: string) => { + if (cryptErr) { + return next(cryptErr); } user.password = hash; next(); @@ -97,7 +97,7 @@ const comparePassword: comparePasswordFunction = function (this: DashUserModel, userSchema.methods.comparePassword = comparePassword; -const User = mongoose.model('User', userSchema); +const User: any = mongoose.model('User', userSchema); export function initializeGuest() { new User({ email: 'guest', diff --git a/src/server/authentication/Passport.ts b/src/server/authentication/Passport.ts index a9cf6698b..a5222e531 100644 --- a/src/server/authentication/Passport.ts +++ b/src/server/authentication/Passport.ts @@ -1,6 +1,6 @@ import * as passport from 'passport'; import * as passportLocal from 'passport-local'; -import { DashUserModel, default as User } from './DashUserModel'; +import User, { DashUserModel } from './DashUserModel'; const LocalStrategy = passportLocal.Strategy; @@ -11,22 +11,25 @@ passport.serializeUser<any, any>((req, user, done) => { passport.deserializeUser<any, any>((id, done) => { User.findById(id) .exec() - .then(user => done(undefined, user)); + .then((user: any) => done(undefined, user)); }); // AUTHENTICATE JUST WITH EMAIL AND PASSWORD passport.use( new LocalStrategy({ usernameField: 'email', passReqToCallback: true }, (req, email, password, done) => { User.findOne({ email: email.toLowerCase() }) - .then(user => { - if (!user) return done(undefined, false, { message: 'Invalid email or password' }); // invalid email - (user as any as DashUserModel).comparePassword(password, (error: Error, isMatch: boolean) => { - if (error) return done(error); - if (!isMatch) return done(undefined, false, { message: 'Invalid email or password' }); // invalid password - // valid authentication HERE - return done(undefined, user); - }); + .then((user: any) => { + if (!user) { + done(undefined, false, { message: 'Invalid email or password' }); // invalid email + } else { + (user as any as DashUserModel).comparePassword(password, (error: Error, isMatch: boolean) => { + if (error) return done(error); + if (!isMatch) return done(undefined, false, { message: 'Invalid email or password' }); // invalid password + // valid authentication HERE + return done(undefined, user); + }); + } }) - .catch(error => done(error)); + .catch((error: any) => done(error)); }) ); diff --git a/src/server/database.ts b/src/server/database.ts index 3a28dc87e..ff5635b2c 100644 --- a/src/server/database.ts +++ b/src/server/database.ts @@ -58,6 +58,7 @@ export namespace Database { } } + // eslint-disable-next-line @typescript-eslint/no-shadow export class Database implements IDatabase { private MongoClient = mongodb.MongoClient; private currentWrites: { [id: string]: Promise<void> } = {}; diff --git a/src/server/index.ts b/src/server/index.ts index 5a86f36d9..4374a72b7 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -1,15 +1,14 @@ import { yellow } from 'colors'; +// eslint-disable-next-line import/no-extraneous-dependencies import * as dotenv from 'dotenv'; import * as mobileDetect from 'mobile-detect'; import * as path from 'path'; -import * as qs from 'query-string'; import { logExecution } from './ActionUtilities'; import { AdminPrivileges, resolvedPorts } from './SocketData'; import DataVizManager from './ApiManagers/DataVizManager'; import DeleteManager from './ApiManagers/DeleteManager'; import DownloadManager from './ApiManagers/DownloadManager'; import GeneralGoogleManager from './ApiManagers/GeneralGoogleManager'; -//import GooglePhotosManager from './ApiManagers/GooglePhotosManager'; import { SearchManager } from './ApiManagers/SearchManager'; import SessionManager from './ApiManagers/SessionManager'; import UploadManager from './ApiManagers/UploadManager'; @@ -26,8 +25,11 @@ import { Logger } from './ProcessFactory'; import RouteManager, { Method, PublicHandler } from './RouteManager'; import RouteSubscriber from './RouteSubscriber'; import initializeServer from './server_Initialization'; +// import GooglePhotosManager from './ApiManagers/GooglePhotosManager'; + dotenv.config(); export const onWindows = process.platform === 'win32'; +// eslint-disable-next-line import/no-mutable-exports export let sessionAgent: AppliedSessionAgent; /** @@ -60,8 +62,18 @@ async function preliminaryFunctions() { * that will manage the registration of new routes * with the server */ -function routeSetter({ isRelease, addSupervisedRoute, logRegistrationOutcome }: RouteManager) { - const managers = [new SessionManager(), new UserManager(), new UploadManager(), new DownloadManager(), new SearchManager(), new DeleteManager(), new UtilManager(), new GeneralGoogleManager(), /* new GooglePhotosManager(),*/ new DataVizManager()]; +function routeSetter({ addSupervisedRoute, logRegistrationOutcome }: RouteManager) { + const managers = [ + new SessionManager(), + new UserManager(), + new UploadManager(), + new DownloadManager(), + new SearchManager(), + new DeleteManager(), + new UtilManager(), + new GeneralGoogleManager(), + /* new GooglePhotosManager(), */ new DataVizManager(), + ]; // initialize API Managers console.log(yellow('\nregistering server routes...')); @@ -102,6 +114,7 @@ function routeSetter({ isRelease, addSupervisedRoute, logRegistrationOutcome }: }); const serve: PublicHandler = ({ req, res }) => { + // eslint-disable-next-line new-cap const detector = new mobileDetect(req.headers['user-agent'] || ''); const filename = detector.mobile() !== null ? 'mobile/image.html' : 'index.html'; res.sendFile(path.join(__dirname, '../../deploy/' + filename)); @@ -116,9 +129,8 @@ function routeSetter({ isRelease, addSupervisedRoute, logRegistrationOutcome }: secureHandler: ({ res, isRelease }) => { const { PASSWORD } = process.env; if (!(isRelease && PASSWORD)) { - return res.redirect('/home'); - } - res.render('admin.pug', { title: 'Enter Administrator Password' }); + res.redirect('/home'); + } else res.render('admin.pug', { title: 'Enter Administrator Password' }); }, }); @@ -128,18 +140,19 @@ function routeSetter({ isRelease, addSupervisedRoute, logRegistrationOutcome }: secureHandler: async ({ req, res, isRelease, user: { id } }) => { const { PASSWORD } = process.env; if (!(isRelease && PASSWORD)) { - return res.redirect('/home'); - } - const { password } = req.body; - const { previous_target } = req.params; - let redirect: string; - if (password === PASSWORD) { - AdminPrivileges.set(id, true); - redirect = `/${previous_target.replace(':', '/')}`; + res.redirect('/home'); } else { - redirect = `/admin/${previous_target}`; + const { password } = req.body; + const { previous_target: previousTarget } = req.params; + let redirect: string; + if (password === PASSWORD) { + AdminPrivileges.set(id, true); + redirect = `/${previousTarget.replace(':', '/')}`; + } else { + redirect = `/admin/${previousTarget}`; + } + res.redirect(redirect); } - res.redirect(redirect); }, }); @@ -149,7 +162,6 @@ function routeSetter({ isRelease, addSupervisedRoute, logRegistrationOutcome }: secureHandler: serve, publicHandler: ({ req, res, ...remaining }) => { const { originalUrl: target } = req; - const sharing = qs.parse(qs.extract(req.originalUrl), { sort: false }).sharing === 'true'; const docAccess = target.startsWith('/doc/'); // since this is the public handler, there's no meaning of '/home' to speak of // since there's no user logged in, so the only viable operation diff --git a/src/server/updateProtos.ts b/src/server/updateProtos.ts index 2f3772a77..72a44ebf4 100644 --- a/src/server/updateProtos.ts +++ b/src/server/updateProtos.ts @@ -6,15 +6,15 @@ const protos = ['text', 'image', 'web', 'collection', 'kvp', 'video', 'audio', ' await Promise.all( protos.map( protoId => - new Promise(res => + new Promise(res => { Database.Instance.update( protoId, { $set: { 'fields.isBaseProto': true }, }, res - ) - ) + ); + }) ) ); diff --git a/src/server/websocket.ts b/src/server/websocket.ts index cb16bce72..cece8a1b7 100644 --- a/src/server/websocket.ts +++ b/src/server/websocket.ts @@ -79,7 +79,7 @@ export namespace WebSocket { timeMap[userEmail] = Date.now(); socketMap.set(socket, userEmail + ' at ' + datetime); userOperations.set(userEmail, 0); - DashStats.logUserLogin(userEmail, socket); + DashStats.logUserLogin(userEmail); } function getField([id, callback]: [string, (result?: Transferable) => void]) { @@ -417,38 +417,12 @@ export namespace WebSocket { socket.in(room).emit('message', message); }); - socket.on('create or join', room => { - console.log('Received request to create or join room ' + room); - - const clientsInRoom = socket.rooms.has(room); - const numClients = clientsInRoom ? Object.keys(room.sockets).length : 0; - console.log('Room ' + room + ' now has ' + numClients + ' client(s)'); - - if (numClients === 0) { - socket.join(room); - console.log('Client ID ' + socket.id + ' created room ' + room); - socket.emit('created', room, socket.id); - } else if (numClients === 1) { - console.log('Client ID ' + socket.id + ' joined room ' + room); - socket.in(room).emit('join', room); - socket.join(room); - socket.emit('joined', room, socket.id); - socket.in(room).emit('ready'); - } else { - // max two clients - socket.emit('full', room); - } - }); - socket.on('ipaddr', () => { - const ifaces = networkInterfaces(); - for (const dev in ifaces) { - ifaces[dev]?.forEach(details => { - if (details.family === 'IPv4' && details.address !== '127.0.0.1') { - socket.emit('ipaddr', details.address); - } - }); - } + networkInterfaces().keys?.forEach(dev => { + if (dev.family === 'IPv4' && dev.address !== '127.0.0.1') { + socket.emit('ipaddr', dev.address); + } + }); }); socket.on('bye', () => { @@ -459,7 +433,7 @@ export namespace WebSocket { const currentUser = socketMap.get(socket); if (!(currentUser === undefined)) { const currentUsername = currentUser.split(' ')[0]; - DashStats.logUserLogout(currentUsername, socket); + DashStats.logUserLogout(currentUsername); delete timeMap[currentUsername]; } }); |