From b7105063030ee29ba644d8fbb5878c8ee41fd389 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 16 Dec 2024 11:52:42 -0500
Subject: fixed api for creating an image using another image as a structure
 reference.

---
 .../collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx | 3 ---
 1 file changed, 3 deletions(-)

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

diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
index 79aad0ef2..272c13546 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
@@ -102,7 +102,6 @@ export function computePassLayout(poolData: Map<string, PoolData>, pivotDoc: Doc
             replica: '',
         });
     });
-    // eslint-disable-next-line no-use-before-define
     return normalizeResults(panelDim, 12, docMap, poolData, viewDefsToJSX, [], 0, []);
 }
 
@@ -272,7 +271,6 @@ export function computePivotLayout(poolData: Map<string, PoolData>, pivotDoc: Do
         payload: pivotColumnGroups.get(key)?.filters,
     }));
     groupNames.push(...dividers);
-    // eslint-disable-next-line no-use-before-define
     return normalizeResults(panelDim, maxText, docMap, poolData, viewDefsToJSX, groupNames, 0, []);
 }
 
@@ -347,7 +345,6 @@ export function computeTimelineLayout(poolData: Map<string, PoolData>, pivotDoc:
         if (!stack && (curTime === undefined || Math.abs(x - (curTime - minTime) * scaling) > pivotAxisWidth)) {
             groupNames.push({ type: 'text', text: toLabel(key), x: x, y: stack * 25, height: fontHeight, fontSize, payload: undefined });
         }
-        // eslint-disable-next-line no-use-before-define
         layoutDocsAtTime(keyDocs, key);
     });
     if (sortedKeys.length && curTime !== undefined && curTime > sortedKeys[sortedKeys.length - 1]) {
-- 
cgit v1.2.3-70-g09d2


From 45aa1b54ab37d821257fb00b3be5ef5eca03ef2c Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 29 Jan 2025 15:09:10 -0500
Subject: generlized some of cardDeck ui.  showTags is under View for all
 collections.  chat popup is available for all docs, but only works for card
 views.   added clear filters button for chat popup.

---
 src/client/util/CurrentUserUtils.ts                |  34 +++----
 .../views/collections/CollectionCardDeckView.tsx   | 106 +++++++++----------
 .../CollectionFreeFormLayoutEngines.tsx            |   2 +
 .../collectionFreeForm/CollectionFreeFormView.tsx  |   1 +
 .../views/nodes/CollectionFreeFormDocumentView.tsx |   2 +-
 src/client/views/pdf/GPTPopup/GPTPopup.tsx         | 113 ++++++++-------------
 6 files changed, 118 insertions(+), 140 deletions(-)

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

diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 306914450..b75961575 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -710,8 +710,6 @@ pie title Minerals in my tap water
             { title: "Type",      icon:"eye",            toolTip:"Sort by document type",                          btnType: ButtonType.ToggleButton, expertMode: false, toolType:"docType",    funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}},
             { title: "Color",     icon:"palette",        toolTip:"Sort by document color",                         btnType: ButtonType.ToggleButton, expertMode: false, toolType:"color",      funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}},
             { title: "Tags",      icon:"bolt",           toolTip:"Sort by document's tags",                        btnType: ButtonType.ToggleButton, expertMode: false, toolType:"tag",        funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}},
