diff options
Diffstat (limited to 'src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu')
7 files changed, 1045 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..2ca0bde3f --- /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 any; + }); + 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..1d8139d40 --- /dev/null +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/DocCreatorMenuButton.tsx @@ -0,0 +1,41 @@ +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; + function: () => any; + styles?: string; +} + +@observer +export class DocCreatorMenuButton extends ObservableReactComponent<DocCreatorMenuButtonProps> { + + 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..3eaed79b6 --- /dev/null +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplateEditingWindow.tsx @@ -0,0 +1,242 @@ +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { action, makeAutoObservable, makeObservable, observable, reaction, runInAction } from "mobx"; +import React from "react"; +import { returnFalse, returnEmptyFilter, returnTrue } from "../../../../../../ClientUtils"; +import { emptyFunction } from "../../../../../../Utils"; +import { Doc, 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 { TemplateMenuAIUtils } from "../Backend/TemplateMenuAIUtils"; +import { ObservableReactComponent } from "../../../../ObservableReactComponent"; +import { IDisposer } from "mobx-utils"; +import { ImageField } from "../../../../../../fields/URLField"; +import { DocCreatorMenuButton } from "./DocCreatorMenuButton"; +import { TbHistory } from "react-icons/tb"; +import { IconProp } from "@fortawesome/fontawesome-svg-core"; +import { docStyle } from "pdfjs-dist/types/web/ui_utils"; + +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.'; + + @observable private promptInput: HTMLTextAreaElement | null = null; + + @observable _loading: boolean = false; + @observable _variationsTabOpen: boolean = false; + @observable _variationURLs: string[] = []; + + @observable private fireflyOptions: FireflyStructureOptions = {numVariations: 3, temperature: 0, useStyleRef: false}; + + constructor(props: FireflyVariationsTabProps) { + super(props); + makeObservable(this); + } + + generateVariations = async () => { + this._props.menu._variations = []; + this._loading = true; + const cloneTemplate = this._props.template.clone(false); + cloneTemplate.setMatteBackground(); + const doc: Doc = cloneTemplate.getRenderedDoc()!; + this._variationURLs = await this._props.menu.generateVariations(doc, this.prompt, this.fireflyOptions); + this._variationURLs.forEach(url => { + const newTemplate: Template = this._props.template.clone(true); + this._props.menu._variations.push(newTemplate); + }); + setTimeout(() => { + this._variationURLs.forEach((url, i) => { + this._props.menu._variations[i].setImageAsBackground(url, true); + }); + this._loading = false; + }); + } + + setPromptInputRef: React.LegacyRef<HTMLTextAreaElement> = (node) => { + this.promptInput = node; + } + + private optionsButtonOpts: [IconProp, () => any] = ['gear', () => {}]; + private previewBoxRightButtonOpts: [IconProp, () => any] = ['gear', () => this.forceUpdate()]; + + 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={this.setPromptInputRef} + 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={(e) => runInAction(() => 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={(e) => runInAction(() => 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={(e) => runInAction(() => 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?.()); + } + + setContainerRef: React.LegacyRef<HTMLDivElement> = (node) => { + this.previewWindow = node; + } + + @action setVariationTab = (open: boolean) => { + this._variationsTabOpen = open; + if (this.previewWindow && open) { + this.previewWindow.style.height = String(Number(this.previewWindow.clientHeight) * .6); + } else if (this.previewWindow && !open) { + this.previewWindow.style.height = String(Number(this.previewWindow.clientHeight) * 5/3); + } + } + + get renderedDocPreview(){ + const doc: Doc = this._props.template.getRenderedDoc() as unknown as Doc; + + return ( + <div className="docCreatorMenu-expanded-template-preview" ref={this.setContainerRef}> + {this.previewWindow ? <DocumentView + Document={doc} + isContentActive={emptyFunction} + addDocument={returnFalse} + moveDocument={returnFalse} + removeDocument={returnFalse} + PanelWidth={() => this.previewWindow?.clientWidth ?? 500} + PanelHeight={() => this.previewWindow?.clientHeight ?? 500} + ScreenToLocalTransform={() => new Transform(-this._props.menu._pageX - 5, -this._props.menu._pageY - 35, 1)} + renderDepth={5} + whenChildContentsActiveChanged={emptyFunction} + focus={emptyFunction} + styleProvider={DefaultStyleProvider} + addDocTab={DocumentViewInternal.addDocTabFunc} + pinToPres={() => undefined} + childFilters={returnEmptyFilter} + childFiltersByRanges={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} + fitContentsToBox={returnFalse} + fitWidth={returnFalse} + /> : null} + </div> + ) + } + + 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={() => { + // if (this._props.template === this._props.menu._selectedTemplate) { + // this._props.menu.updateRenderedPreviewCollection(this._props.template); + // } + this._props.menu.setExpandedView(undefined); + }}/> + <DocCreatorMenuButton icon={'lightbulb'} function={() => this.setVariationTab(!this._variationsTabOpen)}/> + <DocCreatorMenuButton icon={'arrow-rotate-backward'} function={() => { this._props.menu.editLastTemplate(); this.forceUpdate(); }}/> + </div> + </div> + </div> + ); + } +}
\ No newline at end of file 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..beda45ac3 --- /dev/null +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplateMenuFieldOptions.tsx @@ -0,0 +1,193 @@ +import { makeObservable, observable, runInAction } 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'); + var 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={e => runInAction(() => 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 => + <div className='operator-dropdown-option' onPointerDown={() => {params.target = fieldTitle}}>{fieldTitle === title ? 'Own' : fieldTitle}</div> + )} + </div> + <input + className="form-row-textarea" + onChange={e => runInAction(() => params.attribute = e.target.value)} + placeholder='attribute' + value={params.attribute} + /> + <div className='form-row-plain-text'>{'becomes'}</div> + <input + className="form-row-textarea" + onChange={e => runInAction(() => 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, runInAction(() => () => { + 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={() => runInAction(() => (this._props.menu._menuContent = 'templates'))}/> + </div> + <div className="panels-container">{this._props.menu.fieldsInfos.map((field, i) => this.fieldPanel(field, i))}</div> + </div> + ); + } + + +}
\ No newline at end of file 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..de2f9e455 --- /dev/null +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplatePreviewBox.tsx @@ -0,0 +1,97 @@ +import { Colors } from "@dash/components/src"; +import { FontAwesomeIcon} from "@fortawesome/react-fontawesome"; +import { Template } from "../Template"; +import { makeObservable, observable, reaction, runInAction } 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 { IDisposer } from "mobx-utils"; +import { ImageField } from "../../../../../../fields/URLField"; +import { ImageCast } from "../../../../../../fields/Types"; +import { observer } from "mobx-react"; + +export interface TemplatePreviewBoxProps { + template: Template; + menu: DocCreatorMenu; + leftButtonOpts?: [icon: IconProp, func: (...args: any) => void] + rightButtonOpts?: [icon: IconProp, func: (...args: any) => void] +} + +@observer +export class TemplatePreviewBox extends ObservableReactComponent<TemplatePreviewBoxProps> { + + @observable private previewWindow: HTMLDivElement | null = null; + + setContainerRef: React.LegacyRef<HTMLDivElement> = (node) => { + this.previewWindow = node; + } + + constructor(props: TemplatePreviewBoxProps) { + super(props); + makeObservable(this); + } + + get doc() { + return this._props.template.getRenderedDoc() as Doc; + } + + render() { + const template = this._props.template; + + return ( + <div + key={template.title} + className="docCreatorMenu-preview-window" + ref={this.setContainerRef} + 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.previewWindow?.clientWidth ?? this._props.menu._menuDimensions.height * .3} + PanelHeight={() => this.previewWindow?.clientHeight ?? this._props.menu._menuDimensions.height * .3} + ScreenToLocalTransform={() => new Transform(-this._props.menu._pageX - 5, -this._props.menu._pageY - 35, 1)} + renderDepth={1} + whenChildContentsActiveChanged={emptyFunction} + focus={emptyFunction} + styleProvider={DefaultStyleProvider} + addDocTab={this._props.menu._props.addDocTab} + pinToPres={() => undefined} + childFilters={returnEmptyFilter} + childFiltersByRanges={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} + fitContentsToBox={returnFalse} + fitWidth={returnFalse} + hideDecorations={true} + /> + </div> + ) + } +}
\ No newline at end of file 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..d53853c52 --- /dev/null +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplatePreviewGrid.tsx @@ -0,0 +1,61 @@ +import { Colors } from "@dash/components/src"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { action, makeObservable, observable, 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; + optionsButtonOpts?: [IconProp, (...args: any) => any]; + previewBoxLeftButtonOpts?: [IconProp, (...args: any) => any]; + 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 => ( + <TemplatePreviewBox + 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..219152549 --- /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 { DataVizTemplateLayout, 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 { StrCast } from "../../../../../../fields/Types"; +import { DefaultStyleProvider } from "../../../../StyleProvider"; +import { DocumentView } from "../../../DocumentView"; +import { Transform } from "../../../../../util/Transform"; +import { Docs, DocumentOptions } from "../../../../../documents/Documents"; +import { Template } from "../Template"; + +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: 0, 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 ?? 0; + 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; + + const columns: number = this._layout.columns ?? this.columnsCount; + const xGap: number = this._layout.xMargin; + const yGap: number = this._layout.yMargin; + const startX: number = -Number(collection._width) / 2; + const startY: number = -Number(collection._height) / 2; + const docHeight: number = Number(docs[0]._height); + const docWidth: number = Number(docs[0]._width); + + if (columns === 0 || docs.length === 0) { + return; + } + + 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 += docWidth + xGap; + ++docsChanged; + ++i; + } + i = 0; + curX = startX; + curY += docHeight + yGap; + } + }; + + @computed + get previewInfo() { + const docHeight: number = Number(this.renderedDocs[0]._height); + const docWidth: number = Number(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; + } + } + + 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._props.menu._menuDimensions.width - 80} + PanelHeight={() => this._props.menu._menuDimensions.height - 105} + ScreenToLocalTransform={() => new Transform(-this._props.menu._pageX - 5, -this._props.menu._pageY - 35, 1)} + renderDepth={5} + whenChildContentsActiveChanged={emptyFunction} + focus={emptyFunction} + styleProvider={DefaultStyleProvider} + addDocTab={this._props.menu._props.addDocTab} + pinToPres={() => undefined} + childFilters={returnEmptyFilter} + childFiltersByRanges={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} + fitContentsToBox={returnFalse} + fitWidth={returnFalse} + 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 } +}
\ No newline at end of file |
