diff options
Diffstat (limited to 'src')
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} </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(), |