diff options
43 files changed, 831 insertions, 262 deletions
diff --git a/src/.DS_Store b/src/.DS_Store Binary files differindex 071dafa1e..c544bc837 100644 --- a/src/.DS_Store +++ b/src/.DS_Store diff --git a/src/client/cognitive_services/CognitiveServices.ts b/src/client/cognitive_services/CognitiveServices.ts index bbc438a9b..076907f09 100644 --- a/src/client/cognitive_services/CognitiveServices.ts +++ b/src/client/cognitive_services/CognitiveServices.ts @@ -42,7 +42,7 @@ export enum Confidence { */ export namespace CognitiveServices { - const executeQuery = async <D, R>(service: Service, manager: APIManager<D>, data: D): Promise<Opt<R>> => { + const ExecuteQuery = async <D, R>(service: Service, manager: APIManager<D>, data: D): Promise<Opt<R>> => { return fetch(Utils.prepend(`${RouteStore.cognitiveServices}/${service}`)).then(async response => { let apiKey = await response.text(); if (!apiKey) { @@ -103,15 +103,15 @@ export namespace CognitiveServices { return request.post(options); }, - analyzer: async (target: Doc, keys: string[], service: Service, converter: Converter) => { + analyzer: async (target: Doc, keys: string[], url: string, service: Service, converter: Converter) => { let batch = UndoManager.StartBatch("Image Analysis"); - let imageData = Cast(target.data, ImageField); + let storageKey = keys[0]; - if (!imageData || await Cast(target[storageKey], Doc)) { + if (!url || await Cast(target[storageKey], Doc)) { return; } let toStore: any; - let results = await executeQuery<string, any>(service, Manager, imageData.url.href); + let results = await ExecuteQuery<string, any>(service, Manager, url); if (!results) { toStore = "Cognitive Services could not process the given image URL."; } else { @@ -122,6 +122,7 @@ export namespace CognitiveServices { } } target[storageKey] = toStore; + batch.end(); } @@ -129,31 +130,6 @@ export namespace CognitiveServices { export type Face = { faceAttributes: any, faceId: string, faceRectangle: Rectangle }; - export const generateMetadata = async (target: Doc, threshold: Confidence = Confidence.Excellent) => { - let converter = (results: any) => { - let tagDoc = new Doc; - results.tags.map((tag: Tag) => { - let sanitized = tag.name.replace(" ", "_"); - let script = `return (${tag.confidence} >= this.confidence) ? ${tag.confidence} : "${ComputedField.undefined}"`; - let computed = CompileScript(script, { params: { this: "Doc" } }); - computed.compiled && (tagDoc[sanitized] = new ComputedField(computed)); - }); - tagDoc.title = "Generated Tags"; - tagDoc.confidence = threshold; - return tagDoc; - }; - Manager.analyzer(target, ["generatedTags"], Service.ComputerVision, converter); - }; - - export const extractFaces = async (target: Doc) => { - let converter = (results: any) => { - let faceDocs = new List<Doc>(); - results.map((face: Face) => faceDocs.push(Docs.Get.DocumentHierarchyFromJson(face, `Face: ${face.faceId}`)!)); - return faceDocs; - }; - Manager.analyzer(target, ["faces"], Service.Face, converter); - }; - } export namespace Inking { @@ -209,7 +185,8 @@ export namespace CognitiveServices { analyzer: async (target: Doc, keys: string[], inkData: InkData) => { let batch = UndoManager.StartBatch("Ink Analysis"); - let results = await executeQuery<InkData, any>(Service.Handwriting, Manager, inkData); + + let results = await ExecuteQuery<InkData, any>(Service.Handwriting, Manager, inkData); if (results) { results.recognitionUnits && (results = results.recognitionUnits); target[keys[0]] = Docs.Get.DocumentHierarchyFromJson(results, "Ink Analysis"); @@ -217,6 +194,7 @@ export namespace CognitiveServices { let individualWords = recognizedText.filter((text: string) => text && text.split(" ").length === 1); target[keys[1]] = individualWords.join(" "); } + batch.end(); } diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index ee1b9fd0d..d1b3071ed 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -82,6 +82,7 @@ export interface DocumentOptions { backgroundColor?: string; dropAction?: dropActionType; backgroundLayout?: string; + chromeStatus?: string; curPage?: number; documentText?: string; borderRounding?: string; @@ -164,7 +165,6 @@ export namespace Docs { [DocumentType.LINKDOC, { data: new List<Doc>(), layout: { view: EmptyBox }, - options: {} }], [DocumentType.BUTTON, { layout: { view: ButtonBox }, @@ -404,23 +404,23 @@ export namespace Docs { } export function FreeformDocument(documents: Array<Doc>, options: DocumentOptions) { - return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { schemaColumns: new List([new SchemaHeaderField("title")]), ...options, viewType: CollectionViewType.Freeform }); + return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { chromeStatus: "collapsed", schemaColumns: new List([new SchemaHeaderField("title")]), ...options, viewType: CollectionViewType.Freeform }); } export function SchemaDocument(schemaColumns: SchemaHeaderField[], documents: Array<Doc>, options: DocumentOptions) { - return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { schemaColumns: new List(schemaColumns), ...options, viewType: CollectionViewType.Schema }); + return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { chromeStatus: "collapsed", schemaColumns: new List(schemaColumns), ...options, viewType: CollectionViewType.Schema }); } export function TreeDocument(documents: Array<Doc>, options: DocumentOptions) { - return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { schemaColumns: new List([new SchemaHeaderField("title")]), ...options, viewType: CollectionViewType.Tree }); + return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { chromeStatus: "collapsed", schemaColumns: new List([new SchemaHeaderField("title")]), ...options, viewType: CollectionViewType.Tree }); } export function StackingDocument(documents: Array<Doc>, options: DocumentOptions) { - return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { schemaColumns: new List([new SchemaHeaderField("title")]), ...options, viewType: CollectionViewType.Stacking }); + return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { chromeStatus: "collapsed", schemaColumns: new List([new SchemaHeaderField("title")]), ...options, viewType: CollectionViewType.Stacking }); } export function MasonryDocument(documents: Array<Doc>, options: DocumentOptions) { - return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { schemaColumns: new List([new SchemaHeaderField("title")]), ...options, viewType: CollectionViewType.Masonry }); + return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { chromeStatus: "collapsed", schemaColumns: new List([new SchemaHeaderField("title")]), ...options, viewType: CollectionViewType.Masonry }); } export function ButtonDocument(options?: DocumentOptions) { diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index 9221ef274..abcc3a4e1 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -216,6 +216,7 @@ export namespace DragManager { this.annotationDocument = annotationDoc; this.xOffset = this.yOffset = 0; } + targetContext: Doc | undefined; dragDocument: Doc; annotationDocument: Doc; dropDocument: Doc; diff --git a/src/client/views/ContextMenuItem.tsx b/src/client/views/ContextMenuItem.tsx index badb9cf19..a1787e78f 100644 --- a/src/client/views/ContextMenuItem.tsx +++ b/src/client/views/ContextMenuItem.tsx @@ -4,12 +4,14 @@ import { observer } from "mobx-react"; import { IconProp, library } from '@fortawesome/fontawesome-svg-core'; import { faAngleRight } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { UndoManager } from "../util/UndoManager"; library.add(faAngleRight); export interface OriginalMenuProps { description: string; event: () => void; + undoable?: boolean; icon: IconProp; //maybe should be optional (icon?) closeMenu?: () => void; } @@ -35,9 +37,14 @@ export class ContextMenuItem extends React.Component<ContextMenuProps & { select } } - handleEvent = (e: React.MouseEvent<HTMLDivElement>) => { + handleEvent = async (e: React.MouseEvent<HTMLDivElement>) => { if ("event" in this.props) { - this.props.event(); + let batch: UndoManager.Batch | undefined; + if (this.props.undoable !== false) { + batch = UndoManager.StartBatch(`Context menu event: ${this.props.description}`); + } + await this.props.event(); + batch && batch.end(); this.props.closeMenu && this.props.closeMenu(); } } @@ -94,7 +101,7 @@ export class ContextMenuItem extends React.Component<ContextMenuProps & { select ) : null} <div className="contextMenu-description"> {this.props.description} - <FontAwesomeIcon icon={faAngleRight} size="lg" style={{ position: "absolute", right: "10px"}} /> + <FontAwesomeIcon icon={faAngleRight} size="lg" style={{ position: "absolute", right: "10px" }} /> </div> {submenu} </div> diff --git a/src/client/views/MainOverlayTextBox.tsx b/src/client/views/MainOverlayTextBox.tsx index b2fb11afe..72eb956e3 100644 --- a/src/client/views/MainOverlayTextBox.tsx +++ b/src/client/views/MainOverlayTextBox.tsx @@ -4,7 +4,7 @@ import "normalize.css"; import * as React from 'react'; import { Doc } from '../../new_fields/Doc'; import { BoolCast } from '../../new_fields/Types'; -import { emptyFunction, returnTrue, returnZero, Utils } from '../../Utils'; +import { emptyFunction, returnTrue, returnZero, Utils, returnOne } from '../../Utils'; import { DragManager } from '../util/DragManager'; import { Transform } from '../util/Transform'; import { CollectionDockingView } from './collections/CollectionDockingView'; @@ -30,6 +30,7 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps> private _outerdiv: HTMLElement | null = null; private _textBox: FormattedTextBox | undefined; private _tooltip?: HTMLElement; + ChromeHeight?: () => number; @observable public TextDoc?: Doc; @observable public TextDataDoc?: Doc; @@ -50,6 +51,7 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps> (box?: FormattedTextBox) => { this._textBox = box; if (box) { + this.ChromeHeight = box.props.ChromeHeight; this.TextDoc = box.props.Document; this.TextDataDoc = box.props.DataDoc; let xf = () => { @@ -141,8 +143,8 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps> Document={FormattedTextBox.InputBoxOverlay.props.Document} DataDoc={FormattedTextBox.InputBoxOverlay.props.DataDoc} isSelected={returnTrue} select={emptyFunction} renderDepth={0} selectOnLoad={true} - ContainingCollectionView={undefined} whenActiveChanged={emptyFunction} active={returnTrue} - ScreenToLocalTransform={this._textXf} PanelWidth={returnZero} PanelHeight={returnZero} focus={emptyFunction} addDocTab={this.addDocTab} firstinstance={this.props.firstinstance} outer_div={(tooltip: HTMLElement) => { this._tooltip = tooltip; this.updateTooltip(); }} /> + ContainingCollectionView={undefined} whenActiveChanged={emptyFunction} active={returnTrue} ContentScaling={returnOne} + ScreenToLocalTransform={this._textXf} PanelWidth={returnZero} PanelHeight={returnZero} focus={emptyFunction} addDocTab={this.addDocTab} outer_div={(tooltip: HTMLElement) => { this._tooltip = tooltip; this.updateTooltip(); }} /> </div> </div> </div> diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 80b674092..1726b328f 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -92,6 +92,8 @@ export class MainView extends React.Component { componentWillUnMount() { window.removeEventListener("keydown", KeyManager.Instance.handle); + window.removeEventListener("pointerdown", this.globalPointerDown); + window.removeEventListener("pointerup", this.globalPointerUp); } constructor(props: Readonly<{}>) { @@ -138,18 +140,23 @@ export class MainView extends React.Component { this.initAuthenticationRouters(); } + globalPointerDown = action((e: PointerEvent) => { + this.isPointerDown = true; + const targets = document.elementsFromPoint(e.x, e.y); + if (targets && targets.length && targets[0].className.toString().indexOf("contextMenu") === -1) { + ContextMenu.Instance.closeMenu(); + } + }); + + globalPointerUp = () => this.isPointerDown = false; + initEventListeners = () => { // window.addEventListener("pointermove", (e) => this.reportLocation(e)) window.addEventListener("drop", (e) => e.preventDefault(), false); // drop event handler window.addEventListener("dragover", (e) => e.preventDefault(), false); // drag event handler // click interactions for the context menu - document.addEventListener("pointerdown", action((e: PointerEvent) => { - this.isPointerDown = true; - const targets = document.elementsFromPoint(e.x, e.y); - if (targets && targets.length && targets[0].className.toString().indexOf("contextMenu") === -1) { - ContextMenu.Instance.closeMenu(); - } - }), true); + document.addEventListener("pointerdown", this.globalPointerDown); + document.addEventListener("pointerup", this.globalPointerUp); } initAuthenticationRouters = async () => { @@ -292,7 +299,6 @@ export class MainView extends React.Component { } @action onPointerUp = (e: PointerEvent) => { - this.isPointerDown = false; if (Math.abs(e.clientX - this._downsize) < 4) { if (this.flyoutWidth < 5) this.flyoutWidth = 250; else this.flyoutWidth = 0; diff --git a/src/client/views/MetadataEntryMenu.tsx b/src/client/views/MetadataEntryMenu.tsx index 652e0e91a..36c240dd8 100644 --- a/src/client/views/MetadataEntryMenu.tsx +++ b/src/client/views/MetadataEntryMenu.tsx @@ -5,6 +5,7 @@ import { observable, action, runInAction, trace } from 'mobx'; import { KeyValueBox } from './nodes/KeyValueBox'; import { Doc, Field } from '../../new_fields/Doc'; import * as Autosuggest from 'react-autosuggest'; +import { undoBatch } from '../util/UndoManager'; export type DocLike = Doc | Doc[] | Promise<Doc> | Promise<Doc[]>; export interface MetadataEntryProps { @@ -74,6 +75,7 @@ export class MetadataEntryMenu extends React.Component<MetadataEntryProps>{ this.userModified = e.target.value.trim() !== ""; } + @undoBatch @action onValueKeyDown = async (e: React.KeyboardEvent) => { if (e.key === "Enter") { diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 1859ebee7..588102f01 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -210,8 +210,23 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp docs.push(document); } let docContentConfig = CollectionDockingView.makeDocumentConfig(document, dataDocument); - var newContentItem = stack.layoutManager.createContentItem(docContentConfig, this._goldenLayout); - stack.addChild(newContentItem.contentItems[0], undefined); + if (stack === undefined) { + let stack: any = this._goldenLayout.root; + while (!stack.isStack) { + if (stack.contentItems.length) { + stack = stack.contentItems[0]; + } else { + stack.addChild({ type: 'stack', content: [docContentConfig] }); + stack = undefined; + break; + } + } + if (stack) { + stack.addChild(docContentConfig); + } + } else { + stack.addChild(docContentConfig, undefined); + } this.layoutChanged(); } @@ -561,7 +576,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { } return Transform.Identity(); } - get previewPanelCenteringOffset() { return this.nativeWidth && !BoolCast(this._document!.ignoreAspect) ? (this._panelWidth - this.nativeWidth() * this.contentScaling()) / 2 : 0; } + get previewPanelCenteringOffset() { return this.nativeWidth && !BoolCast(this._document!.ignoreAspect) ? (this._panelWidth - this.nativeWidth()) / 2 : 0; } addDocTab = (doc: Doc, dataDoc: Doc | undefined, location: string) => { if (doc.dockingConfig) { diff --git a/src/client/views/collections/CollectionSchemaCells.tsx b/src/client/views/collections/CollectionSchemaCells.tsx index 194765880..fdf0896ac 100644 --- a/src/client/views/collections/CollectionSchemaCells.tsx +++ b/src/client/views/collections/CollectionSchemaCells.tsx @@ -115,7 +115,7 @@ export class CollectionSchemaCell extends React.Component<CellProps> { } } - private dropRef = (ele: HTMLElement) => { + private dropRef = (ele: HTMLElement | null) => { this._dropDisposer && this._dropDisposer(); if (ele) { this._dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } }); @@ -154,6 +154,7 @@ export class CollectionSchemaCell extends React.Component<CellProps> { PanelHeight: returnZero, PanelWidth: returnZero, addDocTab: this.props.addDocTab, + ContentScaling: returnOne }; let field = props.Document[props.fieldKey]; diff --git a/src/client/views/collections/CollectionSchemaHeaders.tsx b/src/client/views/collections/CollectionSchemaHeaders.tsx index 9fc28eafa..387107c55 100644 --- a/src/client/views/collections/CollectionSchemaHeaders.tsx +++ b/src/client/views/collections/CollectionSchemaHeaders.tsx @@ -129,6 +129,10 @@ export class CollectionSchemaColumnMenu extends React.Component<ColumnMenuProps> } } + changeColumnColor = (color: string): void => { + + } + renderTypes = () => { if (this.props.typeConst) return <></>; return ( @@ -168,6 +172,40 @@ export class CollectionSchemaColumnMenu extends React.Component<ColumnMenuProps> ); } + renderColors = () => { + return ( + <div className="collectionSchema-headerMenu-group"> + <label>Color:</label> + <div className="columnMenu-colors"> + <input type="radio" name="column-color" id="pink" value="#FFB4E8" onClick={() => this.changeColumnColor("#FFB4E8")} /> + <label htmlFor="pink"> + <div className="columnMenu-colorPicker" style={{ backgroundColor: "#FFB4E8" }}></div> + </label> + <input type="radio" name="column-color" id="purple" value="#b28dff" onClick={() => this.changeColumnColor("#b28dff")} /> + <label htmlFor="purple"> + <div className="columnMenu-colorPicker" style={{ backgroundColor: "#FFB4E8" }}></div> + </label> + <input type="radio" name="column-color" id="blue" value="#afcbff" onClick={() => this.changeColumnColor("#afcbff")} /> + <label htmlFor="blue"> + <div className="columnMenu-colorPicker" style={{ backgroundColor: "#FFB4E8" }}></div> + </label> + <input type="radio" name="column-color" id="yellow" value="#f3ffe3" onClick={() => this.changeColumnColor("#f3ffe3")} /> + <label htmlFor="yellow"> + <div className="columnMenu-colorPicker" style={{ backgroundColor: "#FFB4E8" }}></div> + </label> + <input type="radio" name="column-color" id="red" value="#ffc9de" onClick={() => this.changeColumnColor("#ffc9de")} /> + <label htmlFor="red"> + <div className="columnMenu-colorPicker" style={{ backgroundColor: "#FFB4E8" }}></div> + </label> + <input type="radio" name="column=color" id="none" value="#f1efeb" onClick={() => this.changeColumnColor("#f1efeb")} /> + <label htmlFor="none"> + <div className="columnMenu-colorPicker" style={{ backgroundColor: "#FFB4E8" }}></div> + </label> + </div> + </div> + ); + } + renderContent = () => { return ( <div className="collectionSchema-header-menuOptions"> @@ -187,6 +225,7 @@ export class CollectionSchemaColumnMenu extends React.Component<ColumnMenuProps> <> {this.renderTypes()} {this.renderSorting()} + {this.renderColors()} <div className="collectionSchema-headerMenu-group"> <button onClick={() => this.props.deleteColumn(this.props.keyValue)}>Delete Column</button> </div> diff --git a/src/client/views/collections/CollectionSchemaView.scss b/src/client/views/collections/CollectionSchemaView.scss index e0de76247..b1e98b162 100644 --- a/src/client/views/collections/CollectionSchemaView.scss +++ b/src/client/views/collections/CollectionSchemaView.scss @@ -6,14 +6,15 @@ border-style: solid; border-radius: $border-radius; box-sizing: border-box; - // position: absolute; + position: absolute; + top: 0; width: 100%; - height: calc(100% - 50px); + transition: height .5s; + height: 100%; // overflow: hidden; // overflow-x: scroll; // border: none; overflow: hidden; - transition: top 0.5s; // .collectionSchemaView-cellContents { // height: $MAX_ROW_HEIGHT; @@ -78,7 +79,7 @@ overflow-x: auto; height: 100%; display: -webkit-inline-box; - direction: ltr; + direction: ltr; } .rt-thead { @@ -122,7 +123,7 @@ font-size: 13px; text-align: center; background-color: $light-color-secondary; - + &:last-child { overflow: visible; } @@ -147,7 +148,7 @@ // &:nth-child(even) { // background-color: $light-color; // } - + // &:nth-child(odd) { // background-color: $light-color-secondary; // } @@ -175,7 +176,7 @@ padding: 0; font-size: 13px; text-align: center; - + // white-space: normal; .imageBox-cont { @@ -195,6 +196,11 @@ height: 100%; } } + + .rt-resizer { + width: 20px; + right: -10px; + } } .documentView-node-topmost { @@ -207,18 +213,19 @@ background: $light-color; } -.collectionSchema-col{ +.collectionSchema-col { height: 100%; .collectionSchema-col-wrapper { &.col-before { border-left: 2px solid red; } + &.col-after { border-right: 2px solid red; } } -} +} .collectionSchemaView-header { @@ -285,7 +292,7 @@ button.add-column { background-color: $light-color; border: 1px solid $light-color-secondary; padding: 2px 3px; - + &:not(:last-child) { border-top: 0; } @@ -305,6 +312,19 @@ button.add-column { border-radius: 20px; } } + + .columnMenu-colors { + + + input[type="radio"] { + display: none; + } + + .columnMenu-colorPicker { + width: 20px; + height: 20px; + } + } } .collectionSchema-row { @@ -312,8 +332,8 @@ button.add-column { height: 100%; background-color: white; - &.row-focused { - background-color: rgb(255, 246, 246);//$light-color-secondary; + &.row-focused .rt-tr { + background-color: rgb(255, 246, 246); //$light-color-secondary; } &.row-wrapped { @@ -358,9 +378,11 @@ button.add-column { &.row-above { border-top: 1px solid red; } + &.row-below { border-bottom: 1px solid red; } + &.row-inside { border: 1px solid red; } @@ -461,7 +483,9 @@ button.add-column { .rt-table { overflow-x: hidden; // todo; this shouldnt be like this :(( overflow-y: visible; - } // TODO fix + } + + // TODO fix .row-dragger { background-color: rgb(252, 252, 252); diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index 8436b22a4..08ab22725 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -72,7 +72,6 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { @observable private _node: HTMLDivElement | null = null; @observable private _focusedTable: Doc = this.props.Document; - @computed get chromeCollapsed() { return this.props.chromeCollapsed; } @computed get previewWidth() { return () => NumCast(this.props.Document.schemaPreviewWidth); } @computed get previewHeight() { return () => this.props.PanelHeight() - 2 * this.borderWidth; } @computed get tableWidth() { return this.props.PanelWidth() - 2 * this.borderWidth - this.DIVIDER_WIDTH - this.previewWidth(); } @@ -199,13 +198,13 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { get schemaTable() { return ( <SchemaTable - Document={this.props.Document} // child doc + Document={this.props.Document} PanelHeight={this.props.PanelHeight} PanelWidth={this.props.PanelWidth} childDocs={this.childDocs} CollectionView={this.props.CollectionView} ContainingCollectionView={this.props.ContainingCollectionView} - fieldKey={this.props.fieldKey} // might just be this. + fieldKey={this.props.fieldKey} renderDepth={this.props.renderDepth} moveDocument={this.props.moveDocument} ScreenToLocalTransform={this.props.ScreenToLocalTransform} @@ -234,11 +233,12 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { } render() { + Doc.UpdateDocumentExtensionForField(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey); // if (SelectionManager.SelectedDocuments().length > 0) console.log(StrCast(SelectionManager.SelectedDocuments()[0].Document.title)); // if (DocumentManager.Instance.getDocumentView(this.props.Document)) console.log(StrCast(this.props.Document.title), SelectionManager.IsSelected(DocumentManager.Instance.getDocumentView(this.props.Document)!)) return ( - <div className="collectionSchemaView-container" onPointerDown={this.onPointerDown} onWheel={this.onWheel} - onDrop={(e: React.DragEvent) => this.onDrop(e, {})} ref={this.createTarget}> + <div className="collectionSchemaView-container" + onPointerDown={this.onPointerDown} onWheel={this.onWheel} onDrop={(e: React.DragEvent) => this.onDrop(e, {})} ref={this.createTarget}> {this.schemaTable} {this.dividerDragger} {!this.previewWidth() ? (null) : this.previewPanel} @@ -252,7 +252,7 @@ export interface SchemaTableProps { dataDoc?: Doc; PanelHeight: () => number; PanelWidth: () => number; - childDocs: Doc[]; + childDocs?: Doc[]; CollectionView: CollectionView | CollectionPDFView | CollectionVideoView; ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>; fieldKey: string; @@ -287,9 +287,19 @@ export class SchemaTable extends React.Component<SchemaTableProps> { @computed get previewHeight() { return () => this.props.PanelHeight() - 2 * this.borderWidth; } @computed get tableWidth() { return this.props.PanelWidth() - 2 * this.borderWidth - this.DIVIDER_WIDTH - this.previewWidth(); } @computed get columns() { + console.log("columns"); return Cast(this.props.Document.schemaColumns, listSpec(SchemaHeaderField), []); } - @computed get childDocs() { return this.props.childDocs; } + @computed get childDocs() { + if (this.props.childDocs) return this.props.childDocs; + + let doc = this.props.dataDoc ? this.props.dataDoc : this.props.Document; + return DocListCast(doc[this.props.fieldKey]); + } + set childDocs(docs: Doc[]) { + let doc = this.props.dataDoc ? this.props.dataDoc : this.props.Document; + doc[this.props.fieldKey] = new List<Doc>(docs); + } set columns(columns: SchemaHeaderField[]) { this.props.Document.schemaColumns = new List<SchemaHeaderField>(columns); } @computed get borderWidth() { return Number(COLLECTION_BORDER_WIDTH); } @computed get tableColumns(): Column<Doc>[] { @@ -406,8 +416,9 @@ export class SchemaTable extends React.Component<SchemaTableProps> { super(props); // convert old schema columns (list of strings) into new schema columns (list of schema header fields) let oldSchemaColumns = Cast(this.props.Document.schemaColumns, listSpec("string"), []); - if (oldSchemaColumns && oldSchemaColumns.length) { - let newSchemaColumns = oldSchemaColumns.map(i => typeof i === "string" ? new SchemaHeaderField(i) : i); + if (oldSchemaColumns && oldSchemaColumns.length && typeof oldSchemaColumns[0] !== "object") { + console.log("REMAKING COLUMNs"); + let newSchemaColumns = oldSchemaColumns.map(i => typeof i === "string" ? new SchemaHeaderField(i, "#f1efeb") : i); this.props.Document.schemaColumns = new List<SchemaHeaderField>(newSchemaColumns); } } @@ -425,11 +436,12 @@ export class SchemaTable extends React.Component<SchemaTableProps> { } tableRemoveDoc = (document: Doc): boolean => { - let doc = this.props.dataDoc ? this.props.dataDoc : this.props.Document; - let children = Cast(doc[this.props.fieldKey], listSpec(Doc), []); - // let children = this.childDocs; + // let doc = this.props.dataDoc ? this.props.dataDoc : this.props.Document; + // let children = Cast(doc[this.props.fieldKey], listSpec(Doc), []); + let children = this.childDocs; if (children.indexOf(document) !== -1) { children.splice(children.indexOf(document), 1); + this.childDocs = children; return true; } return false; @@ -521,7 +533,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> { let direction = e.key === "Tab" ? "tab" : e.which === 39 ? "right" : e.which === 37 ? "left" : e.which === 38 ? "up" : e.which === 40 ? "down" : ""; this.changeFocusedCellByDirection(direction); - let doc = this.props.dataDoc ? this.props.dataDoc : this.props.Document; + // let doc = this.props.dataDoc ? this.props.dataDoc : this.props.Document; // let children = Cast(doc[this.props.fieldKey], listSpec(Doc), []); let children = this.childDocs; const pdoc = FieldValue(children[this._focusedCell.row]); @@ -531,7 +543,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> { @action changeFocusedCellByDirection = (direction: string): void => { - let doc = this.props.dataDoc ? this.props.dataDoc : this.props.Document; + // let doc = this.props.dataDoc ? this.props.dataDoc : this.props.Document; // let children = Cast(doc[this.props.fieldKey], listSpec(Doc), []); let children = this.childDocs; switch (direction) { @@ -563,7 +575,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> { @action changeFocusedCellByIndex = (row: number, col: number): void => { - let doc = this.props.dataDoc ? this.props.dataDoc : this.props.Document; + // let doc = this.props.dataDoc ? this.props.dataDoc : this.props.Document; // let children = Cast(doc[this.props.fieldKey], listSpec(Doc), []); this._focusedCell = { row: row, col: col }; @@ -574,7 +586,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> { } createRow = () => { - let doc = this.props.dataDoc ? this.props.dataDoc : this.props.Document; + // let doc = this.props.dataDoc ? this.props.dataDoc : this.props.Document; // let children = Cast(doc[this.props.fieldKey], listSpec(Doc), []); let children = this.childDocs; @@ -582,6 +594,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> { let proto = Doc.GetProto(newDoc); proto.title = ""; children.push(newDoc); + this.childDocs = children; } @action @@ -589,20 +602,24 @@ export class SchemaTable extends React.Component<SchemaTableProps> { let index = 0; let found = this.columns.findIndex(col => col.heading.toUpperCase() === "New field".toUpperCase()) > -1; if (!found) { - this.columns.push(new SchemaHeaderField("New field")); + console.log("create column found"); + this.columns.push(new SchemaHeaderField("New field", "#f1efeb")); return; } while (found) { index++; found = this.columns.findIndex(col => col.heading.toUpperCase() === ("New field (" + index + ")").toUpperCase()) > -1; } - this.columns.push(new SchemaHeaderField("New field (" + index + ")")); + console.log("create column new"); + this.columns.push(new SchemaHeaderField("New field (" + index + ")", "#f1efeb")); } @action deleteColumn = (key: string) => { + console.log("deleting columnnn"); let list = Cast(this.props.Document.schemaColumns, listSpec(SchemaHeaderField)); if (list === undefined) { + console.log("delete column"); this.props.Document.schemaColumns = list = new List<SchemaHeaderField>([]); } else { const index = list.map(c => c.heading).indexOf(key); @@ -614,16 +631,19 @@ export class SchemaTable extends React.Component<SchemaTableProps> { @action changeColumns = (oldKey: string, newKey: string, addNew: boolean) => { + console.log("changingin columnsdfhs"); let list = Cast(this.props.Document.schemaColumns, listSpec(SchemaHeaderField)); if (list === undefined) { - this.props.Document.schemaColumns = list = new List<SchemaHeaderField>([new SchemaHeaderField(newKey)]); + console.log("change columns new"); + this.props.Document.schemaColumns = list = new List<SchemaHeaderField>([new SchemaHeaderField(newKey, "f1efeb")]); } else { + console.log("change column"); if (addNew) { - this.columns.push(new SchemaHeaderField(newKey)); + this.columns.push(new SchemaHeaderField(newKey, "f1efeb")); } else { const index = list.map(c => c.heading).indexOf(oldKey); if (index > -1) { - list[index] = new SchemaHeaderField(newKey); + list[index] = new SchemaHeaderField(newKey, "f1efeb"); } } } @@ -688,8 +708,9 @@ export class SchemaTable extends React.Component<SchemaTableProps> { } get documentKeys() { - // const docs = DocListCast(this.props.Document[this.props.fieldKey]); - let docs = this.childDocs; + const docs = DocListCast(this.props.Document[this.props.fieldKey]); + + // let docs = this.childDocs; let keys: { [key: string]: boolean } = {}; // bcz: ugh. this is untracked since otherwise a large collection of documents will blast the server for all their fields. // then as each document's fields come back, we update the documents _proxies. Each time we do this, the whole schema will be @@ -717,7 +738,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> { @computed get reactTable() { - let cdoc = this.props.dataDoc ? this.props.dataDoc : this.props.Document; + // let cdoc = this.props.dataDoc ? this.props.dataDoc : this.props.Document; // let children = DocListCast(cdoc[this.props.fieldKey]); let children = this.childDocs; @@ -731,7 +752,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> { return <ReactTable style={{ position: "relative", float: "left", width: `calc(100% - ${previewWidth}px` }} - data={this.childDocs} + data={children} page={0} pageSize={children.length} showPagination={false} @@ -746,7 +767,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> { row => { if (row.original.type === "collection") { // let childDocs = DocListCast(row.original[this.props.fieldKey]); - return <div className="sub"><SchemaTable {...this.props} Document={row.original} /></div>; + return <div className="sub"><SchemaTable {...this.props} Document={row.original} childDocs={undefined} /></div>; } } : undefined} @@ -765,6 +786,8 @@ export class SchemaTable extends React.Component<SchemaTableProps> { let csv: string = this.columns.reduce((val, col) => val + col + ",", ""); csv = csv.substr(0, csv.length - 1) + "\n"; let self = this; + let cdoc = this.props.dataDoc ? this.props.dataDoc : this.props.Document; + // let children = DocListCast(cdoc[this.props.fieldKey]); this.childDocs.map(doc => { csv += self.columns.reduce((val, col) => val + (doc[col.heading] ? doc[col.heading]!.toString() : "0") + ",", ""); csv = csv.substr(0, csv.length - 1) + "\n"; @@ -785,8 +808,8 @@ export class SchemaTable extends React.Component<SchemaTableProps> { // const docs = DocListCast(this.props.Document[this.props.fieldKey]); let cdoc = this.props.dataDoc ? this.props.dataDoc : this.props.Document; - // const docs = DocListCast(cdoc[this.props.fieldKey]); - let docs = this.childDocs; + const docs = DocListCast(cdoc[this.props.fieldKey]); + // let docs = this.childDocs; row = row % docs.length; while (row < 0) row += docs.length; diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss index 9dbe4ccb8..0cb01dc9d 100644 --- a/src/client/views/collections/CollectionStackingView.scss +++ b/src/client/views/collections/CollectionStackingView.scss @@ -5,6 +5,7 @@ width: 100%; position: absolute; display: flex; + top: 0; overflow-y: auto; flex-wrap: wrap; transition: top .5s; @@ -73,6 +74,7 @@ transform-origin: top left; grid-column-end: span 1; height: 100%; + margin: auto; } .collectionStackingView-sectionHeader { @@ -133,9 +135,9 @@ .collectionStackingView-addDocumentButton, .collectionStackingView-addGroupButton { - display: inline-block; - margin: 0 5px; + display: flex; overflow: hidden; + margin: auto; width: 90%; color: lightgrey; overflow: ellipses; @@ -144,6 +146,7 @@ .editableView-container-editing { color: grey; padding: 10px; + width: 100%; } .editableView-input:hover, diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index f647da8f0..bcf3a85d7 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -276,9 +276,11 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { SetValue: this.addGroup, contents: "+ ADD A GROUP" }; + Doc.UpdateDocumentExtensionForField(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey); + // let uniqueHeadings = headings.map((i, idx) => headings.indexOf(i) === idx); return ( - <div className="collectionStackingView" style={{ top: this.chromeCollapsed ? 0 : 100 }} + <div className="collectionStackingView" ref={this.createRef} onDrop={this.onDrop.bind(this)} onWheel={(e: React.WheelEvent) => e.stopPropagation()} > {/* {sectionFilter as boolean ? [ ["width > height", this.filteredChildren.filter(f => f[WidthSym]() >= 1 + f[HeightSym]())], @@ -287,9 +289,9 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { {this.props.Document.sectionFilter ? Array.from(this.Sections.entries()).sort(this.sortFunc). map(section => this.section(section[0], section[1])) : this.section(undefined, this.filteredChildren)} - {this.props.Document.sectionFilter ? + {(this.props.Document.sectionFilter && this.props.CollectionView.props.Document.chromeStatus !== 'disabled') ? <div key={`${this.props.Document[Id]}-addGroup`} className="collectionStackingView-addGroupButton" - style={{ width: (this.columnWidth / (headings.length + 1)) - 10, marginTop: 10 }}> + style={{ width: (this.columnWidth / (headings.length + (this.props.CollectionView.props.Document.chromeStatus !== 'disabled' ? 1 : 0))) - 10, marginTop: 10 }}> <EditableView {...editableViewProps} /> </div> : null} </div> diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx index 387e189e7..d8bed7e88 100644 --- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx +++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx @@ -34,7 +34,7 @@ interface CSVFieldColumnProps { @observer export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldColumnProps> { - @observable private _background = "white"; + @observable private _background = "inherit"; private _dropRef: HTMLDivElement | null = null; private dropDisposer?: DragManager.DragDropDisposer; @@ -111,7 +111,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC let outerXf = Utils.GetScreenTransform(this.props.parent._masonryGridRef!); let offset = this.props.parent.props.ScreenToLocalTransform().transformDirection(outerXf.translateX - translateX, outerXf.translateY - translateY); return this.props.parent.props.ScreenToLocalTransform(). - translate(offset[0], offset[1] - (this.props.parent.chromeCollapsed ? 0 : 100)). + translate(offset[0], offset[1]). scale(NumCast(doc.width, 1) / this.props.parent.columnWidth); } @@ -158,7 +158,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC @action pointerLeave = () => { - this._background = "white"; + this._background = "inherit"; } @action @@ -239,7 +239,11 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC }; let headingView = this.props.headingObject ? <div key={heading} className="collectionStackingView-sectionHeader" ref={this._headerRef} - style={{ width: (style.columnWidth) / (uniqueHeadings.length + 1) }}> + style={{ + width: (style.columnWidth) / + ((uniqueHeadings.length + + (this.props.parent.props.CollectionView.props.Document.chromeStatus !== 'disabled' ? 1 : 0)) || 1) + }}> {/* the default bucket (no key value) has a tooltip that describes what it is. Further, it does not have a color and cannot be deleted. */} <div className="collectionStackingView-sectionHeader-subCont" onPointerDown={this.headerDown} @@ -261,7 +265,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC </div> : (null); for (let i = 0; i < cols; i++) templatecols += `${style.columnWidth}px `; return ( - <div key={heading} style={{ width: `${100 / (uniqueHeadings.length + 1)}%`, background: this._background }} + <div key={heading} style={{ width: `${100 / ((uniqueHeadings.length + (this.props.parent.props.CollectionView.props.Document.chromeStatus !== 'disabled' ? 1 : 0)) || 1)}%`, background: this._background }} ref={this.createColumnDropRef} onPointerEnter={this.pointerEntered} onPointerLeave={this.pointerLeave}> {headingView} <div key={`${heading}-stack`} className={`collectionStackingView-masonry${singleColumn ? "Single" : "Grid"}`} @@ -279,10 +283,11 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC {this.children(this.props.docList)} {singleColumn ? (null) : this.props.parent.columnDragger} </div> - <div key={`${heading}-add-document`} className="collectionStackingView-addDocumentButton" - style={{ width: style.columnWidth / (uniqueHeadings.length + 1) }}> - <EditableView {...newEditableViewProps} /> - </div> + {(this.props.parent.props.CollectionView.props.Document.chromeStatus !== 'disabled') ? + <div key={`${heading}-add-document`} className="collectionStackingView-addDocumentButton" + style={{ width: style.columnWidth / (uniqueHeadings.length + (this.props.parent.props.CollectionView.props.Document.chromeStatus !== 'disabled' ? 1 : 0)) }}> + <EditableView {...newEditableViewProps} /> + </div> : null} </div> ); } diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 4d31c3ae7..b1e6eada0 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -237,10 +237,10 @@ class TreeView extends React.Component<TreeViewProps> { if (DocumentManager.Instance.getDocumentViews(this.dataDoc).length) { ContextMenu.Instance.addItem({ description: "Focus", event: () => DocumentManager.Instance.getDocumentViews(this.dataDoc).map(view => view.props.focus(this.props.document, true)), icon: "camera" }); } - ContextMenu.Instance.addItem({ description: "Delete Item", event: undoBatch(() => this.props.deleteDoc(this.props.document)), icon: "trash-alt" }); + ContextMenu.Instance.addItem({ description: "Delete Item", event: () => this.props.deleteDoc(this.props.document), icon: "trash-alt" }); } else { - ContextMenu.Instance.addItem({ description: "Open as Workspace", event: undoBatch(() => MainView.Instance.openWorkspace(this.dataDoc)), icon: "caret-square-right" }); - ContextMenu.Instance.addItem({ description: "Delete Workspace", event: undoBatch(() => this.props.deleteDoc(this.props.document)), icon: "trash-alt" }); + ContextMenu.Instance.addItem({ description: "Open as Workspace", event: () => MainView.Instance.openWorkspace(this.dataDoc), icon: "caret-square-right" }); + ContextMenu.Instance.addItem({ description: "Delete Workspace", event: () => this.props.deleteDoc(this.props.document), icon: "trash-alt" }); } ContextMenu.Instance.addItem({ description: "Open Fields", event: () => { let kvp = Docs.Create.KVPDocument(this.props.document, { width: 300, height: 300 }); this.props.addDocTab(kvp, this.props.dataDoc ? this.props.dataDoc : kvp, "onRight"); }, icon: "layer-group" }); ContextMenu.Instance.displayMenu(e.pageX > 156 ? e.pageX - 156 : 0, e.pageY - 15); @@ -522,8 +522,8 @@ export class CollectionTreeView extends CollectionSubView(Document) { onContextMenu = (e: React.MouseEvent): void => { // need to test if propagation has stopped because GoldenLayout forces a parallel react hierarchy to be created for its top-level layout if (!e.isPropagationStopped() && this.props.Document.workspaceLibrary) { // excludeFromLibrary means this is the user document - ContextMenu.Instance.addItem({ description: "Create Workspace", event: undoBatch(() => MainView.Instance.createNewWorkspace()), icon: "plus" }); - ContextMenu.Instance.addItem({ description: "Delete Workspace", event: undoBatch(() => this.remove(this.props.Document)), icon: "minus" }); + ContextMenu.Instance.addItem({ description: "Create Workspace", event: () => MainView.Instance.createNewWorkspace(), icon: "plus" }); + ContextMenu.Instance.addItem({ description: "Delete Workspace", event: () => this.remove(this.props.Document), icon: "minus" }); e.stopPropagation(); e.preventDefault(); ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15); @@ -572,6 +572,7 @@ export class CollectionTreeView extends CollectionSubView(Document) { render() { + Doc.UpdateDocumentExtensionForField(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey); let dropAction = StrCast(this.props.Document.dropAction) as dropActionType; let addDoc = (doc: Doc, relativeTo?: Doc, before?: boolean) => Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, relativeTo, before); let moveDoc = (d: Doc, target: Doc, addDoc: (doc: Doc) => boolean) => this.props.moveDocument(d, target, addDoc); diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 81c84852a..b7ac8768f 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -18,7 +18,7 @@ import { CollectionTreeView } from "./CollectionTreeView"; import { StrCast, PromiseValue } from '../../../new_fields/Types'; import { DocumentType } from '../../documents/Documents'; import { CollectionStackingViewChrome, CollectionViewBaseChrome } from './CollectionViewChromes'; -import { observable, action, runInAction } from 'mobx'; +import { observable, action, runInAction, IReactionDisposer, reaction } from 'mobx'; import { faEye } from '@fortawesome/free-regular-svg-icons'; export const COLLECTION_BORDER_WIDTH = 2; @@ -35,16 +35,25 @@ library.add(faImage, faEye); @observer export class CollectionView extends React.Component<FieldViewProps> { - @observable private _collapsed = false; + @observable private _collapsed = true; + + private _reactionDisposer: IReactionDisposer | undefined; public static LayoutString(fieldStr: string = "data", fieldExt: string = "") { return FieldView.LayoutString(CollectionView, fieldStr, fieldExt); } componentDidMount = () => { - // chrome status is one of disabled, collapsed, or visible. this determines initial state from document - let chromeStatus = this.props.Document.chromeStatus; - if (chromeStatus && (chromeStatus === "disabled" || chromeStatus === "collapsed")) { - runInAction(() => this._collapsed = true); - } + this._reactionDisposer = reaction(() => StrCast(this.props.Document.chromeStatus), + () => { + // chrome status is one of disabled, collapsed, or visible. this determines initial state from document + let chromeStatus = this.props.Document.chromeStatus; + if (chromeStatus && (chromeStatus === "disabled" || chromeStatus === "collapsed")) { + runInAction(() => this._collapsed = true); + } + }); + } + + componentWillUnmount = () => { + this._reactionDisposer && this._reactionDisposer(); } private SubViewHelper = (type: CollectionViewType, renderProps: CollectionRenderProps) => { @@ -87,14 +96,14 @@ export class CollectionView extends React.Component<FieldViewProps> { onContextMenu = (e: React.MouseEvent): void => { if (!this.isAnnotationOverlay && !e.isPropagationStopped() && this.props.Document[Id] !== CurrentUserUtils.MainDocId) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7 let subItems: ContextMenuProps[] = []; - subItems.push({ description: "Freeform", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Freeform), icon: "signature" }); + subItems.push({ description: "Freeform", event: () => this.props.Document.viewType = CollectionViewType.Freeform, icon: "signature" }); if (CollectionBaseView.InSafeMode()) { - ContextMenu.Instance.addItem({ description: "Test Freeform", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Invalid), icon: "project-diagram" }); + ContextMenu.Instance.addItem({ description: "Test Freeform", event: () => this.props.Document.viewType = CollectionViewType.Invalid, icon: "project-diagram" }); } - subItems.push({ description: "Schema", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Schema), icon: "th-list" }); - subItems.push({ description: "Treeview", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Tree), icon: "tree" }); - subItems.push({ description: "Stacking", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Stacking), icon: "ellipsis-v" }); - subItems.push({ description: "Masonry", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Masonry), icon: "columns" }); + subItems.push({ description: "Schema", event: () => this.props.Document.viewType = CollectionViewType.Schema, icon: "th-list" }); + subItems.push({ description: "Treeview", event: () => this.props.Document.viewType = CollectionViewType.Tree, icon: "tree" }); + subItems.push({ description: "Stacking", event: () => this.props.Document.viewType = CollectionViewType.Stacking, icon: "ellipsis-v" }); + subItems.push({ description: "Masonry", event: () => this.props.Document.viewType = CollectionViewType.Masonry, icon: "columns" }); switch (this.props.Document.viewType) { case CollectionViewType.Freeform: { subItems.push({ description: "Custom", icon: "fingerprint", event: CollectionFreeFormView.AddCustomLayout(this.props.Document, this.props.fieldKey) }); @@ -102,7 +111,7 @@ export class CollectionView extends React.Component<FieldViewProps> { } } ContextMenu.Instance.addItem({ description: "View Modes...", subitems: subItems, icon: "eye" }); - ContextMenu.Instance.addItem({ description: "Apply Template", event: undoBatch(() => this.props.addDocTab && this.props.addDocTab(Doc.ApplyTemplate(this.props.Document)!, undefined, "onRight")), icon: "project-diagram" }); + ContextMenu.Instance.addItem({ description: "Apply Template", event: () => this.props.addDocTab && this.props.addDocTab(Doc.ApplyTemplate(this.props.Document)!, undefined, "onRight"), icon: "project-diagram" }); } } diff --git a/src/client/views/collections/CollectionViewChromes.scss b/src/client/views/collections/CollectionViewChromes.scss index 6525f3b07..989315194 100644 --- a/src/client/views/collections/CollectionViewChromes.scss +++ b/src/client/views/collections/CollectionViewChromes.scss @@ -3,16 +3,19 @@ .collectionViewChrome-cont { position: relative; + opacity: 0.9; z-index: 9001; transition: top .5s; - background: lightslategray; + background: lightgrey; + transition: margin-top .5s; + background: lightgray; padding: 10px; .collectionViewChrome { display: grid; grid-template-columns: 1fr auto; padding-bottom: 10px; - border-bottom: .5px solid lightgrey; + border-bottom: .5px solid rgb(180, 180, 180); .collectionViewBaseChrome { display: flex; diff --git a/src/client/views/collections/CollectionViewChromes.tsx b/src/client/views/collections/CollectionViewChromes.tsx index 9c751c4df..2bffe3cc0 100644 --- a/src/client/views/collections/CollectionViewChromes.tsx +++ b/src/client/views/collections/CollectionViewChromes.tsx @@ -185,11 +185,11 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro render() { return ( - <div className="collectionViewChrome-cont" style={{ top: this._collapsed ? -100 : 0 }}> + <div className="collectionViewChrome-cont" style={{ marginTop: this._collapsed ? -70 : 0, height: 70 }}> <div className="collectionViewChrome"> <div className="collectionViewBaseChrome"> <button className="collectionViewBaseChrome-collapse" - style={{ top: this._collapsed ? 90 : 10, transform: `rotate(${this._collapsed ? 180 : 0}deg)` }} + style={{ marginTop: this._collapsed ? 60 : 0, transform: `rotate(${this._collapsed ? 180 : 0}deg)` }} title="Collapse collection chrome" onClick={this.toggleCollapse}> <FontAwesomeIcon icon="caret-up" size="2x" /> </button> diff --git a/src/client/views/collections/KeyRestrictionRow.tsx b/src/client/views/collections/KeyRestrictionRow.tsx index 9c3c9c07c..9baa250a6 100644 --- a/src/client/views/collections/KeyRestrictionRow.tsx +++ b/src/client/views/collections/KeyRestrictionRow.tsx @@ -29,6 +29,7 @@ export default class KeyRestrictionRow extends React.Component<IKeyRestrictionPr else { this.props.script(""); } + return ( <div className="collectionViewBaseChrome-viewSpecsMenu-row"> <input className="collectionViewBaseChrome-viewSpecsMenu-rowLeft" @@ -36,7 +37,7 @@ export default class KeyRestrictionRow extends React.Component<IKeyRestrictionPr onChange={(e) => runInAction(() => this._key = e.target.value)} placeholder="KEY" /> <button className="collectionViewBaseChrome-viewSpecsMenu-rowMiddle" - style={{ background: PastelSchemaPalette.get(this._contains ? "green" : "red") }} + style={{ background: this._contains ? "#77dd77" : "#ff6961" }} onClick={() => runInAction(() => this._contains = !this._contains)}> {this._contains ? "CONTAINS" : "DOES NOT CONTAIN"} </button> diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index d70022280..8dac785e1 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -65,8 +65,20 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { return (this.props as any).ContentScaling && this.fitToBox && !this.isAnnotationOverlay ? (this.props as any).ContentScaling() : 1; } + ComputeContentBounds(boundsList: { x: number, y: number, width: number, height: number }[]) { + let bounds = boundsList.reduce((bounds, b) => { + var [sptX, sptY] = [b.x, b.y]; + let [bptX, bptY] = [sptX + b.width, sptY + b.height]; + return { + x: Math.min(sptX, bounds.x), y: Math.min(sptY, bounds.y), + r: Math.max(bptX, bounds.r), b: Math.max(bptY, bounds.b) + }; + }, { x: Number.MAX_VALUE, y: Number.MAX_VALUE, r: -Number.MAX_VALUE, b: -Number.MAX_VALUE }); + return bounds; + } + @computed get contentBounds() { - let bounds = this.fitToBox && !this.isAnnotationOverlay ? Doc.ComputeContentBounds(DocListCast(this.props.Document.data)) : undefined; + let bounds = this.fitToBox && !this.isAnnotationOverlay ? this.ComputeContentBounds(this.elements.filter(e => e.bounds).map(e => e.bounds!)) : undefined; return { panX: bounds ? (bounds.x + bounds.r) / 2 : this.Document.panX || 0, panY: bounds ? (bounds.y + bounds.b) / 2 : this.Document.panY || 0, @@ -151,6 +163,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { let dropY = NumCast(de.data.dropDocument.y); dragDoc.x = x + NumCast(dragDoc.x) - dropX; dragDoc.y = y + NumCast(dragDoc.y) - dropY; + de.data.targetContext = this.props.Document; + dragDoc.targetContext = this.props.Document; this.bringToFront(dragDoc); } } @@ -426,7 +440,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { return result.result === undefined ? {} : result.result; } - private viewDefToJSX(viewDef: any): JSX.Element | undefined { + private viewDefToJSX(viewDef: any): { ele: JSX.Element, bounds?: { x: number, y: number, width: number, height: number } } | undefined { if (viewDef.type === "text") { const text = Cast(viewDef.text, "string"); const x = Cast(viewDef.x, "number"); @@ -434,25 +448,27 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { const width = Cast(viewDef.width, "number"); const height = Cast(viewDef.height, "number"); const fontSize = Cast(viewDef.fontSize, "number"); - if ([text, x, y].some(val => val === undefined)) { + if ([text, x, y, width, height].some(val => val === undefined)) { return undefined; } - return <div className="collectionFreeform-customText" style={{ - transform: `translate(${x}px, ${y}px)`, - width, height, fontSize - }}>{text}</div>; + return { + ele: <div className="collectionFreeform-customText" style={{ + transform: `translate(${x}px, ${y}px)`, + width, height, fontSize + }}>{text}</div>, bounds: { x: x!, y: y!, width: width!, height: height! } + }; } } @computed.struct - get views() { + get elements() { let curPage = FieldValue(this.Document.curPage, -1); const initScript = this.Document.arrangeInit; const script = this.Document.arrangeScript; let state: any = undefined; const docs = this.childDocs; - let elements: JSX.Element[] = []; + let elements: { ele: JSX.Element, bounds?: { x: number, y: number, width: number, height: number } }[] = []; if (initScript) { const initResult = initScript.script.run({ docs, collection: this.Document }); if (initResult.success) { @@ -460,7 +476,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { const { state: scriptState, views } = result; state = scriptState; if (Array.isArray(views)) { - elements = views.reduce<JSX.Element[]>((prev, ele) => { + elements = views.reduce<typeof elements>((prev, ele) => { const jsx = this.viewDefToJSX(ele); jsx && prev.push(jsx); return prev; @@ -468,15 +484,18 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } } } - let docviews = docs.reduce((prev, doc) => { - if (!(doc instanceof Doc)) return prev; + let docviews = docs.filter(doc => doc instanceof Doc).reduce((prev, doc) => { var page = NumCast(doc.page, -1); + let bounds: { x?: number, y?: number, width?: number, height?: number }; if ((Math.abs(Math.round(page) - Math.round(curPage)) < 3) || page === -1) { let minim = BoolCast(doc.isMinimized); if (minim === undefined || !minim) { const pos = script ? this.getCalculatedPositions(script, { doc, index: prev.length, collection: this.Document, docs, state }) : {}; state = pos.state === undefined ? state : pos.state; - prev.push(<CollectionFreeFormDocumentView key={doc[Id]} x={pos.x} y={pos.y} width={pos.width} height={pos.height} {...this.getChildDocumentViewProps(doc)} />); + prev.push({ + ele: <CollectionFreeFormDocumentView key={doc[Id]} x={pos.x} y={pos.y} width={pos.width} height={pos.height} {...this.getChildDocumentViewProps(doc)} />, + bounds: (pos.x !== undefined && pos.y !== undefined && pos.width !== undefined && pos.height !== undefined) ? { x: pos.x, y: pos.y, width: pos.width, height: pos.height } : undefined + }); } } return prev; @@ -487,6 +506,11 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { return docviews; } + @computed.struct + get views() { + return this.elements.map(ele => ele.ele); + } + @action onCursorMove = (e: React.PointerEvent) => { super.setCursorPosition(this.getTransform().transformPoint(e.clientX, e.clientY)); @@ -496,7 +520,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { let layoutItems: ContextMenuProps[] = []; layoutItems.push({ description: `${this.fitToBox ? "Unset" : "Set"} Fit To Container`, - event: undoBatch(async () => this.props.Document.fitToBox = !this.fitToBox), + event: async () => this.props.Document.fitToBox = !this.fitToBox, icon: !this.fitToBox ? "expand-arrows-alt" : "compress-arrows-alt" }); layoutItems.push({ diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 1c767e012..67bed284f 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -293,15 +293,16 @@ export class MarqueeView extends React.Component<MarqueeViewProps> d.page = -1; return d; }); + newCollection.chromeStatus = "disabled"; let summary = Docs.Create.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, backgroundColor: "#e2ad32" /* yellow */, title: "-summary-" }); newCollection.proto!.summaryDoc = summary; selected = [newCollection]; newCollection.x = bounds.left + bounds.width; summary.proto!.subBulletDocs = new List<Doc>(selected); - //summary.proto!.maximizeLocation = "inTab"; // or "inPlace", or "onRight" summary.templates = new List<string>([Templates.Bullet.Layout]); - let container = Docs.Create.FreeformDocument([summary, newCollection], { x: bounds.left, y: bounds.top, width: 300, height: 200, title: "-summary-" }); + let container = Docs.Create.FreeformDocument([summary, newCollection], { x: bounds.left, y: bounds.top, width: 300, height: 200, chromeStatus: "disabled", title: "-summary-" }); container.viewType = CollectionViewType.Stacking; + container.autoHeight = true; this.props.addLiveTextDocument(container); // }); } else if (e.key === "S") { @@ -312,6 +313,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps> d.page = -1; return d; }); + newCollection.chromeStatus = "disabled"; let summary = Docs.Create.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, backgroundColor: "#e2ad32" /* yellow */, title: "-summary-" }); newCollection.proto!.summaryDoc = summary; selected = [newCollection]; @@ -319,6 +321,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps> //this.props.addDocument(newCollection, false); summary.proto!.summarizedDocs = new List<Doc>(selected); summary.proto!.maximizeLocation = "inTab"; // or "inPlace", or "onRight" + summary.autoHeight = true; this.props.addLiveTextDocument(summary); } diff --git a/src/client/views/nodes/ButtonBox.tsx b/src/client/views/nodes/ButtonBox.tsx index d2c23fdab..e2c559c9a 100644 --- a/src/client/views/nodes/ButtonBox.tsx +++ b/src/client/views/nodes/ButtonBox.tsx @@ -70,7 +70,7 @@ export class ButtonBox extends DocComponent<FieldViewProps, ButtonDocument>(Butt render() { return ( <div className="buttonBox-outerDiv" onContextMenu={this.onContextMenu}> - <button className="buttonBox-mainButton" onClick={this.onClick}>{this.Document.text || "Button"}</button> + <button className="buttonBox-mainButton" onClick={this.onClick}>{this.Document.text || this.Document.title || "Button"}</button> </div> ); } diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index f101222ae..4b5cf3a43 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -99,6 +99,7 @@ export interface DocumentViewProps { zoomToScale: (scale: number) => void; getScale: () => number; animateBetweenIcon?: (iconPos: number[], startTime: number, maximizing: boolean) => void; + ChromeHeight?: () => number; } const schema = createSchema({ @@ -439,7 +440,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu e.stopPropagation(); let annotationDoc = de.data.annotationDocument; annotationDoc.linkedToDoc = true; + de.data.targetContext = this.props.ContainingCollectionView!.props.Document; let targetDoc = this.props.Document; + targetDoc.targetContext = de.data.targetContext; let annotations = await DocListCastAsync(annotationDoc.annotations); if (annotations) { annotations.forEach(anno => { @@ -448,7 +451,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } let pdfDoc = await Cast(annotationDoc.pdfDoc, Doc); if (pdfDoc) { - DocUtils.MakeLink(annotationDoc, targetDoc, undefined, `Annotation from ${StrCast(pdfDoc.title)}`, "", StrCast(pdfDoc.title)); + DocUtils.MakeLink(annotationDoc, targetDoc, this.props.ContainingCollectionView!.props.Document, `Annotation from ${StrCast(pdfDoc.title)}`, "", StrCast(pdfDoc.title)); } } if (de.data instanceof DragManager.LinkDragData) { @@ -644,6 +647,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu @computed get nativeHeight() { return this.Document.nativeHeight || 0; } @computed get contents() { return (<DocumentContentsView {...this.props} + ChromeHeight={this.chromeHeight} isSelected={this.isSelected} select={this.select} selectOnLoad={this.props.selectOnLoad} layoutKey={"layout"} @@ -651,6 +655,12 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu DataDoc={this.dataDoc} />); } + chromeHeight = () => { + let showOverlays = this.props.showOverlays ? this.props.showOverlays(this.layoutDoc) : undefined; + let showTitle = showOverlays && "title" in showOverlays ? showOverlays.title : StrCast(this.layoutDoc.showTitle); + return showTitle ? 25 : 0; + } + get layoutDoc() { // if this document's layout field contains a document (ie, a rendering template), then we will use that // to determine the render JSX string, otherwise the layout field should directly contain a JSX layout string. @@ -666,8 +676,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu var nativeWidth = this.nativeWidth > 0 && !BoolCast(this.props.Document.ignoreAspect) ? `${this.nativeWidth}px` : "100%"; var nativeHeight = BoolCast(this.props.Document.ignoreAspect) ? this.props.PanelHeight() / this.props.ContentScaling() : this.nativeHeight > 0 ? `${this.nativeHeight}px` : "100%"; let showOverlays = this.props.showOverlays ? this.props.showOverlays(this.layoutDoc) : undefined; - let showTitle = showOverlays && showOverlays.title !== "undefined" ? showOverlays.title : StrCast(this.layoutDoc.showTitle); - let showCaption = showOverlays && showOverlays.caption !== "undefined" ? showOverlays.caption : StrCast(this.layoutDoc.showCaption); + let showTitle = showOverlays && "title" in showOverlays ? showOverlays.title : StrCast(this.layoutDoc.showTitle); + let showCaption = showOverlays && "caption" in showOverlays ? showOverlays.caption : StrCast(this.layoutDoc.showCaption); let templates = Cast(this.layoutDoc.templates, listSpec("string")); if (!showOverlays && templates instanceof List) { templates.map(str => { @@ -716,11 +726,11 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu transformOrigin: "top left", transform: `scale(${1 / this.props.ContentScaling()})` }}> <EditableView - contents={this.layoutDoc[showTitle]} + contents={(this.layoutDoc.isTemplate || !this.dataDoc ? this.layoutDoc : this.dataDoc)[showTitle]} display={"block"} height={72} fontSize={12} - GetValue={() => StrCast(this.layoutDoc[showTitle!])} + GetValue={() => StrCast((this.layoutDoc.isTemplate || !this.dataDoc ? this.layoutDoc : this.dataDoc)[showTitle!])} SetValue={(value: string) => (Doc.GetProto(this.layoutDoc)[showTitle!] = value) ? true : true} /> </div> diff --git a/src/client/views/nodes/FaceRectangles.tsx b/src/client/views/nodes/FaceRectangles.tsx index 3570531b2..acf1aced3 100644 --- a/src/client/views/nodes/FaceRectangles.tsx +++ b/src/client/views/nodes/FaceRectangles.tsx @@ -20,7 +20,7 @@ export interface RectangleTemplate { export default class FaceRectangles extends React.Component<FaceRectanglesProps> { render() { - let faces = DocListCast(Doc.GetProto(this.props.document).faces); + let faces = DocListCast(this.props.document.faces); let templates: RectangleTemplate[] = faces.map(faceDoc => { let rectangle = Cast(faceDoc.faceRectangle, Doc) as Doc; let style = { diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index ffaee8042..da54ecc3a 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -48,6 +48,8 @@ export interface FieldViewProps { PanelHeight: () => number; setVideoBox?: (player: VideoBox) => void; setPdfBox?: (player: PDFBox) => void; + ContentScaling: () => number; + ChromeHeight?: () => number; } @observer diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index 2db0776ba..87b1d43c1 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -37,6 +37,7 @@ import React = require("react"); import { For } from 'babel-types'; import { DateField } from '../../../new_fields/DateField'; import { Utils } from '../../../Utils'; +import { MainOverlayTextBox } from '../MainOverlayTextBox'; library.add(faEdit); library.add(faSmile, faTextHeight); @@ -413,7 +414,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe fieldExtDoc.annotations = new List<Doc>(targetAnnotations); } - let link = DocUtils.MakeLink(this.props.Document, region); + let link = DocUtils.MakeLink(this.props.Document, region, doc); if (link) { cbe.clipboardData!.setData("dash/linkDoc", link[Id]); linkId = link[Id]; @@ -527,6 +528,9 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } if (targetContext) { DocumentManager.Instance.jumpToDocument(targetContext, ctrlKey, false, document => this.props.addDocTab(document, undefined, location ? location : "inTab")); + } else if (jumpToDoc) { + DocumentManager.Instance.jumpToDocument(jumpToDoc, ctrlKey, false, document => this.props.addDocTab(document, undefined, location ? location : "inTab")); + } } }); @@ -642,7 +646,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe let nh = NumCast(this.dataDoc.nativeHeight, 0); let dh = NumCast(this.props.Document.height, 0); let sh = scrBounds.height; - this.props.Document.height = nh ? dh / nh * sh : sh; + const ChromeHeight = MainOverlayTextBox.Instance.ChromeHeight; + this.props.Document.height = (nh ? dh / nh * sh : sh) + (ChromeHeight ? ChromeHeight() : 0); this.dataDoc.nativeHeight = nh ? sh : undefined; } } diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 29a76b0c8..dbe545048 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -25,9 +25,12 @@ import { Docs, DocumentType } from '../../documents/Documents'; import { DocServer } from '../../DocServer'; import { Font } from '@react-pdf/renderer'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { CognitiveServices } from '../../cognitive_services/CognitiveServices'; +import { CognitiveServices, Service, Tag, Confidence } from '../../cognitive_services/CognitiveServices'; import FaceRectangles from './FaceRectangles'; import { faEye } from '@fortawesome/free-regular-svg-icons'; +import { ComputedField } from '../../../new_fields/ScriptField'; +import { CompileScript } from '../../util/Scripting'; +import { thisExpression } from 'babel-types'; var requestImageSize = require('../../util/request-image-size'); var path = require('path'); const { Howl } = require('howler'); @@ -96,7 +99,11 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD this.props.Document.width = drop.width; this.props.Document.height = drop.height; Doc.GetProto(this.props.Document).type = DocumentType.TEMPLATE; - this.props.Document.layout = temp; + if (this.props.DataDoc && this.props.DataDoc.layout === this.props.Document) { + this.props.DataDoc.layout = temp; + } else { + this.props.Document.layout = temp; + } e.stopPropagation(); } else if (de.mods === "AltKey" && /*this.dataDoc !== this.props.Document &&*/ drop.data instanceof ImageField) { Doc.GetProto(this.dataDoc)[this.props.fieldKey] = new ImageField(drop.data.url); @@ -226,20 +233,56 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD funcs.push({ description: "Rotate", event: this.rotate, icon: "expand-arrows-alt" }); let modes: ContextMenuProps[] = []; - let dataDoc = Doc.GetProto(this.props.Document); - modes.push({ description: "Generate Tags", event: () => CognitiveServices.Image.generateMetadata(dataDoc), icon: "tag" }); - modes.push({ description: "Find Faces", event: () => CognitiveServices.Image.extractFaces(dataDoc), icon: "camera" }); + modes.push({ description: "Generate Tags", event: this.generateMetadata, icon: "tag" }); + modes.push({ description: "Find Faces", event: this.extractFaces, icon: "camera" }); ContextMenu.Instance.addItem({ description: "Image Funcs...", subitems: funcs, icon: "asterisk" }); ContextMenu.Instance.addItem({ description: "Analyze...", subitems: modes, icon: "eye" }); } } + extractFaces = () => { + let converter = (results: any) => { + let faceDocs = new List<Doc>(); + results.map((face: CognitiveServices.Image.Face) => faceDocs.push(Docs.Get.DocumentHierarchyFromJson(face, `Face: ${face.faceId}`)!)); + return faceDocs; + }; + CognitiveServices.Image.Manager.analyzer(this.extensionDoc, ["faces"], this.url, Service.Face, converter); + } + + generateMetadata = (threshold: Confidence = Confidence.Excellent) => { + let converter = (results: any) => { + let tagDoc = new Doc; + let tagsList = new List(); + results.tags.map((tag: Tag) => { + tagsList.push(tag.name); + let sanitized = tag.name.replace(" ", "_"); + let script = `return (${tag.confidence} >= this.confidence) ? ${tag.confidence} : "${ComputedField.undefined}"`; + let computed = CompileScript(script, { params: { this: "Doc" } }); + computed.compiled && (tagDoc[sanitized] = new ComputedField(computed)); + }); + this.extensionDoc.generatedTags = tagsList; + tagDoc.title = "Generated Tags Doc"; + tagDoc.confidence = threshold; + return tagDoc; + }; + CognitiveServices.Image.Manager.analyzer(this.extensionDoc, ["generatedTagsDoc"], this.url, Service.ComputerVision, converter); + } + @action onDotDown(index: number) { this.Document.curPage = index; } + @computed get fieldExtensionDoc() { + return Doc.resolvedFieldDataDoc(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey, "true"); + } + + @computed private get url() { + let data = Cast(Doc.GetProto(this.props.Document).data, ImageField); + return data ? data.url.href : undefined; + } + dots(paths: string[]) { let nativeWidth = FieldValue(this.Document.nativeWidth, 1); let dist = Math.min(nativeWidth / paths.length, 40); @@ -287,7 +330,7 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD let aspect = size.height / size.width; let rotation = NumCast(this.dataDoc.rotation) % 180; if (rotation === 90 || rotation === 270) aspect = 1 / aspect; - if (Math.abs(layoutdoc[HeightSym]() / layoutdoc[WidthSym]() - aspect) > 0.01) { + if (Math.abs(NumCast(layoutdoc.height) - size.height) > 1 || Math.abs(NumCast(layoutdoc.width) - size.width) > 1) { setTimeout(action(() => { layoutdoc.height = layoutdoc[WidthSym]() * aspect; layoutdoc.nativeHeight = size.height; @@ -394,7 +437,7 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD style={{ color: [DocListCast(this.extensionDoc.audioAnnotations).length ? "blue" : "gray", "green", "red"][this._audioState] }} icon={faFileAudio} size="sm" /> </div> {/* {this.lightbox(paths)} */} - <FaceRectangles document={this.props.Document} color={"#0000FF"} backgroundColor={"#0000FF"} /> + <FaceRectangles document={this.extensionDoc} color={"#0000FF"} backgroundColor={"#0000FF"} /> </div>); } }
\ No newline at end of file diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx index 77824b4ff..f10079169 100644 --- a/src/client/views/nodes/KeyValueBox.tsx +++ b/src/client/views/nodes/KeyValueBox.tsx @@ -20,6 +20,8 @@ import { RichTextField } from "../../../new_fields/RichTextField"; import { ImageField } from "../../../new_fields/URLField"; import { SelectionManager } from "../../util/SelectionManager"; import { listSpec } from "../../../new_fields/Schema"; +import { CollectionViewType } from "../collections/CollectionBaseView"; +import { undoBatch } from "../../util/UndoManager"; export type KVPScript = { script: CompiledScript; @@ -89,6 +91,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> { return false; } + @undoBatch public static SetField(doc: Doc, key: string, value: string) { const script = this.CompileKVPScript(value); if (!script) return false; @@ -195,6 +198,9 @@ export class KeyValueBox extends React.Component<FieldViewProps> { } let fieldTemplate = await this.inferType(sourceDoc[metaKey], metaKey); + if (!fieldTemplate) { + return; + } let previousViewType = fieldTemplate.viewType; Doc.MakeTemplate(fieldTemplate, metaKey, Doc.GetProto(parentStackingDoc)); previousViewType && (fieldTemplate.viewType = previousViewType); @@ -211,14 +217,17 @@ export class KeyValueBox extends React.Component<FieldViewProps> { return Docs.Create.StackingDocument([], options); } let first = await Cast(data[0], Doc); - if (!first) { + if (!first || !first.data) { return Docs.Create.StackingDocument([], options); } - switch (first.type) { - case "image": - return Docs.Create.StackingDocument([], options); - case "text": + switch (first.data.constructor) { + case RichTextField: return Docs.Create.TreeDocument([], options); + case ImageField: + return Docs.Create.MasonryDocument([], options); + default: + console.log(`Template for ${first.data.constructor} not supported!`); + return undefined; } } else if (data instanceof ImageField) { return Docs.Create.ImageDocument("https://image.flaticon.com/icons/png/512/23/23765.png", options); diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx index 064f3edcc..3775f0f47 100644 --- a/src/client/views/nodes/KeyValuePair.tsx +++ b/src/client/views/nodes/KeyValuePair.tsx @@ -1,7 +1,7 @@ import { action, observable } from 'mobx'; import { observer } from "mobx-react"; import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app -import { emptyFunction, returnFalse, returnZero, returnTrue } from '../../../Utils'; +import { emptyFunction, returnFalse, returnZero, returnTrue, returnOne } from '../../../Utils'; import { CompileScript, CompiledScript, ScriptOptions } from "../../util/Scripting"; import { Transform } from '../../util/Transform'; import { EditableView } from "../EditableView"; @@ -16,6 +16,7 @@ import { DragManager, SetupDrag } from '../../util/DragManager'; import { ContextMenu } from '../ContextMenu'; import { Docs } from '../../documents/Documents'; import { CollectionDockingView } from '../collections/CollectionDockingView'; +import { undoBatch } from '../../util/UndoManager'; // Represents one row in a key value plane @@ -70,6 +71,7 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> { PanelWidth: returnZero, PanelHeight: returnZero, addDocTab: returnZero, + ContentScaling: returnOne }; let contents = <FieldView {...props} />; // let fieldKey = Object.keys(props.Document).indexOf(props.fieldKey) !== -1 ? props.fieldKey : "(" + props.fieldKey + ")"; @@ -91,12 +93,12 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> { <tr className={this.props.rowStyle} onPointerEnter={action(() => this.isPointerOver = true)} onPointerLeave={action(() => this.isPointerOver = false)}> <td className="keyValuePair-td-key" style={{ width: `${this.props.keyWidth}%` }}> <div className="keyValuePair-td-key-container"> - <button style={hover} className="keyValuePair-td-key-delete" onClick={() => { + <button style={hover} className="keyValuePair-td-key-delete" onClick={undoBatch(() => { if (Object.keys(props.Document).indexOf(props.fieldKey) !== -1) { props.Document[props.fieldKey] = undefined; } else props.Document.proto![props.fieldKey] = undefined; - }}> + })}> X </button> <input diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 4973340df..fa072aecf 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -69,24 +69,10 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen componentDidMount() { if (this.props.setPdfBox) this.props.setPdfBox(this); - - document.removeEventListener("copy", this.copy); - document.addEventListener("copy", this.copy); } componentWillUnmount() { this._reactionDisposer && this._reactionDisposer(); - document.removeEventListener("copy", this.copy); - } - - private copy = (e: ClipboardEvent) => { - if (this.props.active()) { - if (e.clipboardData) { - e.clipboardData.setData("text/plain", text); - e.clipboardData.setData("dash/pdfOrigin", this.props.Document[Id]); - e.preventDefault(); - } - } } public GetPage() { diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx index ed7081b1d..513f9fed6 100644 --- a/src/client/views/pdf/Annotation.tsx +++ b/src/client/views/pdf/Annotation.tsx @@ -9,6 +9,8 @@ import { List } from "../../../new_fields/List"; import PDFMenu from "./PDFMenu"; import { DocumentManager } from "../../util/DocumentManager"; import { PresentationView } from "../presentationview/PresentationView"; +import { LinkManager } from "../../util/LinkManager"; +import { CollectionDockingView } from "../collections/CollectionDockingView"; interface IAnnotationProps { anno: Doc; @@ -110,11 +112,15 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> { } @action - onPointerDown = (e: React.PointerEvent) => { + onPointerDown = async (e: React.PointerEvent) => { if (e.button === 0) { - let targetDoc = Cast(this.props.document.target, Doc, null); + let targetDoc = await Cast(this.props.document.target, Doc); if (targetDoc) { - DocumentManager.Instance.jumpToDocument(targetDoc, false); + let context = await Cast(targetDoc.targetContext, Doc); + if (context) { + DocumentManager.Instance.jumpToDocument(targetDoc, false, undefined, + ((doc) => this.props.parent.props.parent.props.addDocTab(context!, context!.proto, e.ctrlKey ? "onRight" : "inTab"))); + } } } if (e.button === 2) { diff --git a/src/client/views/pdf/Page.tsx b/src/client/views/pdf/Page.tsx index c205617b4..c5b2a1dda 100644 --- a/src/client/views/pdf/Page.tsx +++ b/src/client/views/pdf/Page.tsx @@ -175,7 +175,7 @@ export default class Page extends React.Component<IPageProps> { } let pdfDoc = await Cast(annotationDoc.pdfDoc, Doc); if (pdfDoc) { - DocUtils.MakeLink(annotationDoc, targetDoc, undefined, `Annotation from ${StrCast(pdfDoc.title)}`, "", StrCast(pdfDoc.title)); + DocUtils.MakeLink(annotationDoc, targetDoc, dragData.targetContext, `Annotation from ${StrCast(pdfDoc.title)}`, "", StrCast(pdfDoc.title)); } } } diff --git a/src/client/views/presentationview/PresentationElement.tsx b/src/client/views/presentationview/PresentationElement.tsx index 36f1178f1..ccc3a72a9 100644 --- a/src/client/views/presentationview/PresentationElement.tsx +++ b/src/client/views/presentationview/PresentationElement.tsx @@ -1,6 +1,6 @@ import { library } from '@fortawesome/fontawesome-svg-core'; import { faFile as fileRegular } from '@fortawesome/free-regular-svg-icons'; -import { faArrowUp, faFile as fileSolid, faFileDownload, faLocationArrow, faSearch } from '@fortawesome/free-solid-svg-icons'; +import { faArrowUp, faFile as fileSolid, faFileDownload, faLocationArrow, faSearch, faArrowRight } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { action, computed, observable, runInAction } from "mobx"; import { observer } from "mobx-react"; @@ -9,17 +9,26 @@ import { Id } from "../../../new_fields/FieldSymbols"; import { List } from "../../../new_fields/List"; import { listSpec } from "../../../new_fields/Schema"; import { BoolCast, Cast, NumCast, StrCast } from "../../../new_fields/Types"; -import { Utils } from "../../../Utils"; +import { Utils, returnFalse, emptyFunction, returnOne } from "../../../Utils"; import { DragManager, dropActionType, SetupDrag } from "../../util/DragManager"; import { SelectionManager } from "../../util/SelectionManager"; -import "./PresentationView.scss"; +import { indexOf } from "typescript-collections/dist/lib/arrays"; +import { map } from "bluebird"; +import { ContextMenu } from "../ContextMenu"; +import { DocumentContentsView } from "../nodes/DocumentContentsView"; +import { Transform } from "../../util/Transform"; +import { FieldView } from "../nodes/FieldView"; +import { DocumentView } from "../nodes/DocumentView"; +import { DocumentType } from "../../documents/Documents"; import React = require("react"); + library.add(faArrowUp); library.add(fileSolid); library.add(faLocationArrow); library.add(fileRegular as any); library.add(faSearch); +library.add(faArrowRight); interface PresentationElementProps { mainDocument: Doc; @@ -46,6 +55,7 @@ export enum buttonIndex { FadeAfter = 3, HideAfter = 4, Group = 5, + OpenRight = 6 } @@ -68,7 +78,7 @@ export default class PresentationElement extends React.Component<PresentationEle constructor(props: PresentationElementProps) { super(props); - this.selectedButtons = new Array(6); + this.selectedButtons = new Array(7); this.presElRef = React.createRef(); } @@ -132,7 +142,7 @@ export default class PresentationElement extends React.Component<PresentationEle if (!foundDoc) { let newDoc = new Doc(); - let defaultBooleanArray: boolean[] = new Array(6); + let defaultBooleanArray: boolean[] = new Array(7); newDoc.selectedButtons = new List(defaultBooleanArray); newDoc.docId = this.props.document[Id]; castedList.push(newDoc); @@ -394,6 +404,18 @@ export default class PresentationElement extends React.Component<PresentationEle } + @action + onRightTabClick = (e: React.MouseEvent) => { + e.stopPropagation(); + if (this.selectedButtons[buttonIndex.OpenRight]) { + this.selectedButtons[buttonIndex.OpenRight] = false; + // action maybe + } else { + this.selectedButtons[buttonIndex.OpenRight] = true; + } + this.autoSaveButtonChange(buttonIndex.OpenRight); + } + /** * Creating a drop target for drag and drop when called. */ @@ -629,7 +651,7 @@ export default class PresentationElement extends React.Component<PresentationEle */ getSelectedButtonsOfDoc = async (paramDoc: Doc) => { let castedList = Cast(this.props.presButtonBackUp.selectedButtonDocs, listSpec(Doc)); - let foundSelectedButtons: boolean[] = new Array(6); + let foundSelectedButtons: boolean[] = new Array(7); //if this is the first time this doc mounts, push a doc for it to store for (let doc of castedList!) { @@ -649,7 +671,7 @@ export default class PresentationElement extends React.Component<PresentationEle //This is used to add dragging as an event. onPointerEnter = (e: React.PointerEvent): void => { - this.props.document.libraryBrush = true; + // this.props.document.libraryBrush = true; if (e.buttons === 1 && SelectionManager.GetIsDragging()) { let selected = NumCast(this.props.mainDocument.selectedDoc, 0); @@ -666,7 +688,7 @@ export default class PresentationElement extends React.Component<PresentationEle //This is used to remove the dragging when dropped. onPointerLeave = (e: React.PointerEvent): void => { - this.props.document.libraryBrush = false; + // this.props.document.libraryBrush = false; //to get currently selected presentation doc let selected = NumCast(this.props.mainDocument.selectedDoc, 0); @@ -766,8 +788,78 @@ export default class PresentationElement extends React.Component<PresentationEle } } + private get embedInline() { + return BoolCast(this.props.document.embedOpen); + } + private set embedInline(value: boolean) { + this.props.document.embedOpen = value; + } + onContextMenu = (e: React.MouseEvent<HTMLDivElement>) => { + e.preventDefault(); + e.stopPropagation(); + ContextMenu.Instance.addItem({ description: this.embedInline ? "Collapse Inline" : "Expand Inline", event: () => this.embedInline = !this.embedInline, icon: "expand" }); + ContextMenu.Instance.displayMenu(e.clientX, e.clientY); + } + + renderEmbeddedInline = () => { + if (!this.embedInline) { + return (null); + } + + // return <ul key={this.props.document[Id] + "more"}> + // {TreeView.GetChildElements([this.props.document], "", new Doc(), undefined, "", (doc: Doc, relativeTo?: Doc, before?: boolean) => false, this.props.removeDocByRef, this.move, + // StrCast(this.props.document.dropAction) as dropActionType, (doc: Doc, dataDoc: Doc | undefined, where: string) => { }, Transform.Identity, () => ({ translateX: 0, translateY: 0 }), () => false, () => 400, 7)} + // </ul >; + let propDocWidth = NumCast(this.props.document.nativeWidth); + let propDocHeight = NumCast(this.props.document.nativeHeight); + let scale = () => { + let newScale = 175 / NumCast(this.props.document.nativeWidth, 175); + console.log("New Scale: ", newScale); + return newScale; + }; + return ( + <div style={{ + position: "relative", + height: propDocHeight === 0 ? 100 : propDocHeight * scale(), + width: propDocWidth === 0 ? "auto" : propDocWidth * scale(), + marginTop: 15 + + }}> + <DocumentView + fitToBox={StrCast(this.props.document.type).indexOf(DocumentType.COL) !== -1} + Document={this.props.document} + addDocument={returnFalse} + removeDocument={returnFalse} + ScreenToLocalTransform={Transform.Identity} + addDocTab={returnFalse} + renderDepth={1} + PanelWidth={() => 350} + PanelHeight={() => 100} + focus={emptyFunction} + selectOnLoad={false} + parentActive={returnFalse} + whenActiveChanged={returnFalse} + bringToFront={emptyFunction} + zoomToScale={emptyFunction} + getScale={returnOne} + ContainingCollectionView={undefined} + ContentScaling={scale} + /> + <div style={{ + width: " 100%", + height: " 100%", + position: "absolute", + left: 0, + top: 0, + background: "transparent", + zIndex: 2, + + }}></div> + </div> + ); + } render() { let p = this.props; @@ -784,7 +876,7 @@ export default class PresentationElement extends React.Component<PresentationEle let dropAction = StrCast(this.props.document.dropAction) as dropActionType; let onItemDown = SetupDrag(this.presElRef, () => p.document, this.move, dropAction, this.props.mainDocument[Id], true); return ( - <div className={className} key={p.document[Id] + p.index} + <div className={className} onContextMenu={this.onContextMenu} key={p.document[Id] + p.index} ref={this.presElRef} onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave} onPointerDown={onItemDown} @@ -809,7 +901,10 @@ export default class PresentationElement extends React.Component<PresentationEle this.changeGroupStatus(); this.onGroupClick(p.document, p.index, this.selectedButtons[buttonIndex.Group]); }}> <FontAwesomeIcon icon={"arrow-up"} /> </button> + <button title="Open Right" className={this.selectedButtons[buttonIndex.OpenRight] ? "presentation-interaction-selected" : "presentation-interaction"} onPointerDown={(e) => e.stopPropagation()} onClick={this.onRightTabClick}><FontAwesomeIcon icon={"arrow-right"} /></button> + <br /> + {this.renderEmbeddedInline()} </div> ); } diff --git a/src/client/views/presentationview/PresentationModeMenu.scss b/src/client/views/presentationview/PresentationModeMenu.scss new file mode 100644 index 000000000..336f43d20 --- /dev/null +++ b/src/client/views/presentationview/PresentationModeMenu.scss @@ -0,0 +1,30 @@ +.presMenu-cont { + position: fixed; + z-index: 10000; + height: 35px; + background: #323232; + box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.25); + border-radius: 0px 6px 6px 6px; + overflow: hidden; + display: flex; + + .presMenu-button { + background-color: transparent; + width: 35px; + height: 35px; + } + + .presMenu-button:hover { + background-color: #121212; + } + + .presMenu-dragger { + height: 100%; + transition: width .2s; + background-image: url("https://logodix.com/logo/1020374.png"); + background-size: 90% 100%; + background-repeat: no-repeat; + background-position: left center; + } + +}
\ No newline at end of file diff --git a/src/client/views/presentationview/PresentationModeMenu.tsx b/src/client/views/presentationview/PresentationModeMenu.tsx new file mode 100644 index 000000000..b3edeb1e2 --- /dev/null +++ b/src/client/views/presentationview/PresentationModeMenu.tsx @@ -0,0 +1,92 @@ +import React = require("react"); +import { observable, action, runInAction } from "mobx"; +import "./PresentationModeMenu.scss"; +import { observer } from "mobx-react"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + + +export interface PresModeMenuProps { + next: () => void; + back: () => void; + presStatus: boolean; + startOrResetPres: () => void; + closePresMode: () => void; +} + +@observer +export default class PresModeMenu extends React.Component<PresModeMenuProps> { + + @observable private _top: number = 20; + @observable private _right: number = 0; + @observable private _opacity: number = 1; + @observable private _transition: string = "opacity 0.5s"; + @observable private _transitionDelay: string = ""; + //@observable private Pinned: boolean = false; + + + private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); + + @action + pointerEntered = (e: React.PointerEvent) => { + this._transition = "opacity 0.1s"; + this._transitionDelay = ""; + this._opacity = 1; + } + + @action + dragging = (e: PointerEvent) => { + this._right -= e.movementX; + this._top += e.movementY; + + e.stopPropagation(); + e.preventDefault(); + } + + dragEnd = (e: PointerEvent) => { + document.removeEventListener("pointermove", this.dragging); + document.removeEventListener("pointerup", this.dragEnd); + e.stopPropagation(); + e.preventDefault(); + } + + dragStart = (e: React.PointerEvent) => { + document.removeEventListener("pointermove", this.dragging); + document.addEventListener("pointermove", this.dragging); + document.removeEventListener("pointerup", this.dragEnd); + document.addEventListener("pointerup", this.dragEnd); + let clientRect = this._mainCont.current!.getBoundingClientRect(); + + // runInAction(() => this._left = (clientRect.width - e.nativeEvent.offsetX) + clientRect.left); + // runInAction(() => this._top = e.nativeEvent.offsetY); + + e.stopPropagation(); + e.preventDefault(); + } + + renderPlayPauseButton = () => { + if (this.props.presStatus) { + return <button title="Reset Presentation" className="presMenu-button" onClick={this.props.startOrResetPres}><FontAwesomeIcon icon="stop" /></button>; + } else { + return <button title="Start Presentation From Start" className="presMenu-button" onClick={this.props.startOrResetPres}><FontAwesomeIcon icon="play" /></button>; + } + } + + render() { + return ( + <div className="presMenu-cont" onPointerEnter={this.pointerEntered} ref={this._mainCont} + style={{ right: this._right, top: this._top, opacity: this._opacity, transition: this._transition, transitionDelay: this._transitionDelay }}> + <button title="Back" className="presMenu-button" onClick={this.props.back}><FontAwesomeIcon icon={"arrow-left"} /></button> + {this.renderPlayPauseButton()} + <button title="Next" className="presMenu-button" onClick={this.props.next}><FontAwesomeIcon icon={"arrow-right"} /></button> + <button className="presMenu-button" title="Close Presentation Menu" onClick={this.props.closePresMode}> + <FontAwesomeIcon icon="times" size="lg" /> + </button> + <div className="presMenu-dragger" onPointerDown={this.dragStart} style={{ width: "20px" }} /> + </div > + ); + } + + + + +}
\ No newline at end of file diff --git a/src/client/views/presentationview/PresentationView.scss b/src/client/views/presentationview/PresentationView.scss index 2bb0ec8c8..97cbd4a24 100644 --- a/src/client/views/presentationview/PresentationView.scss +++ b/src/client/views/presentationview/PresentationView.scss @@ -1,7 +1,7 @@ .presentationView-cont { position: absolute; background: white; - z-index: 1; + z-index: 2; box-shadow: #AAAAAA .2vw .2vw .4vw; right: 0; top: 0; @@ -19,6 +19,17 @@ -ms-user-select: none; user-select: none; transition: all .1s; + //max-height: 250px; + + + + .documentView-node { + // height: auto !important; + // width: aut !important; + position: absolute; + z-index: 1; + } + } .presentationView-item-above { @@ -55,7 +66,7 @@ padding-bottom: 3px; font-size: 25px; display: inline-block; - width: calc(100% - 160px); + width: calc(100% - 200px); } .presentation-icon { diff --git a/src/client/views/presentationview/PresentationView.tsx b/src/client/views/presentationview/PresentationView.tsx index e25725275..ea85a8c6a 100644 --- a/src/client/views/presentationview/PresentationView.tsx +++ b/src/client/views/presentationview/PresentationView.tsx @@ -4,7 +4,7 @@ import { observable, action, runInAction, reaction, autorun } from "mobx"; import "./PresentationView.scss"; import { DocumentManager } from "../../util/DocumentManager"; import { Utils } from "../../../Utils"; -import { Doc, DocListCast, DocListCastAsync } from "../../../new_fields/Doc"; +import { Doc, DocListCast, DocListCastAsync, WidthSym } from "../../../new_fields/Doc"; import { listSpec } from "../../../new_fields/Schema"; import { Cast, NumCast, FieldValue, PromiseValue, StrCast, BoolCast } from "../../../new_fields/Types"; import { Id } from "../../../new_fields/FieldSymbols"; @@ -12,10 +12,13 @@ import { List } from "../../../new_fields/List"; import PresentationElement, { buttonIndex } from "./PresentationElement"; import { library } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faArrowRight, faArrowLeft, faPlay, faStop, faPlus, faTimes, faMinus, faEdit } from '@fortawesome/free-solid-svg-icons'; +import { faArrowRight, faArrowLeft, faPlay, faStop, faPlus, faTimes, faMinus, faEdit, faEye } from '@fortawesome/free-solid-svg-icons'; import { Docs } from "../../documents/Documents"; import { undoBatch, UndoManager } from "../../util/UndoManager"; import PresentationViewList from "./PresentationList"; +import { ContextMenu } from "../ContextMenu"; +import PresModeMenu from "./PresentationModeMenu"; +import { CollectionDockingView } from "../collections/CollectionDockingView"; library.add(faArrowLeft); library.add(faArrowRight); @@ -25,6 +28,7 @@ library.add(faPlus); library.add(faTimes); library.add(faMinus); library.add(faEdit); +library.add(faEye); export interface PresViewProps { @@ -62,6 +66,8 @@ export class PresentationView extends React.Component<PresViewProps> { //Variable that holds reference to title input, so that new presentations get titles assigned. @observable titleInputElement: HTMLInputElement | undefined; @observable PresTitleChangeOpen: boolean = false; + @observable presMode: boolean = false; + @observable opacity = 1; @observable persistOpacity = true; @@ -84,6 +90,7 @@ export class PresentationView extends React.Component<PresViewProps> { //The first lifecycle function that gets called to set up the current presentation. async componentWillMount() { + this.props.Documents.forEach(async (doc, index: number) => { //For each presentation received from mainContainer, a mapping is created. @@ -361,11 +368,16 @@ export class PresentationView extends React.Component<PresViewProps> { //checking if curDoc has navigation open let curDocButtons = this.presElementsMappings.get(curDoc)!.selected; if (curDocButtons[buttonIndex.Navigate]) { - DocumentManager.Instance.jumpToDocument(curDoc, false); + this.jumpToTabOrRight(curDocButtons, curDoc); } else if (curDocButtons[buttonIndex.Show]) { let curScale = DocumentManager.Instance.getScaleOfDocView(this.childrenDocs[fromDoc]); - //awaiting jump so that new scale can be found, since jumping is async - await DocumentManager.Instance.jumpToDocument(curDoc, true); + if (curDocButtons[buttonIndex.OpenRight]) { + //awaiting jump so that new scale can be found, since jumping is async + await DocumentManager.Instance.jumpToDocument(curDoc, true); + } else { + await DocumentManager.Instance.jumpToDocument(curDoc, false, undefined, doc => CollectionDockingView.Instance.AddTab(undefined, doc, undefined)); + } + let newScale = DocumentManager.Instance.getScaleOfDocView(curDoc); curDoc.viewScale = newScale; @@ -378,9 +390,15 @@ export class PresentationView extends React.Component<PresViewProps> { return; } let curScale = DocumentManager.Instance.getScaleOfDocView(this.childrenDocs[fromDoc]); + let curDocButtons = this.presElementsMappings.get(docToJump)!.selected; + - //awaiting jump so that new scale can be found, since jumping is async - await DocumentManager.Instance.jumpToDocument(docToJump, willZoom); + if (curDocButtons[buttonIndex.OpenRight]) { + //awaiting jump so that new scale can be found, since jumping is async + await DocumentManager.Instance.jumpToDocument(docToJump, willZoom); + } else { + await DocumentManager.Instance.jumpToDocument(docToJump, willZoom, undefined, doc => CollectionDockingView.Instance.AddTab(undefined, doc, undefined)); + } let newScale = DocumentManager.Instance.getScaleOfDocView(curDoc); curDoc.viewScale = newScale; //saving the scale that user was on @@ -390,6 +408,14 @@ export class PresentationView extends React.Component<PresViewProps> { } + jumpToTabOrRight = (curDocButtons: boolean[], curDoc: Doc) => { + if (curDocButtons[buttonIndex.OpenRight]) { + DocumentManager.Instance.jumpToDocument(curDoc, false); + } else { + DocumentManager.Instance.jumpToDocument(curDoc, false, undefined, doc => CollectionDockingView.Instance.AddTab(undefined, doc, undefined)); + } + } + /** * Async function that supposedly return the doc that is located at given index. */ @@ -579,18 +605,35 @@ export class PresentationView extends React.Component<PresViewProps> { //The function that starts or resets presentaton functionally, depending on status flag. @action - startOrResetPres = () => { + startOrResetPres = async () => { if (this.presStatus) { this.resetPresentation(); } else { this.presStatus = true; - this.startPresentation(0); + let startIndex = await this.findStartDocument(); + this.startPresentation(startIndex); const current = NumCast(this.curPresentation.selectedDoc); - this.gotoDocument(0, current); + this.gotoDocument(startIndex, current); } this.curPresentation.presStatus = this.presStatus; } + findStartDocument = async () => { + let docAtZero = await this.getDocAtIndex(0); + if (docAtZero === undefined) { + return 0; + } + let docAtZeroPresId = StrCast(docAtZero.presentId); + + if (this.groupMappings.has(docAtZeroPresId)) { + let group = this.groupMappings.get(docAtZeroPresId)!; + let lastDoc = group[group.length - 1]; + return this.childrenDocs.indexOf(lastDoc); + } else { + return 0; + } + } + //The function that resets the presentation by removing every action done by it. It also //stops the presentaton. @action @@ -809,56 +852,127 @@ export class PresentationView extends React.Component<PresViewProps> { this.presElementsMappings.set(keyDoc, elem); } + _downsize = 0; + onPointerDown = (e: React.PointerEvent) => { + this._downsize = e.clientX; + document.removeEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + document.addEventListener("pointermove", this.onPointerMove); + document.addEventListener("pointerup", this.onPointerUp); + e.stopPropagation(); + e.preventDefault(); + } + @action + onPointerMove = (e: PointerEvent) => { + + this.curPresentation.width = Math.max(window.innerWidth - e.clientX, 300); + } + @action + onPointerUp = (e: PointerEvent) => { + if (Math.abs(e.clientX - this._downsize) < 4) { + let presWidth = NumCast(this.curPresentation.width); + if (presWidth - 300 !== 0) { + this.curPresentation.width = 0; + } + } + document.removeEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + } + + togglePresView = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + let width = NumCast(this.curPresentation.width); + if (width === 0) { + this.curPresentation.width = 300; + } + } + @action + openPresMode = () => { + if (!this.presMode) { + this.curPresentation.width = 0; + this.presMode = true; + } + } + + @action + closePresMode = () => { + if (this.presMode) { + this.presMode = false; + this.curPresentation.width = 300; + } + + } + + renderPresMode = () => { + if (this.presMode) { + return <PresModeMenu next={this.next} back={this.back} startOrResetPres={this.startOrResetPres} presStatus={this.presStatus} closePresMode={this.closePresMode} />; + } else { + return (null); + } + + } render() { let width = NumCast(this.curPresentation.width); return ( - <div className="presentationView-cont" onPointerEnter={action(() => !this.persistOpacity && (this.opacity = 1))} onPointerLeave={action(() => !this.persistOpacity && (this.opacity = 0.4))} style={{ width: width, overflow: "hidden", opacity: this.opacity, transition: "0.7s opacity ease" }}> - <div className="presentationView-heading"> - {this.renderSelectOrPresSelection()} - <button title="Close Presentation" className='presentation-icon' onClick={this.closePresentation}><FontAwesomeIcon icon={"times"} /></button> - <button title="Add Presentation" className="presentation-icon" style={{ marginRight: 10 }} onClick={() => { - runInAction(() => { if (this.PresTitleChangeOpen) { this.PresTitleChangeOpen = false; } }); - runInAction(() => this.PresTitleInputOpen ? this.PresTitleInputOpen = false : this.PresTitleInputOpen = true); - }}><FontAwesomeIcon icon={"plus"} /></button> - <button title="Remove Presentation" className='presentation-icon' style={{ marginRight: 10 }} onClick={this.removePresentation}><FontAwesomeIcon icon={"minus"} /></button> - <button title="Change Presentation Title" className="presentation-icon" style={{ marginRight: 10 }} onClick={() => { - runInAction(() => { if (this.PresTitleInputOpen) { this.PresTitleInputOpen = false; } }); - runInAction(() => this.PresTitleChangeOpen ? this.PresTitleChangeOpen = false : this.PresTitleChangeOpen = true); - }}><FontAwesomeIcon icon={"edit"} /></button> + <div> + <div className="presentationView-cont" onPointerEnter={action(() => !this.persistOpacity && (this.opacity = 1))} onPointerLeave={action(() => !this.persistOpacity && (this.opacity = 0.4))} style={{ width: width, overflowY: "scroll", overflowX: "hidden", opacity: this.opacity, transition: "0.7s opacity ease" }}> + <div className="presentationView-heading"> + {this.renderSelectOrPresSelection()} + <button title="Close Presentation" className='presentation-icon' onClick={this.closePresentation}><FontAwesomeIcon icon={"times"} /></button> + <button title="Open Presentation Mode" className="presentation-icon" style={{ marginRight: 10 }} onClick={this.openPresMode}><FontAwesomeIcon icon={"eye"} /></button> + <button title="Add Presentation" className="presentation-icon" style={{ marginRight: 10 }} onClick={() => { + runInAction(() => { if (this.PresTitleChangeOpen) { this.PresTitleChangeOpen = false; } }); + runInAction(() => this.PresTitleInputOpen ? this.PresTitleInputOpen = false : this.PresTitleInputOpen = true); + }}><FontAwesomeIcon icon={"plus"} /></button> + <button title="Remove Presentation" className='presentation-icon' style={{ marginRight: 10 }} onClick={this.removePresentation}><FontAwesomeIcon icon={"minus"} /></button> + <button title="Change Presentation Title" className="presentation-icon" style={{ marginRight: 10 }} onClick={() => { + runInAction(() => { if (this.PresTitleInputOpen) { this.PresTitleInputOpen = false; } }); + runInAction(() => this.PresTitleChangeOpen ? this.PresTitleChangeOpen = false : this.PresTitleChangeOpen = true); + }}><FontAwesomeIcon icon={"edit"} /></button> + </div> + <div className="presentation-buttons"> + <button title="Back" className="presentation-button" onClick={this.back}><FontAwesomeIcon icon={"arrow-left"} /></button> + {this.renderPlayPauseButton()} + <button title="Next" className="presentation-button" onClick={this.next}><FontAwesomeIcon icon={"arrow-right"} /></button> + </div> + + <PresentationViewList + mainDocument={this.curPresentation} + deleteDocument={this.RemoveDoc} + gotoDocument={this.gotoDocument} + groupMappings={this.groupMappings} + PresElementsMappings={this.presElementsMappings} + setChildrenDocs={this.setChildrenDocs} + presStatus={this.presStatus} + presButtonBackUp={this.presButtonBackUp} + presGroupBackUp={this.presGroupBackUp} + removeDocByRef={this.removeDocByRef} + clearElemMap={() => this.presElementsMappings.clear()} + /> + <input + type="checkbox" + onChange={action((e: React.ChangeEvent<HTMLInputElement>) => { + this.persistOpacity = e.target.checked; + this.opacity = this.persistOpacity ? 1 : 0.4; + })} + checked={this.persistOpacity} + style={{ position: "absolute", bottom: 5, left: 5 }} + onPointerEnter={action(() => this.labelOpacity = 1)} + onPointerLeave={action(() => this.labelOpacity = 0)} + /> + <p style={{ position: "absolute", bottom: 1, left: 22, opacity: this.labelOpacity, transition: "0.7s opacity ease" }}>opacity {this.persistOpacity ? "persistent" : "on focus"}</p> </div> - <div className="presentation-buttons"> - <button title="Back" className="presentation-button" onClick={this.back}><FontAwesomeIcon icon={"arrow-left"} /></button> - {this.renderPlayPauseButton()} - <button title="Next" className="presentation-button" onClick={this.next}><FontAwesomeIcon icon={"arrow-right"} /></button> + <div className="mainView-libraryHandle" + style={{ cursor: "ew-resize", right: `${width - 10}px`, backgroundColor: "white", opacity: this.opacity, transition: "0.7s opacity ease" }} + onPointerDown={this.onPointerDown} onClick={this.togglePresView}> + <span title="library View Dragger" style={{ width: "100%", height: "100%", position: "absolute" }} /> </div> - <input - type="checkbox" - onChange={action((e: React.ChangeEvent<HTMLInputElement>) => { - this.persistOpacity = e.target.checked; - this.opacity = this.persistOpacity ? 1 : 0.4; - })} - checked={this.persistOpacity} - style={{ position: "absolute", bottom: 5, left: 5 }} - onPointerEnter={action(() => this.labelOpacity = 1)} - onPointerLeave={action(() => this.labelOpacity = 0)} - /> - <p style={{ position: "absolute", bottom: 1, left: 22, opacity: this.labelOpacity, transition: "0.7s opacity ease" }}>opacity {this.persistOpacity ? "persistent" : "on focus"}</p> - <PresentationViewList - mainDocument={this.curPresentation} - deleteDocument={this.RemoveDoc} - gotoDocument={this.gotoDocument} - groupMappings={this.groupMappings} - PresElementsMappings={this.presElementsMappings} - setChildrenDocs={this.setChildrenDocs} - presStatus={this.presStatus} - presButtonBackUp={this.presButtonBackUp} - presGroupBackUp={this.presGroupBackUp} - removeDocByRef={this.removeDocByRef} - clearElemMap={() => this.presElementsMappings.clear()} - /> + {this.renderPresMode()} + </div> ); } diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts index da4f459e2..59314783b 100644 --- a/src/new_fields/Doc.ts +++ b/src/new_fields/Doc.ts @@ -12,6 +12,7 @@ import { scriptingGlobal } from "../client/util/Scripting"; import { List } from "./List"; import { DocumentType } from "../client/documents/Documents"; import { ComputedField } from "./ScriptField"; +import { PrefetchProxy } from "./Proxy"; export namespace Field { export function toKeyValueString(doc: Doc, key: string): string { @@ -348,7 +349,7 @@ export namespace Doc { while (proto && !Doc.IsPrototype(proto)) { proto = proto.proto; } - (proto ? proto : doc)[fieldKey + "_ext"] = docExtensionForField; + (proto ? proto : doc)[fieldKey + "_ext"] = new PrefetchProxy(docExtensionForField); }, 0); } else if (doc instanceof Doc) { // backward compatibility -- add fields for docs that don't have them already docExtensionForField.extendsDoc === undefined && setTimeout(() => docExtensionForField.extendsDoc = doc, 0); diff --git a/src/new_fields/SchemaHeaderField.ts b/src/new_fields/SchemaHeaderField.ts index a6df31e81..d5da56b10 100644 --- a/src/new_fields/SchemaHeaderField.ts +++ b/src/new_fields/SchemaHeaderField.ts @@ -48,11 +48,11 @@ export class SchemaHeaderField extends ObjectField { color: string; type: number; - constructor(heading: string = "", color: string = RandomPastel(), type?: ColumnType) { + constructor(heading: string = "", color?: string, type?: ColumnType) { super(); this.heading = heading; - this.color = color; + this.color = color === "" || color === undefined ? RandomPastel() : color; if (type) { this.type = type; } diff --git a/src/scraping/buxton/scraper.py b/src/scraping/buxton/scraper.py index 182b22a1a..1ff0e3b31 100644 --- a/src/scraping/buxton/scraper.py +++ b/src/scraping/buxton/scraper.py @@ -139,7 +139,7 @@ def write_text_doc(content): data_doc = { "_id": data_doc_guid, "fields": { - "proto": protofy("commonImportProto"), + "proto": protofy("textProto"), "data": { "Data": '{"doc":{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"' + content + '"}]}]},"selection":{"type":"text","anchor":1,"head":1}' + '}', "__type": "RichTextField" @@ -235,7 +235,7 @@ def parse_document(file_name: str): count += 1 view_guids.append(write_image(pure_name, image)) copyfile(dir_path + "/" + image, dir_path + - "/" + image.replace(".", "_o.", 1)) + "/" + image.replace(".", "_o.", 1)) os.rename(dir_path + "/" + image, dir_path + "/" + image.replace(".", "_m.", 1)) print(f"extracted {count} images...") diff --git a/src/server/index.ts b/src/server/index.ts index 0afbcc4ee..378485b33 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -440,8 +440,22 @@ app.post(RouteStore.forgot, postForgot); app.get(RouteStore.reset, getReset); app.post(RouteStore.reset, postReset); -app.use(RouteStore.corsProxy, (req, res) => - req.pipe(request(decodeURIComponent(req.url.substring(1)))).pipe(res)); +const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/; +app.use(RouteStore.corsProxy, (req, res) => { + req.pipe(request(decodeURIComponent(req.url.substring(1)))).on("response", res => { + const headers = Object.keys(res.headers); + headers.forEach(headerName => { + const header = res.headers[headerName]; + if (Array.isArray(header)) { + res.headers[headerName] = header.filter(h => !headerCharRegex.test(h)); + } else if (header) { + if (headerCharRegex.test(header as any)) { + delete res.headers[headerName]; + } + } + }); + }).pipe(res); +}); app.get(RouteStore.delete, (req, res) => { if (release) { |