diff options
Diffstat (limited to 'src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu')
7 files changed, 1007 insertions, 0 deletions
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/ConditionalsTextarea.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/ConditionalsTextarea.tsx new file mode 100644 index 000000000..89c2e44ff --- /dev/null +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/ConditionalsTextarea.tsx @@ -0,0 +1,65 @@ +import { observer } from "mobx-react"; +import { ObservableReactComponent } from "../../../../ObservableReactComponent"; +import { Conditional } from "../Backend/TemplateManager"; +import { action, makeObservable, observable, runInAction } from "mobx"; +import React from "react"; + +interface ConditionalsTextAreaProps { + conditional: Conditional; + property: keyof Conditional; +} + +@observer +export class ConditionalsTextArea extends ObservableReactComponent<ConditionalsTextAreaProps> { + + private mirrorRef: HTMLSpanElement | null = null; + + @observable private inputWidth: string = '60px'; + + constructor(props: ConditionalsTextAreaProps) { + super(props); + makeObservable(this); + } + + setMirrorRef: React.LegacyRef<HTMLSpanElement> = (node) => { this.mirrorRef = node } + + @action updateInputWidth() { + const mirror = this.mirrorRef; + if (mirror) { + const width = mirror.offsetWidth; + if ( width + 8 > 60) this.inputWidth = `${width + 8}px`; + } + } + + render() { + return ( + <div style={{ display: 'inline-block', position: 'relative' }}> + <span + ref={this.setMirrorRef} + style={{ + position: 'absolute', + visibility: 'hidden', + whiteSpace: 'pre', + font: 'inherit', + padding: 0, + }} + > + {this._props.conditional[this._props.property] || ' '} + </span> + <input + className="form-row-input" + value={this.props.conditional[this.props.property] ?? ''} + onChange={e => { + runInAction(() => { + this.props.conditional[this.props.property] = e.target.value as "=" | ">" | "<" | "contains"; + }); + this.updateInputWidth(); + }} + style={{ width: this.inputWidth }} + placeholder={this.props.property} + /> + </div> + ); + } + +}
\ No newline at end of file diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/DocCreatorMenuButton.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/DocCreatorMenuButton.tsx new file mode 100644 index 000000000..48d2de4de --- /dev/null +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/DocCreatorMenuButton.tsx @@ -0,0 +1,42 @@ +import { IconProp } from "@fortawesome/fontawesome-svg-core"; +import { ObservableReactComponent } from "../../../../ObservableReactComponent"; +import React from "react"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { setupMoveUpEvents, returnFalse } from "../../../../../../ClientUtils"; +import { emptyFunction } from "../../../../../../Utils"; +import { undoable } from "../../../../../util/UndoManager"; +import { observer } from "mobx-react"; + +interface DocCreatorMenuButtonProps { + icon: IconProp; + // eslint-disable-next-line + function: () => any; + styles?: string; +} + +@observer +export class DocCreatorMenuButton extends ObservableReactComponent<DocCreatorMenuButtonProps> { + // eslint-disable-next-line + setupButtonClick = (e: React.PointerEvent, func: (...args: any) => void) => { + setupMoveUpEvents( + this, + e, + returnFalse, + emptyFunction, + undoable(clickEv => { + clickEv.stopPropagation(); + clickEv.preventDefault(); + func(); + }, 'create docs') + ); + }; + + render() { + + return ( + <button className={`docCreatorMenu-menu-button ${this._props.styles}`} onPointerDown={e => this.setupButtonClick(e, async () => this._props.function())}> + <FontAwesomeIcon icon={this._props.icon} /> + </button> + ); + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplateEditingWindow.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplateEditingWindow.tsx new file mode 100644 index 000000000..c35099e82 --- /dev/null +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplateEditingWindow.tsx @@ -0,0 +1,220 @@ +import { action, makeObservable, observable, reaction } from 'mobx'; +import React from 'react'; +import { returnFalse, returnEmptyFilter } from '../../../../../../ClientUtils'; +import { emptyFunction } from '../../../../../../Utils'; +import { returnEmptyDoclist } from '../../../../../../fields/Doc'; +import { DefaultStyleProvider } from '../../../../StyleProvider'; +import { DocumentView, DocumentViewInternal } from '../../../DocumentView'; +import { DocCreatorMenu } from '../DocCreatorMenu'; +import { TemplatePreviewGrid } from './TemplatePreviewGrid'; +import { observer } from 'mobx-react'; +import { Transform } from '../../../../../util/Transform'; +import { Template } from '../Template'; +import { ObservableReactComponent } from '../../../../ObservableReactComponent'; +import { IDisposer } from 'mobx-utils'; +import { DocCreatorMenuButton } from './DocCreatorMenuButton'; +import { IconProp } from '@fortawesome/fontawesome-svg-core'; + +export type FireflyStructureOptions = { + numVariations: number; + temperature: number; + useStyleRef: boolean; +}; + +interface FireflyVariationsTabProps { + menu: DocCreatorMenu; + template: Template; +} + +@observer +export class FireflyVariationsTab extends ObservableReactComponent<FireflyVariationsTabProps> { + private _prompt: string = 'Use this template to generate an empty baseball card template.'; + private _optionsButtonOpts: [IconProp, () => void] = ['gear', emptyFunction]; + private _previewBoxRightButtonOpts: [IconProp, () => void] = ['gear', () => this.forceUpdate()]; + + @observable _fireflyOptions: FireflyStructureOptions = { numVariations: 3, temperature: 0, useStyleRef: false }; + @observable _promptInput: HTMLTextAreaElement | null = null; + @observable _loading: boolean = false; + @observable _variationsTabOpen: boolean = false; + @observable _variationURLs: string[] = []; + + constructor(props: FireflyVariationsTabProps) { + super(props); + makeObservable(this); + } + + generateVariations = action(async () => { + this._props.menu._variations = []; + this._loading = true; + const cloneTemplate = this._props.template.clone(false); + cloneTemplate.setMatteBackground(); + const doc = cloneTemplate.getRenderedDoc()!; + this._props.menu.generateVariations(doc, this._prompt, this._fireflyOptions).then( + action((urls: string[]) => { + (this._variationURLs = urls).forEach(url => { + const template = this._props.template.clone(true); + template.setImageAsBackground(url, true); + this._props.menu._variations.push(template); + }); + this._loading = false; + }) + ); + }); + + render() { + return ( + <div className="docCreatorMenu-editing-firefly-section"> + <div className="docCreatorMenu-option-divider full no-margin-bottom" /> + <TemplatePreviewGrid + menu={this._props.menu} + title="Generate Variations" + loading={this._loading} + styles="scrolling" + templates={this._props.menu._variations} + optionsButtonOpts={this._optionsButtonOpts} + previewBoxRightButtonOpts={this._previewBoxRightButtonOpts} + /> + <div className="docCreatorMenu-firefly-options"> + <div className="docCreatorMenu-variation-prompt-row"> + <textarea + className="docCreatorMenu-variation-prompt-input-textbox" + ref={action((node: HTMLTextAreaElement | null) => (this._promptInput = node))} + onChange={e => (this._prompt = e.target.value)} + onInput={() => { + if (this._promptInput !== null) { + this._promptInput.style.height = 'auto'; + this._promptInput.style.height = this._promptInput.scrollHeight + 'px'; + } + }} + defaultValue="" + placeholder="Enter a custom prompt here (optional)" + /> + <DocCreatorMenuButton icon={'arrows-rotate'} styles={'border'} function={this.generateVariations} /> + </div> + <nav className="options‑menu"> + <label className="menu‑item switch"> + <input type="checkbox" checked={this._fireflyOptions.useStyleRef} onChange={action(e => (this._fireflyOptions.useStyleRef = e.target.checked))} /> + <span className="slider round"></span> + <span className="firefly-option-label">Use template as style guide</span> + </label> + <div className="menu‑item"> + <span className="firefly-option-label">Variations</span> + <input type="range" id="variations" min="1" max="5" value={this._fireflyOptions.numVariations} onChange={action(e => (this._fireflyOptions.numVariations = Number(e.target.value)))} /> + <span className="value" id="varVal"> + {this._fireflyOptions.numVariations} + </span> + </div> + <div className="menu‑item"> + <span className="firefly-option-label">Temperature</span> + <input type="range" id="temperature" min="1" max="100" value={this._fireflyOptions.temperature} onChange={action(e => (this._fireflyOptions.temperature = Number(e.target.value)))} /> + <span className="value" id="tempVal"> + {this._fireflyOptions.temperature} + </span> + </div> + </nav> + </div> + </div> + ); + } +} + +interface TemplateEditingWindowProps { + menu: DocCreatorMenu; + template: Template; +} + +@observer +export class TemplateEditingWindow extends ObservableReactComponent<TemplateEditingWindowProps> { + private disposers: { [name: string]: IDisposer } = {}; + + @observable private _previewWindow: HTMLDivElement | null = null; + @observable _variationsTabOpen: boolean = false; + + constructor(props: TemplateEditingWindowProps) { + super(props); + makeObservable(this); + } + + componentDidMount(): void { + this.disposers.windowDimensions = reaction( + () => this._props.menu._resizing, + () => this.forceUpdate(), + { fireImmediately: true } + ); + } + + componentWillUnmount() { + Object.values(this.disposers).forEach(disposer => disposer?.()); + } + + @action setVariationTab = (open: boolean) => { + this._variationsTabOpen = open; + if (this._previewWindow && open) { + this._previewWindow.style.height = String(Number(this._previewWindow.clientHeight) * 0.6); + } else if (this._previewWindow && !open) { + this._previewWindow.style.height = String((Number(this._previewWindow.clientHeight) * 5) / 3); + } + }; + + previewPanelWidth = () => this._previewWindow?.clientWidth ?? 500; + previewPanelHeight = () => this._previewWindow?.clientHeight ?? 500; + previewScreenToLocalXf = () => new Transform(-this._props.menu._pageX - 5, -this._props.menu._pageY - 35, 1); + get renderedDocPreview() { + const doc = this._props.template.getRenderedDoc(); + return ( + <div className="docCreatorMenu-expanded-template-preview" ref={action((node: HTMLDivElement | null) => (this._previewWindow = node))}> + {this._previewWindow && doc ? ( + <DocumentView + Document={doc} + isContentActive={emptyFunction} + addDocument={returnFalse} + moveDocument={returnFalse} + removeDocument={returnFalse} + PanelWidth={this.previewPanelWidth} + PanelHeight={this.previewPanelHeight} + ScreenToLocalTransform={this.previewScreenToLocalXf} + renderDepth={5} + whenChildContentsActiveChanged={emptyFunction} + focus={emptyFunction} + styleProvider={DefaultStyleProvider} + addDocTab={DocumentViewInternal.addDocTabFunc} + pinToPres={emptyFunction} + childFilters={returnEmptyFilter} + childFiltersByRanges={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} + // fitContentsToBox={returnFalse} + // fitWidth={returnFalse} + /> + ) : null} + </div> + ); + } + + expandFunc = () => { + // if (this._props.template === this._props.menu._selectedTemplate) { + // this._props.menu.updateRenderedPreviewCollection(this._props.template); + // } + this._props.menu.setExpandedView(undefined); + }; + lastFunc = () => { + this._props.menu.editLastTemplate(); + this.forceUpdate(); + }; + variationFunc = () => this.setVariationTab(!this._variationsTabOpen); + render() { + return ( + <div className="docCreatorMenu-templates-view"> + <div className="docCreatorMenu-expanded-template-preview"> + <div className="top-panel" /> + {this.renderedDocPreview} + {this._variationsTabOpen ? <FireflyVariationsTab menu={this._props.menu} template={this._props.template} /> : null} + <div className="right-buttons-panel"> + <DocCreatorMenuButton icon="minimize" function={this.expandFunc} /> + <DocCreatorMenuButton icon="lightbulb" function={this.variationFunc} /> + <DocCreatorMenuButton icon="arrow-rotate-backward" function={this.lastFunc} /> + </div> + </div> + </div> + ); + } +} diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplateMenuFieldOptions.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplateMenuFieldOptions.tsx new file mode 100644 index 000000000..f0e20837c --- /dev/null +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplateMenuFieldOptions.tsx @@ -0,0 +1,185 @@ +import { action, makeObservable, observable } from 'mobx'; +import { observer } from 'mobx-react'; +import { ObservableReactComponent } from '../../../../ObservableReactComponent'; +import { Col, DocCreatorMenu } from '../DocCreatorMenu'; +import React from 'react'; +import { Conditional, TemplateManager } from '../Backend/TemplateManager'; +import { TemplateFieldType, TemplateFieldSize } from '../TemplateBackend'; +import { DocCreatorMenuButton } from './DocCreatorMenuButton'; + +interface TemplateMenuFieldOptionsProps { + menu: DocCreatorMenu; + templateManager: TemplateManager; +} + +@observer +export class TemplateMenuFieldOptions extends ObservableReactComponent<TemplateMenuFieldOptionsProps> { + @observable _collapsedCols: string[] = []; //any columns whose options panels are hidden + + constructor(props: TemplateMenuFieldOptionsProps) { + super(props); + makeObservable(this); + } + + @observable private _newCondCache: Record<string, Conditional> = {}; + + getParams = (title: string, parameters?: Conditional): Conditional => { + if (parameters) return parameters; + + if (!this._newCondCache[title]) { + this._newCondCache[title] = observable<Conditional>({ + field: title, + operator: '=', + condition: '', + target: 'Own', + attribute: '', + value: '', + }); + } + return this._newCondCache[title]; + }; + + conditionForm = (title: string, parameters?: Conditional, empty: boolean = false) => { + const contentFieldTitles = this._props.menu.fieldsInfos + .filter(field => field.type !== TemplateFieldType.DATA) + .map(field => field.title) + .concat('Template'); + const params: Conditional = this.getParams(title, parameters); + + return ( + <div className="form"> + <div className="form-row"> + <div className="form-row-plain-text">If</div> + <div className="form-row-plain-text">{title}</div> + <div className="operator-options-dropdown"> + <span className="operator-dropdown-current">{params.operator ?? '='}</span> + <div className="operator-dropdown-option" onPointerDown={() => (params.operator = '=')}> + {'='} + </div> + </div> + <input className="form-row-textarea" onChange={action(e => (params.condition = e.target.value))} placeholder="value" value={params.condition} /> + <div className="form-row-plain-text">then</div> + <div className="operator-options-dropdown"> + <span className="operator-dropdown-current">{params.target ?? 'Own'}</span> + {contentFieldTitles.map((fieldTitle, i) => ( + <div className="operator-dropdown-option" key={i} onPointerDown={() => (params.target = fieldTitle)}> + {fieldTitle === title ? 'Own' : fieldTitle} + </div> + ))} + </div> + <input className="form-row-textarea" onChange={action(e => (params.attribute = e.target.value))} placeholder="attribute" value={params.attribute} /> + <div className="form-row-plain-text">{'becomes'}</div> + <input className="form-row-textarea" onChange={action(e => (params.value = e.target.value))} placeholder="value" value={params.value} /> + </div> + {empty ? ( + <DocCreatorMenuButton + icon={'plus'} + styles={'float-right border'} + function={() => { + this._newCondCache[title] = observable<Conditional>({ + field: title, + operator: '=', + condition: '', + target: 'Own', + attribute: '', + value: '', + }); + this._props.templateManager.addFieldCondition(title, params); + }} + /> + ) : ( + <DocCreatorMenuButton icon={'minus'} styles={'float-right border'} function={() => this._props.templateManager.removeFieldCondition(title, params)} /> + )} + </div> + ); + }; + + fieldPanel = (field: Col, id: number) => ( + <div className="field-panel" key={id}> + <div + className="top-bar" + onPointerDown={e => + this._props.menu.setUpButtonClick( + e, + action(() => { + if (this._collapsedCols.includes(field.title)) { + this._collapsedCols = this._collapsedCols.filter(col => col !== field.title); + } else { + this._collapsedCols.push(field.title); + } + }) + ) + }> + <span className="field-title">{`${field.title} Field`}</span> + <DocCreatorMenuButton icon={'minus'} styles={'no-margin absolute-right'} function={() => this._props.menu.removeField(field)} /> + </div> + {this._collapsedCols.includes(field.title) ? null : ( + <> + <div className="opts-bar"> + <div className="opt-box"> + <div className="top-bar"> Title </div> + <textarea className="content" style={{ width: '100%', height: 'calc(100% - 20px)' }} value={field.title} placeholder={'Enter title'} onChange={e => this._props.menu.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' : field.type === TemplateFieldType.DATA ? 'Data Field' : ''}</span> + <div className="bubbles"> + <input className="bubble" type="radio" name="type" onClick={() => this._props.menu.setColType(field, TemplateFieldType.TEXT)} /> + <div className="text">Text</div> + <input className="bubble" type="radio" name="type" onClick={() => this._props.menu.setColType(field, TemplateFieldType.VISUAL)} /> + <div className="text">File</div> + <input className="bubble" type="radio" name="type" onClick={() => this._props.menu.setColType(field, TemplateFieldType.DATA)} /> + <div className="text">Data</div> + </div> + </div> + </div> + </div> + {field.type === TemplateFieldType.DATA ? null : ( + <> + <div className="sizes-box"> + <div className="top-bar"> Valid Sizes </div> + <div className="content"> + <div className="bubbles"> + {Object.values(TemplateFieldSize).map(size => ( + <div key={field + size}> + <input className="bubble" type="checkbox" name="type" checked={field.sizes.includes(size)} onChange={e => this._props.menu.modifyColSizes(field, size, e.target.checked)} /> + <div className="text">{size}</div> + </div> + ))} + </div> + </div> + </div> + <div className="desc-box"> + <div className="top-bar"> Prompt </div> + <textarea + className="content" + onChange={e => this._props.menu.setColDesc(field, e.target.value)} + defaultValue={field.desc === this._props.menu._dataViz?.GPTSummary?.get(field.title)?.desc ? '' : field.desc} + placeholder={this._props.menu._dataViz?.GPTSummary?.get(field.title)?.desc ?? 'Add a description/prompt to help with template generation.'} + /> + </div> + </> + )} + <div className="conditionals-section"> + <span className="conditionals-title">Conditional Logic</span> + {this.conditionForm(field.title, undefined, true)} + {this._props.templateManager._conditionalFieldLogic[field.title]?.map(condition => this.conditionForm(condition.field, condition))} + </div> + </> + )} + </div> + ); + + render() { + return ( + <div className="docCreatorMenu-dashboard-view"> + <div className="topbar"> + <DocCreatorMenuButton icon="plus" function={this._props.menu.addField} /> + <DocCreatorMenuButton icon="arrow-left" styles="float-right" function={action(() => (this._props.menu._menuContent = 'templates'))} /> + </div> + <div className="panels-container">{this._props.menu.fieldsInfos.map((field, i) => this.fieldPanel(field, i))}</div> + </div> + ); + } +} diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplatePreviewBox.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplatePreviewBox.tsx new file mode 100644 index 000000000..7d02fff12 --- /dev/null +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplatePreviewBox.tsx @@ -0,0 +1,89 @@ +import { Colors } from '@dash/components/src'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Template } from '../Template'; +import { action, makeObservable, observable } from 'mobx'; +import React from 'react'; +import { ObservableReactComponent } from '../../../../ObservableReactComponent'; +import { DocCreatorMenu } from '../DocCreatorMenu'; +import { IconProp } from '@fortawesome/fontawesome-svg-core'; +import { DocumentView } from '../../../DocumentView'; +import { emptyFunction } from '../../../../../../Utils'; +import { returnEmptyFilter, returnFalse } from '../../../../../../ClientUtils'; +import { Transform } from '../../../../../util/Transform'; +import { DefaultStyleProvider } from '../../../../StyleProvider'; +import { Doc, returnEmptyDoclist } from '../../../../../../fields/Doc'; +import { observer } from 'mobx-react'; + +export interface TemplatePreviewBoxProps { + template: Template; + menu: DocCreatorMenu; // eslint-disable-next-line + leftButtonOpts?: [icon: IconProp, func: (...args: any) => void]; // eslint-disable-next-line + rightButtonOpts?: [icon: IconProp, func: (...args: any) => void]; +} + +@observer +export class TemplatePreviewBox extends ObservableReactComponent<TemplatePreviewBoxProps> { + @observable private previewWindow: HTMLDivElement | null = null; + + constructor(props: TemplatePreviewBoxProps) { + super(props); + makeObservable(this); + } + + get doc() { + return this._props.template.getRenderedDoc() as Doc; + } + + docPanelWidth = () => this.previewWindow?.clientWidth ?? this._props.menu._menuDimensions.height * 0.3; + docPanelHeight = () => this.previewWindow?.clientHeight ?? this._props.menu._menuDimensions.height * 0.3; + docScreenToLocalXf = () => new Transform(-this._props.menu._pageX - 5, -this._props.menu._pageY - 35, 1); + + render() { + const template = this._props.template; + + return ( + <div + key={template.title} + className="docCreatorMenu-preview-window" + ref={action((node: HTMLDivElement | null) => (this.previewWindow = node))} + style={{ + border: this._props.menu._selectedTemplate === template ? `solid 3px ${Colors.MEDIUM_BLUE}` : '', + boxShadow: this._props.menu._selectedTemplate === template ? `0 0 15px rgba(68, 118, 247, .8)` : '', + }} + onPointerDown={e => this._props.menu.setUpButtonClick(e, () => this._props.menu.updateSelectedTemplate(template))}> + {this._props.leftButtonOpts ? ( + <button className="option-button left" onPointerDown={e => this._props.menu.setUpButtonClick(e, () => this._props.leftButtonOpts)}> + <FontAwesomeIcon icon={this._props.leftButtonOpts![0]} color="white" /> + </button> + ) : null} + {this._props.rightButtonOpts ? ( + <button className="option-button right" onPointerDown={e => this._props.menu.setUpButtonClick(e, () => this._props.rightButtonOpts)}> + <FontAwesomeIcon icon={this._props.rightButtonOpts![0]} color="white" /> + </button> + ) : null} + <DocumentView + Document={this.doc} + isContentActive={emptyFunction} // !!! should be return false + addDocument={returnFalse} + moveDocument={returnFalse} + removeDocument={returnFalse} + PanelWidth={this.docPanelWidth} + PanelHeight={this.docPanelHeight} + ScreenToLocalTransform={this.docScreenToLocalXf} + renderDepth={1} + whenChildContentsActiveChanged={emptyFunction} + focus={emptyFunction} + styleProvider={DefaultStyleProvider} + addDocTab={this._props.menu._props.addDocTab} + pinToPres={emptyFunction} + childFilters={returnEmptyFilter} + childFiltersByRanges={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} + // fitContentsToBox={returnFalse} + // fitWidth={returnFalse} + hideDecorations={true} + /> + </div> + ); + } +} diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplatePreviewGrid.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplatePreviewGrid.tsx new file mode 100644 index 000000000..da4851f84 --- /dev/null +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplatePreviewGrid.tsx @@ -0,0 +1,60 @@ +import { makeObservable, runInAction } from "mobx"; +import React from "react"; +import ReactLoading from "react-loading"; +import { Doc } from "../../../../../../fields/Doc"; +import { StrCast } from "../../../../../../fields/Types"; +import { ObservableReactComponent } from "../../../../ObservableReactComponent"; +import { Template } from "../Template"; +import { observer } from "mobx-react"; +import { DocCreatorMenu } from "../DocCreatorMenu"; +import { TemplatePreviewBox } from "./TemplatePreviewBox"; +import { IconProp } from "@fortawesome/fontawesome-svg-core"; +import { DocCreatorMenuButton } from "./DocCreatorMenuButton"; + +export interface SuggestedTemplatesProps { + menu: DocCreatorMenu; + loading?: boolean; + templates: Template[]; + title: string; + styles?: string; // eslint-disable-next-line + optionsButtonOpts?: [IconProp, (...args: any) => any]; // eslint-disable-next-line + previewBoxLeftButtonOpts?: [IconProp, (...args: any) => any]; // eslint-disable-next-line + previewBoxRightButtonOpts?: [IconProp, (...args: any) => any]; +} + +@observer +export class TemplatePreviewGrid extends ObservableReactComponent<SuggestedTemplatesProps> { + + constructor(props: SuggestedTemplatesProps) { + super(props); + makeObservable(this); + } + + render() { + return ( + <div className="docCreatorMenu-section"> + <div className="docCreatorMenu-section-topbar"> + <div className="docCreatorMenu-section-title">{this.props.title}</div> + {this._props.optionsButtonOpts ? + <DocCreatorMenuButton icon={this._props.optionsButtonOpts[0] as IconProp} styles={'float-right'} function={() => runInAction(this._props.optionsButtonOpts![1])}/> + : null} + </div> + <div className={"docCreatorMenu-templates-preview-window " + this._props.styles}> + {this._props.loading ? + (<div className="loading-spinner"> + <ReactLoading type="spin" color={StrCast(Doc.UserDoc().userVariantColor)} height={30} width={30} /> + </div>) + : this.props.templates.map((template, i) => ( + <TemplatePreviewBox + key={i} + template={template} + menu={this.props.menu} + leftButtonOpts={["magnifying-glass", (template: Template) => { this.props.menu.setExpandedView(template); this.forceUpdate(); }]} + rightButtonOpts={this._props.previewBoxRightButtonOpts} + /> + ))} + </div> + </div> + ); + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplateRenderPreviewWindow.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplateRenderPreviewWindow.tsx new file mode 100644 index 000000000..9222d7349 --- /dev/null +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplateRenderPreviewWindow.tsx @@ -0,0 +1,346 @@ +import { action, computed, makeObservable, observable, runInAction } from 'mobx'; +import { observer } from 'mobx-react'; +import { ObservableReactComponent } from '../../../../ObservableReactComponent'; +import { DocCreatorMenu, LayoutType } from '../DocCreatorMenu'; +import React from 'react'; +import { IconProp } from '@fortawesome/fontawesome-svg-core'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { setupMoveUpEvents, returnFalse, returnEmptyFilter } from '../../../../../../ClientUtils'; +import { emptyFunction } from '../../../../../../Utils'; +import { undoable } from '../../../../../util/UndoManager'; +import ReactLoading from 'react-loading'; +import { Doc, NumListCast, returnEmptyDoclist } from '../../../../../../fields/Doc'; +import { NumCast, StrCast } from '../../../../../../fields/Types'; +import { DefaultStyleProvider } from '../../../../StyleProvider'; +import { DocumentView } from '../../../DocumentView'; +import { Transform } from '../../../../../util/Transform'; +import { Docs, DocumentOptions } from '../../../../../documents/Documents'; + +interface TemplatesRenderPreviewWindowProps { + menu: DocCreatorMenu; +} + +@observer +export class TemplatesRenderPreviewWindow extends ObservableReactComponent<TemplatesRenderPreviewWindowProps> { + @observable private _layout: { type: LayoutType; yMargin: number; xMargin: number; columns?: number; repeat: number } = { type: LayoutType.FREEFORM, yMargin: 10, xMargin: 10, columns: 1, repeat: 0 }; + + @observable private renderedDocs: Doc[] = []; + @observable private renderedDocCollection: Doc | undefined = undefined; + + @observable private loading: boolean = false; + + constructor(props: TemplatesRenderPreviewWindowProps) { + super(props); + makeObservable(this); + this.updateRenderedPreviewCollection(); + } + + @computed get canMakeDocs() { + return this._props.menu._selectedTemplate !== undefined && this._layout !== undefined; + } + + @computed get docsToRender() { + if (this._props.menu.DEBUG_MODE) { + return [1, 2, 3, 4]; + } else { + return NumListCast(this._props.menu._dataViz?.layoutDoc.dataViz_selectedRows); + } + } + + @computed get rowsCount() { + switch (this._layout.type) { + case LayoutType.FREEFORM: + return Math.ceil(this.docsToRender.length / (this._layout.columns ?? 1)) ?? 0; + case LayoutType.CAROUSEL3D: + return 1.8; + default: + return 1; + } + } + + @computed get columnsCount() { + switch (this._layout.type) { + case LayoutType.FREEFORM: + return this._layout.columns ?? 1; + case LayoutType.CAROUSEL3D: + return 3; + default: + return 1; + } + } + + @action updateRenderedPreviewCollection = async () => { + this.loading = true; + this.renderedDocs = await this._props.menu.createDocsForPreview(); + this.updateRenderedDocCollection(); + }; + + /** + * Updates the preview that shows how all docs will be rendered in the chosen collection type. + @type the type of collection the docs should render to (ie. freeform, carousel, card) + */ + updateRenderedDocCollection = () => { + if (!this.renderedDocs) return; + + const collectionFactory = (): ((docs: Doc[], options: DocumentOptions) => Doc) => { + switch (this._layout.type) { + case LayoutType.CAROUSEL3D: return Docs.Create.Carousel3DDocument; + case LayoutType.FREEFORM: return Docs.Create.FreeformDocument; + case LayoutType.CARD: return Docs.Create.CardDeckDocument; + case LayoutType.MASONRY: return Docs.Create.MasonryDocument; + case LayoutType.CAROUSEL: return Docs.Create.CarouselDocument; + default: return Docs.Create.FreeformDocument; + } // prettier-ignore + }; + + const collection = collectionFactory()(this.renderedDocs, { + isDefaultTemplateDoc: true, + title: 'title', + backgroundColor: 'gray', + x: 200, + y: 200, + _width: 4000, + _height: 4000, + }); + + this.applyLayout(collection, this.renderedDocs); + + this.renderedDocCollection = collection; + + this.loading = false; + + this.forceUpdate(); + }; + + @action updateMargin = (input: string, xOrY: 'x' | 'y') => { + this._layout[`${xOrY}Margin`] = Number(input); + setTimeout(() => { + if (!this.renderedDocCollection || !this.renderedDocs) return; + this.applyLayout(this.renderedDocCollection, this.renderedDocs); + }); + }; + + @action updateColumns = (input: string) => { + this._layout.columns = Number(input); + this.updateRenderedDocCollection(); + }; + + applyLayout = (collection: Doc, docs: Doc[]) => { + const { horizontalSpan, verticalSpan } = this.previewInfo; + collection._height = verticalSpan; + collection._width = horizontalSpan; + collection.layout_fitWidth = true; + collection.freeform_fitContentsToBox = true; + + const columns = (this._layout.columns ?? this.columnsCount) || 1; + const xGap = this._layout.xMargin; + const yGap = this._layout.yMargin; + const startX = -collection._width / 2; + const startY = -collection._height / 2; + const docHeight = NumCast(docs[0]?._height); + const docWidth = NumCast(docs[0]?._width); + + let i = 0; + let docsChanged = 0; + let curX = startX; + let curY = startY; + + while (docsChanged < docs.length) { + while (i < columns && docsChanged < docs.length) { + docs[docsChanged].x = curX; + docs[docsChanged].y = curY; + docs[docsChanged].layout_fitWidth = false; + curX += docWidth + xGap; + ++docsChanged; + ++i; + } + i = 0; + curX = startX; + curY += docHeight + yGap; + } + }; + + @computed + get previewInfo() { + const docHeight = NumCast(this.renderedDocs[0]?._height); + const docWidth = NumCast(this.renderedDocs[0]?._width); + const layout = this._layout; + return { + docHeight: docHeight, + docWidth: docWidth, + horizontalSpan: (docWidth + layout.xMargin) * this.columnsCount - layout.xMargin, + verticalSpan: (docHeight + layout.yMargin) * this.rowsCount - layout.yMargin, + }; + } + + get layoutConfigOptions() { + const optionInput = (icon: string, func: (input: string) => void, 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 IconProp} /> + </div> + <input defaultValue={def} onInput={e => func(e.currentTarget.value)} className="docCreatorMenu-input config layout-config" /> + </div> + ); + }; + + switch (this._layout.type) { + case LayoutType.FREEFORM: + return ( + <div className="docCreatorMenu-configuration-bar"> + {optionInput('arrows-up-down', (input: string) => this.updateMargin(input, 'y'), this._layout.xMargin, '2')} + {optionInput('arrows-left-right', (input: string) => this.updateMargin(input, 'x'), this._layout.xMargin, '3')} + {optionInput('table-columns', this.updateColumns, this._layout.columns, '4', true)} + </div> + ); + default: + break; + } + } + + layoutPanelWidth = () => this._props.menu._menuDimensions.width - 80; + layoutPanelHeight = () => this._props.menu._menuDimensions.height - 105; + layoutScreenToLocalXf = () => new Transform(-this._props.menu._pageX - 5, -this._props.menu._pageY - 35, 1); + + layoutPreviewContents = action(() => { + return this.loading ? ( + <div className="docCreatorMenu-layout-preview-window-wrapper loading"> + <div className="loading-spinner"> + <ReactLoading type="spin" color={StrCast(Doc.UserDoc().userVariantColor)} height={30} width={30} /> + </div> + </div> + ) : !this.renderedDocCollection ? null : ( + <div className="docCreatorMenu-layout-preview-window-wrapper"> + <DocumentView + Document={this.renderedDocCollection} + isContentActive={emptyFunction} + addDocument={returnFalse} + moveDocument={returnFalse} + removeDocument={returnFalse} + PanelWidth={this.layoutPanelWidth} + PanelHeight={this.layoutPanelHeight} + ScreenToLocalTransform={this.layoutScreenToLocalXf} + renderDepth={5} + whenChildContentsActiveChanged={emptyFunction} + focus={emptyFunction} + styleProvider={DefaultStyleProvider} + addDocTab={this._props.menu._props.addDocTab} + pinToPres={emptyFunction} + childFilters={returnEmptyFilter} + childFiltersByRanges={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} + hideDecorations={true} + /> + </div> + ); + }); + + 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 IconProp} /> + </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> + ); + }; + + layoutOption = (option: LayoutType, optStyle?: object, specialFunc?: () => void) => { + return ( + <div + className="docCreatorMenu-dropdown-option" + style={optStyle} + onPointerDown={e => + this._props.menu.setUpButtonClick(e, () => { + specialFunc?.(); + runInAction(() => { + this._layout.type = option; + this.updateRenderedDocCollection(); + }); + }) + }> + {option} + </div> + ); + }; + + get optionsMenuContents() { + 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"> + {this.layoutOption(LayoutType.FREEFORM, undefined, () => { + if (!this._layout.columns) this._layout.columns = Math.ceil(Math.sqrt(this.docsToRender.length)); + })} + {this.layoutOption(LayoutType.CAROUSEL)} + {this.layoutOption(LayoutType.CAROUSEL3D)} + {this.layoutOption(LayoutType.MASONRY)} + </div> + </div> + </div> + {this._layout.type ? this.layoutConfigOptions : null} + {this.layoutPreviewContents()} + {this.selectionBox( + 60, + 20, + 'repeat', + undefined, + repeatOptions.map(num => <option key={num} onPointerDown={() => (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(); + //previous implementation deprecated; return later to add or scrap + return; + }, 'save layout') + ) + }> + <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(); + this.renderedDocCollection && this._props.menu.addRenderedCollectionToMainview(this.renderedDocCollection); + }, 'make docs') + ) + }> + <FontAwesomeIcon icon="plus" /> + </button> + </div> + </div> + ); + } + + render() { + return this.optionsMenuContents; + } +} |
