aboutsummaryrefslogtreecommitdiff
path: root/src/client/views
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views')
-rw-r--r--src/client/views/Main.tsx3
-rw-r--r--src/client/views/nodes/DataVizBox/DocCreatorMenu/Backend/TemplateManager.ts118
-rw-r--r--src/client/views/nodes/DataVizBox/DocCreatorMenu/Backend/TemplateMenuAIUtils.ts130
-rw-r--r--src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.scss512
-rw-r--r--src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.tsx1064
-rw-r--r--src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/DynamicField.tsx117
-rw-r--r--src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/Field.tsx66
-rw-r--r--src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/FieldUtils.tsx79
-rw-r--r--src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/StaticField.tsx147
-rw-r--r--src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/ConditionalsTextarea.tsx65
-rw-r--r--src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/DocCreatorMenuButton.tsx41
-rw-r--r--src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplateEditingWindow.tsx242
-rw-r--r--src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplateMenuFieldOptions.tsx193
-rw-r--r--src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplatePreviewBox.tsx97
-rw-r--r--src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplatePreviewGrid.tsx61
-rw-r--r--src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplateRenderPreviewWindow.tsx346
-rw-r--r--src/client/views/nodes/DataVizBox/DocCreatorMenu/Template.ts220
-rw-r--r--src/client/views/nodes/DataVizBox/DocCreatorMenu/Template.tsx139
-rw-r--r--src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateBackend.ts (renamed from src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateBackend.tsx)309
-rw-r--r--src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/DataField.ts21
-rw-r--r--src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/DecorationField.ts3
-rw-r--r--src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/DynamicField.ts136
-rw-r--r--src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/StaticContentField.ts63
-rw-r--r--src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/TemplateField.ts174
-rw-r--r--src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/TemplateFieldUtils.ts71
-rw-r--r--src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateManager.tsx22
-rw-r--r--src/client/views/nodes/DataVizBox/components/TableBox.tsx10
-rw-r--r--src/client/views/nodes/ImageBox.scss2
-rw-r--r--src/client/views/smartdraw/DrawingFillHandler.tsx1
29 files changed, 2608 insertions, 1844 deletions
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index b884eb8c8..67e8078ba 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -65,6 +65,8 @@ import { PresBox, PresSlideBox } from './nodes/trails';
import { FaceRecognitionHandler } from './search/FaceRecognitionHandler';
import { SearchBox } from './search/SearchBox';
import { StickerPalette } from './smartdraw/StickerPalette';
+import { TemplateField } from './nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/TemplateField';
+import { TemplateFieldUtils } from './nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/TemplateFieldUtils';
import { ScrapbookBox } from './nodes/scrapbook/ScrapbookBox';
dotenv.config();
@@ -101,6 +103,7 @@ FieldLoader.ServerLoadStatus = { requested: 0, retrieved: 0, message: 'cache' };
new PingManager();
new KeyManager();
new FaceRecognitionHandler();
+ TemplateField.CreateField = TemplateFieldUtils.CreateField; // set the init function for fields
// initialize plugins and classes that require plugins
CollectionDockingView.Init(TabDocView);
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/Backend/TemplateManager.ts b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Backend/TemplateManager.ts
new file mode 100644
index 000000000..6fcca7e30
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Backend/TemplateManager.ts
@@ -0,0 +1,118 @@
+import { action, makeAutoObservable } from 'mobx';
+import { Col } from '../DocCreatorMenu';
+import { FieldSettings, TemplateField } from '../TemplateFieldTypes/TemplateField';
+import { Template } from '../Template';
+import { Doc, NumListCast } from '../../../../../../fields/Doc';
+import { DataVizBox } from '../../DataVizBox';
+import { TemplateFieldType } from '../TemplateBackend';
+import { TemplateMenuAIUtils } from './TemplateMenuAIUtils';
+
+export type Conditional = {
+ field: string;
+ operator: '=' | '>' | '<' | 'contains';
+ condition: string;
+ target: string;
+ attribute: string;
+ value: string;
+}
+
+export class TemplateManager {
+
+ templates: Template[] = [];
+
+ conditionalFieldLogic: Record<string, Conditional[]> = {};
+
+ constructor(templateSettings: FieldSettings[]) {
+ makeAutoObservable(this);
+ this.templates = this.initializeTemplates(templateSettings);
+ }
+
+ initializeTemplates = (templateSettings: FieldSettings[]) => templateSettings.map(settings => {
+ return new Template(settings)});
+
+ getValidTemplates = (cols: Col[]) => this.templates.filter(template => template.isValidTemplate(cols));
+
+ addTemplate = (newTemplate: Template) => this.templates.push(newTemplate);
+
+ removeTemplate = (template: Template) => {
+ this.templates.splice(this.templates.indexOf(template), 1);
+ template.cleanup();
+ };
+
+ addFieldCondition = (fieldTitle: string, condition: Conditional) => {
+ if (this.conditionalFieldLogic[fieldTitle] === undefined) {
+ this.conditionalFieldLogic[fieldTitle] = [condition];
+ } else {
+ this.conditionalFieldLogic[fieldTitle].push(condition);
+ }
+ }
+
+ removeFieldCondition = (fieldTitle: string, condition: Conditional) => {
+ if (this.conditionalFieldLogic[fieldTitle]) {
+ this.conditionalFieldLogic[fieldTitle] = this.conditionalFieldLogic[fieldTitle].filter(cond => cond !== condition);
+ }
+ }
+
+ addDataField = (title: string) => {
+ this.templates.forEach(template => template.addDataField(title));
+ }
+
+ removeDataField = (title: string) => {
+ this.templates.forEach(template => template.removeDataField(title));
+ }
+
+ createDocsFromTemplate = action((dv: DataVizBox, template: Template, cols: Col[], debug: boolean = false) => {
+ const csvFields = Array.from(Object.keys(dv.records[0]));
+
+ const processContent = async (content: { [title: string]: string }) => {
+ const templateCopy = template.clone();
+
+ csvFields
+ .filter(title => title)
+ .forEach(title => {
+ const field = templateCopy.getFieldByTitle(title);
+ field && field.setContent(content[title], field.viewType);
+ });
+
+ const gptFunc = (type: TemplateFieldType) => (type === TemplateFieldType.VISUAL ? TemplateMenuAIUtils.renderGPTImageCall : TemplateMenuAIUtils.renderGPTTextCall);
+ const applyGPTContent = async () => {
+ const promises = cols
+ .filter(field => field.AIGenerated)
+ .map(field => {
+ const templateField: TemplateField = templateCopy.getFieldByTitle(field.title) as TemplateField;
+ if (templateField !== undefined) {
+ return gptFunc(field.type)(templateCopy, field, templateField.getID);
+ }
+ return null;
+ })
+ .filter(p => p !== null);
+
+ await Promise.all(promises);
+ };
+
+ await applyGPTContent();
+
+ templateCopy.applyConditionalLogic(this.conditionalFieldLogic);
+
+ return templateCopy.getRenderedDoc();
+ };
+
+ const rowContents = debug
+ ? [{}, {}, {}, {}]
+ : NumListCast(dv.layoutDoc.dataViz_selectedRows).map(row =>
+ csvFields.reduce(
+ (values, col) => {
+ values[col] = dv.records[row][col];
+ return values;
+ },
+ {} as { [title: string]: string }
+ )
+ );
+
+ return Promise.all(rowContents.map(processContent)).then(
+ action(renderedDocs => {
+ return renderedDocs;
+ })
+ );
+ });
+}
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/Backend/TemplateMenuAIUtils.ts b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Backend/TemplateMenuAIUtils.ts
new file mode 100644
index 000000000..9bc2bfce2
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Backend/TemplateMenuAIUtils.ts
@@ -0,0 +1,130 @@
+import { action } from "mobx";
+import { Upload } from "openai/resources";
+import { ClientUtils } from "../../../../../../ClientUtils";
+import { Networking } from "../../../../../Network";
+import { gptImageCall, gptAPICall, GPTCallType } from "../../../../../apis/gpt/GPT";
+import { Col } from "../DocCreatorMenu";
+import { TemplateFieldSize, TemplateFieldType } from "../TemplateBackend";
+import { TemplateField, ViewType } from "../TemplateFieldTypes/TemplateField";
+import { Template } from "../Template";
+import { Doc } from "../../../../../../fields/Doc";
+import { DrawingFillHandler } from "../../../../smartdraw/DrawingFillHandler";
+import { CollectionFreeFormView } from "../../../../collections/collectionFreeForm";
+
+export class TemplateMenuAIUtils {
+
+ public static generateGPTImage = async (prompt: string): Promise<string | undefined> => {
+ try {
+ const res = await gptImageCall(prompt);
+
+ if (res) {
+ const result = (await Networking.PostToServer('/uploadRemoteImage', { sources: res })) as Upload.FileInformation[];
+ const source = ClientUtils.prepend(result[0].accessPaths.agnostic.client);
+ return source;
+ }
+ } catch (e) {
+ console.log(e);
+ }
+ };
+
+ public static renderGPTImageCall = async (template: Template, col: Col, fieldNumber: number): Promise<boolean> => {
+ const generateAndLoadImage = async (id: number, prompt: string) => {
+ const url = await this.generateGPTImage(prompt);
+ var field: TemplateField = template.getFieldByID(id);
+
+ field.setContent(url ?? '', ViewType.IMG);
+ field = template.getFieldByID(id);
+ field.setTitle(col.title);
+ };
+
+ const fieldContent: string = template.compiledContent;
+
+ try {
+ const sysPrompt =
+ `#${Math.random() * 100}: 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);
+
+ await generateAndLoadImage(fieldNumber, prompt);
+ } catch (e) {
+ console.log(e);
+ }
+ return true;
+ };
+
+ public static renderGPTTextCall = async (template: Template, col: Col, fieldNum: number | undefined): Promise<boolean> => {
+ 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 textAssignment = `--- title: ${col.title}, prompt: ${col.desc}, word limit: ${wordLimit(col.sizes[0])} words, assigned field: ${fieldNum} ---`;
+
+ const fieldContent: string = template.compiledContent;
+
+ try {
+ const prompt = fieldContent + textAssignment;
+
+ const res = await gptAPICall(`${Math.random() * 100000}: ${prompt}`, GPTCallType.FILL);
+
+ if (res) {
+ const assignments: { [title: string]: { number: string; content: string } } = JSON.parse(res);
+ Object.entries(assignments).forEach(([, /* title */ info]) => {
+ const field: TemplateField = template.getFieldByID(Number(info.number));
+
+ field.setContent(info.content ?? '', ViewType.TEXT);
+ field.setTitle(col.title);
+ });
+ }
+ } catch (err) {
+ console.log(err);
+ }
+
+ return true;
+ };
+
+ /**
+ * 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
+ */
+ public static applyGPTContentToTemplate = async (template: Template, assignments: { [field: string]: Col }): Promise<Template | undefined> => {
+ const GPTTextCalls = Object.entries(assignments).filter(([, col]) => col.type === TemplateFieldType.TEXT && col.AIGenerated);
+ const GPTIMGCalls = Object.entries(assignments).filter(([, col]) => col.type === TemplateFieldType.VISUAL && col.AIGenerated);
+
+ if (GPTTextCalls.length) {
+ const promises = GPTTextCalls.map(([id, col]) => {
+ return TemplateMenuAIUtils.renderGPTTextCall(template, col, Number(id));
+ });
+
+ await Promise.all(promises);
+ }
+
+ if (GPTIMGCalls.length) {
+ const promises = GPTIMGCalls.map(async ([id, col]) => {
+ return TemplateMenuAIUtils.renderGPTImageCall(template, col, Number(id));
+ });
+
+ await Promise.all(promises);
+ }
+
+ return template;
+ };
+
+} \ No newline at end of file
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.scss b/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.scss
index 57f4a1e94..463e69c67 100644
--- a/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.scss
+++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.scss
@@ -27,54 +27,36 @@
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;
+ background-color: rgb(60, 60, 65);
}
- &.close-menu {
- font-size: 12px;
- width: 18px;
- height: 18px;
- font-size: 12px;
- margin-left: auto;
- margin-right: 5px;
- margin-bottom: 3px;
+ &.no-margin {
+ margin: 0px;
}
- &.options {
- margin-left: 0px;
+ &.border {
+ border: 1px solid rgb(180, 180, 180);
}
- &:hover {
- background-color: rgb(60, 60, 65);
+ &.float-right {
+ float: right;
+ margin-left: auto;
}
- &.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;
+ &.absolute-right {
+ position: absolute;
+ right: 0px;
}
-
- &.preview-toggle {
- margin: 0px;
- border-top-left-radius: 0px;
- border-bottom-left-radius: 0px;
- border-left: 0px;
+
+ &.right{
+ margin-left: 0px;
+ font-size: 12px;
}
}
@@ -230,6 +212,14 @@
&.full {
width: 100%;
}
+
+ &.no-margin-bottom {
+ margin-bottom: 0px;
+ }
+
+ &.no-margin-top {
+ margin-top: 0px;
+ }
}
//------------------------------------------------------------------------------------------------------------------------------------------
@@ -277,18 +267,6 @@
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 {
display: flex;
flex-direction: column;
@@ -297,6 +275,7 @@
position: relative;
width: 100%;
height: 100%;
+ flex-grow: 1;
.top-panel{
width: 100%;
@@ -307,7 +286,7 @@
display: flex;
flex-direction: column;
justify-content: flex-start;
- height: 100%;
+ height: fit-content;
position: absolute;
right: 0px;
top: 0px;
@@ -322,15 +301,12 @@
display: flex;
justify-content: center;
align-items: center;
- width: 113px;
- height: 113px;
- margin-top: 10px;
- margin-left: 10px;
+ height: 100%;
+ aspect-ratio: 1;
color: none;
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);
@@ -382,16 +358,15 @@
.docCreatorMenu-preview-image{
background-color: transparent;
- height: 100px;
- width: 100px;
+ height: 100%;
display: block;
object-fit: contain;
border-radius: 5px;
- &.expanded {
- height: 100%;
- width: 100%;
- }
+}
+
+.docCreatorMenu-variations-tab {
+ flex-grow: .5;
}
.docCreatorMenu-section {
@@ -399,12 +374,12 @@
flex-direction: column;
align-items: center;
position: relative;
+ flex-grow: 1;
+ height: 100%;
+ width: 100%;
margin: 0px;
margin-top: 0px;
margin-bottom: 0px;
- width: 100%;
- height: 200;
- flex: 0 0 auto;
}
.docCreatorMenu-GPT-options-container {
@@ -412,28 +387,29 @@
flex-direction: row;
justify-content: center;
align-items: center;
- position: relative;
- width: auto;
+ position: absolute;
+ left: 50%;
+ bottom: 0px;
margin: 0px;
+ margin-bottom: 10px;
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;
- color: black;
- height: 125px;
+ display: grid;
+ justify-content: space-evenly;
+ row-gap: 2rem;
+ grid-template-columns: repeat(auto-fill, minmax(150px, 30%));
+ margin: 5px;
width: calc(100% - 10px);
- -ms-overflow-style: none;
- scrollbar-width: none;
+ height: 100%;
+ padding-bottom: 40px;
- .loading-spinner {
- justify-self: center;
+ &.scrolling {
+ overflow-y: scroll;
+ max-height: 300px;
+ padding-bottom: 0px;
}
}
@@ -447,20 +423,14 @@
position: relative;
display: flex;
flex-direction: row;
+ color: whitesmoke;
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-templates-displays {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
}
.docCreatorMenu-section-title {
@@ -690,6 +660,19 @@
}
}
+.loading-spinner {
+ position: absolute;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 100%;
+ width: 100%;
+ z-index: 200;
+ font-size: 20px;
+ font-weight: bold;
+ color: #17175e;
+}
+
.docCreatorMenu-layout-preview-window-wrapper {
flex: 0 0 auto;
display: flex;
@@ -776,6 +759,8 @@
}
}
+
+
//------------------------------------------------------------------------------------------------------------------------------------------
// DocCreatorMenu dashboard CSS
//--------------------------------------------------------------------------------------------------------------------------------------------
@@ -797,6 +782,7 @@
scrollbar-width: none;
.panels-container {
+ display: flex;
height: 100%;
width: 100%;
flex-direction: column;
@@ -810,114 +796,12 @@
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;
+ height: fit-content;
width: calc(100% - 10px);
border: 1px solid rgb(180, 180, 180);
margin: 5px;
@@ -938,12 +822,19 @@
border-top-right-radius: 5px;
border-top-left-radius: 5px;
width: 100%;
- height: 20px;
+ height: fit-content;
background-color: rgb(50, 50, 50);
color: rgb(168, 167, 167);
+ font-size: medium;
.field-title {
color: whitesmoke;
+ font-size: large;
+ }
+
+ &:hover {
+ background-color: rgb(72, 72, 72);
+ cursor: pointer;
}
}
@@ -1038,14 +929,14 @@
.desc-box {
width: 88%;
- height: 50px;
+ height: fit-content;
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);
+ height: fit-content;
width: 100%;
background-color: rgb(50, 50, 50);
border-bottom-right-radius: 5px;
@@ -1057,4 +948,243 @@
}
+ .conditionals-section {
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+ align-items: center;
+ width: 100%;
+
+ .conditionals-title {
+ display: flex;
+ flex-direction: row;
+ width: 100%;
+ justify-content: center;
+ align-items: center;
+ margin: 5px;
+ margin-bottom: 20px;
+ font-size: large;
+ }
+ }
+
+ .form-row {
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-start;
+ align-items: flex-start;
+ color: whitesmoke;
+ width: 100%;
+ height: fit-content;
+ margin-bottom: 15px;
+ flex-wrap: wrap;
+ gap: 5px;
+
+ .form-row-plain-text {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 100%;
+ width: fit-content;
+ padding-top: 2px;
+ padding-bottom: 2px;
+ }
+
+ .operator-options-dropdown {
+ display: flex;
+ flex-direction: column;
+ height: fit-content;
+
+ .operator-dropdown-option {
+ display: none;
+ }
+
+ .operator-dropdown-current {
+ border-radius: 5px;
+ background-color: rgb(50, 50, 50);
+ border: 1px solid rgb(180, 180, 180);
+ text-align: center;
+ padding: 2.25px;
+ padding-left: 4px;
+ padding-right: 4px;
+ }
+
+ &:hover .operator-dropdown-current {
+ border-bottom-right-radius: 0;
+ border-bottom-left-radius: 0;
+ }
+
+ &:hover .operator-dropdown-option {
+ display: flex;
+ height: fit-content;
+ align-items: center;
+ border: 1px solid rgb(180, 180, 180);
+ background-color: rgb(50, 50, 50);
+ padding: 2.25px;
+ padding-left: 8px;
+ padding-right: 8px;
+ text-align: center;
+
+ &:hover {
+ background-color: rgb(70, 70, 70);
+ cursor: pointer;
+ }
+ }
+ }
+
+ .form-row-textarea {
+ height: 24px;
+ width: 110px;
+ border-radius: 5px;
+ background-color: rgb(50, 50, 50);
+ border: 1px solid rgb(180, 180, 180);
+ resize: none;
+ overflow-y: scroll;
+ white-space: nowrap;
+ }
+
+ }
+
+ .form {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: flex-start;
+ width: 80%;
+
+ .form-action-button {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ margin: 3px;
+ cursor: pointer;
+
+ }
+ }
+
+}
+
+//------------------------------------------------------------------------------------------------------------------------------------------
+// EditingWindow CSS
+//--------------------------------------------------------------------------------------------------------------------------------------------
+
+.docCreatorMenu-editing-firefly-section {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ height: 100%;
+ width: 100%;
+ padding: 5px;
}
+
+.docCreatorMenu-firefly-options {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: flex-end;
+ height: fit-content;
+ width: 100%;
+}
+
+.docCreatorMenu-variation-prompt-row {
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-start;
+ align-items: center;
+ gap: 15px;
+ height: fit-content;
+ width: 100%;
+}
+
+.docCreatorMenu-variation-prompt-input-textbox {
+ height: 40px;
+ width: 80%;
+ color: white;
+ margin-top: 1%;
+ margin-bottom: 1%;
+ margin-left: 5%;
+ background-color: rgb(50, 50, 50);
+ border-radius: 5px;
+ overflow: hidden;
+ resize: none;
+}
+
+.options‑menu {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 2rem;
+ padding: 0.5rem 1rem;
+ background: rgb(50, 50, 50);
+ color: whitesmoke;
+ font-family: system-ui, sans-serif;
+ font-size: 0.9rem;
+ flex-wrap: wrap;
+ }
+
+ .menu‑item {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ white-space: nowrap;
+ }
+
+ .menu‑item input[type="range"] {
+ width: 7rem;
+ accent-color: whitesmoke;
+ }
+
+ .value {
+ min-width: 2ch;
+ text-align: right;
+ }
+
+ .switch {
+ gap: 0.75rem;
+ margin-bottom: 0px;
+ }
+
+ .switch .slider {
+ position: relative;
+ width: 2.2rem;
+ height: 1.1rem;
+ background: whitesmoke;
+ border-radius: 1rem;
+ cursor: pointer;
+ transition: background 0.2s;
+ }
+
+ .switch .slider::before {
+ content: '';
+ position: absolute;
+ top: 0.1rem;
+ left: 0.1rem;
+ width: 0.9rem;
+ height: 0.9rem;
+ background: rgb(50, 50, 50);
+ border-radius: 50%;
+ transition: transform 0.2s;
+ }
+
+ .switch input {
+ display: none;
+ }
+
+ .switch input:checked + .slider {
+ background: #78c2f1;
+ }
+
+ .switch input:checked + .slider::before {
+ transform: translateX(1.1rem);
+ }
+
+.firefly-option-label {
+ padding: .2em .6em .3em;
+ font-size: 100%;
+ color: whitesmoke;
+ text-align: center;
+ margin-bottom: 0px;
+ font-weight: 500;
+}
+
+
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.tsx
index 64416c26d..9a84e69a9 100644
--- a/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.tsx
+++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.tsx
@@ -1,5 +1,6 @@
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Colors } from '@dash/components';
+import { IconProp } from '@fortawesome/fontawesome-svg-core';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { action, computed, makeObservable, observable, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import { IDisposer } from 'mobx-utils';
@@ -11,26 +12,35 @@ import { Doc, NumListCast, StrListCast, returnEmptyDoclist } from '../../../../.
import { Id } from '../../../../../fields/FieldSymbols';
import { ImageCast, StrCast } from '../../../../../fields/Types';
import { ImageField } from '../../../../../fields/URLField';
+import { Upload } from '../../../../../server/SharedMediaTypes';
import { Networking } from '../../../../Network';
import { GPTCallType, gptAPICall, gptImageCall } from '../../../../apis/gpt/GPT';
import { Docs, DocumentOptions } from '../../../../documents/Documents';
import { DragManager } from '../../../../util/DragManager';
import { SnappingManager } from '../../../../util/SnappingManager';
+import { Transform } from '../../../../util/Transform';
import { UndoManager, undoable } from '../../../../util/UndoManager';
import { ObservableReactComponent } from '../../../ObservableReactComponent';
+import { DefaultStyleProvider } from '../../../StyleProvider';
import { CollectionFreeFormView } from '../../../collections/collectionFreeForm/CollectionFreeFormView';
import { DocumentView, DocumentViewInternal } from '../../DocumentView';
import { OpenWhere } from '../../OpenWhere';
import { DataVizBox } from '../DataVizBox';
import './DocCreatorMenu.scss';
-import { DefaultStyleProvider } from '../../../StyleProvider';
-import { Transform } from '../../../../util/Transform';
-import { TemplateFieldSize, TemplateFieldType, TemplateLayouts } from './TemplateBackend';
-import { TemplateManager } from './TemplateManager';
+import { TemplateField, ViewType } from './TemplateFieldTypes/TemplateField';
import { Template } from './Template';
-import { Field, FieldContentType } from './FieldTypes/Field';
-import { IconProp } from '@fortawesome/fontawesome-svg-core';
-import { Upload } from '../../../../../server/SharedMediaTypes';
+import { TemplateFieldSize, TemplateFieldType, TemplateLayouts } from './TemplateBackend';
+import { Conditional, TemplateManager } from './Backend/TemplateManager';
+import { DrawingFillHandler } from '../../../smartdraw/DrawingFillHandler';
+import { CgPathIntersect } from 'react-icons/cg';
+import { StaticContentField } from './TemplateFieldTypes/StaticContentField';
+import { TemplateMenuAIUtils } from './Backend/TemplateMenuAIUtils'
+import { TemplatePreviewGrid } from './Menu/TemplatePreviewGrid';
+import { FireflyStructureOptions, TemplateEditingWindow } from './Menu/TemplateEditingWindow';
+import { DocCreatorMenuButton } from './Menu/DocCreatorMenuButton';
+import { ConditionalsTextArea } from './Menu/ConditionalsTextarea';
+import { TemplatesRenderPreviewWindow } from './Menu/TemplateRenderPreviewWindow';
+import { TemplateMenuFieldOptions } from './Menu/TemplateMenuFieldOptions';
export enum LayoutType {
FREEFORM = 'Freeform',
@@ -61,6 +71,7 @@ export type Col = {
title: string;
type: TemplateFieldType;
defaultContent?: string;
+ AIGenerated?: boolean;
};
interface DocCreateMenuProps {
@@ -72,33 +83,20 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps>
// eslint-disable-next-line no-use-before-define
static Instance: DocCreatorMenu;
- private _disposers: { [name: string]: IDisposer } = {};
-
+ DEBUG_MODE: boolean = false;
private _ref: HTMLDivElement | null = null;
-
private templateManager: TemplateManager;
- @observable _fullyRenderedDocs: Doc[] = [];
- @observable _renderedDocCollectionPreview: Doc | undefined = undefined;
- @observable _renderedDocCollection: Doc | undefined = undefined;
- @observable _docsRendering: boolean = false;
+ @observable _docsRendering: boolean = false; // dictates loading symbol
- @observable _userTemplates: { template: Template; doc: Doc }[] = []; //!!! used to keep track of all templates, should be refactored to work with actual templates and not docs
+ @observable _userTemplates: Template[] = [];
@observable _selectedTemplate: Template | undefined = undefined;
@observable _currEditingTemplate: Template | undefined = undefined;
+ @observable _editedTemplateTrail: Template[] = [];
@observable _userCreatedFields: Col[] = [];
- @observable _selectedCols: { title: string; type: string; desc: string }[] | undefined = [];
-
- @observable _layout: { type: LayoutType; yMargin: number; xMargin: number; columns?: number; repeat: number } = { type: LayoutType.FREEFORM, yMargin: 10, xMargin: 10, columns: 3, repeat: 0 };
- @observable _layoutPreviewScale: number = 1;
- @observable _savedLayouts: DataVizTemplateLayout[] = [];
- @observable _expandedPreview: Doc | undefined = undefined;
@observable _suggestedTemplates: Template[] = [];
- @observable _suggestedTemplatePreviews: { doc: Doc; template: Template }[] = [];
- @observable _GPTOpt: boolean = false;
- @observable _callCount: number = 0;
@observable _GPTLoading: boolean = false;
@observable _pageX: number = 0;
@@ -110,9 +108,8 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps>
@observable _startPos?: { x: number; y: number };
@observable _shouldDisplay: boolean = false;
- @observable _menuContent: 'templates' | 'options' | 'saved' | 'dashboard' = 'templates';
+ @observable _menuContent: 'templates' | 'renderPreview' | 'saved' | 'dashboard' | 'templateEditing' = 'templates';
@observable _dragging: boolean = false;
- @observable _draggingIndicator: boolean = false;
@observable _dataViz?: DataVizBox;
@observable _interactionLock: boolean | undefined;
@observable _snapPt: { x: number; y: number } = { x: 0, y: 0 };
@@ -122,7 +119,8 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps>
@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;
+
+ @observable _variations: Template[] = [];
constructor(props: DocCreateMenuProps) {
super(props);
@@ -134,56 +132,13 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps>
@action setDataViz = (dataViz: DataVizBox) => {
this._dataViz = dataViz;
this._selectedTemplate = undefined;
- this._renderedDocCollection = undefined;
- this._renderedDocCollectionPreview = undefined;
- this._fullyRenderedDocs = [];
- this._suggestedTemplatePreviews = [];
this._suggestedTemplates = [];
this._userCreatedFields = [];
};
- @action addUserTemplate = (template: Template) => {
- this._userTemplates.push({ template: template.cloneBase(), doc: template.getRenderedDoc() });
- };
- @action removeUserTemplate = (template: Template) => {
- this._userTemplates = this._userTemplates.filter(info => info.template !== template);
- };
- @action updateTemplatePreview = (template: Template) => {
- template.renderUpdates();
- const preview = { template: template, doc: template.getRenderedDoc() };
- this._suggestedTemplatePreviews = this._suggestedTemplatePreviews.map(t => { return t.template === preview.template ? preview : t }); //prettier-ignore
- this._userTemplates = this._userTemplates.map(t => { return t.template === preview.template ? preview : t }); //prettier-ignore
- };
@action setSuggestedTemplates = (templates: Template[]) => {
- this._suggestedTemplates = templates;
- this._suggestedTemplatePreviews = templates.map(template => {return {template: template, doc: template.getRenderedDoc()}}); //prettier-ignore
+ this._suggestedTemplates = templates; //prettier-ignore
};
- @computed get docsToRender() {
- return this._selectedTemplate ? NumListCast(this._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;
- }
- }
-
@computed get selectedFields() {
return StrListCast(this._dataViz?.layoutDoc._dataViz_axes);
}
@@ -210,17 +165,13 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps>
.concat(this._userCreatedFields);
}
- @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: React.PointerEvent, func: () => void) => {
+ setUpButtonClick = (e: React.PointerEvent, func: (...args: any) => void) => {
setupMoveUpEvents(
this,
e,
@@ -269,7 +220,6 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps>
}
componentWillUnmount() {
- Object.values(this._disposers).forEach(disposer => disposer?.());
document.removeEventListener('pointerdown', this.onPointerDown, true);
document.removeEventListener('pointerup', this.onPointerUp);
}
@@ -319,10 +269,10 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps>
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<boolean | undefined>(res => { setTimeout(() => { res(this._interactionLock = undefined)})});
+ this._interactionLock = true;
+ const scaleAspect = {x: scale.x, y: scale.y};
+ this.resizeView(refPt, scaleAspect, transl); // prettier-ignore
+ await new Promise<boolean | undefined>(res => { setTimeout(() => { res(this._interactionLock = undefined)})});
}); // prettier-ignore
return true;
};
@@ -364,15 +314,8 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps>
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;
- }
+
+ async createDocsForPreview(): Promise<Doc[]> { return this._dataViz && this._selectedTemplate ? ((await this.templateManager.createDocsFromTemplate(this._dataViz, this._selectedTemplate, this.fieldsInfos, this.DEBUG_MODE)).filter(doc => doc).map(doc => doc!) ?? []) as unknown as Doc[] : []; }
@action updateSelectedTemplate = async (template: Template) => {
if (this._selectedTemplate === template) {
@@ -380,31 +323,15 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps>
return;
} else {
this._selectedTemplate = template;
- template.renderUpdates();
- this._fullyRenderedDocs = (await this.createDocsFromTemplate(template)) ?? [];
- this.updateRenderedDocCollection();
}
};
- @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;
- };
-
- editTemplate = (doc: Doc) => {
- DocumentViewInternal.addDocTabFunc(doc, OpenWhere.addRight);
- DocumentView.DeselectAll();
- Doc.UnBrushDoc(doc);
- };
+ // testTemplate = async () => {
+ // this._suggestedTemplates = this.templateManager.templates; //prettier-ignore
+ // };
@action addField = () => {
- const newFields: Col[] = this._userCreatedFields.concat([{ title: '', type: TemplateFieldType.UNSET, desc: '', sizes: [] }]);
+ const newFields: Col[] = this._userCreatedFields.concat([{ title: '', type: TemplateFieldType.UNSET, desc: '', sizes: [], AIGenerated: true }]);
this._userCreatedFields = newFields;
};
@@ -439,11 +366,18 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps>
};
@action setColType = (column: Col, type: TemplateFieldType) => {
+ if (type === TemplateFieldType.DATA) {
+ this.templateManager.addDataField(column.title);
+ } else if (column.type === TemplateFieldType.DATA) {
+ this.templateManager.removeDataField(column.title);
+ }
+
if (this.selectedFields.includes(column.title)) {
this._dataViz?.setColumnType(column.title, type);
} else {
column.type = type;
}
+
this.forceUpdate();
};
@@ -469,53 +403,10 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps>
this.forceUpdate();
};
- generateGPTImage = async (prompt: string): Promise<string | undefined> => {
- try {
- const res = await gptImageCall(prompt);
-
- if (res) {
- const result = (await Networking.PostToServer('/uploadRemoteImage', { sources: res })) as Upload.FileInformation[];
- const source = ClientUtils.prepend(result[0].accessPaths.agnostic.client);
- return source;
- }
- } catch (e) {
- console.log(e);
- }
- };
-
- /**
- * 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
- */
- applyGPTContentToTemplate = async (template: Template, assignments: { [field: string]: Col }): Promise<Template | undefined> => {
- const GPTTextCalls = Object.entries(assignments).filter(([, col]) => col.type === TemplateFieldType.TEXT && this._userCreatedFields.includes(col));
- const GPTIMGCalls = Object.entries(assignments).filter(([, col]) => col.type === TemplateFieldType.VISUAL && this._userCreatedFields.includes(col));
-
- if (GPTTextCalls.length) {
- const promises = GPTTextCalls.map(([str, col]) => {
- return this.renderGPTTextCall(template, col, Number(str));
- });
-
- await Promise.all(promises);
- }
-
- if (GPTIMGCalls.length) {
- const promises = GPTIMGCalls.map(async ([fieldNum, col]) => {
- return this.renderGPTImageCall(template, col, Number(fieldNum));
- });
-
- await Promise.all(promises);
- }
-
- return template;
- };
-
compileFieldDescriptions = (templates: Template[]): string => {
let descriptions: string = '';
templates.forEach(template => {
- descriptions += `---------- NEW TEMPLATE TO INCLUDE: The title is: ${template.mainField.getTitle()}. Its fields are: `;
+ descriptions += `---------- NEW TEMPLATE TO INCLUDE: The title is: ${template.title}. Its fields are: `;
descriptions += template.descriptionSummary;
});
@@ -540,10 +431,7 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps>
const inputText = fieldDescriptions.concat(colDescriptions);
- ++this._callCount;
- const origCount = this._callCount;
-
- const prompt: string = `(${origCount}) ${inputText}`;
+ const prompt: string = `(${Math.random() * 100000}) ${inputText}`;
this._GPTLoading = true;
@@ -555,15 +443,15 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps>
const brokenDownAssignments: [Template, { [fieldID: number]: Col }][] = [];
Object.entries(assignments).forEach(([tempTitle, assignment]) => {
- const template = templates.filter(t => t.mainField.getTitle() === tempTitle)[0];
+ const template = templates.filter(temp => temp.title === tempTitle)[0];
if (!template) return;
const toObj = Object.entries(assignment).reduce(
(a, [fieldID, colTitle]) => {
const col = this.getColByTitle(colTitle);
- if (!this._userCreatedFields.includes(col)) {
- // do the following for any fields not added by the user; will change in the future, for now only GPT content works with user-added fields
- const field = template.getFieldByID(Number(fieldID));
- field.setContent(col.defaultContent ?? '', col.type === TemplateFieldType.VISUAL ? FieldContentType.IMAGE : FieldContentType.STRING);
+ if (!col.AIGenerated) {
+ var field = template.getFieldByID(Number(fieldID));
+ field.setContent(col.defaultContent ?? '', col.type === TemplateFieldType.VISUAL ? ViewType.IMG : ViewType.TEXT);
+ field = template.getFieldByID(Number(fieldID));
field.setTitle(col.title);
} else {
a[Number(fieldID)] = this.getColByTitle(colTitle);
@@ -585,776 +473,109 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps>
};
generatePresetTemplates = async () => {
- this._dataViz?.updateColDefaults();
-
- const cols = this.fieldsInfos;
- const templates = this.templateManager.getValidTemplates(cols);
-
- const assignments: [Template, { [field: number]: Col }][] = await this.assignColsToFields(templates, cols);
-
- const renderedTemplatePromises: Promise<Template | undefined>[] = assignments.map(([template, asns]) => this.applyGPTContentToTemplate(template, asns));
-
- await Promise.all(renderedTemplatePromises);
+ const templates: Template[] = [];
- setTimeout(() => {
- this.setSuggestedTemplates(templates);
- this._GPTLoading = false;
- });
- };
+ if (this.DEBUG_MODE) {
+ templates.push(...this.templateManager.templates);
+ } else {
+ this._dataViz?.updateColDefaults();
- renderGPTImageCall = async (template: Template, col: Col, fieldNumber: number): Promise<boolean> => {
- const generateAndLoadImage = async (fieldNum: string, column: Col, prompt: string) => {
- const url = await this.generateGPTImage(prompt);
- const field: Field = template.getFieldByID(Number(fieldNum));
+ const contentFields = this.fieldsInfos.filter(field => field.type !== TemplateFieldType.DATA);
- field.setContent(url ?? '', FieldContentType.IMAGE);
- field.setTitle(column.title);
- };
+ templates.push(...this.templateManager.getValidTemplates(contentFields));
- const fieldContent: string = template.compiledContent;
+ const assignments = await this.assignColsToFields(templates, contentFields);
- try {
- 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);
+ const renderedTemplatePromises = assignments.map(([template, assgns]) => TemplateMenuAIUtils.applyGPTContentToTemplate(template, assgns));
- await generateAndLoadImage(String(fieldNumber), col, prompt);
- } catch (e) {
- console.log(e);
+ await Promise.all(renderedTemplatePromises);
}
- return true;
- };
-
- renderGPTTextCall = async (template: Template, col: Col, fieldNum: number): Promise<boolean> => {
- 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 textAssignment = `--- title: ${col.title}, prompt: ${col.desc}, word limit: ${wordLimit(col.sizes[0])} words, assigned field: ${fieldNum} ---`;
-
- const fieldContent: string = template.compiledContent;
-
- try {
- const prompt = fieldContent + textAssignment;
-
- const res = await gptAPICall(`${++this._callCount}: ${prompt}`, GPTCallType.FILL);
-
- if (res) {
- const assignments: { [title: string]: { number: string; content: string } } = JSON.parse(res);
- Object.entries(assignments).forEach(([title, info]) => {
- const field: Field = template.getFieldByID(Number(info.number));
- const column = this.getColByTitle(title);
-
- field.setContent(info.content ?? '', FieldContentType.STRING);
- field.setTitle(column.title);
- });
- }
- } catch (err) {
- console.log(err);
- }
-
- return true;
- };
-
- createDocsFromTemplate = async (template: Template) => {
- const dv = this._dataViz;
-
- if (!dv) return;
-
- this._docsRendering = true;
-
- const fields: string[] = Array.from(Object.keys(dv.records[0]));
- const selectedRows = NumListCast(dv.layoutDoc.dataViz_selectedRows);
-
- const rowContents: { [title: string]: string }[] = selectedRows.map(row => {
- const values: { [title: string]: string } = {};
- fields.forEach(col => {
- values[col] = dv.records[row][col];
- });
-
- return values;
- });
-
- const processContent = async (content: { [title: string]: string }) => {
- const templateCopy = template.cloneBase();
-
- fields
- .filter(title => title)
- .forEach(title => {
- const field = templateCopy.getFieldByTitle(title);
- if (field === undefined) {
- return;
- }
- field.setContent(content[title]);
- });
-
- const gptPromises = this._userCreatedFields
- .filter(field => field.type === TemplateFieldType.TEXT)
- .map(field => {
- const title = field.title;
- const templateField = templateCopy.getFieldByTitle(title);
- if (templateField === undefined) {
- return;
- }
- const templatefieldID = templateField.getID;
-
- return this.renderGPTTextCall(templateCopy, field, templatefieldID);
- });
-
- const imagePromises = this._userCreatedFields
- .filter(field => field.type === TemplateFieldType.VISUAL)
- .map(field => {
- const title = field.title;
- const templateField = templateCopy.getFieldByTitle(title);
- if (templateField === undefined) {
- return;
- }
- const templatefieldID = templateField.getID;
-
- return this.renderGPTImageCall(templateCopy, field, templatefieldID);
- });
-
- await Promise.all(gptPromises);
-
- await Promise.all(imagePromises);
-
- return templateCopy.getRenderedDoc();
- };
-
- const promises = rowContents.map(content => processContent(content));
-
- const renderedDocs = await Promise.all(promises);
-
- this._docsRendering = false;
- return renderedDocs;
+ setTimeout(
+ action(() => {
+ this.setSuggestedTemplates(templates);
+ this._GPTLoading = false;
+ })
+ );
};
- addRenderedCollectionToMainview = () => {
- const collection = this._renderedDocCollection;
- if (!collection) return;
+ generateVariations = async (onDoc: Doc, prompt: string, options: FireflyStructureOptions): Promise<string[]> => {
+ const { numVariations, temperature, useStyleRef } = options;
+ this.variations = [];
const mainCollection = this._dataViz?.DocumentView?.().containerViewPath?.().lastElement()?.ComponentView as CollectionFreeFormView;
- collection.x = this._pageX - this._menuDimensions.width;
- collection.y = this._pageY - this._menuDimensions.height;
- mainCollection.addDocument(collection);
- this.closeMenu();
- };
- @action setExpandedView = (template: Template | undefined) => {
- if (template) {
- this._currEditingTemplate = template;
- this._expandedPreview = template.mainField.renderedDoc(); //Docs.Create.FreeformDocument([doc], { _height: NumListCast(doc._height)[0], _width: NumListCast(doc._width)[0], title: ''});
- } else {
- this._currEditingTemplate = undefined;
- this._expandedPreview = undefined;
- }
- };
+ const clone: Doc = (await Doc.MakeClone(onDoc)).clone;
+ mainCollection.addDocument(clone);
+ clone.x = 10000;
+ clone.y = 10000;
- get editingWindow() {
- const rendered = !this._expandedPreview ? null : (
- <div className="docCreatorMenu-expanded-template-preview">
- <DocumentView
- Document={this._expandedPreview}
- isContentActive={emptyFunction}
- addDocument={returnFalse}
- moveDocument={returnFalse}
- removeDocument={returnFalse}
- PanelWidth={() => this._menuDimensions.width - 10}
- PanelHeight={() => this._menuDimensions.height - 60}
- ScreenToLocalTransform={() => new Transform(-this._pageX - 5, -this._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}
- />
- </div>
- );
+ await DrawingFillHandler.drawingToImage(clone, 100 - temperature, prompt, useStyleRef ? clone : undefined, this, numVariations)
- return (
- <div className="docCreatorMenu-expanded-template-preview">
- <div className="top-panel" />
- {rendered}
- <div className="right-buttons-panel">
- <button
- className="docCreatorMenu-menu-button section-reveal-options top-right"
- onPointerDown={e =>
- this.setUpButtonClick(e, () => {
- this._currEditingTemplate && this.updateTemplatePreview(this._currEditingTemplate);
- this.setExpandedView(undefined);
- })
- }>
- <FontAwesomeIcon icon="minimize" />
- </button>
- <button
- className="docCreatorMenu-menu-button section-reveal-options top-right-lower"
- onPointerDown={e =>
- this.setUpButtonClick(e, () => {
- this._currEditingTemplate?.resetToBase();
- this.setExpandedView(this._currEditingTemplate);
- })
- }>
- <FontAwesomeIcon icon="arrows-rotate" color="white" />
- </button>
- </div>
- </div>
- );
+ return this.variations;
}
- get templatesPreviewContents() {
- const GPTOptions = <div></div>;
+ variations: string[] = []
- return (
- <div className={`docCreatorMenu-templates-view`}>
- {this._expandedPreview ? (
- this.editingWindow
- ) : (
- <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._suggestedTemplatePreviews.map(({ doc, template }) => (
- <div
- className="docCreatorMenu-preview-window"
- key="0"
- style={{
- border: this._selectedTemplate === template ? `solid 3px ${Colors.MEDIUM_BLUE}` : '',
- boxShadow: this._selectedTemplate === template ? `0 0 15px rgba(68, 118, 247, .8)` : '',
- }}
- onPointerDown={e => this.setUpButtonClick(e, () => runInAction(() => this.updateSelectedTemplate(template)))}>
- <button
- className="option-button left"
- onPointerDown={e =>
- this.setUpButtonClick(e, () => {
- this.setExpandedView(template);
- })
- }>
- <FontAwesomeIcon icon="magnifying-glass" color="white" />
- </button>
- <button className="option-button right" onPointerDown={e => this.setUpButtonClick(e, () => this.addUserTemplate(template))}>
- <FontAwesomeIcon icon="plus" color="white" />
- </button>
- <DocumentView
- Document={doc}
- isContentActive={emptyFunction} // !!! should be return false
- addDocument={returnFalse}
- moveDocument={returnFalse}
- removeDocument={returnFalse}
- PanelWidth={() => (this._selectedTemplate === template ? 104 : 111)}
- PanelHeight={() => (this._selectedTemplate === template ? 104 : 111)}
- ScreenToLocalTransform={() => new Transform(-this._pageX - 5, -this._pageY - 35, 1)}
- renderDepth={1}
- whenChildContentsActiveChanged={emptyFunction}
- focus={emptyFunction}
- styleProvider={DefaultStyleProvider}
- addDocTab={this._props.addDocTab}
- pinToPres={() => undefined}
- childFilters={returnEmptyFilter}
- childFiltersByRanges={returnEmptyFilter}
- searchFilterDocs={returnEmptyDoclist}
- fitContentsToBox={returnFalse}
- fitWidth={returnFalse}
- hideDecorations={true}
- />
- </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">
- <FontAwesomeIcon icon="plus" color="rgb(160, 160, 160)" />
- </div>
- {this._userTemplates.map(({ template, doc }) => (
- <div
- className="docCreatorMenu-preview-window"
- key="0"
- style={{
- border: this._selectedTemplate === template ? `solid 3px ${Colors.MEDIUM_BLUE}` : '',
- boxShadow: this._selectedTemplate === template ? `0 0 15px rgba(68, 118, 247, .8)` : '',
- }}
- onPointerDown={e => this.setUpButtonClick(e, () => runInAction(() => this.updateSelectedTemplate(template)))}>
- <button
- className="option-button left"
- onPointerDown={e =>
- this.setUpButtonClick(e, () => {
- this.setExpandedView(template);
- })
- }>
- <FontAwesomeIcon icon="magnifying-glass" color="white" />
- </button>
- <button className="option-button right" onPointerDown={e => this.setUpButtonClick(e, () => this.removeUserTemplate(template))}>
- <FontAwesomeIcon icon="minus" color="white" />
- </button>
- <DocumentView
- Document={doc}
- isContentActive={emptyFunction} // !!! should be return false
- addDocument={returnFalse}
- moveDocument={returnFalse}
- removeDocument={returnFalse}
- PanelWidth={() => (this._selectedTemplate === template ? 104 : 111)}
- PanelHeight={() => (this._selectedTemplate === template ? 104 : 111)}
- ScreenToLocalTransform={() => new Transform(-this._pageX - 5, -this._pageY - 35, 1)}
- renderDepth={1}
- whenChildContentsActiveChanged={emptyFunction}
- focus={emptyFunction}
- styleProvider={DefaultStyleProvider}
- addDocTab={this._props.addDocTab}
- pinToPres={() => undefined}
- childFilters={returnEmptyFilter}
- childFiltersByRanges={returnEmptyFilter}
- searchFilterDocs={returnEmptyDoclist}
- fitContentsToBox={returnFalse}
- fitWidth={returnFalse}
- hideDecorations={true}
- />
- </div>
- ))}
- </div>
- </div>
- </div>
- )}
- </div>
- );
+ @action addVariation = (url: string) => {
+ this.variations.push(url);
}
- @action updateXMargin = (input: string) => {
- this._layout.xMargin = Number(input);
- setTimeout(() => {
- if (!this._renderedDocCollection || !this._fullyRenderedDocs) return;
- this.applyLayout(this._renderedDocCollection, this._fullyRenderedDocs);
- });
- };
- @action updateYMargin = (input: string) => {
- this._layout.yMargin = Number(input);
- setTimeout(() => {
- if (!this._renderedDocCollection || !this._fullyRenderedDocs) return;
- this.applyLayout(this._renderedDocCollection, this._fullyRenderedDocs);
- });
- };
- @action updateColumns = (input: string) => {
- this._layout.columns = Number(input);
- this.updateRenderedDocCollection();
- };
-
- 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', 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>
- );
- default:
- break;
- }
- }
-
- applyLayout = (collection: Doc, docs: Doc[]) => {
- const { horizontalSpan, verticalSpan } = this.previewInfo;
- collection._height = verticalSpan;
- collection._width = horizontalSpan;
-
- const layout = this._layout;
- const columns: number = layout.columns ?? this.columnsCount;
- const xGap: number = layout.xMargin;
- const yGap: number = layout.yMargin;
- // const repeat: number = templateInfo.layout.repeat;
- 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;
+ addRenderedCollectionToMainview = (collection: Doc) => {
+ if (collection) {
+ const mainCollection = this._dataViz?.DocumentView?.().containerViewPath?.().lastElement()?.ComponentView as CollectionFreeFormView;
+ collection.x = this._pageX - this._menuDimensions.width;
+ collection.y = this._pageY - this._menuDimensions.height;
+ mainCollection?.addDocument(collection);
+ this.closeMenu();
}
};
- @computed
- get previewInfo() {
- const docHeight: number = Number(this._fullyRenderedDocs[0]._height);
- const docWidth: number = Number(this._fullyRenderedDocs[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,
- };
- }
+ @action editLastTemplate = () => { if (this._editedTemplateTrail.length) this._currEditingTemplate = this._editedTemplateTrail.pop()}
- /**
- * 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._fullyRenderedDocs) return;
-
- const { horizontalSpan, verticalSpan } = this.previewInfo;
-
- 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;
- }
- };
+ @action setExpandedView = (template: Template | undefined) => {
- const collection: Doc = collectionFactory()(this._fullyRenderedDocs, {
- isDefaultTemplateDoc: true,
- _height: verticalSpan,
- _width: horizontalSpan,
- title: 'title',
- backgroundColor: 'gray',
- });
+ if (template) {
+ this._menuContent = 'templateEditing';
+ this._currEditingTemplate && this._editedTemplateTrail.push(this._currEditingTemplate);
+ } else {
+ this._menuContent = 'templates';
+ }
- this.applyLayout(collection, this._fullyRenderedDocs);
+ this._currEditingTemplate = template;
- this._renderedDocCollection = collection;
+ //Docs.Create.FreeformDocument([doc], { _height: NumListCast(doc._height)[0], _width: NumListCast(doc._width)[0], title: ''});
};
- layoutPreviewContents = () => {
- return this._docsRendering ? (
- <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._menuDimensions.width - 80}
- PanelHeight={() => this._menuDimensions.height - 105}
- ScreenToLocalTransform={() => new Transform(-this._pageX - 5, -this._pageY - 35, 1)}
- renderDepth={5}
- whenChildContentsActiveChanged={emptyFunction}
- focus={emptyFunction}
- styleProvider={DefaultStyleProvider}
- addDocTab={this._props.addDocTab}
- pinToPres={() => undefined}
- childFilters={returnEmptyFilter}
- childFiltersByRanges={returnEmptyFilter}
- searchFilterDocs={returnEmptyDoclist}
- fitContentsToBox={returnFalse}
- fitWidth={returnFalse}
- hideDecorations={true}
+ @computed
+ get templatesView() { return (
+ <div className='docCreatorMenu-templates-view'>
+ <div className="docCreatorMenu-templates-displays">
+ <TemplatePreviewGrid
+ title={'Suggested Templates'}
+ menu={this}
+ loading={this._GPTLoading}
+ optionsButtonOpts={this.optionsButtonOpts}
+ templates={this._suggestedTemplates}
/>
- </div>
- );
- };
-
- get optionsMenuContents() {
- const layoutOption = (option: LayoutType, optStyle?: object, specialFunc?: () => void) => {
- return (
- <div
- className="docCreatorMenu-dropdown-option"
- style={optStyle}
- onPointerDown={e =>
- this.setUpButtonClick(e, () => {
- specialFunc?.();
- runInAction(() => {
- this._layout.type = option;
- this.updateRenderedDocCollection();
- });
- })
- }>
- {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 IconProp} />
+ <div className="docCreatorMenu-GPT-options">
+ <div className="docCreatorMenu-GPT-options-container">
+ <DocCreatorMenuButton icon={'arrows-rotate'} styles={'border'} function={this.generatePresetTemplates}/>
</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.FREEFORM, undefined, () => {
- if (!this._layout.columns) this._layout.columns = Math.ceil(Math.sqrt(this.docsToRender.length));
- })}
- {layoutOption(LayoutType.CAROUSEL)}
- {layoutOption(LayoutType.CAROUSEL3D)}
- {layoutOption(LayoutType.MASONRY)}
- </div>
- </div>
- </div>
- {this._layout.type ? this.layoutConfigOptions : null}
- {this.layoutPreviewContents()}
- {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();
- if (!this._selectedTemplate) return;
- const layout: DataVizTemplateLayout = {
- template: this._selectedTemplate.getRenderedDoc(),
- 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;
- this.addRenderedCollectionToMainview();
- }, 'make docs')
- )
- }>
- <FontAwesomeIcon icon="plus" />
- </button>
</div>
</div>
- );
- }
-
- get dashboardContents() {
- const sizes: string[] = ['tiny', 'small', 'medium', 'large', 'huge'];
-
- const fieldPanel = (field: Col, id: number) => {
- return (
- <div className="field-panel" key={id}>
- <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.removeField(field))} 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)' }} value={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"> Prompt </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/prompt 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, i) => fieldPanel(field, i))}</div>
- </div>
- );
- }
+ </div>
+ )};
+
+ private optionsButtonOpts: [IconProp, () => any] = ['gear', () => (this._menuContent = 'dashboard')];
get renderSelectedViewType() {
switch (this._menuContent) {
- case 'templates':
- return this.templatesPreviewContents;
- case 'options':
- return this.optionsMenuContents;
- case 'dashboard':
- return this.dashboardContents;
- default:
- return undefined;
- }
+ case 'templates': return this.templatesView;
+ case 'templateEditing': return <TemplateEditingWindow template={this._currEditingTemplate as Template} menu={this} />;
+ case 'renderPreview': return <TemplatesRenderPreviewWindow menu={this}/>;
+ case 'dashboard': return <TemplateMenuFieldOptions menu={this} templateManager={this.templateManager}/>;
+ } // prettier-ignore
+ return undefined;
}
get resizePanes() {
@@ -1375,34 +596,17 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps>
}
render() {
- const topButton = (icon: string, opt: string, func: () => void, 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 IconProp} />
- </div>
+ const topButton = (icon: string, opt: string, func: () => void, tag: string) => (
+ <div className={`top-button-container ${tag} ${opt === this._menuContent ? 'selected' : ''}`}>
+ <div className="top-button-content" onPointerDown={e => this.setUpButtonClick(e, action(func))}>
+ <FontAwesomeIcon icon={icon as IconProp} />
</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));
- };
+ const onPreviewSelected = () => (this._menuContent = 'templates');
+ const onSavedSelected = () => (this._menuContent = 'dashboard');
+ const onOptionsSelected = () => (this._menuContent = 'renderPreview');
return (
<div className="docCreatorMenu">
@@ -1435,9 +639,7 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps>
return true;
},
emptyFunction,
- undoable(clickEv => {
- clickEv.stopPropagation();
- }, 'drag menu')
+ undoable(clickEv => clickEv.stopPropagation(), 'drag menu')
)
}>
<div className="docCreatorMenu-top-buttons-container">
@@ -1445,9 +647,7 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps>
{topButton('magnifying-glass', 'options', onOptionsSelected, 'middle')}
{topButton('bars', 'saved', onSavedSelected, 'right')}
</div>
- <button className="docCreatorMenu-menu-button close-menu" onPointerDown={e => this.setUpButtonClick(e, this.closeMenu)}>
- <FontAwesomeIcon icon={'minus'} />
- </button>
+ <DocCreatorMenuButton icon={'minus'} styles={'float-right'} function={this.closeMenu}/>
</div>
{this.renderSelectedViewType}
</div>
@@ -1455,4 +655,4 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps>
</div>
);
}
-}
+} \ No newline at end of file
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/DynamicField.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/DynamicField.tsx
deleted file mode 100644
index c5254c17d..000000000
--- a/src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/DynamicField.tsx
+++ /dev/null
@@ -1,117 +0,0 @@
-import { Doc } from "../../../../../../fields/Doc";
-import { Docs } from "../../../../../documents/Documents";
-import { Field, FieldDimensions, FieldSettings, ViewType } from "./Field";
-import { FieldUtils } from "./FieldUtils";
-import { StaticField } from "./StaticField";
-
-export class DynamicField implements Field {
- private subfields: Field[] = [];
-
- private id: number;
- private settings: FieldSettings;
- private title: string = '';
-
- private parent: Field;
- private dimensions: FieldDimensions;
-
- constructor(settings: FieldSettings, id: number, parent?: Field) {
- this.id = id;
- this.settings = settings;
- if (settings.title) { this.title = settings.title };
- if (!parent) {
- this.parent = this;
- this.dimensions = {width: this.settings.br[0] - this.settings.tl[0], height: this.settings.br[1] - this.settings.tl[1], coord: {x: this.settings.tl[0], y: this.settings.tl[1]}};
- } else {
- this.parent = parent;
- this.dimensions = FieldUtils.getLocalDimensions({tl: settings.tl, br: settings.br}, this.parent.getDimensions);
- }
- this.subfields = this.setupSubfields();
- }
-
- setContent = () => { return };
- getContent = () => { return '' };
-
- setTitle = (title: string) => { this.title = title };
- getTitle = () => { return this.title };
-
- get getSubfields() { return this.subfields };
- get getAllSubfields() {
- let fields: Field[] = [];
- this.subfields?.forEach(field => {
- fields.push(field);
- fields = fields.concat(field.getAllSubfields)
- });
- return fields;
- };
-
- get getDimensions() { return this.dimensions };
- get getID() { return this.id };
- get getViewType() { return this.settings.viewType };
-
- get getDescription(): string {
- return this.settings.description ?? '';
- }
-
- matches = (): Array<number> => {
- return [];
- }
-
- updateRenderedDoc = () => {
- return new Doc();
- }
-
- setupSubfields = (): Field[] => {
- const fields: Field[] = [];
- this.settings.subfields?.forEach((fieldSettings, index) => {
- let field: Field;
- const type = fieldSettings.viewType;
-
- const id = Number(String(this.id) + String(index));
-
- if (type == ViewType.CAROUSEL3D || type === ViewType.FREEFORM) {
- field = new DynamicField(fieldSettings, id, this);
- } else {
- field = new StaticField(fieldSettings, this, id);
- }
- fields.push(field);
- });
- return fields;
- }
-
- applyAttributes = (field: Field) => {
- field.setTitle(this.title);
- field.updateRenderedDoc(this.renderedDoc());
- }
-
- getChildDimensions = (coords: { tl: [number, number]; br: [number, number] }): FieldDimensions => {
- const l = (coords.tl[0] * this.dimensions.height) / 2;
- const t = coords.tl[1] * this.dimensions.width / 2; //prettier-ignore
- const r = (coords.br[0] * this.dimensions.height) / 2;
- const b = coords.br[1] * this.dimensions.width / 2; //prettier-ignore
- const width = r - l;
- const height = b - t;
- const coord = { x: l, y: t };
- return { width, height, coord };
- };
-
- renderedDoc = (): Doc => {
- let doc: Doc;
- switch (this.settings.viewType) {
- case ViewType.CAROUSEL3D:
- doc = Docs.Create.Carousel3DDocument(this.subfields.map(field => field.renderedDoc()), {
- title: this.title,
- });
- FieldUtils.applyBasicOpts(doc, this.dimensions, this.settings);
- return doc;
- case ViewType.FREEFORM:
- doc = Docs.Create.FreeformDocument(this.subfields.map(field => field.renderedDoc()), {
- title: this.title,
- });
- FieldUtils.applyBasicOpts(doc, this.dimensions, this.settings);
- return doc;
- default:
- return new Doc();
- }
- }
-
-}
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/Field.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/Field.tsx
deleted file mode 100644
index ea9b566b3..000000000
--- a/src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/Field.tsx
+++ /dev/null
@@ -1,66 +0,0 @@
-import { Doc } from "../../../../../../fields/Doc";
-import { Col } from "../DocCreatorMenu";
-import { TemplateFieldSize, TemplateFieldType } from "../TemplateBackend";
-
-export enum FieldContentType {
- STRING = 'string',
- IMAGE = 'image',
-}
-
-export enum ViewType {
- CAROUSEL3D = 'carousel3d',
- FREEFORM = 'freeform',
- STATIC = 'static',
- DEC = 'decoration'
-}
-
-export type FieldDimensions = {
- width: number;
- height: number;
- coord: {x: number, y: number};
-}
-
-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;
- fontBold?: boolean;
- fontTransform?: 'uppercase' | 'lowercase';
- fieldViewType?: 'freeform' | 'stacked';
-}
-
-export type FieldSettings = {
- tl: [number, number];
- br: [number, number];
- opts: FieldOpts;
- viewType: ViewType;
- title?: string;
- subfields?: FieldSettings[];
- types?: TemplateFieldType[];
- sizes?: TemplateFieldSize[];
- description?: string;
-};
-
-export interface Field {
- getContent: () => string;
- setContent: (content: string, type?: FieldContentType) => void;
- getDimensions: FieldDimensions;
- getSubfields: Field[];
- getAllSubfields: Field[];
- getID: number;
- getViewType: ViewType;
- getDescription: string;
- getTitle: () => string;
- setTitle: (title: string) => void;
- setupSubfields: () => Field[];
- applyAttributes: (field: Field) => void;
- renderedDoc: () => Doc;
- matches: (cols: Col[]) => number[];
- updateRenderedDoc: (oldDoc?: Doc) => Doc;
-} \ No newline at end of file
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/FieldUtils.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/FieldUtils.tsx
deleted file mode 100644
index 3886774d2..000000000
--- a/src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/FieldUtils.tsx
+++ /dev/null
@@ -1,79 +0,0 @@
-import { Doc } from "../../../../../../fields/Doc";
-import { ComputedField, ScriptField } from "../../../../../../fields/ScriptField";
-import { Col } from "../DocCreatorMenu";
-import { TemplateFieldSize, TemplateFieldType, TemplateLayouts } from "../TemplateBackend";
-import { FieldDimensions, FieldSettings } from "./Field";
-
-export class FieldUtils {
- public static getLocalDimensions = (coords: { tl: [number, number]; br: [number, number] }, parentDimensions: FieldDimensions): FieldDimensions => {
- const l = (coords.tl[0] * parentDimensions.width) / 2;
- const t = coords.tl[1] * parentDimensions.height / 2; //prettier-ignore
- const r = (coords.br[0] * parentDimensions.width) / 2;
- const b = coords.br[1] * parentDimensions.height / 2; //prettier-ignore
- const width = r - l;
- const height = b - t;
- const coord = { x: l, y: t };
- return { width, height, coord };
- };
-
- public static applyBasicOpts = (doc: Doc, parentDimensions: FieldDimensions, settings: FieldSettings, oldDoc?: Doc) => {
- const opts = settings.opts;
- doc.isDefaultTemplateDoc = oldDoc ? oldDoc.isDefaultTemplateDoc : true;
- doc._layout_hideScroll = oldDoc ? oldDoc._layout_hideScroll : true;
- doc.x = oldDoc ? oldDoc.x : parentDimensions.coord.x;
- doc.y = oldDoc ? oldDoc.y : parentDimensions.coord.y;
- doc._height = oldDoc ? oldDoc.height : parentDimensions.height;
- doc._width = oldDoc ? oldDoc.width : parentDimensions.width;
- doc.backgroundColor = oldDoc ? oldDoc.backgroundColor : opts.backgroundColor ?? '';
- doc._layout_borderRounding = !opts.cornerRounding ? '0px' : ScriptField.MakeFunction(`${opts.cornerRounding} * this.width + 'px'`);
- doc.borderColor = oldDoc ? oldDoc.borderColor : opts.borderColor;
- doc.borderWidth = oldDoc ? oldDoc.borderWidth : opts.borderWidth;
- doc.opacity = oldDoc ? oldDoc.opacity : opts.opacity;
- doc._rotation = oldDoc ? oldDoc._rotation : opts.rotation;
- doc.hCentering = oldDoc ? oldDoc.hCentering : opts.contentXCentering;
- doc.nativeWidth = parentDimensions.width;
- doc.nativeHeight = parentDimensions.height;
- doc._layout_nativeDimEditable = true;
- };
-
- 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.7;
-
- 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;
-
- currFontSize += 1;
- }
-
- return currFontSize - 1;
- };
-} \ No newline at end of file
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/StaticField.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/StaticField.tsx
deleted file mode 100644
index 47b43f051..000000000
--- a/src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/StaticField.tsx
+++ /dev/null
@@ -1,147 +0,0 @@
-import { Doc } from "../../../../../../fields/Doc";
-import { Docs } from "../../../../../documents/Documents";
-import { Col } from "../DocCreatorMenu";
-import { DynamicField } from "./DynamicField";
-import { FieldUtils } from "./FieldUtils";
-import { Field, FieldContentType, FieldDimensions, FieldSettings, ViewType } from "./Field";
-
-export class StaticField {
- private content: string;
- private contentType: FieldContentType | undefined;
- private subfields: Field[] = [];
- private renderedDocument: Doc;
-
- private id: number;
- private title: string = '';
-
- private settings: FieldSettings;
-
- private parent: Field;
- private dimensions: FieldDimensions;
-
- constructor(settings: FieldSettings, parent: Field, id: number) {
- this.settings = settings;
- if (settings.title) { this.title = settings.title };
- this.id = id;
- this.parent = parent;
- this.dimensions = FieldUtils.getLocalDimensions({tl: settings.tl, br: settings.br}, this.parent.getDimensions);
- this.content = '';
- this.subfields = this.setupSubfields();
- this.renderedDocument = this.updateRenderedDoc();
- };
-
- get getSubfields(): Field[] { return this.subfields ?? []; };
-
- get getAllSubfields(): Field[] {
- let fields: Field[] = [];
- this.subfields?.forEach(field => {
- fields.push(field);
- fields = fields.concat(field.getAllSubfields);
- });
- return fields;
- };
-
- get getDimensions() { return this.dimensions };
- get getID() { return this.id };
- get getViewType() { return this.settings.viewType };
-
- get getDescription(): string {
- return this.settings.description ?? '';
- }
-
- renderedDoc = () => {
- return this.renderedDocument;
- }
-
- setContent = (newContent: string, type?: FieldContentType) => {
- this.content = newContent;
- if (type) this.contentType = type;
- this.updateRenderedDoc(this.renderedDocument);
- };
- getContent() { return this.content };
-
- setTitle = (title: string) => {
- this.title = title;
- this.renderedDocument.title = title;
- this.updateRenderedDoc(this.renderedDocument);
- };
- getTitle = () => { return this.title };
-
- applyAttributes = (field: Field) => { //!!! can be updated later for more robust clonign; this is all ythat's needed now
- field.setTitle(this.title);
- field.setContent('', this.contentType);
- field.updateRenderedDoc(this.renderedDoc());
- }
-
- setupSubfields = (): Field[] => {
- const fields: Field[] = [];
- this.settings.subfields?.forEach((fieldSettings, index) => {
- let field: Field;
- const type = fieldSettings.viewType;
-
- const id = Number(String(this.id) + String(index));
-
- if (type === ViewType.FREEFORM || type === ViewType.CAROUSEL3D) {
- field = new DynamicField(fieldSettings, id, this);
- } else {
- field = new StaticField(fieldSettings, this, id);
- };
-
- fields.push(field);
- });
- return fields;
- };
-
- matches = (cols: Col[]): number[] => {
- const colMatchesField = (col: Col) => {
- const isMatch: boolean = (
- this.settings.sizes?.some(size => col.sizes?.includes(size))
- && this.settings.types?.includes(col.type))
- ?? false;
- return isMatch;
- }
-
- const matches: Array<number> = [];
-
- cols.forEach((col, v) => {
- if (colMatchesField(col)) {
- matches.push(v);
- }
- });
-
- return matches;
- };
-
- updateRenderedDoc = (oldDoc?: Doc): Doc => {
- const opts = this.settings.opts;
-
- if (!this.contentType) { this.contentType = FieldContentType.STRING };
-
- let doc: Doc;
-
- switch (this.contentType) {
- case FieldContentType.STRING:
- doc = Docs.Create.TextDocument(String(this.content), {
- title: this.title,
- text_fontColor: oldDoc ? String(oldDoc.color) : opts.color,
- contentBold: oldDoc ? Boolean(oldDoc.fontBold) : opts.fontBold,
- textTransform: oldDoc ? String(oldDoc.fontTransform) : opts.fontTransform,
- color: oldDoc ? String(oldDoc.color) : opts.color,
- _text_fontSize: `${FieldUtils.calculateFontSize(this.dimensions.width, this.dimensions.height, String(this.content), true)}`
- });
- FieldUtils.applyBasicOpts(doc, this.dimensions, this.settings, oldDoc);
- break;
- case FieldContentType.IMAGE:
- doc = Docs.Create.ImageDocument(String(this.content), {
- title: this.title,
- _layout_fitWidth: false,
- });
- FieldUtils.applyBasicOpts(doc, this.dimensions, this.settings, oldDoc);
- break;
- }
-
- this.renderedDocument = doc;
-
- return doc;
- };
-} \ No newline at end of file
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![1](template))
+ }>
+ <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![1](template))}>
+ <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
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/Template.ts b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Template.ts
new file mode 100644
index 000000000..fd87ae973
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Template.ts
@@ -0,0 +1,220 @@
+import { makeAutoObservable } from 'mobx';
+import { Col } from './DocCreatorMenu';
+import { TemplateFieldType, TemplateLayouts } from './TemplateBackend';
+import { DynamicField } from './TemplateFieldTypes/DynamicField';
+import { FieldSettings, TemplateField, ViewType } from './TemplateFieldTypes/TemplateField';
+import { Conditional } from './Backend/TemplateManager';
+import { ImageField } from '../../../../../fields/URLField';
+import { Doc } from '../../../../../fields/Doc';
+import { TemplateDataField } from './TemplateFieldTypes/DataField';
+
+export class Template {
+ _mainField: DynamicField;
+
+ private dataFields: TemplateDataField[] = [];
+
+ /**
+ * A Template can be created from a description of its fields (FieldSettings) or from a DynamicField
+ * @param definition definition of template as settings or DynamicField
+ */
+ constructor(definition: FieldSettings | DynamicField) {
+ makeAutoObservable(this);
+ this._mainField = definition instanceof DynamicField ? definition : this.setupMainField(definition);
+ }
+
+ get childFields(): TemplateField[] {
+ return this._mainField?.getSubfields ?? [];
+ }
+ get allFields(): TemplateField[] {
+ return this._mainField?.getAllSubfields ?? [];
+ }
+ get contentFields(): TemplateField[] {
+ return this.allFields.filter(field => field.isContentField);
+ }
+ get doc() {
+ return this._mainField?.renderedDoc;
+ }
+ get title() {
+ return this._mainField?.getTitle();
+ }
+
+ get descriptionSummary(): string {
+ let summary: string = '';
+ this.contentFields.forEach(field => {
+ summary += `--- Field #${field.getID} (title: ${field.getTitle()}): ${field.getDescription ?? ''} ---`;
+ });
+ return summary;
+ }
+
+ get compiledContent(): string {
+ let summary: string = '';
+ this.contentFields.forEach(field => {
+ summary += `--- Field #${field.getID} (title: ${field.getTitle()}): ${field.getContent() ?? ''} ---`;
+ });
+ return summary;
+ }
+
+ cleanup = () => {
+ //dispose each subfields disposers, etc.
+ };
+
+ clone = (withContent: boolean = false) => {
+ const clone = new Template(this._mainField?.makeClone(undefined, withContent) ?? TemplateLayouts.BasicSettings);
+ this.dataFields.forEach(field => clone.addDataField(field.title));
+ return clone;
+ };
+
+ getRenderedDoc = () => this.doc;
+
+ getFieldByID = (id: number): TemplateField => this.allFields.filter(field => field.getID === id)[0];
+
+ getFieldByTitle = (title: string) => [...this.allFields, ...this.dataFields].filter(field => field.getTitle() === title)[0];
+
+ setupMainField = (templateInfo: FieldSettings) => TemplateField.CreateField(templateInfo, 1, undefined) as DynamicField;
+
+ printFieldInfo = () => {
+ this.allFields.forEach(field => {
+ const doc = field.renderedDoc;
+ });
+ };
+
+ assignColToField = (fieldID: number, col: Col) => {
+ const field = this.getFieldByID(fieldID);
+ field.setContent(col.defaultContent ?? '', col.type === TemplateFieldType.VISUAL ? ViewType.IMG : ViewType.TEXT);
+ field.setTitle(col.title);
+ }
+
+ addDataField = (title: string, content?: string) => {
+ this.dataFields.push(new TemplateDataField(title, content));
+ }
+
+ removeDataField = (title: string) => {
+ this.dataFields = this.dataFields.filter(field => !(field.title === title));
+ }
+
+ isValidTemplate = (cols: Col[]) => {
+ const maxMatches = this.maxMatches(this.getMatches(cols));
+ return maxMatches === this.contentFields.length && this.title !== 'template_framework';
+ };
+
+ applyConditionalLogicToField = (field: TemplateField | TemplateDataField, logic: Record<string, Conditional[]>) => {
+ if (field instanceof DynamicField) return;
+ const fieldStatements: Conditional[] = logic[field.getTitle()];
+ const content = field.getContent()
+ fieldStatements && fieldStatements.forEach(statement => {
+ console.log(statement);
+ if (content === statement.condition) {
+ if (statement.target === 'Template') {
+ this._mainField.renderedDoc![statement.attribute] = statement.value;
+ Object.assign(this._mainField.settings.opts, {[statement.attribute]: statement.value});
+ } else {
+ const targetField: TemplateField = this.getFieldByTitle(statement.target) as TemplateField;
+ if (targetField) {
+ targetField.renderedDoc![statement.attribute] = statement.value;
+ Object.assign(targetField.settings.opts, {[statement.attribute]: statement.value});
+ }
+ }
+ }
+ })
+ }
+
+ applyConditionalLogic = (logic: Record<string, Conditional[]>) => {
+ const fields: (TemplateField | TemplateDataField)[] = [...this.allFields, ...this.dataFields];
+ fields.forEach(field => this.applyConditionalLogicToField(field, logic));
+ }
+
+ setImageAsBackground(url: string, makeTransparent: boolean = false) {
+ const fieldSettings: FieldSettings = {
+ tl: [-1, -1],
+ br: [1, 1],
+ opts: {},
+ viewType: ViewType.IMG,
+ }
+
+ const field: TemplateField = TemplateField.CreateField(fieldSettings, Math.random() * 100 + 100, this._mainField);
+ field.setContent(url);
+
+ if (makeTransparent) {
+ this.allFields.forEach(field => {
+ field.updateDocSetting('backgroundColor', 'transparent');
+ field.updateDocSetting('borderWidth', '0');
+ });
+ }
+
+ this._mainField.makeBackgroundField(field);
+ }
+
+ /**
+ * This function is just a hack for now to get around weird document icon stuff (specifically it misses the background)
+ */
+ setMatteBackground(makeTransparent: boolean = false) {
+ if (this._mainField.hasBackground) {
+ return;
+ }
+
+ const fieldSettings: FieldSettings = {
+ tl: [-1, -1],
+ br: [1, 1],
+ opts: {backgroundColor: String(this._mainField.renderedDoc!.backgroundColor)},
+ viewType: ViewType.TEXT,
+ }
+
+ const field: TemplateField = TemplateField.CreateField(fieldSettings, Math.random() * 100 + 100, this._mainField);
+
+ if (makeTransparent) {
+ this.allFields.forEach(field => {
+ field.updateDocSetting('backgroundColor', 'transparent');
+ field.updateDocSetting('borderWidth', '0');
+ });
+ }
+
+ this._mainField.makeBackgroundField(field);
+ }
+
+ getMatches = (cols: Col[]): number[][] => {
+ const numFields = this.contentFields.length;
+
+ if (cols.length !== numFields) return [];
+
+ const matches: number[][] = Array(numFields)
+ .fill([])
+ .map(() => []);
+
+ this.contentFields.forEach((field, i) => (matches[i] = field.matches(cols)));
+
+ return matches;
+ };
+
+ maxMatches = (matches: number[][]) => {
+ if (matches.length === 0) return 0;
+
+ const fieldsCt = this.contentFields.length;
+ const used: boolean[] = Array(fieldsCt).fill(false);
+ const mt: number[] = Array(fieldsCt).fill(-1);
+
+ const augmentingPath = (v: number): boolean => {
+ if (!used[v]) {
+ 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;
+ };
+}
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/Template.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Template.tsx
deleted file mode 100644
index 0a5097d4a..000000000
--- a/src/client/views/nodes/DataVizBox/DocCreatorMenu/Template.tsx
+++ /dev/null
@@ -1,139 +0,0 @@
-import { Doc, FieldType } from "../../../../../fields/Doc";
-import { Col } from "./DocCreatorMenu";
-import { DynamicField } from "./FieldTypes/DynamicField";
-import { Field, FieldSettings, ViewType } from "./FieldTypes/Field";
-import { } from "./FieldTypes/FieldUtils";
-import { } from "./FieldTypes/StaticField";
-
-export class Template {
-
- mainField: DynamicField;
- settings: FieldSettings;
-
- constructor(templateInfo: FieldSettings) {
- this.mainField = this.setupMainField(templateInfo);
- this.settings = templateInfo;
- }
-
- get childFields(): Field[] { return this.mainField.getSubfields };
- get allFields(): Field[] { return this.mainField.getAllSubfields };
- get contentFields(): Field[] { return this.allFields.filter(field => field.getViewType === ViewType.STATIC) };
- get doc(){ return this.mainField.renderedDoc(); };
-
- cloneBase = () => {
- const clone: Template = new Template(this.settings);
- clone.allFields.forEach(field => {
- const matchingField: Field = this.allFields.filter(f => f.getID === field.getID)[0];
- matchingField.applyAttributes(field);
- })
- return clone;
- }
-
- getRenderedDoc = () => {
- const doc: Doc = this.mainField.renderedDoc();
- this.contentFields.forEach(field => {
- const title: string = field.getTitle();
- const val: FieldType = field.getContent() as FieldType;
- if (!title || !val) return;
- doc[title] = val;
- });
- return doc;
- }
-
- getFieldByID = (id: number): Field => {
- return this.allFields.filter(field => field.getID === id)[0];
- }
-
- getFieldByTitle = (title: string) => {
- return this.allFields.filter(field => field.getTitle() === title)[0];
- }
-
- setupMainField = (templateInfo: FieldSettings) => {
- return new DynamicField(templateInfo, 1);
- }
-
- get descriptionSummary(): string {
- let summary: string = '';
- this.contentFields.forEach(field => {
- summary += `--- Field #${field.getID} (title: ${field.getTitle()}): ${field.getDescription ?? ''} ---`;
- });
- return summary;
- }
-
- get compiledContent(): string {
- let summary: string = '';
- this.contentFields.forEach(field => {
- summary += `--- Field #${field.getID} (title: ${field.getTitle()}): ${field.getContent() ?? ''} ---`;
- });
- return summary;
- }
-
- renderUpdates = () => {
- this.allFields.forEach(field => {
- field.updateRenderedDoc(field.renderedDoc());
- });
- };
-
- resetToBase = () => {
- this.allFields.forEach(field => {
- field.updateRenderedDoc();
- })
- }
-
- isValidTemplate = (cols: Col[]) => {
- const matches: number[][] = this.getMatches(cols);
- const maxMatches: number = this.maxMatches(matches);
- return maxMatches === this.contentFields.length;
- }
-
- getMatches = (cols: Col[]): number[][] => {
- const numFields = this.contentFields.length;
-
- if (cols.length !== numFields) return [];
-
- const matches: number[][] = Array(numFields)
- .fill([])
- .map(() => []);
-
- this.contentFields.forEach((field, i) => {
- matches[i] = (field.matches(cols));
- });
-
- return matches;
- }
-
- maxMatches = (matches: number[][]) => {
- if (matches.length === 0) return 0;
-
- const fieldsCt = this.contentFields.length;
- 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;
- };
-
-} \ No newline at end of file
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateBackend.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateBackend.ts
index d3282eda3..26fd3a8fc 100644
--- a/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateBackend.tsx
+++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateBackend.ts
@@ -1,10 +1,10 @@
-import { FieldSettings, ViewType } from "./FieldTypes/Field";
-import { } from "./FieldTypes/StaticField";
+import { FieldSettings, ViewType } from './TemplateFieldTypes/TemplateField';
export enum TemplateFieldType {
TEXT = 'text',
VISUAL = 'visual',
UNSET = 'unset',
+ DATA = 'data',
}
export enum TemplateFieldSize {
@@ -20,6 +20,14 @@ export class TemplateLayouts {
return Object.values(TemplateLayouts);
}
+ public static BasicSettings: FieldSettings = {
+ title: 'template_framework',
+ tl: [0, 0],
+ br: [400, 700],
+ viewType: ViewType.FREEFORM,
+ opts: {},
+ };
+
public static FourField001: FieldSettings = {
title: 'fourfield001',
tl: [0, 0],
@@ -27,7 +35,7 @@ export class TemplateLayouts {
viewType: ViewType.FREEFORM,
opts: {
backgroundColor: '#C0B887',
- cornerRounding: .05,
+ _layout_borderRounding: '.05',
//borderColor: '#6B461F',
//borderWidth: '12',
},
@@ -41,9 +49,9 @@ export class TemplateLayouts {
description: 'A title field for very short text that contextualizes the content.',
opts: {
backgroundColor: 'transparent',
- color: '#F1F0E9',
- contentXCentering: 'h-center',
- fontBold: true,
+ text_fontColor: '#F1F0E9',
+ hCentering: 'h-center',
+ contentBold: true,
},
},
{
@@ -54,9 +62,9 @@ export class TemplateLayouts {
sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE],
description: 'The main focus of the template; could be an image, long text, etc.',
opts: {
- cornerRounding: .05,
+ _layout_borderRounding: '.05',
borderColor: '#8F5B25',
- borderWidth: '6',
+ borderWidth: 6,
backgroundColor: '#CECAB9',
},
},
@@ -69,8 +77,8 @@ export class TemplateLayouts {
description: 'A caption for field #2, very short text.',
opts: {
backgroundColor: 'transparent',
- contentXCentering: 'h-center',
- color: '#F1F0E9',
+ hCentering: 'h-center',
+ text_fontColor: '#F1F0E9',
},
},
{
@@ -81,9 +89,9 @@ export class TemplateLayouts {
sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE],
description: 'A medium-sized field for medium/long text.',
opts: {
- cornerRounding: .05,
+ _layout_borderRounding: '.05',
borderColor: '#8F5B25',
- borderWidth: '6',
+ borderWidth: 6,
backgroundColor: '#CECAB9',
},
},
@@ -93,7 +101,7 @@ export class TemplateLayouts {
public static FourField002: FieldSettings = {
title: 'fourfield002',
viewType: ViewType.FREEFORM,
- tl: [0,0],
+ tl: [0, 0],
br: [425, 778],
opts: {
backgroundColor: '#242425',
@@ -107,10 +115,10 @@ export class TemplateLayouts {
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',
+ borderWidth: 8,
borderColor: '#F8E71C',
backgroundColor: '#242425',
- color: 'white',
+ text_fontColor: 'white',
},
},
{
@@ -122,9 +130,9 @@ export class TemplateLayouts {
description: 'A tiny field for just a word or two of plain text.',
opts: {
backgroundColor: 'transparent',
- color: 'white',
- contentXCentering: 'h-center',
- fontTransform: 'uppercase',
+ text_fontColor: 'white',
+ hCentering: 'h-center',
+ text_transform: 'uppercase',
},
},
{
@@ -136,9 +144,9 @@ export class TemplateLayouts {
description: 'A tiny field for just a word or two of plain text.',
opts: {
backgroundColor: 'transparent',
- color: 'white',
- contentXCentering: 'h-center',
- fontTransform: 'uppercase',
+ text_fontColor: 'white',
+ hCentering: 'h-center',
+ text_transform: 'uppercase',
},
},
{
@@ -149,9 +157,9 @@ export class TemplateLayouts {
sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE],
description: 'A medium to large-sized field suitable for longer text that should contextualize field 1.',
opts: {
- borderWidth: '8',
+ borderWidth: 8,
borderColor: '#F8E71C',
- color: 'white',
+ text_fontColor: 'white',
backgroundColor: '#242425',
},
},
@@ -161,7 +169,7 @@ export class TemplateLayouts {
br: [-0.525, 0.075],
opts: {
backgroundColor: '#F8E71C',
- rotation: 45,
+ _rotation: 45,
},
},
{
@@ -170,7 +178,7 @@ export class TemplateLayouts {
br: [-0.2175, 0.0245],
opts: {
backgroundColor: '#F8E71C',
- rotation: 45,
+ _rotation: 45,
},
},
{
@@ -179,7 +187,7 @@ export class TemplateLayouts {
br: [0.045, 0.0245],
opts: {
backgroundColor: '#F8E71C',
- rotation: 45,
+ _rotation: 45,
},
},
{
@@ -188,7 +196,7 @@ export class TemplateLayouts {
br: [0.3075, 0.0245],
opts: {
backgroundColor: '#F8E71C',
- rotation: 45,
+ _rotation: 45,
},
},
{
@@ -197,7 +205,7 @@ export class TemplateLayouts {
br: [0.8, 0.075],
opts: {
backgroundColor: '#F8E71C',
- rotation: 45,
+ _rotation: 45,
},
},
],
@@ -266,8 +274,8 @@ export class TemplateLayouts {
public static FourField004: FieldSettings = {
title: 'fourfield04',
viewType: ViewType.FREEFORM,
- tl: [0,0],
- br: [414,583],
+ tl: [0, 0],
+ br: [414, 583],
opts: {
backgroundColor: '#6CCAF0',
//borderColor: '#1088C3',
@@ -283,9 +291,9 @@ export class TemplateLayouts {
description: 'A tiny field for just a word or two of plain text.',
opts: {
backgroundColor: '#E2B4F5',
- borderWidth: '9',
+ borderWidth: 9,
borderColor: '#9222F1',
- contentXCentering: 'h-center',
+ hCentering: 'h-center',
},
},
{
@@ -297,9 +305,9 @@ export class TemplateLayouts {
description: 'A tiny field for just a word or two of plain text.',
opts: {
backgroundColor: '#F5B4DD',
- borderWidth: '9',
+ borderWidth: 9,
borderColor: '#E260F3',
- contentXCentering: 'h-center',
+ hCentering: 'h-center',
},
},
{
@@ -310,7 +318,7 @@ export class TemplateLayouts {
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',
+ borderWidth: 16,
borderColor: '#A2BD77',
},
},
@@ -322,7 +330,7 @@ export class TemplateLayouts {
sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE],
description: 'A medium to large field for text that describes the visual content above',
opts: {
- borderWidth: '9',
+ borderWidth: 9,
borderColor: '#F0D601',
backgroundColor: '#F3F57D',
},
@@ -334,7 +342,7 @@ export class TemplateLayouts {
opts: {
backgroundColor: 'transparent',
borderColor: '#007C0C',
- borderWidth: '10',
+ borderWidth: 10,
},
},
],
@@ -343,218 +351,229 @@ export class TemplateLayouts {
public static FourField005: FieldSettings = {
title: 'fourfield05',
viewType: ViewType.FREEFORM,
- tl: [0,0],
- br: [400,550],
+ tl: [0, 0],
+ br: [400, 514],
opts: {
backgroundColor: '#95A575',
},
subfields: [
{
viewType: ViewType.STATIC,
- tl: [-0.9, -.925],
- br: [-.075, -.775],
+ tl: [-0.9, -0.925],
+ br: [-0.075, -0.775],
types: [TemplateFieldType.TEXT],
sizes: [TemplateFieldSize.TINY, TemplateFieldSize.SMALL],
description: 'A small text field for a title or word(s) that categorize the rest of the content.',
opts: {
borderColor: '#3B4A2C',
- borderWidth: '8',
- contentXCentering: "h-center",
+ borderWidth: 8,
+ hCentering: 'h-center',
backgroundColor: '#B8DC90',
},
},
{
viewType: ViewType.STATIC,
- tl: [.075, -.925],
- br: [.9, -.775],
+ tl: [0.075, -0.925],
+ br: [0.9, -0.775],
types: [TemplateFieldType.TEXT],
sizes: [TemplateFieldSize.TINY, TemplateFieldSize.SMALL],
description: 'A small text field for a title that categorizes the rest of the content.',
opts: {
borderColor: '#3B4A2C',
- borderWidth: '8',
- contentXCentering: "h-center",
+ borderWidth: 8,
+ hCentering: 'h-center',
backgroundColor: '#B8DC90',
},
},
{
viewType: ViewType.DEC,
- tl: [-.82, -.4],
- br: [-.5, -.2],
+ tl: [-0.82, -0.4],
+ br: [-0.5, -0.2],
opts: {
backgroundColor: '#94B058',
borderColor: '#3B4A2C',
- borderWidth: '8',
+ borderWidth: 8,
},
},
{
viewType: ViewType.STATIC,
- tl: [-0.66, -.65],
- br: [0.66, .25],
+ tl: [-0.66, -0.65],
+ br: [0.66, 0.25],
types: [TemplateFieldType.VISUAL],
sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE],
description: 'A medium to large field in the center of the template, for the main visual content.',
opts: {
borderColor: '#3B4A2C',
- borderWidth: '8',
+ borderWidth: 8,
backgroundColor: '#B8DC90',
},
},
{
viewType: ViewType.STATIC,
- tl: [-.875, .425],
- br: [0.875, .925],
+ tl: [-0.875, 0.425],
+ br: [0.875, 0.925],
types: [TemplateFieldType.TEXT],
sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE],
description: 'A medium to large field at the bottom of the template, for the main text content.',
opts: {
borderColor: '#3B4A2C',
- borderWidth: '8',
- contentXCentering: "h-center",
+ borderWidth: 8,
+ hCentering: 'h-center',
backgroundColor: '#B8DC90',
},
},
{
viewType: ViewType.DEC,
- tl: [-1.1, -.62],
- br: [-.9, -.5],
+ tl: [-1.1, -0.62],
+ br: [-0.9, -0.5],
opts: {
backgroundColor: '#7A9D31',
borderColor: '#3B4A2C',
- borderWidth: '8',
+ borderWidth: 8,
},
},
{
viewType: ViewType.DEC,
tl: [-1.1, 0],
- br: [-.9, .15],
+ br: [-0.9, 0.15],
opts: {
backgroundColor: '#94B058',
borderColor: '#3B4A2C',
- borderWidth: '8',
+ borderWidth: 8,
},
},
{
viewType: ViewType.DEC,
- tl: [-.93, -.265],
- br: [-.715, -.125],
+ tl: [-0.93, -0.265],
+ br: [-0.715, -0.125],
opts: {
backgroundColor: '#728745',
borderColor: '#3B4A2C',
- borderWidth: '8',
+ borderWidth: 8,
},
},
{
viewType: ViewType.DEC,
- tl: [.7, -.45],
- br: [.85, -.3],
+ tl: [0.7, -0.45],
+ br: [0.85, -0.3],
opts: {
backgroundColor: '#7A9D31',
borderColor: '#3B4A2C',
- borderWidth: '8',
+ borderWidth: 8,
},
},
{
viewType: ViewType.DEC,
- tl: [.8, .03],
- br: [1.2, .33],
+ tl: [0.8, 0.03],
+ br: [1.2, 0.33],
opts: {
backgroundColor: '#728745',
borderColor: '#3B4A2C',
- borderWidth: '8',
+ borderWidth: 8,
},
},
{
viewType: ViewType.DEC,
- tl: [.875, -.13],
- br: [1.2, .12],
+ tl: [0.875, -0.13],
+ br: [1.2, 0.12],
opts: {
backgroundColor: '#94B058',
borderColor: '#3B4A2C',
- borderWidth: '8',
+ borderWidth: 8,
},
},
- ]
- }
+ ],
+ };
public static FourFieldCarousel: FieldSettings = {
title: 'title_fourfieldcarousel',
viewType: ViewType.FREEFORM,
- tl:[0,0],
- br:[500, 600],
+ tl: [0, 0],
+ br: [500, 600],
opts: {
- backgroundColor: '#DDD3A9',
+ backgroundColor: '#D7CBAB',
},
subfields: [
{
viewType: ViewType.STATIC,
- tl: [-0.8, -.9],
- br: [0.8, -.5],
+ tl: [-0.8, -0.9],
+ br: [0.8, -0.5],
types: [TemplateFieldType.TEXT],
sizes: [TemplateFieldSize.TINY, TemplateFieldSize.SMALL],
description: 'A small text field for a title that categorizes the rest of the content.',
opts: {
- borderColor: 'yellow',
- borderWidth: '8',
- contentXCentering: "h-center",
+ hCentering: 'h-center',
backgroundColor: 'transparent',
+ text_transform: 'uppercase',
},
},
{
viewType: ViewType.CAROUSEL3D,
- tl: [-0.9, -.3],
- br: [0.9, .9],
+ tl: [-0.9, -0.5],
+ br: [0.9, 0.25],
opts: {
- borderColor: 'yellow',
- borderWidth: '8',
- backgroundColor: 'transparent',
+ borderColor: '#847F69',
+ borderWidth: 8,
+ backgroundColor: '#C8BA94',
},
subfields: [
{
viewType: ViewType.STATIC,
- tl: [-.3, -.6],
- br: [.3, .6],
+ tl: [-0.4, -0.6],
+ br: [0.4, 0.6],
types: [TemplateFieldType.VISUAL, TemplateFieldType.TEXT],
sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE],
description: 'A medium to large field for content that will share central focus with other content in the carousel.',
opts: {
- borderColor: 'yellow',
- borderWidth: '8',
+ //borderColor: 'yellow',
+ //borderWidth: '8',
},
},
{
viewType: ViewType.STATIC,
- tl: [-.3, -.6],
- br: [.3, .6],
+ tl: [-0.4, -0.6],
+ br: [0.4, 0.6],
types: [TemplateFieldType.VISUAL, TemplateFieldType.TEXT],
sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE],
description: 'A medium to large field for content that will share central focus with other content in the carousel.',
opts: {
- borderColor: 'black',
- borderWidth: '8',
+ //borderColor: 'black',
+ //borderWidth: '8',
},
},
{
viewType: ViewType.STATIC,
- tl: [-.3, -.6],
- br: [.3, .6],
+ tl: [-0.4, -0.6],
+ br: [0.4, 0.6],
types: [TemplateFieldType.VISUAL, TemplateFieldType.TEXT],
sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE],
description: 'A medium to large field for content that will share central focus with other content in the carousel.',
opts: {
- borderColor: 'yellow',
- borderWidth: '8',
+ //borderColor: 'yellow',
+ //borderWidth: '8',
},
},
- ]
+ ],
},
- ]
- }
+ {
+ viewType: ViewType.STATIC,
+ tl: [-0.9, 0.35],
+ br: [0.9, 0.9],
+ types: [TemplateFieldType.TEXT],
+ sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE],
+ description: 'A medium text field for a description of the content in the carousel.',
+ opts: {
+ hCentering: 'h-center',
+ backgroundColor: 'transparent',
+ },
+ },
+ ],
+ };
public static ThreeField001: FieldSettings = {
title: 'threefield001',
viewType: ViewType.FREEFORM,
- tl: [0,0],
+ tl: [0, 0],
br: [575, 770],
opts: {
backgroundColor: '#DDD3A9',
@@ -567,23 +586,23 @@ export class TemplateLayouts {
description: 'A medium to large field for visual content that is the central focus.',
opts: {
borderColor: 'yellow',
- borderWidth: '8',
+ borderWidth: 8,
backgroundColor: '#DDD3A9',
- rotation: 45,
+ _rotation: 45,
},
subfields: [
{
- viewType: ViewType.STATIC,
- tl: [-1.25, -1.25],
- br: [1.25, 1.25],
- 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: {
- rotation: -45,
+ viewType: ViewType.STATIC,
+ tl: [-1.25, -1.25],
+ br: [1.25, 1.25],
+ 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: {
+ _rotation: -45,
+ },
},
- },
- ]
+ ],
},
{
viewType: ViewType.STATIC,
@@ -594,7 +613,7 @@ export class TemplateLayouts {
description: 'A very small text field for one to a few words. A good caption for the image.',
opts: {
backgroundColor: 'transparent',
- contentXCentering: 'h-center',
+ hCentering: 'h-center',
},
},
{
@@ -606,7 +625,7 @@ export class TemplateLayouts {
description: 'A medium to large text field for a thorough description of the image. ',
opts: {
backgroundColor: 'transparent',
- color: 'white',
+ text_fontColor: 'white',
},
},
{
@@ -615,18 +634,18 @@ export class TemplateLayouts {
br: [1.8, -0.66],
opts: {
backgroundColor: '#CEB155',
- rotation: 45,
+ _rotation: 45,
},
subfields: [
{
viewType: ViewType.DEC,
- tl: [-1, -.7],
- br: [1, -.625],
+ tl: [-1, -0.7],
+ br: [1, -0.625],
opts: {
backgroundColor: 'yellow',
},
},
- ]
+ ],
},
{
viewType: ViewType.FREEFORM,
@@ -634,18 +653,18 @@ export class TemplateLayouts {
br: [-0.2, -0.66],
opts: {
backgroundColor: '#CEB155',
- rotation: 135,
+ _rotation: 135,
},
subfields: [
{
viewType: ViewType.DEC,
- tl: [-1, -.7],
- br: [1, -.625],
+ tl: [-1, -0.7],
+ br: [1, -0.625],
opts: {
backgroundColor: 'yellow',
},
},
- ]
+ ],
},
{
viewType: ViewType.FREEFORM,
@@ -653,18 +672,18 @@ export class TemplateLayouts {
br: [1.66, 1.25],
opts: {
backgroundColor: '#CEB155',
- rotation: 135,
+ _rotation: 135,
},
subfields: [
{
viewType: ViewType.DEC,
- tl: [-1, -.7],
- br: [1, -.625],
+ tl: [-1, -0.7],
+ br: [1, -0.625],
opts: {
backgroundColor: 'yellow',
},
},
- ]
+ ],
},
{
viewType: ViewType.FREEFORM,
@@ -672,18 +691,18 @@ export class TemplateLayouts {
br: [-0.33, 1.25],
opts: {
backgroundColor: '#CEB155',
- rotation: 45,
+ _rotation: 45,
},
subfields: [
{
viewType: ViewType.DEC,
- tl: [-1, -.7],
- br: [1, -.625],
+ tl: [-1, -0.7],
+ br: [1, -0.625],
opts: {
backgroundColor: 'yellow',
},
},
- ]
+ ],
},
],
};
@@ -691,7 +710,7 @@ export class TemplateLayouts {
public static ThreeField002: FieldSettings = {
title: 'threefield002',
viewType: ViewType.FREEFORM,
- tl: [0,0],
+ tl: [0, 0],
br: [477, 662],
opts: {
backgroundColor: '#9E9C95',
@@ -705,7 +724,7 @@ export class TemplateLayouts {
sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE],
description: 'A medium to large visual field for the main content of the template',
opts: {
- borderWidth: '15',
+ borderWidth: 15,
borderColor: '#E0E0DA',
},
},
@@ -718,10 +737,10 @@ export class TemplateLayouts {
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',
+ text_fontColor: '#AF0D0D',
+ text_transform: 'uppercase',
+ contentBold: true,
+ hCentering: 'h-left',
},
},
{
@@ -733,8 +752,8 @@ export class TemplateLayouts {
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',
+ text_fontColor: 'black',
+ hCentering: 'h-right',
},
},
{
@@ -747,6 +766,4 @@ export class TemplateLayouts {
},
],
};
-}
-
-
+} \ No newline at end of file
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/DataField.ts b/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/DataField.ts
new file mode 100644
index 000000000..aaa475bed
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/DataField.ts
@@ -0,0 +1,21 @@
+
+import { Template } from "../Template";
+import { TemplateField, ViewType } from "./TemplateField";
+
+export class TemplateDataField {
+
+ viewType: ViewType = ViewType.NONE;
+
+ title: string = '';
+ content: string | undefined;
+
+ constructor(title: string, content?: string) {
+ this.title = title;
+ this.content = content;
+ }
+
+ setContent(content: string, viewType?: ViewType) { this.content = content }
+ getContent() { return this.content }
+
+ getTitle() { return this.title }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/DecorationField.ts b/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/DecorationField.ts
new file mode 100644
index 000000000..98a9dc7a6
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/DecorationField.ts
@@ -0,0 +1,3 @@
+import { DynamicField } from './DynamicField';
+
+export class DecorationField extends DynamicField {}
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/DynamicField.ts b/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/DynamicField.ts
new file mode 100644
index 000000000..1576dd240
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/DynamicField.ts
@@ -0,0 +1,136 @@
+import { reaction } from 'mobx';
+import { IDisposer } from 'mobx-utils';
+import { Doc, DocListCast } from '../../../../../../fields/Doc';
+import { DocData } from '../../../../../../fields/DocSymbols';
+import { List } from '../../../../../../fields/List';
+import { NumCast } from '../../../../../../fields/Types';
+import { Docs } from '../../../../../documents/Documents';
+import { DocumentType } from '../../../../../documents/DocumentTypes';
+import { FieldSettings, TemplateField, ViewType } from './TemplateField';
+
+export class DynamicField extends TemplateField {
+ protected _disposers: { [name: string]: IDisposer } = {};
+ protected _subfields: TemplateField[] = [];
+ protected backgroundField: TemplateField | undefined;
+
+ get getSubfields() {
+ return this._subfields;
+ }
+ get getAllSubfields(): TemplateField[] {
+ return this.getSubfields.flatMap(field => [field, ...((field as DynamicField).getAllSubfields ?? [])]);
+ }
+
+ get hasBackground() {
+ return this.backgroundField !== undefined;
+ }
+
+ handleFieldUpdate = (newDocsList: Doc[]) => {
+ const currRenderedDocs = new Set(this.getSubfields.filter(field => field.renderedDoc).map(field => field.renderedDoc!));
+ newDocsList.forEach(doc => !currRenderedDocs.has(doc) && this.addFieldFromDoc(doc));
+ currRenderedDocs.forEach(doc => {
+ if (!newDocsList.includes(doc)) {
+ this._subfields.forEach(field => field.renderedDoc === doc && this.removeField(field));
+ }
+ });
+ };
+
+ addFieldFromDoc = (doc: Doc) => {
+ const par = this._renderDoc;
+ const settings: FieldSettings = {
+ tl: [Number(doc._x) / NumCast(par?._width, 1), Number(doc?._y) / NumCast(par?._height, 1)],
+ br: [(Number(doc._x) + Number(doc._width)) / NumCast(par?._width, 1), (Number(doc._y) + Number(doc._height)) / NumCast(par?._height, 1)],
+ viewType: doc.type === DocumentType.COL ? ViewType.FREEFORM : ViewType.STATIC,
+ opts: {},
+ };
+
+ this._subfields.push(TemplateField.CreateField(settings, this._subfields.length, this));
+ };
+
+ addField = (field: TemplateField, layer: number = 0) => {
+ if (!this._subfields.includes(field)) {
+ console.log('success')
+ console.log('subs: ', this._subfields)
+ this._subfields.splice(layer, 0, field);
+ console.log('subffelds: ', this._subfields)
+ }
+ };
+
+ dispose = () => Object.values(this._disposers).forEach(disposer => disposer?.());
+
+ removeField = (field: TemplateField) => {
+ // field.renderedDoc && this._renderDoc && Doc.RemoveDocFromList(this._renderDoc, undefined, field.renderedDoc);
+ this._subfields.splice(this._subfields.indexOf(field), 1);
+ (field as DynamicField).dispose?.();
+ };
+
+ // implement Field's abstract method for replacing a subfield with a new one
+ exchangeFields(newField: TemplateField, oldField: TemplateField) {
+ this._subfields.splice(this._subfields.indexOf(oldField), 1, newField);
+ this.refreshRenderedDoc();
+ }
+
+ get isContentField(): boolean {
+ return false;
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ setContent(content: string, type: ViewType) {}
+
+ getContent = () => '';
+
+ addChildToDocument = (doc: Doc) => this._renderDoc && Doc.SetContainer(doc, this._renderDoc);
+
+ makeBackgroundField = (field: TemplateField) => {
+ if (this.backgroundField && this.backgroundField !== field) {
+ this.removeField(this.backgroundField);
+ this.backgroundField = undefined;
+ }
+ if (field && field !== this.backgroundField) {
+ this.addField(field);
+ this.backgroundField = field;
+ }
+ this.refreshRenderedDoc();
+ }
+
+ matches = (): Array<number> => [];
+
+ makeClone(parent?: DynamicField, withContent: boolean = false) {
+ const dynClone = super.makeClone(parent) as DynamicField;
+ dynClone._subfields = this.getSubfields.map(field => {
+ if (field === this.backgroundField) {
+ console.log('background found')
+ const backgroundField: TemplateField = field.makeClone(dynClone, true);
+ dynClone.makeBackgroundField(backgroundField);
+ return backgroundField;
+ } else {
+ return field.makeClone(dynClone, withContent)
+ }
+ });
+ if (dynClone._renderDoc) {
+ dynClone._renderDoc[DocData].data = new List<Doc>(dynClone.getSubfields.filter(sub => sub.renderedDoc).map(sub => sub.renderedDoc!));
+ }
+ return dynClone;
+ }
+
+ initRenderDoc = (settings: FieldSettings) => {
+ this._disposers.fieldList = reaction(() => DocListCast(this._renderDoc?.[Doc.LayoutFieldKey(this._renderDoc)]), this.handleFieldUpdate);
+ this._subfields = settings.subfields?.map((fieldSettings, index) => {return TemplateField.CreateField(fieldSettings, index, this)}) || [];
+ const renderedSubfields = this._subfields.filter(field => field.renderedDoc).map(field => field.renderedDoc!);
+ settings.opts.title = settings.title;
+ this._renderDoc = (() => { switch (settings.viewType) {
+ case ViewType.CAROUSEL3D: return Docs.Create.Carousel3DDocument(renderedSubfields, settings.opts);
+ case ViewType.FREEFORM:
+ default: return Docs.Create.FreeformDocument(renderedSubfields, settings.opts);
+ }})(); // prettier-ignore
+ return this;
+ };
+
+ refreshRenderedDoc = () => {
+ const renderedSubfields = this._subfields.filter(field => field.renderedDoc).map(field => field.renderedDoc!);
+ this._renderDoc = (() => { switch (this.settings.viewType) {
+ case ViewType.CAROUSEL3D: return Docs.Create.Carousel3DDocument(renderedSubfields, this.settings.opts);
+ case ViewType.FREEFORM:
+ default: return Docs.Create.FreeformDocument(renderedSubfields, this.settings.opts);
+ }})();
+ }
+}
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/StaticContentField.ts b/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/StaticContentField.ts
new file mode 100644
index 000000000..2a8e4f09b
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/StaticContentField.ts
@@ -0,0 +1,63 @@
+import { FontSize } from '@dash/components';
+import { FieldResult } from '../../../../../../fields/Doc';
+import { DocData } from '../../../../../../fields/DocSymbols';
+import { RichTextField } from '../../../../../../fields/RichTextField';
+import { ImageField } from '../../../../../../fields/URLField';
+import { Docs } from '../../../../../documents/Documents';
+import { FieldSettings, TemplateField, ViewType } from './TemplateField';
+import { TemplateFieldUtils } from './TemplateFieldUtils';
+
+export abstract class StaticContentField extends TemplateField {
+ protected _content: string = '';
+
+ getContent = () => this._content ?? 'unset';
+ get isContentField(): boolean {
+ return true;
+ }
+ protected setDataContent(viewType: ViewType, fieldKey: string, data: FieldResult, content: string, type?: ViewType) {
+ super.setContent(content, type);
+
+ if (type === viewType || type === undefined) {
+ this._content = content;
+ this._renderDoc && (this._renderDoc[DocData][fieldKey] = data);
+ } else {
+ this.changeFieldType(type).setContent(content, type);
+ }
+ }
+}
+
+export class ImageTemplateField extends StaticContentField {
+ setContent(url: string, type?: ViewType) {
+ this.setDataContent(ViewType.IMG, 'data', new ImageField(url), url, type);
+ this._renderDoc!['backgroundColor'] = 'white';
+ }
+
+ initRenderDoc(settings: FieldSettings) {
+ settings.opts.title = settings.title ?? '';
+ settings.opts._layout_fitWidth = false;
+ this._renderDoc = Docs.Create.ImageDocument(this._content, settings.opts);
+ return this;
+ }
+
+ updateDocSetting(setting: string, newVal: string) {
+ if (this._renderDoc) this._renderDoc[setting] = newVal;
+ if (setting !== 'backgroundColor') {
+ const settings: {[s: string]: string } = {[setting]: newVal}
+ Object.assign(this.settings.opts, settings);
+ }
+ }
+}
+
+export class TextTemplateField extends StaticContentField {
+ setContent(text: string, type?: ViewType) {
+ const fontSize: number = TemplateFieldUtils.calculateFontSize(this._dimensions?.width ?? 10, this._dimensions?.height ?? 10, text, true);
+ this.setDataContent(ViewType.TEXT, 'text', RichTextField.textToRtf(text, undefined, {fontSize: fontSize}), text, type);
+ }
+
+ initRenderDoc(settings: FieldSettings) {
+ settings.opts.title = settings.title ?? '';
+ settings.opts.text_fontSize = TemplateFieldUtils.calculateFontSize(this._dimensions?.width ?? 10, this._dimensions?.height ?? 10, '', true) + '';
+ this._renderDoc = Docs.Create.TextDocument(this._content, settings.opts);
+ return this;
+ }
+}
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/TemplateField.ts b/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/TemplateField.ts
new file mode 100644
index 000000000..a1107caf3
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/TemplateField.ts
@@ -0,0 +1,174 @@
+/* eslint-disable no-use-before-define */
+import { Doc } from '../../../../../../fields/Doc';
+import { DocumentOptions } from '../../../../../documents/Documents';
+import { Conditional } from '../Backend/TemplateManager';
+import { Col } from '../DocCreatorMenu';
+import { Template } from '../Template';
+import { TemplateFieldSize, TemplateFieldType } from '../TemplateBackend';
+
+export abstract class TemplateField {
+ /**
+ * Creates and initializes a new TemplateField based on the settings and parameters
+ *
+ * implemented in FieldUtils and assigned in main (to avoid import cycles)
+ *
+ * @param settings - specification of the field type and other parameters
+ * @param index -
+ * @param parent - TemplateField that contains the new field
+ * @param sameId -
+ * @returns TemplateField
+ */
+ static CreateField: (settings: FieldSettings, index: number, parent: TemplateField | undefined, sameId?: boolean) => TemplateField;
+
+ protected _parent?: TemplateField;
+ protected _id: number;
+ protected _title: string = '';
+ protected _settings: FieldSettings;
+ protected _renderDoc: Doc | undefined;
+ protected _dimensions: FieldDimensions | undefined;
+
+ constructor(settings: FieldSettings, id: number = 1, parent?: TemplateField) {
+ this._id = id;
+ this._parent = parent;
+ this._settings = settings;
+ this._title = settings.title ?? '';
+ this._dimensions = this.getLocalDimensions(this._settings, this._parent?.getDimensions);
+ this.applyBasicOpts(this._dimensions, settings);
+ return this;
+ }
+
+ get renderedDoc() {
+ return this._renderDoc;
+ }
+ get getDimensions() {
+ return this._dimensions;
+ }
+ get getID() {
+ return this._id;
+ }
+ get getDescription(): string {
+ return this._settings?.description ?? '';
+ }
+ get viewType(): ViewType | undefined {
+ return this._settings?.viewType;
+ }
+
+ get settings(): FieldSettings {
+ return this._settings;
+ }
+
+ abstract get isContentField(): boolean;
+ abstract initRenderDoc(settings: FieldSettings): TemplateField;
+ abstract getContent(): string;
+
+ setContent(content: string, type?: ViewType) {
+ if (type) this._settings.viewType = type;
+ }
+
+ setTitle = (title: string) => {
+ this._title = title;
+ this.settings.title = title;
+ if (this._renderDoc) this._renderDoc.title = title
+ };
+ getTitle = () => this._title;
+
+ updateDocSetting(setting: string, newVal: string) {
+ if (this._renderDoc) this._renderDoc[setting] = newVal;
+ const settings: {[s: string]: string } = {[setting]: newVal}
+ Object.assign(this.settings.opts, settings);
+ }
+
+ makeClone(parent?: TemplateField, withContent: boolean = false) {
+ const settings: FieldSettings = structuredClone(this._settings);
+ const cloned = TemplateField.CreateField(settings, this._id, parent, true); // create a value for this.Document/subfields that we want to ignore
+ cloned.renderedDoc!.width = this.renderedDoc!.width;
+ cloned.renderedDoc!.height = this.renderedDoc!.height;
+ cloned.renderedDoc!.x = this.renderedDoc!.x;
+ cloned.renderedDoc!.y = this.renderedDoc!.y;
+ cloned.renderedDoc!.backgroundColor = this.renderedDoc!.backgroundColor;
+ cloned.setTitle(this._title);
+ cloned._dimensions = this._dimensions;
+ withContent && cloned.setContent(this.getContent());
+ return cloned;
+ }
+
+ exchangeFields(newField: TemplateField, oldField: TemplateField) {
+ throw new Error('Only DynamicField can exchange fields.' + newField._title + ' ' + oldField._title);
+ }
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ changeFieldType = (newType: ViewType): TemplateField => {
+ this._settings.viewType = newType;
+ const newField = TemplateField.CreateField(this._settings, this._id, this._parent, true);
+ this._parent?.exchangeFields(newField, this);
+ return newField;
+ };
+
+ matches = (cols: Col[]): number[] => {
+ const colMatchesField = (col: Col) => (this._settings?.sizes?.some(size => col.sizes?.includes(size)) && this._settings.types?.includes(col.type)) ?? false;
+
+ const matches: Array<number> = [];
+
+ cols.forEach((col, v) => {
+ if (colMatchesField(col)) {
+ matches.push(v);
+ }
+ });
+
+ return matches;
+ };
+
+ private getLocalDimensions = (coords: { tl: [number, number]; br: [number, number] }, parentDimensions?: FieldDimensions): FieldDimensions => {
+ if (!parentDimensions) {
+ return { width: coords.br[0] - coords.tl[0], height: coords.br[1] - coords.tl[1], coord: { x: coords.tl[0], y: coords.tl[1] } };
+ }
+ const l = (coords.tl[0] * parentDimensions.width) / 2;
+ const t = coords.tl[1] * parentDimensions.height / 2; //prettier-ignore
+ const r = (coords.br[0] * parentDimensions.width) / 2;
+ const b = coords.br[1] * parentDimensions.height / 2; //prettier-ignore
+ return { width: r-l, height: b-t, coord: { x: l, y: t } }; //prettier-ignore
+ };
+
+ private applyBasicOpts = (dimensions: FieldDimensions, settings: FieldSettings | undefined) => {
+ const opts: DocumentOptions = settings?.opts ?? {};
+ opts.isDefaultTemplateDoc ??= true;
+ opts._layout_hideScroll ??= true;
+ opts.x ??= dimensions.coord.x;
+ opts.y ??= dimensions.coord.y;
+ opts._height ??= dimensions.height;
+ opts._width ??= dimensions.width;
+ opts._nativeWidth ??= dimensions.width;
+ opts._nativeHeight ??= dimensions.height;
+ opts._layout_nativeDimEditable ??= true;
+ opts.layout_boxShadow = 'none';
+ };
+}
+
+export type FieldSettings = {
+ tl: [number, number];
+ br: [number, number];
+ opts: DocumentOptions;
+ types?: TemplateFieldType[];
+ sizes?: TemplateFieldSize[];
+ title?: string;
+ viewType: ViewType;
+ template?: Template;
+ subfields?: FieldSettings[];
+ description?: string;
+};
+
+export enum ViewType {
+ CAROUSEL3D = 'carousel3d',
+ FREEFORM = 'freeform',
+ STATIC = 'static',
+ DEC = 'decoration',
+ IMG = 'image',
+ TEXT = 'text',
+ NONE = 'none'
+}
+
+export type FieldDimensions = {
+ width: number;
+ height: number;
+ coord: { x: number; y: number };
+};
+
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/TemplateFieldUtils.ts b/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/TemplateFieldUtils.ts
new file mode 100644
index 000000000..b0b531b57
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/TemplateFieldUtils.ts
@@ -0,0 +1,71 @@
+import { DecorationField } from './DecorationField';
+import { DynamicField } from './DynamicField';
+import { ImageTemplateField, TextTemplateField } from './StaticContentField';
+import { FieldSettings, TemplateField, ViewType } from './TemplateField';
+
+export class TemplateFieldUtils {
+ /**
+ * Creates and initializes a new TemplateField based on the settings and parameters
+ *
+ * implements Field.initField ... see main.tsx
+ *
+ * @param settings - specification of the field type and other parameters
+ * @param index -
+ * @param parent - optional TemplateField that contains the new field
+ * @param sameId -
+ * @returns TemplateField
+ */
+ public static CreateField = (settings: FieldSettings, index: number, parent?: TemplateField, sameId: boolean = false): TemplateField =>
+ ((...args) => {
+ switch (settings?.viewType) {
+ case ViewType.FREEFORM:
+ case ViewType.CAROUSEL3D: return new DynamicField(...args).initRenderDoc(settings);
+ case ViewType.IMG: return new ImageTemplateField(...args).initRenderDoc(settings);
+ case ViewType.TEXT: return new TextTemplateField(...args).initRenderDoc(settings);
+ case ViewType.DEC: return new DecorationField(...args).initRenderDoc(settings);
+ default: return new TextTemplateField(...args).initRenderDoc(settings);
+ } // prettier-ignore
+ })(settings, sameId ? index : parent ? Number(`${parent.getID}${index}`) : 1, parent);
+
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ 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.7;
+
+ 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;
+
+ currFontSize += 1;
+ }
+
+ return currFontSize - 1;
+ };
+}
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateManager.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateManager.tsx
deleted file mode 100644
index 50ae4d72a..000000000
--- a/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateManager.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import { Col } from "./DocCreatorMenu";
-import { FieldSettings } from "./FieldTypes/Field";
-import { Template } from "./Template";
-
-export class TemplateManager {
-
- templates: Template[] = [];
-
- constructor(templateSettings: FieldSettings[]) {
- this.templates = this.initializeTemplates(templateSettings);
- }
-
- initializeTemplates = (templateSettings: FieldSettings[]): Template[] => {
- const initializedTemplates: Template[] = [];
- templateSettings.forEach(settings => initializedTemplates.push(new Template(settings)));
- return initializedTemplates;
- }
-
- getValidTemplates = (cols: Col[]): Template[] => {
- return this.templates.filter(template => template.isValidTemplate(cols));
- }
-} \ No newline at end of file
diff --git a/src/client/views/nodes/DataVizBox/components/TableBox.tsx b/src/client/views/nodes/DataVizBox/components/TableBox.tsx
index ad2731109..21bef3426 100644
--- a/src/client/views/nodes/DataVizBox/components/TableBox.tsx
+++ b/src/client/views/nodes/DataVizBox/components/TableBox.tsx
@@ -100,7 +100,7 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> {
return this._props.docView?.()?.screenToViewTransform().Scale || 1;
}
@computed get rowHeight() {
- return (this.viewScale * this._tableHeight) / this._tableDataIds.length;
+ return (this.viewScale * this._tableHeight) / (this._tableDataIds.length + 1); // add 1 for header row
}
@computed get startID() {
return this.rowHeight ? Math.max(Math.floor(this._scrollTop / this.rowHeight) - 1, 0) : 0;
@@ -400,8 +400,9 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> {
this._tableHeight = r?.getBoundingClientRect().height ?? 0;
}
})}>
- <div style={{ height: this.startID * Number(DATA_VIZ_TABLE_ROW_HEIGHT) }} />
+ {/* <div style={{ height: this.startID * Number(DATA_VIZ_TABLE_ROW_HEIGHT) }} /> */}
<thead>
+ <tr style={{ height: this.startID * Number(DATA_VIZ_TABLE_ROW_HEIGHT) }}></tr>
<tr>
{this.columns.map(col => (
<th
@@ -440,7 +441,7 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> {
<tbody>
{this._tableDataIds
.filter((rowId, i) => this.startID - 2 <= i && i <= this.endID + 2)
- ?.map(rowId => (
+ .map(rowId => (
<tr
key={rowId}
className={`tableBox-row ${this.columns[0]}`}
@@ -470,8 +471,9 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> {
})}
</tr>
))}
+ <tr style={{ height: (this._tableDataIds.length - this.endID) * Number(DATA_VIZ_TABLE_ROW_HEIGHT) }}></tr>
</tbody>
- <div style={{ height: (this._tableDataIds.length - this.endID) * Number(DATA_VIZ_TABLE_ROW_HEIGHT) }} />
+ {/* <div style={{ height: (this._tableDataIds.length - this.endID) * Number(DATA_VIZ_TABLE_ROW_HEIGHT) }} /> */}
</table>
</div>
</div>
diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss
index 5a6292fab..060ba353e 100644
--- a/src/client/views/nodes/ImageBox.scss
+++ b/src/client/views/nodes/ImageBox.scss
@@ -105,7 +105,7 @@
display: flex;
height: 100%;
img {
- object-fit: contain;
+ // object-fit: contain;
height: 100%;
}
diff --git a/src/client/views/smartdraw/DrawingFillHandler.tsx b/src/client/views/smartdraw/DrawingFillHandler.tsx
index f773957e7..23055fdc3 100644
--- a/src/client/views/smartdraw/DrawingFillHandler.tsx
+++ b/src/client/views/smartdraw/DrawingFillHandler.tsx
@@ -6,6 +6,7 @@ import { Upload } from '../../../server/SharedMediaTypes';
import { gptDescribeImage } from '../../apis/gpt/GPT';
import { Docs } from '../../documents/Documents';
import { Networking } from '../../Network';
+import { DocCreatorMenu } from '../nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu';
import { DocumentView, DocumentViewInternal } from '../nodes/DocumentView';
import { OpenWhere } from '../nodes/OpenWhere';
import { AspectRatioLimits, FireflyDimensionsMap, FireflyImageDimensions, FireflyStylePresets } from './FireflyConstants';