Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/client/cypress/support/Pages/JSEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ export class JSEditor {
);
//Checking JS object was created successfully
this.assertHelper.AssertNetworkStatus("@createNewJSCollection", 201);
cy.get(this._jsObjName).click({ force: true });
this.agHelper.AssertElementVisibility(this._jsObjTxt);
// Assert that the name of the JS Object is focused when newly created
this.agHelper.PressEnter();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useCallback } from "react";
import { lazy, Suspense, useCallback, useMemo } from "react";
import React from "react";
import { useDispatch, useSelector } from "react-redux";
import { createNewJSCollection } from "actions/jsPaneActions";
import { getCurrentPageId } from "selectors/editorSelectors";
Expand All @@ -7,17 +8,16 @@ import { createMessage, EDITOR_PANE_TEXTS } from "ee/constants/messages";
import { JsFileIconV2 } from "pages/Editor/Explorer/ExplorerIcons";
import { SEARCH_ITEM_TYPES } from "components/editorComponents/GlobalSearch/utils";
import type { UseRoutes } from "ee/entities/IDE/constants";
import JSEditor from "pages/Editor/JSEditor";
import AddJS from "pages/Editor/IDE/EditorPane/JS/Add";
import { ADD_PATH } from "ee/constants/routes/appRoutes";
import history from "utils/history";
import { FocusEntity, identifyEntityFromPath } from "navigation/FocusEntity";
import { useModuleOptions } from "ee/utils/moduleInstanceHelpers";
import { getJSUrl } from "ee/pages/Editor/IDE/EditorPane/JS/utils";
import { JSBlankState } from "pages/Editor/JSEditor/JSBlankState";
import { getIDEViewMode } from "selectors/ideSelectors";
import { EditorViewMode } from "ee/entities/IDE/constants";
import { setListViewActiveState } from "actions/ideActions";
import { retryPromise } from "utils/AppsmithUtils";
import Skeleton from "widgets/Skeleton";

export const useJSAdd = () => {
const pageId = useSelector(getCurrentPageId);
Expand Down Expand Up @@ -93,25 +93,64 @@ export const useGroupedAddJsOperations = (): GroupedAddOperations => {
];
};

const AddJS = lazy(async () =>
retryPromise(
async () =>
import(
/* webpackChunkName: "AddJS" */ "pages/Editor/IDE/EditorPane/JS/Add"
),
),
);
const JSEditor = lazy(async () =>
retryPromise(
async () =>
import(/* webpackChunkName: "JSEditor" */ "pages/Editor/JSEditor"),
),
);

const JSEmpty = lazy(async () =>
retryPromise(
async () =>
import(
/* webpackChunkName: "JSEmpty" */ "pages/Editor/JSEditor/JSBlankState"
),
),
);

