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 { Utils, emptyFunction } from '../../Utils'; import { Doc, DocListCast, StrListCast } from '../../fields/Doc'; import { DocData } from '../../fields/DocSymbols'; import { List } from '../../fields/List'; import { NumCast, StrCast } from '../../fields/Types'; import { DocumentType } from '../documents/DocumentTypes'; import { DragManager } from '../util/DragManager'; import { SelectionManager } from '../util/SelectionManager'; import { SnappingManager } from '../util/SnappingManager'; import { undoable } from '../util/UndoManager'; 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 { /** * return list of all Docs that collect Docs with specified keywords */ public static get AllKeywordCollections() { return DocListCast(Doc.ActiveDashboard?.myKeywordCollections); } /** * Find Doc that collects all Docs with given keyword * @param keyword keyword string * @returns keyword collection Doc or undefined */ public static findKeywordCollectionDoc = (keyword: String) => KeywordItem.AllKeywordCollections.find(doc => doc.title === keyword); /** * Creates a Doc that collects Docs with the specified keyword * @param keyword keyword string * @returns collection Doc */ public static createKeywordCollectionDoc = (keyword: string) => { const newKeywordCol = new Doc(); newKeywordCol.title = keyword; newKeywordCol.collections = new List(); newKeywordCol[DocData].docs = new List(); // If the active Dashboard does not have a keyword collection, create it. if (Doc.ActiveDashboard) { if (!Doc.ActiveDashboard.myKeywordCollections) Doc.ActiveDashboard.myKeywordCollections = new List(); Doc.AddDocToList(Doc.ActiveDashboard, 'myKeywordCollections', newKeywordCol); } return newKeywordCol; }; /** * Gets all Docs that have the specified keyword * @param keyword keyword string * @returns An array of documents that contain the keyword. */ public static allDocsWithKeyword = (keyword: string) => DocListCast(KeywordItem.findKeywordCollectionDoc(keyword)?.[DocData].docs); /** * Adds a keyword to the metadata of this document * @param keyword keyword string */ public static addLabelToDoc = (doc: Doc, keyword: string) => { // If the keyword collection is not in active Dashboard, add it as a new doc, with the keyword as its title. const keywordCollection = KeywordItem.findKeywordCollectionDoc(keyword) ?? KeywordItem.createKeywordCollectionDoc(keyword); // If the document is of type COLLECTION, make it a smart collection, otherwise, add the keyword to the document. if (doc.type === DocumentType.COL) { Doc.AddDocToList(keywordCollection[DocData], 'collections', doc); // Iterate through the keyword Doc collections and add a copy of the document to each collection for (const cdoc of DocListCast(keywordCollection[DocData].docs)) { if (!DocListCast(doc[DocData].data).find(d => Doc.AreProtosEqual(d, cdoc))) { const newEmbedding = Doc.MakeEmbedding(cdoc); Doc.AddDocToList(doc[DocData], 'data', newEmbedding); Doc.SetContainer(newEmbedding, doc); } } } else { // Add this document to the keyword's collection of associated documents. Doc.AddDocToList(keywordCollection[DocData], 'docs', 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)) { if (!DocListCast(collection[DocData].data).find(d => Doc.AreProtosEqual(d, doc))) { const newEmbedding = Doc.MakeEmbedding(doc); Doc.AddDocToList(collection[DocData], 'data', newEmbedding); Doc.SetContainer(newEmbedding, collection); } } } if (!doc[DocData].tags) doc[DocData].tags = new List(); const tagList = doc[DocData].tags as List; if (!tagList.includes(keyword)) tagList.push(keyword); }; public static RemoveLabel = (doc: Doc, keyword: string, keywordDoc?: Doc) => { if (doc[DocData].tags) { if (doc.type === DocumentType.COL) { keywordDoc && Doc.RemoveDocFromList(keywordDoc[DocData], 'collections', doc); for (const cur_doc of KeywordItem.allDocsWithKeyword(keyword)) { doc[DocData].data = new List(DocListCast(doc[DocData].data).filter(d => !Doc.AreProtosEqual(cur_doc, d))); } } else { keywordDoc && Doc.RemoveDocFromList(keywordDoc[DocData], 'docs', doc); for (const collection of DocListCast(keywordDoc?.collections)) { collection[DocData].data = new List(DocListCast(collection[DocData].data).filter(d => !Doc.AreProtosEqual(doc, d))); } } } doc[DocData].tags = new List((doc[DocData].tags as List).filter(label => label !== keyword)); }; private _ref: React.RefObject; constructor(props: any) { super(props); makeObservable(this); this._ref = React.createRef(); } /** * Creates a smart collection. * @returns */ createCollection = () => { // Get the documents that contain the keyword. const newEmbeddings = KeywordItem.allDocsWithKeyword(this._props.keyword).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; docData.tags = new List([this._props.keyword]); docData.showLabels = true; docData.freeform_fitContentsToBox = true; doc._freeform_panX = doc._freeform_panY = 0; doc._width = 900; doc._height = 900; doc.layout_fitWidth = true; return doc; })(Doc.MakeCopy(Doc.UserDoc().emptyCollection as Doc, true)); newEmbeddings.forEach(embed => Doc.SetContainer(embed, newCollection)); // Add the collection to the keyword document's list of associated smart collections. this._props.keywordDoc && Doc.AddDocToList(this._props.keywordDoc, 'collections', newCollection); return newCollection; }; @action handleDragStart = (e: React.PointerEvent) => { if (this._props.isEditing) { 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(); } }; render() { setTimeout(() => KeywordItem.addLabelToDoc(this._props.doc, this._props.keyword)); // bcz: hack to make sure that Docs are added to their keyword Doc collection since metadata can get set anywhere without a guard triggering an add to the collection const keyword = this._props.keyword.replace(/^#/, ''); const metadata = keyword.startsWith('@') ? keyword.replace(/^@/, '') : ''; return (
{metadata ? ( {keyword}  {this._props.doc[metadata] as string} ) : ( keyword )} {this.props.isEditing && ( KeywordItem.RemoveLabel(this._props.doc, this._props.keyword, this._props.keywordDoc), `remove label ${this._props.keyword}`)} icon={'X'} style={{ width: '8px', height: '8px', marginLeft: '10px' }} /> )}
); } } interface KeywordBoxProps { Document: Doc; } /** * A component that handles the keyword display for documents. */ @observer export class KeywordBox extends ObservableReactComponent { private _height: number = 0; private _ref: React.RefObject; constructor(props: any) { super(props); makeObservable(this); this._ref = React.createRef(); reaction( () => this.cur_height, () => { this._props.Document[DocData].keywordHeight = this._height; } ); } @observable _currentInput = ''; @observable _isEditing = !StrListCast(this._props.Document[DocData].tags).length; @computed get currentScale() { return NumCast((this._props.Document.embedContainer as Doc)?._freeform_scale, 1); } @computed get cur_height() { return this._ref.current?.offsetHeight ?? 0; } @computed get isEditing() { return this._isEditing && SelectionManager.Docs().includes(this._props.Document); } componentDidMount() { this._height = this._ref.current?.offsetHeight ?? 0; this._props.Document[DocData].keywordHeight = this._height; } componentDidUpdate(prevProps: Readonly): void { this._height = this._ref.current?.offsetHeight ?? 0; this._props.Document[DocData].keywordHeight = this._height; } @action setToEditing = () => { this._isEditing = true; }; /** * Adds the keyword to the document. * @param keyword */ submitLabel = undoable((keyword: string) => { const submittedLabel = keyword.trim(); submittedLabel && KeywordItem.addLabelToDoc(this._props.Document, '#' + submittedLabel.replace(/^#/, '')); this._currentInput = ''; // Clear the input box }, 'added doc label'); render() { const keywordsList = StrListCast(this._props.Document[DocData].tags); return !this._props.Document.showLabels ? null : (
{keywordsList.map(keyword => ( ))}
{this.isEditing ? (
(this._currentInput = e.target.value))} onKeyDown={e => { 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' }} />
{KeywordItem.AllKeywordCollections.map(doc => { const keyword = StrCast(doc.title); return (
{!keywordsList.length ? null : ( // (this._isEditing = false))} icon="x" /> )}
) : null}
); } }