From 6900008b1bb89cca1eab7b95f17ee33fa335282f Mon Sep 17 00:00:00 2001 From: sharkiecodes Date: Mon, 28 Apr 2025 20:18:26 -0400 Subject: adding autotagging --- src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/client/views/collections') diff --git a/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx b/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx index ff9fb14e7..28d3ccd48 100644 --- a/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx +++ b/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx @@ -158,7 +158,7 @@ export class ImageLabelBox extends ViewBoxBaseComponent() { this._currentLabel = e.target.value; }); - classifyImagesInBox = async () => { + classifyImagesInBox = async (prompt? : string) => { this.startLoading(); // Converts the images into a Base64 format, afterwhich the information is sent to GPT to label them. @@ -168,7 +168,7 @@ export class ImageLabelBox extends ViewBoxBaseComponent() { const url = ImageCastWithSuffix(doc[Doc.LayoutDataKey(doc)], '_o') ?? ''; return imageUrlToBase64(url).then(hrefBase64 => !hrefBase64 ? undefined : - gptImageLabel(hrefBase64,'Give three labels to describe this image.').then(labels => + gptImageLabel(hrefBase64, prompt ?? 'Give three labels to describe this image.').then(labels => ({ doc, labels }))) ; // prettier-ignore } }); -- cgit v1.2.3-70-g09d2 From 2a36216359054532084be24ab9034cd08c1c8798 Mon Sep 17 00:00:00 2001 From: sharkiecodes Date: Mon, 28 Apr 2025 21:13:31 -0400 Subject: working on autotagging --- src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx | 6 ++++-- src/client/views/nodes/ImageBox.tsx | 6 +++++- 2 files changed, 9 insertions(+), 3 deletions(-) (limited to 'src/client/views/collections') diff --git a/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx b/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx index 28d3ccd48..bf674c025 100644 --- a/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx +++ b/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx @@ -158,12 +158,14 @@ export class ImageLabelBox extends ViewBoxBaseComponent() { this._currentLabel = e.target.value; }); - classifyImagesInBox = async (prompt? : string) => { + classifyImagesInBox = async (selectedImages? : Doc[], prompt? : string) => { this.startLoading(); + selectedImages ??= this._selectedImages; + // Converts the images into a Base64 format, afterwhich the information is sent to GPT to label them. - const imageInfos = this._selectedImages.map(async doc => { + const imageInfos = selectedImages.map(async doc => { if (!doc.$tags_chat) { const url = ImageCastWithSuffix(doc[Doc.LayoutDataKey(doc)], '_o') ?? ''; return imageUrlToBase64(url).then(hrefBase64 => diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 87168d17d..6e6ca3f73 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -46,6 +46,7 @@ import { FieldView, FieldViewProps } from './FieldView'; import { FocusViewOptions } from './FocusViewOptions'; import './ImageBox.scss'; import { OpenWhere } from './OpenWhere'; +import { ImageLabelBox } from '../collections/collectionFreeForm/ImageLabelBox'; const DefaultPath = '/assets/unknown-file-icon-hi.png'; export class ImageEditorData { @@ -120,7 +121,10 @@ export class ImageBox extends ViewBoxAnnotatableComponent() { ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.Document)); }; - autoTag = () => { + autoTag = async () => { + ImageLabelBox.Instance.classifyImagesInBox([this.Document], "Classify this image as a PERSON or LANDSCAPE. You may only respond with one of these two options"); + + //Doc.getDescription(this.Document).then(desc => this.desc = desc) } -- cgit v1.2.3-70-g09d2 From 609ec6d37b5abf94f3ab84784544ebe47804cdf5 Mon Sep 17 00:00:00 2001 From: sharkiecodes Date: Tue, 29 Apr 2025 23:01:11 -0400 Subject: Autotag --- src/client/apis/gpt/GPT.ts | 35 +++++++++++---- .../collectionFreeForm/ImageLabelBox.tsx | 2 +- src/client/views/nodes/ImageBox.tsx | 48 ++++++++++++++++++--- .../views/nodes/formattedText/FormattedTextBox.tsx | 18 ++++++++ src/client/views/nodes/scrapbook/ScrapbookBox.tsx | 50 ++++++++++++++++++++-- src/client/views/search/FaceRecognitionHandler.tsx | 4 +- 6 files changed, 137 insertions(+), 20 deletions(-) (limited to 'src/client/views/collections') diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts index 9cb47995c..1956fef0c 100644 --- a/src/client/apis/gpt/GPT.ts +++ b/src/client/apis/gpt/GPT.ts @@ -11,6 +11,10 @@ export enum GPTDocCommand { export const DescriptionSeperator = '======'; export const DocSeperator = '------'; +export enum TextClassifications { + Title = 'title', //a few words + Caption = 'caption', //few sentences + LengthyDescription = 'lengthy description' } enum GPTCallType { SUMMARY = 'summary', @@ -36,6 +40,7 @@ enum GPTCallType { SUBSETDOCS = 'subset_docs', // select a subset of documents based on their descriptions DOCINFO = 'doc_info', // provide information about a document SORTDOCS = 'sort_docs', + CLASSIFYTEXT = 'classify_text', // classify text into one of the three categories: title, caption, lengthy description } type GPTCallOpts = { @@ -48,6 +53,23 @@ type GPTCallOpts = { const callTypeMap: { [type in GPTCallType]: GPTCallOpts } = { // newest model: gpt-4 summary: { model: 'gpt-4-turbo', maxTokens: 256, temp: 0.5, prompt: 'Summarize the text given in simpler terms.' }, + + + sort_docs: { + model: 'gpt-4o', + maxTokens: 2048, + temp: 0.25, + prompt: + `The user is going to give you a list of descriptions. + Each one is separated by '${DescriptionSeperator}' on either side. + Descriptions will vary in length, so make sure to only separate when you see '${DescriptionSeperator}'. + Sort them by the user's specifications. + Make sure each description is only in the list once. Each item should be separated by '${DescriptionSeperator}'. + Immediately afterward, surrounded by '${DocSeperator}' on BOTH SIDES, provide some insight into your reasoning for the way you sorted (and mention nothing about the formatting details given in this description). + It is VERY important that you format it exactly as described, ensuring the proper number of '${DescriptionSeperator[0]}' and '${DocSeperator[0]}' (${DescriptionSeperator.length} of each) and NO commas`, + }, + + edit: { model: 'gpt-4-turbo', maxTokens: 256, temp: 0.5, prompt: 'Reword the text.' }, stack: { model: 'gpt-4o', @@ -69,17 +91,14 @@ const callTypeMap: { [type in GPTCallType]: GPTCallOpts } = { temp: 0.5, prompt: "You are a helpful resarch assistant. Analyze the user's data to find meaningful patterns and/or correlation. Please only return a JSON with a correlation column 1 propert, a correlation column 2 property, and an analysis property. ", }, - sort_docs: { + //new + classify_text: { model: 'gpt-4o', maxTokens: 2048, temp: 0.25, - prompt: `The user is going to give you a list of descriptions. - Each one is separated by '${DescriptionSeperator}' on either side. - Descriptions will vary in length, so make sure to only separate when you see '${DescriptionSeperator}'. - Sort them by the user's specifications. - Make sure each description is only in the list once. Each item should be separated by '${DescriptionSeperator}'. - Immediately afterward, surrounded by '${DocSeperator}' on BOTH SIDES, provide some insight into your reasoning for the way you sorted (and mention nothing about the formatting details given in this description). - It is VERY important that you format it exactly as described, ensuring the proper number of '${DescriptionSeperator[0]}' and '${DocSeperator[0]}' (${DescriptionSeperator.length} of each) and NO commas`, + prompt: `Based on the content of the the text, classify it into the + most appropriate category: '${TextClassifications.Title}', '${TextClassifications.Caption}', or '${TextClassifications.LengthyDescription}'. Output exclusively the classification in your response. + ` }, describe: { model: 'gpt-4-vision-preview', maxTokens: 2048, temp: 0, prompt: 'Describe these images in 3-5 words' }, flashcard: { diff --git a/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx b/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx index bf674c025..038b1c6f9 100644 --- a/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx +++ b/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx @@ -160,7 +160,7 @@ export class ImageLabelBox extends ViewBoxBaseComponent() { classifyImagesInBox = async (selectedImages? : Doc[], prompt? : string) => { this.startLoading(); - + alert('Classifying images...'); selectedImages ??= this._selectedImages; // Converts the images into a Base64 format, afterwhich the information is sent to GPT to label them. diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 6e6ca3f73..0b6814c01 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -7,8 +7,9 @@ import { observer } from 'mobx-react'; import { extname } from 'path'; import * as React from 'react'; import { AiOutlineSend } from 'react-icons/ai'; +import { ImageLabelBoxData } from '../collections/collectionFreeForm/ImageLabelBox'; import ReactLoading from 'react-loading'; -import { ClientUtils, DashColor, returnEmptyString, returnFalse, returnOne, returnZero, setupMoveUpEvents, UpdateIcon } from '../../../ClientUtils'; +import { ClientUtils, imageUrlToBase64, DashColor, returnEmptyString, returnFalse, returnOne, returnZero, setupMoveUpEvents, UpdateIcon } from '../../../ClientUtils'; import { Doc, DocListCast, Opt } from '../../../fields/Doc'; import { DocData } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; @@ -16,7 +17,7 @@ import { InkTool } from '../../../fields/InkField'; import { List } from '../../../fields/List'; import { ObjectField } from '../../../fields/ObjectField'; import { ComputedField } from '../../../fields/ScriptField'; -import { Cast, DocCast, ImageCast, NumCast, RTFCast, StrCast } from '../../../fields/Types'; +import { Cast, DocCast, ImageCast, NumCast, RTFCast, StrCast, ImageCastWithSuffix } from '../../../fields/Types'; import { ImageField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; import { emptyFunction } from '../../../Utils'; @@ -46,6 +47,7 @@ import { FieldView, FieldViewProps } from './FieldView'; import { FocusViewOptions } from './FocusViewOptions'; import './ImageBox.scss'; import { OpenWhere } from './OpenWhere'; +import { gptImageLabel } from '../../apis/gpt/GPT'; import { ImageLabelBox } from '../collections/collectionFreeForm/ImageLabelBox'; const DefaultPath = '/assets/unknown-file-icon-hi.png'; @@ -122,11 +124,47 @@ export class ImageBox extends ViewBoxAnnotatableComponent() { }; autoTag = async () => { - ImageLabelBox.Instance.classifyImagesInBox([this.Document], "Classify this image as a PERSON or LANDSCAPE. You may only respond with one of these two options"); - - //Doc.getDescription(this.Document).then(desc => this.desc = desc) + try { + // 1) grab the full-size URL + const layoutKey = Doc.LayoutDataKey(this.Document); + const url = ImageCastWithSuffix(this.Document[layoutKey], '_o') ?? ''; + if (!url) throw new Error('No image URL found'); + + // 2) convert to base64 + const base64 = await imageUrlToBase64(url); + if (!base64) throw new Error('Failed to load image data'); + + // 3) ask GPT for exactly one label: PERSON or LANDSCAPE + const raw = await gptImageLabel( + base64, + 'Classify this image as PERSON or LANDSCAPE. You may only respond with one of these two options.' + ); + + // 4) normalize and prefix + const label = raw + .trim() + .toUpperCase() + + // 5) stash it on the Doc + // overwrite any old tags so re-runs still work + this.Document.$tags_chat = new List(); + (this.Document.$tags_chat as List).push(label); + + // 6) flip on “show tags” in the layout + // (same flag that ImageLabelBox.toggleDisplayInformation uses) + //note to self: What if i used my own field (ex: Document.$auto_description or something + //Would i still have to toggle it on for it to show in the metadata? + this.Document._layout_showTags = true; + + } catch (err) { + console.error('autoTag failed:', err); + } finally { } + }; + + //Doc.getDescription(this.Document).then(desc => this.desc = desc) + diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 98e461a52..f4cbbcc9e 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -64,6 +64,7 @@ import { removeMarkWithAttrs } from './prosemirrorPatches'; import { RichTextMenu, RichTextMenuPlugin } from './RichTextMenu'; import { RichTextRules } from './RichTextRules'; import { schema } from './schema_rts'; +import { tickStep } from 'd3'; // import * as applyDevTools from 'prosemirror-dev-tools'; export interface FormattedTextBoxProps extends FieldViewProps { @@ -304,6 +305,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { + this.Document.$tags_chat = new List(); + gptAPICall(RTFCast(this.Document[Doc.LayoutDataKey(this.Document)])?.Text ?? StrCast(this.Document[Doc.LayoutDataKey(this.Document)]), + GPTCallType.CLASSIFYTEXT).then(desc => (this.Document.$tags_chat as List).push(desc)); + this.Document._layout_showTags = true; + //or... then(desc => this.Document.$tags_chat = desc); + } + leafText = (node: Node) => { if (node.type === this.EditorView?.state.schema.nodes.dashField) { const refDoc = !node.attrs.docId ? this.rootDoc : (DocServer.GetCachedRefField(node.attrs.docId as string) as Doc); @@ -1236,6 +1245,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent Doc.IsSearchMatch(this.Document), @@ -1269,6 +1279,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent ({ title: this.Document.title, sel: this.props.isSelected() }), + action(() => { + this.autoTag(); + }), + { fireImmediately: true } + ); + if (!this._props.dontRegisterView) { this._disposers.record = reaction( () => this.recordingDictation, diff --git a/src/client/views/nodes/scrapbook/ScrapbookBox.tsx b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx index 55d0df585..39729a1c5 100644 --- a/src/client/views/nodes/scrapbook/ScrapbookBox.tsx +++ b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx @@ -1,6 +1,6 @@ import { action, makeObservable, observable } from 'mobx'; import * as React from 'react'; -import { Doc, DocListCast } from '../../../../fields/Doc'; +import { Doc, DocListCast, StrListCast } from '../../../../fields/Doc'; import { List } from '../../../../fields/List'; import { emptyFunction } from '../../../../Utils'; import { Docs } from '../../../documents/Documents'; @@ -100,20 +100,62 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent() }; filterAddDocument = (docIn: Doc | Doc[]) => { - const docs = toList(docIn); + const docs = toList(docIn); //The docs being added to the scrapbook + + // 1) Grab all template slots: + const slots = DocListCast(this.dataDoc[this.fieldKey]); + + // 2) recursive unwrap: + const unwrap = (items: Doc[]): Doc[] => + items.flatMap(d => + d.$type === DocumentType.COL + ? unwrap(DocListCast(d[Doc.LayoutDataKey(d)])) + : [d] + ); + + // 3) produce a flat list of every doc, unwrapping any number of nested COLs + const allDocs: Doc[] = unwrap(slots); + + if (docs?.length === 1) { - const placeholder = DocListCast(this.dataDoc[this.fieldKey]).find(d => + const placeholder = allDocs.filter(d => + (d.accepts_docType === docs[0].$type || // match fields based on type, or by analyzing content .. simple example of matching text in placeholder to dropped doc's type RTFCast(d[Doc.LayoutDataKey(d)])?.Text.includes(StrCast(docs[0].$type))) ); // prettier-ignore + //DocListCast(this.Document.items).map(doc => DocListCast(doc[Doc.LayoutDataKey(doc)]) + if (placeholder) { + /**Look at the autotags and see what matches*RTFCast(d[Doc.LayoutDataKey(d)])?.Text*/ // ugh. we have to tell the underlying view not to add the Doc so that we can add it where we want it. // However, returning 'false' triggers an undo. so this settimeout is needed to make the assignment happen after the undo. setTimeout( undoable(() => { + + const slotTagsList: Set[] = placeholder.map(doc => + new Set(StrListCast(doc.$tags_chat)) + ); + // turn docs[0].$tags_chat into a Set + const targetTags = new Set(StrListCast(docs[0].$tags_chat)); + //StrListCast(placeholder.overrideFields).map(field => (docs[0][field] = placeholder[field])); // // shouldn't need to do this for layout fields since the placeholder already overrides its protos - placeholder.proto = docs[0]; + + // find the first placeholder that shares *any* tag + const match = placeholder.find(ph => + StrListCast(ph.$tags_chat).some(tag => targetTags.has(tag)) + ); + if (match) { + match.proto = docs[0]; + } + + /*const chosenPlaceholder = placeholder.find(d => + pl = new Set(StrListCast(d.$tags_chat) + + d.$tags_chat && d.$tags_chat[0].equals(docs[0].$tags_chat)); //why [0] + if (chosenPlaceholder){ + chosenPlaceholder.proto = docs[0];}*/ + //excess if statement?? }, 'Scrapbook add') ); return false; diff --git a/src/client/views/search/FaceRecognitionHandler.tsx b/src/client/views/search/FaceRecognitionHandler.tsx index 60744588f..256e68afd 100644 --- a/src/client/views/search/FaceRecognitionHandler.tsx +++ b/src/client/views/search/FaceRecognitionHandler.tsx @@ -210,8 +210,8 @@ export class FaceRecognitionHandler { } else if (imgDoc.type === DocumentType.LOADING && !imgDoc.loadingError) { setTimeout(() => this.classifyFacesInImage(imgDocView), 1000); } else { - reaction(() => ({sel:imgDocView.isSelected()}), ({sel}) => !sel && imgDoc.type == 'text' - && imgDocView.ComponentView?.autoTag?.(), {fireImmediately: true} + reaction(() => ({sel:imgDocView.isSelected()}), ({sel}) => !sel && + imgDocView.ComponentView?.autoTag?.(), {fireImmediately: true} ) const imgUrl = ImageCast(imgDoc[Doc.LayoutDataKey(imgDoc)]); if (imgUrl && !DocListCast(Doc.MyFaceCollection?.examinedFaceDocs).includes(imgDoc[DocData])) { -- cgit v1.2.3-70-g09d2 From fec7f7dd736af3296a2df52acd07f3eadb8123c3 Mon Sep 17 00:00:00 2001 From: sharkiecodes Date: Thu, 1 May 2025 00:44:14 -0400 Subject: marquee selection --- src/client/apis/gpt/GPT.ts | 9 + .../collectionFreeForm/MarqueeOptionsMenu.tsx | 2 + .../collections/collectionFreeForm/MarqueeView.tsx | 35 ++++ src/client/views/nodes/scrapbook/ScrapbookBox.tsx | 231 ++++++++++++++++++++- .../views/nodes/scrapbook/ScrapbookPreset.tsx | 146 +++++++++++++ .../nodes/scrapbook/ScrapbookSettingsPanel.tsx | 60 ++++++ 6 files changed, 482 insertions(+), 1 deletion(-) create mode 100644 src/client/views/nodes/scrapbook/ScrapbookPreset.tsx create mode 100644 src/client/views/nodes/scrapbook/ScrapbookSettingsPanel.tsx (limited to 'src/client/views/collections') diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts index 1956fef0c..693b4f901 100644 --- a/src/client/apis/gpt/GPT.ts +++ b/src/client/apis/gpt/GPT.ts @@ -126,6 +126,7 @@ const callTypeMap: { [type in GPTCallType]: GPTCallOpts } = { temp: 0.5, prompt: 'You will be given a list of field descriptions for one or more templates in the format {field #0: “description”}{field #1: “description”}{...}, and a list of column descriptions in the format {“title”: “description”}{...}. Your job is to match columns with fields based on their descriptions. Your output should be in the following JSON format: {“template_title”:{“#”: “title”, “#”: “title”, “#”: “title” …}, “template_title”:{“#”: “title”, “#”: “title”, “#”: “title” …}} where “template_title” is the templates title as specified in the description provided, # represents the field # and “title” the title of the column assigned to it. A filled out example might look like {“fivefield2”:{“0”:”Name”, “1”:”Image”, “2”:”Caption”, “3”:”Position”, “4”:”Stats”}, “fivefield3”:{0:”Image”, 1:”Name”, 2:”Caption”, 3:”Stats”, 4:”Position”}. Include one object for each template. IT IS VERY IMPORTANT THAT YOU ONLY INCLUDE TEXT IN THE FORMAT ABOVE, WITH NO ADDITIONS WHATSOEVER. Do not include extraneous ‘#’ characters, ‘column’ for columns, or ‘template’ for templates: ONLY THE TITLES AND NUMBERS. There should never be one column assigned to more than one field (ie. if the “name” column is assigned to field 1, it can’t be assigned to any other fields) . Do this for each template whose fields are described. The descriptions are as follows:', }, + vizsum: { model: 'gpt-4-turbo', maxTokens: 512, @@ -157,6 +158,14 @@ const callTypeMap: { [type in GPTCallType]: GPTCallOpts } = { temp: 0.5, prompt: 'You will be coloring drawings. You will be given what the drawing is, then a list of descriptions for parts of the drawing. Based on each description, respond with the stroke and fill color that it should be. Follow the rules: 1. Avoid using black for stroke color 2. Make the stroke color 1-3 shades darker than the fill color 3. Use the same colors when possible. Format as {#abcdef #abcdef}, making sure theres a color for each description, and do not include any additional text.', }, + + scrapbook: { + model: 'gpt-4-turbo', + maxTokens: 512, + temp: 0.5, + prompt: 'You will be given a list of field descriptions for one or more templates in the format {field #0: “description”}{field #1: “description”}{...}, and a list of column descriptions in the format {“title”: “description”}{...}. Your job is to match columns with fields based on their descriptions. Your output should be in the following JSON format: {“template_title”:{“#”: “title”, “#”: “title”, “#”: “title” …}, “template_title”:{“#”: “title”, “#”: “title”, “#”: “title” …}} where “template_title” is the templates title as specified in the description provided, # represents the field # and “title” the title of the column assigned to it. A filled out example might look like {“fivefield2”:{“0”:”Name”, “1”:”Image”, “2”:”Caption”, “3”:”Position”, “4”:”Stats”}, “fivefield3”:{0:”Image”, 1:”Name”, 2:”Caption”, 3:”Stats”, 4:”Position”}. Include one object for each template. IT IS VERY IMPORTANT THAT YOU ONLY INCLUDE TEXT IN THE FORMAT ABOVE, WITH NO ADDITIONS WHATSOEVER. Do not include extraneous ‘#’ characters, ‘column’ for columns, or ‘template’ for templates: ONLY THE TITLES AND NUMBERS. There should never be one column assigned to more than one field (ie. if the “name” column is assigned to field 1, it can’t be assigned to any other fields) . Do this for each template whose fields are described. The descriptions are as follows:', + }, + command_type: { model: 'gpt-4-turbo', maxTokens: 1024, diff --git a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx index abd828945..2ec59e5d5 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx @@ -16,6 +16,7 @@ export class MarqueeOptionsMenu extends AntimodeMenu { public createCollection: (e: KeyboardEvent | React.PointerEvent | undefined, group?: boolean, selection?: Doc[]) => Doc | void = unimplementedFunction; public delete: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction; public summarize: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction; + public generateScrapbook: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction; public showMarquee: () => void = unimplementedFunction; public hideMarquee: () => void = unimplementedFunction; public pinWithView: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction; @@ -38,6 +39,7 @@ export class MarqueeOptionsMenu extends AntimodeMenu { } color={this.userColor} /> this.createCollection(e, true)} icon={} color={this.userColor} /> } color={this.userColor} /> + } color={this.userColor} /> } color={this.userColor} /> } color={this.userColor} /> } color={this.userColor} /> diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 3cc7c0f2d..f5e699d3e 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -275,6 +275,7 @@ export class MarqueeView extends ObservableReactComponent { + let docs = new Array(); + const selected = this.marqueeSelect(false).map(d => { + this._props.removeDocument?.(d); + d.x = NumCast(d.x) - this.Bounds.left; + d.y = NumCast(d.y) - this.Bounds.top; + docs.push(d); + return d; + }); + const scrapbook = Docs.Create.ScrapbookDocument(docs, { + backgroundColor: '#e2ad32', + x: this.Bounds.left, + y: this.Bounds.top, + followLinkToggle: true, + _width: 200, + _height: 200, + _layout_showSidebar: true, + title: 'overview', + }); + const portal = Docs.Create.FreeformDocument(selected, { title: 'summarized documents', x: this.Bounds.left + 200, y: this.Bounds.top, isGroup: true, backgroundColor: 'transparent' }); + DocUtils.MakeLink(scrapbook, portal, { link_relationship: 'summary of:summarized by' }); + + portal.hidden = true; + this._props.addDocument?.(portal); + //this._props.addLiveTextDocument(summary); + this._props.addDocument?.(scrapbook); + MarqueeOptionsMenu.Instance.fadeOut(true); + }); + + @action marqueeCommand = (e: KeyboardEvent) => { const ee = e as unknown as KeyboardEvent & { propagationIsStopped?: boolean }; @@ -538,6 +572,7 @@ export class MarqueeView extends ObservableReactComponent() { @observable createdDate: string; + @observable configs : ScrapbookItemConfig[] constructor(props: FieldViewProps) { super(props); makeObservable(this); this.createdDate = this.getFormattedDate(); + this.configs = + ScrapbookPreset.createPreset(presetType); + // ensure we always have a List in dataDoc['items'] if (!this.dataDoc[this.fieldKey]) { this.dataDoc[this.fieldKey] = new List(); } this.createdDate = this.getFormattedDate(); + //this.initScrapbook(ScrapbookPresetType.Default); this.setTitle(); + //this.setLayout(ScrapbookPreset.Spotlight); } public static LayoutString(fieldStr: string) { @@ -41,6 +56,188 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent() }); } + + @action + initScrapbook(presetType: ScrapbookPresetType) { + // 1) ensure title is set + const title = `Scrapbook - ${this.createdDate}`; + if (this.dataDoc.title !== title) { + this.dataDoc.title = title; + } + + // 2) build placeholders from the preset + const configs = ScrapbookPreset.createPreset(presetType); + const placeholders: Doc[] = []; + + for (const cfg of configs) { + if (cfg.children) { + // --- nested container --- + const childDocs = cfg.children.map(child => { + const doc = Docs.Create.TextDocument(child.tag); + doc.accepts_docType = child.type; + doc.accepts_tagType = child.acceptTag ?? child.tag; + + const ph = new Doc(); + ph.proto = doc; + ph.original = doc; + ph.x = child.x; + ph.y = child.y; + if (child.width != null) ph._width = child.width; + if (child.height != null) ph._height = child.height; + return ph; + }); + + const protoW = cfg.containerWidth ?? cfg.width; + const protoH = cfg.containerHeight ?? cfg.height; + const containerProto = Docs.Create.StackingDocument( + childDocs, + { + ...(protoW != null ? { _width: protoW } : {}), + ...(protoH != null ? { _height: protoH } : {}), + title: cfg.tag + } + ); + + const ph = new Doc(); + ph.proto = containerProto; + ph.original = containerProto; + ph.x = cfg.x; + ph.y = cfg.y; + if (cfg.width != null) ph._width = cfg.width; + if (cfg.height != null) ph._height = cfg.height; + placeholders.push(ph); + + } else { + // --- flat placeholder --- + const doc = Docs.Create.TextDocument(cfg.tag); + doc.accepts_docType = cfg.type; + doc.accepts_tagType = cfg.acceptTag ?? cfg.tag; + + const ph = new Doc(); + ph.proto = doc; + ph.original = doc; + ph.x = cfg.x; + ph.y = cfg.y; + if (cfg.width != null) ph._width = cfg.width; + if (cfg.height != null) ph._height = cfg.height; + placeholders.push(ph); + } + } + + // 3) commit them into the field + this.dataDoc[this.fieldKey] = new List(placeholders); + } + @action + //INACTIVE VER ignore!! not in use rn, implementation ver 1 + setLayout(preset: ScrapbookPreset) { + // helper to wrap a TextDocument proto in a Doc with positioning + function makePlaceholder( + proto: Doc, x: number, y: number, + width: number, height: number + ): Doc { + const d = new Doc(); + d.proto = proto; + d.original = proto; + d.x = x; + d.y = y; + d._width = width; + d._height = height; + return d; + } + + let placeholders: Doc[]; + + switch (preset) { + case ScrapbookPresetType.Classic: + // One large landscape image on top, caption below, sidebar at right + const imgClassic = Docs.Create.TextDocument('image'); + imgClassic.accepts_docType = DocumentType.IMG; + imgClassic.accepts_tagType = 'LANDSCAPE'; + const phImageClassic = makePlaceholder(imgClassic, 0, -120, 300, 180); + + const captionClassic = Docs.Create.TextDocument('caption'); + captionClassic.accepts_docType = DocumentType.RTF; + captionClassic.accepts_tagType = 'caption'; + const phCaptionClassic = makePlaceholder(captionClassic, 0, 80, 300, 60); + + const sidebarClassic = Docs.Create.TextDocument('sidebar'); + sidebarClassic.accepts_docType = DocumentType.RTF; + sidebarClassic.accepts_tagType = 'lengthy description'; + const phSidebarClassic = makePlaceholder(sidebarClassic, 320, -50, 80, 200); + + placeholders = [phImageClassic, phCaptionClassic, phSidebarClassic]; + break; + + case ScrapbookPresetType.Collage: + // Grid of four person images, small captions under each + const personDocs: Doc[] = []; + for (let i = 0; i < 4; i++) { + const img = Docs.Create.TextDocument(`person ${i+1}`); + img.accepts_docType = DocumentType.IMG; + img.accepts_tagType = 'PERSON'; + // position in 2x2 grid + const x = (i % 2) * 160 - 80; + const y = Math.floor(i / 2) * 160 - 80; + personDocs.push(makePlaceholder(img, x, y, 150, 120)); + + const cap = Docs.Create.TextDocument(`caption ${i+1}`); + cap.accepts_docType = DocumentType.RTF; + cap.accepts_tagType = 'caption'; + personDocs.push(makePlaceholder(cap, x, y + 70, 150, 30)); + } + placeholders = personDocs; + break; + + case ScrapbookPresetType.Spotlight: + // Full-width title, then a stacking of an internal person image + landscape, then description + const titleSpot = Docs.Create.TextDocument('title'); + titleSpot.accepts_docType = DocumentType.RTF; + titleSpot.accepts_tagType = 'title'; + const phTitleSpot = makePlaceholder(titleSpot, 0, -180, 400, 60); + + const internalImg = Docs.Create.TextDocument(''); + internalImg.accepts_docType = DocumentType.IMG; + internalImg.accepts_tagType = 'PERSON'; + const phInternal = makePlaceholder(internalImg, -100, -120, 120, 160); + + const landscapeImg = Docs.Create.TextDocument(''); + landscapeImg.accepts_docType = DocumentType.IMG; + landscapeImg.accepts_tagType = 'LANDSCAPE'; + const phLandscape = makePlaceholder(landscapeImg, 50, 0, 200, 160); + + const stack = Docs.Create.StackingDocument( + [phInternal, phLandscape], + { _width: 360, _height: 180, title: 'spotlight stack' } + ); + const phStack = (() => { + const d = new Doc(); + d.proto = stack; + d.original = stack; + d.x = 8; + d.y = -84; + d._width = 360; + d._height = 180; + return d; + })(); + + const descSpot = Docs.Create.TextDocument('description'); + descSpot.accepts_docType = DocumentType.RTF; + descSpot.accepts_tagType = 'lengthy description'; + const phDescSpot = makePlaceholder(descSpot, 0, 140, 400, 100); + + placeholders = [phTitleSpot, phStack, phDescSpot]; + break; + + default: + placeholders = []; + } + + // finally assign into the dataDoc + this.dataDoc[this.fieldKey] = new List(placeholders); + } + + + @action setTitle() { const title = `Scrapbook - ${this.createdDate}`; @@ -120,13 +317,14 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent() difference between passing a new List versus just the raw array? */ - this.dataDoc[this.fieldKey] = new List([placeholder, placeholder2, placeholder3, placeholder4]); + this.dataDoc[this.fieldKey] = this.dataDoc[this.fieldKey] ?? new List([placeholder, placeholder2, placeholder3, placeholder4]); } } componentDidMount() { + //this.initScrapbook(ScrapbookPresetType.Default); this.setTitle(); } @@ -222,6 +420,37 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent() } } + + +function extractScrapbookConfigs(docs: Doc[]): ScrapbookItemConfig[] { + return docs.map(doc => extractConfig(doc)); +} + +// function extractConfig(doc: Doc): ScrapbookItemConfig { +// const layoutKey = Doc.LayoutDataKey(doc); +// const childDocs = doc[layoutKey] ? DocListCast(doc[layoutKey]) : []; + +// const isContainer = childDocs.length > 0; + +// const cfg: ScrapbookItemConfig = { +// type: isContainer ? DocumentType.COL : doc.$type, +// tag: +// acceptTag: doc.accepts_tagType, +// x: doc.x || 0, +// y: doc.y || 0, +// width: doc._width, +// height: doc._height, +// }; + +// if (isContainer) { +// cfg.containerWidth = doc.proto._width; +// cfg.containerHeight = doc.proto._height; +// cfg.children = childDocs.map(child => extractConfig(child)); +// } + +// return cfg; +// } + // Register scrapbook Docs.Prototypes.TemplateMap.set(DocumentType.SCRAPBOOK, { layout: { view: ScrapbookBox, dataField: 'items' }, diff --git a/src/client/views/nodes/scrapbook/ScrapbookPreset.tsx b/src/client/views/nodes/scrapbook/ScrapbookPreset.tsx new file mode 100644 index 000000000..3cae4382b --- /dev/null +++ b/src/client/views/nodes/scrapbook/ScrapbookPreset.tsx @@ -0,0 +1,146 @@ +import { DocumentType } from '../../../documents/DocumentTypes'; + +export enum ScrapbookPresetType { + Default = 'Default', + Classic = 'Classic', + Collage = 'Collage', + Spotlight = 'Spotlight', +} + +export interface ScrapbookItemConfig { + type: DocumentType; + /** text shown in the placeholder bubble */ + tag: string; + /** what this slot actually accepts (defaults to `tag`) */ + acceptTag?: string; + + x: number; + y: number; + /** the frame this placeholder occupies */ + width?: number; + height?: number; + /** if this is a container with children, use these for the proto’s own size */ + containerWidth?: number; + containerHeight?: number; + children?: ScrapbookItemConfig[]; +} + +export class ScrapbookPreset { + static createPreset(presetType: ScrapbookPresetType): ScrapbookItemConfig[] { + switch (presetType) { + case ScrapbookPresetType.Classic: + return ScrapbookPreset.createClassicPreset(); + case ScrapbookPresetType.Collage: + return ScrapbookPreset.createCollagePreset(); + case ScrapbookPresetType.Spotlight: + return ScrapbookPreset.createSpotlightPreset(); + case ScrapbookPresetType.Default: + return ScrapbookPreset.createDefaultPreset(); + default: + throw new Error(`Unknown preset type: ${presetType}`); + } + } + + private static createClassicPreset(): ScrapbookItemConfig[] { + return [ + { type: DocumentType.IMG, + tag: 'LANDSCAPE', + acceptTag: 'LANDSCAPE', + x: 0, y: -100, width: 250, height: 200 + }, + { type: DocumentType.RTF, + tag: 'caption', + acceptTag: 'caption', + x: 0, y: 200, width: 250, height: 50 + }, + { type: DocumentType.RTF, + tag: 'lengthy description', + acceptTag: 'lengthy description', + x: 280, y: -50, width: 50, height: 200 + }, + { type: DocumentType.IMG, + tag: 'PERSON', + acceptTag: 'PERSON', + x: -200, y: -100, width: 100, height: 200 + }, + ]; + } + + private static createDefaultPreset(): ScrapbookItemConfig[] { + return [ + { type: DocumentType.IMG, + tag: 'image', + acceptTag: 'LANDSCAPE', + x: 0, y: -100, width: 250, height: 200 + }, + { type: DocumentType.RTF, + tag: 'summary', + acceptTag: 'caption', + x: 0, y: 200, width: 250 + }, + { type: DocumentType.RTF, + tag: 'sidebar', + acceptTag: 'lengthy description', + x: 280, y: -50, width: 50, height: 200 + }, + { + type: DocumentType.COL, + tag: 'internal coll', + x: -200, y: -100, width: 100, height: 200, + containerWidth: 300, containerHeight: 300, + children: [ + { type: DocumentType.IMG, + tag: 'image internal', + acceptTag: 'PERSON', + x: 0, y: 0, width: 50, height: 100 + } + ] + } + ]; + } + + private static createCollagePreset(): ScrapbookItemConfig[] { + return [ + { type: DocumentType.IMG, + tag: 'LANDSCAPE', + acceptTag: 'LANDSCAPE', + x: -150, y: -150, width: 150, height: 150 + }, + { type: DocumentType.IMG, + tag: 'PERSON', + acceptTag: 'PERSON', + x: 0, y: -150, width: 150, height: 150 + }, + { type: DocumentType.RTF, + tag: 'caption', + acceptTag: 'caption', + x: -150, y: 0, width: 300, height: 100 + }, + { type: DocumentType.RTF, + tag: 'lengthy description', + acceptTag: 'lengthy description', + x: 0, y: 100, width: 300, height: 100 + } + ]; + } + + private static createSpotlightPreset(): ScrapbookItemConfig[] { + return [ + { type: DocumentType.RTF, + tag: 'title', + acceptTag: 'title', + x: 0, y: -180, width: 300, height: 40 + }, + { type: DocumentType.IMG, + tag: 'LANDSCAPE', + acceptTag: 'LANDSCAPE', + x: 0, y: 0, width: 300, height: 200 + }, + { type: DocumentType.RTF, + tag: 'caption', + acceptTag: 'caption', + x: 0, y: 230, width: 300, height: 50 + } + ]; + } +} diff --git a/src/client/views/nodes/scrapbook/ScrapbookSettingsPanel.tsx b/src/client/views/nodes/scrapbook/ScrapbookSettingsPanel.tsx new file mode 100644 index 000000000..5808ab4d1 --- /dev/null +++ b/src/client/views/nodes/scrapbook/ScrapbookSettingsPanel.tsx @@ -0,0 +1,60 @@ +import React from 'react'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faRedoAlt } from '@fortawesome/free-solid-svg-icons'; +import { action } from 'mobx'; + +export default class ScrapbookSettingsPanel extends React.Component { + + constructor(props) { + super(props); + this.state = { regenerating: false }; + } + + regenerateScrapbook = async () => { + this.setState({ regenerating: true }); + try { + // Example API call or method invoking ChatGPT for JSON + const newLayout = await fetch('/api/generate-scrapbook-layout', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ currentLayout: this.props.currentLayout }) + }).then(res => res.json()); + + action(() => { + // Apply new layout + this.props.applyNewLayout(newLayout); + })(); + } catch (err) { + console.error('Failed to regenerate layout:', err); + } finally { + this.setState({ regenerating: false }); + } + }; + + render() { + const { regenerating } = this.state; + + return ( +
+ +
+ ); + } +} \ No newline at end of file -- cgit v1.2.3-70-g09d2 From 87484331f805f583c98226c5a8c6b1ec5fc88925 Mon Sep 17 00:00:00 2001 From: bobzel Date: Tue, 13 May 2025 13:37:54 -0400 Subject: fixed setting up link anchors for video to be added to Doc. fixed imageTemplate to work with templates in tabs. fixed link following from link menu. cleaned up getView with sidebars to open them when needed properly.. --- src/client/documents/DocUtils.ts | 2 +- src/client/util/DocumentManager.ts | 2 +- src/client/views/MarqueeAnnotator.tsx | 3 +- src/client/views/SidebarAnnos.tsx | 29 +++++++++-- .../collectionFreeForm/CollectionFreeFormView.tsx | 7 +-- src/client/views/linking/LinkMenuItem.tsx | 18 +++---- src/client/views/nodes/DocumentView.tsx | 2 +- src/client/views/nodes/MapBox/MapBox.tsx | 59 +++++++--------------- .../views/nodes/MapboxMapBox/MapboxContainer.tsx | 51 ++++++++++--------- src/client/views/nodes/PDFBox.tsx | 17 +++---- src/client/views/nodes/VideoBox.tsx | 17 ++++--- src/client/views/nodes/WebBox.tsx | 26 +++++----- .../views/nodes/formattedText/FormattedTextBox.tsx | 17 +++---- 13 files changed, 124 insertions(+), 126 deletions(-) (limited to 'src/client/views/collections') diff --git a/src/client/documents/DocUtils.ts b/src/client/documents/DocUtils.ts index 36e03daed..41a61b154 100644 --- a/src/client/documents/DocUtils.ts +++ b/src/client/documents/DocUtils.ts @@ -724,7 +724,7 @@ export namespace DocUtils { _width: width || BoolCast(Doc.UserDoc().fitBox) ? Number(StrCast(Doc.UserDoc().fontSize).replace('px', '')) * 1.5 * 6 : 200, _height: BoolCast(Doc.UserDoc().fitBox) ? Number(StrCast(Doc.UserDoc().fontSize).replace('px', '')) * 1.5 : 35, _layout_autoHeight: true, - backgroundColor: StrCast(Doc.UserDoc().textBackgroundColor), + backgroundColor: backgroundColor ?? StrCast(Doc.UserDoc().textBackgroundColor), borderColor: Doc.UserDoc().borderColor as string, borderWidth: Doc.UserDoc().borderWidth as number, text_centered: BoolCast(Doc.UserDoc().textCentered), diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index fc8f22cf3..422613968 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -250,7 +250,7 @@ export class DocumentManager { const options = optionsIn; Doc.MyRecentlyClosed && Doc.RemoveDocFromList(Doc.MyRecentlyClosed, undefined, targetDoc); const docContextPath = DocumentManager.GetContextPath(targetDoc, true); - if (docContextPath.some(doc => doc.hidden)) options.toggleTarget = false; + if (docContextPath.some(doc => doc !== targetDoc && doc.hidden)) options.toggleTarget = false; let activatedTab = false; if (DocumentView.activateTabView(docContextPath[0])) { options.toggleTarget = false; diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx index b2e42652d..d354dd2c0 100644 --- a/src/client/views/MarqueeAnnotator.tsx +++ b/src/client/views/MarqueeAnnotator.tsx @@ -194,14 +194,13 @@ export class MarqueeAnnotator extends ObservableReactComponent { e.preventDefault(); e.stopPropagation(); - const sourceAnchorCreator = () => this.highlight(this.props.highlightDragSrcColor ?? 'rgba(173, 216, 230, 0.75)', true, undefined, true); // hyperlink color - const targetCreator = (annotationOn: Doc | undefined) => { const target = DocUtils.GetNewTextDoc('Note linked to ' + this.props.Document.title, 0, 0, 100, 100, annotationOn, 'yellow'); target.layout_fitWidth = true; DocumentView.SetSelectOnLoad(target); return target; }; + const sourceAnchorCreator = () => this.highlight(this.props.highlightDragSrcColor ?? 'rgba(173, 216, 230, 0.75)', true, undefined, true); // hyperlink color DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(this.props.docView(), sourceAnchorCreator, targetCreator), e.pageX, e.pageY, { dragComplete: dragEv => { if (!dragEv.aborted && dragEv.annoDragData && dragEv.annoDragData.linkSourceDoc && dragEv.annoDragData.dropDocument && dragEv.linkDocument) { diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx index 573c28ccf..71b479a22 100644 --- a/src/client/views/SidebarAnnos.tsx +++ b/src/client/views/SidebarAnnos.tsx @@ -3,7 +3,7 @@ import { observer } from 'mobx-react'; import * as React from 'react'; import { ClientUtils, returnFalse, returnOne, returnZero } from '../../ClientUtils'; import { emptyFunction } from '../../Utils'; -import { Doc, DocListCast, Field, FieldResult, FieldType, StrListCast } from '../../fields/Doc'; +import { Doc, DocListCast, Field, FieldResult, FieldType, Opt, StrListCast } from '../../fields/Doc'; import { Id } from '../../fields/FieldSymbols'; import { List } from '../../fields/List'; import { RichTextField } from '../../fields/RichTextField'; @@ -19,6 +19,7 @@ import { StyleProp } from './StyleProp'; import { CollectionStackingView } from './collections/CollectionStackingView'; import { DocumentView } from './nodes/DocumentView'; import { FieldViewProps } from './nodes/FieldView'; +import { FocusViewOptions } from './nodes/FocusViewOptions'; interface ExtraProps { fieldKey: string; @@ -149,15 +150,33 @@ export class SidebarAnnos extends ObservableReactComponent { if (DocListCast(this._props.Doc[this.sidebarKey]).find(anno => Doc.AreProtosEqual(doc.layout_unrendered ? DocCast(doc.annotationOn) : doc, anno))) { - if (this.childFilters()) { + if (this.childFilters().length) { // if any child filters exist, get rid of them this._props.layoutDoc._childFilters = new List(); + return true; } - return true; } return false; }; + public static getView(sidebar: SidebarAnnos | null, sidebarShown: boolean, toggleSidebar: () => void, doc: Doc, options: FocusViewOptions) { + if (!sidebarShown) { + options.didMove = true; + toggleSidebar(); + } + options.didMove = sidebar?.makeDocUnfiltered(doc) || options.didMove; + + if (!doc.hidden) { + if (!options.didMove && options.toggleTarget) { + options.toggleTarget = false; + options.didMove = doc.hidden = true; + } + } else { + options.didMove = !(doc.hidden = false); + } + return new Promise>(res => DocumentView.addViewRenderedCb(doc, res)); + } + get sidebarKey() { return this._props.fieldKey + '_sidebar'; } @@ -181,8 +200,8 @@ export class SidebarAnnos extends ObservableReactComponent 'title'; setHeightCallback = (height: number) => this._props.setHeight?.(height + this.filtersHeight()); sortByLinkAnchorY = (a: Doc, b: Doc) => { - const ay = Doc.Links(a).length && DocCast(Doc.Links(a)[0].link_anchor_1).y; - const by = Doc.Links(b).length && DocCast(Doc.Links(b)[0].link_anchor_1).y; + const ay = Doc.Links(a).length && DocCast(Doc.Links(a)[0].link_anchor_1)?.y; + const by = Doc.Links(b).length && DocCast(Doc.Links(b)[0].link_anchor_1)?.y; return NumCast(ay) - NumCast(by); }; render() { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index bb3c59eae..77615c650 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1738,10 +1738,11 @@ export class CollectionFreeFormView extends CollectionSubView([anchor]); + this.dataDoc[fieldKey] = new List([anchor]); } } return anchor; diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx index c984a7053..6c1ad568d 100644 --- a/src/client/views/linking/LinkMenuItem.tsx +++ b/src/client/views/linking/LinkMenuItem.tsx @@ -105,11 +105,7 @@ export class LinkMenuItem extends ObservableReactComponent { LinkManager.Instance.currentLinkAnchor = LinkManager.Instance.currentLink ? this.sourceAnchor : undefined; if ((SnappingManager.PropertiesWidth ?? 0) < 100) { - setTimeout( - action(() => { - SnappingManager.SetPropertiesWidth(250); - }) - ); + setTimeout(action(() => SnappingManager.SetPropertiesWidth(250))); } } }) @@ -136,14 +132,14 @@ export class LinkMenuItem extends ObservableReactComponent { this._props.itemHandler?.(this._props.linkDoc); } else { const focusDoc = - Cast(this._props.linkDoc.link_anchor_1, Doc, null)?.annotationOn === this._props.sourceDoc - ? Cast(this._props.linkDoc.link_anchor_1, Doc, null) - : Cast(this._props.linkDoc.link_anchor_2, Doc, null)?.annotationOn === this._props.sourceDoc - ? Cast(this._props.linkDoc.link_anchor_12, Doc, null) - : undefined; + DocCast(this._props.linkDoc.link_anchor_1)?.annotationOn === this._props.sourceDoc + ? DocCast(this._props.linkDoc.link_anchor_1) + : DocCast(this._props.linkDoc.link_anchor_2)?.annotationOn === this._props.sourceDoc + ? DocCast(this._props.linkDoc.link_anchor_2) + : undefined; // prettier-ignore if (focusDoc) this._props.docView._props.focus(focusDoc, { instant: true }); - DocumentView.FollowLink(this._props.linkDoc, this._props.sourceDoc, false); + DocumentView.FollowLink(this._props.linkDoc, focusDoc ?? this._props.sourceDoc, false); } } ); diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 05706fe6b..b0415ef18 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -1412,7 +1412,7 @@ export class DocumentView extends DocComponent() { } public static setDefaultImageTemplate(checkResult?: boolean) { if (checkResult) return Doc.UserDoc().defaultImageLayout; - const view = DocumentView.Selected()[0]?._props.renderDepth > 0 ? DocumentView.Selected()[0] : undefined; + const view = DocumentView.Selected()[0]?._props.renderDepth > 0 || DocumentView.Selected()[0]?.Document.isTemplateDoc ? DocumentView.Selected()[0] : undefined; undoable(() => { const tempDoc = DocumentView.getTemplate(view); Doc.UserDoc().defaultImageLayout = tempDoc ? new PrefetchProxy(tempDoc) : undefined; diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx index a563b7c1b..a279ccc48 100644 --- a/src/client/views/nodes/MapBox/MapBox.tsx +++ b/src/client/views/nodes/MapBox/MapBox.tsx @@ -13,7 +13,7 @@ import { CirclePicker, ColorResult } from 'react-color'; import { Layer, MapProvider, MapRef, Map as MapboxMap, Marker, Source, ViewState, ViewStateChangeEvent } from 'react-map-gl/mapbox'; import { ClientUtils, setupMoveUpEvents } from '../../../../ClientUtils'; import { emptyFunction } from '../../../../Utils'; -import { Doc, DocListCast, Field, LinkedTo, Opt, StrListCast } from '../../../../fields/Doc'; +import { Doc, DocListCast, Field, LinkedTo, StrListCast } from '../../../../fields/Doc'; import { List } from '../../../../fields/List'; import { RichTextField } from '../../../../fields/RichTextField'; import { DocCast, NumCast, StrCast, toList } from '../../../../fields/Types'; @@ -111,12 +111,12 @@ export class MapBox extends ViewBoxAnnotatableComponent() { // this list contains pushpins and configs @computed get allAnnotations() { return DocListCast(this.dataDoc[this.annotationKey]); } // prettier-ignore - @computed get allSidebarDocs() { return DocListCast(this.dataDoc[this.SidebarKey]); } // prettier-ignore + @computed get allSidebarDocs() { return DocListCast(this.dataDoc[this.sidebarKey]); } // prettier-ignore @computed get allPushpins() { return this.allAnnotations.filter(anno => anno.type === DocumentType.PUSHPIN); } // prettier-ignore @computed get allRoutes() { return this.allAnnotations.filter(anno => anno.type === DocumentType.MAPROUTE); } // prettier-ignore @computed get SidebarShown() { return !!this.layoutDoc._layout_showSidebar; } // prettier-ignore @computed get sidebarWidthPercent() { return StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%'); } // prettier-ignore - @computed get SidebarKey() { return this.fieldKey + '_sidebar'; } // prettier-ignore + @computed get sidebarKey() { return this.fieldKey + '_sidebar'; } // prettier-ignore @computed get sidebarColor() { return StrCast(this.layoutDoc.sidebar_color, StrCast(this.layoutDoc[this._props.fieldKey + '_backgroundColor'], '#e4e4e4')); } @@ -260,7 +260,7 @@ export class MapBox extends ViewBoxAnnotatableComponent() { removeMapDocument = (doc: Doc | Doc[], annotationKey?: string) => { this.allAnnotations - .filter(anno => toList(doc).includes(DocCast(anno.mapPin))) + .filter(anno => toList(doc).includes(DocCast(anno.mapPin)!)) .forEach(anno => { anno.mapPin = undefined; }); @@ -339,27 +339,19 @@ export class MapBox extends ViewBoxAnnotatableComponent() { startAnchorDrag = (e: PointerEvent, ele: HTMLElement) => { e.preventDefault(); e.stopPropagation(); - - const sourceAnchorCreator = action(() => { - const note = this.getAnchor(true); - if (note && this._selectedPinOrRoute) { - note.latitude = this._selectedPinOrRoute.latitude; - note.longitude = this._selectedPinOrRoute.longitude; - note.map = this._selectedPinOrRoute.map; - } - return note as Doc; - }); - const targetCreator = (annotationOn: Doc | undefined) => { const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, annotationOn, 'yellow'); + target.layout_fitWidth = true; DocumentView.SetSelectOnLoad(target); return target; }; + + const sourceAnchorCreator = () => this.getAnchor(true); const docView = this.DocumentView?.(); docView && DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(docView, sourceAnchorCreator, targetCreator), e.pageX, e.pageY, { dragComplete: dragEv => { - if (!dragEv.aborted && dragEv.annoDragData && dragEv.annoDragData.linkSourceDoc && dragEv.annoDragData.dropDocument && dragEv.linkDocument) { + if (!dragEv.aborted && dragEv.annoDragData?.linkSourceDoc && dragEv.annoDragData.dropDocument && dragEv.linkDocument) { dragEv.annoDragData.linkSourceDoc.followLinkToggle = dragEv.annoDragData.dropDocument.annotationOn === this.Document; dragEv.annoDragData.linkSourceDoc.followLinkZoom = false; } @@ -368,17 +360,7 @@ export class MapBox extends ViewBoxAnnotatableComponent() { }; createNoteAnnotation = () => { - const createFunc = undoable( - action(() => { - const note = this._sidebarRef.current?.anchorMenuClick(this.getAnchor(true), ['latitude', 'longitude', LinkedTo]); - if (note && this._selectedPinOrRoute) { - note.latitude = this._selectedPinOrRoute.latitude; - note.longitude = this._selectedPinOrRoute.longitude; - note.map = this._selectedPinOrRoute.map; - } - }), - 'create note annotation' - ); + const createFunc = undoable(() => this._sidebarRef.current?.anchorMenuClick(this.getAnchor(true), ['latitude', 'longitude', LinkedTo]), 'create note annotation'); if (!this.layoutDoc.layout_showSidebar) { this.toggleSidebar(); setTimeout(createFunc); @@ -428,14 +410,11 @@ export class MapBox extends ViewBoxAnnotatableComponent() { } }; - getView = (doc: Doc, options: FocusViewOptions) => { - if (this._sidebarRef?.current?.makeDocUnfiltered(doc) && !this.SidebarShown) { - this.toggleSidebar(); - options.didMove = true; + getView = async (doc: Doc, options: FocusViewOptions) => { + if (DocListCast(this.dataDoc[this.sidebarKey]).find(anno => Doc.AreProtosEqual(doc.layout_unrendered ? DocCast(doc.annotationOn) : doc, anno))) { + SidebarAnnos.getView(this._sidebarRef.current, this.SidebarShown, this.toggleSidebar, doc, options); } - return new Promise>(res => { - DocumentView.addViewRenderedCb(doc, dv => res(dv)); - }); + return undefined; }; /* @@ -476,13 +455,14 @@ export class MapBox extends ViewBoxAnnotatableComponent() { @action deleteSelectedPinOrRoute = undoable(() => { - if (this._selectedPinOrRoute) { + const selPin = DocCast(this._selectedPinOrRoute); + if (selPin) { // Removes filter - Doc.setDocFilter(this.Document, 'latitude', NumCast(this._selectedPinOrRoute.latitude), 'remove'); - Doc.setDocFilter(this.Document, 'longitude', NumCast(this._selectedPinOrRoute.longitude), 'remove'); - Doc.setDocFilter(this.Document, LinkedTo, `mapPin=${Field.toScriptString(DocCast(this._selectedPinOrRoute))}`, 'remove'); + Doc.setDocFilter(this.Document, 'latitude', NumCast(selPin.latitude), 'remove'); + Doc.setDocFilter(this.Document, 'longitude', NumCast(selPin.longitude), 'remove'); + Doc.setDocFilter(this.Document, LinkedTo, `mapPin=${Field.toScriptString(selPin)}`, 'remove'); - this.removePushpinOrRoute(this._selectedPinOrRoute); + this.removePushpinOrRoute(selPin); } MapAnchorMenu.Instance.fadeOut(true); document.removeEventListener('pointerdown', this.tryHideMapAnchorMenu, true); @@ -1299,7 +1279,6 @@ export class MapBox extends ViewBoxAnnotatableComponent() { removeMapDocument = (docsIn: Doc | Doc[], annotationKey?: string) => { const docs = toList(docsIn); this.allAnnotations - .filter(anno => docs.includes(DocCast(anno.mapPin))) + .filter(anno => docs.includes(DocCast(anno.mapPin)!)) .forEach(anno => { anno.mapPin = undefined; }); @@ -224,6 +224,12 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent startAnchorDrag = (e: PointerEvent, ele: HTMLElement) => { e.preventDefault(); e.stopPropagation(); + const targetCreator = (annotationOn: Doc | undefined) => { + const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, annotationOn, 'yellow'); + target.layout_fitWidth = true; + DocumentView.SetSelectOnLoad(target); + return target; + }; const sourceAnchorCreator = action(() => { const note = this.getAnchor(true); @@ -235,11 +241,6 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent return note as Doc; }); - const targetCreator = (annotationOn: Doc | undefined) => { - const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, annotationOn, 'yellow'); - DocumentView.SetSelectOnLoad(target); - return target; - }; const docView = this.DocumentView?.(); docView && DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(docView, sourceAnchorCreator, targetCreator), e.pageX, e.pageY, { @@ -362,22 +363,22 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent @action deselectPin = () => { - if (this.selectedPin) { + const selPin = DocCast(this.selectedPin); + if (selPin) { // Removes filter - Doc.setDocFilter(this.Document, 'latitude', NumCast(this.selectedPin.latitude), 'remove'); - Doc.setDocFilter(this.Document, 'longitude', NumCast(this.selectedPin.longitude), 'remove'); - Doc.setDocFilter(this.Document, LinkedTo, `mapPin=${Field.toScriptString(DocCast(this.selectedPin))}`, 'remove'); + Doc.setDocFilter(this.Document, 'latitude', NumCast(selPin.latitude), 'remove'); + Doc.setDocFilter(this.Document, 'longitude', NumCast(selPin.longitude), 'remove'); + Doc.setDocFilter(this.Document, LinkedTo, `mapPin=${Field.toScriptString(selPin)}`, 'remove'); - const temp = this.selectedPin; if (!this._unmounting) { - this._bingMap.current.entities.remove(this.map_docToPinMap.get(temp)); + this._bingMap.current.entities.remove(this.map_docToPinMap.get(selPin)); } - const newpin = new this.MicrosoftMaps.Pushpin(new this.MicrosoftMaps.Location(temp.latitude, temp.longitude)); - this.MicrosoftMaps.Events.addHandler(newpin, 'click', () => this.pushpinClicked(temp as Doc)); + const newpin = new this.MicrosoftMaps.Pushpin(new this.MicrosoftMaps.Location(selPin.latitude, selPin.longitude)); + this.MicrosoftMaps.Events.addHandler(newpin, 'click', () => this.pushpinClicked(selPin)); if (!this._unmounting) { this._bingMap.current.entities.push(newpin); } - this.map_docToPinMap.set(temp, newpin); + this.map_docToPinMap.set(selPin, newpin); this.selectedPin = undefined; this.bingSearchBarContents = this.Document.map; } @@ -388,9 +389,7 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent this.toggleSidebar(); options.didMove = true; } - return new Promise>(res => { - DocumentView.addViewRenderedCb(doc, dv => res(dv)); - }); + return new Promise>(res => DocumentView.addViewRenderedCb(doc, res)); }; /* * Pushpin onclick @@ -535,13 +534,14 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent @action deleteSelectedPin = undoable(() => { - if (this.selectedPin) { + const selPin = this.selectedPin; + if (selPin) { // Removes filter - Doc.setDocFilter(this.Document, 'latitude', NumCast(this.selectedPin.latitude), 'remove'); - Doc.setDocFilter(this.Document, 'longitude', NumCast(this.selectedPin.longitude), 'remove'); - Doc.setDocFilter(this.Document, LinkedTo, `mapPin=${Field.toScriptString(DocCast(this.selectedPin))}`, 'remove'); + Doc.setDocFilter(this.Document, 'latitude', NumCast(selPin.latitude), 'remove'); + Doc.setDocFilter(this.Document, 'longitude', NumCast(selPin.longitude), 'remove'); + Doc.setDocFilter(this.Document, LinkedTo, `mapPin=${Field.toScriptString(selPin)}`, 'remove'); - this.removePushpin(this.selectedPin); + this.removePushpin(selPin); } MapAnchorMenu.Instance.fadeOut(true); document.removeEventListener('pointerdown', this.tryHideMapAnchorMenu, true); @@ -638,7 +638,10 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent this._disposers.highlight = reaction( () => this.allAnnotations.map(doc => doc[Highlight]), () => { - const allConfigPins = this.allAnnotations.map(doc => ({ doc, pushpin: DocCast(doc.mapPin) })).filter(pair => pair.pushpin); + const allConfigPins = this.allAnnotations + .map(doc => ({ doc, pushpin: DocCast(doc.mapPin) })) + .filter(pair => pair.pushpin) + .map(pair => ({ doc: pair.doc, pushpin: pair.pushpin! })); allConfigPins.forEach(({ pushpin }) => { if (!pushpin[Highlight] && this.map_pinHighlighted.get(pushpin)) { this.recolorPin(pushpin); diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 55e6d5596..e4050a3c3 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -10,7 +10,7 @@ import { DocData } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { InkTool } from '../../../fields/InkField'; import { ComputedField } from '../../../fields/ScriptField'; -import { Cast, FieldValue, NumCast, StrCast, toList } from '../../../fields/Types'; +import { Cast, DocCast, FieldValue, NumCast, StrCast, toList } from '../../../fields/Types'; import { ImageField, PdfField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; import { emptyFunction } from '../../../Utils'; @@ -27,7 +27,6 @@ import { Colors } from '../global/globalEnums'; import { PDFViewer } from '../pdf/PDFViewer'; import { PinDocView, PinProps } from '../PinFuncs'; import { SidebarAnnos } from '../SidebarAnnos'; -import { DocumentView } from './DocumentView'; import { FieldView, FieldViewProps } from './FieldView'; import { FocusViewOptions } from './FocusViewOptions'; import { ImageBox } from './ImageBox'; @@ -56,6 +55,9 @@ export class PDFBox extends ViewBoxAnnotatableComponent() { @observable private _pdf: Opt = undefined; @observable private _pageControls = false; + @computed get sidebarKey() { + return this.fieldKey + '_sidebar'; + } @computed get pdfUrl() { return Cast(this.dataDoc[this._props.fieldKey], PdfField); } @@ -232,14 +234,11 @@ export class PDFBox extends ViewBoxAnnotatableComponent() { return this._pdfViewer?.scrollFocus(anchor, NumCast(anchor.y, NumCast(anchor.config_scrollTop)), options); }; - getView = (doc: Doc, options: FocusViewOptions) => { - if (this._sidebarRef?.current?.makeDocUnfiltered(doc) && !this.SidebarShown) { - options.didMove = true; - this.toggleSidebar(false); + getView = async (doc: Doc, options: FocusViewOptions) => { + if (DocListCast(this.dataDoc[this.sidebarKey]).find(anno => Doc.AreProtosEqual(doc.layout_unrendered ? DocCast(doc.annotationOn) : doc, anno))) { + SidebarAnnos.getView(this._sidebarRef.current, this.SidebarShown, () => this.toggleSidebar(false), doc, options); } - return new Promise>(res => { - DocumentView.addViewRenderedCb(doc, dv => res(dv)); - }); + return undefined; }; getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => { diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index fa099178c..b3cb0e1db 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -338,12 +338,17 @@ export class VideoBox extends ViewBoxAnnotatableComponent() { getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => { const timecode = Cast(this.layoutDoc._layout_currentTimecode, 'number', null); const marquee = AnchorMenu.Instance.GetAnchor?.(undefined, addAsAnnotation); + const docAnchor = () => + Docs.Create.ConfigDocument({ + title: '#' + timecode, + _timecodeToShow: timecode, + annotationOn: this.Document, + }); if (!addAsAnnotation && marquee) marquee.backgroundColor = 'transparent'; - const anchor = - addAsAnnotation && marquee - ? CollectionStackedTimeline.createAnchor(this.Document, this.dataDoc, this.annotationKey, timecode || undefined, undefined, marquee, addAsAnnotation) || this.Document - : Docs.Create.ConfigDocument({ title: '#' + timecode, _timecodeToShow: timecode, annotationOn: this.Document }); + const visibleAnchor = () => addAsAnnotation && marquee && (CollectionStackedTimeline.createAnchor(this.Document, this.dataDoc, this.annotationKey, timecode || undefined, undefined, marquee, addAsAnnotation) || this.Document); + const anchor = visibleAnchor() || docAnchor(); PinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), temporal: true, pannable: true } }, this.Document); + addAsAnnotation && this.addDocument(anchor); return anchor; }; @@ -376,9 +381,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent() { } return this._stackedTimeline.getView(doc, options); } - return new Promise>(res => { - DocumentView.addViewRenderedCb(doc, dv => res(dv)); - }); + return new Promise>(res => DocumentView.addViewRenderedCb(doc, res)); }; // extracts video thumbnails and saves them as field of doc diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index 838dbea9d..779ccab40 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -13,7 +13,7 @@ import { InkTool } from '../../../fields/InkField'; import { List } from '../../../fields/List'; import { RefField } from '../../../fields/RefField'; import { listSpec } from '../../../fields/Schema'; -import { Cast, NumCast, StrCast, toList, WebCast } from '../../../fields/Types'; +import { Cast, DocCast, NumCast, StrCast, toList, WebCast } from '../../../fields/Types'; import { ImageField, WebField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; import { emptyFunction, stringHash } from '../../../Utils'; @@ -104,6 +104,9 @@ export class WebBox extends ViewBoxAnnotatableComponent() { @computed get webField() { return Cast(this.Document[this._props.fieldKey], WebField)?.url; } + @computed get sidebarKey() { + return this.fieldKey + '_sidebar'; + } constructor(props: FieldViewProps) { super(props); @@ -308,18 +311,17 @@ export class WebBox extends ViewBoxAnnotatableComponent() { }; @action - getView = (doc: Doc /* , options: FocusViewOptions */) => { - if (Doc.AreProtosEqual(doc, this.Document)) - return new Promise>(res => { - res(this.DocumentView?.()); - }); + getView = async (doc: Doc, options: FocusViewOptions) => { + if (Doc.AreProtosEqual(doc, this.Document)) return new Promise>(res => res(this.DocumentView?.())); + if (this.Document.layout_fieldKey === 'layout_icon') this.DocumentView?.().iconify(); const webUrl = WebCast(doc.config_data)?.url; if (this._url && webUrl && webUrl.href !== this._url) this.setData(webUrl.href); - if (this._sidebarRef?.current?.makeDocUnfiltered(doc) && !this.SidebarShown) this.toggleSidebar(false); - return new Promise>(res => { - DocumentView.addViewRenderedCb(doc, dv => res(dv)); - }); + + if (DocListCast(this.dataDoc[this.sidebarKey]).find(anno => Doc.AreProtosEqual(doc.layout_unrendered ? DocCast(doc.annotationOn) : doc, anno))) { + return SidebarAnnos.getView(this._sidebarRef.current, this.SidebarShown, () => this.toggleSidebar(false), doc, options); + } + return undefined; }; sidebarAddDocTab = (doc: Doc, where: OpenWhere) => { @@ -454,7 +456,7 @@ export class WebBox extends ViewBoxAnnotatableComponent() { iframeDown = (e: PointerEvent) => { this._textAnnotationCreator = undefined; const sel = this._url ? this._iframe?.contentDocument?.getSelection() : window.document.getSelection(); - if (sel?.empty && !(e.target as any).textContent) + if (sel?.empty && !(e.target as HTMLElement).textContent) sel.empty(); // Chrome else if (sel?.removeAllRanges) sel.removeAllRanges(); // Firefox @@ -565,7 +567,7 @@ export class WebBox extends ViewBoxAnnotatableComponent() { 'click', undoable( action((e: MouseEvent) => { - let eleHref = (e.target as any)?.outerHTML?.split('"="')[1]?.split('"')[0]; + let eleHref = (e.target as HTMLElement)?.outerHTML?.split('"="')[1]?.split('"')[0]; for (let ele = e.target as HTMLElement | Element | null; ele; ele = ele.parentElement) { if ('href' in ele) { eleHref = (typeof ele.href === 'string' ? ele.href : eleHref) || (ele.parentElement && 'href' in ele.parentElement ? (ele.parentElement.href as string) : eleHref); diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 57720baae..bab97f681 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -270,13 +270,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { - const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, annotationOn); + const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, annotationOn, 'yellow'); + target.layout_fitWidth = true; DocumentView.SetSelectOnLoad(target); return target; }; + const sourceAnchorCreator = () => this.getAnchor(true); + const docView = this.DocumentView?.(); - docView && DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(docView, () => this.getAnchor(true), targetCreator), e.pageX, e.pageY); + docView && DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(docView, sourceAnchorCreator, targetCreator), e.pageX, e.pageY); }); AnchorMenu.Instance.AddDrawingAnnotation = (drawing: Doc) => { @@ -1070,15 +1073,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { if (DocListCast(this.dataDoc[this.sidebarKey]).find(anno => Doc.AreProtosEqual(doc.layout_unrendered ? DocCast(doc.annotationOn) : doc, anno))) { - if (!this.SidebarShown) { - this.toggleSidebar(false); - options.didMove = true; - } - setTimeout(() => this._sidebarRef?.current?.makeDocUnfiltered(doc)); + return SidebarAnnos.getView(this._sidebarRef.current, this.SidebarShown, () => this.toggleSidebar(false), doc, options); } - return new Promise>(res => { - DocumentView.addViewRenderedCb(doc, dv => res(dv)); - }); + return new Promise>(res => DocumentView.addViewRenderedCb(doc, res)); }; focus = (textAnchor: Doc, options: FocusViewOptions) => { const focusSpeed = options.zoomTime ?? 500; -- cgit v1.2.3-70-g09d2 From 2c26d7c96e56344a1a73a7c6f6dec3d6b0f0ee7e Mon Sep 17 00:00:00 2001 From: bobzel Date: Fri, 16 May 2025 11:46:22 -0400 Subject: changed isGroup to freeform_isGroup to fix issues with groups that are changed into other collection types. fixed toggleTarget links to work when opening in a light box (previously they flickered on, then off). fixed text boxes with a number field as data to act like a number input box. --- src/client/documents/Documents.ts | 2 +- src/client/util/DocumentManager.ts | 1 + src/client/views/DocumentDecorations.tsx | 8 ++-- src/client/views/PropertiesButtons.tsx | 4 +- src/client/views/PropertiesView.tsx | 6 +-- src/client/views/StyleProvider.tsx | 8 ++-- src/client/views/collections/CollectionView.tsx | 2 +- .../collectionFreeForm/CollectionFreeFormView.tsx | 42 +++++++++-------- .../collections/collectionFreeForm/MarqueeView.tsx | 4 +- src/client/views/nodes/DocumentLinksButton.tsx | 1 - src/client/views/nodes/DocumentView.tsx | 2 +- src/client/views/nodes/KeyValueBox.tsx | 8 ++-- .../views/nodes/formattedText/FormattedTextBox.tsx | 12 ++--- src/client/views/nodes/trails/PresBox.tsx | 2 +- src/fields/Doc.ts | 54 +++++++++++++--------- 15 files changed, 85 insertions(+), 71 deletions(-) (limited to 'src/client/views/collections') diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 2df6f3e23..9aa1d53c2 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -361,7 +361,6 @@ export class DocumentOptions { isBaseProto?: BOOLt = new BoolInfo('is doc a base level prototype for data documents as opposed to data documents which are prototypes for layout documents. base protos are not cloned during a deep'); isTemplateForField?: string; // the field key for which the containing document is a rendering template isTemplateDoc?: BOOLt = new BoolInfo('is the document a template for creating other documents'); - isGroup?: BOOLt = new BoolInfo('should collection use a grouping UI behavior'); isFolder?: BOOLt = new BoolInfo('is document a tree view folder'); _isTimelineLabel?: BOOLt = new BoolInfo('is document a timeline label'); isLightbox?: BOOLt = new BoolInfo('whether a collection acts as a lightbox by opening lightbox links by hiding all other documents in collection besides link target'); @@ -401,6 +400,7 @@ export class DocumentOptions { // freeform properties freeform?: STRt = new StrInfo(''); + freeform_isGroup?: BOOLt = new BoolInfo('should collection use a grouping UI behavior'); _freeform_backgroundGrid?: BOOLt = new BoolInfo('whether background grid is shown on freeform collections'); _freeform_scale_min?: NUMt = new NumInfo('how far out a view can zoom (used by image/videoBoxes that are clipped'); _freeform_scale_max?: NUMt = new NumInfo('how far in a view can zoom (used by sidebar freeform views'); diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index 422613968..1c2ffab1b 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -274,6 +274,7 @@ export class DocumentManager { // even if we found the document view, if the target is a lightbox, we try to open it in the lightbox to preserve lightbox semantics (eg, there's only one active doc in the lightbox) const target = DocCast(targetDoc.annotationOn, targetDoc)!; const compView = this.getDocumentView(DocCast(target.embedContainer))?.ComponentView; + options.didMove = target.hidden || options.didMove ? true : false; if ((compView?.addDocTab ?? compView?._props.addDocTab)?.(target, options.openLocation)) { await new Promise(waitres => { setTimeout(() => waitres()); diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index ab665e984..1e7f4fb52 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -524,7 +524,7 @@ export class DocumentDecorations extends ObservableReactComponent { const doc = docView.Document; - if (doc.isGroup) { + if (Doc.IsFreeformGroup(doc)) { DocListCast(doc.data) .map(member => DocumentView.getDocumentView(member, docView)!) .forEach(member => this.resizeViewForOutpainting(member, refPt, scale, opts)); @@ -598,14 +598,14 @@ export class DocumentDecorations extends ObservableReactComponent (doc.isGroup ? DocListCast(doc.data).some(this.hasFixedAspect) : !BoolCast(doc._layout_nativeDimEditable)); + hasFixedAspect = (doc: Doc): boolean => (Doc.IsFreeformGroup(doc) ? DocListCast(doc.data).some(this.hasFixedAspect) : !BoolCast(doc._layout_nativeDimEditable)); // // resize a single DocumentView about the specified reference point, possibly setting/updating the native dimensions of the Doc // resizeView = (docView: DocumentView, refPt: number[], scale: { x: number; y: number }, opts: { dragHdl: string; freezeNativeDims: boolean }) => { const doc = docView.Document; - if (doc.isGroup) { + if (Doc.IsFreeformGroup(doc)) { DocListCast(doc.data) .map(member => DocumentView.getDocumentView(member, docView)!) .forEach(member => this.resizeView(member, refPt, scale, opts)); @@ -737,7 +737,7 @@ export class DocumentDecorations extends ObservableReactComponent docView.Document._dragOnlyWithinContainer || docView.Document.isGroup || docView.Document.layout_hideOpenButton) || + DocumentView.Selected().some(docView => docView.Document._dragOnlyWithinContainer || docView.Document.layout_hideOpenButton) || this._isRounding || this._isRotating; const hideDeleteButton = diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx index ded342df0..2c84d7fe7 100644 --- a/src/client/views/PropertiesButtons.tsx +++ b/src/client/views/PropertiesButtons.tsx @@ -238,8 +238,8 @@ export class PropertiesButtons extends React.Component { // on => `Display collection as a Group`, // on => 'object-group', // (dv, doc) => { - // doc.isGroup = !doc.isGroup; - // doc.forceActive = doc.isGroup; + // docfreeform_isGroup = !docfreeform_isGroup; + // doc.forceActive = docfreeform_isGroup; // } // ); // } diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx index acf6f928a..06463b2a2 100644 --- a/src/client/views/PropertiesView.tsx +++ b/src/client/views/PropertiesView.tsx @@ -81,7 +81,7 @@ export class PropertiesView extends ObservableReactComponent, props: Opt, props: Opt, props: Opt, props: Opt Doc) { - if (!Doc.IsSystem(this.Document) && this.Document._type_collection !== CollectionViewType.Docking && !this.dataDoc.isGroup && !this.Document.annotationOn) { + if (!Doc.IsSystem(this.Document) && this.Document._type_collection !== CollectionViewType.Docking && !this.Document.annotationOn) { // prettier-ignore const subItems: ContextMenuProps[] = [ { description: 'Freeform', event: () => func(CollectionViewType.Freeform), icon: 'signature' }, diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 77615c650..3571dab1a 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -176,7 +176,7 @@ export class CollectionFreeFormView extends CollectionSubView { const { pointFocus, zoomTime, didMove } = options; - if (!this.Document.isGroup && pointFocus && !didMove) { + if (!this.Document.freeform_isGroup && pointFocus && !didMove) { const dfltScale = this.isAnnotationOverlay ? 1 : 0.25; if (this.layoutDoc[this.scaleFieldKey] !== dfltScale) { this.zoomSmoothlyAboutPt(this.screenToFreeformContentsXf.transformPoint(pointFocus.X, pointFocus.Y), dfltScale, zoomTime); @@ -380,7 +380,7 @@ export class CollectionFreeFormView extends CollectionSubView { - if (anchor.isGroup && !options.docTransform && options.contextPath?.length) { + if (Doc.IsFreeformGroup(anchor) && !options.docTransform && options.contextPath?.length) { // don't focus on group if there's a context path because we're about to focus on a group item // which will override any group focus. (If we allowed the group to focus, it would mark didMove even if there were no net movement) return undefined; @@ -395,7 +395,7 @@ export class CollectionFreeFormView extends CollectionSubView { - if (this.Document.isGroup || this.Document[(this._props.viewField ?? '_') + 'freeform_noZoom']) return; + if (this.Document.freeform_isGroup || this.Document[(this._props.viewField ?? '_') + 'freeform_noZoom']) return; let deltaScale = deltaY > 0 ? 1 / 1.05 : 1.05; if (deltaScale < 0) deltaScale = -deltaScale; const [x, y] = this.screenToFreeformContentsXf.transformPoint(pointX, pointY); @@ -1298,7 +1298,7 @@ export class CollectionFreeFormView extends CollectionSubView { - if (this.Document.isGroup || !this.isContentActive()) return; // group style collections neither pan nor zoom + if (this.Document.freeform_isGroup || !this.isContentActive()) return; // group style collections neither pan nor zoom SnappingManager.TriggerUserPanned(); if (this.layoutDoc._Transform || this.Document.treeView_OutlineMode === TreeViewType.outline) return; e.stopPropagation(); @@ -1422,7 +1422,7 @@ export class CollectionFreeFormView extends CollectionSubView { const ret = !!this._props.removeDocument?.(docs, annotationKey); // if this is a group and we have fewer than 2 Docs, then just promote what's left to our parent and get rid of the group. - if (ret && DocListCast(this.dataDoc[annotationKey ?? this.fieldKey]).length < 2 && this.Document.isGroup) { + if (ret && DocListCast(this.dataDoc[annotationKey ?? this.fieldKey]).length < 2 && this.Document.freeform_isGroup) { this.promoteCollection(); } return ret; @@ -1548,7 +1548,7 @@ export class CollectionFreeFormView extends CollectionSubView { - if (this.Document.isGroup && this.childDocs.length === this.childDocList?.length) { + if (this.Document.freeform_isGroup && this.childDocs.length === this.childDocList?.length) { const clist = this.childDocs.map(cd => ({ x: NumCast(cd.x), y: NumCast(cd.y), width: NumCast(cd._width), height: NumCast(cd._height) })); return aggregateBounds(clist, NumCast(this.layoutDoc._xMargin), NumCast(this.layoutDoc._yMargin)); } @@ -1929,8 +1929,8 @@ export class CollectionFreeFormView extends CollectionSubView this.updateIcon(), icon: 'compress-arrows-alt' }); this._props.renderDepth && appearanceItems.push({ description: 'Ungroup collection', event: this.promoteCollection, icon: 'table' }); - this.Document.isGroup && this.Document.transcription && appearanceItems.push({ description: 'Ink to text', event: this.transcribeStrokes, icon: 'font' }); + this.Document.freeform_isGroup && this.Document.transcription && appearanceItems.push({ description: 'Ink to text', event: this.transcribeStrokes, icon: 'font' }); !Doc.noviceMode ? appearanceItems.push({ description: 'Arrange contents in grid', event: this.layoutDocsInGrid, icon: 'table' }) : null; !Doc.noviceMode ? appearanceItems.push({ description: (this.Document._freeform_useClusters ? 'Hide' : 'Show') + ' Clusters', event: () => this._clusters.updateClusters(!this.Document._freeform_useClusters), icon: 'braille' }) : null; @@ -2007,7 +2007,7 @@ export class CollectionFreeFormView extends CollectionSubView { - if (this.Document.isGroup && this.Document.transcription) { + if (this.Document.freeform_isGroup && this.Document.transcription) { const text = StrCast(this.Document.transcription); const lines = text.split('\n'); const height = 30 + 15 * lines.length; @@ -2025,7 +2025,7 @@ export class CollectionFreeFormView extends CollectionSubView()) => { if (visited.has(this.Document)) return; visited.add(this.Document); - showGroupDragTarget && (this.GroupChildDrag = BoolCast(this.Document.isGroup)); + showGroupDragTarget && (this.GroupChildDrag = BoolCast(this.Document.freeform_isGroup)); const activeDocs = this.getActiveDocuments(); const size = this.screenToFreeformContentsXf.transformDirection(this._props.PanelWidth(), this._props.PanelHeight()); const selRect = { left: this.panX() - size[0] / 2, top: this.panY() - size[1] / 2, width: size[0], height: size[1] }; @@ -2033,13 +2033,15 @@ export class CollectionFreeFormView extends CollectionSubView intersectRect(docDims(doc), rect); const snappableDocs = activeDocs.filter(doc => doc.z === undefined && isDocInView(doc, selRect)); // first see if there are any foreground docs to snap to - activeDocs.filter(doc => doc.isGroup && SnappingManager.IsResizing !== doc[Id] && !DragManager.docsBeingDragged.includes(doc)).forEach(doc => DocumentView.getDocumentView(doc)?.ComponentView?.dragStarting?.(snapToDraggedDoc, false, visited)); + activeDocs + .filter(doc => Doc.IsFreeformGroup(doc) && SnappingManager.IsResizing !== doc[Id] && !DragManager.docsBeingDragged.includes(doc)) + .forEach(doc => DocumentView.getDocumentView(doc)?.ComponentView?.dragStarting?.(snapToDraggedDoc, false, visited)); const horizLines: number[] = []; const vertLines: number[] = []; const invXf = this.screenToFreeformContentsXf.inverse(); snappableDocs - .filter(doc => !doc.isGroup && (snapToDraggedDoc || (SnappingManager.IsResizing !== doc[Id] && !DragManager.docsBeingDragged.includes(doc)))) + .filter(doc => !Doc.IsFreeformGroup(doc) && (snapToDraggedDoc || (SnappingManager.IsResizing !== doc[Id] && !DragManager.docsBeingDragged.includes(doc)))) .forEach(doc => { const { left, top, width, height } = docDims(doc); const topLeftInScreen = invXf.transformPoint(left, top); @@ -2131,7 +2133,7 @@ export class CollectionFreeFormView extends CollectionSubView 0 ? undefined : this.nudge} addDocTab={this.addDocTab} slowLoadDocuments={this.slowLoadDocuments} diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 3cc7c0f2d..37959a5f0 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -372,7 +372,7 @@ export class MarqueeView extends ObservableReactComponent { doc.$data = new List(selected); - doc.$isGroup = makeGroup; + doc.$freeform_isGroup = makeGroup; doc.$title = makeGroup ? 'grouping' : 'nested freeform'; doc._freeform_panX = doc._freeform_panY = 0; return doc; @@ -508,7 +508,7 @@ export class MarqueeView extends ObservableReactComponent
() { }; createFieldView = (templateDoc: Doc, row: KeyValuePair) => { - const metaKey = row._props.keyName; - const fieldTempDoc = Doc.IsDelegateField(templateDoc, metaKey) ? Doc.MakeDelegate(templateDoc) : Doc.MakeEmbedding(templateDoc); - fieldTempDoc.title = metaKey; + const keyName = row._props.keyName; + const fieldTempDoc = Doc.IsDelegateField(templateDoc, keyName) ? Doc.MakeDelegate(templateDoc) : Doc.MakeEmbedding(templateDoc); + fieldTempDoc.title = keyName; fieldTempDoc.layout_fitWidth = true; fieldTempDoc._xMargin = 10; fieldTempDoc._yMargin = 10; fieldTempDoc._width = 100; fieldTempDoc._height = 40; - fieldTempDoc.layout = this.inferType(templateDoc[metaKey], metaKey); + fieldTempDoc.layout = this.inferType(templateDoc[keyName], keyName); return fieldTempDoc; }; diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index bab97f681..4189fcc0c 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -428,10 +428,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent(); const oldAutoLinks = Doc.Links(this.Document).filter( link => - ((!Doc.isTemplateForField(this.Document) && - ((DocCast(link.link_anchor_1) && !Doc.isTemplateForField(DocCast(link.link_anchor_1)!)) || !Doc.AreProtosEqual(DocCast(link.link_anchor_1), this.Document)) && - ((DocCast(link.link_anchor_2) && !Doc.isTemplateForField(DocCast(link.link_anchor_2)!)) || !Doc.AreProtosEqual(DocCast(link.link_anchor_2), this.Document))) || - (Doc.isTemplateForField(this.Document) && (link.link_anchor_1 === this.Document || link.link_anchor_2 === this.Document))) && + ((!Doc.IsTemplateForField(this.Document) && + ((DocCast(link.link_anchor_1) && !Doc.IsTemplateForField(DocCast(link.link_anchor_1)!)) || !Doc.AreProtosEqual(DocCast(link.link_anchor_1), this.Document)) && + ((DocCast(link.link_anchor_2) && !Doc.IsTemplateForField(DocCast(link.link_anchor_2)!)) || !Doc.AreProtosEqual(DocCast(link.link_anchor_2), this.Document))) || + (Doc.IsTemplateForField(this.Document) && (link.link_anchor_1 === this.Document || link.link_anchor_2 === this.Document))) && link.link_relationship === LinkManager.AutoKeywords ); // prettier-ignore if (this.EditorView?.state.doc.textContent) { @@ -1216,7 +1216,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent= layoutTime ? (protoTime >= dataTime ? protoData : dataData) : layoutTime >= protoTime ? layoutData : protoData; const whichData = recentData ?? (this.layoutDoc.isTemplateDoc ? layoutData : protoData) ?? protoData; - return !whichData ? undefined : { data: RTFCast(whichData), str: Field.toString(DocCast(whichData) ?? StrCast(whichData)) }; + return !whichData ? undefined : { data: RTFCast(whichData), str: Field.toString(DocCast(whichData) ?? NumCast(whichData)?.toString() ?? StrCast(whichData)) }; }, incomingValue => { if (this.EditorView && this.ApplyingChange !== this.fieldKey) { @@ -1767,7 +1767,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx index 11f35b8ef..cb2a1f13f 100644 --- a/src/client/views/nodes/trails/PresBox.tsx +++ b/src/client/views/nodes/trails/PresBox.tsx @@ -702,7 +702,7 @@ export class PresBox extends ViewBoxBaseComponent() { transTime + 10 ); } - if ((pinDataTypes?.pannable || (!pinDataTypes && (activeItem.config_viewBounds !== undefined || activeItem.config_panX !== undefined || activeItem.config_viewScale !== undefined))) && !bestTarget.isGroup) { + if ((pinDataTypes?.pannable || (!pinDataTypes && (activeItem.config_viewBounds !== undefined || activeItem.config_panX !== undefined || activeItem.config_viewScale !== undefined))) && !Doc.IsFreeformGroup(bestTarget)) { const contentBounds = Cast(activeItem.config_viewBounds, listSpec('number')); if (contentBounds) { const viewport = { panX: (contentBounds[0] + contentBounds[2]) / 2, panY: (contentBounds[1] + contentBounds[3]) / 2, width: contentBounds[2] - contentBounds[0], height: contentBounds[3] - contentBounds[1] }; diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index 0d11b9743..e110550e6 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -536,23 +536,35 @@ export namespace Doc { export function GetT(doc: Doc, key: string, ctor: ToConstructor, ignoreProto: boolean = false): FieldResult { return Cast(Get(doc, key, ignoreProto), ctor) as FieldResult; } - export function isTemplateDoc(doc: Doc) { - return GetT(doc, 'isTemplateDoc', 'boolean', true); - } - export function isTemplateForField(doc: Doc) { - return GetT(doc, 'isTemplateForField', 'string', true); - } - export function IsDataProto(doc: Doc) { - return GetT(doc, 'isDataDoc', 'boolean', true); - } - export function IsBaseProto(doc: Doc) { - return GetT(doc, 'isBaseProto', 'boolean', true); - } - export function IsSystem(doc: Doc) { - return GetT(doc, 'isSystem', 'boolean', true); - } - export function IsDelegateField(doc: Doc, fieldKey: string) { - return doc && Get(doc, fieldKey, true) !== undefined; + /** + * Tests whether the Doc is flagged as being a template. + * Templates can be set as a layout for another target Doc. When rendered by the target Doc, the template + * creates an instance of itself that holds rendering data specific to the target + * @param doc + */ + export function IsTemplateDoc(doc: Doc) { return GetT(doc, 'isTemplateDoc', 'boolean', true); } // prettier-ignore + /** + * Tests whether the Doc is flagged as being a template for rendering a specific field of a Doc + * When flagged as field template and rendered, the template will redirect its componentView to write to the + * specified template field. In general, a compound Doc template will contain multiple field templates, one for each of the + * data fields rendered by the compound template. + * @param doc + * @returns + */ + export function IsTemplateForField(doc: Doc) { return GetT(doc, 'isTemplateForField', 'string', true); } // prettier-ignore + export function IsDataProto(doc: Doc) { return GetT(doc, 'isDataDoc', 'boolean', true); } // prettier-ignore + export function IsBaseProto(doc: Doc) { return GetT(doc, 'isBaseProto', 'boolean', true); } // prettier-ignore + export function IsSystem(doc: Doc) { return GetT(doc, 'isSystem', 'boolean', true); } // prettier-ignore + export function IsDelegateField(doc: Doc, fieldKey: string) { return doc && Get(doc, fieldKey, true) !== undefined; } // prettier-ignore + /** + * Tests whether a doc is a freeform collection that renders as a group. + * The group variant of a collection automatically resizes so that none of its contents + * are ever hidden. + * @param doc doc to test for being a freeform group + * @returns boolean + */ + export function IsFreeformGroup(doc: Doc) { + return doc.freeform_isGroup && doc.type_collection === CollectionViewType.Freeform; } // // this will write the value to the key on either the data doc or the embedding doc. The choice @@ -734,7 +746,7 @@ export namespace Doc { const FindDocsInRTF = new RegExp(/(audioId|textId|anchorId|docId)"\s*:\s*"(.*?)"/g); export function makeClone(doc: Doc, cloneMap: Map, linkMap: Map, rtfs: { copy: Doc; key: string; field: RichTextField }[], exclusions: string[], pruneDocs: Doc[], cloneLinks: boolean, cloneTemplates: boolean): Doc { - if (Doc.IsBaseProto(doc) || ((Doc.isTemplateDoc(doc) || Doc.isTemplateForField(doc)) && !cloneTemplates)) { + if (Doc.IsBaseProto(doc) || ((Doc.IsTemplateDoc(doc) || Doc.IsTemplateForField(doc)) && !cloneTemplates)) { return doc; } if (cloneMap.get(doc[Id])) return cloneMap.get(doc[Id])!; @@ -808,7 +820,7 @@ export namespace Doc { const docAtKey = DocCast(clone[key]); if (docAtKey && !Doc.IsSystem(docAtKey)) { if (!Array.from(cloneMap.values()).includes(docAtKey)) { - clone[key] = !cloneTemplates && (Doc.isTemplateDoc(docAtKey) || Doc.isTemplateForField(docAtKey)) ? docAtKey : cloneMap.get(docAtKey[Id]); + clone[key] = !cloneTemplates && (Doc.IsTemplateDoc(docAtKey) || Doc.IsTemplateForField(docAtKey)) ? docAtKey : cloneMap.get(docAtKey[Id]); } else { repairClone(docAtKey, cloneMap, cloneTemplates, visited); } @@ -859,7 +871,7 @@ export namespace Doc { */ export function expandTemplateLayout(templateLayoutDoc: Doc, targetDoc?: Doc, layoutFieldKey?: string) { // nothing to do if the layout isn't a template or we don't have a target that's different than the template - if (!targetDoc || templateLayoutDoc === targetDoc || (!Doc.isTemplateForField(templateLayoutDoc) && !Doc.isTemplateDoc(templateLayoutDoc))) { + if (!targetDoc || templateLayoutDoc === targetDoc || (!Doc.IsTemplateForField(templateLayoutDoc) && !Doc.IsTemplateDoc(templateLayoutDoc))) { return templateLayoutDoc; } @@ -922,7 +934,7 @@ export namespace Doc { console.log('Warning: GetLayoutDataDocPair childDoc not defined'); return { layout: childDoc, data: childDoc }; } - const data = Doc.AreProtosEqual(containerDataDoc, containerDoc) || (!Doc.isTemplateDoc(childDoc) && !Doc.isTemplateForField(childDoc)) ? undefined : containerDataDoc; + const data = Doc.AreProtosEqual(containerDataDoc, containerDoc) || (!Doc.IsTemplateDoc(childDoc) && !Doc.IsTemplateForField(childDoc)) ? undefined : containerDataDoc; const templateRoot = DocCast(containerDoc?.rootDocument); return { layout: Doc.expandTemplateLayout(childDoc, templateRoot, layoutFieldKey), data }; } -- cgit v1.2.3-70-g09d2 From 51b4b64eae4749542590785bca9d33d3ccdded69 Mon Sep 17 00:00:00 2001 From: bobzel Date: Mon, 19 May 2025 19:43:28 -0400 Subject: fixed text boxes to calculate height correctly. removed usertag modification time in text. fixed masonry and stacking views with pivots. --- src/ClientUtils.ts | 3 +- .../collections/CollectionMasonryViewFieldRow.tsx | 42 +++--- .../views/collections/CollectionStackingView.scss | 5 +- .../views/collections/CollectionStackingView.tsx | 154 ++++++++------------- .../CollectionStackingViewFieldColumn.tsx | 30 ++-- .../views/nodes/formattedText/FormattedTextBox.tsx | 25 ++-- src/client/views/nodes/formattedText/marks_rts.ts | 14 -- 7 files changed, 111 insertions(+), 162 deletions(-) (limited to 'src/client/views/collections') diff --git a/src/ClientUtils.ts b/src/ClientUtils.ts index cc8b715b4..8eb1a39ec 100644 --- a/src/ClientUtils.ts +++ b/src/ClientUtils.ts @@ -670,7 +670,8 @@ export function setupMoveUpEvents( } export function DivHeight(ele: HTMLElement | null): number { - return ele ? Number(getComputedStyle(ele).height.replace('px', '')) : 0; + const hgt = ele ? Number(getComputedStyle(ele).height.replace('px', '')) : 0; + return Number.isNaN(hgt) ? 0 : hgt; } export function DivWidth(ele: HTMLElement | null): number { return ele ? Number(getComputedStyle(ele).width.replace('px', '')) : 0; diff --git a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx index 89ccf5a0f..d1f7971d4 100644 --- a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx +++ b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx @@ -28,12 +28,14 @@ interface CMVFieldRowProps { headingObject: SchemaHeaderField | undefined; docList: Doc[]; parent: CollectionStackingView; + panelWidth: () => number; + columnWidth: () => number; pivotField: string; type: 'string' | 'number' | 'bigint' | 'boolean' | 'symbol' | 'undefined' | 'object' | 'function' | undefined; createDropTarget: (ele: HTMLDivElement) => void; screenToLocalTransform: () => Transform; setDocHeight: (key: string, thisHeight: number) => void; - refList: Element[]; + sectionRefs: Element[]; showHandle: boolean; } @@ -74,7 +76,7 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent { this._dropDisposer?.(); if (ele) this._dropDisposer = DragManager.MakeDropTarget(ele, this.rowDrop.bind(this), this._props.Doc); - else if (this._ele) this.props.refList.splice(this.props.refList.indexOf(this._ele), 1); + else if (this._ele) this.props.sectionRefs.splice(this.props.sectionRefs.indexOf(this._ele), 1); this._ele = ele; }; @action @@ -82,10 +84,10 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent i.heading).indexOf(castedValue.toString()) > -1) { - return false; - } + if (this._props.parent.colHeaderData?.map(i => i.heading).indexOf(castedValue.toString()) || 0 > -1) { + return false; } key && this._props.docList.forEach(d => Doc.SetInPlace(d, key, castedValue, true)); this._heading = castedValue.toString(); @@ -251,20 +251,11 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent this.addDocument('', false); @computed get contentLayout() { - const rows = Math.max(1, Math.min(this._props.docList.length, Math.floor((this._props.parent._props.PanelWidth() - 2 * this._props.parent.xMargin) / (this._props.parent.columnWidth + this._props.parent.gridGap)))); - const showChrome = !this._props.chromeHidden; - const stackPad = showChrome ? `0px ${this._props.parent.xMargin}px` : `${this._props.parent.yMargin}px ${this._props.parent.xMargin}px 0px ${this._props.parent.xMargin}px `; + const rows = Math.max(1, Math.min(this._props.docList.length, Math.floor(this._props.panelWidth() / this._props.columnWidth()))); return this.collapsed ? null : (
- {showChrome ? ( -
+ {!this._props.chromeHidden ? ( +
) : null} @@ -272,11 +263,9 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent list + ` ${this._props.parent.columnWidth}px`, ''), + gridTemplateColumns: numberRange(rows).reduce(list => list + ` ${this._props.columnWidth()}px`, ''), }}> {this._props.parent.children(this._props.docList)}
@@ -339,7 +328,12 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent +
{this.headingView} {this.contentLayout}
diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss index 05ac52ff9..d6e4943ff 100644 --- a/src/client/views/collections/CollectionStackingView.scss +++ b/src/client/views/collections/CollectionStackingView.scss @@ -2,10 +2,12 @@ .collectionMasonryView { display: inline; + flex-wrap: wrap; } .collectionStackingView { display: flex; + justify-content: space-between; } .collectionStackingMasonry-cont { @@ -56,7 +58,6 @@ top: 0; overflow-y: auto; overflow-x: hidden; - flex-wrap: wrap; transition: top 0.5s; > div { @@ -210,7 +211,6 @@ .collectionStackingView-sectionHeader { text-align: center; margin: auto; - margin-bottom: 10px; background: global.$medium-gray; // overflow: hidden; overflow is visible so the color menu isn't hidden -ftong @@ -367,7 +367,6 @@ .collectionStackingView-addGroupButton { display: flex; overflow: hidden; - margin: auto; width: 90%; overflow: ellipses; diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index 25a222cbb..cc514f262 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -4,7 +4,7 @@ import { action, computed, IReactionDisposer, makeObservable, observable, Observ import { observer } from 'mobx-react'; import * as React from 'react'; import { ClientUtils, DivHeight, returnNone, returnZero, setupMoveUpEvents, smoothScroll } from '../../../ClientUtils'; -import { Doc, Opt } from '../../../fields/Doc'; +import { Doc, Field, Opt } from '../../../fields/Doc'; import { Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; import { listSpec } from '../../../fields/Schema'; @@ -35,6 +35,7 @@ import { CollectionStackingViewFieldColumn } from './CollectionStackingViewField import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView'; import { computedFn } from 'mobx-utils'; import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox'; +import { FieldsDropdown } from '../FieldsDropdown'; export type collectionStackingViewProps = { sortFunc?: (a: Doc, b: Doc) => number; @@ -48,15 +49,15 @@ export type collectionStackingViewProps = { @observer export class CollectionStackingView extends CollectionSubView>() { _disposers: { [key: string]: IReactionDisposer } = {}; + _addGroupRef = React.createRef(); _masonryGridRef: HTMLDivElement | null = null; // used in a column dragger, likely due for the masonry grid view. We want to use this _draggerRef = React.createRef(); // keeping track of documents. Updated on internal and external drops. What's the difference? _docXfs: { height: () => number; width: () => number; stackedDocTransform: () => Transform }[] = []; - // Doesn't look like this field is being used anywhere. Obsolete? - _columnStart: number = 0; - @observable _refList: HTMLElement[] = []; + @observable _colStackRefs: HTMLElement[] = []; + @observable _colHdrRefs: HTMLElement[] = []; // map of node headers to their heights. Used in Masonry @observable _heightMap = new Map(); // Assuming that this is the current css cursor style @@ -67,10 +68,25 @@ export class CollectionStackingView extends CollectionSubView { + const docHeader = d[this.pivotField] ? d[this.pivotField] : `NO ${this.pivotField.toUpperCase()} VALUE`; + const docHeaderString = docHeader !== undefined ? Field.toString(docHeader) : `NO ${this.pivotField.toUpperCase()} VALUE`; + + // find existing header or create + const existingHeader = Array.from(map.keys()).find(sh => sh.heading === docHeaderString); + if (!existingHeader) map.set(new SchemaHeaderField(docHeaderString), [d]); + else map.get(existingHeader)!.push(d); + return map; + }, + new ObservableMap(this.colHeaderData?.map(hdata => [hdata, []] as [SchemaHeaderField, Doc[]]) ?? []) + ); + } // Still not sure what a pivot is, but it appears that we can actually filter docs somehow? @computed get pivotField() { return StrCast(this.layoutDoc._pivotField); @@ -107,9 +123,12 @@ export class CollectionStackingView extends CollectionSubView(); - } } + availableWidthFn = () => this.availableWidth; columnWidthFn = () => this.columnWidth; columnDocHeightFn = (doc: Doc) => () => (this.isStackingView ? this.getDocHeight(doc)() : Math.min(this.getDocHeight(doc)(), this._props.PanelHeight())); - // TODO: plj - these are the children children = (docs: Doc[]) => { - // TODO: can somebody explain me to what exactly TraceMobX is? TraceMobx(); - // appears that we are going to reset the _docXfs. TODO: what is Xfs? this._docXfs.length = 0; - this._renderCount < docs.length && - setTimeout( - action(() => { - this._renderCount = Math.min(docs.length, this._renderCount + 5); - }) - ); + this._renderCount < docs.length && + setTimeout(action(() => (this._renderCount = Math.min(docs.length, this._renderCount + 5)))); // prettier-ignore return docs.map((d, i) => { // assuming we need to get rowSpan because we might be dealing with many columns. Grid gap makes sense if multiple columns const rowSpan = Math.ceil((this.getDocHeight(d)() + this.gridGap) / this.gridGap); @@ -153,76 +161,28 @@ export class CollectionStackingView extends CollectionSubView +
{this.getDisplayDoc(d, this.getDocTransition(d), i)}
); }); }; @action - setDocHeight = (key: string, sectionHeight: number) => { - this._heightMap.set(key, sectionHeight); + setDocHeight = (key: string, sectionHeight: number) => this._heightMap.set(key, sectionHeight); + + setAutoHeight = () => { + const maxHeader = this.isStackingView ? this._colHdrRefs.reduce((p, r) => Math.max(p, DivHeight(r)), 0) + (this._colHdrRefs.length ? this.gridGap : 0) : 0; + const maxCol = this.isStackingView + ? this._colStackRefs.reduce((p, r) => Math.max(p, DivHeight(r)), 0) + this.gridGap + : this._colStackRefs.reduce((p, r) => p + DivHeight(r), this._addGroupRef.current ? DivHeight(this._addGroupRef.current) : 0); + this._props.setHeight?.(this.headerMargin + 2 * this.yMargin + maxCol + maxHeader); }; - - // is sections that all collections inherit? I think this is how we show the masonry/columns - // TODO: this seems important - get Sections() { - // appears that pivot field IS actually for sorting - if (!this.pivotField || this.colHeaderData instanceof Promise) return new Map(); - - if (this.colHeaderData === undefined) { - setTimeout(() => { - this.dataDoc['_' + this.fieldKey + '_columnHeaders'] = new List(); - }); - return new Map(); - } - const colHeaderData = Array.from(this.colHeaderData); - const fields = new Map(colHeaderData.map(sh => [sh, []] as [SchemaHeaderField, []])); - let changed = false; - this.filteredChildren.forEach(d => { - const sectionValue = (d[this.pivotField] ? d[this.pivotField] : `NO ${this.pivotField.toUpperCase()} VALUE`) as object; - // the next five lines ensures that floating point rounding errors don't create more than one section -syip - const parsed = parseInt(sectionValue.toString()); - const castedSectionValue = !isNaN(parsed) ? parsed : sectionValue; - - // look for if header exists already - const existingHeader = colHeaderData.find(sh => sh.heading === (castedSectionValue ? castedSectionValue.toString() : `NO ${this.pivotField.toUpperCase()} VALUE`)); - if (existingHeader) { - fields.get(existingHeader)!.push(d); - } else { - const newSchemaHeader = new SchemaHeaderField(castedSectionValue ? castedSectionValue.toString() : `NO ${this.pivotField.toUpperCase()} VALUE`); - fields.set(newSchemaHeader, [d]); - colHeaderData.push(newSchemaHeader); - changed = true; - } - }); - // remove all empty columns if hideHeadings is set - // we will want to have something like this, so that we can hide columns and add them back in - if (this.layoutDoc._columnsHideIfEmpty) { - Array.from(fields.keys()) - .filter(key => !fields.get(key)!.length) - .forEach(header => { - fields.delete(header); - colHeaderData.splice(colHeaderData.indexOf(header), 1); - changed = true; - }); - } - changed && - setTimeout( - action(() => this.colHeaderData?.splice(0, this.colHeaderData.length, ...colHeaderData)), - 0 - ); - return fields; - } - - setAutoHeight = () => this._props.setHeight?.(this.headerMargin + (this.isStackingView ? Math.max(...this._refList.map(DivHeight)) : 2 * this.yMargin + this._refList.reduce((p, r) => p + DivHeight(r), 0))); observer = new ResizeObserver(this.setAutoHeight); componentDidMount() { @@ -232,9 +192,7 @@ export class CollectionStackingView extends CollectionSubView this.pivotField, - () => { - this.dataDoc['_' + this.fieldKey + '_columnHeaders'] = new List(); - } + () => (this.dataDoc['_' + this.fieldKey + '_columnHeaders'] = new List()) ); // reset section headers when a new filter is inputted this._disposers.width = reaction( @@ -252,7 +210,7 @@ export class CollectionStackingView extends CollectionSubView ({ refList: this._refList.slice(), autoHeight: this.layoutDoc._layout_autoHeight && !DocumentView.LightboxContains(this.DocumentView?.()) }), + () => ({ refList: this._colStackRefs.slice(), autoHeight: this.layoutDoc._layout_autoHeight && !DocumentView.LightboxContains(this.DocumentView?.()) }), ({ refList, autoHeight }) => { this.observer.disconnect(); if (autoHeight) refList.forEach(r => this.observer.observe(r)); @@ -409,11 +367,10 @@ export class CollectionStackingView extends CollectionSubView () => { if (!d) return 0; const childLayoutDoc = Doc.LayoutDoc(d, this._props.childLayoutTemplate?.()); - const maxWidth = this.columnWidth / this.numGroupColumns; if (!this.layoutDoc._columnsFill && !this.childFitWidth(childLayoutDoc)) { - return Math.min(NumCast(d._width), maxWidth); + return Math.min(NumCast(d._width), this.columnWidth); } - return maxWidth; + return this.columnWidth; }); getDocTransition = computedFn((d?: Doc) => () => StrCast(d?.dataTransition)); getDocHeight = computedFn((d?: Doc) => () => { @@ -424,7 +381,7 @@ export class CollectionStackingView extends CollectionSubView { if (value && this.colHeaderData) { - const schemaHdrField = new SchemaHeaderField(value); - this.colHeaderData.push(schemaHdrField); + this.colHeaderData.push(new SchemaHeaderField(value)); return true; } return false; @@ -745,7 +704,11 @@ export class CollectionStackingView extends CollectionSubView this.isContentActive() && e.stopPropagation()}> {this.renderedSections} - {!this.showAddAGroup ? null : ( -
- -
- )} +
+ +
+
+ (this.layoutDoc._pivotField = fieldKey)} placeholder={StrCast(this.layoutDoc._pivotField)} /> +
diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx index 345f60e75..8c7cb8276 100644 --- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx +++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx @@ -1,5 +1,5 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx'; +import { action, computed, IReactionDisposer, makeObservable, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { DivHeight, DivWidth, returnEmptyString, returnTrue, setupMoveUpEvents } from '../../../ClientUtils'; @@ -50,14 +50,14 @@ interface CSVFieldColumnProps { addDocument: (doc: Doc | Doc[]) => boolean; createDropTarget: (ele: HTMLDivElement) => void; screenToLocalTransform: () => Transform; - refList: HTMLElement[]; + colStackRefs: HTMLElement[]; + colHeaderRefs: HTMLElement[]; } @observer export class CollectionStackingViewFieldColumn extends ObservableReactComponent { private dropDisposer?: DragManager.DragDropDisposer; private _disposers: { [name: string]: IReactionDisposer } = {}; - private _headerRef: React.RefObject = React.createRef(); @observable _background = 'inherit'; @observable _paletteOn = false; @observable _heading = ''; @@ -72,6 +72,7 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent< _ele: HTMLElement | null = null; _eleMasonrySingle = React.createRef(); + _headerRef: HTMLDivElement | null = null; protected onInternalPreDrop = (e: Event, de: DragManager.DropEvent, targetDropAction: dropActionType) => { const dragData = de.complete.docDragData; @@ -93,13 +94,13 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent< createColumnDropRef = (ele: HTMLDivElement | null) => { this.dropDisposer?.(); if (ele) this.dropDisposer = DragManager.MakeDropTarget(ele, this.columnDrop.bind(this), this._props.Doc, this.onInternalPreDrop.bind(this)); - else if (this._eleMasonrySingle.current) this.props.refList.splice(this.props.refList.indexOf(this._eleMasonrySingle.current), 1); + else if (this._eleMasonrySingle.current) runInAction(() => this.props.colStackRefs.splice(this.props.colStackRefs.indexOf(this._eleMasonrySingle.current!), 1)); this._ele = ele; }; @action componentDidMount() { - this._eleMasonrySingle.current && this.props.refList.push(this._eleMasonrySingle.current); + this._eleMasonrySingle.current && this.props.colStackRefs.push(this._eleMasonrySingle.current); this._disposers.collapser = reaction( () => this._props.headingObject?.collapsed, collapsed => { this.collapsed = collapsed !== undefined ? BoolCast(collapsed) : false; }, // prettier-ignore @@ -108,7 +109,7 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent< } componentWillUnmount() { this._disposers.collapser?.(); - this._ele && this.props.refList.splice(this.props.refList.indexOf(this._ele), 1); + this._ele && runInAction(() => this.props.colStackRefs.splice(this.props.colStackRefs.indexOf(this._ele!), 1)); this._ele = null; } @@ -192,7 +193,7 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent< value = typeof value === 'string' ? `"${value}"` : value; embedding.viewSpecScript = ScriptField.MakeFunction(`doc.${this._props.pivotField} === ${value}`, { doc: Doc.name }); if (embedding.viewSpecScript) { - DragManager.StartDocumentDrag([this._headerRef.current!], new DragManager.DocumentDragData([embedding]), e.clientX, e.clientY); + DragManager.StartDocumentDrag([this._headerRef!], new DragManager.DocumentDragData([embedding]), e.clientX, e.clientY); return true; } return false; @@ -314,9 +315,14 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
{ + if (this._headerRef && this._props.colHeaderRefs.includes(this._headerRef)) this._props.colHeaderRefs.splice(this._props.colStackRefs.indexOf(this._headerRef), 1); + r && this._props.colHeaderRefs.push(r); + this._headerRef = r; + }} style={{ - marginTop: this._props.yMargin, + marginTop: 0, + marginBottom: this._props.gridGap, width: this._props.columnWidth, }}> {/* the default bucket (no key value) has a tooltip that describes what it is. @@ -367,7 +373,7 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent< ref={this._eleMasonrySingle} className="collectionStackingView-masonrySingle" style={{ - padding: `${columnYMargin}px ${0}px ${this._props.yMargin}px ${0}px`, + padding: `${columnYMargin}px ${0}px ${0}px ${0}px`, margin: this._props.dontCenter.includes('x') ? undefined : 'auto', height: 'max-content', position: 'relative', @@ -400,15 +406,13 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent< render() { TraceMobx(); - const headings = this._props.headings(); const heading = this._heading; - const uniqueHeadings = headings.map((i, idx) => headings.indexOf(i) === idx); return (
m.type !== mark.type), mark]; if (selLoadChar === 'Enter') { @@ -1549,7 +1549,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent boundsRect.left && e.clientX < boundsRect.right && e.clientY > boundsRect.bottom) { // if we clicked below the last prosemirror div, then set the selection to be the end of the document editorView.focus(); - editorView.dispatch(editorView.state.tr.setSelection(TextSelection.create(editorView.state.doc, editorView.state.doc.content.size))); + // editorView.dispatch(editorView.state.tr.setSelection(TextSelection.create(editorView.state.doc, editorView.state.doc.content.size))); } } else if (node && [editorView.state.schema.nodes.ordered_list, editorView.state.schema.nodes.listItem].includes(node.type) && node !== (editorView.state.selection as NodeSelection)?.node && pcords) { editorView.dispatch(editorView.state.tr.setSelection(NodeSelection.create(editorView.state.doc, pcords.pos))); @@ -1814,7 +1814,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent m.type === schema.marks.user_mark && m.attrs.modified === modified); - _editorView.dispatch(state.tr.removeStoredMark(schema.marks.user_mark).addStoredMark(mark ?? schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified }))); + const mark = state.selection.$to.marks().find(m => m.type === schema.marks.user_mark); + _editorView.dispatch( + state.tr + .removeStoredMark(schema.marks.user_mark) // + .addStoredMark(mark ?? schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail() })) + ); } break; } @@ -1884,14 +1885,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent kids?.reduce((p, child) => p + toHgt(child), margins) ?? 0; + const getChildrenHeights = (kids: Element[] | undefined) => kids?.reduce((p, child) => p + toHgt(child), 0) ?? 0; const toNum = (val: string) => Number(val.replace('px', '')); const toHgt = (node: Element): number => { const { height, marginTop, marginBottom } = getComputedStyle(node); const childHeight = height === 'auto' ? getChildrenHeights(Array.from(node.children)) : toNum(height); return childHeight + Math.max(0, toNum(marginTop)) + Math.max(0, toNum(marginBottom)); }; - const proseHeight = !this.ProseRef ? 0 : getChildrenHeights(children); + const proseHeight = !this.ProseRef ? 0 : getChildrenHeights(children) + margins; const scrollHeight = this.ProseRef && proseHeight; if (this._props.setHeight && !this._props.suppressSetHeight && scrollHeight && !this._props.dontRegisterView) { // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation diff --git a/src/client/views/nodes/formattedText/marks_rts.ts b/src/client/views/nodes/formattedText/marks_rts.ts index b7dae1ca3..333ee6be8 100644 --- a/src/client/views/nodes/formattedText/marks_rts.ts +++ b/src/client/views/nodes/formattedText/marks_rts.ts @@ -330,20 +330,6 @@ export const marks: { [index: string]: MarkSpec } = { return ['span', { class: 'UM-' + uid + remote + ' UM-min-' + min + ' UM-hr-' + hr + ' UM-day-' + day }, 0]; }, }, - // the id of the user who entered the text - user_tag: { - attrs: { - userid: { default: '' }, - modified: { default: 'when?' }, // 1 second intervals since 1970 - tag: { default: '' }, - }, - group: 'inline', - inclusive: false, - toDOM: node => { - const uid = node.attrs.userid.replace('.', '').replace('@', ''); - return ['span', { class: 'UT-' + uid + ' UT-' + node.attrs.tag }, 0]; - }, - }, // :: MarkSpec Code font mark. Represented as a `` element. code: { -- cgit v1.2.3-70-g09d2 From fdaecd254fe56c5dab5b45258d6595ff9c5bcf42 Mon Sep 17 00:00:00 2001 From: bobzel Date: Tue, 20 May 2025 12:26:36 -0400 Subject: fixed creating list of headers in stacking view when it doesn't exist. start of making fields dropdown more reactive. --- src/client/views/FieldsDropdown.tsx | 18 ++++++++++-------- src/client/views/collections/CollectionPivotView.tsx | 8 +------- .../views/collections/CollectionStackingView.tsx | 9 ++++++--- 3 files changed, 17 insertions(+), 18 deletions(-) (limited to 'src/client/views/collections') diff --git a/src/client/views/FieldsDropdown.tsx b/src/client/views/FieldsDropdown.tsx index e7ab6a180..74ff8ffd4 100644 --- a/src/client/views/FieldsDropdown.tsx +++ b/src/client/views/FieldsDropdown.tsx @@ -6,7 +6,7 @@ * this list is then pruned down to only include fields that are not marked in Documents.ts to be non-filterable */ -import { computed, makeObservable, observable, runInAction } from 'mobx'; +import { action, computed, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import Select from 'react-select'; @@ -24,6 +24,7 @@ interface fieldsDropdownProps { placeholder?: string | (() => string); showPlaceholder?: true; // if true, then input field always shows the placeholder value; otherwise, it shows the current selection addedFields?: string[]; + isInactive?: boolean; } @observer @@ -57,10 +58,11 @@ export class FieldsDropdown extends ObservableReactComponent facet[0] === facet.charAt(0).toUpperCase())]; Object.entries(DocOptions) - .filter(opts => opts[1].filterable) - .forEach((pair: [string, FInfo]) => filteredOptions.push(pair[0])); + .filter(opts => opts[1] instanceof FInfo && opts[1].filterable) + .forEach((pair: [string, unknown]) => filteredOptions.push(pair[0])); const options = filteredOptions.sort().map(facet => ({ value: facet, label: facet })); + console.log(this._props.isInactive); return (