From b9f15c10e4cfa1288e176cbd1d312c628c5998ad Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 10 Sep 2024 14:54:44 -0400
Subject: moved TagsView up into DocumentView so that one row of tags will be
 visible. fixed setting pixel size of images to resize annotations so they
 don't appear to change. added vert/horiz centering for stacking views.  fixed
 pres box to not have scroll bars.  fixed resizing properties panel.

---
 src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx | 1 +
 1 file changed, 1 insertion(+)

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

diff --git a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx
index 717081666..f9f6c81ab 100644
--- a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx
+++ b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx
@@ -264,6 +264,7 @@ export class FaceCollectionBox extends ViewBoxBaseComponent<FieldViewProps>() {
                     isContentActive={returnTrue}
                     isAnyChildContentActive={returnTrue}
                     childHideDecorations={true}
+                    dontCenter="y"
                 />
             </div>
         );
-- 
cgit v1.2.3-70-g09d2


From 623da0b4eec34fbd238cc26a5e0c105426a6711e Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 12 Sep 2024 09:50:31 -0400
Subject: added tags for multi-selections.

---
 src/client/views/DocumentButtonBar.tsx             | 10 +++-
 src/client/views/DocumentDecorations.scss          |  7 +++
 src/client/views/DocumentDecorations.tsx           | 11 +++-
 src/client/views/StyleProvider.tsx                 |  2 +-
 src/client/views/TagsView.tsx                      | 65 ++++++++++++++--------
 .../collectionFreeForm/FaceCollectionBox.tsx       |  2 +-
 6 files changed, 69 insertions(+), 28 deletions(-)

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

diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index 437ef045f..785e69f84 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -267,7 +267,15 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (
     get keywordButton() {
         return !DocumentView.Selected().length ? null : (
             <Tooltip title={<div className="dash-keyword-button">Open keyword menu</div>}>
-                <div className="documentButtonBar-icon" style={{ color: 'white' }} onClick={() => DocumentView.Selected().map(dv => (dv.dataDoc.showTags = !dv.dataDoc.showTags))}>
+                <div
+                    className="documentButtonBar-icon"
+                    style={{ color: 'white' }}
+                    onClick={() => {
+                        const showing = DocumentView.Selected().some(dv => dv.dataDoc.showTags);
+                        DocumentView.Selected().forEach(dv => {
+                            dv.dataDoc.showTags = !showing;
+                        });
+                    }}>
                     <FontAwesomeIcon className="documentdecorations-icon" icon="tag" />
                 </div>
             </Tooltip>
diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss
index 67e1054c3..346df10d5 100644
--- a/src/client/views/DocumentDecorations.scss
+++ b/src/client/views/DocumentDecorations.scss
@@ -38,6 +38,13 @@ $resizeHandler: 8px;
         background: green;
         border-radius: 50%;
     }
+    .documentDecorations-tagsView {
+        position: absolute;
+        height: 100%;
+        pointer-events: all;
+        border-radius: 50%;
+        color: black;
+    }
 }
 
 .documentDecorations-container {
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index da35459bb..118a5bd3d 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -35,6 +35,7 @@ import { DocumentView } from './nodes/DocumentView';
 import { ImageBox } from './nodes/ImageBox';
 import { OpenWhere, OpenWhereMod } from './nodes/OpenWhere';
 import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
+import { TagsView } from './TagsView';
 
 interface DocumentDecorationsProps {
     PanelWidth: number;
@@ -835,12 +836,20 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
                                 <div
                                     className="link-button-container"
                                     style={{
-                                        top: `${doc[DocData].showTags ? 4 + seldocview.TagPanelHeight : 4}px`,
+                                        top: DocumentView.Selected().length > 1 ? 0 : `${doc[DocData].showTags ? 4 + seldocview.TagPanelHeight : 4}px`,
                                         transform: `translate(${-this._resizeBorderWidth / 2 + 10}px, ${this._resizeBorderWidth + bounds.b - bounds.y + this._titleHeight}px) `,
                                     }}>
                                     <DocumentButtonBar views={() => DocumentView.Selected()} />
                                 </div>
                             )}
+                            <div
+                                className="documentDecorations-tagsView"
+                                style={{
+                                    top: `${doc[DocData].showTags ? 4 + seldocview.TagPanelHeight : 4}px`,
+                                    transform: `translate(${-this._resizeBorderWidth / 2 + 10}px, ${this._resizeBorderWidth + bounds.b - bounds.y + this._titleHeight}px) `,
+                                }}>
+                                {DocumentView.Selected().length > 1 ? <TagsView Views={DocumentView.Selected()} /> : null}
+                            </div>
                         </div>
 
                         {useRotation && (
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index 513953d17..262f888fb 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -365,7 +365,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
                     </Tooltip>
                 );
             };
-            const tags = () => props?.DocumentView?.() ? <TagsView View={props.DocumentView()}/> :  null;
+            const tags = () => props?.DocumentView?.() ? <TagsView Views={[props.DocumentView()]}/> :  null;
             return (
                 <>
                     {paint()}
diff --git a/src/client/views/TagsView.tsx b/src/client/views/TagsView.tsx
index 8047363d9..ae9d42749 100644
--- a/src/client/views/TagsView.tsx
+++ b/src/client/views/TagsView.tsx
@@ -33,7 +33,7 @@ import { FaceRecognitionHandler } from './search/FaceRecognitionHandler';
  */
 
 interface TagItemProps {
-    doc: Doc;
+    docs: Doc[];
     tag: string;
     tagDoc: Opt<Doc>;
     showRemoveUI: boolean;
@@ -207,8 +207,12 @@ export class TagItem extends ObservableReactComponent<TagItemProps> {
         e.preventDefault();
     };
 
+    @computed get doc() {
+        return this._props.docs.lastElement();
+    }
+
     render() {
-        this._props.tagDoc && setTimeout(() => TagItem.addTagToDoc(this._props.doc, this._props.tag)); // bcz: hack to make sure that Docs are added to their tag Doc collection since metadata can get set anywhere without a guard triggering an add to the collection
+        this._props.tagDoc && setTimeout(() => this._props.docs.forEach(doc => TagItem.addTagToDoc(doc, this._props.tag))); // bcz: hack to make sure that Docs are added to their tag Doc collection since metadata can get set anywhere without a guard triggering an add to the collection
         const tag = this._props.tag.replace(/^#/, '');
         const metadata = tag.startsWith('@') ? tag.replace(/^@/, '') : '';
         return (
@@ -216,16 +220,16 @@ export class TagItem extends ObservableReactComponent<TagItemProps> {
                 {metadata ? (
                     <span>
                         <b style={{ fontSize: 'smaller' }}>{tag}&nbsp;</b>
-                        {typeof this._props.doc[metadata] === 'boolean' ? (
+                        {typeof this.doc[metadata] === 'boolean' ? (
                             <input
                                 type="checkbox"
                                 onClick={e => e.stopPropagation()}
                                 onPointerDown={e => e.stopPropagation()}
-                                onChange={undoable(e => (this._props.doc[metadata] = !this._props.doc[metadata]), 'metadata toggle')}
-                                checked={this._props.doc[metadata] as boolean}
+                                onChange={undoable(e => (this.doc[metadata] = !this.doc[metadata]), 'metadata toggle')}
+                                checked={this.doc[metadata] as boolean}
                             />
                         ) : (
-                            Field.toString(this._props.doc[metadata])
+                            Field.toString(this.doc[metadata])
                         )}
                     </span>
                 ) : (
@@ -234,7 +238,7 @@ export class TagItem extends ObservableReactComponent<TagItemProps> {
                 {this.props.showRemoveUI && this._props.tagDoc && (
                     <IconButton
                         tooltip="Remove tag"
-                        onPointerDown={undoable(() => TagItem.removeTagFromDoc(this._props.doc, this._props.tag, this._props.tagDoc), `remove tag ${this._props.tag}`)}
+                        onPointerDown={undoable(() => this._props.docs.forEach(doc => TagItem.removeTagFromDoc(doc, this._props.tag, this._props.tagDoc)), `remove tag ${this._props.tag}`)}
                         icon={<FontAwesomeIcon icon="times" size="sm" />}
                         style={{ width: '8px', height: '8px', marginLeft: '10px' }}
                     />
@@ -245,7 +249,7 @@ export class TagItem extends ObservableReactComponent<TagItemProps> {
 }
 
 interface TagViewProps {
-    View: DocumentView;
+    Views: DocumentView[];
 }
 
 /**
@@ -261,12 +265,12 @@ export class TagsView extends ObservableReactComponent<TagViewProps> {
 
     @observable _panelHeightDirty = 0;
     @observable _currentInput = '';
-    @observable _isEditing = !StrListCast(this._props.View.dataDoc.tags).length;
+    @observable _isEditing = !StrListCast(this.View.dataDoc.tags).length;
     _heightDisposer: IReactionDisposer | undefined;
 
     componentDidMount() {
         this._heightDisposer = reaction(
-            () => this._props.View.screenToContentsTransform(),
+            () => this.View.screenToContentsTransform(),
             xf => {
                 this._panelHeightDirty = this._panelHeightDirty + 1;
             }
@@ -276,11 +280,15 @@ export class TagsView extends ObservableReactComponent<TagViewProps> {
         this._heightDisposer?.();
     }
 
+    @computed get View() {
+        return this._props.Views.lastElement();
+    }
+
     @computed get currentScale() {
-        return Math.max(1, 1 / this._props.View.screenToLocalScale());
+        return this._props.Views.length > 1 ? 1 : Math.max(1, 1 / this.View.screenToLocalScale());
     }
     @computed get isEditing() {
-        return this._isEditing && DocumentView.SelectedDocs().includes(this._props.View.Document);
+        return this._isEditing && (this._props.Views.length > 1 || DocumentView.SelectedDocs().includes(this.View.Document));
     }
 
     /**
@@ -290,7 +298,7 @@ export class TagsView extends ObservableReactComponent<TagViewProps> {
     @action
     setToEditing = (editing = true) => {
         this._isEditing = editing;
-        editing && this._props.View.select(false);
+        editing && this._props.Views.length === 1 && this.View.select(false);
     };
 
     /**
@@ -303,8 +311,10 @@ export class TagsView extends ObservableReactComponent<TagViewProps> {
         action((tag: string) => {
             const submittedLabel = tag.trim().replace(/^#/, '').split(':');
             if (submittedLabel[0]) {
-                TagItem.addTagToDoc(this._props.View.Document, '#' + submittedLabel[0]);
-                if (submittedLabel.length > 1) Doc.SetField(this._props.View.Document, submittedLabel[0].replace(/^@/, ''), ':' + submittedLabel[1]);
+                this._props.Views.forEach(view => {
+                    TagItem.addTagToDoc(view.Document, '#' + submittedLabel[0]);
+                    if (submittedLabel.length > 1) Doc.SetField(view.Document, submittedLabel[0].replace(/^@/, ''), ':' + submittedLabel[1]);
+                });
             }
             this._currentInput = ''; // Clear the input box
         }),
@@ -316,20 +326,20 @@ export class TagsView extends ObservableReactComponent<TagViewProps> {
      * When the dropdown is clicked, this will toggle an extended UI that allows additional tags to be added/removed.
      */
     render() {
-        const tagsList = new Set<string>(StrListCast(this._props.View.dataDoc.tags));
-        const chatTagsList = new Set<string>(StrListCast(this._props.View.dataDoc.tags_chat));
+        const tagsList = new Set<string>(StrListCast(this.View.dataDoc.tags));
+        const chatTagsList = new Set<string>(StrListCast(this.View.dataDoc.tags_chat));
         const facesList = new Set<string>(
-            DocListCast(this._props.View.dataDoc[Doc.LayoutFieldKey(this._props.View.Document) + '_annotations'])
-                .concat(this._props.View.Document)
+            DocListCast(this.View.dataDoc[Doc.LayoutFieldKey(this.View.Document) + '_annotations'])
+                .concat(this.View.Document)
                 .filter(d => d.face)
                 .map(doc => StrCast(DocCast(doc.face)?.title))
         );
         this._panelHeightDirty;
 
-        return !this._props.View.Document.showTags ? null : (
+        return !this.View.Document.showTags && this._props.Views.length === 1 ? null : (
             <div
                 className="tagsView-container"
-                ref={r => r && new ResizeObserver(action(() => (this._props.View.TagPanelHeight = Math.max(0, (r?.getBoundingClientRect().height ?? 0) - this.InsetDist)))).observe(r)}
+                ref={r => r && new ResizeObserver(action(() => this._props.Views.length === 1 && (this.View.TagPanelHeight = Math.max(0, (r?.getBoundingClientRect().height ?? 0) - this.InsetDist)))).observe(r)}
                 style={{
                     transformOrigin: 'top left',
                     maxWidth: `${100 * this.currentScale}%`,
@@ -338,7 +348,7 @@ export class TagsView extends ObservableReactComponent<TagViewProps> {
                     backgroundColor: this.isEditing ? Colors.LIGHT_GRAY : Colors.TRANSPARENT,
                     borderColor: this.isEditing ? Colors.BLACK : Colors.TRANSPARENT,
                     position: 'relative',
-                    top: `calc(-${this.InsetDist} * ${1 / this.currentScale}px)`,
+                    top: this._props.Views.length > 1 ? 25 : `calc(-${this.InsetDist} * ${1 / this.currentScale}px)`,
                 }}>
                 <div className="tagsView-content" style={{ width: '100%' }}>
                     <div className="tagsView-list">
@@ -346,10 +356,17 @@ export class TagsView extends ObservableReactComponent<TagViewProps> {
                             <IconButton style={{ width: '8px' }} tooltip="Close Menu" onPointerDown={() => this.setToEditing(!this._isEditing)} icon={<FontAwesomeIcon icon={this._isEditing ? 'chevron-up' : 'chevron-down'} size="sm" />} />
                         )}
                         {Array.from(tagsList).map((tag, i) => (
-                            <TagItem key={i} doc={this._props.View.Document} tag={tag} tagDoc={TagItem.findTagCollectionDoc(tag) ?? TagItem.createTagCollectionDoc(tag)} setToEditing={this.setToEditing} showRemoveUI={this.isEditing} />
+                            <TagItem
+                                key={i}
+                                docs={this._props.Views.map(view => view.Document)}
+                                tag={tag}
+                                tagDoc={TagItem.findTagCollectionDoc(tag) ?? TagItem.createTagCollectionDoc(tag)}
+                                setToEditing={this.setToEditing}
+                                showRemoveUI={this.isEditing}
+                            />
                         ))}
                         {Array.from(facesList).map((tag, i) => (
-                            <TagItem key={i} doc={this._props.View.Document} tag={tag} tagDoc={undefined} setToEditing={this.setToEditing} showRemoveUI={this.isEditing} />
+                            <TagItem key={i} docs={this._props.Views.map(view => view.Document)} tag={tag} tagDoc={undefined} setToEditing={this.setToEditing} showRemoveUI={this.isEditing} />
                         ))}
                     </div>
                     {this.isEditing ? (
diff --git a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx
index f9f6c81ab..534f67927 100644
--- a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx
+++ b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx
@@ -179,7 +179,7 @@ export class UniqueFaceBox extends ViewBoxBaseComponent<FieldViewProps>() {
                             ele?.addEventListener('wheel', this.onPassiveWheel, { passive: false });
                         })}>
                         {FaceRecognitionHandler.UniqueFaceImages(this.Document).map((doc, i) => {
-                            const [name, type] = ImageCast(doc[Doc.LayoutFieldKey(doc)]).url.href.split('.');
+                            const [name, type] = ImageCast(doc[Doc.LayoutFieldKey(doc)])?.url.href.split('.') ?? ['-missing-', '.png'];
                             return (
                                 <div
                                     className="image-wrapper"
-- 
cgit v1.2.3-70-g09d2

