aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorZachary Zhang <zacharyzhang7@gmail.com>2024-05-12 21:50:44 -0400
committerZachary Zhang <zacharyzhang7@gmail.com>2024-05-12 21:50:44 -0400
commit4738bb6d8a9098a49acab771830d0f2029a62de1 (patch)
tree261ae0b2fe5ba7b806608ada1c44f92a7d3f8698 /src
parentbd82ad3ebefebd4e8354007568ca27f3e2f13a9b (diff)
parent2caf7b7bb80b663b6ba585f88cdbd2d725f8505e (diff)
Merge branch 'master' into zach-starter
Diffstat (limited to 'src')
-rw-r--r--src/Utils.ts24
-rw-r--r--src/client/DocServer.ts63
-rw-r--r--src/client/apis/gpt/GPT.ts19
-rw-r--r--src/client/documents/Documents.ts2
-rw-r--r--src/client/util/CurrentUserUtils.ts9
-rw-r--r--src/client/util/DragManager.ts2
-rw-r--r--src/client/views/DocumentDecorations.tsx9
-rw-r--r--src/client/views/InkingStroke.tsx2
-rw-r--r--src/client/views/MainView.tsx6
-rw-r--r--src/client/views/PropertiesView.scss5
-rw-r--r--src/client/views/StyleProvider.scss1
-rw-r--r--src/client/views/collections/CollectionCarousel3DView.tsx3
-rw-r--r--src/client/views/collections/CollectionNoteTakingView.scss47
-rw-r--r--src/client/views/collections/CollectionNoteTakingView.tsx97
-rw-r--r--src/client/views/collections/CollectionNoteTakingViewColumn.tsx45
-rw-r--r--src/client/views/collections/CollectionStackingView.tsx2
-rw-r--r--src/client/views/collections/CollectionStackingViewFieldColumn.tsx21
-rw-r--r--src/client/views/collections/CollectionSubView.tsx19
-rw-r--r--src/client/views/collections/CollectionTreeView.tsx14
-rw-r--r--src/client/views/collections/TreeView.tsx8
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormBackgroundGrid.tsx10
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx43
-rw-r--r--src/client/views/collections/collectionSchema/CollectionSchemaView.tsx2
-rw-r--r--src/client/views/nodes/DataVizBox/DataVizBox.scss4
-rw-r--r--src/client/views/nodes/DataVizBox/DataVizBox.tsx35
-rw-r--r--src/client/views/nodes/DataVizBox/components/Chart.scss63
-rw-r--r--src/client/views/nodes/DataVizBox/components/TableBox.tsx224
-rw-r--r--src/client/views/nodes/DocumentView.tsx1
-rw-r--r--src/client/views/nodes/ImageBox.tsx55
-rw-r--r--src/client/views/nodes/LinkAnchorBox.tsx2
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.scss1
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx20
-rw-r--r--src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts65
-rw-r--r--src/client/views/nodes/formattedText/RichTextMenu.tsx40
-rw-r--r--src/client/views/nodes/generativeFill/GenerativeFill.tsx6
-rw-r--r--src/client/views/nodes/trails/PresBox.tsx7
-rw-r--r--src/client/views/nodes/trails/PresElementBox.tsx2
-rw-r--r--src/client/views/pdf/AnchorMenu.tsx15
-rw-r--r--src/client/views/pdf/GPTPopup/GPTPopup.tsx152
-rw-r--r--src/client/views/pdf/PDFViewer.tsx2
-rw-r--r--src/mobile/ImageUpload.tsx2
-rw-r--r--src/server/authentication/AuthenticationManager.ts24
-rw-r--r--src/server/server_Initialization.ts2
-rw-r--r--src/server/websocket.ts22
-rw-r--r--src/typings/connect-flash/index.d.ts1
-rw-r--r--src/typings/connect-mongo/index.d.ts1
-rw-r--r--src/typings/express-flash/index.d.ts1
-rw-r--r--src/typings/image-data-uri/index.d.ts3
-rw-r--r--src/typings/index.d.ts5
-rw-r--r--src/typings/jpeg-autorotate/index.d.ts3
50 files changed, 831 insertions, 380 deletions
diff --git a/src/Utils.ts b/src/Utils.ts
index a64c7c8a7..291d7c799 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -1,13 +1,12 @@
-import { ColorResult } from 'react-color';
-import * as uuid from 'uuid';
-//import { Socket } from '../node_modules/socket.io-client';
import * as Color from 'color';
+import { ColorResult } from 'react-color';
import * as rp from 'request-promise';
-import { Socket } from '../node_modules/socket.io/dist/index';
+import { Socket } from 'socket.io';
+import * as uuid from 'uuid';
import { DocumentType } from './client/documents/DocumentTypes';
import { Colors } from './client/views/global/globalEnums';
-import { Message } from './server/Message';
import { DocumentView } from './client/views/nodes/DocumentView';
+import { Message } from './server/Message';
export namespace Utils {
export let CLICK_TIME = 300;
@@ -383,7 +382,7 @@ export namespace Utils {
export const loggingEnabled: Boolean = false;
export const logFilter: number | undefined = undefined;
- function log(prefix: string, messageName: string, message: any, receiving: boolean) {
+ export function log(prefix: string, messageName: string, message: any, receiving: boolean) {
if (!loggingEnabled) {
return;
}
@@ -396,7 +395,7 @@ export namespace Utils {
console.log(`${prefix}: ${idString}, ${receiving ? 'receiving' : 'sending'} ${messageName} with data ${JSON.stringify(message)} `);
}
- function loggingCallback(prefix: string, func: (args: any) => any, messageName: string) {
+ export function loggingCallback(prefix: string, func: (args: any) => any, messageName: string) {
return (args: any) => {
log(prefix, messageName, args, true);
func(args);
@@ -408,17 +407,6 @@ export namespace Utils {
socket.emit(message.Message, args);
}
- export function EmitCallback<T>(socket: Socket, message: Message<T>, args: T): Promise<any>;
- export function EmitCallback<T>(socket: Socket, message: Message<T>, args: T, fn: (args: any) => any): void;
- export function EmitCallback<T>(socket: Socket, message: Message<T>, args: T, fn?: (args: any) => any): void | Promise<any> {
- log('Emit', message.Name, args, false);
- if (fn) {
- socket.emit(message.Message, args, loggingCallback('Receiving', fn, message.Name));
- } else {
- return new Promise<any>(res => socket.emit(message.Message, args, loggingCallback('Receiving', res, message.Name)));
- }
- }
-
export function AddServerHandler<T>(socket: Socket, message: Message<T>, handler: (args: T) => any) {
socket.on(message.Message, loggingCallback('Incoming', handler, message.Name));
}
diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts
index bd60a205c..321572071 100644
--- a/src/client/DocServer.ts
+++ b/src/client/DocServer.ts
@@ -7,10 +7,10 @@ import { HandleUpdate, Id, Parent } from '../fields/FieldSymbols';
import { ObjectField } from '../fields/ObjectField';
import { RefField } from '../fields/RefField';
import { DocCast, StrCast } from '../fields/Types';
-import { Socket } from '../../node_modules/socket.io/dist/index';
//import MobileInkOverlay from '../mobile/MobileInkOverlay';
+import { io, Socket } from 'socket.io-client';
import { emptyFunction, Utils } from '../Utils';
-import { GestureContent, MessageStore, MobileDocumentUploadContent, MobileInkOverlayContent, UpdateMobileInkOverlayPositionContent, YoutubeQueryTypes } from './../server/Message';
+import { GestureContent, Message, MessageStore, MobileDocumentUploadContent, MobileInkOverlayContent, UpdateMobileInkOverlayPositionContent, YoutubeQueryTypes } from './../server/Message';
import { DocumentType } from './documents/DocumentTypes';
import { LinkManager } from './util/LinkManager';
import { SerializationHelper } from './util/SerializationHelper';
@@ -32,6 +32,24 @@ import { SerializationHelper } from './util/SerializationHelper';
export namespace DocServer {
let _cache: { [id: string]: RefField | Promise<Opt<RefField>> } = {};
+ export function AddServerHandler<T>(socket: Socket, message: Message<T>, handler: (args: T) => any) {
+ socket.on(message.Message, Utils.loggingCallback('Incoming', handler, message.Name));
+ }
+ export function Emit<T>(socket: Socket, message: Message<T>, args: T) {
+ //log('Emit', message.Name, args, false);
+ socket.emit(message.Message, args);
+ }
+ export function EmitCallback<T>(socket: Socket, message: Message<T>, args: T): Promise<any>;
+ export function EmitCallback<T>(socket: Socket, message: Message<T>, args: T, fn: (args: any) => any): void;
+ export function EmitCallback<T>(socket: Socket, message: Message<T>, args: T, fn?: (args: any) => any): void | Promise<any> {
+ //log('Emit', message.Name, args, false);
+ if (fn) {
+ socket.emit(message.Message, args, Utils.loggingCallback('Receiving', fn, message.Name));
+ } else {
+ return new Promise<any>(res => socket.emit(message.Message, args, Utils.loggingCallback('Receiving', res, message.Name)));
+ }
+ }
+
export function FindDocByTitle(title: string) {
const foundDocId =
title &&
@@ -132,20 +150,20 @@ export namespace DocServer {
export namespace Mobile {
export function dispatchGesturePoints(content: GestureContent) {
- Utils.Emit(_socket, MessageStore.GesturePoints, content);
+ DocServer.Emit(_socket, MessageStore.GesturePoints, content);
}
export function dispatchOverlayTrigger(content: MobileInkOverlayContent) {
// _socket.emit("dispatchBoxTrigger");
- Utils.Emit(_socket, MessageStore.MobileInkOverlayTrigger, content);
+ DocServer.Emit(_socket, MessageStore.MobileInkOverlayTrigger, content);
}
export function dispatchOverlayPositionUpdate(content: UpdateMobileInkOverlayPositionContent) {
- Utils.Emit(_socket, MessageStore.UpdateMobileInkOverlayPosition, content);
+ DocServer.Emit(_socket, MessageStore.UpdateMobileInkOverlayPosition, content);
}
export function dispatchMobileDocumentUpload(content: MobileDocumentUploadContent) {
- Utils.Emit(_socket, MessageStore.MobileDocumentUpload, content);
+ DocServer.Emit(_socket, MessageStore.MobileDocumentUpload, content);
}
}
@@ -171,7 +189,8 @@ export namespace DocServer {
_cache = {};
USER_ID = identifier;
protocol = protocol.startsWith('https') ? 'wss' : 'ws';
- _socket = require('socket.io-client')(`${protocol}://${hostname}:${port}`, { transports: ['websocket'], rejectUnauthorized: false });
+ _socket = io(`${protocol}://${hostname}:${port}`, { transports: ['websocket'], rejectUnauthorized: false });
+ _socket.on("connect_error", (err:any) => console.log(err));
// io.connect(`https://7f079dda.ngrok.io`);// if using ngrok, create a special address for the websocket
_GetCachedRefField = _GetCachedRefFieldImpl;
@@ -184,11 +203,11 @@ export namespace DocServer {
* Whenever the server sends us its handshake message on our
* websocket, we use the above function to return the handshake.
*/
- Utils.AddServerHandler(_socket, MessageStore.Foo, onConnection);
- Utils.AddServerHandler(_socket, MessageStore.UpdateField, respondToUpdate);
- Utils.AddServerHandler(_socket, MessageStore.DeleteField, respondToDelete);
- Utils.AddServerHandler(_socket, MessageStore.DeleteFields, respondToDelete);
- Utils.AddServerHandler(_socket, MessageStore.ConnectionTerminated, alertUser);
+ DocServer.AddServerHandler(_socket, MessageStore.Foo, onConnection);
+ DocServer.AddServerHandler(_socket, MessageStore.UpdateField, respondToUpdate);
+ DocServer.AddServerHandler(_socket, MessageStore.DeleteField, respondToDelete);
+ DocServer.AddServerHandler(_socket, MessageStore.DeleteFields, respondToDelete);
+ DocServer.AddServerHandler(_socket, MessageStore.ConnectionTerminated, alertUser);
// // mobile ink overlay socket events to communicate between mobile view and desktop view
// _socket.addEventListener('receiveGesturePoints', (content: GestureContent) => {
@@ -252,7 +271,7 @@ export namespace DocServer {
* all documents in the database.
*/
export function deleteDatabase() {
- Utils.Emit(_socket, MessageStore.DeleteAll, {});
+ DocServer.Emit(_socket, MessageStore.DeleteAll, {});
}
}
@@ -275,7 +294,7 @@ export namespace DocServer {
// synchronously, we emit a single callback to the server requesting the serialized (i.e. represented by a string)
// field for the given ids. This returns a promise, which, when resolved, indicates the the JSON serialized version of
// the field has been returned from the server
- const getSerializedField = Utils.EmitCallback(_socket, MessageStore.GetRefField, id);
+ const getSerializedField = DocServer.EmitCallback(_socket, MessageStore.GetRefField, id);
// when the serialized RefField has been received, go head and begin deserializing it into an object.
// Here, once deserialized, we also invoke .proto to 'load' the document's prototype, which ensures that all
@@ -339,15 +358,15 @@ export namespace DocServer {
}
export async function getYoutubeChannels() {
- return await Utils.EmitCallback(_socket, MessageStore.YoutubeApiQuery, { type: YoutubeQueryTypes.Channels });
+ return await DocServer.EmitCallback(_socket, MessageStore.YoutubeApiQuery, { type: YoutubeQueryTypes.Channels });
}
export function getYoutubeVideos(videoTitle: string, callBack: (videos: any[]) => void) {
- Utils.EmitCallback(_socket, MessageStore.YoutubeApiQuery, { type: YoutubeQueryTypes.SearchVideo, userInput: videoTitle }, callBack);
+ DocServer.EmitCallback(_socket, MessageStore.YoutubeApiQuery, { type: YoutubeQueryTypes.SearchVideo, userInput: videoTitle }, callBack);
}
export function getYoutubeVideoDetails(videoIds: string, callBack: (videoDetails: any[]) => void) {
- Utils.EmitCallback(_socket, MessageStore.YoutubeApiQuery, { type: YoutubeQueryTypes.VideoDetails, videoIds: videoIds }, callBack);
+ DocServer.EmitCallback(_socket, MessageStore.YoutubeApiQuery, { type: YoutubeQueryTypes.VideoDetails, videoIds: videoIds }, callBack);
}
/**
@@ -397,7 +416,7 @@ export namespace DocServer {
// the fields have been returned from the server
console.log('Requesting ' + requestedIds.length);
setTimeout(() => runInAction(() => (FieldLoader.ServerLoadStatus.requested = requestedIds.length)));
- const serializedFields = await Utils.EmitCallback(_socket, MessageStore.GetRefFields, requestedIds);
+ const serializedFields = await DocServer.EmitCallback(_socket, MessageStore.GetRefFields, requestedIds);
// 3) when the serialized RefFields have been received, go head and begin deserializing them into objects.
// Here, once deserialized, we also invoke .proto to 'load' the documents' prototypes, which ensures that all
@@ -502,7 +521,7 @@ export namespace DocServer {
function _CreateFieldImpl(field: RefField) {
_cache[field[Id]] = field;
const initialState = SerializationHelper.Serialize(field);
- Doc.CurrentUserEmail !== 'guest' && Utils.Emit(_socket, MessageStore.CreateField, initialState);
+ Doc.CurrentUserEmail !== 'guest' && DocServer.Emit(_socket, MessageStore.CreateField, initialState);
}
let _CreateField: (field: RefField) => void = errorFunc;
@@ -522,7 +541,7 @@ export namespace DocServer {
}
function _UpdateFieldImpl(id: string, diff: any) {
- !DocServer.Control.isReadOnly() && Doc.CurrentUserEmail !== 'guest' && Utils.Emit(_socket, MessageStore.UpdateField, { id, diff });
+ !DocServer.Control.isReadOnly() && Doc.CurrentUserEmail !== 'guest' && DocServer.Emit(_socket, MessageStore.UpdateField, { id, diff });
}
let _UpdateField: (id: string, diff: any) => void = errorFunc;
@@ -559,11 +578,11 @@ export namespace DocServer {
}
export function DeleteDocument(id: string) {
- Doc.CurrentUserEmail !== 'guest' && Utils.Emit(_socket, MessageStore.DeleteField, id);
+ Doc.CurrentUserEmail !== 'guest' && DocServer.Emit(_socket, MessageStore.DeleteField, id);
}
export function DeleteDocuments(ids: string[]) {
- Doc.CurrentUserEmail !== 'guest' && Utils.Emit(_socket, MessageStore.DeleteFields, ids);
+ Doc.CurrentUserEmail !== 'guest' && DocServer.Emit(_socket, MessageStore.DeleteFields, ids);
}
function _respondToDeleteImpl(ids: string | string[]) {
diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts
index cde408382..3370f19fc 100644
--- a/src/client/apis/gpt/GPT.ts
+++ b/src/client/apis/gpt/GPT.ts
@@ -5,7 +5,8 @@ enum GPTCallType {
SUMMARY = 'summary',
COMPLETION = 'completion',
EDIT = 'edit',
- MERMAID='mermaid'
+ MERMAID='mermaid',
+ DATA = 'data',
}
type GPTCallOpts = {
@@ -15,11 +16,16 @@ type GPTCallOpts = {
prompt: string;
};
+/**
+ * Replace completions (deprecated) with chat
+ */
+
const callTypeMap: { [type: string]: GPTCallOpts } = {
summary: { model: 'gpt-3.5-turbo-instruct', maxTokens: 256, temp: 0.5, prompt: 'Summarize this text in simpler terms: ' },
edit: { model: 'gpt-3.5-turbo-instruct', maxTokens: 256, temp: 0.5, prompt: 'Reword this: ' },
completion: { model: 'gpt-3.5-turbo-instruct', maxTokens: 256, temp: 0.5, prompt: '' },
- mermaid:{model:'gpt-4-turbo',maxTokens:2048,temp:0,prompt:"(Heres an example of changing color of a pie chart to help you pie title Example \"Red\": 20 \"Blue\": 50 \"Green\": 30 %%{init: {'theme': 'base', 'themeVariables': {'pie1': '#0000FF', 'pie2': '#00FF00', 'pie3': '#FF0000'}}}%% keep in mind that pie1 is the highest since its sorted in descending order. Heres an example of a mindmap: mindmap root((mindmap)) Origins Long history ::icon(fa fa-book) Popularisation British popular psychology author Tony Buzan Research On effectivness<br/>and features On Automatic creation Uses Creative techniques Strategic planning Argument mapping Tools Pen and paper Mermaid. "}
+ mermaid:{model:'gpt-4-turbo',maxTokens:2048,temp:0,prompt:"(Heres an example of changing color of a pie chart to help you pie title Example \"Red\": 20 \"Blue\": 50 \"Green\": 30 %%{init: {'theme': 'base', 'themeVariables': {'pie1': '#0000FF', 'pie2': '#00FF00', 'pie3': '#FF0000'}}}%% keep in mind that pie1 is the highest since its sorted in descending order. Heres an example of a mindmap: mindmap root((mindmap)) Origins Long history ::icon(fa fa-book) Popularisation British popular psychology author Tony Buzan Research On effectivness<br/>and features On Automatic creation Uses Creative techniques Strategic planning Argument mapping Tools Pen and paper Mermaid. "},
+ data: { model: 'gpt-3.5-turbo', maxTokens: 256, temp: 0.5, prompt: "You are a helpful resarch assistant. Analyze the user's data to find meaningful patterns and/or correlation. Please keep your response short and to the point." },
};
@@ -29,7 +35,7 @@ const callTypeMap: { [type: string]: GPTCallOpts } = {
* @param inputText Text to process
* @returns AI Output
*/
-const gptAPICall = async (inputText: string, callType: GPTCallType) => {
+const gptAPICall = async (inputText: string, callType: GPTCallType, prompt?: any) => {
if (callType === GPTCallType.SUMMARY) inputText += '.';
const opts: GPTCallOpts = callTypeMap[callType];
try {
@@ -39,8 +45,9 @@ const gptAPICall = async (inputText: string, callType: GPTCallType) => {
};
const openai = new OpenAI(configuration);
+ let usePrompt = prompt ? opts.prompt + prompt : opts.prompt;
let messages: ChatCompletionMessageParam[] = [
- { role: 'system', content: opts.prompt },
+ { role: 'system', content: usePrompt },
{ role: 'user', content: inputText },
];
@@ -48,7 +55,6 @@ const gptAPICall = async (inputText: string, callType: GPTCallType) => {
model: opts.model,
messages: messages,
temperature: opts.temp,
- max_tokens: opts.maxTokens,
});
const content = response.choices[0].message.content;
return content;
@@ -71,8 +77,7 @@ const gptImageCall = async (prompt: string, n?: number) => {
n: n ?? 1,
size: '1024x1024',
});
- return response.data.map((data: any) => data.url);
- // return response.data.data[0].url;
+ return response.data.map(data => data.url);
} catch (err) {
console.error(err);
return;
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 668d52e4a..0d25674f1 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -61,7 +61,7 @@ import { PresBox } from '../views/nodes/trails/PresBox';
import { PresElementBox } from '../views/nodes/trails/PresElementBox';
import { SearchBox } from '../views/search/SearchBox';
import { CollectionViewType, DocumentType } from './DocumentTypes';
-const { default: { DFLT_IMAGE_NATIVE_DIM } } = require('../views/global/globalCssVariables.module.scss'); // prettier-ignore
+const { DFLT_IMAGE_NATIVE_DIM } = require('../views/global/globalCssVariables.module.scss'); // prettier-ignore
const defaultNativeImageDim = Number(DFLT_IMAGE_NATIVE_DIM.replace('px', ''));
class EmptyBox {
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 21b74e30c..dbf08c66c 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -823,7 +823,7 @@ pie title Minerals in my tap water
}
/// initializes a context menu button for the top bar context menu
- static setupContextMenuButton(params:Button, btnDoc?:Doc) {
+ static setupContextMenuButton(params:Button, btnDoc?:Doc, btnContainer?:Doc) {
const reqdOpts:DocumentOptions = {
...OmitKeys(params, ["scripts", "funcs", "subMenu"]).omit,
color: Colors.WHITE, isSystem: true,
@@ -831,6 +831,7 @@ pie title Minerals in my tap water
_height: 30, _nativeHeight: 30, linearBtnWidth: params.linearBtnWidth,
toolType: params.toolType, expertMode: params.expertMode,
_dragOnlyWithinContainer: true, _lockedPosition: true,
+ _embedContainer: btnContainer
};
const reqdFuncs:{[key:string]:any} = {
...params.funcs,
@@ -840,15 +841,15 @@ pie title Minerals in my tap water
static setupContextMenuBtn(params:Button, menuDoc:Doc):Doc {
- const menuBtnDoc = DocListCast(menuDoc?.data).find(doc => doc.title === params.title);
+ const menuBtnDoc = DocListCast(menuDoc?.data).find( doc => doc.title === params.title);
const subMenu = params.subMenu;
if (!subMenu) { // button does not have a sub menu
- return this.setupContextMenuButton(params, menuBtnDoc);
+ return this.setupContextMenuButton(params, menuBtnDoc, menuDoc);
}
// linear view
const reqdSubMenuOpts = { ...OmitKeys(params, ["scripts", "funcs", "subMenu"]).omit, undoIgnoreFields: new List<string>(['width', "linearView_IsOpen"]),
childDontRegisterViews: true, flexGap: 0, _height: 30, ignoreClick: params.scripts?.onClick ? false : true,
- linearView_SubMenu: true, linearView_Expandable: true};
+ linearView_SubMenu: true, linearView_Expandable: true, embedContainer: menuDoc};
const items = (menuBtnDoc?:Doc) => !menuBtnDoc ? [] : subMenu.map(sub => this.setupContextMenuBtn(sub, menuBtnDoc) );
const creator = params.btnType === ButtonType.MultiToggleButton ? this.multiToggleList : this.linearButtonList;
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index aa0f77c72..9627c5df2 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -28,7 +28,7 @@ import { SelectionManager } from './SelectionManager';
import { SnappingManager } from './SnappingManager';
import { UndoManager } from './UndoManager';
import { DocData } from '../../fields/DocSymbols';
-const { default : { contextMenuZindex } } = require('../views/global/globalCssVariables.module.scss'); // prettier-ignore
+const { contextMenuZindex } = require('../views/global/globalCssVariables.module.scss'); // prettier-ignore
export enum dropActionType {
embed = 'embed', // create a new embedding of the dragged document for the new location
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 9e469ed1f..2a44a9739 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -34,6 +34,7 @@ import { DocumentView, OpenWhereMod } from './nodes/DocumentView';
import { ImageBox } from './nodes/ImageBox';
import { KeyValueBox } from './nodes/KeyValueBox';
import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
+import { identity } from 'lodash';
interface DocumentDecorationsProps {
PanelWidth: number;
@@ -426,9 +427,13 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
SnappingManager.SetIsResizing(SelectionManager.Docs.lastElement()); // turns off pointer events on things like youtube videos and web pages so that dragging doesn't get "stuck" when cursor moves over them
setupMoveUpEvents(this, e, this.onPointerMove, this.onPointerUp, emptyFunction);
e.stopPropagation();
- this._resizeHdlId = e.currentTarget.className;
+ const id = (this._resizeHdlId = e.currentTarget.className);
+ const pad = id.includes('Left') || id.includes('Right') ? Number(getComputedStyle(e.target as any).width.replace('px', '')) / 2 : 0;
const bounds = e.currentTarget.getBoundingClientRect();
- this._offset = { x: this._resizeHdlId.toLowerCase().includes('left') ? bounds.right - e.clientX : bounds.left - e.clientX, y: this._resizeHdlId.toLowerCase().includes('top') ? bounds.bottom - e.clientY : bounds.top - e.clientY };
+ this._offset = {
+ x: id.toLowerCase().includes('left') ? bounds.right - e.clientX - pad : bounds.left - e.clientX + pad, //
+ y: id.toLowerCase().includes('top') ? bounds.bottom - e.clientY - pad : bounds.top - e.clientY + pad,
+ };
this._resizeUndo = UndoManager.StartBatch('drag resizing');
this._snapPt = { x: e.pageX, y: e.pageY };
SelectionManager.Views.forEach(docView => docView.CollectionFreeFormView?.dragStarting(false, false));
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 5a9f10ba4..51baaa23e 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -44,7 +44,7 @@ import { FieldView, FieldViewProps } from './nodes/FieldView';
import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
import { PinProps, PresBox } from './nodes/trails';
import { StyleProp } from './StyleProvider';
-const { default: { INK_MASK_SIZE } } = require('./global/globalCssVariables.module.scss'); // prettier-ignore
+const { INK_MASK_SIZE } = require('./global/globalCssVariables.module.scss'); // prettier-ignore
@observer
export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>() implements ViewBoxInterface {
static readonly MaskDim = INK_MASK_SIZE; // choose a really big number to make sure mask fits over container (which in theory can be arbitrarily big)
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index e8a3a37cb..58b8d255a 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -57,7 +57,7 @@ import { AudioBox } from './nodes/AudioBox';
import { SchemaCSVPopUp } from './nodes/DataVizBox/SchemaCSVPopUp';
import { DocButtonState } from './nodes/DocumentLinksButton';
import { DocumentView, DocumentViewInternal, OpenWhere, OpenWhereMod, returnEmptyDocViewList } from './nodes/DocumentView';
-import { ImageBox } from './nodes/ImageBox';
+import { ImageBox, ImageEditorData as ImageEditor } from './nodes/ImageBox';
import { LinkDescriptionPopup } from './nodes/LinkDescriptionPopup';
import { LinkDocPreview, LinkInfo } from './nodes/LinkDocPreview';
import { DirectionsAnchorMenu } from './nodes/MapBox/DirectionsAnchorMenu';
@@ -71,7 +71,7 @@ import { PresBox } from './nodes/trails';
import { AnchorMenu } from './pdf/AnchorMenu';
import { GPTPopup } from './pdf/GPTPopup/GPTPopup';
import { TopBar } from './topbar/TopBar';
-const { default: { LEFT_MENU_WIDTH, TOPBAR_HEIGHT } } = require('./global/globalCssVariables.module.scss'); // prettier-ignore
+const { LEFT_MENU_WIDTH, TOPBAR_HEIGHT } = require('./global/globalCssVariables.module.scss'); // prettier-ignore
const _global = (window /* browser */ || global) /* node */ as any;
@observer
@@ -1042,7 +1042,7 @@ export class MainView extends ObservableReactComponent<{}> {
<OverlayView />
<GPTPopup key="gptpopup" />
<SchemaCSVPopUp key="schemacsvpopup" />
- <GenerativeFill imageEditorOpen={ImageBox.imageEditorOpen} imageEditorSource={ImageBox.imageEditorSource} imageRootDoc={ImageBox.imageRootDoc} addDoc={ImageBox.addDoc} />
+ <GenerativeFill imageEditorOpen={ImageEditor.Open} imageEditorSource={ImageEditor.Source} imageRootDoc={ImageEditor.RootDoc} addDoc={ImageEditor.AddDoc} />
{/* <NewLightboxView key="newLightbox" PanelWidth={this._windowWidth} PanelHeight={this._windowHeight} maxBorder={[200, 50]} /> */}
</div>
);
diff --git a/src/client/views/PropertiesView.scss b/src/client/views/PropertiesView.scss
index 8581bdf73..476b46905 100644
--- a/src/client/views/PropertiesView.scss
+++ b/src/client/views/PropertiesView.scss
@@ -227,14 +227,15 @@
font-weight: bold;
width: 95px;
overflow-x: hidden;
- display: inline-block;
text-overflow: ellipsis;
white-space: nowrap;
+ display: flex;
+ align-items: center;
}
.propertiesView-sharingTable-item-permission {
display: flex;
- align-items: flex-end;
+ align-items: center;
text-align: right;
margin-left: auto;
margin-right: -12px;
diff --git a/src/client/views/StyleProvider.scss b/src/client/views/StyleProvider.scss
index 30a026dbc..ce00f6101 100644
--- a/src/client/views/StyleProvider.scss
+++ b/src/client/views/StyleProvider.scss
@@ -3,6 +3,7 @@
.styleProvider-paint,
.styleProvider-paint-selected,
.styleProvider-lock {
+ z-index: 2; // has to be above title which is z-index 1
font-size: 10;
width: 15;
height: 15;
diff --git a/src/client/views/collections/CollectionCarousel3DView.tsx b/src/client/views/collections/CollectionCarousel3DView.tsx
index 5454c6f9f..4e4bd43bf 100644
--- a/src/client/views/collections/CollectionCarousel3DView.tsx
+++ b/src/client/views/collections/CollectionCarousel3DView.tsx
@@ -14,7 +14,8 @@ import { DocumentView } from '../nodes/DocumentView';
import { FocusViewOptions } from '../nodes/FieldView';
import './CollectionCarousel3DView.scss';
import { CollectionSubView } from './CollectionSubView';
-const { default: { CAROUSEL3D_CENTER_SCALE, CAROUSEL3D_SIDE_SCALE, CAROUSEL3D_TOP } } = require('../global/globalCssVariables.module.scss'); // prettier-ignore
+const { CAROUSEL3D_CENTER_SCALE, CAROUSEL3D_SIDE_SCALE, CAROUSEL3D_TOP } = require('../global/globalCssVariables.module.scss');
+
@observer
export class CollectionCarousel3DView extends CollectionSubView() {
@computed get scrollSpeed() {
diff --git a/src/client/views/collections/CollectionNoteTakingView.scss b/src/client/views/collections/CollectionNoteTakingView.scss
index 4c2dcf9ab..95fda7b0a 100644
--- a/src/client/views/collections/CollectionNoteTakingView.scss
+++ b/src/client/views/collections/CollectionNoteTakingView.scss
@@ -1,7 +1,7 @@
@import '../global/globalCssVariables.module.scss';
.collectionNoteTakingView-DocumentButtons {
- display: none;
+ opacity: 0;
justify-content: space-between;
margin: auto;
}
@@ -15,7 +15,6 @@
.editableView-container-editing-oneLine,
.editableView-container-editing {
- color: grey;
padding: 10px;
width: 100%;
}
@@ -29,7 +28,6 @@
.editableView-input {
outline-color: black;
letter-spacing: 2px;
- color: grey;
border: 0px;
padding: 12px 10px 11px 10px;
}
@@ -41,7 +39,6 @@
.editableView-input {
outline-color: black;
letter-spacing: 2px;
- color: grey;
border: 0px;
padding: 12px 10px 11px 10px;
}
@@ -51,9 +48,9 @@
display: flex;
}
-.collectionNoteTakingViewFieldColumn:hover {
+.collectionNoteTakingViewFieldColumnHover:hover {
.collectionNoteTakingView-DocumentButtons {
- display: flex;
+ opacity: 1;
}
}
@@ -100,6 +97,9 @@
flex-direction: column;
height: max-content;
}
+ .collectionNoteTakingViewFieldColumnHover {
+ min-height: 100%; // if we use this, then we can't have autoHeight
+ }
.collectionSchemaView-previewDoc {
height: 100%;
@@ -250,7 +250,6 @@
overflow: visible;
width: 100%;
display: flex;
- color: gray;
align-items: center;
}
}
@@ -262,10 +261,6 @@
background: $medium-gray;
// overflow: hidden; overflow is visible so the color menu isn't hidden -ftong
- .editableView-input {
- color: $dark-gray;
- }
-
.editableView-input:hover,
.editableView-container-editing:hover,
.editableView-container-editing-oneLine:hover {
@@ -288,7 +283,6 @@
.editableView-container-editing-oneLine,
.editableView-container-editing {
- color: grey;
padding: 10px;
}
@@ -301,7 +295,6 @@
.editableView-input {
padding: 12px 10px 11px 10px;
border: 0px;
- color: grey;
text-align: center;
letter-spacing: 2px;
outline-color: black;
@@ -409,7 +402,6 @@
.editableView-container-editing-oneLine,
.editableView-container-editing {
- color: grey;
padding: 10px;
width: 100%;
}
@@ -423,13 +415,16 @@
.editableView-input {
outline-color: black;
letter-spacing: 2px;
- color: grey;
border: 0px;
padding: 12px 10px 11px 10px;
+ &::placeholder {
+ color: black;
+ }
}
}
.collectionNoteTakingView-addDocumentButton {
+ opacity: 0.5;
font-size: 75%;
letter-spacing: 2px;
cursor: pointer;
@@ -437,10 +432,12 @@
.editableView-input {
outline-color: black;
letter-spacing: 2px;
- color: grey;
border: 0px;
padding: 12px 10px 11px 10px;
}
+ &:hover {
+ opacity: unset;
+ }
}
.collectionNoteTakingView-addGroupButton {
@@ -497,6 +494,24 @@
.rc-switch-checked .rc-switch-inner {
left: 8px;
}
+
+ .collectionNoteTaking-pivotField {
+ display: none;
+ }
+ &:hover {
+ .collectionNoteTaking-pivotField {
+ display: unset;
+ }
+ }
+}
+
+.collectionNoteTakingViewLight {
+ .collectionNoteTakingView-addDocumentButton,
+ .collectionNoteTakingView-addGroupButton {
+ .editableView-input::placeholder {
+ color: white;
+ }
+ }
}
@media only screen and (max-device-width: 480px) {
diff --git a/src/client/views/collections/CollectionNoteTakingView.tsx b/src/client/views/collections/CollectionNoteTakingView.tsx
index 5b91216cb..d8a0aebb1 100644
--- a/src/client/views/collections/CollectionNoteTakingView.tsx
+++ b/src/client/views/collections/CollectionNoteTakingView.tsx
@@ -9,14 +9,15 @@ import { listSpec } from '../../../fields/Schema';
import { SchemaHeaderField } from '../../../fields/SchemaHeaderField';
import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
import { TraceMobx } from '../../../fields/util';
-import { DivHeight, emptyFunction, returnZero, smoothScroll, Utils } from '../../../Utils';
+import { DivHeight, emptyFunction, lightOrDark, returnZero, smoothScroll, Utils } from '../../../Utils';
import { Docs, DocUtils } from '../../documents/Documents';
import { DragManager, dropActionType } from '../../util/DragManager';
import { SnappingManager } from '../../util/SnappingManager';
import { Transform } from '../../util/Transform';
-import { undoBatch } from '../../util/UndoManager';
+import { undoable, undoBatch } from '../../util/UndoManager';
import { ContextMenu } from '../ContextMenu';
import { ContextMenuProps } from '../ContextMenuItem';
+import { Colors } from '../global/globalEnums';
import { LightboxView } from '../LightboxView';
import { DocumentView } from '../nodes/DocumentView';
import { FieldViewProps, FocusViewOptions } from '../nodes/FieldView';
@@ -26,6 +27,7 @@ import './CollectionNoteTakingView.scss';
import { CollectionNoteTakingViewColumn } from './CollectionNoteTakingViewColumn';
import { CollectionNoteTakingViewDivider } from './CollectionNoteTakingViewDivider';
import { CollectionSubView } from './CollectionSubView';
+import { FieldsDropdown } from '../FieldsDropdown';
const _global = (window /* browser */ || global) /* node */ as any;
/**
@@ -40,7 +42,9 @@ export class CollectionNoteTakingView extends CollectionSubView() {
_disposers: { [key: string]: IReactionDisposer } = {};
_masonryGridRef: HTMLDivElement | null = null;
_draggerRef = React.createRef<HTMLDivElement>();
- notetakingCategoryField = 'NotetakingCategory';
+ @computed get notetakingCategoryField() {
+ return StrCast(this.dataDoc.notetaking_column, StrCast(this.layoutDoc.pivotField, 'notetaking_column'));
+ }
public DividerWidth = 16;
@observable docsDraggedRowCol: number[] = [];
@observable _scroll = 0;
@@ -152,25 +156,34 @@ export class CollectionNoteTakingView extends CollectionSubView() {
);
};
+ @computed get allFieldValues() {
+ return new Set(this.childDocs.map(doc => StrCast(doc[this.notetakingCategoryField])));
+ }
+
componentDidMount() {
super.componentDidMount?.();
document.addEventListener('pointerup', this.removeDocDragHighlight, true);
- this._disposers.layout_autoHeight = reaction(
- () => this.layoutDoc._layout_autoHeight,
- layout_autoHeight => layout_autoHeight && this._props.setHeight?.(this.headerMargin + Math.max(...this._refList.map(DivHeight)))
+
+ this._disposers.autoColumns = reaction(
+ () => (this.layoutDoc._notetaking_columns_autoCreate ? Array.from(this.allFieldValues) : undefined),
+ columns => undoable(() => columns?.filter(col => !this.colHeaderData.some(h => h.heading === col)).forEach(col => this.addColumn(col)), 'adding columns')(),
+ { fireImmediately: true }
);
this._disposers.refList = reaction(
() => ({ refList: this._refList.slice(), autoHeight: this.layoutDoc._layout_autoHeight && !LightboxView.Contains(this.DocumentView?.()) }),
({ refList, autoHeight }) => {
- if (autoHeight) refList.forEach(r => this.observer.observe(r));
- else this.observer.disconnect();
+ if (autoHeight) {
+ refList.forEach(r => this.observer.observe(r));
+ this._props.setHeight?.(this.headerMargin + Math.max(...this._refList.map(DivHeight)));
+ } else this.observer.disconnect();
},
{ fireImmediately: true }
);
}
componentWillUnmount() {
+ this.observer.disconnect();
document.removeEventListener('pointerup', this.removeDocDragHighlight, true);
super.componentWillUnmount();
Object.keys(this._disposers).forEach(key => this._disposers[key]());
@@ -297,7 +310,7 @@ export class CollectionNoteTakingView extends CollectionSubView() {
getDocWidth(d: Doc) {
const heading = !d[this.notetakingCategoryField] ? 'unset' : Field.toString(d[this.notetakingCategoryField] as Field);
const existingHeader = this.colHeaderData.find(sh => sh.heading === heading);
- const existingWidth = existingHeader?.width ? existingHeader.width : 0;
+ 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;
const width = d.layout_fitWidth ? maxWidth : NumCast(d._width);
return Math.min(maxWidth - CollectionNoteTakingViewColumn.ColumnMargin, width < maxWidth ? width : maxWidth);
@@ -369,7 +382,7 @@ export class CollectionNoteTakingView extends CollectionSubView() {
// we alter the pivot fields of the docs in case they are moved to a new column.
const colIndex = this.getColumnFromXCoord(xCoord);
const colHeader = colIndex === undefined ? 'unset' : StrCast(this.colHeaderData[colIndex].heading);
- DragManager.docsBeingDragged.forEach(d => (d[this.notetakingCategoryField] = colHeader));
+ DragManager.docsBeingDragged.map(doc => doc[DocData]).forEach(d => (d[this.notetakingCategoryField] = colHeader));
// used to notify sections to re-render
this.docsDraggedRowCol.length = 0;
const columnFromCoord = this.getColumnFromXCoord(xCoord);
@@ -499,11 +512,12 @@ export class CollectionNoteTakingView extends CollectionSubView() {
editableViewProps = () => ({
GetValue: () => '',
- SetValue: this.addGroup,
- contents: '+ New Column',
+ SetValue: this.addColumn,
+ contents: '+ Column',
});
refList = () => this._refList;
+ backgroundColor = () => this._props.DocumentView?.().backgroundColor();
// sectionNoteTaking returns a CollectionNoteTakingViewColumn (which is an individual column)
sectionNoteTaking = (heading: SchemaHeaderField | undefined, docList: Doc[]) => (
@@ -511,7 +525,9 @@ export class CollectionNoteTakingView extends CollectionSubView() {
key={heading?.heading ?? 'unset'}
PanelWidth={this._props.PanelWidth}
refList={this._refList}
+ backgroundColor={this.backgroundColor}
select={this._props.select}
+ isContentActive={this.isContentActive}
addDocument={this.addDocument}
chromeHidden={this.chromeHidden}
colHeaderData={this.colHeaderData}
@@ -537,18 +553,26 @@ export class CollectionNoteTakingView extends CollectionSubView() {
/>
);
+ @undoBatch
+ remColumn = (value: SchemaHeaderField) => {
+ const colHdrData = Array.from(Cast(this._props.Document[this._props.fieldKey + '_columnHeaders'], listSpec(SchemaHeaderField), null));
+ if (value) {
+ const index = colHdrData.indexOf(value);
+ index !== -1 && colHdrData.splice(index, 1);
+ this.resizeColumns(colHdrData);
+ }
+ };
+
// addGroup is called when adding a new columnHeader, adding a SchemaHeaderField to our list of
// columnHeaders and resizing the existing columns to make room for our new one.
@undoBatch
- addGroup = (value: string) => {
- if (this.colHeaderData) {
- for (const header of this.colHeaderData) {
- if (header.heading === value) {
- alert('You cannot use an existing column name. Please try a new column name');
- return value;
- }
+ addColumn = (value: string) => {
+ this.colHeaderData.forEach(header => {
+ if (header.heading === value) {
+ alert('You cannot use an existing column name. Please try a new column name');
+ return value;
}
- }
+ });
const columnHeaders = Array.from(Cast(this.dataDoc[this.fieldKey + '_columnHeaders'], listSpec(SchemaHeaderField), null));
const newColWidth = 1 / (this.numGroupColumns + 1);
columnHeaders.push(new SchemaHeaderField(value, undefined, undefined, newColWidth));
@@ -556,11 +580,25 @@ export class CollectionNoteTakingView extends CollectionSubView() {
return true;
};
+ removeEmptyColumns = undoable(() => {
+ this.colHeaderData.filter(h => !this.allFieldValues.has(h.heading)).forEach(this.remColumn);
+ }, 'remove empty Columns');
+
onContextMenu = (e: React.MouseEvent): void => {
// need to test if propagation has stopped because GoldenLayout forces a parallel react hierarchy to be created for its top-level layout
if (!e.isPropagationStopped()) {
const subItems: ContextMenuProps[] = [];
- subItems.push({ description: `${this.layoutDoc._columnsFill ? 'Variable Size' : 'Autosize'} Column`, event: () => (this.layoutDoc._columnsFill = !this.layoutDoc._columnsFill), icon: 'plus' });
+ subItems.push({
+ description: `${this.layoutDoc._notetaking_columns_autoCreate ? 'Manually' : 'Automatically'} Create columns`,
+ event: () => (this.layoutDoc._notetaking_columns_autoCreate = !this.layoutDoc._notetaking_columns_autoCreate),
+ icon: 'computer',
+ });
+ subItems.push({ description: 'Remove Empty Columns', event: this.removeEmptyColumns, icon: 'computer' });
+ subItems.push({
+ description: `${this.layoutDoc._notetaking_columns_autoSize ? 'Variable Size' : 'Autosize'} Columns`,
+ event: () => (this.layoutDoc._notetaking_columns_autoSize = !this.layoutDoc._notetaking_columns_autoSize),
+ icon: 'plus',
+ });
subItems.push({ description: `${this.layoutDoc._layout_autoHeight ? 'Variable Height' : 'Auto Height'}`, event: () => (this.layoutDoc._layout_autoHeight = !this.layoutDoc._layout_autoHeight), icon: 'plus' });
subItems.push({ description: 'Clear All', event: () => (this.dataDoc.data = new List([])), icon: 'times' });
ContextMenu.Instance.addItem({ description: 'Options...', subitems: subItems, icon: 'eye' });
@@ -613,12 +651,11 @@ export class CollectionNoteTakingView extends CollectionSubView() {
TraceMobx();
return (
<div
- className="collectionNoteTakingView"
+ className={`collectionNoteTakingView ${lightOrDark(this.backgroundColor()) === Colors.WHITE ? 'collectionNoteTakingViewLight' : ''}`}
ref={this.createRef}
- key="notes"
style={{
- overflowY: this._props.isContentActive() ? 'auto' : 'hidden',
- background: this._props.styleProvider?.(this.Document, this._props, StyleProp.BackgroundColor),
+ overflowY: this.isContentActive() ? 'auto' : 'hidden',
+ background: this.backgroundColor(),
pointerEvents: this.backgroundEvents,
}}
onScroll={action(e => (this._scroll = e.currentTarget.scrollTop))}
@@ -629,6 +666,16 @@ export class CollectionNoteTakingView extends CollectionSubView() {
onContextMenu={this.onContextMenu}
onWheel={e => this._props.isContentActive() && e.stopPropagation()}>
<>{this.renderedSections}</>
+ <div className="collectionNotetaking-pivotField" style={{ right: 0, top: 0, position: 'absolute' }}>
+ <FieldsDropdown
+ Document={this.Document}
+ selectFunc={undoable(fieldKey => {
+ this.layoutDoc._pivotField = fieldKey;
+ this.removeEmptyColumns();
+ }, 'change pivot field')}
+ placeholder={StrCast(this.layoutDoc._pivotField)}
+ />
+ </div>
</div>
);
}
diff --git a/src/client/views/collections/CollectionNoteTakingViewColumn.tsx b/src/client/views/collections/CollectionNoteTakingViewColumn.tsx
index db178d500..448b11b05 100644
--- a/src/client/views/collections/CollectionNoteTakingViewColumn.tsx
+++ b/src/client/views/collections/CollectionNoteTakingViewColumn.tsx
@@ -1,10 +1,9 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, observable } from 'mobx';
+import { action, computed, makeObservable, observable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
-import { returnEmptyString } from '../../../Utils';
+import { lightOrDark, returnEmptyString } from '../../../Utils';
import { Doc, DocListCast, Opt } from '../../../fields/Doc';
-import { Id } from '../../../fields/FieldSymbols';
import { RichTextField } from '../../../fields/RichTextField';
import { listSpec } from '../../../fields/Schema';
import { SchemaHeaderField } from '../../../fields/SchemaHeaderField';
@@ -26,6 +25,7 @@ import './CollectionNoteTakingView.scss';
interface CSVFieldColumnProps {
Document: Doc;
TemplateDataDocument: Opt<Doc>;
+ backgroundColor?: (() => string) | undefined;
docList: Doc[];
heading: string;
pivotField: string;
@@ -38,6 +38,7 @@ interface CSVFieldColumnProps {
gridGap: number;
headings: () => object[];
select: (ctrlPressed: boolean) => void;
+ isContentActive: () => boolean | undefined;
renderChildren: (docs: Doc[]) => JSX.Element[];
addDocument: (doc: Doc | Doc[]) => boolean;
createDropTarget: (ele: HTMLDivElement) => void;
@@ -57,10 +58,16 @@ interface CSVFieldColumnProps {
*/
@observer
export class CollectionNoteTakingViewColumn extends ObservableReactComponent<CSVFieldColumnProps> {
- @observable private _background = 'inherit';
+ @observable private _hover = false;
+
+ constructor(props: CSVFieldColumnProps) {
+ super(props);
+ makeObservable(this);
+ }
// columnWidth returns the width of a column in absolute pixels
@computed get columnWidth() {
+ if (this._props.Document._notetaking_columns_autoSize) return this._props.availableWidth / (this._props.colHeaderData?.length || 1);
if (!this._props.colHeaderData || !this._props.headingObject || this._props.colHeaderData.length === 1) return `${(this._props.availableWidth / this._props.PanelWidth()) * 100}%`;
const i = this._props.colHeaderData.findIndex(hd => hd.heading === this._props.headingObject?.heading && hd.color === this._props.headingObject.color);
return ((this._props.colHeaderData[i].width * this._props.availableWidth) / this._props.PanelWidth()) * 100 + '%';
@@ -122,8 +129,8 @@ export class CollectionNoteTakingViewColumn extends ObservableReactComponent<CSV
return false;
};
- @action pointerEntered = () => SnappingManager.IsDragging && (this._background = '#b4b4b4');
- @action pointerLeave = () => (this._background = 'inherit');
+ @action pointerEntered = () => (this._hover = true);
+ @action pointerLeave = () => (this._hover = false);
@undoBatch
addTextNote = (char: string) => this.addNewTextDoc('-typed text-', false, true);
@@ -147,7 +154,7 @@ export class CollectionNoteTakingViewColumn extends ObservableReactComponent<CSV
deleteColumn = () => {
const colHdrData = Array.from(Cast(this._props.Document[this._props.fieldKey + '_columnHeaders'], listSpec(SchemaHeaderField), null));
if (this._props.headingObject) {
- this._props.docList.forEach(d => (d[this._props.pivotField] = undefined));
+ // this._props.docList.forEach(d => (d[DocData][this._props.pivotField] = undefined));
colHdrData.splice(colHdrData.indexOf(this._props.headingObject), 1);
this._props.resizeColumns(colHdrData);
}
@@ -273,11 +280,11 @@ export class CollectionNoteTakingViewColumn extends ObservableReactComponent<CSV
</div>
{!this._props.chromeHidden ? (
- <div className="collectionNoteTakingView-DocumentButtons" style={{ marginBottom: 10 }}>
- <div key={`${heading}-add-document`} className="collectionNoteTakingView-addDocumentButton">
- <EditableView GetValue={returnEmptyString} SetValue={this.addNewTextDoc} textCallback={this.addTextNote} placeholder={"Type ':' for commands"} contents={'+ New Node'} menuCallback={this.menuCallback} />
+ <div className="collectionNoteTakingView-DocumentButtons" style={{ display: this._props.isContentActive() ? 'flex' : 'none', marginBottom: 10 }}>
+ <div className="collectionNoteTakingView-addDocumentButton" style={{ color: lightOrDark(this._props.backgroundColor?.()) }}>
+ <EditableView GetValue={returnEmptyString} SetValue={this.addNewTextDoc} textCallback={this.addTextNote} placeholder={"Type ':' for commands"} contents={'+ Node'} menuCallback={this.menuCallback} />
</div>
- <div key={`${this._props.Document[Id]}-addGroup`} className="collectionNoteTakingView-addDocumentButton">
+ <div className="collectionNoteTakingView-addDocumentButton" style={{ color: lightOrDark(this._props.backgroundColor?.()) }}>
<EditableView {...this._props.editableViewProps()} />
</div>
</div>
@@ -292,17 +299,17 @@ export class CollectionNoteTakingViewColumn extends ObservableReactComponent<CSV
TraceMobx();
return (
<div
- className="collectionNoteTakingViewFieldColumn"
- key={this._heading}
+ className="collectionNoteTakingViewFieldColumnHover"
+ onPointerEnter={this.pointerEntered}
+ onPointerLeave={this.pointerLeave}
style={{
width: this.columnWidth,
- background: this._background,
+ background: this._hover && SnappingManager.IsDragging ? '#b4b4b4' : 'inherit',
marginLeft: this._props.headings().findIndex((h: any) => h[0] === this._props.headingObject) === 0 ? NumCast(this._props.Document.xMargin) : 0,
- }}
- ref={this.createColumnDropRef}
- onPointerEnter={this.pointerEntered}
- onPointerLeave={this.pointerLeave}>
- {this.innards}
+ }}>
+ <div className="collectionNoteTakingViewFieldColumn" key={this._heading} ref={this.createColumnDropRef}>
+ {this.innards}
+ </div>
</div>
);
}
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 3a62a53d7..bf0393883 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -316,7 +316,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
<DocumentView
ref={action((r: DocumentView) => r?.ContentDiv && this.docRefs.set(doc, r))}
Document={doc}
- TemplateDataDocument={dataDoc ?? (Doc.AreProtosEqual(doc[DocData], doc) ? undefined : doc[DocData])}
+ TemplateDataDocument={dataDoc}
renderDepth={this._props.renderDepth + 1}
PanelWidth={panelWidth}
PanelHeight={panelHeight}
diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
index 641e01b81..c5292f880 100644
--- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
+++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
@@ -12,7 +12,7 @@ import { TraceMobx } from '../../../fields/util';
import { DivHeight, DivWidth, emptyFunction, returnEmptyString, setupMoveUpEvents } from '../../../Utils';
import { Docs, DocUtils } from '../../documents/Documents';
import { DocumentType } from '../../documents/DocumentTypes';
-import { DragManager } from '../../util/DragManager';
+import { DragManager, dropActionType } from '../../util/DragManager';
import { SnappingManager } from '../../util/SnappingManager';
import { Transform } from '../../util/Transform';
import { undoBatch } from '../../util/UndoManager';
@@ -66,11 +66,26 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
_ele: HTMLElement | null = null;
+ protected onInternalPreDrop = (e: Event, de: DragManager.DropEvent, targetDropAction: dropActionType) => {
+ const dragData = de.complete.docDragData;
+ if (dragData) {
+ const sourceDragAction = dragData.dropAction;
+ const sameCollection = !dragData.draggedDocuments.some(d => !this._props.docList.includes(d));
+ dragData.dropAction = !sameCollection // if doc from another tree
+ ? sourceDragAction || targetDropAction // then use the source's dragAction otherwise the target's
+ : sourceDragAction === dropActionType.inPlace // if source drag is inPlace
+ ? sourceDragAction // keep the doc in place
+ : dropActionType.same; // otherwise use same tree semantics to move within tree
+
+ e.stopPropagation();
+ }
+ };
+
// This is likely similar to what we will be doing. Why do we need to make these refs?
// is that the only way to have drop targets?
createColumnDropRef = (ele: HTMLDivElement | null) => {
this.dropDisposer?.();
- if (ele) this.dropDisposer = DragManager.MakeDropTarget(ele, this.columnDrop.bind(this), this._props.Document);
+ if (ele) this.dropDisposer = DragManager.MakeDropTarget(ele, this.columnDrop.bind(this), this._props.Document, this.onInternalPreDrop.bind(this));
else if (this._ele) this.props.refList.splice(this.props.refList.indexOf(this._ele), 1);
this._ele = ele;
};
@@ -345,7 +360,7 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
<div>
<div
key={`${heading}-stack`}
- className={`collectionStackingView-masonrySingle`}
+ className="collectionStackingView-masonrySingle"
style={{
padding: `${columnYMargin}px ${0}px ${this._props.yMargin}px ${0}px`,
margin: 'auto',
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index ecc2aea31..32198e3a2 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -4,7 +4,7 @@ import * as rp from 'request-promise';
import { Utils, returnFalse } from '../../../Utils';
import CursorField from '../../../fields/CursorField';
import { Doc, DocListCast, Field, Opt, StrListCast } from '../../../fields/Doc';
-import { AclPrivate } from '../../../fields/DocSymbols';
+import { AclPrivate, DocData } from '../../../fields/DocSymbols';
import { Id } from '../../../fields/FieldSymbols';
import { List } from '../../../fields/List';
import { listSpec } from '../../../fields/Schema';
@@ -196,13 +196,16 @@ export function CollectionSubView<X>(moreProps?: X) {
@undoBatch
protected onGesture(e: Event, ge: GestureUtils.GestureEvent) {}
- protected onInternalPreDrop(e: Event, de: DragManager.DropEvent, dropAction: dropActionType) {
- if (de.complete.docDragData) {
- // if the dropEvent's dragAction is, say 'embed', but we're just dragging within a collection, we may not actually want to make an embedding.
- // so we check if our collection has a dropAction set on it and if so, we use that instead.
- if (dropAction && !de.complete.docDragData.draggedDocuments.some(d => d.embedContainer === this.Document && this.childDocs.includes(d))) {
- de.complete.docDragData.dropAction = dropAction;
- }
+ protected onInternalPreDrop(e: Event, de: DragManager.DropEvent, targetDropAction: dropActionType) {
+ const dragData = de.complete.docDragData;
+ if (dragData) {
+ const sourceDragAction = dragData.dropAction;
+ const sameCollection = !dragData.draggedDocuments.some(d => d.embedContainer !== this._props.Document);
+ dragData.dropAction = !sameCollection // if doc from another tree
+ ? sourceDragAction || targetDropAction // then use the source's dragAction otherwise the target's
+ : sourceDragAction === dropActionType.inPlace // if source drag is inPlace
+ ? sourceDragAction // keep the doc in place
+ : dropActionType.same; // otherwise use same tree semantics to move within tree
e.stopPropagation();
}
}
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index 293c79119..5741fc29b 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -157,14 +157,12 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
const dragData = de.complete.docDragData;
if (dragData) {
const sourceDragAction = dragData.dropAction;
- const sameTree = () => Doc.AreProtosEqual(dragData.treeViewDoc, this.Document);
- const isAlreadyInTree = () => sameTree || dragData.draggedDocuments.some(d => d.embedContainer === this.Document && this.childDocs.includes(d));
- dragData.dropAction =
- targetDropAction && !isAlreadyInTree() // if dropped document is not in the tree
- ? targetDropAction // then use the target's drop action if it's specified
- : !sameTree() || sourceDragAction === dropActionType.inPlace // if doc from another tree, or a non inPlace source drag action is specified
- ? sourceDragAction // use the source dragAction
- : dropActionType.same; // otherwise use same tree semantics to move within tree
+ const sameTree = dragData.treeViewDoc?.[DocData] === this.dataDoc;
+ dragData.dropAction = !sameTree // if doc from another tree
+ ? sourceDragAction || targetDropAction // then use the source's dragAction otherwise the target's
+ : sourceDragAction === dropActionType.inPlace // if source drag is inPlace
+ ? sourceDragAction // keep the doc in place
+ : dropActionType.same; // otherwise use same tree semantics to move within tree
e.stopPropagation();
}
};
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index 5eac6cb09..4fd49f8fe 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -35,7 +35,7 @@ import { CollectionTreeView, TreeViewType } from './CollectionTreeView';
import { CollectionView } from './CollectionView';
import { TreeSort } from './TreeSort';
import './TreeView.scss';
-const { default: { TREE_BULLET_WIDTH } } = require('../global/globalCssVariables.module.scss'); // prettier-ignore
+const { TREE_BULLET_WIDTH } = require('../global/globalCssVariables.module.scss'); // prettier-ignore
export interface TreeViewProps {
treeView: CollectionTreeView;
@@ -449,7 +449,9 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
const addDoc = inside ? this.localAdd : parentAddDoc;
const canAdd = !StrCast((inside ? this.Document : this._props.treeViewParent)?.treeView_FreezeChildren).includes('add') || forceAdd;
if (canAdd && (dropAction !== dropActionType.inPlace || droppedDocuments.every(d => d.embedContainer === this._props.parentTreeView?.Document))) {
- const move = (!dropAction || canEmbed || dropAction === dropActionType.proto || dropAction === dropActionType.move || dropAction === dropActionType.same || dropAction === dropActionType.inPlace) && moveDocument;
+ const move =
+ (!dropAction || (canEmbed && dropAction !== dropActionType.copy) || dropAction === dropActionType.proto || dropAction === dropActionType.move || dropAction === dropActionType.same || dropAction === dropActionType.inPlace) &&
+ moveDocument;
this._props.parentTreeView instanceof TreeView && (this._props.parentTreeView.dropping = true);
const res = droppedDocuments.reduce((added, d) => (move ? move(d, undefined, addDoc) || (dropAction === dropActionType.proto ? addDoc(d) : false) : addDoc(d)) || added, false);
this._props.parentTreeView instanceof TreeView && (this._props.parentTreeView.dropping = false);
@@ -928,7 +930,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
case 'Tab':
e.stopPropagation?.();
e.preventDefault?.();
- setTimeout(() => RichTextMenu.Instance.TextView?.EditorView?.focus(), 150);
+ setTimeout(() => RichTextMenu.Instance?.TextView?.EditorView?.focus(), 150);
UndoManager.RunInBatch(() => (e.shiftKey ? this._props.outdentDocument?.(true) : this._props.indentDocument?.(true)), 'tab');
return true;
case 'Backspace':
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormBackgroundGrid.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormBackgroundGrid.tsx
index 08dfb32ad..0acc99360 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormBackgroundGrid.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormBackgroundGrid.tsx
@@ -14,8 +14,8 @@ export interface CollectionFreeFormViewBackgroundGridProps {
nativeDimScaling: () => number;
zoomScaling: () => number;
layoutDoc: Doc;
- cachedCenteringShiftX: number;
- cachedCenteringShiftY: number;
+ centeringShiftX: number;
+ centeringShiftY: number;
}
@observer
export class CollectionFreeFormBackgroundGrid extends React.Component<CollectionFreeFormViewBackgroundGridProps> {
@@ -32,7 +32,7 @@ export class CollectionFreeFormBackgroundGrid extends React.Component<Collection
const w = this.props.PanelWidth() / this.props.nativeDimScaling() + 2 * renderGridSpace;
const h = this.props.PanelHeight() / this.props.nativeDimScaling() + 2 * renderGridSpace;
const strokeStyle = this.props.color();
- return !this.props.nativeDimScaling() ? null : (
+ return (
<canvas
className="collectionFreeFormView-grid"
width={w}
@@ -41,8 +41,8 @@ export class CollectionFreeFormBackgroundGrid extends React.Component<Collection
ref={el => {
const ctx = el?.getContext('2d');
if (ctx) {
- const Cx = this.props.cachedCenteringShiftX % renderGridSpace;
- const Cy = this.props.cachedCenteringShiftY % renderGridSpace;
+ const Cx = this.props.centeringShiftX % renderGridSpace;
+ const Cy = this.props.centeringShiftY % renderGridSpace;
ctx.lineWidth = Math.min(1, Math.max(0.5, this.props.zoomScaling()));
ctx.setLineDash(gridSpace > 50 ? [3, 3] : [1, 5]);
ctx.clearRect(0, 0, w, h);
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 791124f50..079a5d977 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -169,17 +169,19 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
@computed get nativeHeight() {
return this._props.NativeHeight?.() || Doc.NativeHeight(this.Document, Cast(this.Document.resolvedDataDoc, Doc, null));
}
- @computed get cachedCenteringShiftX(): number {
- const scaling = !this.nativeDimScaling ? 1 : this.nativeDimScaling;
+ @computed get centeringShiftX(): number {
+ const scaling = this.nativeDimScaling;
return this._props.isAnnotationOverlay || this._props.originTopLeft ? 0 : this._props.PanelWidth() / 2 / scaling; // shift so pan position is at center of window for non-overlay collections
}
- @computed get cachedCenteringShiftY(): number {
+ @computed get centeringShiftY(): number {
+ const panLocAtCenter = !(this._props.isAnnotationOverlay || this._props.originTopLeft);
+ if (!panLocAtCenter) return 0;
const dv = this.DocumentView?.();
- const fitWidth = this._props.layout_fitWidth?.(this.Document) ?? dv?.layoutDoc.layout_fitWidth;
- const scaling = !this.nativeDimScaling ? 1 : this.nativeDimScaling;
+ const aspect = !(this._props.layout_fitWidth?.(this.Document) ?? dv?.layoutDoc.layout_fitWidth) && dv?.nativeWidth && dv?.nativeHeight;
+ const scaling = this.nativeDimScaling;
// if freeform has a native aspect, then the panel height needs to be adjusted to match it
- const aspect = dv?.nativeWidth && dv?.nativeHeight && !fitWidth ? dv.nativeHeight / dv.nativeWidth : this._props.PanelHeight() / this._props.PanelWidth();
- return this._props.isAnnotationOverlay || this._props.originTopLeft ? 0 : (aspect * this._props.PanelWidth()) / 2 / scaling; // shift so pan position is at center of window for non-overlay collections
+ const height = aspect ? (dv.nativeHeight / dv.nativeWidth) * this._props.PanelWidth() : this._props.PanelHeight();
+ return height / 2 / scaling; // shift so pan position is at center of window for non-overlay collections
}
@computed get panZoomXf() {
return new Transform(this.panX(), this.panY(), 1 / this.zoomScaling());
@@ -187,7 +189,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
@computed get screenToFreeformContentsXf() {
return this._props
.ScreenToLocalTransform() //
- .translate(-this.cachedCenteringShiftX, -this.cachedCenteringShiftY)
+ .translate(-this.centeringShiftX, -this.centeringShiftY)
.transform(this.panZoomXf);
}
@@ -250,8 +252,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
panX = () => this.freeformData()?.bounds.cx ?? NumCast(this.Document[this.panXFieldKey], NumCast(Cast(this.Document.resolvedDataDoc, Doc, null)?.freeform_panX, 1));
panY = () => this.freeformData()?.bounds.cy ?? NumCast(this.Document[this.panYFieldKey], NumCast(Cast(this.Document.resolvedDataDoc, Doc, null)?.freeform_panY, 1));
zoomScaling = () => this.freeformData()?.scale ?? NumCast(Doc.Layout(this.Document)[this.scaleFieldKey], 1); //, NumCast(DocCast(this.Document.resolvedDataDoc)?.[this.scaleFieldKey], 1));
- PanZoomCenterXf = () =>
- this._props.isAnnotationOverlay && this.zoomScaling() === 1 ? `` : `translate(${this.cachedCenteringShiftX}px, ${this.cachedCenteringShiftY}px) scale(${this.zoomScaling()}) translate(${-this.panX()}px, ${-this.panY()}px)`;
+ PanZoomCenterXf = () => (this._props.isAnnotationOverlay && this.zoomScaling() === 1 ? `` : `translate(${this.centeringShiftX}px, ${this.centeringShiftY}px) scale(${this.zoomScaling()}) translate(${-this.panX()}px, ${-this.panY()}px)`);
ScreenToContentsXf = () => this.screenToFreeformContentsXf.copy();
getActiveDocuments = () => this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map(pair => pair.layout);
isAnyChildContentActive = () => this._props.isAnyChildContentActive();
@@ -1114,9 +1115,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
if (scale !== undefined) {
const maxZoom = 5; // sets the limit for how far we will zoom. this is useful for preventing small text boxes from filling the screen. So probably needs to be more sophisticated to consider more about the target and context
const newScale =
- scale === 0
- ? NumCast(this.layoutDoc[this.scaleFieldKey])
- : Math.min(maxZoom, (1 / (this.nativeDimScaling || 1)) * scale * Math.min(this._props.PanelWidth() / Math.abs(bounds.width), this._props.PanelHeight() / Math.abs(bounds.height)));
+ scale === 0 ? NumCast(this.layoutDoc[this.scaleFieldKey]) : Math.min(maxZoom, (1 / this.nativeDimScaling) * scale * Math.min(this._props.PanelWidth() / Math.abs(bounds.width), this._props.PanelHeight() / Math.abs(bounds.height)));
return {
panX: this._props.isAnnotationOverlay ? bounds.left - (Doc.NativeWidth(this.layoutDoc) / newScale - bounds.width) / 2 : (bounds.left + bounds.right) / 2,
panY: this._props.isAnnotationOverlay ? bounds.top - (Doc.NativeHeight(this.layoutDoc) / newScale - bounds.height) / 2 : (bounds.top + bounds.bot) / 2,
@@ -1413,7 +1412,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
presentation_transition: 500,
annotationOn: this.Document,
});
- PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), pannable: !this.Document.isGroup, type_collection: true, filters: true } }, this.Document);
+ PresBox.pinDocView(
+ anchor,
+ { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ? { ...pinProps.pinData, poslayoutview: pinProps.pinData.dataview } : {}), pannable: !this.Document.isGroup, type_collection: true, filters: true } },
+ this.Document
+ );
if (addAsAnnotation) {
if (Cast(this.dataDoc[this._props.fieldKey + '_annotations'], listSpec(Doc), null) !== undefined) {
@@ -1763,8 +1766,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
zoomScaling={this.zoomScaling}
layoutDoc={this.layoutDoc}
isAnnotationOverlay={this.isAnnotationOverlay}
- cachedCenteringShiftX={this.cachedCenteringShiftX}
- cachedCenteringShiftY={this.cachedCenteringShiftY}
+ centeringShiftX={this.centeringShiftX}
+ centeringShiftY={this.centeringShiftY}
/>
</div>
);
@@ -1812,7 +1815,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
}
@computed get nativeDimScaling() {
- if (this._firstRender || (this._props.isAnnotationOverlay && !this._props.annotationLayerHostsContent)) return 0;
+ if (this._firstRender || (this._props.isAnnotationOverlay && !this._props.annotationLayerHostsContent)) return 1;
const nw = this.nativeWidth;
const nh = this.nativeHeight;
const hscale = nh ? this._props.PanelHeight() / nh : 1;
@@ -1866,9 +1869,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
style={{
pointerEvents: this._props.isContentActive() && SnappingManager.IsDragging ? 'all' : (this._props.pointerEvents?.() as any),
textAlign: this.isAnnotationOverlay ? 'initial' : undefined,
- transform: `scale(${this.nativeDimScaling || 1})`,
- width: `${100 / (this.nativeDimScaling || 1)}%`,
- height: this._props.getScrollHeight?.() ?? `${100 / (this.nativeDimScaling || 1)}%`,
+ transform: `scale(${this.nativeDimScaling})`,
+ width: `${100 / this.nativeDimScaling}%`,
+ height: this._props.getScrollHeight?.() ?? `${100 / this.nativeDimScaling}%`,
}}>
{this.paintFunc ? (
<FormattedTextBox {...this.props} /> // need this so that any live dashfieldviews will update the underlying text that the code eval reads
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
index df023b00f..6a956f2ac 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
@@ -28,7 +28,7 @@ import { CollectionSubView } from '../CollectionSubView';
import './CollectionSchemaView.scss';
import { SchemaColumnHeader } from './SchemaColumnHeader';
import { SchemaRowBox } from './SchemaRowBox';
-const { default: { SCHEMA_NEW_NODE_HEIGHT } } = require('../../global/globalCssVariables.module.scss'); // prettier-ignore
+const { SCHEMA_NEW_NODE_HEIGHT } = require('../../global/globalCssVariables.module.scss'); // prettier-ignore
export enum ColumnType {
Number,
diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.scss b/src/client/views/nodes/DataVizBox/DataVizBox.scss
index 6b5738790..e9a346fbe 100644
--- a/src/client/views/nodes/DataVizBox/DataVizBox.scss
+++ b/src/client/views/nodes/DataVizBox/DataVizBox.scss
@@ -32,6 +32,10 @@
.liveSchema-checkBox {
margin-bottom: -35px;
}
+
+ .displaySchemaLive {
+ margin-bottom: 20px;
+ }
.dataviz-sidebar {
position: absolute;
diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
index 22f1f7b79..60c5fdba2 100644
--- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx
+++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
@@ -18,7 +18,7 @@ import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../../DocComponen
import { MarqueeAnnotator } from '../../MarqueeAnnotator';
import { SidebarAnnos } from '../../SidebarAnnos';
import { AnchorMenu } from '../../pdf/AnchorMenu';
-import { GPTPopup } from '../../pdf/GPTPopup/GPTPopup';
+import { GPTPopup, GPTPopupMode } from '../../pdf/GPTPopup/GPTPopup';
import { DocumentView } from '../DocumentView';
import { FocusViewOptions, FieldView, FieldViewProps } from '../FieldView';
import { PinProps } from '../trails';
@@ -28,6 +28,7 @@ import { LineChart } from './components/LineChart';
import { PieChart } from './components/PieChart';
import { TableBox } from './components/TableBox';
import { Checkbox } from '@mui/material';
+import { ContextMenu } from '../../ContextMenu';
export enum DataVizView {
TABLE = 'table',
@@ -43,6 +44,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im
private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
private _disposers: { [name: string]: IReactionDisposer } = {};
anchorMenuClick?: () => undefined | ((anchor: Doc) => void);
+ sidebarAddDoc: ((doc: Doc | Doc[], sidebarKey?: string | undefined) => boolean) | undefined;
crop: ((region: Doc | undefined, addCrop?: boolean) => Doc | undefined) | undefined;
@observable _marqueeing: number[] | undefined = undefined;
@observable _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>();
@@ -400,8 +402,28 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im
@action
changeLiveSchemaCheckbox = () => {
- this.layoutDoc.dataViz_schemaLive = !this.layoutDoc.dataViz_schemaLive;
- };
+ this.layoutDoc.dataViz_schemaLive = !this.layoutDoc.dataViz_schemaLive
+ }
+
+ specificContextMenu = (e: React.MouseEvent): void => {
+ const cm = ContextMenu.Instance;
+ const options = cm.findByDescription('Options...');
+ const optionItems = options && 'subitems' in options ? options.subitems : [];
+ optionItems.push({ description: `Analyze with AI`, event: () => this.askGPT(), icon: 'lightbulb' });
+ !options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'eye' });
+ }
+
+
+ askGPT = action(async () => {
+ GPTPopup.Instance.setSidebarId('data_sidebar');
+ GPTPopup.Instance.addDoc = this.sidebarAddDocument;
+ GPTPopup.Instance.setDataJson("");
+ GPTPopup.Instance.setMode(GPTPopupMode.DATA);
+ let data = DataVizBox.dataset.get(CsvCast(this.dataDoc[this.fieldKey]).url.href);
+ let input = JSON.stringify(data);
+ GPTPopup.Instance.setDataJson(input);
+ GPTPopup.Instance.generateDataAnalysis();
+ });
render() {
const scale = this._props.NativeDimScaling?.() || 1;
@@ -419,6 +441,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im
transform: `scale(${scale})`,
position: 'absolute',
}}
+ onContextMenu={this.specificContextMenu}
onWheel={e => e.stopPropagation()}
ref={this._mainCont}>
<div className="datatype-button">
@@ -428,11 +451,13 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im
<Toggle text={'PIE CHART'} toggleType={ToggleType.BUTTON} type={Type.SEC} color={'black'} onClick={e => (this.layoutDoc._dataViz = DataVizView.PIECHART)} toggleStatus={this.layoutDoc._dataViz == -DataVizView.PIECHART} />
</div>
- {this.layoutDoc && this.layoutDoc.dataViz_asSchema ? (
- <div className={'liveSchema-checkBox'} style={{ width: this._props.width }}>
+ {(this.layoutDoc && this.layoutDoc.dataViz_asSchema)?(
+ <div className={'displaySchemaLive'}>
+ <div className={'liveSchema-checkBox'} style={{ width: this._props.width }}>
<Checkbox color="primary" onChange={this.changeLiveSchemaCheckbox} checked={this.layoutDoc.dataViz_schemaLive as boolean} />
Display Live Updates to Canvas
</div>
+ </div>
) : null}
{this.renderVizView}
diff --git a/src/client/views/nodes/DataVizBox/components/Chart.scss b/src/client/views/nodes/DataVizBox/components/Chart.scss
index 41ce637ac..cf0007cfd 100644
--- a/src/client/views/nodes/DataVizBox/components/Chart.scss
+++ b/src/client/views/nodes/DataVizBox/components/Chart.scss
@@ -120,11 +120,62 @@
}
}
}
-.selectAll-buttons {
- display: flex;
- flex-direction: row;
- justify-content: flex-end;
+.tableBox-selectButtons {
margin-top: 5px;
- margin-right: 10px;
- float: right;
+ margin-left: 25px;
+ display: inline-block;
+ padding: 2px;
+ .tableBox-selectTitle {
+ display: inline-flex;
+ flex-direction: row;
+ }
+ .tableBox-filtering {
+ display: flex;
+ flex-direction: row;
+ float: right;
+ margin-right: 10px;
+ .tableBox-filterAll {
+ min-width: 75px;
+ }
+ }
+}
+
+.tableBox-filterPopup {
+ background: $light-gray;
+ position: absolute;
+ min-width: 235px;
+ top: 60px;
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ z-index: 2;
+ padding: 7px;
+ border-radius: 5px;
+ margin: 3px;
+ .tableBox-filterPopup-selectColumn {
+ margin-top: 5px;
+ flex-direction: row;
+ .tableBox-filterPopup-selectColumn-each {
+ margin-left: 25px;
+ border-radius: 3px;
+ background: $light-gray;
+ }
+ }
+ .tableBox-filterPopup-setValue {
+ margin-top: 5px;
+ display: flex;
+ flex-direction: row;
+ .tableBox-filterPopup-setValue-each {
+ margin-right: 5px;
+ border-radius: 3px;
+ background: $light-gray;
+ }
+ .tableBox-filterPopup-setValue-input {
+ margin: 5px;
+ }
+ }
+ .tableBox-filterPopup-setFilter {
+ margin-top: 5px;
+ align-self: center;
+ }
}
diff --git a/src/client/views/nodes/DataVizBox/components/TableBox.tsx b/src/client/views/nodes/DataVizBox/components/TableBox.tsx
index 1b239b5e5..67e1c67bd 100644
--- a/src/client/views/nodes/DataVizBox/components/TableBox.tsx
+++ b/src/client/views/nodes/DataVizBox/components/TableBox.tsx
@@ -12,7 +12,8 @@ import { ObservableReactComponent } from '../../../ObservableReactComponent';
import { DocumentView } from '../../DocumentView';
import { DataVizView } from '../DataVizBox';
import './Chart.scss';
-const { default: { DATA_VIZ_TABLE_ROW_HEIGHT } } = require('../../../global/globalCssVariables.module.scss'); // prettier-ignore
+import { undoable } from '../../../../util/UndoManager';
+const { DATA_VIZ_TABLE_ROW_HEIGHT } = require('../../../global/globalCssVariables.module.scss'); // prettier-ignore
interface TableBoxProps {
Document: Doc;
layoutDoc: Doc;
@@ -37,6 +38,13 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> {
_inputChangedDisposer?: IReactionDisposer;
_containerRef: HTMLDivElement | null = null;
+ @observable settingTitle: boolean = false; // true when setting a title column
+ @observable hasRowsToFilter: boolean = false; // true when any rows are selected
+ @observable filtering: boolean = false; // true when the filtering menu is open
+ @observable filteringColumn: any = ''; // column to filter
+ @observable filteringType: string = 'Value'; // "Value" or "Range"
+ filteringVal: any[] = ['', '']; // value or range to filter the column with
+
@observable _scrollTop = -1;
@observable _tableHeight = 0;
@observable _tableContainerHeight = 0;
@@ -49,6 +57,8 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> {
// if the tableData changes (ie., when records are selected by the parent (input) visulization),
// then we need to remove any selected rows that are no longer part of the visualized dataset.
this._inputChangedDisposer = reaction(() => this._tableData.slice(), this.filterSelectedRowsDown, { fireImmediately: true });
+ const selected = NumListCast(this._props.layoutDoc.dataViz_selectedRows);
+ if (selected.length > 0) this.hasRowsToFilter = true;
this.handleScroll();
}
componentWillUnmount() {
@@ -64,9 +74,6 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> {
@computed get parentViz() {
return DocCast(this._props.Document.dataViz_parentViz);
- // return LinkManager.Instance.getAllRelatedLinks(this._props.Document) // out of all links
- // .filter(link => link.link_anchor_1 == this._props.Document.dataViz_parentViz) // get links where this chart doc is the target of the link
- // .map(link => DocCast(link.link_anchor_1)); // then return the source of the link
}
@computed get columns() {
@@ -115,6 +122,7 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> {
} else selected?.push(rowId);
}
e.stopPropagation();
+ this.hasRowsToFilter = selected.length > 0 ? true : false;
};
columnPointerDown = (e: React.PointerEvent, col: string) => {
@@ -155,15 +163,15 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> {
},
emptyFunction,
action(e => {
- if (e.shiftKey){
- if (this._props.titleCol == col) this._props.titleCol = "";
+ if (e.shiftKey || this.settingTitle) {
+ if (this.settingTitle) this.settingTitle = false;
+ if (this._props.titleCol == col) this._props.titleCol = '';
else this._props.titleCol = col;
this._props.selectTitleCol(this._props.titleCol);
- }
- else{
+ } else {
const newAxes = this._props.axes;
if (newAxes.includes(col)) newAxes.splice(newAxes.indexOf(col), 1);
- else if (newAxes.length > 2) newAxes[newAxes.length-1] = col;
+ else if (newAxes.length > 2) newAxes[newAxes.length - 1] = col;
else newAxes.push(col);
this._props.selectAxes(newAxes);
}
@@ -171,6 +179,134 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> {
);
};
+ /**
+ * These functions handle the filtering popup for when the "filter" button is pressed to select rows
+ */
+ filter = undoable((e: any) => {
+ var start: any;
+ var end: any;
+ if (this.filteringType == 'Range') {
+ start = (this.filteringVal[0] as Number) ? Number(this.filteringVal[0]) : this.filteringVal[0];
+ end = (this.filteringVal[1] as Number) ? Number(this.filteringVal[1]) : this.filteringVal[0];
+ }
+
+ this._tableDataIds.forEach(rowID => {
+ if (this.filteringType == 'Value') {
+ if (this._props.records[rowID][this.filteringColumn] == this.filteringVal[0]) {
+ if (!NumListCast(this._props.layoutDoc.dataViz_selectedRows).includes(rowID)) {
+ this.tableRowClick(e, rowID);
+ }
+ }
+ } else {
+ let compare = this._props.records[rowID][this.filteringColumn];
+ if (compare as Number) compare = Number(compare);
+ if (start <= compare && compare <= end) {
+ if (!NumListCast(this._props.layoutDoc.dataViz_selectedRows).includes(rowID)) {
+ this.tableRowClick(e, rowID);
+ }
+ }
+ }
+ });
+ this.filtering = false;
+ this.filteringColumn = '';
+ this.filteringVal = ['', ''];
+ }, 'filter table');
+ @action
+ setFilterColumn = (e: any) => {
+ this.filteringColumn = e.currentTarget.value;
+ };
+ @action
+ setFilterType = (e: any) => {
+ this.filteringType = e.currentTarget.value;
+ };
+ changeFilterValue = action((e: React.ChangeEvent<HTMLInputElement>) => {
+ this.filteringVal[0] = e.target.value;
+ });
+ changeFilterRange0 = action((e: React.ChangeEvent<HTMLInputElement>) => {
+ this.filteringVal[0] = e.target.value;
+ });
+ changeFilterRange1 = action((e: React.ChangeEvent<HTMLInputElement>) => {
+ this.filteringVal[1] = e.target.value;
+ });
+ @computed get renderFiltering() {
+ if (this.filteringColumn === '') this.filteringColumn = this.columns[0];
+ return (
+ <div className="tableBox-filterPopup" style={{ right: this._props.width * 0.05 }}>
+ <div className="tableBox-filterPopup-selectColumn">
+ Column:
+ <select className="tableBox-filterPopup-selectColumn-each" value={this.filteringColumn != '' ? this.filteringColumn : this.columns[0]} onChange={e => this.setFilterColumn(e)}>
+ {this.columns.map(column => (
+ <option className="" key={column} value={column}>
+ {' '}
+ {column}{' '}
+ </option>
+ ))}
+ </select>
+ </div>
+ <div className="tableBox-filterPopup-setValue">
+ <select className="tableBox-filterPopup-setValue-each" value={this.filteringType} onChange={e => this.setFilterType(e)}>
+ <option className="" key={'Value'} value={'Value'}>
+ {' '}
+ {'Value'}{' '}
+ </option>
+ <option className="" key={'Range'} value={'Range'}>
+ {' '}
+ {'Range'}{' '}
+ </option>
+ </select>
+ :
+ {this.filteringType == 'Value' ? (
+ <input
+ className="tableBox-filterPopup-setValue-input"
+ defaultValue=""
+ autoComplete="off"
+ onChange={this.changeFilterValue}
+ onKeyDown={e => {
+ e.stopPropagation();
+ }}
+ type="text"
+ placeholder=""
+ id="search-input"
+ />
+ ) : (
+ <div>
+ <input
+ className="tableBox-filterPopup-setValue-input"
+ defaultValue=""
+ autoComplete="off"
+ onChange={this.changeFilterRange0}
+ onKeyDown={e => {
+ e.stopPropagation();
+ }}
+ type="text"
+ placeholder=""
+ id="search-input"
+ style={{ width: this._props.width * 0.15 }}
+ />
+ to
+ <input
+ className="tableBox-filterPopup-setValue-input"
+ defaultValue=""
+ autoComplete="off"
+ onChange={this.changeFilterRange1}
+ onKeyDown={e => {
+ e.stopPropagation();
+ }}
+ type="text"
+ placeholder=""
+ id="search-input"
+ style={{ width: this._props.width * 0.15 }}
+ />
+ </div>
+ )}
+ </div>
+ <div className="tableBox-filterPopup-setFilter">
+ <Button onClick={action(e => this.filter(e))} text="Set Filter" type={Type.SEC} color={'black'} />
+ </div>
+ </div>
+ );
+ }
+
render() {
if (this._tableData.length > 0) {
return (
@@ -184,9 +320,39 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> {
this._props.layoutDoc.dataViz_selectedRows = new List<number>(this._tableDataIds);
}
}}>
- <div className="selectAll-buttons">
- <Button onClick={action(() => (this._props.layoutDoc.dataViz_selectedRows = new List<number>(this._tableDataIds)))} text="Select All" type={Type.SEC} color={'black'} />
- <Button onClick={action(() => (this._props.layoutDoc.dataViz_selectedRows = new List<number>()))} text="Deselect All" type={Type.SEC} color={'black'} />
+ <div className="tableBox-selectButtons">
+ <div className="tableBox-selectTitle">
+ <Button onClick={action(() => (this.settingTitle = !this.settingTitle))} text="Select Title Column" type={Type.SEC} color={'black'} />
+ </div>
+ <div className="tableBox-filtering">
+ {this.filtering ? this.renderFiltering : null}
+ <Button onClick={action(() => (this.filtering = !this.filtering))} text="Filter" type={Type.SEC} color={'black'} />
+ <div className="tableBox-filterAll">
+ {this.hasRowsToFilter ? (
+ <Button
+ onClick={action(() => {
+ this._props.layoutDoc.dataViz_selectedRows = new List<number>();
+ this.hasRowsToFilter = false;
+ })}
+ text="Deselect All"
+ type={Type.SEC}
+ color={'black'}
+ tooltip="Select rows to be displayed in any DataViz boxes dragged off of this one."
+ />
+ ) : (
+ <Button
+ onClick={action(() => {
+ this._props.layoutDoc.dataViz_selectedRows = new List<number>(this._tableDataIds);
+ this.hasRowsToFilter = true;
+ })}
+ text="Select All"
+ type={Type.SEC}
+ color={'black'}
+ tooltip="Select rows to be displayed in any DataViz boxes dragged off of this one."
+ />
+ )}
+ </div>
+ </div>
</div>
<div
className={`tableBox-container ${this.columns[0]}`}
@@ -220,15 +386,23 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> {
<th
key={this.columns.indexOf(col)}
style={{
- color: this._props.axes.slice().reverse().lastElement() === col ? 'darkgreen'
- : (this._props.axes.length>2 && this._props.axes.lastElement() === col) ? 'darkred'
- : (this._props.axes.lastElement()===col || (this._props.axes.length>2 && this._props.axes[1]==col))? 'darkblue' : undefined,
- background: this._props.axes.slice().reverse().lastElement() === col ? '#E3fbdb'
- : (this._props.axes.length>2 && this._props.axes.lastElement() === col) ? '#Fbdbdb'
- : (this._props.axes.lastElement()===col || (this._props.axes.length>2 && this._props.axes[1]==col))? '#c6ebf7' : undefined,
- // blue: #ADD8E6
- // green: #E3fbdb
- // red: #Fbdbdb
+ color:
+ this._props.axes.slice().reverse().lastElement() === col
+ ? 'darkgreen'
+ : this._props.axes.length > 2 && this._props.axes.lastElement() === col
+ ? 'darkred'
+ : this._props.axes.lastElement() === col || (this._props.axes.length > 2 && this._props.axes[1] == col)
+ ? 'darkblue'
+ : undefined,
+ background: this.settingTitle
+ ? 'lightgrey'
+ : this._props.axes.slice().reverse().lastElement() === col
+ ? '#E3fbdb'
+ : this._props.axes.length > 2 && this._props.axes.lastElement() === col
+ ? '#Fbdbdb'
+ : this._props.axes.lastElement() === col || (this._props.axes.length > 2 && this._props.axes[1] == col)
+ ? '#c6ebf7'
+ : undefined,
fontWeight: 'bolder',
border: '3px solid black',
}}
@@ -251,10 +425,10 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> {
}}>
{this.columns.map(col => {
var colSelected = false;
- if (this._props.axes.length>2) colSelected = this._props.axes[0]==col || this._props.axes[1]==col || this._props.axes[2]==col;
- else if (this._props.axes.length>1) colSelected = this._props.axes[0]==col || this._props.axes[1]==col;
- else if (this._props.axes.length>0) colSelected = this._props.axes[0]==col;
- if (this._props.titleCol==col) colSelected = true;
+ if (this._props.axes.length > 2) colSelected = this._props.axes[0] == col || this._props.axes[1] == col || this._props.axes[2] == col;
+ else if (this._props.axes.length > 1) colSelected = this._props.axes[0] == col || this._props.axes[1] == col;
+ else if (this._props.axes.length > 0) colSelected = this._props.axes[0] == col;
+ if (this._props.titleCol == col) colSelected = true;
return (
<td key={this.columns.indexOf(col)} style={{ border: colSelected ? '3px solid black' : '1px solid black', fontWeight: colSelected ? 'bolder' : 'normal' }}>
<div className="tableBox-cell">{this._props.records[rowId][col]}</div>
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index fc2da18d9..ee7bbbdba 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1382,6 +1382,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
}
}
};
+ backgroundColor = () => this._docViewInternal?.backgroundBoxColor;
DataTransition = () => this._props.DataTransition?.() || StrCast(this.Document.dataTransition);
ShouldNotScale = () => this.shouldNotScale;
NativeWidth = () => this.effectiveNativeWidth;
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 469869e21..bb1f70f97 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -1,13 +1,15 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Tooltip } from '@mui/material';
-import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction } from 'mobx';
+import { Colors } from 'browndash-components';
+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';
-import { Doc, DocListCast, Opt } from '../../../fields/Doc';
+import { Doc, 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 { Cast, ImageCast, NumCast, StrCast } from '../../../fields/Types';
import { ImageField } from '../../../fields/URLField';
@@ -15,6 +17,7 @@ import { TraceMobx } from '../../../fields/util';
import { DashColor, emptyFunction, returnEmptyString, returnFalse, returnOne, returnZero, setupMoveUpEvents, Utils } from '../../../Utils';
import { Docs, DocUtils } from '../../documents/Documents';
import { DocumentType } from '../../documents/DocumentTypes';
+import { Networking } from '../../Network';
import { DocumentManager } from '../../util/DocumentManager';
import { DragManager } from '../../util/DragManager';
import { undoBatch } from '../../util/UndoManager';
@@ -23,35 +26,39 @@ import { CollectionFreeFormView } from '../collections/collectionFreeForm/Collec
import { ContextMenuProps } from '../ContextMenuItem';
import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../DocComponent';
import { MarqueeAnnotator } from '../MarqueeAnnotator';
+import { OverlayView } from '../OverlayView';
import { AnchorMenu } from '../pdf/AnchorMenu';
import { StyleProp } from '../StyleProvider';
import { OpenWhere } from './DocumentView';
-import { FocusViewOptions, FieldView, FieldViewProps } from './FieldView';
+import { FieldView, FieldViewProps, FocusViewOptions } from './FieldView';
import './ImageBox.scss';
import { PinProps, PresBox } from './trails';
-import { Colors } from 'browndash-components';
-import { listSpec } from '../../../fields/Schema';
-import { List } from '../../../fields/List';
-import { url } from 'inspector';
-import { OverlayView } from '../OverlayView';
-import { Networking } from '../../Network';
+export class ImageEditorData {
+ private static _instance: ImageEditorData;
+ private static get imageData() { return (ImageEditorData._instance ?? new ImageEditorData()).imageData; } // prettier-ignore
+ @observable imageData: { rootDoc: Doc | undefined; open: boolean; source: string; addDoc: Opt<(doc: Doc | Doc[], annotationKey?: string) => boolean> } = observable({ rootDoc: undefined, open: false, source: '', addDoc: undefined });
+ @action private static set = (open: boolean, rootDoc: Doc | undefined, source: string, addDoc: Opt<(doc: Doc | Doc[], annotationKey?: string) => boolean>) => (this._instance.imageData = { open, rootDoc, source, addDoc });
+
+ constructor() {
+ makeObservable(this);
+ ImageEditorData._instance = this;
+ }
+
+ public static get Open() { return ImageEditorData.imageData.open; } // prettier-ignore
+ public static get Source() { return ImageEditorData.imageData.source; } // prettier-ignore
+ public static get RootDoc() { return ImageEditorData.imageData.rootDoc; } // prettier-ignore
+ public static get AddDoc() { return ImageEditorData.imageData.addDoc; } // prettier-ignore
+ public static set Open(open: boolean) { ImageEditorData.set(open, this.imageData.rootDoc, this.imageData.source, this.imageData.addDoc); } // prettier-ignore
+ public static set Source(source: string) { ImageEditorData.set(this.imageData.open, this.imageData.rootDoc, source, this.imageData.addDoc); } // prettier-ignore
+ public static set RootDoc(rootDoc: Opt<Doc>) { ImageEditorData.set(this.imageData.open, rootDoc, this.imageData.source, this.imageData.addDoc); } // prettier-ignore
+ public static set AddDoc(addDoc: Opt<(doc: Doc | Doc[], annotationKey?: string) => boolean>) { ImageEditorData.set(this.imageData.open, this.imageData.rootDoc, this.imageData.source, addDoc); } // prettier-ignore
+}
@observer
export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implements ViewBoxInterface {
public static LayoutString(fieldKey: string) {
return FieldView.LayoutString(ImageBox, fieldKey);
}
-
- @observable public static imageRootDoc: Doc | undefined = undefined;
- @observable public static imageEditorOpen: boolean = false;
- @observable public static imageEditorSource: string = '';
- @observable public static addDoc: ((doc: Doc | Doc[], annotationKey?: string) => boolean) | undefined = undefined;
- @action public static setImageEditorOpen(open: boolean) {
- ImageBox.imageEditorOpen = open;
- }
- @action public static setImageEditorSource(source: string) {
- ImageBox.imageEditorSource = source;
- }
private _ignoreScroll = false;
private _forcedScroll = false;
private _dropDisposer?: DragManager.DragDropDisposer;
@@ -248,10 +255,10 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
funcs.push({
description: 'Open Image Editor',
event: action(() => {
- ImageBox.setImageEditorOpen(true);
- ImageBox.setImageEditorSource(this.choosePath(field.url));
- ImageBox.addDoc = this._props.addDocument;
- ImageBox.imageRootDoc = this.Document;
+ ImageEditorData.Open = true;
+ ImageEditorData.Source = this.choosePath(field.url);
+ ImageEditorData.AddDoc = this._props.addDocument;
+ ImageEditorData.RootDoc = this.Document;
}),
icon: 'pencil-alt',
});
diff --git a/src/client/views/nodes/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx
index 0a4325d8c..ff1e62885 100644
--- a/src/client/views/nodes/LinkAnchorBox.tsx
+++ b/src/client/views/nodes/LinkAnchorBox.tsx
@@ -13,7 +13,7 @@ import { StyleProp } from '../StyleProvider';
import { FieldView, FieldViewProps } from './FieldView';
import './LinkAnchorBox.scss';
import { LinkInfo } from './LinkDocPreview';
-const { default: { MEDIUM_GRAY }, } = require('../global/globalCssVariables.module.scss'); // prettier-ignore
+const { MEDIUM_GRAY } = require('../global/globalCssVariables.module.scss'); // prettier-ignore
@observer
export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() {
public static LayoutString(fieldKey: string) {
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss
index 3dcc45c96..38dd2e847 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.scss
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss
@@ -350,6 +350,7 @@ footnote::before {
span {
font-family: inherit;
background-color: inherit;
+ display: contents; // fixes problem where extra space is added around <ol> lists when inside a prosemirror span
}
blockquote {
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index c2f3a6e4b..43010b2ed 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -8,7 +8,7 @@ import { history } from 'prosemirror-history';
import { inputRules } from 'prosemirror-inputrules';
import { keymap } from 'prosemirror-keymap';
import { Fragment, Mark, Node, Slice } from 'prosemirror-model';
-import { EditorState, NodeSelection, Plugin, TextSelection, Transaction } from 'prosemirror-state';
+import { EditorState, NodeSelection, Plugin, Selection, TextSelection, Transaction } from 'prosemirror-state';
import { EditorView } from 'prosemirror-view';
import * as React from 'react';
import { BsMarkdownFill } from 'react-icons/bs';
@@ -983,10 +983,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
animateRes = (resIndex: number, newText: string) => {
if (resIndex < newText.length) {
const marks = this._editorView?.state.storedMarks ?? [];
- this._editorView?.dispatch(this._editorView.state.tr.setStoredMarks(marks).insertText(newText[resIndex]).setStoredMarks(marks));
- setTimeout(() => {
- this.animateRes(resIndex + 1, newText);
- }, 20);
+ this._editorView?.dispatch(this._editorView?.state.tr.insertText(newText[resIndex]).setStoredMarks(marks));
+ setTimeout(() => this.animateRes(resIndex + 1, newText), 20);
}
};
@@ -994,13 +992,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
try {
let res = await gptAPICall((this.dataDoc.text as RichTextField)?.Text, GPTCallType.COMPLETION);
if (!res) {
- console.error('GPT call failed');
this.animateRes(0, 'Something went wrong.');
- } else {
- this.animateRes(0, res);
+ } else if (this._editorView) {
+ const { dispatch, state } = this._editorView;
+ // for no animation, use: dispatch(state.tr.insertText(res));
+ // for animted response starting at end of text, use:
+ dispatch(state.tr.setSelection(Selection.atEnd(state.doc)));
+ this.animateRes(0, '\n\n' + res);
}
} catch (err) {
- console.error('GPT call failed');
+ console.error(err);
this.animateRes(0, 'Something went wrong.');
}
});
@@ -1484,6 +1485,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
this._editorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(tr.doc.content.size))));
} else if (curText && !FormattedTextBox.DontSelectInitialText) {
selectAll(this._editorView.state, this._editorView?.dispatch);
+ this.tryUpdateDoc(true); // calling select() above will make isContentActive() true only after a render .. which means the selectAll() above won't write to the Document and the incomingValue will overwrite the selection with the non-updated data
}
}
if (selectOnLoad) {
diff --git a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
index ec8879487..03c902580 100644
--- a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
+++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
@@ -257,7 +257,7 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
});
// backspace = chainCommands(deleteSelection, joinBackward, selectNodeBackward);
- const backspace = (state: EditorState, dispatch: (tx: Transaction) => void, view: EditorView, once = true) => {
+ const backspace = (state: EditorState, dispatch: (tx: Transaction) => void, view: EditorView) => {
if (props.onKey?.(event, props)) return true;
if (!canEdit(state)) return true;
@@ -269,7 +269,10 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
if (
!joinBackward(state, (tx: Transaction) => {
dispatch(updateBullets(tx, schema));
- if (once && view.state.selection.$from.depth > 1 && view.state.selection.$from.node(view.state.selection.$from.depth - 1).type === view.state.schema.nodes.list_item) backspace(view.state, view.dispatch, view, false);
+ if (view.state.selection.$anchor.node(-1)?.type === schema.nodes.list_item) {
+ // gets rid of an extra paragraph when joining two list items together.
+ joinBackward(view.state, (tx: Transaction) => view.dispatch(tx));
+ }
})
) {
if (
@@ -296,7 +299,7 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
const depth = trange ? liftTarget(trange) : null;
if (
depth !== null &&
- state.selection.$from.node(state.selection.$from.depth - 1)?.type === state.schema.nodes.blockquote && //
+ state.selection.$from.node(-1)?.type === state.schema.nodes.blockquote && //
!state.selection.$from.node().content.size &&
trange
) {
@@ -306,7 +309,13 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
if (!newlineInCode(state, dispatch as any)) {
- if (once && view.state.selection.$from.depth > 1 && !view.state.selection.$from.nodeBefore && !view.state.selection.$from.nodeBefore) {
+ const olNode = view.state.selection.$anchor.node(-2);
+ const liNode = view.state.selection.$anchor.node(-1);
+ // prettier-ignore
+ if (liNode?.type === schema.nodes.list_item && !liNode.textContent &&
+ olNode?.type === schema.nodes.ordered_list && once && view.state.selection.$from.depth === 3)
+ {
+ // handles case of hitting enter at then end of a top-level empty list item - the result is to create a paragraph
for (let i = 0; i < 10 && view.state.selection.$from.depth > 1 && liftListItem(schema.nodes.list_item)(view.state, view.dispatch); i++);
} else if (
!splitListItem(schema.nodes.list_item)(state as any, (tx2: Transaction) => {
@@ -314,32 +323,32 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
marks && tx3.ensureMarks([...marks]);
marks && tx3.setStoredMarks([...marks]);
dispatch(tx3);
+ // removes an extra paragraph created when selecting text across two list items or splitting an empty list item
+ !once && view.dispatch(view.state.tr.deleteRange(view.state.selection.from - 5, view.state.selection.from - 2));
})
) {
- const fromattrs = state.selection.$from.node().attrs;
- if (
- !splitBlockKeepMarks(state, (tx3: Transaction) => {
- const tonode = tx3.selection.$to.node();
- if (tx3.selection.to && tx3.doc.nodeAt(tx3.selection.to - 1)) {
- const tx4 = tx3.setNodeMarkup(tx3.selection.to - 1, tonode.type, fromattrs, tonode.marks);
- dispatch(tx4);
- if (
- view.state.selection.$from.parentOffset && //
- !view.state.selection.$from.node().content.size
- )
- liftListItem(schema.nodes.list_item)(view.state, view.dispatch);
- else if (
- once &&
- view.state.selection.$from.parentOffset &&
- view.state.selection.$from.depth > 1 && //
- view.state.selection.$from.node(view.state.selection.$from.depth - 1).type === schema.nodes.list_item
- )
- enter(view.state, view.dispatch, view, false);
- else if (once && depth && !view.state.selection.$from.parentOffset) backspace(view.state, view.dispatch, view, false);
- } else dispatch(tx3.insertText('\r\n'));
- })
- ) {
- return false;
+ if (once && view.state.selection.$from.node(-2)?.type === schema.nodes.ordered_list && view.state.selection.$from.node(-1)?.type === schema.nodes.list_item && view.state.selection.$from.node(-1)?.textContent === '') {
+ // handles case of hitting enter on an empty list item which needs to create a second empty paragraph, then split it by calling enter() again
+ view.dispatch(view.state.tr.insert(view.state.selection.from, schema.nodes.paragraph.create({})));
+ enter(view.state, view.dispatch, view, false);
+ } else {
+ const fromattrs = state.selection.$from.node().attrs;
+ if (
+ !splitBlockKeepMarks(state, (tx3: Transaction) => {
+ const tonode = tx3.selection.$to.node();
+ if (tx3.selection.to && tx3.doc.nodeAt(tx3.selection.to - 1)) {
+ const tx4 = tx3.setNodeMarkup(tx3.selection.to - 1, tonode.type, fromattrs, tonode.marks);
+ dispatch(tx4);
+ }
+
+ if (view.state.selection.$anchor.nodeAfter?.type === schema.nodes.text && once) {
+ // if text is selected across list items, then we need to forcibly insert a new line since the splitBlock code joins the two list items.
+ enter(view.state, dispatch, view, false);
+ }
+ })
+ ) {
+ return false;
+ }
}
}
}
diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx
index 4bd4ca72b..cecf106a3 100644
--- a/src/client/views/nodes/formattedText/RichTextMenu.tsx
+++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx
@@ -404,39 +404,23 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
// remove all node type and apply the passed-in one to the selected text
changeListType = (mapStyle: string) => {
const active = this.view?.state && RichTextMenu.Instance?.getActiveListStyle();
- const nodeType = this.view?.state.schema.nodes.ordered_list.create({ mapStyle: active === mapStyle ? '' : mapStyle });
- if (!this.view || nodeType?.attrs.mapStyle === '') return;
-
- const nextIsOL = this.view.state.selection.$from.nodeAfter?.type === schema.nodes.ordered_list;
- let inList: any = undefined;
- let fromList = -1;
- const path: any = Array.from((this.view.state.selection.$from as any).path);
- for (let i = 0; i < path.length; i++) {
- if (path[i]?.type === schema.nodes.ordered_list) {
- inList = path[i];
- fromList = path[i - 1];
- }
- }
+ const newMapStyle = active === mapStyle ? '' : mapStyle;
+ if (!this.view || newMapStyle === '') return;
+ let inList = this.view.state.selection.$anchor.node(1).type === schema.nodes.ordered_list;
const marks = this.view.state.storedMarks || (this.view.state.selection.$to.parentOffset && this.view.state.selection.$from.marks());
- if (
- inList ||
+ if (inList) {
+ const tx2 = updateBullets(this.view.state.tr, schema, newMapStyle, this.view.state.doc.resolve(this.view.state.selection.$anchor.before(1) + 1).pos, this.view.state.doc.resolve(this.view.state.selection.$anchor.after(1)).pos);
+ marks && tx2.ensureMarks([...marks]);
+ marks && tx2.setStoredMarks([...marks]);
+ this.view.dispatch(tx2);
+ } else
!wrapInList(schema.nodes.ordered_list)(this.view.state, (tx2: any) => {
- const tx3 = updateBullets(tx2, schema, nodeType && (nodeType as any).attrs.mapStyle, this.view!.state.selection.from - 1, this.view!.state.selection.to + 1);
- marks && tx3.ensureMarks([...marks]);
- marks && tx3.setStoredMarks([...marks]);
-
- this.view!.dispatch(tx2);
- })
- ) {
- const tx2 = this.view.state.tr;
- if (nodeType && (inList || nextIsOL)) {
- const tx3 = updateBullets(tx2, schema, nodeType && (nodeType as any).attrs.mapStyle, inList ? fromList : this.view.state.selection.from, inList ? fromList + inList.nodeSize : this.view.state.selection.to);
+ const tx3 = updateBullets(tx2, schema, newMapStyle, this.view!.state.selection.from - 1, this.view!.state.selection.to + 1);
marks && tx3.ensureMarks([...marks]);
marks && tx3.setStoredMarks([...marks]);
- this.view.dispatch(tx3);
- }
- }
+ this.view!.dispatch(tx3);
+ });
this.view.focus();
this.updateMenu(this.view, undefined, this.props, this.layoutDoc);
};
diff --git a/src/client/views/nodes/generativeFill/GenerativeFill.tsx b/src/client/views/nodes/generativeFill/GenerativeFill.tsx
index 87e1b69c3..a485ea4c3 100644
--- a/src/client/views/nodes/generativeFill/GenerativeFill.tsx
+++ b/src/client/views/nodes/generativeFill/GenerativeFill.tsx
@@ -13,7 +13,7 @@ import { DocumentManager } from '../../../util/DocumentManager';
import { CollectionDockingView } from '../../collections/CollectionDockingView';
import { CollectionFreeFormView } from '../../collections/collectionFreeForm';
import { OpenWhereMod } from '../DocumentView';
-import { ImageBox } from '../ImageBox';
+import { ImageBox, ImageEditorData } from '../ImageBox';
import './GenerativeFill.scss';
import Buttons from './GenerativeFillButtons';
import { BrushHandler } from './generativeFillUtils/BrushHandler';
@@ -419,8 +419,8 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD
// Closes the editor view
const handleViewClose = () => {
- ImageBox.setImageEditorOpen(false);
- ImageBox.setImageEditorSource('');
+ ImageEditorData.Open = false;
+ ImageEditorData.Source = '';
if (newCollectionRef.current) {
DocumentManager.Instance.AddViewRenderedCb(newCollectionRef.current, dv => (dv.ComponentView as CollectionFreeFormView)?.fitContentOnce());
}
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index cd9fec839..91fdb90fc 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -324,8 +324,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
// Case 2: Last slide and presLoop is toggled ON or it is in Edit mode
this.nextSlide(0);
progressiveReveal(true); // shows first progressive document, but without a transition effect
+ return 0;
}
- return 0;
+ return false;
}
return this.itemIndex;
};
@@ -963,7 +964,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
const func = () => {
const delay = NumCast(this.activeItem.presentation_duration, this.activeItem.type === DocumentType.SCRIPTING ? 0 : 2500) + NumCast(this.activeItem.presentation_transition);
this._presTimer = setTimeout(() => {
- if (!this.next()) this.layoutDoc.presentation_status = this._exitTrail?.() ?? PresStatus.Manual;
+ if (this.next() === false) this.layoutDoc.presentation_status = this._exitTrail?.() ?? PresStatus.Manual;
this.layoutDoc.presentation_status === PresStatus.Autoplay && func();
}, delay);
};
@@ -1065,7 +1066,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
} else if (doc.type !== DocumentType.PRES) {
if (!doc.presentation_targetDoc) doc.title = doc.title + ' - Slide';
- doc.presentation_targetDoc = doc.createdFrom; // dropped document will be a new embedding of an embedded document somewhere else.
+ doc.presentation_targetDoc = doc.createdFrom ?? doc; // dropped document will be a new embedding of an embedded document somewhere else.
doc.presentation_movement = PresMovement.Zoom;
if (this._expandBoolean) doc.presentation_expandInlineButton = true;
}
diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx
index 5b2aa1cde..28139eb14 100644
--- a/src/client/views/nodes/trails/PresElementBox.tsx
+++ b/src/client/views/nodes/trails/PresElementBox.tsx
@@ -61,7 +61,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
// Since this node is being rendered with a template, this method retrieves
// the actual slide being rendered from the auto-generated rendering template
@computed get slideDoc() {
- return this._props.TemplateDataDocument ?? this.Document;
+ return DocCast(this.Document.rootDocument, this.Document);
}
// this is the document in the workspaces that is targeted by the slide
diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx
index 59f191af0..0b3ba81d3 100644
--- a/src/client/views/pdf/AnchorMenu.tsx
+++ b/src/client/views/pdf/AnchorMenu.tsx
@@ -6,7 +6,6 @@ import * as React from 'react';
import { ColorResult } from 'react-color';
import { Utils, returnFalse, setupMoveUpEvents, unimplementedFunction } from '../../../Utils';
import { Doc, Opt } from '../../../fields/Doc';
-import { GPTCallType, gptAPICall } from '../../apis/gpt/GPT';
import { DocumentType } from '../../documents/DocumentTypes';
import { SelectionManager } from '../../util/SelectionManager';
import { SettingsManager } from '../../util/SettingsManager';
@@ -73,18 +72,8 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
* @param e pointer down event
*/
gptSummarize = async (e: React.PointerEvent) => {
- // move this logic to gptpopup, need to implement generate again
- GPTPopup.Instance.setVisible(true);
- GPTPopup.Instance.setMode(GPTPopupMode.SUMMARY);
- GPTPopup.Instance.setLoading(true);
-
- try {
- const res = await gptAPICall(this.selectedText, GPTCallType.SUMMARY);
- GPTPopup.Instance.setText(res || 'Something went wrong.');
- } catch (err) {
- console.error(err);
- }
- GPTPopup.Instance.setLoading(false);
+ GPTPopup.Instance?.setSelectedText(this.selectedText);
+ GPTPopup.Instance.generateSummary();
};
pointerDown = (e: React.PointerEvent) => {
diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.tsx b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
index da8a88803..29b1ca365 100644
--- a/src/client/views/pdf/GPTPopup/GPTPopup.tsx
+++ b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
@@ -1,5 +1,5 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Button, IconButton, Type } from 'browndash-components';
+import { Button, EditableText, IconButton, Size, Type } from 'browndash-components';
import { action, makeObservable, observable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
@@ -10,7 +10,7 @@ import { Utils } from '../../../../Utils';
import { Doc } from '../../../../fields/Doc';
import { NumCast, StrCast } from '../../../../fields/Types';
import { Networking } from '../../../Network';
-import { gptImageCall } from '../../../apis/gpt/GPT';
+import { GPTCallType, gptAPICall, gptImageCall } from '../../../apis/gpt/GPT';
import { DocUtils, Docs } from '../../../documents/Documents';
import { ObservableReactComponent } from '../../ObservableReactComponent';
import { AnchorMenu } from '../AnchorMenu';
@@ -20,6 +20,7 @@ export enum GPTPopupMode {
SUMMARY,
EDIT,
IMAGE,
+ DATA,
}
interface GPTPopupProps {}
@@ -27,6 +28,7 @@ interface GPTPopupProps {}
@observer
export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
static Instance: GPTPopup;
+ @observable private chatMode: boolean = false;
@observable
public visible: boolean = false;
@@ -46,6 +48,20 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
public setText = (text: string) => {
this.text = text;
};
+ @observable
+ public selectedText: string = '';
+ @action
+ public setSelectedText = (text: string) => {
+ this.selectedText = text;
+ };
+ @observable
+ public dataJson: string = '';
+ public dataChatPrompt: string | null = null;
+ @action
+ public setDataJson = (text: string) => {
+ if (text=="") this.dataChatPrompt = "";
+ this.dataJson = text;
+ };
@observable
public imgDesc: string = '';
@@ -79,6 +95,7 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
@action
public setDone = (done: boolean) => {
this.done = done;
+ this.chatMode = false;
};
// change what can be a ref into a ref
@@ -118,18 +135,46 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
try {
let image_urls = await gptImageCall(this.imgDesc);
+ console.log('Image urls: ', image_urls);
if (image_urls && image_urls[0]) {
const [result] = await Networking.PostToServer('/uploadRemoteImage', { sources: [image_urls[0]] });
+ console.log('Upload result: ', result);
const source = Utils.prepend(result.accessPaths.agnostic.client);
+ console.log('Upload source: ', source);
this.setImgUrls([[image_urls[0], source]]);
}
} catch (err) {
- console.log(err);
- return '';
+ console.error(err);
}
this.setLoading(false);
};
+ generateSummary = async () => {
+ GPTPopup.Instance.setVisible(true);
+ GPTPopup.Instance.setMode(GPTPopupMode.SUMMARY);
+ GPTPopup.Instance.setLoading(true);
+
+ try {
+ const res = await gptAPICall(this.selectedText, GPTCallType.SUMMARY);
+ GPTPopup.Instance.setText(res || 'Something went wrong.');
+ } catch (err) {
+ console.error(err);
+ }
+ GPTPopup.Instance.setLoading(false);
+ }
+
+ generateDataAnalysis = async () => {
+ GPTPopup.Instance.setVisible(true);
+ GPTPopup.Instance.setLoading(true);
+ try {
+ let res = await gptAPICall(this.dataJson, GPTCallType.DATA, this.dataChatPrompt);
+ GPTPopup.Instance.setText(res || 'Something went wrong.');
+ } catch (err) {
+ console.error(err);
+ }
+ GPTPopup.Instance.setLoading(false);
+ }
+
/**
* Transfers the summarization text to a sidebar annotation text document.
*/
@@ -174,6 +219,16 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
DocUtils.MakeLink(textAnchor, newDoc, { link_relationship: 'Image Prompt' });
};
+ /**
+ * Creates a chatbox for analyzing data so that users can ask specific questions.
+ */
+ private chatWithAI = () => {
+ this.chatMode = true;
+ }
+ dataPromptChanged = action((e: React.ChangeEvent<HTMLInputElement>) => {
+ this.dataChatPrompt = e.target.value;
+ });
+
private getPreviewUrl = (source: string) => source.split('.').join('_m.');
constructor(props: GPTPopupProps) {
@@ -213,31 +268,6 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
);
};
- data = () => {
- return (
- <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
- {this.heading('GENERATED IMAGE')}
- <div className="image-content-wrapper">
- {this.imgUrls.map(rawSrc => (
- <div className="img-wrapper">
- <div className="img-container">
- <img key={rawSrc[0]} src={rawSrc[0]} width={150} height={150} alt="dalle generation" />
- </div>
- <div className="btn-container">
- <Button text="Save Image" onClick={() => this.transferToImage(rawSrc[1])} color={StrCast(Doc.UserDoc().userColor)} type={Type.TERT} />
- </div>
- </div>
- ))}
- </div>
- {!this.loading && (
- <>
- <IconButton tooltip="Generate Again" onClick={this.generateImage} icon={<FontAwesomeIcon icon="redo-alt" size="lg" />} color={StrCast(Doc.UserDoc().userVariantColor)} />
- </>
- )}
- </div>
- );
- };
-
summaryBox = () => (
<>
<div>
@@ -255,7 +285,6 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
}, 500);
},
]}
- //cursor={{ hideWhenDone: true }}
/>
) : (
this.text
@@ -266,7 +295,7 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
<div className="btns-wrapper">
{this.done ? (
<>
- <IconButton tooltip="Generate Again" onClick={this.callSummaryApi} icon={<FontAwesomeIcon icon="redo-alt" size="lg" />} color={StrCast(Doc.UserDoc().userVariantColor)} />
+ <IconButton tooltip="Generate Again" onClick={this.generateSummary} icon={<FontAwesomeIcon icon="redo-alt" size="lg" />} color={StrCast(Doc.UserDoc().userVariantColor)} />
<Button tooltip="Transfer to text" text="Transfer To Text" onClick={this.transferToText} color={StrCast(Doc.UserDoc().userVariantColor)} type={Type.TERT} />
</>
) : (
@@ -288,6 +317,65 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
</>
);
+ dataAnalysisBox = () => (
+ <>
+ <div>
+ {this.heading("ANALYSIS")}
+ <div className="content-wrapper">
+ {!this.loading &&
+ (!this.done ? (
+ <TypeAnimation
+ speed={50}
+ sequence={[
+ this.text,
+ () => {
+ setTimeout(() => {
+ this.setDone(true);
+ }, 500);
+ },
+ ]}
+ />
+ ) : (
+ this.text
+ ))}
+ </div>
+ </div>
+ {!this.loading && (
+ <div className="btns-wrapper">
+ {this.done?
+ this.chatMode?(
+ <input
+ defaultValue=""
+ autoComplete="off"
+ onChange={this.dataPromptChanged}
+ onKeyDown={e => {
+ e.key === 'Enter' ? this.generateDataAnalysis() : null;
+ e.stopPropagation();
+ }}
+ type="text"
+ placeholder="Ask GPT a question about the data..."
+ id="search-input"
+ className="searchBox-input"
+ style={{width: "100%"}}
+ />
+ )
+ :(
+ <>
+ <Button tooltip="Transfer to text" text="Transfer To Text" onClick={this.transferToText} color={StrCast(Doc.UserDoc().userVariantColor)} type={Type.TERT} />
+ <Button tooltip="Chat with AI" text="Chat with AI" onClick={this.chatWithAI} color={StrCast(Doc.UserDoc().userVariantColor)} type={Type.TERT} />
+ </>
+ ) : (
+ <div className="summarizing">
+ <span>Summarizing</span>
+ <ReactLoading type="bubbles" color="#bcbcbc" width={20} height={20} />
+ <Button text="Stop Animation" onClick={() => {this.setDone(true);}} color={StrCast(Doc.UserDoc().userVariantColor)} type={Type.TERT}/>
+ </div>
+ )}
+ </div>
+ )}
+ </>
+ );
+
aiWarning = () =>
this.done ? (
<div className="ai-warning">
@@ -308,7 +396,7 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
render() {
return (
<div className="summary-box" style={{ display: this.visible ? 'flex' : 'none' }}>
- {this.mode === GPTPopupMode.SUMMARY ? this.summaryBox() : this.mode === GPTPopupMode.IMAGE ? this.imageBox() : <></>}
+ {this.mode === GPTPopupMode.SUMMARY? this.summaryBox() : this.mode === GPTPopupMode.DATA? this.dataAnalysisBox() : this.mode === GPTPopupMode.IMAGE ? this.imageBox() : <></>}
</div>
);
}
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index a582f8004..aaff2a342 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -30,7 +30,7 @@ const _global = (window /* browser */ || global) /* node */ as any;
//pdfjsLib.GlobalWorkerOptions.workerSrc = `/assets/pdf.worker.js`;
// The workerSrc property shall be specified.
-Pdfjs.GlobalWorkerOptions.workerSrc = 'https://unpkg.com/pdfjs-dist@4.0.379/build/pdf.worker.mjs';
+Pdfjs.GlobalWorkerOptions.workerSrc = 'https://unpkg.com/pdfjs-dist@4.1.392/build/pdf.worker.mjs';
interface IViewerProps extends FieldViewProps {
pdfBox: PDFBox;
diff --git a/src/mobile/ImageUpload.tsx b/src/mobile/ImageUpload.tsx
index e333e6a2e..da01e099c 100644
--- a/src/mobile/ImageUpload.tsx
+++ b/src/mobile/ImageUpload.tsx
@@ -14,7 +14,7 @@ import { listSpec } from '../fields/Schema';
import { Cast } from '../fields/Types';
import './ImageUpload.scss';
import { MobileInterface } from './MobileInterface';
-const { default: { DFLT_IMAGE_NATIVE_DIM } } = require('../client/views/global/globalCssVariables.module.scss'); // prettier-ignore
+const { DFLT_IMAGE_NATIVE_DIM } = require('../client/views/global/globalCssVariables.module.scss'); // prettier-ignore
export interface ImageUploadProps {
Document: Doc; // Target document for upload (upload location)
}
diff --git a/src/server/authentication/AuthenticationManager.ts b/src/server/authentication/AuthenticationManager.ts
index b1b84c300..9c1525df0 100644
--- a/src/server/authentication/AuthenticationManager.ts
+++ b/src/server/authentication/AuthenticationManager.ts
@@ -56,7 +56,7 @@ export let postSignup = (req: Request, res: Response, next: NextFunction) => {
const user = new User(model);
User.findOne({ email })
- .then(existingUser => {
+ .then((existingUser: any) => {
if (existingUser) {
return res.redirect('/login');
}
@@ -67,9 +67,9 @@ export let postSignup = (req: Request, res: Response, next: NextFunction) => {
tryRedirectToTarget(req, res);
});
})
- .catch(err => next(err));
+ .catch((err: any) => next(err));
})
- .catch(err => next(err));
+ .catch((err: any) => next(err));
};
const tryRedirectToTarget = (req: Request, res: Response) => {
@@ -104,8 +104,8 @@ export let getLogin = (req: Request, res: Response) => {
export let postLogin = (req: Request, res: Response, next: NextFunction) => {
if (req.body.email === '') {
User.findOne({ email: 'guest' })
- .then(user => !user && initializeGuest())
- .catch(err => err);
+ .then((user: any) => !user && initializeGuest())
+ .catch((err: any) => err);
req.body.email = 'guest';
req.body.password = 'guest';
} else {
@@ -115,7 +115,7 @@ export let postLogin = (req: Request, res: Response, next: NextFunction) => {
}
if (validationResult(req).array().length) {
- req.flash('errors', 'Unable to login at this time. Please try again.');
+ // req.flash('errors', 'Unable to login at this time. Please try again.');
return res.redirect('/signup');
}
@@ -171,7 +171,7 @@ export let postForgot = function (req: Request, res: Response, next: NextFunctio
});
},
function (token: string, done: any) {
- User.findOne({ email }).then(user => {
+ User.findOne({ email }).then((user: any) => {
if (!user) {
// NO ACCOUNT WITH SUBMITTED EMAIL
res.redirect('/forgotPassword');
@@ -219,14 +219,14 @@ export let postForgot = function (req: Request, res: Response, next: NextFunctio
export let getReset = function (req: Request, res: Response) {
User.findOne({ passwordResetToken: req.params.token, passwordResetExpires: { $gt: Date.now() } })
- .then(user => {
+ .then((user: any) => {
if (!user) return res.redirect('/forgotPassword');
res.render('reset.pug', {
title: 'Reset Password',
user: req.user,
});
})
- .catch(err => res.redirect('/forgotPassword'));
+ .catch((err: any) => res.redirect('/forgotPassword'));
};
export let postReset = function (req: Request, res: Response) {
@@ -234,7 +234,7 @@ export let postReset = function (req: Request, res: Response) {
[
function (done: any) {
User.findOne({ passwordResetToken: req.params.token, passwordResetExpires: { $gt: Date.now() } })
- .then(user => {
+ .then((user: any) => {
if (!user) return res.redirect('back');
check('password', 'Password must be at least 4 characters long').isLength({ min: 4 }).run(req);
@@ -251,10 +251,10 @@ export let postReset = function (req: Request, res: Response) {
() => (req as any).logIn(user),
(err: any) => err
)
- .catch(err => res.redirect('/login'));
+ .catch((err: any) => res.redirect('/login'));
done(null, user);
})
- .catch(err => res.redirect('back'));
+ .catch((err: any) => res.redirect('back'));
},
function (user: DashUserModel, done: any) {
const smtpTransport = nodemailer.createTransport({
diff --git a/src/server/server_Initialization.ts b/src/server/server_Initialization.ts
index afc6231e5..2d52ea906 100644
--- a/src/server/server_Initialization.ts
+++ b/src/server/server_Initialization.ts
@@ -58,7 +58,7 @@ export default async function InitializeServer(routeSetter: RouteSetter) {
// initialize the web socket (bidirectional communication: if a user changes
// a field on one client, that change must be broadcast to all other clients)
- await WebSocket.initialize(isRelease, app);
+ await WebSocket.initialize(isRelease, SSL.Credentials);
//disconnect = async () => new Promise<Error>(resolve => server.close(resolve));
return isRelease;
diff --git a/src/server/websocket.ts b/src/server/websocket.ts
index a26b81bdf..38134f2c1 100644
--- a/src/server/websocket.ts
+++ b/src/server/websocket.ts
@@ -1,22 +1,21 @@
import { blue } from 'colors';
-import * as express from 'express';
import { createServer } from 'https';
-import { Server, Socket } from '../../node_modules/socket.io/dist/index';
+import * as _ from 'lodash';
import { networkInterfaces } from 'os';
+import { Server, Socket } from 'socket.io';
import { Utils } from '../Utils';
import { logPort } from './ActionUtilities';
import { timeMap } from './ApiManagers/UserManager';
-import { GoogleCredentialsLoader, SSL } from './apis/google/CredentialsLoader';
-import YoutubeApi from './apis/youtube/youtubeApiSample';
-import { initializeGuest } from './authentication/DashUserModel';
import { Client } from './Client';
import { DashStats } from './DashStats';
-import { Database } from './database';
import { DocumentsCollection } from './IDatabase';
import { Diff, GestureContent, MessageStore, MobileDocumentUploadContent, MobileInkOverlayContent, Transferable, Types, UpdateMobileInkOverlayPositionContent, YoutubeQueryInput, YoutubeQueryTypes } from './Message';
import { Search } from './Search';
+import { GoogleCredentialsLoader } from './apis/google/CredentialsLoader';
+import YoutubeApi from './apis/youtube/youtubeApiSample';
+import { initializeGuest } from './authentication/DashUserModel';
+import { Database } from './database';
import { resolvedPorts } from './server_Initialization';
-import * as _ from 'lodash';
export namespace WebSocket {
export let _socket: Socket;
@@ -25,15 +24,16 @@ export namespace WebSocket {
export const userOperations = new Map<string, number>();
export let disconnect: Function;
- export async function initialize(isRelease: boolean, app: express.Express) {
+ export async function initialize(isRelease: boolean, credentials:any) {
let io: Server;
if (isRelease) {
const { socketPort } = process.env;
if (socketPort) {
resolvedPorts.socket = Number(socketPort);
- }
- io = new Server(createServer(SSL.Credentials, app), SSL.Credentials as any);
- io.listen(resolvedPorts.socket);
+ }
+ const httpsServer = createServer(credentials);
+ io = new Server(httpsServer, {})
+ httpsServer.listen(resolvedPorts.socket);
} else {
io = new Server();
io.listen(resolvedPorts.socket);
diff --git a/src/typings/connect-flash/index.d.ts b/src/typings/connect-flash/index.d.ts
new file mode 100644
index 000000000..74cb7d3c6
--- /dev/null
+++ b/src/typings/connect-flash/index.d.ts
@@ -0,0 +1 @@
+declare module 'connect-flash';
diff --git a/src/typings/connect-mongo/index.d.ts b/src/typings/connect-mongo/index.d.ts
new file mode 100644
index 000000000..ac2e35b09
--- /dev/null
+++ b/src/typings/connect-mongo/index.d.ts
@@ -0,0 +1 @@
+declare module 'connect-mongo';
diff --git a/src/typings/express-flash/index.d.ts b/src/typings/express-flash/index.d.ts
new file mode 100644
index 000000000..4e03d914f
--- /dev/null
+++ b/src/typings/express-flash/index.d.ts
@@ -0,0 +1 @@
+declare module 'express-flash';
diff --git a/src/typings/image-data-uri/index.d.ts b/src/typings/image-data-uri/index.d.ts
new file mode 100644
index 000000000..7f5b9617f
--- /dev/null
+++ b/src/typings/image-data-uri/index.d.ts
@@ -0,0 +1,3 @@
+/// <reference types="node" />
+
+declare module 'image-data-uri';
diff --git a/src/typings/index.d.ts b/src/typings/index.d.ts
index d46977816..a9ebbb480 100644
--- a/src/typings/index.d.ts
+++ b/src/typings/index.d.ts
@@ -13,11 +13,6 @@ declare module 'iink-js';
declare module 'pdfjs-dist/web/pdf_viewer';
declare module 'react-jsx-parser';
-declare module 'express-flash';
-declare module 'connect-flash';
-declare module 'connect-mongo';
-declare module '@mui/material';
-
declare module '@react-pdf/renderer' {
import * as React from 'react';
diff --git a/src/typings/jpeg-autorotate/index.d.ts b/src/typings/jpeg-autorotate/index.d.ts
new file mode 100644
index 000000000..7cc194b72
--- /dev/null
+++ b/src/typings/jpeg-autorotate/index.d.ts
@@ -0,0 +1,3 @@
+/// <reference types="node" />
+
+declare module 'jpeg-autorotate';