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/util/SharingManager.tsx | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) (limited to 'src/client/util/SharingManager.tsx') diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx index fddf735e3..03f7e9b71 100644 --- a/src/client/util/SharingManager.tsx +++ b/src/client/util/SharingManager.tsx @@ -6,13 +6,14 @@ import { observer } from 'mobx-react'; import * as React from 'react'; import Select from 'react-select'; import * as RequestPromise from 'request-promise'; +import { ClientUtils } from '../../ClientUtils'; +import { Utils } from '../../Utils'; import { Doc, DocListCast, DocListCastAsync, HierarchyMapping, ReverseHierarchyMap } from '../../fields/Doc'; import { AclAdmin, AclPrivate, DocAcl, DocData } from '../../fields/DocSymbols'; import { FieldLoader } from '../../fields/FieldLoader'; import { Id } from '../../fields/FieldSymbols'; import { StrCast } from '../../fields/Types'; -import { distributeAcls, GetEffectiveAcl, normalizeEmail, SharingPermissions, TraceMobx } from '../../fields/util'; -import { Utils } from '../../Utils'; +import { GetEffectiveAcl, SharingPermissions, TraceMobx, distributeAcls, normalizeEmail } from '../../fields/util'; import { DocServer } from '../DocServer'; import { DictationOverlay } from '../views/DictationOverlay'; import { MainViewModal } from '../views/MainViewModal'; @@ -84,7 +85,6 @@ export class SharingManager extends React.Component<{}> { @observable private upgradeNested: boolean = false; // whether child docs in a collection/dashboard should be changed to be less private - initially selected so default is upgrade all @observable private layoutDocAcls: boolean = false; // whether the layout doc or data doc's acls are to be used @observable private myDocAcls: boolean = false; // whether the My Docs checkbox is selected or not - @observable private _buttonDown = false; // private get linkVisible() { // return this.targetDoc ? this.targetDoc["acl-" + PublicKey] !== SharingPermissions.None : false; @@ -136,9 +136,11 @@ export class SharingManager extends React.Component<{}> { populateUsers = async () => { if (!this.populating && Doc.UserDoc()[Id] !== Utils.GuestID()) { this.populating = true; - const userList = await RequestPromise.get(Utils.prepend('/getUsers')); - const raw = (JSON.parse(userList) as User[]).filter(user => user.email !== 'guest' && user.email !== Doc.CurrentUserEmail); - runInAction(() => (FieldLoader.ServerLoadStatus.message = 'users')); + const userList = await RequestPromise.get(ClientUtils.prepend('/getUsers')); + const raw = (JSON.parse(userList) as User[]).filter(user => user.email !== 'guest' && user.email !== ClientUtils.CurrentUserEmail); + runInAction(() => { + FieldLoader.ServerLoadStatus.message = 'users'; + }); const docs = await DocServer.GetRefFields(raw.reduce((list, user) => [...list, user.sharingDocumentId, user.linkDatabaseId], [] as string[])); raw.map( action((newUser: User) => { @@ -191,7 +193,8 @@ export class SharingManager extends React.Component<{}> { this.users .filter(({ user: { email } }) => JSON.parse(StrCast(group.members)).includes(email)) .forEach(({ user, sharingDoc }) => { - if (permission !== SharingPermissions.None) Doc.AddDocToList(sharingDoc, doc.dockingConfig ? dashStorage : storage, doc); // add the doc to the sharingDoc if it hasn't already been added + if (permission !== SharingPermissions.None) + Doc.AddDocToList(sharingDoc, doc.dockingConfig ? dashStorage : storage, doc); // add the doc to the sharingDoc if it hasn't already been added else GetEffectiveAcl(doc, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, ((doc.createdFrom as Doc) || doc).dockingConfig ? dashStorage : storage, (doc.createdFrom as Doc) || doc); // remove the doc from the list if it already exists }); } @@ -231,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' ? Doc.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); @@ -300,7 +303,7 @@ export class SharingManager extends React.Component<{}> { * Copies the Public sharing url to the user's clipboard. */ private copyURL = (e: any) => { - Utils.CopyText(Utils.shareUrl(this.targetDoc![Id])); + ClientUtils.CopyText(ClientUtils.shareUrl(this.targetDoc![Id])); }; /** @@ -496,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(Doc.CurrentUserEmail)}`; + const userKey = `acl-${normalizeEmail(ClientUtils.CurrentUserEmail)}`; const curUserPermission = StrCast(targetDoc[userKey]); // const curUserPermission = HierarchyMapping.get(effectiveAcls[0])!.name userListContents.unshift( sameAuthor ? (
- {targetDoc?.author === Doc.CurrentUserEmail ? 'Me' : StrCast(targetDoc?.author)} + {targetDoc?.author === ClientUtils.CurrentUserEmail ? 'Me' : StrCast(targetDoc?.author)}
Owner
) : null, - sameAuthor && targetDoc?.author !== Doc.CurrentUserEmail ? ( + sameAuthor && targetDoc?.author !== ClientUtils.CurrentUserEmail ? (
Me
-- 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/util/SharingManager.tsx') 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 939e18624af4252551f38c43335ee8ef0acd144c Mon Sep 17 00:00:00 2001 From: bobzel Date: Sun, 21 Apr 2024 19:03:49 -0400 Subject: more lint cleanup --- .eslintrc.json | 1 + package-lock.json | 3 +- package.json | 2 +- .../apis/google_docs/GooglePhotosClientUtils.ts | 82 ++- src/client/util/BranchingTrailManager.tsx | 41 +- src/client/util/CalendarManager.tsx | 39 +- src/client/util/CurrentUserUtils.ts | 2 +- src/client/util/DragManager.ts | 18 +- src/client/util/GroupManager.tsx | 85 ++- src/client/util/HypothesisUtils.ts | 2 - src/client/util/Import & Export/ImageUtils.ts | 2 +- src/client/util/LinkFollower.ts | 7 +- src/client/util/LinkManager.ts | 28 +- src/client/util/ScriptingGlobals.ts | 1 + src/client/util/SearchUtil.ts | 20 +- src/client/util/SettingsManager.tsx | 323 +++++---- src/client/util/SharingManager.tsx | 753 +++++++++++---------- src/client/util/SnappingManager.ts | 3 + src/client/util/UndoManager.ts | 90 +-- src/client/util/reportManager/ReportManager.tsx | 38 +- src/client/views/ContextMenuItem.tsx | 15 +- src/client/views/DashboardView.tsx | 41 +- src/client/views/DictationOverlay.tsx | 55 +- src/client/views/DocComponent.tsx | 5 +- src/client/views/DocumentButtonBar.tsx | 114 ++-- src/client/views/EditableView.tsx | 23 +- src/client/views/FieldsDropdown.tsx | 16 +- src/client/views/FilterPanel.tsx | 1 + src/client/views/GlobalKeyHandler.ts | 58 +- src/client/views/InkingStroke.tsx | 109 +-- src/client/views/MainView.tsx | 42 +- src/client/views/MetadataEntryMenu.tsx | 196 ------ src/client/views/OverlayView.tsx | 7 +- src/client/views/PropertiesButtons.tsx | 182 +++-- src/client/views/PropertiesView.tsx | 44 +- src/client/views/SidebarAnnos.tsx | 13 +- src/client/views/StyleProvider.tsx | 75 +- src/client/views/UndoStack.tsx | 19 +- src/client/views/animationtimeline/Timeline.tsx | 1 + src/client/views/animationtimeline/Track.tsx | 2 +- .../collections/CollectionMasonryViewFieldRow.tsx | 96 +-- src/client/views/collections/CollectionMenu.tsx | 19 +- .../views/collections/CollectionNoteTakingView.tsx | 85 ++- .../views/collections/CollectionPileView.tsx | 13 +- .../collections/CollectionStackedTimeline.tsx | 152 +++-- src/client/views/collections/CollectionSubView.tsx | 31 +- .../views/collections/CollectionTimeView.tsx | 32 +- .../views/collections/CollectionTreeView.tsx | 79 ++- src/client/views/collections/TabDocView.tsx | 347 +++++----- src/client/views/collections/TreeView.tsx | 46 +- .../CollectionFreeFormInfoUI.tsx | 33 +- .../CollectionFreeFormLayoutEngines.tsx | 38 +- .../collectionFreeForm/CollectionFreeFormView.tsx | 295 +++++--- .../collections/collectionFreeForm/MarqueeView.tsx | 10 +- .../collectionSchema/SchemaTableCell.tsx | 33 +- src/client/views/global/globalScripts.ts | 13 +- src/client/views/linking/LinkMenuItem.tsx | 32 +- src/client/views/linking/LinkPopup.tsx | 21 +- .../views/nodes/CollectionFreeFormDocumentView.tsx | 44 +- src/client/views/nodes/ComparisonBox.tsx | 17 +- src/client/views/nodes/DataVizBox/DataVizBox.tsx | 115 ++-- .../nodes/DataVizBox/components/Histogram.tsx | 230 +++---- .../nodes/DataVizBox/components/LineChart.tsx | 107 ++- .../views/nodes/DataVizBox/components/TableBox.tsx | 80 ++- src/client/views/nodes/DocumentContentsView.tsx | 33 +- src/client/views/nodes/DocumentLinksButton.tsx | 34 +- src/client/views/nodes/DocumentView.tsx | 2 +- src/client/views/nodes/FieldView.tsx | 10 +- src/client/views/nodes/FontIconBox/FontIconBox.tsx | 60 +- src/client/views/nodes/ImageBox.tsx | 7 +- src/client/views/nodes/KeyValuePair.tsx | 13 +- src/client/views/nodes/LabelBox.tsx | 25 +- src/client/views/nodes/LinkAnchorBox.tsx | 6 +- src/client/views/nodes/MapBox/MapBox.tsx | 25 +- .../views/nodes/MapboxMapBox/MapboxContainer.tsx | 31 +- src/client/views/nodes/PDFBox.tsx | 100 ++- .../views/nodes/RecordingBox/RecordingBox.tsx | 37 +- src/client/views/nodes/ScreenshotBox.tsx | 29 +- src/client/views/nodes/VideoBox.tsx | 103 ++- src/client/views/nodes/WebBox.tsx | 153 +++-- .../views/nodes/formattedText/DashFieldView.tsx | 310 +++++---- .../nodes/formattedText/FormattedTextBox.scss | 3 +- .../views/nodes/formattedText/FormattedTextBox.tsx | 2 +- .../formattedText/ProsemirrorExampleTransfer.ts | 51 +- .../views/nodes/formattedText/RichTextMenu.tsx | 226 +++---- .../views/nodes/formattedText/RichTextRules.ts | 2 +- src/client/views/nodes/formattedText/marks_rts.ts | 30 +- src/client/views/nodes/formattedText/nodes_rts.ts | 14 +- src/client/views/nodes/trails/PresBox.tsx | 635 ++++++++++------- src/client/views/nodes/trails/PresElementBox.tsx | 96 +-- src/client/views/pdf/Annotation.tsx | 4 +- src/client/views/pdf/GPTPopup/GPTPopup.tsx | 3 +- src/client/views/search/SearchBox.tsx | 68 +- src/client/views/topbar/TopBar.tsx | 25 +- src/fields/Doc.ts | 56 +- src/fields/List.ts | 34 +- src/fields/ObjectField.ts | 1 + src/fields/Proxy.ts | 17 +- src/fields/Schema.ts | 2 +- src/fields/Types.ts | 15 +- src/fields/documentSchemas.ts | 3 +- src/mobile/ImageUpload.tsx | 77 +-- src/mobile/MobileInterface.tsx | 2 +- src/server/ApiManagers/UploadManager.ts | 4 +- 104 files changed, 3740 insertions(+), 3134 deletions(-) delete mode 100644 src/client/views/MetadataEntryMenu.tsx (limited to 'src/client/util/SharingManager.tsx') diff --git a/.eslintrc.json b/.eslintrc.json index 0c4e375a9..780626412 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -53,6 +53,7 @@ "react/destructuring-assignment": 0, "no-restricted-globals": ["error", "event"], "no-param-reassign": ["error", { "props": false }], + "import/no-cycle": 0, "no-alert": 0, "radix": "off" }, diff --git a/package-lock.json b/package-lock.json index cc1a0ea64..c616bb6ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -183,6 +183,7 @@ "react-measure": "^2.5.2", "react-resizable": "^3.0.5", "react-select": "^5.8.0", + "react-type-animation": "^3.2.0", "react-xarrows": "^2.0.2", "readline": "^1.3.0", "recharts": "^2.10.3", @@ -287,7 +288,6 @@ "jsdom": "^24.0.0", "mocha": "^10.2.0", "prettier": "^3.1.0", - "react-type-animation": "^3.2.0", "scss-loader": "0.0.1", "style-loader": "^4.0.0", "ts-loader": "^9.5.1", @@ -30110,7 +30110,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/react-type-animation/-/react-type-animation-3.2.0.tgz", "integrity": "sha512-WXTe0i3rRNKjmggPvT5ntye1QBt0ATGbijeW6V3cQe2W0jaMABXXlPPEdtofnS9tM7wSRHchEvI9SUw+0kUohw==", - "dev": true, "peerDependencies": { "prop-types": "^15.5.4", "react": ">= 15.0.0", diff --git a/package.json b/package.json index 3f2f5a70f..a3ba6bdd9 100644 --- a/package.json +++ b/package.json @@ -83,7 +83,6 @@ "jsdom": "^24.0.0", "mocha": "^10.2.0", "prettier": "^3.1.0", - "react-type-animation": "^3.2.0", "scss-loader": "0.0.1", "style-loader": "^4.0.0", "ts-loader": "^9.5.1", @@ -267,6 +266,7 @@ "react-measure": "^2.5.2", "react-resizable": "^3.0.5", "react-select": "^5.8.0", + "react-type-animation": "^3.2.0", "react-xarrows": "^2.0.2", "readline": "^1.3.0", "recharts": "^2.10.3", diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts index 757031fec..07a2708ec 100644 --- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts +++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts @@ -1,3 +1,5 @@ +/* eslint-disable no-use-before-define */ +import Photos = require('googlephotos'); import { AssertionError } from 'assert'; import { EditorState } from 'prosemirror-state'; import { ClientUtils } from '../../../ClientUtils'; @@ -5,14 +7,12 @@ import { Doc, DocListCastAsync, Opt } from '../../../fields/Doc'; import { Id } from '../../../fields/FieldSymbols'; import { RichTextField } from '../../../fields/RichTextField'; import { RichTextUtils } from '../../../fields/RichTextUtils'; -import { Cast, StrCast } from '../../../fields/Types'; -import { ImageField } from '../../../fields/URLField'; +import { Cast, ImageCast, StrCast } from '../../../fields/Types'; import { MediaItem, NewMediaItemResult } from '../../../server/apis/google/SharedTypes'; import { Networking } from '../../Network'; import { DocUtils, Docs, DocumentOptions } from '../../documents/Documents'; import { FormattedTextBox } from '../../views/nodes/formattedText/FormattedTextBox'; import { GoogleAuthenticationManager } from '../GoogleAuthenticationManager'; -import Photos = require('googlephotos'); export namespace GooglePhotos { const endpoint = async () => new Photos(await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken()); @@ -76,17 +76,16 @@ export namespace GooglePhotos { export const CollectionToAlbum = async (options: AlbumCreationOptions): Promise> => { const { collection, title, descriptionKey, tag } = options; const dataDocument = Doc.GetProto(collection); - const images = ((await DocListCastAsync(dataDocument.data)) || []).filter(doc => Cast(doc.data, ImageField)); + const images = ((await DocListCastAsync(dataDocument.data)) || []).filter(doc => ImageCast(doc.data)); if (!images || !images.length) { return undefined; } - const resolved = title ? title : StrCast(collection.title) || `Dash Collection (${collection[Id]}`; + const resolved = title || StrCast(collection.title) || `Dash Collection (${collection[Id]}`; const { id, productUrl } = await Create.Album(resolved); const response = await Transactions.UploadImages(images, { id }, descriptionKey); if (response) { const { results, failed } = response; - let index: Opt; - while ((index = failed.pop()) !== undefined) { + for (let index = failed.pop(); index !== undefined; index = failed.pop()) { Doc.RemoveDocFromList(dataDocument, 'data', images.splice(index, 1)[0]); } const mediaItems: MediaItem[] = results.map(item => item.mediaItem); @@ -97,13 +96,12 @@ export namespace GooglePhotos { for (let i = 0; i < images.length; i++) { const image = Doc.GetProto(images[i]); const mediaItem = mediaItems[i]; - if (!mediaItem) { - continue; + if (mediaItem) { + image.googlePhotosId = mediaItem.id; + image.googlePhotosAlbumUrl = productUrl; + image.googlePhotosUrl = mediaItem.productUrl || mediaItem.baseUrl; + idMapping[mediaItem.id] = image; } - image.googlePhotosId = mediaItem.id; - image.googlePhotosAlbumUrl = productUrl; - image.googlePhotosUrl = mediaItem.productUrl || mediaItem.baseUrl; - idMapping[mediaItem.id] = image; } collection.googlePhotosAlbumUrl = productUrl; collection.googlePhotosIdMapping = idMapping; @@ -114,6 +112,7 @@ export namespace GooglePhotos { Transactions.AddTextEnrichment(collection, `Find me at ${ClientUtils.prepend(`/doc/${collection[Id]}?sharing=true`)}`); return { albumId: id, mediaItems }; } + return undefined; }; } @@ -124,7 +123,7 @@ export namespace GooglePhotos { await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken(); const response = await Query.ContentSearch(requested); const uploads = await Transactions.WriteMediaItemsToServer(response); - const children = uploads.map((upload: Transactions.UploadInformation) => Docs.Create.ImageDocument(ClientUtils.fileUrl(upload.fileNames.clean) /*, {"data_contentSize":upload.contentSize}*/)); + const children = uploads.map((upload: Transactions.UploadInformation) => Docs.Create.ImageDocument(ClientUtils.fileUrl(upload.fileNames.clean) /* , {"data_contentSize":upload.contentSize} */)); const options = { _width: 500, _height: 500 }; return constructor(children, options); }; @@ -144,7 +143,7 @@ export namespace GooglePhotos { const images = (await DocListCastAsync(collection.data))!.map(Doc.GetProto); images?.forEach(image => tagMapping.set(image[Id], ContentCategories.NONE)); const values = Object.values(ContentCategories).filter(value => value !== ContentCategories.NONE); - for (const value of values) { + values.forEach(async value => { const searched = (await ContentSearch({ included: [value] }))?.mediaItems?.map(({ id }) => id); searched?.forEach(async id => { const image = await Cast(idMapping[id], Doc); @@ -154,7 +153,7 @@ export namespace GooglePhotos { !tags?.includes(value) && tagMapping.set(key, tags + delimiter + value); } }); - } + }); images?.forEach(image => { const concatenated = tagMapping.get(image[Id])!; const tags = concatenated.split(delimiter); @@ -200,9 +199,10 @@ export namespace GooglePhotos { export const AlbumSearch = async (albumId: string, pageSize = 100): Promise => { const photos = await endpoint(); const mediaItems: MediaItem[] = []; - let nextPageTokenStored: Opt = undefined; + let nextPageTokenStored: Opt; const found = 0; do { + // eslint-disable-next-line no-await-in-loop const response: any = await photos.mediaItems.search(albumId, pageSize, nextPageTokenStored); mediaItems.push(...response.mediaItems); nextPageTokenStored = response.nextPageToken; @@ -222,7 +222,7 @@ export namespace GooglePhotos { excluded.length && excluded.forEach(category => contentFilter.addExcludedContentCategories(category)); filters.setContentFilter(contentFilter); - const date = options.date; + const { date } = options; if (date) { const dateFilter = new photos.DateFilter(); if (date instanceof Date) { @@ -240,15 +240,11 @@ export namespace GooglePhotos { }); }; - export const GetImage = async (mediaItemId: string): Promise => { - return (await endpoint()).mediaItems.get(mediaItemId); - }; + export const GetImage = async (mediaItemId: string): Promise => (await endpoint()).mediaItems.get(mediaItemId); } namespace Create { - export const Album = async (title: string) => { - return (await endpoint()).albums.create(title); - }; + export const Album = async (title: string) => (await endpoint()).albums.create(title); } export namespace Transactions { @@ -278,6 +274,7 @@ export namespace GooglePhotos { return enrichmentItem.id; } } + return undefined; }; export const WriteMediaItemsToServer = async (body: { mediaItems: any[] }): Promise => { @@ -291,9 +288,12 @@ export namespace GooglePhotos { return undefined; } const baseUrls: string[] = await Promise.all( - response.results.map(item => { - return new Promise(resolve => Query.GetImage(item.mediaItem.id).then(item => resolve(item.baseUrl))); - }) + response.results.map( + item => + new Promise(resolve => { + Query.GetImage(item.mediaItem.id).then(item => resolve(item.baseUrl)); + }) + ) ); return baseUrls; }; @@ -303,27 +303,25 @@ export namespace GooglePhotos { failed: number[]; } - export const UploadImages = async (sources: Doc[], album?: AlbumReference, descriptionKey = 'caption'): Promise> => { + export const UploadImages = async (sources: Doc[], albumIn?: AlbumReference, descriptionKey = 'caption'): Promise> => { await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken(); - if (album && 'title' in album) { - album = await Create.Album(album.title); - } + const album = albumIn && 'title' in albumIn ? await Create.Album(albumIn.title) : albumIn; const media: MediaInput[] = []; - for (const source of sources) { - const data = Cast(Doc.GetProto(source).data, ImageField); - if (!data) { - return; - } - const url = data.url.href; - const target = Doc.MakeEmbedding(source); - const description = parseDescription(target, descriptionKey); - await DocUtils.makeCustomViewClicked(target, Docs.Create.FreeformDocument); - media.push({ url, description }); - } + sources + .filter(source => ImageCast(Doc.GetProto(source).data)) + .forEach(async source => { + const data = ImageCast(Doc.GetProto(source).data); + const url = data.url.href; + const target = Doc.MakeEmbedding(source); + const description = parseDescription(target, descriptionKey); + await DocUtils.makeCustomViewClicked(target, Docs.Create.FreeformDocument); + media.push({ url, description }); + }); if (media.length) { const results = await Networking.PostToServer('/googlePhotosMediaPost', { media, album }); return results; } + return undefined; }; const parseDescription = (document: Doc, descriptionKey: string) => { diff --git a/src/client/util/BranchingTrailManager.tsx b/src/client/util/BranchingTrailManager.tsx index 02879e3c4..28c00644f 100644 --- a/src/client/util/BranchingTrailManager.tsx +++ b/src/client/util/BranchingTrailManager.tsx @@ -1,18 +1,31 @@ +/* eslint-disable react/no-unused-class-component-methods */ +/* eslint-disable react/no-array-index-key */ import { action, computed, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc } from '../../fields/Doc'; import { Id } from '../../fields/FieldSymbols'; -import { PresBox } from '../views/nodes/trails'; import { OverlayView } from '../views/OverlayView'; +import { PresBox } from '../views/nodes/trails'; import { DocumentManager } from './DocumentManager'; -import { Docs } from '../documents/Documents'; -import { nullAudio } from '../../fields/URLField'; @observer export class BranchingTrailManager extends React.Component { + // eslint-disable-next-line no-use-before-define public static Instance: BranchingTrailManager; + // stack of the history + @observable private slideHistoryStack: String[] = []; + @observable private containsSet: Set = new Set(); + // docId to Doc map + @observable private docIdToDocMap: Map = new Map(); + + // prev pres to copmare with + @observable private prevPresId: String | null = null; + @action setPrevPres = action((newId: String | null) => { + this.prevPresId = newId; + }); + constructor(props: any) { super(props); makeObservable(this); @@ -22,7 +35,7 @@ export class BranchingTrailManager extends React.Component { } setupUi = () => { - OverlayView.Instance.addWindow(, { x: 100, y: 150, width: 1000, title: 'Branching Trail' }); + OverlayView.Instance.addWindow(, { x: 100, y: 150, width: 1000, title: 'Branching Trail' }); // OverlayView.Instance.forceUpdate(); console.log(OverlayView.Instance); // let hi = Docs.Create.TextDocument("beee", { @@ -36,23 +49,11 @@ export class BranchingTrailManager extends React.Component { console.log(DocumentManager._overlayViews); }; - // stack of the history - @observable private slideHistoryStack: String[] = []; @action setSlideHistoryStack = action((newArr: String[]) => { this.slideHistoryStack = newArr; }); - @observable private containsSet: Set = new Set(); - - // prev pres to copmare with - @observable private prevPresId: String | null = null; - @action setPrevPres = action((newId: String | null) => { - this.prevPresId = newId; - }); - - // docId to Doc map - @observable private docIdToDocMap: Map = new Map(); - + // eslint-disable-next-line react/sort-comp observeDocumentChange = (targetDoc: Doc, pres: PresBox) => { const presId = pres.Document[Id]; if (this.prevPresId === presId) { @@ -106,7 +107,7 @@ export class BranchingTrailManager extends React.Component { if (this.slideHistoryStack.length === 0) { Doc.UserDoc().isBranchingMode = false; } - //PresBox.NavigateToTarget(targetDoc, targetDoc); + // PresBox.NavigateToTarget(targetDoc, targetDoc); }; @computed get trailBreadcrumbs() { @@ -116,11 +117,11 @@ export class BranchingTrailManager extends React.Component { const [presId, targetDocId] = info.split(','); const doc = this.docIdToDocMap.get(targetDocId); if (!doc) { - return <>; + return null; } return ( - -{'>'} diff --git a/src/client/util/CalendarManager.tsx b/src/client/util/CalendarManager.tsx index 6e9094b3a..46aa4d238 100644 --- a/src/client/util/CalendarManager.tsx +++ b/src/client/util/CalendarManager.tsx @@ -1,4 +1,10 @@ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ +import { DateRangePicker, Provider, defaultTheme } from '@adobe/react-spectrum'; +import { IconLookup, faPlus } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { TextField } from '@mui/material'; +import { Button } from 'browndash-components'; import { action, computed, makeObservable, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; @@ -6,21 +12,16 @@ import Select from 'react-select'; import { Doc, DocListCast } from '../../fields/Doc'; import { DocData } from '../../fields/DocSymbols'; import { StrCast } from '../../fields/Types'; +import { Docs } from '../documents/Documents'; import { DictationOverlay } from '../views/DictationOverlay'; import { MainViewModal } from '../views/MainViewModal'; +import { ObservableReactComponent } from '../views/ObservableReactComponent'; import { DocumentView } from '../views/nodes/DocumentView'; import { TaskCompletionBox } from '../views/nodes/TaskCompletedBox'; import './CalendarManager.scss'; import { DocumentManager } from './DocumentManager'; import { SelectionManager } from './SelectionManager'; import { SettingsManager } from './SettingsManager'; -// import { DateRange, Range, RangeKeyDict } from 'react-date-range'; -import { DateRangePicker, Provider, defaultTheme } from '@adobe/react-spectrum'; -import { IconLookup, faPlus } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Button } from 'browndash-components'; -import { Docs } from '../documents/Documents'; -import { ObservableReactComponent } from '../views/ObservableReactComponent'; // import 'react-date-range/dist/styles.css'; // import 'react-date-range/dist/theme/default.css'; @@ -47,6 +48,7 @@ const formatCalendarDateToString = (calendarDate: any) => { @observer export class CalendarManager extends ObservableReactComponent<{}> { + // eslint-disable-next-line no-use-before-define public static Instance: CalendarManager; @observable private isOpen = false; @observable private targetDoc: Doc | undefined = undefined; // the target document @@ -83,10 +85,10 @@ export class CalendarManager extends ObservableReactComponent<{}> { this.creationType = type; }; - public open = (target?: DocumentView, target_doc?: Doc) => { + public open = (target?: DocumentView, targetDoc?: Doc) => { console.log('hi'); runInAction(() => { - this.targetDoc = target_doc || target?.Document; + this.targetDoc = targetDoc || target?.Document; this.targetDocView = target; DictationOverlay.Instance.hasActiveModal = true; this.isOpen = this.targetDoc !== undefined; @@ -117,7 +119,7 @@ export class CalendarManager extends ObservableReactComponent<{}> { @action handleSelectChange = (option: any) => { if (option) { - let selectOpt = option as CalendarSelectOptions; + const selectOpt = option as CalendarSelectOptions; this.selectedExistingCalendarOption = selectOpt; this.calendarName = selectOpt.value; // or label } @@ -136,7 +138,7 @@ export class CalendarManager extends ObservableReactComponent<{}> { // TODO: Make undoable private addToCalendar = () => { - let docs = SelectionManager.Views.length < 2 ? [this.targetDoc] : SelectionManager.Views.map(docView => docView.Document); + const docs = SelectionManager.Views.length < 2 ? [this.targetDoc] : SelectionManager.Views.map(docView => docView.Document); const targetDoc = this.layoutDocAcls ? docs[0] : docs[0]?.[DocData]; // doc to add to calendar console.log(targetDoc); @@ -159,7 +161,7 @@ export class CalendarManager extends ObservableReactComponent<{}> { } } else { // find existing calendar based on selected name (should technically always find one) - const existingCalendar = this.existingCalendars.find(calendar => StrCast(calendar.title) === this.calendarName); + const existingCalendar = this.existingCalendars.find(findCal => StrCast(findCal.title) === this.calendarName); if (existingCalendar) calendar = existingCalendar; else { this.errorMessage = 'Must select an existing calendar'; @@ -252,11 +254,9 @@ export class CalendarManager extends ObservableReactComponent<{}> { @computed get calendarInterface() { - let docs = SelectionManager.Views.length < 2 ? [this.targetDoc] : SelectionManager.Views.map(docView => docView.Document); + const docs = SelectionManager.Views.length < 2 ? [this.targetDoc] : SelectionManager.Views.map(docView => docView.Document); const targetDoc = this.layoutDocAcls ? docs[0] : docs[0]?.[DocData]; - const currentDate = new Date(); - return (
{ {this.focusOn(docs.length < 2 ? StrCast(targetDoc?.title, 'this document') : '-multiple-')}

-
this.setInterationType('new-calendar')}> +
this.setInterationType('new-calendar')}> Add to New Calendar
-
this.setInterationType('existing-calendar')}> +
this.setInterationType('existing-calendar')}> Add to Existing calendar
@@ -317,7 +317,8 @@ export class CalendarManager extends ObservableReactComponent<{}> { color: StrCast(Doc.UserDoc().userColor), width: '100%', }), - }}> + }} + /> )}
@@ -351,6 +352,6 @@ export class CalendarManager extends ObservableReactComponent<{}> { } render() { - return ; + return ; } } diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 27ae5c9a0..6dba8027d 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -709,7 +709,7 @@ pie title Minerals in my tap water return [ { title: "Back", toolTip: "Go back", btnType: ButtonType.ClickButton, icon: "arrow-left", scripts: { onClick: '{ return webBack(); }' }}, { title: "Forward", toolTip: "Go forward", btnType: ButtonType.ClickButton, icon: "arrow-right", scripts: { onClick: '{ return webForward(); }'}}, - { title: "URL", toolTip: "URL", width: 250, btnType: ButtonType.EditableText, icon: "lock", ignoreClick: true, scripts: { script: '{ return webSetURL(value, _readOnly_); }'} }, + { title: "URL", toolTip: "URL", width: 250, btnType: ButtonType.EditText, icon: "lock", ignoreClick: true, scripts: { script: '{ return webSetURL(value, _readOnly_); }'} }, ]; } static videoTools() { diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index 62f055f1a..3890b7845 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -495,18 +495,20 @@ export namespace DragManager { .filter(pb => pb.width && pb.height) .map((pb, i) => pb.getContext('2d')!.drawImage(pdfBoxSrc[i], 0, 0)); } - [dragElement, ...Array.from(dragElement.getElementsByTagName('*'))].forEach(ele => { - (ele as any).style && ((ele as any).style.pointerEvents = 'none'); - }); + [dragElement, ...Array.from(dragElement.getElementsByTagName('*'))] + .map(dele => (dele as any).style) + .forEach(style => { + style && (style.pointerEvents = 'none'); + }); dragDiv.appendChild(dragElement); if (dragElement !== ele) { - const children = [Array.from(ele.children), Array.from(dragElement.children)]; - while (children[0].length) { - const childs = [children[0].pop(), children[1].pop()]; + const dragChildren = [Array.from(ele.children), Array.from(dragElement.children)]; + while (dragChildren[0].length) { + const childs = [dragChildren[0].pop(), dragChildren[1].pop()]; if (childs[0]?.children) { - children[0].push(...Array.from(childs[0].children)); - children[1].push(...Array.from(childs[1]!.children)); + dragChildren[0].push(...Array.from(childs[0].children)); + dragChildren[1].push(...Array.from(childs[1]!.children)); } if (childs[0]?.scrollTop) childs[1]!.scrollTop = childs[0].scrollTop; } diff --git a/src/client/util/GroupManager.tsx b/src/client/util/GroupManager.tsx index c261c0f1e..8d84dbad8 100644 --- a/src/client/util/GroupManager.tsx +++ b/src/client/util/GroupManager.tsx @@ -1,3 +1,5 @@ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Button, IconButton, Size, Type } from 'browndash-components'; import { action, computed, makeObservable, observable } from 'mobx'; @@ -30,6 +32,7 @@ export interface UserOptions { @observer export class GroupManager extends ObservableReactComponent<{}> { + // eslint-disable-next-line no-use-before-define static Instance: GroupManager; @observable isOpen: boolean = false; // whether the GroupManager is to be displayed or not. @observable private users: string[] = []; // list of users populated from the database. @@ -160,7 +163,7 @@ export class GroupManager extends ObservableReactComponent<{}> { addGroup(groupDoc: Doc): boolean { if (this.GroupManagerDoc) { Doc.AddDocToList(this.GroupManagerDoc, 'data', groupDoc); - this.GroupManagerDoc['data_modificationDate'] = new DateField(); + this.GroupManagerDoc.data_modificationDate = new DateField(); return true; } return false; @@ -202,7 +205,7 @@ export class GroupManager extends ObservableReactComponent<{}> { !memberList.includes(email) && memberList.push(email); groupDoc.members = JSON.stringify(memberList); SharingManager.Instance.shareWithAddedMember(groupDoc, email); - this.GroupManagerDoc && (this.GroupManagerDoc['data_modificationDate'] = new DateField()); + this.GroupManagerDoc && (this.GroupManagerDoc.data_modificationDate = new DateField()); } } @@ -216,10 +219,9 @@ export class GroupManager extends ObservableReactComponent<{}> { const memberList = JSON.parse(StrCast(groupDoc.members)); const index = memberList.indexOf(email); if (index !== -1) { - const user = memberList.splice(index, 1)[0]; groupDoc.members = JSON.stringify(memberList); SharingManager.Instance.removeMember(groupDoc, email); - this.GroupManagerDoc && (this.GroupManagerDoc['data_modificationDate'] = new DateField()); + this.GroupManagerDoc && (this.GroupManagerDoc.data_modificationDate = new DateField()); } } } @@ -275,7 +277,9 @@ export class GroupManager extends ObservableReactComponent<{}> { TaskCompletionBox.textDisplayed = 'Group created!'; TaskCompletionBox.taskCompleted = true; setTimeout( - action(() => (TaskCompletionBox.taskCompleted = false)), + action(() => { + TaskCompletionBox.taskCompleted = false; + }), 2000 ); }; @@ -292,7 +296,7 @@ export class GroupManager extends ObservableReactComponent<{}> {

- (this.buttonColour = this.inputRef.current?.value ? 'black' : '#979797'))} /> + { + this.buttonColour = this.inputRef.current?.value ? 'black' : '#979797'; + })} + />
this.setInternalSharing({ user, linkDatabase, sharingDoc, userColor }, e.currentTarget.value, undefined)}> + {this.sharingOptions(uniform)} + + ) : ( +
+ {concat(ReverseHierarchyMap.get(permissions)?.image, ' ', permissions)} +   +
+ )} +
+
+ ); }); - const docs = await DocServer.GetRefFields(raw.reduce((list, user) => [...list, user.sharingDocumentId, user.linkDatabaseId], [] as string[])); - raw.map( - action((newUser: User) => { - const sharingDoc = docs[newUser.sharingDocumentId]; - const linkDatabase = docs[newUser.linkDatabaseId]; - if (sharingDoc instanceof Doc && linkDatabase instanceof Doc) { - if (!this.users.find(user => user.user.email === newUser.email)) { - this.users.push({ user: newUser, sharingDoc, linkDatabase, userColor: StrCast(sharingDoc.userColor) }); - //LinkManager.addLinkDB(linkDatabase); - } - } - }) + + // checks if every doc has the same author + 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 curUserPermission = StrCast(targetDoc[userKey]); + // const curUserPermission = HierarchyMapping.get(effectiveAcls[0])!.name + userListContents.unshift( + sameAuthor ? ( +
+ {targetDoc?.author === ClientUtils.CurrentUserEmail() ? 'Me' : StrCast(targetDoc?.author)} +
+
Owner
+
+
+ ) : null, + sameAuthor && targetDoc?.author !== ClientUtils.CurrentUserEmail() ? ( +
+ Me +
+
+ {effectiveAcls.every(acl => acl === effectiveAcls[0]) ? concat(ReverseHierarchyMap.get(curUserPermission!)?.image, ' ', curUserPermission) : '-multiple-'} +   +
+
+
+ ) : null + ); + + // the list of groups shared with + const groupListMap: (Doc | { title: string })[] = groups.filter(({ title }) => (docs.length > 1 ? commonKeys.includes(`acl-${normalizeEmail(StrCast(title))}`) : true)); + groupListMap.unshift({ title: 'Guest' }); // , { title: "ALL" }); + const groupListContents = groupListMap.map(group => { + const groupKey = `acl-${StrCast(group.title)}`; + const uniform = docs.every(doc => doc?.[DocAcl]?.[groupKey] === docs[0]?.[DocAcl]?.[groupKey]); + const permissions = uniform ? StrCast(targetDoc?.[groupKey]) : '-multiple-'; + + return !permissions ? null : ( +
+
{StrCast(group.title)}
+   + {group instanceof Doc ? ( + } + size={Size.XSMALL} + color={SettingsManager.userColor} + onClick={action(() => { + GroupManager.Instance.currentGroup = group; + })} + /> + ) : null} +
+ {admin || this.myDocAcls ? ( + + ) : ( +
+ {concat(ReverseHierarchyMap.get(permissions)?.image, ' ', permissions)} +   +
+ )} +
+
); - this.populating = false; - } - }; + }); + return ( +
+ {GroupManager.Instance?.currentGroup ? ( + { + GroupManager.Instance.currentGroup = undefined; + })} + /> + ) : null} +
+

