Skip to content

Commit

Permalink
Merge branch 'main' of github.com:quadratichq/quadratic into qa
Browse files Browse the repository at this point in the history
  • Loading branch information
AyushAgrawal-A2 committed Dec 23, 2024
2 parents 30c82db + c5bd15b commit 097a038
Show file tree
Hide file tree
Showing 29 changed files with 388 additions and 119 deletions.
13 changes: 7 additions & 6 deletions quadratic-api/src/middleware/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ const getOrCreateUser = async (auth0Id: string) => {
});

if (user) {
return user;
return { user, userCreated: false };
}

// If they don't exist yet, create them
Expand All @@ -74,31 +74,32 @@ const getOrCreateUser = async (auth0Id: string) => {
await runFirstTimeUserLogic(newUser);

// Return the user
return newUser;
return { user: newUser, userCreated: true };
};

export const userMiddleware = async (req: Request, res: Response, next: NextFunction) => {
const { auth } = req as RequestWithAuth;

const user = await getOrCreateUser(auth.sub);
const { user, userCreated } = await getOrCreateUser(auth.sub);
if (!user) {
return res.status(500).json({ error: { message: 'Unable to get authenticated user' } });
}

(req as RequestWithUser).user = user;
(req as RequestWithUser).userCreated = userCreated === true;
next();
};