export const useJSEditorRoutes = (path: string): UseRoutes => {
return [
{
exact: true,
key: "AddJS",
component: AddJS,
path: [`${path}${ADD_PATH}`, `${path}/:baseCollectionId${ADD_PATH}`],
},
{
exact: true,
key: "JSEditor",
component: JSEditor,
path: [path + "/:baseCollectionId"],
},
{
key: "JSEmpty",
component: JSBlankState,
exact: true,
path: [path],
},
];
return useMemo(
() => [
{
exact: true,
key: "AddJS",
component: (args) => (
<Suspense fallback={<Skeleton />}>
<AddJS {...args} />
</Suspense>
),
path: [`${path}${ADD_PATH}`, `${path}/:baseCollectionId${ADD_PATH}`],
},
{
exact: true,
key: "JSEditor",
component: (args) => (
<Suspense fallback={<Skeleton />}>
<JSEditor {...args} />
</Suspense>
),
path: [path + "/:baseCollectionId"],
},
{
key: "JSEmpty",
component: (args) => (
<Suspense fallback={<Skeleton />}>
<JSEmpty {...args} />
</Suspense>
),
exact: true,
path: [path],
},
],
[path],
);
Comment on lines +121 to +155
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider enhancing the loading state UX.

The route configuration with Suspense and memoization is well implemented. However, the Skeleton component could benefit from explicit sizing props to match the component it's replacing.

Consider updating the Skeleton implementation:

-          <Suspense fallback={<Skeleton />}>
+          <Suspense 
+            fallback={
+              <Skeleton 
+                className="t--js-editor-skeleton"
+                height="100%" 
+                width="100%" 
+              />
+            }
+          >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return useMemo(
() => [
{
exact: true,
key: "AddJS",
component: (args) => (
<Suspense fallback={<Skeleton />}>
<AddJS {...args} />
</Suspense>
),
path: [`${path}${ADD_PATH}`, `${path}/:baseCollectionId${ADD_PATH}`],
},
{
exact: true,
key: "JSEditor",
component: (args) => (
<Suspense fallback={<Skeleton />}>
<JSEditor {...args} />
</Suspense>
),
path: [path + "/:baseCollectionId"],
},
{
key: "JSEmpty",
component: (args) => (
<Suspense fallback={<Skeleton />}>
<JSEmpty {...args} />
</Suspense>
),
exact: true,
path: [path],
},
],
[path],
);
return useMemo(
() => [
{
exact: true,
key: "AddJS",
component: (args) => (
<Suspense
fallback={
<Skeleton
className="t--js-editor-skeleton"
height="100%"
width="100%"
/>
}
>
<AddJS {...args} />
</Suspense>
),
path: [`${path}${ADD_PATH}`, `${path}/:baseCollectionId${ADD_PATH}`],
},
{
exact: true,
key: "JSEditor",
component: (args) => (
<Suspense
fallback={
<Skeleton
className="t--js-editor-skeleton"
height="100%"
width="100%"
/>
}
>
<JSEditor {...args} />
</Suspense>
),
path: [path + "/:baseCollectionId"],
},
{
key: "JSEmpty",
component: (args) => (
<Suspense
fallback={
<Skeleton
className="t--js-editor-skeleton"
height="100%"
width="100%"
/>
}
>
<JSEmpty {...args} />
</Suspense>
),
exact: true,
path: [path],
},
],
[path],
);

};
149 changes: 104 additions & 45 deletions app/client/src/ce/pages/Editor/IDE/EditorPane/Query/hooks.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useCallback, useMemo } from "react";
import { lazy, Suspense, useCallback, useMemo } from "react";
import React from "react";
import history from "utils/history";
import { useLocation } from "react-router";
import { FocusEntity, identifyEntityFromPath } from "navigation/FocusEntity";
Expand All @@ -23,19 +24,17 @@ import {
BUILDER_PATH_DEPRECATED,
} from "ee/constants/routes/appRoutes";
import { SAAS_EDITOR_API_ID_PATH } from "pages/Editor/SaaSEditor/constants";
import ApiEditor from "pages/Editor/APIEditor";
import type { UseRoutes } from "ee/entities/IDE/constants";
import QueryEditor from "pages/Editor/QueryEditor";
import AddQuery from "pages/Editor/IDE/EditorPane/Query/Add";
import type { AppState } from "ee/reducers";
import keyBy from "lodash/keyBy";
import { getPluginEntityIcon } from "pages/Editor/Explorer/ExplorerIcons";
import type { ListItemProps } from "@appsmith/ads";
import { createAddClassName } from "pages/Editor/IDE/EditorPane/utils";
import { QueriesBlankState } from "pages/Editor/QueryEditor/QueriesBlankState";
import { getIDEViewMode } from "selectors/ideSelectors";
import { EditorViewMode } from "ee/entities/IDE/constants";
import { setListViewActiveState } from "actions/ideActions";
import { retryPromise } from "utils/AppsmithUtils";
import Skeleton from "widgets/Skeleton";

