Skip to content

Commit be741e6

Browse files
authored
feat: display workspace logos in sidebar navigation (#41377)
- Modified Workspace.java to return null for logoUrl when no logo exists - Updated workspace reducer to sync logo changes to search results - Enhanced WorkspaceMenuItem to display uploaded logos with fallback to default icon - Added proper error handling and styling for workspace logo display ## Description > [!TIP] > _Add a TL;DR when the description is longer than 500 words or extremely technical (helps the content, marketing, and DevRel team)._ > > _Please also include relevant motivation and context. List any dependencies that are required for this change. Add links to Notion, Figma or any other documents that might be relevant to the PR._ Fixes #`41376` ## Automation /ok-to-test tags="@tag.Workspace" ### 🔍 Cypress test results <!-- This is an auto-generated comment: Cypress test results --> > [!TIP] > 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉 > Workflow run: <https://github.com/appsmithorg/appsmith/actions/runs/20007050511> > Commit: 3dc1661 > <a href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=20007050511&attempt=1" target="_blank">Cypress dashboard</a>. > Tags: `@tag.Workspace` > Spec: > <hr>Sun, 07 Dec 2025 16:47:49 UTC <!-- end of auto-generated comment: Cypress test results --> ## Communication Should the DevRel and Marketing teams inform users about this change? - [ ] Yes - [ ] No <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Workspace menu items and left navigation now show workspace logos when available, with a compact row layout, improved hover/selected styling, and graceful image fallback. * Search results items use the same logo-aware row for consistent navigation and click behavior. * **Bug Fixes** * Missing or failed workspace logos no longer trigger unnecessary asset requests and fall back to text/icon display. * Search results stay synchronized when workspace details are updated. <sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 7046aeb commit be741e6

File tree

4 files changed

+177
-19
lines changed

4 files changed

+177
-19
lines changed

app/client/src/ce/pages/Applications/index.tsx

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ import {
6060
MenuItem as ListItem,
6161
Text,
6262
TextType,
63+
FontWeight,
6364
} from "@appsmith/ads-old";
6465
import { loadingUserWorkspaces } from "pages/Applications/ApplicationLoaders";
6566
import PageWrapper from "pages/common/PageWrapper";
@@ -395,6 +396,59 @@ export const textIconStyles = (props: { color: string; hover: string }) => {
395396
`;
396397
};
397398

399+
const WorkspaceItemRow = styled.a<{ disabled?: boolean; selected?: boolean }>`
400+
display: flex;
401+
align-items: center;
402+
justify-content: space-between;
403+
text-decoration: none;
404+
padding: 0px var(--ads-spaces-6);
405+
background-color: ${(props) =>
406+
props.selected ? "var(--ads-v2-color-bg-muted)" : "transparent"};
407+
.${Classes.TEXT} {
408+
color: var(--ads-v2-color-fg);
409+
}
410+
.${Classes.ICON} {
411+
svg {
412+
path {
413+
fill: var(--ads-v2-color-fg);
414+
}
415+
}
416+
}
417+
height: 38px;
418+
419+
${(props) =>
420+
!props.disabled
421+
? `
422+
&:hover {
423+
text-decoration: none;
424+
cursor: pointer;
425+
background-color: var(--ads-v2-color-bg-subtle);
426+
border-radius: var(--ads-v2-border-radius);
427+
}`
428+
: `
429+
&:hover {
430+
text-decoration: none;
431+
cursor: default;
432+
}
433+
`}
434+
`;
435+
436+
const WorkspaceIconContainer = styled.span`
437+
display: flex;
438+
align-items: center;
439+
440+
.${Classes.ICON} {
441+
margin-right: var(--ads-spaces-5);
442+
}
443+
`;
444+
445+
const WorkspaceLogoImage = styled.img`
446+
width: 16px;
447+
height: 16px;
448+
object-fit: contain;
449+
margin-right: var(--ads-spaces-5);
450+
`;
451+
398452
export function WorkspaceMenuItem({
399453
isFetchingWorkspaces,
400454
selected,
@@ -403,6 +457,7 @@ export function WorkspaceMenuItem({
403457
}: any) {
404458
const history = useHistory();
405459
const location = useLocation();
460+
const [imageError, setImageError] = React.useState(false);
406461

407462
const handleWorkspaceClick = () => {
408463
const workspaceId = workspace?.id;
@@ -414,8 +469,50 @@ export function WorkspaceMenuItem({
414469
}
415470
};
416471

472+
const handleImageError = () => {
473+
setImageError(true);
474+
};
475+
417476
if (!workspace.id) return null;
418477

478+
const hasLogo = workspace?.logoUrl && !imageError;
479+
const displayText = isFetchingWorkspaces
480+
? workspace?.name
481+
: workspace?.name?.length > 22
482+
? workspace.name.slice(0, 22).concat(" ...")
483+
: workspace?.name;
484+
485+
// Use custom component when there's a logo, otherwise use ListItem
486+
if (hasLogo && !isFetchingWorkspaces) {
487+
const showTooltip = workspace?.name && workspace.name.length > 22;
488+
const itemContent = (
489+
<WorkspaceItemRow
490+
className={selected ? "selected-workspace" : ""}
491+
onClick={handleWorkspaceClick}
492+
selected={selected}
493+
>
494+
<WorkspaceIconContainer>
495+
<WorkspaceLogoImage
496+
alt={`${workspace.name} logo`}
497+
onError={handleImageError}
498+
src={workspace.logoUrl}
499+
/>
500+
<Text type={TextType.H5} weight={FontWeight.NORMAL}>
501+
{displayText}
502+
</Text>
503+
</WorkspaceIconContainer>
504+
</WorkspaceItemRow>
505+
);
506+
507+
return showTooltip ? (
508+
<Tooltip content={workspace?.name} placement="bottomLeft">
509+
{itemContent}
510+
</Tooltip>
511+
) : (
512+
itemContent
513+
);
514+
}
515+
419516
return (
420517
<ListItem
421518
className={selected ? "selected-workspace" : ""}

app/client/src/ce/reducers/uiReducers/workspaceReducer.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,21 @@ export const handlers = {
108108

109109
draftState.loadingStates.isSavingWorkspaceInfo = false;
110110
draftState.list = [...workspaces];
111+
112+
// Also update searchEntities if they exist to keep search results in sync
113+
if (draftState.searchEntities?.workspaces) {
114+
const searchWorkspaceIndex =
115+
draftState.searchEntities.workspaces.findIndex(
116+
(workspace: Workspace) => workspace.id === action.payload.id,
117+
);
118+
119+
if (searchWorkspaceIndex !== -1) {
120+
draftState.searchEntities.workspaces[searchWorkspaceIndex] = {
121+
...draftState.searchEntities.workspaces[searchWorkspaceIndex],
122+
...action.payload,
123+
};
124+
}
125+
}
111126
},
112127
[ReduxActionErrorTypes.SAVE_WORKSPACE_ERROR]: (
113128
draftState: WorkspaceReduxState,

app/client/src/pages/common/SearchBar/WorkspaceSearchItems.tsx

Lines changed: 60 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { Workspace } from "ee/constants/workspaceConstants";
22
import { Icon, Text } from "@appsmith/ads";
3-
import React from "react";
3+
import React, { useState } from "react";
44
import { useHistory } from "react-router";
55
import styled from "styled-components";
66

@@ -15,14 +15,68 @@ export const SearchListItem = styled.div`
1515
}
1616
`;
1717

18+
const WorkspaceLogoImage = styled.img`
19+
width: 16px;
20+
height: 16px;
21+
min-width: 16px;
22+
min-height: 16px;
23+
object-fit: contain;
24+
margin-right: 8px;
25+
`;
26+
1827
interface Props {
1928
workspacesList: Workspace[] | undefined;
2029
setIsDropdownOpen: (isOpen: boolean) => void;
2130
}
2231

32+
interface WorkspaceItemProps {
33+
workspace: Workspace;
34+
setIsDropdownOpen: (isOpen: boolean) => void;
35+
}
36+
37+
const WorkspaceItem = ({
38+
setIsDropdownOpen,
39+
workspace,
40+
}: WorkspaceItemProps) => {
41+
const history = useHistory();
42+
const [imageError, setImageError] = useState(false);
43+
const hasLogo = workspace.logoUrl && !imageError;
44+
45+
const handleImageError = () => {
46+
setImageError(true);
47+
};
48+
49+
return (
50+
<SearchListItem
51+
data-testid={workspace.name}
52+
onClick={() => {
53+
setIsDropdownOpen(false);
54+
history.push(`/applications?workspaceId=${workspace?.id}`);
55+
}}
56+
>
57+
{hasLogo ? (
58+
<WorkspaceLogoImage
59+
alt={`${workspace.name} logo`}
60+
onError={handleImageError}
61+
src={workspace.logoUrl}
62+
/>
63+
) : (
64+
<Icon
65+
className="!mr-2"
66+
color="var(--ads-v2-color-fg)"
67+
name="group-2-line"
68+
size="md"
69+
/>
70+
)}
71+
<Text className="truncate" kind="body-m">
72+
{workspace.name}
73+
</Text>
74+
</SearchListItem>
75+
);
76+
};
77+
2378
const WorkspaceSearchItems = (props: Props) => {
2479
const { setIsDropdownOpen, workspacesList } = props;
25-
const history = useHistory();
2680

2781
if (!workspacesList || workspacesList?.length === 0) return null;
2882

@@ -32,24 +86,11 @@ const WorkspaceSearchItems = (props: Props) => {
3286
Workspaces
3387
</Text>
3488
{workspacesList.map((workspace: Workspace) => (
35-
<SearchListItem
36-
data-testid={workspace.name}
89+
<WorkspaceItem
3790
key={workspace.id}
38-
onClick={() => {
39-
setIsDropdownOpen(false);
40-
history.push(`/applications?workspaceId=${workspace?.id}`);
41-
}}
42-
>
43-
<Icon
44-
className="!mr-2"
45-
color="var(--ads-v2-color-fg)"
46-
name="group-2-line"
47-
size="md"
48-
/>
49-
<Text className="truncate" kind="body-m">
50-
{workspace.name}
51-
</Text>
52-
</SearchListItem>
91+
setIsDropdownOpen={setIsDropdownOpen}
92+
workspace={workspace}
93+
/>
5394
))}
5495
</div>
5596
);

app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Workspace.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,11 @@ public static String toSlug(String text) {
7171

7272
@JsonView(Views.Public.class)
7373
public String getLogoUrl() {
74+
// If there's no logo, return null instead of constructing a URL like "/api/v1/assets/null"
75+
// This prevents the frontend from making pointless requests to load a non-existent image
76+
if (logoAssetId == null || logoAssetId.isEmpty()) {
77+
return null;
78+
}
7479
return Url.ASSET_URL + "/" + logoAssetId;
7580
}
7681

0 commit comments

Comments
 (0)