aboutsummaryrefslogtreecommitdiff
path: root/src/client/views
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views')
-rw-r--r--src/client/views/ContextMenu.scss50
-rw-r--r--src/client/views/ContextMenu.tsx2
-rw-r--r--src/client/views/ContextMenuItem.tsx2
-rw-r--r--src/client/views/EditableView.scss10
-rw-r--r--src/client/views/EditableView.tsx86
-rw-r--r--src/client/views/FieldsDropdown.tsx4
-rw-r--r--src/client/views/MainView.tsx15
-rw-r--r--src/client/views/PropertiesView.tsx10
-rw-r--r--src/client/views/ScriptingRepl.scss2
-rw-r--r--src/client/views/StyleProvider.tsx47
-rw-r--r--src/client/views/collections/collectionSchema/CollectionSchemaView.scss53
-rw-r--r--src/client/views/collections/collectionSchema/CollectionSchemaView.tsx970
-rw-r--r--src/client/views/collections/collectionSchema/SchemaCellField.tsx405
-rw-r--r--src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx258
-rw-r--r--src/client/views/collections/collectionSchema/SchemaRowBox.tsx133
-rw-r--r--src/client/views/collections/collectionSchema/SchemaTableCell.tsx119
-rw-r--r--src/client/views/global/globalScripts.ts2
-rw-r--r--src/client/views/nodes/DataVizBox/DataVizBox.tsx305
-rw-r--r--src/client/views/nodes/DataVizBox/DocCreatorMenu.scss1036
-rw-r--r--src/client/views/nodes/DataVizBox/DocCreatorMenu.tsx2335
-rw-r--r--src/client/views/nodes/DataVizBox/TemplateDocTypes.tsx0
-rw-r--r--src/client/views/nodes/DataVizBox/components/TableBox.tsx21
-rw-r--r--src/client/views/nodes/DocumentIcon.tsx9
-rw-r--r--src/client/views/nodes/DocumentView.scss6
-rw-r--r--src/client/views/nodes/DocumentView.tsx4
-rw-r--r--src/client/views/nodes/FieldView.tsx2
-rw-r--r--src/client/views/nodes/ImageBox.tsx25
-rw-r--r--src/client/views/nodes/LabelBox.tsx4
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.scss29
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx8
30 files changed, 5406 insertions, 546 deletions
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..98087e224 100644
--- a/src/client/views/ContextMenu.tsx
+++ b/src/client/views/ContextMenu.tsx
@@ -277,4 +277,4 @@ export class ContextMenu extends ObservableReactComponent<{ noexpand?: boolean }
this._selectedIndex = Math.min(this.flatItems.length - 1, this._selectedIndex);
}
};
-}
+} \ No newline at end of file
diff --git a/src/client/views/ContextMenuItem.tsx b/src/client/views/ContextMenuItem.tsx
index 5d31173e1..610032225 100644
--- a/src/client/views/ContextMenuItem.tsx
+++ b/src/client/views/ContextMenuItem.tsx
@@ -95,4 +95,4 @@ export class ContextMenuItem extends ObservableReactComponent<ContextMenuProps &
const submenu = this._items.map(prop => <ContextMenuItem {...prop} key={prop.description} closeMenu={this._props.closeMenu} />);
return this.props.event || this._props.noexpand ? this.renderItem(submenu) : <div className="contextMenu-inlineMenu">{submenu}</div>;
}
-}
+} \ No newline at end of file
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..9722b2d4b 100644
--- a/src/client/views/EditableView.tsx
+++ b/src/client/views/EditableView.tsx
@@ -7,6 +7,8 @@ import { DocumentIconContainer } from './nodes/DocumentIcon';
import { FieldView, FieldViewProps } from './nodes/FieldView';
import { ObservableReactComponent } from './ObservableReactComponent';
import { OverlayView } from './OverlayView';
+import { Padding } from 'browndash-components';
+import { SchemaFieldType } from './collections/collectionSchema/SchemaColumnHeader';
export interface EditableProps {
/**
@@ -51,6 +53,13 @@ export interface EditableProps {
background?: string | undefined;
placeholder?: string;
wrap?: string; // nowrap, pre-wrap, etc
+
+ schemaFieldType?: SchemaFieldType;
+ 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 {
@@ -187,7 +200,7 @@ export class EditableView extends ObservableReactComponent<EditableProps> {
}
}
};
-
+
@action
finalizeEdit(value: string, shiftDown: boolean, lostFocus: boolean, enterKey: boolean) {
if (this._props.SetValue(value, shiftDown, enterKey)) {
@@ -218,6 +231,12 @@ 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
@@ -235,11 +254,11 @@ export class EditableView extends ObservableReactComponent<EditableProps> {
onChange: this._props.autosuggestProps.onChange,
}}
/>
- ) : this._props.oneLine !== false && this._props.GetValue()?.toString().indexOf('\n') === -1 ? (
+ ) : ( this._props.oneLine !== false && this._props.GetValue()?.toString().indexOf('\n') === -1 ? (
<input
- className="editableView-input"
+ className="editableView-input"
ref={r => { this._inputref = r; }} // prettier-ignore
- style={{ display: this._props.display, overflow: 'auto', fontSize: this._props.fontSize, minWidth: 20, background: this._props.background }}
+ style={{ display: this._props.display, overflow: 'auto', fontSize: this._props.fontSize, minWidth: 20, background: this._props.background}}
placeholder={this._props.placeholder}
onBlur={e => this.finalizeEdit(e.currentTarget.value, false, true, false)}
defaultValue={this._props.GetValue()}
@@ -265,16 +284,42 @@ export class EditableView extends ObservableReactComponent<EditableProps> {
onClick={this.stopPropagation}
onPointerUp={this.stopPropagation}
/>
- );
+ ));
+ }
+
+ staticDisplay = () => {
+ let toDisplay;
+ const gval = this._props.GetValue()?.replace(/\n/g, '\\r\\n');
+ if (this._props.schemaFieldType === SchemaFieldType.Header){
+ toDisplay = <input className="editableView-input"
+ value={gval}
+ placeholder='Add key'
+ 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
+ }}>
+ {
+ // eslint-disable-next-line react/jsx-props-no-spreading
+ this._props.fieldContents ? <FieldView {...this._props.fieldContents} /> : this.props.contents ? this._props.contents?.valueOf() : '' as any
+ }
+ </span>)
+ }
+
+ return toDisplay;
}
render() {
const gval = this._props.GetValue()?.replace(/\n/g, '\\r\\n');
- if (this._editing && gval !== undefined) {
+ 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 +336,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/FieldsDropdown.tsx b/src/client/views/FieldsDropdown.tsx
index 407031b40..176ac96b6 100644
--- a/src/client/views/FieldsDropdown.tsx
+++ b/src/client/views/FieldsDropdown.tsx
@@ -34,7 +34,7 @@ export class FieldsDropdown extends ObservableReactComponent<fieldsDropdownProps
makeObservable(this);
}
- @computed get allDescendantDocs() {
+ @computed get allDescendantDocs() { //!!!
const allDocs = new Set<Doc>();
SearchUtil.foreachRecursiveDoc([this._props.Document], (depth, doc) => allDocs.add(doc));
return Array.from(allDocs);
@@ -57,7 +57,7 @@ export class FieldsDropdown extends ObservableReactComponent<fieldsDropdownProps
const filteredOptions = ['author', ...(this._newField ? [this._newField] : []), ...(this._props.addedFields ?? []), ...this.fieldsOfDocuments.filter(facet => facet[0] === facet.charAt(0).toUpperCase())];
Object.entries(DocOptions)
- .filter(opts => opts[1].filterable)
+ .filter(opts => opts[1].filterable) //!!!
.forEach((pair: [string, FInfo]) => filteredOptions.push(pair[0]));
const options = filteredOptions.sort().map(facet => ({ value: facet, label: facet }));
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 31d7e82a6..6feb6bd16 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -74,6 +74,7 @@ import { PresBox } from './nodes/trails';
import { AnchorMenu } from './pdf/AnchorMenu';
import { GPTPopup } from './pdf/GPTPopup/GPTPopup';
import { TopBar } from './topbar/TopBar';
+import { DocCreatorMenu } from './nodes/DataVizBox/DocCreatorMenu';
import { SmartDrawHandler } from './smartdraw/SmartDrawHandler';
import { InkTranscription } from './InkTranscription';
@@ -87,6 +88,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)
@@ -278,6 +280,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,
@@ -1087,6 +1101,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 d0c47875f..69c46052e 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -881,6 +881,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);
+ }
+ set borderColor(value) { this.selectedDoc && (this.selectedDoc[DocData].color = value || undefined); } // prettier-ignore
colorButton(value: string, type: string, setter: () => void) {
return (
@@ -932,6 +938,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/ScriptingRepl.scss b/src/client/views/ScriptingRepl.scss
index adc82238e..5fe176920 100644
--- a/src/client/views/ScriptingRepl.scss
+++ b/src/client/views/ScriptingRepl.scss
@@ -35,6 +35,8 @@
opacity: 0.3;
}
+
+
.scriptingObject-icon {
padding: 3px;
cursor: pointer;
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/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..0076caaf8 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
@@ -1,12 +1,12 @@
/* 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, Popup, PopupTrigger, Size, Type } from 'browndash-components';
+import { IReactionDisposer, ObservableMap, action, autorun, computed, makeObservable, observable, observe, override, reaction, runInAction } 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 { Doc, DocListCast, Field, FieldType, IdToDoc, NumListCast, Opt, StrListCast } from '../../../../fields/Doc';
import { DocData } from '../../../../fields/DocSymbols';
import { Id } from '../../../../fields/FieldSymbols';
import { List } from '../../../../fields/List';
@@ -16,7 +16,6 @@ import { DocUtils } from '../../../documents/DocUtils';
import { Docs, DocumentOptions, FInfo } from '../../../documents/Documents';
import { DragManager } from '../../../util/DragManager';
import { dropActionType } from '../../../util/DropActionTypes';
-import { SettingsManager } from '../../../util/SettingsManager';
import { undoBatch, undoable } from '../../../util/UndoManager';
import { ContextMenu } from '../../ContextMenu';
import { EditableView } from '../../EditableView';
@@ -31,6 +30,22 @@ import { CollectionSubView, SubCollectionViewProps } from '../CollectionSubView'
import './CollectionSchemaView.scss';
import { SchemaColumnHeader } from './SchemaColumnHeader';
import { SchemaRowBox } from './SchemaRowBox';
+import { ContextMenuProps } from '../../ContextMenuItem';
+import { DocumentManager } from '../../../util/DocumentManager';
+import { SchemaCellField } from './SchemaCellField';
+import { SnappingManager } from '../../../util/SnappingManager';
+
+/**
+ * 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.
+ */
const { SCHEMA_NEW_NODE_HEIGHT } = require('../../global/globalCssVariables.module.scss'); // prettier-ignore
@@ -48,16 +63,31 @@ const defaultColumnKeys: string[] = ['title', 'type', 'author', 'author_date', '
@observer
export class CollectionSchemaView extends CollectionSubView() {
- private _keysDisposer?: () => void;
+ private _keysDisposer: any;
+ 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}]> = [];
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): [any, any] => {return [{r: r, g: g, b: 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,15 +109,21 @@ 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() {
return this._menuTarget.current;
@@ -95,7 +131,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 +144,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 +171,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 +178,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 +216,33 @@ 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 +252,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 +271,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 +301,26 @@ 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?: any) => {
+ 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 +328,36 @@ 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, key?: string, defaultVal?: any) => {
+ 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: any) => {
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);
@@ -312,24 +365,29 @@ export class CollectionSchemaView extends CollectionSubView() {
const currKeys = this.columnKeys.slice();
currKeys.splice(index, 1);
- this.layoutDoc.schema_columnKeys = new List<string>(currKeys);
+ this.layoutDoc.schema_columnKeys = new List<string>(currKeys);
+
+ this._colEles.splice(index, 1);
};
@action
- startResize = (e: React.PointerEvent, index: number) => {
+ startResize = (e: any, 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 +425,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 +440,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 +454,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 +503,196 @@ 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 sorted = i === this.columnKeys.indexOf(this.sortField);
const cellEles = [
colRef,
- ...this.childDocs //
- .filter(doc => i !== this._selectedCol || !this._selectedDocs.includes(doc))
+ ...this.docsWithDrag.docs
+ .filter(doc => (i !== this._selectedCol || !this._selectedDocs.includes(doc)) && !sorted)
.map(doc => this._rowEles.get(doc).children[1].children[i]),
];
- cellEles[0].style.borderTop = edgeStyle;
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: Array<any> = [];
+ matches.forEach((match: Match) => {
+ const {docRef, field} = match;
+ 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 +713,48 @@ 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 +764,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 +778,24 @@ 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;
+ setTimeout(() => {this.setColDrag(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 = '';
- }
- });
- });
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 +848,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?: any, 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 +900,110 @@ 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 +1027,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 +1037,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 +1054,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 +1068,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,7 +1095,7 @@ export class CollectionSchemaView extends CollectionSubView() {
}
return (
<div key={key} className="schema-filter-option">
- <input //
+ <input
type="checkbox"
onPointerDown={e => e.stopPropagation()}
onClick={e => e.stopPropagation()}
@@ -955,7 +1111,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 +1126,177 @@ 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;
+ _oldWheel: any;
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 +1309,38 @@ 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 +1358,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;
@@ -1187,7 +1481,7 @@ class CollectionSchemaViewDoc extends ObservableReactComponent<CollectionSchemaV
interface CollectionSchemaViewDocsProps {
schema: CollectionSchemaView;
setRef: (ref: HTMLDivElement | null) => void;
- childDocs: () => { docs: Doc[] };
+ childDocs: () => Doc[];
rowHeight: () => number;
}
@@ -1196,7 +1490,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>
@@ -1204,4 +1498,4 @@ class CollectionSchemaViewDocs extends React.Component<CollectionSchemaViewDocsP
</div>
);
}
-}
+} \ No newline at end of file
diff --git a/src/client/views/collections/collectionSchema/SchemaCellField.tsx b/src/client/views/collections/collectionSchema/SchemaCellField.tsx
new file mode 100644
index 000000000..84e7b62bf
--- /dev/null
+++ b/src/client/views/collections/collectionSchema/SchemaCellField.tsx
@@ -0,0 +1,405 @@
+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 { 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: any;
+ fieldContents?: FieldViewProps;
+ editing?: boolean;
+ oneLine?: boolean;
+ Document: Doc;
+ fieldKey: string;
+ 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((match: string) => {
+ chunkedText = chunkedText.replace(match, this.generateSpan(match, cells.get(match)));
+ });
+
+ 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();
+ let cursorPos = 0;
+ if (this.cursorPosition !== null) cursorPos = this.cursorPosition + 1;
+ 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;
+ // eslint-disable-next-line no-fallthrough
+ 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..c5cdac8af 100644
--- a/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx
+++ b/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx
@@ -1,78 +1,248 @@
/* 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}
+ schemaFieldType={SchemaFieldType.Header}
+ 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;
+ @action handlePointerLeave = () => this._showMenuIcon = false;
+
+ @computed get displayButton() {return this._showMenuIcon;}
+
render() {
return (
- <div
- className="schema-column-header"
- style={{
- width: this.props.columnWidths[this.props.columnIndex],
- }}
- onPointerDown={this.onPointerDown}
- ref={col => {
- if (col) {
- this.props.setColRef(this.props.columnIndex, col);
+ <div
+ className="schema-column-header"
+ style={{
+ 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,
+ );
+ }
}
- }}>
- <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-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>
+ ref={col => {
+ if (col) {
+ this._props.setColRef(this._props.columnIndex, col);
+ }
+ }}>
+ <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" 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>
- </div>
);
}
-}
+} \ No newline at end of file
diff --git a/src/client/views/collections/collectionSchema/SchemaRowBox.tsx b/src/client/views/collections/collectionSchema/SchemaRowBox.tsx
index a7e0e916b..a8a4ef2c2 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,77 @@ 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: any, value: any) => 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(info => {cells.push(this.schemaView.getCellElement(info[0], info[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,11 +131,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" />}
+ tooltip="Open actions menu"
+ icon={ <FontAwesomeIcon icon="caret-right" size='lg'/>}
size={Size.XSMALL}
+ color={'black'}
onPointerDown={e =>
setupMoveUpEvents(
this,
@@ -89,50 +146,25 @@ export class SchemaRowBox extends ViewBoxBaseComponent<SchemaRowBoxProps>() {
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 />}
- size={Size.XSMALL}
- onPointerDown={e =>
- setupMoveUpEvents(
- this,
- e,
- returnFalse,
- 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 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 +178,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={() => {
@@ -161,4 +192,4 @@ export class SchemaRowBox extends ViewBoxBaseComponent<SchemaRowBoxProps>() {
</div>
);
}
-}
+} \ No newline at end of file
diff --git a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
index 22506cac1..c05382ce0 100644
--- a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
+++ b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
@@ -12,7 +12,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 +31,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 +55,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 +63,44 @@ 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.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 +110,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 +146,66 @@ 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 = (field: string) => {return ((field.startsWith('`') && field.endsWith('`')) || (field.startsWith("'") && field.endsWith("'")) || (field.startsWith('"') && field.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 +241,48 @@ 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>
);
}
@@ -441,4 +524,4 @@ export class SchemaEnumerationCell extends ObservableReactComponent<SchemaTableC
</div>
);
}
-}
+} \ No newline at end of file
diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts
index 423a2d6ef..962a21dbb 100644
--- a/src/client/views/global/globalScripts.ts
+++ b/src/client/views/global/globalScripts.ts
@@ -84,6 +84,8 @@ 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', '');
+ console.log('color: ' + dv.dataDoc[fieldKey + alternate] + ' to set to: ' + color)
+ 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 df6e74d85..12196f290 100644
--- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx
+++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
@@ -5,14 +5,14 @@ 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 { InkTool } from '../../../../fields/InkField';
import { List } from '../../../../fields/List';
import { Cast, CsvCast, DocCast, NumCast, StrCast } from '../../../../fields/Types';
import { CsvField } from '../../../../fields/URLField';
-import { TraceMobx } from '../../../../fields/util';
+import { GetEffectiveAcl, TraceMobx, inheritParentAcls } from '../../../../fields/util';
import { DocUtils } from '../../../documents/DocUtils';
import { DocumentType } from '../../../documents/DocumentTypes';
import { Docs } from '../../../documents/Documents';
@@ -32,6 +32,18 @@ import { Histogram } from './components/Histogram';
import { LineChart } from './components/LineChart';
import { PieChart } from './components/PieChart';
import { TableBox } from './components/TableBox';
+import { LinkManager } from '../../../util/LinkManager';
+import { Col, DataVizTemplateInfo, DataVizTemplateLayout, DocCreatorMenu, TemplateFieldSize, LayoutType, TemplateFieldType } from './DocCreatorMenu';
+import { CollectionFreeFormView, MarqueeView } from '../../collections/collectionFreeForm';
+import { PrefetchProxy } from '../../../../fields/Proxy';
+import { AclAdmin, AclAugment, AclEdit } from '../../../../fields/DocSymbols';
+import { template } from 'lodash';
+import { data } from 'jquery';
+import { listSpec } from '../../../../fields/Schema';
+import { ObjectField } from '../../../../fields/ObjectField';
+import { Id } from '../../../../fields/FieldSymbols';
+import { GPTCallType, gptAPICall } from '../../../apis/gpt/GPT';
+import { TbSortDescendingShapes } from 'react-icons/tb';
export enum DataVizView {
TABLE = 'table',
@@ -42,6 +54,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();
@@ -50,7 +63,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);
@@ -100,8 +117,17 @@ 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 (e){
+ this._urlError = true;
+ const data: { [key: string]: string; }[] = [
+ { error: "Data not found"},
+ ];
+ return data;
+ }
}
// currently chosen visualization type: line, pie, histogram, table
@@ -125,6 +151,61 @@ 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) => {
// const changedView = data.config_dataViz && this.dataVizView !== data.config_dataViz && (this.layoutDoc._dataViz = data.config_dataViz);
@@ -146,6 +227,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
@@ -272,7 +354,7 @@ 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;
@@ -333,6 +415,10 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
},
{ fireImmediately: true }
);
+ this._disposers.contentSummary = reaction(
+ () => this.records,
+ () => this.updateGPTSummary()
+ );
}
fetchData = () => {
@@ -359,7 +445,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;}}
@@ -426,11 +512,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 = (x: number, y: number): void => {
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(x, y), icon: 'table-cells' });
!options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'eye' });
};
@@ -445,6 +538,198 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
GPTPopup.Instance.generateDataAnalysis();
});
+ getColSummary = (): string => {
+ let 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, i) => {
+ prompt += `Row #${row}: `
+ cols.forEach(col => {
+ prompt += `${col}: ${this.records[row][col]} -----`
+ })
+ })
+
+ return prompt;
+ }
+
+ updateColDefaults = () => {
+ let 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
@@ -498,7 +783,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
transform: `scale(${scale})`,
position: 'absolute',
}}
- onContextMenu={this.specificContextMenu}
+ onContextMenu={(e) => this.specificContextMenu(e.pageX, e.pageY)}
onWheel={e => e.stopPropagation()}
ref={this._mainCont}>
<div className="datatype-button">
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu.scss b/src/client/views/nodes/DataVizBox/DocCreatorMenu.scss
new file mode 100644
index 000000000..9d82ada37
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu.scss
@@ -0,0 +1,1036 @@
+.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 {
+ position: relative;
+ width: 100%;
+ height: 100%;
+
+ .right-buttons-panel {
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+ height: 100%;
+ width: 40px;
+ position: absolute;
+ right: 0px;
+ top: 0px;
+ 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..43e9248a7
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu.tsx
@@ -0,0 +1,2335 @@
+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, returnFalse, setupMoveUpEvents } from '../../../../ClientUtils';
+import { emptyFunction } from '../../../../Utils';
+import { Doc, NumListCast, StrListCast } from '../../../../fields/Doc';
+import { Id } from '../../../../fields/FieldSymbols';
+import { 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';
+
+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 => docs.map(this.getIcon)
+ );
+ this._disposers.gpt = reaction(
+ () => this._suggestedTemplates.slice(),
+ docs => docs.map(this.getIcon)
+ );
+ //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[]) => {
+ 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 () => {
+ // const temp = TemplateLayouts.FourField001;
+ // const title: Doc = FieldFuncs.TextField({tl: temp.fields[0].tl, br: temp.fields[0].br}, temp.height, temp.width, 'title', 'Title', {backgroundColor: 'transparent'});
+ // const image: Doc = FieldFuncs.ImageField({tl: temp.fields[1].tl, br: temp.fields[1].br}, temp.height, temp.width, 'title', '', {borderColor: '#159fe4', borderWidth: '10', cornerRounding: 10, rotation: 40});
+ // const caption: Doc = FieldFuncs.TextField({tl: temp.fields[2].tl, br: temp.fields[2].br}, temp.height, temp.width, 'title', 'Caption', {backgroundColor: 'transparent'});
+ // const desc: Doc = FieldFuncs.TextField({tl: temp.fields[3].tl, br: temp.fields[3].br}, temp.height, temp.width, 'title', '', {backgroundColor: 'lightblue', borderColor: '#159fe4', borderWidth: '10', cornerRounding: 10});
+
+ // const doc = Docs.Create.FreeformDocument([title, image, caption, desc], { _height: temp.height, _width: temp.width, title: 'hey', x: 400, y: 400 });
+
+ // const mainCollection = this._dataViz?.DocumentView?.().containerViewPath?.().lastElement()?.ComponentView as CollectionFreeFormView;
+ // mainCollection.addDocument(doc);
+
+ // const temp = TemplateLayouts.FourField001;
+
+ // const img: Col = {type: TemplateFieldType.TEXT, title: 'Type', desc: 'description whpoo', size: TemplateFieldSize.LARGE, defaultContent: ''};
+ // const capt1: Col = {type: TemplateFieldType.TEXT, title: 'Image', desc: 'description hey', size: TemplateFieldSize.TINY};
+ // const capt2: Col = {type: TemplateFieldType.TEXT, title: 'Locality', desc: '', size: TemplateFieldSize.TINY, defaultContent: ''};
+ // const desc: Col = {type: TemplateFieldType.TEXT, title: 'Description', desc: '', size: TemplateFieldSize.LARGE, defaultContent: 'This is a description of a rock. It is kind of long. It is very long. It is gratuitous. This description should be shorter. Oh well. This is a description of a rock. It is kind of long. It is very long. It is gratuitous. This description should be shorter. Oh well. This is a description of a rock. It is kind of long. It is very long. It is gratuitous. This description should be shorter. Oh well.'};
+
+ // const assignments = {'0': img, '1': capt1, '2': capt2, '3': desc}
+
+ // this.createEmptyTemplate(temp, assignments);
+ // console.log(this.findValidTemplates(this.fieldsInfos, TemplateLayouts.allTemplates));
+
+ // console.log(this._dataViz?.colsInfo.get("IMG")?.size, this._dataViz?.colsInfo.get("IMG")?.type)
+ // console.log(this.fieldsInfos)
+
+ 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) => {
+ this._expandedPreview = info;
+ };
+
+ 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 ? (
+ <div className="docCreatorMenu-expanded-template-preview">
+ <img className="docCreatorMenu-preview-image expanded" src={this._expandedPreview.icon!.url.href.replace('.png', '_o.png')} />
+ <div className="right-buttons-panel">
+ <button className="docCreatorMenu-menu-button section-reveal-options top-right" onPointerDown={e => this.setUpButtonClick(e, () => 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>
+ ) : (
+ <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.addField)} 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"> Description </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 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;
+};
+
+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;
+
+export interface TemplateDocInfos {
+ title: string;
+ height: number;
+ width: number;
+ opts: TemplateOpts;
+ fields: Field[];
+ decorations: Field[];
+}
+
+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';
+}
+
+interface TemplateOpts extends FieldOpts {}
+
+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}px` ?? '0px',
+ 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 bool = true;
+
+ 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}px` ?? '0px',
+ 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}px` ?? '0px',
+ 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;
+ };
+}
+
+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',
+ },
+ },
+ ],
+ };
+
+ // 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]
+ // }]
+ // };
+}
+
+// export class ContentField extends Field {
+
+// }
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..6cc773da0 100644
--- a/src/client/views/nodes/DataVizBox/components/TableBox.tsx
+++ b/src/client/views/nodes/DataVizBox/components/TableBox.tsx
@@ -1,4 +1,6 @@
-import { Button, Type } from 'browndash-components';
+/* eslint-disable jsx-a11y/no-noninteractive-tabindex */
+/* eslint-disable jsx-a11y/no-static-element-interactions */
+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';
@@ -35,6 +37,7 @@ interface TableBoxProps {
left: number;
};
docView?: () => DocumentView | undefined;
+ specHighlightedRow: number | undefined;
}
@observer
@@ -178,7 +181,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 +412,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 +427,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 +448,8 @@ 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..0b94ae4f7 100644
--- a/src/client/views/nodes/DocumentIcon.tsx
+++ b/src/client/views/nodes/DocumentIcon.tsx
@@ -25,19 +25,16 @@ export class DocumentIcon extends ObservableReactComponent<DocumentIconProps> {
render() {
const { view } = this._props;
- const { left, top, right } = view.getBounds || { left: 0, top: 0, right: 0, bottom: 0 };
+ const { left, top, right, 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>
@@ -47,7 +44,7 @@ export class DocumentIcon extends ObservableReactComponent<DocumentIconProps> {
}
}
-@observer
+@observer
export class DocumentIconContainer extends React.Component {
public static getTransformer(): Transformer {
const usedDocuments = new Set<number>();
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 d730e661b..4a249838b 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>;
@@ -1160,7 +1161,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
return Math.max(minTextScale, this._props.PanelHeight() / (this.effectiveNativeHeight || 1)); // height-limited or unscaled
}
@computed private get panelWidth() {
- return this.effectiveNativeWidth ? this.effectiveNativeWidth * this.nativeScaling : this._props.PanelWidth();
+ return this.effectiveNativeWidth ? this.effectiveNativeWidth * this.nativeScaling: this._props.PanelWidth();
}
@computed private get panelHeight() {
if (this.effectiveNativeHeight && (!this.layout_fitWidth || !this.layoutDoc.layout_reflowVertical)) {
@@ -1471,6 +1472,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
render() {
TraceMobx();
+ const borderWidth = 50/*Number(StrCast(this.layoutDoc.layout_borderWidth).replace('px', ''))*/;
const xshift = Math.abs(this.Xshift) <= 0.001 ? this._props.PanelWidth() : undefined;
const yshift = Math.abs(this.Yshift) <= 0.001 ? this._props.PanelHeight() : undefined;
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index 683edba16..a0d69d29d 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -139,7 +139,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>{''}</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..226fad977 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -124,15 +124,15 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
{ fireImmediately: true, delay: 1000 }
);
const { layoutDoc } = this;
- this._disposers.path = reaction(
- () => ({ nativeSize: this.nativeSize, width: NumCast(this.layoutDoc._width) }),
- ({ nativeSize, width }) => {
- if (layoutDoc === this.layoutDoc || !this.layoutDoc._height) {
- this.layoutDoc._height = (width * nativeSize.nativeHeight) / nativeSize.nativeWidth;
- }
- },
- { fireImmediately: true }
- );
+ // this._disposers.path = reaction(
+ // () => ({ nativeSize: this.nativeSize, width: NumCast(this.layoutDoc._width) }),
+ // ({ nativeSize, width }) => {
+ // if (layoutDoc === this.layoutDoc || !this.layoutDoc._height) {
+ // this.layoutDoc._height = (width * nativeSize.nativeHeight) / nativeSize.nativeWidth;
+ // }
+ // },
+ // { fireImmediately: true }
+ // );
this._disposers.scroll = reaction(
() => this.layoutDoc.layout_scrollTop,
sTop => {
@@ -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/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx
index 8974cccaf..cfcf76b12 100644
--- a/src/client/views/nodes/LabelBox.tsx
+++ b/src/client/views/nodes/LabelBox.tsx
@@ -2,7 +2,7 @@ import { Property } from 'csstype';
import { action, computed, makeObservable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
-import * as textfit from 'textfit';
+//import * as textfit from 'textfit';
import { Field, FieldType } from '../../../fields/Doc';
import { BoolCast, NumCast, StrCast } from '../../../fields/Types';
import { TraceMobx } from '../../../fields/util';
@@ -96,7 +96,7 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
this._timeout = setTimeout(() => this.fitTextToBox(r));
return textfitParams;
}
- textfit(r, textfitParams);
+ //textfit(r, textfitParams);
}
return textfitParams;
};
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 d3bc08bd3..730c57794 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 {
@@ -1493,6 +1494,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
pdfAnchorId && this.addPdfReference(pdfAnchorId);
}
if (this._props.autoFocus) setTimeout(() => this._editorView!.focus()); // not sure why setTimeout is needed but editing dashFieldView's doesn't work without it.
+
}
// add user mark for any first character that was typed since the user mark that gets set in KeyPress won't have been called yet.
@@ -2059,7 +2061,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),
@@ -2067,6 +2069,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>
@@ -2077,6 +2082,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
</div>
</div>
);
+
}
}