aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplateRenderPreviewWindow.tsx
diff options
context:
space:
mode:
authorbobzel <zzzman@gmail.com>2025-06-26 10:53:54 -0400
committerbobzel <zzzman@gmail.com>2025-06-26 10:53:54 -0400
commitbaae27b205356898c5866a0f095e4ec056e02459 (patch)
tree1b62de5579b8de8be81b6d342a9767f0f379bb91 /src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplateRenderPreviewWindow.tsx
parentccfdf905400cd4b81d8cde0f16bb0e15cd65621b (diff)
parent0093370a04348ef38b91252d02ab850f25d753b2 (diff)
Merge branch 'master' into agent-paper-main
Diffstat (limited to 'src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplateRenderPreviewWindow.tsx')
-rw-r--r--src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplateRenderPreviewWindow.tsx346
1 files changed, 346 insertions, 0 deletions
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;
+ }
+}