diff options
author | bobzel <zzzman@gmail.com> | 2024-04-23 16:20:08 -0400 |
---|---|---|
committer | bobzel <zzzman@gmail.com> | 2024-04-23 16:20:08 -0400 |
commit | 9e809f8748d1812bb03ec6471aa6f97467f8f75a (patch) | |
tree | 6657f2290e1c1a10456a32d2e1462981f461c8d0 /src | |
parent | 939e18624af4252551f38c43335ee8ef0acd144c (diff) |
fixes for rich text menu updates and setting parameters on text doc vs within in RTF. Lots of lint cleanup.
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]; } }); |