aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/client/DocServer.ts2
-rw-r--r--src/client/Network.ts7
-rw-r--r--src/client/documents/Documents.ts58
-rw-r--r--src/client/util/CurrentUserUtils.ts303
-rw-r--r--src/client/util/DropConverter.ts2
-rw-r--r--src/client/util/LinkManager.ts123
-rw-r--r--src/client/util/SettingsManager.tsx5
-rw-r--r--src/client/views/ContextMenu.tsx1
-rw-r--r--src/client/views/ContextMenuItem.tsx4
-rw-r--r--src/client/views/DashboardView.scss18
-rw-r--r--src/client/views/DashboardView.tsx153
-rw-r--r--src/client/views/DocumentDecorations.tsx23
-rw-r--r--src/client/views/Main.tsx2
-rw-r--r--src/client/views/MainView.tsx14
-rw-r--r--src/client/views/collections/CollectionDockingView.tsx37
-rw-r--r--src/client/views/collections/CollectionStackedTimeline.tsx6
-rw-r--r--src/client/views/collections/CollectionSubView.tsx6
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx51
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx4
-rw-r--r--src/client/views/nodes/AudioBox.tsx18
-rw-r--r--src/client/views/nodes/DataViz.tsx21
-rw-r--r--src/client/views/nodes/DataVizBox/DataVizBox.scss (renamed from src/client/views/nodes/DataViz.scss)0
-rw-r--r--src/client/views/nodes/DataVizBox/DataVizBox.tsx90
-rw-r--r--src/client/views/nodes/DataVizBox/DrawHelper.ts247
-rw-r--r--src/client/views/nodes/DataVizBox/HistogramBox.scss18
-rw-r--r--src/client/views/nodes/DataVizBox/HistogramBox.tsx159
-rw-r--r--src/client/views/nodes/DataVizBox/TableBox.scss22
-rw-r--r--src/client/views/nodes/DataVizBox/TableBox.tsx37
-rw-r--r--src/client/views/nodes/DocumentContentsView.tsx2
-rw-r--r--src/client/views/nodes/DocumentView.tsx8
-rw-r--r--src/client/views/nodes/button/FontIconBadge.tsx2
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx7
-rw-r--r--src/client/views/topbar/TopBar.scss4
-rw-r--r--src/client/views/topbar/TopBar.tsx37
-rw-r--r--src/fields/Doc.ts44
-rw-r--r--src/fields/URLField.ts1
-rw-r--r--src/server/ApiManagers/UploadManager.ts1
-rw-r--r--src/server/DashUploadUtils.ts18
-rw-r--r--src/server/DataVizUtils.ts13
39 files changed, 1120 insertions, 448 deletions
diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts
index dbc4783d8..c9a30b8e3 100644
--- a/src/client/DocServer.ts
+++ b/src/client/DocServer.ts
@@ -180,7 +180,7 @@ export namespace DocServer {
_isReadOnly = true;
_CreateField = field => _cache[field[Id]] = field;
_UpdateField = emptyFunction;
- _RespondToUpdate = emptyFunction;
+ _RespondToUpdate = emptyFunction; // bcz: option: don't clear RespondToUpdate to continue to receive updates as others change the DB
}
}
diff --git a/src/client/Network.ts b/src/client/Network.ts
index b26f2458d..c781d4b6b 100644
--- a/src/client/Network.ts
+++ b/src/client/Network.ts
@@ -18,6 +18,13 @@ export namespace Networking {
return requestPromise.post(options);
}
+ /**
+ * Handles uploading basic file types to server and makes the API call to "/uploadFormData" endpoint
+ * with the mapping of GUID to filem as parameters.
+ *
+ * @param files the files to be uploaded to the server
+ * @returns the response as a json from the server
+ */
export async function UploadFilesToServer<T extends Upload.FileInformation = Upload.FileInformation>(files: File | File[]): Promise<Upload.FileResponse<T>[]> {
const formData = new FormData();
if (Array.isArray(files)) {
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index fc73deb36..65b3db0e2 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -12,8 +12,8 @@ import { RichTextField } from "../../fields/RichTextField";
import { SchemaHeaderField } from "../../fields/SchemaHeaderField";
import { ComputedField, ScriptField } from "../../fields/ScriptField";
import { Cast, NumCast, StrCast } from "../../fields/Types";
-import { AudioField, ImageField, MapField, PdfField, RecordingField, VideoField, WebField, YoutubeField } from "../../fields/URLField";
-import { SharingPermissions } from "../../fields/util";
+import { AudioField, CsvField, ImageField, MapField, PdfField, RecordingField, VideoField, WebField, YoutubeField } from "../../fields/URLField";
+import { inheritParentAcls, SharingPermissions } from "../../fields/util";
import { Upload } from "../../server/SharedMediaTypes";
import { aggregateBounds, OmitKeys, Utils } from "../../Utils";
import { YoutubeBox } from "../apis/youtube/YoutubeBox";
@@ -37,7 +37,7 @@ import { AudioBox } from "../views/nodes/AudioBox";
import { FontIconBox } from "../views/nodes/button/FontIconBox";
import { ColorBox } from "../views/nodes/ColorBox";
import { ComparisonBox } from "../views/nodes/ComparisonBox";
-import { DataVizBox } from "../views/nodes/DataViz";
+import { DataVizBox } from "../views/nodes/DataVizBox/DataVizBox";
import { DocFocusOptions } from "../views/nodes/DocumentView";
import { EquationBox } from "../views/nodes/EquationBox";
import { FieldViewProps } from "../views/nodes/FieldView";
@@ -105,7 +105,6 @@ export class DocumentOptions {
userColor?: STRt = new StrInfo("color associated with a Dash user (seen in header fields of shared documents)");
color?: STRt = new StrInfo("foreground color data doc");
backgroundColor?: STRt = new StrInfo("background color for data doc");
- _backgroundColor?: STRt = new StrInfo("background color for each template layout doc (overrides backgroundColor)");
_autoHeight?: BOOLt = new BoolInfo("whether document automatically resizes vertically to display contents");
_headerHeight?: NUMt = new NumInfo("height of document header used for displaying title");
_headerFontSize?: NUMt = new NumInfo("font size of header of custom notes");
@@ -261,7 +260,7 @@ export class DocumentOptions {
numBtnType?: string;
numBtnMax?: number;
numBtnMin?: number;
- switchToggle?: boolean;
+ switchToggle?: boolean;
badgeValue?: ScriptField;
//LINEAR VIEW
@@ -308,7 +307,6 @@ export class DocumentOptions {
treeViewHideHeaderFields?: boolean; // whether to hide the drop down options for tree view items.
treeViewGrowsHorizontally?: boolean; // whether an embedded tree view of the document can grow horizontally without growing vertically
treeViewChildDoubleClick?: ScriptField; //
-
// Action Button
buttonMenu?: boolean; // whether a action button should be displayed
buttonMenuDoc?: Doc;
@@ -329,6 +327,7 @@ export class DocumentOptions {
text?: string;
textTransform?: string; // is linear view expanded
letterSpacing?: string; // is linear view expanded
+ iconTemplate?: string; // name of icon template style
selectedIndex?: number; // which item in a linear view has been selected using the "thumb doc" ui
clipboard?: Doc;
searchQuery?: string; // for quersyBox
@@ -362,7 +361,7 @@ export namespace Docs {
[DocumentType.RTF, {
layout: { view: FormattedTextBox, dataField: "text" },
options: {
- _height: 35, _xMargin: 10, _yMargin: 10, nativeDimModifiable: true, nativeHeightUnfrozen: true, treeViewGrowsHorizontally: true,
+ _height: 35, _xMargin: 10, _yMargin: 10, nativeDimModifiable: true, treeViewGrowsHorizontally: true,
forceReflow: true, links: "@links(self)"
}
}],
@@ -905,8 +904,8 @@ export namespace Docs {
return InstanceFromProto(Prototypes.get(DocumentType.PRESELEMENT), undefined, { ...(options || {}) });
}
- export function DataVizDocument(options?: DocumentOptions) {
- return InstanceFromProto(Prototypes.get(DocumentType.DATAVIZ), undefined, { title: "Data Viz", ...options });
+ export function DataVizDocument(url: string, options?: DocumentOptions) {
+ return InstanceFromProto(Prototypes.get(DocumentType.DATAVIZ), new CsvField(url), { title: "Data Viz", ...options });
}
export function DockDocument(documents: Array<Doc>, config: string, options: DocumentOptions, id?: string) {
@@ -1218,6 +1217,11 @@ export namespace DocUtils {
if (!options._width) options._width = 400;
if (!options._height) options._height = (options._width as number) * 1200 / 927;
}
+ if (type.indexOf("csv") !== -1) {
+ ctor = Docs.Create.DataVizDocument;
+ if (!options._width) options._width = 400;
+ if (!options._height) options._height = (options._width as number) * 1200 / 927;
+ }
//TODO:al+glr
// if (type.indexOf("map") !== -1) {
// ctor = Docs.Create.MapDocument;
@@ -1243,6 +1247,7 @@ export namespace DocUtils {
ctor = Docs.Create.WebDocument;
options = { ...options, _width: 400, _height: 512, title: path, };
}
+
return ctor ? ctor(path, options) : undefined;
}
@@ -1267,7 +1272,7 @@ export namespace DocUtils {
const documentList: ContextMenuProps[] = DocListCast(DocListCast(CurrentUserUtils.MyTools?.data)[0]?.data).filter(btnDoc => !btnDoc.hidden).map(btnDoc => Cast(btnDoc?.dragFactory, Doc, null)).filter(doc => doc && doc !== Doc.UserDoc().emptyPresentation).map((dragDoc, i) => ({
description: ":" + StrCast(dragDoc.title).replace("Untitled ",""),
event: undoBatch((args: { x: number, y: number }) => {
- const newDoc = Doc.copyDragFactory(dragDoc);
+ const newDoc = DocUtils.copyDragFactory(dragDoc);
if (newDoc) {
newDoc.author = Doc.CurrentUserEmail;
newDoc.x = x;
@@ -1289,9 +1294,7 @@ export namespace DocUtils {
const batch = UndoManager.StartBatch("makeCustomViewClicked");
runInAction(() => {
doc.layoutKey = "layout_" + templateSignature;
- if (doc[doc.layoutKey] === undefined) {
- createCustomView(doc, creator, templateSignature, docLayoutTemplate);
- }
+ createCustomView(doc, creator, templateSignature, docLayoutTemplate);
});
batch.end();
return doc;
@@ -1319,7 +1322,9 @@ export namespace DocUtils {
const options = { title: "data", backgroundColor: StrCast(doc.backgroundColor), _autoHeight: true, _width, x: -_width / 2, y: - _height / 2, _showSidebar: false };
if (docLayoutTemplate) {
- Doc.ApplyTemplateTo(docLayoutTemplate, doc, customName, undefined);
+ if (docLayoutTemplate !== doc[customName]) {
+ Doc.ApplyTemplateTo(docLayoutTemplate, doc, customName, undefined);
+ }
} else {
let fieldTemplate: Opt<Doc>;
if (doc.data instanceof RichTextField || typeof (doc.data) === "string") {
@@ -1480,9 +1485,34 @@ export namespace DocUtils {
}
return generatedDocuments;
}
+
+ // copies the specified drag factory document
+ export function copyDragFactory(dragFactory: Doc) {
+ if (!dragFactory) return undefined;
+ const ndoc = dragFactory.isTemplateDoc ? Doc.ApplyTemplate(dragFactory) : Doc.MakeCopy(dragFactory, true);
+ ndoc && Doc.AddDocToList(CurrentUserUtils.MyFileOrphans, "data", Doc.GetProto(ndoc));
+ if (ndoc && dragFactory["dragFactory-count"] !== undefined) {
+ dragFactory["dragFactory-count"] = NumCast(dragFactory["dragFactory-count"]) + 1;
+ Doc.SetInPlace(ndoc, "title", ndoc.title + " " + NumCast(dragFactory["dragFactory-count"]).toString(), true);
+ }
+
+ if (ndoc && CurrentUserUtils.ActiveDashboard) inheritParentAcls(CurrentUserUtils.ActiveDashboard, ndoc);
+
+ return ndoc;
+ }
+ export function delegateDragFactory(dragFactory: Doc) {
+ const ndoc = Doc.MakeDelegateWithProto(dragFactory);
+ if (ndoc && dragFactory["dragFactory-count"] !== undefined) {
+ dragFactory["dragFactory-count"] = NumCast(dragFactory["dragFactory-count"]) + 1;
+ Doc.GetProto(ndoc).title = ndoc.title + " " + NumCast(dragFactory["dragFactory-count"]).toString();
+ }
+ return ndoc;
+ }
}
ScriptingGlobals.add("Docs", Docs);
+ScriptingGlobals.add(function copyDragFactory(dragFactory: Doc) { return DocUtils.copyDragFactory(dragFactory); });
+ScriptingGlobals.add(function delegateDragFactory(dragFactory: Doc) { return DocUtils.delegateDragFactory(dragFactory); });
ScriptingGlobals.add(function makeDelegate(proto: any) { const d = Docs.Create.DelegateDocument(proto, { title: "child of " + proto.title }); return d; });
ScriptingGlobals.add(function generateLinkTitle(self: Doc) {
const anchor1title = self.anchor1 && self.anchor1 !== self ? Cast(self.anchor1, Doc, null).title : "<?>";
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 28d20c56b..dca77250c 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -9,29 +9,25 @@ import { RichTextField } from "../../fields/RichTextField";
import { listSpec } from "../../fields/Schema";
import { ComputedField, ScriptField } from "../../fields/ScriptField";
import { Cast, DateCast, DocCast, FieldValue, NumCast, PromiseValue, ScriptCast, StrCast } from "../../fields/Types";
-import { ImageField, nullAudio } from "../../fields/URLField";
+import { nullAudio } from "../../fields/URLField";
import { SharingPermissions } from "../../fields/util";
import { OmitKeys, Utils } from "../../Utils";
import { DocServer } from "../DocServer";
import { Docs, DocumentOptions, DocUtils, FInfo } from "../documents/Documents";
import { DocumentType } from "../documents/DocumentTypes";
import { CollectionDockingView } from "../views/collections/CollectionDockingView";
-import { CollectionFreeFormView } from "../views/collections/collectionFreeForm";
import { TreeViewType } from "../views/collections/CollectionTreeView";
import { CollectionView, CollectionViewType } from "../views/collections/CollectionView";
import { TreeView } from "../views/collections/TreeView";
import { Colors } from "../views/global/globalEnums";
import { MainView } from "../views/MainView";
import { ButtonType, NumButtonType } from "../views/nodes/button/FontIconBox";
-import { CollectionFreeFormDocumentView } from "../views/nodes/CollectionFreeFormDocumentView";
import { OverlayView } from "../views/OverlayView";
-import { DocumentManager } from "./DocumentManager";
import { DragManager } from "./DragManager";
-import { makeTemplate, MakeTemplate } from "./DropConverter";
+import { MakeTemplate } from "./DropConverter";
import { HistoryUtil } from "./History";
import { LinkManager } from "./LinkManager";
import { ScriptingGlobals } from "./ScriptingGlobals";
-import { SearchUtil } from "./SearchUtil";
import { SelectionManager } from "./SelectionManager";
import { ColorScheme } from "./SettingsManager";
import { SharingManager } from "./SharingManager";
@@ -148,11 +144,7 @@ export class CurrentUserUtils {
}
return templateBtn;
};
- const makeTemp = (doc:Doc) => {
- doc.isTemplateDoc = makeTemplate(doc);
- return doc;
- }
- return this.AssignScripts(assignBtnAndTempOpts(tempBtn, btnOpts, templateOpts) ?? this.createToolButton( {...btnOpts, dragFactory: makeTemp(template(templateOpts))}), reqdScripts);
+ return this.AssignScripts(assignBtnAndTempOpts(tempBtn, btnOpts, templateOpts) ?? this.createToolButton( {...btnOpts, dragFactory: MakeTemplate(template(templateOpts))}), reqdScripts);
});
const reqdOpts:DocumentOptions = {
@@ -175,11 +167,7 @@ export class CurrentUserUtils {
const reqdNoteList = reqdTempOpts.map(opts => {
const reqdOpts = {...opts, title: "text", system: true};
const noteType = tempNotes ? DocListCast(tempNotes.data).find(doc => doc.noteType === opts.noteType): undefined;
- const makeTemp = (doc:Doc, noteType?:string) => {
- doc.isTemplateDoc = makeTemplate(doc, true, noteType??"Note");
- return doc;
- }
- return this.AssignOpts(noteType, reqdOpts) ?? makeTemp(Docs.Create.TextDocument("",reqdOpts), opts.noteType);
+ return this.AssignOpts(noteType, reqdOpts) ?? MakeTemplate(Docs.Create.TextDocument("",reqdOpts), true, opts.noteType??"Note");
});
const reqdOpts:DocumentOptions = { title: "Note Layouts", _height: 75, system: true };
@@ -204,29 +192,37 @@ export class CurrentUserUtils {
const reqdOpts = { title: "icon templates", _height: 75, system: true };
const templateIconsDoc = this.AssignOpts(DocCast(doc[field]), reqdOpts) ?? (doc[field] = Docs.Create.TreeDocument([], reqdOpts));
- const makeIconTemplate = (type: DocumentType | undefined, templateField: string, iconTemplate: (opts:DocumentOptions) => Doc) => {
+ const makeIconTemplate = (type: DocumentType | undefined, templateField: string, opts:DocumentOptions) => {
const iconFieldName = "icon" + (type ? "_" + type : "");
- return DocCast(templateIconsDoc[iconFieldName] ?? (templateIconsDoc[iconFieldName] = MakeTemplate(iconTemplate({onClick:deiconifyScript(), system: true}), true, iconFieldName, templateField))) ;
+ const curIcon = DocCast(templateIconsDoc[iconFieldName]);
+ let creator = labelBox;
+ switch (opts.iconTemplate) {
+ case DocumentType.IMG : creator = imageBox; break;
+ case DocumentType.FONTICON: creator = fontBox; break;
+ }
+ const allopts = {system: true, ...opts};
+ return this.AssignScripts( (curIcon?.iconTemplate === opts.iconTemplate ?
+ this.AssignOpts(curIcon, allopts):undefined) ?? ((templateIconsDoc[iconFieldName] = MakeTemplate(creator(allopts), true, iconFieldName, templateField))),
+ {onClick:"deiconifyView(documentView)"});
};
- const deiconifyScript = () => ScriptField.MakeScript("deiconifyView(documentView)", { documentView: "any" });
- const labelBox = (opts: object) => Docs.Create.LabelDocument({
+ const labelBox = (opts: DocumentOptions, data?:string) => Docs.Create.LabelDocument({
textTransform: "unset", letterSpacing: "unset", _singleLine: false, _minFontSize: 14, _maxFontSize: 24, borderRounding: "5px", _width: 150, _height: 70, _xPadding: 10, _yPadding: 10, ...opts
});
- const imageBox = (url: Opt<string>, opts: object) => Docs.Create.ImageDocument(url ?? "http://www.cs.brown.edu/~bcz/noImage.png", { "icon-nativeWidth": 360 / 4, "icon-nativeHeight": 270 / 4, _width: 360 / 4, _height: 270 / 4, _showTitle: "title", ...opts });
- const fontBox = (opts:DocumentOptions) => Docs.Create.FontIconDocument({ _nativeHeight: 30, _nativeWidth: 30, _width: 30, _height: 30, ...opts });
+ const imageBox = (opts: DocumentOptions, url?:string) => Docs.Create.ImageDocument(url ?? "http://www.cs.brown.edu/~bcz/noImage.png", { "icon-nativeWidth": 360 / 4, "icon-nativeHeight": 270 / 4, iconTemplate:DocumentType.IMG, _width: 360 / 4, _height: 270 / 4, _showTitle: "title", ...opts });
+ const fontBox = (opts:DocumentOptions, data?:string) => Docs.Create.FontIconDocument({ _nativeHeight: 30, _nativeWidth: 30, _width: 30, _height: 30, ...opts });
const iconTemplates = [
- makeIconTemplate(undefined, "title", (opts) => labelBox({ ...opts, _backgroundColor: "dimgray" })),
- makeIconTemplate(DocumentType.AUDIO, "title", (opts) => labelBox({ ...opts, _backgroundColor: "lightgreen" })),
- makeIconTemplate(DocumentType.PDF, "title", (opts) => labelBox({ ...opts, _backgroundColor: "pink" })),
- makeIconTemplate(DocumentType.WEB, "title", (opts) => labelBox({...opts, _backgroundColor: "brown" })),
- makeIconTemplate(DocumentType.RTF, "text", (opts) => labelBox({ ...opts, _showTitle: "creationDate" })),
- makeIconTemplate(DocumentType.IMG, "data", (opts) => imageBox("", { ...opts, _height: undefined, })),
- makeIconTemplate(DocumentType.COL, "icon", (opts) => imageBox(undefined, opts)),
- makeIconTemplate(DocumentType.VID, "icon", (opts) => imageBox(undefined, opts)),
- makeIconTemplate(DocumentType.BUTTON, "data", (opts) => fontBox(opts)),
+ makeIconTemplate(undefined, "title", { iconTemplate:DocumentType.LABEL, backgroundColor: "dimgray"}),
+ makeIconTemplate(DocumentType.AUDIO, "title", { iconTemplate:DocumentType.LABEL, backgroundColor: "lightgreen"}),
+ makeIconTemplate(DocumentType.PDF, "title", { iconTemplate:DocumentType.LABEL, backgroundColor: "pink"}),
+ makeIconTemplate(DocumentType.WEB, "title", { iconTemplate:DocumentType.LABEL, backgroundColor: "brown"}),
+ makeIconTemplate(DocumentType.RTF, "text", { iconTemplate:DocumentType.LABEL, _showTitle: "creationDate"}),
+ makeIconTemplate(DocumentType.IMG, "data", { iconTemplate:DocumentType.IMG, _height: undefined}),
+ makeIconTemplate(DocumentType.COL, "icon", { iconTemplate:DocumentType.IMG}),
+ makeIconTemplate(DocumentType.VID, "icon", { iconTemplate:DocumentType.IMG}),
+ makeIconTemplate(DocumentType.BUTTON,"data", { iconTemplate:DocumentType.FONTICON}),
//nasty hack .. templates are looked up exclusively by type -- but we want a template for a document with a certain field (transcription) .. so this hack and the companion hack in createCustomView does this for now
- makeIconTemplate("transcription" as any, "transcription", (opts) => labelBox({ ...opts, _backgroundColor: "orange" })),
- // makeIconTemplate(DocumentType.PDF, "icon", () => imageBox("http://www.cs.brown.edu/~bcz/noImage.png", {}))
+ makeIconTemplate("transcription" as any, "transcription", { iconTemplate:DocumentType.LABEL, backgroundColor: "orange" }),
+ //makeIconTemplate(DocumentType.PDF, "icon", {iconTemplate:DocumentType.IMG}, (opts) => imageBox("http://www.cs.brown.edu/~bcz/noImage.png", opts))
].filter(d => d).map(d => d!);
this.AssignOpts(DocCast(doc[field]), {}, iconTemplates);
}
@@ -271,8 +267,7 @@ export class CurrentUserUtils {
// " <FormattedTextBox {...props} fieldKey={'header'} dontSelectOnLoad={'true'} ignoreAutoHeight={'true'} pointerEvents='{this._headerPointerEvents||`none`}' fontSize='{this._headerFontSize}px' height='{this._headerHeight}px' background='{this._headerColor||this.target.mySharedDocs.userColor}' />" +
// " <FormattedTextBox {...props} fieldKey={'text'} position='absolute' top='{(this._headerHeight)*scale}px' height='calc({100/scale}% - {this._headerHeight}px)'/>" +
// "</div>";
- Doc.GetProto(header).isTemplateDoc = makeTemplate(Doc.GetProto(header), true, "headerView");
- Doc.GetProto(header).title = "Untitled Header";
+ MakeTemplate(Doc.GetProto(header), true, "Untitled Header");
return header;
}
const emptyThings:{key:string, // the field name where the empty thing will be stored
@@ -291,7 +286,7 @@ export class CurrentUserUtils {
{key: "WebCam", creator: opts => Docs.Create.WebCamDocument("", opts), opts: { _width: 400, _height: 200, recording:true, system: true, cloneFieldFilter: new List<string>(["system"]) }},
{key: "Button", creator: Docs.Create.ButtonDocument, opts: { _width: 150, _height: 50, _xPadding: 10, _yPadding: 10, }},
{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: "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: "Tab", creator: opts => Docs.Create.FreeformDocument([], opts), opts: { _width: 500, _height: 800, _backgroundGridShow: true, }},
@@ -696,7 +691,7 @@ export class CurrentUserUtils {
{ title: "Color", toolTip: "Font color", btnType: ButtonType.ColorButton, icon: "font", ignoreClick: true, scripts: {script: '{ return setFontColor(value, _readOnly_); }'}},
{ title: "Bold", toolTip: "Bold (Ctrl+B)", btnType: ButtonType.ToggleButton, icon: "bold", scripts: {onClick: '{ return toggleBold(_readOnly_); }'} },
{ title: "Italic", toolTip: "Italic (Ctrl+I)", btnType: ButtonType.ToggleButton, icon: "italic", scripts: {onClick: '{ return toggleItalic(_readOnly_);}'} },
- { title: "Under", toolTip: "Underline (Ctrl+U)", btnType: ButtonType.ToggleButton, icon: "underline", scripts: {onClick:'{ return toggleUnderline(_readOnly_);}'} },
+ { title: "Under", toolTip: "Underline (Ctrl+U)", btnType: ButtonType.ToggleButton, icon: "underline", scripts: {onClick: '{ return toggleUnderline(_readOnly_);}'} },
{ title: "Bullets", toolTip: "Bullet List", btnType: ButtonType.ToggleButton, icon: "list", scripts: {onClick: '{ return setBulletList("bullet", _readOnly_);}'} },
{ title: "#", toolTip: "Number List", btnType: ButtonType.ToggleButton, icon: "list-ol", scripts: {onClick: '{ return setBulletList("decimal", _readOnly_);}'} },
@@ -746,8 +741,8 @@ export class CurrentUserUtils {
CollectionViewType.Carousel3D, CollectionViewType.Linear, CollectionViewType.Map,
CollectionViewType.Grid]),
title: "Perspective", toolTip: "View", width: 100,btnType: ButtonType.DropdownList,ignoreClick: true, scripts: { script: 'setView(value, _readOnly_)'}},
- { title: "Back", icon: "chevron-left", toolTip: "Prev Animation Frame", width: 20, btnType: ButtonType.ClickButton, scripts: { onClick: 'prevKeyFrame(_readOnly_)'}, funcs: {hidden: 'IsNoviceMode()'}},
- { title: "Fwd", icon: "chevron-right", toolTip: "Next Animation Frame", width: 20, btnType: ButtonType.ClickButton, scripts: { onClick: 'nextKeyFrame(_readOnly_)'}, funcs: {hidden: 'IsNoviceMode()'}},
+ { title: "Back", icon: "chevron-left", toolTip: "Prev Animation Frame", width: 20, btnType: ButtonType.ClickButton, scripts: { onClick: 'prevKeyFrame(_readOnly_)'}, funcs: {hidden: 'IsNoviceMode() || !selectedDocumentType(undefined, "freeform")'}},
+ { title: "Fwd", icon: "chevron-right", toolTip: "Next Animation Frame", width: 20, btnType: ButtonType.ClickButton, scripts: { onClick: 'nextKeyFrame(_readOnly_)'}, funcs: {hidden: 'IsNoviceMode() || !selectedDocumentType(undefined, "freeform")'}},
{ title: "Fill", icon: "fill-drip", toolTip: "Background Fill Color",width: 20, btnType: ButtonType.ColorButton, ignoreClick: true, scripts: { script: 'setBackgroundColor(value, _readOnly_)'},funcs: {hidden: '!selectedDocumentType()'}}, // Only when a document is selected
{ title: "Header", icon: "heading", toolTip: "Header Color", btnType: ButtonType.ColorButton, ignoreClick: true, scripts: { script: 'setHeaderColor(value, _readOnly_)'}, funcs: {hidden: '!selectedDocumentType()'}},
{ title: "Overlay", icon: "layer-group", toolTip: "Overlay", btnType: ButtonType.ToggleButton, scripts: { onClick: 'toggleOverlay(_readOnly_)'}, funcs: {hidden: '!selectedDocumentType(undefined, "freeform", true)'}}, // Only when floating document is selected in freeform
@@ -778,23 +773,24 @@ export class CurrentUserUtils {
/// Initializes all the default buttons for the top bar context menu
static setupContextMenuButtons(doc: Doc, field="myContextMenuBtns") {
- const ctxtMenuBtnsDoc = DocCast(doc[field]);
+ const reqdCtxtOpts = { title: "context menu buttons", flexGap: 0, childDontRegisterViews: true, linearViewIsExpanded: true, ignoreClick: true, linearViewExpandable: false, _height: 35 };
+ const ctxtMenuBtnsDoc = this.AssignDocField(doc, field, (opts, items) => this.linearButtonList(opts, items??[]), reqdCtxtOpts, undefined);
const ctxtMenuBtns = CurrentUserUtils.contextMenuTools().map(params => {
const menuBtnDoc = DocListCast(ctxtMenuBtnsDoc?.data).find(doc => doc.title === params.title);
if (!params.subMenu) {
return this.setupContextMenuButton(params, menuBtnDoc);
} else {
- const reqdSubMenuOpts = { ...OmitKeys(params, ["scripts", "funcs", "subMenu"]).omit, title:"submenu",
+ const reqdSubMenuOpts = { ...OmitKeys(params, ["scripts", "funcs", "subMenu"]).omit,
childDontRegisterViews: true, flexGap: 0, _height: 30, ignoreClick: true,
linearViewSubMenu: true, linearViewExpandable: true, };
- return this.AssignScripts(this.AssignOpts(menuBtnDoc, reqdSubMenuOpts) ??
- this.linearButtonList(reqdSubMenuOpts, params.subMenu.map(sub =>
- this.setupContextMenuButton(sub, DocListCast(menuBtnDoc?.data).find(doc => doc.title === sub.title))
- )), undefined, params.funcs);
+ const items = params.subMenu?.map(sub =>
+ this.setupContextMenuButton(sub, DocListCast(menuBtnDoc?.data).find(doc => doc.title === sub.title))
+ );
+ return this.AssignScripts(
+ this.AssignDocField(ctxtMenuBtnsDoc, StrCast(params.title), (opts) => this.linearButtonList(opts, items??[]), reqdSubMenuOpts, items), undefined, params.funcs);
}
});
- const reqdCtxtOpts = { title: "context menu buttons", flexGap: 0, childDontRegisterViews: true, linearViewIsExpanded: true, ignoreClick: true, linearViewExpandable: false, _height: 35 };
- return this.AssignDocField(doc, field, (opts, items) => this.linearButtonList(opts, items??[]), reqdCtxtOpts, ctxtMenuBtns);
+ return this.AssignOpts(ctxtMenuBtnsDoc, reqdCtxtOpts, ctxtMenuBtns);
}
/// collection of documents rendered in the overlay layer above all tabs and other UI
@@ -807,16 +803,13 @@ export class CurrentUserUtils {
}
/// The database of all links on all documents
- static async setupLinkDocs(doc: Doc, linkDatabaseId: string) {
- if (doc.myLinkDatabase === undefined) {
- let linkDocs = Docs.newAccount ? undefined : await DocServer.GetRefField(linkDatabaseId);
- if (!linkDocs) {
- linkDocs = new Doc(linkDatabaseId, true);
- (linkDocs as Doc).title = "LINK DATABASE: " + Doc.CurrentUserEmail;
- (linkDocs as Doc).author = Doc.CurrentUserEmail;
- (linkDocs as Doc).data = new List<Doc>([]);
- (linkDocs as Doc)["acl-Public"] = SharingPermissions.Augment;
- }
+ static setupLinkDocs(doc: Doc, linkDatabaseId: string) {
+ if (!(Docs.newAccount ? undefined : DocCast(doc.myLinkDatabase))) {
+ const linkDocs = new Doc(linkDatabaseId, true);
+ linkDocs.title = "LINK DATABASE: " + Doc.CurrentUserEmail;
+ linkDocs.author = Doc.CurrentUserEmail;
+ linkDocs.data = new List<Doc>([]);
+ linkDocs["acl-Public"] = SharingPermissions.Augment;
doc.myLinkDatabase = new PrefetchProxy(linkDocs);
}
}
@@ -825,14 +818,12 @@ export class CurrentUserUtils {
// A user's sharing document is where all documents that are shared to that user are placed.
// When the user views one of these documents, it will be added to the sharing documents 'viewed' list field
// The sharing document also stores the user's color value which helps distinguish shared documents from personal documents
- static async setupSharedDocs(doc: Doc, sharingDocumentId: string) {
+ static setupSharedDocs(doc: Doc, sharingDocumentId: string) {
const addToDashboards = ScriptField.MakeScript(`addToDashboards(self)`);
const dashboardFilter = ScriptField.MakeFunction(`doc._viewType === '${CollectionViewType.Docking}'`, { doc: Doc.name });
const dblClkScript = "{scriptContext.openLevel(documentView); addDocToList(scriptContext.props.treeView.props.Document, 'viewed', documentView.rootDoc);}";
- const sharedScripts = {
- treeViewChildDoubleClick: dblClkScript,
- }
+ const sharedScripts = { treeViewChildDoubleClick: dblClkScript, }
const sharedDocOpts:DocumentOptions = {
title: "My Shared Docs",
userColor: "rgb(202, 202, 202)",
@@ -847,8 +838,7 @@ export class CurrentUserUtils {
explainer: "This is where documents or dashboards that other users have shared with you will appear. To share a document or dashboard right click and select 'Share'"
};
- await DocServer.GetRefField(sharingDocumentId + "outer");
- return this.AssignDocField(doc, "mySharedDocs", (opts) => Docs.Create.TreeDocument([], opts, sharingDocumentId + "outer", sharingDocumentId), sharedDocOpts, undefined, sharedScripts);
+ this.AssignDocField(doc, "mySharedDocs", opts => Docs.Create.TreeDocument([], opts, sharingDocumentId + "layout", sharingDocumentId), sharedDocOpts, undefined, sharedScripts);
}
/// Import option on the left side button panel
@@ -864,7 +854,8 @@ export class CurrentUserUtils {
const reqdBtnOpts:DocumentOptions = { _forceActive: true, toolTip: "Import from computer",
_width: 30, _height: 30, _stayInCollection: true, _hideContextMenu: true, title: "Import", btnType: ButtonType.ClickButton,
buttonText: "Import", icon: "upload", system: true };
- return this.AssignDocField(myImports, "buttonMenuDoc", (opts) => Docs.Create.FontIconDocument(opts), reqdBtnOpts, undefined, { onClick: "importDocument()" });
+ this.AssignDocField(myImports, "buttonMenuDoc", (opts) => Docs.Create.FontIconDocument(opts), reqdBtnOpts, undefined, { onClick: "importDocument()" });
+ return myImports;
}
static setupClickEditorTemplates(doc: Doc) {
@@ -914,16 +905,18 @@ export class CurrentUserUtils {
return doc.clickFuncs as Doc;
}
- static async updateUserDocument(doc: Doc, sharingDocumentId: string, linkDatabaseId: string) {
+
+ /// Updates the UserDoc to have all required fields, docs, etc. No changes should need to be
+ /// written to the server if the code hasn't changed. However, choices need to be made for each Doc/field
+ /// whether to revert to "default" values, or to leave them as the user/system last set them.
+ static updateUserDocument(doc: Doc, sharingDocumentId: string, linkDatabaseId: string) {
this.AssignDocField(doc, "globalGroupDatabase", () => Docs.Prototypes.MainGroupDocument(), {});
- await DocListCastAsync(DocCast(doc.globalGroupDatabase).data);
reaction(() => DateCast(DocCast(doc.globalGroupDatabase)["data-lastModified"]),
async () => {
const groups = await DocListCastAsync(DocCast(doc.globalGroupDatabase).data);
const mygroups = groups?.filter(group => JSON.parse(StrCast(group.members)).includes(Doc.CurrentUserEmail)) || [];
SnappingManager.SetCachedGroups(["Public", ...mygroups?.map(g => StrCast(g.title))]);
}, { fireImmediately: true });
- // Document properties on load
doc.system ?? (doc.system = true);
doc.title ?? (doc.title = Doc.CurrentUserEmail);
Doc.noviceMode ?? (Doc.noviceMode = true);
@@ -945,8 +938,8 @@ export class CurrentUserUtils {
doc.savedFilters ?? (doc.savedFilters = new List<Doc>());
doc.filterDocCount = 0;
doc.freezeChildren = "remove|add";
- await this.setupLinkDocs(doc, linkDatabaseId);
- await this.setupSharedDocs(doc, sharingDocumentId); // sets up the right sidebar collection for mobile upload documents and sharing
+ this.setupLinkDocs(doc, linkDatabaseId);
+ this.setupSharedDocs(doc, sharingDocumentId); // sets up the right sidebar collection for mobile upload documents and sharing
this.setupDefaultIconTemplates(doc); // creates a set of icon templates triggered by the document deoration icon
this.setupActiveMobileMenu(doc); // sets up the current mobile menu for Dash Mobile
this.setupOverlays(doc); // sets up the overlay panel where documents and other widgets can be added to float over the rest of the dashboard
@@ -959,11 +952,13 @@ export class CurrentUserUtils {
this.AssignDocField(doc, "globalScriptDatabase", (opts) => Docs.Prototypes.MainScriptDocument(), {});
this.AssignDocField(doc, "myHeaderBar", (opts) => Docs.Create.MulticolumnDocument([], opts), { title: "header bar", system: true }); // drop down panel at top of dashboard for stashing documents
- setTimeout(() => DocServer.UPDATE_SERVER_CACHE(), 2500);
if (doc.activeDashboard instanceof Doc) {
// undefined means ColorScheme.Light until all CSS is updated with values for each color scheme (e.g., see MainView.scss, DocumentDecorations.scss)
doc.activeDashboard.colorScheme = doc.activeDashboard.colorScheme === ColorScheme.Light ? undefined : doc.activeDashboard.colorScheme;
}
+ new LinkManager();
+
+ DocServer.UPDATE_SERVER_CACHE();
return doc;
}
static setupFieldInfos(doc:Doc, field="fieldInfos") {
@@ -977,7 +972,7 @@ export class CurrentUserUtils {
switch (options.fieldType) {
case "boolean": opts.fieldValues = new List<boolean>(options.values as any); break;
case "number": opts.fieldValues = new List<number>(options.values as any); break;
- case "Doc": opts.fieldValues = new List<Doc>(options.values as any); break;
+ case Doc.name: opts.fieldValues = new List<Doc>(options.values as any); break;
default: opts.fieldValues = new List<string>(options.values as any); break;// string, pointerEvents, dimUnit, dropActionType
}
this.AssignDocField(infos, pair[0], opts => Doc.assign(new Doc(), OmitKeys(opts,["values"]).omit), opts);
@@ -1010,12 +1005,7 @@ export class CurrentUserUtils {
await Docs.Prototypes.initialize();
const userDoc = Docs.newAccount ? new Doc(userDocumentId, true) : field as Doc;
Docs.newAccount &&(userDoc.activePage = "home");
- const updated = this.updateUserDocument(Doc.SetUserDoc(userDoc), sharingDocumentId, linkDatabaseId);
- (await DocListCastAsync(Doc.LinkDBDoc()?.data))?.forEach(async link => { // make sure anchors are loaded to avoid incremental updates to computedFn's in LinkManager
- const a1 = await Cast(link?.anchor1, Doc, null);
- const a2 = await Cast(link?.anchor2, Doc, null);
- });
- return updated;
+ return this.updateUserDocument(Doc.SetUserDoc(userDoc), sharingDocumentId, linkDatabaseId);
});
} else {
throw new Error("There should be a user id! Why does Dash think there isn't one?");
@@ -1025,7 +1015,10 @@ export class CurrentUserUtils {
public static _urlState: HistoryUtil.DocUrl;
- public static openDashboard = (doc: Doc, fromHistory = false) => {
+ /// opens a dashboard as the ActiveDashboard (and adds the dashboard to the users list of dashboards if it's not already there).
+ /// this also sets the readonly state of the dashboard based on the current mode of dash (from its url)
+ public static openDashboard = (doc: Doc|undefined, fromHistory = false) => {
+ if (!doc) return false;
CurrentUserUtils.MainDocId = doc[Id];
Doc.AddDocToList(CurrentUserUtils.MyDashboards, "data", doc);
@@ -1069,72 +1062,44 @@ export class CurrentUserUtils {
input.onchange = async _e => {
const upload = Utils.prepend("/uploadDoc");
const formData = new FormData();
- const file = input.files && input.files[0];
+ const file = input.files?.[0];
if (file?.type === 'application/zip') {
- formData.append('file', file);
- formData.append('remap', "true");
- const response = await fetch(upload, { method: "POST", body: formData });
- const json = await response.json();
- if (json !== "error") {
- const doc = Docs.newAccount ? undefined : await DocServer.GetRefField(json);
- if (doc instanceof Doc) {
- setTimeout(() => SearchUtil.Search(`{!join from=id to=proto_i}id:link*`, true, {}).then(docs =>
- docs.docs.forEach(d => LinkManager.Instance.addLink(d))), 2000); // need to give solr some time to update so that this query will find any link docs we've added.
- }
- }
+ const doc = await Doc.importDocument(file);
+ // NOT USING SOLR, so need to replace this with something else // if (doc instanceof Doc) {
+ // setTimeout(() => SearchUtil.Search(`{!join from=id to=proto_i}id:link*`, true, {}).then(docs =>
+ // docs.docs.forEach(d => LinkManager.Instance.addLink(d))), 2000); // need to give solr some time to update so that this query will find any link docs we've added.
+ // }
+ const list = Cast(CurrentUserUtils.MyImports.data, listSpec(Doc), null);
+ doc instanceof Doc && list?.splice(0, 0, doc);
} else if (input.files && input.files.length !== 0) {
const disposer = OverlayView.ShowSpinner();
- DocListCastAsync(CurrentUserUtils.MyImports.data).then(async list => {
- const results = await DocUtils.uploadFilesToDocs(Array.from(input.files || []), {});
- if (results.length !== input.files?.length) {
- alert("Error uploading files - possibly due to unsupported file types");
- }
- list?.splice(0, 0, ...results);
- disposer();
- });
+ const results = await DocUtils.uploadFilesToDocs(Array.from(input.files || []), {});
+ if (results.length !== input.files?.length) {
+ alert("Error uploading files - possibly due to unsupported file types");
+ }
+ const list = Cast(CurrentUserUtils.MyImports.data, listSpec(Doc), null);
+ list?.splice(0, 0, ...results);
+ disposer();
} else {
console.log("No file selected");
}
};
input.click();
}
+
+ public static snapshotDashboard() { return CollectionDockingView.TakeSnapshot(CurrentUserUtils.ActiveDashboard); }
- public static CaptureDashboardThumbnail() {
- const activeDashboard = CurrentUserUtils.ActiveDashboard;
- const docView = CollectionDockingView.Instance.props.DocumentView?.();
- const content = docView?.ContentDiv;
- if (docView && content && activeDashboard) {
- const _width = Number(getComputedStyle(content).width.replace("px",""));
- const _height = Number(getComputedStyle(content).height.replace("px",""));
- return CollectionFreeFormView.UpdateIcon(
- docView.layoutDoc[Id] + "-icon" + (new Date()).getTime(),
- content,
- _width, _height,
- _width, _height, 0, 1, true, docView.layoutDoc[Id] + "-icon",
- (iconFile, _nativeWidth, _nativeHeight) => {
- const img = Docs.Create.ImageDocument(new ImageField(iconFile), { title: docView.rootDoc.title+"-icon", _width, _height, _nativeWidth, _nativeHeight});
- const proto = Cast(img.proto, Doc, null)!;
- proto["data-nativeWidth"] = _width;
- proto["data-nativeHeight"] = _height;
- Doc.GetProto(activeDashboard).thumb = img;
- });
- }
-
- }
+ public static closeActiveDashboard = () => { CurrentUserUtils.ActiveDashboard = undefined; }
- public static async snapshotDashboard() {
- if (CurrentUserUtils.ActiveDashboard) {
- const copy = await CollectionDockingView.Copy(CurrentUserUtils.ActiveDashboard);
- Doc.AddDocToList(CurrentUserUtils.MyDashboards, "data", copy);
- CurrentUserUtils.openDashboard(copy);
+ public static removeDashboard = async (dashboard:Doc) => {
+ const dashboards = await DocListCastAsync(CurrentUserUtils.MyDashboards.data);
+ if (dashboards?.length) {
+ if (dashboard === CurrentUserUtils.ActiveDashboard) CurrentUserUtils.openDashboard(dashboards.find(doc => doc !== dashboard));
+ Doc.RemoveDocFromList(CurrentUserUtils.MyDashboards, "data", dashboard);
+ if (!dashboards.length) CurrentUserUtils.ActivePage = "home";
}
}
-
- public static closeActiveDashboard = () => {
- CurrentUserUtils.ActiveDashboard = undefined;
- }
-
- public static createNewDashboard = (id?: string) => {
+ public static createNewDashboard = (id?: string, name?: string) => {
const presentation = Doc.MakeCopy(Doc.UserDoc().emptyPresentation as Doc, true);
const dashboards = CurrentUserUtils.MyDashboards;
const dashboardCount = DocListCast(dashboards.data).length + 1;
@@ -1147,8 +1112,9 @@ export class CurrentUserUtils {
_backgroundGridShow: true,
title: `Untitled Tab 1`,
};
+ const title = name ? name : `Dashboard ${dashboardCount}`
const freeformDoc = CurrentUserUtils.GuestTarget || Docs.Create.FreeformDocument([], freeformOptions);
- const dashboardDoc = Docs.Create.StandardCollectionDockingDocument([{ doc: freeformDoc, initialWidth: 600 }], { title: `Dashboard ${dashboardCount}` }, id, "row");
+ const dashboardDoc = Docs.Create.StandardCollectionDockingDocument([{ doc: freeformDoc, initialWidth: 600 }], { title: title }, id, "row");
freeformDoc.context = dashboardDoc;
// switching the tabs from the datadoc to the regular doc
@@ -1159,7 +1125,9 @@ export class CurrentUserUtils {
CurrentUserUtils.ActivePresentation = presentation;
Doc.AddDocToList(dashboards, "data", dashboardDoc);
- // CurrentUserUtils.openDashboard(dashboardDoc);
+ // open this new dashboard
+ CurrentUserUtils.ActiveDashboard = dashboardDoc;
+ CurrentUserUtils.ActivePage = "dashboard";
}
public static GetNewTextDoc(title: string, x: number, y: number, width?: number, height?: number, noMargins?: boolean, annotationOn?: Doc, maxHeight?: number, backgroundColor?: string) {
@@ -1204,14 +1172,6 @@ export class CurrentUserUtils {
public static get ActiveTool(): InkTool { return StrCast(Doc.UserDoc().activeTool, InkTool.None) as InkTool; }
}
-ScriptingGlobals.add(function openDragFactory(dragFactory: Doc) {
- const copy = Doc.copyDragFactory(dragFactory);
- if (copy) {
- CollectionDockingView.AddSplit(copy, "right");
- const view = DocumentManager.Instance.getFirstDocumentView(copy);
- view && SelectionManager.SelectView(view, false);
- }
-});
ScriptingGlobals.add(function MySharedDocs() { return CurrentUserUtils.MySharedDocs; }, "document containing all shared Docs");
ScriptingGlobals.add(function IsNoviceMode() { return Doc.noviceMode; }, "is Dash in novice mode");
ScriptingGlobals.add(function toggleComicMode() { Doc.UserDoc().renderStyle = Doc.UserDoc().renderStyle === "comic" ? undefined : "comic"; }, "switches between comic and normal document rendering");
@@ -1222,59 +1182,14 @@ ScriptingGlobals.add(function createNewFolder() { return MainView.Instance.creat
ScriptingGlobals.add(function links(doc: any) { return new List(LinkManager.Instance.getAllRelatedLinks(doc)); }, "returns all the links to the document or its annotations", "(doc: any)");
ScriptingGlobals.add(function importDocument() { return CurrentUserUtils.importDocument(); }, "imports files from device directly into the import sidebar");
ScriptingGlobals.add(function shareDashboard(dashboard: Doc) { SharingManager.Instance.open(undefined, dashboard); }, "opens sharing dialog for Dashboard");
-ScriptingGlobals.add(async function removeDashboard(dashboard: Doc) {
- const dashboards = await DocListCastAsync(CurrentUserUtils.MyDashboards.data);
- if (dashboards && dashboards.length > 1) {
- if (dashboard === CurrentUserUtils.ActiveDashboard) CurrentUserUtils.openDashboard(dashboards.find(doc => doc !== dashboard)!);
- Doc.RemoveDocFromList(CurrentUserUtils.MyDashboards, "data", dashboard);
- }
-},
- "Remove Dashboard from Dashboards");
-ScriptingGlobals.add(function addToDashboards(dashboard: Doc) {
- const dashboardAlias = Doc.MakeAlias(dashboard);
- Doc.AddDocToList(CurrentUserUtils.MyDashboards, "data", dashboardAlias);
- CurrentUserUtils.openDashboard(dashboardAlias);
-},
- "adds Dashboard to set of Dashboards");
-
-ScriptingGlobals.add(function selectedDocumentType(docType?: DocumentType, colType?: CollectionViewType, checkParent?: boolean) {
- let selected = SelectionManager.Docs().length ? SelectionManager.Docs()[0] : undefined;
- if (selected && checkParent) {
- const parentDoc: Doc = Cast(selected.context, Doc, null);
- selected = parentDoc;
- }
- if (selected && docType && selected.type === docType) return true;
- else if (selected && colType && selected.viewType === colType) return true;
- else if (selected && !colType && !docType) return true;
- else return false;
+ScriptingGlobals.add(function removeDashboard(dashboard: Doc) { CurrentUserUtils.removeDashboard(dashboard); }, "Remove Dashboard from Dashboards");
+ScriptingGlobals.add(function addToDashboards(dashboard: Doc) { CurrentUserUtils.openDashboard( Doc.MakeAlias(dashboard)); }, "adds Dashboard to set of Dashboards");
+ScriptingGlobals.add(function selectedDocumentType(docType?: DocumentType, colType?: CollectionViewType, checkContext?: boolean) {
+ let selected = (sel => checkContext ? DocCast(sel?.context) : sel)(SelectionManager.SelectedSchemaDoc() ?? SelectionManager.Docs().lastElement());
+ return docType ? selected?.type === docType : colType ? selected?.viewType === colType : true;
});
ScriptingGlobals.add(function makeTopLevelFolder() {
- const folder = Docs.Create.TreeDocument([], { title: "Untitled folder", _stayInCollection: true, isFolder: true });
- TreeView._editTitleOnLoad = { id: folder[Id], parent: undefined };
- return Doc.AddDocToList(CurrentUserUtils.MyFilesystem, "data", folder);
-});
-ScriptingGlobals.add(function nextKeyFrame(readOnly: boolean) {
- if (readOnly) return;
- const sel = SelectionManager.Views()[0];
- const col = (sel.ComponentView as CollectionFreeFormView);
- const currentFrame = Cast(sel.props.Document._currentFrame, "number", null);
- if (currentFrame === undefined) {
- sel.props.Document._currentFrame = 0;
- CollectionFreeFormDocumentView.setupKeyframes(col.childDocs, 0);
- }
- CollectionFreeFormDocumentView.updateKeyframe(col.childDocs, currentFrame || 0);
- sel.rootDoc._currentFrame = Math.max(0, (currentFrame || 0) + 1);
- sel.rootDoc.lastFrame = Math.max(NumCast(sel.rootDoc._currentFrame), NumCast(sel.rootDoc.lastFrame));
-});
-ScriptingGlobals.add(function prevKeyFrame(readOnly: boolean) {
- if (readOnly) return;
- const sel = SelectionManager.Views()[0];
- const col = (sel.ComponentView as CollectionFreeFormView);
- const currentFrame = Cast(sel.props.Document._currentFrame, "number", null);
- if (currentFrame === undefined) {
- sel.props.Document._currentFrame = 0;
- CollectionFreeFormDocumentView.setupKeyframes(col.childDocs, 0);
- }
- CollectionFreeFormDocumentView.gotoKeyframe(col.childDocs.slice());
- sel.rootDoc._currentFrame = Math.max(0, (currentFrame || 0) - 1);
+ TreeView._editTitleOnLoad = { id: Utils.GenerateGuid(), parent: undefined };
+ const opts = { title: "Untitled folder", _stayInCollection: true, isFolder: true };
+ return Doc.AddDocToList(CurrentUserUtils.MyFilesystem, "data", Docs.Create.TreeDocument([], opts, TreeView._editTitleOnLoad.id));
}); \ No newline at end of file
diff --git a/src/client/util/DropConverter.ts b/src/client/util/DropConverter.ts
index 076afd3a0..256ab5c44 100644
--- a/src/client/util/DropConverter.ts
+++ b/src/client/util/DropConverter.ts
@@ -20,7 +20,7 @@ export function MakeTemplate(doc: Doc, first: boolean = true, rename: Opt<string
// 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
// after it has been converted to
-export function makeTemplate(doc: Doc, first: boolean = true, rename: Opt<string> = undefined): boolean {
+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
return true;
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index 2100b1195..d51cd350d 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -1,7 +1,6 @@
-import { validationResult } from "express-validator/check";
import { action, observable, observe } from "mobx";
import { computedFn } from "mobx-utils";
-import { DirectLinksSym, Doc, DocListCast, Field, Opt } from "../../fields/Doc";
+import { DirectLinksSym, Doc, DocListCast, DocListCastAsync, Field, Opt } from "../../fields/Doc";
import { List } from "../../fields/List";
import { ProxyField } from "../../fields/Proxy";
import { BoolCast, Cast, StrCast } from "../../fields/Types";
@@ -35,72 +34,74 @@ export class LinkManager {
constructor() {
LinkManager._instance = this;
this.createLinkrelationshipLists();
- setTimeout(() => {
- LinkManager.userLinkDBs = [];
- const addLinkToDoc = (link: Doc) => {
- const a1Prom = link?.anchor1;
- const a2Prom = link?.anchor2;
- Promise.all([a1Prom, a2Prom]).then((all) => {
- const a1 = all[0];
- const a2 = all[1];
- const a1ProtoProm = (link?.anchor1 as Doc)?.proto;
- const a2ProtoProm = (link?.anchor2 as Doc)?.proto;
- Promise.all([a1ProtoProm, a2ProtoProm]).then(action((all) => {
- if (a1 instanceof Doc && a2 instanceof Doc && ((a1.author !== undefined && a2.author !== undefined) || link.author === Doc.CurrentUserEmail)) {
- Doc.GetProto(a1)[DirectLinksSym].add(link);
- Doc.GetProto(a2)[DirectLinksSym].add(link);
- Doc.GetProto(link)[DirectLinksSym].add(link);
- }
- }));
- });
- };
- const remLinkFromDoc = (link: Doc) => {
- const a1 = link?.anchor1;
- const a2 = link?.anchor2;
- Promise.all([a1, a2]).then(action(() => {
+ LinkManager.userLinkDBs = [];
+ const addLinkToDoc = (link: Doc) => {
+ const a1Prom = link?.anchor1;
+ const a2Prom = link?.anchor2;
+ Promise.all([a1Prom, a2Prom]).then((all) => {
+ const a1 = all[0];
+ const a2 = all[1];
+ const a1ProtoProm = (link?.anchor1 as Doc)?.proto;
+ const a2ProtoProm = (link?.anchor2 as Doc)?.proto;
+ Promise.all([a1ProtoProm, a2ProtoProm]).then(action((all) => {
if (a1 instanceof Doc && a2 instanceof Doc && ((a1.author !== undefined && a2.author !== undefined) || link.author === Doc.CurrentUserEmail)) {
- Doc.GetProto(a1)[DirectLinksSym].delete(link);
- Doc.GetProto(a2)[DirectLinksSym].delete(link);
- Doc.GetProto(link)[DirectLinksSym].delete(link);
+ Doc.GetProto(a1)[DirectLinksSym].add(link);
+ Doc.GetProto(a2)[DirectLinksSym].add(link);
+ Doc.GetProto(link)[DirectLinksSym].add(link);
}
}));
- };
- const watchUserLinkDB = (userLinkDBDoc: Doc) => {
- LinkManager.links.push(...DocListCast(userLinkDBDoc.data));
- const toRealField = (field: Field) => field instanceof ProxyField ? field.value() : field; // see List.ts. data structure is not a simple list of Docs, but a list of ProxyField/Fields
- observe(userLinkDBDoc.data as Doc, change => { // observe pushes/splices on a user link DB 'data' field (should only happen for local changes)
- switch (change.type as any) {
- case "splice":
- (change as any).added.forEach((link: any) => addLinkToDoc(toRealField(link)));
- (change as any).removed.forEach((link: any) => remLinkFromDoc(toRealField(link)));
- break;
- case "update": //let oldValue = change.oldValue;
- }
- }, true);
- observe(userLinkDBDoc, "data", // obsever when a new array of links is assigned as the link DB 'data' field (should happen whenever a remote user adds/removes a link)
- change => {
- switch (change.type as any) {
- case "update":
- Promise.all([...(change.oldValue as any as Doc[] || []), ...(change.newValue as any as Doc[] || [])]).then(doclist => {
- const oldDocs = doclist.slice(0, (change.oldValue as any as Doc[] || []).length);
- const newDocs = doclist.slice((change.oldValue as any as Doc[] || []).length, doclist.length);
-
- const added = newDocs?.filter(link => !(oldDocs || []).includes(link));
- const removed = oldDocs?.filter(link => !(newDocs || []).includes(link));
- added?.forEach((link: any) => addLinkToDoc(toRealField(link)));
- removed?.forEach((link: any) => remLinkFromDoc(toRealField(link)));
- });
- }
- }, true);
- };
- observe(LinkManager.userLinkDBs, change => {
+ });
+ };
+ const remLinkFromDoc = (link: Doc) => {
+ const a1 = link?.anchor1;
+ const a2 = link?.anchor2;
+ Promise.all([a1, a2]).then(action(() => {
+ if (a1 instanceof Doc && a2 instanceof Doc && ((a1.author !== undefined && a2.author !== undefined) || link.author === Doc.CurrentUserEmail)) {
+ Doc.GetProto(a1)[DirectLinksSym].delete(link);
+ Doc.GetProto(a2)[DirectLinksSym].delete(link);
+ Doc.GetProto(link)[DirectLinksSym].delete(link);
+ }
+ }));
+ };
+ const watchUserLinkDB = (userLinkDBDoc: Doc) => {
+ LinkManager.links.push(...DocListCast(userLinkDBDoc.data));
+ const toRealField = (field: Field) => field instanceof ProxyField ? field.value() : field; // see List.ts. data structure is not a simple list of Docs, but a list of ProxyField/Fields
+ observe(userLinkDBDoc.data as Doc, change => { // observe pushes/splices on a user link DB 'data' field (should only happen for local changes)
switch (change.type as any) {
- case "splice": (change as any).added.forEach(watchUserLinkDB); break;
+ case "splice":
+ (change as any).added.forEach((link: any) => addLinkToDoc(toRealField(link)));
+ (change as any).removed.forEach((link: any) => remLinkFromDoc(toRealField(link)));
+ break;
case "update": //let oldValue = change.oldValue;
}
}, true);
- LinkManager.addLinkDB(Doc.LinkDBDoc());
- });
+ observe(userLinkDBDoc, "data", // obsever when a new array of links is assigned as the link DB 'data' field (should happen whenever a remote user adds/removes a link)
+ change => {
+ switch (change.type as any) {
+ case "update":
+ Promise.all([...(change.oldValue as any as Doc[] || []), ...(change.newValue as any as Doc[] || [])]).then(doclist => {
+ const oldDocs = doclist.slice(0, (change.oldValue as any as Doc[] || []).length);
+ const newDocs = doclist.slice((change.oldValue as any as Doc[] || []).length, doclist.length);
+
+ const added = newDocs?.filter(link => !(oldDocs || []).includes(link));
+ const removed = oldDocs?.filter(link => !(newDocs || []).includes(link));
+ added?.forEach((link: any) => addLinkToDoc(toRealField(link)));
+ removed?.forEach((link: any) => remLinkFromDoc(toRealField(link)));
+ });
+ }
+ }, true);
+ };
+ observe(LinkManager.userLinkDBs, change => {
+ switch (change.type as any) {
+ case "splice": (change as any).added.forEach(watchUserLinkDB); break;
+ case "update": //let oldValue = change.oldValue;
+ }
+ }, true);
+ LinkManager.addLinkDB(Doc.LinkDBDoc());
+ DocListCastAsync(Doc.LinkDBDoc()?.data).then(dblist => dblist?.forEach(async link => { // make sure anchors are loaded to avoid incremental updates to computedFn's in LinkManager
+ const a1 = await Cast(link?.anchor1, Doc, null);
+ const a2 = await Cast(link?.anchor2, Doc, null);
+ }));
}
diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx
index 22e33ab1e..080237649 100644
--- a/src/client/util/SettingsManager.tsx
+++ b/src/client/util/SettingsManager.tsx
@@ -67,12 +67,11 @@ export class SettingsManager extends React.Component<{}> {
@undoBatch changeFontSize = action((e: React.ChangeEvent) => Doc.UserDoc().fontSize = (e.currentTarget as any).value);
@undoBatch switchActiveBackgroundColor = action((color: ColorState) => Doc.UserDoc().activeCollectionBackground = String(color.hex));
@undoBatch switchUserColor = action((color: ColorState) => { Doc.SharingDoc().userColor = undefined; Doc.GetProto(Doc.SharingDoc()).userColor = String(color.hex); });
- @undoBatch
- playgroundModeToggle = action(() => {
+ @undoBatch playgroundModeToggle = action(() => {
this.playgroundMode = !this.playgroundMode;
if (this.playgroundMode) {
DocServer.Control.makeReadOnly();
- addStyleSheetRule(SettingsManager._settingsStyle, "lm_header", { background: "pink !important" });
+ addStyleSheetRule(SettingsManager._settingsStyle, "topbar-inner-container", { background: "red !important" });
}
else DocServer.Control.makeEditable();
});
diff --git a/src/client/views/ContextMenu.tsx b/src/client/views/ContextMenu.tsx
index 9034cacb2..cffcd0f17 100644
--- a/src/client/views/ContextMenu.tsx
+++ b/src/client/views/ContextMenu.tsx
@@ -74,7 +74,6 @@ export class ContextMenu extends React.Component {
componentDidMount = () => {
document.addEventListener("pointerdown", this.onPointerDown);
document.addEventListener("pointerup", this.onPointerUp);
-
}
@action
diff --git a/src/client/views/ContextMenuItem.tsx b/src/client/views/ContextMenuItem.tsx
index c136de1c3..30073e21f 100644
--- a/src/client/views/ContextMenuItem.tsx
+++ b/src/client/views/ContextMenuItem.tsx
@@ -1,5 +1,5 @@
import React = require("react");
-import { observable, action } from "mobx";
+import { observable, action, runInAction } from "mobx";
import { observer } from "mobx-react";
import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
@@ -32,7 +32,7 @@ export class ContextMenuItem extends React.Component<ContextMenuProps & { select
componentDidMount() {
this._items.length = 0;
if ((this.props as SubmenuProps)?.subitems) {
- (this.props as SubmenuProps).subitems?.forEach(i => this._items.push(i));
+ (this.props as SubmenuProps).subitems?.forEach(i => runInAction(() => this._items.push(i)));
}
}
diff --git a/src/client/views/DashboardView.scss b/src/client/views/DashboardView.scss
index 67587dd2b..3db23b86f 100644
--- a/src/client/views/DashboardView.scss
+++ b/src/client/views/DashboardView.scss
@@ -2,6 +2,8 @@
padding: 50px;
display: flex;
flex-direction: row;
+ width: 100%;
+ position: absolute;
.left-menu {
display: flex;
@@ -45,4 +47,20 @@
margin: 10px;
font-weight: 500;
}
+
+ img {
+ width: auto;
+ height: 80%;
+ }
+
+ .info {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: center;
+ }
+
+ .more {
+ z-index: 100;
+ }
} \ No newline at end of file
diff --git a/src/client/views/DashboardView.tsx b/src/client/views/DashboardView.tsx
index a126218c4..868d63a90 100644
--- a/src/client/views/DashboardView.tsx
+++ b/src/client/views/DashboardView.tsx
@@ -1,4 +1,4 @@
-import { action, observable } from "mobx";
+import { action, computed, observable } from "mobx";
import { extname } from 'path';
import { observer } from "mobx-react";
import * as React from 'react';
@@ -6,64 +6,159 @@ import { Doc, DocListCast } from "../../fields/Doc";
import { Id } from "../../fields/FieldSymbols";
import { Cast, ImageCast, StrCast } from "../../fields/Types";
import { CurrentUserUtils } from "../util/CurrentUserUtils";
-import { UndoManager } from "../util/UndoManager";
+import { undoBatch, UndoManager } from "../util/UndoManager";
import "./DashboardView.scss"
+import { MainViewModal } from "./MainViewModal";
+import { ContextMenu } from "./ContextMenu";
+import { DocumentManager } from "../util/DocumentManager";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { ContextMenuProps } from "./ContextMenuItem";
+import { simulateMouseClick } from "../../Utils";
+import { SharingManager } from "../util/SharingManager";
+import { CollectionViewType } from "./collections/CollectionView";
enum DashboardGroup {
MyDashboards, SharedDashboards
}
+// DashboardView is the view with the dashboard previews, rendered when the app first loads
+
@observer
export class DashboardView extends React.Component {
//TODO: delete dashboard, share dashboard, etc.
- @observable
- private selectedDashboardGroup = DashboardGroup.MyDashboards;
+ @observable private selectedDashboardGroup = DashboardGroup.MyDashboards;
+
+ @observable private newDashboardName: string | undefined = undefined;
+ @action abortCreateNewDashboard = () => { this.newDashboardName = undefined }
+ @action setNewDashboardName(name: string) { this.newDashboardName = name }
@action
selectDashboardGroup = (group: DashboardGroup) => {
this.selectedDashboardGroup = group
}
- newDashboard = async () => {
- const batch = UndoManager.StartBatch("new dash");
- await CurrentUserUtils.createNewDashboard();
- batch.end();
- }
-
clickDashboard = async (e: React.MouseEvent, dashboard: Doc) => {
if (e.detail === 2) {
+ Doc.AddDocToList(CurrentUserUtils.MySharedDocs, "viewed", dashboard)
CurrentUserUtils.ActiveDashboard = dashboard;
CurrentUserUtils.ActivePage = "dashboard";
}
}
getDashboards = () => {
- const allDashbaords = DocListCast(CurrentUserUtils.MyDashboards.data);
- // TODO: filter the dashboards
- // return allDashbaords.filter(...)
- return allDashbaords
+ const allDashboards = DocListCast(CurrentUserUtils.MyDashboards.data);
+ if (this.selectedDashboardGroup === DashboardGroup.MyDashboards) {
+ return allDashboards.filter((dashboard) => Doc.GetProto(dashboard).author === Doc.CurrentUserEmail)
+ } else {
+ const sharedDashboards = DocListCast(CurrentUserUtils.MySharedDocs.data).filter(doc => doc._viewType === CollectionViewType.Docking);
+ return sharedDashboards
+ }
+ }
+
+ isUnviewedSharedDashboard = (dashboard: Doc): boolean => {
+ // const sharedDashboards = DocListCast(CurrentUserUtils.MySharedDocs.data).filter(doc => doc._viewType === CollectionViewType.Docking);
+ return !DocListCast(CurrentUserUtils.MySharedDocs.viewed).includes(dashboard)
+ }
+
+ getSharedDashboards = () => {
+ const sharedDashs = DocListCast(CurrentUserUtils.MySharedDocs.data).filter(doc => doc._viewType === CollectionViewType.Docking);
+ return sharedDashs.filter((dashboard) => !DocListCast(CurrentUserUtils.MySharedDocs.viewed).includes(dashboard))
+ }
+
+ @undoBatch
+ createNewDashboard = async (name: string) => {
+ CurrentUserUtils.createNewDashboard(undefined, name);
+ this.abortCreateNewDashboard();
}
+ @computed
+ get namingInterface() {
+ return <div>
+ <input className="password-inputs" placeholder="Untitled Dashboard" onChange={e => this.setNewDashboardName((e.target as any).value)} />
+ <button className="password-submit" onClick={this.abortCreateNewDashboard}>Cancel</button>
+ <button className="password-submit" onClick={() => { this.createNewDashboard(this.newDashboardName!) }}>Create</button>
+ </div>;
+ }
+
+ _downX: number = 0;
+ _downY: number = 0;
+ @action
+ onContextMenu = (dashboard: Doc, e?: React.MouseEvent, pageX?: number, pageY?: number) => {
+ // the touch onContextMenu is button 0, the pointer onContextMenu is button 2
+ if (e) {
+ e.preventDefault();
+ e.stopPropagation();
+ e.persist();
+
+ if (!navigator.userAgent.includes("Mozilla") && (Math.abs(this._downX - e?.clientX) > 3 || Math.abs(this._downY - e?.clientY) > 3)) {
+ return;
+ }
+ const cm = ContextMenu.Instance;
+ cm.addItem({
+ description: "Share Dashboard", event: async () => {
+ SharingManager.Instance.open(undefined, dashboard)
+ }, icon: "edit"
+ });
+ cm.addItem({
+ description: "Delete Dashboard", event: async () => {
+ CurrentUserUtils.removeDashboard(dashboard)
+ }, icon: "trash"
+ });
+ cm.displayMenu((e?.pageX || pageX || 0) - 15, (e?.pageY || pageY || 0) - 15);
+ }
+ }
+
+
render() {
- return <div className="dashboard-view">
- <div className="left-menu">
- <div className="text-button" onClick={this.newDashboard}>New</div>
- <div className={`text-button ${this.selectedDashboardGroup === DashboardGroup.MyDashboards && 'selected'}`}onClick={() => this.selectDashboardGroup(DashboardGroup.MyDashboards) }>My Dashboards</div>
- <div className={`text-button ${this.selectedDashboardGroup === DashboardGroup.SharedDashboards && 'selected'}`} onClick={() => this.selectDashboardGroup(DashboardGroup.SharedDashboards) }>Shared Dashboards</div>
- </div>
- <div className="all-dashboards">
- {this.getDashboards().map((dashboard) => {
- const href = ImageCast((dashboard.thumb as Doc)?.data)?.url.href;
- return <div className="dashboard-container" key={dashboard[Id]} onClick={e => this.clickDashboard(e, dashboard)}>
- <img src={href ?? "https://media.istockphoto.com/photos/hot-air-balloons-flying-over-the-botan-canyon-in-turkey-picture-id1297349747?b=1&k=20&m=1297349747&s=170667a&w=0&h=oH31fJty_4xWl_JQ4OIQWZKP8C6ji9Mz7L4XmEnbqRU="}></img>
- <div className="title"> {StrCast(dashboard.title)} </div>
- </div>
+ return <>
+ <div className="dashboard-view">
+ <div className="left-menu">
+ <div className="text-button" onClick={() => { this.setNewDashboardName("") }}>New</div>
+ <div className={`text-button ${this.selectedDashboardGroup === DashboardGroup.MyDashboards && 'selected'}`} onClick={() => this.selectDashboardGroup(DashboardGroup.MyDashboards)}>My Dashboards</div>
+ <div className={`text-button ${this.selectedDashboardGroup === DashboardGroup.SharedDashboards && 'selected'}`} onClick={() => this.selectDashboardGroup(DashboardGroup.SharedDashboards)}>Shared Dashboards</div>
+ </div>
+ <div className="all-dashboards">
+ {this.getDashboards().map((dashboard) => {
+ const href = ImageCast((dashboard.thumb as Doc)?.data)?.url.href;
+ return <div className="dashboard-container" key={dashboard[Id]}
+ onContextMenu={(e) => {this.onContextMenu(dashboard, e)}}
+ onClick={e => this.clickDashboard(e, dashboard)}>
+ <img src={href ?? "https://media.istockphoto.com/photos/hot-air-balloons-flying-over-the-botan-canyon-in-turkey-picture-id1297349747?b=1&k=20&m=1297349747&s=170667a&w=0&h=oH31fJty_4xWl_JQ4OIQWZKP8C6ji9Mz7L4XmEnbqRU="}></img>
+ <div className="info">
+ <div className="title"> {StrCast(dashboard.title)} </div>
+ {this.selectedDashboardGroup === DashboardGroup.SharedDashboards && this.isUnviewedSharedDashboard(dashboard) ?
+ <div>unviewed</div> : <div></div>
+ }
+ <div className="more" onPointerDown={e => {
+ this._downX = e.clientX;
+ this._downY = e.clientY;
+ }}
+ onClick={(e) => {this.onContextMenu(dashboard, e)}}
+ >
+ <FontAwesomeIcon color="black" size="lg" icon="bars" />
+ </div>
+ </div>
- })}
+ </div>
+
+ })}
+ </div>
</div>
- </div>
+ <MainViewModal
+ contents={this.namingInterface}
+ isDisplayed={this.newDashboardName !== undefined}
+ interactive={true}
+ closeOnExternalClick={this.abortCreateNewDashboard}
+ dialogueBoxStyle={{ width: "500px", height: "300px", background: Cast(Doc.SharingDoc().userColor, "string", null) }} />;
+ </>
+
}
}
+
+export function AddToList(MySharedDocs: Doc, arg1: string, dash: any) {
+ throw new Error("Function not implemented.");
+}
+
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 4247501bb..669718e81 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -311,7 +311,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P
move[1] = thisPt.y - this._snapY;
this._snapX = thisPt.x;
this._snapY = thisPt.y;
- let dragBottom = false, dragRight = false, dragBotRight = false;
+ let dragBottom = false, dragRight = false, dragBotRight = false, dragTop = false;
let dX = 0, dY = 0, dW = 0, dH = 0;
switch (this._resizeHdlId.split(" ")[0]) {
case "": break;
@@ -329,7 +329,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P
case "documentDecorations-topResizer":
dY = -1;
dH = -move[1];
- dragBottom = true;
+ dragTop = true;
break;
case "documentDecorations-bottomLeftResizer":
dX = -1;
@@ -361,27 +361,28 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P
const doc = Document(docView.rootDoc);
const nwidth = docView.nativeWidth;
const nheight = docView.nativeHeight;
- const docheight = doc._height || 0;
- const docwidth = doc._width || 0;
+ let docheight = doc._height || 0;
+ let docwidth = doc._width || 0;
const width = docwidth;
let height = (docheight || (nheight / nwidth * width));
height = !height || isNaN(height) ? 20 : height;
const scale = docView.props.ScreenToLocalTransform().Scale;
- const modifyNativeDim = (e.ctrlKey || doc.forceReflow) && doc.nativeDimModifiable;
+ const modifyNativeDim = (e.ctrlKey || doc.forceReflow) && doc.nativeDimModifiable && ((!dragBottom && !dragTop) || e.ctrlKey || doc.nativeHeightUnfrozen);
if (nwidth && nheight) {
- if (nwidth / nheight !== width / height && !dragBottom) {
+ if (nwidth / nheight !== width / height && !dragBottom && !dragTop) {
height = nheight / nwidth * width;
}
- if (modifyNativeDim && !dragBottom) { // ctrl key enables modification of the nativeWidth or nativeHeight durin the interaction
+ if (modifyNativeDim && !dragBottom && !dragTop) { // ctrl key enables modification of the nativeWidth or nativeHeight durin the interaction
if (Math.abs(dW) > Math.abs(dH)) dH = dW * nheight / nwidth;
else dW = dH * nwidth / nheight;
}
}
let actualdW = Math.max(width + (dW * scale), 20);
let actualdH = Math.max(height + (dH * scale), 20);
- const fixedAspect = (nwidth && nheight && !doc._fitWidth);
+ const fixedAspect = (nwidth && nheight && (!doc._fitWidth || e.ctrlKey || doc.nativeHeightUnfrozen));
+ console.log(fixedAspect);
if (fixedAspect) {
- if ((Math.abs(dW) > Math.abs(dH) && (!dragBottom || !modifyNativeDim)) || dragRight) {
+ if ((Math.abs(dW) > Math.abs(dH) && ((!dragBottom && !dragTop)|| !modifyNativeDim)) || dragRight) {
if (dragRight && modifyNativeDim) {
doc._nativeWidth = actualdW / (doc._width || 1) * Doc.NativeWidth(doc);
} else {
@@ -394,7 +395,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P
doc._width = actualdW;
}
else {
- if (dragBottom && (modifyNativeDim ||
+ if ((dragBottom|| dragTop) && (modifyNativeDim ||
(docView.layoutDoc.nativeHeightUnfrozen && docView.layoutDoc._fitWidth))) { // frozen web pages, PDFs, and some RTFS have frozen nativewidth/height. But they are marked to allow their nativeHeight to be explicitly modified with fitWidth and vertical resizing. (ie, with fitWidth they can't grow horizontally to match a vertical resize so it makes more sense to change their nativeheight even if the ctrl key isn't used)
doc._nativeHeight = actualdH / (doc._height || 1) * Doc.NativeHeight(doc);
doc._autoHeight = false;
@@ -417,7 +418,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P
dH && (doc._autoHeight = false);
}
doc.x = (doc.x || 0) + dX * (actualdW - docwidth);
- doc.y = (doc.y || 0) + dY * (actualdH - docheight);
+ doc.y = (doc.y || 0) + (dragBottom ? 0: dY * (actualdH - docheight));
doc._lastModified = new DateField();
}
const val = this._dragHeights.get(docView.layoutDoc);
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index acc74e914..31fa5b157 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -11,7 +11,6 @@ import { LinkManager } from "../util/LinkManager";
import { ReplayMovements } from '../util/ReplayMovements';
import { TrackMovements } from "../util/TrackMovements";
import { CollectionView } from "./collections/CollectionView";
-import { DashboardView } from './DashboardView';
import { MainView } from "./MainView";
AssignAllExtensions();
@@ -25,6 +24,7 @@ AssignAllExtensions();
await CurrentUserUtils.loadUserDocument(info.id);
} else {
await Docs.Prototypes.initialize();
+ new LinkManager();
}
document.getElementById('root')!.addEventListener('wheel', event => {
if (event.ctrlKey) {
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 67a73feff..4940c5f9d 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -219,16 +219,16 @@ export class MainView extends React.Component {
}
initAuthenticationRouters = async () => {
- // Load the user's active dashboard, or create a new one if initial session after signup
const received = CurrentUserUtils.MainDocId;
if (received && !this.userDoc) {
reaction(() => CurrentUserUtils.GuestTarget, target => target && CurrentUserUtils.createNewDashboard(), { fireImmediately: true });
- } else {
- PromiseValue(this.userDoc.activeDashboard).then(dash => {
- if (dash instanceof Doc) CurrentUserUtils.openDashboard(dash);
- else CurrentUserUtils.createNewDashboard();
- });
- }
+ }
+ // else {
+ // PromiseValue(this.userDoc.activeDashboard).then(dash => {
+ // if (dash instanceof Doc) CurrentUserUtils.openDashboard(dash);
+ // else CurrentUserUtils.createNewDashboard();
+ // });
+ // }
}
@action
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 07fcd6a7d..0830b6fdf 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -4,12 +4,12 @@ import { action, IReactionDisposer, observable, reaction, runInAction } from "mo
import { observer } from "mobx-react";
import * as ReactDOM from 'react-dom';
import * as GoldenLayout from "../../../client/goldenLayout";
-import { DataSym, Doc, DocListCast, DocListCastAsync, Opt } from "../../../fields/Doc";
+import { Doc, DocListCast, Opt } from "../../../fields/Doc";
import { Id } from '../../../fields/FieldSymbols';
import { InkTool } from '../../../fields/InkField';
import { List } from '../../../fields/List';
-import { listSpec } from '../../../fields/Schema';
import { Cast, NumCast, StrCast } from "../../../fields/Types";
+import { ImageField } from '../../../fields/URLField';
import { inheritParentAcls } from '../../../fields/util';
import { emptyFunction, incrementTitleCopy } from '../../../Utils';
import { DocServer } from "../../DocServer";
@@ -19,14 +19,15 @@ import { CurrentUserUtils } from '../../util/CurrentUserUtils';
import { DragManager } from "../../util/DragManager";
import { InteractionUtils } from '../../util/InteractionUtils';
import { ScriptingGlobals } from '../../util/ScriptingGlobals';
+import { SelectionManager } from '../../util/SelectionManager';
import { undoBatch, UndoManager } from "../../util/UndoManager";
import { LightboxView } from '../LightboxView';
import "./CollectionDockingView.scss";
+import { CollectionFreeFormView } from './collectionFreeForm';
import { CollectionSubView, SubCollectionViewProps } from "./CollectionSubView";
import { CollectionViewType } from './CollectionView';
import { TabDocView } from './TabDocView';
import React = require("react");
-import { SelectionManager } from '../../util/SelectionManager';
const _global = (window /* browser */ || global /* node */) as any;
@observer
@@ -373,13 +374,34 @@ export class CollectionDockingView extends CollectionSubView() {
}
}
- public static async Copy(doc: Doc, clone = false) {
+ public CaptureThumbnail() {
+ const content = this.props.DocumentView?.()?.ContentDiv;
+ if (content) {
+ const _width = Number(getComputedStyle(content).width.replace("px",""));
+ const _height = Number(getComputedStyle(content).height.replace("px",""));
+ return CollectionFreeFormView.UpdateIcon(
+ this.layoutDoc[Id] + "-icon" + (new Date()).getTime(),
+ content,
+ _width, _height,
+ _width, _height, 0, 1, true, this.layoutDoc[Id] + "-icon",
+ (iconFile, _nativeWidth, _nativeHeight) => {
+ const img = Docs.Create.ImageDocument(new ImageField(iconFile), { title: this.rootDoc.title+"-icon", _width, _height, _nativeWidth, _nativeHeight});
+ const proto = Cast(img.proto, Doc, null)!;
+ proto["data-nativeWidth"] = _width;
+ proto["data-nativeHeight"] = _height;
+ this.dataDoc.thumb = img;
+ });
+ }
+
+ }
+ public static async TakeSnapshot(doc: Doc|undefined, clone = false) {
+ if (!doc) return undefined;
let json = StrCast(doc.dockingConfig);
if (clone) {
- const cloned = (await Doc.MakeClone(doc));
+ const cloned = await Doc.MakeClone(doc);
Array.from(cloned.map.entries()).map(entry => json = json.replace(entry[0], entry[1][Id]));
Doc.GetProto(cloned.clone).dockingConfig = json;
- return cloned.clone;
+ return CurrentUserUtils.openDashboard(cloned.clone);
}
const matches = json.match(/\"documentId\":\"[a-z0-9-]+\"/g);
const origtabids = matches?.map(m => m.replace("\"documentId\":\"", "").replace("\"", "")) || [];
@@ -395,7 +417,8 @@ export class CollectionDockingView extends CollectionSubView() {
json = json.replace(origtab[Id], newtab[Id]);
return newtab;
});
- return Docs.Create.DockDocument(newtabs, json, { title: incrementTitleCopy(StrCast(doc.title)) });
+ const copy = Docs.Create.DockDocument(newtabs, json, { title: incrementTitleCopy(StrCast(doc.title)) });
+ return CurrentUserUtils.openDashboard(await copy);
}
@action
diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx
index 3e85edac8..b00017453 100644
--- a/src/client/views/collections/CollectionStackedTimeline.tsx
+++ b/src/client/views/collections/CollectionStackedTimeline.tsx
@@ -725,12 +725,12 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
/>
{/* {this.renderDictation} */}
- { /* check time to prevent weird div overflow */ this._hoverTime < this.clipDuration && <div
+ <div
className="collectionStackedTimeline-hover"
style={{
left: `${((this._hoverTime - this.clipStart) / this.clipDuration) * 100}%`,
}}
- />}
+ />
<div
className="collectionStackedTimeline-current"
@@ -775,7 +775,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
</div>
</div>
<div className="timeline-hoverUI" style={{ left: `calc(${((this._hoverTime - this.clipStart) / this.clipDuration) * 100}%` }}>
- <div className="hoverTime">{formatTime(this._hoverTime)}</div>
+ <div className="hoverTime">{formatTime(this._hoverTime - this.clipStart)}</div>
{this._thumbnail && <img className="videoBox-thumbnail" src={this._thumbnail} />}
</div>
</div >);
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 17fdba764..03450b798 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -274,7 +274,7 @@ export function CollectionSubView<X>(moreProps?: X) {
if (docid) { // prosemirror text containing link to dash document
DocServer.GetRefField(docid).then(f => {
if (f instanceof Doc) {
- if (options.x || options.y) { f.x = options.x; f.y = options.y; } // should be in CollectionFreeFormView
+ if (options.x || options.y) { f.x = options.x as number; f.y = options.y as number; } // should be in CollectionFreeFormView
(f instanceof Doc) && addDocument(f);
}
});
@@ -311,7 +311,7 @@ export function CollectionSubView<X>(moreProps?: X) {
const docid = text.replace(Doc.globalServerPath(), "").split("?")[0];
DocServer.GetRefField(docid).then(f => {
if (f instanceof Doc) {
- if (options.x || options.y) { f.x = options.x; f.y = options.y; } // should be in CollectionFreeFormView
+ if (options.x || options.y) { f.x = options.x as number; f.y = options.y as number; } // should be in CollectionFreeFormView
(f instanceof Doc) && addDocument(f);
}
});
@@ -445,7 +445,7 @@ export function CollectionSubView<X>(moreProps?: X) {
if (completed) completed(set);
else {
if (isFreeformView && generatedDocuments.length > 1) {
- addDocument(DocUtils.pileup(generatedDocuments, options.x!, options.y!)!,);
+ addDocument(DocUtils.pileup(generatedDocuments, options.x as number, options.y as number)!,);
} else {
generatedDocuments.forEach(addDocument);
}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 1320785a9..13cccb7dd 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -153,6 +153,21 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
return this.getContainerTransform().translate(- this.cachedCenteringShiftX, - this.cachedCenteringShiftY).transform(this.cachedGetLocalTransform);
}
+ changeKeyFrame = (back=false) => {
+ const currentFrame = Cast(this.Document._currentFrame, "number", null);
+ if (currentFrame === undefined) {
+ this.Document._currentFrame = 0;
+ CollectionFreeFormDocumentView.setupKeyframes(this.childDocs, 0);
+ }
+ if (back) {
+ CollectionFreeFormDocumentView.gotoKeyframe(this.childDocs.slice());
+ this.Document._currentFrame = Math.max(0, (currentFrame || 0) - 1);
+ } else {
+ CollectionFreeFormDocumentView.updateKeyframe(this.childDocs, currentFrame || 0);
+ this.Document._currentFrame = Math.max(0, (currentFrame || 0) + 1);
+ this.Document.lastFrame = Math.max(NumCast(this.Document._currentFrame), NumCast(this.Document.lastFrame));
+ }
+ }
@action setKeyFrameEditing = (set: boolean) => this._keyframeEditing = set;
getKeyFrameEditing = () => this._keyframeEditing;
onBrowseClickHandler = () => this.props.onBrowseClick?.() || ScriptCast(this.layoutDoc.onBrowseClick);
@@ -1653,8 +1668,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
const mores = ContextMenu.Instance.findByDescription("More...");
const moreItems = mores && "subitems" in mores ? mores.subitems : [];
if (!Doc.noviceMode) {
+ e.persist();
moreItems.push({ description: "Export collection", icon: "download", event: async () => Doc.Zip(this.props.Document) });
- moreItems.push({ description: "Import exported collection", icon: "upload", event: ({ x, y }) => this.importDocument(x, y) });
+ moreItems.push({ description: "Import exported collection", icon: "upload", event: ({ x, y }) => this.importDocument(e.clientX, e.clientY) });
}
!mores && ContextMenu.Instance.addItem({ description: "More...", subitems: moreItems, icon: "eye" });
}
@@ -1663,28 +1679,13 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
const input = document.createElement("input");
input.type = "file";
input.accept = ".zip";
- input.onchange = async _e => {
- const upload = Utils.prepend("/uploadDoc");
- const formData = new FormData();
- const file = input.files && input.files[0];
- if (file) {
- formData.append('file', file);
- formData.append('remap', "true");
- const response = await fetch(upload, { method: "POST", body: formData });
- const json = await response.json();
- if (json !== "error") {
- const doc = await DocServer.GetRefField(json);
- if (doc instanceof Doc) {
- const [xx, yy] = this.props.ScreenToLocalTransform().transformPoint(x, y);
- doc.x = xx, doc.y = yy;
- this.props.addDocument?.(doc);
- setTimeout(() =>
- SearchUtil.Search(`{!join from=id to=proto_i}id:link*`, true, {}).then(docs => {
- docs.docs.forEach(d => LinkManager.Instance.addLink(d));
- }), 2000); // need to give solr some time to update so that this query will find any link docs we've added.
- }
- }
- }
+ input.onchange = _e => {
+ input.files && Doc.importDocument(input.files[0]).then(doc => {
+ if (doc instanceof Doc) {
+ const [xx, yy] = this.getTransform().transformPoint(x, y);
+ doc.x = xx, doc.y = yy;
+ this.props.addDocument?.(doc);}
+ });
};
input.click();
}
@@ -2093,4 +2094,6 @@ export function CollectionBrowseClick(dv: DocumentView, clientX: number, clientY
});
Doc.linkFollowHighlight(dv?.props.Document, false);
}
-ScriptingGlobals.add(CollectionBrowseClick); \ No newline at end of file
+ScriptingGlobals.add(CollectionBrowseClick);
+ScriptingGlobals.add(function nextKeyFrame(readOnly: boolean) { !readOnly && (SelectionManager.Views()[0].ComponentView as CollectionFreeFormView)?.changeKeyFrame(); });
+ScriptingGlobals.add(function prevKeyFrame(readOnly: boolean) { !readOnly && (SelectionManager.Views()[0].ComponentView as CollectionFreeFormView)?.changeKeyFrame(true); }); \ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 081a1a924..ab8a34d5a 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -156,7 +156,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
}));
} else if (e.key === "s" && e.ctrlKey) {
e.preventDefault();
- const slide = Doc.copyDragFactory(DocCast(Doc.UserDoc().emptySlide))!;
+ const slide = DocUtils.copyDragFactory(DocCast(Doc.UserDoc().emptySlide))!;
slide.x = x;
slide.y = y;
FormattedTextBox.SelectOnLoad = slide[Id];
@@ -517,7 +517,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
d.y = NumCast(d.y) - this.Bounds.top;
return d;
});
- const summary = Docs.Create.TextDocument("", { _backgroundColor: "#e2ad32", x: this.Bounds.left, y: this.Bounds.top, isPushpin: true, _width: 200, _height: 200, _fitContentsToBox: true, _showSidebar: true, title: "overview" });
+ const summary = Docs.Create.TextDocument("", { backgroundColor: "#e2ad32", x: this.Bounds.left, y: this.Bounds.top, isPushpin: true, _width: 200, _height: 200, _fitContentsToBox: true, _showSidebar: true, title: "overview" });
const portal = Docs.Create.FreeformDocument(selected, { x: this.Bounds.left + 200, y: this.Bounds.top, isGroup: true, backgroundColor: "transparent" });
DocUtils.MakeLink({ doc: summary }, { doc: portal }, "summary of:summarized by", "");
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 94cef2906..c42c2306a 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -63,7 +63,6 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
_recorder: any; // MediaRecorder
_recordStart = 0;
_pauseStart = 0; // time when recording is paused (used to keep track of recording timecodes)
- _pauseEnd = 0;
_pausedTime = 0;
_stream: MediaStream | undefined; // passed to MediaRecorder, records device input audio
_play: any = null; // timeout for playback
@@ -81,7 +80,6 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
@computed get miniPlayer() { return this.props.PanelHeight() < 50; } // used to collapse timeline when node is shrunk
@computed get links() { return DocListCast(this.dataDoc.links); }
- @computed get pauseTime() { return this._pauseEnd - this._pauseStart; } // total time paused to update the correct recording time
@computed get mediaState() { return this.dataDoc.mediaState as media_state; }
@computed get path() { // returns the path of the audio file
const path = Cast(this.props.Document[this.fieldKey], AudioField, null)?.url.href || "";
@@ -97,9 +95,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
this._dropDisposer?.();
Object.values(this._disposers).forEach((disposer) => disposer?.());
- // removes doc from active recordings if recording when closed
- const ind = DocUtils.ActiveRecordings.indexOf(this);
- ind !== -1 && DocUtils.ActiveRecordings.splice(ind, 1);
+ this.mediaState === media_state.Recording && this.stopRecording();
}
@action
@@ -220,10 +216,8 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
updateRecordTime = () => {
if (this.mediaState === media_state.Recording) {
setTimeout(this.updateRecordTime, 30);
- if (this._paused) {
- this._pausedTime += (new Date().getTime() - this._recordStart) / 1000;
- } else {
- this.layoutDoc._currentTimecode = (new Date().getTime() - this._recordStart - this.pauseTime) / 1000;
+ if (!this._paused) {
+ this.layoutDoc._currentTimecode = (new Date().getTime() - this._recordStart - this._pausedTime) / 1000;
}
}
}
@@ -253,7 +247,9 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
if (this._recorder) {
this._recorder.stop();
this._recorder = undefined;
- this.dataDoc[this.fieldKey + "-duration"] = (new Date().getTime() - this._recordStart - this.pauseTime) / 1000;
+ const now = new Date().getTime();
+ this._paused && (this._pausedTime += now - this._pauseStart);
+ this.dataDoc[this.fieldKey + "-duration"] = (now - this._recordStart - this._pausedTime) / 1000;
this.mediaState = media_state.Paused;
this._stream?.getAudioTracks()[0].stop();
const ind = DocUtils.ActiveRecordings.indexOf(this);
@@ -379,8 +375,8 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
// continue the recording
recordPlay = (e: React.PointerEvent) => {
setupMoveUpEvents(this, e, returnFalse, returnFalse, action(() => {
- this._pauseEnd = new Date().getTime();
this._paused = false;
+ this._pausedTime += new Date().getTime() - this._pauseStart;
this._recorder.resume();
}), false);
}
diff --git a/src/client/views/nodes/DataViz.tsx b/src/client/views/nodes/DataViz.tsx
deleted file mode 100644
index d9541dba0..000000000
--- a/src/client/views/nodes/DataViz.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import { observer } from "mobx-react";
-import * as React from "react";
-import { ViewBoxBaseComponent } from '../DocComponent';
-import "./DataViz.scss";
-import { FieldView, FieldViewProps } from "./FieldView";
-
-@observer
-export class DataVizBox extends ViewBoxBaseComponent<FieldViewProps>() {
-
- public static LayoutString(fieldKey: string) { return FieldView.LayoutString(DataVizBox, fieldKey); }
-
- render() {
- return (
- <div >
- <div>
- Hi
- </div>
- </div>
- );
- }
-} \ No newline at end of file
diff --git a/src/client/views/nodes/DataViz.scss b/src/client/views/nodes/DataVizBox/DataVizBox.scss
index e69de29bb..e69de29bb 100644
--- a/src/client/views/nodes/DataViz.scss
+++ b/src/client/views/nodes/DataVizBox/DataVizBox.scss
diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
new file mode 100644
index 000000000..592723ee9
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
@@ -0,0 +1,90 @@
+import { action, computed, observable } from "mobx";
+import { observer } from "mobx-react";
+import * as React from "react";
+import { StrCast } from "../../../../fields/Types";
+import { ViewBoxBaseComponent } from "../../DocComponent";
+import { FieldViewProps, FieldView } from "../FieldView";
+import "./DataVizBox.scss";
+import { HistogramBox } from "./HistogramBox";
+import { TableBox } from "./TableBox";
+
+enum DataVizView {
+ TABLE = "table",
+ HISTOGRAM= "histogram"
+}
+
+
+@observer
+export class DataVizBox extends ViewBoxBaseComponent<FieldViewProps>() {
+ @observable private pairs: {x: number, y:number}[] = [{x: 1, y:2}];
+
+ // TODO: nda - make this use enum values instead
+ // @observable private currView: DataVizView = DataVizView.TABLE;
+ @computed get currView() {
+ if (this.rootDoc._dataVizView) {
+ return StrCast(this.rootDoc._dataVizView);
+ } else {
+ return "table";
+ }
+ }
+
+ constructor(props: any) {
+ super(props);
+ if (!this.rootDoc._dataVizView) {
+ // TODO: nda - this might not always want to default to "table"
+ this.rootDoc._dataVizView = "table";
+ }
+ }
+
+ public static LayoutString(fieldKey: string) { return FieldView.LayoutString(DataVizBox, fieldKey); }
+
+ @action
+ private createPairs() {
+ const xVals: number[] = [0, 1, 2, 3, 4, 5];
+ // const yVals: number[] = [10, 20, 30, 40, 50, 60];
+ const yVals: number[] = [1, 2, 3, 4, 5, 6];
+ let pairs: {
+ x: number,
+ y:number
+ }[] = [];
+ if (xVals.length != yVals.length) return pairs;
+ for (let i = 0; i < xVals.length; i++) {
+ pairs.push({x: xVals[i], y: yVals[i]});
+ }
+ this.pairs = pairs;
+ return pairs;
+ }
+
+ @computed get selectView() {
+ switch(this.currView) {
+ case "table":
+ return (<TableBox pairs={this.pairs} />)
+ case "histogram":
+ return (<HistogramBox rootDoc={this.rootDoc} pairs={this.pairs}/>)
+ }
+ }
+
+ @computed get pairVals() {
+ return this.createPairs();
+ }
+
+ componentDidMount() {
+ this.createPairs();
+ }
+
+ // handle changing the view using a button
+ @action changeViewHandler(e: React.MouseEvent<HTMLButtonElement>) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.rootDoc._dataVizView = this.currView == "table" ? "histogram" : "table";
+ }
+
+ render() {
+ return (
+ <div className="dataViz">
+ <button onClick={(e) => this.changeViewHandler(e)}>Change View</button>
+ {this.selectView}
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/DataVizBox/DrawHelper.ts b/src/client/views/nodes/DataVizBox/DrawHelper.ts
new file mode 100644
index 000000000..595cecebf
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/DrawHelper.ts
@@ -0,0 +1,247 @@
+export class PIXIPoint {
+ public get x() { return this.coords[0]; }
+ public get y() { return this.coords[1]; }
+ public set x(value: number) { this.coords[0] = value; }
+ public set y(value: number) { this.coords[1] = value; }
+ public coords: number[] = [0, 0];
+ constructor(x: number, y: number) {
+ this.coords[0] = x;
+ this.coords[1] = y;
+ }
+}
+
+export class PIXIRectangle {
+ public x: number;
+ public y: number;
+ public width: number;
+ public height: number;
+ public get left() { return this.x; }
+ public get right() { return this.x + this.width; }
+ public get top() { return this.y; }
+ public get bottom() { return this.top + this.height; }
+ public static get EMPTY() { return new PIXIRectangle(0, 0, -1, -1); }
+ constructor(x: number, y: number, width: number, height: number) {
+ this.x = x;
+ this.y = y;
+ this.width = width;
+ this.height = height;
+ }
+}
+
+export class MathUtil {
+
+ public static EPSILON: number = 0.001;
+
+ public static Sign(value: number): number {
+ return value >= 0 ? 1 : -1;
+ }
+
+ public static AddPoint(p1: PIXIPoint, p2: PIXIPoint, inline: boolean = false): PIXIPoint {
+ if (inline) {
+ p1.x += p2.x;
+ p1.y += p2.y;
+ return p1;
+ }
+ else {
+ return new PIXIPoint(p1.x + p2.x, p1.y + p2.y);
+ }
+ }
+
+ public static Perp(p1: PIXIPoint): PIXIPoint {
+ return new PIXIPoint(-p1.y, p1.x);
+ }
+
+ public static DividePoint(p1: PIXIPoint, by: number, inline: boolean = false): PIXIPoint {
+ if (inline) {
+ p1.x /= by;
+ p1.y /= by;
+ return p1;
+ }
+ else {
+ return new PIXIPoint(p1.x / by, p1.y / by);
+ }
+ }
+
+ public static MultiplyConstant(p1: PIXIPoint, by: number, inline: boolean = false) {
+ if (inline) {
+ p1.x *= by;
+ p1.y *= by;
+ return p1;
+ }
+ else {
+ return new PIXIPoint(p1.x * by, p1.y * by);
+ }
+ }
+
+ public static SubtractPoint(p1: PIXIPoint, p2: PIXIPoint, inline: boolean = false): PIXIPoint {
+ if (inline) {
+ p1.x -= p2.x;
+ p1.y -= p2.y;
+ return p1;
+ }
+ else {
+ return new PIXIPoint(p1.x - p2.x, p1.y - p2.y);
+ }
+ }
+
+ public static Area(rect: PIXIRectangle): number {
+ return rect.width * rect.height;
+ }
+
+ public static DistToLineSegment(v: PIXIPoint, w: PIXIPoint, p: PIXIPoint) {
+ // Return minimum distance between line segment vw and point p
+ var l2 = MathUtil.DistSquared(v, w); // i.e. |w-v|^2 - avoid a sqrt
+ if (l2 === 0.0) return MathUtil.Dist(p, v); // v === w case
+ // Consider the line extending the segment, parameterized as v + t (w - v).
+ // We find projection of point p onto the line.
+ // It falls where t = [(p-v) . (w-v)] / |w-v|^2
+ // We clamp t from [0,1] to handle points outside the segment vw.
+ var dot = MathUtil.Dot(
+ MathUtil.SubtractPoint(p, v),
+ MathUtil.SubtractPoint(w, v)) / l2;
+ var t = Math.max(0, Math.min(1, dot));
+ // Projection falls on the segment
+ var projection = MathUtil.AddPoint(v,
+ MathUtil.MultiplyConstant(
+ MathUtil.SubtractPoint(w, v), t));
+ return MathUtil.Dist(p, projection);
+ }
+
+ public static LineSegmentIntersection(ps1: PIXIPoint, pe1: PIXIPoint, ps2: PIXIPoint, pe2: PIXIPoint): PIXIPoint | undefined {
+ var a1 = pe1.y - ps1.y;
+ var b1 = ps1.x - pe1.x;
+
+ var a2 = pe2.y - ps2.y;
+ var b2 = ps2.x - pe2.x;
+
+ var delta = a1 * b2 - a2 * b1;
+ if (delta === 0) {
+ return undefined;
+ }
+ var c2 = a2 * ps2.x + b2 * ps2.y;
+ var c1 = a1 * ps1.x + b1 * ps1.y;
+ var invdelta = 1 / delta;
+ return new PIXIPoint((b2 * c1 - b1 * c2) * invdelta, (a1 * c2 - a2 * c1) * invdelta);
+ }
+
+ public static PointInPIXIRectangle(p: PIXIPoint, rect: PIXIRectangle): boolean {
+ if (p.x < rect.left - this.EPSILON) {
+ return false;
+ }
+ if (p.x > rect.right + this.EPSILON) {
+ return false;
+ }
+ if (p.y < rect.top - this.EPSILON) {
+ return false;
+ }
+ if (p.y > rect.bottom + this.EPSILON) {
+ return false;
+ }
+
+ return true;
+ }
+
+ public static LinePIXIRectangleIntersection(lineFrom: PIXIPoint, lineTo: PIXIPoint, rect: PIXIRectangle): Array<PIXIPoint> {
+ var r1 = new PIXIPoint(rect.left, rect.top);
+ var r2 = new PIXIPoint(rect.right, rect.top);
+ var r3 = new PIXIPoint(rect.right, rect.bottom);
+ var r4 = new PIXIPoint(rect.left, rect.bottom);
+ var ret = new Array<PIXIPoint>();
+ var dist = this.Dist(lineFrom, lineTo);
+ var inter = this.LineSegmentIntersection(lineFrom, lineTo, r1, r2);
+ if (inter && this.PointInPIXIRectangle(inter, rect) &&
+ this.Dist(inter, lineFrom) < dist && this.Dist(inter, lineTo) < dist) {
+ ret.push(inter);
+ }
+ inter = this.LineSegmentIntersection(lineFrom, lineTo, r2, r3);
+ if (inter && this.PointInPIXIRectangle(inter, rect) &&
+ this.Dist(inter, lineFrom) < dist && this.Dist(inter, lineTo) < dist) {
+ ret.push(inter);
+ }
+ inter = this.LineSegmentIntersection(lineFrom, lineTo, r3, r4);
+ if (inter && this.PointInPIXIRectangle(inter, rect) &&
+ this.Dist(inter, lineFrom) < dist && this.Dist(inter, lineTo) < dist) {
+ ret.push(inter);
+ }
+ inter = this.LineSegmentIntersection(lineFrom, lineTo, r4, r1);
+ if (inter && this.PointInPIXIRectangle(inter, rect) &&
+ this.Dist(inter, lineFrom) < dist && this.Dist(inter, lineTo) < dist) {
+ ret.push(inter);
+ }
+ return ret;
+ }
+
+ public static Intersection(rect1: PIXIRectangle, rect2: PIXIRectangle): PIXIRectangle {
+ const left = Math.max(rect1.x, rect2.x);
+ const right = Math.min(rect1.x + rect1.width, rect2.x + rect2.width);
+ const top = Math.max(rect1.y, rect2.y);
+ const bottom = Math.min(rect1.y + rect1.height, rect2.y + rect2.height);
+ return new PIXIRectangle(left, top, right - left, bottom - top);
+ }
+
+ public static Dist(p1: PIXIPoint, p2: PIXIPoint): number {
+ return Math.sqrt(MathUtil.DistSquared(p1, p2));
+ }
+
+ public static Dot(p1: PIXIPoint, p2: PIXIPoint): number {
+ return p1.x * p2.x + p1.y * p2.y;
+ }
+
+ public static Normalize(p1: PIXIPoint) {
+ var d = this.Length(p1);
+ return new PIXIPoint(p1.x / d, p1.y / d);
+ }
+
+ public static Length(p1: PIXIPoint): number {
+ return Math.sqrt(p1.x * p1.x + p1.y * p1.y);
+ }
+
+ public static DistSquared(p1: PIXIPoint, p2: PIXIPoint): number {
+ const a = p1.x - p2.x;
+ const b = p1.y - p2.y;
+ return (a * a + b * b);
+ }
+
+ public static RectIntersectsRect(r1: PIXIRectangle, r2: PIXIRectangle): boolean {
+ return !(r2.x > r1.x + r1.width ||
+ r2.x + r2.width < r1.x ||
+ r2.y > r1.y + r1.height ||
+ r2.y + r2.height < r1.y);
+ }
+
+ public static ArgMin(temp: number[]): number {
+ let index = 0;
+ let value = temp[0];
+ for (let i = 1; i < temp.length; i++) {
+ if (temp[i] < value) {
+ value = temp[i];
+ index = i;
+ }
+ }
+ return index;
+ }
+
+ public static ArgMax(temp: number[]): number {
+ let index = 0;
+ let value = temp[0];
+ for (let i = 1; i < temp.length; i++) {
+ if (temp[i] > value) {
+ value = temp[i];
+ index = i;
+ }
+ }
+ return index;
+ }
+
+ public static Combinations<T>(chars: T[]) {
+ let result = new Array<T>();
+ let f = (prefix: any, chars: any) => {
+ for (let i = 0; i < chars.length; i++) {
+ result.push(prefix.concat(chars[i]));
+ f(prefix.concat(chars[i]), chars.slice(i + 1));
+ }
+ };
+ f([], chars);
+ return result;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/DataVizBox/HistogramBox.scss b/src/client/views/nodes/DataVizBox/HistogramBox.scss
new file mode 100644
index 000000000..5aac9dc77
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/HistogramBox.scss
@@ -0,0 +1,18 @@
+// change the stroke color of line-svg class
+.svgLine {
+ position: absolute;
+ background: darkGray;
+ stroke: #000;
+ stroke-width: 1px;
+ width:100%;
+ height:100%;
+ opacity: 0.4;
+}
+
+.svgContainer {
+ position: absolute;
+ top:0;
+ left:0;
+ width:100%;
+ height: 100%;
+} \ No newline at end of file
diff --git a/src/client/views/nodes/DataVizBox/HistogramBox.tsx b/src/client/views/nodes/DataVizBox/HistogramBox.tsx
new file mode 100644
index 000000000..00dc2ef46
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/HistogramBox.tsx
@@ -0,0 +1,159 @@
+import { action, computed, observable } from "mobx";
+import { observer } from "mobx-react";
+import * as React from "react";
+import { Doc } from "../../../../fields/Doc";
+import { NumCast } from "../../../../fields/Types";
+import "./HistogramBox.scss";
+
+interface HistogramBoxProps {
+ rootDoc: Doc;
+ pairs: {
+ x: number,
+ y: number
+ }[]
+}
+
+
+export class HistogramBox extends React.Component<HistogramBoxProps> {
+
+ private origin = {x: 0.1 * this.width, y: 0.9 * this.height};
+
+ @computed get width() {
+ return NumCast(this.props.rootDoc.width);
+ }
+
+ @computed get height() {
+ return NumCast(this.props.rootDoc.height);
+ }
+
+ @computed get x() {
+ return NumCast(this.props.rootDoc.x);
+ }
+
+ @computed get y() {
+ return NumCast(this.props.rootDoc.y);
+ }
+
+ @computed get generatePoints() {
+ // evenly distribute points along the x axis
+ const xVals: number[] = this.props.pairs.map(p => p.x);
+ const yVals: number[] = this.props.pairs.map(p => p.y);
+
+ const xMin = Math.min(...xVals);
+ const xMax = Math.max(...xVals);
+ const yMin = Math.min(...yVals);
+ const yMax = Math.max(...yVals);
+
+ const xRange = xMax - xMin;
+ const yRange = yMax - yMin;
+
+ const xScale = this.width / xRange;
+ const yScale = this.height / yRange;
+
+ const xOffset = (this.x + (0.1 * this.width)) - xMin * xScale;
+ const yOffset = (this.y + (0.25 * this.height)) - yMin * yScale;
+
+ const points: {
+ x: number,
+ y: number
+ }[] = this.props.pairs.map(p => {
+ return {
+ x: (p.x * xScale + xOffset) + this.origin.x,
+ y: (p.y * yScale + yOffset)
+ }
+ });
+
+ return points;
+ }
+
+ @computed get generateGraphLine() {
+ const points = this.generatePoints;
+ // loop through points and create a line from each point to the next
+ let lines: {
+ x1: number,
+ y1: number,
+ x2: number,
+ y2: number
+ }[] = [];
+ for (let i = 0; i < points.length - 1; i++) {
+ lines.push({
+ x1: points[i].x,
+ y1: points[i].y,
+ x2: points[i + 1].x,
+ y2: points[i + 1].y
+ });
+ }
+ // generate array of svg with lines
+ let svgLines: JSX.Element[] = [];
+ for (let i = 0; i < lines.length; i++) {
+ svgLines.push(
+ <line
+ className="svgLine"
+ key={i}
+ x1={lines[i].x1}
+ y1={lines[i].y1}
+ x2={lines[i].x2}
+ y2={lines[i].y2}
+ stroke="black"
+ strokeWidth={2}
+ />
+ );
+ }
+
+ let res = [];
+ for (let i = 0; i < svgLines.length; i++) {
+ res.push(<svg className="svgContainer">{svgLines[i]}</svg>)
+ }
+ return res;
+ }
+
+ @computed get generateAxes() {
+
+ const xAxis = {
+ x1: 0.1 * this.width,
+ x2: 0.9 * this.width,
+ y1: 0.9 * this.height,
+ y2: 0.9 * this.height,
+ };
+
+ const yAxis = {
+ x1: 0.1 * this.width,
+ x2: 0.1 * this.width,
+ y1: 0.25 * this.height,
+ y2: 0.9 * this.height,
+ };
+
+
+ return (
+ [
+ (<svg className="svgContainer">
+ {/* <line className="svgLine" x1={yAxis} y1={xAxis} x2={this.width - (0.1 * this.width)} y2={xAxis} /> */}
+ <line className="svgLine" x1={xAxis.x1} y1={xAxis.y1} x2={xAxis.x2} y2={xAxis.y2}/>
+
+ {/* <line className="svgLine" x1={yAxis} y1={xAxis} x2={yAxis} y2={this.y + 50} /> */}
+ </svg>),
+ (
+ <svg className="svgContainer">
+ <line className="svgLine" x1={yAxis.x1} y1={yAxis.y1} x2={yAxis.x2} y2={yAxis.y2} />
+ {/* <line className="svgLine" x1={yAxis} y1={xAxis} x2={yAxis} y2={this.y + 50} /> */}
+ </svg>)
+ ]
+ )
+ }
+
+
+ render() {
+ return (
+ <div>histogram box
+ {/* <svg className="svgContainer">
+ {this.generateSVGLine}
+ </svg> */}
+ {this.generateAxes[0]}
+ {this.generateAxes[1]}
+ {this.generateGraphLine.map(line => line)}
+ </div>
+ )
+
+ }
+
+} \ No newline at end of file
diff --git a/src/client/views/nodes/DataVizBox/TableBox.scss b/src/client/views/nodes/DataVizBox/TableBox.scss
new file mode 100644
index 000000000..1264d6a46
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/TableBox.scss
@@ -0,0 +1,22 @@
+.table {
+ margin-top: 10px;
+ margin-bottom: 10px;
+ margin-left: 10px;
+ margin-right: 10px;
+}
+
+.table-row {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: center;
+ padding: 5px;
+ border-bottom: 1px solid #ccc;
+}
+
+.table-container {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+} \ No newline at end of file
diff --git a/src/client/views/nodes/DataVizBox/TableBox.tsx b/src/client/views/nodes/DataVizBox/TableBox.tsx
new file mode 100644
index 000000000..dfa8262d8
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/TableBox.tsx
@@ -0,0 +1,37 @@
+import { action, computed, observable } from "mobx";
+import { observer } from "mobx-react";
+import * as React from "react";
+
+interface TableBoxProps {
+ pairs: {x: number, y:number}[]
+}
+
+
+export class TableBox extends React.Component<TableBoxProps> {
+
+
+
+ render() {
+ return (
+ <div className="table-container">
+ <table className="table">
+ <thead>
+ <tr className="table-row">
+ <th>x</th>
+ <th>y</th>
+ </tr>
+ </thead>
+ <tbody>
+ {this.props.pairs.map(p => {
+ return (<tr className="table-row">
+ <td>{p.x}</td>
+ <td>{p.y}</td>
+ </tr>)
+ })}
+ </tbody>
+ </table>
+ </div>
+ )
+ }
+
+} \ No newline at end of file
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index 371d85a32..96ac3e332 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -19,7 +19,7 @@ import { AudioBox } from "./AudioBox";
import { FontIconBox } from "./button/FontIconBox";
import { ColorBox } from "./ColorBox";
import { ComparisonBox } from "./ComparisonBox";
-import { DataVizBox } from "./DataViz";
+import { DataVizBox } from "./DataVizBox/DataVizBox";
import { DocumentViewProps } from "./DocumentView";
import "./DocumentView.scss";
import { EquationBox } from "./EquationBox";
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 8d4fd376f..2ea976813 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1243,13 +1243,14 @@ export class DocumentView extends React.Component<DocumentViewProps> {
@computed get panelWidth() { return this.effectiveNativeWidth ? this.effectiveNativeWidth * this.nativeScaling : this.props.PanelWidth(); }
@computed get panelHeight() {
- if (this.effectiveNativeHeight) {
- return Math.min(this.props.PanelHeight(), Math.max(this.ComponentView?.getScrollHeight?.() ?? NumCast(this.layoutDoc.scrollHeight), this.effectiveNativeHeight) * this.nativeScaling);
+ if (this.effectiveNativeHeight && !this.layoutDoc.nativeHeightUnfrozen) {
+ const scrollHeight = this.fitWidth ? Math.max(this.ComponentView?.getScrollHeight?.() ?? NumCast(this.layoutDoc.scrollHeight)) : 0;
+ return Math.min(this.props.PanelHeight(), Math.max(scrollHeight, this.effectiveNativeHeight) * this.nativeScaling);
}
return this.props.PanelHeight();
}
@computed get Xshift() { return this.effectiveNativeWidth ? (this.props.PanelWidth() - this.effectiveNativeWidth * this.nativeScaling) / 2 : 0; }
- @computed get Yshift() { return this.effectiveNativeWidth && this.effectiveNativeHeight && Math.abs(this.Xshift) < 0.001 ? (this.props.PanelHeight() - this.effectiveNativeHeight * this.nativeScaling) / 2 : 0; }
+ @computed get Yshift() { return this.effectiveNativeWidth && this.effectiveNativeHeight && Math.abs(this.Xshift) < 0.001 && !this.layoutDoc.nativeHeightUnfrozen ? Math.max(0, (this.props.PanelHeight() - this.effectiveNativeHeight * this.nativeScaling) / 2) : 0; }
@computed get centeringX() { return this.props.dontCenter?.includes("x") ? 0 : this.Xshift; }
@computed get centeringY() { return this.props.dontCenter?.includes("y") ? 0 : this.Yshift; }
@@ -1343,7 +1344,6 @@ export class DocumentView extends React.Component<DocumentViewProps> {
transition: this.props.dataTransition,
position: this.props.Document.isInkMask ? "absolute" : undefined,
transform: isButton ? undefined : `translate(${this.centeringX}px, ${this.centeringY}px)`,
- margin: this.fitWidth ? "auto" : undefined,
width: isButton || isPresTreeElement ? "100%" : xshift() ?? `${100 * (this.props.PanelWidth() - this.Xshift * 2) / this.props.PanelWidth()}%`,
height: isButton || this.props.forceAutoHeight ? undefined : yshift() ?? (this.fitWidth ? `${this.panelHeight}px` :
`${100 * this.effectiveNativeHeight / this.effectiveNativeWidth * this.props.PanelWidth() / this.props.PanelHeight()}%`),
diff --git a/src/client/views/nodes/button/FontIconBadge.tsx b/src/client/views/nodes/button/FontIconBadge.tsx
index df17d603f..3b5aac221 100644
--- a/src/client/views/nodes/button/FontIconBadge.tsx
+++ b/src/client/views/nodes/button/FontIconBadge.tsx
@@ -6,7 +6,7 @@ import { emptyFunction, returnFalse, setupMoveUpEvents } from "../../../../Utils
import { DragManager } from "../../../util/DragManager";
import "./FontIconBadge.scss";
-interface FontIconBadgeProps {
+interface FontIconBadgeProps {
value: string | undefined;
}
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index d3dee3c89..8e1698eba 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -29,7 +29,7 @@ import { CurrentUserUtils } from '../../../util/CurrentUserUtils';
import { DictationManager } from '../../../util/DictationManager';
import { DocumentManager } from '../../../util/DocumentManager';
import { DragManager } from "../../../util/DragManager";
-import { makeTemplate } from '../../../util/DropConverter';
+import { MakeTemplate } from '../../../util/DropConverter';
import { LinkManager } from '../../../util/LinkManager';
import { SelectionManager } from "../../../util/SelectionManager";
import { SnappingManager } from '../../../util/SnappingManager';
@@ -682,7 +682,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
if (!this.layoutDoc.isTemplateDoc) {
const title = StrCast(this.rootDoc.title);
this.rootDoc.title = "text";
- this.rootDoc.isTemplateDoc = makeTemplate(this.rootDoc, true, title);
+ MakeTemplate(this.rootDoc, true, title);
} else if (!this.rootDoc.isTemplateDoc) {
const title = StrCast(this.rootDoc.title);
this.rootDoc.title = "text";
@@ -691,7 +691,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
this.rootDoc.isTemplateDoc = false;
this.rootDoc.isTemplateForField = "";
this.rootDoc.layoutKey = "layout";
- this.rootDoc.isTemplateDoc = makeTemplate(this.rootDoc, true, title);
+ MakeTemplate(this.rootDoc, true, title);
setTimeout(() => {
this.rootDoc._autoHeight = this.layoutDoc._autoHeight; // autoHeight, width and height
this.rootDoc._width = this.layoutDoc._width || 300; // are stored on the template, since we're getting rid of the old template
@@ -854,6 +854,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
return this._didScroll ? this._focusSpeed : undefined; // if we actually scrolled, then return some focusSpeed
}
+ getScrollHeight = () => this.scrollHeight;
// if the scroll height has changed and we're in autoHeight mode, then we need to update the textHeight component of the doc.
// Since we also monitor all component height changes, this will update the document's height.
resetNativeHeight = (scrollHeight: number) => {
diff --git a/src/client/views/topbar/TopBar.scss b/src/client/views/topbar/TopBar.scss
index c5b340514..d415e9367 100644
--- a/src/client/views/topbar/TopBar.scss
+++ b/src/client/views/topbar/TopBar.scss
@@ -2,7 +2,6 @@
.topbar-container {
- display: flex;
flex-direction: column;
font-size: 10px;
line-height: 1;
@@ -11,8 +10,10 @@
background: $dark-gray;
overflow: visible;
z-index: 1000;
+ align-items: center;
height: $topbar-height;
background-color: $dark-gray;
+ cursor: default;
.topbar-inner-container {
display: flex;
@@ -52,6 +53,7 @@
&:hover {
background-color: darken($color: $light-gray, $amount: 20);
+ font-weight: 500;
}
}
diff --git a/src/client/views/topbar/TopBar.tsx b/src/client/views/topbar/TopBar.tsx
index 6a4deca38..b447bdc19 100644
--- a/src/client/views/topbar/TopBar.tsx
+++ b/src/client/views/topbar/TopBar.tsx
@@ -14,6 +14,7 @@ import { SelectionManager } from "../../util/SelectionManager";
import { SettingsManager } from "../../util/SettingsManager";
import { SharingManager } from "../../util/SharingManager";
import { undoBatch, UndoManager } from "../../util/UndoManager";
+import { CollectionDockingView } from "../collections/CollectionDockingView";
import { ContextMenu } from "../ContextMenu";
import { Borders, Colors } from "../global/globalEnums";
import { MainView } from "../MainView";
@@ -26,7 +27,7 @@ import "./TopBar.scss";
@observer
export class TopBar extends React.Component {
navigateToHome = () => {
- CurrentUserUtils.CaptureDashboardThumbnail()?.then(() => {
+ CollectionDockingView.Instance.CaptureThumbnail()?.then(() => {
CurrentUserUtils.ActivePage = "home";
CurrentUserUtils.closeActiveDashboard(); // bcz: if we do this, we need some other way to keep track, for user convenience, of the last dashboard in use
});
@@ -38,10 +39,18 @@ export class TopBar extends React.Component {
<div style={{ pointerEvents: "all", background: Colors.DARK_GRAY, borderBottom: Borders.STANDARD }} className="topbar-container">
<div className="topbar-inner-container">
<div className="topbar-left">
- {activeDashboard ? <div className="topbar-button-text" onClick={e => {
- ContextMenu.Instance.addItem({ description: "Logout", event: () => window.location.assign(Utils.prepend("/logout")), icon: "edit" });
- ContextMenu.Instance.displayMenu(e.clientX +5, e.clientY + 10);
- }}>{Doc.CurrentUserEmail}</div> : (null)}
+ {activeDashboard ?
+ <>
+ <div className="topbar-button-icon" onClick={e => {
+ ContextMenu.Instance.addItem({ description: "Logout", event: () => window.location.assign(Utils.prepend("/logout")), icon: "edit" });
+ ContextMenu.Instance.displayMenu(e.clientX + 5, e.clientY + 10);
+ }}>{Doc.CurrentUserEmail}</div>
+ <div className="topbar-button-icon" onClick={this.navigateToHome}>
+ <FontAwesomeIcon icon="home" />
+ </div>
+ </>
+ : (null)}
+
</div>
<div className="topbar-center" >
<div className="topbar-title" onClick={() => activeDashboard && SelectionManager.SelectView(DocumentManager.Instance.getDocumentView(activeDashboard)!, false)}>
@@ -57,20 +66,18 @@ export class TopBar extends React.Component {
}, icon: "edit" });
dashView?.showContextMenu(e.clientX+20, e.clientY+30);
}}>
- <FontAwesomeIcon color="white" size="lg" icon="bars" />
+ <FontAwesomeIcon color="white" size="lg" icon="bars" />
</div>
<Tooltip title={<div className="dash-tooltip">Browsing mode for directly navigating to documents</div>} placement="bottom">
- <div className="topbar-icon" style={{ background: MainView.Instance._exploreMode ? Colors.LIGHT_BLUE : undefined }} onClick={action(() => MainView.Instance._exploreMode = !MainView.Instance._exploreMode)}>
- <FontAwesomeIcon color={MainView.Instance._exploreMode ? "red":"white"} icon="eye" size="lg" />
- </div>
+ <div className="topbar-button-icon" style={{ background: MainView.Instance._exploreMode ? Colors.LIGHT_BLUE : undefined }} onClick={action(() => MainView.Instance._exploreMode = !MainView.Instance._exploreMode)}>
+ <FontAwesomeIcon color={MainView.Instance._exploreMode ? "red" : "white"} icon="eye" size="lg" />
+ </div>
</Tooltip>
</div>
<div className="topbar-right" >
- <div className="topbar-button-text" onClick={() => {SharingManager.Instance.open(undefined, activeDashboard)}}>
- {/* TODO: if this is my dashboard, display share
- if this is a shared dashboard, display "view original or view annotated" */}
- { CurrentUserUtils.ActiveDashboard && (Doc.GetProto(CurrentUserUtils.ActiveDashboard)?.author === Doc.CurrentUserEmail ? "Share": "view original") }
- </div>
+ {CurrentUserUtils.ActiveDashboard ? <div className="topbar-button-icon" onClick={() => { SharingManager.Instance.open(undefined, activeDashboard) }}>
+ {GetEffectiveAcl(Doc.GetProto(CurrentUserUtils.ActiveDashboard)) === AclAdmin ? "Share" : "view original"}
+ </div> : (null)}
<div className="topbar-button-icon" onClick={() => window.open(
"https://brown-dash.github.io/Dash-Documentation/", "_blank")}>
<FontAwesomeIcon icon="question-circle" />
@@ -81,7 +88,7 @@ export class TopBar extends React.Component {
</div>
</div>
</div>
-
+
);
}
} \ No newline at end of file
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 981514b25..b30ca644d 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -21,9 +21,9 @@ import { FieldId, RefField } from "./RefField";
import { RichTextField } from "./RichTextField";
import { listSpec } from "./Schema";
import { ComputedField, ScriptField } from "./ScriptField";
-import { BoolCast, Cast, FieldValue, NumCast, StrCast, ToConstructor } from "./Types";
+import { Cast, FieldValue, NumCast, StrCast, ToConstructor } from "./Types";
import { AudioField, ImageField, MapField, PdfField, VideoField, WebField } from "./URLField";
-import { deleteProperty, GetEffectiveAcl, getField, getter, inheritParentAcls, makeEditable, makeReadOnly, normalizeEmail, setter, SharingPermissions, updateFunction } from "./util";
+import { deleteProperty, GetEffectiveAcl, getField, getter, makeEditable, makeReadOnly, normalizeEmail, setter, SharingPermissions, updateFunction } from "./util";
import JSZip = require("jszip");
export namespace Field {
@@ -658,7 +658,7 @@ export namespace Doc {
const zip = new JSZip();
- zip.file(doc.title + ".json", docString);
+ zip.file("doc.json", docString);
// // Generate a directory within the Zip file structure
// var img = zip.folder("images");
@@ -1242,27 +1242,6 @@ export namespace Doc {
return !curPres ? false : DocListCast(curPres.data).findIndex((val) => Doc.AreProtosEqual(val, doc)) !== -1;
}
- export function copyDragFactory(dragFactory: Doc) {
- const ndoc = dragFactory.isTemplateDoc ? Doc.ApplyTemplate(dragFactory) : Doc.MakeCopy(dragFactory, true);
- ndoc && Doc.AddDocToList(CurrentUserUtils.MyFileOrphans, "data", Doc.GetProto(ndoc));
- if (ndoc && dragFactory["dragFactory-count"] !== undefined) {
- dragFactory["dragFactory-count"] = NumCast(dragFactory["dragFactory-count"]) + 1;
- Doc.SetInPlace(ndoc, "title", ndoc.title + " " + NumCast(dragFactory["dragFactory-count"]).toString(), true);
- }
-
- if (ndoc && CurrentUserUtils.ActiveDashboard) inheritParentAcls(CurrentUserUtils.ActiveDashboard, ndoc);
-
- return ndoc;
- }
- export function delegateDragFactory(dragFactory: Doc) {
- const ndoc = Doc.MakeDelegateWithProto(dragFactory);
- if (ndoc && dragFactory["dragFactory-count"] !== undefined) {
- dragFactory["dragFactory-count"] = NumCast(dragFactory["dragFactory-count"]) + 1;
- Doc.GetProto(ndoc).title = ndoc.title + " " + NumCast(dragFactory["dragFactory-count"]).toString();
- }
- return ndoc;
- }
-
export function toIcon(doc?: Doc, isOpen?: boolean) {
switch (StrCast(doc?.type)) {
case DocumentType.IMG: return "image";
@@ -1289,6 +1268,21 @@ export namespace Doc {
}
}
+ export async function importDocument(file:File) {
+ const upload = Utils.prepend("/uploadDoc");
+ const formData = new FormData();
+ if (file) {
+ formData.append('file', file);
+ formData.append('remap', "true");
+ const response = await fetch(upload, { method: "POST", body: formData });
+ const json = await response.json();
+ if (json !== "error") {
+ const doc = await DocServer.GetRefField(json);
+ return doc;
+ }
+ }
+ return undefined;
+ }
export namespace Get {
@@ -1439,8 +1433,6 @@ ScriptingGlobals.add(function getProto(doc: any) { return Doc.GetProto(doc); });
ScriptingGlobals.add(function getDocTemplate(doc?: any) { return Doc.getDocTemplate(doc); });
ScriptingGlobals.add(function getAlias(doc: any) { return Doc.MakeAlias(doc); });
ScriptingGlobals.add(function getCopy(doc: any, copyProto: any) { return doc.isTemplateDoc ? Doc.ApplyTemplate(doc) : Doc.MakeCopy(doc, copyProto); });
-ScriptingGlobals.add(function copyDragFactory(dragFactory: Doc) { return Doc.copyDragFactory(dragFactory); });
-ScriptingGlobals.add(function delegateDragFactory(dragFactory: Doc) { return Doc.delegateDragFactory(dragFactory); });
ScriptingGlobals.add(function copyField(field: any) { return Field.Copy(field); });
ScriptingGlobals.add(function docList(field: any) { return DocListCast(field); });
ScriptingGlobals.add(function addDocToList(doc: Doc, field: string, added:Doc) { return Doc.AddDocToList(doc,field, added); });
diff --git a/src/fields/URLField.ts b/src/fields/URLField.ts
index 3e7542e46..36dd56a1a 100644
--- a/src/fields/URLField.ts
+++ b/src/fields/URLField.ts
@@ -59,6 +59,7 @@ export const nullAudio = "https://actions.google.com/sounds/v1/alarms/beep_short
@scriptingGlobal @Deserializable("pdf") export class PdfField extends URLField { }
@scriptingGlobal @Deserializable("web") export class WebField extends URLField { }
@scriptingGlobal @Deserializable("map") export class MapField extends URLField { }
+@scriptingGlobal @Deserializable("csv") export class CsvField extends URLField { }
@scriptingGlobal @Deserializable("youtube") export class YoutubeField extends URLField { }
@scriptingGlobal @Deserializable("webcam") export class WebCamField extends URLField { }
diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts
index 217c88107..332ba3d35 100644
--- a/src/server/ApiManagers/UploadManager.ts
+++ b/src/server/ApiManagers/UploadManager.ts
@@ -24,6 +24,7 @@ export enum Directory {
text = "text",
pdf_thumbnails = "pdf_thumbnails",
audio = "audio",
+ csv = "csv",
}
export function serverPathToFile(directory: Directory, filename: string) {
diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts
index df5888c5a..cae35da60 100644
--- a/src/server/DashUploadUtils.ts
+++ b/src/server/DashUploadUtils.ts
@@ -18,6 +18,7 @@ import { AcceptableMedia, Upload } from './SharedMediaTypes';
import request = require('request-promise');
import formidable = require('formidable');
import { file } from 'jszip';
+import { csvParser } from './DataVizUtils';
const { exec } = require("child_process");
const parse = require('pdf-parse');
const ffmpeg = require("fluent-ffmpeg");
@@ -124,7 +125,7 @@ export namespace DashUploadUtils {
const category = types[0];
let format = `.${types[1]}`;
console.log(green(`Processing upload of file (${name}) and format (${format}) with upload type (${type}) in category (${category}).`));
-
+
switch (category) {
case "image":
if (imageFormats.includes(format)) {
@@ -156,6 +157,11 @@ export namespace DashUploadUtils {
if (audioFormats.includes(format)) {
return UploadAudio(file, format);
}
+ case "text":
+ if (types[1] == "csv") {
+ return UploadCsv(file);
+ }
+
}
console.log(red(`Ignoring unsupported file (${name}) with upload type (${type}).`));
@@ -175,6 +181,16 @@ export namespace DashUploadUtils {
return MoveParsedFile(file, Directory.pdfs, undefined, result.text);
}
+ async function UploadCsv(file: File) {
+ const { path: sourcePath } = file;
+ // read the file as a string
+ const data = readFileSync(sourcePath, 'utf8');
+ // split the string into an array of lines
+ return MoveParsedFile(file, Directory.csv, undefined, data);
+ // console.log(csvParser(data));
+
+ }
+
const manualSuffixes = [".webm"];
async function UploadAudio(file: File, format: string) {
diff --git a/src/server/DataVizUtils.ts b/src/server/DataVizUtils.ts
new file mode 100644
index 000000000..4fd0ca6ff
--- /dev/null
+++ b/src/server/DataVizUtils.ts
@@ -0,0 +1,13 @@
+export function csvParser(csv: string) {
+ const lines = csv.split("\n");
+ const headers = lines[0].split(",");
+ const data = lines.slice(1).map(line => {
+ const values = line.split(",");
+ const obj: any = {};
+ for (let i = 0; i < headers.length; i++) {
+ obj[headers[i]] = values[i];
+ }
+ return obj;
+ });
+ return data;
+} \ No newline at end of file