diff options
author | bobzel <zzzman@gmail.com> | 2025-04-30 14:43:29 -0400 |
---|---|---|
committer | bobzel <zzzman@gmail.com> | 2025-04-30 14:43:29 -0400 |
commit | 0317a667a5d4a0f298522eeddb8afa96e7b7ecfc (patch) | |
tree | 2d66c3caa5c102a3ca78837b87e61c16fd07fc01 | |
parent | 9ec7df6af508eccdfda3a195a69c8bb1f86c41ea (diff) | |
parent | 2296c314be710f983d595de37c9d8039d73568a6 (diff) |
Merge branch 'master' into task_nodes_aarav
86 files changed, 1279 insertions, 502 deletions
diff --git a/package-lock.json b/package-lock.json index 990f03462..b4c008ffd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -95,7 +95,6 @@ "cookie-parser": "^1.4.6", "cookie-session": "^2.0.0", "copy-webpack-plugin": "^13.0.0", - "copyfiles": "^2.4.1", "core-js": "^3.33.3", "cors": "^2.8.5", "css-loader": "^7.1.2", @@ -333,6 +332,7 @@ "@types/webscopeio__react-textarea-autocomplete": "^4.7.5", "@types/youtube": "^0.1.0", "chai": "^5.0.0", + "copyfiles": "^2.4.1", "cross-env": "^7.0.3", "eslint": "^9.12.0", "eslint-plugin-react": "^7.37.1", @@ -18747,6 +18747,7 @@ "version": "2.4.1", "resolved": "https://registry.npmjs.org/copyfiles/-/copyfiles-2.4.1.tgz", "integrity": "sha512-fereAvAvxDrQDOXybk3Qu3dPbOoKoysFMWtkY3mv5BsL8//OSZVL5DCLYqgRfY5cWirgRzlC+WSrxp6Bo3eNZg==", + "dev": true, "license": "MIT", "dependencies": { "glob": "^7.0.5", @@ -18767,6 +18768,7 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -27705,6 +27707,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, "license": "MIT", "bin": { "mkdirp": "bin/cmd.js" @@ -28566,6 +28569,7 @@ "version": "0.0.0", "resolved": "https://registry.npmjs.org/noms/-/noms-0.0.0.tgz", "integrity": "sha512-lNDU9VJaOPxUmXcLb+HQFeUgQQPtMI24Gt6hgfuMHRJgMRHMF/qZ4HJD3GDru4sSw9IQl2jPjAYnQrdIeLbwow==", + "dev": true, "license": "ISC", "dependencies": { "inherits": "^2.0.1", @@ -28576,12 +28580,14 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "dev": true, "license": "MIT" }, "node_modules/noms/node_modules/readable-stream": { "version": "1.0.34", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", "integrity": "sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==", + "dev": true, "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", @@ -28594,6 +28600,7 @@ "version": "0.10.31", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", + "dev": true, "license": "MIT" }, "node_modules/normalize-path": { @@ -37286,6 +37293,7 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, "license": "MIT", "dependencies": { "readable-stream": "~2.3.6", @@ -37296,12 +37304,14 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, "license": "MIT" }, "node_modules/through2/node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", @@ -37317,12 +37327,14 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, "license": "MIT" }, "node_modules/through2/node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "~5.1.0" @@ -38769,6 +38781,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -40229,6 +40242,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.4" diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts index 4b86a8341..15fd6313a 100644 --- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts +++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts @@ -311,7 +311,7 @@ export namespace GooglePhotos { sources .filter(source => ImageCast(Doc.GetProto(source).data)) .forEach(async source => { - const data = ImageCast(Doc.GetProto(source).data); + const data = ImageCast(Doc.GetProto(source).data)!; const url = data.url.href; const target = Doc.MakeEmbedding(source); const description = parseDescription(target, descriptionKey); diff --git a/src/client/documents/DocUtils.ts b/src/client/documents/DocUtils.ts index 7b36c3a66..df14dce5a 100644 --- a/src/client/documents/DocUtils.ts +++ b/src/client/documents/DocUtils.ts @@ -308,6 +308,7 @@ export namespace DocUtils { */ export async function DocumentFromType(type: string, path: string, options: DocumentOptions, overwriteDoc?: Doc): Promise<Opt<Doc>> { let ctor: ((path: string, options: DocumentOptions, overwriteDoc?: Doc) => Doc | Promise<Doc | undefined>) | undefined; + if (type.indexOf('image') !== -1) { ctor = Docs.Create.ImageDocument; if (!options._width) options._width = 300; @@ -711,33 +712,31 @@ export namespace DocUtils { nativeWidth: 40, nativeHeight: 40, }) - : (defaultTextTemplate?.type === DocumentType.JOURNAL ? Docs.Create.DailyJournalDocument:Docs.Create.TextDocument)( - '', - { - annotationOn, - backgroundColor, - x, - y, - title, - ...(defaultTextTemplate - ? {} // if the new doc will inherit from a template, don't set any layout fields since that would block the inheritance - : { - _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), - borderColor: Doc.UserDoc().borderColor as string, - borderWidth: Doc.UserDoc().borderWidth as number, - text_centered: BoolCast(Doc.UserDoc().textCentered), - text_fitBox: BoolCast(Doc.UserDoc().fitBox), - text_align: StrCast(Doc.UserDoc().textAlign), - text_fontColor: StrCast(Doc.UserDoc().fontColor), - text_fontFamily: StrCast(Doc.UserDoc().fontFamily), - text_fontWeight: StrCast(Doc.UserDoc().fontWeight), - text_fontStyle: StrCast(Doc.UserDoc().fontStyle), - text_fontDecoration: StrCast(Doc.UserDoc().fontDecoration), - }), - }); + : (defaultTextTemplate?.type === DocumentType.JOURNAL ? Docs.Create.DailyJournalDocument : Docs.Create.TextDocument)('', { + annotationOn, + backgroundColor, + x, + y, + title, + ...(defaultTextTemplate + ? {} // if the new doc will inherit from a template, don't set any layout fields since that would block the inheritance + : { + _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), + borderColor: Doc.UserDoc().borderColor as string, + borderWidth: Doc.UserDoc().borderWidth as number, + text_centered: BoolCast(Doc.UserDoc().textCentered), + text_fitBox: BoolCast(Doc.UserDoc().fitBox), + text_align: StrCast(Doc.UserDoc().textAlign), + text_fontColor: StrCast(Doc.UserDoc().fontColor), + text_fontFamily: StrCast(Doc.UserDoc().fontFamily), + text_fontWeight: StrCast(Doc.UserDoc().fontWeight), + text_fontStyle: StrCast(Doc.UserDoc().fontStyle), + text_fontDecoration: StrCast(Doc.UserDoc().fontDecoration), + }), + }); if (defaultTextTemplate) { tbox.layout_fieldKey = 'layout_' + StrCast(defaultTextTemplate.title); diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts index 83faf6ed1..a73d8ba59 100644 --- a/src/client/documents/DocumentTypes.ts +++ b/src/client/documents/DocumentTypes.ts @@ -46,6 +46,7 @@ export enum DocumentType { JOURNAL = 'journal', // AARAV ADD JOURNAL TASK = 'task', // AARAV ADD TASK + SCRAPBOOK = 'scrapbook', } export enum CollectionViewType { // general collections diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 112a73e46..217967c52 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -536,6 +536,15 @@ export class DocumentOptions { allDay?: BoolInfo | boolean = new BoolInfo('all-day task', /*filterable*/ false); /** Whether the task is completed */ completed?: BoolInfo | boolean = new BoolInfo('whether the task is completed', /*filterable*/ false); + /** + * JSON‐stringified slot configuration for ScrapbookBox + */ + scrapbookConfig?: string; + + /** + * The list of embedded Doc instances in each Scrapbook slot + */ + scrapbookContents?: List<Doc>; } export const DocOptions = new DocumentOptions(); @@ -941,6 +950,25 @@ export namespace Docs { return InstanceFromProto(Prototypes.get(DocumentType.RTF), field, options, undefined, fieldKey); } + export function ScrapbookDocument(items: Doc[] = [], options: DocumentOptions = {}, fieldKey: string = 'items') { + return InstanceFromProto( + Prototypes.get(DocumentType.SCRAPBOOK), + new List<Doc>(items), + { + title: + options.title ?? + new Date().toLocaleDateString(undefined, { + year: 'numeric', + month: 'short', + day: 'numeric', + }), + ...options, + }, + undefined, + fieldKey + ); + } + // AARAV ADD // export function DailyJournalDocument(text: string | RichTextField, options: DocumentOptions = {}, fieldKey: string = 'text') { diff --git a/src/client/util/CalendarManager.tsx b/src/client/util/CalendarManager.tsx index 4e321a893..b50e39c02 100644 --- a/src/client/util/CalendarManager.tsx +++ b/src/client/util/CalendarManager.tsx @@ -162,7 +162,7 @@ export class CalendarManager extends ObservableReactComponent<object> { console.log('my calendars: ', Doc.MyCalendars); if (this.creationType === 'new-calendar') { - Doc.AddDocToList(Doc.MyCalendars, 'data', calendar); // add to new calendar to dashboard calendars + Doc.MyCalendars && Doc.AddDocToList(Doc.MyCalendars, 'data', calendar); // add to new calendar to dashboard calendars } } }; diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 121af72ed..1288abd3e 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -408,6 +408,8 @@ pie title Minerals in my tap water // AARAV ADD // {key: "DailyJournal",creator:opts => Docs.Create.DailyJournalDocument("", opts),opts: { _width: 300, _height: 300, }}, {key: "Task", creator: opts => Docs.Create.TaskDocument("", opts), opts: { _width: 250, _height: 100, _layout_autoHeight: true, title: "Task", }}, + {key: "Scrapbook", creator: opts => Docs.Create.ScrapbookDocument([], opts), opts: { _width: 300, _height: 300}}, + //{key: "Scrapbook",creator:opts => Docs.Create.ScrapbookDocument([], opts),opts:{ _width: 300, _height: 300}}, {key: "Chat", creator: Docs.Create.ChatDocument, opts: { _width: 500, _height: 500, _layout_fitWidth: true, }}, {key: "MetaNote", creator: metaNoteTemplate, opts: { _width: 300, _height: 120, _header_pointerEvents: "all", _header_height: 50, _header_fontSize: 9,_layout_autoHeightMargins: 50, _layout_autoHeight: true, treeView_HideUnrendered: true}}, {key: "ViewSlide", creator: slideView, opts: { _width: 400, _height: 300, _xMargin: 3, _yMargin: 3,}}, @@ -450,6 +452,7 @@ pie title Minerals in my tap water { toolTip: "Tap or drag to create a button", title: "Button", icon: "circle", dragFactory: doc.emptyButton as Doc, clickFactory: DocCast(doc.emptyButton)}, { toolTip: "Tap or drag to create a scripting box", title: "Script", icon: "terminal", dragFactory: doc.emptyScript as Doc, clickFactory: DocCast(doc.emptyScript), funcs: { hidden: "IsNoviceMode()"}}, { toolTip: "Tap or drag to create a data viz node", title: "DataViz", icon: "chart-bar", dragFactory: doc.emptyDataViz as Doc, clickFactory: DocCast(doc.emptyDataViz)}, + { toolTip: "Tap or drag to create a scrapbook template", title: "Scrapbook", icon: "palette", dragFactory: doc.emptyScrapbook as Doc,clickFactory:DocCast(doc.emptyScrapbook), }, { toolTip: "Tap or drag to create a journal entry", title: "Journal", icon: "book", dragFactory:doc.emptyDailyJournal as Doc,clickFactory: DocCast(doc.emptyDataJournal), }, { toolTip: "Tap or drag to create a task", title: "Task", icon: "check-square", dragFactory: doc.emptyTask as Doc, clickFactory: DocCast(doc.emptyTask), }, { toolTip: "Tap or drag to create a bullet slide", title: "PPT Slide", icon:"person-chalkboard",dragFactory: doc.emptySlide as Doc, clickFactory: DocCast(doc.emptySlide), openFactoryLocation: OpenWhere.overlay, funcs: { hidden: "IsNoviceMode()"}}, diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index ad57c2a62..3bae2881e 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -173,10 +173,10 @@ export class DocumentManager { while ( containerDocContext.length && DocCast(containerDocContext[0]?.embedContainer) && - DocCast(containerDocContext[0].embedContainer)?._type_collection !== CollectionViewType.Docking && + DocCast(containerDocContext[0].embedContainer)!._type_collection !== CollectionViewType.Docking && (includeExistingViews || !DocumentManager.Instance.getDocumentView(containerDocContext[0])) ) { - containerDocContext = [Cast(containerDocContext[0].embedContainer, Doc, null), ...containerDocContext]; + containerDocContext = [DocCast(containerDocContext[0].embedContainer)!, ...containerDocContext]; } return containerDocContext; } @@ -248,7 +248,7 @@ export class DocumentManager { finished?: (changed: boolean) => void // func called after focusing on target with flag indicating whether anything needed to be done. ) => { const options = optionsIn; - Doc.RemoveDocFromList(Doc.MyRecentlyClosed, undefined, targetDoc); + Doc.MyRecentlyClosed && Doc.RemoveDocFromList(Doc.MyRecentlyClosed, undefined, targetDoc); const docContextPath = DocumentManager.GetContextPath(targetDoc, true); if (docContextPath.some(doc => doc.hidden)) options.toggleTarget = false; let activatedTab = false; @@ -272,7 +272,7 @@ export class DocumentManager { })); if (options.openLocation?.includes(OpenWhere.lightbox)) { // 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 target = DocCast(targetDoc.annotationOn, targetDoc)!; const compView = this.getDocumentView(DocCast(target.embedContainer))?.ComponentView; if ((compView?.addDocTab ?? compView?._props.addDocTab)?.(target, options.openLocation)) { await new Promise<void>(waitres => { diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts index 344e2e4c0..d8e0c4cbe 100644 --- a/src/client/util/LinkManager.ts +++ b/src/client/util/LinkManager.ts @@ -217,7 +217,8 @@ export class LinkManager { } // finds the opposite anchor of a given anchor in a link - public static getOppositeAnchor(linkDoc: Doc, anchor: Doc): Doc | undefined { + public static getOppositeAnchor(linkDoc: Doc | undefined, anchor: Doc | undefined): Doc | undefined { + if (!linkDoc || !anchor) return undefined; const id = LinkManager.anchorIndex(linkDoc, anchor); const a1 = DocCast(linkDoc.link_anchor_1); const a2 = DocCast(linkDoc.link_anchor_2); diff --git a/src/client/util/ReplayMovements.ts b/src/client/util/ReplayMovements.ts index 62a09a8bc..4f0423342 100644 --- a/src/client/util/ReplayMovements.ts +++ b/src/client/util/ReplayMovements.ts @@ -108,9 +108,11 @@ export class ReplayMovements { movements.forEach((movement, i) => { if (typeof movement.doc === 'string') { - movements[i].doc = IdToDoc(movement.doc); - if (!movements[i].doc) { + const doc = IdToDoc(movement.doc); + if (!doc) { console.log('ERROR: tracked doc not found'); + } else { + movements[i].doc = doc; } } }); diff --git a/src/client/util/ScriptManager.ts b/src/client/util/ScriptManager.ts index 9158f6c0b..8c7f88bf6 100644 --- a/src/client/util/ScriptManager.ts +++ b/src/client/util/ScriptManager.ts @@ -1,7 +1,6 @@ -import { Doc, DocListCast } from '../../fields/Doc'; +import { Doc, DocListCast, StrListCast } from '../../fields/Doc'; import { List } from '../../fields/List'; -import { listSpec } from '../../fields/Schema'; -import { Cast, StrCast } from '../../fields/Types'; +import { StrCast } from '../../fields/Types'; import { Docs } from '../documents/Documents'; import { ScriptingGlobals } from './ScriptingGlobals'; @@ -10,7 +9,6 @@ export class ScriptManager { // eslint-disable-next-line no-use-before-define private static _instance: ScriptManager; public static get Instance(): ScriptManager { - // eslint-disable-next-line no-return-assign return this._instance || (this._instance = new this()); } private constructor() { @@ -58,7 +56,7 @@ export class ScriptManager { public static addScriptToGlobals(scriptDoc: Doc): void { ScriptingGlobals.removeGlobal(StrCast(scriptDoc.name)); - const params = Cast(scriptDoc['data-params'], listSpec('string'), []); + const params = StrListCast(scriptDoc['data-params']); const paramNames = params.reduce((o: string, p: string) => { let out = o; if (params.indexOf(p) === params.length - 1) { @@ -69,7 +67,6 @@ export class ScriptManager { return out; }, '' as string); - // eslint-disable-next-line no-new-func const f = new Function(paramNames, StrCast(scriptDoc.script)); Object.defineProperty(f, 'name', { value: StrCast(scriptDoc.name), writable: false }); diff --git a/src/client/util/SearchUtil.ts b/src/client/util/SearchUtil.ts index e4adcaa7e..fc3bb99ab 100644 --- a/src/client/util/SearchUtil.ts +++ b/src/client/util/SearchUtil.ts @@ -13,7 +13,7 @@ export namespace SearchUtil { const blockedKeys = matchKeyNames ? [] : Object.entries(DocOptions) - .filter(([, info]: [string, FInfo]) => !info?.searchable()) + .filter(([, info]: [string, FieldType | FInfo | undefined]) => (info instanceof FInfo ? !info.searchable() : true)) .map(([key]) => key); const exact = queryIn.startsWith('='); @@ -22,8 +22,7 @@ export namespace SearchUtil { const results = new ObservableMap<Doc, string[]>(); if (collectionDoc) { const docs = DocListCast(collectionDoc[Doc.LayoutDataKey(collectionDoc)]); - // eslint-disable-next-line @typescript-eslint/ban-types - const docIDs: String[] = []; + const docIDs: string[] = []; SearchUtil.foreachRecursiveDoc(docs, (depth: number, doc: Doc) => { const dtype = StrCast(doc.type) as DocumentType; if (dtype && !blockedTypes.includes(dtype) && !docIDs.includes(doc[Id]) && depth >= 0) { diff --git a/src/client/util/reportManager/ReportManagerComponents.tsx b/src/client/util/reportManager/ReportManagerComponents.tsx index 92f877859..80653779e 100644 --- a/src/client/util/reportManager/ReportManagerComponents.tsx +++ b/src/client/util/reportManager/ReportManagerComponents.tsx @@ -1,5 +1,3 @@ -/* eslint-disable react/require-default-props */ -/* eslint-disable prefer-destructuring */ /* eslint-disable no-use-before-define */ import * as React from 'react'; import ReactMarkdown from 'react-markdown'; @@ -299,7 +297,7 @@ export function IssueView({ issue }: IssueViewProps) { </div> </div> )} - <ReactMarkdown className="issue-content" remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]}> + <ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]}> {issueBody} </ReactMarkdown> </div> diff --git a/src/client/views/DashboardView.tsx b/src/client/views/DashboardView.tsx index f61f6db18..3ceb23ffd 100644 --- a/src/client/views/DashboardView.tsx +++ b/src/client/views/DashboardView.tsx @@ -113,12 +113,12 @@ export class DashboardView extends ObservableReactComponent<object> { getDashboards = (whichGroup: DashboardGroup) => { if (whichGroup === DashboardGroup.MyDashboards) { - return DocListCast(Doc.MyDashboards.data).filter(dashboard => dashboard.$author === ClientUtils.CurrentUserEmail()); + return DocListCast(Doc.MyDashboards?.data).filter(dashboard => dashboard.$author === ClientUtils.CurrentUserEmail()); } - return DocListCast(Doc.MySharedDocs.data_dashboards).filter(doc => doc.dockingConfig); + return DocListCast(Doc.MySharedDocs?.data_dashboards).filter(doc => doc.dockingConfig); }; - isUnviewedSharedDashboard = (dashboard: Doc) => !DocListCast(Doc.MySharedDocs.viewed).includes(dashboard); + isUnviewedSharedDashboard = (dashboard: Doc) => !DocListCast(Doc.MySharedDocs?.viewed).includes(dashboard); @undoBatch createNewDashboard = (name: string, background?: string) => { @@ -155,7 +155,7 @@ export class DashboardView extends ObservableReactComponent<object> { @action openNewDashboardModal = () => { this.openModal = true; - this.setNewDashboardName(`Dashboard ${DocListCast(Doc.MyDashboards.data).length + 1}`); + this.setNewDashboardName(`Dashboard ${DocListCast(Doc.MyDashboards?.data).length + 1}`); }; _downX: number = 0; @@ -191,7 +191,7 @@ export class DashboardView extends ObservableReactComponent<object> { <Button text={'Shared Dashboards (' + this.getDashboards(DashboardGroup.SharedDashboards).length + ')'} active={this.selectedDashboardGroup === DashboardGroup.SharedDashboards} - color={this.getDashboards(DashboardGroup.SharedDashboards).some(dash => !DocListCast(Doc.MySharedDocs.viewed).includes(dash)) ? 'green' : color} + color={this.getDashboards(DashboardGroup.SharedDashboards).some(dash => !DocListCast(Doc.MySharedDocs?.viewed).includes(dash)) ? 'green' : color} align="flex-start" onClick={() => this.selectDashboardGroup(DashboardGroup.SharedDashboards)} fillWidth @@ -275,7 +275,7 @@ export class DashboardView extends ObservableReactComponent<object> { } public static openSharedDashboard = (dashboard: Doc) => { - Doc.AddDocToList(Doc.MySharedDocs, 'viewed', dashboard); + Doc.MySharedDocs && Doc.AddDocToList(Doc.MySharedDocs, 'viewed', dashboard); DashboardView.openDashboard(Doc.BestEmbedding(dashboard)); }; @@ -283,8 +283,8 @@ export class DashboardView extends ObservableReactComponent<object> { /// this also sets the readonly state of the dashboard based on the current mode of dash (from its url) public static openDashboard = (doc: Doc | undefined, fromHistory = false) => { if (!doc) return false; - Doc.AddDocToList(Doc.MyDashboards, 'data', doc); - Doc.RemoveDocFromList(Doc.MyRecentlyClosed, 'data', doc); + Doc.MyDashboards && Doc.AddDocToList(Doc.MyDashboards, 'data', doc); + Doc.MyRecentlyClosed && Doc.RemoveDocFromList(Doc.MyRecentlyClosed, 'data', doc); // this has the side-effect of setting the main container since we're assigning the active/guest dashboard Doc.UserDoc() ? (Doc.ActiveDashboard = doc) : (Doc.GuestDashboard = doc); @@ -316,11 +316,11 @@ export class DashboardView extends ObservableReactComponent<object> { }; public static removeDashboard = (dashboard: Doc) => { - const dashboards = DocListCast(Doc.MyDashboards.data).filter(dash => dash !== dashboard); + const dashboards = DocListCast(Doc.MyDashboards?.data).filter(dash => dash !== dashboard); undoable(() => { if (dashboard === Doc.ActiveDashboard) DashboardView.openDashboard(dashboards.lastElement()); - Doc.RemoveDocFromList(Doc.MyDashboards, 'data', dashboard); - Doc.AddDocToList(Doc.MyRecentlyClosed, 'data', dashboard, undefined, true, true); + Doc.MyDashboards && Doc.RemoveDocFromList(Doc.MyDashboards, 'data', dashboard); + Doc.MyRecentlyClosed && Doc.AddDocToList(Doc.MyRecentlyClosed, 'data', dashboard, undefined, true, true); if (!dashboards.length) Doc.ActivePage = 'home'; }, 'remove dashboard')(); }; @@ -409,7 +409,7 @@ export class DashboardView extends ObservableReactComponent<object> { }; public static createNewDashboard = (id?: string, name?: string, background?: string) => { - const dashboardCount = DocListCast(Doc.MyDashboards.data).length + 1; + const dashboardCount = DocListCast(Doc.MyDashboards?.data).length + 1; const freeformOptions: DocumentOptions = { x: 0, y: 400, @@ -425,8 +425,8 @@ export class DashboardView extends ObservableReactComponent<object> { const freeformDoc = Doc.GuestTarget || Docs.Create.FreeformDocument([], freeformOptions); const dashboardDoc = DashboardView.StandardCollectionDockingDocument([{ doc: freeformDoc, initialWidth: 600 }], { title: title }, id, 'row'); - Doc.AddDocToList(Doc.MyHeaderBar, 'data', freeformDoc, undefined, undefined, true); - Doc.AddDocToList(Doc.MyDashboards, 'data', dashboardDoc); + Doc.MyHeaderBar && Doc.AddDocToList(Doc.MyHeaderBar, 'data', freeformDoc, undefined, undefined, true); + Doc.MyDashboards && Doc.AddDocToList(Doc.MyDashboards, 'data', dashboardDoc); freeformDoc._embedContainer = dashboardDoc; dashboardDoc.$myPaneCount = 1; dashboardDoc.$myOverlayDocs = new List<Doc>(); diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx index 8a850467a..bc669fc4e 100644 --- a/src/client/views/DocumentButtonBar.tsx +++ b/src/client/views/DocumentButtonBar.tsx @@ -507,7 +507,7 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => ( )} {DocumentLinksButton.StartLink && DocumentLinksButton.StartLink !== doc ? <div className="documentButtonBar-button">{this.endLinkButton} </div> : null} - <div className="documentButtonBar-button">{this.templateButton}</div> + {Doc.noviceMode ? null : <div className="documentButtonBar-button">{this.templateButton}</div>} {!DocumentView.Selected().some(v => v.allLinks.length) ? null : <div className="documentButtonBar-button">{this.followLinkButton}</div>} <div className="documentButtonBar-button">{this.pinButton}</div> <div className="documentButtonBar-button">{this.recordButton}</div> diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 120467e8a..ab665e984 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -240,7 +240,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora } else { // if Doc is in the sticker palette, remove the flag indicating that it's saved const dragFactory = DocCast(iconView.Document.dragFactory); - if (dragFactory && DocCast(dragFactory.cloneOf).savedAsSticker) DocCast(dragFactory.cloneOf).savedAsSticker = undefined; + if (dragFactory && DocCast(dragFactory.cloneOf)?.savedAsSticker) DocCast(dragFactory.cloneOf)!.savedAsSticker = undefined; // if this is a face Annotation doc, then just hide it. if (iconView.Document.annotationOn && iconView.Document.face) iconView.Document.hidden = true; @@ -445,6 +445,12 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora @action onPointerDown = (e: React.PointerEvent): void => { SnappingManager.SetIsResizing(DocumentView.Selected().lastElement()?.Document[Id]); // turns off pointer events on things like youtube videos and web pages so that dragging doesn't get "stuck" when cursor moves over them + DocumentView.Selected() + .filter(dv => e.shiftKey && dv.ComponentView instanceof ImageBox) + .forEach(dv => { + dv.Document[dv.ComponentView!.fieldKey + '_outpaintOriginalWidth'] = NumCast(dv.Document._width); + dv.Document[dv.ComponentView!.fieldKey + '_outpaintOriginalHeight'] = NumCast(dv.Document._height); + }); setupMoveUpEvents(this, e, this.onPointerMove, this.onPointerUp, emptyFunction); e.stopPropagation(); const id = (this._resizeHdlId = e.currentTarget.className); @@ -473,6 +479,8 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora const tl = docView.screenToContentsTransform().inverse().transformPoint(0, 0); return project([e.clientX + this._offset.x, e.clientY + this._offset.y], tl, [tl[0] + fixedAspect, tl[1] + 1]); }; + + // Modify the onPointerMove method to handle shift+click during resize onPointerMove = (e: PointerEvent): boolean => { const first = DocumentView.Selected()[0]; const effectiveAcl = GetEffectiveAcl(first.Document); @@ -482,26 +490,92 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora const dragHdl = this._resizeHdlId.split(' ')[0].replace('documentDecorations-', '').replace('Resizer', ''); const thisPt = // do snapping of drag point - fixedAspect && (dragHdl === 'bottomRight' || dragHdl === 'topLeft') + fixedAspect && (dragHdl === 'bottomRight' || dragHdl === 'topLeft') && e.ctrlKey ? DragManager.snapDragAspect(this.projectDragToAspect(e, first, fixedAspect), fixedAspect) : DragManager.snapDrag(e, -this._offset.x, -this._offset.y, this._offset.x, this._offset.y); const { scale, refPt } = this.getResizeVals(thisPt, dragHdl); - !this._interactionLock && runInAction(async () => { // resize selected docs if we're not in the middle of a resize (ie, throttle input events to frame rate) - this._interactionLock = true; - this._snapPt = thisPt; - e.ctrlKey && (DocumentView.Selected().forEach(docView => !Doc.NativeHeight(docView.Document) && docView.toggleNativeDimensions())); - const hasFixedAspect = DocumentView.Selected().map(dv => dv.Document).some(this.hasFixedAspect); - const scaleAspect = {x:scale.x === 1 && hasFixedAspect ? scale.y : scale.x, y: scale.x !== 1 && hasFixedAspect ? scale.x : scale.y}; - DocumentView.Selected().forEach(docView => - this.resizeView(docView, refPt, scaleAspect, { dragHdl, ctrlKey:e.ctrlKey })); // prettier-ignore - await new Promise<void>(res => { setTimeout(() => { res(this._interactionLock = undefined)})}); - }); // prettier-ignore + !this._interactionLock && + runInAction(() => { + // resize selected docs if we're not in the middle of a resize (ie, throttle input events to frame rate) + this._interactionLock = true; + this._snapPt = thisPt; + + const outpainted = e.shiftKey ? DocumentView.Selected().filter(dv => dv.ComponentView instanceof ImageBox) : []; + const notOutpainted = e.shiftKey ? DocumentView.Selected().filter(dv => !outpainted.includes(dv)) : DocumentView.Selected(); + + // Special handling for shift-drag resize (outpainting of Images by resizing without scaling content - fill in with firefly GAI) + e.shiftKey && outpainted.forEach(dv => this.resizeViewForOutpainting(dv, refPt, scale, { dragHdl, shiftKey: e.shiftKey })); + + // Special handling for not outpainted Docs when ctrl-resizing (setup native dimesions for modification) + e.ctrlKey && notOutpainted.forEach(docView => !Doc.NativeHeight(docView.Document) && docView.toggleNativeDimensions()); + + const hasFixedAspect = notOutpainted.map(dv => dv.Document).some(this.hasFixedAspect); + const scaleAspect = { x: scale.x === 1 && hasFixedAspect ? scale.y : scale.x, y: scale.x !== 1 && hasFixedAspect ? scale.x : scale.y }; + notOutpainted.forEach(docView => this.resizeView(docView, refPt, scaleAspect, { dragHdl, freezeNativeDims: e.ctrlKey })); + + new Promise<void>(res => setTimeout(() => res((this._interactionLock = undefined)))); + }); return false; }; + resizeViewForOutpainting = (docView: DocumentView, refPt: number[], scale: { x: number; y: number }, opts: { dragHdl: string; shiftKey: boolean }) => { + const doc = docView.Document; + + if (doc.isGroup) { + DocListCast(doc.data) + .map(member => DocumentView.getDocumentView(member, docView)!) + .forEach(member => this.resizeViewForOutpainting(member, refPt, scale, opts)); + doc.xPadding = NumCast(doc.xPadding) * scale.x; + doc.yPadding = NumCast(doc.yPadding) * scale.y; + return; + } + + // Calculate new boundary dimensions + const originalWidth = NumCast(doc._width); + const originalHeight = NumCast(doc._height); + const newWidth = Math.max(NumCast(doc._width_min, 25), originalWidth * scale.x); + const newHeight = Math.max(NumCast(doc._height_min, 10), originalHeight * scale.y); + + // Apply new dimensions + doc._width = newWidth; + doc._height = newHeight; + + const refCent = docView.screenToViewTransform().transformPoint(refPt[0], refPt[1]); + const { deltaX, deltaY } = this.realignRefPt(doc, refCent, originalWidth, originalHeight); + doc.x = NumCast(doc.x) + deltaX; + doc.y = NumCast(doc.y) + deltaY; + doc._layout_modificationDate = new DateField(); + }; + + @action + onPointerUp = () => { + SnappingManager.SetIsResizing(undefined); + SnappingManager.clearSnapLines(); + + this._resizeHdlId = ''; + this._resizeUndo?.end(); + + // detect layout_autoHeight gesture and apply + DocumentView.Selected().forEach(view => { + NumCast(view.Document._height) < 20 && (view.layoutDoc._layout_autoHeight = true); + }); + // need to change points for resize, or else rotation/control points will fail. + this._inkDragDocs + .map(oldbds => ({ oldbds, inkPts: Cast(oldbds.doc.data, InkField)?.inkData || [] })) + .forEach(({ oldbds: { doc, x, y, width, height }, inkPts }) => { + doc.$data = new InkField(inkPts.map( + (ipt) => ({// (new x — oldx) + newWidth * (oldxpoint /oldWidth) + X: NumCast(doc.x) - x + (NumCast(doc._width) * ipt.X) / width, + Y: NumCast(doc.y) - y + (NumCast(doc._height) * ipt.Y) / height, + }))); // prettier-ignore + Doc.SetNativeWidth(doc, undefined); + Doc.SetNativeHeight(doc, undefined); + }); + }; + // // determines how much to resize, and determines the resize reference point // @@ -529,7 +603,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora // // 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; ctrlKey: boolean }) => { + resizeView = (docView: DocumentView, refPt: number[], scale: { x: number; y: number }, opts: { dragHdl: string; freezeNativeDims: boolean }) => { const doc = docView.Document; if (doc.isGroup) { DocListCast(doc.data) @@ -542,25 +616,24 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora const [nwidth, nheight] = [docView.nativeWidth, docView.nativeHeight]; const [initWidth, initHeight] = [NumCast(doc._width, 1), NumCast(doc._height)]; - const modifyNativeDim = - (opts.ctrlKey && doc._layout_nativeDimEditable) || // e.g., PDF or web page - (doc._layout_reflowHorizontal && opts.dragHdl !== 'bottom' && opts.dragHdl !== 'top') || // eg rtf or some web pages - (doc._layout_reflowVertical && (opts.dragHdl === 'bottom' || opts.dragHdl === 'top' || opts.ctrlKey)); // eg rtf, web, pdf - if (nwidth && nheight && !modifyNativeDim) { - // eg., dragging right resizer on PDF -- enforce native dimensions because not expliclty overridden with ctrl or bottom resize drag + const cornerReflow = !opts.freezeNativeDims && doc._layout_reflowHorizontal && doc._layout_reflowVertical && ['topLeft', 'topRight', 'bottomLeft', 'bottomRight'].includes(opts.dragHdl); + const horizontalReflow = !opts.freezeNativeDims && doc._layout_reflowHorizontal && ['left', 'right'].includes(opts.dragHdl); // eg rtf or some web pages + const verticalReflow = !opts.freezeNativeDims && doc._layout_reflowVertical && ['bottom', 'top'].includes(opts.dragHdl); // eg rtf, web, pdf + // preserve aspect ratio if Doc has a native ize and drag won't cause reflow (eg, ctrl-dragging a Doc's corner doesn't allow reflow, or dragging right side of a PDF which isn't horizontally reflowable) + if (nwidth && nheight && !cornerReflow && !horizontalReflow && !verticalReflow) { scale.x === 1 ? (scale.x = scale.y) : (scale.y = scale.x); } - if (['right', 'left', 'bottomRight'].includes(opts.dragHdl) && modifyNativeDim && Doc.NativeWidth(doc)) { + if ((horizontalReflow || cornerReflow) && Doc.NativeWidth(doc)) { const setData = Doc.NativeWidth(doc[DocData]) === doc.nativeWidth; - doc.nativeWidth = scale.x * Doc.NativeWidth(doc); + doc._nativeWidth = scale.x * Doc.NativeWidth(doc); if (setData) Doc.SetNativeWidth(doc[DocData], NumCast(doc.nativeWidth)); if (doc._layout_reflowVertical && !NumCast(doc.nativeHeight)) { doc._nativeHeight = (initHeight / initWidth) * nwidth; // initializes the nativeHeight for a PDF } } - if (['bottom', 'top', 'bottomRight'].includes(opts.dragHdl) && modifyNativeDim && Doc.NativeHeight(doc)) { - const setData = Doc.NativeHeight(doc[DocData]) === doc.nativeHeight && (!doc.layout_reflowVertical || opts.ctrlKey); + if ((verticalReflow || cornerReflow) && Doc.NativeHeight(doc)) { + const setData = Doc.NativeHeight(doc[DocData]) === doc.nativeHeight && !doc.layout_reflowVertical; doc._nativeHeight = scale.y * Doc.NativeHeight(doc); if (setData) Doc.SetNativeHeight(doc[DocData], NumCast(doc._nativeHeight)); } @@ -606,31 +679,6 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora }; }; - @action - onPointerUp = (): void => { - SnappingManager.SetIsResizing(undefined); - SnappingManager.clearSnapLines(); - this._resizeHdlId = ''; - this._resizeUndo?.end(); - - // detect layout_autoHeight gesture and apply - DocumentView.Selected().forEach(view => { - NumCast(view.Document._height) < 20 && (view.layoutDoc._layout_autoHeight = true); - }); - // need to change points for resize, or else rotation/control points will fail. - this._inkDragDocs - .map(oldbds => ({ oldbds, inkPts: Cast(oldbds.doc.data, InkField)?.inkData || [] })) - .forEach(({ oldbds: { doc, x, y, width, height }, inkPts }) => { - doc.$data = new InkField(inkPts.map( - (ipt) => ({// (new x — oldx) + newWidth * (oldxpoint /oldWidth) - X: NumCast(doc.x) - x + (NumCast(doc._width) * ipt.X) / width, - Y: NumCast(doc.y) - y + (NumCast(doc._height) * ipt.Y) / height, - }))); // prettier-ignore - Doc.SetNativeWidth(doc, undefined); - Doc.SetNativeHeight(doc, undefined); - }); - }; - @computed get selectionTitle(): string { if (DocumentView.Selected().length === 1) { const selected = DocumentView.Selected()[0]; diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts index 425f5b6bb..41f38c008 100644 --- a/src/client/views/InkStrokeProperties.ts +++ b/src/client/views/InkStrokeProperties.ts @@ -99,7 +99,7 @@ export class InkStrokeProperties { controls.splice(i, 4, ...splicepts.map(p => ({ X: p.x, Y: p.y }))); // Updating the indices of the control points whose handle tangency has been broken. - doc.brokenInkIndices = new List(Cast(doc.brokenInkIndices, listSpec('number'), []).map(control => (control > i ? control + 4 : control))); + doc.brokenInkIndices = new List(NumListCast(doc.brokenInkIndices).map(control => (control > i ? control + 4 : control))); runInAction(() => { this._currentPoint = -1; }); @@ -252,7 +252,7 @@ export class InkStrokeProperties { this.applyFunction(inkView, (view: DocumentView, ink: InkData) => { const order = controlIndex % 4; const closed = InkingStroke.IsClosed(ink); - const brokenIndices = Cast(inkView.Document.brokenInkIndices, listSpec('number'), []); + const brokenIndices = NumListCast(inkView.Document.brokenInkIndices); if (origInk && this._currentPoint > 0 && this._currentPoint < ink.length - 1 && brokenIndices.findIndex(value => value === controlIndex) === -1) { const cptBefore = ink[controlIndex]; const cpt = { X: cptBefore.X + deltaX, Y: cptBefore.Y + deltaY }; @@ -405,9 +405,9 @@ export class InkStrokeProperties { this.applyFunction(inkView, (view: DocumentView, ink: InkData) => { const doc = view.Document; const brokenIndices = Cast(doc.brokenInkIndices, listSpec('number'), []); - const ind = brokenIndices.findIndex(value => value === controlIndex); + const ind = brokenIndices?.findIndex(value => value === controlIndex) ?? -1; if (ind !== -1) { - brokenIndices.splice(ind, 1); + brokenIndices!.splice(ind, 1); const [controlPoint, handleA, handleB] = [ink[controlIndex], ink[handleIndexA], ink[handleIndexB]]; const oppositeHandleA = this.rotatePoint(handleA, controlPoint, Math.PI); const angleDifference = InkStrokeProperties.angleChange(handleB, oppositeHandleA, controlPoint); diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx index f555808ef..0136f6abe 100644 --- a/src/client/views/InkingStroke.tsx +++ b/src/client/views/InkingStroke.tsx @@ -26,8 +26,8 @@ import { observer } from 'mobx-react'; import * as React from 'react'; import { DashColor, returnFalse, setupMoveUpEvents } from '../../ClientUtils'; import { Doc } from '../../fields/Doc'; -import { InkData, InkField } from '../../fields/InkField'; -import { BoolCast, Cast, NumCast, RTFCast, StrCast } from '../../fields/Types'; +import { InkData } from '../../fields/InkField'; +import { BoolCast, InkCast, NumCast, RTFCast, StrCast } from '../../fields/Types'; import { TraceMobx } from '../../fields/util'; import { Gestures } from '../../pen-gestures/GestureTypes'; import { Docs } from '../documents/Documents'; @@ -35,6 +35,7 @@ import { DocumentType } from '../documents/DocumentTypes'; import { InteractionUtils } from '../util/InteractionUtils'; import { SnappingManager } from '../util/SnappingManager'; import { UndoManager } from '../util/UndoManager'; +import { CollectionFreeFormView } from './collections/collectionFreeForm'; import { ContextMenu } from './ContextMenu'; import { ViewBoxAnnotatableComponent } from './DocComponent'; import { Colors } from './global/globalEnums'; @@ -42,14 +43,13 @@ import { InkControlPtHandles, InkEndPtHandles } from './InkControlPtHandles'; import './InkStroke.scss'; import { InkStrokeProperties } from './InkStrokeProperties'; import { InkTangentHandles } from './InkTangentHandles'; +import { InkTranscription } from './InkTranscription'; +import { DocumentView } from './nodes/DocumentView'; import { FieldView, FieldViewProps } from './nodes/FieldView'; import { FormattedTextBox, FormattedTextBoxProps } from './nodes/formattedText/FormattedTextBox'; import { PinDocView, PinProps } from './PinFuncs'; import { StyleProp } from './StyleProp'; import { ViewBoxInterface } from './ViewBoxInterface'; -import { InkTranscription } from './InkTranscription'; -import { CollectionFreeFormView } from './collections/collectionFreeForm'; -import { DocumentView } from './nodes/DocumentView'; // eslint-disable-next-line @typescript-eslint/no-require-imports const { INK_MASK_SIZE } = require('./global/globalCssVariables.module.scss'); // prettier-ignore @@ -245,7 +245,7 @@ export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>() * factor for converting between ink and screen space. */ inkScaledData = () => { - const inkData = Cast(this.dataDoc[this.fieldKey], InkField, Cast(this.layoutDoc[this.fieldKey], InkField, null))?.inkData ?? []; + const inkData = InkCast(this.dataDoc[this.fieldKey], InkCast(this.layoutDoc[this.fieldKey]) ?? null)?.inkData ?? []; const inkStrokeWidth = NumCast(this.layoutDoc.stroke_width, 1); const inkTop = Math.min(...inkData.map(p => p.Y)) - inkStrokeWidth / 2; const inkBottom = Math.max(...inkData.map(p => p.Y)) + inkStrokeWidth / 2; diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index b0bed927e..ecbef3497 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -66,6 +66,7 @@ import { FaceRecognitionHandler } from './search/FaceRecognitionHandler'; import { SearchBox } from './search/SearchBox'; import { StickerPalette } from './smartdraw/StickerPalette'; import { TaskManagerTask } from './nodes/TaskManagerTask'; +import { ScrapbookBox } from './nodes/scrapbook/ScrapbookBox'; dotenv.config(); @@ -136,6 +137,7 @@ FieldLoader.ServerLoadStatus = { requested: 0, retrieved: 0, message: 'cache' }; VideoBox, AudioBox, RecordingBox, + ScrapbookBox, PresBox, PresSlideBox, SearchBox, diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx index 66936c64c..28566ba1d 100644 --- a/src/client/views/PropertiesButtons.tsx +++ b/src/client/views/PropertiesButtons.tsx @@ -385,18 +385,19 @@ export class PropertiesButtons extends React.Component { const followLoc = this.selectedDoc._followLinkLocation; const linkedToLightboxView = () => Doc.Links(this.selectedDoc).some(link => Doc.getOppositeAnchor(link, this.selectedDoc)?._isLightbox); - let active = false; - // prettier-ignore - switch (value[0]) { - case 'linkInPlace': active = linkButton && followLoc === OpenWhere.lightbox && !linkedToLightboxView(); break; - case 'linkOnRight': active = linkButton && followLoc === OpenWhere.addRight; break; - case 'enterPortal': active = linkButton && this.selectedDoc._followLinkLocation === OpenWhere.lightbox && linkedToLightboxView(); break; - case 'toggleDetail':active = ScriptCast(this.selectedDoc.onClick)?.script.originalScript.includes('toggleDetail'); break; - case 'nothing': active = !linkButton && this.selectedDoc.onClick === undefined;break; - default: - } + const active = () => { + // prettier-ignore + switch (value[0]) { + case 'linkInPlace': return linkButton && followLoc === OpenWhere.lightbox && !linkedToLightboxView(); break; + case 'linkOnRight': return linkButton && followLoc === OpenWhere.addRight; break; + case 'enterPortal': return linkButton && this.selectedDoc._followLinkLocation === OpenWhere.lightbox && linkedToLightboxView(); break; + case 'toggleDetail':return ScriptCast(this.selectedDoc.onClick)?.script.originalScript.includes('toggleDetail'); break; + case 'nothing': return !linkButton && this.selectedDoc.onClick === undefined;break; + default: return false; + } + }; return ( - <div className="list-item" key={`${value}`} style={{ backgroundColor: active ? Colors.LIGHT_BLUE : undefined }} onClick={click}> + <div className="list-item" key={`${value}`} style={{ backgroundColor: active() ? Colors.LIGHT_BLUE : undefined }} onClick={click}> {value[1]} </div> ); diff --git a/src/client/views/PropertiesDocBacklinksSelector.tsx b/src/client/views/PropertiesDocBacklinksSelector.tsx index e30d14eae..5a00e6a89 100644 --- a/src/client/views/PropertiesDocBacklinksSelector.tsx +++ b/src/client/views/PropertiesDocBacklinksSelector.tsx @@ -1,18 +1,16 @@ -/* eslint-disable react/require-default-props */ -/* eslint-disable react/no-unused-prop-types */ import { action } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc } from '../../fields/Doc'; -import { Cast } from '../../fields/Types'; +import { DocCast } from '../../fields/Types'; import { DocumentType } from '../documents/DocumentTypes'; import { LinkManager } from '../util/LinkManager'; import { SettingsManager } from '../util/SettingsManager'; import './PropertiesDocBacklinksSelector.scss'; import { CollectionDockingView } from './collections/CollectionDockingView'; import { LinkMenu } from './linking/LinkMenu'; -import { OpenWhere } from './nodes/OpenWhere'; import { DocumentView } from './nodes/DocumentView'; +import { OpenWhere } from './nodes/OpenWhere'; type PropertiesDocBacklinksSelectorProps = { Document: Doc; @@ -26,7 +24,7 @@ export class PropertiesDocBacklinksSelector extends React.Component<PropertiesDo getOnClick = action((link: Doc) => { const linkAnchor = this.props.Document; const other = Doc.getOppositeAnchor(link, linkAnchor); - const otherdoc = !other ? undefined : other.annotationOn && other.type !== DocumentType.RTF ? Cast(other.annotationOn, Doc, null) : other; + const otherdoc = !other ? undefined : other.annotationOn && other.type !== DocumentType.RTF ? DocCast(other.annotationOn) : other; LinkManager.Instance.currentLink = link; if (otherdoc) { otherdoc.hidden = false; diff --git a/src/client/views/PropertiesDocContextSelector.tsx b/src/client/views/PropertiesDocContextSelector.tsx index ee6486a9c..1aa956e16 100644 --- a/src/client/views/PropertiesDocContextSelector.tsx +++ b/src/client/views/PropertiesDocContextSelector.tsx @@ -3,7 +3,7 @@ import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc } from '../../fields/Doc'; import { Id } from '../../fields/FieldSymbols'; -import { Cast, StrCast } from '../../fields/Types'; +import { DocCast, StrCast } from '../../fields/Types'; import { ObservableReactComponent } from './ObservableReactComponent'; import './PropertiesDocContextSelector.scss'; import { CollectionDockingView } from './collections/CollectionDockingView'; @@ -29,7 +29,7 @@ export class PropertiesDocContextSelector extends ObservableReactComponent<Prope const target = this._props.DocView.Document; const targetContext = this._props.DocView.containerViewPath?.().lastElement()?.Document; const embeddings = Doc.GetEmbeddings(target); - const containerProtos = embeddings.filter(embedding => embedding.embedContainer && embedding.embedContainer instanceof Doc).reduce((set, embedding) => set.add(Cast(embedding.embedContainer, Doc, null)), new Set<Doc>()); + const containerProtos = embeddings.filter(embedding => DocCast(embedding.embedContainer)).reduce((set, embedding) => set.add(DocCast(embedding.embedContainer)!), new Set<Doc>()); const containerSets = Array.from(containerProtos.keys()).map(container => (Doc.GetEmbeddings(container).length ? Doc.GetEmbeddings(container) : [container])); const containers = containerSets.reduce((p, set) => { set.map(s => p.add(s)); diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx index e7186c0e3..acf6f928a 100644 --- a/src/client/views/PropertiesView.tsx +++ b/src/client/views/PropertiesView.tsx @@ -161,7 +161,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps CollectionViewType.Card, CollectionViewType.Carousel, CollectionViewType.Grid, - ].includes(this.selectedDoc?.type_collection as CollectionViewType); + ].includes(this.selectedDoc?.type_collection as CollectionViewType) || this.selectedDoc.$type === DocumentType.SCRAPBOOK; // prettier-ignore } rtfWidth = () => (!this.selectedLayoutDoc ? 0 : Math.min(NumCast(this.selectedLayoutDoc?._width), this._props.width - 20)); diff --git a/src/client/views/UndoStack.tsx b/src/client/views/UndoStack.tsx index 32b97b31a..067020a62 100644 --- a/src/client/views/UndoStack.tsx +++ b/src/client/views/UndoStack.tsx @@ -7,9 +7,8 @@ import { SettingsManager } from '../util/SettingsManager'; import { UndoManager } from '../util/UndoManager'; import './UndoStack.scss'; -interface UndoStackProps {} @observer -export class UndoStack extends React.Component<UndoStackProps> { +export class UndoStack extends React.Component<object> { render() { const background = UndoManager.batchCounter.get() ? 'yellow' : SettingsManager.userVariantColor; const color = UndoManager.batchCounter.get() ? 'black' : SettingsManager.userColor; @@ -34,7 +33,6 @@ export class UndoStack extends React.Component<UndoStackProps> { {Array.from(UndoManager.undoStackNames).map((name, i) => ( <div className="undoStack-resultContainer" - // eslint-disable-next-line react/no-array-index-key key={i} onClick={() => { const size = UndoManager.undoStackNames.length; @@ -48,7 +46,6 @@ export class UndoStack extends React.Component<UndoStackProps> { .map((name, i) => ( <div className="undoStack-resultContainer" - // eslint-disable-next-line react/no-array-index-key key={i} onClick={() => { for (let n = 0; n <= i; n++) UndoManager.Redo(); diff --git a/src/client/views/animationtimeline/Region.tsx b/src/client/views/animationtimeline/Region.tsx index 99163f6c6..40fcb2bf6 100644 --- a/src/client/views/animationtimeline/Region.tsx +++ b/src/client/views/animationtimeline/Region.tsx @@ -12,6 +12,18 @@ import './Region.scss'; import './Timeline.scss'; import { TimelineMenu } from './TimelineMenu'; +export const RegionDataSchema = createSchema({ + position: defaultSpec('number', 0), + duration: defaultSpec('number', 0), + keyframes: listSpec(Doc), + fadeIn: defaultSpec('number', 0), + fadeOut: defaultSpec('number', 0), + functions: listSpec(Doc), + hasData: defaultSpec('boolean', false), +}); +export type RegionData = makeInterface<[typeof RegionDataSchema]>; +export const RegionData = makeInterface(RegionDataSchema); + /** * Useful static functions that you can use. Mostly for logic, but you can also add UI logic here also */ @@ -108,18 +120,6 @@ export namespace RegionHelpers { }; } -export const RegionDataSchema = createSchema({ - position: defaultSpec('number', 0), - duration: defaultSpec('number', 0), - keyframes: listSpec(Doc), - fadeIn: defaultSpec('number', 0), - fadeOut: defaultSpec('number', 0), - functions: listSpec(Doc), - hasData: defaultSpec('boolean', false), -}); -export type RegionData = makeInterface<[typeof RegionDataSchema]>; -export const RegionData = makeInterface(RegionDataSchema); - interface IProps { animatedDoc: Doc; RegionData: Doc; @@ -184,7 +184,7 @@ export class Region extends ObservableReactComponent<IProps> { return RegionHelpers.convertPixelTime(this.regiondata.fadeOut, 'mili', 'pixel', this._props.tickSpacing, this._props.tickIncrement); } - constructor(props: any) { + constructor(props: IProps) { super(props); makeObservable(this); } @@ -220,9 +220,7 @@ export class Region extends ObservableReactComponent<IProps> { }, 200); this._doubleClickEnabled = true; document.addEventListener('pointermove', this.onBarPointerMove); - document.addEventListener('pointerup', (e: PointerEvent) => { - document.removeEventListener('pointermove', this.onBarPointerMove); - }); + document.addEventListener('pointerup', () => document.removeEventListener('pointermove', this.onBarPointerMove)); } }; @@ -426,6 +424,7 @@ export class Region extends ObservableReactComponent<IProps> { .map(region => ({ pos: NumCast(region.position), dur: NumCast(region.duration) })) .forEach(({ pos, dur }) => { if (pos !== this.regiondata.position) { + // eslint-disable-next-line no-param-reassign val += this.regiondata.position; if (val < 0 || (val > pos && val < pos + dur)) { cannotMove = true; @@ -483,7 +482,6 @@ export class Region extends ObservableReactComponent<IProps> { * this probably needs biggest change, since everyone expected all keyframes to have a circle (and draggable) */ drawKeyframes = () => { - const keyframeDivs: JSX.Element[] = []; return DocListCast(this.regiondata.keyframes).map(kf => { return ( <> diff --git a/src/client/views/animationtimeline/Track.tsx b/src/client/views/animationtimeline/Track.tsx index 1e4ed74be..dea7b6aae 100644 --- a/src/client/views/animationtimeline/Track.tsx +++ b/src/client/views/animationtimeline/Track.tsx @@ -1,4 +1,4 @@ -import { action, computed, intercept, makeObservable, observable, reaction, runInAction } from 'mobx'; +import { action, computed, intercept, IReactionDisposer, makeObservable, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc, DocListCast, DocListCastAsync, Opt } from '../../../fields/Doc'; @@ -6,7 +6,7 @@ import { Copy } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; import { ObjectField } from '../../../fields/ObjectField'; import { listSpec } from '../../../fields/Schema'; -import { Cast, NumCast } from '../../../fields/Types'; +import { Cast, DocCast, NumCast } from '../../../fields/Types'; import { Transform } from '../../util/Transform'; import { ObservableReactComponent } from '../ObservableReactComponent'; import { Region, RegionData, RegionHelpers } from './Region'; @@ -29,16 +29,16 @@ interface IProps { @observer export class Track extends ObservableReactComponent<IProps> { @observable private _inner = React.createRef<HTMLDivElement>(); - @observable private _currentBarXReaction: any = undefined; - @observable private _timelineVisibleReaction: any = undefined; - @observable private _autoKfReaction: any = undefined; + @observable private _currentBarXReaction: IReactionDisposer | undefined = undefined; + @observable private _timelineVisibleReaction: IReactionDisposer | undefined = undefined; + @observable private _autoKfReaction: IReactionDisposer | undefined = undefined; @observable private _newKeyframe: boolean = false; private readonly MAX_TITLE_HEIGHT = 75; @observable private _trackHeight = 0; private primitiveWhitelist = ['x', 'y', '_freeform_panX', '_freeform_panY', '_width', '_height', '_rotation', 'opacity', '_layout_scrollTop']; private objectWhitelist = ['data']; - constructor(props: any) { + constructor(props: IProps) { super(props); makeObservable(this); } @@ -101,11 +101,11 @@ export class Track extends ObservableReactComponent<IProps> { } const keyframes = Cast(this.saveStateRegion.keyframes, listSpec(Doc)) as List<Doc>; const kfIndex = keyframes.indexOf(this.saveStateKf); - const kf = keyframes[kfIndex] as Doc; //index in the keyframe + const kf = DocCast(keyframes[kfIndex]); //index in the keyframe if (this._newKeyframe) { - DocListCast(this.saveStateRegion.keyframes).forEach((kf, index) => { - this.copyDocDataToKeyFrame(kf); - kf.opacity = index === 0 || index === 3 ? 0.1 : 1; + DocListCast(this.saveStateRegion.keyframes).forEach((keyF, index) => { + this.copyDocDataToKeyFrame(keyF); + keyF.opacity = index === 0 || index === 3 ? 0.1 : 1; }); this._newKeyframe = false; } @@ -144,7 +144,7 @@ export class Track extends ObservableReactComponent<IProps> { () => { return [...this.primitiveWhitelist.map(key => this._props.animatedDoc[key]), ...objects]; }, - (changed, reaction) => { + (/* changed, reaction */) => { //check for region const region = this.findRegion(this.time); if (region !== undefined) { @@ -374,7 +374,7 @@ export class Track extends ObservableReactComponent<IProps> { @action copyDocDataToKeyFrame = (doc: Doc) => { - var somethingChanged = false; + let somethingChanged = false; this.primitiveWhitelist.map(key => { const originalVal = this._props.animatedDoc[key]; somethingChanged = somethingChanged || originalVal !== doc[key]; diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index ea6259a32..13c3eb72f 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -443,7 +443,7 @@ export class CollectionDockingView extends CollectionSubView() { window.addEventListener('mouseup', this.onPointerUp); if (!htmlTarget.closest('*.lm_content') && (htmlTarget.closest('*.lm_tab') || htmlTarget.closest('*.lm_stack'))) { const className = typeof htmlTarget.className === 'string' ? htmlTarget.className : ''; - if (className.includes('lm_maximise')) { + if (className.includes('lm_maximise') || className.includes('lm_close_tab')) { // this._flush = UndoManager.StartBatch('tab maximize'); } else { const tabTarget = (e.target as HTMLElement)?.parentElement?.className.includes('lm_tab') ? (e.target as HTMLElement).parentElement : (e.target as HTMLElement); diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx index 1576a8e4a..7f835938b 100644 --- a/src/client/views/collections/CollectionMenu.tsx +++ b/src/client/views/collections/CollectionMenu.tsx @@ -46,7 +46,7 @@ export class CollectionMenu extends AntimodeMenu<CollectionMenuProps> { makeObservable(this); CollectionMenu.Instance = this; this._canFade = false; // don't let the inking menu fade away - this.Pinned = Cast(Doc.UserDoc().menuCollections_pinned, 'boolean', true); + this.Pinned = BoolCast(Doc.UserDoc().menuCollections_pinned, true); this.jumpTo(300, 300); } @@ -269,7 +269,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu initialize: (button: Doc) => { const activeDash = Doc.ActiveDashboard; if (activeDash) { - button.target_childFilters = (Doc.MySearcher._childFilters || activeDash._childFilters) instanceof ObjectField ? ObjectField.MakeCopy((Doc.MySearcher._childFilters || activeDash._childFilters) as ObjectField) : undefined; + button.target_childFilters = (Doc.MySearcher?._childFilters || activeDash._childFilters) instanceof ObjectField ? ObjectField.MakeCopy((Doc.MySearcher?._childFilters || activeDash._childFilters) as ObjectField) : undefined; button.target_searchFilterDocs = activeDash._searchFilterDocs instanceof ObjectField ? ObjectField.MakeCopy(activeDash._searchFilterDocs) : undefined; } }, diff --git a/src/client/views/collections/CollectionNoteTakingViewColumn.tsx b/src/client/views/collections/CollectionNoteTakingViewColumn.tsx index f283b0abe..461689a70 100644 --- a/src/client/views/collections/CollectionNoteTakingViewColumn.tsx +++ b/src/client/views/collections/CollectionNoteTakingViewColumn.tsx @@ -155,7 +155,7 @@ export class CollectionNoteTakingViewColumn extends ObservableReactComponent<CSV // all docs are added to the column directly to the left. @undoBatch deleteColumn = () => { - const colHdrData = Array.from(Cast(this._props.Doc[this._props.fieldKey + '_columnHeaders'], listSpec(SchemaHeaderField), null)); + const colHdrData = Array.from(Cast(this._props.Doc[this._props.fieldKey + '_columnHeaders'], listSpec(SchemaHeaderField), [])!); if (this._props.headingObject) { // this._props.docList.forEach(d => (d['$'+this._props.pivotField] = undefined)); colHdrData.splice(colHdrData.indexOf(this._props.headingObject), 1); diff --git a/src/client/views/collections/CollectionPileView.tsx b/src/client/views/collections/CollectionPileView.tsx index eea128803..5a85c8ee3 100644 --- a/src/client/views/collections/CollectionPileView.tsx +++ b/src/client/views/collections/CollectionPileView.tsx @@ -67,7 +67,6 @@ export class CollectionPileView extends CollectionSubView() { return ( <div className="collectionPileView-innards" style={{ pointerEvents: this.contentEvents }}> <CollectionFreeFormView - // eslint-disable-next-line react/jsx-props-no-spreading {...this._props} // layoutEngine={this.layoutEngine} addDocument={this.addPileDoc} diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx index fd4bdf364..4e7e19548 100644 --- a/src/client/views/collections/CollectionStackedTimeline.tsx +++ b/src/client/views/collections/CollectionStackedTimeline.tsx @@ -435,7 +435,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack anchor['$' + endTag] = anchorEndTime; if (addAsAnnotation) { if (Cast(dataDoc[fieldKey], listSpec(Doc), null)) { - Cast(dataDoc[fieldKey], listSpec(Doc), []).push(anchor); + Cast(dataDoc[fieldKey], listSpec(Doc), [])!.push(anchor); } else { dataDoc[fieldKey] = new List<Doc>([anchor]); } diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index 9155227dd..f11e646cc 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -343,7 +343,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection childFitWidth = (doc: Doc) => Cast(this.Document.childLayoutFitWidth, 'boolean', this._props.childLayoutFitWidth?.(doc) ?? Cast(doc.layout_fitWidth, 'boolean', null) ?? null); // this is what renders the document that you see on the screen // called in Children: this actually adds a document to our children list - getDisplayDoc(doc: Doc, trans: () => string, count: number) { + getDisplayDoc = (doc: Doc, trans: () => string, count: number) => { const dataDoc = doc.isTemplateDoc || doc.isTemplateForField ? this._props.TemplateDataDocument : undefined; this._docXfs.push({ stackedDocTransform: this.getDocTransform(doc), width: this.getDocWidth(doc), height: this.getDocHeight(doc) }); return count > this._renderCount ? null : ( @@ -384,6 +384,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection searchFilterDocs={this.searchFilterDocs} xPadding={NumCast(this.layoutDoc._childXPadding, this._props.childXPadding)} yPadding={NumCast(this.layoutDoc._childYPadding, this._props.childYPadding)} + rejectDrop={this._props.childRejectDrop} addDocument={this._props.addDocument} moveDocument={this._props.moveDocument} removeDocument={this._props.removeDocument} @@ -393,7 +394,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection pinToPres={this._props.pinToPres} /> ); - } + }; getDocTransform = computedFn((doc: Doc) => () => { // these must be referenced for document decorations to update when the text box container is scrolled diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index bc7d6f897..e79d0a76d 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -58,6 +58,7 @@ export interface CollectionViewProps extends React.PropsWithChildren<FieldViewPr childOpacity?: () => number; childContextMenuItems?: () => { script: ScriptField; label: string }[]; childLayoutTemplate?: () => Doc | undefined; // specify a layout Doc template to use for children of the collection + childRejectDrop?: (de: DragManager.DropEvent, subView?: DocumentView) => boolean; // whether a child document can be dropped on this document childHideDecorationTitle?: boolean; childHideResizeHandles?: boolean; childHideDecorations?: boolean; @@ -323,7 +324,7 @@ export function CollectionSubView<X>() { protected onInternalDrop(e: Event, de: DragManager.DropEvent): boolean { const { docDragData } = de.complete; - if (docDragData && !docDragData.draggedDocuments.includes(this.Document)) { + if (docDragData && !docDragData.draggedDocuments.includes(this.Document) && !this._props.rejectDrop?.(de, this.DocumentView?.())) { let added; const dropAction = docDragData.dropAction || docDragData.userDropAction; const targetDocments = DocListCast(this.dataDoc[this._props.fieldKey]); diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 3497a62cb..bee5d016d 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -137,7 +137,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree if (res && de.complete.docDragData) { if (this.Document !== Doc.MyRecentlyClosed) de.complete.docDragData.droppedDocuments.forEach(doc => { - if (this.Document !== Doc.MyRecentlyClosed) Doc.RemoveDocFromList(Doc.MyRecentlyClosed, undefined, doc); + if (this.Document !== Doc.MyRecentlyClosed) Doc.MyRecentlyClosed && Doc.RemoveDocFromList(Doc.MyRecentlyClosed, undefined, doc); }); } return res; @@ -193,7 +193,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree res && doclist.forEach(doc => { Doc.SetContainer(doc, this.Document); - if (this.Document !== Doc.MyRecentlyClosed) Doc.RemoveDocFromList(Doc.MyRecentlyClosed, undefined, doc); + if (this.Document !== Doc.MyRecentlyClosed) Doc.MyRecentlyClosed && Doc.RemoveDocFromList(Doc.MyRecentlyClosed, undefined, doc); }); return res; }; @@ -273,8 +273,8 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree ); } childContextMenuItems = () => { - const customScripts = Cast(this.Document.childContextMenuScripts, listSpec(ScriptField), []); - const customFilters = Cast(this.Document.childContextMenuFilters, listSpec(ScriptField), []); + const customScripts = Cast(this.Document.childContextMenuScripts, listSpec(ScriptField), [])!; + const customFilters = Cast(this.Document.childContextMenuFilters, listSpec(ScriptField), [])!; const icons = StrListCast(this.Document.childContextMenuIcons); return StrListCast(this.Document.childContextMenuLabels).map((label, i) => ({ script: customScripts[i], filter: customFilters[i], icon: icons[i], label })); }; diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index c9af92a1b..eb9caf29d 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -183,15 +183,17 @@ export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewPr }, }) ); - DocListCast(Cast(Doc.UserDoc()['clickFuncs-child'], Doc, null)?.data).forEach(childClick => - onClicks.push({ - description: `Set child ${childClick.title}`, - icon: 'edit', - event: () => { - this.dataDoc[StrCast(childClick.targetScriptKey)] = ObjectField.MakeCopy(ScriptCast(childClick.data)); - }, - }) - ); + DocListCast(Cast(Doc.UserDoc()['clickFuncs-child'], Doc, null)?.data) + .filter(childClick => ScriptCast(childClick.data)) + .forEach(childClick => + onClicks.push({ + description: `Set child ${childClick.title}`, + icon: 'edit', + event: () => { + this.dataDoc[StrCast(childClick.targetScriptKey)] = ObjectField.MakeCopy(ScriptCast(childClick.data)!); + }, + }) + ); !Doc.IsSystem(this.Document) && !existingOnClick && cm.addItem({ description: 'OnClick...', noexpand: true, subitems: onClicks, icon: 'mouse-pointer' }); } diff --git a/src/client/views/collections/FlashcardPracticeUI.tsx b/src/client/views/collections/FlashcardPracticeUI.tsx index f24a8acb7..2f46c00bd 100644 --- a/src/client/views/collections/FlashcardPracticeUI.tsx +++ b/src/client/views/collections/FlashcardPracticeUI.tsx @@ -60,7 +60,7 @@ export class FlashcardPracticeUI extends ObservableReactComponent<PracticeUIProp get practiceField() { return this._props.fieldKey + "_practice"; } // prettier-ignore - @computed get filterDoc() { return DocListCast(Doc.MyContextMenuBtns.data).find(doc => doc.title === 'Filter'); } // prettier-ignore + @computed get filterDoc() { return DocListCast(Doc.MyContextMenuBtns?.data).find(doc => doc.title === 'Filter'); } // prettier-ignore @computed get practiceMode() { return this._props.allChildDocs().some(doc => doc._layout_flashcardType) ? StrCast(this._props.layoutDoc.practiceMode) : ''; } // prettier-ignore btnHeight = () => NumCast(this.filterDoc?.height); diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx index 4beb75074..fb2d0955f 100644 --- a/src/client/views/collections/TreeView.tsx +++ b/src/client/views/collections/TreeView.tsx @@ -70,7 +70,7 @@ export interface TreeViewProps { treeViewHideHeaderFields: () => boolean; renderedIds: string[]; // list of document ids rendered used to avoid unending expansion of items in a cycle onCheckedClick?: () => ScriptField; - onChildClick?: () => ScriptField; + onChildClick?: () => ScriptField | undefined; skipFields?: string[]; firstLevel: boolean; // TODO: [AL] add these @@ -1268,7 +1268,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> { treeViewHideHeaderFields: () => boolean, renderedIds: string[], onCheckedClick: undefined | (() => ScriptField), - onChildClick: undefined | (() => ScriptField), + onChildClick: undefined | (() => ScriptField | undefined), skipFields: string[] | undefined, firstLevel: boolean, whenChildContentsActiveChanged: (isActive: boolean) => void, diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx index 158bac7ba..4d17dedfb 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx @@ -108,7 +108,7 @@ export function computePassLayout(poolData: Map<string, PoolData>, pivotDoc: Doc } function toNumber(val: FieldResult<FieldType>) { - return val === undefined ? undefined : DateCast(val) ? DateCast(val).date.getTime() : NumCast(val, Number(StrCast(val))); + return val === undefined ? undefined : DateCast(val) ? DateCast(val)!.date.getTime() : NumCast(val, Number(StrCast(val))); } export function computeStarburstLayout(poolData: Map<string, PoolData>, pivotDoc: Doc, childPairs: { layout: Doc; data?: Doc }[], panelDim: number[], viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[] /* , engineProps: any */) { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 842293358..c4971c204 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -256,8 +256,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection override contentBounds = () => { const { x, y, r, b } = aggregateBounds( this._layoutElements.filter(e => e.bounds?.width && !e.bounds.z).map(e => e.bounds!), - NumCast(this.layoutDoc._xPadding, this._props.xPadding ?? 0), - NumCast(this.layoutDoc._yPadding, this._props.yPadding ?? 0) + NumCast(this.layoutDoc._xPadding, NumCast(this.layoutDoc._xMargin, this._props.xPadding ?? 0)), + NumCast(this.layoutDoc._yPadding, NumCast(this.layoutDoc._yMargin, this._props.yPadding ?? 0)) ); const [width, height] = [r - x, b - y]; return { @@ -496,6 +496,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection }, 'link drop'); onInternalDrop = (e: Event, de: DragManager.DropEvent): boolean => { + if (this._props.rejectDrop?.(de, this._props.DocumentView?.())) return false; if (de.complete.annoDragData?.dragDocument && super.onInternalDrop(e, de)) return this.internalAnchorAnnoDrop(e, de, de.complete.annoDragData); if (de.complete.linkDragData) return this.internalLinkDrop(e, de, de.complete.linkDragData); if (de.complete.docDragData?.droppedDocuments.length) return this.internalDocDrop(e, de, de.complete.docDragData); @@ -1562,6 +1563,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection pinToPres={this._props.pinToPres} whenChildContentsActiveChanged={this._props.whenChildContentsActiveChanged} dragAction={(this.Document.childDragAction ?? this._props.childDragAction) as dropActionType} + rejectDrop={this._props.childRejectDrop} showTitle={this._props.childlayout_showTitle} dontRegisterView={this._props.dontRegisterView} pointerEvents={this.childPointerEventsFunc} diff --git a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx index 72485aa86..624c85beb 100644 --- a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx +++ b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx @@ -24,6 +24,7 @@ import { FaceRecognitionHandler } from '../../search/FaceRecognitionHandler'; import { CollectionStackingView } from '../CollectionStackingView'; import './FaceCollectionBox.scss'; import { MarqueeOptionsMenu } from './MarqueeOptionsMenu'; +import { returnEmptyDocViewList } from '../../StyleProvider'; /** * This code is used to render the sidebar collection of unique recognized faces, where each @@ -106,7 +107,7 @@ export class UniqueFaceBox extends ViewBoxBaseComponent<FieldViewProps>() { // assign the face in the image that's closest to the face collection's face if (faceAnno) { - faceAnno.face && FaceRecognitionHandler.UniqueFaceRemoveFaceImage(faceAnno, DocCast(faceAnno.face)); + DocCast(faceAnno.face) && FaceRecognitionHandler.UniqueFaceRemoveFaceImage(faceAnno, DocCast(faceAnno.face)!); FaceRecognitionHandler.UniqueFaceAddFaceImage(faceAnno, this.Document); faceAnno.$face = this.Document[DocData]; } @@ -116,7 +117,7 @@ export class UniqueFaceBox extends ViewBoxBaseComponent<FieldViewProps>() { ?.filter(doc => DocCast(doc.face)?.type === DocumentType.UFACE) .forEach(faceAnno => { const imgDoc = faceAnno; - faceAnno.face && FaceRecognitionHandler.UniqueFaceRemoveFaceImage(imgDoc, DocCast(faceAnno.face)); + DocCast(faceAnno.face) && FaceRecognitionHandler.UniqueFaceRemoveFaceImage(imgDoc, DocCast(faceAnno.face)!); FaceRecognitionHandler.UniqueFaceAddFaceImage(faceAnno, this.Document); faceAnno.$face = this.Document[DocData]; }); @@ -187,7 +188,7 @@ export class UniqueFaceBox extends ViewBoxBaseComponent<FieldViewProps>() { ele?.addEventListener('wheel', this.onPassiveWheel, { passive: false }); })}> {FaceRecognitionHandler.UniqueFaceImages(this.Document).map((doc, i) => { - const [name, type] = ImageCastToNameType(doc[Doc.LayoutDataKey(doc)]) ?? ['-missing-', '.png']; + const [name, type] = ImageCastToNameType(doc?.[Doc.LayoutDataKey(doc)]) ?? ['-missing-', '.png']; return ( <div className="image-wrapper" @@ -197,7 +198,7 @@ export class UniqueFaceBox extends ViewBoxBaseComponent<FieldViewProps>() { this, e, () => { - const dragDoc = DocListCast(doc.data_annotations).find(a => a.face === this.Document[DocData]) ?? this.Document; + const dragDoc = DocListCast(doc?.data_annotations).find(a => a.face === this.Document[DocData]) ?? this.Document; DragManager.StartDocumentDrag([e.target as HTMLElement], new DragManager.DocumentDragData([dragDoc], dropActionType.embed), e.clientX, e.clientY); return true; }, @@ -205,7 +206,7 @@ export class UniqueFaceBox extends ViewBoxBaseComponent<FieldViewProps>() { emptyFunction ) }> - <img onClick={() => DocumentView.showDocument(doc, { willZoomCentered: true })} style={{ maxWidth: '60px', margin: '10px' }} src={`${name}_o.${type}`} /> + <img onClick={() => doc && DocumentView.showDocument(doc, { willZoomCentered: true })} style={{ maxWidth: '60px', margin: '10px' }} src={`${name}_o.${type}`} /> <div className="remove-item"> <IconButton tooltip={'Remove Doc From Face Collection'} onPointerDown={() => this.removeFaceImageFromUniqueFace(doc)} icon={'x'} style={{ width: '4px' }} size={Size.XSMALL} /> </div> @@ -239,11 +240,12 @@ export class FaceCollectionBox extends ViewBoxBaseComponent<FieldViewProps>() { } moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean): boolean => !!(this._props.removeDocument?.(doc) && addDocument?.(doc)); + // eslint-disable-next-line @typescript-eslint/no-unused-vars addDocument = (doc: Doc | Doc[], annotationKey?: string) => { const uniqueFaceDoc = doc instanceof Doc ? doc : doc[0]; const added = uniqueFaceDoc.type === DocumentType.UFACE; if (added) { - Doc.SetContainer(uniqueFaceDoc, Doc.MyFaceCollection); + Doc.MyFaceCollection && Doc.SetContainer(uniqueFaceDoc, Doc.MyFaceCollection); Doc.ActiveDashboard && Doc.AddDocToList(Doc.ActiveDashboard[DocData], 'myUniqueFaces', uniqueFaceDoc); } return added; @@ -267,6 +269,8 @@ export class FaceCollectionBox extends ViewBoxBaseComponent<FieldViewProps>() { {...this._props} // styleProvider={this.stackingStyleProvider} Document={Doc.ActiveDashboard} + DocumentView={undefined} + docViewPath={returnEmptyDocViewList} fieldKey="myUniqueFaces" moveDocument={this.moveDocument} addDocument={this.addDocument} diff --git a/src/client/views/collections/collectionGrid/CollectionGridView.tsx b/src/client/views/collections/collectionGrid/CollectionGridView.tsx index a7603a45b..1d5e70be7 100644 --- a/src/client/views/collections/collectionGrid/CollectionGridView.tsx +++ b/src/client/views/collections/collectionGrid/CollectionGridView.tsx @@ -201,6 +201,7 @@ export class CollectionGridView extends CollectionSubView() { isContentActive={this.isChildContentActive} PanelWidth={width} PanelHeight={height} + rejectDrop={this._props.childRejectDrop} ScreenToLocalTransform={dxf} setContentViewBox={emptyFunction} whenChildContentsActiveChanged={this._props.whenChildContentsActiveChanged} diff --git a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx index 8d3947653..3c2a99b1e 100644 --- a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx +++ b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx @@ -192,6 +192,7 @@ export class CollectionLinearView extends CollectionSubView() { renderDepth={this._props.renderDepth + 1} dontRegisterView={BoolCast(this.Document.childDontRegisterViews)} focus={emptyFunction} + rejectDrop={this._props.childRejectDrop} styleProvider={this._props.styleProvider} containerViewPath={this.childContainerViewPath} whenChildContentsActiveChanged={emptyFunction} diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx index adaceb053..caad1c7e0 100644 --- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx +++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx @@ -281,6 +281,7 @@ export class CollectionMulticolumnView extends CollectionSubView() { PanelWidth={this.childWidth(childLayout)} PanelHeight={this.childHeight} rootSelected={this.rootSelected} + rejectDrop={this._props.childRejectDrop} dragAction={StrCast(this.Document.childDragAction, this._props.childDragAction) as dropActionType} onClickScript={this.onChildClickHandler} onDoubleClickScript={this.onChildDoubleClickHandler} diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx index 27c41583c..593598479 100644 --- a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx +++ b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx @@ -263,6 +263,7 @@ export class CollectionMultirowView extends CollectionSubView() { PanelWidth={this.childWidth} PanelHeight={this.childHeight(childLayout)} rootSelected={this.rootSelected} + rejectDrop={this._props.childRejectDrop} dragAction={StrCast(this.Document.childDragAction, this._props.childDragAction) as dropActionType} onClickScript={this.onChildClickHandler} onDoubleClickScript={this.onChildDoubleClickHandler} diff --git a/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx b/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx index 81a2d8e64..16d33eb93 100644 --- a/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx +++ b/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx @@ -107,7 +107,7 @@ export class SchemaColumnHeader extends ObservableReactComponent<SchemaColumnHea focus: emptyFunction, addDocTab: SchemaTableCell.addFieldDoc, pinToPres: returnZero, - Document: DocCast(Document.rootDocument, Document), + Document: DocCast(Document.rootDocument, Document)!, fieldKey: fieldKey, PanelWidth: columnWidth, PanelHeight: props.rowHeight, diff --git a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx index 8e1edc1ee..8b34b4139 100644 --- a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx +++ b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx @@ -106,7 +106,7 @@ export class SchemaTableCell extends ObservableReactComponent<SchemaTableCellPro const { Doc: Document, fieldKey, /* getFinfo,*/ columnWidth, isRowActive } = props; let protoCount = 0; const layoutDoc = fieldKey.startsWith('_') ? Document[DocLayout] : Document; - let doc = Document; + let doc: Doc | undefined = Document; while (doc) { if (Object.keys(doc).includes(fieldKey.replace(/^_/, ''))) break; protoCount++; @@ -168,9 +168,11 @@ export class SchemaTableCell extends ObservableReactComponent<SchemaTableCellPro while ((matches = idPattern.exec(field)) !== null) { results.push([matches[0], matches[1].replace(/"/g, '')]); } - results.forEach(idFuncPair => { - modField = modField.replace(idFuncPair[0], 'd' + DocumentView.getDocViewIndex(IdToDoc(idFuncPair[1])).toString()); - }); + results + .filter(idFuncPair => IdToDoc(idFuncPair[1])) + .forEach(idFuncPair => { + modField = modField.replace(idFuncPair[0], 'd' + DocumentView.getDocViewIndex(IdToDoc(idFuncPair[1])!).toString()); + }); if (modField.endsWith(';')) modField = modField.substring(0, modField.length - 1); @@ -328,7 +330,7 @@ export class SchemaImageCell extends ObservableReactComponent<SchemaTableCellPro const altpaths = alts .map(doc => Cast(doc[Doc.LayoutDataKey(doc)], ImageField, null)?.url) .filter(url => url) - .map(url => this.choosePath(url)); // access the primary layout data of the alternate documents + .map(url => this.choosePath(url!)); // access the primary layout data of the alternate documents const paths = field ? [this.choosePath(field.url), ...altpaths] : altpaths; // If there is a path, follow it; otherwise, follow a link to a default image icon const url = paths.length ? paths : [ClientUtils.CorsProxy('http://www.cs.brown.edu/~bcz/noImage.png')]; @@ -379,7 +381,7 @@ export class SchemaDateCell extends ObservableReactComponent<SchemaTableCellProp } @observable _pickingDate: boolean = false; - @computed get date(): DateField { + @computed get date(): DateField | undefined { // if the cell is a date field, cast then contents to a date. Otherrwwise, make the contents undefined. return DateCast(this._props.Doc[this._props.fieldKey]); } @@ -399,7 +401,7 @@ export class SchemaDateCell extends ObservableReactComponent<SchemaTableCellProp return ( <> <div style={{ pointerEvents: 'none' }} tabIndex={1}> - <DatePicker dateFormat="Pp" selected={this.date?.date ?? Date.now()} onChange={emptyFunction} /> + <DatePicker dateFormat="Pp" selected={this.date?.date ?? new Date()} onChange={emptyFunction} /> </div> {pointerEvents === 'none' || !selectedCell(this._props) ? null : ( <Popup @@ -410,7 +412,7 @@ export class SchemaDateCell extends ObservableReactComponent<SchemaTableCellProp background={SnappingManager.userBackgroundColor} popup={ <div style={{ width: 'fit-content', height: '200px' }}> - <DatePicker open dateFormat="Pp" selected={this.date?.date ?? Date.now()} onChange={this.handleChange} /> + <DatePicker open dateFormat="Pp" selected={this.date?.date ?? new Date()} onChange={this.handleChange} /> </div> } /> diff --git a/src/client/views/linking/LinkMenuGroup.tsx b/src/client/views/linking/LinkMenuGroup.tsx index c15508669..9eaf0e491 100644 --- a/src/client/views/linking/LinkMenuGroup.tsx +++ b/src/client/views/linking/LinkMenuGroup.tsx @@ -1,10 +1,9 @@ -/* eslint-disable react/require-default-props */ import { action, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc, StrListCast } from '../../../fields/Doc'; import { Id } from '../../../fields/FieldSymbols'; -import { Cast, DocCast } from '../../../fields/Types'; +import { DocCast } from '../../../fields/Types'; import { DocumentType } from '../../documents/DocumentTypes'; import { DocumentView } from '../nodes/DocumentView'; import './LinkMenu.scss'; @@ -56,7 +55,7 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> { ? this.props.docView._props.LayoutTemplateString?.includes('link_anchor_1') ? DocCast(linkDoc.link_anchor_2) : DocCast(linkDoc.link_anchor_1) - : Doc.getOppositeAnchor(linkDoc, sourceDoc) || Doc.getOppositeAnchor(linkDoc, Cast(linkDoc.link_anchor_2, Doc, null)?.annotationOn === sourceDoc ? Cast(linkDoc.link_anchor_2, Doc, null) : Cast(linkDoc.link_anchor_1, Doc, null)); + : Doc.getOppositeAnchor(linkDoc, sourceDoc) || Doc.getOppositeAnchor(linkDoc, DocCast(linkDoc.link_anchor_2)?.annotationOn === sourceDoc ? DocCast(linkDoc.link_anchor_2) : DocCast(linkDoc.link_anchor_1)); return !destDoc || !sourceDoc ? null : ( <LinkMenuItem key={linkDoc[Id]} diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx index 1c92c3b0d..c984a7053 100644 --- a/src/client/views/linking/LinkMenuItem.tsx +++ b/src/client/views/linking/LinkMenuItem.tsx @@ -68,8 +68,8 @@ export class LinkMenuItem extends ObservableReactComponent<LinkMenuItemProps> { @computed get sourceAnchor() { const ldoc = this._props.linkDoc; if (this._props.sourceDoc !== ldoc.link_anchor_1 && this._props.sourceDoc !== ldoc.link_anchor_2) { - if (Doc.AreProtosEqual(DocCast(DocCast(ldoc.link_anchor_1).annotationOn), this._props.sourceDoc)) return DocCast(ldoc.link_anchor_1); - if (Doc.AreProtosEqual(DocCast(DocCast(ldoc.link_anchor_2).annotationOn), this._props.sourceDoc)) return DocCast(ldoc.link_anchor_2); + if (Doc.AreProtosEqual(DocCast(DocCast(ldoc.link_anchor_1)?.annotationOn), this._props.sourceDoc)) return DocCast(ldoc.link_anchor_1); + if (Doc.AreProtosEqual(DocCast(DocCast(ldoc.link_anchor_2)?.annotationOn), this._props.sourceDoc)) return DocCast(ldoc.link_anchor_2); } return this._props.sourceDoc; } diff --git a/src/client/views/newlightbox/ExploreView/ExploreView.tsx b/src/client/views/newlightbox/ExploreView/ExploreView.tsx index f8c07cc43..7f9a13549 100644 --- a/src/client/views/newlightbox/ExploreView/ExploreView.tsx +++ b/src/client/views/newlightbox/ExploreView/ExploreView.tsx @@ -1,5 +1,3 @@ -/* eslint-disable jsx-a11y/no-static-element-interactions */ -/* eslint-disable jsx-a11y/click-events-have-key-events */ import * as React from 'react'; import { StrCast } from '../../../../fields/Types'; import { NewLightboxView } from '../NewLightboxView'; @@ -19,7 +17,7 @@ export function ExploreView(props: IExploreView) { const x = (rec.embedding.x / xBound) * 50; const y = (rec.embedding.y / yBound) * 50; return ( - <div className="exploreView-doc" onClick={() => {}} style={{ top: `calc(50% + ${y}%)`, left: `calc(50% + ${x}%)` }}> + <div key={'' + x + ' ' + y} className="exploreView-doc" onClick={() => {}} style={{ top: `calc(50% + ${y}%)`, left: `calc(50% + ${x}%)` }}> {rec.title} </div> ); diff --git a/src/client/views/newlightbox/NewLightboxView.tsx b/src/client/views/newlightbox/NewLightboxView.tsx index 87cef01d2..7ec45d0dd 100644 --- a/src/client/views/newlightbox/NewLightboxView.tsx +++ b/src/client/views/newlightbox/NewLightboxView.tsx @@ -5,7 +5,7 @@ import { returnEmptyFilter, returnTrue } from '../../../ClientUtils'; import { emptyFunction } from '../../../Utils'; import { CreateLinkToActiveAudio, Doc, DocListCast, Opt, returnEmptyDoclist } from '../../../fields/Doc'; import { InkTool } from '../../../fields/InkField'; -import { Cast, NumCast, StrCast, toList } from '../../../fields/Types'; +import { Cast, DocCast, NumCast, StrCast, toList } from '../../../fields/Types'; import { SnappingManager } from '../../util/SnappingManager'; import { Transform } from '../../util/Transform'; import { GestureOverlay } from '../GestureOverlay'; @@ -100,7 +100,7 @@ export class NewLightboxView extends React.Component<LightboxViewProps> { SnappingManager.SetExploreMode(false); } else { const l = CreateLinkToActiveAudio(() => doc).lastElement(); - l && (Cast(l.link_anchor_2, Doc, null).backgroundColor = 'lightgreen'); + DocCast(l?.link_anchor_2) && (DocCast(l!.link_anchor_2)!.backgroundColor = 'lightgreen'); DocumentView.CurrentlyPlaying?.forEach(dv => dv.ComponentView?.Pause?.()); // DocumentView.PinDoc(doc, { hidePresBox: true }); this._history ? this._history.push({ doc, target }) : (this._history = [{ doc, target }]); @@ -142,7 +142,7 @@ export class NewLightboxView extends React.Component<LightboxViewProps> { const targetDocView = target && DocumentView.getLightboxDocumentView(target); if (targetDocView && target) { const l = CreateLinkToActiveAudio(() => targetDocView.ComponentView?.getAnchor?.(true) || target).lastElement(); - l && (Cast(l.link_anchor_2, Doc, null).backgroundColor = 'lightgreen'); + DocCast(l?.link_anchor_2) && (DocCast(l!.link_anchor_2)!.backgroundColor = 'lightgreen'); DocumentView.showDocument(target, { willZoomCentered: true, zoomScale: 0.9 }); if (NewLightboxView._history?.lastElement().target !== target) NewLightboxView._history?.push({ doc, target }); } else if (!target && NewLightboxView.path.length) { @@ -326,7 +326,6 @@ export class NewLightboxView extends React.Component<LightboxViewProps> { } interface NewLightboxTourBtnProps { navBtn: (left: Opt<string | number>, bottom: Opt<number>, top: number, icon: string, display: () => string, click: (e: React.MouseEvent) => void, color?: string) => JSX.Element; - // eslint-disable-next-line react/no-unused-prop-types future: () => Opt<Doc[]>; stepInto: () => void; } diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx index c80e8ebc1..9369ff98a 100644 --- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx +++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx @@ -108,7 +108,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { // all CSV records in the dataset (that aren't an empty row) @computed.struct get records() { try { - const records = DataVizBox.dataset.get(CsvCast(this.dataDoc[this.fieldKey]).url.href); + const records = DataVizBox.dataset.get(CsvCast(this.dataDoc[this.fieldKey])?.url.href ?? ''); this._urlError = false; return records?.filter(record => Object.keys(record).some(key => record[key])) ?? []; } catch { @@ -344,7 +344,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { componentDidMount() { this._props.setContentViewBox?.(this); if (!this._urlError) { - if (!DataVizBox.dataset.has(CsvCast(this.dataDoc[this.fieldKey]).url.href)) this.fetchData(); + if (!DataVizBox.dataset.has(CsvCast(this.dataDoc[this.fieldKey])?.url.href ?? '')) this.fetchData(); } this._disposers.datavis = reaction( () => { @@ -386,8 +386,8 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { this.layoutDoc._dataViz_schemaOG = loading; } const ogDoc = this.layoutDoc._dataViz_schemaOG as Doc; - const ogHref = CsvCast(ogDoc[this.fieldKey]) ? CsvCast(ogDoc[this.fieldKey]).url.href : undefined; - const { href } = CsvCast(this.Document[this.fieldKey]).url; + const ogHref = CsvCast(ogDoc[this.fieldKey]) ? CsvCast(ogDoc[this.fieldKey])!.url.href : undefined; + const { href } = CsvCast(this.Document[this.fieldKey])?.url ?? { href: '' }; if (ogHref && !DataVizBox.datasetSchemaOG.has(href)) { // sets original dataset to the var const lastRow = current.pop(); @@ -399,7 +399,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { }, current => { if (current) { - const { href } = CsvCast(this.Document[this.fieldKey]).url; + const { href } = CsvCast(this.Document[this.fieldKey])?.url ?? { href: '' }; if (this.layoutDoc.dataViz_schemaLive) DataVizBox.dataset.set(href, current); else DataVizBox.dataset.set(href, DataVizBox.datasetSchemaOG.get(href)!); } @@ -414,9 +414,9 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { fetchData = () => { if (!this.Document.dataViz_asSchema) { - DataVizBox.dataset.set(CsvCast(this.dataDoc[this.fieldKey]).url.href, []); // assign temporary dataset as a lock to prevent duplicate server requests + DataVizBox.dataset.set(CsvCast(this.dataDoc[this.fieldKey])?.url.href ?? '', []); // assign temporary dataset as a lock to prevent duplicate server requests fetch('/csvData?uri=' + (this.dataUrl?.url.href ?? '')) // - .then(res => res.json().then(action(jsonRes => !jsonRes.errno && DataVizBox.dataset.set(CsvCast(this.dataDoc[this.fieldKey]).url.href, jsonRes)))); + .then(res => res.json().then(action(jsonRes => !jsonRes.errno && DataVizBox.dataset.set(CsvCast(this.dataDoc[this.fieldKey])?.url.href ?? '', jsonRes)))); } }; @@ -523,7 +523,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { GPTPopup.Instance.createFilteredDoc = this.createFilteredDoc; GPTPopup.Instance.setDataJson(''); GPTPopup.Instance.setMode(GPTPopupMode.DATA); - const csvdata = DataVizBox.dataset.get(CsvCast(this.dataDoc[this.fieldKey]).url.href); + const csvdata = DataVizBox.dataset.get(CsvCast(this.dataDoc[this.fieldKey])?.url.href ?? ''); GPTPopup.Instance.setDataJson(JSON.stringify(csvdata)); GPTPopup.Instance.generateDataAnalysis(); }); diff --git a/src/client/views/nodes/DataVizBox/SchemaCSVPopUp.tsx b/src/client/views/nodes/DataVizBox/SchemaCSVPopUp.tsx index 8ae29a88c..1e2a95d31 100644 --- a/src/client/views/nodes/DataVizBox/SchemaCSVPopUp.tsx +++ b/src/client/views/nodes/DataVizBox/SchemaCSVPopUp.tsx @@ -11,10 +11,8 @@ import { DragManager } from '../../../util/DragManager'; import { DocumentView } from '../DocumentView'; import './SchemaCSVPopUp.scss'; -interface SchemaCSVPopUpProps {} - @observer -export class SchemaCSVPopUp extends React.Component<SchemaCSVPopUpProps> { +export class SchemaCSVPopUp extends React.Component<object> { // eslint-disable-next-line no-use-before-define static Instance: SchemaCSVPopUp; @@ -23,7 +21,7 @@ export class SchemaCSVPopUp extends React.Component<SchemaCSVPopUpProps> { @observable public target: Doc | undefined = undefined; @observable public visible: boolean = false; - constructor(props: SchemaCSVPopUpProps) { + constructor(props: object) { super(props); makeObservable(this); SchemaCSVPopUp.Instance = this; diff --git a/src/client/views/nodes/DataVizBox/components/Histogram.tsx b/src/client/views/nodes/DataVizBox/components/Histogram.tsx index 5450d03b1..a7c4a00b0 100644 --- a/src/client/views/nodes/DataVizBox/components/Histogram.tsx +++ b/src/client/views/nodes/DataVizBox/components/Histogram.tsx @@ -5,10 +5,9 @@ import { IReactionDisposer, action, computed, makeObservable, observable, reacti import { observer } from 'mobx-react'; import * as React from 'react'; import { FaFillDrip } from 'react-icons/fa'; -import { Doc, NumListCast } from '../../../../../fields/Doc'; +import { Doc, NumListCast, StrListCast } from '../../../../../fields/Doc'; import { List } from '../../../../../fields/List'; -import { listSpec } from '../../../../../fields/Schema'; -import { Cast, DocCast, StrCast } from '../../../../../fields/Types'; +import { DocCast, StrCast } from '../../../../../fields/Types'; import { Docs } from '../../../../documents/Documents'; import { undoable } from '../../../../util/UndoManager'; import { ObservableReactComponent } from '../../../ObservableReactComponent'; @@ -108,13 +107,13 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { } @computed get defaultBarColor() { - return Cast(this.layoutDoc.dataViz_histogram_defaultColor, 'string', '#69b3a2'); + return StrCast(this.layoutDoc.dataViz_histogram_defaultColor, '#69b3a2')!; } @computed get barColors() { - return Cast(this.layoutDoc.dataViz_histogram_barColors, listSpec('string'), null); + return StrListCast(this.layoutDoc.dataViz_histogram_barColors); } @computed get selectedBins() { - return Cast(this.layoutDoc.dataViz_histogram_selectedBins, listSpec('number'), null); + return NumListCast(this.layoutDoc.dataViz_histogram_selectedBins); } @computed get rangeVals(): { xMin?: number; xMax?: number; yMin?: number; yMax?: number } { @@ -129,7 +128,7 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { // restore selected bars this._histogramSvg?.selectAll('rect').attr('class', dIn => { const d = dIn as HistogramData; - if (this.selectedBins.some(selBin => d[0] === selBin)) { + if (this.selectedBins?.some(selBin => d[0] === selBin)) { this._selectedBars.push(d); return 'histogram-bar hover'; } @@ -199,10 +198,10 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { const alreadySelected = this._selectedBars.findIndex(eachData => !Object.keys(d).some(key => d[key] !== eachData[key])); if (alreadySelected !== -1) { this._selectedBars.splice(alreadySelected, 1); - this.selectedBins.splice(alreadySelected, 1); + this.selectedBins?.splice(alreadySelected, 1); } else { this._selectedBars.push(d); - this.selectedBins.push(d[0] as number); + this.selectedBins?.push(d[0] as number); } const showSelectedLabel = (dataset: HistogramData[]) => { const datum = dataset.lastElement(); diff --git a/src/client/views/nodes/DataVizBox/components/LineChart.tsx b/src/client/views/nodes/DataVizBox/components/LineChart.tsx index 6b047546c..80fadf178 100644 --- a/src/client/views/nodes/DataVizBox/components/LineChart.tsx +++ b/src/client/views/nodes/DataVizBox/components/LineChart.tsx @@ -26,7 +26,7 @@ export interface LineChartProps { layoutDoc: Doc; axes: string[]; titleCol: string; - records: { [key: string]: any }[]; + records: { [key: string]: string }[]; width: number; height: number; dataDoc: Doc; @@ -47,7 +47,7 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { @observable _currSelected: DataPoint | undefined = undefined; // TODO: nda - some sort of mapping that keeps track of the annotated points so we can easily remove when annotations list updates - constructor(props: any) { + constructor(props: LineChartProps) { super(props); makeObservable(this); } @@ -79,7 +79,7 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { @computed get incomingHighlited() { // return selected x and y axes // otherwise, use the selection of whatever is linked to us - const incomingVizBox = DocumentView.getFirstDocumentView(this.parentViz)?.ComponentView as DataVizBox; + const incomingVizBox = this.parentViz && (DocumentView.getFirstDocumentView(this.parentViz)?.ComponentView as DataVizBox); const highlitedRowIds = NumListCast(incomingVizBox?.layoutDoc?.dataViz_highlitedRows); return this._tableData.filter((record, i) => highlitedRowIds.includes(this._tableDataIds[i])); // get all the datapoints they have selected field set by incoming anchor } @@ -170,8 +170,8 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { } }); if (!ptWasSelected) { - selectedDatapoints.push(d.x + ',' + d.y); - this._currSelected = selectedDatapoints.length > 1 ? undefined : d; + selectedDatapoints?.push(d.x + ',' + d.y); + this._currSelected = (selectedDatapoints?.length ?? 0 > 1) ? undefined : d; } // for filtering child dataviz docs @@ -190,7 +190,14 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { } } - drawDataPoints(data: DataPoint[], idx: number, xScale: d3.ScaleLinear<number, number, never>, yScale: d3.ScaleLinear<number, number, never>, higlightFocusPt: any, tooltip: any) { + drawDataPoints( + data: DataPoint[], // + idx: number, + xScale: d3.ScaleLinear<number, number, never>, + yScale: d3.ScaleLinear<number, number, never>, + higlightFocusPt: d3.Selection<SVGGElement, unknown, null, undefined>, + tooltip: d3.Selection<HTMLDivElement, unknown, null, undefined> + ) { if (this._lineChartSvg) { const circleClass = '.circle-' + idx; this._lineChartSvg @@ -211,7 +218,7 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { .on('mouseleave', () => { tooltip?.transition().duration(300).style('opacity', 0); }) - .on('click', (e: any) => { + .on('click', e => { const d0 = { x: Number(e.target.getAttribute('data-x')), y: Number(e.target.getAttribute('data-y')) }; // find .circle-d1 with data-x = d0.x and data-y = d0.y this.setCurrSelected(d0); @@ -220,7 +227,7 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { } } - drawChart = (dataSet: any[][], rangeVals: { xMin?: number; xMax?: number; yMin?: number; yMax?: number }, width: number, height: number) => { + drawChart = (dataSet: { x: number; y: number }[][], rangeVals: { xMin?: number; xMax?: number; yMin?: number; yMax?: number }, width: number, height: number) => { // clearing tooltip and the current chart d3.select(this._lineChartRef).select('svg').remove(); d3.select(this._lineChartRef).select('.tooltip').remove(); @@ -277,7 +284,7 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { svg.append('path').attr('stroke', 'red'); // legend - const color: any = d3.scaleOrdinal().range(['black', 'blue']).domain([this._props.axes[1], this._props.axes[2]]); + const color = d3.scaleOrdinal().range(['black', 'blue']).domain([this._props.axes[1], this._props.axes[2]]); svg.selectAll('mydots') .data([this._props.axes[1], this._props.axes[2]]) .enter() diff --git a/src/client/views/nodes/DataVizBox/components/TableBox.tsx b/src/client/views/nodes/DataVizBox/components/TableBox.tsx index b73123691..ad2731109 100644 --- a/src/client/views/nodes/DataVizBox/components/TableBox.tsx +++ b/src/client/views/nodes/DataVizBox/components/TableBox.tsx @@ -128,7 +128,7 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> { selected.splice(selected.indexOf(rowId), 1); } else selected?.push(rowId); e.stopPropagation(); - this.hasRowsToFilter = selected.length > 0; + this.hasRowsToFilter = (selected?.length ?? 0) > 0; }; columnPointerDown = (e: React.PointerEvent, col: string) => { diff --git a/src/client/views/nodes/EquationBox.tsx b/src/client/views/nodes/EquationBox.tsx index dcc6e27ed..3cacb6692 100644 --- a/src/client/views/nodes/EquationBox.tsx +++ b/src/client/views/nodes/EquationBox.tsx @@ -1,7 +1,6 @@ import { action, computed, makeObservable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { Doc } from '../../../fields/Doc'; import { NumCast, StrCast } from '../../../fields/Types'; import { TraceMobx } from '../../../fields/util'; import { DocUtils } from '../../documents/DocUtils'; diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index de49f502f..a6872f8dc 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -6,16 +6,17 @@ import { DateField } from '../../../fields/DateField'; import { Doc, Field, FieldType, Opt } from '../../../fields/Doc'; import { List } from '../../../fields/List'; import { ScriptField } from '../../../fields/ScriptField'; +import { WebField } from '../../../fields/URLField'; +import { DragManager } from '../../util/DragManager'; import { dropActionType } from '../../util/DropActionTypes'; import { Transform } from '../../util/Transform'; +import { ContextMenuProps } from '../ContextMenuItem'; import { PinProps } from '../PinFuncs'; import { ViewBoxInterface } from '../ViewBoxInterface'; import { DocumentView } from './DocumentView'; import { FocusViewOptions } from './FocusViewOptions'; -import { OpenWhere } from './OpenWhere'; -import { WebField } from '../../../fields/URLField'; -import { ContextMenuProps } from '../ContextMenuItem'; import { FormattedTextBox } from './formattedText/FormattedTextBox'; +import { OpenWhere } from './OpenWhere'; export type FocusFuncType = (doc: Doc, options: FocusViewOptions) => Opt<number>; export type StyleProviderFuncType = ( @@ -68,7 +69,7 @@ export interface FieldViewSharedProps { isGroupActive?: () => string | undefined; // is this document part of a group that is active // eslint-disable-next-line no-use-before-define setContentViewBox?: (view: ViewBoxInterface<FieldViewProps>) => void; // called by rendered field's viewBox so that DocumentView can make direct calls to the viewBox - + rejectDrop?: (de: DragManager.DropEvent, subView?: DocumentView) => boolean; // whether a document drop is rejected PanelWidth: () => number; PanelHeight: () => number; isDocumentActive?: () => boolean | undefined; // whether a document should handle pointer events diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.tsx b/src/client/views/nodes/FontIconBox/FontIconBox.tsx index 3190757e2..a3167ee06 100644 --- a/src/client/views/nodes/FontIconBox/FontIconBox.tsx +++ b/src/client/views/nodes/FontIconBox/FontIconBox.tsx @@ -117,10 +117,10 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { default: type = 'slider'; break; } // prettier-ignore - const numScript = (value?: number) => ScriptCast(this.Document.script).script.run({ this: this.Document, value, _readOnly_: value === undefined }); + const numScript = (value?: number) => ScriptCast(this.Document.script)?.script.run({ this: this.Document, value, _readOnly_: value === undefined }); const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string; // Script for checking the outcome of the toggle - const checkResult = Number(Number(numScript().result ?? 0).toPrecision(NumCast(this.dataDoc.numPrecision, 3))); + const checkResult = Number(Number(numScript()?.result ?? 0).toPrecision(NumCast(this.dataDoc.numPrecision, 3))); return ( <NumberDropdown @@ -207,6 +207,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { */ @computed get dropdownListButton() { const script = ScriptCast(this.Document.script); + if (!script) return null; const selectedFunc = () => script?.script.run({ this: this.Document, value: '', _readOnly_: true }).result as string; const { buttonList, selectedVal, getStyle, jsx, toolTip } = (() => { switch (this.Document.title) { @@ -291,7 +292,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string; const background = this._props.styleProvider?.(this.Document, this._props, StyleProp.BackgroundColor) as string; const items = DocListCast(this.dataDoc.data); - const selectedItems = items.filter(itemDoc => ScriptCast(itemDoc.onClick).script.run({ this: itemDoc, value: undefined, _readOnly_: true }).result).map(item => StrCast(item.toolType)); + const selectedItems = items.filter(itemDoc => ScriptCast(itemDoc.onClick)?.script.run({ this: itemDoc, value: undefined, _readOnly_: true }).result).map(item => StrCast(item.toolType)); return ( <MultiToggle @@ -316,7 +317,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { // it would be better to pas the 'added' flag to the callback script, but our script generator from currentUserUtils makes it hard to define // arbitrary parameter variables (but it could be done as a special case or with additional effort when creating the sript) const itemsChanged = items.filter(item => (val instanceof Array ? val.includes(item.toolType as string | number) : item.toolType === val)); - itemsChanged.forEach(itemDoc => ScriptCast(itemDoc.onClick).script.run({ this: itemDoc, _added_: added, value: toggleStatus, itemDoc, _readOnly_: false })); + itemsChanged.forEach(itemDoc => ScriptCast(itemDoc.onClick)?.script.run({ this: itemDoc, _added_: added, value: toggleStatus, itemDoc, _readOnly_: false })); }} /> ); diff --git a/src/client/views/nodes/FunctionPlotBox.tsx b/src/client/views/nodes/FunctionPlotBox.tsx index 91c351895..8e4b64851 100644 --- a/src/client/views/nodes/FunctionPlotBox.tsx +++ b/src/client/views/nodes/FunctionPlotBox.tsx @@ -2,10 +2,11 @@ import functionPlot, { Chart } from 'function-plot'; import { action, computed, makeObservable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { Doc, DocListCast } from '../../../fields/Doc'; +import { returnFalse, setupMoveUpEvents } from '../../../ClientUtils'; +import { emptyFunction } from '../../../Utils'; +import { Doc, DocListCast, NumListCast } from '../../../fields/Doc'; import { List } from '../../../fields/List'; -import { listSpec } from '../../../fields/Schema'; -import { Cast, StrCast } from '../../../fields/Types'; +import { StrCast } from '../../../fields/Types'; import { TraceMobx } from '../../../fields/util'; import { DocUtils } from '../../documents/DocUtils'; import { DocumentType } from '../../documents/DocumentTypes'; @@ -15,8 +16,6 @@ import { undoBatch } from '../../util/UndoManager'; import { ViewBoxAnnotatableComponent } from '../DocComponent'; import { PinDocView, PinProps } from '../PinFuncs'; import { FieldView, FieldViewProps } from './FieldView'; -import { returnFalse, setupMoveUpEvents } from '../../../ClientUtils'; -import { emptyFunction } from '../../../Utils'; @observer export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { @@ -76,7 +75,7 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps> this._plotEle = ele || this._plotEle; const width = this._props.PanelWidth(); const height = this._props.PanelHeight(); - const xrange = Cast(this.layoutDoc.xRange, listSpec('number'), [-10, 10]); + const xrange = NumListCast(this.layoutDoc.xRange, [-10, 10])!; try { this._plotEle?.children.length && this._plotEle.removeChild(this._plotEle.children[0]); this._plot = functionPlot({ diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss index 4a6e8eb49..ac1a6ece9 100644 --- a/src/client/views/nodes/ImageBox.scss +++ b/src/client/views/nodes/ImageBox.scss @@ -106,7 +106,7 @@ height: 100%; img { object-fit: contain; - height: 100%; + height: fit-content; } .imageBox-fadeBlocker, @@ -241,3 +241,37 @@ color: black; } } +.imageBox-regenerate-dialog { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: white; + padding: 20px; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); + z-index: 10000; + + h3 { + margin-top: 0; + } + + input { + width: 300px; + padding: 8px; + margin-bottom: 10px; + } + + .buttons { + display: flex; + justify-content: flex-end; + gap: 10px; + + .generate-btn { + background: #0078d4; + color: white; + border: none; + padding: 8px 16px; + } + } +} diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 0c475b7bb..31a135fa7 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -1,8 +1,8 @@ -import { Button, Colors, Size, Type } from '@dash/components'; +import { Button, Colors, EditableText, Size, Type } from '@dash/components'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Slider, Tooltip } from '@mui/material'; import axios from 'axios'; -import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction } from 'mobx'; +import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import { extname } from 'path'; import * as React from 'react'; @@ -13,11 +13,12 @@ import { Doc, DocListCast, Opt } from '../../../fields/Doc'; import { DocData } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; 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 { ImageField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; -import { Upload } from '../../../server/SharedMediaTypes'; import { emptyFunction } from '../../../Utils'; import { Docs } from '../../documents/Documents'; import { DocumentType } from '../../documents/DocumentTypes'; @@ -26,7 +27,7 @@ import { Networking } from '../../Network'; import { DragManager } from '../../util/DragManager'; import { SettingsManager } from '../../util/SettingsManager'; import { SnappingManager } from '../../util/SnappingManager'; -import { undoable, undoBatch } from '../../util/UndoManager'; +import { undoable, undoBatch, UndoManager } from '../../util/UndoManager'; import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView'; import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; @@ -45,7 +46,6 @@ import { FieldView, FieldViewProps } from './FieldView'; import { FocusViewOptions } from './FocusViewOptions'; import './ImageBox.scss'; import { OpenWhere } from './OpenWhere'; -import { ComputedField } from '../../../fields/ScriptField'; const DefaultPath = '/assets/unknown-file-icon-hi.png'; export class ImageEditorData { @@ -104,6 +104,10 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { @observable private _regenerateLoading = false; @observable private _prevImgs: FireflyImageData[] = StrCast(this.Document.ai_firefly_history) ? JSON.parse(StrCast(this.Document.ai_firefly_history)) : []; + // Add these observable properties to the ImageBox class + @observable private _outpaintingInProgress = false; + @observable private _outpaintingPrompt = ''; + constructor(props: FieldViewProps) { super(props); makeObservable(this); @@ -166,6 +170,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { }, { fireImmediately: true } ); + this._disposers.outpaint = reaction( + () => this.Document[this.fieldKey + '_outpaintOriginalWidth'] !== undefined && !SnappingManager.ShiftKey, + complete => complete && this.openOutpaintPrompt(), + { fireImmediately: true } + ); } componentWillUnmount() { @@ -200,7 +209,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { drop = undoable( action((e: Event, de: DragManager.DropEvent) => { - if (de.complete.docDragData) { + if (de.complete.docDragData && !this._props.rejectDrop?.(de, this.DocumentView?.())) { let added: boolean | undefined; const hitDropTarget = (ele: HTMLElement, dropTarget: HTMLDivElement | null): boolean => { if (!ele) return false; @@ -294,7 +303,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { const anchy = NumCast(cropping.y); const anchw = NumCast(cropping._width); const anchh = NumCast(cropping._height); - const viewScale = NumCast(this.dataDoc[this.fieldKey + '_nativeHeight']) / anchh; + const viewScale = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']) / anchw; cropping.x = NumCast(this.Document.x) + NumCast(this.layoutDoc._width); cropping.y = NumCast(this.Document.y); cropping.onClick = undefined; @@ -339,6 +348,142 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { } }); + @observable _showOutpaintPrompt: boolean = false; + @observable _outpaintPromptInput: string = 'Extend this image naturally with matching content'; + + @action + openOutpaintPrompt = () => { + this._showOutpaintPrompt = true; + }; + + @action + closeOutpaintPrompt = () => { + this._showOutpaintPrompt = false; + }; + + @action + handlePromptChange = (val: string | number) => { + this._outpaintPromptInput = '' + val; + }; + + @action + submitOutpaintPrompt = () => { + this.closeOutpaintPrompt(); + this.processOutpaintingWithPrompt(this._outpaintPromptInput); + }; + + @action + processOutpaintingWithPrompt = async (customPrompt: string) => { + const field = Cast(this.dataDoc[this.fieldKey], ImageField); + if (!field) return; + + const origWidth = NumCast(this.Document[this.fieldKey + '_outpaintOriginalWidth']); + const origHeight = NumCast(this.Document[this.fieldKey + '_outpaintOriginalHeight']); + + // Set flag that outpainting is in progress + this._outpaintingInProgress = true; + + // Revert dimensions if prompt is blank (acts like Cancel) + if (!customPrompt) { + this.Document._width = origWidth; + this.Document._height = origHeight; + this._outpaintingInProgress = false; + return; + } + + try { + const currentPath = this.choosePath(field.url); + const newWidth = NumCast(this.Document._width); + const newHeight = NumCast(this.Document._height); + + // Optional: add loading indicator + const loadingOverlay = document.createElement('div'); + loadingOverlay.style.position = 'absolute'; + loadingOverlay.style.top = '0'; + loadingOverlay.style.left = '0'; + loadingOverlay.style.width = '100%'; + loadingOverlay.style.height = '100%'; + loadingOverlay.style.background = 'rgba(0,0,0,0.5)'; + loadingOverlay.style.display = 'flex'; + loadingOverlay.style.justifyContent = 'center'; + loadingOverlay.style.alignItems = 'center'; + loadingOverlay.innerHTML = '<div style="color: white; font-size: 16px;">Generating outpainted image...</div>'; + this._mainCont?.appendChild(loadingOverlay); + + const response = await Networking.PostToServer('/outpaintImage', { + imageUrl: currentPath, + prompt: customPrompt, + originalDimensions: { width: Math.min(newWidth, origWidth), height: Math.min(newHeight, origHeight) }, + newDimensions: { width: newWidth, height: newHeight }, + }); + + const error = ('error' in response && (response.error as string)) || ''; + if (error.includes('Dropbox') && confirm('Outpaint image failed. Try authorizing DropBox?\r\n' + error.replace(/^[^"]*/, ''))) { + DrawingFillHandler.authorizeDropbox(); + } else { + const batch = UndoManager.StartBatch('outpaint image'); + if (response && typeof response === 'object' && 'url' in response && typeof response.url === 'string') { + if (!this.dataDoc[this.fieldKey + '_alternates']) { + this.dataDoc[this.fieldKey + '_alternates'] = new List<Doc>(); + } + + const originalDoc = Docs.Create.ImageDocument(field.url.href, { + title: `Original: ${this.Document.title}`, + _nativeWidth: Doc.NativeWidth(this.dataDoc), + _nativeHeight: Doc.NativeHeight(this.dataDoc), + }); + + Doc.AddDocToList(this.dataDoc, this.fieldKey + '_alternates', originalDoc); + + // Replace with new outpainted image + this.dataDoc[this.fieldKey] = new ImageField(response.url); + + Doc.SetNativeWidth(this.dataDoc, newWidth); + Doc.SetNativeHeight(this.dataDoc, newHeight); + + this.Document.$ai = true; + this.Document.$ai_outpainted = true; + this.Document.$ai_outpaint_prompt = customPrompt; + this.Document[this.fieldKey + '_outpaintOriginalWidth'] = undefined; + this.Document[this.fieldKey + '_outpaintOriginalHeight'] = undefined; + } else { + this.Document._width = origWidth; + this.Document._height = origHeight; + alert('Failed to receive a valid image URL from server.'); + } + batch.end(); + } + + this._mainCont?.removeChild(loadingOverlay); + } catch (error) { + console.error('Error during outpainting:', error); + this.Document._width = origWidth; + this.Document._height = origHeight; + alert('An error occurred while outpainting. Please try again.'); + } finally { + runInAction(() => (this._outpaintingInProgress = false)); + } + }; + + componentUI = () => + !this._showOutpaintPrompt ? null : ( + <div key="imageBox-componentui" className="imageBox-regenerate-dialog" style={{ backgroundColor: SettingsManager.userBackgroundColor, color: SettingsManager.userColor }}> + <h3>Outpaint Image</h3> + <EditableText + placeholder="Enter a prompt for extending the image:" + setVal={val => this.handlePromptChange(val)} + val={this._outpaintPromptInput} + type={Type.TERT} + color={SettingsManager.userColor} + background={SettingsManager.userBackgroundColor} + /> + <div className="buttons"> + <Button text="Cancel" type={Type.TERT} onClick={this.closeOutpaintPrompt} color={SnappingManager.userColor} background={SnappingManager.userVariantColor} /> + <Button text="Generate" type={Type.TERT} onClick={this.submitOutpaintPrompt} color={SnappingManager.userColor} background={SnappingManager.userVariantColor} /> + </div> + </div> + ); + specificContextMenu = (): void => { const field = Cast(this.dataDoc[this.fieldKey], ImageField); if (field) { @@ -346,9 +491,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { funcs.push({ description: 'Rotate Clockwise 90', event: this.rotate, icon: 'redo-alt' }); funcs.push({ description: `Show ${this.layoutDoc._showFullRes ? 'Dynamic Res' : 'Full Res'}`, event: this.resolution, icon: 'expand' }); funcs.push({ description: 'Set Native Pixel Size', event: this.setNativeSize, icon: 'expand-arrows-alt' }); - funcs.push({ - description: 'GetImageText', - event: () => { + funcs.push({ description: 'GetImageText', event: () => { Networking.PostToServer('/queryFireflyImageText', { file: (file => { const ext = file ? extname(file) : ''; @@ -357,25 +500,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { }).then(text => alert(text)); }, icon: 'expand-arrows-alt', - }); - funcs.push({ - description: 'Expand Image', - event: () => { - Networking.PostToServer('/expandImage', { - prompt: 'sunny skies', - file: (file => { - const ext = file ? extname(file) : ''; - return file?.replace(ext, (this._error ? '_o' : this._curSuffix) + ext); - })(ImageCast(this.Document[Doc.LayoutDataKey(this.Document)])?.url.href), - }).then(res => { - const info = res as Upload.ImageInformation; - const img = Docs.Create.ImageDocument(info.accessPaths.agnostic.client, { title: 'expand:' + this.Document.title }); - DocUtils.assignImageInfo(info, img); - this._props.addDocTab(img, OpenWhere.addRight); - }); - }, - icon: 'expand-arrows-alt', - }); + }); // prettier-ignore funcs.push({ description: 'Copy path', event: () => ClientUtils.CopyText(this.choosePath(field.url)), icon: 'copy' }); funcs.push({ description: 'Open Image Editor', event: this.docEditorView, icon: 'pencil-alt' }); this.layoutDoc.ai && @@ -396,6 +521,22 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { event: action(undoable(async () => await StickerPalette.addToPalette(this.Document), 'save to palette')), icon: this.Document.savedAsSticker ? 'clipboard-check' : 'file-arrow-down', }); + // Add new outpainting option + funcs.push({ description: 'Outpaint Image', event: () => this.openOutpaintPrompt(), icon: 'brush' }); + + // Add outpainting history option if the image was outpainted + this.Document.ai_outpainted && + funcs.push({ + description: 'View Original Image', + event: action(() => { + const alternates = DocListCast(this.dataDoc[this.fieldKey + '_alternates']); + if (alternates && alternates.length) { + // Toggle to show the original image + this.layoutDoc[this.fieldKey + '_usePath'] = 'alternate'; + } + }), + icon: 'image', + }); ContextMenu.Instance?.addItem({ description: 'Options...', subitems: funcs, icon: 'asterisk' }); } }; @@ -432,7 +573,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { const ext = extname(url.href); return url.href.replace(ext, (this._error ? '_o' : this._curSuffix) + ext); }; - getScrollHeight = () => (this._props.fitWidth?.(this.Document) !== false && NumCast(this.layoutDoc._freeform_scale, 1) === NumCast(this.dataDoc._freeform_scaleMin, 1) ? this.nativeSize.nativeHeight : undefined); + getScrollHeight = () => (this._props.fitWidth?.(this.Document) !== false && NumCast(this.layoutDoc._freeform_scale, 1) === NumCast(this.dataDoc.freeform_scaleMin, 1) ? this.nativeSize.nativeHeight : undefined); @computed get usingAlternate() { const usePath = StrCast(this.Document[this.fieldKey + '_usePath']); @@ -583,7 +724,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { ref={action((r: HTMLImageElement | null) => (this.imageRef = r))} key="paths" src={srcpath} - style={{ transform, transformOrigin }} + style={{ transform, transformOrigin, height: this.Document[this.fieldKey + '_outpaintOriginalWidth'] !== undefined ? '100%' : undefined }} onError={action(e => (this._error = e.toString()))} draggable={false} width={nativeWidth} @@ -752,82 +893,90 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { const borderRad = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BorderRounding) as string; const borderRadius = borderRad?.includes('px') ? `${Number(borderRad.split('px')[0]) / (this._props.NativeDimScaling?.() || 1)}px` : borderRad; return ( - <div - className="imageBox" - onContextMenu={this.specificContextMenu} - ref={this.createDropTarget} - onScroll={action(() => { - if (!this._forcedScroll) { - if (this.layoutDoc._layout_scrollTop || this._mainCont?.scrollTop) { - this._ignoreScroll = true; - this.layoutDoc._layout_scrollTop = this._mainCont?.scrollTop; - this._ignoreScroll = false; + <> + <div + className="imageBox" + onContextMenu={this.specificContextMenu} + ref={this.createDropTarget} + onScroll={action(() => { + if (!this._forcedScroll) { + if (this.layoutDoc._layout_scrollTop || this._mainCont?.scrollTop) { + this._ignoreScroll = true; + this.layoutDoc._layout_scrollTop = this._mainCont?.scrollTop; + this._ignoreScroll = false; + } } - } - })} - style={{ - width: this._props.PanelWidth() ? undefined : `100%`, - height: this._props.PanelHeight() ? undefined : `100%`, - pointerEvents: this.layoutDoc._lockedPosition ? 'none' : undefined, - borderRadius, - overflow: this.layoutDoc.layout_fitWidth || this._props.fitWidth?.(this.Document) ? 'auto' : 'hidden', - }}> - <CollectionFreeFormView - ref={this._ffref} - {...this._props} - Document={this.Document} - setContentViewBox={emptyFunction} - NativeWidth={returnZero} - NativeHeight={returnZero} - renderDepth={this._props.renderDepth + 1} - fieldKey={this.annotationKey} - styleProvider={this._props.styleProvider} - isAnnotationOverlay - annotationLayerHostsContent - PanelWidth={this._props.PanelWidth} - PanelHeight={this._props.PanelHeight} - ScreenToLocalTransform={this.screenToLocalTransform} - select={emptyFunction} - focus={this.focus} - getScrollHeight={this.getScrollHeight} - NativeDimScaling={returnOne} - isAnyChildContentActive={returnFalse} - whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} - removeDocument={this.removeDocument} - moveDocument={this.moveDocument} - addDocument={this.addDocument}> - {this.content} - </CollectionFreeFormView> - {this.Loading ? ( - <div className="loading-spinner" style={{ position: 'absolute' }}> - <ReactLoading type="spin" height={50} width={50} color={'blue'} /> - </div> - ) : null} - {this.regenerateImageIcon} - {this.overlayImageIcon} - {this.annotationLayer} - {!this._mainCont || !this.DocumentView || !this._annotationLayer.current ? null : ( - <MarqueeAnnotator + })} + style={{ + width: this._props.PanelWidth() ? undefined : `100%`, + height: this._props.PanelHeight() ? undefined : `100%`, + pointerEvents: this.layoutDoc._lockedPosition ? 'none' : undefined, + borderRadius, + overflow: this.layoutDoc.layout_fitWidth || this._props.fitWidth?.(this.Document) ? 'auto' : 'hidden', + }}> + <CollectionFreeFormView + ref={this._ffref} + {...this._props} Document={this.Document} - ref={this.marqueeref} - scrollTop={0} - annotationLayerScrollTop={0} - scaling={returnOne} - annotationLayerScaling={this._props.NativeDimScaling} - screenTransform={this.DocumentView().screenToViewTransform} - docView={this.DocumentView} - addDocument={this.addDocument} - finishMarquee={this.finishMarquee} - savedAnnotations={this.savedAnnotations} - selectionText={returnEmptyString} - annotationLayer={this._annotationLayer.current} - marqueeContainer={this._mainCont} - highlightDragSrcColor="" - anchorMenuCrop={this.crop} - // anchorMenuFlashcard={() => this.getImageDesc()} - /> - )} - </div> + setContentViewBox={emptyFunction} + NativeWidth={returnZero} + NativeHeight={returnZero} + renderDepth={this._props.renderDepth + 1} + fieldKey={this.annotationKey} + styleProvider={this._props.styleProvider} + isAnnotationOverlay + annotationLayerHostsContent + PanelWidth={this._props.PanelWidth} + PanelHeight={this._props.PanelHeight} + ScreenToLocalTransform={this.screenToLocalTransform} + select={emptyFunction} + focus={this.focus} + rejectDrop={this._props.rejectDrop} + getScrollHeight={this.getScrollHeight} + NativeDimScaling={returnOne} + isAnyChildContentActive={returnFalse} + whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} + removeDocument={this.removeDocument} + moveDocument={this.moveDocument} + addDocument={this.addDocument}> + {this.content} + </CollectionFreeFormView> + {this.Loading ? ( + <div className="loading-spinner" style={{ position: 'absolute' }}> + <ReactLoading type="spin" height={50} width={50} color={'blue'} /> + </div> + ) : null} + {this.regenerateImageIcon} + {this.overlayImageIcon} + {this.annotationLayer} + {!this._mainCont || !this.DocumentView || !this._annotationLayer.current ? null : ( + <MarqueeAnnotator + Document={this.Document} + ref={this.marqueeref} + scrollTop={0} + annotationLayerScrollTop={0} + scaling={returnOne} + annotationLayerScaling={this._props.NativeDimScaling} + screenTransform={this.DocumentView().screenToViewTransform} + docView={this.DocumentView} + addDocument={this.addDocument} + finishMarquee={this.finishMarquee} + savedAnnotations={this.savedAnnotations} + selectionText={returnEmptyString} + annotationLayer={this._annotationLayer.current} + marqueeContainer={this._mainCont} + highlightDragSrcColor="" + anchorMenuCrop={this.crop} + // anchorMenuFlashcard={() => this.getImageDesc()} + /> + )} + {this._outpaintingInProgress && ( + <div className="imageBox-outpaintingSpinner"> + <ReactLoading type="spin" color="#666" height={60} width={60} /> + </div> + )} + </div> + </> ); } @@ -840,10 +989,14 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { const file = input.files?.[0]; if (file) { const disposer = OverlayView.ShowSpinner(); - DocUtils.uploadFileToDoc(file, {}, this.Document).then(doc => { - disposer(); - doc && (doc.height = undefined); - }); + const [{ result }] = await Networking.UploadFilesToServer({ file }); + if (result instanceof Error) { + alert('Error uploading files - possibly due to unsupported file types'); + } else { + this.dataDoc[this.fieldKey] = new ImageField(result.accessPaths.agnostic.client); + !(result instanceof Error) && DocUtils.assignImageInfo(result, this.dataDoc); + } + disposer(); } else { console.log('No file selected'); } diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx index 8bf65b637..78c8a686c 100644 --- a/src/client/views/nodes/LinkBox.tsx +++ b/src/client/views/nodes/LinkBox.tsx @@ -128,6 +128,7 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() { const getAnchor = (field: FieldResult): Element[] => { const docField = DocCast(field); const doc = docField?.layout_unrendered ? DocCast(docField.annotationOn, docField) : docField; + if (!doc) return []; const ele = document.getElementById(DocumentView.UniquifyId(DocumentView.LightboxContains(this.DocumentView?.()), doc[Id])); if (ele?.className === 'linkBox-label') foundParent = true; if (ele?.getBoundingClientRect().width) return [ele]; diff --git a/src/client/views/nodes/MapBox/DirectionsAnchorMenu.tsx b/src/client/views/nodes/MapBox/DirectionsAnchorMenu.tsx index 8784a709a..8bb7ddd9f 100644 --- a/src/client/views/nodes/MapBox/DirectionsAnchorMenu.tsx +++ b/src/client/views/nodes/MapBox/DirectionsAnchorMenu.tsx @@ -43,7 +43,7 @@ export class DirectionsAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { return this._left > 0; } - constructor(props: Readonly<{}>) { + constructor(props: AntimodeMenuProps) { super(props); DirectionsAnchorMenu.Instance = this; diff --git a/src/client/views/nodes/calendarBox/CalendarBox.tsx b/src/client/views/nodes/calendarBox/CalendarBox.tsx index 4ee2413be..84fa25dba 100644 --- a/src/client/views/nodes/calendarBox/CalendarBox.tsx +++ b/src/client/views/nodes/calendarBox/CalendarBox.tsx @@ -115,6 +115,7 @@ export class CalendarBox extends CollectionSubView() { return 'red'; }; + // eslint-disable-next-line @typescript-eslint/no-unused-vars internalDocDrop = (e: Event, de: DragManager.DropEvent, docDragData: DragManager.DocumentDragData) => { if (!super.onInternalDrop(e, de)) return false; de.complete.docDragData?.droppedDocuments.forEach(doc => { @@ -153,7 +154,6 @@ export class CalendarBox extends CollectionSubView() { doc.startTime = new DateField(startDate); doc.endTime = new DateField(endDate); } - }; handleEventClick = (arg: EventClickArg) => { @@ -201,40 +201,36 @@ export class CalendarBox extends CollectionSubView() { eventDrop: this.handleEventDrop, eventResize: this.handleEventDrop, eventDidMount: arg => { + const doc = DocServer.GetCachedRefField(arg.event._def.groupId ?? ''); + if (!doc) return; - const doc = DocServer.GetCachedRefField(arg.event._def.groupId ?? ''); - if (!doc) return; - - if (doc.type === 'task') { - const checkButton = document.createElement('button'); - checkButton.innerText = doc.completed ? '✅' : '⬜'; - checkButton.style.position = 'absolute'; - checkButton.style.right = '5px'; - checkButton.style.top = '50%'; - checkButton.style.transform = 'translateY(-50%)'; - checkButton.style.background = 'transparent'; - checkButton.style.border = 'none'; - checkButton.style.cursor = 'pointer'; - checkButton.style.fontSize = '18px'; - checkButton.style.zIndex = '1000'; - checkButton.style.padding = '0'; - checkButton.style.margin = '0'; - - checkButton.onclick = ev => { - ev.stopPropagation(); - doc.completed = !doc.completed; - this._calendar?.refetchEvents(); - }; - - // Make sure the parent box is positioned relative - arg.el.style.position = 'relative'; - arg.el.appendChild(checkButton); - } - - // (keep your other pointerup/contextmenu handlers here) + if (doc.type === 'task') { + const checkButton = document.createElement('button'); + checkButton.innerText = doc.completed ? '✅' : '⬜'; + checkButton.style.position = 'absolute'; + checkButton.style.right = '5px'; + checkButton.style.top = '50%'; + checkButton.style.transform = 'translateY(-50%)'; + checkButton.style.background = 'transparent'; + checkButton.style.border = 'none'; + checkButton.style.cursor = 'pointer'; + checkButton.style.fontSize = '18px'; + checkButton.style.zIndex = '1000'; + checkButton.style.padding = '0'; + checkButton.style.margin = '0'; + checkButton.onclick = ev => { + ev.stopPropagation(); + doc.completed = !doc.completed; + this._calendar?.refetchEvents(); + }; + // Make sure the parent box is positioned relative + arg.el.style.position = 'relative'; + arg.el.appendChild(checkButton); + } + // (keep your other pointerup/contextmenu handlers here) arg.el.addEventListener('pointerdown', ev => { ev.button && ev.stopPropagation(); diff --git a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx index 528bcd05a..6c3da8977 100644 --- a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx +++ b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx @@ -196,7 +196,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { // Add CSV details to linked files this._linked_csv_files.push({ - filename: CsvCast(newLinkedDoc.data).url.pathname, + filename: CsvCast(newLinkedDoc.data)?.url.pathname ?? '', id: csvId, text: csvData, }); @@ -634,6 +634,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { }; getDirectMatchingSegmentStart = (doc: Doc, citationText: string, indexesOfSegments: string[]): number => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any const originalSegments = JSON.parse(StrCast(doc.original_segments!)).map((segment: any, index: number) => ({ index: index.toString(), text: segment.text, @@ -877,7 +878,8 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { return LinkManager.Instance.getAllRelatedLinks(this.Document) .map(d => DocCast(LinkManager.getOppositeAnchor(d, this.Document))) .map(d => DocCast(d?.annotationOn, d)) - .filter(d => d); + .filter(d => d) + .map(d => d!); } /** @@ -889,6 +891,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { .map(d => DocCast(LinkManager.getOppositeAnchor(d, this.Document))) .map(d => DocCast(d?.annotationOn, d)) .filter(d => d) + .map(d => d!) .filter(d => { console.log(d.ai_doc_id); return d.ai_doc_id; @@ -905,15 +908,14 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { LinkManager.Instance.getAllRelatedLinks(this.Document) .map(d => DocCast(LinkManager.getOppositeAnchor(d, this.Document))) .map(d => DocCast(d?.annotationOn, d)) - .filter(d => d) - .filter(d => d.summary) + .filter(d => d?.summary) .map((doc, index) => { - if (PDFCast(doc.data)) { - return `<summary file_name="${PDFCast(doc.data).url.pathname}" applicable_tools=["rag"]>${doc.summary}</summary>`; - } else if (CsvCast(doc.data)) { - return `<summary file_name="${CsvCast(doc.data).url.pathname}" applicable_tools=["dataAnalysis"]>${doc.summary}</summary>`; + if (PDFCast(doc?.data)) { + return `<summary file_name="${PDFCast(doc!.data)!.url.pathname}" applicable_tools=["rag"]>${doc!.summary}</summary>`; + } else if (CsvCast(doc?.data)) { + return `<summary file_name="${CsvCast(doc!.data)!.url.pathname}" applicable_tools=["dataAnalysis"]>${doc!.summary}</summary>`; } else { - return `${index + 1}) ${doc.summary}`; + return `${index + 1}) ${doc?.summary}`; } }) .join('\n') + '\n' diff --git a/src/client/views/nodes/chatbot/tools/GetDocsTool.ts b/src/client/views/nodes/chatbot/tools/GetDocsTool.ts index 05482a66e..42a7747d3 100644 --- a/src/client/views/nodes/chatbot/tools/GetDocsTool.ts +++ b/src/client/views/nodes/chatbot/tools/GetDocsTool.ts @@ -40,7 +40,10 @@ export class GetDocsTool extends BaseTool<GetDocsToolParamsType> { } async execute(args: ParametersType<GetDocsToolParamsType>): Promise<Observation[]> { - const docs = args.document_ids.map(doc_id => DocCast(DocServer.GetCachedRefField(doc_id))); + const docs = args.document_ids + .map(doc_id => DocCast(DocServer.GetCachedRefField(doc_id))) + .filter(d => d) + .map(d => d!); const collection = Docs.Create.FreeformDocument(docs, { title: args.title }); this._docView._props.addDocTab(collection, OpenWhere.addRight); return [{ type: 'text', text: `Collection created in Dash called ${args.title}` }]; diff --git a/src/client/views/nodes/chatbot/vectorstore/Vectorstore.ts b/src/client/views/nodes/chatbot/vectorstore/Vectorstore.ts index c1915a398..6d524e40f 100644 --- a/src/client/views/nodes/chatbot/vectorstore/Vectorstore.ts +++ b/src/client/views/nodes/chatbot/vectorstore/Vectorstore.ts @@ -15,7 +15,6 @@ import { Networking } from '../../../../Network'; import { AI_Document, CHUNK_TYPE, RAGChunk } from '../types/types'; import OpenAI from 'openai'; import { Embedding } from 'openai/resources'; -import { PineconeEnvironmentVarsNotSupportedError } from '@pinecone-database/pinecone/dist/errors'; dotenv.config(); @@ -101,7 +100,7 @@ export class Vectorstore { } else { // Start processing the document. doc.ai_document_status = 'PROGRESS'; - const local_file_path: string = CsvCast(doc.data)?.url?.pathname ?? PDFCast(doc.data)?.url?.pathname ?? VideoCast(doc.data)?.url?.pathname ?? AudioCast(doc.data)?.url?.pathname; + const local_file_path = CsvCast(doc.data)?.url?.pathname ?? PDFCast(doc.data)?.url?.pathname ?? VideoCast(doc.data)?.url?.pathname ?? AudioCast(doc.data)?.url?.pathname; if (!local_file_path) { console.log('Invalid file path.'); @@ -112,13 +111,13 @@ export class Vectorstore { let result: AI_Document & { doc_id: string }; if (isAudioOrVideo) { console.log('Processing media file...'); - const response = await Networking.PostToServer('/processMediaFile', { fileName: path.basename(local_file_path) }); + const response = (await Networking.PostToServer('/processMediaFile', { fileName: path.basename(local_file_path) })) as { [key: string]: unknown }; const segmentedTranscript = response.condensed; console.log(segmentedTranscript); - const summary = response.summary; + const summary = response.summary as string; doc.summary = summary; // Generate embeddings for each chunk - const texts = segmentedTranscript.map((chunk: any) => chunk.text); + const texts = (segmentedTranscript as { text: string }[])?.map(chunk => chunk.text); try { const embeddingsResponse = await this.openai.embeddings.create({ @@ -138,7 +137,7 @@ export class Vectorstore { file_name: local_file_path, num_pages: 0, summary: '', - chunks: segmentedTranscript.map((chunk: any, index: number) => ({ + chunks: (segmentedTranscript as { text: string; start: number; end: number; indexes: string[] }[]).map((chunk, index) => ({ id: uuidv4(), values: (embeddingsResponse.data as Embedding[])[index].embedding, // Assign embedding metadata: { @@ -173,7 +172,7 @@ export class Vectorstore { } else { // Existing document processing logic remains unchanged console.log('Processing regular document...'); - const { jobId } = await Networking.PostToServer('/createDocument', { file_path: local_file_path }); + const { jobId } = (await Networking.PostToServer('/createDocument', { file_path: local_file_path })) as { jobId: string }; while (true) { await new Promise(resolve => setTimeout(resolve, 2000)); @@ -297,7 +296,7 @@ export class Vectorstore { encoding_format: 'float', }); - let queryEmbedding = queryEmbeddingResponse.data[0].embedding; + const queryEmbedding = queryEmbeddingResponse.data[0].embedding; // Extract the embedding from the response. diff --git a/src/client/views/nodes/formattedText/DailyJournal.tsx b/src/client/views/nodes/formattedText/DailyJournal.tsx index e4127fba3..d6d30dc13 100644 --- a/src/client/views/nodes/formattedText/DailyJournal.tsx +++ b/src/client/views/nodes/formattedText/DailyJournal.tsx @@ -206,7 +206,7 @@ export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>() // Check if doc or selection changed if (!prevState.doc.eq(state.doc) || !prevState.selection.eq(state.selection)) { - let found = false; + const found = false; const textToRemove = this.predictiveText; state.doc.descendants((node, pos) => { diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index dc23a695d..d6fa3172d 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -586,7 +586,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB return true; } const dragData = de.complete.docDragData; - if (dragData) { + if (dragData && !this._props.rejectDrop?.(de, this.DocumentView?.())) { const layoutProto = DocCast(this.layoutDoc.proto); const dataDoc = layoutProto && Doc.IsDelegateField(layoutProto, this.fieldKey) ? layoutProto : this.dataDoc; const effectiveAcl = GetEffectiveAcl(dataDoc); @@ -1149,7 +1149,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB // Since we also monitor all component height changes, this will update the document's height. resetNativeHeight = action((scrollHeight: number) => { this.layoutDoc['_' + this.fieldKey + '_height'] = scrollHeight; - if (!this.layoutDoc.isTemplateForField) this.layoutDoc._nativeHeight = scrollHeight; + if (!this.layoutDoc.isTemplateForField && NumCast(this.layoutDoc._nativeHeight)) this.layoutDoc._nativeHeight = scrollHeight; }); addPlugin = (plugin: Plugin) => { @@ -1344,8 +1344,62 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB return text; }; - handlePaste = (view: EditorView, event: Event /* , slice: Slice */): boolean => { - const pdfAnchorId = (event as ClipboardEvent).clipboardData?.getData('dash/pdfAnchor'); + handlePaste = (view: EditorView, event: ClipboardEvent /* , slice: Slice */): boolean => { + return this.doPaste(view, event.clipboardData); + }; + doPaste = (view: EditorView, data: DataTransfer | null) => { + const html = data?.getData('text/html'); + const pdfAnchorId = data?.getData('dash/pdfAnchor'); + if (html && !pdfAnchorId) { + const replaceDivsWithParagraphs = (expr: string) => { + // Create a temporary DOM container + const container = document.createElement('div'); + container.innerHTML = expr; + + // Recursive function to process all divs + function processDivs(node: HTMLElement) { + // Get all div elements in the current node (live collection) + const divs = node.getElementsByTagName('div'); + + // We need to convert to array because we'll be modifying the DOM + const divsArray = Array.from(divs); + + for (const div of divsArray) { + // Create replacement paragraph + const p = document.createElement('p'); + + // Copy all attributes + for (const attr of div.attributes) { + p.setAttribute(attr.name, attr.value); + } + + // Move all child nodes + while (div.firstChild) { + p.appendChild(div.firstChild); + } + + // Replace the div with the paragraph + div.parentNode?.replaceChild(p, div); + + // Process any nested divs that were moved into the new paragraph + processDivs(p); + } + } + + // Start processing from the container + processDivs(container); + + return container.innerHTML; + }; + const fixedHTML = replaceDivsWithParagraphs(html); + // .replace(/<div\b([^>]*)>(.*?)<\/div>/g, '<p$1>$2</p>'); // prettier-ignore + this._inDrop = true; + view.pasteHTML(html.split('<p').length < 2 ? fixedHTML : html); + this._inDrop = false; + + return true; + } + return !!(pdfAnchorId && this.addPdfReference(pdfAnchorId)); }; @@ -1485,9 +1539,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } if (this._props.isContentActive()) this.prepareForTyping(); if (this.EditorView && FormattedTextBox.PasteOnLoad) { - const pdfAnchorId = FormattedTextBox.PasteOnLoad.clipboardData?.getData('dash/pdfAnchor'); + this.doPaste(this.EditorView, FormattedTextBox.PasteOnLoad.clipboardData); FormattedTextBox.PasteOnLoad = undefined; - pdfAnchorId && this.addPdfReference(pdfAnchorId); } if (this._props.autoFocus) setTimeout(() => this.EditorView!.focus()); // not sure why setTimeout is needed but editing dashFieldView's doesn't work without it. } diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts index 26ccf6931..f26a75fe4 100644 --- a/src/client/views/nodes/formattedText/RichTextRules.ts +++ b/src/client/views/nodes/formattedText/RichTextRules.ts @@ -349,7 +349,8 @@ export class RichTextRules { let count = 0; // ignore first return value which will be the notation that chat is pending a result Doc.SetField(this.Document, '', match[2], false, (gptval: FieldResult) => { if (count) { - const tr = this.TextBox.EditorView?.state.tr.insertText(' ' + (gptval as string)); + this.TextBox.EditorView?.pasteText(' ' + (gptval as string), undefined); + const tr = this.TextBox.EditorView?.state.tr; //.insertText(' ' + (gptval as string)); tr && this.TextBox.EditorView?.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(end + 2), tr.doc.resolve(end + 2 + (gptval as string).length)))); RichTextMenu.Instance?.elideSelection(this.TextBox.EditorView?.state, true); } diff --git a/src/client/views/nodes/imageEditor/imageMeshTool/imageMeshToolButton.tsx b/src/client/views/nodes/imageEditor/imageMeshTool/imageMeshToolButton.tsx index eb68410b0..e580c7070 100644 --- a/src/client/views/nodes/imageEditor/imageMeshTool/imageMeshToolButton.tsx +++ b/src/client/views/nodes/imageEditor/imageMeshTool/imageMeshToolButton.tsx @@ -13,20 +13,11 @@ interface ButtonContainerProps { btnText: string; imageWidth: number; imageHeight: number; - gridXSize: number; // X subdivisions - gridYSize: number; // Y subdivisions + gridXSize: number; // X subdivisions + gridYSize: number; // Y subdivisions } -export function MeshTransformButton({ - loading, - onClick: startMeshTransform, - onReset, - btnText, - imageWidth, - imageHeight, - gridXSize, - gridYSize -}: ButtonContainerProps) { +export function MeshTransformButton({ loading, onClick, onReset, btnText, imageWidth, imageHeight, gridXSize, gridYSize }: ButtonContainerProps) { const [showGrid, setShowGrid] = React.useState(false); const [isGridInteractive, setIsGridInteractive] = React.useState(false); // Controls the dragging of control points const imageRef = React.useRef<HTMLImageElement>(null); // Reference to the image element diff --git a/src/client/views/nodes/scrapbook/EmbeddedDocView.tsx b/src/client/views/nodes/scrapbook/EmbeddedDocView.tsx new file mode 100644 index 000000000..e99bf67c7 --- /dev/null +++ b/src/client/views/nodes/scrapbook/EmbeddedDocView.tsx @@ -0,0 +1,52 @@ +//IGNORE FOR NOW, CURRENTLY NOT USED IN SCRAPBOOK IMPLEMENTATION +import * as React from "react"; +import { observer } from "mobx-react"; +import { Doc } from "../../../../fields/Doc"; +import { DocumentView } from "../DocumentView"; +import { Transform } from "../../../util/Transform"; + +interface EmbeddedDocViewProps { + doc: Doc; + width?: number; + height?: number; + slotId?: string; +} + +@observer +export class EmbeddedDocView extends React.Component<EmbeddedDocViewProps> { + render() { + const { doc, width = 300, height = 200, slotId } = this.props; + + // Use either an existing embedding or create one + let docToDisplay = doc; + + // If we need an embedding, create or use one + if (!docToDisplay.isEmbedding) { + docToDisplay = Doc.BestEmbedding(doc) || Doc.MakeEmbedding(doc); + // Set the container to the slot's ID so we can track it + if (slotId) { + docToDisplay.embedContainer = `scrapbook-slot-${slotId}`; + } + } + + return ( + <DocumentView + Document={docToDisplay} + renderDepth={0} + // Required sizing functions + NativeWidth={() => width} + NativeHeight={() => height} + PanelWidth={() => width} + PanelHeight={() => height} + // Required state functions + isContentActive={() => true} + childFilters={() => []} + ScreenToLocalTransform={() => new Transform()} + // Display options + hideDeleteButton={true} + hideDecorations={true} + hideResizeHandles={true} + /> + ); + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/scrapbook/ScrapbookBox.tsx b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx new file mode 100644 index 000000000..6cfe9a62c --- /dev/null +++ b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx @@ -0,0 +1,143 @@ +import { action, makeObservable, observable } from 'mobx'; +import * as React from 'react'; +import { Doc, DocListCast } from '../../../../fields/Doc'; +import { List } from '../../../../fields/List'; +import { emptyFunction } from '../../../../Utils'; +import { Docs } from '../../../documents/Documents'; +import { DocumentType } from '../../../documents/DocumentTypes'; +import { CollectionView } from '../../collections/CollectionView'; +import { ViewBoxAnnotatableComponent } from '../../DocComponent'; +import { DocumentView } from '../DocumentView'; +import { FieldView, FieldViewProps } from '../FieldView'; +import { DragManager } from '../../../util/DragManager'; +import { RTFCast, StrCast, toList } from '../../../../fields/Types'; +import { undoable } from '../../../util/UndoManager'; +// Scrapbook view: a container that lays out its child items in a grid/template +export class ScrapbookBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { + @observable createdDate: string; + + constructor(props: FieldViewProps) { + super(props); + makeObservable(this); + this.createdDate = this.getFormattedDate(); + + // ensure we always have a List<Doc> in dataDoc['items'] + if (!this.dataDoc[this.fieldKey]) { + this.dataDoc[this.fieldKey] = new List<Doc>(); + } + this.createdDate = this.getFormattedDate(); + this.setTitle(); + } + + public static LayoutString(fieldStr: string) { + return FieldView.LayoutString(ScrapbookBox, fieldStr); + } + + getFormattedDate(): string { + return new Date().toLocaleDateString(undefined, { + year: 'numeric', + month: 'short', + day: 'numeric', + }); + } + + @action + setTitle() { + const title = `Scrapbook - ${this.createdDate}`; + if (this.dataDoc.title !== title) { + this.dataDoc.title = title; + + const image = Docs.Create.TextDocument('image'); + image.accepts_docType = DocumentType.IMG; + const placeholder = new Doc(); + placeholder.proto = image; + placeholder.original = image; + placeholder._width = 250; + placeholder._height = 200; + placeholder.x = 0; + placeholder.y = -100; + //placeholder.overrideFields = new List<string>(['x', 'y']); // shouldn't need to do this for layout fields since the placeholder already overrides its protos + + const summary = Docs.Create.TextDocument('summary'); + summary.accepts_docType = DocumentType.RTF; + summary.accepts_textType = 'one line'; + const placeholder2 = new Doc(); + placeholder2.proto = summary; + placeholder2.original = summary; + placeholder2.x = 0; + placeholder2.y = 200; + placeholder2._width = 250; + //placeholder2.overrideFields = new List<string>(['x', 'y', '_width']); // shouldn't need to do this for layout fields since the placeholder already overrides its protos + this.dataDoc[this.fieldKey] = new List<Doc>([placeholder, placeholder2]); + } + } + + componentDidMount() { + this.setTitle(); + } + + childRejectDrop = (de: DragManager.DropEvent, subView?: DocumentView) => { + return true; // disable dropping documents onto any child of the scrapbook. + }; + rejectDrop = (de: DragManager.DropEvent, subView?: DocumentView) => { + // Test to see if the dropped doc is dropped on an acceptable location (anywerhe? on a specific box). + // const draggedDocs = de.complete.docDragData?.draggedDocuments; + return false; // allow all Docs to be dropped onto scrapbook -- let filterAddDocument make the final decision. + }; + + filterAddDocument = (docIn: Doc | Doc[]) => { + const docs = toList(docIn); + if (docs?.length === 1) { + const placeholder = DocListCast(this.dataDoc[this.fieldKey]).find(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 + + if (placeholder) { + // 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(() => { + //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]; + }, 'Scrapbook add') + ); + return false; + } + } + return false; + }; + + render() { + return ( + <div style={{ background: 'beige', width: '100%', height: '100%' }}> + <CollectionView + {...this._props} // + setContentViewBox={emptyFunction} + rejectDrop={this.rejectDrop} + childRejectDrop={this.childRejectDrop} + filterAddDocument={this.filterAddDocument} + /> + {/* <div style={{ border: '1px black', borderStyle: 'dotted', position: 'absolute', top: '50%', width: '100%', textAlign: 'center' }}>Drop an image here</div> */} + </div> + ); + } +} + +// Register scrapbook +Docs.Prototypes.TemplateMap.set(DocumentType.SCRAPBOOK, { + layout: { view: ScrapbookBox, dataField: 'items' }, + options: { + acl: '', + _height: 200, + _xMargin: 10, + _yMargin: 10, + _layout_fitWidth: false, + _layout_autoHeight: true, + _layout_reflowVertical: true, + _layout_reflowHorizontal: true, + _freeform_fitContentsToBox: true, + defaultDoubleClick: 'ignore', + systemIcon: 'BsImages', + }, +}); diff --git a/src/client/views/nodes/scrapbook/ScrapbookContent.tsx b/src/client/views/nodes/scrapbook/ScrapbookContent.tsx new file mode 100644 index 000000000..ad1d308e8 --- /dev/null +++ b/src/client/views/nodes/scrapbook/ScrapbookContent.tsx @@ -0,0 +1,23 @@ +import React from "react"; +import { observer } from "mobx-react-lite"; +// Import the Doc type from your actual module. +import { Doc } from "../../../../fields/Doc"; + +export interface ScrapbookContentProps { + doc: Doc; +} + +// A simple view that displays a document's title and content. +// Adjust how you extract the text if your Doc fields are objects. +export const ScrapbookContent: React.FC<ScrapbookContentProps> = observer(({ doc }) => { + // If doc.title or doc.content are not plain strings, convert them. + const titleText = doc.title ? doc.title.toString() : "Untitled"; + const contentText = doc.content ? doc.content.toString() : "No content available."; + + return ( + <div className="scrapbook-content"> + <h3>{titleText}</h3> + <p>{contentText}</p> + </div> + ); +}); diff --git a/src/client/views/nodes/scrapbook/ScrapbookSlot.scss b/src/client/views/nodes/scrapbook/ScrapbookSlot.scss new file mode 100644 index 000000000..ae647ad36 --- /dev/null +++ b/src/client/views/nodes/scrapbook/ScrapbookSlot.scss @@ -0,0 +1,85 @@ +//IGNORE FOR NOW, CURRENTLY NOT USED IN SCRAPBOOK IMPLEMENTATION +.scrapbook-slot { + position: absolute; + background-color: rgba(245, 245, 245, 0.7); + border: 2px dashed #ccc; + border-radius: 5px; + box-sizing: border-box; + transition: all 0.2s ease; + overflow: hidden; + + &.scrapbook-slot-over { + border-color: #4a90e2; + background-color: rgba(74, 144, 226, 0.1); + } + + &.scrapbook-slot-filled { + border-style: solid; + border-color: rgba(0, 0, 0, 0.1); + background-color: transparent; + + &.scrapbook-slot-over { + border-color: #4a90e2; + background-color: rgba(74, 144, 226, 0.1); + } + } + + .scrapbook-slot-empty { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + } + + .scrapbook-slot-placeholder { + text-align: center; + color: #888; + } + + .scrapbook-slot-title { + font-weight: bold; + margin-bottom: 5px; + } + + .scrapbook-slot-instruction { + font-size: 0.9em; + font-style: italic; + } + + .scrapbook-slot-content { + width: 100%; + height: 100%; + position: relative; + } + + .scrapbook-slot-controls { + position: absolute; + top: 5px; + right: 5px; + z-index: 10; + opacity: 0; + transition: opacity 0.2s ease; + + .scrapbook-slot-remove-btn { + background-color: rgba(255, 255, 255, 0.8); + border: 1px solid #ccc; + border-radius: 50%; + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + font-size: 10px; + + &:hover { + background-color: rgba(255, 0, 0, 0.1); + } + } + } + + &:hover .scrapbook-slot-controls { + opacity: 1; + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/scrapbook/ScrapbookSlot.tsx b/src/client/views/nodes/scrapbook/ScrapbookSlot.tsx new file mode 100644 index 000000000..2c8f93778 --- /dev/null +++ b/src/client/views/nodes/scrapbook/ScrapbookSlot.tsx @@ -0,0 +1,28 @@ + +//IGNORE FOR NOW, CURRENTLY NOT USED IN SCRAPBOOK IMPLEMENTATION +export interface SlotDefinition { + id: string; + x: number; y: number; + defaultWidth: number; + defaultHeight: number; + } + + export interface SlotContentMap { + slotId: string; + docId?: string; + } + + export interface ScrapbookConfig { + slots: SlotDefinition[]; + contents?: SlotContentMap[]; + } + + export const DEFAULT_SCRAPBOOK_CONFIG: ScrapbookConfig = { + slots: [ + { id: "slot1", x: 10, y: 10, defaultWidth: 180, defaultHeight: 120 }, + { id: "slot2", x: 200, y: 10, defaultWidth: 180, defaultHeight: 120 }, + // …etc + ], + contents: [] + }; +
\ No newline at end of file diff --git a/src/client/views/nodes/scrapbook/ScrapbookSlotTypes.ts b/src/client/views/nodes/scrapbook/ScrapbookSlotTypes.ts new file mode 100644 index 000000000..686917d9a --- /dev/null +++ b/src/client/views/nodes/scrapbook/ScrapbookSlotTypes.ts @@ -0,0 +1,25 @@ +// ScrapbookSlotTypes.ts +export interface SlotDefinition { + id: string; + title: string; + x: number; + y: number; + defaultWidth: number; + defaultHeight: number; + } + + export interface ScrapbookConfig { + slots: SlotDefinition[]; + contents?: { slotId: string; docId: string }[]; + } + + // give it three slots by default: + export const DEFAULT_SCRAPBOOK_CONFIG: ScrapbookConfig = { + slots: [ + { id: "main", title: "Main Content", x: 20, y: 20, defaultWidth: 360, defaultHeight: 200 }, + { id: "notes", title: "Notes", x: 20, y: 240, defaultWidth: 360, defaultHeight: 160 }, + { id: "resources", title: "Resources", x: 400, y: 20, defaultWidth: 320, defaultHeight: 380 }, + ], + contents: [], + }; +
\ No newline at end of file diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx index 1891cfd4c..e8a5235c9 100644 --- a/src/client/views/pdf/Annotation.tsx +++ b/src/client/views/pdf/Annotation.tsx @@ -50,7 +50,7 @@ interface IAnnotationProps extends FieldViewProps { } @observer export class Annotation extends ObservableReactComponent<IAnnotationProps> { - constructor(props: any) { + constructor(props: IAnnotationProps) { super(props); makeObservable(this); } @@ -58,7 +58,7 @@ export class Annotation extends ObservableReactComponent<IAnnotationProps> { @computed get linkHighlighted() { const found = LinkManager.Instance.getAllDirectLinks(this._props.annoDoc).find(link => { const a1 = Doc.getOppositeAnchor(link, this._props.annoDoc); - return a1 && Doc.GetBrushStatus(DocCast(a1.annotationOn, a1)); + return a1 && Doc.GetBrushStatus(DocCast(a1.annotationOn, a1)!); }); return found; } @@ -112,6 +112,7 @@ export class Annotation extends ObservableReactComponent<IAnnotationProps> { outline = () => (this.linkHighlighted ? 'solid 1px lightBlue' : undefined); background = () => (this._props.annoDoc[Highlight] ? 'orange' : StrCast(this._props.annoDoc.backgroundColor)); render() { + // eslint-disable-next-line @typescript-eslint/no-unused-vars const forceRenderHack = [this.background(), this.outline(), this.opacity()]; // forces a re-render when these change -- because RegionAnnotation doesn't do this internally.. return ( <div style={{ display: this._props.annoDoc.textCopied && !Doc.GetBrushHighlightStatus(this._props.annoDoc) ? 'none' : undefined }}> @@ -121,7 +122,7 @@ export class Annotation extends ObservableReactComponent<IAnnotationProps> { .map(([x, y, width, height]) => ( <div key={'' + x + y + width + height} - style={{ pointerEvents: this._props.pointerEvents?.() as any }} + style={{ pointerEvents: this._props.pointerEvents?.() as Property.PointerEvents }} onPointerDown={this.onPointerDown} onContextMenu={this.onContextMenu} onPointerEnter={() => { diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index 21d987587..fc2567fbc 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -31,7 +31,7 @@ import { Annotation } from './Annotation'; import { GPTPopup } from './GPTPopup/GPTPopup'; import './PDFViewer.scss'; import { DocumentViewProps } from '../nodes/DocumentContentsView'; -if (window?.Worker) GlobalWorkerOptions.workerSrc = 'files/pdf.worker.min.mjs'; +if (window?.Worker) GlobalWorkerOptions.workerSrc = 'files/node_modules/pdfjs-dist/build/pdf.worker.min.mjs'; // npm start/etc use copyfiles to copy the worker from the pdfjs-dist package to the public folder export * from 'pdfjs-dist/build/pdf.mjs'; interface IViewerProps extends FieldViewProps { @@ -146,6 +146,7 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> { e.clipboardData.setData('dash/pdfAnchor', anchor[DocData][Id]); } e.preventDefault(); + e.stopPropagation(); } }; diff --git a/src/client/views/search/FaceRecognitionHandler.tsx b/src/client/views/search/FaceRecognitionHandler.tsx index f60eb486e..3ad5bc844 100644 --- a/src/client/views/search/FaceRecognitionHandler.tsx +++ b/src/client/views/search/FaceRecognitionHandler.tsx @@ -43,7 +43,7 @@ export class FaceRecognitionHandler { * Loads an image */ private static loadImage = (imgUrl: ImageField): Promise<HTMLImageElement> => { - const [name, type] = ImageCastToNameType(imgUrl.url.href); + const [name, type] = ImageCastToNameType(imgUrl); const imageURL = `${name}_o.${type}`; return new Promise((resolve, reject) => { @@ -162,7 +162,7 @@ export class FaceRecognitionHandler { }); uniqueFaceDoc.$face = `Face${faceDocNum}`; uniqueFaceDoc.$face_annos = new List<Doc>(); - Doc.SetContainer(uniqueFaceDoc, Doc.MyFaceCollection); + Doc.MyFaceCollection && Doc.SetContainer(uniqueFaceDoc, Doc.MyFaceCollection); Doc.ActiveDashboard && Doc.AddDocToList(Doc.ActiveDashboard[DocData], 'myUniqueFaces', uniqueFaceDoc); return uniqueFaceDoc; @@ -208,9 +208,9 @@ export class FaceRecognitionHandler { setTimeout(() => this.classifyFacesInImage(imgDoc), 1000); } else { const imgUrl = ImageCast(imgDoc[Doc.LayoutDataKey(imgDoc)]); - if (imgUrl && !DocListCast(Doc.MyFaceCollection.examinedFaceDocs).includes(imgDoc[DocData])) { + if (imgUrl && !DocListCast(Doc.MyFaceCollection?.examinedFaceDocs).includes(imgDoc[DocData])) { // only examine Docs that have an image and that haven't already been examined. - Doc.AddDocToList(Doc.MyFaceCollection, 'examinedFaceDocs', imgDoc[DocData]); + Doc.MyFaceCollection && Doc.AddDocToList(Doc.MyFaceCollection, 'examinedFaceDocs', imgDoc[DocData]); FaceRecognitionHandler.loadImage(imgUrl).then( // load image and analyze faces img => faceapi diff --git a/src/client/views/smartdraw/DrawingFillHandler.tsx b/src/client/views/smartdraw/DrawingFillHandler.tsx index b0945fd83..2c69284db 100644 --- a/src/client/views/smartdraw/DrawingFillHandler.tsx +++ b/src/client/views/smartdraw/DrawingFillHandler.tsx @@ -12,6 +12,9 @@ import { AspectRatioLimits, FireflyDimensionsMap, FireflyImageDimensions, Firefl const DashDropboxId = '2m86iveqdr9vzsa'; export class DrawingFillHandler { + static authorizeDropbox = () => { + window.open(`https://www.dropbox.com/oauth2/authorize?client_id=${DashDropboxId}&response_type=code&token_access_type=offline&redirect_uri=http://localhost:1050/refreshDropbox`, '_blank')?.focus(); + }; static drawingToImage = async (drawing: Doc, strength: number, user_prompt: string, styleDoc?: Doc) => { const tags = StrListCast(drawing.$tags).map(tag => tag.slice(1)); const styles = tags.filter(tag => FireflyStylePresets.has(tag)); @@ -46,8 +49,7 @@ export class DrawingFillHandler { .then(res => { const error = ('error' in res && (res.error as string)) || ''; if (error.includes('Dropbox') && confirm('Create image failed. Try authorizing DropBox?\r\n' + error.replace(/^[^"]*/, ''))) { - window.open(`https://www.dropbox.com/oauth2/authorize?client_id=${DashDropboxId}&response_type=code&token_access_type=offline&redirect_uri=http://localhost:1050/refreshDropbox`, '_blank')?.focus(); - return; + return DrawingFillHandler.authorizeDropbox(); } const genratedDocs = DocCast(drawing.ai_firefly_generatedDocs) ?? Docs.Create.MasonryDocument([], { title: StrCast(drawing.title) + ' AI Images', _width: 400, _height: 400 }); drawing.$ai_firefly_generatedDocs = genratedDocs; diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx index 2283ef965..4f0cd3978 100644 --- a/src/client/views/smartdraw/SmartDrawHandler.tsx +++ b/src/client/views/smartdraw/SmartDrawHandler.tsx @@ -576,6 +576,7 @@ export class SmartDrawHandler extends ObservableReactComponent<object> { type="text" autoFocus value={this._userInput} + onPointerDown={e => e.stopPropagation()} onChange={action(e => this._canInteract && (this._userInput = e.target.value))} placeholder="Enter item to draw" onKeyDown={this.handleKeyPress} diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index abce7ed26..ba94f0504 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -231,7 +231,7 @@ export class Doc extends RefField { public static AddLink: (link: Doc, checkExists?: boolean) => void; public static DeleteLink: (link: Doc) => void; public static Links: (link: Doc | undefined) => Doc[]; - public static getOppositeAnchor: (linkDoc: Doc, anchor: Doc) => Doc | undefined; + public static getOppositeAnchor: (linkDoc: Doc | undefined, anchor: Doc | undefined) => Doc | undefined; // KeyValueBox SetField (defined there) public static SetField: (doc: Doc, key: string, value: string, forceOnDelegate?: boolean, setResult?: (value: FieldResult) => void) => boolean; // UserDoc "API" @@ -1192,6 +1192,26 @@ export namespace Doc { const dheight = NumCast((dataDoc || doc)[Doc.LayoutDataKey(doc) + '_nativeHeight'], useHeight ? NumCast(doc._height) : 0); return NumCast(doc._nativeHeight, nheight || dheight); } + + export function OutpaintingWidth(doc?: Doc, dataDoc?: Doc, useWidth?: boolean) { + return !doc ? 0 : NumCast(doc._outpaintingWidth, NumCast((dataDoc || doc)[Doc.LayoutDataKey(doc) + '_outpaintingWidth'], useWidth ? NumCast(doc._width) : 0)); + } + + export function OutpaintingHeight(doc?: Doc, dataDoc?: Doc, useHeight?: boolean) { + if (!doc) return 0; + const oheight = (Doc.OutpaintingWidth(doc, dataDoc, useHeight) / NumCast(doc._width)) * NumCast(doc._height); + const dheight = NumCast((dataDoc || doc)[Doc.LayoutDataKey(doc) + '_outpaintingHeight'], useHeight ? NumCast(doc._height) : 0); + return NumCast(doc._outpaintingHeight, oheight || dheight); + } + + export function SetOutpaintingWidth(doc: Doc, width: number | undefined, fieldKey?: string) { + doc[(fieldKey || Doc.LayoutDataKey(doc)) + '_outpaintingWidth'] = width; + } + + export function SetOutpaintingHeight(doc: Doc, height: number | undefined, fieldKey?: string) { + doc[(fieldKey || Doc.LayoutDataKey(doc)) + '_outpaintingHeight'] = height; + } + export function SetNativeWidth(doc: Doc, width: number | undefined, fieldKey?: string) { doc[(fieldKey || Doc.LayoutDataKey(doc)) + '_nativeWidth'] = width; } diff --git a/src/fields/Types.ts b/src/fields/Types.ts index e6755f828..ba2e9bb6f 100644 --- a/src/fields/Types.ts +++ b/src/fields/Types.ts @@ -1,5 +1,6 @@ import { DateField } from './DateField'; import { Doc, FieldType, FieldResult, Opt } from './Doc'; +import { InkField } from './InkField'; import { List } from './List'; import { ProxyField } from './Proxy'; import { RefField } from './RefField'; @@ -99,6 +100,7 @@ export function VideoCast (field: FieldResult, defaultVal: VideoField | null = n export function AudioCast (field: FieldResult, defaultVal: AudioField | null = null) { return Cast(field, AudioField, defaultVal); } // prettier-ignore export function PDFCast (field: FieldResult, defaultVal: PdfField | null = null) { return Cast(field, PdfField, defaultVal); } // prettier-ignore export function ImageCast (field: FieldResult, defaultVal: ImageField | null = null) { return Cast(field, ImageField, defaultVal); } // prettier-ignore +export function InkCast (field: FieldResult, defaultVal: InkField | null = null) { return Cast(field, InkField, defaultVal); } // prettier-ignore export function ImageCastToNameType(field: FieldResult, defaultVal: ImageField | null = null) { const href = ImageCast(field, defaultVal)?.url.href; diff --git a/src/fields/URLField.ts b/src/fields/URLField.ts index 3a83e7ca0..8dedb0be7 100644 --- a/src/fields/URLField.ts +++ b/src/fields/URLField.ts @@ -17,9 +17,7 @@ export abstract class URLField extends ObjectField { readonly url: URL; constructor(urlVal: string); - // eslint-disable-next-line @typescript-eslint/no-shadow constructor(urlVal: URL); - // eslint-disable-next-line @typescript-eslint/no-shadow constructor(urlVal: URL | string) { super(); this.url = @@ -50,6 +48,7 @@ export abstract class URLField extends ObjectField { } [Copy](): this { + // eslint-disable-next-line @typescript-eslint/no-explicit-any return new (this.constructor as any)(this.url); } } diff --git a/src/server/ApiManagers/FireflyManager.ts b/src/server/ApiManagers/FireflyManager.ts index d22142d7d..1b8a85a5c 100644 --- a/src/server/ApiManagers/FireflyManager.ts +++ b/src/server/ApiManagers/FireflyManager.ts @@ -6,6 +6,7 @@ import * as path from 'path'; import { DashUserModel } from '../authentication/DashUserModel'; import { DashUploadUtils } from '../DashUploadUtils'; import { _error, _invalid, _success, Method } from '../RouteManager'; +import { Upload } from '../SharedMediaTypes'; import { Directory, filesDirectory } from '../SocketData'; import ApiManager, { Registration } from './ApiManager'; @@ -85,25 +86,23 @@ export default class FireflyManager extends ApiManager { .then(link => resolve(link.result.link)) .catch(linkErr => reject(new Error('Failed to get temporary link: ' + linkErr.message))) ) - .catch(uploadErr => reject(new Error('Failed to upload file to Dropbox: ' + uploadErr.message))); + .catch(uploadErr => { + if (user?.dropboxRefresh) { + console.log('Attempting to refresh Dropbox token for user:', user.email); + this.refreshDropboxToken(user) + .then(token => { + if (!token) return reject(new Error('Failed to refresh Dropbox token.' + user.email)); - uploadToDropbox(dbx).catch(e => { - if (user?.dropboxRefresh) { - console.log('Attempting to refresh Dropbox token for user:', user.email); - this.refreshDropboxToken(user) - .then(token => { - if (!token) { - return reject(new Error('Failed to refresh Dropbox token.' + user.email)); - } + const dbxNew = new Dropbox({ accessToken: token }); + uploadToDropbox(dbxNew).catch(finalErr => reject(new Error('Failed to refresh Dropbox token:' + finalErr.message))); + }) + .catch(refreshErr => reject(new Error('Failed to refresh Dropbox token: ' + refreshErr.message))); + } else { + reject(new Error('Dropbox error: ' + uploadErr.message)); + } + }); - const dbxNew = new Dropbox({ accessToken: token }); - uploadToDropbox(dbxNew).catch(finalErr => reject(new Error('Failed to refresh Dropbox token:' + finalErr.message))); - }) - .catch(refreshErr => reject(new Error('Failed to refresh Dropbox token: ' + refreshErr.message))); - } else { - reject(new Error('Dropbox error: ' + e.message)); - } - }); + uploadToDropbox(dbx); }); }); @@ -313,6 +312,85 @@ export default class FireflyManager extends ApiManager { }) ), // prettier-ignore }); + + register({ + method: Method.POST, + subscription: '/outpaintImage', + secureHandler: ({ req, res }) => + new Promise<void>(resolver => + this.uploadImageToDropbox(req.body.imageUrl, req.user as DashUserModel) + .then(uploadUrl => + this.getBearerToken() + .then(tokenResponse => tokenResponse?.json()) + .then((tokenData: { access_token: string }) => + fetch('https://firefly-api.adobe.io/v3/images/expand', { + method: 'POST', + headers: [ + ['Content-Type', 'application/json'], + ['Accept', 'application/json'], + ['x-api-key', process.env._CLIENT_FIREFLY_CLIENT_ID ?? ''], + ['Authorization', `Bearer ${tokenData.access_token}`], + ], + body: JSON.stringify({ + image: { + source: { url: uploadUrl }, + }, + size: { + width: Math.round(req.body.newDimensions.width), + height: Math.round(req.body.newDimensions.height), + }, + prompt: req.body.prompt ?? '', + numVariations: 1, + placement: { + inset: { + left: Math.round((req.body.newDimensions.width - req.body.originalDimensions.width) / 2), + top: Math.round((req.body.newDimensions.height - req.body.originalDimensions.height) / 2), + right: Math.round((req.body.newDimensions.width - req.body.originalDimensions.width) / 2), + bottom: Math.round((req.body.newDimensions.height - req.body.originalDimensions.height) / 2), + }, + alignment: { + horizontal: 'center', + vertical: 'center', + }, + }, + }), + }) + .then(expandResp => expandResp?.json()) + .then(expandData => { + if (expandData.error_code || !expandData.outputs?.[0]?.image?.url) { + console.error('Firefly validation error:', expandData); + _error(res, expandData.message ?? 'Failed to generate image'); + } else { + return DashUploadUtils.UploadImage(expandData.outputs[0].image.url) + .then((info: Upload.ImageInformation | Error) => { + if (info instanceof Error) { + _invalid(res, info.message); + } else { + _success(res, { url: info.accessPaths.agnostic.client }); + } + }) + .catch(uploadErr => { + console.error('DashUpload Error:', uploadErr); + _error(res, 'Failed to upload generated image.'); + }); + } + }) + ) + ) + .catch(e => { + _invalid(res, e.message); + resolver(); + }) + ), + }); + + /* register({ + method: Method.POST + subscription: '/queryFireflyOutpaint', + secureHandler: ({req, res}) => + this.outpaintImage() + })*/ + register({ method: Method.POST, subscription: '/queryFireflyImage', @@ -339,23 +417,6 @@ export default class FireflyManager extends ApiManager { ) ), }); - register({ - method: Method.POST, - subscription: '/expandImage', - secureHandler: ({ req, res }) => - this.uploadImageToDropbox(req.body.file, req.user as DashUserModel) - .then(uploadUrl => - this.expandImage(uploadUrl, req.body.prompt).then(text => { - if (text.error_code) _error(res, text.message); - else - DashUploadUtils.UploadImage(text.outputs[0].image.url).then(info => { - if (info instanceof Error) _invalid(res, info.message); - else _success(res, info); - }); - }) - ) - .catch(e => _invalid(res, e.message)), - }); // construct this url and send user to it. It will allow them to authorize their dropbox account and will send the resulting token to our endpoint /refreshDropbox // https://www.dropbox.com/oauth2/authorize?client_id=DROPBOX_CLIENT_ID&response_type=code&token_access_type=offline&redirect_uri=http://localhost:1050/refreshDropbox |