From 18774b42e3c8e1e899978fe9f16a4d123adee803 Mon Sep 17 00:00:00 2001
From: Geireann Lindfield Roberts <geireann_lindfield_roberts@brown.edu>
Date: Wed, 1 Jan 2025 22:32:19 -0800
Subject: monorepo setup

---
 src/client/views/collections/FlashcardPracticeUI.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

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

diff --git a/src/client/views/collections/FlashcardPracticeUI.tsx b/src/client/views/collections/FlashcardPracticeUI.tsx
index c298bff22..77f1db9ad 100644
--- a/src/client/views/collections/FlashcardPracticeUI.tsx
+++ b/src/client/views/collections/FlashcardPracticeUI.tsx
@@ -1,7 +1,7 @@
 import { IconProp } from '@fortawesome/fontawesome-svg-core';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Tooltip } from '@mui/material';
-import { MultiToggle, Type } from 'browndash-components';
+import { MultiToggle, Type } from '@dash/components';
 import { computed, makeObservable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-- 
cgit v1.2.3-70-g09d2


From a496c583f375fe5e118f786b1244c42bc8a34fec Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 28 Jan 2025 14:20:40 -0500
Subject: fixed flashcard practice mode.  moved doc sorting from cardDeck into
 collectionSubView to apply it to all collections.

---
 .../src/components/Dropdown/Dropdown.tsx           |  30 ++----
 src/client/util/CurrentUserUtils.ts                |  10 +-
 .../views/collections/CollectionCardDeckView.tsx   | 119 ++++-----------------
 src/client/views/collections/CollectionSubView.tsx |  49 ++++++++-
 .../views/collections/FlashcardPracticeUI.tsx      |   4 +-
 src/client/views/global/globalScripts.ts           |  35 +++---
 src/client/views/nodes/ComparisonBox.tsx           |   2 +-
 src/client/views/nodes/imageEditor/ImageEditor.tsx |  69 ++----------
 .../views/nodes/imageEditor/ImageEditorButtons.tsx |   1 +
 9 files changed, 108 insertions(+), 211 deletions(-)

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

diff --git a/packages/components/src/components/Dropdown/Dropdown.tsx b/packages/components/src/components/Dropdown/Dropdown.tsx
index 0953f412c..b9b6f01b8 100644
--- a/packages/components/src/components/Dropdown/Dropdown.tsx
+++ b/packages/components/src/components/Dropdown/Dropdown.tsx
@@ -99,22 +99,14 @@ export const Dropdown = (props: IDropdownProps) => {
         background: background ?? color,
     };
 
-    const getCaretDirection = (active: boolean, placement: Placement = 'left'): JSX.Element => {
-        if (iconProvider) return iconProvider(active, placement);
-        switch (placement) {
-            case 'bottom':
-                if (active) return <FaCaretUp />;
-                return <FaCaretDown />;
-            case 'right':
-                if (active) return <FaCaretLeft />;
-                return <FaCaretRight />;
-            case 'top':
-                if (active) return <FaCaretDown />;
-                return <FaCaretUp />;
-            default:
-                if (active) return <FaCaretUp />;
-                return <FaCaretDown />;
-        }
+    const getCaretDirection = (isActive: boolean, caretPlacement: Placement = 'left'): JSX.Element => {
+        if (iconProvider) return iconProvider(isActive, caretPlacement);
+        switch (caretPlacement) {
+            default:      
+            case 'bottom':return isActive ?  <FaCaretUp />: <FaCaretDown />;
+            case 'right': return isActive ? <FaCaretLeft /> : <FaCaretRight />;
+            case 'top':   return isActive ? <FaCaretDown />: <FaCaretUp />;
+        } // prettier-ignore
     };
 
     const getToggle = () => {
@@ -143,9 +135,9 @@ export const Dropdown = (props: IDropdownProps) => {
         }
     };
 
-    const setActiveChanged = (active: boolean) => {
-        setActive(active);
-        props.activeChanged?.(active);
+    const setActiveChanged = (isActive: boolean) => {
+        setActive(isActive);
+        props.activeChanged?.(isActive);
     };
 
     const dropdown: JSX.Element = (
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 7c36a82f2..e806675aa 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -688,7 +688,7 @@ pie title Minerals in my tap water
             { title: "Center",    icon: "align-center",  toolTip: "Center Align Stack",  btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"hcenter",    funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform
         ]
     }
-    static cardTools(): Button[] {
+    static sortTools(): Button[] {
         return [
             { title: "Time",      icon:"hourglass-half", toolTip:"Sort by most recent document creation",          btnType: ButtonType.ToggleButton, expertMode: false, toolType:"time",       funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}},
             { title: "Type",      icon:"eye",            toolTip:"Sort by document type",                          btnType: ButtonType.ToggleButton, expertMode: false, toolType:"docType",    funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}},
@@ -704,7 +704,7 @@ pie title Minerals in my tap water
          ]
     }
   
-    static tagGroupTools(): Button[] {
+    static filterTools(): Button[] {
         const defaultTagButtonDescs = [
             { title: "Star", isSystem: false,icon: "star",  toolTip:"Click to toggle visibility of Star tagged Docs",  btnType: ButtonType.ToggleButton,  expertMode: false, toolType:"#star", funcs: {}, scripts: { onClick: '{ return setTagFilter(this.toolType, _added_, _readOnly_);}'}},
             { title: "Like", isSystem: false,icon: "heart", toolTip:"Click to toggle visibility of Like tagged Docs",  btnType: ButtonType.ToggleButton,  expertMode: false, toolType:"#like", funcs: {}, scripts: { onClick: '{ return setTagFilter(this.toolType, _added_, _readOnly_);}'}},
@@ -723,7 +723,6 @@ pie title Minerals in my tap water
             { 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: "Cards",     icon: "brain",               toolTip: "Flashcards",          btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"flashcards", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform
         ]
     }
     static textTools():Button[] {
@@ -822,15 +821,14 @@ pie title Minerals in my tap water
             { 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: "Filter",  icon: "=",      toolTip: "Filter cards by tags", subMenu: CurrentUserUtils.tagGroupTools(),ignoreClick:true, toolType:DocumentType.COL,            funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)'}, btnType: ButtonType.MultiToggleButton, width: 30, backgroundColor: doc.userVariantColor as string},
+            { 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: "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: "Card",    icon: "Card",   toolTip: "Card View Tools",      subMenu: CurrentUserUtils.cardTools(),   expertMode: false, toolType:CollectionViewType.Card,     funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available
-         // { title: "Create",  icon: "Create", toolTip: "Assign card labels",   subMenu: CurrentUserUtils.labelTools(),  expertMode: false, toolType:CollectionViewType.Card,     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
             { title: "Video",   icon: "Video",  toolTip: "Video functions",      subMenu: CurrentUserUtils.videoTools(),  expertMode: false, toolType:DocumentType.VID,            funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Only when video is selected
             { title: "Image",   icon: "Image",  toolTip: "Image functions",      subMenu: CurrentUserUtils.imageTools(),  expertMode: false, toolType:DocumentType.IMG,            funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Only when image is selected
diff --git a/src/client/views/collections/CollectionCardDeckView.tsx b/src/client/views/collections/CollectionCardDeckView.tsx
index 1bca68846..aaf33c3a1 100644
--- a/src/client/views/collections/CollectionCardDeckView.tsx
+++ b/src/client/views/collections/CollectionCardDeckView.tsx
@@ -1,16 +1,16 @@
-import { IReactionDisposer, ObservableMap, action, computed, makeObservable, observable, reaction, runInAction, trace } from 'mobx';
+import { IReactionDisposer, ObservableMap, action, computed, makeObservable, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import { computedFn } from 'mobx-utils';
 import * as React from 'react';
 import * as CSS from 'csstype';
-import { ClientUtils, DashColor, imageUrlToBase64, returnFalse, returnNever, returnZero, setupMoveUpEvents } from '../../../ClientUtils';
+import { ClientUtils, imageUrlToBase64, returnFalse, returnNever, returnZero, setupMoveUpEvents } from '../../../ClientUtils';
 import { emptyFunction } from '../../../Utils';
-import { Doc, DocListCast, Opt, StrListCast } from '../../../fields/Doc';
+import { Doc, DocListCast, Opt } from '../../../fields/Doc';
 import { Animation, DocData } from '../../../fields/DocSymbols';
 import { Id } from '../../../fields/FieldSymbols';
 import { List } from '../../../fields/List';
 import { ScriptField } from '../../../fields/ScriptField';
-import { BoolCast, DateCast, DocCast, NumCast, RTFCast, ScriptCast, StrCast } from '../../../fields/Types';
+import { BoolCast, DocCast, NumCast, RTFCast, ScriptCast, StrCast } from '../../../fields/Types';
 import { URLField } from '../../../fields/URLField';
 import { gptImageLabel } from '../../apis/gpt/GPT';
 import { DocumentType } from '../../documents/DocumentTypes';
@@ -25,21 +25,12 @@ import { StyleProp } from '../StyleProp';
 import { TagItem } from '../TagsView';
 import { DocumentView, DocumentViewProps } from '../nodes/DocumentView';
 import { FocusViewOptions } from '../nodes/FocusViewOptions';
-import { GPTPopup, GPTPopupMode } from '../pdf/GPTPopup/GPTPopup';
+import { GPTPopup } from '../pdf/GPTPopup/GPTPopup';
 import './CollectionCardDeckView.scss';
 import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { SettingsManager } from '../../util/SettingsManager';
 
-enum cardSortings {
-    Time = 'time',
-    Type = 'type',
-    Color = 'color',
-    Chat = 'chat',
-    Tag = 'tag',
-    None = '',
-}
-
 /**
  * New view type specifically for studying more dynamically. Allows you to reorder docs however you see fit, easily
  * sort and filter using presets, and customize your experience with chat gpt.
@@ -60,7 +51,6 @@ export class CollectionCardView extends CollectionSubView() {
     @observable _forceChildXf = 0;
     @observable _hoveredNodeIndex = -1;
     @observable _docRefs = new ObservableMap<Doc, DocumentView>();
-    @observable _docDraggedIndex: number = -1;
     @observable _cursor: CSS.Property.Cursor = 'ew-resize';
 
     constructor(props: SubCollectionViewProps) {
@@ -102,18 +92,8 @@ export class CollectionCardView extends CollectionSubView() {
 
     componentDidMount() {
         this._props.setContentViewBox?.(this);
-        this._disposers.sort = reaction(
-            () => GPTPopup.Instance.visible,
-            isVis => {
-                if (isVis) {
-                    this.openChatPopup();
-                } else {
-                    this.Document.card_sort = this.cardSort === cardSortings.Chat ? '' : this.Document.card_sort;
-                }
-            }
-        );
         // 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 renders, we need to act like the
+        // when inquired from the dom (below in childScreenToLocal).  When the doc is actually rendered, we need to act like the
         // dash data just changed and trigger a React involidation with the correct data (read from the dom).
         this._disposers.child = reaction(
             () => [this.Document.x, this.Document.y],
@@ -136,14 +116,11 @@ export class CollectionCardView extends CollectionSubView() {
         this._dropDisposer?.();
     }
 
-    @computed get cardSort() {
-        return StrCast(this.Document.card_sort) as cardSortings;
-    }
     /**
      * Number of rows of cards to be rendered
      */
     @computed get numRows() {
-        return Math.ceil(this.sortedDocs.length / this._maxRowCount);
+        return Math.ceil(this.childDocs.length / this._maxRowCount);
     }
     /**
      * Circle arc size, in radians, to layout cards
@@ -211,7 +188,7 @@ export class CollectionCardView extends CollectionSubView() {
      * @returns the card's new index
      */
     findCardDropIndex = (mouseX: number, mouseY: number) => {
-        const cardCount = this.sortedDocs.length;
+        const cardCount = this.childDocs.length;
         let index = 0;
         const cardWidth = cardCount < this._maxRowCount ? this._props.PanelWidth() / cardCount : this._props.PanelWidth() / this._maxRowCount;
 
@@ -245,8 +222,8 @@ export class CollectionCardView extends CollectionSubView() {
      */
     @action
     onPointerMove = (x: number, y: number) => {
-        if (DragManager.docsBeingDragged.some(doc => this.sortedDocs.includes(doc)) || SnappingManager.CanEmbed) {
-            this._docDraggedIndex = this.findCardDropIndex(x, y);
+        if (DragManager.docsBeingDragged.some(doc => this.childDocs.includes(doc)) || SnappingManager.CanEmbed) {
+            this.docDraggedIndex = this.findCardDropIndex(x, y);
         }
     };
 
@@ -259,11 +236,11 @@ export class CollectionCardView extends CollectionSubView() {
     onInternalDrop = undoable(
         action((e: Event, de: DragManager.DropEvent) => {
             if (de.complete.docDragData) {
-                const dragIndex = this._docDraggedIndex;
+                const dragIndex = this.docDraggedIndex;
                 const draggedDoc = DragManager.docsBeingDragged[0];
                 if (dragIndex > -1 && draggedDoc) {
-                    this._docDraggedIndex = -1;
-                    const sorted = this.sortedDocs;
+                    this.docDraggedIndex = -1;
+                    const sorted = this.childDocs;
                     const originalIndex = sorted.findIndex(doc => doc === draggedDoc);
 
                     this.Document.card_sort = '';
@@ -295,50 +272,6 @@ export class CollectionCardView extends CollectionSubView() {
             .map(({ i }) => i)
             .join('.');
 
-    /**
-     * Called in the sortedDocsType method. Compares the cards' value in regards to the desired sort type-- earlier cards are move to the
-     * front, latter cards to the back
-     * @param docs
-     * @param sortType
-     * @param isDesc
-     * @returns
-     */
-    sort = (docsIn: Doc[], sortType: cardSortings, isDesc: boolean, dragIndex: number) => {
-        const docs = docsIn.slice(); // need make new object list since sort() modifies the incoming list which confuses mobx caching
-        sortType &&
-            docs.sort((docA, docB) => {
-                const [typeA, typeB] = (() => {
-                    switch (sortType) {
-                        default:                
-                        case cardSortings.Type: return [StrCast(docA.type),                                   StrCast(docB.type)];
-                        case cardSortings.Chat: return [NumCast(docA.chatIndex, 9999),                        NumCast(docB.chatIndex,9999)];
-                        case cardSortings.Time: return [DateCast(docA.author_date)?.date ?? Date.now(),       DateCast(docB.author_date)?.date ?? Date.now()];
-                        case cardSortings.Color:return [DashColor(StrCast(docA.backgroundColor)).hsv().hue(), DashColor(StrCast(docB.backgroundColor)).hsv().hue()];
-                        case cardSortings.Tag:  return [StrListCast(docA.tags).join(""),                      StrListCast(docB.tags).join("")];
-                    }
-                })(); //prettier-ignore
-                return (typeA < typeB ? -1 : typeA > typeB ? 1 : 0) * (isDesc ? 1 : -1);
-            });
-        if (dragIndex !== -1) {
-            const draggedDoc = DragManager.docsBeingDragged[0];
-            const originalIndex = docs.findIndex(doc => doc === draggedDoc);
-
-            originalIndex !== -1 && docs.splice(originalIndex, 1);
-            draggedDoc && docs.splice(dragIndex, 0, draggedDoc);
-        }
-
-        return docs;
-    };
-
-    @computed get sortedDocs() {
-        return this.sort(
-            this.childCards.map(card => card.layout),
-            this.cardSort,
-            BoolCast(this.Document.card_sort_isDesc),
-            this._docDraggedIndex
-        );
-    }
-
     isChildContentActive = computedFn(
         (doc: Doc) => () =>
             this._props.isContentActive?.() === false
@@ -560,16 +493,6 @@ export class CollectionCardView extends CollectionSubView() {
         });
     }, '');
 
-    /**
-     * Opens up the chat popup and starts the process for smart sorting.
-     */
-    openChatPopup = async () => {
-        GPTPopup.Instance.setVisible(true);
-        GPTPopup.Instance.setMode(GPTPopupMode.CARD);
-        GPTPopup.Instance.setCardsDoneLoading(true); // Set dataDoneLoading to true after data is loaded
-        await this.childPairStringListAndUpdateSortDesc();
-    };
-
     childScreenToLocal = computedFn((doc: Doc, index: number, isSelected: boolean) => () => {
         // need to explicitly trigger an invalidation since we're reading everything from the Dom
         this._forceChildXf;
@@ -615,7 +538,7 @@ export class CollectionCardView extends CollectionSubView() {
         setupMoveUpEvents(
             this,
             e,
-            (e: PointerEvent, down: number[], delta: number[]) => {
+            (emove: PointerEvent, down: number[], delta: number[]) => {
                 this.layoutDoc._cardWidth = Math.max(10, delta[0] < 0 ? Math.floor(this.cardWidth + delta[0]) : Math.ceil(this.cardWidth + delta[0]));
                 return false;
             },
@@ -662,9 +585,9 @@ export class CollectionCardView extends CollectionSubView() {
      * Actually renders all the cards
      */
     @computed get renderCards() {
-        trace();
+        console.log(this.docDraggedIndex, this.childDocs[0].title, this.childDocs[1].title);
         // Map sorted documents to their rendered components
-        return this.sortedDocs.map((doc, index) => {
+        return this.childDocs.map((doc, index) => {
             const cardsInRow = this.cardsInRowThatIncludesCardIndex(index);
 
             const childScreenToLocal = this.childScreenToLocal(doc, index, doc === this.curDoc());
@@ -674,7 +597,7 @@ export class CollectionCardView extends CollectionSubView() {
             const aspect = NumCast(doc.height) / NumCast(doc.width, 1);
             const vscale = Math.max(1,Math.min((this._props.PanelHeight() * 0.95 * this.fitContentScale * this.nativeScaling) / (aspect * this.childPanelWidth()), 
                                     (this._props.PanelHeight() - 80) / (aspect * (this._props.PanelWidth() / 10)))); // prettier-ignore
-            const hscale = Math.min(this.sortedDocs.length, this._maxRowCount) / 2; // bcz: hack - the grid is divided evenly into maxRowCount cells, so the max scaling would be maxRowCount -- but making things that wide is ugly, so cap it off at half the window size
+            const hscale = Math.min(this.childDocs.length, this._maxRowCount) / 2; // bcz: hack - the grid is divided evenly into maxRowCount cells, so the max scaling would be maxRowCount -- but making things that wide is ugly, so cap it off at half the window size
             return (
                 <div
                     key={doc[Id]}
@@ -710,17 +633,13 @@ export class CollectionCardView extends CollectionSubView() {
     curDoc = () => DocCast(this.layoutDoc._card_curDoc);
 
     render() {
-        trace();
         const fitContentScale = this.childCards.length === 0 ? 1 : this.fitContentScale;
         return (
             <div
                 className="collectionCardView-outer"
                 ref={(ele: HTMLDivElement | null) => this.createDashEventsTarget(ele)}
-                onPointerDown={action(e => {
-                    if (e.button === 2 || e.ctrlKey) return;
-                    this.releaseCurDoc();
-                })}
-                onPointerLeave={action(() => (this._docDraggedIndex = -1))}
+                onPointerDown={e => e.button !== 2 && !e.ctrlKey && this.releaseCurDoc()}
+                onPointerLeave={action(() => (this.docDraggedIndex = -1))}
                 onPointerMove={e => this.onPointerMove(...this._props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY))}
                 onDrop={this.onExternalDrop.bind(this)}
                 style={{
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 0c059f729..5e99bec39 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -1,7 +1,7 @@
 import { action, computed, makeObservable, observable } from 'mobx';
 import * as React from 'react';
 import * as rp from 'request-promise';
-import { ClientUtils, returnFalse } from '../../../ClientUtils';
+import { ClientUtils, DashColor, returnFalse } from '../../../ClientUtils';
 import CursorField from '../../../fields/CursorField';
 import { Doc, DocListCast, GetDocFromUrl, GetHrefFromHTML, Opt, RTFIsFragment, StrListCast } from '../../../fields/Doc';
 import { AclPrivate, DocData } from '../../../fields/DocSymbols';
@@ -9,7 +9,7 @@ import { Id } from '../../../fields/FieldSymbols';
 import { List } from '../../../fields/List';
 import { listSpec } from '../../../fields/Schema';
 import { ScriptField } from '../../../fields/ScriptField';
-import { BoolCast, Cast, NumCast, ScriptCast, StrCast, toList } from '../../../fields/Types';
+import { BoolCast, Cast, DateCast, NumCast, ScriptCast, StrCast, toList } from '../../../fields/Types';
 import { WebField } from '../../../fields/URLField';
 import { GetEffectiveAcl, TraceMobx } from '../../../fields/util';
 import { GestureUtils } from '../../../pen-gestures/GestureUtils';
@@ -29,6 +29,14 @@ import { DocumentView, DocumentViewProps } from '../nodes/DocumentView';
 import { FlashcardPracticeUI } from './FlashcardPracticeUI';
 import { OpenWhere, OpenWhereMod } from '../nodes/OpenWhere';
 
+export enum docSortings {
+    Time = 'time',
+    Type = 'type',
+    Color = 'color',
+    Chat = 'chat',
+    Tag = 'tag',
+    None = '',
+}
 export interface CollectionViewProps extends React.PropsWithChildren<FieldViewProps> {
     isAnnotationOverlay?: boolean; // is the collection an annotation overlay (eg an overlay on an image/video/etc)
     isAnnotationOverlayScrollable?: boolean; // whether the annotation overlay can be vertically scrolled (just for tree views, currently)
@@ -150,6 +158,8 @@ export function CollectionSubView<X>() {
         unrecursiveDocFilters = () => [...(this._props.childFilters?.().filter(f => !ClientUtils.IsRecursiveFilter(f)) || [])];
         childDocRangeFilters = () => [...(this._props.childFiltersByRanges?.() || []), ...this.collectionRangeDocFilters()];
         searchFilterDocs = () => this._props.searchFilterDocs?.() ?? DocListCast(this.Document._searchFilterDocs);
+
+        @observable docDraggedIndex = -1;
         @computed.struct get childDocs() {
             TraceMobx();
             let rawdocs: (Doc | Promise<Doc>)[] = [];
@@ -166,8 +176,10 @@ export function CollectionSubView<X>() {
                 const templateRoot = this._props.TemplateDataDocument;
                 rawdocs = templateRoot && !this._props.isAnnotationOverlay ? [Doc.GetProto(templateRoot)] : [];
             }
-
-            const childDocs = rawdocs.filter(d => !(d instanceof Promise) && GetEffectiveAcl(Doc.GetProto(d)) !== AclPrivate && (this._props.ignoreUnrendered || !d.layout_unrendered)).map(d => d as Doc);
+            const childDocs = this.childSortedDocs(
+                rawdocs.filter(d => !(d instanceof Promise) && GetEffectiveAcl(Doc.GetProto(d)) !== AclPrivate && (this._props.ignoreUnrendered || !d.layout_unrendered)).map(d => d as Doc),
+                this.docDraggedIndex
+            );
 
             const childDocFilters = this.childDocFilters();
             const childFiltersByRanges = this.childDocRangeFilters();
@@ -214,6 +226,35 @@ export function CollectionSubView<X>() {
             return docsforFilter;
         }
 
+        childSortedDocs = (docsIn: Doc[], dragIndex: number) => {
+            const sortType = StrCast(this.Document[this._props.fieldKey + '_sort']) as docSortings;
+            const isDesc = BoolCast(this.Document[this._props.fieldKey + '_sort_desc']);
+            const docs = docsIn.slice();
+            if (sortType) {
+                docs.sort((docA, docB) => {
+                    const [typeA, typeB] = (() => {
+                        switch (sortType) {
+                            default:                
+                            case docSortings.Type: return [StrCast(docA.type),                                   StrCast(docB.type)];
+                            case docSortings.Chat: return [NumCast(docA.chatIndex, 9999),                        NumCast(docB.chatIndex,9999)];
+                            case docSortings.Time: return [DateCast(docA.author_date)?.date ?? Date.now(),       DateCast(docB.author_date)?.date ?? Date.now()];
+                            case docSortings.Color:return [DashColor(StrCast(docA.backgroundColor)).hsv().hue(), DashColor(StrCast(docB.backgroundColor)).hsv().hue()];
+                            case docSortings.Tag:  return [StrListCast(docA.tags).join(""),                      StrListCast(docB.tags).join("")];
+                        }
+                    })(); //prettier-ignore
+                    return (typeA < typeB ? -1 : typeA > typeB ? 1 : 0) * (isDesc ? 1 : -1);
+                });
+            }
+            if (dragIndex !== -1) {
+                const draggedDoc = DragManager.docsBeingDragged[0];
+                const originalIndex = docs.findIndex(doc => doc === draggedDoc);
+
+                originalIndex !== -1 && docs.splice(originalIndex, 1);
+                draggedDoc && docs.splice(dragIndex, 0, draggedDoc);
+            }
+            return docs;
+        };
+
         @action
         protected async setCursorPosition(position: [number, number]) {
             let ind;
diff --git a/src/client/views/collections/FlashcardPracticeUI.tsx b/src/client/views/collections/FlashcardPracticeUI.tsx
index 77f1db9ad..c071c5fb8 100644
--- a/src/client/views/collections/FlashcardPracticeUI.tsx
+++ b/src/client/views/collections/FlashcardPracticeUI.tsx
@@ -58,7 +58,7 @@ export class FlashcardPracticeUI extends ObservableReactComponent<PracticeUIProp
     get practiceField() { return this._props.fieldKey + "_practice"; } // prettier-ignore
 
     @computed get filterDoc() { return DocListCast(Doc.MyContextMenuBtns.data).find(doc => doc.title === 'Filter'); } // prettier-ignore
-    @computed get practiceMode() {  return this._props.allChildDocs().some(doc => doc._flashcardType) ? StrCast(this._props.layoutDoc.practiceMode) : ''; } // prettier-ignore
+    @computed get practiceMode() {  return this._props.allChildDocs().some(doc => doc._layout_flashcardType) ? StrCast(this._props.layoutDoc.practiceMode) : ''; } // prettier-ignore
 
     btnHeight = () => NumCast(this.filterDoc?.height) * Math.min(1, this._props.ScreenToLocalBoxXf().Scale);
     btnWidth = () => (!this.filterDoc ? 1 : (this.btnHeight() * NumCast(this.filterDoc._width)) / NumCast(this.filterDoc._height));
@@ -179,7 +179,7 @@ export class FlashcardPracticeUI extends ObservableReactComponent<PracticeUIProp
             </div>
         );
     }
-    tryFilterOut = (doc: Doc) => (this.practiceMode && BoolCast(doc?._flashcardType) && doc[this.practiceField] === practiceVal.CORRECT ? true : false); // show only cards that aren't marked as correct
+    tryFilterOut = (doc: Doc) => (this.practiceMode && doc?._layout_flashcardType && doc[this.practiceField] === practiceVal.CORRECT ? true : false); // show only cards that aren't marked as correct
     render() {
         return (
             <div className="FlashcardPracticeUI">
diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts
index 542417531..b02eabab0 100644
--- a/src/client/views/global/globalScripts.ts
+++ b/src/client/views/global/globalScripts.ts
@@ -40,6 +40,7 @@ import { WebBox } from '../nodes/WebBox';
 import { RichTextMenu } from '../nodes/formattedText/RichTextMenu';
 import { GPTPopup, GPTPopupMode } from '../pdf/GPTPopup/GPTPopup';
 import { OpenWhere } from '../nodes/OpenWhere';
+import { docSortings } from '../collections/CollectionSubView';
 
 // eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function IsNoneSelected() {
@@ -197,43 +198,35 @@ ScriptingGlobals.add(function showFreeform(
             checkResult: (doc: Doc) => BoolCast(doc?._freeform_useClusters, false),
             setDoc: (doc: Doc) => { doc._freeform_useClusters = !doc._freeform_useClusters; },
         }],
-        ['flashcards', {
-            checkResult: (doc: Doc) => BoolCast(Doc.UserDoc().defaultToFlashcards, false),
-            setDoc: (doc: Doc, dv: DocumentView) => { Doc.UserDoc().defaultToFlashcards = !Doc.UserDoc().defaultToFlashcards}, // prettier-ignore
-        }],
         ['time', {
-            checkResult: (doc: Doc) => StrCast(doc?.card_sort) === "time",
-            setDoc: (doc: Doc, dv: DocumentView) => { doc.card_sort === "time" ? doc.card_sort = '' : doc.card_sort = 'time'}, // prettier-ignore
+            checkResult: (doc: Doc) => StrCast(doc?.[Doc.LayoutFieldKey(doc)+"_sort"]) === "time",
+            setDoc: (doc: Doc, dv: DocumentView) => { doc[Doc.LayoutFieldKey(doc)+"_sort"] === "time" ? doc[Doc.LayoutFieldKey(doc)+"_sort"] = '' : doc[Doc.LayoutFieldKey(doc)+"_sort"] = docSortings.Time}, // prettier-ignore
         }],
         ['docType', {
-            checkResult: (doc: Doc) => StrCast(doc?.card_sort) === "type",
-            setDoc: (doc: Doc, dv: DocumentView) => { doc.card_sort === "type" ? doc.card_sort = '' : doc.card_sort = 'type'}, // prettier-ignore
+            checkResult: (doc: Doc) => StrCast(doc?.[Doc.LayoutFieldKey(doc)+"_sort"]) === "type",
+            setDoc: (doc: Doc, dv: DocumentView) => { doc[Doc.LayoutFieldKey(doc)+"_sort"] === "type" ? doc[Doc.LayoutFieldKey(doc)+"_sort"] = '' : doc[Doc.LayoutFieldKey(doc)+"_sort"] = docSortings.Type}, // prettier-ignore
         }],
         ['color', {
-            checkResult: (doc: Doc) => StrCast(doc?.card_sort) === "color",
-            setDoc: (doc: Doc, dv: DocumentView) => { doc.card_sort === "color" ? doc.card_sort = '' : doc.card_sort = 'color'}, // prettier-ignore
+            checkResult: (doc: Doc) => StrCast(doc?.[Doc.LayoutFieldKey(doc)+"_sort"]) === "color",
+            setDoc: (doc: Doc, dv: DocumentView) => { doc?.[Doc.LayoutFieldKey(doc)+"_sort"] === "color" ? doc[Doc.LayoutFieldKey(doc)+"_sort"] = '' : doc[Doc.LayoutFieldKey(doc)+"_sort"] = docSortings.Color}, // prettier-ignore
         }],
         ['tag', {
-            checkResult: (doc: Doc) => StrCast(doc?.card_sort) === "tag",
-            setDoc: (doc: Doc, dv: DocumentView) => { doc.card_sort === "tag" ? doc.card_sort = '' : doc.card_sort = 'tag'}, // prettier-ignore
+            checkResult: (doc: Doc) => StrCast(doc?.[Doc.LayoutFieldKey(doc)+"_sort"]) === "tag",
+            setDoc: (doc: Doc, dv: DocumentView) => { doc[Doc.LayoutFieldKey(doc)+"_sort"] === "tag" ? doc[Doc.LayoutFieldKey(doc)+"_sort"] = '' : doc[Doc.LayoutFieldKey(doc)+"_sort"] = docSortings.Tag}, // prettier-ignore
         }],
         ['up', {
-            checkResult: (doc: Doc) => BoolCast(!doc?.card_sort_isDesc),
-            setDoc: (doc: Doc, dv: DocumentView) => {
-                doc.card_sort_isDesc = false;
-            },
+            checkResult: (doc: Doc) => BoolCast(!doc?.[Doc.LayoutFieldKey(doc)+"_sort_desc"]),
+            setDoc: (doc: Doc, dv: DocumentView) => { doc[Doc.LayoutFieldKey(doc)+"_sort_desc"] = undefined; },
         }],
         ['down', {
-            checkResult: (doc: Doc) => BoolCast(doc?.card_sort_isDesc),
-            setDoc: (doc: Doc, dv: DocumentView) => {
-                doc.card_sort_isDesc = true;
-            },
+            checkResult: (doc: Doc) => BoolCast(doc?.[Doc.LayoutFieldKey(doc)+"_sort_desc"]),
+            setDoc: (doc: Doc, dv: DocumentView) => { doc[Doc.LayoutFieldKey(doc)+"_sort_desc"] = true; },
         }],
         ['toggle-chat', {
             checkResult: (doc: Doc) => GPTPopup.Instance.visible,
             setDoc: (doc: Doc, dv: DocumentView) => {
                 if (GPTPopup.Instance.visible){
-                    doc.card_sort = ''
+                    doc[Doc.LayoutFieldKey(doc)+"_sort"] = '';
                     GPTPopup.Instance.setVisible(false);
    
                 } else {
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index e0c360132..f5291a4c1 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -1,7 +1,7 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Tooltip } from '@mui/material';
 import axios from 'axios';
-import { IReactionDisposer, action, computed, makeObservable, observable, reaction, runInAction, trace } from 'mobx';
+import { IReactionDisposer, action, computed, makeObservable, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import ReactLoading from 'react-loading';
diff --git a/src/client/views/nodes/imageEditor/ImageEditor.tsx b/src/client/views/nodes/imageEditor/ImageEditor.tsx
index 2ae6ee1dd..6b1d05031 100644
--- a/src/client/views/nodes/imageEditor/ImageEditor.tsx
+++ b/src/client/views/nodes/imageEditor/ImageEditor.tsx
@@ -551,8 +551,7 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
         img.src = src;
         if (!currImg.current || !originalImg.current || !imageRootDoc) return undefined;
         try {
-            const res = await createNewImgDoc(img, false);
-            return res;
+            return await createNewImgDoc(img, false);
         } catch (err) {
             console.log(err);
         }
@@ -589,9 +588,7 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
                                 // disable once edited has been clicked (doesn't make sense to change after first edit)
                                 disabled={edited}
                                 checked={isNewCollection}
-                                onChange={() => {
-                                    setIsNewCollection(prev => !prev);
-                                }}
+                                onChange={() => setIsNewCollection(prev => !prev)}
                             />
                         }
                         label="Create New Collection"
@@ -610,49 +607,13 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
         return (
             <div className="sideControlsContainer" style={{ backgroundColor: bgColor }}>
                 <div className="sideControls">
-                    <div className="imageToolsContainer">
-                        {imageEditTools.map(tool => {
-                            return ImageToolButton(tool, tool.type === currTool.type, changeTool);
-                        })}
-                    </div>
+                    <div className="imageToolsContainer">{imageEditTools.map(tool => ImageToolButton(tool, tool.type === currTool.type, changeTool))}</div>
                     {currTool.type == ImageToolType.Cut && (
                         <div className="cutToolsContainer">
-                            <Button
-                                style={{ width: '100%' }}
-                                text="Keep in"
-                                type={Type.TERT}
-                                color={cutType == CutMode.IN ? SettingsManager.userColor : bgColor}
-                                onClick={() => {
-                                    setCutType(CutMode.IN);
-                                }}
-                            />
-                            <Button
-                                style={{ width: '100%' }}
-                                text="Keep out"
-                                type={Type.TERT}
-                                color={cutType == CutMode.OUT ? SettingsManager.userColor : bgColor}
-                                onClick={() => {
-                                    setCutType(CutMode.OUT);
-                                }}
-                            />
-                            <Button
-                                style={{ width: '100%' }}
-                                text="Draw in"
-                                type={Type.TERT}
-                                color={cutType == CutMode.DRAW_IN ? SettingsManager.userColor : bgColor}
-                                onClick={() => {
-                                    setCutType(CutMode.DRAW_IN);
-                                }}
-                            />
-                            <Button
-                                style={{ width: '100%' }}
-                                text="Erase"
-                                type={Type.TERT}
-                                color={cutType == CutMode.ERASE ? SettingsManager.userColor : bgColor}
-                                onClick={() => {
-                                    setCutType(CutMode.ERASE);
-                                }}
-                            />
+                            <Button style={{ width: '100%' }} text="Keep in" type={Type.TERT} color={cutType == CutMode.IN ? SettingsManager.userColor : bgColor} onClick={() => setCutType(CutMode.IN)} />
+                            <Button style={{ width: '100%' }} text="Keep out" type={Type.TERT} color={cutType == CutMode.OUT ? SettingsManager.userColor : bgColor} onClick={() => setCutType(CutMode.OUT)} />
+                            <Button style={{ width: '100%' }} text="Draw in" type={Type.TERT} color={cutType == CutMode.DRAW_IN ? SettingsManager.userColor : bgColor} onClick={() => setCutType(CutMode.DRAW_IN)} />
+                            <Button style={{ width: '100%' }} text="Erase" type={Type.TERT} color={cutType == CutMode.ERASE ? SettingsManager.userColor : bgColor} onClick={() => setCutType(CutMode.ERASE)} />
                         </div>
                     )}
                     <div className="sliderContainer" onPointerDown={e => e.stopPropagation()}>
@@ -669,9 +630,7 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
                                 defaultValue={genFillTool.sliderDefault}
                                 size="small"
                                 valueLabelDisplay="auto"
-                                onChange={(e, val) => {
-                                    setCursorData(prev => ({ ...prev, width: val as number }));
-                                }}
+                                onChange={(e, val) => setCursorData(prev => ({ ...prev, width: val as number }))}
                             />
                         )}
                         {currTool.type === ImageToolType.Cut && (
@@ -687,9 +646,7 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
                                 defaultValue={cutTool.sliderDefault}
                                 size="small"
                                 valueLabelDisplay="auto"
-                                onChange={(e, val) => {
-                                    setCursorData(prev => ({ ...prev, width: val as number }));
-                                }}
+                                onChange={(e, val) => setCursorData(prev => ({ ...prev, width: val as number }))}
                             />
                         )}
                     </div>
@@ -701,9 +658,7 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
                                 e.stopPropagation();
                                 handleUndo();
                             }}
-                            onPointerUp={e => {
-                                e.stopPropagation();
-                            }}
+                            onPointerUp={e => e.stopPropagation()}
                             color={activeColor}
                             tooltip="Undo"
                             icon={<IoMdUndo />}
@@ -714,9 +669,7 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
                                 e.stopPropagation();
                                 handleRedo();
                             }}
-                            onPointerUp={e => {
-                                e.stopPropagation();
-                            }}
+                            onPointerUp={e => e.stopPropagation()}
                             color={activeColor}
                             tooltip="Redo"
                             icon={<IoMdRedo />}
diff --git a/src/client/views/nodes/imageEditor/ImageEditorButtons.tsx b/src/client/views/nodes/imageEditor/ImageEditorButtons.tsx
index de2116253..e810881a5 100644
--- a/src/client/views/nodes/imageEditor/ImageEditorButtons.tsx
+++ b/src/client/views/nodes/imageEditor/ImageEditorButtons.tsx
@@ -55,6 +55,7 @@ export function ImageToolButton(tool: ImageEditTool, isActive: boolean, selectTo
     return (
         <div className="imageEditorButtonContainer">
             <Button
+                key={tool.name}
                 style={{ width: '100%' }}
                 text={tool.name}
                 type={Type.TERT}
-- 
cgit v1.2.3-70-g09d2

