Skip to content
Open
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
38 changes: 38 additions & 0 deletions app/client/src/actions/applicationActions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import {
ReduxActionErrorTypes,
ReduxActionTypes,
} from "ee/constants/ReduxActionConstants";
import type { ApplicationPayload } from "entities/Application";

export const toggleFavoriteApplication = (applicationId: string) => ({
type: ReduxActionTypes.TOGGLE_FAVORITE_APPLICATION_INIT,
payload: { applicationId },
});

export const toggleFavoriteApplicationSuccess = (
applicationId: string,
isFavorited: boolean,
) => ({
type: ReduxActionTypes.TOGGLE_FAVORITE_APPLICATION_SUCCESS,
payload: { applicationId, isFavorited },
});

export const toggleFavoriteApplicationError = (applicationId: string) => ({
type: ReduxActionErrorTypes.TOGGLE_FAVORITE_APPLICATION_ERROR,
payload: { applicationId },
});

export const fetchFavoriteApplications = () => ({
type: ReduxActionTypes.FETCH_FAVORITE_APPLICATIONS_INIT,
});

export const fetchFavoriteApplicationsSuccess = (
applications: ApplicationPayload[],
) => ({
type: ReduxActionTypes.FETCH_FAVORITE_APPLICATIONS_SUCCESS,
payload: applications,
});

export const fetchFavoriteApplicationsError = () => ({
type: ReduxActionErrorTypes.FETCH_FAVORITE_APPLICATIONS_ERROR,
});
4 changes: 4 additions & 0 deletions app/client/src/assets/icons/ads/heart-fill-red.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions app/client/src/ce/api/ApplicationApi.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,16 @@ export class ApplicationApi extends Api {
`${ApplicationApi.baseURL}/${applicationId}/static-url/suggest-app-slug`,
);
}

static async toggleFavoriteApplication(
applicationId: string,
): Promise<AxiosPromise<ApiResponse>> {
return Api.put(`v1/users/applications/${applicationId}/favorite`);
}

static async getFavoriteApplications(): Promise<AxiosPromise<ApiResponse>> {
return Api.get("v1/users/favoriteApplications");
}
}

export default ApplicationApi;
6 changes: 6 additions & 0 deletions app/client/src/ce/constants/ReduxActionConstants.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -679,6 +679,10 @@ const ApplicationActionTypes = {
FORK_APPLICATION_INIT: "FORK_APPLICATION_INIT",
FORK_APPLICATION_SUCCESS: "FORK_APPLICATION_SUCCESS",
RESET_CURRENT_APPLICATION: "RESET_CURRENT_APPLICATION",
TOGGLE_FAVORITE_APPLICATION_INIT: "TOGGLE_FAVORITE_APPLICATION_INIT",
TOGGLE_FAVORITE_APPLICATION_SUCCESS: "TOGGLE_FAVORITE_APPLICATION_SUCCESS",
FETCH_FAVORITE_APPLICATIONS_INIT: "FETCH_FAVORITE_APPLICATIONS_INIT",
FETCH_FAVORITE_APPLICATIONS_SUCCESS: "FETCH_FAVORITE_APPLICATIONS_SUCCESS",
};

