Skip to content

Commit e6a9f40

Browse files
committed
feat: allow to customize icons in contributed actions
it's done by providing the icon font fixes podman-desktop#5562 Signed-off-by: Florent Benoit <[email protected]>
1 parent 479a7e0 commit e6a9f40

6 files changed

+153
-7
lines changed

packages/renderer/src/lib/actions/ContributionActions.spec.ts

+38
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,21 @@
1+
/**********************************************************************
2+
* Copyright (C) 2023-2024 Red Hat, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-License-Identifier: Apache-2.0
17+
***********************************************************************/
18+
119
import '@testing-library/jest-dom/vitest';
220
import { beforeAll, test, expect, vi } from 'vitest';
321
import { fireEvent, render, screen } from '@testing-library/svelte';
@@ -209,3 +227,23 @@ test('Expect when property to be true multiple args', async () => {
209227
const item = screen.getByText('dummy-title');
210228
expect(item).toBeInTheDocument();
211229
});
230+
231+
test('Expect custom icon on the contributed action', async () => {
232+
render(ContributionActions, {
233+
args: [],
234+
contributions: [
235+
{
236+
command: 'dummy.command',
237+
title: 'dummy-title',
238+
icon: '${dummyIcon}',
239+
},
240+
],
241+
onError: () => {},
242+
dropdownMenu: true,
243+
});
244+
245+
const iconItem = screen.getByRole('img', { name: 'dummy-title' });
246+
expect(iconItem).toBeInTheDocument();
247+
// expect to have the podman desktop icon class
248+
expect(iconItem).toHaveClass('podman-desktop-icon-dummyIcon');
249+
});

packages/renderer/src/lib/actions/ContributionActions.svelte

