From 6b070202843e1e2a11c09c4d29aee4ab8750f31f Mon Sep 17 00:00:00 2001
From: geireann <geireann.lindfield@gmail.com>
Date: Thu, 24 Oct 2024 12:01:21 -0400
Subject: fixed pinning of card and carousel views.

---
 src/client/views/collections/CollectionCarousel3DView.tsx | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

(limited to 'src/client/views/collections/CollectionCarousel3DView.tsx')

diff --git a/src/client/views/collections/CollectionCarousel3DView.tsx b/src/client/views/collections/CollectionCarousel3DView.tsx
index c080ba27e..b7ecf9a2f 100644
--- a/src/client/views/collections/CollectionCarousel3DView.tsx
+++ b/src/client/views/collections/CollectionCarousel3DView.tsx
@@ -12,7 +12,7 @@ import { DocumentType } from '../../documents/DocumentTypes';
 import { Docs } from '../../documents/Documents';
 import { DragManager } from '../../util/DragManager';
 import { Transform } from '../../util/Transform';
-import { PinDocView } from '../PinFuncs';
+import { PinDocView, PinProps } from '../PinFuncs';
 import { StyleProp } from '../StyleProp';
 import { DocumentView } from '../nodes/DocumentView';
 import { FocusViewOptions } from '../nodes/FocusViewOptions';
@@ -107,9 +107,9 @@ export class CollectionCarousel3DView extends CollectionSubView() {
         }
         return undefined;
     };
-    getAnchor = (addAsAnnotation: boolean) => {
+    getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
         const anchor = Docs.Create.ConfigDocument({ annotationOn: this.Document, config_carousel_index: this.layoutDoc._carousel_index as number });
-        PinDocView(anchor, { pinData: { collectionType: true, filters: true } }, this.Document);
+        PinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { collectionType: true, filters: true } }, this.Document);
         addAsAnnotation && Doc.AddDocToList(this.dataDoc, this.fieldKey + '_annotations', anchor); // when added as an annotation, links to anchors can be found as links to the document even if the anchors are not rendered
         return anchor;
     };
-- 
cgit v1.2.3-70-g09d2


From 205fdacf9807be2691e13e1d9d04d6b98620391a Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 15 Jan 2025 13:03:59 -0500
Subject: fixed so that users are sentt to dropbox to authorized uploads once,
 and then a refresh token is used afterwards.  fixed up some collection views
 to work better with the tagsView.  fixed grid view to show entire image. 
 tweaked notetaking view to work better with tags, but it's incomplete.

---
 .../views/collections/CollectionCarousel3DView.tsx |  1 +
 .../views/collections/CollectionNoteTakingView.tsx | 11 +--
 .../views/collections/CollectionStackingView.tsx   | 13 ++-
 .../collectionGrid/CollectionGridView.tsx          | 12 +--
 src/client/views/smartdraw/DrawingFillHandler.tsx  | 28 +++++--
 src/server/ApiManagers/FireflyManager.ts           | 94 ++++++++++++++++++++--
 src/server/authentication/DashUserModel.ts         |  5 ++
 7 files changed, 141 insertions(+), 23 deletions(-)

(limited to 'src/client/views/collections/CollectionCarousel3DView.tsx')

diff --git a/src/client/views/collections/CollectionCarousel3DView.tsx b/src/client/views/collections/CollectionCarousel3DView.tsx
index b7ecf9a2f..9c8ef5519 100644
--- a/src/client/views/collections/CollectionCarousel3DView.tsx
+++ b/src/client/views/collections/CollectionCarousel3DView.tsx
@@ -136,6 +136,7 @@ export class CollectionCarousel3DView extends CollectionSubView() {
                 isDocumentActive={this._props.childDocumentsActive?.() || this.Document._childDocumentsActive ? this._props.isDocumentActive : this.isContentActive}
                 PanelWidth={this.panelWidth}
                 PanelHeight={this.panelHeight}
+                showTags={BoolCast(this.layoutDoc.showChildTags) || BoolCast(this.Document._layout_showTags)}
             />
         );
 