-            { title: "Chat Popup",icon:"lightbulb",      toolTip:"Toggle the chat popup's visibility", width: 45,  btnType: ButtonType.ToggleButton, expertMode: false, toolType:"toggle-chat",funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'} },
-            { title: "Show Tags", icon:"id-card",        toolTip:"Toggle tag annotation panel",         width: 45, btnType: ButtonType.ToggleButton, expertMode: false, toolType:"toggle-tags",funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'} },
             { title: "Sort",      icon: "sort" ,         toolTip: "Manage sort order / lock status",               btnType: ButtonType.MultiToggleButton,               toolType:"alignment", ignoreClick: true,
               subMenu: [
                 { title: "Ascending", toolTip: "Sort the cards in ascending order", btnType: ButtonType.ToggleButton, icon: "sort-up",  toolType:"up",  ignoreClick: true,  scripts: {onClick: '{ return showFreeform(this.toolType, _readOnly_);}'} },
@@ -736,13 +734,15 @@ pie title Minerals in my tap water
             { title:"Options",isSystem: true,icon: "gear",  toolTip:"Click to customize list of filter buttons", btnType: ButtonType.ClickButton, expertMode: false, toolType:"-opts-",funcs: {}, scripts: { onClick: '{ return setTagFilter(this.toolType, false,_readOnly_);}'}},
             ...tagButtonDescs
         ]
-    }
+    } 
     static viewTools(): Button[] {
         return [
-            { title: "Snap",      icon: "th",                  toolTip: "Show Snap Lines",     btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"snaplines",  funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform
-            { title: "Grid",      icon: "border-all",          toolTip: "Show Grid",           btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"grid",       funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform
-            { title: "Fit All",   icon: "object-group",        toolTip: "Fit Docs to View (double click to make sticky)",btnType: ButtonType.ToggleButton, ignoreClick:true, expertMode: false, toolType:"viewAll",    funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}', onDoubleClick: '{ return showFreeform(this.toolType, _readOnly_, true);}'}}, // Only when floating document is selected in freeform
-            { title: "Clusters",  icon: "braille",             toolTip: "Show Doc Clusters",   btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"clusters",   funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform
+            { title: "Show Tags", icon: "id-card",             toolTip: "Toggle tags",         btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"toggle-tags",funcs: { },                                                                              scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'} },
+            { title: "Snap",      icon: "th",                  toolTip: "Show Snap Lines",     btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"snaplines",  funcs: { hidden: `!SelectedDocType("${CollectionViewType.Freeform}", this.expertMode)`}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform
+            { title: "Grid",      icon: "border-all",          toolTip: "Show Grid",           btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"grid",       funcs: { hidden: `!SelectedDocType("${CollectionViewType.Freeform}", this.expertMode)`}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform
+            { title: "Fit All",   icon: "object-group",        toolTip:"Fit Docs to View (double tap to persist)",
+                                                                                               btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"viewAll",    funcs: { hidden: `!SelectedDocType("${CollectionViewType.Freeform}", this.expertMode)`}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}', onDoubleClick: '{ return showFreeform(this.toolType, _readOnly_, true);}'}}, // Only when floating document is selected in freeform
+            { title: "Clusters",  icon: "braille",             toolTip: "Show Doc Clusters",   btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"clusters",   funcs: { hidden: `!SelectedDocType("${CollectionViewType.Freeform}", this.expertMode)`}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform
         ]
     }
     static textTools():Button[] {
@@ -832,21 +832,21 @@ pie title Minerals in my tap water
                     CollectionViewType.Map, CollectionViewType.NoteTaking, CollectionViewType.Schema, CollectionViewType.Stacking, 
                     CollectionViewType.Calendar, CollectionViewType.Grid, CollectionViewType.Tree, CollectionViewType.Time, ]),
               title: "Perspective",                    toolTip: "View",                 btnType: ButtonType.DropdownList,                                                                                                                                    ignoreClick: true, width: 100, scripts: { script:  '{ return setView(value, shiftKey, _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_)'} },
-            { title: "Template",icon: "scroll",        toolTip: "Default Note Template",btnType: ButtonType.ToggleButton,        expertMode: false, toolType:DocumentType.RTF,                                                                                                              scripts: { onClick: '{ return setDefaultTemplate(_readOnly_); }'} },
-            { title: "Fill",    icon: "fill-drip",     toolTip: "Fill/Background Color",btnType: ButtonType.ColorButton,         expertMode: false,                                                                                                          ignoreClick: true, width: 30,  scripts: { script:  'return setBackgroundColor(value, _readOnly_)'} }, // Only when a document is selected
-            { title: "Overlay", icon: "layer-group",   toolTip: "Overlay",              btnType: ButtonType.ToggleButton,        expertMode: true,  toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode, true)'},                        scripts: { onClick: '{ return toggleOverlay(_readOnly_); }'}}, // Only when floating document is selected in freeform
-            { title: "Back",    icon: "chevron-left",  toolTip: "Prev Animation Frame", btnType: ButtonType.ClickButton,         expertMode: true,  toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)'},                  width: 30,  scripts: { onClick: 'prevKeyFrame(_readOnly_)'}},
-            { title: "Num",     icon:"", toolTip: "Frame # (click to toggle edit mode)",btnType: ButtonType.TextButton,          expertMode: true,  toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)', buttonText: 'selectedDocs()?.lastElement()?.currentFrame?.toString()'}, width: 20, scripts: { onClick: '{ return curKeyFrame(_readOnly_);}'}},
-            { title: "Fwd",     icon: "chevron-right", toolTip: "Next Animation Frame", btnType: ButtonType.ClickButton,         expertMode: true,  toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)'},                  width: 30,  scripts: { onClick: 'nextKeyFrame(_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_)'} },
+            { title: "Template",icon: "scroll",        toolTip: "Default Note Template",btnType: ButtonType.ToggleButton, expertMode: false, toolType:DocumentType.RTF,                                                                                                              scripts: { onClick: '{ return setDefaultTemplate(_readOnly_); }'} },
+            { title: "Fill",    icon: "fill-drip",     toolTip: "Fill/Background Color",btnType: ButtonType.ColorButton,  expertMode: false,                                                                                                          ignoreClick: true, width: 30,  scripts: { script:  'return setBackgroundColor(value, _readOnly_)'} }, // Only when a document is selected
+            { title: "Overlay", icon: "layer-group",   toolTip: "Overlay",              btnType: ButtonType.ToggleButton, expertMode: true,  toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode, true)'},                        scripts: { onClick: '{ return toggleOverlay(_readOnly_); }'}}, // Only when floating document is selected in freeform
+            { title: "Back",    icon: "chevron-left",  toolTip: "Prev Animation Frame", btnType: ButtonType.ClickButton,  expertMode: true,  toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)'},                  width: 30,  scripts: { onClick: 'prevKeyFrame(_readOnly_)'}},
+            { title: "Num",     icon:"", toolTip: "Frame # (click to toggle edit mode)",btnType: ButtonType.TextButton,   expertMode: true,  toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)', buttonText: 'selectedDocs()?.lastElement()?.currentFrame?.toString()'}, width: 20, scripts: { onClick: '{ return curKeyFrame(_readOnly_);}'}},
+            { title: "Fwd",     icon: "chevron-right", toolTip: "Next Animation Frame", btnType: ButtonType.ClickButton,  expertMode: true,  toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)'},                  width: 30,  scripts: { onClick: 'nextKeyFrame(_readOnly_)'}},
+            { title: "Chat",    icon:"lightbulb",      toolTip: "Toggle Chat Assistant",btnType: ButtonType.ToggleButton, expertMode: false, toolType:"toggle-chat",               funcs: {},                                                                            width: 30,  scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'} },
             { title: "Filter",  icon: "=",      toolTip: "Filter cards by tags", subMenu: CurrentUserUtils.filterTools(), ignoreClick:true,  toolType:DocumentType.COL,            funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)'}, btnType: ButtonType.MultiToggleButton, width: 30, backgroundColor: doc.userVariantColor as string},
             { title: "Sort",    icon: "Sort",   toolTip: "Sort Documents",       subMenu: CurrentUserUtils.sortTools(),   expertMode: false, toolType:DocumentType.COL,            funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available
             { title: "Text",    icon: "Text",   toolTip: "Text functions",       subMenu: CurrentUserUtils.textTools(),   expertMode: false, toolType:DocumentType.RTF,            funcs: {                                                                             linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available
             { title: "Ink",     icon: "Ink",    toolTip: "Ink functions",        subMenu: CurrentUserUtils.inkTools(),    expertMode: false, toolType:DocumentType.INK,            funcs: {hidden: `IsExploreMode()`,                                                   linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`}, scripts: { onClick: 'setInkToolDefaults()'} }, // Always available
             { title: "Doc",     icon: "Doc",    toolTip: "Freeform Doc tools",   subMenu: CurrentUserUtils.freeTools(),   expertMode: false, toolType:CollectionViewType.Freeform, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode, true)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available
-            { title: "View",    icon: "View",   toolTip: "View tools",           subMenu: CurrentUserUtils.viewTools(),   expertMode: false, toolType:CollectionViewType.Freeform, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available
+            { title: "View",    icon: "View",   toolTip: "View tools",           subMenu: CurrentUserUtils.viewTools(),   expertMode: false, toolType:DocumentType.COL,            funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available
             { title: "Stack",   icon: "View",   toolTip: "Stacking tools",       subMenu: CurrentUserUtils.stackTools(),  expertMode: false, toolType:CollectionViewType.Stacking, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available
             
             { title: "Web",     icon: "Web",    toolTip: "Web functions",        subMenu: CurrentUserUtils.webTools(),    expertMode: false, toolType:DocumentType.WEB,            funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Only when Web is selected
diff --git a/src/client/views/collections/CollectionCardDeckView.tsx b/src/client/views/collections/CollectionCardDeckView.tsx
index b40aa37de..f24673c39 100644
--- a/src/client/views/collections/CollectionCardDeckView.tsx
+++ b/src/client/views/collections/CollectionCardDeckView.tsx
@@ -56,7 +56,6 @@ export class CollectionCardView extends CollectionSubView() {
     constructor(props: SubCollectionViewProps) {
         super(props);
         makeObservable(this);
-        this.setRegenerateCallback();
     }
     protected createDashEventsTarget = (ele: HTMLDivElement | null) => {
         this._dropDisposer?.();
@@ -74,23 +73,19 @@ export class CollectionCardView extends CollectionSubView() {
     @computed get _maxRowCount() {
         return Math.ceil(this.cardDeckWidth / this.cardWidth);
     }
-    /**
-     * Callback to ensure gpt's text versions of the child docs are updated
-     */
-    setRegenerateCallback = () => GPTPopup.Instance.setRegenerateCallback(this.childPairStringListAndUpdateSortDesc);
 
     /**
      * update's gpt's doc-text list and initializes callbacks
      */
-    @action
-    childPairStringListAndUpdateSortDesc = async () => {
-        const sortDesc = await this.childPairStringList(); // Await the promise to get the string result
-        GPTPopup.Instance.setSortDesc(sortDesc.join());
-        GPTPopup.Instance.onSortComplete = (sortResult: string, questionType: string, tag?: string) => this.processGptOutput(sortResult, questionType, tag);
-        GPTPopup.Instance.onQuizRandom = () => this.quizMode();
-    };
+    childPairStringListAndUpdateSortDesc = () =>
+        this.childPairStringList().then(sortDesc => {
+            GPTPopup.Instance.setSortDesc(sortDesc.join());
+            GPTPopup.Instance.onSortComplete = this.processGptOutput;
+            GPTPopup.Instance.onQuizRandom = this.quizMode;
+        });
 
     componentDidMount() {
+        GPTPopup.Instance.setRegenerateCallback(this.Document, this.childPairStringListAndUpdateSortDesc);
         this._props.setContentViewBox?.(this);
         // if card deck moves, then the child doc views are hidden so their screen to local transforms will return empty rectangles
         // when inquired from the dom (below in childScreenToLocal).  When the doc is actually rendered, we need to act like the
@@ -112,6 +107,10 @@ export class CollectionCardView extends CollectionSubView() {
     }
 
     componentWillUnmount() {
+        GPTPopup.Instance.setSortDesc('');
+        GPTPopup.Instance.onSortComplete = undefined;
+        GPTPopup.Instance.onQuizRandom = undefined;
+        GPTPopup.Instance.setRegenerateCallback(undefined, null);
         Object.keys(this._disposers).forEach(key => this._disposers[key]?.());
         this._dropDisposer?.();
     }
@@ -166,8 +165,7 @@ export class CollectionCardView extends CollectionSubView() {
      * When in quiz mode, randomly selects a document
      */
     quizMode = () => {
-        const randomIndex = Math.floor(Math.random() * this.childDocs.length);
-        this.layoutDoc._card_curDoc = this.childDocs[randomIndex];
+        this.layoutDoc._card_curDoc = this.childDocs[Math.floor(Math.random() * this.childDocs.length)];
     };
 
     setHoveredNodeIndex = action((index: number) => {
@@ -437,49 +435,51 @@ export class CollectionCardView extends CollectionSubView() {
      * Processes gpt's output depending on the type of question the user asked. Converts gpt's string output to
      * usable code
      * @param gptOutput
+     * @param questionType
+     * @param tag
      */
-    @action
-    processGptOutput = undoable((gptOutput: string, questionType: string, tag?: string) => {
-        // Split the string into individual list items
-        const listItems = gptOutput.split('======').filter(item => item.trim() !== '');
-
-        if (questionType === '2' || questionType === '4') {
-            this.childDocs.forEach(d => {
-                d.chatFilter = false;
-            });
-        }
+    processGptOutput = (gptOutput: string, questionType: string, tag?: string) =>
+        undoable(() => {
+            // Split the string into individual list items
+            const listItems = gptOutput.split('======').filter(item => item.trim() !== '');
+
+            if (questionType === '2' || questionType === '4') {
+                this.childDocs.forEach(d => {
+                    d.chatFilter = false;
+                });
+            }
 
-        if (questionType === '6') {
-            this.Document[this._props.fieldKey + '_sort'] = docSortings.Chat;
-        }
+            if (questionType === '6') {
+                this.Document[this._props.fieldKey + '_sort'] = docSortings.Chat;
+            }
 
-        listItems.forEach((item, index) => {
-            const normalizedItem = item.trim();
-            // find the corresponding Doc in the textToDoc map
-            const doc = this._textToDoc.get(normalizedItem);
-            if (doc) {
-                switch (questionType) {
-                    case '6':
-                        doc.chatIndex = index;
-                        break;
-                    case '1':
-                        if (tag) {
-                            const hashTag = tag.startsWith('#') ? tag : '#' + tag[0].toLowerCase() + tag.slice(1);
-                            const filterTag = Doc.MyFilterHotKeys.map(key => StrCast(key.toolType)).find(key => key.includes(tag)) ?? hashTag;
-                            TagItem.addTagToDoc(doc, filterTag);
-                        }
-                        break;
-                    case '2':
-                    case '4':
-                        doc.chatFilter = true;
-                        Doc.setDocFilter(DocCast(this.Document.embedContainer), 'chatFilter', true, 'match');
-                        break;
+            listItems.forEach((item, index) => {
+                const normalizedItem = item.trim();
+                // find the corresponding Doc in the textToDoc map
+                const doc = this._textToDoc.get(normalizedItem);
+                if (doc) {
+                    switch (questionType) {
+                        case '6':
+                            doc.chatIndex = index;
+                            break;
+                        case '1':
+                            if (tag) {
+                                const hashTag = tag.startsWith('#') ? tag : '#' + tag[0].toLowerCase() + tag.slice(1);
+                                const filterTag = Doc.MyFilterHotKeys.map(key => StrCast(key.toolType)).find(key => key.includes(tag)) ?? hashTag;
+                                TagItem.addTagToDoc(doc, filterTag);
+                            }
+                            break;
+                        case '2':
+                        case '4':
+                            doc.chatFilter = true;
+                            Doc.setDocFilter(DocCast(this.Document), 'chatFilter', true, 'check');
+                            break;
+                    }
+                } else {
+                    console.warn(`No matching document found for item: ${normalizedItem}`);
                 }
-            } else {
-                console.warn(`No matching document found for item: ${normalizedItem}`);
-            }
-        });
-    }, '');
+            });
+        }, '')();
 
     childScreenToLocal = computedFn((doc: Doc, index: number, isSelected: boolean) => () => {
         // need to explicitly trigger an invalidation since we're reading everything from the Dom
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
index 272c13546..bebdbd731 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
@@ -44,6 +44,7 @@ export interface PoolData {
     transition?: string;
     highlight?: boolean;
     pointerEvents?: string;
+    showTags?: boolean;
 }
 
 export interface ViewDefResult {
@@ -425,6 +426,7 @@ function normalizeResults(
                     opacity: newPosRaw.opacity,
                     color: newPosRaw.color,
                     pair: ele[1].pair,
+                    showTags: newPosRaw.showTags,
                 };
                 if (newPosRaw.transition) newPos.transition = newPosRaw.transition;
                 poolData.set(newPos.pair.layout[Id] + (newPos.replica || ''), { transition: 'all 1s', ...newPos });
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 796949378..69fdf52ff 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1658,6 +1658,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             width: _width,
             height: _height,
             transition: StrCast(childDocLayout.dataTransition),
+            showTags: BoolCast(childDocLayout.showTags) || BoolCast(this.Document.showChildTags) || BoolCast(this.Document._layout_showTags),
             pointerEvents: Cast(childDoc.pointerEvents, 'string', null),
             pair,
             replica: '',
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 3cbfb1796..68105f2f1 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -28,7 +28,7 @@ export enum GroupActive { // flags for whether a view is activate because of its
 }
 /// Ugh, typescript has no run-time way of iterating through the keys of an interface.  so we need
 /// manaully keep this list of keys in synch wih the fields of the freeFormProps interface
-const freeFormPropsKeys = ['x', 'y', 'z', 'zIndex', 'rotation', 'opacity', 'backgroundColor', 'color', 'highlight', 'width', 'height', 'autoDim', 'transition'];
+const freeFormPropsKeys = ['x', 'y', 'z', 'width', 'height', 'zIndex', 'autoDim', 'rotation', 'color', 'backgroundColor', 'opacity', 'highlight', 'transition'];
 interface freeFormProps {
     x: number;
     y: number;
diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.tsx b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
index c33b81eb4..63130d056 100644
--- a/src/client/views/pdf/GPTPopup/GPTPopup.tsx
+++ b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
@@ -1,5 +1,5 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Button, IconButton, Type } from '@dash/components';
+import { Button, IconButton, Toggle, ToggleType, Type } from '@dash/components';
 import { action, makeObservable, observable, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
@@ -46,105 +46,70 @@ export class GPTPopup extends ObservableReactComponent<object> {
     @observable private chatMode: boolean = false;
     private correlatedColumns: string[] = [];
 
-    @observable
-    public visible: boolean = false;
-    @action
-    public setVisible = (vis: boolean) => {
+    @observable public visible: boolean = false;
+    @action public setVisible = (vis: boolean) => {
         this.visible = vis;
     };
-    @observable
-    public loading: boolean = false;
-    @action
-    public setLoading = (loading: boolean) => {
+    @observable public loading: boolean = false;
+    @action public setLoading = (loading: boolean) => {
         this.loading = loading;
     };
-    @observable
-    public text: string = '';
-    @action
-    public setText = (text: string) => {
+    @observable public text: string = '';
+    @action public setText = (text: string) => {
         this.text = text;
     };
-    @observable
-    public selectedText: string = '';
-    @action
-    public setSelectedText = (text: string) => {
+    @observable public selectedText: string = '';
+    @action public setSelectedText = (text: string) => {
         this.selectedText = text;
     };
-    @observable
-    public dataJson: string = '';
+    @observable public dataJson: string = '';
     public dataChatPrompt: string | undefined = undefined;
-    @action
-    public setDataJson = (text: string) => {
+    @action public setDataJson = (text: string) => {
         if (text === '') this.dataChatPrompt = '';
         this.dataJson = text;
     };
 
-    @observable
-    public imgDesc: string = '';
-    @action
-    public setImgDesc = (text: string) => {
+    @observable public imgDesc: string = '';
+    @action public setImgDesc = (text: string) => {
         this.imgDesc = text;
     };
 
-    @observable
-    public imgUrls: string[][] = [];
-    @action
-    public setImgUrls = (imgs: string[][]) => {
+    @observable public imgUrls: string[][] = [];
+    @action public setImgUrls = (imgs: string[][]) => {
         this.imgUrls = imgs;
     };
 
-    @observable
-    public mode: GPTPopupMode = GPTPopupMode.SUMMARY;
-    @action
-    public setMode = (mode: GPTPopupMode) => {
+    @observable public mode: GPTPopupMode = GPTPopupMode.SUMMARY;
+    @action public setMode = (mode: GPTPopupMode) => {
         this.mode = mode;
     };
 
-    @observable
-    public highlightRange: number[] = [];
+    @observable public highlightRange: number[] = [];
     @action callSummaryApi = () => {};
 
-    @observable
-    private done: boolean = false;
-    @action
-    public setDone = (done: boolean) => {
+    @observable private done: boolean = false;
+    @action public setDone = (done: boolean) => {
         this.done = done;
         this.chatMode = false;
     };
 
-    @observable
-    private sortDone: boolean = false; // this is so redundant but the og done variable was causing weird unknown problems and im just a girl
-
-    @action
-    public setSortDone = (done: boolean) => {
-        this.sortDone = done;
-    };
-
     // change what can be a ref into a ref
-    @observable
-    private sidebarId: string = '';
-    @action
-    public setSidebarId = (id: string) => {
+    @observable private sidebarId: string = '';
+    @action public setSidebarId = (id: string) => {
         this.sidebarId = id;
     };
 
-    @observable
-    private imgTargetDoc: Doc | undefined;
-    @action
-    public setImgTargetDoc = (anchor: Doc) => {
+    @observable private imgTargetDoc: Doc | undefined;
+    @action public setImgTargetDoc = (anchor: Doc) => {
         this.imgTargetDoc = anchor;
     };
 
-    @observable
-    private textAnchor: Doc | undefined;
-    @action
-    public setTextAnchor = (anchor: Doc) => {
+    @observable private textAnchor: Doc | undefined;
+    @action public setTextAnchor = (anchor: Doc) => {
         this.textAnchor = anchor;
     };
 
-    @observable
-    public sortDesc: string = '';
-
+    @observable public sortDesc: string = '';
     @action public setSortDesc = (t: string) => {
         this.sortDesc = t;
     };
@@ -153,8 +118,12 @@ export class GPTPopup extends ObservableReactComponent<object> {
     @observable onQuizRandom?: () => void;
     @observable cardsDoneLoading = false;
 
+    @observable collectionDoc: Doc | undefined = undefined;
+    @action setCollectionDoc(doc: Doc | undefined) {
+        this.collectionDoc = doc;
+    }
+
     @action setCardsDoneLoading(done: boolean) {
-        console.log(done + 'HI HIHI');
         this.cardsDoneLoading = done;
     }
 
@@ -184,7 +153,6 @@ export class GPTPopup extends ObservableReactComponent<object> {
      */
     generateQuiz = async () => {
         this.setLoading(true);
-        this.setSortDone(false);
 
         const selected = DocumentView.SelectedDocs().lastElement();
 
@@ -203,7 +171,6 @@ export class GPTPopup extends ObservableReactComponent<object> {
             this.conversationArray.push(res);
 
             this.setLoading(false);
-            this.setSortDone(true);
         } catch (err) {
             console.error('GPT call failed', err);
         }
@@ -235,7 +202,8 @@ export class GPTPopup extends ObservableReactComponent<object> {
      * Callback function that causes the card view to update the childpair string list
      * @param callback
      */
-    @action public setRegenerateCallback(callback: () => Promise<void>) {
+    @action public setRegenerateCallback(collectionDoc: Doc | undefined, callback: null | (() => Promise<void>)) {
+        this.setCollectionDoc(collectionDoc);
         this.regenerateCallback = callback;
     }
 
@@ -254,7 +222,6 @@ export class GPTPopup extends ObservableReactComponent<object> {
      */
     generateCard = async () => {
         this.setLoading(true);
-        this.setSortDone(false);
 
         if (this.regenerateCallback) {
             await this.regenerateCallback();
@@ -296,7 +263,6 @@ export class GPTPopup extends ObservableReactComponent<object> {
         }
 
         this.setLoading(false);
-        this.setSortDone(true);
     };
 
     /**
@@ -554,7 +520,7 @@ export class GPTPopup extends ObservableReactComponent<object> {
     };
 
     sortBox = () => (
-        <div style={{ height: '80%' }}>
+        <div className="gptPopup-sortBox" style={{ height: '80%' }}>
             {this.heading(this.mode === GPTPopupMode.SORT ? 'SORTING' : 'QUIZ')}
             <>
                 {
@@ -727,6 +693,15 @@ export class GPTPopup extends ObservableReactComponent<object> {
                     {(this.mode === GPTPopupMode.SORT || this.mode === GPTPopupMode.QUIZ) && (
                         <IconButton color={StrCast(SettingsManager.userVariantColor)} tooltip="back" icon={<CgCornerUpLeft size="16px" />} onClick={() => (this.mode = GPTPopupMode.CARD)} style={{ right: '50px', position: 'absolute' }} />
                     )}
+                    <Toggle
+                        tooltip="clear filters"
+                        toggleType={ToggleType.BUTTON}
+                        type={Type.PRIM}
+                        toggleStatus={this.collectionDoc?.childFilters ? true : false}
+                        text={this.collectionDoc?.childFilters ? 'filtered' : ''}
+                        color="red"
+                        onClick={() => this.collectionDoc && (this.collectionDoc.childFilters = undefined)}
+                    />
                     <IconButton
                         color={StrCast(SettingsManager.userVariantColor)}
                         tooltip="close"
-- 
cgit v1.2.3-70-g09d2


From 28792ad2da9f5dfab96f98f34a11136bd1453e0f Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 6 Mar 2025 10:49:20 -0500
Subject: fixed resizing height to be > 10 pixels.  split pivotView out of
 timeView.

---
 src/client/documents/DocumentTypes.ts              |   1 +
 src/client/util/CurrentUserUtils.ts                |   4 +-
 src/client/views/DocumentDecorations.tsx           |   3 +-
 .../views/collections/CollectionCarouselView.tsx   |   2 +-
 .../views/collections/CollectionPivotView.tsx      | 147 +++++++++++++++++++
 .../views/collections/CollectionTimeView.tsx       | 158 ++-------------------
 src/client/views/collections/CollectionView.tsx    |   7 +-
 .../CollectionFreeFormLayoutEngines.tsx            |   6 +-
 8 files changed, 174 insertions(+), 154 deletions(-)
 create mode 100644 src/client/views/collections/CollectionPivotView.tsx

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

diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts
index 8aa844c0b..845aa280c 100644
--- a/src/client/documents/DocumentTypes.ts
+++ b/src/client/documents/DocumentTypes.ts
@@ -61,6 +61,7 @@ export enum CollectionViewType {
     Multirow = 'multirow',
     NoteTaking = 'notetaking',
     Pile = 'pileup',
+    Pivot = 'pivot',
     Schema = 'schema',
     Stacking = 'stacking',
     StackedTimeline = 'stacked timeline',
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 60acd5b21..8736225d0 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -826,7 +826,7 @@ pie title Minerals in my tap water
         return [
             { btnList: new List<string>([CollectionViewType.Freeform, CollectionViewType.Card, CollectionViewType.Carousel,CollectionViewType.Carousel3D, 
                     CollectionViewType.Masonry, CollectionViewType.Multicolumn, CollectionViewType.Multirow, CollectionViewType.Linear, 
-                    CollectionViewType.Map, CollectionViewType.NoteTaking, CollectionViewType.Schema, CollectionViewType.Stacking, 
+                    CollectionViewType.Map, CollectionViewType.NoteTaking, CollectionViewType.Pivot, CollectionViewType.Schema, CollectionViewType.Stacking, 
                     CollectionViewType.Calendar, CollectionViewType.Grid, CollectionViewType.Tree, CollectionViewType.Time, ]),
               title: "Perspective",                    toolTip: "View",                 btnType: ButtonType.DropdownList,                                                                                                                             ignoreClick: true, width: 100, scripts: { script:  '{ return setView(value, shiftKey, _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()"}},
@@ -911,7 +911,7 @@ pie title Minerals in my tap water
                             CurrentUserUtils.createToolButton(opts), scripts, funcs);
         
         const btnDescs = [// setup reactions to change the highlights on the undo/redo buttons -- would be better to encode this in the undo/redo buttons, but the undo/redo stacks are not wired up that way yet
-            { opts: { title: "Replicate",icon:"camera",toolTip: "Copy dashboard layout",btnType: ButtonType.ClickButton, expertMode: true},                                                                                             scripts: { onClick:  `snapshotDashboard()`}},
+            { opts: { title: "Replicate",icon:"camera",toolTip:"Copy dashboard layout",btnType: ButtonType.ClickButton, expertMode: true},                                                                                             scripts: { onClick:  `snapshotDashboard()`}},
             { opts: { title: "Recordings",            toolTip: "Workspace Recordings", btnType: ButtonType.DropdownList,expertMode: false, ignoreClick: true, width: 100}, funcs: {hidden: `false`, btnList:`getWorkspaceRecordings()`},scripts: { script:  `{ return replayWorkspace(value, _readOnly_); }`, onDragScript: `{ return startRecordingDrag(value); }`}},
             { opts: { title: "Stop Rec",icon: "stop", toolTip: "Stop recording",       btnType: ButtonType.ClickButton, expertMode: false}, funcs: {hidden: `!isWorkspaceRecording()`},                                                 scripts: { onClick: `stopWorkspaceRecording()`}},
             { opts: { title: "Play",    icon: "play", toolTip: "Play recording",       btnType: ButtonType.ClickButton, expertMode: false}, funcs: {hidden: `isWorkspaceReplaying() !== "${mediaState.Paused}"`},                       scripts: { onClick: `resumeWorkspaceReplaying(getCurrentRecording())`}},
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index d9b6bdf1a..54e050f9f 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -36,7 +36,6 @@ import { ImageBox } from './nodes/ImageBox';
 import { OpenWhere, OpenWhereMod } from './nodes/OpenWhere';
 import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
 import { TagsView } from './TagsView';
-import { ImageField } from '../../fields/URLField';
 
 interface DocumentDecorationsProps {
     PanelWidth: number;
@@ -567,7 +566,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
             }
 
             doc._width = Math.max(NumCast(doc._width_min, 25), NumCast(doc._width) * scale.x);
-            doc._height = Math.max(NumCast(doc._height_min), NumCast(doc._height) * scale.y);
+            doc._height = Math.max(NumCast(doc._height_min, 10), NumCast(doc._height) * scale.y);
             const { deltaX, deltaY } = this.realignRefPt(doc, refCent, initWidth || 1, initHeight || 1);
             doc.x = NumCast(doc.x) + deltaX;
             doc.y = NumCast(doc.y) + deltaY;
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index 87c6e3e5c..a7d217076 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -249,9 +249,9 @@ export class CollectionCarouselView extends CollectionSubView() {
                         position: 'relative',
                     }}>
                     {this.content}
-                    {this.flashCardUI(this.curDoc, this.docViewProps, this.answered)}
                     {this.navButtons}
                 </div>
+                {this.flashCardUI(this.curDoc, this.docViewProps, this.answered)}
             </div>
         );
     }
diff --git a/src/client/views/collections/CollectionPivotView.tsx b/src/client/views/collections/CollectionPivotView.tsx
new file mode 100644
index 000000000..2600c0f57
--- /dev/null
+++ b/src/client/views/collections/CollectionPivotView.tsx
@@ -0,0 +1,147 @@
+import { action, computed, makeObservable, observable, runInAction } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { returnTrue } from '../../../ClientUtils';
+import { Doc, Opt, StrListCast } from '../../../fields/Doc';
+import { List } from '../../../fields/List';
+import { ObjectField } from '../../../fields/ObjectField';
+import { ComputedField, ScriptField } from '../../../fields/ScriptField';
+import { NumCast, StrCast } from '../../../fields/Types';
+import { Docs } from '../../documents/Documents';
+import { ScriptingGlobals } from '../../util/ScriptingGlobals';
+import { FieldsDropdown } from '../FieldsDropdown';
+import { PinDocView } from '../PinFuncs';
+import { DocumentView } from '../nodes/DocumentView';
+import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
+import './CollectionTimeView.scss';
+import { ViewDefBounds, computePivotLayout } from './collectionFreeForm/CollectionFreeFormLayoutEngines';
+import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView';
+
+@observer
+export class CollectionPivotView extends CollectionSubView() {
+    _changing = false;
+    @observable _collapsed: boolean = false;
+    @observable _childClickedScript: Opt<ScriptField> = undefined;
+    @observable _viewDefDivClick: Opt<ScriptField> = undefined;
+    @observable _focusPivotField: Opt<string> = undefined;
+
+    constructor(props: SubCollectionViewProps) {
+        super(props);
+        makeObservable(this);
+    }
+
+    componentDidMount() {
+        this._props.setContentViewBox?.(this);
+        runInAction(() => {
+            this._childClickedScript = ScriptField.MakeScript('openInLightbox(this)', { this: Doc.name });
+            this._viewDefDivClick = ScriptField.MakeScript('pivotColumnClick(this,payload)', { payload: 'any' });
+        });
+    }
+
+    get pivotField() {
+        return this._focusPivotField || StrCast(this.layoutDoc._pivotField);
+    }
+
+    getAnchor = (addAsAnnotation: boolean) => {
+        const anchor = Docs.Create.ConfigDocument({
+            title: ComputedField.MakeFunction(`"${this.pivotField}"])`) as unknown as string, // title can take a functiono or a string
+            annotationOn: this.Document,
+        });
+        PinDocView(anchor, { pinData: { collectionType: true, pivot: true, filters: true } }, this.Document);
+        addAsAnnotation && Doc.AddDocToList(this.dataDoc, this.fieldKey + '_annotations', anchor); // when added as an annotation, links to anchors can be found as links to the document even if the anchors are not rendered
+        return anchor;
+    };
+
+    @action
+    scrollPreview = (docView: DocumentView, anchor: Doc /* , focusSpeed: number, options: FocusViewOptions */) => {
+        // if in preview, then override document's fields with view spec
+        this._focusFilters = StrListCast(anchor.config_docFilters);
+        this._focusRangeFilters = StrListCast(anchor.config_docRangeFilters);
+        this._focusPivotField = StrCast(anchor.config_pivotField);
+        return undefined;
+    };
+
+    toggleVisibility = action(() => {
+        this._collapsed = !this._collapsed;
+    });
+
+    goTo = (prevFilterIndex: number) => {
+        this.layoutDoc._pivotField = this.layoutDoc['_prevPivotFields' + prevFilterIndex];
+        this.layoutDoc._childFilters = ObjectField.MakeCopy(this.layoutDoc['_prevDocFilter' + prevFilterIndex] as ObjectField);
+        this.layoutDoc._childFiltersByRanges = ObjectField.MakeCopy(this.layoutDoc['_prevDocRangeFilters' + prevFilterIndex] as ObjectField);
+        this.layoutDoc._prevFilterIndex = prevFilterIndex;
+    };
+
+    @action
+    contentsDown = () => {
+        const prevFilterIndex = NumCast(this.layoutDoc._prevFilterIndex);
+        if (prevFilterIndex > 0) {
+            this.goTo(prevFilterIndex - 1);
+        } else {
+            this.layoutDoc._childFilters = new List([]);
+        }
+    };
+    layoutEngine = () => computePivotLayout.name;
+    @computed get contents() {
+        return (
+            <div className="collectionTimeView-innards" key="timeline" style={{ pointerEvents: this._props.isContentActive() ? undefined : 'none' }} onClick={this.contentsDown}>
+                <CollectionFreeFormView
+                    {...this._props}
+                    engineProps={{ pivotField: this.pivotField, childFilters: this.childDocFilters, childFiltersByRanges: this.childDocRangeFilters }}
+                    fitContentsToBox={returnTrue}
+                    childClickScript={this._childClickedScript}
+                    viewDefDivClick={this._viewDefDivClick}
+                    layoutEngine={this.layoutEngine}
+                />
+            </div>
+        );
+    }
+
+    render() {
+        return (
+            <div className="collectionTimeView-pivot" style={{ width: this._props.PanelWidth(), height: '100%' }}>
+                {this.contents}
+                <div style={{ right: 0, top: 0, position: 'absolute' }}>
+                    <FieldsDropdown
+                        Document={this.Document}
+                        selectFunc={fieldKey => {
+                            this.layoutDoc._pivotField = fieldKey;
+                        }}
+                        placeholder={StrCast(this.layoutDoc._pivotField)}
+                    />
+                </div>
+            </div>
+        );
+    }
+}
+
+// eslint-disable-next-line prefer-arrow-callback
+ScriptingGlobals.add(function pivotColumnClick(pivotDoc: Doc, bounds: ViewDefBounds) {
+    const pivotField = StrCast(pivotDoc._pivotField, 'author');
+    let prevFilterIndex = NumCast(pivotDoc._prevFilterIndex);
+    const originalFilter = StrListCast(ObjectField.MakeCopy(pivotDoc._childFilters as ObjectField));
+    pivotDoc['_prevDocFilter' + prevFilterIndex] = ObjectField.MakeCopy(pivotDoc._childFilters as ObjectField);
+    pivotDoc['_prevDocRangeFilters' + prevFilterIndex] = ObjectField.MakeCopy(pivotDoc._childFiltersByRanges as ObjectField);
+    pivotDoc['_prevPivotFields' + prevFilterIndex] = pivotField;
+    pivotDoc._prevFilterIndex = ++prevFilterIndex;
+    pivotDoc._childFilters = new List();
+    setTimeout(
+        action(() => {
+            const filterVals = bounds.payload as string[];
+            filterVals.map(filterVal => Doc.setDocFilter(pivotDoc, pivotField, filterVal, 'check'));
+            const pivotView = DocumentView.getDocumentView(pivotDoc);
+            if (pivotDoc && pivotView?.ComponentView instanceof CollectionPivotView && filterVals.length === 1) {
+                if (pivotView?.ComponentView.childDocs.length && pivotView.ComponentView.childDocs[0][filterVals[0]]) {
+                    pivotDoc._pivotField = filterVals[0];
+                }
+            }
+            const newFilters = StrListCast(pivotDoc._childFilters);
+            if (newFilters.length && originalFilter.length && newFilters.lastElement() === originalFilter.lastElement()) {
+                pivotDoc._prevFilterIndex = --prevFilterIndex;
+                pivotDoc['_prevDocFilter' + prevFilterIndex] = undefined;
+                pivotDoc['_prevDocRangeFilters' + prevFilterIndex] = undefined;
+                pivotDoc['_prevPivotFields' + prevFilterIndex] = undefined;
+            }
+        })
+    );
+});
diff --git a/src/client/views/collections/CollectionTimeView.tsx b/src/client/views/collections/CollectionTimeView.tsx
index d75c633ac..98bd06221 100644
--- a/src/client/views/collections/CollectionTimeView.tsx
+++ b/src/client/views/collections/CollectionTimeView.tsx
@@ -1,33 +1,22 @@
-import { action, computed, makeObservable, observable, runInAction } from 'mobx';
+import { action, computed, makeObservable, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { returnFalse, returnTrue, setupMoveUpEvents } from '../../../ClientUtils';
 import { emptyFunction } from '../../../Utils';
 import { Doc, Opt, StrListCast } from '../../../fields/Doc';
-import { List } from '../../../fields/List';
-import { ObjectField } from '../../../fields/ObjectField';
-import { listSpec } from '../../../fields/Schema';
-import { ComputedField, ScriptField } from '../../../fields/ScriptField';
-import { Cast, NumCast, StrCast } from '../../../fields/Types';
+import { NumCast, StrCast } from '../../../fields/Types';
 import { Docs } from '../../documents/Documents';
-import { ScriptingGlobals } from '../../util/ScriptingGlobals';
-import { ContextMenu } from '../ContextMenu';
-import { ContextMenuProps } from '../ContextMenuItem';
 import { FieldsDropdown } from '../FieldsDropdown';
 import { PinDocView } from '../PinFuncs';
 import { DocumentView } from '../nodes/DocumentView';
 import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
 import './CollectionTimeView.scss';
-import { ViewDefBounds, computePivotLayout, computeTimelineLayout } from './collectionFreeForm/CollectionFreeFormLayoutEngines';
+import { computeTimelineLayout } from './collectionFreeForm/CollectionFreeFormLayoutEngines';
 import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView';
 
 @observer
 export class CollectionTimeView extends CollectionSubView() {
-    _changing = false;
-    @observable _layoutEngine = computePivotLayout.name;
     @observable _collapsed: boolean = false;
-    @observable _childClickedScript: Opt<ScriptField> = undefined;
-    @observable _viewDefDivClick: Opt<ScriptField> = undefined;
     @observable _focusPivotField: Opt<string> = undefined;
 
     constructor(props: SubCollectionViewProps) {
@@ -37,10 +26,6 @@ export class CollectionTimeView extends CollectionSubView() {
 
     componentDidMount() {
         this._props.setContentViewBox?.(this);
-        runInAction(() => {
-            this._childClickedScript = ScriptField.MakeScript('openInLightbox(this)', { this: Doc.name });
-            this._viewDefDivClick = ScriptField.MakeScript('pivotColumnClick(this,payload)', { payload: 'any' });
-        });
     }
 
     get pivotField() {
@@ -49,19 +34,10 @@ export class CollectionTimeView extends CollectionSubView() {
 
     getAnchor = (addAsAnnotation: boolean) => {
         const anchor = Docs.Create.ConfigDocument({
-            title: ComputedField.MakeFunction(`"${this.pivotField}"])`) as unknown as string, // title can take a functiono or a string
             annotationOn: this.Document,
         });
         PinDocView(anchor, { pinData: { collectionType: true, pivot: true, filters: true } }, this.Document);
-
-        if (addAsAnnotation) {
-            // when added as an annotation, links to anchors can be found as links to the document even if the anchors are not rendered
-            if (Cast(this.dataDoc[this._props.fieldKey + '_annotations'], listSpec(Doc), null) !== undefined) {
-                Cast(this.dataDoc[this._props.fieldKey + '_annotations'], listSpec(Doc), []).push(anchor);
-            } else {
-                this.dataDoc[this._props.fieldKey + '_annotations'] = new List<Doc>([anchor]);
-            }
-        }
+        addAsAnnotation && Doc.AddDocToList(this.dataDoc, this.fieldKey + '_annotations', anchor); // when added as an annotation, links to anchors can be found as links to the document even if the anchors are not rendered
         return anchor;
     };
 
@@ -74,7 +50,6 @@ export class CollectionTimeView extends CollectionSubView() {
         return undefined;
     };
 
-    layoutEngine = () => this._layoutEngine;
     toggleVisibility = action(() => {
         this._collapsed = !this._collapsed;
     });
@@ -126,105 +101,24 @@ export class CollectionTimeView extends CollectionSubView() {
         );
     };
 
-    goTo = (prevFilterIndex: number) => {
-        this.layoutDoc._pivotField = this.layoutDoc['_prevPivotFields' + prevFilterIndex];
-        this.layoutDoc._childFilters = ObjectField.MakeCopy(this.layoutDoc['_prevDocFilter' + prevFilterIndex] as ObjectField);
-        this.layoutDoc._childFiltersByRanges = ObjectField.MakeCopy(this.layoutDoc['_prevDocRangeFilters' + prevFilterIndex] as ObjectField);
-        this.layoutDoc._prevFilterIndex = prevFilterIndex;
-    };
-
-    @action
-    contentsDown = () => {
-        const prevFilterIndex = NumCast(this.layoutDoc._prevFilterIndex);
-        if (prevFilterIndex > 0) {
-            this.goTo(prevFilterIndex - 1);
-        } else {
-            this.layoutDoc._childFilters = new List([]);
-        }
-    };
-
+    layoutEngine = () => computeTimelineLayout.name;
     @computed get contents() {
         return (
-            <div className="collectionTimeView-innards" key="timeline" style={{ pointerEvents: this._props.isContentActive() ? undefined : 'none' }} onClick={this.contentsDown}>
+            <div className="collectionTimeView-innards" key="timeline" style={{ pointerEvents: this._props.isContentActive() ? undefined : 'none' }}>
                 <CollectionFreeFormView
                     {...this._props}
                     engineProps={{ pivotField: this.pivotField, childFilters: this.childDocFilters, childFiltersByRanges: this.childDocRangeFilters }}
                     fitContentsToBox={returnTrue}
-                    childClickScript={this._childClickedScript}
-                    viewDefDivClick={this.layoutEngine() === computeTimelineLayout.name ? undefined : this._viewDefDivClick}
                     layoutEngine={this.layoutEngine}
                 />
             </div>
         );
     }
 
-    public static SyncTimelineToPresentation(doc: Doc) {
-        const fieldKey = Doc.LayoutFieldKey(doc);
-        doc[fieldKey + '-timelineCur'] = ComputedField.MakeFunction("(activePresentationItem()[this._pivotField || 'year'] || 0)");
-    }
-    specificMenu = () => {
-        const layoutItems: ContextMenuProps[] = [];
-        const doc = this.layoutDoc;
-
-        layoutItems.push({
-            description: 'Force Timeline',
-            event: () => {
-                doc._forceRenderEngine = computeTimelineLayout.name;
-            },
-            icon: 'compress-arrows-alt',
-        });
-        layoutItems.push({
-            description: 'Force Pivot',
-            event: () => {
-                doc._forceRenderEngine = computePivotLayout.name;
-            },
-            icon: 'compress-arrows-alt',
-        });
-        layoutItems.push({
-            description: 'Auto Time/Pivot layout',
-            event: () => {
-                doc._forceRenderEngine = undefined;
-            },
-            icon: 'compress-arrows-alt',
-        });
-        layoutItems.push({ description: 'Sync with presentation', event: () => CollectionTimeView.SyncTimelineToPresentation(doc), icon: 'compress-arrows-alt' });
-
-        ContextMenu.Instance.addItem({ description: 'Options...', subitems: layoutItems, icon: 'eye' });
-    };
-
     render() {
-        let nonNumbers = 0;
-        this.childDocs.forEach(doc => {
-            const num = NumCast(doc[this.pivotField], Number(StrCast(doc[this.pivotField])));
-            if (isNaN(num)) {
-                nonNumbers++;
-            }
-        });
-        const forceLayout = StrCast(this.layoutDoc._forceRenderEngine);
-        const doTimeline = forceLayout ? forceLayout === computeTimelineLayout.name : nonNumbers / this.childDocs.length < 0.1 && this._props.PanelWidth() / this._props.PanelHeight() > 6;
-        if (doTimeline !== (this._layoutEngine === computeTimelineLayout.name)) {
-            if (!this._changing) {
-                this._changing = true;
-                setTimeout(
-                    action(() => {
-                        this._layoutEngine = doTimeline ? computeTimelineLayout.name : computePivotLayout.name;
-                        this._changing = false;
-                    }),
-                    0
-                );
-            }
-        }
-
         return (
-            <div className={'collectionTimeView' + (doTimeline ? '' : '-pivot')} onContextMenu={this.specificMenu} style={{ width: this._props.PanelWidth(), height: '100%' }}>
+            <div className="collectionTimeView" style={{ width: this._props.PanelWidth(), height: '100%' }}>
                 {this.contents}
-                {!this._props.isSelected() || !doTimeline ? null : (
-                    <>
-                        <div className="collectionTimeView-thumb-min collectionTimeView-thumb" key="min" onPointerDown={this.onMinDown} />
-                        <div className="collectionTimeView-thumb-max collectionTimeView-thumb" key="mid" onPointerDown={this.onMaxDown} />
-                        <div className="collectionTimeView-thumb-mid collectionTimeView-thumb" key="max" onPointerDown={this.onMidDown} />
-                    </>
-                )}
                 <div style={{ right: 0, top: 0, position: 'absolute' }}>
                     <FieldsDropdown
                         Document={this.Document}
@@ -234,38 +128,14 @@ export class CollectionTimeView extends CollectionSubView() {
                         placeholder={StrCast(this.layoutDoc._pivotField)}
                     />
                 </div>
+                {!this._props.isSelected() ? null : (
+                    <>
+                        <div className="collectionTimeView-thumb-min collectionTimeView-thumb" key="min" onPointerDown={this.onMinDown} />
+                        <div className="collectionTimeView-thumb-max collectionTimeView-thumb" key="mid" onPointerDown={this.onMaxDown} />
+                        <div className="collectionTimeView-thumb-mid collectionTimeView-thumb" key="max" onPointerDown={this.onMidDown} />
+                    </>
+                )}
             </div>
         );
     }
 }
-
-// eslint-disable-next-line prefer-arrow-callback
-ScriptingGlobals.add(function pivotColumnClick(pivotDoc: Doc, bounds: ViewDefBounds) {
-    const pivotField = StrCast(pivotDoc._pivotField, 'author');
-    let prevFilterIndex = NumCast(pivotDoc._prevFilterIndex);
-    const originalFilter = StrListCast(ObjectField.MakeCopy(pivotDoc._childFilters as ObjectField));
-    pivotDoc['_prevDocFilter' + prevFilterIndex] = ObjectField.MakeCopy(pivotDoc._childFilters as ObjectField);
-    pivotDoc['_prevDocRangeFilters' + prevFilterIndex] = ObjectField.MakeCopy(pivotDoc._childFiltersByRanges as ObjectField);
-    pivotDoc['_prevPivotFields' + prevFilterIndex] = pivotField;
-    pivotDoc._prevFilterIndex = ++prevFilterIndex;
-    pivotDoc._childFilters = new List();
-    setTimeout(
-        action(() => {
-            const filterVals = bounds.payload as string[];
-            filterVals.map(filterVal => Doc.setDocFilter(pivotDoc, pivotField, filterVal, 'check'));
-            const pivotView = DocumentView.getDocumentView(pivotDoc);
-            if (pivotDoc && pivotView?.ComponentView instanceof CollectionTimeView && filterVals.length === 1) {
-                if (pivotView?.ComponentView.childDocs.length && pivotView.ComponentView.childDocs[0][filterVals[0]]) {
-                    pivotDoc._pivotField = filterVals[0];
-                }
-            }
-            const newFilters = StrListCast(pivotDoc._childFilters);
-            if (newFilters.length && originalFilter.length && newFilters.lastElement() === originalFilter.lastElement()) {
-                pivotDoc._prevFilterIndex = --prevFilterIndex;
-                pivotDoc['_prevDocFilter' + prevFilterIndex] = undefined;
-                pivotDoc['_prevDocRangeFilters' + prevFilterIndex] = undefined;
-                pivotDoc['_prevPivotFields' + prevFilterIndex] = undefined;
-            }
-        })
-    );
-});
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index a4900e9d7..7ba8989ce 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -15,12 +15,14 @@ import { ContextMenuProps } from '../ContextMenuItem';
 import { ViewBoxAnnotatableComponent } from '../DocComponent';
 import { FieldView } from '../nodes/FieldView';
 import { OpenWhere } from '../nodes/OpenWhere';
+import { CalendarBox } from '../nodes/calendarBox/CalendarBox';
 import { CollectionCardView } from './CollectionCardDeckView';
 import { CollectionCarousel3DView } from './CollectionCarousel3DView';
 import { CollectionCarouselView } from './CollectionCarouselView';
 import { CollectionDockingView } from './CollectionDockingView';
 import { CollectionNoteTakingView } from './CollectionNoteTakingView';
 import { CollectionPileView } from './CollectionPileView';
+import { CollectionPivotView } from './CollectionPivotView';
 import { CollectionStackingView } from './CollectionStackingView';
 import { CollectionViewProps, SubCollectionViewProps } from './CollectionSubView';
 import { CollectionTimeView } from './CollectionTimeView';
@@ -32,7 +34,6 @@ 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>() {
@@ -105,6 +106,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewPr
             case CollectionViewType.NoteTaking:   return <CollectionNoteTakingView  key="collview" {...props} />;
             case CollectionViewType.Masonry:      return <CollectionStackingView    key="collview" {...props} />;
             case CollectionViewType.Time:         return <CollectionTimeView        key="collview" {...props} />;
+            case CollectionViewType.Pivot:        return <CollectionPivotView       key="collview" {...props} />;
             case CollectionViewType.Grid:         return <CollectionGridView        key="collview" {...props} />;
             case CollectionViewType.Card:         return <CollectionCardView        key="collview" {...props} />;
         }
@@ -126,7 +128,8 @@ export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewPr
                 { 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: 'Pivot',       event: () => func(CollectionViewType.Pivot), icon: 'columns' },
+                { description: '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/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
index bebdbd731..241a56a88 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
@@ -4,7 +4,7 @@ import { Id, ToString } from '../../../../fields/FieldSymbols';
 import { ObjectField } from '../../../../fields/ObjectField';
 import { RefField } from '../../../../fields/RefField';
 import { listSpec } from '../../../../fields/Schema';
-import { Cast, NumCast, StrCast } from '../../../../fields/Types';
+import { Cast, DateCast, NumCast, StrCast } from '../../../../fields/Types';
 import { aggregateBounds } from '../../../../Utils';
 
 export interface ViewDefBounds {
@@ -107,7 +107,7 @@ export function computePassLayout(poolData: Map<string, PoolData>, pivotDoc: Doc
 }
 
 function toNumber(val: FieldResult<FieldType>) {
-    return val === undefined ? undefined : NumCast(val, Number(StrCast(val)));
+    return val === undefined ? undefined : DateCast(val) ? DateCast(val).date.getMilliseconds() : NumCast(val, Number(StrCast(val)));
 }
 
 export function computeStarburstLayout(poolData: Map<string, PoolData>, pivotDoc: Doc, childPairs: { layout: Doc; data?: Doc }[], panelDim: number[], viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[] /* , engineProps: any */) {
@@ -295,7 +295,7 @@ export function computeTimelineLayout(poolData: Map<string, PoolData>, pivotDoc:
     let minTime = minTimeReq === undefined ? Number.MAX_VALUE : minTimeReq;
     let maxTime = maxTimeReq === undefined ? -Number.MAX_VALUE : maxTimeReq;
     childPairs.forEach(pair => {
-        const num = NumCast(pair.layout[timelineFieldKey], Number(StrCast(pair.layout[timelineFieldKey])));
+        const num = toNumber(pair.layout[timelineFieldKey]) ?? 0;
         if (!isNaN(num) && (!minTimeReq || num >= minTimeReq) && (!maxTimeReq || num <= maxTimeReq)) {
             !pivotDateGroups.get(num) && pivotDateGroups.set(num, []);
             pivotDateGroups.get(num)!.push(pair.layout);
-- 
cgit v1.2.3-70-g09d2

