diff options
Diffstat (limited to 'src/client/views/collections')
9 files changed, 482 insertions, 285 deletions
diff --git a/src/client/views/collections/CollectionCardDeckView.scss b/src/client/views/collections/CollectionCardDeckView.scss index cc797d0bd..d1731c244 100644 --- a/src/client/views/collections/CollectionCardDeckView.scss +++ b/src/client/views/collections/CollectionCardDeckView.scss @@ -8,19 +8,13 @@ overflow: hidden; button { - width: 35px; - height: 35px; border-radius: 50%; - background-color: $dark-gray; - // border-color: $medium-blue; - margin: 5px; // transform: translateY(-50px); } } .card-wrapper { display: grid; grid-template-columns: repeat(10, 1fr); - // width: 100%; transform-origin: top left; position: absolute; @@ -31,45 +25,18 @@ transition: transform 0.3s cubic-bezier(0.455, 0.03, 0.515, 0.955); } -.card-button-container { - display: flex; - padding: 3px; - // width: 300px; - background-color: rgb(218, 218, 218); /* Background color of the container */ - border-radius: 50px; /* Rounds the corners of the container */ - transform: translateY(75px); - // box-shadow: 0 4px 8px rgba(0,0,0,0.1); /* Optional: Adds shadow for depth */ - align-items: center; /* Centers buttons vertically */ - justify-content: start; /* Centers buttons horizontally */ +.no-card-span { + position: relative; + width: fit-content; + text-align: center; + font-size: 65px; } -// button:hover { -// transform: translateY(-50px); -// } - -// .card-wrapper::after { -// content: ""; -// width: 100%; /* Forces wrapping */ -// } - -// .card-wrapper > .card-item:nth-child(10n)::after { -// content: ""; -// width: 100%; /* Forces wrapping after every 10th item */ -// } - -// .card-row{ -// display: flex; -// position: absolute; -// align-items: center; -// transition: transform 0.3s cubic-bezier(0.455, 0.03, 0.515, 0.955); - -// } - .card-item-inactive, .card-item-active, .card-item { position: relative; - transition: transform 0.5s ease-in-out; + transition: transform 0.3s ease-in-out; display: flex; flex-direction: column; } @@ -79,6 +46,5 @@ } .card-item-active { - position: absolute; z-index: 100; } diff --git a/src/client/views/collections/CollectionCardDeckView.tsx b/src/client/views/collections/CollectionCardDeckView.tsx index 37b1adeff..66014a1e0 100644 --- a/src/client/views/collections/CollectionCardDeckView.tsx +++ b/src/client/views/collections/CollectionCardDeckView.tsx @@ -1,20 +1,24 @@ -import { IReactionDisposer, ObservableMap, action, computed, makeObservable, observable, reaction } from 'mobx'; +import { IReactionDisposer, ObservableMap, action, computed, makeObservable, observable, reaction, trace } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { ClientUtils, DashColor, returnFalse, returnZero } from '../../../ClientUtils'; -import { numberRange } from '../../../Utils'; -import { Doc, NumListCast } from '../../../fields/Doc'; +import { emptyFunction } from '../../../Utils'; +import { Doc } from '../../../fields/Doc'; import { DocData } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; -import { BoolCast, Cast, DateCast, NumCast, RTFCast, ScriptCast, StrCast } from '../../../fields/Types'; +import { List } from '../../../fields/List'; +import { BoolCast, DateCast, DocCast, NumCast, RTFCast, ScriptCast, StrCast } from '../../../fields/Types'; import { URLField } from '../../../fields/URLField'; import { gptImageLabel } from '../../apis/gpt/GPT'; import { DocumentType } from '../../documents/DocumentTypes'; import { DragManager } from '../../util/DragManager'; +import { dropActionType } from '../../util/DropActionTypes'; +import { SelectionManager } from '../../util/SelectionManager'; import { SnappingManager } from '../../util/SnappingManager'; import { Transform } from '../../util/Transform'; import { undoable } from '../../util/UndoManager'; import { StyleProp } from '../StyleProp'; +import { TagItem } from '../TagsView'; import { DocumentView } from '../nodes/DocumentView'; import { GPTPopup, GPTPopupMode } from '../pdf/GPTPopup/GPTPopup'; import './CollectionCardDeckView.scss'; @@ -25,24 +29,31 @@ enum cardSortings { Type = 'type', Color = 'color', Custom = 'custom', + Chat = 'chat', + Tag = 'tag', None = '', } + +/** + * New view type specifically for studying more dynamically. Allows you to reorder docs however you see fit, easily + * sort and filter using presets, and customize your experience with chat gpt. + * + * This file contains code as to how the docs are to be rendered (there place geographically and also in regards to sorting), + * and callback functions for the gpt popup + */ @observer export class CollectionCardView extends CollectionSubView() { private _dropDisposer?: DragManager.DragDropDisposer; - private _childDocumentWidth = 600; // target width of a Doc... private _disposers: { [key: string]: IReactionDisposer } = {}; private _textToDoc = new Map<string, Doc>(); + private _dropped = false; // indicate when a card doc has just moved; - @observable _forceChildXf = false; - @observable _isLoading = false; + @observable _forceChildXf = 0; @observable _hoveredNodeIndex = -1; @observable _docRefs = new ObservableMap<Doc, DocumentView>(); @observable _maxRowCount = 10; - - static getButtonGroup(groupFieldKey: 'chat' | 'star' | 'idea' | 'like', doc: Doc): number | undefined { - return Cast(doc[groupFieldKey], 'number', null); - } + @observable _docDraggedIndex: number = -1; + @observable overIndex: number = -1; static imageUrlToBase64 = async (imageUrl: string): Promise<string> => { try { @@ -61,22 +72,56 @@ export class CollectionCardView extends CollectionSubView() { } }; + constructor(props: SubCollectionViewProps) { + super(props); + makeObservable(this); + this.setRegenerateCallback(); + } protected createDashEventsTarget = (ele: HTMLDivElement | null) => { this._dropDisposer?.(); if (ele) { this._dropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.layoutDoc); } }; + /** + * Callback to ensure gpt's text versions of the child docs are updated + */ + setRegenerateCallback = () => GPTPopup.Instance.setRegenerateCallback(this.childPairStringListAndUpdateSortDesc); - constructor(props: SubCollectionViewProps) { - super(props); - makeObservable(this); - } + /** + * update's gpt's doc-text list and initializes callbacks + */ + @action + childPairStringListAndUpdateSortDesc = async () => { + const sortDesc = await this.childPairStringList(); // Await the promise to get the string result + GPTPopup.Instance.setSortDesc(sortDesc.join()); + GPTPopup.Instance.onSortComplete = (sortResult: string, questionType: string, tag?: string) => this.processGptOutput(sortResult, questionType, tag); + GPTPopup.Instance.onQuizRandom = () => this.quizMode(); + }; - componentDidMount(): void { + componentDidMount() { + this._props.setContentViewBox?.(this); + // Reaction to cardSort changes this._disposers.sort = reaction( - () => ({ cardSort: this.cardSort, field: this.cardSort_customField }), - ({ cardSort, field }) => (cardSort === cardSortings.Custom && field === 'chat' ? this.openChatPopup() : GPTPopup.Instance.setVisible(false)) + () => GPTPopup.Instance.visible, + isVis => { + if (isVis) { + this.openChatPopup(); + } else { + this.Document.cardSort = this.cardSort === cardSortings.Chat ? '' : this.Document.cardSort; + } + } + ); + // if card deck moves, then the child doc views are hidden so their screen to local transforms will return empty rectangles + // when inquired from the dom (below in childScreenToLocal). When the doc is actually renders, we need to act like the + // dash data just changed and trigger a React involidation with the correct data (read from the dom). + this._disposers.child = reaction( + () => [this.Document.x, this.Document.y], + () => { + if (!Array.from(this._docRefs.values()).every(dv => dv.ContentDiv?.getBoundingClientRect().width)) { + setTimeout(action(() => this._forceChildXf++)); + } + } ); } @@ -85,56 +130,40 @@ export class CollectionCardView extends CollectionSubView() { this._dropDisposer?.(); } - @computed get cardSort_customField() { - return StrCast(this.Document.cardSort_customField) as 'chat' | 'star' | 'idea' | 'like'; - } - @computed get cardSort() { return StrCast(this.Document.cardSort) as cardSortings; } + + /** + * The child documents to be rendered-- either all of them except the Links or the docs in the currently active + * custom group + */ + @computed get childDocsWithoutLinks() { + return this.childDocs.filter(l => l.type !== DocumentType.LINK); + } + /** * how much to scale down the contents of the view so that everything will fit */ @computed get fitContentScale() { const length = Math.min(this.childDocsWithoutLinks.length, this._maxRowCount); - return (this._childDocumentWidth * length) / this._props.PanelWidth(); - } - - @computed get translateWrapperX() { - let translate = 0; - - if (this.inactiveDocs().length !== this.childDocsWithoutLinks.length && this.inactiveDocs().length < 10) { - translate += this.panelWidth() / 2; - } - return translate; + return (this.childPanelWidth() * length) / this._props.PanelWidth(); } /** - * The child documents to be rendered-- either all of them except the Links or the docs in the currently active - * custom group + * When in quiz mode, randomly selects a document */ - @computed get childDocsWithoutLinks() { - const regularDocs = this.childDocs.filter(l => l.type !== DocumentType.LINK); - const activeGroups = NumListCast(this.Document.cardSort_visibleSortGroups); - - if (activeGroups.length > 0 && this.cardSort === cardSortings.Custom) { - return regularDocs.filter(doc => { - // Get the group number for the current index - const groupNumber = CollectionCardView.getButtonGroup(this.cardSort_customField, doc); - // Check if the group number is in the active groups - return groupNumber !== undefined && activeGroups.includes(groupNumber); - }); - } - - // Default return for non-custom cardSort or other cases, filtering out links - return regularDocs; - } + quizMode = () => { + const randomIndex = Math.floor(Math.random() * this.childDocs.length); + SelectionManager.DeselectAll(); + DocumentView.SelectView(DocumentView.getDocumentView(this.childDocs[randomIndex]), false); + }; /** - * Determines the order in which the cards will be rendered depending on the current sort type + * Number of rows of cards to be rendered */ - @computed get sortedDocs() { - return this.sort(this.childDocsWithoutLinks, this.cardSort, BoolCast(this.layoutDoc.sortDesc)); + @computed get numRows() { + return Math.ceil(this.sortedDocs.length / this._maxRowCount); } @action @@ -157,8 +186,8 @@ export class CollectionCardView extends CollectionSubView() { */ inactiveDocs = () => this.childDocsWithoutLinks.filter(d => !DocumentView.SelectedDocs().includes(d)); - panelWidth = () => this._childDocumentWidth; - panelHeight = (layout: Doc) => () => (this.panelWidth() * NumCast(layout._height)) / NumCast(layout._width); + childPanelWidth = () => NumCast(this.layoutDoc.childPanelWidth, this._props.PanelWidth() / 2); + childPanelHeight = () => this._props.PanelHeight() * this.fitContentScale; onChildDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick); isContentActive = () => this._props.isSelected() || this._props.isContentActive() || this._props.isAnyChildContentActive(); isChildContentActive = () => !!this.isContentActive(); @@ -170,6 +199,8 @@ export class CollectionCardView extends CollectionSubView() { * @returns */ rotate = (amCards: number, index: number) => { + if (amCards == 1) return 0; + const possRotate = -30 + index * (30 / ((amCards - (amCards % 2)) / 2)); const stepMag = Math.abs(-30 + (amCards / 2 - 1) * (30 / ((amCards - (amCards % 2)) / 2))); @@ -188,11 +219,12 @@ export class CollectionCardView extends CollectionSubView() { translateY = (amCards: number, index: number, realIndex: number) => { const evenOdd = amCards % 2; const apex = (amCards - evenOdd) / 2; - const stepMag = 200 / ((amCards - evenOdd) / 2) + Math.abs((apex - index) * 25); + const Magnitude = this.childPanelWidth() / 2; // 400 + const stepMag = Magnitude / 2 / ((amCards - evenOdd) / 2) + Math.abs((apex - index) * 25); let rowOffset = 0; if (realIndex > this._maxRowCount - 1) { - rowOffset = 400 * ((realIndex - (realIndex % this._maxRowCount)) / this._maxRowCount); + rowOffset = Magnitude * ((realIndex - (realIndex % this._maxRowCount)) / this._maxRowCount); } if (evenOdd === 1 || index < apex - 1) { return Math.abs(stepMag * (apex - index)) - rowOffset; @@ -205,27 +237,108 @@ export class CollectionCardView extends CollectionSubView() { }; /** - * Translates the selected node to the middle fo the screen - * @param index - * @returns + * When dragging a card, determines the index the card should be set to if dropped + * @param mouseX mouse's x location + * @param mouseY mouses' y location + * @returns the card's new index */ - translateSelected = (index: number): number => { - // if (this.isSelected(index)) { - const middleOfPanel = this._props.PanelWidth() / 2; - const scaledNodeWidth = this.panelWidth() * 1.25; + findCardDropIndex = (mouseX: number, mouseY: number) => { + const amCardsTotal = this.sortedDocs.length; + let index = 0; + const cardWidth = amCardsTotal < this._maxRowCount ? this._props.PanelWidth() / amCardsTotal : this._props.PanelWidth() / this._maxRowCount; + + // Calculate the adjusted X position accounting for the initial offset + let adjustedX = mouseX; + + const amRows = Math.ceil(amCardsTotal / this._maxRowCount); + const rowHeight = this._props.PanelHeight() / amRows; + const currRow = Math.floor((mouseY - 100) / rowHeight); //rows start at 0 - // Calculate the position of the node's left edge before scaling - const nodeLeftEdge = index * this.panelWidth(); - // Find the center of the node after scaling - const scaledNodeCenter = nodeLeftEdge + scaledNodeWidth / 2; + if (adjustedX < 0) { + return 0; // Before the first column + } - // Calculate the translation needed to align the scaled node's center with the panel's center - const translation = middleOfPanel - scaledNodeCenter - scaledNodeWidth - scaledNodeWidth / 4; + if (amCardsTotal < this._maxRowCount) { + index = Math.floor(adjustedX / cardWidth); + } else if (currRow != amRows - 1) { + index = Math.floor(adjustedX / cardWidth) + currRow * this._maxRowCount; + } else { + const rowAmCards = amCardsTotal - currRow * this._maxRowCount; + const offset = ((this._maxRowCount - rowAmCards) / 2) * cardWidth; + adjustedX = mouseX - offset; - return translation; + index = Math.floor(adjustedX / cardWidth) + currRow * this._maxRowCount; + } + return index; }; /** + * Checks to see if a card is being dragged and calls the appropriate methods if so + * @param e the current pointer event + */ + + @action + onPointerMove = (x: number, y: number) => { + this._docDraggedIndex = DragManager.docsBeingDragged.length ? this.findCardDropIndex(x, y) : -1; + }; + + /** + * Handles external drop of images/PDFs etc from outside Dash. + */ + onExternalDrop = async (e: React.DragEvent): Promise<void> => { + super.onExternalDrop(e, {}); + }; + + /** + * Resets all the doc dragging vairables once a card is dropped + * @param e + * @param de drop event + * @returns true if a card has been dropped, falls if not + */ + onInternalDrop = undoable( + action((e: Event, de: DragManager.DropEvent) => { + if (de.complete.docDragData) { + const dragIndex = this._docDraggedIndex; + const draggedDoc = DragManager.docsBeingDragged[0]; + if (dragIndex > -1 && draggedDoc) { + this._docDraggedIndex = -1; + const sorted = this.sortedDocs; + const originalIndex = sorted.findIndex(doc => doc === draggedDoc); + + this.Document.cardSort = ''; + originalIndex !== -1 && sorted.splice(originalIndex, 1); + sorted.splice(dragIndex, 0, draggedDoc); + if (de.complete.docDragData.removeDocument?.(draggedDoc)) { + this.dataDoc[this.fieldKey] = new List<Doc>(sorted); + } + this._dropped = true; + } + e.stopPropagation(); + return true; + } + return false; + }), + '' + ); + + @computed get sortedDocs() { + return this.sort(this.childDocsWithoutLinks, this.cardSort, BoolCast(this.Document.cardSort_isDesc), this._docDraggedIndex); + } + + /** + * Used to determine how to sort cards based on tags. The lestmost tags are given lower values while cards to the right are + * given higher values. Decimals are used to determine placement for cards with multiple tags + * @param doc the doc whose value is being determined + * @returns its value based on its tags + */ + + tagValue = (doc: Doc) => + Doc.MyFilterHotKeys.map((key, i) => ({ has: TagItem.docHasTag(doc, StrCast(key.toolType)), i })) + .filter(({ has }) => has) + .map(({ i }) => i) + .join('.'); + + /** * Called in the sortedDocsType method. Compares the cards' value in regards to the desired sort type-- earlier cards are move to the * front, latter cards to the back * @param docs @@ -233,37 +346,46 @@ export class CollectionCardView extends CollectionSubView() { * @param isDesc * @returns */ - sort = (docs: Doc[], sortType: cardSortings, isDesc: boolean) => { - if (sortType === cardSortings.None) return docs; - docs.sort((docA, docB) => { - const [typeA, typeB] = (() => { - switch (sortType) { - case cardSortings.Time: - return [DateCast(docA.author_date)?.date ?? Date.now(), - DateCast(docB.author_date)?.date ?? Date.now()]; - case cardSortings.Color: - return [DashColor(StrCast(docA.backgroundColor)).hsv().toString(), // If docA.type is undefined, use an empty string - DashColor(StrCast(docB.backgroundColor)).hsv().toString()]; // If docB.type is undefined, use an empty string - case cardSortings.Custom: - return [CollectionCardView.getButtonGroup(this.cardSort_customField, docA)??0, - CollectionCardView.getButtonGroup(this.cardSort_customField, docB)??0]; - default: return [StrCast(docA.type), // If docA.type is undefined, use an empty string - StrCast(docB.type)]; // If docB.type is undefined, use an empty string - } // prettier-ignore - })(); - - const out = typeA < typeB ? -1 : typeA > typeB ? 1 : 0; - return isDesc ? -out : out; // Reverse the sort order if descending is true - }); + sort = (docsIn: Doc[], sortType: cardSortings, isDesc: boolean, dragIndex: number) => { + const docs = docsIn.slice(); // need make new object list since sort() modifies the incoming list which confuses mobx caching + sortType && + docs.sort((docA, docB) => { + const [typeA, typeB] = (() => { + switch (sortType) { + case cardSortings.Time: + return [DateCast(docA.author_date)?.date ?? Date.now(), DateCast(docB.author_date)?.date ?? Date.now()]; + case cardSortings.Color: { + const d1 = DashColor(StrCast(docA.backgroundColor)); + const d2 = DashColor(StrCast(docB.backgroundColor)); + return [d1.hsv().hue(), d2.hsv().hue()]; + } + case cardSortings.Tag: + return [this.tagValue(docA) ?? 9999, this.tagValue(docB) ?? 9999]; + case cardSortings.Chat: + return [NumCast(docA.chatIndex) ?? 9999, NumCast(docB.chatIndex) ?? 9999]; + default: + return [StrCast(docA.type), StrCast(docB.type)]; + } + })(); + + const out = typeA < typeB ? -1 : typeA > typeB ? 1 : 0; + return isDesc ? out : -out; + }); + if (dragIndex !== -1) { + const draggedDoc = DragManager.docsBeingDragged[0]; + const originalIndex = docs.findIndex(doc => doc === draggedDoc); + + originalIndex !== -1 && docs.splice(originalIndex, 1); + draggedDoc && docs.splice(dragIndex, 0, draggedDoc); + } return docs; }; displayDoc = (doc: Doc, screenToLocalTransform: () => Transform) => ( <DocumentView - // eslint-disable-next-line react/jsx-props-no-spreading {...this._props} - ref={action((r: DocumentView) => r?.ContentDiv && this._docRefs.set(doc, r))} + ref={action((r: DocumentView) => (!r?.ContentDiv ? this._docRefs.delete(doc) : this._docRefs.set(doc, r)))} Document={doc} NativeWidth={returnZero} NativeHeight={returnZero} @@ -272,11 +394,16 @@ export class CollectionCardView extends CollectionSubView() { renderDepth={this._props.renderDepth + 1} LayoutTemplate={this._props.childLayoutTemplate} LayoutTemplateString={this._props.childLayoutString} + containerViewPath={this.childContainerViewPath} ScreenToLocalTransform={screenToLocalTransform} // makes sure the box wrapper thing is in the right spot - isContentActive={this.isChildContentActive} + isContentActive={emptyFunction} isDocumentActive={this._props.childDocumentsActive?.() || this.Document._childDocumentsActive ? this._props.isDocumentActive : this.isContentActive} - PanelWidth={this.panelWidth} - PanelHeight={this.panelHeight(doc)} + PanelWidth={this.childPanelWidth} + PanelHeight={this.childPanelHeight} + dontCenter="y" // Don't center it vertically, because the grid it's in is already doing that and we don't want to do it twice. + dragAction={(this.Document.childDragAction ?? this._props.childDragAction) as dropActionType} + showTags={true} + dontHideOnDrag /> ); @@ -286,24 +413,24 @@ export class CollectionCardView extends CollectionSubView() { * @returns */ overflowAmCardsCalc = (index: number) => { - if (this.inactiveDocs().length < this._maxRowCount) { - return this.inactiveDocs().length; + if (this.sortedDocs.length < this._maxRowCount) { + return this.sortedDocs.length; } // 13 - 3 = 10 - const totalCards = this.inactiveDocs().length; + const totalCards = this.sortedDocs.length; // if 9 or less - if (index < totalCards - (totalCards % 10)) { + if (index < totalCards - (totalCards % this._maxRowCount)) { return this._maxRowCount; } // (3) - return totalCards % 10; + return totalCards % this._maxRowCount; }; /** * Determines the index a card is in in a row * @param realIndex * @returns */ - overflowIndexCalc = (realIndex: number) => realIndex % 10; + overflowIndexCalc = (realIndex: number) => realIndex % this._maxRowCount; /** * Translates the cards in the second rows and beyond over to the right * @param realIndex @@ -311,7 +438,7 @@ export class CollectionCardView extends CollectionSubView() { * @param calcRowCards * @returns */ - translateOverflowX = (realIndex: number, calcRowCards: number) => (realIndex < this._maxRowCount ? 0 : (10 - calcRowCards) * (this.panelWidth() / 2)); + translateOverflowX = (realIndex: number, calcRowCards: number) => (realIndex < this._maxRowCount ? 0 : (this._maxRowCount - calcRowCards) * (this.childPanelWidth() / 2)); /** * Determines how far to translate a card in the y direction depending on its index, whether or not its being hovered, or if it's selected @@ -323,22 +450,15 @@ export class CollectionCardView extends CollectionSubView() { * @returns */ calculateTranslateY = (isHovered: boolean, isSelected: boolean, realIndex: number, amCards: number, calcRowIndex: number) => { - if (isSelected) return 50 * this.fitContentScale; - const trans = isHovered ? this.translateHover(realIndex) : 0; - return trans + this.translateY(amCards, calcRowIndex, realIndex); + const rowHeight = (this._props.PanelHeight() * this.fitContentScale) / this.numRows; + const rowIndex = Math.trunc(realIndex / this._maxRowCount); + const rowToCenterShift = this.numRows / 2 - rowIndex; + if (isSelected) return rowToCenterShift * rowHeight - rowHeight / 2; + if (amCards == 1) return 50 * this.fitContentScale; + return this.translateY(amCards, calcRowIndex, realIndex); }; /** - * Toggles the buttons between on and off when creating custom sort groupings/changing those created by gpt - * @param childPairIndex - * @param buttonID - * @param doc - */ - toggleButton = undoable((buttonID: number, doc: Doc) => { - this.cardSort_customField && (doc[this.cardSort_customField] = buttonID); - }, 'toggle custom button'); - - /** * A list of the text content of all the child docs. RTF documents will have just their text and pdf documents will have the first 50 words. * Image documents are converted to bse64 and gpt generates a description for them. all other documents use their title. This string is * inputted into the gpt prompt to sort everything together @@ -355,6 +475,7 @@ export class CollectionCardView extends CollectionSubView() { }; const docTextPromises = this.childDocsWithoutLinks.map(async doc => { const docText = (await docToText(doc)) ?? ''; + doc.gptInputText = docText; this._textToDoc.set(docText.replace(/\n/g, ' ').trim(), doc); return `======${docText.replace(/\n/g, ' ').trim()}======`; }); @@ -377,81 +498,113 @@ export class CollectionCardView extends CollectionSubView() { image[DocData].description = response.trim(); return response; // Return the response from gptImageLabel } catch (error) { - console.log('bad things have happened'); + console.log(error); } return ''; }; /** - * Converts the gpt output into a hashmap that can be used for sorting. lists are seperated by ==== while elements within the list are seperated by ~~~~~~ + * Processes gpt's output depending on the type of question the user asked. Converts gpt's string output to + * usable code * @param gptOutput */ - processGptOutput = (gptOutput: string) => { + @action + processGptOutput = undoable((gptOutput: string, questionType: string, tag?: string) => { // Split the string into individual list items const listItems = gptOutput.split('======').filter(item => item.trim() !== ''); + + if (questionType === '2' || questionType === '4') { + this.childDocs.forEach(d => { + d.chatFilter = false; + }); + } + + if (questionType === '6') { + this.Document.cardSort = 'chat'; + } + listItems.forEach((item, index) => { - // Split the item by '~~~~~~' to get all descriptors - const parts = item.split('~~~~~~').map(part => part.trim()); - - parts.forEach(part => { - // Find the corresponding Doc in the textToDoc map - const doc = this._textToDoc.get(part); - if (doc) { - doc.chat = index; + const normalizedItem = item.trim(); + // find the corresponding Doc in the textToDoc map + const doc = this._textToDoc.get(normalizedItem); + + if (doc) { + switch (questionType) { + case '6': + doc.chatIndex = index; + break; + case '1': { + const allHotKeys = Doc.MyFilterHotKeys; + + let myTag = ''; + + if (tag) { + for (let i = 0; i < allHotKeys.length; i++) { + // bcz: CHECK THIS CODE OUT -- SOMETHING CHANGED + const keyTag = StrCast(allHotKeys[i].toolType); + if (tag.includes(keyTag)) { + myTag = keyTag; + break; + } + } + + if (myTag != '') { + doc[myTag] = true; + } + } + break; + } + case '2': + case '4': + doc.chatFilter = true; + Doc.setDocFilter(DocCast(this.Document.embedContainer), 'chatFilter', true, 'match'); + break; } - }); + } else { + console.warn(`No matching document found for item: ${normalizedItem}`); + } }); - }; + }, ''); + /** * Opens up the chat popup and starts the process for smart sorting. */ openChatPopup = async () => { GPTPopup.Instance.setVisible(true); - GPTPopup.Instance.setMode(GPTPopupMode.SORT); - const sortDesc = await this.childPairStringList(); // Await the promise to get the string result + GPTPopup.Instance.setMode(GPTPopupMode.CARD); GPTPopup.Instance.setCardsDoneLoading(true); // Set dataDoneLoading to true after data is loaded - GPTPopup.Instance.setSortDesc(sortDesc.join()); - GPTPopup.Instance.onSortComplete = (sortResult: string) => this.processGptOutput(sortResult); + await this.childPairStringListAndUpdateSortDesc(); }; /** - * Renders the buttons to customize sorting depending on which group the card belongs to and the amount of total groups - * @param childPairIndex - * @param doc - * @returns - */ - renderButtons = (doc: Doc, cardSort: cardSortings) => { - if (cardSort !== cardSortings.Custom) return ''; - const amButtons = Math.max(4, this.childDocs?.reduce((set, d) => this.cardSort_customField && set.add(NumCast(d[this.cardSort_customField])), new Set<number>()).size ?? 0); - const activeButtonIndex = CollectionCardView.getButtonGroup(this.cardSort_customField, doc); - const totalWidth = amButtons * 35 + amButtons * 2 * 5 + 6; - return ( - <div className="card-button-container" style={{ width: `${totalWidth}px` }}> - {numberRange(amButtons).map(i => ( - <button - key={i} - type="button" - style={{ backgroundColor: activeButtonIndex === i ? '#4476f7' : '#323232' }} // - onClick={() => this.toggleButton(i, doc)} - /> - ))} - </div> - ); - }; - /** * Actually renders all the cards */ - renderCards = () => { + @computed get renderCards() { + const sortedDocs = this.sortedDocs; const anySelected = this.childDocs.some(doc => DocumentView.SelectedDocs().includes(doc)); + const isEmpty = this.childDocsWithoutLinks.length === 0; + + if (isEmpty) { + return ( + <span className="no-card-span" style={{ width: ` ${this._props.PanelWidth()}px`, height: ` ${this._props.PanelHeight()}px` }}> + Sorry ! There are no cards in this group + </span> + ); + } + // Map sorted documents to their rendered components - return this.sortedDocs.map((doc, index) => { - const realIndex = this.sortedDocs.filter(sortDoc => !DocumentView.SelectedDocs().includes(sortDoc)).indexOf(doc); + return sortedDocs.map((doc, index) => { + const realIndex = sortedDocs.indexOf(doc); const calcRowIndex = this.overflowIndexCalc(realIndex); const amCards = this.overflowAmCardsCalc(realIndex); - const isSelected = DocumentView.SelectedDocs().includes(doc); + const view = DocumentView.getDocumentView(doc, this.DocumentView?.()); + const isSelected = view?.ComponentView?.isAnyChildContentActive?.() || view?.IsSelected ? true : false; const childScreenToLocal = () => { + // need to explicitly trigger an invalidation since we're reading everything from the Dom this._forceChildXf; + this._props.ScreenToLocalTransform(); + const dref = this._docRefs.get(doc); const { translateX, translateY, scale } = ClientUtils.GetScreenTransform(dref?.ContentDiv); return new Transform(-translateX + (dref?.centeringX || 0) * scale, @@ -459,41 +612,62 @@ export class CollectionCardView extends CollectionSubView() { .scale(1 / scale).rotate(!isSelected ? -this.rotate(amCards, calcRowIndex) : 0); // prettier-ignore }; + const translateIfSelected = () => { + const indexInRow = index % this._maxRowCount; + const rowIndex = Math.trunc(index / this._maxRowCount); + const rowCenterIndex = Math.min(this._maxRowCount, sortedDocs.length - rowIndex * this._maxRowCount) / 2; + return (rowCenterIndex - indexInRow) * 100 - 50; + }; + const aspect = NumCast(doc.height) / NumCast(doc.width, 1); + const vscale = Math.max(1,Math.min((this._props.PanelHeight() * 0.95 * this.fitContentScale) / (aspect * this.childPanelWidth()), + (this._props.PanelHeight() - 80) / (aspect * (this._props.PanelWidth() / 10)))); // prettier-ignore + const hscale = Math.min(this.sortedDocs.length, this._maxRowCount) / 2; // bcz: hack - the grid is divided evenly into maxRowCount cells, so the max scaling would be maxRowCount -- but making things that wide is ugly, so cap it off at half the window size return ( <div key={doc[Id]} className={`card-item${isSelected ? '-active' : anySelected ? '-inactive' : ''}`} - onPointerUp={() => { - // this turns off documentDecorations during a transition, then turns them back on afterward. - SnappingManager.SetIsResizing(this.Document[Id]); - setTimeout( - action(() => { - SnappingManager.SetIsResizing(undefined); - this._forceChildXf = !this._forceChildXf; - }), - 700 - ); - }} + onPointerUp={action(() => { + // if a card doc has just moved, or a card is selected and in front, then ignore this event + if (DocumentView.SelectedDocs().includes(doc) || this._dropped) { + this._dropped = false; + } else { + // otherwise, turn off documentDecorations becase we're in a selection transition and want to avoid artifacts. + // Turn them back on when the animation has completed and the render and backend structures are in synch + SnappingManager.SetIsResizing(doc[Id]); + setTimeout( + action(() => { + SnappingManager.SetIsResizing(undefined); + this._forceChildXf++; + }), + 600 + ); + } + })} style={{ - width: this.panelWidth(), - height: 'max-content', // this.panelHeight(childPair.layout)(), + width: this.childPanelWidth(), + height: 'max-content', transform: `translateY(${this.calculateTranslateY(this._hoveredNodeIndex === index, isSelected, realIndex, amCards, calcRowIndex)}px) - translateX(${isSelected ? this.translateSelected(calcRowIndex) : this.translateOverflowX(realIndex, amCards)}px) + translateX(calc(${isSelected ? translateIfSelected() : 0}% + ${this.translateOverflowX(realIndex, amCards)}px)) rotate(${!isSelected ? this.rotate(amCards, calcRowIndex) : 0}deg) - scale(${isSelected ? 1.25 : 1})`, - }} - onMouseEnter={() => this.setHoveredNodeIndex(index)}> + scale(${isSelected ? `${Math.min(hscale, vscale) * 100}%` : this._hoveredNodeIndex === index ? 1.05 : 1})`, + }} // prettier-ignore + onPointerEnter={() => !SnappingManager.IsDragging && this.setHoveredNodeIndex(index)}> {this.displayDoc(doc, childScreenToLocal)} - {this.renderButtons(doc, this.cardSort)} </div> ); }); - }; + } + render() { + const isEmpty = this.childDocsWithoutLinks.length === 0; + return ( <div className="collectionCardView-outer" - ref={this.createDashEventsTarget} + ref={(ele: HTMLDivElement | null) => this.createDashEventsTarget(ele)} + onPointerLeave={action(() => (this._docDraggedIndex = -1))} + onPointerMove={e => this.onPointerMove(...this._props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY))} + onDrop={this.onExternalDrop.bind(this)} style={{ background: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor) as string, color: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string, @@ -501,11 +675,12 @@ export class CollectionCardView extends CollectionSubView() { <div className="card-wrapper" style={{ - transform: ` scale(${1 / this.fitContentScale}) translateX(${this.translateWrapperX}px)`, - height: `${100 * this.fitContentScale}%`, + ...(!isEmpty && { transform: `scale(${1 / this.fitContentScale})` }), + ...(!isEmpty && { height: `${100 * this.fitContentScale}%` }), + gridAutoRows: `${100 / this.numRows}%`, }} onMouseLeave={() => this.setHoveredNodeIndex(-1)}> - {this.renderCards()} + {this.renderCards} </div> </div> ); diff --git a/src/client/views/collections/CollectionCarousel3DView.tsx b/src/client/views/collections/CollectionCarousel3DView.tsx index 54cc02825..139aebb02 100644 --- a/src/client/views/collections/CollectionCarousel3DView.tsx +++ b/src/client/views/collections/CollectionCarousel3DView.tsx @@ -14,6 +14,7 @@ import { DocumentView } from '../nodes/DocumentView'; import { FocusViewOptions } from '../nodes/FocusViewOptions'; import './CollectionCarousel3DView.scss'; import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView'; +import { Transform } from '../../util/Transform'; // eslint-disable-next-line @typescript-eslint/no-var-requires const { CAROUSEL3D_CENTER_SCALE, CAROUSEL3D_SIDE_SCALE, CAROUSEL3D_TOP } = require('../global/globalCssVariables.module.scss'); @@ -46,15 +47,32 @@ export class CollectionCarousel3DView extends CollectionSubView() { } centerScale = Number(CAROUSEL3D_CENTER_SCALE); + sideScale = Number(CAROUSEL3D_SIDE_SCALE); panelWidth = () => this._props.PanelWidth() / 3; - panelHeight = () => this._props.PanelHeight() * Number(CAROUSEL3D_SIDE_SCALE); + panelHeight = () => this._props.PanelHeight() * this.sideScale; onChildDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick); isContentActive = () => this._props.isSelected() || this._props.isContentActive() || this._props.isAnyChildContentActive(); isChildContentActive = () => !!this.isContentActive(); - childScreenToLocal = () => - this._props // document's left is the panel shifted by the doc's index * panelWidth/#docs. But it scales by centerScale around its center, so it's left moves left by the distance of the left from the center (panelwidth/2) * the scale delta (centerScale-1) - .ScreenToLocalTransform() // the top behaves the same way ecept it's shifted by the 'top' amount specified for the panel in css and then by the scale factor. - .translate(-this.panelWidth() + ((this.centerScale - 1) * this.panelWidth()) / 2, -((Number(CAROUSEL3D_TOP) / 100) * this._props.PanelHeight()) + ((this.centerScale - 1) * this.panelHeight()) / 2) + childScreenLeftToLocal = () => + this._props + .ScreenToLocalTransform() + .scale(this._props.NativeDimScaling?.() || 1) + .translate(-(this.panelWidth() - this.panelWidth() * this.sideScale) / 2, -(this.panelHeight() - this.panelHeight() * this.sideScale) / 2 - (Number(CAROUSEL3D_TOP) / 100) * this._props.PanelHeight()) + .scale(1 / this.sideScale); + childScreenRightToLocal = () => + this._props + .ScreenToLocalTransform() + .scale(this._props.NativeDimScaling?.() || 1) + .translate(-2 * this.panelWidth() - (this.panelWidth() - this.panelWidth() * this.sideScale) / 2, -(this.panelHeight() - this.panelHeight() * this.sideScale) / 2 - (Number(CAROUSEL3D_TOP) / 100) * this._props.PanelHeight()) + .scale(1 / this.sideScale); + childCenterScreenToLocal = () => + this._props + .ScreenToLocalTransform() + .scale(this._props.NativeDimScaling?.() || 1) + .translate( + -this.panelWidth() + ((this.centerScale - 1) * this.panelWidth()) / 2, // Focused Doc is shifted right by 1/3 panel width then left by increased size percent of center * 1/2 * panel width / 3 + -((Number(CAROUSEL3D_TOP) / 100) * this._props.PanelHeight()) + ((this.centerScale - 1) * this.panelHeight()) / 2 + ) // top is top margin % of panelHeight - increased size percent of center * panelHeight / 2 .scale(1 / this.centerScale); focus = (anchor: Doc, options: FocusViewOptions): Opt<number> => { @@ -66,9 +84,10 @@ export class CollectionCarousel3DView extends CollectionSubView() { index !== -1 && (this.layoutDoc._carousel_index = index); return undefined; }; + @computed get content() { const currentIndex = NumCast(this.layoutDoc._carousel_index); - const displayDoc = (childPair: { layout: Doc; data: Doc }) => ( + const displayDoc = (childPair: { layout: Doc; data: Doc }, dxf: () => Transform) => ( <DocumentView // eslint-disable-next-line react/jsx-props-no-spreading {...this._props} @@ -84,7 +103,7 @@ export class CollectionCarousel3DView extends CollectionSubView() { LayoutTemplate={this._props.childLayoutTemplate} LayoutTemplateString={this._props.childLayoutString} focus={this.focus} - ScreenToLocalTransform={this.childScreenToLocal} + ScreenToLocalTransform={dxf} isContentActive={this.isChildContentActive} isDocumentActive={this._props.childDocumentsActive?.() || this.Document._childDocumentsActive ? this._props.isDocumentActive : this.isContentActive} PanelWidth={this.panelWidth} @@ -94,7 +113,7 @@ export class CollectionCarousel3DView extends CollectionSubView() { return this.carouselItems.map((childPair, index) => ( <div key={childPair.layout[Id]} className={`collectionCarousel3DView-item${index === currentIndex ? '-active' : ''} ${index}`} style={{ width: this.panelWidth() }}> - {displayDoc(childPair)} + {displayDoc(childPair, index < currentIndex ? this.childScreenLeftToLocal : index === currentIndex ? this.childCenterScreenToLocal : this.childScreenRightToLocal)} </div> )); } diff --git a/src/client/views/collections/CollectionCarouselView.scss b/src/client/views/collections/CollectionCarouselView.scss index c40f471d6..af617254a 100644 --- a/src/client/views/collections/CollectionCarouselView.scss +++ b/src/client/views/collections/CollectionCarouselView.scss @@ -85,7 +85,6 @@ .carouselView-quiz { position: relative; display: flex; - flex-direction: column; height: 20px; align-items: center; margin: auto; @@ -127,7 +126,7 @@ align-items: center; display: flex; top: 2px; - right: 2px; + left: 2px; width: 30; border-radius: 5px; color: rgba(255, 255, 255, 0.5); diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx index 04a0320fc..108cdbdb4 100644 --- a/src/client/views/collections/CollectionCarouselView.tsx +++ b/src/client/views/collections/CollectionCarouselView.tsx @@ -1,11 +1,10 @@ -/* eslint-disable react/jsx-props-no-spreading */ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; import { IReactionDisposer, action, computed, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { StopEvent, returnOne, returnZero } from '../../../ClientUtils'; -import { Doc, Opt } from '../../../fields/Doc'; +import { Doc, DocListCast, Opt } from '../../../fields/Doc'; import { BoolCast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; import { DocumentType } from '../../documents/DocumentTypes'; import { Docs } from '../../documents/Documents'; @@ -16,6 +15,8 @@ import { FieldViewProps } from '../nodes/FieldView'; import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox'; import './CollectionCarouselView.scss'; import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView'; +import { TagItem } from '../TagsView'; +import { tickStep } from 'd3'; enum cardMode { STAR = 'star', @@ -34,7 +35,7 @@ export class CollectionCarouselView extends CollectionSubView() { private _dropDisposer?: DragManager.DragDropDisposer; get practiceField() { return this.fieldKey + "_practice"; } // prettier-ignore get sideField() { return "_" + this.fieldKey + "_usePath"; } // prettier-ignore - get starField() { return "star"; } // prettier-ignore + get starField() { return "#star"; } // prettier-ignore _fadeTimer: NodeJS.Timeout | undefined; @@ -63,7 +64,7 @@ export class CollectionCarouselView extends CollectionSubView() { @computed get practiceMessage() { const cardCount = this.carouselItems.length; if (this.practiceMode) { - if (!Doc.hasDocFilter(this.layoutDoc, 'star') && !cardCount) { + if (!Doc.hasDocFilter(this.layoutDoc, 'tags', Doc.FilterAny) && !cardCount) { return 'Finished! Click here to view all flashcards.'; } } @@ -73,8 +74,8 @@ export class CollectionCarouselView extends CollectionSubView() { @computed get filterMessage() { const cardCount = this.carouselItems.length; if (!this.practiceMessage) { - if (Doc.hasDocFilter(this.layoutDoc, 'star') && !cardCount) { - return 'No starred items. Click here to view all flash cards.'; + if (Doc.hasDocFilter(this.layoutDoc, 'tags', Doc.FilterAny) && !cardCount) { + return 'No tagged items. Click here to view all flash cards.'; } if (this.practiceMode) { if (!cardCount) return 'No flashcards to show! Click here to leave practice mode'; @@ -120,7 +121,10 @@ export class CollectionCarouselView extends CollectionSubView() { toggleStar = (e: React.MouseEvent) => { e.stopPropagation(); const curDoc = this.carouselItems[this.carouselIndex]; - curDoc && (curDoc[this.starField] = curDoc[this.starField] ? undefined : true); + if (curDoc) { + if (TagItem.docHasTag(curDoc, this.starField)) TagItem.removeTagFromDoc(curDoc, this.starField); + else TagItem.addTagToDoc(curDoc, this.starField); + } }; /* @@ -165,6 +169,8 @@ export class CollectionCarouselView extends CollectionSubView() { ? false : undefined; + childScreenToLocalXf = () => this._props.ScreenToLocalTransform().scale(this._props.NativeDimScaling?.() || 1); + renderDoc = (doc: Doc, showCaptions: boolean, overlayFunc?: (r: DocumentView | null) => void) => { return ( <DocumentView @@ -174,6 +180,8 @@ export class CollectionCarouselView extends CollectionSubView() { NativeWidth={returnZero} NativeHeight={returnZero} fitWidth={undefined} + showTags={true} + hideFilterStatus={true} containerViewPath={this.childContainerViewPath} setContentViewBox={undefined} onDoubleClickScript={this.onContentDoubleClick} @@ -261,11 +269,11 @@ export class CollectionCarouselView extends CollectionSubView() { return ( <> <div> - <Tooltip title="star"> - <div key="star" className="carouselView-star" onClick={this.toggleStar}> - <FontAwesomeIcon icon="star" color={this.carouselItems[this.carouselIndex]?.[this.starField] ? 'yellow' : 'gray'} size="1x" /> + {/* <Tooltip title="add new flashcard to pile"> + <div key="add" className="carouselView-add" onClick={this.addFlashcard}> + <FontAwesomeIcon icon="plus" color={this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)].layout[this.starField] ? 'yellow' : 'gray'} size="1x" /> </div> - </Tooltip> + </Tooltip> */} </div> <div key="back" className="carouselView-back" onClick={this.goback}> <FontAwesomeIcon icon="chevron-left" size="2x" /> @@ -292,18 +300,48 @@ export class CollectionCarouselView extends CollectionSubView() { } togglePracticeMode = (mode: practiceMode) => this.setPracticeMode(mode === this.practiceMode ? undefined : mode); - toggleFilterMode = () => Doc.setDocFilter(this.Document, 'star', true, 'match', true); - setColor = (mode: practiceMode | cardMode, which: string) => { return which === mode ? 'white' : 'light gray'}; //prettier-ignore + toggleFilterMode = () => Doc.setDocFilter(this.Document, 'tags', this.starField, 'check', true); + setColor = (mode: practiceMode | cardMode, which: string) => (which === mode ? 'white' : 'light gray'); + @computed get filterDoc() { + return DocListCast(Doc.MyContextMenuBtns.data).find(doc => doc.title === 'Filter'); + } + filterHeight = () => NumCast(this.filterDoc?.height); + filterWidth = () => (!this.filterDoc ? 1 : (this.filterHeight() * NumCast(this.filterDoc._width)) / NumCast(this.filterDoc._height)); @computed get menu() { const curDoc = this.carouselItems?.[this.carouselIndex]; return ( <div className="carouselView-menu"> - <Tooltip title={Doc.hasDocFilter(this.Document, 'star') ? 'Show all cards' : 'Show only starred cards'}> - <div key="back" className="carouselView-starFilter" onClick={e => this.toggleFilterMode()}> - <FontAwesomeIcon icon="filter" color={Doc.hasDocFilter(this.Document, 'star') ? 'white' : 'lightgray'} size="1x" /> + {!this.filterDoc ? null : ( + <div style={{ height: this.filterHeight(), width: this.filterHeight() }}> + <DocumentView + {...this._props} + Document={this.filterDoc} + TemplateDataDocument={undefined} + LayoutTemplate={this._props.childLayoutTemplate} + LayoutTemplateString={this._props.childLayoutString} + renderDepth={this._props.renderDepth + 1} + NativeWidth={returnZero} + NativeHeight={returnZero} + fitWidth={undefined} + showTags={false} + hideFilterStatus={true} + containerViewPath={this.childContainerViewPath} + setContentViewBox={undefined} + onDoubleClickScript={this.onContentDoubleClick} + onClickScript={this.onContentClick} + isDocumentActive={this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this._props.isContentActive} + isContentActive={this.isChildContentActive} + hideCaptions={true} + childFilters={this.childDocFilters} + hideDecorations={BoolCast(this.layoutDoc.layout_hideDecorations)} + addDocument={this._props.addDocument} + ScreenToLocalTransform={this.contentScreentToLocalXf} + PanelWidth={this.filterWidth} + PanelHeight={this.filterHeight} + /> </div> - </Tooltip> + )} <div className="carouselView-practiceModes" style={{ display: BoolCast(curDoc?._layout_isFlashcard) ? undefined : 'none' }}> <Tooltip title="Practice flashcards using GPT"> <div key="back" className="carouselView-quiz" onClick={e => this.togglePracticeMode(practiceMode.QUIZ)}> @@ -322,27 +360,33 @@ export class CollectionCarouselView extends CollectionSubView() { render() { return ( - <div - className="collectionCarouselView-outer" - ref={this.createDashEventsTarget} - style={{ - background: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor) as string, - color: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string, - }}> - {!this.practiceMessage && !this.filterMessage ? ( - this.content - ) : ( - <p - className="message" - onClick={e => { - if (this.filterMessage || this.practiceMessage) { - this.setPracticeMode(undefined); - Doc.setDocFilter(this.layoutDoc, 'star', undefined, 'remove'); - } - }}> - {this.filterMessage || this.practiceMessage} - </p> - )} + <div> + <div + className="collectionCarouselView-outer" + ref={this.createDashEventsTarget} + style={{ + background: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor) as string, + color: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string, + width: `calc(100% - ${NumCast(this.layoutDoc._xMargin)}px)`, + height: `calc(100% - ${NumCast(this.layoutDoc._yMargin)}px)`, + left: NumCast(this.layoutDoc._xMargin), + top: NumCast(this.layoutDoc._yMargin), + }}> + {!this.practiceMessage && !this.filterMessage ? ( + this.content + ) : ( + <p + className="message" + onClick={e => { + if (this.filterMessage || this.practiceMessage) { + this.setPracticeMode(undefined); + Doc.setDocFilter(this.layoutDoc, 'tags', Doc.FilterAny, 'remove'); + } + }}> + {this.filterMessage || this.practiceMessage} + </p> + )} + </div> {!this.Document._chromeHidden ? this.menu : null} {!this.Document._chromeHidden ? this.buttons : null} </div> diff --git a/src/client/views/collections/CollectionNoteTakingView.tsx b/src/client/views/collections/CollectionNoteTakingView.tsx index e1f0a3e41..ac8e37358 100644 --- a/src/client/views/collections/CollectionNoteTakingView.tsx +++ b/src/client/views/collections/CollectionNoteTakingView.tsx @@ -653,7 +653,6 @@ export class CollectionNoteTakingView extends CollectionSubView() { const sections = Array.from(this.Sections.entries()); return sections.reduce((list, sec, i) => { list.push(this.sectionNoteTaking(sec[0], sec[1])); - // eslint-disable-next-line react/no-array-index-key i !== sections.length - 1 && list.push(<CollectionNoteTakingViewDivider key={`divider${i}`} isContentActive={this.isContentActive} index={i} setColumnStartXCoords={this.setColumnStartXCoords} xMargin={this.xMargin} />); return list; }, [] as JSX.Element[]); diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index e97ee713e..1ac0b6d70 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -1,4 +1,3 @@ -/* eslint-disable react/jsx-props-no-spreading */ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import * as CSS from 'csstype'; import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction, runInAction } from 'mobx'; @@ -540,7 +539,6 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection if (this.pivotField) { const types = docList.length ? docList.map(d => typeof d[key]) : this.filteredChildren.map(d => typeof d[key]); if (types.map((i, idx) => types.indexOf(i) === idx).length === 1) { - // eslint-disable-next-line prefer-destructuring type = types[0]; } } @@ -577,7 +575,6 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection let type: 'string' | 'number' | 'bigint' | 'boolean' | 'symbol' | 'undefined' | 'object' | 'function' | undefined; const types = docList.length ? docList.map(d => typeof d[key]) : this.filteredChildren.map(d => typeof d[key]); if (types.map((i, idx) => types.indexOf(i) === idx).length === 1) { - // eslint-disable-next-line prefer-destructuring type = types[0]; } const rows = () => (!this.isStackingView ? 1 : Math.max(1, Math.min(docList.length, Math.floor((this._props.PanelWidth() - 2 * this.xMargin) / (this.columnWidth + this.gridGap))))); diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 6aca8f2ca..5d32482c3 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -122,6 +122,9 @@ export function CollectionSubView<X>() { ); return validPairs.map(({ data, layout }) => ({ data: data as Doc, layout: layout! })); // this mapping is a bit of a hack to coerce types } + /** + * This is the raw, stored list of children on a collection. If you modify this list, the database will be updated + */ @computed get childDocList() { return Cast(this.dataField, listSpec(Doc)); } @@ -218,7 +221,6 @@ export function CollectionSubView<X>() { if (!cursors) { proto.cursors = cursors = new List<CursorField>(); } - // eslint-disable-next-line no-cond-assign if (cursors.length > 0 && (ind = cursors.findIndex(entry => entry.data.metadata.id === id)) > -1) { cursors[ind].setPosition(pos); } else { @@ -251,7 +253,7 @@ export function CollectionSubView<X>() { protected onInternalDrop(e: Event, de: DragManager.DropEvent): boolean { const { docDragData } = de.complete; - if (docDragData) { + if (docDragData && !docDragData.draggedDocuments.includes(this.Document)) { let added; const dropAction = docDragData.dropAction || docDragData.userDropAction; const targetDocments = DocListCast(this.dataDoc[this._props.fieldKey]); diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index dbf781e63..f106eba26 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1,4 +1,3 @@ -/* eslint-disable react/jsx-props-no-spreading */ import { Bezier } from 'bezier-js'; import { Colors } from 'browndash-components'; import { Property } from 'csstype'; @@ -1211,7 +1210,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection for (let j = 0; j < otherCtrlPts.length - 3; j += 4) { const neighboringSegment = i === j || i === j - 4 || i === j + 4; // Ensuring that the curve intersected by the eraser is not checked for further ink intersections. - // eslint-disable-next-line no-continue if (ink?.Document === otherInk.Document && neighboringSegment) continue; const otherCurve = new Bezier(otherCtrlPts.slice(j, j + 4).map(p => ({ x: p.X, y: p.Y }))); @@ -1481,8 +1479,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection const childData = entry.pair.data; return ( <CollectionFreeFormDocumentView - // eslint-disable-next-line react/jsx-props-no-spreading, @typescript-eslint/no-explicit-any - {...(OmitKeys(entry, ['replica', 'pair']).omit as any)} + {...(OmitKeys(entry, ['replica', 'pair']).omit as { x: number; y: number; z: number; width: number; height: number })} key={childLayout[Id] + (entry.replica || '')} Document={childLayout} reactParent={this} @@ -1786,7 +1783,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection if (!code.includes('dashDiv')) { const script = CompileScript(code, { params: { docView: 'any' }, typecheck: false, editable: true }); if (script.compiled) script.run({ this: this.DocumentView?.() }); - // eslint-disable-next-line no-eval } else code && !first && eval?.(code); }, { fireImmediately: true } |
