From 2a313f28fcb8675223708b0657de7517a3281095 Mon Sep 17 00:00:00 2001 From: bobzel Date: Wed, 17 Apr 2024 12:27:21 -0400 Subject: restoring eslint - updates not complete yet --- src/client/DocServer.ts | 228 ++++++++++++++++++++---------------------------- 1 file changed, 95 insertions(+), 133 deletions(-) (limited to 'src/client/DocServer.ts') diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts index 321572071..804e6d1d7 100644 --- a/src/client/DocServer.ts +++ b/src/client/DocServer.ts @@ -1,20 +1,15 @@ import { runInAction } from 'mobx'; -import * as rp from 'request-promise'; -import { Doc, DocListCast, Opt } from '../fields/Doc'; +import { Socket, io } from 'socket.io-client'; +import { ClientUtils } from '../ClientUtils'; +import { Utils, emptyFunction } from '../Utils'; +import { Doc, Opt } from '../fields/Doc'; import { UpdatingFromServer } from '../fields/DocSymbols'; import { FieldLoader } from '../fields/FieldLoader'; import { HandleUpdate, Id, Parent } from '../fields/FieldSymbols'; -import { ObjectField } from '../fields/ObjectField'; +import { ObjectField, SetObjGetRefField, SetObjGetRefFields } from '../fields/ObjectField'; import { RefField } from '../fields/RefField'; -import { DocCast, StrCast } from '../fields/Types'; -//import MobileInkOverlay from '../mobile/MobileInkOverlay'; -import { io, Socket } from 'socket.io-client'; -import { emptyFunction, Utils } from '../Utils'; import { GestureContent, Message, MessageStore, MobileDocumentUploadContent, MobileInkOverlayContent, UpdateMobileInkOverlayPositionContent, YoutubeQueryTypes } from './../server/Message'; -import { DocumentType } from './documents/DocumentTypes'; -import { LinkManager } from './util/LinkManager'; import { SerializationHelper } from './util/SerializationHelper'; -//import { GestureOverlay } from './views/GestureOverlay'; /** * This class encapsulates the transfer and cross-client synchronization of @@ -30,23 +25,32 @@ import { SerializationHelper } from './util/SerializationHelper'; * or update ourselves based on the server's update message, that occurs here */ export namespace DocServer { - let _cache: { [id: string]: RefField | Promise> } = {}; + // eslint-disable-next-line import/no-mutable-exports + export let _cache: { [id: string]: RefField | Promise> } = {}; + + function errorFunc(): never { + throw new Error("Can't use DocServer without calling init first"); + } + let _UpdateField: (id: string, diff: any) => void = errorFunc; + let _CreateField: (field: RefField) => void = errorFunc; export function AddServerHandler(socket: Socket, message: Message, handler: (args: T) => any) { socket.on(message.Message, Utils.loggingCallback('Incoming', handler, message.Name)); } export function Emit(socket: Socket, message: Message, args: T) { - //log('Emit', message.Name, args, false); + // log('Emit', message.Name, args, false); socket.emit(message.Message, args); } export function EmitCallback(socket: Socket, message: Message, args: T): Promise; export function EmitCallback(socket: Socket, message: Message, args: T, fn: (args: any) => any): void; export function EmitCallback(socket: Socket, message: Message, args: T, fn?: (args: any) => any): void | Promise { - //log('Emit', message.Name, args, false); + // log('Emit', message.Name, args, false); if (fn) { socket.emit(message.Message, args, Utils.loggingCallback('Receiving', fn, message.Name)); } else { - return new Promise(res => socket.emit(message.Message, args, Utils.loggingCallback('Receiving', res, message.Name))); + return new Promise(res => { + socket.emit(message.Message, args, Utils.loggingCallback('Receiving', res, message.Name)); + }); } } @@ -59,57 +63,21 @@ export namespace DocServer { return foundDocId ? (_cache[foundDocId] as Doc) : undefined; } - let cacheDocumentIds = ''; // ; separate string of all documents ids in the user's working set (cached on the server) - export let CacheNeedsUpdate = false; - export function UPDATE_SERVER_CACHE() { - const prototypes = Object.values(DocumentType) - .filter(type => type !== DocumentType.NONE) - .map(type => _cache[type + 'Proto']) - .filter(doc => doc instanceof Doc) - .map(doc => doc as Doc); - const references = new Set(prototypes); - Doc.FindReferences(Doc.UserDoc(), references, undefined); - DocListCast(DocCast(Doc.UserDoc().myLinkDatabase).data).forEach(link => { - if (!references.has(DocCast(link.link_anchor_1)) && !references.has(DocCast(link.link_anchor_2))) { - Doc.RemoveDocFromList(DocCast(Doc.UserDoc().myLinkDatabase), 'data', link); - Doc.AddDocToList(Doc.MyRecentlyClosed, undefined, link); - } - }); - LinkManager.Instance.userLinkDBs.forEach(linkDb => Doc.FindReferences(linkDb, references, undefined)); - const filtered = Array.from(references); - - const newCacheUpdate = filtered.map(doc => doc[Id]).join(';'); - if (newCacheUpdate === cacheDocumentIds) return; - cacheDocumentIds = newCacheUpdate; - - // print out cached docs - Doc.MyDockedBtns.linearView_IsOpen && console.log('Set cached docs = '); - const is_filtered = filtered.filter(doc => !Doc.IsSystem(doc)); - const strings = is_filtered.map(doc => StrCast(doc.title) + ' ' + (Doc.IsDataProto(doc) ? '(data)' : '(embedding)')); - Doc.MyDockedBtns.linearView_IsOpen && strings.sort().forEach((str, i) => console.log(i.toString() + ' ' + str)); - - rp.post(Utils.prepend('/setCacheDocumentIds'), { - body: { - cacheDocumentIds, - }, - json: true, - }); - } export 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 export enum WriteMode { - Default = 0, //Anything goes - Playground = 1, //Playground (write own/no read other updates) - LiveReadonly = 2, //Live Readonly (no write/read others) - LivePlayground = 3, //Live Playground (write own/read others) + Default = 0, // Anything goes + Playground = 1, // Playground (write own/no read other updates) + LiveReadonly = 2, // Live Readonly (no write/read others) + LivePlayground = 3, // Live Playground (write own/read others) } const fieldWriteModes: { [field: string]: WriteMode } = {}; const docsWithUpdates: { [field: string]: Set } = {}; - export var PlaygroundFields: string[] = []; + export const PlaygroundFields: string[] = []; export function setLivePlaygroundFields(livePlaygroundFields: string[]) { DocServer.PlaygroundFields.push(...livePlaygroundFields); livePlaygroundFields.forEach(f => DocServer.setFieldWriteMode(f, DocServer.WriteMode.LivePlayground)); @@ -134,7 +102,7 @@ export namespace DocServer { } export function getFieldWriteMode(field: string) { - return Doc.CurrentUserEmail === 'guest' ? WriteMode.LivePlayground : fieldWriteModes[field] || WriteMode.Default; + return ClientUtils.CurrentUserEmail === 'guest' ? WriteMode.LivePlayground : fieldWriteModes[field] || WriteMode.Default; } export function registerDocWithCachedUpdate(doc: Doc, field: string, oldValue: any) { @@ -185,56 +153,14 @@ export namespace DocServer { window.location.reload(); } - export function init(protocol: string, hostname: string, port: number, identifier: string) { - _cache = {}; - USER_ID = identifier; - protocol = protocol.startsWith('https') ? 'wss' : 'ws'; - _socket = io(`${protocol}://${hostname}:${port}`, { transports: ['websocket'], rejectUnauthorized: false }); - _socket.on("connect_error", (err:any) => console.log(err)); - // io.connect(`https://7f079dda.ngrok.io`);// if using ngrok, create a special address for the websocket - - _GetCachedRefField = _GetCachedRefFieldImpl; - _GetRefField = _GetRefFieldImpl; - _GetRefFields = _GetRefFieldsImpl; - _CreateField = _CreateFieldImpl; - _UpdateField = _UpdateFieldImpl; - - /** - * Whenever the server sends us its handshake message on our - * websocket, we use the above function to return the handshake. - */ - DocServer.AddServerHandler(_socket, MessageStore.Foo, onConnection); - DocServer.AddServerHandler(_socket, MessageStore.UpdateField, respondToUpdate); - DocServer.AddServerHandler(_socket, MessageStore.DeleteField, respondToDelete); - DocServer.AddServerHandler(_socket, MessageStore.DeleteFields, respondToDelete); - DocServer.AddServerHandler(_socket, MessageStore.ConnectionTerminated, alertUser); - - // // mobile ink overlay socket events to communicate between mobile view and desktop view - // _socket.addEventListener('receiveGesturePoints', (content: GestureContent) => { - // // MobileInkOverlay.Instance.drawStroke(content); - // }); - // _socket.addEventListener('receiveOverlayTrigger', (content: MobileInkOverlayContent) => { - // //GestureOverlay.Instance.enableMobileInkOverlay(content); - // // MobileInkOverlay.Instance.initMobileInkOverlay(content); - // }); - // _socket.addEventListener('receiveUpdateOverlayPosition', (content: UpdateMobileInkOverlayPositionContent) => { - // // MobileInkOverlay.Instance.updatePosition(content); - // }); - // _socket.addEventListener('receiveMobileDocumentUpload', (content: MobileDocumentUploadContent) => { - // // MobileInkOverlay.Instance.uploadDocument(content); - // }); - } - - function errorFunc(): never { - throw new Error("Can't use DocServer without calling init first"); - } - export namespace Control { let _isReadOnly = false; export function makeReadOnly() { if (!_isReadOnly) { _isReadOnly = true; - _CreateField = field => (_cache[field[Id]] = field); + _CreateField = field => { + _cache[field[Id]] = field; + }; _UpdateField = emptyFunction; // _RespondToUpdate = emptyFunction; // bcz: option: don't clear RespondToUpdate to continue to receive updates as others change the DB } @@ -313,7 +239,8 @@ export namespace DocServer { }); cached[UpdatingFromServer] = false; return cached; - } else if (field !== undefined) { + } + if (field !== undefined) { _cache[id] = field; } else { delete _cache[id]; @@ -327,24 +254,25 @@ export namespace DocServer { // being retrieved and cached !force && (_cache[id] = deserializeField); return force ? (cached as any) : deserializeField; - } else if (cached instanceof Promise) { + } + if (cached instanceof Promise) { // BEING RETRIEVED AND CACHED => some other caller previously (likely recently) called GetRefField(s), // and requested the document I'm looking for. Shouldn't fetch again, just // return this promise which will resolve to the field itself (see 7) return cached; - } else { - // CACHED => great, let's just return the cached field we have - return Promise.resolve(cached).then(field => { - //(field instanceof Doc) && fetchProto(field); - return field; - }); } + // CACHED => great, let's just return the cached field we have + return Promise.resolve(cached).then( + field => field + // (field instanceof Doc) && fetchProto(field); + ); }; const _GetCachedRefFieldImpl = (id: string): Opt => { const cached = _cache[id]; if (cached !== undefined && !(cached instanceof Promise)) { return cached; } + return undefined; }; let _GetRefField: (id: string, force: boolean) => Promise> = errorFunc; @@ -358,7 +286,7 @@ export namespace DocServer { } export async function getYoutubeChannels() { - return await DocServer.EmitCallback(_socket, MessageStore.YoutubeApiQuery, { type: YoutubeQueryTypes.Channels }); + return DocServer.EmitCallback(_socket, MessageStore.YoutubeApiQuery, { type: YoutubeQueryTypes.Channels }); } export function getYoutubeVideos(videoTitle: string, callBack: (videos: any[]) => void) { @@ -379,21 +307,24 @@ export namespace DocServer { const requestedIds: string[] = []; const promises: Promise[] = []; - let defaultRes: any = undefined; - const defaultPromise = new Promise(res => (defaultRes = res)); - let defaultPromises: { p: Promise; id: string }[] = []; + let defaultRes: any; + const defaultPromise = new Promise(res => { + defaultRes = res; + }); + const defaultPromises: { p: Promise; id: string }[] = []; // 1) an initial pass through the cache to determine // i) which documents need to be fetched // 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)) { const cached = _cache[id]; if (cached === undefined) { defaultPromises.push({ id, - p: (_cache[id] = new Promise(async res => { - await defaultPromise; - res(_cache[id]); + // eslint-disable-next-line no-loop-func + p: (_cache[id] = new Promise(res => { + defaultPromise.then(() => res(_cache[id])); })), }); // NOT CACHED => we'll have to send a request to the server @@ -415,7 +346,11 @@ export namespace DocServer { // fields for the given ids. This returns a promise, which, when resolved, indicates that all the JSON serialized versions of // the fields have been returned from the server console.log('Requesting ' + requestedIds.length); - setTimeout(() => runInAction(() => (FieldLoader.ServerLoadStatus.requested = requestedIds.length))); + setTimeout(() => + runInAction(() => { + FieldLoader.ServerLoadStatus.requested = requestedIds.length; + }) + ); const serializedFields = await DocServer.EmitCallback(_socket, MessageStore.GetRefFields, requestedIds); // 3) when the serialized RefFields have been received, go head and begin deserializing them into objects. @@ -423,11 +358,17 @@ export namespace DocServer { // future .proto calls on the Doc won't have to go farther than the cache to get their actual value. let processed = 0; console.log('deserializing ' + serializedFields.length + ' fields'); + // eslint-disable-next-line no-restricted-syntax for (const field of serializedFields) { processed++; if (processed % 150 === 0) { - runInAction(() => (FieldLoader.ServerLoadStatus.retrieved = processed)); - await new Promise(res => setTimeout(res)); // force loading to yield to splash screen rendering to update progress + runInAction(() => { + FieldLoader.ServerLoadStatus.retrieved = processed; + }); + // eslint-disable-next-line no-await-in-loop + await new Promise(res => { + setTimeout(res); + }); // force loading to yield to splash screen rendering to update progress } const cached = _cache[field.id]; if (!cached || (cached instanceof Promise && defaultPromises.some(dp => dp.p === cached))) { @@ -435,7 +376,7 @@ export namespace DocServer { // adds to a list of promises that will be awaited asynchronously promises.push( (_cache[field.id] = SerializationHelper.Deserialize(field).then(deserialized => { - //overwrite or delete any promises (that we inserted as flags + // overwrite or delete any promises (that we inserted as flags // to indicate that the field was in the process of being fetched). Now everything // should be an actual value within or entirely absent from the cache. if (deserialized !== undefined) { @@ -455,7 +396,7 @@ export namespace DocServer { // get the resolved field } else if (cached instanceof Promise) { console.log('.'); - //promises.push(cached); + // promises.push(cached); } else if (field) { // console.log('-'); } @@ -472,7 +413,7 @@ export namespace DocServer { // 6) with this confidence, we can now go through and update the cache at the ids of the fields that // we explicitly had to fetch. To finish it off, we add whatever value we've come up with for a given // id to the soon-to-be-returned field mapping. - //ids.forEach(id => (map[id] = _cache[id] as any)); + // ids.forEach(id => (map[id] = _cache[id] as any)); // 7) those promises we encountered in the else if of 1), which represent // other callers having already submitted a request to the server for (a) document(s) @@ -481,7 +422,7 @@ export namespace DocServer { // // fortunately, those other callers will also hit their own version of 6) and clean up // the shared cache when these promises resolve, so all we have to do is... - //const otherCallersFetching = await Promise.all(promises); + // const otherCallersFetching = await Promise.all(promises); // ...extract the RefFields returned from the resolution of those promises and add them to our // own map. // waitingIds.forEach((id, index) => (map[id] = otherCallersFetching[index])); @@ -506,6 +447,7 @@ export namespace DocServer { } // WRITE A NEW DOCUMENT TO THE SERVER + export let CacheNeedsUpdate = false; /** * A wrapper around the function local variable _createField. @@ -521,11 +463,9 @@ export namespace DocServer { function _CreateFieldImpl(field: RefField) { _cache[field[Id]] = field; const initialState = SerializationHelper.Serialize(field); - Doc.CurrentUserEmail !== 'guest' && DocServer.Emit(_socket, MessageStore.CreateField, initialState); + ClientUtils.CurrentUserEmail !== 'guest' && DocServer.Emit(_socket, MessageStore.CreateField, initialState); } - let _CreateField: (field: RefField) => void = errorFunc; - // NOTIFY THE SERVER OF AN UPDATE TO A DOC'S STATE /** @@ -541,13 +481,11 @@ export namespace DocServer { } function _UpdateFieldImpl(id: string, diff: any) { - !DocServer.Control.isReadOnly() && Doc.CurrentUserEmail !== 'guest' && DocServer.Emit(_socket, MessageStore.UpdateField, { id, diff }); + !DocServer.Control.isReadOnly() && ClientUtils.CurrentUserEmail !== 'guest' && DocServer.Emit(_socket, MessageStore.UpdateField, { id, diff }); } - let _UpdateField: (id: string, diff: any) => void = errorFunc; - function _respondToUpdateImpl(diff: any) { - const id = diff.id; + const { id } = diff; // to be valid, the Diff object must reference // a document's id if (id === undefined) { @@ -578,11 +516,11 @@ export namespace DocServer { } export function DeleteDocument(id: string) { - Doc.CurrentUserEmail !== 'guest' && DocServer.Emit(_socket, MessageStore.DeleteField, id); + ClientUtils.CurrentUserEmail !== 'guest' && DocServer.Emit(_socket, MessageStore.DeleteField, id); } export function DeleteDocuments(ids: string[]) { - Doc.CurrentUserEmail !== 'guest' && DocServer.Emit(_socket, MessageStore.DeleteFields, ids); + ClientUtils.CurrentUserEmail !== 'guest' && DocServer.Emit(_socket, MessageStore.DeleteFields, ids); } function _respondToDeleteImpl(ids: string | string[]) { @@ -596,7 +534,7 @@ export namespace DocServer { } } - let _RespondToUpdate = _respondToUpdateImpl; + const _RespondToUpdate = _respondToUpdateImpl; const _respondToDelete = _respondToDeleteImpl; function respondToUpdate(diff: any) { @@ -606,4 +544,28 @@ export namespace DocServer { function respondToDelete(ids: string | string[]) { _respondToDelete(ids); } + + export function init(protocol: string, hostname: string, port: number, identifier: string) { + _cache = {}; + USER_ID = identifier; + _socket = io(`${protocol.startsWith('https') ? 'wss' : 'ws'}://${hostname}:${port}`, { transports: ['websocket'], rejectUnauthorized: false }); + _socket.on('connect_error', (err: any) => console.log(err)); + // io.connect(`https://7f079dda.ngrok.io`);// if using ngrok, create a special address for the websocket + + _GetCachedRefField = _GetCachedRefFieldImpl; + SetObjGetRefField((_GetRefField = _GetRefFieldImpl)); + SetObjGetRefFields((_GetRefFields = _GetRefFieldsImpl)); + _CreateField = _CreateFieldImpl; + _UpdateField = _UpdateFieldImpl; + + /** + * Whenever the server sends us its handshake message on our + * websocket, we use the above function to return the handshake. + */ + DocServer.AddServerHandler(_socket, MessageStore.Foo, onConnection); + DocServer.AddServerHandler(_socket, MessageStore.UpdateField, respondToUpdate); + DocServer.AddServerHandler(_socket, MessageStore.DeleteField, respondToDelete); + DocServer.AddServerHandler(_socket, MessageStore.DeleteFields, respondToDelete); + DocServer.AddServerHandler(_socket, MessageStore.ConnectionTerminated, alertUser); + } } -- cgit v1.2.3-70-g09d2 From 89e5b4e224d77c7a029ec7d9c9027095665508ac Mon Sep 17 00:00:00 2001 From: bobzel Date: Fri, 19 Apr 2024 11:32:46 -0400 Subject: lint fixes. --- .gitignore | 1 - src/.DS_Store | Bin 10244 -> 10244 bytes src/ClientUtils.ts | 649 +++++++++++++++++++++ src/client/DocServer.ts | 10 +- src/client/documents/Documents.ts | 10 +- src/client/util/CurrentUserUtils.ts | 14 +- src/client/util/DocumentManager.ts | 2 +- src/client/util/GroupManager.tsx | 6 +- src/client/util/HypothesisUtils.ts | 2 +- src/client/util/SettingsManager.tsx | 4 +- src/client/util/SharingManager.tsx | 10 +- src/client/util/reportManager/ReportManager.tsx | 2 +- src/client/views/DashboardView.tsx | 6 +- src/client/views/MainView.tsx | 2 +- src/client/views/PropertiesView.tsx | 4 +- src/client/views/SidebarAnnos.tsx | 2 +- src/client/views/StyleProvider.tsx | 2 +- src/client/views/collections/CollectionSubView.tsx | 2 +- src/client/views/collections/TreeView.tsx | 2 +- src/client/views/nodes/DocumentView.tsx | 2 +- .../views/nodes/formattedText/FormattedTextBox.tsx | 18 +- .../formattedText/FormattedTextBoxComment.tsx | 2 +- .../formattedText/ProsemirrorExampleTransfer.ts | 4 +- .../views/nodes/formattedText/RichTextRules.ts | 4 +- src/client/views/nodes/formattedText/marks_rts.ts | 2 +- src/fields/Doc.ts | 75 ++- src/fields/util.ts | 10 +- 27 files changed, 744 insertions(+), 103 deletions(-) create mode 100644 src/ClientUtils.ts (limited to 'src/client/DocServer.ts') diff --git a/.gitignore b/.gitignore index f841c1a86..6a1963b4e 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,6 @@ node_modules dist/ .DS_Store .env -ClientUtils.ts solr-8.3.1/server/logs/ solr-8.3.1/server/solr/dash/data/tlog/* solr-8.3.1/server/solr/dash/data/index/* diff --git a/src/.DS_Store b/src/.DS_Store index f8d745dbf..75cff7b55 100644 Binary files a/src/.DS_Store and b/src/.DS_Store differ diff --git a/src/ClientUtils.ts b/src/ClientUtils.ts new file mode 100644 index 000000000..57e0373c4 --- /dev/null +++ b/src/ClientUtils.ts @@ -0,0 +1,649 @@ +import * as Color from 'color'; +import * as React from 'react'; +import { ColorResult } from 'react-color'; +import * as rp from 'request-promise'; +import { numberRange, decimalToHexString } from './Utils'; +import { DocumentType } from './client/documents/DocumentTypes'; +import { Colors } from './client/views/global/globalEnums'; + +export function DashColor(color: string) { + try { + return color ? Color(color.toLowerCase()) : Color('transparent'); + } catch (e) { + if (color.includes('gradient')) console.log("using color 'white' in place of :" + color); + else console.log('COLOR error:', e); + return Color('white'); + } +} + +export function lightOrDark(color: any) { + if (color === 'transparent' || !color) return Colors.BLACK; + if (color.startsWith?.('linear')) return Colors.BLACK; + if (DashColor(color).isLight()) return Colors.BLACK; + return Colors.WHITE; +} + +export function returnTransparent() { + return 'transparent'; +} + +export function returnTrue() { + return true; +} + +export function returnIgnore(): 'ignore' { + return 'ignore'; +} +export function returnAlways(): 'always' { + return 'always'; +} +export function returnNever(): 'never' { + return 'never'; +} + +export function returnDefault(): 'default' { + return 'default'; +} + +export function return18() { + return 18; +} + +export function returnFalse() { + return false; +} + +export function returnAll(): 'all' { + return 'all'; +} + +export function returnNone(): 'none' { + return 'none'; +} + +export function returnVal(val1?: number, val2?: number) { + return val1 || (val2 !== undefined ? val2 : 0); +} + +export function returnOne() { + return 1; +} + +export function returnZero() { + return 0; +} + +export function returnEmptyString() { + return ''; +} + +export function returnEmptyFilter() { + return [] as string[]; +} + +export function returnEmptyDoclist() { + return [] as any[]; +} + +export namespace ClientUtils { + export const CLICK_TIME = 300; + export const DRAG_THRESHOLD = 4; + export const SNAP_THRESHOLD = 10; + let _currentUserEmail: string = ''; + export function CurrentUserEmail() { + return _currentUserEmail; + } + export function SetCurrentUserEmail(email: string) { + _currentUserEmail = email; + } + export function isClick(x: number, y: number, downX: number, downY: number, downTime: number) { + return Date.now() - downTime < ClientUtils.CLICK_TIME && Math.abs(x - downX) < ClientUtils.DRAG_THRESHOLD && Math.abs(y - downY) < ClientUtils.DRAG_THRESHOLD; + } + + export function cleanDocumentType(type: DocumentType) { + switch (type) { + case DocumentType.IMG: return 'Image'; + case DocumentType.AUDIO: return 'Audio'; + case DocumentType.COL: return 'Collection'; + case DocumentType.RTF: return 'Text'; + default: return type.charAt(0).toUpperCase() + type.slice(1); + } // prettier-ignore + } + + export function readUploadedFileAsText(inputFile: File) { + // eslint-disable-next-line no-undef + const temporaryFileReader = new FileReader(); + + return new Promise((resolve, reject) => { + temporaryFileReader.onerror = () => { + temporaryFileReader.abort(); + reject(new DOMException('Problem parsing input file.')); + }; + + temporaryFileReader.onload = () => { + resolve(temporaryFileReader.result); + }; + temporaryFileReader.readAsText(inputFile); + }); + } + + /** + * Uploads an image buffer to the server and stores with specified filename. by default the image + * is stored at multiple resolutions each retrieved by using the filename appended with _o, _s, _m, _l (indicating original, small, medium, or large) + * @param imageUri the bytes of the image + * @param returnedFilename the base filename to store the image on the server + * @param nosuffix optionally suppress creating multiple resolution images + */ + export async function convertDataUri(imageUri: string, returnedFilename: string, nosuffix = false, replaceRootFilename: string | undefined = undefined) { + try { + const posting = ClientUtils.prepend('/uploadURI'); + const returnedUri = await rp.post(posting, { + body: { + uri: imageUri, + name: returnedFilename, + nosuffix, + replaceRootFilename, + }, + json: true, + }); + return returnedUri; + } catch (e) { + console.log('ConvertDataURI :' + e); + } + return undefined; + } + + export function GetScreenTransform(ele?: HTMLElement | null): { scale: number; translateX: number; translateY: number } { + if (!ele) { + return { scale: 1, translateX: 1, translateY: 1 }; + } + const rect = ele.getBoundingClientRect(); + const scale = ele.offsetWidth === 0 && rect.width === 0 ? 1 : rect.width / ele.offsetWidth; + const translateX = rect.left; + const translateY = rect.top; + + return { scale, translateX, translateY }; + } + + /** + * A convenience method. Prepends the full path (i.e. http://localhost:) to the + * requested extension + * @param extension the specified sub-path to append to the window origin + */ + export function prepend(extension: string): string { + return window.location.origin + extension; + } + export function fileUrl(filename: string): string { + return prepend(`/files/${filename}`); + } + + export function shareUrl(documentId: string): string { + return prepend(`/doc/${documentId}?sharing=true`); + } + + export function CorsProxy(url: string): string { + return prepend('/corsProxy/') + encodeURIComponent(url); + } + + export function CopyText(text: string) { + navigator.clipboard.writeText(text); + } + + export function colorString(color: ColorResult) { + return color.hex.startsWith('#') && color.hex.length < 8 ? color.hex + (color.rgb.a ? decimalToHexString(Math.round(color.rgb.a * 255)) : 'ff') : color.hex; + } + + export function fromRGBAstr(rgba: string) { + const rm = rgba.match(/rgb[a]?\(([ 0-9]+)/); + const r = rm ? Number(rm[1]) : 0; + const gm = rgba.match(/rgb[a]?\([ 0-9]+,([ 0-9]+)/); + const g = gm ? Number(gm[1]) : 0; + const bm = rgba.match(/rgb[a]?\([ 0-9]+,[ 0-9]+,([ 0-9]+)/); + const b = bm ? Number(bm[1]) : 0; + const am = rgba.match(/rgba?\([ 0-9]+,[ 0-9]+,[ 0-9]+,([ .0-9]+)/); + const a = am ? Number(am[1]) : 1; + return { r: r, g: g, b: b, a: a }; + } + + const isTransparentFunctionHack = 'isTransparent(__value__)'; + export const noRecursionHack = '__noRecursion'; + + // special case filters + export const noDragDocsFilter = 'noDragDocs::any::check'; + export const TransparentBackgroundFilter = `backgroundColor::${isTransparentFunctionHack},${noRecursionHack}::check`; // bcz: hack. noRecursion should probably be either another ':' delimited field, or it should be a modifier to the comparision (eg., check, x, etc) field + export const OpaqueBackgroundFilter = `backgroundColor::${isTransparentFunctionHack},${noRecursionHack}::x`; // bcz: hack. noRecursion should probably be either another ':' delimited field, or it should be a modifier to the comparision (eg., check, x, etc) field + + export function IsRecursiveFilter(val: string) { + return !val.includes(noRecursionHack); + } + export function HasFunctionFilter(val: string) { + if (val.includes(isTransparentFunctionHack)) return (color: string) => color !== '' && DashColor(color).alpha() !== 1; + // add other function filters here... + return undefined; + } + + export function toRGBAstr(col: { r: number; g: number; b: number; a?: number }) { + return 'rgba(' + col.r + ',' + col.g + ',' + col.b + (col.a !== undefined ? ',' + col.a : '') + ')'; + } + + export function HSLtoRGB(h: number, s: number, l: number) { + // Must be fractions of 1 + // s /= 100; + // l /= 100; + + const c = (1 - Math.abs(2 * l - 1)) * s; + const x = c * (1 - Math.abs(((h / 60) % 2) - 1)); + const m = l - c / 2; + let r = 0; + let g = 0; + let b = 0; + if (h >= 0 && h < 60) { + r = c; + g = x; + b = 0; + } else if (h >= 60 && h < 120) { + r = x; + g = c; + b = 0; + } else if (h >= 120 && h < 180) { + r = 0; + g = c; + b = x; + } else if (h >= 180 && h < 240) { + r = 0; + g = x; + b = c; + } else if (h >= 240 && h < 300) { + r = x; + g = 0; + b = c; + } else if (h >= 300 && h < 360) { + r = c; + g = 0; + b = x; + } + r = Math.round((r + m) * 255); + g = Math.round((g + m) * 255); + b = Math.round((b + m) * 255); + return { r: r, g: g, b: b }; + } + + export function RGBToHSL(red: number, green: number, blue: number) { + // Make r, g, and b fractions of 1 + const r = red / 255; + const g = green / 255; + const b = blue / 255; + + // Find greatest and smallest channel values + const cmin = Math.min(r, g, b); + const cmax = Math.max(r, g, b); + const delta = cmax - cmin; + let h = 0; + let s = 0; + let l = 0; + // Calculate hue + + // No difference + if (delta === 0) h = 0; + // Red is max + else if (cmax === r) h = ((g - b) / delta) % 6; + // Green is max + else if (cmax === g) h = (b - r) / delta + 2; + // Blue is max + else h = (r - g) / delta + 4; + + h = Math.round(h * 60); + + // Make negative hues positive behind 360° + if (h < 0) h += 360; // Calculate lightness + + l = (cmax + cmin) / 2; + + // Calculate saturation + s = delta === 0 ? 0 : delta / (1 - Math.abs(2 * l - 1)); + + // Multiply l and s by 100 + // s = +(s * 100).toFixed(1); + // l = +(l * 100).toFixed(1); + + return { h: h, s: s, l: l }; + } + + export function scrollIntoView(targetY: number, targetHgt: number, scrollTop: number, contextHgt: number, minSpacing: number, scrollHeight: number) { + if (!targetHgt) return targetY; // if there's no height, then assume that + if (scrollTop + contextHgt < Math.min(scrollHeight, targetY + minSpacing + targetHgt)) { + return Math.ceil(targetY + minSpacing + targetHgt - contextHgt); + } + if (scrollTop >= Math.max(0, targetY - minSpacing)) { + return Math.max(0, Math.floor(targetY - minSpacing)); + } + return undefined; + } + + export function GetClipboardText(): string { + const textArea = document.createElement('textarea'); + document.body.appendChild(textArea); + textArea.focus(); + textArea.select(); + + try { + document.execCommand('paste'); + } catch (err) { + /* empty */ + } + + const val = textArea.value; + document.body.removeChild(textArea); + return val; + } +} + +export function OmitKeys(obj: any, keys: string[], pattern?: string, addKeyFunc?: (dup: any) => void): { omit: any; extract: any } { + const omit: any = { ...obj }; + const extract: any = {}; + keys.forEach(key => { + extract[key] = omit[key]; + delete omit[key]; + }); + pattern && + Array.from(Object.keys(omit)) + .filter(key => key.match(pattern)) + .forEach(key => { + extract[key] = omit[key]; + delete omit[key]; + }); + addKeyFunc?.(omit); + return { omit, extract }; +} + +export function WithKeys(obj: any, keys: string[], addKeyFunc?: (dup: any) => void) { + const dup: any = {}; + keys.forEach(key => { + dup[key] = obj[key]; + }); + addKeyFunc && addKeyFunc(dup); + return dup; +} + +export function incrementTitleCopy(title: string) { + const numstr = title.match(/.*(\{([0-9]*)\})+/); + const copyNumStr = `{${1 + (numstr ? +numstr[2] : 0)}}`; + return (numstr ? title.replace(numstr[1], '') : title) + copyNumStr; +} + +const easeFunc = (transition: 'ease' | 'linear' | undefined, currentTime: number, start: number, change: number, duration: number) => { + if (transition === 'linear') { + const newCurrentTime = currentTime / duration; // currentTime / (duration / 2); + return start + newCurrentTime * change; + } + + let newCurrentTime = currentTime / (duration / 2); + if (newCurrentTime < 1) { + return (change / 2) * newCurrentTime * newCurrentTime + start; + } + + newCurrentTime -= 1; + return (-change / 2) * (newCurrentTime * (newCurrentTime - 2) - 1) + start; +}; + +export function smoothScroll(duration: number, element: HTMLElement | HTMLElement[], to: number, transition: 'ease' | 'linear' | undefined, stopper?: () => void) { + stopper?.(); + const elements = element instanceof HTMLElement ? [element] : element; + const starts = elements.map(element => element.scrollTop); + const startDate = new Date().getTime(); + let _stop = false; + const stop = () => { + _stop = true; + }; + const animateScroll = () => { + const currentDate = new Date().getTime(); + const currentTime = currentDate - startDate; + const setScrollTop = (element: HTMLElement, value: number) => { + element.scrollTop = value; + }; + if (!_stop) { + if (currentTime < duration) { + elements.forEach((element, i) => currentTime && setScrollTop(element, easeFunc(transition, Math.min(currentTime, duration), starts[i], to - starts[i], duration))); + requestAnimationFrame(animateScroll); + } else { + elements.forEach(element => setScrollTop(element, to)); + } + } + }; + animateScroll(); + return stop; +} + +export function smoothScrollHorizontal(duration: number, element: HTMLElement | HTMLElement[], to: number) { + const elements = element instanceof HTMLElement ? [element] : element; + const starts = elements.map(element => element.scrollLeft); + const startDate = new Date().getTime(); + + const animateScroll = () => { + const currentDate = new Date().getTime(); + const currentTime = currentDate - startDate; + elements.forEach((element, i) => { + element.scrollLeft = easeFunc('ease', currentTime, starts[i], to - starts[i], duration); + }); + + if (currentTime < duration) { + requestAnimationFrame(animateScroll); + } else { + elements.forEach(element => { + element.scrollLeft = to; + }); + } + }; + animateScroll(); +} + +export function addStyleSheet(styleType: string = 'text/css') { + const style = document.createElement('style'); + style.type = styleType; + const sheets = document.head.appendChild(style); + return (sheets as any).sheet; +} +export function addStyleSheetRule(sheet: any, selector: any, css: any, selectorPrefix = '.') { + const propText = + typeof css === 'string' + ? css + : Object.keys(css) + .map(p => p + ':' + (p === 'content' ? "'" + css[p] + "'" : css[p])) + .join(';'); + return sheet.insertRule(selectorPrefix + selector + '{' + propText + '}', sheet.cssRules.length); +} +export function removeStyleSheetRule(sheet: any, rule: number) { + if (sheet.rules.length) { + sheet.removeRule(rule); + return true; + } + return false; +} +export function clearStyleSheetRules(sheet: any) { + if (sheet.rules.length) { + numberRange(sheet.rules.length).map(() => sheet.removeRule(0)); + return true; + } + return false; +} + +export function simulateMouseClick(element: Element | null | undefined, x: number, y: number, sx: number, sy: number, rightClick = true) { + if (!element) return; + ['pointerdown', 'pointerup'].forEach(event => { + const me = new PointerEvent(event, { + view: window, + bubbles: true, + cancelable: true, + button: 2, + pointerType: 'mouse', + clientX: x, + clientY: y, + screenX: sx, + screenY: sy, + }); + (me as any).dash = true; + element.dispatchEvent(me); + }); + + if (rightClick) { + const me = new MouseEvent('contextmenu', { + view: window, + bubbles: true, + cancelable: true, + button: 2, + clientX: x, + clientY: y, + movementX: 0, + movementY: 0, + screenX: sx, + screenY: sy, + }); + (me as any).dash = true; + element.dispatchEvent(me); + } +} + +export function getWordAtPoint(elem: any, x: number, y: number): string | undefined { + if (elem.tagName === 'INPUT') return 'input'; + if (elem.tagName === 'TEXTAREA') return 'textarea'; + if (elem.nodeType === elem.TEXT_NODE) { + const range = elem.ownerDocument.createRange(); + range.selectNodeContents(elem); + let currentPos = 0; + const endPos = range.endOffset; + while (currentPos + 1 <= endPos) { + range.setStart(elem, currentPos); + range.setEnd(elem, currentPos + 1); + const rangeRect = range.getBoundingClientRect(); + if (rangeRect.left <= x && rangeRect.right >= x && rangeRect.top <= y && rangeRect.bottom >= y) { + range.expand?.('word'); // doesn't exist in firefox + const ret = range.toString(); + range.detach(); + return ret; + } + currentPos += 1; + } + } else { + Array.from(elem.childNodes).forEach((childNode: any) => { + const range = childNode.ownerDocument.createRange(); + range.selectNodeContents(childNode); + const rangeRect = range.getBoundingClientRect(); + if (rangeRect.left <= x && rangeRect.right >= x && rangeRect.top <= y && rangeRect.bottom >= y) { + range.detach(); + const word = getWordAtPoint(childNode, x, y); + if (word) return word; + } else { + range.detach(); + } + return undefined; + }); + } + return undefined; +} + +export function isTargetChildOf(ele: HTMLDivElement | null, target: Element | null) { + let entered = false; + for (let child = target; !entered && child; child = child.parentElement) { + entered = child === ele; + } + return entered; +} + +export function StopEvent(e: React.PointerEvent | React.MouseEvent | React.KeyboardEvent) { + e.stopPropagation(); + e.preventDefault(); +} + +export function setupMoveUpEvents( + target: object, + e: React.PointerEvent, + moveEvent: (e: PointerEvent, down: number[], delta: number[]) => boolean, + upEvent: (e: PointerEvent, movement: number[], isClick: boolean) => any, + clickEvent: (e: PointerEvent, doubleTap?: boolean) => any, + // eslint-disable-next-line default-param-last + stopPropagation: boolean = true, + // eslint-disable-next-line default-param-last + stopMovePropagation: boolean = true, + noDoubleTapTimeout?: () => void +) { + const targetAny = target as any; + const doubleTapTimeout = 300; + targetAny._doubleTap = Date.now() - (target as any)._lastTap < doubleTapTimeout; + targetAny._lastTap = Date.now(); + targetAny._downX = targetAny._lastX = e.clientX; + targetAny._downY = targetAny._lastY = e.clientY; + targetAny._noClick = false; + let moving = false; + + const _moveEvent = (e: PointerEvent): void => { + if (moving || Math.abs(e.clientX - (target as any)._downX) > ClientUtils.DRAG_THRESHOLD || Math.abs(e.clientY - (target as any)._downY) > ClientUtils.DRAG_THRESHOLD) { + moving = true; + if ((target as any)._doubleTime) { + clearTimeout((target as any)._doubleTime); + targetAny._doubleTime = undefined; + } + if (moveEvent(e, [(target as any)._downX, (target as any)._downY], [e.clientX - (target as any)._lastX, e.clientY - (target as any)._lastY])) { + document.removeEventListener('pointermove', _moveEvent); + // eslint-disable-next-line no-use-before-define + document.removeEventListener('pointerup', _upEvent); + } + } + targetAny._lastX = e.clientX; + targetAny._lastY = e.clientY; + stopMovePropagation && e.stopPropagation(); + }; + const _upEvent = (e: PointerEvent): void => { + const isClick = Math.abs(e.clientX - targetAny._downX) < 4 && Math.abs(e.clientY - targetAny._downY) < 4; + upEvent(e, [e.clientX - targetAny._downX, e.clientY - targetAny._downY], isClick); + if (isClick) { + if (!targetAny._doubleTime && noDoubleTapTimeout) { + targetAny._doubleTime = setTimeout(() => { + noDoubleTapTimeout?.(); + targetAny._doubleTime = undefined; + }, doubleTapTimeout); + } + if (targetAny._doubleTime && targetAny._doubleTap) { + clearTimeout(targetAny._doubleTime); + targetAny._doubleTime = undefined; + } + targetAny._noClick = clickEvent(e, targetAny._doubleTap); + } + document.removeEventListener('pointermove', _moveEvent); + document.removeEventListener('pointerup', _upEvent, true); + }; + const _clickEvent = (e: MouseEvent): void => { + if ((target as any)._noClick) e.stopPropagation(); + document.removeEventListener('click', _clickEvent, true); + }; + if (stopPropagation) { + e.stopPropagation(); + e.preventDefault(); + } + document.addEventListener('pointermove', _moveEvent); + document.addEventListener('pointerup', _upEvent, true); + document.addEventListener('click', _clickEvent, true); +} + +export function DivHeight(ele: HTMLElement): number { + return Number(getComputedStyle(ele).height.replace('px', '')); +} +export function DivWidth(ele: HTMLElement): number { + return Number(getComputedStyle(ele).width.replace('px', '')); +} + +export function dateRangeStrToDates(dateStr: string) { + // dateStr in yyyy-mm-dd format + const dateRangeParts = dateStr.split('|'); // splits into from and to date + const fromParts = dateRangeParts[0].split('-'); + const toParts = dateRangeParts[1].split('-'); + + const fromYear = parseInt(fromParts[0]); + const fromMonth = parseInt(fromParts[1]) - 1; + const fromDay = parseInt(fromParts[2]); + + const toYear = parseInt(toParts[0]); + const toMonth = parseInt(toParts[1]) - 1; + const toDay = parseInt(toParts[2]); + + return [new Date(fromYear, fromMonth, fromDay), new Date(toYear, toMonth, toDay)]; +} diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts index 804e6d1d7..b833d3287 100644 --- a/src/client/DocServer.ts +++ b/src/client/DocServer.ts @@ -102,7 +102,7 @@ export namespace DocServer { } export function getFieldWriteMode(field: string) { - return ClientUtils.CurrentUserEmail === 'guest' ? WriteMode.LivePlayground : fieldWriteModes[field] || WriteMode.Default; + return ClientUtils.CurrentUserEmail() === 'guest' ? WriteMode.LivePlayground : fieldWriteModes[field] || WriteMode.Default; } export function registerDocWithCachedUpdate(doc: Doc, field: string, oldValue: any) { @@ -463,7 +463,7 @@ export namespace DocServer { function _CreateFieldImpl(field: RefField) { _cache[field[Id]] = field; const initialState = SerializationHelper.Serialize(field); - ClientUtils.CurrentUserEmail !== 'guest' && DocServer.Emit(_socket, MessageStore.CreateField, initialState); + ClientUtils.CurrentUserEmail() !== 'guest' && DocServer.Emit(_socket, MessageStore.CreateField, initialState); } // NOTIFY THE SERVER OF AN UPDATE TO A DOC'S STATE @@ -481,7 +481,7 @@ export namespace DocServer { } function _UpdateFieldImpl(id: string, diff: any) { - !DocServer.Control.isReadOnly() && ClientUtils.CurrentUserEmail !== 'guest' && DocServer.Emit(_socket, MessageStore.UpdateField, { id, diff }); + !DocServer.Control.isReadOnly() && ClientUtils.CurrentUserEmail() !== 'guest' && DocServer.Emit(_socket, MessageStore.UpdateField, { id, diff }); } function _respondToUpdateImpl(diff: any) { @@ -516,11 +516,11 @@ export namespace DocServer { } export function DeleteDocument(id: string) { - ClientUtils.CurrentUserEmail !== 'guest' && DocServer.Emit(_socket, MessageStore.DeleteField, id); + ClientUtils.CurrentUserEmail() !== 'guest' && DocServer.Emit(_socket, MessageStore.DeleteField, id); } export function DeleteDocuments(ids: string[]) { - ClientUtils.CurrentUserEmail !== 'guest' && DocServer.Emit(_socket, MessageStore.DeleteFields, ids); + ClientUtils.CurrentUserEmail() !== 'guest' && DocServer.Emit(_socket, MessageStore.DeleteFields, ids); } function _respondToDeleteImpl(ids: string | string[]) { diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index d41a96db2..f827ea81c 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -970,7 +970,7 @@ export namespace Docs { dataProps['acl-Guest'] = options['acl-Guest'] ?? (Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.View); dataProps.isSystem = viewProps.isSystem; dataProps.isDataDoc = true; - dataProps.author = ClientUtils.CurrentUserEmail; + dataProps.author = ClientUtils.CurrentUserEmail(); dataProps.author_date = new DateField(); if (fieldKey) { dataProps[`${fieldKey}_modificationDate`] = new DateField(); @@ -990,7 +990,7 @@ export namespace Docs { } if (!noView) { - const viewFirstProps: { [id: string]: any } = { author: ClientUtils.CurrentUserEmail }; + const viewFirstProps: { [id: string]: any } = { author: ClientUtils.CurrentUserEmail() }; viewFirstProps['acl-Guest'] = options['_acl-Guest'] ?? (Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.View); let viewDoc: Doc; // determines whether viewDoc should be created using placeholder Doc or default @@ -1330,7 +1330,7 @@ export namespace Docs { const doc = DockDocument( configs.map(c => c.doc), JSON.stringify(layoutConfig), - ClientUtils.CurrentUserEmail === 'guest' ? options : { 'acl-Guest': SharingPermissions.View, ...options }, + ClientUtils.CurrentUserEmail() === 'guest' ? options : { 'acl-Guest': SharingPermissions.View, ...options }, id ); configs.forEach(c => { @@ -1722,7 +1722,7 @@ export namespace DocUtils { event: undoable(() => { const newDoc = DocUtils.copyDragFactory(dragDoc); if (newDoc) { - newDoc.author = ClientUtils.CurrentUserEmail; + newDoc.author = ClientUtils.CurrentUserEmail(); newDoc.x = x; newDoc.y = y; EquationBox.SelectOnLoad = newDoc[Id]; @@ -1773,7 +1773,7 @@ export namespace DocUtils { event: undoable(() => { const newDoc = DocUtils.delegateDragFactory(dragDoc); if (newDoc) { - newDoc.author = ClientUtils.CurrentUserEmail; + newDoc.author = ClientUtils.CurrentUserEmail(); newDoc.x = x; newDoc.y = y; EquationBox.SelectOnLoad = newDoc[Id]; diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 535f7cf9d..27ae5c9a0 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -586,7 +586,7 @@ pie title Minerals in my tap water /// initializes the left sidebar panel view of the UserDoc static setupUserDocView(doc: Doc, field:string) { const reqdOpts:DocumentOptions = { - _lockedPosition: true, _gridGap: 5, _forceActive: true, title: ClientUtils.CurrentUserEmail +"-view", + _lockedPosition: true, _gridGap: 5, _forceActive: true, title: ClientUtils.CurrentUserEmail() +"-view", layout_boxShadow: "0 0", childDontRegisterViews: true, dropAction: dropActionType.same, ignoreClick: true, isSystem: true, treeView_HideTitle: true, treeView_TruncateTitleWidth: 350 }; @@ -832,8 +832,8 @@ pie title Minerals in my tap water static setupLinkDocs(doc: Doc, linkDatabaseId: string) { if (!(CurrentUserUtils.newAccount ? undefined : DocCast(doc.myLinkDatabase))) { const linkDocs = new Doc(linkDatabaseId, true); - linkDocs.title = "LINK DATABASE: " + ClientUtils.CurrentUserEmail; - linkDocs.author = ClientUtils.CurrentUserEmail; + linkDocs.title = "LINK DATABASE: " + ClientUtils.CurrentUserEmail(); + linkDocs.author = ClientUtils.CurrentUserEmail(); linkDocs.isSystem = true; linkDocs.data = new List([]); linkDocs["acl-Guest"] = SharingPermissions.Augment; @@ -893,11 +893,11 @@ pie title Minerals in my tap water reaction(() => DateCast(DocCast(doc.globalGroupDatabase).data_modificationDate), async () => { const groups = await DocListCastAsync(DocCast(doc.globalGroupDatabase).data); - const mygroups = groups?.filter(group => JSON.parse(StrCast(group.members)).includes(ClientUtils.CurrentUserEmail)) || []; + const mygroups = groups?.filter(group => JSON.parse(StrCast(group.members)).includes(ClientUtils.CurrentUserEmail())) || []; SetCachedGroups(["Guest", ...(mygroups?.map(g => StrCast(g.title))??[])]); }, { fireImmediately: true }); doc.isSystem ?? (doc.isSystem = true); - doc.title ?? (doc.title = ClientUtils.CurrentUserEmail); + doc.title ?? (doc.title = ClientUtils.CurrentUserEmail()); Doc.noviceMode ?? (Doc.noviceMode = true); doc._showLabel ?? (doc._showLabel = true); doc.textAlign ?? (doc.textAlign = "left"); @@ -975,7 +975,7 @@ pie title Minerals in my tap water if (response) { const result: { version: string, userDocumentId: string, sharingDocumentId: string, linkDatabaseId: string, email: string, cacheDocumentIds: string, resolvedPorts: string } = JSON.parse(response); runInAction(() => { CurrentUserUtils.ServerVersion = result.version; }); - ClientUtils.CurrentUserEmail = result.email; + ClientUtils.SetCurrentUserEmail(result.email); resolvedPorts = result.resolvedPorts as any; DocServer.init(window.location.protocol, window.location.hostname, resolvedPorts?.socket, result.email); if (result.cacheDocumentIds) @@ -1004,7 +1004,7 @@ pie title Minerals in my tap water const userDoc = CurrentUserUtils.newAccount ? new Doc(info.userDocumentId, true) : field as Doc; this.updateUserDocument(Doc.SetUserDoc(userDoc), info.sharingDocumentId, info.linkDatabaseId); if (CurrentUserUtils.newAccount) { - if (ClientUtils.CurrentUserEmail === "guest") { + if (ClientUtils.CurrentUserEmail() === "guest") { DashboardView.createNewDashboard(undefined, "guest dashboard"); } else { userDoc.activePage = "home"; diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index 67a61f17e..9a7786125 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -321,7 +321,7 @@ export class DocumentManager { if (options.toggleTarget && (!options.didMove || docView.Document.hidden)) docView.Document.hidden = !docView.Document.hidden; if (options.effect) docView.Document[Animation] = options.effect; - if (options.zoomTextSelections && Doc.UnhighlightTimer && contextView && targetDoc.text_html) { + if (options.zoomTextSelections && Doc.IsUnhighlightTimerSet() && contextView && targetDoc.text_html) { // if the docView is a text anchor, the contextView is the PDF/Web/Text doc contextView.setTextHtmlOverlay(StrCast(targetDoc.text_html), options.effect); DocumentManager._overlayViews.add(contextView); diff --git a/src/client/util/GroupManager.tsx b/src/client/util/GroupManager.tsx index 6b20b885b..c261c0f1e 100644 --- a/src/client/util/GroupManager.tsx +++ b/src/client/util/GroupManager.tsx @@ -136,7 +136,7 @@ export class GroupManager extends ObservableReactComponent<{}> { hasEditAccess(groupDoc: Doc): boolean { if (!groupDoc) return false; const accessList: string[] = JSON.parse(StrCast(groupDoc.owners)); - return accessList.includes(ClientUtils.CurrentUserEmail) || this.adminGroupMembers?.includes(ClientUtils.CurrentUserEmail); + return accessList.includes(ClientUtils.CurrentUserEmail()) || this.adminGroupMembers?.includes(ClientUtils.CurrentUserEmail()); } /** @@ -148,7 +148,7 @@ export class GroupManager extends ObservableReactComponent<{}> { const name = groupName.toLowerCase() === 'admin' ? 'Admin' : groupName; const groupDoc = new Doc('GROUP:' + name, true); groupDoc.title = name; - groupDoc.owners = JSON.stringify([ClientUtils.CurrentUserEmail]); + groupDoc.owners = JSON.stringify([ClientUtils.CurrentUserEmail()]); groupDoc.members = JSON.stringify(memberEmails); this.addGroup(groupDoc); } @@ -177,7 +177,7 @@ export class GroupManager extends ObservableReactComponent<{}> { Doc.RemoveDocFromList(this.GroupManagerDoc, 'data', group); SharingManager.Instance.removeGroup(group); const members = JSON.parse(StrCast(group.members)); - if (members.includes(ClientUtils.CurrentUserEmail)) { + if (members.includes(ClientUtils.CurrentUserEmail())) { const index = DocListCast(this.GroupManagerDoc.data).findIndex(grp => grp === group); index !== -1 && Cast(this.GroupManagerDoc.data, listSpec(Doc), [])?.splice(index, 1); } diff --git a/src/client/util/HypothesisUtils.ts b/src/client/util/HypothesisUtils.ts index d1600468a..48d3ac046 100644 --- a/src/client/util/HypothesisUtils.ts +++ b/src/client/util/HypothesisUtils.ts @@ -33,7 +33,7 @@ export namespace Hypothesis { // await SearchUtil.Search('web', true).then( // action(async (res: SearchUtil.DocSearchResult) => { // const docs = res.docs; - // const filteredDocs = docs.filter(doc => doc.author === ClientUtils.CurrentUserEmail && doc.type === DocumentType.WEB && doc.data); + // const filteredDocs = docs.filter(doc => doc.author === ClientUtils.CurrentUserEmail() && doc.type === DocumentType.WEB && doc.data); // filteredDocs.forEach(doc => { // uri === Cast(doc.data, WebField)?.url.href && results.push(doc); // TODO check visited sites history? // }); diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx index e2971895a..f983c29b7 100644 --- a/src/client/util/SettingsManager.tsx +++ b/src/client/util/SettingsManager.tsx @@ -128,7 +128,7 @@ export class SettingsManager extends React.Component<{}> { if (this.playgroundMode) { DocServer.Control.makeReadOnly(); addStyleSheetRule(SettingsManager._settingsStyle, 'topbar-inner-container', { background: 'red !important' }); - } else ClientUtils.CurrentUserEmail !== 'guest' && DocServer.Control.makeEditable(); + } else ClientUtils.CurrentUserEmail() !== 'guest' && DocServer.Control.makeEditable(); }); @undoBatch @@ -537,7 +537,7 @@ export class SettingsManager extends React.Component<{}> {
{DashVersion}
- {ClientUtils.CurrentUserEmail} + {ClientUtils.CurrentUserEmail()}
diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx index 03f7e9b71..ade4cc218 100644 --- a/src/client/util/SharingManager.tsx +++ b/src/client/util/SharingManager.tsx @@ -137,7 +137,7 @@ export class SharingManager extends React.Component<{}> { if (!this.populating && Doc.UserDoc()[Id] !== Utils.GuestID()) { this.populating = true; const userList = await RequestPromise.get(ClientUtils.prepend('/getUsers')); - const raw = (JSON.parse(userList) as User[]).filter(user => user.email !== 'guest' && user.email !== ClientUtils.CurrentUserEmail); + const raw = (JSON.parse(userList) as User[]).filter(user => user.email !== 'guest' && user.email !== ClientUtils.CurrentUserEmail()); runInAction(() => { FieldLoader.ServerLoadStatus.message = 'users'; }); @@ -234,7 +234,7 @@ export class SharingManager extends React.Component<{}> { shareFromPropertiesSidebar = undoable((shareWith: string, permission: SharingPermissions, docs: Doc[], layout: boolean) => { if (layout) this.layoutDocAcls = true; if (shareWith !== 'Guest') { - const user = this.users.find(({ user: { email } }) => email === (shareWith === 'Me' ? ClientUtils.CurrentUserEmail : shareWith)); + const user = this.users.find(({ user: { email } }) => email === (shareWith === 'Me' ? ClientUtils.CurrentUserEmail() : shareWith)); docs.forEach(doc => { if (user) this.setInternalSharing(user, permission, doc); else this.setInternalGroupSharing(GroupManager.Instance.getGroup(shareWith)!, permission, doc, undefined, true); @@ -499,19 +499,19 @@ export class SharingManager extends React.Component<{}> { const sameAuthor = docs.every(doc => doc?.author === docs[0]?.author); // the owner of the doc and the current user are placed at the top of the user list. - const userKey = `acl-${normalizeEmail(ClientUtils.CurrentUserEmail)}`; + const userKey = `acl-${normalizeEmail(ClientUtils.CurrentUserEmail())}`; const curUserPermission = StrCast(targetDoc[userKey]); // const curUserPermission = HierarchyMapping.get(effectiveAcls[0])!.name userListContents.unshift( sameAuthor ? (
- {targetDoc?.author === ClientUtils.CurrentUserEmail ? 'Me' : StrCast(targetDoc?.author)} + {targetDoc?.author === ClientUtils.CurrentUserEmail() ? 'Me' : StrCast(targetDoc?.author)}
Owner
) : null, - sameAuthor && targetDoc?.author !== ClientUtils.CurrentUserEmail ? ( + sameAuthor && targetDoc?.author !== ClientUtils.CurrentUserEmail() ? (
Me
diff --git a/src/client/util/reportManager/ReportManager.tsx b/src/client/util/reportManager/ReportManager.tsx index d6fd339a9..02b3ee32c 100644 --- a/src/client/util/reportManager/ReportManager.tsx +++ b/src/client/util/reportManager/ReportManager.tsx @@ -134,7 +134,7 @@ export class ReportManager extends React.Component<{}> { const req = await this.octokit.request('POST /repos/{owner}/{repo}/issues', { owner: 'brown-dash', repo: 'Dash-Web', - title: formatTitle(this.formData.title, ClientUtils.CurrentUserEmail), + title: formatTitle(this.formData.title, ClientUtils.CurrentUserEmail()), body: `${this.formData.description} ${formattedLinks.length > 0 ? `\n\nFiles:\n${formattedLinks.join('\n')}` : ''}`, labels: ['from-dash-app', this.formData.type, this.formData.priority], }); diff --git a/src/client/views/DashboardView.tsx b/src/client/views/DashboardView.tsx index 1d286c987..14abd5f89 100644 --- a/src/client/views/DashboardView.tsx +++ b/src/client/views/DashboardView.tsx @@ -64,7 +64,7 @@ export class DashboardView extends ObservableReactComponent<{}> { getDashboards = (whichGroup: DashboardGroup) => { if (whichGroup === DashboardGroup.MyDashboards) { - return DocListCast(Doc.MyDashboards.data).filter(dashboard => dashboard[DocData].author === ClientUtils.CurrentUserEmail); + return DocListCast(Doc.MyDashboards.data).filter(dashboard => dashboard[DocData].author === ClientUtils.CurrentUserEmail()); } return DocListCast(Doc.MySharedDocs.data_dashboards).filter(doc => doc.dockingConfig); }; @@ -155,7 +155,7 @@ export class DashboardView extends ObservableReactComponent<{}> { : this.getDashboards(this.selectedDashboardGroup).map(dashboard => { const href = ImageCast(dashboard.thumb)?.url?.href; const shared = Object.keys(dashboard[DocAcl]) - .filter(key => key !== `acl-${normalizeEmail(ClientUtils.CurrentUserEmail)}` && !['acl-Me', 'acl-Guest'].includes(key)) + .filter(key => key !== `acl-${normalizeEmail(ClientUtils.CurrentUserEmail())}` && !['acl-Me', 'acl-Guest'].includes(key)) .some(key => dashboard[DocAcl][key] !== AclPrivate); return (
{ } else if (doc.readOnly) { DocServer.Control.makeReadOnly(); } else { - ClientUtils.CurrentUserEmail !== 'guest' && DocServer.Control.makeEditable(); + ClientUtils.CurrentUserEmail() !== 'guest' && DocServer.Control.makeEditable(); } } diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 8a060d0e0..13945cacf 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -122,7 +122,7 @@ export class MainView extends ObservableReactComponent<{}> { } @observable mainDoc: Opt = undefined; @computed private get mainContainer() { - if (window.location.pathname.startsWith('/doc/') && ClientUtils.CurrentUserEmail === 'guest') { + if (window.location.pathname.startsWith('/doc/') && ClientUtils.CurrentUserEmail() === 'guest') { DocServer.GetRefField(window.location.pathname.substring('/doc/'.length)).then(main => runInAction(() => { this.mainDoc = main as Doc; diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx index 77e68866e..2a847e51a 100644 --- a/src/client/views/PropertiesView.tsx +++ b/src/client/views/PropertiesView.tsx @@ -405,7 +405,7 @@ export class PropertiesView extends ObservableReactComponent, props: Opt(moreProps?: X) { let ind; const doc = this.Document; const id = Doc.UserDoc()[Id]; - const email = ClientUtils.CurrentUserEmail; + const email = ClientUtils.CurrentUserEmail(); const pos = { x: position[0], y: position[1] }; if (id && email) { const proto = Doc.GetProto(doc); diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx index e266ccd14..20323a521 100644 --- a/src/client/views/collections/TreeView.tsx +++ b/src/client/views/collections/TreeView.tsx @@ -229,7 +229,7 @@ export class TreeView extends ObservableReactComponent { this.treeViewOpen = !this.treeViewOpen; } else { // choose an appropriate embedding or make one. --- choose the first embedding that (1) user owns, (2) has no context field ... otherwise make a new embedding - const bestEmbedding = docView.Document.author === ClientUtils.CurrentUserEmail && !Doc.IsDataProto(docView.Document) ? docView.Document : Doc.BestEmbedding(docView.Document); + const bestEmbedding = docView.Document.author === ClientUtils.CurrentUserEmail() && !Doc.IsDataProto(docView.Document) ? docView.Document : Doc.BestEmbedding(docView.Document); this._props.addDocTab(bestEmbedding, OpenWhere.lightbox); } }; diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index dbd2ebe0d..62a3e2467 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -1512,7 +1512,7 @@ export class DocumentView extends DocComponent LightboxView.Instance.SetLightboxDoc( diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index c7543560d..a82f025f9 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -580,7 +580,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { const view = this._editorView!; - const nmark = view.state.schema.marks.user_mark.create({ ...mark.attrs, userid: ClientUtils.CurrentUserEmail }); + const nmark = view.state.schema.marks.user_mark.create({ ...mark.attrs, userid: ClientUtils.CurrentUserEmail() }); view.dispatch(view.state.tr.removeMark(start, end, nmark).addMark(start, end, nmark)); }; protected createDropTarget = (ele: HTMLDivElement) => { @@ -722,7 +722,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-min-' + (min - i), { opacity: ((10 - i - 1) / 10).toString() })); } if (highlights.includes('By Recent Hour')) { - addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-' + ClientUtils.CurrentUserEmail.replace('.', '').replace('@', ''), { opacity: '0.1' }); + addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-' + ClientUtils.CurrentUserEmail().replace('.', '').replace('@', ''), { opacity: '0.1' }); const hr = Math.round(Date.now() / 1000 / 60 / 60); numberRange(10).map(i => addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-hr-' + (hr - i), { opacity: ((10 - i - 1) / 10).toString() })); } @@ -1489,7 +1489,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent m.type !== mark.type), mark]; const tr1 = this._editorView.state.tr.setStoredMarks(storedMarks); @@ -1503,7 +1503,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent m.type !== mark.type), mark]; const { tr } = this._editorView.state; @@ -1534,7 +1534,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent i && node?.marks?.().some(mark => mark.type === schema.marks.user_mark && mark.attrs.userid !== ClientUtils.CurrentUserEmail) && [AclAugment, AclSelfEdit].includes(GetEffectiveAcl(this.Document))) { + if (state.doc.content.size - 1 > i && node?.marks?.().some(mark => mark.type === schema.marks.user_mark && mark.attrs.userid !== ClientUtils.CurrentUserEmail()) && [AclAugment, AclSelfEdit].includes(GetEffectiveAcl(this.Document))) { e.preventDefault(); } } @@ -1799,7 +1799,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent m.type === schema.marks.user_mark && m.attrs.modified === modified); - _editorView.dispatch(state.tr.removeStoredMark(schema.marks.user_mark).addStoredMark(mark ?? schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail, modified }))); + _editorView.dispatch(state.tr.removeStoredMark(schema.marks.user_mark).addStoredMark(mark ?? schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified }))); } break; } diff --git a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx index 533fefa09..52d93ec38 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx @@ -10,7 +10,7 @@ import './FormattedTextBoxComment.scss'; import { schema } from './schema_rts'; export function findOtherUserMark(marks: readonly Mark[]): Mark | undefined { - return marks.find(m => m.attrs.userid && m.attrs.userid !== ClientUtils.CurrentUserEmail); + return marks.find(m => m.attrs.userid && m.attrs.userid !== ClientUtils.CurrentUserEmail()); } export function findUserMark(marks: readonly Mark[]): Mark | undefined { return marks.find(m => m.attrs.userid); diff --git a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts index e9ed2549e..c3a5a2c86 100644 --- a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts +++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts @@ -51,8 +51,8 @@ export function buildKeymap>(schema: S, props: any, mapKey case AclAugment: { const prevNode = state.selection.$cursor.nodeBefore; - const prevUser = !prevNode ? ClientUtils.CurrentUserEmail : prevNode.marks[prevNode.marks.length - 1].attrs.userid; - if (prevUser !== ClientUtils.CurrentUserEmail) { + const prevUser = !prevNode ? ClientUtils.CurrentUserEmail() : prevNode.marks[prevNode.marks.length - 1].attrs.userid; + if (prevUser !== ClientUtils.CurrentUserEmail()) { return false; } } diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts index 78ea99592..5b5617484 100644 --- a/src/client/views/nodes/formattedText/RichTextRules.ts +++ b/src/client/views/nodes/formattedText/RichTextRules.ts @@ -433,8 +433,8 @@ export class RichTextRules { return node ? state.tr .removeMark(start, end, schema.marks.user_mark) - .addMark(start, end, schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) })) - .addMark(start, end, schema.marks.user_tag.create({ userid: ClientUtils.CurrentUserEmail, tag: tag, modified: Math.round(Date.now() / 1000 / 60) })) + .addMark(start, end, schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified: Math.floor(Date.now() / 1000) })) + .addMark(start, end, schema.marks.user_tag.create({ userid: ClientUtils.CurrentUserEmail(), tag: tag, modified: Math.round(Date.now() / 1000 / 60) })) : state.tr; }), diff --git a/src/client/views/nodes/formattedText/marks_rts.ts b/src/client/views/nodes/formattedText/marks_rts.ts index 0f3306bef..d1c7b72a5 100644 --- a/src/client/views/nodes/formattedText/marks_rts.ts +++ b/src/client/views/nodes/formattedText/marks_rts.ts @@ -335,7 +335,7 @@ export const marks: { [index: string]: MarkSpec } = { const min = Math.round(node.attrs.modified / 60); const hr = Math.round(min / 60); const day = Math.round(hr / 60 / 24); - const remote = node.attrs.userid !== ClientUtils.CurrentUserEmail ? ' UM-remote' : ''; + const remote = node.attrs.userid !== ClientUtils.CurrentUserEmail() ? ' UM-remote' : ''; return ['span', { class: 'UM-' + uid + remote + ' UM-min-' + min + ' UM-hr-' + hr + ' UM-day-' + day }, 0]; }, }, diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index 3fb914423..f8351c238 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -152,7 +152,7 @@ export const ReverseHierarchyMap: Map { key.startsWith('acl') && (permissions[key] = ReverseHierarchyMap.get(StrCast(target[key]))!.acl); }); @@ -301,15 +301,16 @@ export class Doc extends RefField { private set __fieldTuples(value) { // called by deserializer to set all fields in one shot this[FieldTuples] = value; - // eslint-disable-next-line no-restricted-syntax - for (const key in value) { - const field = value[key]; - field !== undefined && (this[FieldKeys][key] = true); - if (field instanceof ObjectField) { - field[Parent] = this[Self]; - field[FieldChanged] = containedFieldChangedHandler(this[SelfProxy], key, field); + Object.keys(value).forEach(key => { + if (Object.prototype.hasOwnProperty.call(value, key)) { + const field = value[key]; + field !== undefined && (this[FieldKeys][key] = true); + if (field instanceof ObjectField) { + field[Parent] = this[Self]; + field[FieldChanged] = containedFieldChangedHandler(this[SelfProxy], key, field); + } } - } + }); } @observable private [FieldTuples]: any = {}; @@ -365,14 +366,11 @@ export class Doc extends RefField { public async [HandleUpdate](diff: any) { const set = diff.$set; - const sameAuthor = this.author === ClientUtils.CurrentUserEmail; - if (set) { - for (const key in set) { - const fprefix = 'fields.'; - if (!key.startsWith(fprefix)) { - // eslint-disable-next-line no-continue - continue; - } + const sameAuthor = this.author === ClientUtils.CurrentUserEmail(); + const fprefix = 'fields.'; + Object.keys(set ?? {}) + .filter(key => Object.prototype.hasOwnProperty.call(set, key) && key.startsWith(fprefix)) + .forEach(async key => { const fKey = key.substring(fprefix.length); const fn = async () => { const value = await SerializationHelper.Deserialize(set[key]); @@ -395,15 +393,11 @@ export class Doc extends RefField { } else { this[CachedUpdates][fKey] = fn; } - } - } + }); const unset = diff.$unset; - if (unset) { - for (const key in unset) { - if (!key.startsWith('fields.')) { - // eslint-disable-next-line no-continue - continue; - } + Object.keys(unset ?? {}) + .filter(key => Object.prototype.hasOwnProperty.call(unset, key) && key.startsWith(fprefix)) + .forEach(async key => { const fKey = key.substring(7); const fn = () => { this[UpdatingFromServer] = true; @@ -412,13 +406,11 @@ export class Doc extends RefField { }; if (sameAuthor || DocServer.getFieldWriteMode(fKey) !== DocServer.WriteMode.Playground) { delete this[CachedUpdates][fKey]; - // eslint-disable-next-line no-await-in-loop await fn(); } else { this[CachedUpdates][fKey] = fn; } - } - } + }); } } @@ -519,15 +511,15 @@ export namespace Doc { */ export function assign(doc: Doc, fields: Partial>>, skipUndefineds: boolean = false, isInitializing = false) { isInitializing && (doc[Initializing] = true); - for (const key in fields) { - if (fields.hasOwnProperty(key)) { - const value = fields[key]; + Object.keys(fields).forEach(key => { + if (Object.prototype.hasOwnProperty.call(fields.hasOwnProperty, key)) { + const value = (fields as any)[key]; if (!skipUndefineds || value !== undefined) { // Do we want to filter out undefineds? doc[key] = value; } } - } + }); isInitializing && (doc[Initializing] = false); return doc; } @@ -640,7 +632,7 @@ export namespace Doc { embedding.createdFrom = doc; embedding.proto_embeddingId = doc[DocData].proto_embeddingId = Doc.GetEmbeddings(doc).length - 1; !Doc.GetT(embedding, 'title', 'string', true) && (embedding.title = ComputedField.MakeFunction(`renameEmbedding(this)`)); - embedding.author = ClientUtils.CurrentUserEmail; + embedding.author = ClientUtils.CurrentUserEmail(); return embedding; } @@ -648,7 +640,7 @@ export namespace Doc { export function BestEmbedding(doc: Doc) { const dataDoc = doc[DocData]; const availableEmbeddings = Doc.GetEmbeddings(dataDoc); - const bestEmbedding = [...(dataDoc !== doc ? [doc] : []), ...availableEmbeddings].find(doc => !doc.embedContainer && doc.author === ClientUtils.CurrentUserEmail); + const bestEmbedding = [...(dataDoc !== doc ? [doc] : []), ...availableEmbeddings].find(doc => !doc.embedContainer && doc.author === ClientUtils.CurrentUserEmail()); bestEmbedding && Doc.AddEmbedding(doc, doc); return bestEmbedding ?? Doc.MakeEmbedding(doc); } @@ -708,7 +700,7 @@ export namespace Doc { }; const docAtKey = doc[key]; if (key === 'author') { - assignKey(ClientUtils.CurrentUserEmail); + assignKey(ClientUtils.CurrentUserEmail()); } else if (docAtKey instanceof Doc) { if (pruneDocs.includes(docAtKey)) { // prune doc and do nothing @@ -717,7 +709,7 @@ export namespace Doc { (key.includes('layout[') || key.startsWith('layout') || // ['embedContainer', 'annotationOn', 'proto'].includes(key) || - (['link_anchor_1', 'link_anchor_2'].includes(key) && doc.author === ClientUtils.CurrentUserEmail)) + (['link_anchor_1', 'link_anchor_2'].includes(key) && doc.author === ClientUtils.CurrentUserEmail())) ) { assignKey(await Doc.makeClone(docAtKey, cloneMap, linkMap, rtfs, exclusions, pruneDocs, cloneLinks, cloneTemplates)); } else { @@ -886,7 +878,7 @@ export namespace Doc { } } else { const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key])); - const field = key === 'author' ? ClientUtils.CurrentUserEmail : ProxyField.WithoutProxy(() => doc[key]); + const field = key === 'author' ? ClientUtils.CurrentUserEmail() : ProxyField.WithoutProxy(() => doc[key]); if (field instanceof RefField) { if (field instanceof Doc) { if (key === 'myLinkDatabase') { @@ -934,7 +926,7 @@ export namespace Doc { } } else { const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key])); - const field = key === 'author' ? ClientUtils.CurrentUserEmail : ProxyField.WithoutProxy(() => doc[key]); + const field = key === 'author' ? ClientUtils.CurrentUserEmail() : ProxyField.WithoutProxy(() => doc[key]); if (field instanceof RefField) { copy[key] = field; } else if (cfield instanceof ComputedField) { @@ -973,7 +965,7 @@ export namespace Doc { delegate[Initializing] = true; updateCachedAcls(delegate); delegate.proto = doc; - delegate.author = ClientUtils.CurrentUserEmail; + delegate.author = ClientUtils.CurrentUserEmail(); Object.keys(doc) .filter(key => key.startsWith('acl')) .forEach(key => { @@ -1004,7 +996,7 @@ export namespace Doc { export function ApplyTemplate(templateDoc: Doc) { if (templateDoc) { const proto = new Doc(); - proto.author = ClientUtils.CurrentUserEmail; + proto.author = ClientUtils.CurrentUserEmail(); const target = Doc.MakeDelegate(proto); const targetKey = StrCast(templateDoc.layout_fieldKey, 'layout'); const applied = ApplyTemplateTo(templateDoc, target, targetKey, templateDoc.title + '(...' + _applyCount++ + ')'); @@ -1199,7 +1191,8 @@ export namespace Doc { } const UnhighlightWatchers: (() => void)[] = []; - export let UnhighlightTimer: any; + let UnhighlightTimer: any; + export function IsUnhighlightTimerSet() { return UnhighlightTimer; } // prettier-ignore export function AddUnHighlightWatcher(watcher: () => void) { if (UnhighlightTimer) { UnhighlightWatchers.push(watcher); diff --git a/src/fields/util.ts b/src/fields/util.ts index 5ed10cf05..d7268f31a 100644 --- a/src/fields/util.ts +++ b/src/fields/util.ts @@ -84,7 +84,7 @@ const _setterImpl = action((target: any, prop: string | symbol | number, valueIn const writeMode = DocServer.getFieldWriteMode(prop as string); const fromServer = target[UpdatingFromServer]; - const sameAuthor = fromServer || receiver.author === ClientUtils.CurrentUserEmail; + const sameAuthor = fromServer || receiver.author === ClientUtils.CurrentUserEmail(); const writeToDoc = sameAuthor || effectiveAcl === AclEdit || effectiveAcl === AclAdmin || writeMode === DocServer.WriteMode.Playground || writeMode === DocServer.WriteMode.LivePlayground || (effectiveAcl === AclAugment && value instanceof RichTextField); const writeToServer = @@ -163,7 +163,7 @@ const getEffectiveAclCache = computedFn((target: any, user?: string) => getEffec */ export function GetEffectiveAcl(target: any, user?: string): symbol { if (!target) return AclPrivate; - if (target[UpdatingFromServer] || ClientUtils.CurrentUserEmail === 'guest') return AclAdmin; + if (target[UpdatingFromServer] || ClientUtils.CurrentUserEmail() === 'guest') return AclAdmin; return getEffectiveAclCache(target, user); // all changes received from the server must be processed as Admin. return this directly so that the acls aren't cached (UpdatingFromServer is not observable) } @@ -186,7 +186,7 @@ function getEffectiveAcl(target: any, user?: string): symbol { const targetAcls = target[DocAcl]; if (targetAcls?.['acl-Me'] === AclAdmin || GetCachedGroupByName('Admin')) return AclAdmin; - const userChecked = user || ClientUtils.CurrentUserEmail; // if the current user is the author of the document / the current user is a member of the admin group + const userChecked = user || ClientUtils.CurrentUserEmail(); // if the current user is the author of the document / the current user is a member of the admin group if (targetAcls && Object.keys(targetAcls).length) { let effectiveAcl = AclPrivate; Object.entries(targetAcls).forEach(([key, value]) => { @@ -219,7 +219,7 @@ function getEffectiveAcl(target: any, user?: string): symbol { */ // eslint-disable-next-line default-param-last export function distributeAcls(key: string, acl: SharingPermissions, target: Doc, visited: Doc[] = [], allowUpgrade?: boolean, layoutOnly = false) { - const selfKey = `acl-${normalizeEmail(ClientUtils.CurrentUserEmail)}`; + const selfKey = `acl-${normalizeEmail(ClientUtils.CurrentUserEmail())}`; if (!target || visited.includes(target) || key === selfKey) return; visited.push(target); @@ -268,7 +268,7 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc * Copies parent's acl fields to the child */ export function inheritParentAcls(parent: Doc, child: Doc, layoutOnly: boolean) { - [...Object.keys(parent), ...(ClientUtils.CurrentUserEmail !== parent.author ? ['acl-Owner'] : [])] + [...Object.keys(parent), ...(ClientUtils.CurrentUserEmail() !== parent.author ? ['acl-Owner'] : [])] .filter(key => key.startsWith('acl')) .forEach(key => { // if the default acl mode is private, then don't inherit the acl-guest permission, but set it to private. -- cgit v1.2.3-70-g09d2 From 9e809f8748d1812bb03ec6471aa6f97467f8f75a Mon Sep 17 00:00:00 2001 From: bobzel Date: Tue, 23 Apr 2024 16:20:08 -0400 Subject: fixes for rich text menu updates and setting parameters on text doc vs within in RTF. Lots of lint cleanup. --- package-lock.json | 2 +- package.json | 2 +- src/client/DocServer.ts | 15 +- src/client/util/CurrentUserUtils.ts | 2 +- src/client/util/DocumentManager.ts | 36 ++- src/client/util/request-image-size.ts | 26 +- src/client/views/ScriptingRepl.tsx | 191 +++++++------- src/client/views/StyleProvider.tsx | 108 +++++--- src/client/views/collections/CollectionMenu.tsx | 73 ++++-- .../collectionSchema/CollectionSchemaView.tsx | 181 +++++++++---- src/client/views/nodes/DocumentContentsView.tsx | 2 - .../nodes/formattedText/FormattedTextBox.scss | 2 +- .../views/nodes/formattedText/FormattedTextBox.tsx | 53 ++-- .../formattedText/ProsemirrorExampleTransfer.ts | 14 +- .../views/nodes/formattedText/RichTextMenu.tsx | 56 ++-- src/client/views/nodes/formattedText/nodes_rts.ts | 19 +- src/client/views/webcam/DashWebRTCVideo.scss | 82 ------ src/client/views/webcam/DashWebRTCVideo.tsx | 76 ------ src/client/views/webcam/WebCamLogic.js | 292 --------------------- src/fields/Doc.ts | 3 + src/fields/util.ts | 6 +- src/server/ActionUtilities.ts | 79 +++--- src/server/ApiManagers/ApiManager.ts | 4 +- src/server/ApiManagers/DeleteManager.ts | 14 +- src/server/ApiManagers/DownloadManager.ts | 6 +- src/server/ApiManagers/MongoStore.js | 21 +- src/server/ApiManagers/SearchManager.ts | 9 +- src/server/ApiManagers/SessionManager.ts | 26 +- src/server/ApiManagers/UtilManager.ts | 31 +-- .../Session/agents/applied_session_agent.ts | 23 +- src/server/DashSession/Session/agents/monitor.ts | 48 ++-- .../Session/agents/process_message_router.ts | 12 +- .../Session/agents/promisified_ipc_manager.ts | 51 ++-- .../DashSession/Session/agents/server_worker.ts | 46 ++-- src/server/DashSession/Session/utilities/repl.ts | 76 +++--- .../Session/utilities/session_config.ts | 104 ++++---- .../DashSession/Session/utilities/utilities.ts | 43 ++- src/server/DashStats.ts | 5 +- src/server/DashUploadUtils.ts | 8 +- src/server/GarbageCollector.ts | 70 +++-- src/server/MemoryDatabase.ts | 37 +-- src/server/PdfTypes.ts | 20 +- src/server/ProcessFactory.ts | 54 ++-- src/server/RouteSubscriber.ts | 5 +- src/server/SharedMediaTypes.ts | 31 +-- src/server/apis/google/CredentialsLoader.ts | 24 +- src/server/apis/google/GoogleApiServerUtils.ts | 8 +- src/server/apis/google/SharedTypes.ts | 13 +- src/server/apis/youtube/youtubeApiSample.d.ts | 2 +- src/server/authentication/DashUserModel.ts | 8 +- src/server/authentication/Passport.ts | 25 +- src/server/database.ts | 1 + src/server/index.ts | 48 ++-- src/server/updateProtos.ts | 6 +- src/server/websocket.ts | 40 +-- 55 files changed, 965 insertions(+), 1274 deletions(-) delete mode 100644 src/client/views/webcam/DashWebRTCVideo.scss delete mode 100644 src/client/views/webcam/DashWebRTCVideo.tsx delete mode 100644 src/client/views/webcam/WebCamLogic.js (limited to 'src/client/DocServer.ts') diff --git a/package-lock.json b/package-lock.json index c616bb6ea..03b788ba4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -86,6 +86,7 @@ "D": "^1.0.0", "d3": "^7.8.5", "depcheck": "^1.4.7", + "dotenv": "^16.3.1", "eslint-webpack-plugin": "^4.1.0", "exif": "^0.6.0", "exifr": "^7.1.3", @@ -273,7 +274,6 @@ "@types/youtube": "0.0.50", "chai": "^5.0.0", "cross-env": "^7.0.3", - "dotenv": "^16.3.1", "eslint": "^8.55.0", "eslint-config-airbnb": "^19.0.4", "eslint-config-node": "^4.1.0", diff --git a/package.json b/package.json index a3ba6bdd9..9be1e093d 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,6 @@ "@types/youtube": "0.0.50", "chai": "^5.0.0", "cross-env": "^7.0.3", - "dotenv": "^16.3.1", "eslint": "^8.55.0", "eslint-config-airbnb": "^19.0.4", "eslint-config-node": "^4.1.0", @@ -169,6 +168,7 @@ "D": "^1.0.0", "d3": "^7.8.5", "depcheck": "^1.4.7", + "dotenv": "^16.3.1", "eslint-webpack-plugin": "^4.1.0", "exif": "^0.6.0", "exifr": "^7.1.3", 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(); 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(res => { + // eslint-disable-next-line no-loop-func + const prom = new Promise(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 { + 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 ? {this._props.name} : : <> } + {name} + + ); + if (typeof val === 'object') { + // eslint-disable-next-line no-use-before-define + return ; + } + if (typeof val === 'function') { + return
{title('[Function]')}
; + } + return
{title(String(val))}
; + } +} interface ReplProps { scrollToBottom: () => void; value: { [key: string]: any }; @@ -37,7 +70,7 @@ export class ScriptingObjectDisplay extends ObservableReactComponent const name = (proto && proto.constructor && proto.constructor.name) || String(val); const title = ( <> - {this.props.name ? {this._props.name} : : <>} + {this.props.name ? {this._props.name} : : null} {name} ); @@ -50,53 +83,23 @@ export class ScriptingObjectDisplay extends ObservableReactComponent {title} (+{Object.keys(val).length})
); - } else { - return ( -
-
- - - - {title} -
-
- {Object.keys(val).map(key => ( - - ))} -
-
- ); } - } -} - -interface replValueProps { - scrollToBottom: () => void; - value: any; - name?: string; -} -@observer -export class ScriptingValueDisplay extends ObservableReactComponent { - 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 ? {this._props.name} : : <> } - {name} - + return ( +
+
+ + + + {title} +
+
+ {Object.keys(val).map(key => ( + // eslint-disable-next-line react/jsx-props-no-spreading + + ))} +
+
); - if (typeof val === 'object') { - return ; - } else if (typeof val === 'function') { - const name = '[Function]'; - return
{title('[Function]')}
; - } - return
{title(String(val))}
; } } @@ -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 (
- {this.commands.map(({ command, result }, i) => { - return ( -
-
- {command ||
} -
-
- {} -
+ {this.commands.map(({ command, result }, i) => ( +
+
+ {command ||
} +
+
+
- ); - })} +
+ ))}
, props: Opt 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, props: Opt 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, props: Opt, props: Opt - +
@@ -251,13 +275,13 @@ export function DefaultStyleProvider(doc: Opt, props: Opt 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, props: Opt, props: Opt, props: Opt doc?.pointerEvents !== 'none' ? null : ( @@ -312,7 +336,7 @@ export function DefaultStyleProvider(doc: Opt, props: Opt ); const paint = () => !doc?.onPaint ? null : ( -
togglePaintView(e, doc, props)}> +
togglePaintView(e, doc, props)}>
); @@ -321,7 +345,7 @@ export function DefaultStyleProvider(doc: Opt, props: Opt 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, props: Opt 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, props: Opt { - 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 ( {StrListCast(doc[fieldKey + 'audioAnnotations_text']).lastElement()}
}> 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(); @@ -345,11 +351,13 @@ export class CollectionViewBaseChrome extends React.Component { 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 c.title === this._currentKey).map(c => c.immediate(docDragData.draggedDocuments || [])); e.stopPropagation(); @@ -420,11 +428,11 @@ export class CollectionViewBaseChrome extends React.Component drop document to apply or drag to create button
} placement="bottom">
- e.stopPropagation()} onChange={action(e => (this._newFieldDefault = e.target.value))} />; + return ( + e.stopPropagation()} + onChange={action(e => { + this._newFieldDefault = e.target.value; + })} + /> + ); case ColumnType.Boolean: return ( <> - e.stopPropagation()} onChange={action(e => (this._newFieldDefault = e.target.checked))} /> + e.stopPropagation()} + onChange={action(e => { + this._newFieldDefault = e.target.checked; + })} + /> {this._newFieldDefault ? 'true' : 'false'} ); case ColumnType.String: - return e.stopPropagation()} onChange={action(e => (this._newFieldDefault = e.target.value))} />; + return ( + 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) => (this._filterSearchValue = e.target.value); + updateFilterSearch = (e: React.ChangeEvent) => { + this._filterSearchValue = e.target.value; + }; @computed get newFieldMenu() { return ( @@ -632,7 +692,7 @@ export class CollectionSchemaView extends CollectionSubView() { { this._newFieldType = ColumnType.Number; this._newFieldDefault = 0; @@ -644,7 +704,7 @@ export class CollectionSchemaView extends CollectionSubView() { { this._newFieldType = ColumnType.Boolean; this._newFieldDefault = false; @@ -656,7 +716,7 @@ export class CollectionSchemaView extends CollectionSubView() { { this._newFieldType = ColumnType.String; this._newFieldDefault = ''; @@ -668,7 +728,7 @@ export class CollectionSchemaView extends CollectionSubView() {
{this._newFieldWarning}
{ + 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 (
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 (
this.createDashEventsTarget(ele)} onDrop={this.onExternalDrop.bind(this)}> -
+
this.openColumnMenu(-1, true)} icon="plus" />} + toggle={ 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() {
{this.columnKeys.map((key, index) => ( {this._columnMenuIndex !== undefined && this._columnMenuIndex !== -1 && this.renderColumnMenu} {this._filterColumnIndex !== undefined && this.renderFilterMenu} - (this._tableContentRef = ref)} /> + { + // eslint-disable-next-line no-use-before-define + { + this._tableContentRef = ref; + }} + /> + } {this.layoutDoc.chromeHidden ? null : (
(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} />
)}
- {this.previewWidth > 0 &&
} + {this.previewWidth > 0 &&
} {this.previewWidth > 0 && ( -
(this._previewRef = ref)}> +
{ + this._previewRef = ref; + }}> {Array.from(this._selectedDocs).lastElement() && ( {this.props.childDocs().docs.map((doc: Doc, index: number) => (
- + { + // eslint-disable-next-line no-use-before-define + + }
))}
@@ -977,6 +1055,7 @@ class CollectionSchemaViewDoc extends ObservableReactComponent ); 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 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 { - 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 !isClick && batch.end(), + (moveEv, movement, isClick) => !isClick && batch.end(), () => { this.toggleSidebar(); batch.end(); @@ -1301,7 +1302,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent 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 { + 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 { - 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 node that wraps the hyerlink - 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 node that wraps the hyerlink + 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 { - 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>(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>(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>(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>(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 { 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 { 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 { 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 { 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 { 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 { - 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 = ( -
-
DashWebRTC
- (this.roomText = e!)} onKeyDown={this.onEnterKeyDown} /> -
- - -
-
-
- -
-
- -
-
-
- ); - - const frozen = !this.props.isSelected() || SnappingManager.IsResizing; - const classname = 'webBox-cont' + (this.props.isSelected() && Doc.ActiveTool === InkTool.None && !SnappingManager.IsResizing ? '-interactive' : ''); - - return ( - <> -
{content}
- {!frozen ? null :
} - - ); - } -} 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 = Opt | 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; +// eslint-disable-next-line no-redeclare export function DocListCastAsync(field: FieldResult, defaultValue: Doc[]): Promise; +// 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(resolve => logStream.on('open', resolve)); +export async function fileDescriptorFromStream(filePath: string) { + const logStream = createWriteStream(filePath); + return new Promise(resolve => { + logStream.on('open', resolve); + }); } -export const command_line = (command: string, fromDirectory?: string) => { - return new Promise((resolve, reject) => { +export const commandLine = (command: string, fromDirectory?: string) => + new Promise((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((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((resolve, reject) => { writeFile(target, contents, err => (err ? reject(err) : resolve())); @@ -55,39 +56,38 @@ export interface LogData { 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({ startMessage, endMessage, action, color }: LogData): Promise { - 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(resolve => exists(path, resolve))) { +export const createIfNotExists = async (filePath: string) => { + if ( + await new Promise(resolve => { + exists(filePath, resolve); + }) + ) { return true; } - return new Promise(resolve => mkdir(path, error => resolve(error === null))); + return new Promise(resolve => { + mkdir(filePath, error => resolve(error === null)); + }); }; export async function Prune(rootDirectory: string): Promise { // const error = await new Promise(resolve => rimraf(rootDirectory).then(resolve)); - await new Promise(resolve => rimraf(rootDirectory).then(() => resolve())); + await new Promise(resolve => { + rimraf(rootDirectory).then(() => resolve()); + }); // return error === null; return true; } -export const Destroy = (mediaPath: string) => new Promise(resolve => unlink(mediaPath, error => resolve(error === null))); +export const Destroy = (mediaPath: string) => + new Promise(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(resolve => smtpTransport.sendMail(mailOptions, resolve)); + return new Promise(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(); 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(resolve => { + subscription: '/pull', + secureHandler: async ({ res }) => + new Promise(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(resolve => { + subscription: '/version', + secureHandler: ({ res }) => + new Promise(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; 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; @@ -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(resolve => { + public exec = (command: string, options?: ExecOptions) => + new Promise(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 = { + name: string; + args?: T; +}; +export type MessageHandler = (args: T) => any | Promise; /** * 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 = { - name: string; - args?: T; -}; -export type MessageHandler = (args: T) => any | Promise; - +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 = (args: T) => any | Promise; 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>(resolve => { const messageId = Utilities.guid(); - type InternalMessageHandler = (message: any /* MessageListener*/) => any | Promise; + type InternalMessageHandler = (message: any /* MessageListener */) => any | Promise; 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(async resolve => { + public destroy = () => + new Promise(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("kill", { reason, graceful, errorCode }); + public killSession = (reason: string, graceful = true, errorCode = 0) => this.emit('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 => { await new Promise(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; - 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; + // 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 = 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(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(); 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(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(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 { 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 { + public updateMany(/* query: any, update: any, collectionName = DocumentsCollection */): Promise { throw new Error("Can't updateMany a MemoryDatabase"); } @@ -54,7 +53,9 @@ export class MemoryDatabase implements IDatabase { } public delete(query: any, collectionName?: string): Promise; + // eslint-disable-next-line no-dupe-class-members public delete(id: string, collectionName?: string): Promise; + // eslint-disable-next-line no-dupe-class-members public delete(id: any, collectionName = DocumentsCollection): Promise { 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 { - 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 { - 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(resolve => rimraf(logPath).then(resolve)); + await new Promise(resolve => { + rimraf(logPath).then(resolve); + }); } } mkdirSync(logPath); } - export async function create(command: string, args?: readonly string[]): Promise { - 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 { + 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 { + 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 = { 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 = { 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((req, user, done) => { passport.deserializeUser((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 } = {}; 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]; } }); -- cgit v1.2.3-70-g09d2 From 9b424c94d7a89950e9cf3f72e684bd15a61e87ae Mon Sep 17 00:00:00 2001 From: bobzel Date: Thu, 2 May 2024 11:19:37 -0400 Subject: another push to remove cycles by pushing things onto Doc and DocumentView --- src/client/DocServer.ts | 16 +++----- src/client/util/SharingManager.tsx | 1 + src/client/views/DashboardView.tsx | 6 +-- src/client/views/DocComponent.tsx | 20 ++++------ src/client/views/DocumentDecorations.tsx | 3 +- src/client/views/InkingStroke.tsx | 10 ++--- src/client/views/Main.tsx | 5 ++- src/client/views/PropertiesView.tsx | 9 ++--- .../views/collections/CollectionDockingView.tsx | 14 ++++--- src/client/views/collections/TreeView.tsx | 5 +-- .../collectionSchema/CollectionSchemaView.tsx | 8 ++-- .../collectionSchema/SchemaTableCell.tsx | 9 ++--- src/client/views/nodes/ComparisonBox.tsx | 7 ++-- src/client/views/nodes/DocumentView.tsx | 26 ++++++------ src/client/views/nodes/FieldView.tsx | 2 +- src/client/views/nodes/KeyValueBox.tsx | 46 +++++++++++----------- src/client/views/nodes/KeyValuePair.tsx | 3 +- src/client/views/nodes/PDFBox.tsx | 6 +-- src/client/views/nodes/WebBox.tsx | 6 +-- .../views/nodes/formattedText/FormattedTextBox.tsx | 6 +-- .../views/nodes/formattedText/RichTextRules.ts | 14 +++---- src/fields/Doc.ts | 18 +++++++-- 22 files changed, 122 insertions(+), 118 deletions(-) (limited to 'src/client/DocServer.ts') diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts index cf7a61d24..ac865382d 100644 --- a/src/client/DocServer.ts +++ b/src/client/DocServer.ts @@ -26,7 +26,10 @@ import { SerializationHelper } from './util/SerializationHelper'; */ export namespace DocServer { // eslint-disable-next-line import/no-mutable-exports - export let _cache: { [id: string]: RefField | Promise> } = {}; + let _cache: { [id: string]: RefField | Promise> } = {}; + export function Cache() { + return _cache; + } function errorFunc(): never { throw new Error("Can't use DocServer without calling init first"); @@ -54,15 +57,6 @@ export namespace DocServer { } } - export function FindDocByTitle(title: string) { - const foundDocId = - title && - Array.from(Object.keys(_cache)) - .filter(key => _cache[key] instanceof Doc) - .find(key => (_cache[key] as Doc).title === title); - - return foundDocId ? (_cache[foundDocId] as Doc) : undefined; - } let _socket: Socket; // this client's distinct GUID created at initialization let USER_ID: string; @@ -228,7 +222,7 @@ export namespace DocServer { const deserializeField = getSerializedField.then(async fieldJson => { // deserialize const field = await SerializationHelper.Deserialize(fieldJson); - if (force && field instanceof Doc && cached instanceof Doc) { + if (force && field && cached instanceof Doc) { cached[UpdatingFromServer] = true; Array.from(Object.keys(field)).forEach(key => { const fieldval = field[key]; diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx index 31a99896f..c2a52cae9 100644 --- a/src/client/util/SharingManager.tsx +++ b/src/client/util/SharingManager.tsx @@ -94,6 +94,7 @@ export class SharingManager extends React.Component<{}> { super(props); makeObservable(this); SharingManager.Instance = this; + DocumentView.ShareOpen = this.open; } /** diff --git a/src/client/views/DashboardView.tsx b/src/client/views/DashboardView.tsx index d92a73b63..fc6a330f7 100644 --- a/src/client/views/DashboardView.tsx +++ b/src/client/views/DashboardView.tsx @@ -22,7 +22,6 @@ import { Docs, DocumentOptions } from '../documents/Documents'; import { dropActionType } from '../util/DropActionTypes'; import { HistoryUtil } from '../util/History'; import { ScriptingGlobals } from '../util/ScriptingGlobals'; -import { SharingManager } from '../util/SharingManager'; import { SnappingManager } from '../util/SnappingManager'; import { undoBatch, undoable } from '../util/UndoManager'; import { ContextMenu } from './ContextMenu'; @@ -30,6 +29,7 @@ import './DashboardView.scss'; import { MainViewModal } from './MainViewModal'; import { ObservableReactComponent } from './ObservableReactComponent'; import { Colors } from './global/globalEnums'; +import { DocumentView } from './nodes/DocumentView'; import { ButtonType } from './nodes/FontIconBox/FontIconBox'; enum DashboardGroup { @@ -170,7 +170,7 @@ export class DashboardView extends ObservableReactComponent<{}> { ContextMenu.Instance.addItem({ description: `Share Dashboard`, - event: () => SharingManager.Instance.open(undefined, dashboard), + event: () => DocumentView.ShareOpen(undefined, dashboard), icon: 'edit', }); ContextMenu.Instance.addItem({ @@ -535,7 +535,7 @@ ScriptingGlobals.add(function createNewDashboard() { }, 'creates a new dashboard when called'); // eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function shareDashboard(dashboard: Doc) { - SharingManager.Instance.open(undefined, dashboard); + DocumentView.ShareOpen(undefined, dashboard); }, 'opens sharing dialog for Dashboard'); // eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function removeDashboard(dashboard: Doc) { diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx index 97ff346e4..70b20aec7 100644 --- a/src/client/views/DocComponent.tsx +++ b/src/client/views/DocComponent.tsx @@ -23,9 +23,7 @@ import { OpenWhere } from './nodes/OpenWhere'; * Many of these methods only make sense for specific viewBox'es, but they should be written to * be as general as possible */ -export interface ViewBoxInterface { - fieldKey?: string; - annotationKey?: string; +export class ViewBoxInterface

extends ObservableReactComponent> { promoteCollection?: () => void; // moves contents of collection to parent updateIcon?: () => void; // updates the icon representation of the document getAnchor?: (addAsAnnotation: boolean, pinData?: PinProps) => Doc; // returns an Anchor Doc that represents the current state of the doc's componentview (e.g., the current playhead location of a an audio/video box) @@ -61,7 +59,7 @@ export interface ViewBoxInterface { snapPt?: (pt: { X: number; Y: number }, excludeSegs?: number[]) => { nearestPt: { X: number; Y: number }; distance: number }; search?: (str: string, bwd?: boolean, clear?: boolean) => boolean; dontRegisterView?: () => boolean; // KeyValueBox's don't want to register their views - isUnstyledView?: () => boolean; // SchemaView and KeyValue are untyled -- not tiles, no opacity + isUnstyledView?: () => boolean; // SchemaView and KeyValue are unstyled -- not titles, no opacity, no animations } /** * DocComponent returns a React base class used by Doc views with accessors for unpacking the Document,layoutDoc, and dataDoc's @@ -113,7 +111,7 @@ export function DocComponent

() { * Example views include: InkingStroke, FontIconBox, EquationBox, etc */ export function ViewBoxBaseComponent

() { - class Component extends ObservableReactComponent> { + class Component extends ViewBoxInterface

{ constructor(props: P) { super(props); makeObservable(this); @@ -168,7 +166,7 @@ export function ViewBoxBaseComponent

() { * Example views include: PDFBox, ImageBox, MapBox, etc */ export function ViewBoxAnnotatableComponent

() { - class Component extends ObservableReactComponent> { + class Component extends ViewBoxInterface

{ @observable _annotationKeySuffix = () => 'annotations'; @observable _isAnyChildContentActive = false; @@ -219,8 +217,7 @@ export function ViewBoxAnnotatableComponent

() { return this.fieldKey + (this._annotationKeySuffix() ? '_' + this._annotationKeySuffix() : ''); } - @action.bound - removeDocument(docIn: Doc | Doc[], annotationKey?: string, leavePushpin?: boolean, dontAddToRemoved?: boolean): boolean { + override removeDocument = (docIn: Doc | Doc[], annotationKey?: string, leavePushpin?: boolean, dontAddToRemoved?: boolean): boolean => { const effectiveAcl = GetEffectiveAcl(this.dataDoc); const docs = toList(docIn).filter(fdoc => [AclEdit, AclAdmin].includes(effectiveAcl) || GetEffectiveAcl(fdoc) === AclAdmin); @@ -249,12 +246,11 @@ export function ViewBoxAnnotatableComponent

() { } return false; - } + }; // this is called with the document that was dragged and the collection to move it into. // if the target collection is the same as this collection, then the move will be allowed. // otherwise, the document being moved must be able to be removed from its container before // moving it into the target. - @action.bound moveDocument = (docs: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[], annotationKey?: string) => boolean, annotationKey?: string): boolean => { if (Doc.AreProtosEqual(this._props.Document, targetCollection)) { return true; @@ -265,8 +261,8 @@ export function ViewBoxAnnotatableComponent

() { } return false; }; - @action.bound - addDocument = (docIn: Doc | Doc[], annotationKey?: string): boolean => { + + override addDocument = (docIn: Doc | Doc[], annotationKey?: string): boolean => { const docs = toList(docIn); if (this._props.filterAddDocument?.(docs) === false || docs.find(fdoc => Doc.AreProtosEqual(fdoc, this.Document) && Doc.LayoutField(fdoc) === Doc.LayoutField(this.Document))) { return false; diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 432b02782..4262c2d57 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -33,7 +33,6 @@ import { Colors } from './global/globalEnums'; import { CollectionFreeFormDocumentView } from './nodes/CollectionFreeFormDocumentView'; import { DocumentView } from './nodes/DocumentView'; import { ImageBox } from './nodes/ImageBox'; -import { KeyValueBox } from './nodes/KeyValueBox'; import { OpenWhereMod } from './nodes/OpenWhere'; import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox'; @@ -137,7 +136,7 @@ export class DocumentDecorations extends ObservableReactComponent() implements ViewBoxInterface { +export class InkingStroke extends ViewBoxAnnotatableComponent() { static readonly MaskDim = INK_MASK_SIZE; // choose a really big number to make sure mask fits over container (which in theory can be arbitrarily big) public static LayoutString(fieldStr: string) { return FieldView.LayoutString(InkingStroke, fieldStr); @@ -337,9 +337,9 @@ export class InkingStroke extends ViewBoxAnnotatableComponent() ); }; - _subContentView: ViewBoxInterface | undefined; - setSubContentView = (doc: ViewBoxInterface) => { - this._subContentView = doc; + _subContentView: ViewBoxInterface | undefined; + setSubContentView = (box: ViewBoxInterface) => { + this._subContentView = box; }; @computed get fillColor(): string { const isInkMask = BoolCast(this.layoutDoc.stroke_isInkMask); diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index b6dfed687..259ffbbc5 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -7,9 +7,11 @@ import * as dotenv from 'dotenv'; // see https://github.com/motdotla/dotenv#how- import * as React from 'react'; import * as ReactDOM from 'react-dom/client'; import { AssignAllExtensions } from '../../extensions/Extensions'; +import { Doc } from '../../fields/Doc'; import { FieldLoader } from '../../fields/FieldLoader'; import { BranchingTrailManager } from '../util/BranchingTrailManager'; import { CurrentUserUtils } from '../util/CurrentUserUtils'; +import { LinkFollower } from '../util/LinkFollower'; import { PingManager } from '../util/PingManager'; import { ReplayMovements } from '../util/ReplayMovements'; import { TrackMovements } from '../util/TrackMovements'; @@ -18,7 +20,7 @@ import { MainView } from './MainView'; import { CollectionView } from './collections/CollectionView'; import { CollectionFreeFormInfoUI } from './collections/collectionFreeForm/CollectionFreeFormInfoUI'; import './global/globalScripts'; -import { LinkFollower } from '../util/LinkFollower'; +import { KeyValueBox } from './nodes/KeyValueBox'; dotenv.config(); @@ -66,6 +68,7 @@ FieldLoader.ServerLoadStatus = { requested: 0, retrieved: 0, message: 'cache' }; // iniitialize plugin apis CollectionFreeFormInfoUI.Init(); LinkFollower.Init(); + KeyValueBox.Init(); root.render(); }, 0); })(); diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx index 7c16e0ddb..b03c1a64e 100644 --- a/src/client/views/PropertiesView.tsx +++ b/src/client/views/PropertiesView.tsx @@ -41,7 +41,6 @@ import './PropertiesView.scss'; import { DefaultStyleProvider, SetFilterOpener as SetPropertiesFilterOpener } from './StyleProvider'; import { DocumentView } from './nodes/DocumentView'; import { StyleProviderFuncType } from './nodes/FieldView'; -import { KeyValueBox } from './nodes/KeyValueBox'; import { OpenWhere } from './nodes/OpenWhere'; import { PresBox, PresEffect, PresEffectDirection } from './nodes/trails'; @@ -208,7 +207,7 @@ export class PropertiesView extends ObservableReactComponent editableContents} SetValue={(value: string) => { - value !== '-multiple-' && docs.map(doc => KeyValueBox.SetField(doc, key, value, true)); + value !== '-multiple-' && docs.map(doc => Doc.SetField(doc, key, value, true)); return true; }} /> @@ -252,10 +251,10 @@ export class PropertiesView extends ObservableReactComponent Doc.SetInPlace(dv.Document, 'title', value, true)); } else if (this.dataDoc) { if (this.selectedDoc) Doc.SetInPlace(this.selectedDoc, 'title', value, true); - else KeyValueBox.SetField(this.dataDoc, 'title', value as string, true); + else Doc.SetField(this.dataDoc, 'title', value as string, true); } }; diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index fc9e2e39b..0ee3575f3 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -2,7 +2,7 @@ import { action, IReactionDisposer, makeObservable, observable, reaction } from import { observer } from 'mobx-react'; import * as React from 'react'; import * as ReactDOM from 'react-dom/client'; -import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, DivHeight, DivWidth, incrementTitleCopy, UpdateIcon } from '../../../ClientUtils'; +import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, DivHeight, DivWidth, incrementTitleCopy, returnTrue, UpdateIcon } from '../../../ClientUtils'; import { Doc, DocListCast, Field, Opt } from '../../../fields/Doc'; import { AclAdmin, AclEdit, DocData } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; @@ -49,7 +49,8 @@ export class CollectionDockingView extends CollectionSubView() { private _reactionDisposer?: IReactionDisposer; private _lightboxReactionDisposer?: IReactionDisposer; private _containerRef = React.createRef(); - public _flush: UndoManager.Batch | undefined; + private _flush: UndoManager.Batch | undefined; + private _unmounting = false; private _ignoreStateChange = ''; public tabMap: Set = new Set(); public get HasFullScreen() { @@ -330,6 +331,7 @@ export class CollectionDockingView extends CollectionSubView() { }; componentDidMount: () => void = async () => { + this._props.setContentViewBox?.(this); this._unmounting = false; SetPropSetterCb('title', this.titleChanged); // this overrides any previously assigned callback for the property if (this._containerRef.current) { @@ -366,7 +368,6 @@ export class CollectionDockingView extends CollectionSubView() { } }; - _unmounting = false; componentWillUnmount: () => void = () => { this._unmounting = true; try { @@ -383,6 +384,9 @@ export class CollectionDockingView extends CollectionSubView() { this._lightboxReactionDisposer?.(); }; + // ViewBoxInterface overrides + override isUnstyledView = returnTrue; + @action onResize = () => { const cur = this._containerRef.current; @@ -437,8 +441,8 @@ export class CollectionDockingView extends CollectionSubView() { } else { const tabTarget = (e.target as HTMLElement)?.parentElement?.className.includes('lm_tab') ? (e.target as HTMLElement).parentElement : (e.target as HTMLElement); const map = Array.from(this.tabMap).find(tab => tab.element[0] === tabTarget); - if (map?.DashDoc && DocumentView.getFirstDocumentView(map.DashDoc)) { - DocumentView.SelectView(DocumentView.getFirstDocumentView(map.DashDoc), false); + if (map?.DashDoc && DocumentView.getDocumentView(map.DashDoc, this.DocumentView?.())) { + DocumentView.SelectView(DocumentView.getDocumentView(map.DashDoc, this.DocumentView?.()), false); } } } diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx index 969b98d91..6ea6bbfbd 100644 --- a/src/client/views/collections/TreeView.tsx +++ b/src/client/views/collections/TreeView.tsx @@ -32,7 +32,6 @@ import { ObservableReactComponent } from '../ObservableReactComponent'; import { StyleProp } from '../StyleProp'; import { DocumentView, DocumentViewInternal } from '../nodes/DocumentView'; import { FieldViewProps, StyleProviderFuncType } from '../nodes/FieldView'; -import { KeyValueBox } from '../nodes/KeyValueBox'; import { OpenWhere } from '../nodes/OpenWhere'; import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox'; import { RichTextMenu } from '../nodes/formattedText/RichTextMenu'; @@ -568,7 +567,7 @@ export class TreeView extends ObservableReactComponent { height={13} fontSize={12} GetValue={() => Field.toKeyValueString(doc, key)} - SetValue={(value: string) => KeyValueBox.SetField(doc, key, value, true)} + SetValue={(value: string) => Doc.SetField(doc, key, value, true)} /> ); } @@ -600,7 +599,7 @@ export class TreeView extends ObservableReactComponent { const key = match[1]; const assign = match[2]; const val = match[3]; - KeyValueBox.SetField(doc, key, assign + val, false); + Doc.SetField(doc, key, assign + val, false); return true; } return false; diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx index b30954ffd..77247e675 100644 --- a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx +++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx @@ -28,7 +28,6 @@ import { Colors } from '../../global/globalEnums'; import { DocumentView } from '../../nodes/DocumentView'; import { FieldViewProps } from '../../nodes/FieldView'; import { FocusViewOptions } from '../../nodes/FocusViewOptions'; -import { KeyValueBox } from '../../nodes/KeyValueBox'; import { CollectionSubView } from '../CollectionSubView'; import './CollectionSchemaView.scss'; import { SchemaColumnHeader } from './SchemaColumnHeader'; @@ -174,7 +173,9 @@ export class CollectionSchemaView extends CollectionSubView() { document.removeEventListener('keydown', this.onKeyDown); } - isUnstyledView = returnTrue; // used by style provide via ViewBoxInterface + // ViewBoxInterface overrides + override isUnstyledView = returnTrue; // used by style provider : turns off opacity, animation effects, scaling + rowIndex = (doc: Doc) => this.sortedDocs.docs.indexOf(doc); @action @@ -606,7 +607,7 @@ export class CollectionSchemaView extends CollectionSubView() { }; setColumnValues = (key: string, value: string) => { - this.childDocs.forEach(doc => KeyValueBox.SetField(doc, key, value)); + this.childDocs.forEach(doc => Doc.SetField(doc, key, value)); return true; }; @@ -804,7 +805,6 @@ export class CollectionSchemaView extends CollectionSubView() { ); } get renderKeysMenu() { - console.log('RNDERMENUT:' + this._columnMenuIndex); return (

e.stopPropagation()} /> diff --git a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx index ee6987e89..3df7ecdbe 100644 --- a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx +++ b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx @@ -29,7 +29,6 @@ import { DefaultStyleProvider } from '../../StyleProvider'; import { Colors } from '../../global/globalEnums'; import { DocFocusOrOpen, returnEmptyDocViewList } from '../../nodes/DocumentView'; import { FieldViewProps } from '../../nodes/FieldView'; -import { KeyValueBox } from '../../nodes/KeyValueBox'; import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox'; import { FInfotoColType } from './CollectionSchemaView'; import './CollectionSchemaView.scss'; @@ -144,7 +143,7 @@ export class SchemaTableCell extends ObservableReactComponent | undefined) => { if ((value?.nativeEvent as any).shiftKey) { this._props.setColumnValues(this._props.fieldKey.replace(/^_/, ''), (color === 'black' ? '=' : '') + (value?.target?.checked.toString() ?? '')); - } else KeyValueBox.SetField(this._props.Document, this._props.fieldKey.replace(/^_/, ''), (color === 'black' ? '=' : '') + (value?.target?.checked.toString() ?? '')); + } else Doc.SetField(this._props.Document, this._props.fieldKey.replace(/^_/, ''), (color === 'black' ? '=' : '') + (value?.target?.checked.toString() ?? '')); })} /> KeyValueBox.SetField(this._props.Document, this._props.fieldKey.replace(/^_/, ''), `"${val?.value ?? ''}"`)} + onChange={val => Doc.SetField(this._props.Document, this._props.fieldKey.replace(/^_/, ''), `"${val?.value ?? ''}"`)} />
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx index 6ae6bb228..474d54119 100644 --- a/src/client/views/nodes/ComparisonBox.tsx +++ b/src/client/views/nodes/ComparisonBox.tsx @@ -19,11 +19,10 @@ import { StyleProp } from '../StyleProp'; import './ComparisonBox.scss'; import { DocumentView } from './DocumentView'; import { FieldView, FieldViewProps } from './FieldView'; -import { KeyValueBox } from './KeyValueBox'; import { FormattedTextBox } from './formattedText/FormattedTextBox'; @observer -export class ComparisonBox extends ViewBoxAnnotatableComponent() implements ViewBoxInterface { +export class ComparisonBox extends ViewBoxAnnotatableComponent() { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ComparisonBox, fieldKey); } @@ -188,9 +187,9 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() // where (this) is replaced by the text in the fieldKey slot abd this.excludeWords is repalced by the conetnts of the excludeWords field // The GPT call will put the "answer" in the second slot of the comparison (eg., text_2) if (whichSlot.endsWith('2') && !layoutTemplateString?.includes(whichSlot)) { - const queryText = altText?.replace('(this)', subjectText); // TODO: this should be done in KeyValueBox.setField but it doesn't know about the fieldKey ... + const queryText = altText?.replace('(this)', subjectText); // TODO: this should be done in Doc.setField but it doesn't know about the fieldKey ... if (queryText?.match(/\(\(.*\)\)/)) { - KeyValueBox.SetField(this.Document, whichSlot, ':=' + queryText, false); // make the second slot be a computed field on the data doc that calls ChatGpt + Doc.SetField(this.Document, whichSlot, ':=' + queryText, false); // make the second slot be a computed field on the data doc that calls ChatGpt } } return layoutTemplateString; diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index f8ce50c98..4a5c32b4c 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -31,7 +31,6 @@ import { MakeTemplate, makeUserTemplateButton } from '../../util/DropConverter'; import { UPDATE_SERVER_CACHE } from '../../util/LinkManager'; import { ScriptingGlobals } from '../../util/ScriptingGlobals'; import { SearchUtil } from '../../util/SearchUtil'; -import { SharingManager } from '../../util/SharingManager'; import { SnappingManager } from '../../util/SnappingManager'; import { UndoManager, undoBatch, undoable } from '../../util/UndoManager'; import { ContextMenu } from '../ContextMenu'; @@ -47,7 +46,6 @@ import { DocumentLinksButton } from './DocumentLinksButton'; import './DocumentView.scss'; import { FieldViewProps, FieldViewSharedProps } from './FieldView'; import { FocusViewOptions } from './FocusViewOptions'; -import { KeyValueBox } from './KeyValueBox'; import { OpenWhere } from './OpenWhere'; import { FormattedTextBox } from './formattedText/FormattedTextBox'; import { PresEffect, PresEffectDirection } from './trails'; @@ -115,7 +113,7 @@ export class DocumentViewInternal extends DocComponent = undefined; // needs to be accessed from DocumentView wrapper class + @observable _componentView: Opt> = undefined; // needs to be accessed from DocumentView wrapper class @observable _animateScaleTime: Opt = undefined; // milliseconds for animating between views. defaults to 300 if not uset @observable _animateScalingTo = 0; @@ -627,7 +625,7 @@ export class DocumentViewInternal extends DocComponent DocUtils.Zip(this.Document) }); - (this.Document._type_collection !== CollectionViewType.Docking || !Doc.noviceMode) && constantItems.push({ description: 'Share', event: () => SharingManager.Instance.open(this._docView), icon: 'users' }); + constantItems.push({ description: 'Share', event: () => DocumentView.ShareOpen(this._docView), icon: 'users' }); if (this._props.removeDocument && Doc.ActiveDashboard !== this.Document) { // need option to gray out menu items ... preferably with a '?' that explains why they're grayed out (eg., no permissions) constantItems.push({ description: 'Close', event: this.deleteClicked, icon: 'times' }); @@ -699,7 +697,7 @@ export class DocumentViewInternal extends DocComponent this._props.ScreenToLocalTransform().translate(0, -this.headerMargin); onClickFunc = this.disableClickScriptFunc ? undefined : () => this.onClickHandler; setHeight = (height: number) => { !this._props.suppressSetHeight && (this.layoutDoc._height = Math.min(NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER), height)); } // prettier-ignore - setContentView = action((view: ViewBoxInterface) => { this._componentView = view; }); // prettier-ignore + setContentView = action((view: ViewBoxInterface) => { this._componentView = view; }); // prettier-ignore isContentActive = (): boolean | undefined => this._isContentActive; childFilters = () => [...this._props.childFilters(), ...StrListCast(this.layoutDoc.childFilters)]; @@ -725,7 +723,7 @@ export class DocumentViewInternal extends DocComponent u.user.email === this.dataDoc.author)?.sharingDoc.headingColor, StrCast(Doc.SharingDoc().headingColor, SnappingManager.userBackgroundColor)) + // StrCast(SharingManager.Instance.users.find(u => u.user.email === this.dataDoc.author)?.sharingDoc.headingColor, + StrCast(Doc.SharingDoc().headingColor, SnappingManager.userBackgroundColor) + // ) ); const dropdownWidth = this._titleRef.current?._editing || this._changingTitleField ? Math.max(10, (this._titleDropDownInnerWidth * this.titleHeight) / 30) : 0; const sidebarWidthPercent = +StrCast(this.layoutDoc.layout_sidebarWidthPercent).replace('%', ''); @@ -839,7 +839,7 @@ export class DocumentViewInternal extends DocComponent - {this._componentView instanceof KeyValueBox ? renderDoc : DocumentViewInternal.AnimationEffect(renderDoc, this.Document[Animation])} + {this._componentView?.isUnstyledView?.() ? renderDoc : DocumentViewInternal.AnimationEffect(renderDoc, this.Document[Animation])} {borderPath?.jsx}
); @@ -980,6 +980,8 @@ export class DocumentViewInternal extends DocComponent() { public static ROOT_DIV = 'documentView-effectsWrapper'; + // Sharing Manager + public static ShareOpen: (target?: DocumentView, targetDoc?: Doc) => void; // LinkFollower public static FollowLink: (linkDoc: Opt, sourceDoc: Doc, altKey: boolean) => boolean; // selection funcs @@ -1049,7 +1051,7 @@ export class DocumentView extends DocComponent() { @observable public static LongPress = false; @computed private get shouldNotScale() { - return (this.layout_fitWidth && !this.nativeWidth) || this._props.LayoutTemplateString?.includes(KeyValueBox.name) || [CollectionViewType.Docking].includes(this.Document._type_collection as any); + return (this.layout_fitWidth && !this.nativeWidth) || this.ComponentView?.isUnstyledView?.(); } @computed private get effectiveNativeWidth() { return this.shouldNotScale ? 0 : this.nativeWidth || NumCast(this.layoutDoc.width); @@ -1144,10 +1146,10 @@ export class DocumentView extends DocComponent() { } @computed get nativeWidth() { - return this._props.LayoutTemplateString?.includes(KeyValueBox.name) ? 0 : returnVal(this._props.NativeWidth?.(), Doc.NativeWidth(this.layoutDoc, this._props.TemplateDataDocument, !this.layout_fitWidth)); + return returnVal(this._props.NativeWidth?.(), Doc.NativeWidth(this.layoutDoc, this._props.TemplateDataDocument, !this.layout_fitWidth)); } @computed get nativeHeight() { - return this._props.LayoutTemplateString?.includes(KeyValueBox.name) ? 0 : returnVal(this._props.NativeHeight?.(), Doc.NativeHeight(this.layoutDoc, this._props.TemplateDataDocument, !this.layout_fitWidth)); + return returnVal(this._props.NativeHeight?.(), Doc.NativeHeight(this.layoutDoc, this._props.TemplateDataDocument, !this.layout_fitWidth)); } @computed public get centeringX() { return this._props.dontCenter?.includes('x') ? 0 : this.Xshift; } // prettier-ignore @computed public get centeringY() { return this._props.dontCenter?.includes('y') ? 0 : this.Yshift; } // prettier-ignore diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index c6c77d8d2..8a37000f7 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -45,7 +45,7 @@ export interface FieldViewSharedProps { containerViewPath?: () => DocumentView[]; fitContentsToBox?: () => boolean; // used by freeformview to fit its contents to its panel. corresponds to _freeform_fitContentsToBox property on a Document isGroupActive?: () => string | undefined; // is this document part of a group that is active - setContentViewBox?: (view: ViewBoxInterface) => any; // called by rendered field's viewBox so that DocumentView can make direct calls to the viewBox + setContentViewBox?: (view: ViewBoxInterface) => any; // called by rendered field's viewBox so that DocumentView can make direct calls to the viewBox PanelWidth: () => number; PanelHeight: () => number; isDocumentActive?: () => boolean | undefined; // whether a document should handle pointer events diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx index cd591fa42..66e210c03 100644 --- a/src/client/views/nodes/KeyValueBox.tsx +++ b/src/client/views/nodes/KeyValueBox.tsx @@ -13,10 +13,10 @@ import { DocumentType } from '../../documents/DocumentTypes'; import { Docs } from '../../documents/Documents'; import { SetupDrag } from '../../util/DragManager'; import { CompiledScript } from '../../util/Scripting'; -import { undoBatch } from '../../util/UndoManager'; +import { undoable } from '../../util/UndoManager'; import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; -import { ObservableReactComponent } from '../ObservableReactComponent'; +import { ViewBoxBaseComponent } from '../DocComponent'; import { DocumentIconContainer } from './DocumentIcon'; import { FieldView, FieldViewProps } from './FieldView'; import { ImageBox } from './ImageBox'; @@ -31,7 +31,7 @@ export type KVPScript = { onDelegate: boolean; }; @observer -export class KeyValueBox extends ObservableReactComponent { +export class KeyValueBox extends ViewBoxBaseComponent() { public static LayoutString() { return FieldView.LayoutString(KeyValueBox, 'data'); } @@ -48,19 +48,17 @@ export class KeyValueBox extends ObservableReactComponent { componentDidMount() { this._props.setContentViewBox?.(this); } - isKeyValueBox = returnTrue; - able = returnAlways; - layout_fitWidth = returnTrue; - onClickScriptDisable = returnAlways; + // ViewBoxInterface overrides + override isUnstyledView = returnTrue; // used by style provider via ViewBoxInterface - ignore opacity, anim effects, titles + override dontRegisterView = returnTrue; // don't want to follow links to this view + override onClickScriptDisable = returnAlways; @observable private rows: KeyValuePair[] = []; @observable _splitPercentage = 50; get fieldDocToLayout() { - return DocCast(this._props.Document); + return DocCast(this.Document); } - isUnstyledView = returnTrue; // used by style provider via ViewBoxInterface - dontRegisterView = returnTrue; // used by ViewBoxInterface @action onEnterKey = (e: React.KeyboardEvent): void => { @@ -83,7 +81,7 @@ export class KeyValueBox extends ObservableReactComponent { * @param value * @returns */ - public static CompileKVPScript(rawvalueIn: string): KVPScript | undefined { + public static CompileKVPScript = (rawvalueIn: string): KVPScript | undefined => { let rawvalue = rawvalueIn; const onDelegate = rawvalue.startsWith('='); rawvalue = onDelegate ? rawvalue.substring(1) : rawvalue; @@ -97,9 +95,9 @@ export class KeyValueBox extends ObservableReactComponent { script = ScriptField.CompileScript(value, {}, true, undefined, DocumentIconContainer.getTransformer()); } return !script.compiled ? undefined : { script, type, onDelegate }; - } + }; - public static ApplyKVPScript(doc: Doc, key: string, kvpScript: KVPScript, forceOnDelegate?: boolean, setResult?: (value: FieldResult) => void) { + public static ApplyKVPScript = (doc: Doc, key: string, kvpScript: KVPScript, forceOnDelegate?: boolean, setResult?: (value: FieldResult) => void) => { const { script, type, onDelegate } = kvpScript; // const target = onDelegate ? Doc.Layout(doc.layout) : Doc.GetProto(doc); // bcz: TODO need to be able to set fields on layout templates const target = forceOnDelegate || onDelegate || key.startsWith('_') ? doc : DocCast(doc.proto, doc); @@ -127,14 +125,13 @@ export class KeyValueBox extends ObservableReactComponent { return true; } return false; - } + }; - @undoBatch - public static SetField(doc: Doc, key: string, value: string, forceOnDelegate?: boolean, setResult?: (value: FieldResult) => void) { - const script = this.CompileKVPScript(value); + public static SetField = undoable((doc: Doc, key: string, value: string, forceOnDelegate?: boolean, setResult?: (value: FieldResult) => void) => { + const script = KeyValueBox.CompileKVPScript(value); if (!script) return false; - return this.ApplyKVPScript(doc, key, script, forceOnDelegate, setResult); - } + return KeyValueBox.ApplyKVPScript(doc, key, script, forceOnDelegate, setResult); + }, 'Set Doc Field'); onPointerDown = (e: React.PointerEvent): void => { if (e.buttons === 1 && this._props.isSelected()) { @@ -249,15 +246,15 @@ export class KeyValueBox extends ObservableReactComponent { getFieldView = () => { const rows = this.rows.filter(row => row.isChecked); if (rows.length > 1) { - const parent = Docs.Create.StackingDocument([], { _layout_autoHeight: true, _width: 300, title: `field views for ${DocCast(this._props.Document).title}`, _chromeHidden: true }); + const parent = Docs.Create.StackingDocument([], { _layout_autoHeight: true, _width: 300, title: `field views for ${DocCast(this.Document).title}`, _chromeHidden: true }); rows.forEach(row => { - const field = this.createFieldView(DocCast(this._props.Document), row); + const field = this.createFieldView(DocCast(this.Document), row); field && Doc.AddDocToList(parent, 'data', field); row.uncheck(); }); return parent; } - return rows.length ? this.createFieldView(DocCast(this._props.Document), rows.lastElement()) : undefined; + return rows.length ? this.createFieldView(DocCast(this.Document), rows.lastElement()) : undefined; }; createFieldView = (templateDoc: Doc, row: KeyValuePair) => { @@ -305,7 +302,7 @@ export class KeyValueBox extends ObservableReactComponent { openItems.push({ description: 'Default Perspective', event: () => { - this._props.addDocTab(this._props.Document, OpenWhere.close); + this._props.addDocTab(this.Document, OpenWhere.close); this._props.addDocTab(this.fieldDocToLayout, OpenWhere.addRight); }, icon: 'image', @@ -341,6 +338,9 @@ export class KeyValueBox extends ObservableReactComponent {
); } + public static Init() { + Doc.SetField = KeyValueBox.SetField; + } } Docs.Prototypes.TemplateMap.set(DocumentType.KVP, { diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx index 878b0e54c..397cc15ed 100644 --- a/src/client/views/nodes/KeyValuePair.tsx +++ b/src/client/views/nodes/KeyValuePair.tsx @@ -15,7 +15,6 @@ import { EditableView } from '../EditableView'; import { ObservableReactComponent } from '../ObservableReactComponent'; import { DefaultStyleProvider } from '../StyleProvider'; import { returnEmptyDocViewList } from './DocumentView'; -import { KeyValueBox } from './KeyValueBox'; import './KeyValueBox.scss'; import './KeyValuePair.scss'; import { OpenWhere } from './OpenWhere'; @@ -136,7 +135,7 @@ export class KeyValuePair extends ObservableReactComponent { pinToPres: returnZero, }} GetValue={() => Field.toKeyValueString(this._props.doc, this._props.keyName)} - SetValue={(value: string) => KeyValueBox.SetField(this._props.doc, this._props.keyName, value)} + SetValue={(value: string) => Doc.SetField(this._props.doc, this._props.keyName, value)} />
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 9038ed27a..b8b9f63a9 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -25,7 +25,7 @@ import { CollectionFreeFormView } from '../collections/collectionFreeForm'; import { CollectionStackingView } from '../collections/CollectionStackingView'; import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; -import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../DocComponent'; +import { ViewBoxAnnotatableComponent } from '../DocComponent'; import { Colors } from '../global/globalEnums'; import { PDFViewer } from '../pdf/PDFViewer'; import { PinDocView, PinProps } from '../PinFuncs'; @@ -39,7 +39,7 @@ import './PDFBox.scss'; import { CreateImage } from './WebBoxRenderer'; @observer -export class PDFBox extends ViewBoxAnnotatableComponent() implements ViewBoxInterface { +export class PDFBox extends ViewBoxAnnotatableComponent() { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(PDFBox, fieldKey); } @@ -283,7 +283,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent() implem !this.Document._layout_fitWidth && (this.Document._height = NumCast(this.Document._width) * (nh / nw)); }; - public search = action((searchString: string, bwd?: boolean, clear: boolean = false) => { + override search = action((searchString: string, bwd?: boolean, clear: boolean = false) => { if (!this._searching && !clear) { this._searching = true; setTimeout(() => { diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index 1003bc473..198652310 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -48,7 +48,7 @@ import './WebBox.scss'; const { CreateImage } = require('./WebBoxRenderer'); @observer -export class WebBox extends ViewBoxAnnotatableComponent() implements ViewBoxInterface { +export class WebBox extends ViewBoxAnnotatableComponent() { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(WebBox, fieldKey); } @@ -112,7 +112,7 @@ export class WebBox extends ViewBoxAnnotatableComponent() implem } @action - search = (searchString: string, bwd?: boolean, clear: boolean = false) => { + override search = (searchString: string, bwd?: boolean, clear: boolean = false) => { if (!this._searching && !clear) { this._searching = true; setTimeout(() => { @@ -1000,7 +1000,7 @@ export class WebBox extends ViewBoxAnnotatableComponent() implem }; _innerCollectionView: CollectionFreeFormView | undefined; zoomScaling = () => this._innerCollectionView?.zoomScaling() ?? 1; - setInnerContent = (component: ViewBoxInterface) => { + setInnerContent = (component: ViewBoxInterface) => { this._innerCollectionView = component as CollectionFreeFormView; }; diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 8a4a7718a..321fdbb91 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -43,7 +43,7 @@ import { CollectionStackingView } from '../../collections/CollectionStackingView import { CollectionTreeView } from '../../collections/CollectionTreeView'; import { ContextMenu } from '../../ContextMenu'; import { ContextMenuProps } from '../../ContextMenuItem'; -import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../../DocComponent'; +import { ViewBoxAnnotatableComponent } from '../../DocComponent'; import { Colors } from '../../global/globalEnums'; import { LightboxView } from '../../LightboxView'; import { AnchorMenu } from '../../pdf/AnchorMenu'; @@ -73,12 +73,12 @@ import { schema } from './schema_rts'; import { SummaryView } from './SummaryView'; // import * as applyDevTools from 'prosemirror-dev-tools'; -interface FormattedTextBoxProps extends FieldViewProps { +export interface FormattedTextBoxProps extends FieldViewProps { onBlur?: () => void; // callback when text loses focus autoFocus?: boolean; // whether text should get input focus when created } @observer -export class FormattedTextBox extends ViewBoxAnnotatableComponent() implements ViewBoxInterface { +export class FormattedTextBox extends ViewBoxAnnotatableComponent() { public static LayoutString(fieldStr: string) { return FieldView.LayoutString(FormattedTextBox, fieldStr); } diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts index 1ff862859..bf11dfe62 100644 --- a/src/client/views/nodes/formattedText/RichTextRules.ts +++ b/src/client/views/nodes/formattedText/RichTextRules.ts @@ -7,13 +7,11 @@ import { Id } from '../../../../fields/FieldSymbols'; import { List } from '../../../../fields/List'; import { NumCast, StrCast } from '../../../../fields/Types'; import { Utils } from '../../../../Utils'; -import { DocServer } from '../../../DocServer'; import { Docs } from '../../../documents/Documents'; -import { DocUtils } from '../../../documents/DocUtils'; import { CollectionViewType } from '../../../documents/DocumentTypes'; +import { DocUtils } from '../../../documents/DocUtils'; import { CollectionView } from '../../collections/CollectionView'; import { ContextMenu } from '../../ContextMenu'; -import { KeyValueBox } from '../KeyValueBox'; import { FormattedTextBox } from './FormattedTextBox'; import { wrappingInputRule } from './prosemirrorPatches'; import { RichTextMenu } from './RichTextMenu'; @@ -311,10 +309,10 @@ export class RichTextRules { } }; const getTitledDoc = (title: string) => { - if (!DocServer.FindDocByTitle(title)) { + if (!Doc.FindDocByTitle(title)) { Docs.Create.TextDocument('', { title: title, _width: 400, _layout_fitWidth: true, _layout_autoHeight: true }); } - const titledDoc = DocServer.FindDocByTitle(title); + const titledDoc = Doc.FindDocByTitle(title); return titledDoc ? Doc.BestEmbedding(titledDoc) : titledDoc; }; const target = getTitledDoc(docTitle); @@ -337,14 +335,14 @@ export class RichTextRules { const assign = match[4] === ':' ? (match[4] = '') : match[4]; const value = match[5]; const dataDoc = value === undefined ? !fieldKey.startsWith('_') : !assign?.startsWith('='); - const getTitledDoc = (title: string) => DocServer.FindDocByTitle(title); + const getTitledDoc = (title: string) => Doc.FindDocByTitle(title); // if the value has commas assume its an array (unless it's part of a chat gpt call indicated by '((' ) if (value?.includes(',') && !value.startsWith('((')) { const values = value.split(','); const strs = values.some(v => !v.match(/^[-]?[0-9.]$/)); this.Document[DocData][fieldKey] = strs ? new List(values) : new List(values.map(v => Number(v))); } else if (value) { - KeyValueBox.SetField( + Doc.SetField( this.Document, fieldKey, assign + value, @@ -367,7 +365,7 @@ export class RichTextRules { // pass the contents between '((' and '))' to chatGPT and append the result new InputRule(/(^|[^=])(\(\(.*\)\))$/, (state, match, start, end) => { let count = 0; // ignore first return value which will be the notation that chat is pending a result - KeyValueBox.SetField(this.Document, '', match[2], false, (gptval: FieldResult) => { + Doc.SetField(this.Document, '', match[2], false, (gptval: FieldResult) => { if (count) { const tr = this.TextBox.EditorView?.state.tr.insertText(' ' + (gptval as string)); tr && this.TextBox.EditorView?.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(end + 2), tr.doc.resolve(end + 2 + (gptval as string).length)))); diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index bb6995398..725221a66 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -225,7 +225,17 @@ export class Doc extends RefField { @observable public static GuestTarget: Doc | undefined = undefined; @observable public static GuestMobile: Doc | undefined = undefined; @observable.shallow public static CurrentlyLoading: Doc[] = observable([]); - // removes from currently loading display + // DocServer api + public static FindDocByTitle(title: string) { + const foundDocId = + title && + Array.from(Object.keys(DocServer.Cache())) + .filter(key => DocServer.Cache()[key] instanceof Doc) + .find(key => (DocServer.Cache()[key] as Doc).title === title); + + return foundDocId ? (DocServer.Cache()[foundDocId] as Doc) : undefined; + } + // removes from currently loading doc set public static removeCurrentlyLoading(doc: Doc) { if (Doc.CurrentlyLoading) { const index = Doc.CurrentlyLoading.indexOf(doc); @@ -238,12 +248,14 @@ export class Doc extends RefField { runInAction(() => Doc.CurrentlyLoading.push(doc)); } } - + // LinkManager api public static AddLink: (link: Doc, checkExists?: boolean) => void; public static DeleteLink: (link: Doc) => void; public static Links: (link: Doc | undefined) => Doc[]; public static getOppositeAnchor: (linkDoc: Doc, anchor: Doc) => Doc | undefined; - + // KeyValue SetField + public static SetField: (doc: Doc, key: string, value: string, forceOnDelegate?: boolean, setResult?: (value: FieldResult) => void) => boolean; + // UserDoc "API" public static get MySharedDocs() { return DocCast(Doc.UserDoc().mySharedDocs); } // prettier-ignore public static get MyUserDocView() { return DocCast(Doc.UserDoc().myUserDocView); } // prettier-ignore public static get MyDockedBtns() { return DocCast(Doc.UserDoc().myDockedBtns); } // prettier-ignore -- cgit v1.2.3-70-g09d2