From 299398cb7e21259de3bf7597995840b84e7e0590 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 9 May 2025 23:01:36 -0400
Subject: added user templates to template_user global field. fixed schemas to
 not erase field values when setting a new column name. fixed templates to
 render properly when switching by fixing contentsRef to be observable. fixed
 templates with fields in stacks to autoHeight update correctly by .  fixed
 images in templates to set their nativeDim after rendering.  fixed text views
 of descriptions to auto-updated by setting the modification date of the
 description field.

---
 src/client/views/collections/CollectionStackingViewFieldColumn.tsx | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

(limited to 'src/client/views/collections/CollectionStackingViewFieldColumn.tsx')

diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
index 994669734..345f60e75 100644
--- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
+++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
@@ -71,6 +71,7 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
     }
 
     _ele: HTMLElement | null = null;
+    _eleMasonrySingle = React.createRef<HTMLDivElement>();
 
     protected onInternalPreDrop = (e: Event, de: DragManager.DropEvent, targetDropAction: dropActionType) => {
         const dragData = de.complete.docDragData;
@@ -92,13 +93,13 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
     createColumnDropRef = (ele: HTMLDivElement | null) => {
         this.dropDisposer?.();
         if (ele) this.dropDisposer = DragManager.MakeDropTarget(ele, this.columnDrop.bind(this), this._props.Doc, this.onInternalPreDrop.bind(this));
-        else if (this._ele) this.props.refList.splice(this.props.refList.indexOf(this._ele), 1);
+        else if (this._eleMasonrySingle.current) this.props.refList.splice(this.props.refList.indexOf(this._eleMasonrySingle.current), 1);
         this._ele = ele;
     };
 
     @action
     componentDidMount() {
-        this._ele && this.props.refList.push(this._ele);
+        this._eleMasonrySingle.current && this.props.refList.push(this._eleMasonrySingle.current);
         this._disposers.collapser = reaction(
             () => this._props.headingObject?.collapsed,
             collapsed => { this.collapsed = collapsed !== undefined ? BoolCast(collapsed) : false; }, // prettier-ignore
@@ -363,6 +364,7 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
                         }}>
                         <div
                             key={`${heading}-stack`}
+                            ref={this._eleMasonrySingle}
                             className="collectionStackingView-masonrySingle"
                             style={{
                                 padding: `${columnYMargin}px ${0}px ${this._props.yMargin}px ${0}px`,
-- 
cgit v1.2.3-70-g09d2


From 51b4b64eae4749542590785bca9d33d3ccdded69 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 19 May 2025 19:43:28 -0400
Subject: fixed text boxes to calculate height correctly.  removed usertag
 modification time in text.  fixed masonry and stacking views with pivots.

---
 src/ClientUtils.ts                                 |   3 +-
 .../collections/CollectionMasonryViewFieldRow.tsx  |  42 +++---
 .../views/collections/CollectionStackingView.scss  |   5 +-
 .../views/collections/CollectionStackingView.tsx   | 154 ++++++++-------------
 .../CollectionStackingViewFieldColumn.tsx          |  30 ++--
 .../views/nodes/formattedText/FormattedTextBox.tsx |  25 ++--
 src/client/views/nodes/formattedText/marks_rts.ts  |  14 --
 7 files changed, 111 insertions(+), 162 deletions(-)

(limited to 'src/client/views/collections/CollectionStackingViewFieldColumn.tsx')

diff --git a/src/ClientUtils.ts b/src/ClientUtils.ts
index cc8b715b4..8eb1a39ec 100644
--- a/src/ClientUtils.ts
+++ b/src/ClientUtils.ts
@@ -670,7 +670,8 @@ export function setupMoveUpEvents(
 }
 
 export function DivHeight(ele: HTMLElement | null): number {
-    return ele ? Number(getComputedStyle(ele).height.replace('px', '')) : 0;
+    const hgt = ele ? Number(getComputedStyle(ele).height.replace('px', '')) : 0;
+    return Number.isNaN(hgt) ? 0 : hgt;
 }
 export function DivWidth(ele: HTMLElement | null): number {
     return ele ? Number(getComputedStyle(ele).width.replace('px', '')) : 0;
diff --git a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
index 89ccf5a0f..d1f7971d4 100644
--- a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
+++ b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
@@ -28,12 +28,14 @@ interface CMVFieldRowProps {
     headingObject: SchemaHeaderField | undefined;
     docList: Doc[];
     parent: CollectionStackingView;
+    panelWidth: () => number;
+    columnWidth: () => number;
     pivotField: string;
     type: 'string' | 'number' | 'bigint' | 'boolean' | 'symbol' | 'undefined' | 'object' | 'function' | undefined;
     createDropTarget: (ele: HTMLDivElement) => void;
     screenToLocalTransform: () => Transform;
     setDocHeight: (key: string, thisHeight: number) => void;
-    refList: Element[];
+    sectionRefs: Element[];
     showHandle: boolean;
 }
 
@@ -74,7 +76,7 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent<CMVF
     createRowDropRef = (ele: HTMLDivElement | null) => {
         this._dropDisposer?.();
         if (ele) this._dropDisposer = DragManager.MakeDropTarget(ele, this.rowDrop.bind(this), this._props.Doc);
-        else if (this._ele) this.props.refList.splice(this.props.refList.indexOf(this._ele), 1);
+        else if (this._ele) this.props.sectionRefs.splice(this.props.sectionRefs.indexOf(this._ele), 1);
         this._ele = ele;
     };
     @action
@@ -82,10 +84,10 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent<CMVF
         this.heading = this._props.headingObject?.heading || '';
         this.color = this._props.headingObject?.color || '#f1efeb';
         this.collapsed = this._props.headingObject?.collapsed || false;
-        this._ele && this.props.refList.push(this._ele);
+        this._ele && this.props.sectionRefs.push(this._ele);
     }
     componentWillUnmount() {
-        this._ele && this.props.refList.splice(this.props.refList.indexOf(this._ele), 1);
+        this._ele && this.props.sectionRefs.splice(this.props.sectionRefs.indexOf(this._ele), 1);
         this._ele = null;
     }
 
@@ -128,10 +130,8 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent<CMVF
         const key = this._props.pivotField;
         const castedValue = this.getValue(value);
         if (castedValue) {
-            if (this._props.parent.colHeaderData) {
-                if (this._props.parent.colHeaderData.map(i => i.heading).indexOf(castedValue.toString()) > -1) {
-                    return false;
-                }
+            if (this._props.parent.colHeaderData?.map(i => i.heading).indexOf(castedValue.toString()) || 0 > -1) {
+                return false;
             }
             key && this._props.docList.forEach(d => Doc.SetInPlace(d, key, castedValue, true));
             this._heading = castedValue.toString();
@@ -251,20 +251,11 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent<CMVF
     textCallback = (/* char: string */) => this.addDocument('', false);
 
     @computed get contentLayout() {
-        const rows = Math.max(1, Math.min(this._props.docList.length, Math.floor((this._props.parent._props.PanelWidth() - 2 * this._props.parent.xMargin) / (this._props.parent.columnWidth + this._props.parent.gridGap))));
-        const showChrome = !this._props.chromeHidden;
-        const stackPad = showChrome ? `0px ${this._props.parent.xMargin}px` : `${this._props.parent.yMargin}px ${this._props.parent.xMargin}px 0px ${this._props.parent.xMargin}px `;
+        const rows = Math.max(1, Math.min(this._props.docList.length, Math.floor(this._props.panelWidth() / this._props.columnWidth())));
         return this.collapsed ? null : (
             <div style={{ position: 'relative' }}>
-                {showChrome ? (
-                    <div
-                        className="collectionStackingView-addDocumentButton"
-                        style={
-                            {
-                                // width: style.columnWidth / style.numGroupColumns,
-                                // padding: `${NumCast(this._props.parent.layoutDoc._yPadding, this._props.parent.yMargin)}px 0px 0px 0px`,
-                            }
-                        }>
+                {!this._props.chromeHidden ? (
+                    <div className="collectionStackingView-addDocumentButton">
                         <EditableView GetValue={returnEmptyString} SetValue={this.addDocument} textCallback={this.textCallback} contents="+ NEW" />
                     </div>
                 ) : null}
@@ -272,11 +263,9 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent<CMVF
                     className="collectionStackingView-masonryGrid"
                     ref={this._contRef}
                     style={{
-                        padding: stackPad,
                         minHeight: this._props.showHandle && this._props.parent._props.isContentActive() ? '10px' : undefined,
-                        width: this._props.parent.NodeWidth,
                         gridGap: this._props.parent.gridGap,
-                        gridTemplateColumns: numberRange(rows).reduce(list => list + ` ${this._props.parent.columnWidth}px`, ''),
+                        gridTemplateColumns: numberRange(rows).reduce(list => list + ` ${this._props.columnWidth()}px`, ''),
                     }}>
                     {this._props.parent.children(this._props.docList)}
                 </div>
@@ -339,7 +328,12 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent<CMVF
     render() {
         const background = this._background;
         return (
-            <div className="collectionStackingView-masonrySection" style={{ width: this._props.parent.NodeWidth, background }} ref={this.createRowDropRef} onPointerEnter={this.pointerEnteredRow} onPointerLeave={this.pointerLeaveRow}>
+            <div
+                className="collectionStackingView-masonrySection"
+                style={{ width: this._props.pivotField ? this._props.panelWidth() : '100%', background }}
+                ref={this.createRowDropRef}
+                onPointerEnter={this.pointerEnteredRow}
+                onPointerLeave={this.pointerLeaveRow}>
                 {this.headingView}
                 {this.contentLayout}
             </div>
diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss
index 05ac52ff9..d6e4943ff 100644
--- a/src/client/views/collections/CollectionStackingView.scss
+++ b/src/client/views/collections/CollectionStackingView.scss
@@ -2,10 +2,12 @@
 
 .collectionMasonryView {
     display: inline;
+    flex-wrap: wrap;
 }
 
 .collectionStackingView {
     display: flex;
+    justify-content: space-between;
 }
 
 .collectionStackingMasonry-cont {
@@ -56,7 +58,6 @@
         top: 0;
         overflow-y: auto;
         overflow-x: hidden;
-        flex-wrap: wrap;
         transition: top 0.5s;
 
         > div {
@@ -210,7 +211,6 @@
     .collectionStackingView-sectionHeader {
         text-align: center;
         margin: auto;
-        margin-bottom: 10px;
         background: global.$medium-gray;
         // overflow: hidden; overflow is visible so the color menu isn't hidden -ftong
 
@@ -367,7 +367,6 @@
     .collectionStackingView-addGroupButton {
         display: flex;
         overflow: hidden;
-        margin: auto;
         width: 90%;
         overflow: ellipses;
 
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 25a222cbb..cc514f262 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -4,7 +4,7 @@ import { action, computed, IReactionDisposer, makeObservable, observable, Observ
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { ClientUtils, DivHeight, returnNone, returnZero, setupMoveUpEvents, smoothScroll } from '../../../ClientUtils';
-import { Doc, Opt } from '../../../fields/Doc';
+import { Doc, Field, Opt } from '../../../fields/Doc';
 import { Id } from '../../../fields/FieldSymbols';
 import { List } from '../../../fields/List';
 import { listSpec } from '../../../fields/Schema';
@@ -35,6 +35,7 @@ import { CollectionStackingViewFieldColumn } from './CollectionStackingViewField
 import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
 import { computedFn } from 'mobx-utils';
 import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
+import { FieldsDropdown } from '../FieldsDropdown';
 
 export type collectionStackingViewProps = {
     sortFunc?: (a: Doc, b: Doc) => number;
@@ -48,15 +49,15 @@ export type collectionStackingViewProps = {
 @observer
 export class CollectionStackingView extends CollectionSubView<Partial<collectionStackingViewProps>>() {
     _disposers: { [key: string]: IReactionDisposer } = {};
+    _addGroupRef = React.createRef<HTMLDivElement>();
     _masonryGridRef: HTMLDivElement | null = null;
     // used in a column dragger, likely due for the masonry grid view. We want to use this
     _draggerRef = React.createRef<HTMLDivElement>();
     // keeping track of documents. Updated on internal and external drops. What's the difference?
     _docXfs: { height: () => number; width: () => number; stackedDocTransform: () => Transform }[] = [];
-    // Doesn't look like this field is being used anywhere. Obsolete?
-    _columnStart: number = 0;
 
-    @observable _refList: HTMLElement[] = [];
+    @observable _colStackRefs: HTMLElement[] = [];
+    @observable _colHdrRefs: HTMLElement[] = [];
     // map of node headers to their heights. Used in Masonry
     @observable _heightMap = new Map<string, number>();
     // Assuming that this is the current css cursor style
@@ -67,10 +68,25 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
     @computed get chromeHidden() {
         return this._props.chromeHidden || BoolCast(this.layoutDoc.chromeHidden);
     }
-    // it looks like this gets the column headers that Mehek was showing just now
     @computed get colHeaderData() {
         return Cast(this.dataDoc['_' + this.fieldKey + '_columnHeaders'], listSpec(SchemaHeaderField), null);
     }
+
+    @computed get Sections() {
+        return this.filteredChildren.reduce(
+            (map, d) => {
+                const docHeader = d[this.pivotField] ? d[this.pivotField] : `NO ${this.pivotField.toUpperCase()} VALUE`;
+                const docHeaderString = docHeader !== undefined ? Field.toString(docHeader) : `NO ${this.pivotField.toUpperCase()} VALUE`;
+
+                // find existing header or create
+                const existingHeader = Array.from(map.keys()).find(sh => sh.heading === docHeaderString);
+                if (!existingHeader) map.set(new SchemaHeaderField(docHeaderString), [d]);
+                else map.get(existingHeader)!.push(d);
+                return map;
+            },
+            new ObservableMap<SchemaHeaderField, Doc[]>(this.colHeaderData?.map(hdata => [hdata, []] as [SchemaHeaderField, Doc[]]) ?? [])
+        );
+    }
     // Still not sure what a pivot is, but it appears that we can actually filter docs somehow?
     @computed get pivotField() {
         return StrCast(this.layoutDoc._pivotField);
@@ -107,9 +123,12 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
     @computed get showAddAGroup() {
         return this.pivotField && !this.chromeHidden;
     }
+    @computed get availableWidth() {
+        return this._props.PanelWidth() - 2 * this.xMargin - (this.isStackingView ? this.gridGap * ((this.numGroupColumns || 1) - 1) : 0);
+    }
     // columnWidth handles the margin on the left and right side of the documents
     @computed get columnWidth() {
-        const availableWidth = this._props.PanelWidth() - 2 * this.xMargin;
+        const availableWidth = this.availableWidth;
         const cwid = availableWidth / (NumCast(this.Document._layout_columnCount) || this._props.PanelWidth() / NumCast(this.Document._layout_columnWidth, this._props.PanelWidth() / 4));
         return Math.min(availableWidth, this.isStackingView ? availableWidth / (this.numGroupColumns || 1) : cwid - this.gridGap);
     }
@@ -121,28 +140,17 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
     constructor(props: SubCollectionViewProps) {
         super(props);
         makeObservable(this);
-        if (this.colHeaderData === undefined) {
-            // TODO: what is a layout doc? Is it literally how this document is supposed to be layed out?
-            // here we're making an empty list of column headers (again, what Mehek showed us)
-            this.dataDoc['_' + this.fieldKey + '_columnHeaders'] = new List<SchemaHeaderField>();
-        }
     }
 
+    availableWidthFn = () => this.availableWidth;
     columnWidthFn = () => this.columnWidth;
     columnDocHeightFn = (doc: Doc) => () => (this.isStackingView ? this.getDocHeight(doc)() : Math.min(this.getDocHeight(doc)(), this._props.PanelHeight()));
 
-    // TODO: plj - these are the children
     children = (docs: Doc[]) => {
-        // TODO: can somebody explain me to what exactly TraceMobX is?
         TraceMobx();
-        // appears that we are going to reset the _docXfs. TODO: what is Xfs?
         this._docXfs.length = 0;
-        this._renderCount < docs.length &&
-            setTimeout(
-                action(() => {
-                    this._renderCount = Math.min(docs.length, this._renderCount + 5);
-                })
-            );
+        this._renderCount < docs.length && 
+            setTimeout(action(() => (this._renderCount = Math.min(docs.length, this._renderCount + 5)))); // prettier-ignore
         return docs.map((d, i) => {
             // assuming we need to get rowSpan because we might be dealing with many columns. Grid gap makes sense if multiple columns
             const rowSpan = Math.ceil((this.getDocHeight(d)() + this.gridGap) / this.gridGap);
@@ -153,76 +161,28 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
                       margin: undefined,
                       transition: this.getDocTransition(d)(),
                       width: this.columnWidth,
-                      marginTop: i ? this.gridGap : 0,
                       height: this.getDocHeight(d)(),
                       zIndex: DocumentView.getFirstDocumentView(d)?.IsSelected ? 1000 : 0,
                   }
                 : { gridRowEnd: `span ${rowSpan}`, zIndex: DocumentView.getFirstDocumentView(d)?.IsSelected ? 1000 : 0 };
             // So we're choosing whether we're going to render a column or a masonry doc
             return (
-                <div className={`collectionStackingView-${this.isStackingView ? 'columnDoc' : 'masonryDoc'}`} key={d[Id]} style={style}>
+                <div className={`collectionStackingView${this.isStackingView ? '-columnDoc' : '-masonryDoc'}`} key={d[Id]} style={style}>
                     {this.getDisplayDoc(d, this.getDocTransition(d), i)}
                 </div>
             );
         });
     };
     @action
-    setDocHeight = (key: string, sectionHeight: number) => {
-        this._heightMap.set(key, sectionHeight);
+    setDocHeight = (key: string, sectionHeight: number) => this._heightMap.set(key, sectionHeight);
+
+    setAutoHeight = () => {
+        const maxHeader = this.isStackingView ? this._colHdrRefs.reduce((p, r) => Math.max(p, DivHeight(r)), 0) + (this._colHdrRefs.length ? this.gridGap : 0) : 0;
+        const maxCol = this.isStackingView
+            ? this._colStackRefs.reduce((p, r) => Math.max(p, DivHeight(r)), 0) + this.gridGap
+            : this._colStackRefs.reduce((p, r) => p + DivHeight(r), this._addGroupRef.current ? DivHeight(this._addGroupRef.current) : 0);
+        this._props.setHeight?.(this.headerMargin + 2 * this.yMargin + maxCol + maxHeader);
     };
-
-    // is sections that all collections inherit? I think this is how we show the masonry/columns
-    // TODO: this seems important
-    get Sections() {
-        // appears that pivot field IS actually for sorting
-        if (!this.pivotField || this.colHeaderData instanceof Promise) return new Map<SchemaHeaderField, Doc[]>();
-
-        if (this.colHeaderData === undefined) {
-            setTimeout(() => {
-                this.dataDoc['_' + this.fieldKey + '_columnHeaders'] = new List<SchemaHeaderField>();
-            });
-            return new Map<SchemaHeaderField, Doc[]>();
-        }
-        const colHeaderData = Array.from(this.colHeaderData);
-        const fields = new Map<SchemaHeaderField, Doc[]>(colHeaderData.map(sh => [sh, []] as [SchemaHeaderField, []]));
-        let changed = false;
-        this.filteredChildren.forEach(d => {
-            const sectionValue = (d[this.pivotField] ? d[this.pivotField] : `NO ${this.pivotField.toUpperCase()} VALUE`) as object;
-            // the next five lines ensures that floating point rounding errors don't create more than one section -syip
-            const parsed = parseInt(sectionValue.toString());
-            const castedSectionValue = !isNaN(parsed) ? parsed : sectionValue;
-
-            // look for if header exists already
-            const existingHeader = colHeaderData.find(sh => sh.heading === (castedSectionValue ? castedSectionValue.toString() : `NO ${this.pivotField.toUpperCase()} VALUE`));
-            if (existingHeader) {
-                fields.get(existingHeader)!.push(d);
-            } else {
-                const newSchemaHeader = new SchemaHeaderField(castedSectionValue ? castedSectionValue.toString() : `NO ${this.pivotField.toUpperCase()} VALUE`);
-                fields.set(newSchemaHeader, [d]);
-                colHeaderData.push(newSchemaHeader);
-                changed = true;
-            }
-        });
-        // remove all empty columns if hideHeadings is set
-        // we will want to have something like this, so that we can hide columns and add them back in
-        if (this.layoutDoc._columnsHideIfEmpty) {
-            Array.from(fields.keys())
-                .filter(key => !fields.get(key)!.length)
-                .forEach(header => {
-                    fields.delete(header);
-                    colHeaderData.splice(colHeaderData.indexOf(header), 1);
-                    changed = true;
-                });
-        }
-        changed &&
-            setTimeout(
-                action(() => this.colHeaderData?.splice(0, this.colHeaderData.length, ...colHeaderData)),
-                0
-            );
-        return fields;
-    }
-
-    setAutoHeight = () => this._props.setHeight?.(this.headerMargin + (this.isStackingView ? Math.max(...this._refList.map(DivHeight)) : 2 * this.yMargin + this._refList.reduce((p, r) => p + DivHeight(r), 0)));
     observer = new ResizeObserver(this.setAutoHeight);
 
     componentDidMount() {
@@ -232,9 +192,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
         // reset section headers when a new filter is inputted
         this._disposers.pivotField = reaction(
             () => this.pivotField,
-            () => {
-                this.dataDoc['_' + this.fieldKey + '_columnHeaders'] = new List();
-            }
+            () => (this.dataDoc['_' + this.fieldKey + '_columnHeaders'] = new List())
         );
         // reset section headers when a new filter is inputted
         this._disposers.width = reaction(
@@ -252,7 +210,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
         );
 
         this._disposers.refList = reaction(
-            () => ({ refList: this._refList.slice(), autoHeight: this.layoutDoc._layout_autoHeight && !DocumentView.LightboxContains(this.DocumentView?.()) }),
+            () => ({ refList: this._colStackRefs.slice(), autoHeight: this.layoutDoc._layout_autoHeight && !DocumentView.LightboxContains(this.DocumentView?.()) }),
             ({ refList, autoHeight }) => {
                 this.observer.disconnect();
                 if (autoHeight) refList.forEach(r => this.observer.observe(r));
@@ -409,11 +367,10 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
     getDocWidth = computedFn((d?: Doc) => () => {
         if (!d) return 0;
         const childLayoutDoc = Doc.LayoutDoc(d, this._props.childLayoutTemplate?.());
-        const maxWidth = this.columnWidth / this.numGroupColumns;
         if (!this.layoutDoc._columnsFill && !this.childFitWidth(childLayoutDoc)) {
-            return Math.min(NumCast(d._width), maxWidth);
+            return Math.min(NumCast(d._width), this.columnWidth);
         }
-        return maxWidth;
+        return this.columnWidth;
     });
     getDocTransition = computedFn((d?: Doc) => () => StrCast(d?.dataTransition));
     getDocHeight = computedFn((d?: Doc) => () => {
@@ -424,7 +381,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
         const nw = Doc.NativeWidth(childLayoutDoc, childDataDoc) || (!this.childFitWidth(childLayoutDoc) ? NumCast(d._width) : 0);
         const nh = Doc.NativeHeight(childLayoutDoc, childDataDoc) || (!this.childFitWidth(childLayoutDoc) ? NumCast(d._height) : 0);
         if (nw && nh) {
-            const colWid = this.columnWidth / (this.isStackingView ? this.numGroupColumns : 1);
+            const colWid = this.columnWidth;
             const docWid = this.layoutDoc._columnsFill ? colWid : Math.min(this.getDocWidth(d)(), colWid);
             return Math.min(maxHeight, (docWid * nh) / nw);
         }
@@ -571,7 +528,8 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
         }
         return (
             <CollectionStackingViewFieldColumn
-                refList={this._refList}
+                colStackRefs={this._colStackRefs}
+                colHeaderRefs={this._colHdrRefs}
                 addDocument={this.addDocument}
                 chromeHidden={this.chromeHidden}
                 colHeaderData={this.colHeaderData}
@@ -613,8 +571,10 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
                         showHandle={first}
                         Doc={this.Document}
                         chromeHidden={this.chromeHidden}
+                        panelWidth={this.availableWidthFn}
+                        columnWidth={this.columnWidthFn}
                         pivotField={this.pivotField}
-                        refList={this._refList}
+                        sectionRefs={this._colStackRefs}
                         key={heading ? heading.heading : ''}
                         rows={rows}
                         headings={this.headings}
@@ -636,8 +596,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
     @action
     addGroup = (value: string) => {
         if (value && this.colHeaderData) {
-            const schemaHdrField = new SchemaHeaderField(value);
-            this.colHeaderData.push(schemaHdrField);
+            this.colHeaderData.push(new SchemaHeaderField(value));
             return true;
         }
         return false;
@@ -745,7 +704,11 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
                             this.fixWheelEvents(ele, this._props.isContentActive);
                         }}
                         style={{
-                            overflowY: this.isContentActive() ? 'auto' : 'hidden',
+                            paddingBottom: this.yMargin,
+                            paddingTop: this.yMargin,
+                            paddingLeft: this.xMargin,
+                            paddingRight: this.xMargin,
+                            overflowY: this.isContentActive() && !this.layoutDoc._layout_autoHeight ? 'auto' : 'hidden',
                             background: this._props.styleProvider?.(this.Document, this._props, StyleProp.BackgroundColor) as string,
                             pointerEvents: this._props.pointerEvents?.() ?? this.backgroundEvents,
                         }}
@@ -756,11 +719,12 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
                         onContextMenu={this.onContextMenu}
                         onWheel={e => this.isContentActive() && e.stopPropagation()}>
                         {this.renderedSections}
-                        {!this.showAddAGroup ? null : (
-                            <div key={`${this.Document[Id]}-addGroup`} className="collectionStackingView-addGroupButton" style={{ width: !this.isStackingView ? '100%' : this.columnWidth / this.numGroupColumns - 10, marginTop: 10 }}>
-                                <EditableView {...editableViewProps} />
-                            </div>
-                        )}
+                        <div className="collectionStackingView-addGroupButton" ref={this._addGroupRef} style={{ width: !this.isStackingView ? '100%' : this.columnWidth, display: this.showAddAGroup ? undefined : 'none' }}>
+                            <EditableView {...editableViewProps} />
+                        </div>
+                        <div style={{ right: 0, top: 0, position: 'absolute', display: !this.layoutDoc._pivotField ? 'none' : undefined }}>
+                            <FieldsDropdown Doc={this.Document} selectFunc={fieldKey => (this.layoutDoc._pivotField = fieldKey)} placeholder={StrCast(this.layoutDoc._pivotField)} />
+                        </div>
                     </div>
                 </div>
             </>
diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
index 345f60e75..8c7cb8276 100644
--- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
+++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
@@ -1,5 +1,5 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx';
+import { action, computed, IReactionDisposer, makeObservable, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { DivHeight, DivWidth, returnEmptyString, returnTrue, setupMoveUpEvents } from '../../../ClientUtils';
@@ -50,14 +50,14 @@ interface CSVFieldColumnProps {
     addDocument: (doc: Doc | Doc[]) => boolean;
     createDropTarget: (ele: HTMLDivElement) => void;
     screenToLocalTransform: () => Transform;
-    refList: HTMLElement[];
+    colStackRefs: HTMLElement[];
+    colHeaderRefs: HTMLElement[];
 }
 
 @observer
 export class CollectionStackingViewFieldColumn extends ObservableReactComponent<CSVFieldColumnProps> {
     private dropDisposer?: DragManager.DragDropDisposer;
     private _disposers: { [name: string]: IReactionDisposer } = {};
-    private _headerRef: React.RefObject<HTMLDivElement> = React.createRef();
     @observable _background = 'inherit';
     @observable _paletteOn = false;
     @observable _heading = '';
@@ -72,6 +72,7 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
 
     _ele: HTMLElement | null = null;
     _eleMasonrySingle = React.createRef<HTMLDivElement>();
+    _headerRef: HTMLDivElement | null = null;
 
     protected onInternalPreDrop = (e: Event, de: DragManager.DropEvent, targetDropAction: dropActionType) => {
         const dragData = de.complete.docDragData;
@@ -93,13 +94,13 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
     createColumnDropRef = (ele: HTMLDivElement | null) => {
         this.dropDisposer?.();
         if (ele) this.dropDisposer = DragManager.MakeDropTarget(ele, this.columnDrop.bind(this), this._props.Doc, this.onInternalPreDrop.bind(this));
-        else if (this._eleMasonrySingle.current) this.props.refList.splice(this.props.refList.indexOf(this._eleMasonrySingle.current), 1);
+        else if (this._eleMasonrySingle.current) runInAction(() => this.props.colStackRefs.splice(this.props.colStackRefs.indexOf(this._eleMasonrySingle.current!), 1));
         this._ele = ele;
     };
 
     @action
     componentDidMount() {
-        this._eleMasonrySingle.current && this.props.refList.push(this._eleMasonrySingle.current);
+        this._eleMasonrySingle.current && this.props.colStackRefs.push(this._eleMasonrySingle.current);
         this._disposers.collapser = reaction(
             () => this._props.headingObject?.collapsed,
             collapsed => { this.collapsed = collapsed !== undefined ? BoolCast(collapsed) : false; }, // prettier-ignore
@@ -108,7 +109,7 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
     }
     componentWillUnmount() {
         this._disposers.collapser?.();
-        this._ele && this.props.refList.splice(this.props.refList.indexOf(this._ele), 1);
+        this._ele && runInAction(() => this.props.colStackRefs.splice(this.props.colStackRefs.indexOf(this._ele!), 1));
         this._ele = null;
     }
 
@@ -192,7 +193,7 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
         value = typeof value === 'string' ? `"${value}"` : value;
         embedding.viewSpecScript = ScriptField.MakeFunction(`doc.${this._props.pivotField} === ${value}`, { doc: Doc.name });
         if (embedding.viewSpecScript) {
-            DragManager.StartDocumentDrag([this._headerRef.current!], new DragManager.DocumentDragData([embedding]), e.clientX, e.clientY);
+            DragManager.StartDocumentDrag([this._headerRef!], new DragManager.DocumentDragData([embedding]), e.clientX, e.clientY);
             return true;
         }
         return false;
@@ -314,9 +315,14 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
             <div
                 key={heading}
                 className="collectionStackingView-sectionHeader"
-                ref={this._headerRef}
+                ref={r => {
+                    if (this._headerRef && this._props.colHeaderRefs.includes(this._headerRef)) this._props.colHeaderRefs.splice(this._props.colStackRefs.indexOf(this._headerRef), 1);
+                    r && this._props.colHeaderRefs.push(r);
+                    this._headerRef = r;
+                }}
                 style={{
-                    marginTop: this._props.yMargin,
+                    marginTop: 0,
+                    marginBottom: this._props.gridGap,
                     width: this._props.columnWidth,
                 }}>
                 {/* the default bucket (no key value) has a tooltip that describes what it is.
@@ -367,7 +373,7 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
                             ref={this._eleMasonrySingle}
                             className="collectionStackingView-masonrySingle"
                             style={{
-                                padding: `${columnYMargin}px ${0}px ${this._props.yMargin}px ${0}px`,
+                                padding: `${columnYMargin}px ${0}px ${0}px ${0}px`,
                                 margin: this._props.dontCenter.includes('x') ? undefined : 'auto',
                                 height: 'max-content',
                                 position: 'relative',
@@ -400,15 +406,13 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
 
     render() {
         TraceMobx();
-        const headings = this._props.headings();
         const heading = this._heading;
-        const uniqueHeadings = headings.map((i, idx) => headings.indexOf(i) === idx);
         return (
             <div
                 className="collectionStackingViewFieldColumn"
                 key={heading}
                 style={{
-                    width: `${100 / (uniqueHeadings.length + (this._props.chromeHidden ? 0 : 1) || 1)}%`,
+                    width: this._props.columnWidth,
                     height: undefined,
                     background: this._background,
                 }}
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 4189fcc0c..fea2986c7 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -1522,7 +1522,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         }
         if (this.EditorView && selectOnLoad && !this._props.dontRegisterView && !this._props.dontSelectOnLoad && this.isActiveTab(this.ProseRef)) {
             const { $from } = this.EditorView.state.selection;
-            const mark = schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified: Math.floor(Date.now() / 1000) });
+            const mark = schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail() });
             const curMarks = this.EditorView.state.storedMarks ?? $from?.marksAcross(this.EditorView.state.selection.$head) ?? [];
             const storedMarks = [...curMarks.filter(m => m.type !== mark.type), mark];
             if (selLoadChar === 'Enter') {
@@ -1549,7 +1549,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             const { text, paragraph } = schema.nodes;
             const selNode = this.EditorView.state.selection.$anchor.node();
             if (this.EditorView.state.selection.from === 1 && this.EditorView.state.selection.empty && [undefined, text, paragraph].includes(selNode?.type)) {
-                const docDefaultMarks = [schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified: Math.floor(Date.now() / 1000) })];
+                const docDefaultMarks = [schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail() })];
                 this.EditorView.state.selection.empty && this.EditorView.state.selection.from === 1 && this.EditorView?.dispatch(this.EditorView?.state.tr.setStoredMarks(docDefaultMarks).removeStoredMark(schema.marks.pFontColor));
             }
         }
@@ -1685,7 +1685,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                 if (e.clientX > boundsRect.left && e.clientX < boundsRect.right && e.clientY > boundsRect.bottom) {
                     // if we clicked below the last prosemirror div, then set the selection to be the end of the document
                     editorView.focus();
-                    editorView.dispatch(editorView.state.tr.setSelection(TextSelection.create(editorView.state.doc, editorView.state.doc.content.size)));
+                    // editorView.dispatch(editorView.state.tr.setSelection(TextSelection.create(editorView.state.doc, editorView.state.doc.content.size)));
                 }
             } else if (node && [editorView.state.schema.nodes.ordered_list, editorView.state.schema.nodes.listItem].includes(node.type) && node !== (editorView.state.selection as NodeSelection)?.node && pcords) {
                 editorView.dispatch(editorView.state.tr.setSelection(NodeSelection.create(editorView.state.doc, pcords.pos)));
@@ -1814,7 +1814,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             return;
         }
         if (this._enteringStyle && 'tix!'.includes(e.key)) {
-            const tag = e.key === 't' ? 'todo' : e.key === 'i' ? 'ignore' : e.key === 'x' ? 'disagree' : e.key === '!' ? 'important' : '??';
             const node = state.selection.$from.nodeAfter;
             const start = state.selection.from;
             const end = state.selection.to;
@@ -1823,9 +1822,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                 StopEvent(e);
                 _editorView.dispatch(
                     state.tr
-                        .removeMark(start, end, schema.marks.user_mark)
-                        .addMark(start, end, schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified: Math.floor(Date.now() / 1000) }))
-                        .addMark(start, end, schema.marks.user_tag.create({ userid: ClientUtils.CurrentUserEmail(), tag, modified: Math.round(Date.now() / 1000 / 60) }))
+                        .removeMark(start, end, schema.marks.user_mark) //
+                        .addMark(start, end, schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail() }))
                 );
                 return;
             }
@@ -1858,9 +1856,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                 break;
             default:
                 if ([AclEdit, AclAugment, AclAdmin].includes(GetEffectiveAcl(this.Document))) {
-                    const modified = Math.floor(Date.now() / 1000);
-                    const mark = state.selection.$to.marks().find(m => m.type === schema.marks.user_mark && m.attrs.modified === modified);
-                    _editorView.dispatch(state.tr.removeStoredMark(schema.marks.user_mark).addStoredMark(mark ?? schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified })));
+                    const mark = state.selection.$to.marks().find(m => m.type === schema.marks.user_mark);
+                    _editorView.dispatch(
+                        state.tr
+                            .removeStoredMark(schema.marks.user_mark) //
+                            .addStoredMark(mark ?? schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail() }))
+                    );
                 }
                 break;
         }
@@ -1884,14 +1885,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         const margins = 2 * NumCast(this.layoutDoc._yMargin, this._props.yMargin || 0);
         const children = this.ProseRef?.children.length ? Array.from(this.ProseRef.children[0].children) : undefined;
         if (this.EditorView && children && !SnappingManager.IsDragging) {
-            const getChildrenHeights = (kids: Element[] | undefined) => kids?.reduce((p, child) => p + toHgt(child), margins) ?? 0;
+            const getChildrenHeights = (kids: Element[] | undefined) => kids?.reduce((p, child) => p + toHgt(child), 0) ?? 0;
             const toNum = (val: string) => Number(val.replace('px', ''));
             const toHgt = (node: Element): number => {
                 const { height, marginTop, marginBottom } = getComputedStyle(node);
                 const childHeight = height === 'auto' ? getChildrenHeights(Array.from(node.children)) : toNum(height);
                 return childHeight + Math.max(0, toNum(marginTop)) + Math.max(0, toNum(marginBottom));
             };
-            const proseHeight = !this.ProseRef ? 0 : getChildrenHeights(children);
+            const proseHeight = !this.ProseRef ? 0 : getChildrenHeights(children) + margins;
             const scrollHeight = this.ProseRef && proseHeight;
             if (this._props.setHeight && !this._props.suppressSetHeight && scrollHeight && !this._props.dontRegisterView) {
                 // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation
diff --git a/src/client/views/nodes/formattedText/marks_rts.ts b/src/client/views/nodes/formattedText/marks_rts.ts
index b7dae1ca3..333ee6be8 100644
--- a/src/client/views/nodes/formattedText/marks_rts.ts
+++ b/src/client/views/nodes/formattedText/marks_rts.ts
@@ -330,20 +330,6 @@ export const marks: { [index: string]: MarkSpec } = {
             return ['span', { class: 'UM-' + uid + remote + ' UM-min-' + min + ' UM-hr-' + hr + ' UM-day-' + day }, 0];
         },
     },
-    // the id of the user who entered the text
-    user_tag: {
-        attrs: {
-            userid: { default: '' },
-            modified: { default: 'when?' }, // 1 second intervals since 1970
-            tag: { default: '' },
-        },
-        group: 'inline',
-        inclusive: false,
-        toDOM: node => {
-            const uid = node.attrs.userid.replace('.', '').replace('@', '');
-            return ['span', { class: 'UT-' + uid + ' UT-' + node.attrs.tag }, 0];
-        },
-    },
 
     // :: MarkSpec Code font mark. Represented as a `<code>` element.
     code: {
-- 
cgit v1.2.3-70-g09d2

