aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/ClientUtils.ts18
-rw-r--r--src/client/Network.ts2
-rw-r--r--src/client/documents/DocumentTypes.ts1
-rw-r--r--src/client/documents/Documents.ts18
-rw-r--r--src/client/util/CurrentUserUtils.ts14
-rw-r--r--src/client/util/DocumentManager.ts4
-rw-r--r--src/client/util/node_modules/type_decls.d (renamed from src/typings/type_decls.d)0
-rw-r--r--src/client/util/request-image-size.ts9
-rw-r--r--src/client/views/DashboardView.tsx2
-rw-r--r--src/client/views/DocumentButtonBar.tsx18
-rw-r--r--src/client/views/FilterPanel.tsx26
-rw-r--r--src/client/views/MainView.tsx8
-rw-r--r--src/client/views/PropertiesButtons.tsx20
-rw-r--r--src/client/views/SidebarAnnos.tsx1
-rw-r--r--src/client/views/StyleProvider.tsx2
-rw-r--r--src/client/views/TagsView.tsx38
-rw-r--r--src/client/views/collections/CollectionCalendarView.tsx5
-rw-r--r--src/client/views/collections/CollectionCardDeckView.scss18
-rw-r--r--src/client/views/collections/CollectionStackingView.scss7
-rw-r--r--src/client/views/collections/CollectionStackingView.tsx62
-rw-r--r--src/client/views/collections/CollectionStackingViewFieldColumn.tsx26
-rw-r--r--src/client/views/collections/CollectionView.tsx4
-rw-r--r--src/client/views/collections/TreeView.tsx2
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx40
-rw-r--r--src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx1
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx4
-rw-r--r--src/client/views/collections/collectionGrid/CollectionGridView.tsx17
-rw-r--r--src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss1
-rw-r--r--src/client/views/collections/collectionMulticolumn/CollectionMultirowView.scss1
-rw-r--r--src/client/views/global/globalScripts.ts14
-rw-r--r--src/client/views/nodes/ComparisonBox.tsx7
-rw-r--r--src/client/views/nodes/FieldView.tsx1
-rw-r--r--src/client/views/nodes/FontIconBox/FontIconBox.tsx2
-rw-r--r--src/client/views/nodes/ImageBox.tsx14
-rw-r--r--src/client/views/nodes/KeyValueBox.tsx2
-rw-r--r--src/client/views/nodes/PDFBox.tsx4
-rw-r--r--src/client/views/nodes/calendarBox/CalendarBox.scss25
-rw-r--r--src/client/views/nodes/calendarBox/CalendarBox.tsx258
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx3
-rw-r--r--src/server/ApiManagers/SearchManager.ts200
-rw-r--r--src/server/ApiManagers/UploadManager.ts53
-rw-r--r--src/server/DashUploadUtils.ts94
-rw-r--r--src/server/index.ts2
43 files changed, 470 insertions, 578 deletions
diff --git a/src/ClientUtils.ts b/src/ClientUtils.ts
index 55801df81..01eda7e98 100644
--- a/src/ClientUtils.ts
+++ b/src/ClientUtils.ts
@@ -648,20 +648,14 @@ export function DivWidth(ele: HTMLElement | null): number {
}
export function dateRangeStrToDates(dateStr: string) {
+ const toDate = (str: string) => {
+ return !str.includes('T') && str.includes('-') ? new Date(Number(str.split('-')[0]), Number(str.split('-')[1]) - 1, Number(str.split('-')[2])) : new Date(str);
+ };
// dateStr in yyyy-mm-dd format
const dateRangeParts = dateStr.split('|'); // splits into from and to date
- const fromParts = dateRangeParts[0].split('-');
- const toParts = dateRangeParts[1].split('-');
-
- const fromYear = parseInt(fromParts[0]);
- const fromMonth = parseInt(fromParts[1]) - 1;
- const fromDay = parseInt(fromParts[2]);
-
- const toYear = parseInt(toParts[0]);
- const toMonth = parseInt(toParts[1]) - 1;
- const toDay = parseInt(toParts[2]);
-
- return [new Date(fromYear, fromMonth, fromDay), new Date(toYear, toMonth, toDay)];
+ if (dateRangeParts.length < 2 && !dateRangeParts[0]) return { start: new Date(), end: new Date() };
+ if (dateRangeParts.length < 2) return { start: toDate(dateRangeParts[0]), end: toDate(dateRangeParts[0]) };
+ return { start: new Date(dateRangeParts[0]), end: new Date(dateRangeParts[1]) };
}
function replaceCanvases(oldDiv: HTMLElement, newDiv: HTMLElement) {
diff --git a/src/client/Network.ts b/src/client/Network.ts
index 204fcf0ac..9afdc844f 100644
--- a/src/client/Network.ts
+++ b/src/client/Network.ts
@@ -50,7 +50,7 @@ export namespace Networking {
if (!fileguidpairs.length) {
return [];
}
- const maxFileSize = 6000000;
+ const maxFileSize = 50000000;
if (fileguidpairs.some(f => f.file.size > maxFileSize)) {
return new Promise<Upload.FileResponse<T>[]>(res => res([{ source: { newFilename: '', mimetype: '' } as formidable.File, result: new Error(`max file size (${maxFileSize / 1000000}MB) exceeded`) }]));
}
diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts
index 5a5c3d5bf..59a121de7 100644
--- a/src/client/documents/DocumentTypes.ts
+++ b/src/client/documents/DocumentTypes.ts
@@ -40,7 +40,6 @@ export enum DocumentType {
COMPARISON = 'comparison',
PUSHPIN = 'pushpin',
MAPROUTE = 'maproute',
- CALENDAR = 'calendar',
SCRIPTDB = 'scriptdb', // database of scripts
GROUPDB = 'groupdb', // database of groups
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 91bba4755..26a937817 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -260,6 +260,7 @@ export class DocumentOptions {
_layout_noSidebar?: BOOLt = new BoolInfo('whether to display the sidebar toggle button');
layout_boxShadow?: string; // box-shadow css string OR "standard" to use dash standard box shadow
layout_maxShown?: NUMt = new NumInfo('maximum number of children to display at one time (see multicolumnview)');
+ _layout_dontCenter?: STRt = new StrInfo("whether collections will center their content - values of 'x', 'xy', or 'y'");
_layout_autoHeight?: BOOLt = new BoolInfo('whether document automatically resizes vertically to display contents');
_layout_autoHeightMargins?: NUMt = new NumInfo('Margin heights to be added to the computed auto height of a Doc');
_layout_curPage?: NUMt = new NumInfo('current page of a PDF or other? paginated document', false);
@@ -450,6 +451,7 @@ export class DocumentOptions {
onDragStart?: ScriptField; // script to execute at start of drag operation -- e.g., when a "creator" button is dragged this script generates a different document to drop
target?: Doc; // available for use in scripts. used to provide a document parameter to the script (Note, this is a convenience entry since any field could be used for parameterizing a script)
tags?: LISTt = new ListInfo('hashtags added to document, typically using a text view', true);
+ tags_chat?: LISTt = new ListInfo('hashtags added to document by chatGPT', true);
treeView_HideTitle?: BOOLt = new BoolInfo('whether to hide the top document title of a tree view');
treeView_HideUnrendered?: BOOLt = new BoolInfo("tells tree view not to display documents that have an 'layout_unrendered' tag unless they also have a treeView_FieldKey tag (presBox)");
treeView_HideHeaderIfTemplate?: BOOLt = new BoolInfo('whether to hide the header for a document in a tree view only if a childLayoutTemplate is provided (presBox)');
@@ -912,7 +914,15 @@ export namespace Docs {
}
export function CalendarDocument(options: DocumentOptions, documents: Array<Doc>) {
- return InstanceFromProto(Prototypes.get(DocumentType.CALENDAR), new List(documents), { ...options });
+ const inst = InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), {
+ _layout_nativeDimEditable: true,
+ _layout_reflowHorizontal: true,
+ _layout_reflowVertical: true,
+ ...options,
+ _type_collection: CollectionViewType.Calendar,
+ });
+ documents.forEach(d => Doc.SetContainer(d, inst));
+ return inst;
}
// shouldn't ever need to create a KVP document-- instead set the LayoutTemplateString to be a KeyValueBox for the DocumentView (see addDocTab in TabDocView)
@@ -969,12 +979,8 @@ export namespace Docs {
return doc;
}
- export function CalendarCollectionDocument(documents: Array<Doc>, options: DocumentOptions) {
- return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _type_collection: CollectionViewType.Calendar });
- }
-
export function StackingDocument(documents: Array<Doc>, options: DocumentOptions, id?: string, protoId?: string) {
- return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _type_collection: CollectionViewType.Stacking }, id, undefined, protoId);
+ return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _layout_dontCenter: 'y', ...options, _type_collection: CollectionViewType.Stacking }, id, undefined, protoId);
}
export function NoteTakingDocument(documents: Array<Doc>, options: DocumentOptions, id?: string, protoId?: string) {
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 710f3e00a..079ab46cf 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -388,7 +388,7 @@ pie title Minerals in my tap water
{key: "Chat", creator: Docs.Create.ChatDocument, opts: { _width: 300, _height: 300, }},
{key: "Header", creator: headerTemplate, opts: { _width: 300, _height: 120, _header_pointerEvents: "all", _header_height: 50, _header_fontSize: 9,_layout_autoHeightMargins: 50, _layout_autoHeight: true, treeView_HideUnrendered: true}},
{key: "ViewSlide", creator: slideView, opts: { _width: 400, _height: 300, _xMargin: 3, _yMargin: 3,}},
- {key: "Trail", creator: Docs.Create.PresDocument, opts: { _width: 400, _height: 30, _type_collection: CollectionViewType.Stacking, dropAction: dropActionType.embed, treeView_HideTitle: true, _layout_fitWidth:true, layout_boxShadow: "0 0" }},
+ {key: "Trail", creator: Docs.Create.PresDocument, opts: { _width: 400, _height: 30, _type_collection: CollectionViewType.Stacking, _layout_dontCenter:'xy', dropAction: dropActionType.embed, treeView_HideTitle: true, _layout_fitWidth:true, layout_boxShadow: "0 0" }},
{key: "Tab", creator: opts => Docs.Create.FreeformDocument([], opts), opts: { _width: 500, _height: 800, _layout_fitWidth: true, _freeform_backgroundGrid: true, }},
{key: "Slide", creator: opts => Docs.Create.TreeDocument([], opts), opts: { _width: 300, _height: 200, _type_collection: CollectionViewType.Tree,
treeView_HasOverlay: true, _text_fontSize: "20px", _layout_autoHeight: true,
@@ -497,6 +497,7 @@ pie title Minerals in my tap water
const reqdStackOpts:DocumentOptions ={
title: "menuItemPanel", childDragAction: dropActionType.same, layout_boxShadow: "rgba(0,0,0,0)", dontRegisterView: true, ignoreClick: true,
+ _layout_dontCenter: 'y',
_chromeHidden: true, _gridGap: 0, _yMargin: 0, _xMargin: 0, _layout_autoHeight: false, _width: 60, _columnWidth: 60, _lockedPosition: true, isSystem: true,
};
return DocUtils.AssignDocField(doc, field, (opts, items) => Docs.Create.StackingDocument(items??[], opts), reqdStackOpts, menuBtns, { dropConverter: "convertToButtons(dragData)" });
@@ -530,7 +531,7 @@ pie title Minerals in my tap water
// doc.myUserBtns = new PrefetchProxy(userBtns);
const reqdToolOps:DocumentOptions = {
title: "My Tools", isSystem: true, ignoreClick: true, layout_boxShadow: "0 0",
- layout_explainer: "This is a palette of documents that can be created.",
+ layout_explainer: "This is a palette of documents that can be created.", _layout_dontCenter: "y",
_layout_showTitle: "title", _width: 500, _yMargin: 20, _lockedPosition: true, _forceActive: true, _dragOnlyWithinContainer: true, layout_hideContextMenu: true, _chromeHidden: true,
};
return DocUtils.AssignDocField(doc, field, (opts, items) => Docs.Create.StackingDocument(items??[], opts), reqdToolOps, [creatorBtns, userBtns]);
@@ -683,7 +684,8 @@ pie title Minerals in my tap water
}
static stackTools(): Button[] {
return [
- { title: "Center", icon: "align-center", toolTip: "Center Align Stack", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"center", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform
+ { title: "V. Align", icon: "pallet", toolTip: "Center Align Stack", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"vcenter", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform
+ { title: "Center", icon: "align-center", toolTip: "Center Align Stack", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"hcenter", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform
]
}
static cardTools(): Button[] {
@@ -799,8 +801,8 @@ pie title Minerals in my tap water
}
static imageTools() {
return [
- { title: "Pixels",toolTip: "Set Native Pixel Sizze", btnType: ButtonType.ClickButton, icon: "portrait", scripts: { onClick: 'imageSetPixelSize();' }},
- { title: "Rotate",toolTip: "Rotate 90", btnType: ButtonType.ClickButton, icon: "redo-alt", scripts: { onClick: 'imageRotate90();' }},
+ { title: "Pixels",toolTip: "Set Native Pixel Size", btnType: ButtonType.ClickButton, icon: "portrait", scripts: { onClick: 'imageSetPixelSize();' }},
+ { title: "Rotate",toolTip: "Rotate 90", btnType: ButtonType.ClickButton, icon: "redo-alt", scripts: { onClick: 'imageRotate90();' }},
];
}
static contextMenuTools():Button[] {
@@ -809,7 +811,7 @@ pie title Minerals in my tap water
CollectionViewType.Stacking, CollectionViewType.Masonry, CollectionViewType.Multicolumn,
CollectionViewType.Multirow, CollectionViewType.Time, CollectionViewType.Carousel,
CollectionViewType.Carousel3D, CollectionViewType.Card, CollectionViewType.Linear, CollectionViewType.Map,
- CollectionViewType.Grid, CollectionViewType.NoteTaking, ]),
+ CollectionViewType.Calendar, CollectionViewType.Grid, CollectionViewType.NoteTaking, ]),
title: "Perspective", toolTip: "View", btnType: ButtonType.DropdownList, ignoreClick: true, width: 100, scripts: { script: '{ return setView(value, _readOnly_); }'}},
{ title: "Pin", icon: "map-pin", toolTip: "Pin View to Trail", btnType: ButtonType.ClickButton, expertMode: false, width: 30, scripts: { onClick: 'pinWithView(altKey)'}, funcs: {hidden: "IsNoneSelected()"}},
{ title: "Header", icon: "heading", toolTip: "Doc Titlebar Color", btnType: ButtonType.ColorButton, expertMode: false, ignoreClick: true, scripts: { script: 'return setHeaderColor(value, _readOnly_)'} },
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 83b83240e..5ae292760 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -269,8 +269,8 @@ export class DocumentManager {
if (options.openLocation?.includes(OpenWhere.lightbox)) {
// even if we found the document view, if the target is a lightbox, we try to open it in the lightbox to preserve lightbox semantics (eg, there's only one active doc in the lightbox)
const target = DocCast(targetDoc.annotationOn, targetDoc);
- const contextView = this.getDocumentView(DocCast(target.embedContainer));
- if (contextView?.ComponentView?.addDocTab?.(target, options.openLocation)) {
+ const compView = this.getDocumentView(DocCast(target.embedContainer))?.ComponentView;
+ if ((compView?.addDocTab ?? compView?._props.addDocTab)?.(target, options.openLocation)) {
await new Promise<void>(waitres => {
setTimeout(() => waitres());
});
diff --git a/src/typings/type_decls.d b/src/client/util/node_modules/type_decls.d
index 1a93bbe59..1a93bbe59 100644
--- a/src/typings/type_decls.d
+++ b/src/client/util/node_modules/type_decls.d
diff --git a/src/client/util/request-image-size.ts b/src/client/util/request-image-size.ts
index 7a2ecd486..c619192ed 100644
--- a/src/client/util/request-image-size.ts
+++ b/src/client/util/request-image-size.ts
@@ -35,7 +35,11 @@ module.exports = function requestImageSize(url: string) {
res.on('data', chunk => {
buffer = Buffer.concat([buffer, chunk]);
+ });
+
+ res.on('error', reject);
+ res.on('end', () => {
try {
size = imageSize(buffer);
if (size) {
@@ -46,11 +50,6 @@ module.exports = function requestImageSize(url: string) {
/* empty */
console.log('Error: ', err);
}
- });
-
- res.on('error', reject);
-
- res.on('end', () => {
if (!size) {
reject(new Error('Image has no size'));
return;
diff --git a/src/client/views/DashboardView.tsx b/src/client/views/DashboardView.tsx
index 33e905a54..eced64524 100644
--- a/src/client/views/DashboardView.tsx
+++ b/src/client/views/DashboardView.tsx
@@ -465,7 +465,7 @@ export class DashboardView extends ObservableReactComponent<object> {
isSystem: true,
layout_explainer: 'All of the calendars that you have created will appear here.',
};
- const myCalendars = DocUtils.AssignScripts(Docs.Create.CalendarCollectionDocument([], reqdOpts));
+ const myCalendars = DocUtils.AssignScripts(Docs.Create.StackingDocument([], reqdOpts));
// { treeView_ChildDoubleClick: 'openPresentation(documentView.rootDoc)' }
return new PrefetchProxy(myCalendars);
}
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index 096f058ad..0cfdfa9f7 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -278,23 +278,6 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (
}
@computed
- get calendarButton() {
- const targetDoc = this.view0?.Document;
- return !targetDoc ? null : (
- <Tooltip title={<div className="dash-calendar-button">Open calendar menu</div>}>
- <div
- className="documentButtonBar-icon"
- style={{ color: 'white' }}
- onClick={() => {
- CalendarManager.Instance.open(this.view0, targetDoc);
- }}>
- <FontAwesomeIcon className="documentdecorations-icon" icon={faCalendarDays as IconLookup} />
- </div>
- </Tooltip>
- );
- }
-
- @computed
get keywordButton() {
return !DocumentView.Selected().length ? null : (
<Tooltip title={<div className="dash-keyword-button">Open keyword menu</div>}>
@@ -475,7 +458,6 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (
<div className="documentButtonBar-button">{this.pinButton}</div>
<div className="documentButtonBar-button">{this.saveAnnoButton}</div>
<div className="documentButtonBar-button">{this.recordButton}</div>
- <div className="documentButtonBar-button">{this.calendarButton}</div>
<div className="documentButtonBar-button">{this.keywordButton}</div>
{!Doc.UserDoc().documentLinksButton_fullMenu ? null : <div className="documentButtonBar-button">{this.shareButton}</div>}
<div className="documentButtonBar-button">{this.menuButton}</div>
diff --git a/src/client/views/FilterPanel.tsx b/src/client/views/FilterPanel.tsx
index b11fa3bd5..2f6d1fbaa 100644
--- a/src/client/views/FilterPanel.tsx
+++ b/src/client/views/FilterPanel.tsx
@@ -192,21 +192,17 @@ export class FilterPanel extends ObservableReactComponent<filterProps> {
const allCollectionDocs = new Set<Doc>();
SearchUtil.foreachRecursiveDoc(this.targetDocChildren, (depth: number, doc: Doc) => allCollectionDocs.add(doc));
const set = new Set<string>([...StrListCast(this.Document.childFilters).map(filter => filter.split(Doc.FilterSep)[1]), Doc.FilterNone, Doc.FilterAny]);
- if (facetHeader === 'tags')
- allCollectionDocs.forEach(child =>
- StrListCast(child[facetHeader])
- .filter(h => h)
- .forEach(key => set.add(key))
- );
- else
- allCollectionDocs.forEach(child => {
- const fieldVal = child[facetHeader] as FieldType;
- if (!(fieldVal instanceof List)) {
- // currently we have no good way of filtering based on a field that is a list
- set.add(Field.toString(fieldVal));
- (fieldVal === true || fieldVal === false) && set.add((!fieldVal).toString());
- }
- });
+
+ allCollectionDocs.forEach(child => {
+ const fieldVal = child[facetHeader] as FieldType;
+ const fieldStrList = StrListCast(child[facetHeader]).filter(h => h);
+ if (fieldStrList.length) fieldStrList.forEach(key => set.add(key));
+ else if (!(fieldVal instanceof List)) {
+ // currently we have no good way of filtering based on a field that is a list
+ set.add(Field.toString(fieldVal));
+ (fieldVal === true || fieldVal === false) && set.add((!fieldVal).toString());
+ }
+ });
const facetValues = Array.from(set).filter(v => v);
let nonNumbers = 0;
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 2adeefa37..0b1e5f857 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -3,7 +3,7 @@ import { faBuffer, faHireAHelper } from '@fortawesome/free-brands-svg-icons';
import * as far from '@fortawesome/free-regular-svg-icons';
import * as fa from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, configure, makeObservable, observable, reaction, runInAction } from 'mobx';
+import { action, computed, configure, makeObservable, observable, reaction, runInAction, trace } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import ResizeObserver from 'resize-observer-polyfill';
@@ -720,8 +720,8 @@ export class MainView extends ObservableReactComponent<object> {
setupMoveUpEvents(
this,
e,
- action(() => {
- SnappingManager.SetPropertiesWidth(Math.max(0, this._dashUIWidth - e.clientX));
+ action(moveEv => {
+ SnappingManager.SetPropertiesWidth(Math.max(0, this._dashUIWidth - moveEv.clientX));
return !SnappingManager.PropertiesWidth;
}),
action(() => {
@@ -820,6 +820,7 @@ export class MainView extends ObservableReactComponent<object> {
childFilters={returnEmptyFilter}
childFiltersByRanges={returnEmptyFilter}
searchFilterDocs={returnEmptyDoclist}
+ dontCenter="y"
/>
</div>
);
@@ -845,6 +846,7 @@ export class MainView extends ObservableReactComponent<object> {
};
@computed get mainInnerContent() {
+ trace();
const leftMenuFlyoutWidth = this._leftMenuFlyoutWidth + this.leftMenuWidth();
const width = this.propertiesWidth() + leftMenuFlyoutWidth;
return (
diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx
index f346d4ba8..f96a4a255 100644
--- a/src/client/views/PropertiesButtons.tsx
+++ b/src/client/views/PropertiesButtons.tsx
@@ -246,26 +246,6 @@ export class PropertiesButtons extends React.Component {
// );
// }
- // @computed get freezeThumb() {
- // return this.propertyToggleBtn(
- // 'FreezeThumb',
- // '_thumb-frozen',
- // on => `${on ? 'Freeze' : 'Unfreeze'} thumbnail`,
- // on => 'snowflake',
- // (dv, doc) => {
- // if (doc['thumb-frozen']) doc['thumb-frozen'] = undefined;
- // else {
- // document.body.focus(); // so that we can access the clipboard without an error
- // setTimeout(() =>
- // pasteImageBitmap((data_url: any, error: any) => {
- // error && console.log(error);
- // data_url && Utils.convertDataUri(data_url, doc[Id] + '-thumb-frozen', true).then(returnedfilename => (doc['thumb-frozen'] = new ImageField(returnedfilename)));
- // })
- // );
- // }
- // }
- // );
- // }
@computed get snapButton() {
// THESE ARE NOT COMING
return this.propertyToggleBtn(
diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx
index 8f0a35df0..dd60bfa65 100644
--- a/src/client/views/SidebarAnnos.tsx
+++ b/src/client/views/SidebarAnnos.tsx
@@ -246,6 +246,7 @@ export class SidebarAnnos extends ObservableReactComponent<FieldViewProps & Extr
isAnnotationOverlay={false}
select={emptyFunction}
NativeDimScaling={returnOne}
+ dontCenter="y"
// childlayout_showTitle={this.layout_showTitle}
isAnyChildContentActive={returnFalse}
childDocumentsActive={this._props.isContentActive}
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index 76cb119ab..513953d17 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -365,7 +365,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
</Tooltip>
);
};
- const tags = () => props?.DocumentView?.() && CollectionFreeFormDocumentView.from(props.DocumentView()) ? <TagsView View={props.DocumentView()}/> : null;
+ const tags = () => props?.DocumentView?.() ? <TagsView View={props.DocumentView()}/> : null;
return (
<>
{paint()}
diff --git a/src/client/views/TagsView.tsx b/src/client/views/TagsView.tsx
index 0ac015b36..8047363d9 100644
--- a/src/client/views/TagsView.tsx
+++ b/src/client/views/TagsView.tsx
@@ -9,7 +9,7 @@ import { emptyFunction } from '../../Utils';
import { Doc, DocListCast, Field, Opt, StrListCast } from '../../fields/Doc';
import { DocData } from '../../fields/DocSymbols';
import { List } from '../../fields/List';
-import { DocCast, NumCast, StrCast } from '../../fields/Types';
+import { DocCast, StrCast } from '../../fields/Types';
import { DocumentType } from '../documents/DocumentTypes';
import { DragManager } from '../util/DragManager';
import { SnappingManager } from '../util/SnappingManager';
@@ -91,7 +91,7 @@ export class TagItem extends ObservableReactComponent<TagItemProps> {
const tagCollection = TagItem.findTagCollectionDoc(tag) ?? TagItem.createTagCollectionDoc(tag);
// If the document is of type COLLECTION, make it a smart collection, otherwise, add the tag to the document.
- if (doc.type === DocumentType.COL) {
+ if (doc.type === DocumentType.COL && !doc.annotationOn) {
Doc.AddDocToList(tagCollection[DocData], 'collections', doc);
// Iterate through the tag Doc collections and add a copy of the document to each collection
@@ -216,7 +216,17 @@ export class TagItem extends ObservableReactComponent<TagItemProps> {
{metadata ? (
<span>
<b style={{ fontSize: 'smaller' }}>{tag}&nbsp;</b>
- {Field.toString(this._props.doc[metadata])}
+ {typeof this._props.doc[metadata] === 'boolean' ? (
+ <input
+ type="checkbox"
+ onClick={e => e.stopPropagation()}
+ onPointerDown={e => e.stopPropagation()}
+ onChange={undoable(e => (this._props.doc[metadata] = !this._props.doc[metadata]), 'metadata toggle')}
+ checked={this._props.doc[metadata] as boolean}
+ />
+ ) : (
+ Field.toString(this._props.doc[metadata])
+ )}
</span>
) : (
tag
@@ -247,6 +257,7 @@ export class TagsView extends ObservableReactComponent<TagViewProps> {
super(props);
makeObservable(this);
}
+ InsetDist = 25; // how far tag panel is moved up to overlap DocumentView.
@observable _panelHeightDirty = 0;
@observable _currentInput = '';
@@ -266,7 +277,7 @@ export class TagsView extends ObservableReactComponent<TagViewProps> {
}
@computed get currentScale() {
- return NumCast(DocCast(this._props.View.Document.embedContainer)?._freeform_scale, 1);
+ return Math.max(1, 1 / this._props.View.screenToLocalScale());
}
@computed get isEditing() {
return this._isEditing && DocumentView.SelectedDocs().includes(this._props.View.Document);
@@ -283,15 +294,18 @@ export class TagsView extends ObservableReactComponent<TagViewProps> {
};
/**
- * Adds the specified tag to the Doc. If the tag is not prefixed with '#', then a '#' prefix is added.
- * Whne the tag (after the '#') begins with '@', then a metadata key/value pair is displayed instead of
- * just the tag.
- * @param tag tag string to add
+ * Adds the specified tag or metadata to the Doc. If the tag is not prefixed with '#', then a '#' prefix is added.
+ * When the tag (after the '#') begins with '@', then a metadata key/value pair is displayed instead of
+ * just the tag. In addition, a suffix of :<value> can be added to set a metadata value
+ * @param tag tag string to add (format: #<tag> | #@field(:(=)?value)? )
*/
submitTag = undoable(
action((tag: string) => {
- const submittedLabel = tag.trim();
- submittedLabel && TagItem.addTagToDoc(this._props.View.Document, '#' + submittedLabel.replace(/^#/, ''));
+ const submittedLabel = tag.trim().replace(/^#/, '').split(':');
+ if (submittedLabel[0]) {
+ TagItem.addTagToDoc(this._props.View.Document, '#' + submittedLabel[0]);
+ if (submittedLabel.length > 1) Doc.SetField(this._props.View.Document, submittedLabel[0].replace(/^@/, ''), ':' + submittedLabel[1]);
+ }
this._currentInput = ''; // Clear the input box
}),
'added doc label'
@@ -315,7 +329,7 @@ export class TagsView extends ObservableReactComponent<TagViewProps> {
return !this._props.View.Document.showTags ? null : (
<div
className="tagsView-container"
- ref={r => r && new ResizeObserver(action(() => (this._props.View.TagPanelHeight = r?.getBoundingClientRect().height ?? 0))).observe(r)}
+ ref={r => r && new ResizeObserver(action(() => (this._props.View.TagPanelHeight = Math.max(0, (r?.getBoundingClientRect().height ?? 0) - this.InsetDist)))).observe(r)}
style={{
transformOrigin: 'top left',
maxWidth: `${100 * this.currentScale}%`,
@@ -323,6 +337,8 @@ export class TagsView extends ObservableReactComponent<TagViewProps> {
transform: `scale(${1 / this.currentScale})`,
backgroundColor: this.isEditing ? Colors.LIGHT_GRAY : Colors.TRANSPARENT,
borderColor: this.isEditing ? Colors.BLACK : Colors.TRANSPARENT,
+ position: 'relative',
+ top: `calc(-${this.InsetDist} * ${1 / this.currentScale}px)`,
}}>
<div className="tagsView-content" style={{ width: '100%' }}>
<div className="tagsView-list">
diff --git a/src/client/views/collections/CollectionCalendarView.tsx b/src/client/views/collections/CollectionCalendarView.tsx
index 9eb16917b..0ea9f8ebc 100644
--- a/src/client/views/collections/CollectionCalendarView.tsx
+++ b/src/client/views/collections/CollectionCalendarView.tsx
@@ -38,8 +38,8 @@ export class CollectionCalendarView extends CollectionSubView() {
const aDateRangeStr = StrCast(DocListCast(calendarA.data).lastElement()?.date_range);
const bDateRangeStr = StrCast(DocListCast(calendarB.data).lastElement()?.date_range);
- const [aFromDate, aToDate] = dateRangeStrToDates(aDateRangeStr);
- const [bFromDate, bToDate] = dateRangeStrToDates(bDateRangeStr);
+ const { start: aFromDate, end: aToDate } = dateRangeStrToDates(aDateRangeStr);
+ const { start: bFromDate, end: bToDate } = dateRangeStrToDates(bDateRangeStr);
if (aFromDate > bFromDate) {
return -1; // a comes first
@@ -82,6 +82,7 @@ export class CollectionCalendarView extends CollectionSubView() {
isAnnotationOverlay={false}
// select={emptyFunction} What does this mean?
isAnyChildContentActive={returnTrue} // ??
+ dontCenter="y"
// childDocumentsActive={}
// whenChildContentsActiveChanged={}
childHideDecorationTitle={false}
diff --git a/src/client/views/collections/CollectionCardDeckView.scss b/src/client/views/collections/CollectionCardDeckView.scss
index a089b248d..cc797d0bd 100644
--- a/src/client/views/collections/CollectionCardDeckView.scss
+++ b/src/client/views/collections/CollectionCardDeckView.scss
@@ -6,6 +6,15 @@
position: relative;
background-color: white;
overflow: hidden;
+
+ button {
+ width: 35px;
+ height: 35px;
+ border-radius: 50%;
+ background-color: $dark-gray;
+ // border-color: $medium-blue;
+ margin: 5px; // transform: translateY(-50px);
+ }
}
.card-wrapper {
@@ -34,15 +43,6 @@
justify-content: start; /* Centers buttons horizontally */
}
-button {
- width: 35px;
- height: 35px;
- border-radius: 50%;
- background-color: $dark-gray;
- // border-color: $medium-blue;
- margin: 5px; // transform: translateY(-50px);
-}
-
// button:hover {
// transform: translateY(-50px);
// }
diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss
index 6225cc52a..6400a0a8e 100644
--- a/src/client/views/collections/CollectionStackingView.scss
+++ b/src/client/views/collections/CollectionStackingView.scss
@@ -54,11 +54,8 @@
}
.collectionStackingViewFieldColumn {
- height: max-content;
- }
-
- .collectionStackingViewFieldColumnDragging {
- height: 100%;
+ display: flex;
+ flex-direction: column;
}
.collectionSchemaView-previewDoc {
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 6402ef16c..e97ee713e 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -35,6 +35,7 @@ import { CollectionMasonryViewFieldRow } from './CollectionMasonryViewFieldRow';
import './CollectionStackingView.scss';
import { CollectionStackingViewFieldColumn } from './CollectionStackingViewFieldColumn';
import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
+import { computedFn } from 'mobx-utils';
export type collectionStackingViewProps = {
sortFunc?: (a: Doc, b: Doc) => number;
@@ -127,6 +128,9 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
}
}
+ 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?
@@ -140,17 +144,14 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
})
);
return docs.map((d, i) => {
- const height = () => this.getDocHeight(d);
- const width = () => this.getDocWidth(d);
- const trans = () => this.getDocTransition(d);
// 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((height() + this.gridGap) / this.gridGap);
+ const rowSpan = Math.ceil((this.getDocHeight(d)() + this.gridGap) / this.gridGap);
// just getting the style
- const style = this.isStackingView ? { margin: this.Document._stacking_alignCenter ? 'auto' : undefined, transition: trans(), width: width(), marginTop: i ? this.gridGap : 0, height: height() } : { gridRowEnd: `span ${rowSpan}` };
+ const style = this.isStackingView ? { margin: undefined, transition: this.getDocTransition(d)(), width: this.columnWidth, marginTop: i ? this.gridGap : 0, height: this.getDocHeight(d)() } : { gridRowEnd: `span ${rowSpan}` };
// 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}>
- {this.getDisplayDoc(d, width, trans, i)}
+ {this.getDisplayDoc(d, this.getDocTransition(d), i)}
</div>
);
});
@@ -311,26 +312,23 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
: this._props.childDocumentsActive?.() === false || this.Document.childDocumentsActive === false
? false
: undefined;
+
isChildButtonContentActive = () => (this._props.childDocumentsActive?.() === false || this.Document.childDocumentsActive === false ? false : undefined);
@observable docRefs = new ObservableMap<Doc, DocumentView>();
childFitWidth = (doc: Doc) => Cast(this.Document.childLayoutFitWidth, 'boolean', this._props.childLayoutFitWidth?.(doc) ?? Cast(doc.layout_fitWidth, 'boolean', null));
// this is what renders the document that you see on the screen
// called in Children: this actually adds a document to our children list
- getDisplayDoc(doc: Doc, width: () => number, trans: () => string, count: number) {
+ getDisplayDoc(doc: Doc, trans: () => string, count: number) {
const dataDoc = doc.isTemplateDoc || doc.isTemplateForField ? this._props.TemplateDataDocument : undefined;
- const height = () => this.getDocHeight(doc);
- const panelHeight = () => (this.isStackingView ? height() : Math.min(height(), this._props.PanelHeight()));
- const panelWidth = () => this.columnWidth;
- const stackedDocTransform = () => this.getDocTransform(doc);
- this._docXfs.push({ stackedDocTransform, width, height });
+ this._docXfs.push({ stackedDocTransform: this.getDocTransform(doc), width: this.getDocWidth(doc), height: this.getDocHeight(doc) });
return count > this._renderCount ? null : (
<DocumentView
ref={action((r: DocumentView) => r?.ContentDiv && this.docRefs.set(doc, r))}
Document={doc}
TemplateDataDocument={dataDoc}
renderDepth={this._props.renderDepth + 1}
- PanelWidth={panelWidth}
- PanelHeight={panelHeight}
+ PanelWidth={this.columnWidthFn}
+ PanelHeight={this.columnDocHeightFn(doc)}
pointerEvents={this.DocumentView?.()._props.onClickScript?.() ? returnNone : undefined} // if the stack has an onClick, then we don't want the contents to be interactive (see CollectionPileView)
styleProvider={this.styleProvider}
containerViewPath={this.childContainerViewPath}
@@ -341,16 +339,16 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
isDocumentActive={this.isContentActive}
LayoutTemplate={this._props.childLayoutTemplate}
LayoutTemplateString={this._props.childLayoutString}
- NativeWidth={this._props.childIgnoreNativeSize ? returnZero : this._props.childLayoutFitWidth?.(doc) || (this.childFitWidth(doc) && !Doc.NativeWidth(doc)) ? width : undefined} // explicitly ignore nativeWidth/height if childIgnoreNativeSize is set- used by PresBox
- NativeHeight={this._props.childIgnoreNativeSize ? returnZero : this._props.childLayoutFitWidth?.(doc) || (this.childFitWidth(doc) && !Doc.NativeHeight(doc)) ? height : undefined}
- dontCenter={this._props.childIgnoreNativeSize ? 'xy' : (StrCast(this.layoutDoc.layout_dontCenter) as 'x' | 'y' | 'xy')}
+ NativeWidth={this._props.childIgnoreNativeSize ? returnZero : this._props.childLayoutFitWidth?.(doc) || (this.childFitWidth(doc) && !Doc.NativeWidth(doc)) ? this.getDocWidth(doc) : undefined} // explicitly ignore nativeWidth/height if childIgnoreNativeSize is set- used by PresBox
+ NativeHeight={this._props.childIgnoreNativeSize ? returnZero : this._props.childLayoutFitWidth?.(doc) || (this.childFitWidth(doc) && !Doc.NativeHeight(doc)) ? this.getDocHeight(doc) : undefined}
+ dontCenter={this.dontCenter}
dontRegisterView={BoolCast(this.layoutDoc.childDontRegisterViews, this._props.dontRegisterView)} // used to be true if DataDoc existed, but template textboxes won't layout_autoHeight resize if dontRegisterView is set, but they need to.
rootSelected={this.rootSelected}
showTitle={this._props.childlayout_showTitle}
dragAction={(this.layoutDoc.childDragAction ?? this._props.childDragAction) as dropActionType}
onClickScript={this.onChildClickHandler}
onDoubleClickScript={this.onChildDoubleClickHandler}
- ScreenToLocalTransform={stackedDocTransform}
+ ScreenToLocalTransform={this.getDocTransform(doc)}
focus={this.focusDocument}
childFilters={this.childDocFilters}
hideDecorationTitle={this._props.childHideDecorationTitle}
@@ -371,15 +369,18 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
);
}
- getDocTransform(doc: Doc) {
+ getDocTransform = computedFn((doc: Doc) => () => {
+ // these must be referenced for document decorations to update when the text box container is scrolled
+ this._scroll;
+ this._props.ScreenToLocalTransform();
+
const dref = this.docRefs.get(doc);
- this._scroll; // must be referenced for document decorations to update when the text box container is scrolled
const { translateX, translateY, scale } = ClientUtils.GetScreenTransform(dref?.ContentDiv);
return new Transform(-translateX + (dref?.centeringX || 0) * scale,
-translateY + (dref?.centeringY || 0) * scale, 1)
.scale(1 / scale); // prettier-ignore
- }
- getDocWidth(d?: Doc) {
+ });
+ getDocWidth = computedFn((d?: Doc) => () => {
if (!d) return 0;
const childLayoutDoc = Doc.Layout(d, this._props.childLayoutTemplate?.());
const maxWidth = this.columnWidth / this.numGroupColumns;
@@ -387,12 +388,9 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
return Math.min(NumCast(d._width), maxWidth);
}
return maxWidth;
- }
- getDocTransition(d?: Doc) {
- if (!d) return '';
- return StrCast(d.dataTransition);
- }
- getDocHeight(d?: Doc) {
+ });
+ getDocTransition = computedFn((d?: Doc) => () => StrCast(d?.dataTransition));
+ getDocHeight = computedFn((d?: Doc) => () => {
if (!d || d.hidden) return 0;
const childLayoutDoc = Doc.Layout(d, this._props.childLayoutTemplate?.());
const childDataDoc = !d.isTemplateDoc && !d.isTemplateForField ? undefined : this._props.TemplateDataDocument;
@@ -401,13 +399,13 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
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 docWid = this.layoutDoc._columnsFill ? colWid : Math.min(this.getDocWidth(d), colWid);
+ const docWid = this.layoutDoc._columnsFill ? colWid : Math.min(this.getDocWidth(d)(), colWid);
return Math.min(maxHeight, (docWid * nh) / nw);
}
const childHeight = NumCast(childLayoutDoc._height);
const panelHeight = this.childFitWidth(childLayoutDoc) ? Number.MAX_SAFE_INTEGER : this._props.PanelHeight() - 2 * this.yMargin;
return Math.min(childHeight, maxHeight, panelHeight);
- }
+ });
// This following three functions must be from the view Mehek showed
columnDividerDown = (e: React.PointerEvent) => {
@@ -531,6 +529,9 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
}
});
};
+ @computed get dontCenter() {
+ return this._props.dontCenter ?? (this._props.childIgnoreNativeSize ? 'xy' : (StrCast(this.layoutDoc.layout_dontCenter) as 'x' | 'y' | 'xy'));
+ }
headings = () => Array.from(this.Sections);
// what a section looks like if we're in stacking view
sectionStacking = (heading: SchemaHeaderField | undefined, docList: Doc[]) => {
@@ -565,6 +566,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
type={type}
createDropTarget={this.createDashEventsTarget}
screenToLocalTransform={this.ScreenToLocalBoxXf}
+ dontCenter={this.dontCenter}
/>
);
};
diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
index 5ae08e535..ed0cabd0a 100644
--- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
+++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
@@ -41,6 +41,7 @@ interface CSVFieldColumnProps {
columnWidth: number;
numGroupColumns: number;
gridGap: number;
+ dontCenter: 'x' | 'xy' | 'y';
type: 'string' | 'number' | 'bigint' | 'boolean' | 'symbol' | 'undefined' | 'object' | 'function' | undefined;
headings: () => object[];
// I think that stacking view actually has a single column and then supposedly you can add more columns? Unsure
@@ -345,15 +346,6 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
<button type="button" className="collectionStackingView-sectionDelete" onClick={this.deleteColumn}>
<FontAwesomeIcon icon="trash" size="lg" />
</button>
- {/* {evContents === noValueHeader ? null : (
- <div className="collectionStackingView-sectionOptions">
- <Flyout anchorPoint={anchorPoints.TOP_RIGHT} content={this.renderMenu()}>
- <button className="collectionStackingView-sectionOptionButton">
- <FontAwesomeIcon icon="ellipsis-v" size="lg"></FontAwesomeIcon>
- </button>
- </Flyout>
- </div>
- )} */}
</div>
<div
className={'collectionStackingView-collapseBar' + (this._props.headingObject.collapsed === true ? ' active' : '')}
@@ -368,14 +360,20 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
<>
{this._props.Document._columnsHideIfEmpty ? null : headingView}
{this.collapsed ? null : (
- <div>
+ <div
+ style={{
+ margin: 'auto',
+ marginTop: this._props.dontCenter.includes('y') ? undefined : 'auto',
+ marginBottom: this._props.dontCenter.includes('y') ? undefined : 'auto',
+ width: this._props.columnWidth / (uniqueHeadings.length + (this._props.chromeHidden ? 0 : 1) || 1),
+ }}>
<div
key={`${heading}-stack`}
className="collectionStackingView-masonrySingle"
style={{
padding: `${columnYMargin}px ${0}px ${this._props.yMargin}px ${0}px`,
- margin: 'auto',
- width: 'max-content', // singleColumn ? undefined : `${cols * (style.columnWidth + style.gridGap) + 2 * style.xMargin - style.gridGap}px`,
+ margin: this._props.dontCenter.includes('x') ? undefined : 'auto',
+ // width: 'max-content', // singleColumn ? undefined : `${cols * (style.columnWidth + style.gridGap) + 2 * style.xMargin - style.gridGap}px`,
height: 'max-content',
position: 'relative',
gridGap: this._props.gridGap,
@@ -419,11 +417,11 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
const uniqueHeadings = headings.map((i, idx) => headings.indexOf(i) === idx);
return (
<div
- className={'collectionStackingViewFieldColumn' + (SnappingManager.IsDragging ? 'Dragging' : '')}
+ className="collectionStackingViewFieldColumn"
key={heading}
style={{
width: `${100 / (uniqueHeadings.length + (this._props.chromeHidden ? 0 : 1) || 1)}%`,
- height: undefined, // DraggingManager.GetIsDragging() ? "100%" : undefined,
+ height: undefined,
background: this._background,
}}
ref={this.createColumnDropRef}
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index ab93abab6..c9e934448 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -34,6 +34,7 @@ import { CollectionLinearView } from './collectionLinear';
import { CollectionMulticolumnView } from './collectionMulticolumn/CollectionMulticolumnView';
import { CollectionMultirowView } from './collectionMulticolumn/CollectionMultirowView';
import { CollectionSchemaView } from './collectionSchema/CollectionSchemaView';
+import { CalendarBox } from '../nodes/calendarBox/CalendarBox';
@observer
export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewProps>() {
@@ -91,7 +92,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewPr
if (type === undefined) return null;
switch (type) {
case CollectionViewType.Schema: return <CollectionSchemaView key="collview" {...props} />;
- case CollectionViewType.Calendar: return <CollectionCalendarView key="collview" {...props} />;
+ case CollectionViewType.Calendar: return <CalendarBox key="collview" {...props} />;
case CollectionViewType.Docking: return <CollectionDockingView key="collview" {...props} />;
case CollectionViewType.Tree: return <CollectionTreeView key="collview" {...props} />;
case CollectionViewType.Multicolumn: return <CollectionMulticolumnView key="collview" {...props} />;
@@ -126,6 +127,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewPr
{ description: 'Masonry', event: () => func(CollectionViewType.Masonry), icon: 'columns' },
{ description: 'Carousel', event: () => func(CollectionViewType.Carousel), icon: 'columns' },
{ description: '3D Carousel', event: () => func(CollectionViewType.Carousel3D), icon: 'columns' },
+ { description: 'Calendar', event: () => func(CollectionViewType.Calendar), icon: 'columns' },
{ description: 'Pivot/Time', event: () => func(CollectionViewType.Time), icon: 'columns' },
{ description: 'Map', event: () => func(CollectionViewType.Map), icon: 'globe-americas' },
{ description: 'Grid', event: () => func(CollectionViewType.Grid), icon: 'th-list' },
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index 015f77ffd..5444a7a57 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -474,7 +474,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
};
docTransform = () => this.refTransform(this._dref?.ContentDiv);
getTransform = () => this.refTransform(this._tref.current);
- embeddedPanelWidth = () => this._props.panelWidth() / (this.treeView._props.NativeDimScaling?.() || 1);
+ embeddedPanelWidth = () => this._props.panelWidth() / (this.treeView._props.NativeDimScaling?.() || 1) - 3 /* paddingRight for bullet */;
embeddedPanelHeight = () => {
const layoutDoc = (temp => temp && Doc.expandTemplateLayout(temp, this.Document))(this.treeView._props.childLayoutTemplate?.()) || this.layoutDoc;
return Math.min(
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 4b4a07757..9aac724cc 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1876,24 +1876,28 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
Object.values(this._disposers).forEach(disposer => disposer?.());
}
- updateIcon = (usePanelDimensions?: boolean) =>
- UpdateIcon(
- this.layoutDoc[Id] + '_icon_' + new Date().getTime(),
- this.DocumentView?.().ContentDiv!,
- usePanelDimensions ? this._props.PanelWidth() : NumCast(this.layoutDoc._width),
- usePanelDimensions ? this._props.PanelHeight() : NumCast(this.layoutDoc._height),
- this._props.PanelWidth(),
- this._props.PanelHeight(),
- 0,
- 1,
- false,
- '',
- (iconFile, nativeWidth, nativeHeight) => {
- this.dataDoc.icon = new ImageField(iconFile);
- this.dataDoc.icon_nativeWidth = nativeWidth;
- this.dataDoc.icon_nativeHeight = nativeHeight;
- }
- );
+ updateIcon = (usePanelDimensions?: boolean) => {
+ const contentDiv = this.DocumentView?.().ContentDiv;
+ return !contentDiv
+ ? new Promise<void>(res => res())
+ : UpdateIcon(
+ this.layoutDoc[Id] + '_icon_' + new Date().getTime(),
+ contentDiv,
+ usePanelDimensions ? this._props.PanelWidth() : NumCast(this.layoutDoc._width),
+ usePanelDimensions ? this._props.PanelHeight() : NumCast(this.layoutDoc._height),
+ this._props.PanelWidth(),
+ this._props.PanelHeight(),
+ 0,
+ 1,
+ false,
+ '',
+ (iconFile, nativeWidth, nativeHeight) => {
+ this.dataDoc.icon = new ImageField(iconFile);
+ this.dataDoc.icon_nativeWidth = nativeWidth;
+ this.dataDoc.icon_nativeHeight = nativeHeight;
+ }
+ );
+ };
@action
onCursorMove = (e: React.PointerEvent) => {
diff --git a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx
index 717081666..f9f6c81ab 100644
--- a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx
+++ b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx
@@ -264,6 +264,7 @@ export class FaceCollectionBox extends ViewBoxBaseComponent<FieldViewProps>() {
isContentActive={returnTrue}
isAnyChildContentActive={returnTrue}
childHideDecorations={true}
+ dontCenter="y"
/>
</div>
);
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index bfb1d12cb..f1dba58ce 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -162,8 +162,8 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
pasteImageBitmap((data: any, error: any) => {
error && console.log(error);
data &&
- ClientUtils.convertDataUri(data, this._props.Document[Id] + '-thumb-frozen').then(returnedfilename => {
- this._props.Document['thumb-frozen'] = new ImageField(returnedfilename);
+ ClientUtils.convertDataUri(data, this._props.Document[Id] + '_icon_' + new Date().getTime()).then(returnedfilename => {
+ this._props.Document[DocData].icon = new ImageField(returnedfilename);
});
})
);
diff --git a/src/client/views/collections/collectionGrid/CollectionGridView.tsx b/src/client/views/collections/collectionGrid/CollectionGridView.tsx
index 61bd0241c..5c41fee37 100644
--- a/src/client/views/collections/collectionGrid/CollectionGridView.tsx
+++ b/src/client/views/collections/collectionGrid/CollectionGridView.tsx
@@ -9,7 +9,7 @@ import { emptyFunction } from '../../../../Utils';
import { Docs } from '../../../documents/Documents';
import { DragManager } from '../../../util/DragManager';
import { Transform } from '../../../util/Transform';
-import { undoBatch } from '../../../util/UndoManager';
+import { undoable, undoBatch } from '../../../util/UndoManager';
import { ContextMenu } from '../../ContextMenu';
import { ContextMenuProps } from '../../ContextMenuItem';
import { DocumentView } from '../../nodes/DocumentView';
@@ -221,13 +221,13 @@ export class CollectionGridView extends CollectionSubView() {
});
if (this.Document.gridStartCompaction) {
- undoBatch(() => {
+ undoable(() => {
this.Document.gridCompaction = this.Document.gridStartCompaction;
this.setLayoutList(savedLayouts);
- })();
+ }, 'start grid compaction')();
this.Document.gridStartCompaction = undefined;
} else {
- undoBatch(() => this.setLayoutList(savedLayouts))();
+ undoable(() => this.setLayoutList(savedLayouts), 'start grid compaction')();
}
}
};
@@ -315,9 +315,9 @@ export class CollectionGridView extends CollectionSubView() {
e,
returnFalse,
action(() => {
- undoBatch(() => {
+ undoable(() => {
this.Document.gridRowHeight = this._rowHeight;
- })();
+ }, 'changing row height')();
this._rowHeight = undefined;
}),
emptyFunction,
@@ -360,13 +360,14 @@ export class CollectionGridView extends CollectionSubView() {
returnFalse,
(clickEv: PointerEvent, doubleTap?: boolean) => {
if (doubleTap && !clickEv.button) {
- undoBatch(
+ undoable(
action(() => {
const text = Docs.Create.TextDocument('', { _width: 150, _height: 50 });
Doc.SetSelectOnLoad(text); // track the new text box so we can give it a prop that tells it to focus itself when it's displayed
Doc.AddDocToList(this.Document, this._props.fieldKey, text);
this.setLayoutList(this.addLayoutItem(this.savedLayoutList, this.makeLayoutItem(text, this.screenToCell(clickEv.clientX, clickEv.clientY))));
- })
+ }),
+ 'create grid text'
)();
}
},
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss
index 06d78c39e..9ed247d50 100644
--- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss
@@ -1,5 +1,6 @@
.collectionMulticolumnView_drop {
height: 100%;
+ width: 100%;
top: 0;
left: 0;
position: absolute;
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.scss b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.scss
index 0d49fabaa..91779065d 100644
--- a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.scss
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.scss
@@ -1,5 +1,6 @@
.collectionMultirowView_drop {
height: 100%;
+ width: 100%;
top: 0;
left: 0;
position: absolute;
diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts
index 68d287ac8..afb4e2eff 100644
--- a/src/client/views/global/globalScripts.ts
+++ b/src/client/views/global/globalScripts.ts
@@ -131,10 +131,10 @@ ScriptingGlobals.add(function toggleOverlay(checkResult?: boolean) {
});
// eslint-disable-next-line prefer-arrow-callback
-ScriptingGlobals.add(function showFreeform(attr: 'center' | 'grid' | 'snaplines' | 'clusters' | 'viewAll' | 'fitOnce', checkResult?: boolean, persist?: boolean) {
+ScriptingGlobals.add(function showFreeform(attr: 'hcenter' | 'vcenter' | 'grid' | 'snaplines' | 'clusters' | 'viewAll' | 'fitOnce', checkResult?: boolean, persist?: boolean) {
const selected = DocumentView.SelectedDocs().lastElement();
// prettier-ignore
- const map: Map<'flashcards' | 'center' | 'grid' | 'snaplines' | 'clusters' | 'arrange' | 'viewAll' | 'fitOnce' | 'time' | 'docType' | 'color' | 'links' | 'like' | 'star' | 'idea' | 'chat' | '1' | '2' | '3' | '4',
+ const map: Map<'flashcards' | 'hcenter' | 'vcenter' | 'grid' | 'snaplines' | 'clusters' | 'arrange' | 'viewAll' | 'fitOnce' | 'time' | 'docType' | 'color' | 'links' | 'like' | 'star' | 'idea' | 'chat' | '1' | '2' | '3' | '4',
{
waitForRender?: boolean;
checkResult: (doc: Doc) => boolean;
@@ -156,9 +156,13 @@ ScriptingGlobals.add(function showFreeform(attr: 'center' | 'grid' | 'snaplines'
else (dv.ComponentView as CollectionFreeFormView)?.fitContentOnce();
},
}],
- ['center', {
- checkResult: (doc:Doc) => BoolCast(doc?._stacking_alignCenter, false),
- setDoc: (doc:Doc) => { doc._stacking_alignCenter = !doc._stacking_alignCenter; },
+ ['vcenter', {
+ checkResult: (doc:Doc) => !StrCast(doc?._layout_dontCenter).includes('y'),
+ setDoc: (doc:Doc) => { doc._layout_dontCenter = StrCast(doc.layout_dontCenter).includes('y') ? StrCast(doc.layout_dontCenter).replace(/y/,"") : StrCast(doc.layout_dontCenter) + 'y'; },
+ }],
+ ['hcenter', {
+ checkResult: (doc:Doc) => !StrCast(doc?._layout_dontCenter).includes('x'),
+ setDoc: (doc:Doc) => { doc._layout_dontCenter = StrCast(doc.layout_dontCenter).includes('x') ? StrCast(doc.layout_dontCenter).replace(/x/,"") : 'x'+ StrCast(doc.layout_dontCenter); },
}],
['clusters', {
waitForRender: true, // flags that undo batch should terminate after a re-render giving the script the chance to fire
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index 1eae163df..39a2e3a31 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -3,7 +3,7 @@ import { Tooltip } from '@mui/material';
import { action, computed, makeObservable, observable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
-import { returnFalse, returnNone, setupMoveUpEvents } from '../../../ClientUtils';
+import { returnFalse, returnNone, returnZero, setupMoveUpEvents } from '../../../ClientUtils';
import { emptyFunction } from '../../../Utils';
import { Doc, Opt } from '../../../fields/Doc';
import { DocData } from '../../../fields/DocSymbols';
@@ -303,6 +303,9 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
<DocumentView
// eslint-disable-next-line react/jsx-props-no-spreading
{...this._props}
+ fitWidth={undefined}
+ NativeHeight={returnZero}
+ NativeWidth={returnZero}
ignoreUsePath={layoutString ? true : undefined}
renderDepth={this.props.renderDepth + 1}
LayoutTemplateString={layoutString}
@@ -310,8 +313,6 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
containerViewPath={this.DocumentView?.().docViewPath}
moveDocument={whichSlot.endsWith('1') ? this.moveDoc1 : this.moveDoc2}
removeDocument={whichSlot.endsWith('1') ? this.remDoc1 : this.remDoc2}
- NativeWidth={this.layoutWidth}
- NativeHeight={this.layoutHeight}
isContentActive={emptyFunction}
isDocumentActive={returnFalse}
whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index dd71fd946..c269c7bcb 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -82,6 +82,7 @@ export interface FieldViewSharedProps {
// eslint-disable-next-line no-use-before-define
onKey?: (e: React.KeyboardEvent, fieldProps: FieldViewProps) => boolean | undefined;
fitWidth?: (doc: Doc) => boolean | undefined;
+ dontCenter?: 'x' | 'y' | 'xy' | undefined;
searchFilterDocs: () => Doc[];
showTitle?: () => string;
whenChildContentsActiveChanged: (isActive: boolean) => void;
diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.tsx b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
index f2f7f39bb..7a09ad9e2 100644
--- a/src/client/views/nodes/FontIconBox/FontIconBox.tsx
+++ b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
@@ -345,7 +345,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
@computed get editableText() {
const script = ScriptCast(this.Document.script);
- const checkResult = script?.script.run({ this: this.Document, value: '', _readOnly_: true }).result;
+ const checkResult = script?.script.run({ this: this.Document, value: '', _readOnly_: true }).result as string;
const setValue = (value: string) => script?.script.run({ this: this.Document, value, _readOnly_: false }).result as boolean;
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index d0a7fc6ac..be3525544 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -11,7 +11,7 @@ import { DocData } from '../../../fields/DocSymbols';
import { Id } from '../../../fields/FieldSymbols';
import { InkTool } from '../../../fields/InkField';
import { ObjectField } from '../../../fields/ObjectField';
-import { Cast, ImageCast, NumCast, StrCast } from '../../../fields/Types';
+import { Cast, ImageCast, NumCast, RTFCast, StrCast } from '../../../fields/Types';
import { ImageField } from '../../../fields/URLField';
import { TraceMobx } from '../../../fields/util';
import { emptyFunction } from '../../../Utils';
@@ -188,8 +188,9 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
@undoBatch
setNativeSize = action(() => {
+ const oldnativeWidth = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']);
const nscale = NumCast(this._props.PanelWidth()) * NumCast(this.layoutDoc._freeform_scale, 1);
- const nw = nscale / NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']);
+ 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.dataDoc._freeform_panX = nw * NumCast(this.dataDoc._freeform_panX);
@@ -198,6 +199,15 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
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']);
+ DocListCast(this.dataDoc[this.annotationKey]).forEach(doc => {
+ doc.x = (NumCast(doc.x) / oldnativeWidth) * newnativeWidth;
+ doc.y = (NumCast(doc.y) / oldnativeWidth) * newnativeWidth;
+ if (!RTFCast(doc[Doc.LayoutFieldKey(doc)])) {
+ doc.width = (NumCast(doc.width) / oldnativeWidth) * newnativeWidth;
+ doc.height = (NumCast(doc.height) / oldnativeWidth) * newnativeWidth;
+ }
+ });
});
@undoBatch
rotate = action(() => {
diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index 95e344004..3daacc9bb 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -84,7 +84,7 @@ export class KeyValueBox extends ViewBoxBaseComponent<FieldViewProps>() {
const onDelegate = rawvalue.startsWith('=');
rawvalue = onDelegate ? rawvalue.substring(1) : rawvalue;
const type: 'computed' | 'script' | false = rawvalue.startsWith(':=') ? 'computed' : rawvalue.startsWith('$=') ? 'script' : false;
- rawvalue = type ? rawvalue.substring(2) : rawvalue;
+ rawvalue = type ? rawvalue.substring(2) : rawvalue.replace(/^:/, '');
rawvalue = rawvalue.replace(/.*\(\((.*)\)\)/, 'dashCallChat(_setCacheResult_, this, `$1`)');
const value = ["'", '"', '`'].includes(rawvalue.length ? rawvalue[0] : '') || !isNaN(+rawvalue) ? rawvalue : '`' + rawvalue + '`';
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 209c5abbc..42ac51107 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -56,10 +56,6 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
@computed get pdfUrl() {
return Cast(this.dataDoc[this._props.fieldKey], PdfField);
}
- @computed get pdfThumb() {
- return ImageCast(this.layoutDoc['thumb-frozen'], ImageCast(this.layoutDoc.thumb))?.url;
- }
-
constructor(props: FieldViewProps) {
super(props);
makeObservable(this);
diff --git a/src/client/views/nodes/calendarBox/CalendarBox.scss b/src/client/views/nodes/calendarBox/CalendarBox.scss
new file mode 100644
index 000000000..f8ac4b2d1
--- /dev/null
+++ b/src/client/views/nodes/calendarBox/CalendarBox.scss
@@ -0,0 +1,25 @@
+.calendarBox {
+ display: flex;
+ width: 100%;
+ height: 100%;
+ transform-origin: top left;
+ .calendarBox-wrapper {
+ width: 100%;
+ height: 100%;
+ .fc-timegrid-body {
+ width: 100% !important;
+ table {
+ width: 100% !important;
+ }
+ }
+ .fc-col-header {
+ width: 100% !important;
+ }
+ .fc-daygrid-body {
+ width: 100% !important;
+ .fc-scrollgrid-sync-table {
+ width: 100% !important;
+ }
+ }
+ }
+}
diff --git a/src/client/views/nodes/calendarBox/CalendarBox.tsx b/src/client/views/nodes/calendarBox/CalendarBox.tsx
index bd66941c3..678b7dd0b 100644
--- a/src/client/views/nodes/calendarBox/CalendarBox.tsx
+++ b/src/client/views/nodes/calendarBox/CalendarBox.tsx
@@ -1,127 +1,207 @@
-import { Calendar, EventSourceInput } from '@fullcalendar/core';
+import { Calendar, DateInput, EventClickArg, EventSourceInput } from '@fullcalendar/core';
import dayGridPlugin from '@fullcalendar/daygrid';
import multiMonthPlugin from '@fullcalendar/multimonth';
-import { makeObservable } from 'mobx';
+import timeGrid from '@fullcalendar/timegrid';
+import interactionPlugin from '@fullcalendar/interaction';
+import { IReactionDisposer, action, computed, makeObservable, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { dateRangeStrToDates } from '../../../../ClientUtils';
import { Doc } from '../../../../fields/Doc';
-import { StrCast } from '../../../../fields/Types';
-import { DocumentType } from '../../../documents/DocumentTypes';
-import { Docs } from '../../../documents/Documents';
-import { ViewBoxBaseComponent } from '../../DocComponent';
-import { FieldView, FieldViewProps } from '../FieldView';
-
-type CalendarView = 'month' | 'multi-month' | 'week';
+import { BoolCast, NumCast, StrCast } from '../../../../fields/Types';
+import { CollectionSubView, SubCollectionViewProps } from '../../collections/CollectionSubView';
+import './CalendarBox.scss';
+import { Id } from '../../../../fields/FieldSymbols';
+import { DocServer } from '../../../DocServer';
+import { DocumentView } from '../DocumentView';
+import { OpenWhere } from '../OpenWhere';
+import { DragManager } from '../../../util/DragManager';
+import { DocData } from '../../../../fields/DocSymbols';
+
+type CalendarView = 'multiMonth' | 'dayGridMonth' | 'timeGridWeek' | 'timeGridDay';
@observer
-export class CalendarBox extends ViewBoxBaseComponent<FieldViewProps>() {
- public static LayoutString(fieldKey: string = 'calendar') {
- return FieldView.LayoutString(CalendarBox, fieldKey);
- }
-
- constructor(props: FieldViewProps) {
+export class CalendarBox extends CollectionSubView() {
+ _calendarRef: HTMLDivElement | null = null;
+ _calendar: Calendar | undefined;
+ _oldWheel: HTMLElement | null = null;
+ _observer: ResizeObserver | undefined;
+ _eventsDisposer: IReactionDisposer | undefined;
+ _selectDisposer: IReactionDisposer | undefined;
+
+ constructor(props: SubCollectionViewProps) {
super(props);
makeObservable(this);
}
- componentDidMount(): void {}
-
- componentWillUnmount(): void {}
-
- _calendarRef = React.createRef<HTMLElement>();
+ @observable _multiMonth = 0;
+ isMultiMonth: boolean | undefined;
- get dateRangeStr() {
- return StrCast(this.Document.date_range);
+ componentDidMount(): void {
+ this._props.setContentViewBox?.(this);
+ this._eventsDisposer = reaction(
+ () => ({ events: this.calendarEvents }),
+ ({ events }) => this._calendar?.setOption('events', events),
+ { fireImmediately: true }
+ );
+ this._selectDisposer = reaction(
+ () => ({ initialDate: this.dateSelect }),
+ ({ initialDate }) => {
+ const state = this._calendar?.getCurrentData();
+ state &&
+ this._calendar?.dispatch({
+ 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)));
+ },
+ { fireImmediately: true }
+ );
}
-
- // Choose a calendar view based on the date range
- get calendarViewType(): CalendarView {
- const [fromDate, toDate] = dateRangeStrToDates(this.dateRangeStr);
-
- if (fromDate.getFullYear() !== toDate.getFullYear() || fromDate.getMonth() !== toDate.getMonth()) return 'multi-month';
-
- if (Math.abs(fromDate.getDay() - toDate.getDay()) > 7) return 'month';
- return 'week';
+ componentWillUnmount(): void {
+ this._eventsDisposer?.();
+ this._selectDisposer?.();
}
- get calendarStartDate() {
- return this.dateRangeStr.split('|')[0];
+ @computed get calendarEvents(): EventSourceInput | undefined {
+ return this.childDocs.map(doc => {
+ const { start, end } = dateRangeStrToDates(StrCast(doc.date_range));
+ return {
+ title: StrCast(doc.title),
+ start,
+ end,
+ groupId: doc[Id],
+ startEditable: true,
+ endEditable: true,
+ allDay: BoolCast(doc.allDay),
+ classNames: ['mother'], // will determine the style
+ editable: true, // subject to change in the future
+ backgroundColor: this.eventToColor(doc),
+ borderColor: this.eventToColor(doc),
+ color: 'white',
+ extendedProps: {
+ description: StrCast(doc.description),
+ },
+ };
+ });
}
- get calendarToDate() {
- return this.dateRangeStr.split('|')[1];
+ @computed get dateRangeStrDates() {
+ return dateRangeStrToDates(StrCast(this.Document.date_range));
}
-
- get childDocs(): Doc[] {
- return this.childDocs; // get all sub docs for a calendar
+ get dateSelect() {
+ return dateRangeStrToDates(StrCast(this.Document.date));
}
- docBackgroundColor(type: string): string {
- // TODO: Return a different color based on the event type
- console.log(type);
- return 'blue';
+ // 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';
+ 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';
+ return 'timeGridWeek';
}
- get calendarEvents(): EventSourceInput | undefined {
- if (this.childDocs.length === 0) return undefined;
- return this.childDocs.map(doc => {
- const docTitle = StrCast(doc.title);
- const docDateRange = StrCast(doc.date_range);
- const [startDate, endDate] = dateRangeStrToDates(docDateRange);
- const docType = doc.type;
- const docDescription = doc.description ? StrCast(doc.description) : '';
+ // TODO: Return a different color based on the event type
+ eventToColor(event: Doc): string {
+ return 'red';
+ }
- return {
- title: docTitle,
- start: startDate,
- end: endDate,
- allDay: false,
- classNames: [StrCast(docType)], // will determine the style
- editable: false, // subject to change in the future
- backgroundColor: this.docBackgroundColor(StrCast(doc.type)),
- color: 'white',
- extendedProps: {
- description: docDescription,
- },
- };
+ internalDocDrop(e: Event, de: DragManager.DropEvent, docDragData: DragManager.DocumentDragData) {
+ if (!super.onInternalDrop(e, de)) return false;
+ de.complete.docDragData?.droppedDocuments.forEach(doc => {
+ const today = new Date().toISOString();
+ if (!doc.date_range) doc[DocData].date_range = `${today}|${today}`;
});
+ return true;
}
- handleEventClick = (/* arg: EventClickArg */) => {
- // TODO: open popover with event description, option to open CalendarManager and change event date, delete event, etc.
+ onInternalDrop = (e: Event, de: DragManager.DropEvent): boolean => {
+ if (de.complete.docDragData?.droppedDocuments.length) return this.internalDocDrop(e, de, de.complete.docDragData);
+ return false;
};
- calendarEl: HTMLElement = document.getElementById('calendar-box-v1')!;
+ handleEventClick = (arg: EventClickArg) => {
+ const doc = DocServer.GetCachedRefField(arg.event._def.groupId ?? '');
+ DocumentView.DeselectAll();
+ if (doc) {
+ DocumentView.showDocument(doc, { openLocation: OpenWhere.lightboxAlways });
+ arg.jsEvent.stopPropagation();
+ }
+ };
// https://fullcalendar.io
- get calendar() {
- return new Calendar(this.calendarEl, {
- plugins: [this.calendarViewType === 'multi-month' ? multiMonthPlugin : dayGridPlugin],
- headerToolbar: {
- left: 'prev,next today',
- center: 'title',
- right: 'dayGridMonth,timeGridWeek,timeGridDay,listWeek',
- },
- initialDate: this.calendarStartDate,
- navLinks: true,
- editable: false,
- displayEventTime: false,
- displayEventEnd: false,
- events: this.calendarEvents,
- eventClick: this.handleEventClick,
- });
- }
+ 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,
+ }));
+ cal?.render();
+ setTimeout(() => cal?.view.calendar.select(this.dateSelect.start, this.dateSelect.end));
+ };
+ onPassiveWheel = (e: WheelEvent) => e.stopPropagation();
render() {
return (
- <div className="calendar-box-conatiner">
- <div id="calendar-box-v1" />
+ <div
+ key={this.calendarViewType}
+ className="calendarBox"
+ 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';
+ })
+ );
+ }}
+ 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._oldWheel?.removeEventListener('wheel', this.onPassiveWheel);
+ this._oldWheel = r;
+ // prevent wheel events from passively propagating up through containers and prevents containers from preventDefault which would block scrolling
+ r?.addEventListener('wheel', this.onPassiveWheel, { passive: false });
+
+ 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>
);
}
}
-Docs.Prototypes.TemplateMap.set(DocumentType.CALENDAR, {
- layout: { view: CalendarBox, dataField: 'data' },
- options: { acl: '' },
-});
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 996c6843e..1d3251496 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -2043,7 +2043,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
style={{
cursor: this._props.isContentActive() ? 'text' : undefined,
height: this._props.height ? 'max-content' : undefined,
- overflow: this.layout_autoHeight ? 'hidden' : undefined,
pointerEvents: Doc.ActiveTool === InkTool.None && !SnappingManager.ExploreMode ? undefined : 'none',
}}
onContextMenu={this.specificContextMenu}
@@ -2062,7 +2061,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
}}
style={{
width: this.noSidebar ? '100%' : `calc(100% - ${this.layout_sidebarWidthPercent})`,
- overflow: this.layoutDoc._createDocOnCR ? 'hidden' : this.layoutDoc._layout_autoHeight ? 'visible' : undefined,
+ overflow: this.layoutDoc._createDocOnCR || this.layoutDoc._layout_hideScroll ? 'hidden' : this.layout_autoHeight ? 'visible' : undefined,
}}
onScroll={this.onScroll}
onDrop={this.ondrop}>
diff --git a/src/server/ApiManagers/SearchManager.ts b/src/server/ApiManagers/SearchManager.ts
deleted file mode 100644
index 684b49eaf..000000000
--- a/src/server/ApiManagers/SearchManager.ts
+++ /dev/null
@@ -1,200 +0,0 @@
-/* eslint-disable no-use-before-define */
-import { exec } from 'child_process';
-import { cyan, green, red, yellow } from 'colors';
-import { logExecution } from '../ActionUtilities';
-import { Method } from '../RouteManager';
-import RouteSubscriber from '../RouteSubscriber';
-import { Search } from '../Search';
-import { Database } from '../database';
-import ApiManager, { Registration } from './ApiManager';
-
-export class SearchManager extends ApiManager {
- protected initialize(register: Registration): void {
- register({
- method: Method.GET,
- subscription: new RouteSubscriber('solr').add('action'),
- secureHandler: async ({ req, res }) => {
- const { action } = req.params;
- switch (action) {
- case 'start':
- case 'stop':
- {
- const status = req.params.action === 'start';
- SolrManager.SetRunning(status);
- }
- break;
- case 'update':
- await SolrManager.update();
- break;
- default:
- console.log(yellow(`${action} is an unknown solr operation.`));
- }
- res.redirect('/home');
- },
- });
-
- register({
- method: Method.GET,
- subscription: '/dashsearch',
- secureHandler: async ({ req, res }) => {
- const solrQuery: any = {};
- ['q', 'fq', 'start', 'rows', 'sort', 'hl.maxAnalyzedChars', 'hl', 'hl.fl'].forEach(key => {
- solrQuery[key] = req.query[key];
- });
- if (solrQuery.q === undefined) {
- res.send([]);
- return;
- }
- const results = await Search.search(solrQuery);
- res.send(results);
- },
- });
- }
-}
-
-export namespace SolrManager {
- export function SetRunning(status: boolean) {
- const args = status ? 'start' : 'stop -p 8983';
- console.log(`solr management: trying to ${args}`);
- exec(`solr ${args}`, { cwd: './solr-8.3.1/bin' }, (error, stdout, stderr) => {
- if (error) {
- console.log(red(`solr management error: unable to ${args} server`));
- console.log(red(error.message));
- }
- console.log(cyan(stdout));
- console.log(yellow(stderr));
- });
- if (status) {
- console.log(cyan('Start script is executing: please allow 15 seconds for solr to start on port 8983.'));
- }
- }
-
- export async function update() {
- console.log(green('Beginning update...'));
- await logExecution<void>({
- startMessage: 'Clearing existing Solr information...',
- endMessage: 'Solr information successfully cleared',
- action: Search.clear,
- color: cyan,
- });
- const cursor = await logExecution({
- startMessage: 'Connecting to and querying for all documents from database...',
- endMessage: ({ result, error }) => {
- const success = error === null && result !== undefined;
- if (!success) {
- console.log(red('Unable to connect to the database.'));
- process.exit(0);
- }
- return 'Connection successful and query complete';
- },
- action: () => Database.Instance.query({}),
- color: yellow,
- });
- const updates: any[] = [];
- let numDocs = 0;
- function updateDoc(doc: any) {
- numDocs++;
- if (numDocs % 50 === 0) {
- console.log(`Batch of 50 complete, total of ${numDocs}`);
- }
- if (doc.__type !== 'Doc') {
- return;
- }
- const { fields } = doc;
- if (!fields) {
- return;
- }
- const update2: any = { id: doc._id };
- let dynfield = false;
- fields.forEach((key: any) => {
- const value = fields[key];
- const term = ToSearchTerm(value);
- if (term !== undefined) {
- const { suffix, value: tvalue } = term;
- if (key.endsWith('modificationDate')) {
- update2['modificationDate' + suffix] = tvalue;
- }
- update2[key + suffix] = value;
- dynfield = true;
- }
- });
- if (dynfield) {
- updates.push(update2);
- }
- }
- await cursor?.forEach(updateDoc);
- const result = await logExecution({
- startMessage: `Dispatching updates for ${updates.length} documents`,
- endMessage: 'Dispatched updates complete',
- action: () => Search.updateDocuments(updates),
- color: cyan,
- });
- try {
- if (result) {
- const { status } = JSON.parse(result).responseHeader;
- console.log(status ? red(`Failed with status code (${status})`) : green('Success!'));
- } else {
- console.log(red('Solr is likely not running!'));
- }
- } catch (e) {
- console.log(red('Error:'));
- console.log(e);
- console.log('\n');
- }
- await cursor?.close();
- }
-
- const suffixMap: { [type: string]: string | [string, string | ((json: any) => any)] } = {
- number: '_n',
- string: '_t',
- boolean: '_b',
- image: ['_t', 'url'],
- video: ['_t', 'url'],
- pdf: ['_t', 'url'],
- audio: ['_t', 'url'],
- web: ['_t', 'url'],
- map: ['_t', 'url'],
- date: ['_d', value => new Date(value.date).toISOString()],
- proxy: ['_i', 'fieldId'],
- prefetch_proxy: ['_i', 'fieldId'],
- list: [
- '_l',
- list => {
- const results = [];
- // eslint-disable-next-line no-restricted-syntax
- for (const value of list.fields) {
- const term = ToSearchTerm(value);
- if (term) {
- results.push(term.value);
- }
- }
- return results.length ? results : null;
- },
- ],
- };
-
- function ToSearchTerm(valIn: any): { suffix: string; value: any } | undefined {
- let val = valIn;
- if (val === null || val === undefined) {
- return undefined;
- }
- const type = val.__type || typeof val;
- let suffix = suffixMap[type];
- if (!suffix) {
- return undefined;
- }
-
- if (Array.isArray(suffix)) {
- const accessor = suffix[1];
- if (typeof accessor === 'function') {
- val = accessor(val);
- } else {
- val = val[accessor];
- }
- // eslint-disable-next-line prefer-destructuring
- suffix = suffix[0];
- }
-
- return { suffix, value: val };
- }
-}
diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts
index b2624f654..868373474 100644
--- a/src/server/ApiManagers/UploadManager.ts
+++ b/src/server/ApiManagers/UploadManager.ts
@@ -1,20 +1,18 @@
import * as AdmZip from 'adm-zip';
import * as formidable from 'formidable';
import * as fs from 'fs';
-import { createReadStream, createWriteStream, unlink } from 'fs';
+import { unlink } from 'fs';
import * as imageDataUri from 'image-data-uri';
-import Jimp from 'jimp';
import * as path from 'path';
import * as uuid from 'uuid';
import { retrocycle } from '../../decycler/decycler';
import { DashVersion } from '../../fields/DocSymbols';
-import { DashUploadUtils, InjectSize, SizeSuffix } from '../DashUploadUtils';
+import { DashUploadUtils, InjectSize, SizeSuffix, workerResample } from '../DashUploadUtils';
import { Method, _success } from '../RouteManager';
import { AcceptableMedia, Upload } from '../SharedMediaTypes';
-import { clientPathToFile, Directory, pathToDirectory, publicDirectory, serverPathToFile } from '../SocketData';
+import { Directory, clientPathToFile, pathToDirectory, publicDirectory, serverPathToFile } from '../SocketData';
import { Database } from '../database';
import ApiManager, { Registration } from './ApiManager';
-import { SolrManager } from './SearchManager';
export default class UploadManager extends ApiManager {
protected initialize(register: Registration): void {
@@ -185,7 +183,7 @@ export default class UploadManager extends ApiManager {
let linkids: string[] = [];
try {
// eslint-disable-next-line no-restricted-syntax
- for (const name in Object.keys(files)) {
+ for (const name in files) {
if (Object.prototype.hasOwnProperty.call(files, name)) {
const f = files[name];
// eslint-disable-next-line no-continue
@@ -194,26 +192,17 @@ export default class UploadManager extends ApiManager {
const zip = new AdmZip(path2.filepath);
zip.getEntries().forEach(entry => {
const entryName = entry.entryName.replace(/%%%/g, '/');
- if (!entryName.startsWith('files/')) {
- return;
- }
- const extension = path.extname(entryName);
- const pathname = publicDirectory + '/' + entry.entryName;
- const targetname = publicDirectory + '/' + entryName;
- try {
- zip.extractEntryTo(entry.entryName, publicDirectory, true, false);
- createReadStream(pathname).pipe(createWriteStream(targetname));
- Jimp.read(pathname).then(imgIn => {
- let img = imgIn;
- DashUploadUtils.imageResampleSizes(extension).forEach(({ width, suffix }) => {
- const outputPath = InjectSize(targetname, suffix);
- if (!width) createReadStream(pathname).pipe(createWriteStream(outputPath));
- else img = img.resize(width, Jimp.AUTO).write(outputPath);
- });
- unlink(pathname, () => {});
- });
- } catch (e) {
- console.log(e);
+ if (entryName.startsWith('files/')) {
+ const pathname = publicDirectory + '/' + entry.entryName;
+ const targetname = publicDirectory + '/' + entryName;
+ try {
+ zip.extractEntryTo(entry.entryName, publicDirectory, true, false);
+ const extension = path.extname(targetname).toLowerCase();
+ const basefilename = targetname.substring(0, targetname.length - extension.length);
+ workerResample(pathname, basefilename.replace(/_o$/, '') + extension, SizeSuffix.Original, true);
+ } catch (e) {
+ console.log(e);
+ }
}
});
const json = zip.getEntry('docs.json');
@@ -244,7 +233,6 @@ export default class UploadManager extends ApiManager {
unlink(path2.filepath, () => {});
}
}
- SolrManager.update();
res.send(JSON.stringify({ id, docids, linkids }) || 'error');
} catch (e) {
console.log(e);
@@ -287,16 +275,9 @@ export default class UploadManager extends ApiManager {
}
imageDataUri.outputFile(uri, serverPathToFile(Directory.images, InjectSize(filename, origSuffix))).then((savedName: string) => {
const ext = path.extname(savedName).toLowerCase();
+ const outputPath = serverPathToFile(Directory.images, filename + ext);
if (AcceptableMedia.imageFormats.includes(ext)) {
- Jimp.read(savedName).then(imgIn => {
- let img = imgIn;
- (!origSuffix ? [{ width: 400, suffix: SizeSuffix.Medium }] : Object.values(DashUploadUtils.Sizes)) //
- .forEach(({ width, suffix }) => {
- const outputPath = serverPathToFile(Directory.images, InjectSize(filename, suffix) + ext);
- if (!width) createReadStream(savedName).pipe(createWriteStream(outputPath));
- else img = img.resize(width, Jimp.AUTO).write(outputPath);
- });
- });
+ workerResample(savedName, outputPath, origSuffix, false);
}
res.send(clientPathToFile(Directory.images, filename + ext));
});
diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts
index 8f012f783..1e55a885a 100644
--- a/src/server/DashUploadUtils.ts
+++ b/src/server/DashUploadUtils.ts
@@ -1,5 +1,5 @@
import axios from 'axios';
-import { spawn, exec } from 'child_process';
+import { exec, spawn } from 'child_process';
import { green, red } from 'colors';
import { ExifData, ExifImage } from 'exif';
import * as exifr from 'exifr';
@@ -8,8 +8,7 @@ import * as formidable from 'formidable';
import { File } from 'formidable';
import * as fs from 'fs';
import { createReadStream, createWriteStream, existsSync, readFileSync, rename, unlinkSync, writeFile } from 'fs';
-import Jimp from 'jimp';
-import * as autorotate from 'jpeg-autorotate';
+import { Jimp } from 'jimp';
import * as md5File from 'md5-file';
import * as path from 'path';
import { basename } from 'path';
@@ -23,6 +22,44 @@ import { AcceptableMedia, Upload } from './SharedMediaTypes';
import { Directory, clientPathToFile, filesDirectory, pathToDirectory, publicDirectory, serverPathToFile } from './SocketData';
import { resolvedServerUrl } from './server_Initialization';
+import { Worker, isMainThread, parentPort } from 'worker_threads';
+
+// Create an array to store worker threads
+enum workertasks {
+ JIMP = 'jimp',
+}
+const JimpWorker: Worker | undefined = isMainThread ? new Worker(__filename) : undefined;
+export const workerResample = (imgSourcePath: string, outputPath: string, origSuffix: SizeSuffix, unlinkSource: boolean) => {
+ JimpWorker?.postMessage({ task: workertasks.JIMP, imgSourcePath, outputPath, origSuffix, unlinkSource });
+};
+
+if (isMainThread) {
+ // main thread code if needed ...
+} else {
+ // Worker thread code - Listens for messages from the main thread
+ parentPort?.on('message', message => {
+ switch (message.task) {
+ case workertasks.JIMP:
+ return workerResampleImage(message);
+ default:
+ }
+ });
+
+ async function workerResampleImage(message: { imgSourcePath: string; outputPath: string; origSuffix: string; unlinkSource: boolean }) {
+ const { imgSourcePath, outputPath, origSuffix, unlinkSource } = message;
+ const sizes = !origSuffix ? [{ width: 400, suffix: SizeSuffix.Medium }] : DashUploadUtils.imageResampleSizes(path.extname(imgSourcePath));
+ // prettier-ignore
+ Jimp.read(imgSourcePath)
+ .then(img =>
+ sizes.forEach(({ width, suffix }) =>
+ img.resize({ w: width || img.bitmap.width })
+ .write(InjectSize(outputPath, suffix) as `${string}.${string}`)
+ ))
+ .catch(e => console.log('Error Jimp:', e))
+ .finally(() => unlinkSource && unlinkSync(imgSourcePath));
+ }
+}
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
const requestImageSize = require('../client/util/request-image-size');
@@ -277,15 +314,6 @@ export namespace DashUploadUtils {
}
};
- async function correctRotation(imgSourcePath: string) {
- const buffer = fs.readFileSync(imgSourcePath);
- try {
- return (await autorotate.rotate(buffer, { quality: 30 })).buffer;
- } catch (e) {
- return buffer;
- }
- }
-
/**
* define the resizers to use
* @param ext the extension
@@ -310,38 +338,23 @@ export namespace DashUploadUtils {
* @param outputDirectory the directory to output to, usually Directory.Images
* @returns a map with suffixes as keys and resized filenames as values.
*/
- export async function outputResizedImages(imgSourcePath: string, outputFileName: string, outputDirectory: string) {
+ export async function outputResizedImages(imgSourcePath: string, outputFileName: string, unlinkSource: boolean) {
const writtenFiles: { [suffix: string]: string } = {};
+ const outputPath = path.resolve(pathToDirectory(Directory.images), outputFileName);
const sizes = imageResampleSizes(path.extname(outputFileName));
- const imgBuffer = await correctRotation(imgSourcePath);
const imgReadStream = new Duplex();
- imgReadStream.push(imgBuffer);
+ imgReadStream.push(fs.readFileSync(imgSourcePath));
imgReadStream.push(null);
- const outputPath = (suffix: SizeSuffix) => {
- writtenFiles[suffix] = InjectSize(outputFileName, suffix);
- return path.resolve(outputDirectory, writtenFiles[suffix]);
- };
await Promise.all(
- sizes.filter(({ width }) => !width).map(({ suffix }) =>
- new Promise<void>(res => {
- imgReadStream.pipe(createWriteStream(outputPath(suffix))).on('close', res);
- })
+ sizes.map(({ suffix }) =>
+ new Promise<unknown>(res =>
+ imgReadStream.pipe(createWriteStream(writtenFiles[suffix] = InjectSize(outputPath, suffix))).on('close', res)
+ )
)); // prettier-ignore
- return Jimp.read(imgBuffer)
- .then(async imgIn => {
- let img = imgIn;
- await Promise.all( sizes.filter(({ width }) => width).map(({ width, suffix }) => {
- img = img.resize(width, Jimp.AUTO).write(outputPath(suffix));
- return img;
- } )); // prettier-ignore
- return writtenFiles;
- })
- .catch(e => {
- console.log('ERROR' + e);
- return writtenFiles;
- });
+ workerResample(imgSourcePath, outputPath, SizeSuffix.Original, unlinkSource);
+ return writtenFiles;
}
/**
@@ -387,8 +400,9 @@ export namespace DashUploadUtils {
writtenFiles = {};
}
} else {
+ const unlinkSrcWhenFinished = isLocal().test(source) && cleanUp;
try {
- writtenFiles = await outputResizedImages(metadata.source, resolved, pathToDirectory(Directory.images));
+ writtenFiles = await outputResizedImages(metadata.source, resolved, unlinkSrcWhenFinished);
} catch (e) {
// input is a blob or other, try reading it to create a metadata source file.
const reqSource = request(metadata.source);
@@ -400,16 +414,14 @@ export namespace DashUploadUtils {
.on('close', () => res())
.on('error', () => rej());
});
- writtenFiles = await outputResizedImages(readSource, resolved, pathToDirectory(Directory.images));
+ writtenFiles = await outputResizedImages(readSource, resolved, unlinkSrcWhenFinished);
fs.unlink(readSource, err => console.log("Couldn't unlink temporary image file:" + readSource, err));
}
}
Array.from(Object.keys(writtenFiles)).forEach(suffix => {
information.accessPaths[suffix] = getAccessPaths(images, writtenFiles[suffix]);
});
- if (isLocal().test(source) && cleanUp) {
- unlinkSync(source);
- }
+
return information;
};
diff --git a/src/server/index.ts b/src/server/index.ts
index 3e0d86814..88dbd232d 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -8,7 +8,6 @@ import DataVizManager from './ApiManagers/DataVizManager';
import DeleteManager from './ApiManagers/DeleteManager';
import DownloadManager from './ApiManagers/DownloadManager';
import GeneralGoogleManager from './ApiManagers/GeneralGoogleManager';
-import { SearchManager } from './ApiManagers/SearchManager';
import SessionManager from './ApiManagers/SessionManager';
import UploadManager from './ApiManagers/UploadManager';
import UserManager from './ApiManagers/UserManager';
@@ -67,7 +66,6 @@ function routeSetter({ addSupervisedRoute, logRegistrationOutcome }: RouteManage
new UserManager(),
new UploadManager(),
new DownloadManager(),
- new SearchManager(),
new DeleteManager(),
new UtilManager(),
new GeneralGoogleManager(),