diff options
Diffstat (limited to 'src')
13 files changed, 705 insertions, 31 deletions
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index f7070a862..dd72051fa 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -688,8 +688,8 @@ export class CurrentUserUtils { static schemaTools():Button[] { return [ {title: "Preview", toolTip: "Show selection preview", btnType: ButtonType.ToggleButton, icon: "portrait", scripts:{ onClick: '{ return toggleSchemaPreview(_readOnly_); }'} }, - {title: "1 Line",toolTip: "Single Line Rows", btnType: ButtonType.ToggleButton, icon: "eye", scripts:{ onClick: '{ return toggleSingleLineSchema(_readOnly_); }'} }, - ]; + {title: "1 Line", toolTip: "Single Line Rows", btnType: ButtonType.ToggleButton, icon: "eye", scripts:{ onClick: '{ return toggleSingleLineSchema(_readOnly_); }'} }, + {title: "DataViz", toolTip: "Turn Schema Table into Data Visualization Doc", btnType: ButtonType.ClickButton, icon: "chart-bar", scripts:{ onClick: '{ datavizFromSchema()'} }, ]; } static webTools() { diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index c71c72257..f2a1ca57e 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -31,6 +31,7 @@ import { SnappingManager } from '../util/SnappingManager'; import { Transform } from '../util/Transform'; import { ReportManager } from '../util/reportManager/ReportManager'; import { ComponentDecorations } from './ComponentDecorations'; +import { SchemaCSVPopUp } from './nodes/DataVizBox/SchemaCSVPopUp'; import { ContextMenu } from './ContextMenu'; import { DashboardView } from './DashboardView'; import { DictationOverlay } from './DictationOverlay'; @@ -1071,6 +1072,7 @@ export class MainView extends ObservableReactComponent<{}> { <OverlayView /> {/* {this.mapBoxHack} */} <GPTPopup key="gptpopup" /> + <SchemaCSVPopUp key="schemacsvpopup" /> <GenerativeFill imageEditorOpen={ImageBox.imageEditorOpen} imageEditorSource={ImageBox.imageEditorSource} imageRootDoc={ImageBox.imageRootDoc} addDoc={ImageBox.addDoc} /> {/* <NewLightboxView key="newLightbox" PanelWidth={this._windowWidth} PanelHeight={this._windowHeight} maxBorder={[200, 50]} /> */} </div> diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 1f4688729..88cdd51ff 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -52,6 +52,7 @@ import { CollectionFreeFormPannableContents } from './CollectionFreeFormPannable import { CollectionFreeFormRemoteCursors } from './CollectionFreeFormRemoteCursors'; import './CollectionFreeFormView.scss'; import { MarqueeView } from './MarqueeView'; +import { SchemaCSVPopUp } from '../../nodes/DataVizBox/SchemaCSVPopUp'; export type collectionFreeformViewProps = { NativeWidth?: () => number; @@ -1982,3 +1983,40 @@ ScriptingGlobals.add(function bringToFront() { ScriptingGlobals.add(function sendToBack(doc: Doc) { SelectionManager.Views.forEach(view => view.CollectionFreeFormView?.bringToFront(view.Document, true)); }); +ScriptingGlobals.add(function datavizFromSchema(doc: Doc) { + SelectionManager.Views.forEach(view => { + if (!view.layoutDoc.schema_columnKeys){ + view.layoutDoc.schema_columnKeys = new List<string>(['title', 'type', 'author', 'author_date']) + } + const keys = Cast(view.layoutDoc.schema_columnKeys, listSpec('string'))?.filter(key => key!="text"); + if (!keys) return; + + const children = DocListCast(view.Document[Doc.LayoutFieldKey(view.Document)]); + let csvRows = []; + csvRows.push(keys.join(',')); + for (let i=0; i < children.length; i++) { + let eachRow = []; + for (let j=0; j<keys.length; j++){ + var cell = children[i][keys[j]]; + if (cell && cell as string) cell = cell.toString().replace(/\,/g, ''); + eachRow.push(cell); + } + csvRows.push(eachRow); + } + const blob = new Blob([csvRows.join('\n')], { type: 'text/csv' }); + const options = { x:0, y:-300, title: 'schemaTable', _width: 300, _height: 100, type: 'text/csv' }; + const file = new File([blob], 'schemaTable', options); + const loading = Docs.Create.LoadingDocument(file, options); + loading.presentation_openInLightbox = true; + DocUtils.uploadFileToDoc(file, {}, loading); + + if (view.ComponentView?.addDocument) { + // loading.dataViz_fromSchema = true; + loading.dataViz_asSchema = view.layoutDoc; + SchemaCSVPopUp.Instance.setView(view); + SchemaCSVPopUp.Instance.setTarget(view.layoutDoc); + SchemaCSVPopUp.Instance.setDataVizDoc(loading); + SchemaCSVPopUp.Instance.setVisible(true); + } + }) +}); diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.scss b/src/client/views/nodes/DataVizBox/DataVizBox.scss index 430446c06..a3132dc6e 100644 --- a/src/client/views/nodes/DataVizBox/DataVizBox.scss +++ b/src/client/views/nodes/DataVizBox/DataVizBox.scss @@ -1,4 +1,4 @@ -.dataviz { +.dataViz-box { overflow: auto; height: 100%; width: 100%; @@ -7,9 +7,46 @@ display: flex; flex-direction: row; } + + .dataviz-overlayButton-sidebar { + background: #121721; + height: 25px; + width: 25px; + right: 5px; + display: flex; + position: absolute; + align-items: center; + justify-content: center; + border-radius: 3px; + pointer-events: all; + z-index: 1; // so it appears on top of the document's title, if shown + + // box-shadow: $standard-box-shadow; + // transition: 0.2s; + + &:hover { + filter: brightness(0.85); + } + } + + .dataviz-sidebar { + position: absolute; + right: 0; + top: 0; + height: 100%; + } .button-container { pointer-events: unset; } + + .dataVizBox-annotationLayer{ + position: absolute; + transform-origin: left top; + top: 0; + width: 100%; + pointer-events: none; + mix-blend-mode: multiply; + } } .start-message { margin: 10px; diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx index e31c04c40..56c130e0f 100644 --- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx +++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx @@ -1,12 +1,12 @@ -import { Toggle, ToggleType, Type } from 'browndash-components'; -import { action, computed, ObservableMap } from 'mobx'; +import { Colors, Toggle, ToggleType, Type } from 'browndash-components'; +import { action, computed, observable, ObservableMap, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { Doc, Field, StrListCast } from '../../../../fields/Doc'; +import { Doc, DocListCast, Field, Opt, StrListCast } from '../../../../fields/Doc'; import { List } from '../../../../fields/List'; -import { Cast, CsvCast, StrCast } from '../../../../fields/Types'; +import { Cast, CsvCast, DocCast, NumCast, StrCast } from '../../../../fields/Types'; import { CsvField } from '../../../../fields/URLField'; -import { Docs } from '../../../documents/Documents'; +import { DocUtils, Docs } from '../../../documents/Documents'; import { ViewBoxAnnotatableComponent } from '../../DocComponent'; import { FieldView, FieldViewProps } from '../FieldView'; import { PinProps } from '../trails'; @@ -15,6 +15,22 @@ import { LineChart } from './components/LineChart'; import { PieChart } from './components/PieChart'; import { TableBox } from './components/TableBox'; import './DataVizBox.scss'; +import { UndoManager, undoable } from '../../../util/UndoManager'; +import { SidebarAnnos } from '../../SidebarAnnos'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Utils, emptyFunction, returnEmptyString, returnFalse, setupMoveUpEvents } from '../../../../Utils'; +import { DocumentView } from '../DocumentView'; +import { DocumentManager } from '../../../util/DocumentManager'; +import { TraceMobx } from '../../../../fields/util'; +import { MarqueeAnnotator } from '../../MarqueeAnnotator'; +import { InkTool } from '../../../../fields/InkField'; +import { AnchorMenu } from '../../pdf/AnchorMenu'; +import { GPTPopup } from '../../pdf/GPTPopup/GPTPopup'; +import { CollectionFreeFormView } from '../../collections/collectionFreeForm'; +import { ContextMenu } from '../../ContextMenu'; +import { gptImageCall } from '../../../apis/gpt/GPT'; +import { Networking } from '../../../Network'; +import { listSpec } from '../../../../fields/Schema'; export enum DataVizView { TABLE = 'table', @@ -25,6 +41,44 @@ export enum DataVizView { @observer export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { + private _getAnchor: (savedAnnotations: Opt<ObservableMap<number, HTMLDivElement[]>>, addAsAnnotation: boolean) => Opt<Doc> = () => undefined; + private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); + private _ffref = React.createRef<CollectionFreeFormView>(); + private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef(); + anchorMenuClick?: () => undefined | ((anchor: Doc) => void); + crop: ((region: Doc | undefined, addCrop?: boolean) => Doc | undefined) | undefined; + @observable schemaDataVizChildren: any; + @observable _marqueeing: number[] | undefined; + @observable _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>(); + @computed get annotationLayer() { + TraceMobx(); + return <div className="dataVizBox-annotationLayer" style={{ height: this._props.PanelHeight(), width: this._props.PanelWidth() }} ref={this._annotationLayer} />; + } + marqueeDown = (e: React.PointerEvent) => { + if (!e.altKey && e.button === 0 && NumCast(this.Document._freeform_scale, 1) <= NumCast(this.Document.freeform_scaleMin, 1) && this._props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) { + setupMoveUpEvents( + this, + e, + action(e => { + MarqueeAnnotator.clearAnnotations(this._savedAnnotations); + //this._marqueeing = [e.pageX, e.pageY]; + this._marqueeing = [e.clientX, e.clientY]; + return true; + }), + returnFalse, + () => MarqueeAnnotator.clearAnnotations(this._savedAnnotations), + false + ); + } + }; + @action + finishMarquee = () => { + this._getAnchor = AnchorMenu.Instance?.GetAnchor; + this._marqueeing = undefined; + this._props.select(false); + }; + savedAnnotations = () => this._savedAnnotations; + public static LayoutString(fieldStr: string) { return FieldView.LayoutString(DataVizBox, fieldStr); } @@ -32,6 +86,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { // all datasets that have been retrieved from the server stored as a map from the dataset url to an array of records static dataset = new ObservableMap<string, { [key: string]: string }[]>(); private _vizRenderer: LineChart | Histogram | PieChart | undefined; + private _sidebarRef = React.createRef<SidebarAnnos>(); // all CSV records in the dataset (that aren't an empty row) @computed.struct get records() { @@ -74,10 +129,17 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { return func() ?? false; }; getAnchor = (addAsAnnotation?: boolean, pinProps?: PinProps) => { + const visibleAnchor = this._getAnchor?.(this._savedAnnotations, false); // use marquee anchor, otherwise, save zoom/pan as anchor const anchor = !pinProps ? this.Document : this._vizRenderer?.getAnchor(pinProps) ?? + visibleAnchor ?? Docs.Create.ConfigDocument({ + title: 'ImgAnchor:' + this.Document.title, + config_panX: NumCast(this.layoutDoc._freeform_panX), + config_panY: NumCast(this.layoutDoc._freeform_panY), + config_viewScale: Cast(this.layoutDoc._freeform_scale, 'number', null), + annotationOn: this.Document, // when we clear selection -> we should have it so chartBox getAnchor returns undefined // this is for when we want the whole doc (so when the chartBox getAnchor returns without a marker) /*put in some options*/ @@ -93,10 +155,101 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { anchor[key] = this.layoutDoc[key]; } }); + this.addDocument(anchor); + //addAsAnnotation && this.addDocument(anchor); return anchor; }; + createNoteAnnotation = () => { + const createFunc = undoable( + action(() => { + const note = this._sidebarRef.current?.anchorMenuClick(this.getAnchor(false), ['latitude', 'longitude', '-linkedTo']); + }), + 'create note annotation' + ); + if (!this.layoutDoc.layout_showSidebar) { + this.toggleSidebar(); + setTimeout(createFunc); + } else createFunc(); + }; + + @observable _showSidebar = false; + @observable _previewNativeWidth: Opt<number> = undefined; + @observable _previewWidth: Opt<number> = undefined; + @action + toggleSidebar = () => { + const prevWidth = this.sidebarWidth(); + this.layoutDoc._layout_showSidebar = (this.layoutDoc._layout_sidebarWidthPercent = StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%') === '0%' ? `${(100 * 0.2) / 1.2}%` : '0%') !== '0%'; + this.layoutDoc._width = this.layoutDoc._layout_showSidebar ? NumCast(this.layoutDoc._width) * 1.2 : Math.max(20, NumCast(this.layoutDoc._width) - prevWidth); + }; + @computed get SidebarShown() { + return this.layoutDoc._layout_showSidebar ? true : false; + } + @computed get sidebarHandle() { + return ( + <div + className="dataviz-overlayButton-sidebar" + key="sidebar" + title="Toggle Sidebar" + style={{ + display: !this._props.isContentActive() ? 'none' : undefined, + top: StrCast(this.Document._layout_showTitle) === 'title' ? 20 : 5, + backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK, + }} + onPointerDown={this.sidebarBtnDown}> + <FontAwesomeIcon style={{ color: Colors.WHITE }} icon={'comment-alt'} size="sm" /> + </div> + ); + } + /** + * Toggle sidebar onclick the tiny comment button on the top right corner + * @param e + */ + sidebarBtnDown = (e: React.PointerEvent) => { + setupMoveUpEvents( + this, + e, + (e, down, delta) => + runInAction(() => { + const localDelta = this._props + .ScreenToLocalTransform() + .scale(this._props.NativeDimScaling?.() || 1) + .transformDirection(delta[0], delta[1]); + const fullWidth = this._props.width; + const mapWidth = fullWidth! - this.sidebarWidth(); + if (this.sidebarWidth() + localDelta[0] > 0) { + this.layoutDoc._layout_showSidebar = true; + this.layoutDoc._width = fullWidth! + localDelta[0]; + this.layoutDoc._layout_sidebarWidthPercent = ((100 * (this.sidebarWidth() + localDelta[0])) / (fullWidth! + localDelta[0])).toString() + '%'; + } else { + this.layoutDoc._layout_showSidebar = false; + this.layoutDoc._width = mapWidth; + this.layoutDoc._layout_sidebarWidthPercent = '0%'; + } + return false; + }), + emptyFunction, + () => UndoManager.RunInBatch(this.toggleSidebar, 'toggle sidebar') + ); + }; + getView = async (doc: Doc) => { + if (this._sidebarRef?.current?.makeDocUnfiltered(doc) && !this.SidebarShown) this.toggleSidebar(); + return new Promise<Opt<DocumentView>>(res => DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv))); + }; + @computed get sidebarWidthPercent() { + return StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%'); + } + @computed get sidebarColor() { + return StrCast(this.layoutDoc.sidebar_color, StrCast(this.layoutDoc[this._props.fieldKey + '_backgroundColor'], '#e4e4e4')); + } + sidebarWidth = () => (Number(this.sidebarWidthPercent.substring(0, this.sidebarWidthPercent.length - 1)) / 100) * this._props.PanelWidth(); + sidebarAddDocument = (doc: Doc | Doc[], sidebarKey?: string) => { + if (!this.SidebarShown) this.toggleSidebar(); + return this.addDocument(doc, sidebarKey); + }; + sidebarRemoveDocument = (doc: Doc | Doc[], sidebarKey?: string) => this.removeDocument(doc, sidebarKey); + componentDidMount() { this._props.setContentView?.(this); if (!DataVizBox.dataset.has(CsvCast(this.dataDoc[this.fieldKey]).url.href)) this.fetchData(); @@ -116,13 +269,14 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { layoutDoc: this.layoutDoc, records: this.records, axes: this.axes, + //width: this.SidebarShown? this._props.PanelWidth()*.9/1.2: this._props.PanelWidth() * 0.9, height: (this._props.PanelHeight() / scale - 32) /* height of 'change view' button */ * 0.9, width: (this._props.PanelWidth() / scale) * 0.9, margin: { top: 10, right: 25, bottom: 75, left: 45 }, }; if (!this.records.length) return 'no data/visualization'; switch (this.dataVizView) { - case DataVizView.TABLE: return <TableBox {...sharedProps} docView={this._props.DocumentView} selectAxes={this.selectAxes} />; + case DataVizView.TABLE: return <TableBox {...sharedProps} docView={this.DocumentView} selectAxes={this.selectAxes} />; case DataVizView.LINECHART: return <LineChart {...sharedProps} dataDoc={this.dataDoc} fieldKey={this.fieldKey} ref={r => (this._vizRenderer = r ?? undefined)} />; case DataVizView.HISTOGRAM: return <Histogram {...sharedProps} dataDoc={this.dataDoc} fieldKey={this.fieldKey} ref={r => (this._vizRenderer = r ?? undefined)} />; case DataVizView.PIECHART: return <PieChart {...sharedProps} dataDoc={this.dataDoc} fieldKey={this.fieldKey} ref={r => (this._vizRenderer = r ?? undefined)} @@ -130,15 +284,81 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { } // prettier-ignore } + @action + onPointerDown = (e: React.PointerEvent): void => { + if ((this.Document._freeform_scale || 1) !== 1) return; + if (!e.altKey && e.button === 0 && this._props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) { + this._props.select(false); + MarqueeAnnotator.clearAnnotations(this._savedAnnotations); + this._marqueeing = [e.clientX, e.clientY]; + const target = e.target as any; + if (e.target && (target.className.includes('endOfContent') || (target.parentElement.className !== 'textLayer' && target.parentElement.parentElement?.className !== 'textLayer'))) { + } else { + // if textLayer is hit, then we select text instead of using a marquee so clear out the marquee. + setTimeout( + action(() => (this._marqueeing = undefined)), + 100 + ); // bcz: hack .. anchor menu is setup within MarqueeAnnotator so we need to at least create the marqueeAnnotator even though we aren't using it. + + document.addEventListener('pointerup', this.onSelectEnd); + } + } + }; + + @action + onSelectEnd = (e: PointerEvent): void => { + this._props.select(false); + document.removeEventListener('pointerup', this.onSelectEnd); + + const sel = window.getSelection(); + if (sel) { + AnchorMenu.Instance.setSelectedText(sel.toString()); + } + + if (sel?.type === 'Range') { + AnchorMenu.Instance.jumpTo(e.clientX, e.clientY); + } + + // Changing which document to add the annotation to (the currently selected PDF) + GPTPopup.Instance.setSidebarId('data_sidebar'); + GPTPopup.Instance.addDoc = this.sidebarAddDocument; + }; + + @action + updateSchemaViz = () => { + const getFrom = DocCast(this.layoutDoc.dataViz_asSchema) + const keys = Cast(getFrom.schema_columnKeys, listSpec('string'))?.filter(key => key!="text"); + if (!keys) return; + const children = DocListCast(getFrom[Doc.LayoutFieldKey(getFrom)]); + var current: {[key: string]: string}[] = [] + for (let i=1; i<children.length; i++){ + var row: {[key:string]: string} = {}; + if (children[i]){ + for (let j=0; j<keys.length; j++){ + var cell = children[i][keys[j]]; + if (cell && cell as string) cell = cell.toString().replace(/\,/g, ''); + row[keys[j]] = StrCast(cell) + } + } + current.push(row) + } + DataVizBox.dataset.set(CsvCast(this.Document[this.fieldKey]).url.href, current ) + } + render() { - const scale = this._props.NativeDimScaling?.() || 1; + if (this.layoutDoc && this.layoutDoc.dataViz_asSchema){ + this.schemaDataVizChildren = DocListCast(DocCast(this.layoutDoc.dataViz_asSchema)[Doc.LayoutFieldKey(DocCast(this.layoutDoc.dataViz_asSchema))]).length + this.updateSchemaViz(); + } + const scale = this._props.NativeDimScaling?.() || 1; return !this.records.length ? ( // displays how to get data into the DataVizBox if its empty <div className="start-message">To create a DataViz box, either import / drag a CSV file into your canvas or copy a data table and use the command 'ctrl + p' to bring the data table to your canvas.</div> ) : ( <div - className="dataViz" + className="dataViz-box" + onPointerDown={this.marqueeDown} style={{ pointerEvents: this._props.isContentActive() === true ? 'all' : 'none', width: `${100 / scale}%`, @@ -147,23 +367,76 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { position: 'absolute', }} onWheel={e => e.stopPropagation()} - ref={r => - r?.addEventListener( - 'wheel', // if scrollTop is 0, then don't let wheel trigger scroll on any container (which it would since onScroll won't be triggered on this) - (e: WheelEvent) => { - if (!r.scrollTop && e.deltaY <= 0) e.preventDefault(); - e.stopPropagation(); - }, - { passive: false } - ) - }> + ref={this._mainCont} + > <div className={'datatype-button'}> <Toggle text={' TABLE '} toggleType={ToggleType.BUTTON} type={Type.SEC} color={'black'} onClick={e => (this.layoutDoc._dataViz = DataVizView.TABLE)} toggleStatus={this.layoutDoc._dataViz === DataVizView.TABLE} /> <Toggle text={'LINECHART'} toggleType={ToggleType.BUTTON} type={Type.SEC} color={'black'} onClick={e => (this.layoutDoc._dataViz = DataVizView.LINECHART)} toggleStatus={this.layoutDoc._dataViz === DataVizView.LINECHART} /> <Toggle text={'HISTOGRAM'} toggleType={ToggleType.BUTTON} type={Type.SEC} color={'black'} onClick={e => (this.layoutDoc._dataViz = DataVizView.HISTOGRAM)} toggleStatus={this.layoutDoc._dataViz === DataVizView.HISTOGRAM} /> <Toggle text={'PIE CHART'} toggleType={ToggleType.BUTTON} type={Type.SEC} color={'black'} onClick={e => (this.layoutDoc._dataViz = DataVizView.PIECHART)} toggleStatus={this.layoutDoc._dataViz == -DataVizView.PIECHART} /> </div> + + {/* <CollectionFreeFormView + ref={this._ffref} + {...this._props} + setContentView={emptyFunction} + renderDepth={this._props.renderDepth - 1} + fieldKey={this.annotationKey} + styleProvider={this._props.styleProvider} + isAnnotationOverlay={true} + annotationLayerHostsContent={false} + PanelWidth={this._props.PanelWidth} + PanelHeight={this._props.PanelHeight} + select={emptyFunction} + isAnyChildContentActive={returnFalse} + whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} + removeDocument={this.removeDocument} + moveDocument={this.moveDocument} + addDocument={this.addDocument}> + {this.renderVizView} + </CollectionFreeFormView> */} + {this.renderVizView} + <div className="dataviz-sidebar" + style={{ width: `${this.sidebarWidthPercent}`, + backgroundColor: `${this.sidebarColor}` }} + onPointerDown={this.onPointerDown}> + <SidebarAnnos + ref={this._sidebarRef} + {...this._props} + fieldKey={this.fieldKey} + rootDoc={this.Document} + layoutDoc={this.layoutDoc} + dataDoc={this.dataDoc} + usePanelWidth={true} + showSidebar={this.SidebarShown} + nativeWidth={NumCast(this.layoutDoc._nativeWidth)} + whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} + PanelWidth={this.sidebarWidth} + sidebarAddDocument={this.sidebarAddDocument} + moveDocument={this.moveDocument} + removeDocument={this.sidebarRemoveDocument} + /> + </div> + {this.sidebarHandle} + {this.annotationLayer} + {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? null : ( + <MarqueeAnnotator + rootDoc={this.Document} + getPageFromScroll={undefined} + anchorMenuClick={this.anchorMenuClick} + scrollTop={0} + annotationLayerScrollTop={NumCast(this.Document._layout_scrollTop)} + addDocument={this.sidebarAddDocument} + docView={this.DocumentView!} + finishMarquee={this.finishMarquee} + savedAnnotations={this.savedAnnotations} + selectionText={returnEmptyString} + annotationLayer={this._annotationLayer.current} + mainCont={this._mainCont.current} + anchorMenuCrop={this.crop} + /> + )} </div> ); } diff --git a/src/client/views/nodes/DataVizBox/SchemaCSVPopUp.scss b/src/client/views/nodes/DataVizBox/SchemaCSVPopUp.scss new file mode 100644 index 000000000..63a693918 --- /dev/null +++ b/src/client/views/nodes/DataVizBox/SchemaCSVPopUp.scss @@ -0,0 +1,175 @@ +$textgrey: #707070; +$lighttextgrey: #a3a3a3; +$greyborder: #d3d3d3; +$lightgrey: #ececec; +$button: #5b97ff; +$highlightedText: #82e0ff; + +.summary-box { + position: fixed; + bottom: 10px; + right: 10px; + width: 250px; + min-height: 200px; + border-radius: 15px; + padding: 15px; + padding-bottom: 0; + z-index: 999; + display: flex; + flex-direction: column; + justify-content: space-between; + background-color: #ffffff; + box-shadow: 0 2px 5px #7474748d; + color: $textgrey; + + .summary-heading { + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid $greyborder; + padding-bottom: 5px; + + .summary-text { + font-size: 12px; + font-weight: 500; + } + } + + label { + color: $textgrey; + font-size: 12px; + font-weight: 400; + letter-spacing: 1px; + margin: 0; + padding-right: 5px; + } + + a { + cursor: pointer; + } + + .content-wrapper { + padding-top: 10px; + min-height: 50px; + max-height: 150px; + overflow-y: auto; + } + + .btns-wrapper { + height: 50px; + display: flex; + justify-content: space-between; + align-items: center; + + .summarizing { + display: flex; + align-items: center; + } + } + + button { + font-size: 9px; + padding: 10px; + color: #ffffff; + background-color: $button; + border-radius: 5px; + } + + .text-btn { + &:hover { + background-color: $button; + } + } + + .btn-secondary { + font-size: 8px; + padding: 10px 5px; + background-color: $lightgrey; + color: $textgrey; + &:hover { + background-color: $lightgrey; + } + } + + .icon-btn { + background-color: #ffffff; + padding: 10px; + border-radius: 50%; + color: $button; + border: 1px solid $button; + } +} + +.image-content-wrapper { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + padding-bottom: 16px; + + .img-wrapper { + position: relative; + cursor: pointer; + + .img-container { + pointer-events: none; + position: relative; + + img { + pointer-events: all; + position: relative; + } + } + + .img-container::after { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + opacity: 0; + transition: opacity 0.3s ease; + } + + .btn-container { + position: absolute; + right: 8px; + bottom: 8px; + opacity: 0; + transition: opacity 0.3s ease; + } + + &:hover { + .img-container::after { + opacity: 1; + } + + .btn-container { + opacity: 1; + } + } + } +} + +// Typist CSS +.Typist .Cursor { + display: inline-block; +} +.Typist .Cursor--blinking { + opacity: 1; + animation: blink 1s linear infinite; +} + +@keyframes blink { + 0% { + opacity: 1; + } + 50% { + opacity: 0; + } + 100% { + opacity: 1; + } +} diff --git a/src/client/views/nodes/DataVizBox/SchemaCSVPopUp.tsx b/src/client/views/nodes/DataVizBox/SchemaCSVPopUp.tsx new file mode 100644 index 000000000..3cb5125da --- /dev/null +++ b/src/client/views/nodes/DataVizBox/SchemaCSVPopUp.tsx @@ -0,0 +1,111 @@ +import * as React from 'react'; +import './SchemaCSVPopUp.scss'; +import { action, makeObservable, observable } from 'mobx'; +import { observer } from 'mobx-react'; +import { Doc } from '../../../../fields/Doc'; +import { Button, IconButton, Type } from 'browndash-components'; +import { StrCast } from '../../../../fields/Types'; +import { MarqueeView } from '../../collections/collectionFreeForm/MarqueeView'; +import { Utils, emptyFunction, setupMoveUpEvents } from '../../../../Utils'; +import { DragManager } from '../../../util/DragManager'; +import { DocumentView } from '../DocumentView'; +import { CgClose } from 'react-icons/cg'; + +interface SchemaCSVPopUpProps {} + +@observer +export class SchemaCSVPopUp extends React.Component<SchemaCSVPopUpProps> { + static Instance: SchemaCSVPopUp; + + @observable + public dataVizDoc: Doc | undefined = undefined; + @action + public setDataVizDoc = (doc: Doc) => { + this.dataVizDoc = doc; + }; + + @observable + public view: DocumentView | undefined = undefined; + @action + public setView = (docView: DocumentView) => { + this.view = docView; + }; + + @observable + public target: Doc | undefined = undefined; + @action + public setTarget = (doc: Doc) => { + this.target = doc; + }; + + @observable + public visible: boolean = false; + @action + public setVisible = (vis: boolean) => { + this.visible = vis; + }; + + constructor(props: SchemaCSVPopUpProps) { + super(props); + makeObservable(this); + SchemaCSVPopUp.Instance = this; + } + + dataBox = () => { + return ( + <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}> + {this.heading('Schema Table as Data Visualization Doc')} + <div className="image-content-wrapper"> + <div className="img-wrapper"> + <div className="img-container" onPointerDown={e => this.drag(e)}> + <img + width={150} height={150} src={"/assets/dataVizBox.png"} + /> + </div> + </div> + </div> + </div> + ); + }; + + heading = (headingText: string) => ( + <div className="summary-heading"> + <label className="summary-text">{headingText}</label> + <IconButton color={StrCast(Doc.UserDoc().userVariantColor)} tooltip="close" icon={<CgClose size="16px" />} onClick={() => this.setVisible(false)} /> + </div> + ); + + drag = (e: React.PointerEvent) => { + const downX = e.clientX; + const downY = e.clientY; + setupMoveUpEvents( + {}, + e, + e => { + const sourceAnchorCreator = () => this.dataVizDoc!; + const targetCreator = (annotationOn: Doc | undefined) => { + const embedding = Doc.MakeEmbedding(this.dataVizDoc!); + return embedding; + }; + if (this.view && sourceAnchorCreator && !Utils.isClick(e.clientX, e.clientY, downX, downY, Date.now())) { + DragManager.StartAnchorAnnoDrag(e.target instanceof HTMLElement ? [e.target] : [], + new DragManager.AnchorAnnoDragData(this.view, sourceAnchorCreator, targetCreator), downX, downY, { + dragComplete: e => {this.setVisible(false);}, + }); + return true; + } + return false; + }, + emptyFunction, + action(e => {}) + ); + }; + + render() { + return ( + <div className="summary-box" style={{ display: this.visible ? 'flex' : 'none' }}> + {this.dataBox()} + </div> + ); + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/DataVizBox/components/Chart.scss b/src/client/views/nodes/DataVizBox/components/Chart.scss index c0c0f10a2..2f7dd0487 100644 --- a/src/client/views/nodes/DataVizBox/components/Chart.scss +++ b/src/client/views/nodes/DataVizBox/components/Chart.scss @@ -4,7 +4,7 @@ flex-direction: column; align-items: center; cursor: default; - margin-top: 10px; + margin-top: 30px; overflow-y: visible; .graph { @@ -15,8 +15,8 @@ font-size: larger; display: flex; flex-direction: row; - margin-top: -10px; - margin-bottom: -10px; + margin-top: -20px; + margin-bottom: -20px; } .asHistogram-checkBox { // display: flex; @@ -93,6 +93,7 @@ display: flex; flex-direction: column; cursor: default; + margin-top: 30px; height: calc(100% - 40px); // bcz: hack 40px is the size of the button rows .tableBox-container { overflow: scroll; @@ -111,6 +112,7 @@ white-space: pre; max-width: 150; overflow: hidden; + margin-left: 2px; } } } diff --git a/src/client/views/nodes/DataVizBox/components/Histogram.tsx b/src/client/views/nodes/DataVizBox/components/Histogram.tsx index d786e84ad..227c993c7 100644 --- a/src/client/views/nodes/DataVizBox/components/Histogram.tsx +++ b/src/client/views/nodes/DataVizBox/components/Histogram.tsx @@ -461,7 +461,7 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { if (this._histogramData.length > 0 || !this.parentViz) { return this._props.axes.length >= 1 ? ( - <div className="chart-container"> + <div className="chart-container" style={{width: this._props.width+this._props.margin.right}}> <div className="graph-title"> <EditableText val={StrCast(this._props.layoutDoc[titleAccessor])} diff --git a/src/client/views/nodes/DataVizBox/components/LineChart.tsx b/src/client/views/nodes/DataVizBox/components/LineChart.tsx index abb95aa4c..4b0df0457 100644 --- a/src/client/views/nodes/DataVizBox/components/LineChart.tsx +++ b/src/client/views/nodes/DataVizBox/components/LineChart.tsx @@ -1,4 +1,4 @@ -import { EditableText, Size } from 'browndash-components'; +import { Button, EditableText, Size } from 'browndash-components'; import * as d3 from 'd3'; import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; @@ -24,6 +24,7 @@ export interface SelectedDataPoint extends DataPoint { elem?: d3.Selection<d3.BaseType, unknown, SVGGElement, unknown>; } export interface LineChartProps { + vizBox: DataVizBox; Document: Doc; layoutDoc: Doc; axes: string[]; @@ -358,7 +359,7 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { const selectedPt = this._currSelected ? `{ ${this._props.axes[0]}: ${this._currSelected.x} ${this._props.axes[1]}: ${this._currSelected.y} }` : 'none'; if (this._lineChartData.length > 0 || !this.parentViz || this.parentViz.length == 0) { return this._props.axes.length >= 2 && /\d/.test(this._props.records[0][this._props.axes[0]]) && /\d/.test(this._props.records[0][this._props.axes[1]]) ? ( - <div className="chart-container"> + <div className="chart-container" style={{width: this.props.width+this.props.margin.right}}> <div className="graph-title"> <EditableText val={StrCast(this._props.layoutDoc[titleAccessor])} @@ -372,7 +373,16 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { /> </div> <div ref={this._lineChartRef} /> - {selectedPt != 'none' ? <div className={'selected-data'}> {`Selected: ${selectedPt}`}</div> : null} + {selectedPt != 'none' ? + <div className={'selected-data'}> + {`Selected: ${selectedPt}`} + <Button onClick={e=>{ + console.log("test plzz") + this.props.vizBox.sidebarBtnDown; + this.props.vizBox.sidebarAddDocument;} + }></Button> + </div> + : null} </div> ) : ( <span className="chart-container"> {'first use table view to select two numerical axes to plot'}</span> diff --git a/src/client/views/nodes/DataVizBox/components/PieChart.tsx b/src/client/views/nodes/DataVizBox/components/PieChart.tsx index 9e7265b26..e644870da 100644 --- a/src/client/views/nodes/DataVizBox/components/PieChart.tsx +++ b/src/client/views/nodes/DataVizBox/components/PieChart.tsx @@ -356,7 +356,7 @@ export class PieChart extends ObservableReactComponent<PieChartProps> { if (this._pieChartData.length > 0 || !this.parentViz) { return this._props.axes.length >= 1 ? ( - <div className="chart-container"> + <div className="chart-container" style={{width: this._props.width+this._props.margin.right}}> <div className="graph-title"> <EditableText val={StrCast(this._props.layoutDoc[titleAccessor])} diff --git a/src/client/views/nodes/DataVizBox/components/TableBox.tsx b/src/client/views/nodes/DataVizBox/components/TableBox.tsx index f77c138df..16e66d0c3 100644 --- a/src/client/views/nodes/DataVizBox/components/TableBox.tsx +++ b/src/client/views/nodes/DataVizBox/components/TableBox.tsx @@ -87,7 +87,7 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> { return (this.viewScale * this._tableHeight) / this._tableDataIds.length; } @computed get startID() { - return this.rowHeight ? Math.floor(this._scrollTop / this.rowHeight) : 0; + return this.rowHeight ? Math.max(Math.floor(this._scrollTop / this.rowHeight)-1, 0) : 0; } @computed get endID() { console.log('start = ' + this.startID + ' container = ' + this._tableContainerHeight + ' scale = ' + this.viewScale + ' row = ' + this.rowHeight); @@ -169,6 +169,7 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> { return ( <div className="tableBox" + style={{width: this.props.width+this.props.margin.right}} tabIndex={0} onKeyDown={e => { if (this._props.layoutDoc && e.key === 'a' && (e.ctrlKey || e.metaKey)) { diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.tsx b/src/client/views/pdf/GPTPopup/GPTPopup.tsx index cdd0cb95a..db21d9a3d 100644 --- a/src/client/views/pdf/GPTPopup/GPTPopup.tsx +++ b/src/client/views/pdf/GPTPopup/GPTPopup.tsx @@ -127,7 +127,7 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { console.log(err); return ''; } - GPTPopup.Instance.setLoading(false); + this.setLoading(false); }; /** @@ -213,6 +213,31 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> { ); }; + data = () => { + return ( + <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}> + {this.heading('GENERATED IMAGE')} + <div className="image-content-wrapper"> + {this.imgUrls.map(rawSrc => ( + <div className="img-wrapper"> + <div className="img-container"> + <img key={rawSrc[0]} src={rawSrc[0]} width={150} height={150} alt="dalle generation" /> + </div> + <div className="btn-container"> + <Button text="Save Image" onClick={() => this.transferToImage(rawSrc[1])} color={StrCast(Doc.UserDoc().userColor)} type={Type.TERT} /> + </div> + </div> + ))} + </div> + {!this.loading && ( + <> + <IconButton tooltip="Generate Again" onClick={this.generateImage} icon={<FontAwesomeIcon icon="redo-alt" size="lg" />} color={StrCast(Doc.UserDoc().userVariantColor)} /> + </> + )} + </div> + ); + }; + summaryBox = () => ( <> <div> |