diff options
Diffstat (limited to 'src/client/views/nodes')
| -rw-r--r-- | src/client/views/nodes/DataVizBox/DataVizBox.tsx | 305 | ||||
| -rw-r--r-- | src/client/views/nodes/DataVizBox/DocCreatorMenu.scss | 1036 | ||||
| -rw-r--r-- | src/client/views/nodes/DataVizBox/DocCreatorMenu.tsx | 2335 | ||||
| -rw-r--r-- | src/client/views/nodes/DataVizBox/TemplateDocTypes.tsx | 0 | ||||
| -rw-r--r-- | src/client/views/nodes/DataVizBox/components/TableBox.tsx | 21 | ||||
| -rw-r--r-- | src/client/views/nodes/DocumentIcon.tsx | 9 | ||||
| -rw-r--r-- | src/client/views/nodes/DocumentView.scss | 6 | ||||
| -rw-r--r-- | src/client/views/nodes/DocumentView.tsx | 4 | ||||
| -rw-r--r-- | src/client/views/nodes/FieldView.tsx | 2 | ||||
| -rw-r--r-- | src/client/views/nodes/ImageBox.tsx | 25 | ||||
| -rw-r--r-- | src/client/views/nodes/LabelBox.tsx | 4 | ||||
| -rw-r--r-- | src/client/views/nodes/formattedText/FormattedTextBox.scss | 29 | ||||
| -rw-r--r-- | src/client/views/nodes/formattedText/FormattedTextBox.tsx | 8 |
13 files changed, 3744 insertions, 40 deletions
diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx index df6e74d85..12196f290 100644 --- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx +++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx @@ -5,14 +5,14 @@ import { Colors, Toggle, ToggleType, Type } from 'browndash-components'; import { IReactionDisposer, ObservableMap, action, computed, makeObservable, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { returnEmptyString, returnFalse, returnOne, setupMoveUpEvents } from '../../../../ClientUtils'; +import { ClientUtils, returnEmptyString, returnFalse, returnOne, setupMoveUpEvents } from '../../../../ClientUtils'; import { emptyFunction } from '../../../../Utils'; -import { Doc, DocListCast, Field, Opt, StrListCast } from '../../../../fields/Doc'; +import { Doc, DocListCast, Field, FieldType, NumListCast, Opt, StrListCast } from '../../../../fields/Doc'; import { InkTool } from '../../../../fields/InkField'; import { List } from '../../../../fields/List'; import { Cast, CsvCast, DocCast, NumCast, StrCast } from '../../../../fields/Types'; import { CsvField } from '../../../../fields/URLField'; -import { TraceMobx } from '../../../../fields/util'; +import { GetEffectiveAcl, TraceMobx, inheritParentAcls } from '../../../../fields/util'; import { DocUtils } from '../../../documents/DocUtils'; import { DocumentType } from '../../../documents/DocumentTypes'; import { Docs } from '../../../documents/Documents'; @@ -32,6 +32,18 @@ import { Histogram } from './components/Histogram'; import { LineChart } from './components/LineChart'; import { PieChart } from './components/PieChart'; import { TableBox } from './components/TableBox'; +import { LinkManager } from '../../../util/LinkManager'; +import { Col, DataVizTemplateInfo, DataVizTemplateLayout, DocCreatorMenu, TemplateFieldSize, LayoutType, TemplateFieldType } from './DocCreatorMenu'; +import { CollectionFreeFormView, MarqueeView } from '../../collections/collectionFreeForm'; +import { PrefetchProxy } from '../../../../fields/Proxy'; +import { AclAdmin, AclAugment, AclEdit } from '../../../../fields/DocSymbols'; +import { template } from 'lodash'; +import { data } from 'jquery'; +import { listSpec } from '../../../../fields/Schema'; +import { ObjectField } from '../../../../fields/ObjectField'; +import { Id } from '../../../../fields/FieldSymbols'; +import { GPTCallType, gptAPICall } from '../../../apis/gpt/GPT'; +import { TbSortDescendingShapes } from 'react-icons/tb'; export enum DataVizView { TABLE = 'table', @@ -42,6 +54,7 @@ export enum DataVizView { @observer export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { + private _urlError: boolean = false; private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); private _marqueeref = React.createRef<MarqueeAnnotator>(); private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef(); @@ -50,7 +63,11 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { sidebarAddDoc: ((doc: Doc | Doc[], sidebarKey?: string | undefined) => boolean) | undefined; crop: ((region: Doc | undefined, addCrop?: boolean) => Doc | undefined) | undefined; @observable _marqueeing: number[] | undefined = undefined; - @observable _savedAnnotations = new ObservableMap<number, (HTMLDivElement & { marqueeing?: boolean })[]>(); + @observable _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>(); + @observable _specialHighlightedRow: number | undefined = undefined; + @observable GPTSummary: ObservableMap<string, {desc?: string, type?: TemplateFieldType, size?: TemplateFieldSize}> | undefined = undefined; + @observable colsInfo: ObservableMap<string, Col> = new ObservableMap(); + @observable _GPTLoading: boolean = false; constructor(props: FieldViewProps) { super(props); @@ -100,8 +117,17 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { // all CSV records in the dataset (that aren't an empty row) @computed.struct get records() { - const records = DataVizBox.dataset.get(CsvCast(this.dataDoc[this.fieldKey]).url.href); - return records?.filter(record => Object.keys(record).some(key => record[key])) ?? []; + try { + const records = DataVizBox.dataset.get(CsvCast(this.dataDoc[this.fieldKey]).url.href); + this._urlError = false; + return records?.filter(record => Object.keys(record).some(key => record[key])) ?? []; + } catch (e){ + this._urlError = true; + const data: { [key: string]: string; }[] = [ + { error: "Data not found"}, + ]; + return data; + } } // currently chosen visualization type: line, pie, histogram, table @@ -125,6 +151,61 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { this.layoutDoc._dataViz_titleCol = titleCol; }; + @action setSpecialHighlightedRow = (row: number | undefined) => { + this._specialHighlightedRow = row; + } + + @action setColumnType = (colTitle: string, type: TemplateFieldType) => { + const colInfo = this.colsInfo.get(colTitle); + if (colInfo) { + colInfo.type = type; + } else { + this.colsInfo.set(colTitle, {title: colTitle, desc: '', type: type, sizes: [TemplateFieldSize.MEDIUM]}) + } + } + + @action modifyColumnSizes = (colTitle: string, size: TemplateFieldSize, valid: boolean) => { + const column = this.colsInfo.get(colTitle); + if (column) { + if (!valid && column.sizes.includes(size)) { + column.sizes.splice(column.sizes.indexOf(size), 1); + } else if (valid && !column.sizes.includes(size)) { + column.sizes.push(size); + } + } else { + this.colsInfo.set(colTitle, {title: colTitle, desc: '', type: TemplateFieldType.UNSET, sizes: [size]}) + } + } + + @action setColumnTitle = (colTitle: string, newTitle: string) => { + const colInfo = this.colsInfo.get(colTitle); + if (colInfo) { + colInfo.title = newTitle; + console.log(colInfo.title) + } else { + this.colsInfo.set(colTitle, {title: newTitle, desc: '', type: TemplateFieldType.UNSET, sizes: []}) + } + } + + @action setColumnDesc = (colTitle: string, desc: string) => { + const colInfo = this.colsInfo.get(colTitle); + if (colInfo) { + if (!desc) { colInfo.desc = this.GPTSummary?.get(colTitle)?.desc ?? ''; } + else { colInfo.desc = desc; } + } else { + this.colsInfo.set(colTitle, {title: colTitle, desc: desc, type: TemplateFieldType.UNSET, sizes: []}) + } + } + + @action setColumnDefault = (colTitle: string, cont: string) => { + const colInfo = this.colsInfo.get(colTitle); + if (colInfo) { + colInfo.defaultContent = cont; + } else { + this.colsInfo.set(colTitle, {title: colTitle, desc: '', type: TemplateFieldType.UNSET, sizes: [], defaultContent: cont}) + } + } + @action // pinned / linked anchor doc includes selected rows, graph titles, and graph colors restoreView = (data: Doc) => { // const changedView = data.config_dataViz && this.dataVizView !== data.config_dataViz && (this.layoutDoc._dataViz = data.config_dataViz); @@ -146,6 +227,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { // } // return func() ?? false; }; + getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => { const visibleAnchor = AnchorMenu.Instance.GetAnchor?.(undefined, addAsAnnotation); const anchor = !pinProps @@ -272,7 +354,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { componentDidMount() { this._props.setContentViewBox?.(this); - if (!DataVizBox.dataset.has(CsvCast(this.dataDoc[this.fieldKey]).url.href)) this.fetchData(); + if (!this._urlError) { if (!DataVizBox.dataset.has(CsvCast(this.dataDoc[this.fieldKey]).url.href)) this.fetchData() }; this._disposers.datavis = reaction( () => { if (this.layoutDoc.dataViz_schemaLive === undefined) this.layoutDoc.dataViz_schemaLive = true; @@ -333,6 +415,10 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { }, { fireImmediately: true } ); + this._disposers.contentSummary = reaction( + () => this.records, + () => this.updateGPTSummary() + ); } fetchData = () => { @@ -359,7 +445,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { }; if (!this.records.length) return 'no data/visualization'; switch (this.dataVizView) { - case DataVizView.TABLE: return <TableBox {...sharedProps} docView={this.DocumentView} selectAxes={this.selectAxes} selectTitleCol={this.selectTitleCol}/>; + case DataVizView.TABLE: return <TableBox {...sharedProps} specHighlightedRow={this._specialHighlightedRow} docView={this.DocumentView} selectAxes={this.selectAxes} selectTitleCol={this.selectTitleCol}/>; case DataVizView.LINECHART: return <LineChart {...sharedProps} dataDoc={this.dataDoc} fieldKey={this.fieldKey} ref={r => {this._vizRenderer = r ?? undefined;}} vizBox={this} />; 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;}} @@ -426,11 +512,18 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { this.layoutDoc.dataViz_filterSelection = !this.layoutDoc.dataViz_filterSelection; }; - specificContextMenu = (): void => { + openDocCreatorMenu = (x: number, y: number) => { + DocCreatorMenu.Instance.toggleDisplay(x, y); + DocCreatorMenu.Instance.setDataViz(this); + DocCreatorMenu.Instance.setTemplateDocs(this.getPossibleTemplates()); + } + + specificContextMenu = (x: number, y: number): void => { const cm = ContextMenu.Instance; const options = cm.findByDescription('Options...'); const optionItems = options?.subitems ?? []; optionItems.push({ description: `Analyze with AI`, event: () => this.askGPT(), icon: 'lightbulb' }); + optionItems.push({ description: `Create documents`, event: () => this.openDocCreatorMenu(x, y), icon: 'table-cells' }); !options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'eye' }); }; @@ -445,6 +538,198 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { GPTPopup.Instance.generateDataAnalysis(); }); + getColSummary = (): string => { + let possibleIds: number[] = this.records.map((_, index) => index); + + for (let i = possibleIds.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [possibleIds[i], possibleIds[j]] = [possibleIds[j], possibleIds[i]]; + } + + const rowsToCheck = possibleIds.slice(0, Math.min(10, this.records.length)); + + let prompt: string = 'Col titles: '; + + const cols = Array.from(Object.keys(this.records[0])).filter(header => header !== '' && header !== undefined); + + cols.forEach((col, i) => { + prompt += `Col #${i}: ${col} ------` + }) + + prompt += '----------- Rows: ' + + rowsToCheck.forEach((row, i) => { + prompt += `Row #${row}: ` + cols.forEach(col => { + prompt += `${col}: ${this.records[row][col]} -----` + }) + }) + + return prompt; + } + + updateColDefaults = () => { + let possibleIds: number[] = this.records.map((_, index) => index); + + for (let i = possibleIds.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [possibleIds[i], possibleIds[j]] = [possibleIds[j], possibleIds[i]]; + } + + const rowToCheck = possibleIds[0]; + + const cols = Array.from(Object.keys(this.records[0])).filter(header => header !== '' && header !== undefined); + + cols.forEach(col => { + this.setColumnDefault(col, `${this.records[rowToCheck][col]}`) + }); + } + + updateGPTSummary = async () => { + this._GPTLoading = true; + + this.updateColDefaults(); + + const prompt = this.getColSummary(); + + const cols = Array.from(Object.keys(this.records[0])).filter(header => header !== '' && header !== undefined); + cols.forEach(col => { + if (!this.colsInfo.get(col)) this.colsInfo.set(col, {title: col, desc: '', sizes: [], type: TemplateFieldType.UNSET}); + }); + + try { + const [res1, res2] = await Promise.all([ + gptAPICall(prompt, GPTCallType.VIZSUM), + gptAPICall('Info:' + prompt, GPTCallType.VIZSUM2) + ]); + + if (res1) { + this.GPTSummary = new ObservableMap(); + const descs: { [col: string]: string } = JSON.parse(res1); + for (const [key, val] of Object.entries(descs)) { + this.GPTSummary.set(key, { desc: val }); + if (!this.colsInfo.get(key)?.desc) this.setColumnDesc(key, val); + } + } + + if (res2) { + !this.GPTSummary && (this.GPTSummary = new ObservableMap()); + const info: { [col: string]: { type: TemplateFieldType, size: TemplateFieldSize } } = JSON.parse(res2); + for (const [key, val] of Object.entries(info)) { + const colSummary = this.GPTSummary.get(key); + if (colSummary) { + colSummary.size = val.size; + colSummary.type = val.type; + this.setColumnType(key, val.type); + this.modifyColumnSizes(key, val.size, true); + } + } + } + } catch (err) { + console.error(err); + } + + } + + getPossibleTemplates = (): Doc[] => { + const linkedDocs: Doc[] = LinkManager.Instance.getAllRelatedLinks(this.Document).map(d => DocCast(LinkManager.getOppositeAnchor(d, this.Document))); + const linkedCollections: Doc[] = linkedDocs.filter(doc => doc.type === 'config').map(doc => DocCast(doc.annotationOn)); + const isColumnTitle = (title: string): boolean => { + const colTitles: string[] = Object.keys(this.records[0]); + for (let i = 0; i < colTitles.length; ++i){ + if (colTitles[i] === title) { + return true; + } + } + return false; + } + const isValidTemplate = (collection: Doc) => { + const childDocs = DocListCast(collection[Doc.LayoutFieldKey(collection)]); + for (let i = 0; i < childDocs.length; ++i){ + if (isColumnTitle(String(childDocs[i].title))) return true; + } + return false; + } + return linkedCollections.filter(col => isValidTemplate(col)); + } + + ApplyTemplateTo = (templateDoc: Doc, target: Doc, targetKey: string, titleTarget: string | undefined) => { + if (!Doc.AreProtosEqual(target[targetKey] as Doc, templateDoc)) { + if (target.resolvedDataDoc) { + target[targetKey] = new PrefetchProxy(templateDoc); + } else { + titleTarget && (Doc.GetProto(target).title = titleTarget); + const setDoc = [AclAdmin, AclEdit, AclAugment].includes(GetEffectiveAcl(Doc.GetProto(target))) ? Doc.GetProto(target) : target; + setDoc[targetKey] = new PrefetchProxy(templateDoc); + } + } + return target; + } + + applyLayout = (templateInfo: DataVizTemplateInfo, docs: Doc[]) => { + if (templateInfo.layout.type === LayoutType.Stacked) return; + const columns: number = templateInfo.columns; + const xGap: number = templateInfo.layout.xMargin; + const yGap: number = templateInfo.layout.yMargin; + const repeat: number = templateInfo.layout.repeat; + const startX: number = templateInfo.referencePos.x; + const startY: number = templateInfo.referencePos.y; + const templWidth = Number(templateInfo.doc._width); + const templHeight = Number(templateInfo.doc._height); + + let i: number = 0; + let docsChanged: number = 0; + let curX: number = startX; + let curY: number = startY; + + while (docsChanged < docs.length){ + while (i < columns && docsChanged < docs.length){ + docs[docsChanged].x = curX; + docs[docsChanged].y = curY; + curX += templWidth + xGap; + ++docsChanged; + ++i; + } + + i = 0; + curX = startX; + curY += templHeight + yGap; + } + } + + // @action addSavedLayout = (layout: DataVizTemplateLayout) => { + // const saved = Cast(this.layoutDoc.dataViz_savedTemplates, listSpec('RefField')); + + // } + + @action + createDocsFromTemplate = (templateInfo: DataVizTemplateInfo) => { + if (!templateInfo.doc) return; + const mainCollection = this.DocumentView?.().containerViewPath?.().lastElement()?.ComponentView as CollectionFreeFormView; + const fields: string[] = Array.from(Object.keys(this.records[0])); + const selectedRows = NumListCast(this.layoutDoc.dataViz_selectedRows); + const docs: Doc[] = selectedRows.map(row => { + const values: String[] = []; + fields.forEach(col => values.push(this.records[row][col])); + + const proto = new Doc(); + proto.author = ClientUtils.CurrentUserEmail(); + values.forEach((val, i) => {proto[fields[i]] = val as FieldType}); + + const target = Doc.MakeDelegate(proto); + const targetKey = StrCast(templateInfo.doc!.layout_fieldKey, 'layout'); + const applied = this.ApplyTemplateTo(templateInfo.doc!, target, targetKey, templateInfo.doc!.title + `${row}`); + target.layout_fieldKey = targetKey; + + //this.applyImagesTo(target, fields); + return applied; + }); + + docs.forEach(doc => mainCollection.addDocument(doc)); + + this.applyLayout(templateInfo, docs); + } + /** * creates a new dataviz document filter from this one * it appears to the right of this document, with the @@ -498,7 +783,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { transform: `scale(${scale})`, position: 'absolute', }} - onContextMenu={this.specificContextMenu} + onContextMenu={(e) => this.specificContextMenu(e.pageX, e.pageY)} onWheel={e => e.stopPropagation()} ref={this._mainCont}> <div className="datatype-button"> diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu.scss b/src/client/views/nodes/DataVizBox/DocCreatorMenu.scss new file mode 100644 index 000000000..9d82ada37 --- /dev/null +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu.scss @@ -0,0 +1,1036 @@ +.no-margin { + margin-top: 0px !important; + margin-bottom: 0px !important; + margin-left: 0px !important; + margin-right: 0px !important; +} + +.docCreatorMenu-cont { + position: absolute; + z-index: 100000; + // box-shadow: 0px 3px 4px rgba(0, 0, 0, 30%); + // background: whitesmoke; + // color: black; + border-radius: 3px; +} + +.docCreatorMenu-menu { + display: flex; + flex-direction: row; + height: 25px; + align-items: flex-end; +} + +.docCreatorMenu-menu-button { + width: 25px; + height: 25px; + background: whitesmoke; + background-color: rgb(50, 50, 50); + border-radius: 5px; + border: 1px solid rgb(180, 180, 180); + padding: 0px; + font-size: 13px; + //box-shadow: 3px 3px rgb(29, 29, 31); + + &:hover { + box-shadow: none; + } + + &.right{ + margin-left: 0px; + font-size: 12px; + } + + &.close-menu { + font-size: 12px; + width: 18px; + height: 18px; + border-radius: 2px; + font-size: 12px; + margin-left: auto; + } + + &.options { + margin-left: 0px; + } + + &:hover { + background-color: rgb(60, 60, 65); + } + + &.top-bar { + border-bottom: 25px solid #555; + border-left: 12px solid transparent; + border-right: 12px solid transparent; + // border-top-left-radius: 5px; + // border-top-right-radius: 5px; + border-radius: 0px; + height: 0; + width: 50px; + } + + &.preview-toggle { + margin: 0px; + border-top-left-radius: 0px; + border-bottom-left-radius: 0px; + border-left: 0px; + } +} + +.docCreatorMenu-top-buttons-container { + position: relative; + margin-top: 5px; + margin-left: 7px; + display: flex; + flex-direction: row; + align-items: flex-end; + width: 150px; + height: auto; +} + +.top-button-container { + position: relative; + width: 52px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + + &.left { + z-index: 3; + } + + &.middle { + position: absolute; + left: 40px; + z-index: 2; + + &.selected { + z-index: 4; + } + } + + &.right { + position: absolute; + left: 80px; + z-index: 1; + + &.selected { + z-index: 4; + } + } + + &:hover::before{ + border-bottom: 20px solid rgb(82, 82, 82); + } + + &::before { + content: ""; + position: absolute; + top: 0; + left: 0; + border-bottom: 20px solid rgb(50, 50, 50); + border-left: 12px solid transparent; + border-right: 12px solid transparent; + height: 0; + width: 50px; + } + + &::after { + content: ""; + position: absolute; + top: -1px; + left: -1px; + border-bottom: 22px solid rgb(180, 180, 180); + border-left: 12px solid transparent; + border-right: 12px solid transparent; + height: 0; + width: 52px; + z-index: -1; + } + + &.selected::before { + border-bottom-color: rgb(67, 119, 214); + } +} + +.top-button-content { + position: relative; + z-index: 1; + color: white; +} + +.docCreatorMenu-menu-hr{ + margin-top: 0px; + margin-bottom: 0px; + color: rgb(180, 180, 180); +} + +.docCreatorMenu-placement-indicator { + position: absolute; + z-index: 100000; + border-left: solid 3px #9fd7fb; + border-top: solid 3px #9fd7fb; + width: 25px; + height: 25px; +} + +.docCreatorMenu-general-options-container { + display: flex; + justify-content: center; + align-items: center; + margin: 0px; + padding: 0px; + gap: 5px; +} + +.docCreatorMenu-save-layout-button { + display: flex; + justify-content: center; + align-items: center; + width: 40px; + height: 40px; + background-color: rgb(99, 148, 238); + border: 2px solid rgb(80, 107, 152); + border-radius: 5px; + margin-bottom: 20px; + font-size: 25px; + + &:hover{ + background-color: rgb(59, 128, 255); + border: 2px solid rgb(53, 80, 127); + } +} + +.docCreatorMenu-create-docs-button { + width: 40px; + height: 40px; + background-color: rgb(176, 229, 149); + border: 2px solid rgb(126, 219, 80); + border-radius: 5px; + padding: 0px; + font-size: 25px; + color: white; + flex: 0 0 auto; + margin-bottom: 20px; //remove later !!! + + &:hover { + background-color: rgb(129, 223, 83); + border: 2px solid rgb(80, 185, 28); + } +} + +.docCreatorMenu-option-divider { + border-top: 1px solid rgb(180, 180, 180); + width: 95%; + margin-top: 10px; + margin-bottom: 10px; + + &.full { + width: 100%; + } +} + +//------------------------------------------------------------------------------------------------------------------------------------------ +// Resizers CSS +//-------------------------------------------------------------------------------------------------------------------------------------------- + +.docCreatorMenu-resizer { + position: absolute; + background-color: none; + + &.top, &.bottom { + height: 10px; + cursor: ns-resize; + } + + &.right, &.left { + width: 10px; + cursor: ew-resize; + } + + &.topRight, &.topLeft, &.bottomRight, &.bottomLeft { + height: 15px; + width: 15px; + background-color: none; + } +} + +//------------------------------------------------------------------------------------------------------------------------------------------ +// DocCreatorMenu templates preview CSS +//-------------------------------------------------------------------------------------------------------------------------------------------- + +.docCreatorMenu-templates-view { + display: flex; + flex-direction: column; + justify-content: flex-start; + overflow-y: scroll; + //align-items: flex-start; + margin: 5px; + margin-top: 0px; + width: calc(100% - 10px); + height: calc(100% - 30px); + border: 1px solid rgb(180, 180, 180); + border-radius: 5px; + -ms-overflow-style: none; + scrollbar-width: none; +} + +.docCreatorMenu-preview-container { + display: grid; + grid-template-columns: repeat(2, 140px); + grid-template-rows: 140px; + grid-auto-rows: 141px; + overflow-y: scroll; + margin: 0px; + margin-top: 0px; + width: 100%; + height: 100%; +} + +.docCreatorMenu-expanded-template-preview { + position: relative; + width: 100%; + height: 100%; + + .right-buttons-panel { + display: flex; + flex-direction: column; + justify-content: flex-start; + height: 100%; + width: 40px; + position: absolute; + right: 0px; + top: 0px; + padding: 5px; + gap: 2px; + } +} + +.docCreatorMenu-preview-window { + position: relative; + display: flex; + justify-content: center; + align-items: center; + width: 113px; + height: 113px; + margin-top: 10px; + margin-left: 10px; + border: 1px solid rgb(163, 163, 163); + border-radius: 5px; + box-shadow: 5px 5px rgb(29, 29, 31); + flex: 0 0 auto; + + &:hover{ + background-color: rgb(72, 72, 73); + } + + &.empty { + font-size: 35px; + + &.GPT { + margin-top: 0px; + } + } + + .option-button { + display: none; + height: 25px; + width: 25px; + margin: 0px; + background: none; + border: 0px; + padding: 0px; + font-size: 15px; + + &.right { + position: absolute; + bottom: 0px; + right: 0px; + } + + &.left { + position: absolute; + bottom: 0px; + left: 0px; + } + + &.top-left { + position: absolute; + top: 0px; + left: 0px; + } + } + + &:hover .option-button { + display: block; + } + +} + +.docCreatorMenu-preview-image{ + background-color: transparent; + height: 100px; + width: 100px; + display: block; + object-fit: contain; + border-radius: 5px; + + &.expanded { + height: 100%; + width: 100%; + } +} + +.docCreatorMenu-section { + display: flex; + flex-direction: column; + align-items: center; + position: relative; + margin: 0px; + margin-top: 0px; + margin-bottom: 0px; + width: 100%; + height: 200; + flex: 0 0 auto; +} + +.docCreatorMenu-GPT-options-container { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + position: relative; + width: auto; + margin: 0px; + margin-top: 5px; + padding: 0px; +} + +.docCreatorMenu-templates-preview-window { + display: flex; + flex-direction: row; + //justify-content: center; + align-items: center; + overflow-y: scroll; + position: relative; + height: 125px; + width: calc(100% - 10px); + -ms-overflow-style: none; + scrollbar-width: none; + + .loading-spinner { + justify-self: center; + } +} + +.divvv{ + width: 200; + height: 200; + border: solid 1px white; +} + +.docCreatorMenu-section-topbar { + position: relative; + display: flex; + flex-direction: row; + width: 100%; +} + +.section-reveal-options { + margin-top: 0px; + margin-bottom: 0px; + margin-right: 0px; + margin-left: auto; + border: 0px; + background: none; + + &.float-right { + float: right; + } +} + +.docCreatorMenu-section-title { + border: 1px solid rgb(163, 163, 163); + border-top: 0px; + border-left: 0px; + border-bottom-right-radius: 5px; + font-size: 12px; + padding: 2px; + padding-left: 3px; + padding-right: 3px; + margin-bottom: 3px; +} + +.docCreatorMenu-GPT-generate { + height: 30px; + width: 30px; + background-color: rgb(176, 229, 149); + border: 1px solid rgb(126, 219, 80); + border-radius: 5px; + padding: 0px; + font-size: 14px; + color: white; + letter-spacing: 1px; + flex: 0 0 auto; + + &:hover { + background-color: rgb(129, 223, 83); + border: 2px solid rgb(80, 185, 28); + } +} + +.docCreatorMenu-GPT-prompt-input { + width: 140px; + height: 25px; + overflow-y: scroll; + border: 1px solid rgb(180, 180, 180); + background-color: rgb(35, 35, 35); + border-radius: 3px; + padding-left: 4px; +} + +//------------------------------------------------------------------------------------------------------------------------------------------ +// DocCreatorMenu options CSS +//-------------------------------------------------------------------------------------------------------------------------------------------- + +.docCreatorMenu-option-container{ + display: flex; + width: 180px; + height: 30px; + flex-direction: row; + justify-content: center; + align-items: center; + margin-top: 10px; + margin-bottom: 10px; + + &.layout{ + z-index: 5; + } +} + +.docCreatorMenu-option-title{ + display: flex; + width: 140px; + height: 30px; + background: whitesmoke; + background-color: rgb(34, 34, 37); + border-radius: 5px; + border-top-right-radius: 0px; + border-bottom-right-radius: 0px; + border: 1px solid rgb(180, 180, 180); + padding: 0px; + font-size: 12px; + align-items: center; + justify-content: center; + text-transform: uppercase; + cursor: pointer; + + &.spacer { + border-left: none; + border-right: none; + border-radius: 0px; + width: auto; + text-transform: none; + + &.small { + height: 20px; + transform: translateY(-5px); + } + } + + &.config { + border-radius: 4px; + border-top-right-radius: 0px; + border-bottom-right-radius: 0px; + width: 30px; + border-right: 0px; + gap: 3px; + + &.layout-config { + height: 20px; + transform: translateY(-5px); + text-transform: none; + padding-left: 2px; + } + + &.dimensions { + text-transform: none; + height: 20px; + transform: translateY(-5px); + width: 70px; + } + } +} + +.docCreatorMenu-input { + display: flex; + height: 30px; + background-color: rgb(34, 34, 37); + border: 1px solid rgb(180, 180, 180); + align-items: center; + justify-content: center; + + &.config { + border-radius: 4px; + margin: 0px; + border-top-left-radius: 0px; + border-bottom-left-radius: 0px; + border-left: 0px; + width: 25px; + + &.layout-config { + height: 20px; + transform: translateY(-5px); + } + + &.dimensions { + height: 20px; + width: 30px; + transform: translateY(-5px); + + &.right { + border-top-left-radius: 0px; + border-bottom-left-radius: 0px; + } + + &.left { + border-radius: 0px; + border-right: 0px; + } + } + } +} + +.docCreatorMenu-configuration-bar { + width: 200; + gap: 5px; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + + &.no-gap { + gap: 0px; + } +} + +.docCreatorMenu-menu-container { + display: flex; + flex-direction: column; + align-items: center; + overflow-y: scroll; + margin: 5px; + margin-top: 0px; + width: calc(100% - 10px); + height: calc(100% - 30px); + border: 1px solid rgb(180, 180, 180); + border-radius: 5px; + + .docCreatorMenu-option-container{ + width: 180px; + height: 30px; + + .docCreatorMenu-dropdown-hoverable { + width: 140px; + height: 30px; + + &:hover .docCreatorMenu-dropdown-content { + display: block; + } + + &:hover .docCreatorMenu-option-title { + border-bottom-left-radius: 0px; + border-bottom-right-radius: 0px; + } + + .docCreatorMenu-dropdown-content { + display: none; + min-width: 100px; + height: 75px; + overflow-y: scroll; + -ms-overflow-style: none; + scrollbar-width: none; + border-bottom: 1px solid rgb(180, 180, 180); + border-bottom-left-radius: 5px; + border-bottom-right-radius: 5px; + + .docCreatorMenu-dropdown-option{ + display: flex; + background-color: rgb(42, 42, 46); + border-left: 1px solid rgb(180, 180, 180); + border-right: 1px solid rgb(180, 180, 180); + border-bottom: 1px solid rgb(180, 180, 180); + width: 140px; + height: 25px; + justify-content: center; + justify-items: center; + padding-top: 3px; + + &:hover { + background-color: rgb(68, 68, 74); + cursor: pointer; + } + } + } + } + } +} + +.docCreatorMenu-layout-preview-window-wrapper { + display: flex; + justify-content: center; + align-items: center; + width: 85%; + height: auto; + position: relative; + padding: 0px; + + &:hover .docCreatorMenu-zoom-button-container { + display: block; + } + + .docCreatorMenu-layout-preview-window { + padding: 5px; + flex: 0 0 auto; + overflow: scroll; + display: grid; + width: 100%; + aspect-ratio: 1; + //height: auto; + // max-width: 240; + // max-height: 240; + border: 1px solid rgb(180, 180, 180); + border-radius: 5px; + background-color: rgb(34, 34, 37); + -ms-overflow-style: none; + scrollbar-width: none; + + &.small { + max-width: 100; + max-height: 100; + } + + .docCreatorMenu-layout-preview-item { + display: flex; + justify-content: center; + align-items: center; + border-radius: 3px; + border: solid 1px lightblue; + + &:hover { + border: solid 2px rgb(68, 153, 233); + z-index: 2; + } + } + } + + .docCreatorMenu-zoom-button-container { + position: absolute; + top: 0px; + display: flex; + justify-content: center; + align-items: center; + display: none; + z-index: 999; + } + + .docCreatorMenu-zoom-button{ + width: 15px; + height: 15px; + background: whitesmoke; + background-color: rgb(34, 34, 37); + border-radius: 3px; + border: 1px solid rgb(180, 180, 180); + padding: 0px; + font-size: 10px; + z-index: 6; + margin-left: 0px; + margin-top: 0px; + margin-right: 0px; //225px + margin-bottom: 0px; + } +} + +//------------------------------------------------------------------------------------------------------------------------------------------ +// DocCreatorMenu dashboard CSS +//-------------------------------------------------------------------------------------------------------------------------------------------- + +.docCreatorMenu-dashboard-view { + position: relative; + display: flex; + flex-direction: column; + justify-content: flex-start; + overflow-y: hidden; + //align-items: flex-start; + margin: 5px; + margin-top: 0px; + width: calc(100% - 10px); + height: calc(100% - 30px); + border: 1px solid rgb(180, 180, 180); + border-radius: 5px; + -ms-overflow-style: none; + scrollbar-width: none; + + .panels-container { + height: 100%; + width: 100%; + flex-direction: column; + justify-content: flex-start; + overflow-y: scroll; + } + + .topbar { + height: 30px; + width: 100%; + background-color: rgb(50, 50, 50); + } + +// .field-panel { +// position: relative; +// display: flex; +// // align-items: flex-start; +// flex-direction: column; +// gap: 5px; +// padding: 5px; +// height: 100px; +// //width: 100%; +// border: 1px solid rgb(180, 180, 180); +// margin: 5px; +// margin-top: 0px; +// border-radius: 3px; +// flex: 0 0 auto; + +// .properties-wrapper { +// display: flex; +// flex-direction: row; +// align-items: flex-start; +// gap: 5px; + +// .field-property-container { +// background-color: rgb(40, 40, 40); +// border: 1px solid rgb(100, 100, 100); +// border-radius: 3px; +// width: 30%; +// height: 25px; +// padding-left: 3px; +// align-items: center; +// color: whitesmoke; +// } + +// .field-type-selection-container { +// display: flex; +// flex-direction: row; +// align-items: center; +// background-color: rgb(40, 40, 40); +// border: 1px solid rgb(100, 100, 100); +// border-radius: 3px; +// width: 31%; +// height: 25px; +// padding-left: 3px; +// color: whitesmoke; + +// .placeholder { +// color: gray; +// } + +// &:hover .placeholder { +// display: none; +// } + +// .bubbles { +// display: none; +// } + +// .text { +// margin-top: 5px; +// margin-bottom: 5px; +// } + +// &:hover .bubbles { +// display: flex; +// flex-direction: row; +// align-items: flex-start; +// } + +// &:hover .type-display { +// display: none; +// } + +// .bubble { +// margin: 5px; +// } + +// &:hover .bubble { +// margin-top: 7px; +// } +// } +// } + +// .field-description-container { +// background-color: rgb(40, 40, 40); +// border: 1px solid rgb(100, 100, 100); +// border-radius: 3px; +// width: 100%; +// height: 100%; +// resize: none; + +// ::-webkit-scrollbar-track { +// background: none; +// } +// } + +// .top-right { +// position: absolute; +// top: 0px; +// right: 0px; +// } +// } +// } + + .field-panel { + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; + height: 285px; + width: calc(100% - 10px); + border: 1px solid rgb(180, 180, 180); + margin: 5px; + margin-top: 0px; + margin-bottom: 10px; + border-radius: 3px; + flex: 0 0 auto; + gap: 25px; + background-color: rgb(60, 60, 60); + + .top-bar { + position: relative; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + border-bottom: 1px solid rgb(180, 180, 180); + border-top-right-radius: 5px; + border-top-left-radius: 5px; + width: 100%; + height: 20px; + background-color: rgb(50, 50, 50); + color: rgb(168, 167, 167); + + .field-title { + color: whitesmoke; + } + } + + .opts-bar { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + width: 100%; + + .opt-box { + border: 1px solid rgb(180, 180, 180); + border-radius: 5px; + width: 40%; + height: 50px; + margin-right: 4%; + margin-left: 4%; + box-shadow: 5px 5px rgb(29, 29, 31); + } + + .content { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + height: calc(100% - 20px); + width: 100%; + background-color: rgb(50, 50, 50); + border-bottom-right-radius: 5px; + border-bottom-left-radius: 5px; + resize: none; + + .bubbles { + display: none; + } + + .text { + margin-right: 5px; + } + + &:hover .bubbles { + display: flex; + flex-direction: row; + align-items: flex-start; + } + + &:hover .type-display { + display: none; + } + + .bubble { + margin: 3px; + } + } + } + + .sizes-box { + width: 88%; + height: 60px; + border: 1px solid rgb(180, 180, 180); + border-radius: 5px; + background-color: rgb(50, 50, 50); + box-shadow: 5px 5px rgb(29, 29, 31); + + .content { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + height: calc(100% - 20px); + width: 100%; + background-color: rgb(50, 50, 50); + border-bottom-right-radius: 5px; + border-bottom-left-radius: 5px; + + .text { + margin-right: 9px; + } + + .bubbles { + display: flex; + flex-direction: row; + align-items: center; + } + + .bubble { + margin: 3px; + margin-right: 4px; + } + } + } + + .desc-box { + width: 88%; + height: 50px; + border: 1px solid rgb(180, 180, 180); + border-radius: 5px; + background-color: rgb(50, 50, 50); + box-shadow: 5px 5px rgb(29, 29, 31); + + .content { + height: calc(100% - 20px); + width: 100%; + background-color: rgb(50, 50, 50); + border-bottom-right-radius: 5px; + border-bottom-left-radius: 5px; + resize: none; + + } + } + + } + +} diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu.tsx new file mode 100644 index 000000000..43e9248a7 --- /dev/null +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu.tsx @@ -0,0 +1,2335 @@ +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Colors } from 'browndash-components'; +import { action, computed, makeObservable, observable, reaction, runInAction } from 'mobx'; +import { observer } from 'mobx-react'; +import { IDisposer } from 'mobx-utils'; +import * as React from 'react'; +import ReactLoading from 'react-loading'; +import { ClientUtils, returnFalse, setupMoveUpEvents } from '../../../../ClientUtils'; +import { emptyFunction } from '../../../../Utils'; +import { Doc, NumListCast, StrListCast } from '../../../../fields/Doc'; +import { Id } from '../../../../fields/FieldSymbols'; +import { DocCast, ImageCast, StrCast } from '../../../../fields/Types'; +import { ImageField } from '../../../../fields/URLField'; +import { Networking } from '../../../Network'; +import { GPTCallType, gptAPICall, gptImageCall } from '../../../apis/gpt/GPT'; +import { Docs } from '../../../documents/Documents'; +import { DragManager } from '../../../util/DragManager'; +import { MakeTemplate } from '../../../util/DropConverter'; +import { SnappingManager } from '../../../util/SnappingManager'; +import { UndoManager, undoable } from '../../../util/UndoManager'; +import { LightboxView } from '../../LightboxView'; +import { ObservableReactComponent } from '../../ObservableReactComponent'; +import { CollectionFreeFormView } from '../../collections/collectionFreeForm/CollectionFreeFormView'; +import { DocumentView, DocumentViewInternal } from '../DocumentView'; +import { FieldViewProps } from '../FieldView'; +import { OpenWhere } from '../OpenWhere'; +import { DataVizBox } from './DataVizBox'; +import './DocCreatorMenu.scss'; + +export enum LayoutType { + Stacked = 'stacked', + Grid = 'grid', + Row = 'row', + Column = 'column', + Custom = 'custom', +} + +@observer +export class DocCreatorMenu extends ObservableReactComponent<FieldViewProps> { + static Instance: DocCreatorMenu; + + private _disposers: { [name: string]: IDisposer } = {}; + + private _ref: HTMLDivElement | null = null; + + @observable _templateDocs: Doc[] = []; + @observable _selectedTemplate: Doc | undefined = undefined; + @observable _columns: Col[] = []; + @observable _selectedCols: { title: string; type: string; desc: string }[] | undefined = []; + + @observable _layout: { type: LayoutType; yMargin: number; xMargin: number; columns?: number; repeat: number } = { type: LayoutType.Grid, yMargin: 0, xMargin: 0, repeat: 0 }; + @observable _layoutPreview: boolean = true; + @observable _layoutPreviewScale: number = 1; + @observable _savedLayouts: DataVizTemplateLayout[] = []; + @observable _expandedPreview: { icon: ImageField; doc: Doc } | undefined = undefined; + + @observable _suggestedTemplates: Doc[] = []; + @observable _GPTOpt: boolean = false; + @observable _userPrompt: string = ''; + @observable _callCount: number = 0; + @observable _GPTLoading: boolean = false; + + @observable _pageX: number = 0; + @observable _pageY: number = 0; + @observable _indicatorX: number | undefined = undefined; + @observable _indicatorY: number | undefined = undefined; + + @observable _hoveredLayoutPreview: number | undefined = undefined; + @observable _mouseX: number = -1; + @observable _mouseY: number = -1; + @observable _startPos?: { x: number; y: number }; + @observable _shouldDisplay: boolean = false; + + @observable _menuContent: 'templates' | 'options' | 'saved' | 'dashboard' = 'templates'; + @observable _dragging: boolean = false; + @observable _draggingIndicator: boolean = false; + @observable _dataViz?: DataVizBox; + @observable _interactionLock: any; + @observable _snapPt: any; + @observable _resizeHdlId: string = ''; + @observable _resizing: boolean = false; + @observable _offset: { x: number; y: number } = { x: 0, y: 0 }; + @observable _resizeUndo: UndoManager.Batch | undefined = undefined; + @observable _initDimensions: { width: number; height: number; x?: number; y?: number } = { width: 300, height: 400, x: undefined, y: undefined }; + @observable _menuDimensions: { width: number; height: number } = { width: 400, height: 400 }; + @observable _editing: boolean = false; + + constructor(props: any) { + super(props); + makeObservable(this); + DocCreatorMenu.Instance = this; + //setTimeout(() => this.generateTemplates('')); + } + + @action setDataViz = (dataViz: DataVizBox) => { + this._dataViz = dataViz; + }; + @action setTemplateDocs = (docs: Doc[]) => { + this._templateDocs = docs.map(doc => (doc.annotationOn ? DocCast(doc.annotationOn) : doc)); + }; + @action setGSuggestedTemplates = (docs: Doc[]) => { + this._suggestedTemplates = docs; + }; + + @computed get docsToRender() { + return this._selectedTemplate ? NumListCast(this._dataViz?.layoutDoc.dataViz_selectedRows) : []; + } + + @computed get rowsCount() { + switch (this._layout.type) { + case LayoutType.Row: + case LayoutType.Stacked: + return 1; + case LayoutType.Column: + return this.docsToRender.length; + case LayoutType.Grid: + return Math.ceil(this.docsToRender.length / (this._layout.columns ?? 1)) ?? 0; + default: + return 0; + } + } + + @computed get columnsCount() { + switch (this._layout.type) { + case LayoutType.Row: + return this.docsToRender.length; + case LayoutType.Column: + case LayoutType.Stacked: + return 1; + case LayoutType.Grid: + return this._layout.columns ?? 0; + default: + return 0; + } + } + + @computed get selectedFields() { + return StrListCast(this._dataViz?.layoutDoc._dataViz_axes); + } + + @computed get fieldsInfos(): Col[] { + const colInfo = this._dataViz?.colsInfo; + return this.selectedFields + .map(field => { + const fieldInfo = colInfo?.get(field); + + const col: Col = { + title: field, + type: fieldInfo?.type ?? TemplateFieldType.UNSET, + desc: fieldInfo?.desc ?? '', + sizes: fieldInfo?.sizes ?? [TemplateFieldSize.MEDIUM], + }; + + if (fieldInfo?.defaultContent !== undefined) { + col.defaultContent = fieldInfo.defaultContent; + } + + return col; + }) + .concat(this._columns); + } + + @computed get canMakeDocs() { + return this._selectedTemplate !== undefined && this._layout !== undefined; + } + + get bounds(): { t: number; b: number; l: number; r: number } { + const rect = this._ref?.getBoundingClientRect(); + const bounds = { t: rect?.top ?? 0, b: rect?.bottom ?? 0, l: rect?.left ?? 0, r: rect?.right ?? 0 }; + return bounds; + } + + setUpButtonClick = (e: any, func: Function) => { + setupMoveUpEvents( + this, + e, + returnFalse, + emptyFunction, + undoable(clickEv => { + clickEv.stopPropagation(); + clickEv.preventDefault(); + func(); + }, 'create docs') + ); + }; + + @action + onPointerDown = (e: PointerEvent) => { + this._mouseX = e.clientX; + this._mouseY = e.clientY; + }; + + @action + onPointerUp = (e: PointerEvent) => { + if (this._resizing) { + this._initDimensions.width = this._menuDimensions.width; + this._initDimensions.height = this._menuDimensions.height; + this._initDimensions.x = this._pageX; + this._initDimensions.y = this._pageY; + document.removeEventListener('pointermove', this.onResize); + SnappingManager.SetIsResizing(undefined); + this._resizing = false; + } + if (this._dragging) { + document.removeEventListener('pointermove', this.onDrag); + this._dragging = false; + } + if (e.button !== 2 && !e.ctrlKey) return; + const curX = e.clientX; + const curY = e.clientY; + if (Math.abs(this._mouseX - curX) > 1 || Math.abs(this._mouseY - curY) > 1) { + this._shouldDisplay = false; + } + }; + + componentDidMount() { + document.addEventListener('pointerdown', this.onPointerDown, true); + document.addEventListener('pointerup', this.onPointerUp); + this._disposers.templates = reaction( + () => this._templateDocs.slice(), + docs => docs.map(this.getIcon) + ); + this._disposers.gpt = reaction( + () => this._suggestedTemplates.slice(), + docs => docs.map(this.getIcon) + ); + //this._disposers.columns = reaction(() => this._dataViz?.layoutDoc._dataViz_axes, () => {this.generateTemplates('')}) + this._disposers.lightbox = reaction( + () => LightboxView.LightboxDoc(), + doc => { + doc ? this._shouldDisplay && this.closeMenu() : !this._shouldDisplay && this.openMenu(); + } + ); + //this._disposers.fields = reaction(() => this._dataViz?.axes, cols => this._selectedCols = cols?.map(col => { return {title: col, type: '', desc: ''}})) + } + + componentWillUnmount() { + Object.values(this._disposers).forEach(disposer => disposer?.()); + document.removeEventListener('pointerdown', this.onPointerDown, true); + document.removeEventListener('pointerup', this.onPointerUp); + } + + updateIcons = (docs: Doc[]) => { + docs.map(this.getIcon); + }; + + @action + updateSelectedCols = (cols: string[]) => { + this._selectedCols; + }; + + @action + toggleDisplay = (x: number, y: number) => { + if (this._shouldDisplay) { + this._shouldDisplay = false; + } else { + this._pageX = x; + this._pageY = y; + this._shouldDisplay = true; + } + }; + + @action + closeMenu = () => { + this._shouldDisplay = false; + }; + + @action + openMenu = () => { + const allTemplates = this._templateDocs.concat(this._suggestedTemplates); + this._shouldDisplay = true; + this.updateIcons(allTemplates); + }; + + @action + onResizePointerDown = (e: React.PointerEvent): void => { + this._resizing = true; + document.addEventListener('pointermove', this.onResize); + SnappingManager.SetIsResizing(DocumentView.Selected().lastElement()?.Document[Id]); // turns off pointer events on things like youtube videos and web pages so that dragging doesn't get "stuck" when cursor moves over them + e.stopPropagation(); + const id = (this._resizeHdlId = e.currentTarget.className); + const pad = id.includes('Left') || id.includes('Right') ? Number(getComputedStyle(e.target as any).width.replace('px', '')) / 2 : 0; + const bounds = e.currentTarget.getBoundingClientRect(); + this._offset = { + x: id.toLowerCase().includes('left') ? bounds.right - e.clientX - pad : bounds.left - e.clientX + pad, // + y: id.toLowerCase().includes('top') ? bounds.bottom - e.clientY - pad : bounds.top - e.clientY + pad, + }; + this._resizeUndo = UndoManager.StartBatch('drag resizing'); + this._snapPt = { x: e.pageX, y: e.pageY }; + }; + + @action + onResize = (e: any): boolean => { + const dragHdl = this._resizeHdlId.split(' ')[1]; + const thisPt = DragManager.snapDrag(e, -this._offset.x, -this._offset.y, this._offset.x, this._offset.y); + + const { scale, refPt, transl } = this.getResizeVals(thisPt, dragHdl); + !this._interactionLock && runInAction(async () => { // resize selected docs if we're not in the middle of a resize (ie, throttle input events to frame rate) + this._interactionLock = true; + const scaleAspect = {x: scale.x, y: scale.y}; + this.resizeView(refPt, scaleAspect, transl); // prettier-ignore + await new Promise<any>(res => { setTimeout(() => { res(this._interactionLock = undefined)})}); + }); // prettier-ignore + return true; + }; + + @action + onDrag = (e: any): boolean => { + this._pageX = e.pageX - (this._startPos?.x ?? 0); + this._pageY = e.pageY - (this._startPos?.y ?? 0); + this._initDimensions.x = this._pageX; + this._initDimensions.y = this._pageY; + return true; + }; + + getResizeVals = (thisPt: { x: number; y: number }, dragHdl: string) => { + const [w, h] = [this._initDimensions.width, this._initDimensions.height]; + const [moveX, moveY] = [thisPt.x - this._snapPt.x, thisPt.y - this._snapPt.y]; + let vals: { scale: { x: number; y: number }; refPt: [number, number]; transl: { x: number; y: number } }; + switch (dragHdl) { + case 'topLeft': vals = { scale: { x: 1 - moveX / w, y: 1 -moveY / h }, refPt: [this.bounds.r, this.bounds.b], transl: {x: moveX, y: moveY } }; break; + case 'topRight': vals = { scale: { x: 1 + moveX / w, y: 1 -moveY / h }, refPt: [this.bounds.l, this.bounds.b], transl: {x: 0, y: moveY } }; break; + case 'top': vals = { scale: { x: 1, y: 1 -moveY / h }, refPt: [this.bounds.l, this.bounds.b], transl: {x: 0, y: moveY } }; break; + case 'left': vals = { scale: { x: 1 - moveX / w, y: 1 }, refPt: [this.bounds.r, this.bounds.t], transl: {x: moveX, y: 0 } }; break; + case 'bottomLeft': vals = { scale: { x: 1 - moveX / w, y: 1 + moveY / h }, refPt: [this.bounds.r, this.bounds.t], transl: {x: moveX, y: 0 } }; break; + case 'right': vals = { scale: { x: 1 + moveX / w, y: 1 }, refPt: [this.bounds.l, this.bounds.t], transl: {x: 0, y: 0 } }; break; + case 'bottomRight':vals = { scale: { x: 1 + moveX / w, y: 1 + moveY / h }, refPt: [this.bounds.l, this.bounds.t], transl: {x: 0, y: 0 } }; break; + case 'bottom': vals = { scale: { x: 1, y: 1 + moveY / h }, refPt: [this.bounds.l, this.bounds.t], transl: {x: 0, y: 0 } }; break; + default: vals = { scale: { x: 1, y: 1 }, refPt: [this.bounds.l, this.bounds.t], transl: {x: 0, y: 0 } }; break; + } // prettier-ignore + return vals; + }; + + resizeView = (refPt: number[], scale: { x: number; y: number }, translation: { x: number; y: number }) => { + const refCent = [refPt[0], refPt[1]]; // fixed reference point for resize (ie, a point that doesn't move) + if (this._initDimensions.x === undefined) this._initDimensions.x = this._pageX; + if (this._initDimensions.y === undefined) this._initDimensions.y = this._pageY; + const { height, width, x, y } = this._initDimensions; + + this._menuDimensions.width = Math.max(300, scale.x * width); + this._menuDimensions.height = Math.max(200, scale.y * height); + this._pageX = x + translation.x; + this._pageY = y + translation.y; + }; + + async getIcon(doc: Doc) { + const docView = DocumentView.getDocumentView(doc); + if (docView) { + docView.ComponentView?.updateIcon?.(); + return new Promise<ImageField | undefined>(res => setTimeout(() => res(ImageCast(docView.Document.icon)), 500)); + } + return undefined; + } + + @action updateSelectedTemplate = (template: Doc) => { + if (this._selectedTemplate === template) { + this._selectedTemplate = undefined; + return; + } else { + this._selectedTemplate = template; + MakeTemplate(template); + } + }; + + @action updateSelectedSavedLayout = (layout: DataVizTemplateLayout) => { + this._layout.xMargin = layout.layout.xMargin; + this._layout.yMargin = layout.layout.yMargin; + this._layout.type = layout.layout.type; + this._layout.columns = layout.columns; + }; + + isSelectedLayout = (layout: DataVizTemplateLayout) => { + return this._layout.xMargin === layout.layout.xMargin && this._layout.yMargin === layout.layout.yMargin && this._layout.type === layout.layout.type && this._layout.columns === layout.columns; + }; + + @action + generateTemplates = async (inputText: string) => { + ++this._callCount; + const origCount = this._callCount; + + let prompt: string = `(#${origCount}) Please generate for the fields:`; + this.selectedFields?.forEach(field => (prompt += ` ${field},`)); + prompt += ` (-----NOT A FIELD-----) Additional prompt: ${inputText}`; + + this._GPTLoading = true; + + try { + const res = await gptAPICall(prompt, GPTCallType.TEMPLATE); + + if (res && this._callCount === origCount) { + this._suggestedTemplates = []; + const templates: { template_type: string; fieldVals: { title: string; tlx: string; tly: string; brx: string; bry: string }[] }[] = JSON.parse(res); + this.createGeneratedTemplates(templates, 500, 500); + } + } catch (err) { + console.error(err); + } + }; + + @action + createGeneratedTemplates = (layouts: { template_type: string; fieldVals: { title: string; tlx: string; tly: string; brx: string; bry: string }[] }[], tempWidth: number, tempHeight: number) => { + const mainCollection = this._dataViz?.DocumentView?.().containerViewPath?.().lastElement()?.ComponentView as CollectionFreeFormView; + const GPTTemplates: Doc[] = []; + + layouts.forEach(layout => { + const fields: Doc[] = layout.fieldVals.map(field => { + const left: number = (Number(field.tlx) * tempWidth) / 2; + const top: number = Number(field.tly) * tempHeight / 2; //prettier-ignore + const right: number = (Number(field.brx) * tempWidth) / 2; + const bottom: number = Number(field.bry) * tempHeight / 2; //prettier-ignore + const height = bottom - top; + const width = right - left; + const doc = !field.title.includes('$$') + ? Docs.Create.TextDocument('', { _height: height, _width: width, title: field.title, x: left, y: top, _text_fontSize: `${height / 2}` }) + : Docs.Create.ImageDocument('', { _height: height, _width: width, title: field.title.replace(/\$\$/g, ''), x: left, y: top }); + return doc; + }); + + const template = Docs.Create.FreeformDocument(fields, { _height: tempHeight, _width: tempWidth, title: layout.template_type, x: 400000, y: 400000 }); + + mainCollection.addDocument(template); + + GPTTemplates.push(template); + }); + + setTimeout(() => { + this.setGSuggestedTemplates(GPTTemplates); /*GPTTemplates.forEach(template => mainCollection.removeDocument(template))*/ + }, 100); + + this.forceUpdate(); + }; + + editTemplate = (doc: Doc) => { + //this.closeMenu(); + DocumentViewInternal.addDocTabFunc(doc, OpenWhere.addRight); + DocumentView.DeselectAll(); + Doc.UnBrushDoc(doc); + }; + + removeTemplate = (doc: Doc) => { + this._templateDocs.splice(this._templateDocs.indexOf(doc), 1); + }; + + testTemplate = async () => { + // const temp = TemplateLayouts.FourField001; + // const title: Doc = FieldFuncs.TextField({tl: temp.fields[0].tl, br: temp.fields[0].br}, temp.height, temp.width, 'title', 'Title', {backgroundColor: 'transparent'}); + // const image: Doc = FieldFuncs.ImageField({tl: temp.fields[1].tl, br: temp.fields[1].br}, temp.height, temp.width, 'title', '', {borderColor: '#159fe4', borderWidth: '10', cornerRounding: 10, rotation: 40}); + // const caption: Doc = FieldFuncs.TextField({tl: temp.fields[2].tl, br: temp.fields[2].br}, temp.height, temp.width, 'title', 'Caption', {backgroundColor: 'transparent'}); + // const desc: Doc = FieldFuncs.TextField({tl: temp.fields[3].tl, br: temp.fields[3].br}, temp.height, temp.width, 'title', '', {backgroundColor: 'lightblue', borderColor: '#159fe4', borderWidth: '10', cornerRounding: 10}); + + // const doc = Docs.Create.FreeformDocument([title, image, caption, desc], { _height: temp.height, _width: temp.width, title: 'hey', x: 400, y: 400 }); + + // const mainCollection = this._dataViz?.DocumentView?.().containerViewPath?.().lastElement()?.ComponentView as CollectionFreeFormView; + // mainCollection.addDocument(doc); + + // const temp = TemplateLayouts.FourField001; + + // const img: Col = {type: TemplateFieldType.TEXT, title: 'Type', desc: 'description whpoo', size: TemplateFieldSize.LARGE, defaultContent: ''}; + // const capt1: Col = {type: TemplateFieldType.TEXT, title: 'Image', desc: 'description hey', size: TemplateFieldSize.TINY}; + // const capt2: Col = {type: TemplateFieldType.TEXT, title: 'Locality', desc: '', size: TemplateFieldSize.TINY, defaultContent: ''}; + // const desc: Col = {type: TemplateFieldType.TEXT, title: 'Description', desc: '', size: TemplateFieldSize.LARGE, defaultContent: 'This is a description of a rock. It is kind of long. It is very long. It is gratuitous. This description should be shorter. Oh well. This is a description of a rock. It is kind of long. It is very long. It is gratuitous. This description should be shorter. Oh well. This is a description of a rock. It is kind of long. It is very long. It is gratuitous. This description should be shorter. Oh well.'}; + + // const assignments = {'0': img, '1': capt1, '2': capt2, '3': desc} + + // this.createEmptyTemplate(temp, assignments); + // console.log(this.findValidTemplates(this.fieldsInfos, TemplateLayouts.allTemplates)); + + // console.log(this._dataViz?.colsInfo.get("IMG")?.size, this._dataViz?.colsInfo.get("IMG")?.type) + // console.log(this.fieldsInfos) + + try { + const res = await gptImageCall('Image of panda eating a cookie'); + + if (res) { + const result = await Networking.PostToServer('/uploadRemoteImage', { sources: res }); + + console.log(result); + } + } catch (e) { + console.log(e); + } + }; + + @action addField = () => { + const newFields: Col[] = this._columns.concat([{ title: '', type: TemplateFieldType.UNSET, desc: '', sizes: [] }]); + this._columns = newFields; + }; + + @action removeField = (field: { title: string; type: string; desc: string }) => { + if (this._dataViz?.axes.includes(field.title)) { + this._dataViz.selectAxes(this._dataViz.axes.filter(col => col !== field.title)); + } else { + const toRemove = this._columns.filter(f => f === field); + if (!toRemove) return; + + if (toRemove.length > 1) { + while (toRemove.length > 1) { + toRemove.pop(); + } + } + + if (this._columns.length === 1) { + this._columns = []; + } else { + this._columns.splice(this._columns.indexOf(toRemove[0]), 1); + } + } + }; + + @action setColTitle = (column: Col, title: string) => { + if (this.selectedFields.includes(column.title)) { + this._dataViz?.setColumnTitle(column.title, title); + } else { + column.title = title; + } + this.forceUpdate(); + }; + + @action setColType = (column: Col, type: TemplateFieldType) => { + if (this.selectedFields.includes(column.title)) { + this._dataViz?.setColumnType(column.title, type); + } else { + column.type = type; + } + this.forceUpdate(); + }; + + modifyColSizes = (column: Col, size: TemplateFieldSize, valid: boolean) => { + if (this.selectedFields.includes(column.title)) { + this._dataViz?.modifyColumnSizes(column.title, size, valid); + } else { + if (!valid && column.sizes.includes(size)) { + column.sizes.splice(column.sizes.indexOf(size), 1); + } else if (valid && !column.sizes.includes(size)) { + column.sizes.push(size); + } + } + this.forceUpdate(); + }; + + setColDesc = (column: Col, desc: string) => { + if (this.selectedFields.includes(column.title)) { + this._dataViz?.setColumnDesc(column.title, desc); + } else { + column.desc = desc; + } + this.forceUpdate(); + }; + + generateGPTImage = async (prompt: string): Promise<string | undefined> => { + console.log(prompt); + + try { + const res = await gptImageCall(prompt); + + if (res) { + const result = await Networking.PostToServer('/uploadRemoteImage', { sources: res }); + const source = ClientUtils.prepend(result[0].accessPaths.agnostic.client); + return source; + } + } catch (e) { + console.log(e); + } + }; + + matchesForTemplate = (template: TemplateDocInfos, cols: Col[]): number[][] => { + const colMatchesField = (col: Col, field: Field) => { + return field.sizes?.some(size => col.sizes?.includes(size)) && field.types?.includes(col.type); + }; + + const matches: number[][] = Array(template.fields.length) + .fill([]) + .map(() => []); + + template.fields.forEach((field, i) => { + cols.forEach((col, v) => { + if (colMatchesField(col, field)) { + matches[i].push(v); + } + }); + }); + + return matches; + }; + + maxMatches = (fieldsCt: number, matches: number[][]) => { + const used: boolean[] = Array(fieldsCt).fill(false); + const mt: number[] = Array(fieldsCt).fill(-1); + + const augmentingPath = (v: number): boolean => { + if (used[v]) return false; + used[v] = true; + for (const to of matches[v]) { + if (mt[to] === -1 || augmentingPath(mt[to])) { + mt[to] = v; + return true; + } + } + return false; + }; + + for (let v = 0; v < fieldsCt; ++v) { + used.fill(false); + augmentingPath(v); + } + + let count: number = 0; + + for (let i = 0; i < fieldsCt; ++i) { + if (mt[i] !== -1) ++count; + } + + return count; + }; + + findValidTemplates = (cols: Col[], templates: TemplateDocInfos[]) => { + let validTemplates: any[] = []; + templates.forEach(template => { + const numFields = template.fields.length; + if (!(numFields === cols.length)) return; + const matches = this.matchesForTemplate(template, cols); + if (this.maxMatches(numFields, matches) === numFields) { + validTemplates.push(template.title); + } + }); + + validTemplates = validTemplates.map(title => TemplateLayouts.getTemplateByTitle(title)); + + return validTemplates; + }; + + // createColumnField = (template: TemplateDocInfos, field: Field, column: Col): Doc => { + + // if (field.subfields) { + // const doc = FieldFuncs.FreeformField({ + // tl: field.tl, + // br: field.br }, + // template.height, + // template.width, + // column.title, + // '', + // field.opts + // ); + + // field.subfields[1].forEach(f => { + // const fDoc = () + // }) + + // } + + // return new Doc; + // } + + /** + * Populates a preset template framework with content from a datavizbox or any AI-generated content. + * @param template the preloaded template framework being filled in + * @param assignments a list of template field numbers (from top to bottom) and their assigned columns from the linked dataviz + * @returns a doc containing the fully rendered template + */ + fillPresetTemplate = async (template: TemplateDocInfos, assignments: { [field: string]: Col }): Promise<Doc> => { + const wordLimit = (size: TemplateFieldSize) => { + switch (size) { + case TemplateFieldSize.TINY: + return 2; + case TemplateFieldSize.SMALL: + return 5; + case TemplateFieldSize.MEDIUM: + return 20; + case TemplateFieldSize.LARGE: + return 50; + case TemplateFieldSize.HUGE: + return 100; + default: + return 10; + } + }; + + const renderTextCalls = async (): Promise<Doc[]> => { + const rendered: Doc[] = []; + + if (GPTTextCalls.length) { + try { + const prompt = fieldContent + GPTTextAssignment; + + const res = await gptAPICall(prompt, GPTCallType.FILL); + + if (res) { + const assignments: { [title: string]: { number: string; content: string } } = JSON.parse(res); + //console.log('assignments', GPTAssignments, 'assignment string', GPTAssignmentString, 'field content', fieldContent, 'response', res, 'assignments', assignments); + Object.entries(assignments).forEach(([title, info]) => { + const field: Field = template.fields[Number(info.number)]; + const col = this.getColByTitle(title); + + const doc = FieldUtils.TextField( + { + tl: field.tl, + br: field.br, + }, + template.height, + template.width, + col.title, + info.content ?? '', + field.opts + ); + + rendered.push(doc); + }); + } + } catch (err) { + console.log(err); + } + } + + return rendered; + }; + + const createGeneratedImage = async (fieldNum: string, col: Col, prompt: string) => { + const url = await this.generateGPTImage(prompt); + const field: Field = template.fields[Number(fieldNum)]; + const doc = FieldUtils.ImageField( + { + tl: field.tl, + br: field.br, + }, + template.height, + template.width, + col.title, + url ?? '', + field.opts + ); + + return doc; + }; + + const renderImageCalls = async (): Promise<Doc[]> => { + const rendered: Doc[] = []; + const calls = GPTIMGCalls; + + if (calls.length) { + try { + const renderedImages: Doc[] = await Promise.all( + calls.map(async ([fieldNum, col]) => { + const sysPrompt = + 'Your job is to create a prompt for an AI image generator to help it generate an image based on existing content in a template and a user prompt. Your prompt should focus heavily on visual elements to help the image generator; avoid unecessary info that might distract it. ONLY INCLUDE THE PROMPT, NO OTHER TEXT OR EXPLANATION. The existing content is as follows: ' + + fieldContent + + ' **** The user prompt is: ' + + col.desc; + + const prompt = await gptAPICall(sysPrompt, GPTCallType.COMPLETEPROMPT); + console.log(sysPrompt, prompt); + + return createGeneratedImage(fieldNum, col, prompt); + }) + ); + + const renderedTemplates: Doc[] = await Promise.all(renderedImages); + renderedTemplates.forEach(doc => rendered.push(doc)); + } catch (e) { + console.log(e); + } + } + + return rendered; + }; + + const fields: Doc[] = []; + + const GPTAssignments = Object.entries(assignments).filter(([f, col]) => this._columns.includes(col)); + const nonGPTAssignments: [string, Col][] = Object.entries(assignments).filter(a => !GPTAssignments.includes(a)); + const GPTTextCalls = GPTAssignments.filter(([str, col]) => col.type === TemplateFieldType.TEXT); + const GPTIMGCalls = GPTAssignments.filter(([str, col]) => col.type === TemplateFieldType.VISUAL); + + const stringifyGPTInfo = (calls: [string, Col][]): string => { + let string: string = '*** COLUMN INFO:'; + calls.forEach(([fieldNum, col]) => { + string += `--- title: ${col.title}, prompt: ${col.desc}, word limit: ${wordLimit(col.sizes[0])} words, assigned field: ${fieldNum} ---`; + }); + return (string += ' ***'); + }; + + const GPTTextAssignment = stringifyGPTInfo(GPTTextCalls); + + let fieldContent: string = ''; + + Object.entries(nonGPTAssignments).forEach(([f, strCol]) => { + const field: Field = template.fields[Number(f)]; + const col = strCol[1]; + + const doc = (col.type === TemplateFieldType.VISUAL ? FieldUtils.ImageField : FieldUtils.TextField)( + { + tl: field.tl, + br: field.br, + }, + template.height, + template.width, + col.title, + col.defaultContent ?? '', + field.opts + ); + + fieldContent += `--- Field #${f} (title: ${col.title}): ${col.defaultContent ?? ''} ---`; + + fields.push(doc); + }); + + template.decorations.forEach(dec => { + const doc = FieldUtils.FreeformField( + { + tl: dec.tl, + br: dec.br, + }, + template.height, + template.width, + '', + '', + dec.opts + ); + + fields.push(doc); + }); + + const createMainDoc = (): Doc => { + const main = Docs.Create.FreeformDocument(fields, { + _height: template.height, + _width: template.width, + title: template.title, + backgroundColor: template.opts.backgroundColor, + _layout_borderRounding: `${template.opts.cornerRounding}px` ?? '0px', + borderWidth: template.opts.borderWidth, + borderColor: template.opts.borderColor, + x: 40000, + y: 40000, + }); + + const mainCollection = this._dataViz?.DocumentView?.().containerViewPath?.().lastElement()?.ComponentView as CollectionFreeFormView; + mainCollection.addDocument(main); + + return main; + }; + + const textCalls = await renderTextCalls(); + const imageCalls = await renderImageCalls(); + + textCalls.forEach(doc => { + fields.push(doc); + }); + imageCalls.forEach(doc => { + fields.push(doc); + }); + + return createMainDoc(); + }; + + compileFieldDescriptions = (templates: TemplateDocInfos[]): string => { + let descriptions: string = ''; + templates.forEach(template => { + descriptions += `---------- NEW TEMPLATE TO INCLUDE: Description of template ${template.title}'s fields: `; + template.fields.forEach((field, index) => { + descriptions += `{Field #${index}: ${field.description}} `; + }); + }); + + return descriptions; + }; + + compileColDescriptions = (cols: Col[]): string => { + let descriptions: string = ' ------------- COL DESCRIPTIONS START HERE:'; + cols.forEach(col => (descriptions += `{title: ${col.title}, sizes: ${String(col.sizes)}, type: ${col.type}, descreiption: ${col.desc}} `)); + + return descriptions; + }; + + getColByTitle = (title: string) => { + return this.fieldsInfos.filter(col => col.title === title)[0]; + }; + + @action + assignColsToFields = async (templates: TemplateDocInfos[], cols: Col[]): Promise<[TemplateDocInfos, { [field: number]: Col }][]> => { + const fieldDescriptions: string = this.compileFieldDescriptions(templates); + const colDescriptions: string = this.compileColDescriptions(cols); + + const inputText = fieldDescriptions.concat(colDescriptions); + + ++this._callCount; + const origCount = this._callCount; + + let prompt: string = `(${origCount}) ${inputText}`; + + this._GPTLoading = true; + + try { + const res = await gptAPICall(prompt, GPTCallType.TEMPLATE); + + if (res && this._callCount === origCount) { + const assignments: { [templateTitle: string]: { [field: string]: string } } = JSON.parse(res); + const brokenDownAssignments: [TemplateDocInfos, { [field: number]: Col }][] = []; + + Object.entries(assignments).forEach(([tempTitle, assignment]) => { + const template = TemplateLayouts.getTemplateByTitle(tempTitle); + if (!template) return; + const toObj = Object.entries(assignment).reduce( + (a, [fieldNum, colTitle]) => { + a[Number(fieldNum)] = this.getColByTitle(colTitle); + return a; + }, + {} as { [field: number]: Col } + ); + brokenDownAssignments.push([template, toObj]); + }); + return brokenDownAssignments; + } + } catch (err) { + console.error(err); + } + + return []; + }; + + generatePresetTemplates = async () => { + this._dataViz?.updateColDefaults(); + + const cols = this.fieldsInfos; + const templates = this.findValidTemplates(cols, TemplateLayouts.allTemplates); + + const assignments: [TemplateDocInfos, { [field: number]: Col }][] = await this.assignColsToFields(templates, cols); + + const renderedTemplatePromises: Promise<Doc>[] = assignments.map(([template, assignments]) => this.fillPresetTemplate(template, assignments)); + + const renderedTemplates: Doc[] = await Promise.all(renderedTemplatePromises); + + setTimeout(() => { + this.setGSuggestedTemplates(renderedTemplates); + this._GPTLoading = false; + }); + }; + + @action setExpandedView = (info: { icon: ImageField; doc: Doc } | undefined) => { + this._expandedPreview = info; + }; + + get templatesPreviewContents() { + const renderedTemplates: Doc[] = []; + + const GPTOptions = <div></div>; + + //<img className='docCreatorMenu-preview-image expanded' src={this._expandedPreview.icon!.url.href.replace(".png", "_o.png")} /> + + return ( + <div className={`docCreatorMenu-templates-view`}> + {this._expandedPreview ? ( + <div className="docCreatorMenu-expanded-template-preview"> + <img className="docCreatorMenu-preview-image expanded" src={this._expandedPreview.icon!.url.href.replace('.png', '_o.png')} /> + <div className="right-buttons-panel"> + <button className="docCreatorMenu-menu-button section-reveal-options top-right" onPointerDown={e => this.setUpButtonClick(e, () => this.setExpandedView(undefined))}> + <FontAwesomeIcon icon="minimize" /> + </button> + <button className="docCreatorMenu-menu-button section-reveal-options top-right-lower" onPointerDown={e => this.setUpButtonClick(e, () => this._expandedPreview && this._templateDocs.push(this._expandedPreview.doc))}> + <FontAwesomeIcon icon="plus" color="white" /> + </button> + </div> + </div> + ) : ( + <div> + <div className="docCreatorMenu-section" style={{ height: this._GPTOpt ? 200 : 200 }}> + <div className="docCreatorMenu-section-topbar"> + <div className="docCreatorMenu-section-title">Suggested Templates</div> + <button className="docCreatorMenu-menu-button section-reveal-options" onPointerDown={e => this.setUpButtonClick(e, () => runInAction(() => (this._menuContent = 'dashboard')))}> + <FontAwesomeIcon icon="gear" /> + </button> + </div> + <div className="docCreatorMenu-templates-preview-window" style={{ justifyContent: this._GPTLoading || this._menuDimensions.width > 400 ? 'center' : '' }}> + {this._GPTLoading ? ( + <div className="loading-spinner"> + <ReactLoading type="spin" color={StrCast(Doc.UserDoc().userVariantColor)} height={30} width={30} /> + </div> + ) : ( + this._suggestedTemplates + ?.map(doc => ({ icon: ImageCast(doc.icon), doc })) + .filter(info => info.icon && info.doc) + .map(info => ( + <div + className="docCreatorMenu-preview-window" + style={{ + border: this._selectedTemplate === info.doc ? `solid 3px ${Colors.MEDIUM_BLUE}` : '', + boxShadow: this._selectedTemplate === info.doc ? `0 0 15px rgba(68, 118, 247, .8)` : '', + }} + onPointerDown={e => this.setUpButtonClick(e, () => runInAction(() => this.updateSelectedTemplate(info.doc)))}> + <button + className="option-button left" + onPointerDown={e => + this.setUpButtonClick(e, () => { + this.setExpandedView(info); + }) + }> + <FontAwesomeIcon icon="magnifying-glass" color="white" /> + </button> + <button className="option-button right" onPointerDown={e => this.setUpButtonClick(e, () => this._templateDocs.push(info.doc))}> + <FontAwesomeIcon icon="plus" color="white" /> + </button> + <img className="docCreatorMenu-preview-image" src={info.icon!.url.href.replace('.png', '_o.png')} /> + </div> + )) + )} + </div> + <div className="docCreatorMenu-GPT-options"> + <div className="docCreatorMenu-GPT-options-container"> + <button className="docCreatorMenu-menu-button" onPointerDown={e => this.setUpButtonClick(e, () => this.generatePresetTemplates())}> + <FontAwesomeIcon icon="arrows-rotate" /> + </button> + </div> + {this._GPTOpt ? GPTOptions : null} + </div> + </div> + <hr className="docCreatorMenu-option-divider full no-margin" /> + <div className="docCreatorMenu-section"> + <div className="docCreatorMenu-section-topbar"> + <div className="docCreatorMenu-section-title">Your Templates</div> + <button className="docCreatorMenu-menu-button section-reveal-options" onPointerDown={e => this.setUpButtonClick(e, () => (this._GPTOpt = !this._GPTOpt))}> + <FontAwesomeIcon icon="gear" /> + </button> + </div> + <div className="docCreatorMenu-templates-preview-window" style={{ justifyContent: this._menuDimensions.width > 400 ? 'center' : '' }}> + <div className="docCreatorMenu-preview-window empty" onPointerDown={e => this.testTemplate()}> + <FontAwesomeIcon icon="plus" color="rgb(160, 160, 160)" /> + </div> + {this._templateDocs + .map(doc => ({ icon: ImageCast(doc.icon), doc })) + .filter(info => info.icon && info.doc) + .map(info => { + if (renderedTemplates.includes(info.doc)) return undefined; + renderedTemplates.push(info.doc); + return ( + <div + className="docCreatorMenu-preview-window" + style={{ + border: this._selectedTemplate === info.doc ? `solid 3px ${Colors.MEDIUM_BLUE}` : '', + boxShadow: this._selectedTemplate === info.doc ? `0 0 15px rgba(68, 118, 247, .8)` : '', + }} + onPointerDown={e => this.setUpButtonClick(e, () => runInAction(() => this.updateSelectedTemplate(info.doc)))}> + <button + className="option-button left" + onPointerDown={e => + this.setUpButtonClick(e, () => { + this.editTemplate(info.doc); + }) + }> + <FontAwesomeIcon icon="pencil" color="black" /> + </button> + <button + className="option-button right" + onPointerDown={e => + this.setUpButtonClick(e, () => { + this.removeTemplate(info.doc); + }) + }> + <FontAwesomeIcon icon="trash" color="black" /> + </button> + <img className="docCreatorMenu-preview-image" src={info.icon!.url.href.replace('.png', '_o.png')} /> + </div> + ); + })} + </div> + </div> + </div> + )} + </div> + ); + } + + get savedLayoutsPreviewContents() { + return ( + <div className="docCreatorMenu-preview-container"> + {this._savedLayouts.map((layout, index) => ( + <div + className="docCreatorMenu-preview-window" + style={{ + border: this.isSelectedLayout(layout) ? `solid 3px ${Colors.MEDIUM_BLUE}` : '', + boxShadow: this.isSelectedLayout(layout) ? `0 0 15px rgba(68, 118, 247, .8)` : '', + }} + onPointerDown={e => this.setUpButtonClick(e, () => runInAction(() => this.updateSelectedSavedLayout(layout)))}> + {this.layoutPreviewContents(87, layout, true, index)} + </div> + ))} + </div> + ); + } + + @action updateXMargin = (input: string) => { + this._layout.xMargin = Number(input); + }; + @action updateYMargin = (input: string) => { + this._layout.yMargin = Number(input); + }; + @action updateColumns = (input: string) => { + this._layout.columns = Number(input); + }; + + get layoutConfigOptions() { + const optionInput = (icon: string, func: Function, def?: number, key?: string, noMargin?: boolean) => { + return ( + <div className="docCreatorMenu-option-container small no-margin" key={key} style={{ marginTop: noMargin ? '0px' : '' }}> + <div className="docCreatorMenu-option-title config layout-config"> + <FontAwesomeIcon icon={icon as any} /> + </div> + <input defaultValue={def} onInput={e => func(e.currentTarget.value)} className="docCreatorMenu-input config layout-config" /> + </div> + ); + }; + + switch (this._layout.type) { + case LayoutType.Row: + return <div className="docCreatorMenu-configuration-bar">{optionInput('arrows-left-right', this.updateXMargin, this._layout.xMargin, '0')}</div>; + case LayoutType.Column: + return <div className="docCreatorMenu-configuration-bar">{optionInput('arrows-up-down', this.updateYMargin, this._layout.yMargin, '1')}</div>; + case LayoutType.Grid: + return ( + <div className="docCreatorMenu-configuration-bar"> + {optionInput('arrows-up-down', this.updateYMargin, this._layout.xMargin, '2')} + {optionInput('arrows-left-right', this.updateXMargin, this._layout.xMargin, '3')} + {optionInput('table-columns', this.updateColumns, this._layout.columns, '4', true)} + </div> + ); + case LayoutType.Stacked: + return null; + default: + break; + } + } + + // doc = () => { + // return Docs.Create.FreeformDocument([], { _height: 200, _width: 200, title: 'title'}); + // } + + screenToLocalTransform = () => this._props.ScreenToLocalTransform(); + + layoutPreviewContents = (outerSpan: number, altLayout?: DataVizTemplateLayout, small: boolean = false, id?: number) => { + const doc: Doc | undefined = altLayout ? altLayout.template : this._selectedTemplate; + if (!doc) return; + + const layout = altLayout ? altLayout.layout : this._layout; + + const docWidth: number = Number(doc._width); + const docHeight: number = Number(doc._height); + const horizontalSpan: number = (docWidth + layout.xMargin) * (altLayout ? altLayout.columns : this.columnsCount) - layout.xMargin; + const verticalSpan: number = (docHeight + layout.yMargin) * (altLayout ? altLayout.rows : this.rowsCount) - layout.yMargin; + const largerSpan: number = horizontalSpan > verticalSpan ? horizontalSpan : verticalSpan; + const scaledDown = (input: number) => { + return input / ((largerSpan / outerSpan) * this._layoutPreviewScale); + }; + const fontSize = Math.min(scaledDown(docWidth / 3), scaledDown(docHeight / 3)); + + return ( + // <div className='divvv' style={{width: 100, height: 100, border: `1px solid white`}}> + // <CollectionFreeFormView + // // eslint-disable-next-line react/jsx-props-no-spreading + // {...this._props} + // Document={new Doc()} + // isContentActive={returnFalse} + // setContentViewBox={emptyFunction} + // NativeWidth={() => 100} + // NativeHeight={() => 100} + // pointerEvents={SnappingManager.IsDragging ? returnAll : returnNone} + // isAnnotationOverlay + // isAnnotationOverlayScrollable + // childDocumentsActive={returnFalse} + // fieldKey={this._props.fieldKey + '_annotations'} + // dropAction={dropActionType.move} + // select={emptyFunction} + // addDocument={returnFalse} + // removeDocument={returnFalse} + // moveDocument={returnFalse} + // renderDepth={this._props.renderDepth + 1}> + // {null} + // </CollectionFreeFormView> + // </div> + <div className="docCreatorMenu-layout-preview-window-wrapper" id={String(id) ?? undefined}> + <div className="docCreatorMenu-zoom-button-container"> + <button className="docCreatorMenu-zoom-button" onPointerDown={e => this.setUpButtonClick(e, () => runInAction(() => (this._layoutPreviewScale *= 1.25)))}> + <FontAwesomeIcon icon={'minus'} /> + </button> + <button className="docCreatorMenu-zoom-button zoom-in" onPointerDown={e => this.setUpButtonClick(e, () => runInAction(() => (this._layoutPreviewScale *= 0.75)))}> + <FontAwesomeIcon icon={'plus'} /> + </button> + {altLayout ? ( + <button className="docCreatorMenu-zoom-button trash" onPointerDown={e => this.setUpButtonClick(e, () => runInAction(() => this._savedLayouts.splice(this._savedLayouts.indexOf(altLayout), 1)))}> + <FontAwesomeIcon icon={'trash'} /> + </button> + ) : null} + </div> + { + <div + id={String(id) ?? undefined} + className={`docCreatorMenu-layout-preview-window ${small ? 'small' : ''}`} + style={{ + gridTemplateColumns: `repeat(${altLayout ? altLayout.columns : this.columnsCount}, ${scaledDown(docWidth)}px`, + gridTemplateRows: `${scaledDown(docHeight)}px`, + gridAutoRows: `${scaledDown(docHeight)}px`, + rowGap: `${scaledDown(layout.yMargin)}px`, + columnGap: `${scaledDown(layout.xMargin)}px`, + }}> + {this._layout.type === LayoutType.Stacked ? ( + <div + className="docCreatorMenu-layout-preview-item" + style={{ + width: scaledDown(docWidth), + height: scaledDown(docHeight), + fontSize: fontSize, + }}> + All + </div> + ) : ( + this.docsToRender.map(num => ( + <div + onMouseEnter={() => this._dataViz?.setSpecialHighlightedRow(num)} + onMouseLeave={() => this._dataViz?.setSpecialHighlightedRow(undefined)} + className="docCreatorMenu-layout-preview-item" + style={{ + width: scaledDown(docWidth), + height: scaledDown(docHeight), + fontSize: fontSize, + }}> + {num} + </div> + )) + )} + </div> + } + </div> + ); + }; + + get optionsMenuContents() { + const layoutEquals = (layout: DataVizTemplateLayout) => {}; //TODO: ADD LATER + + const layoutOption = (option: LayoutType, optStyle?: {}, specialFunc?: Function) => { + return ( + <div + className="docCreatorMenu-dropdown-option" + style={optStyle} + onPointerDown={e => + this.setUpButtonClick(e, () => { + specialFunc?.(); + runInAction(() => (this._layout.type = option)); + }) + }> + {option} + </div> + ); + }; + + const selectionBox = (width: number, height: number, icon: string, specClass?: string, options?: JSX.Element[], manual?: boolean): JSX.Element => { + return ( + <div className="docCreatorMenu-option-container"> + <div className={`docCreatorMenu-option-title config ${specClass}`} style={{ width: width * 0.4, height: height }}> + <FontAwesomeIcon icon={icon as any} /> + </div> + {manual ? ( + <input className={`docCreatorMenu-input config ${specClass}`} style={{ width: width * 0.6, height: height }} /> + ) : ( + <select className={`docCreatorMenu-input config ${specClass}`} style={{ width: width * 0.6, height: height }}> + {options} + </select> + )} + </div> + ); + }; + + const repeatOptions = [0, 1, 2, 3, 4, 5]; + + return ( + <div className="docCreatorMenu-menu-container"> + <div className="docCreatorMenu-option-container layout"> + <div className="docCreatorMenu-dropdown-hoverable"> + <div className="docCreatorMenu-option-title">{this._layout.type ? this._layout.type.toUpperCase() : 'Choose Layout'}</div> + <div className="docCreatorMenu-dropdown-content"> + {layoutOption(LayoutType.Stacked)} + {layoutOption(LayoutType.Grid, undefined, () => { + if (!this._layout.columns) this._layout.columns = Math.ceil(Math.sqrt(this.docsToRender.length)); + })} + {layoutOption(LayoutType.Row)} + {layoutOption(LayoutType.Column)} + {layoutOption(LayoutType.Custom, { borderBottom: `0px` })} + </div> + </div> + <button className="docCreatorMenu-menu-button preview-toggle" onPointerDown={e => this.setUpButtonClick(e, () => runInAction(() => (this._layoutPreview = !this._layoutPreview)))}> + <FontAwesomeIcon icon={this._layoutPreview ? 'minus' : 'magnifying-glass'} /> + </button> + </div> + {this._layout.type ? this.layoutConfigOptions : null} + {this._layoutPreview ? this.layoutPreviewContents(this._menuDimensions.width * 0.75) : null} + {selectionBox( + 60, + 20, + 'repeat', + undefined, + repeatOptions.map(num => <option onPointerDown={e => (this._layout.repeat = num)}>{`${num}x`}</option>) + )} + <hr className="docCreatorMenu-option-divider" /> + <div className="docCreatorMenu-general-options-container"> + <button + className="docCreatorMenu-save-layout-button" + onPointerDown={e => + setupMoveUpEvents( + this, + e, + returnFalse, + emptyFunction, + undoable(clickEv => { + clickEv.stopPropagation(); + if (!this._selectedTemplate) return; + const layout: DataVizTemplateLayout = { + template: this._selectedTemplate, + layout: { type: this._layout.type, xMargin: this._layout.xMargin, yMargin: this._layout.yMargin, repeat: 0 }, + columns: this.columnsCount, + rows: this.rowsCount, + docsNumList: this.docsToRender, + }; + if (!this._savedLayouts.includes(layout)) { + this._savedLayouts.push(layout); + } + }, 'make docs') + ) + }> + <FontAwesomeIcon icon="floppy-disk" /> + </button> + <button + className="docCreatorMenu-create-docs-button" + style={{ backgroundColor: this.canMakeDocs ? '' : 'rgb(155, 155, 155)', border: this.canMakeDocs ? '' : 'solid 2px rgb(180, 180, 180)' }} + onPointerDown={e => + setupMoveUpEvents( + this, + e, + returnFalse, + emptyFunction, + undoable(clickEv => { + clickEv.stopPropagation(); + if (!this._selectedTemplate) return; + const templateInfo: DataVizTemplateInfo = { doc: this._selectedTemplate, layout: this._layout, referencePos: { x: this._pageX + 450, y: this._pageY }, columns: this.columnsCount }; + this._dataViz?.createDocsFromTemplate(templateInfo); + }, 'make docs') + ) + }> + <FontAwesomeIcon icon="plus" /> + </button> + </div> + </div> + ); + } + + get dashboardContents() { + const sizes: string[] = ['tiny', 'small', 'medium', 'large', 'huge']; + + const fieldPanel = (field: Col) => { + return ( + <div className="field-panel"> + <div className="top-bar"> + <span className="field-title">{`${field.title} Field`}</span> + <button className="docCreatorMenu-menu-button section-reveal-options no-margin" onPointerDown={e => this.setUpButtonClick(e, this.addField)} style={{ position: 'absolute', right: '0px' }}> + <FontAwesomeIcon icon="minus" /> + </button> + </div> + <div className="opts-bar"> + <div className="opt-box"> + <div className="top-bar"> Title </div> + <textarea className="content" style={{ width: '100%', height: 'calc(100% - 20px)' }} defaultValue={field.title} placeholder={'Enter title'} onChange={e => this.setColTitle(field, e.target.value)} /> + </div> + <div className="opt-box"> + <div className="top-bar"> Type </div> + <div className="content"> + <span className="type-display">{field.type === TemplateFieldType.TEXT ? 'Text Field' : field.type === TemplateFieldType.VISUAL ? 'File Field' : ''}</span> + <div className="bubbles"> + <input + className="bubble" + type="radio" + name="type" + onClick={() => { + this.setColType(field, TemplateFieldType.TEXT); + }} + /> + <div className="text">Text</div> + <input + className="bubble" + type="radio" + name="type" + onClick={() => { + this.setColType(field, TemplateFieldType.VISUAL); + }} + /> + <div className="text">File</div> + </div> + </div> + </div> + </div> + <div className="sizes-box"> + <div className="top-bar"> Valid Sizes </div> + <div className="content"> + <div className="bubbles"> + {sizes.map(size => ( + <> + <input + className="bubble" + type="checkbox" + name="type" + checked={field.sizes.includes(size as TemplateFieldSize)} + onChange={e => { + this.modifyColSizes(field, size as TemplateFieldSize, e.target.checked); + }} + /> + <div className="text">{size}</div> + </> + ))} + </div> + </div> + </div> + <div className="desc-box"> + <div className="top-bar"> Description </div> + <textarea + className="content" + onChange={e => this.setColDesc(field, e.target.value)} + defaultValue={field.desc === this._dataViz?.GPTSummary?.get(field.title)?.desc ? '' : field.desc} + placeholder={this._dataViz?.GPTSummary?.get(field.title)?.desc ?? 'Add a description to help with template generation.'} + /> + </div> + </div> + ); + }; + + return ( + <div className="docCreatorMenu-dashboard-view"> + <div className="topbar"> + <button className="docCreatorMenu-menu-button section-reveal-options" onPointerDown={e => this.setUpButtonClick(e, this.addField)}> + <FontAwesomeIcon icon="plus" /> + </button> + <button className="docCreatorMenu-menu-button section-reveal-options float-right" onPointerDown={e => this.setUpButtonClick(e, () => runInAction(() => (this._menuContent = 'templates')))}> + <FontAwesomeIcon icon="arrow-left" /> + </button> + </div> + <div className="panels-container">{this.fieldsInfos.map(field => fieldPanel(field))}</div> + </div> + ); + } + + get renderSelectedViewType() { + switch (this._menuContent) { + case 'templates': + return this.templatesPreviewContents; + case 'options': + return this.optionsMenuContents; + case 'saved': + return this.savedLayoutsPreviewContents; + case 'dashboard': + return this.dashboardContents; + default: + return undefined; + } + } + + get resizePanes() { + const ref = this._ref?.getBoundingClientRect(); + const height: number = ref?.height ?? 0; + const width: number = ref?.width ?? 0; + + return [ + <div className='docCreatorMenu-resizer top' onPointerDown={this.onResizePointerDown} style={{width: width, left: 0, top: -7}}/>, + <div className='docCreatorMenu-resizer right' onPointerDown={this.onResizePointerDown} style={{height: height, left: width - 3, top: 0}}/>, + <div className='docCreatorMenu-resizer bottom' onPointerDown={this.onResizePointerDown} style={{width: width, left: 0, top: height - 3}}/>, + <div className='docCreatorMenu-resizer left' onPointerDown={this.onResizePointerDown} style={{height: height, left: -7, top: 0}}/>, + <div className='docCreatorMenu-resizer topRight' onPointerDown={this.onResizePointerDown} style={{left: width - 5, top: -10, cursor: 'nesw-resize'}}/>, + <div className='docCreatorMenu-resizer topLeft' onPointerDown={this.onResizePointerDown} style={{left: -10, top: -10, cursor: 'nwse-resize'}}/>, + <div className='docCreatorMenu-resizer bottomRight' onPointerDown={this.onResizePointerDown} style={{left: width - 5, top: height - 5, cursor: 'nwse-resize'}}/>, + <div className='docCreatorMenu-resizer bottomLeft' onPointerDown={this.onResizePointerDown} style={{left: -10, top: height - 5, cursor: 'nesw-resize'}}/> + ]; //prettier-ignore + } + + render() { + const topButton = (icon: string, opt: string, func: Function, tag: string) => { + return ( + <div className={`top-button-container ${tag} ${opt === this._menuContent ? 'selected' : ''}`}> + <div + className="top-button-content" + onPointerDown={e => + this.setUpButtonClick(e, () => + runInAction(() => { + func(); + }) + ) + }> + <FontAwesomeIcon icon={icon as any} /> + </div> + </div> + ); + }; + + const onPreviewSelected = () => { + this._menuContent = 'templates'; + }; + const onSavedSelected = () => { + this._menuContent = 'dashboard'; + }; + const onOptionsSelected = () => { + this._menuContent = 'options'; + if (!this._layout.columns) this._layout.columns = Math.ceil(Math.sqrt(this.docsToRender.length)); + }; + + return ( + <div className="docCreatorMenu"> + {!this._shouldDisplay ? undefined : ( + <div + className="docCreatorMenu-cont" + ref={r => (this._ref = r)} + style={{ + display: '', + left: this._pageX, + top: this._pageY, + width: this._menuDimensions.width, + height: this._menuDimensions.height, + background: SnappingManager.userBackgroundColor, + color: SnappingManager.userColor, + }}> + {this.resizePanes} + <div + className="docCreatorMenu-menu" + onPointerDown={e => + setupMoveUpEvents( + this, + e, + e => { + this._dragging = true; + this._startPos = { x: 0, y: 0 }; + this._startPos.x = e.pageX - (this._ref?.getBoundingClientRect().left ?? 0); + this._startPos.y = e.pageY - (this._ref?.getBoundingClientRect().top ?? 0); + document.addEventListener('pointermove', this.onDrag); + return true; + }, + emptyFunction, + undoable(clickEv => { + clickEv.stopPropagation(); + }, 'drag menu') + ) + }> + <div className="docCreatorMenu-top-buttons-container"> + {topButton('table-cells', 'templates', onPreviewSelected, 'left')} + {topButton('bars', 'options', onOptionsSelected, 'middle')} + {topButton('floppy-disk', 'saved', onSavedSelected, 'right')} + </div> + <button className="docCreatorMenu-menu-button close-menu" onPointerDown={e => this.setUpButtonClick(e, this.closeMenu)}> + <FontAwesomeIcon icon={'minus'} /> + </button> + </div> + {this.renderSelectedViewType} + </div> + )} + </div> + ); + } +} + +export interface DataVizTemplateInfo { + doc: Doc; + layout: { type: LayoutType; xMargin: number; yMargin: number; repeat: number }; + columns: number; + referencePos: { x: number; y: number }; +} + +export interface DataVizTemplateLayout { + template: Doc; + docsNumList: number[]; + layout: { type: LayoutType; xMargin: number; yMargin: number; repeat: number }; + columns: number; + rows: number; +} + +export enum TemplateFieldType { + TEXT = 'text', + VISUAL = 'visual', + UNSET = 'unset', +} + +export enum TemplateFieldSize { + TINY = 'tiny', + SMALL = 'small', + MEDIUM = 'medium', + LARGE = 'large', + HUGE = 'huge', +} + +export type Col = { + sizes: TemplateFieldSize[]; + desc: string; + title: string; + type: TemplateFieldType; + defaultContent?: string; +}; + +type Field = { + tl: [number, number]; + br: [number, number]; + opts: FieldOpts; + subfields?: Field[]; + types?: TemplateFieldType[]; + sizes?: TemplateFieldSize[]; + isDecoration?: boolean; + description?: string; +}; + +// class ContentField implements Field { +// tl: [number, number]; +// br: [number, number]; +// opts: FieldOpts; +// subfields?: Field[]; +// types?: TemplateFieldType[]; +// sizes?: TemplateFieldSize[]; +// description?: string; + +// constructor( tl: [number, number], br: [number, number], +// opts: FieldOpts, subfields?: Field[], +// types?: TemplateFieldType[], +// sizes?: TemplateFieldSize[], +// description?: string) { +// this.tl = tl; +// this.br = br; +// this.opts = opts; +// this.subfields = subfields; +// this.types = types; +// this.sizes = sizes; +// this.description = description; +// } + +// render = (content: any): Doc => { +// return new Doc; +// } +// } + +type DecorationField = Field; + +type InkDecoration = {}; + +type TemplateDecorations = Field | InkDecoration; + +export interface TemplateDocInfos { + title: string; + height: number; + width: number; + opts: TemplateOpts; + fields: Field[]; + decorations: Field[]; +} + +export interface FieldOpts { + backgroundColor?: string; + color?: string; + cornerRounding?: number; + borderWidth?: string; + borderColor?: string; + contentXCentering?: 'h-left' | 'h-center' | 'h-right'; + contentYCentering?: 'top' | 'center' | 'bottom'; + opacity?: number; + rotation?: number; + //animation?: boolean; + fontBold?: boolean; + fontTransform?: 'uppercase' | 'lowercase'; + fieldViewType?: 'freeform' | 'stacked'; +} + +interface TemplateOpts extends FieldOpts {} + +export class FieldUtils { + public static contentFields = (fields: Field[]) => { + let toRet: Field[] = []; + fields.forEach(field => { + if (!field.isDecoration) { + toRet.push(field); + } + toRet = toRet.concat(FieldUtils.contentFields(field.subfields ?? [])); + }); + + return toRet; + }; + + public static calculateFontSize = (contWidth: number, contHeight: number, text: string, uppercase: boolean): number => { + const words: string[] = text.split(/\s+/).filter(Boolean); + + let currFontSize = 1; + let rowsCount = 1; + let currTextHeight = currFontSize * rowsCount * 2; + + while (currTextHeight <= contHeight) { + let wordIndex = 0; + let currentRowWidth = 0; + let wordsInCurrRow = 0; + rowsCount = 1; + + while (wordIndex < words.length) { + const word = words[wordIndex]; + const wordWidth = word.length * currFontSize * 0.5; + //console.log(wordWidth) + + if (currentRowWidth + wordWidth <= contWidth) { + currentRowWidth += wordWidth; + ++wordsInCurrRow; + } else { + if (words.length !== 1 && words.length > wordsInCurrRow) { + rowsCount++; + currentRowWidth = wordWidth; + wordsInCurrRow = 1; + } else { + break; + } + } + + wordIndex++; + } + + currTextHeight = rowsCount * currFontSize * 2; + //console.log(rowsCount, currFontSize, currTextHeight) + + currFontSize += 1; + } + + return currFontSize - 1; + }; + + private static getDimensions = (coords: { tl: [number, number]; br: [number, number] }, parentWidth: number, parentHeight: number): { width: number; height: number; coord: { x: number; y: number } } => { + const l = (coords.tl[0] * parentHeight) / 2; + const t = coords.tl[1] * parentWidth / 2; //prettier-ignore + const r = (coords.br[0] * parentHeight) / 2; + const b = coords.br[1] * parentWidth / 2; //prettier-ignore + const width = r - l; + const height = b - t; + const coord = { x: l, y: t }; + //console.log(coords, parentWidth, parentHeight, height); + return { width, height, coord }; + }; + + public static FreeformField = (coords: { tl: [number, number]; br: [number, number] }, parentWidth: number, parentHeight: number, title: string, content: string, opts: FieldOpts) => { + const { width, height, coord } = FieldUtils.getDimensions(coords, parentWidth, parentHeight); + + const docWithBasicOpts = Docs.Create.FreeformDocument([], { + isDefaultTemplateDoc: true, + _height: height, + _width: width, + title: title, + x: coord.x, + y: coord.y, + backgroundColor: opts.backgroundColor ?? '', + _layout_borderRounding: `${opts.cornerRounding}px` ?? '0px', + borderColor: opts.borderColor, + borderWidth: opts.borderWidth, + opacity: opts.opacity, + hCentering: opts.contentXCentering, + _rotation: opts.rotation, + }); + + return docWithBasicOpts; + }; + + public static TextField = (coords: { tl: [number, number]; br: [number, number] }, parentWidth: number, parentHeight: number, title: string, content: string, opts: FieldOpts) => { + const { width, height, coord } = FieldUtils.getDimensions(coords, parentWidth, parentHeight); + + const bool = true; + + const docWithBasicOpts = Docs.Create.TextDocument(content, { + isDefaultTemplateDoc: true, + _height: height, + _width: width, + title: title, + x: coord.x, + y: coord.y, + _text_fontSize: `${FieldUtils.calculateFontSize(width, height, content, true)}`, + backgroundColor: opts.backgroundColor ?? '', + text_fontColor: opts.color, + contentBold: opts.fontBold, + textTransform: opts.fontTransform, + color: opts.color, + _layout_borderRounding: `${opts.cornerRounding}px` ?? '0px', + borderColor: opts.borderColor, + borderWidth: opts.borderWidth, + opacity: opts.opacity, + hCentering: opts.contentXCentering, + _rotation: opts.rotation, + }); + + docWithBasicOpts._layout_hideScroll = true; + + return docWithBasicOpts; + }; + + public static ImageField = (coords: { tl: [number, number]; br: [number, number] }, parentWidth: number, parentHeight: number, title: string, content: string, opts: FieldOpts) => { + const { width, height, coord } = FieldUtils.getDimensions(coords, parentWidth, parentHeight); + + const doc = Docs.Create.ImageDocument(content, { + isDefaultTemplateDoc: true, + _height: height, + _width: width, + title: title, + x: coord.x, + y: coord.y, + _layout_fitWidth: false, + backgroundColor: opts.backgroundColor ?? '', + _layout_borderRounding: `${opts.cornerRounding}px` ?? '0px', + borderColor: opts.borderColor, + borderWidth: opts.borderWidth, + opacity: opts.opacity, + _rotation: opts.rotation, + }); + + //setTimeout(() => {doc._height = height; doc._width = width}, 10); + + return doc; + }; + + public static CarouselField = (coords: { tl: [number, number]; br: [number, number] }, parentWidth: number, parentHeight: number, title: string, fields: Doc[]) => { + const { width, height, coord } = FieldUtils.getDimensions(coords, parentWidth, parentHeight); + + const doc = Docs.Create.Carousel3DDocument(fields, { _height: height, _width: width, title: title, x: coord.x, y: coord.y, _text_fontSize: `${height / 2}` }); + + return doc; + }; +} + +export class TemplateLayouts { + public static get allTemplates(): TemplateDocInfos[] { + return Object.values(TemplateLayouts).filter(value => typeof value === 'object' && value !== null && 'title' in value) as TemplateDocInfos[]; + } + + public static getTemplateByTitle = (title: string): TemplateDocInfos | undefined => { + switch (title) { + case 'fourfield1': + return TemplateLayouts.FourField001; + case 'fourfield2': + return TemplateLayouts.FourField002; + // case 'fourfield3': + // return TemplateLayouts.FourField003; + case 'fourfield4': + return TemplateLayouts.FourField004; + case 'threefield1': + return TemplateLayouts.ThreeField001; + case 'threefield2': + return TemplateLayouts.ThreeField002; + default: + break; + } + + return undefined; + }; + + public static FourField001: TemplateDocInfos = { + title: 'fourfield1', + width: 416, + height: 700, + opts: { + backgroundColor: '#C0B887', + cornerRounding: 20, + borderColor: '#6B461F', + borderWidth: '12', + }, + fields: [ + { + tl: [-0.95, -1], + br: [0.95, -0.85], + types: [TemplateFieldType.TEXT], + sizes: [TemplateFieldSize.TINY], + description: 'A title field for very short text that contextualizes the content.', + opts: { + backgroundColor: 'transparent', + color: '#F1F0E9', + contentXCentering: 'h-center', + fontBold: true, + }, + }, + { + tl: [-0.87, -0.83], + br: [0.87, 0.2], + types: [TemplateFieldType.TEXT, TemplateFieldType.VISUAL], + sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE], + description: 'The main focus of the template; could be an image, long text, etc.', + opts: { + cornerRounding: 20, + borderColor: '#8F5B25', + borderWidth: '6', + backgroundColor: '#CECAB9', + }, + }, + { + tl: [-0.8, 0.2], + br: [0.8, 0.3], + types: [TemplateFieldType.TEXT], + sizes: [TemplateFieldSize.TINY, TemplateFieldSize.SMALL], + description: 'A caption for field #2, very short to short text that contextualizes the content of field #2', + opts: { + backgroundColor: 'transparent', + contentXCentering: 'h-center', + color: '#F1F0E9', + }, + }, + { + tl: [-0.87, 0.37], + br: [0.87, 0.96], + types: [TemplateFieldType.TEXT, TemplateFieldType.VISUAL], + sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE], + description: 'A medium-sized field for medium/long text.', + opts: { + cornerRounding: 15, + borderColor: '#8F5B25', + borderWidth: '6', + backgroundColor: '#CECAB9', + }, + }, + ], + decorations: [], + }; + + public static FourField002: TemplateDocInfos = { + title: 'fourfield2', + width: 425, + height: 778, + opts: { + backgroundColor: '#242425', + }, + fields: [ + { + tl: [-0.83, -0.95], + br: [0.83, -0.2], + types: [TemplateFieldType.VISUAL, TemplateFieldType.TEXT], + sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE], + description: 'A medium to large-sized field suitable for an image or longer text that should be the main focus.', + opts: { + borderWidth: '8', + borderColor: '#F8E71C', + }, + }, + { + tl: [-0.65, -0.2], + br: [0.65, -0.02], + types: [TemplateFieldType.TEXT], + sizes: [TemplateFieldSize.TINY], + description: 'A tiny field for just a word or two of plain text.', + opts: { + backgroundColor: 'transparent', + color: 'white', + contentXCentering: 'h-center', + fontTransform: 'uppercase', + }, + }, + { + tl: [-0.65, 0], + br: [0.65, 0.18], + types: [TemplateFieldType.TEXT], + sizes: [TemplateFieldSize.TINY], + description: 'A tiny field for just a word or two of plain text.', + opts: { + backgroundColor: 'transparent', + color: 'white', + contentXCentering: 'h-center', + fontTransform: 'uppercase', + }, + }, + { + tl: [-0.83, 0.2], + br: [0.83, 0.95], + types: [TemplateFieldType.TEXT, TemplateFieldType.VISUAL], + sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE], + description: 'A medium to large-sized field suitable for an image or longer text that should be the main focus, or share focus with field 1.', + opts: { + borderWidth: '8', + borderColor: '#F8E71C', + color: 'white', + backgroundColor: '#242425', + }, + }, + ], + decorations: [ + { + tl: [-0.8, -0.075], + br: [-0.525, 0.075], + opts: { + backgroundColor: '#F8E71C', + rotation: 45, + }, + }, + { + tl: [-0.3075, -0.0245], + br: [-0.2175, 0.0245], + opts: { + backgroundColor: '#F8E71C', + rotation: 45, + }, + }, + { + tl: [-0.045, -0.0245], + br: [0.045, 0.0245], + opts: { + backgroundColor: '#F8E71C', + rotation: 45, + }, + }, + { + tl: [0.2175, -0.0245], + br: [0.3075, 0.0245], + opts: { + backgroundColor: '#F8E71C', + rotation: 45, + }, + }, + { + tl: [0.525, -0.075], + br: [0.8, 0.075], + opts: { + backgroundColor: '#F8E71C', + rotation: 45, + }, + }, + ], + }; + + // public static FourField003: TemplateDocInfos = { + // title: 'fourfield3', + // width: 477, + // height: 662, + // opts: { + // backgroundColor: '#9E9C95' + // }, + // fields: [{ + // tl: [-.875, -.9], + // br: [.875, .7], + // types: [TemplateFieldType.VISUAL], + // sizes: [TemplateFieldSize.LARGE, TemplateFieldSize.HUGE], + // description: '', + // opts: { + // borderWidth: '15', + // borderColor: '#E0E0DA', + // } + // }, { + // tl: [-.95, .8], + // br: [-.1, .95], + // types: [TemplateFieldType.TEXT], + // sizes: [TemplateFieldSize.TINY, TemplateFieldSize.SMALL], + // description: '', + // opts: { + // backgroundColor: 'transparent', + // color: 'white', + // contentXCentering: 'h-right', + // } + // }, { + // tl: [.1, .8], + // br: [.95, .95], + // types: [TemplateFieldType.TEXT], + // sizes: [TemplateFieldSize.TINY, TemplateFieldSize.SMALL], + // description: '', + // opts: { + // backgroundColor: 'transparent', + // color: 'red', + // fontTransform: 'uppercase', + // contentXCentering: 'h-left' + // } + // }, { + // tl: [0, -.9], + // br: [.85, -.66], + // types: [TemplateFieldType.TEXT, TemplateFieldType.VISUAL], + // sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE], + // description: '', + // opts: { + // backgroundColor: 'transparent', + // contentXCentering: 'h-right' + // } + // }], + // decorations: [{ + // tl: [-.025, .8], + // br: [.025, .95], + // opts: { + // backgroundColor: '#E0E0DA', + // } + // }] + // }; + + public static FourField004: TemplateDocInfos = { + title: 'fourfield4', + width: 414, + height: 583, + opts: { + backgroundColor: '#6CCAF0', + borderColor: '#1088C3', + borderWidth: '10', + }, + fields: [ + { + tl: [-0.86, -0.92], + br: [-0.075, -0.77], + types: [TemplateFieldType.TEXT], + sizes: [TemplateFieldSize.TINY], + description: 'A tiny field for just a word or two of plain text.', + opts: { + backgroundColor: '#E2B4F5', + borderWidth: '9', + borderColor: '#9222F1', + contentXCentering: 'h-center', + }, + }, + { + tl: [0.075, -0.92], + br: [0.86, -0.77], + types: [TemplateFieldType.TEXT], + sizes: [TemplateFieldSize.TINY], + description: 'A tiny field for just a word or two of plain text.', + opts: { + backgroundColor: '#F5B4DD', + borderWidth: '9', + borderColor: '#E260F3', + contentXCentering: 'h-center', + }, + }, + { + tl: [-0.81, -0.64], + br: [0.81, 0.48], + types: [TemplateFieldType.VISUAL], + sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE], + description: 'A large to huge field for visual content that is the main content of the template.', + opts: { + borderWidth: '16', + borderColor: '#A2BD77', + }, + }, + { + tl: [-0.86, 0.6], + br: [0.86, 0.92], + types: [TemplateFieldType.TEXT], + sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE], + description: 'A medium to large field for text that describes the visual content above', + opts: { + borderWidth: '9', + borderColor: '#F0D601', + backgroundColor: '#F3F57D', + }, + }, + ], + decorations: [ + { + tl: [-0.852, -0.67], + br: [0.852, 0.51], + opts: { + backgroundColor: 'transparent', + borderColor: '#007C0C', + borderWidth: '10', + }, + }, + ], + }; + + public static ThreeField001: TemplateDocInfos = { + title: 'threefield1', + width: 575, + height: 770, + opts: { + backgroundColor: '#DDD3A9', + }, + fields: [ + { + tl: [-0.66, -0.747], + br: [0.66, 0.247], + types: [TemplateFieldType.VISUAL], + sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE], + description: 'A medium to large field for visual content that is the central focus.', + opts: { + borderColor: 'yellow', + borderWidth: '8', + rotation: 45, + }, + }, + { + tl: [-0.7, 0.2], + br: [0.7, 0.46], + types: [TemplateFieldType.TEXT], + sizes: [TemplateFieldSize.TINY, TemplateFieldSize.SMALL], + description: 'A very small text field for one to a few words. A good caption for the image.', + opts: { + backgroundColor: 'transparent', + contentXCentering: 'h-center', + }, + }, + { + tl: [-0.95, 0.5], + br: [0.95, 0.95], + types: [TemplateFieldType.TEXT], + sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE], + description: 'A medium to large text field for a thorough description of the image. ', + opts: { + backgroundColor: 'transparent', + color: 'white', + }, + }, + ], + decorations: [ + { + tl: [0.2, -1.32], + br: [1.8, -0.66], + opts: { + backgroundColor: '#CEB155', + rotation: 45, + }, + }, + { + tl: [-1.8, -1.32], + br: [-0.2, -0.66], + opts: { + backgroundColor: '#CEB155', + rotation: 135, + }, + }, + { + tl: [0.33, 0.75], + br: [1.66, 1.25], + opts: { + backgroundColor: '#CEB155', + rotation: 135, + }, + }, + { + tl: [-1.66, 0.75], + br: [-0.33, 1.25], + opts: { + backgroundColor: '#CEB155', + rotation: 45, + }, + }, + ], + }; + + public static ThreeField002: TemplateDocInfos = { + title: 'threefield2', + width: 477, + height: 662, + opts: { + backgroundColor: '#9E9C95', + }, + fields: [ + { + tl: [-0.875, -0.9], + br: [0.875, 0.7], + types: [TemplateFieldType.VISUAL], + sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE], + description: 'A medium to large visual field for the main content of the template', + opts: { + borderWidth: '15', + borderColor: '#E0E0DA', + }, + }, + { + tl: [0.1, 0.775], + br: [0.95, 0.975], + types: [TemplateFieldType.TEXT], + sizes: [TemplateFieldSize.TINY, TemplateFieldSize.SMALL], + description: 'A very small text field for one to a few words. The content should represent a general categorization of the image.', + opts: { + backgroundColor: 'transparent', + color: '#AF0D0D', + fontTransform: 'uppercase', + fontBold: true, + contentXCentering: 'h-left', + }, + }, + { + tl: [-0.95, 0.775], + br: [-0.1, 0.975], + types: [TemplateFieldType.TEXT], + sizes: [TemplateFieldSize.TINY, TemplateFieldSize.SMALL], + description: 'A very small text field for one to a few words. The content should contextualize field 2.', + opts: { + backgroundColor: 'transparent', + color: 'black', + contentXCentering: 'h-right', + }, + }, + ], + decorations: [ + { + tl: [-0.025, 0.8], + br: [0.025, 0.95], + opts: { + backgroundColor: '#E0E0DA', + }, + }, + ], + }; + + // public static FourField002: TemplateDocInfos = { + // width: 450, + // height: 600, + // fields: [{ + // tl: [-.6, -.9], + // br: [.6, -.8], + // types: [FieldType.TEXT], + // sizes: [FieldSize.TINY] + // }, { + // tl: [-.9, -.7], + // br: [.9, .2], + // types: [FieldType.TEXT, FieldType.VISUAL], + // sizes: [FieldSize.MEDIUM, FieldSize.LARGE, FieldSize.HUGE] + // }, { + // tl: [-.9, .3], + // br: [-.05, .9], + // types: [FieldType.TEXT], + // sizes: [FieldSize.TINY] + // }, { + // tl: [.05, .3], + // br: [.9, .9], + // types: [FieldType.TEXT, FieldType.VISUAL], + // sizes: [FieldSize.MEDIUM, FieldSize.LARGE, FieldSize.HUGE] + // }] + // }; + + // public static TwoFieldPlusCarousel: TemplateDocInfos = { + // width: 500, + // height: 600, + // fields: [{ + // tl: [-.9, -.99], + // br: [.9, -.7], + // types: [FieldType.TEXT], + // sizes: [FieldSize.TINY] + // }, { + // tl: [-.9, -.65], + // br: [.9, .35], + // types: [], + // sizes: [] + // }, { + // tl: [-.9, .4], + // br: [.9, .95], + // types: [FieldType.TEXT], + // sizes: [FieldSize.TINY] + // }] + // }; +} + +// export class ContentField extends Field { + +// } diff --git a/src/client/views/nodes/DataVizBox/TemplateDocTypes.tsx b/src/client/views/nodes/DataVizBox/TemplateDocTypes.tsx new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/src/client/views/nodes/DataVizBox/TemplateDocTypes.tsx diff --git a/src/client/views/nodes/DataVizBox/components/TableBox.tsx b/src/client/views/nodes/DataVizBox/components/TableBox.tsx index e57c9e842..6cc773da0 100644 --- a/src/client/views/nodes/DataVizBox/components/TableBox.tsx +++ b/src/client/views/nodes/DataVizBox/components/TableBox.tsx @@ -1,4 +1,6 @@ -import { Button, Type } from 'browndash-components'; +/* eslint-disable jsx-a11y/no-noninteractive-tabindex */ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +import { Button, Colors, Type } from 'browndash-components'; import { IReactionDisposer, action, computed, makeObservable, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; @@ -35,6 +37,7 @@ interface TableBoxProps { left: number; }; docView?: () => DocumentView | undefined; + specHighlightedRow: number | undefined; } @observer @@ -178,7 +181,6 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> { } else { const newAxes = this._props.axes; if (newAxes.includes(col)) newAxes.splice(newAxes.indexOf(col), 1); - else if (newAxes.length > 2) newAxes[newAxes.length - 1] = col; else newAxes.push(col); this._props.selectAxes(newAxes); } @@ -410,11 +412,13 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> { color: this._props.axes.slice().reverse().lastElement() === col ? 'darkgreen' - : this._props.axes.length > 2 && this._props.axes.lastElement() === col + : this._props.axes.length > 3 && this._props.axes.lastElement() === col ? 'darkred' - : this._props.axes.lastElement() === col || (this._props.axes.length > 2 && this._props.axes[1] === col) + : this._props.axes.length > 3 && this._props.axes[1] === col ? 'darkblue' - : undefined, + : this._props.axes.lastElement() === col || (this._props.axes.length > 3 && this._props.axes[2] === col) + ? 'darkcyan' + : undefined, background: this.settingTitle ? 'lightgrey' : this._props.axes.slice().reverse().lastElement() === col @@ -423,7 +427,9 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> { ? '#Fbdbdb' : this._props.axes.lastElement() === col || (this._props.axes.length > 2 && this._props.axes[1] === col) ? '#c6ebf7' - : undefined, + : this._props.axes.lastElement() === col || (this._props.axes.length > 3 && this._props.axes[2] === col) + ? '#c2f0f4' + : undefined, fontWeight: 'bolder', border: '3px solid black', }} @@ -442,7 +448,8 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> { className={`tableBox-row ${this.columns[0]}`} onClick={e => this.tableRowClick(e, rowId)} style={{ - background: NumListCast(this._props.layoutDoc.dataViz_highlitedRows).includes(rowId) ? 'lightYellow' : NumListCast(this._props.layoutDoc.dataViz_selectedRows).includes(rowId) ? 'lightgrey' : '', + background: rowId === this._props.specHighlightedRow ? 'lightblue' : NumListCast(this._props.layoutDoc.dataViz_highlitedRows).includes(rowId) ? 'lightYellow' : NumListCast(this._props.layoutDoc.dataViz_selectedRows).includes(rowId) ? 'lightgrey' : '', + border: rowId === this._props.specHighlightedRow ? `solid 3px ${Colors.MEDIUM_BLUE}` : '' }}> {this.columns.map(col => { let colSelected = false; diff --git a/src/client/views/nodes/DocumentIcon.tsx b/src/client/views/nodes/DocumentIcon.tsx index ffd350e92..0b94ae4f7 100644 --- a/src/client/views/nodes/DocumentIcon.tsx +++ b/src/client/views/nodes/DocumentIcon.tsx @@ -25,19 +25,16 @@ export class DocumentIcon extends ObservableReactComponent<DocumentIconProps> { render() { const { view } = this._props; - const { left, top, right } = view.getBounds || { left: 0, top: 0, right: 0, bottom: 0 }; + const { left, top, right, bottom } = view.getBounds || { left: 0, top: 0, right: 0, bottom: 0 }; return ( <div className="documentIcon-outerDiv" - onPointerEnter={action(() => { this._hovered = true; })} // prettier-ignore - onPointerLeave={action(() => { this._hovered = false; })} // prettier-ignore style={{ pointerEvents: 'all', - opacity: this._hovered ? 0.3 : 1, position: 'absolute', background: SnappingManager.userBackgroundColor, - transform: `translate(${(left + right) / 2}px, ${top}px)`, + transform: `translate(${left}px, ${bottom - (bottom - top)/2}px)`, //**!** }}> <Tooltip title={<div>{StrCast(this._props.view.Document?.title)}</div>}> <p>d{this._props.index}</p> @@ -47,7 +44,7 @@ export class DocumentIcon extends ObservableReactComponent<DocumentIconProps> { } } -@observer +@observer export class DocumentIconContainer extends React.Component { public static getTransformer(): Transformer { const usedDocuments = new Set<number>(); diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss index 23dada260..7568e3b57 100644 --- a/src/client/views/nodes/DocumentView.scss +++ b/src/client/views/nodes/DocumentView.scss @@ -240,6 +240,12 @@ } } +.contentFittingDocumentView * { + ::-webkit-scrollbar-track { + background: none; + } +} + .contentFittingDocumentView { position: relative; display: flex; diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index d730e661b..4a249838b 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -1075,6 +1075,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { public static getViews = (doc?: Doc) => Array.from(doc?.[DocViews] ?? []) as DocumentView[]; public static getFirstDocumentView: (toFind: Doc) => DocumentView | undefined; public static getDocumentView: (target: Doc | undefined, preferredCollection?: DocumentView) => Opt<DocumentView>; + public static getDocViewIndex: (target: Doc) => number; public static getContextPath: (doc: Opt<Doc>, includeExistingViews?: boolean) => Doc[]; public static getLightboxDocumentView: (toFind: Doc) => Opt<DocumentView>; public static showDocumentView: (targetDocView: DocumentView, options: FocusViewOptions) => Promise<void>; @@ -1160,7 +1161,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { return Math.max(minTextScale, this._props.PanelHeight() / (this.effectiveNativeHeight || 1)); // height-limited or unscaled } @computed private get panelWidth() { - return this.effectiveNativeWidth ? this.effectiveNativeWidth * this.nativeScaling : this._props.PanelWidth(); + return this.effectiveNativeWidth ? this.effectiveNativeWidth * this.nativeScaling: this._props.PanelWidth(); } @computed private get panelHeight() { if (this.effectiveNativeHeight && (!this.layout_fitWidth || !this.layoutDoc.layout_reflowVertical)) { @@ -1471,6 +1472,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { render() { TraceMobx(); + const borderWidth = 50/*Number(StrCast(this.layoutDoc.layout_borderWidth).replace('px', ''))*/; const xshift = Math.abs(this.Xshift) <= 0.001 ? this._props.PanelWidth() : undefined; const yshift = Math.abs(this.Yshift) <= 0.001 ? this._props.PanelHeight() : undefined; diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index 683edba16..a0d69d29d 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -139,7 +139,7 @@ export class FieldView extends React.Component<FieldViewProps> { const field = this.fieldval; // prettier-ignore if (field instanceof Doc) return <p> <b>{field.title?.toString()}</b></p>; - if (field === undefined) return <p>{'<null>'}</p>; + if (field === undefined) return <p>{''}</p>; if (field instanceof DateField) return <p>{field.date.toLocaleString()}</p>; if (field instanceof List) return <div> {field.map(f => Field.toString(f)).join(', ')} </div>; if (field instanceof WebField) return <p>{Field.toString(field.url.href)}</p>; diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index ec5e062c8..226fad977 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -124,15 +124,15 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { { fireImmediately: true, delay: 1000 } ); const { layoutDoc } = this; - this._disposers.path = reaction( - () => ({ nativeSize: this.nativeSize, width: NumCast(this.layoutDoc._width) }), - ({ nativeSize, width }) => { - if (layoutDoc === this.layoutDoc || !this.layoutDoc._height) { - this.layoutDoc._height = (width * nativeSize.nativeHeight) / nativeSize.nativeWidth; - } - }, - { fireImmediately: true } - ); + // this._disposers.path = reaction( + // () => ({ nativeSize: this.nativeSize, width: NumCast(this.layoutDoc._width) }), + // ({ nativeSize, width }) => { + // if (layoutDoc === this.layoutDoc || !this.layoutDoc._height) { + // this.layoutDoc._height = (width * nativeSize.nativeHeight) / nativeSize.nativeWidth; + // } + // }, + // { fireImmediately: true } + // ); this._disposers.scroll = reaction( () => this.layoutDoc.layout_scrollTop, sTop => { @@ -306,6 +306,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { @computed get nativeSize() { TraceMobx(); + if (this.paths.length && this.paths[0].includes('icon-hi')) return { nativeWidth: NumCast(this.layoutDoc._width), nativeHeight: NumCast(this.layoutDoc._height), nativeOrientation: 0} const nativeWidth = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth'], NumCast(this.layoutDoc[this.fieldKey + '_nativeWidth'], 500)); const nativeHeight = NumCast(this.dataDoc[this.fieldKey + '_nativeHeight'], NumCast(this.layoutDoc[this.fieldKey + '_nativeHeight'], 500)); const nativeOrientation = NumCast(this.dataDoc[this.fieldKey + '_nativeOrientation'], 1); @@ -352,7 +353,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { } @computed get paths() { - const field = Cast(this.dataDoc[this.fieldKey], ImageField, null); // retrieve the primary image URL that is being rendered from the data doc + const field = this.dataDoc[this.fieldKey] instanceof ImageField ? Cast(this.dataDoc[this.fieldKey], ImageField, null) : new ImageField(String(this.dataDoc[this.fieldKey])); // retrieve the primary image URL that is being rendered from the data doc const alts = DocListCast(this.dataDoc[this.fieldKey + '_alternates']); // retrieve alternate documents that may be rendered as alternate images const defaultUrl = new URL(ClientUtils.prepend('/assets/unknown-file-icon-hi.png')); const altpaths = @@ -405,7 +406,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { alt="" key="paths" src={srcpath} - style={{ transform, transformOrigin }} + style={{ transform, transformOrigin, objectFit: 'fill', height: '100%' }} onError={action(e => { this._error = e.toString(); })} @@ -486,7 +487,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { })} style={{ width: this._props.PanelWidth() ? undefined : `100%`, - height: this._props.PanelWidth() ? undefined : `100%`, + height: this._props.PanelHeight() ? undefined : `100%`, pointerEvents: this.layoutDoc._lockedPosition ? 'none' : undefined, borderRadius, overflow: this.layoutDoc.layout_fitWidth || this._props.fitWidth?.(this.Document) ? 'auto' : undefined, diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx index 8974cccaf..cfcf76b12 100644 --- a/src/client/views/nodes/LabelBox.tsx +++ b/src/client/views/nodes/LabelBox.tsx @@ -2,7 +2,7 @@ import { Property } from 'csstype'; import { action, computed, makeObservable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import * as textfit from 'textfit'; +//import * as textfit from 'textfit'; import { Field, FieldType } from '../../../fields/Doc'; import { BoolCast, NumCast, StrCast } from '../../../fields/Types'; import { TraceMobx } from '../../../fields/util'; @@ -96,7 +96,7 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() { this._timeout = setTimeout(() => this.fitTextToBox(r)); return textfitParams; } - textfit(r, textfitParams); + //textfit(r, textfitParams); } return textfitParams; }; diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss index 99b4a84fc..72d550c7e 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.scss +++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss @@ -14,6 +14,33 @@ } } +.formattedTextBox-inner { + &.h-center * { + display: flex; + justify-content: center; + } + + &.h-left * { + display: flex; + justify-content: flex-start; + } + + &.h-right * { + display: flex; + justify-content: flex-end; + } + + &.template * { + ::-webkit-scrollbar-track { + background: none; + } + } + + &.bold * { + font-weight: bold; + } +} + .ProseMirror:focus { outline: none !important; } @@ -52,6 +79,7 @@ audiotag:hover { transform-origin: left top; top: 0; left: 0; + } .formattedTextBox-cont { @@ -1035,3 +1063,4 @@ footnote::before { } } } + diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index d3bc08bd3..730c57794 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -64,6 +64,7 @@ import { removeMarkWithAttrs } from './prosemirrorPatches'; import { RichTextMenu, RichTextMenuPlugin } from './RichTextMenu'; import { RichTextRules } from './RichTextRules'; import { schema } from './schema_rts'; +import { Property } from 'csstype'; // import * as applyDevTools from 'prosemirror-dev-tools'; export interface FormattedTextBoxProps extends FieldViewProps { @@ -1493,6 +1494,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB pdfAnchorId && this.addPdfReference(pdfAnchorId); } if (this._props.autoFocus) setTimeout(() => this._editorView!.focus()); // not sure why setTimeout is needed but editing dashFieldView's doesn't work without it. + } // add user mark for any first character that was typed since the user mark that gets set in KeyPress won't have been called yet. @@ -2059,7 +2061,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB onScroll={this.onScroll} onDrop={this.ondrop}> <div - className={`formattedTextBox-inner${rounded} ${this.layoutDoc._layout_centered ? 'centered' : ''}`} + className={`formattedTextBox-inner${rounded} ${this.layoutDoc._layout_centered ? 'centered' : ''} ${this.layoutDoc.hCentering}`} ref={this.createDropTarget} style={{ padding: StrCast(this.layoutDoc._textBoxPadding), @@ -2067,6 +2069,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB paddingRight: StrCast(this.layoutDoc._textBoxPaddingX, `${paddingX}px`), paddingTop: StrCast(this.layoutDoc._textBoxPaddingY, `${paddingY}px`), paddingBottom: StrCast(this.layoutDoc._textBoxPaddingY, `${paddingY}px`), + color: StrCast(this.layoutDoc.text_fontColor), + fontWeight: `${this.layoutDoc.contentBold ? 'bold' : ''}`, + textTransform: `${this.layoutDoc.textTransform}` as Property.TextTransform, }} /> </div> @@ -2077,6 +2082,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB </div> </div> ); + } } |
