aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/ClientUtils.ts20
-rw-r--r--src/client/apis/gpt/GPT.ts45
-rw-r--r--src/client/documents/Documents.ts15
-rw-r--r--src/client/util/DocumentManager.ts5
-rw-r--r--src/client/util/Scripting.ts2
-rw-r--r--src/client/views/ContextMenu.scss50
-rw-r--r--src/client/views/ContextMenu.tsx3
-rw-r--r--src/client/views/ContextMenuItem.tsx1
-rw-r--r--src/client/views/EditableView.scss10
-rw-r--r--src/client/views/EditableView.tsx73
-rw-r--r--src/client/views/MainView.tsx15
-rw-r--r--src/client/views/PropertiesView.tsx10
-rw-r--r--src/client/views/StyleProvider.tsx47
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx2
-rw-r--r--src/client/views/collections/collectionSchema/CollectionSchemaView.scss53
-rw-r--r--src/client/views/collections/collectionSchema/CollectionSchemaView.tsx987
-rw-r--r--src/client/views/collections/collectionSchema/SchemaCellField.tsx406
-rw-r--r--src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx253
-rw-r--r--src/client/views/collections/collectionSchema/SchemaRowBox.tsx141
-rw-r--r--src/client/views/collections/collectionSchema/SchemaTableCell.tsx141
-rw-r--r--src/client/views/global/globalScripts.ts1
-rw-r--r--src/client/views/nodes/DataVizBox/DataVizBox.tsx313
-rw-r--r--src/client/views/nodes/DataVizBox/DocCreatorMenu.scss1044
-rw-r--r--src/client/views/nodes/DataVizBox/DocCreatorMenu.tsx2361
-rw-r--r--src/client/views/nodes/DataVizBox/TemplateDocTypes.tsx0
-rw-r--r--src/client/views/nodes/DataVizBox/components/TableBox.tsx27
-rw-r--r--src/client/views/nodes/DocumentIcon.tsx11
-rw-r--r--src/client/views/nodes/DocumentView.scss6
-rw-r--r--src/client/views/nodes/DocumentView.tsx1
-rw-r--r--src/client/views/nodes/FieldView.tsx2
-rw-r--r--src/client/views/nodes/ImageBox.tsx7
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.scss29
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx6
-rw-r--r--src/fields/Doc.ts13
-rw-r--r--src/fields/SchemaHeaderField.ts1
35 files changed, 5534 insertions, 567 deletions
diff --git a/src/ClientUtils.ts b/src/ClientUtils.ts
index 972910071..3066499d8 100644
--- a/src/ClientUtils.ts
+++ b/src/ClientUtils.ts
@@ -316,6 +316,26 @@ export namespace ClientUtils {
return { h: h, s: s, l: l };
}
+ export function lightenRGB(rVal: number, gVal: number, bVal: number, percent: number): [number, number, number] {
+ const amount = 1 + percent / 100;
+ const r = rVal * amount;
+ const g = gVal * amount;
+ const b = bVal * amount;
+
+ const threshold = 255.999;
+ const maxVal = Math.max(r, g, b);
+ if (maxVal <= threshold) {
+ return [Math.round(r), Math.round(g), Math.round(b)];
+ }
+ const total = r + g + b;
+ if (total >= 3 * threshold) {
+ return [Math.round(threshold), Math.round(threshold), Math.round(threshold)];
+ }
+ const x = (3 * threshold - total) / (3 * maxVal - total);
+ const gray = threshold - x * maxVal;
+ return [Math.round(gray + x * r), Math.round(gray + x * g), Math.round(gray + x * b)];
+ }
+
export function scrollIntoView(targetY: number, targetHgt: number, scrollTop: number, contextHgt: number, minSpacing: number, scrollHeight: number) {
if (!targetHgt) return targetY; // if there's no height, then assume that
if (scrollTop + contextHgt < Math.min(scrollHeight, targetY + minSpacing + targetHgt)) {
diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts
index 858957ac2..eeac57a5e 100644
--- a/src/client/apis/gpt/GPT.ts
+++ b/src/client/apis/gpt/GPT.ts
@@ -18,6 +18,11 @@ enum GPTCallType {
TYPE = 'type',
SUBSET = 'subset',
INFO = 'info',
+ TEMPLATE = 'template',
+ VIZSUM = 'vizsum',
+ VIZSUM2 = 'vizsum2',
+ FILL = 'fill',
+ COMPLETEPROMPT = 'completeprompt',
}
type GPTCallOpts = {
@@ -58,36 +63,33 @@ const callTypeMap: { [type: string]: GPTCallOpts } = {
model: 'gpt-4-turbo',
maxTokens: 1024,
temp: 0,
- prompt: "BRIEFLY (<50 words) describe any differences between the rubric and the user's answer answer in second person. If there are no differences, say correct",
+ prompt: 'List unique differences between the content of the UserAnswer and Rubric. Before each difference, label it and provide any additional information the UserAnswer missed and explain it in second person without separating it into UserAnswer and Rubric content and additional information. If there are no differences, say correct',
},
-
- rubric: {
+ template: {
model: 'gpt-4-turbo',
- maxTokens: 1024,
- temp: 0,
- prompt: "BRIEFLY (<25 words) provide a definition for the following term. It will be used as a rubric to evaluate the user's understanding of the topic",
+ maxTokens: 512,
+ temp: 0.5,
+ prompt: 'You will be given a list of field descriptions for multiple templates in the format {field #0: “description”}{field #1: “description”}{...}, and a list of column descriptions in the format {“title”: “description”}{...}. Your job is to match columns with fields based on their descriptions. Your output should be in the following JSON format: {“Template title”:{“#”: “title”, “#”: “title”, “#”: “title” …}, “Template title”:{“#”: “title”, “#”: “title”, “#”: “title” …}} where “Template title” represents the template, # represents the field # and “title” the title of the column assigned to it. A filled out example might look like {“fivefield2”:{“0”:”Name”, “1”:”Image”, “2”:”Caption”, “3”:”Position”, “4”:”Stats”}, “fivefield3”:{0:”Image”, 1:”Name”, 2:”Caption”, 3:”Stats”, 4:”Position”}. Include one object for each template. IT IS VERY IMPORTANT THAT YOU ONLY INCLUDE TEXT IN THE FORMAT ABOVE, WITH NO ADDITIONS WHATSOEVER. Do not include extraneous ‘#’ characters, ‘column’ for columns, or ‘template’ for templates: ONLY THE TITLES AND NUMBERS. There should never be one column assigned to more than one field (ie. if the “name” column is assigned to field 1, it can’t be assigned to any other fields) . Do this for each template whose fields are described. The descriptions are as follows:',
},
-
- type: {
+ vizsum: {
model: 'gpt-4-turbo',
- maxTokens: 1024,
- temp: 0,
- prompt: "I'm going to provide you with a question. Based on the question, is the user asking you to 1. Assigns docs with tags(like star / heart etc)/labels, 2. Filter docs, 3. Provide information about a specific doc 4. Provide a specific doc based on a question/information 5. Provide general information 6. Put cards in a specific order. Answer with only the number for 2-6. For number one, provide the number (1) and the appropriate tag",
+ maxTokens: 512,
+ temp: 0.5,
+ prompt: 'Your job is to provide brief descriptions for columns in a dataset based on example rows. Your descriptions should be geared towards how each column’s data might fit together into a visual template. Would they make good titles, main focuses, captions, descriptions, etc. Pay special attention to connections between columns, i.e. is there one column that specifically seems to describe/be related to another more than the rest? You should provide your analysis in JSON format like so: {“col1”:”description”, “col2”:”description”, …}. DO NOT INCLUDE ANY OTHER TEXT, ONLY THE JSON.',
},
-
- subset: {
+ vizsum2: {
model: 'gpt-4-turbo',
- maxTokens: 1024,
- temp: 0,
- prompt: "I'm going to give you a list of descriptions. Each one is separated by `======` on either side. Descriptions will vary in length, so make sure to only separate when you see `======`. Based on the question the user asks, provide a subset of the given descriptions that best matches the user's specifications. Make sure each description is only in the list once. Each item should be separated by `======`. Immediately afterward, surrounded by `------` on BOTH SIDES, provide some insight into your reasoning in the 2nd person (and mention nothing about the formatting details given in this description). It is VERY important that you format it exactly as described, ensuring the proper number of `=` and `-` (6 of each) and no commas",
+ maxTokens: 512,
+ temp: 0.5,
+ prompt: 'Your job is to provide structured information on columns in a dataset based on example rows. You will categorize each column in two ways: by type and size. The size categories are as follows: tiny (one or two words), small (a sentence/multiple words), medium (a few sentences), large (a longer paragraph), and huge (a very long or multiple paragraphs). The type categories are as follows: visual (links/file paths to images, pdfs, maps, or any other visual media type), and text (plain text that isn’t a link/file path). Visual media should be assumed to have size “medium” “large” or “huge”. You will give your responses in JSON format, like so: {“title (of column)”:{“type”:”text”, “size”:”small”}, “title (of column)”:{“type”:”visual”, “size”:”medium”}, …}. DO NOT INCLUDE ANY OTHER TEXT, ONLY THE JSON.',
},
-
- info: {
+ fill: {
model: 'gpt-4-turbo',
- maxTokens: 1024,
- temp: 0,
- prompt: "Answer the user's question with a short (<100 word) response. If a particular document is selected I will provide that information (which may help with your response)",
+ maxTokens: 512,
+ temp: 0.5,
+ prompt: 'Your job is to generate content for fields based on a user prompt and background context given to you. You will be given the content of the other fields present in the format: ---- Field # (field title): content ---- Field # (field title): content ----- (etc.) You will be given info on the columns to generate for in the format ---- title: , prompt: , word limit: , assigned field: ----. For each column, based on the prompt, word limit, and the context of existing fields, you should generate a short response in the following JSON format: {“___”(where ___ is the title from the column description with no additions): {“number”:”#” (where # is the assigned field of the column), “content”:”response” (where response is your response to the prompt in the column info)}}. Here’s another example of the format with only one column: {“position”: {“number”:”2”, “content”:”*your response goes here*”}}. ONLY INCLUDE THE JSON TEXT WITH NO OTHER ADDED TEXT. YOUR RESPONSE MUST BE VALID JSON. The word limit for each column applies only to that column’s response. Do not include speculation or information that you can’t glean from your factual knowledge or the content of the other fields (no description of images you can’t see, for example). You should include one object per column you are provided info on.',
},
+ completeprompt: { model: 'gpt-4-turbo', maxTokens: 512, temp: 0.5, prompt: 'Your prompt is as follows:' },
draw: {
model: 'gpt-4o',
maxTokens: 1024,
@@ -101,7 +103,6 @@ const callTypeMap: { [type: string]: GPTCallOpts } = {
prompt: 'You will be coloring drawings. You will be given what the drawing is, then a list of descriptions for parts of the drawing. Based on each description, respond with the stroke and fill color that it should be. Follow the rules: 1. Avoid using black for stroke color 2. Make the stroke color 1-3 shades darker than the fill color 3. Use the same colors when possible. Format as {#abcdef #abcdef}, making sure theres a color for each description, and do not include any additional text.',
},
};
-
let lastCall = '';
let lastResp = '';
/**
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index fafb1af8a..951632d71 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -42,7 +42,7 @@ export class FInfo {
readOnly: boolean = false;
fieldType?: FInfoFieldType;
values?: FieldType[];
-
+ onLayout?: boolean;
filterable?: boolean = true; // can be used as a Filter in FilterPanel
// format?: string; // format to display values (e.g, decimal places, $, etc)
// parse?: ScriptField; // parse a value from a string
@@ -176,6 +176,8 @@ export class DocumentOptions {
map_pitch?: NUMt = new NumInfo('pitch of a map view', false);
map_bearing?: NUMt = new NumInfo('bearing of a map view', false);
map_style?: STRt = new StrInfo('mapbox style for a map view', false);
+ identifier?: STRt = new StrInfo('documentIcon displayed for each doc as "d[x]"', false);
+ _rotation?: NUMt = new NumInfo('Amount of rotation on a document in degrees', false);
date_range?: STRt = new StrInfo('date range for calendar', false);
@@ -225,12 +227,23 @@ export class DocumentOptions {
_header_pointerEvents?: PEVt = new PEInfo('types of events the header of a custom text document can consume');
_lockedPosition?: BOOLt = new BoolInfo("lock the x,y coordinates of the document so that it can't be dragged");
_lockedTransform?: BOOLt = new BoolInfo('lock the freeform_panx,freeform_pany and scale parameters of the document so that it be panned/zoomed');
+ _childrenSharedWithSchema?: BOOLt = new BoolInfo("whether this document's children are displayed in its parent schema view", false);
+ _lockedSchemaEditing?: BOOLt = new BoolInfo('', false);
dataViz_title?: string;
dataViz_line?: string;
dataViz_pie?: string;
dataViz_histogram?: string;
dataViz?: string;
+ dataViz_savedTemplates?: LISTt;
+
+ borderWidth?: STRt = new StrInfo('Width of user-added border', false);
+ borderColor?: STRt = new StrInfo('Color of user-added border', false);
+ text_fontColor?: STRt = new StrInfo('Color of text', false);
+ text_align?: STRt = new StrInfo('alignment');
+ hCentering?: 'h-left' | 'h-center' | 'h-right';
+ isDefaultTemplateDoc?: BOOLt = new BoolInfo('');
+ contentBold?: BOOLt = new BoolInfo('');
layout?: string | Doc; // default layout string or template document
layout_isSvg?: BOOLt = new BoolInfo('whether document decorations and other selections should handle pointerEvents for svg content or use doc bounding box');
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 5ae292760..4ab2e8d05 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -45,6 +45,7 @@ export class DocumentManager {
DocumentView.addViewRenderedCb = this.AddViewRenderedCb;
DocumentView.getFirstDocumentView = this.getFirstDocumentView;
DocumentView.getDocumentView = this.getDocumentView;
+ DocumentView.getDocViewIndex = this.getDocViewIndex;
DocumentView.getContextPath = DocumentManager.GetContextPath;
DocumentView.getLightboxDocumentView = this.getLightboxDocumentView;
observe(Doc.CurrentlyLoading, change => {
@@ -140,6 +141,10 @@ export class DocumentManager {
);
}
+ public getDocViewIndex(target: Doc): number {
+ return DocumentManager.Instance.DocumentViews.findIndex(dv => dv.Document === target);
+ }
+
public getLightboxDocumentView = (toFind: Doc): DocumentView | undefined => {
const views: DocumentView[] = [];
DocumentManager.Instance.DocumentViews.forEach(view => DocumentView.LightboxContains(view) && Doc.AreProtosEqual(view.Document, toFind) && views.push(view));
diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts
index c7b86815a..b1db0bf39 100644
--- a/src/client/util/Scripting.ts
+++ b/src/client/util/Scripting.ts
@@ -186,7 +186,7 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp
if (captureVal instanceof Array) return p + captureVal.map(formatCapture);
return p + formatCapture(captured[v]);
}, '');
- const found = ScriptField.GetScriptFieldCache(script + ':' + signature);
+ const found = ScriptField.GetScriptFieldCache(script + ':' + signature); // if already compiled, found is the result; cache set below
if (found) return found as CompiledScript;
const { requiredType = '', addReturn = false, params = {}, capturedVariables = {}, typecheck = true } = options;
if (options.params && !options.params.this) options.params.this = Doc.name;
diff --git a/src/client/views/ContextMenu.scss b/src/client/views/ContextMenu.scss
index 4aaf2d03b..af0f717fe 100644
--- a/src/client/views/ContextMenu.scss
+++ b/src/client/views/ContextMenu.scss
@@ -162,3 +162,53 @@
border-radius: 5px;
width: 100%;
}
+
+.contextMenu-borderMenu {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ width: 222px;
+ height: 300px;
+ background-color: white;
+ border: solid 1px black;
+ color: black;
+ z-index: 99999999;
+
+ .top-bar {
+ height: 20px;
+ width: 100%;
+ display: flex;
+
+ .close-menu {
+ margin-top: 0;
+ margin-bottom: 0;
+ margin-right: 0;
+ padding: 0;
+ margin-left: auto;
+ z-index: 999999999;
+ width: 20px;
+ height: 20px;
+ color: black;
+ background-color: transparent;
+ }
+ }
+
+ .bottom-box{
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ align-items: center;
+ gap: 3px;
+ height: 100%;
+ width: 100%;
+
+ .width-selector{
+ width: 100px;
+ }
+
+ .max-min-selector{
+ height: 15px;
+ width: 30px;
+ }
+ }
+}
diff --git a/src/client/views/ContextMenu.tsx b/src/client/views/ContextMenu.tsx
index 399e8a238..4b67ef704 100644
--- a/src/client/views/ContextMenu.tsx
+++ b/src/client/views/ContextMenu.tsx
@@ -1,6 +1,3 @@
-/* eslint-disable react/no-array-index-key */
-/* eslint-disable react/jsx-props-no-spreading */
-/* eslint-disable default-param-last */
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { action, computed, IReactionDisposer, makeObservable, observable, runInAction } from 'mobx';
import { observer } from 'mobx-react';
diff --git a/src/client/views/ContextMenuItem.tsx b/src/client/views/ContextMenuItem.tsx
index 5d31173e1..6f8f41bdd 100644
--- a/src/client/views/ContextMenuItem.tsx
+++ b/src/client/views/ContextMenuItem.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable react/jsx-props-no-spreading */
import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { action, makeObservable, observable, runInAction } from 'mobx';
diff --git a/src/client/views/EditableView.scss b/src/client/views/EditableView.scss
index 27b260450..fa4542ac4 100644
--- a/src/client/views/EditableView.scss
+++ b/src/client/views/EditableView.scss
@@ -3,10 +3,17 @@
overflow-wrap: break-word;
word-wrap: break-word;
hyphens: auto;
- overflow: hidden;
+ overflow-y: auto;
height: 100%;
+ width: 100%;
min-width: 20;
text-overflow: ellipsis;
+ -ms-overflow-style: none;
+ scrollbar-width: none;
+}
+
+.editableView-container-editing::-webkit-scrollbar {
+ display: none;
}
.editableView-container-editing-oneLine {
@@ -37,3 +44,4 @@
border: none;
outline: none;
}
+
diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx
index 23da5a666..898a98c98 100644
--- a/src/client/views/EditableView.tsx
+++ b/src/client/views/EditableView.tsx
@@ -7,6 +7,7 @@ import { DocumentIconContainer } from './nodes/DocumentIcon';
import { FieldView, FieldViewProps } from './nodes/FieldView';
import { ObservableReactComponent } from './ObservableReactComponent';
import { OverlayView } from './OverlayView';
+import { SchemaFieldType } from './collections/collectionSchema/SchemaColumnHeader';
export interface EditableProps {
/**
@@ -51,6 +52,14 @@ export interface EditableProps {
background?: string | undefined;
placeholder?: string;
wrap?: string; // nowrap, pre-wrap, etc
+
+ inputString?: boolean;
+ inputStringPlaceholder?: string;
+ prohibitedText?: Array<string>;
+ onClick?: () => void;
+ updateAlt?: (newAlt: string) => void;
+ updateSearch?: (value: string) => void;
+ highlightCells?: (text: string) => void;
}
/**
@@ -62,18 +71,17 @@ export interface EditableProps {
export class EditableView extends ObservableReactComponent<EditableProps> {
private _ref = React.createRef<HTMLDivElement>();
private _inputref: HTMLInputElement | HTMLTextAreaElement | null = null;
+ private _disposers: { [name: string]: IReactionDisposer } = {};
_overlayDisposer?: () => void;
- _editingDisposer?: IReactionDisposer;
@observable _editing: boolean = false;
constructor(props: EditableProps) {
super(props);
makeObservable(this);
- this._editing = !!this._props.editing;
}
componentDidMount(): void {
- this._editingDisposer = reaction(
+ this._disposers.editing = reaction(
() => this._editing,
editing => {
if (editing) {
@@ -81,11 +89,13 @@ export class EditableView extends ObservableReactComponent<EditableProps> {
if (this._inputref?.value.startsWith('=') || this._inputref?.value.startsWith(':=')) {
this._overlayDisposer?.();
this._overlayDisposer = OverlayView.Instance.addElement(<DocumentIconContainer />, { x: 0, y: 0 });
+ this._props.highlightCells?.(this._props.GetValue() ?? '');
}
});
} else {
this._overlayDisposer?.();
this._overlayDisposer = undefined;
+ this._props.highlightCells?.('');
}
},
{ fireImmediately: true }
@@ -104,7 +114,7 @@ export class EditableView extends ObservableReactComponent<EditableProps> {
componentWillUnmount() {
this._overlayDisposer?.();
- this._editingDisposer?.();
+ this._disposers.editing?.();
this._inputref?.value && this.finalizeEdit(this._inputref.value, false, true, false);
}
@@ -116,6 +126,8 @@ export class EditableView extends ObservableReactComponent<EditableProps> {
} else if (!this._overlayDisposer) {
this._overlayDisposer = OverlayView.Instance.addElement(<DocumentIconContainer />, { x: 0, y: 0 });
}
+ this._props.updateSearch && this._props.updateSearch(targVal);
+ this._props.highlightCells?.(targVal);
};
@action
@@ -152,7 +164,7 @@ export class EditableView extends ObservableReactComponent<EditableProps> {
case 'ArrowDown':
case 'ArrowLeft':
case 'ArrowRight':
- e.stopPropagation();
+ //e.stopPropagation();
break;
case 'Shift':
case 'Alt':
@@ -176,9 +188,10 @@ export class EditableView extends ObservableReactComponent<EditableProps> {
};
@action
- onClick = (e: React.MouseEvent) => {
+ onClick = (e?: React.MouseEvent) => {
+ this._props.onClick && this._props.onClick();
if (this._props.editing !== false) {
- e.nativeEvent.stopPropagation();
+ e?.nativeEvent.stopPropagation();
if (this._ref.current && this._props.showMenuOnLoad) {
this._props.menuCallback?.(this._ref.current.getBoundingClientRect().x, this._ref.current.getBoundingClientRect().y);
} else {
@@ -218,10 +231,15 @@ export class EditableView extends ObservableReactComponent<EditableProps> {
return wasFocused !== this._editing;
};
+ @action
+ setIsEditing = (value: boolean) => {
+ this._editing = value;
+ return this._editing;
+ };
+
renderEditor() {
return this._props.autosuggestProps ? (
<Autosuggest
- // eslint-disable-next-line react/jsx-props-no-spreading
{...this._props.autosuggestProps.autosuggestProps}
inputProps={{
className: 'editableView-input',
@@ -268,13 +286,39 @@ export class EditableView extends ObservableReactComponent<EditableProps> {
);
}
+ staticDisplay = () => {
+ let toDisplay;
+ const gval = this._props.GetValue()?.replace(/\n/g, '\\r\\n');
+ if (this._props.inputString){
+ toDisplay = <input className="editableView-input"
+ value={gval}
+ placeholder={this._props.inputStringPlaceholder}
+ readOnly
+ style={{ display: this._props.display, overflow: 'auto', pointerEvents: 'none', fontSize: this._props.fontSize, width: '100%', margin: 0, background: this._props.background}}
+ // eslint-disable-next-line jsx-a11y/no-autofocus
+ />
+ } else {
+ toDisplay = (
+ <span
+ className="editableView-static"
+ style={{
+ fontStyle: this._props.fontStyle,
+ fontSize: this._props.fontSize,
+ }}>
+ {this._props.fieldContents ? <FieldView {...this._props.fieldContents} /> : (this.props.contents ?? '')}
+ </span>
+ );
+ }
+
+ return toDisplay;
+ };
+
render() {
const gval = this._props.GetValue()?.replace(/\n/g, '\\r\\n');
if (this._editing && gval !== undefined) {
return this._props.sizeToContent ? (
<div style={{ display: 'grid', minWidth: 100 }}>
- <div style={{ display: 'inline-block', position: 'relative', height: 0, width: '100%', overflow: 'hidden' }}>{gval}</div>
- {this.renderEditor()}
+ <div style={{ display: 'inline-block', position: 'relative', height: 0, width: '100%', overflow: 'hidden' }}>{this.renderEditor()}</div>
</div>
) : (
this.renderEditor()
@@ -291,18 +335,13 @@ export class EditableView extends ObservableReactComponent<EditableProps> {
minHeight: '10px',
whiteSpace: this._props.oneLine ? 'nowrap' : 'pre-line',
height: this._props.height,
+ width: '100%',
maxHeight: this._props.maxHeight,
fontStyle: this._props.fontStyle,
fontSize: this._props.fontSize,
}}
onClick={this.onClick}>
- <span
- style={{
- fontStyle: this._props.fontStyle,
- fontSize: this._props.fontSize,
- }}>
- {this._props.fieldContents ? <FieldView {...this._props.fieldContents} /> : (this._props.contents ?? '')}
- </span>
+ {this.staticDisplay()}
</div>
);
}
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index c61cdea54..e469531b0 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -59,6 +59,7 @@ import { ImageLabelHandler } from './collections/collectionFreeForm/ImageLabelHa
import { MarqueeOptionsMenu } from './collections/collectionFreeForm/MarqueeOptionsMenu';
import { CollectionLinearView } from './collections/collectionLinear';
import { LinkMenu } from './linking/LinkMenu';
+import { DocCreatorMenu } from './nodes/DataVizBox/DocCreatorMenu';
import { SchemaCSVPopUp } from './nodes/DataVizBox/SchemaCSVPopUp';
import { DocButtonState } from './nodes/DocumentLinksButton';
import { DocumentView, DocumentViewInternal } from './nodes/DocumentView';
@@ -89,6 +90,7 @@ export class MainView extends ObservableReactComponent<object> {
public static Live: boolean = false;
private _docBtnRef = React.createRef<HTMLDivElement>();
+ @observable private _keepContextMenuOpen: boolean = false;
@observable private _windowWidth: number = 0;
@observable private _windowHeight: number = 0;
@observable private _dashUIWidth: number = 0; // width of entire main dashboard region including left menu buttons and properties panel (but not including the dashboard selector button row)
@@ -280,6 +282,18 @@ export class MainView extends ObservableReactComponent<object> {
library.add(
...[
+ fa.faMinimize,
+ fa.faArrowsRotate,
+ fa.faFloppyDisk,
+ fa.faRepeat,
+ fa.faArrowsUpDown,
+ fa.faArrowsLeftRight,
+ fa.faWindowMaximize,
+ fa.faGift,
+ fa.faLockOpen,
+ fa.faSort,
+ fa.faArrowUpZA,
+ fa.faArrowDownAZ,
fa.faExclamationCircle,
fa.faEdit,
fa.faArrowDownShortWide,
@@ -1116,6 +1130,7 @@ export class MainView extends ObservableReactComponent<object> {
<PreviewCursor />
<TaskCompletionBox />
<ContextMenu />
+ <DocCreatorMenu />
<ImageLabelHandler />
<SmartDrawHandler />
<AnchorMenu />
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index 715f079d8..789333995 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -882,6 +882,12 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
doc[DocData].color = value || undefined;
});
}
+ @computed get borderColor() {
+ const doc = this.selectedDoc;
+ const layoutDoc = doc ? Doc.Layout(doc) : doc;
+ return StrCast(layoutDoc.color);
+ }
+ set borderColor(value) { this.selectedDoc && (this.selectedDoc[DocData].color = value || undefined); } // prettier-ignore
colorButton(value: string, type: string, setter: () => void) {
return (
@@ -933,6 +939,10 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
return this.colorPicker(this.colorStk, (color: string) => { this.colorStk = color; }); // prettier-ignore
}
+ @computed get borderColorPicker() {
+ return this.colorPicker(this.colorStk, (color: string) => { this.colorStk = color; }); // prettier-ignore
+ }
+
@computed get strokeAndFill() {
return (
<div>
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index 3545afcee..1e98695d1 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -52,14 +52,25 @@ export function styleFromLayoutString(doc: Doc, props: FieldViewProps, scale: nu
return style;
}
-export function wavyBorderPath(pw: number, ph: number, inset: number = 0.05) {
- return `M ${pw * 0.5} ${ph * inset} C ${pw * 0.6} ${ph * inset} ${pw * (1 - 2 * inset)} 0 ${pw * (1 - inset)} ${ph * inset} C ${pw} ${ph * (2 * inset)} ${pw * (1 - inset)} ${ph * 0.25} ${pw * (1 - inset)} ${ph * 0.3} C ${
- pw * (1 - inset)
- } ${ph * 0.4} ${pw} ${ph * (1 - 2 * inset)} ${pw * (1 - inset)} ${ph * (1 - inset)} C ${pw * (1 - 2 * inset)} ${ph} ${pw * 0.6} ${ph * (1 - inset)} ${pw * 0.5} ${ph * (1 - inset)} C ${pw * 0.3} ${ph * (1 - inset)} ${pw * (2 * inset)} ${ph} ${
- pw * inset
- } ${ph * (1 - inset)} C 0 ${ph * (1 - 2 * inset)} ${pw * inset} ${ph * 0.8} ${pw * inset} ${ph * 0.75} C ${pw * inset} ${ph * 0.7} 0 ${ph * (2 * inset)} ${pw * inset} ${ph * inset} C ${pw * (2 * inset)} 0 ${pw * 0.25} ${ph * inset} ${
- pw * 0.5
- } ${ph * inset}`;
+export function border(doc: Doc, pw: number, ph: number, rad: number = 0, inset: number = 0) {
+ if (!rad) rad = 0;
+ const width = pw * inset;
+ const height = ph * inset;
+
+ const radius = Math.min(rad, (pw - 2 * width) / 2, (ph - 2 * height) / 2);
+
+ return `
+ M ${width + radius} ${height}
+ L ${pw - width - radius} ${height}
+ A ${radius} ${radius} 0 0 1 ${pw - width} ${height + radius}
+ L ${pw - width} ${ph - height - radius}
+ A ${radius} ${radius} 0 0 1 ${pw - width - radius} ${ph - height}
+ L ${width + radius} ${ph - height}
+ A ${radius} ${radius} 0 0 1 ${width} ${ph - height - radius}
+ L ${width} ${height + radius}
+ A ${radius} ${radius} 0 0 1 ${width + radius} ${height}
+ Z
+ `;
}
let _filterOpener: () => void;
@@ -186,18 +197,28 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
const rounding = StrCast(doc?.[fieldKey + 'borderRounding'], StrCast(doc?.layout_borderRounding, doc?._type_collection === CollectionViewType.Pile ? '50%' : ''));
return (doc?.[StrCast(doc?.layout_fieldKey)] instanceof Doc || doc?.isTemplateDoc) ? StrCast(doc._layout_borderRounding,rounding) : rounding;
}
+ // Doc.IsComicStyle(doc) &&
+ // renderDepth &&
+ // !doc?.layout_isSvg &&
+ //case StyleProp.
case StyleProp.BorderPath: {
- const borderPath = Doc.IsComicStyle(doc) &&
- renderDepth &&
- !doc?.layout_isSvg && { path: wavyBorderPath(PanelWidth?.() || 0, PanelHeight?.() || 0), fill: wavyBorderPath(PanelWidth?.() || 0, PanelHeight?.() || 0, 0.08), width: 3 };
+ const docWidth = Number(doc?._width);
+ const borderWidth = Number(StrCast(doc?.borderWidth));
+ //console.log(borderWidth);
+ const ratio = borderWidth / docWidth;
+ const borderRadius = Number(StrCast(layoutDoc?._layout_borderRounding).replace('px', ''));
+ const radiusRatio = borderRadius / docWidth;
+ const radius = radiusRatio * ((2 * borderWidth) + docWidth);
+
+ const borderPath = doc && border(doc, NumCast(doc._width), NumCast(doc._height), radius, -ratio/2 ?? 0);
return !borderPath
? null
: {
- clipPath: `path('${borderPath.path}')`,
+ clipPath: `path('${borderPath}')`,
jsx: (
<div key="border2" className="documentView-customBorder" style={{ pointerEvents: 'none' }}>
<svg style={{ overflow: 'visible', height: '100%' }} viewBox={`0 0 ${PanelWidth?.()} ${PanelHeight?.()}`}>
- <path d={borderPath.path} style={{ stroke: 'black', fill: 'transparent', strokeWidth: borderPath.width }} />
+ <path d={borderPath} style={{ stroke: StrCast(doc?.borderColor), fill: 'transparent', strokeWidth: `${StrCast(doc?.borderWidth)}px` }} />
</svg>
</div>
),
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index d8678eebc..4043091d5 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1221,7 +1221,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
* Function that creates a drawing--a group of ink strokes--to go with the smart draw function.
*/
@undoBatch
- createDrawingDoc = (strokeData: [InkData, string, string][], opts: DrawingOptions, gptRes: string) => {
+ createDrawingDoc = (strokeData: [InkData, string, string][], opts: DrawingOptions) => {
this._drawing = [];
const xf = this.screenToFreeformContentsXf;
strokeData.forEach((stroke: [InkData, string, string]) => {
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.scss b/src/client/views/collections/collectionSchema/CollectionSchemaView.scss
index 6fb8e40db..c32661214 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaView.scss
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.scss
@@ -50,18 +50,15 @@
.schema-column-menu,
.schema-filter-menu {
background: $light-gray;
- position: relative;
- min-width: 200px;
- max-width: 400px;
+ position: absolute;
+ border: 1px solid $medium-gray;
+ border-bottom: 2px solid $medium-gray;
+ max-height: 201px;
display: flex;
+ overflow: hidden;
flex-direction: column;
align-items: flex-start;
- z-index: 1;
-
- .schema-key-search-input {
- width: calc(100% - 20px);
- margin: 10px;
- }
+ z-index: 5;
.schema-search-result {
cursor: pointer;
@@ -104,7 +101,7 @@
.schema-key-list {
width: 100%;
- max-height: 300px;
+ max-height: 250px;
overflow-y: auto;
}
@@ -153,12 +150,18 @@
padding: 0;
z-index: 1;
border: 1px solid $medium-gray;
- //overflow: hidden;
.schema-column-title {
flex-grow: 2;
margin: 5px;
overflow: hidden;
+ min-width: 100%;
+ }
+
+ .schema-column-edit-wrapper {
+ flex-grow: 2;
+ margin: 5px;
+ overflow: hidden;
min-width: 20%;
}
@@ -176,6 +179,11 @@
}
}
+ .editableView-input {
+ border: none;
+ outline: none;
+ }
+
/*.schema-column-resizer.left {
min-width: 5px;
transform: translate(-3px, 0px);
@@ -245,9 +253,6 @@
flex-direction: row;
min-width: 50px;
justify-content: center;
- .iconButton-container {
- min-width: unset !important;
- }
}
.row-cells {
@@ -255,6 +260,20 @@
flex-direction: row;
justify-content: flex-end;
}
+
+ .row-menu-infos {
+ position: absolute;
+ top: 3;
+ left: 3;
+ z-index: 1;
+ display: flex;
+ justify-content: flex-end;
+ align-items: center;
+
+ .row-infos-icon {
+ padding-right: 2px;
+ }
+ }
}
.schema-row-button,
@@ -287,3 +306,9 @@
width: 12px;
}
}
+
+.schemaField-editing {
+ outline: none;
+}
+
+
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
index 325628d53..aef97e723 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
@@ -1,10 +1,9 @@
-/* eslint-disable no-restricted-syntax */
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Popup, PopupTrigger, Type } from 'browndash-components';
-import { ObservableMap, action, computed, makeObservable, observable, observe, runInAction } from 'mobx';
+import { IconButton, Size } from 'browndash-components';
+import { IReactionDisposer, Lambda, ObservableMap, action, computed, makeObservable, observable, observe, reaction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
-import { returnEmptyString, returnFalse, returnIgnore, returnNever, returnTrue, setupMoveUpEvents, smoothScroll } from '../../../../ClientUtils';
+import { ClientUtils, returnEmptyString, returnFalse, returnIgnore, returnNever, returnTrue, setupMoveUpEvents, smoothScroll } from '../../../../ClientUtils';
import { emptyFunction } from '../../../../Utils';
import { Doc, DocListCast, Field, FieldType, NumListCast, Opt, StrListCast } from '../../../../fields/Doc';
import { DocData } from '../../../../fields/DocSymbols';
@@ -14,11 +13,13 @@ import { ColumnType } from '../../../../fields/SchemaHeaderField';
import { BoolCast, NumCast, StrCast } from '../../../../fields/Types';
import { DocUtils } from '../../../documents/DocUtils';
import { Docs, DocumentOptions, FInfo } from '../../../documents/Documents';
+import { DocumentManager } from '../../../util/DocumentManager';
import { DragManager } from '../../../util/DragManager';
import { dropActionType } from '../../../util/DropActionTypes';
-import { SettingsManager } from '../../../util/SettingsManager';
+import { SnappingManager } from '../../../util/SnappingManager';
import { undoBatch, undoable } from '../../../util/UndoManager';
import { ContextMenu } from '../../ContextMenu';
+import { ContextMenuProps } from '../../ContextMenuItem';
import { EditableView } from '../../EditableView';
import { ObservableReactComponent } from '../../ObservableReactComponent';
import { StyleProp } from '../../StyleProp';
@@ -29,9 +30,23 @@ import { FieldViewProps } from '../../nodes/FieldView';
import { FocusViewOptions } from '../../nodes/FocusViewOptions';
import { CollectionSubView, SubCollectionViewProps } from '../CollectionSubView';
import './CollectionSchemaView.scss';
+import { SchemaCellField } from './SchemaCellField';
import { SchemaColumnHeader } from './SchemaColumnHeader';
import { SchemaRowBox } from './SchemaRowBox';
+/**
+ * The schema view offers a spreadsheet-like interface for users to interact with documents. Within the schema,
+ * each doc is represented by its own row. Each column represents a field, for example the author or title fields.
+ * Users can apply varoius filters and sorts to columns to change what is displayed. The schemaview supports equations for
+ * cell linking.
+ *
+ * This class supports the main functionality for choosing which docs to render in the view, applying visual
+ * updates to rows and columns (such as user dragging or sort-related highlighting), applying edits to multiple cells
+ * at once, and applying filters and sorts to columns. It contains SchemaRowBoxes (which themselves contain SchemaTableCells,
+ * and SchemaCellFields) and SchemaColumnHeaders.
+ */
+
+// eslint-disable-next-line @typescript-eslint/no-require-imports
const { SCHEMA_NEW_NODE_HEIGHT } = require('../../global/globalCssVariables.module.scss'); // prettier-ignore
export const FInfotoColType: { [key: string]: ColumnType } = {
@@ -48,16 +63,32 @@ const defaultColumnKeys: string[] = ['title', 'type', 'author', 'author_date', '
@observer
export class CollectionSchemaView extends CollectionSubView() {
- private _keysDisposer?: () => void;
+ private _keysDisposer?: Lambda;
+ private _disposers: { [name: string]: IReactionDisposer } = {};
private _previewRef: HTMLDivElement | null = null;
private _makeNewColumn: boolean = false;
private _documentOptions: DocumentOptions = new DocumentOptions();
private _tableContentRef: HTMLDivElement | null = null;
private _menuTarget = React.createRef<HTMLDivElement>();
+ private _headerRefs: SchemaColumnHeader[] = [];
+ private _eqHighlightColors: Array<[{ r: number; g: number; b: number }, { r: number; g: number; b: number }]> = [];
+ private _oldWheel: HTMLDivElement | null = null;
constructor(props: SubCollectionViewProps) {
super(props);
makeObservable(this);
+ const lightenedColor = (r: number, g: number, b:number) => { const lightened = ClientUtils.lightenRGB(r, g, b, 165); return {r: lightened[0], g: lightened[1], b: lightened[2]}} // prettier-ignore
+ const colors = (r: number, g: number, b: number):[{r:number,g:number,b:number},{r:number,g:number,b:number}] => ([{r, g, b}, lightenedColor(r, g, b)]); // prettier-ignore
+ this._eqHighlightColors.push(colors(70, 150, 50));
+ this._eqHighlightColors.push(colors(180, 70, 20));
+ this._eqHighlightColors.push(colors(70, 50, 150));
+ this._eqHighlightColors.push(colors(0, 140, 140));
+ this._eqHighlightColors.push(colors(140, 30, 110));
+ this._eqHighlightColors.push(colors(20, 50, 200));
+ this._eqHighlightColors.push(colors(210, 30, 40));
+ this._eqHighlightColors.push(colors(120, 130, 30));
+ this._eqHighlightColors.push(colors(50, 150, 70));
+ this._eqHighlightColors.push(colors(10, 90, 180));
}
static _rowHeight: number = 50;
@@ -79,14 +110,20 @@ export class CollectionSchemaView extends CollectionSubView() {
@observable _newFieldType: ColumnType = ColumnType.Number;
@observable _menuValue: string = '';
@observable _filterColumnIndex: number | undefined = undefined;
- @observable _filterSearchValue: string = '';
+ @observable _filterSearchValue: string = ''; //the current text inside the filter search bar, used to determine which values to display
@observable _selectedCol: number = 0;
@observable _selectedCells: Array<Doc> = [];
- @observable _mouseCoordinates = { x: 0, y: 0 };
- @observable _lowestSelectedIndex = -1; // lowest index among selected rows; used to properly sync dragged docs with cursor position
- @observable _relCursorIndex = -1; // cursor index relative to the current selected cells
- @observable _draggedColIndex = 0;
- @observable _colBeingDragged = false;
+ @observable _mouseCoordinates = { x: 0, y: 0, prevX: 0, prevY: 0 };
+ @observable _lowestSelectedIndex: number = -1; //lowest index among selected rows; used to properly sync dragged docs with cursor position
+ @observable _relCursorIndex: number = -1; //cursor index relative to the current selected cells
+ @observable _draggedColIndex: number = 0;
+ @observable _colBeingDragged: boolean = false; //whether a column is being dragged by the user
+ @observable _colKeysFiltered: boolean = false;
+ @observable _cellTags: ObservableMap = new ObservableMap<Doc, Array<string>>();
+ @observable _highlightedCellsInfo: Array<[doc: Doc, field: string]> = [];
+ @observable _cellHighlightColors: ObservableMap = new ObservableMap<string, string[]>();
+ @observable _containedDocs: Doc[] = []; //all direct children of the schema
+ @observable _referenceSelectMode: { enabled: boolean; currEditing: SchemaCellField | undefined } = { enabled: false, currEditing: undefined };
// target HTMLelement portal for showing a popup menu to edit cell values.
public get MenuTarget() {
@@ -95,7 +132,8 @@ export class CollectionSchemaView extends CollectionSubView() {
@computed get _selectedDocs() {
// get all selected documents then filter out any whose parent is not this schema document
- const selected = DocumentView.SelectedDocs().filter(doc => this.childDocs.includes(doc));
+ const selected = DocumentView.SelectedDocs().filter(doc => this.docs.includes(doc));
+ //&& this._selectedCells.includes(doc)
if (!selected.length) {
// if no schema doc is directly selected, test if a child of a schema doc is selected (such as in the preview window)
const childOfSchemaDoc = DocumentView.SelectedDocs().find(sel => DocumentView.getContextPath(sel, true).includes(this.Document));
@@ -107,6 +145,10 @@ export class CollectionSchemaView extends CollectionSubView() {
return selected;
}
+ @computed get highlightedCells() {
+ return this._highlightedCellsInfo.map(info => this.getCellElement(info[0], info[1]));
+ }
+
@computed get documentKeys() {
return Array.from(this.fieldInfos.keys());
}
@@ -130,7 +172,6 @@ export class CollectionSchemaView extends CollectionSubView() {
);
const totalWidth = widths.reduce((sum, width) => sum + width, 0);
- // If the total width of all columns is not the width of the schema table minus the width of the row menu, resize them appropriately
if (totalWidth !== this.tableWidth - CollectionSchemaView._rowMenuWidth) {
return widths.map(w => (w / totalWidth) * (this.tableWidth - CollectionSchemaView._rowMenuWidth));
}
@@ -138,7 +179,7 @@ export class CollectionSchemaView extends CollectionSubView() {
}
@computed get rowHeights() {
- return this.childDocs.map(() => this.rowHeightFunc());
+ return this.docs.map(() => this.rowHeightFunc());
}
@computed get displayColumnWidths() {
@@ -176,17 +217,35 @@ export class CollectionSchemaView extends CollectionSubView() {
},
true
);
+ this._disposers.docdata = reaction(
+ () => DocListCast(this.dataDoc[this.fieldKey]),
+ docs => (this._containedDocs = docs),
+ { fireImmediately: true }
+ );
+ this._disposers.sortHighlight = reaction(
+ () => [this.sortField, this._containedDocs, this._selectedDocs, this._highlightedCellsInfo],
+ () => {
+ this.sortField && setTimeout(() => this.highlightSortedColumn());
+ },
+ { fireImmediately: true }
+ );
}
componentWillUnmount() {
this._keysDisposer?.();
+ Object.values(this._disposers).forEach(disposer => disposer?.());
document.removeEventListener('keydown', this.onKeyDown);
}
// ViewBoxInterface overrides
override isUnstyledView = returnTrue; // used by style provider : turns off opacity, animation effects, scaling
- rowIndex = (doc: Doc) => this.sortedDocs.docs.indexOf(doc);
+ removeDoc = (doc: Doc) => {
+ this.removeDocument(doc);
+ this._containedDocs = this._containedDocs.filter(d => d !== doc);
+ };
+
+ rowIndex = (doc: Doc) => this.docsWithDrag.docs.indexOf(doc);
@action
onKeyDown = (e: KeyboardEvent) => {
@@ -196,9 +255,9 @@ export class CollectionSchemaView extends CollectionSubView() {
{
const lastDoc = this._selectedDocs.lastElement();
const lastIndex = this.rowIndex(lastDoc);
- const curDoc = this.sortedDocs.docs[lastIndex];
+ const curDoc = this.docs[lastIndex];
if (lastIndex >= 0 && lastIndex < this.childDocs.length - 1) {
- const newDoc = this.sortedDocs.docs[lastIndex + 1];
+ const newDoc = this.docs[lastIndex + 1];
if (this._selectedDocs.includes(newDoc)) {
DocumentView.DeselectView(DocumentView.getFirstDocumentView(curDoc));
this.deselectCell(curDoc);
@@ -215,9 +274,9 @@ export class CollectionSchemaView extends CollectionSubView() {
{
const firstDoc = this._selectedDocs.lastElement();
const firstIndex = this.rowIndex(firstDoc);
- const curDoc = this.sortedDocs.docs[firstIndex];
+ const curDoc = this.docs[firstIndex];
if (firstIndex > 0 && firstIndex < this.childDocs.length) {
- const newDoc = this.sortedDocs.docs[firstIndex - 1];
+ const newDoc = this.docs[firstIndex - 1];
if (this._selectedDocs.includes(newDoc)) {
DocumentView.DeselectView(DocumentView.getFirstDocumentView(curDoc));
this.deselectCell(curDoc);
@@ -245,34 +304,28 @@ export class CollectionSchemaView extends CollectionSubView() {
}
break;
case 'Backspace': {
- undoable(() => this.removeDocument(this._selectedDocs), 'delete schema row');
+ undoable(() => {
+ this._selectedDocs.forEach(d => this._containedDocs.includes(d) && this.removeDoc(d));
+ }, 'delete schema row');
break;
}
case 'Escape': {
this.deselectAllCells();
break;
}
+ case 'P': {
+ break;
+ }
default:
}
}
};
- @action
- changeSelectedCellColumn = () => {};
-
- @undoBatch
- setColumnSort = (field: string | undefined, desc: boolean = false) => {
- this.layoutDoc.sortField = field;
- this.layoutDoc.sortDesc = desc;
- };
-
addRow = (doc: Doc | Doc[]) => this.addDocument(doc);
@undoBatch
- changeColumnKey = (index: number, newKey: string, defaultVal?: string | number | boolean) => {
- if (!this.documentKeys.includes(newKey)) {
- this.addNewKey(newKey, defaultVal);
- }
+ changeColumnKey = (index: number, newKey: string, defaultVal?: FieldType) => {
+ if (!this.documentKeys.includes(newKey)) this.addNewKey(newKey, defaultVal);
const currKeys = this.columnKeys.slice(); // copy the column key array first, then change it.
currKeys[index] = newKey;
@@ -280,31 +333,37 @@ export class CollectionSchemaView extends CollectionSubView() {
};
@undoBatch
- addColumn = (key: string, defaultVal?: string | number | boolean) => {
- if (!this.documentKeys.includes(key)) {
- this.addNewKey(key, defaultVal);
- }
+ addColumn = (index: number = 0, keyIn?: string, defaultVal?: FieldType) => {
+ let key = keyIn;
+ if (key && !this.documentKeys.includes(key)) this.addNewKey(key, defaultVal);
const newColWidth = this.tableWidth / (this.storedColumnWidths.length + 1);
const currWidths = this.storedColumnWidths.slice();
- currWidths.splice(0, 0, newColWidth);
+ currWidths.splice(index, 0, newColWidth);
const newDesiredTableWidth = currWidths.reduce((w, cw) => w + cw, 0);
this.layoutDoc.schema_columnWidths = new List<number>(currWidths.map(w => (w / newDesiredTableWidth) * (this.tableWidth - CollectionSchemaView._rowMenuWidth)));
const currKeys = this.columnKeys.slice();
- currKeys.splice(0, 0, key);
+ if (!key) key = 'EmptyColumnKey' + Math.floor(Math.random() * 1000000000000000).toString();
+ currKeys.splice(index, 0, key);
+ this.changeColumnKey(index, 'EmptyColumnKey' + Math.floor(Math.random() * 1000000000000000).toString());
this.layoutDoc.schema_columnKeys = new List<string>(currKeys);
};
@action
- addNewKey = (key: string, defaultVal?: string | number | boolean) =>
+ addNewKey = (key: string, defaultVal: FieldType | undefined) => {
this.childDocs.forEach(doc => {
doc[DocData][key] = defaultVal;
});
+ };
@undoBatch
removeColumn = (index: number) => {
if (this.columnKeys.length === 1) return;
+ if (this._columnMenuIndex === index) {
+ this._headerRefs[index].toggleEditing(false);
+ this.closeNewColumnMenu();
+ }
const currWidths = this.storedColumnWidths.slice();
currWidths.splice(index, 1);
const newDesiredTableWidth = currWidths.reduce((w, cw) => w + cw, 0);
@@ -313,23 +372,28 @@ export class CollectionSchemaView extends CollectionSubView() {
const currKeys = this.columnKeys.slice();
currKeys.splice(index, 1);
this.layoutDoc.schema_columnKeys = new List<string>(currKeys);
+
+ this._colEles.splice(index, 1);
};
@action
- startResize = (e: React.PointerEvent, index: number) => {
+ startResize = (e: React.PointerEvent, index: number, rightSide: boolean) => {
this._displayColumnWidths = this.storedColumnWidths;
- setupMoveUpEvents(this, e, moveEv => this.resizeColumn(moveEv, index), this.finishResize, emptyFunction);
+ setupMoveUpEvents(this, e, moveEv => this.resizeColumn(moveEv, index, rightSide), this.finishResize, emptyFunction);
};
@action
- resizeColumn = (e: PointerEvent, index: number) => {
+ resizeColumn = (e: PointerEvent, index: number, rightSide: boolean) => {
if (this._displayColumnWidths) {
let shrinking;
let growing;
let change = e.movementX;
- if (index !== 0) {
+ if (rightSide && index !== this._displayColumnWidths.length - 1) {
+ growing = change < 0 ? index + 1 : index;
+ shrinking = change < 0 ? index : index + 1;
+ } else if (index !== 0) {
growing = change < 0 ? index : index - 1;
shrinking = change < 0 ? index - 1 : index;
}
@@ -367,14 +431,14 @@ export class CollectionSchemaView extends CollectionSubView() {
const currWidths = this.storedColumnWidths.slice();
currWidths.splice(toIndex, 0, currWidths.splice(fromIndex, 1)[0]);
this.layoutDoc.schema_columnWidths = new List<number>(currWidths);
-
- this._draggedColIndex = toIndex;
};
@action
dragColumn = (e: PointerEvent, index: number) => {
+ this.closeNewColumnMenu();
+ this._headerRefs.forEach(ref => ref.toggleEditing(false));
this._draggedColIndex = index;
- this._colBeingDragged = true;
+ this.setColDrag(true);
const dragData = new DragManager.ColumnDragData(index);
const dragEles = [this._colEles[index]];
this.childDocs.forEach(doc => dragEles.push(this._rowEles.get(doc).children[1].children[index]));
@@ -382,7 +446,13 @@ export class CollectionSchemaView extends CollectionSubView() {
return true;
};
+ /**
+ * Uses cursor x coordinate to calculate which index the column should be rendered/dropped in
+ * @param mouseX cursor x coordinate
+ * @returns column index
+ */
findColDropIndex = (mouseX: number) => {
+ const xOffset: number = this._props.ScreenToLocalTransform().inverse().transformPoint(0, 0)[0] + CollectionSchemaView._rowMenuWidth;
let index: number | undefined;
this.displayColumnWidths.reduce((total, curr, i) => {
if (total <= mouseX && total + curr >= mouseX) {
@@ -390,16 +460,35 @@ export class CollectionSchemaView extends CollectionSubView() {
else index = i + 1;
}
return total + curr;
- }, 2 * CollectionSchemaView._rowMenuWidth); // probably prone to issues; find better implementation (!!!)
+ }, xOffset);
return index;
};
/**
- * Calculates the relative index of the cursor in the group of selected rows, ie.
- * if five rows are selected and the cursor is in the middle row, its relative index would be 2.
- * Used to align actively dragged documents properly with the cursor.
- * @param mouseY the initial Y position of the cursor on drag
+ * Calculates the current index of dragged rows for dynamic rendering and drop logic.
+ * @param mouseY user's cursor position relative to the viewport
+ * @returns row index the dragged doc should be rendered/dropped in
*/
+ findRowDropIndex = (mouseY: number): number => {
+ const rowHeight = CollectionSchemaView._rowHeight;
+ let index: number = 0;
+ this.rowHeights.reduce((total, curr, i) => {
+ if (total <= mouseY && total + curr >= mouseY) {
+ if (mouseY <= total + curr) index = i;
+ else index = i + 1;
+ }
+ return total + curr;
+ }, rowHeight);
+
+ // fix index if selected rows are dragged out of bounds
+ let adjIndex = index - this._relCursorIndex;
+ const maxY = this.rowHeights.reduce((total, curr) => total + curr, 0) + rowHeight;
+ if (mouseY > maxY) adjIndex = this.childDocs.length - 1;
+ else if (adjIndex <= 0) adjIndex = 0;
+
+ return adjIndex;
+ };
+
@action
setRelCursorIndex = (mouseY: number) => {
this._mouseCoordinates.y = mouseY; // updates this.rowDropIndex computed value to overwrite the old cached value
@@ -420,43 +509,192 @@ export class CollectionSchemaView extends CollectionSubView() {
this._relCursorIndex = index;
};
- findRowDropIndex = (mouseY: number) => {
- const rowHeight = CollectionSchemaView._rowHeight;
- let index: number = 0;
- this.rowHeights.reduce((total, curr, i) => {
- if (total <= mouseY && total + curr >= mouseY) {
- if (mouseY <= total + curr) index = i;
- else index = i + 1;
- }
- return total + curr;
- }, rowHeight);
-
- // fix index if selected rows are dragged out of bounds
- let adjIndex = index - this._relCursorIndex;
- const maxY = this.rowHeights.reduce((total, curr) => total + curr, 0) + rowHeight;
- if (mouseY > maxY) adjIndex = this.childDocs.length - 1;
- else if (adjIndex <= 0) adjIndex = 0;
-
- return adjIndex;
- };
-
highlightDraggedColumn = (index: number) =>
this._colEles.forEach((colRef, i) => {
const edgeStyle = i === index ? `solid 2px ${Colors.MEDIUM_BLUE}` : '';
- const cellEles = [
- colRef,
- ...this.childDocs //
- .filter(doc => i !== this._selectedCol || !this._selectedDocs.includes(doc))
- .map(doc => this._rowEles.get(doc).children[1].children[i]),
- ];
- cellEles[0].style.borderTop = edgeStyle;
+ const sorted = i === this.columnKeys.indexOf(this.sortField);
+ const cellEles = [colRef, ...this.docsWithDrag.docs.filter(doc => (i !== this._selectedCol || !this._selectedDocs.includes(doc)) && !sorted).map(doc => this._rowEles.get(doc).children[1].children[i])];
cellEles.forEach(ele => {
+ if (sorted || this.highlightedCells.includes(ele)) return;
+ ele.style.borderTop = ele === cellEles[0] ? edgeStyle : '';
ele.style.borderLeft = edgeStyle;
ele.style.borderRight = edgeStyle;
+ ele.style.borderBottom = ele === cellEles.slice(-1)[0] ? edgeStyle : '';
});
- cellEles.slice(-1)[0].style.borderBottom = edgeStyle;
});
+ removeDragHighlight = () => {
+ this._colEles.forEach((colRef, i) => {
+ const sorted = i === this.columnKeys.indexOf(this.sortField);
+ if (sorted) return;
+
+ colRef.style.borderLeft = '';
+ colRef.style.borderRight = '';
+ colRef.style.borderTop = '';
+
+ this.childDocs.forEach(doc => {
+ const cell = this._rowEles.get(doc).children[1].children[i];
+ if (!(this._selectedDocs.includes(doc) && i === this._selectedCol) && !this.highlightedCells.includes(cell) && cell) {
+ cell.style.borderLeft = '';
+ cell.style.borderRight = '';
+ cell.style.borderBottom = '';
+ }
+ });
+ });
+ };
+
+ /**
+ * Applies a gradient highlight to a sorted column. The direction of the gradient depends
+ * on whether the sort is ascending or descending.
+ * @param field the column being sorted
+ * @param descending whether the sort is descending or ascending; descending if true
+ */
+ highlightSortedColumn = (field?: string, descending?: boolean) => {
+ let index = -1;
+ const highlightColors: string[] = [];
+ const rowCount: number = this._containedDocs.length + 1;
+ if (field || this.sortField) {
+ index = this.columnKeys.indexOf(field || this.sortField);
+ const increment: number = 110 / rowCount;
+ for (let i = 1; i <= rowCount; ++i) {
+ const adjColor = ClientUtils.lightenRGB(16, 66, 230, increment * i);
+ highlightColors.push(`solid 2px rgb(${adjColor[0]}, ${adjColor[1]}, ${adjColor[2]})`);
+ }
+ }
+
+ this._colEles.forEach((colRef, i) => {
+ const highlight: boolean = i === index;
+ const desc: boolean = descending || this.sortDesc;
+ const cellEles = [colRef, ...this.docsWithDrag.docs.filter(doc => i !== this._selectedCol || !this._selectedDocs.includes(doc)).map(doc => this._rowEles.get(doc).children[1].children[i])];
+ const cellCount = cellEles.length;
+ for (let ele = 0; ele < cellCount; ++ele) {
+ const currCell = cellEles[ele];
+ if (this.highlightedCells.includes(currCell)) continue;
+ const style = highlight ? (desc ? `${highlightColors[cellCount - 1 - ele]}` : `${highlightColors[ele]}`) : '';
+ currCell.style.borderLeft = style;
+ currCell.style.borderRight = style;
+ }
+ cellEles[0].style.borderTop = highlight ? (desc ? `${highlightColors[cellCount - 1]}` : `${highlightColors[0]}`) : '';
+ if (!(this._selectedDocs.includes(this.docsWithDrag.docs[this.docsWithDrag.docs.length - 1]) && this._selectedCol === index) && !this.highlightedCells.includes(cellEles[cellCount - 1]))
+ cellEles[cellCount - 1].style.borderBottom = highlight ? (desc ? `${highlightColors[0]}` : `${highlightColors[cellCount - 1]}`) : '';
+ });
+ };
+
+ /**
+ * Gets the html element representing a cell so that styles can be applied on it.
+ * @param doc the cell's row document
+ * @param fieldKey the cell's column's field key
+ * @returns the html element representing the cell at the given location
+ */
+ getCellElement = (doc: Doc, fieldKey: string) => {
+ const index = this.columnKeys.indexOf(fieldKey);
+ const cell = this._rowEles.get(doc).children[1].children[index];
+ return cell;
+ };
+
+ /**
+ * Given text in a cell, find references to other cells (for equations).
+ * @param text the text in the cell
+ * @returns the html cell elements referenced in the text.
+ */
+ findCellRefs = (text: string) => {
+ const pattern = /(this|d(\d+))\.(\w+)/g;
+ interface Match {
+ docRef: string;
+ field: string;
+ }
+
+ const matches: Match[] = [];
+ let match: RegExpExecArray | null;
+
+ while ((match = pattern.exec(text)) !== null) {
+ const docRef = match[1] === 'this' ? match[1] : match[2];
+ matches.push({ docRef, field: match[3] });
+ }
+
+ const cells: [Doc, string][] = [];
+ matches.forEach((m: Match) => {
+ const { docRef, field } = m;
+ const docView = DocumentManager.Instance.DocumentViews[Number(docRef)];
+ const doc = docView?.Document ?? undefined;
+ if (this.columnKeys.includes(field) && this._containedDocs.includes(doc)) {
+ cells.push([doc, field]);
+ }
+ });
+
+ return cells;
+ };
+
+ /**
+ * Determines whether the rows above or below a given row have been
+ * selected, so selection highlights don't overlap.
+ * @param doc the document row to check
+ * @returns a boolean tuple where 0 is the row above, and 1 is the row below
+ */
+ selectionOverlap = (doc: Doc): [boolean, boolean] => {
+ const docs = this.docsWithDrag.docs;
+ const index = this.rowIndex(doc);
+ const selectedBelow: boolean = this._selectedDocs.includes(docs[index + 1]);
+ const selectedAbove: boolean = this._selectedDocs.includes(docs[index - 1]);
+ return [selectedAbove, selectedBelow];
+ };
+
+ @action
+ removeCellHighlights = () => {
+ this._highlightedCellsInfo.forEach(info => {
+ const doc = info[0];
+ const field = info[1];
+ const cell = this.getCellElement(doc, field);
+ if (this._selectedDocs.includes(doc) && this._selectedCol === this.columnKeys.indexOf(field)) {
+ cell.style.border = `solid 2px ${Colors.MEDIUM_BLUE}`;
+ if (this.selectionOverlap(doc)[0]) cell.style.borderTop = '';
+ if (this.selectionOverlap(doc)[1]) cell.style.borderBottom = '';
+ } else cell.style.border = '';
+ cell.style.backgroundColor = '';
+ });
+ this._highlightedCellsInfo = [];
+ };
+
+ restoreCellHighlights = () => {
+ this._highlightedCellsInfo.forEach(info => {
+ const doc = info[0];
+ const field = info[1];
+ const key = `${doc[Id]}_${field}`;
+ const cell = this.getCellElement(doc, field);
+ const color = this._cellHighlightColors.get(key)[0];
+ cell.style.borderTop = color;
+ cell.style.borderLeft = color;
+ cell.style.borderRight = color;
+ cell.style.borderBottom = color;
+ });
+ };
+
+ /**
+ * Highlights cells based on equation text in the cell currently being edited.
+ * Does not highlight selected cells (that's done directly in SchemaTableCell).
+ * @param text the equation
+ */
+ highlightCells = (text: string) => {
+ this.removeCellHighlights();
+
+ const cellsToHighlight = this.findCellRefs(text);
+ this._highlightedCellsInfo = [...cellsToHighlight];
+
+ for (let i = 0; i < this._highlightedCellsInfo.length; ++i) {
+ const info = this._highlightedCellsInfo[i];
+ const color = this._eqHighlightColors[i % 10];
+ const colorStrings = [`solid 2px rgb(${color[0].r}, ${color[0].g}, ${color[0].b})`, `rgb(${color[1].r}, ${color[1].g}, ${color[1].b})`];
+ const doc = info[0];
+ const field = info[1];
+ const key = `${doc[Id]}_${field}`;
+ const cell = this.getCellElement(doc, field);
+ this._cellHighlightColors.set(key, [colorStrings[0], colorStrings[1]]);
+ cell.style.border = colorStrings[0];
+ cell.style.backgroundColor = colorStrings[1];
+ }
+ };
+
+ //Used in SchemaRowBox
@action
addRowRef = (doc: Doc, ref: HTMLDivElement) => this._rowEles.set(doc, ref);
@@ -477,33 +715,47 @@ export class CollectionSchemaView extends CollectionSubView() {
@action
clearSelection = () => {
+ if (this._referenceSelectMode.enabled) return;
DocumentView.DeselectAll();
this.deselectAllCells();
};
- selectRows = (doc: Doc, lastSelected: Doc) => {
+ selectRow = (doc: Doc, lastSelected: Doc) => {
const index = this.rowIndex(doc);
const lastSelectedRow = this.rowIndex(lastSelected);
const startRow = Math.min(lastSelectedRow, index);
const endRow = Math.max(lastSelectedRow, index);
for (let i = startRow; i <= endRow; i++) {
- const currDoc = this.sortedDocs.docs[i];
+ const currDoc = this.docsWithDrag.docs[i];
if (!this._selectedDocs.includes(currDoc)) {
this.selectCell(currDoc, this._selectedCol, false, true);
}
}
};
+ //Used in SchemaRowBox
+ selectReference = (doc: Doc | undefined, col: number) => {
+ if (!doc) return;
+ const docIndex = DocumentView.getDocViewIndex(doc);
+ const field = this.columnKeys[col];
+ const refToAdd = `d${docIndex}.${field}`;
+ const editedField = this._referenceSelectMode.currEditing ? (this._referenceSelectMode.currEditing as SchemaCellField) : null;
+ editedField?.insertText(refToAdd, true);
+ editedField?.setupRefSelect(false);
+ return;
+ };
+
@action
selectCell = (doc: Doc, col: number, shiftKey: boolean, ctrlKey: boolean) => {
+ this.closeNewColumnMenu();
if (!shiftKey && !ctrlKey) this.clearSelection();
!this._selectedCells && (this._selectedCells = []);
- !shiftKey && this._selectedCells && this._selectedCells.push(doc);
+ !shiftKey && this._selectedCells.push(doc);
const index = this.rowIndex(doc);
if (!this) return;
const lastSelected = Array.from(this._selectedDocs).lastElement();
- if (shiftKey && lastSelected && !this._selectedDocs.includes(doc)) this.selectRows(doc, lastSelected);
+ if (shiftKey && lastSelected && !this._selectedDocs.includes(doc)) this.selectRow(doc, lastSelected);
else if (ctrlKey) {
if (lastSelected && this._selectedDocs.includes(doc)) {
DocumentView.DeselectView(DocumentView.getFirstDocumentView(doc));
@@ -513,8 +765,6 @@ export class CollectionSchemaView extends CollectionSubView() {
this._selectedCol = col;
if (this._lowestSelectedIndex === -1 || index < this._lowestSelectedIndex) this._lowestSelectedIndex = index;
-
- // let selectedIndexes: Array<Number> = this._selectedCells.map(doc => this.rowIndex(doc));
};
@action
@@ -529,41 +779,26 @@ export class CollectionSchemaView extends CollectionSubView() {
this._lowestSelectedIndex = -1;
};
- sortedSelectedDocs = () => this.sortedDocs.docs.filter(doc => this._selectedDocs.includes(doc));
-
@computed
get rowDropIndex() {
const mouseY = this.ScreenToLocalBoxXf().transformPoint(this._mouseCoordinates.x, this._mouseCoordinates.y)[1];
return this.findRowDropIndex(mouseY);
}
+ @action
onInternalDrop = (e: Event, de: DragManager.DropEvent) => {
if (de.complete.columnDragData) {
- this._colBeingDragged = false;
- e.stopPropagation();
-
- this._colEles.forEach((colRef, i) => {
- // style for menu cell
- colRef.style.borderLeft = '';
- colRef.style.borderRight = '';
- colRef.style.borderTop = '';
-
- this.childDocs.forEach(doc => {
- if (!(this._selectedDocs.includes(doc) && i === this._selectedCol)) {
- this._rowEles.get(doc).children[1].children[i].style.borderLeft = '';
- this._rowEles.get(doc).children[1].children[i].style.borderRight = '';
- this._rowEles.get(doc).children[1].children[i].style.borderBottom = '';
- }
- });
+ setTimeout(() => {
+ this.setColDrag(false);
});
+ e.stopPropagation();
return true;
}
const draggedDocs = de.complete.docDragData?.draggedDocuments;
if (draggedDocs && super.onInternalDrop(e, de) && !this.sortField) {
- const map = draggedDocs?.map(doc => this.rowIndex(doc));
- console.log(map);
- this.dataDoc[this.fieldKey ?? 'data'] = new List<Doc>([...this.sortedDocs.docs]);
+ const docs = this.docsWithDrag.docs.slice();
+ this.dataDoc[this.fieldKey ?? 'data'] = new List<Doc>([...docs]);
this.clearSelection();
draggedDocs.forEach(doc => {
DocumentView.addViewRenderedCb(doc, dv => dv.select(true));
@@ -616,119 +851,44 @@ export class CollectionSchemaView extends CollectionSubView() {
return undefined;
};
- @computed get fieldDefaultInput() {
- switch (this._newFieldType) {
- case ColumnType.Number:
- return (
- <input
- type="number"
- name=""
- id=""
- value={Number(this._newFieldDefault ?? 0)}
- onPointerDown={e => e.stopPropagation()}
- onChange={action(e => {
- this._newFieldDefault = e.target.value;
- })}
- />
- );
- case ColumnType.Boolean:
- return (
- <>
- <input
- type="checkbox"
- value={this._newFieldDefault?.toString()}
- onPointerDown={e => e.stopPropagation()}
- onChange={action(e => {
- this._newFieldDefault = e.target.checked;
- })}
- />
- {this._newFieldDefault ? 'true' : 'false'}
- </>
- );
- case ColumnType.String:
- return (
- <input
- type="text"
- name=""
- id=""
- value={this._newFieldDefault?.toString() ?? ''}
- onPointerDown={e => e.stopPropagation()}
- onChange={action(e => {
- this._newFieldDefault = e.target.value;
- })}
- />
- );
- default:
- return undefined;
- }
- }
-
- onSearchKeyDown = (e: React.KeyboardEvent) => {
- switch (e.key) {
- case 'Enter':
- this._menuKeys.length > 0 && this._menuValue.length > 0
- ? this.setKey(this._menuKeys[0])
- : runInAction(() => {
- this._makeNewField = true;
- });
- break;
- case 'Escape':
- this.closeColumnMenu();
- break;
- default:
- }
- };
-
@action
- setKey = (key: string, defaultVal?: string | number | boolean) => {
+ setKey = (key: string, defaultVal?: string, index?: number) => {
+ if (this.columnKeys.includes(key)) return;
+
if (this._makeNewColumn) {
- this.addColumn(key, defaultVal);
- } else {
- this.changeColumnKey(this._columnMenuIndex!, key, defaultVal);
- }
- this.closeColumnMenu();
- };
+ this.addColumn(this.columnKeys.indexOf(key), key, defaultVal);
+ this._makeNewColumn = false;
+ } else this.changeColumnKey(this._columnMenuIndex! | index!, key, defaultVal);
- setColumnValues = (key: string, value: string) => {
- const selectedDocs: Doc[] = [];
- this.childDocs.forEach(doc => {
- const docIsSelected = this._selectedCells && !(this._selectedCells?.filter(d => d === doc).length === 0);
- if (docIsSelected) {
- selectedDocs.push(doc);
- }
- });
- if (selectedDocs.length === 1) {
- this.childDocs.forEach(doc => Doc.SetField(doc, key, value));
- } else {
- selectedDocs.forEach(doc => Doc.SetField(doc, key, value));
- }
- return true;
+ this.closeNewColumnMenu();
};
- setSelectedColumnValues = (key: string, value: string) => {
- this.childDocs.forEach(doc => {
- const docIsSelected = this._selectedCells && !(this._selectedCells?.filter(d => d === doc).length === 0);
- if (docIsSelected) {
- Doc.SetField(doc, key, value);
- }
- });
+ /**
+ * Used in SchemaRowBox to set
+ * @param key
+ * @param value
+ * @returns
+ */
+ setCellValues = (key: string, value: string) => {
+ if (this._selectedCells.length === 1) this.docs.forEach(doc => !doc._lockedSchemaEditing && Doc.SetField(doc, key, value));
+ else this._selectedCells.forEach(doc => !doc._lockedSchemaEditing && Doc.SetField(doc, key, value));
return true;
};
@action
- openColumnMenu = (index: number, newCol: boolean) => {
+ openNewColumnMenu = (index: number, newCol: boolean) => {
+ this.closeFilterMenu();
+
this._makeNewColumn = false;
this._columnMenuIndex = index;
this._menuValue = '';
this._menuKeys = this.documentKeys;
- this._makeNewField = false;
this._newFieldWarning = '';
- this._makeNewField = false;
this._makeNewColumn = newCol;
};
@action
- closeColumnMenu = () => {
+ closeNewColumnMenu = () => {
this._columnMenuIndex = undefined;
};
@@ -743,32 +903,112 @@ export class CollectionSchemaView extends CollectionSubView() {
this._filterColumnIndex = undefined;
};
+ @undoBatch
+ setColumnSort = (field: string | undefined, desc: boolean = false) => {
+ this.layoutDoc.sortField = field;
+ this.layoutDoc.sortDesc = desc;
+ };
+
openContextMenu = (x: number, y: number, index: number) => {
- this.closeColumnMenu();
+ this.closeNewColumnMenu();
this.closeFilterMenu();
- ContextMenu.Instance.clearItems();
- ContextMenu.Instance.addItem({
- description: 'Change field',
- event: () => this.openColumnMenu(index, false),
+ const cm = ContextMenu.Instance;
+ cm.clearItems();
+
+ const fieldSortedAsc = this.sortField === this.columnKeys[index] && !this.sortDesc;
+ const fieldSortedDesc = this.sortField === this.columnKeys[index] && this.sortDesc;
+ const revealOptions = cm.findByDescription('Sort column');
+ const sortOptions: ContextMenuProps[] = revealOptions && revealOptions && 'subitems' in revealOptions ? (revealOptions.subitems ?? []) : [];
+ sortOptions.push({
+ description: 'Sort A-Z',
+ event: () => {
+ this.setColumnSort(undefined);
+ const field = this.columnKeys[index];
+ this._containedDocs = this.sortDocs(field, false);
+ setTimeout(() => {
+ this.highlightSortedColumn(field, false);
+ setTimeout(() => this.highlightSortedColumn(), 480);
+ }, 20);
+ },
+ icon: 'arrow-down-a-z',
+ });
+ sortOptions.push({
+ description: 'Sort Z-A',
+ event: () => {
+ this.setColumnSort(undefined);
+ const field = this.columnKeys[index];
+ this._containedDocs = this.sortDocs(field, true);
+ setTimeout(() => {
+ this.highlightSortedColumn(field, true);
+ setTimeout(() => this.highlightSortedColumn(), 480);
+ }, 20);
+ },
+ icon: 'arrow-up-z-a',
+ });
+ sortOptions.push({
+ description: 'Persistent Sort A-Z',
+ event: () => {
+ if (fieldSortedAsc){
+ this.setColumnSort(undefined);
+ this.highlightSortedColumn();
+ } else {
+ this.sortDocs(this.columnKeys[index], false);
+ this.setColumnSort(this.columnKeys[index], false);
+ }
+ },
+ icon: fieldSortedAsc ? 'lock' : 'lock-open'}); // prettier-ignore
+ sortOptions.push({
+ description: 'Persistent Sort Z-A',
+ event: () => {
+ if (fieldSortedDesc){
+ this.setColumnSort(undefined);
+ this.highlightSortedColumn();
+ } else {
+ this.sortDocs(this.columnKeys[index], true);
+ this.setColumnSort(this.columnKeys[index], true);
+ }
+ },
+ icon: fieldSortedDesc ? 'lock' : 'lock-open'}); // prettier-ignore
+
+ cm.addItem({
+ description: `Change field`,
+ event: () => this.openNewColumnMenu(index, false),
icon: 'pencil-alt',
});
- ContextMenu.Instance.addItem({
+ cm.addItem({
description: 'Filter field',
event: () => this.openFilterMenu(index),
icon: 'filter',
});
- ContextMenu.Instance.addItem({
+ cm.addItem({
+ description: 'Sort column',
+ addDivider: false,
+ noexpand: true,
+ subitems: sortOptions,
+ icon: 'sort',
+ });
+ cm.addItem({
+ description: 'Add column to left',
+ event: () => this.addColumn(index),
+ icon: 'plus',
+ });
+ cm.addItem({
+ description: 'Add column to right',
+ event: () => this.addColumn(index + 1),
+ icon: 'plus',
+ });
+ cm.addItem({
description: 'Delete column',
event: () => this.removeColumn(index),
icon: 'trash',
});
- ContextMenu.Instance.displayMenu(x, y, undefined, false);
+ cm.displayMenu(x, y, undefined, false);
};
+ //used in schemacolumnheader
@action
- updateKeySearch = (e: React.ChangeEvent<HTMLInputElement>) => {
- this._menuValue = e.target.value;
- this._menuKeys = this.documentKeys.filter(value => value.toLowerCase().includes(this._menuValue.toLowerCase()));
+ updateKeySearch = (val: string) => {
+ this._menuKeys = this.documentKeys.filter(value => value.toLowerCase().includes(val.toLowerCase()));
};
getFieldFilters = (field: string) => StrListCast(this.Document._childFilters).filter(filter => filter.split(Doc.FilterSep)[0] === field);
@@ -792,65 +1032,6 @@ export class CollectionSchemaView extends CollectionSubView() {
this._filterSearchValue = e.target.value;
};
- @computed get newFieldMenu() {
- return (
- <div className="schema-new-key-options">
- <div className="schema-key-type-option">
- <input
- type="radio"
- name="newFieldType"
- checked={this._newFieldType === ColumnType.Number}
- onChange={action(() => {
- this._newFieldType = ColumnType.Number;
- this._newFieldDefault = 0;
- })}
- />
- number
- </div>
- <div className="schema-key-type-option">
- <input
- type="radio"
- name="newFieldType"
- checked={this._newFieldType === ColumnType.Boolean}
- onChange={action(() => {
- this._newFieldType = ColumnType.Boolean;
- this._newFieldDefault = false;
- })}
- />
- boolean
- </div>
- <div className="schema-key-type-option">
- <input
- type="radio"
- name="newFieldType"
- checked={this._newFieldType === ColumnType.String}
- onChange={action(() => {
- this._newFieldType = ColumnType.String;
- this._newFieldDefault = '';
- })}
- />
- string
- </div>
- <div className="schema-key-default-val">value: {this.fieldDefaultInput}</div>
- <div className="schema-key-warning">{this._newFieldWarning}</div>
- <div
- className="schema-column-menu-button"
- onPointerDown={action(() => {
- if (this.documentKeys.includes(this._menuValue)) {
- this._newFieldWarning = 'Field already exists';
- } else if (this._menuValue.length === 0) {
- this._newFieldWarning = 'Field cannot be an empty string';
- } else {
- this.setKey(this._menuValue, this._newFieldDefault);
- }
- this._columnMenuIndex = undefined;
- })}>
- done
- </div>
- </div>
- );
- }
-
onKeysPassiveWheel = (e: WheelEvent) => {
// if scrollTop is 0, then don't let wheel trigger scroll on any container (which it would since onScroll won't be triggered on this)
if (!this._oldKeysWheel?.scrollTop && e.deltaY <= 0) e.preventDefault();
@@ -861,14 +1042,6 @@ export class CollectionSchemaView extends CollectionSubView() {
return (
<div className="schema-key-search">
<div
- className="schema-column-menu-button"
- onPointerDown={action(e => {
- e.stopPropagation();
- this._makeNewField = true;
- })}>
- + new field
- </div>
- <div
className="schema-key-list"
ref={r => {
this._oldKeysWheel?.removeEventListener('wheel', this.onKeysPassiveWheel);
@@ -886,11 +1059,8 @@ export class CollectionSchemaView extends CollectionSubView() {
<p>
<span className="schema-search-result-key">
<b>{key}</b>
- {this.fieldInfos.get(key)!.fieldType ? ':' : ''}
- </span>
- <span className="schema-search-result-type" style={{ color: this.fieldInfos.get(key)!.readOnly ? 'red' : 'inherit' }}>
- {this.fieldInfos.get(key)!.fieldType}
</span>
+ <span>: </span>
<span className="schema-search-result-desc">&nbsp;&nbsp;{this.fieldInfos.get(key)!.description}</span>
</p>
</div>
@@ -903,17 +1073,8 @@ export class CollectionSchemaView extends CollectionSubView() {
@computed get renderColumnMenu() {
const x = this._columnMenuIndex! === -1 ? 0 : this.displayColumnWidths.reduce((total, curr, index) => total + (index < this._columnMenuIndex! ? curr : 0), CollectionSchemaView._rowMenuWidth);
return (
- <div className="schema-column-menu" style={{ left: x, minWidth: CollectionSchemaView._minColWidth }}>
- <input className="schema-key-search-input" type="text" onKeyDown={this.onSearchKeyDown} onChange={this.updateKeySearch} onPointerDown={e => e.stopPropagation()} />
- {this._makeNewField ? this.newFieldMenu : this.keysDropdown}
- </div>
- );
- }
- get renderKeysMenu() {
- return (
- <div className="schema-column-menu" style={{ left: 0, minWidth: CollectionSchemaView._minColWidth }}>
- <input className="schema-key-search-input" type="text" onKeyDown={this.onSearchKeyDown} onChange={this.updateKeySearch} onPointerDown={e => e.stopPropagation()} />
- {this._makeNewField ? this.newFieldMenu : this.keysDropdown}
+ <div className="schema-column-menu" style={{ left: x, maxWidth: `${Math.max(this._colEles[this._columnMenuIndex ?? 0].offsetWidth, 150)}px` }}>
+ {this.keysDropdown}
</div>
);
}
@@ -939,13 +1100,7 @@ export class CollectionSchemaView extends CollectionSubView() {
}
return (
<div key={key} className="schema-filter-option">
- <input //
- type="checkbox"
- onPointerDown={e => e.stopPropagation()}
- onClick={e => e.stopPropagation()}
- onChange={e => Doc.setDocFilter(this.Document, columnKey, key, e.target.checked ? 'check' : 'remove')}
- checked={bool}
- />
+ <input type="checkbox" onPointerDown={e => e.stopPropagation()} onClick={e => e.stopPropagation()} onChange={e => Doc.setDocFilter(this.Document, columnKey, key, e.target.checked ? 'check' : 'remove')} checked={bool} />
<span style={{ paddingLeft: 4 }}>{key}</span>
</div>
);
@@ -955,7 +1110,7 @@ export class CollectionSchemaView extends CollectionSubView() {
@computed get renderFilterMenu() {
const x = this.displayColumnWidths.reduce((total, curr, index) => total + (index < this._filterColumnIndex! ? curr : 0), CollectionSchemaView._rowMenuWidth);
return (
- <div className="schema-filter-menu" style={{ left: x, minWidth: CollectionSchemaView._minColWidth }}>
+ <div className="schema-filter-menu" style={{ left: x, maxWidth: `${Math.max(this._colEles[this._columnMenuIndex ?? 0].offsetWidth, 150)}px` }}>
<input className="schema-filter-input" type="text" value={this._filterSearchValue} onKeyDown={this.onFilterKeyDown} onChange={this.updateFilterSearch} onPointerDown={e => e.stopPropagation()} />
{this.renderFilterOptions}
<div
@@ -970,51 +1125,181 @@ export class CollectionSchemaView extends CollectionSubView() {
);
}
+ @action setColDrag = (beingDragged: boolean) => {
+ this._colBeingDragged = beingDragged;
+ !beingDragged && this.removeDragHighlight();
+ };
+
+ @action updateMouseCoordinates = (e: React.PointerEvent<HTMLDivElement>) => {
+ const prevX = this._mouseCoordinates.x;
+ const prevY = this._mouseCoordinates.y;
+ this._mouseCoordinates = { x: e.clientX, y: e.clientY, prevX: prevX, prevY: prevY };
+ };
+
@action
onPointerMove = (e: React.PointerEvent<HTMLDivElement>) => {
if (DragManager.docsBeingDragged.length) {
- this._mouseCoordinates = { x: e.clientX, y: e.clientY };
+ this.updateMouseCoordinates(e);
}
if (this._colBeingDragged) {
+ this.updateMouseCoordinates(e);
const newIndex = this.findColDropIndex(e.clientX);
- if (newIndex !== this._draggedColIndex) this.moveColumn(this._draggedColIndex, newIndex ?? this._draggedColIndex);
- this._draggedColIndex = newIndex || this._draggedColIndex;
- this.highlightDraggedColumn(newIndex ?? this._draggedColIndex);
+ const direction: number = this._mouseCoordinates.x > this._mouseCoordinates.prevX ? 1 : 0;
+ if (newIndex !== undefined && ((newIndex > this._draggedColIndex && direction === 1) || (newIndex < this._draggedColIndex && direction === 0))) {
+ this.moveColumn(this._draggedColIndex, newIndex ?? this._draggedColIndex);
+ this._draggedColIndex = newIndex !== undefined ? newIndex : this._draggedColIndex;
+ }
+ this.highlightSortedColumn(); //TODO: Make this more efficient
+ this.restoreCellHighlights();
+ !(this.sortField && this._draggedColIndex === this.columnKeys.indexOf(this.sortField)) && this.highlightDraggedColumn(this._draggedColIndex);
}
};
- @computed get sortedDocs() {
- const draggedDocs = this.isContentActive() ? DragManager.docsBeingDragged : [];
- const field = StrCast(this.layoutDoc.sortField);
- const desc = BoolCast(this.layoutDoc.sortDesc); // is this an ascending or descending sort
- const staticDocs = this.childDocs.filter(d => !draggedDocs.includes(d));
- const docs = !field
- ? staticDocs
- : [...staticDocs].sort((docA, docB) => {
- // this sorts the documents based on the selected field. returning -1 for a before b, 0 for a = b, 1 for a > b
- const aStr = Field.toString(docA[field] as FieldType);
- const bStr = Field.toString(docB[field] as FieldType);
- let out = 0;
- if (aStr < bStr) out = -1;
- if (aStr > bStr) out = 1;
- if (desc) out *= -1;
- return out;
- });
-
- docs.splice(this.rowDropIndex, 0, ...draggedDocs);
+ /**
+ * Gets docs contained by collections within the schema. Currently defunct.
+ * @param doc
+ * @param displayed
+ * @returns
+ */
+ // subCollectionDocs = (doc: Doc, displayed: boolean) => {
+ // const childDocs = DocListCast(doc[Doc.LayoutFieldKey(doc)]);
+ // let collections: Array<Doc> = [];
+ // if (displayed) collections = childDocs.filter(d => d.type === 'collection' && d._childrenSharedWithSchema);
+ // else collections = childDocs.filter(d => d.type === 'collection' && !d._childrenSharedWithSchema);
+ // let toReturn: Doc[] = [...childDocs];
+ // collections.forEach(d => toReturn = toReturn.concat(this.subCollectionDocs(d, displayed)));
+ // return toReturn;
+ // }
+
+ /**
+ * Applies any filters active on the schema to filter out docs that don't match.
+ */
+ @computed get filteredDocs() {
+ const childDocFilters = this.childDocFilters();
+ const childFiltersByRanges = this.childDocRangeFilters();
+ const searchDocs = this.searchFilterDocs();
+
+ const docsforFilter: Doc[] = [];
+ this._containedDocs.forEach(d => {
+ // dragging facets
+ const dragged = this._props.childFilters?.().some(f => f.includes(ClientUtils.noDragDocsFilter));
+ if (dragged && SnappingManager.CanEmbed && DragManager.docsBeingDragged.includes(d)) return;
+ let notFiltered = d.z || Doc.IsSystem(d) || DocUtils.FilterDocs([d], this.unrecursiveDocFilters(), childFiltersByRanges, this.Document).length > 0;
+ if (notFiltered) {
+ notFiltered = (!searchDocs.length || searchDocs.includes(d)) && DocUtils.FilterDocs([d], childDocFilters, childFiltersByRanges, this.Document).length > 0;
+ const fieldKey = Doc.LayoutFieldKey(d);
+ const isAnnotatableDoc = d[fieldKey] instanceof List && !(d[fieldKey] as List<Doc>)?.some(ele => !(ele instanceof Doc));
+ const docChildDocs = d[isAnnotatableDoc ? fieldKey + '_annotations' : fieldKey];
+ const sidebarDocs = isAnnotatableDoc && d[fieldKey + '_sidebar'];
+ if (docChildDocs !== undefined || sidebarDocs !== undefined) {
+ let subDocs = [...DocListCast(docChildDocs), ...DocListCast(sidebarDocs)];
+ if (subDocs.length > 0) {
+ let newarray: Doc[] = [];
+ notFiltered = notFiltered || (!searchDocs.length && DocUtils.FilterDocs(subDocs, childDocFilters, childFiltersByRanges, d).length);
+ while (subDocs.length > 0 && !notFiltered) {
+ newarray = [];
+ // eslint-disable-next-line no-loop-func
+ subDocs.forEach(t => {
+ const docFieldKey = Doc.LayoutFieldKey(t);
+ const isSubDocAnnotatable = t[docFieldKey] instanceof List && !(t[docFieldKey] as List<Doc>)?.some(ele => !(ele instanceof Doc));
+ notFiltered = notFiltered || ((!searchDocs.length || searchDocs.includes(t)) && ((!childDocFilters.length && !childFiltersByRanges.length) || DocUtils.FilterDocs([t], childDocFilters, childFiltersByRanges, d).length));
+ DocListCast(t[isSubDocAnnotatable ? docFieldKey + '_annotations' : docFieldKey]).forEach(newdoc => newarray.push(newdoc));
+ isSubDocAnnotatable && DocListCast(t[docFieldKey + '_sidebar']).forEach(newdoc => newarray.push(newdoc));
+ });
+ subDocs = newarray;
+ }
+ }
+ }
+ }
+ notFiltered && docsforFilter.push(d);
+ });
+ return docsforFilter;
+ }
+
+ /**
+ * Returns all child docs of the schema and child docs of contained collections that satisfy applied filters.
+ */
+ @computed get docs() {
+ //let docsFromChildren: Doc[] = [];
+
+ // Functionality for adding child docs
+ //const displayedCollections = this.childDocs.filter(d => d.type === 'collection' && d._childrenSharedWithSchema);
+ // displayedCollections.forEach(d => {
+ // let docsNotAlreadyDisplayed = this.subCollectionDocs(d, true).filter(dc => !this._containedDocs.includes(dc));
+ // docsFromChildren = docsFromChildren.concat(docsNotAlreadyDisplayed);
+ // });
+
+ return this.filteredDocs;
+ }
+
+ /**
+ * Sorts docs first alphabetically and then numerically.
+ * @param field the column being sorted
+ * @param desc whether the sort is ascending or descending
+ * @param persistent whether the sort is applied persistently or is one-shot
+ * @returns
+ */
+ sortDocs = (field: string, desc: boolean, persistent?: boolean) => {
+ const numbers: Doc[] = [];
+ const strings: Doc[] = [];
+
+ this.docs.forEach(doc => {
+ if (!isNaN(Number(Field.toString(doc[field] as FieldType)))) numbers.push(doc);
+ else strings.push(doc);
+ });
+
+ const sortedNums = numbers.sort((numOne, numTwo) => {
+ const numA = Number(Field.toString(numOne[field] as FieldType));
+ const numB = Number(Field.toString(numTwo[field] as FieldType));
+ return desc ? numA - numB : numB - numA;
+ });
+
+ const collator = new Intl.Collator(undefined, { sensitivity: 'base' });
+ let sortedStrings;
+ if (!desc) {
+ sortedStrings = strings.slice().sort((docA, docB) => collator.compare(Field.toString(docA[field] as FieldType), Field.toString(docB[field] as FieldType)));
+ } else sortedStrings = strings.slice().sort((docB, docA) => collator.compare(Field.toString(docA[field] as FieldType), Field.toString(docB[field] as FieldType)));
+
+ const sortedDocs = desc ? sortedNums.concat(sortedStrings) : sortedStrings.concat(sortedNums);
+ if (!persistent) this._containedDocs = sortedDocs;
+ return sortedDocs;
+ };
+
+ /**
+ * Returns all docs minus those currently being dragged by the user.
+ */
+ @computed get docsWithDrag() {
+ let docs = this.docs.slice();
+ if (this.sortField) {
+ const field = StrCast(this.layoutDoc.sortField);
+ const desc = BoolCast(this.layoutDoc.sortDesc); // is this an ascending or descending sort
+ docs = this.sortDocs(field, desc, true);
+ } else {
+ const draggedDocs = this.isContentActive() ? DragManager.docsBeingDragged.filter(doc => !(doc.type === 'fonticonbox')) : [];
+ docs = docs.filter(d => !draggedDocs.includes(d));
+ docs.splice(this.rowDropIndex, 0, ...draggedDocs);
+ }
+
return { docs };
}
rowHeightFunc = () => (BoolCast(this.layoutDoc._schema_singleLine) ? CollectionSchemaView._rowSingleLineHeight : CollectionSchemaView._rowHeight);
- sortedDocsFunc = () => this.sortedDocs;
isContentActive = () => this._props.isSelected() || this._props.isContentActive();
screenToLocal = () => this.ScreenToLocalBoxXf().translate(-this.tableWidth, 0);
previewWidthFunc = () => this.previewWidth;
onPassiveWheel = (e: WheelEvent) => e.stopPropagation();
- _oldWheel: HTMLDivElement | null = null;
+ displayedDocsFunc = () => this.docsWithDrag.docs;
render() {
return (
- <div className="collectionSchemaView" ref={(ele: HTMLDivElement | null) => this.createDashEventsTarget(ele)} onDrop={this.onExternalDrop.bind(this)} onPointerMove={e => this.onPointerMove(e)}>
+ <div
+ className="collectionSchemaView"
+ ref={(ele: HTMLDivElement | null) => this.createDashEventsTarget(ele)}
+ onDrop={this.onExternalDrop.bind(this)}
+ onPointerMove={e => this.onPointerMove(e)}
+ onPointerDown={() => {
+ this.closeNewColumnMenu();
+ this.setColDrag(false);
+ }}>
<div ref={this._menuTarget} style={{ background: 'red', top: 0, left: 0, position: 'absolute', zIndex: 10000 }} />
<div
className="schema-table"
@@ -1027,26 +1312,37 @@ export class CollectionSchemaView extends CollectionSubView() {
}}>
<div className="schema-header-row" style={{ height: this.rowHeightFunc() }}>
<div className="row-menu" style={{ width: CollectionSchemaView._rowMenuWidth }}>
- <Popup
- placement="right"
- background={SettingsManager.userBackgroundColor}
- color={SettingsManager.userColor}
- toggle={<FontAwesomeIcon onPointerDown={() => this.openColumnMenu(-1, true)} icon="plus" />}
- trigger={PopupTrigger.CLICK}
- type={Type.TERT}
- isOpen={this._columnMenuIndex !== -1 ? false : undefined}
- popup={this.renderKeysMenu}
+ <IconButton
+ tooltip="Add a new key"
+ icon={<FontAwesomeIcon icon="plus" size="lg" />}
+ size={Size.XSMALL}
+ color={'black'}
+ onPointerDown={e =>
+ setupMoveUpEvents(
+ this,
+ e,
+ returnFalse,
+ emptyFunction,
+ undoable(clickEv => {
+ clickEv.stopPropagation();
+ this.addColumn();
+ }, 'add key to schema')
+ )
+ }
/>
</div>
{this.columnKeys.map((key, index) => (
<SchemaColumnHeader
- // eslint-disable-next-line react/no-array-index-key
+ //cleanupField={this.cleanupComputedField}
+ ref={r => r && this._headerRefs.push(r)}
+ keysDropdown={this.keysDropdown}
+ schemaView={this}
+ columnWidth={() => CollectionSchemaView._minColWidth} //TODO: update
+ Document={this.Document}
key={index}
columnIndex={index}
columnKeys={this.columnKeys}
columnWidths={this.displayColumnWidths}
- sortField={this.sortField}
- sortDesc={this.sortDesc}
setSort={this.setColumnSort}
rowHeight={this.rowHeightFunc}
removeColumn={this.removeColumn}
@@ -1064,7 +1360,7 @@ export class CollectionSchemaView extends CollectionSubView() {
// eslint-disable-next-line no-use-before-define
<CollectionSchemaViewDocs
schema={this}
- childDocs={this.sortedDocsFunc}
+ childDocs={this.displayedDocsFunc}
rowHeight={this.rowHeightFunc}
setRef={(ref: HTMLDivElement | null) => {
this._tableContentRef = ref;
@@ -1151,7 +1447,6 @@ class CollectionSchemaViewDoc extends ObservableReactComponent<CollectionSchemaV
return (
<DocumentView
key={this._props.doc[Id]}
- // eslint-disable-next-line react/jsx-props-no-spreading
{...this._props.schema._props}
containerViewPath={this._props.schema.childContainerViewPath}
LayoutTemplate={this._props.schema._props.childLayoutTemplate}
@@ -1187,7 +1482,7 @@ class CollectionSchemaViewDoc extends ObservableReactComponent<CollectionSchemaV
interface CollectionSchemaViewDocsProps {
schema: CollectionSchemaView;
setRef: (ref: HTMLDivElement | null) => void;
- childDocs: () => { docs: Doc[] };
+ childDocs: () => Doc[];
rowHeight: () => number;
}
@@ -1196,7 +1491,7 @@ class CollectionSchemaViewDocs extends React.Component<CollectionSchemaViewDocsP
render() {
return (
<div className="schema-table-content" ref={this.props.setRef} style={{ height: `calc(100% - ${CollectionSchemaView._newNodeInputHeight + this.props.rowHeight()}px)` }}>
- {this.props.childDocs().docs.map((doc: Doc, index: number) => (
+ {this.props.childDocs().map((doc: Doc, index: number) => (
<div key={doc[Id]} className="schema-row-wrapper" style={{ height: this.props.rowHeight() }}>
<CollectionSchemaViewDoc doc={doc} schema={this.props.schema} index={index} rowHeight={this.props.rowHeight} />
</div>
diff --git a/src/client/views/collections/collectionSchema/SchemaCellField.tsx b/src/client/views/collections/collectionSchema/SchemaCellField.tsx
new file mode 100644
index 000000000..5a64ecc62
--- /dev/null
+++ b/src/client/views/collections/collectionSchema/SchemaCellField.tsx
@@ -0,0 +1,406 @@
+import { IReactionDisposer, action, computed, makeObservable, observable, reaction, runInAction } from 'mobx';
+import { ObservableReactComponent } from '../../ObservableReactComponent';
+import { observer } from 'mobx-react';
+import { OverlayView } from '../../OverlayView';
+import { DocumentIconContainer } from '../../nodes/DocumentIcon';
+import React, { FormEvent } from 'react';
+import { FieldView, FieldViewProps } from '../../nodes/FieldView';
+import { FieldType, ObjectField } from '../../../../fields/ObjectField';
+import { Doc } from '../../../../fields/Doc';
+import { DocumentView } from '../../nodes/DocumentView';
+import DOMPurify from 'dompurify';
+
+/**
+ * The SchemaCellField renders text in schema cells while the user is editing, and updates the
+ * contents of the field based on user input. It handles some cell-side logic for equations, such
+ * as how equations are broken up within the text.
+ *
+ * The current implementation parses innerHTML to create spans based on the text in the cell.
+ * A more robust/safer approach would directly add elements in the react structure, but this
+ * has been challenging to implement.
+ */
+
+export interface SchemaCellFieldProps {
+ contents: FieldType;
+ fieldContents?: FieldViewProps;
+ editing?: boolean;
+ oneLine?: boolean;
+ Document: Doc;
+ fieldKey: string;
+ // eslint-disable-next-line no-use-before-define
+ refSelectModeInfo: { enabled: boolean; currEditing: SchemaCellField | undefined };
+ highlightCells?: (text: string) => void;
+ GetValue(): string | undefined;
+ SetValue(value: string, shiftDown?: boolean, enterKey?: boolean): boolean;
+ getCells: (text: string) => HTMLDivElement[] | [];
+}
+
+@observer
+export class SchemaCellField extends ObservableReactComponent<SchemaCellFieldProps> {
+ private _disposers: { [name: string]: IReactionDisposer } = {};
+ private _inputref: HTMLDivElement | null = null;
+ private _unrenderedContent: string = '';
+ _overlayDisposer?: () => void;
+ @observable _editing: boolean = false;
+ @observable _displayedContent = '';
+ @observable _inCellSelectMode: boolean = false;
+ @observable _dependencyMessageShown: boolean = false;
+
+ constructor(props: SchemaCellFieldProps) {
+ super(props);
+ makeObservable(this);
+ setTimeout(() => {
+ this._unrenderedContent = this._props.GetValue() ?? '';
+ this.setContent(this._unrenderedContent);
+ }); //must be moved to end of batch or else other docs aren't loaded, so render as d-1 in function
+ }
+
+ get docIndex(){return DocumentView.getDocViewIndex(this._props.Document);} // prettier-ignore
+
+ get selfRefPattern() {
+ return `d${this.docIndex}.${this._props.fieldKey}`;
+ }
+
+ @computed get lastCharBeforeCursor() {
+ const pos = this.cursorPosition;
+ const content = this._unrenderedContent;
+ const text = this._unrenderedContent.substring(0, pos ?? content.length);
+ for (let i = text.length - 1; i > 0; --i) {
+ if (text.charCodeAt(i) !== 160 && text.charCodeAt(i) !== 32) {
+ return text[i];
+ }
+ }
+ return null;
+ }
+
+ @computed get refSelectConditionMet() {
+ const char = this.lastCharBeforeCursor;
+ return char === '+' || char === '*' || char === '/' || char === '%' || char === '=';
+ }
+
+ componentDidMount(): void {
+ this._unrenderedContent = this._props.GetValue() ?? '';
+ this.setContent(this._unrenderedContent, true);
+ this._disposers.editing = reaction(
+ () => this._editing,
+ editing => {
+ if (editing) {
+ this.setupRefSelect(this.refSelectConditionMet);
+ setTimeout(() => {
+ if (this._inputref?.innerText.startsWith('=') || this._inputref?.innerText.startsWith(':=')) {
+ this._overlayDisposer?.();
+ this._overlayDisposer = OverlayView.Instance.addElement(<DocumentIconContainer />, { x: 0, y: 0 });
+ this._props.highlightCells?.(this._unrenderedContent);
+ this.setContent(this._unrenderedContent);
+ setTimeout(() => this.setCursorPosition(this._unrenderedContent.length));
+ }
+ });
+ } else {
+ this._overlayDisposer?.();
+ this._overlayDisposer = undefined;
+ this._props.highlightCells?.('');
+ this.setupRefSelect(false);
+ }
+ },
+ { fireImmediately: true }
+ );
+ this._disposers.fieldUpdate = reaction(
+ () => this._props.GetValue(),
+ fieldVal => {
+ console.log('Update: ' + this._props.Document.title, this._props.fieldKey, fieldVal);
+ this._unrenderedContent = fieldVal ?? '';
+ this.finalizeEdit(false, false, false);
+ }
+ );
+ }
+
+ componentDidUpdate(prevProps: Readonly<SchemaCellFieldProps>) {
+ super.componentDidUpdate(prevProps);
+ if (this._editing && this._props.editing === false) {
+ this.finalizeEdit(false, true, false);
+ } else
+ runInAction(() => {
+ if (this._props.editing !== undefined) this._editing = this._props.editing;
+ });
+ }
+
+ _unmounted = false;
+ componentWillUnmount(): void {
+ this._unmounted = true;
+ console.log('Unmount: ' + this._props.Document.title, this._props.fieldKey);
+ this._overlayDisposer?.();
+ Object.values(this._disposers).forEach(disposer => disposer?.());
+ this.finalizeEdit(false, true, false);
+ }
+
+ generateSpan = (text: string, cell: HTMLDivElement | undefined) => {
+ const selfRef = text === this.selfRefPattern;
+ return `<span style="text-decoration: ${selfRef ? 'underline' : 'none'}; text-decoration-color: red; color: ${selfRef ? 'gray' : cell?.style.borderTop.replace('2px solid', '')}">${text}</span>`;
+ };
+
+ makeSpans = (content: string) => {
+ let chunkedText = content;
+
+ const pattern = /(this|d(\d+))\.(\w+)/g;
+ const matches: string[] = [];
+ let match: RegExpExecArray | null;
+
+ const cells: Map<string, HTMLDivElement> = new Map();
+
+ while ((match = pattern.exec(content)) !== null) {
+ const cell = this._props.getCells(match[0]);
+ if (cell.length) {
+ matches.push(match[0]);
+ cells.set(match[0], cell[0]);
+ }
+ }
+
+ matches.forEach(m => {
+ chunkedText = chunkedText.replace(m, this.generateSpan(m, cells.get(m)));
+ });
+
+ return chunkedText;
+ };
+
+ /**
+ * Sets the rendered content of the cell to save user inputs.
+ * @param content the content to set
+ * @param restoreCursorPos whether the cursor should be set back to where it was rather than the 0th index; should usually be true
+ */
+ @action
+ setContent = (content: string, restoreCursorPos?: boolean) => {
+ const pos = this.cursorPosition;
+ this._displayedContent = DOMPurify.sanitize(this.makeSpans(content));
+ restoreCursorPos && setTimeout(() => this.setCursorPosition(pos));
+ };
+
+ //Called from schemaview when a cell is selected to add a reference to the equation
+ /**
+ * Inserts text at the given index.
+ * @param text The text to append.
+ * @param atPos he index at which to insert the text. If empty, defaults to end.
+ */
+ @action
+ insertText = (text: string, atPos?: boolean) => {
+ const content = this._unrenderedContent;
+ const cursorPos = this.cursorPosition;
+ const robustPos = cursorPos ?? content.length;
+ const newText = atPos ? content.slice(0, robustPos) + text + content.slice(cursorPos ?? content.length) : this._unrenderedContent.concat(text);
+ this.onChange(undefined, newText);
+ setTimeout(() => this.setCursorPosition(robustPos + text.length));
+ };
+
+ @action
+ setIsFocused = (value: boolean) => {
+ const wasFocused = this._editing;
+ this._editing = value;
+ return wasFocused !== this._editing;
+ };
+
+ /**
+ * Gets the cursor's position index within the text being edited.
+ */
+ get cursorPosition() {
+ const selection = window.getSelection();
+ if (!selection || selection.rangeCount === 0 || !this._inputref) return null;
+
+ const range = selection.getRangeAt(0);
+ const adjRange = range.cloneRange();
+
+ adjRange.selectNodeContents(this._inputref);
+ adjRange.setEnd(range.startContainer, range.startOffset);
+
+ return adjRange.toString().length;
+ }
+
+ setCursorPosition = (position: number | null) => {
+ const selection = window.getSelection();
+ if (!selection || position === null || !this._inputref) return;
+
+ const range = document.createRange();
+ range.setStart(this._inputref, 0);
+ range.collapse(true);
+
+ let currentPos = 0;
+ const setRange = (nodes: NodeList) => {
+ for (let i = 0; i < nodes.length; ++i) {
+ const node = nodes[i];
+
+ if (node.nodeType === Node.TEXT_NODE) {
+ if (!node.textContent) return;
+ const nextPos = currentPos + node.textContent.length;
+ if (position <= nextPos) {
+ range.setStart(node, position - currentPos);
+ range.collapse(true);
+ selection.removeAllRanges();
+ selection.addRange(range);
+ return true;
+ }
+ currentPos = nextPos;
+ } else if (node.nodeType === Node.ELEMENT_NODE && setRange(node.childNodes)) return true;
+ }
+ return false;
+ };
+
+ setRange(this._inputref.childNodes);
+ };
+
+ //This function checks if a visual update (eg. coloring a cell reference) should be made. It's meant to
+ //save on processing upkeep vs. constantly rerendering, but I think the savings are minimal for now
+ shouldUpdate = (prevVal: string, currVal: string) => {
+ if (this._props.getCells(currVal).length !== this._props.getCells(prevVal).length) return true;
+ };
+
+ onChange = (e: FormEvent<HTMLDivElement> | undefined, newText?: string) => {
+ const prevVal = this._unrenderedContent;
+ const targVal = newText ?? e!.currentTarget.innerText; // TODO: bang
+ if (!(targVal.startsWith(':=') || targVal.startsWith('='))) {
+ this._overlayDisposer?.();
+ this._overlayDisposer = undefined;
+ } else if (!this._overlayDisposer) {
+ this._overlayDisposer = OverlayView.Instance.addElement(<DocumentIconContainer />, { x: 0, y: 0 });
+ }
+ this._unrenderedContent = targVal;
+ this._props.highlightCells?.(targVal);
+ if (this.shouldUpdate(prevVal, targVal)) this.setContent(targVal, true);
+ this.setupRefSelect(this.refSelectConditionMet);
+ };
+
+ setupRefSelect = (enabled: boolean) => {
+ const properties = this._props.refSelectModeInfo;
+ properties.enabled = enabled;
+ properties.currEditing = enabled ? this : undefined;
+ };
+
+ @action
+ onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
+ if (e.nativeEvent.defaultPrevented) return; // hack .. DashFieldView grabs native events, but react ignores stoppedPropagation and preventDefault, so we need to check it here
+
+ switch (e.key) {
+ case 'Tab':
+ e.stopPropagation();
+ this.finalizeEdit(e.shiftKey, false, false);
+ break;
+ case 'Backspace':
+ e.stopPropagation();
+ break;
+ case 'Enter':
+ e.stopPropagation();
+ if (!e.ctrlKey) {
+ this.finalizeEdit(e.shiftKey, false, true);
+ }
+ break;
+ case 'Escape':
+ e.stopPropagation();
+ this._editing = false;
+ break;
+ case 'ArrowUp':
+ case 'ArrowDown':
+ case 'ArrowLeft':
+ case 'ArrowRight': // prettier-ignore
+ e.stopPropagation();
+ setTimeout(() => this.setupRefSelect(this.refSelectConditionMet), 0);
+ break;
+ case ' ':
+ {
+ e.stopPropagation();
+ const cursorPos = this.cursorPosition !== null ? this.cursorPosition + 1 : 0;
+ setTimeout(() => {
+ this.setContent(this._unrenderedContent);
+ setTimeout(() => this.setCursorPosition(cursorPos));
+ }, 0);
+ }
+ break;
+ case 'u': // for some reason 'u' otherwise exits the editor
+ e.stopPropagation();
+ break;
+ case 'Shift':
+ case 'Alt':
+ case 'Meta':
+ case 'Control':
+ case ':': // prettier-ignore
+ break;
+ default:
+ break;
+ }
+ };
+
+ @action
+ onClick = (e?: React.MouseEvent) => {
+ if (this._props.editing !== false) {
+ e?.nativeEvent.stopPropagation();
+ this._editing = true;
+ }
+ };
+
+ @action
+ finalizeEdit = (shiftDown: boolean, lostFocus: boolean, enterKey: boolean) => {
+ if (this._unmounted) {
+ return;
+ }
+ if (this._unrenderedContent.replace(this.selfRefPattern, '') !== this._unrenderedContent) {
+ if (this._dependencyMessageShown) {
+ this._dependencyMessageShown = false;
+ } else alert(`Circular dependency detected. Please update the field at ${this.selfRefPattern}.`);
+ this._dependencyMessageShown = true;
+ return;
+ }
+
+ this.setContent(this._unrenderedContent);
+
+ if (!this._props.SetValue(this._unrenderedContent, shiftDown, enterKey) && !lostFocus) {
+ setTimeout(action(() => (this._editing = true)));
+ }
+ this._editing = false;
+ };
+
+ staticDisplay = () => {
+ return <span className="editableView-static">{this._props.fieldContents ? <FieldView {...this._props.fieldContents} /> : ''}</span>;
+ };
+
+ renderEditor = () => {
+ return (
+ <div
+ contentEditable
+ className="schemaField-editing"
+ ref={r => {
+ this._inputref = r;
+ }}
+ style={{ cursor: 'text', outline: 'none', overflow: 'auto', minHeight: `min(100%, ${(this._props.GetValue()?.split('\n').length || 1) * 15})`, minWidth: 20 }}
+ onBlur={() => (this._props.refSelectModeInfo.enabled ? setTimeout(() => this.setIsFocused(true), 1000) : this.finalizeEdit(false, true, false))}
+ autoFocus
+ onInput={this.onChange}
+ onKeyDown={this.onKeyDown}
+ onPointerDown={e => {
+ e.stopPropagation();
+ setTimeout(() => this.setupRefSelect(this.refSelectConditionMet), 0);
+ }} //timeout callback ensures that refSelectMode is properly set
+ onClick={e => e.stopPropagation}
+ onPointerUp={e => e.stopPropagation}
+ onPointerMove={e => {
+ e.stopPropagation();
+ e.preventDefault();
+ }}
+ dangerouslySetInnerHTML={{ __html: this._displayedContent }}></div>
+ );
+ };
+
+ render() {
+ const gval = this._props.GetValue()?.replace(/\n/g, '\\r\\n');
+ if (this._editing && gval !== undefined) {
+ return <div className={`editableView-container-editing${this._props.oneLine ? '-oneLine' : ''}`}>{this.renderEditor()}</div>;
+ } else
+ return this._props.contents instanceof ObjectField ? null : (
+ <div
+ className={`editableView-container-editing${this._props.oneLine ? '-oneLine' : ''}`}
+ style={{
+ minHeight: '10px',
+ whiteSpace: this._props.oneLine ? 'nowrap' : 'pre-line',
+ width: '100%',
+ }}
+ onClick={this.onClick}>
+ {this.staticDisplay()}
+ </div>
+ );
+ }
+}
diff --git a/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx b/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx
index e0ed8d01e..9ffdd812f 100644
--- a/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx
+++ b/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx
@@ -1,77 +1,264 @@
-/* eslint-disable react/no-unused-prop-types */
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action } from 'mobx';
+import { action, computed, makeObservable, observable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
-import { setupMoveUpEvents } from '../../../../ClientUtils';
+import { returnEmptyFilter, returnFalse, returnZero, setupMoveUpEvents } from '../../../../ClientUtils';
import { emptyFunction } from '../../../../Utils';
-import { Colors } from '../../global/globalEnums';
import './CollectionSchemaView.scss';
+import { EditableView } from '../../EditableView';
+import { ObservableReactComponent } from '../../ObservableReactComponent';
+import { DefaultStyleProvider, returnEmptyDocViewList } from '../../StyleProvider';
+import { FieldViewProps } from '../../nodes/FieldView';
+import { Doc, returnEmptyDoclist } from '../../../../fields/Doc';
+import { dropActionType } from '../../../util/DropActionTypes';
+import { Transform } from '../../../util/Transform';
+import { SchemaTableCell } from './SchemaTableCell';
+import { DocCast } from '../../../../fields/Types';
+import { computedFn } from 'mobx-utils';
+import { CollectionSchemaView } from './CollectionSchemaView';
+import { undoable } from '../../../util/UndoManager';
+import { IconButton, Size } from 'browndash-components';
+
+export enum SchemaFieldType {
+ Header,
+ Cell,
+}
export interface SchemaColumnHeaderProps {
+ Document: Doc;
+ autoFocus?: boolean;
columnKeys: string[];
columnWidths: number[];
columnIndex: number;
- sortField: string;
- sortDesc: boolean;
+ schemaView: CollectionSchemaView;
+ keysDropdown: React.JSX.Element;
+ //cleanupField: (s: string) => string;
isContentActive: (outsideReaction?: boolean | undefined) => boolean | undefined;
setSort: (field: string | undefined, desc?: boolean) => void;
removeColumn: (index: number) => void;
rowHeight: () => number;
- resizeColumn: (e: React.PointerEvent, index: number) => void;
+ resizeColumn: (e: React.PointerEvent, index: number, rightSide: boolean) => void;
dragColumn: (e: PointerEvent, index: number) => boolean;
openContextMenu: (x: number, y: number, index: number) => void;
setColRef: (index: number, ref: HTMLDivElement) => void;
+ rootSelected?: () => boolean;
+ columnWidth: () => number;
+ finishEdit?: () => void; // notify container that edit is over (eg. to hide view in DashFieldView)
+ //transform: () => Transform;
}
@observer
-export class SchemaColumnHeader extends React.Component<SchemaColumnHeaderProps> {
- get fieldKey() {
- return this.props.columnKeys[this.props.columnIndex];
+export class SchemaColumnHeader extends ObservableReactComponent<SchemaColumnHeaderProps> {
+ private _inputRef: EditableView | null = null;
+ @observable _altTitle: string | undefined = undefined;
+ @observable _showMenuIcon: boolean = false;
+
+ @computed get fieldKey() {
+ return this._props.columnKeys[this._props.columnIndex];
}
- @action
- sortClicked = (e: React.PointerEvent) => {
- e.stopPropagation();
- e.preventDefault();
- if (this.props.sortField === this.fieldKey && this.props.sortDesc) {
- this.props.setSort(undefined);
- } else if (this.props.sortField === this.fieldKey) {
- this.props.setSort(this.fieldKey, true);
- } else {
- this.props.setSort(this.fieldKey, false);
- }
+ constructor(props: SchemaColumnHeaderProps) {
+ super(props);
+ makeObservable(this);
+ }
+
+ getFinfo = computedFn((fieldKey: string) => this._props.schemaView?.fieldInfos.get(fieldKey));
+ setColumnValues = (field: string, defaultValue: string) => {
+ this._props.schemaView?.setKey(field, defaultValue, this._props.columnIndex);
+ };
+ @action updateAlt = (newAlt: string) => {
+ this._altTitle = newAlt;
+ };
+ updateKeyDropdown = (value: string) => {
+ this._props.schemaView.updateKeySearch(value);
+ };
+ openKeyDropdown = () => {
+ !this._props.schemaView._colBeingDragged && this._props.schemaView.openNewColumnMenu(this._props.columnIndex, false);
+ };
+ toggleEditing = (editing: boolean) => {
+ this._inputRef?.setIsEditing(editing);
+ this._inputRef?.setIsFocused(editing);
};
@action
- onPointerDown = (e: React.PointerEvent) => {
- this.props.isContentActive(true) && setupMoveUpEvents(this, e, moveEv => this.props.dragColumn(moveEv, this.props.columnIndex), emptyFunction, emptyFunction);
+ setupDrag = (e: React.PointerEvent) => {
+ this._props.isContentActive(true) && setupMoveUpEvents(this, e, moveEv => this._props.dragColumn(moveEv, this._props.columnIndex), emptyFunction, emptyFunction);
+ };
+
+ renderProps = (props: SchemaColumnHeaderProps) => {
+ const { columnKeys, columnWidth, Document } = props;
+ const fieldKey = columnKeys[props.columnIndex];
+ const color = 'black';
+ const fieldProps: FieldViewProps = {
+ childFilters: returnEmptyFilter,
+ childFiltersByRanges: returnEmptyFilter,
+ docViewPath: returnEmptyDocViewList,
+ searchFilterDocs: returnEmptyDoclist,
+ styleProvider: DefaultStyleProvider,
+ isSelected: returnFalse,
+ setHeight: returnFalse,
+ select: emptyFunction,
+ dragAction: dropActionType.move,
+ renderDepth: 1,
+ noSidebar: true,
+ isContentActive: returnFalse,
+ whenChildContentsActiveChanged: emptyFunction,
+ ScreenToLocalTransform: Transform.Identity,
+ focus: emptyFunction,
+ addDocTab: SchemaTableCell.addFieldDoc,
+ pinToPres: returnZero,
+ Document: DocCast(Document.rootDocument, Document),
+ fieldKey: fieldKey,
+ PanelWidth: columnWidth,
+ PanelHeight: props.rowHeight,
+ rootSelected: props.rootSelected,
+ };
+ const readOnly = this.getFinfo(fieldKey)?.readOnly ?? false;
+ const cursor = !readOnly ? 'text' : 'default';
+ const pointerEvents: 'all' | 'none' = 'all';
+ return { color, fieldProps, cursor, pointerEvents };
};
+ @computed get editableView() {
+ const { color, fieldProps, pointerEvents } = this.renderProps(this._props);
+
+ return <div className='schema-column-edit-wrapper' onPointerUp={() => {
+ SchemaColumnHeader.isDefaultField(this.fieldKey) && this.openKeyDropdown();
+ this._props.schemaView.deselectAllCells();
+ }}
+ style={{
+ color,
+ width: '100%',
+ pointerEvents,
+ }}>
+ <EditableView
+ ref={r => {this._inputRef = r; this._props.autoFocus && r?.setIsFocused(true)}}
+ oneLine={true}
+ allowCRs={false}
+ contents={''}
+ onClick={this.openKeyDropdown}
+ fieldContents={fieldProps}
+ editing={undefined}
+ placeholder={'Add key'}
+ updateAlt={this.updateAlt} // alternate title to display
+ updateSearch={this.updateKeyDropdown}
+ inputString={true}
+ inputStringPlaceholder={'Add key'}
+ GetValue={() => {
+ if (SchemaColumnHeader.isDefaultField(this.fieldKey)) return '';
+ else if (this._altTitle) return this._altTitle;
+ else return this.fieldKey;
+ }}
+ SetValue={undoable((value: string, shiftKey?: boolean, enterKey?: boolean) => {
+ if (enterKey) {
+ // if shift & enter, set value of each cell in column
+ this.setColumnValues(value, '');
+ this._altTitle = undefined;
+ this._props.finishEdit?.();
+ return true;
+ }
+ this._props.finishEdit?.();
+ return true;
+ }, 'edit column header')}/>
+ </div>
+ }
+
+ public static isDefaultField = (key: string) => {
+ const defaultPattern = /EmptyColumnKey/;
+ const isDefault: boolean = defaultPattern.exec(key) != null;
+ return isDefault;
+ };
+
+ get headerButton() {
+ const toRender = SchemaColumnHeader.isDefaultField(this.fieldKey) ? (
+ <IconButton
+ icon={<FontAwesomeIcon icon="trash" size="sm" />}
+ size={Size.XSMALL}
+ color={'black'}
+ onPointerDown={e =>
+ setupMoveUpEvents(
+ this,
+ e,
+ returnFalse,
+ emptyFunction,
+ undoable(clickEv => {
+ clickEv.stopPropagation();
+ this._props.schemaView.removeColumn(this._props.columnIndex);
+ }, 'open column menu')
+ )
+ }
+ />
+ ) : (
+ <IconButton
+ icon={<FontAwesomeIcon icon="caret-down" size="lg" />}
+ size={Size.XSMALL}
+ color={'black'}
+ onPointerDown={e =>
+ setupMoveUpEvents(
+ this,
+ e,
+ returnFalse,
+ emptyFunction,
+ undoable(clickEv => {
+ clickEv.stopPropagation();
+ this._props.openContextMenu(e.clientX, e.clientY, this._props.columnIndex);
+ }, 'open column menu')
+ )
+ }
+ />
+ );
+
+ return toRender;
+ }
+
+ @action handlePointerEnter = () => { this._showMenuIcon = true; } // prettier-ignore
+ @action handlePointerLeave = () => { this._showMenuIcon = false; } // prettier-ignore
+
+ @computed get displayButton() {
+ return this._showMenuIcon;
+ }
+
render() {
return (
<div
className="schema-column-header"
style={{
- width: this.props.columnWidths[this.props.columnIndex],
+ width: this._props.columnWidths[this._props.columnIndex],
+ }}
+ onPointerEnter={() => {
+ this.handlePointerEnter();
+ }}
+ onPointerLeave={() => {
+ this.handlePointerLeave();
+ }}
+ onPointerDown={e => {
+ this.setupDrag(e);
+ setupMoveUpEvents(
+ this,
+ e,
+ () => {
+ return this._inputRef?.setIsEditing(false) ?? false;
+ },
+ emptyFunction,
+ emptyFunction
+ );
}}
- onPointerDown={this.onPointerDown}
ref={col => {
if (col) {
- this.props.setColRef(this.props.columnIndex, col);
+ this._props.setColRef(this._props.columnIndex, col);
}
}}>
- <div className="schema-column-resizer left" onPointerDown={e => this.props.resizeColumn(e, this.props.columnIndex)} />
- <div className="schema-column-title">{this.fieldKey}</div>
+ <div className="schema-column-resizer left" onPointerDown={e => this._props.resizeColumn(e, this._props.columnIndex, false)} />
+
+ <div className="schema-header-text">{this.editableView}</div>
<div className="schema-header-menu">
- <div className="schema-header-button" onPointerDown={e => this.props.openContextMenu(e.clientX, e.clientY, this.props.columnIndex)}>
- <FontAwesomeIcon icon="ellipsis-h" />
- </div>
- <div className="schema-sort-button" onPointerDown={this.sortClicked} style={this.props.sortField === this.fieldKey ? { backgroundColor: Colors.MEDIUM_BLUE } : {}}>
- <FontAwesomeIcon icon="caret-right" style={this.props.sortField === this.fieldKey ? { transform: `rotate(${this.props.sortDesc ? '270deg' : '90deg'})` } : {}} />
+ <div className="schema-header-button" style={{ opacity: this.displayButton ? '1.0' : '0.0' }}>
+ {this.headerButton}
</div>
</div>
+
+ <div className="schema-column-resizer right" onPointerDown={e => this._props.resizeColumn(e, this._props.columnIndex, true)} />
</div>
);
}
diff --git a/src/client/views/collections/collectionSchema/SchemaRowBox.tsx b/src/client/views/collections/collectionSchema/SchemaRowBox.tsx
index a7e0e916b..6ffb0865a 100644
--- a/src/client/views/collections/collectionSchema/SchemaRowBox.tsx
+++ b/src/client/views/collections/collectionSchema/SchemaRowBox.tsx
@@ -1,10 +1,8 @@
import { IconButton, Size } from 'browndash-components';
-import { computed, makeObservable } from 'mobx';
+import { computed, makeObservable, observable } from 'mobx';
import { observer } from 'mobx-react';
import { computedFn } from 'mobx-utils';
import * as React from 'react';
-import { CgClose, CgLock, CgLockUnlock } from 'react-icons/cg';
-import { FaExternalLinkAlt } from 'react-icons/fa';
import { returnFalse, setupMoveUpEvents } from '../../../../ClientUtils';
import { emptyFunction } from '../../../../Utils';
import { Doc } from '../../../../fields/Doc';
@@ -12,12 +10,20 @@ import { BoolCast } from '../../../../fields/Types';
import { Transform } from '../../../util/Transform';
import { undoable } from '../../../util/UndoManager';
import { ViewBoxBaseComponent } from '../../DocComponent';
-import { Colors } from '../../global/globalEnums';
import { FieldView, FieldViewProps } from '../../nodes/FieldView';
import { OpenWhere } from '../../nodes/OpenWhere';
import { CollectionSchemaView } from './CollectionSchemaView';
import './CollectionSchemaView.scss';
import { SchemaTableCell } from './SchemaTableCell';
+import { ContextMenu } from '../../ContextMenu';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { IconProp } from '@fortawesome/fontawesome-svg-core';
+
+/**
+ * The SchemaRowBox renders a doc as a row of cells, with each cell representing
+ * one field value of the doc. It mostly handles communication from the SchemaView
+ * to each SchemaCell, passing down necessary functions are props.
+ */
interface SchemaRowBoxProps extends FieldViewProps {
rowIndex: number;
@@ -28,6 +34,7 @@ export class SchemaRowBox extends ViewBoxBaseComponent<SchemaRowBoxProps>() {
return FieldView.LayoutString(SchemaRowBox, fieldKey).replace('fieldKey', `rowIndex={${rowIndex}} fieldKey`);
}
private _ref: HTMLDivElement | null = null;
+ @observable _childrenAddedToSchema: boolean = false;
constructor(props: SchemaRowBoxProps) {
super(props);
@@ -44,29 +51,85 @@ export class SchemaRowBox extends ViewBoxBaseComponent<SchemaRowBoxProps>() {
return this.schemaView.Document;
}
- @computed get rowIndex() {
- return this.schemaView?.rowIndex(this.Document) ?? -1;
- }
-
componentDidMount(): void {
this._props.setContentViewBox?.(this);
}
+ openContextMenu = (x: number, y: number) => {
+ ContextMenu.Instance.clearItems();
+ ContextMenu.Instance.addItem({
+ description: this.Document._lockedSchemaEditing ? 'Unlock field editing' : 'Lock field editing',
+ event: () => (this.Document._lockedSchemaEditing = !this.Document._lockedSchemaEditing),
+ icon: this.Document._lockedSchemaEditing ? 'lock-open' : 'lock',
+ });
+ ContextMenu.Instance.addItem({
+ description: 'Open preview',
+ event: () => this._props.addDocTab(this.Document, OpenWhere.addRight),
+ icon: 'magnifying-glass',
+ });
+ ContextMenu.Instance.addItem({
+ description: `Close doc`,
+ event: () => this.schemaView.removeDoc(this.Document),
+ icon: 'minus',
+ });
+ // Defunct option to add child docs of collections to the main schema
+ // const childDocs = DocListCast(this.Document[Doc.LayoutFieldKey(this.Document)])
+ // if (this.Document.type === 'collection' && childDocs.length) {
+ // ContextMenu.Instance.addItem({
+ // description: this.Document._childrenSharedWithSchema ? 'Remove children from schema' : 'Add children to schema',
+ // event: () => {
+ // this.Document._childrenSharedWithSchema = !this.Document._childrenSharedWithSchema;
+ // },
+ // icon: this.Document._childrenSharedWithSchema ? 'minus' : 'plus',
+ // });
+ // }
+ ContextMenu.Instance.displayMenu(x, y, undefined, false);
+ };
+
+ @computed get menuBackgroundColor() {
+ if (this.Document._lockedSchemaEditing) {
+ return '#F5F5F5';
+ }
+ return '';
+ }
+
+ @computed get menuInfos() {
+ const infos: Array<IconProp> = [];
+ if (this.Document._lockedSchemaEditing) infos.push('lock');
+ if (this.Document._childrenSharedWithSchema) infos.push('star');
+ return infos;
+ }
+
+ isolatedSelection = (doc: Doc) => {
+ return this.schemaView?.selectionOverlap(doc);
+ };
setCursorIndex = (mouseY: number) => this.schemaView?.setRelCursorIndex(mouseY);
selectedCol = () => this.schemaView._selectedCol;
getFinfo = computedFn((fieldKey: string) => this.schemaView?.fieldInfos.get(fieldKey));
selectCell = (doc: Doc, col: number, shift: boolean, ctrl: boolean) => this.schemaView?.selectCell(doc, col, shift, ctrl);
deselectCell = () => this.schemaView?.deselectAllCells();
selectedCells = () => this.schemaView?._selectedDocs;
- setColumnValues = (field: string, value: string) => this.schemaView?.setColumnValues(field, value) ?? false;
- setSelectedColumnValues = (field: string, value: string) => this.schemaView?.setSelectedColumnValues(field, value) ?? false;
+ setColumnValues = (field: string, value: string) => this.schemaView?.setCellValues(field, value) ?? false;
columnWidth = computedFn((index: number) => () => this.schemaView?.displayColumnWidths[index] ?? CollectionSchemaView._minColWidth);
+ computeRowIndex = () => this.schemaView?.rowIndex(this.Document);
+ highlightCells = (text: string) => this.schemaView?.highlightCells(text);
+ selectReference = (doc: Doc, col: number) => {
+ this.schemaView.selectReference(doc, col);
+ };
+ eqHighlightFunc = (text: string) => {
+ const info = this.schemaView.findCellRefs(text);
+ const cells: HTMLDivElement[] = [];
+ info.forEach(inf => {
+ cells.push(this.schemaView.getCellElement(inf[0], inf[1]));
+ });
+ return cells;
+ };
render() {
return (
<div
className="schema-row"
onPointerDown={e => this.setCursorIndex(e.clientY)}
- style={{ height: this._props.PanelHeight(), backgroundColor: this._props.isSelected() ? Colors.LIGHT_BLUE : undefined }}
+ style={{ height: this._props.PanelHeight() }}
ref={(row: HTMLDivElement | null) => {
row && this.schemaView?.addRowRef?.(this.Document, row);
this._ref = row;
@@ -76,45 +139,13 @@ export class SchemaRowBox extends ViewBoxBaseComponent<SchemaRowBoxProps>() {
style={{
width: CollectionSchemaView._rowMenuWidth,
pointerEvents: !this._props.isContentActive() ? 'none' : undefined,
+ backgroundColor: this.menuBackgroundColor,
}}>
<IconButton
- tooltip="close"
- icon={<CgClose size="16px" />}
- size={Size.XSMALL}
- onPointerDown={e =>
- setupMoveUpEvents(
- this,
- e,
- returnFalse,
- emptyFunction,
- undoable(clickEv => {
- clickEv.stopPropagation();
- this._props.removeDocument?.(this.Document);
- }, 'Delete Row')
- )
- }
- />
- <IconButton
- tooltip="whether document interactions are enabled"
- icon={this.Document._lockedPosition ? <CgLockUnlock size="12px" /> : <CgLock size="12px" />}
- size={Size.XSMALL}
- onPointerDown={e =>
- setupMoveUpEvents(
- this,
- e,
- returnFalse,
- emptyFunction,
- undoable(clickEv => {
- clickEv.stopPropagation();
- Doc.toggleLockedPosition(this.Document);
- }, 'toggle document lock')
- )
- }
- />
- <IconButton
- tooltip="open preview"
- icon={<FaExternalLinkAlt />}
+ tooltip="Open actions menu"
+ icon={<FontAwesomeIcon icon="caret-right" size="lg" />}
size={Size.XSMALL}
+ color={'black'}
onPointerDown={e =>
setupMoveUpEvents(
this,
@@ -123,16 +154,27 @@ export class SchemaRowBox extends ViewBoxBaseComponent<SchemaRowBoxProps>() {
emptyFunction,
undoable(clickEv => {
clickEv.stopPropagation();
- this._props.addDocTab(this.Document, OpenWhere.addRight);
- }, 'Open schema Doc preview')
+ this.openContextMenu(e.clientX, e.clientY);
+ }, 'open actions menu')
)
}
/>
+ <div className="row-menu-infos">
+ {this.menuInfos.map(icn => (
+ <FontAwesomeIcon key={icn.toString()} className="row-infos-icon" icon={icn} size="2xs" />
+ ))}
+ </div>
</div>
<div className="row-cells">
{this.schemaView?.columnKeys?.map((key, index) => (
<SchemaTableCell
+ selectReference={this.selectReference}
+ refSelectModeInfo={this.schemaView._referenceSelectMode}
+ eqHighlightFunc={this.eqHighlightFunc}
+ highlightCells={this.highlightCells}
+ isolatedSelection={this.isolatedSelection}
key={key}
+ rowSelected={this._props.isSelected}
Document={this.Document}
col={index}
fieldKey={key}
@@ -146,7 +188,6 @@ export class SchemaRowBox extends ViewBoxBaseComponent<SchemaRowBoxProps>() {
selectedCells={this.selectedCells}
selectedCol={this.selectedCol}
setColumnValues={this.setColumnValues}
- setSelectedColumnValues={this.setSelectedColumnValues}
oneLine={BoolCast(this.schemaDoc?._singleLine)}
menuTarget={this.schemaView.MenuTarget}
transform={() => {
diff --git a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
index 22506cac1..f036ff843 100644
--- a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
+++ b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable react/jsx-props-no-spreading */
/* eslint-disable no-use-before-define */
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Popup, Size, Type } from 'browndash-components';
@@ -12,7 +11,7 @@ import Select from 'react-select';
import { ClientUtils, StopEvent, returnEmptyFilter, returnFalse, returnZero } from '../../../../ClientUtils';
import { emptyFunction } from '../../../../Utils';
import { DateField } from '../../../../fields/DateField';
-import { Doc, DocListCast, Field, returnEmptyDoclist } from '../../../../fields/Doc';
+import { Doc, DocListCast, Field, IdToDoc, returnEmptyDoclist } from '../../../../fields/Doc';
import { RichTextField } from '../../../../fields/RichTextField';
import { ColumnType } from '../../../../fields/SchemaHeaderField';
import { BoolCast, Cast, DateCast, DocCast, FieldValue, StrCast, toList } from '../../../../fields/Types';
@@ -31,6 +30,14 @@ import { FieldViewProps } from '../../nodes/FieldView';
import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox';
import { FInfotoColType } from './CollectionSchemaView';
import './CollectionSchemaView.scss';
+import { SchemaColumnHeader } from './SchemaColumnHeader';
+import { SchemaCellField } from './SchemaCellField';
+
+/**
+ * SchemaTableCells make up the majority of the visual representation of the SchemaView.
+ * They are rendered for each cell in the SchemaView, and each represents one field value
+ * of a doc. Editing the content of the cell changes the corresponding doc's field value.
+ */
export interface SchemaTableCellProps {
Document: Doc;
@@ -47,7 +54,6 @@ export interface SchemaTableCellProps {
isRowActive: () => boolean | undefined;
getFinfo: (fieldKey: string) => FInfo | undefined;
setColumnValues: (field: string, value: string) => boolean;
- setSelectedColumnValues: (field: string, value: string) => boolean;
oneLine?: boolean; // whether all input should fit on one line vs allowing textare multiline inputs
allowCRs?: boolean; // allow carriage returns in text input (othewrise CR ends the edit)
finishEdit?: () => void; // notify container that edit is over (eg. to hide view in DashFieldView)
@@ -56,23 +62,41 @@ export interface SchemaTableCellProps {
transform: () => Transform;
autoFocus?: boolean; // whether to set focus on creation, othwerise wait for a click
rootSelected?: () => boolean;
+ rowSelected: () => boolean;
+ isolatedSelection: (doc: Doc) => [boolean, boolean];
+ highlightCells: (text: string) => void;
+ eqHighlightFunc: (text: string) => HTMLDivElement[] | [];
+ refSelectModeInfo: { enabled: boolean; currEditing: SchemaCellField | undefined };
+ selectReference: (doc: Doc, col: number) => void;
}
function selectedCell(props: SchemaTableCellProps) {
- return (
- props.isRowActive() &&
- props.selectedCol() === props.col && //
- props.selectedCells()?.filter(d => d === props.Document)?.length
- );
+ return props.isRowActive() && props.selectedCol() === props.col && props.selectedCells()?.filter(d => d === props.Document)?.length;
}
@observer
export class SchemaTableCell extends ObservableReactComponent<SchemaTableCellProps> {
+ // private _fieldRef: SchemaCellField | null = null;
+ private _submittedValue: string = '';
+
constructor(props: SchemaTableCellProps) {
super(props);
makeObservable(this);
}
+ get docIndex(){return DocumentView.getDocViewIndex(this._props.Document);} // prettier-ignore
+
+ get isDefault(){return SchemaColumnHeader.isDefaultField(this._props.fieldKey);} // prettier-ignore
+
+ get lockedInteraction(){return (this.isDefault || this._props.Document._lockedSchemaEditing);} // prettier-ignore
+
+ get backgroundColor() {
+ if (this.lockedInteraction) {
+ return '#F5F5F5';
+ }
+ return '';
+ }
+
static addFieldDoc = (docs: Doc | Doc[] /* , where: OpenWhere */) => {
DocumentView.FocusOrOpen(toList(docs)[0]);
return true;
@@ -82,15 +106,12 @@ export class SchemaTableCell extends ObservableReactComponent<SchemaTableCellPro
let protoCount = 0;
let doc: Doc | undefined = Document;
while (doc) {
- if (Object.keys(doc).includes(fieldKey.replace(/^_/, ''))) {
- break;
- }
+ if (Object.keys(doc).includes(fieldKey.replace(/^_/, ''))) break;
protoCount++;
doc = DocCast(doc.proto);
}
- const parenCount = Math.max(0, protoCount - 1);
const color = protoCount === 0 || (fieldKey.startsWith('_') && Document[fieldKey] === undefined) ? 'black' : 'blue'; // color of text in cells
- const textDecoration = color !== 'black' && parenCount ? 'underline' : '';
+ const textDecoration = '';
const fieldProps: FieldViewProps = {
childFilters: returnEmptyFilter,
childFiltersByRanges: returnEmptyFilter,
@@ -121,33 +142,78 @@ export class SchemaTableCell extends ObservableReactComponent<SchemaTableCellPro
return { color, textDecoration, fieldProps, cursor, pointerEvents };
}
+ adjustSelfReference = (field: string) => {
+ const modField = field.replace(/\bthis.\b/g, `d${this.docIndex}.`);
+ return modField;
+ };
+
+ // parses a field from the "idToDoc(####)" format to DocumentId (d#) format for readability
+ cleanupField = (field: string) => {
+ let modField = field.slice();
+ let eqSymbol: string = '';
+ if (modField.startsWith('=')) {
+ modField = modField.substring(1);
+ eqSymbol += '=';
+ }
+ if (modField.startsWith(':=')) {
+ modField = modField.substring(2);
+ eqSymbol += ':=';
+ }
+
+ const idPattern = /idToDoc\((.*?)\)/g;
+ let matches;
+ const results = new Array<[id: string, func: string]>();
+ while ((matches = idPattern.exec(field)) !== null) {
+ results.push([matches[0], matches[1].replace(/"/g, '')]);
+ }
+ results.forEach(idFuncPair => {
+ modField = modField.replace(idFuncPair[0], 'd' + DocumentView.getDocViewIndex(IdToDoc(idFuncPair[1])).toString());
+ });
+
+ if (modField.endsWith(';')) modField = modField.substring(0, modField.length - 1);
+
+ const inQuotes = (strField: string) => {
+ return (strField.startsWith('`') && strField.endsWith('`')) || (strField.startsWith("'") && strField.endsWith("'")) || (strField.startsWith('"') && strField.endsWith('"'));
+ };
+ if (!inQuotes(this._submittedValue) && inQuotes(modField)) modField = modField.substring(1, modField.length - 1);
+
+ return eqSymbol + modField;
+ };
+
@computed get defaultCellContent() {
const { color, textDecoration, fieldProps, pointerEvents } = SchemaTableCell.renderProps(this._props);
return (
<div
className="schemacell-edit-wrapper"
+ // onContextMenu={}
style={{
color,
textDecoration,
width: '100%',
- pointerEvents,
+ pointerEvents: this.lockedInteraction ? 'none' : pointerEvents,
}}>
- <EditableView
+ <SchemaCellField
+ fieldKey={this._props.fieldKey}
+ refSelectModeInfo={this._props.refSelectModeInfo}
+ Document={this._props.Document}
+ highlightCells={(text: string) => this._props.highlightCells(this.adjustSelfReference(text))}
+ getCells={(text: string) => this._props.eqHighlightFunc(this.adjustSelfReference(text))}
ref={r => selectedCell(this._props) && this._props.autoFocus && r?.setIsFocused(true)}
oneLine={this._props.oneLine}
- allowCRs={this._props.allowCRs}
- contents={''}
+ contents={undefined}
fieldContents={fieldProps}
editing={selectedCell(this._props) ? undefined : false}
- GetValue={() => Field.toKeyValueString(fieldProps.Document, this._props.fieldKey, SnappingManager.MetaKey)}
+ GetValue={() => this.cleanupField(Field.toKeyValueString(fieldProps.Document, this._props.fieldKey, SnappingManager.MetaKey))}
SetValue={undoable((value: string, shiftDown?: boolean, enterKey?: boolean) => {
if (shiftDown && enterKey) {
this._props.setColumnValues(this._props.fieldKey.replace(/^_/, ''), value);
this._props.finishEdit?.();
return true;
}
- const ret = Doc.SetField(fieldProps.Document, this._props.fieldKey.replace(/^_/, ''), value, Doc.IsDataProto(fieldProps.Document) ? true : undefined);
+ const hasNoLayout = Doc.IsDataProto(fieldProps.Document) ? true : undefined; // the "delegate" is a a data document so never write to it's proto
+ const ret = Doc.SetField(fieldProps.Document, this._props.fieldKey.replace(/^_/, ''), value, hasNoLayout);
+ this._submittedValue = value;
this._props.finishEdit?.();
return ret;
}, 'edit schema cell')}
@@ -183,23 +249,54 @@ export class SchemaTableCell extends ObservableReactComponent<SchemaTableCellPro
}
}
+ @computed get borderColor() {
+ const sides: Array<string | undefined> = [];
+ sides[0] = selectedCell(this._props) ? `solid 2px ${Colors.MEDIUM_BLUE}` : undefined; // left
+ sides[1] = selectedCell(this._props) ? `solid 2px ${Colors.MEDIUM_BLUE}` : undefined; // right
+ sides[2] = !this._props.isolatedSelection(this._props.Document)[0] && selectedCell(this._props) ? `solid 2px ${Colors.MEDIUM_BLUE}` : undefined; // top
+ sides[3] = !this._props.isolatedSelection(this._props.Document)[1] && selectedCell(this._props) ? `solid 2px ${Colors.MEDIUM_BLUE}` : undefined; // bottom
+ return sides;
+ }
+
render() {
return (
<div
className="schema-table-cell"
onContextMenu={e => StopEvent(e)}
onPointerDown={action(e => {
+ if (this.lockedInteraction) {
+ e.stopPropagation();
+ e.preventDefault();
+ return;
+ }
+
+ if (this._props.refSelectModeInfo.enabled && !selectedCell(this._props)) {
+ e.stopPropagation();
+ e.preventDefault();
+ this._props.selectReference(this._props.Document, this._props.col);
+ return;
+ }
+
const shift: boolean = e.shiftKey;
const ctrl: boolean = e.ctrlKey;
- if (this._props.isRowActive?.() !== false) {
+ if (this._props.isRowActive?.()) {
if (selectedCell(this._props) && ctrl) {
this._props.selectCell(this._props.Document, this._props.col, shift, ctrl);
e.stopPropagation();
} else !selectedCell(this._props) && this._props.selectCell(this._props.Document, this._props.col, shift, ctrl);
}
})}
- style={{ padding: this._props.padding, maxWidth: this._props.maxWidth?.(), width: this._props.columnWidth() || undefined, border: selectedCell(this._props) ? `solid 2px ${Colors.MEDIUM_BLUE}` : undefined }}>
- {this.content}
+ style={{
+ padding: this._props.padding,
+ maxWidth: this._props.maxWidth?.(),
+ width: this._props.columnWidth() || undefined,
+ borderLeft: this.borderColor[0],
+ borderRight: this.borderColor[1],
+ borderTop: this.borderColor[2],
+ borderBottom: this.borderColor[3],
+ backgroundColor: this.backgroundColor,
+ }}>
+ {this.isDefault ? '' : this.content}
</div>
);
}
diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts
index 2b8908899..903e04ad7 100644
--- a/src/client/views/global/globalScripts.ts
+++ b/src/client/views/global/globalScripts.ts
@@ -85,6 +85,7 @@ ScriptingGlobals.add(function setBackgroundColor(color?: string, checkResult?: b
} else {
const dataKey = Doc.LayoutFieldKey(dv.Document);
const alternate = (dv.layoutDoc[dataKey + '_usePath'] ? '_' + dv.layoutDoc[dataKey + '_usePath'] : '').replace(':hover', '');
+ dv.layoutDoc[fieldKey + alternate] = undefined;
dv.dataDoc[fieldKey + alternate] = color;
}
});
diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
index 3dd568fda..896048ab3 100644
--- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx
+++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
@@ -4,29 +4,35 @@ import { Colors, Toggle, ToggleType, Type } from 'browndash-components';
import { IReactionDisposer, ObservableMap, action, computed, makeObservable, observable, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
-import { returnEmptyString, returnFalse, returnOne, setupMoveUpEvents } from '../../../../ClientUtils';
+import { ClientUtils, returnEmptyString, returnFalse, returnOne, setupMoveUpEvents } from '../../../../ClientUtils';
import { emptyFunction } from '../../../../Utils';
-import { Doc, DocListCast, Field, Opt, StrListCast } from '../../../../fields/Doc';
+import { Doc, DocListCast, Field, FieldType, NumListCast, Opt, StrListCast } from '../../../../fields/Doc';
+import { AclAdmin, AclAugment, AclEdit } from '../../../../fields/DocSymbols';
import { InkTool } from '../../../../fields/InkField';
import { List } from '../../../../fields/List';
+import { PrefetchProxy } from '../../../../fields/Proxy';
import { Cast, CsvCast, DocCast, NumCast, StrCast } from '../../../../fields/Types';
import { CsvField } from '../../../../fields/URLField';
-import { TraceMobx } from '../../../../fields/util';
+import { GetEffectiveAcl, TraceMobx } from '../../../../fields/util';
+import { GPTCallType, gptAPICall } from '../../../apis/gpt/GPT';
import { DocUtils } from '../../../documents/DocUtils';
import { DocumentType } from '../../../documents/DocumentTypes';
import { Docs } from '../../../documents/Documents';
+import { LinkManager } from '../../../util/LinkManager';
import { UndoManager, undoable } from '../../../util/UndoManager';
import { ContextMenu } from '../../ContextMenu';
import { ViewBoxAnnotatableComponent } from '../../DocComponent';
import { MarqueeAnnotator } from '../../MarqueeAnnotator';
import { PinProps } from '../../PinFuncs';
import { SidebarAnnos } from '../../SidebarAnnos';
+import { CollectionFreeFormView } from '../../collections/collectionFreeForm';
import { AnchorMenu } from '../../pdf/AnchorMenu';
import { GPTPopup, GPTPopupMode } from '../../pdf/GPTPopup/GPTPopup';
import { DocumentView } from '../DocumentView';
import { FieldView, FieldViewProps } from '../FieldView';
import { FocusViewOptions } from '../FocusViewOptions';
import './DataVizBox.scss';
+import { Col, DataVizTemplateInfo, DocCreatorMenu, LayoutType, TemplateFieldSize, TemplateFieldType } from './DocCreatorMenu';
import { Histogram } from './components/Histogram';
import { LineChart } from './components/LineChart';
import { PieChart } from './components/PieChart';
@@ -41,6 +47,7 @@ export enum DataVizView {
@observer
export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
+ private _urlError: boolean = false;
private _mainCont: React.RefObject<HTMLDivElement> = React.createRef();
private _marqueeref = React.createRef<MarqueeAnnotator>();
private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
@@ -49,7 +56,11 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
sidebarAddDoc: ((doc: Doc | Doc[], sidebarKey?: string | undefined) => boolean) | undefined;
crop: ((region: Doc | undefined, addCrop?: boolean) => Doc | undefined) | undefined;
@observable _marqueeing: number[] | undefined = undefined;
- @observable _savedAnnotations = new ObservableMap<number, (HTMLDivElement & { marqueeing?: boolean })[]>();
+ @observable _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>();
+ @observable _specialHighlightedRow: number | undefined = undefined;
+ @observable GPTSummary: ObservableMap<string, { desc?: string; type?: TemplateFieldType; size?: TemplateFieldSize }> | undefined = undefined;
+ @observable colsInfo: ObservableMap<string, Col> = new ObservableMap();
+ @observable _GPTLoading: boolean = false;
constructor(props: FieldViewProps) {
super(props);
@@ -99,8 +110,14 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
// all CSV records in the dataset (that aren't an empty row)
@computed.struct get records() {
- const records = DataVizBox.dataset.get(CsvCast(this.dataDoc[this.fieldKey]).url.href);
- return records?.filter(record => Object.keys(record).some(key => record[key])) ?? [];
+ try {
+ const records = DataVizBox.dataset.get(CsvCast(this.dataDoc[this.fieldKey]).url.href);
+ this._urlError = false;
+ return records?.filter(record => Object.keys(record).some(key => record[key])) ?? [];
+ } catch {
+ this._urlError = true;
+ return [{ error: 'Data not found' }] as { [key: string]: string }[];
+ }
}
// currently chosen visualization type: line, pie, histogram, table
@@ -124,17 +141,75 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
this.layoutDoc._dataViz_titleCol = titleCol;
};
+ @action setSpecialHighlightedRow = (row: number | undefined) => {
+ this._specialHighlightedRow = row;
+ };
+
+ @action setColumnType = (colTitle: string, type: TemplateFieldType) => {
+ const colInfo = this.colsInfo.get(colTitle);
+ if (colInfo) {
+ colInfo.type = type;
+ } else {
+ this.colsInfo.set(colTitle, { title: colTitle, desc: '', type: type, sizes: [TemplateFieldSize.MEDIUM] });
+ }
+ };
+
+ @action modifyColumnSizes = (colTitle: string, size: TemplateFieldSize, valid: boolean) => {
+ const column = this.colsInfo.get(colTitle);
+ if (column) {
+ if (!valid && column.sizes.includes(size)) {
+ column.sizes.splice(column.sizes.indexOf(size), 1);
+ } else if (valid && !column.sizes.includes(size)) {
+ column.sizes.push(size);
+ }
+ } else {
+ this.colsInfo.set(colTitle, { title: colTitle, desc: '', type: TemplateFieldType.UNSET, sizes: [size] });
+ }
+ };
+
+ @action setColumnTitle = (colTitle: string, newTitle: string) => {
+ const colInfo = this.colsInfo.get(colTitle);
+ if (colInfo) {
+ colInfo.title = newTitle;
+ console.log(colInfo.title);
+ } else {
+ this.colsInfo.set(colTitle, { title: newTitle, desc: '', type: TemplateFieldType.UNSET, sizes: [] });
+ }
+ };
+
+ @action setColumnDesc = (colTitle: string, desc: string) => {
+ const colInfo = this.colsInfo.get(colTitle);
+ if (colInfo) {
+ if (!desc) {
+ colInfo.desc = this.GPTSummary?.get(colTitle)?.desc ?? '';
+ } else {
+ colInfo.desc = desc;
+ }
+ } else {
+ this.colsInfo.set(colTitle, { title: colTitle, desc: desc, type: TemplateFieldType.UNSET, sizes: [] });
+ }
+ };
+
+ @action setColumnDefault = (colTitle: string, cont: string) => {
+ const colInfo = this.colsInfo.get(colTitle);
+ if (colInfo) {
+ colInfo.defaultContent = cont;
+ } else {
+ this.colsInfo.set(colTitle, { title: colTitle, desc: '', type: TemplateFieldType.UNSET, sizes: [], defaultContent: cont });
+ }
+ };
+
@action // pinned / linked anchor doc includes selected rows, graph titles, and graph colors
- restoreView = (data: Doc) => {
+ restoreView = (viewData: Doc) => {
// const changedView = data.config_dataViz && this.dataVizView !== data.config_dataViz && (this.layoutDoc._dataViz = data.config_dataViz);
// const changedAxes = data.config_dataVizAxes && this.axes.join('') !== StrListCast(data.config_dataVizAxes).join('') && (this.layoutDoc._dataViz_axes = new List<string>(StrListCast(data.config_dataVizAxes)));
- this.layoutDoc.dataViz_selectedRows = Field.Copy(data.dataViz_selectedRows);
- this.layoutDoc.dataViz_histogram_barColors = Field.Copy(data.dataViz_histogram_barColors);
- this.layoutDoc.dataViz_histogram_defaultColor = data.dataViz_histogram_defaultColor;
- this.layoutDoc.dataViz_pie_sliceColors = Field.Copy(data.dataViz_pie_sliceColors);
+ this.layoutDoc.dataViz_selectedRows = Field.Copy(viewData.dataViz_selectedRows);
+ this.layoutDoc.dataViz_histogram_barColors = Field.Copy(viewData.dataViz_histogram_barColors);
+ this.layoutDoc.dataViz_histogram_defaultColor = viewData.dataViz_histogram_defaultColor;
+ this.layoutDoc.dataViz_pie_sliceColors = Field.Copy(viewData.dataViz_pie_sliceColors);
Object.keys(this.layoutDoc).forEach(key => {
if (key.startsWith('dataViz_histogram_title') || key.startsWith('dataViz_lineChart_title') || key.startsWith('dataViz_pieChart_title')) {
- this.layoutDoc['_' + key] = data[key];
+ this.layoutDoc['_' + key] = viewData[key];
}
});
return true;
@@ -145,6 +220,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
// }
// return func() ?? false;
};
+
getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
const visibleAnchor = AnchorMenu.Instance.GetAnchor?.(undefined, addAsAnnotation);
const anchor = !pinProps
@@ -271,7 +347,9 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
componentDidMount() {
this._props.setContentViewBox?.(this);
- if (!DataVizBox.dataset.has(CsvCast(this.dataDoc[this.fieldKey]).url.href)) this.fetchData();
+ if (!this._urlError) {
+ if (!DataVizBox.dataset.has(CsvCast(this.dataDoc[this.fieldKey]).url.href)) this.fetchData();
+ }
this._disposers.datavis = reaction(
() => {
if (this.layoutDoc.dataViz_schemaLive === undefined) this.layoutDoc.dataViz_schemaLive = true;
@@ -332,6 +410,10 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
},
{ fireImmediately: true }
);
+ this._disposers.contentSummary = reaction(
+ () => this.records,
+ () => this.updateGPTSummary()
+ );
}
fetchData = () => {
@@ -358,7 +440,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
};
if (!this.records.length) return 'no data/visualization';
switch (this.dataVizView) {
- case DataVizView.TABLE: return <TableBox {...sharedProps} docView={this.DocumentView} selectAxes={this.selectAxes} selectTitleCol={this.selectTitleCol}/>;
+ case DataVizView.TABLE: return <TableBox {...sharedProps} specHighlightedRow={this._specialHighlightedRow} docView={this.DocumentView} selectAxes={this.selectAxes} selectTitleCol={this.selectTitleCol}/>;
case DataVizView.LINECHART: return <LineChart {...sharedProps} dataDoc={this.dataDoc} fieldKey={this.fieldKey} ref={r => {this._vizRenderer = r ?? undefined;}} vizBox={this} />;
case DataVizView.HISTOGRAM: return <Histogram {...sharedProps} dataDoc={this.dataDoc} fieldKey={this.fieldKey} ref={r => {this._vizRenderer = r ?? undefined;}} />;
case DataVizView.PIECHART: return <PieChart {...sharedProps} dataDoc={this.dataDoc} fieldKey={this.fieldKey} ref={r => {this._vizRenderer = r ?? undefined;}}
@@ -425,11 +507,18 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
this.layoutDoc.dataViz_filterSelection = !this.layoutDoc.dataViz_filterSelection;
};
- specificContextMenu = (): void => {
+ openDocCreatorMenu = (x: number, y: number) => {
+ DocCreatorMenu.Instance.toggleDisplay(x, y);
+ DocCreatorMenu.Instance.setDataViz(this);
+ DocCreatorMenu.Instance.setTemplateDocs(this.getPossibleTemplates());
+ };
+
+ specificContextMenu = (e: React.MouseEvent) => {
const cm = ContextMenu.Instance;
const options = cm.findByDescription('Options...');
const optionItems = options?.subitems ?? [];
optionItems.push({ description: `Analyze with AI`, event: () => this.askGPT(), icon: 'lightbulb' });
+ optionItems.push({ description: `Create documents`, event: () => this.openDocCreatorMenu(e.pageX, e.pageY), icon: 'table-cells' });
!options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'eye' });
};
@@ -439,11 +528,201 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
GPTPopup.Instance.createFilteredDoc = this.createFilteredDoc;
GPTPopup.Instance.setDataJson('');
GPTPopup.Instance.setMode(GPTPopupMode.DATA);
- const data = DataVizBox.dataset.get(CsvCast(this.dataDoc[this.fieldKey]).url.href);
- GPTPopup.Instance.setDataJson(JSON.stringify(data));
+ const csvdata = DataVizBox.dataset.get(CsvCast(this.dataDoc[this.fieldKey]).url.href);
+ GPTPopup.Instance.setDataJson(JSON.stringify(csvdata));
GPTPopup.Instance.generateDataAnalysis();
});
+ getColSummary = (): string => {
+ const possibleIds: number[] = this.records.map((_, index) => index);
+
+ for (let i = possibleIds.length - 1; i > 0; i--) {
+ const j = Math.floor(Math.random() * (i + 1));
+ [possibleIds[i], possibleIds[j]] = [possibleIds[j], possibleIds[i]];
+ }
+
+ const rowsToCheck = possibleIds.slice(0, Math.min(10, this.records.length));
+
+ let prompt: string = 'Col titles: ';
+
+ const cols = Array.from(Object.keys(this.records[0])).filter(header => header !== '' && header !== undefined);
+
+ cols.forEach((col, i) => {
+ prompt += `Col #${i}: ${col} ------`;
+ });
+
+ prompt += '----------- Rows: ';
+
+ rowsToCheck.forEach(row => {
+ prompt += `Row #${row}: `;
+ cols.forEach(col => {
+ prompt += `${col}: ${this.records[row][col]} -----`;
+ });
+ });
+
+ return prompt;
+ };
+
+ updateColDefaults = () => {
+ const possibleIds: number[] = this.records.map((_, index) => index);
+
+ for (let i = possibleIds.length - 1; i > 0; i--) {
+ const j = Math.floor(Math.random() * (i + 1));
+ [possibleIds[i], possibleIds[j]] = [possibleIds[j], possibleIds[i]];
+ }
+
+ const rowToCheck = possibleIds[0];
+
+ const cols = Array.from(Object.keys(this.records[0])).filter(header => header !== '' && header !== undefined);
+
+ cols.forEach(col => {
+ this.setColumnDefault(col, `${this.records[rowToCheck][col]}`);
+ });
+ };
+
+ updateGPTSummary = async () => {
+ this._GPTLoading = true;
+
+ this.updateColDefaults();
+
+ const prompt = this.getColSummary();
+
+ const cols = Array.from(Object.keys(this.records[0])).filter(header => header !== '' && header !== undefined);
+ cols.forEach(col => {
+ if (!this.colsInfo.get(col)) this.colsInfo.set(col, { title: col, desc: '', sizes: [], type: TemplateFieldType.UNSET });
+ });
+
+ try {
+ const [res1, res2] = await Promise.all([gptAPICall(prompt, GPTCallType.VIZSUM), gptAPICall('Info:' + prompt, GPTCallType.VIZSUM2)]);
+
+ if (res1) {
+ this.GPTSummary = new ObservableMap();
+ const descs: { [col: string]: string } = JSON.parse(res1);
+ for (const [key, val] of Object.entries(descs)) {
+ this.GPTSummary.set(key, { desc: val });
+ if (!this.colsInfo.get(key)?.desc) this.setColumnDesc(key, val);
+ }
+ }
+
+ if (res2) {
+ !this.GPTSummary && (this.GPTSummary = new ObservableMap());
+ const info: { [col: string]: { type: TemplateFieldType; size: TemplateFieldSize } } = JSON.parse(res2);
+ for (const [key, val] of Object.entries(info)) {
+ const colSummary = this.GPTSummary.get(key);
+ if (colSummary) {
+ colSummary.size = val.size;
+ colSummary.type = val.type;
+ this.setColumnType(key, val.type);
+ this.modifyColumnSizes(key, val.size, true);
+ }
+ }
+ }
+ } catch (err) {
+ console.error(err);
+ }
+ };
+
+ getPossibleTemplates = (): Doc[] => {
+ const linkedDocs: Doc[] = LinkManager.Instance.getAllRelatedLinks(this.Document).map(d => DocCast(LinkManager.getOppositeAnchor(d, this.Document)));
+ const linkedCollections: Doc[] = linkedDocs.filter(doc => doc.type === 'config').map(doc => DocCast(doc.annotationOn));
+ const isColumnTitle = (title: string): boolean => {
+ const colTitles: string[] = Object.keys(this.records[0]);
+ for (let i = 0; i < colTitles.length; ++i) {
+ if (colTitles[i] === title) {
+ return true;
+ }
+ }
+ return false;
+ };
+ const isValidTemplate = (collection: Doc) => {
+ const childDocs = DocListCast(collection[Doc.LayoutFieldKey(collection)]);
+ for (let i = 0; i < childDocs.length; ++i) {
+ if (isColumnTitle(String(childDocs[i].title))) return true;
+ }
+ return false;
+ };
+ return linkedCollections.filter(col => isValidTemplate(col));
+ };
+
+ ApplyTemplateTo = (templateDoc: Doc, target: Doc, targetKey: string, titleTarget: string | undefined) => {
+ if (!Doc.AreProtosEqual(target[targetKey] as Doc, templateDoc)) {
+ if (target.resolvedDataDoc) {
+ target[targetKey] = new PrefetchProxy(templateDoc);
+ } else {
+ titleTarget && (Doc.GetProto(target).title = titleTarget);
+ const setDoc = [AclAdmin, AclEdit, AclAugment].includes(GetEffectiveAcl(Doc.GetProto(target))) ? Doc.GetProto(target) : target;
+ setDoc[targetKey] = new PrefetchProxy(templateDoc);
+ }
+ }
+ return target;
+ };
+
+ applyLayout = (templateInfo: DataVizTemplateInfo, docs: Doc[]) => {
+ if (templateInfo.layout.type === LayoutType.Stacked) return;
+ const columns: number = templateInfo.columns;
+ const xGap: number = templateInfo.layout.xMargin;
+ const yGap: number = templateInfo.layout.yMargin;
+ // const repeat: number = templateInfo.layout.repeat;
+ const startX: number = templateInfo.referencePos.x;
+ const startY: number = templateInfo.referencePos.y;
+ const templWidth = Number(templateInfo.doc._width);
+ const templHeight = Number(templateInfo.doc._height);
+
+ let i: number = 0;
+ let docsChanged: number = 0;
+ let curX: number = startX;
+ let curY: number = startY;
+
+ while (docsChanged < docs.length) {
+ while (i < columns && docsChanged < docs.length) {
+ docs[docsChanged].x = curX;
+ docs[docsChanged].y = curY;
+ curX += templWidth + xGap;
+ ++docsChanged;
+ ++i;
+ }
+
+ i = 0;
+ curX = startX;
+ curY += templHeight + yGap;
+ }
+ };
+
+ // @action addSavedLayout = (layout: DataVizTemplateLayout) => {
+ // const saved = Cast(this.layoutDoc.dataViz_savedTemplates, listSpec('RefField'));
+
+ // }
+
+ @action
+ createDocsFromTemplate = (templateInfo: DataVizTemplateInfo) => {
+ if (!templateInfo.doc) return;
+ const mainCollection = this.DocumentView?.().containerViewPath?.().lastElement()?.ComponentView as CollectionFreeFormView;
+ const fields: string[] = Array.from(Object.keys(this.records[0]));
+ const selectedRows = NumListCast(this.layoutDoc.dataViz_selectedRows);
+ const docs: Doc[] = selectedRows.map(row => {
+ const values: string[] = [];
+ fields.forEach(col => values.push(this.records[row][col]));
+
+ const proto = new Doc();
+ proto.author = ClientUtils.CurrentUserEmail();
+ values.forEach((val, i) => {
+ proto[fields[i]] = val as FieldType;
+ });
+
+ const target = Doc.MakeDelegate(proto);
+ const targetKey = StrCast(templateInfo.doc!.layout_fieldKey, 'layout');
+ const applied = this.ApplyTemplateTo(templateInfo.doc!, target, targetKey, templateInfo.doc!.title + `${row}`);
+ target.layout_fieldKey = targetKey;
+
+ //this.applyImagesTo(target, fields);
+ return applied;
+ });
+
+ docs.forEach(doc => mainCollection.addDocument(doc));
+
+ this.applyLayout(templateInfo, docs);
+ };
+
/**
* creates a new dataviz document filter from this one
* it appears to the right of this document, with the
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu.scss b/src/client/views/nodes/DataVizBox/DocCreatorMenu.scss
new file mode 100644
index 000000000..4ea904b8e
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu.scss
@@ -0,0 +1,1044 @@
+.no-margin {
+ margin-top: 0px !important;
+ margin-bottom: 0px !important;
+ margin-left: 0px !important;
+ margin-right: 0px !important;
+}
+
+.docCreatorMenu-cont {
+ position: absolute;
+ z-index: 100000;
+ // box-shadow: 0px 3px 4px rgba(0, 0, 0, 30%);
+ // background: whitesmoke;
+ // color: black;
+ border-radius: 3px;
+}
+
+.docCreatorMenu-menu {
+ display: flex;
+ flex-direction: row;
+ height: 25px;
+ align-items: flex-end;
+}
+
+.docCreatorMenu-menu-button {
+ width: 25px;
+ height: 25px;
+ background: whitesmoke;
+ background-color: rgb(50, 50, 50);
+ border-radius: 5px;
+ border: 1px solid rgb(180, 180, 180);
+ padding: 0px;
+ font-size: 13px;
+ //box-shadow: 3px 3px rgb(29, 29, 31);
+
+ &:hover {
+ box-shadow: none;
+ }
+
+ &.right{
+ margin-left: 0px;
+ font-size: 12px;
+ }
+
+ &.close-menu {
+ font-size: 12px;
+ width: 18px;
+ height: 18px;
+ border-radius: 2px;
+ font-size: 12px;
+ margin-left: auto;
+ }
+
+ &.options {
+ margin-left: 0px;
+ }
+
+ &:hover {
+ background-color: rgb(60, 60, 65);
+ }
+
+ &.top-bar {
+ border-bottom: 25px solid #555;
+ border-left: 12px solid transparent;
+ border-right: 12px solid transparent;
+ // border-top-left-radius: 5px;
+ // border-top-right-radius: 5px;
+ border-radius: 0px;
+ height: 0;
+ width: 50px;
+ }
+
+ &.preview-toggle {
+ margin: 0px;
+ border-top-left-radius: 0px;
+ border-bottom-left-radius: 0px;
+ border-left: 0px;
+ }
+}
+
+.docCreatorMenu-top-buttons-container {
+ position: relative;
+ margin-top: 5px;
+ margin-left: 7px;
+ display: flex;
+ flex-direction: row;
+ align-items: flex-end;
+ width: 150px;
+ height: auto;
+}
+
+.top-button-container {
+ position: relative;
+ width: 52px;
+ height: 20px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ &.left {
+ z-index: 3;
+ }
+
+ &.middle {
+ position: absolute;
+ left: 40px;
+ z-index: 2;
+
+ &.selected {
+ z-index: 4;
+ }
+ }
+
+ &.right {
+ position: absolute;
+ left: 80px;
+ z-index: 1;
+
+ &.selected {
+ z-index: 4;
+ }
+ }
+
+ &:hover::before{
+ border-bottom: 20px solid rgb(82, 82, 82);
+ }
+
+ &::before {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: 0;
+ border-bottom: 20px solid rgb(50, 50, 50);
+ border-left: 12px solid transparent;
+ border-right: 12px solid transparent;
+ height: 0;
+ width: 50px;
+ }
+
+ &::after {
+ content: "";
+ position: absolute;
+ top: -1px;
+ left: -1px;
+ border-bottom: 22px solid rgb(180, 180, 180);
+ border-left: 12px solid transparent;
+ border-right: 12px solid transparent;
+ height: 0;
+ width: 52px;
+ z-index: -1;
+ }
+
+ &.selected::before {
+ border-bottom-color: rgb(67, 119, 214);
+ }
+}
+
+.top-button-content {
+ position: relative;
+ z-index: 1;
+ color: white;
+}
+
+.docCreatorMenu-menu-hr{
+ margin-top: 0px;
+ margin-bottom: 0px;
+ color: rgb(180, 180, 180);
+}
+
+.docCreatorMenu-placement-indicator {
+ position: absolute;
+ z-index: 100000;
+ border-left: solid 3px #9fd7fb;
+ border-top: solid 3px #9fd7fb;
+ width: 25px;
+ height: 25px;
+}
+
+.docCreatorMenu-general-options-container {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ margin: 0px;
+ padding: 0px;
+ gap: 5px;
+}
+
+.docCreatorMenu-save-layout-button {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 40px;
+ height: 40px;
+ background-color: rgb(99, 148, 238);
+ border: 2px solid rgb(80, 107, 152);
+ border-radius: 5px;
+ margin-bottom: 20px;
+ font-size: 25px;
+
+ &:hover{
+ background-color: rgb(59, 128, 255);
+ border: 2px solid rgb(53, 80, 127);
+ }
+}
+
+.docCreatorMenu-create-docs-button {
+ width: 40px;
+ height: 40px;
+ background-color: rgb(176, 229, 149);
+ border: 2px solid rgb(126, 219, 80);
+ border-radius: 5px;
+ padding: 0px;
+ font-size: 25px;
+ color: white;
+ flex: 0 0 auto;
+ margin-bottom: 20px; //remove later !!!
+
+ &:hover {
+ background-color: rgb(129, 223, 83);
+ border: 2px solid rgb(80, 185, 28);
+ }
+}
+
+.docCreatorMenu-option-divider {
+ border-top: 1px solid rgb(180, 180, 180);
+ width: 95%;
+ margin-top: 10px;
+ margin-bottom: 10px;
+
+ &.full {
+ width: 100%;
+ }
+}
+
+//------------------------------------------------------------------------------------------------------------------------------------------
+// Resizers CSS
+//--------------------------------------------------------------------------------------------------------------------------------------------
+
+.docCreatorMenu-resizer {
+ position: absolute;
+ background-color: none;
+
+ &.top, &.bottom {
+ height: 10px;
+ cursor: ns-resize;
+ }
+
+ &.right, &.left {
+ width: 10px;
+ cursor: ew-resize;
+ }
+
+ &.topRight, &.topLeft, &.bottomRight, &.bottomLeft {
+ height: 15px;
+ width: 15px;
+ background-color: none;
+ }
+}
+
+//------------------------------------------------------------------------------------------------------------------------------------------
+// DocCreatorMenu templates preview CSS
+//--------------------------------------------------------------------------------------------------------------------------------------------
+
+.docCreatorMenu-templates-view {
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+ overflow-y: scroll;
+ //align-items: flex-start;
+ margin: 5px;
+ margin-top: 0px;
+ width: calc(100% - 10px);
+ height: calc(100% - 30px);
+ border: 1px solid rgb(180, 180, 180);
+ border-radius: 5px;
+ -ms-overflow-style: none;
+ scrollbar-width: none;
+}
+
+.docCreatorMenu-preview-container {
+ display: grid;
+ grid-template-columns: repeat(2, 140px);
+ grid-template-rows: 140px;
+ grid-auto-rows: 141px;
+ overflow-y: scroll;
+ margin: 0px;
+ margin-top: 0px;
+ width: 100%;
+ height: 100%;
+}
+
+.docCreatorMenu-expanded-template-preview {
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+ position: relative;
+ width: 100%;
+ height: 100%;
+
+ .top-panel{
+ width: 100%;
+ height: 10px;
+ }
+
+ .right-buttons-panel {
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+ height: 100%;
+ position: absolute;
+ right: 0px;
+ top: 0px;
+ width: 40px;
+ padding: 5px;
+ gap: 2px;
+ }
+}
+
+.docCreatorMenu-preview-window {
+ position: relative;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 113px;
+ height: 113px;
+ margin-top: 10px;
+ margin-left: 10px;
+ border: 1px solid rgb(163, 163, 163);
+ border-radius: 5px;
+ box-shadow: 5px 5px rgb(29, 29, 31);
+ flex: 0 0 auto;
+
+ &:hover{
+ background-color: rgb(72, 72, 73);
+ }
+
+ &.empty {
+ font-size: 35px;
+
+ &.GPT {
+ margin-top: 0px;
+ }
+ }
+
+ .option-button {
+ display: none;
+ height: 25px;
+ width: 25px;
+ margin: 0px;
+ background: none;
+ border: 0px;
+ padding: 0px;
+ font-size: 15px;
+
+ &.right {
+ position: absolute;
+ bottom: 0px;
+ right: 0px;
+ }
+
+ &.left {
+ position: absolute;
+ bottom: 0px;
+ left: 0px;
+ }
+
+ &.top-left {
+ position: absolute;
+ top: 0px;
+ left: 0px;
+ }
+ }
+
+ &:hover .option-button {
+ display: block;
+ }
+
+}
+
+.docCreatorMenu-preview-image{
+ background-color: transparent;
+ height: 100px;
+ width: 100px;
+ display: block;
+ object-fit: contain;
+ border-radius: 5px;
+
+ &.expanded {
+ height: 100%;
+ width: 100%;
+ }
+}
+
+.docCreatorMenu-section {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ position: relative;
+ margin: 0px;
+ margin-top: 0px;
+ margin-bottom: 0px;
+ width: 100%;
+ height: 200;
+ flex: 0 0 auto;
+}
+
+.docCreatorMenu-GPT-options-container {
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ align-items: center;
+ position: relative;
+ width: auto;
+ margin: 0px;
+ margin-top: 5px;
+ padding: 0px;
+}
+
+.docCreatorMenu-templates-preview-window {
+ display: flex;
+ flex-direction: row;
+ //justify-content: center;
+ align-items: center;
+ overflow-y: scroll;
+ position: relative;
+ height: 125px;
+ width: calc(100% - 10px);
+ -ms-overflow-style: none;
+ scrollbar-width: none;
+
+ .loading-spinner {
+ justify-self: center;
+ }
+}
+
+.divvv{
+ width: 200;
+ height: 200;
+ border: solid 1px white;
+}
+
+.docCreatorMenu-section-topbar {
+ position: relative;
+ display: flex;
+ flex-direction: row;
+ width: 100%;
+}
+
+.section-reveal-options {
+ margin-top: 0px;
+ margin-bottom: 0px;
+ margin-right: 0px;
+ margin-left: auto;
+ border: 0px;
+ background: none;
+
+ &.float-right {
+ float: right;
+ }
+}
+
+.docCreatorMenu-section-title {
+ border: 1px solid rgb(163, 163, 163);
+ border-top: 0px;
+ border-left: 0px;
+ border-bottom-right-radius: 5px;
+ font-size: 12px;
+ padding: 2px;
+ padding-left: 3px;
+ padding-right: 3px;
+ margin-bottom: 3px;
+}
+
+.docCreatorMenu-GPT-generate {
+ height: 30px;
+ width: 30px;
+ background-color: rgb(176, 229, 149);
+ border: 1px solid rgb(126, 219, 80);
+ border-radius: 5px;
+ padding: 0px;
+ font-size: 14px;
+ color: white;
+ letter-spacing: 1px;
+ flex: 0 0 auto;
+
+ &:hover {
+ background-color: rgb(129, 223, 83);
+ border: 2px solid rgb(80, 185, 28);
+ }
+}
+
+.docCreatorMenu-GPT-prompt-input {
+ width: 140px;
+ height: 25px;
+ overflow-y: scroll;
+ border: 1px solid rgb(180, 180, 180);
+ background-color: rgb(35, 35, 35);
+ border-radius: 3px;
+ padding-left: 4px;
+}
+
+//------------------------------------------------------------------------------------------------------------------------------------------
+// DocCreatorMenu options CSS
+//--------------------------------------------------------------------------------------------------------------------------------------------
+
+.docCreatorMenu-option-container{
+ display: flex;
+ width: 180px;
+ height: 30px;
+ flex-direction: row;
+ justify-content: center;
+ align-items: center;
+ margin-top: 10px;
+ margin-bottom: 10px;
+
+ &.layout{
+ z-index: 5;
+ }
+}
+
+.docCreatorMenu-option-title{
+ display: flex;
+ width: 140px;
+ height: 30px;
+ background: whitesmoke;
+ background-color: rgb(34, 34, 37);
+ border-radius: 5px;
+ border-top-right-radius: 0px;
+ border-bottom-right-radius: 0px;
+ border: 1px solid rgb(180, 180, 180);
+ padding: 0px;
+ font-size: 12px;
+ align-items: center;
+ justify-content: center;
+ text-transform: uppercase;
+ cursor: pointer;
+
+ &.spacer {
+ border-left: none;
+ border-right: none;
+ border-radius: 0px;
+ width: auto;
+ text-transform: none;
+
+ &.small {
+ height: 20px;
+ transform: translateY(-5px);
+ }
+ }
+
+ &.config {
+ border-radius: 4px;
+ border-top-right-radius: 0px;
+ border-bottom-right-radius: 0px;
+ width: 30px;
+ border-right: 0px;
+ gap: 3px;
+
+ &.layout-config {
+ height: 20px;
+ transform: translateY(-5px);
+ text-transform: none;
+ padding-left: 2px;
+ }
+
+ &.dimensions {
+ text-transform: none;
+ height: 20px;
+ transform: translateY(-5px);
+ width: 70px;
+ }
+ }
+}
+
+.docCreatorMenu-input {
+ display: flex;
+ height: 30px;
+ background-color: rgb(34, 34, 37);
+ border: 1px solid rgb(180, 180, 180);
+ align-items: center;
+ justify-content: center;
+
+ &.config {
+ border-radius: 4px;
+ margin: 0px;
+ border-top-left-radius: 0px;
+ border-bottom-left-radius: 0px;
+ border-left: 0px;
+ width: 25px;
+
+ &.layout-config {
+ height: 20px;
+ transform: translateY(-5px);
+ }
+
+ &.dimensions {
+ height: 20px;
+ width: 30px;
+ transform: translateY(-5px);
+
+ &.right {
+ border-top-left-radius: 0px;
+ border-bottom-left-radius: 0px;
+ }
+
+ &.left {
+ border-radius: 0px;
+ border-right: 0px;
+ }
+ }
+ }
+}
+
+.docCreatorMenu-configuration-bar {
+ width: 200;
+ gap: 5px;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: center;
+
+ &.no-gap {
+ gap: 0px;
+ }
+}
+
+.docCreatorMenu-menu-container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ overflow-y: scroll;
+ margin: 5px;
+ margin-top: 0px;
+ width: calc(100% - 10px);
+ height: calc(100% - 30px);
+ border: 1px solid rgb(180, 180, 180);
+ border-radius: 5px;
+
+ .docCreatorMenu-option-container{
+ width: 180px;
+ height: 30px;
+
+ .docCreatorMenu-dropdown-hoverable {
+ width: 140px;
+ height: 30px;
+
+ &:hover .docCreatorMenu-dropdown-content {
+ display: block;
+ }
+
+ &:hover .docCreatorMenu-option-title {
+ border-bottom-left-radius: 0px;
+ border-bottom-right-radius: 0px;
+ }
+
+ .docCreatorMenu-dropdown-content {
+ display: none;
+ min-width: 100px;
+ height: 75px;
+ overflow-y: scroll;
+ -ms-overflow-style: none;
+ scrollbar-width: none;
+ border-bottom: 1px solid rgb(180, 180, 180);
+ border-bottom-left-radius: 5px;
+ border-bottom-right-radius: 5px;
+
+ .docCreatorMenu-dropdown-option{
+ display: flex;
+ background-color: rgb(42, 42, 46);
+ border-left: 1px solid rgb(180, 180, 180);
+ border-right: 1px solid rgb(180, 180, 180);
+ border-bottom: 1px solid rgb(180, 180, 180);
+ width: 140px;
+ height: 25px;
+ justify-content: center;
+ justify-items: center;
+ padding-top: 3px;
+
+ &:hover {
+ background-color: rgb(68, 68, 74);
+ cursor: pointer;
+ }
+ }
+ }
+ }
+ }
+}
+
+.docCreatorMenu-layout-preview-window-wrapper {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 85%;
+ height: auto;
+ position: relative;
+ padding: 0px;
+
+ &:hover .docCreatorMenu-zoom-button-container {
+ display: block;
+ }
+
+ .docCreatorMenu-layout-preview-window {
+ padding: 5px;
+ flex: 0 0 auto;
+ overflow: scroll;
+ display: grid;
+ width: 100%;
+ aspect-ratio: 1;
+ //height: auto;
+ // max-width: 240;
+ // max-height: 240;
+ border: 1px solid rgb(180, 180, 180);
+ border-radius: 5px;
+ background-color: rgb(34, 34, 37);
+ -ms-overflow-style: none;
+ scrollbar-width: none;
+
+ &.small {
+ max-width: 100;
+ max-height: 100;
+ }
+
+ .docCreatorMenu-layout-preview-item {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ border-radius: 3px;
+ border: solid 1px lightblue;
+
+ &:hover {
+ border: solid 2px rgb(68, 153, 233);
+ z-index: 2;
+ }
+ }
+ }
+
+ .docCreatorMenu-zoom-button-container {
+ position: absolute;
+ top: 0px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ display: none;
+ z-index: 999;
+ }
+
+ .docCreatorMenu-zoom-button{
+ width: 15px;
+ height: 15px;
+ background: whitesmoke;
+ background-color: rgb(34, 34, 37);
+ border-radius: 3px;
+ border: 1px solid rgb(180, 180, 180);
+ padding: 0px;
+ font-size: 10px;
+ z-index: 6;
+ margin-left: 0px;
+ margin-top: 0px;
+ margin-right: 0px; //225px
+ margin-bottom: 0px;
+ }
+}
+
+//------------------------------------------------------------------------------------------------------------------------------------------
+// DocCreatorMenu dashboard CSS
+//--------------------------------------------------------------------------------------------------------------------------------------------
+
+.docCreatorMenu-dashboard-view {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+ overflow-y: hidden;
+ //align-items: flex-start;
+ margin: 5px;
+ margin-top: 0px;
+ width: calc(100% - 10px);
+ height: calc(100% - 30px);
+ border: 1px solid rgb(180, 180, 180);
+ border-radius: 5px;
+ -ms-overflow-style: none;
+ scrollbar-width: none;
+
+ .panels-container {
+ height: 100%;
+ width: 100%;
+ flex-direction: column;
+ justify-content: flex-start;
+ overflow-y: scroll;
+ }
+
+ .topbar {
+ height: 30px;
+ width: 100%;
+ background-color: rgb(50, 50, 50);
+ }
+
+// .field-panel {
+// position: relative;
+// display: flex;
+// // align-items: flex-start;
+// flex-direction: column;
+// gap: 5px;
+// padding: 5px;
+// height: 100px;
+// //width: 100%;
+// border: 1px solid rgb(180, 180, 180);
+// margin: 5px;
+// margin-top: 0px;
+// border-radius: 3px;
+// flex: 0 0 auto;
+
+// .properties-wrapper {
+// display: flex;
+// flex-direction: row;
+// align-items: flex-start;
+// gap: 5px;
+
+// .field-property-container {
+// background-color: rgb(40, 40, 40);
+// border: 1px solid rgb(100, 100, 100);
+// border-radius: 3px;
+// width: 30%;
+// height: 25px;
+// padding-left: 3px;
+// align-items: center;
+// color: whitesmoke;
+// }
+
+// .field-type-selection-container {
+// display: flex;
+// flex-direction: row;
+// align-items: center;
+// background-color: rgb(40, 40, 40);
+// border: 1px solid rgb(100, 100, 100);
+// border-radius: 3px;
+// width: 31%;
+// height: 25px;
+// padding-left: 3px;
+// color: whitesmoke;
+
+// .placeholder {
+// color: gray;
+// }
+
+// &:hover .placeholder {
+// display: none;
+// }
+
+// .bubbles {
+// display: none;
+// }
+
+// .text {
+// margin-top: 5px;
+// margin-bottom: 5px;
+// }
+
+// &:hover .bubbles {
+// display: flex;
+// flex-direction: row;
+// align-items: flex-start;
+// }
+
+// &:hover .type-display {
+// display: none;
+// }
+
+// .bubble {
+// margin: 5px;
+// }
+
+// &:hover .bubble {
+// margin-top: 7px;
+// }
+// }
+// }
+
+// .field-description-container {
+// background-color: rgb(40, 40, 40);
+// border: 1px solid rgb(100, 100, 100);
+// border-radius: 3px;
+// width: 100%;
+// height: 100%;
+// resize: none;
+
+// ::-webkit-scrollbar-track {
+// background: none;
+// }
+// }
+
+// .top-right {
+// position: absolute;
+// top: 0px;
+// right: 0px;
+// }
+// }
+// }
+
+ .field-panel {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: flex-start;
+ height: 285px;
+ width: calc(100% - 10px);
+ border: 1px solid rgb(180, 180, 180);
+ margin: 5px;
+ margin-top: 0px;
+ margin-bottom: 10px;
+ border-radius: 3px;
+ flex: 0 0 auto;
+ gap: 25px;
+ background-color: rgb(60, 60, 60);
+
+ .top-bar {
+ position: relative;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: center;
+ border-bottom: 1px solid rgb(180, 180, 180);
+ border-top-right-radius: 5px;
+ border-top-left-radius: 5px;
+ width: 100%;
+ height: 20px;
+ background-color: rgb(50, 50, 50);
+ color: rgb(168, 167, 167);
+
+ .field-title {
+ color: whitesmoke;
+ }
+ }
+
+ .opts-bar {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+
+ .opt-box {
+ border: 1px solid rgb(180, 180, 180);
+ border-radius: 5px;
+ width: 40%;
+ height: 50px;
+ margin-right: 4%;
+ margin-left: 4%;
+ box-shadow: 5px 5px rgb(29, 29, 31);
+ }
+
+ .content {
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ align-items: center;
+ height: calc(100% - 20px);
+ width: 100%;
+ background-color: rgb(50, 50, 50);
+ border-bottom-right-radius: 5px;
+ border-bottom-left-radius: 5px;
+ resize: none;
+
+ .bubbles {
+ display: none;
+ }
+
+ .text {
+ margin-right: 5px;
+ }
+
+ &:hover .bubbles {
+ display: flex;
+ flex-direction: row;
+ align-items: flex-start;
+ }
+
+ &:hover .type-display {
+ display: none;
+ }
+
+ .bubble {
+ margin: 3px;
+ }
+ }
+ }
+
+ .sizes-box {
+ width: 88%;
+ height: 60px;
+ border: 1px solid rgb(180, 180, 180);
+ border-radius: 5px;
+ background-color: rgb(50, 50, 50);
+ box-shadow: 5px 5px rgb(29, 29, 31);
+
+ .content {
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ align-items: center;
+ height: calc(100% - 20px);
+ width: 100%;
+ background-color: rgb(50, 50, 50);
+ border-bottom-right-radius: 5px;
+ border-bottom-left-radius: 5px;
+
+ .text {
+ margin-right: 9px;
+ }
+
+ .bubbles {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ }
+
+ .bubble {
+ margin: 3px;
+ margin-right: 4px;
+ }
+ }
+ }
+
+ .desc-box {
+ width: 88%;
+ height: 50px;
+ border: 1px solid rgb(180, 180, 180);
+ border-radius: 5px;
+ background-color: rgb(50, 50, 50);
+ box-shadow: 5px 5px rgb(29, 29, 31);
+
+ .content {
+ height: calc(100% - 20px);
+ width: 100%;
+ background-color: rgb(50, 50, 50);
+ border-bottom-right-radius: 5px;
+ border-bottom-left-radius: 5px;
+ resize: none;
+
+ }
+ }
+
+ }
+
+}
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu.tsx
new file mode 100644
index 000000000..6c649bde3
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu.tsx
@@ -0,0 +1,2361 @@
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { Colors } from 'browndash-components';
+import { action, computed, makeObservable, observable, reaction, runInAction } from 'mobx';
+import { observer } from 'mobx-react';
+import { IDisposer } from 'mobx-utils';
+import * as React from 'react';
+import ReactLoading from 'react-loading';
+import { ClientUtils, returnEmptyFilter, returnFalse, returnTrue, setupMoveUpEvents } from '../../../../ClientUtils';
+import { emptyFunction } from '../../../../Utils';
+import { Doc, NumListCast, StrListCast, returnEmptyDoclist } from '../../../../fields/Doc';
+import { Id } from '../../../../fields/FieldSymbols';
+import { Cast, DocCast, ImageCast, StrCast } from '../../../../fields/Types';
+import { ImageField } from '../../../../fields/URLField';
+import { Networking } from '../../../Network';
+import { GPTCallType, gptAPICall, gptImageCall } from '../../../apis/gpt/GPT';
+import { Docs } from '../../../documents/Documents';
+import { DragManager } from '../../../util/DragManager';
+import { MakeTemplate } from '../../../util/DropConverter';
+import { SnappingManager } from '../../../util/SnappingManager';
+import { UndoManager, undoable } from '../../../util/UndoManager';
+import { LightboxView } from '../../LightboxView';
+import { ObservableReactComponent } from '../../ObservableReactComponent';
+import { CollectionFreeFormView } from '../../collections/collectionFreeForm/CollectionFreeFormView';
+import { DocumentView, DocumentViewInternal } from '../DocumentView';
+import { FieldViewProps } from '../FieldView';
+import { OpenWhere } from '../OpenWhere';
+import { DataVizBox } from './DataVizBox';
+import './DocCreatorMenu.scss';
+import { DefaultStyleProvider, returnEmptyDocViewList } from '../../StyleProvider';
+import { Transform } from '../../../util/Transform';
+import { IconProp } from '@fortawesome/fontawesome-svg-core';
+
+export enum LayoutType {
+ Stacked = 'stacked',
+ Grid = 'grid',
+ Row = 'row',
+ Column = 'column',
+ Custom = 'custom',
+}
+
+@observer
+export class DocCreatorMenu extends ObservableReactComponent<FieldViewProps> {
+ static Instance: DocCreatorMenu;
+
+ private _disposers: { [name: string]: IDisposer } = {};
+
+ private _ref: HTMLDivElement | null = null;
+
+ @observable _templateDocs: Doc[] = [];
+ @observable _selectedTemplate: Doc | undefined = undefined;
+ @observable _columns: Col[] = [];
+ @observable _selectedCols: { title: string; type: string; desc: string }[] | undefined = [];
+
+ @observable _layout: { type: LayoutType; yMargin: number; xMargin: number; columns?: number; repeat: number } = { type: LayoutType.Grid, yMargin: 0, xMargin: 0, repeat: 0 };
+ @observable _layoutPreview: boolean = true;
+ @observable _layoutPreviewScale: number = 1;
+ @observable _savedLayouts: DataVizTemplateLayout[] = [];
+ @observable _expandedPreview: { icon: ImageField; doc: Doc } | undefined = undefined;
+
+ @observable _suggestedTemplates: Doc[] = [];
+ @observable _GPTOpt: boolean = false;
+ @observable _userPrompt: string = '';
+ @observable _callCount: number = 0;
+ @observable _GPTLoading: boolean = false;
+
+ @observable _pageX: number = 0;
+ @observable _pageY: number = 0;
+ @observable _indicatorX: number | undefined = undefined;
+ @observable _indicatorY: number | undefined = undefined;
+
+ @observable _hoveredLayoutPreview: number | undefined = undefined;
+ @observable _mouseX: number = -1;
+ @observable _mouseY: number = -1;
+ @observable _startPos?: { x: number; y: number };
+ @observable _shouldDisplay: boolean = false;
+
+ @observable _menuContent: 'templates' | 'options' | 'saved' | 'dashboard' = 'templates';
+ @observable _dragging: boolean = false;
+ @observable _draggingIndicator: boolean = false;
+ @observable _dataViz?: DataVizBox;
+ @observable _interactionLock: any;
+ @observable _snapPt: any;
+ @observable _resizeHdlId: string = '';
+ @observable _resizing: boolean = false;
+ @observable _offset: { x: number; y: number } = { x: 0, y: 0 };
+ @observable _resizeUndo: UndoManager.Batch | undefined = undefined;
+ @observable _initDimensions: { width: number; height: number; x?: number; y?: number } = { width: 300, height: 400, x: undefined, y: undefined };
+ @observable _menuDimensions: { width: number; height: number } = { width: 400, height: 400 };
+ @observable _editing: boolean = false;
+
+ constructor(props: any) {
+ super(props);
+ makeObservable(this);
+ DocCreatorMenu.Instance = this;
+ //setTimeout(() => this.generateTemplates(''));
+ }
+
+ @action setDataViz = (dataViz: DataVizBox) => {
+ this._dataViz = dataViz;
+ };
+ @action setTemplateDocs = (docs: Doc[]) => {
+ this._templateDocs = docs.map(doc => (doc.annotationOn ? DocCast(doc.annotationOn) : doc));
+ };
+ @action setGSuggestedTemplates = (docs: Doc[]) => {
+ this._suggestedTemplates = docs;
+ };
+
+ @computed get docsToRender() {
+ return this._selectedTemplate ? NumListCast(this._dataViz?.layoutDoc.dataViz_selectedRows) : [];
+ }
+
+ @computed get rowsCount() {
+ switch (this._layout.type) {
+ case LayoutType.Row:
+ case LayoutType.Stacked:
+ return 1;
+ case LayoutType.Column:
+ return this.docsToRender.length;
+ case LayoutType.Grid:
+ return Math.ceil(this.docsToRender.length / (this._layout.columns ?? 1)) ?? 0;
+ default:
+ return 0;
+ }
+ }
+
+ @computed get columnsCount() {
+ switch (this._layout.type) {
+ case LayoutType.Row:
+ return this.docsToRender.length;
+ case LayoutType.Column:
+ case LayoutType.Stacked:
+ return 1;
+ case LayoutType.Grid:
+ return this._layout.columns ?? 0;
+ default:
+ return 0;
+ }
+ }
+
+ @computed get selectedFields() {
+ return StrListCast(this._dataViz?.layoutDoc._dataViz_axes);
+ }
+
+ @computed get fieldsInfos(): Col[] {
+ const colInfo = this._dataViz?.colsInfo;
+ return this.selectedFields
+ .map(field => {
+ const fieldInfo = colInfo?.get(field);
+
+ const col: Col = {
+ title: field,
+ type: fieldInfo?.type ?? TemplateFieldType.UNSET,
+ desc: fieldInfo?.desc ?? '',
+ sizes: fieldInfo?.sizes ?? [TemplateFieldSize.MEDIUM],
+ };
+
+ if (fieldInfo?.defaultContent !== undefined) {
+ col.defaultContent = fieldInfo.defaultContent;
+ }
+
+ return col;
+ })
+ .concat(this._columns);
+ }
+
+ @computed get canMakeDocs() {
+ return this._selectedTemplate !== undefined && this._layout !== undefined;
+ }
+
+ get bounds(): { t: number; b: number; l: number; r: number } {
+ const rect = this._ref?.getBoundingClientRect();
+ const bounds = { t: rect?.top ?? 0, b: rect?.bottom ?? 0, l: rect?.left ?? 0, r: rect?.right ?? 0 };
+ return bounds;
+ }
+
+ setUpButtonClick = (e: any, func: Function) => {
+ setupMoveUpEvents(
+ this,
+ e,
+ returnFalse,
+ emptyFunction,
+ undoable(clickEv => {
+ clickEv.stopPropagation();
+ clickEv.preventDefault();
+ func();
+ }, 'create docs')
+ );
+ };
+
+ @action
+ onPointerDown = (e: PointerEvent) => {
+ this._mouseX = e.clientX;
+ this._mouseY = e.clientY;
+ };
+
+ @action
+ onPointerUp = (e: PointerEvent) => {
+ if (this._resizing) {
+ this._initDimensions.width = this._menuDimensions.width;
+ this._initDimensions.height = this._menuDimensions.height;
+ this._initDimensions.x = this._pageX;
+ this._initDimensions.y = this._pageY;
+ document.removeEventListener('pointermove', this.onResize);
+ SnappingManager.SetIsResizing(undefined);
+ this._resizing = false;
+ }
+ if (this._dragging) {
+ document.removeEventListener('pointermove', this.onDrag);
+ this._dragging = false;
+ }
+ if (e.button !== 2 && !e.ctrlKey) return;
+ const curX = e.clientX;
+ const curY = e.clientY;
+ if (Math.abs(this._mouseX - curX) > 1 || Math.abs(this._mouseY - curY) > 1) {
+ this._shouldDisplay = false;
+ }
+ };
+
+ componentDidMount() {
+ document.addEventListener('pointerdown', this.onPointerDown, true);
+ document.addEventListener('pointerup', this.onPointerUp);
+ this._disposers.templates = reaction(
+ () => this._templateDocs.slice(),
+ docs => this.updateIcons(docs)
+ );
+ this._disposers.gpt = reaction(
+ () => this._suggestedTemplates.slice(),
+ docs => this.updateIcons(docs)
+ );
+ //this._disposers.columns = reaction(() => this._dataViz?.layoutDoc._dataViz_axes, () => {this.generateTemplates('')})
+ this._disposers.lightbox = reaction(
+ () => LightboxView.LightboxDoc(),
+ doc => {
+ doc ? this._shouldDisplay && this.closeMenu() : !this._shouldDisplay && this.openMenu();
+ }
+ );
+ //this._disposers.fields = reaction(() => this._dataViz?.axes, cols => this._selectedCols = cols?.map(col => { return {title: col, type: '', desc: ''}}))
+ }
+
+ componentWillUnmount() {
+ Object.values(this._disposers).forEach(disposer => disposer?.());
+ document.removeEventListener('pointerdown', this.onPointerDown, true);
+ document.removeEventListener('pointerup', this.onPointerUp);
+ }
+
+ updateIcons = (docs: Doc[]) => {
+ console.log('called')
+ docs.map(this.getIcon);
+ };
+
+ @action
+ updateSelectedCols = (cols: string[]) => {
+ this._selectedCols;
+ };
+
+ @action
+ toggleDisplay = (x: number, y: number) => {
+ if (this._shouldDisplay) {
+ this._shouldDisplay = false;
+ } else {
+ this._pageX = x;
+ this._pageY = y;
+ this._shouldDisplay = true;
+ }
+ };
+
+ @action
+ closeMenu = () => {
+ this._shouldDisplay = false;
+ };
+
+ @action
+ openMenu = () => {
+ const allTemplates = this._templateDocs.concat(this._suggestedTemplates);
+ this._shouldDisplay = true;
+ this.updateIcons(allTemplates);
+ };
+
+ @action
+ onResizePointerDown = (e: React.PointerEvent): void => {
+ this._resizing = true;
+ document.addEventListener('pointermove', this.onResize);
+ SnappingManager.SetIsResizing(DocumentView.Selected().lastElement()?.Document[Id]); // turns off pointer events on things like youtube videos and web pages so that dragging doesn't get "stuck" when cursor moves over them
+ e.stopPropagation();
+ const id = (this._resizeHdlId = e.currentTarget.className);
+ const pad = id.includes('Left') || id.includes('Right') ? Number(getComputedStyle(e.target as any).width.replace('px', '')) / 2 : 0;
+ const bounds = e.currentTarget.getBoundingClientRect();
+ this._offset = {
+ x: id.toLowerCase().includes('left') ? bounds.right - e.clientX - pad : bounds.left - e.clientX + pad, //
+ y: id.toLowerCase().includes('top') ? bounds.bottom - e.clientY - pad : bounds.top - e.clientY + pad,
+ };
+ this._resizeUndo = UndoManager.StartBatch('drag resizing');
+ this._snapPt = { x: e.pageX, y: e.pageY };
+ };
+
+ @action
+ onResize = (e: any): boolean => {
+ const dragHdl = this._resizeHdlId.split(' ')[1];
+ const thisPt = DragManager.snapDrag(e, -this._offset.x, -this._offset.y, this._offset.x, this._offset.y);
+
+ const { scale, refPt, transl } = this.getResizeVals(thisPt, dragHdl);
+ !this._interactionLock && runInAction(async () => { // resize selected docs if we're not in the middle of a resize (ie, throttle input events to frame rate)
+ this._interactionLock = true;
+ const scaleAspect = {x: scale.x, y: scale.y};
+ this.resizeView(refPt, scaleAspect, transl); // prettier-ignore
+ await new Promise<any>(res => { setTimeout(() => { res(this._interactionLock = undefined)})});
+ }); // prettier-ignore
+ return true;
+ };
+
+ @action
+ onDrag = (e: any): boolean => {
+ this._pageX = e.pageX - (this._startPos?.x ?? 0);
+ this._pageY = e.pageY - (this._startPos?.y ?? 0);
+ this._initDimensions.x = this._pageX;
+ this._initDimensions.y = this._pageY;
+ return true;
+ };
+
+ getResizeVals = (thisPt: { x: number; y: number }, dragHdl: string) => {
+ const [w, h] = [this._initDimensions.width, this._initDimensions.height];
+ const [moveX, moveY] = [thisPt.x - this._snapPt.x, thisPt.y - this._snapPt.y];
+ let vals: { scale: { x: number; y: number }; refPt: [number, number]; transl: { x: number; y: number } };
+ switch (dragHdl) {
+ case 'topLeft': vals = { scale: { x: 1 - moveX / w, y: 1 -moveY / h }, refPt: [this.bounds.r, this.bounds.b], transl: {x: moveX, y: moveY } }; break;
+ case 'topRight': vals = { scale: { x: 1 + moveX / w, y: 1 -moveY / h }, refPt: [this.bounds.l, this.bounds.b], transl: {x: 0, y: moveY } }; break;
+ case 'top': vals = { scale: { x: 1, y: 1 -moveY / h }, refPt: [this.bounds.l, this.bounds.b], transl: {x: 0, y: moveY } }; break;
+ case 'left': vals = { scale: { x: 1 - moveX / w, y: 1 }, refPt: [this.bounds.r, this.bounds.t], transl: {x: moveX, y: 0 } }; break;
+ case 'bottomLeft': vals = { scale: { x: 1 - moveX / w, y: 1 + moveY / h }, refPt: [this.bounds.r, this.bounds.t], transl: {x: moveX, y: 0 } }; break;
+ case 'right': vals = { scale: { x: 1 + moveX / w, y: 1 }, refPt: [this.bounds.l, this.bounds.t], transl: {x: 0, y: 0 } }; break;
+ case 'bottomRight':vals = { scale: { x: 1 + moveX / w, y: 1 + moveY / h }, refPt: [this.bounds.l, this.bounds.t], transl: {x: 0, y: 0 } }; break;
+ case 'bottom': vals = { scale: { x: 1, y: 1 + moveY / h }, refPt: [this.bounds.l, this.bounds.t], transl: {x: 0, y: 0 } }; break;
+ default: vals = { scale: { x: 1, y: 1 }, refPt: [this.bounds.l, this.bounds.t], transl: {x: 0, y: 0 } }; break;
+ } // prettier-ignore
+ return vals;
+ };
+
+ resizeView = (refPt: number[], scale: { x: number; y: number }, translation: { x: number; y: number }) => {
+ const refCent = [refPt[0], refPt[1]]; // fixed reference point for resize (ie, a point that doesn't move)
+ if (this._initDimensions.x === undefined) this._initDimensions.x = this._pageX;
+ if (this._initDimensions.y === undefined) this._initDimensions.y = this._pageY;
+ const { height, width, x, y } = this._initDimensions;
+
+ this._menuDimensions.width = Math.max(300, scale.x * width);
+ this._menuDimensions.height = Math.max(200, scale.y * height);
+ this._pageX = x + translation.x;
+ this._pageY = y + translation.y;
+ };
+
+ async getIcon(doc: Doc) {
+ const docView = DocumentView.getDocumentView(doc);
+ if (docView) {
+ docView.ComponentView?.updateIcon?.();
+ return new Promise<ImageField | undefined>(res => setTimeout(() => res(ImageCast(docView.Document.icon)), 500));
+ }
+ return undefined;
+ }
+
+ @action updateSelectedTemplate = (template: Doc) => {
+ if (this._selectedTemplate === template) {
+ this._selectedTemplate = undefined;
+ return;
+ } else {
+ this._selectedTemplate = template;
+ MakeTemplate(template);
+ }
+ };
+
+ @action updateSelectedSavedLayout = (layout: DataVizTemplateLayout) => {
+ this._layout.xMargin = layout.layout.xMargin;
+ this._layout.yMargin = layout.layout.yMargin;
+ this._layout.type = layout.layout.type;
+ this._layout.columns = layout.columns;
+ };
+
+ isSelectedLayout = (layout: DataVizTemplateLayout) => {
+ return this._layout.xMargin === layout.layout.xMargin && this._layout.yMargin === layout.layout.yMargin && this._layout.type === layout.layout.type && this._layout.columns === layout.columns;
+ };
+
+ @action
+ generateTemplates = async (inputText: string) => {
+ ++this._callCount;
+ const origCount = this._callCount;
+
+ let prompt: string = `(#${origCount}) Please generate for the fields:`;
+ this.selectedFields?.forEach(field => (prompt += ` ${field},`));
+ prompt += ` (-----NOT A FIELD-----) Additional prompt: ${inputText}`;
+
+ this._GPTLoading = true;
+
+ try {
+ const res = await gptAPICall(prompt, GPTCallType.TEMPLATE);
+
+ if (res && this._callCount === origCount) {
+ this._suggestedTemplates = [];
+ const templates: { template_type: string; fieldVals: { title: string; tlx: string; tly: string; brx: string; bry: string }[] }[] = JSON.parse(res);
+ this.createGeneratedTemplates(templates, 500, 500);
+ }
+ } catch (err) {
+ console.error(err);
+ }
+ };
+
+ @action
+ createGeneratedTemplates = (layouts: { template_type: string; fieldVals: { title: string; tlx: string; tly: string; brx: string; bry: string }[] }[], tempWidth: number, tempHeight: number) => {
+ const mainCollection = this._dataViz?.DocumentView?.().containerViewPath?.().lastElement()?.ComponentView as CollectionFreeFormView;
+ const GPTTemplates: Doc[] = [];
+
+ layouts.forEach(layout => {
+ const fields: Doc[] = layout.fieldVals.map(field => {
+ const left: number = (Number(field.tlx) * tempWidth) / 2;
+ const top: number = Number(field.tly) * tempHeight / 2; //prettier-ignore
+ const right: number = (Number(field.brx) * tempWidth) / 2;
+ const bottom: number = Number(field.bry) * tempHeight / 2; //prettier-ignore
+ const height = bottom - top;
+ const width = right - left;
+ const doc = !field.title.includes('$$')
+ ? Docs.Create.TextDocument('', { _height: height, _width: width, title: field.title, x: left, y: top, _text_fontSize: `${height / 2}` })
+ : Docs.Create.ImageDocument('', { _height: height, _width: width, title: field.title.replace(/\$\$/g, ''), x: left, y: top });
+ return doc;
+ });
+
+ const template = Docs.Create.FreeformDocument(fields, { _height: tempHeight, _width: tempWidth, title: layout.template_type, x: 400000, y: 400000 });
+
+ mainCollection.addDocument(template);
+
+ GPTTemplates.push(template);
+ });
+
+ setTimeout(() => {
+ this.setGSuggestedTemplates(GPTTemplates); /*GPTTemplates.forEach(template => mainCollection.removeDocument(template))*/
+ }, 100);
+
+ this.forceUpdate();
+ };
+
+ editTemplate = (doc: Doc) => {
+ //this.closeMenu();
+ DocumentViewInternal.addDocTabFunc(doc, OpenWhere.addRight);
+ DocumentView.DeselectAll();
+ Doc.UnBrushDoc(doc);
+ };
+
+ removeTemplate = (doc: Doc) => {
+ this._templateDocs.splice(this._templateDocs.indexOf(doc), 1);
+ };
+
+ testTemplate = async () => {
+ this.updateIcons(this._suggestedTemplates.slice());
+ this.forceUpdate();
+
+ // try {
+ // const res = await gptImageCall('Image of panda eating a cookie');
+
+ // if (res) {
+ // const result = await Networking.PostToServer('/uploadRemoteImage', { sources: res });
+
+ // console.log(result);
+ // }
+ // } catch (e) {
+ // console.log(e);
+ // }
+ };
+
+ @action addField = () => {
+ const newFields: Col[] = this._columns.concat([{ title: '', type: TemplateFieldType.UNSET, desc: '', sizes: [] }]);
+ this._columns = newFields;
+ };
+
+ @action removeField = (field: { title: string; type: string; desc: string }) => {
+ if (this._dataViz?.axes.includes(field.title)) {
+ this._dataViz.selectAxes(this._dataViz.axes.filter(col => col !== field.title));
+ } else {
+ const toRemove = this._columns.filter(f => f === field);
+ if (!toRemove) return;
+
+ if (toRemove.length > 1) {
+ while (toRemove.length > 1) {
+ toRemove.pop();
+ }
+ }
+
+ if (this._columns.length === 1) {
+ this._columns = [];
+ } else {
+ this._columns.splice(this._columns.indexOf(toRemove[0]), 1);
+ }
+ }
+ };
+
+ @action setColTitle = (column: Col, title: string) => {
+ if (this.selectedFields.includes(column.title)) {
+ this._dataViz?.setColumnTitle(column.title, title);
+ } else {
+ column.title = title;
+ }
+ this.forceUpdate();
+ };
+
+ @action setColType = (column: Col, type: TemplateFieldType) => {
+ if (this.selectedFields.includes(column.title)) {
+ this._dataViz?.setColumnType(column.title, type);
+ } else {
+ column.type = type;
+ }
+ this.forceUpdate();
+ };
+
+ modifyColSizes = (column: Col, size: TemplateFieldSize, valid: boolean) => {
+ if (this.selectedFields.includes(column.title)) {
+ this._dataViz?.modifyColumnSizes(column.title, size, valid);
+ } else {
+ if (!valid && column.sizes.includes(size)) {
+ column.sizes.splice(column.sizes.indexOf(size), 1);
+ } else if (valid && !column.sizes.includes(size)) {
+ column.sizes.push(size);
+ }
+ }
+ this.forceUpdate();
+ };
+
+ setColDesc = (column: Col, desc: string) => {
+ if (this.selectedFields.includes(column.title)) {
+ this._dataViz?.setColumnDesc(column.title, desc);
+ } else {
+ column.desc = desc;
+ }
+ this.forceUpdate();
+ };
+
+ generateGPTImage = async (prompt: string): Promise<string | undefined> => {
+ console.log(prompt);
+
+ try {
+ const res = await gptImageCall(prompt);
+
+ if (res) {
+ const result = await Networking.PostToServer('/uploadRemoteImage', { sources: res });
+ const source = ClientUtils.prepend(result[0].accessPaths.agnostic.client);
+ return source;
+ }
+ } catch (e) {
+ console.log(e);
+ }
+ };
+
+ matchesForTemplate = (template: TemplateDocInfos, cols: Col[]): number[][] => {
+ const colMatchesField = (col: Col, field: Field) => {
+ return field.sizes?.some(size => col.sizes?.includes(size)) && field.types?.includes(col.type);
+ };
+
+ const matches: number[][] = Array(template.fields.length)
+ .fill([])
+ .map(() => []);
+
+ template.fields.forEach((field, i) => {
+ cols.forEach((col, v) => {
+ if (colMatchesField(col, field)) {
+ matches[i].push(v);
+ }
+ });
+ });
+
+ return matches;
+ };
+
+ maxMatches = (fieldsCt: number, matches: number[][]) => {
+ const used: boolean[] = Array(fieldsCt).fill(false);
+ const mt: number[] = Array(fieldsCt).fill(-1);
+
+ const augmentingPath = (v: number): boolean => {
+ if (used[v]) return false;
+ used[v] = true;
+ for (const to of matches[v]) {
+ if (mt[to] === -1 || augmentingPath(mt[to])) {
+ mt[to] = v;
+ return true;
+ }
+ }
+ return false;
+ };
+
+ for (let v = 0; v < fieldsCt; ++v) {
+ used.fill(false);
+ augmentingPath(v);
+ }
+
+ let count: number = 0;
+
+ for (let i = 0; i < fieldsCt; ++i) {
+ if (mt[i] !== -1) ++count;
+ }
+
+ return count;
+ };
+
+ findValidTemplates = (cols: Col[], templates: TemplateDocInfos[]) => {
+ let validTemplates: any[] = [];
+ templates.forEach(template => {
+ const numFields = template.fields.length;
+ if (!(numFields === cols.length)) return;
+ const matches = this.matchesForTemplate(template, cols);
+ if (this.maxMatches(numFields, matches) === numFields) {
+ validTemplates.push(template.title);
+ }
+ });
+
+ validTemplates = validTemplates.map(title => TemplateLayouts.getTemplateByTitle(title));
+
+ return validTemplates;
+ };
+
+ // createColumnField = (template: TemplateDocInfos, field: Field, column: Col): Doc => {
+
+ // if (field.subfields) {
+ // const doc = FieldFuncs.FreeformField({
+ // tl: field.tl,
+ // br: field.br },
+ // template.height,
+ // template.width,
+ // column.title,
+ // '',
+ // field.opts
+ // );
+
+ // field.subfields[1].forEach(f => {
+ // const fDoc = ()
+ // })
+
+ // }
+
+ // return new Doc;
+ // }
+
+ /**
+ * Populates a preset template framework with content from a datavizbox or any AI-generated content.
+ * @param template the preloaded template framework being filled in
+ * @param assignments a list of template field numbers (from top to bottom) and their assigned columns from the linked dataviz
+ * @returns a doc containing the fully rendered template
+ */
+ fillPresetTemplate = async (template: TemplateDocInfos, assignments: { [field: string]: Col }): Promise<Doc> => {
+ const wordLimit = (size: TemplateFieldSize) => {
+ switch (size) {
+ case TemplateFieldSize.TINY:
+ return 2;
+ case TemplateFieldSize.SMALL:
+ return 5;
+ case TemplateFieldSize.MEDIUM:
+ return 20;
+ case TemplateFieldSize.LARGE:
+ return 50;
+ case TemplateFieldSize.HUGE:
+ return 100;
+ default:
+ return 10;
+ }
+ };
+
+ const renderTextCalls = async (): Promise<Doc[]> => {
+ const rendered: Doc[] = [];
+
+ if (GPTTextCalls.length) {
+ try {
+ const prompt = fieldContent + GPTTextAssignment;
+
+ const res = await gptAPICall(prompt, GPTCallType.FILL);
+
+ if (res) {
+ const assignments: { [title: string]: { number: string; content: string } } = JSON.parse(res);
+ //console.log('assignments', GPTAssignments, 'assignment string', GPTAssignmentString, 'field content', fieldContent, 'response', res, 'assignments', assignments);
+ Object.entries(assignments).forEach(([title, info]) => {
+ const field: Field = template.fields[Number(info.number)];
+ const col = this.getColByTitle(title);
+
+ const doc = FieldUtils.TextField(
+ {
+ tl: field.tl,
+ br: field.br,
+ },
+ template.height,
+ template.width,
+ col.title,
+ info.content ?? '',
+ field.opts
+ );
+
+ rendered.push(doc);
+ });
+ }
+ } catch (err) {
+ console.log(err);
+ }
+ }
+
+ return rendered;
+ };
+
+ const createGeneratedImage = async (fieldNum: string, col: Col, prompt: string) => {
+ const url = await this.generateGPTImage(prompt);
+ const field: Field = template.fields[Number(fieldNum)];
+ const doc = FieldUtils.ImageField(
+ {
+ tl: field.tl,
+ br: field.br,
+ },
+ template.height,
+ template.width,
+ col.title,
+ url ?? '',
+ field.opts
+ );
+
+ return doc;
+ };
+
+ const renderImageCalls = async (): Promise<Doc[]> => {
+ const rendered: Doc[] = [];
+ const calls = GPTIMGCalls;
+
+ if (calls.length) {
+ try {
+ const renderedImages: Doc[] = await Promise.all(
+ calls.map(async ([fieldNum, col]) => {
+ const sysPrompt =
+ 'Your job is to create a prompt for an AI image generator to help it generate an image based on existing content in a template and a user prompt. Your prompt should focus heavily on visual elements to help the image generator; avoid unecessary info that might distract it. ONLY INCLUDE THE PROMPT, NO OTHER TEXT OR EXPLANATION. The existing content is as follows: ' +
+ fieldContent +
+ ' **** The user prompt is: ' +
+ col.desc;
+
+ const prompt = await gptAPICall(sysPrompt, GPTCallType.COMPLETEPROMPT);
+ console.log(sysPrompt, prompt);
+
+ return createGeneratedImage(fieldNum, col, prompt);
+ })
+ );
+
+ const renderedTemplates: Doc[] = await Promise.all(renderedImages);
+ renderedTemplates.forEach(doc => rendered.push(doc));
+ } catch (e) {
+ console.log(e);
+ }
+ }
+
+ return rendered;
+ };
+
+ const fields: Doc[] = [];
+
+ const GPTAssignments = Object.entries(assignments).filter(([f, col]) => this._columns.includes(col));
+ const nonGPTAssignments: [string, Col][] = Object.entries(assignments).filter(a => !GPTAssignments.includes(a));
+ const GPTTextCalls = GPTAssignments.filter(([str, col]) => col.type === TemplateFieldType.TEXT);
+ const GPTIMGCalls = GPTAssignments.filter(([str, col]) => col.type === TemplateFieldType.VISUAL);
+
+ const stringifyGPTInfo = (calls: [string, Col][]): string => {
+ let string: string = '*** COLUMN INFO:';
+ calls.forEach(([fieldNum, col]) => {
+ string += `--- title: ${col.title}, prompt: ${col.desc}, word limit: ${wordLimit(col.sizes[0])} words, assigned field: ${fieldNum} ---`;
+ });
+ return (string += ' ***');
+ };
+
+ const GPTTextAssignment = stringifyGPTInfo(GPTTextCalls);
+
+ let fieldContent: string = '';
+
+ Object.entries(nonGPTAssignments).forEach(([f, strCol]) => {
+ const field: Field = template.fields[Number(f)];
+ const col = strCol[1];
+
+ const doc = (col.type === TemplateFieldType.VISUAL ? FieldUtils.ImageField : FieldUtils.TextField)(
+ {
+ tl: field.tl,
+ br: field.br,
+ },
+ template.height,
+ template.width,
+ col.title,
+ col.defaultContent ?? '',
+ field.opts
+ );
+
+ fieldContent += `--- Field #${f} (title: ${col.title}): ${col.defaultContent ?? ''} ---`;
+
+ fields.push(doc);
+ });
+
+ template.decorations.forEach(dec => {
+ const doc = FieldUtils.FreeformField(
+ {
+ tl: dec.tl,
+ br: dec.br,
+ },
+ template.height,
+ template.width,
+ '',
+ '',
+ dec.opts
+ );
+
+ fields.push(doc);
+ });
+
+ const createMainDoc = (): Doc => {
+ const main = Docs.Create.FreeformDocument(fields, {
+ _height: template.height,
+ _width: template.width,
+ title: template.title,
+ backgroundColor: template.opts.backgroundColor,
+ _layout_borderRounding: `${template.opts.cornerRounding}px` ?? '0px',
+ borderWidth: template.opts.borderWidth,
+ borderColor: template.opts.borderColor,
+ x: 40000,
+ y: 40000,
+ });
+
+ const mainCollection = this._dataViz?.DocumentView?.().containerViewPath?.().lastElement()?.ComponentView as CollectionFreeFormView;
+ mainCollection.addDocument(main);
+
+ return main;
+ };
+
+ const textCalls = await renderTextCalls();
+ const imageCalls = await renderImageCalls();
+
+ textCalls.forEach(doc => {
+ fields.push(doc);
+ });
+ imageCalls.forEach(doc => {
+ fields.push(doc);
+ });
+
+ return createMainDoc();
+ };
+
+ compileFieldDescriptions = (templates: TemplateDocInfos[]): string => {
+ let descriptions: string = '';
+ templates.forEach(template => {
+ descriptions += `---------- NEW TEMPLATE TO INCLUDE: Description of template ${template.title}'s fields: `;
+ template.fields.forEach((field, index) => {
+ descriptions += `{Field #${index}: ${field.description}} `;
+ });
+ });
+
+ return descriptions;
+ };
+
+ compileColDescriptions = (cols: Col[]): string => {
+ let descriptions: string = ' ------------- COL DESCRIPTIONS START HERE:';
+ cols.forEach(col => (descriptions += `{title: ${col.title}, sizes: ${String(col.sizes)}, type: ${col.type}, descreiption: ${col.desc}} `));
+
+ return descriptions;
+ };
+
+ getColByTitle = (title: string) => {
+ return this.fieldsInfos.filter(col => col.title === title)[0];
+ };
+
+ @action
+ assignColsToFields = async (templates: TemplateDocInfos[], cols: Col[]): Promise<[TemplateDocInfos, { [field: number]: Col }][]> => {
+ const fieldDescriptions: string = this.compileFieldDescriptions(templates);
+ const colDescriptions: string = this.compileColDescriptions(cols);
+
+ const inputText = fieldDescriptions.concat(colDescriptions);
+
+ ++this._callCount;
+ const origCount = this._callCount;
+
+ let prompt: string = `(${origCount}) ${inputText}`;
+
+ this._GPTLoading = true;
+
+ try {
+ const res = await gptAPICall(prompt, GPTCallType.TEMPLATE);
+
+ if (res && this._callCount === origCount) {
+ const assignments: { [templateTitle: string]: { [field: string]: string } } = JSON.parse(res);
+ const brokenDownAssignments: [TemplateDocInfos, { [field: number]: Col }][] = [];
+
+ Object.entries(assignments).forEach(([tempTitle, assignment]) => {
+ const template = TemplateLayouts.getTemplateByTitle(tempTitle);
+ if (!template) return;
+ const toObj = Object.entries(assignment).reduce(
+ (a, [fieldNum, colTitle]) => {
+ a[Number(fieldNum)] = this.getColByTitle(colTitle);
+ return a;
+ },
+ {} as { [field: number]: Col }
+ );
+ brokenDownAssignments.push([template, toObj]);
+ });
+ return brokenDownAssignments;
+ }
+ } catch (err) {
+ console.error(err);
+ }
+
+ return [];
+ };
+
+ generatePresetTemplates = async () => {
+ this._dataViz?.updateColDefaults();
+
+ const cols = this.fieldsInfos;
+ const templates = this.findValidTemplates(cols, TemplateLayouts.allTemplates);
+
+ const assignments: [TemplateDocInfos, { [field: number]: Col }][] = await this.assignColsToFields(templates, cols);
+
+ const renderedTemplatePromises: Promise<Doc>[] = assignments.map(([template, assignments]) => this.fillPresetTemplate(template, assignments));
+
+ const renderedTemplates: Doc[] = await Promise.all(renderedTemplatePromises);
+
+ setTimeout(() => {
+ this.setGSuggestedTemplates(renderedTemplates);
+ this._GPTLoading = false;
+ });
+ };
+
+ @action setExpandedView = (info: { icon: ImageField; doc: Doc } | undefined) => {
+ if (info) {
+ const doc = info.doc;
+ const wrapper: Doc = Docs.Create.FreeformDocument([info.doc], { _height: NumListCast(doc._height)[0], _width: NumListCast(doc._width)[0], title: ''});
+ const newInfo = {icon: new ImageField(''), doc: wrapper}
+ this._expandedPreview = newInfo;
+ } else {
+ this._expandedPreview = info;
+ }
+ };
+
+ get editingWindow(){
+ const doc = this._expandedPreview?.doc ?? new Doc();
+ const rendered =
+ <div className="docCreatorMenu-expanded-template-preview">
+ <CollectionFreeFormView
+ Document={this._expandedPreview!.doc}
+ docViewPath={returnEmptyDocViewList}
+ childLayoutTemplate={() => Cast(doc.childLayoutTemplate, Doc, null)}
+ isContentActive={emptyFunction}
+ isAnyChildContentActive={() => true}
+ select={emptyFunction}
+ isSelected={returnFalse}
+ fieldKey={Doc.LayoutFieldKey(doc)}
+ addDocument={returnFalse}
+ moveDocument={returnFalse}
+ removeDocument={returnFalse}
+ PanelWidth={() => this._menuDimensions.width - 10}
+ PanelHeight={() => this._menuDimensions.height - 60}
+ ScreenToLocalTransform={() => new Transform(-this._pageX,-this._pageY, 1)}
+ renderDepth={5}
+ whenChildContentsActiveChanged={emptyFunction}
+ focus={emptyFunction}
+ styleProvider={DefaultStyleProvider}
+ addDocTab={this._props.addDocTab}
+ // eslint-disable-next-line no-use-before-define
+ pinToPres={() => undefined}
+ childFilters={returnEmptyFilter}
+ childFiltersByRanges={returnEmptyFilter}
+ searchFilterDocs={returnEmptyDoclist}
+ fitContentsToBox={returnTrue}
+ xPadding={0}
+ yPadding={0}
+ />
+ </div>
+
+
+ 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._expandedPreview && this.updateIcons(this._suggestedTemplates.slice()); this.setExpandedView(undefined)})}>
+ <FontAwesomeIcon icon="minimize" />
+ </button>
+ <button className="docCreatorMenu-menu-button section-reveal-options top-right-lower" onPointerDown={e => this.setUpButtonClick(e, () => this._expandedPreview && this._templateDocs.push(this._expandedPreview.doc))}>
+ <FontAwesomeIcon icon="plus" color="white" />
+ </button>
+ </div>
+ </div>
+
+ );
+ }
+
+ get templatesPreviewContents() {
+ const renderedTemplates: Doc[] = [];
+
+ const GPTOptions = <div></div>;
+
+ //<img className='docCreatorMenu-preview-image expanded' src={this._expandedPreview.icon!.url.href.replace(".png", "_o.png")} />
+
+ return (
+ <div className={`docCreatorMenu-templates-view`}>
+ {this._expandedPreview ? (
+ 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._suggestedTemplates
+ ?.map(doc => ({ icon: ImageCast(doc.icon), doc }))
+ .filter(info => info.icon && info.doc)
+ .map(info => (
+ <div
+ className="docCreatorMenu-preview-window"
+ style={{
+ border: this._selectedTemplate === info.doc ? `solid 3px ${Colors.MEDIUM_BLUE}` : '',
+ boxShadow: this._selectedTemplate === info.doc ? `0 0 15px rgba(68, 118, 247, .8)` : '',
+ }}
+ onPointerDown={e => this.setUpButtonClick(e, () => runInAction(() => this.updateSelectedTemplate(info.doc)))}>
+ <button
+ className="option-button left"
+ onPointerDown={e =>
+ this.setUpButtonClick(e, () => {
+ this.setExpandedView(info);
+ })
+ }>
+ <FontAwesomeIcon icon="magnifying-glass" color="white" />
+ </button>
+ <button className="option-button right" onPointerDown={e => this.setUpButtonClick(e, () => this._templateDocs.push(info.doc))}>
+ <FontAwesomeIcon icon="plus" color="white" />
+ </button>
+ <img className="docCreatorMenu-preview-image" src={info.icon!.url.href.replace('.png', '_o.png')} />
+ </div>
+ ))
+ )}
+ </div>
+ <div className="docCreatorMenu-GPT-options">
+ <div className="docCreatorMenu-GPT-options-container">
+ <button className="docCreatorMenu-menu-button" onPointerDown={e => this.setUpButtonClick(e, () => this.generatePresetTemplates())}>
+ <FontAwesomeIcon icon="arrows-rotate" />
+ </button>
+ </div>
+ {this._GPTOpt ? GPTOptions : null}
+ </div>
+ </div>
+ <hr className="docCreatorMenu-option-divider full no-margin" />
+ <div className="docCreatorMenu-section">
+ <div className="docCreatorMenu-section-topbar">
+ <div className="docCreatorMenu-section-title">Your Templates</div>
+ <button className="docCreatorMenu-menu-button section-reveal-options" onPointerDown={e => this.setUpButtonClick(e, () => (this._GPTOpt = !this._GPTOpt))}>
+ <FontAwesomeIcon icon="gear" />
+ </button>
+ </div>
+ <div className="docCreatorMenu-templates-preview-window" style={{ justifyContent: this._menuDimensions.width > 400 ? 'center' : '' }}>
+ <div className="docCreatorMenu-preview-window empty" onPointerDown={e => this.testTemplate()}>
+ <FontAwesomeIcon icon="plus" color="rgb(160, 160, 160)" />
+ </div>
+ {this._templateDocs
+ .map(doc => ({ icon: ImageCast(doc.icon), doc }))
+ .filter(info => info.icon && info.doc)
+ .map(info => {
+ if (renderedTemplates.includes(info.doc)) return undefined;
+ renderedTemplates.push(info.doc);
+ return (
+ <div
+ className="docCreatorMenu-preview-window"
+ style={{
+ border: this._selectedTemplate === info.doc ? `solid 3px ${Colors.MEDIUM_BLUE}` : '',
+ boxShadow: this._selectedTemplate === info.doc ? `0 0 15px rgba(68, 118, 247, .8)` : '',
+ }}
+ onPointerDown={e => this.setUpButtonClick(e, () => runInAction(() => this.updateSelectedTemplate(info.doc)))}>
+ <button
+ className="option-button left"
+ onPointerDown={e =>
+ this.setUpButtonClick(e, () => {
+ this.editTemplate(info.doc);
+ })
+ }>
+ <FontAwesomeIcon icon="pencil" color="black" />
+ </button>
+ <button
+ className="option-button right"
+ onPointerDown={e =>
+ this.setUpButtonClick(e, () => {
+ this.removeTemplate(info.doc);
+ })
+ }>
+ <FontAwesomeIcon icon="trash" color="black" />
+ </button>
+ <img className="docCreatorMenu-preview-image" src={info.icon!.url.href.replace('.png', '_o.png')} />
+ </div>
+ );
+ })}
+ </div>
+ </div>
+ </div>
+ )}
+ </div>
+ );
+ }
+
+ get savedLayoutsPreviewContents() {
+ return (
+ <div className="docCreatorMenu-preview-container">
+ {this._savedLayouts.map((layout, index) => (
+ <div
+ className="docCreatorMenu-preview-window"
+ style={{
+ border: this.isSelectedLayout(layout) ? `solid 3px ${Colors.MEDIUM_BLUE}` : '',
+ boxShadow: this.isSelectedLayout(layout) ? `0 0 15px rgba(68, 118, 247, .8)` : '',
+ }}
+ onPointerDown={e => this.setUpButtonClick(e, () => runInAction(() => this.updateSelectedSavedLayout(layout)))}>
+ {this.layoutPreviewContents(87, layout, true, index)}
+ </div>
+ ))}
+ </div>
+ );
+ }
+
+ @action updateXMargin = (input: string) => {
+ this._layout.xMargin = Number(input);
+ };
+ @action updateYMargin = (input: string) => {
+ this._layout.yMargin = Number(input);
+ };
+ @action updateColumns = (input: string) => {
+ this._layout.columns = Number(input);
+ };
+
+ get layoutConfigOptions() {
+ const optionInput = (icon: string, func: Function, def?: number, key?: string, noMargin?: boolean) => {
+ return (
+ <div className="docCreatorMenu-option-container small no-margin" key={key} style={{ marginTop: noMargin ? '0px' : '' }}>
+ <div className="docCreatorMenu-option-title config layout-config">
+ <FontAwesomeIcon icon={icon as any} />
+ </div>
+ <input defaultValue={def} onInput={e => func(e.currentTarget.value)} className="docCreatorMenu-input config layout-config" />
+ </div>
+ );
+ };
+
+ switch (this._layout.type) {
+ case LayoutType.Row:
+ return <div className="docCreatorMenu-configuration-bar">{optionInput('arrows-left-right', this.updateXMargin, this._layout.xMargin, '0')}</div>;
+ case LayoutType.Column:
+ return <div className="docCreatorMenu-configuration-bar">{optionInput('arrows-up-down', this.updateYMargin, this._layout.yMargin, '1')}</div>;
+ case LayoutType.Grid:
+ return (
+ <div className="docCreatorMenu-configuration-bar">
+ {optionInput('arrows-up-down', this.updateYMargin, this._layout.xMargin, '2')}
+ {optionInput('arrows-left-right', this.updateXMargin, this._layout.xMargin, '3')}
+ {optionInput('table-columns', this.updateColumns, this._layout.columns, '4', true)}
+ </div>
+ );
+ case LayoutType.Stacked:
+ return null;
+ default:
+ break;
+ }
+ }
+
+ // doc = () => {
+ // return Docs.Create.FreeformDocument([], { _height: 200, _width: 200, title: 'title'});
+ // }
+
+ screenToLocalTransform = () => this._props.ScreenToLocalTransform();
+
+ layoutPreviewContents = (outerSpan: number, altLayout?: DataVizTemplateLayout, small: boolean = false, id?: number) => {
+ const doc: Doc | undefined = altLayout ? altLayout.template : this._selectedTemplate;
+ if (!doc) return;
+
+ const layout = altLayout ? altLayout.layout : this._layout;
+
+ const docWidth: number = Number(doc._width);
+ const docHeight: number = Number(doc._height);
+ const horizontalSpan: number = (docWidth + layout.xMargin) * (altLayout ? altLayout.columns : this.columnsCount) - layout.xMargin;
+ const verticalSpan: number = (docHeight + layout.yMargin) * (altLayout ? altLayout.rows : this.rowsCount) - layout.yMargin;
+ const largerSpan: number = horizontalSpan > verticalSpan ? horizontalSpan : verticalSpan;
+ const scaledDown = (input: number) => {
+ return input / ((largerSpan / outerSpan) * this._layoutPreviewScale);
+ };
+ const fontSize = Math.min(scaledDown(docWidth / 3), scaledDown(docHeight / 3));
+
+ return (
+ // <div className='divvv' style={{width: 100, height: 100, border: `1px solid white`}}>
+ // <CollectionFreeFormView
+ // // eslint-disable-next-line react/jsx-props-no-spreading
+ // {...this._props}
+ // Document={new Doc()}
+ // isContentActive={returnFalse}
+ // setContentViewBox={emptyFunction}
+ // NativeWidth={() => 100}
+ // NativeHeight={() => 100}
+ // pointerEvents={SnappingManager.IsDragging ? returnAll : returnNone}
+ // isAnnotationOverlay
+ // isAnnotationOverlayScrollable
+ // childDocumentsActive={returnFalse}
+ // fieldKey={this._props.fieldKey + '_annotations'}
+ // dropAction={dropActionType.move}
+ // select={emptyFunction}
+ // addDocument={returnFalse}
+ // removeDocument={returnFalse}
+ // moveDocument={returnFalse}
+ // renderDepth={this._props.renderDepth + 1}>
+ // {null}
+ // </CollectionFreeFormView>
+ // </div>
+ <div className="docCreatorMenu-layout-preview-window-wrapper" id={String(id) ?? undefined}>
+ <div className="docCreatorMenu-zoom-button-container">
+ <button className="docCreatorMenu-zoom-button" onPointerDown={e => this.setUpButtonClick(e, () => runInAction(() => (this._layoutPreviewScale *= 1.25)))}>
+ <FontAwesomeIcon icon={'minus'} />
+ </button>
+ <button className="docCreatorMenu-zoom-button zoom-in" onPointerDown={e => this.setUpButtonClick(e, () => runInAction(() => (this._layoutPreviewScale *= 0.75)))}>
+ <FontAwesomeIcon icon={'plus'} />
+ </button>
+ {altLayout ? (
+ <button className="docCreatorMenu-zoom-button trash" onPointerDown={e => this.setUpButtonClick(e, () => runInAction(() => this._savedLayouts.splice(this._savedLayouts.indexOf(altLayout), 1)))}>
+ <FontAwesomeIcon icon={'trash'} />
+ </button>
+ ) : null}
+ </div>
+ {
+ <div
+ id={String(id) ?? undefined}
+ className={`docCreatorMenu-layout-preview-window ${small ? 'small' : ''}`}
+ style={{
+ gridTemplateColumns: `repeat(${altLayout ? altLayout.columns : this.columnsCount}, ${scaledDown(docWidth)}px`,
+ gridTemplateRows: `${scaledDown(docHeight)}px`,
+ gridAutoRows: `${scaledDown(docHeight)}px`,
+ rowGap: `${scaledDown(layout.yMargin)}px`,
+ columnGap: `${scaledDown(layout.xMargin)}px`,
+ }}>
+ {this._layout.type === LayoutType.Stacked ? (
+ <div
+ className="docCreatorMenu-layout-preview-item"
+ style={{
+ width: scaledDown(docWidth),
+ height: scaledDown(docHeight),
+ fontSize: fontSize,
+ }}>
+ All
+ </div>
+ ) : (
+ this.docsToRender.map(num => (
+ <div
+ onMouseEnter={() => this._dataViz?.setSpecialHighlightedRow(num)}
+ onMouseLeave={() => this._dataViz?.setSpecialHighlightedRow(undefined)}
+ className="docCreatorMenu-layout-preview-item"
+ style={{
+ width: scaledDown(docWidth),
+ height: scaledDown(docHeight),
+ fontSize: fontSize,
+ }}>
+ {num}
+ </div>
+ ))
+ )}
+ </div>
+ }
+ </div>
+ );
+ };
+
+ get optionsMenuContents() {
+ const layoutEquals = (layout: DataVizTemplateLayout) => {}; //TODO: ADD LATER
+
+ const layoutOption = (option: LayoutType, optStyle?: {}, specialFunc?: Function) => {
+ return (
+ <div
+ className="docCreatorMenu-dropdown-option"
+ style={optStyle}
+ onPointerDown={e =>
+ this.setUpButtonClick(e, () => {
+ specialFunc?.();
+ runInAction(() => (this._layout.type = option));
+ })
+ }>
+ {option}
+ </div>
+ );
+ };
+
+ const selectionBox = (width: number, height: number, icon: string, specClass?: string, options?: JSX.Element[], manual?: boolean): JSX.Element => {
+ return (
+ <div className="docCreatorMenu-option-container">
+ <div className={`docCreatorMenu-option-title config ${specClass}`} style={{ width: width * 0.4, height: height }}>
+ <FontAwesomeIcon icon={icon as any} />
+ </div>
+ {manual ? (
+ <input className={`docCreatorMenu-input config ${specClass}`} style={{ width: width * 0.6, height: height }} />
+ ) : (
+ <select className={`docCreatorMenu-input config ${specClass}`} style={{ width: width * 0.6, height: height }}>
+ {options}
+ </select>
+ )}
+ </div>
+ );
+ };
+
+ const repeatOptions = [0, 1, 2, 3, 4, 5];
+
+ return (
+ <div className="docCreatorMenu-menu-container">
+ <div className="docCreatorMenu-option-container layout">
+ <div className="docCreatorMenu-dropdown-hoverable">
+ <div className="docCreatorMenu-option-title">{this._layout.type ? this._layout.type.toUpperCase() : 'Choose Layout'}</div>
+ <div className="docCreatorMenu-dropdown-content">
+ {layoutOption(LayoutType.Stacked)}
+ {layoutOption(LayoutType.Grid, undefined, () => {
+ if (!this._layout.columns) this._layout.columns = Math.ceil(Math.sqrt(this.docsToRender.length));
+ })}
+ {layoutOption(LayoutType.Row)}
+ {layoutOption(LayoutType.Column)}
+ {layoutOption(LayoutType.Custom, { borderBottom: `0px` })}
+ </div>
+ </div>
+ <button className="docCreatorMenu-menu-button preview-toggle" onPointerDown={e => this.setUpButtonClick(e, () => runInAction(() => (this._layoutPreview = !this._layoutPreview)))}>
+ <FontAwesomeIcon icon={this._layoutPreview ? 'minus' : 'magnifying-glass'} />
+ </button>
+ </div>
+ {this._layout.type ? this.layoutConfigOptions : null}
+ {this._layoutPreview ? this.layoutPreviewContents(this._menuDimensions.width * 0.75) : null}
+ {selectionBox(
+ 60,
+ 20,
+ 'repeat',
+ undefined,
+ repeatOptions.map(num => <option onPointerDown={e => (this._layout.repeat = num)}>{`${num}x`}</option>)
+ )}
+ <hr className="docCreatorMenu-option-divider" />
+ <div className="docCreatorMenu-general-options-container">
+ <button
+ className="docCreatorMenu-save-layout-button"
+ onPointerDown={e =>
+ setupMoveUpEvents(
+ this,
+ e,
+ returnFalse,
+ emptyFunction,
+ undoable(clickEv => {
+ clickEv.stopPropagation();
+ if (!this._selectedTemplate) return;
+ const layout: DataVizTemplateLayout = {
+ template: this._selectedTemplate,
+ layout: { type: this._layout.type, xMargin: this._layout.xMargin, yMargin: this._layout.yMargin, repeat: 0 },
+ columns: this.columnsCount,
+ rows: this.rowsCount,
+ docsNumList: this.docsToRender,
+ };
+ if (!this._savedLayouts.includes(layout)) {
+ this._savedLayouts.push(layout);
+ }
+ }, 'make docs')
+ )
+ }>
+ <FontAwesomeIcon icon="floppy-disk" />
+ </button>
+ <button
+ className="docCreatorMenu-create-docs-button"
+ style={{ backgroundColor: this.canMakeDocs ? '' : 'rgb(155, 155, 155)', border: this.canMakeDocs ? '' : 'solid 2px rgb(180, 180, 180)' }}
+ onPointerDown={e =>
+ setupMoveUpEvents(
+ this,
+ e,
+ returnFalse,
+ emptyFunction,
+ undoable(clickEv => {
+ clickEv.stopPropagation();
+ if (!this._selectedTemplate) return;
+ const templateInfo: DataVizTemplateInfo = { doc: this._selectedTemplate, layout: this._layout, referencePos: { x: this._pageX + 450, y: this._pageY }, columns: this.columnsCount };
+ this._dataViz?.createDocsFromTemplate(templateInfo);
+ }, 'make docs')
+ )
+ }>
+ <FontAwesomeIcon icon="plus" />
+ </button>
+ </div>
+ </div>
+ );
+ }
+
+ get dashboardContents() {
+ const sizes: string[] = ['tiny', 'small', 'medium', 'large', 'huge'];
+
+ const fieldPanel = (field: Col) => {
+ return (
+ <div className="field-panel">
+ <div className="top-bar">
+ <span className="field-title">{`${field.title} Field`}</span>
+ <button className="docCreatorMenu-menu-button section-reveal-options no-margin" onPointerDown={e => this.setUpButtonClick(e, () => this.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)' }} defaultValue={field.title} placeholder={'Enter title'} onChange={e => this.setColTitle(field, e.target.value)} />
+ </div>
+ <div className="opt-box">
+ <div className="top-bar"> Type </div>
+ <div className="content">
+ <span className="type-display">{field.type === TemplateFieldType.TEXT ? 'Text Field' : field.type === TemplateFieldType.VISUAL ? 'File Field' : ''}</span>
+ <div className="bubbles">
+ <input
+ className="bubble"
+ type="radio"
+ name="type"
+ onClick={() => {
+ this.setColType(field, TemplateFieldType.TEXT);
+ }}
+ />
+ <div className="text">Text</div>
+ <input
+ className="bubble"
+ type="radio"
+ name="type"
+ onClick={() => {
+ this.setColType(field, TemplateFieldType.VISUAL);
+ }}
+ />
+ <div className="text">File</div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div className="sizes-box">
+ <div className="top-bar"> Valid Sizes </div>
+ <div className="content">
+ <div className="bubbles">
+ {sizes.map(size => (
+ <>
+ <input
+ className="bubble"
+ type="checkbox"
+ name="type"
+ checked={field.sizes.includes(size as TemplateFieldSize)}
+ onChange={e => {
+ this.modifyColSizes(field, size as TemplateFieldSize, e.target.checked);
+ }}
+ />
+ <div className="text">{size}</div>
+ </>
+ ))}
+ </div>
+ </div>
+ </div>
+ <div className="desc-box">
+ <div className="top-bar"> 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 => fieldPanel(field))}</div>
+ </div>
+ );
+ }
+
+ get renderSelectedViewType() {
+ switch (this._menuContent) {
+ case 'templates':
+ return this.templatesPreviewContents;
+ case 'options':
+ return this.optionsMenuContents;
+ case 'saved':
+ return this.savedLayoutsPreviewContents;
+ case 'dashboard':
+ return this.dashboardContents;
+ default:
+ return undefined;
+ }
+ }
+
+ get resizePanes() {
+ const ref = this._ref?.getBoundingClientRect();
+ const height: number = ref?.height ?? 0;
+ const width: number = ref?.width ?? 0;
+
+ return [
+ <div className='docCreatorMenu-resizer top' onPointerDown={this.onResizePointerDown} style={{width: width, left: 0, top: -7}}/>,
+ <div className='docCreatorMenu-resizer right' onPointerDown={this.onResizePointerDown} style={{height: height, left: width - 3, top: 0}}/>,
+ <div className='docCreatorMenu-resizer bottom' onPointerDown={this.onResizePointerDown} style={{width: width, left: 0, top: height - 3}}/>,
+ <div className='docCreatorMenu-resizer left' onPointerDown={this.onResizePointerDown} style={{height: height, left: -7, top: 0}}/>,
+ <div className='docCreatorMenu-resizer topRight' onPointerDown={this.onResizePointerDown} style={{left: width - 5, top: -10, cursor: 'nesw-resize'}}/>,
+ <div className='docCreatorMenu-resizer topLeft' onPointerDown={this.onResizePointerDown} style={{left: -10, top: -10, cursor: 'nwse-resize'}}/>,
+ <div className='docCreatorMenu-resizer bottomRight' onPointerDown={this.onResizePointerDown} style={{left: width - 5, top: height - 5, cursor: 'nwse-resize'}}/>,
+ <div className='docCreatorMenu-resizer bottomLeft' onPointerDown={this.onResizePointerDown} style={{left: -10, top: height - 5, cursor: 'nesw-resize'}}/>
+ ]; //prettier-ignore
+ }
+
+ render() {
+ const topButton = (icon: string, opt: string, func: Function, tag: string) => {
+ return (
+ <div className={`top-button-container ${tag} ${opt === this._menuContent ? 'selected' : ''}`}>
+ <div
+ className="top-button-content"
+ onPointerDown={e =>
+ this.setUpButtonClick(e, () =>
+ runInAction(() => {
+ func();
+ })
+ )
+ }>
+ <FontAwesomeIcon icon={icon as any} />
+ </div>
+ </div>
+ );
+ };
+
+ const onPreviewSelected = () => {
+ this._menuContent = 'templates';
+ };
+ const onSavedSelected = () => {
+ this._menuContent = 'dashboard';
+ };
+ const onOptionsSelected = () => {
+ this._menuContent = 'options';
+ if (!this._layout.columns) this._layout.columns = Math.ceil(Math.sqrt(this.docsToRender.length));
+ };
+
+ return (
+ <div className="docCreatorMenu">
+ {!this._shouldDisplay ? undefined : (
+ <div
+ className="docCreatorMenu-cont"
+ ref={r => (this._ref = r)}
+ style={{
+ display: '',
+ left: this._pageX,
+ top: this._pageY,
+ width: this._menuDimensions.width,
+ height: this._menuDimensions.height,
+ background: SnappingManager.userBackgroundColor,
+ color: SnappingManager.userColor,
+ }}>
+ {this.resizePanes}
+ <div
+ className="docCreatorMenu-menu"
+ onPointerDown={e =>
+ setupMoveUpEvents(
+ this,
+ e,
+ e => {
+ this._dragging = true;
+ this._startPos = { x: 0, y: 0 };
+ this._startPos.x = e.pageX - (this._ref?.getBoundingClientRect().left ?? 0);
+ this._startPos.y = e.pageY - (this._ref?.getBoundingClientRect().top ?? 0);
+ document.addEventListener('pointermove', this.onDrag);
+ return true;
+ },
+ emptyFunction,
+ undoable(clickEv => {
+ clickEv.stopPropagation();
+ }, 'drag menu')
+ )
+ }>
+ <div className="docCreatorMenu-top-buttons-container">
+ {topButton('table-cells', 'templates', onPreviewSelected, 'left')}
+ {topButton('bars', 'options', onOptionsSelected, 'middle')}
+ {topButton('floppy-disk', 'saved', onSavedSelected, 'right')}
+ </div>
+ <button className="docCreatorMenu-menu-button close-menu" onPointerDown={e => this.setUpButtonClick(e, this.closeMenu)}>
+ <FontAwesomeIcon icon={'minus'} />
+ </button>
+ </div>
+ {this.renderSelectedViewType}
+ </div>
+ )}
+ </div>
+ );
+ }
+}
+
+export interface DataVizTemplateInfo {
+ doc: Doc;
+ layout: { type: LayoutType; xMargin: number; yMargin: number; repeat: number };
+ columns: number;
+ referencePos: { x: number; y: number };
+}
+
+export interface DataVizTemplateLayout {
+ template: Doc;
+ docsNumList: number[];
+ layout: { type: LayoutType; xMargin: number; yMargin: number; repeat: number };
+ columns: number;
+ rows: number;
+}
+
+export enum TemplateFieldType {
+ TEXT = 'text',
+ VISUAL = 'visual',
+ UNSET = 'unset',
+}
+
+export enum TemplateFieldSize {
+ TINY = 'tiny',
+ SMALL = 'small',
+ MEDIUM = 'medium',
+ LARGE = 'large',
+ HUGE = 'huge',
+}
+
+export type Col = {
+ sizes: TemplateFieldSize[];
+ desc: string;
+ title: string;
+ type: TemplateFieldType;
+ defaultContent?: string;
+};
+export interface FieldOpts {
+ backgroundColor?: string;
+ color?: string;
+ cornerRounding?: number;
+ borderWidth?: string;
+ borderColor?: string;
+ contentXCentering?: 'h-left' | 'h-center' | 'h-right';
+ contentYCentering?: 'top' | 'center' | 'bottom';
+ opacity?: number;
+ rotation?: number;
+ //animation?: boolean;
+ fontBold?: boolean;
+ fontTransform?: 'uppercase' | 'lowercase';
+ fieldViewType?: 'freeform' | 'stacked';
+}
+
+type Field = {
+ tl: [number, number];
+ br: [number, number];
+ opts: FieldOpts;
+ subfields?: Field[];
+ types?: TemplateFieldType[];
+ sizes?: TemplateFieldSize[];
+ isDecoration?: boolean;
+ description?: string;
+};
+
+// class ContentField implements Field {
+// tl: [number, number];
+// br: [number, number];
+// opts: FieldOpts;
+// subfields?: Field[];
+// types?: TemplateFieldType[];
+// sizes?: TemplateFieldSize[];
+// description?: string;
+
+// constructor( tl: [number, number], br: [number, number],
+// opts: FieldOpts, subfields?: Field[],
+// types?: TemplateFieldType[],
+// sizes?: TemplateFieldSize[],
+// description?: string) {
+// this.tl = tl;
+// this.br = br;
+// this.opts = opts;
+// this.subfields = subfields;
+// this.types = types;
+// this.sizes = sizes;
+// this.description = description;
+// }
+
+// render = (content: any): Doc => {
+// return new Doc;
+// }
+// }
+
+type DecorationField = Field;
+
+type InkDecoration = {};
+
+type TemplateDecorations = Field | InkDecoration;
+
+interface TemplateOpts extends FieldOpts {}
+
+export interface TemplateDocInfos {
+ title: string;
+ height: number;
+ width: number;
+ opts: TemplateOpts;
+ fields: Field[];
+ decorations: Field[];
+}
+
+export class TemplateLayouts {
+ public static get allTemplates(): TemplateDocInfos[] {
+ return Object.values(TemplateLayouts).filter(value => typeof value === 'object' && value !== null && 'title' in value) as TemplateDocInfos[];
+ }
+
+ public static getTemplateByTitle = (title: string): TemplateDocInfos | undefined => {
+ switch (title) {
+ case 'fourfield1':
+ return TemplateLayouts.FourField001;
+ case 'fourfield2':
+ return TemplateLayouts.FourField002;
+ // case 'fourfield3':
+ // return TemplateLayouts.FourField003;
+ case 'fourfield4':
+ return TemplateLayouts.FourField004;
+ case 'threefield1':
+ return TemplateLayouts.ThreeField001;
+ case 'threefield2':
+ return TemplateLayouts.ThreeField002;
+ default:
+ break;
+ }
+
+ return undefined;
+ };
+
+ public static FourField001: TemplateDocInfos = {
+ title: 'fourfield1',
+ width: 416,
+ height: 700,
+ opts: {
+ backgroundColor: '#C0B887',
+ cornerRounding: 20,
+ borderColor: '#6B461F',
+ borderWidth: '12',
+ },
+ fields: [
+ {
+ tl: [-0.95, -1],
+ br: [0.95, -0.85],
+ types: [TemplateFieldType.TEXT],
+ sizes: [TemplateFieldSize.TINY],
+ description: 'A title field for very short text that contextualizes the content.',
+ opts: {
+ backgroundColor: 'transparent',
+ color: '#F1F0E9',
+ contentXCentering: 'h-center',
+ fontBold: true,
+ },
+ },
+ {
+ tl: [-0.87, -0.83],
+ br: [0.87, 0.2],
+ types: [TemplateFieldType.TEXT, TemplateFieldType.VISUAL],
+ sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE],
+ description: 'The main focus of the template; could be an image, long text, etc.',
+ opts: {
+ cornerRounding: 20,
+ borderColor: '#8F5B25',
+ borderWidth: '6',
+ backgroundColor: '#CECAB9',
+ },
+ },
+ {
+ tl: [-0.8, 0.2],
+ br: [0.8, 0.3],
+ types: [TemplateFieldType.TEXT],
+ sizes: [TemplateFieldSize.TINY, TemplateFieldSize.SMALL],
+ description: 'A caption for field #2, very short to short text that contextualizes the content of field #2',
+ opts: {
+ backgroundColor: 'transparent',
+ contentXCentering: 'h-center',
+ color: '#F1F0E9',
+ },
+ },
+ {
+ tl: [-0.87, 0.37],
+ br: [0.87, 0.96],
+ types: [TemplateFieldType.TEXT, TemplateFieldType.VISUAL],
+ sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE],
+ description: 'A medium-sized field for medium/long text.',
+ opts: {
+ cornerRounding: 15,
+ borderColor: '#8F5B25',
+ borderWidth: '6',
+ backgroundColor: '#CECAB9',
+ },
+ },
+ ],
+ decorations: [],
+ };
+
+ public static FourField002: TemplateDocInfos = {
+ title: 'fourfield2',
+ width: 425,
+ height: 778,
+ opts: {
+ backgroundColor: '#242425',
+ },
+ fields: [
+ {
+ tl: [-0.83, -0.95],
+ br: [0.83, -0.2],
+ types: [TemplateFieldType.VISUAL, TemplateFieldType.TEXT],
+ sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE],
+ description: 'A medium to large-sized field suitable for an image or longer text that should be the main focus.',
+ opts: {
+ borderWidth: '8',
+ borderColor: '#F8E71C',
+ },
+ },
+ {
+ tl: [-0.65, -0.2],
+ br: [0.65, -0.02],
+ types: [TemplateFieldType.TEXT],
+ sizes: [TemplateFieldSize.TINY],
+ description: 'A tiny field for just a word or two of plain text.',
+ opts: {
+ backgroundColor: 'transparent',
+ color: 'white',
+ contentXCentering: 'h-center',
+ fontTransform: 'uppercase',
+ },
+ },
+ {
+ tl: [-0.65, 0],
+ br: [0.65, 0.18],
+ types: [TemplateFieldType.TEXT],
+ sizes: [TemplateFieldSize.TINY],
+ description: 'A tiny field for just a word or two of plain text.',
+ opts: {
+ backgroundColor: 'transparent',
+ color: 'white',
+ contentXCentering: 'h-center',
+ fontTransform: 'uppercase',
+ },
+ },
+ {
+ tl: [-0.83, 0.2],
+ br: [0.83, 0.95],
+ types: [TemplateFieldType.TEXT, TemplateFieldType.VISUAL],
+ sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE],
+ description: 'A medium to large-sized field suitable for an image or longer text that should be the main focus, or share focus with field 1.',
+ opts: {
+ borderWidth: '8',
+ borderColor: '#F8E71C',
+ color: 'white',
+ backgroundColor: '#242425',
+ },
+ },
+ ],
+ decorations: [
+ {
+ tl: [-0.8, -0.075],
+ br: [-0.525, 0.075],
+ opts: {
+ backgroundColor: '#F8E71C',
+ rotation: 45,
+ },
+ },
+ {
+ tl: [-0.3075, -0.0245],
+ br: [-0.2175, 0.0245],
+ opts: {
+ backgroundColor: '#F8E71C',
+ rotation: 45,
+ },
+ },
+ {
+ tl: [-0.045, -0.0245],
+ br: [0.045, 0.0245],
+ opts: {
+ backgroundColor: '#F8E71C',
+ rotation: 45,
+ },
+ },
+ {
+ tl: [0.2175, -0.0245],
+ br: [0.3075, 0.0245],
+ opts: {
+ backgroundColor: '#F8E71C',
+ rotation: 45,
+ },
+ },
+ {
+ tl: [0.525, -0.075],
+ br: [0.8, 0.075],
+ opts: {
+ backgroundColor: '#F8E71C',
+ rotation: 45,
+ },
+ },
+ ],
+ };
+
+ // public static FourField003: TemplateDocInfos = {
+ // title: 'fourfield3',
+ // width: 477,
+ // height: 662,
+ // opts: {
+ // backgroundColor: '#9E9C95'
+ // },
+ // fields: [{
+ // tl: [-.875, -.9],
+ // br: [.875, .7],
+ // types: [TemplateFieldType.VISUAL],
+ // sizes: [TemplateFieldSize.LARGE, TemplateFieldSize.HUGE],
+ // description: '',
+ // opts: {
+ // borderWidth: '15',
+ // borderColor: '#E0E0DA',
+ // }
+ // }, {
+ // tl: [-.95, .8],
+ // br: [-.1, .95],
+ // types: [TemplateFieldType.TEXT],
+ // sizes: [TemplateFieldSize.TINY, TemplateFieldSize.SMALL],
+ // description: '',
+ // opts: {
+ // backgroundColor: 'transparent',
+ // color: 'white',
+ // contentXCentering: 'h-right',
+ // }
+ // }, {
+ // tl: [.1, .8],
+ // br: [.95, .95],
+ // types: [TemplateFieldType.TEXT],
+ // sizes: [TemplateFieldSize.TINY, TemplateFieldSize.SMALL],
+ // description: '',
+ // opts: {
+ // backgroundColor: 'transparent',
+ // color: 'red',
+ // fontTransform: 'uppercase',
+ // contentXCentering: 'h-left'
+ // }
+ // }, {
+ // tl: [0, -.9],
+ // br: [.85, -.66],
+ // types: [TemplateFieldType.TEXT, TemplateFieldType.VISUAL],
+ // sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE],
+ // description: '',
+ // opts: {
+ // backgroundColor: 'transparent',
+ // contentXCentering: 'h-right'
+ // }
+ // }],
+ // decorations: [{
+ // tl: [-.025, .8],
+ // br: [.025, .95],
+ // opts: {
+ // backgroundColor: '#E0E0DA',
+ // }
+ // }]
+ // };
+
+ public static FourField004: TemplateDocInfos = {
+ title: 'fourfield4',
+ width: 414,
+ height: 583,
+ opts: {
+ backgroundColor: '#6CCAF0',
+ borderColor: '#1088C3',
+ borderWidth: '10',
+ },
+ fields: [
+ {
+ tl: [-0.86, -0.92],
+ br: [-0.075, -0.77],
+ types: [TemplateFieldType.TEXT],
+ sizes: [TemplateFieldSize.TINY],
+ description: 'A tiny field for just a word or two of plain text.',
+ opts: {
+ backgroundColor: '#E2B4F5',
+ borderWidth: '9',
+ borderColor: '#9222F1',
+ contentXCentering: 'h-center',
+ },
+ },
+ {
+ tl: [0.075, -0.92],
+ br: [0.86, -0.77],
+ types: [TemplateFieldType.TEXT],
+ sizes: [TemplateFieldSize.TINY],
+ description: 'A tiny field for just a word or two of plain text.',
+ opts: {
+ backgroundColor: '#F5B4DD',
+ borderWidth: '9',
+ borderColor: '#E260F3',
+ contentXCentering: 'h-center',
+ },
+ },
+ {
+ tl: [-0.81, -0.64],
+ br: [0.81, 0.48],
+ types: [TemplateFieldType.VISUAL],
+ sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE],
+ description: 'A large to huge field for visual content that is the main content of the template.',
+ opts: {
+ borderWidth: '16',
+ borderColor: '#A2BD77',
+ },
+ },
+ {
+ tl: [-0.86, 0.6],
+ br: [0.86, 0.92],
+ types: [TemplateFieldType.TEXT],
+ sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE],
+ description: 'A medium to large field for text that describes the visual content above',
+ opts: {
+ borderWidth: '9',
+ borderColor: '#F0D601',
+ backgroundColor: '#F3F57D',
+ },
+ },
+ ],
+ decorations: [
+ {
+ tl: [-0.852, -0.67],
+ br: [0.852, 0.51],
+ opts: {
+ backgroundColor: 'transparent',
+ borderColor: '#007C0C',
+ borderWidth: '10',
+ },
+ },
+ ],
+ };
+
+ public static ThreeField001: TemplateDocInfos = {
+ title: 'threefield1',
+ width: 575,
+ height: 770,
+ opts: {
+ backgroundColor: '#DDD3A9',
+ },
+ fields: [
+ {
+ tl: [-0.66, -0.747],
+ br: [0.66, 0.247],
+ types: [TemplateFieldType.VISUAL],
+ sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE],
+ description: 'A medium to large field for visual content that is the central focus.',
+ opts: {
+ borderColor: 'yellow',
+ borderWidth: '8',
+ rotation: 45,
+ },
+ },
+ {
+ tl: [-0.7, 0.2],
+ br: [0.7, 0.46],
+ types: [TemplateFieldType.TEXT],
+ sizes: [TemplateFieldSize.TINY, TemplateFieldSize.SMALL],
+ description: 'A very small text field for one to a few words. A good caption for the image.',
+ opts: {
+ backgroundColor: 'transparent',
+ contentXCentering: 'h-center',
+ },
+ },
+ {
+ tl: [-0.95, 0.5],
+ br: [0.95, 0.95],
+ types: [TemplateFieldType.TEXT],
+ sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE],
+ description: 'A medium to large text field for a thorough description of the image. ',
+ opts: {
+ backgroundColor: 'transparent',
+ color: 'white',
+ },
+ },
+ ],
+ decorations: [
+ {
+ tl: [0.2, -1.32],
+ br: [1.8, -0.66],
+ opts: {
+ backgroundColor: '#CEB155',
+ rotation: 45,
+ },
+ },
+ {
+ tl: [-1.8, -1.32],
+ br: [-0.2, -0.66],
+ opts: {
+ backgroundColor: '#CEB155',
+ rotation: 135,
+ },
+ },
+ {
+ tl: [0.33, 0.75],
+ br: [1.66, 1.25],
+ opts: {
+ backgroundColor: '#CEB155',
+ rotation: 135,
+ },
+ },
+ {
+ tl: [-1.66, 0.75],
+ br: [-0.33, 1.25],
+ opts: {
+ backgroundColor: '#CEB155',
+ rotation: 45,
+ },
+ },
+ ],
+ };
+
+ public static ThreeField002: TemplateDocInfos = {
+ title: 'threefield2',
+ width: 477,
+ height: 662,
+ opts: {
+ backgroundColor: '#9E9C95',
+ },
+ fields: [
+ {
+ tl: [-0.875, -0.9],
+ br: [0.875, 0.7],
+ types: [TemplateFieldType.VISUAL],
+ sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE],
+ description: 'A medium to large visual field for the main content of the template',
+ opts: {
+ borderWidth: '15',
+ borderColor: '#E0E0DA',
+ },
+ },
+ {
+ tl: [0.1, 0.775],
+ br: [0.95, 0.975],
+ types: [TemplateFieldType.TEXT],
+ sizes: [TemplateFieldSize.TINY, TemplateFieldSize.SMALL],
+ description: 'A very small text field for one to a few words. The content should represent a general categorization of the image.',
+ opts: {
+ backgroundColor: 'transparent',
+ color: '#AF0D0D',
+ fontTransform: 'uppercase',
+ fontBold: true,
+ contentXCentering: 'h-left',
+ },
+ },
+ {
+ tl: [-0.95, 0.775],
+ br: [-0.1, 0.975],
+ types: [TemplateFieldType.TEXT],
+ sizes: [TemplateFieldSize.TINY, TemplateFieldSize.SMALL],
+ description: 'A very small text field for one to a few words. The content should contextualize field 2.',
+ opts: {
+ backgroundColor: 'transparent',
+ color: 'black',
+ contentXCentering: 'h-right',
+ },
+ },
+ ],
+ decorations: [
+ {
+ tl: [-0.025, 0.8],
+ br: [0.025, 0.95],
+ opts: {
+ backgroundColor: '#E0E0DA',
+ },
+ },
+ ],
+ };
+}
+
+export class FieldUtils {
+ public static contentFields = (fields: Field[]) => {
+ let toRet: Field[] = [];
+ fields.forEach(field => {
+ if (!field.isDecoration) {
+ toRet.push(field);
+ }
+ toRet = toRet.concat(FieldUtils.contentFields(field.subfields ?? []));
+ });
+
+ return toRet;
+ };
+
+ public static calculateFontSize = (contWidth: number, contHeight: number, text: string, uppercase: boolean): number => {
+ const words: string[] = text.split(/\s+/).filter(Boolean);
+
+ let currFontSize = 1;
+ let rowsCount = 1;
+ let currTextHeight = currFontSize * rowsCount * 2;
+
+ while (currTextHeight <= contHeight) {
+ let wordIndex = 0;
+ let currentRowWidth = 0;
+ let wordsInCurrRow = 0;
+ rowsCount = 1;
+
+ while (wordIndex < words.length) {
+ const word = words[wordIndex];
+ const wordWidth = word.length * currFontSize * 0.5;
+ //console.log(wordWidth)
+
+ if (currentRowWidth + wordWidth <= contWidth) {
+ currentRowWidth += wordWidth;
+ ++wordsInCurrRow;
+ } else {
+ if (words.length !== 1 && words.length > wordsInCurrRow) {
+ rowsCount++;
+ currentRowWidth = wordWidth;
+ wordsInCurrRow = 1;
+ } else {
+ break;
+ }
+ }
+
+ wordIndex++;
+ }
+
+ currTextHeight = rowsCount * currFontSize * 2;
+ //console.log(rowsCount, currFontSize, currTextHeight)
+
+ currFontSize += 1;
+ }
+
+ return currFontSize - 1;
+ };
+
+ private static getDimensions = (coords: { tl: [number, number]; br: [number, number] }, parentWidth: number, parentHeight: number): { width: number; height: number; coord: { x: number; y: number } } => {
+ const l = (coords.tl[0] * parentHeight) / 2;
+ const t = coords.tl[1] * parentWidth / 2; //prettier-ignore
+ const r = (coords.br[0] * parentHeight) / 2;
+ const b = coords.br[1] * parentWidth / 2; //prettier-ignore
+ const width = r - l;
+ const height = b - t;
+ const coord = { x: l, y: t };
+ //console.log(coords, parentWidth, parentHeight, height);
+ return { width, height, coord };
+ };
+
+ public static FreeformField = (coords: { tl: [number, number]; br: [number, number] }, parentWidth: number, parentHeight: number, title: string, content: string, opts: FieldOpts) => {
+ const { width, height, coord } = FieldUtils.getDimensions(coords, parentWidth, parentHeight);
+
+ const docWithBasicOpts = Docs.Create.FreeformDocument([], {
+ isDefaultTemplateDoc: true,
+ _height: height,
+ _width: width,
+ title: title,
+ x: coord.x,
+ y: coord.y,
+ backgroundColor: opts.backgroundColor ?? '',
+ _layout_borderRounding: `${opts.cornerRounding ?? 0}px`,
+ borderColor: opts.borderColor,
+ borderWidth: opts.borderWidth,
+ opacity: opts.opacity,
+ hCentering: opts.contentXCentering,
+ _rotation: opts.rotation,
+ });
+
+ return docWithBasicOpts;
+ };
+
+ public static TextField = (coords: { tl: [number, number]; br: [number, number] }, parentWidth: number, parentHeight: number, title: string, content: string, opts: FieldOpts) => {
+ const { width, height, coord } = FieldUtils.getDimensions(coords, parentWidth, parentHeight);
+
+ const docWithBasicOpts = Docs.Create.TextDocument(content, {
+ isDefaultTemplateDoc: true,
+ _height: height,
+ _width: width,
+ title: title,
+ x: coord.x,
+ y: coord.y,
+ _text_fontSize: `${FieldUtils.calculateFontSize(width, height, content, true)}`,
+ backgroundColor: opts.backgroundColor ?? '',
+ text_fontColor: opts.color,
+ contentBold: opts.fontBold,
+ textTransform: opts.fontTransform,
+ color: opts.color,
+ _layout_borderRounding: `${opts.cornerRounding ?? 0}px`,
+ borderColor: opts.borderColor,
+ borderWidth: opts.borderWidth,
+ opacity: opts.opacity,
+ hCentering: opts.contentXCentering,
+ _rotation: opts.rotation,
+ });
+
+ docWithBasicOpts._layout_hideScroll = true;
+
+ return docWithBasicOpts;
+ };
+
+ public static ImageField = (coords: { tl: [number, number]; br: [number, number] }, parentWidth: number, parentHeight: number, title: string, content: string, opts: FieldOpts) => {
+ const { width, height, coord } = FieldUtils.getDimensions(coords, parentWidth, parentHeight);
+
+ const doc = Docs.Create.ImageDocument(content, {
+ isDefaultTemplateDoc: true,
+ _height: height,
+ _width: width,
+ title: title,
+ x: coord.x,
+ y: coord.y,
+ _layout_fitWidth: false,
+ backgroundColor: opts.backgroundColor ?? '',
+ _layout_borderRounding: `${opts.cornerRounding ?? 0}px`,
+ borderColor: opts.borderColor,
+ borderWidth: opts.borderWidth,
+ opacity: opts.opacity,
+ _rotation: opts.rotation,
+ });
+
+ //setTimeout(() => {doc._height = height; doc._width = width}, 10);
+
+ return doc;
+ };
+
+ public static CarouselField = (coords: { tl: [number, number]; br: [number, number] }, parentWidth: number, parentHeight: number, title: string, fields: Doc[]) => {
+ const { width, height, coord } = FieldUtils.getDimensions(coords, parentWidth, parentHeight);
+
+ const doc = Docs.Create.Carousel3DDocument(fields, { _height: height, _width: width, title: title, x: coord.x, y: coord.y, _text_fontSize: `${height / 2}` });
+
+ return doc;
+ };
+}
+
+// public static FourField002: TemplateDocInfos = {
+// width: 450,
+// height: 600,
+// fields: [{
+// tl: [-.6, -.9],
+// br: [.6, -.8],
+// types: [FieldType.TEXT],
+// sizes: [FieldSize.TINY]
+// }, {
+// tl: [-.9, -.7],
+// br: [.9, .2],
+// types: [FieldType.TEXT, FieldType.VISUAL],
+// sizes: [FieldSize.MEDIUM, FieldSize.LARGE, FieldSize.HUGE]
+// }, {
+// tl: [-.9, .3],
+// br: [-.05, .9],
+// types: [FieldType.TEXT],
+// sizes: [FieldSize.TINY]
+// }, {
+// tl: [.05, .3],
+// br: [.9, .9],
+// types: [FieldType.TEXT, FieldType.VISUAL],
+// sizes: [FieldSize.MEDIUM, FieldSize.LARGE, FieldSize.HUGE]
+// }]
+// };
+
+// public static TwoFieldPlusCarousel: TemplateDocInfos = {
+// width: 500,
+// height: 600,
+// fields: [{
+// tl: [-.9, -.99],
+// br: [.9, -.7],
+// types: [FieldType.TEXT],
+// sizes: [FieldSize.TINY]
+// }, {
+// tl: [-.9, -.65],
+// br: [.9, .35],
+// types: [],
+// sizes: []
+// }, {
+// tl: [-.9, .4],
+// br: [.9, .95],
+// types: [FieldType.TEXT],
+// sizes: [FieldSize.TINY]
+// }]
+// };
+// }
+
diff --git a/src/client/views/nodes/DataVizBox/TemplateDocTypes.tsx b/src/client/views/nodes/DataVizBox/TemplateDocTypes.tsx
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/TemplateDocTypes.tsx
diff --git a/src/client/views/nodes/DataVizBox/components/TableBox.tsx b/src/client/views/nodes/DataVizBox/components/TableBox.tsx
index e57c9e842..fe596bc36 100644
--- a/src/client/views/nodes/DataVizBox/components/TableBox.tsx
+++ b/src/client/views/nodes/DataVizBox/components/TableBox.tsx
@@ -1,4 +1,4 @@
-import { Button, Type } from 'browndash-components';
+import { Button, Colors, Type } from 'browndash-components';
import { IReactionDisposer, action, computed, makeObservable, observable, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
@@ -16,6 +16,7 @@ import { DocumentView } from '../../DocumentView';
import { DataVizView } from '../DataVizBox';
import './Chart.scss';
+// eslint-disable-next-line @typescript-eslint/no-require-imports
const { DATA_VIZ_TABLE_ROW_HEIGHT } = require('../../../global/globalCssVariables.module.scss'); // prettier-ignore
interface TableBoxProps {
@@ -35,6 +36,7 @@ interface TableBoxProps {
left: number;
};
docView?: () => DocumentView | undefined;
+ specHighlightedRow: number | undefined;
}
@observer
@@ -178,7 +180,6 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> {
} else {
const newAxes = this._props.axes;
if (newAxes.includes(col)) newAxes.splice(newAxes.indexOf(col), 1);
- else if (newAxes.length > 2) newAxes[newAxes.length - 1] = col;
else newAxes.push(col);
this._props.selectAxes(newAxes);
}
@@ -410,11 +411,13 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> {
color:
this._props.axes.slice().reverse().lastElement() === col
? 'darkgreen'
- : this._props.axes.length > 2 && this._props.axes.lastElement() === col
+ : this._props.axes.length > 3 && this._props.axes.lastElement() === col
? 'darkred'
- : this._props.axes.lastElement() === col || (this._props.axes.length > 2 && this._props.axes[1] === col)
+ : this._props.axes.length > 3 && this._props.axes[1] === col
? 'darkblue'
- : undefined,
+ : this._props.axes.lastElement() === col || (this._props.axes.length > 3 && this._props.axes[2] === col)
+ ? 'darkcyan'
+ : undefined,
background: this.settingTitle
? 'lightgrey'
: this._props.axes.slice().reverse().lastElement() === col
@@ -423,7 +426,9 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> {
? '#Fbdbdb'
: this._props.axes.lastElement() === col || (this._props.axes.length > 2 && this._props.axes[1] === col)
? '#c6ebf7'
- : undefined,
+ : this._props.axes.lastElement() === col || (this._props.axes.length > 3 && this._props.axes[2] === col)
+ ? '#c2f0f4'
+ : undefined,
fontWeight: 'bolder',
border: '3px solid black',
}}
@@ -442,7 +447,15 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> {
className={`tableBox-row ${this.columns[0]}`}
onClick={e => this.tableRowClick(e, rowId)}
style={{
- background: NumListCast(this._props.layoutDoc.dataViz_highlitedRows).includes(rowId) ? 'lightYellow' : NumListCast(this._props.layoutDoc.dataViz_selectedRows).includes(rowId) ? 'lightgrey' : '',
+ background:
+ rowId === this._props.specHighlightedRow
+ ? 'lightblue'
+ : NumListCast(this._props.layoutDoc.dataViz_highlitedRows).includes(rowId)
+ ? 'lightYellow'
+ : NumListCast(this._props.layoutDoc.dataViz_selectedRows).includes(rowId)
+ ? 'lightgrey'
+ : '',
+ border: rowId === this._props.specHighlightedRow ? `solid 3px ${Colors.MEDIUM_BLUE}` : '',
}}>
{this.columns.map(col => {
let colSelected = false;
diff --git a/src/client/views/nodes/DocumentIcon.tsx b/src/client/views/nodes/DocumentIcon.tsx
index ffd350e92..9769ecb3d 100644
--- a/src/client/views/nodes/DocumentIcon.tsx
+++ b/src/client/views/nodes/DocumentIcon.tsx
@@ -1,5 +1,5 @@
import { Tooltip } from '@mui/material';
-import { action, makeObservable, observable } from 'mobx';
+import { makeObservable, observable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { factory } from 'typescript';
@@ -18,26 +18,23 @@ interface DocumentIconProps {
@observer
export class DocumentIcon extends ObservableReactComponent<DocumentIconProps> {
@observable _hovered = false;
- constructor(props: any) {
+ constructor(props: DocumentIconProps) {
super(props);
makeObservable(this);
}
render() {
const { view } = this._props;
- const { left, top, right } = view.getBounds || { left: 0, top: 0, right: 0, bottom: 0 };
+ const { left, top, bottom } = view.getBounds || { left: 0, top: 0, right: 0, bottom: 0 };
return (
<div
className="documentIcon-outerDiv"
- onPointerEnter={action(() => { this._hovered = true; })} // prettier-ignore
- onPointerLeave={action(() => { this._hovered = false; })} // prettier-ignore
style={{
pointerEvents: 'all',
- opacity: this._hovered ? 0.3 : 1,
position: 'absolute',
background: SnappingManager.userBackgroundColor,
- transform: `translate(${(left + right) / 2}px, ${top}px)`,
+ transform: `translate(${left}px, ${bottom - (bottom - top) / 2}px)`, //**!**
}}>
<Tooltip title={<div>{StrCast(this._props.view.Document?.title)}</div>}>
<p>d{this._props.index}</p>
diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss
index 23dada260..7568e3b57 100644
--- a/src/client/views/nodes/DocumentView.scss
+++ b/src/client/views/nodes/DocumentView.scss
@@ -240,6 +240,12 @@
}
}
+.contentFittingDocumentView * {
+ ::-webkit-scrollbar-track {
+ background: none;
+ }
+}
+
.contentFittingDocumentView {
position: relative;
display: flex;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 81b5f946a..428fe5acb 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1075,6 +1075,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
public static getViews = (doc?: Doc) => Array.from(doc?.[DocViews] ?? []) as DocumentView[];
public static getFirstDocumentView: (toFind: Doc) => DocumentView | undefined;
public static getDocumentView: (target: Doc | undefined, preferredCollection?: DocumentView) => Opt<DocumentView>;
+ public static getDocViewIndex: (target: Doc) => number;
public static getContextPath: (doc: Opt<Doc>, includeExistingViews?: boolean) => Doc[];
public static getLightboxDocumentView: (toFind: Doc) => Opt<DocumentView>;
public static showDocumentView: (targetDocView: DocumentView, options: FocusViewOptions) => Promise<void>;
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index f6eb4009c..741d63909 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -138,7 +138,7 @@ export class FieldView extends React.Component<FieldViewProps> {
const field = this.fieldval;
// prettier-ignore
if (field instanceof Doc) return <p> <b>{field.title?.toString()}</b></p>;
- if (field === undefined) return <p>{'<null>'}</p>;
+ if (field === undefined) return <p />;
if (field instanceof DateField) return <p>{field.date.toLocaleString()}</p>;
if (field instanceof List) return <div> {field.map(f => Field.toString(f)).join(', ')} </div>;
if (field instanceof WebField) return <p>{Field.toString(field.url.href)}</p>;
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index ec5e062c8..c156c80e4 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -306,6 +306,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
@computed get nativeSize() {
TraceMobx();
+ if (this.paths.length && this.paths[0].includes('icon-hi')) return { nativeWidth: NumCast(this.layoutDoc._width), nativeHeight: NumCast(this.layoutDoc._height), nativeOrientation: 0}
const nativeWidth = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth'], NumCast(this.layoutDoc[this.fieldKey + '_nativeWidth'], 500));
const nativeHeight = NumCast(this.dataDoc[this.fieldKey + '_nativeHeight'], NumCast(this.layoutDoc[this.fieldKey + '_nativeHeight'], 500));
const nativeOrientation = NumCast(this.dataDoc[this.fieldKey + '_nativeOrientation'], 1);
@@ -352,7 +353,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
}
@computed get paths() {
- const field = Cast(this.dataDoc[this.fieldKey], ImageField, null); // retrieve the primary image URL that is being rendered from the data doc
+ const field = this.dataDoc[this.fieldKey] instanceof ImageField ? Cast(this.dataDoc[this.fieldKey], ImageField, null) : new ImageField(String(this.dataDoc[this.fieldKey])); // retrieve the primary image URL that is being rendered from the data doc
const alts = DocListCast(this.dataDoc[this.fieldKey + '_alternates']); // retrieve alternate documents that may be rendered as alternate images
const defaultUrl = new URL(ClientUtils.prepend('/assets/unknown-file-icon-hi.png'));
const altpaths =
@@ -405,7 +406,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
alt=""
key="paths"
src={srcpath}
- style={{ transform, transformOrigin }}
+ style={{ transform, transformOrigin, objectFit: 'fill', height: '100%' }}
onError={action(e => {
this._error = e.toString();
})}
@@ -486,7 +487,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
})}
style={{
width: this._props.PanelWidth() ? undefined : `100%`,
- height: this._props.PanelWidth() ? undefined : `100%`,
+ height: this._props.PanelHeight() ? undefined : `100%`,
pointerEvents: this.layoutDoc._lockedPosition ? 'none' : undefined,
borderRadius,
overflow: this.layoutDoc.layout_fitWidth || this._props.fitWidth?.(this.Document) ? 'auto' : undefined,
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss
index 99b4a84fc..72d550c7e 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.scss
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss
@@ -14,6 +14,33 @@
}
}
+.formattedTextBox-inner {
+ &.h-center * {
+ display: flex;
+ justify-content: center;
+ }
+
+ &.h-left * {
+ display: flex;
+ justify-content: flex-start;
+ }
+
+ &.h-right * {
+ display: flex;
+ justify-content: flex-end;
+ }
+
+ &.template * {
+ ::-webkit-scrollbar-track {
+ background: none;
+ }
+ }
+
+ &.bold * {
+ font-weight: bold;
+ }
+}
+
.ProseMirror:focus {
outline: none !important;
}
@@ -52,6 +79,7 @@ audiotag:hover {
transform-origin: left top;
top: 0;
left: 0;
+
}
.formattedTextBox-cont {
@@ -1035,3 +1063,4 @@ footnote::before {
}
}
}
+
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 70e25d119..0d7914a82 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -64,6 +64,7 @@ import { removeMarkWithAttrs } from './prosemirrorPatches';
import { RichTextMenu, RichTextMenuPlugin } from './RichTextMenu';
import { RichTextRules } from './RichTextRules';
import { schema } from './schema_rts';
+import { Property } from 'csstype';
// import * as applyDevTools from 'prosemirror-dev-tools';
export interface FormattedTextBoxProps extends FieldViewProps {
@@ -2064,7 +2065,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
onScroll={this.onScroll}
onDrop={this.ondrop}>
<div
- className={`formattedTextBox-inner${rounded} ${this.layoutDoc._layout_centered ? 'centered' : ''}`}
+ className={`formattedTextBox-inner${rounded} ${this.layoutDoc._layout_centered ? 'centered' : ''} ${this.layoutDoc.hCentering}`}
ref={this.createDropTarget}
style={{
padding: StrCast(this.layoutDoc._textBoxPadding),
@@ -2072,6 +2073,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
paddingRight: StrCast(this.layoutDoc._textBoxPaddingX, `${paddingX}px`),
paddingTop: StrCast(this.layoutDoc._textBoxPaddingY, `${paddingY}px`),
paddingBottom: StrCast(this.layoutDoc._textBoxPaddingY, `${paddingY}px`),
+ color: StrCast(this.layoutDoc.text_fontColor),
+ fontWeight: `${this.layoutDoc.contentBold ? 'bold' : ''}`,
+ textTransform: `${this.layoutDoc.textTransform}` as Property.TextTransform,
}}
/>
</div>
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 4d256e8f2..6f05ddf96 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -44,7 +44,7 @@ export namespace Field {
* @param showComputedValue whether copmuted function should display its value instead of its function
* @returns string representation of the field
*/
- export function toKeyValueString(doc: Doc, key: string, showComputedValue?: boolean): string {
+ export function toKeyValueString(doc: Doc, key: string, showComputedValue?: boolean, schemaCell?: boolean): string {
const isOnDelegate = !Doc.IsDataProto(doc) && Object.keys(doc).includes(key.replace(/^_/, ''));
const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key]));
const valFunc = (field: FieldType): string => {
@@ -55,7 +55,7 @@ export namespace Field {
? `:=${field.script.originalScript.replace(/dashCallChat\(_setCacheResult_, this, `(.*)`\)/, '(($1))')}`
: field instanceof ScriptField
? `$=${field.script.originalScript}`
- : Field.toScriptString(field);
+ : Field.toScriptString(field, schemaCell);
const resStr = (res + '').replace(/^`(.*)`$/, '$1');
return typeof field === 'string' && (+resStr).toString() !== resStr && !Array.from('+-*/.').some(k => Array.from(resStr).includes(k))
? resStr
@@ -65,10 +65,10 @@ export namespace Field {
};
return !Field.IsField(cfield) ? (key.startsWith('_') ? '=' : '') : (isOnDelegate ? '=' : '') + valFunc(cfield);
}
- export function toScriptString(field: FieldType) {
+ export function toScriptString(field: FieldType, schemaCell?: boolean) {
switch (typeof field) {
case 'string': if (field.startsWith('{"')) return `'${field}'`; // bcz: hack ... want to quote the string the right way. if there are nested "'s, then use ' instead of ". In this case, test for the start of a JSON string of the format {"property": ... } and use outer 's instead of "s
- return !field.includes('`') ? `\`${field}\`` : `"${field}"`;
+ return !field.includes('`') ? schemaCell ? `${field}` : `\`${field}\`` : `"${field}"`;
case 'number':
case 'boolean':return String(field);
default: return field?.[ToScriptString]?.() ?? 'null';
@@ -215,7 +215,7 @@ export class Doc extends RefField {
public static DeleteLink: (link: Doc) => void;
public static Links: (link: Doc | undefined) => Doc[];
public static getOppositeAnchor: (linkDoc: Doc, anchor: Doc) => Doc | undefined;
- // KeyValue SetField
+ // KeyValueBox SetField (defined there)
public static SetField: (doc: Doc, key: string, value: string, forceOnDelegate?: boolean, setResult?: (value: FieldResult) => void) => boolean;
// UserDoc "API"
public static get MySharedDocs() { return DocCast(Doc.UserDoc().mySharedDocs); } // prettier-ignore
@@ -1057,12 +1057,13 @@ export namespace Doc {
const target = Doc.MakeDelegate(proto);
const targetKey = StrCast(templateDoc.layout_fieldKey, 'layout');
const applied = ApplyTemplateTo(templateDoc, target, targetKey, templateDoc.title + '(...' + _applyCount++ + ')');
- target.layout_fieldKey = targetKey;
+ target.layout_fieldKey = targetKey; //this and line above
applied && (Doc.GetProto(applied).type = templateDoc.type);
return applied;
}
return undefined;
}
+
export function ApplyTemplateTo(templateDoc: Doc, target: Doc, targetKey: string, titleTarget: string | undefined) {
if (!Doc.AreProtosEqual(target[targetKey] as Doc, templateDoc)) {
if (target.resolvedDataDoc) {
diff --git a/src/fields/SchemaHeaderField.ts b/src/fields/SchemaHeaderField.ts
index 5f4d59cf9..902f5a856 100644
--- a/src/fields/SchemaHeaderField.ts
+++ b/src/fields/SchemaHeaderField.ts
@@ -12,6 +12,7 @@ export enum ColumnType {
Image,
RTF,
Enumeration,
+ Equation,
Any,
}