import { Button, Colors, IconButton } from 'browndash-components'; import { action, computed, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import React from 'react'; import { returnFalse, setupMoveUpEvents } from '../../ClientUtils'; import { Doc, DocListCast } from '../../fields/Doc'; import { DocData } from '../../fields/DocSymbols'; import { List } from '../../fields/List'; import { NumCast, StrCast } from '../../fields/Types'; import { emptyFunction } from '../../Utils'; import { DocumentType } from '../documents/DocumentTypes'; import { DragManager } from '../util/DragManager'; import { SnappingManager } from '../util/SnappingManager'; import { DocumentView } from './nodes/DocumentView'; import { ObservableReactComponent } from './ObservableReactComponent'; interface KeywordItemProps { doc: Doc; keyword: string; keywordDoc: Doc; setToEditing: () => void; isEditing: boolean; } /** * A component that handles individual keywords. */ @observer export class KeywordItem extends ObservableReactComponent { constructor(props: any) { super(props); makeObservable(this); this.ref = React.createRef(); } private ref: React.RefObject; /** * Gets the documents that a keyword is associated with. * @returns An array of documents that contain the keyword. */ getKeywordCollectionDocs = () => { for (const doc of DocListCast(Doc.ActiveDashboard?.myKeywordCollections)) { if (doc.title === this._props.keyword) { return doc[DocData].docs; } } return null; }; /** * Creates a smart collection. * @returns */ createCollection = () => { // Get the documents that contain the keyword. const selected = DocListCast(this.getKeywordCollectionDocs()!); const newEmbeddings = selected.map(doc => Doc.MakeEmbedding(doc)); // Create a new collection and set up configurations. const newCollection = ((doc: Doc) => { const docData = doc[DocData]; docData.data = new List(newEmbeddings); docData.title = this._props.keyword; doc._freeform_panX = doc._freeform_panY = 0; return doc; })(Doc.MakeCopy(Doc.UserDoc().emptyCollection as Doc, true)); newEmbeddings.forEach(embed => (embed.embedContainer = newCollection)); newCollection._width = 900; newCollection._height = 900; newCollection.layout_fitWidth = true; // Add the collection to the keyword document's list of associated smart collections. this._props.keywordDoc.collections = new List([...DocListCast(this._props.keywordDoc.collections), newCollection]); newCollection[DocData].data_labels = new List([this._props.keyword]); newCollection[DocData][`${this._props.keyword}`] = true; newCollection[DocData].showLabels = true; return newCollection; }; @action handleDragStart = (e: React.PointerEvent) => { if (this._props.isEditing) { const clone = this.ref.current?.cloneNode(true) as HTMLElement; if (!clone) return; setupMoveUpEvents( this, e, () => { const dragData = new DragManager.DocumentDragData([this.createCollection()]); DragManager.StartDocumentDrag([this.ref.current!], dragData, e.clientX, e.clientY, {}); return true; }, returnFalse, emptyFunction ); e.preventDefault(); } }; @action removeLabel = () => { if (this._props.doc[DocData].data_labels) { if (this._props.doc.type === DocumentType.COL) { const filtered_collections = new List(DocListCast(this._props.keywordDoc.collections).filter(doc => doc !== this._props.doc)); this._props.keywordDoc.collections = filtered_collections; for (const cur_doc of DocListCast(this.getKeywordCollectionDocs()!, [])) { this._props.doc[DocData].data = new List(DocListCast(this._props.doc[DocData].data).filter(doc => !Doc.AreProtosEqual(cur_doc, doc))); } } else { const filtered_docs = new List(DocListCast(this.getKeywordCollectionDocs()!).filter(doc => doc !== this._props.doc)); this._props.keywordDoc[DocData].docs = filtered_docs; for (const collection of DocListCast(this._props.keywordDoc.collections)) { collection[DocData].data = new List(DocListCast(collection[DocData].data).filter(doc => !Doc.AreProtosEqual(this._props.doc, doc))); } } } this._props.doc[DocData].data_labels = (this._props.doc[DocData].data_labels as List).filter(label => label !== this._props.keyword) as List; this._props.doc![DocData][`${this._props.keyword}`] = false; }; render() { return (
{this._props.keyword} {this.props.isEditing && }
); } } interface KeywordBoxProps { doc: Doc; isEditing: boolean; } /** * A component that handles the keyword display for documents. */ @observer export class KeywordBox extends ObservableReactComponent { @observable _currentInput: string = ''; private height: number = 0; private ref: React.RefObject; @computed get currentScale() { return NumCast((this._props.doc.embedContainer as Doc)?._freeform_scale, 1); } @computed get cur_height() { return this.ref.current?.offsetHeight ? this.ref.current?.offsetHeight : 0; } constructor(props: any) { super(props); makeObservable(this); this.ref = React.createRef(); reaction( () => this.cur_height, () => { this._props.doc[DocData].keywordHeight = this.height; } ); } componentDidMount(): void { this.height = this.ref.current?.offsetHeight ? this.ref.current?.offsetHeight : 0; this._props.doc[DocData].keywordHeight = this.height; } componentDidUpdate(prevProps: Readonly): void { this.height = this.ref.current?.offsetHeight ? this.ref.current?.offsetHeight : 0; this._props.doc[DocData].keywordHeight = this.height; } @action setToEditing = () => { this._props.isEditing = true; }; @action setToView = () => { this._props.isEditing = false; }; /** * Gets the document associated with a keyword. * @param keyword The keyword being searched for * @returns A Doc containing keyword information */ getKeywordCollection = (keyword: string) => { // Look for the keyword document. for (const doc of DocListCast(Doc.ActiveDashboard!.myKeywordCollections)) { if (doc.title === keyword) { return doc; } } // If not contained, create a new document and add it to the active Dashboard's keyword list. const keywordCollection = new Doc(); keywordCollection.title = keyword; keywordCollection[DocData].docs = new List(); keywordCollection.collections = new List(); if (Doc.ActiveDashboard) { Doc.ActiveDashboard.myKeywordCollections = new List([...DocListCast(Doc.ActiveDashboard.myKeywordCollections), keywordCollection]); } return keywordCollection; }; /** * Adds the keyword to the document. * @param keyword */ submitLabel = (keyword: string) => { // If the active Dashboard does not have a keyword collection, create it. if (Doc.ActiveDashboard && !Doc.ActiveDashboard.myKeywordCollections) { Doc.ActiveDashboard.myKeywordCollections = new List(); } const submittedLabel = keyword.trim(); if (submittedLabel && !this._props.doc![DocData][`${submittedLabel}`]) { // If the keyword collection is not in active Dashboard, add it as a new doc, with the keyword as its title. const keywordCollection = this.getKeywordCollection(submittedLabel); // If the document has no keywords field, create the field. if (!this._props.doc[DocData].data_labels) { this._props.doc[DocData].data_labels = new List(); } // If the document is of type COLLECTION, make it a smart collection, otherwise, add the keyword to the document. if (this._props.doc.type === DocumentType.COL) { keywordCollection.collections = new List([...DocListCast(keywordCollection.collections), this._props.doc]); // Iterate through the keyword document's collections and add a copy of the document to each collection for (const doc of DocListCast(keywordCollection[DocData].docs)) { const newEmbedding = Doc.MakeEmbedding(doc); this._props.doc[DocData].data = new List([...DocListCast(this._props.doc[DocData].data), newEmbedding]); newEmbedding.embedContainer = this._props.doc; } } else { // Add this document to the keyword's collection of associated documents. keywordCollection[DocData].docs = new List([...DocListCast(keywordCollection[DocData].docs), this._props.doc]); // Iterate through the keyword document's collections and add a copy of the document to each collection for (const collection of DocListCast(keywordCollection.collections)) { const newEmbedding = Doc.MakeEmbedding(this._props.doc); collection[DocData].data = new List([...DocListCast(collection.data), newEmbedding]); newEmbedding.embedContainer = collection; } } // Push the keyword to the document's keyword list field. (this._props.doc![DocData].data_labels! as List).push(submittedLabel); this._props.doc![DocData][`${submittedLabel}`] = true; this._currentInput = ''; // Clear the input box } }; @action onInputChange = (e: React.ChangeEvent) => { this._currentInput = e.target.value; }; render() { const keywordsList = this._props.doc[DocData].data_labels ? this._props.doc[DocData].data_labels : new List(); const seldoc = DocumentView.SelectedDocs().lastElement(); if (SnappingManager.IsDragging || !(seldoc === this._props.doc) || !this._props.isEditing) { setTimeout( action(() => { if ((keywordsList as List).length === 0) { this._props.doc[DocData].showLabels = false; } this.setToView(); }) ); } return (
{(keywordsList as List).map(keyword => { return ; })}
{this._props.isEditing ? (
{ e.key === 'Enter' ? this.submitLabel(this._currentInput) : null; e.stopPropagation(); }} type="text" placeholder="Input keywords for document..." aria-label="keyword-input" className="keyword-input" style={{ width: '100%', borderRadius: '5px' }} />
{Doc.ActiveDashboard?.myKeywordCollections ? (
{DocListCast(Doc.ActiveDashboard?.myKeywordCollections).map(doc => { const keyword = StrCast(doc.title); return (
) : (
)}
{ if ((keywordsList as List).length === 0) { this._props.doc[DocData].showLabels = false; } else { this.setToView(); } }} icon={'x'} style={{ width: '4px' }} />
) : (
)}
); } }