From 4574b7f03ccc85c4bebdbfd9475788456086704f Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 8 Aug 2024 12:27:40 -0400
Subject: many changes to add typing in place of 'any's etc

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

(limited to 'src/client/views/DashboardView.tsx')

diff --git a/src/client/views/DashboardView.tsx b/src/client/views/DashboardView.tsx
index b7383a37e..dcc5442f0 100644
--- a/src/client/views/DashboardView.tsx
+++ b/src/client/views/DashboardView.tsx
@@ -1,5 +1,3 @@
-/* eslint-disable jsx-a11y/no-static-element-interactions */
-/* eslint-disable jsx-a11y/click-events-have-key-events */
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Button, ColorPicker, EditableText, Size, Type } from 'browndash-components';
 import { action, computed, makeObservable, observable } from 'mobx';
@@ -46,7 +44,7 @@ export type DocConfig = {
 // DashboardView is the view with the dashboard previews, rendered when the app first loads
 
 @observer
-export class DashboardView extends ObservableReactComponent<{}> {
+export class DashboardView extends ObservableReactComponent<object> {
     public static _urlState: HistoryUtil.DocUrl;
     public static makeDocumentConfig(document: Doc, panelName?: string, width?: number, keyValue?: boolean) {
         return {
@@ -82,7 +80,7 @@ export class DashboardView extends ObservableReactComponent<{}> {
         });
         return doc;
     }
-    constructor(props: any) {
+    constructor(props: object) {
         super(props);
         makeObservable(this);
     }
-- 
cgit v1.2.3-70-g09d2


From 6bc79c5dfc5f61bf5d1b1544d399703c8006b898 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 26 Aug 2024 10:59:56 -0400
Subject: cleanup of myXXXstuff on dashboards

---
 src/client/views/DashboardView.tsx | 5 +++--
 src/client/views/TagsView.tsx      | 6 +-----
 src/fields/Doc.ts                  | 8 ++++----
 3 files changed, 8 insertions(+), 11 deletions(-)

(limited to 'src/client/views/DashboardView.tsx')

diff --git a/src/client/views/DashboardView.tsx b/src/client/views/DashboardView.tsx
index dcc5442f0..f4fc8ee9f 100644
--- a/src/client/views/DashboardView.tsx
+++ b/src/client/views/DashboardView.tsx
@@ -429,7 +429,8 @@ export class DashboardView extends ObservableReactComponent<object> {
         dashboardDoc.pane_count = 1;
         freeformDoc.embedContainer = dashboardDoc;
         dashboardDoc.myOverlayDocs = new List<Doc>();
-        dashboardDoc.myPublishedDocs = new List<Doc>();
+        dashboardDoc[DocData].myPublishedDocs = new List<Doc>();
+        dashboardDoc[DocData].myTagCollections = new List<Doc>();
 
         Doc.AddDocToList(Doc.MyDashboards, 'data', dashboardDoc);
 
@@ -467,7 +468,7 @@ export class DashboardView extends ObservableReactComponent<object> {
         };
         const myCalendars = DocUtils.AssignScripts(Docs.Create.CalendarCollectionDocument([], reqdOpts));
         // { treeView_ChildDoubleClick: 'openPresentation(documentView.rootDoc)' }
-        dashboardDoc.myCalendars = new PrefetchProxy(myCalendars);
+        dashboardDoc[DocData].myCalendars = new PrefetchProxy(myCalendars);
     }
 
     public static SetupDashboardTrails(dashboardDoc: Doc) {
diff --git a/src/client/views/TagsView.tsx b/src/client/views/TagsView.tsx
index dffd1e096..394162f46 100644
--- a/src/client/views/TagsView.tsx
+++ b/src/client/views/TagsView.tsx
@@ -70,11 +70,7 @@ export class TagItem extends ObservableReactComponent<TagItemProps> {
         newTagCol.title = tag;
         newTagCol.collections = new List<Doc>();
         newTagCol[DocData].docs = new List<Doc>();
-        // If the active Dashboard does not have a tag Doc collection, create it.
-        if (Doc.ActiveDashboard) {
-            if (!Doc.ActiveDashboard.myTagCollections) Doc.ActiveDashboard.myTagCollections = new List<Doc>();
-            Doc.AddDocToList(Doc.ActiveDashboard, 'myTagCollections', newTagCol);
-        }
+        Doc.ActiveDashboard && Doc.AddDocToList(Doc.ActiveDashboard, 'myTagCollections', newTagCol);
 
         return newTagCol;
     };
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index ffb5aab79..e3178f20c 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -258,16 +258,16 @@ export class Doc extends RefField {
     public static set ActiveDashboard(val: Opt<Doc>) {  Doc.UserDoc().activeDashboard = val; } // prettier-ignore
 
     public static IsInMyOverlay(doc: Doc)     { return Doc.MyOverlayDocs.includes(doc); } // prettier-ignore
-    public static AddToMyOverlay(doc: Doc)    { return Doc.ActiveDashboard?.myOverlayDocs ? Doc.AddDocToList(Doc.ActiveDashboard, 'myOverlayDocs', doc) : Doc.AddDocToList(DocCast(Doc.UserDoc().myOverlayDocs), undefined, doc); } // prettier-ignore
-    public static RemFromMyOverlay(doc: Doc)  { return Doc.ActiveDashboard?.myOverlayDocs ? Doc.RemoveDocFromList(Doc.ActiveDashboard,'myOverlayDocs', doc) : Doc.RemoveDocFromList(DocCast(Doc.UserDoc().myOverlayDocs), undefined, doc); } // prettier-ignore
+    public static AddToMyOverlay(doc: Doc)    { return Doc.ActiveDashboard ? Doc.AddDocToList(Doc.ActiveDashboard, 'myOverlayDocs', doc) : Doc.AddDocToList(DocCast(Doc.UserDoc().myOverlayDocs), undefined, doc); } // prettier-ignore
+    public static RemFromMyOverlay(doc: Doc)  { return Doc.ActiveDashboard ? Doc.RemoveDocFromList(Doc.ActiveDashboard,'myOverlayDocs', doc) : Doc.RemoveDocFromList(DocCast(Doc.UserDoc().myOverlayDocs), undefined, doc); } // prettier-ignore
     public static AddToMyPublished(doc: Doc)  {
         doc[DocData].title_custom = true;
         doc[DocData].layout_showTitle = 'title'; 
-        Doc.ActiveDashboard?.myPublishedDocs ? Doc.AddDocToList(Doc.ActiveDashboard, 'myPublishedDocs', doc) : Doc.AddDocToList(DocCast(Doc.UserDoc().myPublishedDocs), undefined, doc); } // prettier-ignore
+        Doc.ActiveDashboard ? Doc.AddDocToList(Doc.ActiveDashboard, 'myPublishedDocs', doc) : Doc.AddDocToList(DocCast(Doc.UserDoc().myPublishedDocs), undefined, doc); } // prettier-ignore
     public static RemFromMyPublished(doc: Doc){ 
         doc[DocData].title_custom = false;
         doc[DocData].layout_showTitle = undefined;
-        Doc.ActiveDashboard?.myPublishedDocs ? Doc.RemoveDocFromList(Doc.ActiveDashboard,'myPublishedDocs', doc) : Doc.RemoveDocFromList(DocCast(Doc.UserDoc().myPublishedDocs), undefined, doc); } // prettier-ignore
+        Doc.ActiveDashboard ? Doc.RemoveDocFromList(Doc.ActiveDashboard,'myPublishedDocs', doc) : Doc.RemoveDocFromList(DocCast(Doc.UserDoc().myPublishedDocs), undefined, doc); } // prettier-ignore
     public static IsComicStyle(doc?: Doc)     { return doc && Doc.ActiveDashboard && !Doc.IsSystem(doc) && Doc.UserDoc().renderStyle === 'comic' ; } // prettier-ignore
 
     constructor(id?: FieldId, forceSave?: boolean) {
-- 
cgit v1.2.3-70-g09d2


From cc3e1bc2c317cf34aba04e4935ae842b16ad4cae Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 26 Aug 2024 11:22:38 -0400
Subject: cleanup of setting myXXstuff on dashboards

---
 src/client/views/DashboardView.tsx                           | 12 +++++-------
 .../collections/collectionFreeForm/FaceCollectionBox.tsx     |  2 +-
 src/client/views/search/FaceRecognitionHandler.tsx           | 12 ++++++------
 3 files changed, 12 insertions(+), 14 deletions(-)

(limited to 'src/client/views/DashboardView.tsx')

diff --git a/src/client/views/DashboardView.tsx b/src/client/views/DashboardView.tsx
index f4fc8ee9f..4616e15e5 100644
--- a/src/client/views/DashboardView.tsx
+++ b/src/client/views/DashboardView.tsx
@@ -426,16 +426,14 @@ export class DashboardView extends ObservableReactComponent<object> {
         const dashboardDoc = DashboardView.StandardCollectionDockingDocument([{ doc: freeformDoc, initialWidth: 600 }], { title: title }, id, 'row');
 
         Doc.AddDocToList(Doc.MyHeaderBar, 'data', freeformDoc, undefined, undefined, true);
+        Doc.AddDocToList(Doc.MyDashboards, 'data', dashboardDoc);
         dashboardDoc.pane_count = 1;
         freeformDoc.embedContainer = dashboardDoc;
         dashboardDoc.myOverlayDocs = new List<Doc>();
         dashboardDoc[DocData].myPublishedDocs = new List<Doc>();
         dashboardDoc[DocData].myTagCollections = new List<Doc>();
-
-        Doc.AddDocToList(Doc.MyDashboards, 'data', dashboardDoc);
-
-        DashboardView.SetupDashboardTrails(dashboardDoc);
-        DashboardView.SetupDashboardCalendars(dashboardDoc);
+        dashboardDoc[DocData].myTrails = DashboardView.SetupDashboardTrails(dashboardDoc);
+        dashboardDoc[DocData].myCalendars = DashboardView.SetupDashboardCalendars(dashboardDoc);
         // open this new dashboard
         Doc.ActiveDashboard = dashboardDoc;
         Doc.ActivePage = 'dashboard';
@@ -468,7 +466,7 @@ export class DashboardView extends ObservableReactComponent<object> {
         };
         const myCalendars = DocUtils.AssignScripts(Docs.Create.CalendarCollectionDocument([], reqdOpts));
         // { treeView_ChildDoubleClick: 'openPresentation(documentView.rootDoc)' }
-        dashboardDoc[DocData].myCalendars = new PrefetchProxy(myCalendars);
+        return new PrefetchProxy(myCalendars);
     }
 
     public static SetupDashboardTrails(dashboardDoc: Doc) {
@@ -514,12 +512,12 @@ export class DashboardView extends ObservableReactComponent<object> {
             layout_explainer: 'All of the trails that you have created will appear here.',
         };
         const myTrails = DocUtils.AssignScripts(Docs.Create.TreeDocument([], reqdOpts), { treeView_ChildDoubleClick: 'openPresentation(documentView.Document)' });
-        dashboardDoc.myTrails = new PrefetchProxy(myTrails);
 
         const contextMenuScripts = [reqdBtnScript.onClick];
         if (Cast(myTrails.contextMenuScripts, listSpec(ScriptField), null)?.length !== contextMenuScripts.length) {
             myTrails.contextMenuScripts = new List<ScriptField>(contextMenuScripts.map(script => ScriptField.MakeFunction(script)!));
         }
+        return new PrefetchProxy(myTrails);
     }
 }
 
diff --git a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx
index de7c2c027..1cc1f59dc 100644
--- a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx
+++ b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx
@@ -164,7 +164,7 @@ export class UniqueFaceView extends ObservableReactComponent<UniqueFaceProps> {
  * This renders the sidebar collection of the unique faces that have been recognized.
  *
  * Since the collection of recognized faces is stored on the active dashboard, this class
- * does not itself store any Docs, but accesses the uniqueFaces field of the current
+ * does not itself store any Docs, but accesses the myUniqueFaces field of the current
  * dashboard.  Each Face collection Doc is rendered using a FaceCollectionDocView which
  * is not a CollectionView or even a DocumentView (but probably should be).
  */
diff --git a/src/client/views/search/FaceRecognitionHandler.tsx b/src/client/views/search/FaceRecognitionHandler.tsx
index f5fc12a8d..a17e4c54a 100644
--- a/src/client/views/search/FaceRecognitionHandler.tsx
+++ b/src/client/views/search/FaceRecognitionHandler.tsx
@@ -20,7 +20,7 @@ import { ImageField } from '../../../fields/URLField';
  *     <image data field>_faceDescriptors - list of all the numerical face representations found in the image.
  *     <image data field>_faces - list of unique face Docs corresponding to recognized faces in the image.
  *
- * unique face Doc's are created for each person identified and are stored in the Dashboard's uniqueFaces field
+ * unique face Doc's are created for each person identified and are stored in the Dashboard's myUniqueFaces field
  *
  * Each unique face Doc represents a unique face and collects all matching face images for that person.  It has these fields:
  *     face_label - a string label for the person that was recognized                                     (TODO: currently it's just a 'face#')
@@ -93,14 +93,14 @@ export class FaceRecognitionHandler {
      * returns a list of all face collection Docs on the current dashboard
      * @returns face collection Doc list
      */
-    public static UniqueFaces = () => DocListCast(Doc.ActiveDashboard?.[DocData].uniqueFaces);
+    public static UniqueFaces = () => DocListCast(Doc.ActiveDashboard?.[DocData].myUniqueFaces);
 
     /**
      * Removes a unique face from the set of recognized unique faces
      * @param faceDoc unique face Doc
      * @returns
      */
-    public static DeleteUniqueFace = (faceDoc: Doc) => Doc.ActiveDashboard && Doc.RemoveDocFromList(Doc.ActiveDashboard[DocData], 'uniqueFaces', faceDoc);
+    public static DeleteUniqueFace = (faceDoc: Doc) => Doc.ActiveDashboard && Doc.RemoveDocFromList(Doc.ActiveDashboard[DocData], 'myUniqueFaces', faceDoc);
 
     /**
      * returns the labels associated with a face collection Doc
@@ -169,8 +169,8 @@ export class FaceRecognitionHandler {
      * @returns a unique face Doc
      */
     private createUniqueFaceDoc = (dashboard: Doc) => {
-        const faceDocNum = NumCast(dashboard.uniqueFaces_count) + 1;
-        dashboard.uniqueFaces_count = faceDocNum; // TODO: improve to a better name
+        const faceDocNum = NumCast(dashboard.myUniqueFaces_count) + 1;
+        dashboard.myUniqueFaces_count = faceDocNum; // TODO: improve to a better name
 
         const uniqueFaceDoc = new Doc();
         uniqueFaceDoc.title = `Face ${faceDocNum}`;
@@ -179,7 +179,7 @@ export class FaceRecognitionHandler {
         uniqueFaceDoc.face_images = new List<Doc>();
         uniqueFaceDoc.face_descriptors = new List<List<number>>();
 
-        Doc.ActiveDashboard && Doc.AddDocToList(Doc.ActiveDashboard[DocData], 'uniqueFaces', uniqueFaceDoc);
+        Doc.ActiveDashboard && Doc.AddDocToList(Doc.ActiveDashboard[DocData], 'myUniqueFaces', uniqueFaceDoc);
         return uniqueFaceDoc;
     };
 
-- 
cgit v1.2.3-70-g09d2


From 2e345c1ebd498e3f7e088e6fc3e1ca17082b23c1 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 26 Aug 2024 13:53:26 -0400
Subject: converted unique faces to be a Doc type similar to a collection.

---
 src/client/documents/DocumentTypes.ts              |   1 +
 src/client/documents/Documents.ts                  |   4 +
 src/client/views/DashboardView.tsx                 |   1 +
 src/client/views/Main.tsx                          |   3 +-
 .../views/collections/CollectionStackingView.tsx   |   3 +-
 src/client/views/collections/CollectionSubView.tsx |   1 +
 .../collectionFreeForm/FaceCollectionBox.scss      |  24 ++++-
 .../collectionFreeForm/FaceCollectionBox.tsx       | 113 +++++++++++++--------
 src/client/views/search/FaceRecognitionHandler.tsx |  34 +++++--
 9 files changed, 123 insertions(+), 61 deletions(-)

(limited to 'src/client/views/DashboardView.tsx')

diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts
index 49df943d8..b055546fc 100644
--- a/src/client/documents/DocumentTypes.ts
+++ b/src/client/documents/DocumentTypes.ts
@@ -18,6 +18,7 @@ export enum DocumentType {
     SEARCH = 'search', // search query
     IMAGEGROUPER = 'imagegrouper',
     FACECOLLECTION = 'facecollection',
+    UFACE = 'uniqueface', // unique face collection doc
     LABEL = 'label', // simple text label
     BUTTON = 'button', // onClick button
     WEBCAM = 'webcam', // webcam
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index c0e4e961c..c2211fb80 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -795,6 +795,10 @@ export namespace Docs {
             return InstanceFromProto(Prototypes.get(DocumentType.FACECOLLECTION), undefined, options);
         }
 
+        export function UniqeFaceDocument(options: DocumentOptions = {}) {
+            return InstanceFromProto(Prototypes.get(DocumentType.UFACE), undefined, options);
+        }
+
         export function LoadingDocument(file: File | string, options: DocumentOptions) {
             return InstanceFromProto(Prototypes.get(DocumentType.LOADING), undefined, { _height: 150, _width: 200, title: typeof file === 'string' ? file : file.name, ...options }, undefined, '');
         }
diff --git a/src/client/views/DashboardView.tsx b/src/client/views/DashboardView.tsx
index 4616e15e5..33e905a54 100644
--- a/src/client/views/DashboardView.tsx
+++ b/src/client/views/DashboardView.tsx
@@ -432,6 +432,7 @@ export class DashboardView extends ObservableReactComponent<object> {
         dashboardDoc.myOverlayDocs = new List<Doc>();
         dashboardDoc[DocData].myPublishedDocs = new List<Doc>();
         dashboardDoc[DocData].myTagCollections = new List<Doc>();
+        dashboardDoc[DocData].myUniqueFaces = new List<Doc>();
         dashboardDoc[DocData].myTrails = DashboardView.SetupDashboardTrails(dashboardDoc);
         dashboardDoc[DocData].myCalendars = DashboardView.SetupDashboardCalendars(dashboardDoc);
         // open this new dashboard
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index 023324881..f7cd0e925 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -63,7 +63,7 @@ import { PresBox, PresElementBox } from './nodes/trails';
 import { SearchBox } from './search/SearchBox';
 import { ImageLabelBox } from './collections/collectionFreeForm/ImageLabelBox';
 import { FaceRecognitionHandler } from './search/FaceRecognitionHandler';
-import { FaceCollectionBox } from './collections/collectionFreeForm/FaceCollectionBox';
+import { FaceCollectionBox, UniqueFaceBox } from './collections/collectionFreeForm/FaceCollectionBox';
 import { Node } from 'prosemirror-model';
 import { EditorView } from 'prosemirror-view';
 
@@ -140,6 +140,7 @@ FieldLoader.ServerLoadStatus = { requested: 0, retrieved: 0, message: 'cache' };
             SearchBox,
             ImageLabelBox,
             FaceCollectionBox,
+            UniqueFaceBox,
             FunctionPlotBox,
             InkingStroke,
             LinkBox,
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 3f8aee792..6402ef16c 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -98,7 +98,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
     }
     // are we stacking or masonry?
     @computed get isStackingView() {
-        return (this._props.type_collection ?? this.layoutDoc._type_collection) === CollectionViewType.Stacking;
+        return (this._props.type_collection ?? this.layoutDoc._type_collection) !== CollectionViewType.Masonry;
     }
     // this is the number of StackingViewFieldColumns that we have
     @computed get numGroupColumns() {
@@ -355,6 +355,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
                 childFilters={this.childDocFilters}
                 hideDecorationTitle={this._props.childHideDecorationTitle}
                 hideResizeHandles={this._props.childHideResizeHandles}
+                hideDecorations={this._props.childHideDecorations}
                 childFiltersByRanges={this.childDocRangeFilters}
                 searchFilterDocs={this.searchFilterDocs}
                 xPadding={NumCast(this.layoutDoc._childXPadding, this._props.childXPadding)}
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 5782d407e..6aca8f2ca 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -45,6 +45,7 @@ export interface CollectionViewProps extends React.PropsWithChildren<FieldViewPr
     childLayoutTemplate?: () => Doc | undefined; // specify a layout Doc template to use for children of the collection
     childHideDecorationTitle?: boolean;
     childHideResizeHandles?: boolean;
+    childHideDecorations?: boolean;
     childDragAction?: dropActionType;
     childXPadding?: number;
     childYPadding?: number;
diff --git a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.scss b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.scss
index 480d109c8..86120f966 100644
--- a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.scss
+++ b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.scss
@@ -1,10 +1,11 @@
 .face-document-item {
-    background: #555555;
     margin-top: 10px;
     margin-bottom: 10px;
     padding: 10px;
-    border-radius: 10px;
+    border-radius: inherit;
     position: relative;
+    width: 100%;
+    height: 100%;
 
     h1 {
         color: white;
@@ -14,14 +15,31 @@
 
     .face-collection-buttons {
         position: absolute;
-        top: 10px;
+        top: 0px;
         right: 10px;
     }
+    .face-collection-toggle {
+        position: absolute;
+        top: 0px;
+        left: 10px;
+    }
+    .face-document-top {
+        position: absolute;
+        top: 0;
+        margin: auto;
+        width: 100%;
+        left: 0;
+    }
 
     .face-document-image-container {
         display: flex;
         justify-content: center;
         flex-wrap: wrap;
+        overflow-x: hidden;
+        overflow-y: auto;
+        position: absolute;
+        top: 75px;
+        height: calc(100% - 75px);
 
         .image-wrapper {
             position: relative;
diff --git a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx
index 1cc1f59dc..6a0d51e32 100644
--- a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx
+++ b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx
@@ -6,23 +6,21 @@ import 'ldrs/ring';
 import { action, makeObservable, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import React from 'react';
-import { setupMoveUpEvents } from '../../../../ClientUtils';
-import { Utils, emptyFunction } from '../../../../Utils';
+import { lightOrDark, returnTrue, setupMoveUpEvents } from '../../../../ClientUtils';
+import { emptyFunction } from '../../../../Utils';
 import { Doc, Opt } from '../../../../fields/Doc';
-import { Id } from '../../../../fields/FieldSymbols';
 import { List } from '../../../../fields/List';
-import { ImageCast } from '../../../../fields/Types';
+import { ImageCast, StrCast } from '../../../../fields/Types';
 import { DocumentType } from '../../../documents/DocumentTypes';
 import { Docs } from '../../../documents/Documents';
 import { DragManager } from '../../../util/DragManager';
 import { dropActionType } from '../../../util/DropActionTypes';
-import { SnappingManager } from '../../../util/SnappingManager';
 import { undoable } from '../../../util/UndoManager';
 import { ViewBoxBaseComponent } from '../../DocComponent';
-import { ObservableReactComponent } from '../../ObservableReactComponent';
 import { DocumentView } from '../../nodes/DocumentView';
 import { FieldView, FieldViewProps } from '../../nodes/FieldView';
 import { FaceRecognitionHandler } from '../../search/FaceRecognitionHandler';
+import { CollectionStackingView } from '../CollectionStackingView';
 import './FaceCollectionBox.scss';
 import { MarqueeOptionsMenu } from './MarqueeOptionsMenu';
 
@@ -31,31 +29,29 @@ import { MarqueeOptionsMenu } from './MarqueeOptionsMenu';
  * unique face in turn displays the set of images that correspond to the face.
  */
 
-interface UniqueFaceProps {
-    faceDoc: Doc;
-}
-
 /**
- * React component for rendering a unique face and its collection of image Docs.
+ * Viewer for unique face Doc collections.
  *
  * This both displays a collection of images corresponding tp a unique face, and
  * allows for editing the face collection by removing an image, or drag-and-dropping
  * an image that was not recognized.
  */
 @observer
-export class UniqueFaceView extends ObservableReactComponent<UniqueFaceProps> {
+export class UniqueFaceBox extends ViewBoxBaseComponent<FieldViewProps>() {
+    public static LayoutString(fieldKey: string) {
+        return FieldView.LayoutString(UniqueFaceBox, fieldKey);
+    }
     private _dropDisposer?: DragManager.DragDropDisposer;
+    private _oldWheel: HTMLElement | null = null;
 
-    constructor(props: UniqueFaceProps) {
+    constructor(props: FieldViewProps) {
         super(props);
         makeObservable(this);
     }
 
-    @observable _displayImages: boolean = true;
-
     protected createDropTarget = (ele: HTMLDivElement) => {
         this._dropDisposer?.();
-        ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this._props.faceDoc));
+        ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this._props.Document));
     };
 
     protected onInternalDrop(e: Event, de: DragManager.DropEvent): boolean {
@@ -63,12 +59,12 @@ export class UniqueFaceView extends ObservableReactComponent<UniqueFaceProps> {
             ?.filter(doc => doc.type === DocumentType.IMG)
             .forEach(imgDoc => {
                 // If the current Face Document has no faces, and the doc has more than one face descriptor, don't let the user add the document first.  Or should we just use the first face ?
-                if (FaceRecognitionHandler.UniqueFaceDescriptors(this._props.faceDoc).length === 0 && FaceRecognitionHandler.ImageDocFaceDescriptors(imgDoc).length > 1) {
+                if (FaceRecognitionHandler.UniqueFaceDescriptors(this._props.Document).length === 0 && FaceRecognitionHandler.ImageDocFaceDescriptors(imgDoc).length > 1) {
                     alert('Cannot add a document with multiple faces as the first item!');
                 } else {
                     // Loop through the documents' face descriptors and choose the face in the iage with the smallest distance (most similar to the face colleciton)
-                    const faceDescriptorsAsFloat32Array = FaceRecognitionHandler.UniqueFaceDescriptors(this._props.faceDoc).map(fd => new Float32Array(Array.from(fd)));
-                    const labeledFaceDescriptor = new faceapi.LabeledFaceDescriptors(FaceRecognitionHandler.UniqueFaceLabel(this._props.faceDoc), faceDescriptorsAsFloat32Array);
+                    const faceDescriptorsAsFloat32Array = FaceRecognitionHandler.UniqueFaceDescriptors(this._props.Document).map(fd => new Float32Array(Array.from(fd)));
+                    const labeledFaceDescriptor = new faceapi.LabeledFaceDescriptors(FaceRecognitionHandler.UniqueFaceLabel(this._props.Document), faceDescriptorsAsFloat32Array);
                     const faceMatcher = new FaceMatcher([labeledFaceDescriptor], 1);
                     const { face_match } = FaceRecognitionHandler.ImageDocFaceDescriptors(imgDoc).reduce(
                         (prev, face) => {
@@ -80,11 +76,12 @@ export class UniqueFaceView extends ObservableReactComponent<UniqueFaceProps> {
 
                     // assign the face in the image that's closest to the face collection to be the face that's assigned to the collection
                     if (face_match) {
-                        FaceRecognitionHandler.UniqueFaceAddFaceImage(imgDoc, face_match, this._props.faceDoc);
+                        FaceRecognitionHandler.UniqueFaceAddFaceImage(imgDoc, face_match, this._props.Document);
                     }
                 }
             });
-        return false;
+        e.stopPropagation();
+        return true;
     }
 
     /**
@@ -92,14 +89,15 @@ export class UniqueFaceView extends ObservableReactComponent<UniqueFaceProps> {
      */
     @action
     onDisplayClick() {
-        this._displayImages = !this._displayImages;
+        this._props.Document._displayImages = !this.props.Document._displayImages;
+        this._props.Document.height = this._props.Document._displayImages ? 400 : 100;
     }
 
     /**
      * Removes a unique face Doc from the colelction of unique faces.
      */
     deleteUniqueFace = undoable(() => {
-        FaceRecognitionHandler.DeleteUniqueFace(this._props.faceDoc);
+        FaceRecognitionHandler.DeleteUniqueFace(this._props.Document);
     }, 'delete face');
 
     /**
@@ -107,9 +105,11 @@ export class UniqueFaceView extends ObservableReactComponent<UniqueFaceProps> {
      * @param imgDoc - image Doc to remove
      */
     removeFaceImageFromUniqueFace = undoable((imgDoc: Doc) => {
-        FaceRecognitionHandler.UniqueFaceRemoveFaceImage(imgDoc, this._props.faceDoc);
+        FaceRecognitionHandler.UniqueFaceRemoveFaceImage(imgDoc, this._props.Document);
     }, 'remove doc from face');
 
+    onPassiveWheel = (e: WheelEvent) => e.stopPropagation();
+
     render() {
         return (
             <div className="face-document-item" ref={ele => this.createDropTarget(ele!)}>
@@ -117,23 +117,35 @@ export class UniqueFaceView extends ObservableReactComponent<UniqueFaceProps> {
                     <IconButton tooltip="Delete Face From Collection" onPointerDown={this.deleteUniqueFace} icon={'x'} style={{ width: '4px' }} size={Size.XSMALL} />
                 </div>
                 <div className="face-document-top">
-                    <h1>{FaceRecognitionHandler.UniqueFaceLabel(this._props.faceDoc)}</h1>
+                    <h1 style={{ color: lightOrDark(StrCast(this._props.Document.backgroundColor)) }}>{FaceRecognitionHandler.UniqueFaceLabel(this._props.Document)}</h1>
                 </div>
-                <IconButton
-                    tooltip="See image information"
-                    onPointerDown={() => this.onDisplayClick()}
-                    icon={<FontAwesomeIcon icon={this._displayImages ? 'caret-up' : 'caret-down'} />}
-                    color={MarqueeOptionsMenu.Instance.userColor}
-                    style={{ width: '19px' }}
-                />
-                {this._displayImages ? (
-                    <div className="face-document-image-container">
-                        {FaceRecognitionHandler.UniqueFaceImages(this._props.faceDoc).map(doc => {
+                <div className="face-collection-toggle">
+                    <IconButton
+                        tooltip="See image information"
+                        onPointerDown={() => this.onDisplayClick()}
+                        icon={<FontAwesomeIcon icon={this._props.Document._displayImages ? 'caret-up' : 'caret-down'} />}
+                        color={MarqueeOptionsMenu.Instance.userColor}
+                        style={{ width: '19px' }}
+                    />
+                </div>
+                {this.props.Document._displayImages ? (
+                    <div
+                        className="face-document-image-container"
+                        style={{
+                            pointerEvents: this._props.isContentActive() ? undefined : 'none',
+                        }}
+                        ref={ele => {
+                            this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel);
+                            this._oldWheel = ele;
+                            // prevent wheel events from passively propagating up through containers and prevents containers from preventDefault which would block scrolling
+                            ele?.addEventListener('wheel', this.onPassiveWheel, { passive: false });
+                        }}>
+                        {FaceRecognitionHandler.UniqueFaceImages(this._props.Document).map((doc, i) => {
                             const [name, type] = ImageCast(doc[Doc.LayoutFieldKey(doc)]).url.href.split('.');
                             return (
                                 <div
                                     className="image-wrapper"
-                                    key={Utils.GenerateGuid()}
+                                    key={i}
                                     onPointerDown={e =>
                                         setupMoveUpEvents(
                                             this,
@@ -165,8 +177,8 @@ export class UniqueFaceView extends ObservableReactComponent<UniqueFaceProps> {
  *
  * Since the collection of recognized faces is stored on the active dashboard, this class
  * does not itself store any Docs, but accesses the myUniqueFaces field of the current
- * dashboard.  Each Face collection Doc is rendered using a FaceCollectionDocView which
- * is not a CollectionView or even a DocumentView (but probably should be).
+ * dashboard.  (This should probably go away as Doc type in favor of it just being a
+ * stacking collection of uniqueFace docs)
  */
 @observer
 export class FaceCollectionBox extends ViewBoxBaseComponent<FieldViewProps>() {
@@ -179,18 +191,29 @@ export class FaceCollectionBox extends ViewBoxBaseComponent<FieldViewProps>() {
         makeObservable(this);
     }
 
+    moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean): boolean => !!(this._props.removeDocument?.(doc) && addDocument?.(doc));
+
     render() {
-        return (
-            <div className="searchBox-container" style={{ pointerEvents: 'all', color: SnappingManager.userColor, background: SnappingManager.userBackgroundColor }}>
-                {FaceRecognitionHandler.UniqueFaces().map(doc => (
-                    <UniqueFaceView key={doc[Id]} faceDoc={doc} />
-                ))}
-            </div>
+        return !Doc.ActiveDashboard ? null : (
+            <CollectionStackingView
+                {...this._props} //
+                Document={Doc.ActiveDashboard}
+                fieldKey="myUniqueFaces"
+                moveDocument={this.moveDocument}
+                isContentActive={returnTrue}
+                isAnyChildContentActive={returnTrue}
+                childHideDecorations={true}
+            />
         );
     }
 }
 
 Docs.Prototypes.TemplateMap.set(DocumentType.FACECOLLECTION, {
     layout: { view: FaceCollectionBox, dataField: 'data' },
-    options: { acl: '', _width: 400 },
+    options: { acl: '', _width: 400, dropAction: dropActionType.embed },
+});
+
+Docs.Prototypes.TemplateMap.set(DocumentType.UFACE, {
+    layout: { view: UniqueFaceBox, dataField: 'face_images' },
+    options: { acl: '', _width: 400, _height: 400 },
 });
diff --git a/src/client/views/search/FaceRecognitionHandler.tsx b/src/client/views/search/FaceRecognitionHandler.tsx
index a17e4c54a..1ab084eaa 100644
--- a/src/client/views/search/FaceRecognitionHandler.tsx
+++ b/src/client/views/search/FaceRecognitionHandler.tsx
@@ -8,6 +8,8 @@ import { Cast, ImageCast, NumCast, StrCast } from '../../../fields/Types';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { DocumentManager } from '../../util/DocumentManager';
 import { ImageField } from '../../../fields/URLField';
+import { UniqueFaceBox } from '../collections/collectionFreeForm/FaceCollectionBox';
+import { Docs } from '../../documents/Documents';
 
 /**
  * A singleton class that handles face recognition and manages face Doc collections for each face found.
@@ -169,15 +171,25 @@ export class FaceRecognitionHandler {
      * @returns a unique face Doc
      */
     private createUniqueFaceDoc = (dashboard: Doc) => {
-        const faceDocNum = NumCast(dashboard.myUniqueFaces_count) + 1;
-        dashboard.myUniqueFaces_count = faceDocNum; // TODO: improve to a better name
-
-        const uniqueFaceDoc = new Doc();
-        uniqueFaceDoc.title = `Face ${faceDocNum}`;
-        uniqueFaceDoc.face = ''; // just to make prettyprinting look better
-        uniqueFaceDoc.face_label = `Face${faceDocNum}`;
-        uniqueFaceDoc.face_images = new List<Doc>();
-        uniqueFaceDoc.face_descriptors = new List<List<number>>();
+        const faceDocNum = NumCast(dashboard[DocData].myUniqueFaces_count) + 1;
+        dashboard[DocData].myUniqueFaces_count = faceDocNum; // TODO: improve to a better name
+
+        const uniqueFaceDoc = Docs.Create.UniqeFaceDocument({
+            title: `Face ${faceDocNum}`,
+            _layout_reflowHorizontal: true,
+            _layout_reflowVertical: true,
+            _layout_nativeDimEditable: true,
+            _layout_borderRounding: '20px',
+            backgroundColor: '#555555',
+            _width: 400,
+            _height: 400,
+        });
+        const uface = uniqueFaceDoc[DocData];
+        uface.face = ''; // just to make prettyprinting look better
+        uface.face_label = `Face${faceDocNum}`;
+        uface.face_images = new List<Doc>();
+        uface.face_descriptors = new List<List<number>>();
+        Doc.SetContainer(uniqueFaceDoc, Doc.MyFaceCollection);
 
         Doc.ActiveDashboard && Doc.AddDocToList(Doc.ActiveDashboard[DocData], 'myUniqueFaces', uniqueFaceDoc);
         return uniqueFaceDoc;
@@ -222,8 +234,8 @@ export class FaceRecognitionHandler {
             setTimeout(() => this.classifyFacesInImage(imgDoc), 1000);
         } else {
             const imgUrl = ImageCast(imgDoc[Doc.LayoutFieldKey(imgDoc)]);
-            if (imgUrl && !DocListCast(Doc.MyFaceCollection.examinedFaceDocs).includes(imgDoc)) { // only examine Docs that have an image and that haven't already been examined.
-                Doc.AddDocToList(Doc.MyFaceCollection, 'examinedFaceDocs', imgDoc);
+            if (imgUrl && !DocListCast(Doc.MyFaceCollection.examinedFaceDocs).includes(imgDoc[DocData])) { // only examine Docs that have an image and that haven't already been examined.
+                Doc.AddDocToList(Doc.MyFaceCollection, 'examinedFaceDocs', imgDoc[DocData]);
                 FaceRecognitionHandler.initImageDocFaceDescriptors(imgDoc);
                 FaceRecognitionHandler.loadImage(imgUrl).then( // load image and analyze faces
                 img => faceapi.detectAllFaces(img).withFaceLandmarks().withFaceDescriptors()
-- 
cgit v1.2.3-70-g09d2

