Skip to content
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
2ea0ad0
Working on favorites application and virtual workspace
salevine Dec 12, 2025
6625359
Adding favorites for applications
salevine Dec 12, 2025
91b3e97
fix: remove unused ApplicationPayload import from ce/actions/applicat…
salevine Dec 14, 2025
3094280
revert: remove export from ee/actions/applicationActions to avoid pre…
salevine Dec 14, 2025
ad85f62
Fix favorites limit, correct home page bug, address permissions issue
salevine Dec 14, 2025
c8e7267
Fix found issues
salevine Dec 15, 2025
d4065de
Fix a few more bugs to avoid unnnecessary re-renders
salevine Dec 15, 2025
82d542e
Adjust favorite icon and fix new workspace creation issue. Also addre…
salevine Dec 15, 2025
f5c0aec
Address favorites possibly overwriting non-favorites and better error…
salevine Dec 15, 2025
eb2635e
Fix minor card permission issue
salevine Dec 15, 2025
a9b7911
Addressed code review concerns
salevine Jan 23, 2026
1863d2c
Addressed code rabbit conerns
salevine Jan 23, 2026
710f2df
Addressing comments
salevine Jan 26, 2026
c4f4e5b
Update ApplicationPageServiceImpl.java
salevine Jan 26, 2026
2c8fc7a
fix: favorites cleanup and review feedback
salevine Feb 9, 2026
c7b242c
fix: favorites saga error handling and validation
salevine Feb 9, 2026
9c1d474
fix: make favorite icon a semantic button with a11y (Card)
salevine Feb 9, 2026
ffdad18
fix(favorites): redirect deleted-favorite 404 to Favorites and fix vi…
salevine Feb 9, 2026
0555dfb
fix: extract inline JSX handlers into useCallback to fix lint warnings
salevine Feb 9, 2026
90ab0af
fix(favorites): avoid duplicate favorite fetches and add toggle error…
salevine Feb 10, 2026
396208b
refactor(favorites): decouple ApplicationCard permissions and separat…
salevine Feb 12, 2026
a674c75
fix(favorites): correct API URLs, normalize reducer IDs, and enforce …
salevine Feb 13, 2026
26eca73
fix(favorites): scope 404-to-favorites redirect to only favorited apps
salevine Feb 13, 2026
397b831
fix(favorites): make toggle fully atomic and handle new-user save races
salevine Feb 13, 2026
72a8032
fix(favorites): use unfiltered app list for permissions lookup
salevine Feb 13, 2026
56d760c
fix(favorites): decouple favorites loading from main app-loading indi…
salevine Feb 13, 2026
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
41 changes: 41 additions & 0 deletions app/client/src/actions/applicationActions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
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 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,
});

export const toggleFavoriteApplicationError = (
applicationId: string,
error: unknown,
) => ({
type: ReduxActionErrorTypes.TOGGLE_FAVORITE_APPLICATION_ERROR,
payload: { applicationId, error, show: false },
});
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
41 changes: 39 additions & 2 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 @@ -330,7 +336,6 @@ export function LeftPaneSection(props: {
},
dispatch,
);
dispatch(fetchAllWorkspaces());
};

return (
Expand Down Expand Up @@ -475,13 +480,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 +1144,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 +1160,14 @@ export const ApplictionsMainPage = (props: any) => {
) as any;
}

// Inject virtual Favorites workspace at the top if user has favorites or URL is Favorites (e.g. after 404 redirect)
if (
(hasFavorites || workspaceIdFromQueryParams === FAVORITES_KEY) &&
!isFetchingWorkspaces
) {
workspaces = [DEFAULT_FAVORITES_WORKSPACE, ...workspaces];
}

const [activeWorkspaceId, setActiveWorkspaceId] = useState<
string | undefined
>(
Expand All @@ -1159,10 +1192,14 @@ export const ApplictionsMainPage = (props: any) => {
fetchedWorkspaceId &&
fetchedWorkspaceId !== activeWorkspaceId
) {
const activeWorkspace: Workspace = workspaces.find(
let activeWorkspace: Workspace | undefined = workspaces.find(
(workspace: Workspace) => workspace.id === activeWorkspaceId,
);

if (!activeWorkspace && activeWorkspaceId === FAVORITES_KEY) {
activeWorkspace = DEFAULT_FAVORITES_WORKSPACE;
}

if (activeWorkspace) {
dispatch({
type: ReduxActionTypes.SET_CURRENT_WORKSPACE,
Expand Down
46 changes: 46 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,48 @@ export const handlers = {
isPersistingAppSlug: false,
};
},
[ReduxActionTypes.TOGGLE_FAVORITE_APPLICATION_SUCCESS]: (
state: ApplicationsReduxState,
action: ReduxAction<{ applicationId: string; isFavorited: boolean }>,
) => {
const { applicationId, isFavorited } = action.payload;
const matchedApp = state.applicationList.find(
(app) => (app.baseId || app.id) === applicationId,
);
const canonicalId = matchedApp ? matchedApp.id : applicationId;

return {
...state,
favoriteApplicationIds: isFavorited
? [...state.favoriteApplicationIds, canonicalId]
: state.favoriteApplicationIds.filter((id) => id !== canonicalId),
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 +942,8 @@ export interface ApplicationsReduxState {
createApplicationError?: string;
deletingApplication: boolean;
forkingApplication: boolean;
favoriteApplicationIds: string[];
isFetchingFavorites: boolean;
currentApplication?: ApplicationPayload;
importingApplication: boolean;
importedApplication: unknown;
Expand Down
46 changes: 46 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 All @@ -20,6 +21,7 @@ export interface SelectedWorkspaceReduxState {
packages: Package[];
loadingStates: {
isFetchingApplications: boolean;
isFetchingFavoriteApplications: boolean;
isFetchingAllUsers: boolean;
isFetchingCurrentWorkspace: boolean;
};
Expand All @@ -35,6 +37,7 @@ export const initialState: SelectedWorkspaceReduxState = {
packages: [],
loadingStates: {
isFetchingApplications: false,
isFetchingFavoriteApplications: false,
isFetchingAllUsers: false,
isFetchingCurrentWorkspace: false,
},
Expand All @@ -59,6 +62,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.isFetchingFavoriteApplications = true;
},
[ReduxActionTypes.FETCH_FAVORITE_APPLICATIONS_SUCCESS]: (
draftState: SelectedWorkspaceReduxState,
action: ReduxAction<ApplicationPayload[]>,
) => {
draftState.loadingStates.isFetchingFavoriteApplications = 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.isFetchingFavoriteApplications = false;
},
[ReduxActionTypes.DELETE_APPLICATION_SUCCESS]: (
draftState: SelectedWorkspaceReduxState,
action: ReduxAction<ApplicationPayload>,
Expand Down Expand Up @@ -242,6 +269,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
Loading
Loading