export const userOptionalMiddleware = async (req: Request, res: Response, next: NextFunction) => {
const { auth } = req as RequestWithOptionalAuth;

if (auth && auth.sub) {
const user = await getOrCreateUser(auth.sub);
const { user } = await getOrCreateUser(auth.sub);
if (!user) {
return res.status(500).json({ error: { message: 'Unable to get authenticated user' } });
}
// @ts-expect-error
req.user = user;

(req as RequestWithUser).user = user;
}

next();
Expand Down
2 changes: 1 addition & 1 deletion quadratic-api/src/routes/v0/users.acknowledge.GET.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,5 @@ export default [validateAccessToken, userMiddleware, handler];
* been created yet or associated with teams and/or files.
*/
async function handler(req: RequestWithUser, res: Response<ApiTypes['/v0/users/acknowledge.GET.response']>) {
return res.status(200).json({ message: 'acknowledged' });
return res.status(200).json({ message: 'acknowledged', userCreated: req.userCreated });
}
1 change: 1 addition & 0 deletions quadratic-api/src/types/Request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export type RequestWithAuth = JWTRequest & {

export type RequestWithUser = RequestWithAuth & {
user: User;
userCreated: boolean;
};

export type RequestWithOptionalUser = RequestWithOptionalAuth & {
Expand Down
2 changes: 1 addition & 1 deletion quadratic-client/src/app/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export const createNewFileAction = {
label: 'New',
isAvailable: isAvailableBecauseFileLocationIsAccessibleAndWriteable,
run({ teamUuid }: { teamUuid: string }) {
window.location.href = ROUTES.CREATE_FILE_PRIVATE(teamUuid);
window.location.href = ROUTES.CREATE_FILE(teamUuid, { private: true });
},
};

Expand Down
2 changes: 2 additions & 0 deletions quadratic-client/src/app/atoms/aiAnalystAtom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { aiAnalystOfflineChats } from '@/app/ai/offline/aiAnalystChats';
import { getPromptMessages } from '@/app/ai/tools/message.helper';
import { editorInteractionStateUserAtom, editorInteractionStateUuidAtom } from '@/app/atoms/editorInteractionStateAtom';
import { showAIAnalystOnStartupAtom } from '@/app/atoms/gridSettingsAtom';
import { events } from '@/app/events/events';
import { focusGrid } from '@/app/helpers/focusGrid';
import { Chat, ChatMessage } from 'quadratic-shared/typesAndSchemasAI';
import { atom, DefaultValue, selector } from 'recoil';
Expand Down Expand Up @@ -57,6 +58,7 @@ export const aiAnalystAtom = atom<AIAnalystState>({
console.error('[AIAnalystOfflineChats]: ', error);
}
}
events.emit('aiAnalystInitialized');
}
},
({ onSet }) => {
Expand Down
11 changes: 1 addition & 10 deletions quadratic-client/src/app/atoms/codeEditorAtom.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { getPromptMessages } from '@/app/ai/tools/message.helper';
import { events } from '@/app/events/events';
import { pixiAppSettings } from '@/app/gridGL/pixiApp/PixiAppSettings';
import { CodeCell } from '@/app/gridGL/types/codeCell';
import { focusGrid } from '@/app/helpers/focusGrid';
import { JsCellsAccessed, JsCoordinate } from '@/app/quadratic-core-types';
Expand Down Expand Up @@ -188,15 +187,7 @@ export const codeEditorUnsavedChangesAtom = selector<boolean>({
key: 'codeEditorUnsavedChangesAtom',
get: ({ get }) => {
const { editorContent, codeString } = get(codeEditorAtom);
const unsavedChanges = editorContent !== codeString;

if (unsavedChanges) {
pixiAppSettings.unsavedEditorChanges = editorContent;
} else {
pixiAppSettings.unsavedEditorChanges = undefined;
}

return unsavedChanges;
return editorContent !== codeString;
},
});

Expand Down
2 changes: 1 addition & 1 deletion quadratic-client/src/app/debugFlags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export const debugGridSettings = debug && false;

export const debugShowMultiplayer = debug && false;

export const debugSaveURLState = (debug && false) || url.has('state');
export const debugSaveURLState = debug && false;

// --------
// UI
Expand Down
4 changes: 4 additions & 0 deletions quadratic-client/src/app/events/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,10 @@ interface EventTypes {
hashContentChanged: (sheetId: string, hashX: number, hashY: number) => void;

codeEditorCodeCell: (codeCell?: CodeCell) => void;

aiAnalystInitialized: () => void;

pixiAppSettingsInitialized: () => void;
}

export const events = new EventEmitter<EventTypes>();
138 changes: 138 additions & 0 deletions quadratic-client/src/app/gridGL/HTMLGrid/EmptyGridMessage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import {
editorInteractionStateShowCellTypeMenuAtom,
editorInteractionStateShowConnectionsMenuAtom,
} from '@/app/atoms/editorInteractionStateAtom';
import { events } from '@/app/events/events';
import { sheets } from '@/app/grid/controller/Sheets';
import { supportedFileTypes } from '@/app/helpers/files';
import { useConnectionsFetcher } from '@/app/ui/hooks/useConnectionsFetcher';
import { useFileImport } from '@/app/ui/hooks/useFileImport';
import { useFileRouteLoaderData } from '@/shared/hooks/useFileRouteLoaderData';
import { Button } from '@/shared/shadcn/ui/button';
import { useEffect, useRef, useState } from 'react';
import { useSetRecoilState } from 'recoil';

const fileHasData = () => sheets.sheets.filter((sheet) => sheet.bounds.type === 'nonEmpty').length > 0;

// When a file loads, if it's totally empty, show this message. Then once the
// user has edited the file, we'll hide it permanently.
export function EmptyGridMessage() {
const {
userMakingRequest: { filePermissions },
team: { uuid: teamUuid },
} = useFileRouteLoaderData();
const canEdit = filePermissions.includes('FILE_EDIT');
const [open, setOpen] = useState(false);
const showConnectionsMenu = useSetRecoilState(editorInteractionStateShowConnectionsMenuAtom);
const showCellTypeMenu = useSetRecoilState(editorInteractionStateShowCellTypeMenuAtom);
const { data } = useConnectionsFetcher();
const connections = data?.connections ?? [];

// Show/hide depending on whether the file has any data in it
useEffect(() => {
const checkBounds = () => {
setOpen(fileHasData() ? false : true);
};

events.on('hashContentChanged', checkBounds);
return () => {
events.off('hashContentChanged', checkBounds);
};
}, [open]);

if (!canEdit) {
return null;
}

if (!open) {
return null;
}

return (
<div className="absolute bottom-4 right-4 flex w-72 flex-col items-center rounded border border-border bg-background p-4 text-center shadow-md">
<div className="flex gap-4 pb-8 pt-10">
<div className="flex flex-col items-start gap-2">
<div className="h-2 w-12 bg-foreground/15" />
<div className="h-1 w-9 bg-foreground/15" />
<div className="h-1 w-11 bg-foreground/15" />
<div className="h-1 w-7 bg-foreground/15" />
<div className="h-1 w-8 bg-foreground/15" />
</div>
<div className="flex flex-col items-center gap-2">
<div className="h-2 w-12 bg-foreground/15" />
<div className="h-1 w-9 bg-foreground/15" />
<div className="h-1 w-11 bg-foreground/15" />
<div className="h-1 w-7 bg-foreground/15" />
<div className="h-1 w-8 bg-foreground/15" />
</div>
<div className="flex flex-col items-end gap-2">
<div className="h-2 w-12 bg-foreground/15" />
<div className="h-1 w-9 bg-foreground/15" />
<div className="h-1 w-11 bg-foreground/15" />
<div className="h-1 w-7 bg-foreground/15" />
<div className="h-1 w-8 bg-foreground/15" />
</div>
</div>
<h2 className="text-md font-semibold">Import data</h2>
<p className="text-sm text-muted-foreground">
Bring in your own data via a file (CSV, Excel, Parquet) or a connection (Postgres, MySQL, and more).
</p>
<div className="mt-2 flex w-full flex-col justify-center gap-2">
<UploadFileButton teamUuid={teamUuid} />

{connections.length === 0 ? (
<Button
variant="outline"
className="w-full"
onClick={() => {
showConnectionsMenu(true);
}}
>
Create connection
</Button>
) : (
<Button
variant="outline"
className="w-full"
onClick={() => {
showCellTypeMenu(true);
}}
>
Use connection
</Button>
)}
</div>
</div>
);
}

function UploadFileButton({ teamUuid }: { teamUuid: string }) {
const handleFileImport = useFileImport();
const fileInputRef = useRef<HTMLInputElement>(null);

return (
<>
<Button className="w-full" onClick={() => fileInputRef.current?.click()}>
Upload file
</Button>
<input
ref={fileInputRef}
type="file"
hidden
accept={supportedFileTypes.join(',')}
onChange={(e) => {
const files = e.target.files;
if (files) {
handleFileImport({
files,
sheetId: sheets.sheet.id,
insertAt: { x: 1, y: 1 },
cursor: sheets.getCursorPosition(),
teamUuid,
});
}
}}
/>
</>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Annotations } from '@/app/gridGL/HTMLGrid/annotations/Annotations';
import { AskAISelection } from '@/app/gridGL/HTMLGrid/askAISelection/AskAISelection';
import { CodeHint } from '@/app/gridGL/HTMLGrid/CodeHint';
import { CodeRunning } from '@/app/gridGL/HTMLGrid/codeRunning/CodeRunning';
import { EmptyGridMessage } from '@/app/gridGL/HTMLGrid/EmptyGridMessage';
import { GridContextMenu } from '@/app/gridGL/HTMLGrid/GridContextMenu';
import { HoverCell } from '@/app/gridGL/HTMLGrid/hoverCell/HoverCell';
import { HoverTooltip } from '@/app/gridGL/HTMLGrid/hoverTooltip/HoverTooltip';
Expand Down Expand Up @@ -124,6 +125,8 @@ export const HTMLGridContainer = (props: Props): ReactNode | null => {

<Following />

<EmptyGridMessage />

{/* This is positioned on the grid over the headings and not zoomed. It comes
after the above, so it's above it on the grid. */}
<div
Expand Down
18 changes: 16 additions & 2 deletions quadratic-client/src/app/gridGL/PixiAppEffects.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import { editorInteractionStateAtom } from '@/app/atoms/editorInteractionStateAt
import { gridPanModeAtom } from '@/app/atoms/gridPanModeAtom';
import { gridSettingsAtom, presentationModeAtom, showHeadingsAtom } from '@/app/atoms/gridSettingsAtom';
import { inlineEditorAtom } from '@/app/atoms/inlineEditorAtom';
import { events } from '@/app/events/events';
import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp';
import { pixiAppSettings } from '@/app/gridGL/pixiApp/PixiAppSettings';
import { useSubmitAIAnalystPrompt } from '@/app/ui/menus/AIAnalyst/hooks/useSubmitAIAnalystPrompt';
import { useGlobalSnackbar } from '@/shared/components/GlobalSnackbarProvider';
import { useEffect } from 'react';
import { isMobile } from 'react-device-detect';
Expand Down Expand Up @@ -42,6 +44,13 @@ export const PixiAppEffects = () => {
const [codeEditorState, setCodeEditorState] = useRecoilState(codeEditorAtom);
useEffect(() => {
pixiAppSettings.updateCodeEditorState(codeEditorState, setCodeEditorState);

const unsavedChanges = codeEditorState.editorContent !== codeEditorState.codeString;
if (unsavedChanges) {
pixiAppSettings.unsavedEditorChanges = codeEditorState.editorContent;
} else {
pixiAppSettings.unsavedEditorChanges = undefined;
}
}, [codeEditorState, setCodeEditorState]);

const { addGlobalSnackbar } = useGlobalSnackbar();
Expand All @@ -60,9 +69,14 @@ export const PixiAppEffects = () => {
}, [gridPanMode, setGridPanMode]);

const [aiAnalystState, setAIAnalystState] = useRecoilState(aiAnalystAtom);
const { submitPrompt } = useSubmitAIAnalystPrompt();
useEffect(() => {
pixiAppSettings.updateAIAnalystState(aiAnalystState, setAIAnalystState, submitPrompt);
}, [aiAnalystState, setAIAnalystState, submitPrompt]);

useEffect(() => {
pixiAppSettings.updateAIAnalystState(aiAnalystState, setAIAnalystState);
}, [aiAnalystState, setAIAnalystState]);
events.emit('pixiAppSettingsInitialized');
}, []);

useEffect(() => {
const handleMouseUp = () => {
Expand Down
9 changes: 8 additions & 1 deletion quadratic-client/src/app/gridGL/pixiApp/PixiAppSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { sheets } from '@/app/grid/controller/Sheets';
import { inlineEditorHandler } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorHandler';
import { CursorMode } from '@/app/gridGL/HTMLGrid/inlineEditor/inlineEditorKeyboard';
import { pixiApp } from '@/app/gridGL/pixiApp/PixiApp';
import { SubmitAIAnalystPromptArgs } from '@/app/ui/menus/AIAnalyst/hooks/useSubmitAIAnalystPrompt';
import { multiplayer } from '@/app/web-workers/multiplayerWebWorker/multiplayer';
import { GlobalSnackbar, SnackbarOptions } from '@/shared/components/GlobalSnackbarProvider';
import { ApiTypes } from 'quadratic-shared/typesAndSchemas';
Expand Down Expand Up @@ -59,6 +60,7 @@ class PixiAppSettings {

aiAnalystState = defaultAIAnalystState;
setAIAnalystState?: SetterOrUpdater<AIAnalystState>;
submitAIAnalystPrompt?: (prompt: SubmitAIAnalystPromptArgs) => Promise<void>;

constructor() {
const settings = localStorage.getItem('viewSettings');
Expand Down Expand Up @@ -141,9 +143,14 @@ class PixiAppSettings {
this.setCodeEditorState = setCodeEditorState;
}

updateAIAnalystState(aiAnalystState: AIAnalystState, setAIAnalystState: SetterOrUpdater<AIAnalystState>): void {
updateAIAnalystState(
aiAnalystState: AIAnalystState,
setAIAnalystState: SetterOrUpdater<AIAnalystState>,
submitAIAnalystPrompt: (prompt: SubmitAIAnalystPromptArgs) => Promise<void>
): void {
this.aiAnalystState = aiAnalystState;
this.setAIAnalystState = setAIAnalystState;
this.submitAIAnalystPrompt = submitAIAnalystPrompt;
}

get showGridLines(): boolean {
Expand Down
Loading

0 comments on commit 097a038

Please sign in to comment.