diff options
author | IEatChili <nanunguyen99@gmail.com> | 2024-07-17 15:13:11 -0400 |
---|---|---|
committer | IEatChili <nanunguyen99@gmail.com> | 2024-07-17 15:13:11 -0400 |
commit | 732a00ddba502e3692fde374554c2ed394d275e4 (patch) | |
tree | 03223bc8fa24606c847480a0a8b1648aa8d6a6fc /src | |
parent | 7e13e1df797f1d3358f553802527bf42c5574e81 (diff) |
feat: created smart collections
Diffstat (limited to 'src')
-rw-r--r-- | src/client/documents/Documents.ts | 9 | ||||
-rw-r--r-- | src/client/views/DocumentDecorations.scss | 2 | ||||
-rw-r--r-- | src/client/views/DocumentDecorations.tsx | 2 | ||||
-rw-r--r-- | src/client/views/KeywordBox.tsx | 197 | ||||
-rw-r--r-- | src/client/views/StyleProvider.scss | 1 | ||||
-rw-r--r-- | src/client/views/collections/CollectionSubView.tsx | 4 | ||||
-rw-r--r-- | src/client/views/collections/CollectionView.tsx | 1 | ||||
-rw-r--r-- | src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx | 4 | ||||
-rw-r--r-- | src/client/views/collections/collectionFreeForm/MarqueeView.tsx | 3 |
9 files changed, 174 insertions, 49 deletions
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 449347403..3737aa0b5 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -37,12 +37,13 @@ export enum FInfoFieldType { date = 'date', list = 'list', rtf = 'rich text', + map = 'map', } export class FInfo { description: string = ''; readOnly: boolean = false; fieldType?: FInfoFieldType; - values?: FieldType[]; + values?: FieldType[] | Map<any, any>; filterable?: boolean = true; // can be used as a Filter in FilterPanel // format?: string; // format to display values (e.g, decimal places, $, etc) @@ -143,6 +144,10 @@ class ListInfo extends FInfo { fieldType? = FInfoFieldType.list; values?: List<any>[] = []; } +class MapInfo extends FInfo { + fieldType? = FInfoFieldType.map; + values?: Map<any, any> = new Map(); +} type BOOLt = BoolInfo | boolean; type NUMt = NumInfo | number; type STRt = StrInfo | string; @@ -155,6 +160,7 @@ type COLLt = CTypeInfo | CollectionViewType; type DROPt = DAInfo | dropActionType; type DATEt = DateInfo | number; type DTYPEt = DTypeInfo | string; +type MAPt = MapInfo | Map<any, any>; export class DocumentOptions { // coordinate and dimensions depending on view x?: NUMt = new NumInfo('horizontal coordinate in freeform view', false); @@ -481,6 +487,7 @@ export class DocumentOptions { cardSort?: STRt = new StrInfo('way cards are sorted in deck view'); cardSort_customField?: STRt = new StrInfo('field key used for sorting cards'); cardSort_visibleSortGroups?: List<number>; // which sorting values are being filtered (shown) + keywords?: MAPt = new MapInfo('keywords', true); } export const DocOptions = new DocumentOptions(); diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss index 239c0a977..67e1054c3 100644 --- a/src/client/views/DocumentDecorations.scss +++ b/src/client/views/DocumentDecorations.scss @@ -512,7 +512,7 @@ $resizeHandler: 8px; justify-content: center; align-items: center; gap: 5px; - top: 4px; + //top: 4px; background: $light-gray; opacity: 0.2; pointer-events: all; diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 20bf8fd9f..dc40562e8 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -640,6 +640,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora render() { const { b, r, x, y } = this.Bounds; const seldocview = DocumentView.Selected().lastElement(); + const doc = DocumentView.SelectedDocs().lastElement(); if (SnappingManager.IsDragging || r - x < 1 || x === Number.MAX_VALUE || !seldocview || this._hidden || isNaN(r) || isNaN(b) || isNaN(x) || isNaN(y)) { setTimeout( action(() => { @@ -831,6 +832,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora <div className="link-button-container" style={{ + top: `${doc[DocData].showLabels ? 4 + (doc._keywordHeight as number) : 4}px`, transform: `translate(${-this._resizeBorderWidth / 2 + 10}px, ${this._resizeBorderWidth + bounds.b - bounds.y + this._titleHeight}px) `, }}> <DocumentButtonBar views={() => DocumentView.Selected()} /> diff --git a/src/client/views/KeywordBox.tsx b/src/client/views/KeywordBox.tsx index d94f011f4..321362299 100644 --- a/src/client/views/KeywordBox.tsx +++ b/src/client/views/KeywordBox.tsx @@ -2,17 +2,26 @@ import { Colors, IconButton } from 'browndash-components'; import { action, computed, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import React from 'react'; -import { Doc } from '../../fields/Doc'; +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 { emptyFunction } from '../../Utils'; +import { Docs } from '../documents/Documents'; +import { DocUtils } from '../documents/DocUtils'; import { DragManager, SetupDrag } 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'; interface KeywordItemProps { doc: Doc; - label: string; + keyword: string; + keywordDoc: Doc; + keywordCollection: Doc[]; setToEditing: () => void; isEditing: boolean; } @@ -25,28 +34,77 @@ export class KeywordItem extends ObservableReactComponent<KeywordItemProps> { this.ref = React.createRef(); } - private _dropDisposer?: DragManager.DragDropDisposer; private ref: React.RefObject<HTMLDivElement>; - protected createDropTarget = (ele: HTMLDivElement) => { - this._dropDisposer?.(); - SetupDrag(this.ref, () => undefined); - //ele && (this._dropDisposer = DragManager. (ele, this.onInternalDrop.bind(this), this.layoutDoc)); - //ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.layoutDoc)); + getKeywordCollectionDocs = () => { + for (const doc of DocListCast(Doc.UserDoc().myKeywordCollections)) { + if (doc.title === this._props.keyword) { + return doc[DocData].docs; + } + } + return null; + }; + + createCollection = () => { + const selected = DocListCast(this.getKeywordCollectionDocs()!); + const newEmbeddings = selected.map(doc => Doc.MakeEmbedding(doc)); + const newCollection = ((doc: Doc) => { + const docData = doc[DocData]; + docData.data = new List<Doc>(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; + //newCollection[DocData].smartCollection = this._props.keywordDoc; + + this._props.keywordDoc.collections = new List<Doc>([...DocListCast(this._props.keywordDoc.collections), newCollection]); + 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) { - this._props.doc[DocData].data_labels = (this._props.doc[DocData].data_labels as List<string>).filter(label => label !== this._props.label) as List<string>; - this._props.doc![DocData][`${this._props.label}`] = false; + const filtered_docs = new List<Doc>(DocListCast(this.getKeywordCollectionDocs()!).filter(doc => doc !== this._props.doc)); + this._props.keywordDoc[DocData].docs = filtered_docs; + + 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 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))); + } } }; render() { return ( - <div className="keyword" onClick={this._props.setToEditing} onPointerDown={() => {}} ref={this.ref}> - {this._props.label} + <div className="keyword" onClick={this._props.setToEditing} onPointerDown={this.handleDragStart} ref={this.ref}> + {this._props.keyword} {this.props.isEditing && <IconButton tooltip={'Remove label'} onPointerDown={this.removeLabel} icon={'X'} style={{ width: '8px', height: '8px', marginLeft: '10px' }} />} </div> ); @@ -61,31 +119,39 @@ interface KeywordBoxProps { @observer export class KeywordBox extends ObservableReactComponent<KeywordBoxProps> { @observable _currentInput: string = ''; - //private disposer: () => void; + private height: number = 0; + private ref: React.RefObject<HTMLDivElement>; + + @computed + get currentScale() { + return NumCast((this._props.doc.embedContainer as Doc)?._freeform_scale, 1); + } constructor(props: any) { super(props); makeObservable(this); + this.ref = React.createRef(); } - // componentDidMount(): void { - // reaction( - // () => ({ - // isDragging: SnappingManager.IsDragging, - // selectedDoc: DocumentView.SelectedDocs().lastElement(), - // isEditing: this._props.isEditing, - // }), - // ({ isDragging, selectedDoc, isEditing }) => { - // if (isDragging || selectedDoc !== this._props.doc || !isEditing) { - // this.setToView(); - // } - // } - // ); - // } - - // componentWillUnmount() { - // this.disposer(); - // } + componentDidMount(): void { + this.height = this.ref.current?.getBoundingClientRect().height ? this.ref.current?.getBoundingClientRect().height : 0; + this._props.doc._keywordHeight = this.height; + + reaction( + () => this.currentScale, + () => { + if (this.currentScale < 1) { + this.height = this.ref.current?.getBoundingClientRect().height ? this.ref.current?.getBoundingClientRect().height : 0; + this._props.doc._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; + } @action setToEditing = () => { @@ -97,14 +163,51 @@ export class KeywordBox extends ObservableReactComponent<KeywordBoxProps> { this._props.isEditing = false; }; + getKeywordCollection = (keyword: string) => { + for (const doc of DocListCast(Doc.UserDoc().myKeywordCollections)) { + if (doc.title === keyword) { + return doc; + } + } + + 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]); + + return keywordCollection; + }; + submitLabel = () => { - if (this._currentInput.trim()) { + if (!Doc.UserDoc().myKeywordCollections) { + Doc.UserDoc().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 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<string>(); } - (this._props.doc![DocData].data_labels! as List<string>).push(this._currentInput.trim()); + // 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; + + // 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; + } + this._currentInput = ''; // Clear the input box } }; @@ -120,19 +223,35 @@ 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(); }) ); } return ( - <div className="keywords-container" style={{ backgroundColor: this._props.isEditing ? Colors.LIGHT_GRAY : Colors.TRANSPARENT, borderColor: this._props.isEditing ? Colors.BLACK : Colors.TRANSPARENT }}> + <div + className="keywords-container" + ref={this.ref} + style={{ + transformOrigin: 'top left', + transform: `scale(${1 / this.currentScale})`, + backgroundColor: this._props.isEditing ? Colors.LIGHT_GRAY : Colors.TRANSPARENT, + borderColor: this._props.isEditing ? Colors.BLACK : Colors.TRANSPARENT, + }}> <div className="keywords-list"> - {(keywordsList as List<string>).map(label => { - return <KeywordItem doc={this._props.doc} label={label} setToEditing={this.setToEditing} isEditing={this._props.isEditing}></KeywordItem>; + {(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 ? ( diff --git a/src/client/views/StyleProvider.scss b/src/client/views/StyleProvider.scss index 7cc06f922..4267762aa 100644 --- a/src/client/views/StyleProvider.scss +++ b/src/client/views/StyleProvider.scss @@ -58,7 +58,6 @@ display: flex; flex-wrap: wrap; flex-direction: column; - padding-bottom: 4px; border: 1px solid; border-radius: 4px; } diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index e250d7a90..26528b2b3 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -1,4 +1,4 @@ -import { action, computed, makeObservable, observable } from 'mobx'; +import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx'; import * as React from 'react'; import * as rp from 'request-promise'; import { ClientUtils, returnFalse } from '../../../ClientUtils'; @@ -9,7 +9,7 @@ import { Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; import { listSpec } from '../../../fields/Schema'; import { ScriptField } from '../../../fields/ScriptField'; -import { BoolCast, Cast, ScriptCast, StrCast } from '../../../fields/Types'; +import { BoolCast, Cast, DocCast, ScriptCast, StrCast } from '../../../fields/Types'; import { WebField } from '../../../fields/URLField'; import { GetEffectiveAcl, TraceMobx } from '../../../fields/util'; import { GestureUtils } from '../../../pen-gestures/GestureUtils'; diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 5c304b4a9..a750b731a 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -34,6 +34,7 @@ import { CollectionMulticolumnView } from './collectionMulticolumn/CollectionMul import { CollectionMultirowView } from './collectionMulticolumn/CollectionMultirowView'; import { CollectionSchemaView } from './collectionSchema/CollectionSchemaView'; import { CollectionCardView } from './CollectionCardDeckView'; +import { DocData } from '../../../fields/DocSymbols'; @observer export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewProps>() { diff --git a/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx b/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx index fec4d3e12..af01d6cbc 100644 --- a/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx +++ b/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx @@ -238,14 +238,14 @@ export class ImageLabelBox extends ViewBoxBaseComponent<FieldViewProps>() { if (this._selectedImages.length === 0) { return ( - <div className="searchBox-container" style={{ pointerEvents: 'all', color: SnappingManager.userColor, background: SnappingManager.userBackgroundColor }} ref={ele => this.createDropTarget(ele)}> + <div className="searchBox-container" style={{ pointerEvents: 'all', color: SnappingManager.userColor, background: SnappingManager.userBackgroundColor }} ref={ele => this.createDropTarget(ele!)}> <p style={{ fontSize: 'large' }}>In order to classify and sort images, marquee select the desired images and press the 'Classify and Sort Images' button. Then, add the desired groups for the images to be put in.</p> </div> ); } return ( - <div className="searchBox-container" style={{ pointerEvents: 'all', color: SnappingManager.userColor, background: SnappingManager.userBackgroundColor }} ref={ele => this.createDropTarget(ele)}> + <div className="searchBox-container" style={{ pointerEvents: 'all', color: SnappingManager.userColor, background: SnappingManager.userBackgroundColor }} ref={ele => this.createDropTarget(ele!)}> <div className="searchBox-bar" style={{ pointerEvents: 'all', color: SnappingManager.userColor, background: SnappingManager.userBackgroundColor }}> <IconButton tooltip={'See image information'} diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 07e3acb1d..d7a41df64 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -482,11 +482,8 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps } labelToCollection.set(label, newCollection); this._props.addDocument?.(newCollection); - console.log('added collection!'); } - console.log(labelToCollection); - for (const doc of selectedImages) { if (doc[DocData].data_label) { Doc.AddDocToList(labelToCollection.get(doc[DocData].data_label as string)!, undefined, doc); |