+

window.open('https://brown-dash.github.io/Dash-Documentation/features/collaboration/', '_blank')}> + window.open('https://brown-dash.github.io/Dash-Documentation/features/collaboration/', '_blank')} /> +
+ Share + {this.focusOn(docs.length < 2 ? StrCast(targetDoc?.title, 'this document') : '-multiple-')} +

+
+
+
+
+ {admin ? ( +
+
+ + {this.sharingOptions(true)} + +
+
+
+
+
+ { + this.showUserOptions = !this.showUserOptions; + })} + />{' '} + + { + this.showGroupOptions = !this.showGroupOptions; + })} + />{' '} + +
+ +
+ {Doc.noviceMode ? null : ( +
+ { + this.upgradeNested = !this.upgradeNested; + })} + checked={this.upgradeNested} + />{' '} + + { + this.layoutDocAcls = !this.layoutDocAcls; + })} + checked={this.layoutDocAcls} + />{' '} + +
+ )} +
+
+ ) : ( +
+
+
+ { + this.layoutDocAcls = !this.layoutDocAcls; + })} + checked={this.layoutDocAcls} + />{' '} + +
+
+
+ )} +
+
+
{ + this.individualSort = this.individualSort === 'ascending' ? 'descending' : this.individualSort === 'descending' ? 'none' : 'ascending'; + })}> +
+ Individuals + } + size={Size.XSMALL} + color={StrCast(Doc.UserDoc().userColor)} + /> +
+
+
{userListContents}
+
+
+
{ + this.groupSort = this.groupSort === 'ascending' ? 'descending' : this.groupSort === 'descending' ? 'none' : 'ascending'; + })}> +
+ Groups + } size={Size.XSMALL} color={StrCast(Doc.UserDoc().userColor)} onClick={action(() => GroupManager.Instance.open())} /> + } + size={Size.XSMALL} + color={StrCast(Doc.UserDoc().userColor)} + /> +
+
+
{groupListContents}
+
+
+
+
+ ); + } /** * Shares the document with a user. @@ -200,12 +479,69 @@ export class SharingManager extends React.Component<{}> { } }); }, 'set group permissions'); + /** + * Populates the list of validated users (this.users) by adding registered users which have a sharingDocument. + */ + populateUsers = async () => { + if (!this.populating && Doc.UserDoc()[Id] !== Utils.GuestID()) { + this.populating = true; + const userList = await RequestPromise.get(ClientUtils.prepend('/getUsers')); + const raw = (JSON.parse(userList) as User[]).filter(user => user.email !== 'guest' && user.email !== ClientUtils.CurrentUserEmail()); + runInAction(() => { + FieldLoader.ServerLoadStatus.message = 'users'; + }); + const docs = await DocServer.GetRefFields(raw.reduce((list, user) => [...list, user.sharingDocumentId, user.linkDatabaseId], [] as string[])); + raw.map( + action((newUser: User) => { + const sharingDoc = docs[newUser.sharingDocumentId]; + const linkDatabase = docs[newUser.linkDatabaseId]; + if (sharingDoc instanceof Doc && linkDatabase instanceof Doc) { + if (!this.users.find(user => user.user.email === newUser.email)) { + this.users.push({ user: newUser, sharingDoc, linkDatabase, userColor: StrCast(sharingDoc.userColor) }); + // LinkManager.addLinkDB(linkDatabase); + } + } + }) + ); + this.populating = false; + } + }; + + // eslint-disable-next-line react/sort-comp + public close = action(() => { + this.isOpen = false; + this.selectedUsers = null; // resets the list of users and selected users (in the react-select component) + TaskCompletionBox.taskCompleted = false; + setTimeout( + action(() => { + // this.copied = false; + DictationOverlay.Instance.hasActiveModal = false; + this.targetDoc = undefined; + }), + 500 + ); + this.layoutDocAcls = false; + }); + + // eslint-disable-next-line react/no-unused-class-component-methods + public open = (target?: DocumentView, targetDoc?: Doc) => { + this.populateUsers(); + runInAction(() => { + this.targetDocView = target; + this.targetDoc = targetDoc || target?.Document; + DictationOverlay.Instance.hasActiveModal = true; + this.isOpen = this.targetDoc !== undefined; + this.permissions = SharingPermissions.Augment; + this.upgradeNested = true; + }); + }; /** * Shares the documents shared with a group with a new user who has been added to that group. * @param group * @param emailId */ + // eslint-disable-next-line react/no-unused-class-component-methods shareWithAddedMember = (group: Doc, emailId: string, retry: boolean = true) => { const user = this.users.find(({ user: { email } }) => email === emailId)!; const self = this; @@ -231,6 +567,7 @@ export class SharingManager extends React.Component<{}> { /** * Called from the properties sidebar to change permissions of a user. */ + // eslint-disable-next-line react/no-unused-class-component-methods shareFromPropertiesSidebar = undoable((shareWith: string, permission: SharingPermissions, docs: Doc[], layout: boolean) => { if (layout) this.layoutDocAcls = true; if (shareWith !== 'Guest') { @@ -254,6 +591,7 @@ export class SharingManager extends React.Component<{}> { * @param group * @param emailId */ + // eslint-disable-next-line react/no-unused-class-component-methods removeMember = (group: Doc, emailId: string) => { const user: ValidatedUser = this.users.find(({ user: { email } }) => email === emailId)!; @@ -277,6 +615,7 @@ export class SharingManager extends React.Component<{}> { * Removes a group's permissions from documents that have been shared with it. * @param group */ + // eslint-disable-next-line react/no-unused-class-component-methods removeGroup = (group: Doc) => { if (group.docsShared) { DocListCast(group.docsShared).forEach(doc => { @@ -299,25 +638,12 @@ export class SharingManager extends React.Component<{}> { // targetDoc["acl-" + PublicKey] = permission; // }s - /** - * Copies the Public sharing url to the user's clipboard. - */ - private copyURL = (e: any) => { - ClientUtils.CopyText(ClientUtils.shareUrl(this.targetDoc![Id])); - }; - - /** - * Returns the SharingPermissions (Admin, Can Edit etc) access that's used to share - */ - private sharingOptions(uniform: boolean, showGuestOptions?: boolean) { - const dropdownValues: string[] = showGuestOptions ? [SharingPermissions.None, SharingPermissions.View] : Object.values(SharingPermissions); - if (!uniform) dropdownValues.unshift('-multiple-'); - return dropdownValues.map(permission => ( - - )); - } + /** + * Copies the Public sharing url to the user's clipboard. + */ + private copyURL = () => { + ClientUtils.CopyText(ClientUtils.shareUrl(this.targetDoc![Id])); + }; private focusOn = (contents: string) => { const title = this.targetDoc ? StrCast(this.targetDoc.title) : ''; @@ -350,24 +676,6 @@ export class SharingManager extends React.Component<{}> { ); }; - /** - * Handles changes in the users selected in react-select - */ - @action - handleUsersChange = (selectedOptions: any) => { - this.selectedUsers = selectedOptions as UserOptions[]; - }; - - /** - * Handles changes in the permission chosen to share with someone with - */ - handlePermissionsChange = undoable( - action((event: React.ChangeEvent) => { - this.permissions = event.currentTarget.value as SharingPermissions; - }), - 'permission change' - ); - /** * Calls the relevant method for sharing, displays the popup, and resets the relevant variables. */ @@ -389,7 +697,9 @@ export class SharingManager extends React.Component<{}> { TaskCompletionBox.textDisplayed = 'Document shared!'; TaskCompletionBox.taskCompleted = true; setTimeout( - action(() => (TaskCompletionBox.taskCompleted = false)), + action(() => { + TaskCompletionBox.taskCompleted = false; + }), 2000 ); } @@ -418,263 +728,20 @@ export class SharingManager extends React.Component<{}> { const g2 = StrCast(group2.title); return g1 < g2 ? -1 : g1 === g2 ? 0 : 1; }; - /** - * @returns the main interface of the SharingManager. + * Returns the SharingPermissions (Admin, Can Edit etc) access that's used to share */ - @computed get sharingInterface() { - if (!this.targetDoc) return null; - TraceMobx(); - const groupList = GroupManager.Instance?.allGroups || []; - - const sortedUsers = this.users - .slice() - .sort(this.sortUsers) - .map(({ user: { email } }) => ({ label: email, value: indType + email })); - const sortedGroups = groupList - .slice() - .sort(this.sortGroups) - .map(({ title }) => ({ label: StrCast(title), value: groupType + StrCast(title) })); - - // the next block handles the users shown (individuals/groups/both) - const options: GroupedOptions[] = []; - if (GroupManager.Instance) { - if ((this.showUserOptions && this.showGroupOptions) || (!this.showUserOptions && !this.showGroupOptions)) { - options.push({ label: 'Individuals', options: sortedUsers }, { label: 'Groups', options: sortedGroups }); - } else if (this.showUserOptions) options.push({ label: 'Individuals', options: sortedUsers }); - else options.push({ label: 'Groups', options: sortedGroups }); - } - - const users = this.individualSort === 'ascending' ? this.users.slice().sort(this.sortUsers) : this.individualSort === 'descending' ? this.users.slice().sort(this.sortUsers).reverse() : this.users; - const groups = this.groupSort === 'ascending' ? groupList.slice().sort(this.sortGroups) : this.groupSort === 'descending' ? groupList.slice().sort(this.sortGroups).reverse() : groupList; - - let docs = SelectionManager.Views.length < 2 ? [this.targetDoc] : SelectionManager.Views.map(docView => docView.Document); - - if (this.myDocAcls) { - const newDocs: Doc[] = []; - SearchUtil.foreachRecursiveDoc(docs, (depth, doc) => newDocs.push(doc)); - docs = newDocs.filter(doc => GetEffectiveAcl(doc) === AclAdmin); - } - - const targetDoc = this.layoutDocAcls ? docs[0] : docs[0]?.[DocData]; - - // tslint:disable-next-line: no-unnecessary-callback-wrapper - const effectiveAcls = docs.map(doc => GetEffectiveAcl(doc)); - const admin = this.myDocAcls ? Boolean(docs.length) : effectiveAcls.every(acl => acl === AclAdmin); - - // users in common between all docs - const commonKeys = intersection(docs).reduce((list, doc) => (doc?.[DocAcl] ? [...list, ...Object.keys(doc[DocAcl])] : list), [] as string[]); - - // the list of users shared with - const userListContents = users - // .filter(({ user }) => (docs.length > 1 ? commonKeys.includes(`acl-${normalizeEmail(user.email)}`) : docs[0]?.author !== user.email)) - .filter(({ user }) => docs[0]?.author !== user.email) - .map(({ user, linkDatabase, sharingDoc, userColor }) => { - const userKey = `acl-${normalizeEmail(user.email)}`; - const uniform = docs.every(doc => doc?.[DocAcl]?.[userKey] === docs[0]?.[DocAcl]?.[userKey]); - // const permissions = uniform ? StrCast(targetDoc?.[userKey]) : '-multiple-'; - let permissions = targetDoc[DocAcl][userKey] ? HierarchyMapping.get(targetDoc[DocAcl][userKey])?.name : StrCast(targetDoc[userKey]); - permissions = uniform ? StrCast(targetDoc?.[userKey]) : '-multiple-'; - - return !permissions ? null : ( -
- {user.email} -
- {admin || this.myDocAcls ? ( - - ) : ( -
- {concat(ReverseHierarchyMap.get(permissions)?.image, ' ', permissions)} -   -
- )} -
-
- ); - }); - - // checks if every doc has the same author - 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 curUserPermission = StrCast(targetDoc[userKey]); - // const curUserPermission = HierarchyMapping.get(effectiveAcls[0])!.name - userListContents.unshift( - sameAuthor ? ( -
- {targetDoc?.author === ClientUtils.CurrentUserEmail() ? 'Me' : StrCast(targetDoc?.author)} -
-
Owner
-
-
- ) : null, - sameAuthor && targetDoc?.author !== ClientUtils.CurrentUserEmail() ? ( -
- Me -
-
- {effectiveAcls.every(acl => acl === effectiveAcls[0]) ? concat(ReverseHierarchyMap.get(curUserPermission!)?.image, ' ', curUserPermission) : '-multiple-'} -   -
-
-
- ) : null - ); - - // the list of groups shared with - const groupListMap: (Doc | { title: string })[] = groups.filter(({ title }) => (docs.length > 1 ? commonKeys.includes(`acl-${normalizeEmail(StrCast(title))}`) : true)); - groupListMap.unshift({ title: 'Guest' }); //, { title: "ALL" }); - const groupListContents = groupListMap.map(group => { - let groupKey = `acl-${StrCast(group.title)}`; - const uniform = docs.every(doc => doc?.[DocAcl]?.[groupKey] === docs[0]?.[DocAcl]?.[groupKey]); - const permissions = uniform ? StrCast(targetDoc?.[groupKey]) : '-multiple-'; - - return !permissions ? null : ( -
-
{StrCast(group.title)}
-   - {group instanceof Doc ? } size={Size.XSMALL} color={SettingsManager.userColor} onClick={action(() => (GroupManager.Instance.currentGroup = group))} /> : null} -
- {admin || this.myDocAcls ? ( - - ) : ( -
- {concat(ReverseHierarchyMap.get(permissions)?.image, ' ', permissions)} -   -
- )} -
-
- ); - }); - return ( -
- {GroupManager.Instance?.currentGroup ? (GroupManager.Instance.currentGroup = undefined))} /> : null} -
-