export const useQueryAdd = () => {
const location = useLocation();
Expand Down Expand Up @@ -114,47 +113,107 @@ export const useGroupedAddQueryOperations = (): GroupedAddOperations => {
return groups;
};

const ApiEditor = lazy(async () =>
retryPromise(
async () =>
import(/* webpackChunkName: "APIEditor" */ "pages/Editor/APIEditor"),
),
);

const AddQuery = lazy(async () =>
retryPromise(
async () =>
import(
/* webpackChunkName: "AddQuery" */ "pages/Editor/IDE/EditorPane/Query/Add"
),
),
);
const QueryEditor = lazy(async () =>
retryPromise(
async () =>
import(/* webpackChunkName: "QueryEditor" */ "pages/Editor/QueryEditor"),
),
);

const QueryEmpty = lazy(async () =>
retryPromise(
async () =>
import(
/* webpackChunkName: "QueryEmpty" */ "pages/Editor/QueryEditor/QueriesBlankState"
),
),
);

export const useQueryEditorRoutes = (path: string): UseRoutes => {
return [
{
key: "ApiEditor",
component: ApiEditor,
exact: true,
path: [
BUILDER_PATH + API_EDITOR_ID_PATH,
BUILDER_CUSTOM_PATH + API_EDITOR_ID_PATH,
BUILDER_PATH_DEPRECATED + API_EDITOR_ID_PATH,
],
},
{
key: "AddQuery",
exact: true,
component: AddQuery,
path: [`${path}${ADD_PATH}`, `${path}/:baseQueryId${ADD_PATH}`],
},
{
key: "SAASEditor",
component: QueryEditor,
exact: true,
path: [
BUILDER_PATH + SAAS_EDITOR_API_ID_PATH,
BUILDER_CUSTOM_PATH + SAAS_EDITOR_API_ID_PATH,
BUILDER_PATH_DEPRECATED + SAAS_EDITOR_API_ID_PATH,
],
},
{
key: "QueryEditor",
component: QueryEditor,
exact: true,
path: [path + "/:baseQueryId"],
},
{
key: "QueryEmpty",
component: QueriesBlankState,
exact: true,
path: [path],
},
];
return useMemo(
() => [
{
key: "ApiEditor",
component: (args) => {
return (
<Suspense fallback={<Skeleton />}>
<ApiEditor {...args} />
</Suspense>
);
},
exact: true,
path: [
BUILDER_PATH + API_EDITOR_ID_PATH,
BUILDER_CUSTOM_PATH + API_EDITOR_ID_PATH,
BUILDER_PATH_DEPRECATED + API_EDITOR_ID_PATH,
],
},
{
key: "AddQuery",
exact: true,
component: (args) => (
<Suspense fallback={<Skeleton />}>
<AddQuery {...args} />
</Suspense>
),
path: [`${path}${ADD_PATH}`, `${path}/:baseQueryId${ADD_PATH}`],
},
{
key: "SAASEditor",
component: (args) => {
return (
<Suspense fallback={<Skeleton />}>
<QueryEditor {...args} />
</Suspense>
);
},
exact: true,
path: [
BUILDER_PATH + SAAS_EDITOR_API_ID_PATH,
BUILDER_CUSTOM_PATH + SAAS_EDITOR_API_ID_PATH,
BUILDER_PATH_DEPRECATED + SAAS_EDITOR_API_ID_PATH,
],
},
{
key: "QueryEditor",
component: (args) => {
return (
<Suspense fallback={<Skeleton />}>
<QueryEditor {...args} />
</Suspense>
);
},
exact: true,
path: [path + "/:baseQueryId"],
},
{
key: "QueryEmpty",
component: (args) => (
<Suspense fallback={<Skeleton />}>
<QueryEmpty {...args} />
</Suspense>
),
exact: true,
path: [path],
},
],
[path],
);
};

export const useAddQueryListItems = () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import {
WIDGETS_EDITOR_ID_PATH,
} from "constants/routes";
import CreateNewDatasourceTab from "pages/Editor/IntegrationEditor/CreateNewDatasourceTab";
import OnboardingChecklist from "pages/Editor/FirstTimeUserOnboarding/Checklist";
import {
SAAS_EDITOR_API_ID_ADD_PATH,
SAAS_EDITOR_API_ID_PATH,
Expand All @@ -38,7 +37,28 @@ import GeneratePage from "pages/Editor/GeneratePage";
import type { RouteProps } from "react-router";
import { useSelector } from "react-redux";
import { combinedPreviewModeSelector } from "selectors/editorSelectors";
import { lazy, Suspense } from "react";
import React from "react";

import { retryPromise } from "utils/AppsmithUtils";
import Skeleton from "widgets/Skeleton";

const FirstTimeUserOnboardingChecklist = lazy(async () =>
retryPromise(
async () =>
import(
/* webpackChunkName: "FirstTimeUserOnboardingChecklist" */ "pages/Editor/FirstTimeUserOnboarding/Checklist"
),
),
);
Comment on lines +46 to +53
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider adding an error boundary for lazy loading failures.

The lazy loading implementation is solid with retry capability. However, adding an error boundary would provide a better user experience when the chunk fails to load.

class LazyLoadErrorBoundary extends React.Component {
  state = { hasError: false };
  
  static getDerivedStateFromError() {
    return { hasError: true };
  }
  
  render() {
    if (this.state.hasError) {
      return <div>Failed to load component. Please refresh the page.</div>;
    }
    return this.props.children;
  }
}


export const LazilyLoadedFirstTimeUserOnboardingChecklist = () => {
return (
<Suspense fallback={<Skeleton />}>
<FirstTimeUserOnboardingChecklist />
</Suspense>
);
};
Comment on lines +55 to +61
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider making the wrapper component more reusable.

While the implementation is correct, consider creating a generic wrapper to handle lazy loading of components with consistent loading states.

-export const LazilyLoadedFirstTimeUserOnboardingChecklist = () => {
+interface LazyComponentProps {
+  Component: React.LazyExoticComponent<any>;
+}
+
+export const LazyLoadWrapper = ({ Component }: LazyComponentProps) => {
   return (
     <Suspense fallback={<Skeleton />}>
-      <FirstTimeUserOnboardingChecklist />
+      <Component />
     </Suspense>
   );
 };
+
+export const LazilyLoadedFirstTimeUserOnboardingChecklist = () => (
+  <LazyLoadWrapper Component={FirstTimeUserOnboardingChecklist} />
+);

Committable suggestion was skipped due to low confidence.

export interface RouteReturnType extends RouteProps {
key: string;
}
Expand Down Expand Up @@ -95,7 +115,9 @@ function useRoutes(path: string): RouteReturnType[] {
},
{
key: "OnboardingChecklist",
component: isPreviewMode ? WidgetsEditor : OnboardingChecklist,
component: isPreviewMode
? WidgetsEditor
: FirstTimeUserOnboardingChecklist,
Comment on lines +118 to +120
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Use the wrapper component in route configuration.

The route is using the lazy component directly instead of the wrapper component. This could lead to inconsistent loading states across routes.

-      component: isPreviewMode
-        ? WidgetsEditor
-        : FirstTimeUserOnboardingChecklist,
+      component: isPreviewMode
+        ? WidgetsEditor
+        : LazilyLoadedFirstTimeUserOnboardingChecklist,

Committable suggestion was skipped due to low confidence.

exact: true,
path: `${path}${BUILDER_CHECKLIST_PATH}`,
},
Expand Down
24 changes: 21 additions & 3 deletions app/client/src/pages/Editor/FirstTimeUserOnboarding/Modal.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,29 @@
import React from "react";
import React, { lazy, Suspense } from "react";
import { MenuContent } from "@appsmith/ads";
import styled from "styled-components";
import Checklist from "./Checklist";
import HelpMenu from "./HelpMenu";
import { useDispatch } from "react-redux";
import { showSignpostingModal } from "actions/onboardingActions";

import { retryPromise } from "utils/AppsmithUtils";
import Skeleton from "widgets/Skeleton";

const Checklist = lazy(async () =>
retryPromise(
async () =>
import(
/* webpackChunkName: "FirstTimeUserOnboardingChecklist" */ "./Checklist"
),
),
);

export const LazilyLoadedChecklist = () => {
return (
<Suspense fallback={<Skeleton />}>
<Checklist />
</Suspense>
);
};
Comment on lines +20 to +26
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider adding error boundary

While the Suspense fallback is implemented correctly, consider adding an error boundary to handle loading failures gracefully.

 export const LazilyLoadedChecklist = () => {
   return (
+    <ErrorBoundary fallback={<ErrorState />}>
     <Suspense fallback={<Skeleton />}>
       <Checklist />
     </Suspense>
+    </ErrorBoundary>
   );
 };

Committable suggestion was skipped due to low confidence.

const SIGNPOSTING_POPUP_WIDTH = "360px";

const StyledMenuContent = styled(MenuContent)<{ animate: boolean }>`
Expand Down Expand Up @@ -48,7 +66,7 @@ function OnboardingModal(props: {
width={SIGNPOSTING_POPUP_WIDTH}
>
<Wrapper>
{!props.showIntercomConsent && <Checklist />}
{!props.showIntercomConsent && <LazilyLoadedChecklist />}
<HelpMenu
setShowIntercomConsent={props.setShowIntercomConsent}
showIntercomConsent={props.showIntercomConsent}
Expand Down
Loading