From 4d66767c2bd9f822fcc2544de898e463bcb08579 Mon Sep 17 00:00:00 2001 From: Jim Nielsen Date: Wed, 11 Dec 2024 14:12:29 -0700 Subject: [PATCH 001/155] initial commit --- quadratic-client/src/router.tsx | 12 ++++++++++++ quadratic-client/src/routes/teams.$teamUuid.tsx | 4 +++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/quadratic-client/src/router.tsx b/quadratic-client/src/router.tsx index ca1dc74a8a..edffb5fc60 100644 --- a/quadratic-client/src/router.tsx +++ b/quadratic-client/src/router.tsx @@ -1,5 +1,6 @@ import { BrowserCompatibilityLayoutRoute } from '@/dashboard/components/BrowserCompatibilityLayoutRoute'; import * as Page404 from '@/routes/404'; +import { useDashboardRouteLoaderData } from '@/routes/_dashboard'; import * as Login from '@/routes/login'; import * as LoginResult from '@/routes/login-result'; import * as Logout from '@/routes/logout'; @@ -87,6 +88,17 @@ export const router = createBrowserRouter( /> import('./routes/labs')} /> + { + const data = useDashboardRouteLoaderData(); + // This renders + return
Hello
; + // This triggers a route boundary error + return ; + }} + /> + import('./routes/teams.create')} /> import('./routes/teams.$teamUuid')}> diff --git a/quadratic-client/src/routes/teams.$teamUuid.tsx b/quadratic-client/src/routes/teams.$teamUuid.tsx index dce927472d..e3ee823ede 100644 --- a/quadratic-client/src/routes/teams.$teamUuid.tsx +++ b/quadratic-client/src/routes/teams.$teamUuid.tsx @@ -5,7 +5,7 @@ import { Button } from '@/shared/shadcn/ui/button'; import { ExclamationTriangleIcon } from '@radix-ui/react-icons'; import mixpanel from 'mixpanel-browser'; import { ApiTypes } from 'quadratic-shared/typesAndSchemas'; -import { ActionFunctionArgs, Link, Outlet, redirectDocument } from 'react-router-dom'; +import { ActionFunctionArgs, Link, Outlet, redirectDocument, useRouteError } from 'react-router-dom'; export type TeamAction = { 'request.update-team': ReturnType; @@ -139,6 +139,8 @@ export const Component = () => { }; export const ErrorBoundary = () => { + const error = useRouteError(); + console.error(error); // Maybe we log this to Sentry? return ( Date: Thu, 12 Dec 2024 03:20:22 +0530 Subject: [PATCH 002/155] fix pixiAppSettings circular dependency --- quadratic-client/src/app/atoms/codeEditorAtom.ts | 11 +---------- quadratic-client/src/app/gridGL/PixiAppEffects.tsx | 7 +++++++ 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/quadratic-client/src/app/atoms/codeEditorAtom.ts b/quadratic-client/src/app/atoms/codeEditorAtom.ts index 50fd8b348c..688b0e0499 100644 --- a/quadratic-client/src/app/atoms/codeEditorAtom.ts +++ b/quadratic-client/src/app/atoms/codeEditorAtom.ts @@ -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 { Coordinate } from '@/app/gridGL/types/size'; import { focusGrid } from '@/app/helpers/focusGrid'; @@ -189,15 +188,7 @@ export const codeEditorUnsavedChangesAtom = selector({ 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; }, }); diff --git a/quadratic-client/src/app/gridGL/PixiAppEffects.tsx b/quadratic-client/src/app/gridGL/PixiAppEffects.tsx index 5f1f14cff7..606b263b92 100644 --- a/quadratic-client/src/app/gridGL/PixiAppEffects.tsx +++ b/quadratic-client/src/app/gridGL/PixiAppEffects.tsx @@ -42,6 +42,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(); From d3f3466d980fea89ed6cf11d0031a31d78f9efbc Mon Sep 17 00:00:00 2001 From: Jim Nielsen Date: Wed, 11 Dec 2024 17:19:16 -0700 Subject: [PATCH 003/155] more tweaks --- quadratic-client/src/app/actions.ts | 2 +- .../dashboard/components/DashboardSidebar.tsx | 2 +- .../components/FilesListEmptyState.tsx | 2 +- .../dashboard/components/NewFileButton.tsx | 8 +-- .../src/dashboard/shared/getActiveTeam.ts | 62 +++++++++++++++++++ quadratic-client/src/router.tsx | 24 +++---- quadratic-client/src/routes/_dashboard.tsx | 57 +++++------------ .../src/routes/_teams-redirect.tsx | 14 +++++ quadratic-client/src/routes/new.tsx | 15 +++++ .../routes/teams.$teamUuid.files.create.tsx | 15 ++++- .../src/shared/constants/routes.ts | 29 +++++++-- .../src/shared/hooks/useNewFileFromState.ts | 8 +-- 12 files changed, 160 insertions(+), 78 deletions(-) create mode 100644 quadratic-client/src/dashboard/shared/getActiveTeam.ts create mode 100644 quadratic-client/src/routes/_teams-redirect.tsx create mode 100644 quadratic-client/src/routes/new.tsx diff --git a/quadratic-client/src/app/actions.ts b/quadratic-client/src/app/actions.ts index ca0232a6de..440b9a1a23 100644 --- a/quadratic-client/src/app/actions.ts +++ b/quadratic-client/src/app/actions.ts @@ -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 }); }, }; diff --git a/quadratic-client/src/dashboard/components/DashboardSidebar.tsx b/quadratic-client/src/dashboard/components/DashboardSidebar.tsx index 85ac190aae..2d5d2117eb 100644 --- a/quadratic-client/src/dashboard/components/DashboardSidebar.tsx +++ b/quadratic-client/src/dashboard/components/DashboardSidebar.tsx @@ -193,7 +193,7 @@ function SidebarNavLinkCreateButton({ size="icon-sm" className="absolute right-2 top-1 ml-auto !bg-transparent opacity-30 hover:opacity-100" > - + diff --git a/quadratic-client/src/dashboard/components/FilesListEmptyState.tsx b/quadratic-client/src/dashboard/components/FilesListEmptyState.tsx index b4d8b38906..ee49d4be7c 100644 --- a/quadratic-client/src/dashboard/components/FilesListEmptyState.tsx +++ b/quadratic-client/src/dashboard/components/FilesListEmptyState.tsx @@ -21,7 +21,7 @@ export const FilesListEmptyState = ({ isPrivate = false }: { isPrivate?: boolean <> You don’t have any files yet.{' '} { diff --git a/quadratic-client/src/dashboard/components/NewFileButton.tsx b/quadratic-client/src/dashboard/components/NewFileButton.tsx index a568937a91..f856f91975 100644 --- a/quadratic-client/src/dashboard/components/NewFileButton.tsx +++ b/quadratic-client/src/dashboard/components/NewFileButton.tsx @@ -36,7 +36,7 @@ export default function NewFileButton({ isPrivate }: { isPrivate: boolean }) { return (
- + diff --git a/quadratic-client/src/dashboard/shared/getActiveTeam.ts b/quadratic-client/src/dashboard/shared/getActiveTeam.ts new file mode 100644 index 0000000000..7ad25e3f52 --- /dev/null +++ b/quadratic-client/src/dashboard/shared/getActiveTeam.ts @@ -0,0 +1,62 @@ +import { ACTIVE_TEAM_UUID_KEY } from '@/routes/_dashboard'; +import { apiClient } from '@/shared/api/apiClient'; +import * as Sentry from '@sentry/react'; + +// TODO: explain this +// It's implicit what team is currently active. This is the function that +// tells us what is most likely the currently active team. +// Only once we do a get of the team do we know for sure the user has access to it. +export default async function getActiveTeam( + teams: Awaited>['teams'], + teamUuidFromUrl: string | undefined +) { + let teamCreated = false; + + /** + * Determine what the active team is + */ + let initialActiveTeamUuid = undefined; + // const uuidFromUrl = params.teamUuid; + const uuidFromLocalStorage = localStorage.getItem(ACTIVE_TEAM_UUID_KEY); + + // FYI: if you have a UUID in the URL or localstorage, it doesn’t mean you + // have access to it (maybe you were removed from a team, so it’s a 404) + // So we have to ensure we A) have a UUID, and B) it's in the list of teams + // we have access to from the server. + + // 1) Check the URL for a team UUID. If there's one, use that as that's + // explicitly what the user is trying to look at + if (teamUuidFromUrl) { + initialActiveTeamUuid = teamUuidFromUrl; + + // 2) Check localstorage for a team UUID + // If what's in localstorage is not in the list of teams from the server — + // e.g. you lost access to a team — we'll skip this + } else if (uuidFromLocalStorage && teams.find((team) => team.team.uuid === uuidFromLocalStorage)) { + initialActiveTeamUuid = uuidFromLocalStorage; + + // 3) There's no default preference (yet), so pick the 1st one in the API + } else if (teams.length > 0) { + initialActiveTeamUuid = teams[0].team.uuid; + + // 4) There are no teams in the API, so we will create one + } else if (teams.length === 0) { + const newTeam = await apiClient.teams.create({ name: 'My Team' }); + initialActiveTeamUuid = newTeam.uuid; + teamCreated = true; + } + + // This should never happen, but if it does, we'll log it to sentry + if (initialActiveTeamUuid === undefined) { + Sentry.captureEvent({ + message: 'No active team was found or could be created.', + level: 'fatal', + }); + throw new Error('No active team could be found or created.'); + } + + return { + teamUuid: initialActiveTeamUuid, + teamCreated, + }; +} diff --git a/quadratic-client/src/router.tsx b/quadratic-client/src/router.tsx index edffb5fc60..1c84a5806a 100644 --- a/quadratic-client/src/router.tsx +++ b/quadratic-client/src/router.tsx @@ -1,6 +1,5 @@ import { BrowserCompatibilityLayoutRoute } from '@/dashboard/components/BrowserCompatibilityLayoutRoute'; import * as Page404 from '@/routes/404'; -import { useDashboardRouteLoaderData } from '@/routes/_dashboard'; import * as Login from '@/routes/login'; import * as LoginResult from '@/routes/login-result'; import * as Logout from '@/routes/logout'; @@ -69,6 +68,10 @@ export const router = createBrowserRouter( /> + {/* Helper routes, e.g. /connections -> /teams/:uuid/connections */} + + import('./routes/new')} /> + {/* Dashboard UI routes */} import('./routes/_dashboard')}> import('./routes/labs')} /> - { - const data = useDashboardRouteLoaderData(); - // This renders - return
Hello
; - // This triggers a route boundary error - return ; - }} - /> + {/* TODO: handle these + import('./routes/_teams-redirect')} /> + import('./routes/_teams-redirect')} /> + import('./routes/_teams-redirect')} /> + + import('./routes/_teams-redirect')} /> + import('./routes/files.create')} /> + import('./routes/_teams-redirect')} /> + */} import('./routes/teams.create')} /> diff --git a/quadratic-client/src/routes/_dashboard.tsx b/quadratic-client/src/routes/_dashboard.tsx index 8cee523e35..4960387017 100644 --- a/quadratic-client/src/routes/_dashboard.tsx +++ b/quadratic-client/src/routes/_dashboard.tsx @@ -3,6 +3,7 @@ import { DashboardSidebar } from '@/dashboard/components/DashboardSidebar'; import { EducationDialog } from '@/dashboard/components/EducationDialog'; import { Empty } from '@/dashboard/components/Empty'; import { ImportProgressList } from '@/dashboard/components/ImportProgressList'; +import getActiveTeam from '@/dashboard/shared/getActiveTeam'; import { apiClient } from '@/shared/api/apiClient'; import { MenuIcon } from '@/shared/components/Icons'; import { ROUTES, ROUTE_LOADER_IDS, SEARCH_PARAMS } from '@/shared/constants/routes'; @@ -12,7 +13,6 @@ import { Sheet, SheetContent, SheetTrigger } from '@/shared/shadcn/ui/sheet'; import { TooltipProvider } from '@/shared/shadcn/ui/tooltip'; import { cn } from '@/shared/shadcn/utils'; import { ExclamationTriangleIcon, InfoCircledIcon } from '@radix-ui/react-icons'; -import * as Sentry from '@sentry/react'; import { ApiTypes } from 'quadratic-shared/typesAndSchemas'; import { useEffect, useRef, useState } from 'react'; import { isMobile } from 'react-device-detect'; @@ -54,10 +54,10 @@ type LoaderData = { teams: ApiTypes['/v0/teams.GET.response']['teams']; userMakingRequest: ApiTypes['/v0/teams.GET.response']['userMakingRequest']; eduStatus: ApiTypes['/v0/education.GET.response']['eduStatus']; - initialActiveTeamUuid: string; activeTeam: ApiTypes['/v0/teams/:uuid.GET.response']; }; +// getActiveTeam() export const loader = async ({ params, request }: LoaderFunctionArgs): Promise => { /** * Get the initial data @@ -68,69 +68,40 @@ export const loader = async ({ params, request }: LoaderFunctionArgs): Promise team.team.uuid === uuidFromLocalStorage)) { - initialActiveTeamUuid = uuidFromLocalStorage; - - // 3) there's no default preference (yet), so pick the 1st one in the API - } else if (teams.length > 0) { - initialActiveTeamUuid = teams[0].team.uuid; - - // 4) there's no teams in the API, so create one - } else if (teams.length === 0) { - const newTeam = await apiClient.teams.create({ name: 'My Team' }); - // Send user to team dashboard if mobile, otherwise to a new file - return isMobile ? redirect(ROUTES.TEAM(newTeam.uuid)) : redirect(ROUTES.CREATE_FILE(newTeam.uuid)); - } - - // This should never happen, but if it does, we'll log it to sentry - if (initialActiveTeamUuid === undefined) { - Sentry.captureEvent({ - message: 'No active team was found or could be created.', - level: 'fatal', - }); - throw new Error('No active team could be found or created.'); + // If a team was created, it was probably a first time user so send them to + // the team dashboard if mobile, otherwise to a new file + if (teamCreated) { + return isMobile ? redirect(ROUTES.TEAM(teamUuid)) : redirect(ROUTES.CREATE_FILE(teamUuid)); } // If this was a request to the root of the app, re-route to the active team const url = new URL(request.url); if (url.pathname === '/') { // If there are search params, keep 'em - return redirect(ROUTES.TEAM(initialActiveTeamUuid) + url.search); + return redirect(ROUTES.TEAM(teamUuid) + url.search); } + // TODO: replace this with /connections -> /teams/:uuid/connections // If it was a shortcut team route, redirect there // e.g. /?team-shortcut=connections const teamShortcut = url.searchParams.get('team-shortcut'); if (teamShortcut) { url.searchParams.delete('team-shortcut'); - return redirect(ROUTES.TEAM_CONNECTIONS(initialActiveTeamUuid) + url.search); + return redirect(ROUTES.TEAM_CONNECTIONS(teamUuid) + url.search); } /** * Get data for the active team */ const activeTeam = await apiClient.teams - .get(initialActiveTeamUuid) + .get(teamUuid) .then((data) => { // If we got to here, we successfully loaded the active team so now this is // the one we keep in localstorage for when the page loads anew - localStorage.setItem(ACTIVE_TEAM_UUID_KEY, initialActiveTeamUuid); + localStorage.setItem(ACTIVE_TEAM_UUID_KEY, teamUuid); // Sort the users so the logged-in user is first in the list data.users.sort((a, b) => { @@ -154,7 +125,7 @@ export const loader = async ({ params, request }: LoaderFunctionArgs): Promise useRouteLoaderData(ROUTE_LOADER_IDS.DASHBOARD) as LoaderData; diff --git a/quadratic-client/src/routes/_teams-redirect.tsx b/quadratic-client/src/routes/_teams-redirect.tsx new file mode 100644 index 0000000000..d7413a9f73 --- /dev/null +++ b/quadratic-client/src/routes/_teams-redirect.tsx @@ -0,0 +1,14 @@ +import { useDashboardRouteLoaderData } from '@/routes/_dashboard'; +import { Navigate, useLocation } from 'react-router-dom'; + +export const Component = () => { + const { + activeTeam: { + team: { uuid }, + }, + } = useDashboardRouteLoaderData(); + const { pathname } = useLocation(); + console.log(pathname); + + return ; +}; diff --git a/quadratic-client/src/routes/new.tsx b/quadratic-client/src/routes/new.tsx new file mode 100644 index 0000000000..4500653083 --- /dev/null +++ b/quadratic-client/src/routes/new.tsx @@ -0,0 +1,15 @@ +import getActiveTeam from '@/dashboard/shared/getActiveTeam'; +import { apiClient } from '@/shared/api/apiClient'; +import { ROUTES } from '@/shared/constants/routes'; +import { LoaderFunctionArgs, redirect } from 'react-router-dom'; + +export const loader = async ({ request }: LoaderFunctionArgs) => { + const url = new URL(request.url); + const prompt = url.searchParams.get('prompt'); + + const { teams } = await apiClient.teams.list(); + const { teamUuid } = await getActiveTeam(teams, undefined); + + const redirectUrl = ROUTES.CREATE_FILE(teamUuid, { prompt, private: true }); + return redirect(redirectUrl); +}; diff --git a/quadratic-client/src/routes/teams.$teamUuid.files.create.tsx b/quadratic-client/src/routes/teams.$teamUuid.files.create.tsx index e5ba8b3c10..973be81db6 100644 --- a/quadratic-client/src/routes/teams.$teamUuid.files.create.tsx +++ b/quadratic-client/src/routes/teams.$teamUuid.files.create.tsx @@ -66,10 +66,19 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { const { file: { uuid }, } = await apiClient.files.create({ teamUuid, isPrivate }); - // If there's a `state=...` query param, for starting a file in a specific - // state, pass that along + + // Pass along a few of the search params + let searchParamsToPass = new URLSearchParams(); const state = searchParams.get('state'); - return replace(ROUTES.FILE(uuid) + (state ? `?state=${state}` : '')); + if (state) { + searchParamsToPass.set('state', state); + } + const prompt = searchParams.get('prompt'); + if (prompt) { + searchParamsToPass.set('prompt', prompt); + } + + return replace(ROUTES.FILE(uuid) + (searchParamsToPass ? '?' + searchParamsToPass.toString() : '')); } catch (error) { return replace(getFailUrl(ROUTES.TEAM(teamUuid))); } diff --git a/quadratic-client/src/shared/constants/routes.ts b/quadratic-client/src/shared/constants/routes.ts index 5db407031e..454ec221e6 100644 --- a/quadratic-client/src/shared/constants/routes.ts +++ b/quadratic-client/src/shared/constants/routes.ts @@ -12,14 +12,30 @@ export const ROUTES = { FILES_SHARED_WITH_ME: '/files/shared-with-me', FILE: (uuid: string) => `/file/${uuid}`, - CREATE_FILE: (teamUuid: string, state?: UrlParamsDevState['insertAndRunCodeInNewSheet']) => - `/teams/${teamUuid}/files/create` + - (state ? `?state=${btoa(JSON.stringify({ insertAndRunCodeInNewSheet: state }))}` : ''), + CREATE_FILE: ( + teamUuid: string, + searchParams: { + state?: UrlParamsDevState['insertAndRunCodeInNewSheet']; + prompt?: string | null; + private?: boolean; + } = {} + ) => { + let url = new URL(window.location.origin + `/teams/${teamUuid}/files/create`); + + if (searchParams.state) { + url.searchParams.set('state', btoa(JSON.stringify({ insertAndRunCodeInNewSheet: searchParams.state }))); + } + if (searchParams.prompt) { + url.searchParams.set('prompt', searchParams.prompt); + } + if (searchParams.private) { + url.searchParams.set('private', 'true'); + } + + return url.toString(); + }, CREATE_FILE_EXAMPLE: (teamUuid: string, publicFileUrlInProduction: string, isPrivate: boolean) => `/teams/${teamUuid}/files/create?example=${publicFileUrlInProduction}${isPrivate ? '&private' : ''}`, - CREATE_FILE_PRIVATE: (teamUuid: string, state?: UrlParamsDevState['insertAndRunCodeInNewSheet']) => - `/teams/${teamUuid}/files/create?private` + - (state ? `&state=${btoa(JSON.stringify({ insertAndRunCodeInNewSheet: state }))}` : ''), TEAMS: `/teams`, TEAMS_CREATE: `/teams/create`, TEAM: (teamUuid: string) => `/teams/${teamUuid}`, @@ -38,6 +54,7 @@ export const ROUTES = { TEAM_SHORTCUT: { CONNECTIONS: `/?team-shortcut=connections`, }, + CONNECTIONS: '/connections', EDIT_TEAM: (teamUuid: string) => `/teams/${teamUuid}/edit`, EXAMPLES: '/examples', ACCOUNT: '/account', diff --git a/quadratic-client/src/shared/hooks/useNewFileFromState.ts b/quadratic-client/src/shared/hooks/useNewFileFromState.ts index 14d164342e..967286ab8b 100644 --- a/quadratic-client/src/shared/hooks/useNewFileFromState.ts +++ b/quadratic-client/src/shared/hooks/useNewFileFromState.ts @@ -12,9 +12,7 @@ export const useNewFileFromStatePythonApi = ({ isPrivate, teamUuid }: { isPrivat language: 'Python' as CodeCellLanguage, }; - const to = isPrivate - ? ROUTES.CREATE_FILE_PRIVATE(teamUuid, stateUrlParam) - : ROUTES.CREATE_FILE(teamUuid, stateUrlParam); + const to = ROUTES.CREATE_FILE(teamUuid, { state: stateUrlParam, private: isPrivate }); return to; }; @@ -40,9 +38,7 @@ export const newNewFileFromStateConnection = ({ language: { Connection: { kind: connectionType, id: connectionUuid } }, }; - const to = isPrivate - ? ROUTES.CREATE_FILE_PRIVATE(teamUuid, stateUrlParam) - : ROUTES.CREATE_FILE(teamUuid, stateUrlParam); + const to = ROUTES.CREATE_FILE(teamUuid, { state: stateUrlParam, private: isPrivate }); return to; }; From 15c437fd2733817ca35895d41f8cbece4f84d4a6 Mon Sep 17 00:00:00 2001 From: AyushAgrawal-A2 Date: Thu, 12 Dec 2024 06:45:54 +0530 Subject: [PATCH 004/155] submit analyst prompt from query params --- .../src/app/atoms/aiAnalystAtom.ts | 2 ++ quadratic-client/src/app/events/events.ts | 2 ++ .../src/app/gridGL/PixiAppEffects.tsx | 6 +++-- .../src/app/gridGL/pixiApp/PixiAppSettings.ts | 9 ++++++- .../gridGL/pixiApp/urlParams/UrlParamsUser.ts | 25 +++++++++++++++++-- .../hooks/useSubmitAIAnalystPrompt.tsx | 19 ++++++-------- 6 files changed, 47 insertions(+), 16 deletions(-) diff --git a/quadratic-client/src/app/atoms/aiAnalystAtom.ts b/quadratic-client/src/app/atoms/aiAnalystAtom.ts index 4ba67d8dbc..c98a923ac6 100644 --- a/quadratic-client/src/app/atoms/aiAnalystAtom.ts +++ b/quadratic-client/src/app/atoms/aiAnalystAtom.ts @@ -1,6 +1,7 @@ import { aiAnalystOfflineChats } from '@/app/ai/offline/aiAnalystChats'; import { getPromptMessages } from '@/app/ai/tools/message.helper'; import { editorInteractionStateUserAtom, editorInteractionStateUuidAtom } from '@/app/atoms/editorInteractionStateAtom'; +import { events } from '@/app/events/events'; import { sheets } from '@/app/grid/controller/Sheets'; import { focusGrid } from '@/app/helpers/focusGrid'; import { Chat, ChatMessage } from 'quadratic-shared/typesAndSchemasAI'; @@ -62,6 +63,7 @@ export const aiAnalystAtom = atom({ console.error('[AIAnalystOfflineChats]: ', error); } } + events.emit('aiAnalystInitialized'); } }, ({ onSet }) => { diff --git a/quadratic-client/src/app/events/events.ts b/quadratic-client/src/app/events/events.ts index 7281499e92..743a20e04b 100644 --- a/quadratic-client/src/app/events/events.ts +++ b/quadratic-client/src/app/events/events.ts @@ -133,6 +133,8 @@ interface EventTypes { hashContentChanged: (sheetId: string, hashX: number, hashY: number) => void; codeEditorCodeCell: (codeCell?: CodeCell) => void; + + aiAnalystInitialized: () => void; } export const events = new EventEmitter(); diff --git a/quadratic-client/src/app/gridGL/PixiAppEffects.tsx b/quadratic-client/src/app/gridGL/PixiAppEffects.tsx index 606b263b92..863db83493 100644 --- a/quadratic-client/src/app/gridGL/PixiAppEffects.tsx +++ b/quadratic-client/src/app/gridGL/PixiAppEffects.tsx @@ -6,6 +6,7 @@ import { gridSettingsAtom, presentationModeAtom, showHeadingsAtom } from '@/app/ import { inlineEditorAtom } from '@/app/atoms/inlineEditorAtom'; 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'; @@ -67,9 +68,10 @@ export const PixiAppEffects = () => { }, [gridPanMode, setGridPanMode]); const [aiAnalystState, setAIAnalystState] = useRecoilState(aiAnalystAtom); + const { submitPrompt } = useSubmitAIAnalystPrompt(); useEffect(() => { - pixiAppSettings.updateAIAnalystState(aiAnalystState, setAIAnalystState); - }, [aiAnalystState, setAIAnalystState]); + pixiAppSettings.updateAIAnalystState(aiAnalystState, setAIAnalystState, submitPrompt); + }, [aiAnalystState, setAIAnalystState, submitPrompt]); useEffect(() => { const handleMouseUp = () => { diff --git a/quadratic-client/src/app/gridGL/pixiApp/PixiAppSettings.ts b/quadratic-client/src/app/gridGL/pixiApp/PixiAppSettings.ts index 13572935e5..c325738098 100644 --- a/quadratic-client/src/app/gridGL/pixiApp/PixiAppSettings.ts +++ b/quadratic-client/src/app/gridGL/pixiApp/PixiAppSettings.ts @@ -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 } from '@/shared/components/GlobalSnackbarProvider'; import { ApiTypes } from 'quadratic-shared/typesAndSchemas'; @@ -55,6 +56,7 @@ class PixiAppSettings { aiAnalystState = defaultAIAnalystState; setAIAnalystState?: SetterOrUpdater; + submitAIAnalystPrompt?: (prompt: SubmitAIAnalystPromptArgs) => Promise; constructor() { const settings = localStorage.getItem('viewSettings'); @@ -138,9 +140,14 @@ class PixiAppSettings { this.setCodeEditorState = setCodeEditorState; } - updateAIAnalystState(aiAnalystState: AIAnalystState, setAIAnalystState: SetterOrUpdater): void { + updateAIAnalystState( + aiAnalystState: AIAnalystState, + setAIAnalystState: SetterOrUpdater, + submitAIAnalystPrompt: (prompt: SubmitAIAnalystPromptArgs) => Promise + ): void { this.aiAnalystState = aiAnalystState; this.setAIAnalystState = setAIAnalystState; + this.submitAIAnalystPrompt = submitAIAnalystPrompt; } get showGridLines(): boolean { diff --git a/quadratic-client/src/app/gridGL/pixiApp/urlParams/UrlParamsUser.ts b/quadratic-client/src/app/gridGL/pixiApp/urlParams/UrlParamsUser.ts index 25901ad188..5a0553b56e 100644 --- a/quadratic-client/src/app/gridGL/pixiApp/urlParams/UrlParamsUser.ts +++ b/quadratic-client/src/app/gridGL/pixiApp/urlParams/UrlParamsUser.ts @@ -13,7 +13,7 @@ export class UrlParamsUser { this.loadSheet(params); this.loadCursor(params); this.loadCode(params); - this.setupListeners(); + this.setupListeners(params); } private loadSheet(params: URLSearchParams) { @@ -65,10 +65,31 @@ export class UrlParamsUser { } } - private setupListeners() { + private loadAIAnalystPrompt = (params: URLSearchParams) => { + const prompt = params.get('prompt'); + if (!prompt) return; + + const { submitAIAnalystPrompt } = pixiAppSettings; + if (!submitAIAnalystPrompt) { + throw new Error('Expected submitAIAnalystPrompt to be set in urlParams.loadAIAnalystPrompt'); + } + + submitAIAnalystPrompt({ + userPrompt: prompt, + context: { + sheets: [], + currentSheet: sheets.sheet.name, + selection: undefined, + }, + clearMessages: true, + }); + }; + + private setupListeners(params: URLSearchParams) { events.on('cursorPosition', this.setDirty); events.on('changeSheet', this.setDirty); events.on('codeEditor', this.setDirty); + events.on('aiAnalystInitialized', () => this.loadAIAnalystPrompt(params)); } private setDirty = () => { diff --git a/quadratic-client/src/app/ui/menus/AIAnalyst/hooks/useSubmitAIAnalystPrompt.tsx b/quadratic-client/src/app/ui/menus/AIAnalyst/hooks/useSubmitAIAnalystPrompt.tsx index 8b29f9b037..a8eb0f1b24 100644 --- a/quadratic-client/src/app/ui/menus/AIAnalyst/hooks/useSubmitAIAnalystPrompt.tsx +++ b/quadratic-client/src/app/ui/menus/AIAnalyst/hooks/useSubmitAIAnalystPrompt.tsx @@ -28,6 +28,13 @@ import { useRecoilCallback } from 'recoil'; const MAX_TOOL_CALL_ITERATIONS = 5; +export type SubmitAIAnalystPromptArgs = { + userPrompt: string; + context: Context; + messageIndex?: number; + clearMessages?: boolean; +}; + export function useSubmitAIAnalystPrompt() { const { handleAIRequestToAPI } = useAIRequestToAPI(); const { getQuadraticContext } = useQuadraticContextMessages(); @@ -79,17 +86,7 @@ export function useSubmitAIAnalystPrompt() { const submitPrompt = useRecoilCallback( ({ set, snapshot }) => - async ({ - userPrompt, - context, - messageIndex, - clearMessages, - }: { - userPrompt: string; - context: Context; - messageIndex?: number; - clearMessages?: boolean; - }) => { + async ({ userPrompt, context, messageIndex, clearMessages }: SubmitAIAnalystPromptArgs) => { set(showAIAnalystAtom, true); set(aiAnalystShowChatHistoryAtom, false); From abca5a1b871d360b2ea0910ec8c5ceb284781d0a Mon Sep 17 00:00:00 2001 From: Jim Nielsen Date: Thu, 12 Dec 2024 17:01:33 -0700 Subject: [PATCH 005/155] remove url param --- .../src/app/gridGL/pixiApp/urlParams/UrlParamsUser.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/quadratic-client/src/app/gridGL/pixiApp/urlParams/UrlParamsUser.ts b/quadratic-client/src/app/gridGL/pixiApp/urlParams/UrlParamsUser.ts index 5a0553b56e..e362913493 100644 --- a/quadratic-client/src/app/gridGL/pixiApp/urlParams/UrlParamsUser.ts +++ b/quadratic-client/src/app/gridGL/pixiApp/urlParams/UrlParamsUser.ts @@ -83,6 +83,12 @@ export class UrlParamsUser { }, clearMessages: true, }); + + // Remove the `prompt` param when we're done + const url = new URL(window.location.href); + params.delete('prompt'); + url.search = params.toString(); + window.history.replaceState(null, '', url.toString()); }; private setupListeners(params: URLSearchParams) { From f4487f2ab4775cb51c186d828c02edcfaedf70d8 Mon Sep 17 00:00:00 2001 From: Jim Nielsen Date: Thu, 12 Dec 2024 17:01:41 -0700 Subject: [PATCH 006/155] update route --- quadratic-client/src/router.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quadratic-client/src/router.tsx b/quadratic-client/src/router.tsx index 1c84a5806a..1dd3de5c62 100644 --- a/quadratic-client/src/router.tsx +++ b/quadratic-client/src/router.tsx @@ -70,7 +70,7 @@ export const router = createBrowserRouter( {/* Helper routes, e.g. /connections -> /teams/:uuid/connections */} - import('./routes/new')} /> + import('./routes/new')} /> {/* Dashboard UI routes */} import('./routes/_dashboard')}> From bcae9146838a63b03ca981540c00f9c1d2872191 Mon Sep 17 00:00:00 2001 From: Jim Nielsen Date: Thu, 12 Dec 2024 17:02:36 -0700 Subject: [PATCH 007/155] first time user creation logic --- quadratic-api/src/middleware/user.ts | 13 +++++++------ .../src/routes/v0/users.acknowledge.GET.ts | 2 +- quadratic-api/src/types/Request.ts | 1 + quadratic-client/src/routes/login-result.tsx | 15 ++++++++++++++- quadratic-shared/typesAndSchemas.ts | 2 +- 5 files changed, 24 insertions(+), 9 deletions(-) diff --git a/quadratic-api/src/middleware/user.ts b/quadratic-api/src/middleware/user.ts index e2fe6f2fb9..61b8c9c996 100644 --- a/quadratic-api/src/middleware/user.ts +++ b/quadratic-api/src/middleware/user.ts @@ -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 @@ -74,18 +74,19 @@ 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(); }; @@ -93,12 +94,12 @@ export const userOptionalMiddleware = async (req: Request, res: Response, next: 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(); diff --git a/quadratic-api/src/routes/v0/users.acknowledge.GET.ts b/quadratic-api/src/routes/v0/users.acknowledge.GET.ts index dce3263540..bc2a42ed83 100644 --- a/quadratic-api/src/routes/v0/users.acknowledge.GET.ts +++ b/quadratic-api/src/routes/v0/users.acknowledge.GET.ts @@ -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) { - return res.status(200).json({ message: 'acknowledged' }); + return res.status(200).json({ message: 'acknowledged', userCreated: req.userCreated }); } diff --git a/quadratic-api/src/types/Request.ts b/quadratic-api/src/types/Request.ts index a63ae6cb25..56cc2b1f6e 100644 --- a/quadratic-api/src/types/Request.ts +++ b/quadratic-api/src/types/Request.ts @@ -34,6 +34,7 @@ export type RequestWithAuth = JWTRequest & { export type RequestWithUser = RequestWithAuth & { user: User; + userCreated: boolean; }; export type RequestWithOptionalUser = RequestWithOptionalAuth & { diff --git a/quadratic-client/src/routes/login-result.tsx b/quadratic-client/src/routes/login-result.tsx index 10678d1279..32bdc77e02 100644 --- a/quadratic-client/src/routes/login-result.tsx +++ b/quadratic-client/src/routes/login-result.tsx @@ -11,7 +11,20 @@ export const loader = async () => { if (isAuthenticated) { // Acknowledge the user has just logged in. The backend may need // to run some logic before making any other API calls in parallel - await apiClient.users.acknowledge(); + const { userCreated } = await apiClient.users.acknowledge(); + + // Special case for first-time users + if (userCreated) { + console.log('First-time user created'); + try { + // @ts-expect-error + window.dataLayer.push({ + event: 'registrationComplete', + }); + } catch (e) { + // No google analytics available + } + } let redirectTo = new URLSearchParams(window.location.search).get('redirectTo') || '/'; return redirect(redirectTo); diff --git a/quadratic-shared/typesAndSchemas.ts b/quadratic-shared/typesAndSchemas.ts index 37aa5c1ceb..88b68572ed 100644 --- a/quadratic-shared/typesAndSchemas.ts +++ b/quadratic-shared/typesAndSchemas.ts @@ -377,7 +377,7 @@ export const ApiSchemas = { * Users * =========================================================================== */ - '/v0/users/acknowledge.GET.response': z.object({ message: z.string() }), + '/v0/users/acknowledge.GET.response': z.object({ message: z.string(), userCreated: z.boolean() }), /** * =========================================================================== From c2b1bb1f4a3fd5acf0176b28633f442259185c48 Mon Sep 17 00:00:00 2001 From: Jim Nielsen Date: Thu, 12 Dec 2024 20:09:34 -0700 Subject: [PATCH 008/155] Update login-result.tsx --- quadratic-client/src/routes/login-result.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/quadratic-client/src/routes/login-result.tsx b/quadratic-client/src/routes/login-result.tsx index 32bdc77e02..570c87743b 100644 --- a/quadratic-client/src/routes/login-result.tsx +++ b/quadratic-client/src/routes/login-result.tsx @@ -15,7 +15,6 @@ export const loader = async () => { // Special case for first-time users if (userCreated) { - console.log('First-time user created'); try { // @ts-expect-error window.dataLayer.push({ From 68c0e8b61c68b330cd3a1f3da767821542b25e4d Mon Sep 17 00:00:00 2001 From: Jim Nielsen Date: Fri, 13 Dec 2024 16:07:29 -0700 Subject: [PATCH 009/155] cleanup --- .../src/dashboard/shared/getActiveTeam.ts | 20 +++++++++---------- quadratic-client/src/router.tsx | 15 ++------------ quadratic-client/src/routes/_dashboard.tsx | 9 --------- .../src/routes/{new.tsx => files.create.tsx} | 0 .../src/shared/constants/routes.ts | 7 ------- 5 files changed, 11 insertions(+), 40 deletions(-) rename quadratic-client/src/routes/{new.tsx => files.create.tsx} (100%) diff --git a/quadratic-client/src/dashboard/shared/getActiveTeam.ts b/quadratic-client/src/dashboard/shared/getActiveTeam.ts index 7ad25e3f52..2b3eb34dd9 100644 --- a/quadratic-client/src/dashboard/shared/getActiveTeam.ts +++ b/quadratic-client/src/dashboard/shared/getActiveTeam.ts @@ -2,9 +2,8 @@ import { ACTIVE_TEAM_UUID_KEY } from '@/routes/_dashboard'; import { apiClient } from '@/shared/api/apiClient'; import * as Sentry from '@sentry/react'; -// TODO: explain this -// It's implicit what team is currently active. This is the function that -// tells us what is most likely the currently active team. +// When a user lands on the app, we don't necessarily know what their "active" +// team is. It’s semi-implicit, but we can make a guess. // Only once we do a get of the team do we know for sure the user has access to it. export default async function getActiveTeam( teams: Awaited>['teams'], @@ -15,8 +14,7 @@ export default async function getActiveTeam( /** * Determine what the active team is */ - let initialActiveTeamUuid = undefined; - // const uuidFromUrl = params.teamUuid; + let teamUuid = undefined; const uuidFromLocalStorage = localStorage.getItem(ACTIVE_TEAM_UUID_KEY); // FYI: if you have a UUID in the URL or localstorage, it doesn’t mean you @@ -27,27 +25,27 @@ export default async function getActiveTeam( // 1) Check the URL for a team UUID. If there's one, use that as that's // explicitly what the user is trying to look at if (teamUuidFromUrl) { - initialActiveTeamUuid = teamUuidFromUrl; + teamUuid = teamUuidFromUrl; // 2) Check localstorage for a team UUID // If what's in localstorage is not in the list of teams from the server — // e.g. you lost access to a team — we'll skip this } else if (uuidFromLocalStorage && teams.find((team) => team.team.uuid === uuidFromLocalStorage)) { - initialActiveTeamUuid = uuidFromLocalStorage; + teamUuid = uuidFromLocalStorage; // 3) There's no default preference (yet), so pick the 1st one in the API } else if (teams.length > 0) { - initialActiveTeamUuid = teams[0].team.uuid; + teamUuid = teams[0].team.uuid; // 4) There are no teams in the API, so we will create one } else if (teams.length === 0) { const newTeam = await apiClient.teams.create({ name: 'My Team' }); - initialActiveTeamUuid = newTeam.uuid; + teamUuid = newTeam.uuid; teamCreated = true; } // This should never happen, but if it does, we'll log it to sentry - if (initialActiveTeamUuid === undefined) { + if (teamUuid === undefined) { Sentry.captureEvent({ message: 'No active team was found or could be created.', level: 'fatal', @@ -56,7 +54,7 @@ export default async function getActiveTeam( } return { - teamUuid: initialActiveTeamUuid, + teamUuid, teamCreated, }; } diff --git a/quadratic-client/src/router.tsx b/quadratic-client/src/router.tsx index 1dd3de5c62..cce28b273b 100644 --- a/quadratic-client/src/router.tsx +++ b/quadratic-client/src/router.tsx @@ -68,9 +68,8 @@ export const router = createBrowserRouter( /> - {/* Helper routes, e.g. /connections -> /teams/:uuid/connections */} - - import('./routes/new')} /> + {/* Route to redirect to a new file in the app */} + import('./routes/files.create')} /> {/* Dashboard UI routes */} import('./routes/_dashboard')}> @@ -91,16 +90,6 @@ export const router = createBrowserRouter( /> import('./routes/labs')} /> - {/* TODO: handle these - import('./routes/_teams-redirect')} /> - import('./routes/_teams-redirect')} /> - import('./routes/_teams-redirect')} /> - - import('./routes/_teams-redirect')} /> - import('./routes/files.create')} /> - import('./routes/_teams-redirect')} /> - */} - import('./routes/teams.create')} /> import('./routes/teams.$teamUuid')}> diff --git a/quadratic-client/src/routes/_dashboard.tsx b/quadratic-client/src/routes/_dashboard.tsx index 4960387017..8c811493f6 100644 --- a/quadratic-client/src/routes/_dashboard.tsx +++ b/quadratic-client/src/routes/_dashboard.tsx @@ -57,7 +57,6 @@ type LoaderData = { activeTeam: ApiTypes['/v0/teams/:uuid.GET.response']; }; -// getActiveTeam() export const loader = async ({ params, request }: LoaderFunctionArgs): Promise => { /** * Get the initial data @@ -84,14 +83,6 @@ export const loader = async ({ params, request }: LoaderFunctionArgs): Promise /teams/:uuid/connections - // If it was a shortcut team route, redirect there - // e.g. /?team-shortcut=connections - const teamShortcut = url.searchParams.get('team-shortcut'); - if (teamShortcut) { - url.searchParams.delete('team-shortcut'); - return redirect(ROUTES.TEAM_CONNECTIONS(teamUuid) + url.search); - } /** * Get data for the active team diff --git a/quadratic-client/src/routes/new.tsx b/quadratic-client/src/routes/files.create.tsx similarity index 100% rename from quadratic-client/src/routes/new.tsx rename to quadratic-client/src/routes/files.create.tsx diff --git a/quadratic-client/src/shared/constants/routes.ts b/quadratic-client/src/shared/constants/routes.ts index 454ec221e6..a391bbf4d5 100644 --- a/quadratic-client/src/shared/constants/routes.ts +++ b/quadratic-client/src/shared/constants/routes.ts @@ -48,13 +48,6 @@ export const ROUTES = { TEAM_FILES_PRIVATE: (teamUuid: string) => `/teams/${teamUuid}/files/private`, TEAM_MEMBERS: (teamUuid: string) => `/teams/${teamUuid}/members`, TEAM_SETTINGS: (teamUuid: string) => `/teams/${teamUuid}/settings`, - // This is a way to navigate to a team route without necessariliy knowing - // the teamUuid upfront. It’s useful from the app-side when you want to navigate - // back to the dashboard. - TEAM_SHORTCUT: { - CONNECTIONS: `/?team-shortcut=connections`, - }, - CONNECTIONS: '/connections', EDIT_TEAM: (teamUuid: string) => `/teams/${teamUuid}/edit`, EXAMPLES: '/examples', ACCOUNT: '/account', From b17ec37ffcf4f55e99fcddc0cb4a0a2d512e57eb Mon Sep 17 00:00:00 2001 From: Jim Nielsen Date: Fri, 13 Dec 2024 16:11:41 -0700 Subject: [PATCH 010/155] Delete _teams-redirect.tsx --- quadratic-client/src/routes/_teams-redirect.tsx | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 quadratic-client/src/routes/_teams-redirect.tsx diff --git a/quadratic-client/src/routes/_teams-redirect.tsx b/quadratic-client/src/routes/_teams-redirect.tsx deleted file mode 100644 index d7413a9f73..0000000000 --- a/quadratic-client/src/routes/_teams-redirect.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { useDashboardRouteLoaderData } from '@/routes/_dashboard'; -import { Navigate, useLocation } from 'react-router-dom'; - -export const Component = () => { - const { - activeTeam: { - team: { uuid }, - }, - } = useDashboardRouteLoaderData(); - const { pathname } = useLocation(); - console.log(pathname); - - return ; -}; From bb48954b52ef19ea8573a7f1372ffe645ce4d941 Mon Sep 17 00:00:00 2001 From: Jim Nielsen Date: Fri, 13 Dec 2024 16:21:53 -0700 Subject: [PATCH 011/155] Update router.tsx --- quadratic-client/src/router.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/quadratic-client/src/router.tsx b/quadratic-client/src/router.tsx index cce28b273b..2d2112563b 100644 --- a/quadratic-client/src/router.tsx +++ b/quadratic-client/src/router.tsx @@ -101,9 +101,8 @@ export const router = createBrowserRouter( + - - From b0e890f0d5fb402ab80b0c29c447f71ad49e3fdc Mon Sep 17 00:00:00 2001 From: AyushAgrawal-A2 Date: Sat, 14 Dec 2024 06:28:35 +0530 Subject: [PATCH 012/155] fix debug flag and improve initialisation trigger --- quadratic-client/src/app/debugFlags.ts | 2 +- quadratic-client/src/app/events/events.ts | 2 ++ .../src/app/gridGL/PixiAppEffects.tsx | 5 +++++ .../gridGL/pixiApp/urlParams/UrlParamsUser.ts | 20 ++++++++++++++++++- .../app/gridGL/pixiApp/urlParams/urlParams.ts | 2 +- 5 files changed, 28 insertions(+), 3 deletions(-) diff --git a/quadratic-client/src/app/debugFlags.ts b/quadratic-client/src/app/debugFlags.ts index 2399a29d32..971f42c5d1 100644 --- a/quadratic-client/src/app/debugFlags.ts +++ b/quadratic-client/src/app/debugFlags.ts @@ -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 diff --git a/quadratic-client/src/app/events/events.ts b/quadratic-client/src/app/events/events.ts index 743a20e04b..e298033b45 100644 --- a/quadratic-client/src/app/events/events.ts +++ b/quadratic-client/src/app/events/events.ts @@ -135,6 +135,8 @@ interface EventTypes { codeEditorCodeCell: (codeCell?: CodeCell) => void; aiAnalystInitialized: () => void; + + pixiAppSettingsInitialized: () => void; } export const events = new EventEmitter(); diff --git a/quadratic-client/src/app/gridGL/PixiAppEffects.tsx b/quadratic-client/src/app/gridGL/PixiAppEffects.tsx index 863db83493..a881bf6e00 100644 --- a/quadratic-client/src/app/gridGL/PixiAppEffects.tsx +++ b/quadratic-client/src/app/gridGL/PixiAppEffects.tsx @@ -4,6 +4,7 @@ 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'; @@ -73,6 +74,10 @@ export const PixiAppEffects = () => { pixiAppSettings.updateAIAnalystState(aiAnalystState, setAIAnalystState, submitPrompt); }, [aiAnalystState, setAIAnalystState, submitPrompt]); + useEffect(() => { + events.emit('pixiAppSettingsInitialized'); + }, []); + useEffect(() => { const handleMouseUp = () => { setGridPanMode((prev) => ({ ...prev, mouseIsDown: false })); diff --git a/quadratic-client/src/app/gridGL/pixiApp/urlParams/UrlParamsUser.ts b/quadratic-client/src/app/gridGL/pixiApp/urlParams/UrlParamsUser.ts index e362913493..5e436b274c 100644 --- a/quadratic-client/src/app/gridGL/pixiApp/urlParams/UrlParamsUser.ts +++ b/quadratic-client/src/app/gridGL/pixiApp/urlParams/UrlParamsUser.ts @@ -7,12 +7,17 @@ import { getLanguage } from '@/app/helpers/codeCellLanguage'; import { CodeCellLanguage } from '@/app/quadratic-core-types'; export class UrlParamsUser { + private pixiAppSettingsInitialized = false; + private aiAnalystInitialized = false; + private aiAnalystPromptLoaded = false; + dirty = false; constructor(params: URLSearchParams) { this.loadSheet(params); this.loadCursor(params); this.loadCode(params); + this.loadAIAnalystPrompt(params); this.setupListeners(params); } @@ -66,6 +71,9 @@ export class UrlParamsUser { } private loadAIAnalystPrompt = (params: URLSearchParams) => { + if (!this.pixiAppSettingsInitialized || !this.aiAnalystInitialized) return; + if (this.aiAnalystPromptLoaded) return; + const prompt = params.get('prompt'); if (!prompt) return; @@ -89,13 +97,23 @@ export class UrlParamsUser { params.delete('prompt'); url.search = params.toString(); window.history.replaceState(null, '', url.toString()); + + this.aiAnalystPromptLoaded = true; }; private setupListeners(params: URLSearchParams) { events.on('cursorPosition', this.setDirty); events.on('changeSheet', this.setDirty); events.on('codeEditor', this.setDirty); - events.on('aiAnalystInitialized', () => this.loadAIAnalystPrompt(params)); + + events.on('pixiAppSettingsInitialized', () => { + this.pixiAppSettingsInitialized = true; + this.loadAIAnalystPrompt(params); + }); + events.on('aiAnalystInitialized', () => { + this.aiAnalystInitialized = true; + this.loadAIAnalystPrompt(params); + }); } private setDirty = () => { diff --git a/quadratic-client/src/app/gridGL/pixiApp/urlParams/urlParams.ts b/quadratic-client/src/app/gridGL/pixiApp/urlParams/urlParams.ts index 19372007e8..5838ed15c3 100644 --- a/quadratic-client/src/app/gridGL/pixiApp/urlParams/urlParams.ts +++ b/quadratic-client/src/app/gridGL/pixiApp/urlParams/urlParams.ts @@ -19,7 +19,7 @@ class UrlParams { show() { const params = new URLSearchParams(window.location.search); - if (debugSaveURLState) { + if (debugSaveURLState || params.has('state')) { this.urlParamsDev = new UrlParamsDev(params); if (this.urlParamsDev.noUpdates) return; } else { From 7ed75bc43150b5db1391db3291ceaff49d5b9d05 Mon Sep 17 00:00:00 2001 From: Jim Nielsen Date: Thu, 19 Dec 2024 11:19:50 -0700 Subject: [PATCH 013/155] updates --- .../app/gridGL/HTMLGrid/EmptyGridMessage.tsx | 150 ++++++++++++++++++ .../app/gridGL/HTMLGrid/HTMLGridContainer.tsx | 3 + 2 files changed, 153 insertions(+) create mode 100644 quadratic-client/src/app/gridGL/HTMLGrid/EmptyGridMessage.tsx diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/EmptyGridMessage.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/EmptyGridMessage.tsx new file mode 100644 index 0000000000..f55b00b8f3 --- /dev/null +++ b/quadratic-client/src/app/gridGL/HTMLGrid/EmptyGridMessage.tsx @@ -0,0 +1,150 @@ +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 { CloseIcon } from '@/shared/components/Icons'; +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(fileHasData() ? false : true); + const showConnectionsMenu = useSetRecoilState(editorInteractionStateShowConnectionsMenuAtom); + const showCellTypeMenu = useSetRecoilState(editorInteractionStateShowCellTypeMenuAtom); + const { data } = useConnectionsFetcher(); + const connections = data?.connections ?? []; + + useEffect(() => { + const checkBounds = () => { + if (open && fileHasData()) { + setOpen(false); + } + }; + + events.on('hashContentChanged', checkBounds); + return () => { + events.off('hashContentChanged', checkBounds); + }; + }, [open]); + + if (!canEdit) { + return null; + } + + if (!open) { + return null; + } + + return ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

Import data

+

+ Bring in your own data via a file (CSV, Excel, Parquet) or a connection (Postgres, MySQL, and more). +

+
+ + + + + {connections.length && false && ( +

+ Press / to use an exisiting connection +

+ )} +
+
+ ); +} + +function UploadFileButton({ teamUuid }: { teamUuid: string }) { + const handleFileImport = useFileImport(); + const fileInputRef = useRef(null); + return ( + <> + + { + const files = e.target.files; + if (files) { + handleFileImport({ + files, + sheetId: sheets.sheet.id, + insertAt: { x: 1, y: 1 }, + cursor: sheets.getCursorPosition(), + teamUuid, + }); + } + }} + /> + + ); +} diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/HTMLGridContainer.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/HTMLGridContainer.tsx index 7a448d2e2a..cefdfa8be5 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/HTMLGridContainer.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/HTMLGridContainer.tsx @@ -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'; @@ -123,6 +124,8 @@ export const HTMLGridContainer = (props: Props): ReactNode | null => { + + {/* 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. */}
Date: Thu, 19 Dec 2024 11:42:47 -0700 Subject: [PATCH 014/155] Update EmptyGridMessage.tsx --- quadratic-client/src/app/gridGL/HTMLGrid/EmptyGridMessage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/EmptyGridMessage.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/EmptyGridMessage.tsx index f55b00b8f3..399d94f9d2 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/EmptyGridMessage.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/EmptyGridMessage.tsx @@ -109,7 +109,7 @@ export function EmptyGridMessage() { > - {connections.length && false && ( + {connections.length && (

Press / to use an exisiting connection

From 9c653a4562a1403bde40a76e25b082488f49a46b Mon Sep 17 00:00:00 2001 From: Jim Nielsen Date: Thu, 19 Dec 2024 12:20:02 -0700 Subject: [PATCH 015/155] fix --- .../app/gridGL/HTMLGrid/EmptyGridMessage.tsx | 27 +++++++++---------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/EmptyGridMessage.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/EmptyGridMessage.tsx index 399d94f9d2..9904441c4e 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/EmptyGridMessage.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/EmptyGridMessage.tsx @@ -91,16 +91,18 @@ export function EmptyGridMessage() { > Create connection - + {connections.length > 0 && ( + + )} - {connections.length && ( -

- Press / to use an exisiting connection -

- )}
); From 14ea521ec1d193c86408bb0e762c766f5e712898 Mon Sep 17 00:00:00 2001 From: Jim Nielsen Date: Fri, 20 Dec 2024 09:21:22 -0700 Subject: [PATCH 016/155] updates --- .../app/gridGL/HTMLGrid/EmptyGridMessage.tsx | 42 +++++++------------ .../src/app/ui/hooks/useFileImport.tsx | 2 + 2 files changed, 18 insertions(+), 26 deletions(-) diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/EmptyGridMessage.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/EmptyGridMessage.tsx index 9904441c4e..565fd78fda 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/EmptyGridMessage.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/EmptyGridMessage.tsx @@ -7,7 +7,6 @@ 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 { CloseIcon } from '@/shared/components/Icons'; import { useFileRouteLoaderData } from '@/shared/hooks/useFileRouteLoaderData'; import { Button } from '@/shared/shadcn/ui/button'; import { useEffect, useRef, useState } from 'react'; @@ -23,17 +22,16 @@ export function EmptyGridMessage() { team: { uuid: teamUuid }, } = useFileRouteLoaderData(); const canEdit = filePermissions.includes('FILE_EDIT'); - const [open, setOpen] = useState(fileHasData() ? false : true); + 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 = () => { - if (open && fileHasData()) { - setOpen(false); - } + setOpen(fileHasData() ? false : true); }; events.on('hashContentChanged', checkBounds); @@ -81,36 +79,28 @@ export function EmptyGridMessage() {

- - {connections.length > 0 && ( + + {connections.length === 0 ? ( + + ) : ( )} -
); diff --git a/quadratic-client/src/app/ui/hooks/useFileImport.tsx b/quadratic-client/src/app/ui/hooks/useFileImport.tsx index b78535312d..e820337407 100644 --- a/quadratic-client/src/app/ui/hooks/useFileImport.tsx +++ b/quadratic-client/src/app/ui/hooks/useFileImport.tsx @@ -8,6 +8,7 @@ import { ApiError } from '@/shared/api/fetchFromApi'; import { useGlobalSnackbar } from '@/shared/components/GlobalSnackbarProvider'; import { ROUTES } from '@/shared/constants/routes'; import { Buffer } from 'buffer'; +import mixpanel from 'mixpanel-browser'; import { useLocation, useNavigate } from 'react-router-dom'; import { useSetRecoilState } from 'recoil'; @@ -35,6 +36,7 @@ export function useFileImport() { isPrivate?: boolean; teamUuid?: string; }) => { + mixpanel.track('[ImportData].useFileImport'); quadraticCore.initWorker(); if (!files) files = await uploadFile(supportedFileTypes); From 6325639663f8cc37f1b525d506298ed31b6091f4 Mon Sep 17 00:00:00 2001 From: Jim Nielsen Date: Fri, 20 Dec 2024 10:00:13 -0700 Subject: [PATCH 017/155] Update EmptyGridMessage.tsx --- quadratic-client/src/app/gridGL/HTMLGrid/EmptyGridMessage.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/quadratic-client/src/app/gridGL/HTMLGrid/EmptyGridMessage.tsx b/quadratic-client/src/app/gridGL/HTMLGrid/EmptyGridMessage.tsx index 565fd78fda..b779dc9ae3 100644 --- a/quadratic-client/src/app/gridGL/HTMLGrid/EmptyGridMessage.tsx +++ b/quadratic-client/src/app/gridGL/HTMLGrid/EmptyGridMessage.tsx @@ -109,6 +109,7 @@ export function EmptyGridMessage() { function UploadFileButton({ teamUuid }: { teamUuid: string }) { const handleFileImport = useFileImport(); const fileInputRef = useRef(null); + return ( <>