Skip to content

Commit 72a38f3

Browse files
committed
fix: label the app chip rows and render manifest icons in them
The app rows showed two unlabelled chip rows, so nothing told the user that one lists the pages the app adds to the project menu and the other the permissions it was granted. Both rows now carry a caption with an explanatory tooltip, and say so when they are empty. The page chips also concatenated the manifest icon into the label, which rendered a native icon name ("Key01") as literal text. They now go through AppIcon like the project menu does. AppIcon only forces a size when one is given, so an app icon in the menu matches the built-in items instead of rendering smaller, and the emoji fallback follows the same size.
1 parent ec9c96e commit 72a38f3

7 files changed

Lines changed: 170 additions & 25 deletions

File tree

apps/tolgee-apps-sdk/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ const manifest: AppManifest = {
5858
}
5959
```
6060

61+
`icon` is either a name from the platform icon set or an emoji (`'🔑'`). A name has to
62+
match an exported icon component exactly — `Key01`, not `Key`. Anything the platform
63+
does not recognise is rendered as literal text, so a typo shows up as the name itself.
64+
6165
Ship the manifest as a template with a `__BASE_URL__` placeholder and render it
6266
per request, so the URL can change between dev restarts (tunnels) without
6367
editing the file:

apps/tolgee-apps-sdk/src/shared/manifestTypes.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,11 @@ export type AppDashboardPage = {
3232
key: string
3333
/** Menu item label. */
3434
title: string
35-
/** Named icon from the platform icon set (e.g. `LayoutAlt04`). */
35+
/**
36+
* Either a named icon from the platform icon set or an emoji. A name must match an
37+
* exported icon component exactly (`Key01`, not `Key`); anything the platform does
38+
* not recognise is rendered as literal text.
39+
*/
3640
icon: string
3741
/** Route the iframe loads, relative to `baseUrl`. */
3842
entry: string
Lines changed: 86 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,104 @@
1-
import { Chip, styled } from '@mui/material';
1+
import React from 'react';
2+
import { Chip, styled, Tooltip, Typography } from '@mui/material';
3+
4+
const StyledRow = styled('div')`
5+
display: flex;
6+
flex-wrap: wrap;
7+
align-items: center;
8+
gap: ${({ theme }) => theme.spacing(0.75)};
9+
min-width: 0;
10+
`;
211

312
const StyledChips = styled('div')`
413
display: flex;
514
flex-wrap: wrap;
615
gap: ${({ theme }) => theme.spacing(0.75)};
16+
min-width: 0;
717
`;
818

19+
const StyledLabel = styled('span')(({ theme }) => ({
20+
...theme.typography.caption,
21+
color: theme.palette.text.secondary,
22+
textDecoration: 'underline dotted',
23+
textUnderlineOffset: 3,
24+
cursor: 'help',
25+
}));
26+
27+
const StyledChipLabel = styled('span')`
28+
display: inline-flex;
29+
align-items: center;
30+
gap: ${({ theme }) => theme.spacing(0.5)};
31+
`;
32+
33+
export type AppChipItem = {
34+
/** Stable identity within the row — used as the React key. */
35+
id: string;
36+
label: React.ReactNode;
37+
};
38+
939
type AppChipsProps = {
10-
items: string[];
40+
items: (string | AppChipItem)[];
1141
dataCy: string;
1242
color?: 'default' | 'info' | 'warning';
1343
variant?: 'filled' | 'outlined';
44+
/** Short caption naming what the chips are. */
45+
label?: React.ReactNode;
46+
/** Explanation of the row, shown when hovering its caption. */
47+
tooltip?: React.ReactNode;
48+
/**
49+
* Shown in place of the chips when there are none. Without it, an empty row
50+
* renders nothing at all.
51+
*/
52+
emptyLabel?: React.ReactNode;
1453
};
1554

16-
export const AppChips = ({ items, dataCy, color, variant }: AppChipsProps) => {
17-
if (items.length === 0) {
55+
const normalize = (item: string | AppChipItem): AppChipItem =>
56+
typeof item === 'string' ? { id: item, label: item } : item;
57+
58+
export const AppChips = ({
59+
items,
60+
dataCy,
61+
color,
62+
variant,
63+
label,
64+
tooltip,
65+
emptyLabel,
66+
}: AppChipsProps) => {
67+
if (items.length === 0 && !emptyLabel) {
1868
return null;
1969
}
70+
71+
const caption = label && (
72+
<Tooltip title={tooltip ?? ''} disableHoverListener={!tooltip}>
73+
<StyledLabel>{label}</StyledLabel>
74+
</Tooltip>
75+
);
76+
77+
if (items.length === 0) {
78+
return (
79+
<StyledRow>
80+
{caption}
81+
<Typography variant="caption" color="text.secondary" data-cy={dataCy}>
82+
{emptyLabel}
83+
</Typography>
84+
</StyledRow>
85+
);
86+
}
87+
2088
return (
21-
<StyledChips data-cy={dataCy}>
22-
{items.map((item) => (
23-
<Chip
24-
key={item}
25-
size="small"
26-
color={color}
27-
variant={variant}
28-
label={item}
29-
/>
30-
))}
31-
</StyledChips>
89+
<StyledRow>
90+
{caption}
91+
<StyledChips data-cy={dataCy}>
92+
{items.map(normalize).map((item) => (
93+
<Chip
94+
key={item.id}
95+
size="small"
96+
color={color}
97+
variant={variant}
98+
label={<StyledChipLabel>{item.label}</StyledChipLabel>}
99+
/>
100+
))}
101+
</StyledChips>
102+
</StyledRow>
32103
);
33104
};

webapp/src/views/projects/apps/AppIcon.tsx renamed to webapp/src/component/apps/AppIcon.tsx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,19 @@ const REGISTRY: Record<string, IconComponent> = {
1111
...(CustomIcons as unknown as Record<string, IconComponent>),
1212
};
1313

14+
/**
15+
* The size @untitled-ui/icons-react renders at when given no width/height. The emoji
16+
* fallback has to match it explicitly, otherwise an emoji icon comes out smaller than
17+
* a native one sitting next to it.
18+
*/
19+
const NATIVE_ICON_DEFAULT_SIZE = 24;
20+
1421
type Props = {
1522
/** Manifest icon string. Either a Tolgee native icon name or an emoji. */
1623
icon?: string | null;
17-
/** px size for native icons. */
24+
/** px size for the icon. Omit to render at the same size as any other native icon. */
1825
size?: number;
19-
/** CSS font-size for the emoji / text fallback. */
26+
/** CSS font-size for the emoji / text fallback. Defaults to `size`. */
2027
fontSize?: string | number;
2128
};
2229

@@ -27,16 +34,17 @@ type Props = {
2734
* Otherwise the string is rendered as text — preserving the emoji path
2835
* and producing a self-explanatory fallback for unknown names.
2936
*/
30-
export const AppIcon = ({ icon, size = 18, fontSize }: Props) => {
37+
export const AppIcon = ({ icon, size, fontSize }: Props) => {
3138
if (!icon) return null;
3239
const Component = REGISTRY[icon];
3340
if (typeof Component === 'function') {
41+
if (size === undefined) return <Component />;
3442
return <Component width={size} height={size} />;
3543
}
3644
return (
3745
<span
3846
style={{
39-
fontSize: fontSize ?? '1em',
47+
fontSize: fontSize ?? `${size ?? NATIVE_ICON_DEFAULT_SIZE}px`,
4048
lineHeight: 1,
4149
display: 'inline-flex',
4250
alignItems: 'center',

webapp/src/views/administration/apps/AdministrationApps.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,24 @@ export const AdministrationApps = () => {
175175
items={app.scopes}
176176
variant="outlined"
177177
dataCy="administration-apps-item-scopes"
178+
label={
179+
<T
180+
keyName="app_scopes_chips_label"
181+
defaultValue="Permissions"
182+
/>
183+
}
184+
tooltip={
185+
<T
186+
keyName="app_scopes_chips_tooltip"
187+
defaultValue="Permissions the app was granted when it was registered. They apply in every project the app is enabled for."
188+
/>
189+
}
190+
emptyLabel={
191+
<T
192+
keyName="app_scopes_chips_empty"
193+
defaultValue="No permissions granted"
194+
/>
195+
}
178196
/>
179197
{app.availableToAllOrganizations && (
180198
<Box>

webapp/src/views/organizations/apps/registeredApps/RegisteredAppsSection.tsx

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { RegisterAppDialog } from './RegisterAppDialog';
1919
import { RefreshAppDialog } from './RefreshAppDialog';
2020
import { AppSummary } from 'tg.component/apps/AppSummary';
2121
import { AppChips } from 'tg.component/apps/AppChips';
22+
import { AppIcon } from 'tg.component/apps/AppIcon';
2223

2324
type AppInstallModel = components['schemas']['AppInstallModel'];
2425

@@ -145,15 +146,54 @@ export const RegisteredAppsSection = () => {
145146
url={item.manifestUrl}
146147
/>
147148
<AppChips
148-
items={dashboardPages.map(
149-
(module) => `${module.icon} ${module.title}`
150-
)}
149+
items={dashboardPages.map((module) => ({
150+
id: module.key,
151+
label: (
152+
<>
153+
<AppIcon icon={module.icon} size={14} />
154+
{module.title}
155+
</>
156+
),
157+
}))}
151158
dataCy="organization-apps-item-modules"
159+
label={
160+
<T keyName="app_pages_chips_label" defaultValue="Pages" />
161+
}
162+
tooltip={
163+
<T
164+
keyName="app_pages_chips_tooltip"
165+
defaultValue="Pages this app adds to the project menu of every project it is enabled for."
166+
/>
167+
}
168+
emptyLabel={
169+
<T
170+
keyName="app_pages_chips_empty"
171+
defaultValue="Adds no pages"
172+
/>
173+
}
152174
/>
153175
<AppChips
154176
items={item.scopes}
155177
variant="outlined"
156178
dataCy="organization-apps-item-scopes"
179+
label={
180+
<T
181+
keyName="app_scopes_chips_label"
182+
defaultValue="Permissions"
183+
/>
184+
}
185+
tooltip={
186+
<T
187+
keyName="app_scopes_chips_tooltip"
188+
defaultValue="Permissions the app was granted when it was registered. They apply in every project the app is enabled for."
189+
/>
190+
}
191+
emptyLabel={
192+
<T
193+
keyName="app_scopes_chips_empty"
194+
defaultValue="No permissions granted"
195+
/>
196+
}
157197
/>
158198
</StyledItemMeta>
159199
<Box display="flex" gap={1}>

webapp/src/views/projects/projectMenu/ProjectMenu.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import { useAddProjectMenuItems } from 'tg.ee';
2525
import { useProject } from 'tg.hooks/useProject';
2626
import { useBranchLinks } from 'tg.component/branching/useBranchLinks';
2727
import { useApiQuery } from 'tg.service/http/useQueryApi';
28-
import { AppIcon } from '../apps/AppIcon';
28+
import { AppIcon } from 'tg.component/apps/AppIcon';
2929

3030
export const ProjectMenu = () => {
3131
const project = useProject();
@@ -208,7 +208,7 @@ export const ProjectMenu = () => {
208208
[PARAMS.APP_MODULE_KEY]: page.moduleKey,
209209
})}
210210
text={page.title}
211-
icon={<AppIcon icon={page.icon} fontSize="1em" />}
211+
icon={<AppIcon icon={page.icon} />}
212212
data-cy="project-menu-item-app"
213213
data-cy-app-install={page.installId}
214214
/>

0 commit comments

Comments
 (0)