From 623da0b4eec34fbd238cc26a5e0c105426a6711e Mon Sep 17 00:00:00 2001 From: bobzel 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') 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 : ( Open keyword menu}> -
DocumentView.Selected().map(dv => (dv.dataDoc.showTags = !dv.dataDoc.showTags))}> +
{ + const showing = DocumentView.Selected().some(dv => dv.dataDoc.showTags); + DocumentView.Selected().forEach(dv => { + dv.dataDoc.showTags = !showing; + }); + }}>
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 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) `, }}> DocumentView.Selected()} />
)} +
+ {DocumentView.Selected().length > 1 ? : null} +
{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, props: Opt ); }; - const tags = () => props?.DocumentView?.() ? : null; + const tags = () => 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; showRemoveUI: boolean; @@ -207,8 +207,12 @@ export class TagItem extends ObservableReactComponent { 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 { {metadata ? ( {tag}  - {typeof this._props.doc[metadata] === 'boolean' ? ( + {typeof this.doc[metadata] === 'boolean' ? ( 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]) )} ) : ( @@ -234,7 +238,7 @@ export class TagItem extends ObservableReactComponent { {this.props.showRemoveUI && this._props.tagDoc && ( 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={} style={{ width: '8px', height: '8px', marginLeft: '10px' }} /> @@ -245,7 +249,7 @@ export class TagItem extends ObservableReactComponent { } interface TagViewProps { - View: DocumentView; + Views: DocumentView[]; } /** @@ -261,12 +265,12 @@ export class TagsView extends ObservableReactComponent { @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 { 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 { @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 { 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 { * When the dropdown is clicked, this will toggle an extended UI that allows additional tags to be added/removed. */ render() { - const tagsList = new Set(StrListCast(this._props.View.dataDoc.tags)); - const chatTagsList = new Set(StrListCast(this._props.View.dataDoc.tags_chat)); + const tagsList = new Set(StrListCast(this.View.dataDoc.tags)); + const chatTagsList = new Set(StrListCast(this.View.dataDoc.tags_chat)); const facesList = new Set( - 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 : (
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 { 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)`, }}>
@@ -346,10 +356,17 @@ export class TagsView extends ObservableReactComponent { this.setToEditing(!this._isEditing)} icon={} /> )} {Array.from(tagsList).map((tag, i) => ( - + view.Document)} + tag={tag} + tagDoc={TagItem.findTagCollectionDoc(tag) ?? TagItem.createTagCollectionDoc(tag)} + setToEditing={this.setToEditing} + showRemoveUI={this.isEditing} + /> ))} {Array.from(facesList).map((tag, i) => ( - + view.Document)} tag={tag} tagDoc={undefined} setToEditing={this.setToEditing} showRemoveUI={this.isEditing} /> ))}
{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() { 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 (