aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/client/views/KeywordBox.tsx263
-rw-r--r--src/client/views/StyleProvider.scss26
2 files changed, 188 insertions, 101 deletions
diff --git a/src/client/views/KeywordBox.tsx b/src/client/views/KeywordBox.tsx
index 321362299..68584a7fa 100644
--- a/src/client/views/KeywordBox.tsx
+++ b/src/client/views/KeywordBox.tsx
@@ -1,4 +1,4 @@
-import { Colors, IconButton } from 'browndash-components';
+import { Button, Colors, IconButton } from 'browndash-components';
import { action, computed, makeObservable, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import React from 'react';
@@ -6,14 +6,11 @@ import { returnFalse, setupMoveUpEvents } from '../../ClientUtils';
import { Doc, DocListCast } from '../../fields/Doc';
import { DocData } from '../../fields/DocSymbols';
import { List } from '../../fields/List';
-import { DocCast, NumCast } from '../../fields/Types';
+import { NumCast, StrCast } from '../../fields/Types';
import { emptyFunction } from '../../Utils';
-import { Docs } from '../documents/Documents';
-import { DocUtils } from '../documents/DocUtils';
-import { DragManager, SetupDrag } from '../util/DragManager';
+import { DocumentType } from '../documents/DocumentTypes';
+import { DragManager } from '../util/DragManager';
import { SnappingManager } from '../util/SnappingManager';
-import { CollectionFreeFormView } from './collections/collectionFreeForm';
-import { MainView } from './MainView';
import { DocumentView } from './nodes/DocumentView';
import { ObservableReactComponent } from './ObservableReactComponent';
@@ -21,11 +18,13 @@ interface KeywordItemProps {
doc: Doc;
keyword: string;
keywordDoc: Doc;
- keywordCollection: Doc[];
setToEditing: () => void;
isEditing: boolean;
}
+/**
+ * A component that handles individual keywords.
+ */
@observer
export class KeywordItem extends ObservableReactComponent<KeywordItemProps> {
constructor(props: any) {
@@ -36,8 +35,12 @@ export class KeywordItem extends ObservableReactComponent<KeywordItemProps> {
private ref: React.RefObject<HTMLDivElement>;
+ /**
+ * 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.UserDoc().myKeywordCollections)) {
+ for (const doc of DocListCast(Doc.ActiveDashboard?.myKeywordCollections)) {
if (doc.title === this._props.keyword) {
return doc[DocData].docs;
}
@@ -45,9 +48,16 @@ export class KeywordItem extends ObservableReactComponent<KeywordItemProps> {
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<Doc>(newEmbeddings);
@@ -59,9 +69,12 @@ export class KeywordItem extends ObservableReactComponent<KeywordItemProps> {
newCollection._width = 900;
newCollection._height = 900;
newCollection.layout_fitWidth = true;
- //newCollection[DocData].smartCollection = this._props.keywordDoc;
+ // Add the collection to the keyword document's list of associated smart collections.
this._props.keywordDoc.collections = new List<Doc>([...DocListCast(this._props.keywordDoc.collections), newCollection]);
+ newCollection[DocData].data_labels = new List<string>([this._props.keyword]);
+ newCollection[DocData][`${this._props.keyword}`] = true;
+ newCollection[DocData].showLabels = true;
return newCollection;
};
@@ -89,16 +102,24 @@ export class KeywordItem extends ObservableReactComponent<KeywordItemProps> {
@action
removeLabel = () => {
if (this._props.doc[DocData].data_labels) {
- const filtered_docs = new List<Doc>(DocListCast(this.getKeywordCollectionDocs()!).filter(doc => doc !== this._props.doc));
- this._props.keywordDoc[DocData].docs = filtered_docs;
+ if (this._props.doc.type === DocumentType.COL) {
+ const filtered_collections = new List<Doc>(DocListCast(this._props.keywordDoc.collections).filter(doc => doc !== this._props.doc));
+ this._props.keywordDoc.collections = filtered_collections;
- this._props.doc[DocData].data_labels = (this._props.doc[DocData].data_labels as List<string>).filter(label => label !== this._props.keyword) as List<string>;
- this._props.doc![DocData][`${this._props.keyword}`] = false;
+ for (const cur_doc of DocListCast(this.getKeywordCollectionDocs()!, [])) {
+ this._props.doc[DocData].data = new List<Doc>(DocListCast(this._props.doc[DocData].data).filter(doc => !Doc.AreProtosEqual(cur_doc, doc)));
+ }
+ } else {
+ const filtered_docs = new List<Doc>(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<Doc>(DocListCast(collection[DocData].data).filter(doc => !Doc.AreProtosEqual(this._props.doc, doc)));
+ for (const collection of DocListCast(this._props.keywordDoc.collections)) {
+ collection[DocData].data = new List<Doc>(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<string>).filter(label => label !== this._props.keyword) as List<string>;
+ this._props.doc![DocData][`${this._props.keyword}`] = false;
};
render() {
@@ -116,6 +137,9 @@ interface KeywordBoxProps {
isEditing: boolean;
}
+/**
+ * A component that handles the keyword display for documents.
+ */
@observer
export class KeywordBox extends ObservableReactComponent<KeywordBoxProps> {
@observable _currentInput: string = '';
@@ -127,30 +151,32 @@ export class KeywordBox extends ObservableReactComponent<KeywordBoxProps> {
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();
- }
-
- componentDidMount(): void {
- this.height = this.ref.current?.getBoundingClientRect().height ? this.ref.current?.getBoundingClientRect().height : 0;
- this._props.doc._keywordHeight = this.height;
reaction(
- () => this.currentScale,
+ () => this.cur_height,
() => {
- if (this.currentScale < 1) {
- this.height = this.ref.current?.getBoundingClientRect().height ? this.ref.current?.getBoundingClientRect().height : 0;
- this._props.doc._keywordHeight = this.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<KeywordBoxProps>): void {
- this.height = this.ref.current?.getBoundingClientRect().height ? this.ref.current?.getBoundingClientRect().height : 0;
- this._props.doc._keywordHeight = this.height;
+ this.height = this.ref.current?.offsetHeight ? this.ref.current?.offsetHeight : 0;
+ this._props.doc[DocData].keywordHeight = this.height;
}
@action
@@ -163,30 +189,44 @@ export class KeywordBox extends ObservableReactComponent<KeywordBoxProps> {
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) => {
- for (const doc of DocListCast(Doc.UserDoc().myKeywordCollections)) {
+ // 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<Doc>();
keywordCollection.collections = new List<Doc>();
- Doc.UserDoc().myKeywordCollections = new List<Doc>([...DocListCast(Doc.UserDoc().myKeywordCollections), keywordCollection]);
+ if (Doc.ActiveDashboard) {
+ Doc.ActiveDashboard.myKeywordCollections = new List<Doc>([...DocListCast(Doc.ActiveDashboard.myKeywordCollections), keywordCollection]);
+ }
return keywordCollection;
};
- submitLabel = () => {
- if (!Doc.UserDoc().myKeywordCollections) {
- Doc.UserDoc().myKeywordCollections = new List<Doc>();
+ /**
+ * 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<Doc>();
}
- const submittedLabel = this._currentInput.trim();
- if (submittedLabel) {
- // If the keyword collection is not in the user doc, add it as a new doc, with the keyword as its title.
+ 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.
@@ -194,20 +234,31 @@ export class KeywordBox extends ObservableReactComponent<KeywordBoxProps> {
this._props.doc[DocData].data_labels = new List<string>();
}
- // Add this document to the keyword's collection of associated documents.
- keywordCollection[DocData].docs = new List<Doc>([...DocListCast(keywordCollection[DocData].docs), this._props.doc]);
-
- // Push the keyword to the document's keyword list field.
- (this._props.doc![DocData].data_labels! as List<string>).push(submittedLabel);
- this._props.doc![DocData][`${this._currentInput}`] = true;
+ // 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<Doc>([...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 collection of DocListCast(keywordCollection.collections)) {
- const newEmbedding = Doc.MakeEmbedding(this._props.doc);
- collection[DocData].data = new List<Doc>([...DocListCast(collection.data), newEmbedding]);
- newEmbedding.embedContainer = collection;
+ // 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<Doc>([...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<Doc>([...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<Doc>([...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<string>).push(submittedLabel);
+ this._props.doc![DocData][`${submittedLabel}`] = true;
this._currentInput = ''; // Clear the input box
}
};
@@ -223,9 +274,9 @@ export class KeywordBox extends ObservableReactComponent<KeywordBoxProps> {
if (SnappingManager.IsDragging || !(seldoc === this._props.doc) || !this._props.isEditing) {
setTimeout(
action(() => {
- // if ((keywordsList as List<string>).length === 0) {
- // this._props.doc[DocData].showLabels = false;
- // }
+ if ((keywordsList as List<string>).length === 0) {
+ this._props.doc[DocData].showLabels = false;
+ }
this.setToView();
})
);
@@ -237,59 +288,75 @@ export class KeywordBox extends ObservableReactComponent<KeywordBoxProps> {
ref={this.ref}
style={{
transformOrigin: 'top left',
+ overflow: 'hidden',
transform: `scale(${1 / this.currentScale})`,
backgroundColor: this._props.isEditing ? Colors.LIGHT_GRAY : Colors.TRANSPARENT,
borderColor: this._props.isEditing ? Colors.BLACK : Colors.TRANSPARENT,
+ maxWidth: `400px`,
}}>
- <div className="keywords-list">
- {(keywordsList as List<string>).map(keyword => {
- return (
- <KeywordItem
- doc={this._props.doc}
- keyword={keyword}
- keywordDoc={this.getKeywordCollection(keyword)}
- keywordCollection={DocListCast(this.getKeywordCollection(keyword))}
- setToEditing={this.setToEditing}
- isEditing={this._props.isEditing}></KeywordItem>
- );
- })}
- </div>
- {this._props.isEditing ? (
- <div className="keyword-editing-box">
- <div className="keyword-input-box">
- <input
- value={this._currentInput}
- autoComplete="off"
- onChange={this.onInputChange}
- onKeyDown={e => {
- e.key === 'Enter' ? this.submitLabel() : null;
- e.stopPropagation();
- }}
- type="text"
- placeholder="Input keywords for document..."
- aria-label="keyword-input"
- className="keyword-input"
- style={{ width: '100%', borderRadius: '5px' }}
- />
- </div>
- <div className="keyword-buttons">
- <IconButton
- tooltip={'Close Menu'}
- onPointerDown={() => {
- if ((keywordsList as List<string>).length === 0) {
- this._props.doc[DocData].showLabels = false;
- } else {
- this.setToView();
- }
- }}
- icon={'x'}
- style={{ width: '4px' }}
- />
- </div>
+ <div className="keywords-content">
+ <div className="keywords-list">
+ {(keywordsList as List<string>).map(keyword => {
+ return <KeywordItem doc={this._props.doc} keyword={keyword} keywordDoc={this.getKeywordCollection(keyword)} setToEditing={this.setToEditing} isEditing={this._props.isEditing}></KeywordItem>;
+ })}
</div>
- ) : (
- <div></div>
- )}
+ {this._props.isEditing ? (
+ <div className="keyword-editing-box">
+ <div className="keyword-input-box">
+ <input
+ value={this._currentInput}
+ autoComplete="off"
+ onChange={this.onInputChange}
+ 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' }}
+ />
+ </div>
+ {Doc.ActiveDashboard?.myKeywordCollections ? (
+ <div className="keyword-suggestions-box">
+ {DocListCast(Doc.ActiveDashboard?.myKeywordCollections).map(doc => {
+ const keyword = StrCast(doc.title);
+ return (
+ <Button
+ style={{ margin: '2px 2px', border: '1px solid black', backgroundColor: 'lightblue', color: 'black' }}
+ text={keyword}
+ color={SnappingManager.userVariantColor}
+ tooltip={'Add existing keyword'}
+ onClick={() => {
+ this.submitLabel(keyword);
+ }}
+ />
+ );
+ })}
+ </div>
+ ) : (
+ <div></div>
+ )}
+ <div className="keyword-buttons">
+ <IconButton
+ tooltip={'Close Menu'}
+ onPointerDown={() => {
+ if ((keywordsList as List<string>).length === 0) {
+ this._props.doc[DocData].showLabels = false;
+ } else {
+ this.setToView();
+ }
+ }}
+ icon={'x'}
+ style={{ width: '4px' }}
+ />
+ </div>
+ </div>
+ ) : (
+ <div></div>
+ )}
+ </div>
</div>
);
}
diff --git a/src/client/views/StyleProvider.scss b/src/client/views/StyleProvider.scss
index 4267762aa..1d41697f5 100644
--- a/src/client/views/StyleProvider.scss
+++ b/src/client/views/StyleProvider.scss
@@ -77,14 +77,34 @@
align-items: center;
}
+.keyword-suggestions-box {
+ display: flex;
+ flex-wrap: wrap;
+ margin: auto;
+ align-self: center;
+ width: 90%;
+ border: 1px solid black;
+ border-radius: 2px;
+ margin-top: 8px;
+}
+
+.keyword-suggestion {
+ cursor: pointer;
+ padding: 1px 1px;
+ margin: 2px 2px;
+ background-color: lightblue;
+ border: 1px solid black;
+ border-radius: 5px;
+ white-space: nowrap;
+ display: flex;
+ align-items: center;
+}
+
.keyword-editing-box {
margin-top: 8px;
}
.keyword-input-box {
- // display: flex;
- // align-items: center;
- // align-content: center;
margin: auto;
align-self: center;
width: 90%;