+17-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<script lang="ts">
22
import type { Menu } from '../../../../main/src/plugin/menu-registry';
33
import { faPlug } from '@fortawesome/free-solid-svg-icons';
4+
import type { IconDefinition } from '@fortawesome/fontawesome-common-types';
45
import ListItemButtonIcon from '../ui/ListItemButtonIcon.svelte';
56
import { removeNonSerializableProperties } from '/@/lib/actions/ActionUtils';
67
import type { ContextUI } from '/@/lib/context/context';
@@ -62,6 +63,21 @@ $: {
6263
}
6364
}
6465
66+
function getIcon(menu: Menu): IconDefinition | string {
67+
const defaultIcon = faPlug;
68+
if (!menu.icon) {
69+
return defaultIcon;
70+
}
71+
72+
const match = menu.icon.match(/\$\{(.*)\}/);
73+
if (match && match.length === 2) {
74+
const className = match[1];
75+
return menu.icon.replace(match[0], `podman-desktop-icon-${className}`);
76+
}
77+
console.error(`Invalid icon name: ${menu.icon}`);
78+
return defaultIcon;
79+
}
80+
6581
onDestroy(() => {
6682
// unsubscribe from the store
6783
if (contextsUnsubscribe) {
@@ -84,7 +100,7 @@ async function executeContribution(menu: Menu): Promise<void> {
84100
title="{menu.title}"
85101
onClick="{() => executeContribution(menu)}"
86102
menu="{dropdownMenu}"
87-
icon="{faPlug}"
103+
icon="{getIcon(menu)}"
88104
detailed="{detailed}"
89105
disabledWhen="{menu.disabled}"
90106
contextUI="{globalContext}" />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/**********************************************************************
2+
* Copyright (C) 2024 Red Hat, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-License-Identifier: Apache-2.0
17+
***********************************************************************/
18+
19+
import '@testing-library/jest-dom/vitest';
20+
import { test, expect } from 'vitest';
21+
import { render, screen } from '@testing-library/svelte';
22+
23+
import DropDownMenuItem from './DropDownMenuItem.svelte';
24+
import { faCircleUp } from '@fortawesome/free-solid-svg-icons';
25+
26+
test('Expect custom font icon on the contributed action', async () => {
27+
render(DropDownMenuItem, {
28+
title: 'dummy-title',
29+
icon: 'podman-desktop-icon-dummyIcon',
30+
});
31+
32+
const iconItem = screen.getByRole('img', { name: 'dummy-title' });
33+
expect(iconItem).toBeInTheDocument();
34+
// expect to have the podman desktop icon class
35+
expect(iconItem).toHaveClass('podman-desktop-icon-dummyIcon');
36+
});
37+
38+
test('Expect Font Awesome icon on the contributed action', async () => {
39+
render(DropDownMenuItem, {
40+
title: 'dummy-title',
41+
icon: faCircleUp,
42+
});
43+
44+
// grab the svg element
45+
const svgElement = screen.getByRole('img', { hidden: true });
46+
expect(svgElement).toBeInTheDocument();
47+
48+
// check it is a svelte-fa class
49+
expect(svgElement).toHaveClass('svelte-fa');
50+
});

packages/renderer/src/lib/ui/DropDownMenuItem.svelte

+7-3
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
<script lang="ts">
2-
import type { IconDefinition } from '@fortawesome/free-solid-svg-icons';
2+
import type { IconDefinition } from '@fortawesome/fontawesome-common-types';
33
import Fa from 'svelte-fa';
44
55
export let title: string;
6-
export let icon: IconDefinition;
6+
export let icon: IconDefinition | string;
77
export let enabled = true;
88
export let hidden = false;
99
export let onClick: () => void = () => {};
@@ -16,7 +16,11 @@ const disabledClasses = 'text-gray-900 bg-charcoal-800';
1616
<!-- Use a div + onclick so there's no "blind spots" for when clicking-->
1717
<div class="{`p-3 ${enabled ? enabledClasses : disabledClasses}`}" role="none" on:click="{onClick}">
1818
<span title="{title}" class="group flex items-center text-sm no-underline whitespace-nowrap" tabindex="-1">
19-
<Fa class="h-4 w-4 text-md" icon="{icon}" />
19+
{#if typeof icon === 'string'}
20+
<span role="img" aria-label="{title}" class="{icon} h-4 w-4"></span>
21+
{:else}
22+
<Fa class="h-4 w-4 text-md" icon="{icon}" />
23+
{/if}
2024
{#if title}<span class="ml-2">{title}</span>{/if}
2125
</span>
2226
</div>

packages/renderer/src/lib/ui/ListItemButtonIcon.spec.ts

+39-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import '@testing-library/jest-dom/vitest';
2020
import { test, expect, vi } from 'vitest';
2121
import { fireEvent, render, screen } from '@testing-library/svelte';
2222
import ListItemButtonIcon from './ListItemButtonIcon.svelte';
23-
import { faRocket } from '@fortawesome/free-solid-svg-icons';
23+
import { faCircleCheck, faRocket } from '@fortawesome/free-solid-svg-icons';
2424
import { ContextUI } from '../context/context';
2525
import { context } from '/@/stores/context';
2626

@@ -158,3 +158,41 @@ test('With confirmation=no, there will be no confirmation when clicking the butt
158158
await fireEvent.click(button);
159159
expect(showMessageBoxMock).not.toHaveBeenCalled();
160160
});
161+
162+
test('With custom font icon', async () => {
163+
const title = 'Dummy item';
164+
165+
render(ListItemButtonIcon, {
166+
title,
167+
icon: 'podman-desktop-icon-dummyIcon',
168+
menu: true,
169+
confirm: false,
170+
enabled: true,
171+
inProgress: false,
172+
});
173+
174+
const iconItem = screen.getByRole('img', { name: title });
175+
expect(iconItem).toBeInTheDocument();
176+
// expect to have the podman desktop icon class
177+
expect(iconItem).toHaveClass('podman-desktop-icon-dummyIcon');
178+
});
179+
180+
test('With custom Fa icon', async () => {
181+
const title = 'Dummy item';
182+
183+
render(ListItemButtonIcon, {
184+
title,
185+
icon: faCircleCheck,
186+
menu: true,
187+
confirm: false,
188+
enabled: true,
189+
inProgress: false,
190+
});
191+
192+
// grab the svg element
193+
const svgElement = screen.getByRole('img', { hidden: true });
194+
expect(svgElement).toBeInTheDocument();
195+
196+
// check it is a svelte-fa class
197+
expect(svgElement).toHaveClass('svelte-fa');
198+
});

packages/renderer/src/lib/ui/ListItemButtonIcon.svelte

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script lang="ts">
2-
import type { IconDefinition } from '@fortawesome/free-solid-svg-icons';
2+
import type { IconDefinition } from '@fortawesome/fontawesome-common-types';
33
import DropdownMenuItem from './DropDownMenuItem.svelte';
44
import Fa from 'svelte-fa';
55
import { onDestroy } from 'svelte';
@@ -9,7 +9,7 @@ import { context as storeContext } from '/@/stores/context';
99
import { ContextKeyExpr } from '../context/contextKey';
1010
1111
export let title: string;
12-
export let icon: IconDefinition;
12+
export let icon: IconDefinition | string;
1313
export let hidden = false;
1414
export let disabledWhen = '';
1515
export let enabled: boolean = true;

0 commit comments

Comments
 (0)