diff --git a/src/client/views/collections/CollectionNoteTakingView.tsx b/src/client/views/collections/CollectionNoteTakingView.tsx
index ac8e37358..323a679cc 100644
--- a/src/client/views/collections/CollectionNoteTakingView.tsx
+++ b/src/client/views/collections/CollectionNoteTakingView.tsx
@@ -2,7 +2,7 @@ import { action, computed, IReactionDisposer, makeObservable, observable, reacti
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { ClientUtils, DivHeight, lightOrDark, returnZero, smoothScroll } from '../../../ClientUtils';
-import { Doc, Field, FieldType, Opt } from '../../../fields/Doc';
+import { Doc, Field, FieldType, Opt, StrListCast } from '../../../fields/Doc';
 import { DocData } from '../../../fields/DocSymbols';
 import { Copy, Id } from '../../../fields/FieldSymbols';
 import { List } from '../../../fields/List';
@@ -48,6 +48,7 @@ export class CollectionNoteTakingView extends CollectionSubView() {
     @computed get notetakingCategoryField() {
         return StrCast(this.dataDoc.notetaking_column, StrCast(this.layoutDoc.pivotField, 'notetaking_column'));
     }
+    toHeader = (d: Doc) => (d[this.notetakingCategoryField] instanceof List ? StrListCast(d[this.notetakingCategoryField]).join('.') : (d[this.notetakingCategoryField] ?? 'unset'));
     public DividerWidth = 16;
     @observable docsDraggedRowCol: number[] = [];
     @observable _scroll = 0;
@@ -136,7 +137,7 @@ export class CollectionNoteTakingView extends CollectionSubView() {
         const rowCol = this.docsDraggedRowCol;
         // this will sort the docs into the correct columns (minus the ones you're currently dragging)
         docs.forEach(d => {
-            const sectionValue = (d[this.notetakingCategoryField] as object) ?? `unset`;
+            const sectionValue = this.toHeader(d);
             // look for if header exists already
             const existingHeader = columnHeaders.find(sh => sh.heading === sectionValue.toString());
             if (existingHeader) {
@@ -161,7 +162,7 @@ export class CollectionNoteTakingView extends CollectionSubView() {
     };
 
     @computed get allFieldValues() {
-        return new Set(this.childDocs.map(doc => StrCast(doc[this.notetakingCategoryField])));
+        return new Set(this.childDocs.map(doc => (doc[this.notetakingCategoryField] instanceof List ? StrListCast(doc[this.notetakingCategoryField]).join('.') : StrCast(doc[this.notetakingCategoryField]))));
     }
 
     componentDidMount() {
@@ -313,7 +314,7 @@ export class CollectionNoteTakingView extends CollectionSubView() {
     // how to get the width of a document. Currently returns the width of the column (minus margins)
     // if a note doc. Otherwise, returns the normal width (for graphs, images, etc...)
     getDocWidth = (d: Doc) => {
-        const heading = !d[this.notetakingCategoryField] ? 'unset' : Field.toString(d[this.notetakingCategoryField] as FieldType);
+        const heading = this.toHeader(d);
         const existingHeader = this.colHeaderData.find(sh => sh.heading === heading);
         const existingWidth = this.layoutDoc._notetaking_columns_autoSize ? 1 / (this.colHeaderData.length ?? 1) : existingHeader?.width ? existingHeader.width : 0;
         const maxWidth = existingWidth > 0 ? existingWidth * this.availableWidth : this.maxColWidth;
@@ -427,7 +428,7 @@ export class CollectionNoteTakingView extends CollectionSubView() {
         const colHeader = colIndex === undefined ? 'unset' : StrCast(this.colHeaderData[colIndex].heading);
         this.childDocs?.forEach(d => {
             if (d instanceof Promise) return;
-            const sectionValue = (d[this.notetakingCategoryField] as object) ?? 'unset';
+            const sectionValue = this.toHeader(d);
             if (sectionValue.toString() === colHeader) {
                 docsMatchingHeader.push(d);
             }
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 1ac0b6d70..a57256424 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -146,7 +146,17 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
             // assuming we need to get rowSpan because we might be dealing with many columns. Grid gap makes sense if multiple columns
             const rowSpan = Math.ceil((this.getDocHeight(d)() + this.gridGap) / this.gridGap);
             // just getting the style
-            const style = this.isStackingView ? { margin: undefined, transition: this.getDocTransition(d)(), width: this.columnWidth, marginTop: i ? this.gridGap : 0, height: this.getDocHeight(d)() } : { gridRowEnd: `span ${rowSpan}` };
+            const style = this.isStackingView
+                ? {
+                      //
+                      margin: undefined,
+                      transition: this.getDocTransition(d)(),
+                      width: this.columnWidth,
+                      marginTop: i ? this.gridGap : 0,
+                      height: this.getDocHeight(d)(),
+                      zIndex: DocumentView.getFirstDocumentView(d)?.IsSelected ? 1000 : 0,
+                  }
+                : { gridRowEnd: `span ${rowSpan}`, zIndex: DocumentView.getFirstDocumentView(d)?.IsSelected ? 1000 : 0 };
             // So we're choosing whether we're going to render a column or a masonry doc
             return (
                 <div className={`collectionStackingView-${this.isStackingView ? 'columnDoc' : 'masonryDoc'}`} key={d[Id]} style={style}>
@@ -344,6 +354,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
                 dontRegisterView={BoolCast(this.layoutDoc.childDontRegisterViews, this._props.dontRegisterView)} // used to be true if DataDoc existed, but template textboxes won't layout_autoHeight resize if dontRegisterView is set, but they need to.
                 rootSelected={this.rootSelected}
                 showTitle={this._props.childlayout_showTitle}
+                showTags={BoolCast(this.layoutDoc.showChildTags) || BoolCast(this.Document._layout_showTags)}
                 dragAction={(this.layoutDoc.childDragAction ?? this._props.childDragAction) as dropActionType}
                 onClickScript={this.onChildClickHandler}
                 onDoubleClickScript={this.onChildDoubleClickHandler}
diff --git a/src/client/views/collections/collectionGrid/CollectionGridView.tsx b/src/client/views/collections/collectionGrid/CollectionGridView.tsx
index 5c41fee37..6dffb80f1 100644
--- a/src/client/views/collections/collectionGrid/CollectionGridView.tsx
+++ b/src/client/views/collections/collectionGrid/CollectionGridView.tsx
@@ -186,21 +186,23 @@ export class CollectionGridView extends CollectionSubView() {
     getDisplayDoc(layout: Doc, dxf: () => Transform, width: () => number, height: () => number) {
         return (
             <DocumentView
-                // eslint-disable-next-line react/jsx-props-no-spreading
                 {...this._props}
-                NativeWidth={returnZero}
-                NativeHeight={returnZero}
-                setContentViewBox={emptyFunction}
                 Document={layout}
                 TemplateDataDocument={layout.resolvedDataDoc as Doc}
+                NativeWidth={returnZero}
+                NativeHeight={returnZero}
+                fitWidth={this._props.childLayoutFitWidth}
+                containerViewPath={this.childContainerViewPath}
+                renderDepth={this._props.renderDepth + 1}
                 isContentActive={this.isChildContentActive}
                 PanelWidth={width}
                 PanelHeight={height}
                 ScreenToLocalTransform={dxf}
+                setContentViewBox={emptyFunction}
                 whenChildContentsActiveChanged={this._props.whenChildContentsActiveChanged}
                 onClickScript={this.onChildClickHandler}
-                renderDepth={this._props.renderDepth + 1}
                 dontCenter={StrCast(this.layoutDoc.layout_dontCenter) as 'x' | 'y' | 'xy'}
+                showTags={BoolCast(this.layoutDoc.showChildTags) || BoolCast(this.Document._layout_showTags)}
             />
         );
     }
diff --git a/src/client/views/smartdraw/DrawingFillHandler.tsx b/src/client/views/smartdraw/DrawingFillHandler.tsx
index d9a01c436..bd8499806 100644
--- a/src/client/views/smartdraw/DrawingFillHandler.tsx
+++ b/src/client/views/smartdraw/DrawingFillHandler.tsx
@@ -10,6 +10,7 @@ import { DocumentView, DocumentViewInternal } from '../nodes/DocumentView';
 import { OpenWhere } from '../nodes/OpenWhere';
 import { AspectRatioLimits, FireflyDimensionsMap, FireflyImageDimensions, FireflyStylePresets } from './FireflyConstants';
 
+const DashDropboxId = '2m86iveqdr9vzsa';
 export class DrawingFillHandler {
     static drawingToImage = (drawing: Doc, strength: number, user_prompt: string) => {
         const docData = drawing[DocData];
@@ -33,14 +34,27 @@ export class DrawingFillHandler {
                 const structureUrl = `${hrefParts.slice(0, -1).join('.')}_o.${hrefParts.lastElement()}`;
                 return imageUrlToBase64(structureUrl)
                     .then(gptDescribeImage)
-                    .then(prompt => 
-                        Networking.PostToServer('/queryFireflyImageFromStructure', 
-                            { prompt: `${user_prompt || prompt}`, width: dims.width, height: dims.height, structureUrl, strength, styles })
+                    .then(prompt =>
+                        Networking.PostToServer('/queryFireflyImageFromStructure', { prompt: `${user_prompt || prompt}`, width: dims.width, height: dims.height, structureUrl, strength, styles })
                             .then((info: Upload.ImageInformation) =>
-                                DocumentViewInternal.addDocTabFunc(Docs.Create.ImageDocument(info.accessPaths.agnostic.client, 
-                                    { ai: 'firefly', title: user_prompt || prompt, ai_firefly_prompt: user_prompt || prompt, _width: 500, data_nativeWidth: info.nativeWidth, data_nativeHeight: info.nativeHeight }), OpenWhere.addRight)
-                        ).catch(e => alert("create image failed: " + e.toString())) 
-                    ); // prettier-ignore
+                                DocumentViewInternal.addDocTabFunc(
+                                    Docs.Create.ImageDocument(info.accessPaths.agnostic.client, {
+                                        ai: 'firefly',
+                                        title: user_prompt || prompt,
+                                        ai_firefly_prompt: user_prompt || prompt,
+                                        _width: 500,
+                                        data_nativeWidth: info.nativeWidth,
+                                        data_nativeHeight: info.nativeHeight,
+                                    }),
+                                    OpenWhere.addRight
+                                )
+                            )
+                            .catch(e => {
+                                if (e.toString().includes('Dropbox') && confirm('Create image failed. Try authorizing DropBox?\r\n' + e.toString().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();
+                                }
+                            })
+                    ); // prettier-ignore:q
             }
         });
     };
diff --git a/src/server/ApiManagers/FireflyManager.ts b/src/server/ApiManagers/FireflyManager.ts
index a1f8fab8d..6039f5675 100644
--- a/src/server/ApiManagers/FireflyManager.ts
+++ b/src/server/ApiManagers/FireflyManager.ts
@@ -1,7 +1,9 @@
+import axios from 'axios';
 import { Dropbox } from 'dropbox';
 import * as fs from 'fs';
 import * as multipart from 'parse-multipart-data';
 import * as path from 'path';
+import { DashUserModel } from '../authentication/DashUserModel';
 import { DashUploadUtils } from '../DashUploadUtils';
 import { _error, _invalid, _success, Method } from '../RouteManager';
 import { Directory, filesDirectory } from '../SocketData';
@@ -55,7 +57,7 @@ export default class FireflyManager extends ApiManager {
             )
         );
 
-    uploadImageToDropbox = (fileUrl: string, dbx = new Dropbox({ accessToken: process.env.DROPBOX_TOKEN })) =>
+    uploadImageToDropbox = (fileUrl: string, user: DashUserModel | undefined, dbx = new Dropbox({ accessToken: user?.dropboxToken || '' })) =>
         new Promise<string | Error>((res, rej) =>
             fs.readFile(path.join(filesDirectory, `${Directory.images}/${path.basename(fileUrl)}`), undefined, (err, contents) => {
                 if (err) {
@@ -69,8 +71,32 @@ export default class FireflyManager extends ApiManager {
                                 .catch(e => res(new Error(e.toString())));
                         })
                         .catch(e => {
-                            console.log('Dropbox error:', e);
-                            res(new Error(e.toString()));
+                            if (user?.dropboxRefresh) {
+                                console.log('*********** try refresh dropbox for: ' + user.email + ' ***********');
+                                this.refreshDropboxToken(user).then(token => {
+                                    if (!token) {
+                                        console.log('Dropbox error: cannot refresh token');
+                                        res(new Error(e.toString()));
+                                    } else {
+                                        const dbxNew = new Dropbox({ accessToken: user.dropboxToken || '' });
+                                        dbxNew
+                                            .filesUpload({ path: `/Apps/browndash/${path.basename(fileUrl)}`, contents })
+                                            .then(response => {
+                                                dbxNew
+                                                    .filesGetTemporaryLink({ path: response.result.path_display ?? '' })
+                                                    .then(link => res(link.result.link))
+                                                    .catch(linkErr => res(new Error(linkErr.toString())));
+                                            })
+                                            .catch(uploadErr => {
+                                                console.log('Dropbox error:', uploadErr);
+                                                res(new Error(uploadErr.toString()));
+                                            });
+                                    }
+                                });
+                            } else {
+                                console.log('Dropbox error:', e);
+                                res(new Error(e.toString()));
+                            }
                         });
                 }
             })
@@ -230,12 +256,35 @@ export default class FireflyManager extends ApiManager {
         );
         return fetched;
     };
+
+    refreshDropboxToken = (user: DashUserModel) =>
+        axios
+            .post(
+                'https://api.dropbox.com/oauth2/token',
+                new URLSearchParams({
+                    refresh_token: user.dropboxRefresh || '',
+                    grant_type: 'refresh_token',
+                    client_id: process.env._CLIENT_DROPBOX_CLIENT_ID || '',
+                    client_secret: process.env._CLIENT_DROPBOX_SECRET || '',
+                }).toString()
+            )
+            .then(refresh => {
+                console.log('***** dropbox token refreshed for ' + user?.email + ' ******* ');
+                user.dropboxToken = refresh.data.access_token;
+                user.save();
+                return user.dropboxToken;
+            })
+            .catch(e => {
+                console.log(e);
+                return undefined;
+            });
+
     protected initialize(register: Registration): void {
         register({
             method: Method.POST,
             subscription: '/queryFireflyImageFromStructure',
             secureHandler: async ({ req, res }) =>
-                this.uploadImageToDropbox(req.body.structureUrl).then(uploadUrl =>
+                this.uploadImageToDropbox(req.body.structureUrl, req.user as DashUserModel).then(uploadUrl =>
                     uploadUrl instanceof Error
                         ? _invalid(res, uploadUrl.message)
                         : this.generateImageFromStructure(req.body.prompt, req.body.width, req.body.height, uploadUrl, req.body.strength, req.body.styles).then(fire =>
@@ -275,7 +324,7 @@ export default class FireflyManager extends ApiManager {
             method: Method.POST,
             subscription: '/expandImage',
             secureHandler: ({ req, res }) =>
-                this.uploadImageToDropbox(req.body.file).then(uploadUrl =>
+                this.uploadImageToDropbox(req.body.file, req.user as DashUserModel).then(uploadUrl =>
                     uploadUrl instanceof Error
                         ? _invalid(res, uploadUrl.message)
                         : this.expandImage(uploadUrl, req.body.prompt).then(text => {
@@ -288,5 +337,40 @@ export default class FireflyManager extends ApiManager {
                           })
                 ),
         });
+
+        // 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
+        // see: https://dropbox.tech/developers/using-oauth-2-0-with-offline-access
+        //
+        register({
+            method: Method.GET,
+            subscription: '/refreshDropbox',
+            secureHandler: ({ req, res }) => {
+                const user = req.user as DashUserModel;
+                console.log(`******************* dropbox authorized for ${user?.email} ******************`);
+                _success(res, 'dropbox authorized for ' + user?.email);
+
+                const data = new URLSearchParams({
+                    code: req.query.code as string,
+                    grant_type: 'authorization_code',
+                    client_id: process.env._CLIENT_DROPBOX_CLIENT_ID ?? '',
+                    client_secret: process.env._CLIENT_DROPBOX_SECRET ?? '',
+                    redirect_uri: 'http://localhost:1050/refreshDropbox',
+                });
+                axios
+                    .post('https://api.dropbox.com/oauth2/token', data.toString())
+                    .then(response => {
+                        console.log('***** dropbox token (and refresh) received for ' + user?.email + ' ******* ');
+                        user.dropboxToken = response.data.access_token;
+                        user.dropboxRefresh = response.data.refresh_token;
+                        user.save();
+
+                        setTimeout(() => this.refreshDropboxToken(user), response.data.expires_in - 600);
+                    })
+                    .catch(e => {
+                        console.log(e);
+                    });
+            },
+        });
     }
 }
diff --git a/src/server/authentication/DashUserModel.ts b/src/server/authentication/DashUserModel.ts
index bfa6d7bdb..debeef60c 100644
--- a/src/server/authentication/DashUserModel.ts
+++ b/src/server/authentication/DashUserModel.ts
@@ -9,6 +9,9 @@ export type DashUserModel = mongoose.Document & {
     passwordResetToken?: string;
     passwordResetExpires?: Date;
 
+    dropboxRefresh?: string;
+    dropboxToken?: string;
+
     userDocumentId: string;
     sharingDocumentId: string;
     linkDatabaseId: string;
@@ -37,6 +40,8 @@ const userSchema = new mongoose.Schema(
         passwordResetToken: String,
         passwordResetExpires: Date,
 
+        dropboxRefresh: String,
+        dropboxToken: String,
         userDocumentId: String, // id that identifies a document which hosts all of a user's account data
         sharingDocumentId: String, // id that identifies a document that stores documents shared to a user, their user color, and any additional info needed to communicate between users
         linkDatabaseId: String,
-- 
cgit v1.2.3-70-g09d2

