aboutsummaryrefslogtreecommitdiff
path: root/src/client/util
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/util')
-rw-r--r--src/client/util/CurrentUserUtils.ts13
-rw-r--r--src/client/util/DragManager.ts3
-rw-r--r--src/client/util/DropConverter.ts72
-rw-r--r--src/client/util/InteractionUtils.tsx240
-rw-r--r--src/client/util/ReportManager.scss88
-rw-r--r--src/client/util/ReportManager.tsx282
-rw-r--r--src/client/util/SelectionManager.ts3
7 files changed, 550 insertions, 151 deletions
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index f7d072d80..99a8c895f 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -235,9 +235,9 @@ export class CurrentUserUtils {
const header = Docs.Create.RTFDocument(new RichTextField(JSON.stringify(json), ""), { ...opts, title: "text",
layout:
"<HTMLdiv transformOrigin='top left' width='{100/scale}%' height='{100/scale}%' transform='scale({scale})'>" +
- ` <FormattedTextBox {...props} dontScale='true' fieldKey={'text'} height='calc(100% - ${headerBtnHgt}px - {this._headerHeight||0}px)'/>` +
- " <FormattedTextBox {...props} dontScale='true' fieldKey={'header'} dontSelectOnLoad='true' ignoreAutoHeight='true' fontSize='{this._headerFontSize||9}px' height='{(this._headerHeight||0)}px' background='{this._headerColor || MySharedDocs().userColor||`lightGray`}' />" +
- ` <HTMLdiv fontSize='${headerBtnHgt - 1}px' height='${headerBtnHgt}px' background='yellow' onClick={‘(this._headerHeight=scale*Math.min(Math.max(0,this._height-30),this._headerHeight===0?50:0)) + (this._autoHeightMargins=this._headerHeight ? this._headerHeight+${headerBtnHgt}:0)’} >Metadata</HTMLdiv>` +
+ ` <FormattedTextBox {...props} dontScale='true' fieldKey={'text'} height='calc(100% - ${headerBtnHgt}px - {this._headerHeight||0}px)'/>` +
+ " <FormattedTextBox {...props} dontScale='true' fieldKey={'header'} dontSelectOnLoad='true' ignoreAutoHeight='true' fontSize='{this._headerFontSize||9}px' height='{(this._headerHeight||0)}px' backgroundColor='{this._headerColor || MySharedDocs().userColor||`lightGray`}' />" +
+ ` <HTMLdiv fontSize='${headerBtnHgt - 1}px' height='${headerBtnHgt}px' backgroundColor='yellow' onClick={‘(this._headerHeight=scale*Math.min(Math.max(0,this._height-30),this._headerHeight===0?50:0)) + (this._autoHeightMargins=this._headerHeight ? this._headerHeight+${headerBtnHgt}:0)’} >Metadata</HTMLdiv>` +
"</HTMLdiv>"
}, "header");
@@ -267,7 +267,7 @@ export class CurrentUserUtils {
{key: "Script", creator: opts => Docs.Create.ScriptingDocument(null, opts), opts: { _width: 200, _height: 250, }},
// {key: "DataViz", creator: opts => Docs.Create.DataVizDocument(opts), opts: { _width: 300, _height: 300 }},
{key: "Header", creator: headerTemplate, opts: { _width: 300, _height: 70, _headerPointerEvents: "all", _headerHeight: 12, _headerFontSize: 9, _autoHeight: true,}},
- {key: "Presentation",creator: Docs.Create.PresDocument, opts: { _width: 400, _height: 500, _viewType: CollectionViewType.Stacking, targetDropAction: "alias" as any, _chromeHidden: true, boxShadow: "0 0" }},
+ {key: "Presentation",creator: Docs.Create.PresDocument, opts: { _width: 400, _height: 500, _viewType: CollectionViewType.Stacking, targetDropAction: "alias" as any, treeViewHideTitle: true, _chromeHidden: true, boxShadow: "0 0" }},
{key: "Tab", creator: opts => Docs.Create.FreeformDocument([], opts), opts: { _width: 500, _height: 800, _backgroundGridShow: true, }},
{key: "Slide", creator: opts => Docs.Create.TreeDocument([], opts), opts: { _width: 300, _height: 200, _viewType: CollectionViewType.Tree,
treeViewHasOverlay: true, _fontSize: "20px", _autoHeight: true,
@@ -633,7 +633,7 @@ export class CurrentUserUtils {
{ title: "Left", toolTip: "Left align", btnType: ButtonType.ToggleButton, icon: "align-left", scripts: {onClick:'{ return setAlignment("left", _readOnly_);}' }},
{ title: "Center", toolTip: "Center align", btnType: ButtonType.ToggleButton, icon: "align-center", scripts: {onClick:'{ return setAlignment("center", _readOnly_);}'} },
{ title: "Right", toolTip: "Right align", btnType: ButtonType.ToggleButton, icon: "align-right", scripts: {onClick:'{ return setAlignment("right", _readOnly_);}'} },
- { title: "NoLink", toolTip: "Auto Link", btnType: ButtonType.ToggleButton, icon: "link", scripts: {onClick:'{ return toggleNoAutoLinkAnchor(_readOnly_);}'}},
+ { title: "NoLink", toolTip: "Auto Link", btnType: ButtonType.ToggleButton, icon: "link", scripts: {onClick:'{ return toggleNoAutoLinkAnchor(_readOnly_);}'}, funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()'}},
{ title: "Dictate",toolTip: "Dictate", btnType: ButtonType.ToggleButton, icon: "microphone", scripts: {onClick:'{ return toggleDictation(_readOnly_);}'}},
];
}
@@ -675,8 +675,9 @@ export class CurrentUserUtils {
CollectionViewType.Carousel3D, CollectionViewType.Linear, CollectionViewType.Map,
CollectionViewType.Grid, CollectionViewType.NoteTaking]),
title: "Perspective", toolTip: "View", btnType: ButtonType.DropdownList, ignoreClick: true, width: 100, scripts: { script: 'setView(value, _readOnly_)'}},
+ { title: "Pin", icon: "map-pin", toolTip: "Pin View to Trail", btnType: ButtonType.ClickButton, funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "tab")'}, width: 20, scripts: { onClick: 'pinWithView(_readOnly_)'}},
{ title: "Back", icon: "chevron-left", toolTip: "Prev Animation Frame", btnType: ButtonType.ClickButton, funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()'}, width: 20, scripts: { onClick: 'prevKeyFrame(_readOnly_)'}},
- { title: "Num", icon: "", toolTip: "Frame Number", btnType: ButtonType.TextButton, funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()', buttonText: 'selectedDocs()?.lastElement().currentFrame.toString()'}, width: 20, scripts: {}},
+ { title: "Num", icon: "", toolTip: "Frame Number", btnType: ButtonType.TextButton, funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()', buttonText: 'selectedDocs()?.lastElement()?.currentFrame.toString()'}, width: 20, scripts: {}},
{ title: "Fwd", icon: "chevron-right", toolTip: "Next Animation Frame", btnType: ButtonType.ClickButton, funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()'}, width: 20, scripts: { onClick: 'nextKeyFrame(_readOnly_)'}},
{ title: "Fill", icon: "fill-drip", toolTip: "Background Fill Color",btnType: ButtonType.ColorButton, funcs: {hidden: '!SelectionManager_selectedDocType()'}, ignoreClick: true, width: 20, scripts: { script: 'return setBackgroundColor(value, _readOnly_)'}}, // Only when a document is selected
{ title: "Header", icon: "heading", toolTip: "Header Color", btnType: ButtonType.ColorButton, funcs: {hidden: '!SelectionManager_selectedDocType()'}, ignoreClick: true, scripts: { script: 'return setHeaderColor(value, _readOnly_)'}},
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index d781a87ab..6386c87a0 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -457,8 +457,7 @@ export namespace DragManager {
document.removeEventListener('pointerup', upHandler, true);
SnappingManager.SetIsDragging(false);
SnappingManager.clearSnapLines();
- const ended = batch.end();
- if (undo && ended) UndoManager.Undo();
+ if (batch.end() && undo) UndoManager.Undo();
docsBeingDragged.length = 0;
});
var startWindowDragTimer: any;
diff --git a/src/client/util/DropConverter.ts b/src/client/util/DropConverter.ts
index 256ab5c44..7c209d1e0 100644
--- a/src/client/util/DropConverter.ts
+++ b/src/client/util/DropConverter.ts
@@ -1,37 +1,41 @@
-import { DragManager } from "./DragManager";
-import { Doc, DocListCast, Opt } from "../../fields/Doc";
-import { DocumentType } from "../documents/DocumentTypes";
-import { ObjectField } from "../../fields/ObjectField";
-import { StrCast, Cast } from "../../fields/Types";
-import { Docs } from "../documents/Documents";
-import { ScriptField, ComputedField } from "../../fields/ScriptField";
-import { RichTextField } from "../../fields/RichTextField";
-import { ImageField } from "../../fields/URLField";
-import { ScriptingGlobals } from "./ScriptingGlobals";
-import { listSpec } from "../../fields/Schema";
+import { DragManager } from './DragManager';
+import { Doc, DocListCast, Opt } from '../../fields/Doc';
+import { DocumentType } from '../documents/DocumentTypes';
+import { ObjectField } from '../../fields/ObjectField';
+import { StrCast, Cast } from '../../fields/Types';
+import { Docs } from '../documents/Documents';
+import { ScriptField, ComputedField } from '../../fields/ScriptField';
+import { RichTextField } from '../../fields/RichTextField';
+import { ImageField } from '../../fields/URLField';
+import { ScriptingGlobals } from './ScriptingGlobals';
+import { listSpec } from '../../fields/Schema';
+import { ButtonType } from '../views/nodes/button/FontIconBox';
-export function MakeTemplate(doc: Doc, first: boolean = true, rename: Opt<string> = undefined, templateField: string = "") {
+export function MakeTemplate(doc: Doc, first: boolean = true, rename: Opt<string> = undefined, templateField: string = '') {
if (templateField) Doc.GetProto(doc).title = templateField; /// the title determines which field is being templated
doc.isTemplateDoc = makeTemplate(doc, first, rename);
return doc;
}
-//
+//
// converts 'doc' into a template that can be used to render other documents.
// the title of doc is used to determine which field is being templated, so
-// passing a value for 'rename' allows the doc to be given a meangingful name
+// passing a value for 'rename' allows the doc to be given a meangingful name
// after it has been converted to
function makeTemplate(doc: Doc, first: boolean = true, rename: Opt<string> = undefined): boolean {
const layoutDoc = doc.layout instanceof Doc && doc.layout.isTemplateForField ? doc.layout : doc;
- if (layoutDoc.layout instanceof Doc) { // its already a template
+ if (layoutDoc.layout instanceof Doc) {
+ // its already a template
return true;
}
const layout = StrCast(layoutDoc.layout).match(/fieldKey={'[^']*'}/)![0];
- const fieldKey = layout.replace("fieldKey={'", "").replace(/'}$/, "");
+ const fieldKey = layout.replace("fieldKey={'", '').replace(/'}$/, '');
const docs = DocListCast(layoutDoc[fieldKey]);
let any = false;
docs.forEach(d => {
- if (!StrCast(d.title).startsWith("-")) {
- const params = StrCast(d.title).match(/\(([a-zA-Z0-9._\-]*)\)/)?.[1].replace("()", "");
+ if (!StrCast(d.title).startsWith('-')) {
+ const params = StrCast(d.title)
+ .match(/\(([a-zA-Z0-9._\-]*)\)/)?.[1]
+ .replace('()', '');
if (params) {
any = makeTemplate(d, false) || any;
d.PARAMS = params;
@@ -43,12 +47,13 @@ function makeTemplate(doc: Doc, first: boolean = true, rename: Opt<string> = und
}
});
if (first) {
- if (!docs.length) { // bcz: feels hacky : if the root level document has items, it's not a field template
+ if (!docs.length) {
+ // bcz: feels hacky : if the root level document has items, it's not a field template
any = Doc.MakeMetadataFieldTemplate(doc, Doc.GetProto(layoutDoc)) || any;
}
}
if (layoutDoc[fieldKey] instanceof RichTextField || layoutDoc[fieldKey] instanceof ImageField) {
- if (!StrCast(layoutDoc.title).startsWith("-")) {
+ if (!StrCast(layoutDoc.title).startsWith('-')) {
any = Doc.MakeMetadataFieldTemplate(layoutDoc, Doc.GetProto(layoutDoc));
}
}
@@ -59,23 +64,29 @@ export function convertDropDataToButtons(data: DragManager.DocumentDragData) {
data?.draggedDocuments.map((doc, i) => {
let dbox = doc;
// bcz: isButtonBar is intended to allow a collection of linear buttons to be dropped and nested into another collection of buttons... it's not being used yet, and isn't very elegant
- if (doc.type === DocumentType.FONTICON || StrCast(Doc.Layout(doc).layout).includes("FontIconBox")) {
+ if (doc.type === DocumentType.FONTICON || StrCast(Doc.Layout(doc).layout).includes('FontIconBox')) {
if (data.removeDropProperties || dbox.removeDropProperties) {
//dbox = Doc.MakeAlias(doc); // don't need to do anything if dropping an icon doc onto an icon bar since there should be no layout data for an icon
dbox = Doc.MakeAlias(dbox);
- const dragProps = Cast(dbox.removeDropProperties, listSpec("string"), []);
+ const dragProps = Cast(dbox.removeDropProperties, listSpec('string'), []);
const remProps = (data.removeDropProperties || []).concat(Array.from(dragProps));
- remProps.map(prop => dbox[prop] = undefined);
+ remProps.map(prop => (dbox[prop] = undefined));
}
} else if (!doc.onDragStart && !doc.isButtonBar) {
- const layoutDoc = doc;// doc.layout instanceof Doc && doc.layout.isTemplateForField ? doc.layout : doc;
+ const layoutDoc = doc; // doc.layout instanceof Doc && doc.layout.isTemplateForField ? doc.layout : doc;
if (layoutDoc.type !== DocumentType.FONTICON) {
!layoutDoc.isTemplateDoc && makeTemplate(layoutDoc);
}
layoutDoc.isTemplateDoc = true;
dbox = Docs.Create.FontIconDocument({
- _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100,
- backgroundColor: StrCast(doc.backgroundColor), title: StrCast(layoutDoc.title), icon: layoutDoc.isTemplateDoc ? "font" : "bolt"
+ _nativeWidth: 100,
+ _nativeHeight: 100,
+ _width: 100,
+ _height: 100,
+ backgroundColor: StrCast(doc.backgroundColor),
+ title: StrCast(layoutDoc.title),
+ btnType: ButtonType.ClickButton,
+ icon: layoutDoc.isTemplateDoc ? 'font' : 'bolt',
});
dbox.dragFactory = layoutDoc;
dbox.removeDropProperties = doc.removeDropProperties instanceof ObjectField ? ObjectField.MakeCopy(doc.removeDropProperties) : undefined;
@@ -86,5 +97,10 @@ export function convertDropDataToButtons(data: DragManager.DocumentDragData) {
data.droppedDocuments[i] = dbox;
});
}
-ScriptingGlobals.add(function convertToButtons(dragData: any) { convertDropDataToButtons(dragData as DragManager.DocumentDragData); },
- "converts the dropped data to buttons", "(dragData: any)"); \ No newline at end of file
+ScriptingGlobals.add(
+ function convertToButtons(dragData: any) {
+ convertDropDataToButtons(dragData as DragManager.DocumentDragData);
+ },
+ 'converts the dropped data to buttons',
+ '(dragData: any)'
+);
diff --git a/src/client/util/InteractionUtils.tsx b/src/client/util/InteractionUtils.tsx
index 289c5bc51..4af51b9a0 100644
--- a/src/client/util/InteractionUtils.tsx
+++ b/src/client/util/InteractionUtils.tsx
@@ -1,12 +1,12 @@
-import React = require("react");
-import { Utils } from "../../Utils";
-import "./InteractionUtils.scss";
+import React = require('react');
+import { Utils } from '../../Utils';
+import './InteractionUtils.scss';
export namespace InteractionUtils {
- export const MOUSETYPE = "mouse";
- export const TOUCHTYPE = "touch";
- export const PENTYPE = "pen";
- export const ERASERTYPE = "eraser";
+ export const MOUSETYPE = 'mouse';
+ export const TOUCHTYPE = 'touch';
+ export const PENTYPE = 'pen';
+ export const ERASERTYPE = 'eraser';
const POINTER_PEN_BUTTON = -1;
const REACT_POINTER_PEN_BUTTON = 0;
@@ -19,24 +19,23 @@ export namespace InteractionUtils {
readonly touches: T extends React.TouchEvent ? React.Touch[] : Touch[],
readonly changedTouches: T extends React.TouchEvent ? React.Touch[] : Touch[],
readonly touchEvent: T extends React.TouchEvent ? React.TouchEvent : TouchEvent
- ) { }
+ ) {}
}
- export interface MultiTouchEventDisposer { (): void; }
+ export interface MultiTouchEventDisposer {
+ (): void;
+ }
/**
*
* @param element - element to turn into a touch target
* @param startFunc - event handler, typically Touchable.onTouchStart (classes that inherit touchable can pass in this.onTouchStart)
*/
- export function MakeMultiTouchTarget(
- element: HTMLElement,
- startFunc: (e: Event, me: MultiTouchEvent<React.TouchEvent>) => void
- ): MultiTouchEventDisposer {
+ export function MakeMultiTouchTarget(element: HTMLElement, startFunc: (e: Event, me: MultiTouchEvent<React.TouchEvent>) => void): MultiTouchEventDisposer {
const onMultiTouchStartHandler = (e: Event) => startFunc(e, (e as CustomEvent<MultiTouchEvent<React.TouchEvent>>).detail);
// const onMultiTouchMoveHandler = moveFunc ? (e: Event) => moveFunc(e, (e as CustomEvent<MultiTouchEvent<TouchEvent>>).detail) : undefined;
// const onMultiTouchEndHandler = endFunc ? (e: Event) => endFunc(e, (e as CustomEvent<MultiTouchEvent<TouchEvent>>).detail) : undefined;
- element.addEventListener("dashOnTouchStart", onMultiTouchStartHandler);
+ element.addEventListener('dashOnTouchStart', onMultiTouchStartHandler);
// if (onMultiTouchMoveHandler) {
// element.addEventListener("dashOnTouchMove", onMultiTouchMoveHandler);
// }
@@ -44,7 +43,7 @@ export namespace InteractionUtils {
// element.addEventListener("dashOnTouchEnd", onMultiTouchEndHandler);
// }
return () => {
- element.removeEventListener("dashOnTouchStart", onMultiTouchStartHandler);
+ element.removeEventListener('dashOnTouchStart', onMultiTouchStartHandler);
// if (onMultiTouchMoveHandler) {
// element.removeEventListener("dashOnTouchMove", onMultiTouchMoveHandler);
// }
@@ -59,14 +58,11 @@ export namespace InteractionUtils {
* @param element - element to add events to
* @param func - function to add to the event
*/
- export function MakeHoldTouchTarget(
- element: HTMLElement,
- func: (e: Event, me: MultiTouchEvent<React.TouchEvent>) => void
- ): MultiTouchEventDisposer {
+ export function MakeHoldTouchTarget(element: HTMLElement, func: (e: Event, me: MultiTouchEvent<React.TouchEvent>) => void): MultiTouchEventDisposer {
const handler = (e: Event) => func(e, (e as CustomEvent<MultiTouchEvent<React.TouchEvent>>).detail);
- element.addEventListener("dashOnTouchHoldStart", handler);
+ element.addEventListener('dashOnTouchHoldStart', handler);
return () => {
- element.removeEventListener("dashOnTouchHoldStart", handler);
+ element.removeEventListener('dashOnTouchHoldStart', handler);
};
}
@@ -89,71 +85,108 @@ export namespace InteractionUtils {
return myTouches;
}
- export function CreatePolyline(points: { X: number, Y: number }[], left: number, top: number,
- color: string, width: number, strokeWidth: number, lineJoin: string, lineCap: string, bezier: string, fill: string, arrowStart: string, arrowEnd: string,
- markerScale: number, dash: string | undefined, scalex: number, scaley: number, shape: string, pevents: string, opacity: number, nodefs: boolean,
- downHdlr?: ((e: React.PointerEvent) => void)) {
+ export function CreatePolyline(
+ points: { X: number; Y: number }[],
+ left: number,
+ top: number,
+ color: string,
+ width: number,
+ strokeWidth: number,
+ lineJoin: string,
+ lineCap: string,
+ bezier: string,
+ fill: string,
+ arrowStart: string,
+ arrowEnd: string,
+ markerScale: number,
+ dash: string | undefined,
+ scalex: number,
+ scaley: number,
+ shape: string,
+ pevents: string,
+ opacity: number,
+ nodefs: boolean,
+ downHdlr?: (e: React.PointerEvent) => void
+ ) {
const pts = shape ? makePolygon(shape, points) : points;
if (isNaN(scalex)) scalex = 1;
if (isNaN(scaley)) scaley = 1;
- const toScr = (p: { X: number, Y: number }) => ` ${!p ? 0 : (p.X - left - width / 2) * scalex + width / 2}, ${!p ? 0 : (p.Y - top - width / 2) * scaley + width / 2} `;
- const strpts = bezier ?
- pts.reduce((acc: string, pt, i) => acc + (i % 4 !== 0 ? "" : (i === 0 ? "M" + toScr(pt) : "") + "C" + toScr(pts[i + 1]) + toScr(pts[i + 2]) + toScr(pts[i + 3])), "") :
- pts.reduce((acc: string, pt) => acc + `${toScr(pt)} `, "");
+ const toScr = (p: { X: number; Y: number }) => ` ${!p ? 0 : (p.X - left - width / 2) * scalex + width / 2}, ${!p ? 0 : (p.Y - top - width / 2) * scaley + width / 2} `;
+ const strpts = bezier
+ ? pts.reduce((acc: string, pt, i) => acc + (i % 4 !== 0 ? '' : (i === 0 ? 'M' + toScr(pt) : '') + 'C' + toScr(pts[i + 1]) + toScr(pts[i + 2]) + toScr(pts[i + 3])), '')
+ : pts.reduce((acc: string, pt) => acc + `${toScr(pt)} `, '');
const dashArray = dash && Number(dash) ? String(Number(width) * Number(dash)) : undefined;
const defGuid = Utils.GenerateGuid();
- const Tag = (bezier ? "path" : "polyline") as keyof JSX.IntrinsicElements;
+ const Tag = (bezier ? 'path' : 'polyline') as keyof JSX.IntrinsicElements;
const markerStrokeWidth = strokeWidth / 2;
- const arrowWidthFactor = 3 * (markerScale || 0.5);// used to be 1.5
+ const arrowWidthFactor = 3 * (markerScale || 0.5); // used to be 1.5
const arrowLengthFactor = 5 * (markerScale || 0.5);
const arrowNotchFactor = 2 * (markerScale || 0.5);
- return (<svg fill={color} onPointerDown={downHdlr}> {/* setting the svg fill sets the arrowStart fill */}
- {nodefs ? (null) : <defs>
- {arrowStart !== "dot" && arrowEnd !== "dot" ? (null) :
- <marker id={`dot${defGuid}`} orient="auto" markerUnits="userSpaceOnUse" refX={0} refY="0" overflow="visible">
- <circle r={strokeWidth * arrowWidthFactor} fill="context-stroke" />
- </marker>}
- {arrowStart !== "arrow" ? (null) :
- <marker id={`arrowStart${defGuid}`} markerUnits="userSpaceOnUse" orient="auto" overflow="visible" refX={markerStrokeWidth * (arrowLengthFactor - arrowNotchFactor)} refY={0} markerWidth="10" markerHeight="7">
- <polygon style={{ stroke: color }} strokeLinejoin={lineJoin as any} strokeWidth={markerStrokeWidth * 2 / 3}
- points={`${arrowLengthFactor * markerStrokeWidth} ${-markerStrokeWidth * arrowWidthFactor}, ${markerStrokeWidth * (arrowLengthFactor - arrowNotchFactor)} 0, ${arrowLengthFactor * markerStrokeWidth} ${markerStrokeWidth * arrowWidthFactor}, 0 0`} />
- </marker>}
- {arrowEnd !== "arrow" ? (null) :
- <marker id={`arrowEnd${defGuid}`} markerUnits="userSpaceOnUse" orient="auto" overflow="visible" refX={markerStrokeWidth * arrowNotchFactor} refY={0} markerWidth="10" markerHeight="7">
- <polygon style={{ stroke: color }} strokeLinejoin={lineJoin as any} strokeWidth={markerStrokeWidth * 2 / 3}
- points={`0 ${-markerStrokeWidth * arrowWidthFactor}, ${markerStrokeWidth * arrowNotchFactor} 0, 0 ${markerStrokeWidth * arrowWidthFactor}, ${arrowLengthFactor * markerStrokeWidth} 0`} />
- </marker>}
- </defs>}
-
- <Tag
- d={bezier ? strpts : undefined}
- points={bezier ? undefined : strpts}
- style={{
- // filter: drawHalo ? "url(#inkSelectionHalo)" : undefined,
- fill: fill && fill !== "transparent" ? fill : "none",
- opacity: 1.0,
- // opacity: strokeWidth !== width ? 0.5 : undefined,
- pointerEvents: pevents as any,
- stroke: color ?? "rgb(0, 0, 0)",
- strokeWidth: strokeWidth,
- strokeLinecap: lineCap as any,
- strokeDasharray: dashArray
- }}
- markerStart={`url(#${arrowStart === "dot" ? arrowStart + defGuid : arrowStart + "Start" + defGuid})`}
- markerEnd={`url(#${arrowEnd === "dot" ? arrowEnd + defGuid : arrowEnd + "End" + defGuid})`}
- />
-
- </svg>);
+ return (
+ <svg fill={color} style={{ transition: 'inherit' }} onPointerDown={downHdlr}>
+ {' '}
+ {/* setting the svg fill sets the arrowStart fill */}
+ {nodefs ? null : (
+ <defs>
+ {arrowStart !== 'dot' && arrowEnd !== 'dot' ? null : (
+ <marker id={`dot${defGuid}`} orient="auto" markerUnits="userSpaceOnUse" refX={0} refY="0" overflow="visible">
+ <circle r={strokeWidth * arrowWidthFactor} fill="context-stroke" />
+ </marker>
+ )}
+ {arrowStart !== 'arrow' ? null : (
+ <marker id={`arrowStart${defGuid}`} markerUnits="userSpaceOnUse" orient="auto" overflow="visible" refX={markerStrokeWidth * (arrowLengthFactor - arrowNotchFactor)} refY={0} markerWidth="10" markerHeight="7">
+ <polygon
+ style={{ stroke: color }}
+ strokeLinejoin={lineJoin as any}
+ strokeWidth={(markerStrokeWidth * 2) / 3}
+ points={`${arrowLengthFactor * markerStrokeWidth} ${-markerStrokeWidth * arrowWidthFactor}, ${markerStrokeWidth * (arrowLengthFactor - arrowNotchFactor)} 0, ${arrowLengthFactor * markerStrokeWidth} ${
+ markerStrokeWidth * arrowWidthFactor
+ }, 0 0`}
+ />
+ </marker>
+ )}
+ {arrowEnd !== 'arrow' ? null : (
+ <marker id={`arrowEnd${defGuid}`} markerUnits="userSpaceOnUse" orient="auto" overflow="visible" refX={markerStrokeWidth * arrowNotchFactor} refY={0} markerWidth="10" markerHeight="7">
+ <polygon
+ style={{ stroke: color }}
+ strokeLinejoin={lineJoin as any}
+ strokeWidth={(markerStrokeWidth * 2) / 3}
+ points={`0 ${-markerStrokeWidth * arrowWidthFactor}, ${markerStrokeWidth * arrowNotchFactor} 0, 0 ${markerStrokeWidth * arrowWidthFactor}, ${arrowLengthFactor * markerStrokeWidth} 0`}
+ />
+ </marker>
+ )}
+ </defs>
+ )}
+ <Tag
+ d={bezier ? strpts : undefined}
+ points={bezier ? undefined : strpts}
+ style={{
+ // filter: drawHalo ? "url(#inkSelectionHalo)" : undefined,
+ fill: fill && fill !== 'transparent' ? fill : 'none',
+ opacity: 1.0,
+ // opacity: strokeWidth !== width ? 0.5 : undefined,
+ pointerEvents: pevents as any,
+ stroke: color ?? 'rgb(0, 0, 0)',
+ strokeWidth: strokeWidth,
+ strokeLinecap: lineCap as any,
+ strokeDasharray: dashArray,
+ transition: 'inherit',
+ }}
+ markerStart={`url(#${arrowStart === 'dot' ? arrowStart + defGuid : arrowStart + 'Start' + defGuid})`}
+ markerEnd={`url(#${arrowEnd === 'dot' ? arrowEnd + defGuid : arrowEnd + 'End' + defGuid})`}
+ />
+ </svg>
+ );
}
- export function makePolygon(shape: string, points: { X: number, Y: number }[]) {
+ export function makePolygon(shape: string, points: { X: number; Y: number }[]) {
if (points.length > 1 && points[points.length - 1].X === points[0].X && points[points.length - 1].Y + 1 === points[0].Y) {
//pointer is up (first and last points are the same)
- if (shape === "arrow" || shape === "line" || shape === "circle") {
+ if (shape === 'arrow' || shape === 'line' || shape === 'circle') {
//if arrow or line, the two end points should be the starting and the ending point
var left = points[0].X;
var top = points[0].Y;
@@ -175,7 +208,7 @@ export namespace InteractionUtils {
left = points[0].X;
bottom = points[points.length - 1].Y;
top = points[0].Y;
- if (shape !== "arrow" && shape !== "line" && shape !== "circle") {
+ if (shape !== 'arrow' && shape !== 'line' && shape !== 'circle') {
//switch left/right and top/bottom if needed
if (left > right) {
const temp = right;
@@ -191,14 +224,13 @@ export namespace InteractionUtils {
}
points = [];
switch (shape) {
- case "rectangle":
+ case 'rectangle':
points.push({ X: left, Y: top });
points.push({ X: right, Y: top });
points.push({ X: right, Y: bottom });
points.push({ X: left, Y: bottom });
points.push({ X: left, Y: top });
- return points;
- case "triangle":
+ case 'triangle':
// points.push({ X: left, Y: bottom });
// points.push({ X: right, Y: bottom });
// points.push({ X: (right + left) / 2, Y: top });
@@ -219,62 +251,39 @@ export namespace InteractionUtils {
points.push({ X: left, Y: bottom });
points.push({ X: left, Y: bottom });
-
-
- return points;
- case "circle":
+ case 'circle':
const centerX = (Math.max(left, right) + Math.min(left, right)) / 2;
const centerY = (Math.max(top, bottom) + Math.min(top, bottom)) / 2;
const radius = Math.max(centerX - Math.min(left, right), centerY - Math.min(top, bottom));
if (centerX - Math.min(left, right) < centerY - Math.min(top, bottom)) {
for (var y = Math.min(top, bottom); y < Math.max(top, bottom); y++) {
- const x = Math.sqrt(Math.pow(radius, 2) - (Math.pow((y - centerY), 2))) + centerX;
+ const x = Math.sqrt(Math.pow(radius, 2) - Math.pow(y - centerY, 2)) + centerX;
points.push({ X: x, Y: y });
}
for (var y = Math.max(top, bottom); y > Math.min(top, bottom); y--) {
- const x = Math.sqrt(Math.pow(radius, 2) - (Math.pow((y - centerY), 2))) + centerX;
+ const x = Math.sqrt(Math.pow(radius, 2) - Math.pow(y - centerY, 2)) + centerX;
const newX = centerX - (x - centerX);
points.push({ X: newX, Y: y });
}
- points.push({ X: Math.sqrt(Math.pow(radius, 2) - (Math.pow((Math.min(top, bottom) - centerY), 2))) + centerX, Y: Math.min(top, bottom) });
+ points.push({ X: Math.sqrt(Math.pow(radius, 2) - Math.pow(Math.min(top, bottom) - centerY, 2)) + centerX, Y: Math.min(top, bottom) });
} else {
for (var x = Math.min(left, right); x < Math.max(left, right); x++) {
- const y = Math.sqrt(Math.pow(radius, 2) - (Math.pow((x - centerX), 2))) + centerY;
+ const y = Math.sqrt(Math.pow(radius, 2) - Math.pow(x - centerX, 2)) + centerY;
points.push({ X: x, Y: y });
}
for (var x = Math.max(left, right); x > Math.min(left, right); x--) {
- const y = Math.sqrt(Math.pow(radius, 2) - (Math.pow((x - centerX), 2))) + centerY;
+ const y = Math.sqrt(Math.pow(radius, 2) - Math.pow(x - centerX, 2)) + centerY;
const newY = centerY - (y - centerY);
points.push({ X: x, Y: newY });
}
- points.push({ X: Math.min(left, right), Y: Math.sqrt(Math.pow(radius, 2) - (Math.pow((Math.min(left, right) - centerX), 2))) + centerY });
+ points.push({ X: Math.min(left, right), Y: Math.sqrt(Math.pow(radius, 2) - Math.pow(Math.min(left, right) - centerX, 2)) + centerY });
}
- return points;
- // case "arrow":
- // const x1 = left;
- // const y1 = top;
- // const x2 = right;
- // const y2 = bottom;
- // const L1 = Math.sqrt(Math.pow(Math.abs(x1 - x2), 2) + (Math.pow(Math.abs(y1 - y2), 2)));
- // const L2 = L1 / 5;
- // const angle = 0.785398;
- // const x3 = x2 + (L2 / L1) * ((x1 - x2) * Math.cos(angle) + (y1 - y2) * Math.sin(angle));
- // const y3 = y2 + (L2 / L1) * ((y1 - y2) * Math.cos(angle) - (x1 - x2) * Math.sin(angle));
- // const x4 = x2 + (L2 / L1) * ((x1 - x2) * Math.cos(angle) - (y1 - y2) * Math.sin(angle));
- // const y4 = y2 + (L2 / L1) * ((y1 - y2) * Math.cos(angle) + (x1 - x2) * Math.sin(angle));
- // points.push({ X: x1, Y: y1 });
- // points.push({ X: x2, Y: y2 });
- // points.push({ X: x3, Y: y3 });
- // points.push({ X: x4, Y: y4 });
- // points.push({ X: x2, Y: y2 });
- // return points;
- case "line":
+ case 'line':
points.push({ X: left, Y: top });
points.push({ X: right, Y: bottom });
return points;
- default:
- return points;
}
+ return points;
}
/**
* Returns whether or not the pointer event passed in is of the type passed in
@@ -284,11 +293,14 @@ export namespace InteractionUtils {
export function IsType(e: PointerEvent | React.PointerEvent, type: string): boolean {
switch (type) {
// pen and eraser are both pointer type 'pen', but pen is button 0 and eraser is button 5. -syip2
- case PENTYPE: return e.pointerType === PENTYPE && (e.button === -1 || e.button === 0);
- case ERASERTYPE: return e.pointerType === PENTYPE && e.button === (e instanceof PointerEvent ? ERASER_BUTTON : ERASER_BUTTON);
+ case PENTYPE:
+ return e.pointerType === PENTYPE && (e.button === -1 || e.button === 0);
+ case ERASERTYPE:
+ return e.pointerType === PENTYPE && e.button === (e instanceof PointerEvent ? ERASER_BUTTON : ERASER_BUTTON);
case TOUCHTYPE:
return e.pointerType === TOUCHTYPE;
- default: return e.pointerType === type;
+ default:
+ return e.pointerType === type;
}
}
@@ -305,7 +317,7 @@ export namespace InteractionUtils {
* Returns the centroid of an n-arbitrary long list of points (takes the average the x and y components of each point)
* @param pts - n-arbitrary long list of points
*/
- export function CenterPoint(pts: React.Touch[]): { X: number, Y: number } {
+ export function CenterPoint(pts: React.Touch[]): { X: number; Y: number } {
const centerX = pts.map(pt => pt.clientX).reduce((a, b) => a + b, 0) / pts.length;
const centerY = pts.map(pt => pt.clientY).reduce((a, b) => a + b, 0) / pts.length;
return { X: centerX, Y: centerY };
@@ -324,9 +336,9 @@ export namespace InteractionUtils {
const newDist = TwoPointEuclidist(pt1, pt2);
/** if they have the same sign, then we are either pinching in or out.
- * threshold it by 10 (it has to be pinching by at least threshold to be a valid pinch)
- * so that it can still pan without freaking out
- */
+ * threshold it by 10 (it has to be pinching by at least threshold to be a valid pinch)
+ * so that it can still pan without freaking out
+ */
if (Math.sign(oldDist) === Math.sign(newDist) && Math.abs(oldDist - newDist) > threshold) {
return Math.sign(oldDist - newDist);
}
@@ -372,8 +384,6 @@ export namespace InteractionUtils {
// These might not be very useful anymore, but I'll leave them here for now -syip2
{
-
-
/**
* Returns the type of Touch Interaction from a list of points.
* Also returns any data that is associated with a Touch Interaction
diff --git a/src/client/util/ReportManager.scss b/src/client/util/ReportManager.scss
new file mode 100644
index 000000000..5a2f2fcad
--- /dev/null
+++ b/src/client/util/ReportManager.scss
@@ -0,0 +1,88 @@
+@import '../views/global/globalCssVariables';
+
+.issue-list-wrapper {
+ position: relative;
+ min-width: 250px;
+ background-color: $light-blue;
+ overflow-y: scroll;
+}
+
+.issue-list {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 5px;
+ margin: 5px;
+ border-radius: 5px;
+ border: 1px solid grey;
+ background-color: lightgoldenrodyellow;
+}
+
+// issue should pop up when the user hover over the issue
+.issue-list:hover {
+ box-shadow: 2px;
+ cursor: pointer;
+ border: 3px solid #252b33;
+}
+
+.issue-content {
+ background-color: white;
+ padding: 10px;
+ flex: 1 1 auto;
+ overflow-y: scroll;
+}
+
+.issue-title {
+ font-size: 20px;
+ font-weight: 600;
+ color: black;
+}
+
+.issue-body {
+ padding: 0 10px;
+ width: 100%;
+ text-align: left;
+}
+
+.issue-body > * {
+ margin-top: 5px;
+}
+
+.issue-body img,
+.issue-body video {
+ display: block;
+ max-width: 100%;
+}
+
+.report-issue-fab {
+ position: fixed;
+ bottom: 20px;
+ right: 20px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+}
+
+.loading-center {
+ margin: auto 0;
+}
+
+.settings-content label {
+ margin-top: 10px;
+}
+
+.report-disclaimer {
+ font-size: 8px;
+ color: grey;
+ padding-right: 50px;
+ font-style: italic;
+ text-align: left;
+}
+
+.flex-select {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 10px;
+}
diff --git a/src/client/util/ReportManager.tsx b/src/client/util/ReportManager.tsx
new file mode 100644
index 000000000..55c5ca87f
--- /dev/null
+++ b/src/client/util/ReportManager.tsx
@@ -0,0 +1,282 @@
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { action, computed, observable, runInAction } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { ColorState, SketchPicker } from 'react-color';
+import { Doc } from '../../fields/Doc';
+import { Id } from '../../fields/FieldSymbols';
+import { BoolCast, Cast, StrCast } from '../../fields/Types';
+import { addStyleSheet, addStyleSheetRule, Utils } from '../../Utils';
+import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager';
+import { DocServer } from '../DocServer';
+import { Networking } from '../Network';
+import { MainViewModal } from '../views/MainViewModal';
+import { FontIconBox } from '../views/nodes/button/FontIconBox';
+import { DragManager } from './DragManager';
+import { GroupManager } from './GroupManager';
+import './SettingsManager.scss';
+import './ReportManager.scss';
+import { undoBatch } from './UndoManager';
+import { Octokit } from "@octokit/core";
+import { CheckBox } from '../views/search/CheckBox';
+import ReactLoading from 'react-loading';
+import ReactMarkdown from 'react-markdown';
+import rehypeRaw from 'rehype-raw';
+import remarkGfm from 'remark-gfm';
+const higflyout = require('@hig/flyout');
+export const { anchorPoints } = higflyout;
+export const Flyout = higflyout.default;
+
+@observer
+export class ReportManager extends React.Component<{}> {
+ public static Instance: ReportManager;
+ @observable private isOpen = false;
+
+ private octokit: Octokit;
+
+ @observable public issues: any[] = [];
+ @action setIssues = action((issues: any[]) => { this.issues = issues; });
+
+ // undefined is the default - null is if the user is making an issue
+ @observable public selectedIssue: any = undefined;
+ @action setSelectedIssue = action((issue: any) => { this.selectedIssue = issue; });
+
+ // only get the open issues
+ @observable public shownIssues = this.issues.filter(issue => issue.state === 'open');
+
+ public updateIssueSearch = action((query: string = '') => {
+ if (query === '') {
+ this.shownIssues = this.issues.filter(issue => issue.state === 'open');
+ return;
+ }
+ this.shownIssues = this.issues.filter(issue => issue.title.toLowerCase().includes(query.toLowerCase()));
+ });
+
+ constructor(props: {}) {
+ super(props);
+ ReportManager.Instance = this;
+
+ this.octokit = new Octokit({
+ auth: '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'
+ });
+ }
+
+ public close = action(() => (this.isOpen = false));
+ public open = action(() => {
+ if (this.issues.length === 0) {
+ // load in the issues if not already loaded
+ this.getAllIssues()
+ .then(issues => {
+ this.setIssues(issues);
+ this.updateIssueSearch();
+ })
+ .catch(err => console.log(err));
+ }
+ (this.isOpen = true)
+ });
+
+ @observable private bugTitle = '';
+ @action setBugTitle = action((title: string) => { this.bugTitle = title; });
+ @observable private bugDescription = '';
+ @action setBugDescription = action((description: string) => { this.bugDescription = description; });
+ @observable private bugType = '';
+ @action setBugType = action((type: string) => { this.bugType = type; });
+ @observable private bugPriority = '';
+ @action setBugPriority = action((priortiy: string) => { this.bugPriority = priortiy; });
+
+ // private toGithub = false;
+ // will always be set to true - no alterntive option yet
+ private toGithub = true;
+
+ private formatTitle = (title: string, userEmail: string) => `${title} - ${userEmail.replace('@brown.edu', '')}`;
+
+ public async getAllIssues() : Promise<any[]> {
+ const res = await this.octokit.request('GET /repos/{owner}/{repo}/issues', {
+ owner: 'brown-dash',
+ repo: 'Dash-Web',
+ });
+
+ // 200 status means success
+ if (res.status === 200) {
+ return res.data;
+ } else {
+ throw new Error('Error getting issues');
+ }
+ }
+
+ public async reportIssue() {
+ if (this.bugTitle === '' || this.bugDescription === ''
+ || this.bugType === '' || this.bugPriority === '') {
+ alert('Please fill out all required fields to report an issue.');
+ return;
+ }
+
+
+ if (this.toGithub) {
+
+ const req = await this.octokit.request('POST /repos/{owner}/{repo}/issues', {
+ owner: 'brown-dash',
+ repo: 'Dash-Web',
+ title: this.formatTitle(this.bugTitle, Doc.CurrentUserEmail),
+ body: `${this.bugDescription} \n\nfiles:\n${(this.fileLinks ?? []).join('\n')}`,
+ labels: [
+ 'from-dash-app',
+ this.bugType,
+ this.bugPriority
+ ]
+ });
+
+ // 201 status means success
+ if (req.status !== 201) {
+ alert('Error creating issue on github.');
+ // on error, don't close the modal
+ return;
+ }
+ }
+ else {
+ // if not going to github issues, not sure what to do yet...
+ }
+
+ // if we're down here, then we're good to go. reset the fields.
+ this.setBugTitle('');
+ this.setBugDescription('');
+ this.toGithub = false;
+ this.setFileLinks([]);
+ this.setBugType('');
+ this.setBugPriority('');
+ this.close();
+ }
+
+ @observable public fileLinks: any = [];
+ @action setFileLinks = action((links: any) => { this.fileLinks = links; });
+
+ private getServerPath = (link: any) => { return link.result.accessPaths.agnostic.server }
+
+ private uploadFiles = (input: any) => {
+ // keep null while uploading
+ this.setFileLinks(null);
+ // upload the files to the server
+ if (input.files && input.files.length !== 0) {
+ const fileArray: File[] = Array.from(input.files);
+ (Networking.UploadFilesToServer(fileArray)).then(links => {
+ console.log('finshed uploading', links.map(this.getServerPath));
+ this.setFileLinks((links ?? []).map(this.getServerPath));
+ })
+ }
+
+ }
+
+
+ private renderIssue = (issue: any) => {
+
+ const isReportingIssue = issue === null;
+
+ return isReportingIssue ?
+ // report issue
+ (<div className="settings-content">
+ <h3 style={{ 'textDecoration': 'underline'}}>Report an Issue</h3>
+ <label>Please leave a title for the bug.</label><br />
+ <input type="text" placeholder='title' onChange={(e) => this.bugTitle = e.target.value} required/>
+ <br />
+ <label>Please leave a description for the bug and how it can be recreated.</label>
+ <textarea placeholder='description' onChange={(e) => this.bugDescription = e.target.value} required/>
+ <br />
+ {/* {<label>Send to github issues? </label>
+ <input type="checkbox" onChange={(e) => this.toGithub = e.target.checked} />
+ <br /> } */}
+
+ <label>Please label the issue</label>
+ <div className='flex-select'>
+ <select name="bugType">
+ <option value="" disabled selected>Type</option>
+ <option value="bug">Bug</option>
+ <option value="cosmetic">Poor Design or Cosmetic</option>
+ <option value="documentation">Poor Documentation</option>
+ </select>
+
+ <select name="bigPriority">
+ <option value="" disabled selected>Priority</option>
+ <option value="priority-low">Low</option>
+ <option value="priority-medium">Medium</option>
+ <option value="priority-high">High</option>
+ </select>
+ </div>
+
+
+ <div>
+ <label>Upload media that shows the bug (optional)</label>
+ <input type="file" name="file" multiple accept='audio/*, video/*' onChange={e => this.uploadFiles(e.target)}/>
+ </div>
+ <br />
+
+ <button onClick={() => this.reportIssue()} disabled={this.fileLinks === null} style={{ backgroundColor: this.fileLinks === null ? 'grey' : '' }}>{this.fileLinks === null ? 'Uploading...' : 'Submit'}</button>
+ </div>)
+ :
+ // view issue
+ (
+ <div className='issue-container'>
+ <h5 style={{'textAlign': "left"}}><a href={issue.html_url} target="_blank">Issue #{issue.number}</a></h5>
+ <div className='issue-title'>
+ {issue.title}
+ </div>
+ <ReactMarkdown children={issue.body} className='issue-body' linkTarget={"_blank"} remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]} />
+ </div>
+ );
+ }
+
+ private showReportIssueScreen = () => {
+ this.setSelectedIssue(null);
+ }
+
+ private closeReportIssueScreen = () => {
+ this.setSelectedIssue(undefined);
+ }
+
+ private get reportInterface() {
+
+ const isReportingIssue = this.selectedIssue === null;
+
+ return (
+ <div className="settings-interface">
+ <div className='issue-list-wrapper'>
+ <h3>Current Issues</h3>
+ <input type="text" placeholder='search issues' onChange={(e => this.updateIssueSearch(e.target.value))}></input><br />
+ {this.issues.length === 0 ? <ReactLoading className='loading-center'/> : this.shownIssues.map(issue => <div className='issue-list' key={issue.number} onClick={() => this.setSelectedIssue(issue)}>{issue.title}</div>)}
+
+ {/* <div className="settings-user">
+ <button onClick={() => this.getAllIssues().then(issues => this.issues = issues)}>Poll Issues</button>
+ </div> */}
+ </div>
+
+ <div className="close-button" onClick={this.close}>
+ <FontAwesomeIcon icon={'times'} color="black" size={'lg'} />
+ </div>
+
+ <div className="issue-content" style={{'paddingTop' : this.selectedIssue === undefined ? '50px' : 'inherit'}}>
+ {this.selectedIssue === undefined ? "no issue selected" : this.renderIssue(this.selectedIssue)}
+ </div>
+
+ <div className='report-issue-fab'>
+ <span className='report-disclaimer' hidden={!isReportingIssue}>Note: issue reporting is not anonymous.</span>
+ <button
+ onClick={() => isReportingIssue ? this.closeReportIssueScreen() : this.showReportIssueScreen()}
+ >{isReportingIssue ? 'Cancel' : 'Report New Issue'}</button>
+ </div>
+
+
+ </div>
+ );
+ }
+
+ render() {
+ return (
+ <MainViewModal
+ contents={this.reportInterface}
+ isDisplayed={this.isOpen}
+ interactive={true}
+ closeOnExternalClick={this.close}
+ dialogueBoxStyle={{ width: 'auto', height: '500px', background: Cast(Doc.SharingDoc().userColor, 'string', null) }}
+ />
+ );
+ }
+}
diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index 1c84af94a..7a555d5f8 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -101,6 +101,9 @@ export namespace SelectionManager {
}
}
ScriptingGlobals.add(function SelectionManager_selectedDocType(docType?: DocumentType, colType?: CollectionViewType, checkContext?: boolean) {
+ if (colType === ('tab' as any)) {
+ return SelectionManager.Views().lastElement()?.props.renderDepth === 0;
+ }
let selected = (sel => (checkContext ? DocCast(sel?.context) : sel))(SelectionManager.SelectedSchemaDoc() ?? SelectionManager.Docs().lastElement());
return docType ? selected?.type === docType : colType ? selected?.viewType === colType : true;
});