const ApplicationActionErrorTypes = {
Expand All @@ -692,6 +696,8 @@ const ApplicationActionErrorTypes = {
FETCH_APP_SLUG_SUGGESTION_ERROR: "FETCH_APP_SLUG_SUGGESTION_ERROR",
ENABLE_STATIC_URL_ERROR: "ENABLE_STATIC_URL_ERROR",
DISABLE_STATIC_URL_ERROR: "DISABLE_STATIC_URL_ERROR",
TOGGLE_FAVORITE_APPLICATION_ERROR: "TOGGLE_FAVORITE_APPLICATION_ERROR",
FETCH_FAVORITE_APPLICATIONS_ERROR: "FETCH_FAVORITE_APPLICATIONS_ERROR",
};

const IDEDebuggerActionTypes = {
Expand Down
10 changes: 10 additions & 0 deletions app/client/src/ce/constants/workspaceConstants.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
export const FAVORITES_KEY = "__favorites__";

export const DEFAULT_FAVORITES_WORKSPACE = {
id: FAVORITES_KEY,
name: "Favorites",
isVirtual: true,
userPermissions: [] as string[],
};

export interface WorkspaceRole {
id: string;
name: string;
Expand All @@ -13,6 +22,7 @@ export interface Workspace {
logoUrl?: string;
uploadProgress?: number;
userPermissions?: string[];
isVirtual?: boolean;
}

export interface WorkspaceUserRoles {
Expand Down
31 changes: 31 additions & 0 deletions app/client/src/ce/pages/Applications/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,16 @@ import {
getIsCreatingApplication,
getIsDeletingApplication,
} from "ee/selectors/applicationSelectors";
import { getHasFavorites } from "ee/selectors/applicationSelectors";
import {
DEFAULT_FAVORITES_WORKSPACE,
FAVORITES_KEY,
} from "ee/constants/workspaceConstants";
import { Classes as BlueprintClasses } from "@blueprintjs/core";
import { Position } from "@blueprintjs/core/lib/esm/common/position";
import { leaveWorkspace } from "actions/userActions";
import NoSearchImage from "assets/images/NoSearchResult.svg";
import HeartIconRed from "assets/icons/ads/heart-fill-red.svg";
import CenteredWrapper from "components/designSystems/appsmith/CenteredWrapper";
import {
thinScrollbar,
Expand Down Expand Up @@ -475,13 +481,32 @@ export function WorkspaceMenuItem({

if (!workspace.id) return null;

const isFavoritesWorkspace = workspace.id === FAVORITES_KEY;
const hasLogo = workspace?.logoUrl && !imageError;
const displayText = isFetchingWorkspaces
? workspace?.name
: workspace?.name?.length > 22
? workspace.name.slice(0, 22).concat(" ...")
: workspace?.name;

// Use custom component for favorites workspace with heart icon
if (isFavoritesWorkspace && !isFetchingWorkspaces) {
return (
<WorkspaceItemRow
className={selected ? "selected-workspace" : ""}
onClick={handleWorkspaceClick}
selected={selected}
>
<WorkspaceIconContainer>
<WorkspaceLogoImage alt="Favorites" src={HeartIconRed} />
<Text type={TextType.H5} weight={FontWeight.NORMAL}>
{displayText}
</Text>
</WorkspaceIconContainer>
</WorkspaceItemRow>
);
}

// Use custom component when there's a logo, otherwise use ListItem
if (hasLogo && !isFetchingWorkspaces) {
const showTooltip = workspace?.name && workspace.name.length > 22;
Expand Down Expand Up @@ -1120,6 +1145,7 @@ export const ApplictionsMainPage = (props: any) => {
const isFetchingOrganizations = useSelector(getIsFetchingMyOrganizations);
const currentOrganizationId = useSelector(activeOrganizationId);
const isCloudBillingEnabled = useIsCloudBillingEnabled();
const hasFavorites = useSelector(getHasFavorites);

// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand All @@ -1135,6 +1161,11 @@ export const ApplictionsMainPage = (props: any) => {
) as any;
}

// Inject virtual Favorites workspace at the top if user has favorites
if (hasFavorites && !isFetchingWorkspaces) {
workspaces = [DEFAULT_FAVORITES_WORKSPACE, ...workspaces];
}
Comment on lines 1164 to 1167
Copy link
Contributor

@coderabbitai coderabbitai bot Dec 17, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Potential cause of intermittent navigation bug reported in PR comments.

When hasFavorites is true, the favorites workspace is prepended to the array. If workspaceIdFromQueryParams is null/undefined, line 1188 defaults to workspaces[0]?.id, which would be __favorites__.

This may explain the reviewer's report of sometimes landing on Favorites instead of the expected workspace when navigating Home.

Consider preserving the user's last-visited real workspace or excluding the virtual workspace from the default selection:

  // Inject virtual Favorites workspace at the top if user has favorites
  if (hasFavorites && !isFetchingWorkspaces) {
    const favoritesWorkspace = {
      id: "__favorites__",
      name: "Favorites",
      isVirtual: true,
      userPermissions: [],
    };

    workspaces = [favoritesWorkspace, ...workspaces];
  }

+  // Find first real workspace for default selection (skip virtual workspaces)
+  const defaultWorkspace = workspaces.find((ws: Workspace) => !ws.isVirtual);
+
   const [activeWorkspaceId, setActiveWorkspaceId] = useState<
     string | undefined
   >(
-    workspaceIdFromQueryParams ? workspaceIdFromQueryParams : workspaces[0]?.id,
+    workspaceIdFromQueryParams ? workspaceIdFromQueryParams : defaultWorkspace?.id,
   );

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In app/client/src/ce/pages/Applications/index.tsx around lines 1160 to 1170,
prepending a virtual "Favorites" workspace causes the later defaulting logic
(when workspaceIdFromQueryParams is falsy) to pick workspaces[0]?.id which
becomes "__favorites__"; change the default-selection logic so the virtual
workspace is excluded: when workspaceIdFromQueryParams is null/undefined, choose
lastVisitedWorkspaceId if available, otherwise pick the first workspace whose
isVirtual flag is false (or the first real workspace in the list), and only
prepend the virtual Favorites for display (not for determining the default
selection). Ensure the code checks for existence of a real workspace before
falling back to the virtual id so navigation no longer lands on Favorites
unexpectedly.

Copy link
Contributor

Choose a reason for hiding this comment

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

@salevine This callout seems legit; can you check?

Copy link
Contributor

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I resolved this


const [activeWorkspaceId, setActiveWorkspaceId] = useState<
string | undefined
>(
Expand Down
42 changes: 42 additions & 0 deletions app/client/src/ce/reducers/uiReducers/applicationsReducer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ export const initialState: ApplicationsReduxState = {
creatingApplication: {},
deletingApplication: false,
forkingApplication: false,
favoriteApplicationIds: [],
isFetchingFavorites: false,
importingApplication: false,
importedApplication: null,
isImportAppModalOpen: false,
Expand Down Expand Up @@ -881,6 +883,44 @@ export const handlers = {
isPersistingAppSlug: false,
};
},
[ReduxActionTypes.TOGGLE_FAVORITE_APPLICATION_SUCCESS]: (
state: ApplicationsReduxState,
action: ReduxAction<{ applicationId: string; isFavorited: boolean }>,
) => {
const { applicationId, isFavorited } = action.payload;

return {
...state,
favoriteApplicationIds: isFavorited
? [...state.favoriteApplicationIds, applicationId]
: state.favoriteApplicationIds.filter((id) => id !== applicationId),
applicationList: state.applicationList.map((app) =>
(app.baseId || app.id) === applicationId
? { ...app, isFavorited }
: app,
),
};
},
[ReduxActionTypes.FETCH_FAVORITE_APPLICATIONS_INIT]: (
state: ApplicationsReduxState,
) => ({
...state,
isFetchingFavorites: true,
}),
[ReduxActionTypes.FETCH_FAVORITE_APPLICATIONS_SUCCESS]: (
state: ApplicationsReduxState,
action: ReduxAction<ApplicationPayload[]>,
) => ({
...state,
isFetchingFavorites: false,
favoriteApplicationIds: action.payload.map((app) => app.id),
}),
[ReduxActionErrorTypes.FETCH_FAVORITE_APPLICATIONS_ERROR]: (
state: ApplicationsReduxState,
) => ({
...state,
isFetchingFavorites: false,
}),
};

const applicationsReducer = createReducer(initialState, handlers);
Expand All @@ -898,6 +938,8 @@ export interface ApplicationsReduxState {
createApplicationError?: string;
deletingApplication: boolean;
forkingApplication: boolean;
favoriteApplicationIds: string[];
isFetchingFavorites: boolean;
currentApplication?: ApplicationPayload;
importingApplication: boolean;
importedApplication: unknown;
Expand Down
44 changes: 44 additions & 0 deletions app/client/src/ce/reducers/uiReducers/selectedWorkspaceReducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type {
WorkspaceUser,
WorkspaceUserRoles,
} from "ee/constants/workspaceConstants";
import { FAVORITES_KEY } from "ee/constants/workspaceConstants";
import type { Package } from "ee/constants/PackageConstants";
import type { UpdateApplicationRequest } from "ee/api/ApplicationApi";

Expand Down Expand Up @@ -59,6 +60,30 @@ export const handlers = {
) => {
draftState.loadingStates.isFetchingApplications = false;
},
// Handle favorites workspace - populate applications with favorite apps
[ReduxActionTypes.FETCH_FAVORITE_APPLICATIONS_INIT]: (
draftState: SelectedWorkspaceReduxState,
) => {
draftState.loadingStates.isFetchingApplications = true;
Copy link
Contributor

Choose a reason for hiding this comment

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

Why are we updating isFetchingApplications while fetching favorite applications?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

favorites is being treated like a regular workspace - so we reused this without creating a new loading state. If you really wanted, we could create a different state. Seemed simpler to keep one

},
[ReduxActionTypes.FETCH_FAVORITE_APPLICATIONS_SUCCESS]: (
draftState: SelectedWorkspaceReduxState,
action: ReduxAction<ApplicationPayload[]>,
) => {
draftState.loadingStates.isFetchingApplications = false;

// Only replace applications when we're in the virtual favorites workspace.
// This prevents overwriting a real workspace's applications when favorites
// are fetched in the background.
if (draftState.workspace.id === FAVORITES_KEY) {
draftState.applications = action.payload;
}
},
[ReduxActionErrorTypes.FETCH_FAVORITE_APPLICATIONS_ERROR]: (
draftState: SelectedWorkspaceReduxState,
) => {
draftState.loadingStates.isFetchingApplications = false;
},
[ReduxActionTypes.DELETE_APPLICATION_SUCCESS]: (
draftState: SelectedWorkspaceReduxState,
action: ReduxAction<ApplicationPayload>,
Expand Down Expand Up @@ -242,6 +267,25 @@ export const handlers = {
) => {
draftState.loadingStates.isFetchingCurrentWorkspace = false;
},
[ReduxActionTypes.TOGGLE_FAVORITE_APPLICATION_SUCCESS]: (
draftState: SelectedWorkspaceReduxState,
action: ReduxAction<{ applicationId: string; isFavorited: boolean }>,
) => {
const { applicationId, isFavorited } = action.payload;
const isFavoritesWorkspace = draftState.workspace.id === FAVORITES_KEY;

if (isFavoritesWorkspace && !isFavorited) {
draftState.applications = draftState.applications.filter(
(app) => (app.baseId || app.id) !== applicationId,
);
} else {
draftState.applications = draftState.applications.map((app) =>
(app.baseId || app.id) === applicationId
? { ...app, isFavorited }
: app,
);
}
},
};

const selectedWorkspaceReducer = createImmerReducer(initialState, handlers);
Expand Down
30 changes: 29 additions & 1 deletion app/client/src/ce/sagas/WorkspaceSagas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ import WorkspaceApi from "ee/api/WorkspaceApi";
import type { ApiResponse } from "api/ApiResponses";
import { getFetchedWorkspaces } from "ee/selectors/workspaceSelectors";
import { getCurrentUser } from "selectors/usersSelectors";
import {
DEFAULT_FAVORITES_WORKSPACE,
FAVORITES_KEY,
} from "ee/constants/workspaceConstants";
import type { Workspace } from "ee/constants/workspaceConstants";
import history from "utils/history";
import { APPLICATIONS_URL } from "constants/routes";
Expand Down Expand Up @@ -62,6 +66,10 @@ export function* fetchAllWorkspacesSaga(
payload: workspaces,
});

// Fetch user's favorite applications to populate favoriteApplicationIds
// This ensures favorites are shown correctly across all workspaces
yield put({ type: ReduxActionTypes.FETCH_FAVORITE_APPLICATIONS_INIT });

if (action?.payload?.workspaceId || action?.payload?.fetchEntities) {
yield call(fetchEntitiesOfWorkspaceSaga, action);
}
Expand All @@ -82,6 +90,18 @@ export function* fetchEntitiesOfWorkspaceSaga(
try {
const allWorkspaces: Workspace[] = yield select(getFetchedWorkspaces);
const workspaceId = action?.payload?.workspaceId || allWorkspaces[0]?.id;

// Handle virtual favorites workspace specially
if (workspaceId === FAVORITES_KEY) {
yield put({
type: ReduxActionTypes.SET_CURRENT_WORKSPACE,
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need to explicitly set the current workspace here? I believe this would happen automatically when the workspace is clicked on the left pane

payload: DEFAULT_FAVORITES_WORKSPACE,
});
yield put({ type: ReduxActionTypes.FETCH_FAVORITE_APPLICATIONS_INIT });

return;
}

const activeWorkspace = allWorkspaces.find(
(workspace) => workspace.id === workspaceId,
);
Expand Down Expand Up @@ -338,10 +358,18 @@ export function* createWorkspaceSaga(
yield call(resolve);
}

// get created workspace in focus
// Get created workspace in focus
// @ts-expect-error: response is of type unknown
const workspaceId = response.data.id;

// Refresh workspaces and entities for the newly created workspace so that
// the left panel and applications list reflect the new workspace instead of
// staying on the previous (e.g. Favorites) virtual workspace.
yield put({
type: ReduxActionTypes.FETCH_ALL_WORKSPACES_INIT,
payload: { workspaceId, fetchEntities: true },
});

Comment on lines +365 to +372
Copy link
Contributor

Choose a reason for hiding this comment

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

I didn't quite get the intent of this. Right now when we create a new workspace, user is redirected to the new workspace and you can see that happening in line 374. When the redirection happens we automatically fetch. So what problem are we fixing here and how does it get resolved by fetching all workspaces?

history.push(`${window.location.pathname}?workspaceId=${workspaceId}`);
} catch (error) {
yield call(reject, { _error: (error as Error).message });
Expand Down
2 changes: 2 additions & 0 deletions app/client/src/ce/sagas/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import SuperUserSagas from "ee/sagas/SuperUserSagas";
import organizationSagas from "ee/sagas/organizationSagas";
import userSagas from "ee/sagas/userSagas";
import workspaceSagas from "ee/sagas/WorkspaceSagas";
import favoritesSagasListener from "sagas/FavoritesSagas";
Copy link
Contributor

Choose a reason for hiding this comment

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

Do not create a new saga; since they are just virtual workspaces, move all your logic to workspaceSagas

import { watchPluginActionExecutionSagas } from "sagas/ActionExecution/PluginActionSaga";
import { watchActionSagas } from "sagas/ActionSagas";
import apiPaneSagas from "sagas/ApiPaneSagas";
Expand Down Expand Up @@ -115,4 +116,5 @@ export const sagas = [
gitSagas,
gitApplicationSagas,
PostEvaluationSagas,
favoritesSagasListener,
];
Loading
Loading