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
6 changes: 6 additions & 0 deletions workspaces/homepage/.changeset/yellow-rings-decide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@red-hat-developer-hub/backstage-plugin-homepage': patch
---

fix missing translations for homepage card widgets
lazy load HomePageLayout
18 changes: 13 additions & 5 deletions workspaces/homepage/e2e-tests/homepageCustomizable.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@ import { test, expect, BrowserContext, Page } from '@playwright/test';
import { TestUtils } from './utils/testUtils.js';
import { HomePageCustomization } from './pages/homePageCustomization.js';
import { runAccessibilityTests } from './utils/accessibility.js';
import { getTranslations } from './utils/translations.js';

test.describe.serial('Dynamic Home Page Customization', () => {
let testUtils: TestUtils;
let homePageCustomization: HomePageCustomization;
let sharedPage: Page;
let sharedContext: BrowserContext;
const translations = getTranslations('en');

test.beforeAll(async ({ browser }) => {
sharedContext = await browser.newContext();
Expand Down Expand Up @@ -80,13 +82,15 @@ test.describe.serial('Dynamic Home Page Customization', () => {
});

test('Verify Add Widget Button Adds Cards', async () => {
await homePageCustomization.addWidget('Onboarding');
await homePageCustomization.addWidget(translations.onboarding.title);
await expect(
sharedPage.getByText(/Good (morning|afternoon|evening)/),
).toBeVisible();

await homePageCustomization.addWidget('Quick Access');
await expect(sharedPage.getByText('Quick Access')).toBeVisible();
await homePageCustomization.addWidget(translations.quickAccess.title);
await expect(
sharedPage.getByText(translations.quickAccess.title),
).toBeVisible();
});

// ── Persistent storage ────────────────────────────────────────────────
Expand All @@ -103,7 +107,9 @@ test.describe.serial('Dynamic Home Page Customization', () => {
await homePageCustomization.verifyCardHidden(
'Good (morning|afternoon|evening)',
);
await homePageCustomization.verifyCardVisible('Quick Access');
await homePageCustomization.verifyCardVisible(
translations.quickAccess.title,
);
const countAfterReload =
await homePageCustomization.getVisibleCardCount();
expect(countAfterReload).toBe(countBeforeReload);
Expand All @@ -120,7 +126,9 @@ test.describe.serial('Dynamic Home Page Customization', () => {
await homePageCustomization.verifyCardHidden(
'Good (morning|afternoon|evening)',
);
await homePageCustomization.verifyCardVisible('Quick Access');
await homePageCustomization.verifyCardVisible(
translations.quickAccess.title,
);
const countAfterLogout =
await homePageCustomization.getVisibleCardCount();
expect(countAfterLogout).toBe(countBeforeLogout);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ export class HomePageCustomization {
await this.page.waitForTimeout(1000); // Wait for dialog to open

// Select the specific widget type from the dialog
await this.page.getByRole('button', { name: title }).click();
await this.page.getByRole('button', { name: title, exact: true }).click();
await this.page.waitForTimeout(1000);
}

Expand Down
12 changes: 9 additions & 3 deletions workspaces/homepage/packages/app-legacy/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,11 @@ import {
TemplateSection,
SearchBar,
Headline,
// Markdown,
// MarkdownCard,
// Placeholder,
CatalogStarredEntitiesCard,
RecentlyVisitedCard,
TopVisitedCard,
FeaturedDocsCard,
JokeCard,
WorldClock,
HomePageCardMountPoint,
} from '@red-hat-developer-hub/backstage-plugin-homepage';
Expand Down Expand Up @@ -346,6 +344,14 @@ const cardMountPoints: HomePageCardMountPoint[] = [
titleKey: 'featuredDocs.title',
},
},
{
Component: JokeCard as ComponentType,
config: {
id: 'joke-card',
titleKey: 'randomJoke.title',
descriptionKey: 'randomJoke.description',
},
},
{
Component: WorldClock as ComponentType,
config: {
Expand Down
21 changes: 17 additions & 4 deletions workspaces/homepage/plugins/homepage/app-config.dynamic.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ dynamicPlugins:
- mountPoint: home.page/widgets
importName: RecentlyVisitedCard
config:
title: 'Recently Visited'
description: 'Quick access to recently viewed entities and pages'
titleKey: recentlyVisited.title
descriptionKey: recentlyVisited.description
priority: 2
layouts:
xl: { w: 4, h: 3 }
Expand All @@ -55,8 +55,8 @@ dynamicPlugins:
- mountPoint: home.page/widgets
importName: TopVisitedCard
config:
title: 'Most Visited'
description: 'Your most frequently accessed entities and services'
titleKey: topVisited.title
descriptionKey: topVisited.description
priority: 1
layouts:
xl: { w: 4, h: 3 }
Expand All @@ -65,3 +65,16 @@ dynamicPlugins:
sm: { w: 12, h: 3 }
xs: { w: 12, h: 3 }
xxs: { w: 12, h: 3 }
- mountPoint: home.page/widgets
importName: JokeCard
config:
titleKey: randomJoke.title
descriptionKey: randomJoke.description
priority: 3
layouts:
xl: { w: 4, h: 4 }
lg: { w: 4, h: 4 }
md: { w: 6, h: 4 }
sm: { w: 12, h: 4 }
xs: { w: 12, h: 4 }
xxs: { w: 12, h: 4 }
3 changes: 2 additions & 1 deletion workspaces/homepage/plugins/homepage/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@
"@red-hat-developer-hub/backstage-plugin-homepage-common": "workspace:^",
"@scalprum/react-core": "0.11.3",
"react-grid-layout": "1.5.3",
"react-use": "17.6.1"
"react-use": "17.6.1",
"zod": "^4.0.0"
},
"peerDependencies": {
"react": "16.13.1 || ^17.0.0 || ^18.2.0",
Expand Down
9 changes: 8 additions & 1 deletion workspaces/homepage/plugins/homepage/report-alpha.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ export const homepageTranslationRef: TranslationRef<
readonly 'header.local': string;
readonly 'header.welcome': string;
readonly 'header.welcomePersonalized': string;
readonly 'search.title': string;
readonly 'search.placeholder': string;
readonly 'search.clearButton': string;
readonly 'homePage.empty': string;
readonly 'quickAccess.title': string;
readonly 'quickAccess.error': string;
Expand All @@ -26,14 +28,19 @@ export const homepageTranslationRef: TranslationRef<
readonly 'featuredDocs.learnMore': string;
readonly 'starredEntities.title': string;
readonly 'recentlyVisited.title': string;
readonly 'recentlyVisited.description': string;
readonly 'topVisited.title': string;
readonly 'topVisited.description': string;
readonly 'randomJoke.title': string;
readonly 'randomJoke.description': string;
readonly 'templates.title': string;
readonly 'templates.error': string;
readonly 'templates.empty': string;
readonly 'templates.fetchError': string;
readonly 'templates.emptyDescription': string;
readonly 'templates.register': string;
readonly 'templates.viewAll': string;
readonly 'onboarding.title': string;
readonly 'onboarding.guest': string;
readonly 'onboarding.greeting.goodMorning': string;
readonly 'onboarding.greeting.goodAfternoon': string;
Expand All @@ -55,9 +62,9 @@ export const homepageTranslationRef: TranslationRef<
readonly 'entities.close': string;
readonly 'entities.empty': string;
readonly 'entities.fetchError': string;
readonly 'entities.description': string;
readonly 'entities.emptyDescription': string;
readonly 'entities.register': string;
readonly 'entities.description': string;
readonly 'entities.browseTheCatalog': string;
}
>;
Expand Down
16 changes: 16 additions & 0 deletions workspaces/homepage/plugins/homepage/report.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,14 @@ export interface HomePageCardMountPointConfig {
titleKey?: string;
}

// @public (undocumented)
export const JokeCard: (props: JokeCardProps) => JSX_2.Element;

// @public (undocumented)
export type JokeCardProps = TranslatableCardTitleProps & {
defaultCategory?: 'any' | 'programming';
};

// @public (undocumented)
export interface Layout {
// (undocumented)
Expand Down Expand Up @@ -242,6 +250,14 @@ export const TemplateSection: () => JSX_2.Element;
// @public (undocumented)
export const TopVisitedCard: ComponentType<VisitedByTypeProps>;

// @public
export interface TranslatableCardTitleProps {
// (undocumented)
title?: string;
// (undocumented)
titleKey?: string;
}

// @public (undocumented)
export const VisitListener: () => JSX_2.Element | null;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* Copyright Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { HomePageLayoutProps } from '@backstage/plugin-home-react/alpha';

import { HomePageCardConfig } from '../../types';
import { HomePageLayout } from './HomePageLayout';

/**
* Widget layout configuration keyed by widget name.
* @alpha
*/
export type WidgetLayoutConfig = Record<
string,
{
priority?: number;
breakpoints?: Record<
string,
{ w?: number; h?: number; x?: number; y?: number }
>;
}
>;

/**
* Options for creating a config-driven home page layout component.
* @alpha
*/
export interface CustomHomePageLayoutOptions {
customizable: boolean;
layoutConfig: WidgetLayoutConfig;
}

/**
* Creates a home page layout component that applies widget layout config
* (priority, breakpoints) before rendering the grid.
*
* @alpha
*/
export function createCustomHomePageLayout({
customizable,
layoutConfig,
}: CustomHomePageLayoutOptions) {
return function CustomHomePageLayout({ widgets }: HomePageLayoutProps) {
const processedWidgets: HomePageCardConfig[] = widgets
.map(widget => {
const widgetConfig = layoutConfig[widget.name ?? ''];
const configBreakpoints = widgetConfig?.breakpoints;

if (!configBreakpoints) return widget;

return {
...widget,
breakpointLayouts: configBreakpoints,
};
})
.sort((a, b) => {
if (customizable) return 0;

const priorityA = layoutConfig[a.name ?? '']?.priority ?? 0;
const priorityB = layoutConfig[b.name ?? '']?.priority ?? 0;
return priorityB - priorityA;
});

return (
<HomePageLayout widgets={processedWidgets} customizable={customizable} />
);
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ import GlobalStyles from '@mui/material/GlobalStyles';
import { cardWrapperSx } from '../../styles/cardWrapperSx';
import { HomePageCardConfig } from '../../types';
import { useContainerQuery } from '../../hooks/useContainerQuery';
import { useTranslation } from '../../hooks/useTranslation';
import { translateHomepageWidget } from '../../utils/translateHomepageWidgets';

import 'react-grid-layout/css/styles.css';
import { isCardADefaultConfiguration } from '../utils';
Expand All @@ -53,13 +55,19 @@ export const CustomizableGridLayout = ({
homepageCards,
}: CustomizableGridLayoutProps) => {
const theme = useTheme();
const { t } = useTranslation();
const gridContainerRef = useRef<HTMLDivElement>(null);
useContainerQuery(gridContainerRef, { notifyWindowResize: true });

const translatedHomepageCards = useMemo(
() => homepageCards.map(card => translateHomepageWidget(card, t)),
[homepageCards, t],
);

const config = useMemo(() => {
const defaultConfig: LayoutConfiguration[] = [];

homepageCards.forEach(homepageCard => {
translatedHomepageCards.forEach(homepageCard => {
if (!homepageCard.node) {
return;
}
Expand All @@ -81,7 +89,7 @@ export const CustomizableGridLayout = ({
});

return defaultConfig;
}, [homepageCards]);
}, [translatedHomepageCards]);

return (
<>
Expand All @@ -106,7 +114,7 @@ export const CustomizableGridLayout = ({
compactType="vertical"
style={{ margin: '-10px' }}
>
{homepageCards.map((card, index) => (
{translatedHomepageCards.map((card, index) => (
<Fragment key={card.name ?? index}>{card.component}</Fragment>
))}
</CustomHomepageGrid>
Expand Down
Loading
Loading