-

window.open('https://brown-dash.github.io/Dash-Documentation/features/collaboration/', '_blank')}> - window.open('https://brown-dash.github.io/Dash-Documentation/features/collaboration/', '_blank')} /> -
- Share - {this.focusOn(docs.length < 2 ? StrCast(targetDoc?.title, 'this document') : '-multiple-')} -

-
-
-
-
- {admin ? ( -
-
- - {this.sharingOptions(true)} - -
-
-
-
-
- (this.showUserOptions = !this.showUserOptions))} /> - (this.showGroupOptions = !this.showGroupOptions))} /> -
- -
- {Doc.noviceMode ? null : ( -
- (this.upgradeNested = !this.upgradeNested))} checked={this.upgradeNested} /> - (this.layoutDocAcls = !this.layoutDocAcls))} checked={this.layoutDocAcls} /> -
- )} -
-
- ) : ( -
-
-
- (this.layoutDocAcls = !this.layoutDocAcls))} checked={this.layoutDocAcls} /> -
-
-
- )} -
-
-
(this.individualSort = this.individualSort === 'ascending' ? 'descending' : this.individualSort === 'descending' ? 'none' : 'ascending'))}> -
- Individuals - } - size={Size.XSMALL} - color={StrCast(Doc.UserDoc().userColor)} - /> -
-
-
{userListContents}
-
-
-
(this.groupSort = this.groupSort === 'ascending' ? 'descending' : this.groupSort === 'descending' ? 'none' : 'ascending'))}> -
- Groups - } size={Size.XSMALL} color={StrCast(Doc.UserDoc().userColor)} onClick={action(() => GroupManager.Instance.open())} /> - } - size={Size.XSMALL} - color={StrCast(Doc.UserDoc().userColor)} - /> -
-
-
{groupListContents}
-
-
-
-
- ); + private sharingOptions(uniform: boolean, showGuestOptions?: boolean) { + const dropdownValues: string[] = showGuestOptions ? [SharingPermissions.None, SharingPermissions.View] : Object.values(SharingPermissions); + if (!uniform) dropdownValues.unshift('-multiple-'); + return dropdownValues.map(permission => ( + + )); } render() { - return ; + return ; } } diff --git a/src/client/util/SnappingManager.ts b/src/client/util/SnappingManager.ts index eb47bbe88..3da85191f 100644 --- a/src/client/util/SnappingManager.ts +++ b/src/client/util/SnappingManager.ts @@ -10,6 +10,7 @@ export class SnappingManager { @observable _shiftKey = false; @observable _ctrlKey = false; @observable _metaKey = false; + @observable _showPresPaths = false; @observable _isLinkFollowing = false; @observable _isDragging: boolean = false; @observable _isResizing: string | undefined = undefined; // the string is the Id of the document being resized @@ -36,6 +37,7 @@ export class SnappingManager { public static get ShiftKey() { return this.Instance._shiftKey; } // prettier-ignore public static get CtrlKey() { return this.Instance._ctrlKey; } // prettier-ignore public static get MetaKey() { return this.Instance._metaKey; } // prettier-ignore + public static get ShowPresPaths() { return this.Instance._showPresPaths; } // prettier-ignore public static get IsLinkFollowing(){ return this.Instance._isLinkFollowing; } // prettier-ignore public static get IsDragging() { return this.Instance._isDragging; } // prettier-ignore public static get IsResizing() { return this.Instance._isResizing; } // prettier-ignore @@ -44,6 +46,7 @@ export class SnappingManager { public static SetShiftKey = (down: boolean) => runInAction(() => {this.Instance._shiftKey = down}); // prettier-ignore public static SetCtrlKey = (down: boolean) => runInAction(() => {this.Instance._ctrlKey = down}); // prettier-ignore public static SetMetaKey = (down: boolean) => runInAction(() => {this.Instance._metaKey = down}); // prettier-ignore + public static SetShowPresPaths = (paths:boolean) => runInAction(() => {this.Instance._showPresPaths = paths}); // prettier-ignore public static SetIsLinkFollowing= (follow:boolean)=> runInAction(() => {this.Instance._isLinkFollowing = follow}); // prettier-ignore public static SetIsDragging = (drag: boolean) => runInAction(() => {this.Instance._isDragging = drag}); // prettier-ignore public static SetIsResizing = (docid?:string) => runInAction(() => {this.Instance._isResizing = docid}); // prettier-ignore diff --git a/src/client/util/UndoManager.ts b/src/client/util/UndoManager.ts index 4e941508d..956c0e674 100644 --- a/src/client/util/UndoManager.ts +++ b/src/client/util/UndoManager.ts @@ -1,8 +1,11 @@ +/* eslint-disable prefer-spread */ +/* eslint-disable no-use-before-define */ import { action, observable, runInAction } from 'mobx'; import { Without } from '../../Utils'; import { RichTextField } from '../../fields/RichTextField'; -export let printToConsole = false; // Doc.MyDockedBtns.linearView_IsOpen +// eslint-disable-next-line prefer-const +let printToConsole = false; // Doc.MyDockedBtns.linearView_IsOpen function getBatchName(target: any, key: string | symbol): string { const keyName = key.toString(); @@ -38,10 +41,11 @@ function propertyDecorator(target: any, key: string | symbol) { } export function undoable(fn: (...args: any[]) => any, batchName: string): (...args: any[]) => any { - return function () { + return function (...fargs) { const batch = UndoManager.StartBatch(batchName); try { - return fn.apply(undefined, arguments as any); + // eslint-disable-next-line prefer-rest-params + return fn.apply(undefined, fargs); } finally { batch.end(); } @@ -49,13 +53,15 @@ export function undoable(fn: (...args: any[]) => any, batchName: string): (...ar } export function undoBatch(target: any, key: string | symbol, descriptor?: TypedPropertyDescriptor): any; +// eslint-disable-next-line no-redeclare export function undoBatch(fn: (...args: any[]) => any): (...args: any[]) => any; +// eslint-disable-next-line no-redeclare export function undoBatch(target: any, key?: string | symbol, descriptor?: TypedPropertyDescriptor): any { if (!key) { - return function () { + return function (...fargs: any[]) { const batch = UndoManager.StartBatch(''); try { - return target.apply(undefined, arguments); + return target.apply(undefined, fargs); } finally { batch.end(); } @@ -63,7 +69,7 @@ export function undoBatch(target: any, key?: string | symbol, descriptor?: Typed } if (!descriptor) { propertyDecorator(target, key); - return; + return undefined; } const oldFunction = descriptor.value; @@ -87,14 +93,18 @@ export namespace UndoManager { } type UndoBatch = UndoEvent[]; - export let undoStackNames: string[] = observable([]); - export let redoStackNames: string[] = observable([]); - export let undoStack: UndoBatch[] = observable([]); - export let redoStack: UndoBatch[] = observable([]); let currentBatch: UndoBatch | undefined; - export let batchCounter = observable.box(0); let undoing = false; - export let tempEvents: UndoEvent[] | undefined = undefined; + let tempEvents: UndoEvent[] | undefined; + export const undoStackNames: string[] = observable([]); + export const redoStackNames: string[] = observable([]); + export const undoStack: UndoBatch[] = observable([]); + export const redoStack: UndoBatch[] = observable([]); + export const batchCounter = observable.box(0); + let _fieldPrinter: (val: any) => string = val => val?.toString(); + export function SetFieldPrinter(printer: (val: any) => string) { + _fieldPrinter = printer; + } export function AddEvent(event: UndoEvent, value?: any): void { if (currentBatch && batchCounter.get() && !undoing) { @@ -103,8 +113,8 @@ export namespace UndoManager { ' '.slice(0, batchCounter.get()) + 'UndoEvent : ' + event.prop + - ' = ' + - (value instanceof RichTextField ? value.Text : value instanceof Array ? value.map(val => Field.toJavascriptString(val)).join(',') : Field.toJavascriptString(value)) + ' = ' + // prettier-ignore + (value instanceof RichTextField ? value.Text : value instanceof Array ? value.map(_fieldPrinter).join(',') : _fieldPrinter(value)) ); currentBatch.push(event); tempEvents?.push(event); @@ -130,21 +140,22 @@ export namespace UndoManager { } export function FilterBatches(fieldTypes: string[]) { const fieldCounts: { [key: string]: number } = {}; - const lastStack = UndoManager.undoStack.slice(-1)[0]; //.lastElement(); + const lastStack = UndoManager.undoStack.slice(-1)[0]; // .lastElement(); if (lastStack) { - lastStack.forEach(ev => fieldTypes.includes(ev.prop) && (fieldCounts[ev.prop] = (fieldCounts[ev.prop] || 0) + 1)); + lastStack.forEach(ev => { + fieldTypes.includes(ev.prop) && (fieldCounts[ev.prop] = (fieldCounts[ev.prop] || 0) + 1); + }); const fieldCount2: { [key: string]: number } = {}; - runInAction( - () => - (UndoManager.undoStack[UndoManager.undoStack.length - 1] = lastStack.filter(ev => { - if (fieldTypes.includes(ev.prop)) { - fieldCount2[ev.prop] = (fieldCount2[ev.prop] || 0) + 1; - if (fieldCount2[ev.prop] === 1 || fieldCount2[ev.prop] === fieldCounts[ev.prop]) return true; - return false; - } - return true; - })) - ); + runInAction(() => { + UndoManager.undoStack[UndoManager.undoStack.length - 1] = lastStack.filter(ev => { + if (fieldTypes.includes(ev.prop)) { + fieldCount2[ev.prop] = (fieldCount2[ev.prop] || 0) + 1; + if (fieldCount2[ev.prop] === 1 || fieldCount2[ev.prop] === fieldCounts[ev.prop]) return true; + return false; + } + return true; + }); + }); } } export function TraceOpenBatches() { @@ -161,11 +172,10 @@ export namespace UndoManager { if (this.disposed) { console.log('WARNING: undo batch already disposed'); return false; - } else { - this.disposed = true; - openBatches.splice(openBatches.indexOf(this)); - return EndBatch(this.batchName, cancel); } + this.disposed = true; + openBatches.splice(openBatches.indexOf(this)); + return EndBatch(this.batchName, cancel); }; end = () => this.dispose(false); @@ -183,7 +193,7 @@ export namespace UndoManager { const EndBatch = action((batchName: string, cancel: boolean = false) => { runInAction(() => batchCounter.set(batchCounter.get() - 1)); - printToConsole && console.log(' '.slice(0, batchCounter.get()) + 'End ' + batchName + ' (' + currentBatch?.length + ')'); + printToConsole && console.log(' '.slice(0, batchCounter.get()) + 'End ' + batchName + ' (' + (currentBatch?.length ?? 0) + ')'); if (batchCounter.get() === 0 && currentBatch?.length) { if (!cancel) { undoStack.push(currentBatch); @@ -200,10 +210,10 @@ export namespace UndoManager { export function StartTempBatch() { tempEvents = []; } - export function EndTempBatch(success: boolean) { + export function EndTempBatch(success: boolean) { UndoManager.UndoTempBatch(success); } - //TODO Make this return the return value + // TODO Make this return the return value export function RunInBatch(fn: () => T, batchName: string) { const batch = StartBatch(batchName); try { @@ -235,9 +245,11 @@ export namespace UndoManager { } undoing = true; - for (let i = commands.length - 1; i >= 0; i--) { - commands[i].undo(); - } + // eslint-disable-next-line prettier/prettier + commands + .slice() + .reverse() + .forEach(command => command.undo()); undoing = false; redoStackNames.push(names ?? '???'); @@ -256,9 +268,7 @@ export namespace UndoManager { } undoing = true; - for (const command of commands) { - command.redo(); - } + commands.forEach(command => command.redo()); undoing = false; undoStackNames.push(names ?? '???'); diff --git a/src/client/util/reportManager/ReportManager.tsx b/src/client/util/reportManager/ReportManager.tsx index 02b3ee32c..2224e642d 100644 --- a/src/client/util/reportManager/ReportManager.tsx +++ b/src/client/util/reportManager/ReportManager.tsx @@ -1,3 +1,6 @@ +/* eslint-disable jsx-a11y/label-has-associated-control */ +/* eslint-disable jsx-a11y/media-has-caption */ +/* eslint-disable react/no-unused-class-component-methods */ import { Octokit } from '@octokit/core'; import { Button, Dropdown, DropdownType, IconButton, Type } from 'browndash-components'; import { action, makeObservable, observable } from 'mobx'; @@ -13,7 +16,7 @@ import { ClientUtils } from '../../../ClientUtils'; import { Doc } from '../../../fields/Doc'; import { StrCast } from '../../../fields/Types'; import { MainViewModal } from '../../views/MainViewModal'; -import '.././SettingsManager.scss'; +import '../SettingsManager.scss'; import { SettingsManager } from '../SettingsManager'; import './ReportManager.scss'; import { Filter, FormInput, FormTextArea, IssueCard, IssueView } from './ReportManagerComponents'; @@ -25,10 +28,12 @@ import { BugType, FileData, Priority, ReportForm, ViewState, bugDropdownItems, d */ @observer export class ReportManager extends React.Component<{}> { + // eslint-disable-next-line no-use-before-define public static Instance: ReportManager; @observable private isOpen = false; @observable private query = ''; + // eslint-disable-next-line react/sort-comp @action private setQuery = (q: string) => { this.query = q; }; @@ -83,7 +88,9 @@ export class ReportManager extends React.Component<{}> { this.formData = newData; }); - public close = action(() => (this.isOpen = false)); + public close = action(() => { + this.isOpen = false; + }); public open = action(async () => { this.isOpen = true; if (this.shownIssues.length === 0) { @@ -165,7 +172,7 @@ export class ReportManager extends React.Component<{}> { * @returns JSX element of a piece of media (image, video, audio) */ private getMediaPreview = (fileData: FileData): JSX.Element => { - const file = fileData.file; + const { file } = fileData; const mimeType = file.type; const preview = URL.createObjectURL(file); @@ -180,7 +187,8 @@ export class ReportManager extends React.Component<{}> {
); - } else if (mimeType.startsWith('video/')) { + } + if (mimeType.startsWith('video/')) { return (
@@ -194,7 +202,8 @@ export class ReportManager extends React.Component<{}> {
); - } else if (mimeType.startsWith('audio/')) { + } + if (mimeType.startsWith('audio/')) { return (
); } - return <>; + return
; }; /** @@ -307,8 +316,8 @@ export class ReportManager extends React.Component<{}> {
{ @@ -320,8 +329,8 @@ export class ReportManager extends React.Component<{}> { /> { @@ -347,7 +356,7 @@ export class ReportManager extends React.Component<{}> { text="Submit" type={Type.TERT} color={StrCast(Doc.UserDoc().userVariantColor)} - icon={} + icon={} iconPlacement="right" onClick={() => { this.reportIssue(); @@ -364,7 +373,7 @@ export class ReportManager extends React.Component<{}> { /> )}
- } onClick={this.close} /> + } onClick={this.close} />
); @@ -376,9 +385,8 @@ export class ReportManager extends React.Component<{}> { private reportComponent = () => { if (this.viewState === ViewState.VIEW) { return this.viewIssuesComponent(); - } else { - return this.reportIssueComponent(); } + return this.reportIssueComponent(); }; render() { @@ -386,7 +394,7 @@ export class ReportManager extends React.Component<{}> { diff --git a/src/client/views/ContextMenuItem.tsx b/src/client/views/ContextMenuItem.tsx index 5760872fb..eb1030eec 100644 --- a/src/client/views/ContextMenuItem.tsx +++ b/src/client/views/ContextMenuItem.tsx @@ -1,3 +1,4 @@ +/* eslint-disable react/jsx-props-no-spreading */ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, makeObservable, observable, runInAction } from 'mobx'; @@ -17,6 +18,7 @@ export interface OriginalMenuProps { export interface SubmenuProps { description: string; + // eslint-disable-next-line no-use-before-define subitems: ContextMenuProps[]; noexpand?: boolean; addDivider?: boolean; @@ -37,7 +39,9 @@ export class ContextMenuItem extends ObservableReactComponent (this._items.length = 0)); + runInAction(() => { + this._items.length = 0; + }); if ((this._props as SubmenuProps)?.subitems) { (this._props as SubmenuProps).subitems?.forEach(i => runInAction(() => this._items.push(i))); } @@ -83,7 +87,9 @@ export class ContextMenuItem extends ObservableReactComponent (this.overItem = false)), + action(() => { + this.overItem = false; + }), ContextMenuItem.timeout ); }; @@ -147,10 +153,10 @@ export class ContextMenuItem extends ObservableReactComponent {this._props.description} - +
); } + return null; } } diff --git a/src/client/views/DashboardView.tsx b/src/client/views/DashboardView.tsx index 14abd5f89..25415a4f0 100644 --- a/src/client/views/DashboardView.tsx +++ b/src/client/views/DashboardView.tsx @@ -1,3 +1,5 @@ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Button, ColorPicker, EditableText, Size, Type } from 'browndash-components'; import { action, computed, makeObservable, observable } from 'mobx'; @@ -48,10 +50,18 @@ export class DashboardView extends ObservableReactComponent<{}> { @observable private selectedDashboardGroup = DashboardGroup.MyDashboards; @observable private newDashboardName = ''; @observable private newDashboardColor = '#AFAFAF'; - @action abortCreateNewDashboard = () => (this.openModal = false); - @action setNewDashboardName = (name: string) => (this.newDashboardName = name); - @action setNewDashboardColor = (color: string) => (this.newDashboardColor = color); - @action selectDashboardGroup = (group: DashboardGroup) => (this.selectedDashboardGroup = group); + @action abortCreateNewDashboard = () => { + this.openModal = false; + }; + @action setNewDashboardName = (name: string) => { + this.newDashboardName = name; + }; + @action setNewDashboardColor = (color: string) => { + this.newDashboardColor = color; + }; + @action selectDashboardGroup = (group: DashboardGroup) => { + this.selectedDashboardGroup = group; + }; clickDashboard = (e: React.MouseEvent, dashboard: Doc) => { if (this.selectedDashboardGroup === DashboardGroup.SharedDashboards) { @@ -138,9 +148,9 @@ export class DashboardView extends ObservableReactComponent<{}> { <>
-
open linked trail
}>
-
- +
); @@ -134,8 +144,12 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => ( key={icon.toString()} size="sm" icon={icon} - onPointerEnter={action(e => (this.subEndLink = (pinLayout ? 'Layout' : '') + (pinLayout && pinContent ? ' &' : '') + (pinContent ? ' Content' : '')))} - onPointerLeave={action(e => (this.subEndLink = ''))} + onPointerEnter={action(() => { + this.subEndLink = (pinLayout ? 'Layout' : '') + (pinLayout && pinContent ? ' &' : '') + (pinContent ? ' Content' : ''); + })} + onPointerLeave={action(() => { + this.subEndLink = ''; + })} onClick={e => { this.view0 && DocumentLinksButton.finishLinkClick(e.clientX, e.clientY, DocumentLinksButton.StartLink, this.view0.Document, true, this.view0, { @@ -157,7 +171,7 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => ( {linkBtn(false, true, 'address-card')} {linkBtn(true, true, 'id-card')}
- + ); } @@ -177,15 +191,16 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => ( key={icon.toString()} size="sm" icon={icon} - onPointerEnter={action( - e => - (this.subPin = - (pinLayoutView ? 'Layout' : '') + - (pinLayoutView && pinContentView ? ' &' : '') + - (pinContentView ? ' Content View' : '') + - (pinLayoutView && pinContentView ? '(shift+alt)' : pinLayoutView ? '(shift)' : pinContentView ? '(alt)' : '')) - )} - onPointerLeave={action(e => (this.subPin = ''))} + onPointerEnter={action(() => { + this.subPin = + (pinLayoutView ? 'Layout' : '') + + (pinLayoutView && pinContentView ? ' &' : '') + + (pinContentView ? ' Content View' : '') + + (pinLayoutView && pinContentView ? '(shift+alt)' : pinLayoutView ? '(shift)' : pinContentView ? '(alt)' : ''); + })} + onPointerLeave={action(() => { + this.subPin = ''; + })} onClick={e => { const docs = this._props .views() @@ -232,8 +247,8 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => ( get shareButton() { const targetDoc = this.view0?.Document; return !targetDoc ? null : ( - {'Open Sharing Manager'}}> -
SharingManager.Instance.open(this.view0, targetDoc)}> + Open Sharing Manager
}> +
SharingManager.Instance.open(this.view0, targetDoc)}>
@@ -244,7 +259,7 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => ( get menuButton() { const targetDoc = this.view0?.Document; return !targetDoc ? null : ( - {`Open Context Menu`}}> + Open Context Menu}>
setupMoveUpEvents(this, e, returnFalse, emptyFunction, e => this.openContextMenu(e))}>
@@ -260,8 +275,7 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (
{ - console.log('hi: ', CalendarManager.Instance); + onClick={() => { CalendarManager.Instance.open(this.view0, targetDoc); }}> @@ -282,7 +296,18 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => ( style={{ backgroundColor: this._isRecording ? Colors.ERROR_RED : Colors.DARK_GRAY, color: Colors.WHITE }} onPointerDown={action((e: React.PointerEvent) => { this._isRecording = true; - this._props.views().map(view => view && DocumentViewInternal.recordAudioAnnotation(view.dataDoc, view.LayoutFieldKey, stopFunc => (this._stopFunc = stopFunc), emptyFunction)); + this._props.views().map( + view => + view && + DocumentViewInternal.recordAudioAnnotation( + view.dataDoc, + view.LayoutFieldKey, + stopFunc => { + this._stopFunc = stopFunc; + }, + emptyFunction + ) + ); const b = UndoManager.StartBatch('Recording'); setupMoveUpEvents( this, @@ -310,10 +335,10 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => ( if (this._dragRef.current) { const dragDocView = this.view0!; const dragData = new DragManager.DocumentDragData([dragDocView.Document]); - const [left, top] = dragDocView.screenToContentsTransform().inverse().transformPoint(0, 0); + const origin = dragDocView.screenToContentsTransform().inverse().transformPoint(0, 0); dragData.defaultDropAction = dropActionType.embed; dragData.canEmbed = true; - DragManager.StartDocumentDrag([dragDocView.ContentDiv!], dragData, left, top, { hideSource: false }); + DragManager.StartDocumentDrag([dragDocView.ContentDiv!], dragData, origin[0], origin[1], { hideSource: false }); return true; } return false; @@ -336,8 +361,19 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => ( @computed get templateButton() { return !this.view0 ? null : ( - Tap to Customize Layout. Drag an embedding
} open={this._tooltipOpen} onClose={action(() => (this._tooltipOpen = false))} placement="bottom"> -
!this._ref.current?.getBoundingClientRect().width && (this._tooltipOpen = true))}> + Tap to Customize Layout. Drag an embedding
} + open={this._tooltipOpen} + onClose={action(() => { + this._tooltipOpen = false; + })} + placement="bottom"> +
{ + !this._ref.current?.getBoundingClientRect().width && (this._tooltipOpen = true); + })}> } popup={this.templateMenu} popupContainsPt={returnTrue} />
@@ -365,17 +401,17 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => ( @observable _captureEndLinkLayout = false; @action - captureEndLinkLayout = (e: React.PointerEvent) => { + captureEndLinkLayout = () => { this._captureEndLinkLayout = !this._captureEndLinkLayout; }; @observable _captureEndLinkContent = false; @action - captureEndLinkContent = (e: React.PointerEvent) => { + captureEndLinkContent = () => { this._captureEndLinkContent = !this._captureEndLinkContent; }; @action - captureEndLinkState = (e: React.PointerEvent) => { + captureEndLinkState = () => { this._captureEndLinkContent = this._captureEndLinkLayout = !this._captureEndLinkLayout; }; @@ -402,13 +438,15 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => ( return (
- +
{this._showLinkPopup ? (
(link.link_displayLine = !IsFollowLinkScript(this._props.views().lastElement()?.Document.onClick))} + linkCreated={link => { + link.link_displayLine = !IsFollowLinkScript(this._props.views().lastElement()?.Document.onClick); + }} linkCreateAnchor={() => this._props.views().lastElement()?.ComponentView?.getAnchor?.(true)} linkFrom={() => this._props.views().lastElement()?.Document} /> @@ -423,7 +461,7 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (
{this.pinButton}
{this.recordButton}
{this.calendarButton}
- {!Doc.UserDoc()['documentLinksButton-fullMenu'] ? null :
{this.shareButton}
} + {!Doc.UserDoc().documentLinksButton_fullMenu ? null :
{this.shareButton}
}
{this.menuButton}
); diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx index 85e893e19..684b948af 100644 --- a/src/client/views/EditableView.tsx +++ b/src/client/views/EditableView.tsx @@ -1,3 +1,5 @@ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ import { action, IReactionDisposer, makeObservable, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; @@ -70,7 +72,7 @@ export class EditableView extends ObservableReactComponent { constructor(props: EditableProps) { super(props); makeObservable(this); - this._editing = this._props.editing ? true : false; + this._editing = !!this._props.editing; } componentDidMount(): void { @@ -166,7 +168,7 @@ export class EditableView extends ObservableReactComponent { this._props.menuCallback(e.currentTarget.getBoundingClientRect().x, e.currentTarget.getBoundingClientRect().y); break; } - + // eslint-disable-next-line no-fallthrough default: if (this._props.textCallback?.(e.key)) { e.stopPropagation(); @@ -186,7 +188,6 @@ export class EditableView extends ObservableReactComponent { this._editing = true; this._props.isEditingCallback?.(true); } - // e.stopPropagation(); } }; @@ -223,6 +224,7 @@ export class EditableView extends ObservableReactComponent { renderEditor() { return this._props.autosuggestProps ? ( { ) : this._props.oneLine !== false && this._props.GetValue()?.toString().indexOf('\n') === -1 ? ( (this._inputref = r)} + ref={r => { this._inputref = r; }} // prettier-ignore style={{ display: this._props.display, overflow: 'auto', fontSize: this._props.fontSize, minWidth: 20, background: this._props.background }} placeholder={this._props.placeholder} onBlur={e => this.finalizeEdit(e.currentTarget.value, false, true, false)} defaultValue={this._props.GetValue()} - autoFocus={true} + // eslint-disable-next-line jsx-a11y/no-autofocus + autoFocus onChange={this.onChange} onKeyDown={this.onKeyDown} onPointerDown={this.stopPropagation} @@ -256,12 +259,13 @@ export class EditableView extends ObservableReactComponent { ) : (