aboutsummaryrefslogtreecommitdiff
path: root/src/client/views
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views')
-rw-r--r--src/client/views/ContextMenu.scss12
-rw-r--r--src/client/views/DashboardView.scss8
-rw-r--r--src/client/views/DocumentButtonBar.tsx4
-rw-r--r--src/client/views/DocumentDecorations.scss42
-rw-r--r--src/client/views/DocumentDecorations.tsx119
-rw-r--r--src/client/views/EditableView.scss9
-rw-r--r--src/client/views/EditableView.tsx5
-rw-r--r--src/client/views/FieldsDropdown.tsx17
-rw-r--r--src/client/views/FilterPanel.scss54
-rw-r--r--src/client/views/GestureOverlay.scss2
-rw-r--r--src/client/views/GestureOverlay.tsx4
-rw-r--r--src/client/views/InkStroke.scss4
-rw-r--r--src/client/views/InkStrokeProperties.ts23
-rw-r--r--src/client/views/InkTangentHandles.tsx2
-rw-r--r--src/client/views/InkTranscription.tsx197
-rw-r--r--src/client/views/InkingStroke.tsx2
-rw-r--r--src/client/views/LightboxView.scss48
-rw-r--r--src/client/views/LightboxView.tsx3
-rw-r--r--src/client/views/Main.scss6
-rw-r--r--src/client/views/Main.tsx7
-rw-r--r--src/client/views/MainView.scss40
-rw-r--r--src/client/views/MainView.tsx41
-rw-r--r--src/client/views/MainViewModal.tsx2
-rw-r--r--src/client/views/MarqueeAnnotator.tsx3
-rw-r--r--src/client/views/MetadataEntryMenu.scss20
-rw-r--r--src/client/views/OverlayView.scss12
-rw-r--r--src/client/views/PreviewCursor.scss4
-rw-r--r--src/client/views/PropertiesButtons.tsx4
-rw-r--r--src/client/views/PropertiesSection.tsx1
-rw-r--r--src/client/views/PropertiesView.scss50
-rw-r--r--src/client/views/PropertiesView.tsx6
-rw-r--r--src/client/views/SidebarAnnos.scss12
-rw-r--r--src/client/views/SidebarAnnos.tsx29
-rw-r--r--src/client/views/StyleProvider.scss22
-rw-r--r--src/client/views/StyleProvider.tsx8
-rw-r--r--src/client/views/StyleProviderQuiz.scss8
-rw-r--r--src/client/views/UndoStack.tsx3
-rw-r--r--src/client/views/ViewBoxInterface.ts3
-rw-r--r--src/client/views/animationtimeline/Timeline.scss4
-rw-r--r--src/client/views/animationtimeline/Timeline.tsx3
-rw-r--r--src/client/views/animationtimeline/TimelineMenu.tsx4
-rw-r--r--src/client/views/animationtimeline/TimelineOverview.tsx5
-rw-r--r--src/client/views/collections/CollectionCardDeckView.scss4
-rw-r--r--src/client/views/collections/CollectionCarousel3DView.scss12
-rw-r--r--src/client/views/collections/CollectionCarouselView.scss16
-rw-r--r--src/client/views/collections/CollectionDockingView.scss132
-rw-r--r--src/client/views/collections/CollectionMasonryViewFieldRow.tsx44
-rw-r--r--src/client/views/collections/CollectionNoteTakingView.scss30
-rw-r--r--src/client/views/collections/CollectionNoteTakingView.tsx3
-rw-r--r--src/client/views/collections/CollectionPivotView.tsx8
-rw-r--r--src/client/views/collections/CollectionStackedTimeline.scss16
-rw-r--r--src/client/views/collections/CollectionStackingView.scss39
-rw-r--r--src/client/views/collections/CollectionStackingView.tsx177
-rw-r--r--src/client/views/collections/CollectionStackingViewFieldColumn.tsx34
-rw-r--r--src/client/views/collections/CollectionTimeView.scss13
-rw-r--r--src/client/views/collections/CollectionTreeView.scss6
-rw-r--r--src/client/views/collections/CollectionTreeView.tsx3
-rw-r--r--src/client/views/collections/CollectionView.scss8
-rw-r--r--src/client/views/collections/CollectionView.tsx2
-rw-r--r--src/client/views/collections/FlashcardPracticeUI.scss8
-rw-r--r--src/client/views/collections/FlashcardPracticeUI.tsx9
-rw-r--r--src/client/views/collections/KeyRestrictionRow.tsx72
-rw-r--r--src/client/views/collections/TabDocView.scss4
-rw-r--r--src/client/views/collections/TabDocView.tsx4
-rw-r--r--src/client/views/collections/TreeView.scss4
-rw-r--r--src/client/views/collections/TreeView.tsx3
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.scss2
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss32
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx105
-rw-r--r--src/client/views/collections/collectionFreeForm/FaceCollectionBox.scss14
-rw-r--r--src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx3
-rw-r--r--src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx5
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx2
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.scss8
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx99
-rw-r--r--src/client/views/collections/collectionGrid/CollectionGridView.scss6
-rw-r--r--src/client/views/collections/collectionLinear/CollectionLinearView.scss8
-rw-r--r--src/client/views/collections/collectionLinear/CollectionLinearView.tsx5
-rw-r--r--src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss8
-rw-r--r--src/client/views/collections/collectionMulticolumn/CollectionMultirowView.scss4
-rw-r--r--src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx1
-rw-r--r--src/client/views/collections/collectionMulticolumn/MultirowHeightLabel.tsx1
-rw-r--r--src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx3
-rw-r--r--src/client/views/collections/collectionSchema/CollectionSchemaView.scss10
-rw-r--r--src/client/views/collections/collectionSchema/CollectionSchemaView.tsx24
-rw-r--r--src/client/views/collections/collectionSchema/SchemaCellField.tsx4
-rw-r--r--src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx9
-rw-r--r--src/client/views/collections/collectionSchema/SchemaTableCell.tsx3
-rw-r--r--src/client/views/global/globalCssVariables.module.scss2
-rw-r--r--src/client/views/global/globalScripts.ts2
-rw-r--r--src/client/views/linking/LinkMenuItem.scss4
-rw-r--r--src/client/views/linking/LinkMenuItem.tsx18
-rw-r--r--src/client/views/linking/LinkPopup.scss6
-rw-r--r--src/client/views/linking/LinkPopup.tsx1
-rw-r--r--src/client/views/newlightbox/NewLightboxView.scss4
-rw-r--r--src/client/views/newlightbox/components/Recommendation/Recommendation.scss2
-rw-r--r--src/client/views/nodes/AudioBox.scss4
-rw-r--r--src/client/views/nodes/CollectionFreeFormDocumentView.scss4
-rw-r--r--src/client/views/nodes/ComparisonBox.scss18
-rw-r--r--src/client/views/nodes/DataVizBox/DataVizBox.scss10
-rw-r--r--src/client/views/nodes/DataVizBox/DataVizBox.tsx7
-rw-r--r--src/client/views/nodes/DataVizBox/DocCreatorMenu/Backend/TemplateManager.ts91
-rw-r--r--src/client/views/nodes/DataVizBox/DocCreatorMenu/Backend/TemplateMenuAIUtils.ts122
-rw-r--r--src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.scss621
-rw-r--r--src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.tsx1156
-rw-r--r--src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/DynamicField.tsx117
-rw-r--r--src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/Field.tsx66
-rw-r--r--src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/FieldUtils.tsx79
-rw-r--r--src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/StaticField.tsx147
-rw-r--r--src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/ConditionalsTextarea.tsx65
-rw-r--r--src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/DocCreatorMenuButton.tsx42
-rw-r--r--src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplateEditingWindow.tsx220
-rw-r--r--src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplateMenuFieldOptions.tsx185
-rw-r--r--src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplatePreviewBox.tsx89
-rw-r--r--src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplatePreviewGrid.tsx60
-rw-r--r--src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplateRenderPreviewWindow.tsx346
-rw-r--r--src/client/views/nodes/DataVizBox/DocCreatorMenu/Template.ts179
-rw-r--r--src/client/views/nodes/DataVizBox/DocCreatorMenu/Template.tsx139
-rw-r--r--src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateBackend.ts (renamed from src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateBackend.tsx)309
-rw-r--r--src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/DataField.ts20
-rw-r--r--src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/DecorationField.ts3
-rw-r--r--src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/DynamicField.ts131
-rw-r--r--src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/StaticContentField.ts62
-rw-r--r--src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/TemplateField.ts172
-rw-r--r--src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/TemplateFieldUtils.ts71
-rw-r--r--src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateManager.tsx22
-rw-r--r--src/client/views/nodes/DataVizBox/SchemaCSVPopUp.scss8
-rw-r--r--src/client/views/nodes/DataVizBox/components/Chart.scss4
-rw-r--r--src/client/views/nodes/DataVizBox/components/Histogram.tsx11
-rw-r--r--src/client/views/nodes/DataVizBox/components/LineChart.tsx11
-rw-r--r--src/client/views/nodes/DataVizBox/components/PieChart.tsx15
-rw-r--r--src/client/views/nodes/DataVizBox/components/TableBox.tsx16
-rw-r--r--src/client/views/nodes/DiagramBox.tsx6
-rw-r--r--src/client/views/nodes/DocumentContentsView.tsx4
-rw-r--r--src/client/views/nodes/DocumentLinksButton.scss4
-rw-r--r--src/client/views/nodes/DocumentLinksButton.tsx1
-rw-r--r--src/client/views/nodes/DocumentView.scss37
-rw-r--r--src/client/views/nodes/DocumentView.tsx28
-rw-r--r--src/client/views/nodes/EquationBox.tsx44
-rw-r--r--src/client/views/nodes/FontIconBox/FontIconBadge.scss2
-rw-r--r--src/client/views/nodes/FontIconBox/FontIconBox.scss20
-rw-r--r--src/client/views/nodes/FunctionPlotBox.tsx3
-rw-r--r--src/client/views/nodes/IconTagBox.scss2
-rw-r--r--src/client/views/nodes/ImageBox.scss26
-rw-r--r--src/client/views/nodes/ImageBox.tsx147
-rw-r--r--src/client/views/nodes/KeyValueBox.scss10
-rw-r--r--src/client/views/nodes/KeyValueBox.tsx8
-rw-r--r--src/client/views/nodes/KeyValuePair.scss2
-rw-r--r--src/client/views/nodes/LabelBox.scss12
-rw-r--r--src/client/views/nodes/LabelBox.tsx42
-rw-r--r--src/client/views/nodes/LinkBox.tsx23
-rw-r--r--src/client/views/nodes/LinkDocPreview.scss2
-rw-r--r--src/client/views/nodes/LinkDocPreview.tsx10
-rw-r--r--src/client/views/nodes/LoadingBox.scss4
-rw-r--r--src/client/views/nodes/LoadingBox.tsx2
-rw-r--r--src/client/views/nodes/MapBox/MapAnchorMenu.scss37
-rw-r--r--src/client/views/nodes/MapBox/MapBox.scss10
-rw-r--r--src/client/views/nodes/MapBox/MapBox.tsx59
-rw-r--r--src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx51
-rw-r--r--src/client/views/nodes/PDFBox.scss36
-rw-r--r--src/client/views/nodes/PDFBox.tsx88
-rw-r--r--src/client/views/nodes/PhysicsBox/PhysicsSimulationBox.scss4
-rw-r--r--src/client/views/nodes/RecordingBox/ProgressBar.scss141
-rw-r--r--src/client/views/nodes/RecordingBox/RecordingView.scss12
-rw-r--r--src/client/views/nodes/ScreenshotBox.scss8
-rw-r--r--src/client/views/nodes/ScreenshotBox.tsx18
-rw-r--r--src/client/views/nodes/ScriptingBox.scss6
-rw-r--r--src/client/views/nodes/TaskBox.scss132
-rw-r--r--src/client/views/nodes/TaskBox.tsx670
-rw-r--r--src/client/views/nodes/VideoBox.scss10
-rw-r--r--src/client/views/nodes/VideoBox.tsx64
-rw-r--r--src/client/views/nodes/WebBox.scss44
-rw-r--r--src/client/views/nodes/WebBox.tsx29
-rw-r--r--src/client/views/nodes/audio/AudioWaveform.scss10
-rw-r--r--src/client/views/nodes/calendarBox/CalendarBox.scss38
-rw-r--r--src/client/views/nodes/calendarBox/CalendarBox.tsx336
-rw-r--r--src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.scss42
-rw-r--r--src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx45
-rw-r--r--src/client/views/nodes/formattedText/DailyJournal.tsx248
-rw-r--r--src/client/views/nodes/formattedText/DashFieldView.scss6
-rw-r--r--src/client/views/nodes/formattedText/DashFieldView.tsx1
-rw-r--r--src/client/views/nodes/formattedText/EquationEditor.scss10
-rw-r--r--src/client/views/nodes/formattedText/EquationEditor.tsx4
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.scss54
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx117
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBoxComment.scss16
-rw-r--r--src/client/views/nodes/formattedText/RichTextMenu.scss4
-rw-r--r--src/client/views/nodes/formattedText/TooltipTextMenu.scss8
-rw-r--r--src/client/views/nodes/formattedText/marks_rts.ts14
-rw-r--r--src/client/views/nodes/imageEditor/ImageEditor.scss10
-rw-r--r--src/client/views/nodes/scrapbook/AIPresetGenerator.ts31
-rw-r--r--src/client/views/nodes/scrapbook/EmbeddedDocView.tsx52
-rw-r--r--src/client/views/nodes/scrapbook/ScrapbookBox.scss66
-rw-r--r--src/client/views/nodes/scrapbook/ScrapbookBox.tsx302
-rw-r--r--src/client/views/nodes/scrapbook/ScrapbookContent.tsx23
-rw-r--r--src/client/views/nodes/scrapbook/ScrapbookPreset.tsx94
-rw-r--r--src/client/views/nodes/scrapbook/ScrapbookPresetRegistry.ts36
-rw-r--r--src/client/views/nodes/scrapbook/ScrapbookSlot.scss85
-rw-r--r--src/client/views/nodes/scrapbook/ScrapbookSlot.tsx28
-rw-r--r--src/client/views/nodes/scrapbook/ScrapbookSlotTypes.ts25
-rw-r--r--src/client/views/nodes/trails/PresBox.scss104
-rw-r--r--src/client/views/nodes/trails/PresBox.tsx53
-rw-r--r--src/client/views/nodes/trails/PresSlideBox.scss10
-rw-r--r--src/client/views/pdf/AnchorMenu.scss14
-rw-r--r--src/client/views/pdf/GPTPopup/GPTPopup.scss10
-rw-r--r--src/client/views/pdf/GPTPopup/GPTPopup.tsx30
-rw-r--r--src/client/views/pdf/PDFViewer.scss13
-rw-r--r--src/client/views/pdf/PDFViewer.tsx6
-rw-r--r--src/client/views/search/FaceRecognitionHandler.tsx13
-rw-r--r--src/client/views/smartdraw/SmartDrawHandler.tsx5
-rw-r--r--src/client/views/topbar/TopBar.scss2
211 files changed, 6187 insertions, 4156 deletions
diff --git a/src/client/views/ContextMenu.scss b/src/client/views/ContextMenu.scss
index d22c4d096..337c976cb 100644
--- a/src/client/views/ContextMenu.scss
+++ b/src/client/views/ContextMenu.scss
@@ -68,8 +68,8 @@
width: 100%;
height: 100%;
position: absolute;
- top: 0;
- left: 0;
+ top: 0px;
+ left: 0px;
z-index: 0;
filter: opacity(0);
}
@@ -187,10 +187,10 @@
display: flex;
.close-menu {
- margin-top: 0;
- margin-bottom: 0;
- margin-right: 0;
- padding: 0;
+ margin-top: 0px;
+ margin-bottom: 0px;
+ margin-right: 0px;
+ padding: 0px;
margin-left: auto;
z-index: 999999999;
width: 20px;
diff --git a/src/client/views/DashboardView.scss b/src/client/views/DashboardView.scss
index daa711bc4..d74441b9c 100644
--- a/src/client/views/DashboardView.scss
+++ b/src/client/views/DashboardView.scss
@@ -79,8 +79,8 @@ $dashboard-container-width: 250px;
position: absolute;
width: 100%;
height: 100%;
- left: 0;
- top: 0;
+ left: 0px;
+ top: 0px;
z-index: -1;
}
}
@@ -137,8 +137,8 @@ $dashboard-container-width: 250px;
position: absolute;
width: 100%;
height: 100%;
- left: 0;
- top: 0;
+ left: 0px;
+ top: 0px;
z-index: -1;
}
}
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index bc669fc4e..a845e4936 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -294,7 +294,7 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (
{metaBtn('keywords', 'id-card')}
</div> */}
- <Tooltip title={<div className="dash-keyword-button">Open keyword menu</div>}>
+ <Tooltip title={<div className="dash-keyword-button">Open tags menu</div>}>
<div
className="documentButtonBar-icon"
style={{ color: 'white' }}
@@ -511,7 +511,7 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (
{!DocumentView.Selected().some(v => v.allLinks.length) ? null : <div className="documentButtonBar-button">{this.followLinkButton}</div>}
<div className="documentButtonBar-button">{this.pinButton}</div>
<div className="documentButtonBar-button">{this.recordButton}</div>
- <div className="documentButtonBar-button">{this.calendarButton}</div>
+ {Doc.noviceMode ? null : <div className="documentButtonBar-button">{this.calendarButton}</div>}
{this.view0?.HasAIEditor ? <div className="documentButtonBar-button">{this.aiEditorButton}</div> : null}
<div className="documentButtonBar-button">{this.keywordButton}</div>
{!Doc.UserDoc().documentLinksButton_fullMenu ? null : <div className="documentButtonBar-button">{this.shareButton}</div>}
diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss
index 09a13634f..d79826b3c 100644
--- a/src/client/views/DocumentDecorations.scss
+++ b/src/client/views/DocumentDecorations.scss
@@ -11,10 +11,10 @@ $resizeHandler: 8px;
// Rotation handler
.documentDecorations-rotation {
border-radius: 100%;
- height: 30;
- width: 30;
- right: -40;
- bottom: -20;
+ height: 30px;
+ width: 30px;
+ right: -40px;
+ bottom: -20px;
//top: calc(50% - 15px);
position: absolute;
pointer-events: all;
@@ -32,8 +32,10 @@ $resizeHandler: 8px;
}
.documentDecorations-rotationCenter {
position: absolute;
- width: 6px;
- height: 6px;
+ width: 9px;
+ height: 9px;
+ left: -4.5px;
+ top: -4.5px;
pointer-events: all;
background: green;
border-radius: 50%;
@@ -43,15 +45,15 @@ $resizeHandler: 8px;
width: 100%;
pointer-events: all;
border-radius: 50%;
- top: 30; // offset by height of documentButtonBar so that items can be clicked without overlap interference
+ top: 30px; // offset by height of documentButtonBar so that items can be clicked without overlap interference
color: black;
}
}
.documentDecorations-container {
position: absolute;
- top: 0;
- left: 0;
+ top: 0px;
+ left: 0px;
transform-origin: 50% calc(50% + 10px);
display: grid;
grid-template-rows: $headerHeight $resizeHandler 1fr $resizeHandler;
@@ -119,7 +121,7 @@ $resizeHandler: 8px;
}
> svg {
- margin: 0;
+ margin: 0px;
}
}
@@ -148,7 +150,7 @@ $resizeHandler: 8px;
}
> svg {
- margin: 0;
+ margin: 0px;
}
}
&:hover {
@@ -183,7 +185,7 @@ $resizeHandler: 8px;
}
> svg {
- margin: 0;
+ margin: 0px;
}
}
@@ -244,7 +246,7 @@ $resizeHandler: 8px;
display: inline;
position: relative;
top: -2.5;
- left: 35;
+ left: 35px;
zoom: 0.7;
}
@@ -358,8 +360,8 @@ $resizeHandler: 8px;
left: 7px;
top: 7px;
background: global.$medium-gray;
- height: 10;
- width: 10;
+ height: 10px;
+ width: 10px;
opacity: 0.5;
pointer-events: all;
cursor: nwse-resize;
@@ -369,8 +371,8 @@ $resizeHandler: 8px;
position: relative;
background: black;
color: rgb(145, 144, 144);
- height: 20;
- width: 20;
+ height: 20px;
+ width: 20px;
pointer-events: all;
margin: auto;
display: flex;
@@ -378,8 +380,8 @@ $resizeHandler: 8px;
border-radius: 100%;
cursor: default;
svg {
- width: 10;
- height: 10;
+ width: 10px;
+ height: 10px;
margin: auto;
}
}
@@ -387,7 +389,7 @@ $resizeHandler: 8px;
.documentDecorations-rotationPath {
position: absolute;
width: 100%;
- height: 0;
+ height: 0px;
transform: translate(0px, -25%);
padding-bottom: 100%;
border-radius: 100%;
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index ab665e984..2b7050cf0 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -1,7 +1,7 @@
+import { IconButton } from '@dash/components';
import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Tooltip } from '@mui/material';
-import { IconButton } from '@dash/components';
import { action, computed, makeObservable, observable, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
@@ -27,15 +27,13 @@ import './DocumentDecorations.scss';
import { InkStrokeProperties } from './InkStrokeProperties';
import { InkingStroke } from './InkingStroke';
import { ObservableReactComponent } from './ObservableReactComponent';
+import { TagsView } from './TagsView';
import { CollectionDockingView } from './collections/CollectionDockingView';
import { CollectionFreeFormView } from './collections/collectionFreeForm';
import { Colors } from './global/globalEnums';
import { CollectionFreeFormDocumentView } from './nodes/CollectionFreeFormDocumentView';
import { DocumentView } from './nodes/DocumentView';
-import { ImageBox } from './nodes/ImageBox';
import { OpenWhere, OpenWhereMod } from './nodes/OpenWhere';
-import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
-import { TagsView } from './TagsView';
interface DocumentDecorationsProps {
PanelWidth: number;
@@ -58,7 +56,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
private _inkDragDocs: { doc: Doc; x: number; y: number; width: number; height: number }[] = [];
private _interactionLock?: boolean;
- @observable _showNothing = true;
+ @observable private _showNothing = true;
@observable private _forceRender = 0;
@observable private _accumulatedTitle = '';
@observable private _titleControlString: string = '$title';
@@ -349,13 +347,14 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
e.stopPropagation();
};
- setRotateCenter = (seldocview: DocumentView, rotCenter: number[]) => {
+ setDocRotateCenter = (seldocview: DocumentView, rotCenter: number[]) => {
const selDoc = seldocview.Document;
const newloccentern = seldocview.screenToViewTransform().transformPoint(rotCenter[0], rotCenter[1]);
const newlocenter = [newloccentern[0] - NumCast(seldocview.layoutDoc._width) / 2, newloccentern[1] - NumCast(seldocview.layoutDoc._height) / 2];
const final = Utils.rotPt(newlocenter[0], newlocenter[1], -(NumCast(seldocview.Document._rotation) / 180) * Math.PI);
selDoc._rotation_centerX = final.x / NumCast(seldocview.layoutDoc._width);
selDoc._rotation_centerY = final.y / NumCast(seldocview.layoutDoc._height);
+ return false;
};
@action
@@ -365,10 +364,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
setupMoveUpEvents(
this,
e,
- (moveEv: PointerEvent, down: number[], delta: number[]) => {
- this.setRotateCenter(seldocview, [this.rotCenter[0] + delta[0], this.rotCenter[1] + delta[1]]);
- return false;
- },
+ (moveEv) => this.setDocRotateCenter(seldocview, [moveEv.clientX, moveEv.clientY]),
action(() => { this._isRotating = false; }), // upEvent
action(() => { seldocview.Document._rotation_centerX = seldocview.Document._rotation_centerY = 0; }),
true
@@ -377,7 +373,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
};
@action
- onRotateDown = (e: React.PointerEvent): void => {
+ onRotateHdlDown = (e: React.PointerEvent): void => {
this._isRotating = true;
const rcScreen = { X: this.rotCenter[0], Y: this.rotCenter[1] };
const rotateUndo = UndoManager.StartBatch('drag rotation');
@@ -393,17 +389,20 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
const unrotatedDocPos = { x: NumCast(dv.Document.x) + localRotCtrOffset[0] - startRotCtr.x, y: NumCast(dv.Document.y) + localRotCtrOffset[1] - startRotCtr.y };
infos.set(dv.Document, { unrotatedDocPos, startRotCtr, accumRot });
});
- const infoRot = (angle: number, isAbs = false) => {
- DocumentView.Selected().forEach(
- action(dv => {
- const { unrotatedDocPos, startRotCtr, accumRot } = infos.get(dv.Document)!;
- const endRotCtr = Utils.rotPt(startRotCtr.x, startRotCtr.y, isAbs ? angle : accumRot + angle);
- infos.set(dv.Document, { unrotatedDocPos, startRotCtr, accumRot: isAbs ? angle : accumRot + angle });
- dv.Document.x = infos.get(dv.Document)!.unrotatedDocPos.x - (endRotCtr.x - startRotCtr.x);
- dv.Document.y = infos.get(dv.Document)!.unrotatedDocPos.y - (endRotCtr.y - startRotCtr.y);
- dv.Document._rotation = ((isAbs ? 0 : NumCast(dv.Document._rotation)) + (angle * 180) / Math.PI) % 360; // Rotation between -360 and 360
- })
- );
+ const rotateDocs = (angle: number, isAbs = false) => {
+ if (selectedInk.length) {
+ InkStrokeProperties.Instance.rotateInk(selectedInk, angle, rcScreen); // rotate ink
+ return this.setDocRotateCenter(seldocview, centerPoint);
+ }
+ DocumentView.Selected().forEach(action(dv => {
+ const { unrotatedDocPos, startRotCtr, accumRot } = infos.get(dv.Document)!;
+ const endRotCtr = Utils.rotPt(startRotCtr.x, startRotCtr.y, isAbs ? angle : accumRot + angle);
+ infos.set(dv.Document, { unrotatedDocPos, startRotCtr, accumRot: isAbs ? angle : accumRot + angle });
+ dv.Document.x = infos.get(dv.Document)!.unrotatedDocPos.x - (endRotCtr.x - startRotCtr.x);
+ dv.Document.y = infos.get(dv.Document)!.unrotatedDocPos.y - (endRotCtr.y - startRotCtr.y);
+ dv.Document._rotation = ((isAbs ? 0 : NumCast(dv.Document._rotation)) + (angle * 180) / Math.PI) % 360; // Rotation between -360 and 360
+ })); // prettier-ignore
+ return false;
};
setupMoveUpEvents(
this,
@@ -411,34 +410,17 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
(moveEv: PointerEvent, down: number[], delta: number[]) => {
const previousPoint = { X: moveEv.clientX, Y: moveEv.clientY };
const movedPoint = { X: moveEv.clientX - delta[0], Y: moveEv.clientY - delta[1] };
- const deltaAng = InkStrokeProperties.angleChange(movedPoint, previousPoint, rcScreen);
- if (selectedInk.length) {
- deltaAng && InkStrokeProperties.Instance.rotateInk(selectedInk, deltaAng, rcScreen);
- this.setRotateCenter(seldocview, centerPoint);
- } else {
- infoRot(deltaAng);
- }
- return false;
+ return rotateDocs(InkStrokeProperties.angleChange(movedPoint, previousPoint, rcScreen));
}, // moveEvent
action(() => {
const oldRotation = NumCast(seldocview.Document._rotation);
- const diff = oldRotation - Math.round(oldRotation / 45) * 45;
- if (Math.abs(diff) < 5) {
- if (selectedInk.length) {
- InkStrokeProperties.Instance.rotateInk(selectedInk, ((Math.round(oldRotation / 45) * 45 - oldRotation) / 180) * Math.PI, rcScreen);
- } else {
- infoRot(((Math.round(oldRotation / 45) * 45) / 180) * Math.PI, true);
- }
- }
- if (selectedInk.length) {
- this.setRotateCenter(seldocview, centerPoint);
- }
+ if (Math.abs(oldRotation - Math.round(oldRotation / 45) * 45) < 5) { // rptation witihin 5deg of a 45deg angle multiple
+ rotateDocs(((Math.round(oldRotation / 45) * 45) / 180) * Math.PI, true);
+ } // prettier-ignore
this._isRotating = false;
rotateUndo?.end();
}), // upEvent
- action(() => {
- this._showRotCenter = !this._showRotCenter;
- }) // clickEvent
+ action(() => (this._showRotCenter = !this._showRotCenter)) // clickEvent
);
};
@@ -446,7 +428,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
onPointerDown = (e: React.PointerEvent): void => {
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
DocumentView.Selected()
- .filter(dv => e.shiftKey && dv.ComponentView instanceof ImageBox)
+ .filter(dv => e.shiftKey && dv.ComponentView?.isOutpaintable?.())
.forEach(dv => {
dv.Document[dv.ComponentView!.fieldKey + '_outpaintOriginalWidth'] = NumCast(dv.Document._width);
dv.Document[dv.ComponentView!.fieldKey + '_outpaintOriginalHeight'] = NumCast(dv.Document._height);
@@ -502,7 +484,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
this._interactionLock = true;
this._snapPt = thisPt;
- const outpainted = e.shiftKey ? DocumentView.Selected().filter(dv => dv.ComponentView instanceof ImageBox) : [];
+ const outpainted = e.shiftKey ? DocumentView.Selected().filter(dv => dv.ComponentView?.isOutpaintable?.()) : [];
const notOutpainted = e.shiftKey ? DocumentView.Selected().filter(dv => !outpainted.includes(dv)) : DocumentView.Selected();
// Special handling for shift-drag resize (outpainting of Images by resizing without scaling content - fill in with firefly GAI)
@@ -524,7 +506,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
resizeViewForOutpainting = (docView: DocumentView, refPt: number[], scale: { x: number; y: number }, opts: { dragHdl: string; shiftKey: boolean }) => {
const doc = docView.Document;
- if (doc.isGroup) {
+ if (Doc.IsFreeformGroup(doc)) {
DocListCast(doc.data)
.map(member => DocumentView.getDocumentView(member, docView)!)
.forEach(member => this.resizeViewForOutpainting(member, refPt, scale, opts));
@@ -598,14 +580,14 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
//
// determines if anything being dragged directly or via a group has a fixed aspect ratio (in which case we resize uniformly)
//
- hasFixedAspect = (doc: Doc): boolean => (doc.isGroup ? DocListCast(doc.data).some(this.hasFixedAspect) : !BoolCast(doc._layout_nativeDimEditable));
+ hasFixedAspect = (doc: Doc): boolean => (Doc.IsFreeformGroup(doc) ? DocListCast(doc.data).some(this.hasFixedAspect) : !BoolCast(doc._layout_nativeDimEditable));
//
// resize a single DocumentView about the specified reference point, possibly setting/updating the native dimensions of the Doc
//
resizeView = (docView: DocumentView, refPt: number[], scale: { x: number; y: number }, opts: { dragHdl: string; freezeNativeDims: boolean }) => {
const doc = docView.Document;
- if (doc.isGroup) {
+ if (Doc.IsFreeformGroup(doc)) {
DocListCast(doc.data)
.map(member => DocumentView.getDocumentView(member, docView)!)
.forEach(member => this.resizeView(member, refPt, scale, opts));
@@ -623,8 +605,10 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
if (nwidth && nheight && !cornerReflow && !horizontalReflow && !verticalReflow) {
scale.x === 1 ? (scale.x = scale.y) : (scale.y = scale.x);
}
+ if (NumCast(doc._height) * scale.y < NumCast(doc._height_min, 10)) scale.y = NumCast(doc._height_min, 10) / NumCast(doc._height);
+ if (NumCast(doc._width) * scale.x < NumCast(doc._width_min, 25)) scale.x = NumCast(doc._width_min, 25) / NumCast(doc._width);
- if ((horizontalReflow || cornerReflow) && Doc.NativeWidth(doc)) {
+ if ((horizontalReflow || cornerReflow) && Doc.NativeWidth(doc) && scale.x > 0) {
const setData = Doc.NativeWidth(doc[DocData]) === doc.nativeWidth;
doc._nativeWidth = scale.x * Doc.NativeWidth(doc);
if (setData) Doc.SetNativeWidth(doc[DocData], NumCast(doc.nativeWidth));
@@ -632,25 +616,23 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
doc._nativeHeight = (initHeight / initWidth) * nwidth; // initializes the nativeHeight for a PDF
}
}
- if ((verticalReflow || cornerReflow) && Doc.NativeHeight(doc)) {
+ if ((verticalReflow || cornerReflow) && Doc.NativeHeight(doc) && scale.y > 0) {
const setData = Doc.NativeHeight(doc[DocData]) === doc.nativeHeight && !doc.layout_reflowVertical;
doc._nativeHeight = scale.y * Doc.NativeHeight(doc);
if (setData) Doc.SetNativeHeight(doc[DocData], NumCast(doc._nativeHeight));
}
- doc._width = Math.max(NumCast(doc._width_min, 25), NumCast(doc._width) * scale.x);
- doc._height = Math.max(NumCast(doc._height_min, 10), NumCast(doc._height) * scale.y);
+ doc._width = NumCast(doc._width) * scale.x;
+ doc._height = NumCast(doc._height) * scale.y;
const { deltaX, deltaY } = this.realignRefPt(doc, refCent, initWidth || 1, initHeight || 1);
doc.x = NumCast(doc.x) + deltaX;
doc.y = NumCast(doc.y) + deltaY;
doc._layout_modificationDate = new DateField();
- if (scale.y !== 1) {
- const docLayout = docView.layoutDoc;
- docLayout._layout_autoHeight = undefined;
- if (docView.layoutDoc._layout_autoHeight) {
- // if autoHeight is still on because of a prototype
- docLayout._layout_autoHeight = false; // then don't inherit, but explicitly set it to false
+ if (scale.y !== 1 && !opts.freezeNativeDims) {
+ doc._layout_autoHeight = undefined;
+ if (doc._layout_autoHeight) {
+ doc._layout_autoHeight = false; // set explicitly to false if inherited from parent of layout
}
}
}
@@ -695,7 +677,8 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
if (lastView) {
const invXf = lastView.screenToViewTransform().inverse();
const seldoc = lastView.layoutDoc;
- const loccenter = Utils.rotPt(NumCast(seldoc._rotation_centerX) * NumCast(seldoc._width), NumCast(seldoc._rotation_centerY) * NumCast(seldoc._height), invXf.Rotate);
+ const rcent = this._showRotCenter ? [NumCast(seldoc._rotation_centerX), NumCast(seldoc._rotation_centerY)] : [0, 0];
+ const loccenter = Utils.rotPt(rcent[0] * NumCast(seldoc._width), rcent[1] * NumCast(seldoc._height), invXf.Rotate);
return invXf.transformPoint(loccenter.x + NumCast(seldoc._width) / 2, loccenter.y + NumCast(seldoc._height) / 2);
}
return this._rotCenter;
@@ -737,7 +720,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
hideDecorations ||
seldocview._props.hideOpenButton ||
seldocview.Document.layout_hideOpenButton ||
- DocumentView.Selected().some(docView => docView.Document._dragOnlyWithinContainer || docView.Document.isGroup || docView.Document.layout_hideOpenButton) ||
+ DocumentView.Selected().some(docView => docView.Document._dragOnlyWithinContainer || docView.Document.layout_hideOpenButton) ||
this._isRounding ||
this._isRotating;
const hideDeleteButton =
@@ -765,7 +748,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
const rotation = DocumentView.Selected().length === 1 ? seldocview.screenToContentsTransform().inverse().RotateDeg : 0;
// Radius constants
- const useRounding = seldocview.ComponentView instanceof ImageBox || seldocview.ComponentView instanceof FormattedTextBox || seldocview.ComponentView instanceof CollectionFreeFormView;
+ const useRounding = seldocview.ComponentView?.showBorderRounding?.();
const borderRadius = numberValue(Cast(seldocview.Document.layout_borderRounding, 'string', null));
const docMax = Math.min(NumCast(seldocview.Document._width) / 2, NumCast(seldocview.Document._height) / 2);
const maxDist = Math.min((this.Bounds.r - this.Bounds.x) / 2, (this.Bounds.b - this.Bounds.y) / 2);
@@ -923,16 +906,14 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
<div
style={{
position: 'absolute',
- transform: `rotate(${rotation}deg)`,
- width: this.Bounds.r - this.Bounds.x + 'px',
- height: this.Bounds.b - this.Bounds.y + 'px',
- left: this.Bounds.x,
- top: this.Bounds.y,
+ transform: `translate(${bounds.x - this._resizeBorderWidth}px, ${bounds.y - this._resizeBorderWidth}px) rotate(${rotation}deg)`,
+ width: bounds.r - bounds.x + 2 * this._resizeBorderWidth + 'px',
+ height: bounds.b - bounds.y + 2 * this._resizeBorderWidth + 'px',
pointerEvents: 'none',
}}>
{this._isRotating ? null : (
<Tooltip enterDelay={750} title={<div className="dash-tooltip">tap to set rotate center, drag to rotate</div>}>
- <div className="documentDecorations-rotation" onPointerDown={this.onRotateDown} onContextMenu={e => e.preventDefault()}>
+ <div className="documentDecorations-rotation" onPointerDown={this.onRotateHdlDown} onContextMenu={e => e.preventDefault()}>
<IconButton icon={<FaUndo />} color={SettingsManager.userColor} />
</div>
</Tooltip>
@@ -941,7 +922,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
{!this._showRotCenter ? null : (
<div
className="documentDecorations-rotationCenter"
- style={{ transform: `translate(${this.rotCenter[0] - 3}px, ${this.rotCenter[1] - 3}px)` }}
+ style={{ transform: `translate(${this.rotCenter[0]}px, ${this.rotCenter[1]}px)` }}
onPointerDown={this.onRotateCenterDown}
onContextMenu={e => e.preventDefault()}
/>
diff --git a/src/client/views/EditableView.scss b/src/client/views/EditableView.scss
index fa4542ac4..333939d03 100644
--- a/src/client/views/EditableView.scss
+++ b/src/client/views/EditableView.scss
@@ -6,14 +6,14 @@
overflow-y: auto;
height: 100%;
width: 100%;
- min-width: 20;
+ min-width: 20px;
text-overflow: ellipsis;
- -ms-overflow-style: none;
- scrollbar-width: none;
+ -ms-overflow-style: none;
+ scrollbar-width: none;
}
.editableView-container-editing::-webkit-scrollbar {
- display: none;
+ display: none;
}
.editableView-container-editing-oneLine {
@@ -44,4 +44,3 @@
border: none;
outline: none;
}
-
diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx
index d9447b7ec..deeabaa28 100644
--- a/src/client/views/EditableView.tsx
+++ b/src/client/views/EditableView.tsx
@@ -236,6 +236,7 @@ export class EditableView extends ObservableReactComponent<EditableProps> {
return this._editing;
};
+ setInputRef = (r: HTMLInputElement | HTMLTextAreaElement | null) => (this._inputref = r);
renderEditor() {
return this._props.autosuggestProps ? (
<Autosuggest
@@ -255,7 +256,7 @@ export class EditableView extends ObservableReactComponent<EditableProps> {
) : this._props.oneLine !== false && this._props.GetValue()?.toString().indexOf('\n') === -1 ? (
<input
className="editableView-input"
- ref={r => { this._inputref = r; }} // prettier-ignore
+ ref={this.setInputRef}
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)}
@@ -270,7 +271,7 @@ export class EditableView extends ObservableReactComponent<EditableProps> {
) : (
<textarea
className="editableView-input"
- ref={r => { this._inputref = r; }} // prettier-ignore
+ ref={this.setInputRef}
style={{ display: this._props.display, overflow: 'auto', fontSize: this._props.fontSize, minHeight: `min(100%, ${(this._props.GetValue()?.split('\n').length || 1) * 15})`, minWidth: 20, background: this._props.background }}
placeholder={this._props.placeholder}
onBlur={e => this.finalizeEdit(e.currentTarget.value, false, true, false)}
diff --git a/src/client/views/FieldsDropdown.tsx b/src/client/views/FieldsDropdown.tsx
index e7ab6a180..0bdf92bbc 100644
--- a/src/client/views/FieldsDropdown.tsx
+++ b/src/client/views/FieldsDropdown.tsx
@@ -6,7 +6,7 @@
* this list is then pruned down to only include fields that are not marked in Documents.ts to be non-filterable
*/
-import { computed, makeObservable, observable, runInAction } from 'mobx';
+import { action, computed, makeObservable, observable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import Select from 'react-select';
@@ -24,6 +24,7 @@ interface fieldsDropdownProps {
placeholder?: string | (() => string);
showPlaceholder?: true; // if true, then input field always shows the placeholder value; otherwise, it shows the current selection
addedFields?: string[];
+ isInactive?: boolean;
}
@observer
@@ -57,8 +58,8 @@ 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)
- .forEach((pair: [string, FInfo]) => filteredOptions.push(pair[0]));
+ .filter(opts => opts[1] instanceof FInfo && opts[1].filterable)
+ .forEach((pair: [string, unknown]) => filteredOptions.push(pair[0]));
const options = filteredOptions.sort().map(facet => ({ value: facet, label: facet }));
return (
@@ -77,11 +78,13 @@ export class FieldsDropdown extends ObservableReactComponent<fieldsDropdownProps
...baseStyles,
color: SnappingManager.userColor,
background: SnappingManager.userBackgroundColor,
+ display: this._props.isInactive ? 'none' : undefined,
}),
placeholder: (baseStyles /* , state */) => ({
...baseStyles,
color: SnappingManager.userColor,
background: SnappingManager.userBackgroundColor,
+ display: this._props.isInactive ? 'none' : undefined,
}),
input: (baseStyles /* , state */) => ({
...baseStyles,
@@ -104,14 +107,12 @@ export class FieldsDropdown extends ObservableReactComponent<fieldsDropdownProps
options={options}
isMulti={false}
onChange={val => this._props.selectFunc((val as { value: string; label: string }).value)}
- onKeyDown={e => {
+ onKeyDown={action(e => {
if (e.key === 'Enter') {
- runInAction(() => {
- this._props.selectFunc((this._newField = (e.nativeEvent.target as HTMLSelectElement)?.value));
- });
+ this._props.selectFunc((this._newField = (e.nativeEvent.target as HTMLSelectElement)?.value));
}
e.stopPropagation();
- }}
+ })}
onMenuClose={this._props.menuClose}
closeMenuOnSelect
value={this._props.showPlaceholder ? null : undefined}
diff --git a/src/client/views/FilterPanel.scss b/src/client/views/FilterPanel.scss
index 508b1ee1f..e32db000f 100644
--- a/src/client/views/FilterPanel.scss
+++ b/src/client/views/FilterPanel.scss
@@ -1,4 +1,3 @@
-
.filterBox-flyout {
display: block;
text-align: left;
@@ -29,7 +28,7 @@
// .filterBox-bottom {
// // position: fixed;
-// // bottom: 0;
+// // bottom: 0px;
// // width: 100%;
// }
@@ -88,7 +87,7 @@
// padding-bottom: 20px;
// border-bottom: 2px solid black;
// position: fixed;
-// top: 0;
+// top: 0px;
// width: 100%;
// }
@@ -154,8 +153,8 @@
flex-direction: column;
width: 100%;
position: relative;
- right: 0;
- top: 0;
+ right: 0px;
+ top: 0px;
z-index: 1;
// background-color: #9f9f9f;
@@ -240,60 +239,52 @@
transition: all 0.3s ease-out;
display: flex;
flex-direction: row;
- padding: 5px;
-
+ padding: 5px;
- &:hover{
+ &:hover {
border-color: #e9e9e9;
- background-color: #6d6c6c
+ background-color: #6d6c6c;
}
- .hotKey-icon, .hotKey-close{
+ .hotKey-icon,
+ .hotKey-close {
background-color: transparent;
border-radius: 10%;
padding: 5px;
-
- &:hover{
+ &:hover {
background-color: #616060;
}
}
- .hotKey-close{
+ .hotKey-close {
right: 30px;
- position: fixed;
+ position: fixed;
padding-top: 10px;
-
-}
+ }
- .hotkey-title{
+ .hotkey-title {
top: 6px;
position: relative;
cursor: text;
-
}
- .hotkey-title-input{
+ .hotkey-title-input {
background-color: transparent;
border: none;
border-color: transparent;
outline: none;
cursor: text;
-
}
}
.hotKeyButtons {
position: relative;
width: 100%;
-
}
.hotKey-icon-button {
-
- background-color: transparent;
-
-
+ background-color: transparent;
}
.icon-panel {
@@ -305,24 +296,19 @@
border-radius: 10%;
background-color: #323232;
- .icon-panel-button{
+ .icon-panel-button {
background-color: #323232;
border-radius: 10%;
-
- &:hover{
- background-color:#7a7878
+ &:hover {
+ background-color: #7a7878;
}
}
-
-
-
}
-
// .sliderBox-outerDiv {
// width: 30%;// width: calc(100% - 14px); // 14px accounts for handles that are at the max value of the slider that would extend outside the box
-// height: 40; // height: 100%;
+// height: 40px; // height: 100%;
// border-radius: inherit;
// display: flex;
// flex-direction: column;
diff --git a/src/client/views/GestureOverlay.scss b/src/client/views/GestureOverlay.scss
index bfe2d5c64..0fa3fd973 100644
--- a/src/client/views/GestureOverlay.scss
+++ b/src/client/views/GestureOverlay.scss
@@ -3,7 +3,7 @@
height: 100%;
position: absolute;
touch-action: none;
- top: 0;
+ top: 0px;
.pointerBubbles {
width: 100%;
diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index 777a34ebc..8488c5293 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -3,7 +3,7 @@ import { action, computed, makeObservable, observable, runInAction } from 'mobx'
import { observer } from 'mobx-react';
import * as React from 'react';
import { setupMoveUpEvents } from '../../ClientUtils';
-import { emptyFunction, intersectRect } from '../../Utils';
+import { emptyFunction, intersectRect, rectContains } from '../../Utils';
import { Doc } from '../../fields/Doc';
import { InkData, InkField, InkTool } from '../../fields/InkField';
import { NumCast } from '../../fields/Types';
@@ -166,7 +166,7 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
*/
docsInBoundingBox = (boundingBox: { topLeft: number[]; bottomRight: number[] }, childDocs: Doc[]): Doc[] => {
const rect = { left: boundingBox.topLeft[0], top: boundingBox.topLeft[1], width: boundingBox.bottomRight[0] - boundingBox.topLeft[0], height: boundingBox.bottomRight[1] - boundingBox.topLeft[1] };
- return childDocs.filter(doc => intersectRect(rect, { left: NumCast(doc.x), top: NumCast(doc.y), width: NumCast(doc._width), height: NumCast(doc._height) }));
+ return childDocs.filter(doc => rectContains(rect, { left: NumCast(doc.x), top: NumCast(doc.y), width: NumCast(doc._width), height: NumCast(doc._height) }));
};
/**
* Determines if what the array of cusp/intersection data corresponds to a scribble.
diff --git a/src/client/views/InkStroke.scss b/src/client/views/InkStroke.scss
index c672824bf..0595283fc 100644
--- a/src/client/views/InkStroke.scss
+++ b/src/client/views/InkStroke.scss
@@ -8,8 +8,8 @@
svg:not(:root) {
overflow: visible !important;
position: absolute;
- left: 0;
- top: 0;
+ left: 0px;
+ top: 0px;
}
}
diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts
index 41f38c008..9f2c1fc4e 100644
--- a/src/client/views/InkStrokeProperties.ts
+++ b/src/client/views/InkStrokeProperties.ts
@@ -209,17 +209,18 @@ export class InkStrokeProperties {
* @param scrpt The center point of the rotation in screen coordinates
*/
rotateInk = undoable((inkStrokes: DocumentView[], angle: number, scrpt: PointData) => {
- this.applyFunction(inkStrokes, (view: DocumentView, ink: InkData, xScale: number, yScale: number /* , inkStrokeWidth: number */) => {
- const inkCenterPt = view.ComponentView?.ptFromScreen?.(scrpt);
- return !inkCenterPt
- ? ink
- : ink.map(i => {
- const pt = { X: i.X - inkCenterPt.X, Y: i.Y - inkCenterPt.Y };
- const newX = Math.cos(angle) * pt.X - (Math.sin(angle) * pt.Y * yScale) / xScale;
- const newY = (Math.sin(angle) * pt.X * xScale) / yScale + Math.cos(angle) * pt.Y;
- return { X: newX + inkCenterPt.X, Y: newY + inkCenterPt.Y };
- });
- });
+ angle &&
+ this.applyFunction(inkStrokes, (view: DocumentView, ink: InkData, xScale: number, yScale: number /* , inkStrokeWidth: number */) => {
+ const inkCenterPt = view.ComponentView?.ptFromScreen?.(scrpt);
+ return !inkCenterPt
+ ? ink
+ : ink.map(i => {
+ const pt = { X: i.X - inkCenterPt.X, Y: i.Y - inkCenterPt.Y };
+ const newX = Math.cos(angle) * pt.X - (Math.sin(angle) * pt.Y * yScale) / xScale;
+ const newY = (Math.sin(angle) * pt.X * xScale) / yScale + Math.cos(angle) * pt.Y;
+ return { X: newX + inkCenterPt.X, Y: newY + inkCenterPt.Y };
+ });
+ });
}, 'rotate ink');
/**
diff --git a/src/client/views/InkTangentHandles.tsx b/src/client/views/InkTangentHandles.tsx
index 577acc4d1..0af46df5d 100644
--- a/src/client/views/InkTangentHandles.tsx
+++ b/src/client/views/InkTangentHandles.tsx
@@ -105,7 +105,6 @@ export class InkTangentHandles extends React.Component<InkHandlesProps> {
return (
<>
{tangentHandles.map((pts, i) => (
- // eslint-disable-next-line react/no-array-index-key
<svg height="10" width="10" key={`hdl${i}`}>
<circle
cx={pts.X}
@@ -135,7 +134,6 @@ export class InkTangentHandles extends React.Component<InkHandlesProps> {
/>
);
return (
- // eslint-disable-next-line react/no-array-index-key
<svg height="100" width="100" key={`line${i}`}>
{tangentLine(pts.X1, pts.Y1, pts.X2, pts.Y2)}
{tangentLine(pts.X2, pts.Y2, pts.X3, pts.Y3)}
diff --git a/src/client/views/InkTranscription.tsx b/src/client/views/InkTranscription.tsx
index 2e6b477e9..32ca5f56b 100644
--- a/src/client/views/InkTranscription.tsx
+++ b/src/client/views/InkTranscription.tsx
@@ -4,8 +4,8 @@ import * as React from 'react';
import { imageUrlToBase64 } from '../../ClientUtils';
import { aggregateBounds } from '../../Utils';
import { Doc, DocListCast } from '../../fields/Doc';
-import { InkData, InkField, InkInkTool, InkTool } from '../../fields/InkField';
-import { Cast, DateCast, ImageCast, NumCast } from '../../fields/Types';
+import { InkData, InkInkTool, InkTool } from '../../fields/InkField';
+import { Cast, DateCast, ImageCast, InkCast, NumCast } from '../../fields/Types';
import { ImageField, URLField } from '../../fields/URLField';
import { gptHandwriting } from '../apis/gpt/GPT';
import { DocumentType } from '../documents/DocumentTypes';
@@ -30,9 +30,9 @@ export class InkTranscription extends React.Component {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@observable _textRef: any = undefined;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
- @observable iinkEditor: any = undefined;
+ @observable _iinkEditor: any = undefined;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
- private lastJiix: any;
+ @observable _iinkMathEditor: any = undefined;
private currGroup?: Doc;
private collectionFreeForm?: CollectionFreeFormView;
@@ -44,7 +44,7 @@ export class InkTranscription extends React.Component {
@action
// eslint-disable-next-line @typescript-eslint/no-explicit-any
setMathRef = async (r: any) => {
- if (!this._textRegister && r) {
+ if (!this._mathRegister && r) {
const options = {
configuration: {
server: {
@@ -55,19 +55,17 @@ export class InkTranscription extends React.Component {
protocol: 'WEBSOCKET',
},
recognition: {
- type: 'TEXT',
- lang: 'en_US',
- text: {
- mimeTypes: ['application/vnd.myscript.jiix'] as 'application/vnd.myscript.jiix'[],
+ type: 'MATH',
+ math: {
+ mimeTypes: ['application/x-latex'] as unknown as 'application/vnd.myscript.jiix'[],
},
},
},
};
- await iink.Editor.load(r, 'INKV2', options);
-
- this._textRegister = r;
+ this._iinkMathEditor = await iink.Editor.load(r, 'INKV2', options);
+ this._mathRegister = r;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
- r?.addEventListener('exported', (e: any) => this.exportInk(e, this._textRef));
+ r?.addEventListener('exported', (e: any) => this.exportInk(e));
return (this._textRef = r);
}
@@ -94,15 +92,17 @@ export class InkTranscription extends React.Component {
},
},
};
- this.iinkEditor = await iink.Editor.load(r, 'INKV2', options);
+ this._iinkEditor = await iink.Editor.load(r, 'INKV2', options);
this._textRegister = r;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
- r?.addEventListener('exported', (e: any) => this.exportInk(e, this._textRef));
+ r?.addEventListener('exported', (e: any) => this.exportInk(e));
return (this._textRef = r);
}
};
+ _ffview: CollectionFreeFormView | undefined;
+
/**
* Handles processing Dash Doc data for ink transcription.
*
@@ -110,7 +110,7 @@ export class InkTranscription extends React.Component {
* @param inkDocs the ink docs contained within the selected group
* @param math boolean whether to do math transcription or not
*/
- transcribeInk = (groupDoc: Doc | undefined, inkDocs: Doc[], math: boolean) => {
+ transcribeInk = (ffview: CollectionFreeFormView, groupDoc: Doc | undefined, inkDocs: Doc[], math: boolean) => {
if (!groupDoc) return;
const validInks = inkDocs.filter(s => s.type === DocumentType.INK);
@@ -118,20 +118,18 @@ export class InkTranscription extends React.Component {
const times: number[] = [];
validInks
- .filter(i => Cast(i[Doc.LayoutDataKey(i)], InkField))
+ .filter(i => InkCast(i[Doc.LayoutDataKey(i)]))
.forEach(i => {
- const d = Cast(i[Doc.LayoutDataKey(i)], InkField, null);
+ const d = InkCast(i[Doc.LayoutDataKey(i)])!;
+ const authorTime = DateCast(i.author_date)?.getDate().getTime() ?? 0;
const inkStroke = DocumentView.getDocumentView(i)?.ComponentView as InkingStroke;
strokes.push(d.inkData.map(pd => inkStroke.ptToScreen({ X: pd.X, Y: pd.Y })));
- times.push(DateCast(i.author_date).getDate().getTime());
+ times.push(authorTime);
});
+ this._ffview = ffview;
this.currGroup = groupDoc;
const pointerData = strokes.map((stroke, i) => this.inkJSON(stroke, times[i]));
- if (math) {
- this.iinkEditor.importPointEvents(pointerData);
- } else {
- this.iinkEditor.importPointEvents(pointerData);
- }
+ (math ? this._iinkMathEditor : this._iinkEditor).importPointEvents(pointerData);
};
convertPointsToString(points: InkData[]): string {
return points[0].map(point => `new Point(${point.X}, ${point.Y})`).join(',');
@@ -203,83 +201,92 @@ export class InkTranscription extends React.Component {
* @param ref the ref to the editor
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
- exportInk = async (e: any, ref: any) => {
- const exports = e.detail['application/vnd.myscript.jiix'];
- if (exports) {
- if (exports['type'] == 'Math') {
- const latex = exports['application/x-latex'];
- if (this.currGroup) {
- this.currGroup.text = latex;
- this.currGroup.title = latex;
- }
+ exportInk = (e: { detail: { [key: string]: string } }) => {
+ const exports = e.detail;
+ if (exports['application/x-latex']) {
+ const latex = exports['application/x-latex'];
+ if (this.currGroup) {
+ this.currGroup.text = latex;
+ this.currGroup.title = latex;
+ }
- ref.editor.clear();
- } else if (exports['type'] == 'Text') {
- if (exports['application/vnd.myscript.jiix']) {
- this.lastJiix = JSON.parse(exports['application/vnd.myscript.jiix']);
- // map timestamp to strokes
- const timestampWord = new Map<number, string>();
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- this.lastJiix.words.map((word: any) => {
- if (word.items) {
- word.items.forEach((i: { id: string; timestamp: string; X: Array<number>; Y: Array<number>; F: Array<number> }) => {
- const ms = Date.parse(i.timestamp);
- timestampWord.set(ms, word.label);
- });
- }
+ this._ffview?.addDocument(
+ Docs.Create.EquationDocument(latex, {
+ title: '',
+ x: this.currGroup?.x as number,
+ y: (this.currGroup?.y as number) + (this.currGroup?.height as number),
+ nativeHeight: 40,
+ _height: 50,
+ nativeWidth: 40,
+ _width: 50,
+ })
+ );
+ // this.showRecogBox(latex as string);
+ this._iinkMathEditor.clear();
+ } else if (exports['application/vnd.myscript.jiix']) {
+ const lastJiix = exports['application/vnd.myscript.jiix'] as unknown as { words: string[]; label: string };
+ // map timestamp to strokes
+ const timestampWord = new Map<number, string>();
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ lastJiix.words.map((word: any) => {
+ if (word.items) {
+ word.items.forEach((i: { id: string; timestamp: string; X: Array<number>; Y: Array<number>; F: Array<number> }) => {
+ const ms = Date.parse(i.timestamp);
+ timestampWord.set(ms, word.label);
});
-
- const wordInkDocMap = new Map<string, Doc[]>();
- if (this.currGroup) {
- const docList = DocListCast(this.currGroup.data);
- docList.forEach((inkDoc: Doc) => {
- // just having the times match up and be a unique value (actual timestamp doesn't matter)
- const ms = DateCast(inkDoc.author_date).getDate().getTime() + 14400000;
- const word = timestampWord.get(ms);
- if (!word) {
- return;
- }
- const entry = wordInkDocMap.get(word);
- if (entry) {
- entry.push(inkDoc);
- wordInkDocMap.set(word, entry);
- } else {
- const newEntry = [inkDoc];
- wordInkDocMap.set(word, newEntry);
- }
- });
- if (this.lastJiix.words.length > 1) this.subgroupsTranscriptions(wordInkDocMap);
- }
}
- const text = exports['label'];
+ });
- if (this.currGroup && text) {
- DocumentView.getDocumentView(this.currGroup)?.ComponentView?.updateIcon?.();
- const image = await this.getIcon();
- const { href } = (image as URLField).url;
- const hrefParts = href.split('.');
- const hrefComplete = `${hrefParts[0]}_o.${hrefParts[1]}`;
- let response;
- try {
- const hrefBase64 = await imageUrlToBase64(hrefComplete);
- response = await gptHandwriting(hrefBase64);
- } catch {
- console.error('Error getting image');
- }
- const textBoxText = 'iink: ' + text + '\n' + '\n' + 'ChatGPT: ' + response;
- this.currGroup.transcription = response;
- this.currGroup.title = response;
- if (!this.currGroup.hasTextBox) {
- const newDoc = Docs.Create.TextDocument(textBoxText, { title: '', x: this.currGroup.x as number, y: (this.currGroup.y as number) + (this.currGroup.height as number) });
- newDoc.height = 200;
- this.collectionFreeForm?.addDocument(newDoc);
- this.currGroup.hasTextBox = true;
+ const wordInkDocMap = new Map<string, Doc[]>();
+ if (this.currGroup) {
+ DocListCast(this.currGroup.data).forEach((inkDoc: Doc) => {
+ // just having the times match up and be a unique value (actual timestamp doesn't matter)
+ const ms = (DateCast(inkDoc.author_date)?.getDate().getTime() ?? 0) + 14400000;
+ const word = timestampWord.get(ms);
+ if (word) {
+ const entry = wordInkDocMap.get(word);
+ if (entry) {
+ entry.push(inkDoc);
+ wordInkDocMap.set(word, entry);
+ } else {
+ const newEntry = [inkDoc];
+ wordInkDocMap.set(word, newEntry);
+ }
}
- ref.editor.clear();
- }
+ });
+ if (lastJiix.words.length > 1) this.subgroupsTranscriptions(wordInkDocMap);
}
+ this.showRecogBox(lastJiix.label);
+ this._iinkEditor.clear();
}
};
+ private showRecogBox(text: string) {
+ if (this.currGroup) {
+ let response;
+ // DocumentView.getDocumentView(this.currGroup)?.ComponentView?.updateIcon?.();
+ // const image = await this.getIcon();
+ // const { href } = (image as URLField).url;
+ // const hrefParts = href.split('.');
+ // const hrefComplete = `${hrefParts[0]}_o.${hrefParts[1]}`;
+ // try {
+ // const hrefBase64 = await imageUrlToBase64(hrefComplete);
+ // response = await gptHandwriting(hrefBase64);
+ // } catch {
+ // console.error('Error getting image');
+ // }
+ const textBoxText = 'iink: ' + text + '\n' + '\n' + 'ChatGPT: ' + response;
+
+ this.currGroup.transcription = response;
+ this.currGroup.title = response;
+ if (!this.currGroup.hasTextBox) {
+ const newDoc = Docs.Create.TextDocument(textBoxText, { title: '', x: this.currGroup.x as number, y: (this.currGroup.y as number) + (this.currGroup.height as number) });
+ newDoc.height = 200;
+ this.collectionFreeForm?.addDocument(newDoc);
+ this.currGroup.hasTextBox = true;
+ }
+ }
+ }
+
/**
* gets the icon of the collection that was just made
* @returns the image of the collection
@@ -298,7 +305,7 @@ export class InkTranscription extends React.Component {
*/
createInkGroup() {
// TODO nda - if document being added to is a inkGrouping then we can just add to that group
- if (Doc.ActiveTool === InkTool.Ink && Doc.ActiveInk === InkInkTool.Write) {
+ if (Doc.ActiveTool === InkTool.Ink && [InkInkTool.Write, InkInkTool.Math].includes(Doc.ActiveInk)) {
CollectionFreeFormView.collectionsWithUnprocessedInk.forEach(ffView => {
// TODO: nda - will probably want to go through ffView unprocessed docs and then see if any of the inksToGroup docs are in it and only use those
const selected = ffView.unprocessedDocs;
@@ -308,7 +315,7 @@ export class InkTranscription extends React.Component {
);
ffView.unprocessedDocs = [];
- InkTranscription.Instance.transcribeInk(newCollection, selected, false);
+ InkTranscription.Instance.transcribeInk(ffView, newCollection, selected, Doc.ActiveInk === InkInkTool.Math);
});
}
CollectionFreeFormView.collectionsWithUnprocessedInk.clear();
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 253db08de..4b651af7d 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -124,7 +124,7 @@ export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>()
);
ffView.unprocessedDocs = [];
- InkTranscription.Instance.transcribeInk(newCollection, selected, false);
+ InkTranscription.Instance.transcribeInk(ffView, newCollection, selected, false);
}
};
diff --git a/src/client/views/LightboxView.scss b/src/client/views/LightboxView.scss
index 3e65843df..7a481d887 100644
--- a/src/client/views/LightboxView.scss
+++ b/src/client/views/LightboxView.scss
@@ -1,12 +1,12 @@
.lightboxView-navBtn {
margin: auto;
position: absolute;
- right: 19;
- top: 10;
+ right: 19px;
+ top: 10px;
background: transparent;
- border-radius: 8;
+ border-radius: 8px;
opacity: 0.7;
- width: 25;
+ width: 25px;
flex-direction: column;
display: flex;
&:hover {
@@ -16,12 +16,12 @@
.lightboxView-tabBtn {
margin: auto;
position: absolute;
- right: 54;
- top: 10;
+ right: 54px;
+ top: 10px;
background: transparent;
- border-radius: 8;
+ border-radius: 8px;
opacity: 0.7;
- width: 25;
+ width: 25px;
flex-direction: column;
display: flex;
&:hover {
@@ -31,12 +31,12 @@
.lightboxView-paletteBtn {
margin: auto;
position: absolute;
- right: 89;
- top: 10;
+ right: 89px;
+ top: 10px;
background: transparent;
- border-radius: 8;
+ border-radius: 8px;
opacity: 0.7;
- width: 25;
+ width: 25px;
flex-direction: column;
display: flex;
&:hover {
@@ -47,12 +47,12 @@
.lightboxView-penBtn {
margin: auto;
position: absolute;
- right: 124;
- top: 10;
+ right: 124px;
+ top: 10px;
background: transparent;
- border-radius: 8;
+ border-radius: 8px;
opacity: 0.7;
- width: 25;
+ width: 25px;
flex-direction: column;
display: flex;
&:hover {
@@ -62,12 +62,12 @@
.lightboxView-exploreBtn {
margin: auto;
position: absolute;
- right: 159;
- top: 10;
+ right: 159px;
+ top: 10px;
background: transparent;
- border-radius: 8;
+ border-radius: 8px;
opacity: 0.7;
- width: 25;
+ width: 25px;
flex-direction: column;
display: flex;
&:hover {
@@ -76,8 +76,8 @@
}
.lightboxView-frame {
position: absolute;
- top: 0;
- left: 0;
+ top: 0px;
+ left: 0px;
width: 100%;
height: 100%;
z-index: 1000;
@@ -91,9 +91,9 @@
margin: auto;
position: relative;
background: transparent;
- border-radius: 8;
+ border-radius: 8px;
opacity: 0.7;
- width: 35;
+ width: 35px;
&:hover {
opacity: 1;
}
diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx
index 0eb21b943..62b1180ec 100644
--- a/src/client/views/LightboxView.tsx
+++ b/src/client/views/LightboxView.tsx
@@ -244,6 +244,7 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
</div>
</div>
);
+ setStickerRef = (r: StickerPalette | null) => (this._annoPaletteView = r);
render() {
let downx = 0;
let downy = 0;
@@ -317,7 +318,7 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
</GestureOverlay>
</div>
- {this._showPalette && <StickerPalette ref={r => (this._annoPaletteView = r)} Doc={DocCast(Doc.UserDoc().myLightboxDrawings)} />}
+ {this._showPalette && DocCast(Doc.UserDoc().myLightboxDrawings) && <StickerPalette ref={this.setStickerRef} Doc={DocCast(Doc.UserDoc().myLightboxDrawings)!} />}
{this.renderNavBtn(0, undefined, this._props.PanelHeight / 2 - 12.5, 'chevron-left', this._doc && this._history.length ? true : false, this.previous)}
{this.renderNavBtn(
this._props.PanelWidth - Math.min(this._props.PanelWidth / 4, this._props.maxBorder[0]),
diff --git a/src/client/views/Main.scss b/src/client/views/Main.scss
index bea1de435..df4160fc1 100644
--- a/src/client/views/Main.scss
+++ b/src/client/views/Main.scss
@@ -12,10 +12,10 @@ body {
overflow: hidden;
font-family: global.$sans-serif;
font-size: global.$body-text;
- margin: 0;
+ margin: 0px;
position: absolute;
- top: 0;
- left: 0;
+ top: 0px;
+ left: 0px;
}
// div {
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index b884eb8c8..33e930abf 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -31,6 +31,8 @@ import './global/globalScripts';
import { AudioBox } from './nodes/AudioBox';
import { ComparisonBox } from './nodes/ComparisonBox';
import { DataVizBox } from './nodes/DataVizBox/DataVizBox';
+import { TemplateField } from './nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/TemplateField';
+import { TemplateFieldUtils } from './nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/TemplateFieldUtils';
import { DiagramBox } from './nodes/DiagramBox';
import { DocumentContentsView, HTMLtag } from './nodes/DocumentContentsView';
import { EquationBox } from './nodes/EquationBox';
@@ -48,6 +50,7 @@ import { PDFBox } from './nodes/PDFBox';
import { RecordingBox } from './nodes/RecordingBox';
import { ScreenshotBox } from './nodes/ScreenshotBox';
import { ScriptingBox } from './nodes/ScriptingBox';
+import { TaskBox } from './nodes/TaskBox';
import { VideoBox } from './nodes/VideoBox';
import { WebBox } from './nodes/WebBox';
import { CalendarBox } from './nodes/calendarBox/CalendarBox';
@@ -61,11 +64,11 @@ import { FootnoteView } from './nodes/formattedText/FootnoteView';
import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
import { SummaryView } from './nodes/formattedText/SummaryView';
import { ImportElementBox } from './nodes/importBox/ImportElementBox';
+import { ScrapbookBox } from './nodes/scrapbook/ScrapbookBox';
import { PresBox, PresSlideBox } from './nodes/trails';
import { FaceRecognitionHandler } from './search/FaceRecognitionHandler';
import { SearchBox } from './search/SearchBox';
import { StickerPalette } from './smartdraw/StickerPalette';
-import { ScrapbookBox } from './nodes/scrapbook/ScrapbookBox';
dotenv.config();
@@ -101,6 +104,7 @@ FieldLoader.ServerLoadStatus = { requested: 0, retrieved: 0, message: 'cache' };
new PingManager();
new KeyManager();
new FaceRecognitionHandler();
+ TemplateField.CreateField = TemplateFieldUtils.CreateField; // set the init function for fields
// initialize plugins and classes that require plugins
CollectionDockingView.Init(TabDocView);
@@ -120,6 +124,7 @@ FieldLoader.ServerLoadStatus = { requested: 0, retrieved: 0, message: 'cache' };
StickerPalette: StickerPalette,
FormattedTextBox,
DailyJournal, // AARAV
+ TaskBox, // AARAV
ImageBox,
FontIconBox,
LabelBox,
diff --git a/src/client/views/MainView.scss b/src/client/views/MainView.scss
index db949285b..d5e6b8998 100644
--- a/src/client/views/MainView.scss
+++ b/src/client/views/MainView.scss
@@ -10,7 +10,7 @@ body {
.dash-tooltip {
font-size: 11px;
padding: 2px;
- max-width: 150;
+ max-width: 150px;
line-height: 150%;
}
@@ -49,8 +49,8 @@ body {
.mainView-snapLines {
position: absolute;
- top: 0;
- left: 0;
+ top: 0px;
+ left: 0px;
width: 100%;
height: 100%;
pointer-events: none;
@@ -61,8 +61,8 @@ body {
height: 100%;
position: absolute;
pointer-events: all;
- top: 0;
- left: 0;
+ top: 0px;
+ left: 0px;
z-index: 1;
touch-action: none;
}
@@ -86,8 +86,8 @@ body {
.properties-container {
height: 100%;
position: absolute;
- right: 0;
- top: 0;
+ right: 0px;
+ top: 0px;
}
.mainView-propertiesDragger-minified,
@@ -98,7 +98,7 @@ body {
width: 17px;
position: absolute;
top: 50%;
- border-radius: 0;
+ border-radius: 0px;
border-top-left-radius: 10px;
border-bottom-left-radius: 10px;
border-right: unset;
@@ -141,7 +141,7 @@ body {
}
.propertiesView {
- left: 0;
+ left: 0px;
position: absolute;
z-index: 2;
// background-color: linen; //$light-gray;
@@ -165,7 +165,7 @@ body {
}
::-webkit-scrollbar {
- width: 0;
+ width: 0px;
}
}
@@ -173,19 +173,19 @@ body {
width: 100%;
height: 100%;
position: absolute;
- top: 0;
- left: 0;
+ top: 0px;
+ left: 0px;
overflow: hidden;
}
.buttonContainer {
position: absolute;
- bottom: 0;
+ bottom: 0px;
.mainView-settings {
// position: absolute;
- // left: 0;
- // bottom: 0;
+ // left: 0px;
+ // bottom: 0px;
border-radius: 25%;
margin-left: -5px;
background: darkblue;
@@ -198,8 +198,8 @@ body {
.mainView-logout {
position: absolute;
- right: 0;
- bottom: 0;
+ right: 0px;
+ bottom: 0px;
font-size: 8px;
}
@@ -223,12 +223,12 @@ body {
}
.mainView-libraryFlyout-close {
- right: 6;
- top: 5;
+ right: 6px;
+ top: 5px;
position: absolute;
margin-right: 6px;
z-index: 10;
- margin-bottom: 10;
+ margin-bottom: 10px;
}
}
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index c49b7e6de..867a5a304 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -730,7 +730,8 @@ export class MainView extends ObservableReactComponent<object> {
style={{
width: `calc(100% - ${this._leftMenuFlyoutWidth + this.leftMenuWidth() + this.propertiesWidth()}px)`,
minWidth: `calc(100% - ${this._leftMenuFlyoutWidth + this.leftMenuWidth() + this.propertiesWidth()}px)`,
- transform: DocumentView.LightboxDoc() ? 'scale(0.0001)' : undefined,
+ opacity: DocumentView.LightboxDoc() ? 0 : undefined,
+ pointerEvents: DocumentView.LightboxDoc() ? 'none' : undefined,
}}>
{!this.mainContainer ? null : this.mainDocView}
</div>
@@ -920,19 +921,19 @@ export class MainView extends ObservableReactComponent<object> {
);
}
+ setMainDashRef = (r: HTMLDivElement | null) =>
+ r &&
+ new ResizeObserver(
+ action(() => {
+ this._dashUIWidth = r.getBoundingClientRect().width;
+ this._dashUIHeight = r.getBoundingClientRect().height;
+ })
+ ).observe(r);
@computed get mainDashboardArea() {
return !this.userDoc ? null : (
<div
className="mainView-dashboardArea"
- ref={r => {
- r &&
- new ResizeObserver(
- action(() => {
- this._dashUIWidth = r.getBoundingClientRect().width;
- this._dashUIHeight = r.getBoundingClientRect().height;
- })
- ).observe(r);
- }}
+ ref={this.setMainDashRef}
style={{
color: 'black',
height: `calc(100% - ${this.topOfDashUI + this.topMenuHeight()}px)`,
@@ -1038,7 +1039,7 @@ export class MainView extends ObservableReactComponent<object> {
@computed get inkResources() {
return (
- <svg width={0} height={0}>
+ <svg width={0} height={0} style={{ display: 'block' }}>
<defs>
<filter id="inkSelectionHalo">
<feColorMatrix
@@ -1071,6 +1072,14 @@ export class MainView extends ObservableReactComponent<object> {
};
lightboxMaxBorder = [200, 50];
+ setMainViewRef = (r: HTMLDivElement | null) =>
+ r &&
+ new ResizeObserver(
+ action(() => {
+ this._windowWidth = r.getBoundingClientRect().width;
+ this._windowHeight = r.getBoundingClientRect().height;
+ })
+ ).observe(r);
render() {
return (
<div
@@ -1084,15 +1093,7 @@ export class MainView extends ObservableReactComponent<object> {
ele.scrollTop = ele.scrollLeft = 0;
})(document.getElementById('root')!)
}
- ref={r => {
- r &&
- new ResizeObserver(
- action(() => {
- this._windowWidth = r.getBoundingClientRect().width;
- this._windowHeight = r.getBoundingClientRect().height;
- })
- ).observe(r);
- }}>
+ ref={this.setMainViewRef}>
{this.inkResources}
<DictationOverlay />
<SharingManager />
diff --git a/src/client/views/MainViewModal.tsx b/src/client/views/MainViewModal.tsx
index b05292c47..d7640dc72 100644
--- a/src/client/views/MainViewModal.tsx
+++ b/src/client/views/MainViewModal.tsx
@@ -41,7 +41,7 @@ export class MainViewModal extends React.Component<MainViewOverlayProps> {
className="overlay"
onClick={this.props?.closeOnExternalClick}
style={{
- backgroundColor: isDark(SnappingManager.userColor) ? '#DFDFDF30' : '#32323230',
+ backgroundColor: isDark(SnappingManager.userColor ?? '') ? '#DFDFDF30' : '#32323230',
...(p.overlayStyle || {}),
}}
/>
diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx
index b2e42652d..d354dd2c0 100644
--- a/src/client/views/MarqueeAnnotator.tsx
+++ b/src/client/views/MarqueeAnnotator.tsx
@@ -194,14 +194,13 @@ export class MarqueeAnnotator extends ObservableReactComponent<MarqueeAnnotatorP
AnchorMenu.Instance.StartDrag = action((e: PointerEvent, ele: HTMLElement) => {
e.preventDefault();
e.stopPropagation();
- const sourceAnchorCreator = () => this.highlight(this.props.highlightDragSrcColor ?? 'rgba(173, 216, 230, 0.75)', true, undefined, true); // hyperlink color
-
const targetCreator = (annotationOn: Doc | undefined) => {
const target = DocUtils.GetNewTextDoc('Note linked to ' + this.props.Document.title, 0, 0, 100, 100, annotationOn, 'yellow');
target.layout_fitWidth = true;
DocumentView.SetSelectOnLoad(target);
return target;
};
+ const sourceAnchorCreator = () => this.highlight(this.props.highlightDragSrcColor ?? 'rgba(173, 216, 230, 0.75)', true, undefined, true); // hyperlink color
DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(this.props.docView(), sourceAnchorCreator, targetCreator), e.pageX, e.pageY, {
dragComplete: dragEv => {
if (!dragEv.aborted && dragEv.annoDragData && dragEv.annoDragData.linkSourceDoc && dragEv.annoDragData.dropDocument && dragEv.linkDocument) {
diff --git a/src/client/views/MetadataEntryMenu.scss b/src/client/views/MetadataEntryMenu.scss
index 28de0b7a5..924476a30 100644
--- a/src/client/views/MetadataEntryMenu.scss
+++ b/src/client/views/MetadataEntryMenu.scss
@@ -3,7 +3,7 @@
width: 310px;
flex-direction: column;
- input[type=checkbox] {
+ input[type='checkbox'] {
margin-left: 5px;
}
}
@@ -11,7 +11,7 @@
.metadataEntry-autoSuggester {
width: 80%;
height: 100%;
- margin: 0;
+ margin: 0px;
display: inline-block;
}
@@ -20,13 +20,13 @@
}
.metadataEntry-keys {
- max-height: 80;
- overflow-y: auto;
+ max-height: 80px;
+ overflow-y: auto;
display: flex;
flex-direction: column;
}
.metadataEntry-inputArea {
- display:inline-block;
+ display: inline-block;
flex-direction: row;
}
@@ -53,8 +53,8 @@
}
.react-autosuggest__input--open {
- border-bottom-left-radius: 0;
- border-bottom-right-radius: 0;
+ border-bottom-left-radius: 0px;
+ border-bottom-right-radius: 0px;
}
.react-autosuggest__suggestions-container {
@@ -78,8 +78,8 @@
}
.react-autosuggest__suggestions-list {
- margin: 0;
- padding: 0;
+ margin: 0px;
+ padding: 0px;
list-style-type: none;
}
@@ -90,4 +90,4 @@
.react-autosuggest__suggestion--highlighted {
background-color: #ddd;
-} \ No newline at end of file
+}
diff --git a/src/client/views/OverlayView.scss b/src/client/views/OverlayView.scss
index 2e8621b5b..44203e38f 100644
--- a/src/client/views/OverlayView.scss
+++ b/src/client/views/OverlayView.scss
@@ -1,7 +1,7 @@
.overlayView {
position: absolute;
pointer-events: none;
- top: 0;
+ top: 0px;
width: 100vw;
height: 100vh;
z-index: 2002; // shouold be greater than LightboxView's z-index so that link lines and the presentation mini player appear
@@ -14,8 +14,8 @@
overflow: hidden;
display: flex;
flex-direction: column;
- top: 0;
- left: 0;
+ top: 0px;
+ left: 0px;
pointer-events: all;
box-shadow: black 5px 5px 5px;
}
@@ -46,7 +46,7 @@
float: right;
height: 20px;
width: 20px;
- padding: 0;
+ padding: 0px;
background-color: inherit;
}
@@ -62,6 +62,6 @@
.overlayView-doc {
z-index: 9002; //so that it appears above chroma
position: absolute;
- top: 0;
- left: 0;
+ top: 0px;
+ left: 0px;
}
diff --git a/src/client/views/PreviewCursor.scss b/src/client/views/PreviewCursor.scss
index 82488c750..eef120c58 100644
--- a/src/client/views/PreviewCursor.scss
+++ b/src/client/views/PreviewCursor.scss
@@ -2,8 +2,8 @@
color: black;
position: absolute;
transform-origin: left top;
- top: 0;
- left: 0;
+ top: 0px;
+ left: 0px;
pointer-events: none;
opacity: 1;
z-index: 1001;
diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx
index ded342df0..2c84d7fe7 100644
--- a/src/client/views/PropertiesButtons.tsx
+++ b/src/client/views/PropertiesButtons.tsx
@@ -238,8 +238,8 @@ export class PropertiesButtons extends React.Component {
// on => `Display collection as a Group`,
// on => 'object-group',
// (dv, doc) => {
- // doc.isGroup = !doc.isGroup;
- // doc.forceActive = doc.isGroup;
+ // docfreeform_isGroup = !docfreeform_isGroup;
+ // doc.forceActive = docfreeform_isGroup;
// }
// );
// }
diff --git a/src/client/views/PropertiesSection.tsx b/src/client/views/PropertiesSection.tsx
index 12a46c7a4..9ea9c3a3d 100644
--- a/src/client/views/PropertiesSection.tsx
+++ b/src/client/views/PropertiesSection.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable react/require-default-props */
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { action, computed } from 'mobx';
import { observer } from 'mobx-react';
diff --git a/src/client/views/PropertiesView.scss b/src/client/views/PropertiesView.scss
index 280de4893..de3012948 100644
--- a/src/client/views/PropertiesView.scss
+++ b/src/client/views/PropertiesView.scss
@@ -5,7 +5,7 @@
}
.propertiesView-presentationTrails-title-icon {
position: absolute;
- right: 4;
+ right: 4px;
}
.propertiesView-palette {
cursor: pointer;
@@ -24,7 +24,7 @@
}
.propertiesView {
height: 100%;
- width: 250;
+ width: 250px;
font-family: 'Roboto';
font-size: 12px;
cursor: auto;
@@ -61,7 +61,7 @@
}
.propertiesView-info {
- margin-top: -5;
+ margin-top: -5px;
float: right;
font-size: 20;
path {
@@ -78,10 +78,10 @@
display: flex;
button {
- width: 15;
- height: 15;
- padding: 0;
- margin-top: -5;
+ width: 15px;
+ height: 15px;
+ padding: 0px;
+ margin-top: -5px;
}
}
@@ -89,8 +89,8 @@
display: flex;
button {
- width: 5;
- height: 5;
+ width: 5px;
+ height: 5px;
}
input {
@@ -171,16 +171,16 @@
display: flex;
button {
- width: 15;
- height: 15;
- padding: 0;
- margin-top: -5;
+ width: 15px;
+ height: 15px;
+ padding: 0px;
+ margin-top: -5px;
}
}
button {
- width: 5;
- height: 5;
+ width: 5px;
+ height: 5px;
}
input {
@@ -301,8 +301,8 @@
padding: 7px;
border-radius: 7px;
margin-right: 32px;
- width: 32;
- height: 32;
+ width: 32px;
+ height: 32px;
padding-top: 9px;
margin-left: 18px;
@@ -318,8 +318,8 @@
padding: 7px;
border-radius: 7px;
margin-right: 32px;
- width: 32;
- height: 32;
+ width: 32px;
+ height: 32px;
padding-top: 9px;
padding-left: 10px;
@@ -334,8 +334,8 @@
background-color: #333333;
padding: 7px;
border-radius: 7px;
- width: 32;
- height: 32;
+ width: 32px;
+ height: 32px;
padding-top: 9px;
padding-left: 10px;
@@ -410,7 +410,7 @@
.color-palette {
width: 160px;
- height: 360;
+ height: 360px;
}
.strokeAndFill {
@@ -465,7 +465,7 @@
.propertiesView-selectedList {
min-width: max-content;
width: 100%;
- max-height: 180;
+ max-height: 180px;
overflow: hidden;
overflow-y: scroll;
border-left: solid 1px darkgrey;
@@ -475,7 +475,7 @@
.selectedList-items {
font-size: 12;
font-weight: 300;
- margin-top: 1;
+ margin-top: 1px;
}
}
}
@@ -500,7 +500,7 @@
.width-range {
margin-right: 1px;
- margin-bottom: 6;
+ margin-bottom: 6px;
}
}
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index acf6f928a..06463b2a2 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -81,7 +81,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
}
@computed get selectedDoc() {
- return DocumentView.SelectedSchemaDoc() || this.selectedDocumentView?.Document || Doc.ActiveDashboard;
+ return DocumentView.SelectedSchemaDoc() ?? this.selectedDocumentView?.Document ?? Doc.ActiveDashboard;
}
@computed get selectedLink() {
@@ -89,7 +89,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
}
@computed get selectedLayoutDoc() {
- return DocumentView.SelectedSchemaDoc() || this.selectedDocumentView?.layoutDoc || Doc.ActiveDashboard;
+ return DocumentView.SelectedSchemaDoc() ?? this.selectedDocumentView?.layoutDoc ?? Doc.ActiveDashboard;
}
@computed get selectedDocumentView() {
return DocumentView.Selected().lastElement();
@@ -149,7 +149,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
return this.selectedDoc?.type === DocumentType.INK;
}
@computed get isGroup() {
- return this.selectedDoc?.isGroup;
+ return Doc.IsFreeformGroup(this.selectedDoc);
}
@computed get isStack() {
return [
diff --git a/src/client/views/SidebarAnnos.scss b/src/client/views/SidebarAnnos.scss
index abfd04f11..c2b9dcce5 100644
--- a/src/client/views/SidebarAnnos.scss
+++ b/src/client/views/SidebarAnnos.scss
@@ -2,7 +2,7 @@
position: absolute;
width: 100%;
height: 100%;
- right: 0;
+ right: 0px;
.sidebarAnnos-stacking {
width: 100%;
position: relative;
@@ -20,12 +20,12 @@
.sidebarAnnos-filterUser-active {
font-weight: bold;
font-size: 10px;
- padding-left: 5;
- padding-right: 5;
+ padding-left: 5px;
+ padding-right: 5px;
box-shadow: black 1px 1px 3px;
- border-radius: 5;
- margin: 2;
- height: 15;
+ border-radius: 5px;
+ margin: 2px;
+ height: 15px;
background-color: lightgrey;
}
.sidebarAnnos-filterUser,
diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx
index 573c28ccf..71b479a22 100644
--- a/src/client/views/SidebarAnnos.tsx
+++ b/src/client/views/SidebarAnnos.tsx
@@ -3,7 +3,7 @@ import { observer } from 'mobx-react';
import * as React from 'react';
import { ClientUtils, returnFalse, returnOne, returnZero } from '../../ClientUtils';
import { emptyFunction } from '../../Utils';
-import { Doc, DocListCast, Field, FieldResult, FieldType, StrListCast } from '../../fields/Doc';
+import { Doc, DocListCast, Field, FieldResult, FieldType, Opt, StrListCast } from '../../fields/Doc';
import { Id } from '../../fields/FieldSymbols';
import { List } from '../../fields/List';
import { RichTextField } from '../../fields/RichTextField';
@@ -19,6 +19,7 @@ import { StyleProp } from './StyleProp';
import { CollectionStackingView } from './collections/CollectionStackingView';
import { DocumentView } from './nodes/DocumentView';
import { FieldViewProps } from './nodes/FieldView';
+import { FocusViewOptions } from './nodes/FocusViewOptions';
interface ExtraProps {
fieldKey: string;
@@ -149,15 +150,33 @@ export class SidebarAnnos extends ObservableReactComponent<FieldViewProps & Extr
};
makeDocUnfiltered = (doc: Doc) => {
if (DocListCast(this._props.Doc[this.sidebarKey]).find(anno => Doc.AreProtosEqual(doc.layout_unrendered ? DocCast(doc.annotationOn) : doc, anno))) {
- if (this.childFilters()) {
+ if (this.childFilters().length) {
// if any child filters exist, get rid of them
this._props.layoutDoc._childFilters = new List<string>();
+ return true;
}
- return true;
}
return false;
};
+ public static getView(sidebar: SidebarAnnos | null, sidebarShown: boolean, toggleSidebar: () => void, doc: Doc, options: FocusViewOptions) {
+ if (!sidebarShown) {
+ options.didMove = true;
+ toggleSidebar();
+ }
+ options.didMove = sidebar?.makeDocUnfiltered(doc) || options.didMove;
+
+ if (!doc.hidden) {
+ if (!options.didMove && options.toggleTarget) {
+ options.toggleTarget = false;
+ options.didMove = doc.hidden = true;
+ }
+ } else {
+ options.didMove = !(doc.hidden = false);
+ }
+ return new Promise<Opt<DocumentView>>(res => DocumentView.addViewRenderedCb(doc, res));
+ }
+
get sidebarKey() {
return this._props.fieldKey + '_sidebar';
}
@@ -181,8 +200,8 @@ export class SidebarAnnos extends ObservableReactComponent<FieldViewProps & Extr
layout_showTitle = () => 'title';
setHeightCallback = (height: number) => this._props.setHeight?.(height + this.filtersHeight());
sortByLinkAnchorY = (a: Doc, b: Doc) => {
- const ay = Doc.Links(a).length && DocCast(Doc.Links(a)[0].link_anchor_1).y;
- const by = Doc.Links(b).length && DocCast(Doc.Links(b)[0].link_anchor_1).y;
+ const ay = Doc.Links(a).length && DocCast(Doc.Links(a)[0].link_anchor_1)?.y;
+ const by = Doc.Links(b).length && DocCast(Doc.Links(b)[0].link_anchor_1)?.y;
return NumCast(ay) - NumCast(by);
};
render() {
diff --git a/src/client/views/StyleProvider.scss b/src/client/views/StyleProvider.scss
index 99796f1fb..cbb1fd5d5 100644
--- a/src/client/views/StyleProvider.scss
+++ b/src/client/views/StyleProvider.scss
@@ -4,11 +4,11 @@
.styleProvider-lock {
z-index: 2; // has to be above title which is z-index 1
font-size: 10;
- width: 20;
- height: 20;
+ width: 20px;
+ height: 20px;
position: absolute;
- right: -20;
- top: 0;
+ right: -20px;
+ top: 0px;
background: black;
pointer-events: all;
opacity: 0.3;
@@ -20,10 +20,10 @@
cursor: default;
}
.styleProvider-filter {
- right: 20;
+ right: 20px;
.styleProvider-filterShift {
- left: 0;
- top: 0;
+ left: 0px;
+ top: 0px;
position: absolute;
}
.dropdown-container {
@@ -33,10 +33,10 @@
}
.styleProvider-paint-selected,
.styleProvider-paint {
- top: 15;
+ top: 15px;
}
.styleProvider-paint-selected {
- right: -40;
+ right: -40px;
}
.styleProvider-lock:hover,
.styleProvider-filter:hover {
@@ -45,8 +45,8 @@
.styleProvider-treeView-icon,
.styleProvider-treeView-icon-active {
- margin-left: 0;
- margin-right: 0;
+ margin-left: 0px;
+ margin-right: 0px;
}
.styleProvider-treeView-icon {
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index 9110e198f..b52f17102 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -154,7 +154,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
const highlightStyle = ['solid', 'dashed', 'solid', 'solid', 'solid'][highlightIndex];
if (highlightIndex) {
return {
- highlightStyle: doc.isGroup ? "dotted": highlightStyle,
+ highlightStyle: Doc.IsFreeformGroup(doc) ? "dotted": highlightStyle,
highlightColor,
highlightIndex,
highlightStroke: BoolCast(layoutDoc?.layout_isSvg),
@@ -284,7 +284,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
? SnappingManager.userBackgroundColor
: doc?.annotationOn
? '#00000010' // faint interior for collections on PDFs, images, etc
- : doc?.isGroup
+ : doc && Doc.IsFreeformGroup(doc)
? undefined
: doc?._type_collection === CollectionViewType.Stacking ?
(Colors.DARK_GRAY)
@@ -308,7 +308,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
doc?.layout_boxShadow,
doc?._type_collection === CollectionViewType.Pile
? '4px 4px 10px 2px'
- : lockedPosition() || doc?.isGroup || LayoutTemplateString
+ : lockedPosition() || (doc && Doc.IsFreeformGroup(doc)) || LayoutTemplateString
? undefined // groups have no drop shadow -- they're supposed to be "invisible". LayoutString's imply collection is being rendered as something else (e.g., title of a Slide)
: `${Colors.DARK_GRAY} ${StrCast(doc.layout_boxShadow, '0.2vw 0.2vw 0.8vw')}`
);
@@ -338,7 +338,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
if (SnappingManager.ExploreMode || doc?.layout_unrendered) return isInk() ? 'visiblePainted' : 'all';
if (pointerEvents?.() === 'none') return 'none';
if (opacity() === 0) return 'none';
- if (isGroupActive?.() ) return isInk() ? 'visiblePainted': (doc?.isGroup ) ? undefined: 'all';
+ if (isGroupActive?.()) return isInk() ? 'visiblePainted': doc && Doc.IsFreeformGroup(doc) ? undefined: 'all';
if (isDocumentActive?.()) return isInk() ? 'visiblePainted' : 'all';
return undefined; // fixes problem with tree view elements getting pointer events when the tree view is not active
case StyleProp.Decorations: {
diff --git a/src/client/views/StyleProviderQuiz.scss b/src/client/views/StyleProviderQuiz.scss
index 84b3f1fef..53ca34c1b 100644
--- a/src/client/views/StyleProviderQuiz.scss
+++ b/src/client/views/StyleProviderQuiz.scss
@@ -13,8 +13,8 @@
.check-icon {
position: absolute;
- right: 40;
- bottom: 10;
+ right: 40px;
+ bottom: 10px;
color: green;
display: inline-block;
font-size: 20px;
@@ -23,8 +23,8 @@
.redo-icon {
position: absolute;
- right: 10;
- bottom: 10;
+ right: 10px;
+ bottom: 10px;
color: black;
display: inline-block;
font-size: 20px;
diff --git a/src/client/views/UndoStack.tsx b/src/client/views/UndoStack.tsx
index 067020a62..3755b1ec1 100644
--- a/src/client/views/UndoStack.tsx
+++ b/src/client/views/UndoStack.tsx
@@ -9,6 +9,7 @@ import './UndoStack.scss';
@observer
export class UndoStack extends React.Component<object> {
+ setRef = (r: HTMLDivElement | null) => r?.scroll({ behavior: 'auto', top: (r?.scrollHeight ?? 0) + 20 });
render() {
const background = UndoManager.batchCounter.get() ? 'yellow' : SettingsManager.userVariantColor;
const color = UndoManager.batchCounter.get() ? 'black' : SettingsManager.userColor;
@@ -25,7 +26,7 @@ export class UndoStack extends React.Component<object> {
popup={
<div
className="undoStack-commandsContainer"
- ref={r => r?.scroll({ behavior: 'auto', top: (r?.scrollHeight ?? 0) + 20 })}
+ ref={this.setRef}
style={{
background,
color,
diff --git a/src/client/views/ViewBoxInterface.ts b/src/client/views/ViewBoxInterface.ts
index d8dab8e89..514dc4ae8 100644
--- a/src/client/views/ViewBoxInterface.ts
+++ b/src/client/views/ViewBoxInterface.ts
@@ -24,6 +24,9 @@ export abstract class ViewBoxInterface<P> extends ObservableReactComponent<React
promoteCollection?: () => void; // moves contents of collection to parent
hasChildDocs?: () => Doc[];
docEditorView?: () => void;
+ autoTag?: () => void; // auto tag the document
+ isOutpaintable?: () => boolean; // can document be resized and outpainted
+ showBorderRounding?: () => boolean; // can document borders be rounded
showSmartDraw?: (x: number, y: number, regenerate?: boolean) => void;
updateIcon?: (usePanelDimensions?: boolean) => Promise<void>; // updates the icon representation of the document
getAnchor?: (addAsAnnotation: boolean, pinData?: PinProps) => Doc; // returns an Anchor Doc that represents the current state of the doc's componentview (e.g., the current playhead location of a an audio/video box)
diff --git a/src/client/views/animationtimeline/Timeline.scss b/src/client/views/animationtimeline/Timeline.scss
index e1d3b190c..3a50183d2 100644
--- a/src/client/views/animationtimeline/Timeline.scss
+++ b/src/client/views/animationtimeline/Timeline.scss
@@ -112,8 +112,8 @@ $timelineDark: #77a1aa;
input {
position: absolute;
opacity: 0;
- height: 0;
- width: 0;
+ height: 0px;
+ width: 0px;
}
.round-toggle-slider {
diff --git a/src/client/views/animationtimeline/Timeline.tsx b/src/client/views/animationtimeline/Timeline.tsx
index 814e9a7a0..c2bd01334 100644
--- a/src/client/views/animationtimeline/Timeline.tsx
+++ b/src/client/views/animationtimeline/Timeline.tsx
@@ -533,6 +533,7 @@ export class Timeline extends ObservableReactComponent<FieldViewProps & { Doc: D
this._totalLength = RegionHelpers.convertPixelTime(this._time, 'mili', 'pixel', this._tickSpacing, this._tickIncrement);
};
+ setTrackRef = (r: Track) => this.mapOfTracks.push(r);
/**
* if you have any question here, just shoot me an email or text.
* basically the only thing you need to edit besides render methods in track (individual track lines) and keyframe (green region)
@@ -554,7 +555,7 @@ export class Timeline extends ObservableReactComponent<FieldViewProps & { Doc: D
{[...this.children, this._props.Doc].map(doc => (
<Track
key={doc[Id]}
- ref={ref => this.mapOfTracks.push(ref)}
+ ref={this.setTrackRef}
timeline={this}
animatedDoc={doc}
currentBarX={this._currentBarX}
diff --git a/src/client/views/animationtimeline/TimelineMenu.tsx b/src/client/views/animationtimeline/TimelineMenu.tsx
index 0d7873931..79283479c 100644
--- a/src/client/views/animationtimeline/TimelineMenu.tsx
+++ b/src/client/views/animationtimeline/TimelineMenu.tsx
@@ -1,5 +1,3 @@
-/* eslint-disable jsx-a11y/no-noninteractive-element-interactions */
-/* eslint-disable jsx-a11y/click-events-have-key-events */
import { IconLookup } from '@fortawesome/fontawesome-svg-core';
import { faChartLine, faClipboard } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
@@ -19,7 +17,7 @@ export class TimelineMenu extends React.Component {
@observable private _y = 0;
@observable private _currentMenu: JSX.Element[] = [];
- constructor(props: any) {
+ constructor(props: object) {
super(props);
makeObservable(this);
TimelineMenu.Instance = this;
diff --git a/src/client/views/animationtimeline/TimelineOverview.tsx b/src/client/views/animationtimeline/TimelineOverview.tsx
index 7bf685c9e..fff756980 100644
--- a/src/client/views/animationtimeline/TimelineOverview.tsx
+++ b/src/client/views/animationtimeline/TimelineOverview.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable react/no-unused-prop-types */
import { action, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
@@ -14,7 +13,7 @@ interface TimelineOverviewProps {
isAuthoring: boolean;
parent: Timeline;
changeCurrentBarX: (pixel: number) => void;
- movePanX: (pixel: number) => any;
+ movePanX: (pixel: number) => void;
time: number;
tickSpacing: number;
tickIncrement: number;
@@ -139,7 +138,7 @@ export class TimelineOverview extends React.Component<TimelineOverviewProps> {
const percentVisible = this.visibleTime / this.props.time;
const visibleBarWidth = percentVisible * this.activeOverviewWidth;
- const percentScrubberStart = this.currentX / this.props.time;
+ // const percentScrubberStart = this.currentX / this.props.time;
let scrubberStart = (this.props.currentBarX / this.props.totalLength) * this.activeOverviewWidth;
if (scrubberStart > this.activeOverviewWidth) scrubberStart = this.activeOverviewWidth;
diff --git a/src/client/views/collections/CollectionCardDeckView.scss b/src/client/views/collections/CollectionCardDeckView.scss
index e6cc398af..981e528cc 100644
--- a/src/client/views/collections/CollectionCardDeckView.scss
+++ b/src/client/views/collections/CollectionCardDeckView.scss
@@ -18,7 +18,7 @@
}
.collectionCardView-flashcardUI {
- top: 0;
+ top: 0px;
position: absolute;
width: 100%;
height: 100%;
@@ -33,7 +33,7 @@
}
.collectionCardView-cardSizeDragger {
position: absolute;
- top: 0;
+ top: 0px;
width: 28px;
height: 28px;
> svg {
diff --git a/src/client/views/collections/CollectionCarousel3DView.scss b/src/client/views/collections/CollectionCarousel3DView.scss
index 13e6b54c2..361d88cb6 100644
--- a/src/client/views/collections/CollectionCarousel3DView.scss
+++ b/src/client/views/collections/CollectionCarousel3DView.scss
@@ -12,7 +12,7 @@
position: absolute;
top: global.$CAROUSEL3D_TOP * 1%;
height: (global.$CAROUSEL3D_SIDE_SCALE * 100) * 1%;
- align-items: center;
+ //align-items: center;
transition: transform 0.3s cubic-bezier(0.455, 0.03, 0.515, 0.955);
.collectionCarousel3DView-item,
@@ -67,8 +67,8 @@
.carousel3DView-fwd-scroll-hidden {
position: absolute;
display: flex;
- width: 30;
- height: 30;
+ width: 30px;
+ height: 30px;
align-items: center;
border-radius: 5px;
justify-content: center;
@@ -78,7 +78,7 @@
.carousel3DView-fwd,
.carousel3DView-back {
- top: 0;
+ top: 0px;
background: transparent;
width: calc((1 - #{global.$CAROUSEL3D_CENTER_SCALE} * 0.33) / 2 * 100%);
height: 100%;
@@ -94,13 +94,13 @@
.carousel3DView-fwd,
.carousel3DView-fwd-scroll,
.carousel3DView-fwd-scroll-hidden {
- right: 0;
+ right: 0px;
}
.carousel3DView-back,
.carousel3DView-back-scroll,
.carousel3DView-back-scroll-hidden {
- left: 0;
+ left: 0px;
}
.carousel3DView-fwd-scroll-hidden,
diff --git a/src/client/views/collections/CollectionCarouselView.scss b/src/client/views/collections/CollectionCarouselView.scss
index 962b590c8..4c999b6dd 100644
--- a/src/client/views/collections/CollectionCarouselView.scss
+++ b/src/client/views/collections/CollectionCarouselView.scss
@@ -6,10 +6,10 @@
transform-origin: top left;
.collectionCarouselView-caption {
- height: 50;
+ height: 50px;
display: inline-block;
width: 100%;
- bottom: 0;
+ bottom: 0px;
position: absolute;
}
.collectionCarouselView-image {
@@ -18,8 +18,8 @@
width: 100%;
user-select: none;
position: absolute;
- top: 0;
- left: 0;
+ top: 0px;
+ left: 0px;
}
}
.collectionCarouselView-recentlyMissed {
@@ -34,8 +34,8 @@
.carouselView-fwd {
position: absolute;
display: flex;
- width: 30;
- height: 30;
+ width: 30px;
+ height: 30px;
align-items: center;
border-radius: 5px;
justify-content: center;
@@ -47,12 +47,12 @@
}
.carouselView-fwd {
top: calc(50% - 15px);
- right: 0;
+ right: 0px;
transform-origin: right top;
}
.carouselView-back {
top: calc(50% - 15px);
- left: 0;
+ left: 0px;
transform-origin: top left;
}
.carouselView-back:hover,
diff --git a/src/client/views/collections/CollectionDockingView.scss b/src/client/views/collections/CollectionDockingView.scss
index 7c19d39da..de214e2ef 100644
--- a/src/client/views/collections/CollectionDockingView.scss
+++ b/src/client/views/collections/CollectionDockingView.scss
@@ -17,8 +17,8 @@
}
.lm_maximised {
position: absolute;
- top: 0;
- left: 0;
+ top: 0px;
+ left: 0px;
z-index: 40;
}
.lm_maximise_placeholder {
@@ -62,8 +62,8 @@
text-align: center;
}
.lm_header ul {
- margin: 0;
- padding: 0;
+ margin: 0px;
+ padding: 0px;
list-style-type: none;
}
.lm_header .lm_tab {
@@ -81,11 +81,11 @@
position: absolute;
}
.lm_header .lm_tab i.lm_left {
- top: 0;
+ top: 0px;
left: -2px;
}
.lm_header .lm_tab i.lm_right {
- top: 0;
+ top: 0px;
right: -2px;
}
.lm_header .lm_tab .lm_title {
@@ -97,8 +97,8 @@
width: 14px;
height: 14px;
position: absolute;
- top: 0;
- right: 0;
+ top: 0px;
+ right: 0px;
text-align: center;
}
.lm_stack.lm_left .lm_header,
@@ -118,14 +118,14 @@
.lm_stack.lm_left .lm_header .lm_tabs,
.lm_stack.lm_right .lm_header .lm_tabs {
transform-origin: left top;
- top: 0;
+ top: 0px;
width: 1000px;
}
.lm_dragProxy.lm_left .lm_header .lm_controls,
.lm_dragProxy.lm_right .lm_header .lm_controls,
.lm_stack.lm_left .lm_header .lm_controls,
.lm_stack.lm_right .lm_header .lm_controls {
- bottom: 0;
+ bottom: 0px;
}
.lm_dragProxy.lm_left .lm_items,
.lm_dragProxy.lm_right .lm_items,
@@ -136,7 +136,7 @@
.lm_dragProxy.lm_left .lm_header .lm_tabs,
.lm_stack.lm_left .lm_header .lm_tabs {
transform: rotate(-90deg) scaleX(-1);
- left: 0;
+ left: 0px;
}
.lm_dragProxy.lm_left .lm_header .lm_tabs .lm_tab,
.lm_stack.lm_left .lm_header .lm_tabs .lm_tab {
@@ -156,7 +156,7 @@
.lm_stack.lm_right .lm_header .lm_tabs {
transform: rotate(90deg) scaleX(1);
left: 100%;
- margin-left: 0;
+ margin-left: 0px;
}
.lm_dragProxy.lm_right .lm_header .lm_controls,
.lm_stack.lm_right .lm_header .lm_controls {
@@ -169,7 +169,7 @@
}
.lm_dragProxy.lm_bottom .lm_header .lm_tab,
.lm_stack.lm_bottom .lm_header .lm_tab {
- margin-top: 0;
+ margin-top: 0px;
border-top: none;
}
.lm_dragProxy.lm_bottom .lm_header .lm_controls,
@@ -189,8 +189,8 @@
}
.lm_header .lm_controls .lm_tabdropdown:before {
content: '';
- width: 0;
- height: 0;
+ width: 0px;
+ height: 0px;
vertical-align: middle;
display: inline-block;
border-top: 5px dashed;
@@ -201,14 +201,14 @@
.lm_header .lm_tabdropdown_list {
position: absolute;
top: 20px;
- right: 0;
+ right: 0px;
z-index: 5;
overflow: hidden;
}
.lm_header .lm_tabdropdown_list .lm_tab {
clear: both;
padding-right: 10px;
- margin: 0;
+ margin: 0px;
}
.lm_header .lm_tabdropdown_list .lm_tab .lm_title {
width: 100px;
@@ -218,8 +218,8 @@
}
.lm_dragProxy {
position: absolute;
- top: 0;
- left: 0;
+ top: 0px;
+ left: 0px;
z-index: 30;
}
.lm_dragProxy .lm_header {
@@ -238,32 +238,32 @@
width: 100%;
height: 100%;
position: relative;
- top: 0;
- left: 0;
+ top: 0px;
+ left: 0px;
}
.lm_transition_indicator {
display: none;
width: 20px;
height: 20px;
position: absolute;
- top: 0;
- left: 0;
+ top: 0px;
+ left: 0px;
z-index: 20;
}
.lm_popin {
width: 20px;
height: 20px;
position: absolute;
- bottom: 0;
- right: 0;
+ bottom: 0px;
+ right: 0px;
z-index: 9999;
}
.lm_popin > * {
width: 100%;
height: 100%;
position: absolute;
- top: 0;
- left: 0;
+ top: 0px;
+ left: 0px;
}
.lm_popin > .lm_bg {
z-index: 10;
@@ -307,7 +307,7 @@
width: max-content;
height: 100%;
display: flex;
- max-width: 100;
+ max-width: 100px;
text-overflow: ellipsis;
}
@@ -328,7 +328,7 @@
ul.lm_tabs::before {
content: ' ';
position: absolute;
- bottom: 0;
+ bottom: 0px;
width: 100%;
z-index: 1;
pointer-events: none;
@@ -349,9 +349,9 @@ ul.lm_tabs::before {
}
}
.lm_header .lm_tab.lm_active {
- padding: 0;
+ padding: 0px;
opacity: 1;
- margin: 0;
+ margin: 0px;
box-shadow: none;
height: 27px;
margin-right: 2px;
@@ -405,7 +405,7 @@ ul.lm_tabs::before {
}
.lm_drag_tab {
- padding: 0;
+ padding: 0px;
width: 15px !important;
height: 15px !important;
position: relative !important;
@@ -418,7 +418,7 @@ ul.lm_tabs::before {
.lm_close_tab {
display: inline-flex !important;
- padding: 0;
+ padding: 0px;
opacity: 1 !important;
align-self: center;
margin-right: 5px;
@@ -455,7 +455,7 @@ ul.lm_tabs::before {
content: 'x';
margin: auto;
position: relative;
- top: -2;
+ top: -2px;
font-size: medium;
font-family: sans-serif;
}
@@ -478,8 +478,8 @@ ul.lm_tabs::before {
width: 100%;
height: 100%;
position: absolute;
- top: 0;
- left: 0;
+ top: 0px;
+ left: 0px;
.collectionDockingView-drag {
touch-action: none;
@@ -514,7 +514,7 @@ ul.lm_tabs::before {
border-bottom-left-radius: 10px;
background: #93939347;
z-index: 100;
- //left: -3;
+ //left: -3px;
&:hover {
background: gray;
color: white !important;
@@ -524,7 +524,7 @@ ul.lm_tabs::before {
content: '+';
margin: auto;
font-size: x-large;
- top: -4;
+ top: -4px;
position: relative;
}
.lm_maximise {
@@ -548,10 +548,10 @@ ul.lm_tabs::before {
}
.flexlayout__layout {
- left: 0;
- top: 0;
- right: 0;
- bottom: 0;
+ left: 0px;
+ top: 0px;
+ right: 0px;
+ bottom: 0px;
position: absolute;
overflow: hidden;
}
@@ -691,8 +691,8 @@ ul.lm_tabs::before {
.flexlayout__tabset_header {
position: absolute;
- left: 0;
- right: 0;
+ left: 0px;
+ right: 0px;
color: #eee;
background-color: #212121;
padding: 3px 3px 3px 5px;
@@ -702,17 +702,17 @@ ul.lm_tabs::before {
.flexlayout__tab_header_inner {
position: absolute;
- left: 0;
- top: 0;
- bottom: 0;
+ left: 0px;
+ top: 0px;
+ bottom: 0px;
width: 10000px;
}
.flexlayout__tab_header_outer {
background-color: global.$dark-gray;
position: absolute;
- left: 0;
- right: 0;
+ left: 0px;
+ right: 0px;
/*top: 0px;*/
/*height: 100px;*/
overflow: hidden;
@@ -731,23 +731,23 @@ ul.lm_tabs::before {
display: flex;
flex-direction: row-reverse;
align-items: center;
- top: 0;
- bottom: 0;
- right: 0;
+ top: 0px;
+ bottom: 0px;
+ right: 0px;
}
.flexlayout__tab_toolbar_button-min {
width: 20px;
height: 20px;
border: none;
- outline-width: 0;
+ outline-width: 0px;
}
.flexlayout__tab_toolbar_button-max {
width: 20px;
height: 20px;
border: none;
- outline-width: 0;
+ outline-width: 0px;
}
.flexlayout__popup_menu_item {
@@ -870,9 +870,9 @@ ul.lm_tabs::before {
display: flex;
flex-direction: column-reverse;
align-items: center;
- bottom: 0;
- left: 0;
- right: 0;
+ bottom: 0px;
+ left: 0px;
+ right: 0px;
}
.flexlayout__border_toolbar_right {
@@ -880,9 +880,9 @@ ul.lm_tabs::before {
display: flex;
flex-direction: column-reverse;
align-items: center;
- bottom: 0;
- left: 0;
- right: 0;
+ bottom: 0px;
+ left: 0px;
+ right: 0px;
}
.flexlayout__border_toolbar_top {
@@ -890,9 +890,9 @@ ul.lm_tabs::before {
display: flex;
flex-direction: row-reverse;
align-items: center;
- top: 0;
- bottom: 0;
- right: 0;
+ top: 0px;
+ bottom: 0px;
+ right: 0px;
}
.flexlayout__border_toolbar_bottom {
@@ -900,8 +900,8 @@ ul.lm_tabs::before {
display: flex;
flex-direction: row-reverse;
align-items: center;
- top: 0;
- bottom: 0;
- right: 0;
+ top: 0px;
+ bottom: 0px;
+ right: 0px;
}
}
diff --git a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
index 89ccf5a0f..164c6e831 100644
--- a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
+++ b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
@@ -7,6 +7,7 @@ import { emptyFunction, numberRange } from '../../../Utils';
import { Doc } from '../../../fields/Doc';
import { PastelSchemaPalette, SchemaHeaderField } from '../../../fields/SchemaHeaderField';
import { ScriptField } from '../../../fields/ScriptField';
+import { StrCast } from '../../../fields/Types';
import { Docs } from '../../documents/Documents';
import { DragManager } from '../../util/DragManager';
import { CompileScript } from '../../util/Scripting';
@@ -16,6 +17,7 @@ import { undoBatch, undoable } from '../../util/UndoManager';
import { EditableView } from '../EditableView';
import { ObservableReactComponent } from '../ObservableReactComponent';
import { DocumentView } from '../nodes/DocumentView';
+import { ImportElementBox } from '../nodes/importBox/ImportElementBox';
import { CollectionStackingView } from './CollectionStackingView';
import './CollectionStackingView.scss';
@@ -28,12 +30,14 @@ interface CMVFieldRowProps {
headingObject: SchemaHeaderField | undefined;
docList: Doc[];
parent: CollectionStackingView;
+ panelWidth: () => number;
+ columnWidth: () => number;
pivotField: string;
type: 'string' | 'number' | 'bigint' | 'boolean' | 'symbol' | 'undefined' | 'object' | 'function' | undefined;
createDropTarget: (ele: HTMLDivElement) => void;
screenToLocalTransform: () => Transform;
setDocHeight: (key: string, thisHeight: number) => void;
- refList: Element[];
+ sectionRefs: Element[];
showHandle: boolean;
}
@@ -74,7 +78,7 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent<CMVF
createRowDropRef = (ele: HTMLDivElement | null) => {
this._dropDisposer?.();
if (ele) this._dropDisposer = DragManager.MakeDropTarget(ele, this.rowDrop.bind(this), this._props.Doc);
- else if (this._ele) this.props.refList.splice(this.props.refList.indexOf(this._ele), 1);
+ else if (this._ele) this.props.sectionRefs.splice(this.props.sectionRefs.indexOf(this._ele), 1);
this._ele = ele;
};
@action
@@ -82,10 +86,10 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent<CMVF
this.heading = this._props.headingObject?.heading || '';
this.color = this._props.headingObject?.color || '#f1efeb';
this.collapsed = this._props.headingObject?.collapsed || false;
- this._ele && this.props.refList.push(this._ele);
+ this._ele && this.props.sectionRefs.push(this._ele);
}
componentWillUnmount() {
- this._ele && this.props.refList.splice(this.props.refList.indexOf(this._ele), 1);
+ this._ele && this.props.sectionRefs.splice(this.props.sectionRefs.indexOf(this._ele), 1);
this._ele = null;
}
@@ -128,10 +132,8 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent<CMVF
const key = this._props.pivotField;
const castedValue = this.getValue(value);
if (castedValue) {
- if (this._props.parent.colHeaderData) {
- if (this._props.parent.colHeaderData.map(i => i.heading).indexOf(castedValue.toString()) > -1) {
- return false;
- }
+ if (this._props.parent.colHeaderData?.map(i => i.heading).indexOf(castedValue.toString()) || 0 > -1) {
+ return false;
}
key && this._props.docList.forEach(d => Doc.SetInPlace(d, key, castedValue, true));
this._heading = castedValue.toString();
@@ -251,20 +253,11 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent<CMVF
textCallback = (/* char: string */) => this.addDocument('', false);
@computed get contentLayout() {
- const rows = Math.max(1, Math.min(this._props.docList.length, Math.floor((this._props.parent._props.PanelWidth() - 2 * this._props.parent.xMargin) / (this._props.parent.columnWidth + this._props.parent.gridGap))));
- const showChrome = !this._props.chromeHidden;
- const stackPad = showChrome ? `0px ${this._props.parent.xMargin}px` : `${this._props.parent.yMargin}px ${this._props.parent.xMargin}px 0px ${this._props.parent.xMargin}px `;
+ const rows = Math.max(1, Math.min(this._props.docList.length, Math.floor(this._props.panelWidth() / this._props.columnWidth())));
return this.collapsed ? null : (
<div style={{ position: 'relative' }}>
- {showChrome ? (
- <div
- className="collectionStackingView-addDocumentButton"
- style={
- {
- // width: style.columnWidth / style.numGroupColumns,
- // padding: `${NumCast(this._props.parent.layoutDoc._yPadding, this._props.parent.yMargin)}px 0px 0px 0px`,
- }
- }>
+ {!this._props.chromeHidden && !StrCast(this._props.Doc.childLayoutString).includes(ImportElementBox.name) ? (
+ <div className="collectionStackingView-addDocumentButton">
<EditableView GetValue={returnEmptyString} SetValue={this.addDocument} textCallback={this.textCallback} contents="+ NEW" />
</div>
) : null}
@@ -272,11 +265,9 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent<CMVF
className="collectionStackingView-masonryGrid"
ref={this._contRef}
style={{
- padding: stackPad,
minHeight: this._props.showHandle && this._props.parent._props.isContentActive() ? '10px' : undefined,
- width: this._props.parent.NodeWidth,
gridGap: this._props.parent.gridGap,
- gridTemplateColumns: numberRange(rows).reduce(list => list + ` ${this._props.parent.columnWidth}px`, ''),
+ gridTemplateColumns: numberRange(rows).reduce(list => list + ` ${this._props.columnWidth()}px`, ''),
}}>
{this._props.parent.children(this._props.docList)}
</div>
@@ -339,7 +330,12 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent<CMVF
render() {
const background = this._background;
return (
- <div className="collectionStackingView-masonrySection" style={{ width: this._props.parent.NodeWidth, background }} ref={this.createRowDropRef} onPointerEnter={this.pointerEnteredRow} onPointerLeave={this.pointerLeaveRow}>
+ <div
+ className="collectionStackingView-masonrySection"
+ style={{ width: this._props.pivotField ? this._props.panelWidth() : '100%', background }}
+ ref={this.createRowDropRef}
+ onPointerEnter={this.pointerEnteredRow}
+ onPointerLeave={this.pointerLeaveRow}>
{this.headingView}
{this.contentLayout}
</div>
diff --git a/src/client/views/collections/CollectionNoteTakingView.scss b/src/client/views/collections/CollectionNoteTakingView.scss
index 0d24a56b5..231085338 100644
--- a/src/client/views/collections/CollectionNoteTakingView.scss
+++ b/src/client/views/collections/CollectionNoteTakingView.scss
@@ -82,7 +82,7 @@
height: 100%;
width: 100%;
position: absolute;
- top: 0;
+ top: 0px;
overflow-y: auto;
overflow-x: hidden;
transition: top 0.5s;
@@ -130,8 +130,8 @@
display: flex;
flex-direction: column;
align-items: center;
- top: 0;
- left: 0;
+ top: 0px;
+ left: 0px;
width: 100%;
position: absolute;
margin: auto;
@@ -152,20 +152,20 @@
}
.collectionNoteTakingView-columnDragger {
- width: 15;
- height: 15;
+ width: 15px;
+ height: 15px;
position: absolute;
- margin-left: -5;
+ margin-left: -5px;
}
.collectionNoteTakingView-sectionDelete {
display: none;
position: absolute;
- right: 0;
+ right: 0px;
width: max-content;
height: max-content;
- top: 10;
- padding: 2;
+ top: 10px;
+ padding: 2px;
}
// Documents in NoteTaking view
@@ -210,8 +210,8 @@
height: 5px;
&.active {
- margin-left: 0;
- margin-right: 0;
+ margin-left: 0px;
+ margin-right: 0px;
background: red;
}
}
@@ -303,8 +303,8 @@
.collectionNoteTakingView-sectionColor {
position: absolute;
- left: 0;
- top: 0;
+ left: 0px;
+ top: 0px;
height: 100%;
display: none;
@@ -345,8 +345,8 @@
.collectionNoteTakingView-sectionOptions {
position: absolute;
- right: 0;
- top: 0;
+ right: 0px;
+ top: 0px;
height: 100%;
display: none;
diff --git a/src/client/views/collections/CollectionNoteTakingView.tsx b/src/client/views/collections/CollectionNoteTakingView.tsx
index 7f639a11e..7a4408931 100644
--- a/src/client/views/collections/CollectionNoteTakingView.tsx
+++ b/src/client/views/collections/CollectionNoteTakingView.tsx
@@ -256,9 +256,10 @@ export class CollectionNoteTakingView extends CollectionSubView() {
const height = () => this.getDocHeight(doc);
let dref: Opt<DocumentView>;
const noteTakingDocTransform = () => this.getDocTransform(doc, dref);
+ const setRef = (r: DocumentView | null) => (dref = r || undefined);
return (
<DocumentView
- ref={r => (dref = r || undefined)}
+ ref={setRef}
Document={doc}
TemplateDataDocument={doc.isTemplateDoc || doc.isTemplateForField ? this._props.TemplateDataDocument : undefined}
pointerEvents={this.blockPointerEventsWhenDragging}
diff --git a/src/client/views/collections/CollectionPivotView.tsx b/src/client/views/collections/CollectionPivotView.tsx
index 4736070c3..5487315f0 100644
--- a/src/client/views/collections/CollectionPivotView.tsx
+++ b/src/client/views/collections/CollectionPivotView.tsx
@@ -102,13 +102,7 @@ export class CollectionPivotView extends CollectionSubView() {
<div className="collectionTimeView-pivot" style={{ width: this._props.PanelWidth(), height: '100%' }}>
{this.contents}
<div style={{ right: 0, top: 0, position: 'absolute' }}>
- <FieldsDropdown
- Doc={this.Document}
- selectFunc={fieldKey => {
- this.layoutDoc._pivotField = fieldKey;
- }}
- placeholder={StrCast(this.layoutDoc._pivotField)}
- />
+ <FieldsDropdown Doc={this.Document} isInactive={!this._props.isContentActive()} selectFunc={fieldKey => (this.layoutDoc._pivotField = fieldKey)} placeholder={StrCast(this.layoutDoc._pivotField)} />
</div>
</div>
);
diff --git a/src/client/views/collections/CollectionStackedTimeline.scss b/src/client/views/collections/CollectionStackedTimeline.scss
index d05c0ffde..e71df2164 100644
--- a/src/client/views/collections/CollectionStackedTimeline.scss
+++ b/src/client/views/collections/CollectionStackedTimeline.scss
@@ -38,7 +38,7 @@
height: 100%;
background-color: global.$dark-gray;
opacity: 0.3;
- top: 0;
+ top: 0px;
}
.collectionStackedTimeline-trim-controls {
@@ -49,8 +49,8 @@
display: flex;
justify-content: space-between;
max-width: 100%;
- top: 0;
- left: 0;
+ top: 0px;
+ left: 0px;
.collectionStackedTimeline-trim-handle {
background-color: global.$medium-blue;
@@ -106,18 +106,18 @@
.collectionStackedTimeline-resizer {
background: global.$dark-gray;
position: absolute;
- top: 0;
+ top: 0px;
height: 100%;
width: 10px;
pointer-events: all;
z-index: 100;
}
.collectionStackedTimeline-resizer {
- right: 0;
+ right: 0px;
cursor: e-resize;
}
.collectionStackedTimeline-left-resizer {
- left: 0;
+ left: 0px;
cursor: w-resize;
}
}
@@ -126,8 +126,8 @@
position: absolute;
width: 100%;
height: 100%;
- top: 0;
- left: 0;
+ top: 0px;
+ left: 0px;
pointer-events: none;
}
}
diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss
index 05ac52ff9..2cf361847 100644
--- a/src/client/views/collections/CollectionStackingView.scss
+++ b/src/client/views/collections/CollectionStackingView.scss
@@ -2,10 +2,12 @@
.collectionMasonryView {
display: inline;
+ flex-wrap: wrap;
}
.collectionStackingView {
display: flex;
+ justify-content: space-between;
}
.collectionStackingMasonry-cont {
@@ -14,10 +16,10 @@
width: 100%;
.collectionStackingView-columnDragger {
- width: 28;
- height: 28;
- position: absolute;
- margin-left: -5;
+ width: 28px;
+ height: 28px;
+ position: relative;
+ margin-left: -5px;
z-index: 10;
> svg {
width: 100%;
@@ -53,10 +55,9 @@
height: 100%;
width: 100%;
position: absolute;
- top: 0;
+ top: 0px;
overflow-y: auto;
overflow-x: hidden;
- flex-wrap: wrap;
transition: top 0.5s;
> div {
@@ -92,8 +93,8 @@
.collectionStackingView-masonryGrid {
width: 100%;
display: grid;
- top: 0;
- left: 0;
+ top: 0px;
+ left: 0px;
}
.collectionStackingView-masonrySingle {
@@ -114,8 +115,8 @@
position: absolute;
display: flex;
flex-direction: column;
- top: 0;
- left: 0;
+ top: 0px;
+ left: 0px;
width: 100%;
position: absolute;
}
@@ -158,12 +159,12 @@
width: 100%;
display: none;
position: absolute;
- top: 0;
+ top: 0px;
cursor: default;
&.active {
- margin-left: 0;
- margin-right: 0;
+ margin-left: 0px;
+ margin-right: 0px;
background: red;
}
}
@@ -210,7 +211,6 @@
.collectionStackingView-sectionHeader {
text-align: center;
margin: auto;
- margin-bottom: 10px;
background: global.$medium-gray;
// overflow: hidden; overflow is visible so the color menu isn't hidden -ftong
@@ -262,8 +262,8 @@
.collectionStackingView-sectionColor {
position: absolute;
- left: 0;
- top: 0;
+ left: 0px;
+ top: 0px;
height: 100%;
display: none;
@@ -304,8 +304,8 @@
.collectionStackingView-sectionOptions {
position: absolute;
- right: 0;
- top: 0;
+ right: 0px;
+ top: 0px;
height: 100%;
display: none;
@@ -339,7 +339,7 @@
.collectionStackingView-sectionDelete {
position: absolute;
right: 0px;
- top: 0;
+ top: 0px;
height: 100%;
display: none;
}
@@ -367,7 +367,6 @@
.collectionStackingView-addGroupButton {
display: flex;
overflow: hidden;
- margin: auto;
width: 90%;
overflow: ellipses;
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 25a222cbb..fbdd23315 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -1,10 +1,10 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import * as CSS from 'csstype';
-import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction, runInAction } from 'mobx';
+import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { ClientUtils, DivHeight, returnNone, returnZero, setupMoveUpEvents, smoothScroll } from '../../../ClientUtils';
-import { Doc, Opt } from '../../../fields/Doc';
+import { Doc, Field, Opt } from '../../../fields/Doc';
import { Id } from '../../../fields/FieldSymbols';
import { List } from '../../../fields/List';
import { listSpec } from '../../../fields/Schema';
@@ -35,6 +35,7 @@ import { CollectionStackingViewFieldColumn } from './CollectionStackingViewField
import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
import { computedFn } from 'mobx-utils';
import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
+import { FieldsDropdown } from '../FieldsDropdown';
export type collectionStackingViewProps = {
sortFunc?: (a: Doc, b: Doc) => number;
@@ -48,28 +49,43 @@ export type collectionStackingViewProps = {
@observer
export class CollectionStackingView extends CollectionSubView<Partial<collectionStackingViewProps>>() {
_disposers: { [key: string]: IReactionDisposer } = {};
+ _addGroupRef = React.createRef<HTMLDivElement>();
_masonryGridRef: HTMLDivElement | null = null;
// used in a column dragger, likely due for the masonry grid view. We want to use this
_draggerRef = React.createRef<HTMLDivElement>();
// keeping track of documents. Updated on internal and external drops. What's the difference?
_docXfs: { height: () => number; width: () => number; stackedDocTransform: () => Transform }[] = [];
- // Doesn't look like this field is being used anywhere. Obsolete?
- _columnStart: number = 0;
- @observable _refList: HTMLElement[] = [];
+ @observable _colStackRefs: HTMLElement[] = [];
+ @observable _colHdrRefs: HTMLElement[] = [];
// map of node headers to their heights. Used in Masonry
@observable _heightMap = new Map<string, number>();
// Assuming that this is the current css cursor style
@observable _cursor: CSS.Property.Cursor = 'ew-resize';
// gets reset whenever we scroll. Not sure what it is
@observable _scroll = 0; // used to force the document decoration to update when scrolling
- // does this mean whether the browser is hidden? Or is chrome something else entirely?
+ // whether ui/editing controls are shown
@computed get chromeHidden() {
return this._props.chromeHidden || BoolCast(this.layoutDoc.chromeHidden);
}
- // it looks like this gets the column headers that Mehek was showing just now
@computed get colHeaderData() {
- return Cast(this.dataDoc['_' + this.fieldKey + '_columnHeaders'], listSpec(SchemaHeaderField), null);
+ return Cast(this.dataDoc[this.fieldKey + '_columnHeaders'], listSpec(SchemaHeaderField), null);
+ }
+
+ @computed get Sections() {
+ return this.filteredChildren.reduce(
+ (map, d) => {
+ const docHeader = d[this.pivotField] ? d[this.pivotField] : `NO ${this.pivotField.toUpperCase()} VALUE`;
+ const docHeaderString = docHeader !== undefined ? Field.toString(docHeader) : `NO ${this.pivotField.toUpperCase()} VALUE`;
+
+ // find existing header or create
+ const existingHeader = Array.from(map.keys()).find(sh => sh.heading === docHeaderString);
+ if (!existingHeader) map.set(new SchemaHeaderField(docHeaderString), [d]);
+ else map.get(existingHeader)!.push(d);
+ return map;
+ },
+ new ObservableMap<SchemaHeaderField, Doc[]>(this.colHeaderData?.map(hdata => [hdata, []] as [SchemaHeaderField, Doc[]]) ?? [])
+ );
}
// Still not sure what a pivot is, but it appears that we can actually filter docs somehow?
@computed get pivotField() {
@@ -107,9 +123,12 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
@computed get showAddAGroup() {
return this.pivotField && !this.chromeHidden;
}
+ @computed get availableWidth() {
+ return this._props.PanelWidth() - 2 * this.xMargin - (this.isStackingView ? this.gridGap * ((this.numGroupColumns || 1) - 1) : 0);
+ }
// columnWidth handles the margin on the left and right side of the documents
@computed get columnWidth() {
- const availableWidth = this._props.PanelWidth() - 2 * this.xMargin;
+ const availableWidth = this.availableWidth;
const cwid = availableWidth / (NumCast(this.Document._layout_columnCount) || this._props.PanelWidth() / NumCast(this.Document._layout_columnWidth, this._props.PanelWidth() / 4));
return Math.min(availableWidth, this.isStackingView ? availableWidth / (this.numGroupColumns || 1) : cwid - this.gridGap);
}
@@ -121,28 +140,17 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
constructor(props: SubCollectionViewProps) {
super(props);
makeObservable(this);
- if (this.colHeaderData === undefined) {
- // TODO: what is a layout doc? Is it literally how this document is supposed to be layed out?
- // here we're making an empty list of column headers (again, what Mehek showed us)
- this.dataDoc['_' + this.fieldKey + '_columnHeaders'] = new List<SchemaHeaderField>();
- }
}
+ availableWidthFn = () => this.availableWidth;
columnWidthFn = () => this.columnWidth;
columnDocHeightFn = (doc: Doc) => () => (this.isStackingView ? this.getDocHeight(doc)() : Math.min(this.getDocHeight(doc)(), this._props.PanelHeight()));
- // TODO: plj - these are the children
children = (docs: Doc[]) => {
- // TODO: can somebody explain me to what exactly TraceMobX is?
TraceMobx();
- // appears that we are going to reset the _docXfs. TODO: what is Xfs?
this._docXfs.length = 0;
- this._renderCount < docs.length &&
- setTimeout(
- action(() => {
- this._renderCount = Math.min(docs.length, this._renderCount + 5);
- })
- );
+ this._renderCount < docs.length &&
+ setTimeout(action(() => (this._renderCount = Math.min(docs.length, this._renderCount + 5)))); // prettier-ignore
return docs.map((d, i) => {
// assuming we need to get rowSpan because we might be dealing with many columns. Grid gap makes sense if multiple columns
const rowSpan = Math.ceil((this.getDocHeight(d)() + this.gridGap) / this.gridGap);
@@ -153,76 +161,28 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
margin: undefined,
transition: this.getDocTransition(d)(),
width: this.columnWidth,
- marginTop: i ? this.gridGap : 0,
height: this.getDocHeight(d)(),
zIndex: DocumentView.getFirstDocumentView(d)?.IsSelected ? 1000 : 0,
}
: { gridRowEnd: `span ${rowSpan}`, zIndex: DocumentView.getFirstDocumentView(d)?.IsSelected ? 1000 : 0 };
// So we're choosing whether we're going to render a column or a masonry doc
return (
- <div className={`collectionStackingView-${this.isStackingView ? 'columnDoc' : 'masonryDoc'}`} key={d[Id]} style={style}>
+ <div className={`collectionStackingView${this.isStackingView ? '-columnDoc' : '-masonryDoc'}`} key={d[Id]} style={style}>
{this.getDisplayDoc(d, this.getDocTransition(d), i)}
</div>
);
});
};
@action
- setDocHeight = (key: string, sectionHeight: number) => {
- this._heightMap.set(key, sectionHeight);
+ setDocHeight = (key: string, sectionHeight: number) => this._heightMap.set(key, sectionHeight);
+
+ setAutoHeight = () => {
+ const maxHeader = this.isStackingView ? this._colHdrRefs.reduce((p, r) => Math.max(p, DivHeight(r)), 0) + (this._colHdrRefs.length ? this.gridGap : 0) : 0;
+ const maxCol = this.isStackingView
+ ? this._colStackRefs.reduce((p, r) => Math.max(p, DivHeight(r)), 0) + this.gridGap
+ : this._colStackRefs.reduce((p, r) => p + DivHeight(r), this._addGroupRef.current ? DivHeight(this._addGroupRef.current) : 0);
+ this._props.setHeight?.(this.headerMargin + 2 * this.yMargin + maxCol + maxHeader);
};
-
- // is sections that all collections inherit? I think this is how we show the masonry/columns
- // TODO: this seems important
- get Sections() {
- // appears that pivot field IS actually for sorting
- if (!this.pivotField || this.colHeaderData instanceof Promise) return new Map<SchemaHeaderField, Doc[]>();
-
- if (this.colHeaderData === undefined) {
- setTimeout(() => {
- this.dataDoc['_' + this.fieldKey + '_columnHeaders'] = new List<SchemaHeaderField>();
- });
- return new Map<SchemaHeaderField, Doc[]>();
- }
- const colHeaderData = Array.from(this.colHeaderData);
- const fields = new Map<SchemaHeaderField, Doc[]>(colHeaderData.map(sh => [sh, []] as [SchemaHeaderField, []]));
- let changed = false;
- this.filteredChildren.forEach(d => {
- const sectionValue = (d[this.pivotField] ? d[this.pivotField] : `NO ${this.pivotField.toUpperCase()} VALUE`) as object;
- // the next five lines ensures that floating point rounding errors don't create more than one section -syip
- const parsed = parseInt(sectionValue.toString());
- const castedSectionValue = !isNaN(parsed) ? parsed : sectionValue;
-
- // look for if header exists already
- const existingHeader = colHeaderData.find(sh => sh.heading === (castedSectionValue ? castedSectionValue.toString() : `NO ${this.pivotField.toUpperCase()} VALUE`));
- if (existingHeader) {
- fields.get(existingHeader)!.push(d);
- } else {
- const newSchemaHeader = new SchemaHeaderField(castedSectionValue ? castedSectionValue.toString() : `NO ${this.pivotField.toUpperCase()} VALUE`);
- fields.set(newSchemaHeader, [d]);
- colHeaderData.push(newSchemaHeader);
- changed = true;
- }
- });
- // remove all empty columns if hideHeadings is set
- // we will want to have something like this, so that we can hide columns and add them back in
- if (this.layoutDoc._columnsHideIfEmpty) {
- Array.from(fields.keys())
- .filter(key => !fields.get(key)!.length)
- .forEach(header => {
- fields.delete(header);
- colHeaderData.splice(colHeaderData.indexOf(header), 1);
- changed = true;
- });
- }
- changed &&
- setTimeout(
- action(() => this.colHeaderData?.splice(0, this.colHeaderData.length, ...colHeaderData)),
- 0
- );
- return fields;
- }
-
- setAutoHeight = () => this._props.setHeight?.(this.headerMargin + (this.isStackingView ? Math.max(...this._refList.map(DivHeight)) : 2 * this.yMargin + this._refList.reduce((p, r) => p + DivHeight(r), 0)));
observer = new ResizeObserver(this.setAutoHeight);
componentDidMount() {
@@ -232,9 +192,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
// reset section headers when a new filter is inputted
this._disposers.pivotField = reaction(
() => this.pivotField,
- () => {
- this.dataDoc['_' + this.fieldKey + '_columnHeaders'] = new List();
- }
+ () => (this.dataDoc[this.fieldKey + '_columnHeaders'] = new List())
);
// reset section headers when a new filter is inputted
this._disposers.width = reaction(
@@ -252,7 +210,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
);
this._disposers.refList = reaction(
- () => ({ refList: this._refList.slice(), autoHeight: this.layoutDoc._layout_autoHeight && !DocumentView.LightboxContains(this.DocumentView?.()) }),
+ () => ({ refList: this._colStackRefs.slice(), autoHeight: this.layoutDoc._layout_autoHeight && !DocumentView.LightboxContains(this.DocumentView?.()) }),
({ refList, autoHeight }) => {
this.observer.disconnect();
if (autoHeight) refList.forEach(r => this.observer.observe(r));
@@ -409,11 +367,10 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
getDocWidth = computedFn((d?: Doc) => () => {
if (!d) return 0;
const childLayoutDoc = Doc.LayoutDoc(d, this._props.childLayoutTemplate?.());
- const maxWidth = this.columnWidth / this.numGroupColumns;
if (!this.layoutDoc._columnsFill && !this.childFitWidth(childLayoutDoc)) {
- return Math.min(NumCast(d._width), maxWidth);
+ return Math.min(NumCast(d._width), this.columnWidth);
}
- return maxWidth;
+ return this.columnWidth;
});
getDocTransition = computedFn((d?: Doc) => () => StrCast(d?.dataTransition));
getDocHeight = computedFn((d?: Doc) => () => {
@@ -424,7 +381,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
const nw = Doc.NativeWidth(childLayoutDoc, childDataDoc) || (!this.childFitWidth(childLayoutDoc) ? NumCast(d._width) : 0);
const nh = Doc.NativeHeight(childLayoutDoc, childDataDoc) || (!this.childFitWidth(childLayoutDoc) ? NumCast(d._height) : 0);
if (nw && nh) {
- const colWid = this.columnWidth / (this.isStackingView ? this.numGroupColumns : 1);
+ const colWid = this.columnWidth;
const docWid = this.layoutDoc._columnsFill ? colWid : Math.min(this.getDocWidth(d)(), colWid);
return Math.min(maxHeight, (docWid * nh) / nw);
}
@@ -434,10 +391,8 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
});
// This following three functions must be from the view Mehek showed
- columnDividerDown = (e: React.PointerEvent) => {
- runInAction(() => {
- this._cursor = 'grabbing';
- });
+ columnDividerDown = action((e: React.PointerEvent) => {
+ this._cursor = 'grabbing';
const batch = UndoManager.StartBatch('stacking width');
setupMoveUpEvents(
this,
@@ -449,7 +404,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
}),
emptyFunction
);
- };
+ });
@action
onDividerMove = (e: PointerEvent) => {
this.Document._layout_columnWidth = Math.max(10, (this._props.DocumentView?.().screenToViewTransform().transformPoint(e.clientX, 0)[0] ?? 0) - this.xMargin);
@@ -571,7 +526,8 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
}
return (
<CollectionStackingViewFieldColumn
- refList={this._refList}
+ colStackRefs={this._colStackRefs}
+ colHeaderRefs={this._colHdrRefs}
addDocument={this.addDocument}
chromeHidden={this.chromeHidden}
colHeaderData={this.colHeaderData}
@@ -608,13 +564,15 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
return (
<div key={(heading?.heading ?? '') + 'head'}>
{this._props.isContentActive() && !this.isStackingView && !this.chromeHidden ? this.columnDragger : null}
- <div style={{ top: this.yMargin }}>
+ <div style={{ position: 'relative' }}>
<CollectionMasonryViewFieldRow
showHandle={first}
Doc={this.Document}
chromeHidden={this.chromeHidden}
+ panelWidth={this.availableWidthFn}
+ columnWidth={this.columnWidthFn}
pivotField={this.pivotField}
- refList={this._refList}
+ sectionRefs={this._colStackRefs}
key={heading ? heading.heading : ''}
rows={rows}
headings={this.headings}
@@ -635,9 +593,11 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
/// add a new group category (column) to the active set of note categories. (e.g., if the pivot field is 'transportation', groups might be 'car', 'plane', 'bike', etc)
@action
addGroup = (value: string) => {
+ if (!this.colHeaderData) {
+ this.dataDoc[this.fieldKey + '_columnHeaders'] = new List();
+ }
if (value && this.colHeaderData) {
- const schemaHdrField = new SchemaHeaderField(value);
- this.colHeaderData.push(schemaHdrField);
+ this.colHeaderData.push(new SchemaHeaderField(value));
return true;
}
return false;
@@ -745,22 +705,25 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
this.fixWheelEvents(ele, this._props.isContentActive);
}}
style={{
- overflowY: this.isContentActive() ? 'auto' : 'hidden',
+ paddingBottom: this.yMargin,
+ paddingTop: this.yMargin,
+ paddingLeft: this.xMargin,
+ paddingRight: this.xMargin,
+ overflowY: this.isContentActive() && !this.layoutDoc._layout_autoHeight ? 'auto' : 'hidden',
background: this._props.styleProvider?.(this.Document, this._props, StyleProp.BackgroundColor) as string,
pointerEvents: this._props.pointerEvents?.() ?? this.backgroundEvents,
}}
- onScroll={action(e => {
- this._scroll = e.currentTarget.scrollTop;
- })}
+ onScroll={action(e => (this._scroll = e.currentTarget.scrollTop))}
onDrop={this.onExternalDrop.bind(this)}
onContextMenu={this.onContextMenu}
onWheel={e => this.isContentActive() && e.stopPropagation()}>
{this.renderedSections}
- {!this.showAddAGroup ? null : (
- <div key={`${this.Document[Id]}-addGroup`} className="collectionStackingView-addGroupButton" style={{ width: !this.isStackingView ? '100%' : this.columnWidth / this.numGroupColumns - 10, marginTop: 10 }}>
- <EditableView {...editableViewProps} />
- </div>
- )}
+ <div className="collectionStackingView-addGroupButton" ref={this._addGroupRef} style={{ width: !this.isStackingView ? '100%' : this.columnWidth, display: this.showAddAGroup ? undefined : 'none' }}>
+ <EditableView {...editableViewProps} />
+ </div>
+ <div style={{ right: 0, top: 0, position: 'absolute', display: !this.layoutDoc._pivotField ? 'none' : undefined }}>
+ <FieldsDropdown Doc={this.Document} isInactive={!this._props.isContentActive()} selectFunc={fieldKey => (this.layoutDoc._pivotField = fieldKey)} placeholder={StrCast(this.layoutDoc._pivotField)} />
+ </div>
</div>
</div>
</>
diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
index 345f60e75..b5efa7a72 100644
--- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
+++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
@@ -1,9 +1,10 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx';
+import { action, computed, IReactionDisposer, makeObservable, observable, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { DivHeight, DivWidth, returnEmptyString, returnTrue, setupMoveUpEvents } from '../../../ClientUtils';
import { Doc, DocListCast, Opt } from '../../../fields/Doc';
+import { DocData } from '../../../fields/DocSymbols';
import { RichTextField } from '../../../fields/RichTextField';
import { PastelSchemaPalette, SchemaHeaderField } from '../../../fields/SchemaHeaderField';
import { ScriptField } from '../../../fields/ScriptField';
@@ -26,7 +27,6 @@ import { EditableView } from '../EditableView';
import { DocumentView } from '../nodes/DocumentView';
import { ObservableReactComponent } from '../ObservableReactComponent';
import './CollectionStackingView.scss';
-import { DocData } from '../../../fields/DocSymbols';
// So this is how we are storing a column
interface CSVFieldColumnProps {
@@ -50,14 +50,14 @@ interface CSVFieldColumnProps {
addDocument: (doc: Doc | Doc[]) => boolean;
createDropTarget: (ele: HTMLDivElement) => void;
screenToLocalTransform: () => Transform;
- refList: HTMLElement[];
+ colStackRefs: HTMLElement[];
+ colHeaderRefs: HTMLElement[];
}
@observer
export class CollectionStackingViewFieldColumn extends ObservableReactComponent<CSVFieldColumnProps> {
private dropDisposer?: DragManager.DragDropDisposer;
private _disposers: { [name: string]: IReactionDisposer } = {};
- private _headerRef: React.RefObject<HTMLDivElement> = React.createRef();
@observable _background = 'inherit';
@observable _paletteOn = false;
@observable _heading = '';
@@ -72,6 +72,7 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
_ele: HTMLElement | null = null;
_eleMasonrySingle = React.createRef<HTMLDivElement>();
+ _headerRef: HTMLDivElement | null = null;
protected onInternalPreDrop = (e: Event, de: DragManager.DropEvent, targetDropAction: dropActionType) => {
const dragData = de.complete.docDragData;
@@ -93,13 +94,13 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
createColumnDropRef = (ele: HTMLDivElement | null) => {
this.dropDisposer?.();
if (ele) this.dropDisposer = DragManager.MakeDropTarget(ele, this.columnDrop.bind(this), this._props.Doc, this.onInternalPreDrop.bind(this));
- else if (this._eleMasonrySingle.current) this.props.refList.splice(this.props.refList.indexOf(this._eleMasonrySingle.current), 1);
+ else if (this._eleMasonrySingle.current) runInAction(() => this.props.colStackRefs.splice(this.props.colStackRefs.indexOf(this._eleMasonrySingle.current!), 1));
this._ele = ele;
};
@action
componentDidMount() {
- this._eleMasonrySingle.current && this.props.refList.push(this._eleMasonrySingle.current);
+ this._eleMasonrySingle.current && this.props.colStackRefs.push(this._eleMasonrySingle.current);
this._disposers.collapser = reaction(
() => this._props.headingObject?.collapsed,
collapsed => { this.collapsed = collapsed !== undefined ? BoolCast(collapsed) : false; }, // prettier-ignore
@@ -108,7 +109,7 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
}
componentWillUnmount() {
this._disposers.collapser?.();
- this._ele && this.props.refList.splice(this.props.refList.indexOf(this._ele), 1);
+ this._ele && runInAction(() => this.props.colStackRefs.splice(this.props.colStackRefs.indexOf(this._ele!), 1));
this._ele = null;
}
@@ -192,7 +193,7 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
value = typeof value === 'string' ? `"${value}"` : value;
embedding.viewSpecScript = ScriptField.MakeFunction(`doc.${this._props.pivotField} === ${value}`, { doc: Doc.name });
if (embedding.viewSpecScript) {
- DragManager.StartDocumentDrag([this._headerRef.current!], new DragManager.DocumentDragData([embedding]), e.clientX, e.clientY);
+ DragManager.StartDocumentDrag([this._headerRef!], new DragManager.DocumentDragData([embedding]), e.clientX, e.clientY);
return true;
}
return false;
@@ -303,6 +304,12 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
ContextMenu.Instance.displayMenu(pt[0], pt[1], undefined, true);
};
+ setRef = (r: HTMLDivElement | null) => {
+ if (this._headerRef && this._props.colHeaderRefs.includes(this._headerRef)) this._props.colHeaderRefs.splice(this._props.colStackRefs.indexOf(this._headerRef), 1);
+ r && this._props.colHeaderRefs.push(r);
+ this._headerRef = r;
+ };
+
@computed get innards() {
TraceMobx();
const key = this._props.pivotField;
@@ -314,9 +321,10 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
<div
key={heading}
className="collectionStackingView-sectionHeader"
- ref={this._headerRef}
+ ref={this.setRef}
style={{
- marginTop: this._props.yMargin,
+ marginTop: 0,
+ marginBottom: this._props.gridGap,
width: this._props.columnWidth,
}}>
{/* the default bucket (no key value) has a tooltip that describes what it is.
@@ -367,7 +375,7 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
ref={this._eleMasonrySingle}
className="collectionStackingView-masonrySingle"
style={{
- padding: `${columnYMargin}px ${0}px ${this._props.yMargin}px ${0}px`,
+ padding: `${columnYMargin}px ${0}px ${0}px ${0}px`,
margin: this._props.dontCenter.includes('x') ? undefined : 'auto',
height: 'max-content',
position: 'relative',
@@ -400,15 +408,13 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
render() {
TraceMobx();
- const headings = this._props.headings();
const heading = this._heading;
- const uniqueHeadings = headings.map((i, idx) => headings.indexOf(i) === idx);
return (
<div
className="collectionStackingViewFieldColumn"
key={heading}
style={{
- width: `${100 / (uniqueHeadings.length + (this._props.chromeHidden ? 0 : 1) || 1)}%`,
+ width: this._props.columnWidth,
height: undefined,
background: this._background,
}}
diff --git a/src/client/views/collections/CollectionTimeView.scss b/src/client/views/collections/CollectionTimeView.scss
index d995cbcd2..d56999974 100644
--- a/src/client/views/collections/CollectionTimeView.scss
+++ b/src/client/views/collections/CollectionTimeView.scss
@@ -27,7 +27,7 @@
transform: rotate(45deg);
display: inline-block;
background: gray;
- bottom: 0;
+ bottom: 0px;
margin-bottom: -17px;
border-radius: 9px;
opacity: 0.25;
@@ -67,9 +67,9 @@
pointer-events: all;
padding: 5px;
border: 1px solid black;
- display:none;
+ display: none;
span {
- margin-left : 10px;
+ margin-left: 10px;
}
}
@@ -86,8 +86,9 @@
}
}
-.collectionTimeView:hover, .collectionTimeView-pivot:hover {
+.collectionTimeView:hover,
+.collectionTimeView-pivot:hover {
.pivotKeyEntry {
- display:unset;
+ display: unset;
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss
index 2a03ea708..95faaa3f0 100644
--- a/src/client/views/collections/CollectionTreeView.scss
+++ b/src/client/views/collections/CollectionTreeView.scss
@@ -11,7 +11,7 @@
height: 100%;
width: 100%;
position: relative;
- top: 0;
+ top: 0px;
// background: global.$light-gray;
font-size: 13px;
overflow: auto;
@@ -33,12 +33,12 @@
}
.no-indent {
- padding-left: 0;
+ padding-left: 0px;
//width: max-content;
}
.no-indent-outline {
- padding-left: 0;
+ padding-left: 0px;
width: 100%;
}
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index bee5d016d..4625965b4 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -407,6 +407,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
remAnnotationDocument = (doc: Doc | Doc[]) => this.removeDocument(doc, `${this._props.fieldKey}_annotations`) || false;
moveAnnotationDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[], annotationKey?: string) => boolean) => this.moveDocument(doc, targetCollection, addDocument) || false;
+ setRef = (r: HTMLDivElement | null) => !this.Document.treeView_HasOverlay && r && this.createTreeDropTarget(r);
@observable _headerHeight = 0;
@computed get content() {
const background = () => this._props.styleProvider?.(this.Document, this._props, StyleProp.BackgroundColor) as string;
@@ -428,7 +429,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
<div
className="collectionTreeView-contents"
key="tree"
- ref={r => !this.Document.treeView_HasOverlay && r && this.createTreeDropTarget(r)}
+ ref={this.setRef}
style={{
...(!titleBar ? { marginLeft: this.marginX(), paddingTop: this.marginTop() } : {}),
color: color(),
diff --git a/src/client/views/collections/CollectionView.scss b/src/client/views/collections/CollectionView.scss
index 06c324bd0..837219e1d 100644
--- a/src/client/views/collections/CollectionView.scss
+++ b/src/client/views/collections/CollectionView.scss
@@ -1,7 +1,7 @@
@use '../global/globalCssVariables.module.scss' as global;
.collectionView {
- border-width: 0;
+ border-width: 0px;
border-color: global.$light-gray;
border-style: solid;
border-radius: 0 0 global.$border-radius global.$border-radius;
@@ -18,7 +18,7 @@
position: absolute;
top: 55%;
border: 1px black solid;
- border-radius: 0;
+ border-radius: 0px;
border-top-left-radius: 20px;
border-bottom-left-radius: 20px;
border-right: unset;
@@ -31,8 +31,8 @@
width: 200px;
height: 100%;
position: absolute;
- right: 0;
- top: 0;
+ right: 0px;
+ top: 0px;
border-left: solid 1px;
z-index: 1;
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index eb9caf29d..72c8c3f8c 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -113,7 +113,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewPr
};
setupViewTypes(category: string, func: (type_collection: CollectionViewType) => Doc) {
- if (!Doc.IsSystem(this.Document) && this.Document._type_collection !== CollectionViewType.Docking && !this.dataDoc.isGroup && !this.Document.annotationOn) {
+ if (!Doc.IsSystem(this.Document) && this.Document._type_collection !== CollectionViewType.Docking && !this.Document.annotationOn) {
// prettier-ignore
const subItems: ContextMenuProps[] = [
{ description: 'Freeform', event: () => func(CollectionViewType.Freeform), icon: 'signature' },
diff --git a/src/client/views/collections/FlashcardPracticeUI.scss b/src/client/views/collections/FlashcardPracticeUI.scss
index 210c6798f..0cc4711b3 100644
--- a/src/client/views/collections/FlashcardPracticeUI.scss
+++ b/src/client/views/collections/FlashcardPracticeUI.scss
@@ -8,8 +8,8 @@
.FlashcardPracticeUI-check {
position: absolute;
display: flex;
- width: 30;
- height: 30;
+ width: 30px;
+ height: 30px;
align-items: center;
border-radius: 5px;
justify-content: center;
@@ -39,7 +39,7 @@
display: flex;
top: 0px;
left: 0px;
- width: 30;
+ width: 30px;
transform-origin: top left;
border-radius: 5px;
color: rgba(255, 255, 255, 0.5);
@@ -49,7 +49,7 @@
width: 100%;
display: flex;
flex-direction: column;
- top: 0;
+ top: 0px;
position: relative;
.FlashcardPracticeUI-quiz,
.FlashcardPracticeUI-practice {
diff --git a/src/client/views/collections/FlashcardPracticeUI.tsx b/src/client/views/collections/FlashcardPracticeUI.tsx
index 2f46c00bd..17b65334c 100644
--- a/src/client/views/collections/FlashcardPracticeUI.tsx
+++ b/src/client/views/collections/FlashcardPracticeUI.tsx
@@ -61,7 +61,8 @@ export class FlashcardPracticeUI extends ObservableReactComponent<PracticeUIProp
get practiceField() { return this._props.fieldKey + "_practice"; } // prettier-ignore
@computed get filterDoc() { return DocListCast(Doc.MyContextMenuBtns?.data).find(doc => doc.title === 'Filter'); } // prettier-ignore
- @computed get practiceMode() { return this._props.allChildDocs().some(doc => doc._layout_flashcardType) ? StrCast(this._props.layoutDoc.practiceMode) : ''; } // prettier-ignore
+ @computed get hasFlashcards() { return this._props.allChildDocs().some(doc => doc._layout_flashcardType); } // prettier-ignore
+ @computed get practiceMode() { return this.hasFlashcards ? StrCast(this._props.layoutDoc.practiceMode) : ''; } // prettier-ignore
btnHeight = () => NumCast(this.filterDoc?.height);
btnWidth = () => (!this.filterDoc ? 1 : NumCast(this.filterDoc._width));
@@ -130,7 +131,7 @@ export class FlashcardPracticeUI extends ObservableReactComponent<PracticeUIProp
const setColor = (mode: practiceMode) => (StrCast(this.practiceMode) === mode ? 'white' : 'lightgray');
const togglePracticeMode = (mode: practiceMode) => this.setPracticeMode(mode === this.practiceMode ? undefined : mode);
- return !this._props.allChildDocs().some(doc => doc._layout_flashcardType) ? null : (
+ return !this.hasFlashcards ? null : (
<div
className="FlashcardPracticeUI-practiceModes"
style={{
@@ -141,8 +142,8 @@ export class FlashcardPracticeUI extends ObservableReactComponent<PracticeUIProp
type={Type.PRIM}
color={SnappingManager.userColor}
background={SnappingManager.userVariantColor}
+ showUntilToggle={false}
multiSelect={false}
- toggleStatus={!!this.practiceMode}
label="Practice"
items={[
[practiceMode.QUIZ, 'file-pen', 'Practice flashcards using GPT'],
@@ -160,8 +161,8 @@ export class FlashcardPracticeUI extends ObservableReactComponent<PracticeUIProp
type={Type.PRIM}
color={SnappingManager.userColor}
background={SnappingManager.userVariantColor}
+ showUntilToggle={false}
multiSelect={false}
- toggleStatus={!!this.practiceMode}
label={StrCast(this._props.layoutDoc.revealOp, flashcardRevealOp.FLIP)}
items={[
['reveal', StrCast(this._props.layoutDoc.revealOp) === flashcardRevealOp.SLIDE ? 'expand' : 'question', StrCast(this._props.layoutDoc.revealOp, flashcardRevealOp.FLIP)],
diff --git a/src/client/views/collections/KeyRestrictionRow.tsx b/src/client/views/collections/KeyRestrictionRow.tsx
deleted file mode 100644
index 7dc08389b..000000000
--- a/src/client/views/collections/KeyRestrictionRow.tsx
+++ /dev/null
@@ -1,72 +0,0 @@
-/* eslint-disable react/button-has-type */
-import { observable, runInAction } from 'mobx';
-import { observer } from 'mobx-react';
-import * as React from 'react';
-
-interface IKeyRestrictionProps {
- contains: boolean;
- script: (value: string) => void;
- field: string;
- value: string;
-}
-
-@observer
-export default class KeyRestrictionRow extends React.Component<IKeyRestrictionProps> {
- @observable private _key = this.props.field;
- @observable private _value = this.props.value;
- @observable private _contains = this.props.contains;
-
- render() {
- if (this._key && this._value) {
- let parsedValue: string | number = `"${this._value}"`;
- const parsed = parseInt(this._value);
- let type = 'string';
- if (!isNaN(parsed)) {
- parsedValue = parsed;
- type = 'number';
- }
- const scriptText = `${this._contains ? '' : '!'}(((doc.${this._key} && (doc.${this._key} as ${type})${type === 'string' ? '.includes' : '<='}(${parsedValue}))) ||
- ((doc.data_ext && doc.data_ext.${this._key}) && (doc.data_ext.${this._key} as ${type})${type === 'string' ? '.includes' : '<='}(${parsedValue}))))`;
- // let doc = new Doc();
- // ((doc.data_ext && doc.data_ext!.text) && (doc.data_ext!.text as string).includes("hello"));
- this.props.script(scriptText);
- } else {
- this.props.script('');
- }
-
- return (
- <div className="collectionViewBaseChrome-viewSpecsMenu-row">
- <input
- className="collectionViewBaseChrome-viewSpecsMenu-rowLeft"
- value={this._key}
- onChange={e =>
- runInAction(() => {
- this._key = e.target.value;
- })
- }
- placeholder="KEY"
- />
- <button
- className="collectionViewBaseChrome-viewSpecsMenu-rowMiddle"
- style={{ background: this._contains ? '#77dd77' : '#ff6961' }}
- onClick={() =>
- runInAction(() => {
- this._contains = !this._contains;
- })
- }>
- {this._contains ? 'CONTAINS' : 'DOES NOT CONTAIN'}
- </button>
- <input
- className="collectionViewBaseChrome-viewSpecsMenu-rowRight"
- value={this._value}
- onChange={e =>
- runInAction(() => {
- this._value = e.target.value;
- })
- }
- placeholder="VALUE"
- />
- </div>
- );
- }
-}
diff --git a/src/client/views/collections/TabDocView.scss b/src/client/views/collections/TabDocView.scss
index 931cdac2b..b705d17f3 100644
--- a/src/client/views/collections/TabDocView.scss
+++ b/src/client/views/collections/TabDocView.scss
@@ -92,6 +92,6 @@ input.lm_title {
.miniMap-hidden {
cursor: pointer;
position: absolute;
- bottom: 5;
- right: 5;
+ bottom: 5px;
+ right: 5px;
}
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index c4373aaa7..897a86e7f 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -584,9 +584,7 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
miniMapColor = () => Colors.MEDIUM_GRAY;
tabView = () => this._view;
disableMinimap = () => !this._document;
- whenChildContentActiveChanges = (isActive: boolean) => {
- this._isAnyChildContentActive = isActive;
- };
+ whenChildContentActiveChanges = (isActive: boolean) => (this._isAnyChildContentActive = isActive);
isContentActiveFunc = () => this.isContentActive;
waitForDoubleClick = () => (SnappingManager.ExploreMode ? 'never' : undefined);
renderDocView = (doc: Doc) => (
diff --git a/src/client/views/collections/TreeView.scss b/src/client/views/collections/TreeView.scss
index 78794d112..542b0cc87 100644
--- a/src/client/views/collections/TreeView.scss
+++ b/src/client/views/collections/TreeView.scss
@@ -136,8 +136,8 @@
width: 100%;
height: 100%;
position: absolute;
- top: 0;
- left: 0;
+ top: 0px;
+ left: 0px;
z-index: 0;
filter: opacity(0);
}
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index 5b2f1ff81..f84c7d3c0 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -1294,6 +1294,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
return null;
}
+ const setRef = (r: TreeView | null) => treeViewRefs.set(child, r || undefined);
const dentDoc = (editTitle: boolean, newParent: Doc, addAfter: Doc | undefined, parent: TreeView | CollectionTreeView | undefined) => {
if (parent instanceof TreeView && parent._props.treeView.fileSysMode && !newParent.isFolder) return;
const fieldKey = Doc.LayoutDataKey(newParent);
@@ -1317,7 +1318,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
return (
<TreeView
key={child[Id]}
- ref={r => treeViewRefs.set(child, r || undefined)}
+ ref={setRef}
Document={pair.layout}
dataDoc={pair.data}
treeViewParent={treeViewParent}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.scss
index 7951aff65..32cf3586f 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.scss
@@ -18,5 +18,5 @@
color: black;
// fontStyle: "italic",
margin-left: -12;
- margin-top: 4;
+ margin-top: 4px;
}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
index 6c47a71b0..ac1ef7d65 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
@@ -2,8 +2,8 @@
.collectionfreeformview-none {
position: inherit;
- top: 0;
- left: 0;
+ top: 0px;
+ left: 0px;
width: 100%;
height: 100%;
transform-origin: left top;
@@ -12,10 +12,10 @@
border-radius: inherit;
}
.collectionFreeForm-groupDropper {
- width: 10000;
- height: 10000;
- left: -5000;
- top: -5000;
+ width: 10000px;
+ height: 10000px;
+ left: -5000px;
+ top: -5000px;
position: absolute;
background: transparent;
pointer-events: all;
@@ -24,8 +24,8 @@
.collectionfreeformview-grid {
transform-origin: top left;
position: absolute;
- top: 0;
- left: 0;
+ top: 0px;
+ left: 0px;
pointer-events: none;
}
@@ -219,8 +219,8 @@
border-radius: inherit;
box-sizing: border-box;
position: absolute;
- top: 0;
- left: 0;
+ top: 0px;
+ left: 0px;
width: 100%;
height: 100%;
align-items: center;
@@ -264,7 +264,7 @@
.collectionFreeform-infoUI {
position: absolute;
display: block;
- top: 0;
+ top: 0px;
color: white;
background-color: #5075ef;
@@ -275,19 +275,19 @@
padding: 10px;
.collectionFreeform-infoUI-close {
position: absolute;
- top: -10;
- left: -10;
+ top: -10px;
+ left: -10px;
}
.collectionFreeform-infoUI-msg {
position: relative;
- max-width: 500;
- margin: 10;
+ max-width: 500px;
+ margin: 10px;
}
.collectionFreeform-infoUI-button {
border-radius: 50px;
font-size: 12px;
- padding: 6;
+ padding: 6px;
position: relative;
}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index bb3c59eae..6e9e503f4 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -8,7 +8,7 @@ import { computedFn } from 'mobx-utils';
import * as React from 'react';
import { AiOutlineSend } from 'react-icons/ai';
import ReactLoading from 'react-loading';
-import { ClientUtils, DashColor, lightOrDark, OmitKeys, returnFalse, returnZero, setupMoveUpEvents, UpdateIcon } from '../../../../ClientUtils';
+import { ClientUtils, DashColor, lightOrDark, OmitKeys, returnFalse, returnTrue, returnZero, setupMoveUpEvents, UpdateIcon } from '../../../../ClientUtils';
import { DateField } from '../../../../fields/DateField';
import { Doc, DocListCast, Field, FieldType, Opt, StrListCast } from '../../../../fields/Doc';
import { DocData, DocLayout, Height, Width } from '../../../../fields/DocSymbols';
@@ -176,7 +176,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
return renderableEles;
}
@computed get fitContentsToBox() {
- return (this._props.fitContentsToBox?.() || this.Document._freeform_fitContentsToBox) && !this.isAnnotationOverlay;
+ return (this._props.fitContentsToBox?.() || this.Document._freeform_fitContentsToBox || this.Document.freeform_isGroup) && !this.isAnnotationOverlay;
}
@computed get nativeWidth() {
return this._props.NativeWidth?.() || Doc.NativeWidth(this.Document);
@@ -342,7 +342,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
*/
focusOnPoint = (options: FocusViewOptions) => {
const { pointFocus, zoomTime, didMove } = options;
- if (!this.Document.isGroup && pointFocus && !didMove) {
+ if (!this.Document.freeform_isGroup && pointFocus && !didMove) {
const dfltScale = this.isAnnotationOverlay ? 1 : 0.25;
if (this.layoutDoc[this.scaleFieldKey] !== dfltScale) {
this.zoomSmoothlyAboutPt(this.screenToFreeformContentsXf.transformPoint(pointFocus.X, pointFocus.Y), dfltScale, zoomTime);
@@ -380,7 +380,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
* @returns
*/
focus = (anchor: Doc, options: FocusViewOptions) => {
- if (anchor.isGroup && !options.docTransform && options.contextPath?.length) {
+ if (Doc.IsFreeformGroup(anchor) && !options.docTransform && options.contextPath?.length) {
// don't focus on group if there's a context path because we're about to focus on a group item
// which will override any group focus. (If we allowed the group to focus, it would mark didMove even if there were no net movement)
return undefined;
@@ -395,7 +395,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
}
const xfToCollection = options?.docTransform ?? Transform.Identity();
const savedState = { panX: NumCast(this.Document[this.panXFieldKey]), panY: NumCast(this.Document[this.panYFieldKey]), scale: options?.willZoomCentered ? this.Document[this.scaleFieldKey] : undefined };
- const cantTransform = this.fitContentsToBox || ((this.Document.isGroup || this.layoutDoc._lockedTransform) && !DocumentView.LightboxDoc());
+ const cantTransform = this.fitContentsToBox || ((this.Document.freeform_isGroup || this.layoutDoc._lockedTransform) && !DocumentView.LightboxDoc());
const { panX, panY, scale } = cantTransform || (!options.willPan && !options.willZoomCentered) ? savedState : this.calculatePanIntoView(anchor, xfToCollection, options?.willZoomCentered ? (options?.zoomScale ?? 0.75) : undefined);
// focus on the document in the collection
@@ -514,7 +514,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
this._downTime = Date.now();
const scrollMode = e.altKey ? (Doc.UserDoc().freeformScrollMode === freeformScrollMode.Pan ? freeformScrollMode.Zoom : freeformScrollMode.Pan) : Doc.UserDoc().freeformScrollMode;
if (e.button === 0 && (!(e.ctrlKey && !e.metaKey) || scrollMode !== freeformScrollMode.Pan) && this._props.isContentActive()) {
- if (!this.Document.isGroup) {
+ if (!this.Document.freeform_isGroup) {
// group freeforms don't pan when dragged -- instead let the event go through to allow the group itself to drag
// prettier-ignore
const hit = this._clusters.handlePointerDown(this.screenToFreeformContentsXf.transformPoint(e.clientX, e.clientY));
@@ -562,7 +562,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
const B = this.screenToFreeformContentsXf.transformBounds(ge.bounds.left, ge.bounds.top, ge.bounds.width, ge.bounds.height);
const inkDoc = this.createInkDoc(points, B);
if (Doc.ActiveInk === InkInkTool.Highlight) inkDoc.$backgroundColor = 'transparent';
- if (Doc.ActiveInk === InkInkTool.Write) {
+ if ([InkInkTool.Write, InkInkTool.Math].includes(Doc.ActiveInk)) {
this.unprocessedDocs.push(inkDoc);
CollectionFreeFormView.collectionsWithUnprocessedInk.add(this);
}
@@ -1271,7 +1271,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
@action
zoom = (pointX: number, pointY: number, deltaY: number): void => {
- if (this.Document.isGroup || this.Document[(this._props.viewField ?? '_') + 'freeform_noZoom']) return;
+ if (this.Document.freeform_isGroup || this.Document[(this._props.viewField ?? '_') + 'freeform_noZoom']) return;
let deltaScale = deltaY > 0 ? 1 / 1.05 : 1.05;
if (deltaScale < 0) deltaScale = -deltaScale;
const [x, y] = this.screenToFreeformContentsXf.transformPoint(pointX, pointY);
@@ -1298,7 +1298,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
@action
onPointerWheel = (e: React.WheelEvent): void => {
- if (this.Document.isGroup || !this.isContentActive()) return; // group style collections neither pan nor zoom
+ if (this.Document.freeform_isGroup || !this.isContentActive()) return; // group style collections neither pan nor zoom
SnappingManager.TriggerUserPanned();
if (this.layoutDoc._Transform || this.Document.treeView_OutlineMode === TreeViewType.outline) return;
e.stopPropagation();
@@ -1422,7 +1422,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
@action
zoomSmoothlyAboutPt(docpt: number[], scale: number, transitionTime = 500) {
- if (this.Document.isGroup) return;
+ if (this.Document.freeform_isGroup) return;
this.setPanZoomTransition(transitionTime);
const screenXY = this.screenToFreeformContentsXf.inverse().transformPoint(docpt[0], docpt[1]);
this.layoutDoc[this.scaleFieldKey] = scale;
@@ -1505,7 +1505,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
removeDocument = (docs: Doc | Doc[], annotationKey?: string | undefined) => {
const ret = !!this._props.removeDocument?.(docs, annotationKey);
// if this is a group and we have fewer than 2 Docs, then just promote what's left to our parent and get rid of the group.
- if (ret && DocListCast(this.dataDoc[annotationKey ?? this.fieldKey]).length < 2 && this.Document.isGroup) {
+ if (ret && DocListCast(this.dataDoc[annotationKey ?? this.fieldKey]).length < 2 && this.Document.freeform_isGroup) {
this.promoteCollection();
}
return ret;
@@ -1548,7 +1548,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
searchFilterDocs={this.searchFilterDocs}
isDocumentActive={childLayout.pointerEvents === 'none' ? returnFalse : this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this.isContentActive}
isContentActive={this.childContentsActive}
- focus={this.Document.isGroup ? this.groupFocus : this.isAnnotationOverlay ? this._props.focus : this.focus}
+ focus={this.Document.freeform_isGroup ? this.groupFocus : this.isAnnotationOverlay ? this._props.focus : this.focus}
addDocTab={this.addDocTab}
addDocument={this._props.addDocument}
removeDocument={this.removeDocument}
@@ -1733,15 +1733,16 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
});
PinDocView(
anchor,
- { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ? { ...pinProps.pinData, poslayoutview: pinProps.pinData.dataview } : {}), pannable: !this.Document.isGroup, collectionType: true, filters: true } },
+ { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ? { ...pinProps.pinData, poslayoutview: pinProps.pinData.dataview } : {}), pannable: !this.Document.freeform_isGroup, collectionType: true, filters: true } },
this.Document
);
if (addAsAnnotation) {
- if (Cast(this.dataDoc[this._props.fieldKey + '_annotations'], listSpec(Doc), null) !== undefined) {
- Cast(this.dataDoc[this._props.fieldKey + '_annotations'], listSpec(Doc), [])?.push(anchor);
+ const fieldKey = this._props.isAnnotationOverlay ? this._props.fieldKey : this._props.fieldKey + '_annotations';
+ if (Cast(this.dataDoc[fieldKey], listSpec(Doc), null) !== undefined) {
+ Cast(this.dataDoc[fieldKey], listSpec(Doc), [])?.push(anchor);
} else {
- this.dataDoc[this._props.fieldKey + '_annotations'] = new List<Doc>([anchor]);
+ this.dataDoc[fieldKey] = new List<Doc>([anchor]);
}
}
return anchor;
@@ -1766,7 +1767,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
this._firstRender = false;
this._disposers.groupBounds = reaction(
() => {
- if (this.Document.isGroup && this.childDocs.length === this.childDocList?.length) {
+ if (this.Document.freeform_isGroup && this.childDocs.length === this.childDocList?.length) {
const clist = this.childDocs.map(cd => ({ x: NumCast(cd.x), y: NumCast(cd.y), width: NumCast(cd._width), height: NumCast(cd._height) }));
return aggregateBounds(clist, NumCast(this.layoutDoc._xMargin), NumCast(this.layoutDoc._yMargin));
}
@@ -1928,8 +1929,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
const appearance = ContextMenu.Instance.findByDescription('Appearance...');
const appearanceItems = appearance?.subitems ?? [];
- !this.Document.isGroup && appearanceItems.push({ description: 'Reset View', event: this.resetView, icon: 'compress-arrows-alt' });
- !this.Document.isGroup && appearanceItems.push({ description: 'Toggle Auto Reset View', event: this.toggleResetView, icon: 'compress-arrows-alt' });
+ !this.Document.freeform_isGroup && appearanceItems.push({ description: 'Reset View', event: this.resetView, icon: 'compress-arrows-alt' });
+ !this.Document.freeform_isGroup && appearanceItems.push({ description: 'Toggle Auto Reset View', event: this.toggleResetView, icon: 'compress-arrows-alt' });
if (this._props.setContentViewBox === emptyFunction) {
!appearance && ContextMenu.Instance.addItem({ description: 'Appearance...', subitems: appearanceItems, icon: 'eye' });
return;
@@ -1947,7 +1948,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
!Doc.noviceMode && appearanceItems.push({ description: `update icon`, event: () => this.updateIcon(), icon: 'compress-arrows-alt' });
this._props.renderDepth && appearanceItems.push({ description: 'Ungroup collection', event: this.promoteCollection, icon: 'table' });
- this.Document.isGroup && this.Document.transcription && appearanceItems.push({ description: 'Ink to text', event: this.transcribeStrokes, icon: 'font' });
+ this.Document.freeform_isGroup && this.Document.transcription && appearanceItems.push({ description: 'Ink to text', event: this.transcribeStrokes, icon: 'font' });
!Doc.noviceMode ? appearanceItems.push({ description: 'Arrange contents in grid', event: this.layoutDocsInGrid, icon: 'table' }) : null;
!Doc.noviceMode ? appearanceItems.push({ description: (this.Document._freeform_useClusters ? 'Hide' : 'Show') + ' Clusters', event: () => this._clusters.updateClusters(!this.Document._freeform_useClusters), icon: 'braille' }) : null;
@@ -2006,7 +2007,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
};
transcribeStrokes = undoable(() => {
- if (this.Document.isGroup && this.Document.transcription) {
+ if (this.Document.freeform_isGroup && this.Document.transcription) {
const text = StrCast(this.Document.transcription);
const lines = text.split('\n');
const height = 30 + 15 * lines.length;
@@ -2024,7 +2025,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
dragStarting = (snapToDraggedDoc: boolean = false, showGroupDragTarget: boolean = true, visited = new Set<Doc>()) => {
if (visited.has(this.Document)) return;
visited.add(this.Document);
- showGroupDragTarget && (this.GroupChildDrag = BoolCast(this.Document.isGroup));
+ showGroupDragTarget && (this.GroupChildDrag = BoolCast(this.Document.freeform_isGroup));
const activeDocs = this.getActiveDocuments();
const size = this.screenToFreeformContentsXf.transformDirection(this._props.PanelWidth(), this._props.PanelHeight());
const selRect = { left: this.panX() - size[0] / 2, top: this.panY() - size[1] / 2, width: size[0], height: size[1] };
@@ -2032,13 +2033,15 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
const isDocInView = (doc: Doc, rect: { left: number; top: number; width: number; height: number }) => intersectRect(docDims(doc), rect);
const snappableDocs = activeDocs.filter(doc => doc.z === undefined && isDocInView(doc, selRect)); // first see if there are any foreground docs to snap to
- activeDocs.filter(doc => doc.isGroup && SnappingManager.IsResizing !== doc[Id] && !DragManager.docsBeingDragged.includes(doc)).forEach(doc => DocumentView.getDocumentView(doc)?.ComponentView?.dragStarting?.(snapToDraggedDoc, false, visited));
+ activeDocs
+ .filter(doc => Doc.IsFreeformGroup(doc) && SnappingManager.IsResizing !== doc[Id] && !DragManager.docsBeingDragged.includes(doc))
+ .forEach(doc => DocumentView.getDocumentView(doc)?.ComponentView?.dragStarting?.(snapToDraggedDoc, false, visited));
const horizLines: number[] = [];
const vertLines: number[] = [];
const invXf = this.screenToFreeformContentsXf.inverse();
snappableDocs
- .filter(doc => !doc.isGroup && (snapToDraggedDoc || (SnappingManager.IsResizing !== doc[Id] && !DragManager.docsBeingDragged.includes(doc))))
+ .filter(doc => !Doc.IsFreeformGroup(doc) && (snapToDraggedDoc || (SnappingManager.IsResizing !== doc[Id] && !DragManager.docsBeingDragged.includes(doc))))
.forEach(doc => {
const { left, top, width, height } = docDims(doc);
const topLeftInScreen = invXf.transformPoint(left, top);
@@ -2062,6 +2065,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
}
this.childDocs.some(doc => !this._renderCutoffData.get(doc[Id])) && setTimeout(this.incrementalRender, 1);
});
+ showBorderRounding = returnTrue;
showPresPaths = () => SnappingManager.ShowPresPaths;
brushedView = () => this._brushedView;
gridColor = () => DashColor(lightOrDark(this.backgroundColor)).fade(0.5).toString(); // prettier-ignore
@@ -2130,7 +2134,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
{...this._props}
ref={this._marqueeViewRef}
Doc={this.Document}
- ungroup={this.Document.isGroup ? this.promoteCollection : undefined}
+ ungroup={this.Document.freeform_isGroup ? this.promoteCollection : undefined}
nudge={this.isAnnotationOverlay || this._props.renderDepth > 0 ? undefined : this.nudge}
addDocTab={this.addDocTab}
slowLoadDocuments={this.slowLoadDocuments}
@@ -2216,18 +2220,41 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
);
};
+ @computed get inkEraser() {
+ return (
+ Doc.ActiveTool === InkTool.Eraser &&
+ Doc.ActiveEraser === InkEraserTool.Radius &&
+ this._showEraserCircle && (
+ <div
+ onPointerMove={this.onCursorMove}
+ style={{
+ position: 'fixed',
+ left: this._eraserX,
+ top: this._eraserY,
+ width: (ActiveEraserWidth() + 5) * 2,
+ height: (ActiveEraserWidth() + 5) * 2,
+ borderRadius: '50%',
+ border: '1px solid gray',
+ transform: 'translate(-50%, -50%)',
+ }}
+ />
+ )
+ );
+ }
+
+ setRef = (r: HTMLDivElement | null) => {
+ this.createDashEventsTarget(r);
+ this.fixWheelEvents(r, this._props.isContentActive, this.onPassiveWheel);
+ r?.addEventListener('mouseleave', this.onMouseLeave);
+ r?.addEventListener('mouseenter', this.onMouseEnter);
+ };
render() {
TraceMobx();
return (
<div
className="collectionfreeformview-container"
id={this._paintedId}
- ref={r => {
- this.createDashEventsTarget(r);
- this.fixWheelEvents(r, this._props.isContentActive, this.onPassiveWheel);
- r?.addEventListener('mouseleave', this.onMouseLeave);
- r?.addEventListener('mouseenter', this.onMouseEnter);
- }}
+ ref={this.setRef}
onWheel={this.onPointerWheel}
onClick={this.onClick}
onPointerDown={this.onPointerDown}
@@ -2242,21 +2269,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
width: `${100 / this.nativeDimScaling}%`,
height: this._props.getScrollHeight?.() ?? `${100 / this.nativeDimScaling}%`,
}}>
- {Doc.ActiveTool === InkTool.Eraser && Doc.ActiveEraser === InkEraserTool.Radius && this._showEraserCircle && (
- <div
- onPointerMove={this.onCursorMove}
- style={{
- position: 'fixed',
- left: this._eraserX,
- top: this._eraserY,
- width: (ActiveEraserWidth() + 5) * 2,
- height: (ActiveEraserWidth() + 5) * 2,
- borderRadius: '50%',
- border: '1px solid gray',
- transform: 'translate(-50%, -50%)',
- }}
- />
- )}
+ {this.inkEraser}
{this.paintFunc ? (
<FormattedTextBox {...this.props} /> // need this so that any live dashfieldviews will update the underlying text that the code eval reads
) : this._lightboxDoc ? (
diff --git a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.scss b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.scss
index 0a001d84c..d0685e419 100644
--- a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.scss
+++ b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.scss
@@ -2,7 +2,7 @@
display: flex;
height: max-content;
flex-direction: column;
- top: 0;
+ top: 0px;
position: absolute;
width: 100%;
height: 100%;
@@ -31,9 +31,9 @@
}
.face-document-top {
position: relative;
- top: 0;
+ top: 0px;
width: 100%;
- left: 0;
+ left: 0px;
}
.face-document-image-container {
@@ -69,8 +69,8 @@
.remove-item {
position: absolute;
- bottom: -5;
- right: -5;
+ bottom: -5px;
+ right: -5px;
background-color: rgba(0, 0, 0, 0.5); // Optional: to add a background behind the icon for better visibility
border-radius: 30%;
width: 10px; // Adjust size as needed
@@ -98,7 +98,7 @@
.faceCollectionBox {
width: 100%;
height: 100%;
- top: 0;
- left: 0;
+ top: 0px;
+ left: 0px;
position: absolute;
}
diff --git a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx
index 142085e14..c31558dff 100644
--- a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx
+++ b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx
@@ -150,6 +150,7 @@ export class UniqueFaceBox extends ViewBoxBaseComponent<FieldViewProps>() {
FaceRecognitionHandler.UniqueFaceRemoveFaceImage(imgDoc, this.Document);
}, 'remove doc from face');
+ setRef = (r: HTMLDivElement | null) => this.fixWheelEvents(r, this._props.isContentActive);
render() {
return (
<div className="face-document-item" ref={ele => this.createDropTarget(ele!)}>
@@ -176,7 +177,7 @@ export class UniqueFaceBox extends ViewBoxBaseComponent<FieldViewProps>() {
style={{
pointerEvents: this._props.isContentActive() ? undefined : 'none',
}}
- ref={r => this.fixWheelEvents(r, this._props.isContentActive)}>
+ ref={this.setRef}>
{FaceRecognitionHandler.UniqueFaceImages(this.Document).map((doc, i) => {
const [name, type] = ImageCastToNameType(doc?.[Doc.LayoutDataKey(doc)]) ?? ['-missing-', '.png'];
return (
diff --git a/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx b/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx
index ff9fb14e7..e3a3f9b05 100644
--- a/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx
+++ b/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx
@@ -160,15 +160,16 @@ export class ImageLabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
classifyImagesInBox = async () => {
this.startLoading();
+ const selectedImages = this._selectedImages;
// Converts the images into a Base64 format, afterwhich the information is sent to GPT to label them.
- const imageInfos = this._selectedImages.map(async doc => {
+ const imageInfos = selectedImages.map(async doc => {
if (!doc.$tags_chat) {
const url = ImageCastWithSuffix(doc[Doc.LayoutDataKey(doc)], '_o') ?? '';
return imageUrlToBase64(url).then(hrefBase64 =>
!hrefBase64 ? undefined :
- gptImageLabel(hrefBase64,'Give three labels to describe this image.').then(labels =>
+ gptImageLabel(hrefBase64, 'Give three labels to describe this image.').then(labels =>
({ doc, labels }))) ; // prettier-ignore
}
});
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx
index abd828945..2ec59e5d5 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx
@@ -16,6 +16,7 @@ export class MarqueeOptionsMenu extends AntimodeMenu<AntimodeMenuProps> {
public createCollection: (e: KeyboardEvent | React.PointerEvent | undefined, group?: boolean, selection?: Doc[]) => Doc | void = unimplementedFunction;
public delete: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction;
public summarize: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction;
+ public generateScrapbook: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction;
public showMarquee: () => void = unimplementedFunction;
public hideMarquee: () => void = unimplementedFunction;
public pinWithView: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction;
@@ -38,6 +39,7 @@ export class MarqueeOptionsMenu extends AntimodeMenu<AntimodeMenuProps> {
<IconButton tooltip="Create a Collection" onPointerDown={this.createCollection} icon={<FontAwesomeIcon icon="object-group" />} color={this.userColor} />
<IconButton tooltip="Create a Grouping" onPointerDown={e => this.createCollection(e, true)} icon={<FontAwesomeIcon icon="layer-group" />} color={this.userColor} />
<IconButton tooltip="Summarize Documents" onPointerDown={this.summarize} icon={<FontAwesomeIcon icon="compress-arrows-alt" />} color={this.userColor} />
+ <IconButton tooltip="Generate Scrapbook" onPointerDown={this.generateScrapbook} icon={<FontAwesomeIcon icon="palette" />} color={this.userColor} />
<IconButton tooltip="Delete Documents" onPointerDown={this.delete} icon={<FontAwesomeIcon icon="trash-alt" />} color={this.userColor} />
<IconButton tooltip="Pin selected region" onPointerDown={this.pinWithView} icon={<FontAwesomeIcon icon="map-pin" />} color={this.userColor} />
<IconButton tooltip="Classify and Sort Images" onPointerDown={this.classifyImages} icon={<FontAwesomeIcon icon="object-group" />} color={this.userColor} />
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.scss b/src/client/views/collections/collectionFreeForm/MarqueeView.scss
index 7c9d0f6e1..135f4deac 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.scss
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.scss
@@ -1,7 +1,7 @@
.marqueeView {
position: inherit;
- top: 0;
- left: 0;
+ top: 0px;
+ left: 0px;
width: 100%;
height: 100%;
overflow: hidden;
@@ -20,7 +20,7 @@
pointer-events: none;
.marquee-legend {
bottom: -18px;
- left: 0;
+ left: 0px;
position: absolute;
font-size: 9;
white-space: nowrap;
@@ -28,4 +28,4 @@
.marquee-legend::after {
content: 'Press <space> for lasso';
}
-}
+} \ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 3cc7c0f2d..4191aaca8 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -28,6 +28,9 @@ import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox';
import { SubCollectionViewProps } from '../CollectionSubView';
import { ImageLabelBoxData } from './ImageLabelBox';
import { MarqueeOptionsMenu } from './MarqueeOptionsMenu';
+import { StrListCast } from '../../../../fields/Doc';
+import { requestAiGeneratedPreset, DocumentDescriptor } from '../../nodes/scrapbook/AIPresetGenerator';
+import { buildPlaceholdersFromConfigs, slotRealDocIntoPlaceholders } from '../../nodes/scrapbook/ScrapbookBox';
import './MarqueeView.scss';
interface MarqueeViewProps {
@@ -76,6 +79,11 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
@observable _labelsVisibile: boolean = false;
@observable _lassoPts: [number, number][] = [];
@observable _lassoFreehand: boolean = false;
+ // ─── New Observables for “Pick 1 of N AI Scrapbook” ───
+ @observable aiChoices: Doc[] = []; // temporary hidden Scrapbook docs
+ @observable pickerX = 0; // popup x coordinate
+ @observable pickerY = 0; // popup y coordinate
+ @observable pickerVisible = false; // show/hide ScrapbookPicker
@computed get Transform() {
return this._props.getTransform();
@@ -190,6 +198,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
FormattedTextBox.SelectOnLoadChar = Doc.UserDoc().defaultTextLayout && !this._props.childLayoutString ? e.key : '';
FormattedTextBox.LiveTextUndo = UndoManager.StartBatch('type new note');
this._props.addLiveTextDocument(DocUtils.GetNewTextDoc('-typed text-', x, y, 200, 100));
+ setTimeout(() => FormattedTextBox.LiveTextUndo?.end(), 100);
e.stopPropagation();
}
};
@@ -275,6 +284,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
MarqueeOptionsMenu.Instance.createCollection = this.collection;
MarqueeOptionsMenu.Instance.delete = this.delete;
MarqueeOptionsMenu.Instance.summarize = this.summary;
+ MarqueeOptionsMenu.Instance.generateScrapbook = this.generateScrapbook;
MarqueeOptionsMenu.Instance.showMarquee = this.showMarquee;
MarqueeOptionsMenu.Instance.hideMarquee = this.hideMarquee;
MarqueeOptionsMenu.Instance.jumpTo(e.clientX, e.clientY);
@@ -372,7 +382,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
? creator(selected, { title: 'nested stack' })
: ((doc: Doc) => {
doc.$data = new List<Doc>(selected);
- doc.$isGroup = makeGroup;
+ doc.$freeform_isGroup = makeGroup;
doc.$title = makeGroup ? 'grouping' : 'nested freeform';
doc._freeform_panX = doc._freeform_panY = 0;
return doc;
@@ -508,7 +518,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
_layout_showSidebar: true,
title: 'overview',
});
- const portal = Docs.Create.FreeformDocument(selected, { title: 'summarized documents', x: this.Bounds.left + 200, y: this.Bounds.top, isGroup: true, backgroundColor: 'transparent' });
+ const portal = Docs.Create.FreeformDocument(selected, { title: 'summarized documents', x: this.Bounds.left + 200, y: this.Bounds.top, freeform_isGroup: true, backgroundColor: 'transparent' });
DocUtils.MakeLink(summary, portal, { link_relationship: 'summary of:summarized by' });
portal.hidden = true;
@@ -517,6 +527,77 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
MarqueeOptionsMenu.Instance.fadeOut(true);
});
+ getAiPresetsDescriptors = (): DocumentDescriptor[] =>
+ this.marqueeSelect(false).map(doc => ({
+ type: typeof doc.$type === 'string' ? doc.$type : 'UNKNOWN',
+ tags: Array.from(new Set(StrListCast(doc.$tags_chat))),
+ }));
+
+ generateScrapbook = action(async () => {
+ const selectedDocs = this.marqueeSelect(false);
+ if (!selectedDocs.length) return;
+
+ const descriptors = this.getAiPresetsDescriptors();
+ if (descriptors.length === 0) {
+ alert('No documents selected to generate a scrapbook from!');
+ return;
+ }
+
+ const aiPreset = await requestAiGeneratedPreset(descriptors);
+ if (!aiPreset.length) {
+ alert('Failed to generate preset');
+ return;
+ }
+ const scrapbookPlaceholders: Doc[] = buildPlaceholdersFromConfigs(aiPreset);
+ /*
+ const scrapbookPlaceholders: Doc[] = aiPreset.map(cfg => {
+ const placeholderDoc = Docs.Create.TextDocument(cfg.tag);
+ placeholderDoc.placeholder_docType = cfg.type as DocumentType;
+ placeholderDoc.placeholder_acceptTags = new List<string>(cfg.acceptTags ?? [cfg.tag]);
+
+ const placeholder = new Doc();
+ placeholder.proto = placeholderDoc;
+ placeholder.original = placeholderDoc;
+ placeholder.x = cfg.x;
+ placeholder.y = cfg.y;
+ if (cfg.width != null) placeholder._width = cfg.width;
+ if (cfg.height != null) placeholder._height = cfg.height;
+
+ return placeholder;
+ });*/
+
+ const scrapbook = Docs.Create.ScrapbookDocument(scrapbookPlaceholders, {
+ backgroundColor: '#e2ad32',
+ x: this.Bounds.left,
+ y: this.Bounds.top,
+ _width: 500,
+ _height: 500,
+ title: 'AI-generated Scrapbook',
+ });
+
+ // 3) Now grab that new scrapbook’s flat placeholders
+ const allPlaceholders = DocUtils.unwrapPlaceholders(scrapbookPlaceholders);
+
+ // 4) Slot each selectedDocs[i] into the first matching placeholder
+ selectedDocs.forEach(realDoc => slotRealDocIntoPlaceholders(realDoc, allPlaceholders));
+
+ const selected = selectedDocs.map(d => {
+ this._props.removeDocument?.(d);
+ d.x = NumCast(d.x) - this.Bounds.left;
+ d.y = NumCast(d.y) - this.Bounds.top;
+ return d;
+ });
+
+ this._props.addDocument?.(scrapbook);
+ const portal = Docs.Create.FreeformDocument(selected, { title: 'docs in scrapbook', x: this.Bounds.left + 200, y: this.Bounds.top, isGroup: true, backgroundColor: 'transparent' });
+ DocUtils.MakeLink(scrapbook, portal, { link_relationship: 'scrapbook of:in scrapbook' });
+
+ portal.hidden = true;
+ this._props.addDocument?.(portal);
+ MarqueeOptionsMenu.Instance.fadeOut(true);
+ this.hideMarquee();
+ });
+
@action
marqueeCommand = (e: KeyboardEvent) => {
const ee = e as unknown as KeyboardEvent & { propagationIsStopped?: boolean };
@@ -538,6 +619,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
if (e.key === 'g') this.collection(e, true);
if (e.key === 'c' || e.key === 't') this.collection(e);
if (e.key === 's' || e.key === 'S') this.summary();
+ if (e.key === 'g' || e.key === 'G') this.generateScrapbook(); // ← scrapbook shortcut
if (e.key === 'p') this.pileup();
this.cleanupInteractions(false);
}
@@ -680,22 +762,21 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
}
e.stopPropagation();
};
+ setRef = (r: HTMLDivElement | null) => {
+ r?.addEventListener('dashDragMovePause', this.onDragMovePause as EventListenerOrEventListenerObject);
+ this.MarqueeRef = r;
+ };
render() {
return (
<div
className="marqueeView"
- ref={r => {
- r?.addEventListener('dashDragMovePause', this.onDragMovePause as EventListenerOrEventListenerObject);
- this.MarqueeRef = r;
- }}
+ ref={this.setRef}
style={{
overflow: StrCast(this._props.Document._overflow),
cursor: Doc.ActiveTool === InkTool.Ink || this._visible ? 'crosshair' : 'pointer',
}}
onDragOver={e => e.preventDefault()}
- onScroll={e => {
- e.currentTarget.scrollTop = e.currentTarget.scrollLeft = 0;
- }}
+ onScroll={e => (e.currentTarget.scrollTop = (e.currentTarget.scrollLeft = 0))} // prettier-ignore
onClick={this.onClick}
onPointerDown={this.onPointerDown}>
{this._visible ? this.marqueeDiv : null}
diff --git a/src/client/views/collections/collectionGrid/CollectionGridView.scss b/src/client/views/collections/collectionGrid/CollectionGridView.scss
index 4edaf9745..b95d3ea44 100644
--- a/src/client/views/collections/collectionGrid/CollectionGridView.scss
+++ b/src/client/views/collections/collectionGrid/CollectionGridView.scss
@@ -39,8 +39,8 @@
background: #d3d3d3;
position: absolute;
- height: 3;
- left: 5;
+ height: 3px;
+ left: 5px;
transform-origin: left;
transform: rotate(90deg);
outline: none;
@@ -133,7 +133,7 @@
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
- margin: 0;
+ margin: 0px;
}
/* Firefox */
diff --git a/src/client/views/collections/collectionLinear/CollectionLinearView.scss b/src/client/views/collections/collectionLinear/CollectionLinearView.scss
index 0dfaed38a..0053d3e60 100644
--- a/src/client/views/collections/collectionLinear/CollectionLinearView.scss
+++ b/src/client/views/collections/collectionLinear/CollectionLinearView.scss
@@ -47,7 +47,7 @@
background: global.$medium-blue;
display: flex;
border-radius: 10px;
- height: 35;
+ height: 35px;
transform: translate3d(6px, 0px, 0px);
align-content: center;
justify-content: center;
@@ -95,11 +95,11 @@
pointer-events: all;
cursor: pointer;
background-color: global.$medium-blue;
- padding: 5;
+ padding: 5px;
border-radius: 2px;
height: 100%;
- min-width: 25;
- margin: 0;
+ min-width: 25px;
+ margin: 0px;
color: global.$white;
display: flex;
font-weight: 100;
diff --git a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
index d0a1e6f0d..435f618d9 100644
--- a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
+++ b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
@@ -158,14 +158,13 @@ export class CollectionLinearView extends CollectionSubView() {
let dref: Opt<HTMLDivElement>;
const docXf = () => this.getTransform(dref);
+ const setRef = (r: HTMLDivElement | null) => (dref = r || undefined);
// const scalable = pair.layout.onClick || pair.layout.onDragStart;
return hidden ? null : (
<div
className={preview ? 'preview' : `collectionLinearView-docBtn`}
key={doc[Id]}
- ref={r => {
- dref = r || undefined;
- }}
+ ref={setRef}
style={{
pointerEvents: 'all',
width: NumCast(doc._width),
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss
index 9ed247d50..1dc46102f 100644
--- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss
@@ -1,8 +1,8 @@
.collectionMulticolumnView_drop {
height: 100%;
width: 100%;
- top: 0;
- left: 0;
+ top: 0px;
+ left: 0px;
position: absolute;
.collectionMulticolumnView_contents {
@@ -18,8 +18,8 @@
align-items: center;
position: relative;
> .iconButton-container {
- top: 0;
- left: 0;
+ top: 0px;
+ left: 0px;
position: absolute;
}
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.scss b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.scss
index 91779065d..eb157d155 100644
--- a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.scss
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.scss
@@ -1,8 +1,8 @@
.collectionMultirowView_drop {
height: 100%;
width: 100%;
- top: 0;
- left: 0;
+ top: 0px;
+ left: 0px;
position: absolute;
.collectionMultirowView_contents {
diff --git a/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx b/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx
index 10a6fa2e9..2ff99f134 100644
--- a/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx
+++ b/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable react/require-default-props */
import { action } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
diff --git a/src/client/views/collections/collectionMulticolumn/MultirowHeightLabel.tsx b/src/client/views/collections/collectionMulticolumn/MultirowHeightLabel.tsx
index 66215f109..4f57e1656 100644
--- a/src/client/views/collections/collectionMulticolumn/MultirowHeightLabel.tsx
+++ b/src/client/views/collections/collectionMulticolumn/MultirowHeightLabel.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable react/require-default-props */
import { computed } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
diff --git a/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx b/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx
index 918365700..1954b4743 100644
--- a/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx
+++ b/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable react/require-default-props */
import { action } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
@@ -66,7 +65,7 @@ export default class ResizeBar extends React.Component<ResizerProps> {
style={{
pointerEvents: this.props.isContentActive?.() ? 'all' : 'none',
height: this.props.height,
- backgroundColor: !this.props.isContentActive?.() ? '' : this.props.styleProvider?.(undefined, undefined, StyleProp.WidgetColor) as string,
+ backgroundColor: !this.props.isContentActive?.() ? '' : (this.props.styleProvider?.(undefined, undefined, StyleProp.WidgetColor) as string),
}}>
<div className="multiRowResizer-hdl" onPointerDown={e => this.registerResizing(e)} />
</div>
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.scss b/src/client/views/collections/collectionSchema/CollectionSchemaView.scss
index 53c0823ea..e975ae6f6 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaView.scss
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.scss
@@ -18,7 +18,7 @@
.schema-add {
position: relative;
- height: 35;
+ height: 35px;
display: flex;
align-items: center;
top: -10px;
@@ -147,7 +147,7 @@
flex-direction: row;
justify-content: space-between;
align-items: center;
- padding: 0;
+ padding: 0px;
z-index: 1;
border: 1px solid global.$medium-gray;
@@ -231,7 +231,7 @@
overflow-x: hidden;
overflow-y: auto;
display: inline-flex;
- padding: 0;
+ padding: 0px;
align-items: center;
input[type='text'] {
border: unset;
@@ -272,8 +272,8 @@
.row-menu-infos {
position: absolute;
- top: 3;
- left: 3;
+ top: 3px;
+ left: 3px;
z-index: 1;
display: flex;
justify-content: flex-end;
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
index 6442385c0..2576bdf9b 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
@@ -1043,17 +1043,16 @@ export class CollectionSchemaView extends CollectionSubView() {
if (!this._oldKeysWheel?.scrollTop && e.deltaY <= 0) e.preventDefault();
e.stopPropagation();
};
+ setRef = (r: HTMLDivElement | null) => {
+ this._oldKeysWheel?.removeEventListener('wheel', this.onKeysPassiveWheel);
+ this._oldKeysWheel = r;
+ r?.addEventListener('wheel', this.onKeysPassiveWheel, { passive: false });
+ };
_oldKeysWheel: HTMLDivElement | null = null;
@computed get keysDropdown() {
return (
<div className="schema-key-search">
- <div
- className="schema-key-list"
- ref={r => {
- this._oldKeysWheel?.removeEventListener('wheel', this.onKeysPassiveWheel);
- this._oldKeysWheel = r;
- r?.addEventListener('wheel', this.onKeysPassiveWheel, { passive: false });
- }}>
+ <div className="schema-key-list" ref={this.setRef}>
{this._menuKeys.map(key => (
<div
key={key}
@@ -1294,6 +1293,9 @@ export class CollectionSchemaView extends CollectionSubView() {
screenToLocal = () => this.ScreenToLocalBoxXf().translate(-this.tableWidth, 0);
previewWidthFunc = () => this.previewWidth;
displayedDocsFunc = () => this.docsWithDrag.docs;
+ setColHdrRef = (r: SchemaColumnHeader | null) => r && this._headerRefs.push(r);
+ setPreviewRef = (r: HTMLDivElement | null) => (this._previewRef = r);
+
render() {
return (
<div
@@ -1331,7 +1333,7 @@ export class CollectionSchemaView extends CollectionSubView() {
{this.columnKeys.map((key, index) => (
<SchemaColumnHeader
//cleanupField={this.cleanupComputedField}
- ref={r => r && this._headerRefs.push(r)}
+ ref={this.setColHdrRef}
keysDropdown={this.keysDropdown}
schemaView={this}
columnWidth={() => CollectionSchemaView._minColWidth} //TODO: update
@@ -1379,11 +1381,7 @@ export class CollectionSchemaView extends CollectionSubView() {
</div>
{this.previewWidth > 0 && <div className="schema-preview-divider" style={{ width: CollectionSchemaView._previewDividerWidth }} onPointerDown={this.onDividerDown} />}
{this.previewWidth > 0 && (
- <div
- style={{ width: `${this.previewWidth}px` }}
- ref={ref => {
- this._previewRef = ref;
- }}>
+ <div style={{ width: `${this.previewWidth}px` }} ref={this.setPreviewRef}>
{Array.from(this._selectedDocs).lastElement() && (
<DocumentView
Document={Array.from(this._selectedDocs).lastElement()}
diff --git a/src/client/views/collections/collectionSchema/SchemaCellField.tsx b/src/client/views/collections/collectionSchema/SchemaCellField.tsx
index 9ad94cb31..412daa105 100644
--- a/src/client/views/collections/collectionSchema/SchemaCellField.tsx
+++ b/src/client/views/collections/collectionSchema/SchemaCellField.tsx
@@ -341,12 +341,14 @@ export class SchemaCellField extends ObservableReactComponent<SchemaCellFieldPro
return <span className="editableView-static">{this._props.fieldContents ? <FieldView {...this._props.fieldContents} /> : ''}</span>;
};
+ setRef = (r: HTMLDivElement | null) => (this._inputref = r);
+
renderEditor = () => {
return (
<div
contentEditable
className="schemaField-editing"
- ref={r => (this._inputref = r)}
+ ref={this.setRef}
style={{ 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))}
onInput={this.onChange}
diff --git a/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx b/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx
index 134f2ed31..64bfab856 100644
--- a/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx
+++ b/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx
@@ -118,6 +118,10 @@ export class SchemaColumnHeader extends ObservableReactComponent<SchemaColumnHea
return { color, fieldProps, cursor };
};
+ setRef = (r: EditableView | null) => {
+ this._inputRef = r;
+ this._props.autoFocus && r?.setIsFocused(true);
+ };
@computed get editableView() {
const { color, fieldProps } = this.renderProps(this._props);
@@ -133,10 +137,7 @@ export class SchemaColumnHeader extends ObservableReactComponent<SchemaColumnHea
width: '100%',
}}>
<EditableView
- ref={r => {
- this._inputRef = r;
- this._props.autoFocus && r?.setIsFocused(true);
- }}
+ ref={this.setRef}
oneLine={true}
allowCRs={false}
contents={''}
diff --git a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
index 8b34b4139..02e0d8100 100644
--- a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
+++ b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
@@ -185,6 +185,7 @@ export class SchemaTableCell extends ObservableReactComponent<SchemaTableCellPro
return eqSymbol + modField;
};
+ setRef = (r: SchemaCellField | null) => selectedCell(this._props) && this._props.autoFocus && r?.setIsFocused(true);
@computed get defaultCellContent() {
const { color, textDecoration, fieldProps, pointerEvents } = SchemaTableCell.renderProps(this._props);
@@ -204,7 +205,7 @@ export class SchemaTableCell extends ObservableReactComponent<SchemaTableCellPro
Doc={this._props.Doc}
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)}
+ ref={this.setRef}
oneLine={this._props.oneLine}
contents={undefined}
fieldContents={fieldProps}
diff --git a/src/client/views/global/globalCssVariables.module.scss b/src/client/views/global/globalCssVariables.module.scss
index 82f6caa52..7641d4929 100644
--- a/src/client/views/global/globalCssVariables.module.scss
+++ b/src/client/views/global/globalCssVariables.module.scss
@@ -75,7 +75,7 @@ $CAROUSEL3D_CENTER_SCALE: 1.3;
$CAROUSEL3D_SIDE_SCALE: 0.6;
$CAROUSEL3D_TOP: 15;
-$DATA_VIZ_TABLE_ROW_HEIGHT: 30;
+$DATA_VIZ_TABLE_ROW_HEIGHT: 30px;
:export {
contextMenuZindex: $contextMenu-zindex;
diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts
index cb3adae10..e098d50d8 100644
--- a/src/client/views/global/globalScripts.ts
+++ b/src/client/views/global/globalScripts.ts
@@ -408,7 +408,7 @@ function setActiveTool(tool: InkTool | InkEraserTool | InkInkTool | Gestures, ke
}
runInAction(() => {
const eraserTool = tool === InkTool.Eraser ? Doc.ActiveEraser : [InkEraserTool.Stroke, InkEraserTool.Radius, InkEraserTool.Segment].includes(tool as InkEraserTool) ? (tool as InkEraserTool) : undefined;
- const inkTool = tool === InkTool.Ink ? Doc.ActiveInk : [InkInkTool.Pen, InkInkTool.Write, InkInkTool.Highlight].includes(tool as InkInkTool) ? (tool as InkInkTool) : undefined;
+ const inkTool = tool === InkTool.Ink ? Doc.ActiveInk : [InkInkTool.Pen, InkInkTool.Write, InkInkTool.Math, InkInkTool.Highlight].includes(tool as InkInkTool) ? (tool as InkInkTool) : undefined;
if (GestureOverlay.Instance) {
SnappingManager.SetKeepGestureMode(keepPrim);
}
diff --git a/src/client/views/linking/LinkMenuItem.scss b/src/client/views/linking/LinkMenuItem.scss
index 3cd60c87f..cc8c168cf 100644
--- a/src/client/views/linking/LinkMenuItem.scss
+++ b/src/client/views/linking/LinkMenuItem.scss
@@ -114,7 +114,7 @@
.linkMenu-deleteButton {
width: 20px;
height: 20px;
- margin: 0;
+ margin: 0px;
margin-right: 4px;
padding-right: 6px;
border-radius: 50%;
@@ -134,7 +134,7 @@
}
&:last-child {
- margin-right: 0;
+ margin-right: 0px;
}
&:hover {
diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx
index c984a7053..6c1ad568d 100644
--- a/src/client/views/linking/LinkMenuItem.tsx
+++ b/src/client/views/linking/LinkMenuItem.tsx
@@ -105,11 +105,7 @@ export class LinkMenuItem extends ObservableReactComponent<LinkMenuItemProps> {
LinkManager.Instance.currentLinkAnchor = LinkManager.Instance.currentLink ? this.sourceAnchor : undefined;
if ((SnappingManager.PropertiesWidth ?? 0) < 100) {
- setTimeout(
- action(() => {
- SnappingManager.SetPropertiesWidth(250);
- })
- );
+ setTimeout(action(() => SnappingManager.SetPropertiesWidth(250)));
}
}
})
@@ -136,14 +132,14 @@ export class LinkMenuItem extends ObservableReactComponent<LinkMenuItemProps> {
this._props.itemHandler?.(this._props.linkDoc);
} else {
const focusDoc =
- Cast(this._props.linkDoc.link_anchor_1, Doc, null)?.annotationOn === this._props.sourceDoc
- ? Cast(this._props.linkDoc.link_anchor_1, Doc, null)
- : Cast(this._props.linkDoc.link_anchor_2, Doc, null)?.annotationOn === this._props.sourceDoc
- ? Cast(this._props.linkDoc.link_anchor_12, Doc, null)
- : undefined;
+ DocCast(this._props.linkDoc.link_anchor_1)?.annotationOn === this._props.sourceDoc
+ ? DocCast(this._props.linkDoc.link_anchor_1)
+ : DocCast(this._props.linkDoc.link_anchor_2)?.annotationOn === this._props.sourceDoc
+ ? DocCast(this._props.linkDoc.link_anchor_2)
+ : undefined; // prettier-ignore
if (focusDoc) this._props.docView._props.focus(focusDoc, { instant: true });
- DocumentView.FollowLink(this._props.linkDoc, this._props.sourceDoc, false);
+ DocumentView.FollowLink(this._props.linkDoc, focusDoc ?? this._props.sourceDoc, false);
}
}
);
diff --git a/src/client/views/linking/LinkPopup.scss b/src/client/views/linking/LinkPopup.scss
index 4bfb4b0b9..f8d724767 100644
--- a/src/client/views/linking/LinkPopup.scss
+++ b/src/client/views/linking/LinkPopup.scss
@@ -1,7 +1,9 @@
.linkPopup-container {
background: white;
- box-shadow: 0 10px 20px rgba(0, 0, 0, 0.19), 0 6px 6px rgba(0, 0, 0, 0.23);
- top: 0;
+ box-shadow:
+ 0 10px 20px rgba(0, 0, 0, 0.19),
+ 0 6px 6px rgba(0, 0, 0, 0.23);
+ top: 0px;
height: 200px;
width: 200px;
// padding: 15px;
diff --git a/src/client/views/linking/LinkPopup.tsx b/src/client/views/linking/LinkPopup.tsx
index b654f9bd0..760850241 100644
--- a/src/client/views/linking/LinkPopup.tsx
+++ b/src/client/views/linking/LinkPopup.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable react/require-default-props */
import { observer } from 'mobx-react';
import * as React from 'react';
import { returnEmptyFilter, returnFalse, returnTrue } from '../../../ClientUtils';
diff --git a/src/client/views/newlightbox/NewLightboxView.scss b/src/client/views/newlightbox/NewLightboxView.scss
index 76c34bcf9..c76a7d60d 100644
--- a/src/client/views/newlightbox/NewLightboxView.scss
+++ b/src/client/views/newlightbox/NewLightboxView.scss
@@ -2,8 +2,8 @@
.newLightboxView-frame {
position: absolute;
- top: 0;
- left: 0;
+ top: 0px;
+ left: 0px;
width: 100%;
height: 100%;
background: #474545bb;
diff --git a/src/client/views/newlightbox/components/Recommendation/Recommendation.scss b/src/client/views/newlightbox/components/Recommendation/Recommendation.scss
index cf6b5ccb1..09d3ccc62 100644
--- a/src/client/views/newlightbox/components/Recommendation/Recommendation.scss
+++ b/src/client/views/newlightbox/components/Recommendation/Recommendation.scss
@@ -135,7 +135,7 @@
font-size: 10px;
width: 100%;
background: newstyles.$blue-l1;
- border-radius: 0;
+ border-radius: 0px;
padding: 10px;
.concepts-container {
diff --git a/src/client/views/nodes/AudioBox.scss b/src/client/views/nodes/AudioBox.scss
index 933a383ea..c25c09af9 100644
--- a/src/client/views/nodes/AudioBox.scss
+++ b/src/client/views/nodes/AudioBox.scss
@@ -138,7 +138,7 @@
input[type='range']::-webkit-slider-thumb {
box-shadow: 0;
- border: 0;
+ border: 0px;
height: 10px;
width: 10px;
border-radius: 10px;
@@ -168,7 +168,7 @@
.audiobox-button {
width: 15px;
height: 15px;
- margin: 0;
+ margin: 0px;
svg {
width: 10px;
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.scss b/src/client/views/nodes/CollectionFreeFormDocumentView.scss
index 7f0a39550..300533df8 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.scss
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.scss
@@ -3,7 +3,7 @@
position: absolute;
background-color: transparent;
touch-action: manipulation;
- top: 0;
- left: 0;
+ top: 0px;
+ left: 0px;
pointer-events: none;
}
diff --git a/src/client/views/nodes/ComparisonBox.scss b/src/client/views/nodes/ComparisonBox.scss
index d2ba9796b..cbbd6bde3 100644
--- a/src/client/views/nodes/ComparisonBox.scss
+++ b/src/client/views/nodes/ComparisonBox.scss
@@ -18,7 +18,7 @@
}
.input-box {
position: absolute;
- top: 50;
+ top: 50px;
padding: 10px;
width: 100%;
height: 70%;
@@ -33,7 +33,7 @@
padding-right: 5px;
border-radius: 2px;
height: 17%;
- bottom: 0;
+ bottom: 0px;
overflow: hidden;
display: flex;
width: 100%;
@@ -101,7 +101,7 @@
position: absolute;
display: inline-block;
margin-top: 150px;
- bottom: 0;
+ bottom: 0px;
}
.dropup-content {
@@ -145,8 +145,8 @@
.clip-div {
position: absolute;
- top: 0;
- left: 0;
+ top: 0px;
+ left: 0px;
height: 100%;
overflow: hidden;
@@ -180,8 +180,8 @@
.afterBox-cont {
position: absolute;
- top: 0;
- right: 0;
+ top: 0px;
+ right: 0px;
height: 100%;
width: 100%;
overflow: hidden;
@@ -331,8 +331,8 @@
justify-content: space-between;
height: max-content;
position: absolute;
- bottom: 0;
- right: 2;
+ bottom: 0px;
+ right: 2px;
flex-direction: row-reverse;
display: flex;
cursor: pointer;
diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.scss b/src/client/views/nodes/DataVizBox/DataVizBox.scss
index 9825d926f..32a01355e 100644
--- a/src/client/views/nodes/DataVizBox/DataVizBox.scss
+++ b/src/client/views/nodes/DataVizBox/DataVizBox.scss
@@ -37,25 +37,25 @@
margin-left: 10px;
margin-bottom: -10px;
}
-
+
.displaySchemaLive {
margin-bottom: 20px;
}
.dataviz-sidebar {
position: absolute;
- right: 0;
- top: 0;
+ right: 0px;
+ top: 0px;
height: 100%;
}
.button-container {
pointer-events: unset;
}
- .dataVizBox-annotationLayer{
+ .dataVizBox-annotationLayer {
position: absolute;
transform-origin: left top;
- top: 0;
+ top: 0px;
width: 100%;
pointer-events: none;
mix-blend-mode: multiply;
diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
index 9369ff98a..0b7033e57 100644
--- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx
+++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
@@ -420,6 +420,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
}
};
+ setRef = (r: LineChart | Histogram | PieChart | null) => (this._vizRenderer = r ?? undefined);
// toggles for user to decide which chart type to view the data in
@computed get renderVizView() {
const scale = this._props.NativeDimScaling?.() || 1;
@@ -437,9 +438,9 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
if (!this.records.length) return 'no data/visualization';
switch (this.dataVizView) {
case DataVizView.TABLE: return <TableBox {...sharedProps} Doc={this.Document} specHighlightedRow={this._specialHighlightedRow} docView={this.DocumentView} selectAxes={this.selectAxes} selectTitleCol={this.selectTitleCol}/>;
- case DataVizView.LINECHART: return <LineChart {...sharedProps} Doc={this.Document} dataDoc={this.dataDoc} fieldKey={this.fieldKey} ref={r => {this._vizRenderer = r ?? undefined;}} vizBox={this} />;
- case DataVizView.HISTOGRAM: return <Histogram {...sharedProps} Doc={this.Document} dataDoc={this.dataDoc} fieldKey={this.fieldKey} ref={r => {this._vizRenderer = r ?? undefined;}} />;
- case DataVizView.PIECHART: return <PieChart {...sharedProps} Doc={this.Document} dataDoc={this.dataDoc} fieldKey={this.fieldKey} ref={r => {this._vizRenderer = r ?? undefined;}}
+ case DataVizView.LINECHART: return <LineChart {...sharedProps} Doc={this.Document} dataDoc={this.dataDoc} fieldKey={this.fieldKey} ref={this.setRef} vizBox={this} />;
+ case DataVizView.HISTOGRAM: return <Histogram {...sharedProps} Doc={this.Document} dataDoc={this.dataDoc} fieldKey={this.fieldKey} ref={this.setRef} />;
+ case DataVizView.PIECHART: return <PieChart {...sharedProps} Doc={this.Document} dataDoc={this.dataDoc} fieldKey={this.fieldKey} ref={this.setRef}
margin={{ top: 10, right: 15, bottom: 15, left: 15 }} />;
default:
} // prettier-ignore
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/Backend/TemplateManager.ts b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Backend/TemplateManager.ts
new file mode 100644
index 000000000..526fcf9c4
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Backend/TemplateManager.ts
@@ -0,0 +1,91 @@
+import { action, makeAutoObservable } from 'mobx';
+import { Col } from '../DocCreatorMenu';
+import { FieldSettings, TemplateField } from '../TemplateFieldTypes/TemplateField';
+import { Template } from '../Template';
+import { NumListCast } from '../../../../../../fields/Doc';
+import { DataVizBox } from '../../DataVizBox';
+import { TemplateFieldType } from '../TemplateBackend';
+import { TemplateMenuAIUtils } from './TemplateMenuAIUtils';
+
+export type Conditional = {
+ field: string;
+ operator: '=' | '>' | '<' | 'contains';
+ condition: string;
+ target: string;
+ attribute: string;
+ value: string;
+};
+
+export class TemplateManager {
+ _templates: Template[] = [];
+
+ _conditionalFieldLogic: Record<string, Conditional[]> = {};
+
+ constructor(templateSettings: FieldSettings[]) {
+ makeAutoObservable(this);
+ this._templates = templateSettings.map(settings => new Template(settings));
+ }
+
+ getValidTemplates = (cols: Col[]) => this._templates.filter(template => template.isValidTemplate(cols));
+
+ addTemplate = (newTemplate: Template) => this._templates.push(newTemplate);
+
+ removeTemplate = (template: Template) => {
+ if (this._templates.includes(template)) {
+ this._templates.splice(this._templates.indexOf(template), 1);
+ }
+ template.cleanup();
+ };
+
+ addFieldCondition = (fieldTitle: string, condition: Conditional) => {
+ if (this._conditionalFieldLogic[fieldTitle] === undefined) {
+ this._conditionalFieldLogic[fieldTitle] = [condition];
+ } else {
+ this._conditionalFieldLogic[fieldTitle].push(condition);
+ }
+ };
+
+ removeFieldCondition = (fieldTitle: string, condition: Conditional) => (this._conditionalFieldLogic[fieldTitle] = this._conditionalFieldLogic[fieldTitle]?.filter(cond => cond !== condition));
+
+ addDataField = (title: string) => this._templates.forEach(template => template.addDataField(title));
+
+ removeDataField = (title: string) => this._templates.forEach(template => template.removeDataField(title));
+
+ createDocsFromTemplate = action((dv: DataVizBox, template: Template, cols: Col[], debug: boolean = false) => {
+ const csvFields = Array.from(Object.keys(dv.records[0]));
+
+ const processContent = (content: { [title: string]: string }) => {
+ const templateCopy = template.clone();
+
+ csvFields
+ .filter(title => title)
+ .forEach(title => {
+ const field = templateCopy.getFieldByTitle(title);
+ field?.setContent(content[title], field.viewType);
+ });
+
+ const gptFunc = (type: TemplateFieldType) => (type === TemplateFieldType.VISUAL ? TemplateMenuAIUtils.renderGPTImageCall : TemplateMenuAIUtils.renderGPTTextCall);
+
+ const generateGptContent = cols
+ .map(field => ({ field, templateField: field?.AIGenerated && templateCopy.getFieldByTitle(field.title) }))
+ .filter(({ templateField }) => templateField instanceof TemplateField)
+ .map(({ field, templateField }) => gptFunc(field.type)(templateCopy, field, (templateField as TemplateField).getID));
+
+ return Promise.all(generateGptContent).then(() => templateCopy.applyConditionalLogic(this._conditionalFieldLogic));
+ };
+
+ const rowContents = debug
+ ? [{}, {}, {}, {}]
+ : NumListCast(dv.layoutDoc.dataViz_selectedRows).map(row =>
+ csvFields.reduce(
+ (values, col) => {
+ values[col] = dv.records[row][col];
+ return values;
+ },
+ {} as { [title: string]: string }
+ )
+ );
+
+ return Promise.all(rowContents.map(processContent));
+ });
+}
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/Backend/TemplateMenuAIUtils.ts b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Backend/TemplateMenuAIUtils.ts
new file mode 100644
index 000000000..08818dd6c
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Backend/TemplateMenuAIUtils.ts
@@ -0,0 +1,122 @@
+import { ClientUtils } from '../../../../../../ClientUtils';
+import { Networking } from '../../../../../Network';
+import { gptImageCall, gptAPICall, GPTCallType } from '../../../../../apis/gpt/GPT';
+import { Col } from '../DocCreatorMenu';
+import { TemplateFieldSize, TemplateFieldType } from '../TemplateBackend';
+import { ViewType } from '../TemplateFieldTypes/TemplateField';
+import { Template } from '../Template';
+import { Upload } from '../../../../../../server/SharedMediaTypes';
+
+export class TemplateMenuAIUtils {
+ public static generateGPTImage = async (prompt: string): Promise<string | undefined> => {
+ try {
+ const res = await gptImageCall(prompt);
+
+ if (res) {
+ const result = (await Networking.PostToServer('/uploadRemoteImage', { sources: res })) as Upload.FileInformation[];
+ const source = ClientUtils.prepend(result[0].accessPaths.agnostic.client);
+ return source;
+ }
+ } catch (e) {
+ console.log(e);
+ }
+ };
+
+ public static renderGPTImageCall = async (template: Template, col: Col, fieldNumber: number): Promise<boolean> => {
+ const generateAndLoadImage = async (id: number, prompt: string) => {
+ const field = template.getFieldByID(id);
+ const url = await this.generateGPTImage(prompt);
+ field?.setContent(url ?? '', ViewType.IMG);
+ field?.setTitle(col.title);
+ };
+
+ const fieldContent: string = template.compiledContent;
+
+ try {
+ const sysPrompt =
+ `#${Math.random() * 100}: Your job is to create a prompt for an AI image generator to help it generate an image based on existing content in a template and a user prompt. Your prompt should focus heavily on visual elements to help the image generator; avoid unecessary info that might distract it. ONLY INCLUDE THE PROMPT, NO OTHER TEXT OR EXPLANATION. The existing content is as follows: ` +
+ fieldContent +
+ ' **** The user prompt is: ' +
+ col.desc;
+
+ const prompt = await gptAPICall(sysPrompt, GPTCallType.COMPLETEPROMPT);
+
+ await generateAndLoadImage(fieldNumber, prompt);
+ } catch (e) {
+ console.log(e);
+ }
+ return true;
+ };
+
+ public static renderGPTTextCall = async (template: Template, col: Col, fieldNum: number | undefined): Promise<boolean> => {
+ const wordLimit = (size: TemplateFieldSize) => {
+ switch (size) {
+ case TemplateFieldSize.TINY:
+ return 2;
+ case TemplateFieldSize.SMALL:
+ return 5;
+ case TemplateFieldSize.MEDIUM:
+ return 20;
+ case TemplateFieldSize.LARGE:
+ return 50;
+ case TemplateFieldSize.HUGE:
+ return 100;
+ default:
+ return 10;
+ }
+ };
+
+ const textAssignment = `--- title: ${col.title}, prompt: ${col.desc}, word limit: ${wordLimit(col.sizes[0])} words, assigned field: ${fieldNum} ---`;
+
+ const fieldContent: string = template.compiledContent;
+
+ try {
+ const prompt = fieldContent + textAssignment;
+
+ const res = await gptAPICall(`${Math.random() * 100000}: ${prompt}`, GPTCallType.FILL);
+
+ if (res) {
+ const assignments: { [title: string]: { number: string; content: string } } = JSON.parse(res);
+ Object.entries(assignments).forEach(([, /* title */ info]) => {
+ const field = template.getFieldByID(Number(info.number));
+
+ field?.setContent(info.content ?? '', ViewType.TEXT);
+ field?.setTitle(col.title);
+ });
+ }
+ } catch (err) {
+ console.log(err);
+ }
+
+ return true;
+ };
+
+ /**
+ * Populates a preset template framework with content from a datavizbox or any AI-generated content.
+ * @param template the preloaded template framework being filled in
+ * @param assignments a list of template field numbers (from top to bottom) and their assigned columns from the linked dataviz
+ * @returns a doc containing the fully rendered template
+ */
+ public static applyGPTContentToTemplate = async (template: Template, assignments: { [field: string]: Col }): Promise<Template | undefined> => {
+ const GPTTextCalls = Object.entries(assignments).filter(([, col]) => col.type === TemplateFieldType.TEXT && col.AIGenerated);
+ const GPTIMGCalls = Object.entries(assignments).filter(([, col]) => col.type === TemplateFieldType.VISUAL && col.AIGenerated);
+
+ if (GPTTextCalls.length) {
+ const promises = GPTTextCalls.map(([id, col]) => {
+ return TemplateMenuAIUtils.renderGPTTextCall(template, col, Number(id));
+ });
+
+ await Promise.all(promises);
+ }
+
+ if (GPTIMGCalls.length) {
+ const promises = GPTIMGCalls.map(async ([id, col]) => {
+ return TemplateMenuAIUtils.renderGPTImageCall(template, col, Number(id));
+ });
+
+ await Promise.all(promises);
+ }
+
+ return template;
+ };
+}
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.scss b/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.scss
index 57f4a1e94..e2261b9e2 100644
--- a/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.scss
+++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.scss
@@ -9,10 +9,10 @@
position: absolute;
z-index: 1000;
// box-shadow: 0px 3px 4px rgba(0, 0, 0, 30%);
- // background: whitesmoke;
+ // background: whitesmoke;
// color: black;
border-radius: 3px;
-}
+}
.docCreatorMenu-menu {
display: flex;
@@ -24,57 +24,39 @@
.docCreatorMenu-menu-button {
width: 25px;
height: 25px;
- background: whitesmoke;
+ background: whitesmoke;
background-color: rgb(50, 50, 50);
border-radius: 5px;
- border: 1px solid rgb(180, 180, 180);
padding: 0px;
font-size: 13px;
//box-shadow: 3px 3px rgb(29, 29, 31);
&:hover {
box-shadow: none;
- }
-
- &.right{
- margin-left: 0px;
- font-size: 12px;
+ background-color: rgb(60, 60, 65);
}
- &.close-menu {
- font-size: 12px;
- width: 18px;
- height: 18px;
- font-size: 12px;
- margin-left: auto;
- margin-right: 5px;
- margin-bottom: 3px;
+ &.no-margin {
+ margin: 0px;
}
- &.options {
- margin-left: 0px;
+ &.border {
+ border: 1px solid rgb(180, 180, 180);
}
- &:hover {
- background-color: rgb(60, 60, 65);
+ &.float-right {
+ float: right;
+ margin-left: auto;
}
- &.top-bar {
- border-bottom: 25px solid #555;
- border-left: 12px solid transparent;
- border-right: 12px solid transparent;
- // border-top-left-radius: 5px;
- // border-top-right-radius: 5px;
- border-radius: 0px;
- height: 0;
- width: 50px;
+ &.absolute-right {
+ position: absolute;
+ right: 0px;
}
- &.preview-toggle {
- margin: 0px;
- border-top-left-radius: 0px;
- border-bottom-left-radius: 0px;
- border-left: 0px;
+ &.right {
+ margin-left: 0px;
+ font-size: 12px;
}
}
@@ -121,31 +103,31 @@
}
}
- &:hover::before{
+ &:hover::before {
border-bottom: 20px solid rgb(82, 82, 82);
}
&::before {
- content: "";
+ content: '';
position: absolute;
- top: 0;
- left: 0;
+ top: 0px;
+ left: 0px;
border-bottom: 20px solid rgb(50, 50, 50);
border-left: 12px solid transparent;
border-right: 12px solid transparent;
- height: 0;
+ height: 0px;
width: 50px;
}
&::after {
- content: "";
+ 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;
+ height: 0px;
width: 52px;
z-index: -1;
}
@@ -161,7 +143,7 @@
color: white;
}
-.docCreatorMenu-menu-hr{
+.docCreatorMenu-menu-hr {
margin-top: 0px;
margin-bottom: 0px;
color: rgb(180, 180, 180);
@@ -191,14 +173,14 @@
align-items: center;
width: 40px;
height: 40px;
- background-color: rgb(99, 148, 238);
+ 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);
+ &:hover {
+ background-color: rgb(59, 128, 255);
border: 2px solid rgb(53, 80, 127);
}
}
@@ -206,7 +188,7 @@
.docCreatorMenu-create-docs-button {
width: 40px;
height: 40px;
- background-color: rgb(176, 229, 149);
+ background-color: rgb(176, 229, 149);
border: 2px solid rgb(126, 219, 80);
border-radius: 5px;
padding: 0px;
@@ -217,7 +199,7 @@
&:hover {
background-color: rgb(129, 223, 83);
- border: 2px solid rgb(80, 185, 28);
+ border: 2px solid rgb(80, 185, 28);
}
}
@@ -230,6 +212,14 @@
&.full {
width: 100%;
}
+
+ &.no-margin-bottom {
+ margin-bottom: 0px;
+ }
+
+ &.no-margin-top {
+ margin-top: 0px;
+ }
}
//------------------------------------------------------------------------------------------------------------------------------------------
@@ -240,17 +230,22 @@
position: absolute;
background-color: none;
- &.top, &.bottom {
+ &.top,
+ &.bottom {
height: 10px;
cursor: ns-resize;
}
- &.right, &.left {
+ &.right,
+ &.left {
width: 10px;
cursor: ew-resize;
}
- &.topRight, &.topLeft, &.bottomRight, &.bottomLeft {
+ &.topRight,
+ &.topLeft,
+ &.bottomRight,
+ &.bottomLeft {
height: 15px;
width: 15px;
background-color: none;
@@ -273,22 +268,10 @@
height: calc(100% - 30px);
border: 1px solid rgb(180, 180, 180);
border-radius: 5px;
- -ms-overflow-style: none;
+ -ms-overflow-style: none;
scrollbar-width: none;
}
-.docCreatorMenu-preview-container {
- display: grid;
- grid-template-columns: repeat(2, 140px);
- grid-template-rows: 140px;
- grid-auto-rows: 141px;
- overflow-y: scroll;
- margin: 0px;
- margin-top: 0px;
- width: 100%;
- height: 100%;
-}
-
.docCreatorMenu-expanded-template-preview {
display: flex;
flex-direction: column;
@@ -297,8 +280,9 @@
position: relative;
width: 100%;
height: 100%;
+ flex-grow: 1;
- .top-panel{
+ .top-panel {
width: 100%;
height: 10px;
}
@@ -307,7 +291,7 @@
display: flex;
flex-direction: column;
justify-content: flex-start;
- height: 100%;
+ height: fit-content;
position: absolute;
right: 0px;
top: 0px;
@@ -322,17 +306,15 @@
display: flex;
justify-content: center;
align-items: center;
- width: 113px;
- height: 113px;
- margin-top: 10px;
- margin-left: 10px;
+ height: 100%;
+ width: 100%;
+ aspect-ratio: 1;
color: none;
border: 1px solid rgb(163, 163, 163);
border-radius: 5px;
box-shadow: 5px 5px rgb(29, 29, 31);
- flex: 0 0 auto;
- &:hover{
+ &:hover {
background-color: rgb(72, 72, 73);
}
@@ -377,21 +359,18 @@
&:hover .option-button {
display: block;
}
-
}
-.docCreatorMenu-preview-image{
+.docCreatorMenu-preview-image {
background-color: transparent;
- height: 100px;
- width: 100px;
+ height: 100%;
display: block;
object-fit: contain;
border-radius: 5px;
+}
- &.expanded {
- height: 100%;
- width: 100%;
- }
+.docCreatorMenu-variations-tab {
+ flex-grow: 0.5;
}
.docCreatorMenu-section {
@@ -399,12 +378,12 @@
flex-direction: column;
align-items: center;
position: relative;
+ flex-grow: 1;
+ height: 100%;
+ width: 100%;
margin: 0px;
margin-top: 0px;
margin-bottom: 0px;
- width: 100%;
- height: 200;
- flex: 0 0 auto;
}
.docCreatorMenu-GPT-options-container {
@@ -412,34 +391,35 @@
flex-direction: row;
justify-content: center;
align-items: center;
- position: relative;
- width: auto;
+ position: absolute;
+ left: 50%;
+ bottom: 0px;
margin: 0px;
+ margin-bottom: 10px;
margin-top: 5px;
padding: 0px;
}
.docCreatorMenu-templates-preview-window {
- display: flex;
- flex-direction: row;
- //justify-content: center;
- align-items: center;
- overflow-y: scroll;
- position: relative;
- color: black;
- height: 125px;
+ display: grid;
+ justify-content: space-evenly;
+ row-gap: 2rem;
+ grid-template-columns: repeat(auto-fill, minmax(150px, 50%));
+ margin: 5px;
width: calc(100% - 10px);
- -ms-overflow-style: none;
- scrollbar-width: none;
+ height: 100%;
+ padding-bottom: 40px;
- .loading-spinner {
- justify-self: center;
+ &.scrolling {
+ overflow-y: scroll;
+ max-height: 300px;
+ padding-bottom: 0px;
}
}
-.divvv{
- width: 200;
- height: 200;
+.div {
+ width: 200px;
+ height: 200px;
border: solid 1px white;
}
@@ -447,20 +427,14 @@
position: relative;
display: flex;
flex-direction: row;
+ color: whitesmoke;
width: 100%;
}
-.section-reveal-options {
- margin-top: 0px;
- margin-bottom: 0px;
- margin-right: 0px;
- margin-left: auto;
- border: 0px;
- background: none;
-
- &.float-right {
- float: right;
- }
+.docCreatorMenu-templates-displays {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
}
.docCreatorMenu-section-title {
@@ -478,7 +452,7 @@
.docCreatorMenu-GPT-generate {
height: 30px;
width: 30px;
- background-color: rgb(176, 229, 149);
+ background-color: rgb(176, 229, 149);
border: 1px solid rgb(126, 219, 80);
border-radius: 5px;
padding: 0px;
@@ -489,7 +463,7 @@
&:hover {
background-color: rgb(129, 223, 83);
- border: 2px solid rgb(80, 185, 28);
+ border: 2px solid rgb(80, 185, 28);
}
}
@@ -507,7 +481,7 @@
// DocCreatorMenu options CSS
//--------------------------------------------------------------------------------------------------------------------------------------------
-.docCreatorMenu-option-container{
+.docCreatorMenu-option-container {
display: flex;
width: 180px;
height: 30px;
@@ -517,16 +491,16 @@
margin-top: 10px;
margin-bottom: 10px;
- &.layout{
+ &.layout {
z-index: 5;
}
}
-.docCreatorMenu-option-title{
+.docCreatorMenu-option-title {
display: flex;
width: 140px;
height: 30px;
- background: whitesmoke;
+ background: whitesmoke;
background-color: rgb(34, 34, 37);
border-radius: 5px;
border: 1px solid rgb(180, 180, 180);
@@ -543,7 +517,7 @@
border-radius: 0px;
width: auto;
text-transform: none;
-
+
&.small {
height: 20px;
transform: translateY(-5px);
@@ -614,7 +588,7 @@
}
.docCreatorMenu-configuration-bar {
- width: 200;
+ width: 200px;
gap: 5px;
display: flex;
flex-direction: row;
@@ -637,38 +611,38 @@
height: calc(100% - 30px);
border: 1px solid rgb(180, 180, 180);
border-radius: 5px;
- -ms-overflow-style: none;
+ -ms-overflow-style: none;
scrollbar-width: none;
- .docCreatorMenu-option-container{
+ .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;
+ -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{
+
+ .docCreatorMenu-dropdown-option {
display: flex;
background-color: rgb(42, 42, 46);
border-left: 1px solid rgb(180, 180, 180);
@@ -679,17 +653,30 @@
justify-content: center;
justify-items: center;
padding-top: 3px;
-
+
&:hover {
background-color: rgb(68, 68, 74);
cursor: pointer;
}
}
- }
+ }
}
}
}
+.loading-spinner {
+ position: absolute;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 100%;
+ width: 100%;
+ z-index: 200;
+ font-size: 20px;
+ font-weight: bold;
+ color: #17175e;
+}
+
.docCreatorMenu-layout-preview-window-wrapper {
flex: 0 0 auto;
display: flex;
@@ -722,17 +709,17 @@
width: 100%;
aspect-ratio: 1;
//height: auto;
- // max-width: 240;
- // max-height: 240;
+ // max-width: 240px;
+ // max-height: 240px;
border: 1px solid rgb(180, 180, 180);
border-radius: 5px;
background-color: rgb(34, 34, 37);
- -ms-overflow-style: none;
+ -ms-overflow-style: none;
scrollbar-width: none;
&.small {
- max-width: 100;
- max-height: 100;
+ max-width: 100px;
+ max-height: 100px;
}
.docCreatorMenu-layout-preview-item {
@@ -759,10 +746,10 @@
z-index: 999;
}
- .docCreatorMenu-zoom-button{
+ .docCreatorMenu-zoom-button {
width: 15px;
height: 15px;
- background: whitesmoke;
+ background: whitesmoke;
background-color: rgb(34, 34, 37);
border-radius: 3px;
border: 1px solid rgb(180, 180, 180);
@@ -793,10 +780,11 @@
height: calc(100% - 30px);
border: 1px solid rgb(180, 180, 180);
border-radius: 5px;
- -ms-overflow-style: none;
+ -ms-overflow-style: none;
scrollbar-width: none;
.panels-container {
+ display: flex;
height: 100%;
width: 100%;
flex-direction: column;
@@ -810,114 +798,12 @@
background-color: rgb(50, 50, 50);
}
-// .field-panel {
-// position: relative;
-// display: flex;
-// // align-items: flex-start;
-// flex-direction: column;
-// gap: 5px;
-// padding: 5px;
-// height: 100px;
-// //width: 100%;
-// border: 1px solid rgb(180, 180, 180);
-// margin: 5px;
-// margin-top: 0px;
-// border-radius: 3px;
-// flex: 0 0 auto;
-
-// .properties-wrapper {
-// display: flex;
-// flex-direction: row;
-// align-items: flex-start;
-// gap: 5px;
-
-// .field-property-container {
-// background-color: rgb(40, 40, 40);
-// border: 1px solid rgb(100, 100, 100);
-// border-radius: 3px;
-// width: 30%;
-// height: 25px;
-// padding-left: 3px;
-// align-items: center;
-// color: whitesmoke;
-// }
-
-// .field-type-selection-container {
-// display: flex;
-// flex-direction: row;
-// align-items: center;
-// background-color: rgb(40, 40, 40);
-// border: 1px solid rgb(100, 100, 100);
-// border-radius: 3px;
-// width: 31%;
-// height: 25px;
-// padding-left: 3px;
-// color: whitesmoke;
-
-// .placeholder {
-// color: gray;
-// }
-
-// &:hover .placeholder {
-// display: none;
-// }
-
-// .bubbles {
-// display: none;
-// }
-
-// .text {
-// margin-top: 5px;
-// margin-bottom: 5px;
-// }
-
-// &:hover .bubbles {
-// display: flex;
-// flex-direction: row;
-// align-items: flex-start;
-// }
-
-// &:hover .type-display {
-// display: none;
-// }
-
-// .bubble {
-// margin: 5px;
-// }
-
-// &:hover .bubble {
-// margin-top: 7px;
-// }
-// }
-// }
-
-// .field-description-container {
-// background-color: rgb(40, 40, 40);
-// border: 1px solid rgb(100, 100, 100);
-// border-radius: 3px;
-// width: 100%;
-// height: 100%;
-// resize: none;
-
-// ::-webkit-scrollbar-track {
-// background: none;
-// }
-// }
-
-// .top-right {
-// position: absolute;
-// top: 0px;
-// right: 0px;
-// }
-// }
-// }
-
.field-panel {
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
- height: 285px;
+ height: fit-content;
width: calc(100% - 10px);
border: 1px solid rgb(180, 180, 180);
margin: 5px;
@@ -938,15 +824,22 @@
border-top-right-radius: 5px;
border-top-left-radius: 5px;
width: 100%;
- height: 20px;
+ height: fit-content;
background-color: rgb(50, 50, 50);
color: rgb(168, 167, 167);
+ font-size: medium;
.field-title {
color: whitesmoke;
+ font-size: large;
}
- }
-
+
+ &:hover {
+ background-color: rgb(72, 72, 72);
+ cursor: pointer;
+ }
+ }
+
.opts-bar {
display: flex;
flex-direction: row;
@@ -989,11 +882,11 @@
flex-direction: row;
align-items: flex-start;
}
-
+
&:hover .type-display {
display: none;
}
-
+
.bubble {
margin: 3px;
}
@@ -1028,7 +921,7 @@
flex-direction: row;
align-items: center;
}
-
+
.bubble {
margin: 3px;
margin-right: 4px;
@@ -1038,23 +931,255 @@
.desc-box {
width: 88%;
- height: 50px;
+ height: fit-content;
border: 1px solid rgb(180, 180, 180);
border-radius: 5px;
background-color: rgb(50, 50, 50);
box-shadow: 5px 5px rgb(29, 29, 31);
.content {
- height: calc(100% - 20px);
+ height: fit-content;
width: 100%;
background-color: rgb(50, 50, 50);
border-bottom-right-radius: 5px;
border-bottom-left-radius: 5px;
resize: none;
+ }
+ }
+ }
+ .conditionals-section {
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+ align-items: center;
+ width: 100%;
+
+ .conditionals-title {
+ display: flex;
+ flex-direction: row;
+ width: 100%;
+ justify-content: center;
+ align-items: center;
+ margin: 5px;
+ margin-bottom: 20px;
+ font-size: large;
+ }
+ }
+
+ .form-row {
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-start;
+ align-items: flex-start;
+ color: whitesmoke;
+ width: 100%;
+ height: fit-content;
+ margin-bottom: 15px;
+ flex-wrap: wrap;
+ gap: 5px;
+
+ .form-row-plain-text {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 100%;
+ width: fit-content;
+ padding-top: 2px;
+ padding-bottom: 2px;
+ }
+
+ .operator-options-dropdown {
+ display: flex;
+ flex-direction: column;
+ height: fit-content;
+
+ .operator-dropdown-option {
+ display: none;
+ }
+
+ .operator-dropdown-current {
+ border-radius: 5px;
+ background-color: rgb(50, 50, 50);
+ border: 1px solid rgb(180, 180, 180);
+ text-align: center;
+ padding: 2.25px;
+ padding-left: 4px;
+ padding-right: 4px;
+ }
+
+ &:hover .operator-dropdown-current {
+ border-bottom-right-radius: 0px;
+ border-bottom-left-radius: 0px;
+ }
+
+ &:hover .operator-dropdown-option {
+ display: flex;
+ height: fit-content;
+ align-items: center;
+ border: 1px solid rgb(180, 180, 180);
+ background-color: rgb(50, 50, 50);
+ padding: 2.25px;
+ padding-left: 8px;
+ padding-right: 8px;
+ text-align: center;
+
+ &:hover {
+ background-color: rgb(70, 70, 70);
+ cursor: pointer;
+ }
}
}
+ .form-row-textarea {
+ height: 24px;
+ width: 110px;
+ border-radius: 5px;
+ background-color: rgb(50, 50, 50);
+ border: 1px solid rgb(180, 180, 180);
+ resize: none;
+ overflow-y: scroll;
+ white-space: nowrap;
+ }
+ }
+
+ .form {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: flex-start;
+ width: 80%;
+
+ .form-action-button {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ margin: 3px;
+ cursor: pointer;
+ }
}
+}
+
+//------------------------------------------------------------------------------------------------------------------------------------------
+// EditingWindow CSS
+//--------------------------------------------------------------------------------------------------------------------------------------------
+.docCreatorMenu-editing-firefly-section {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ height: 100%;
+ width: 100%;
+ padding: 5px;
+}
+
+.docCreatorMenu-firefly-options {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: flex-end;
+ height: fit-content;
+ width: 100%;
+}
+
+.docCreatorMenu-variation-prompt-row {
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-start;
+ align-items: center;
+ gap: 15px;
+ height: fit-content;
+ width: 100%;
+}
+
+.docCreatorMenu-variation-prompt-input-textbox {
+ height: 40px;
+ width: 80%;
+ color: white;
+ margin-top: 1%;
+ margin-bottom: 1%;
+ margin-left: 5%;
+ background-color: rgb(50, 50, 50);
+ border-radius: 5px;
+ overflow: hidden;
+ resize: none;
+}
+
+.options‑menu {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 2rem;
+ padding: 0.5rem 1rem;
+ background: rgb(50, 50, 50);
+ color: whitesmoke;
+ font-family: system-ui, sans-serif;
+ font-size: 0.9rem;
+ flex-wrap: wrap;
+}
+
+.menu‑item {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ white-space: nowrap;
+}
+
+.menu‑item input[type='range'] {
+ width: 7rem;
+ accent-color: whitesmoke;
+}
+
+.value {
+ min-width: 2ch;
+ text-align: right;
+}
+
+.switch {
+ gap: 0.75rem;
+ margin-bottom: 0px;
+}
+
+.switch .slider {
+ position: relative;
+ width: 2.2rem;
+ height: 1.1rem;
+ background: whitesmoke;
+ border-radius: 1rem;
+ cursor: pointer;
+ transition: background 0.2s;
+}
+
+.switch .slider::before {
+ content: '';
+ position: absolute;
+ top: 0.1rem;
+ left: 0.1rem;
+ width: 0.9rem;
+ height: 0.9rem;
+ background: rgb(50, 50, 50);
+ border-radius: 50%;
+ transition: transform 0.2s;
+}
+
+.switch input {
+ display: none;
+}
+
+.switch input:checked + .slider {
+ background: #78c2f1;
+}
+
+.switch input:checked + .slider::before {
+ transform: translateX(1.1rem);
+}
+
+.firefly-option-label {
+ padding: 0.2em 0.6em 0.3em;
+ font-size: 100%;
+ color: whitesmoke;
+ text-align: center;
+ margin-bottom: 0px;
+ font-weight: 500;
}
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.tsx
index 64416c26d..8f6ecab57 100644
--- a/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.tsx
+++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.tsx
@@ -1,36 +1,32 @@
+import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Colors } from '@dash/components';
import { action, computed, makeObservable, observable, runInAction } from 'mobx';
import { observer } from 'mobx-react';
-import { IDisposer } from 'mobx-utils';
import * as React from 'react';
-import ReactLoading from 'react-loading';
-import { ClientUtils, returnEmptyFilter, returnFalse, setupMoveUpEvents } from '../../../../../ClientUtils';
+import { returnFalse, setupMoveUpEvents } from '../../../../../ClientUtils';
import { emptyFunction } from '../../../../../Utils';
-import { Doc, NumListCast, StrListCast, returnEmptyDoclist } from '../../../../../fields/Doc';
+import { Doc, StrListCast } from '../../../../../fields/Doc';
import { Id } from '../../../../../fields/FieldSymbols';
-import { ImageCast, StrCast } from '../../../../../fields/Types';
-import { ImageField } from '../../../../../fields/URLField';
-import { Networking } from '../../../../Network';
-import { GPTCallType, gptAPICall, gptImageCall } from '../../../../apis/gpt/GPT';
-import { Docs, DocumentOptions } from '../../../../documents/Documents';
+import { GPTCallType, gptAPICall } from '../../../../apis/gpt/GPT';
import { DragManager } from '../../../../util/DragManager';
import { SnappingManager } from '../../../../util/SnappingManager';
import { UndoManager, undoable } from '../../../../util/UndoManager';
import { ObservableReactComponent } from '../../../ObservableReactComponent';
import { CollectionFreeFormView } from '../../../collections/collectionFreeForm/CollectionFreeFormView';
-import { DocumentView, DocumentViewInternal } from '../../DocumentView';
+import { DocumentView } from '../../DocumentView';
import { OpenWhere } from '../../OpenWhere';
import { DataVizBox } from '../DataVizBox';
import './DocCreatorMenu.scss';
-import { DefaultStyleProvider } from '../../../StyleProvider';
-import { Transform } from '../../../../util/Transform';
-import { TemplateFieldSize, TemplateFieldType, TemplateLayouts } from './TemplateBackend';
-import { TemplateManager } from './TemplateManager';
+import { ViewType } from './TemplateFieldTypes/TemplateField';
import { Template } from './Template';
-import { Field, FieldContentType } from './FieldTypes/Field';
-import { IconProp } from '@fortawesome/fontawesome-svg-core';
-import { Upload } from '../../../../../server/SharedMediaTypes';
+import { TemplateFieldSize, TemplateFieldType, TemplateLayouts } from './TemplateBackend';
+import { TemplateManager } from './Backend/TemplateManager';
+import { TemplateMenuAIUtils } from './Backend/TemplateMenuAIUtils';
+import { TemplatePreviewGrid } from './Menu/TemplatePreviewGrid';
+import { FireflyStructureOptions, TemplateEditingWindow } from './Menu/TemplateEditingWindow';
+import { DocCreatorMenuButton } from './Menu/DocCreatorMenuButton';
+import { TemplatesRenderPreviewWindow } from './Menu/TemplateRenderPreviewWindow';
+import { TemplateMenuFieldOptions } from './Menu/TemplateMenuFieldOptions';
export enum LayoutType {
FREEFORM = 'Freeform',
@@ -61,6 +57,7 @@ export type Col = {
title: string;
type: TemplateFieldType;
defaultContent?: string;
+ AIGenerated?: boolean;
};
interface DocCreateMenuProps {
@@ -72,33 +69,20 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps>
// eslint-disable-next-line no-use-before-define
static Instance: DocCreatorMenu;
- private _disposers: { [name: string]: IDisposer } = {};
-
+ DEBUG_MODE: boolean = false;
private _ref: HTMLDivElement | null = null;
-
private templateManager: TemplateManager;
- @observable _fullyRenderedDocs: Doc[] = [];
- @observable _renderedDocCollectionPreview: Doc | undefined = undefined;
- @observable _renderedDocCollection: Doc | undefined = undefined;
- @observable _docsRendering: boolean = false;
+ @observable _docsRendering: boolean = false; // dictates loading symbol
- @observable _userTemplates: { template: Template; doc: Doc }[] = []; //!!! used to keep track of all templates, should be refactored to work with actual templates and not docs
+ @observable _userTemplates: Template[] = [];
@observable _selectedTemplate: Template | undefined = undefined;
@observable _currEditingTemplate: Template | undefined = undefined;
+ @observable _editedTemplateTrail: Template[] = [];
@observable _userCreatedFields: Col[] = [];
- @observable _selectedCols: { title: string; type: string; desc: string }[] | undefined = [];
-
- @observable _layout: { type: LayoutType; yMargin: number; xMargin: number; columns?: number; repeat: number } = { type: LayoutType.FREEFORM, yMargin: 10, xMargin: 10, columns: 3, repeat: 0 };
- @observable _layoutPreviewScale: number = 1;
- @observable _savedLayouts: DataVizTemplateLayout[] = [];
- @observable _expandedPreview: Doc | undefined = undefined;
@observable _suggestedTemplates: Template[] = [];
- @observable _suggestedTemplatePreviews: { doc: Doc; template: Template }[] = [];
- @observable _GPTOpt: boolean = false;
- @observable _callCount: number = 0;
@observable _GPTLoading: boolean = false;
@observable _pageX: number = 0;
@@ -110,9 +94,8 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps>
@observable _startPos?: { x: number; y: number };
@observable _shouldDisplay: boolean = false;
- @observable _menuContent: 'templates' | 'options' | 'saved' | 'dashboard' = 'templates';
+ @observable _menuContent: 'templates' | 'renderPreview' | 'saved' | 'dashboard' | 'templateEditing' = 'templates';
@observable _dragging: boolean = false;
- @observable _draggingIndicator: boolean = false;
@observable _dataViz?: DataVizBox;
@observable _interactionLock: boolean | undefined;
@observable _snapPt: { x: number; y: number } = { x: 0, y: 0 };
@@ -122,7 +105,8 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps>
@observable _resizeUndo: UndoManager.Batch | undefined = undefined;
@observable _initDimensions: { width: number; height: number; x?: number; y?: number } = { width: 300, height: 400, x: undefined, y: undefined };
@observable _menuDimensions: { width: number; height: number } = { width: 400, height: 400 };
- @observable _editing: boolean = false;
+
+ @observable _variations: Template[] = [];
constructor(props: DocCreateMenuProps) {
super(props);
@@ -134,56 +118,13 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps>
@action setDataViz = (dataViz: DataVizBox) => {
this._dataViz = dataViz;
this._selectedTemplate = undefined;
- this._renderedDocCollection = undefined;
- this._renderedDocCollectionPreview = undefined;
- this._fullyRenderedDocs = [];
- this._suggestedTemplatePreviews = [];
this._suggestedTemplates = [];
this._userCreatedFields = [];
};
- @action addUserTemplate = (template: Template) => {
- this._userTemplates.push({ template: template.cloneBase(), doc: template.getRenderedDoc() });
- };
- @action removeUserTemplate = (template: Template) => {
- this._userTemplates = this._userTemplates.filter(info => info.template !== template);
- };
- @action updateTemplatePreview = (template: Template) => {
- template.renderUpdates();
- const preview = { template: template, doc: template.getRenderedDoc() };
- this._suggestedTemplatePreviews = this._suggestedTemplatePreviews.map(t => { return t.template === preview.template ? preview : t }); //prettier-ignore
- this._userTemplates = this._userTemplates.map(t => { return t.template === preview.template ? preview : t }); //prettier-ignore
- };
@action setSuggestedTemplates = (templates: Template[]) => {
- this._suggestedTemplates = templates;
- this._suggestedTemplatePreviews = templates.map(template => {return {template: template, doc: template.getRenderedDoc()}}); //prettier-ignore
+ this._suggestedTemplates = templates; //prettier-ignore
};
- @computed get docsToRender() {
- return this._selectedTemplate ? NumListCast(this._dataViz?.layoutDoc.dataViz_selectedRows) : [];
- }
-
- @computed get rowsCount() {
- switch (this._layout.type) {
- case LayoutType.FREEFORM:
- return Math.ceil(this.docsToRender.length / (this._layout.columns ?? 1)) ?? 0;
- case LayoutType.CAROUSEL3D:
- return 1.8;
- default:
- return 1;
- }
- }
-
- @computed get columnsCount() {
- switch (this._layout.type) {
- case LayoutType.FREEFORM:
- return this._layout.columns ?? 0;
- case LayoutType.CAROUSEL3D:
- return 3;
- default:
- return 1;
- }
- }
-
@computed get selectedFields() {
return StrListCast(this._dataViz?.layoutDoc._dataViz_axes);
}
@@ -210,10 +151,6 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps>
.concat(this._userCreatedFields);
}
- @computed get canMakeDocs() {
- return this._selectedTemplate !== undefined && this._layout !== undefined;
- }
-
get bounds(): { t: number; b: number; l: number; r: number } {
const rect = this._ref?.getBoundingClientRect();
const bounds = { t: rect?.top ?? 0, b: rect?.bottom ?? 0, l: rect?.left ?? 0, r: rect?.right ?? 0 };
@@ -221,17 +158,11 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps>
}
setUpButtonClick = (e: React.PointerEvent, func: () => void) => {
- setupMoveUpEvents(
- this,
- e,
- returnFalse,
- emptyFunction,
- undoable(clickEv => {
- clickEv.stopPropagation();
- clickEv.preventDefault();
- func();
- }, 'create docs')
- );
+ setupMoveUpEvents(this, e, returnFalse, emptyFunction, clickEv => {
+ clickEv.stopPropagation();
+ clickEv.preventDefault();
+ undoable(func, 'create docs')();
+ });
};
@action
@@ -269,7 +200,6 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps>
}
componentWillUnmount() {
- Object.values(this._disposers).forEach(disposer => disposer?.());
document.removeEventListener('pointerdown', this.onPointerDown, true);
document.removeEventListener('pointerup', this.onPointerUp);
}
@@ -319,10 +249,10 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps>
const { scale, refPt, transl } = this.getResizeVals(thisPt, dragHdl);
!this._interactionLock && runInAction(async () => { // resize selected docs if we're not in the middle of a resize (ie, throttle input events to frame rate)
- this._interactionLock = true;
- const scaleAspect = {x: scale.x, y: scale.y};
- this.resizeView(refPt, scaleAspect, transl); // prettier-ignore
- await new Promise<boolean | undefined>(res => { setTimeout(() => { res(this._interactionLock = undefined)})});
+ this._interactionLock = true;
+ const scaleAspect = {x: scale.x, y: scale.y};
+ this.resizeView(refPt, scaleAspect, transl); // prettier-ignore
+ await new Promise<boolean | undefined>(res => { setTimeout(() => { res(this._interactionLock = undefined)})});
}); // prettier-ignore
return true;
};
@@ -365,66 +295,29 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps>
this._pageY = y + translation.y;
};
- async getIcon(doc: Doc) {
- const docView = DocumentView.getDocumentView(doc);
- if (docView) {
- docView.ComponentView?.updateIcon?.();
- return new Promise<ImageField | undefined>(res => setTimeout(() => res(ImageCast(docView.Document.icon)), 500));
- }
- return undefined;
+ async createDocsForPreview() {
+ return this._dataViz && this._selectedTemplate ? ((await this.templateManager.createDocsFromTemplate(this._dataViz, this._selectedTemplate, this.fieldsInfos, this.DEBUG_MODE)).filter(doc => doc).map(doc => doc!) ?? []) : [];
}
- @action updateSelectedTemplate = async (template: Template) => {
- if (this._selectedTemplate === template) {
- this._selectedTemplate = undefined;
- return;
- } else {
- this._selectedTemplate = template;
- template.renderUpdates();
- this._fullyRenderedDocs = (await this.createDocsFromTemplate(template)) ?? [];
- this.updateRenderedDocCollection();
- }
- };
-
- @action updateSelectedSavedLayout = (layout: DataVizTemplateLayout) => {
- this._layout.xMargin = layout.layout.xMargin;
- this._layout.yMargin = layout.layout.yMargin;
- this._layout.type = layout.layout.type;
- this._layout.columns = layout.columns;
- };
-
- isSelectedLayout = (layout: DataVizTemplateLayout) => {
- return this._layout.xMargin === layout.layout.xMargin && this._layout.yMargin === layout.layout.yMargin && this._layout.type === layout.layout.type && this._layout.columns === layout.columns;
+ @action updateSelectedTemplate = (template: Template) => {
+ this._selectedTemplate = this._selectedTemplate === template ? undefined : template; // toggle selection
};
- editTemplate = (doc: Doc) => {
- DocumentViewInternal.addDocTabFunc(doc, OpenWhere.addRight);
- DocumentView.DeselectAll();
- Doc.UnBrushDoc(doc);
- };
+ // testTemplate = async () => {
+ // this._suggestedTemplates = this.templateManager.templates; //prettier-ignore
+ // };
@action addField = () => {
- const newFields: Col[] = this._userCreatedFields.concat([{ title: '', type: TemplateFieldType.UNSET, desc: '', sizes: [] }]);
- this._userCreatedFields = newFields;
+ this._userCreatedFields = this._userCreatedFields.concat([{ title: '', type: TemplateFieldType.UNSET, desc: '', sizes: [], AIGenerated: true }]);
};
@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._userCreatedFields.filter(f => f === field);
- if (!toRemove) return;
-
- if (toRemove.length > 1) {
- while (toRemove.length > 1) {
- toRemove.pop();
- }
- }
-
- if (this._userCreatedFields.length === 1) {
- this._userCreatedFields = [];
- } else {
- this._userCreatedFields.splice(this._userCreatedFields.indexOf(toRemove[0]), 1);
+ const toRemove = this._userCreatedFields.findIndex(f => f === field);
+ if (toRemove !== -1) {
+ this._userCreatedFields.splice(toRemove, 1);
}
}
};
@@ -439,11 +332,18 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps>
};
@action setColType = (column: Col, type: TemplateFieldType) => {
+ if (type === TemplateFieldType.DATA) {
+ this.templateManager.addDataField(column.title);
+ } else if (column.type === TemplateFieldType.DATA) {
+ this.templateManager.removeDataField(column.title);
+ }
+
if (this.selectedFields.includes(column.title)) {
this._dataViz?.setColumnType(column.title, type);
} else {
column.type = type;
}
+
this.forceUpdate();
};
@@ -469,81 +369,22 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps>
this.forceUpdate();
};
- generateGPTImage = async (prompt: string): Promise<string | undefined> => {
- try {
- const res = await gptImageCall(prompt);
-
- if (res) {
- const result = (await Networking.PostToServer('/uploadRemoteImage', { sources: res })) as Upload.FileInformation[];
- const source = ClientUtils.prepend(result[0].accessPaths.agnostic.client);
- return source;
- }
- } catch (e) {
- console.log(e);
- }
- };
-
- /**
- * Populates a preset template framework with content from a datavizbox or any AI-generated content.
- * @param template the preloaded template framework being filled in
- * @param assignments a list of template field numbers (from top to bottom) and their assigned columns from the linked dataviz
- * @returns a doc containing the fully rendered template
- */
- applyGPTContentToTemplate = async (template: Template, assignments: { [field: string]: Col }): Promise<Template | undefined> => {
- const GPTTextCalls = Object.entries(assignments).filter(([, col]) => col.type === TemplateFieldType.TEXT && this._userCreatedFields.includes(col));
- const GPTIMGCalls = Object.entries(assignments).filter(([, col]) => col.type === TemplateFieldType.VISUAL && this._userCreatedFields.includes(col));
-
- if (GPTTextCalls.length) {
- const promises = GPTTextCalls.map(([str, col]) => {
- return this.renderGPTTextCall(template, col, Number(str));
- });
-
- await Promise.all(promises);
- }
-
- if (GPTIMGCalls.length) {
- const promises = GPTIMGCalls.map(async ([fieldNum, col]) => {
- return this.renderGPTImageCall(template, col, Number(fieldNum));
- });
-
- await Promise.all(promises);
- }
-
- return template;
- };
-
- compileFieldDescriptions = (templates: Template[]): string => {
- let descriptions: string = '';
- templates.forEach(template => {
- descriptions += `---------- NEW TEMPLATE TO INCLUDE: The title is: ${template.mainField.getTitle()}. Its fields are: `;
- descriptions += template.descriptionSummary;
- });
+ compileFieldDescriptions = (templates: Template[]) =>
+ templates.map(template => `---------- NEW TEMPLATE TO INCLUDE: The title is: ${template.title}. Its fields are: ` + template.descriptionSummary).join(''); // prettier-ignore
- return descriptions;
- };
+ compileColDescriptions = (cols: Col[]) =>
+ ' ------------- COL DESCRIPTIONS START HERE:' + cols.map(col => `{title: ${col.title}, sizes: ${String(col.sizes)}, type: ${col.type}, descreiption: ${col.desc}} `).join(''); // prettier-ignore
- 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];
- };
+ getColByTitle = (title: string): Col | undefined => this.fieldsInfos.filter(col => col.title === title)[0];
@action
assignColsToFields = async (templates: Template[], cols: Col[]): Promise<[Template, { [field: number]: Col }][]> => {
- const fieldDescriptions: string = this.compileFieldDescriptions(templates);
- const colDescriptions: string = this.compileColDescriptions(cols);
+ const fieldDescriptions = this.compileFieldDescriptions(templates);
+ const colDescriptions = this.compileColDescriptions(cols);
const inputText = fieldDescriptions.concat(colDescriptions);
- ++this._callCount;
- const origCount = this._callCount;
-
- const prompt: string = `(${origCount}) ${inputText}`;
+ const prompt = `(${Math.random() * 100000}) ${inputText}`;
this._GPTLoading = true;
@@ -555,24 +396,26 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps>
const brokenDownAssignments: [Template, { [fieldID: number]: Col }][] = [];
Object.entries(assignments).forEach(([tempTitle, assignment]) => {
- const template = templates.filter(t => t.mainField.getTitle() === tempTitle)[0];
- if (!template) return;
- const toObj = Object.entries(assignment).reduce(
- (a, [fieldID, colTitle]) => {
- const col = this.getColByTitle(colTitle);
- if (!this._userCreatedFields.includes(col)) {
- // do the following for any fields not added by the user; will change in the future, for now only GPT content works with user-added fields
- const field = template.getFieldByID(Number(fieldID));
- field.setContent(col.defaultContent ?? '', col.type === TemplateFieldType.VISUAL ? FieldContentType.IMAGE : FieldContentType.STRING);
- field.setTitle(col.title);
- } else {
- a[Number(fieldID)] = this.getColByTitle(colTitle);
- }
- return a;
- },
- {} as { [field: number]: Col }
- );
- brokenDownAssignments.push([template, toObj]);
+ const template = templates.filter(temp => temp.title === tempTitle)[0];
+ if (template) {
+ const toObj = Object.entries(assignment).reduce(
+ (a, [fieldID, colTitle]) => {
+ const col = this.getColByTitle(colTitle);
+ if (col) {
+ if (!col.AIGenerated) {
+ const field = template.getFieldByID(Number(fieldID));
+ field?.setContent(col.defaultContent ?? '', col.type === TemplateFieldType.VISUAL ? ViewType.IMG : ViewType.TEXT);
+ field?.setTitle(col.title);
+ } else {
+ a[Number(fieldID)] = col;
+ }
+ }
+ return a;
+ },
+ {} as { [field: number]: Col }
+ );
+ brokenDownAssignments.push([template, toObj]);
+ }
});
return brokenDownAssignments;
@@ -584,777 +427,100 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps>
return [];
};
- generatePresetTemplates = async () => {
- this._dataViz?.updateColDefaults();
-
- const cols = this.fieldsInfos;
- const templates = this.templateManager.getValidTemplates(cols);
-
- const assignments: [Template, { [field: number]: Col }][] = await this.assignColsToFields(templates, cols);
-
- const renderedTemplatePromises: Promise<Template | undefined>[] = assignments.map(([template, asns]) => this.applyGPTContentToTemplate(template, asns));
-
- await Promise.all(renderedTemplatePromises);
-
- setTimeout(() => {
- this.setSuggestedTemplates(templates);
+ generatePresetTemplates = action(() => {
+ if (this.DEBUG_MODE) {
+ this.setSuggestedTemplates(this.templateManager._templates);
this._GPTLoading = false;
- });
- };
-
- renderGPTImageCall = async (template: Template, col: Col, fieldNumber: number): Promise<boolean> => {
- const generateAndLoadImage = async (fieldNum: string, column: Col, prompt: string) => {
- const url = await this.generateGPTImage(prompt);
- const field: Field = template.getFieldByID(Number(fieldNum));
-
- field.setContent(url ?? '', FieldContentType.IMAGE);
- field.setTitle(column.title);
- };
-
- const fieldContent: string = template.compiledContent;
-
- try {
- const sysPrompt =
- 'Your job is to create a prompt for an AI image generator to help it generate an image based on existing content in a template and a user prompt. Your prompt should focus heavily on visual elements to help the image generator; avoid unecessary info that might distract it. ONLY INCLUDE THE PROMPT, NO OTHER TEXT OR EXPLANATION. The existing content is as follows: ' +
- fieldContent +
- ' **** The user prompt is: ' +
- col.desc;
-
- const prompt = await gptAPICall(sysPrompt, GPTCallType.COMPLETEPROMPT);
-
- await generateAndLoadImage(String(fieldNumber), col, prompt);
- } catch (e) {
- console.log(e);
+ } else {
+ this._dataViz?.updateColDefaults();
+ const contentFields = this.fieldsInfos.filter(field => field.type !== TemplateFieldType.DATA);
+ const templates = this.templateManager.getValidTemplates(contentFields);
+
+ return this.assignColsToFields(templates, contentFields)
+ .then(pairs =>
+ Promise.all(pairs.map(([templ, assgns]) => TemplateMenuAIUtils.applyGPTContentToTemplate(templ, assgns))))
+ .then(action(() => {
+ this.setSuggestedTemplates(templates);
+ this._GPTLoading = false;
+ })); // prettier-ignore
}
- return true;
- };
-
- renderGPTTextCall = async (template: Template, col: Col, fieldNum: number): Promise<boolean> => {
- const wordLimit = (size: TemplateFieldSize) => {
- switch (size) {
- case TemplateFieldSize.TINY:
- return 2;
- case TemplateFieldSize.SMALL:
- return 5;
- case TemplateFieldSize.MEDIUM:
- return 20;
- case TemplateFieldSize.LARGE:
- return 50;
- case TemplateFieldSize.HUGE:
- return 100;
- default:
- return 10;
- }
- };
-
- const textAssignment = `--- title: ${col.title}, prompt: ${col.desc}, word limit: ${wordLimit(col.sizes[0])} words, assigned field: ${fieldNum} ---`;
+ });
- const fieldContent: string = template.compiledContent;
-
- try {
- const prompt = fieldContent + textAssignment;
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ generateVariations = async (onDoc: Doc, prompt: string, options: FireflyStructureOptions) => {
+ // const { numVariations, temperature, useStyleRef } = options;
+ this.variations = [];
+ const mainCollection = this._dataViz?.DocumentView?.().containerViewPath?.().lastElement()?.ComponentView as CollectionFreeFormView;
- const res = await gptAPICall(`${++this._callCount}: ${prompt}`, GPTCallType.FILL);
+ const clone = Doc.MakeClone(onDoc).clone;
+ mainCollection.addDocument(clone);
+ clone.x = 10000;
+ clone.y = 10000;
- if (res) {
- const assignments: { [title: string]: { number: string; content: string } } = JSON.parse(res);
- Object.entries(assignments).forEach(([title, info]) => {
- const field: Field = template.getFieldByID(Number(info.number));
- const column = this.getColByTitle(title);
-
- field.setContent(info.content ?? '', FieldContentType.STRING);
- field.setTitle(column.title);
- });
- }
- } catch (err) {
- console.log(err);
- }
+ // await DrawingFillHandler.drawingToImage(clone, 100 - temperature, prompt, useStyleRef ? clone : undefined, this, numVariations)
- return true;
+ return this.variations;
};
- createDocsFromTemplate = async (template: Template) => {
- const dv = this._dataViz;
-
- if (!dv) return;
-
- this._docsRendering = true;
-
- const fields: string[] = Array.from(Object.keys(dv.records[0]));
- const selectedRows = NumListCast(dv.layoutDoc.dataViz_selectedRows);
-
- const rowContents: { [title: string]: string }[] = selectedRows.map(row => {
- const values: { [title: string]: string } = {};
- fields.forEach(col => {
- values[col] = dv.records[row][col];
- });
+ variations: string[] = [];
- return values;
- });
-
- const processContent = async (content: { [title: string]: string }) => {
- const templateCopy = template.cloneBase();
-
- fields
- .filter(title => title)
- .forEach(title => {
- const field = templateCopy.getFieldByTitle(title);
- if (field === undefined) {
- return;
- }
- field.setContent(content[title]);
- });
-
- const gptPromises = this._userCreatedFields
- .filter(field => field.type === TemplateFieldType.TEXT)
- .map(field => {
- const title = field.title;
- const templateField = templateCopy.getFieldByTitle(title);
- if (templateField === undefined) {
- return;
- }
- const templatefieldID = templateField.getID;
-
- return this.renderGPTTextCall(templateCopy, field, templatefieldID);
- });
-
- const imagePromises = this._userCreatedFields
- .filter(field => field.type === TemplateFieldType.VISUAL)
- .map(field => {
- const title = field.title;
- const templateField = templateCopy.getFieldByTitle(title);
- if (templateField === undefined) {
- return;
- }
- const templatefieldID = templateField.getID;
-
- return this.renderGPTImageCall(templateCopy, field, templatefieldID);
- });
-
- await Promise.all(gptPromises);
-
- await Promise.all(imagePromises);
-
- return templateCopy.getRenderedDoc();
- };
-
- const promises = rowContents.map(content => processContent(content));
-
- const renderedDocs = await Promise.all(promises);
-
- this._docsRendering = false;
+ @action addVariation = (url: string) => {
+ this.variations.push(url);
+ };
- return renderedDocs;
+ addRenderedCollectionToMainview = (collection: Doc) => {
+ if (collection) {
+ const mainCollection = this._dataViz?.DocumentView?.().containerViewPath?.().lastElement()?.ComponentView as CollectionFreeFormView;
+ collection.x = this._pageX - this._menuDimensions.width;
+ collection.y = this._pageY - this._menuDimensions.height;
+ mainCollection?.addDocument(collection);
+ this.closeMenu();
+ }
};
- addRenderedCollectionToMainview = () => {
- const collection = this._renderedDocCollection;
- if (!collection) return;
- const mainCollection = this._dataViz?.DocumentView?.().containerViewPath?.().lastElement()?.ComponentView as CollectionFreeFormView;
- collection.x = this._pageX - this._menuDimensions.width;
- collection.y = this._pageY - this._menuDimensions.height;
- mainCollection.addDocument(collection);
- this.closeMenu();
+ @action editLastTemplate = () => {
+ if (this._editedTemplateTrail.length) this._currEditingTemplate = this._editedTemplateTrail.pop();
};
@action setExpandedView = (template: Template | undefined) => {
if (template) {
- this._currEditingTemplate = template;
- this._expandedPreview = template.mainField.renderedDoc(); //Docs.Create.FreeformDocument([doc], { _height: NumListCast(doc._height)[0], _width: NumListCast(doc._width)[0], title: ''});
+ this._menuContent = 'templateEditing';
+ this._currEditingTemplate && this._editedTemplateTrail.push(this._currEditingTemplate);
} else {
- this._currEditingTemplate = undefined;
- this._expandedPreview = undefined;
- }
- };
-
- get editingWindow() {
- const rendered = !this._expandedPreview ? null : (
- <div className="docCreatorMenu-expanded-template-preview">
- <DocumentView
- Document={this._expandedPreview}
- isContentActive={emptyFunction}
- addDocument={returnFalse}
- moveDocument={returnFalse}
- removeDocument={returnFalse}
- PanelWidth={() => this._menuDimensions.width - 10}
- PanelHeight={() => this._menuDimensions.height - 60}
- ScreenToLocalTransform={() => new Transform(-this._pageX - 5, -this._pageY - 35, 1)}
- renderDepth={5}
- whenChildContentsActiveChanged={emptyFunction}
- focus={emptyFunction}
- styleProvider={DefaultStyleProvider}
- addDocTab={DocumentViewInternal.addDocTabFunc}
- pinToPres={() => undefined}
- childFilters={returnEmptyFilter}
- childFiltersByRanges={returnEmptyFilter}
- searchFilterDocs={returnEmptyDoclist}
- fitContentsToBox={returnFalse}
- fitWidth={returnFalse}
- />
- </div>
- );
-
- return (
- <div className="docCreatorMenu-expanded-template-preview">
- <div className="top-panel" />
- {rendered}
- <div className="right-buttons-panel">
- <button
- className="docCreatorMenu-menu-button section-reveal-options top-right"
- onPointerDown={e =>
- this.setUpButtonClick(e, () => {
- this._currEditingTemplate && this.updateTemplatePreview(this._currEditingTemplate);
- this.setExpandedView(undefined);
- })
- }>
- <FontAwesomeIcon icon="minimize" />
- </button>
- <button
- className="docCreatorMenu-menu-button section-reveal-options top-right-lower"
- onPointerDown={e =>
- this.setUpButtonClick(e, () => {
- this._currEditingTemplate?.resetToBase();
- this.setExpandedView(this._currEditingTemplate);
- })
- }>
- <FontAwesomeIcon icon="arrows-rotate" color="white" />
- </button>
- </div>
- </div>
- );
- }
-
- get templatesPreviewContents() {
- const GPTOptions = <div></div>;
-
- return (
- <div className={`docCreatorMenu-templates-view`}>
- {this._expandedPreview ? (
- this.editingWindow
- ) : (
- <div>
- <div className="docCreatorMenu-section" style={{ height: this._GPTOpt ? 200 : 200 }}>
- <div className="docCreatorMenu-section-topbar">
- <div className="docCreatorMenu-section-title">Suggested Templates</div>
- <button className="docCreatorMenu-menu-button section-reveal-options" onPointerDown={e => this.setUpButtonClick(e, () => runInAction(() => (this._menuContent = 'dashboard')))}>
- <FontAwesomeIcon icon="gear" />
- </button>
- </div>
- <div className="docCreatorMenu-templates-preview-window" style={{ justifyContent: this._GPTLoading || this._menuDimensions.width > 400 ? 'center' : '' }}>
- {this._GPTLoading ? (
- <div className="loading-spinner">
- <ReactLoading type="spin" color={StrCast(Doc.UserDoc().userVariantColor)} height={30} width={30} />
- </div>
- ) : (
- this._suggestedTemplatePreviews.map(({ doc, template }) => (
- <div
- className="docCreatorMenu-preview-window"
- key="0"
- style={{
- border: this._selectedTemplate === template ? `solid 3px ${Colors.MEDIUM_BLUE}` : '',
- boxShadow: this._selectedTemplate === template ? `0 0 15px rgba(68, 118, 247, .8)` : '',
- }}
- onPointerDown={e => this.setUpButtonClick(e, () => runInAction(() => this.updateSelectedTemplate(template)))}>
- <button
- className="option-button left"
- onPointerDown={e =>
- this.setUpButtonClick(e, () => {
- this.setExpandedView(template);
- })
- }>
- <FontAwesomeIcon icon="magnifying-glass" color="white" />
- </button>
- <button className="option-button right" onPointerDown={e => this.setUpButtonClick(e, () => this.addUserTemplate(template))}>
- <FontAwesomeIcon icon="plus" color="white" />
- </button>
- <DocumentView
- Document={doc}
- isContentActive={emptyFunction} // !!! should be return false
- addDocument={returnFalse}
- moveDocument={returnFalse}
- removeDocument={returnFalse}
- PanelWidth={() => (this._selectedTemplate === template ? 104 : 111)}
- PanelHeight={() => (this._selectedTemplate === template ? 104 : 111)}
- ScreenToLocalTransform={() => new Transform(-this._pageX - 5, -this._pageY - 35, 1)}
- renderDepth={1}
- whenChildContentsActiveChanged={emptyFunction}
- focus={emptyFunction}
- styleProvider={DefaultStyleProvider}
- addDocTab={this._props.addDocTab}
- pinToPres={() => undefined}
- childFilters={returnEmptyFilter}
- childFiltersByRanges={returnEmptyFilter}
- searchFilterDocs={returnEmptyDoclist}
- fitContentsToBox={returnFalse}
- fitWidth={returnFalse}
- hideDecorations={true}
- />
- </div>
- ))
- )}
- </div>
- <div className="docCreatorMenu-GPT-options">
- <div className="docCreatorMenu-GPT-options-container">
- <button className="docCreatorMenu-menu-button" onPointerDown={e => this.setUpButtonClick(e, () => this.generatePresetTemplates())}>
- <FontAwesomeIcon icon="arrows-rotate" />
- </button>
- </div>
- {this._GPTOpt ? GPTOptions : null}
- </div>
- </div>
- <hr className="docCreatorMenu-option-divider full no-margin" />
- <div className="docCreatorMenu-section">
- <div className="docCreatorMenu-section-topbar">
- <div className="docCreatorMenu-section-title">Your Templates</div>
- <button className="docCreatorMenu-menu-button section-reveal-options" onPointerDown={e => this.setUpButtonClick(e, () => (this._GPTOpt = !this._GPTOpt))}>
- <FontAwesomeIcon icon="gear" />
- </button>
- </div>
- <div className="docCreatorMenu-templates-preview-window" style={{ justifyContent: this._menuDimensions.width > 400 ? 'center' : '' }}>
- <div className="docCreatorMenu-preview-window empty">
- <FontAwesomeIcon icon="plus" color="rgb(160, 160, 160)" />
- </div>
- {this._userTemplates.map(({ template, doc }) => (
- <div
- className="docCreatorMenu-preview-window"
- key="0"
- style={{
- border: this._selectedTemplate === template ? `solid 3px ${Colors.MEDIUM_BLUE}` : '',
- boxShadow: this._selectedTemplate === template ? `0 0 15px rgba(68, 118, 247, .8)` : '',
- }}
- onPointerDown={e => this.setUpButtonClick(e, () => runInAction(() => this.updateSelectedTemplate(template)))}>
- <button
- className="option-button left"
- onPointerDown={e =>
- this.setUpButtonClick(e, () => {
- this.setExpandedView(template);
- })
- }>
- <FontAwesomeIcon icon="magnifying-glass" color="white" />
- </button>
- <button className="option-button right" onPointerDown={e => this.setUpButtonClick(e, () => this.removeUserTemplate(template))}>
- <FontAwesomeIcon icon="minus" color="white" />
- </button>
- <DocumentView
- Document={doc}
- isContentActive={emptyFunction} // !!! should be return false
- addDocument={returnFalse}
- moveDocument={returnFalse}
- removeDocument={returnFalse}
- PanelWidth={() => (this._selectedTemplate === template ? 104 : 111)}
- PanelHeight={() => (this._selectedTemplate === template ? 104 : 111)}
- ScreenToLocalTransform={() => new Transform(-this._pageX - 5, -this._pageY - 35, 1)}
- renderDepth={1}
- whenChildContentsActiveChanged={emptyFunction}
- focus={emptyFunction}
- styleProvider={DefaultStyleProvider}
- addDocTab={this._props.addDocTab}
- pinToPres={() => undefined}
- childFilters={returnEmptyFilter}
- childFiltersByRanges={returnEmptyFilter}
- searchFilterDocs={returnEmptyDoclist}
- fitContentsToBox={returnFalse}
- fitWidth={returnFalse}
- hideDecorations={true}
- />
- </div>
- ))}
- </div>
- </div>
- </div>
- )}
- </div>
- );
- }
-
- @action updateXMargin = (input: string) => {
- this._layout.xMargin = Number(input);
- setTimeout(() => {
- if (!this._renderedDocCollection || !this._fullyRenderedDocs) return;
- this.applyLayout(this._renderedDocCollection, this._fullyRenderedDocs);
- });
- };
- @action updateYMargin = (input: string) => {
- this._layout.yMargin = Number(input);
- setTimeout(() => {
- if (!this._renderedDocCollection || !this._fullyRenderedDocs) return;
- this.applyLayout(this._renderedDocCollection, this._fullyRenderedDocs);
- });
- };
- @action updateColumns = (input: string) => {
- this._layout.columns = Number(input);
- this.updateRenderedDocCollection();
- };
-
- get layoutConfigOptions() {
- const optionInput = (icon: string, func: (input: string) => void, def?: number, key?: string, noMargin?: boolean) => {
- return (
- <div className="docCreatorMenu-option-container small no-margin" key={key} style={{ marginTop: noMargin ? '0px' : '' }}>
- <div className="docCreatorMenu-option-title config layout-config">
- <FontAwesomeIcon icon={icon as IconProp} />
- </div>
- <input defaultValue={def} onInput={e => func(e.currentTarget.value)} className="docCreatorMenu-input config layout-config" />
- </div>
- );
- };
-
- switch (this._layout.type) {
- case LayoutType.FREEFORM:
- return (
- <div className="docCreatorMenu-configuration-bar">
- {optionInput('arrows-up-down', this.updateYMargin, this._layout.xMargin, '2')}
- {optionInput('arrows-left-right', this.updateXMargin, this._layout.xMargin, '3')}
- {optionInput('table-columns', this.updateColumns, this._layout.columns, '4', true)}
- </div>
- );
- default:
- break;
+ this._menuContent = 'templates';
}
- }
- applyLayout = (collection: Doc, docs: Doc[]) => {
- const { horizontalSpan, verticalSpan } = this.previewInfo;
- collection._height = verticalSpan;
- collection._width = horizontalSpan;
-
- const layout = this._layout;
- const columns: number = layout.columns ?? this.columnsCount;
- const xGap: number = layout.xMargin;
- const yGap: number = layout.yMargin;
- // const repeat: number = templateInfo.layout.repeat;
- const startX: number = -Number(collection._width) / 2;
- const startY: number = -Number(collection._height) / 2;
- const docHeight: number = Number(docs[0]._height);
- const docWidth: number = Number(docs[0]._width);
-
- if (columns === 0 || docs.length === 0) {
- return;
- }
+ this._currEditingTemplate = template;
- let i: number = 0;
- let docsChanged: number = 0;
- let curX: number = startX;
- let curY: number = startY;
-
- while (docsChanged < docs.length) {
- while (i < columns && docsChanged < docs.length) {
- docs[docsChanged].x = curX;
- docs[docsChanged].y = curY;
- curX += docWidth + xGap;
- ++docsChanged;
- ++i;
- }
- i = 0;
- curX = startX;
- curY += docHeight + yGap;
- }
+ //Docs.Create.FreeformDocument([doc], { _height: NumListCast(doc._height)[0], _width: NumListCast(doc._width)[0], title: ''});
};
@computed
- get previewInfo() {
- const docHeight: number = Number(this._fullyRenderedDocs[0]._height);
- const docWidth: number = Number(this._fullyRenderedDocs[0]._width);
- const layout = this._layout;
- return {
- docHeight: docHeight,
- docWidth: docWidth,
- horizontalSpan: (docWidth + layout.xMargin) * this.columnsCount - layout.xMargin,
- verticalSpan: (docHeight + layout.yMargin) * this.rowsCount - layout.yMargin,
- };
- }
-
- /**
- * Updates the preview that shows how all docs will be rendered in the chosen collection type.
- @type the type of collection the docs should render to (ie. freeform, carousel, card)
- */
- updateRenderedDocCollection = () => {
- if (!this._fullyRenderedDocs) return;
-
- const { horizontalSpan, verticalSpan } = this.previewInfo;
-
- const collectionFactory = (): ((docs: Doc[], options: DocumentOptions) => Doc) => {
- switch (this._layout.type) {
- case LayoutType.CAROUSEL3D:
- return Docs.Create.Carousel3DDocument;
- case LayoutType.FREEFORM:
- return Docs.Create.FreeformDocument;
- case LayoutType.CARD:
- return Docs.Create.CardDeckDocument;
- case LayoutType.MASONRY:
- return Docs.Create.MasonryDocument;
- case LayoutType.CAROUSEL:
- return Docs.Create.CarouselDocument;
- default:
- return Docs.Create.FreeformDocument;
- }
- };
-
- const collection: Doc = collectionFactory()(this._fullyRenderedDocs, {
- isDefaultTemplateDoc: true,
- _height: verticalSpan,
- _width: horizontalSpan,
- title: 'title',
- backgroundColor: 'gray',
- });
-
- this.applyLayout(collection, this._fullyRenderedDocs);
-
- this._renderedDocCollection = collection;
- };
-
- layoutPreviewContents = () => {
- return this._docsRendering ? (
- <div className="docCreatorMenu-layout-preview-window-wrapper loading">
- <div className="loading-spinner">
- <ReactLoading type="spin" color={StrCast(Doc.UserDoc().userVariantColor)} height={30} width={30} />
- </div>
- </div>
- ) : !this._renderedDocCollection ? null : (
- <div className="docCreatorMenu-layout-preview-window-wrapper">
- <DocumentView
- Document={this._renderedDocCollection}
- isContentActive={emptyFunction}
- addDocument={returnFalse}
- moveDocument={returnFalse}
- removeDocument={returnFalse}
- PanelWidth={() => this._menuDimensions.width - 80}
- PanelHeight={() => this._menuDimensions.height - 105}
- ScreenToLocalTransform={() => new Transform(-this._pageX - 5, -this._pageY - 35, 1)}
- renderDepth={5}
- whenChildContentsActiveChanged={emptyFunction}
- focus={emptyFunction}
- styleProvider={DefaultStyleProvider}
- addDocTab={this._props.addDocTab}
- pinToPres={() => undefined}
- childFilters={returnEmptyFilter}
- childFiltersByRanges={returnEmptyFilter}
- searchFilterDocs={returnEmptyDoclist}
- fitContentsToBox={returnFalse}
- fitWidth={returnFalse}
- hideDecorations={true}
- />
- </div>
- );
- };
-
- get optionsMenuContents() {
- const layoutOption = (option: LayoutType, optStyle?: object, specialFunc?: () => void) => {
- return (
- <div
- className="docCreatorMenu-dropdown-option"
- style={optStyle}
- onPointerDown={e =>
- this.setUpButtonClick(e, () => {
- specialFunc?.();
- runInAction(() => {
- this._layout.type = option;
- this.updateRenderedDocCollection();
- });
- })
- }>
- {option}
- </div>
- );
- };
-
- const selectionBox = (width: number, height: number, icon: string, specClass?: string, options?: JSX.Element[], manual?: boolean): JSX.Element => {
- return (
- <div className="docCreatorMenu-option-container">
- <div className={`docCreatorMenu-option-title config ${specClass}`} style={{ width: width * 0.4, height: height }}>
- <FontAwesomeIcon icon={icon as IconProp} />
- </div>
- {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];
-
+ get templatesView() {
return (
- <div className="docCreatorMenu-menu-container">
- <div className="docCreatorMenu-option-container layout">
- <div className="docCreatorMenu-dropdown-hoverable">
- <div className="docCreatorMenu-option-title">{this._layout.type ? this._layout.type.toUpperCase() : 'Choose Layout'}</div>
- <div className="docCreatorMenu-dropdown-content">
- {layoutOption(LayoutType.FREEFORM, undefined, () => {
- if (!this._layout.columns) this._layout.columns = Math.ceil(Math.sqrt(this.docsToRender.length));
- })}
- {layoutOption(LayoutType.CAROUSEL)}
- {layoutOption(LayoutType.CAROUSEL3D)}
- {layoutOption(LayoutType.MASONRY)}
+ <div className="docCreatorMenu-templates-view">
+ <div className="docCreatorMenu-templates-displays">
+ <TemplatePreviewGrid title={'Suggested Templates'} menu={this} loading={this._GPTLoading} optionsButtonOpts={this.optionsButtonOpts} templates={this._suggestedTemplates} />
+ <div className="docCreatorMenu-GPT-options">
+ <div className="docCreatorMenu-GPT-options-container">
+ <DocCreatorMenuButton icon={'arrows-rotate'} styles={'border'} function={this.generatePresetTemplates} />
</div>
</div>
</div>
- {this._layout.type ? this.layoutConfigOptions : null}
- {this.layoutPreviewContents()}
- {selectionBox(
- 60,
- 20,
- 'repeat',
- undefined,
- repeatOptions.map(num => <option key={num} onPointerDown={() => (this._layout.repeat = num)}>{`${num}x`}</option>)
- )}
- <hr className="docCreatorMenu-option-divider" />
- <div className="docCreatorMenu-general-options-container">
- <button
- className="docCreatorMenu-save-layout-button"
- onPointerDown={e =>
- setupMoveUpEvents(
- this,
- e,
- returnFalse,
- emptyFunction,
- undoable(clickEv => {
- clickEv.stopPropagation();
- if (!this._selectedTemplate) return;
- const layout: DataVizTemplateLayout = {
- template: this._selectedTemplate.getRenderedDoc(),
- layout: { type: this._layout.type, xMargin: this._layout.xMargin, yMargin: this._layout.yMargin, repeat: 0 },
- columns: this.columnsCount,
- rows: this.rowsCount,
- docsNumList: this.docsToRender,
- };
- if (!this._savedLayouts.includes(layout)) {
- this._savedLayouts.push(layout);
- }
- }, 'make docs')
- )
- }>
- <FontAwesomeIcon icon="floppy-disk" />
- </button>
- <button
- className="docCreatorMenu-create-docs-button"
- style={{ backgroundColor: this.canMakeDocs ? '' : 'rgb(155, 155, 155)', border: this.canMakeDocs ? '' : 'solid 2px rgb(180, 180, 180)' }}
- onPointerDown={e =>
- setupMoveUpEvents(
- this,
- e,
- returnFalse,
- emptyFunction,
- undoable(clickEv => {
- clickEv.stopPropagation();
- if (!this._selectedTemplate) return;
- this.addRenderedCollectionToMainview();
- }, 'make docs')
- )
- }>
- <FontAwesomeIcon icon="plus" />
- </button>
- </div>
</div>
);
}
- get dashboardContents() {
- const sizes: string[] = ['tiny', 'small', 'medium', 'large', 'huge'];
-
- const fieldPanel = (field: Col, id: number) => {
- return (
- <div className="field-panel" key={id}>
- <div className="top-bar">
- <span className="field-title">{`${field.title} Field`}</span>
- <button className="docCreatorMenu-menu-button section-reveal-options no-margin" onPointerDown={e => this.setUpButtonClick(e, () => this.removeField(field))} style={{ position: 'absolute', right: '0px' }}>
- <FontAwesomeIcon icon="minus" />
- </button>
- </div>
- <div className="opts-bar">
- <div className="opt-box">
- <div className="top-bar"> Title </div>
- <textarea className="content" style={{ width: '100%', height: 'calc(100% - 20px)' }} value={field.title} placeholder={'Enter title'} onChange={e => this.setColTitle(field, e.target.value)} />
- </div>
- <div className="opt-box">
- <div className="top-bar"> Type </div>
- <div className="content">
- <span className="type-display">{field.type === TemplateFieldType.TEXT ? 'Text Field' : field.type === TemplateFieldType.VISUAL ? 'File Field' : ''}</span>
- <div className="bubbles">
- <input
- className="bubble"
- type="radio"
- name="type"
- onClick={() => {
- this.setColType(field, TemplateFieldType.TEXT);
- }}
- />
- <div className="text">Text</div>
- <input
- className="bubble"
- type="radio"
- name="type"
- onClick={() => {
- this.setColType(field, TemplateFieldType.VISUAL);
- }}
- />
- <div className="text">File</div>
- </div>
- </div>
- </div>
- </div>
- <div className="sizes-box">
- <div className="top-bar"> Valid Sizes </div>
- <div className="content">
- <div className="bubbles">
- {sizes.map(size => (
- <>
- <input
- className="bubble"
- type="checkbox"
- name="type"
- checked={field.sizes.includes(size as TemplateFieldSize)}
- onChange={e => {
- this.modifyColSizes(field, size as TemplateFieldSize, e.target.checked);
- }}
- />
- <div className="text">{size}</div>
- </>
- ))}
- </div>
- </div>
- </div>
- <div className="desc-box">
- <div className="top-bar"> Prompt </div>
- <textarea
- className="content"
- onChange={e => this.setColDesc(field, e.target.value)}
- defaultValue={field.desc === this._dataViz?.GPTSummary?.get(field.title)?.desc ? '' : field.desc}
- placeholder={this._dataViz?.GPTSummary?.get(field.title)?.desc ?? 'Add a description/prompt to help with template generation.'}
- />
- </div>
- </div>
- );
- };
-
- return (
- <div className="docCreatorMenu-dashboard-view">
- <div className="topbar">
- <button className="docCreatorMenu-menu-button section-reveal-options" onPointerDown={e => this.setUpButtonClick(e, this.addField)}>
- <FontAwesomeIcon icon="plus" />
- </button>
- <button className="docCreatorMenu-menu-button section-reveal-options float-right" onPointerDown={e => this.setUpButtonClick(e, () => runInAction(() => (this._menuContent = 'templates')))}>
- <FontAwesomeIcon icon="arrow-left" />
- </button>
- </div>
- <div className="panels-container">{this.fieldsInfos.map((field, i) => fieldPanel(field, i))}</div>
- </div>
- );
- }
+ private optionsButtonOpts: [IconProp, () => void] = ['gear', () => (this._menuContent = 'dashboard')];
get renderSelectedViewType() {
switch (this._menuContent) {
- case 'templates':
- return this.templatesPreviewContents;
- case 'options':
- return this.optionsMenuContents;
- case 'dashboard':
- return this.dashboardContents;
- default:
- return undefined;
- }
+ case 'templates': return this.templatesView;
+ case 'templateEditing': return <TemplateEditingWindow template={this._currEditingTemplate as Template} menu={this} />;
+ case 'renderPreview': return <TemplatesRenderPreviewWindow menu={this}/>;
+ case 'dashboard': return <TemplateMenuFieldOptions menu={this} templateManager={this.templateManager}/>;
+ } // prettier-ignore
+ return undefined;
}
get resizePanes() {
@@ -1374,42 +540,26 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps>
]; //prettier-ignore
}
+ setRef = (r: HTMLDivElement) => (this._ref = r);
render() {
- const topButton = (icon: string, opt: string, func: () => void, tag: string) => {
- return (
- <div className={`top-button-container ${tag} ${opt === this._menuContent ? 'selected' : ''}`}>
- <div
- className="top-button-content"
- onPointerDown={e =>
- this.setUpButtonClick(e, () =>
- runInAction(() => {
- func();
- })
- )
- }>
- <FontAwesomeIcon icon={icon as IconProp} />
- </div>
+ const topButton = (icon: string, opt: string, func: () => void, tag: string) => (
+ <div className={`top-button-container ${tag} ${opt === this._menuContent ? 'selected' : ''}`}>
+ <div className="top-button-content" onPointerDown={e => this.setUpButtonClick(e, action(func))}>
+ <FontAwesomeIcon icon={icon as IconProp} />
</div>
- );
- };
+ </div>
+ );
- const onPreviewSelected = () => {
- this._menuContent = 'templates';
- };
- const onSavedSelected = () => {
- this._menuContent = 'dashboard';
- };
- const onOptionsSelected = () => {
- this._menuContent = 'options';
- if (!this._layout.columns) this._layout.columns = Math.ceil(Math.sqrt(this.docsToRender.length));
- };
+ const onPreviewSelected = () => (this._menuContent = 'templates');
+ const onSavedSelected = () => (this._menuContent = 'dashboard');
+ const onOptionsSelected = () => (this._menuContent = 'renderPreview');
return (
<div className="docCreatorMenu">
{!this._shouldDisplay ? undefined : (
<div
className="docCreatorMenu-cont"
- ref={r => (this._ref = r)}
+ ref={this.setRef}
style={{
display: '',
left: this._pageX,
@@ -1435,9 +585,7 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps>
return true;
},
emptyFunction,
- undoable(clickEv => {
- clickEv.stopPropagation();
- }, 'drag menu')
+ undoable(clickEv => clickEv.stopPropagation(), 'drag menu')
)
}>
<div className="docCreatorMenu-top-buttons-container">
@@ -1445,9 +593,7 @@ export class DocCreatorMenu extends ObservableReactComponent<DocCreateMenuProps>
{topButton('magnifying-glass', 'options', onOptionsSelected, 'middle')}
{topButton('bars', 'saved', onSavedSelected, 'right')}
</div>
- <button className="docCreatorMenu-menu-button close-menu" onPointerDown={e => this.setUpButtonClick(e, this.closeMenu)}>
- <FontAwesomeIcon icon={'minus'} />
- </button>
+ <DocCreatorMenuButton icon={'minus'} styles={'float-right'} function={this.closeMenu} />
</div>
{this.renderSelectedViewType}
</div>
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/DynamicField.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/DynamicField.tsx
deleted file mode 100644
index c5254c17d..000000000
--- a/src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/DynamicField.tsx
+++ /dev/null
@@ -1,117 +0,0 @@
-import { Doc } from "../../../../../../fields/Doc";
-import { Docs } from "../../../../../documents/Documents";
-import { Field, FieldDimensions, FieldSettings, ViewType } from "./Field";
-import { FieldUtils } from "./FieldUtils";
-import { StaticField } from "./StaticField";
-
-export class DynamicField implements Field {
- private subfields: Field[] = [];
-
- private id: number;
- private settings: FieldSettings;
- private title: string = '';
-
- private parent: Field;
- private dimensions: FieldDimensions;
-
- constructor(settings: FieldSettings, id: number, parent?: Field) {
- this.id = id;
- this.settings = settings;
- if (settings.title) { this.title = settings.title };
- if (!parent) {
- this.parent = this;
- this.dimensions = {width: this.settings.br[0] - this.settings.tl[0], height: this.settings.br[1] - this.settings.tl[1], coord: {x: this.settings.tl[0], y: this.settings.tl[1]}};
- } else {
- this.parent = parent;
- this.dimensions = FieldUtils.getLocalDimensions({tl: settings.tl, br: settings.br}, this.parent.getDimensions);
- }
- this.subfields = this.setupSubfields();
- }
-
- setContent = () => { return };
- getContent = () => { return '' };
-
- setTitle = (title: string) => { this.title = title };
- getTitle = () => { return this.title };
-
- get getSubfields() { return this.subfields };
- get getAllSubfields() {
- let fields: Field[] = [];
- this.subfields?.forEach(field => {
- fields.push(field);
- fields = fields.concat(field.getAllSubfields)
- });
- return fields;
- };
-
- get getDimensions() { return this.dimensions };
- get getID() { return this.id };
- get getViewType() { return this.settings.viewType };
-
- get getDescription(): string {
- return this.settings.description ?? '';
- }
-
- matches = (): Array<number> => {
- return [];
- }
-
- updateRenderedDoc = () => {
- return new Doc();
- }
-
- setupSubfields = (): Field[] => {
- const fields: Field[] = [];
- this.settings.subfields?.forEach((fieldSettings, index) => {
- let field: Field;
- const type = fieldSettings.viewType;
-
- const id = Number(String(this.id) + String(index));
-
- if (type == ViewType.CAROUSEL3D || type === ViewType.FREEFORM) {
- field = new DynamicField(fieldSettings, id, this);
- } else {
- field = new StaticField(fieldSettings, this, id);
- }
- fields.push(field);
- });
- return fields;
- }
-
- applyAttributes = (field: Field) => {
- field.setTitle(this.title);
- field.updateRenderedDoc(this.renderedDoc());
- }
-
- getChildDimensions = (coords: { tl: [number, number]; br: [number, number] }): FieldDimensions => {
- const l = (coords.tl[0] * this.dimensions.height) / 2;
- const t = coords.tl[1] * this.dimensions.width / 2; //prettier-ignore
- const r = (coords.br[0] * this.dimensions.height) / 2;
- const b = coords.br[1] * this.dimensions.width / 2; //prettier-ignore
- const width = r - l;
- const height = b - t;
- const coord = { x: l, y: t };
- return { width, height, coord };
- };
-
- renderedDoc = (): Doc => {
- let doc: Doc;
- switch (this.settings.viewType) {
- case ViewType.CAROUSEL3D:
- doc = Docs.Create.Carousel3DDocument(this.subfields.map(field => field.renderedDoc()), {
- title: this.title,
- });
- FieldUtils.applyBasicOpts(doc, this.dimensions, this.settings);
- return doc;
- case ViewType.FREEFORM:
- doc = Docs.Create.FreeformDocument(this.subfields.map(field => field.renderedDoc()), {
- title: this.title,
- });
- FieldUtils.applyBasicOpts(doc, this.dimensions, this.settings);
- return doc;
- default:
- return new Doc();
- }
- }
-
-}
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/Field.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/Field.tsx
deleted file mode 100644
index ea9b566b3..000000000
--- a/src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/Field.tsx
+++ /dev/null
@@ -1,66 +0,0 @@
-import { Doc } from "../../../../../../fields/Doc";
-import { Col } from "../DocCreatorMenu";
-import { TemplateFieldSize, TemplateFieldType } from "../TemplateBackend";
-
-export enum FieldContentType {
- STRING = 'string',
- IMAGE = 'image',
-}
-
-export enum ViewType {
- CAROUSEL3D = 'carousel3d',
- FREEFORM = 'freeform',
- STATIC = 'static',
- DEC = 'decoration'
-}
-
-export type FieldDimensions = {
- width: number;
- height: number;
- coord: {x: number, y: number};
-}
-
-export interface FieldOpts {
- backgroundColor?: string;
- color?: string;
- cornerRounding?: number;
- borderWidth?: string;
- borderColor?: string;
- contentXCentering?: 'h-left' | 'h-center' | 'h-right';
- contentYCentering?: 'top' | 'center' | 'bottom';
- opacity?: number;
- rotation?: number;
- fontBold?: boolean;
- fontTransform?: 'uppercase' | 'lowercase';
- fieldViewType?: 'freeform' | 'stacked';
-}
-
-export type FieldSettings = {
- tl: [number, number];
- br: [number, number];
- opts: FieldOpts;
- viewType: ViewType;
- title?: string;
- subfields?: FieldSettings[];
- types?: TemplateFieldType[];
- sizes?: TemplateFieldSize[];
- description?: string;
-};
-
-export interface Field {
- getContent: () => string;
- setContent: (content: string, type?: FieldContentType) => void;
- getDimensions: FieldDimensions;
- getSubfields: Field[];
- getAllSubfields: Field[];
- getID: number;
- getViewType: ViewType;
- getDescription: string;
- getTitle: () => string;
- setTitle: (title: string) => void;
- setupSubfields: () => Field[];
- applyAttributes: (field: Field) => void;
- renderedDoc: () => Doc;
- matches: (cols: Col[]) => number[];
- updateRenderedDoc: (oldDoc?: Doc) => Doc;
-} \ No newline at end of file
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/FieldUtils.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/FieldUtils.tsx
deleted file mode 100644
index 3886774d2..000000000
--- a/src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/FieldUtils.tsx
+++ /dev/null
@@ -1,79 +0,0 @@
-import { Doc } from "../../../../../../fields/Doc";
-import { ComputedField, ScriptField } from "../../../../../../fields/ScriptField";
-import { Col } from "../DocCreatorMenu";
-import { TemplateFieldSize, TemplateFieldType, TemplateLayouts } from "../TemplateBackend";
-import { FieldDimensions, FieldSettings } from "./Field";
-
-export class FieldUtils {
- public static getLocalDimensions = (coords: { tl: [number, number]; br: [number, number] }, parentDimensions: FieldDimensions): FieldDimensions => {
- const l = (coords.tl[0] * parentDimensions.width) / 2;
- const t = coords.tl[1] * parentDimensions.height / 2; //prettier-ignore
- const r = (coords.br[0] * parentDimensions.width) / 2;
- const b = coords.br[1] * parentDimensions.height / 2; //prettier-ignore
- const width = r - l;
- const height = b - t;
- const coord = { x: l, y: t };
- return { width, height, coord };
- };
-
- public static applyBasicOpts = (doc: Doc, parentDimensions: FieldDimensions, settings: FieldSettings, oldDoc?: Doc) => {
- const opts = settings.opts;
- doc.isDefaultTemplateDoc = oldDoc ? oldDoc.isDefaultTemplateDoc : true;
- doc._layout_hideScroll = oldDoc ? oldDoc._layout_hideScroll : true;
- doc.x = oldDoc ? oldDoc.x : parentDimensions.coord.x;
- doc.y = oldDoc ? oldDoc.y : parentDimensions.coord.y;
- doc._height = oldDoc ? oldDoc.height : parentDimensions.height;
- doc._width = oldDoc ? oldDoc.width : parentDimensions.width;
- doc.backgroundColor = oldDoc ? oldDoc.backgroundColor : opts.backgroundColor ?? '';
- doc._layout_borderRounding = !opts.cornerRounding ? '0px' : ScriptField.MakeFunction(`${opts.cornerRounding} * this.width + 'px'`);
- doc.borderColor = oldDoc ? oldDoc.borderColor : opts.borderColor;
- doc.borderWidth = oldDoc ? oldDoc.borderWidth : opts.borderWidth;
- doc.opacity = oldDoc ? oldDoc.opacity : opts.opacity;
- doc._rotation = oldDoc ? oldDoc._rotation : opts.rotation;
- doc.hCentering = oldDoc ? oldDoc.hCentering : opts.contentXCentering;
- doc.nativeWidth = parentDimensions.width;
- doc.nativeHeight = parentDimensions.height;
- doc._layout_nativeDimEditable = true;
- };
-
- public static calculateFontSize = (contWidth: number, contHeight: number, text: string, uppercase: boolean): number => {
- const words: string[] = text.split(/\s+/).filter(Boolean);
-
- let currFontSize = 1;
- let rowsCount = 1;
- let currTextHeight = currFontSize * rowsCount * 2;
-
- while (currTextHeight <= contHeight) {
- let wordIndex = 0;
- let currentRowWidth = 0;
- let wordsInCurrRow = 0;
- rowsCount = 1;
-
- while (wordIndex < words.length) {
- const word = words[wordIndex];
- const wordWidth = word.length * currFontSize * 0.7;
-
- if (currentRowWidth + wordWidth <= contWidth) {
- currentRowWidth += wordWidth;
- ++wordsInCurrRow;
- } else {
- if (words.length !== 1 && words.length > wordsInCurrRow) {
- rowsCount++;
- currentRowWidth = wordWidth;
- wordsInCurrRow = 1;
- } else {
- break;
- }
- }
-
- wordIndex++;
- }
-
- currTextHeight = rowsCount * currFontSize * 2;
-
- currFontSize += 1;
- }
-
- return currFontSize - 1;
- };
-} \ No newline at end of file
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/StaticField.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/StaticField.tsx
deleted file mode 100644
index 47b43f051..000000000
--- a/src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/StaticField.tsx
+++ /dev/null
@@ -1,147 +0,0 @@
-import { Doc } from "../../../../../../fields/Doc";
-import { Docs } from "../../../../../documents/Documents";
-import { Col } from "../DocCreatorMenu";
-import { DynamicField } from "./DynamicField";
-import { FieldUtils } from "./FieldUtils";
-import { Field, FieldContentType, FieldDimensions, FieldSettings, ViewType } from "./Field";
-
-export class StaticField {
- private content: string;
- private contentType: FieldContentType | undefined;
- private subfields: Field[] = [];
- private renderedDocument: Doc;
-
- private id: number;
- private title: string = '';
-
- private settings: FieldSettings;
-
- private parent: Field;
- private dimensions: FieldDimensions;
-
- constructor(settings: FieldSettings, parent: Field, id: number) {
- this.settings = settings;
- if (settings.title) { this.title = settings.title };
- this.id = id;
- this.parent = parent;
- this.dimensions = FieldUtils.getLocalDimensions({tl: settings.tl, br: settings.br}, this.parent.getDimensions);
- this.content = '';
- this.subfields = this.setupSubfields();
- this.renderedDocument = this.updateRenderedDoc();
- };
-
- get getSubfields(): Field[] { return this.subfields ?? []; };
-
- get getAllSubfields(): Field[] {
- let fields: Field[] = [];
- this.subfields?.forEach(field => {
- fields.push(field);
- fields = fields.concat(field.getAllSubfields);
- });
- return fields;
- };
-
- get getDimensions() { return this.dimensions };
- get getID() { return this.id };
- get getViewType() { return this.settings.viewType };
-
- get getDescription(): string {
- return this.settings.description ?? '';
- }
-
- renderedDoc = () => {
- return this.renderedDocument;
- }
-
- setContent = (newContent: string, type?: FieldContentType) => {
- this.content = newContent;
- if (type) this.contentType = type;
- this.updateRenderedDoc(this.renderedDocument);
- };
- getContent() { return this.content };
-
- setTitle = (title: string) => {
- this.title = title;
- this.renderedDocument.title = title;
- this.updateRenderedDoc(this.renderedDocument);
- };
- getTitle = () => { return this.title };
-
- applyAttributes = (field: Field) => { //!!! can be updated later for more robust clonign; this is all ythat's needed now
- field.setTitle(this.title);
- field.setContent('', this.contentType);
- field.updateRenderedDoc(this.renderedDoc());
- }
-
- setupSubfields = (): Field[] => {
- const fields: Field[] = [];
- this.settings.subfields?.forEach((fieldSettings, index) => {
- let field: Field;
- const type = fieldSettings.viewType;
-
- const id = Number(String(this.id) + String(index));
-
- if (type === ViewType.FREEFORM || type === ViewType.CAROUSEL3D) {
- field = new DynamicField(fieldSettings, id, this);
- } else {
- field = new StaticField(fieldSettings, this, id);
- };
-
- fields.push(field);
- });
- return fields;
- };
-
- matches = (cols: Col[]): number[] => {
- const colMatchesField = (col: Col) => {
- const isMatch: boolean = (
- this.settings.sizes?.some(size => col.sizes?.includes(size))
- && this.settings.types?.includes(col.type))
- ?? false;
- return isMatch;
- }
-
- const matches: Array<number> = [];
-
- cols.forEach((col, v) => {
- if (colMatchesField(col)) {
- matches.push(v);
- }
- });
-
- return matches;
- };
-
- updateRenderedDoc = (oldDoc?: Doc): Doc => {
- const opts = this.settings.opts;
-
- if (!this.contentType) { this.contentType = FieldContentType.STRING };
-
- let doc: Doc;
-
- switch (this.contentType) {
- case FieldContentType.STRING:
- doc = Docs.Create.TextDocument(String(this.content), {
- title: this.title,
- text_fontColor: oldDoc ? String(oldDoc.color) : opts.color,
- contentBold: oldDoc ? Boolean(oldDoc.fontBold) : opts.fontBold,
- textTransform: oldDoc ? String(oldDoc.fontTransform) : opts.fontTransform,
- color: oldDoc ? String(oldDoc.color) : opts.color,
- _text_fontSize: `${FieldUtils.calculateFontSize(this.dimensions.width, this.dimensions.height, String(this.content), true)}`
- });
- FieldUtils.applyBasicOpts(doc, this.dimensions, this.settings, oldDoc);
- break;
- case FieldContentType.IMAGE:
- doc = Docs.Create.ImageDocument(String(this.content), {
- title: this.title,
- _layout_fitWidth: false,
- });
- FieldUtils.applyBasicOpts(doc, this.dimensions, this.settings, oldDoc);
- break;
- }
-
- this.renderedDocument = doc;
-
- return doc;
- };
-} \ No newline at end of file
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/ConditionalsTextarea.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/ConditionalsTextarea.tsx
new file mode 100644
index 000000000..89c2e44ff
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/ConditionalsTextarea.tsx
@@ -0,0 +1,65 @@
+import { observer } from "mobx-react";
+import { ObservableReactComponent } from "../../../../ObservableReactComponent";
+import { Conditional } from "../Backend/TemplateManager";
+import { action, makeObservable, observable, runInAction } from "mobx";
+import React from "react";
+
+interface ConditionalsTextAreaProps {
+ conditional: Conditional;
+ property: keyof Conditional;
+}
+
+@observer
+export class ConditionalsTextArea extends ObservableReactComponent<ConditionalsTextAreaProps> {
+
+ private mirrorRef: HTMLSpanElement | null = null;
+
+ @observable private inputWidth: string = '60px';
+
+ constructor(props: ConditionalsTextAreaProps) {
+ super(props);
+ makeObservable(this);
+ }
+
+ setMirrorRef: React.LegacyRef<HTMLSpanElement> = (node) => { this.mirrorRef = node }
+
+ @action updateInputWidth() {
+ const mirror = this.mirrorRef;
+ if (mirror) {
+ const width = mirror.offsetWidth;
+ if ( width + 8 > 60) this.inputWidth = `${width + 8}px`;
+ }
+ }
+
+ render() {
+ return (
+ <div style={{ display: 'inline-block', position: 'relative' }}>
+ <span
+ ref={this.setMirrorRef}
+ style={{
+ position: 'absolute',
+ visibility: 'hidden',
+ whiteSpace: 'pre',
+ font: 'inherit',
+ padding: 0,
+ }}
+ >
+ {this._props.conditional[this._props.property] || ' '}
+ </span>
+ <input
+ className="form-row-input"
+ value={this.props.conditional[this.props.property] ?? ''}
+ onChange={e => {
+ runInAction(() => {
+ this.props.conditional[this.props.property] = e.target.value as "=" | ">" | "<" | "contains";
+ });
+ this.updateInputWidth();
+ }}
+ style={{ width: this.inputWidth }}
+ placeholder={this.props.property}
+ />
+ </div>
+ );
+ }
+
+} \ No newline at end of file
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/DocCreatorMenuButton.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/DocCreatorMenuButton.tsx
new file mode 100644
index 000000000..48d2de4de
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/DocCreatorMenuButton.tsx
@@ -0,0 +1,42 @@
+import { IconProp } from "@fortawesome/fontawesome-svg-core";
+import { ObservableReactComponent } from "../../../../ObservableReactComponent";
+import React from "react";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { setupMoveUpEvents, returnFalse } from "../../../../../../ClientUtils";
+import { emptyFunction } from "../../../../../../Utils";
+import { undoable } from "../../../../../util/UndoManager";
+import { observer } from "mobx-react";
+
+interface DocCreatorMenuButtonProps {
+ icon: IconProp;
+ // eslint-disable-next-line
+ function: () => any;
+ styles?: string;
+}
+
+@observer
+export class DocCreatorMenuButton extends ObservableReactComponent<DocCreatorMenuButtonProps> {
+ // eslint-disable-next-line
+ setupButtonClick = (e: React.PointerEvent, func: (...args: any) => void) => {
+ setupMoveUpEvents(
+ this,
+ e,
+ returnFalse,
+ emptyFunction,
+ undoable(clickEv => {
+ clickEv.stopPropagation();
+ clickEv.preventDefault();
+ func();
+ }, 'create docs')
+ );
+ };
+
+ render() {
+
+ return (
+ <button className={`docCreatorMenu-menu-button ${this._props.styles}`} onPointerDown={e => this.setupButtonClick(e, async () => this._props.function())}>
+ <FontAwesomeIcon icon={this._props.icon} />
+ </button>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplateEditingWindow.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplateEditingWindow.tsx
new file mode 100644
index 000000000..c35099e82
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplateEditingWindow.tsx
@@ -0,0 +1,220 @@
+import { action, makeObservable, observable, reaction } from 'mobx';
+import React from 'react';
+import { returnFalse, returnEmptyFilter } from '../../../../../../ClientUtils';
+import { emptyFunction } from '../../../../../../Utils';
+import { returnEmptyDoclist } from '../../../../../../fields/Doc';
+import { DefaultStyleProvider } from '../../../../StyleProvider';
+import { DocumentView, DocumentViewInternal } from '../../../DocumentView';
+import { DocCreatorMenu } from '../DocCreatorMenu';
+import { TemplatePreviewGrid } from './TemplatePreviewGrid';
+import { observer } from 'mobx-react';
+import { Transform } from '../../../../../util/Transform';
+import { Template } from '../Template';
+import { ObservableReactComponent } from '../../../../ObservableReactComponent';
+import { IDisposer } from 'mobx-utils';
+import { DocCreatorMenuButton } from './DocCreatorMenuButton';
+import { IconProp } from '@fortawesome/fontawesome-svg-core';
+
+export type FireflyStructureOptions = {
+ numVariations: number;
+ temperature: number;
+ useStyleRef: boolean;
+};
+
+interface FireflyVariationsTabProps {
+ menu: DocCreatorMenu;
+ template: Template;
+}
+
+@observer
+export class FireflyVariationsTab extends ObservableReactComponent<FireflyVariationsTabProps> {
+ private _prompt: string = 'Use this template to generate an empty baseball card template.';
+ private _optionsButtonOpts: [IconProp, () => void] = ['gear', emptyFunction];
+ private _previewBoxRightButtonOpts: [IconProp, () => void] = ['gear', () => this.forceUpdate()];
+
+ @observable _fireflyOptions: FireflyStructureOptions = { numVariations: 3, temperature: 0, useStyleRef: false };
+ @observable _promptInput: HTMLTextAreaElement | null = null;
+ @observable _loading: boolean = false;
+ @observable _variationsTabOpen: boolean = false;
+ @observable _variationURLs: string[] = [];
+
+ constructor(props: FireflyVariationsTabProps) {
+ super(props);
+ makeObservable(this);
+ }
+
+ generateVariations = action(async () => {
+ this._props.menu._variations = [];
+ this._loading = true;
+ const cloneTemplate = this._props.template.clone(false);
+ cloneTemplate.setMatteBackground();
+ const doc = cloneTemplate.getRenderedDoc()!;
+ this._props.menu.generateVariations(doc, this._prompt, this._fireflyOptions).then(
+ action((urls: string[]) => {
+ (this._variationURLs = urls).forEach(url => {
+ const template = this._props.template.clone(true);
+ template.setImageAsBackground(url, true);
+ this._props.menu._variations.push(template);
+ });
+ this._loading = false;
+ })
+ );
+ });
+
+ render() {
+ return (
+ <div className="docCreatorMenu-editing-firefly-section">
+ <div className="docCreatorMenu-option-divider full no-margin-bottom" />
+ <TemplatePreviewGrid
+ menu={this._props.menu}
+ title="Generate Variations"
+ loading={this._loading}
+ styles="scrolling"
+ templates={this._props.menu._variations}
+ optionsButtonOpts={this._optionsButtonOpts}
+ previewBoxRightButtonOpts={this._previewBoxRightButtonOpts}
+ />
+ <div className="docCreatorMenu-firefly-options">
+ <div className="docCreatorMenu-variation-prompt-row">
+ <textarea
+ className="docCreatorMenu-variation-prompt-input-textbox"
+ ref={action((node: HTMLTextAreaElement | null) => (this._promptInput = node))}
+ onChange={e => (this._prompt = e.target.value)}
+ onInput={() => {
+ if (this._promptInput !== null) {
+ this._promptInput.style.height = 'auto';
+ this._promptInput.style.height = this._promptInput.scrollHeight + 'px';
+ }
+ }}
+ defaultValue=""
+ placeholder="Enter a custom prompt here (optional)"
+ />
+ <DocCreatorMenuButton icon={'arrows-rotate'} styles={'border'} function={this.generateVariations} />
+ </div>
+ <nav className="options‑menu">
+ <label className="menu‑item switch">
+ <input type="checkbox" checked={this._fireflyOptions.useStyleRef} onChange={action(e => (this._fireflyOptions.useStyleRef = e.target.checked))} />
+ <span className="slider round"></span>
+ <span className="firefly-option-label">Use template as style guide</span>
+ </label>
+ <div className="menu‑item">
+ <span className="firefly-option-label">Variations</span>
+ <input type="range" id="variations" min="1" max="5" value={this._fireflyOptions.numVariations} onChange={action(e => (this._fireflyOptions.numVariations = Number(e.target.value)))} />
+ <span className="value" id="varVal">
+ {this._fireflyOptions.numVariations}
+ </span>
+ </div>
+ <div className="menu‑item">
+ <span className="firefly-option-label">Temperature</span>
+ <input type="range" id="temperature" min="1" max="100" value={this._fireflyOptions.temperature} onChange={action(e => (this._fireflyOptions.temperature = Number(e.target.value)))} />
+ <span className="value" id="tempVal">
+ {this._fireflyOptions.temperature}
+ </span>
+ </div>
+ </nav>
+ </div>
+ </div>
+ );
+ }
+}
+
+interface TemplateEditingWindowProps {
+ menu: DocCreatorMenu;
+ template: Template;
+}
+
+@observer
+export class TemplateEditingWindow extends ObservableReactComponent<TemplateEditingWindowProps> {
+ private disposers: { [name: string]: IDisposer } = {};
+
+ @observable private _previewWindow: HTMLDivElement | null = null;
+ @observable _variationsTabOpen: boolean = false;
+
+ constructor(props: TemplateEditingWindowProps) {
+ super(props);
+ makeObservable(this);
+ }
+
+ componentDidMount(): void {
+ this.disposers.windowDimensions = reaction(
+ () => this._props.menu._resizing,
+ () => this.forceUpdate(),
+ { fireImmediately: true }
+ );
+ }
+
+ componentWillUnmount() {
+ Object.values(this.disposers).forEach(disposer => disposer?.());
+ }
+
+ @action setVariationTab = (open: boolean) => {
+ this._variationsTabOpen = open;
+ if (this._previewWindow && open) {
+ this._previewWindow.style.height = String(Number(this._previewWindow.clientHeight) * 0.6);
+ } else if (this._previewWindow && !open) {
+ this._previewWindow.style.height = String((Number(this._previewWindow.clientHeight) * 5) / 3);
+ }
+ };
+
+ previewPanelWidth = () => this._previewWindow?.clientWidth ?? 500;
+ previewPanelHeight = () => this._previewWindow?.clientHeight ?? 500;
+ previewScreenToLocalXf = () => new Transform(-this._props.menu._pageX - 5, -this._props.menu._pageY - 35, 1);
+ get renderedDocPreview() {
+ const doc = this._props.template.getRenderedDoc();
+ return (
+ <div className="docCreatorMenu-expanded-template-preview" ref={action((node: HTMLDivElement | null) => (this._previewWindow = node))}>
+ {this._previewWindow && doc ? (
+ <DocumentView
+ Document={doc}
+ isContentActive={emptyFunction}
+ addDocument={returnFalse}
+ moveDocument={returnFalse}
+ removeDocument={returnFalse}
+ PanelWidth={this.previewPanelWidth}
+ PanelHeight={this.previewPanelHeight}
+ ScreenToLocalTransform={this.previewScreenToLocalXf}
+ renderDepth={5}
+ whenChildContentsActiveChanged={emptyFunction}
+ focus={emptyFunction}
+ styleProvider={DefaultStyleProvider}
+ addDocTab={DocumentViewInternal.addDocTabFunc}
+ pinToPres={emptyFunction}
+ childFilters={returnEmptyFilter}
+ childFiltersByRanges={returnEmptyFilter}
+ searchFilterDocs={returnEmptyDoclist}
+ // fitContentsToBox={returnFalse}
+ // fitWidth={returnFalse}
+ />
+ ) : null}
+ </div>
+ );
+ }
+
+ expandFunc = () => {
+ // if (this._props.template === this._props.menu._selectedTemplate) {
+ // this._props.menu.updateRenderedPreviewCollection(this._props.template);
+ // }
+ this._props.menu.setExpandedView(undefined);
+ };
+ lastFunc = () => {
+ this._props.menu.editLastTemplate();
+ this.forceUpdate();
+ };
+ variationFunc = () => this.setVariationTab(!this._variationsTabOpen);
+ render() {
+ return (
+ <div className="docCreatorMenu-templates-view">
+ <div className="docCreatorMenu-expanded-template-preview">
+ <div className="top-panel" />
+ {this.renderedDocPreview}
+ {this._variationsTabOpen ? <FireflyVariationsTab menu={this._props.menu} template={this._props.template} /> : null}
+ <div className="right-buttons-panel">
+ <DocCreatorMenuButton icon="minimize" function={this.expandFunc} />
+ <DocCreatorMenuButton icon="lightbulb" function={this.variationFunc} />
+ <DocCreatorMenuButton icon="arrow-rotate-backward" function={this.lastFunc} />
+ </div>
+ </div>
+ </div>
+ );
+ }
+}
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplateMenuFieldOptions.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplateMenuFieldOptions.tsx
new file mode 100644
index 000000000..f0e20837c
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplateMenuFieldOptions.tsx
@@ -0,0 +1,185 @@
+import { action, makeObservable, observable } from 'mobx';
+import { observer } from 'mobx-react';
+import { ObservableReactComponent } from '../../../../ObservableReactComponent';
+import { Col, DocCreatorMenu } from '../DocCreatorMenu';
+import React from 'react';
+import { Conditional, TemplateManager } from '../Backend/TemplateManager';
+import { TemplateFieldType, TemplateFieldSize } from '../TemplateBackend';
+import { DocCreatorMenuButton } from './DocCreatorMenuButton';
+
+interface TemplateMenuFieldOptionsProps {
+ menu: DocCreatorMenu;
+ templateManager: TemplateManager;
+}
+
+@observer
+export class TemplateMenuFieldOptions extends ObservableReactComponent<TemplateMenuFieldOptionsProps> {
+ @observable _collapsedCols: string[] = []; //any columns whose options panels are hidden
+
+ constructor(props: TemplateMenuFieldOptionsProps) {
+ super(props);
+ makeObservable(this);
+ }
+
+ @observable private _newCondCache: Record<string, Conditional> = {};
+
+ getParams = (title: string, parameters?: Conditional): Conditional => {
+ if (parameters) return parameters;
+
+ if (!this._newCondCache[title]) {
+ this._newCondCache[title] = observable<Conditional>({
+ field: title,
+ operator: '=',
+ condition: '',
+ target: 'Own',
+ attribute: '',
+ value: '',
+ });
+ }
+ return this._newCondCache[title];
+ };
+
+ conditionForm = (title: string, parameters?: Conditional, empty: boolean = false) => {
+ const contentFieldTitles = this._props.menu.fieldsInfos
+ .filter(field => field.type !== TemplateFieldType.DATA)
+ .map(field => field.title)
+ .concat('Template');
+ const params: Conditional = this.getParams(title, parameters);
+
+ return (
+ <div className="form">
+ <div className="form-row">
+ <div className="form-row-plain-text">If</div>
+ <div className="form-row-plain-text">{title}</div>
+ <div className="operator-options-dropdown">
+ <span className="operator-dropdown-current">{params.operator ?? '='}</span>
+ <div className="operator-dropdown-option" onPointerDown={() => (params.operator = '=')}>
+ {'='}
+ </div>
+ </div>
+ <input className="form-row-textarea" onChange={action(e => (params.condition = e.target.value))} placeholder="value" value={params.condition} />
+ <div className="form-row-plain-text">then</div>
+ <div className="operator-options-dropdown">
+ <span className="operator-dropdown-current">{params.target ?? 'Own'}</span>
+ {contentFieldTitles.map((fieldTitle, i) => (
+ <div className="operator-dropdown-option" key={i} onPointerDown={() => (params.target = fieldTitle)}>
+ {fieldTitle === title ? 'Own' : fieldTitle}
+ </div>
+ ))}
+ </div>
+ <input className="form-row-textarea" onChange={action(e => (params.attribute = e.target.value))} placeholder="attribute" value={params.attribute} />
+ <div className="form-row-plain-text">{'becomes'}</div>
+ <input className="form-row-textarea" onChange={action(e => (params.value = e.target.value))} placeholder="value" value={params.value} />
+ </div>
+ {empty ? (
+ <DocCreatorMenuButton
+ icon={'plus'}
+ styles={'float-right border'}
+ function={() => {
+ this._newCondCache[title] = observable<Conditional>({
+ field: title,
+ operator: '=',
+ condition: '',
+ target: 'Own',
+ attribute: '',
+ value: '',
+ });
+ this._props.templateManager.addFieldCondition(title, params);
+ }}
+ />
+ ) : (
+ <DocCreatorMenuButton icon={'minus'} styles={'float-right border'} function={() => this._props.templateManager.removeFieldCondition(title, params)} />
+ )}
+ </div>
+ );
+ };
+
+ fieldPanel = (field: Col, id: number) => (
+ <div className="field-panel" key={id}>
+ <div
+ className="top-bar"
+ onPointerDown={e =>
+ this._props.menu.setUpButtonClick(
+ e,
+ action(() => {
+ if (this._collapsedCols.includes(field.title)) {
+ this._collapsedCols = this._collapsedCols.filter(col => col !== field.title);
+ } else {
+ this._collapsedCols.push(field.title);
+ }
+ })
+ )
+ }>
+ <span className="field-title">{`${field.title} Field`}</span>
+ <DocCreatorMenuButton icon={'minus'} styles={'no-margin absolute-right'} function={() => this._props.menu.removeField(field)} />
+ </div>
+ {this._collapsedCols.includes(field.title) ? null : (
+ <>
+ <div className="opts-bar">
+ <div className="opt-box">
+ <div className="top-bar"> Title </div>
+ <textarea className="content" style={{ width: '100%', height: 'calc(100% - 20px)' }} value={field.title} placeholder={'Enter title'} onChange={e => this._props.menu.setColTitle(field, e.target.value)} />
+ </div>
+ <div className="opt-box">
+ <div className="top-bar"> Type </div>
+ <div className="content">
+ <span className="type-display">{field.type === TemplateFieldType.TEXT ? 'Text Field' : field.type === TemplateFieldType.VISUAL ? 'File Field' : field.type === TemplateFieldType.DATA ? 'Data Field' : ''}</span>
+ <div className="bubbles">
+ <input className="bubble" type="radio" name="type" onClick={() => this._props.menu.setColType(field, TemplateFieldType.TEXT)} />
+ <div className="text">Text</div>
+ <input className="bubble" type="radio" name="type" onClick={() => this._props.menu.setColType(field, TemplateFieldType.VISUAL)} />
+ <div className="text">File</div>
+ <input className="bubble" type="radio" name="type" onClick={() => this._props.menu.setColType(field, TemplateFieldType.DATA)} />
+ <div className="text">Data</div>
+ </div>
+ </div>
+ </div>
+ </div>
+ {field.type === TemplateFieldType.DATA ? null : (
+ <>
+ <div className="sizes-box">
+ <div className="top-bar"> Valid Sizes </div>
+ <div className="content">
+ <div className="bubbles">
+ {Object.values(TemplateFieldSize).map(size => (
+ <div key={field + size}>
+ <input className="bubble" type="checkbox" name="type" checked={field.sizes.includes(size)} onChange={e => this._props.menu.modifyColSizes(field, size, e.target.checked)} />
+ <div className="text">{size}</div>
+ </div>
+ ))}
+ </div>
+ </div>
+ </div>
+ <div className="desc-box">
+ <div className="top-bar"> Prompt </div>
+ <textarea
+ className="content"
+ onChange={e => this._props.menu.setColDesc(field, e.target.value)}
+ defaultValue={field.desc === this._props.menu._dataViz?.GPTSummary?.get(field.title)?.desc ? '' : field.desc}
+ placeholder={this._props.menu._dataViz?.GPTSummary?.get(field.title)?.desc ?? 'Add a description/prompt to help with template generation.'}
+ />
+ </div>
+ </>
+ )}
+ <div className="conditionals-section">
+ <span className="conditionals-title">Conditional Logic</span>
+ {this.conditionForm(field.title, undefined, true)}
+ {this._props.templateManager._conditionalFieldLogic[field.title]?.map(condition => this.conditionForm(condition.field, condition))}
+ </div>
+ </>
+ )}
+ </div>
+ );
+
+ render() {
+ return (
+ <div className="docCreatorMenu-dashboard-view">
+ <div className="topbar">
+ <DocCreatorMenuButton icon="plus" function={this._props.menu.addField} />
+ <DocCreatorMenuButton icon="arrow-left" styles="float-right" function={action(() => (this._props.menu._menuContent = 'templates'))} />
+ </div>
+ <div className="panels-container">{this._props.menu.fieldsInfos.map((field, i) => this.fieldPanel(field, i))}</div>
+ </div>
+ );
+ }
+}
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplatePreviewBox.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplatePreviewBox.tsx
new file mode 100644
index 000000000..7d02fff12
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplatePreviewBox.tsx
@@ -0,0 +1,89 @@
+import { Colors } from '@dash/components/src';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { Template } from '../Template';
+import { action, makeObservable, observable } from 'mobx';
+import React from 'react';
+import { ObservableReactComponent } from '../../../../ObservableReactComponent';
+import { DocCreatorMenu } from '../DocCreatorMenu';
+import { IconProp } from '@fortawesome/fontawesome-svg-core';
+import { DocumentView } from '../../../DocumentView';
+import { emptyFunction } from '../../../../../../Utils';
+import { returnEmptyFilter, returnFalse } from '../../../../../../ClientUtils';
+import { Transform } from '../../../../../util/Transform';
+import { DefaultStyleProvider } from '../../../../StyleProvider';
+import { Doc, returnEmptyDoclist } from '../../../../../../fields/Doc';
+import { observer } from 'mobx-react';
+
+export interface TemplatePreviewBoxProps {
+ template: Template;
+ menu: DocCreatorMenu; // eslint-disable-next-line
+ leftButtonOpts?: [icon: IconProp, func: (...args: any) => void]; // eslint-disable-next-line
+ rightButtonOpts?: [icon: IconProp, func: (...args: any) => void];
+}
+
+@observer
+export class TemplatePreviewBox extends ObservableReactComponent<TemplatePreviewBoxProps> {
+ @observable private previewWindow: HTMLDivElement | null = null;
+
+ constructor(props: TemplatePreviewBoxProps) {
+ super(props);
+ makeObservable(this);
+ }
+
+ get doc() {
+ return this._props.template.getRenderedDoc() as Doc;
+ }
+
+ docPanelWidth = () => this.previewWindow?.clientWidth ?? this._props.menu._menuDimensions.height * 0.3;
+ docPanelHeight = () => this.previewWindow?.clientHeight ?? this._props.menu._menuDimensions.height * 0.3;
+ docScreenToLocalXf = () => new Transform(-this._props.menu._pageX - 5, -this._props.menu._pageY - 35, 1);
+
+ render() {
+ const template = this._props.template;
+
+ return (
+ <div
+ key={template.title}
+ className="docCreatorMenu-preview-window"
+ ref={action((node: HTMLDivElement | null) => (this.previewWindow = node))}
+ style={{
+ border: this._props.menu._selectedTemplate === template ? `solid 3px ${Colors.MEDIUM_BLUE}` : '',
+ boxShadow: this._props.menu._selectedTemplate === template ? `0 0 15px rgba(68, 118, 247, .8)` : '',
+ }}
+ onPointerDown={e => this._props.menu.setUpButtonClick(e, () => this._props.menu.updateSelectedTemplate(template))}>
+ {this._props.leftButtonOpts ? (
+ <button className="option-button left" onPointerDown={e => this._props.menu.setUpButtonClick(e, () => this._props.leftButtonOpts![1](template))}>
+ <FontAwesomeIcon icon={this._props.leftButtonOpts![0]} color="white" />
+ </button>
+ ) : null}
+ {this._props.rightButtonOpts ? (
+ <button className="option-button right" onPointerDown={e => this._props.menu.setUpButtonClick(e, () => this._props.rightButtonOpts![1](template))}>
+ <FontAwesomeIcon icon={this._props.rightButtonOpts![0]} color="white" />
+ </button>
+ ) : null}
+ <DocumentView
+ Document={this.doc}
+ isContentActive={emptyFunction} // !!! should be return false
+ addDocument={returnFalse}
+ moveDocument={returnFalse}
+ removeDocument={returnFalse}
+ PanelWidth={this.docPanelWidth}
+ PanelHeight={this.docPanelHeight}
+ ScreenToLocalTransform={this.docScreenToLocalXf}
+ renderDepth={1}
+ whenChildContentsActiveChanged={emptyFunction}
+ focus={emptyFunction}
+ styleProvider={DefaultStyleProvider}
+ addDocTab={this._props.menu._props.addDocTab}
+ pinToPres={emptyFunction}
+ childFilters={returnEmptyFilter}
+ childFiltersByRanges={returnEmptyFilter}
+ searchFilterDocs={returnEmptyDoclist}
+ // fitContentsToBox={returnFalse}
+ // fitWidth={returnFalse}
+ hideDecorations={true}
+ />
+ </div>
+ );
+ }
+}
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplatePreviewGrid.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplatePreviewGrid.tsx
new file mode 100644
index 000000000..da4851f84
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplatePreviewGrid.tsx
@@ -0,0 +1,60 @@
+import { makeObservable, runInAction } from "mobx";
+import React from "react";
+import ReactLoading from "react-loading";
+import { Doc } from "../../../../../../fields/Doc";
+import { StrCast } from "../../../../../../fields/Types";
+import { ObservableReactComponent } from "../../../../ObservableReactComponent";
+import { Template } from "../Template";
+import { observer } from "mobx-react";
+import { DocCreatorMenu } from "../DocCreatorMenu";
+import { TemplatePreviewBox } from "./TemplatePreviewBox";
+import { IconProp } from "@fortawesome/fontawesome-svg-core";
+import { DocCreatorMenuButton } from "./DocCreatorMenuButton";
+
+export interface SuggestedTemplatesProps {
+ menu: DocCreatorMenu;
+ loading?: boolean;
+ templates: Template[];
+ title: string;
+ styles?: string; // eslint-disable-next-line
+ optionsButtonOpts?: [IconProp, (...args: any) => any]; // eslint-disable-next-line
+ previewBoxLeftButtonOpts?: [IconProp, (...args: any) => any]; // eslint-disable-next-line
+ previewBoxRightButtonOpts?: [IconProp, (...args: any) => any];
+}
+
+@observer
+export class TemplatePreviewGrid extends ObservableReactComponent<SuggestedTemplatesProps> {
+
+ constructor(props: SuggestedTemplatesProps) {
+ super(props);
+ makeObservable(this);
+ }
+
+ render() {
+ return (
+ <div className="docCreatorMenu-section">
+ <div className="docCreatorMenu-section-topbar">
+ <div className="docCreatorMenu-section-title">{this.props.title}</div>
+ {this._props.optionsButtonOpts ?
+ <DocCreatorMenuButton icon={this._props.optionsButtonOpts[0] as IconProp} styles={'float-right'} function={() => runInAction(this._props.optionsButtonOpts![1])}/>
+ : null}
+ </div>
+ <div className={"docCreatorMenu-templates-preview-window " + this._props.styles}>
+ {this._props.loading ?
+ (<div className="loading-spinner">
+ <ReactLoading type="spin" color={StrCast(Doc.UserDoc().userVariantColor)} height={30} width={30} />
+ </div>)
+ : this.props.templates.map((template, i) => (
+ <TemplatePreviewBox
+ key={i}
+ template={template}
+ menu={this.props.menu}
+ leftButtonOpts={["magnifying-glass", (template: Template) => { this.props.menu.setExpandedView(template); this.forceUpdate(); }]}
+ rightButtonOpts={this._props.previewBoxRightButtonOpts}
+ />
+ ))}
+ </div>
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplateRenderPreviewWindow.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplateRenderPreviewWindow.tsx
new file mode 100644
index 000000000..9222d7349
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Menu/TemplateRenderPreviewWindow.tsx
@@ -0,0 +1,346 @@
+import { action, computed, makeObservable, observable, runInAction } from 'mobx';
+import { observer } from 'mobx-react';
+import { ObservableReactComponent } from '../../../../ObservableReactComponent';
+import { DocCreatorMenu, LayoutType } from '../DocCreatorMenu';
+import React from 'react';
+import { IconProp } from '@fortawesome/fontawesome-svg-core';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { setupMoveUpEvents, returnFalse, returnEmptyFilter } from '../../../../../../ClientUtils';
+import { emptyFunction } from '../../../../../../Utils';
+import { undoable } from '../../../../../util/UndoManager';
+import ReactLoading from 'react-loading';
+import { Doc, NumListCast, returnEmptyDoclist } from '../../../../../../fields/Doc';
+import { NumCast, StrCast } from '../../../../../../fields/Types';
+import { DefaultStyleProvider } from '../../../../StyleProvider';
+import { DocumentView } from '../../../DocumentView';
+import { Transform } from '../../../../../util/Transform';
+import { Docs, DocumentOptions } from '../../../../../documents/Documents';
+
+interface TemplatesRenderPreviewWindowProps {
+ menu: DocCreatorMenu;
+}
+
+@observer
+export class TemplatesRenderPreviewWindow extends ObservableReactComponent<TemplatesRenderPreviewWindowProps> {
+ @observable private _layout: { type: LayoutType; yMargin: number; xMargin: number; columns?: number; repeat: number } = { type: LayoutType.FREEFORM, yMargin: 10, xMargin: 10, columns: 1, repeat: 0 };
+
+ @observable private renderedDocs: Doc[] = [];
+ @observable private renderedDocCollection: Doc | undefined = undefined;
+
+ @observable private loading: boolean = false;
+
+ constructor(props: TemplatesRenderPreviewWindowProps) {
+ super(props);
+ makeObservable(this);
+ this.updateRenderedPreviewCollection();
+ }
+
+ @computed get canMakeDocs() {
+ return this._props.menu._selectedTemplate !== undefined && this._layout !== undefined;
+ }
+
+ @computed get docsToRender() {
+ if (this._props.menu.DEBUG_MODE) {
+ return [1, 2, 3, 4];
+ } else {
+ return NumListCast(this._props.menu._dataViz?.layoutDoc.dataViz_selectedRows);
+ }
+ }
+
+ @computed get rowsCount() {
+ switch (this._layout.type) {
+ case LayoutType.FREEFORM:
+ return Math.ceil(this.docsToRender.length / (this._layout.columns ?? 1)) ?? 0;
+ case LayoutType.CAROUSEL3D:
+ return 1.8;
+ default:
+ return 1;
+ }
+ }
+
+ @computed get columnsCount() {
+ switch (this._layout.type) {
+ case LayoutType.FREEFORM:
+ return this._layout.columns ?? 1;
+ case LayoutType.CAROUSEL3D:
+ return 3;
+ default:
+ return 1;
+ }
+ }
+
+ @action updateRenderedPreviewCollection = async () => {
+ this.loading = true;
+ this.renderedDocs = await this._props.menu.createDocsForPreview();
+ this.updateRenderedDocCollection();
+ };
+
+ /**
+ * Updates the preview that shows how all docs will be rendered in the chosen collection type.
+ @type the type of collection the docs should render to (ie. freeform, carousel, card)
+ */
+ updateRenderedDocCollection = () => {
+ if (!this.renderedDocs) return;
+
+ const collectionFactory = (): ((docs: Doc[], options: DocumentOptions) => Doc) => {
+ switch (this._layout.type) {
+ case LayoutType.CAROUSEL3D: return Docs.Create.Carousel3DDocument;
+ case LayoutType.FREEFORM: return Docs.Create.FreeformDocument;
+ case LayoutType.CARD: return Docs.Create.CardDeckDocument;
+ case LayoutType.MASONRY: return Docs.Create.MasonryDocument;
+ case LayoutType.CAROUSEL: return Docs.Create.CarouselDocument;
+ default: return Docs.Create.FreeformDocument;
+ } // prettier-ignore
+ };
+
+ const collection = collectionFactory()(this.renderedDocs, {
+ isDefaultTemplateDoc: true,
+ title: 'title',
+ backgroundColor: 'gray',
+ x: 200,
+ y: 200,
+ _width: 4000,
+ _height: 4000,
+ });
+
+ this.applyLayout(collection, this.renderedDocs);
+
+ this.renderedDocCollection = collection;
+
+ this.loading = false;
+
+ this.forceUpdate();
+ };
+
+ @action updateMargin = (input: string, xOrY: 'x' | 'y') => {
+ this._layout[`${xOrY}Margin`] = Number(input);
+ setTimeout(() => {
+ if (!this.renderedDocCollection || !this.renderedDocs) return;
+ this.applyLayout(this.renderedDocCollection, this.renderedDocs);
+ });
+ };
+
+ @action updateColumns = (input: string) => {
+ this._layout.columns = Number(input);
+ this.updateRenderedDocCollection();
+ };
+
+ applyLayout = (collection: Doc, docs: Doc[]) => {
+ const { horizontalSpan, verticalSpan } = this.previewInfo;
+ collection._height = verticalSpan;
+ collection._width = horizontalSpan;
+ collection.layout_fitWidth = true;
+ collection.freeform_fitContentsToBox = true;
+
+ const columns = (this._layout.columns ?? this.columnsCount) || 1;
+ const xGap = this._layout.xMargin;
+ const yGap = this._layout.yMargin;
+ const startX = -collection._width / 2;
+ const startY = -collection._height / 2;
+ const docHeight = NumCast(docs[0]?._height);
+ const docWidth = NumCast(docs[0]?._width);
+
+ let i = 0;
+ let docsChanged = 0;
+ let curX = startX;
+ let curY = startY;
+
+ while (docsChanged < docs.length) {
+ while (i < columns && docsChanged < docs.length) {
+ docs[docsChanged].x = curX;
+ docs[docsChanged].y = curY;
+ docs[docsChanged].layout_fitWidth = false;
+ curX += docWidth + xGap;
+ ++docsChanged;
+ ++i;
+ }
+ i = 0;
+ curX = startX;
+ curY += docHeight + yGap;
+ }
+ };
+
+ @computed
+ get previewInfo() {
+ const docHeight = NumCast(this.renderedDocs[0]?._height);
+ const docWidth = NumCast(this.renderedDocs[0]?._width);
+ const layout = this._layout;
+ return {
+ docHeight: docHeight,
+ docWidth: docWidth,
+ horizontalSpan: (docWidth + layout.xMargin) * this.columnsCount - layout.xMargin,
+ verticalSpan: (docHeight + layout.yMargin) * this.rowsCount - layout.yMargin,
+ };
+ }
+
+ get layoutConfigOptions() {
+ const optionInput = (icon: string, func: (input: string) => void, def?: number, key?: string, noMargin?: boolean) => {
+ return (
+ <div className="docCreatorMenu-option-container small no-margin" key={key} style={{ marginTop: noMargin ? '0px' : '' }}>
+ <div className="docCreatorMenu-option-title config layout-config">
+ <FontAwesomeIcon icon={icon as IconProp} />
+ </div>
+ <input defaultValue={def} onInput={e => func(e.currentTarget.value)} className="docCreatorMenu-input config layout-config" />
+ </div>
+ );
+ };
+
+ switch (this._layout.type) {
+ case LayoutType.FREEFORM:
+ return (
+ <div className="docCreatorMenu-configuration-bar">
+ {optionInput('arrows-up-down', (input: string) => this.updateMargin(input, 'y'), this._layout.xMargin, '2')}
+ {optionInput('arrows-left-right', (input: string) => this.updateMargin(input, 'x'), this._layout.xMargin, '3')}
+ {optionInput('table-columns', this.updateColumns, this._layout.columns, '4', true)}
+ </div>
+ );
+ default:
+ break;
+ }
+ }
+
+ layoutPanelWidth = () => this._props.menu._menuDimensions.width - 80;
+ layoutPanelHeight = () => this._props.menu._menuDimensions.height - 105;
+ layoutScreenToLocalXf = () => new Transform(-this._props.menu._pageX - 5, -this._props.menu._pageY - 35, 1);
+
+ layoutPreviewContents = action(() => {
+ return this.loading ? (
+ <div className="docCreatorMenu-layout-preview-window-wrapper loading">
+ <div className="loading-spinner">
+ <ReactLoading type="spin" color={StrCast(Doc.UserDoc().userVariantColor)} height={30} width={30} />
+ </div>
+ </div>
+ ) : !this.renderedDocCollection ? null : (
+ <div className="docCreatorMenu-layout-preview-window-wrapper">
+ <DocumentView
+ Document={this.renderedDocCollection}
+ isContentActive={emptyFunction}
+ addDocument={returnFalse}
+ moveDocument={returnFalse}
+ removeDocument={returnFalse}
+ PanelWidth={this.layoutPanelWidth}
+ PanelHeight={this.layoutPanelHeight}
+ ScreenToLocalTransform={this.layoutScreenToLocalXf}
+ renderDepth={5}
+ whenChildContentsActiveChanged={emptyFunction}
+ focus={emptyFunction}
+ styleProvider={DefaultStyleProvider}
+ addDocTab={this._props.menu._props.addDocTab}
+ pinToPres={emptyFunction}
+ childFilters={returnEmptyFilter}
+ childFiltersByRanges={returnEmptyFilter}
+ searchFilterDocs={returnEmptyDoclist}
+ hideDecorations={true}
+ />
+ </div>
+ );
+ });
+
+ selectionBox = (width: number, height: number, icon: string, specClass?: string, options?: JSX.Element[], manual?: boolean): JSX.Element => {
+ return (
+ <div className="docCreatorMenu-option-container">
+ <div className={`docCreatorMenu-option-title config ${specClass}`} style={{ width: width * 0.4, height: height }}>
+ <FontAwesomeIcon icon={icon as IconProp} />
+ </div>
+ {manual ? (
+ <input className={`docCreatorMenu-input config ${specClass}`} style={{ width: width * 0.6, height: height }} />
+ ) : (
+ <select className={`docCreatorMenu-input config ${specClass}`} style={{ width: width * 0.6, height: height }}>
+ {options}
+ </select>
+ )}
+ </div>
+ );
+ };
+
+ layoutOption = (option: LayoutType, optStyle?: object, specialFunc?: () => void) => {
+ return (
+ <div
+ className="docCreatorMenu-dropdown-option"
+ style={optStyle}
+ onPointerDown={e =>
+ this._props.menu.setUpButtonClick(e, () => {
+ specialFunc?.();
+ runInAction(() => {
+ this._layout.type = option;
+ this.updateRenderedDocCollection();
+ });
+ })
+ }>
+ {option}
+ </div>
+ );
+ };
+
+ get optionsMenuContents() {
+ const repeatOptions = [0, 1, 2, 3, 4, 5];
+
+ return (
+ <div className="docCreatorMenu-menu-container">
+ <div className="docCreatorMenu-option-container layout">
+ <div className="docCreatorMenu-dropdown-hoverable">
+ <div className="docCreatorMenu-option-title">{this._layout.type ? this._layout.type.toUpperCase() : 'Choose Layout'}</div>
+ <div className="docCreatorMenu-dropdown-content">
+ {this.layoutOption(LayoutType.FREEFORM, undefined, () => {
+ if (!this._layout.columns) this._layout.columns = Math.ceil(Math.sqrt(this.docsToRender.length));
+ })}
+ {this.layoutOption(LayoutType.CAROUSEL)}
+ {this.layoutOption(LayoutType.CAROUSEL3D)}
+ {this.layoutOption(LayoutType.MASONRY)}
+ </div>
+ </div>
+ </div>
+ {this._layout.type ? this.layoutConfigOptions : null}
+ {this.layoutPreviewContents()}
+ {this.selectionBox(
+ 60,
+ 20,
+ 'repeat',
+ undefined,
+ repeatOptions.map(num => <option key={num} onPointerDown={() => (this._layout.repeat = num)}>{`${num}x`}</option>)
+ )}
+ <hr className="docCreatorMenu-option-divider" />
+ <div className="docCreatorMenu-general-options-container">
+ <button
+ className="docCreatorMenu-save-layout-button"
+ onPointerDown={e =>
+ setupMoveUpEvents(
+ this,
+ e,
+ returnFalse,
+ emptyFunction,
+ undoable(clickEv => {
+ clickEv.stopPropagation();
+ //previous implementation deprecated; return later to add or scrap
+ return;
+ }, 'save layout')
+ )
+ }>
+ <FontAwesomeIcon icon="floppy-disk" />
+ </button>
+ <button
+ className="docCreatorMenu-create-docs-button"
+ style={{ backgroundColor: this.canMakeDocs ? '' : 'rgb(155, 155, 155)', border: this.canMakeDocs ? '' : 'solid 2px rgb(180, 180, 180)' }}
+ onPointerDown={e =>
+ setupMoveUpEvents(
+ this,
+ e,
+ returnFalse,
+ emptyFunction,
+ undoable(clickEv => {
+ clickEv.stopPropagation();
+ this.renderedDocCollection && this._props.menu.addRenderedCollectionToMainview(this.renderedDocCollection);
+ }, 'make docs')
+ )
+ }>
+ <FontAwesomeIcon icon="plus" />
+ </button>
+ </div>
+ </div>
+ );
+ }
+
+ render() {
+ return this.optionsMenuContents;
+ }
+}
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/Template.ts b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Template.ts
new file mode 100644
index 000000000..e2a2a3c1c
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Template.ts
@@ -0,0 +1,179 @@
+import { makeAutoObservable } from 'mobx';
+import { Col } from './DocCreatorMenu';
+import { TemplateFieldType, TemplateLayouts } from './TemplateBackend';
+import { DynamicField } from './TemplateFieldTypes/DynamicField';
+import { FieldSettings, TemplateField, ViewType } from './TemplateFieldTypes/TemplateField';
+import { Conditional } from './Backend/TemplateManager';
+import { TemplateDataField } from './TemplateFieldTypes/DataField';
+
+export class Template {
+ _mainField: DynamicField;
+
+ private _dataFields: TemplateDataField[] = [];
+
+ /**
+ * A Template can be created from a description of its fields (FieldSettings) or from a DynamicField
+ * @param definition definition of template as settings or DynamicField
+ */
+ constructor(definition: FieldSettings | DynamicField) {
+ makeAutoObservable(this);
+ this._mainField = definition instanceof DynamicField ? definition : this.setupMainField(definition);
+ }
+
+ get childFields() { return this._mainField?.getSubfields ?? []; } // prettier-ignore
+ get allFields() { return this._mainField?.getAllSubfields ?? []; } // prettier-ignore
+ get contentFields() { return this.allFields.filter(field => field.isContentField); } // prettier-ignore
+ get doc() { return this._mainField?.renderedDoc; } // prettier-ignore
+ get title() { return this._mainField?.getTitle(); } // prettier-ignore
+ get descriptionSummary() { return this.contentFields.map(f => `--- Field #${f.getID} (title: ${f.getTitle()}): ${f.getDescription ?? ''} ---`).join(); } // prettier-ignore
+ get compiledContent() { return this.contentFields.map(f => `--- Field #${f.getID} (title: ${f.getTitle()}): ${f.getContent() ?? ''} ---`).join(); } // prettier-ignore
+
+ cleanup = () => {
+ //dispose each subfields disposers, etc.
+ };
+
+ clone = (withContent: boolean = false) => {
+ const clone = new Template(this._mainField?.makeClone(undefined, withContent) ?? TemplateLayouts.BasicSettings);
+ this._dataFields.forEach(field => clone.addDataField(field.title));
+ return clone;
+ };
+
+ getRenderedDoc = () => this.doc;
+
+ getFieldByID = (id: number): TemplateField | undefined => this.allFields.filter(field => field.getID === id)[0];
+
+ getFieldByTitle = (title: string) => [...this.allFields, ...this._dataFields].filter(field => field.getTitle() === title)[0];
+
+ setupMainField = (templateInfo: FieldSettings) => TemplateField.CreateField(templateInfo, 1, undefined) as DynamicField;
+
+ assignColToField = (fieldID: number, col: Col) => {
+ const field = this.getFieldByID(fieldID);
+ field?.setContent(col.defaultContent ?? '', col.type === TemplateFieldType.VISUAL ? ViewType.IMG : ViewType.TEXT);
+ field?.setTitle(col.title);
+ };
+
+ addDataField = (title: string, content?: string) => this._dataFields.push(new TemplateDataField(title, content));
+
+ removeDataField = (title: string) => (this._dataFields = this._dataFields.filter(field => field.title !== title));
+
+ isValidTemplate = (cols: Col[]) => this.title !== 'template_framework' && this.maxMatches(this.getMatches(cols)) === this.contentFields.length;
+
+ applyConditionalLogicToField = (field: TemplateField | TemplateDataField, logic: Record<string, Conditional[]>) => {
+ if (field instanceof DynamicField) return;
+ const fieldStatements = logic[field.getTitle()];
+ const content = field.getContent();
+ fieldStatements?.forEach(statement => {
+ if (content === statement.condition) {
+ if (statement.target === 'Template') {
+ if (this._mainField.renderedDoc) {
+ this._mainField.renderedDoc[statement.attribute] = statement.value;
+ Object.assign(this._mainField.settings.opts, { [statement.attribute]: statement.value });
+ }
+ } else {
+ const targetField = this.getFieldByTitle(statement.target);
+ if (targetField instanceof TemplateField && targetField.renderedDoc) {
+ targetField.renderedDoc[statement.attribute] = statement.value;
+ Object.assign(targetField.settings.opts, { [statement.attribute]: statement.value });
+ }
+ }
+ }
+ });
+ };
+
+ applyConditionalLogic = (logic: Record<string, Conditional[]>) => {
+ [...this.allFields, ...this._dataFields].forEach(field => this.applyConditionalLogicToField(field, logic));
+ return this.getRenderedDoc();
+ };
+
+ setImageAsBackground(url: string, makeTransparent: boolean = false) {
+ const fieldSettings: FieldSettings = {
+ tl: [-1, -1],
+ br: [1, 1],
+ opts: {},
+ viewType: ViewType.IMG,
+ };
+
+ const field = TemplateField.CreateField(fieldSettings, Math.random() * 100 + 100, this._mainField);
+ field.setContent(url);
+
+ if (makeTransparent) {
+ this.allFields.forEach(aField => {
+ aField.updateDocSetting('backgroundColor', 'transparent');
+ aField.updateDocSetting('borderWidth', '0');
+ });
+ }
+
+ this._mainField.makeBackgroundField(field);
+ }
+
+ /**
+ * This function is just a hack for now to get around weird document icon stuff (specifically it misses the background)
+ */
+ setMatteBackground(makeTransparent: boolean = false) {
+ if (this._mainField.hasBackground) {
+ return;
+ }
+
+ const fieldSettings: FieldSettings = {
+ tl: [-1, -1],
+ br: [1, 1],
+ opts: { backgroundColor: String(this._mainField.renderedDoc!.backgroundColor) },
+ viewType: ViewType.TEXT,
+ };
+
+ const field = TemplateField.CreateField(fieldSettings, Math.random() * 100 + 100, this._mainField);
+
+ makeTransparent &&
+ this.allFields.forEach(aField => {
+ aField.updateDocSetting('backgroundColor', 'transparent');
+ aField.updateDocSetting('borderWidth', '0');
+ });
+
+ this._mainField.makeBackgroundField(field);
+ }
+
+ getMatches = (cols: Col[]): number[][] => {
+ const numFields = this.contentFields.length;
+
+ if (cols.length !== numFields) return [];
+
+ const matches = Array<number[]>(numFields);
+
+ this.contentFields.forEach((field, i) => (matches[i] = field.matches(cols)));
+
+ return matches;
+ };
+
+ maxMatches = (matches: number[][]) => {
+ if (matches.length === 0) return 0;
+
+ const fieldsCt = this.contentFields.length;
+ const used = Array<boolean>(fieldsCt).fill(false);
+ const mt = Array<number>(fieldsCt).fill(-1);
+
+ const augmentingPath = (v: number): boolean => {
+ if (!used[v]) {
+ used[v] = true;
+
+ for (const to of matches[v]) {
+ if (mt[to] === -1 || augmentingPath(mt[to])) {
+ mt[to] = v;
+ return true;
+ }
+ }
+ }
+ return false;
+ };
+
+ for (let v = 0; v < fieldsCt; ++v) {
+ used.fill(false);
+ augmentingPath(v);
+ }
+
+ let count: number = 0;
+ for (let i = 0; i < fieldsCt; ++i) {
+ if (mt[i] !== -1) ++count;
+ }
+ return count;
+ };
+}
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/Template.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Template.tsx
deleted file mode 100644
index 0a5097d4a..000000000
--- a/src/client/views/nodes/DataVizBox/DocCreatorMenu/Template.tsx
+++ /dev/null
@@ -1,139 +0,0 @@
-import { Doc, FieldType } from "../../../../../fields/Doc";
-import { Col } from "./DocCreatorMenu";
-import { DynamicField } from "./FieldTypes/DynamicField";
-import { Field, FieldSettings, ViewType } from "./FieldTypes/Field";
-import { } from "./FieldTypes/FieldUtils";
-import { } from "./FieldTypes/StaticField";
-
-export class Template {
-
- mainField: DynamicField;
- settings: FieldSettings;
-
- constructor(templateInfo: FieldSettings) {
- this.mainField = this.setupMainField(templateInfo);
- this.settings = templateInfo;
- }
-
- get childFields(): Field[] { return this.mainField.getSubfields };
- get allFields(): Field[] { return this.mainField.getAllSubfields };
- get contentFields(): Field[] { return this.allFields.filter(field => field.getViewType === ViewType.STATIC) };
- get doc(){ return this.mainField.renderedDoc(); };
-
- cloneBase = () => {
- const clone: Template = new Template(this.settings);
- clone.allFields.forEach(field => {
- const matchingField: Field = this.allFields.filter(f => f.getID === field.getID)[0];
- matchingField.applyAttributes(field);
- })
- return clone;
- }
-
- getRenderedDoc = () => {
- const doc: Doc = this.mainField.renderedDoc();
- this.contentFields.forEach(field => {
- const title: string = field.getTitle();
- const val: FieldType = field.getContent() as FieldType;
- if (!title || !val) return;
- doc[title] = val;
- });
- return doc;
- }
-
- getFieldByID = (id: number): Field => {
- return this.allFields.filter(field => field.getID === id)[0];
- }
-
- getFieldByTitle = (title: string) => {
- return this.allFields.filter(field => field.getTitle() === title)[0];
- }
-
- setupMainField = (templateInfo: FieldSettings) => {
- return new DynamicField(templateInfo, 1);
- }
-
- get descriptionSummary(): string {
- let summary: string = '';
- this.contentFields.forEach(field => {
- summary += `--- Field #${field.getID} (title: ${field.getTitle()}): ${field.getDescription ?? ''} ---`;
- });
- return summary;
- }
-
- get compiledContent(): string {
- let summary: string = '';
- this.contentFields.forEach(field => {
- summary += `--- Field #${field.getID} (title: ${field.getTitle()}): ${field.getContent() ?? ''} ---`;
- });
- return summary;
- }
-
- renderUpdates = () => {
- this.allFields.forEach(field => {
- field.updateRenderedDoc(field.renderedDoc());
- });
- };
-
- resetToBase = () => {
- this.allFields.forEach(field => {
- field.updateRenderedDoc();
- })
- }
-
- isValidTemplate = (cols: Col[]) => {
- const matches: number[][] = this.getMatches(cols);
- const maxMatches: number = this.maxMatches(matches);
- return maxMatches === this.contentFields.length;
- }
-
- getMatches = (cols: Col[]): number[][] => {
- const numFields = this.contentFields.length;
-
- if (cols.length !== numFields) return [];
-
- const matches: number[][] = Array(numFields)
- .fill([])
- .map(() => []);
-
- this.contentFields.forEach((field, i) => {
- matches[i] = (field.matches(cols));
- });
-
- return matches;
- }
-
- maxMatches = (matches: number[][]) => {
- if (matches.length === 0) return 0;
-
- const fieldsCt = this.contentFields.length;
- const used: boolean[] = Array(fieldsCt).fill(false);
- const mt: number[] = Array(fieldsCt).fill(-1);
-
- const augmentingPath = (v: number): boolean => {
- if (used[v]) return false;
- used[v] = true;
-
- for (const to of matches[v]) {
- if (mt[to] === -1 || augmentingPath(mt[to])) {
- mt[to] = v;
- return true;
- }
- }
- return false;
- };
-
- for (let v = 0; v < fieldsCt; ++v) {
- used.fill(false);
- augmentingPath(v);
- }
-
- let count: number = 0;
-
- for (let i = 0; i < fieldsCt; ++i) {
- if (mt[i] !== -1) ++count;
- }
-
- return count;
- };
-
-} \ No newline at end of file
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateBackend.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateBackend.ts
index d3282eda3..26fd3a8fc 100644
--- a/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateBackend.tsx
+++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateBackend.ts
@@ -1,10 +1,10 @@
-import { FieldSettings, ViewType } from "./FieldTypes/Field";
-import { } from "./FieldTypes/StaticField";
+import { FieldSettings, ViewType } from './TemplateFieldTypes/TemplateField';
export enum TemplateFieldType {
TEXT = 'text',
VISUAL = 'visual',
UNSET = 'unset',
+ DATA = 'data',
}
export enum TemplateFieldSize {
@@ -20,6 +20,14 @@ export class TemplateLayouts {
return Object.values(TemplateLayouts);
}
+ public static BasicSettings: FieldSettings = {
+ title: 'template_framework',
+ tl: [0, 0],
+ br: [400, 700],
+ viewType: ViewType.FREEFORM,
+ opts: {},
+ };
+
public static FourField001: FieldSettings = {
title: 'fourfield001',
tl: [0, 0],
@@ -27,7 +35,7 @@ export class TemplateLayouts {
viewType: ViewType.FREEFORM,
opts: {
backgroundColor: '#C0B887',
- cornerRounding: .05,
+ _layout_borderRounding: '.05',
//borderColor: '#6B461F',
//borderWidth: '12',
},
@@ -41,9 +49,9 @@ export class TemplateLayouts {
description: 'A title field for very short text that contextualizes the content.',
opts: {
backgroundColor: 'transparent',
- color: '#F1F0E9',
- contentXCentering: 'h-center',
- fontBold: true,
+ text_fontColor: '#F1F0E9',
+ hCentering: 'h-center',
+ contentBold: true,
},
},
{
@@ -54,9 +62,9 @@ export class TemplateLayouts {
sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE],
description: 'The main focus of the template; could be an image, long text, etc.',
opts: {
- cornerRounding: .05,
+ _layout_borderRounding: '.05',
borderColor: '#8F5B25',
- borderWidth: '6',
+ borderWidth: 6,
backgroundColor: '#CECAB9',
},
},
@@ -69,8 +77,8 @@ export class TemplateLayouts {
description: 'A caption for field #2, very short text.',
opts: {
backgroundColor: 'transparent',
- contentXCentering: 'h-center',
- color: '#F1F0E9',
+ hCentering: 'h-center',
+ text_fontColor: '#F1F0E9',
},
},
{
@@ -81,9 +89,9 @@ export class TemplateLayouts {
sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE],
description: 'A medium-sized field for medium/long text.',
opts: {
- cornerRounding: .05,
+ _layout_borderRounding: '.05',
borderColor: '#8F5B25',
- borderWidth: '6',
+ borderWidth: 6,
backgroundColor: '#CECAB9',
},
},
@@ -93,7 +101,7 @@ export class TemplateLayouts {
public static FourField002: FieldSettings = {
title: 'fourfield002',
viewType: ViewType.FREEFORM,
- tl: [0,0],
+ tl: [0, 0],
br: [425, 778],
opts: {
backgroundColor: '#242425',
@@ -107,10 +115,10 @@ export class TemplateLayouts {
sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE],
description: 'A medium to large-sized field suitable for an image or longer text that should be the main focus.',
opts: {
- borderWidth: '8',
+ borderWidth: 8,
borderColor: '#F8E71C',
backgroundColor: '#242425',
- color: 'white',
+ text_fontColor: 'white',
},
},
{
@@ -122,9 +130,9 @@ export class TemplateLayouts {
description: 'A tiny field for just a word or two of plain text.',
opts: {
backgroundColor: 'transparent',
- color: 'white',
- contentXCentering: 'h-center',
- fontTransform: 'uppercase',
+ text_fontColor: 'white',
+ hCentering: 'h-center',
+ text_transform: 'uppercase',
},
},
{
@@ -136,9 +144,9 @@ export class TemplateLayouts {
description: 'A tiny field for just a word or two of plain text.',
opts: {
backgroundColor: 'transparent',
- color: 'white',
- contentXCentering: 'h-center',
- fontTransform: 'uppercase',
+ text_fontColor: 'white',
+ hCentering: 'h-center',
+ text_transform: 'uppercase',
},
},
{
@@ -149,9 +157,9 @@ export class TemplateLayouts {
sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE],
description: 'A medium to large-sized field suitable for longer text that should contextualize field 1.',
opts: {
- borderWidth: '8',
+ borderWidth: 8,
borderColor: '#F8E71C',
- color: 'white',
+ text_fontColor: 'white',
backgroundColor: '#242425',
},
},
@@ -161,7 +169,7 @@ export class TemplateLayouts {
br: [-0.525, 0.075],
opts: {
backgroundColor: '#F8E71C',
- rotation: 45,
+ _rotation: 45,
},
},
{
@@ -170,7 +178,7 @@ export class TemplateLayouts {
br: [-0.2175, 0.0245],
opts: {
backgroundColor: '#F8E71C',
- rotation: 45,
+ _rotation: 45,
},
},
{
@@ -179,7 +187,7 @@ export class TemplateLayouts {
br: [0.045, 0.0245],
opts: {
backgroundColor: '#F8E71C',
- rotation: 45,
+ _rotation: 45,
},
},
{
@@ -188,7 +196,7 @@ export class TemplateLayouts {
br: [0.3075, 0.0245],
opts: {
backgroundColor: '#F8E71C',
- rotation: 45,
+ _rotation: 45,
},
},
{
@@ -197,7 +205,7 @@ export class TemplateLayouts {
br: [0.8, 0.075],
opts: {
backgroundColor: '#F8E71C',
- rotation: 45,
+ _rotation: 45,
},
},
],
@@ -266,8 +274,8 @@ export class TemplateLayouts {
public static FourField004: FieldSettings = {
title: 'fourfield04',
viewType: ViewType.FREEFORM,
- tl: [0,0],
- br: [414,583],
+ tl: [0, 0],
+ br: [414, 583],
opts: {
backgroundColor: '#6CCAF0',
//borderColor: '#1088C3',
@@ -283,9 +291,9 @@ export class TemplateLayouts {
description: 'A tiny field for just a word or two of plain text.',
opts: {
backgroundColor: '#E2B4F5',
- borderWidth: '9',
+ borderWidth: 9,
borderColor: '#9222F1',
- contentXCentering: 'h-center',
+ hCentering: 'h-center',
},
},
{
@@ -297,9 +305,9 @@ export class TemplateLayouts {
description: 'A tiny field for just a word or two of plain text.',
opts: {
backgroundColor: '#F5B4DD',
- borderWidth: '9',
+ borderWidth: 9,
borderColor: '#E260F3',
- contentXCentering: 'h-center',
+ hCentering: 'h-center',
},
},
{
@@ -310,7 +318,7 @@ export class TemplateLayouts {
sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE],
description: 'A large to huge field for visual content that is the main content of the template.',
opts: {
- borderWidth: '16',
+ borderWidth: 16,
borderColor: '#A2BD77',
},
},
@@ -322,7 +330,7 @@ export class TemplateLayouts {
sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE],
description: 'A medium to large field for text that describes the visual content above',
opts: {
- borderWidth: '9',
+ borderWidth: 9,
borderColor: '#F0D601',
backgroundColor: '#F3F57D',
},
@@ -334,7 +342,7 @@ export class TemplateLayouts {
opts: {
backgroundColor: 'transparent',
borderColor: '#007C0C',
- borderWidth: '10',
+ borderWidth: 10,
},
},
],
@@ -343,218 +351,229 @@ export class TemplateLayouts {
public static FourField005: FieldSettings = {
title: 'fourfield05',
viewType: ViewType.FREEFORM,
- tl: [0,0],
- br: [400,550],
+ tl: [0, 0],
+ br: [400, 514],
opts: {
backgroundColor: '#95A575',
},
subfields: [
{
viewType: ViewType.STATIC,
- tl: [-0.9, -.925],
- br: [-.075, -.775],
+ tl: [-0.9, -0.925],
+ br: [-0.075, -0.775],
types: [TemplateFieldType.TEXT],
sizes: [TemplateFieldSize.TINY, TemplateFieldSize.SMALL],
description: 'A small text field for a title or word(s) that categorize the rest of the content.',
opts: {
borderColor: '#3B4A2C',
- borderWidth: '8',
- contentXCentering: "h-center",
+ borderWidth: 8,
+ hCentering: 'h-center',
backgroundColor: '#B8DC90',
},
},
{
viewType: ViewType.STATIC,
- tl: [.075, -.925],
- br: [.9, -.775],
+ tl: [0.075, -0.925],
+ br: [0.9, -0.775],
types: [TemplateFieldType.TEXT],
sizes: [TemplateFieldSize.TINY, TemplateFieldSize.SMALL],
description: 'A small text field for a title that categorizes the rest of the content.',
opts: {
borderColor: '#3B4A2C',
- borderWidth: '8',
- contentXCentering: "h-center",
+ borderWidth: 8,
+ hCentering: 'h-center',
backgroundColor: '#B8DC90',
},
},
{
viewType: ViewType.DEC,
- tl: [-.82, -.4],
- br: [-.5, -.2],
+ tl: [-0.82, -0.4],
+ br: [-0.5, -0.2],
opts: {
backgroundColor: '#94B058',
borderColor: '#3B4A2C',
- borderWidth: '8',
+ borderWidth: 8,
},
},
{
viewType: ViewType.STATIC,
- tl: [-0.66, -.65],
- br: [0.66, .25],
+ tl: [-0.66, -0.65],
+ br: [0.66, 0.25],
types: [TemplateFieldType.VISUAL],
sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE],
description: 'A medium to large field in the center of the template, for the main visual content.',
opts: {
borderColor: '#3B4A2C',
- borderWidth: '8',
+ borderWidth: 8,
backgroundColor: '#B8DC90',
},
},
{
viewType: ViewType.STATIC,
- tl: [-.875, .425],
- br: [0.875, .925],
+ tl: [-0.875, 0.425],
+ br: [0.875, 0.925],
types: [TemplateFieldType.TEXT],
sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE],
description: 'A medium to large field at the bottom of the template, for the main text content.',
opts: {
borderColor: '#3B4A2C',
- borderWidth: '8',
- contentXCentering: "h-center",
+ borderWidth: 8,
+ hCentering: 'h-center',
backgroundColor: '#B8DC90',
},
},
{
viewType: ViewType.DEC,
- tl: [-1.1, -.62],
- br: [-.9, -.5],
+ tl: [-1.1, -0.62],
+ br: [-0.9, -0.5],
opts: {
backgroundColor: '#7A9D31',
borderColor: '#3B4A2C',
- borderWidth: '8',
+ borderWidth: 8,
},
},
{
viewType: ViewType.DEC,
tl: [-1.1, 0],
- br: [-.9, .15],
+ br: [-0.9, 0.15],
opts: {
backgroundColor: '#94B058',
borderColor: '#3B4A2C',
- borderWidth: '8',
+ borderWidth: 8,
},
},
{
viewType: ViewType.DEC,
- tl: [-.93, -.265],
- br: [-.715, -.125],
+ tl: [-0.93, -0.265],
+ br: [-0.715, -0.125],
opts: {
backgroundColor: '#728745',
borderColor: '#3B4A2C',
- borderWidth: '8',
+ borderWidth: 8,
},
},
{
viewType: ViewType.DEC,
- tl: [.7, -.45],
- br: [.85, -.3],
+ tl: [0.7, -0.45],
+ br: [0.85, -0.3],
opts: {
backgroundColor: '#7A9D31',
borderColor: '#3B4A2C',
- borderWidth: '8',
+ borderWidth: 8,
},
},
{
viewType: ViewType.DEC,
- tl: [.8, .03],
- br: [1.2, .33],
+ tl: [0.8, 0.03],
+ br: [1.2, 0.33],
opts: {
backgroundColor: '#728745',
borderColor: '#3B4A2C',
- borderWidth: '8',
+ borderWidth: 8,
},
},
{
viewType: ViewType.DEC,
- tl: [.875, -.13],
- br: [1.2, .12],
+ tl: [0.875, -0.13],
+ br: [1.2, 0.12],
opts: {
backgroundColor: '#94B058',
borderColor: '#3B4A2C',
- borderWidth: '8',
+ borderWidth: 8,
},
},
- ]
- }
+ ],
+ };
public static FourFieldCarousel: FieldSettings = {
title: 'title_fourfieldcarousel',
viewType: ViewType.FREEFORM,
- tl:[0,0],
- br:[500, 600],
+ tl: [0, 0],
+ br: [500, 600],
opts: {
- backgroundColor: '#DDD3A9',
+ backgroundColor: '#D7CBAB',
},
subfields: [
{
viewType: ViewType.STATIC,
- tl: [-0.8, -.9],
- br: [0.8, -.5],
+ tl: [-0.8, -0.9],
+ br: [0.8, -0.5],
types: [TemplateFieldType.TEXT],
sizes: [TemplateFieldSize.TINY, TemplateFieldSize.SMALL],
description: 'A small text field for a title that categorizes the rest of the content.',
opts: {
- borderColor: 'yellow',
- borderWidth: '8',
- contentXCentering: "h-center",
+ hCentering: 'h-center',
backgroundColor: 'transparent',
+ text_transform: 'uppercase',
},
},
{
viewType: ViewType.CAROUSEL3D,
- tl: [-0.9, -.3],
- br: [0.9, .9],
+ tl: [-0.9, -0.5],
+ br: [0.9, 0.25],
opts: {
- borderColor: 'yellow',
- borderWidth: '8',
- backgroundColor: 'transparent',
+ borderColor: '#847F69',
+ borderWidth: 8,
+ backgroundColor: '#C8BA94',
},
subfields: [
{
viewType: ViewType.STATIC,
- tl: [-.3, -.6],
- br: [.3, .6],
+ tl: [-0.4, -0.6],
+ br: [0.4, 0.6],
types: [TemplateFieldType.VISUAL, TemplateFieldType.TEXT],
sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE],
description: 'A medium to large field for content that will share central focus with other content in the carousel.',
opts: {
- borderColor: 'yellow',
- borderWidth: '8',
+ //borderColor: 'yellow',
+ //borderWidth: '8',
},
},
{
viewType: ViewType.STATIC,
- tl: [-.3, -.6],
- br: [.3, .6],
+ tl: [-0.4, -0.6],
+ br: [0.4, 0.6],
types: [TemplateFieldType.VISUAL, TemplateFieldType.TEXT],
sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE],
description: 'A medium to large field for content that will share central focus with other content in the carousel.',
opts: {
- borderColor: 'black',
- borderWidth: '8',
+ //borderColor: 'black',
+ //borderWidth: '8',
},
},
{
viewType: ViewType.STATIC,
- tl: [-.3, -.6],
- br: [.3, .6],
+ tl: [-0.4, -0.6],
+ br: [0.4, 0.6],
types: [TemplateFieldType.VISUAL, TemplateFieldType.TEXT],
sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE],
description: 'A medium to large field for content that will share central focus with other content in the carousel.',
opts: {
- borderColor: 'yellow',
- borderWidth: '8',
+ //borderColor: 'yellow',
+ //borderWidth: '8',
},
},
- ]
+ ],
},
- ]
- }
+ {
+ viewType: ViewType.STATIC,
+ tl: [-0.9, 0.35],
+ br: [0.9, 0.9],
+ types: [TemplateFieldType.TEXT],
+ sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE],
+ description: 'A medium text field for a description of the content in the carousel.',
+ opts: {
+ hCentering: 'h-center',
+ backgroundColor: 'transparent',
+ },
+ },
+ ],
+ };
public static ThreeField001: FieldSettings = {
title: 'threefield001',
viewType: ViewType.FREEFORM,
- tl: [0,0],
+ tl: [0, 0],
br: [575, 770],
opts: {
backgroundColor: '#DDD3A9',
@@ -567,23 +586,23 @@ export class TemplateLayouts {
description: 'A medium to large field for visual content that is the central focus.',
opts: {
borderColor: 'yellow',
- borderWidth: '8',
+ borderWidth: 8,
backgroundColor: '#DDD3A9',
- rotation: 45,
+ _rotation: 45,
},
subfields: [
{
- viewType: ViewType.STATIC,
- tl: [-1.25, -1.25],
- br: [1.25, 1.25],
- types: [TemplateFieldType.VISUAL],
- sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE],
- description: 'A medium to large field for visual content that is the central focus.',
- opts: {
- rotation: -45,
+ viewType: ViewType.STATIC,
+ tl: [-1.25, -1.25],
+ br: [1.25, 1.25],
+ types: [TemplateFieldType.VISUAL],
+ sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE],
+ description: 'A medium to large field for visual content that is the central focus.',
+ opts: {
+ _rotation: -45,
+ },
},
- },
- ]
+ ],
},
{
viewType: ViewType.STATIC,
@@ -594,7 +613,7 @@ export class TemplateLayouts {
description: 'A very small text field for one to a few words. A good caption for the image.',
opts: {
backgroundColor: 'transparent',
- contentXCentering: 'h-center',
+ hCentering: 'h-center',
},
},
{
@@ -606,7 +625,7 @@ export class TemplateLayouts {
description: 'A medium to large text field for a thorough description of the image. ',
opts: {
backgroundColor: 'transparent',
- color: 'white',
+ text_fontColor: 'white',
},
},
{
@@ -615,18 +634,18 @@ export class TemplateLayouts {
br: [1.8, -0.66],
opts: {
backgroundColor: '#CEB155',
- rotation: 45,
+ _rotation: 45,
},
subfields: [
{
viewType: ViewType.DEC,
- tl: [-1, -.7],
- br: [1, -.625],
+ tl: [-1, -0.7],
+ br: [1, -0.625],
opts: {
backgroundColor: 'yellow',
},
},
- ]
+ ],
},
{
viewType: ViewType.FREEFORM,
@@ -634,18 +653,18 @@ export class TemplateLayouts {
br: [-0.2, -0.66],
opts: {
backgroundColor: '#CEB155',
- rotation: 135,
+ _rotation: 135,
},
subfields: [
{
viewType: ViewType.DEC,
- tl: [-1, -.7],
- br: [1, -.625],
+ tl: [-1, -0.7],
+ br: [1, -0.625],
opts: {
backgroundColor: 'yellow',
},
},
- ]
+ ],
},
{
viewType: ViewType.FREEFORM,
@@ -653,18 +672,18 @@ export class TemplateLayouts {
br: [1.66, 1.25],
opts: {
backgroundColor: '#CEB155',
- rotation: 135,
+ _rotation: 135,
},
subfields: [
{
viewType: ViewType.DEC,
- tl: [-1, -.7],
- br: [1, -.625],
+ tl: [-1, -0.7],
+ br: [1, -0.625],
opts: {
backgroundColor: 'yellow',
},
},
- ]
+ ],
},
{
viewType: ViewType.FREEFORM,
@@ -672,18 +691,18 @@ export class TemplateLayouts {
br: [-0.33, 1.25],
opts: {
backgroundColor: '#CEB155',
- rotation: 45,
+ _rotation: 45,
},
subfields: [
{
viewType: ViewType.DEC,
- tl: [-1, -.7],
- br: [1, -.625],
+ tl: [-1, -0.7],
+ br: [1, -0.625],
opts: {
backgroundColor: 'yellow',
},
},
- ]
+ ],
},
],
};
@@ -691,7 +710,7 @@ export class TemplateLayouts {
public static ThreeField002: FieldSettings = {
title: 'threefield002',
viewType: ViewType.FREEFORM,
- tl: [0,0],
+ tl: [0, 0],
br: [477, 662],
opts: {
backgroundColor: '#9E9C95',
@@ -705,7 +724,7 @@ export class TemplateLayouts {
sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE],
description: 'A medium to large visual field for the main content of the template',
opts: {
- borderWidth: '15',
+ borderWidth: 15,
borderColor: '#E0E0DA',
},
},
@@ -718,10 +737,10 @@ export class TemplateLayouts {
description: 'A very small text field for one to a few words. The content should represent a general categorization of the image.',
opts: {
backgroundColor: 'transparent',
- color: '#AF0D0D',
- fontTransform: 'uppercase',
- fontBold: true,
- contentXCentering: 'h-left',
+ text_fontColor: '#AF0D0D',
+ text_transform: 'uppercase',
+ contentBold: true,
+ hCentering: 'h-left',
},
},
{
@@ -733,8 +752,8 @@ export class TemplateLayouts {
description: 'A very small text field for one to a few words. The content should contextualize field 2.',
opts: {
backgroundColor: 'transparent',
- color: 'black',
- contentXCentering: 'h-right',
+ text_fontColor: 'black',
+ hCentering: 'h-right',
},
},
{
@@ -747,6 +766,4 @@ export class TemplateLayouts {
},
],
};
-}
-
-
+} \ No newline at end of file
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/DataField.ts b/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/DataField.ts
new file mode 100644
index 000000000..6b4086483
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/DataField.ts
@@ -0,0 +1,20 @@
+
+import { ViewType } from "./TemplateField";
+
+export class TemplateDataField {
+
+ viewType: ViewType = ViewType.NONE;
+
+ title: string = '';
+ content: string | undefined;
+
+ constructor(title: string, content?: string) {
+ this.title = title;
+ this.content = content;
+ }
+
+ setContent(content: string, viewType?: ViewType) { this.content = content }
+ getContent() { return this.content }
+
+ getTitle() { return this.title }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/DecorationField.ts b/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/DecorationField.ts
new file mode 100644
index 000000000..98a9dc7a6
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/DecorationField.ts
@@ -0,0 +1,3 @@
+import { DynamicField } from './DynamicField';
+
+export class DecorationField extends DynamicField {}
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/DynamicField.ts b/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/DynamicField.ts
new file mode 100644
index 000000000..b9042258b
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/DynamicField.ts
@@ -0,0 +1,131 @@
+import { IDisposer } from 'mobx-utils';
+import { Doc } from '../../../../../../fields/Doc';
+import { DocData } from '../../../../../../fields/DocSymbols';
+import { List } from '../../../../../../fields/List';
+import { NumCast } from '../../../../../../fields/Types';
+import { Docs } from '../../../../../documents/Documents';
+import { DocumentType } from '../../../../../documents/DocumentTypes';
+import { FieldSettings, TemplateField, ViewType } from './TemplateField';
+
+export class DynamicField extends TemplateField {
+ protected _disposers: { [name: string]: IDisposer } = {};
+ protected _subfields: TemplateField[] = [];
+ protected backgroundField: TemplateField | undefined;
+
+ get getSubfields() {
+ return this._subfields;
+ }
+ get getAllSubfields(): TemplateField[] {
+ return this.getSubfields.flatMap(field => [field, ...((field as DynamicField).getAllSubfields ?? [])]);
+ }
+
+ get hasBackground() {
+ return this.backgroundField !== undefined;
+ }
+
+ handleFieldUpdate = (newDocsList: Doc[]) => {
+ const currRenderedDocs = new Set(this.getSubfields.filter(field => field.renderedDoc).map(field => field.renderedDoc!));
+ newDocsList.forEach(doc => !currRenderedDocs.has(doc) && this.addFieldFromDoc(doc));
+ currRenderedDocs.forEach(doc => {
+ if (!newDocsList.includes(doc)) {
+ this._subfields.forEach(field => field.renderedDoc === doc && this.removeField(field));
+ }
+ });
+ };
+
+ addFieldFromDoc = (doc: Doc) => {
+ const par = this._renderDoc;
+ const settings: FieldSettings = {
+ tl: [Number(doc._x) / NumCast(par?._width, 1), Number(doc?._y) / NumCast(par?._height, 1)],
+ br: [(Number(doc._x) + Number(doc._width)) / NumCast(par?._width, 1), (Number(doc._y) + Number(doc._height)) / NumCast(par?._height, 1)],
+ viewType: doc.type === DocumentType.COL ? ViewType.FREEFORM : ViewType.STATIC,
+ opts: {},
+ };
+
+ this._subfields.push(TemplateField.CreateField(settings, this._subfields.length, this));
+ };
+
+ addField = (field: TemplateField, layer: number = 0) => {
+ if (!this._subfields.includes(field)) {
+ this._subfields.splice(layer, 0, field);
+ }
+ };
+
+ dispose = () => Object.values(this._disposers).forEach(disposer => disposer?.());
+
+ removeField = (field: TemplateField) => {
+ // field.renderedDoc && this._renderDoc && Doc.RemoveDocFromList(this._renderDoc, undefined, field.renderedDoc);
+ this._subfields.splice(this._subfields.indexOf(field), 1);
+ (field as DynamicField).dispose?.();
+ };
+
+ // implement Field's abstract method for replacing a subfield with a new one
+ exchangeFields(newField: TemplateField, oldField: TemplateField) {
+ this._subfields.splice(this._subfields.indexOf(oldField), 1, newField);
+ this.refreshRenderedDoc();
+ }
+
+ get isContentField(): boolean {
+ return false;
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ setContent(content: string, type: ViewType) {}
+
+ getContent = () => '';
+
+ addChildToDocument = (doc: Doc) => this._renderDoc && Doc.SetContainer(doc, this._renderDoc);
+
+ makeBackgroundField = (field: TemplateField) => {
+ if (this.backgroundField && this.backgroundField !== field) {
+ this.removeField(this.backgroundField);
+ this.backgroundField = undefined;
+ }
+ if (field && field !== this.backgroundField) {
+ this.addField(field);
+ this.backgroundField = field;
+ }
+ this.refreshRenderedDoc();
+ }
+
+ matches = (): Array<number> => [];
+
+ makeClone(parent?: DynamicField, withContent: boolean = false) {
+ const dynClone = super.makeClone(parent) as DynamicField;
+ dynClone._subfields = this.getSubfields.map(field => {
+ if (field === this.backgroundField) {
+ const backgroundField: TemplateField = field.makeClone(dynClone, true);
+ dynClone.makeBackgroundField(backgroundField);
+ return backgroundField;
+ } else {
+ return field.makeClone(dynClone, withContent)
+ }
+ });
+ if (dynClone._renderDoc) {
+ dynClone._renderDoc[DocData].data = new List<Doc>(dynClone.getSubfields.filter(sub => sub.renderedDoc).map(sub => sub.renderedDoc!));
+ }
+ return dynClone;
+ }
+
+ initRenderDoc = (settings: FieldSettings) => {
+ //this._disposers.fieldList = reaction(() => DocListCast(this._renderDoc?.[Doc.LayoutFieldKey(this._renderDoc)]), this.handleFieldUpdate);
+ this._subfields = settings.subfields?.map((fieldSettings, index) => {return TemplateField.CreateField(fieldSettings, index, this)}) || [];
+ const renderedSubfields = this._subfields.filter(field => field.renderedDoc).map(field => field.renderedDoc!);
+ settings.opts.title = settings.title;
+ this._renderDoc = (() => { switch (settings.viewType) {
+ case ViewType.CAROUSEL3D: return Docs.Create.Carousel3DDocument(renderedSubfields, settings.opts);
+ case ViewType.FREEFORM:
+ default: return Docs.Create.FreeformDocument(renderedSubfields, settings.opts);
+ }})(); // prettier-ignore
+ return this;
+ };
+
+ refreshRenderedDoc = () => {
+ const renderedSubfields = this._subfields.filter(field => field.renderedDoc).map(field => field.renderedDoc!);
+ this._renderDoc = (() => { switch (this.settings.viewType) {
+ case ViewType.CAROUSEL3D: return Docs.Create.Carousel3DDocument(renderedSubfields, this.settings.opts);
+ case ViewType.FREEFORM:
+ default: return Docs.Create.FreeformDocument(renderedSubfields, this.settings.opts);
+ }})();
+ }
+}
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/StaticContentField.ts b/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/StaticContentField.ts
new file mode 100644
index 000000000..569c43af4
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/StaticContentField.ts
@@ -0,0 +1,62 @@
+import { FieldResult } from '../../../../../../fields/Doc';
+import { DocData } from '../../../../../../fields/DocSymbols';
+import { RichTextField } from '../../../../../../fields/RichTextField';
+import { ImageField } from '../../../../../../fields/URLField';
+import { Docs } from '../../../../../documents/Documents';
+import { FieldSettings, TemplateField, ViewType } from './TemplateField';
+import { TemplateFieldUtils } from './TemplateFieldUtils';
+
+export abstract class StaticContentField extends TemplateField {
+ protected _content: string = '';
+
+ getContent = () => this._content ?? 'unset';
+ get isContentField(): boolean {
+ return true;
+ }
+ protected setDataContent(viewType: ViewType, fieldKey: string, data: FieldResult, content: string, type?: ViewType) {
+ super.setContent(content, type);
+
+ if (type === viewType || type === undefined) {
+ this._content = content;
+ this._renderDoc && (this._renderDoc[DocData][fieldKey] = data);
+ } else {
+ this.changeFieldType(type).setContent(content, type);
+ }
+ }
+}
+
+export class ImageTemplateField extends StaticContentField {
+ setContent(url: string, type?: ViewType) {
+ this.setDataContent(ViewType.IMG, 'data', new ImageField(url), url, type);
+ this._renderDoc!['backgroundColor'] = 'white';
+ }
+
+ initRenderDoc(settings: FieldSettings) {
+ settings.opts.title = settings.title ?? '';
+ settings.opts._layout_fitWidth = false;
+ this._renderDoc = Docs.Create.ImageDocument(this._content, settings.opts);
+ return this;
+ }
+
+ updateDocSetting(setting: string, newVal: string) {
+ if (this._renderDoc) this._renderDoc[setting] = newVal;
+ if (setting !== 'backgroundColor') {
+ const settings: {[s: string]: string } = {[setting]: newVal}
+ Object.assign(this.settings.opts, settings);
+ }
+ }
+}
+
+export class TextTemplateField extends StaticContentField {
+ setContent(text: string, type?: ViewType) {
+ const fontSize: number = TemplateFieldUtils.calculateFontSize(this._dimensions?.width ?? 10, this._dimensions?.height ?? 10, text, true);
+ this.setDataContent(ViewType.TEXT, 'text', RichTextField.textToRtf(text, undefined, {fontSize: fontSize}), text, type);
+ }
+
+ initRenderDoc(settings: FieldSettings) {
+ settings.opts.title = settings.title ?? '';
+ settings.opts.text_fontSize = TemplateFieldUtils.calculateFontSize(this._dimensions?.width ?? 10, this._dimensions?.height ?? 10, '', true) + '';
+ this._renderDoc = Docs.Create.TextDocument(this._content, settings.opts);
+ return this;
+ }
+}
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/TemplateField.ts b/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/TemplateField.ts
new file mode 100644
index 000000000..091ef834a
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/TemplateField.ts
@@ -0,0 +1,172 @@
+/* eslint-disable no-use-before-define */
+import { Doc } from '../../../../../../fields/Doc';
+import { DocumentOptions } from '../../../../../documents/Documents';
+import { Col } from '../DocCreatorMenu';
+import { Template } from '../Template';
+import { TemplateFieldSize, TemplateFieldType } from '../TemplateBackend';
+
+export abstract class TemplateField {
+ /**
+ * Creates and initializes a new TemplateField based on the settings and parameters
+ *
+ * implemented in FieldUtils and assigned in main (to avoid import cycles)
+ *
+ * @param settings - specification of the field type and other parameters
+ * @param index -
+ * @param parent - TemplateField that contains the new field
+ * @param sameId -
+ * @returns TemplateField
+ */
+ static CreateField: (settings: FieldSettings, index: number, parent: TemplateField | undefined, sameId?: boolean) => TemplateField;
+
+ protected _parent?: TemplateField;
+ protected _id: number;
+ protected _title: string = '';
+ protected _settings: FieldSettings;
+ protected _renderDoc: Doc | undefined;
+ protected _dimensions: FieldDimensions | undefined;
+
+ constructor(settings: FieldSettings, id: number = 1, parent?: TemplateField) {
+ this._id = id;
+ this._parent = parent;
+ this._settings = settings;
+ this._title = settings.title ?? '';
+ this._dimensions = this.getLocalDimensions(this._settings, this._parent?.getDimensions);
+ this.applyBasicOpts(this._dimensions, settings);
+ return this;
+ }
+
+ get renderedDoc() {
+ return this._renderDoc;
+ }
+ get getDimensions() {
+ return this._dimensions;
+ }
+ get getID() {
+ return this._id;
+ }
+ get getDescription(): string {
+ return this._settings?.description ?? '';
+ }
+ get viewType(): ViewType | undefined {
+ return this._settings?.viewType;
+ }
+
+ get settings(): FieldSettings {
+ return this._settings;
+ }
+
+ abstract get isContentField(): boolean;
+ abstract initRenderDoc(settings: FieldSettings): TemplateField;
+ abstract getContent(): string;
+
+ setContent(content: string, type?: ViewType) {
+ if (type) this._settings.viewType = type;
+ }
+
+ setTitle = (title: string) => {
+ this._title = title;
+ this.settings.title = title;
+ if (this._renderDoc) this._renderDoc.title = title;
+ };
+ getTitle = () => this._title;
+
+ updateDocSetting(setting: string, newVal: string) {
+ if (this._renderDoc) this._renderDoc[setting] = newVal;
+ const settings: { [s: string]: string } = { [setting]: newVal };
+ Object.assign(this.settings.opts, settings);
+ }
+
+ makeClone(parent?: TemplateField, withContent: boolean = false) {
+ const settings: FieldSettings = structuredClone(this._settings);
+ const cloned = TemplateField.CreateField(settings, this._id, parent, true); // create a value for this.Document/subfields that we want to ignore
+ cloned.renderedDoc!.width = this.renderedDoc!.width;
+ cloned.renderedDoc!.height = this.renderedDoc!.height;
+ cloned.renderedDoc!.x = this.renderedDoc!.x;
+ cloned.renderedDoc!.y = this.renderedDoc!.y;
+ cloned.renderedDoc!.backgroundColor = this.renderedDoc!.backgroundColor;
+ cloned.setTitle(this._title);
+ cloned._dimensions = this._dimensions;
+ withContent && cloned.setContent(this.getContent());
+ return cloned;
+ }
+
+ exchangeFields(newField: TemplateField, oldField: TemplateField) {
+ throw new Error('Only DynamicField can exchange fields.' + newField._title + ' ' + oldField._title);
+ }
+
+ changeFieldType = (newType: ViewType): TemplateField => {
+ this._settings.viewType = newType;
+ const newField = TemplateField.CreateField(this._settings, this._id, this._parent, true);
+ this._parent?.exchangeFields(newField, this);
+ return newField;
+ };
+
+ matches = (cols: Col[]): number[] => {
+ const colMatchesField = (col: Col) => (this._settings?.sizes?.some(size => col.sizes?.includes(size)) && this._settings.types?.includes(col.type)) ?? false;
+
+ const matches: Array<number> = [];
+
+ cols.forEach((col, v) => {
+ if (colMatchesField(col)) {
+ matches.push(v);
+ }
+ });
+
+ return matches;
+ };
+
+ private getLocalDimensions = (coords: { tl: [number, number]; br: [number, number] }, parentDimensions?: FieldDimensions): FieldDimensions => {
+ if (!parentDimensions) {
+ return { width: coords.br[0] - coords.tl[0], height: coords.br[1] - coords.tl[1], coord: { x: coords.tl[0], y: coords.tl[1] } };
+ }
+ const l = (coords.tl[0] * parentDimensions.width) / 2;
+ const t = coords.tl[1] * parentDimensions.height / 2; //prettier-ignore
+ const r = (coords.br[0] * parentDimensions.width) / 2;
+ const b = coords.br[1] * parentDimensions.height / 2; //prettier-ignore
+ return { width: r-l, height: b-t, coord: { x: l, y: t } }; //prettier-ignore
+ };
+
+ private applyBasicOpts = (dimensions: FieldDimensions, settings: FieldSettings | undefined) => {
+ const opts: DocumentOptions = settings?.opts ?? {};
+ opts.isDefaultTemplateDoc ??= true;
+ opts._layout_hideScroll ??= true;
+ opts.x ??= dimensions.coord.x;
+ opts.y ??= dimensions.coord.y;
+ opts._height ??= dimensions.height;
+ opts._width ??= dimensions.width;
+ opts._nativeWidth ??= dimensions.width;
+ opts._nativeHeight ??= dimensions.height;
+ opts._layout_nativeDimEditable ??= true;
+ opts.layout_boxShadow = 'none';
+ };
+}
+
+export type FieldSettings = {
+ tl: [number, number];
+ br: [number, number];
+ opts: DocumentOptions;
+ types?: TemplateFieldType[];
+ sizes?: TemplateFieldSize[];
+ title?: string;
+ viewType: ViewType;
+ template?: Template;
+ subfields?: FieldSettings[];
+ description?: string;
+};
+
+export enum ViewType {
+ CAROUSEL3D = 'carousel3d',
+ FREEFORM = 'freeform',
+ STATIC = 'static',
+ DEC = 'decoration',
+ IMG = 'image',
+ TEXT = 'text',
+ NONE = 'none',
+}
+
+export type FieldDimensions = {
+ width: number;
+ height: number;
+ coord: { x: number; y: number };
+};
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/TemplateFieldUtils.ts b/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/TemplateFieldUtils.ts
new file mode 100644
index 000000000..b0b531b57
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateFieldTypes/TemplateFieldUtils.ts
@@ -0,0 +1,71 @@
+import { DecorationField } from './DecorationField';
+import { DynamicField } from './DynamicField';
+import { ImageTemplateField, TextTemplateField } from './StaticContentField';
+import { FieldSettings, TemplateField, ViewType } from './TemplateField';
+
+export class TemplateFieldUtils {
+ /**
+ * Creates and initializes a new TemplateField based on the settings and parameters
+ *
+ * implements Field.initField ... see main.tsx
+ *
+ * @param settings - specification of the field type and other parameters
+ * @param index -
+ * @param parent - optional TemplateField that contains the new field
+ * @param sameId -
+ * @returns TemplateField
+ */
+ public static CreateField = (settings: FieldSettings, index: number, parent?: TemplateField, sameId: boolean = false): TemplateField =>
+ ((...args) => {
+ switch (settings?.viewType) {
+ case ViewType.FREEFORM:
+ case ViewType.CAROUSEL3D: return new DynamicField(...args).initRenderDoc(settings);
+ case ViewType.IMG: return new ImageTemplateField(...args).initRenderDoc(settings);
+ case ViewType.TEXT: return new TextTemplateField(...args).initRenderDoc(settings);
+ case ViewType.DEC: return new DecorationField(...args).initRenderDoc(settings);
+ default: return new TextTemplateField(...args).initRenderDoc(settings);
+ } // prettier-ignore
+ })(settings, sameId ? index : parent ? Number(`${parent.getID}${index}`) : 1, parent);
+
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ public static calculateFontSize = (contWidth: number, contHeight: number, text: string, uppercase: boolean): number => {
+ const words: string[] = text.split(/\s+/).filter(Boolean);
+
+ let currFontSize = 1;
+ let rowsCount = 1;
+ let currTextHeight = currFontSize * rowsCount * 2;
+
+ while (currTextHeight <= contHeight) {
+ let wordIndex = 0;
+ let currentRowWidth = 0;
+ let wordsInCurrRow = 0;
+ rowsCount = 1;
+
+ while (wordIndex < words.length) {
+ const word = words[wordIndex];
+ const wordWidth = word.length * currFontSize * 0.7;
+
+ if (currentRowWidth + wordWidth <= contWidth) {
+ currentRowWidth += wordWidth;
+ ++wordsInCurrRow;
+ } else {
+ if (words.length !== 1 && words.length > wordsInCurrRow) {
+ rowsCount++;
+ currentRowWidth = wordWidth;
+ wordsInCurrRow = 1;
+ } else {
+ break;
+ }
+ }
+
+ wordIndex++;
+ }
+
+ currTextHeight = rowsCount * currFontSize * 2;
+
+ currFontSize += 1;
+ }
+
+ return currFontSize - 1;
+ };
+}
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateManager.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateManager.tsx
deleted file mode 100644
index 50ae4d72a..000000000
--- a/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateManager.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import { Col } from "./DocCreatorMenu";
-import { FieldSettings } from "./FieldTypes/Field";
-import { Template } from "./Template";
-
-export class TemplateManager {
-
- templates: Template[] = [];
-
- constructor(templateSettings: FieldSettings[]) {
- this.templates = this.initializeTemplates(templateSettings);
- }
-
- initializeTemplates = (templateSettings: FieldSettings[]): Template[] => {
- const initializedTemplates: Template[] = [];
- templateSettings.forEach(settings => initializedTemplates.push(new Template(settings)));
- return initializedTemplates;
- }
-
- getValidTemplates = (cols: Col[]): Template[] => {
- return this.templates.filter(template => template.isValidTemplate(cols));
- }
-} \ No newline at end of file
diff --git a/src/client/views/nodes/DataVizBox/SchemaCSVPopUp.scss b/src/client/views/nodes/DataVizBox/SchemaCSVPopUp.scss
index 63a693918..0acc2c847 100644
--- a/src/client/views/nodes/DataVizBox/SchemaCSVPopUp.scss
+++ b/src/client/views/nodes/DataVizBox/SchemaCSVPopUp.scss
@@ -13,7 +13,7 @@ $highlightedText: #82e0ff;
min-height: 200px;
border-radius: 15px;
padding: 15px;
- padding-bottom: 0;
+ padding-bottom: 0px;
z-index: 999;
display: flex;
flex-direction: column;
@@ -40,7 +40,7 @@ $highlightedText: #82e0ff;
font-size: 12px;
font-weight: 400;
letter-spacing: 1px;
- margin: 0;
+ margin: 0px;
padding-right: 5px;
}
@@ -124,8 +124,8 @@ $highlightedText: #82e0ff;
.img-container::after {
content: '';
position: absolute;
- top: 0;
- left: 0;
+ top: 0px;
+ left: 0px;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
diff --git a/src/client/views/nodes/DataVizBox/components/Chart.scss b/src/client/views/nodes/DataVizBox/components/Chart.scss
index ff1fa343d..a22e1153c 100644
--- a/src/client/views/nodes/DataVizBox/components/Chart.scss
+++ b/src/client/views/nodes/DataVizBox/components/Chart.scss
@@ -91,7 +91,7 @@
margin: 5px;
margin-left: 25px;
margin-right: 10px;
- margin-bottom: 0;
+ margin-bottom: 0px;
.tableBox-table {
height: 100%;
width: 100%;
@@ -101,7 +101,7 @@
text-overflow: ellipsis;
width: 100%;
white-space: pre;
- max-width: 150;
+ max-width: 150px;
overflow: hidden;
margin-left: 2px;
}
diff --git a/src/client/views/nodes/DataVizBox/components/Histogram.tsx b/src/client/views/nodes/DataVizBox/components/Histogram.tsx
index a7c4a00b0..f51683991 100644
--- a/src/client/views/nodes/DataVizBox/components/Histogram.tsx
+++ b/src/client/views/nodes/DataVizBox/components/Histogram.tsx
@@ -413,6 +413,10 @@ export class Histogram extends ObservableReactComponent<HistogramProps> {
});
};
+ setChartRef = (r: HTMLDivElement | null) => {
+ this._histogramRef = r;
+ r && this.drawChart(this._histogramData, this.width, this.height);
+ };
render() {
if (!this.selectedBins) this.layoutDoc.dataViz_histogram_selectedBins = new List<string>();
@@ -446,12 +450,7 @@ export class Histogram extends ObservableReactComponent<HistogramProps> {
size={Size.XSMALL}
/>
</div>
- <div
- ref={r => {
- this._histogramRef = r;
- r && this.drawChart(this._histogramData, this.width, this.height);
- }}
- />
+ <div ref={this.setChartRef} />
{selected !== 'none' ? (
<div className="selected-data">
Selected: {selected}
diff --git a/src/client/views/nodes/DataVizBox/components/LineChart.tsx b/src/client/views/nodes/DataVizBox/components/LineChart.tsx
index 80fadf178..732681e05 100644
--- a/src/client/views/nodes/DataVizBox/components/LineChart.tsx
+++ b/src/client/views/nodes/DataVizBox/components/LineChart.tsx
@@ -347,6 +347,10 @@ export class LineChart extends ObservableReactComponent<LineChartProps> {
.style('pointer-events', 'none')
.style('transform', `translate(${xScale(d0.x) - this.width}px,${yScale(d0.y)}px)`);
}
+ setLineRef = (r: HTMLDivElement | null) => {
+ this._lineChartRef = r;
+ this.drawChart([this._lineChartData], this.rangeVals, this.width, this.height);
+ };
render() {
const selectedPt = this._currSelected ? `{ ${this._props.axes[0]}: ${this._currSelected.x} ${this._props.axes[1]}: ${this._currSelected.y} }` : 'none';
@@ -378,12 +382,7 @@ export class LineChart extends ObservableReactComponent<LineChartProps> {
fillWidth
/>
</div>
- <div
- ref={r => {
- this._lineChartRef = r;
- this.drawChart([this._lineChartData], this.rangeVals, this.width, this.height);
- }}
- />
+ <div ref={this.setLineRef} />
{selectedPt !== 'none' ? (
<div className="selected-data">
{`Selected: ${selectedPt}`}
diff --git a/src/client/views/nodes/DataVizBox/components/PieChart.tsx b/src/client/views/nodes/DataVizBox/components/PieChart.tsx
index 0ae70786f..cf476b8d0 100644
--- a/src/client/views/nodes/DataVizBox/components/PieChart.tsx
+++ b/src/client/views/nodes/DataVizBox/components/PieChart.tsx
@@ -373,10 +373,10 @@ export class PieChart extends ObservableReactComponent<PieChartProps> {
const sliceName = StrCast(sliceTitle) ? StrCast(sliceTitle).replace(/\$/g, '').replace(/%/g, '').replace(/#/g, '').replace(/</g, '') : sliceTitle;
const sliceColors = Cast(this._props.layoutDoc.dataViz_pie_sliceColors, listSpec('string'), null);
- sliceColors.forEach(each => {
+ sliceColors?.forEach(each => {
if (each.split('::')[0] === sliceName) sliceColors.splice(sliceColors.indexOf(each), 1);
});
- sliceColors.push(StrCast(sliceName + '::' + color));
+ sliceColors?.push(StrCast(sliceName + '::' + color));
};
@action changeHistogramCheckBox = () => {
@@ -384,6 +384,10 @@ export class PieChart extends ObservableReactComponent<PieChartProps> {
this.drawChart(this._pieChartData, this.width, this.height);
};
+ setChartRef = (r: HTMLDivElement | null) => {
+ this._piechartRef = r;
+ this.drawChart(this._pieChartData, this.width, this.height);
+ };
render() {
let titleAccessor = 'dataViz_pie_title';
if (this._props.axes.length === 2) titleAccessor = titleAccessor + this._props.axes[0] + '-' + this._props.axes[1];
@@ -443,12 +447,7 @@ export class PieChart extends ObservableReactComponent<PieChartProps> {
Organize data as histogram
</div>
) : null}
- <div
- ref={r => {
- this._piechartRef = r;
- this.drawChart(this._pieChartData, this.width, this.height);
- }}
- />
+ <div ref={this.setChartRef} />
{selected !== 'none' ? (
<div className="selected-data">
Selected: {selected}
diff --git a/src/client/views/nodes/DataVizBox/components/TableBox.tsx b/src/client/views/nodes/DataVizBox/components/TableBox.tsx
index ad2731109..cc08cf269 100644
--- a/src/client/views/nodes/DataVizBox/components/TableBox.tsx
+++ b/src/client/views/nodes/DataVizBox/components/TableBox.tsx
@@ -100,7 +100,7 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> {
return this._props.docView?.()?.screenToViewTransform().Scale || 1;
}
@computed get rowHeight() {
- return (this.viewScale * this._tableHeight) / this._tableDataIds.length;
+ return (this.viewScale * this._tableHeight) / (this._tableDataIds.length + 1); // add 1 for header row
}
@computed get startID() {
return this.rowHeight ? Math.max(Math.floor(this._scrollTop / this.rowHeight) - 1, 0) : 0;
@@ -400,12 +400,12 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> {
this._tableHeight = r?.getBoundingClientRect().height ?? 0;
}
})}>
- <div style={{ height: this.startID * Number(DATA_VIZ_TABLE_ROW_HEIGHT) }} />
<thead>
+ <tr style={{ height: this.startID * Number(DATA_VIZ_TABLE_ROW_HEIGHT.replace("px","")) }} />
<tr>
- {this.columns.map(col => (
+ {this.columns.map((col, i) => (
<th
- key={this.columns.indexOf(col)}
+ key={i}
style={{
color:
this._props.axes.slice().reverse().lastElement() === col
@@ -440,7 +440,7 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> {
<tbody>
{this._tableDataIds
.filter((rowId, i) => this.startID - 2 <= i && i <= this.endID + 2)
- ?.map(rowId => (
+ .map(rowId => (
<tr
key={rowId}
className={`tableBox-row ${this.columns[0]}`}
@@ -456,22 +456,22 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> {
: '',
border: rowId === this._props.specHighlightedRow ? `solid 3px ${Colors.MEDIUM_BLUE}` : '',
}}>
- {this.columns.map(col => {
+ {this.columns.map((col, i) => {
let colSelected = false;
if (this._props.axes.length > 2) colSelected = this._props.axes[0] === col || this._props.axes[1] === col || this._props.axes[2] === col;
else if (this._props.axes.length > 1) colSelected = this._props.axes[0] === col || this._props.axes[1] === col;
else if (this._props.axes.length > 0) colSelected = this._props.axes[0] === col;
if (this._props.titleCol === col) colSelected = true;
return (
- <td key={this.columns.indexOf(col)} style={{ border: colSelected ? '3px solid black' : '1px solid black', fontWeight: colSelected ? 'bolder' : 'normal' }}>
+ <td key={i} style={{ border: (colSelected ? '3' : '1') + 'px solid black', fontWeight: colSelected ? 'bolder' : 'normal' }}>
<div className="tableBox-cell">{this._props.records[rowId][col] as string | number}</div>
</td>
);
})}
</tr>
))}
+ <tr style={{ display: this._tableDataIds.length - this.endID ? undefined : 'none', height: (this._tableDataIds.length - this.endID) * Number(DATA_VIZ_TABLE_ROW_HEIGHT.replace("px","")) }} />
</tbody>
- <div style={{ height: (this._tableDataIds.length - this.endID) * Number(DATA_VIZ_TABLE_ROW_HEIGHT) }} />
</table>
</div>
</div>
diff --git a/src/client/views/nodes/DiagramBox.tsx b/src/client/views/nodes/DiagramBox.tsx
index 7cfccf0dc..6a31f64ce 100644
--- a/src/client/views/nodes/DiagramBox.tsx
+++ b/src/client/views/nodes/DiagramBox.tsx
@@ -185,6 +185,8 @@ export class DiagramBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
return '( )';
};
+ setRef = (r: HTMLDivElement | null) => this.fixWheelEvents(r, this._props.isContentActive);
+ setDiagramBoxRef = (r: HTMLDivElement | null) => r && this.renderMermaidAsync.call(this, this.removeWords(this.mermaidcode), r);
render() {
return (
<div
@@ -192,7 +194,7 @@ export class DiagramBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
style={{
pointerEvents: this._props.isContentActive() ? undefined : 'none',
}}
- ref={r => this.fixWheelEvents(r, this._props.isContentActive)}>
+ ref={this.setRef}>
<div className="DIYNodeBox-searchbar">
<input type="text" value={this._inputValue} onKeyDown={action(e => e.key === 'Enter' && this.generateMermaidCode())} onChange={action(e => (this._inputValue = e.target.value))} />
<button type="button" onClick={this.generateMermaidCode}>
@@ -208,7 +210,7 @@ export class DiagramBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
) : this._generating ? (
<div className="loading-circle" />
) : (
- <div className="diagramBox" ref={r => r && this.renderMermaidAsync.call(this, this.removeWords(this.mermaidcode), r)}>
+ <div className="diagramBox" ref={this.setDiagramBoxRef}>
{this._errorMessage || 'Type a prompt to generate a diagram'}
</div>
)}
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index 504c1491e..32741a0fe 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -261,9 +261,7 @@ export class DocumentContentsView extends ObservableReactComponent<DocumentConte
jsx={layoutFrame}
showWarnings
// eslint-disable-next-line @typescript-eslint/no-explicit-any
- onError={(test: any) => {
- console.log('DocumentContentsView:' + test, bindings, layoutFrame);
- }}
+ onError={(test: any) => console.log('DocumentContentsView:' + test, bindings, layoutFrame)}
/>
);
}
diff --git a/src/client/views/nodes/DocumentLinksButton.scss b/src/client/views/nodes/DocumentLinksButton.scss
index e1b83dc59..43b1e083f 100644
--- a/src/client/views/nodes/DocumentLinksButton.scss
+++ b/src/client/views/nodes/DocumentLinksButton.scss
@@ -17,8 +17,8 @@
}
.documentLinksButton-cont {
- min-width: 20;
- min-height: 20;
+ min-width: 20px;
+ min-height: 20px;
position: absolute;
}
diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx
index c35a329c9..d30f00829 100644
--- a/src/client/views/nodes/DocumentLinksButton.tsx
+++ b/src/client/views/nodes/DocumentLinksButton.tsx
@@ -38,7 +38,6 @@ export class DocButtonState {
// eslint-disable-next-line no-use-before-define
public static _instance: DocButtonState | undefined;
public static get Instance() {
- // eslint-disable-next-line no-return-assign
return DocButtonState._instance ?? (DocButtonState._instance = new DocButtonState());
}
constructor() {
diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss
index c4351a200..98ca76339 100644
--- a/src/client/views/nodes/DocumentView.scss
+++ b/src/client/views/nodes/DocumentView.scss
@@ -1,6 +1,7 @@
@use '../global/globalCssVariables.module.scss' as global;
.documentView-effectsWrapper {
+ height: 100%;
border-radius: inherit;
transition: inherit;
}
@@ -14,13 +15,13 @@
width: 100%;
height: 100%;
position: absolute;
- top: 0;
+ top: 0px;
}
.documentView-node {
position: inherit;
- top: 0;
- left: 0;
+ top: 0px;
+ left: 0px;
width: 100%;
height: 100%;
border-radius: inherit;
@@ -55,7 +56,7 @@
.documentView-htmlOverlay {
position: absolute;
display: flex;
- top: 0;
+ top: 0px;
height: 100%;
width: 100%;
.documentView-htmlOverlayInner {
@@ -79,9 +80,9 @@
.documentView-audioBackground {
display: inline-block;
width: 25px;
- height: 25;
+ height: 25px;
position: absolute;
- top: 0;
+ top: 0px;
left: 50%;
border-radius: 25px;
background: white;
@@ -130,7 +131,7 @@
width: 30px;
border-radius: 50%;
position: absolute;
- right: -15;
+ right: -15px;
opacity: 0.9;
pointer-events: auto;
background-color: #9dca96;
@@ -147,8 +148,8 @@
.documentView-anchorCont {
position: absolute;
- top: 0;
- left: 0;
+ top: 0px;
+ left: 0px;
width: 100%;
height: 100%;
display: inline-block;
@@ -160,8 +161,8 @@
position: absolute;
width: 100%;
height: 100%;
- top: 0;
- left: 0;
+ top: 0px;
+ left: 0px;
}
.documentView-styleWrapper {
@@ -183,9 +184,9 @@
.documentView-titleWrapper-hover {
color: global.$black;
transform-origin: top left;
- top: 0;
+ top: 0px;
width: 100%;
- height: 14;
+ height: 14px;
opacity: 0.5;
text-align: center;
text-overflow: ellipsis;
@@ -211,7 +212,7 @@
.documentView-captionWrapper {
position: absolute;
- bottom: 0;
+ bottom: 0px;
width: 100%;
overflow-y: auto;
transform-origin: bottom left;
@@ -275,20 +276,20 @@
.documentView-noAIWidgets {
transform-origin: top left;
position: absolute;
- bottom: 0;
+ bottom: 0px;
pointer-events: none;
}
.documentView-widgetDecorations {
transform-origin: top right;
position: absolute;
- top: 0;
- right: 0;
+ top: 0px;
+ right: 0px;
}
.documentView-editorView-history {
position: absolute;
transform-origin: top right;
- right: 0;
+ right: 0px;
top: 0;
overflow-y: scroll;
scrollbar-width: thin;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 05706fe6b..bd71115db 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -701,16 +701,18 @@ export class DocumentViewInternal extends DocComponent<DocumentViewProps & Field
aiContentsWidth = () => (this.aiContentsHeight() * (this._props.NativeWidth?.() || 1)) / (this._props.NativeHeight?.() || 1);
aiContentsHeight = () => Math.max(10, this._props.PanelHeight() - (this._aiWinHeight + (this.tagsOverlayFunc() ? 22 : 0)) * this.uiBtnScaling);
+ setAiRef = action((r: HTMLDivElement | null) => this.historyRef(this._oldHistoryWheel, (this._oldHistoryWheel = r)));
+
@computed get aiEditor() {
return (
<div
className="documentView-editorView"
+ ref={this.setAiRef}
style={{
background: SnappingManager.userVariantColor,
width: `${100 / this.uiBtnScaling}%`, //
transform: `scale(${this.uiBtnScaling})`,
- }}
- ref={r => this.historyRef(this._oldHistoryWheel, (this._oldHistoryWheel = r))}>
+ }}>
<div className="documentView-editorView-resizer" />
{this._componentView?.componentAIView?.() ?? null}
{this._props.DocumentView?.() ? <TagsView background={this.backgroundBoxColor} Views={[this._props.DocumentView?.()]} /> : null}
@@ -720,7 +722,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewProps & Field
@computed get tagsOverlay() {
return (
<div
- className="documentView-noAiWidgets"
+ className="documentView-noAIWidgets"
style={{
width: `${100 / this.uiBtnScaling}%`, //
transform: `scale(${this.uiBtnScaling})`,
@@ -744,7 +746,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewProps & Field
@computed get viewBoxContents() {
TraceMobx();
const isInk = this.layoutDoc._layout_isSvg && !this._props.LayoutTemplateString;
- const noBackground = this.Document.isGroup && !this._componentView?.isUnstyledView?.() && (!this.layoutDoc.backgroundColor || this.layoutDoc.backgroundColor === 'transparent');
+ const noBackground = Doc.IsFreeformGroup(this.Document) && !this._componentView?.isUnstyledView?.() && (!this.layoutDoc.backgroundColor || this.layoutDoc.backgroundColor === 'transparent');
return (
<>
<div
@@ -1412,7 +1414,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
}
public static setDefaultImageTemplate(checkResult?: boolean) {
if (checkResult) return Doc.UserDoc().defaultImageLayout;
- const view = DocumentView.Selected()[0]?._props.renderDepth > 0 ? DocumentView.Selected()[0] : undefined;
+ const view = DocumentView.Selected()[0]?._props.renderDepth > 0 || DocumentView.Selected()[0]?.Document.isTemplateDoc ? DocumentView.Selected()[0] : undefined;
undoable(() => {
const tempDoc = DocumentView.getTemplate(view);
Doc.UserDoc().defaultImageLayout = tempDoc ? new PrefetchProxy(tempDoc) : undefined;
@@ -1507,18 +1509,16 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
.translate(-(this._docViewInternal?.aiShift() ?? 0), 0)
.scale((this._docViewInternal?.aiScale() ?? 1) / this.nativeScaling);
+ setHtmlOverlayRef = (r: HTMLDivElement | null) => {
+ const val = r?.style.display !== 'none'; // if the outer overlay has been displayed, trigger the innner div to start it's opacity fade in transition
+ if (r && val !== this._enableHtmlOverlayTransitions) {
+ setTimeout(action(() => (this._enableHtmlOverlayTransitions = val)));
+ }
+ };
htmlOverlay = () => {
const effect = StrCast(this._htmlOverlayEffect?.presentation_effect, StrCast(this._htmlOverlayEffect?.followLinkAnimEffect));
return (
- <div
- className="documentView-htmlOverlay"
- ref={r => {
- const val = r?.style.display !== 'none'; // if the outer overlay has been displayed, trigger the innner div to start it's opacity fade in transition
- if (r && val !== this._enableHtmlOverlayTransitions) {
- setTimeout(action(() => (this._enableHtmlOverlayTransitions = val)));
- }
- }}
- style={{ display: !this._htmlOverlayText ? 'none' : undefined }}>
+ <div className="documentView-htmlOverlay" ref={this.setHtmlOverlayRef} style={{ display: !this._htmlOverlayText ? 'none' : undefined }}>
<div className="documentView-htmlOverlayInner" style={{ transition: `all 500ms`, opacity: this._enableHtmlOverlayTransitions ? 0.9 : 0 }}>
{DocumentViewInternal.AnimationEffect(
<div className="webBox-textHighlight">
diff --git a/src/client/views/nodes/EquationBox.tsx b/src/client/views/nodes/EquationBox.tsx
index 3cacb6692..2ce24b688 100644
--- a/src/client/views/nodes/EquationBox.tsx
+++ b/src/client/views/nodes/EquationBox.tsx
@@ -6,7 +6,7 @@ import { TraceMobx } from '../../../fields/util';
import { DocUtils } from '../../documents/DocUtils';
import { DocumentType } from '../../documents/DocumentTypes';
import { Docs } from '../../documents/Documents';
-import { undoBatch } from '../../util/UndoManager';
+import { undoBatch, UndoManager } from '../../util/UndoManager';
import { ViewBoxBaseComponent } from '../DocComponent';
import { StyleProp } from '../StyleProp';
import { DocumentView } from './DocumentView';
@@ -14,6 +14,7 @@ import './EquationBox.scss';
import { FieldView, FieldViewProps } from './FieldView';
import EquationEditor from './formattedText/EquationEditor';
import { FormattedTextBox } from './formattedText/FormattedTextBox';
+import { Doc } from '../../../fields/Doc';
@observer
export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() {
@@ -21,6 +22,7 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() {
return FieldView.LayoutString(EquationBox, fieldKey);
}
_ref: React.RefObject<EquationEditor> = React.createRef();
+ _liveTextUndo: UndoManager.Batch | undefined; // captured undo batch when typing a new text note into a collection
constructor(props: FieldViewProps) {
super(props);
@@ -29,12 +31,17 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() {
componentDidMount() {
this._props.setContentViewBox?.(this);
- if (DocumentView.SelectOnLoad === this.Document && (!DocumentView.LightboxDoc() || DocumentView.LightboxContains(this.DocumentView?.()))) {
+ if (DocumentView.SelectOnLoad === this.rootDoc && (!DocumentView.LightboxDoc() || DocumentView.LightboxContains(this.DocumentView?.()))) {
+ this._liveTextUndo = FormattedTextBox.LiveTextUndo;
+ FormattedTextBox.LiveTextUndo = undefined;
this._props.select(false);
+ this.dataDoc[Doc.LayoutDataKey(this.Document)] = FormattedTextBox.SelectOnLoadChar ?? '';
+
this._ref.current?.mathField.focus();
- this.dataDoc.text === 'x' && this._ref.current?.mathField.select();
+ this.dataDoc[Doc.LayoutDataKey(this.Document)] === 'x' && this._ref.current?.mathField.select();
DocumentView.SetSelectOnLoad(undefined);
+ FormattedTextBox.SelectOnLoadChar = '';
}
reaction(
() => this._props.isSelected(),
@@ -53,7 +60,7 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() {
@action
keyPressed = (e: KeyboardEvent) => {
if (e.key === 'Enter') {
- const nextEq = Docs.Create.EquationDocument(e.shiftKey ? StrCast(this.dataDoc.text) : '', {
+ const nextEq = Docs.Create.EquationDocument(e.shiftKey ? StrCast(this.dataDoc[Doc.LayoutDataKey(this.Document)]) : '', {
title: '# math',
_width: NumCast(this.layoutDoc._width),
_height: NumCast(this.layoutDoc._height),
@@ -70,9 +77,10 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() {
e.stopPropagation();
}
if (e.key === 'Tab') {
+ const target = this.Document.isTemplateDoc ? this.rootDoc : this.Document;
const graph = Docs.Create.FunctionPlotDocument([this.Document], {
- x: NumCast(this.layoutDoc.x) + NumCast(this.layoutDoc._width),
- y: NumCast(this.layoutDoc.y),
+ x: NumCast(target.x) + NumCast(this.layoutDoc._width),
+ y: NumCast(target.y),
_width: 400,
_height: 300,
backgroundColor: 'white',
@@ -82,11 +90,11 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() {
link && this._props.addDocument?.(link);
e.stopPropagation();
}
- if (e.key === 'Backspace' && !this.dataDoc.text) this._props.removeDocument?.(this.Document);
+ if (e.key === 'Backspace' && !this.dataDoc[Doc.LayoutDataKey(this.Document)]) this._props.removeDocument?.(this.Document);
};
@undoBatch
onChange = (str: string) => {
- this.dataDoc.text = str;
+ this.dataDoc[Doc.LayoutDataKey(this.Document)] = str;
};
updateSize = (mathSpan: HTMLSpanElement) => {
@@ -103,19 +111,22 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() {
this.layoutDoc._width = mathWidth * nScale;
this.layoutDoc._height = mathHeight * nScale;
+ if (this.layoutDoc._nativeWidth) {
+ this.layoutDoc._nativeWidth = mathWidth;
+ this.layoutDoc._nativeHeight = mathHeight;
+ }
};
+ setRef = (r: HTMLDivElement) => r && this._ref.current?.element.current && this.updateSize(this._ref.current?.element.current);
render() {
TraceMobx();
const scale = this._props.NativeDimScaling?.() || 1;
return (
<div
- ref={r => r && this._ref.current?.element.current && this.updateSize(this._ref.current?.element.current)}
+ ref={this.setRef}
className="equationBox-cont"
onKeyDown={e => e.stopPropagation()}
onPointerDown={e => !e.ctrlKey && e.stopPropagation()}
- onBlur={() => {
- FormattedTextBox.LiveTextUndo?.end();
- }}
+ onBlur={() => this._liveTextUndo?.end()}
style={{
transform: `scale(${scale})`,
minWidth: `${100 / scale}%`,
@@ -128,7 +139,14 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() {
paddingTop: NumCast(this.layoutDoc.yMargin),
paddingBottom: NumCast(this.layoutDoc.yMargin),
}}>
- <EquationEditor ref={this._ref} value={StrCast(this.dataDoc.text, '')} spaceBehavesLikeTab onChange={this.onChange} autoCommands="pi theta sqrt sum prod alpha beta gamma rho" autoOperatorNames="sin cos tan" />
+ <EquationEditor
+ ref={this._ref}
+ value={StrCast(this.dataDoc[Doc.LayoutDataKey(this.Document)], '')}
+ spaceBehavesLikeTab
+ onChange={this.onChange}
+ autoCommands="pi theta sqrt sum prod alpha beta gamma rho"
+ autoOperatorNames="sin cos tan"
+ />
</div>
);
}
diff --git a/src/client/views/nodes/FontIconBox/FontIconBadge.scss b/src/client/views/nodes/FontIconBox/FontIconBadge.scss
index 2ff5c651f..e741936db 100644
--- a/src/client/views/nodes/FontIconBox/FontIconBadge.scss
+++ b/src/client/views/nodes/FontIconBox/FontIconBadge.scss
@@ -6,7 +6,7 @@
color: black;
display: block;
position: absolute;
- right: 5;
+ right: 5px;
border-radius: 50%;
text-align: center;
}
diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.scss b/src/client/views/nodes/FontIconBox/FontIconBox.scss
index 8bc68c131..52eebba54 100644
--- a/src/client/views/nodes/FontIconBox/FontIconBox.scss
+++ b/src/client/views/nodes/FontIconBox/FontIconBox.scss
@@ -42,7 +42,7 @@
letter-spacing: normal;
background-color: inherit;
border-radius: 8px;
- padding: 0;
+ padding: 0px;
width: 100%;
font-family: 'system-ui';
text-transform: uppercase;
@@ -96,22 +96,22 @@
display: inline-block;
width: 100%;
height: 25px;
- margin: 0;
+ margin: 0px;
}
.switch input {
opacity: 0;
- width: 0;
- height: 0;
+ width: 0px;
+ height: 0px;
}
.slider {
position: absolute;
cursor: pointer;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
+ top: 0px;
+ left: 0px;
+ right: 0px;
+ bottom: 0px;
background-color: lightgrey;
-webkit-transition: 0.4s;
transition: 0.4s;
@@ -223,7 +223,7 @@
height: fit-content;
color: black;
top: 100%;
- left: 0;
+ left: 0px;
z-index: 21;
background-color: #e3e3e3;
box-shadow: 0px 3px 4px rgba(0, 0, 0, 0.3);
@@ -430,7 +430,7 @@
border-radius: 0px 7px 7px 0px;
width: 13px;
height: 100%;
- right: 0;
+ right: 0px;
}
.menuButton-dropdown-header {
diff --git a/src/client/views/nodes/FunctionPlotBox.tsx b/src/client/views/nodes/FunctionPlotBox.tsx
index 8e4b64851..e4d37e006 100644
--- a/src/client/views/nodes/FunctionPlotBox.tsx
+++ b/src/client/views/nodes/FunctionPlotBox.tsx
@@ -121,11 +121,12 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps>
}
// if (this.layout_autoHeight) this.tryUpdateScrollHeight();
};
+ setRef = (r: HTMLDivElement | null) => r && this.createGraph(r);
@computed get theGraph() {
return (
<div
id={`${this._plotId}`}
- ref={r => r && this.createGraph(r)}
+ ref={this.setRef}
style={{ position: 'absolute', width: '100%', height: '100%' }}
onPointerDown={e => {
e.stopPropagation();
diff --git a/src/client/views/nodes/IconTagBox.scss b/src/client/views/nodes/IconTagBox.scss
index d6cf95958..c0977dfc5 100644
--- a/src/client/views/nodes/IconTagBox.scss
+++ b/src/client/views/nodes/IconTagBox.scss
@@ -15,7 +15,7 @@
width: 20px;
height: 20px;
margin: auto;
- padding: 0;
+ padding: 0px;
border-radius: 50%;
background-color: global.$dark-gray;
background-color: transparent;
diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss
index 5a6292fab..90ede69dc 100644
--- a/src/client/views/nodes/ImageBox.scss
+++ b/src/client/views/nodes/ImageBox.scss
@@ -3,14 +3,14 @@
width: 100%;
height: 100%;
position: absolute;
- top: 0;
- left: 0;
+ top: 0px;
+ left: 0px;
transform-origin: top left;
.imageBox-annotationLayer {
position: absolute;
transform-origin: left top;
- top: 0;
+ top: 0px;
width: 100%;
pointer-events: none;
mix-blend-mode: multiply; // bcz: makes text fuzzy!
@@ -24,8 +24,8 @@
#upload-icon {
position: absolute;
- bottom: 0;
- right: 0;
+ bottom: 0px;
+ right: 0px;
width: 20px;
height: 20px;
}
@@ -51,8 +51,8 @@
.imageBox-dot {
position: absolute;
- bottom: 10;
- left: 0;
+ bottom: 10px;
+ left: 0px;
border-radius: 10px;
width: 20px;
height: 20px;
@@ -131,8 +131,8 @@
position: absolute;
color: white;
background: black;
- right: 0;
- bottom: 0;
+ right: 0px;
+ bottom: 0px;
z-index: 2;
transform-origin: bottom right;
cursor: default;
@@ -142,13 +142,13 @@
}
}
.imageBox-regenerateDropTarget {
- right: 35;
+ right: 35px;
transform-origin: 70px 35px;
}
.imageBox-fader img {
position: absolute;
- left: 0;
+ left: 0px;
}
.imageBox-fadeBlocker-hover {
@@ -223,7 +223,7 @@
max-width: 90%;
width: 100%;
.imageBox-aiView-similarity {
- max-width: 65;
+ max-width: 65px;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
@@ -250,7 +250,7 @@
z-index: 10000;
h3 {
- margin-top: 0;
+ margin-top: 0px;
}
input {
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index f7ad5c7e2..78bacdcac 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -8,7 +8,7 @@ import { extname } from 'path';
import * as React from 'react';
import { AiOutlineSend } from 'react-icons/ai';
import ReactLoading from 'react-loading';
-import { ClientUtils, DashColor, returnEmptyString, returnFalse, returnOne, returnZero, setupMoveUpEvents, UpdateIcon } from '../../../ClientUtils';
+import { ClientUtils, imageUrlToBase64, DashColor, returnEmptyString, returnFalse, returnOne, returnZero, setupMoveUpEvents, UpdateIcon, returnTrue } from '../../../ClientUtils';
import { Doc, DocListCast, Opt } from '../../../fields/Doc';
import { DocData } from '../../../fields/DocSymbols';
import { Id } from '../../../fields/FieldSymbols';
@@ -16,7 +16,7 @@ import { InkTool } from '../../../fields/InkField';
import { List } from '../../../fields/List';
import { ObjectField } from '../../../fields/ObjectField';
import { ComputedField } from '../../../fields/ScriptField';
-import { Cast, DocCast, ImageCast, NumCast, RTFCast, StrCast } from '../../../fields/Types';
+import { Cast, DocCast, ImageCast, NumCast, RTFCast, StrCast, ImageCastWithSuffix } from '../../../fields/Types';
import { ImageField } from '../../../fields/URLField';
import { TraceMobx } from '../../../fields/util';
import { emptyFunction } from '../../../Utils';
@@ -45,6 +45,7 @@ import { FieldView, FieldViewProps } from './FieldView';
import { FocusViewOptions } from './FocusViewOptions';
import './ImageBox.scss';
import { OpenWhere } from './OpenWhere';
+import { gptImageLabel } from '../../apis/gpt/GPT';
const DefaultPath = '/assets/unknown-file-icon-hi.png';
export class ImageEditorData {
@@ -71,7 +72,7 @@ export class ImageEditorData {
public static set AddDoc(addDoc: Opt<(doc: Doc | Doc[], annotationKey?: string) => boolean>) { ImageEditorData.set(this.imageData.open, this.imageData.rootDoc, this.imageData.source, addDoc); } // prettier-ignore
}
-const API_URL = 'https://api.unsplash.com/search/photos';
+const UNSPLASH_API = 'https://api.unsplash.com/search/photos';
@observer
export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
public static LayoutString(fieldKey: string) {
@@ -112,11 +113,69 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
this._props.setContentViewBox?.(this);
}
+ @computed get outpaintOriginalSize(): { width: number; height: number } {
+ return {
+ width: NumCast(this.Document[this.fieldKey + '_outpaintOriginalWidth']),
+ height: NumCast(this.Document[this.fieldKey + '_outpaintOriginalHeight']),
+ };
+ }
+ set outpaintOriginalSize(prop: { width: number; height: number } | undefined) {
+ this.Document[this.fieldKey + '_outpaintOriginalWidth'] = prop?.width;
+ this.Document[this.fieldKey + '_outpaintOriginalHeight'] = prop?.height;
+ }
+
+ @computed get imgNativeSize() {
+ return {
+ nativeWidth: NumCast(this.dataDoc[this.fieldKey + '_nativeWidth'], NumCast(this.layoutDoc[this.fieldKey + '_nativeWidth'], 500)),
+ nativeHeight: NumCast(this.dataDoc[this.fieldKey + '_nativeHeight'], NumCast(this.layoutDoc[this.fieldKey + '_nativeHeight'], 500)),
+ };
+ }
+ set imgNativeSize(prop: { nativeWidth: number; nativeHeight: number }) {
+ this.dataDoc[this.fieldKey + '_nativeWidth'] = prop.nativeWidth;
+ this.dataDoc[this.fieldKey + '_nativeHeight'] = prop.nativeHeight;
+ }
+
protected createDropTarget = (ele: HTMLDivElement) => {
this._mainCont = ele;
this._dropDisposer?.();
ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.Document));
};
+
+ autoTag = async () => {
+ if (this.Document.$tags_chat) return;
+ try {
+ // 1) grab the full-size URL
+ const layoutKey = Doc.LayoutDataKey(this.Document);
+ const url = ImageCastWithSuffix(this.Document[layoutKey], '_o');
+ if (!url) throw new Error('No image URL found');
+
+ // 2) convert to base64
+ const base64 = await imageUrlToBase64(url);
+ if (!base64) throw new Error('Failed to load image data');
+
+ // 3) ask GPT for labels one label: PERSON or LANDSCAPE
+ const label = await gptImageLabel(
+ base64,
+ `Classify this image as PERSON or LANDSCAPE. You may only respond with one of these two options.
+ Then provide five additional descriptive tags to describe the image for a total of 6 words outputted, delimited by spaces.
+ For example: "LANDSCAPE BUNNY NATURE FOREST PEACEFUL OUTDOORS".
+ Then add one final lengthier summary tag (separated by underscores) that describes the image.`
+ ).then(raw => raw.trim().toUpperCase());
+
+ const { nativeWidth, nativeHeight } = this.nativeSize;
+ const aspectRatio = ((nativeWidth || 1) / (nativeHeight || 1)).toFixed(2);
+
+ // 5) stash it on the Doc
+ // overwrite any old tags so re-runs still work
+ this.Document.$tags_chat = new List<string>([...label.split(/\s+/), `ASPECT_${aspectRatio}`]);
+
+ // 6) flip on “show tags” in the layout
+ this.Document._layout_showTags = true;
+ } catch (err) {
+ console.error('autoTag failed:', err);
+ }
+ };
+
getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
const visibleAnchor = this._getAnchor?.(this._savedAnnotations, true); // use marquee anchor, otherwise, save zoom/pan as anchor
const anchor =
@@ -153,7 +212,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
() => ({ nativeSize: this.nativeSize, width: NumCast(this.layoutDoc._width), height: this.layoutDoc._height }),
({ nativeSize, width, height }) => {
if (!this.layoutDoc._layout_nativeDimEditable || !height || this.layoutDoc.layout_resetNativeDim) {
- this.layoutDoc.layout_resetNativeDim = undefined; // template images need to reset their dimensions when they are rendered with content. afterwards, remove this flag.
+ if (!this._props.TemplateDataDocument) this.layoutDoc._nativeWidth = this.layoutDoc._nativeHeight = undefined;
+ this.layoutDoc.layout_resetNativeDim = undefined; // reset dimensions of templates rendered with content or if image changes. afterwards, remove this flag.
this.layoutDoc._height = (width * nativeSize.nativeHeight) / nativeSize.nativeWidth;
}
},
@@ -170,7 +230,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
{ fireImmediately: true }
);
this._disposers.outpaint = reaction(
- () => this.Document[this.fieldKey + '_outpaintOriginalWidth'] !== undefined && !SnappingManager.ShiftKey,
+ () => this.outpaintOriginalSize?.width && !SnappingManager.ShiftKey,
complete => complete && this.openOutpaintPrompt(),
{ fireImmediately: true }
);
@@ -185,7 +245,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
*/
fetchImages = async () => {
try {
- const { data } = await axios.get(`${API_URL}?query=${this._searchInput}&page=1&per_page=${1}&client_id=${process.env.VITE_API_KEY}`);
+ const { data } = await axios.get(`${UNSPLASH_API}?query=${this._searchInput}&page=1&per_page=${1}&client_id=${process.env.VITE_API_KEY}`);
const imageSnapshot = Docs.Create.ImageDocument(data.results[0].urls.small, {
_nativeWidth: Doc.NativeWidth(this.layoutDoc),
_nativeHeight: Doc.NativeHeight(this.layoutDoc),
@@ -202,10 +262,6 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
}
};
- handleSelection = async (selection: string) => {
- this._searchInput = selection;
- };
-
drop = undoable(
action((e: Event, de: DragManager.DropEvent) => {
if (de.complete.docDragData && !this._props.rejectDrop?.(de, this.DocumentView?.())) {
@@ -231,14 +287,15 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
DrawingFillHandler.drawingToImage(this.Document, 90, newPrompt(descText), drag)?.then(action(() => (this._regenerateLoading = false)));
added = false;
} else if (de.altKey || !this.dataDoc[this.fieldKey]) {
- const layoutDoc = de.complete.docDragData?.draggedDocuments[0];
- const targetField = Doc.LayoutDataKey(layoutDoc);
- const targetDoc = layoutDoc[DocData];
- if (targetDoc[targetField] instanceof ImageField) {
+ const dropDoc = de.complete.docDragData?.draggedDocuments[0];
+ const dropDocFieldKey = Doc.LayoutDataKey(dropDoc);
+ const dropDataDoc = dropDoc[DocData];
+ if (dropDataDoc[dropDocFieldKey] instanceof ImageField) {
added = true;
- this.dataDoc[this.fieldKey] = ObjectField.MakeCopy(targetDoc[targetField] as ImageField);
- Doc.SetNativeWidth(this.dataDoc, Doc.NativeWidth(targetDoc), this.fieldKey);
- Doc.SetNativeHeight(this.dataDoc, Doc.NativeHeight(targetDoc), this.fieldKey);
+ this.dataDoc.layout_resetNativeDim = true;
+ this.dataDoc[this.fieldKey] = ObjectField.MakeCopy(dropDataDoc[dropDocFieldKey] as ImageField);
+ Doc.SetNativeWidth(this.dataDoc, Doc.NativeWidth(dropDataDoc), this.fieldKey);
+ Doc.SetNativeHeight(this.dataDoc, Doc.NativeHeight(dropDataDoc), this.fieldKey);
}
}
added === false && e.preventDefault();
@@ -257,18 +314,17 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
@undoBatch
setNativeSize = action(() => {
- const oldnativeWidth = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']);
+ const oldnativeWidth = this.imgNativeSize.nativeWidth;
const nscale = NumCast(this._props.PanelWidth()) * NumCast(this.layoutDoc._freeform_scale, 1);
const nw = nscale / oldnativeWidth;
- this.dataDoc[this.fieldKey + '_nativeHeight'] = NumCast(this.dataDoc[this.fieldKey + '_nativeHeight']) * nw;
- this.dataDoc[this.fieldKey + '_nativeWidth'] = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']) * nw;
+ this.imgNativeSize = { nativeWidth: this.imgNativeSize.nativeWidth * nw, nativeHeight: this.imgNativeSize.nativeHeight * nw };
this.dataDoc.freeform_panX = nw * NumCast(this.dataDoc.freeform_panX);
this.dataDoc.freeform_panY = nw * NumCast(this.dataDoc.freeform_panY);
this.dataDoc.freeform_panX_max = this.dataDoc.freeform_panX_max ? nw * NumCast(this.dataDoc.freeform_panX_max) : undefined;
this.dataDoc.freeform_panX_min = this.dataDoc.freeform_panX_min ? nw * NumCast(this.dataDoc.freeform_panX_min) : undefined;
this.dataDoc.freeform_panY_max = this.dataDoc.freeform_panY_max ? nw * NumCast(this.dataDoc.freeform_panY_max) : undefined;
this.dataDoc.freeform_panY_min = this.dataDoc.freeform_panY_min ? nw * NumCast(this.dataDoc.freeform_panY_min) : undefined;
- const newnativeWidth = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']);
+ const newnativeWidth = this.imgNativeSize.nativeWidth;
DocListCast(this.dataDoc[this.annotationKey]).forEach(doc => {
doc.x = (NumCast(doc.x) / oldnativeWidth) * newnativeWidth;
doc.y = (NumCast(doc.y) / oldnativeWidth) * newnativeWidth;
@@ -280,13 +336,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
});
@undoBatch
rotate = action(() => {
- const nw = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']);
- const nh = NumCast(this.dataDoc[this.fieldKey + '_nativeHeight']);
+ const nativeSize = this.imgNativeSize;
const w = this.layoutDoc._width;
const h = this.layoutDoc._height;
this.dataDoc[this.fieldKey + '_rotation'] = (NumCast(this.dataDoc[this.fieldKey + '_rotation']) + 90) % 360;
- this.dataDoc[this.fieldKey + '_nativeWidth'] = nh;
- this.dataDoc[this.fieldKey + '_nativeHeight'] = nw;
+ this.imgNativeSize = { nativeWidth: nativeSize.nativeHeight, nativeHeight: nativeSize.nativeWidth }; // swap width and height
this.layoutDoc._width = h;
this.layoutDoc._height = w;
});
@@ -302,7 +356,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
const anchy = NumCast(cropping.y);
const anchw = NumCast(cropping._width);
const anchh = NumCast(cropping._height);
- const viewScale = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']) / anchw;
+ const viewScale = this.nativeSize.nativeWidth / anchw;
cropping.x = NumCast(this.Document.x) + NumCast(this.layoutDoc._width);
cropping.y = NumCast(this.Document.y);
cropping.onClick = undefined;
@@ -364,18 +418,14 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
@action
cancelOutpaintPrompt = () => {
- const origWidth = NumCast(this.Document[this.fieldKey + '_outpaintOriginalWidth']);
- const origHeight = NumCast(this.Document[this.fieldKey + '_outpaintOriginalHeight']);
- this.Document._width = origWidth;
- this.Document._height = origHeight;
+ [this.Document._width, this.Document._height] = [this.outpaintOriginalSize.width, this.outpaintOriginalSize.height];
this._outpaintingInProgress = false;
+ this.outpaintOriginalSize = undefined;
this.closeOutpaintPrompt();
};
@action
- handlePromptChange = (val: string | number) => {
- this._outpaintPromptInput = '' + val;
- };
+ handlePromptChange = (val: string | number) => (this._outpaintPromptInput = '' + val);
@action
submitOutpaintPrompt = () => {
@@ -416,8 +466,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
loadingOverlay.innerHTML = '<div style="color: white; font-size: 16px;">Generating outpainted image...</div>';
this._mainCont?.appendChild(loadingOverlay);
- const origWidth = NumCast(this.Document[this.fieldKey + '_outpaintOriginalWidth']);
- const origHeight = NumCast(this.Document[this.fieldKey + '_outpaintOriginalHeight']);
+ const { width: origWidth, height: origHeight } = this.outpaintOriginalSize;
const response = await Networking.PostToServer('/outpaintImage', {
imageUrl: currentPath,
prompt: customPrompt,
@@ -454,8 +503,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
this.Document.$ai = true;
this.Document.$ai_outpainted = true;
this.Document.$ai_outpaint_prompt = customPrompt;
- this.Document[this.fieldKey + '_outpaintOriginalWidth'] = undefined;
- this.Document[this.fieldKey + '_outpaintOriginalHeight'] = undefined;
+ this.outpaintOriginalSize = undefined;
} else {
this.cancelOutpaintPrompt();
alert('Failed to receive a valid image URL from server.');
@@ -478,6 +526,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
return this._props.PanelWidth() / this._props.PanelHeight() < this.nativeSize.nativeWidth / this.nativeSize.nativeHeight;
}
+ isOutpaintable = () => true;
+
componentUI = (/* boundsLeft: number, boundsTop: number*/) =>
!this._showOutpaintPrompt ? null : (
<div
@@ -668,8 +718,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
@computed get nativeSize() {
TraceMobx();
if (this.paths.length && this.paths[0].includes(DefaultPath)) 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 { nativeWidth, nativeHeight } = this.imgNativeSize;
const nativeOrientation = NumCast(this.dataDoc[this.fieldKey + '_nativeOrientation'], 1);
return { nativeWidth, nativeHeight, nativeOrientation };
}
@@ -689,7 +738,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
@computed get overlayImageIcon() {
const usePath = this.layoutDoc[`_${this.fieldKey}_usePath`];
- return (
+ return this._regenerateLoading ? null : (
<Tooltip
title={
<div className="dash-tooltip">
@@ -731,7 +780,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
);
}
@computed get regenerateImageIcon() {
- return (
+ return this._regenerateLoading ? null : (
<Tooltip title={'click to show AI generations. Drop an image on to create a new generation'}>
<div
className="imageBox-regenerateDropTarget"
@@ -820,7 +869,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
transform,
transformOrigin,
width: this._outpaintAlign ? 'max-content' : this._outpaintAlign ? '100%' : undefined,
- height: this._outpaintVAlign ? 'max-content' : this.Document[this.fieldKey + '_outpaintOriginalWidth'] !== undefined ? '100%' : undefined,
+ height: this._outpaintVAlign ? 'max-content' : this.outpaintOriginalSize?.width ? '100%' : undefined,
}}
onError={action(e => (this._error = e.toString()))}
draggable={false}
@@ -943,10 +992,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
return { width, height };
};
savedAnnotations = () => this._savedAnnotations;
+ showBorderRounding = returnTrue;
+ rejectDrop = (de: DragManager.DropEvent, subView?: DocumentView | undefined) => (this.dataDoc[this.fieldKey] === undefined ? true : (this._props.rejectDrop?.(de, subView) ?? false));
render() {
TraceMobx();
- const borderRad = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BorderRounding) as string;
- const borderRadius = borderRad?.includes('px') ? `${Number(borderRad.split('px')[0]) / (this._props.NativeDimScaling?.() || 1)}px` : borderRad;
+ const borderRadius = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BorderRounding) as string;
return (
<>
<div
@@ -986,7 +1036,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
ScreenToLocalTransform={this.screenToLocalTransform}
select={emptyFunction}
focus={this.focus}
- rejectDrop={this._props.rejectDrop}
+ rejectDrop={this.rejectDrop}
getScrollHeight={this.getScrollHeight}
NativeDimScaling={returnOne}
isAnyChildContentActive={returnFalse}
@@ -1048,8 +1098,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
if (result instanceof Error) {
alert('Error uploading files - possibly due to unsupported file types');
} else {
- this.dataDoc[this.fieldKey] = new ImageField(result.accessPaths.agnostic.client);
- !(result instanceof Error) && DocUtils.assignUploadInfo(result, this.dataDoc);
+ runInAction(() => {
+ this.dataDoc.layout_resetNativeDim = true;
+ !(result instanceof Error) && DocUtils.assignUploadInfo(result, this.dataDoc, this.fieldKey);
+ this.dataDoc[this.fieldKey] = new ImageField(result.accessPaths.agnostic.client);
+ });
}
disposer();
} else {
diff --git a/src/client/views/nodes/KeyValueBox.scss b/src/client/views/nodes/KeyValueBox.scss
index 441fceba4..80ace6ae0 100644
--- a/src/client/views/nodes/KeyValueBox.scss
+++ b/src/client/views/nodes/KeyValueBox.scss
@@ -93,9 +93,9 @@ $header-height: 30px;
height: 30px;
width: 5px;
z-index: 20;
- right: 0;
- top: 0;
- border-radius: 0;
+ right: 0px;
+ top: 0px;
+ border-radius: 0px;
background: black;
pointer-events: all;
}
@@ -105,8 +105,8 @@ $header-height: 30px;
float: left;
height: 37px;
z-index: 20;
- right: 0;
- top: 0;
+ right: 0px;
+ top: 0px;
background: transparent;
pointer-events: none;
}
diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index aa66b5ba9..606f63d6d 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -273,15 +273,15 @@ export class KeyValueBox extends ViewBoxBaseComponent<FieldViewProps>() {
};
createFieldView = (templateDoc: Doc, row: KeyValuePair) => {
- const metaKey = row._props.keyName;
- const fieldTempDoc = Doc.IsDelegateField(templateDoc, metaKey) ? Doc.MakeDelegate(templateDoc) : Doc.MakeEmbedding(templateDoc);
- fieldTempDoc.title = metaKey;
+ const keyName = row._props.keyName;
+ const fieldTempDoc = Doc.IsDelegateField(templateDoc, keyName) ? Doc.MakeDelegate(templateDoc) : Doc.MakeEmbedding(templateDoc);
+ fieldTempDoc.title = keyName;
fieldTempDoc.layout_fitWidth = true;
fieldTempDoc._xMargin = 10;
fieldTempDoc._yMargin = 10;
fieldTempDoc._width = 100;
fieldTempDoc._height = 40;
- fieldTempDoc.layout = this.inferType(templateDoc[metaKey], metaKey);
+ fieldTempDoc.layout = this.inferType(templateDoc[keyName], keyName);
return fieldTempDoc;
};
diff --git a/src/client/views/nodes/KeyValuePair.scss b/src/client/views/nodes/KeyValuePair.scss
index 913ab641c..154fbdcfa 100644
--- a/src/client/views/nodes/KeyValuePair.scss
+++ b/src/client/views/nodes/KeyValuePair.scss
@@ -17,7 +17,7 @@
}
.keyValuePair-td-key-check {
position: relative;
- margin: 0;
+ margin: 0px;
}
.keyValuePair-keyField {
width: 100%;
diff --git a/src/client/views/nodes/LabelBox.scss b/src/client/views/nodes/LabelBox.scss
index 889cdc0ca..e1974d6a0 100644
--- a/src/client/views/nodes/LabelBox.scss
+++ b/src/client/views/nodes/LabelBox.scss
@@ -24,8 +24,8 @@
.answer-icon {
position: absolute;
- right: 8;
- bottom: 5;
+ right: 8px;
+ bottom: 5px;
color: black;
display: inline-block;
font-size: 10px;
@@ -36,8 +36,8 @@
.q-icon {
position: absolute;
- right: 6;
- bottom: 5;
+ right: 6px;
+ bottom: 5px;
color: white;
display: inline-block;
font-size: 10px;
@@ -48,8 +48,8 @@
.edit-icon {
position: absolute;
- right: 20;
- bottom: 5;
+ right: 20px;
+ bottom: 5px;
display: inline-block;
font-size: 10px;
cursor: pointer;
diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx
index 4cbe01b82..c5948cbbd 100644
--- a/src/client/views/nodes/LabelBox.tsx
+++ b/src/client/views/nodes/LabelBox.tsx
@@ -9,7 +9,7 @@ import { TraceMobx } from '../../../fields/util';
import { DocumentType } from '../../documents/DocumentTypes';
import { Docs } from '../../documents/Documents';
import { DragManager } from '../../util/DragManager';
-import { undoable } from '../../util/UndoManager';
+import { undoable, UndoManager } from '../../util/UndoManager';
import { ViewBoxBaseComponent } from '../DocComponent';
import { PinDocView, PinProps } from '../PinFuncs';
import { StyleProp } from '../StyleProp';
@@ -28,6 +28,7 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
private _timeout: NodeJS.Timeout | undefined;
private _divRef: HTMLDivElement | null = null;
private _disposers: { [key: string]: IReactionDisposer } = {};
+ private _liveTextUndo: UndoManager.Batch | undefined; // captured undo batch when typing a new text note into a collection
constructor(props: FieldViewProps) {
super(props);
@@ -168,6 +169,25 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
};
+ setRef = (r: HTMLDivElement | null) => {
+ this._divRef?.removeEventListener('beforeinput', this.beforeInput);
+ this._divRef = r;
+ if (this._divRef) {
+ this._divRef.addEventListener('beforeinput', this.beforeInput);
+
+ if (DocumentView.SelectOnLoad === this.Document) {
+ DocumentView.SetSelectOnLoad(undefined);
+ this._liveTextUndo = FormattedTextBox.LiveTextUndo;
+ FormattedTextBox.LiveTextUndo = undefined;
+ this._divRef.focus();
+ }
+ this.fitTextToBox(this._divRef);
+ if (this.Title) {
+ this.resetCursor();
+ }
+ } else this._timeout && clearTimeout(this._timeout);
+ };
+
render() {
TraceMobx();
const boxParams = this.fitTextToBox(undefined); // this causes mobx to trigger re-render when data changes
@@ -220,30 +240,14 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
this.setText(this._divRef?.innerText ?? '');
if (!FormattedTextBox.tryKeepingFocus(e.relatedTarget, () => this._divRef?.focus())) {
RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined, undefined);
- FormattedTextBox.LiveTextUndo?.end();
- FormattedTextBox.LiveTextUndo = undefined;
+ this._liveTextUndo?.end();
}
}}
dangerouslySetInnerHTML={{
__html: `<span class="textFitted textFitAlignVert" style="display: inline-block; text-align: center; font-size: 100px; height: 0px;">${this.Title?.startsWith('#') ? '' : (this.Title ?? '')}</span>`,
}}
contentEditable={this._props.onClickScript?.() ? undefined : true}
- ref={r => {
- this._divRef?.removeEventListener('beforeinput', this.beforeInput);
- this._divRef = r;
- if (this._divRef) {
- this._divRef.addEventListener('beforeinput', this.beforeInput);
-
- if (DocumentView.SelectOnLoad === this.Document) {
- DocumentView.SetSelectOnLoad(undefined);
- this._divRef.focus();
- }
- this.fitTextToBox(this._divRef);
- if (this.Title) {
- this.resetCursor();
- }
- } else this._timeout && clearTimeout(this._timeout);
- }}
+ ref={this.setRef}
/>
</div>
</div>
diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx
index 78c8a686c..d31fadf77 100644
--- a/src/client/views/nodes/LinkBox.tsx
+++ b/src/client/views/nodes/LinkBox.tsx
@@ -99,6 +99,7 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
};
+ setRef = (r: HTMLDivElement | null) => (this._divRef = r);
render() {
TraceMobx();
@@ -149,28 +150,18 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() {
const aid = targetAhyperlinks?.find(alink => container?.contains(alink))?.id ?? targetAhyperlinks?.lastElement()?.id;
const bid = targetBhyperlinks?.find(blink => container?.contains(blink))?.id ?? targetBhyperlinks?.lastElement()?.id;
if (!aid || !bid) {
- setTimeout(
- action(() => {
- this._forceAnimate += 0.01;
- })
- );
+ setTimeout(action(() => (this._forceAnimate += 0.01)));
return null;
}
if (foundParent) {
setTimeout(
- action(() => {
- this._forceAnimate += 0.01;
- }),
+ action(() => (this._forceAnimate += 0.01)),
1
);
}
-
- if (at || bt)
- setTimeout(
- action(() => {
- this._forceAnimate += 0.01;
- })
- ); // this forces an update during a transition animation
+ if (at || bt) {
+ setTimeout(action(() => (this._forceAnimate += 0.01))); // this forces an update during a transition animation
+ }
const highlight = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Highlighting) as { highlightStyle: string; highlightColor: string; highlightIndex: number; highlightStroke: boolean };
const highlightColor = highlight?.highlightIndex ? highlight?.highlightColor : undefined;
const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string;
@@ -217,7 +208,7 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() {
id={this.DocumentView?.().DocUniqueId}
className="linkBox-label"
tabIndex={-1}
- ref={r => (this._divRef = r)}
+ ref={this.setRef}
onPointerDown={e => e.stopPropagation()}
onFocus={() => {
RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined, this.dataDoc);
diff --git a/src/client/views/nodes/LinkDocPreview.scss b/src/client/views/nodes/LinkDocPreview.scss
index 28216394d..7d99247e7 100644
--- a/src/client/views/nodes/LinkDocPreview.scss
+++ b/src/client/views/nodes/LinkDocPreview.scss
@@ -42,7 +42,7 @@
.linkDocPreview-button {
display: inline-flex;
- margin: 0;
+ margin: 0px;
margin-right: 3px;
border-radius: 50%;
pointer-events: auto;
diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx
index 5026f52fb..1d6f41c65 100644
--- a/src/client/views/nodes/LinkDocPreview.tsx
+++ b/src/client/views/nodes/LinkDocPreview.tsx
@@ -40,7 +40,6 @@ export class LinkInfo {
LinkInfo._instance = this;
makeObservable(this);
}
- // eslint-disable-next-line no-use-before-define
@observable public LinkInfo: Opt<LinkDocPreviewProps> = undefined;
public static get Instance() {
@@ -251,6 +250,10 @@ export class LinkDocPreview extends ObservableReactComponent<LinkDocPreviewProps
);
}
+ setDocViewRef = (r: DocumentView | null) => {
+ const targetanchor = this._linkDoc && this._linkSrc && Doc.getOppositeAnchor(this._linkDoc, this._linkSrc);
+ targetanchor && this._targetDoc !== targetanchor && r?._props.focus?.(targetanchor, {});
+ };
@computed get docPreview() {
return (!this._linkDoc || !this._targetDoc || !this._linkSrc) && !this._toolTipText ? null : (
<div className="linkDocPreview-inner">
@@ -280,10 +283,7 @@ export class LinkDocPreview extends ObservableReactComponent<LinkDocPreviewProps
this._toolTipText
) : (
<DocumentView
- ref={r => {
- const targetanchor = this._linkDoc && this._linkSrc && Doc.getOppositeAnchor(this._linkDoc, this._linkSrc);
- targetanchor && this._targetDoc !== targetanchor && r?._props.focus?.(targetanchor, {});
- }}
+ ref={this.setDocViewRef}
Document={this._targetDoc!}
moveDocument={returnFalse}
styleProvider={this._props.styleProvider}
diff --git a/src/client/views/nodes/LoadingBox.scss b/src/client/views/nodes/LoadingBox.scss
index cabd4de05..5a8f49000 100644
--- a/src/client/views/nodes/LoadingBox.scss
+++ b/src/client/views/nodes/LoadingBox.scss
@@ -26,7 +26,7 @@
}
.loadingBox-spinner {
position: absolute;
- top: 0;
- left: 0;
+ top: 0px;
+ left: 0px;
}
}
diff --git a/src/client/views/nodes/LoadingBox.tsx b/src/client/views/nodes/LoadingBox.tsx
index 325ab18b4..4220ce5f1 100644
--- a/src/client/views/nodes/LoadingBox.tsx
+++ b/src/client/views/nodes/LoadingBox.tsx
@@ -43,7 +43,7 @@ export class LoadingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
@observable progress = '';
componentDidMount() {
if (!Doc.CurrentlyLoading?.includes(this.Document)) {
- this.Document.loadingError = 'Upload interrupted, please try again';
+ this.Document.loadingError ??= 'Upload interrupted, please try again';
} else {
const updateFunc = async () => {
const result = await Networking.QueryYoutubeProgress(StrCast(this.Document[Id])); // We use the guid of the overwriteDoc to track file uploads.
diff --git a/src/client/views/nodes/MapBox/MapAnchorMenu.scss b/src/client/views/nodes/MapBox/MapAnchorMenu.scss
index c36d98afe..217576203 100644
--- a/src/client/views/nodes/MapBox/MapAnchorMenu.scss
+++ b/src/client/views/nodes/MapBox/MapAnchorMenu.scss
@@ -6,19 +6,19 @@
}
.anchorMenu-highlighter {
padding-right: 5px;
- .antimodeMenu-button {
- padding: 0;
- padding: 0;
+ .antimodeMenu-button {
+ padding: 0px;
+ padding: 0px;
padding-right: 0px;
padding-left: 0px;
width: 5px;
}
}
-.anchor-color-preview-button {
- width: 25px !important;
+.anchor-color-preview-button {
+ width: 25px !important;
.anchor-color-preview {
display: flex;
- flex-direction: column;
+ flex-direction: column;
padding-right: 3px;
width: unset !important;
.color-preview {
@@ -72,12 +72,11 @@
}
}
- .MuiInputBase-input{
+ .MuiInputBase-input {
color: white !important;
}
-
-
- .css-1t8l2tu-MuiInputBase-input-MuiOutlinedInput-input.Mui-disabled{
+
+ .css-1t8l2tu-MuiInputBase-input-MuiOutlinedInput-input.Mui-disabled {
-webkit-text-fill-color: #b3b2b2 !important;
}
@@ -91,7 +90,7 @@
gap: 5px;
}
- .selected-route-details-container{
+ .selected-route-details-container {
display: flex;
flex-direction: column;
gap: 3px;
@@ -99,33 +98,25 @@
align-items: flex-start;
padding: 5px;
}
-
-
}
- .customized-marker-container{
+ .customized-marker-container {
display: flex;
flex-direction: column;
gap: 10px;
- .current-marker-container{
+ .current-marker-container {
display: flex;
align-items: center;
gap: 5px;
}
- .all-markers-container{
+ .all-markers-container {
display: flex;
- align-items: center;
+ align-items: center;
gap: 10px;
flex-wrap: wrap;
max-width: 400px;
}
}
-
-
-
-
}
-
-
diff --git a/src/client/views/nodes/MapBox/MapBox.scss b/src/client/views/nodes/MapBox/MapBox.scss
index bd4b51038..89d381070 100644
--- a/src/client/views/nodes/MapBox/MapBox.scss
+++ b/src/client/views/nodes/MapBox/MapBox.scss
@@ -21,7 +21,7 @@
.mapBox-infoWindow {
background-color: white;
opacity: 0.75;
- padding: 12;
+ padding: 12px;
font-size: 17;
}
.mapBox-searchbar {
@@ -119,7 +119,7 @@
width: 100%;
label {
- margin-bottom: 0;
+ margin-bottom: 0px;
}
.speed-label {
@@ -197,12 +197,12 @@
}
.mapBox-sidebar {
position: absolute;
- right: 0;
+ right: 0px;
height: 100%;
}
.mapBox-sidebar-handle {
- top: 0;
+ top: 0px;
//top: calc(50% - 17.5px); // use this to center vertically -- make sure it looks okay for slide views
width: 10px;
height: 100%;
@@ -215,7 +215,7 @@
left: 50%;
margin-left: 120px;
right: unset !important;
- margin-top: -10;
+ margin-top: -10px;
height: max-content;
}
.searchbox {
diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx
index a563b7c1b..a279ccc48 100644
--- a/src/client/views/nodes/MapBox/MapBox.tsx
+++ b/src/client/views/nodes/MapBox/MapBox.tsx
@@ -13,7 +13,7 @@ import { CirclePicker, ColorResult } from 'react-color';
import { Layer, MapProvider, MapRef, Map as MapboxMap, Marker, Source, ViewState, ViewStateChangeEvent } from 'react-map-gl/mapbox';
import { ClientUtils, setupMoveUpEvents } from '../../../../ClientUtils';
import { emptyFunction } from '../../../../Utils';
-import { Doc, DocListCast, Field, LinkedTo, Opt, StrListCast } from '../../../../fields/Doc';
+import { Doc, DocListCast, Field, LinkedTo, StrListCast } from '../../../../fields/Doc';
import { List } from '../../../../fields/List';
import { RichTextField } from '../../../../fields/RichTextField';
import { DocCast, NumCast, StrCast, toList } from '../../../../fields/Types';
@@ -111,12 +111,12 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
// this list contains pushpins and configs
@computed get allAnnotations() { return DocListCast(this.dataDoc[this.annotationKey]); } // prettier-ignore
- @computed get allSidebarDocs() { return DocListCast(this.dataDoc[this.SidebarKey]); } // prettier-ignore
+ @computed get allSidebarDocs() { return DocListCast(this.dataDoc[this.sidebarKey]); } // prettier-ignore
@computed get allPushpins() { return this.allAnnotations.filter(anno => anno.type === DocumentType.PUSHPIN); } // prettier-ignore
@computed get allRoutes() { return this.allAnnotations.filter(anno => anno.type === DocumentType.MAPROUTE); } // prettier-ignore
@computed get SidebarShown() { return !!this.layoutDoc._layout_showSidebar; } // prettier-ignore
@computed get sidebarWidthPercent() { return StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%'); } // prettier-ignore
- @computed get SidebarKey() { return this.fieldKey + '_sidebar'; } // prettier-ignore
+ @computed get sidebarKey() { return this.fieldKey + '_sidebar'; } // prettier-ignore
@computed get sidebarColor() {
return StrCast(this.layoutDoc.sidebar_color, StrCast(this.layoutDoc[this._props.fieldKey + '_backgroundColor'], '#e4e4e4'));
}
@@ -260,7 +260,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
removeMapDocument = (doc: Doc | Doc[], annotationKey?: string) => {
this.allAnnotations
- .filter(anno => toList(doc).includes(DocCast(anno.mapPin)))
+ .filter(anno => toList(doc).includes(DocCast(anno.mapPin)!))
.forEach(anno => {
anno.mapPin = undefined;
});
@@ -339,27 +339,19 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
startAnchorDrag = (e: PointerEvent, ele: HTMLElement) => {
e.preventDefault();
e.stopPropagation();
-
- const sourceAnchorCreator = action(() => {
- const note = this.getAnchor(true);
- if (note && this._selectedPinOrRoute) {
- note.latitude = this._selectedPinOrRoute.latitude;
- note.longitude = this._selectedPinOrRoute.longitude;
- note.map = this._selectedPinOrRoute.map;
- }
- return note as Doc;
- });
-
const targetCreator = (annotationOn: Doc | undefined) => {
const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, annotationOn, 'yellow');
+ target.layout_fitWidth = true;
DocumentView.SetSelectOnLoad(target);
return target;
};
+
+ const sourceAnchorCreator = () => this.getAnchor(true);
const docView = this.DocumentView?.();
docView &&
DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(docView, sourceAnchorCreator, targetCreator), e.pageX, e.pageY, {
dragComplete: dragEv => {
- if (!dragEv.aborted && dragEv.annoDragData && dragEv.annoDragData.linkSourceDoc && dragEv.annoDragData.dropDocument && dragEv.linkDocument) {
+ if (!dragEv.aborted && dragEv.annoDragData?.linkSourceDoc && dragEv.annoDragData.dropDocument && dragEv.linkDocument) {
dragEv.annoDragData.linkSourceDoc.followLinkToggle = dragEv.annoDragData.dropDocument.annotationOn === this.Document;
dragEv.annoDragData.linkSourceDoc.followLinkZoom = false;
}
@@ -368,17 +360,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
};
createNoteAnnotation = () => {
- const createFunc = undoable(
- action(() => {
- const note = this._sidebarRef.current?.anchorMenuClick(this.getAnchor(true), ['latitude', 'longitude', LinkedTo]);
- if (note && this._selectedPinOrRoute) {
- note.latitude = this._selectedPinOrRoute.latitude;
- note.longitude = this._selectedPinOrRoute.longitude;
- note.map = this._selectedPinOrRoute.map;
- }
- }),
- 'create note annotation'
- );
+ const createFunc = undoable(() => this._sidebarRef.current?.anchorMenuClick(this.getAnchor(true), ['latitude', 'longitude', LinkedTo]), 'create note annotation');
if (!this.layoutDoc.layout_showSidebar) {
this.toggleSidebar();
setTimeout(createFunc);
@@ -428,14 +410,11 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
}
};
- getView = (doc: Doc, options: FocusViewOptions) => {
- if (this._sidebarRef?.current?.makeDocUnfiltered(doc) && !this.SidebarShown) {
- this.toggleSidebar();
- options.didMove = true;
+ getView = async (doc: Doc, options: FocusViewOptions) => {
+ if (DocListCast(this.dataDoc[this.sidebarKey]).find(anno => Doc.AreProtosEqual(doc.layout_unrendered ? DocCast(doc.annotationOn) : doc, anno))) {
+ SidebarAnnos.getView(this._sidebarRef.current, this.SidebarShown, this.toggleSidebar, doc, options);
}
- return new Promise<Opt<DocumentView>>(res => {
- DocumentView.addViewRenderedCb(doc, dv => res(dv));
- });
+ return undefined;
};
/*
@@ -476,13 +455,14 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
@action
deleteSelectedPinOrRoute = undoable(() => {
- if (this._selectedPinOrRoute) {
+ const selPin = DocCast(this._selectedPinOrRoute);
+ if (selPin) {
// Removes filter
- Doc.setDocFilter(this.Document, 'latitude', NumCast(this._selectedPinOrRoute.latitude), 'remove');
- Doc.setDocFilter(this.Document, 'longitude', NumCast(this._selectedPinOrRoute.longitude), 'remove');
- Doc.setDocFilter(this.Document, LinkedTo, `mapPin=${Field.toScriptString(DocCast(this._selectedPinOrRoute))}`, 'remove');
+ Doc.setDocFilter(this.Document, 'latitude', NumCast(selPin.latitude), 'remove');
+ Doc.setDocFilter(this.Document, 'longitude', NumCast(selPin.longitude), 'remove');
+ Doc.setDocFilter(this.Document, LinkedTo, `mapPin=${Field.toScriptString(selPin)}`, 'remove');
- this.removePushpinOrRoute(this._selectedPinOrRoute);
+ this.removePushpinOrRoute(selPin);
}
MapAnchorMenu.Instance.fadeOut(true);
document.removeEventListener('pointerdown', this.tryHideMapAnchorMenu, true);
@@ -1299,7 +1279,6 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
<SidebarAnnos
ref={this._sidebarRef}
{...this._props}
- fieldKey={this.fieldKey}
Doc={this.Document}
layoutDoc={this.layoutDoc}
dataDoc={this.dataDoc}
diff --git a/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx b/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx
index e0efab576..0beefcb67 100644
--- a/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx
+++ b/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx
@@ -145,7 +145,7 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps>
removeMapDocument = (docsIn: Doc | Doc[], annotationKey?: string) => {
const docs = toList(docsIn);
this.allAnnotations
- .filter(anno => docs.includes(DocCast(anno.mapPin)))
+ .filter(anno => docs.includes(DocCast(anno.mapPin)!))
.forEach(anno => {
anno.mapPin = undefined;
});
@@ -224,6 +224,12 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps>
startAnchorDrag = (e: PointerEvent, ele: HTMLElement) => {
e.preventDefault();
e.stopPropagation();
+ const targetCreator = (annotationOn: Doc | undefined) => {
+ const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, annotationOn, 'yellow');
+ target.layout_fitWidth = true;
+ DocumentView.SetSelectOnLoad(target);
+ return target;
+ };
const sourceAnchorCreator = action(() => {
const note = this.getAnchor(true);
@@ -235,11 +241,6 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps>
return note as Doc;
});
- const targetCreator = (annotationOn: Doc | undefined) => {
- const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, annotationOn, 'yellow');
- DocumentView.SetSelectOnLoad(target);
- return target;
- };
const docView = this.DocumentView?.();
docView &&
DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(docView, sourceAnchorCreator, targetCreator), e.pageX, e.pageY, {
@@ -362,22 +363,22 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps>
@action
deselectPin = () => {
- if (this.selectedPin) {
+ const selPin = DocCast(this.selectedPin);
+ if (selPin) {
// Removes filter
- Doc.setDocFilter(this.Document, 'latitude', NumCast(this.selectedPin.latitude), 'remove');
- Doc.setDocFilter(this.Document, 'longitude', NumCast(this.selectedPin.longitude), 'remove');
- Doc.setDocFilter(this.Document, LinkedTo, `mapPin=${Field.toScriptString(DocCast(this.selectedPin))}`, 'remove');
+ Doc.setDocFilter(this.Document, 'latitude', NumCast(selPin.latitude), 'remove');
+ Doc.setDocFilter(this.Document, 'longitude', NumCast(selPin.longitude), 'remove');
+ Doc.setDocFilter(this.Document, LinkedTo, `mapPin=${Field.toScriptString(selPin)}`, 'remove');
- const temp = this.selectedPin;
if (!this._unmounting) {
- this._bingMap.current.entities.remove(this.map_docToPinMap.get(temp));
+ this._bingMap.current.entities.remove(this.map_docToPinMap.get(selPin));
}
- const newpin = new this.MicrosoftMaps.Pushpin(new this.MicrosoftMaps.Location(temp.latitude, temp.longitude));
- this.MicrosoftMaps.Events.addHandler(newpin, 'click', () => this.pushpinClicked(temp as Doc));
+ const newpin = new this.MicrosoftMaps.Pushpin(new this.MicrosoftMaps.Location(selPin.latitude, selPin.longitude));
+ this.MicrosoftMaps.Events.addHandler(newpin, 'click', () => this.pushpinClicked(selPin));
if (!this._unmounting) {
this._bingMap.current.entities.push(newpin);
}
- this.map_docToPinMap.set(temp, newpin);
+ this.map_docToPinMap.set(selPin, newpin);
this.selectedPin = undefined;
this.bingSearchBarContents = this.Document.map;
}
@@ -388,9 +389,7 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps>
this.toggleSidebar();
options.didMove = true;
}
- return new Promise<Opt<DocumentView>>(res => {
- DocumentView.addViewRenderedCb(doc, dv => res(dv));
- });
+ return new Promise<Opt<DocumentView>>(res => DocumentView.addViewRenderedCb(doc, res));
};
/*
* Pushpin onclick
@@ -535,13 +534,14 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps>
@action
deleteSelectedPin = undoable(() => {
- if (this.selectedPin) {
+ const selPin = this.selectedPin;
+ if (selPin) {
// Removes filter
- Doc.setDocFilter(this.Document, 'latitude', NumCast(this.selectedPin.latitude), 'remove');
- Doc.setDocFilter(this.Document, 'longitude', NumCast(this.selectedPin.longitude), 'remove');
- Doc.setDocFilter(this.Document, LinkedTo, `mapPin=${Field.toScriptString(DocCast(this.selectedPin))}`, 'remove');
+ Doc.setDocFilter(this.Document, 'latitude', NumCast(selPin.latitude), 'remove');
+ Doc.setDocFilter(this.Document, 'longitude', NumCast(selPin.longitude), 'remove');
+ Doc.setDocFilter(this.Document, LinkedTo, `mapPin=${Field.toScriptString(selPin)}`, 'remove');
- this.removePushpin(this.selectedPin);
+ this.removePushpin(selPin);
}
MapAnchorMenu.Instance.fadeOut(true);
document.removeEventListener('pointerdown', this.tryHideMapAnchorMenu, true);
@@ -638,7 +638,10 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps>
this._disposers.highlight = reaction(
() => this.allAnnotations.map(doc => doc[Highlight]),
() => {
- const allConfigPins = this.allAnnotations.map(doc => ({ doc, pushpin: DocCast(doc.mapPin) })).filter(pair => pair.pushpin);
+ const allConfigPins = this.allAnnotations
+ .map(doc => ({ doc, pushpin: DocCast(doc.mapPin) }))
+ .filter(pair => pair.pushpin)
+ .map(pair => ({ doc: pair.doc, pushpin: pair.pushpin! }));
allConfigPins.forEach(({ pushpin }) => {
if (!pushpin[Highlight] && this.map_pinHighlighted.get(pushpin)) {
this.recolorPin(pushpin);
diff --git a/src/client/views/nodes/PDFBox.scss b/src/client/views/nodes/PDFBox.scss
index 44013a96d..c9edb2180 100644
--- a/src/client/views/nodes/PDFBox.scss
+++ b/src/client/views/nodes/PDFBox.scss
@@ -17,8 +17,8 @@
height: 100%;
z-index: 1;
pointer-events: none;
- top: 0;
- left: 0;
+ top: 0px;
+ left: 0px;
// glr: This should really be the same component as text and PDFs
.pdfBox-sidebarBtn {
@@ -72,16 +72,16 @@
align-items: center;
height: 20px;
background: none;
- padding: 0;
+ padding: 0px;
position: absolute;
pointer-events: all;
color: white;
- bottom: 0;
- right: 0;
+ bottom: 0px;
+ right: 0px;
.pdfBox-overlayButton-arrow {
- width: 0;
- height: 0;
+ width: 0px;
+ height: 0px;
border-top: 10px solid transparent;
border-bottom: 10px solid transparent;
border-right: 15px solid #121721;
@@ -122,8 +122,8 @@
.pdfBox-settingsCont {
position: absolute;
- right: 0;
- top: 3;
+ right: 0px;
+ top: 3px;
pointer-events: all;
.pdfBox-settingsButton {
@@ -133,11 +133,11 @@
align-items: center;
height: 20px;
background: none;
- padding: 0;
+ padding: 0px;
.pdfBox-settingsButton-arrow {
- width: 0;
- height: 0;
+ width: 0px;
+ height: 0px;
border-top: 10px solid transparent;
border-bottom: 10px solid transparent;
border-right: 15px solid #121721;
@@ -189,7 +189,7 @@
width: calc(100% - 40px);
height: 20px;
background: #121721;
- bottom: 0;
+ bottom: 0px;
display: flex;
justify-content: center;
align-items: center;
@@ -198,7 +198,7 @@
pointer-events: all;
.pdfBox-searchBar {
- width: 70%;
+ width: calc(100% - 120px); // less size of search buttons
font-size: 14px;
}
}
@@ -253,13 +253,13 @@
.pdfBox-container {
position: absolute;
transform-origin: top left;
- top: 0;
+ top: 0px;
}
.pdfBox-sidebarContainer {
position: absolute;
height: 100%;
- right: 0;
- top: 0;
+ right: 0px;
+ top: 0px;
}
.pdfBox-interactive {
@@ -290,7 +290,7 @@
}
.pdfBox-settingsButton-arrow {
- height: 60;
+ height: 60px;
border-top: 30px solid transparent;
border-bottom: 30px solid transparent;
border-right: 30px solid #121721;
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 4ecbd65b6..34211fa07 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -10,7 +10,7 @@ import { DocData } from '../../../fields/DocSymbols';
import { Id } from '../../../fields/FieldSymbols';
import { InkTool } from '../../../fields/InkField';
import { ComputedField } from '../../../fields/ScriptField';
-import { Cast, FieldValue, NumCast, StrCast, toList } from '../../../fields/Types';
+import { Cast, DocCast, FieldValue, NumCast, StrCast, toList } from '../../../fields/Types';
import { ImageField, PdfField } from '../../../fields/URLField';
import { TraceMobx } from '../../../fields/util';
import { emptyFunction } from '../../../Utils';
@@ -27,13 +27,15 @@ import { Colors } from '../global/globalEnums';
import { PDFViewer } from '../pdf/PDFViewer';
import { PinDocView, PinProps } from '../PinFuncs';
import { SidebarAnnos } from '../SidebarAnnos';
-import { DocumentView } from './DocumentView';
import { FieldView, FieldViewProps } from './FieldView';
import { FocusViewOptions } from './FocusViewOptions';
import { ImageBox } from './ImageBox';
import { OpenWhere } from './OpenWhere';
import './PDFBox.scss';
import { CreateImage } from './WebBoxRenderer';
+import { gptAPICall } from '../../apis/gpt/GPT';
+import { List } from '../../../fields/List';
+import { GPTCallType } from '../../apis/gpt/GPT';
@observer
export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
@@ -57,6 +59,9 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
@observable private _pdf: Opt<Pdfjs.PDFDocumentProxy> = undefined;
@observable private _pageControls = false;
+ @computed get sidebarKey() {
+ return this.fieldKey + '_sidebar';
+ }
@computed get pdfUrl() {
return Cast(this.dataDoc[this._props.fieldKey], PdfField);
}
@@ -77,6 +82,36 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
}
}
+ autoTag = async () => {
+ if (!this.Document.$tags_chat && this._pdf) {
+ if (!this.dataDoc.text) {
+ // 1) Extract text from the first few pages (e.g., first 2 pages)
+ const maxPages = Math.min(2, this._pdf.numPages);
+ const promises: Promise<string>[] = [];
+ for (let pageNum = 1; pageNum <= maxPages; pageNum++) {
+ promises.push(
+ this._pdf
+ .getPage(pageNum)
+ .then(page => page.getTextContent())
+ .then(content => content.items.map(item => ('str' in item ? item.str : '')).join(' '))
+ );
+ }
+ this.dataDoc.text = (await Promise.all(promises)).join(' ');
+ }
+
+ const text = StrCast(this.dataDoc.text).trim().slice(0, 2000);
+ if (text) {
+ // 2) Ask GPT to classify and provide descriptive tags, then normalize the results
+ const label = await gptAPICall(`"${text}"`, GPTCallType.CLASSIFYTEXTFULL).then(raw => raw.trim().toUpperCase());
+
+ this.Document.$tags_chat = new List<string>(label.split(/\s+/));
+
+ // 4) Show tags in layout
+ this.Document._layout_showTags = true;
+ }
+ }
+ };
+
replaceCanvases = (oldDiv: HTMLElement, newDiv: HTMLElement) => {
if (oldDiv.childNodes) {
for (let i = 0; i < oldDiv.childNodes.length; i++) {
@@ -233,14 +268,11 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
return this._pdfViewer?.scrollFocus(anchor, NumCast(anchor.y, NumCast(anchor.config_scrollTop)), options);
};
- getView = (doc: Doc, options: FocusViewOptions) => {
- if (this._sidebarRef?.current?.makeDocUnfiltered(doc) && !this.SidebarShown) {
- options.didMove = true;
- this.toggleSidebar(false);
+ getView = async (doc: Doc, options: FocusViewOptions) => {
+ if (DocListCast(this.dataDoc[this.sidebarKey]).find(anno => Doc.AreProtosEqual(doc.layout_unrendered ? DocCast(doc.annotationOn) : doc, anno))) {
+ SidebarAnnos.getView(this._sidebarRef.current, this.SidebarShown, () => this.toggleSidebar(false), doc, options);
}
- return new Promise<Opt<DocumentView>>(res => {
- DocumentView.addViewRenderedCb(doc, dv => res(dv));
- });
+ return undefined;
};
getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
@@ -406,7 +438,15 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
onKeyDown={e => ([KeyCodes.BACKSPACE, KeyCodes.DELETE].includes(e.keyCode) ? e.stopPropagation() : true)}
onPointerDown={e => e.stopPropagation()}
style={{ display: this._props.isContentActive() ? 'flex' : 'none' }}>
- <div className="pdfBox-overlayCont" onPointerDown={e => e.stopPropagation()} style={{ left: `${this._searching ? 0 : 100}%` }}>
+ <div
+ className="pdfBox-overlayCont"
+ onPointerDown={e => e.stopPropagation()}
+ style={{
+ transformOrigin: 'bottom left',
+ transform: `scale(${this._props.DocumentView?.().UIBtnScaling || 1})`,
+ width: `${100 / (this._props.DocumentView?.().UIBtnScaling || 1)}%`,
+ left: `${this._searching ? 0 : 100}%`,
+ }}>
<button type="button" className="pdfBox-overlayButton" title={searchTitle} />
<input
className="pdfBox-searchBar"
@@ -435,17 +475,25 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
type="button"
className="pdfBox-overlayButton"
title={searchTitle}
+ style={{
+ transformOrigin: 'bottom right',
+ transform: `scale(${this._props.DocumentView?.().UIBtnScaling || 1})`,
+ }}
onClick={action(() => {
this._searching = !this._searching;
this.search('', true, true);
})}>
- <div className="pdfBox-overlayButton-arrow" onPointerDown={e => e.stopPropagation()} />
<div className="pdfBox-overlayButton-iconCont" onPointerDown={e => e.stopPropagation()}>
<FontAwesomeIcon icon={this._searching ? 'times' : 'search'} size="lg" />
</div>
</button>
- <div className="pdfBox-pageNums">
+ <div
+ className="pdfBox-pageNums"
+ style={{
+ transformOrigin: 'top left',
+ transform: `scale(${this._props.DocumentView?.().UIBtnScaling || 1})`,
+ }}>
<input
value={curPage}
style={{ width: `${curPage > 99 ? 4 : 3}ch`, pointerEvents: 'all' }}
@@ -649,19 +697,11 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
const pdfView = !this._pdf ? null : this.renderPdfView;
const href = this.pdfUrl?.url.href;
if (!pdfView && href) {
- if (PDFBox.pdfcache.get(href))
- setTimeout(
- action(() => {
- this._pdf = PDFBox.pdfcache.get(href);
- })
- );
+ if (PDFBox.pdfcache.get(href)) setTimeout(action(() => (this._pdf = PDFBox.pdfcache.get(href))));
else {
- if (!PDFBox.pdfpromise.get(href)) PDFBox.pdfpromise.set(href, Pdfjs.getDocument(href).promise);
- PDFBox.pdfpromise.get(href)?.then(
- action(pdf => {
- PDFBox.pdfcache.set(href, (this._pdf = pdf));
- })
- );
+ const pdfPromise = PDFBox.pdfpromise.get(href) ?? Pdfjs.getDocument(href).promise;
+ PDFBox.pdfpromise.set(href, pdfPromise);
+ pdfPromise.then(action(pdf => PDFBox.pdfcache.set(href, (this._pdf = pdf))));
}
}
return pdfView ?? this.renderTitleBox;
diff --git a/src/client/views/nodes/PhysicsBox/PhysicsSimulationBox.scss b/src/client/views/nodes/PhysicsBox/PhysicsSimulationBox.scss
index ac2c611c7..78aa526bf 100644
--- a/src/client/views/nodes/PhysicsBox/PhysicsSimulationBox.scss
+++ b/src/client/views/nodes/PhysicsBox/PhysicsSimulationBox.scss
@@ -29,8 +29,8 @@
.wedge {
pointer-events: none;
position: absolute;
- left: 0;
- top: 0;
+ left: 0px;
+ top: 0px;
}
}
diff --git a/src/client/views/nodes/RecordingBox/ProgressBar.scss b/src/client/views/nodes/RecordingBox/ProgressBar.scss
index 28ad25ffa..ec01f0241 100644
--- a/src/client/views/nodes/RecordingBox/ProgressBar.scss
+++ b/src/client/views/nodes/RecordingBox/ProgressBar.scss
@@ -1,36 +1,34 @@
-
.progressbar {
- touch-action: none;
- vertical-align: middle;
- text-align: center;
-
- align-items: center;
- cursor: default;
-
-
- position: absolute;
- display: flex;
- justify-content: flex-start;
- bottom: 2px;
- width: 99%;
- height: 30px;
- background-color: gray;
-
- &.done {
- top: 0;
- width: 0px;
- height: 5px;
- background-color: red;
- z-index: 2;
- }
-
- &.mark {
- top: 0;
- background-color: transparent;
- border-right: 2px solid white;
- z-index: 3;
- pointer-events: none;
- }
+ touch-action: none;
+ vertical-align: middle;
+ text-align: center;
+
+ align-items: center;
+ cursor: default;
+
+ position: absolute;
+ display: flex;
+ justify-content: flex-start;
+ bottom: 2px;
+ width: 99%;
+ height: 30px;
+ background-color: gray;
+
+ &.done {
+ top: 0px;
+ width: 0px;
+ height: 5px;
+ background-color: red;
+ z-index: 2;
+ }
+
+ &.mark {
+ top: 0px;
+ background-color: transparent;
+ border-right: 2px solid white;
+ z-index: 3;
+ pointer-events: none;
+ }
}
.progressbar-disabled {
@@ -43,37 +41,41 @@
// citation: https://codepen.io/_Master_/pen/PRdjmQ
@keyframes blinker {
- from {opacity: 1.0;}
- to {opacity: 0.0;}
+ from {
+ opacity: 1;
+ }
+ to {
+ opacity: 0;
+ }
}
.blink {
- text-decoration: blink;
- animation-name: blinker;
- animation-duration: 0.6s;
- animation-iteration-count:infinite;
- animation-timing-function:ease-in-out;
- animation-direction: alternate;
+ text-decoration: blink;
+ animation-name: blinker;
+ animation-duration: 0.6s;
+ animation-iteration-count: infinite;
+ animation-timing-function: ease-in-out;
+ animation-direction: alternate;
}
.segment {
- border: 3px solid black;
- background-color: red;
- margin: 1px;
- padding: 0;
- cursor: pointer;
- transition-duration: .5s;
- user-select: none;
-
- vertical-align: middle;
- text-align: center;
+ border: 3px solid black;
+ background-color: red;
+ margin: 1px;
+ padding: 0px;
+ cursor: pointer;
+ transition-duration: 0.5s;
+ user-select: none;
+
+ vertical-align: middle;
+ text-align: center;
}
.segment-expanding {
-border-color: red;
- background-color: white;
- transition-duration: 0s;
- opacity: .75;
- pointer-events: none;
+ border-color: red;
+ background-color: white;
+ transition-duration: 0s;
+ opacity: 0.75;
+ pointer-events: none;
}
.segment-expanding:hover {
@@ -82,10 +84,10 @@ border-color: red;
}
.segment-disabled {
- pointer-events: none;
- opacity: 0.5;
- transition-duration: 0s;
- /* Hide the text. */
+ pointer-events: none;
+ opacity: 0.5;
+ transition-duration: 0s;
+ /* Hide the text. */
text-indent: 100%;
white-space: nowrap;
overflow: hidden;
@@ -99,25 +101,26 @@ border-color: red;
}
.segment:first-child {
- margin-left: 2px;
+ margin-left: 2px;
}
.segment:last-child {
- margin-right: 2px;
+ margin-right: 2px;
}
.segment:hover {
background-color: white;
}
-.segment:hover, .segment-selected {
- margin: 0px;
- border: 4px solid red;
- border-radius: 2px;
+.segment:hover,
+.segment-selected {
+ margin: 0px;
+ border: 4px solid red;
+ border-radius: 2px;
}
.segment-selected {
- border: 4px solid #202020;
- background-color: red;
- opacity: .75;
- cursor: grabbing;
+ border: 4px solid #202020;
+ background-color: red;
+ opacity: 0.75;
+ cursor: grabbing;
}
diff --git a/src/client/views/nodes/RecordingBox/RecordingView.scss b/src/client/views/nodes/RecordingBox/RecordingView.scss
index f2d5a980d..15b48c111 100644
--- a/src/client/views/nodes/RecordingBox/RecordingView.scss
+++ b/src/client/views/nodes/RecordingBox/RecordingView.scss
@@ -28,7 +28,7 @@ video {
justify-content: center;
// overflow: hidden;
border-radius: 10px;
- margin: 0;
+ margin: 0px;
}
.video-wrapper:hover .controls {
@@ -108,7 +108,7 @@ video {
.timer {
font-size: 15px;
color: white;
- margin: 0;
+ margin: 0px;
}
.dot {
@@ -148,7 +148,7 @@ video {
height: 80%;
width: 80%;
align-self: center;
- margin: 0;
+ margin: 0px;
&:hover {
height: 85%;
@@ -163,7 +163,7 @@ video {
height: 70%;
width: 70%;
align-self: center;
- margin: 0;
+ margin: 0px;
// &:hover {
// width: 40px;
@@ -178,8 +178,8 @@ video {
flex-direction: row;
align-content: center;
position: relative;
- top: 0;
- bottom: 0;
+ top: 0px;
+ bottom: 0px;
&.video-edit-wrapper {
// right: 50% - 15;
diff --git a/src/client/views/nodes/ScreenshotBox.scss b/src/client/views/nodes/ScreenshotBox.scss
index 1e9b64a0b..1714d87c2 100644
--- a/src/client/views/nodes/ScreenshotBox.scss
+++ b/src/client/views/nodes/ScreenshotBox.scss
@@ -32,10 +32,10 @@
.screenshotBox-uiButtons {
position: absolute;
- right: 25;
- top: 0;
- width: 22;
- height: 25;
+ right: 25px;
+ top: 0px;
+ width: 22px;
+ height: 25px;
.screenshotBox-recorder {
color: white;
diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx
index 603dcad5c..4677e0e61 100644
--- a/src/client/views/nodes/ScreenshotBox.tsx
+++ b/src/client/views/nodes/ScreenshotBox.tsx
@@ -171,19 +171,21 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
ContextMenu.Instance.addItem({ description: 'Options...', subitems, icon: 'video' });
};
+ setRef = (r: HTMLVideoElement | null) => {
+ this._videoRef = r;
+ setTimeout(() => {
+ if (this.layoutDoc.mediaState === mediaState.PendingRecording && this._videoRef) {
+ this.toggleRecording();
+ }
+ }, 100);
+ };
+
@computed get content() {
return (
<video
className="videoBox-content"
key="video"
- ref={r => {
- this._videoRef = r;
- setTimeout(() => {
- if (this.layoutDoc.mediaState === mediaState.PendingRecording && this._videoRef) {
- this.toggleRecording();
- }
- }, 100);
- }}
+ ref={this.setRef}
autoPlay={this._screenCapture}
style={{ width: this._screenCapture ? '100%' : undefined, height: this._screenCapture ? '100%' : undefined }}
onCanPlay={this.videoLoad}
diff --git a/src/client/views/nodes/ScriptingBox.scss b/src/client/views/nodes/ScriptingBox.scss
index 9789da55a..de70dbe74 100644
--- a/src/client/views/nodes/ScriptingBox.scss
+++ b/src/client/views/nodes/ScriptingBox.scss
@@ -82,14 +82,14 @@
}
.rta__autocomplete--top {
- margin-top: 0;
+ margin-top: 0px;
margin-bottom: 1em;
max-height: 100px;
}
.rta__list {
- margin: 0;
- padding: 0;
+ margin: 0px;
+ padding: 0px;
background: #fff;
border: 1px solid #dfe2e5;
border-radius: 3px;
diff --git a/src/client/views/nodes/TaskBox.scss b/src/client/views/nodes/TaskBox.scss
new file mode 100644
index 000000000..beee58697
--- /dev/null
+++ b/src/client/views/nodes/TaskBox.scss
@@ -0,0 +1,132 @@
+.task-manager-container {
+ color-scheme: light;
+ display: flex;
+ flex-direction: column;
+ padding: 8px;
+ gap: 10px;
+ width: 100%;
+ height: 100%;
+ box-sizing: border-box;
+}
+
+.task-manager-title {
+ width: 100%;
+ font-size: 1.25rem;
+ font-weight: 600;
+ padding: 6px 10px;
+ border: 1px solid #ccc;
+ border-radius: 6px;
+ box-sizing: border-box;
+}
+
+.task-manager-description {
+ width: 100%;
+ font-size: 1rem;
+ padding: 8px 10px;
+ border: 1px solid #ccc;
+ border-radius: 6px;
+ min-height: 40px;
+ box-sizing: border-box;
+ vertical-align: top;
+ text-align: start;
+ resize: none;
+ line-height: 1.4;
+ resize: none;
+ flex-grow: 1;
+}
+
+.task-manager-checkboxes {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+}
+
+.task-manager-allday,
+.task-manager-complete {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 0.95rem;
+}
+
+.task-manager-times {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ width: 100%;
+}
+
+.task-manager-times label {
+ display: flex;
+ flex-direction: column;
+ font-size: 0.9rem;
+ font-weight: 500;
+ gap: 4px;
+}
+
+input[type='datetime-local'] {
+ width: 100%;
+ font-size: 0.9rem;
+ padding: 6px 8px;
+ border: 1px solid #ccc;
+ border-radius: 6px;
+ box-sizing: border-box;
+}
+
+.task-manager-checkboxes {
+ display: flex;
+ flex-wrap: wrap; /* allows wrapping on small screens */
+ align-items: center;
+ gap: 16px;
+ row-gap: 8px; /* optional: tighter vertical spacing if it wraps */
+}
+
+.task-manager-google {
+ align-self: flex-start;
+ width: auto;
+ font-size: 0.85rem;
+ padding: 6px 12px;
+ border-radius: 6px;
+ background-color: #5e88c8;
+ color: white;
+ border: none;
+ cursor: pointer;
+ white-space: nowrap;
+ transition: background-color 0.2s ease;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
+
+ &:hover {
+ background-color: #4773b0; // darker shade of your base blue
+ color: white;
+ transform: scale(1.01); // subtle hover feel without real size change
+ }
+
+ &:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+ pointer-events: none;
+ }
+}
+
+.task-box-blur-wrapper {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ min-width: 0;
+}
+
+.task-manager-button-row {
+ display: flex;
+ flex-direction: row;
+ gap: 8px;
+}
+
+.task-manager-delete {
+ @extend .task-manager-google;
+ background-color: #182430;
+
+ &:hover {
+ background-color: #000000;
+ }
+}
diff --git a/src/client/views/nodes/TaskBox.tsx b/src/client/views/nodes/TaskBox.tsx
new file mode 100644
index 000000000..ed5982c55
--- /dev/null
+++ b/src/client/views/nodes/TaskBox.tsx
@@ -0,0 +1,670 @@
+import { action, IReactionDisposer, makeObservable, observable, reaction, runInAction } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { DateField } from '../../../fields/DateField';
+import { BoolCast, DateCast, DocCast, NumCast, StrCast } from '../../../fields/Types';
+import { GoogleAuthenticationManager } from '../../apis/GoogleAuthenticationManager';
+import { Docs } from '../../documents/Documents';
+import { DocumentType } from '../../documents/DocumentTypes';
+import { ViewBoxBaseComponent } from '../DocComponent';
+import { FieldView, FieldViewProps } from './FieldView';
+import './TaskBox.scss';
+import { DocumentDecorations } from '../DocumentDecorations';
+import { Doc } from '../../../fields/Doc';
+import { DocumentView } from './DocumentView';
+
+/**
+ * TaskBox class for adding task information + completing tasks
+ */
+@observer
+export class TaskBox extends ViewBoxBaseComponent<FieldViewProps>() {
+ _googleTaskCreateDisposer?: IReactionDisposer;
+ _heightDisposer?: IReactionDisposer;
+ _widthDisposer?: IReactionDisposer;
+ @observable _needsSync = false; // Whether the task needs to be synced with Google Tasks
+ @observable _syncing = false; // Whether the task is currently syncing with Google Tasks
+ private _isFocused = false; // Whether the task box is currently focused
+
+ // contains the last synced task information
+ private _lastSyncedTask: {
+ title: string;
+ text: string;
+ due?: string;
+ completed: boolean;
+ deleted?: boolean;
+ } = {
+ title: '',
+ text: '',
+ due: '',
+ completed: false,
+ deleted: false,
+ };
+
+ /**
+ * Getter for needsSync
+ */
+ get needsSync() {
+ return this._needsSync;
+ }
+
+ /**
+ * Constructor for the task box
+ * @param props - props containing the document reference
+ */
+ constructor(props: FieldViewProps) {
+ super(props);
+ makeObservable(this);
+ }
+
+ /**
+ * Return the JSX string that will create this component
+ * @param fieldStr the Doc field that contains the primary data for this component
+ * @returns
+ */
+ public static LayoutString(fieldStr: string) {
+ return FieldView.LayoutString(TaskBox, fieldStr);
+ }
+
+ /**
+ * Method to update the task description
+ * @param e - event of changing the description box input
+ */
+ @action
+ updateText = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
+ this.Document[this.fieldKey] = e.target.value;
+ };
+
+ /**
+ * Method to update the task title
+ * @param e - event of changing the title box input
+ */
+ @action
+ updateTitle = (e: React.ChangeEvent<HTMLInputElement>) => {
+ this.Document.title = e.target.value;
+ };
+
+ /**
+ * Method to update the all day status
+ * @param e - event of changing the all day checkbox
+ */
+ @action
+ updateAllDay = (e: React.ChangeEvent<HTMLInputElement>) => {
+ this.Document.$task_allDay = e.target.checked;
+
+ if (e.target.checked) {
+ delete this.Document.$task_startTime;
+ delete this.Document.$task_endTime;
+ }
+
+ this.setTaskDateRange();
+ };
+
+ /**
+ * Method to update the task start time
+ * @param e - event of changing the start time input
+ */
+ @action
+ updateStart = (e: React.ChangeEvent<HTMLInputElement>) => {
+ const newStart = new Date(e.target.value);
+
+ this.Document.$task_startTime = new DateField(newStart);
+
+ const endDate = this.Document.$task_endTime instanceof DateField ? this.Document.$task_endTime.date : undefined;
+ if (endDate && newStart > endDate) {
+ // Alert user
+ alert('Start time cannot be after end time. End time has been adjusted.');
+
+ // Fix end time
+ const adjustedEnd = new Date(newStart.getTime() + 60 * 60 * 1000);
+ this.Document.$task_endTime = new DateField(adjustedEnd);
+ }
+
+ this.setTaskDateRange();
+ };
+
+ /**
+ * Method to update the task end time
+ * @param e - event of changing the end time input
+ */
+ @action
+ updateEnd = (e: React.ChangeEvent<HTMLInputElement>) => {
+ const newEnd = new Date(e.target.value);
+
+ this.Document.$task_endTime = new DateField(newEnd);
+
+ const startDate = this.Document.$task_startTime instanceof DateField ? this.Document.$task_startTime.date : undefined;
+ if (startDate && newEnd < startDate) {
+ // Alert user
+ alert('End time cannot be before start time. Start time has been adjusted.');
+
+ // Fix start time
+ const adjustedStart = new Date(newEnd.getTime() - 60 * 60 * 1000);
+ this.Document.$task_startTime = new DateField(adjustedStart);
+ }
+
+ this.setTaskDateRange();
+ };
+
+ /**
+ * Method to update the task date range
+ */
+ @action
+ setTaskDateRange() {
+ const doc = this.Document;
+
+ if (doc.$task_allDay) {
+ const range = StrCast(doc.$task_dateRange).split('|');
+ const dateStr = range[0] ?? new Date().toISOString().split('T')[0]; // default to today
+
+ doc.$task_dateRange = `${dateStr}|${dateStr}`;
+ doc.$task_allDay = true;
+ } else {
+ const startField = doc.$task_startTime;
+ const endField = doc.$task_endTime;
+ const startDate = startField instanceof DateField ? startField.date : null;
+ const endDate = endField instanceof DateField ? endField.date : null;
+
+ if (startDate && endDate && !isNaN(startDate.getTime()) && !isNaN(endDate.getTime())) {
+ doc.$task_dateRange = `${startDate.toISOString()}|${endDate.toISOString()}`;
+ doc.$task_allDay = false;
+ }
+ }
+ }
+
+ /**
+ * Method to set task's completion status
+ * @param e - event of changing the "completed" input checkbox
+ */
+
+ @action
+ toggleComplete = (e: React.ChangeEvent<HTMLInputElement>) => {
+ this.Document.$task_completed = e.target.checked;
+ };
+
+ /**
+ * Computes due date for the task (for Google Tasks API)
+ * @returns - a string representing the due date in ISO format, or undefined if no valid date is found
+ */
+ private computeDueDate(): string | undefined {
+ const doc = this.Document;
+ let due: string | undefined;
+ const allDay = !!doc.$task_allDay;
+
+ if (allDay) {
+ const rawRange = StrCast(doc.$task_dateRange);
+ const datePart = rawRange.split('|')[0];
+
+ if (datePart && !isNaN(new Date(datePart).getTime())) {
+ // Set time to midnight UTC to represent the start of the all-day event
+ const baseDate = datePart.includes('T') ? datePart : datePart + 'T00:00:00Z';
+ due = new Date(baseDate).toISOString();
+ } else {
+ due = undefined;
+ }
+ } else if (doc.$task_endTime instanceof DateField && doc.$task_endTime.date) {
+ due = doc.$task_endTime.date.toISOString();
+ } else if (doc.$task_startTime instanceof DateField && doc.$task_startTime.date) {
+ due = doc.$task_startTime.date.toISOString();
+ } else {
+ due = undefined;
+ }
+
+ return due;
+ }
+
+ /**
+ * Builds the body for the Google Tasks API request
+ * @returns - an object containing the task details
+ */
+
+ private buildGoogleTaskBody(): Record<string, string | boolean | undefined> {
+ const doc = this.Document;
+ const title = StrCast(doc.title, 'Untitled Task');
+ const notes = StrCast(doc[this.fieldKey]);
+ const due = this.computeDueDate();
+ const completed = !!doc.$task_completed;
+
+ const body: Record<string, string | boolean | undefined> = {
+ title,
+ notes,
+ due,
+ status: completed ? 'completed' : 'needsAction',
+ completed: completed ? new Date().toISOString() : undefined,
+ };
+
+ if (doc.$dashDeleted === true) {
+ body.deleted = true;
+ } else if (doc.$dashDeleted === false) {
+ body.deleted = false;
+ }
+
+ return body;
+ }
+
+ /**
+ * Handles the focus event for the task box (for auto-syncing)
+ */
+
+ handleFocus = () => {
+ if (!this._isFocused) {
+ this._isFocused = true;
+ this.syncWithGoogleTaskBidirectional(true); // silent sync
+ }
+ };
+
+ /**
+ * Handles the blur event for the task box (for auto-syncing)
+ * @param e - the focus event
+ */
+ handleBlur = (e: React.FocusEvent<HTMLDivElement>) => {
+ // Check if focus is moving outside this component
+ if (!e.currentTarget.contains(e.relatedTarget)) {
+ this._isFocused = false;
+ this.syncWithGoogleTaskBidirectional(true);
+ }
+ };
+
+ /**
+ * Method to sync the task with Google Tasks bidirectionally
+ * (update Dash from Google and vice versa, based on which is newer)
+ * @param silent - whether to suppress UI prompts to connect to Google (default: false)
+ * @returns - a promise that resolves to true if sync was successful, false otherwise
+ */
+
+ syncWithGoogleTaskBidirectional = async (silent = false): Promise<boolean> => {
+ const doc = this.Document;
+ let token: string | undefined;
+ try {
+ token = silent ? await GoogleAuthenticationManager.Instance.fetchAccessTokenSilently() : await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken();
+ } catch (err) {
+ console.warn('Google auth failed:', err);
+ return false;
+ }
+
+ if (!token) {
+ if (!silent) {
+ const listener = () => {
+ window.removeEventListener('focusin', listener);
+ if (confirm('✅ Authorization complete. Try syncing the task again?')) {
+ // try syncing again
+ this.syncWithGoogleTaskBidirectional();
+ }
+ window.removeEventListener('focusin', listener);
+ };
+ setTimeout(() => window.addEventListener('focusin', listener), 100);
+ }
+ return false;
+ }
+
+ if (!doc.$googleTaskId) return false;
+
+ runInAction(() => {
+ this._syncing = true;
+ });
+
+ try {
+ // Fetch current version of Google Task
+ const response = await fetch(`/googleTasks/${doc.$googleTaskId}`, {
+ method: 'GET',
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ credentials: 'include',
+ });
+
+ const googleTask = await response.json();
+ const googleUpdated = new Date(googleTask.updated);
+ const dashUpdated = new Date(StrCast(doc.$task_lastSyncedAt));
+
+ const dashChanged =
+ StrCast(doc.title) !== this._lastSyncedTask.title ||
+ StrCast(doc[this.fieldKey]) !== this._lastSyncedTask.text ||
+ this.computeDueDate() !== this._lastSyncedTask.due ||
+ !!doc.$task_completed !== this._lastSyncedTask.completed ||
+ !!doc.$dashDeleted !== this._lastSyncedTask.deleted;
+
+ if (googleUpdated > dashUpdated && !dashChanged) {
+ // Google version is newer — update Dash
+ runInAction(() => {
+ doc.title = googleTask.title ?? doc.title;
+ doc[this.fieldKey] = googleTask.notes ?? doc[this.fieldKey];
+ doc.$task_completed = googleTask.status === 'completed';
+
+ if (googleTask.due && googleTask.due.split('T')[0] !== this.computeDueDate()?.split('T')[0]) {
+ const dueDate = new Date(googleTask.due);
+ doc.$task_allDay = true;
+ doc.$task_dateRange = `${dueDate.toISOString().split('T')[0]}|${dueDate.toISOString().split('T')[0]}`;
+ }
+
+ doc.$task_lastSyncedAt = googleTask.updated;
+ this._lastSyncedTask = {
+ title: StrCast(doc.title),
+ text: StrCast(doc[this.fieldKey]),
+ due: this.computeDueDate(),
+ completed: !!doc.$task_completed,
+ deleted: !!doc.$dashDeleted,
+ };
+ this._needsSync = false;
+ });
+
+ console.log('Pulled newer version from Google');
+ return true;
+ } else if (googleUpdated <= dashUpdated && !dashChanged) {
+ console.log('No changes to sync');
+ return true;
+ } else {
+ // Dash version is newer — push update to Google
+ const body = this.buildGoogleTaskBody();
+ const res = await fetch(`/googleTasks/${doc.$googleTaskId}`, {
+ method: 'PATCH',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${token}`,
+ },
+ credentials: 'include',
+ body: JSON.stringify(body),
+ });
+
+ const result = await res.json();
+ if (result?.id) {
+ doc.$task_lastSyncedAt = new Date().toISOString();
+ this._lastSyncedTask = {
+ title: StrCast(doc.title),
+ text: StrCast(doc[this.fieldKey]),
+ due: this.computeDueDate(),
+ completed: !!doc.$task_completed,
+ deleted: !!doc.$dashDeleted,
+ };
+ this._needsSync = false;
+ console.log('Pushed newer version to Google');
+ return true;
+ } else {
+ console.warn('❌ Push failed:', result);
+ return false;
+ }
+ }
+ } catch (err) {
+ console.error('❌ Sync error:', err);
+ return false;
+ } finally {
+ runInAction(() => {
+ this._syncing = false;
+ });
+ }
+ };
+
+ /**
+ * Method to set up the task box on mount
+ */
+ componentDidMount() {
+ this.setTaskDateRange();
+ const doc = this.Document;
+
+ // adding task on creation to google
+ (async () => {
+ if (!doc.$googleTaskId && doc.title) {
+ try {
+ const token = await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken();
+ if (!token) return;
+ const body = this.buildGoogleTaskBody();
+
+ const res = await fetch('/googleTasks/create', {
+ method: 'POST',
+ credentials: 'include',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${token}`,
+ },
+ body: JSON.stringify(body),
+ });
+
+ const result = await res.json();
+ if (result?.id) {
+ doc.$googleTaskId = result.id;
+ console.log('✅ Google Task created on mount:', result);
+ } else {
+ console.warn('❌ Google Task creation failed:', result);
+ }
+ } catch (err) {
+ console.warn('❌ Error creating Google Task:', err);
+ }
+ } else if (doc.$googleTaskId) {
+ await this.syncWithGoogleTaskBidirectional();
+ }
+ })();
+
+ this._heightDisposer = reaction(
+ () => NumCast(doc._height),
+ height => {
+ const minHeight = NumCast(doc.height_min);
+ if (height < minHeight) {
+ doc._height = minHeight;
+ }
+ }
+ );
+
+ this._widthDisposer = reaction(
+ () => NumCast(doc._width),
+ width => {
+ const minWidth = NumCast(doc.width_min);
+ if (width < minWidth) {
+ doc._width = minWidth;
+ }
+ }
+ );
+
+ runInAction(() => {
+ const completed = BoolCast(doc.$task_completed);
+ const due = this.computeDueDate();
+
+ this._lastSyncedTask = {
+ title: StrCast(doc.title),
+ text: StrCast(doc[this.fieldKey]),
+ due,
+ completed,
+ deleted: !!doc.$dashDeleted,
+ };
+ this._needsSync = false;
+ });
+
+ if (this.Document.$dashDeleted) {
+ runInAction(() => {
+ this.Document.$dashDeleted = false;
+ });
+ }
+
+ this._googleTaskCreateDisposer = reaction(
+ () => {
+ const completed = BoolCast(doc.$task_completed);
+ const due = this.computeDueDate();
+ const dashDeleted = !!doc.$dashDeleted;
+
+ return { title: StrCast(doc.title), text: StrCast(doc[this.fieldKey]), completed, due, dashDeleted };
+ },
+ ({ title, text, completed, due, dashDeleted }) => {
+ this._needsSync = title !== this._lastSyncedTask.title || text !== this._lastSyncedTask.text || due !== this._lastSyncedTask.due || completed !== this._lastSyncedTask.completed || dashDeleted !== this._lastSyncedTask.deleted;
+ },
+ { fireImmediately: true }
+ );
+ }
+
+ /**
+ * Method to clean up the task box on unmount
+ */
+ componentWillUnmount() {
+ const doc = this.Document;
+ this._googleTaskCreateDisposer?.();
+ this._heightDisposer?.();
+ this._widthDisposer?.();
+ }
+
+ /**
+ * Method to handle task deletion
+ * @returns - a promise that resolves when the task is deleted
+ */
+ handleDeleteTask = async () => {
+ const doc = this.Document;
+ if (!doc.$googleTaskId) return;
+ if (!window.confirm('Are you sure you want to delete this task?')) return;
+
+ doc.$dashDeleted = true;
+ this._needsSync = true;
+
+ try {
+ const token = await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken();
+ if (!token) return;
+
+ await fetch(`/googleTasks/${doc.$googleTaskId}`, {
+ method: 'DELETE',
+ credentials: 'include',
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ });
+
+ const view = DocumentView.getDocumentView(this.Document);
+ if (view) {
+ DocumentView.SelectView(view, false); // select document
+ DocumentDecorations.Instance?.onCloseClick?.(true); // simulate clicking the close button
+ }
+
+ // Remove the task from the recently closed list
+ Doc.MyRecentlyClosed && Doc.RemoveDocFromList(Doc.MyRecentlyClosed, undefined, this.Document);
+ console.log(`✅ Deleted Google Task ${doc.$googleTaskId}`);
+ } catch (err) {
+ console.warn('❌ Failed to delete Google Task:', err);
+ }
+ };
+
+ /**
+ * Method to render the task box
+ * @returns - HTML with taskbox components
+ */
+
+ render() {
+ function toLocalDateTimeString(date: Date): string {
+ const pad = (n: number) => n.toString().padStart(2, '0');
+ return date.getFullYear() + '-' + pad(date.getMonth() + 1) + '-' + pad(date.getDate()) + 'T' + pad(date.getHours()) + ':' + pad(date.getMinutes());
+ }
+
+ const doc = this.Document;
+
+ const taskDesc = StrCast(doc[this.fieldKey]);
+ const taskTitle = StrCast(doc.title);
+ const allDay = !!doc.$task_allDay;
+ const due = this.computeDueDate();
+ const isCompleted = !!this.Document.$task_completed;
+
+ const startTime = DateCast(doc.$task_startTime) ? toLocalDateTimeString(DateCast(doc.$task_startTime)!.date) : '';
+ const endTime = DateCast(doc.$task_endTime) ? toLocalDateTimeString(DateCast(doc.$task_endTime)!.date) : '';
+
+ const handleGoogleTaskSync = async () => {
+ const success = await this.syncWithGoogleTaskBidirectional();
+
+ if (success) {
+ alert('✅ Task successfully synced!');
+ } else {
+ alert('❌ Task sync failed. Try reloading.');
+ }
+ };
+
+ return (
+ <div className="task-box-blur-wrapper" tabIndex={0} onBlur={this.handleBlur} onFocus={this.handleFocus}>
+ <div className="task-manager-container">
+ <input className="task-manager-title" type="text" placeholder="Task Title" value={taskTitle} onChange={this.updateTitle} disabled={isCompleted} style={{ opacity: isCompleted ? 0.7 : 1 }} />
+
+ <textarea className="task-manager-description" placeholder="What’s your task?" value={taskDesc} onChange={this.updateText} disabled={isCompleted} style={{ opacity: isCompleted ? 0.7 : 1 }} />
+
+ <div className="task-manager-checkboxes">
+ <label className="task-manager-allday" style={{ opacity: isCompleted ? 0.7 : 1 }}>
+ <input type="checkbox" checked={allDay} onChange={this.updateAllDay} disabled={isCompleted} />
+ All day
+ {allDay && (
+ <input
+ type="date"
+ value={(() => {
+ const datePart = StrCast(doc.$task_dateRange).split('|')[0];
+ if (!datePart) return '';
+ const d = new Date(datePart);
+ return !isNaN(d.getTime()) ? d.toISOString().split('T')[0] : '';
+ })()}
+ onChange={e => {
+ const newDate = new Date(e.target.value);
+ if (!isNaN(newDate.getTime())) {
+ const dateStr = e.target.value;
+ if (dateStr) {
+ doc.$task_dateRange = `${dateStr}T00:00:00|${dateStr}T00:00:00`;
+ }
+ }
+ }}
+ disabled={isCompleted}
+ style={{ marginLeft: '8px' }}
+ />
+ )}
+ </label>
+
+ <label className="task-manager-complete">
+ <input type="checkbox" checked={isCompleted} onChange={this.toggleComplete} />
+ Complete
+ </label>
+ </div>
+
+ <div className="task-manager-button-row">
+ <button
+ className="task-manager-google"
+ onClick={event => {
+ event.preventDefault();
+ handleGoogleTaskSync();
+ }}>
+ {this._syncing ? 'Syncing...' : this.needsSync ? 'Push Updates' : 'Sync Task'}
+ </button>
+
+ <button
+ className="task-manager-delete"
+ onClick={event => {
+ event.preventDefault();
+ this.handleDeleteTask();
+ }}>
+ <svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="12" height="12" viewBox="0 0 24 24" style={{ fill: 'white', marginRight: '6px', verticalAlign: 'middle', marginTop: '-2px' }}>
+ <path d="M 10 2 L 9 3 L 5 3 C 4.4 3 4 3.4 4 4 C 4 4.6 4.4 5 5 5 L 7 5 L 17 5 L 19 5 C 19.6 5 20 4.6 20 4 C 20 3.4 19.6 3 19 3 L 15 3 L 14 2 L 10 2 z M 5 7 L 5 20 C 5 21.1 5.9 22 7 22 L 17 22 C 18.1 22 19 21.1 19 20 L 19 7 L 5 7 z M 9 9 C 9.6 9 10 9.4 10 10 L 10 19 C 10 19.6 9.6 20 9 20 C 8.4 20 8 19.6 8 19 L 8 10 C 8 9.4 8.4 9 9 9 z M 15 9 C 15.6 9 16 9.4 16 10 L 16 19 C 16 19.6 15.6 20 15 20 C 14.4 20 14 19.6 14 19 L 14 10 C 14 9.4 14.4 9 15 9 z"></path>
+ </svg>
+ Delete
+ </button>
+ </div>
+
+ {!allDay && (
+ <div className="task-manager-times" style={{ opacity: isCompleted ? 0.7 : 1 }}>
+ <label>
+ Start:
+ <input type="datetime-local" value={startTime} onChange={this.updateStart} disabled={isCompleted} />
+ </label>
+ <label>
+ End:
+ <input type="datetime-local" value={endTime} onChange={this.updateEnd} disabled={isCompleted} />
+ </label>
+ </div>
+ )}
+ </div>
+ </div>
+ );
+ }
+}
+
+Docs.Prototypes.TemplateMap.set(DocumentType.TASK, {
+ layout: { view: TaskBox, dataField: 'text' },
+ options: {
+ acl: '',
+ _height: 35,
+ _xMargin: 10,
+ _yMargin: 10,
+ _layout_autoHeight: true,
+ _layout_nativeDimEditable: true,
+ _layout_reflowVertical: true,
+ _layout_reflowHorizontal: true,
+ task: '',
+ defaultDoubleClick: 'ignore',
+ systemIcon: 'BsCheckSquare',
+ height_min: 300,
+ width_min: 300,
+ },
+});
diff --git a/src/client/views/nodes/VideoBox.scss b/src/client/views/nodes/VideoBox.scss
index b5405f0fb..27f419198 100644
--- a/src/client/views/nodes/VideoBox.scss
+++ b/src/client/views/nodes/VideoBox.scss
@@ -3,8 +3,8 @@
.mini-viewer {
cursor: grab;
position: absolute;
- right: 10;
- top: 10;
+ right: 10px;
+ top: 10px;
opacity: 0.1;
transition: all 0.4s;
color: white;
@@ -38,7 +38,7 @@
.videoBox-annotationLayer {
position: relative;
transform-origin: left top;
- top: 0;
+ top: 0px;
width: 100%;
pointer-events: none;
mix-blend-mode: multiply; // bcz: makes text fuzzy!
@@ -81,8 +81,8 @@
// }
.videoBox-ui-wrapper {
- width: 0;
- height: 0;
+ width: 0px;
+ height: 0px;
position: relative;
z-index: 2000;
}
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index fa099178c..f994bdbb5 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -30,6 +30,7 @@ import { StyleProp } from '../StyleProp';
import { DocumentView } from './DocumentView';
import { FieldView, FieldViewProps } from './FieldView';
import { FocusViewOptions } from './FocusViewOptions';
+import { gptImageLabel } from '../../apis/gpt/GPT';
import './VideoBox.scss';
/**
@@ -109,6 +110,52 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
return this._videoRef;
}
+ autoTag = async () => {
+ if (this.Document.$tags_chat) return;
+ try {
+ if (!this.player) throw new Error('Video element not available.');
+
+ // 1) Extract a frame at the video's midpoint
+ const videoDuration = this.player.duration;
+ const snapshotTime = videoDuration / 2;
+
+ // Seek the video element to the midpoint
+ await new Promise<void>(resolve => {
+ const onSeeked = () => {
+ this.player!.removeEventListener('seeked', onSeeked);
+ resolve();
+ };
+ this.player!.addEventListener('seeked', onSeeked);
+ this.player!.currentTime = snapshotTime;
+ });
+
+ // 2) Draw the frame onto a canvas and get a base64 representation
+ const canvas = document.createElement('canvas');
+ canvas.width = this.player.videoWidth;
+ canvas.height = this.player.videoHeight;
+ const ctx = canvas.getContext('2d');
+ if (!ctx) throw new Error('Failed to create canvas context.');
+ ctx.drawImage(this.player, 0, 0, canvas.width, canvas.height);
+ const base64Image = canvas.toDataURL('image/png');
+
+ // 3) Send the image data to GPT for classification and descriptive tags
+ const label = await gptImageLabel(
+ base64Image,
+ `Classify this video frame as either a PERSON or LANDSCAPE.
+ Then provide five additional descriptive tags (single words) separated by spaces.
+ Finally, add one detailed summary phrase using underscores.`
+ ).then(raw => raw.trim().toUpperCase());
+
+ // 4) Normalize and store labels in the Document's tags
+ const aspect = this.player!.videoWidth / (this.player!.videoHeight || 1);
+ this.Document.$tags_chat = new List<string>([...label.split(/\s+/), `ASPECT_${aspect}`]);
+ // 5) Turn on tag display in layout
+ this.Document._layout_showTags = true;
+ } catch (err) {
+ console.error('Video autoTag failed:', err);
+ }
+ };
+
componentDidMount() {
this.unmounting = false;
this._props.setContentViewBox?.(this); // this tells the DocumentView that this VideoBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the VideoBox when making a link.
@@ -338,12 +385,17 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
const timecode = Cast(this.layoutDoc._layout_currentTimecode, 'number', null);
const marquee = AnchorMenu.Instance.GetAnchor?.(undefined, addAsAnnotation);
+ const docAnchor = () =>
+ Docs.Create.ConfigDocument({
+ title: '#' + timecode,
+ _timecodeToShow: timecode,
+ annotationOn: this.Document,
+ });
if (!addAsAnnotation && marquee) marquee.backgroundColor = 'transparent';
- const anchor =
- addAsAnnotation && marquee
- ? CollectionStackedTimeline.createAnchor(this.Document, this.dataDoc, this.annotationKey, timecode || undefined, undefined, marquee, addAsAnnotation) || this.Document
- : Docs.Create.ConfigDocument({ title: '#' + timecode, _timecodeToShow: timecode, annotationOn: this.Document });
+ const visibleAnchor = () => addAsAnnotation && marquee && (CollectionStackedTimeline.createAnchor(this.Document, this.dataDoc, this.annotationKey, timecode || undefined, undefined, marquee, addAsAnnotation) || this.Document);
+ const anchor = visibleAnchor() || docAnchor();
PinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), temporal: true, pannable: true } }, this.Document);
+ addAsAnnotation && this.addDocument(anchor);
return anchor;
};
@@ -376,9 +428,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
}
return this._stackedTimeline.getView(doc, options);
}
- return new Promise<Opt<DocumentView>>(res => {
- DocumentView.addViewRenderedCb(doc, dv => res(dv));
- });
+ return new Promise<Opt<DocumentView>>(res => DocumentView.addViewRenderedCb(doc, res));
};
// extracts video thumbnails and saves them as field of doc
diff --git a/src/client/views/nodes/WebBox.scss b/src/client/views/nodes/WebBox.scss
index 77d7716f4..f1c964980 100644
--- a/src/client/views/nodes/WebBox.scss
+++ b/src/client/views/nodes/WebBox.scss
@@ -3,8 +3,8 @@
.webBox {
height: 100%;
width: 100%;
- top: 0;
- left: 0;
+ top: 0px;
+ left: 0px;
position: relative;
display: flex;
overflow: hidden;
@@ -28,8 +28,8 @@
height: 100%;
z-index: 1;
pointer-events: none;
- top: 0;
- left: 0;
+ top: 0px;
+ left: 0px;
overflow: hidden;
.webBox-overlayButton {
@@ -39,16 +39,16 @@
align-items: center;
height: 20px;
background: none;
- padding: 0;
+ padding: 0px;
position: absolute;
pointer-events: all;
color: white;
- bottom: 0;
- right: 0;
+ bottom: 0px;
+ right: 0px;
.webBox-overlayButton-arrow {
- width: 0;
- height: 0;
+ width: 0px;
+ height: 0px;
border-top: 10px solid transparent;
border-bottom: 10px solid transparent;
border-right: 15px solid #121721;
@@ -92,7 +92,7 @@
width: calc(100% - 40px);
height: 20px;
background: #121721;
- bottom: 0;
+ bottom: 0px;
display: flex;
justify-content: center;
align-items: center;
@@ -137,7 +137,7 @@
.webBox-annotationLayer {
position: absolute;
transform-origin: left top;
- top: 0;
+ top: 0px;
width: 100%;
pointer-events: none;
mix-blend-mode: multiply; // bcz: makes text fuzzy!
@@ -156,8 +156,8 @@
.webBox-htmlSpan {
position: absolute;
- top: 0;
- left: 0;
+ top: 0px;
+ left: 0px;
cursor: text;
padding: 15px;
height: 100%;
@@ -171,8 +171,8 @@
.webBox-cont-interactive {
padding: 0vw;
position: absolute;
- top: 0;
- left: 0;
+ top: 0px;
+ left: 0px;
width: 100%;
height: 100%;
transform-origin: top left;
@@ -181,8 +181,8 @@
width: 100%;
height: 100%;
position: absolute;
- top: 0;
- left: 0;
+ top: 0px;
+ left: 0px;
body {
::selection {
color: white;
@@ -203,8 +203,8 @@
height: 100%;
position: absolute;
transform-origin: top left;
- top: 0;
- left: 0;
+ top: 0px;
+ left: 0px;
overflow: auto;
.webBox-innerContent {
@@ -224,7 +224,7 @@
}
.webBox-buttons {
- margin-left: 44;
+ margin-left: 44px;
background: lightGray;
width: 100%;
}
@@ -232,8 +232,8 @@
.webBox-annotationToggle {
z-index: 901;
position: absolute;
- top: 2;
- left: 2;
+ top: 2px;
+ left: 2px;
cursor: pointer;
box-shadow: black 0.3em 0.3em 1em;
border-radius: 5px;
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index 1e158f484..881cdae37 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -13,7 +13,7 @@ import { InkTool } from '../../../fields/InkField';
import { List } from '../../../fields/List';
import { RefField } from '../../../fields/RefField';
import { listSpec } from '../../../fields/Schema';
-import { Cast, NumCast, StrCast, toList, WebCast } from '../../../fields/Types';
+import { Cast, DocCast, NumCast, StrCast, toList, WebCast } from '../../../fields/Types';
import { ImageField, WebField } from '../../../fields/URLField';
import { TraceMobx } from '../../../fields/util';
import { emptyFunction, stringHash } from '../../../Utils';
@@ -104,6 +104,9 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
@computed get webField() {
return Cast(this.Document[this._props.fieldKey], WebField)?.url;
}
+ @computed get sidebarKey() {
+ return this.fieldKey + '_sidebar';
+ }
constructor(props: FieldViewProps) {
super(props);
@@ -308,18 +311,17 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
};
@action
- getView = (doc: Doc /* , options: FocusViewOptions */) => {
- if (Doc.AreProtosEqual(doc, this.Document))
- return new Promise<Opt<DocumentView>>(res => {
- res(this.DocumentView?.());
- });
+ getView = async (doc: Doc, options: FocusViewOptions) => {
+ if (Doc.AreProtosEqual(doc, this.Document)) return new Promise<Opt<DocumentView>>(res => res(this.DocumentView?.()));
+
if (this.Document.layout_fieldKey === 'layout_icon') this.DocumentView?.().iconify();
const webUrl = WebCast(doc.config_data)?.url;
if (this._url && webUrl && webUrl.href !== this._url) this.setData(webUrl.href);
- if (this._sidebarRef?.current?.makeDocUnfiltered(doc) && !this.SidebarShown) this.toggleSidebar(false);
- return new Promise<Opt<DocumentView>>(res => {
- DocumentView.addViewRenderedCb(doc, dv => res(dv));
- });
+
+ if (DocListCast(this.dataDoc[this.sidebarKey]).find(anno => Doc.AreProtosEqual(doc.layout_unrendered ? DocCast(doc.annotationOn) : doc, anno))) {
+ return SidebarAnnos.getView(this._sidebarRef.current, this.SidebarShown, () => this.toggleSidebar(false), doc, options);
+ }
+ return undefined;
};
sidebarAddDocTab = (doc: Doc, where: OpenWhere) => {
@@ -393,7 +395,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
.transformPoint(e.clientX, e.clientY - NumCast(this.layoutDoc.layout_scrollTop));
if (!this._marqueeref.current?.isEmpty) this._marqueeref.current?.onEnd(theclick[0], theclick[1]);
else {
- if (!(e.target as HTMLElement)?.tagName?.includes('INPUT')) this.finishMarquee(theclick[0], theclick[1]);
+ if (!(e.target as HTMLElement)?.tagName?.includes('INPUT') && !(e.target as HTMLElement)?.tagName?.includes('TEXTAREA')) this.finishMarquee(theclick[0], theclick[1]);
this._getAnchor = AnchorMenu.Instance?.GetAnchor;
this.marqueeing = undefined;
}
@@ -454,7 +456,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
iframeDown = (e: PointerEvent) => {
this._textAnnotationCreator = undefined;
const sel = this._url ? this._iframe?.contentDocument?.getSelection() : window.document.getSelection();
- if (sel?.empty && !(e.target as any).textContent)
+ if (sel?.empty && !(e.target as HTMLElement).textContent)
sel.empty(); // Chrome
else if (sel?.removeAllRanges) sel.removeAllRanges(); // Firefox
@@ -465,6 +467,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
.transformPoint(e.clientX, e.clientY - NumCast(this.layoutDoc.layout_scrollTop));
MarqueeAnnotator.clearAnnotations(this._savedAnnotations);
const target = e.target as HTMLElement;
+ if ((target as HTMLElement)?.tagName?.includes('INPUT') || (target as HTMLElement)?.tagName?.includes('TEXTAREA')) e.stopPropagation();
const word = target && getWordAtPoint(target, e.clientX, e.clientY);
if (!word && !target?.className?.includes('rangeslider') && !target?.onclick && !target?.parentElement?.onclick) {
this.marqueeing = theclick;
@@ -657,7 +660,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
'click',
undoable(
action((e: MouseEvent) => {
- let eleHref = (e.target as any)?.outerHTML?.split('"="')[1]?.split('"')[0];
+ let eleHref = (e.target as HTMLElement)?.outerHTML?.split('"="')[1]?.split('"')[0];
for (let ele = e.target as HTMLElement | Element | null; ele; ele = ele.parentElement) {
if ('href' in ele) {
eleHref = (typeof ele.href === 'string' ? ele.href : eleHref) || (ele.parentElement && 'href' in ele.parentElement ? (ele.parentElement.href as string) : eleHref);
diff --git a/src/client/views/nodes/audio/AudioWaveform.scss b/src/client/views/nodes/audio/AudioWaveform.scss
index 6cbd1759a..c6b0da9c8 100644
--- a/src/client/views/nodes/audio/AudioWaveform.scss
+++ b/src/client/views/nodes/audio/AudioWaveform.scss
@@ -1,17 +1,17 @@
-.audioWaveform {
+.audioWaveform {
position: relative;
width: 100%;
height: 200%;
overflow: hidden;
z-index: -1000;
- bottom: 0;
+ bottom: 0px;
pointer-events: none;
div {
height: 100% !important;
- width: 100% !important;
+ width: 100% !important;
}
- canvas {
+ canvas {
height: 100% !important;
- width: 100% !important;
+ width: 100% !important;
}
}
diff --git a/src/client/views/nodes/calendarBox/CalendarBox.scss b/src/client/views/nodes/calendarBox/CalendarBox.scss
index f8ac4b2d1..89dc294a5 100644
--- a/src/client/views/nodes/calendarBox/CalendarBox.scss
+++ b/src/client/views/nodes/calendarBox/CalendarBox.scss
@@ -1,9 +1,12 @@
+.calendarBox-interactive,
.calendarBox {
display: flex;
width: 100%;
height: 100%;
transform-origin: top left;
- .calendarBox-wrapper {
+ overflow: auto;
+ > div {
+ pointer-events: none;
width: 100%;
height: 100%;
.fc-timegrid-body {
@@ -23,3 +26,36 @@
}
}
}
+
+// AARAV ADD
+
+/* Existing styles */
+.fc-event.mother {
+ font-weight: 500;
+ border-radius: 4px;
+ padding: 2px 4px;
+ border-width: 2px;
+ }
+
+ /* New styles for completed tasks */
+ .fc-event.completed-task {
+ opacity: 1;
+ filter: grayscale(70%) brightness(90%);
+ text-decoration: line-through;
+ color: #ffffff;
+ }
+
+.calendarBox-interactive {
+ > div {
+ pointer-events: unset;
+ }
+}
+
+.custom-drag-mirror {
+ transition: none !important;
+ transform: none !important;
+}
+
+.fc-event-dragging {
+ opacity: 0 !important;
+}
diff --git a/src/client/views/nodes/calendarBox/CalendarBox.tsx b/src/client/views/nodes/calendarBox/CalendarBox.tsx
index 2b20a666d..a2fa83b5a 100644
--- a/src/client/views/nodes/calendarBox/CalendarBox.tsx
+++ b/src/client/views/nodes/calendarBox/CalendarBox.tsx
@@ -1,15 +1,17 @@
-import { Calendar, EventClickArg, EventDropArg, EventSourceInput } from '@fullcalendar/core';
+import { Calendar, DateSelectArg, EventClickArg, EventDropArg, EventMountArg, EventSourceInput } from '@fullcalendar/core';
+import { EventResizeDoneArg } from '@fullcalendar/interaction';
import dayGridPlugin from '@fullcalendar/daygrid';
import interactionPlugin from '@fullcalendar/interaction';
import multiMonthPlugin from '@fullcalendar/multimonth';
import timeGrid from '@fullcalendar/timegrid';
-import { IReactionDisposer, action, computed, makeObservable, observable, reaction } from 'mobx';
+import FullCalendar from '@fullcalendar/react';
+import { IReactionDisposer, action, computed, makeObservable, observable, reaction, untracked } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { dateRangeStrToDates } from '../../../../ClientUtils';
import { Doc } from '../../../../fields/Doc';
import { Id } from '../../../../fields/FieldSymbols';
-import { BoolCast, NumCast, StrCast } from '../../../../fields/Types';
+import { BoolCast, StrCast } from '../../../../fields/Types';
import { DocServer } from '../../../DocServer';
import { DragManager } from '../../../util/DragManager';
import { CollectionSubView, SubCollectionViewProps } from '../../collections/CollectionSubView';
@@ -17,26 +19,35 @@ import { ContextMenu } from '../../ContextMenu';
import { DocumentView } from '../DocumentView';
import { OpenWhere } from '../OpenWhere';
import './CalendarBox.scss';
+import { DateField } from '../../../../fields/DateField';
+import { undoable } from '../../../util/UndoManager';
+import { DocumentType } from '../../../documents/DocumentTypes';
+import { truncate } from 'fs/promises';
type CalendarView = 'multiMonth' | 'dayGridMonth' | 'timeGridWeek' | 'timeGridDay';
@observer
export class CalendarBox extends CollectionSubView() {
- _calendarRef: HTMLDivElement | null = null;
+ _calendarRef: FullCalendar | null = null;
_calendar: Calendar | undefined;
_observer: ResizeObserver | undefined;
_eventsDisposer: IReactionDisposer | undefined;
_selectDisposer: IReactionDisposer | undefined;
+ _isMultiMonth: boolean | undefined;
+
+ @observable _multiMonth = 0;
constructor(props: SubCollectionViewProps) {
super(props);
makeObservable(this);
}
- @observable _multiMonth = 0;
- isMultiMonth: boolean | undefined;
+ @computed get calTypeFieldKey() {
+ return this.fieldKey + '_calendarType';
+ }
componentDidMount(): void {
+ this.Document.$calendar = ''; // needed only to make the keyvalue view look nice.
this._props.setContentViewBox?.(this);
this._eventsDisposer = reaction(
() => ({ events: this.calendarEvents }),
@@ -52,7 +63,7 @@ export class CalendarBox extends CollectionSubView() {
type: 'CHANGE_DATE',
dateMarker: state.dateEnv.createMarker(initialDate.start),
});
- setTimeout(() => (initialDate.start.toISOString() !== initialDate.end.toISOString() ? this._calendar?.select(initialDate.start, initialDate.end) : this._calendar?.select(initialDate.start)));
+ setTimeout(() => initialDate.start.toISOString() !== initialDate.end.toISOString() && this._calendar?.select(initialDate.start, initialDate.end));
},
{ fireImmediately: true }
);
@@ -64,7 +75,20 @@ export class CalendarBox extends CollectionSubView() {
@computed get calendarEvents(): EventSourceInput | undefined {
return this.childDocs.map(doc => {
- const { start, end } = dateRangeStrToDates(StrCast(doc.date_range));
+ // const { start, end } = dateRangeStrToDates(StrCast(doc.$task_dateRange));
+ const isCompleted = BoolCast(doc.$task_completed);
+
+ const rangeStr = StrCast(doc.$task_dateRange);
+ const [startStr, endStr] = rangeStr.split('|');
+ let start: string | Date, end: string | Date;
+
+ if (BoolCast(doc.$task_allDay)) {
+ start = startStr;
+ end = endStr;
+ } else {
+ ({ start, end } = dateRangeStrToDates(rangeStr));
+ }
+
return {
title: StrCast(doc.title),
start,
@@ -72,8 +96,8 @@ export class CalendarBox extends CollectionSubView() {
groupId: doc[Id],
startEditable: true,
endEditable: true,
- allDay: BoolCast(doc.allDay),
- classNames: ['mother'], // will determine the style
+ allDay: BoolCast(doc.$task_allDay),
+ classNames: ['mother', isCompleted ? 'completed-task' : ''], // will determine the style
editable: true, // subject to change in the future
backgroundColor: this.eventToColor(doc),
borderColor: this.eventToColor(doc),
@@ -86,16 +110,16 @@ export class CalendarBox extends CollectionSubView() {
}
@computed get dateRangeStrDates() {
- return dateRangeStrToDates(StrCast(this.Document.date_range));
+ return dateRangeStrToDates(StrCast(this.Document._calendar_dateRange));
}
get dateSelect() {
- return dateRangeStrToDates(StrCast(this.Document.date));
+ return dateRangeStrToDates(StrCast(this.Document._calendar_date));
}
// Choose a calendar view based on the date range
@computed get calendarViewType(): CalendarView {
- if (this.dataDoc[this.fieldKey + '_calendarType']) return StrCast(this.dataDoc[this.fieldKey + '_calendarType']) as CalendarView;
- if (this.isMultiMonth) return 'multiMonth';
+ if (this.dataDoc[this.calTypeFieldKey]) return StrCast(this.dataDoc[this.calTypeFieldKey]) as CalendarView;
+ if (this._isMultiMonth) return 'multiMonth';
const { start, end } = this.dateRangeStrDates;
if (start.getFullYear() !== end.getFullYear() || start.getMonth() !== end.getMonth()) return 'multiMonth';
if (Math.abs(start.getDay() - end.getDay()) > 7) return 'dayGridMonth';
@@ -104,7 +128,9 @@ export class CalendarBox extends CollectionSubView() {
// TODO: Return a different color based on the event type
eventToColor = (event: Doc): string => {
- return 'red' + event;
+ return StrCast(event.type) === DocumentType.TASK
+ ? '#20B2AA' // teal for tasks
+ : 'red';
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -112,7 +138,7 @@ export class CalendarBox extends CollectionSubView() {
if (!super.onInternalDrop(e, de)) return false;
de.complete.docDragData?.droppedDocuments.forEach(doc => {
const today = new Date().toISOString();
- if (!doc.date_range) doc.$date_range = `${today}|${today}`;
+ if (!doc.$task_dateRange) doc.$task_dateRange = `${today}|${today}`;
});
return true;
};
@@ -122,10 +148,31 @@ export class CalendarBox extends CollectionSubView() {
return false;
};
- handleEventDrop = (arg: EventDropArg) => {
+ handleEventDrop = undoable((arg: EventDropArg | EventResizeDoneArg) => {
const doc = DocServer.GetCachedRefField(arg.event._def.groupId ?? '');
- doc && arg.event.start && (doc.date_range = arg.event.start?.toString() + '|' + (arg.event.end ?? arg.event.start).toString());
- };
+ // doc && arg.event.start && (doc.$task_dateRange = arg.event.start?.toString() + '|' + (arg.event.end ?? arg.event.start).toString());
+ if (!doc || !arg.event.start) return;
+
+ // get the new start and end dates
+ const startDate = new Date(arg.event.start);
+ const endDate = new Date(arg.event.end ?? arg.event.start);
+
+ // update date range, time range, and all day status
+ doc.$task_dateRange = `${startDate.toISOString()}|${endDate.toISOString()}`;
+
+ const allDayStatus = arg.event.allDay ?? false;
+ if (doc.$task_allDay !== allDayStatus) {
+ doc.$task_allDay = allDayStatus;
+ }
+
+ if (doc.$task_allDay) {
+ delete doc.$task_startTime;
+ delete doc.$task_endTime;
+ } else {
+ doc.$task_startTime = new DateField(startDate);
+ doc.$task_endTime = new DateField(endDate);
+ }
+ }, 'change event date');
handleEventClick = (arg: EventClickArg) => {
const doc = DocServer.GetCachedRefField(arg.event._def.groupId ?? '');
@@ -144,90 +191,199 @@ export class CalendarBox extends CollectionSubView() {
};
// https://fullcalendar.io
- renderCalendar = () => {
- const cal = !this._calendarRef
- ? null
- : (this._calendar = new Calendar(this._calendarRef, {
- plugins: [multiMonthPlugin, dayGridPlugin, timeGrid, interactionPlugin],
- headerToolbar: {
- left: 'prev,next today',
- center: 'title',
- right: 'multiMonth dayGridMonth timeGridWeek timeGridDay',
- },
- selectable: true,
- initialView: this.calendarViewType === 'multiMonth' ? undefined : this.calendarViewType,
- initialDate: this.dateSelect.start,
- navLinks: true,
- editable: false,
- displayEventTime: false,
- displayEventEnd: false,
- select: info => {
- const start = dateRangeStrToDates(info.startStr).start.toISOString();
- const end = dateRangeStrToDates(info.endStr).start.toISOString();
- this.dataDoc.date = start + '|' + end;
- },
- aspectRatio: NumCast(this.Document.width) / NumCast(this.Document.height),
- events: this.calendarEvents,
- eventClick: this.handleEventClick,
- eventDrop: this.handleEventDrop,
- eventDidMount: arg => {
- arg.el.addEventListener('pointerdown', ev => {
- ev.button && ev.stopPropagation();
- });
- if (navigator.userAgent.includes('Macintosh')) {
- arg.el.addEventListener('pointerup', ev => {
- ev.button && ev.stopPropagation();
- ev.button && this.handleEventContextMenu(ev.pageX, ev.pageY, arg.event._def.groupId);
- });
- }
- arg.el.addEventListener('contextmenu', ev => {
- if (!navigator.userAgent.includes('Macintosh')) {
- this.handleEventContextMenu(ev.pageX, ev.pageY, arg.event._def.groupId);
+ @computed get renderCalendar() {
+ const availableWidth = this._props.PanelWidth() / (this._props.DocumentView?.().UIBtnScaling ?? 1);
+ const btn = (text: string, view: string | (() => void), hint: string) => ({ text, hint, click: typeof view === 'string' ? () => this._calendarRef?.getApi().changeView(view) : view });
+ return (
+ <FullCalendar
+ ref={(r: unknown) => (this._calendarRef = r as FullCalendar)}
+ customButtons={{
+ nowBtn: btn('Now', () => this._calendarRef?.getApi().gotoDate(new Date()), 'Go to Today'),
+ multiBtn: btn('M+', 'multiMonth', 'Multiple Month View'),
+ monthBtn: btn('M', 'dayGridMonth', 'Month View'),
+ weekBtn: btn('W', 'timeGridWeek', 'Week View'),
+ dayBtn: btn('D', 'timeGridDay', 'Day View'),
+ }}
+ headerToolbar={
+ availableWidth > 450
+ ? {
+ left: 'prev,next nowBtn',
+ center: 'title',
+ right: 'multiBtn monthBtn weekBtn dayBtn',
}
- ev.stopPropagation();
- ev.preventDefault();
- });
- },
- }));
- cal?.render();
- setTimeout(() => cal?.view.calendar.select(this.dateSelect.start, this.dateSelect.end));
- };
+ : availableWidth > 300
+ ? {
+ left: 'prev,next',
+ center: 'title',
+ right: '',
+ }
+ : {
+ left: '',
+ center: 'title',
+ right: '',
+ }
+ }
+ selectable={true}
+ initialView={this.calendarViewType === 'multiMonth' ? undefined : this.calendarViewType}
+ views={{
+ multiMonth: {
+ type: 'multiMonth',
+ duration: { months: 12 },
+ },
+ }}
+ initialDate={untracked(() => this.dateSelect.start)}
+ navLinks={true}
+ editable={true}
+ // expandRows={true}
+ // handleWindowResize={true}
+ displayEventTime={false}
+ displayEventEnd={false}
+ plugins={[multiMonthPlugin, dayGridPlugin, timeGrid, interactionPlugin]}
+ aspectRatio={this._props.PanelWidth() / this._props.PanelHeight()}
+ weekends={true}
+ events={this.calendarEvents}
+ eventClick={this.handleEventClick}
+ eventDrop={this.handleEventDrop}
+ eventResize={this.handleEventDrop}
+ unselectAuto={false}
+ // unselect={() => {}}
+ select={(info: DateSelectArg) => {
+ const start = dateRangeStrToDates(info.startStr).start.toISOString();
+ const end = info.allDay ? start : dateRangeStrToDates(info.endStr).start.toISOString();
+ this.Document._calendar_date = start + '|' + end;
+ }}
+ // eventContent={() => {
+ // return null;
+ // }}
+ eventDidMount={(arg: EventMountArg) => {
+ const doc = DocServer.GetCachedRefField(arg.event._def.groupId ?? '');
+ if (!doc) return;
+
+ if (doc.type === DocumentType.TASK) {
+ const checkButton = document.createElement('button');
+ checkButton.innerText = doc.$task_completed ? '✅' : '⬜';
+ checkButton.style.position = 'absolute';
+ checkButton.style.right = '5px';
+ checkButton.style.top = '50%';
+ checkButton.style.transform = 'translateY(-50%)';
+ checkButton.style.background = 'transparent';
+ checkButton.style.border = 'none';
+ checkButton.style.cursor = 'pointer';
+ checkButton.style.fontSize = '18px';
+ checkButton.style.zIndex = '1000';
+ checkButton.style.padding = '0';
+ checkButton.style.margin = '0';
+
+ checkButton.onclick = ev => {
+ ev.stopPropagation();
+ doc.$task_completed = !doc.$task_completed;
+ this._calendar?.refetchEvents();
+ };
+ arg.el.style.position = 'relative';
+ arg.el.appendChild(checkButton);
+ }
+ arg.el.addEventListener('pointerdown', ev => ev.button && ev.stopPropagation());
+ if (navigator.userAgent.includes('Macintosh')) {
+ arg.el.addEventListener('pointerup', ev => {
+ ev.button && ev.stopPropagation();
+ ev.button && this.handleEventContextMenu(ev.pageX, ev.pageY, arg.event._def.groupId);
+ });
+ }
+ arg.el.addEventListener('contextmenu', ev => {
+ if (!navigator.userAgent.includes('Macintosh')) {
+ this.handleEventContextMenu(ev.pageX, ev.pageY, arg.event._def.groupId);
+ }
+ ev.stopPropagation();
+ ev.preventDefault();
+ });
+ }}
+ // for dragging and dropping (mirror)
+
+ eventDragStart={arg => {
+ const mirror = arg.el.cloneNode(true) as HTMLElement;
+ const rect = arg.el.getBoundingClientRect();
+
+ mirror.style.position = 'fixed';
+ mirror.style.pointerEvents = 'none';
+ mirror.style.opacity = '0.8';
+ mirror.style.zIndex = '10000';
+ mirror.classList.add('custom-drag-mirror');
+ mirror.style.width = `${rect.width}px`;
+ mirror.style.height = `${rect.height}px`;
+
+ document.body.appendChild(mirror);
+
+ const moveListener = (ev: MouseEvent) => {
+ mirror.style.left = `${ev.clientX}px`;
+ mirror.style.top = `${ev.clientY}px`;
+ };
+
+ window.addEventListener('mousemove', moveListener);
+
+ // hide the actual box
+ arg.el.style.visibility = 'hidden';
+ arg.el.style.opacity = '0';
+
+ (arg.el as any)._mirrorElement = mirror;
+ (arg.el as any)._moveListener = moveListener;
+ }}
+ eventDragStop={arg => {
+ const el = arg.el as any;
+ const mirror = el._mirrorElement;
+ const moveListener = el._moveListener;
+
+ // show the actual box
+ el.style.visibility = 'visible';
+ el.style.opacity = '1';
+
+ if (mirror) document.body.removeChild(mirror);
+ if (moveListener) window.removeEventListener('mousemove', moveListener);
+ }}
+ />
+ );
+ }
+
+ setRef = (r: HTMLDivElement | null) => {
+ this.createDashEventsTarget(r);
+ this.fixWheelEvents(r, this._props.isContentActive);
+ };
render() {
+ const scale = this._props.ScreenToLocalTransform().Scale;
+ const scaledWidth = this._props.PanelWidth();
+ const scaledHeight = this._props.PanelHeight();
+
return (
<div
key={this.calendarViewType}
- className="calendarBox"
+ className={`calendarBox${this._props.isContentActive() ? '-interactive' : ''}`}
+ style={{
+ width: scaledWidth,
+ height: scaledHeight,
+ overflow: 'hidden',
+ position: 'relative',
+ }}
+ ref={this.setRef}
onPointerDown={e => {
setTimeout(
action(() => {
const cname = (e.nativeEvent.target as HTMLButtonElement)?.className ?? '';
- if (cname.includes('multiMonth')) this.dataDoc[this.fieldKey + '_calendarType'] = 'multiMonth';
- if (cname.includes('dayGridMonth')) this.dataDoc[this.fieldKey + '_calendarType'] = 'dayGridMonth';
- if (cname.includes('timeGridWeek')) this.dataDoc[this.fieldKey + '_calendarType'] = 'timeGridWeek';
- if (cname.includes('timeGridDay')) this.dataDoc[this.fieldKey + '_calendarType'] = 'timeGridDay';
+ if (cname.includes('multiMonth')) this.dataDoc[this.calTypeFieldKey] = 'multiMonth';
+ if (cname.includes('dayGridMonth')) this.dataDoc[this.calTypeFieldKey] = 'dayGridMonth';
+ if (cname.includes('timeGridWeek')) this.dataDoc[this.calTypeFieldKey] = 'timeGridWeek';
+ if (cname.includes('timeGridDay')) this.dataDoc[this.calTypeFieldKey] = 'timeGridDay';
})
);
- }}
- style={{
- width: this._props.PanelWidth() / this._props.ScreenToLocalTransform().Scale,
- height: this._props.PanelHeight() / this._props.ScreenToLocalTransform().Scale,
- transform: `scale(${this._props.ScreenToLocalTransform().Scale})`,
- }}
- ref={r => {
- this.createDashEventsTarget(r);
- this.fixWheelEvents(r, this._props.isContentActive);
-
- if (r) {
- this._observer?.disconnect();
- (this._observer = new ResizeObserver(() => {
- this._calendar?.setOption('aspectRatio', NumCast(this.Document.width) / NumCast(this.Document.height));
- this._calendar?.updateSize();
- })).observe(r);
- this.renderCalendar();
- }
}}>
- <div className="calendarBox-wrapper" ref={r => (this._calendarRef = r)} />
+ <div
+ style={{
+ transform: `scale(${scale})`,
+ transformOrigin: 'top left',
+ width: scaledWidth / scale,
+ height: scaledHeight / scale,
+ }}>
+ {this.renderCalendar}
+ </div>
</div>
);
}
diff --git a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.scss b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.scss
index 8e00cbdb7..18a179c67 100644
--- a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.scss
+++ b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.scss
@@ -50,7 +50,7 @@ $font-size-xlarge: 18px;
position: relative;
h2 {
- margin: 0;
+ margin: 0px;
font-size: 1.25rem;
font-weight: 600;
letter-spacing: 0.01em;
@@ -103,7 +103,7 @@ $font-size-xlarge: 18px;
margin-bottom: 4px;
&:last-child {
- margin-bottom: 0;
+ margin-bottom: 0px;
}
&:hover {
@@ -191,8 +191,8 @@ $font-size-xlarge: 18px;
content: '';
position: absolute;
top: -5px;
- left: 0;
- right: 0;
+ left: 0px;
+ right: 0px;
height: 5px;
background: linear-gradient(to top, rgba(0, 0, 0, 0.06), transparent);
pointer-events: none;
@@ -457,11 +457,11 @@ $font-size-xlarge: 18px;
margin: 8px 0;
&:first-child {
- margin-top: 0;
+ margin-top: 0px;
}
&:last-child {
- margin-bottom: 0;
+ margin-bottom: 0px;
}
}
@@ -521,8 +521,8 @@ $font-size-xlarge: 18px;
border-bottom: 1px dashed #e5e7eb;
&:last-child {
- margin-bottom: 0;
- padding-bottom: 0;
+ margin-bottom: 0px;
+ padding-bottom: 0px;
border-bottom: none;
}
@@ -581,7 +581,7 @@ $font-size-xlarge: 18px;
.message-content {
background-color: inherit;
- padding: 0;
+ padding: 0px;
border-radius: 8px;
font-size: 14px;
line-height: 1.6;
@@ -708,10 +708,10 @@ $font-size-xlarge: 18px;
.uploading-overlay {
position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
+ top: 0px;
+ left: 0px;
+ right: 0px;
+ bottom: 0px;
background-color: rgba(255, 255, 255, 0.92);
display: flex;
justify-content: center;
@@ -807,7 +807,7 @@ $font-size-xlarge: 18px;
@media (max-width: 768px) {
.chat-box {
- border-radius: 0;
+ border-radius: 0px;
}
.message {
@@ -954,10 +954,10 @@ $font-size-xlarge: 18px;
/* Tool Reload Modal Styles */
.tool-reload-modal-overlay {
position: fixed;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
+ top: 0px;
+ left: 0px;
+ right: 0px;
+ bottom: 0px;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
@@ -969,7 +969,7 @@ $font-size-xlarge: 18px;
.tool-reload-modal {
background: white;
border-radius: 12px;
- padding: 0;
+ padding: 0px;
min-width: 400px;
max-width: 500px;
box-shadow:
@@ -995,7 +995,7 @@ $font-size-xlarge: 18px;
border-bottom: 1px solid #e2e8f0;
h3 {
- margin: 0;
+ margin: 0px;
font-size: 18px;
font-weight: 600;
color: #1a202c;
@@ -1019,7 +1019,7 @@ $font-size-xlarge: 18px;
color: #4a5568;
&:last-child {
- margin-bottom: 0;
+ margin-bottom: 0px;
}
strong {
diff --git a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx
index df6c5627c..6e6ef6212 100644
--- a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx
+++ b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx
@@ -15,7 +15,7 @@ import * as React from 'react';
import { v4 as uuidv4 } from 'uuid';
import { ClientUtils, OmitKeys } from '../../../../../ClientUtils';
import { Doc, DocListCast, Opt } from '../../../../../fields/Doc';
-import { DocData, DocLayout, DocViews } from '../../../../../fields/DocSymbols';
+import { DocData, DocViews } from '../../../../../fields/DocSymbols';
import { Id } from '../../../../../fields/FieldSymbols';
import { RichTextField } from '../../../../../fields/RichTextField';
import { ScriptField } from '../../../../../fields/ScriptField';
@@ -44,7 +44,6 @@ import './ChatBox.scss';
import MessageComponentBox from './MessageComponent';
import { OpenWhere } from '../../OpenWhere';
import { Upload } from '../../../../../server/SharedMediaTypes';
-import { DocumentMetadataTool } from '../tools/DocumentMetadataTool';
import { AgentDocumentManager } from '../utils/AgentDocumentManager';
dotenv.config();
@@ -497,7 +496,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
case supportedDocTypes.image: return Docs.Create.ImageDocument(data as string, options);
case supportedDocTypes.equation: return Docs.Create.EquationDocument(data as string, options);
case supportedDocTypes.notetaking: return Docs.Create.NoteTakingDocument([], options);
- case supportedDocTypes.web:
+ case supportedDocTypes.web: {
// Create web document with enhanced safety options
const webOptions = {
...options,
@@ -506,10 +505,11 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
// If iframe_sandbox was passed from AgentDocumentManager, add it to the options
if ('_iframe_sandbox' in options) {
- (webOptions as any)._iframe_sandbox = options._iframe_sandbox;
+ webOptions._iframe_sandbox = options._iframe_sandbox;
}
return Docs.Create.WebDocument(data as string, webOptions);
+ }
case supportedDocTypes.dataviz: return Docs.Create.DataVizDocument('/users/rz/Downloads/addresses.csv', options);
case supportedDocTypes.pdf: return Docs.Create.PdfDocument(data as string, options);
case supportedDocTypes.video: return Docs.Create.VideoDocument(data as string, options);
@@ -640,7 +640,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
handleCitationClick = async (citation: Citation) => {
try {
// Extract values from MobX proxy object if needed
- const chunkId = typeof citation.chunk_id === 'object' ? (citation.chunk_id as any).toString() : citation.chunk_id;
+ const chunkId = typeof citation.chunk_id === 'object' ? (citation.chunk_id as unknown as object).toString() : citation.chunk_id;
// For debugging
console.log('Citation clicked:', {
@@ -682,7 +682,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
}
this.handleOtherChunkTypes(foundChunk, citation, doc, dataDoc);
// Show the chunk text in citation popup
- let chunkText = citation.direct_text || 'Text content not available';
+ const chunkText = citation.direct_text || 'Text content not available';
this.showCitationPopup(chunkText);
// Also navigate to the document
@@ -841,14 +841,16 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
}
break;
case CHUNK_TYPE.TEXT:
- this._citationPopup = { text: citation.direct_text ?? 'No text available', visible: true };
- this.startCitationPopupTimer();
+ {
+ this._citationPopup = { text: citation.direct_text ?? 'No text available', visible: true };
+ this.startCitationPopupTimer();
- // Check if the document is a PDF (has a PDF viewer component)
- const isPDF = PDFCast(dataDoc!.data) !== null || dataDoc!.type === DocumentType.PDF;
+ // Check if the document is a PDF (has a PDF viewer component)
+ const isPDF = PDFCast(dataDoc!.data) !== null || dataDoc!.type === DocumentType.PDF;
- // First ensure document is fully visible before trying to access its views
- this.ensureDocumentIsVisible(dataDoc!, isPDF, citation, foundChunk, doc);
+ // First ensure document is fully visible before trying to access its views
+ this.ensureDocumentIsVisible(dataDoc!, isPDF, citation, foundChunk, doc);
+ }
break;
case CHUNK_TYPE.CSV:
case CHUNK_TYPE.URL:
@@ -1163,6 +1165,9 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
this._inputValue = question;
};
+ _dictation: DictationButton | null = null;
+ setInputRef = (r: HTMLInputElement) => (this._textInputRef = r);
+ setDictationRef = (r: DictationButton) => (this._dictation = r);
/**
* Handles tool creation notification and shows the reload modal
* @param toolName The name of the tool that was created
@@ -1213,8 +1218,6 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
}, 100);
};
- _dictation: DictationButton | null = null;
-
/**
* Toggles the font size modal visibility
*/
@@ -1443,9 +1446,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
<form onSubmit={this.askGPT} className="chat-input">
<div className="input-container">
<input
- ref={r => {
- this._textInputRef = r;
- }}
+ ref={this.setInputRef}
type="text"
name="messageInput"
autoComplete="off"
@@ -1465,13 +1466,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
</svg>
)}
</button>
- <DictationButton
- ref={r => {
- this._dictation = r;
- }}
- setInput={this.setChatInput}
- inputRef={this._textInputRef}
- />
+ <DictationButton ref={this.setDictationRef} setInput={this.setChatInput} inputRef={this._textInputRef} />
</form>
{/* Popup for citation */}
{this._citationPopup.visible && (
@@ -1501,7 +1496,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
The tool <strong>{this._toolReloadModal.toolName}</strong> has been created and saved successfully.
</p>
<p>To make the tool available for future use, the page needs to be reloaded to rebuild the application bundle.</p>
- <p>Click "Reload Page" to complete the tool installation.</p>
+ <p>Click &quot;Reload Page&quot; to complete the tool installation.</p>
</div>
<div className="tool-reload-modal-actions">
<button className="reload-button primary" onClick={this.handleReloadConfirmation}>
diff --git a/src/client/views/nodes/formattedText/DailyJournal.tsx b/src/client/views/nodes/formattedText/DailyJournal.tsx
index 871c556e6..564609494 100644
--- a/src/client/views/nodes/formattedText/DailyJournal.tsx
+++ b/src/client/views/nodes/formattedText/DailyJournal.tsx
@@ -9,14 +9,22 @@ import { gptAPICall, GPTCallType } from '../../../apis/gpt/GPT';
import { RichTextField } from '../../../../fields/RichTextField';
import { Plugin } from 'prosemirror-state';
import { RTFCast } from '../../../../fields/Types';
+import { Mark } from 'prosemirror-model';
+import { observer } from 'mobx-react';
export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>() {
@observable journalDate: string;
- @observable typingTimeout: NodeJS.Timeout | null = null; // Track typing delay
- @observable lastUserText: string = ''; // Store last user-entered text
+ @observable typingTimeout: NodeJS.Timeout | null = null; // track typing delay
+ @observable lastUserText: string = ''; // store last user-entered text
+ @observable isLoadingPrompts: boolean = false; // track if prompts are loading
+ @observable showPromptMenu = false;
+ @observable inlinePromptsEnabled = true;
+ @observable askPromptsEnabled = true;
+
_ref = React.createRef<FormattedTextBox>(); // reference to the formatted textbox
predictiveTextRange: { from: number; to: number } | null = null; // where predictive text starts and ends
private predictiveText: string | null = ' ... why?';
+ private prePredictiveMarks: Mark[] = [];
public static LayoutString(fieldStr: string) {
return FieldView.LayoutString(DailyJournal, fieldStr);
@@ -40,7 +48,7 @@ export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>()
month: 'long',
day: 'numeric',
});
- console.log('getFormattedDate():', date);
+ // console.log('getFormattedDate():', date);
return date;
}
@@ -49,15 +57,15 @@ export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>()
*/
@action
setDailyTitle() {
- console.log('setDailyTitle() called...');
- console.log('Current title before update:', this.dataDoc.title);
+ // console.log('setDailyTitle() called...');
+ // console.log('Current title before update:', this.dataDoc.title);
if (!this.dataDoc.title || this.dataDoc.title !== this.journalDate) {
- console.log('Updating title to:', this.journalDate);
+ // console.log('Updating title to:', this.journalDate);
this.dataDoc.title = this.journalDate;
}
- console.log('New title after update:', this.dataDoc.title);
+ // console.log('New title after update:', this.dataDoc.title);
}
/**
@@ -68,7 +76,7 @@ export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>()
const placeholderText = 'Start writing here...';
const dateText = `${this.journalDate}\n`;
- console.log('Checking if dataDoc has text field...');
+ // console.log('Checking if dataDoc has text field...');
this.dataDoc[this.fieldKey] = RichTextField.textToRtfFormat(
[
@@ -80,7 +88,63 @@ export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>()
placeholderText.length
);
- console.log('Current text field:', this.dataDoc[this.fieldKey]);
+ // console.log('Current text field:', this.dataDoc[this.fieldKey]);
+ }
+
+ /**
+ * Method to show/hide the prompts menu
+ */
+ @action.bound togglePromptMenu() {
+ this.showPromptMenu = !this.showPromptMenu;
+ }
+
+ /**
+ * Method to toggle on/off inline predictive prompts
+ */
+ @action.bound toggleInlinePrompts() {
+ this.inlinePromptsEnabled = !this.inlinePromptsEnabled;
+ }
+
+ /**
+ * Method to toggle on/off inline /ask prompts
+ */
+ @action.bound toggleAskPrompts() {
+ this.askPromptsEnabled = !this.askPromptsEnabled;
+ }
+
+ /**
+ * Method to handle click on document (to close prompt menu)
+ * @param e - a click on the document
+ */
+ @action.bound
+ handleDocumentClick(e: MouseEvent) {
+ const menu = document.getElementById('prompts-menu');
+ const button = document.getElementById('prompts-button');
+ if (this.showPromptMenu && menu && !menu.contains(e.target as Node) && button && !button.contains(e.target as Node)) {
+ this.showPromptMenu = false;
+ }
+ }
+
+ /**
+ * Method to set initial date of document in the calendar view
+ */
+
+ @action setInitialDateRange() {
+ if (!this.dataDoc.$task_dateRange && this.journalDate) {
+ const parsedDate = new Date(this.journalDate);
+ if (!isNaN(parsedDate.getTime())) {
+ const localStart = new Date(parsedDate.getFullYear(), parsedDate.getMonth(), parsedDate.getDate());
+ const localEnd = new Date(localStart); // same day
+
+ this.dataDoc.$task_dateRange = `${localStart.toISOString()}|${localEnd.toISOString()}`;
+ this.dataDoc.$task_allDay = true;
+ this.dataDoc.$task = ''; // needed only to make the keyvalue view look good.
+
+ // console.log('Set task_dateRange and task_allDay on journal (from local date):', this.dataDoc.$task_dateRange);
+ } else {
+ // console.log('Could not parse journalDate:', this.journalDate);
+ }
+ }
}
/**
@@ -94,8 +158,26 @@ export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>()
if (this.typingTimeout) clearTimeout(this.typingTimeout);
- this.typingTimeout = setTimeout(() => {
+ const { state } = editorView;
+ const cursorPos = state.selection.from;
+
+ // characters before cursor
+ const triggerText = state.doc.textBetween(Math.max(0, cursorPos - 4), cursorPos);
+
+ if (triggerText === '/ask' && this.askPromptsEnabled) {
+ // remove /ask text
+ const tr = state.tr.delete(cursorPos - 4, cursorPos);
+ editorView.dispatch(tr);
+
+ // insert predicted question
this.insertPredictiveQuestion();
+ return;
+ }
+
+ this.typingTimeout = setTimeout(() => {
+ if (this.inlinePromptsEnabled) {
+ this.insertPredictiveQuestion();
+ }
}, 3500);
};
@@ -129,28 +211,38 @@ export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>()
// Only insert if we're at end of node, or there's a newline node after
if (!isAtEndOfParent && !hasNewlineAfter) return;
- const fontSizeMark = schema.marks.pFontSize.create({ fontSize: '14px' });
+ // Save current marks at cursor
+ const currentMarks = state.storedMarks || resolvedPos.marks();
+ this.prePredictiveMarks = [...currentMarks];
+
+ // color and italics are preset for predictive question, font and size are adaptive
const fontColorMark = schema.marks.pFontColor.create({ fontColor: 'lightgray' });
const fontItalicsMark = schema.marks.em.create();
+ const fontSizeMark = this.prePredictiveMarks.find(m => m.type.name === 'pFontSize');
+ const fontFamilyMark = this.prePredictiveMarks.find(m => m.type.name === 'pFontFamily'); // if applicable
- this.predictiveText = ' ...'; // placeholder for now
+ this.predictiveText = ' ...'; // placeholder
const fullTextUpToCursor = state.doc.textBetween(0, state.selection.to, '\n', '\n');
- const gptPrompt = `Given the following incomplete journal entry, generate a single 2-5 word question that continues the user's thought:\n\n"${fullTextUpToCursor}"`;
+ const gptPrompt = `Given the following incomplete journal entry, generate a single 2-5 word reflective question that continues the user's thought:\n\n"${fullTextUpToCursor}"`;
const res = await gptAPICall(gptPrompt, GPTCallType.COMPLETION);
if (!res) return;
// styled text node
const text = ` ... ${res.trim()}`;
- const predictedText = schema.text(text, [fontSizeMark, fontColorMark, fontItalicsMark]);
+ const predictedText = schema.text(text, [fontColorMark, fontItalicsMark, ...(fontSizeMark ? [fontSizeMark] : []), ...(fontFamilyMark ? [fontFamilyMark] : [])]);
// Insert styled text at cursor position
- const transaction = state.tr.insert(insertPos, predictedText).setStoredMarks([state.schema.marks.pFontColor.create({ fontColor: 'gray' })]); // should probably instead inquire marks before predictive prompt
+ const transaction = state.tr.insert(insertPos, predictedText).setStoredMarks(this.prePredictiveMarks);
dispatch(transaction);
this.predictiveText = text;
};
+ /**
+ * Method to remove the predictive question upon type/click
+ * @returns - once predictive text is found, or all text has been checked
+ */
createPredictiveCleanupPlugin = () => {
return new Plugin({
view: () => {
@@ -168,15 +260,20 @@ export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>()
if (node.isText && node.text === textToRemove) {
const tr = state.tr.delete(pos, pos + node.nodeSize);
- // Set the desired default marks for future input
+ // default marks for input
const fontSizeMark = state.schema.marks.pFontSize.create({ fontSize: '14px' });
const fontColorMark = state.schema.marks.pFontColor.create({ fontColor: 'gray' });
tr.setStoredMarks([]);
- tr.setStoredMarks([fontSizeMark, fontColorMark]);
+ if (this.prePredictiveMarks.length > 0) {
+ tr.setStoredMarks(this.prePredictiveMarks);
+ } else {
+ tr.setStoredMarks([fontSizeMark, fontColorMark]);
+ }
dispatch(tr);
this.predictiveText = null;
+ this.prePredictiveMarks = [];
return false;
}
return true;
@@ -194,8 +291,9 @@ export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>()
};
componentDidMount(): void {
- console.log('componentDidMount() triggered...');
- console.log('Text: ' + RTFCast(this.Document.text)?.Text);
+ // console.log('componentDidMount() triggered...');
+ document.addEventListener('mousedown', this.handleDocumentClick);
+ // console.log('Text: ' + RTFCast(this.Document.text)?.Text);
const editorView = this._ref.current?.EditorView;
if (editorView) {
@@ -214,15 +312,17 @@ export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>()
const isDefaultTitle = isTitleString && currentTitle.includes('Untitled DailyJournal');
if (isTextEmpty && isDefaultTitle) {
- console.log('Journal title and text are default. Initializing...');
+ // console.log('Journal title and text are default. Initializing...');
this.setDailyTitle();
this.setDailyText();
+ this.setInitialDateRange();
} else {
- console.log('Journal already has content. Skipping initialization.');
+ // console.log('Journal already has content. Skipping initialization.');
}
}
componentWillUnmount(): void {
+ document.removeEventListener('mousedown', this.handleDocumentClick);
const editorView = this._ref.current?.EditorView;
if (editorView) {
editorView.dom.removeEventListener('input', this.onTextInput);
@@ -230,10 +330,20 @@ export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>()
if (this.typingTimeout) clearTimeout(this.typingTimeout);
}
+ /**
+ * Method to generate pormpts via GPT
+ * @returns - if failed
+ */
@action handleGeneratePrompts = async () => {
+ if (this.isLoadingPrompts) {
+ return;
+ }
+
+ this.isLoadingPrompts = true;
+
const rawText = RTFCast(this.Document.text)?.Text ?? '';
- console.log('Extracted Journal Text:', rawText);
- console.log('Before Update:', this.Document.text, 'Type:', typeof this.Document.text);
+ // console.log('Extracted Journal Text:', rawText);
+ // console.log('Before Update:', this.Document.text, 'Type:', typeof this.Document.text);
if (!rawText.trim()) {
alert('Journal is empty! Write something first.');
@@ -273,9 +383,15 @@ export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>()
}
} catch (err) {
console.error('Error calling GPT:', err);
+ } finally {
+ this.isLoadingPrompts = false;
}
};
+ /**
+ * Method to render the styled DailyJournal
+ * @returns - the HTML component for the journal
+ */
render() {
return (
<div
@@ -296,6 +412,7 @@ export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>()
}}>
{/* GPT Button */}
<button
+ id="prompts-button"
style={{
position: 'absolute',
bottom: '5px',
@@ -308,9 +425,88 @@ export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>()
cursor: 'pointer',
zIndex: 10,
}}
- onClick={this.handleGeneratePrompts}>
+ onClick={this.togglePromptMenu}>
Prompts
</button>
+ {this.showPromptMenu && (
+ <div
+ id="prompts-menu"
+ style={{
+ position: 'absolute',
+ bottom: '45px',
+ right: '5px',
+ backgroundColor: 'white',
+ border: '1px solid #ccc',
+ borderRadius: '4px',
+ padding: '10px',
+ boxShadow: '0 2px 6px rgba(0,0,0,0.2)',
+ zIndex: 20,
+ minWidth: '170px',
+ maxWidth: 'fit-content',
+ overflow: 'auto',
+ }}>
+ <div
+ style={{
+ display: 'flex',
+ justifyContent: 'flex-end',
+ alignItems: 'center',
+ marginBottom: '10px',
+ }}>
+ <label
+ style={{
+ display: 'flex',
+ alignItems: 'center',
+ gap: '6px',
+ fontSize: '14px',
+ justifyContent: 'flex-end',
+ width: '100%',
+ }}>
+ /ask
+ <input type="checkbox" checked={this.askPromptsEnabled} onChange={this.toggleAskPrompts} style={{ margin: 0 }} />
+ </label>
+ </div>
+
+ <div
+ style={{
+ display: 'flex',
+ justifyContent: 'flex-end',
+ alignItems: 'center',
+ marginBottom: '10px',
+ }}>
+ <label
+ style={{
+ display: 'flex',
+ alignItems: 'center',
+ gap: '6px',
+ fontSize: '14px',
+ justifyContent: 'flex-end',
+ width: '100%',
+ }}>
+ Inline Prompting
+ <input type="checkbox" checked={this.inlinePromptsEnabled} onChange={this.toggleInlinePrompts} style={{ margin: 0 }} />
+ </label>
+ </div>
+
+ <button
+ onClick={() => {
+ this.showPromptMenu = false;
+ this.handleGeneratePrompts();
+ }}
+ disabled={this.isLoadingPrompts}
+ style={{
+ backgroundColor: '#9EAD7C',
+ color: 'white',
+ border: 'none',
+ borderRadius: '4px',
+ cursor: this.isLoadingPrompts ? 'not-allowed' : 'pointer',
+ opacity: this.isLoadingPrompts ? 0.6 : 1,
+ padding: '5px 10px',
+ float: 'right',
+ }}>
+ Generate Prompts
+ </button>
+ </div>
+ )}
<FormattedTextBox ref={this._ref} {...this._props} fieldKey={'text'} Document={this.Document} TemplateDataDocument={undefined} />
</div>
@@ -318,8 +514,10 @@ export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>()
}
}
+const ObservedDailyJournal = observer(DailyJournal);
+
Docs.Prototypes.TemplateMap.set(DocumentType.JOURNAL, {
- layout: { view: DailyJournal, dataField: 'text' },
+ layout: { view: ObservedDailyJournal, dataField: 'text' },
options: {
acl: '',
_height: 35,
diff --git a/src/client/views/nodes/formattedText/DashFieldView.scss b/src/client/views/nodes/formattedText/DashFieldView.scss
index 3734ad9cc..3ef3c2cef 100644
--- a/src/client/views/nodes/formattedText/DashFieldView.scss
+++ b/src/client/views/nodes/formattedText/DashFieldView.scss
@@ -17,7 +17,7 @@
min-width: 12px;
position: relative;
display: inline-block;
- margin: 0;
+ margin: 0px;
transform: scale(0.7);
background-color: rgba(155, 155, 155, 0.24);
}
@@ -53,7 +53,7 @@
.dashFieldView,
.dashFieldView-active {
.dashFieldView-select {
- height: 10p;
+ height: 100%;
font-size: 12px;
background: transparent;
opacity: 0;
@@ -61,7 +61,7 @@
}
}
-.dashFieldView {
+.dashFieldView-active {
&:hover {
.dashFieldView-select {
opacity: unset;
diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx
index 7ea5d1fcf..6e3bdc5e8 100644
--- a/src/client/views/nodes/formattedText/DashFieldView.tsx
+++ b/src/client/views/nodes/formattedText/DashFieldView.tsx
@@ -262,6 +262,7 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi
render() {
return (
<div
+ // eslint-disable-next-line no-use-before-define
className={`dashFieldView${this.isRowActive() ? '-active' : ''}`}
ref={this._fieldRef}
style={{
diff --git a/src/client/views/nodes/formattedText/EquationEditor.scss b/src/client/views/nodes/formattedText/EquationEditor.scss
index b0c17e56e..602135a30 100644
--- a/src/client/views/nodes/formattedText/EquationEditor.scss
+++ b/src/client/views/nodes/formattedText/EquationEditor.scss
@@ -32,7 +32,7 @@
margin-left: -1px;
position: relative;
z-index: 1;
- padding: 0;
+ padding: 0px;
display: -moz-inline-box;
display: inline-block;
}
@@ -128,8 +128,8 @@
.mq-math-mode * {
font-size: inherit;
line-height: inherit;
- margin: 0;
- padding: 0;
+ margin: 0px;
+ padding: 0px;
border-color: black;
-webkit-user-select: none;
-moz-user-select: none;
@@ -178,7 +178,7 @@
margin-left: 0.1em;
}
.mq-math-mode .mq-roman var.mq-f {
- margin: 0;
+ margin: 0px;
}
.mq-math-mode big {
font-size: 200%;
@@ -323,7 +323,7 @@
padding: 0.1em;
}
.mq-math-mode .mq-sqrt-prefix {
- padding-top: 0;
+ padding-top: 0px;
position: relative;
top: 0.1em;
vertical-align: top;
diff --git a/src/client/views/nodes/formattedText/EquationEditor.tsx b/src/client/views/nodes/formattedText/EquationEditor.tsx
index 48efa6e63..23d273523 100644
--- a/src/client/views/nodes/formattedText/EquationEditor.tsx
+++ b/src/client/views/nodes/formattedText/EquationEditor.tsx
@@ -72,6 +72,10 @@ class EquationEditor extends Component<EquationEditorProps> {
this.mathField.latex(value || '');
}
+ componentDidUpdate(prevProps: Readonly<EquationEditorProps>): void {
+ !prevProps.value && this.mathField.latex(this.props.value || '');
+ }
+
render() {
return <span ref={this.element} style={{ border: '0px', boxShadow: 'None' }} />;
}
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss
index 547a2efa8..d5e566226 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.scss
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss
@@ -46,7 +46,7 @@
}
audiotag {
- left: 0;
+ left: 0px;
position: absolute;
cursor: pointer;
border-radius: 10px;
@@ -62,7 +62,7 @@ audiotag:hover {
.formattedTextBox {
touch-action: none;
background: inherit;
- padding: 0;
+ padding: 0px;
border-width: 0px;
border-color: global.$medium-gray;
box-sizing: border-box;
@@ -77,14 +77,14 @@ audiotag:hover {
width: 100%;
position: relative;
transform-origin: left top;
- top: 0;
- left: 0;
+ top: 0px;
+ left: 0px;
}
.formattedTextBox-cont {
touch-action: none;
background: inherit;
- padding: 0;
+ padding: 0px;
border-width: 0px;
border-radius: inherit;
border-color: global.$medium-gray;
@@ -111,7 +111,7 @@ audiotag:hover {
.answer-tooltip {
font-size: 15px;
padding: 2px;
- max-width: 150;
+ max-width: 150px;
line-height: 150%;
position: relative;
}
@@ -122,10 +122,10 @@ audiotag:hover {
position: absolute;
color: white;
background: black;
- right: 0;
- bottom: 0;
- width: 15;
- height: 22;
+ right: 0px;
+ bottom: 0px;
+ width: 15px;
+ height: 22px;
cursor: default;
}
@@ -139,8 +139,8 @@ audiotag:hover {
.formattedTextBox-sidebar-handle {
position: absolute;
- top: 0;
- right: 0;
+ top: 0px;
+ right: 0px;
width: 20px;
height: 20px;
font-size: 11px;
@@ -168,7 +168,7 @@ audiotag:hover {
height: 100%;
display: inline-block;
position: absolute;
- right: 0;
+ right: 0px;
overflow: hidden;
.collectionfreeformview-container {
@@ -302,8 +302,8 @@ footnote::before {
position: absolute;
top: -0.5em;
content: ' ';
- height: 0;
- width: 0;
+ height: 0px;
+ width: 0px;
}
.formattedTextBox-inlineComment {
@@ -346,7 +346,7 @@ footnote::before {
.prosemirror-linkBtn {
background: unset;
color: unset;
- padding: 0;
+ padding: 0px;
text-transform: unset;
letter-spacing: unset;
font-size: unset;
@@ -357,7 +357,7 @@ footnote::before {
background-color: dimgray;
margin-top: 1.5em;
z-index: 1;
- padding: 5;
+ padding: 5px;
border-radius: 2px;
}
.prosemirror-hrefoptions {
@@ -396,7 +396,7 @@ footnote::before {
blockquote {
padding: 10px 10px;
font-size: smaller;
- margin: 0;
+ margin: 0px;
font-style: italic;
background: lightgray;
border-left: solid 2px dimgray;
@@ -415,7 +415,7 @@ footnote::before {
p {
font-family: inherit;
}
- margin-left: 0;
+ margin-left: 0px;
}
.bullet1 {
p {
@@ -439,7 +439,7 @@ footnote::before {
display: inline-block;
font-family: inherit;
}
- margin-left: 0;
+ margin-left: 0px;
background-color: inherit;
}
.decimal2-ol {
@@ -506,7 +506,7 @@ footnote::before {
display: inline-block;
font-family: inherit;
}
- margin-left: 0;
+ margin-left: 0px;
padding-left: 1.2em;
background-color: inherit;
}
@@ -661,7 +661,7 @@ footnote::before {
.formattedTextBox-cont {
touch-action: none;
background: inherit;
- padding: 0;
+ padding: 0px;
border-width: 0px;
border-radius: inherit;
border-color: global.$medium-gray;
@@ -706,7 +706,7 @@ footnote::before {
height: 100%;
display: inline-block;
position: absolute;
- right: 0;
+ right: 0px;
.collectionfreeformview-container {
position: relative;
@@ -832,8 +832,8 @@ footnote::before {
position: absolute;
top: -0.5em;
content: ' ';
- height: 0;
- width: 0;
+ height: px;
+ width: 0px;
}
.formattedTextBox-inlineComment {
@@ -892,7 +892,7 @@ footnote::before {
display: inline;
font-family: inherit;
}
- margin-left: 0;
+ margin-left: 0px;
}
.decimal2-ol {
counter-reset: deci2;
@@ -952,7 +952,7 @@ footnote::before {
display: inline;
font-family: inherit;
}
- margin-left: 0;
+ margin-left: 0px;
padding-left: 1.2em;
}
.multi2-ol {
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 57720baae..255ee1afe 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -13,7 +13,7 @@ import { EditorState, NodeSelection, Plugin, Selection, TextSelection, Transacti
import { EditorView, NodeViewConstructor } from 'prosemirror-view';
import * as React from 'react';
import { BsMarkdownFill } from 'react-icons/bs';
-import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, ClientUtils, DivWidth, removeStyleSheet, returnFalse, returnZero, setupMoveUpEvents, simMouseEvent, smoothScroll, StopEvent } from '../../../../ClientUtils';
+import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, ClientUtils, DivWidth, removeStyleSheet, returnFalse, returnTrue, returnZero, setupMoveUpEvents, simMouseEvent, smoothScroll, StopEvent } from '../../../../ClientUtils';
import { DateField } from '../../../../fields/DateField';
import { CreateLinkToActiveAudio, Doc, DocListCast, Field, FieldType, Opt, StrListCast } from '../../../../fields/Doc';
import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, DocCss, ForceServerWrite, UpdatingFromServer } from '../../../../fields/DocSymbols';
@@ -98,7 +98,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
public static PasteOnLoad: ClipboardEvent | undefined;
public static SelectOnLoadChar = '';
- public static LiveTextUndo: UndoManager.Batch | undefined; // undo batch when typing a new text note into a collection
+ public static LiveTextUndo: UndoManager.Batch | undefined; // undo batch request when typing a new text note into a collection
+ private _liveTextUndo: UndoManager.Batch | undefined; // captured undo batch when typing a new text note into a collection
private static _nodeViews: (self: FormattedTextBox) => { [key: string]: NodeViewConstructor };
private _curHighlights = new ObservableSet<string>(['Audio Tags']);
@@ -270,13 +271,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
e.preventDefault();
e.stopPropagation();
const targetCreator = (annotationOn?: Doc) => {
- const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, annotationOn);
+ const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, annotationOn, 'yellow');
+ target.layout_fitWidth = true;
DocumentView.SetSelectOnLoad(target);
return target;
};
+ const sourceAnchorCreator = () => this.getAnchor(true);
+
const docView = this.DocumentView?.();
- docView && DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(docView, () => this.getAnchor(true), targetCreator), e.pageX, e.pageY);
+ docView && DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(docView, sourceAnchorCreator, targetCreator), e.pageX, e.pageY);
});
AnchorMenu.Instance.AddDrawingAnnotation = (drawing: Doc) => {
@@ -304,6 +308,21 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
}
};
+ autoTag = () => {
+ const rawText = RTFCast(this.Document[this.fieldKey])?.Text ?? StrCast(this.Document[this.fieldKey]);
+ if (rawText && !this.Document.$tags_chat) {
+ const callType = rawText.includes('[placeholder]') ? GPTCallType.CLASSIFYTEXTMINIMAL : GPTCallType.CLASSIFYTEXTFULL;
+
+ gptAPICall(rawText, callType).then(
+ action(desc => {
+ // Split GPT response into tokens and push individually & clear existing tags
+ this.Document.$tags_chat = new List<string>(desc.trim().split(/\s+/));
+ this.Document._layout_showTags = true;
+ })
+ );
+ }
+ };
+
leafText = (node: Node) => {
if (node.type === this.EditorView?.state.schema.nodes.dashField) {
const refDoc = !node.attrs.docId ? this.rootDoc : (DocServer.GetCachedRefField(node.attrs.docId as string) as Doc);
@@ -365,6 +384,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
dataDoc[this.fieldKey] =
numstring !== undefined ? Number(newText) : newText || (DocCast(dataDoc.proto)?.[this.fieldKey] === undefined && this.layoutDoc[this.fieldKey] === undefined) ? new RichTextField(newJson, newText) : undefined;
textChange && ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.Document, text: newText });
+ if (textChange) this.dataDoc.$tags_chat = undefined;
this.ApplyingChange = ''; // turning this off here allows a Doc to retrieve data from template if noTemplate below is changed to false
unchanged = false;
}
@@ -425,10 +445,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
const newAutoLinks = new Set<Doc>();
const oldAutoLinks = Doc.Links(this.Document).filter(
link =>
- ((!Doc.isTemplateForField(this.Document) &&
- ((DocCast(link.link_anchor_1) && !Doc.isTemplateForField(DocCast(link.link_anchor_1)!)) || !Doc.AreProtosEqual(DocCast(link.link_anchor_1), this.Document)) &&
- ((DocCast(link.link_anchor_2) && !Doc.isTemplateForField(DocCast(link.link_anchor_2)!)) || !Doc.AreProtosEqual(DocCast(link.link_anchor_2), this.Document))) ||
- (Doc.isTemplateForField(this.Document) && (link.link_anchor_1 === this.Document || link.link_anchor_2 === this.Document))) &&
+ ((!Doc.IsTemplateForField(this.Document) &&
+ ((DocCast(link.link_anchor_1) && !Doc.IsTemplateForField(DocCast(link.link_anchor_1)!)) || !Doc.AreProtosEqual(DocCast(link.link_anchor_1), this.Document)) &&
+ ((DocCast(link.link_anchor_2) && !Doc.IsTemplateForField(DocCast(link.link_anchor_2)!)) || !Doc.AreProtosEqual(DocCast(link.link_anchor_2), this.Document))) ||
+ (Doc.IsTemplateForField(this.Document) && (link.link_anchor_1 === this.Document || link.link_anchor_2 === this.Document))) &&
link.link_relationship === LinkManager.AutoKeywords
); // prettier-ignore
if (this.EditorView?.state.doc.textContent) {
@@ -1068,17 +1088,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
return anchorDoc ?? this.Document;
}
+ showBorderRounding = returnTrue;
getView = (doc: Doc, options: FocusViewOptions) => {
if (DocListCast(this.dataDoc[this.sidebarKey]).find(anno => Doc.AreProtosEqual(doc.layout_unrendered ? DocCast(doc.annotationOn) : doc, anno))) {
- if (!this.SidebarShown) {
- this.toggleSidebar(false);
- options.didMove = true;
- }
- setTimeout(() => this._sidebarRef?.current?.makeDocUnfiltered(doc));
+ return SidebarAnnos.getView(this._sidebarRef.current, this.SidebarShown, () => this.toggleSidebar(false), doc, options);
}
- return new Promise<Opt<DocumentView>>(res => {
- DocumentView.addViewRenderedCb(doc, dv => res(dv));
- });
+ return new Promise<Opt<DocumentView>>(res => DocumentView.addViewRenderedCb(doc, res));
};
focus = (textAnchor: Doc, options: FocusViewOptions) => {
const focusSpeed = options.zoomTime ?? 500;
@@ -1145,13 +1160,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
return undefined;
};
- // if the scroll height has changed and we're in layout_autoHeight mode, then we need to update the textHeight component of the doc.
- // Since we also monitor all component height changes, this will update the document's height.
- resetNativeHeight = action((scrollHeight: number) => {
- this.layoutDoc['_' + this.fieldKey + '_height'] = scrollHeight;
- if (!this.layoutDoc.isTemplateForField && NumCast(this.layoutDoc._nativeHeight)) this.layoutDoc._nativeHeight = scrollHeight;
- });
-
addPlugin = (plugin: Plugin) => {
const editorView = this.EditorView;
if (editorView) {
@@ -1182,21 +1190,31 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
this._disposers.width = reaction(this._props.PanelWidth, this.tryUpdateScrollHeight);
this._disposers.scrollHeight = reaction(
() => ({ scrollHeight: this.scrollHeight, layoutAutoHeight: this.layout_autoHeight, width: NumCast(this.layoutDoc._width) }),
- ({ width, scrollHeight, layoutAutoHeight }) => width && layoutAutoHeight && this.resetNativeHeight(scrollHeight),
+ ({ width, scrollHeight, layoutAutoHeight }) => width && layoutAutoHeight && (this.layoutDoc['_' + this.fieldKey + '_height'] = scrollHeight),
{ fireImmediately: true }
);
this._disposers.componentHeights = reaction(
// set the document height when one of the component heights changes and layout_autoHeight is on
- () => ({ border: this._props.PanelHeight(), sidebarHeight: this.sidebarHeight, textHeight: this.textHeight, layoutAutoHeight: this.layout_autoHeight, marginsHeight: this.layout_autoHeightMargins }),
- ({ border, sidebarHeight, textHeight, layoutAutoHeight, marginsHeight }) => {
+ () => ({
+ border: this._props.PanelHeight(),
+ scrollHeight: NumCast(this.layoutDoc['_' + this.fieldKey + '_height']),
+ sidebarHeight: this.sidebarHeight,
+ textHeight: this.textHeight,
+ layoutAutoHeight: this.layout_autoHeight,
+ marginsHeight: this.layout_autoHeightMargins,
+ }),
+ ({ border, sidebarHeight, scrollHeight, textHeight, layoutAutoHeight, marginsHeight }) => {
const newHeight = this.contentScaling * (marginsHeight + Math.max(sidebarHeight, textHeight));
if (
(!Array.from(this._curHighlights).includes('Bold Text') || this._props.isSelected()) && //
layoutAutoHeight &&
newHeight &&
- (newHeight !== this.layoutDoc.height || border < NumCast(this.layoutDoc.height)) &&
+ (newHeight !== this.layoutDoc.height || border < NumCast(this.layoutDoc.height) || this.layoutDoc._nativeHeight !== scrollHeight) &&
!this._props.dontRegisterView
) {
+ if (NumCast(this.layoutDoc.nativeHeight)) {
+ this.layoutDoc._nativeHeight = scrollHeight;
+ }
this._props.setHeight?.(newHeight);
}
},
@@ -1219,7 +1237,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
const protoTime = protoData && this.dataDoc[this.fieldKey + '_autoUpdate'] ? (DateCast(DocCast(this.dataDoc.proto)?.[this.fieldKey + '_modificationDate'])?.date.getTime() ?? 0) : 0;
const recentData = dataTime >= layoutTime ? (protoTime >= dataTime ? protoData : dataData) : layoutTime >= protoTime ? layoutData : protoData;
const whichData = recentData ?? (this.layoutDoc.isTemplateDoc ? layoutData : protoData) ?? protoData;
- return !whichData ? undefined : { data: RTFCast(whichData), str: Field.toString(DocCast(whichData) ?? StrCast(whichData)) };
+ return !whichData ? undefined : { data: RTFCast(whichData), str: Field.toString(DocCast(whichData) ?? NumCast(whichData)?.toString() ?? StrCast(whichData)) };
},
incomingValue => {
if (this.EditorView && this.ApplyingChange !== this.fieldKey) {
@@ -1525,7 +1543,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
}
if (this.EditorView && selectOnLoad && !this._props.dontRegisterView && !this._props.dontSelectOnLoad && this.isActiveTab(this.ProseRef)) {
const { $from } = this.EditorView.state.selection;
- const mark = schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified: Math.floor(Date.now() / 1000) });
+ const mark = schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail() });
const curMarks = this.EditorView.state.storedMarks ?? $from?.marksAcross(this.EditorView.state.selection.$head) ?? [];
const storedMarks = [...curMarks.filter(m => m.type !== mark.type), mark];
if (selLoadChar === 'Enter') {
@@ -1536,6 +1554,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
this.tryUpdateDoc(true); // calling select() above will make isContentActive() true only after a render .. which means the selectAll() above won't write to the Document and the incomingValue will overwrite the selection with the non-updated data
}
if (selectOnLoad) {
+ this._liveTextUndo = FormattedTextBox.LiveTextUndo;
+ FormattedTextBox.LiveTextUndo = undefined;
this.EditorView!.focus();
}
if (this._props.isContentActive()) this.prepareForTyping();
@@ -1552,7 +1572,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
const { text, paragraph } = schema.nodes;
const selNode = this.EditorView.state.selection.$anchor.node();
if (this.EditorView.state.selection.from === 1 && this.EditorView.state.selection.empty && [undefined, text, paragraph].includes(selNode?.type)) {
- const docDefaultMarks = [schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified: Math.floor(Date.now() / 1000) })];
+ const docDefaultMarks = [schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail() })];
this.EditorView.state.selection.empty && this.EditorView.state.selection.from === 1 && this.EditorView?.dispatch(this.EditorView?.state.tr.setStoredMarks(docDefaultMarks).removeStoredMark(schema.marks.pFontColor));
}
}
@@ -1565,8 +1585,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
removeStyleSheet(this._userStyleSheetElement);
Object.values(this._disposers).forEach(disposer => disposer?.());
this.endUndoTypingBatch();
- FormattedTextBox.LiveTextUndo?.end();
- FormattedTextBox.LiveTextUndo = undefined;
+ this._liveTextUndo?.end();
this.unhighlightSearchTerms();
this.EditorView?.destroy();
RichTextMenu.Instance?.TextView === this && RichTextMenu.Instance.updateMenu(undefined, undefined, undefined, undefined);
@@ -1688,7 +1707,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
if (e.clientX > boundsRect.left && e.clientX < boundsRect.right && e.clientY > boundsRect.bottom) {
// if we clicked below the last prosemirror div, then set the selection to be the end of the document
editorView.focus();
- editorView.dispatch(editorView.state.tr.setSelection(TextSelection.create(editorView.state.doc, editorView.state.doc.content.size)));
+ // editorView.dispatch(editorView.state.tr.setSelection(TextSelection.create(editorView.state.doc, editorView.state.doc.content.size)));
}
} else if (node && [editorView.state.schema.nodes.ordered_list, editorView.state.schema.nodes.listItem].includes(node.type) && node !== (editorView.state.selection as NodeSelection)?.node && pcords) {
editorView.dispatch(editorView.state.tr.setSelection(NodeSelection.create(editorView.state.doc, pcords.pos)));
@@ -1770,7 +1789,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
if (this.ProseRef?.children[0] !== e.nativeEvent.target) return;
if (!(this.EditorView?.state.selection instanceof NodeSelection) || this.EditorView.state.selection.node.type !== this.EditorView.state.schema.nodes.footnote) {
const stordMarks = this.EditorView?.state.storedMarks?.slice();
- if (!(this.EditorView?.state.selection instanceof NodeSelection)) {
+ if (!(this.EditorView?.state.selection instanceof NodeSelection) && typeof this.dataDoc[this.fieldKey] !== 'number') {
this.autoLink();
if (this.EditorView?.state.tr) {
const tr = stordMarks?.reduce((tr2, m) => {
@@ -1795,8 +1814,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
this.endUndoTypingBatch();
- FormattedTextBox.LiveTextUndo?.end();
- FormattedTextBox.LiveTextUndo = undefined;
+ this._liveTextUndo?.end();
// if the text box blurs and none of its contents are focused(), then pass the blur along
setTimeout(() => !this.ProseRef?.contains(document.activeElement) && this._props.onBlur?.());
@@ -1817,7 +1835,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
return;
}
if (this._enteringStyle && 'tix!'.includes(e.key)) {
- const tag = e.key === 't' ? 'todo' : e.key === 'i' ? 'ignore' : e.key === 'x' ? 'disagree' : e.key === '!' ? 'important' : '??';
const node = state.selection.$from.nodeAfter;
const start = state.selection.from;
const end = state.selection.to;
@@ -1826,9 +1843,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
StopEvent(e);
_editorView.dispatch(
state.tr
- .removeMark(start, end, schema.marks.user_mark)
- .addMark(start, end, schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified: Math.floor(Date.now() / 1000) }))
- .addMark(start, end, schema.marks.user_tag.create({ userid: ClientUtils.CurrentUserEmail(), tag, modified: Math.round(Date.now() / 1000 / 60) }))
+ .removeMark(start, end, schema.marks.user_mark) //
+ .addMark(start, end, schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail() }))
);
return;
}
@@ -1861,9 +1877,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
break;
default:
if ([AclEdit, AclAugment, AclAdmin].includes(GetEffectiveAcl(this.Document))) {
- const modified = Math.floor(Date.now() / 1000);
- const mark = state.selection.$to.marks().find(m => m.type === schema.marks.user_mark && m.attrs.modified === modified);
- _editorView.dispatch(state.tr.removeStoredMark(schema.marks.user_mark).addStoredMark(mark ?? schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified })));
+ const mark = state.selection.$to.marks().find(m => m.type === schema.marks.user_mark);
+ _editorView.dispatch(
+ state.tr
+ .removeStoredMark(schema.marks.user_mark) //
+ .addStoredMark(mark ?? schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail() }))
+ );
}
break;
}
@@ -1887,14 +1906,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
const margins = 2 * NumCast(this.layoutDoc._yMargin, this._props.yMargin || 0);
const children = this.ProseRef?.children.length ? Array.from(this.ProseRef.children[0].children) : undefined;
if (this.EditorView && children && !SnappingManager.IsDragging) {
- const getChildrenHeights = (kids: Element[] | undefined) => kids?.reduce((p, child) => p + toHgt(child), margins) ?? 0;
+ const getChildrenHeights = (kids: Element[] | undefined) => kids?.reduce((p, child) => p + toHgt(child), 0) ?? 0;
const toNum = (val: string) => Number(val.replace('px', ''));
const toHgt = (node: Element): number => {
const { height, marginTop, marginBottom } = getComputedStyle(node);
const childHeight = height === 'auto' ? getChildrenHeights(Array.from(node.children)) : toNum(height);
return childHeight + Math.max(0, toNum(marginTop)) + Math.max(0, toNum(marginBottom));
};
- const proseHeight = !this.ProseRef ? 0 : getChildrenHeights(children);
+ const proseHeight = !this.ProseRef ? 0 : getChildrenHeights(children) + margins;
const scrollHeight = this.ProseRef && proseHeight;
if (this._props.setHeight && !this._props.suppressSetHeight && scrollHeight && !this._props.dontRegisterView) {
// if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation
@@ -2110,6 +2129,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
}
};
+ setRef = (r: HTMLDivElement | null) => this.fixWheelEvents(r, this._props.isContentActive, this.onPassiveWheel);
+ setScrollRef = (r: HTMLDivElement | null) => (this._scrollRef = r);
render() {
TraceMobx();
const scale = this.nativeScaling();
@@ -2124,7 +2145,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
) : styleFromLayout?.height === '0px' ? null : (
<div
className="formattedTextBox"
- ref={r => this.fixWheelEvents(r, this._props.isContentActive, this.onPassiveWheel)}
+ ref={this.setRef}
style={{
...(this._props.dontScale
? {}
@@ -2162,9 +2183,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
onDoubleClick={this.onDoubleClick}>
<div
className="formattedTextBox-outer"
- ref={r => {
- this._scrollRef = r;
- }}
+ ref={this.setScrollRef}
style={{
width: this.noSidebar ? '100%' : `calc(100% - ${this.sidebarWidthPercent})`,
overflow: this.layoutDoc._createDocOnCR || this.layoutDoc._layout_hideScroll ? 'hidden' : this.layout_autoHeight ? 'visible' : undefined,
diff --git a/src/client/views/nodes/formattedText/FormattedTextBoxComment.scss b/src/client/views/nodes/formattedText/FormattedTextBoxComment.scss
index bc0810f22..92f3e3290 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBoxComment.scss
+++ b/src/client/views/nodes/formattedText/FormattedTextBoxComment.scss
@@ -11,8 +11,8 @@
-webkit-transform: translateX(-50%);
transform: translateX(-50%);
box-shadow: 3px 3px 1.5px grey;
- max-width: 400;
- max-height: 235;
+ max-width: 400px;
+ max-height: 235px;
height: max-content;
.formattedTextBox-tooltipText {
height: max-content;
@@ -22,26 +22,26 @@
.formattedTextBox-tooltip:before {
content: '';
- height: 0;
- width: 0;
+ height: 0px;
+ width: 0px;
position: absolute;
left: 50%;
margin-left: -5px;
bottom: -6px;
border: 5px solid transparent;
- border-bottom-width: 0;
+ border-bottom-width: 0px;
border-top-color: silver;
}
.formattedTextBox-tooltip:after {
content: '';
- height: 0;
- width: 0;
+ height: 0px;
+ width: 0px;
position: absolute;
left: 50%;
margin-left: -5px;
bottom: -4.5px;
border: 5px solid transparent;
- border-bottom-width: 0;
+ border-bottom-width: 0px;
border-top-color: white;
}
diff --git a/src/client/views/nodes/formattedText/RichTextMenu.scss b/src/client/views/nodes/formattedText/RichTextMenu.scss
index fcc816447..7c747de1e 100644
--- a/src/client/views/nodes/formattedText/RichTextMenu.scss
+++ b/src/client/views/nodes/formattedText/RichTextMenu.scss
@@ -23,7 +23,7 @@
.dropdown {
position: absolute;
top: 35px;
- left: 0;
+ left: 0px;
background-color: #323232;
color: global.$light-gray;
border: 1px solid #4d4d4d;
@@ -47,7 +47,7 @@
}
&:last-child {
- margin-bottom: 0;
+ margin-bottom: 0px;
}
}
}
diff --git a/src/client/views/nodes/formattedText/TooltipTextMenu.scss b/src/client/views/nodes/formattedText/TooltipTextMenu.scss
index 87320943d..8980a93a2 100644
--- a/src/client/views/nodes/formattedText/TooltipTextMenu.scss
+++ b/src/client/views/nodes/formattedText/TooltipTextMenu.scss
@@ -195,7 +195,7 @@
left: 1px;
width: 24px;
height: 4px;
- margin-top: 0;
+ margin-top: 0px;
}
}
@@ -221,7 +221,7 @@
display: inline-block;
width: 1em;
height: 1em;
- stroke-width: 0;
+ stroke-width: 0px;
stroke: currentColor;
fill: currentColor;
margin-right: 15px;
@@ -231,7 +231,7 @@
display: inline-block;
width: 1em;
height: 1em;
- stroke-width: 3;
+ stroke-width: 3px;
fill: greenyellow;
margin-right: 15px;
}
@@ -270,7 +270,7 @@
&.ProseMirror-menu-dropdown {
width: 10px;
height: 25px;
- margin: 0;
+ margin: 0px;
padding: 0 2px;
background-color: #323232;
text-align: center;
diff --git a/src/client/views/nodes/formattedText/marks_rts.ts b/src/client/views/nodes/formattedText/marks_rts.ts
index b7dae1ca3..333ee6be8 100644
--- a/src/client/views/nodes/formattedText/marks_rts.ts
+++ b/src/client/views/nodes/formattedText/marks_rts.ts
@@ -330,20 +330,6 @@ export const marks: { [index: string]: MarkSpec } = {
return ['span', { class: 'UM-' + uid + remote + ' UM-min-' + min + ' UM-hr-' + hr + ' UM-day-' + day }, 0];
},
},
- // the id of the user who entered the text
- user_tag: {
- attrs: {
- userid: { default: '' },
- modified: { default: 'when?' }, // 1 second intervals since 1970
- tag: { default: '' },
- },
- group: 'inline',
- inclusive: false,
- toDOM: node => {
- const uid = node.attrs.userid.replace('.', '').replace('@', '');
- return ['span', { class: 'UT-' + uid + ' UT-' + node.attrs.tag }, 0];
- },
- },
// :: MarkSpec Code font mark. Represented as a `<code>` element.
code: {
diff --git a/src/client/views/nodes/imageEditor/ImageEditor.scss b/src/client/views/nodes/imageEditor/ImageEditor.scss
index c691e6a18..942a7d4c6 100644
--- a/src/client/views/nodes/imageEditor/ImageEditor.scss
+++ b/src/client/views/nodes/imageEditor/ImageEditor.scss
@@ -4,8 +4,8 @@ $scale: 0.5;
.imageEditorContainer {
position: absolute;
- top: 0;
- left: 0;
+ top: 0px;
+ left: 0px;
z-index: 9999;
height: 100vh;
width: 100vw;
@@ -78,7 +78,7 @@ $scale: 0.5;
.sideControlsContainer {
width: 160px;
position: absolute;
- left: 0;
+ left: 0px;
height: 100%;
.sideControls {
@@ -129,8 +129,8 @@ $scale: 0.5;
.originalImageLabel {
position: absolute;
- bottom: 10;
- left: 10;
+ bottom: 10px;
+ left: 10px;
color: #ffffff;
font-size: 0.8rem;
letter-spacing: 1px;
diff --git a/src/client/views/nodes/scrapbook/AIPresetGenerator.ts b/src/client/views/nodes/scrapbook/AIPresetGenerator.ts
new file mode 100644
index 000000000..1f159222b
--- /dev/null
+++ b/src/client/views/nodes/scrapbook/AIPresetGenerator.ts
@@ -0,0 +1,31 @@
+import { ScrapbookItemConfig } from './ScrapbookPreset';
+import { GPTCallType, gptAPICall } from '../../../apis/gpt/GPT';
+
+// Represents the descriptor for each document
+export interface DocumentDescriptor {
+ type: string;
+ tags: string[];
+}
+
+// Main function to request AI-generated presets
+export async function requestAiGeneratedPreset(descriptors: DocumentDescriptor[]): Promise<ScrapbookItemConfig[]> {
+ const prompt = createPrompt(descriptors);
+ let aiResponse = await gptAPICall(prompt, GPTCallType.GENERATESCRAPBOOK);
+ // Strip out ```json and ``` if the model wrapped its answer in fences
+ aiResponse = aiResponse
+ .trim()
+ .replace(/^```(?:json)?\s*/, "") // remove leading ``` or ```json
+ .replace(/\s*```$/, ""); // remove trailing ```
+ const parsedPreset = JSON.parse(aiResponse) as ScrapbookItemConfig[];
+ return parsedPreset;
+}
+
+// Helper to generate prompt text for AI
+function createPrompt(descriptors: DocumentDescriptor[]): string {
+ let prompt = "";
+ descriptors.forEach((desc, index) => {
+ prompt += `${index + 1}. Type: ${desc.type}, Tags: ${desc.tags.join(', ')}\n`;
+ });
+
+ return prompt;
+}
diff --git a/src/client/views/nodes/scrapbook/EmbeddedDocView.tsx b/src/client/views/nodes/scrapbook/EmbeddedDocView.tsx
deleted file mode 100644
index e99bf67c7..000000000
--- a/src/client/views/nodes/scrapbook/EmbeddedDocView.tsx
+++ /dev/null
@@ -1,52 +0,0 @@
-//IGNORE FOR NOW, CURRENTLY NOT USED IN SCRAPBOOK IMPLEMENTATION
-import * as React from "react";
-import { observer } from "mobx-react";
-import { Doc } from "../../../../fields/Doc";
-import { DocumentView } from "../DocumentView";
-import { Transform } from "../../../util/Transform";
-
-interface EmbeddedDocViewProps {
- doc: Doc;
- width?: number;
- height?: number;
- slotId?: string;
-}
-
-@observer
-export class EmbeddedDocView extends React.Component<EmbeddedDocViewProps> {
- render() {
- const { doc, width = 300, height = 200, slotId } = this.props;
-
- // Use either an existing embedding or create one
- let docToDisplay = doc;
-
- // If we need an embedding, create or use one
- if (!docToDisplay.isEmbedding) {
- docToDisplay = Doc.BestEmbedding(doc) || Doc.MakeEmbedding(doc);
- // Set the container to the slot's ID so we can track it
- if (slotId) {
- docToDisplay.embedContainer = `scrapbook-slot-${slotId}`;
- }
- }
-
- return (
- <DocumentView
- Document={docToDisplay}
- renderDepth={0}
- // Required sizing functions
- NativeWidth={() => width}
- NativeHeight={() => height}
- PanelWidth={() => width}
- PanelHeight={() => height}
- // Required state functions
- isContentActive={() => true}
- childFilters={() => []}
- ScreenToLocalTransform={() => new Transform()}
- // Display options
- hideDeleteButton={true}
- hideDecorations={true}
- hideResizeHandles={true}
- />
- );
- }
-} \ No newline at end of file
diff --git a/src/client/views/nodes/scrapbook/ScrapbookBox.scss b/src/client/views/nodes/scrapbook/ScrapbookBox.scss
new file mode 100644
index 000000000..6ac2220f9
--- /dev/null
+++ b/src/client/views/nodes/scrapbook/ScrapbookBox.scss
@@ -0,0 +1,66 @@
+.scrapbook-box {
+ /* Make sure the container fills its parent, and set a base background */
+ position: relative; /* so that absolute children (loading overlay, etc.) are positioned relative to this */
+ width: 100%;
+ height: 100%;
+ background: beige;
+ overflow: hidden; /* prevent scrollbars if children overflow */
+}
+
+/* Loading overlay that covers the entire scrapbook while AI-generation is in progress */
+.scrapbook-box-loading-overlay {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ background: rgba(255, 255, 255, 0.8);
+ z-index: 10; /* sits above the ImageBox and other content */
+}
+
+/* The <select> dropdown for choosing presets */
+.scrapbook-box-preset-select {
+ position: relative;
+ top: 8px;
+ left: 8px;
+ z-index: 20;
+ padding: 4px 8px;
+ font-size: 14px;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ background: white;
+}
+
+/* Container for the “Regenerate Background” button */
+.scrapbook-box-ui {
+ position: relative;
+ top: 8px;
+ right: 8px;
+ z-index: 20;
+ background: white;
+ width: 40px;
+ display: flex;
+ justify-content: center;
+}
+
+/* The button itself */
+.scrapbook-box-ui-button {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 4px 8px;
+ font-size: 14px;
+ color: black;
+ background: white;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ cursor: pointer;
+ white-space: nowrap;
+}
+
+.scrapbook-box-ui-button:hover {
+ background: #f5f5f5;
+}
diff --git a/src/client/views/nodes/scrapbook/ScrapbookBox.tsx b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx
index 6cfe9a62c..d0ae6194f 100644
--- a/src/client/views/nodes/scrapbook/ScrapbookBox.tsx
+++ b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx
@@ -1,130 +1,260 @@
-import { action, makeObservable, observable } from 'mobx';
+import { IconButton, Size } from '@dash/components';
+import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx';
+import { observer } from 'mobx-react';
import * as React from 'react';
-import { Doc, DocListCast } from '../../../../fields/Doc';
+import ReactLoading from 'react-loading';
+import { Doc, DocListCast, Opt, StrListCast } from '../../../../fields/Doc';
import { List } from '../../../../fields/List';
+import { DateCast, DocCast, NumCast, toList } from '../../../../fields/Types';
import { emptyFunction } from '../../../../Utils';
import { Docs } from '../../../documents/Documents';
import { DocumentType } from '../../../documents/DocumentTypes';
+import { DragManager } from '../../../util/DragManager';
+import { SnappingManager } from '../../../util/SnappingManager';
+import { undoable } from '../../../util/UndoManager';
import { CollectionView } from '../../collections/CollectionView';
import { ViewBoxAnnotatableComponent } from '../../DocComponent';
+import { AspectRatioLimits, FireflyImageDimensions } from '../../smartdraw/FireflyConstants';
+import { SmartDrawHandler } from '../../smartdraw/SmartDrawHandler';
import { DocumentView } from '../DocumentView';
import { FieldView, FieldViewProps } from '../FieldView';
-import { DragManager } from '../../../util/DragManager';
-import { RTFCast, StrCast, toList } from '../../../../fields/Types';
-import { undoable } from '../../../util/UndoManager';
-// Scrapbook view: a container that lays out its child items in a grid/template
-export class ScrapbookBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
- @observable createdDate: string;
+import { ImageBox } from '../ImageBox';
+import './ScrapbookBox.scss';
+import { ScrapbookItemConfig } from './ScrapbookPreset';
+import { createPreset, getPresetNames } from './ScrapbookPresetRegistry';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { DocUtils } from '../../../documents/DocUtils';
+import { returnTrue } from '../../../../ClientUtils';
- constructor(props: FieldViewProps) {
- super(props);
- makeObservable(this);
- this.createdDate = this.getFormattedDate();
+function createPlaceholder(cfg: ScrapbookItemConfig, doc: Doc) {
+ const placeholder = new Doc();
+ placeholder.proto = doc;
+ placeholder.original = doc;
+ placeholder.x = cfg.x;
+ placeholder.y = cfg.y;
+ if (cfg.width !== null) placeholder._width = cfg.width;
+ if (cfg.height !== null) placeholder._height = cfg.height;
+ return placeholder;
+}
- // ensure we always have a List<Doc> in dataDoc['items']
- if (!this.dataDoc[this.fieldKey]) {
- this.dataDoc[this.fieldKey] = new List<Doc>();
+function createMessagePlaceholder(cfg: ScrapbookItemConfig) {
+ return createPlaceholder(cfg,
+ Docs.Create.TextDocument(cfg.message ?? ('[placeholder] ' + cfg.acceptTags?.[0]), { placeholder: "", placeholder_docType: cfg.type, placeholder_acceptTags: new List<string>(cfg.acceptTags) })
+ ); // prettier-ignore
+}
+export function buildPlaceholdersFromConfigs(configs: ScrapbookItemConfig[]) {
+ return configs.map(cfg => {
+ if (cfg.children?.length) {
+ const childDocs = cfg.children.map(createMessagePlaceholder);
+ const protoW = cfg.containerWidth ?? cfg.width;
+ const protoH = cfg.containerHeight ?? cfg.height;
+ // Create a stacking document with the child placeholders
+ const containerProto = Docs.Create.StackingDocument(childDocs, {
+ ...(protoW !== null ? { _width: protoW } : {}),
+ ...(protoH !== null ? { _height: protoH } : {}),
+ title: cfg.message,
+ });
+ return createPlaceholder(cfg, containerProto);
}
- this.createdDate = this.getFormattedDate();
- this.setTitle();
+ return createMessagePlaceholder(cfg);
+ });
+}
+export async function slotRealDocIntoPlaceholders(realDoc: Doc, placeholders: Doc[]) {
+ if (!realDoc.$tags_chart) {
+ await DocumentView.getFirstDocumentView(realDoc)?.ComponentView?.autoTag?.();
}
+ const realTags = new Set<string>(StrListCast(realDoc.$tags_chat).map(t => t.toLowerCase?.() ?? ''));
+ // Find placeholder with most matching tags
+ let bestMatch: Doc | null = null;
+ let maxMatches = 0;
+
+ // match fields based on type, or by analyzing content .. simple example of matching text in placeholder to dropped doc's type
+ placeholders
+ .filter(ph => ph.placeholder_docType === realDoc.$type) // Skip this placeholder entirely if types do not match.
+ .forEach(ph => {
+ const matches = StrListCast(ph.placeholder_acceptTags)
+ .map(t => t.toLowerCase?.())
+ .filter(tag => realTags.has(tag));
+
+ if (matches.length > maxMatches) {
+ maxMatches = matches.length;
+ bestMatch = ph;
+ }
+ });
+
+ if (bestMatch && maxMatches > 0) {
+ setTimeout(undoable(() => (bestMatch!.proto = realDoc), 'Scrapbook add'));
+ return true;
+ }
+
+ return false;
+}
+
+// Scrapbook view: a container that lays out its child items in a template
+@observer
+export class ScrapbookBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
public static LayoutString(fieldStr: string) {
return FieldView.LayoutString(ScrapbookBox, fieldStr);
}
+ private _disposers: { [name: string]: IReactionDisposer } = {};
+ private _imageBoxRef = React.createRef<ImageBox>();
+
+ constructor(props: FieldViewProps) {
+ super(props);
+ makeObservable(this);
+ }
+
+ @observable _selectedPreset = getPresetNames()[0];
+ @observable _loading = false;
- getFormattedDate(): string {
- return new Date().toLocaleDateString(undefined, {
+ @computed get createdDate() {
+ return DateCast(this.dataDoc.$author_date)?.date.toLocaleDateString(undefined, {
year: 'numeric',
month: 'short',
day: 'numeric',
});
}
+ @computed get ScrapbookLayoutDocs() { return DocListCast(this.dataDoc[this.fieldKey]); } // prettier-ignore
+ @computed get BackgroundDoc() { return DocCast(this.dataDoc[this.fieldKey + '_background']); } // prettier-ignore
+ set ScrapbookLayoutDocs(doc: Doc[]) { this.dataDoc[this.fieldKey] = new List(doc); } // prettier-ignore
+ set BackgroundDoc(doc: Opt<Doc>) { this.dataDoc[this.fieldKey + '_background'] = doc; } // prettier-ignore
@action
- setTitle() {
- const title = `Scrapbook - ${this.createdDate}`;
- if (this.dataDoc.title !== title) {
- this.dataDoc.title = title;
-
- const image = Docs.Create.TextDocument('image');
- image.accepts_docType = DocumentType.IMG;
- const placeholder = new Doc();
- placeholder.proto = image;
- placeholder.original = image;
- placeholder._width = 250;
- placeholder._height = 200;
- placeholder.x = 0;
- placeholder.y = -100;
- //placeholder.overrideFields = new List<string>(['x', 'y']); // shouldn't need to do this for layout fields since the placeholder already overrides its protos
-
- const summary = Docs.Create.TextDocument('summary');
- summary.accepts_docType = DocumentType.RTF;
- summary.accepts_textType = 'one line';
- const placeholder2 = new Doc();
- placeholder2.proto = summary;
- placeholder2.original = summary;
- placeholder2.x = 0;
- placeholder2.y = 200;
- placeholder2._width = 250;
- //placeholder2.overrideFields = new List<string>(['x', 'y', '_width']); // shouldn't need to do this for layout fields since the placeholder already overrides its protos
- this.dataDoc[this.fieldKey] = new List<Doc>([placeholder, placeholder2]);
- }
- }
+ setDefaultPlaceholder = () => {
+ this.ScrapbookLayoutDocs = [
+ createMessagePlaceholder({
+ message: 'To create a scrapbook from existing documents, marquee select. For existing scrapbook arrangements, select a preset from the dropdown.',
+ type: DocumentType.RTF,
+ width: 250,
+ height: 200,
+ x: 0,
+ y: 0,
+ }),
+ ];
+
+ const placeholder1 = createMessagePlaceholder({ acceptTags: ['PERSON'], type: DocumentType.IMG, width: 250, height: 200, x: 0, y: -100 });
+ const placeholder2 = createMessagePlaceholder({ acceptTags: ['lengthy description'], type: DocumentType.RTF, width: 250, height: undefined, x: 0, y: 200 });
+ const placeholder3 = createMessagePlaceholder({ acceptTags: ['title'], type: DocumentType.RTF, width: 50, height: 200, x: 280, y: -50 });
+ const placeholder4 = createPlaceholder( { width: 100, height: 200, x: -200, y: -100 }, Docs.Create.StackingDocument([
+ createMessagePlaceholder({ acceptTags: ['LANDSCAPE'], type: DocumentType.IMG, width: 50, height: 100, x: 0, y: -100 })
+ ], { _width: 300, _height: 300, title: 'internal coll' })); // prettier-ignore
+ console.log('UNUSED', placeholder4, placeholder3, placeholder2, placeholder1);
+ /* note-to-self
+ would doing:
+ const collection = Docs.Create.ScrapbookDocument([placeholder, placeholder2, placeholder3]);
+ create issues with references to the same object? */
+
+ /*note-to-self
+ Should we consider that there are more collections than just COL type collections?
+ when spreading */
+
+ /*note-to-self
+ difference between passing a new List<Doc> versus just the raw array? */
+ };
+
+ selectPreset = action((presetName: string) => (this.ScrapbookLayoutDocs = buildPlaceholdersFromConfigs(createPreset(presetName))));
componentDidMount() {
- this.setTitle();
+ const title = `Scrapbook - ${this.createdDate}`;
+ if (!this.ScrapbookLayoutDocs.length) this.setDefaultPlaceholder();
+ if (!this.BackgroundDoc) this.generateAiImage(this.regenPrompt);
+ if (this.dataDoc.title !== title) this.dataDoc.title = title; // ensure title is set
+
+ this._disposers.propagateResize = reaction(
+ () => ({ w: this.layoutDoc._width, h: this.layoutDoc._height }),
+ (dims, prev) => {
+ const imageBox = this._imageBoxRef.current;
+ // prev is undefined on the first run
+ if (prev && SnappingManager.ShiftKey && this.BackgroundDoc && imageBox) {
+ this.BackgroundDoc[imageBox.fieldKey + '_outpaintOriginalWidth'] = prev.w;
+ this.BackgroundDoc[imageBox.fieldKey + '_outpaintOriginalHeight'] = prev.h;
+ imageBox.layoutDoc._width = dims.w;
+ imageBox.layoutDoc._height = dims.h;
+ }
+ }
+ );
}
- childRejectDrop = (de: DragManager.DropEvent, subView?: DocumentView) => {
- return true; // disable dropping documents onto any child of the scrapbook.
- };
- rejectDrop = (de: DragManager.DropEvent, subView?: DocumentView) => {
- // Test to see if the dropped doc is dropped on an acceptable location (anywerhe? on a specific box).
- // const draggedDocs = de.complete.docDragData?.draggedDocuments;
- return false; // allow all Docs to be dropped onto scrapbook -- let filterAddDocument make the final decision.
+ isOutpaintable = returnTrue;
+ showBorderRounding = returnTrue;
+
+ @action
+ generateAiImage = (prompt: string) => {
+ this._loading = true;
+
+ const ratio = NumCast(this.layoutDoc._width, 1) / NumCast(this.layoutDoc._height, 1); // Measure the scrapbook’s current aspect
+ const choosePresetForDimensions = (() => { // Pick the Firefly preset that best matches the aspect ratio
+ if (ratio > AspectRatioLimits[FireflyImageDimensions.Widescreen]) return FireflyImageDimensions.Widescreen;
+ if (ratio > AspectRatioLimits[FireflyImageDimensions.Landscape]) return FireflyImageDimensions.Landscape;
+ if (ratio < AspectRatioLimits[FireflyImageDimensions.Portrait]) return FireflyImageDimensions.Portrait;
+ return FireflyImageDimensions.Square;
+ })(); // prettier-ignore
+
+ SmartDrawHandler.CreateWithFirefly(prompt, choosePresetForDimensions) // Call exactly the same CreateWithFirefly that ImageBox uses
+ .then(action(doc => {
+ if (doc instanceof Doc) {
+ this.BackgroundDoc = doc; // set the background image directly on the scrapbook
+ } else {
+ alert('Failed to generate document.');
+ }
+ }))
+ .catch(e => alert(`Generation error: ${e}`))
+ .finally(action(() => (this._loading = false))); // prettier-ignore
};
- filterAddDocument = (docIn: Doc | Doc[]) => {
- const docs = toList(docIn);
- if (docs?.length === 1) {
- const placeholder = DocListCast(this.dataDoc[this.fieldKey]).find(d =>
- (d.accepts_docType === docs[0].$type || // match fields based on type, or by analyzing content .. simple example of matching text in placeholder to dropped doc's type
- RTFCast(d[Doc.LayoutDataKey(d)])?.Text.includes(StrCast(docs[0].$type)))
- ); // prettier-ignore
-
- if (placeholder) {
- // ugh. we have to tell the underlying view not to add the Doc so that we can add it where we want it.
- // However, returning 'false' triggers an undo. so this settimeout is needed to make the assignment happen after the undo.
- setTimeout(
- undoable(() => {
- //StrListCast(placeholder.overrideFields).map(field => (docs[0][field] = placeholder[field])); // // shouldn't need to do this for layout fields since the placeholder already overrides its protos
- placeholder.proto = docs[0];
- }, 'Scrapbook add')
- );
- return false;
- }
- }
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ childRejectDrop = (de: DragManager.DropEvent, subView?: DocumentView) => true; // disable dropping documents onto any child of the scrapbook.
+
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ rejectDrop = (de: DragManager.DropEvent, subView?: DocumentView) => false; // allow all Docs to be dropped onto scrapbook -- let filterAddDocument make the final decision.
+
+ /**
+ * Filter function to determine if a document can be added to the scrapbook.
+ * This checks if the document matches any of the placeholder slots in the scrapbook.
+ * @param docs - The document(s) being added to the scrapbook.
+ * @returns true if the document can be added, false otherwise.
+ */
+ filterAddDocument = (docs: Doc | Doc[]) => {
+ toList(docs).forEach(doc => slotRealDocIntoPlaceholders(doc, DocUtils.unwrapPlaceholders(this.ScrapbookLayoutDocs)));
return false;
};
+ @computed get regenPrompt() {
+ const allDocs = DocUtils.unwrapPlaceholders(this.ScrapbookLayoutDocs); // find all non-collections in scrapbook (e.g., placeholder content docs)
+ const internalTagsSet = new Set<string>(allDocs.flatMap(doc => StrListCast(doc.$tags_chat).filter(tag => !tag.startsWith?.('ASPECT_'))));
+ const internalTags = Array.from(internalTagsSet).join(', ');
+
+ return internalTags ? `Create a new scrapbook background featuring: ${internalTags}` : 'A serene mountain landscape at sunrise, ultra-wide, pastel sky, abstract, scrapbook background';
+ }
+
render() {
return (
- <div style={{ background: 'beige', width: '100%', height: '100%' }}>
- <CollectionView
- {...this._props} //
- setContentViewBox={emptyFunction}
- rejectDrop={this.rejectDrop}
- childRejectDrop={this.childRejectDrop}
- filterAddDocument={this.filterAddDocument}
- />
- {/* <div style={{ border: '1px black', borderStyle: 'dotted', position: 'absolute', top: '50%', width: '100%', textAlign: 'center' }}>Drop an image here</div> */}
+ <div className="scrapbook-box">
+ <div style={{ display: this._loading ? undefined : 'none' }} className="scrapbook-box-loading-overlay">
+ <ReactLoading type="spin" width={50} height={50} />
+ </div>
+
+ {this.BackgroundDoc && <ImageBox ref={this._imageBoxRef} {...this._props} Document={this.BackgroundDoc} fieldKey="data" />}
+ <div style={{ display: this._props.isContentActive() ? 'flex' : 'none', alignItems: 'center', justifyContent: 'space-between', padding: '0 10px' }}>
+ <select className="scrapbook-box-preset-select" value={this._selectedPreset} onChange={action(e => this.selectPreset((this._selectedPreset = e.currentTarget.value)))}>
+ {getPresetNames().map(name => (
+ <option key={name} value={name}>
+ {name}
+ </option>
+ ))}
+ </select>
+ <div className="scrapbook-box-ui" style={{ opacity: this._loading ? 0.5 : 1 }}>
+ <IconButton size={Size.SMALL} tooltip="regenerate a new background" label="back-ground" icon={<FontAwesomeIcon icon="redo-alt" size="sm" />} onClick={() => !this._loading && this.generateAiImage(this.regenPrompt)} />
+ </div>
+ </div>
+
+ <CollectionView {...this._props} setContentViewBox={emptyFunction} rejectDrop={this.rejectDrop} childRejectDrop={this.childRejectDrop} filterAddDocument={this.filterAddDocument} />
</div>
);
}
}
-// Register scrapbook
Docs.Prototypes.TemplateMap.set(DocumentType.SCRAPBOOK, {
layout: { view: ScrapbookBox, dataField: 'items' },
options: {
diff --git a/src/client/views/nodes/scrapbook/ScrapbookContent.tsx b/src/client/views/nodes/scrapbook/ScrapbookContent.tsx
deleted file mode 100644
index ad1d308e8..000000000
--- a/src/client/views/nodes/scrapbook/ScrapbookContent.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import React from "react";
-import { observer } from "mobx-react-lite";
-// Import the Doc type from your actual module.
-import { Doc } from "../../../../fields/Doc";
-
-export interface ScrapbookContentProps {
- doc: Doc;
-}
-
-// A simple view that displays a document's title and content.
-// Adjust how you extract the text if your Doc fields are objects.
-export const ScrapbookContent: React.FC<ScrapbookContentProps> = observer(({ doc }) => {
- // If doc.title or doc.content are not plain strings, convert them.
- const titleText = doc.title ? doc.title.toString() : "Untitled";
- const contentText = doc.content ? doc.content.toString() : "No content available.";
-
- return (
- <div className="scrapbook-content">
- <h3>{titleText}</h3>
- <p>{contentText}</p>
- </div>
- );
-});
diff --git a/src/client/views/nodes/scrapbook/ScrapbookPreset.tsx b/src/client/views/nodes/scrapbook/ScrapbookPreset.tsx
new file mode 100644
index 000000000..a3405083b
--- /dev/null
+++ b/src/client/views/nodes/scrapbook/ScrapbookPreset.tsx
@@ -0,0 +1,94 @@
+import { DocumentType } from '../../../documents/DocumentTypes';
+
+export enum ScrapbookPresetType {
+ None = 'None',
+ Collage = 'Collage',
+ Spotlight = 'Spotlight',
+ Gallery = 'Gallery',
+ Default = 'Default',
+ Classic = 'Classic',
+}
+
+export interface ScrapbookItemConfig {
+ x: number;
+ y: number;
+
+ message?: string; // optional text to display instead of [placeholder] + acceptTags[0]
+ type?: DocumentType;
+ /** what this slot actually accepts (defaults to `tag`) */
+ acceptTags?: string[];
+ /** the frame this placeholder occupies */
+ width?: number;
+ height?: number;
+ /** if this is a container with children, use these for the proto’s own size */
+ containerWidth?: number;
+ containerHeight?: number;
+ children?: ScrapbookItemConfig[];
+}
+
+export class ScrapbookPreset {
+ static createPreset(presetType: ScrapbookPresetType): ScrapbookItemConfig[] {
+ switch (presetType) {
+ case ScrapbookPresetType.None: return ScrapbookPreset.createNonePreset();
+ case ScrapbookPresetType.Classic: return ScrapbookPreset.createClassicPreset();
+ case ScrapbookPresetType.Collage: return ScrapbookPreset.createCollagePreset();
+ case ScrapbookPresetType.Spotlight: return ScrapbookPreset.createSpotlightPreset();
+ case ScrapbookPresetType.Default: return ScrapbookPreset.createDefaultPreset();
+ case ScrapbookPresetType.Gallery: return ScrapbookPreset.createGalleryPreset();
+ default:
+ throw new Error(`Unknown preset type: ${presetType}`);
+ } // prettier-ignore
+ }
+
+ private static createNonePreset(): ScrapbookItemConfig[] {
+ return [{ message: 'To create a scrapbook from existing documents, marquee select. For existing scrapbook arrangements, select a preset from the dropdown.', type: DocumentType.RTF, acceptTags: [], x: 0, y: 0, width: 250, height: 200 }];
+ }
+
+ private static createClassicPreset(): ScrapbookItemConfig[] {
+ return [
+ { type: DocumentType.IMG, message: '[placeholder] landscape', acceptTags: ['LANDSCAPE'], x: 0, y: -100, width: 250, height: 200 },
+ { type: DocumentType.RTF, message: '[placeholder] lengthy caption', acceptTags: ['paragraphs'], x: 0, y: 138, width: 250, height: 172 },
+ { type: DocumentType.RTF, message: '[placeholder] brief description', acceptTags: ['sentence'], x: 280, y: -50, width: 50, height: 200 },
+ { type: DocumentType.IMG, message: '[placeholder] person', acceptTags: ['PERSON'], x: -200, y: -100, width: 167, height: 200 },
+ ];
+ }
+
+ private static createGalleryPreset(): ScrapbookItemConfig[] {
+ return [
+ { type: DocumentType.IMG, message: 'Gallery 1 <drop person images into the gallery!>', acceptTags: ['PERSON'], x: -150, y: -150, width: 150, height: 150 },
+ { type: DocumentType.IMG, message: 'Gallery 2', acceptTags: ['PERSON'], x: 0, y: -150, width: 150, height: 150 },
+ { type: DocumentType.IMG, message: 'Gallery 3', acceptTags: ['PERSON'], x: 150, y: -150, width: 150, height: 150 },
+ { type: DocumentType.IMG, message: 'Gallery 4', acceptTags: ['PERSON'], x: -150, y: 0, width: 150, height: 150 },
+ { type: DocumentType.IMG, message: 'Gallery 5', acceptTags: ['PERSON'], x: 0, y: 0, width: 150, height: 150 },
+ { type: DocumentType.IMG, message: 'Gallery 6', acceptTags: ['PERSON'], x: 150, y: 0, width: 150, height: 150 },
+ ];
+ }
+
+ private static createDefaultPreset(): ScrapbookItemConfig[] {
+ return [
+ { type: DocumentType.IMG, message: 'drop a landscape image', acceptTags: ['LANDSCAPE'], x: 44, y: -50, width: 200, height: 120 },
+ { type: DocumentType.PDF, message: 'summary pdf', acceptTags: ['word', 'sentence', 'paragraphs'], x: 45, y: 93, width: 184, height: 273 },
+ { type: DocumentType.RTF, message: 'sidebar text', acceptTags: ['paragraphs'], x: 250, y: -50, width: 100, height: 200 },
+ { containerWidth: 200, containerHeight: 425, x: -171, y: -54, width: 200, height: 425,
+ children: [{ type: DocumentType.IMG, message: 'drop a person image', acceptTags: ['PERSON'], x: -350, y: 200, width: 162, height: 137 }], },
+ ]; // prettier-ignore
+ }
+
+ private static createCollagePreset(): ScrapbookItemConfig[] {
+ return [
+ { type: DocumentType.IMG, message: 'landscape image', acceptTags: ['LANDSCAPE'], x: -174, y: 100, width: 160, height: 150 },
+ { type: DocumentType.IMG, message: 'person image', acceptTags: ['PERSON'], x: 0, y: 100, width: 150, height: 150 },
+ { type: DocumentType.RTF, message: 'caption', acceptTags: ['sentence'], x: -174, y: 50, width: 150, height: 40 },
+ { type: DocumentType.RTF, message: 'caption', acceptTags: ['sentence'], x: 0, y: 50, width: 150, height: 40 },
+ { type: DocumentType.RTF, message: 'lengthy description', acceptTags: ['paragraphs'], x: -180, y: -60, width: 350, height: 100 },
+ ]; // prettier-ignore
+ }
+
+ private static createSpotlightPreset(): ScrapbookItemConfig[] {
+ return [
+ { type: DocumentType.RTF, message: 'title text', acceptTags: ['word'], x: 0, y: -30, width: 300, height: 40 },
+ { type: DocumentType.IMG, message: 'drop a landscape image', acceptTags: ['LANDSCAPE'], x: 0, y: 20, width: 300, height: 200 },
+ { type: DocumentType.RTF, message: 'caption text', acceptTags: ['sentence'], x: 0, y: 230, width: 300, height: 50 },
+ ];
+ }
+}
diff --git a/src/client/views/nodes/scrapbook/ScrapbookPresetRegistry.ts b/src/client/views/nodes/scrapbook/ScrapbookPresetRegistry.ts
new file mode 100644
index 000000000..3a2189d00
--- /dev/null
+++ b/src/client/views/nodes/scrapbook/ScrapbookPresetRegistry.ts
@@ -0,0 +1,36 @@
+import { ScrapbookItemConfig } from './ScrapbookPreset';
+import { ScrapbookPresetType } from './ScrapbookPreset';
+
+type PresetGenerator = () => ScrapbookItemConfig[];
+
+// Internal map of preset name to generator
+const presetRegistry = new Map<string, PresetGenerator>();
+
+/**
+ * Register a new scrapbook preset under the given name.
+ */
+export function registerPreset(name: string, gen: PresetGenerator) {
+ presetRegistry.set(name, gen);
+}
+
+/**
+ * List all registered preset names.
+ */
+export function getPresetNames(): string[] {
+ return Array.from(presetRegistry.keys());
+}
+
+/**
+ * Create the config array for the named preset.
+ */
+export function createPreset(name: string): ScrapbookItemConfig[] {
+ const gen = presetRegistry.get(name);
+ if (!gen) throw new Error(`Unknown scrapbook preset: ${name}`);
+ return gen();
+}
+
+// ------------------------
+// Register built-in presets
+import { ScrapbookPreset } from './ScrapbookPreset';
+
+Object.keys(ScrapbookPresetType).forEach(key => registerPreset(key, () => ScrapbookPreset.createPreset(key as ScrapbookPresetType))); // pretter-ignore
diff --git a/src/client/views/nodes/scrapbook/ScrapbookSlot.scss b/src/client/views/nodes/scrapbook/ScrapbookSlot.scss
deleted file mode 100644
index ae647ad36..000000000
--- a/src/client/views/nodes/scrapbook/ScrapbookSlot.scss
+++ /dev/null
@@ -1,85 +0,0 @@
-//IGNORE FOR NOW, CURRENTLY NOT USED IN SCRAPBOOK IMPLEMENTATION
-.scrapbook-slot {
- position: absolute;
- background-color: rgba(245, 245, 245, 0.7);
- border: 2px dashed #ccc;
- border-radius: 5px;
- box-sizing: border-box;
- transition: all 0.2s ease;
- overflow: hidden;
-
- &.scrapbook-slot-over {
- border-color: #4a90e2;
- background-color: rgba(74, 144, 226, 0.1);
- }
-
- &.scrapbook-slot-filled {
- border-style: solid;
- border-color: rgba(0, 0, 0, 0.1);
- background-color: transparent;
-
- &.scrapbook-slot-over {
- border-color: #4a90e2;
- background-color: rgba(74, 144, 226, 0.1);
- }
- }
-
- .scrapbook-slot-empty {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 100%;
- height: 100%;
- }
-
- .scrapbook-slot-placeholder {
- text-align: center;
- color: #888;
- }
-
- .scrapbook-slot-title {
- font-weight: bold;
- margin-bottom: 5px;
- }
-
- .scrapbook-slot-instruction {
- font-size: 0.9em;
- font-style: italic;
- }
-
- .scrapbook-slot-content {
- width: 100%;
- height: 100%;
- position: relative;
- }
-
- .scrapbook-slot-controls {
- position: absolute;
- top: 5px;
- right: 5px;
- z-index: 10;
- opacity: 0;
- transition: opacity 0.2s ease;
-
- .scrapbook-slot-remove-btn {
- background-color: rgba(255, 255, 255, 0.8);
- border: 1px solid #ccc;
- border-radius: 50%;
- width: 20px;
- height: 20px;
- display: flex;
- align-items: center;
- justify-content: center;
- cursor: pointer;
- font-size: 10px;
-
- &:hover {
- background-color: rgba(255, 0, 0, 0.1);
- }
- }
- }
-
- &:hover .scrapbook-slot-controls {
- opacity: 1;
- }
-} \ No newline at end of file
diff --git a/src/client/views/nodes/scrapbook/ScrapbookSlot.tsx b/src/client/views/nodes/scrapbook/ScrapbookSlot.tsx
deleted file mode 100644
index 2c8f93778..000000000
--- a/src/client/views/nodes/scrapbook/ScrapbookSlot.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-
-//IGNORE FOR NOW, CURRENTLY NOT USED IN SCRAPBOOK IMPLEMENTATION
-export interface SlotDefinition {
- id: string;
- x: number; y: number;
- defaultWidth: number;
- defaultHeight: number;
- }
-
- export interface SlotContentMap {
- slotId: string;
- docId?: string;
- }
-
- export interface ScrapbookConfig {
- slots: SlotDefinition[];
- contents?: SlotContentMap[];
- }
-
- export const DEFAULT_SCRAPBOOK_CONFIG: ScrapbookConfig = {
- slots: [
- { id: "slot1", x: 10, y: 10, defaultWidth: 180, defaultHeight: 120 },
- { id: "slot2", x: 200, y: 10, defaultWidth: 180, defaultHeight: 120 },
- // …etc
- ],
- contents: []
- };
- \ No newline at end of file
diff --git a/src/client/views/nodes/scrapbook/ScrapbookSlotTypes.ts b/src/client/views/nodes/scrapbook/ScrapbookSlotTypes.ts
deleted file mode 100644
index 686917d9a..000000000
--- a/src/client/views/nodes/scrapbook/ScrapbookSlotTypes.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-// ScrapbookSlotTypes.ts
-export interface SlotDefinition {
- id: string;
- title: string;
- x: number;
- y: number;
- defaultWidth: number;
- defaultHeight: number;
- }
-
- export interface ScrapbookConfig {
- slots: SlotDefinition[];
- contents?: { slotId: string; docId: string }[];
- }
-
- // give it three slots by default:
- export const DEFAULT_SCRAPBOOK_CONFIG: ScrapbookConfig = {
- slots: [
- { id: "main", title: "Main Content", x: 20, y: 20, defaultWidth: 360, defaultHeight: 200 },
- { id: "notes", title: "Notes", x: 20, y: 240, defaultWidth: 360, defaultHeight: 160 },
- { id: "resources", title: "Resources", x: 400, y: 20, defaultWidth: 320, defaultHeight: 380 },
- ],
- contents: [],
- };
- \ No newline at end of file
diff --git a/src/client/views/nodes/trails/PresBox.scss b/src/client/views/nodes/trails/PresBox.scss
index e24b47bd1..8875f7012 100644
--- a/src/client/views/nodes/trails/PresBox.scss
+++ b/src/client/views/nodes/trails/PresBox.scss
@@ -229,7 +229,7 @@
.toolbar-transition {
display: flex;
font-size: 10;
- width: 100;
+ width: 100px;
background-color: rgba(0, 0, 0, 0);
min-width: max-content;
@@ -358,7 +358,7 @@
font-weight: 200;
padding: 8px;
border-radius: 4px;
- // height: 20;
+ // height: 20px;
// display: flex;
// margin-left: 5px;
// margin-top: 5px;
@@ -371,8 +371,8 @@
}
.ribbon-propertyUpDown {
- height: 20;
- width: 20;
+ height: 20px;
+ width: 20px;
margin-top: 5px;
display: grid;
grid-template-rows: 10px 10px;
@@ -486,7 +486,7 @@
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
- margin: 0;
+ margin: 0px;
margin-right: 3px;
border-radius: 100%;
height: 15px;
@@ -658,9 +658,9 @@
height: 25px;
color: white;
width: 100%;
- max-width: 120;
- padding-left: 10;
- padding-right: 10;
+ max-width: 120px;
+ padding-left: 10px;
+ padding-right: 10px;
border-radius: 10px;
background-color: global.$medium-gray;
}
@@ -683,9 +683,9 @@
height: 25px;
color: global.$light-gray;
width: 100%;
- max-width: 120;
- padding-left: 10;
- padding-right: 10;
+ max-width: 120px;
+ padding-left: 10px;
+ padding-right: 10px;
border-radius: 10px;
background-color: global.$black;
}
@@ -745,9 +745,9 @@
.selectedList {
display: block;
- min-width: 50;
- max-width: 120;
- height: 70;
+ min-width: 50px;
+ max-width: 120px;
+ height: 70px;
overflow-y: scroll;
.selectedList-items {
@@ -760,7 +760,7 @@
cursor: pointer;
font-size: 10.5;
font-weight: 300;
- height: 20;
+ height: 20px;
background-color: global.$medium-gray;
color: white;
display: flex;
@@ -788,7 +788,7 @@
cursor: pointer;
font-size: 10.5;
font-weight: 200;
- height: 20;
+ height: 20px;
background-color: global.$white;
display: inline-flex;
color: global.$black;
@@ -813,7 +813,7 @@
}
svg.svg-inline--fa.fa-thumbtack.fa-w-12.toolbar-thumbtack {
- right: 40;
+ right: 40px;
position: absolute;
transform: rotate(45deg);
}
@@ -850,7 +850,7 @@
background-color: global.$light-gray;
border-radius: 5px;
font-size: 10;
- height: 25;
+ height: 25px;
color: global.$black;
padding-left: 5px;
align-items: center;
@@ -868,8 +868,8 @@
display: block;
padding-left: 10px;
padding-right: 5px;
- padding-top: 3;
- padding-bottom: 3;
+ padding-top: 3px;
+ padding-bottom: 3px;
opacity: 0.8;
}
@@ -959,8 +959,8 @@
border-radius: 4px;
padding-left: 7px;
padding-right: 7px;
- border-bottom-right-radius: 0;
- border-top-right-radius: 0;
+ border-bottom-right-radius: 0px;
+ border-top-right-radius: 0px;
}
.presBox-button-right {
@@ -976,8 +976,8 @@
border-radius: 4px;
padding-left: 7px;
padding-right: 7px;
- border-bottom-left-radius: 0;
- border-top-left-radius: 0;
+ border-bottom-left-radius: 0px;
+ border-top-left-radius: 0px;
}
.presBox-button-right.active {
@@ -1043,8 +1043,8 @@
cursor: pointer;
align-self: center;
justify-self: center;
- margin-top: 5;
- margin-bottom: 5;
+ margin-top: 5px;
+ margin-bottom: 5px;
position: relative;
height: 55px;
min-width: 90px;
@@ -1063,7 +1063,7 @@
padding-left: 3px;
margin-left: 3px;
margin-right: 3px;
- height: 13;
+ height: 13px;
font-size: 12;
display: flex;
background-color: global.$white;
@@ -1076,7 +1076,7 @@
margin-left: 3px;
margin-right: 3px;
font-weight: 400;
- height: 13;
+ height: 13px;
font-size: 9;
display: flex;
background-color: global.$white;
@@ -1089,11 +1089,11 @@
padding-left: 3px;
margin-left: 3px;
margin-right: 3px;
- height: 13;
+ height: 13px;
font-size: 10;
display: flex;
background-color: global.$white;
- height: 33;
+ height: 33px;
text-align: left;
font-size: 8px;
}
@@ -1112,7 +1112,7 @@
.presBox-viewPicker {
cursor: pointer;
- height: 25;
+ height: 25px;
position: relative;
display: inline-block;
grid-column: 1;
@@ -1184,9 +1184,9 @@
}
.collectionViewBaseChrome-viewPicker {
- min-width: 50;
+ min-width: 50px;
width: 5%;
- height: 25;
+ height: 25px;
position: relative;
display: inline-block;
left: 8px;
@@ -1210,11 +1210,11 @@
}
.presBox-backward {
- left: 5;
+ left: 5px;
}
.presBox-forward {
- right: 5;
+ right: 5px;
}
// CSS adjusted for mobile devices
@@ -1233,8 +1233,8 @@
.presBox-button {
margin-top: 5%;
- height: 250;
- width: 300;
+ height: 250px;
+ width: 300px;
font-size: 100;
display: flex;
align-items: center;
@@ -1256,7 +1256,7 @@
}
.presBox-cont .presBox-listCont {
- top: 50;
+ top: 50px;
height: calc(100% - 80px);
}
@@ -1269,8 +1269,8 @@
.miniPres {
cursor: grab;
position: absolute;
- top: 0;
- left: 0;
+ top: 0px;
+ left: 0px;
opacity: 0.5;
transition: all 0.4s;
color: global.$white;
@@ -1298,7 +1298,7 @@
.presPanel-button-text {
cursor: pointer;
display: flex;
- height: 20;
+ height: 20px;
width: max-content;
font-family: Roboto;
font-weight: 400;
@@ -1327,8 +1327,8 @@
grid-template-columns: auto auto auto;
justify-content: space-around;
font-size: 11;
- margin-left: 7;
- width: 30;
+ margin-left: 7px;
+ width: 30px;
height: 85%;
background-color: rgba(91, 157, 221, 0.4);
border-radius: 5px;
@@ -1337,8 +1337,8 @@
.presPanel-button {
cursor: pointer;
display: flex;
- height: 20;
- min-width: 20;
+ height: 20px;
+ min-width: 20px;
margin-left: 3px;
margin-right: 3px;
border-radius: 100%;
@@ -1361,8 +1361,8 @@
// cursor: grab;
// position: absolute;
// overflow: hidden;
-// right: 10;
-// top: 10;
+// right: 10px;
+// top: 10px;
// opacity: 0.1;
// transition: all 0.4s;
// /* border: solid 1px; */
@@ -1380,7 +1380,7 @@
// .miniPres-button-text {
// cursor: pointer;
// display: flex;
-// height: 20;
+// height: 20px;
// font-weight: 400;
// min-width: 100%;
// border-radius: 5px;
@@ -1397,8 +1397,8 @@
// grid-template-columns: auto auto auto;
// justify-content: space-around;
// font-size: 11;
-// margin-left: 7;
-// width: 30;
+// margin-left: 7px;
+// width: 30px;
// height: 85%;
// background-color: rgba(91, 157, 221, 0.4);
// border-radius: 5px;
@@ -1413,8 +1413,8 @@
// .miniPres-button {
// cursor: pointer;
// display: flex;
-// height: 20;
-// min-width: 20;
+// height: 20px;
+// min-width: 20px;
// border-radius: 100%;
// align-items: center;
// justify-content: center;
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 11f35b8ef..04b312ca5 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -702,7 +702,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
transTime + 10
);
}
- if ((pinDataTypes?.pannable || (!pinDataTypes && (activeItem.config_viewBounds !== undefined || activeItem.config_panX !== undefined || activeItem.config_viewScale !== undefined))) && !bestTarget.isGroup) {
+ if ((pinDataTypes?.pannable || (!pinDataTypes && (activeItem.config_viewBounds !== undefined || activeItem.config_panX !== undefined || activeItem.config_viewScale !== undefined))) && !Doc.IsFreeformGroup(bestTarget)) {
const contentBounds = Cast(activeItem.config_viewBounds, listSpec('number'));
if (contentBounds) {
const viewport = { panX: (contentBounds[0] + contentBounds[2]) / 2, panY: (contentBounds[1] + contentBounds[3]) / 2, width: contentBounds[2] - contentBounds[0], height: contentBounds[3] - contentBounds[1] };
@@ -1743,6 +1743,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
return <div />;
}
+ setAiEffectsRef = (r: HTMLTextAreaElement | null) =>
+ setTimeout(() => {
+ if (r && !r.textContent) {
+ r.style.height = '';
+ r.style.height = r.scrollHeight + 'px';
+ }
+ });
+
+ setAnimDictationRef = (r: DictationButton | null) => (this._animationDictation = r);
/**
* This chatbox is for getting slide effect transition suggestions from gpt and visualizing them
*/
@@ -1755,14 +1764,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
<ReactTextareaAutosize
placeholder="Use AI to suggest effects. Leave blank for random results."
className="pres-chatbox"
- ref={r => {
- setTimeout(() => {
- if (r && !r.textContent) {
- r.style.height = '';
- r.style.height = r.scrollHeight + 'px';
- }
- });
- }}
+ ref={this.setAiEffectsRef}
value={this._animationChat}
onChange={e => {
e.currentTarget.style.height = '';
@@ -1784,12 +1786,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
color={SnappingManager.userVariantColor}
onClick={this.customizeAnimations}
/>
- <DictationButton
- ref={r => {
- this._animationDictation = r;
- }}
- setInput={this.setAnimationChat}
- />
+ <DictationButton ref={this.setAnimDictationRef} setInput={this.setAnimationChat} />
</div>
<div style={{ alignItems: 'center' }}>
Click a box to use the effect.
@@ -1821,6 +1818,16 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
);
}
+ setPropertiesRef = (r: HTMLTextAreaElement | null) =>
+ setTimeout(() => {
+ if (r && !r.textContent) {
+ r.style.height = '';
+ r.style.height = r.scrollHeight + 'px';
+ }
+ });
+
+ setSlideDictationRef = (r: DictationButton | null) => (this._slideDictation = r);
+
@computed get transitionDropdown() {
const { activeItem } = this;
// Retrieving spring timing properties
@@ -1855,14 +1862,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
<ReactTextareaAutosize
placeholder="Describe how to modify the slide properties."
className="pres-chatbox"
- ref={r => {
- setTimeout(() => {
- if (r && !r.textContent) {
- r.style.height = '';
- r.style.height = r.scrollHeight + 'px';
- }
- });
- }}
+ ref={this.setPropertiesRef}
value={this._chatInput}
onChange={e => {
e.currentTarget.style.height = '';
@@ -1874,12 +1874,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
e.stopPropagation();
}}
/>
- <DictationButton
- ref={r => {
- this._slideDictation = r;
- }}
- setInput={this.setChatInput}
- />
+ <DictationButton ref={this.setSlideDictationRef} setInput={this.setChatInput} />
</div>
<Button
style={{ alignSelf: 'flex-end' }}
diff --git a/src/client/views/nodes/trails/PresSlideBox.scss b/src/client/views/nodes/trails/PresSlideBox.scss
index 9ac2b5a94..740bcae5f 100644
--- a/src/client/views/nodes/trails/PresSlideBox.scss
+++ b/src/client/views/nodes/trails/PresSlideBox.scss
@@ -117,8 +117,8 @@ $slide-active: #5b9fdd;
height: 100%;
position: absolute;
border-radius: 3px;
- top: 0;
- left: 0;
+ top: 0px;
+ left: 0px;
z-index: 1;
overflow: hidden;
}
@@ -209,7 +209,7 @@ $slide-active: #5b9fdd;
position: absolute;
/* grid-row: 3; */
/* grid-column: 1/8; */
- top: 28;
+ top: 28px;
display: block;
background: #92adb9;
width: 100%;
@@ -276,7 +276,7 @@ $slide-active: #5b9fdd;
cursor: pointer;
position: absolute;
border-radius: 100px;
- bottom: 0;
+ bottom: 0px;
left: -18;
z-index: 300;
width: 15px;
@@ -302,7 +302,7 @@ $slide-active: #5b9fdd;
color: #d5dce2;
position: absolute;
left: -15px;
- top: 1;
+ top: 1px;
font-weight: 600;
font-size: 12;
}
diff --git a/src/client/views/pdf/AnchorMenu.scss b/src/client/views/pdf/AnchorMenu.scss
index 6990bdcf1..98eeaf80e 100644
--- a/src/client/views/pdf/AnchorMenu.scss
+++ b/src/client/views/pdf/AnchorMenu.scss
@@ -6,19 +6,19 @@
}
.anchorMenu-highlighter {
padding-right: 5px;
- .antimodeMenu-button {
- padding: 0;
- padding: 0;
+ .antimodeMenu-button {
+ padding: 0px;
+ padding: 0px;
padding-right: 0px;
padding-left: 0px;
width: 5px;
}
}
-.anchor-color-preview-button {
- width: 25px !important;
+.anchor-color-preview-button {
+ width: 25px !important;
.anchor-color-preview {
display: flex;
- flex-direction: column;
+ flex-direction: column;
padding-right: 3px;
width: unset !important;
.color-preview {
@@ -51,4 +51,4 @@
border: 2px solid white;
}
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.scss b/src/client/views/pdf/GPTPopup/GPTPopup.scss
index f6fa45221..85c1f6759 100644
--- a/src/client/views/pdf/GPTPopup/GPTPopup.scss
+++ b/src/client/views/pdf/GPTPopup/GPTPopup.scss
@@ -20,8 +20,8 @@ $headingHeight: 32px;
left: 75px;
width: 100%;
height: 100%;
- top: 0;
- left: 0;
+ top: 0px;
+ left: 0px;
border-top: solid gray 20px;
border-radius: 16px;
z-index: 999;
@@ -58,7 +58,7 @@ $headingHeight: 32px;
font-size: 12px;
font-weight: 400;
letter-spacing: 1px;
- margin: 0;
+ margin: 0px;
padding-right: 5px;
}
@@ -214,8 +214,8 @@ $headingHeight: 32px;
.img-container::after {
content: '';
position: absolute;
- top: 0;
- left: 0;
+ top: 0px;
+ left: 0px;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.tsx b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
index 568e48edf..9c37428ee 100644
--- a/src/client/views/pdf/GPTPopup/GPTPopup.tsx
+++ b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
@@ -11,7 +11,7 @@ import { ClientUtils } from '../../../../ClientUtils';
import { Doc } from '../../../../fields/Doc';
import { NumCast, StrCast } from '../../../../fields/Types';
import { Networking } from '../../../Network';
-import { DescriptionSeperator, DocSeperator, GPTCallType, GPTDocCommand, gptAPICall, gptImageCall } from '../../../apis/gpt/GPT';
+import { DataSeperator, DescriptionSeperator, DocSeperator, GPTCallType, GPTDocCommand, gptAPICall, gptImageCall } from '../../../apis/gpt/GPT';
import { DocUtils } from '../../../documents/DocUtils';
import { Docs } from '../../../documents/Documents';
import { SettingsManager } from '../../../util/SettingsManager';
@@ -90,7 +90,7 @@ export class GPTPopup extends ObservableReactComponent<object> {
if (hasChildDocs) {
this._textToDocMap.clear();
this.setCollectionContext(selDoc.Document);
- this.onGptResponse = (sortResult: string, questionType: GPTDocCommand, args?: string) => this.processGptResponse(selDoc, this._textToDocMap, sortResult, questionType, args);
+ this.onGptResponse = (sortResult: string, questionType: GPTDocCommand) => this.processGptResponse(selDoc, this._textToDocMap, sortResult, questionType);
this.onQuizRandom = () => this.randomlyChooseDoc(selDoc.Document, hasChildDocs());
this._documentDescriptions = Promise.all(hasChildDocs().map(doc =>
Doc.getDescription(doc).then(text => this._textToDocMap.set(text.replace(/\n/g, ' ').trim(), doc) && `${DescriptionSeperator}${text}${DescriptionSeperator}`)
@@ -134,7 +134,7 @@ export class GPTPopup extends ObservableReactComponent<object> {
* @param questionType
* @param tag
*/
- processGptResponse = (docView: DocumentView, textToDocMap: Map<string, Doc>, gptOutput: string, questionType: GPTDocCommand, args?: string) =>
+ processGptResponse = (docView: DocumentView, textToDocMap: Map<string, Doc>, gptOutput: string, questionType: GPTDocCommand) =>
undoable(() => {
switch (questionType) { // reset collection based on question typefc
case GPTDocCommand.Sort:
@@ -145,20 +145,17 @@ export class GPTPopup extends ObservableReactComponent<object> {
break;
} // prettier-ignore
- gptOutput.split('======').filter(item => item.trim() !== '') // Split output into individual document contents
- .map(docContentRaw => textToDocMap.get(docContentRaw.replace(/\n/g, ' ').trim())) // the find the corresponding Doc using textToDoc map
- .filter(doc => doc).map(doc => doc!) // filter out undefined values
- .forEach((doc, index) => {
+ gptOutput.split(DescriptionSeperator).filter(item => item.trim() !== '') // Split output into individual document contents
+ .map(docContentRaw => docContentRaw.replace(/\n/g, ' ').trim())
+ .map(docContentRaw => ({doc: textToDocMap.get(docContentRaw.split(DataSeperator)[0]), data: docContentRaw.split(DataSeperator)[1] })) // the find the corresponding Doc using textToDoc map
+ .filter(({doc}) => doc).map(({doc, data}) => ({doc:doc!, data})) // filter out undefined values
+ .forEach(({doc, data}, index) => {
switch (questionType) {
case GPTDocCommand.Sort:
doc[ChatSortField] = index;
break;
case GPTDocCommand.AssignTags:
- if (args) {
- const hashTag = args.startsWith('#') ? args : '#' + args[0].toLowerCase() + args.slice(1);
- const filterTag = Doc.MyFilterHotKeys.map(key => StrCast(key.toolType)).find(key => key.includes(args)) ?? hashTag;
- TagItem.addTagToDoc(doc, filterTag);
- }
+ data && TagItem.addTagToDoc(doc, data.startsWith('#') ? data : '#'+data[0].toLowerCase()+data.slice(1) );
break;
case GPTDocCommand.Filter:
TagItem.addTagToDoc(doc, GPTPopup.ChatTag);
@@ -238,10 +235,10 @@ export class GPTPopup extends ObservableReactComponent<object> {
* @param userPrompt the user's input that chat will respond to
*/
generateUserPromptResponse = (userPrompt: string) =>
- gptAPICall(userPrompt, GPTCallType.COMMANDTYPE, undefined, true).then((commandType, args = commandType.split(' ').slice(1).join(' ')) =>
+ gptAPICall(userPrompt, GPTCallType.COMMANDTYPE, undefined, true).then(commandType =>
(async () => {
switch (this.NumberToCommandType(commandType)) {
- case GPTDocCommand.AssignTags:
+ case GPTDocCommand.AssignTags:return this._documentDescriptions?.then(descs => gptAPICall(userPrompt, GPTCallType.TAGDOCS, descs)) ?? "";
case GPTDocCommand.Filter: return this._documentDescriptions?.then(descs => gptAPICall(userPrompt, GPTCallType.SUBSETDOCS, descs)) ?? "";
case GPTDocCommand.Sort: return this._documentDescriptions?.then(descs => gptAPICall(userPrompt, GPTCallType.SORTDOCS, descs)) ?? "";
default: return Doc.getDescription(DocumentView.SelectedDocs().lastElement()).then(desc => gptAPICall(userPrompt, GPTCallType.DOCINFO, desc));
@@ -249,7 +246,7 @@ export class GPTPopup extends ObservableReactComponent<object> {
})().then(
action(res => {
// Trigger the callback with the result
- this.onGptResponse?.(res || 'Something went wrong :(', this.NumberToCommandType(commandType), args);
+ this.onGptResponse?.(res || 'Something went wrong :(', this.NumberToCommandType(commandType));
this._conversationArray.push(
this.NumberToCommandType(commandType) === GPTDocCommand.GetInfo ? res:
// Extract explanation surrounded by the DocSeperator string (defined in GPT.ts) at the top or both at the top and bottom
@@ -515,6 +512,7 @@ export class GPTPopup extends ObservableReactComponent<object> {
</div>
);
+ setDictationRef = (r: DictationButton | null) => (this._askDictation = r);
promptBox = (heading: string, value: string, onChange: (e: string) => string, placeholder: string) => (
<>
<div className="gptPopup-sortBox">
@@ -541,7 +539,7 @@ export class GPTPopup extends ObservableReactComponent<object> {
background={SettingsManager.userVariantColor}
onClick={() => this.callGpt(this._mode)}
/>
- <DictationButton ref={r => (this._askDictation = r)} setInput={onChange} />
+ <DictationButton ref={this.setDictationRef} setInput={onChange} />
</div>
</>
);
diff --git a/src/client/views/pdf/PDFViewer.scss b/src/client/views/pdf/PDFViewer.scss
index 1aab2b853..dafa908a2 100644
--- a/src/client/views/pdf/PDFViewer.scss
+++ b/src/client/views/pdf/PDFViewer.scss
@@ -3,8 +3,8 @@
width: 100%;
position: absolute;
display: inline-block;
- top: 0;
- left: 0;
+ top: 0px;
+ left: 0px;
}
:root {
@@ -16,8 +16,8 @@
position: absolute;
width: 100%;
height: 100%;
- top: 0;
- left: 0;
+ top: 0px;
+ left: 0px;
position: absolute;
overflow-y: auto;
overflow-x: hidden;
@@ -99,7 +99,7 @@
.pdfViewerDash-annotationLayer {
position: absolute;
transform-origin: left top;
- top: 0;
+ top: 0px;
width: 100%;
pointer-events: none;
mix-blend-mode: multiply; // bcz: makes text fuzzy!
@@ -118,6 +118,9 @@
.pdfViewerDash-interactive {
pointer-events: all;
+ &::-webkit-scrollbar {
+ width: 40px;
+ }
}
.loading-spinner {
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index a88d8b282..616e2119f 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -901,12 +901,6 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> {
@action
onPointerDown = (e: React.PointerEvent): void => {
- // const hit = document.elementFromPoint(e.clientX, e.clientY);
- // bcz: Change. drag selecting requires that preventDefault is NOT called. This used to happen in DocumentView,
- // but that's changed, so this shouldn't be needed.
- // if (hit && hit.localName === "span" && this.annotationsActive(true)) { // drag selecting text stops propagation
- // e.button === 0 && e.stopPropagation();
- // }
// if alt+left click, drag and annotate
this._downX = e.clientX;
this._downY = e.clientY;
diff --git a/src/client/views/search/FaceRecognitionHandler.tsx b/src/client/views/search/FaceRecognitionHandler.tsx
index 3ad5bc844..84404d65a 100644
--- a/src/client/views/search/FaceRecognitionHandler.tsx
+++ b/src/client/views/search/FaceRecognitionHandler.tsx
@@ -9,6 +9,7 @@ import { ImageField } from '../../../fields/URLField';
import { DocumentType } from '../../documents/DocumentTypes';
import { Docs } from '../../documents/Documents';
import { DocumentManager } from '../../util/DocumentManager';
+import { DocumentView } from '../nodes/DocumentView';
/**
* A singleton class that handles face recognition and manages face Doc collections for each face found.
@@ -33,7 +34,7 @@ export class FaceRecognitionHandler {
// eslint-disable-next-line no-use-before-define
static _instance: FaceRecognitionHandler;
private _apiModelReady = false;
- private _pendingAPIModelReadyDocs: Doc[] = [];
+ private _pendingAPIModelReadyDocs: DocumentView[] = [];
public static get Instance() {
return FaceRecognitionHandler._instance ?? new FaceRecognitionHandler();
@@ -126,7 +127,7 @@ export class FaceRecognitionHandler {
constructor() {
FaceRecognitionHandler._instance = this;
this.loadAPIModels().then(() => this._pendingAPIModelReadyDocs.forEach(this.classifyFacesInImage));
- DocumentManager.Instance.AddAnyViewRenderedCB(dv => FaceRecognitionHandler.Instance.classifyFacesInImage(dv.Document));
+ DocumentManager.Instance.AddAnyViewRenderedCB(dv => FaceRecognitionHandler.Instance.classifyFacesInImage(dv));
}
/**
@@ -199,16 +200,18 @@ export class FaceRecognitionHandler {
* match them to existing unique faces, otherwise new unique face(s) are created.
* @param imgDoc The document being analyzed.
*/
- private classifyFacesInImage = async (imgDoc: Doc) => {
+ private classifyFacesInImage = async (imgDocView: DocumentView) => {
+ const imgDoc = imgDocView.Document;
if (!Doc.UserDoc().recognizeFaceImages) return;
const activeDashboard = Doc.ActiveDashboard;
if (!this._apiModelReady || !activeDashboard) {
- this._pendingAPIModelReadyDocs.push(imgDoc);
+ this._pendingAPIModelReadyDocs.push(imgDocView);
} else if (imgDoc.type === DocumentType.LOADING && !imgDoc.loadingError) {
- setTimeout(() => this.classifyFacesInImage(imgDoc), 1000);
+ setTimeout(() => this.classifyFacesInImage(imgDocView), 1000);
} else {
const imgUrl = ImageCast(imgDoc[Doc.LayoutDataKey(imgDoc)]);
if (imgUrl && !DocListCast(Doc.MyFaceCollection?.examinedFaceDocs).includes(imgDoc[DocData])) {
+ imgDocView.ComponentView?.autoTag?.();
// only examine Docs that have an image and that haven't already been examined.
Doc.MyFaceCollection && Doc.AddDocToList(Doc.MyFaceCollection, 'examinedFaceDocs', imgDoc[DocData]);
FaceRecognitionHandler.loadImage(imgUrl).then(
diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx
index 3976ec39e..b7ff5fff7 100644
--- a/src/client/views/smartdraw/SmartDrawHandler.tsx
+++ b/src/client/views/smartdraw/SmartDrawHandler.tsx
@@ -330,8 +330,9 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
switch (doc.type) {
case DocumentType.IMG: {
const func = changeInPlace ? this.recreateImageWithFirefly : this.createImageWithFirefly;
- const newPrompt = doc.ai_prompt && doc.ai_prompt !== this._regenInput ? `${doc.ai_prompt} ~~~ ${this._regenInput}` : this._regenInput;
- return this._regenInput ? func(newPrompt, NumCast(doc?.ai_prompt_seed)) : func(this._lastInput.text || StrCast(doc.ai_prompt));
+ const promptChange = doc.ai_prompt && doc.ai_prompt !== this._regenInput;
+ const newPrompt = promptChange ? `${doc.ai_prompt} ~~~ ${this._regenInput}` : this._regenInput;
+ return this._regenInput ? func(newPrompt, promptChange ? NumCast(doc?.ai_prompt_seed) : undefined) : func(this._lastInput.text || StrCast(doc.ai_prompt));
}
case DocumentType.COL: {
try {
diff --git a/src/client/views/topbar/TopBar.scss b/src/client/views/topbar/TopBar.scss
index 35a3da312..ca177c746 100644
--- a/src/client/views/topbar/TopBar.scss
+++ b/src/client/views/topbar/TopBar.scss
@@ -125,7 +125,7 @@
.topbar-lozenge-user,
.topbar-lozenge {
- height: 23;
+ height: 23px;
font-size: 12;
color: white;
font-family: 'Roboto';